Files
shanhe-puzzle/scripts/prepare-maps.py
T
codex-bot 4845826deb
Shanhe source and offline package checks / verify (pull_request) Failing after 5s
feat: sync Shanhe 2.2 source and add remote package verification
2026-11-01 17:13:46 +08:00

76 lines
5.4 KiB
Python

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'})