76 lines
5.4 KiB
Python
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'})
|