This commit is contained in:
nq
2026-10-14 17:57:55 +00:00
parent fe9e9b593e
commit 3211170ca5
21 changed files with 3529 additions and 635 deletions
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""Audit whether IMG-derived POIs have both OSM display tags and names.
This measures converted OSM, NOT actual OsmAnd screen visibility nor .obf indexing.
Output inventory always includes Garmin codes and example coordinates.
"""
import argparse,csv,gzip,json
from pathlib import Path
from collections import Counter,defaultdict
from xml.etree.ElementTree import iterparse
CLASS_KEYS=('amenity','tourism','historic','shop','emergency','leisure','natural','man_made','place','highway','landuse','power','waterway','aerialway','industrial','barrier','sport','building')
def main():
a=argparse.ArgumentParser();a.add_argument('--osm',required=True);a.add_argument('--catalog',required=True);a.add_argument('--csv',required=True);a.add_argument('--json',required=True);a.add_argument('--samples',type=int,default=5);args=a.parse_args()
with open(args.catalog,encoding='utf-8-sig',newline='') as f:catalog={r['code'].lower():r for r in csv.DictReader(f)}
stats=defaultdict(Counter); samples=defaultdict(list); counts=Counter(); bad=[]; overview=Counter()
opener=gzip.open if args.osm.endswith('.gz') else open
with opener(args.osm,'rb') as f:
for _,el in iterparse(f,events=('end',)):
if el.tag not in ('node','way','relation'):continue
if el.tag!='node':el.clear();continue
tags={x.get('k'):x.get('v') for x in el.findall('tag') if x.get('k') is not None}
if tags.get('garmin:feature')!='poi':el.clear();continue
code=tags.get('garmin:type','').lower();counts[code]+=1;overview['pois']+=1
name=tags.get('name','');kind=[f'{k}={tags[k]}' for k in CLASS_KEYS if k in tags]
if not kind:bad.append((code,name,el.get('lat'),el.get('lon'),'no_osm_semantic_tag'));overview['unclassified']+=1
if not name:overview['nameless']+=1
if tags.get('bgm:generated_name')=='yes':stats[code]['generated_name']+=1;overview['generated_name']+=1
if tags.get('bgm:synthetic_icon')=='yes':stats[code]['synthetic_icon']+=1;overview['synthetic_icon']+=1
if not tags.get('bgm_poi'):overview['missing_raw_type_index_tag']+=1
if len(samples[code])<args.samples:samples[code].append((el.get('lat'),el.get('lon'),name, '|'.join(kind)))
el.clear()
rows=[]
for code in sorted(counts,key=lambda x:(-counts[x],x)):
base=catalog.get(code,{})
row=dict(code=code,osm_poi_count=counts[code],nationwide_type_count=base.get('count',''),
icon_strategy=('synthetic' if stats[code]['synthetic_icon'] else 'native-osm'),
synthetic_count=stats[code]['synthetic_icon'],generated_name_count=stats[code]['generated_name'],
legend=base.get('verified_legend_bg',''),review_status=base.get('review_status',''),
example_lat=samples[code][0][0],example_lon=samples[code][0][1],
example_name=samples[code][0][2],example_osm_tags=samples[code][0][3],
osm_coord_link=f"https://www.openstreetmap.org/?mlat={samples[code][0][0]}&mlon={samples[code][0][1]}#map=18/{samples[code][0][0]}/{samples[code][0][1]}")
rows.append(row)
path=Path(args.csv);path.parent.mkdir(parents=True,exist_ok=True)
with path.open('w',encoding='utf-8',newline='') as f:
if rows:w=csv.DictWriter(f,fieldnames=list(rows[0]));w.writeheader();w.writerows(rows)
summary=dict(status='PASS' if not bad and not overview['nameless'] and not overview['missing_raw_type_index_tag'] else 'FAIL',
counts=dict(overview),catalog_types=len(catalog),exported_types=len(counts),
note='Audit is for OSM tags only. A real .obf inspector comparison and OsmAnd visual inspection remain required.',
errors=bad[:100],missing_types_from_catalog=sorted(set(counts)-set(catalog)))
Path(args.json).write_text(json.dumps(summary,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
print(json.dumps(summary,ensure_ascii=False,indent=2))
return 0 if summary['status']=='PASS' and not summary['missing_types_from_catalog'] else 2
if __name__=='__main__':raise SystemExit(main())
+143
View File
@@ -0,0 +1,143 @@
#!/usr/bin/env python3
"""Rebuild a local-OsmAnd display profile from historical BGMountains CSV + reviewed inventory.
Tagging in this file is NOT approved for OpenStreetMap database import:
uncertain features use an explicit, synthetic tourism=information fallback.
"""
import csv
from pathlib import Path
import argparse
# Additional interpretations backed by Garmin legend or characteristic named samples.
# Uncertain ones are kept descriptive only, with an explicitly synthetic display icon.
INTERPRET = {
'0x6600':('bgm:spot_height=yes','Височинна кота','legend-ambiguous'),
'0x6500':('man_made=water_intake','Вода - Каптаж','legend'),
'0x6614':('natural=rock','Скала','legend'),
'0x650b':('trailblazed=yes','Зимна маркировка','legend'),
'0x660b':('trailblazed=yes','Туристическа маркировка','legend'),
'0x1100':('place=locality','Населено място','uncertain'),
'0xd00':('place=locality','Селище','uncertain'),
'0xe00':('place=neighbourhood','Квартал','uncertain'),
'0x640f':('landuse=cemetery','Гробище','legend'),
'0x6403':('landuse=cemetery','Гробище','legend'),
'0x640a':('hazard=yes','Опасност','legend'),
'0x6502':('ford=yes','Речен брод','legend'),
'0x2f0b':('leisure=track','Тренировъчна площадка','inferred'),
'0x661d':('building=agricultural','Стопанска постройка','inferred'),
'0x2c08':('leisure=horse_riding','Конна база','inferred'),
'0x2f00':('landuse=industrial','Промишлен обект','inferred'),
'0x661c':('bgm:landmark=mound','Могила','uncertain'),
'0x3003':('amenity=townhall','Кметство','inferred'),
'0x2f01':('amenity=fuel','Бензиностанция','inferred'),
'0x3002':('emergency=first_aid','Първа помощ','inferred'),
'0x2c02':('amenity=kindergarten','Детска градина','inferred'),
'0x2c03':('amenity=community_centre','Читалище','inferred'),
'0x2e02':('shop=convenience','Хранителен магазин','inferred'),
'0x2e03':('shop=yes','Магазин','inferred'),
'0x2e06':('shop=yes','Търговски обект','uncertain'),
'0x2f06':('amenity=bank','Банка','inferred'),
'0x6418':('power=generator;generator:source=wind','Вятърен генератор','inferred'),
'0x6419':('power=generator;generator:source=solar','Фотоволтаична централа','inferred'),
'0x6405':('building=hut','Ловна хижа','legend'),
'0x2e0a':('shop=car','Автокъща','inferred'),
'0x2f03':('shop=car_repair','Автосервиз','inferred'),
'0x3007':('man_made=pumping_station','Помпена станция','inferred'),
'0x6601':('bgm:landmark=natural_phenomenon','Природен феномен','legend'),
'0x6503':('bgm:trail_junction=yes','Кръстопът','legend'),
'0x2a07':('bgm:landmark=unknown','Спортен обект','uncertain'),
'0x2d02':('bgm:landmark=unknown','Търговски обект','uncertain'),
'0x2a0e':('shop=yes','Търговски обект','uncertain'),
'0x2f08':('bgm:landmark=unknown','Обществен обект','uncertain'),
'0x4700':('industrial=port','Пристанище','legend'),
'0x640d':('aerialway=station','Лифт','legend'),
'0x6402':('building=yes','Сграда','legend'),
'0x6406':('barrier=gate','Бариера','legend'),
'0x6401':('man_made=bridge','Мост','legend'),
'0x6501':('amenity=restaurant','Ресторант','legend'),
'0x640c':('man_made=mine_shaft','Мина','legend'),
'0x6602':('tourism=viewpoint','Панорама - Гледка','legend'),
'0x6511':('natural=spring','Вода - Извор','legend'),
'0x6414':('amenity=drinking_water','Вода - Чешма','legend'),
'0x6417':('man_made=water_well','Вода - Кладенец','legend'),
'0x6616':('natural=peak','Връх','verified'),
'0x6617':('natural=peak','Връх','legend'),
'0x6618':('natural=tree','Дърво','legend'),
'0x6603':('man_made=survey_point','Триангулачна точка','legend'),
}
# Primary categories with stock OsmAnd POI definitions/icons in normal map builds.
STOCK_PREFIXES={'amenity','tourism','historic','shop','emergency','leisure','natural','man_made'}
STOCK_NATURAL={'peak','spring','tree','cave_entrance','saddle'}
STOCK_MAN_MADE={'water_well','survey_point','tower'}
STOCK_LEISURE={'picnic_table','swimming_pool','park','ice_rink','horse_riding'}
STOCK_EMERGENCY={'mountain_rescue','first_aid'}
def tagdict(s):return dict(item.split('=',1) for item in s.split(';') if '=' in item)
def tags_text(d):return ';'.join(f'{k}={v}' for k,v in d.items())
def has_standard_icon(tags):
if 'amenity' in tags or 'tourism' in tags or 'shop' in tags or 'historic' in tags:return True
if tags.get('natural') in STOCK_NATURAL:return True
if tags.get('man_made') in STOCK_MAN_MADE:return True
if tags.get('leisure') in STOCK_LEISURE:return True
if tags.get('emergency') in STOCK_EMERGENCY:return True
return False
def main():
a=argparse.ArgumentParser();a.add_argument('--original',required=True);a.add_argument('--catalog',required=True);a.add_argument('--output-dir',required=True);args=a.parse_args()
dest=Path(args.output_dir);dest.mkdir(parents=True,exist_ok=True)
with open(args.original,encoding='utf-8-sig',newline='') as f:
old=list(csv.DictReader((line for line in f if not line.startswith('#'))))
assert len(old)>=1800
originals={r['code'].lower():r for r in old}
assert len(originals)==len(old)
with open(args.catalog,encoding='utf-8-sig',newline='') as f:catalog=list(csv.DictReader(f))
assert len(catalog)>=260
report=[];labels=[]
for r in catalog:
code=r['code'].lower();cur=originals.get(code)
if not cur:raise SystemExit('Missing type in base CSV: '+code)
cnt=int(r['count']);raw=tagdict(r.get('reviewed_tags') or r.get('default_tags') or '')
override=INTERPRET.get(code)
if override:
parsed=tagdict(override[0]);label=override[1];confidence=override[2]
else:
parsed=raw
label=r.get('verified_legend_bg') or ''
confidence='legend' if r.get('review_status')=='icon_legend_verified' else ('source-inferred' if parsed else 'unknown')
# Extended, unverified POIs use synthetic rendering. Do not claim a real OSM tag.
if code.startswith('0x1') and int(code,0)>=0x10000:confidence='unverified-extended'
synth=not has_standard_icon(parsed)
out=dict(parsed)
if synth:
# OsmAnd compatibility shim: real tags are preserved, these extra tags only
# give local OBF viewers a stock information icon. NEVER upload to OSM.
out['tourism']='information'
out['information']='board'
out['bgm:synthetic_icon']='yes'
if r.get('verified_legend_en'):
# Keep original icon-derived legend classification independently of display shim.
old_legend=tagdict(cur.get('tags','')).get('garmin:legend')
if old_legend:out['garmin:legend']=old_legend
# No code remains raw-only: low-frequency categories also get an indexable type.
if not out:raise SystemExit('Missing display tags for '+code)
newtags=tags_text(out)
before=cur['tags'];cur['mode']='replace';cur['tags']=newtags
cur['note']=(cur.get('note') or '')[:180]+' | V08 local icon mode; confidence='+confidence+(' | SYNTHETIC INFORMATION-BOARD ICON, NOT PUBLIC OSM DATA' if synth else ' | native-tag candidate')
if not label:label='Garmin обект '+code
if label.strip().lower() in ('каптаж','reservoir'):label='Вода - Каптаж'
labels.append({'code':code,'label_bg':label,'confidence':confidence})
report.append(dict(code=code,count=cnt,old_tags=before,new_tags=newtags,synthetic_fallback='yes' if synth else 'no',confidence=confidence,
label_bg=label,example_lat=r.get('example1_lat',''),example_lon=r.get('example1_lon',''),example_name=r.get('example1_name',''),
note='presentation compatibility: cannot guarantee draw at every zoom' if synth else 'stock-tag candidate: verify in OBF'))
with open(dest/'poi-overrides-all-types.csv','w',encoding='utf-8',newline='') as f:
f.write('# Local OsmAnd display compatibility rules, NOT valid for public OSM imports.\n')
f.write('# All 263 retained types have a semantic tag or synthetic board icon; historic rejected types remain inert.\n')
wr=csv.DictWriter(f,fieldnames=['code','name_contains','mode','tags','note']);wr.writeheader();wr.writerows(old)
for name,rows in [('poi-labels-bg.csv',labels),('poi-icon-audit.csv',report)]:
with open(dest/name,'w',encoding='utf-8',newline='') as f:
wr=csv.DictWriter(f,fieldnames=list(rows[0]));wr.writeheader();wr.writerows(rows)
print('catalog reviewed',len(report),'frequent',sum(int(x['count'])>=100 for x in report),'synthetic fallback',sum(x['synthetic_fallback']=='yes' for x in report))
for code in ('0x6500','0x6602','0x650b','0x660b'):
print(code,next(x for x in report if x['code']==code))
if __name__=='__main__':main()