Files
shanhe-puzzle/scripts/prepare-taiwan.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

54 lines
3.2 KiB
Python

"""Convert the official NLSC county SHP ZIP to a build-only GeoJSON input.
Download https://maps.nlsc.gov.tw/pro/download.jsp's county EPSG:3824 ZIP
into data/taiwan-raw/counties.zip first. Only Python's standard library is used.
"""
import json, pathlib, struct, zipfile
ROOT=pathlib.Path(__file__).resolve().parent.parent
folder=ROOT/'data/taiwan-raw'
with zipfile.ZipFile(folder/'counties.zip') as z:
names=z.namelist();dbf=z.read(next(n for n in names if n.lower().endswith('.dbf')))
shp=z.read(next(n for n in names if n.lower().endswith('.shp')))
prj=z.read(next(n for n in names if n.lower().endswith('.prj'))).decode()
assert 'GEOGCS' in prj and 'TWD_1997' in prj and 'Degree' in prj
count,header,size=struct.unpack_from('<IHH',dbf,4);fields=[];offset=32
while dbf[offset]!=13:
d=dbf[offset:offset+32];fields.append((d[:11].split(b'\0')[0].decode(),d[16]));offset+=32
rows=[]
for i in range(count):
row=dbf[header+i*size:header+(i+1)*size];assert row[0]==32;pos=1;prop={}
for name,length in fields:prop[name]=row[pos:pos+length].decode('utf-8').strip();pos+=length
rows.append(prop)
assert count==22 and struct.unpack_from('<i',shp,32)[0]==5
geometries=[];offset=100
while offset<len(shp):
_,words=struct.unpack_from('>ii',shp,offset);part=shp[offset+8:offset+8+words*2];offset+=8+words*2
assert struct.unpack_from('<i',part)[0]==5
parts,points=struct.unpack_from('<ii',part,36);starts=list(struct.unpack_from('<'+'i'*parts,part,44))+[points];base=44+parts*4
coords=[list(struct.unpack_from('<dd',part,base+i*16)) for i in range(points)]
rings=[coords[starts[i]:starts[i+1]] for i in range(parts)]
def signed(r):return sum(a[0]*b[1]-b[0]*a[1] for a,b in zip(r,r[1:]))/2
# SHP exterior rings are clockwise. Inner holes are not individual pieces.
outers=[r for r in rings if signed(r)<0]
assert outers and all(114<x<125 and 9<y<27 for r in outers for x,y in r)
def inside(p,r):
x,y=p;hit=False
for a,b in zip(r,r[1:]):
if (a[1]>y)!=(b[1]>y) and x<(b[0]-a[0])*(y-a[1])/(b[1]-a[1])+a[0]:hit=not hit
return hit
polys=[[r] for r in outers]
for hole in [r for r in rings if signed(r)>0]:
owners=[poly for poly in polys if inside(hole[0],poly[0])];assert len(owners)==1;owners[0].append(hole)
geometries.append({'type':'MultiPolygon','coordinates':polys})
assert len(geometries)==len(rows)
trans=str.maketrans({'臺':'台','縣':'县','蘭':'兰','雲':'云','義':'义','園':'园','東':'东','蓮':'莲'})
features=[]
for prop,g in zip(rows,geometries):
# These two counties belong to Fujian, not Taiwan Province.
if prop['COUNTYCODE'] in ('09007','09020'):continue
features.append({'type':'Feature','properties':{'adcode':'TW-'+prop['COUNTYCODE'],'sourceCode':prop['COUNTYCODE'],'name':prop['COUNTYNAME'].translate(trans),'level':'county-city'},'geometry':g})
assert len(features)==20
result={'type':'FeatureCollection','source':'NLSC','sourceURL':'https://data.gov.tw/dataset/7442','snapshot':'COUNTY_MOI_1090820','license':'政府資料開放授權條款第1版','features':features}
(folder/'counties.geojson').write_text(json.dumps(result,ensure_ascii=False,separators=(',',':')))
print('台湾县市输入:',len(features),'块;外环',sum(len(f['geometry']['coordinates']) for f in features))