Skip to content

Commit

Permalink
Merge pull request #667 from Freika/fix/map-performance-improvement-w…
Browse files Browse the repository at this point in the history
…ith-canvas

Improve map performance with canvas rendering
  • Loading branch information
Freika authored Jan 14, 2025
2 parents e7c3714 + 5cf2c7e commit b75b867
Show file tree
Hide file tree
Showing 10 changed files with 210 additions and 99 deletions.
2 changes: 1 addition & 1 deletion .app_version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.22.2
0.22.3
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 ✨
Expand Down
4 changes: 2 additions & 2 deletions app/assets/builds/tailwind.css

Large diffs are not rendered by default.

21 changes: 0 additions & 21 deletions app/assets/stylesheets/fog-of-war.css

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ def index
private

def required_params
%i[start_at end_at]
%i[start_at end_at api_key]
end
end
143 changes: 75 additions & 68 deletions app/javascript/controllers/maps_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 { 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 { showFlashMessage, fetchAndDisplayPhotos, debounce } from "../maps/helpers";

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"];
Expand Down Expand Up @@ -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();

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
Expand Down
94 changes: 94 additions & 0 deletions app/javascript/maps/fog_of_war.js
Original file line number Diff line number Diff line change
@@ -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();
}
}
});
}
12 changes: 12 additions & 0 deletions app/javascript/maps/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
}
Loading

0 comments on commit b75b867

Please sign in to comment.