Compare commits
10 Commits
afd95698b0
...
c1fb4d67bc
| Author | SHA1 | Date |
|---|---|---|
|
|
c1fb4d67bc | |
|
|
15ae829fd6 | |
|
|
e0220a9c89 | |
|
|
5a782b51ca | |
|
|
5a0fce5374 | |
|
|
a4b150dfed | |
|
|
8320ea66b9 | |
|
|
4e51c2faad | |
|
|
75d15394c8 | |
|
|
0d2bca0a3e |
|
|
@ -9,6 +9,7 @@ Bygge en løsning med to samtidige kortvisninger over samme positionsdata, hvor
|
||||||
- Zoom og pan skal kunne ske uafhængigt i hvert kort
|
- Zoom og pan skal kunne ske uafhængigt i hvert kort
|
||||||
- Visning skal være viewport-drevet, ikke bundet til et bestemt geografisk udsnit
|
- Visning skal være viewport-drevet, ikke bundet til et bestemt geografisk udsnit
|
||||||
- UI'et skal beskytte læsbarheden, også ved meget store usikkerheder
|
- UI'et skal beskytte læsbarheden, også ved meget store usikkerheder
|
||||||
|
- Demoen skal køres via lokal webserver, ikke `file://`, fordi browser-adfærden ellers bliver upålidelig
|
||||||
|
|
||||||
## Funktionel plan
|
## Funktionel plan
|
||||||
|
|
||||||
|
|
@ -78,6 +79,12 @@ Denne vurdering skal genberegnes:
|
||||||
7. Teste zoom/pan og dynamiske overgange mellem inside/possible/outside
|
7. Teste zoom/pan og dynamiske overgange mellem inside/possible/outside
|
||||||
8. Finjustere styling og læsbarhed
|
8. Finjustere styling og læsbarhed
|
||||||
|
|
||||||
|
## Erfaring fra implementeringen
|
||||||
|
- Flere UI-problemer viste sig at hænge sammen med, at demoen blev åbnet via `file://` i stedet for en lokal webserver.
|
||||||
|
- Zoom, pan og klik-relateret feedback opfører sig mere stabilt, når siden serveres via `http://127.0.0.1:4173/...`.
|
||||||
|
- Derfor er lokal server nu en del af den forventede runtime for demoen.
|
||||||
|
- Næste hovedskridt er stadig at gå fra ét kort til to samtidige kort med samme data og forskellige usikkerhedsperspektiver.
|
||||||
|
|
||||||
## Åbne designvalg
|
## Åbne designvalg
|
||||||
- Om hjørneindikatorer skal ligge i et fast hjørne eller langs den nærmeste kant
|
- Om hjørneindikatorer skal ligge i et fast hjørne eller langs den nærmeste kant
|
||||||
- Om store usikkerheder altid skal tegnes som cirkler på kortet, eller nogle gange kun repræsenteres via indikator
|
- Om store usikkerheder altid skal tegnes som cirkler på kortet, eller nogle gange kun repræsenteres via indikator
|
||||||
|
|
|
||||||
704
kort7/app.js
704
kort7/app.js
|
|
@ -9,19 +9,13 @@ const COLORS = {
|
||||||
"Bil 10": "#00c853"
|
"Bil 10": "#00c853"
|
||||||
};
|
};
|
||||||
|
|
||||||
const map = L.map("map").setView([55.6761, 12.5683], 13);
|
|
||||||
window.__debugMap = map;
|
|
||||||
|
|
||||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
|
||||||
attribution: "© OpenStreetMap contributors",
|
|
||||||
maxZoom: 19
|
|
||||||
}).addTo(map);
|
|
||||||
|
|
||||||
const statusEl = document.getElementById("status");
|
const statusEl = document.getElementById("status");
|
||||||
const alertsEl = document.getElementById("presenceAlerts");
|
|
||||||
const selectedVehicleInfoEl = document.getElementById("selectedVehicleInfo");
|
const selectedVehicleInfoEl = document.getElementById("selectedVehicleInfo");
|
||||||
const runtimeWarningEl = document.getElementById("runtimeWarning");
|
const runtimeWarningEl = document.getElementById("runtimeWarning");
|
||||||
const offmapIndicatorsEl = document.getElementById("offmapIndicators");
|
const tileWarningEl = document.getElementById("tileWarning");
|
||||||
|
const debugPanelEl = document.getElementById("debugPanel");
|
||||||
|
const alertsApproxEl = document.getElementById("presenceAlertsApprox");
|
||||||
|
const alertsTrueEl = document.getElementById("presenceAlertsTrue");
|
||||||
const playBtn = document.getElementById("playBtn");
|
const playBtn = document.getElementById("playBtn");
|
||||||
const pauseBtn = document.getElementById("pauseBtn");
|
const pauseBtn = document.getElementById("pauseBtn");
|
||||||
const resetBtn = document.getElementById("resetBtn");
|
const resetBtn = document.getElementById("resetBtn");
|
||||||
|
|
@ -30,149 +24,22 @@ if (window.location.protocol === "file:") {
|
||||||
runtimeWarningEl.hidden = false;
|
runtimeWarningEl.hidden = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let forcedPopupVehicle = null;
|
function buildVehicleBuckets(events) {
|
||||||
|
const out = {};
|
||||||
function highlightOffmapIndicator(name) {
|
for (const e of events) {
|
||||||
const buttons = offmapIndicatorsEl.querySelectorAll('.offmap-indicator');
|
if (!out[e.vehicle]) out[e.vehicle] = [];
|
||||||
buttons.forEach((button) => {
|
out[e.vehicle].push(e);
|
||||||
button.classList.toggle('active', button.dataset.vehicle === name);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderSelectedVehicleInfo(v, sourceLabel = "kort") {
|
|
||||||
if (!v) {
|
|
||||||
selectedVehicleInfoEl.textContent = "Klik på et køretøj eller en hjørneindikator for detaljer.";
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
for (const k of Object.keys(out)) {
|
||||||
selectedVehicleInfoEl.innerHTML = `
|
out[k].sort((a, b) => a.ts - b.ts);
|
||||||
<strong>${v.name} · valgt fra ${sourceLabel}</strong>
|
|
||||||
Tid: ${fmtTs(v.current.ts)}<br>
|
|
||||||
Position: ${v.displayLat.toFixed(5)}, ${v.displayLon.toFixed(5)}<br>
|
|
||||||
Hastighed: ${v.current.speedMps} m/s<br>
|
|
||||||
Retning: ${v.displayHeading.toFixed(0)}°<br>
|
|
||||||
Usikkerhed: ${v.current.uncertaintyM} m<br>
|
|
||||||
Status: ${v.presenceStateLabel || "ja"}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function flashVehicleMarker(v) {
|
|
||||||
const markerEl = v.marker.getElement();
|
|
||||||
if (!markerEl) return;
|
|
||||||
|
|
||||||
markerEl.classList.remove("flash-marker");
|
|
||||||
void markerEl.offsetWidth;
|
|
||||||
markerEl.classList.add("flash-marker");
|
|
||||||
}
|
|
||||||
|
|
||||||
function openVehiclePopup(name) {
|
|
||||||
const v = vehicles[name];
|
|
||||||
if (!v) return;
|
|
||||||
|
|
||||||
forcedPopupVehicle = name;
|
|
||||||
highlightOffmapIndicator(name);
|
|
||||||
renderSelectedVehicleInfo(v, "hjørneindikator");
|
|
||||||
|
|
||||||
if (!map.hasLayer(v.marker)) {
|
|
||||||
v.marker.addTo(map);
|
|
||||||
}
|
}
|
||||||
|
return out;
|
||||||
const focusBounds = L.latLng(v.displayLat, v.displayLon).toBounds(Math.max(v.current.uncertaintyM * 0.6, 1200));
|
|
||||||
map.fitBounds(focusBounds, {
|
|
||||||
animate: true,
|
|
||||||
duration: 0.5,
|
|
||||||
paddingTopLeft: [290, 60],
|
|
||||||
paddingBottomRight: [80, 80],
|
|
||||||
maxZoom: 13
|
|
||||||
});
|
|
||||||
|
|
||||||
v.marker.setOpacity(1);
|
|
||||||
v.marker.setZIndexOffset(1000);
|
|
||||||
v.marker.setLatLng([v.displayLat, v.displayLon]);
|
|
||||||
setHeading(v.marker, v.displayHeading);
|
|
||||||
v.marker.setPopupContent(popupHtml(v.name, v));
|
|
||||||
|
|
||||||
window.setTimeout(() => {
|
|
||||||
v.marker.openPopup();
|
|
||||||
flashVehicleMarker(v);
|
|
||||||
}, 180);
|
|
||||||
}
|
|
||||||
|
|
||||||
function carIcon(color) {
|
|
||||||
return L.divIcon({
|
|
||||||
className: "",
|
|
||||||
iconSize: [24, 24],
|
|
||||||
iconAnchor: [12, 12],
|
|
||||||
html: `
|
|
||||||
<div class="car" data-role="car">
|
|
||||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
||||||
<path
|
|
||||||
d="M12 2 L20 22 L12 18 L4 22 Z"
|
|
||||||
fill="${color}"
|
|
||||||
stroke="#ffffff"
|
|
||||||
stroke-width="1.5"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function setHeading(marker, deg) {
|
|
||||||
const el = marker.getElement();
|
|
||||||
if (!el) return;
|
|
||||||
const car = el.querySelector("[data-role=car]");
|
|
||||||
if (!car) return;
|
|
||||||
car.style.transform = `rotate(${deg}deg)`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function fmtTs(ts) {
|
function fmtTs(ts) {
|
||||||
return `${ts}s`;
|
return `${ts}s`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDirectionInfo(lat, lon, bounds) {
|
|
||||||
const centerLat = (bounds.getSouth() + bounds.getNorth()) / 2;
|
|
||||||
const centerLon = (bounds.getWest() + bounds.getEast()) / 2;
|
|
||||||
const dLat = lat - centerLat;
|
|
||||||
const dLon = lon - centerLon;
|
|
||||||
|
|
||||||
const vertical = Math.abs(dLat) > 0.01 ? (dLat > 0 ? "N" : "S") : "";
|
|
||||||
const horizontal = Math.abs(dLon) > 0.01 ? (dLon > 0 ? "Ø" : "V") : "";
|
|
||||||
const label = `${vertical}${horizontal}` || "Nær kanten";
|
|
||||||
|
|
||||||
const arrowMap = {
|
|
||||||
"N": "↑",
|
|
||||||
"S": "↓",
|
|
||||||
"Ø": "→",
|
|
||||||
"V": "←",
|
|
||||||
"NØ": "↗",
|
|
||||||
"NV": "↖",
|
|
||||||
"SØ": "↘",
|
|
||||||
"SV": "↙",
|
|
||||||
"Nær kanten": "•"
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
label,
|
|
||||||
arrow: arrowMap[label] || "•"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function popupHtml(name, state) {
|
|
||||||
return `
|
|
||||||
<div style="font:13px/1.4 system-ui,sans-serif;min-width:190px;">
|
|
||||||
<div style="font-weight:600;margin-bottom:6px;">${name}</div>
|
|
||||||
<div><strong>Tid:</strong> ${fmtTs(state.current.ts)}</div>
|
|
||||||
<div><strong>Lat:</strong> ${state.displayLat.toFixed(5)}</div>
|
|
||||||
<div><strong>Lon:</strong> ${state.displayLon.toFixed(5)}</div>
|
|
||||||
<div><strong>Hastighed:</strong> ${state.current.speedMps} m/s</div>
|
|
||||||
<div><strong>Retning:</strong> ${state.displayHeading.toFixed(0)}°</div>
|
|
||||||
<div><strong>Usikkerhed:</strong> ${state.current.uncertaintyM} m</div>
|
|
||||||
<div><strong>På kortet:</strong> ${state.presenceStateLabel || "ja"}</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function smoothstep(t) {
|
function smoothstep(t) {
|
||||||
t = Math.max(0, Math.min(1, t));
|
t = Math.max(0, Math.min(1, t));
|
||||||
return t * t * (3 - 2 * t);
|
return t * t * (3 - 2 * t);
|
||||||
|
|
@ -187,26 +54,12 @@ function lerpAngleDeg(a, b, t) {
|
||||||
return (a + d * t + 360) % 360;
|
return (a + d * t + 360) % 360;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildVehicleBuckets(events) {
|
|
||||||
const out = {};
|
|
||||||
for (const e of events) {
|
|
||||||
if (!out[e.vehicle]) out[e.vehicle] = [];
|
|
||||||
out[e.vehicle].push({ ...e });
|
|
||||||
}
|
|
||||||
for (const k of Object.keys(out)) {
|
|
||||||
out[k].sort((a, b) => a.ts - b.ts);
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
function metersBetween(lat1, lon1, lat2, lon2) {
|
function metersBetween(lat1, lon1, lat2, lon2) {
|
||||||
const meanLatRad = ((lat1 + lat2) / 2) * Math.PI / 180;
|
const meanLatRad = ((lat1 + lat2) / 2) * Math.PI / 180;
|
||||||
const metersPerDegLat = 111320;
|
const metersPerDegLat = 111320;
|
||||||
const metersPerDegLon = 111320 * Math.cos(meanLatRad);
|
const metersPerDegLon = 111320 * Math.cos(meanLatRad);
|
||||||
|
|
||||||
const dLatM = (lat2 - lat1) * metersPerDegLat;
|
const dLatM = (lat2 - lat1) * metersPerDegLat;
|
||||||
const dLonM = (lon2 - lon1) * metersPerDegLon;
|
const dLonM = (lon2 - lon1) * metersPerDegLon;
|
||||||
|
|
||||||
return Math.hypot(dLatM, dLonM);
|
return Math.hypot(dLatM, dLonM);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -215,14 +68,8 @@ function clamp(value, min, max) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function distanceToBoundsMeters(lat, lon, bounds) {
|
function distanceToBoundsMeters(lat, lon, bounds) {
|
||||||
const south = bounds.getSouth();
|
const clampedLat = clamp(lat, bounds.getSouth(), bounds.getNorth());
|
||||||
const north = bounds.getNorth();
|
const clampedLon = clamp(lon, bounds.getWest(), bounds.getEast());
|
||||||
const west = bounds.getWest();
|
|
||||||
const east = bounds.getEast();
|
|
||||||
|
|
||||||
const clampedLat = clamp(lat, south, north);
|
|
||||||
const clampedLon = clamp(lon, west, east);
|
|
||||||
|
|
||||||
return metersBetween(lat, lon, clampedLat, clampedLon);
|
return metersBetween(lat, lon, clampedLat, clampedLon);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -233,52 +80,139 @@ function maxDistanceToBoundsCornerMeters(lat, lon, bounds) {
|
||||||
[bounds.getNorth(), bounds.getWest()],
|
[bounds.getNorth(), bounds.getWest()],
|
||||||
[bounds.getNorth(), bounds.getEast()]
|
[bounds.getNorth(), bounds.getEast()]
|
||||||
];
|
];
|
||||||
|
|
||||||
return Math.max(...corners.map(([cornerLat, cornerLon]) =>
|
return Math.max(...corners.map(([cornerLat, cornerLon]) =>
|
||||||
metersBetween(lat, lon, cornerLat, cornerLon)
|
metersBetween(lat, lon, cornerLat, cornerLon)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPresenceState(lat, lon, uncertaintyM) {
|
function getDirectionInfo(lat, lon, bounds) {
|
||||||
const bounds = map.getBounds();
|
const centerLat = (bounds.getSouth() + bounds.getNorth()) / 2;
|
||||||
const centerInside = bounds.contains([lat, lon]);
|
const centerLon = (bounds.getWest() + bounds.getEast()) / 2;
|
||||||
|
const dLat = lat - centerLat;
|
||||||
|
const dLon = lon - centerLon;
|
||||||
|
const vertical = Math.abs(dLat) > 0.01 ? (dLat > 0 ? "N" : "S") : "";
|
||||||
|
const horizontal = Math.abs(dLon) > 0.01 ? (dLon > 0 ? "Ø" : "V") : "";
|
||||||
|
const label = `${vertical}${horizontal}` || "Nær kanten";
|
||||||
|
const arrowMap = { "N": "↑", "S": "↓", "Ø": "→", "V": "←", "NØ": "↗", "NV": "↖", "SØ": "↘", "SV": "↙", "Nær kanten": "•" };
|
||||||
|
return { label, arrow: arrowMap[label] || "•" };
|
||||||
|
}
|
||||||
|
|
||||||
if (centerInside) {
|
function carIcon(color) {
|
||||||
return {
|
return L.divIcon({
|
||||||
code: "inside",
|
className: "",
|
||||||
label: "ja"
|
iconSize: [24, 24],
|
||||||
};
|
iconAnchor: [12, 12],
|
||||||
|
html: `
|
||||||
|
<div class="car" data-role="car">
|
||||||
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path d="M12 2 L20 22 L12 18 L4 22 Z" fill="${color}" stroke="#ffffff" stroke-width="1.5" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setHeading(marker, deg) {
|
||||||
|
const el = marker.getElement();
|
||||||
|
if (!el) return;
|
||||||
|
const car = el.querySelector("[data-role=car]");
|
||||||
|
if (!car) return;
|
||||||
|
car.style.transform = `rotate(${deg}deg)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function popupHtml(view, name, state) {
|
||||||
|
return `
|
||||||
|
<div style="font:13px/1.4 system-ui,sans-serif;min-width:190px;">
|
||||||
|
<div style="font-weight:600;margin-bottom:6px;">${name} · ${view.label}</div>
|
||||||
|
<div><strong>Tid:</strong> ${fmtTs(state.current.ts)}</div>
|
||||||
|
<div><strong>Lat:</strong> ${state.displayLat.toFixed(5)}</div>
|
||||||
|
<div><strong>Lon:</strong> ${state.displayLon.toFixed(5)}</div>
|
||||||
|
<div><strong>Hastighed:</strong> ${state.current.speedMps} m/s</div>
|
||||||
|
<div><strong>Retning:</strong> ${state.displayHeading.toFixed(0)}°</div>
|
||||||
|
<div><strong>Usikkerhed:</strong> ${state.current.uncertaintyM} m</div>
|
||||||
|
<div><strong>På kortet:</strong> ${state.presenceStateLabel || "ja"}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSelectedVehicleInfo(selection) {
|
||||||
|
if (!selection) {
|
||||||
|
selectedVehicleInfoEl.textContent = "Klik på et køretøj eller en hjørneindikator for detaljer.";
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
selectedVehicleInfoEl.innerHTML = `
|
||||||
|
<strong>${selection.name} · ${selection.viewLabel}</strong>
|
||||||
|
Kilde: ${selection.source}<br>
|
||||||
|
Tid: ${fmtTs(selection.state.current.ts)}<br>
|
||||||
|
Position: ${selection.state.displayLat.toFixed(5)}, ${selection.state.displayLon.toFixed(5)}<br>
|
||||||
|
Hastighed: ${selection.state.current.speedMps} m/s<br>
|
||||||
|
Retning: ${selection.state.displayHeading.toFixed(0)}°<br>
|
||||||
|
Usikkerhed: ${selection.state.current.uncertaintyM} m<br>
|
||||||
|
Status: ${selection.state.presenceStateLabel}
|
||||||
|
`;
|
||||||
|
|
||||||
|
selectedVehicleInfoEl.scrollIntoView({ block: "nearest" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function createView({ id, label, positionKey, alertsEl, indicatorsId }) {
|
||||||
|
const map = L.map(id).setView([55.6761, 12.5683], 13);
|
||||||
|
let tileErrors = 0;
|
||||||
|
let tileLoads = 0;
|
||||||
|
const tileLayer = L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||||
|
attribution: "© OpenStreetMap contributors",
|
||||||
|
maxZoom: 19
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
tileLayer.on("tileerror", () => {
|
||||||
|
tileErrors += 1;
|
||||||
|
tileWarningEl.hidden = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
tileLayer.on("tileload", () => {
|
||||||
|
tileLoads += 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
tileLayer.on("load", () => {
|
||||||
|
if (tileLoads > 0) {
|
||||||
|
tileWarningEl.hidden = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
positionKey,
|
||||||
|
alertsEl,
|
||||||
|
indicatorsEl: document.getElementById(indicatorsId),
|
||||||
|
map,
|
||||||
|
tileLayer,
|
||||||
|
getTileStats: () => ({ tileErrors, tileLoads }),
|
||||||
|
vehicles: {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPresenceState(view, lat, lon, uncertaintyM) {
|
||||||
|
const bounds = view.map.getBounds();
|
||||||
|
if (bounds.contains([lat, lon])) {
|
||||||
|
return { code: "inside", label: "ja" };
|
||||||
|
}
|
||||||
if (uncertaintyM > 0) {
|
if (uncertaintyM > 0) {
|
||||||
const distToBounds = distanceToBoundsMeters(lat, lon, bounds);
|
const distToBounds = distanceToBoundsMeters(lat, lon, bounds);
|
||||||
if (distToBounds <= uncertaintyM) {
|
if (distToBounds <= uncertaintyM) {
|
||||||
const maxCornerDistance = maxDistanceToBoundsCornerMeters(lat, lon, bounds);
|
const maxCornerDistance = maxDistanceToBoundsCornerMeters(lat, lon, bounds);
|
||||||
if (uncertaintyM >= maxCornerDistance) {
|
if (uncertaintyM >= maxCornerDistance) {
|
||||||
return {
|
return { code: "covers", label: "dækker hele kortet" };
|
||||||
code: "covers",
|
|
||||||
label: "dækker hele kortet"
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
return { code: "possible", label: "muligvis" };
|
||||||
return {
|
|
||||||
code: "possible",
|
|
||||||
label: "muligvis"
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return { code: "outside", label: "nej" };
|
||||||
return {
|
|
||||||
code: "outside",
|
|
||||||
label: "nej"
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureUncertaintyCircle(v) {
|
function ensureUncertaintyCircle(view, v) {
|
||||||
if (v.uncertaintyCircle || v.current.uncertaintyM <= 0) {
|
if (view.positionKey === "true") return;
|
||||||
return;
|
if (v.uncertaintyCircle || v.current.uncertaintyM <= 0) return;
|
||||||
}
|
|
||||||
|
|
||||||
v.uncertaintyCircle = L.circle([v.displayLat, v.displayLon], {
|
v.uncertaintyCircle = L.circle([v.displayLat, v.displayLon], {
|
||||||
radius: v.current.uncertaintyM,
|
radius: v.current.uncertaintyM,
|
||||||
color: v.color,
|
color: v.color,
|
||||||
|
|
@ -288,32 +222,25 @@ function ensureUncertaintyCircle(v) {
|
||||||
fillColor: v.color,
|
fillColor: v.color,
|
||||||
fillOpacity: 0.08,
|
fillOpacity: 0.08,
|
||||||
interactive: false
|
interactive: false
|
||||||
}).addTo(map);
|
}).addTo(view.map);
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncUncertaintyCircle(v) {
|
function syncUncertaintyCircle(view, v) {
|
||||||
if (v.current.uncertaintyM > 0) {
|
if (view.positionKey === "true") {
|
||||||
ensureUncertaintyCircle(v);
|
if (v.uncertaintyCircle && view.map.hasLayer(v.uncertaintyCircle)) {
|
||||||
|
view.map.removeLayer(v.uncertaintyCircle);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!v.uncertaintyCircle) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (v.current.uncertaintyM > 0) ensureUncertaintyCircle(view, v);
|
||||||
|
if (!v.uncertaintyCircle) return;
|
||||||
if (v.current.uncertaintyM <= 0) {
|
if (v.current.uncertaintyM <= 0) {
|
||||||
if (map.hasLayer(v.uncertaintyCircle)) {
|
if (view.map.hasLayer(v.uncertaintyCircle)) view.map.removeLayer(v.uncertaintyCircle);
|
||||||
map.removeLayer(v.uncertaintyCircle);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!view.map.hasLayer(v.uncertaintyCircle)) v.uncertaintyCircle.addTo(view.map);
|
||||||
if (!map.hasLayer(v.uncertaintyCircle)) {
|
|
||||||
v.uncertaintyCircle.addTo(map);
|
|
||||||
}
|
|
||||||
|
|
||||||
v.uncertaintyCircle.setLatLng([v.displayLat, v.displayLon]);
|
v.uncertaintyCircle.setLatLng([v.displayLat, v.displayLon]);
|
||||||
v.uncertaintyCircle.setRadius(v.current.uncertaintyM);
|
v.uncertaintyCircle.setRadius(v.current.uncertaintyM);
|
||||||
|
|
||||||
if (v.presenceState === "inside") {
|
if (v.presenceState === "inside") {
|
||||||
v.uncertaintyCircle.setStyle({ opacity: 0.7, fillOpacity: 0.08 });
|
v.uncertaintyCircle.setStyle({ opacity: 0.7, fillOpacity: 0.08 });
|
||||||
} else if (v.presenceState === "covers") {
|
} else if (v.presenceState === "covers") {
|
||||||
|
|
@ -328,62 +255,53 @@ function syncUncertaintyCircle(v) {
|
||||||
const vehicleEvents = buildVehicleBuckets(VEHICLE_EVENTS);
|
const vehicleEvents = buildVehicleBuckets(VEHICLE_EVENTS);
|
||||||
const vehicleNames = Object.keys(vehicleEvents);
|
const vehicleNames = Object.keys(vehicleEvents);
|
||||||
|
|
||||||
const vehicles = {};
|
const views = [
|
||||||
|
createView({ id: "mapApprox", label: "Tilnærmet position", positionKey: "approx", alertsEl: alertsApproxEl, indicatorsId: "offmapIndicatorsApprox" }),
|
||||||
|
createView({ id: "mapTrue", label: "Præcis position", positionKey: "true", alertsEl: alertsTrueEl, indicatorsId: "offmapIndicatorsTrue" })
|
||||||
|
];
|
||||||
|
|
||||||
for (const name of vehicleNames) {
|
function getEventLatLon(event, positionKey) {
|
||||||
|
if (positionKey === "true") return { lat: event.trueLat, lon: event.trueLon };
|
||||||
|
return { lat: event.approxLat, lon: event.approxLon };
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const view of views) {
|
||||||
|
for (const name of vehicleNames) {
|
||||||
const first = vehicleEvents[name][0];
|
const first = vehicleEvents[name][0];
|
||||||
const color = COLORS[name] || "#2979ff";
|
const color = COLORS[name] || "#2979ff";
|
||||||
|
const firstPos = getEventLatLon(first, view.positionKey);
|
||||||
|
const marker = L.marker([firstPos.lat, firstPos.lon], { icon: carIcon(color) }).addTo(view.map);
|
||||||
|
const trail = L.polyline([[firstPos.lat, firstPos.lon]], { color, weight: 3, opacity: 0.85 }).addTo(view.map);
|
||||||
|
|
||||||
const marker = L.marker([first.lat, first.lon], {
|
view.vehicles[name] = {
|
||||||
icon: carIcon(color)
|
|
||||||
}).addTo(map);
|
|
||||||
|
|
||||||
const trail = L.polyline([[first.lat, first.lon]], {
|
|
||||||
color,
|
|
||||||
weight: 3,
|
|
||||||
opacity: 0.85
|
|
||||||
}).addTo(map);
|
|
||||||
|
|
||||||
let uncertaintyCircle = null;
|
|
||||||
if (first.uncertaintyM > 0) {
|
|
||||||
uncertaintyCircle = L.circle([first.lat, first.lon], {
|
|
||||||
radius: first.uncertaintyM,
|
|
||||||
color: color,
|
|
||||||
weight: 2,
|
|
||||||
dashArray: "6,6",
|
|
||||||
opacity: 0.7,
|
|
||||||
fillColor: color,
|
|
||||||
fillOpacity: 0.08,
|
|
||||||
interactive: false
|
|
||||||
}).addTo(map);
|
|
||||||
}
|
|
||||||
|
|
||||||
vehicles[name] = {
|
|
||||||
name,
|
name,
|
||||||
color,
|
color,
|
||||||
events: vehicleEvents[name],
|
events: vehicleEvents[name],
|
||||||
marker,
|
marker,
|
||||||
trail,
|
trail,
|
||||||
uncertaintyCircle,
|
uncertaintyCircle: null,
|
||||||
currentIndex: 0,
|
currentIndex: 0,
|
||||||
current: first,
|
current: first,
|
||||||
next: vehicleEvents[name][1] || null,
|
next: vehicleEvents[name][1] || null,
|
||||||
displayLat: first.lat,
|
displayLat: firstPos.lat,
|
||||||
displayLon: first.lon,
|
displayLon: firstPos.lon,
|
||||||
displayHeading: first.headingDeg,
|
displayHeading: first.headingDeg,
|
||||||
lastTrailLat: first.lat,
|
|
||||||
lastTrailLon: first.lon,
|
|
||||||
presenceState: "inside",
|
presenceState: "inside",
|
||||||
presenceStateLabel: "ja"
|
presenceStateLabel: "ja"
|
||||||
};
|
};
|
||||||
|
|
||||||
marker.bindPopup(popupHtml(name, vehicles[name]));
|
marker.bindPopup(popupHtml(view, name, view.vehicles[name]));
|
||||||
marker.on("click", () => {
|
marker.on("click", () => {
|
||||||
forcedPopupVehicle = name;
|
renderSelectedVehicleInfo({
|
||||||
highlightOffmapIndicator(null);
|
name,
|
||||||
renderSelectedVehicleInfo(vehicles[name], "kort");
|
viewLabel: view.label,
|
||||||
|
source: "kort",
|
||||||
|
state: view.vehicles[name]
|
||||||
|
});
|
||||||
});
|
});
|
||||||
setHeading(marker, first.headingDeg);
|
setHeading(marker, first.headingDeg);
|
||||||
|
syncUncertaintyCircle(view, view.vehicles[name]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let simTime = 0;
|
let simTime = 0;
|
||||||
|
|
@ -391,201 +309,199 @@ let lastFrameTime = null;
|
||||||
let running = true;
|
let running = true;
|
||||||
const TIME_SCALE = 0.175;
|
const TIME_SCALE = 0.175;
|
||||||
|
|
||||||
function resetDemo() {
|
|
||||||
simTime = 0;
|
|
||||||
lastFrameTime = null;
|
|
||||||
forcedPopupVehicle = null;
|
|
||||||
highlightOffmapIndicator(null);
|
|
||||||
renderSelectedVehicleInfo(null);
|
|
||||||
|
|
||||||
for (const name of vehicleNames) {
|
|
||||||
const v = vehicles[name];
|
|
||||||
const first = v.events[0];
|
|
||||||
|
|
||||||
v.currentIndex = 0;
|
|
||||||
v.current = first;
|
|
||||||
v.next = v.events[1] || null;
|
|
||||||
v.displayLat = first.lat;
|
|
||||||
v.displayLon = first.lon;
|
|
||||||
v.displayHeading = first.headingDeg;
|
|
||||||
v.lastTrailLat = first.lat;
|
|
||||||
v.lastTrailLon = first.lon;
|
|
||||||
v.presenceState = "inside";
|
|
||||||
v.presenceStateLabel = "ja";
|
|
||||||
|
|
||||||
v.marker.setLatLng([first.lat, first.lon]);
|
|
||||||
setHeading(v.marker, first.headingDeg);
|
|
||||||
v.marker.setPopupContent(popupHtml(name, v));
|
|
||||||
|
|
||||||
v.trail.setLatLngs([[first.lat, first.lon]]);
|
|
||||||
v.trail.setStyle({ opacity: 0.85 });
|
|
||||||
|
|
||||||
syncUncertaintyCircle(v);
|
|
||||||
|
|
||||||
if (!map.hasLayer(v.marker)) {
|
|
||||||
v.marker.addTo(map);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
function advanceVehiclePointers(v) {
|
function advanceVehiclePointers(v) {
|
||||||
while (v.next && simTime >= v.next.ts) {
|
while (v.next && simTime >= v.next.ts) {
|
||||||
v.currentIndex += 1;
|
v.currentIndex += 1;
|
||||||
v.current = v.events[v.currentIndex];
|
v.current = v.events[v.currentIndex];
|
||||||
v.next = v.events[v.currentIndex + 1] || null;
|
v.next = v.events[v.currentIndex + 1] || null;
|
||||||
|
|
||||||
v.trail.addLatLng([v.current.lat, v.current.lon]);
|
|
||||||
v.lastTrailLat = v.current.lat;
|
|
||||||
v.lastTrailLon = v.current.lon;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateVehicleDisplay(v) {
|
function updateVehicleDisplay(view, v) {
|
||||||
advanceVehiclePointers(v);
|
advanceVehiclePointers(v);
|
||||||
|
let latLon = getEventLatLon(v.current, view.positionKey);
|
||||||
let lat = v.current.lat;
|
let lat = latLon.lat;
|
||||||
let lon = v.current.lon;
|
let lon = latLon.lon;
|
||||||
let heading = v.current.headingDeg;
|
let heading = v.current.headingDeg;
|
||||||
|
|
||||||
if (v.next) {
|
if (v.next) {
|
||||||
const dt = v.next.ts - v.current.ts;
|
const dt = v.next.ts - v.current.ts;
|
||||||
const raw = dt > 0 ? (simTime - v.current.ts) / dt : 1;
|
const raw = dt > 0 ? (simTime - v.current.ts) / dt : 1;
|
||||||
const t = smoothstep(raw);
|
const t = smoothstep(raw);
|
||||||
|
const nextLatLon = getEventLatLon(v.next, view.positionKey);
|
||||||
lat = lerp(v.current.lat, v.next.lat, t);
|
lat = lerp(latLon.lat, nextLatLon.lat, t);
|
||||||
lon = lerp(v.current.lon, v.next.lon, t);
|
lon = lerp(latLon.lon, nextLatLon.lon, t);
|
||||||
heading = lerpAngleDeg(v.current.headingDeg, v.next.headingDeg, t);
|
heading = lerpAngleDeg(v.current.headingDeg, v.next.headingDeg, t);
|
||||||
} else {
|
|
||||||
const age = simTime - v.current.ts;
|
|
||||||
const predictFor = Math.min(age, 8);
|
|
||||||
const meters = v.current.speedMps * predictFor;
|
|
||||||
|
|
||||||
const rad = v.current.headingDeg * Math.PI / 180;
|
|
||||||
const northM = Math.cos(rad) * meters;
|
|
||||||
const eastM = Math.sin(rad) * meters;
|
|
||||||
|
|
||||||
const metersPerDegLat = 111320;
|
|
||||||
const metersPerDegLon = 111320 * Math.cos(v.current.lat * Math.PI / 180);
|
|
||||||
|
|
||||||
lat = v.current.lat + northM / metersPerDegLat;
|
|
||||||
lon = v.current.lon + eastM / metersPerDegLon;
|
|
||||||
heading = v.current.headingDeg;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
v.displayLat = lat;
|
v.displayLat = lat;
|
||||||
v.displayLon = lon;
|
v.displayLon = lon;
|
||||||
v.displayHeading = heading;
|
v.displayHeading = heading;
|
||||||
|
v.presenceState = getPresenceState(view, lat, lon, v.current.uncertaintyM).code;
|
||||||
|
v.presenceStateLabel = getPresenceState(view, lat, lon, v.current.uncertaintyM).label;
|
||||||
|
|
||||||
const presence = getPresenceState(lat, lon, v.current.uncertaintyM);
|
if (v.presenceState === "inside") {
|
||||||
v.presenceState = presence.code;
|
if (!view.map.hasLayer(v.marker)) v.marker.addTo(view.map);
|
||||||
v.presenceStateLabel = presence.label;
|
|
||||||
|
|
||||||
const shouldForcePopup = forcedPopupVehicle === v.name;
|
|
||||||
|
|
||||||
if (presence.code === "inside" || shouldForcePopup) {
|
|
||||||
if (!map.hasLayer(v.marker)) {
|
|
||||||
v.marker.addTo(map);
|
|
||||||
}
|
|
||||||
v.marker.setOpacity(1);
|
|
||||||
v.marker.setZIndexOffset(shouldForcePopup ? 1000 : 0);
|
|
||||||
v.marker.setLatLng([lat, lon]);
|
v.marker.setLatLng([lat, lon]);
|
||||||
setHeading(v.marker, heading);
|
setHeading(v.marker, heading);
|
||||||
v.trail.setStyle({ opacity: 0.85 });
|
v.trail.setStyle({ opacity: 0.85 });
|
||||||
} else {
|
} else {
|
||||||
v.marker.closePopup();
|
if (view.map.hasLayer(v.marker)) view.map.removeLayer(v.marker);
|
||||||
v.marker.setZIndexOffset(0);
|
v.trail.setStyle({ opacity: v.presenceState === "covers" ? 0.4 : 0.25 });
|
||||||
if (map.hasLayer(v.marker)) {
|
|
||||||
map.removeLayer(v.marker);
|
|
||||||
}
|
|
||||||
v.trail.setStyle({ opacity: presence.code === "covers" ? 0.4 : 0.25 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
v.marker.setPopupContent(popupHtml(v.name, v));
|
const trailPoints = v.events
|
||||||
|
.slice(0, v.currentIndex + 1)
|
||||||
|
.map(event => getEventLatLon(event, view.positionKey))
|
||||||
|
.map(pos => [pos.lat, pos.lon]);
|
||||||
|
if (trailPoints.length > 0) {
|
||||||
|
v.trail.setLatLngs(trailPoints);
|
||||||
|
}
|
||||||
|
|
||||||
syncUncertaintyCircle(v);
|
v.marker.setPopupContent(popupHtml(view, v.name, v));
|
||||||
|
syncUncertaintyCircle(view, v);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateAlerts() {
|
function setActiveIndicator(view, vehicleName) {
|
||||||
const alertVehicles = vehicleNames
|
const buttons = view.indicatorsEl.querySelectorAll('.offmap-indicator');
|
||||||
.map(name => vehicles[name])
|
buttons.forEach((button) => {
|
||||||
.filter(v => v.presenceState === "possible" || v.presenceState === "covers");
|
button.classList.toggle('active', button.dataset.vehicle === vehicleName);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateViewAlerts(view) {
|
||||||
|
const alertVehicles = vehicleNames.map(name => view.vehicles[name]).filter(v => v.presenceState === "possible" || v.presenceState === "covers");
|
||||||
if (alertVehicles.length === 0) {
|
if (alertVehicles.length === 0) {
|
||||||
alertsEl.innerHTML = "";
|
view.alertsEl.innerHTML = "";
|
||||||
offmapIndicatorsEl.innerHTML = "";
|
view.indicatorsEl.innerHTML = "";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
alertsEl.innerHTML = alertVehicles.map(v => {
|
view.alertsEl.innerHTML = alertVehicles.map(v => `
|
||||||
if (v.presenceState === "covers") {
|
|
||||||
return `
|
|
||||||
<div class="alert-item">
|
<div class="alert-item">
|
||||||
<strong>${v.name}: usikkerhed dækker hele kortet</strong>
|
<strong>${v.name}: ${v.presenceState === "covers" ? "usikkerhed dækker hele kortet" : "mulig tilstedeværelse"}</strong>
|
||||||
Estimatet er udenfor kortet, men usikkerheden spænder over hele viewporten.
|
${v.presenceState === "covers" ? "Estimatet er udenfor kortet, men usikkerheden spænder over hele viewporten." : "Estimatet er udenfor kortet, men usikkerheden overlapper det viste område delvist."}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`).join("");
|
||||||
}
|
|
||||||
|
|
||||||
return `
|
const bounds = view.map.getBounds();
|
||||||
<div class="alert-item">
|
view.indicatorsEl.innerHTML = alertVehicles.map(v => {
|
||||||
<strong>${v.name}: mulig tilstedeværelse</strong>
|
|
||||||
Estimatet er udenfor kortet, men usikkerheden overlapper det viste område delvist.
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}).join("");
|
|
||||||
|
|
||||||
const bounds = map.getBounds();
|
|
||||||
offmapIndicatorsEl.innerHTML = alertVehicles.map(v => {
|
|
||||||
const direction = getDirectionInfo(v.displayLat, v.displayLon, bounds);
|
const direction = getDirectionInfo(v.displayLat, v.displayLon, bounds);
|
||||||
const subtitle = v.presenceState === "covers"
|
const subtitle = v.presenceState === "covers" ? "Usikkerheden dækker hele viewporten" : `Mulig tilstedeværelse fra ${direction.label}`;
|
||||||
? "Usikkerheden dækker hele viewporten"
|
|
||||||
: `Mulig tilstedeværelse fra ${direction.label}`;
|
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<button type="button" class="offmap-indicator ${v.presenceState === "covers" ? "covers" : ""}" data-vehicle="${v.name}">
|
<button type="button" class="offmap-indicator ${v.presenceState === "covers" ? "covers" : ""}" data-view="${view.id}" data-vehicle="${v.name}">
|
||||||
<div class="offmap-arrow" style="background:${v.color};">${direction.arrow}</div>
|
<div class="offmap-arrow" style="background:${v.color};">${direction.arrow}</div>
|
||||||
<div class="offmap-meta">
|
<div><strong>${v.name}</strong><span>${subtitle}</span></div>
|
||||||
<strong>${v.name}</strong>
|
|
||||||
<span>${subtitle}</span>
|
|
||||||
</div>
|
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
}).join("");
|
}).join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resetDemo() {
|
||||||
|
simTime = 0;
|
||||||
|
lastFrameTime = null;
|
||||||
|
renderSelectedVehicleInfo(null);
|
||||||
|
for (const view of views) {
|
||||||
|
for (const name of vehicleNames) {
|
||||||
|
const v = view.vehicles[name];
|
||||||
|
const first = v.events[0];
|
||||||
|
const firstPos = getEventLatLon(first, view.positionKey);
|
||||||
|
v.currentIndex = 0;
|
||||||
|
v.current = first;
|
||||||
|
v.next = v.events[1] || null;
|
||||||
|
v.displayLat = firstPos.lat;
|
||||||
|
v.displayLon = firstPos.lon;
|
||||||
|
v.displayHeading = first.headingDeg;
|
||||||
|
v.presenceState = "inside";
|
||||||
|
v.presenceStateLabel = "ja";
|
||||||
|
v.marker.setLatLng([firstPos.lat, firstPos.lon]);
|
||||||
|
setHeading(v.marker, first.headingDeg);
|
||||||
|
v.trail.setLatLngs([[firstPos.lat, firstPos.lon]]);
|
||||||
|
if (!view.map.hasLayer(v.marker)) v.marker.addTo(view.map);
|
||||||
|
syncUncertaintyCircle(view, v);
|
||||||
|
}
|
||||||
|
updateViewAlerts(view);
|
||||||
|
}
|
||||||
|
updateStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDebugPanel() {
|
||||||
|
const lines = views.map((view) => {
|
||||||
|
const el = document.getElementById(view.id);
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
const center = view.map.getCenter();
|
||||||
|
const zoom = view.map.getZoom();
|
||||||
|
const stats = view.getTileStats();
|
||||||
|
return [
|
||||||
|
`${view.label}`,
|
||||||
|
` size: ${Math.round(rect.width)}x${Math.round(rect.height)}`,
|
||||||
|
` center: ${center.lat.toFixed(4)}, ${center.lng.toFixed(4)}`,
|
||||||
|
` zoom: ${zoom}`,
|
||||||
|
` tiles loaded/errors: ${stats.tileLoads}/${stats.tileErrors}`
|
||||||
|
].join("\n");
|
||||||
|
});
|
||||||
|
|
||||||
|
debugPanelEl.textContent = lines.join("\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
function updateStatus() {
|
function updateStatus() {
|
||||||
const total = VEHICLE_EVENTS.length;
|
const total = VEHICLE_EVENTS.length;
|
||||||
const passed = VEHICLE_EVENTS.filter(e => e.ts <= simTime).length;
|
const passed = VEHICLE_EVENTS.filter(e => e.ts <= simTime).length;
|
||||||
|
statusEl.textContent = `Simuleret tid: ${simTime.toFixed(1)} s\nEvents passeret: ${passed} / ${total}\nAfspilning: 2x`;
|
||||||
statusEl.textContent =
|
updateDebugPanel();
|
||||||
`Simuleret tid: ${simTime.toFixed(1)} s\n` +
|
|
||||||
`Events passeret: ${passed} / ${total}\n` +
|
|
||||||
`Afspilning: 2x`;
|
|
||||||
|
|
||||||
updateAlerts();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function tick(now) {
|
function tick(now) {
|
||||||
if (!running) return;
|
if (!running) return;
|
||||||
|
if (lastFrameTime === null) lastFrameTime = now;
|
||||||
if (lastFrameTime === null) {
|
|
||||||
lastFrameTime = now;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dtReal = (now - lastFrameTime) / 1000;
|
const dtReal = (now - lastFrameTime) / 1000;
|
||||||
lastFrameTime = now;
|
lastFrameTime = now;
|
||||||
simTime += dtReal / TIME_SCALE;
|
simTime += dtReal / TIME_SCALE;
|
||||||
|
for (const view of views) {
|
||||||
for (const name of vehicleNames) {
|
for (const name of vehicleNames) {
|
||||||
updateVehicleDisplay(vehicles[name]);
|
updateVehicleDisplay(view, view.vehicles[name]);
|
||||||
|
}
|
||||||
|
updateViewAlerts(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStatus();
|
updateStatus();
|
||||||
requestAnimationFrame(tick);
|
requestAnimationFrame(tick);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function invalidateAllMaps() {
|
||||||
|
for (const view of views) {
|
||||||
|
view.map.invalidateSize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const view of views) {
|
||||||
|
function handleIndicatorSelection(button) {
|
||||||
|
if (!button) return;
|
||||||
|
const v = view.vehicles[button.dataset.vehicle];
|
||||||
|
setActiveIndicator(view, v.name);
|
||||||
|
renderSelectedVehicleInfo({
|
||||||
|
name: v.name,
|
||||||
|
viewLabel: view.label,
|
||||||
|
source: "hjørneindikator",
|
||||||
|
state: v
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
view.indicatorsEl.addEventListener("click", (event) => {
|
||||||
|
handleIndicatorSelection(event.target.closest(".offmap-indicator"));
|
||||||
|
});
|
||||||
|
|
||||||
|
view.indicatorsEl.addEventListener("pointerup", (event) => {
|
||||||
|
handleIndicatorSelection(event.target.closest(".offmap-indicator"));
|
||||||
|
});
|
||||||
|
|
||||||
|
view.map.on("moveend zoomend", () => {
|
||||||
|
for (const name of vehicleNames) {
|
||||||
|
updateVehicleDisplay(view, view.vehicles[name]);
|
||||||
|
}
|
||||||
|
updateViewAlerts(view);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
playBtn.addEventListener("click", () => {
|
playBtn.addEventListener("click", () => {
|
||||||
if (running) return;
|
if (running) return;
|
||||||
running = true;
|
running = true;
|
||||||
|
|
@ -604,30 +520,10 @@ resetBtn.addEventListener("click", () => {
|
||||||
requestAnimationFrame(tick);
|
requestAnimationFrame(tick);
|
||||||
});
|
});
|
||||||
|
|
||||||
offmapIndicatorsEl.addEventListener("click", (event) => {
|
|
||||||
const button = event.target.closest(".offmap-indicator");
|
|
||||||
if (!button) return;
|
|
||||||
|
|
||||||
const vehicleName = button.dataset.vehicle;
|
|
||||||
openVehiclePopup(vehicleName);
|
|
||||||
});
|
|
||||||
|
|
||||||
map.on("popupclose", (event) => {
|
|
||||||
const popupVehicle = Object.values(vehicles).find(v => v.marker === event.popup._source);
|
|
||||||
if (popupVehicle && forcedPopupVehicle === popupVehicle.name) {
|
|
||||||
forcedPopupVehicle = null;
|
|
||||||
highlightOffmapIndicator(null);
|
|
||||||
renderSelectedVehicleInfo(null);
|
|
||||||
updateVehicleDisplay(popupVehicle);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
map.on("moveend zoomend", () => {
|
|
||||||
for (const name of vehicleNames) {
|
|
||||||
updateVehicleDisplay(vehicles[name]);
|
|
||||||
}
|
|
||||||
updateStatus();
|
|
||||||
});
|
|
||||||
|
|
||||||
resetDemo();
|
resetDemo();
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
invalidateAllMaps();
|
||||||
|
requestAnimationFrame(invalidateAllMaps);
|
||||||
|
});
|
||||||
|
window.addEventListener("resize", invalidateAllMaps);
|
||||||
requestAnimationFrame(tick);
|
requestAnimationFrame(tick);
|
||||||
|
|
|
||||||
|
|
@ -1,406 +0,0 @@
|
||||||
const COLORS = {
|
|
||||||
"Bil 1": "#ff0000",
|
|
||||||
"Bil 2": "#00c853",
|
|
||||||
"Bil 3": "#2979ff",
|
|
||||||
"Bil 4": "#2979ff",
|
|
||||||
"Bil 5": "#2979ff"
|
|
||||||
};
|
|
||||||
|
|
||||||
const map = L.map("map").setView([55.6761, 12.5683], 13);
|
|
||||||
|
|
||||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
|
||||||
attribution: "© OpenStreetMap contributors",
|
|
||||||
maxZoom: 19
|
|
||||||
}).addTo(map);
|
|
||||||
|
|
||||||
const statusEl = document.getElementById("status");
|
|
||||||
const alertsEl = document.getElementById("presenceAlerts");
|
|
||||||
const playBtn = document.getElementById("playBtn");
|
|
||||||
const pauseBtn = document.getElementById("pauseBtn");
|
|
||||||
const resetBtn = document.getElementById("resetBtn");
|
|
||||||
|
|
||||||
function carIcon(color) {
|
|
||||||
return L.divIcon({
|
|
||||||
className: "",
|
|
||||||
iconSize: [24, 24],
|
|
||||||
iconAnchor: [12, 12],
|
|
||||||
html: `
|
|
||||||
<div class="car" data-role="car">
|
|
||||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
||||||
<path
|
|
||||||
d="M12 2 L20 22 L12 18 L4 22 Z"
|
|
||||||
fill="${color}"
|
|
||||||
stroke="#ffffff"
|
|
||||||
stroke-width="1.5"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function setHeading(marker, deg) {
|
|
||||||
const el = marker.getElement();
|
|
||||||
if (!el) return;
|
|
||||||
const car = el.querySelector("[data-role=car]");
|
|
||||||
if (!car) return;
|
|
||||||
car.style.transform = `rotate(${deg}deg)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fmtTs(ts) {
|
|
||||||
return `${ts}s`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function popupHtml(name, state) {
|
|
||||||
return `
|
|
||||||
<div style="font:13px/1.4 system-ui,sans-serif;min-width:190px;">
|
|
||||||
<div style="font-weight:600;margin-bottom:6px;">${name}</div>
|
|
||||||
<div><strong>Tid:</strong> ${fmtTs(state.current.ts)}</div>
|
|
||||||
<div><strong>Lat:</strong> ${state.displayLat.toFixed(5)}</div>
|
|
||||||
<div><strong>Lon:</strong> ${state.displayLon.toFixed(5)}</div>
|
|
||||||
<div><strong>Hastighed:</strong> ${state.current.speedMps} m/s</div>
|
|
||||||
<div><strong>Retning:</strong> ${state.displayHeading.toFixed(0)}°</div>
|
|
||||||
<div><strong>Usikkerhed:</strong> ${state.current.uncertaintyM} m</div>
|
|
||||||
<div><strong>På kortet:</strong> ${state.presenceStateLabel || "ja"}</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function smoothstep(t) {
|
|
||||||
t = Math.max(0, Math.min(1, t));
|
|
||||||
return t * t * (3 - 2 * t);
|
|
||||||
}
|
|
||||||
|
|
||||||
function lerp(a, b, t) {
|
|
||||||
return a + (b - a) * t;
|
|
||||||
}
|
|
||||||
|
|
||||||
function lerpAngleDeg(a, b, t) {
|
|
||||||
let d = ((b - a + 540) % 360) - 180;
|
|
||||||
return (a + d * t + 360) % 360;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildVehicleBuckets(events) {
|
|
||||||
const out = {};
|
|
||||||
for (const e of events) {
|
|
||||||
if (!out[e.vehicle]) out[e.vehicle] = [];
|
|
||||||
out[e.vehicle].push({ ...e });
|
|
||||||
}
|
|
||||||
for (const k of Object.keys(out)) {
|
|
||||||
out[k].sort((a, b) => a.ts - b.ts);
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
function metersBetween(lat1, lon1, lat2, lon2) {
|
|
||||||
const meanLatRad = ((lat1 + lat2) / 2) * Math.PI / 180;
|
|
||||||
const metersPerDegLat = 111320;
|
|
||||||
const metersPerDegLon = 111320 * Math.cos(meanLatRad);
|
|
||||||
|
|
||||||
const dLatM = (lat2 - lat1) * metersPerDegLat;
|
|
||||||
const dLonM = (lon2 - lon1) * metersPerDegLon;
|
|
||||||
|
|
||||||
return Math.hypot(dLatM, dLonM);
|
|
||||||
}
|
|
||||||
|
|
||||||
function clamp(value, min, max) {
|
|
||||||
return Math.max(min, Math.min(max, value));
|
|
||||||
}
|
|
||||||
|
|
||||||
function distanceToBoundsMeters(lat, lon, bounds) {
|
|
||||||
const south = bounds.getSouth();
|
|
||||||
const north = bounds.getNorth();
|
|
||||||
const west = bounds.getWest();
|
|
||||||
const east = bounds.getEast();
|
|
||||||
|
|
||||||
const clampedLat = clamp(lat, south, north);
|
|
||||||
const clampedLon = clamp(lon, west, east);
|
|
||||||
|
|
||||||
return metersBetween(lat, lon, clampedLat, clampedLon);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPresenceState(lat, lon, uncertaintyM) {
|
|
||||||
const bounds = map.getBounds();
|
|
||||||
const centerInside = bounds.contains([lat, lon]);
|
|
||||||
|
|
||||||
if (centerInside) {
|
|
||||||
return {
|
|
||||||
code: "inside",
|
|
||||||
label: "ja"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uncertaintyM > 0) {
|
|
||||||
const distToBounds = distanceToBoundsMeters(lat, lon, bounds);
|
|
||||||
if (distToBounds <= uncertaintyM) {
|
|
||||||
return {
|
|
||||||
code: "possible",
|
|
||||||
label: "muligvis"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
code: "outside",
|
|
||||||
label: "nej"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const vehicleEvents = buildVehicleBuckets(VEHICLE_EVENTS);
|
|
||||||
const vehicleNames = Object.keys(vehicleEvents);
|
|
||||||
|
|
||||||
const vehicles = {};
|
|
||||||
|
|
||||||
for (const name of vehicleNames) {
|
|
||||||
const first = vehicleEvents[name][0];
|
|
||||||
const color = COLORS[name] || "#2979ff";
|
|
||||||
|
|
||||||
const marker = L.marker([first.lat, first.lon], {
|
|
||||||
icon: carIcon(color)
|
|
||||||
}).addTo(map);
|
|
||||||
|
|
||||||
const trail = L.polyline([[first.lat, first.lon]], {
|
|
||||||
color,
|
|
||||||
weight: 3,
|
|
||||||
opacity: 0.85
|
|
||||||
}).addTo(map);
|
|
||||||
|
|
||||||
let uncertaintyCircle = null;
|
|
||||||
if (name === "Bil 2") {
|
|
||||||
uncertaintyCircle = L.circle([first.lat, first.lon], {
|
|
||||||
radius: first.uncertaintyM,
|
|
||||||
color: color,
|
|
||||||
weight: 2,
|
|
||||||
dashArray: "6,6",
|
|
||||||
opacity: 0.7,
|
|
||||||
fillColor: color,
|
|
||||||
fillOpacity: 0.08
|
|
||||||
}).addTo(map);
|
|
||||||
}
|
|
||||||
|
|
||||||
vehicles[name] = {
|
|
||||||
name,
|
|
||||||
color,
|
|
||||||
events: vehicleEvents[name],
|
|
||||||
marker,
|
|
||||||
trail,
|
|
||||||
uncertaintyCircle,
|
|
||||||
currentIndex: 0,
|
|
||||||
current: first,
|
|
||||||
next: vehicleEvents[name][1] || null,
|
|
||||||
displayLat: first.lat,
|
|
||||||
displayLon: first.lon,
|
|
||||||
displayHeading: first.headingDeg,
|
|
||||||
lastTrailLat: first.lat,
|
|
||||||
lastTrailLon: first.lon,
|
|
||||||
presenceState: "inside",
|
|
||||||
presenceStateLabel: "ja"
|
|
||||||
};
|
|
||||||
|
|
||||||
marker.bindPopup(popupHtml(name, vehicles[name]));
|
|
||||||
setHeading(marker, first.headingDeg);
|
|
||||||
}
|
|
||||||
|
|
||||||
let simTime = 0;
|
|
||||||
let lastFrameTime = null;
|
|
||||||
let running = true;
|
|
||||||
const TIME_SCALE = 0.175;
|
|
||||||
|
|
||||||
function resetDemo() {
|
|
||||||
simTime = 0;
|
|
||||||
lastFrameTime = null;
|
|
||||||
|
|
||||||
for (const name of vehicleNames) {
|
|
||||||
const v = vehicles[name];
|
|
||||||
const first = v.events[0];
|
|
||||||
|
|
||||||
v.currentIndex = 0;
|
|
||||||
v.current = first;
|
|
||||||
v.next = v.events[1] || null;
|
|
||||||
v.displayLat = first.lat;
|
|
||||||
v.displayLon = first.lon;
|
|
||||||
v.displayHeading = first.headingDeg;
|
|
||||||
v.lastTrailLat = first.lat;
|
|
||||||
v.lastTrailLon = first.lon;
|
|
||||||
v.presenceState = "inside";
|
|
||||||
v.presenceStateLabel = "ja";
|
|
||||||
|
|
||||||
v.marker.setLatLng([first.lat, first.lon]);
|
|
||||||
setHeading(v.marker, first.headingDeg);
|
|
||||||
v.marker.setPopupContent(popupHtml(name, v));
|
|
||||||
|
|
||||||
v.trail.setLatLngs([[first.lat, first.lon]]);
|
|
||||||
v.trail.setStyle({ opacity: 0.85 });
|
|
||||||
|
|
||||||
if (v.uncertaintyCircle) {
|
|
||||||
v.uncertaintyCircle.setLatLng([first.lat, first.lon]);
|
|
||||||
v.uncertaintyCircle.setRadius(first.uncertaintyM);
|
|
||||||
v.uncertaintyCircle.setStyle({ opacity: 0.7, fillOpacity: 0.08 });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!map.hasLayer(v.marker)) {
|
|
||||||
v.marker.addTo(map);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
function advanceVehiclePointers(v) {
|
|
||||||
while (v.next && simTime >= v.next.ts) {
|
|
||||||
v.currentIndex += 1;
|
|
||||||
v.current = v.events[v.currentIndex];
|
|
||||||
v.next = v.events[v.currentIndex + 1] || null;
|
|
||||||
|
|
||||||
v.trail.addLatLng([v.current.lat, v.current.lon]);
|
|
||||||
v.lastTrailLat = v.current.lat;
|
|
||||||
v.lastTrailLon = v.current.lon;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateVehicleDisplay(v) {
|
|
||||||
advanceVehiclePointers(v);
|
|
||||||
|
|
||||||
let lat = v.current.lat;
|
|
||||||
let lon = v.current.lon;
|
|
||||||
let heading = v.current.headingDeg;
|
|
||||||
|
|
||||||
if (v.next) {
|
|
||||||
const dt = v.next.ts - v.current.ts;
|
|
||||||
const raw = dt > 0 ? (simTime - v.current.ts) / dt : 1;
|
|
||||||
const t = smoothstep(raw);
|
|
||||||
|
|
||||||
lat = lerp(v.current.lat, v.next.lat, t);
|
|
||||||
lon = lerp(v.current.lon, v.next.lon, t);
|
|
||||||
heading = lerpAngleDeg(v.current.headingDeg, v.next.headingDeg, t);
|
|
||||||
} else {
|
|
||||||
const age = simTime - v.current.ts;
|
|
||||||
const predictFor = Math.min(age, 8);
|
|
||||||
const meters = v.current.speedMps * predictFor;
|
|
||||||
|
|
||||||
const rad = v.current.headingDeg * Math.PI / 180;
|
|
||||||
const northM = Math.cos(rad) * meters;
|
|
||||||
const eastM = Math.sin(rad) * meters;
|
|
||||||
|
|
||||||
const metersPerDegLat = 111320;
|
|
||||||
const metersPerDegLon = 111320 * Math.cos(v.current.lat * Math.PI / 180);
|
|
||||||
|
|
||||||
lat = v.current.lat + northM / metersPerDegLat;
|
|
||||||
lon = v.current.lon + eastM / metersPerDegLon;
|
|
||||||
heading = v.current.headingDeg;
|
|
||||||
}
|
|
||||||
|
|
||||||
v.displayLat = lat;
|
|
||||||
v.displayLon = lon;
|
|
||||||
v.displayHeading = heading;
|
|
||||||
|
|
||||||
const presence = getPresenceState(lat, lon, v.current.uncertaintyM);
|
|
||||||
v.presenceState = presence.code;
|
|
||||||
v.presenceStateLabel = presence.label;
|
|
||||||
|
|
||||||
if (presence.code === "inside") {
|
|
||||||
if (!map.hasLayer(v.marker)) {
|
|
||||||
v.marker.addTo(map);
|
|
||||||
}
|
|
||||||
v.marker.setLatLng([lat, lon]);
|
|
||||||
setHeading(v.marker, heading);
|
|
||||||
v.trail.setStyle({ opacity: 0.85 });
|
|
||||||
} else {
|
|
||||||
if (map.hasLayer(v.marker)) {
|
|
||||||
map.removeLayer(v.marker);
|
|
||||||
}
|
|
||||||
v.trail.setStyle({ opacity: 0.25 });
|
|
||||||
}
|
|
||||||
|
|
||||||
v.marker.setPopupContent(popupHtml(v.name, v));
|
|
||||||
|
|
||||||
if (v.uncertaintyCircle) {
|
|
||||||
v.uncertaintyCircle.setLatLng([lat, lon]);
|
|
||||||
v.uncertaintyCircle.setRadius(v.current.uncertaintyM);
|
|
||||||
|
|
||||||
if (presence.code === "inside") {
|
|
||||||
v.uncertaintyCircle.setStyle({ opacity: 0.7, fillOpacity: 0.08 });
|
|
||||||
} else if (presence.code === "possible") {
|
|
||||||
v.uncertaintyCircle.setStyle({ opacity: 0.35, fillOpacity: 0.03 });
|
|
||||||
} else {
|
|
||||||
v.uncertaintyCircle.setStyle({ opacity: 0.15, fillOpacity: 0.01 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateAlerts() {
|
|
||||||
const possibleVehicles = vehicleNames
|
|
||||||
.map(name => vehicles[name])
|
|
||||||
.filter(v => v.presenceState === "possible");
|
|
||||||
|
|
||||||
if (possibleVehicles.length === 0) {
|
|
||||||
alertsEl.innerHTML = "";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
alertsEl.innerHTML = possibleVehicles.map(v => `
|
|
||||||
<div class="alert-item">
|
|
||||||
<strong>${v.name}: mulig tilstedeværelse</strong>
|
|
||||||
Estimatet er udenfor kortet, men usikkerheden overlapper det viste område.
|
|
||||||
</div>
|
|
||||||
`).join("");
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateStatus() {
|
|
||||||
const total = VEHICLE_EVENTS.length;
|
|
||||||
const passed = VEHICLE_EVENTS.filter(e => e.ts <= simTime).length;
|
|
||||||
|
|
||||||
statusEl.textContent =
|
|
||||||
`Simuleret tid: ${simTime.toFixed(1)} s\n` +
|
|
||||||
`Events passeret: ${passed} / ${total}\n` +
|
|
||||||
`Afspilning: 2x`;
|
|
||||||
|
|
||||||
updateAlerts();
|
|
||||||
}
|
|
||||||
|
|
||||||
function tick(now) {
|
|
||||||
if (!running) return;
|
|
||||||
|
|
||||||
if (lastFrameTime === null) {
|
|
||||||
lastFrameTime = now;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dtReal = (now - lastFrameTime) / 1000;
|
|
||||||
lastFrameTime = now;
|
|
||||||
simTime += dtReal / TIME_SCALE;
|
|
||||||
|
|
||||||
for (const name of vehicleNames) {
|
|
||||||
updateVehicleDisplay(vehicles[name]);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateStatus();
|
|
||||||
requestAnimationFrame(tick);
|
|
||||||
}
|
|
||||||
|
|
||||||
playBtn.addEventListener("click", () => {
|
|
||||||
if (running) return;
|
|
||||||
running = true;
|
|
||||||
lastFrameTime = null;
|
|
||||||
requestAnimationFrame(tick);
|
|
||||||
});
|
|
||||||
|
|
||||||
pauseBtn.addEventListener("click", () => {
|
|
||||||
running = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
resetBtn.addEventListener("click", () => {
|
|
||||||
running = false;
|
|
||||||
resetDemo();
|
|
||||||
running = true;
|
|
||||||
requestAnimationFrame(tick);
|
|
||||||
});
|
|
||||||
|
|
||||||
map.on("moveend zoomend", () => {
|
|
||||||
for (const name of vehicleNames) {
|
|
||||||
updateVehicleDisplay(vehicles[name]);
|
|
||||||
}
|
|
||||||
updateStatus();
|
|
||||||
});
|
|
||||||
|
|
||||||
resetDemo();
|
|
||||||
requestAnimationFrame(tick);
|
|
||||||
137
kort7/data.js
137
kort7/data.js
|
|
@ -1,77 +1,64 @@
|
||||||
const VEHICLE_EVENTS = [
|
const VEHICLE_EVENTS = [
|
||||||
{ vehicle: "Bil 3", ts: 0, lat: 55.6780, lon: 12.5600, speedMps: 8, headingDeg: 190, uncertaintyM: 0 },
|
{ vehicle: "Bil 3", ts: 0, approxLat: 55.6780, approxLon: 12.5600, trueLat: 55.6793, trueLon: 12.5625, speedMps: 8, headingDeg: 190, uncertaintyM: 0 },
|
||||||
{ vehicle: "Bil 2", ts: 2, lat: 55.6748, lon: 12.5650, speedMps: 9, headingDeg: 45, uncertaintyM: 1200 },
|
{ vehicle: "Bil 2", ts: 2, approxLat: 55.6748, approxLon: 12.5650, trueLat: 55.6725, trueLon: 12.5683, speedMps: 9, headingDeg: 45, uncertaintyM: 1200 },
|
||||||
{ vehicle: "Bil 1", ts: 4, lat: 55.6761, lon: 12.5683, speedMps: 12, headingDeg: 90, uncertaintyM: 0 },
|
{ vehicle: "Bil 1", ts: 4, approxLat: 55.6761, approxLon: 12.5683, trueLat: 55.6777, trueLon: 12.5664, speedMps: 12, headingDeg: 90, uncertaintyM: 0 },
|
||||||
{ vehicle: "Bil 4", ts: 5, lat: 55.6728, lon: 12.5710, speedMps: 7, headingDeg: 330, uncertaintyM: 0 },
|
{ vehicle: "Bil 4", ts: 5, approxLat: 55.6728, approxLon: 12.5710, trueLat: 55.6712, trueLon: 12.5680, speedMps: 7, headingDeg: 330, uncertaintyM: 0 },
|
||||||
{ vehicle: "Bil 5", ts: 6, lat: 55.6795, lon: 12.5845, speedMps: 11, headingDeg: 260, uncertaintyM: 0 },
|
{ vehicle: "Bil 5", ts: 6, approxLat: 55.6795, approxLon: 12.5845, trueLat: 55.6814, trueLon: 12.5863, speedMps: 11, headingDeg: 260, uncertaintyM: 0 },
|
||||||
|
{ vehicle: "Bil 8", ts: 9, approxLat: 55.8600, approxLon: 12.8200, trueLat: 55.8571, trueLon: 12.8238, speedMps: 0, headingDeg: 225, uncertaintyM: 500 },
|
||||||
{ vehicle: "Bil 8", ts: 9, lat: 55.8600, lon: 12.8200, speedMps: 0, headingDeg: 225, uncertaintyM: 500 },
|
{ vehicle: "Bil 9", ts: 9, approxLat: 55.7300, approxLon: 12.4700, trueLat: 55.7262, trueLon: 12.4773, speedMps: 0, headingDeg: 135, uncertaintyM: 9000 },
|
||||||
{ vehicle: "Bil 9", ts: 9, lat: 55.7300, lon: 12.4700, speedMps: 0, headingDeg: 135, uncertaintyM: 9000 },
|
{ vehicle: "Bil 10", ts: 9, approxLat: 55.5200, approxLon: 12.9500, trueLat: 55.5247, trueLon: 12.9402, speedMps: 0, headingDeg: 315, uncertaintyM: 42000 },
|
||||||
{ vehicle: "Bil 10", ts: 9, lat: 55.5200, lon: 12.9500, speedMps: 0, headingDeg: 315, uncertaintyM: 42000 },
|
{ vehicle: "Bil 3", ts: 12, approxLat: 55.6772, approxLon: 12.5603, trueLat: 55.6785, trueLon: 12.5628, speedMps: 8, headingDeg: 195, uncertaintyM: 0 },
|
||||||
|
{ vehicle: "Bil 2", ts: 13, approxLat: 55.6760, approxLon: 12.5670, trueLat: 55.6737, trueLon: 12.5703, speedMps: 10, headingDeg: 50, uncertaintyM: 1000 },
|
||||||
{ vehicle: "Bil 3", ts: 12, lat: 55.6772, lon: 12.5603, speedMps: 8, headingDeg: 195, uncertaintyM: 0 },
|
{ vehicle: "Bil 1", ts: 15, approxLat: 55.6762, approxLon: 12.5720, trueLat: 55.6778, trueLon: 12.5701, speedMps: 13, headingDeg: 95, uncertaintyM: 0 },
|
||||||
{ vehicle: "Bil 2", ts: 13, lat: 55.6760, lon: 12.5670, speedMps: 10, headingDeg: 50, uncertaintyM: 1000 },
|
{ vehicle: "Bil 4", ts: 16, approxLat: 55.6736, approxLon: 12.5698, trueLat: 55.6720, trueLon: 12.5668, speedMps: 8, headingDeg: 340, uncertaintyM: 0 },
|
||||||
{ vehicle: "Bil 1", ts: 15, lat: 55.6762, lon: 12.5720, speedMps: 13, headingDeg: 95, uncertaintyM: 0 },
|
{ vehicle: "Bil 5", ts: 18, approxLat: 55.6793, approxLon: 12.5815, trueLat: 55.6812, trueLon: 12.5833, speedMps: 12, headingDeg: 255, uncertaintyM: 0 },
|
||||||
{ vehicle: "Bil 4", ts: 16, lat: 55.6736, lon: 12.5698, speedMps: 8, headingDeg: 340, uncertaintyM: 0 },
|
{ vehicle: "Bil 3", ts: 24, approxLat: 55.6765, approxLon: 12.5610, trueLat: 55.6778, trueLon: 12.5635, speedMps: 9, headingDeg: 200, uncertaintyM: 0 },
|
||||||
{ vehicle: "Bil 5", ts: 18, lat: 55.6793, lon: 12.5815, speedMps: 12, headingDeg: 255, uncertaintyM: 0 },
|
{ vehicle: "Bil 2", ts: 26, approxLat: 55.6772, approxLon: 12.5692, trueLat: 55.6749, trueLon: 12.5725, speedMps: 11, headingDeg: 55, uncertaintyM: 900 },
|
||||||
|
{ vehicle: "Bil 1", ts: 27, approxLat: 55.6763, approxLon: 12.5758, trueLat: 55.6779, trueLon: 12.5739, speedMps: 14, headingDeg: 100, uncertaintyM: 0 },
|
||||||
{ vehicle: "Bil 3", ts: 24, lat: 55.6765, lon: 12.5610, speedMps: 9, headingDeg: 200, uncertaintyM: 0 },
|
{ vehicle: "Bil 4", ts: 28, approxLat: 55.6746, approxLon: 12.5688, trueLat: 55.6730, trueLon: 12.5658, speedMps: 8, headingDeg: 350, uncertaintyM: 0 },
|
||||||
{ vehicle: "Bil 2", ts: 26, lat: 55.6772, lon: 12.5692, speedMps: 11, headingDeg: 55, uncertaintyM: 900 },
|
{ vehicle: "Bil 5", ts: 29, approxLat: 55.6790, approxLon: 12.5784, trueLat: 55.6809, trueLon: 12.5802, speedMps: 12, headingDeg: 250, uncertaintyM: 0 },
|
||||||
{ vehicle: "Bil 1", ts: 27, lat: 55.6763, lon: 12.5758, speedMps: 14, headingDeg: 100, uncertaintyM: 0 },
|
{ vehicle: "Bil 3", ts: 38, approxLat: 55.6758, approxLon: 12.5620, trueLat: 55.6771, trueLon: 12.5645, speedMps: 9, headingDeg: 210, uncertaintyM: 0 },
|
||||||
{ vehicle: "Bil 4", ts: 28, lat: 55.6746, lon: 12.5688, speedMps: 8, headingDeg: 350, uncertaintyM: 0 },
|
{ vehicle: "Bil 2", ts: 40, approxLat: 55.6785, approxLon: 12.5716, trueLat: 55.6762, trueLon: 12.5749, speedMps: 12, headingDeg: 60, uncertaintyM: 850 },
|
||||||
{ vehicle: "Bil 5", ts: 29, lat: 55.6790, lon: 12.5784, speedMps: 12, headingDeg: 250, uncertaintyM: 0 },
|
{ vehicle: "Bil 1", ts: 41, approxLat: 55.6764, approxLon: 12.5798, trueLat: 55.6780, trueLon: 12.5779, speedMps: 14, headingDeg: 105, uncertaintyM: 0 },
|
||||||
|
{ vehicle: "Bil 4", ts: 42, approxLat: 55.6757, approxLon: 12.5682, trueLat: 55.6741, trueLon: 12.5652, speedMps: 9, headingDeg: 0, uncertaintyM: 0 },
|
||||||
{ vehicle: "Bil 3", ts: 38, lat: 55.6758, lon: 12.5620, speedMps: 9, headingDeg: 210, uncertaintyM: 0 },
|
{ vehicle: "Bil 5", ts: 43, approxLat: 55.6786, approxLon: 12.5753, trueLat: 55.6805, trueLon: 12.5771, speedMps: 11, headingDeg: 245, uncertaintyM: 0 },
|
||||||
{ vehicle: "Bil 2", ts: 40, lat: 55.6785, lon: 12.5716, speedMps: 12, headingDeg: 60, uncertaintyM: 850 },
|
{ vehicle: "Bil 8", ts: 44, approxLat: 55.8600, approxLon: 12.8200, trueLat: 55.8571, trueLon: 12.8238, speedMps: 0, headingDeg: 225, uncertaintyM: 500 },
|
||||||
{ vehicle: "Bil 1", ts: 41, lat: 55.6764, lon: 12.5798, speedMps: 14, headingDeg: 105, uncertaintyM: 0 },
|
{ vehicle: "Bil 9", ts: 44, approxLat: 55.7300, approxLon: 12.4700, trueLat: 55.7262, trueLon: 12.4773, speedMps: 0, headingDeg: 135, uncertaintyM: 9000 },
|
||||||
{ vehicle: "Bil 4", ts: 42, lat: 55.6757, lon: 12.5682, speedMps: 9, headingDeg: 0, uncertaintyM: 0 },
|
{ vehicle: "Bil 10", ts: 44, approxLat: 55.5200, approxLon: 12.9500, trueLat: 55.5247, trueLon: 12.9402, speedMps: 0, headingDeg: 315, uncertaintyM: 42000 },
|
||||||
{ vehicle: "Bil 5", ts: 43, lat: 55.6786, lon: 12.5753, speedMps: 11, headingDeg: 245, uncertaintyM: 0 },
|
{ vehicle: "Bil 3", ts: 50, approxLat: 55.6752, approxLon: 12.5634, trueLat: 55.6765, trueLon: 12.5659, speedMps: 10, headingDeg: 220, uncertaintyM: 0 },
|
||||||
|
{ vehicle: "Bil 2", ts: 54, approxLat: 55.6796, approxLon: 12.5744, trueLat: 55.6773, trueLon: 12.5777, speedMps: 12, headingDeg: 70, uncertaintyM: 800 },
|
||||||
{ vehicle: "Bil 8", ts: 44, lat: 55.8600, lon: 12.8200, speedMps: 0, headingDeg: 225, uncertaintyM: 500 },
|
{ vehicle: "Bil 1", ts: 55, approxLat: 55.6766, approxLon: 12.5837, trueLat: 55.6782, trueLon: 12.5818, speedMps: 15, headingDeg: 110, uncertaintyM: 0 },
|
||||||
{ vehicle: "Bil 9", ts: 44, lat: 55.7300, lon: 12.4700, speedMps: 0, headingDeg: 135, uncertaintyM: 9000 },
|
{ vehicle: "Bil 4", ts: 56, approxLat: 55.6769, approxLon: 12.5682, trueLat: 55.6753, trueLon: 12.5652, speedMps: 9, headingDeg: 10, uncertaintyM: 0 },
|
||||||
{ vehicle: "Bil 10", ts: 44, lat: 55.5200, lon: 12.9500, speedMps: 0, headingDeg: 315, uncertaintyM: 42000 },
|
{ vehicle: "Bil 5", ts: 57, approxLat: 55.6781, approxLon: 12.5722, trueLat: 55.6800, trueLon: 12.5740, speedMps: 10, headingDeg: 240, uncertaintyM: 0 },
|
||||||
|
{ vehicle: "Bil 3", ts: 66, approxLat: 55.6747, approxLon: 12.5652, trueLat: 55.6760, trueLon: 12.5677, speedMps: 10, headingDeg: 230, uncertaintyM: 0 },
|
||||||
{ vehicle: "Bil 3", ts: 50, lat: 55.6752, lon: 12.5634, speedMps: 10, headingDeg: 220, uncertaintyM: 0 },
|
{ vehicle: "Bil 2", ts: 68, approxLat: 55.6804, approxLon: 12.5776, trueLat: 55.6781, trueLon: 12.5809, speedMps: 11, headingDeg: 85, uncertaintyM: 950 },
|
||||||
{ vehicle: "Bil 2", ts: 54, lat: 55.6796, lon: 12.5744, speedMps: 12, headingDeg: 70, uncertaintyM: 800 },
|
{ vehicle: "Bil 1", ts: 69, approxLat: 55.6768, approxLon: 12.5875, trueLat: 55.6784, trueLon: 12.5856, speedMps: 14, headingDeg: 115, uncertaintyM: 0 },
|
||||||
{ vehicle: "Bil 1", ts: 55, lat: 55.6766, lon: 12.5837, speedMps: 15, headingDeg: 110, uncertaintyM: 0 },
|
{ vehicle: "Bil 4", ts: 70, approxLat: 55.6781, approxLon: 12.5688, trueLat: 55.6765, trueLon: 12.5658, speedMps: 10, headingDeg: 20, uncertaintyM: 0 },
|
||||||
{ vehicle: "Bil 4", ts: 56, lat: 55.6769, lon: 12.5682, speedMps: 9, headingDeg: 10, uncertaintyM: 0 },
|
{ vehicle: "Bil 5", ts: 72, approxLat: 55.6776, approxLon: 12.5692, trueLat: 55.6795, trueLon: 12.5710, speedMps: 10, headingDeg: 235, uncertaintyM: 0 },
|
||||||
{ vehicle: "Bil 5", ts: 57, lat: 55.6781, lon: 12.5722, speedMps: 10, headingDeg: 240, uncertaintyM: 0 },
|
{ vehicle: "Bil 3", ts: 82, approxLat: 55.6744, approxLon: 12.5673, trueLat: 55.6757, trueLon: 12.5698, speedMps: 9, headingDeg: 240, uncertaintyM: 0 },
|
||||||
|
{ vehicle: "Bil 2", ts: 84, approxLat: 55.6810, approxLon: 12.5808, trueLat: 55.6787, trueLon: 12.5841, speedMps: 10, headingDeg: 100, uncertaintyM: 1100 },
|
||||||
{ vehicle: "Bil 3", ts: 66, lat: 55.6747, lon: 12.5652, speedMps: 10, headingDeg: 230, uncertaintyM: 0 },
|
{ vehicle: "Bil 1", ts: 85, approxLat: 55.6770, approxLon: 12.5912, trueLat: 55.6786, trueLon: 12.5893, speedMps: 13, headingDeg: 120, uncertaintyM: 0 },
|
||||||
{ vehicle: "Bil 2", ts: 68, lat: 55.6804, lon: 12.5776, speedMps: 11, headingDeg: 85, uncertaintyM: 950 },
|
{ vehicle: "Bil 4", ts: 87, approxLat: 55.6792, approxLon: 12.5700, trueLat: 55.6776, trueLon: 12.5670, speedMps: 10, headingDeg: 30, uncertaintyM: 0 },
|
||||||
{ vehicle: "Bil 1", ts: 69, lat: 55.6768, lon: 12.5875, speedMps: 14, headingDeg: 115, uncertaintyM: 0 },
|
{ vehicle: "Bil 5", ts: 88, approxLat: 55.6771, approxLon: 12.5662, trueLat: 55.6790, trueLon: 12.5680, speedMps: 9, headingDeg: 230, uncertaintyM: 0 },
|
||||||
{ vehicle: "Bil 4", ts: 70, lat: 55.6781, lon: 12.5688, speedMps: 10, headingDeg: 20, uncertaintyM: 0 },
|
{ vehicle: "Bil 8", ts: 90, approxLat: 55.8600, approxLon: 12.8200, trueLat: 55.8571, trueLon: 12.8238, speedMps: 0, headingDeg: 225, uncertaintyM: 500 },
|
||||||
{ vehicle: "Bil 5", ts: 72, lat: 55.6776, lon: 12.5692, speedMps: 10, headingDeg: 235, uncertaintyM: 0 },
|
{ vehicle: "Bil 9", ts: 90, approxLat: 55.7300, approxLon: 12.4700, trueLat: 55.7262, trueLon: 12.4773, speedMps: 0, headingDeg: 135, uncertaintyM: 9000 },
|
||||||
|
{ vehicle: "Bil 10", ts: 90, approxLat: 55.5200, approxLon: 12.9500, trueLat: 55.5247, trueLon: 12.9402, speedMps: 0, headingDeg: 315, uncertaintyM: 42000 },
|
||||||
{ vehicle: "Bil 3", ts: 82, lat: 55.6744, lon: 12.5673, speedMps: 9, headingDeg: 240, uncertaintyM: 0 },
|
{ vehicle: "Bil 3", ts: 96, approxLat: 55.6742, approxLon: 12.5696, trueLat: 55.6755, trueLon: 12.5721, speedMps: 8, headingDeg: 250, uncertaintyM: 0 },
|
||||||
{ vehicle: "Bil 2", ts: 84, lat: 55.6810, lon: 12.5808, speedMps: 10, headingDeg: 100, uncertaintyM: 1100 },
|
{ vehicle: "Bil 2", ts: 98, approxLat: 55.6812, approxLon: 12.5842, trueLat: 55.6789, trueLon: 12.5875, speedMps: 10, headingDeg: 115, uncertaintyM: 1200 },
|
||||||
{ vehicle: "Bil 1", ts: 85, lat: 55.6770, lon: 12.5912, speedMps: 13, headingDeg: 120, uncertaintyM: 0 },
|
{ vehicle: "Bil 1", ts: 99, approxLat: 55.6772, approxLon: 12.5948, trueLat: 55.6788, trueLon: 12.5929, speedMps: 12, headingDeg: 125, uncertaintyM: 0 },
|
||||||
{ vehicle: "Bil 4", ts: 87, lat: 55.6792, lon: 12.5700, speedMps: 10, headingDeg: 30, uncertaintyM: 0 },
|
{ vehicle: "Bil 4", ts: 101, approxLat: 55.6800, approxLon: 12.5716, trueLat: 55.6784, trueLon: 12.5686, speedMps: 9, headingDeg: 40, uncertaintyM: 0 },
|
||||||
{ vehicle: "Bil 5", ts: 88, lat: 55.6771, lon: 12.5662, speedMps: 9, headingDeg: 230, uncertaintyM: 0 },
|
{ vehicle: "Bil 5", ts: 102, approxLat: 55.6766, approxLon: 12.5634, trueLat: 55.6785, trueLon: 12.5652, speedMps: 9, headingDeg: 225, uncertaintyM: 0 },
|
||||||
|
{ vehicle: "Bil 3", ts: 112, approxLat: 55.6742, approxLon: 12.5720, trueLat: 55.6755, trueLon: 12.5745, speedMps: 8, headingDeg: 260, uncertaintyM: 0 },
|
||||||
{ vehicle: "Bil 8", ts: 90, lat: 55.8600, lon: 12.8200, speedMps: 0, headingDeg: 225, uncertaintyM: 500 },
|
{ vehicle: "Bil 2", ts: 114, approxLat: 55.6811, approxLon: 12.5877, trueLat: 55.6788, trueLon: 12.5910, speedMps: 9, headingDeg: 125, uncertaintyM: 1000 },
|
||||||
{ vehicle: "Bil 9", ts: 90, lat: 55.7300, lon: 12.4700, speedMps: 0, headingDeg: 135, uncertaintyM: 9000 },
|
{ vehicle: "Bil 1", ts: 115, approxLat: 55.6775, approxLon: 12.5982, trueLat: 55.6791, trueLon: 12.5963, speedMps: 11, headingDeg: 130, uncertaintyM: 0 },
|
||||||
{ vehicle: "Bil 10", ts: 90,lat: 55.5200, lon: 12.9500, speedMps: 0, headingDeg: 315, uncertaintyM: 42000 },
|
{ vehicle: "Bil 4", ts: 117, approxLat: 55.6806, approxLon: 12.5736, trueLat: 55.6790, trueLon: 12.5706, speedMps: 8, headingDeg: 50, uncertaintyM: 0 },
|
||||||
|
{ vehicle: "Bil 5", ts: 118, approxLat: 55.6761, approxLon: 12.5608, trueLat: 55.6780, trueLon: 12.5626, speedMps: 8, headingDeg: 220, uncertaintyM: 0 },
|
||||||
{ vehicle: "Bil 3", ts: 96, lat: 55.6742, lon: 12.5696, speedMps: 8, headingDeg: 250, uncertaintyM: 0 },
|
{ vehicle: "Bil 3", ts: 128, approxLat: 55.6744, approxLon: 12.5742, trueLat: 55.6757, trueLon: 12.5767, speedMps: 7, headingDeg: 270, uncertaintyM: 0 },
|
||||||
{ vehicle: "Bil 2", ts: 98, lat: 55.6812, lon: 12.5842, speedMps: 10, headingDeg: 115, uncertaintyM: 1200 },
|
{ vehicle: "Bil 2", ts: 130, approxLat: 55.6808, approxLon: 12.5910, trueLat: 55.6785, trueLon: 12.5943, speedMps: 8, headingDeg: 130, uncertaintyM: 950 },
|
||||||
{ vehicle: "Bil 1", ts: 99, lat: 55.6772, lon: 12.5948, speedMps: 12, headingDeg: 125, uncertaintyM: 0 },
|
{ vehicle: "Bil 1", ts: 131, approxLat: 55.6778, approxLon: 12.6016, trueLat: 55.6794, trueLon: 12.5997, speedMps: 10, headingDeg: 135, uncertaintyM: 0 },
|
||||||
{ vehicle: "Bil 4", ts: 101,lat: 55.6800, lon: 12.5716, speedMps: 9, headingDeg: 40, uncertaintyM: 0 },
|
{ vehicle: "Bil 4", ts: 133, approxLat: 55.6809, approxLon: 12.5758, trueLat: 55.6793, trueLon: 12.5728, speedMps: 7, headingDeg: 60, uncertaintyM: 0 },
|
||||||
{ vehicle: "Bil 5", ts: 102,lat: 55.6766, lon: 12.5634, speedMps: 9, headingDeg: 225, uncertaintyM: 0 },
|
{ vehicle: "Bil 5", ts: 134, approxLat: 55.6756, approxLon: 12.5584, trueLat: 55.6775, trueLon: 12.5602, speedMps: 8, headingDeg: 215, uncertaintyM: 0 },
|
||||||
|
{ vehicle: "Bil 8", ts: 134, approxLat: 55.8600, approxLon: 12.8200, trueLat: 55.8571, trueLon: 12.8238, speedMps: 0, headingDeg: 225, uncertaintyM: 500 },
|
||||||
{ vehicle: "Bil 3", ts: 112,lat: 55.6742, lon: 12.5720, speedMps: 8, headingDeg: 260, uncertaintyM: 0 },
|
{ vehicle: "Bil 9", ts: 134, approxLat: 55.7300, approxLon: 12.4700, trueLat: 55.7262, trueLon: 12.4773, speedMps: 0, headingDeg: 135, uncertaintyM: 9000 },
|
||||||
{ vehicle: "Bil 2", ts: 114,lat: 55.6811, lon: 12.5877, speedMps: 9, headingDeg: 125, uncertaintyM: 1000 },
|
{ vehicle: "Bil 10", ts: 134, approxLat: 55.5200, approxLon: 12.9500, trueLat: 55.5247, trueLon: 12.9402, speedMps: 0, headingDeg: 315, uncertaintyM: 42000 }
|
||||||
{ vehicle: "Bil 1", ts: 115,lat: 55.6775, lon: 12.5982, speedMps: 11, headingDeg: 130, uncertaintyM: 0 },
|
|
||||||
{ vehicle: "Bil 4", ts: 117,lat: 55.6806, lon: 12.5736, speedMps: 8, headingDeg: 50, uncertaintyM: 0 },
|
|
||||||
{ vehicle: "Bil 5", ts: 118,lat: 55.6761, lon: 12.5608, speedMps: 8, headingDeg: 220, uncertaintyM: 0 },
|
|
||||||
|
|
||||||
{ vehicle: "Bil 3", ts: 128,lat: 55.6744, lon: 12.5742, speedMps: 7, headingDeg: 270, uncertaintyM: 0 },
|
|
||||||
{ vehicle: "Bil 2", ts: 130,lat: 55.6808, lon: 12.5910, speedMps: 8, headingDeg: 130, uncertaintyM: 950 },
|
|
||||||
{ vehicle: "Bil 1", ts: 131,lat: 55.6778, lon: 12.6016, speedMps: 10, headingDeg: 135, uncertaintyM: 0 },
|
|
||||||
{ vehicle: "Bil 4", ts: 133,lat: 55.6809, lon: 12.5758, speedMps: 7, headingDeg: 60, uncertaintyM: 0 },
|
|
||||||
{ vehicle: "Bil 5", ts: 134,lat: 55.6756, lon: 12.5584, speedMps: 8, headingDeg: 215, uncertaintyM: 0 },
|
|
||||||
|
|
||||||
{ vehicle: "Bil 8", ts: 134,lat: 55.8600, lon: 12.8200, speedMps: 0, headingDeg: 225, uncertaintyM: 500 },
|
|
||||||
{ vehicle: "Bil 9", ts: 134,lat: 55.7300, lon: 12.4700, speedMps: 0, headingDeg: 135, uncertaintyM: 9000 },
|
|
||||||
{ vehicle: "Bil 10", ts: 134,lat: 55.5200, lon: 12.9500, speedMps: 0, headingDeg: 315, uncertaintyM: 42000 }
|
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
const VEHICLE_EVENTS = [
|
|
||||||
{ vehicle: "Bil 3", ts: 0, lat: 55.6780, lon: 12.5600, speedMps: 8, headingDeg: 190, uncertaintyM: 0 },
|
|
||||||
{ vehicle: "Bil 2", ts: 2, lat: 55.6748, lon: 12.5650, speedMps: 9, headingDeg: 45, uncertaintyM: 1200 },
|
|
||||||
{ vehicle: "Bil 1", ts: 4, lat: 55.6761, lon: 12.5683, speedMps: 12, headingDeg: 90, uncertaintyM: 0 },
|
|
||||||
{ vehicle: "Bil 4", ts: 5, lat: 55.6728, lon: 12.5710, speedMps: 7, headingDeg: 330, uncertaintyM: 0 },
|
|
||||||
{ vehicle: "Bil 5", ts: 6, lat: 55.6795, lon: 12.5845, speedMps: 11, headingDeg: 260, uncertaintyM: 0 },
|
|
||||||
|
|
||||||
{ vehicle: "Bil 3", ts: 12, lat: 55.6772, lon: 12.5603, speedMps: 8, headingDeg: 195, uncertaintyM: 0 },
|
|
||||||
{ vehicle: "Bil 2", ts: 13, lat: 55.6760, lon: 12.5670, speedMps: 10, headingDeg: 50, uncertaintyM: 1000 },
|
|
||||||
{ vehicle: "Bil 1", ts: 15, lat: 55.6762, lon: 12.5720, speedMps: 13, headingDeg: 95, uncertaintyM: 0 },
|
|
||||||
{ vehicle: "Bil 4", ts: 16, lat: 55.6736, lon: 12.5698, speedMps: 8, headingDeg: 340, uncertaintyM: 0 },
|
|
||||||
{ vehicle: "Bil 5", ts: 18, lat: 55.6793, lon: 12.5815, speedMps: 12, headingDeg: 255, uncertaintyM: 0 },
|
|
||||||
|
|
||||||
{ vehicle: "Bil 3", ts: 24, lat: 55.6765, lon: 12.5610, speedMps: 9, headingDeg: 200, uncertaintyM: 0 },
|
|
||||||
{ vehicle: "Bil 2", ts: 26, lat: 55.6772, lon: 12.5692, speedMps: 11, headingDeg: 55, uncertaintyM: 900 },
|
|
||||||
{ vehicle: "Bil 1", ts: 27, lat: 55.6763, lon: 12.5758, speedMps: 14, headingDeg: 100, uncertaintyM: 0 },
|
|
||||||
{ vehicle: "Bil 4", ts: 28, lat: 55.6746, lon: 12.5688, speedMps: 8, headingDeg: 350, uncertaintyM: 0 },
|
|
||||||
{ vehicle: "Bil 5", ts: 29, lat: 55.6790, lon: 12.5784, speedMps: 12, headingDeg: 250, uncertaintyM: 0 },
|
|
||||||
|
|
||||||
{ vehicle: "Bil 3", ts: 38, lat: 55.6758, lon: 12.5620, speedMps: 9, headingDeg: 210, uncertaintyM: 0 },
|
|
||||||
{ vehicle: "Bil 2", ts: 40, lat: 55.6785, lon: 12.5716, speedMps: 12, headingDeg: 60, uncertaintyM: 850 },
|
|
||||||
{ vehicle: "Bil 1", ts: 41, lat: 55.6764, lon: 12.5798, speedMps: 14, headingDeg: 105, uncertaintyM: 0 },
|
|
||||||
{ vehicle: "Bil 4", ts: 42, lat: 55.6757, lon: 12.5682, speedMps: 9, headingDeg: 0, uncertaintyM: 0 },
|
|
||||||
{ vehicle: "Bil 5", ts: 43, lat: 55.6786, lon: 12.5753, speedMps: 11, headingDeg: 245, uncertaintyM: 0 },
|
|
||||||
|
|
||||||
{ vehicle: "Bil 3", ts: 50, lat: 55.6752, lon: 12.5634, speedMps: 10, headingDeg: 220, uncertaintyM: 0 },
|
|
||||||
{ vehicle: "Bil 2", ts: 54, lat: 55.6796, lon: 12.5744, speedMps: 12, headingDeg: 70, uncertaintyM: 800 },
|
|
||||||
{ vehicle: "Bil 1", ts: 55, lat: 55.6766, lon: 12.5837, speedMps: 15, headingDeg: 110, uncertaintyM: 0 },
|
|
||||||
{ vehicle: "Bil 4", ts: 56, lat: 55.6769, lon: 12.5682, speedMps: 9, headingDeg: 10, uncertaintyM: 0 },
|
|
||||||
{ vehicle: "Bil 5", ts: 57, lat: 55.6781, lon: 12.5722, speedMps: 10, headingDeg: 240, uncertaintyM: 0 },
|
|
||||||
|
|
||||||
{ vehicle: "Bil 3", ts: 66, lat: 55.6747, lon: 12.5652, speedMps: 10, headingDeg: 230, uncertaintyM: 0 },
|
|
||||||
{ vehicle: "Bil 2", ts: 68, lat: 55.6804, lon: 12.5776, speedMps: 11, headingDeg: 85, uncertaintyM: 950 },
|
|
||||||
{ vehicle: "Bil 1", ts: 69, lat: 55.6768, lon: 12.5875, speedMps: 14, headingDeg: 115, uncertaintyM: 0 },
|
|
||||||
{ vehicle: "Bil 4", ts: 70, lat: 55.6781, lon: 12.5688, speedMps: 10, headingDeg: 20, uncertaintyM: 0 },
|
|
||||||
{ vehicle: "Bil 5", ts: 72, lat: 55.6776, lon: 12.5692, speedMps: 10, headingDeg: 235, uncertaintyM: 0 },
|
|
||||||
|
|
||||||
{ vehicle: "Bil 3", ts: 82, lat: 55.6744, lon: 12.5673, speedMps: 9, headingDeg: 240, uncertaintyM: 0 },
|
|
||||||
{ vehicle: "Bil 2", ts: 84, lat: 55.6810, lon: 12.5808, speedMps: 10, headingDeg: 100, uncertaintyM: 1100 },
|
|
||||||
{ vehicle: "Bil 1", ts: 85, lat: 55.6770, lon: 12.5912, speedMps: 13, headingDeg: 120, uncertaintyM: 0 },
|
|
||||||
{ vehicle: "Bil 4", ts: 87, lat: 55.6792, lon: 12.5700, speedMps: 10, headingDeg: 30, uncertaintyM: 0 },
|
|
||||||
{ vehicle: "Bil 5", ts: 88, lat: 55.6771, lon: 12.5662, speedMps: 9, headingDeg: 230, uncertaintyM: 0 },
|
|
||||||
|
|
||||||
{ vehicle: "Bil 3", ts: 96, lat: 55.6742, lon: 12.5696, speedMps: 8, headingDeg: 250, uncertaintyM: 0 },
|
|
||||||
{ vehicle: "Bil 2", ts: 98, lat: 55.6812, lon: 12.5842, speedMps: 10, headingDeg: 115, uncertaintyM: 1200 },
|
|
||||||
{ vehicle: "Bil 1", ts: 99, lat: 55.6772, lon: 12.5948, speedMps: 12, headingDeg: 125, uncertaintyM: 0 },
|
|
||||||
{ vehicle: "Bil 4", ts: 101,lat: 55.6800, lon: 12.5716, speedMps: 9, headingDeg: 40, uncertaintyM: 0 },
|
|
||||||
{ vehicle: "Bil 5", ts: 102,lat: 55.6766, lon: 12.5634, speedMps: 9, headingDeg: 225, uncertaintyM: 0 },
|
|
||||||
|
|
||||||
{ vehicle: "Bil 3", ts: 112,lat: 55.6742, lon: 12.5720, speedMps: 8, headingDeg: 260, uncertaintyM: 0 },
|
|
||||||
{ vehicle: "Bil 2", ts: 114,lat: 55.6811, lon: 12.5877, speedMps: 9, headingDeg: 125, uncertaintyM: 1000 },
|
|
||||||
{ vehicle: "Bil 1", ts: 115,lat: 55.6775, lon: 12.5982, speedMps: 11, headingDeg: 130, uncertaintyM: 0 },
|
|
||||||
{ vehicle: "Bil 4", ts: 117,lat: 55.6806, lon: 12.5736, speedMps: 8, headingDeg: 50, uncertaintyM: 0 },
|
|
||||||
{ vehicle: "Bil 5", ts: 118,lat: 55.6761, lon: 12.5608, speedMps: 8, headingDeg: 220, uncertaintyM: 0 },
|
|
||||||
|
|
||||||
{ vehicle: "Bil 3", ts: 128,lat: 55.6744, lon: 12.5742, speedMps: 7, headingDeg: 270, uncertaintyM: 0 },
|
|
||||||
{ vehicle: "Bil 2", ts: 130,lat: 55.6808, lon: 12.5910, speedMps: 8, headingDeg: 130, uncertaintyM: 950 },
|
|
||||||
{ vehicle: "Bil 1", ts: 131,lat: 55.6778, lon: 12.6016, speedMps: 10, headingDeg: 135, uncertaintyM: 0 },
|
|
||||||
{ vehicle: "Bil 4", ts: 133,lat: 55.6809, lon: 12.5758, speedMps: 7, headingDeg: 60, uncertaintyM: 0 },
|
|
||||||
{ vehicle: "Bil 5", ts: 134,lat: 55.6756, lon: 12.5584, speedMps: 8, headingDeg: 215, uncertaintyM: 0 }
|
|
||||||
];
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
--- data.js
|
|
||||||
+++ data.js
|
|
||||||
@@ -53,5 +53,15 @@
|
|
||||||
{ vehicle: "Bil 3", ts: 128,lat: 55.6744, lon: 12.5742, speedMps: 7, headingDeg: 270, uncertaintyM: 0 },
|
|
||||||
{ vehicle: "Bil 2", ts: 130,lat: 55.6808, lon: 12.5910, speedMps: 8, headingDeg: 130, uncertaintyM: 950 },
|
|
||||||
{ vehicle: "Bil 1", ts: 131,lat: 55.6778, lon: 12.6016, speedMps: 10, headingDeg: 135, uncertaintyM: 0 },
|
|
||||||
{ vehicle: "Bil 4", ts: 133,lat: 55.6809, lon: 12.5758, speedMps: 7, headingDeg: 60, uncertaintyM: 0 },
|
|
||||||
- { vehicle: "Bil 5", ts: 134,lat: 55.6756, lon: 12.5584, speedMps: 8, headingDeg: 215, uncertaintyM: 0 }
|
|
||||||
+ { vehicle: "Bil 5", ts: 134,lat: 55.6756, lon: 12.5584, speedMps: 8, headingDeg: 215, uncertaintyM: 0 },
|
|
||||||
+
|
|
||||||
+ { vehicle: "Bil 6", ts: 8, lat: 55.7080, lon: 12.5050, speedMps: 0, headingDeg: 90, uncertaintyM: 15000 },
|
|
||||||
+ { vehicle: "Bil 6", ts: 44, lat: 55.7080, lon: 12.5050, speedMps: 0, headingDeg: 90, uncertaintyM: 15000 },
|
|
||||||
+ { vehicle: "Bil 6", ts: 90, lat: 55.7080, lon: 12.5050, speedMps: 0, headingDeg: 90, uncertaintyM: 15000 },
|
|
||||||
+ { vehicle: "Bil 6", ts: 134,lat: 55.7080, lon: 12.5050, speedMps: 0, headingDeg: 90, uncertaintyM: 15000 },
|
|
||||||
+
|
|
||||||
+ { vehicle: "Bil 7", ts: 10, lat: 55.7065, lon: 12.6175, speedMps: 0, headingDeg: 270, uncertaintyM: 2800 },
|
|
||||||
+ { vehicle: "Bil 7", ts: 46, lat: 55.7065, lon: 12.6175, speedMps: 0, headingDeg: 270, uncertaintyM: 2800 },
|
|
||||||
+ { vehicle: "Bil 7", ts: 92, lat: 55.7065, lon: 12.6175, speedMps: 0, headingDeg: 270, uncertaintyM: 2800 },
|
|
||||||
+ { vehicle: "Bil 7", ts: 134,lat: 55.7065, lon: 12.6175, speedMps: 0, headingDeg: 270, uncertaintyM: 2800 }
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
<!doctype html>
|
|
||||||
<html lang="da">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
||||||
<title>Vehicle stream demo</title>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
html, body {
|
|
||||||
height: 100%;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#map {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hud {
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
left: 10px;
|
|
||||||
z-index: 1000;
|
|
||||||
background: rgba(255,255,255,0.95);
|
|
||||||
padding: 10px 12px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
font: 13px/1.35 system-ui, sans-serif;
|
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hud button {
|
|
||||||
margin-right: 8px;
|
|
||||||
padding: 6px 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hud .status {
|
|
||||||
margin-top: 8px;
|
|
||||||
white-space: pre-line;
|
|
||||||
}
|
|
||||||
|
|
||||||
.car {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
transform-origin: 12px 12px;
|
|
||||||
will-change: transform;
|
|
||||||
filter: drop-shadow(0 1px 1px rgba(0,0,0,.35));
|
|
||||||
}
|
|
||||||
|
|
||||||
.car svg {
|
|
||||||
display: block;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="map"></div>
|
|
||||||
|
|
||||||
<div class="hud">
|
|
||||||
<div>
|
|
||||||
<button id="playBtn">Play</button>
|
|
||||||
<button id="pauseBtn">Pause</button>
|
|
||||||
<button id="resetBtn">Reset</button>
|
|
||||||
</div>
|
|
||||||
<div class="status" id="status"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="data.js"></script>
|
|
||||||
<script src="app.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
241
kort7/kort7.html
241
kort7/kort7.html
|
|
@ -5,89 +5,79 @@
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<title>Vehicle stream demo</title>
|
<title>Vehicle stream demo</title>
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
|
<link rel="stylesheet" href="vendor/leaflet/leaflet.css">
|
||||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
<script src="vendor/leaflet/leaflet.js"></script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
html, body {
|
html, body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
#map {
|
body {
|
||||||
height: 100%;
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
background: #f3f4f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hud {
|
.toolbar {
|
||||||
position: absolute;
|
display: flex;
|
||||||
top: 10px;
|
gap: 10px;
|
||||||
left: 10px;
|
align-items: center;
|
||||||
z-index: 1000;
|
padding: 12px 14px;
|
||||||
background: rgba(255,255,255,0.95);
|
background: rgba(255,255,255,0.96);
|
||||||
padding: 10px 12px;
|
border-bottom: 1px solid #d1d5db;
|
||||||
border-radius: 8px;
|
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||||
border: 1px solid #ccc;
|
|
||||||
font: 13px/1.35 system-ui, sans-serif;
|
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.hud button {
|
.toolbar button {
|
||||||
margin-right: 8px;
|
padding: 7px 12px;
|
||||||
padding: 6px 10px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hud .status {
|
.toolbar .status {
|
||||||
margin-top: 8px;
|
margin-left: 6px;
|
||||||
white-space: pre-line;
|
white-space: pre-line;
|
||||||
}
|
|
||||||
|
|
||||||
.hud .selection {
|
|
||||||
margin-top: 10px;
|
|
||||||
padding-top: 10px;
|
|
||||||
border-top: 1px solid rgba(0,0,0,0.08);
|
|
||||||
max-width: 320px;
|
|
||||||
font: 12px/1.4 system-ui, sans-serif;
|
|
||||||
color: #1f2937;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hud .selection strong {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 320px;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-panel {
|
||||||
|
position: relative;
|
||||||
|
min-width: 0;
|
||||||
|
border-right: 1px solid #d1d5db;
|
||||||
|
background: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-title {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
left: 12px;
|
||||||
|
z-index: 1000;
|
||||||
|
background: rgba(255,255,255,0.94);
|
||||||
|
border: 1px solid rgba(0,0,0,0.08);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.35;
|
||||||
color: #111827;
|
color: #111827;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hud .alerts {
|
.map-title strong {
|
||||||
margin-top: 8px;
|
display: block;
|
||||||
display: grid;
|
font-size: 13px;
|
||||||
gap: 6px;
|
|
||||||
max-width: 320px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-item {
|
.map {
|
||||||
padding: 8px 10px;
|
height: 100%;
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid #e0b100;
|
|
||||||
background: rgba(255,248,204,0.96);
|
|
||||||
color: #5f4b00;
|
|
||||||
font: 12px/1.35 system-ui,sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-item strong { display:block; margin-bottom:2px; }
|
|
||||||
|
|
||||||
.runtime-warning {
|
|
||||||
margin-top: 10px;
|
|
||||||
padding: 8px 10px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid #c2410c;
|
|
||||||
background: rgba(255, 237, 213, 0.96);
|
|
||||||
color: #7c2d12;
|
|
||||||
font: 12px/1.35 system-ui, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.runtime-warning[hidden] {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.offmap-indicators {
|
.offmap-indicators {
|
||||||
|
|
@ -97,7 +87,7 @@
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
max-width: 260px;
|
max-width: 240px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -117,15 +107,16 @@
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
transition: transform 120ms ease, box-shadow 120ms ease, outline-color 120ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.offmap-indicator:hover {
|
.offmap-indicator:hover {
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
box-shadow: 0 8px 20px rgba(0,0,0,0.28);
|
box-shadow: 0 8px 22px rgba(0,0,0,0.28);
|
||||||
}
|
}
|
||||||
|
|
||||||
.offmap-indicator.active {
|
.offmap-indicator.active {
|
||||||
outline: 2px solid rgba(255,255,255,0.9);
|
outline: 3px solid rgba(255,255,255,0.98);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
transform: translateY(-1px) scale(1.02);
|
transform: translateY(-1px) scale(1.02);
|
||||||
}
|
}
|
||||||
|
|
@ -144,34 +135,78 @@
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
box-shadow: inset 0 0 0 2px rgba(255,255,255,0.18);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.offmap-meta strong {
|
.sidebar {
|
||||||
|
background: rgba(255,255,255,0.97);
|
||||||
|
padding: 14px;
|
||||||
|
display: grid;
|
||||||
|
align-content: start;
|
||||||
|
gap: 12px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 1px 4px rgba(0,0,0,0.04);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.45;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel strong {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 6px;
|
||||||
|
color: #111827;
|
||||||
}
|
}
|
||||||
|
|
||||||
.offmap-meta span {
|
.alerts {
|
||||||
display: block;
|
display: grid;
|
||||||
opacity: 0.82;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flash-marker {
|
.alert-item {
|
||||||
animation: flash-marker-pulse 1.2s ease-out 1;
|
padding: 8px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e0b100;
|
||||||
|
background: rgba(255,248,204,0.96);
|
||||||
|
color: #5f4b00;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.35;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes flash-marker-pulse {
|
.runtime-warning {
|
||||||
0% { transform: scale(1); filter: drop-shadow(0 1px 1px rgba(0,0,0,.35)); }
|
border: 1px solid #c2410c;
|
||||||
20% { transform: scale(1.8); filter: drop-shadow(0 0 0 rgba(0,0,0,0)); }
|
background: rgba(255, 237, 213, 0.96);
|
||||||
100% { transform: scale(1); filter: drop-shadow(0 1px 1px rgba(0,0,0,.35)); }
|
color: #7c2d12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-warning[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-panel {
|
||||||
|
border-color: #1d4ed8;
|
||||||
|
background: rgba(219, 234, 254, 0.98);
|
||||||
|
color: #1e3a8a;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-warning {
|
||||||
|
border-color: #b91c1c;
|
||||||
|
background: rgba(254, 226, 226, 0.98);
|
||||||
|
color: #7f1d1d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.car {
|
.car {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
transform-origin: 12px 12px;
|
transform-origin: 12px 12px;
|
||||||
will-change: transform;
|
|
||||||
filter: drop-shadow(0 1px 1px rgba(0,0,0,.35));
|
filter: drop-shadow(0 1px 1px rgba(0,0,0,.35));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -183,23 +218,51 @@
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="map"></div>
|
<div class="toolbar">
|
||||||
|
|
||||||
<div class="hud">
|
|
||||||
<div>
|
|
||||||
<button id="playBtn">Play</button>
|
<button id="playBtn">Play</button>
|
||||||
<button id="pauseBtn">Pause</button>
|
<button id="pauseBtn">Pause</button>
|
||||||
<button id="resetBtn">Reset</button>
|
<button id="resetBtn">Reset</button>
|
||||||
</div>
|
|
||||||
<div class="status" id="status"></div>
|
<div class="status" id="status"></div>
|
||||||
<div class="selection" id="selectedVehicleInfo">Klik på et køretøj eller en hjørneindikator for detaljer.</div>
|
|
||||||
<div class="runtime-warning" id="runtimeWarning" hidden>
|
|
||||||
Du kører appen via <code>file://</code>. Brug en lokal webserver i stedet, fx <code>python3 -m http.server 4173</code>, og åbn <code>http://127.0.0.1:4173/kort7.html</code>.
|
|
||||||
</div>
|
|
||||||
<div class="alerts" id="presenceAlerts"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="offmap-indicators" id="offmapIndicators"></div>
|
<div class="layout">
|
||||||
|
<section class="map-panel">
|
||||||
|
<div class="map-title">
|
||||||
|
<strong>Tilnærmet position</strong>
|
||||||
|
Viser bevidst usikker og pædagogisk forskudt tracking.
|
||||||
|
</div>
|
||||||
|
<div id="mapApprox" class="map"></div>
|
||||||
|
<div class="offmap-indicators" id="offmapIndicatorsApprox"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="map-panel">
|
||||||
|
<div class="map-title">
|
||||||
|
<strong>Præcis position</strong>
|
||||||
|
Viser den reelle position for samme køretøjer og tidslinje.
|
||||||
|
</div>
|
||||||
|
<div id="mapTrue" class="map"></div>
|
||||||
|
<div class="offmap-indicators" id="offmapIndicatorsTrue"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div class="panel" id="selectedVehicleInfo">Klik på et køretøj eller en hjørneindikator for detaljer.</div>
|
||||||
|
<div class="panel runtime-warning" id="runtimeWarning" hidden>
|
||||||
|
Du kører appen via <code>file://</code>. Brug en lokal webserver i stedet, fx <code>python3 -m http.server 4173</code>, og åbn <code>http://127.0.0.1:4173/kort7.html</code>.
|
||||||
|
</div>
|
||||||
|
<div class="panel runtime-warning tile-warning" id="tileWarning" hidden>
|
||||||
|
Korttiles kunne ikke hentes. Demoen viser derfor grå flader. Tjek netværksadgang eller om browseren blokerer tile-requests.
|
||||||
|
</div>
|
||||||
|
<div class="panel debug-panel" id="debugPanel"></div>
|
||||||
|
<div class="panel">
|
||||||
|
<strong>Mulige off-map køretøjer, tilnærmet kort</strong>
|
||||||
|
<div class="alerts" id="presenceAlertsApprox"></div>
|
||||||
|
</div>
|
||||||
|
<div class="panel">
|
||||||
|
<strong>Mulige off-map køretøjer, præcist kort</strong>
|
||||||
|
<div class="alerts" id="presenceAlertsTrue"></div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="data.js"></script>
|
<script src="data.js"></script>
|
||||||
<script src="app.js"></script>
|
<script src="app.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
127.0.0.1 - - [07/Apr/2026 13:34:37] "GET /kort7.html HTTP/1.1" 200 -
|
||||||
|
|
@ -7,6 +7,9 @@
|
||||||
"": {
|
"": {
|
||||||
"name": "kort7",
|
"name": "kort7",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"leaflet": "^1.9.4"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.59.1",
|
"@playwright/test": "^1.59.1",
|
||||||
"playwright": "^1.59.1"
|
"playwright": "^1.59.1"
|
||||||
|
|
@ -43,6 +46,12 @@
|
||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/leaflet": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
"node_modules/playwright": {
|
"node_modules/playwright": {
|
||||||
"version": "1.59.1",
|
"version": "1.59.1",
|
||||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz",
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -8,5 +8,8 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.59.1",
|
"@playwright/test": "^1.59.1",
|
||||||
"playwright": "^1.59.1"
|
"playwright": "^1.59.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"leaflet": "^1.9.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
37
kort7/patch
37
kort7/patch
|
|
@ -1,37 +0,0 @@
|
||||||
--- a/kort6.html
|
|
||||||
+++ b/kort6.html
|
|
||||||
@@ -41,6 +41,24 @@
|
|
||||||
.hud .status {
|
|
||||||
margin-top: 8px;
|
|
||||||
white-space: pre-line;
|
|
||||||
}
|
|
||||||
+
|
|
||||||
+ .hud .alerts {
|
|
||||||
+ margin-top: 8px;
|
|
||||||
+ display: grid;
|
|
||||||
+ gap: 6px;
|
|
||||||
+ max-width: 320px;
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
+ .alert-item {
|
|
||||||
+ padding: 8px 10px;
|
|
||||||
+ border-radius: 8px;
|
|
||||||
+ border: 1px solid #e0b100;
|
|
||||||
+ background: rgba(255,248,204,0.96);
|
|
||||||
+ color: #5f4b00;
|
|
||||||
+ font: 12px/1.35 system-ui,sans-serif;
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
+ .alert-item strong { display:block; margin-bottom:2px; }
|
|
||||||
|
|
||||||
.car {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
@@ -86,6 +104,7 @@
|
|
||||||
<button id="resetBtn">Reset</button>
|
|
||||||
</div>
|
|
||||||
<div class="status" id="status"></div>
|
|
||||||
+ <div class="alerts" id="presenceAlerts"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="data.js"></script>
|
|
||||||
20
kort7/patch2
20
kort7/patch2
|
|
@ -1,20 +0,0 @@
|
||||||
--- a/data.js
|
|
||||||
+++ b/data.js
|
|
||||||
@@ -53,5 +53,15 @@
|
|
||||||
{ vehicle: "Bil 3", ts: 128,lat: 55.6744, lon: 12.5742, speedMps: 7, headingDeg: 270, uncertaintyM: 0 },
|
|
||||||
{ vehicle: "Bil 2", ts: 130,lat: 55.6808, lon: 12.5910, speedMps: 8, headingDeg: 130, uncertaintyM: 950 },
|
|
||||||
{ vehicle: "Bil 1", ts: 131,lat: 55.6778, lon: 12.6016, speedMps: 10, headingDeg: 135, uncertaintyM: 0 },
|
|
||||||
{ vehicle: "Bil 4", ts: 133,lat: 55.6809, lon: 12.5758, speedMps: 7, headingDeg: 60, uncertaintyM: 0 },
|
|
||||||
- { vehicle: "Bil 5", ts: 134,lat: 55.6756, lon: 12.5584, speedMps: 8, headingDeg: 215, uncertaintyM: 0 }
|
|
||||||
+ { vehicle: "Bil 5", ts: 134,lat: 55.6756, lon: 12.5584, speedMps: 8, headingDeg: 215, uncertaintyM: 0 },
|
|
||||||
+
|
|
||||||
+ { vehicle: "Bil 6", ts: 8, lat: 55.7080, lon: 12.5050, speedMps: 0, headingDeg: 90, uncertaintyM: 15000 },
|
|
||||||
+ { vehicle: "Bil 6", ts: 44, lat: 55.7080, lon: 12.5050, speedMps: 0, headingDeg: 90, uncertaintyM: 15000 },
|
|
||||||
+ { vehicle: "Bil 6", ts: 90, lat: 55.7080, lon: 12.5050, speedMps: 0, headingDeg: 90, uncertaintyM: 15000 },
|
|
||||||
+ { vehicle: "Bil 6", ts: 134,lat: 55.7080, lon: 12.5050, speedMps: 0, headingDeg: 90, uncertaintyM: 15000 },
|
|
||||||
+
|
|
||||||
+ { vehicle: "Bil 7", ts: 10, lat: 55.7065, lon: 12.6175, speedMps: 0, headingDeg: 270, uncertaintyM: 2800 },
|
|
||||||
+ { vehicle: "Bil 7", ts: 46, lat: 55.7065, lon: 12.6175, speedMps: 0, headingDeg: 270, uncertaintyM: 2800 },
|
|
||||||
+ { vehicle: "Bil 7", ts: 92, lat: 55.7065, lon: 12.6175, speedMps: 0, headingDeg: 270, uncertaintyM: 2800 },
|
|
||||||
+ { vehicle: "Bil 7", ts: 134,lat: 55.7065, lon: 12.6175, speedMps: 0, headingDeg: 270, uncertaintyM: 2800 }
|
|
||||||
];
|
|
||||||
11
kort7/patch3
11
kort7/patch3
|
|
@ -1,11 +0,0 @@
|
||||||
--- a/app.js
|
|
||||||
+++ b/app.js
|
|
||||||
@@ -77,7 +77,7 @@
|
|
||||||
}).addTo(map);
|
|
||||||
|
|
||||||
let uncertaintyCircle = null;
|
|
||||||
- if (name === "Bil 2") {
|
|
||||||
+ if (first.uncertaintyM > 0) {
|
|
||||||
uncertaintyCircle = L.circle([first.lat, first.lon], {
|
|
||||||
radius: first.uncertaintyM,
|
|
||||||
color: color,
|
|
||||||
|
|
@ -1,109 +1,106 @@
|
||||||
const { test, expect } = require('@playwright/test');
|
const { test, expect } = require('@playwright/test');
|
||||||
|
|
||||||
async function waitForVehicleMarkers(page) {
|
async function waitForMaps(page) {
|
||||||
await page.waitForSelector('.leaflet-marker-icon', { state: 'attached' });
|
await expect(page.locator('#mapApprox')).toBeVisible();
|
||||||
|
await expect(page.locator('#mapTrue')).toBeVisible();
|
||||||
await expect.poll(async () => page.locator('.leaflet-marker-icon').count(), {
|
await expect.poll(async () => page.locator('.leaflet-marker-icon').count(), {
|
||||||
timeout: 10000
|
timeout: 10000
|
||||||
}).toBeGreaterThan(0);
|
}).toBeGreaterThan(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
test.describe('kort7 vehicle map', () => {
|
test.describe('kort7 dual map demo', () => {
|
||||||
test('loads map and renders vehicles', async ({ page }) => {
|
test('loads both maps and renders vehicles', async ({ page }) => {
|
||||||
await page.goto('/kort7.html');
|
await page.goto('/kort7.html');
|
||||||
await expect(page.locator('#map')).toBeVisible();
|
await waitForMaps(page);
|
||||||
await waitForVehicleMarkers(page);
|
|
||||||
await expect(page.locator('#status')).toContainText('Simuleret tid');
|
await expect(page.locator('#status')).toContainText('Simuleret tid');
|
||||||
await expect(page.locator('#runtimeWarning')).toBeHidden();
|
await expect(page.locator('#runtimeWarning')).toBeHidden();
|
||||||
|
await expect.poll(async () => page.locator('.leaflet-tile').count(), {
|
||||||
|
timeout: 10000
|
||||||
|
}).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('shows vehicle popup with details when clicking a marker', async ({ page }) => {
|
test('shows different map titles for approximate and precise views', async ({ page }) => {
|
||||||
|
await page.goto('/kort7.html');
|
||||||
|
await expect(page.locator('.map-title').first()).toContainText('Tilnærmet position');
|
||||||
|
await expect(page.locator('.map-title').nth(1)).toContainText('Præcis position');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clicking off-map indicator updates detail panel', async ({ page }) => {
|
||||||
await page.goto('/kort7.html');
|
await page.goto('/kort7.html');
|
||||||
await waitForVehicleMarkers(page);
|
|
||||||
await page.getByRole('button', { name: 'Pause' }).click();
|
await page.getByRole('button', { name: 'Pause' }).click();
|
||||||
|
|
||||||
const firstMarker = page.locator('.leaflet-marker-icon').first();
|
const indicator = page.locator('#offmapIndicatorsApprox .offmap-indicator').filter({ hasText: 'Bil 9' });
|
||||||
await firstMarker.click({ force: true });
|
await expect(indicator).toBeVisible();
|
||||||
|
await indicator.click();
|
||||||
|
|
||||||
const popup = page.locator('.leaflet-popup');
|
const selection = page.locator('#selectedVehicleInfo');
|
||||||
await expect(popup).toBeVisible();
|
await expect(selection).toContainText('Bil 9');
|
||||||
await expect(popup).toContainText(/Bil \d/);
|
await expect(selection).toContainText('Tilnærmet position');
|
||||||
await expect(popup).toContainText('Tid:');
|
await expect(selection).toContainText('hjørneindikator');
|
||||||
await expect(popup).toContainText('Lat:');
|
await expect(indicator).toHaveClass(/active/);
|
||||||
await expect(popup).toContainText('Lon:');
|
|
||||||
await expect(popup).toContainText('Hastighed:');
|
|
||||||
await expect(popup).toContainText('Retning:');
|
|
||||||
await expect(popup).toContainText('Usikkerhed:');
|
|
||||||
await expect(popup).toContainText('På kortet:');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('renders uncertainty circles for vehicles with uncertainty', async ({ page }) => {
|
test('renders uncertainty circles only on approximate map', async ({ page }) => {
|
||||||
await page.goto('/kort7.html');
|
await page.goto('/kort7.html');
|
||||||
await waitForVehicleMarkers(page);
|
await waitForMaps(page);
|
||||||
|
|
||||||
await expect.poll(async () => {
|
await expect.poll(async () => {
|
||||||
return await page.locator('path').evaluateAll((nodes) =>
|
return await page.locator('#mapApprox path').evaluateAll((nodes) =>
|
||||||
nodes.filter((node) => {
|
nodes.filter((node) => {
|
||||||
const strokeDasharray = node.getAttribute('stroke-dasharray');
|
const strokeDasharray = node.getAttribute('stroke-dasharray');
|
||||||
return strokeDasharray && strokeDasharray.length > 0;
|
return strokeDasharray && strokeDasharray.length > 0;
|
||||||
}).length
|
}).length
|
||||||
);
|
);
|
||||||
}).toBeGreaterThan(0);
|
}).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
await expect.poll(async () => {
|
||||||
|
return await page.locator('#mapTrue path').evaluateAll((nodes) =>
|
||||||
|
nodes.filter((node) => {
|
||||||
|
const strokeDasharray = node.getAttribute('stroke-dasharray');
|
||||||
|
return strokeDasharray && strokeDasharray.length > 0;
|
||||||
|
}).length
|
||||||
|
);
|
||||||
|
}).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shows off-map indicators on both views', async ({ page }) => {
|
||||||
|
await page.goto('/kort7.html');
|
||||||
|
await expect(page.locator('#offmapIndicatorsApprox .offmap-indicator')).toHaveCount(2);
|
||||||
|
await expect(page.locator('#offmapIndicatorsTrue .offmap-indicator')).toHaveCount(2);
|
||||||
|
await expect(page.locator('#offmapIndicatorsApprox')).toContainText('Bil 9');
|
||||||
|
await expect(page.locator('#offmapIndicatorsTrue')).toContainText('Bil 10');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('approximate and precise positions differ for the same vehicle', async ({ page }) => {
|
||||||
|
await page.goto('/kort7.html');
|
||||||
|
await page.getByRole('button', { name: 'Pause' }).click();
|
||||||
|
|
||||||
|
const selection = page.locator('#selectedVehicleInfo');
|
||||||
|
|
||||||
|
const approxIndicator = page.locator('#offmapIndicatorsApprox .offmap-indicator').filter({ hasText: 'Bil 9' });
|
||||||
|
await approxIndicator.click();
|
||||||
|
const approxText = await selection.textContent();
|
||||||
|
|
||||||
|
const trueIndicator = page.locator('#offmapIndicatorsTrue .offmap-indicator').filter({ hasText: 'Bil 9' });
|
||||||
|
await trueIndicator.click();
|
||||||
|
const trueText = await selection.textContent();
|
||||||
|
|
||||||
|
expect(approxText).not.toEqual(trueText);
|
||||||
|
expect(approxText).toContain('Tilnærmet position');
|
||||||
|
expect(trueText).toContain('Præcis position');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('playback updates simulation status over time', async ({ page }) => {
|
test('playback updates simulation status over time', async ({ page }) => {
|
||||||
await page.goto('/kort7.html');
|
await page.goto('/kort7.html');
|
||||||
|
|
||||||
const status = page.locator('#status');
|
const status = page.locator('#status');
|
||||||
const initialText = await status.textContent();
|
const initialText = await status.textContent();
|
||||||
const initialMatch = initialText.match(/Simuleret tid: ([\d.]+) s/);
|
const initialMatch = initialText.match(/Simuleret tid: ([\d.]+) s/);
|
||||||
expect(initialMatch).not.toBeNull();
|
expect(initialMatch).not.toBeNull();
|
||||||
const initialTime = Number(initialMatch[1]);
|
const initialTime = Number(initialMatch[1]);
|
||||||
|
|
||||||
await page.waitForTimeout(1800);
|
await page.waitForTimeout(1800);
|
||||||
|
|
||||||
const text = await status.textContent();
|
const text = await status.textContent();
|
||||||
const match = text.match(/Simuleret tid: ([\d.]+) s/);
|
const match = text.match(/Simuleret tid: ([\d.]+) s/);
|
||||||
expect(match).not.toBeNull();
|
expect(match).not.toBeNull();
|
||||||
expect(Number(match[1])).toBeGreaterThan(initialTime + 0.5);
|
expect(Number(match[1])).toBeGreaterThan(initialTime + 0.5);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('shows off-map corner indicators for partial overlap and full cover', async ({ page }) => {
|
|
||||||
await page.goto('/kort7.html');
|
|
||||||
|
|
||||||
await expect.poll(async () => page.locator('.offmap-indicator').count(), {
|
|
||||||
timeout: 10000
|
|
||||||
}).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
const bil9 = page.locator('.offmap-indicator').filter({ hasText: 'Bil 9' });
|
|
||||||
const bil10 = page.locator('.offmap-indicator').filter({ hasText: 'Bil 10' });
|
|
||||||
const bil8 = page.locator('.offmap-indicator').filter({ hasText: 'Bil 8' });
|
|
||||||
|
|
||||||
await expect(bil9).toContainText('Mulig tilstedeværelse');
|
|
||||||
await expect(bil10).toContainText('dækker hele viewporten');
|
|
||||||
await expect(bil8).toHaveCount(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('shows matching alert text for off-map overlap states', async ({ page }) => {
|
|
||||||
await page.goto('/kort7.html');
|
|
||||||
|
|
||||||
const alerts = page.locator('#presenceAlerts');
|
|
||||||
await expect(alerts).toContainText('Bil 9: mulig tilstedeværelse');
|
|
||||||
await expect(alerts).toContainText('Bil 10: usikkerhed dækker hele kortet');
|
|
||||||
await expect(alerts).not.toContainText('Bil 8');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('updates left info panel when clicking an off-map corner indicator', async ({ page }) => {
|
|
||||||
await page.goto('/kort7.html');
|
|
||||||
await page.getByRole('button', { name: 'Pause' }).click();
|
|
||||||
|
|
||||||
const indicator = page.locator('.offmap-indicator').filter({ hasText: 'Bil 9' });
|
|
||||||
await expect(indicator).toBeVisible();
|
|
||||||
await indicator.click();
|
|
||||||
|
|
||||||
const selection = page.locator('#selectedVehicleInfo');
|
|
||||||
await expect(selection).toContainText('Bil 9');
|
|
||||||
await expect(selection).toContainText('valgt fra hjørneindikator');
|
|
||||||
await expect(selection).toContainText('Usikkerhed: 9000 m');
|
|
||||||
await expect(indicator).toHaveClass(/active/);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 696 B |
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 618 B |
|
|
@ -0,0 +1,661 @@
|
||||||
|
/* required styles */
|
||||||
|
|
||||||
|
.leaflet-pane,
|
||||||
|
.leaflet-tile,
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow,
|
||||||
|
.leaflet-tile-container,
|
||||||
|
.leaflet-pane > svg,
|
||||||
|
.leaflet-pane > canvas,
|
||||||
|
.leaflet-zoom-box,
|
||||||
|
.leaflet-image-layer,
|
||||||
|
.leaflet-layer {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.leaflet-container {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.leaflet-tile,
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow {
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
}
|
||||||
|
/* Prevents IE11 from highlighting tiles in blue */
|
||||||
|
.leaflet-tile::selection {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
|
||||||
|
.leaflet-safari .leaflet-tile {
|
||||||
|
image-rendering: -webkit-optimize-contrast;
|
||||||
|
}
|
||||||
|
/* hack that prevents hw layers "stretching" when loading new tiles */
|
||||||
|
.leaflet-safari .leaflet-tile-container {
|
||||||
|
width: 1600px;
|
||||||
|
height: 1600px;
|
||||||
|
-webkit-transform-origin: 0 0;
|
||||||
|
}
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
|
||||||
|
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
|
||||||
|
.leaflet-container .leaflet-overlay-pane svg {
|
||||||
|
max-width: none !important;
|
||||||
|
max-height: none !important;
|
||||||
|
}
|
||||||
|
.leaflet-container .leaflet-marker-pane img,
|
||||||
|
.leaflet-container .leaflet-shadow-pane img,
|
||||||
|
.leaflet-container .leaflet-tile-pane img,
|
||||||
|
.leaflet-container img.leaflet-image-layer,
|
||||||
|
.leaflet-container .leaflet-tile {
|
||||||
|
max-width: none !important;
|
||||||
|
max-height: none !important;
|
||||||
|
width: auto;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-container img.leaflet-tile {
|
||||||
|
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
|
||||||
|
mix-blend-mode: plus-lighter;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-container.leaflet-touch-zoom {
|
||||||
|
-ms-touch-action: pan-x pan-y;
|
||||||
|
touch-action: pan-x pan-y;
|
||||||
|
}
|
||||||
|
.leaflet-container.leaflet-touch-drag {
|
||||||
|
-ms-touch-action: pinch-zoom;
|
||||||
|
/* Fallback for FF which doesn't support pinch-zoom */
|
||||||
|
touch-action: none;
|
||||||
|
touch-action: pinch-zoom;
|
||||||
|
}
|
||||||
|
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
|
||||||
|
-ms-touch-action: none;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
.leaflet-container {
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
.leaflet-container a {
|
||||||
|
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
|
||||||
|
}
|
||||||
|
.leaflet-tile {
|
||||||
|
filter: inherit;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
.leaflet-tile-loaded {
|
||||||
|
visibility: inherit;
|
||||||
|
}
|
||||||
|
.leaflet-zoom-box {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
z-index: 800;
|
||||||
|
}
|
||||||
|
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
|
||||||
|
.leaflet-overlay-pane svg {
|
||||||
|
-moz-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-pane { z-index: 400; }
|
||||||
|
|
||||||
|
.leaflet-tile-pane { z-index: 200; }
|
||||||
|
.leaflet-overlay-pane { z-index: 400; }
|
||||||
|
.leaflet-shadow-pane { z-index: 500; }
|
||||||
|
.leaflet-marker-pane { z-index: 600; }
|
||||||
|
.leaflet-tooltip-pane { z-index: 650; }
|
||||||
|
.leaflet-popup-pane { z-index: 700; }
|
||||||
|
|
||||||
|
.leaflet-map-pane canvas { z-index: 100; }
|
||||||
|
.leaflet-map-pane svg { z-index: 200; }
|
||||||
|
|
||||||
|
.leaflet-vml-shape {
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
.lvml {
|
||||||
|
behavior: url(#default#VML);
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* control positioning */
|
||||||
|
|
||||||
|
.leaflet-control {
|
||||||
|
position: relative;
|
||||||
|
z-index: 800;
|
||||||
|
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
.leaflet-top,
|
||||||
|
.leaflet-bottom {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1000;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.leaflet-top {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.leaflet-right {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
.leaflet-bottom {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
.leaflet-left {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.leaflet-control {
|
||||||
|
float: left;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
.leaflet-right .leaflet-control {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
.leaflet-top .leaflet-control {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.leaflet-bottom .leaflet-control {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.leaflet-left .leaflet-control {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
.leaflet-right .leaflet-control {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* zoom and fade animations */
|
||||||
|
|
||||||
|
.leaflet-fade-anim .leaflet-popup {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transition: opacity 0.2s linear;
|
||||||
|
-moz-transition: opacity 0.2s linear;
|
||||||
|
transition: opacity 0.2s linear;
|
||||||
|
}
|
||||||
|
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.leaflet-zoom-animated {
|
||||||
|
-webkit-transform-origin: 0 0;
|
||||||
|
-ms-transform-origin: 0 0;
|
||||||
|
transform-origin: 0 0;
|
||||||
|
}
|
||||||
|
svg.leaflet-zoom-animated {
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-zoom-anim .leaflet-zoom-animated {
|
||||||
|
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||||
|
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||||
|
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||||
|
}
|
||||||
|
.leaflet-zoom-anim .leaflet-tile,
|
||||||
|
.leaflet-pan-anim .leaflet-tile {
|
||||||
|
-webkit-transition: none;
|
||||||
|
-moz-transition: none;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-zoom-anim .leaflet-zoom-hide {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* cursors */
|
||||||
|
|
||||||
|
.leaflet-interactive {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.leaflet-grab {
|
||||||
|
cursor: -webkit-grab;
|
||||||
|
cursor: -moz-grab;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
.leaflet-crosshair,
|
||||||
|
.leaflet-crosshair .leaflet-interactive {
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
.leaflet-popup-pane,
|
||||||
|
.leaflet-control {
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
.leaflet-dragging .leaflet-grab,
|
||||||
|
.leaflet-dragging .leaflet-grab .leaflet-interactive,
|
||||||
|
.leaflet-dragging .leaflet-marker-draggable {
|
||||||
|
cursor: move;
|
||||||
|
cursor: -webkit-grabbing;
|
||||||
|
cursor: -moz-grabbing;
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* marker & overlays interactivity */
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow,
|
||||||
|
.leaflet-image-layer,
|
||||||
|
.leaflet-pane > svg path,
|
||||||
|
.leaflet-tile-container {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-marker-icon.leaflet-interactive,
|
||||||
|
.leaflet-image-layer.leaflet-interactive,
|
||||||
|
.leaflet-pane > svg path.leaflet-interactive,
|
||||||
|
svg.leaflet-image-layer.leaflet-interactive path {
|
||||||
|
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* visual tweaks */
|
||||||
|
|
||||||
|
.leaflet-container {
|
||||||
|
background: #ddd;
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
.leaflet-container a {
|
||||||
|
color: #0078A8;
|
||||||
|
}
|
||||||
|
.leaflet-zoom-box {
|
||||||
|
border: 2px dotted #38f;
|
||||||
|
background: rgba(255,255,255,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* general typography */
|
||||||
|
.leaflet-container {
|
||||||
|
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* general toolbar styles */
|
||||||
|
|
||||||
|
.leaflet-bar {
|
||||||
|
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.leaflet-bar a {
|
||||||
|
background-color: #fff;
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
line-height: 26px;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
.leaflet-bar a,
|
||||||
|
.leaflet-control-layers-toggle {
|
||||||
|
background-position: 50% 50%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.leaflet-bar a:hover,
|
||||||
|
.leaflet-bar a:focus {
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
}
|
||||||
|
.leaflet-bar a:first-child {
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
}
|
||||||
|
.leaflet-bar a:last-child {
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.leaflet-bar a.leaflet-disabled {
|
||||||
|
cursor: default;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
color: #bbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-touch .leaflet-bar a {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-bar a:first-child {
|
||||||
|
border-top-left-radius: 2px;
|
||||||
|
border-top-right-radius: 2px;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-bar a:last-child {
|
||||||
|
border-bottom-left-radius: 2px;
|
||||||
|
border-bottom-right-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* zoom control */
|
||||||
|
|
||||||
|
.leaflet-control-zoom-in,
|
||||||
|
.leaflet-control-zoom-out {
|
||||||
|
font: bold 18px 'Lucida Console', Monaco, monospace;
|
||||||
|
text-indent: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* layers control */
|
||||||
|
|
||||||
|
.leaflet-control-layers {
|
||||||
|
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-toggle {
|
||||||
|
background-image: url(images/layers.png);
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
.leaflet-retina .leaflet-control-layers-toggle {
|
||||||
|
background-image: url(images/layers-2x.png);
|
||||||
|
background-size: 26px 26px;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-control-layers-toggle {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers .leaflet-control-layers-list,
|
||||||
|
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-expanded .leaflet-control-layers-list {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-expanded {
|
||||||
|
padding: 6px 10px 6px 6px;
|
||||||
|
color: #333;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-scrollbar {
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-selector {
|
||||||
|
margin-top: 2px;
|
||||||
|
position: relative;
|
||||||
|
top: 1px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers label {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
font-size: 1.08333em;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-separator {
|
||||||
|
height: 0;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
margin: 5px -10px 5px -6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Default icon URLs */
|
||||||
|
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
|
||||||
|
background-image: url(images/marker-icon.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* attribution and scale controls */
|
||||||
|
|
||||||
|
.leaflet-container .leaflet-control-attribution {
|
||||||
|
background: #fff;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.leaflet-control-attribution,
|
||||||
|
.leaflet-control-scale-line {
|
||||||
|
padding: 0 5px;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.leaflet-control-attribution a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.leaflet-control-attribution a:hover,
|
||||||
|
.leaflet-control-attribution a:focus {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.leaflet-attribution-flag {
|
||||||
|
display: inline !important;
|
||||||
|
vertical-align: baseline !important;
|
||||||
|
width: 1em;
|
||||||
|
height: 0.6669em;
|
||||||
|
}
|
||||||
|
.leaflet-left .leaflet-control-scale {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-bottom .leaflet-control-scale {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-control-scale-line {
|
||||||
|
border: 2px solid #777;
|
||||||
|
border-top: none;
|
||||||
|
line-height: 1.1;
|
||||||
|
padding: 2px 5px 1px;
|
||||||
|
white-space: nowrap;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
text-shadow: 1px 1px #fff;
|
||||||
|
}
|
||||||
|
.leaflet-control-scale-line:not(:first-child) {
|
||||||
|
border-top: 2px solid #777;
|
||||||
|
border-bottom: none;
|
||||||
|
margin-top: -2px;
|
||||||
|
}
|
||||||
|
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
|
||||||
|
border-bottom: 2px solid #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-touch .leaflet-control-attribution,
|
||||||
|
.leaflet-touch .leaflet-control-layers,
|
||||||
|
.leaflet-touch .leaflet-bar {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-control-layers,
|
||||||
|
.leaflet-touch .leaflet-bar {
|
||||||
|
border: 2px solid rgba(0,0,0,0.2);
|
||||||
|
background-clip: padding-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* popup */
|
||||||
|
|
||||||
|
.leaflet-popup {
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.leaflet-popup-content-wrapper {
|
||||||
|
padding: 1px;
|
||||||
|
text-align: left;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
.leaflet-popup-content {
|
||||||
|
margin: 13px 24px 13px 20px;
|
||||||
|
line-height: 1.3;
|
||||||
|
font-size: 13px;
|
||||||
|
font-size: 1.08333em;
|
||||||
|
min-height: 1px;
|
||||||
|
}
|
||||||
|
.leaflet-popup-content p {
|
||||||
|
margin: 17px 0;
|
||||||
|
margin: 1.3em 0;
|
||||||
|
}
|
||||||
|
.leaflet-popup-tip-container {
|
||||||
|
width: 40px;
|
||||||
|
height: 20px;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
margin-top: -1px;
|
||||||
|
margin-left: -20px;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.leaflet-popup-tip {
|
||||||
|
width: 17px;
|
||||||
|
height: 17px;
|
||||||
|
padding: 1px;
|
||||||
|
|
||||||
|
margin: -10px auto 0;
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
|
-webkit-transform: rotate(45deg);
|
||||||
|
-moz-transform: rotate(45deg);
|
||||||
|
-ms-transform: rotate(45deg);
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
.leaflet-popup-content-wrapper,
|
||||||
|
.leaflet-popup-tip {
|
||||||
|
background: white;
|
||||||
|
color: #333;
|
||||||
|
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
.leaflet-container a.leaflet-popup-close-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
border: none;
|
||||||
|
text-align: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
font: 16px/24px Tahoma, Verdana, sans-serif;
|
||||||
|
color: #757575;
|
||||||
|
text-decoration: none;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.leaflet-container a.leaflet-popup-close-button:hover,
|
||||||
|
.leaflet-container a.leaflet-popup-close-button:focus {
|
||||||
|
color: #585858;
|
||||||
|
}
|
||||||
|
.leaflet-popup-scrolled {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-oldie .leaflet-popup-content-wrapper {
|
||||||
|
-ms-zoom: 1;
|
||||||
|
}
|
||||||
|
.leaflet-oldie .leaflet-popup-tip {
|
||||||
|
width: 24px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
|
||||||
|
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-oldie .leaflet-control-zoom,
|
||||||
|
.leaflet-oldie .leaflet-control-layers,
|
||||||
|
.leaflet-oldie .leaflet-popup-content-wrapper,
|
||||||
|
.leaflet-oldie .leaflet-popup-tip {
|
||||||
|
border: 1px solid #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* div icon */
|
||||||
|
|
||||||
|
.leaflet-div-icon {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Tooltip */
|
||||||
|
/* Base styles for the element that has a tooltip */
|
||||||
|
.leaflet-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
padding: 6px;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #fff;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #222;
|
||||||
|
white-space: nowrap;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
.leaflet-tooltip.leaflet-interactive {
|
||||||
|
cursor: pointer;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-top:before,
|
||||||
|
.leaflet-tooltip-bottom:before,
|
||||||
|
.leaflet-tooltip-left:before,
|
||||||
|
.leaflet-tooltip-right:before {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
border: 6px solid transparent;
|
||||||
|
background: transparent;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Directions */
|
||||||
|
|
||||||
|
.leaflet-tooltip-bottom {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-top {
|
||||||
|
margin-top: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-bottom:before,
|
||||||
|
.leaflet-tooltip-top:before {
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-top:before {
|
||||||
|
bottom: 0;
|
||||||
|
margin-bottom: -12px;
|
||||||
|
border-top-color: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-bottom:before {
|
||||||
|
top: 0;
|
||||||
|
margin-top: -12px;
|
||||||
|
margin-left: -6px;
|
||||||
|
border-bottom-color: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-left {
|
||||||
|
margin-left: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-right {
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-left:before,
|
||||||
|
.leaflet-tooltip-right:before {
|
||||||
|
top: 50%;
|
||||||
|
margin-top: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-left:before {
|
||||||
|
right: 0;
|
||||||
|
margin-right: -12px;
|
||||||
|
border-left-color: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-right:before {
|
||||||
|
left: 0;
|
||||||
|
margin-left: -12px;
|
||||||
|
border-right-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Printing */
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
/* Prevent printers from removing background-images of controls. */
|
||||||
|
.leaflet-control {
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue