Files

160 lines
9.7 KiB
Python
Raw Permalink Normal View History

2026-10-14 17:04:46 +00:00
#!/usr/bin/env python3
2026-10-19 08:52:24 +00:00
"""BGMountains v0.95 native OsmAnd pipeline: only TWO editable mapping files.
2026-10-14 17:04:46 +00:00
2026-10-19 08:52:24 +00:00
Never needs extracted Garmin icons or custom styles on Android.
2026-10-14 17:04:46 +00:00
"""
from __future__ import annotations
2026-10-19 08:52:24 +00:00
import argparse,datetime,hashlib,json,os,pathlib,shutil,subprocess,sys,time,urllib.request,zipfile
ROOT=pathlib.Path(__file__).resolve().parents[1]
TRANSLATOR=ROOT/'scripts/native_translate.py'
2026-10-14 17:04:46 +00:00
2026-10-19 08:52:24 +00:00
def run(*args,log=None,cwd=None,env=None):
cmd=[str(x) for x in args];print('+',' '.join(cmd),flush=True)
2026-10-14 17:04:46 +00:00
if log:
2026-10-19 08:52:24 +00:00
with open(log,'w',encoding='utf-8') as f:
p=subprocess.run(cmd,stdout=f,stderr=subprocess.STDOUT,cwd=cwd,env=env)
if p.returncode:
print(''.join(pathlib.Path(log).read_text(errors='replace').splitlines(True)[-40:]),file=sys.stderr)
raise RuntimeError('Command failed, see '+str(log))
else:subprocess.run(cmd,check=True,cwd=cwd,env=env)
2026-10-14 17:04:46 +00:00
2026-10-19 08:52:24 +00:00
def creator_dir(x):
if not x:return None
p=pathlib.Path(x).expanduser().resolve();p=p.parent if p.name=='utilities.sh' else p
if not (p/'utilities.sh').exists():raise FileNotFoundError('MapCreator utilities.sh missing: '+str(p))
return p
2026-10-14 17:04:46 +00:00
2026-10-19 08:52:24 +00:00
def fetch_creator(target):
extracted=target/'unpacked';c=list(extracted.rglob('utilities.sh')) if extracted.exists() else []
if c:return c[0].parent
target.mkdir(parents=True,exist_ok=True)
url='https://download.osmand.net/latest-night-build/OsmAndMapCreator-main.zip'
zip_path=target/'OsmAndMapCreator-main.zip'
if not zip_path.exists():
print('Fetching official MapCreator:',url)
with urllib.request.urlopen(url,timeout=120) as src,open(zip_path,'wb') as dst:shutil.copyfileobj(src,dst)
with zipfile.ZipFile(zip_path) as z:
for entry in z.infolist():
parts=pathlib.PurePosixPath(entry.filename)
if parts.is_absolute() or '..' in parts.parts:raise ValueError('Unsafe creator ZIP entry')
z.extractall(extracted)
c=list(extracted.rglob('utilities.sh'))
if not c:raise FileNotFoundError('Creator ZIP lacks utilities.sh')
for f in extracted.rglob('*.sh'):f.chmod(f.stat().st_mode|0o111)
return c[0].parent
2026-10-14 17:04:46 +00:00
2026-10-19 08:52:24 +00:00
def disk_preflight(out, source_bytes, extra_gb=0):
"""Conservative warning/blocking check before disk-heavy MapCreator stage.
In experience 6 GiB OSM has required >25 GiB scratch. 5x + 1 GiB
is intentionally approximate, not a guarantee of completion.
2026-10-14 17:04:46 +00:00
"""
2026-10-19 08:52:24 +00:00
gib=1024**3
free=shutil.disk_usage(out).free
required=max(int(2*gib),int(source_bytes*5 + gib),int(extra_gb*gib))
if free<required:
raise RuntimeError(f'Insufficient free space for OBF build: free={free/gib:.2f} GiB; '
f'estimated minimum={required/gib:.2f} GiB. '
'Use --prepare-only or set --ignore-disk-check if scratch resides on another filesystem.')
print(f'Disk preflight OK: free={free/gib:.1f} GiB, estimated minimum={required/gib:.1f} GiB',flush=True)
2026-10-14 17:04:46 +00:00
def main():
p=argparse.ArgumentParser(description=__doc__,formatter_class=argparse.ArgumentDefaultsHelpFormatter)
2026-10-19 08:52:24 +00:00
p.add_argument('--img',help='Original Garmin IMG')
p.add_argument('--osm',help='Already generated original Garmin-derived raw OSM')
p.add_argument('--out',default='build/bgmountains-v095')
p.add_argument('--poi-config',default=str(ROOT/'config/BGMountains-native-poi-mapping-v094.csv'))
p.add_argument('--line-config',default=str(ROOT/'config/line-mapping.csv'))
p.add_argument('--jobs',type=int,default=1,help='IMG workers: default 1; explicitly set N to parallelize')
p.add_argument('--bbox',help='W,S,E,N region for smoke tests')
p.add_argument('--template',action='store_true',help='Generate blank new-map mapping templates using this IMG (not BGMountains defaults)')
p.add_argument('--attach-routes',action='store_true',help='Opt in to high-memory matching of trail overlays to physical paths (not recommended on full map)')
p.add_argument('--no-route-attachment',action='store_true',help='Deprecated: attachment is disabled by default')
p.add_argument('--prepare-only',action='store_true',help='Extract/translate/validate, do not compile OBF')
p.add_argument('--creator',help='Existing OsmAndMapCreator directory containing utilities.sh')
p.add_argument('--download-creator',action='store_true',help='Obtain official OsmAndMapCreator from download.osmand.net')
p.add_argument('--java-opts',default='-Xms512M -Xmx8192M')
p.add_argument('--keep-raw',action='store_true',help='Keep pre-translation OSM for debugging')
p.add_argument('--ignore-disk-check',action='store_true',help='Override conservative OBF scratch space safety check')
p.add_argument('--disk-headroom-gb',type=float,default=0,help='Minimum free GiB required before Java OBF build (0=auto estimate)')
a=p.parse_args()
if bool(a.img)==bool(a.osm):p.error('Provide exactly one of --img or --osm')
if a.jobs<1 or a.jobs>64:p.error('--jobs must be 1..64')
if not a.template:
for typ,pth in (('POI',a.poi_config),('line',a.line_config)):
if not pathlib.Path(pth).is_file():p.error(f'{typ} mapping file not found: {pth}')
out=pathlib.Path(a.out).resolve();out.mkdir(parents=True,exist_ok=True)
src=pathlib.Path(a.osm).resolve() if a.osm else None
poi=pathlib.Path(a.poi_config).resolve();line=pathlib.Path(a.line_config).resolve()
# Resolve MapCreator before running an expensive full-country extraction.
# Missing download/network credentials must fail before consuming gigabytes.
creator=None
if not a.prepare_only and not a.template:
creator=creator_dir(a.creator or os.environ.get('OSMAND_MAPCREATOR'))
if not creator and a.download_creator:
creator=fetch_creator(ROOT/'build'/'mapcreator')
if not creator:
p.error('OBF requires --creator DIR or --download-creator; for testing use --prepare-only.')
bin_path=ROOT/'build'/'garmin2osm'
if a.img:
run('cmake','-S',ROOT,'-B',ROOT/'build','-DCMAKE_BUILD_TYPE=Release')
run('cmake','--build',ROOT/'build','-j','2','--target','garmin2osm')
src=out/'IMG-raw.osm'
cmd=[str(bin_path),'--input',str(pathlib.Path(a.img).resolve()),'--output',str(src),
'--poi-types-out',str(out/'discovered-pois.csv'),'--poi-manifest',str(out/'source-pois.csv'),
'--jobs',str(a.jobs)]
if a.bbox:cmd+=['--bbox',a.bbox]
if not a.template:
run(sys.executable,TRANSLATOR,'prepare','--poi',poi,'--lines',line,'--out',out)
cmd+=['--poi-overrides',str(out/'cpp-poi-override.csv'),'--poi-labels',str(out/'cpp-name-labels.csv')]
run(*cmd,log=out/'extract.log')
if a.template:
if not a.img:p.error('--template requires --img')
run(sys.executable,TRANSLATOR,'template','--poi',out/'poi-mapping.TEMPLATE.csv',
'--lines',out/'line-mapping.TEMPLATE.csv','--catalog',out/'discovered-pois.csv','--osm',src)
print('Templates created. Review both files before using them for native OSM conversion.')
2026-10-14 17:04:46 +00:00
return 0
2026-10-19 08:52:24 +00:00
dst=out/'BGMountains-native.osm'
run(sys.executable,TRANSLATOR,'transform','--poi',poi,'--lines',line,'--osm',src,
'--out',dst,'--report',out/'route-report.json',log=out/'translate.log')
if a.attach_routes and not a.no_route_attachment:
attached=out/'BGMountains-native-attached.osm'
run(sys.executable,ROOT/'scripts/attach_native_routes.py','--osm',dst,'--out',attached,
'--lines',line,'--report',out/'route-attachment.json',log=out/'route-attachment.log')
dst.unlink();attached.rename(dst)
run(sys.executable,TRANSLATOR,'audit','--poi',poi,'--lines',line,'--osm',dst,
'--out',out/'semantic-audit.json',log=out/'audit.log')
# Fail closed if the translated file is not well-formed XML, including relation refs.
import xml.etree.ElementTree as ET
from native_translate import top_level_elements
n=0
for e in top_level_elements(dst):
if e.tag=='relation':n+=1
if not a.keep_raw and a.img:src.unlink(missing_ok=True)
if a.prepare_only:
print('PREPARED OSM:',dst,'route relations:',n,'No OBF compiled.')
return 0
if not a.ignore_disk_check:disk_preflight(out,dst.stat().st_size,a.disk_headroom_gb)
env=os.environ.copy();env['JAVA_OPTS']=a.java_opts
# utilities.sh versions differ in whether JAVA_OPTS is honored; this flag is
# explicitly recognized by the JVM. It also reaches spawned Java processes.
env['JAVA_TOOL_OPTIONS']=a.java_opts
# Native types are built into OsmAnd: NO --poi-types/--rendering-types override,
# NO custom .render.xml and NO extracted Garmin icons.
started=time.time()
run(creator/'utilities.sh','generate-obf-no-address',dst,log=out/'mapcreator.log',cwd=out,env=env)
obfs=[f for f in out.glob('*.obf') if f.stat().st_mtime >= started-2 and f.stat().st_size>0]
if not obfs:raise RuntimeError('Creator returned no OBF in '+str(out))
obf=max(obfs,key=lambda x:x.stat().st_mtime)
if (creator/'inspector.sh').exists():
run(creator/'inspector.sh','-vpoi',obf,log=out/'obf-inspection.txt',cwd=out,env=env)
(out/'build-manifest.json').write_text(json.dumps({
'version':'v0.95','input':a.img or a.osm,'poi_config':str(poi),'line_config':str(line),
'poi_config_sha256':hashlib.sha256(poi.read_bytes()).hexdigest(),
'line_config_sha256':hashlib.sha256(line.read_bytes()).hexdigest(),
'output_obf':str(obf),'obf_bytes':obf.stat().st_size,
'compiled_at':datetime.datetime.now(datetime.timezone.utc).isoformat(),
'notice':'Native stock rendering. POI display zoom controlled by OsmAnd; no claim that all tags get icons.'},ensure_ascii=False,indent=2)+'\n')
print('BUILT OBF:',obf)
2026-10-14 17:04:46 +00:00
return 0
2026-10-19 08:52:24 +00:00
if __name__=='__main__':sys.exit(main())