v08
This commit is contained in:
@@ -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())
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user