From e25d6f05e221daf6aede337bfed441644e53f368 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Tue, 14 Jan 2025 23:29:48 +0100 Subject: [PATCH] Extract fog of war to a separate file --- app/javascript/controllers/maps_controller.js | 186 +++++------------- app/javascript/maps/fog_of_war.js | 94 +++++++++ app/javascript/maps/helpers.js | 12 ++ 3 files changed, 158 insertions(+), 134 deletions(-) create mode 100644 app/javascript/maps/fog_of_war.js diff --git a/app/javascript/controllers/maps_controller.js b/app/javascript/controllers/maps_controller.js index f7b38746..01fa6ad7 100644 --- a/app/javascript/controllers/maps_controller.js +++ b/app/javascript/controllers/maps_controller.js @@ -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"]; @@ -86,18 +76,7 @@ export default class extends Controller { this.heatmapLayer = L.heatLayer(this.heatmapMarkers, { radius: 20 }).addTo(this.map); // Create a proper Leaflet layer for fog - this.fogOverlay = L.Layer.extend({ - onAdd: (map) => { - this.initializeFogCanvas(); - this.updateFog(this.markers, this.clearFogRadius); - }, - onRemove: (map) => { - const fog = document.getElementById('fog'); - if (fog) { - fog.remove(); - } - } - }); + this.fogOverlay = createFogOverlay(); this.areasLayer = L.layerGroup(); // Initialize areas layer this.photoMarkers = L.layerGroup(); @@ -559,97 +538,9 @@ export default class extends Controller { updateFog(markers, clearFogRadius) { const fog = document.getElementById('fog'); if (!fog) { - this.initializeFogCanvas(); + initializeFogCanvas(this.map); } - requestAnimationFrame(() => this.drawFogCanvas(markers, clearFogRadius)); - } - - initializeFogCanvas() { - // 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 = this.map.getSize(); - fog.width = mapSize.x; - fog.height = mapSize.y; - - // Add canvas to map container - this.map.getContainer().appendChild(fog); - - // Add resize handler - this.map.on('resize', () => { - const newSize = this.map.getSize(); - fog.width = newSize.x; - fog.height = newSize.y; - this.drawFogCanvas(this.markers, this.clearFogRadius); - }); - } - - drawFogCanvas(markers, clearFogRadius) { - const fog = document.getElementById('fog'); - if (!fog) return; - - const ctx = fog.getContext('2d'); - if (!ctx) return; - - const size = this.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 = this.map.latLngToContainerPoint(latLng); - const radiusInPixels = this.metersToPixels(this.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'; - } - - metersToPixels(map, meters) { - const zoom = map.getZoom(); - const latLng = map.getCenter(); - const metersPerPixel = this.getMetersPerPixel(latLng.lat, zoom); - return meters / metersPerPixel; - } - - getMetersPerPixel(latitude, zoom) { - const earthCircumference = 40075016.686; - const metersPerPixel = earthCircumference * Math.cos(latitude * Math.PI / 180) / Math.pow(2, zoom + 8); - return metersPerPixel; + requestAnimationFrame(() => drawFogCanvas(this.map, markers, clearFogRadius)); } initializeDrawControl() { @@ -911,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) { @@ -934,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); diff --git a/app/javascript/maps/fog_of_war.js b/app/javascript/maps/fog_of_war.js new file mode 100644 index 00000000..482a161e --- /dev/null +++ b/app/javascript/maps/fog_of_war.js @@ -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(); + } + } + }); +} diff --git a/app/javascript/maps/helpers.js b/app/javascript/maps/helpers.js index 4dca082d..7c850f03 100644 --- a/app/javascript/maps/helpers.js +++ b/app/javascript/maps/helpers.js @@ -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); + }; +}