feat: save PostgreSQL PostGIS CM95 baseline
This commit is contained in:
1
web/assets/maplibre-gl.css
Normal file
1
web/assets/maplibre-gl.css
Normal file
File diff suppressed because one or more lines are too long
59
web/assets/maplibre-gl.js
Normal file
59
web/assets/maplibre-gl.js
Normal file
File diff suppressed because one or more lines are too long
61
web/mysql_postgresql_compare.html
Normal file
61
web/mysql_postgresql_compare.html
Normal file
@@ -0,0 +1,61 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>CM95 MySQL vs PostgreSQL PBF 对比</title>
|
||||
<link rel="stylesheet" href="/assets/maplibre-gl.css">
|
||||
<style>
|
||||
*{box-sizing:border-box}html,body{height:100%;margin:0;background:#111;color:#eee;font-family:system-ui,-apple-system,"Segoe UI",sans-serif}
|
||||
body{overflow:hidden}#bar{height:52px;display:flex;align-items:center;gap:12px;padding:0 12px;background:#17191d;border-bottom:1px solid #3b3f46;font-size:12px}
|
||||
#bar strong{font-size:14px;white-space:nowrap}#bar button{border:1px solid #555;background:#272b31;color:#fff;border-radius:5px;padding:5px 9px;cursor:pointer}
|
||||
#bar label{display:flex;gap:5px;align-items:center;white-space:nowrap}.muted{color:#aeb7c3}#readout{margin-left:auto;font-variant-numeric:tabular-nums;white-space:nowrap}
|
||||
#maps{height:calc(100% - 52px);display:grid;grid-template-columns:1fr 1fr;gap:2px;background:#555}
|
||||
.pane{position:relative;min-width:0;overflow:hidden;background:#d8edf5}.map{position:absolute;inset:0}
|
||||
.title{position:absolute;z-index:5;left:10px;top:10px;padding:7px 9px;border-radius:6px;background:rgba(18,20,24,.88);font-size:12px;line-height:1.4}
|
||||
.title b{display:block;font-size:14px}.state{font-size:11px;color:#cbd2dc}.ok{color:#91e39b}.err{color:#ff8c8c}
|
||||
.inspect{position:absolute;z-index:5;left:10px;right:10px;bottom:10px;max-height:28%;overflow:auto;padding:7px 9px;border-radius:6px;background:rgba(18,20,24,.9);font:11px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace;white-space:pre-wrap}
|
||||
#stats{position:absolute;z-index:10;top:62px;left:50%;transform:translateX(-50%);max-width:72%;padding:7px 10px;border-radius:6px;background:rgba(18,20,24,.9);font:11px/1.35 ui-monospace,SFMono-Regular,Consolas,monospace;pointer-events:none}
|
||||
@media(max-width:820px){#maps{grid-template-columns:1fr;grid-template-rows:1fr 1fr}#bar .muted{display:none}#stats{max-width:94%}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="bar"><strong>CM95 PBF:MySQL vs PostgreSQL</strong><span class="muted">同源数据 / 同一 S-52 style / 同步视图</span>
|
||||
<label><input id="sync" type="checkbox" checked>同步</label><button id="reset">九州</button><button id="info">统计</button><span id="readout">--</span></div>
|
||||
<div id="stats" hidden></div>
|
||||
<div id="maps">
|
||||
<section class="pane"><div id="mysql-map" class="map"></div><div class="title"><b>左:MySQL</b><span id="mysql-state" class="state">加载中…</span></div><div id="mysql-inspect" class="inspect">点击任一侧,同点检查 Feature。</div></section>
|
||||
<section class="pane"><div id="pg-map" class="map"></div><div class="title"><b>右:PostgreSQL / PostGIS</b><span id="pg-state" class="state">加载中…</span></div><div id="pg-inspect" class="inspect">点击任一侧,同点检查 Feature。</div></section>
|
||||
</div>
|
||||
<script src="/assets/maplibre-gl.js"></script>
|
||||
<script>
|
||||
const BOUNDS=[[128.0,29.5],[132.5,34.8]], START={center:[130.25,32.15],zoom:6.6,bearing:0,pitch:0};
|
||||
const sync=document.getElementById("sync"), readout=document.getElementById("readout"), stats=document.getElementById("stats");
|
||||
let styleTemplate=null,syncing=false,m1=null,m2=null;
|
||||
function state(id,text,ok=true){const e=document.getElementById(id);e.textContent=text;e.className="state "+(ok?"ok":"err")}
|
||||
function blank(){return {version:8,sources:{},layers:[{id:"bg",type:"background",paint:{"background-color":"#d8edf5"}}]}}
|
||||
function styleFor(kind){const s=JSON.parse(JSON.stringify(styleTemplate));const src=s.sources&&s.sources.cm95;if(!src)throw new Error("style 缺少 sources.cm95");
|
||||
src.type="vector";src.tiles=[location.origin+"/"+kind+"/{z}/{x}/{y}.pbf"];src.minzoom=5;src.maxzoom=12;src.bounds=[128,29.5,132.5,34.8];delete src.url;s.sprite=location.origin+"/sprite";return s}
|
||||
const mysqlMap=new maplibregl.Map({container:"mysql-map",style:blank(),...START,attributionControl:false});
|
||||
const pgMap=new maplibregl.Map({container:"pg-map",style:blank(),...START,attributionControl:false});
|
||||
mysqlMap.addControl(new maplibregl.NavigationControl(),"top-right");pgMap.addControl(new maplibregl.NavigationControl(),"top-right");
|
||||
function camera(m){const c=m.getCenter();return {center:[c.lng,c.lat],zoom:m.getZoom(),bearing:m.getBearing(),pitch:m.getPitch()}}
|
||||
function link(a,b){if(syncing||!sync.checked)return;syncing=true;try{b.jumpTo(camera(a))}finally{syncing=false}}
|
||||
mysqlMap.on("move",()=>{link(mysqlMap,pgMap);showReadout(mysqlMap)});pgMap.on("move",()=>{link(pgMap,mysqlMap);showReadout(pgMap)});
|
||||
function showReadout(m){const c=m.getCenter();readout.textContent="z="+m.getZoom().toFixed(2)+" "+c.lng.toFixed(5)+", "+c.lat.toFixed(5)}
|
||||
document.getElementById("reset").onclick=()=>{mysqlMap.fitBounds(BOUNDS,{padding:36,duration:0});link(mysqlMap,pgMap)};
|
||||
document.getElementById("info").onclick=()=>{stats.hidden=!stats.hidden};
|
||||
async function load(){try{const [sr,ir]=await Promise.all([fetch("/style.json",{cache:"no-store"}),fetch("/api/info",{cache:"no-store"})]);styleTemplate=await sr.json();const info=await ir.json();
|
||||
stats.textContent="MySQL: "+info.mysql.release+" | "+info.mysql.files+" tiles\nPostgreSQL: "+info.postgresql.release+" | "+info.postgresql.files+" tiles\nCommon tile hash: "+info.compare.identical+"/"+info.compare.common+" identical";
|
||||
mysqlMap.setStyle(styleFor("mysql"));pgMap.setStyle(styleFor("postgres"));}catch(e){state("mysql-state",String(e),false);state("pg-state",String(e),false)}}
|
||||
mysqlMap.on("style.load",()=>state("mysql-state","S-52 / MySQL PBF",true));pgMap.on("style.load",()=>state("pg-state","S-52 / PostgreSQL PBF",true));
|
||||
function key(f){const p=f.properties||{};return [f.sourceLayer||"",f.id??"",p.canonical_object_type||"",p.source_object_class||""].join("|")}
|
||||
function summary(f){const p=f.properties||{};return [f.sourceLayer||"-","id="+(f.id??"-"),p.canonical_object_type||"",p.source_object_class||"",p.chart_label_text||"",p.contour_depth_m!==undefined?"depth="+p.contour_depth_m:""].filter(Boolean).join(" ")}
|
||||
function inspect(map,ll,id){const el=document.getElementById(id);if(!map.loaded()){el.textContent="地图加载中";return}const p=map.project(ll),box=[[p.x-5,p.y-5],[p.x+5,p.y+5]],u=new Map();
|
||||
for(const f of map.queryRenderedFeatures(box)){if(!u.has(key(f)))u.set(key(f),f)}const a=[...u.values()].slice(0,24);el.textContent=ll.lng.toFixed(6)+", "+ll.lat.toFixed(6)+"\n命中 "+u.size+" 项\n"+a.map((f,i)=>String(i+1).padStart(2,"0")+". "+summary(f)).join("\n")}
|
||||
function compare(ll){if(m1)m1.remove();if(m2)m2.remove();m1=new maplibregl.Marker().setLngLat(ll).addTo(mysqlMap);m2=new maplibregl.Marker().setLngLat(ll).addTo(pgMap);inspect(mysqlMap,ll,"mysql-inspect");inspect(pgMap,ll,"pg-inspect")}
|
||||
mysqlMap.on("click",e=>compare(e.lngLat));pgMap.on("click",e=>compare(e.lngLat));
|
||||
mysqlMap.on("error",e=>{const m=e?.error?.message||"";if(!m.includes("404"))state("mysql-state",m,false)});pgMap.on("error",e=>{const m=e?.error?.message||"";if(!m.includes("404"))state("pg-state",m,false)});
|
||||
window.addEventListener("resize",()=>{mysqlMap.resize();pgMap.resize()});showReadout(mysqlMap);load();
|
||||
</script>
|
||||
</body></html>
|
||||
105
web/mysql_postgresql_compare_server.py
Executable file
105
web/mysql_postgresql_compare_server.py
Executable file
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
|
||||
def parse_args():
|
||||
p=argparse.ArgumentParser(description="CM95 MySQL vs PostgreSQL PBF comparison server")
|
||||
p.add_argument("--host",default="0.0.0.0")
|
||||
p.add_argument("--port",type=int,default=18889)
|
||||
p.add_argument("--mysql-pbf-root",required=True)
|
||||
p.add_argument("--postgres-pbf-root",required=True)
|
||||
p.add_argument("--mysql-release",default="mysql-current")
|
||||
p.add_argument("--postgres-release",default="postgres-current")
|
||||
return p.parse_args()
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
h=hashlib.sha256()
|
||||
with path.open("rb") as f:
|
||||
for b in iter(lambda:f.read(1024*1024),b""):
|
||||
h.update(b)
|
||||
return h.hexdigest()
|
||||
|
||||
def pbf_inventory(root: Path):
|
||||
return {str(p.relative_to(root)):p for p in root.rglob("*.pbf") if p.is_file()}
|
||||
|
||||
def main():
|
||||
args=parse_args()
|
||||
web=Path(__file__).resolve().parent
|
||||
mysql_root=Path(args.mysql_pbf_root).resolve()
|
||||
pg_root=Path(args.postgres_pbf_root).resolve()
|
||||
html=web/"mysql_postgresql_compare.html"
|
||||
style=web/"resources/style.s52.json"
|
||||
assets=web/"assets"
|
||||
resources=web/"resources"
|
||||
for p in [mysql_root,pg_root,html,style,assets/"maplibre-gl.js",assets/"maplibre-gl.css",resources/"sprite.json",resources/"sprite.png"]:
|
||||
if not p.exists():
|
||||
raise SystemExit(f"missing: {p}")
|
||||
mi=pbf_inventory(mysql_root)
|
||||
pi=pbf_inventory(pg_root)
|
||||
common=sorted(set(mi)&set(pi))
|
||||
identical=0
|
||||
different=[]
|
||||
for rel in common:
|
||||
if mi[rel].stat().st_size==pi[rel].stat().st_size and sha256(mi[rel])==sha256(pi[rel]):
|
||||
identical+=1
|
||||
elif len(different)<50:
|
||||
different.append(rel)
|
||||
info={
|
||||
"mysql":{"release":args.mysql_release,"files":len(mi),"root":str(mysql_root)},
|
||||
"postgresql":{"release":args.postgres_release,"files":len(pi),"root":str(pg_root)},
|
||||
"compare":{"common":len(common),"identical":identical,"different":len(common)-identical,"different_examples":different},
|
||||
}
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def send(self,data:bytes,ctype:str,status=200):
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type",ctype)
|
||||
self.send_header("Content-Length",str(len(data)))
|
||||
self.send_header("Cache-Control","no-cache")
|
||||
self.send_header("Access-Control-Allow-Origin","*")
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
def serve_file(self,path:Path,ctype:str):
|
||||
if not path.is_file():
|
||||
self.send_error(404)
|
||||
return
|
||||
self.send(path.read_bytes(),ctype)
|
||||
def do_GET(self):
|
||||
path=urlparse(self.path).path
|
||||
if path in ("/","/index.html","/compare"):
|
||||
return self.serve_file(html,"text/html; charset=utf-8")
|
||||
if path=="/api/info":
|
||||
return self.send(json.dumps(info,ensure_ascii=False,indent=2).encode(),"application/json; charset=utf-8")
|
||||
if path=="/style.json":
|
||||
return self.serve_file(style,"application/json; charset=utf-8")
|
||||
if path in ("/assets/maplibre-gl.js","/assets/maplibre-gl.css"):
|
||||
fn=path.rsplit("/",1)[-1]
|
||||
ct="application/javascript; charset=utf-8" if fn.endswith(".js") else "text/css; charset=utf-8"
|
||||
return self.serve_file(assets/fn,ct)
|
||||
if path in ("/sprite.json","/sprite.png","/sprite@2x.json","/sprite@2x.png"):
|
||||
fn=path[1:]
|
||||
ct="application/json; charset=utf-8" if fn.endswith(".json") else "image/png"
|
||||
return self.serve_file(resources/fn,ct)
|
||||
for prefix,root in (("/mysql/",mysql_root),("/postgres/",pg_root)):
|
||||
if path.startswith(prefix) and path.endswith(".pbf"):
|
||||
rel=path[len(prefix):]
|
||||
parts=rel.split("/")
|
||||
if len(parts)!=3 or not all(x.isdigit() for x in [parts[0],parts[1],parts[2][:-4]]):
|
||||
self.send_error(404); return
|
||||
return self.serve_file(root/rel,"application/vnd.mapbox-vector-tile")
|
||||
self.send_error(404)
|
||||
def log_message(self,fmt,*args):
|
||||
pass
|
||||
|
||||
print(json.dumps(info,ensure_ascii=False,indent=2))
|
||||
print(f"Compare: http://{args.host}:{args.port}/")
|
||||
ThreadingHTTPServer((args.host,args.port),Handler).serve_forever()
|
||||
|
||||
if __name__=="__main__":
|
||||
main()
|
||||
7338
web/resources/sprite.json
Normal file
7338
web/resources/sprite.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
web/resources/sprite.png
Normal file
BIN
web/resources/sprite.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 394 KiB |
7338
web/resources/sprite@2x.json
Normal file
7338
web/resources/sprite@2x.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
web/resources/sprite@2x.png
Normal file
BIN
web/resources/sprite@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 562 KiB |
42381
web/resources/style.s52.json
Normal file
42381
web/resources/style.s52.json
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user