import json, math, pathlib, collections, subprocess, hashlib ROOT=pathlib.Path(__file__).resolve().parent.parent AREAS=json.load(open(ROOT/'research/areas.json'));LOOKUP={a['adcode']:a for a in AREAS} from map_geometry import make_level, project, place_labels, hainan_offshore_inset levels=[] cn=json.load(open(ROOT/'research/china.json'));nation=make_level(cn['features'],100000,'中国 · 省级拼图',100000);levels.append(nation) for p in sorted((ROOT/'data/raw').glob('*.json')): code=int(p.stem);info=LOOKUP.get(code) if not info:continue fs=json.load(open(p)).get('features',[]) # Do not turn city-level outlines into county puzzles. fs=[f for f in fs if f.get('properties',{}).get('level')=='district'] prov=code if info['level']=='province' else info['parent'] level=make_level(fs,code,info['name'],prov) if level:levels.append(level) provinces=[{'id':str(a['adcode']),'name':a['name']} for a in AREAS if a['level']=='province'] result={'source':'DataV.GeoAtlas','downloadDate':'2026-10-16','provinces':provinces,'levels':levels} (ROOT/'data/county-materials').mkdir(parents=True,exist_ok=True) (ROOT/'data/county-materials/maps.js').write_text('window.MAP_DATA='+json.dumps(result,ensure_ascii=False,separators=(',',':'))+';\n') summary={'levels':len(levels),'countyLevels':len(levels)-1,'provincePieces':len(nation['regions']),'countyPieces':sum(len(l['regions']) for l in levels[1:]),'availableProvinces':len({l['province'] for l in levels[1:]}),'seaRoutes':sum(e[2] for l in levels for e in l['edges'])} (ROOT/'data/county-materials/summary.json').write_text(json.dumps(summary,ensure_ascii=False,indent=2));print(summary) # Formal release: prefectures plus province-administered peers, so the province has no filtered-out gaps. citylevels=[nation] direct_summary=[] prefecture_index={'downloadDate':'2026-10-16','source':'DataV.GeoAtlas','levels':[]} for p in sorted((ROOT/'data/provinces-raw').glob('*.json')): code=int(p.stem);info=LOOKUP.get(code) if not info:continue fs=json.load(open(p)).get('features',[]) # These are the province endpoint's immediate children, not every county # nested inside a prefecture. District-only municipalities are added separately. children=[f for f in fs if f.get('properties',{}).get('level') in ('city','district') and f['properties'].get('parent',{}).get('adcode')==code] prefectures=[f for f in children if int(f['properties']['adcode'])%100==0] if not prefectures:continue cityfs=children assert len(cityfs)==len(fs),(code,'unclassified province children') direct=[f for f in cityfs if f not in prefectures] lv=make_level(cityfs,code,info['name'],code) if lv: lv['kind']='prefecture' direct_ids={str(f['properties']['adcode']) for f in direct} for r in lv['regions']:r['unitType']='province-direct' if r['id'] in direct_ids else 'prefecture' if direct: lv['playLabel']='省域拼图' lv['unitCaption']='地级行政区 · 省直辖地区' lv['scopeNote']='含'+str(len(direct))+'个省直辖县级地区,拼完整片省域。' lv['directRegionCount']=len(direct) direct_summary.append({'province':str(code),'names':[f['properties']['name'] for f in direct]}) entries=[] for f in cityfs: prop=f['properties'];geometry=f['geometry'];polys=geometry['coordinates'] if geometry['type']=='MultiPolygon' else [geometry['coordinates']] entries.append({'id':str(prop['adcode']),'name':prop['name'],'unitType':'province-direct' if str(prop['adcode']) in direct_ids else 'prefecture','rings':len(polys),'holes':sum(len(poly)-1 for poly in polys)}) prefecture_index['levels'].append({'id':str(code),'name':info['name'],'sha256':hashlib.sha256(p.read_bytes()).hexdigest(),'regions':entries}) # Disconnected islands retain explicit game-only routes. for edge in lv['edges']: if edge[2]:edge[2]=2 citylevels.append(lv) for lv in citylevels: hainan_offshore_inset(lv) place_labels(lv) # Preserve the source's unnamed maritime boundary features as a separate layer. minx,miny,scale=nation['projection'];decorations=[] for feature in cn['features']: if feature['properties'].get('adcode')=='100000_JD': geom=feature['geometry'];polys=geom['coordinates'] if geom['type']=='MultiPolygon' else [geom['coordinates']] for poly in polys:decorations.append([[round((project(p)[0]-minx)*scale,4),round((project(p)[1]-miny)*scale,4)] for p in poly[0]]) nation['decorations']=decorations for lv in citylevels:lv.pop('projection',None) result={'source':'DataV.GeoAtlas','downloadDate':'2026-10-16','provinces':provinces,'levels':citylevels} (ROOT/'src/maps.js').write_text('window.MAP_DATA='+json.dumps(result,ensure_ascii=False,separators=(',',':'))+';\n') subprocess.run(['node',str(ROOT/'scripts/triangulate-holes.mjs')],check=True,cwd=ROOT) summary={'levels':len(citylevels),'prefectureLevels':len(citylevels)-1,'provincePieces':len(nation['regions']),'prefecturePieces':sum(len(l['regions']) for l in citylevels[1:]),'availableProvinces':len(citylevels)-1,'provinceDirectPieces':sum(len(v['names']) for v in direct_summary),'provinceDirectUnits':direct_summary,'countyMaterials':{'levels':len(levels)-1,'pieces':sum(len(l['regions']) for l in levels[1:]),'path':'data/county-materials/maps.js'}} (ROOT/'research/prefecture-map-index.json').write_text(json.dumps(prefecture_index,ensure_ascii=False,indent=2)+'\n') (ROOT/'data/map-summary.json').write_text(json.dumps(summary,ensure_ascii=False,indent=2));print('Release:',{k:v for k,v in summary.items() if k!='provinceDirectUnits'})