차량 위치 이동 애니메이션 개선
This commit is contained in:
@@ -3,4 +3,4 @@ ADMIN_TOKEN=
|
|||||||
ADMIN_AUTH_REQUIRED=false
|
ADMIN_AUTH_REQUIRED=false
|
||||||
KAKAO_JAVASCRIPT_KEY=07f4728599ceaf4299f77c772a135423
|
KAKAO_JAVASCRIPT_KEY=07f4728599ceaf4299f77c772a135423
|
||||||
SIMULATOR_ENABLED=true
|
SIMULATOR_ENABLED=true
|
||||||
SIMULATOR_INTERVAL_MS=4000
|
SIMULATOR_INTERVAL_MS=1000
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ ADMIN_TOKEN=change_this_admin_token
|
|||||||
ADMIN_AUTH_REQUIRED=false
|
ADMIN_AUTH_REQUIRED=false
|
||||||
KAKAO_JAVASCRIPT_KEY=07f4728599ceaf4299f77c772a135423
|
KAKAO_JAVASCRIPT_KEY=07f4728599ceaf4299f77c772a135423
|
||||||
SIMULATOR_ENABLED=true
|
SIMULATOR_ENABLED=true
|
||||||
SIMULATOR_INTERVAL_MS=4000
|
SIMULATOR_INTERVAL_MS=1000
|
||||||
|
|||||||
@@ -56,7 +56,8 @@ behind Synology Reverse Proxy.
|
|||||||
|
|
||||||
During early testing, keep `SIMULATOR_ENABLED=true` to show moving demo
|
During early testing, keep `SIMULATOR_ENABLED=true` to show moving demo
|
||||||
vehicles without Android phones. The simulator publishes fresh locations every
|
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
|
## Maintenance Scripts
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const ADMIN_TOKEN = process.env.ADMIN_TOKEN || "";
|
|||||||
const ADMIN_AUTH_REQUIRED = parseBoolean(process.env.ADMIN_AUTH_REQUIRED, false);
|
const ADMIN_AUTH_REQUIRED = parseBoolean(process.env.ADMIN_AUTH_REQUIRED, false);
|
||||||
const DEFAULT_KAKAO_JAVASCRIPT_KEY = "07f4728599ceaf4299f77c772a135423";
|
const DEFAULT_KAKAO_JAVASCRIPT_KEY = "07f4728599ceaf4299f77c772a135423";
|
||||||
const SIMULATOR_ENABLED = parseBoolean(process.env.SIMULATOR_ENABLED, true);
|
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 SIMULATOR_HISTORY_EVERY = clampNumber(Number(process.env.SIMULATOR_HISTORY_EVERY || 3), 1, 30);
|
||||||
|
|
||||||
const DRIVERS_FILE = path.join(DATA_DIR, "drivers.json");
|
const DRIVERS_FILE = path.join(DATA_DIR, "drivers.json");
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const state = {
|
|||||||
infoOverlay: null,
|
infoOverlay: null,
|
||||||
routeOverlay: null,
|
routeOverlay: null,
|
||||||
routeDestinationOverlay: null,
|
routeDestinationOverlay: null,
|
||||||
|
markerAnimationMs: 900,
|
||||||
drivers: new Map(),
|
drivers: new Map(),
|
||||||
locations: new Map(),
|
locations: new Map(),
|
||||||
selectedDriverId: "",
|
selectedDriverId: "",
|
||||||
@@ -652,7 +653,8 @@ function updateMarker(location) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateKakaoMarker(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 status = getLocationStatus(location);
|
||||||
const markerElement = createTruckMarkerElement(location, status);
|
const markerElement = createTruckMarkerElement(location, status);
|
||||||
|
|
||||||
@@ -662,23 +664,25 @@ function updateKakaoMarker(location) {
|
|||||||
marker = {
|
marker = {
|
||||||
provider: "kakao",
|
provider: "kakao",
|
||||||
overlay: new kakao.maps.CustomOverlay({
|
overlay: new kakao.maps.CustomOverlay({
|
||||||
position,
|
position: kakaoPosition,
|
||||||
content: markerElement,
|
content: markerElement,
|
||||||
yAnchor: 1
|
yAnchor: 1
|
||||||
})
|
}),
|
||||||
|
currentPosition: targetPosition
|
||||||
};
|
};
|
||||||
marker.overlay.setMap(state.map);
|
marker.overlay.setMap(state.map);
|
||||||
state.markers.set(location.driverId, marker);
|
state.markers.set(location.driverId, marker);
|
||||||
} else {
|
} else {
|
||||||
marker.overlay.setPosition(position);
|
|
||||||
marker.overlay.setContent(markerElement);
|
marker.overlay.setContent(markerElement);
|
||||||
|
animateMarkerPosition(marker, targetPosition);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateLeafletMarker(location) {
|
function updateLeafletMarker(location) {
|
||||||
const status = getLocationStatus(location);
|
const status = getLocationStatus(location);
|
||||||
const vehicleType = getVehicleType(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({
|
const icon = L.divIcon({
|
||||||
className: `leaflet-truck-marker truck-marker ${status.key} ${vehicleType.className}`,
|
className: `leaflet-truck-marker truck-marker ${status.key} ${vehicleType.className}`,
|
||||||
html: createTruckMarkerHtml(location, vehicleType),
|
html: createTruckMarkerHtml(location, vehicleType),
|
||||||
@@ -692,16 +696,17 @@ function updateLeafletMarker(location) {
|
|||||||
if (!marker) {
|
if (!marker) {
|
||||||
marker = {
|
marker = {
|
||||||
provider: "leaflet",
|
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.on("click", () => focusLocation(location));
|
||||||
marker.marker.addTo(state.map);
|
marker.marker.addTo(state.map);
|
||||||
state.markers.set(location.driverId, marker);
|
state.markers.set(location.driverId, marker);
|
||||||
} else {
|
} else {
|
||||||
marker.marker.setLatLng(position);
|
|
||||||
marker.marker.setIcon(icon);
|
marker.marker.setIcon(icon);
|
||||||
marker.marker.off("click");
|
marker.marker.off("click");
|
||||||
marker.marker.on("click", () => focusLocation(location));
|
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) {
|
function removeMarker(marker) {
|
||||||
|
cancelMarkerAnimation(marker);
|
||||||
|
|
||||||
if (marker.provider === "kakao") {
|
if (marker.provider === "kakao") {
|
||||||
marker.overlay.setMap(null);
|
marker.overlay.setMap(null);
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user