bae705aa97
- 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
663 lines
20 KiB
JavaScript
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`);
|
|
}
|