dawarich/app/javascript/maps_maplibre/layers/fog_layer.js
Evgenii Burmakin 8934c29fce
0.36.2 (#2007)
* fix: move foreman to global gems to fix startup crash (#1971)

* Update exporting code to stream points data to file in batches to red… (#1980)

* Update exporting code to stream points data to file in batches to reduce memory usage

* Update changelog

* Update changelog

* Feature/maplibre frontend (#1953)

* Add a plan to use MapLibre GL JS for the frontend map rendering, replacing Leaflet

* Implement phase 1

* Phases 1-3 + part of 4

* Fix e2e tests

* Phase 6

* Implement fog of war

* Phase 7

* Next step: fix specs, phase 7 done

* Use our own map tiles

* Extract v2 map logic to separate manager classes

* Update settings panel on v2 map

* Update v2 e2e tests structure

* Reimplement location search in maps v2

* Update speed routes

* Implement visits and places creation in v2

* Fix last failing test

* Implement visits merging

* Fix a routes e2e test and simplify the routes layer styling.

* Extract js to modules from maps_v2_controller.js

* Implement area creation

* Fix spec problem

* Fix some e2e tests

* Implement live mode in v2 map

* Update icons and panel

* Extract some styles

* Remove unused file

* Start adding dark theme to popups on MapLibre maps

* Make popups respect dark theme

* Move v2 maps to maplibre namespace

* Update v2 references to maplibre

* Put place, area and visit info into side panel

* Update API to use safe settings config method

* Fix specs

* Fix method name to config in SafeSettings and update usages accordingly

* Add missing public files

* Add handling for real time points

* Fix remembering enabled/disabled layers of the v2 map

* Fix lots of e2e tests

* Add settings to select map version

* Use maps/v2 as main path for MapLibre maps

* Update routing

* Update live mode

* Update maplibre controller

* Update changelog

* Remove some console.log statements

---------

Co-authored-by: Robin Tuszik <mail@robin.gg>
2025-12-06 20:54:49 +01:00

140 lines
3.4 KiB
JavaScript

/**
* Fog of war layer
* Shows explored vs unexplored areas using canvas overlay
* Does not extend BaseLayer as it uses canvas instead of MapLibre layers
*/
export class FogLayer {
constructor(map, options = {}) {
this.map = map
this.id = 'fog'
this.visible = options.visible !== undefined ? options.visible : false
this.canvas = null
this.ctx = null
this.clearRadius = options.clearRadius || 1000 // meters
this.points = []
}
add(data) {
this.points = data.features || []
this.createCanvas()
if (this.visible) {
this.show()
}
this.render()
}
update(data) {
this.points = data.features || []
this.render()
}
createCanvas() {
if (this.canvas) return
// Create canvas overlay
this.canvas = document.createElement('canvas')
this.canvas.className = 'fog-canvas'
this.canvas.style.position = 'absolute'
this.canvas.style.top = '0'
this.canvas.style.left = '0'
this.canvas.style.pointerEvents = 'none'
this.canvas.style.zIndex = '10'
this.canvas.style.display = this.visible ? 'block' : 'none'
this.ctx = this.canvas.getContext('2d')
// Add to map container
const mapContainer = this.map.getContainer()
mapContainer.appendChild(this.canvas)
// Update on map move/zoom/resize
this.map.on('move', () => this.render())
this.map.on('zoom', () => this.render())
this.map.on('resize', () => this.resizeCanvas())
this.resizeCanvas()
}
resizeCanvas() {
if (!this.canvas) return
const container = this.map.getContainer()
this.canvas.width = container.offsetWidth
this.canvas.height = container.offsetHeight
this.render()
}
render() {
if (!this.canvas || !this.ctx || !this.visible) return
const { width, height } = this.canvas
// Clear canvas
this.ctx.clearRect(0, 0, width, height)
// Draw fog overlay
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.6)'
this.ctx.fillRect(0, 0, width, height)
// Clear circles around visited points
this.ctx.globalCompositeOperation = 'destination-out'
this.points.forEach(feature => {
const coords = feature.geometry.coordinates
const point = this.map.project(coords)
// Calculate pixel radius based on zoom level
const metersPerPixel = this.getMetersPerPixel(coords[1])
const radiusPixels = this.clearRadius / metersPerPixel
this.ctx.beginPath()
this.ctx.arc(point.x, point.y, radiusPixels, 0, Math.PI * 2)
this.ctx.fill()
})
this.ctx.globalCompositeOperation = 'source-over'
}
getMetersPerPixel(latitude) {
const earthCircumference = 40075017 // meters at equator
const latitudeRadians = latitude * Math.PI / 180
const zoom = this.map.getZoom()
return earthCircumference * Math.cos(latitudeRadians) / (256 * Math.pow(2, zoom))
}
show() {
this.visible = true
if (this.canvas) {
this.canvas.style.display = 'block'
this.render()
}
}
hide() {
this.visible = false
if (this.canvas) {
this.canvas.style.display = 'none'
}
}
toggle(visible = !this.visible) {
if (visible) {
this.show()
} else {
this.hide()
}
}
remove() {
if (this.canvas) {
this.canvas.remove()
this.canvas = null
this.ctx = null
}
// Remove event listeners
this.map.off('move', this.render)
this.map.off('zoom', this.render)
this.map.off('resize', this.resizeCanvas)
}
}