#!/usr/bin/env python3 """BGMountains v0.95 native OsmAnd pipeline: only TWO editable mapping files. Never needs extracted Garmin icons or custom styles on Android. """ from __future__ import annotations 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' def run(*args,log=None,cwd=None,env=None): cmd=[str(x) for x in args];print('+',' '.join(cmd),flush=True) if log: 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) 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 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 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. """ gib=1024**3 free=shutil.disk_usage(out).free required=max(int(2*gib),int(source_bytes*5 + gib),int(extra_gb*gib)) if free64: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.') return 0 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) return 0 if __name__=='__main__':sys.exit(main())