Extract fog of war to a separate file

This commit is contained in:
Eugene Burmakin 2025-01-14 23:29:48 +01:00
parent e457631715
commit e25d6f05e2
3 changed files with 158 additions and 134 deletions

View file

@ -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);

View 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();
}
}
});
}

View file

@ -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);
};
}