Files
boc/quixzoom-capture-pipeline/test-data/synthetic-city.js
T
Bernt bae705aa97 ARCHITECTURE: NFC roadmap, edge AI, audit logging
- Add NFC ePassport roadmap (ICAO 9303, eIDAS)
- Add TensorFlow.js edge face detection (BlazeFace)
- Add structured audit logger (GDPR-compliant)
- Risk scoring support

Part of KYC Apple Native UX v1.1.0
2026-06-29 16:24:48 +00:00

663 lines
20 KiB
JavaScript

/**
* QUIXZOOM Synthetic City Generator
*
* Genererar realistisk stad med korrekt spatial kontext:
* - Gatlyktor längs vägar
* - Skyltar vid korsningar
* - Brunnar i körfält
* - Träd längs trottoarer
* - Elskåp nära belysning
*
* Detta gör att AI kan använda kontext för identitetsmatchning.
* En "gatlykta mitt i en sjö" blir ett tydligt fel.
*/
class SyntheticCity {
constructor(name, sizeKm = 10) {
this.name = name;
this.sizeKm = sizeKm;
this.roads = [];
this.intersections = [];
this.objects = [];
this.observations = [];
this.zoomers = [];
// Konstanter
this.BLOCK_SIZE = 0.2; // km (200m kvarter)
this.ROAD_WIDTH = 0.008; // ~8m väg
this.SIDEWALK_WIDTH = 0.003; // ~3m trottoar
this.KM_TO_DEG = 0.009; // ca 1km = 0.009 grader
}
/**
* ============================================================
* GENERERA STAD
* ============================================================
*/
generate() {
console.log(`[CITY] Generating ${this.name} (${this.sizeKm}x${this.sizeKm} km)...`);
// 1. Generera vägnät (grid)
this.generateRoads();
console.log(`[CITY] ${this.roads.length} roads`);
// 2. Hitta korsningar
this.findIntersections();
console.log(`[CITY] ${this.intersections.length} intersections`);
// 3. Placera objekt längs vägar
this.placeStreetLamps();
this.placeTrafficSigns();
this.placeManholes();
this.placeTrees();
this.placeUtilityBoxes();
console.log(`[CITY] ${this.objects.length} objects placed`);
// 4. Generera Zoomers
this.generateZoomers(100);
// 5. Generera observationer
this.generateObservations();
return this.getStats();
}
/**
* ============================================================
* VÄGNÄT
* ============================================================
*/
generateRoads() {
const blocks = Math.floor(this.sizeKm / this.BLOCK_SIZE);
// Horisontella vägar
for (let i = 0; i <= blocks; i++) {
const y = i * this.BLOCK_SIZE;
this.roads.push({
id: `road_h_${i}`,
type: 'horizontal',
start: { x: 0, y },
end: { x: this.sizeKm, y },
width: this.ROAD_WIDTH,
lanes: 2,
hasSidewalk: true,
});
}
// Vertikala vägar
for (let i = 0; i <= blocks; i++) {
const x = i * this.BLOCK_SIZE;
this.roads.push({
id: `road_v_${i}`,
type: 'vertical',
start: { x, y: 0 },
end: { x, y: this.sizeKm },
width: this.ROAD_WIDTH,
lanes: 2,
hasSidewalk: true,
});
}
}
findIntersections() {
const horizontal = this.roads.filter(r => r.type === 'horizontal');
const vertical = this.roads.filter(r => r.type === 'vertical');
for (const h of horizontal) {
for (const v of vertical) {
this.intersections.push({
id: `intersection_${h.id}_${v.id}`,
x: v.start.x,
y: h.start.y,
roads: [h.id, v.id],
});
}
}
}
/**
* ============================================================
* PLACERA OBJEKT (REALISTISKT)
* ============================================================
*/
kmToLatLng(x, y) {
// Konvertera km till lat/lng (approximation)
return {
lat: 59.3 + y * this.KM_TO_DEG, // Centrerat runt Stockholm
lng: 18.0 + x * this.KM_TO_DEG,
};
}
placeStreetLamps() {
// Gatlyktor längs vägar, var 30e meter
const spacing = 0.03; // 30m
for (const road of this.roads) {
const length = this.roadLength(road);
const count = Math.floor(length / spacing);
for (let i = 1; i < count; i++) {
const t = i / count;
const point = this.pointOnRoad(road, t);
// Placera på trottoaren (växlar sida)
const side = i % 2 === 0 ? 1 : -1;
const offset = this.ROAD_WIDTH / 2 + this.SIDEWALK_WIDTH / 2;
const x = point.x + (road.type === 'horizontal' ? 0 : side * offset);
const y = point.y + (road.type === 'vertical' ? 0 : side * offset);
this.objects.push({
id: `street_lamp_${this.objects.length}`,
type: 'street_lamp',
location: this.kmToLatLng(x, y),
roadId: road.id,
position: t,
side,
attributes: {
height: 7 + Math.random() * 3,
material: Math.random() > 0.3 ? 'steel' : 'aluminum',
paint: ['grey', 'black', 'green'][Math.floor(Math.random() * 3)],
light: Math.random() > 0.95 ? 'broken' : 'working',
rust: Math.random() > 0.9,
lean: Math.random() > 0.95 ? Math.random() * 3 : 0,
},
});
}
}
}
placeTrafficSigns() {
// Skyltar vid korsningar och längs vägar
for (const intersection of this.intersections) {
// 80% chans för skylt vid korsning
if (Math.random() > 0.2) {
const signTypes = ['speed_limit', 'stop', 'yield', 'pedestrian', 'parking', 'direction'];
const x = intersection.x + (Math.random() - 0.5) * 0.01;
const y = intersection.y + (Math.random() - 0.5) * 0.01;
this.objects.push({
id: `traffic_sign_${this.objects.length}`,
type: 'traffic_sign',
location: this.kmToLatLng(x, y),
intersectionId: intersection.id,
attributes: {
signType: signTypes[Math.floor(Math.random() * signTypes.length)],
height: 2 + Math.random() * 1,
reflective: Math.random() > 0.1,
damaged: Math.random() > 0.95,
},
});
}
}
// Hastighetsbegränsning var 200m
for (const road of this.roads) {
const length = this.roadLength(road);
const count = Math.floor(length / 0.2);
for (let i = 1; i < count; i++) {
if (Math.random() > 0.5) continue;
const t = i / count;
const point = this.pointOnRoad(road, t);
const side = Math.random() > 0.5 ? 1 : -1;
const offset = this.ROAD_WIDTH / 2 + 0.005;
const x = point.x + (road.type === 'horizontal' ? 0 : side * offset);
const y = point.y + (road.type === 'vertical' ? 0 : side * offset);
this.objects.push({
id: `traffic_sign_${this.objects.length}`,
type: 'traffic_sign',
location: this.kmToLatLng(x, y),
roadId: road.id,
attributes: {
signType: 'speed_limit',
speed: [30, 50, 70][Math.floor(Math.random() * 3)],
height: 2.5,
reflective: true,
},
});
}
}
}
placeManholes() {
// Brunnar i körfält, var 50e meter
const spacing = 0.05;
for (const road of this.roads) {
const length = this.roadLength(road);
const count = Math.floor(length / spacing);
for (let i = 1; i < count; i++) {
const t = i / count;
const point = this.pointOnRoad(road, t);
// I vägen (inte på trottoaren)
const laneOffset = (Math.random() - 0.5) * this.ROAD_WIDTH * 0.6;
const x = point.x + (road.type === 'horizontal' ? laneOffset : 0);
const y = point.y + (road.type === 'vertical' ? laneOffset : 0);
this.objects.push({
id: `manhole_${this.objects.length}`,
type: 'manhole',
location: this.kmToLatLng(x, y),
roadId: road.id,
attributes: {
diameter: 0.5 + Math.random() * 0.3,
material: ['cast_iron', 'concrete'][Math.floor(Math.random() * 2)],
condition: Math.random() > 0.9 ? 'damaged' : 'good',
},
});
}
}
}
placeTrees() {
// Träd längs trottoarer, var 10-15 meter
for (const road of this.roads) {
const length = this.roadLength(road);
let pos = 0.01;
while (pos < length) {
pos += 0.01 + Math.random() * 0.005; // 10-15m
const t = pos / length;
const point = this.pointOnRoad(road, t);
const side = Math.random() > 0.5 ? 1 : -1;
const offset = this.ROAD_WIDTH / 2 + this.SIDEWALK_WIDTH + 0.002;
const x = point.x + (road.type === 'horizontal' ? 0 : side * offset);
const y = point.y + (road.type === 'vertical' ? 0 : side * offset);
this.objects.push({
id: `tree_${this.objects.length}`,
type: 'tree',
location: this.kmToLatLng(x, y),
roadId: road.id,
attributes: {
species: ['oak', 'pine', 'birch', 'maple'][Math.floor(Math.random() * 4)],
height: 3 + Math.random() * 12,
diameter: 0.2 + Math.random() * 0.8,
health: Math.random() > 0.9 ? 'poor' : 'good',
},
});
}
}
}
placeUtilityBoxes() {
// Elskåp nära gatlyktor eller vid korsningar
for (let i = 0; i < this.objects.length; i++) {
const obj = this.objects[i];
if (obj.type === 'street_lamp' && Math.random() > 0.7) {
// 30% chans för elskåp nära gatlykta
const x = (obj.location.lng - 18.0) / this.KM_TO_DEG + (Math.random() - 0.5) * 0.005;
const y = (obj.location.lat - 59.3) / this.KM_TO_DEG + (Math.random() - 0.5) * 0.005;
this.objects.push({
id: `utility_box_${this.objects.length}`,
type: 'utility_box',
location: this.kmToLatLng(x, y),
roadId: obj.roadId,
nearLamp: obj.id,
attributes: {
type: ['electric', 'telecom', 'traffic'][Math.floor(Math.random() * 3)],
height: 1 + Math.random() * 0.5,
condition: Math.random() > 0.85 ? 'damaged' : 'good',
},
});
}
}
}
/**
* ============================================================
* GENERERA OBSERVATIONER
* ============================================================
*/
generateObservations() {
// Generera 5 observationer per objekt (olika Zoomers, tider, väder)
for (const obj of this.objects) {
const numObservations = 1 + Math.floor(Math.random() * 5);
for (let i = 0; i < numObservations; i++) {
const zoomer = this.zoomers[Math.floor(Math.random() * this.zoomers.length)];
this.observations.push({
id: `obs_${obj.id}_${i}`,
objectId: obj.id,
objectType: obj.type,
zoomerId: zoomer.id,
location: {
// GPS-variation ±1-3 meter
lat: obj.location.lat + (Math.random() - 0.5) * 0.00003,
lng: obj.location.lng + (Math.random() - 0.5) * 0.00003,
},
gpsAccuracy: 1 + Math.random() * 2,
timestamp: this.randomDate(2025, 2026),
attributes: { ...obj.attributes },
// Simulera förändringar över tid
weather: ['clear', 'cloudy', 'rain', 'fog'][Math.floor(Math.random() * 4)],
timeOfDay: ['day', 'evening', 'night'][Math.floor(Math.random() * 3)],
device: zoomer.device,
angle: Math.random() * 360,
quality: {
blur: Math.random() * 0.2,
exposure: 0.5 + Math.random() * 0.5,
},
});
}
}
}
generateZoomers(count) {
for (let i = 0; i < count; i++) {
this.zoomers.push({
id: `zoomer_${i.toString().padStart(3, '0')}`,
device: {
type: ['iPhone', 'Android'][Math.floor(Math.random() * 2)],
model: ['iPhone 15', 'Samsung S24', 'Pixel 8'][Math.floor(Math.random() * 3)],
},
level: ['supplementary', 'active', 'professional'][Math.floor(Math.random() * 3)],
});
}
}
/**
* ============================================================
* BENCHMARK DATASETS
* ============================================================
*/
generateDatasetA() {
// Dataset A: Perfekt scenario
// Samma gatlykta fotograferad 20 gånger
const lamp = this.objects.find(o => o.type === 'street_lamp');
const observations = [];
for (let i = 0; i < 20; i++) {
observations.push({
id: `obs_A_${i}`,
objectType: 'street_lamp',
location: {
lat: lamp.location.lat + (Math.random() - 0.5) * 0.00003, // ±1.5m
lng: lamp.location.lng + (Math.random() - 0.5) * 0.00003,
},
gpsAccuracy: 1 + Math.random() * 2,
timestamp: new Date(2025, 0, 1 + i * 15), // Var 15e dag
attributes: {
...lamp.attributes,
// Små förändringar över tid
rust: i > 10 ? true : lamp.attributes.rust,
paint: i > 15 ? 'faded' : lamp.attributes.paint,
},
weather: ['clear', 'cloudy', 'rain'][i % 3],
timeOfDay: ['day', 'evening', 'night'][i % 3],
device: { type: 'iPhone', model: `iPhone ${12 + (i % 4)}` },
angle: Math.random() * 360,
});
}
return {
name: 'Dataset A - Perfect Scenario',
description: 'Same lamp, 20 observations, varying conditions',
expectedObjects: 1,
expectedEvidence: 20,
observations,
};
}
generateDatasetB() {
// Dataset B: Svårt scenario
// Två identiska gatlyktor 6 meter från varandra
const road = this.roads[0];
const point = this.pointOnRoad(road, 0.5);
const baseLoc = this.kmToLatLng(point.x, point.y);
const lamp1 = {
id: 'lamp_B1',
type: 'street_lamp',
location: {
lat: baseLoc.lat,
lng: baseLoc.lng - 0.00003, // 3m väster
},
attributes: {
height: 8.0,
material: 'steel',
paint: 'grey',
light: 'working',
},
};
const lamp2 = {
id: 'lamp_B2',
type: 'street_lamp',
location: {
lat: baseLoc.lat,
lng: baseLoc.lng + 0.00003, // 3m öster (totalt 6m mellan)
},
attributes: {
height: 8.0,
material: 'steel',
paint: 'grey',
light: 'working',
},
};
const observations = [
{
id: 'obs_B1',
objectType: 'street_lamp',
location: {
lat: lamp1.location.lat + (Math.random() - 0.5) * 0.00001,
lng: lamp1.location.lng + (Math.random() - 0.5) * 0.00001,
},
gpsAccuracy: 2,
attributes: lamp1.attributes,
},
{
id: 'obs_B2',
objectType: 'street_lamp',
location: {
lat: lamp2.location.lat + (Math.random() - 0.5) * 0.00001,
lng: lamp2.location.lng + (Math.random() - 0.5) * 0.00001,
},
gpsAccuracy: 2,
attributes: lamp2.attributes,
},
];
return {
name: 'Dataset B - Difficult Scenario',
description: 'Two identical lamps 6m apart, should NOT merge',
expectedObjects: 2,
expectedEvidence: 2,
observations,
};
}
generateDatasetC() {
// Dataset C: Förändring över tid
const lamp = this.objects.find(o => o.type === 'street_lamp');
const observations = [];
const changes = [
{ month: 0, paint: 'grey', rust: false, light: 'working', lean: 0 },
{ month: 3, paint: 'grey', rust: false, light: 'working', lean: 0.5 },
{ month: 6, paint: 'faded', rust: true, light: 'flickering', lean: 1 },
{ month: 9, paint: 'faded', rust: true, light: 'broken', lean: 2 },
{ month: 12, paint: 'new_grey', rust: false, light: 'working', lean: 0 },
];
for (const change of changes) {
observations.push({
id: `obs_C_${change.month}`,
objectType: 'street_lamp',
location: {
lat: lamp.location.lat + (Math.random() - 0.5) * 0.00002,
lng: lamp.location.lng + (Math.random() - 0.5) * 0.00002,
},
gpsAccuracy: 1.5,
timestamp: new Date(2025, change.month, 15),
attributes: {
height: lamp.attributes.height,
material: lamp.attributes.material,
...change,
},
});
}
return {
name: 'Dataset C - Change Over Time',
description: 'Same lamp with gradual changes, then repaired',
expectedObjects: 1,
expectedEvidence: 5,
observations,
};
}
generateDatasetD() {
// Dataset D: Partiellt skymt
const lamp = this.objects.find(o => o.type === 'street_lamp');
return {
name: 'Dataset D - Partial Occlusion',
description: 'Only 30% of lamp visible',
expectedObjects: 1,
expectedEvidence: 1,
observations: [{
id: 'obs_D1',
objectType: 'street_lamp',
location: {
lat: lamp.location.lat + 0.00002,
lng: lamp.location.lng + 0.00002,
},
gpsAccuracy: 2,
attributes: {
...lamp.attributes,
visiblePortion: 0.3,
occludedBy: 'bus',
},
}],
};
}
generateDatasetE() {
// Dataset E: Flera Zoomers
const lamp = this.objects.find(o => o.type === 'street_lamp');
const observations = [];
for (let i = 0; i < 5; i++) {
observations.push({
id: `obs_E_${i}`,
objectType: 'street_lamp',
location: {
lat: lamp.location.lat + (Math.random() - 0.5) * 0.00004,
lng: lamp.location.lng + (Math.random() - 0.5) * 0.00004,
},
gpsAccuracy: 1.5 + Math.random(),
timestamp: new Date(2025, i, 1 + i * 7),
attributes: lamp.attributes,
zoomerId: `zoomer_${i}`,
device: { type: 'iPhone', model: `iPhone ${14 + i}` },
});
}
return {
name: 'Dataset E - Multiple Zoomers',
description: 'Five zoomers document same street over different days',
expectedObjects: 1,
expectedEvidence: 5,
observations,
};
}
/**
* ============================================================
* HJÄLPMETODER
* ============================================================
*/
roadLength(road) {
if (road.type === 'horizontal') {
return road.end.x - road.start.x;
} else {
return road.end.y - road.start.y;
}
}
pointOnRoad(road, t) {
return {
x: road.start.x + (road.end.x - road.start.x) * t,
y: road.start.y + (road.end.y - road.start.y) * t,
};
}
randomDate(startYear, endYear) {
const start = new Date(startYear, 0, 1).getTime();
const end = new Date(endYear, 11, 31).getTime();
return new Date(start + Math.random() * (end - start));
}
getStats() {
const byType = {};
for (const obj of this.objects) {
byType[obj.type] = (byType[obj.type] || 0) + 1;
}
return {
name: this.name,
size: `${this.sizeKm}x${this.sizeKm} km`,
roads: this.roads.length,
intersections: this.intersections.length,
objects: {
total: this.objects.length,
byType,
},
observations: this.observations.length,
zoomers: this.zoomers.length,
};
}
}
// Exportera
module.exports = SyntheticCity;
// Demo
if (require.main === module) {
const city = new SyntheticCity('TestCity', 5);
const stats = city.generate();
console.log('\n=== SYNTHETIC CITY STATS ===');
console.log(JSON.stringify(stats, null, 2));
console.log('\n=== DATASET A (Perfect) ===');
const datasetA = city.generateDatasetA();
console.log(`${datasetA.name}: ${datasetA.observations.length} observations`);
console.log('\n=== DATASET B (Difficult) ===');
const datasetB = city.generateDatasetB();
console.log(`${datasetB.name}: ${datasetB.observations.length} observations`);
console.log('\n=== DATASET C (Change Over Time) ===');
const datasetC = city.generateDatasetC();
console.log(`${datasetC.name}: ${datasetC.observations.length} observations`);
console.log('\n=== DATASET D (Partial Occlusion) ===');
const datasetD = city.generateDatasetD();
console.log(`${datasetD.name}: ${datasetD.observations.length} observations`);
console.log('\n=== DATASET E (Multiple Zoomers) ===');
const datasetE = city.generateDatasetE();
console.log(`${datasetE.name}: ${datasetE.observations.length} observations`);
}