Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,12 @@ export function getReportUrl(id, options) {
return `${__APIHOST__}/api/reports/${id}/file?jwt=${jwt}&${queryParam}`
}

export function getTileUrl(handle) {
const jwt = localStorage.getItem('access_token')
const base = `${__APIHOST__}/api/media/${handle}/tile/{z}/{x}/{y}`
return jwt === null ? base : `${base}?jwt=${jwt}`
}
Comment thread
DavidMStraub marked this conversation as resolved.
Comment thread
DavidMStraub marked this conversation as resolved.

export function getMediaUrl(handle, download = false) {
const jwt = localStorage.getItem('access_token')
if (jwt === null) {
Expand Down
1 change: 1 addition & 0 deletions src/components/GrampsjsFormEditMapLayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ class GrampsjsFormEditMapLayer extends GrampsjsObjectForm {
url="${getMediaUrl(this.data.handle)}"
opacity="${this.opacity}"
title="${this.data.desc}"
handle="${this.data.handle}"
.bounds="${this._getBounds()}"
></grampsjs-map-overlay>`
: ''
Expand Down
52 changes: 18 additions & 34 deletions src/components/GrampsjsMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class GrampsjsMap extends GrampsjsAppStateMixin(LitElement) {
style="width:${this.width}; height:${this.height};"
>
<div id="${this.mapid}" style="z-index: 0; width: 100%; height: 100%;">
<slot> </slot>
<slot @slotchange="${this._onSlotChange}"> </slot>
</div>
${this.layerSwitcher ? this._renderLayerSwitcher() : html`<div></div>`}
</div>
Expand Down Expand Up @@ -174,7 +174,6 @@ class GrampsjsMap extends GrampsjsAppStateMixin(LitElement) {
[this.longMax, this.latMax],
])
}
this._reAddOverlays()
this._slottedChildren
.filter(el => typeof el.addToMap === 'function')
.forEach(el => el.addToMap(this._map))
Expand All @@ -198,6 +197,14 @@ class GrampsjsMap extends GrampsjsAppStateMixin(LitElement) {
return slot.assignedElements({flatten: true})
}

// Handles children added after the map's load event (e.g. async data layers).
_onSlotChange() {
if (!this._map?.isStyleLoaded()) return
this._slottedChildren
.filter(el => typeof el.addToMap === 'function')
.forEach(el => el.addToMap(this._map))
}

panTo(latitude, longitude) {
if (this._map !== undefined) {
this._map.panTo([longitude, latitude])
Expand Down Expand Up @@ -248,25 +255,16 @@ class GrampsjsMap extends GrampsjsAppStateMixin(LitElement) {

_handleOverlayToggle(e) {
const {overlay, visible} = e.detail

const overlays = this._slottedChildren.filter(
el => el.tagName === 'GRAMPSJS-MAP-OVERLAY'
)

overlays.forEach(overlayElement => {
// Prefer matching by stable handle when available; fall back to title/desc for backward compatibility
const matchesByHandle =
overlay.handle &&
overlayElement.handle &&
overlayElement.handle === overlay.handle
const matchesByTitle =
!overlay.handle && overlayElement.title === overlay.desc

if (matchesByHandle || matchesByTitle) {
this._slottedChildren
.filter(
el =>
(overlay.handle && el.handle === overlay.handle) ||
(!overlay.handle && el.title === overlay.desc)
)
.forEach(el => {
// eslint-disable-next-line no-param-reassign
overlayElement.hidden = !visible
}
})
el.hidden = !visible
})
}

_handleStyleChange(style) {
Expand All @@ -286,20 +284,6 @@ class GrampsjsMap extends GrampsjsAppStateMixin(LitElement) {
}
: undefined
)
this._map.once('style.load', () => {
this._reAddOverlays()
})
}

_reAddOverlays() {
const overlays = this._slottedChildren.filter(
el => el.tagName === 'GRAMPSJS-MAP-OVERLAY'
)
overlays.forEach(overlayElement => {
// After style change, MapLibre cleared all layers. Reset overlay state and re-add.
overlayElement.resetForStyleChange()
overlayElement.addOverlay()
})
}
Comment thread
DavidMStraub marked this conversation as resolved.

_getStyleUrl(style) {
Expand Down
184 changes: 95 additions & 89 deletions src/components/GrampsjsMapOverlay.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ class GrampsjsMapOverlay extends LitElement {
title: {type: String},
handle: {type: String},
hidden: {type: Boolean},
_overlay: {type: String, attribute: false},
}
}

Expand All @@ -25,137 +24,144 @@ class GrampsjsMapOverlay extends LitElement {
this.handle = ''
this.hidden = false
this.bounds = []
this._overlay = ''
this._onStyleLoad = () => {
if (this.hidden) {
this.removeOverlay()
} else {
this.addOverlay()
}
}
}

_layerIdFor(handle, title) {
if (handle) return `overlay-${handle}`
if (title) return `overlay-${title.replace(/\s+/g, '-')}`
return ''
}
Comment thread
DavidMStraub marked this conversation as resolved.

get _layerId() {
return this._layerIdFor(this.handle, this.title)
}

// MapLibre expects coordinates in order: top-left, top-right, bottom-right, bottom-left
_getCoordinates() {
if (!this.bounds || this.bounds.length !== 2) return null
let [[y0, x0], [y1, x1]] = this.bounds
if (y0 < y1) [y0, y1] = [y1, y0]
if (x0 > x1) [x0, x1] = [x1, x0]
return [
[x0, y0], // top left [lng, lat]
[x1, y0], // top right
[x1, y1], // bottom right
[x0, y1], // bottom left
]
}

firstUpdated() {
this._map = this.parentElement._map
if (!this.hidden) {
this.addOverlay()
}
this._map.off('style.load', this._onStyleLoad)
this._map.on('style.load', this._onStyleLoad)
if (!this.hidden) this.addOverlay()
}

addOverlay() {
if (!this._map || !this.url || !this.bounds || this.bounds.length !== 2)
return

// Don't add if overlay is hidden
if (this.hidden) {
return
}

// Do nothing if overlay already exists
if (this._overlay && this._map.getLayer(this._overlay)) {
return
}
if (!this._map || !this.url || !this._layerId) return
if (this.hidden) return
if (this._map.getLayer(this._layerId)) return
Comment thread
DavidMStraub marked this conversation as resolved.

// Wait for style to be loaded before adding source/layer
const addOverlayWhenReady = () => {
// Don't add if hidden (could have changed while waiting)
if (this.hidden) {
return
}

// Generate stable ID if not already set
if (!this._overlay) {
if (this.handle) {
// Prefer handle-based ID for stability across re-renders
this._overlay = `overlay-${this.handle}`
} else if (this.title) {
// Fall back to title-based ID (less stable if title changes)
this._overlay = `overlay-${this.title.replace(/\s+/g, '-')}`
} else {
// Last resort: random ID (not stable across re-renders)
this._overlay = `overlay-${Math.random().toString(36).substr(2, 9)}`
}
}

// Check if already added (shouldn't happen but be safe)
if (this._map.getSource(this._overlay)) {
return
}

// MapLibre expects coordinates in order: top-left, top-right, bottom-right, bottom-left
// Fix: ensure bounds[0] is top-left (northwest), bounds[1] is bottom-right (southeast)
// If bounds are [south, west], [north, east], swap as needed
let [[y0, x0], [y1, x1]] = this.bounds
// Ensure y0 > y1 (top > bottom)
if (y0 < y1) {
;[y0, y1] = [y1, y0]
}
// Ensure x0 < x1 (left < right)
if (x0 > x1) {
;[x0, x1] = [x1, x0]
}
this._map.addSource(this._overlay, {
if (this.hidden) return
if (this._map.getSource(this._layerId)) return
const coordinates = this._getCoordinates()
if (!coordinates) return
this._map.addSource(this._layerId, {
type: 'image',
url: this.url,
coordinates: [
[x0, y0], // top left [lng, lat]
[x1, y0], // top right
[x1, y1], // bottom right
[x0, y1], // bottom left
],
coordinates,
})
this._map.addLayer({
id: this._overlay,
id: this._layerId,
type: 'raster',
source: this._overlay,
paint: {
'raster-opacity': this.opacity,
},
source: this._layerId,
paint: {'raster-opacity': this.opacity},
})
// Bring to front
this._map.moveLayer(this._overlay)
this._map.moveLayer(this._layerId)
}

// Check if style is already loaded
if (this._map.isStyleLoaded()) {
addOverlayWhenReady()
} else {
// Wait for style to load
this._map.once('styledata', addOverlayWhenReady)
}
}

removeOverlay() {
if (this._map && this._overlay) {
if (this._map.getLayer(this._overlay)) {
this._map.removeLayer(this._overlay)
}
if (this._map.getSource(this._overlay)) {
this._map.removeSource(this._overlay)
}
}
if (!this._map || !this._layerId) return
if (this._map.getLayer(this._layerId)) this._map.removeLayer(this._layerId)
if (this._map.getSource(this._layerId))
this._map.removeSource(this._layerId)
}

disconnectedCallback() {
if (this._map) this._map.off('style.load', this._onStyleLoad)
this.removeOverlay()
super.disconnectedCallback()
}

resetForStyleChange() {
// After a style change, MapLibre has already cleared all layers/sources.
// We just need to reset our internal state so addOverlay() can recreate them.
this._overlay = ''
// Called by GrampsjsMap inside setStyle's transformStyle callback so the
// image source/layer survive style switches without a two-pass re-add.
getTransformStyleContribution(_prev, next) {
if (!this.url || !this._layerId || this.hidden) return next
const coordinates = this._getCoordinates()
if (!coordinates) return next
const layerId = this._layerId
return {
...next,
sources: {
...next.sources,
[layerId]: {type: 'image', url: this.url, coordinates},
},
layers: [
...next.layers,
{
id: layerId,
type: 'raster',
source: layerId,
layout: {visibility: this.hidden ? 'none' : 'visible'},
paint: {'raster-opacity': this.opacity},
},
],
}
}

updated(changed) {
if (changed.has('bounds') || changed.has('opacity') || changed.has('url')) {
this.updateOverlay()
if (changed.has('handle') || changed.has('title')) {
const oldId = this._layerIdFor(
changed.has('handle') ? changed.get('handle') : this.handle,
changed.has('title') ? changed.get('title') : this.title
)
if (oldId && this._map) {
if (this._map.getLayer(oldId)) this._map.removeLayer(oldId)
if (this._map.getSource(oldId)) this._map.removeSource(oldId)
}
this.addOverlay()
} else if (
changed.has('bounds') ||
changed.has('opacity') ||
changed.has('url')
) {
this.removeOverlay()
this.addOverlay()
} else if (changed.has('hidden')) {
if (this.hidden) {
this.removeOverlay()
} else if (this._map && this._map.getLayer(this._layerId)) {
this._map.setLayoutProperty(this._layerId, 'visibility', 'visible')
} else {
this.addOverlay()
}
}
}
Comment thread
DavidMStraub marked this conversation as resolved.

updateOverlay() {
this.removeOverlay()
this.addOverlay()
}
}

window.customElements.define('grampsjs-map-overlay', GrampsjsMapOverlay)
Loading
Loading