Files
boc/landvex-all/label.html
T
Bernt 6989a98d75 feat: Passwordless cross-device authentication
- Arkitektur: docs/auth/passwordless-architecture.md
- Backend: iom/quixzoom-auth-service/ (FastAPI + Redis)
- Webb: quixzoom-market-pages/se/login/ (QR-kod + polling)
- App: iom/quixzoom-app/src/features/auth/ (push + deep links)

Flöde: QR-kod → app-godkännande → webb-inloggad
2026-07-07 07:11:50 +00:00

393 lines
17 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>
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Landvex — Vision Training</title>
<link rel="stylesheet" href="/assets/css/landvex-complete.css">
<style>
*{box-sizing:border-box;margin:0;padding:0}
:root{--bg:#060a12;--s1:#0b1220;--s2:#0f1929;--s3:#152236;--border: rgba(0,0,0,0.08);--text:#dce8f5;--t2:rgba(220,232,245,0.55);--accent:#1d6fa4;--accent3:#4aafef}
html,body{min-height:100%;font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text',sans-serif;background:var(--bg);color:var(--text)}
#app{max-width:960px;margin:0 auto;padding:16px}
h1{font-size:22px;font-weight:800;letter-spacing:-.5px;margin-bottom:4px}
.sub{font-size:12px;color:var(--t2);margin-bottom:24px}
#drop-zone{border:2px dashed rgba(0,0,0,0.15);border-radius: var(--radius-md);padding:40px;text-align:center;cursor:pointer;transition:border-color 0.2s;margin-bottom:20px}
#drop-zone:hover,#drop-zone.drag{border-color:var(--accent3)}
#drop-zone p{color:var(--t2);font-size:13px}
#preview-wrap{display:none;position:relative;margin-bottom: 24px}
#preview{max-width:100%;border-radius: var(--radius-md);display:block}
#canvas{position:absolute;top:0;left:0;cursor:crosshair}
#labels-panel{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap: 16px;margin-bottom: 24px}
.label-btn{padding: 24px;border-radius: var(--radius-md);border:1px solid var(--border);background:var(--s2);cursor:pointer;font-size:12px;font-weight:600;color:var(--t2);transition:all 0.12s;display:flex;align-items:center;gap: 16px}
.label-btn:hover{background:var(--s3);color:var(--text)}
.label-btn.active{border-color:currentColor}
.label-btn[data-cat="electrical_hazard"].active{color:#f87171;background:rgba(248,113,113,0.1)}
.label-btn[data-cat="cable_disorder"].active{color:#fb923c;background:rgba(251,146,60,0.1)}
.label-btn[data-cat="graffiti"].active{color:#a78bfa;background:rgba(167,139,250,0.1)}
.label-btn[data-cat="pothole"].active{color:#fbbf24;background:rgba(251,191,36,0.1)}
.label-btn[data-cat="litter"].active{color:#94a3b8;background:rgba(148,163,184,0.1)}
.label-btn[data-cat="vacancy"].active{color:#f87171;background:rgba(248,113,113,0.1)}
.label-btn[data-cat="construction"].active{color:#4ade80;background:rgba(74,222,128,0.1)}
.label-btn[data-cat="commercial_active"].active{color:#38bdf8;background:rgba(56,189,248,0.1)}
.label-btn[data-cat="good_condition"].active{color:#4ade80;background:rgba(74,222,128,0.1)}
.label-btn[data-cat="infrastructure_ok"].active{color:#4ade80;background:rgba(74,222,128,0.1)}
#scores{display:grid;grid-template-columns:1fr;gap: 16px;margin-bottom: 24px}
@media(min-width:560px){#scores{grid-template-columns:1fr 1fr 1fr}}
.score-group{background:var(--s2);border-radius: var(--radius-md);padding: 24px}
.score-group label{font-size:9px;font-weight:700;letter-spacing:.6px;text-transform:uppercase;color:var(--t2);display:block;margin-bottom:6px}
.score-group input[type=range]{width:100%;accent-color:var(--accent3)}
.score-val{font-size:14px;font-weight:800;font-family:monospace;color:var(--accent3)}
#boxes-list{margin-bottom: 24px;font-size:11px;color:var(--t2)}
.box-item{display:flex;align-items:center;justify-content:space-between;padding: 24px;background:var(--s2);border-radius: var(--radius-sm);margin-bottom:4px}
.box-del{color:#f87171;cursor:pointer;font-size:14px;background:none;border:none;padding:0 4px}
.btn{padding: 24px;border-radius: var(--radius-md);border:none;cursor:pointer;font-size:13px;font-weight:700}
.btn-primary{background:var(--accent);color: #1d1d1f}
.btn-secondary{background:var(--s2);color:var(--text);border:1px solid var(--border)}
#actions{display:flex;flex-wrap:wrap;gap: 16px;margin-bottom:20px}
#output{background:var(--s2);border-radius: var(--radius-md);padding:14px;font-size:11px;font-family:monospace;white-space:pre-wrap;color:#4ade80;display:none;max-height:300px;overflow-y:auto}
#file-input{display:none}
.contradiction-row{display:flex;flex-wrap:wrap;align-items:center;gap: 16px;margin-bottom: 24px;background:var(--s2);border-radius: var(--radius-md);padding: 24px}
.contradiction-row label{font-size:11px;font-weight:600;color:var(--t2);flex:1}
.contradiction-row select{background:var(--s3);color:var(--text);border:1px solid var(--border);border-radius: var(--radius-sm);padding: 24px;font-size:11px}
@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>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": [
"Organization",
"Corporation"
],
"@id": "https://landvex.com/#organization",
"name": "Landvex",
"alternateName": [
"LandveX",
"Landvex AB",
"LandveX AB"
],
"url": "https://landvex.com",
"logo": {
"@type": "ImageObject",
"url": "https://landvex.com/apple-touch-icon.png",
"width": 180,
"height": 180
},
"foundingDate": "2024",
"foundingLocation": {
"@type": "Place",
"addressLocality": "Gothenburg",
"addressRegion": "Gothenburg",
"addressCountry": "SE"
},
"legalName": "Landvex Inc",
"address": {
"@type": "PostalAddress",
"addressLocality": "Houston",
"addressRegion": "TX",
"addressCountry": "US"
},
"location": [
{
"@type": "Place",
"name": "Houston, Texas, USA (US HQ)",
"address": {
"@type": "PostalAddress",
"addressLocality": "Houston",
"addressRegion": "TX",
"addressCountry": "US"
}
},
{
"@type": "Place",
"name": "Tyresö, Sweden (EU HQ)",
"address": {
"@type": "PostalAddress",
"addressLocality": "Gothenburg",
"addressRegion": "Gothenburg",
"addressCountry": "SE"
}
}
],
"taxID": "pending-EIN",
"description": "Landvex is a decision intelligence company that identifies contradictions between official narratives and observed physical reality. Using the quiXzoom field observation network and the AMOS AI analysis engine, Landvex delivers infrastructure risk indexes, urban intelligence scores, and contradiction reports to infrastructure owners, municipalities, enterprises, and investors globally.",
"disambiguatingDescription": "Landvex (LandveX) is a decision intelligence company with US headquarters in Houston, Texas (Landvex Inc.) and European headquarters in Tyresö, Sweden (Landvex AB, org.nr 559141-7042). Landvex delivers control intelligence — infrastructure risk scores, urban intelligence, and contradiction analysis — to municipalities, enterprises, infrastructure operators and investors. Landvex is not a UK meat wholesaler, food distributor, logistics company, package tracker, or land investment platform.",
"knowsAbout": [
"control intelligence",
"decision intelligence",
"infrastructure risk assessment",
"urban intelligence",
"field data collection",
"contradiction detection",
"physical world analytics",
"geospatial analytics"
],
"brand": {
"@type": "Brand",
"name": "Landvex",
"slogan": "Control intelligence for the physical world."
},
"sameAs": [
"https://landvex.com",
"https://www.linkedin.com/company/landvex",
"https://x.com/landvex",
"https://twitter.com/landvex",
"https://www.crunchbase.com/organization/landvex",
"https://github.com/landvex",
"https://www.instagram.com/landvex"
],
"contactPoint": {
"@type": "ContactPoint",
"email": "contact@landvex.com",
"contactType": "sales"
},
"slogan": "Where reported reality conflicts with observed reality."
}
</script>
</head>
<body>
<div id="app">
<h1>Landvex Vision Training</h1>
<p class="sub">Label images for quiXzoom infrastructure intelligence training. Draw bounding boxes, select categories, export JSON.</p>
<div id="drop-zone" onclick="document.getElementById('file-input').click()">
<p>Drop image here or click to upload</p>
<p>JPEG / PNG · max 20MB</p>
</div>
<input type="file" id="file-input" accept="image/*">
<div id="preview-wrap">
<img loading="lazy" loading="lazy" loading="lazy" loading="lazy" id="preview" alt="Label preview"/>
<canvas id="canvas"></canvas>
</div>
<div id="labels-panel">
<button class="label-btn" data-cat="electrical_hazard" onclick="toggleLabel(this)"> Electrical hazard</button>
<button class="label-btn" data-cat="cable_disorder" onclick="toggleLabel(this)"> Cable disorder</button>
<button class="label-btn" data-cat="graffiti" onclick="toggleLabel(this)"> Graffiti</button>
<button class="label-btn" data-cat="pothole" onclick="toggleLabel(this)"> Pothole</button>
<button class="label-btn" data-cat="litter" onclick="toggleLabel(this)"> Litter/Waste</button>
<button class="label-btn" data-cat="vacancy" onclick="toggleLabel(this)"> Vacancy</button>
<button class="label-btn" data-cat="construction" onclick="toggleLabel(this)"> Construction</button>
<button class="label-btn" data-cat="commercial_active" onclick="toggleLabel(this)"> Commercial active</button>
<button class="label-btn" data-cat="good_condition" onclick="toggleLabel(this)"> Good condition</button>
<button class="label-btn" data-cat="infrastructure_ok" onclick="toggleLabel(this)"> Infra OK</button>
</div>
<div id="scores">
<div class="score-group">
<label>Infrastructure Risk <span class="score-val" id="risk-val">5</span>/10</label>
<input type="range" min="0" max="10" value="5" step="0.5" id="risk-slider" oninput="document.getElementById('risk-val').textContent=this.value">
</div>
<div class="score-group">
<label>Condition Score <span class="score-val" id="cond-val">5</span>/10</label>
<input type="range" min="0" max="10" value="5" step="0.5" id="cond-slider" oninput="document.getElementById('cond-val').textContent=this.value">
</div>
<div class="score-group">
<label>Contradiction Potential <span class="score-val" id="contra-val">0.0</span></label>
<input type="range" min="0" max="1" value="0" step="0.1" id="contra-slider" oninput="document.getElementById('contra-val').textContent=parseFloat(this.value).toFixed(1)">
</div>
</div>
<div class="contradiction-row">
<label>Official data says:</label>
<select id="official-status">
<option value="approved">Infrastructure approved</option>
<option value="low_risk">Low risk</option>
<option value="moderate">Moderate risk</option>
<option value="high_risk">High risk</option>
<option value="unknown">Unknown</option>
</select>
<label>Vacancy status:</label>
<select id="vacancy-status">
<option value="occupied">Occupied</option>
<option value="vacant">Vacant</option>
<option value="uncertain">Uncertain</option>
</select>
</div>
<div id="boxes-list"></div>
<div id="actions">
<button class="lv-btn lv-btn--primary" onclick="exportLabel()">Export JSON</button>
<button class="btn btn-secondary" onclick="clearAll()">Clear</button>
<input type="text" id="lat-input" placeholder="lat">
<input type="text" id="lng-input" placeholder="lng">
<input type="text" id="city-input" placeholder="city">
<input type="text" id="district-input" placeholder="district">
</div>
<pre id="output"></pre>
</div>
<script>
let img=null, boxes=[], drawing=false, startX=0, startY=0;
let activeLabels=new Set();
let currentBox=null;
const canvas=document.getElementById('canvas');
const ctx=canvas.getContext('2d');
const preview=document.getElementById('preview');
const wrap=document.getElementById('preview-wrap');
// File input
document.getElementById('file-input').addEventListener('change',e=>{
const f=e.target.files[0]; if(!f)return;
const r=new FileReader();
r.onload=ev=>loadImage(ev.target.result);
r.readAsDataURL(f);
});
// Drag & drop
const dz=document.getElementById('drop-zone');
dz.addEventListener('dragover',e=>{e.preventDefault();dz.classList.add('drag')});
dz.addEventListener('dragleave',()=>dz.classList.remove('drag'));
dz.addEventListener('drop',e=>{
e.preventDefault();dz.classList.remove('drag');
const f=e.dataTransfer.files[0]; if(!f)return;
const r=new FileReader();
r.onload=ev=>loadImage(ev.target.result);
r.readAsDataURL(f);
});
function loadImage(src){
img=new Image();
img.onload=()=>{
preview.src=src;
wrap.style.display='block';
dz.style.display='none';
setTimeout(()=>{
canvas.width=preview.offsetWidth;
canvas.height=preview.offsetHeight;
redraw();
},100);
};
img.src=src;
}
// Bounding box drawing
canvas.addEventListener('mousedown',e=>{
const r=canvas.getBoundingClientRect();
startX=e.clientX-r.left; startY=e.clientY-r.top;
drawing=true; currentBox={x:startX,y:startY,w:0,h:0,labels:[...activeLabels]};
});
canvas.addEventListener('mousemove',e=>{
if(!drawing)return;
const r=canvas.getBoundingClientRect();
currentBox.w=(e.clientX-r.left)-startX;
currentBox.h=(e.clientY-r.top)-startY;
redraw();
});
canvas.addEventListener('mouseup',()=>{
if(!drawing)return; drawing=false;
if(Math.abs(currentBox.w)>10&&Math.abs(currentBox.h)>10){
boxes.push({...currentBox});
updateBoxList();
}
currentBox=null; redraw();
});
function redraw(){
ctx.clearRect(0,0,canvas.width,canvas.height);
boxes.forEach((b,i)=>{
ctx.strokeStyle='#4aafef'; ctx.lineWidth=2;
ctx.strokeRect(b.x,b.y,b.w,b.h);
ctx.fillStyle='rgba(74,175,239,0.15)';
ctx.fillRect(b.x,b.y,b.w,b.h);
ctx.fillStyle='#4aafef'; ctx.font='10px monospace';
ctx.fillText(b.labels.join(',')||'unlabeled',b.x+3,b.y+12);
});
if(currentBox){
ctx.strokeStyle='#fbbf24'; ctx.lineWidth=1.5; ctx.setLineDash([4,3]);
ctx.strokeRect(currentBox.x,currentBox.y,currentBox.w,currentBox.h);
ctx.setLineDash([]);
}
}
function updateBoxList(){
const el=document.getElementById('boxes-list');
el.innerHTML=boxes.map((b,i)=>`
<div class="box-item">
<span>Box ${i+1}: ${b.labels.join(', ')||'unlabeled'} (${Math.abs(Math.round(b.w))}×${Math.abs(Math.round(b.h))}px)</span>
<button class="box-del" onclick="deleteBox(${i})">×</button>
</div>`).join('');
}
function deleteBox(i){ boxes.splice(i,1); updateBoxList(); redraw(); }
function toggleLabel(btn){
const cat=btn.dataset.cat;
if(activeLabels.has(cat)){activeLabels.delete(cat);btn.classList.remove('active');}
else{activeLabels.add(cat);btn.classList.add('active');}
}
function exportLabel(){
const scaleX=img?img.naturalWidth/canvas.width:1;
const scaleY=img?img.naturalHeight/canvas.height:1;
const tags=[...document.querySelectorAll('.label-btn.active')].map(b=>b.dataset.cat);
const out={
observation_id:`qx-label-${Date.now()}`,
lat:parseFloat(document.getElementById('lat-input').value)||null,
lng:parseFloat(document.getElementById('lng-input').value)||null,
city:document.getElementById('city-input').value||'unknown',
district:document.getElementById('district-input').value||'unknown',
timestamp:new Date().toISOString(),
manual_labels:{
tags,
infrastructure_risk:parseFloat(document.getElementById('risk-slider').value),
condition_score:parseFloat(document.getElementById('cond-slider').value),
contradiction_potential:parseFloat(document.getElementById('contra-slider').value),
vacancy_status:document.getElementById('vacancy-status').value,
official_status:document.getElementById('official-status').value,
},
bounding_boxes:boxes.map((b,i)=>({
id:i+1,
labels:b.labels,
bbox_normalized:[
Math.round(Math.min(b.x,b.x+b.w)*scaleX),
Math.round(Math.min(b.y,b.y+b.h)*scaleY),
Math.round(Math.abs(b.w)*scaleX),
Math.round(Math.abs(b.h)*scaleY)
]
})),
source_first:{type:'manual_label',labeled_by:'human',pipeline:'landvex-vision-v1'}
};
const el=document.getElementById('output');
el.style.display='block';
el.textContent=JSON.stringify(out,null,2);
// Auto-download
const blob=new Blob([JSON.stringify(out,null,2)],{type:'application/json'});
const a=document.createElement('a');
a.href=URL.createObjectURL(blob);
a.download=`label-${out.observation_id}.json`;
a.click();
}
function clearAll(){
boxes=[]; activeLabels.clear();
document.querySelectorAll('.label-btn.active').forEach(b=>b.classList.remove('active'));
document.getElementById('risk-slider').value=5;
document.getElementById('cond-slider').value=5;
document.getElementById('contra-slider').value=0;
document.getElementById('risk-val').textContent='5';
document.getElementById('cond-val').textContent='5';
document.getElementById('contra-val').textContent='0.0';
document.getElementById('output').style.display='none';
updateBoxList(); redraw();
}
</script>
</body>
</html>