Files
boc/quixzoom-landing-fixed/urban-map.html
T
Bernt aee0f09db8 landvex: Fixar och tester klara för alla komponenter
- Datafabrik: Dockerfile fix, agentorkestrering fungerar
- Vision: Identify-modell, FAISS, OCR alla testade
- API: Alla 7 integrationstester passerade
- Upplösare: Entitetsupplösning verifierad
2026-07-05 06:41:32 +00:00

172 lines
15 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"/>
<title>quiXzoom Urban Intelligence Map</title>
<meta name="description" content="Live map showing Urban Intelligence assessments, mission availability and Contradiction Scores across cities worldwide."/>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}
:root{
--bg:#0d1117;--surface:#161b22;--surface2:#21262d;
--border:rgba(255,255,255,0.08);--text:#e6edf3;--text2:rgba(230,237,243,0.6);
--green:#3fb950;--yellow:#d29922;--orange:#f0883e;--red:#f85149;
--blue:#58a6ff;--safe-top:env(safe-area-inset-top,0px);--safe-bottom:env(safe-area-inset-bottom,0px);
}
html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:var(--bg);color:var(--text);overflow:hidden}
#map{position:fixed;top:0;left:0;right:0;bottom:0;z-index:1}
.leaflet-control-zoom{margin-top:calc(var(--safe-top) + 64px) !important;margin-left:16px !important}
.leaflet-control-zoom a{background:var(--surface) !important;color:var(--text) !important;border-color:var(--border) !important;width:34px !important;height:34px !important;line-height:34px !important;border-radius:8px !important;font-size:18px !important}
#hdr{position:fixed;top:0;left:0;right:0;z-index:1000;background:rgba(13,17,23,0.9);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border-bottom:1px solid var(--border);padding:calc(var(--safe-top) + 10px) 16px 10px}
#hdr-inner{max-width:1200px;margin:0 auto;display:flex;align-items:center;gap:12px}
#logo{font-size:15px;font-weight:700;color:var(--text);text-decoration:none;white-space:nowrap}
#logo span{color:var(--green)}
#hdr-sub{flex:1;font-size:13px;color:var(--text2)}
.badge{font-size:10px;font-weight:700;letter-spacing:0.8px;text-transform:uppercase;background:rgba(63,185,80,0.15);color:var(--green);border:1px solid rgba(63,185,80,0.3);border-radius:20px;padding:3px 9px}
#ui-tog{position:fixed;top:calc(var(--safe-top) + 62px);right:16px;z-index:1000;background:rgba(22,27,34,0.92);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:1px solid var(--border);border-radius:10px;padding:7px 11px;display:flex;align-items:center;gap:7px;cursor:pointer;font-size:12px;font-weight:600;color:var(--text);user-select:none;transition:opacity 0.2s}
#ui-tog:active{opacity:0.7}
#ui-led{width:8px;height:8px;border-radius:50%;background:var(--green);flex-shrink:0;transition:background 0.2s}
#ui-tog.off #ui-led{background:var(--text2)}
#panel{position:fixed;bottom:0;left:0;right:0;z-index:900;background:rgba(22,27,34,0.94);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border-top:1px solid var(--border);padding:10px 16px calc(var(--safe-bottom) + 10px)}
#panel-inner{max-width:1200px;margin:0 auto}
#ph{width:36px;height:4px;border-radius:2px;background:rgba(255,255,255,0.12);margin:0 auto 10px}
#leg{display:flex;gap:6px;flex-wrap:wrap;justify-content:center;margin-bottom:8px}
.li{display:flex;align-items:center;gap:5px;font-size:11px;color:var(--text2);background:var(--surface2);border-radius:20px;padding:4px 10px;border:1px solid var(--border)}
.ld{width:9px;height:9px;border-radius:50%;flex-shrink:0}
#note{font-size:11px;color:var(--text2);text-align:center;line-height:1.4}
#note a{color:var(--blue);text-decoration:none}
.leaflet-popup-content-wrapper{background:var(--surface) !important;border:1px solid var(--border) !important;border-radius:12px !important;box-shadow:0 8px 32px rgba(0,0,0,0.6) !important;padding:0 !important}
.leaflet-popup-content{margin:0 !important;width:260px !important;color:var(--text)}
.leaflet-popup-tip{background:var(--surface) !important}
.leaflet-popup-close-button{color:var(--text2) !important;top:8px !important;right:10px !important}
.zpop{padding:14px 16px}
.zpop-head{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;margin-bottom:10px}
.zpop-name{font-size:14px;font-weight:700;color:var(--text);line-height:1.3}
.zpop-conf{font-size:11px;color:var(--text2);background:var(--surface2);border-radius:6px;padding:2px 7px;white-space:nowrap;flex-shrink:0;font-weight:600}
.zpop-scores{display:grid;grid-template-columns:1fr 1fr;gap:5px;margin-bottom:10px}
.zpop-sc{background:var(--surface2);border-radius:7px;padding:6px 8px}
.zpop-sl{font-size:10px;color:var(--text2);font-weight:500;margin-bottom:3px}
.zpop-bar{height:3px;border-radius:2px;background:rgba(255,255,255,0.08);margin-bottom:2px}
.zpop-fill{height:100%;border-radius:2px}
.zpop-sv{font-size:12px;font-weight:700;color:var(--text)}
.zpop-c{border-radius:8px;padding:7px 10px;font-size:11px;line-height:1.5;margin-bottom:8px}
.zpop-c.low{background:rgba(63,185,80,0.1);color:#7ee787;border:1px solid rgba(63,185,80,0.2)}
.zpop-c.medium{background:rgba(240,136,62,0.1);color:#ffa657;border:1px solid rgba(240,136,62,0.2)}
.zpop-c.high{background:rgba(248,81,73,0.1);color:#ff7b72;border:1px solid rgba(248,81,73,0.2)}
.zpop-cl{font-weight:700;display:block;margin-bottom:2px}
.zpop-d{font-size:10px;color:var(--text2);border-top:1px solid var(--border);padding-top:7px;line-height:1.4}
.mpop{padding:14px 16px}
.mpop-cat{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:0.5px;color:var(--blue);background:rgba(88,166,255,0.1);border-radius:5px;padding:2px 7px;display:inline-block;margin-bottom:7px}
.mpop-title{font-size:14px;font-weight:600;color:var(--text);line-height:1.3;margin-bottom:10px}
.mpop-meta{display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:10px}
.mpop-item{background:var(--surface2);border-radius:8px;padding:7px 9px}
.mpop-lbl{font-size:10px;color:var(--text2);margin-bottom:2px;font-weight:500}
.mpop-val{font-size:13px;font-weight:700;color:var(--text)}
.mpop-reward{color:var(--green)}
.mpop-risk{display:flex;align-items:center;justify-content:space-between;gap:8px;border-top:1px solid var(--border);padding-top:9px}
.rb{font-size:10px;font-weight:700;border-radius:6px;padding:3px 8px}
.rb.L1{background:rgba(63,185,80,0.15);color:#7ee787}
.rb.L2{background:rgba(210,153,34,0.15);color:#e3b341}
.rb.L3{background:rgba(240,136,62,0.15);color:#ffa657}
.rb.L4{background:rgba(248,81,73,0.15);color:#ff7b72}
.rh{font-size:10px;color:var(--text2);text-align:right;line-height:1.3}
@media (max-width: 640px) {
.hero h1 { font-size: 2rem !important; }
.hero p { font-size: 1rem !important; }
.card { padding: 1.5rem !important; }
.grid { grid-template-columns: 1fr !important; }
.btn { padding: 0.75rem 1.5rem !important; font-size: 1rem !important; }
}
</style>
<meta property="og:type" content="website">
<meta property="og:title" content="quiXzoom Urban Intelligence Map">
<meta property="og:description" content="Live map showing Urban Intelligence assessments, mission availability and Contradiction Scores across cities worldwide.">
<meta property="og:url" content="https://www.quixzoom.com/urban-map.html">
<meta property="og:image" content="https://www.quixzoom.com/images/mission-map.jpg">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="quiXzoom urban mission map">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="https://www.quixzoom.com/images/mission-map.jpg">
<meta name="twitter:title" content="quiXzoom Urban Intelligence Map">
</head>
<body>
<h1 style="position:absolute;left:-9999px;">Urban Map - quiXzoom</h1>
<div id="map"></div>
<div id="hdr"><div id="hdr-inner">
<a href="https://www.quixzoom.com" id="logo">quiX<span>zoom</span></a>
<div id="hdr-sub">Urban Intelligence Map</div>
<div class="badge">BETA</div>
</div></div>
<div id="ui-tog"><div id="ui-led"></div><span>Urban Intelligence</span></div>
<div id="panel"><div id="panel-inner">
<div id="ph"></div>
<div id="leg">
<div class="li"><div class="ld" style="background:#3fb950"></div>Low risk</div>
<div class="li"><div class="ld" style="background:#d29922"></div>Normal</div>
<div class="li"><div class="ld" style="background:#f0883e"></div>Elevated</div>
<div class="li"><div class="ld" style="background:#f85149"></div>High risk</div>
<div class="li"><div class="ld" style="background:#8b0000"></div>Restricted</div>
</div>
<div id="note">Landvex Assessment — official sources + quiXzoom observations. <a href="https://www.landvex.com/methodology" target="_blank">Methodology →</a></div>
</div></div>
<script>
const map = L.map('map',{center:[20,10],zoom:3,zoomControl:true,attributionControl:false});
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{maxZoom:19}).addTo(map);
L.control.attribution({position:'bottomleft',prefix:'© <a href="https://openstreetmap.org">OpenStreetMap</a>'}).addTo(map);
const ZONES=[
{name:'Stockholm Innerstad',a:'green',conf:96,s:{inf:9.1,saf:8.7,trf:6.2,ngt:7.8},c:'low',ct:'Observed environment aligns well with official data.',coords:[[59.340,18.040],[59.340,18.100],[59.310,18.100],[59.310,18.040]]},
{name:'RinkebyKista',a:'orange',conf:81,s:{inf:6.2,saf:5.1,trf:7.1,ngt:4.2},c:'high',ct:'Deviation: official data indicates medium risk, but observations show 41% faulty street lighting, high graffiti incidence and low evening activity.',coords:[[59.390,17.910],[59.390,17.970],[59.360,17.970],[59.360,17.910]]},
{name:'Göteborg Centrum',a:'yellow',conf:88,s:{inf:7.8,saf:7.2,trf:5.9,ngt:6.5},c:'medium',ct:'Traffic risk underreported in official data vs observed congestion.',coords:[[57.720,11.950],[57.720,12.000],[57.695,12.000],[57.695,11.950]]},
{name:'Malmö Centrum',a:'yellow',conf:79,s:{inf:7.5,saf:6.4,trf:6.8,ngt:5.9},c:'medium',ct:'Night risk deviates from official statistics. More observations needed.',coords:[[55.615,13.000],[55.615,13.030],[55.595,13.030],[55.595,13.000]]},
{name:'London City',a:'green',conf:91,s:{inf:8.9,saf:8.1,trf:5.4,ngt:7.2},c:'low',ct:'High confidence. Observed environment consistent with official data.',coords:[[51.520,-0.110],[51.520,-0.050],[51.495,-0.050],[51.495,-0.110]]},
{name:'Bangkok Sukhumvit',a:'yellow',conf:74,s:{inf:7.1,saf:6.8,trf:4.2,ngt:6.1},c:'medium',ct:'Traffic risk significantly higher than official classification.',coords:[[13.760,100.550],[13.760,100.590],[13.735,100.590],[13.735,100.550]]},
{name:'Bangkok Klong Toei',a:'orange',conf:62,s:{inf:5.3,saf:4.8,trf:6.1,ngt:3.2},c:'high',ct:'Observed environment deviates significantly from official classification. Low confidence — additional observations needed.',coords:[[13.725,100.550],[13.725,100.590],[13.700,100.590],[13.700,100.550]]},
{name:'São Paulo Centro',a:'red',conf:71,s:{inf:6.1,saf:3.4,trf:4.8,ngt:2.1},c:'high',ct:'High night risk. Official data underrepresents observed conditions after 19:00.',coords:[[-23.545,-46.645],[-23.545,-46.620],[-23.570,-46.620],[-23.570,-46.645]]}
];
const AS={green:{color:'#3fb950',fo:0.12,w:1.5,d:null},yellow:{color:'#d29922',fo:0.15,w:1.5,d:'5 4'},orange:{color:'#f0883e',fo:0.20,w:2,d:'5 4'},red:{color:'#f85149',fo:0.25,w:2,d:'4 3'},darkred:{color:'#8b0000',fo:0.30,w:2.5,d:'4 3'}};
const CL={low:'Contradiction Score: Low',medium:'Contradiction Score: Medium',high:'Contradiction Score: HIGH ⚠️'};
function bar(v){const p=Math.round(v*10),c=v>=7.5?'#3fb950':v>=5?'#d29922':'#f85149';return `<div class="zpop-bar"><div class="zpop-fill" style="width:${p}%;background:${c}"></div></div>`;}
function zpop(z){return `<div class="zpop">
<div class="zpop-head"><span class="zpop-name">${z.name}</span><span class="zpop-conf">Confidence ${z.conf}%</span></div>
<div class="zpop-scores">
<div class="zpop-sc"><div class="zpop-sl">Infrastructure</div>${bar(z.s.inf)}<div class="zpop-sv">${z.s.inf}/10</div></div>
<div class="zpop-sc"><div class="zpop-sl">Safety</div>${bar(z.s.saf)}<div class="zpop-sv">${z.s.saf}/10</div></div>
<div class="zpop-sc"><div class="zpop-sl">Traffic risk</div>${bar(z.s.trf)}<div class="zpop-sv">${z.s.trf}/10</div></div>
<div class="zpop-sc"><div class="zpop-sl">Night risk</div>${bar(z.s.ngt)}<div class="zpop-sv">${z.s.ngt}/10</div></div>
</div>
<div class="zpop-c ${z.c}"><span class="zpop-cl">${CL[z.c]}</span>${z.ct}</div>
<div class="zpop-d">Landvex Assessment — official sources + quiXzoom observations. Not a factual statement about actual conditions.</div>
</div>`;}
let ug=L.layerGroup().addTo(map),uOn=true;
function renderZ(){ug.clearLayers();if(!uOn)return;ZONES.forEach(z=>{const st=AS[z.a]||AS.yellow;L.polygon(z.coords,{color:st.color,fillColor:st.color,fillOpacity:st.fo,weight:st.w,dashArray:st.d}).bindPopup(zpop(z),{maxWidth:300,minWidth:260}).addTo(ug);});}
renderZ();
document.getElementById('ui-tog').onclick=()=>{uOn=!uOn;document.getElementById('ui-tog').classList.toggle('off',!uOn);renderZ();};
const RISK={L1:{l:'L1 — Public Safe',h:'All verified Zoomers'},L2:{l:'L2 — Intermediate',h:'Experience + quality history'},L3:{l:'L3 — Advanced',h:'High reputation + safety training'},L4:{l:'L4 — Restricted',h:'Manual review — team mission'}};
function rl(m){if(m.risk_level)return m.risk_level;const r=Number(m.reward_sek)||0;return r>=100?'L4':r>=60?'L3':r>=35?'L2':'L1';}
function pin(s){const c=s==='open'?'#58a6ff':s==='assigned'?'#f0883e':'#3fb950';return L.divIcon({className:'',iconSize:[30,38],iconAnchor:[15,38],popupAnchor:[0,-40],html:`<svg xmlns="http://www.w3.org/2000/svg" width="30" height="38" viewBox="0 0 30 38"><filter id="d"><feDropShadow dx="0" dy="2" stdDeviation="2" flood-color="rgba(0,0,0,0.5)"/></filter><g filter="url(#d)"><circle cx="15" cy="15" r="12" fill="${c}" stroke="rgba(255,255,255,0.2)" stroke-width="1.5"/><path d="M15 27 L15 38" stroke="${c}" stroke-width="1.5" opacity="0.4"/></g></svg>`});}
function mpop(m){const r=rl(m),rw=m.reward_sek?`${Number(m.reward_sek).toLocaleString()} SEK`:'—',cat=(m.category||'Mission').replace(/_/g,' ');return `<div class="mpop"><div class="mpop-cat">${cat}</div><div class="mpop-title">${(m.title||'').replace(/</g,'&lt;')}</div><div class="mpop-meta"><div class="mpop-item"><div class="mpop-lbl">Reward</div><div class="mpop-val mpop-reward">${rw}</div></div><div class="mpop-item"><div class="mpop-lbl">Status</div><div class="mpop-val">Open</div></div></div><div class="mpop-risk"><span class="rb ${r}">${RISK[r].l}</span><span class="rh">${RISK[r].h}</span></div></div>`;}
async function loadMissions(){
try{
const r=await fetch('https://www.quixzoom.com/api/qz/missions?limit=200&status=open');
const d=await r.json();
(Array.isArray(d)?d:(d.missions||[])).forEach(m=>{
if(!m.lat||!m.lng)return;
L.marker([m.lat,m.lng],{icon:pin(m.status)}).bindPopup(mpop(m),{maxWidth:280,minWidth:260}).addTo(map);
});
}catch(e){console.warn('Missions unavailable:',e);}
}
loadMissions();
</script>
</body>
</html>