차량 위치 이동 애니메이션 개선

This commit is contained in:
2026-06-06 19:17:21 +09:00
parent 1dc93c5c78
commit 412acbc846
5 changed files with 95 additions and 11 deletions

View File

@@ -3,4 +3,4 @@ ADMIN_TOKEN=
ADMIN_AUTH_REQUIRED=false
KAKAO_JAVASCRIPT_KEY=07f4728599ceaf4299f77c772a135423
SIMULATOR_ENABLED=true
SIMULATOR_INTERVAL_MS=4000
SIMULATOR_INTERVAL_MS=1000

View File

@@ -3,4 +3,4 @@ ADMIN_TOKEN=change_this_admin_token
ADMIN_AUTH_REQUIRED=false
KAKAO_JAVASCRIPT_KEY=07f4728599ceaf4299f77c772a135423
SIMULATOR_ENABLED=true
SIMULATOR_INTERVAL_MS=4000
SIMULATOR_INTERVAL_MS=1000

View File

@@ -56,7 +56,8 @@ behind Synology Reverse Proxy.
During early testing, keep `SIMULATOR_ENABLED=true` to show moving demo
vehicles without Android phones. The simulator publishes fresh locations every
`SIMULATOR_INTERVAL_MS` milliseconds.
`SIMULATOR_INTERVAL_MS` milliseconds. The web map interpolates between fresh
positions so vehicles move smoothly instead of jumping between points.
## Maintenance Scripts

View File

@@ -12,7 +12,7 @@ const ADMIN_TOKEN = process.env.ADMIN_TOKEN || "";
const ADMIN_AUTH_REQUIRED = parseBoolean(process.env.ADMIN_AUTH_REQUIRED, false);
const DEFAULT_KAKAO_JAVASCRIPT_KEY = "07f4728599ceaf4299f77c772a135423";
const SIMULATOR_ENABLED = parseBoolean(process.env.SIMULATOR_ENABLED, true);
const SIMULATOR_INTERVAL_MS = clampNumber(Number(process.env.SIMULATOR_INTERVAL_MS || 4000), 1000, 60000);
const SIMULATOR_INTERVAL_MS = clampNumber(Number(process.env.SIMULATOR_INTERVAL_MS || 1000), 1000, 60000);
const SIMULATOR_HISTORY_EVERY = clampNumber(Number(process.env.SIMULATOR_HISTORY_EVERY || 3), 1, 30);
const DRIVERS_FILE = path.join(DATA_DIR, "drivers.json");

View File

@@ -5,6 +5,7 @@ const state = {
infoOverlay: null,
routeOverlay: null,
routeDestinationOverlay: null,
markerAnimationMs: 900,
drivers: new Map(),
locations: new Map(),
selectedDriverId: "",
@@ -652,7 +653,8 @@ function updateMarker(location) {
}
function updateKakaoMarker(location) {
const position = new kakao.maps.LatLng(location.latitude, location.longitude);
const targetPosition = createPosition(location.latitude, location.longitude);
const kakaoPosition = new kakao.maps.LatLng(targetPosition.latitude, targetPosition.longitude);
const status = getLocationStatus(location);
const markerElement = createTruckMarkerElement(location, status);
@@ -662,23 +664,25 @@ function updateKakaoMarker(location) {
marker = {
provider: "kakao",
overlay: new kakao.maps.CustomOverlay({
position,
position: kakaoPosition,
content: markerElement,
yAnchor: 1
})
}),
currentPosition: targetPosition
};
marker.overlay.setMap(state.map);
state.markers.set(location.driverId, marker);
} else {
marker.overlay.setPosition(position);
marker.overlay.setContent(markerElement);
animateMarkerPosition(marker, targetPosition);
}
}
function updateLeafletMarker(location) {
const status = getLocationStatus(location);
const vehicleType = getVehicleType(location);
const position = [location.latitude, location.longitude];
const targetPosition = createPosition(location.latitude, location.longitude);
const leafletPosition = [targetPosition.latitude, targetPosition.longitude];
const icon = L.divIcon({
className: `leaflet-truck-marker truck-marker ${status.key} ${vehicleType.className}`,
html: createTruckMarkerHtml(location, vehicleType),
@@ -692,16 +696,17 @@ function updateLeafletMarker(location) {
if (!marker) {
marker = {
provider: "leaflet",
marker: L.marker(position, { icon, title: location.vehicleNo || location.driverId })
marker: L.marker(leafletPosition, { icon, title: location.vehicleNo || location.driverId }),
currentPosition: targetPosition
};
marker.marker.on("click", () => focusLocation(location));
marker.marker.addTo(state.map);
state.markers.set(location.driverId, marker);
} else {
marker.marker.setLatLng(position);
marker.marker.setIcon(icon);
marker.marker.off("click");
marker.marker.on("click", () => focusLocation(location));
animateMarkerPosition(marker, targetPosition);
}
}
@@ -728,7 +733,85 @@ function createTruckMarkerHtml(location, vehicleType = getVehicleType(location))
`;
}
function animateMarkerPosition(marker, targetPosition) {
const fromPosition = marker.currentPosition || getMarkerPosition(marker) || targetPosition;
if (calculateDistanceKm(fromPosition, targetPosition) < 0.01) {
setMarkerPosition(marker, targetPosition);
marker.currentPosition = targetPosition;
return;
}
cancelMarkerAnimation(marker);
const startedAt = window.performance.now();
const duration = state.markerAnimationMs;
const tick = (timestamp) => {
const progress = Math.min(1, (timestamp - startedAt) / duration);
const easedProgress = easeInOutCubic(progress);
const nextPosition = {
latitude: interpolateNumber(fromPosition.latitude, targetPosition.latitude, easedProgress),
longitude: interpolateNumber(fromPosition.longitude, targetPosition.longitude, easedProgress)
};
setMarkerPosition(marker, nextPosition);
marker.currentPosition = nextPosition;
if (progress < 1) {
marker.animationFrame = window.requestAnimationFrame(tick);
} else {
marker.animationFrame = null;
marker.currentPosition = targetPosition;
setMarkerPosition(marker, targetPosition);
}
};
marker.animationFrame = window.requestAnimationFrame(tick);
}
function cancelMarkerAnimation(marker) {
if (!marker?.animationFrame) return;
window.cancelAnimationFrame(marker.animationFrame);
marker.animationFrame = null;
}
function getMarkerPosition(marker) {
if (marker.provider === "leaflet" && marker.marker?.getLatLng) {
const position = marker.marker.getLatLng();
return createPosition(position.lat, position.lng);
}
return marker.currentPosition || null;
}
function setMarkerPosition(marker, position) {
if (marker.provider === "kakao") {
marker.overlay.setPosition(new kakao.maps.LatLng(position.latitude, position.longitude));
return;
}
if (marker.provider === "leaflet") {
marker.marker.setLatLng([position.latitude, position.longitude]);
}
}
function createPosition(latitude, longitude) {
return {
latitude: Number(latitude),
longitude: Number(longitude)
};
}
function easeInOutCubic(value) {
return value < 0.5
? 4 * value * value * value
: 1 - ((-2 * value + 2) ** 3) / 2;
}
function removeMarker(marker) {
cancelMarkerAnimation(marker);
if (marker.provider === "kakao") {
marker.overlay.setMap(null);
return;