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 ())