mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-11 09:41:40 -05:00
Merge pull request #667 from Freika/fix/map-performance-improvement-with-canvas
Improve map performance with canvas rendering
This commit is contained in:
commit
b75b8670af
10 changed files with 209 additions and 98 deletions
|
|
@ -1 +1 @@
|
|||
0.22.2
|
||||
0.22.3
|
||||
|
|
|
|||
|
|
@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
# 0.22.3 - 2025-01-14
|
||||
|
||||
### Changed
|
||||
|
||||
- The Map now uses a canvas to draw polylines, points and fog of war. This should improve performance in browser with a lot of points and polylines.
|
||||
|
||||
# 0.22.2 - 2025-01-13
|
||||
|
||||
✨ The Fancy Routes release ✨
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,21 +0,0 @@
|
|||
/* Ensure fog overlay is positioned relative to the map container */
|
||||
#fog {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.8); /* Adjust the opacity here */
|
||||
pointer-events: none;
|
||||
mix-blend-mode: multiply;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.unfogged-circle {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
border-radius: 50%;
|
||||
background: white;
|
||||
mix-blend-mode: destination-out;
|
||||
filter: blur(3px); /* Apply no blur to the circles */
|
||||
}
|
||||
|
|
@ -17,6 +17,6 @@ class Api::V1::Countries::VisitedCitiesController < ApiController
|
|||
private
|
||||
|
||||
def required_params
|
||||
%i[start_at end_at]
|
||||
%i[start_at end_at api_key]
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -16,33 +16,23 @@ import {
|
|||
import { fetchAndDrawAreas } from "../maps/areas";
|
||||
import { handleAreaCreated } from "../maps/areas";
|
||||
|
||||
import { showFlashMessage } from "../maps/helpers";
|
||||
import { fetchAndDisplayPhotos } from '../maps/helpers';
|
||||
import { showFlashMessage, fetchAndDisplayPhotos, debounce } from "../maps/helpers";
|
||||
|
||||
import { osmMapLayer } from "../maps/layers";
|
||||
import { osmHotMapLayer } from "../maps/layers";
|
||||
import { OPNVMapLayer } from "../maps/layers";
|
||||
import { openTopoMapLayer } from "../maps/layers";
|
||||
import { cyclOsmMapLayer } from "../maps/layers";
|
||||
import { esriWorldStreetMapLayer } from "../maps/layers";
|
||||
import { esriWorldTopoMapLayer } from "../maps/layers";
|
||||
import { esriWorldImageryMapLayer } from "../maps/layers";
|
||||
import { esriWorldGrayCanvasMapLayer } from "../maps/layers";
|
||||
import {
|
||||
osmMapLayer,
|
||||
osmHotMapLayer,
|
||||
OPNVMapLayer,
|
||||
openTopoMapLayer,
|
||||
cyclOsmMapLayer,
|
||||
esriWorldStreetMapLayer,
|
||||
esriWorldTopoMapLayer,
|
||||
esriWorldImageryMapLayer,
|
||||
esriWorldGrayCanvasMapLayer
|
||||
} from "../maps/layers";
|
||||
import { countryCodesMap } from "../maps/country_codes";
|
||||
|
||||
import "leaflet-draw";
|
||||
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
import { initializeFogCanvas, drawFogCanvas, createFogOverlay } from "../maps/fog_of_war";
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["container"];
|
||||
|
|
@ -84,7 +74,10 @@ export default class extends Controller {
|
|||
|
||||
this.polylinesLayer = createPolylinesLayer(this.markers, this.map, this.timezone, this.routeOpacity, this.userSettings, this.distanceUnit);
|
||||
this.heatmapLayer = L.heatLayer(this.heatmapMarkers, { radius: 20 }).addTo(this.map);
|
||||
this.fogOverlay = L.layerGroup(); // Initialize fog layer
|
||||
|
||||
// Create a proper Leaflet layer for fog
|
||||
this.fogOverlay = createFogOverlay();
|
||||
|
||||
this.areasLayer = L.layerGroup(); // Initialize areas layer
|
||||
this.photoMarkers = L.layerGroup();
|
||||
|
||||
|
|
@ -94,11 +87,12 @@ export default class extends Controller {
|
|||
this.addSettingsButton();
|
||||
}
|
||||
|
||||
// Initialize layers for the layer control
|
||||
const controlsLayer = {
|
||||
Points: this.markersLayer,
|
||||
Routes: this.polylinesLayer,
|
||||
Heatmap: this.heatmapLayer,
|
||||
"Fog of War": this.fogOverlay,
|
||||
"Fog of War": new this.fogOverlay(),
|
||||
"Scratch map": this.scratchLayer,
|
||||
Areas: this.areasLayer,
|
||||
Photos: this.photoMarkers
|
||||
|
|
@ -131,6 +125,7 @@ export default class extends Controller {
|
|||
maxWidth: 120
|
||||
}).addTo(this.map)
|
||||
|
||||
// Initialize layer control
|
||||
this.layerControl = L.control.layers(this.baseMaps(), controlsLayer).addTo(this.map);
|
||||
|
||||
// Fetch and draw areas when the map is loaded
|
||||
|
|
@ -230,6 +225,19 @@ export default class extends Controller {
|
|||
localStorage.setItem('mapPanelOpen', 'false');
|
||||
}
|
||||
}
|
||||
|
||||
// Update event handlers
|
||||
this.map.on('moveend', () => {
|
||||
if (document.getElementById('fog')) {
|
||||
this.updateFog(this.markers, this.clearFogRadius);
|
||||
}
|
||||
});
|
||||
|
||||
this.map.on('zoomend', () => {
|
||||
if (document.getElementById('fog')) {
|
||||
this.updateFog(this.markers, this.clearFogRadius);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
|
|
@ -528,39 +536,11 @@ export default class extends Controller {
|
|||
}
|
||||
|
||||
updateFog(markers, clearFogRadius) {
|
||||
var fog = document.getElementById('fog');
|
||||
fog.innerHTML = ''; // Clear previous circles
|
||||
markers.forEach((point) => {
|
||||
const radiusInPixels = this.metersToPixels(this.map, clearFogRadius);
|
||||
this.clearFog(point[0], point[1], radiusInPixels);
|
||||
});
|
||||
}
|
||||
|
||||
metersToPixels(map, meters) {
|
||||
const zoom = map.getZoom();
|
||||
const latLng = map.getCenter(); // Get map center for correct projection
|
||||
const metersPerPixel = this.getMetersPerPixel(latLng.lat, zoom);
|
||||
return meters / metersPerPixel;
|
||||
}
|
||||
|
||||
getMetersPerPixel(latitude, zoom) {
|
||||
const earthCircumference = 40075016.686; // Earth's circumference in meters
|
||||
const metersPerPixel = earthCircumference * Math.cos(latitude * Math.PI / 180) / Math.pow(2, zoom + 8);
|
||||
return metersPerPixel;
|
||||
}
|
||||
|
||||
clearFog(lat, lng, radius) {
|
||||
var fog = document.getElementById('fog');
|
||||
var point = this.map.latLngToContainerPoint([lat, lng]);
|
||||
var size = radius * 2;
|
||||
var circle = document.createElement('div');
|
||||
circle.className = 'unfogged-circle';
|
||||
circle.style.width = size + 'px';
|
||||
circle.style.height = size + 'px';
|
||||
circle.style.left = (point.x - radius) + 'px';
|
||||
circle.style.top = (point.y - radius) + 'px';
|
||||
circle.style.backdropFilter = 'blur(0px)'; // Remove blur for the circles
|
||||
fog.appendChild(circle);
|
||||
const fog = document.getElementById('fog');
|
||||
if (!fog) {
|
||||
initializeFogCanvas(this.map);
|
||||
}
|
||||
requestAnimationFrame(() => drawFogCanvas(this.map, markers, clearFogRadius));
|
||||
}
|
||||
|
||||
initializeDrawControl() {
|
||||
|
|
@ -822,6 +802,17 @@ export default class extends Controller {
|
|||
// Debounce the heavy operations
|
||||
const updateLayers = debounce(() => {
|
||||
try {
|
||||
// Store current layer visibility states
|
||||
const layerStates = {
|
||||
Points: this.map.hasLayer(this.markersLayer),
|
||||
Routes: this.map.hasLayer(this.polylinesLayer),
|
||||
Heatmap: this.map.hasLayer(this.heatmapLayer),
|
||||
"Fog of War": this.map.hasLayer(this.fogOverlay),
|
||||
"Scratch map": this.map.hasLayer(this.scratchLayer),
|
||||
Areas: this.map.hasLayer(this.areasLayer),
|
||||
Photos: this.map.hasLayer(this.photoMarkers)
|
||||
};
|
||||
|
||||
// Check if speed_colored_routes setting has changed
|
||||
if (newSettings.speed_colored_routes !== this.userSettings.speed_colored_routes) {
|
||||
if (this.polylinesLayer) {
|
||||
|
|
@ -845,19 +836,35 @@ export default class extends Controller {
|
|||
this.routeOpacity = parseFloat(newSettings.route_opacity) || 0.6;
|
||||
this.clearFogRadius = parseInt(newSettings.fog_of_war_meters) || 50;
|
||||
|
||||
// Update layer control
|
||||
this.map.removeControl(this.layerControl);
|
||||
// Remove existing layer control
|
||||
if (this.layerControl) {
|
||||
this.map.removeControl(this.layerControl);
|
||||
}
|
||||
|
||||
// Create new controls layer object with proper initialization
|
||||
const controlsLayer = {
|
||||
Points: this.markersLayer,
|
||||
Routes: this.polylinesLayer,
|
||||
Heatmap: this.heatmapLayer,
|
||||
"Fog of War": this.fogOverlay,
|
||||
"Scratch map": this.scratchLayer,
|
||||
Areas: this.areasLayer,
|
||||
Photos: this.photoMarkers
|
||||
Points: this.markersLayer || L.layerGroup(),
|
||||
Routes: this.polylinesLayer || L.layerGroup(),
|
||||
Heatmap: this.heatmapLayer || L.heatLayer([]),
|
||||
"Fog of War": new this.fogOverlay(),
|
||||
"Scratch map": this.scratchLayer || L.layerGroup(),
|
||||
Areas: this.areasLayer || L.layerGroup(),
|
||||
Photos: this.photoMarkers || L.layerGroup()
|
||||
};
|
||||
|
||||
// Add new layer control
|
||||
this.layerControl = L.control.layers(this.baseMaps(), controlsLayer).addTo(this.map);
|
||||
|
||||
// Restore layer visibility states
|
||||
Object.entries(layerStates).forEach(([name, wasVisible]) => {
|
||||
const layer = controlsLayer[name];
|
||||
if (wasVisible && layer) {
|
||||
layer.addTo(this.map);
|
||||
} else if (layer && this.map.hasLayer(layer)) {
|
||||
this.map.removeLayer(layer);
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error updating map settings:', error);
|
||||
console.error(error.stack);
|
||||
|
|
|
|||
94
app/javascript/maps/fog_of_war.js
Normal file
94
app/javascript/maps/fog_of_war.js
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
export function initializeFogCanvas(map) {
|
||||
// Remove existing fog canvas if it exists
|
||||
const oldFog = document.getElementById('fog');
|
||||
if (oldFog) oldFog.remove();
|
||||
|
||||
// Create new fog canvas
|
||||
const fog = document.createElement('canvas');
|
||||
fog.id = 'fog';
|
||||
fog.style.position = 'absolute';
|
||||
fog.style.top = '0';
|
||||
fog.style.left = '0';
|
||||
fog.style.pointerEvents = 'none';
|
||||
fog.style.zIndex = '400';
|
||||
|
||||
// Set canvas size to match map container
|
||||
const mapSize = map.getSize();
|
||||
fog.width = mapSize.x;
|
||||
fog.height = mapSize.y;
|
||||
|
||||
// Add canvas to map container
|
||||
map.getContainer().appendChild(fog);
|
||||
|
||||
return fog;
|
||||
}
|
||||
|
||||
export function drawFogCanvas(map, markers, clearFogRadius) {
|
||||
const fog = document.getElementById('fog');
|
||||
if (!fog) return;
|
||||
|
||||
const ctx = fog.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
const size = map.getSize();
|
||||
|
||||
// Clear the canvas
|
||||
ctx.clearRect(0, 0, size.x, size.y);
|
||||
|
||||
// Keep the light fog for unexplored areas
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.4)';
|
||||
ctx.fillRect(0, 0, size.x, size.y);
|
||||
|
||||
// Set up for "cutting" holes
|
||||
ctx.globalCompositeOperation = 'destination-out';
|
||||
|
||||
// Draw clear circles for each point
|
||||
markers.forEach(point => {
|
||||
const latLng = L.latLng(point[0], point[1]);
|
||||
const pixelPoint = map.latLngToContainerPoint(latLng);
|
||||
const radiusInPixels = metersToPixels(map, clearFogRadius);
|
||||
|
||||
// Make explored areas completely transparent
|
||||
const gradient = ctx.createRadialGradient(
|
||||
pixelPoint.x, pixelPoint.y, 0,
|
||||
pixelPoint.x, pixelPoint.y, radiusInPixels
|
||||
);
|
||||
gradient.addColorStop(0, 'rgba(255, 255, 255, 1)'); // 100% transparent
|
||||
gradient.addColorStop(0.85, 'rgba(255, 255, 255, 1)'); // Still 100% transparent
|
||||
gradient.addColorStop(1, 'rgba(255, 255, 255, 0)'); // Fade to fog at edge
|
||||
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.beginPath();
|
||||
ctx.arc(pixelPoint.x, pixelPoint.y, radiusInPixels, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
});
|
||||
|
||||
// Reset composite operation
|
||||
ctx.globalCompositeOperation = 'source-over';
|
||||
}
|
||||
|
||||
function metersToPixels(map, meters) {
|
||||
const zoom = map.getZoom();
|
||||
const latLng = map.getCenter();
|
||||
const metersPerPixel = getMetersPerPixel(latLng.lat, zoom);
|
||||
return meters / metersPerPixel;
|
||||
}
|
||||
|
||||
function getMetersPerPixel(latitude, zoom) {
|
||||
const earthCircumference = 40075016.686;
|
||||
return earthCircumference * Math.cos(latitude * Math.PI / 180) / Math.pow(2, zoom + 8);
|
||||
}
|
||||
|
||||
export function createFogOverlay() {
|
||||
return L.Layer.extend({
|
||||
onAdd: (map) => {
|
||||
initializeFogCanvas(map);
|
||||
},
|
||||
onRemove: (map) => {
|
||||
const fog = document.getElementById('fog');
|
||||
if (fog) {
|
||||
fog.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -297,3 +297,15 @@ export function createPhotoMarker(photo, userSettings, photoMarkers, apiKey) {
|
|||
|
||||
photoMarkers.addLayer(marker);
|
||||
}
|
||||
|
||||
export function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,19 @@
|
|||
import { createPopupContent } from "./popups";
|
||||
|
||||
export function createMarkersArray(markersData, userSettings) {
|
||||
// Create a canvas renderer
|
||||
const renderer = L.canvas({ padding: 0.5 });
|
||||
|
||||
if (userSettings.pointsRenderingMode === "simplified") {
|
||||
return createSimplifiedMarkers(markersData);
|
||||
return createSimplifiedMarkers(markersData, renderer);
|
||||
} else {
|
||||
return markersData.map((marker) => {
|
||||
const [lat, lon] = marker;
|
||||
|
||||
const popupContent = createPopupContent(marker, userSettings.timezone, userSettings.distanceUnit);
|
||||
let markerColor = marker[5] < 0 ? "orange" : "blue";
|
||||
|
||||
return L.circleMarker([lat, lon], {
|
||||
renderer: renderer, // Use canvas renderer
|
||||
radius: 4,
|
||||
color: markerColor,
|
||||
zIndexOffset: 1000,
|
||||
|
|
@ -19,7 +23,7 @@ export function createMarkersArray(markersData, userSettings) {
|
|||
}
|
||||
}
|
||||
|
||||
export function createSimplifiedMarkers(markersData) {
|
||||
export function createSimplifiedMarkers(markersData, renderer) {
|
||||
const distanceThreshold = 50; // meters
|
||||
const timeThreshold = 20000; // milliseconds (3 seconds)
|
||||
|
||||
|
|
@ -48,9 +52,15 @@ export function createSimplifiedMarkers(markersData) {
|
|||
const [lat, lon] = marker;
|
||||
const popupContent = createPopupContent(marker);
|
||||
let markerColor = marker[5] < 0 ? "orange" : "blue";
|
||||
|
||||
return L.circleMarker(
|
||||
[lat, lon],
|
||||
{ radius: 4, color: markerColor, zIndexOffset: 1000 }
|
||||
{
|
||||
renderer: renderer, // Use canvas renderer
|
||||
radius: 4,
|
||||
color: markerColor,
|
||||
zIndexOffset: 1000
|
||||
}
|
||||
).bindPopup(popupContent);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -265,6 +265,9 @@ export function addHighlightOnHover(polylineGroup, map, polylineCoordinates, use
|
|||
}
|
||||
|
||||
export function createPolylinesLayer(markers, map, timezone, routeOpacity, userSettings, distanceUnit) {
|
||||
// Create a canvas renderer
|
||||
const renderer = L.canvas({ padding: 0.5 });
|
||||
|
||||
const splitPolylines = [];
|
||||
let currentPolyline = [];
|
||||
const distanceThresholdMeters = parseInt(userSettings.meters_between_routes) || 500;
|
||||
|
|
@ -298,7 +301,6 @@ export function createPolylinesLayer(markers, map, timezone, routeOpacity, userS
|
|||
|
||||
for (let i = 0; i < polylineCoordinates.length - 1; i++) {
|
||||
const speed = calculateSpeed(polylineCoordinates[i], polylineCoordinates[i + 1]);
|
||||
|
||||
const color = getSpeedColor(speed, userSettings.speed_colored_routes);
|
||||
|
||||
const segment = L.polyline(
|
||||
|
|
@ -307,11 +309,12 @@ export function createPolylinesLayer(markers, map, timezone, routeOpacity, userS
|
|||
[polylineCoordinates[i + 1][0], polylineCoordinates[i + 1][1]]
|
||||
],
|
||||
{
|
||||
renderer: renderer, // Use canvas renderer
|
||||
color: color,
|
||||
originalColor: color,
|
||||
opacity: routeOpacity,
|
||||
weight: 3,
|
||||
speed: speed, // Store the calculated speed
|
||||
speed: speed,
|
||||
startTime: polylineCoordinates[i][4],
|
||||
endTime: polylineCoordinates[i + 1][4]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue