Skip to content
Closed
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
1 change: 1 addition & 0 deletions docs/pages/APIs/JavaScript/Main/Event-Bus-API.md
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,7 @@ has already delivered — the request can resolve after a later event lands.
| `map:getBounds` | none | `LatLngBounds` | Get map bounds |
| `map:getZoom` | none | `number` | Get current zoom level |
| `map:getMetersPerPixel` | `{ clientX, clientY }` (optional) | `number \| null` | Ground metres per CSS pixel at that page position, or near the map's bottom-left corner when omitted. Null before the map exists, while it is tilted more than 10°, or when the planet radius is not configured |
| `map:getChromeInsets` | none | `{ bottomLeft, bottomRight, bottomLeftWidth, bottomRightWidth }` | How far, in CSS pixels, the basemap's own logo and attribution reach up from the bottom edge of the map on each side (`bottomLeft`, `bottomRight`), and in from that side's edge (`…Width`). Zero where there is none. `map:chromeInsetsChange` carries the same shape whenever it changes |
| `map:setView` | `{ center, zoom }` | `true` | Set map view |
| `map:fitBounds` | `bounds` | `true` | Fit map to bounds |
| `map:panTo` | `{ lat, lng }` | `true` | Pan map to coordinates |
Expand Down
59 changes: 59 additions & 0 deletions src/essence/Basics/MapEngines/Adapters/DeckGLAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ import type {
IMapEngine,
ComparisonConfig,
ComparisonLayout,
MapChromeInsets,
MapScreenshotResult
} from '../IMapEngine'
import { NO_CHROME_INSETS } from '../IMapEngine'
import { MAP_ENGINE } from '../types/engine'
import type { MapEngineType } from '../types/engine'
import type { LatLng, LatLngLike, BoundsLike, PointLike } from '../types/geometry'
Expand Down Expand Up @@ -453,6 +455,10 @@ export class DeckGLAdapter implements IMapEngine<Deck, Layer, PickingInfo> {
*/
private _comparisonResizeObserver: ResizeObserver | null = null

/** Watches the basemap's bottom control corners (logo, attribution). */
private _chromeObserver: ResizeObserver | null = null
private _chromeInsets: MapChromeInsets = { ...NO_CHROME_INSETS }

// ── Side-by-side comparison state ─────────────────────────────────────────
// The other way of splitting the viewport: rather than one camera drawn
// twice and wiped between, each half gets a map of its own — basemap
Expand Down Expand Up @@ -626,6 +632,10 @@ export class DeckGLAdapter implements IMapEngine<Deck, Layer, PickingInfo> {
this._comparisonEnabled = false
this._destroyComparisonSurfaces()

this._chromeObserver?.disconnect()
this._chromeObserver = null
this._chromeInsets = { ...NO_CHROME_INSETS }

if (this._isOverlayMode) {
if (this._basemap) {
this._basemap.off('move', this._onBasemapMove)
Expand Down Expand Up @@ -2430,6 +2440,55 @@ export class DeckGLAdapter implements IMapEngine<Deck, Layer, PickingInfo> {
// switch re-anchors the layers on the new style's labels.
this._basemap.on('style.load', this._onBasemapLoad)
this._basemap.on('error', this._onBasemapError)
this._watchBasemapChrome()
}

/**
* MapLibre and Mapbox create their corner containers with the map and
* fill them later (logo, attribution), so the corners are measured each
* time they resize.
*/
private _watchBasemapChrome(): void {
this._chromeObserver?.disconnect()
this._chromeObserver = null
if (typeof ResizeObserver === 'undefined') return
const corner = (side: string) =>
this._container.querySelector<HTMLElement>(
`.maplibregl-ctrl-bottom-${side}, .mapboxgl-ctrl-bottom-${side}`
)
const left = corner('left')
const right = corner('right')
// A corner's box includes its controls' margins; an empty one has no height.
const measure = (el: HTMLElement | null, map: DOMRect) => {
const rect = el?.getBoundingClientRect()
if (!rect || rect.height === 0) return { up: 0, left: 0, right: 0 }
return {
up: Math.max(0, Math.ceil(map.bottom - rect.top)),
left: Math.max(0, Math.ceil(rect.right - map.left)),
right: Math.max(0, Math.ceil(map.right - rect.left)),
}
}
this._chromeObserver = new ResizeObserver(() => {
const map = this._container.getBoundingClientRect()
const l = measure(left, map)
const r = measure(right, map)
const next: MapChromeInsets = {
bottomLeft: l.up,
bottomRight: r.up,
bottomLeftWidth: l.left,
bottomRightWidth: r.right,
}
const prev = this._chromeInsets
if ((Object.keys(next) as (keyof MapChromeInsets)[]).every((k) => next[k] === prev[k])) return
this._chromeInsets = next
this._emitEvent('chromeinsetschange', { ...next })
})
if (left) this._chromeObserver.observe(left)
if (right) this._chromeObserver.observe(right)
}

getChromeInsets(): MapChromeInsets {
return { ...this._chromeInsets }
}

/**
Expand Down
8 changes: 7 additions & 1 deletion src/essence/Basics/MapEngines/Adapters/LeafletAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*
*/

import { IMapEngine, MapScreenshotResult } from '../IMapEngine'
import { IMapEngine, MapChromeInsets, MapScreenshotResult, NO_CHROME_INSETS } from '../IMapEngine'
import {
LatLng,
LatLngLike,
Expand Down Expand Up @@ -511,6 +511,12 @@ export default class LeafletAdapter implements IMapEngine<any, any, any>, IMapEn
}
}

/** The attribution control is removed at init. The scale factor control
* Map_ adds when `mapscale` is configured is not measured. */
getChromeInsets(): MapChromeInsets {
return { ...NO_CHROME_INSETS }
}

/**
* Get the current view state
*/
Expand Down
19 changes: 19 additions & 0 deletions src/essence/Basics/MapEngines/IMapEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,22 @@ export interface MapScreenshotResult {
metersPerPixel?: number | null
}

/** How far, in CSS pixels, the basemap's logo and attribution reach up from
* the map's bottom edge on each side, and in from that side's edge. */
export interface MapChromeInsets {
bottomLeft: number
bottomRight: number
bottomLeftWidth: number
bottomRightWidth: number
}

export const NO_CHROME_INSETS: Readonly<MapChromeInsets> = Object.freeze({
bottomLeft: 0,
bottomRight: 0,
bottomLeftWidth: 0,
bottomRightWidth: 0,
})

/**
* Core map engine contract.
*
Expand Down Expand Up @@ -133,6 +149,9 @@ export interface IMapEngine<
*/
getViewState(): ViewState

/** Bottom corner chrome; `'chromeinsetschange'` is emitted when it changes. */
getChromeInsets(): MapChromeInsets

/**
* Set the full camera state. Leaflet adapter ignores bearing and pitch.
*/
Expand Down
2 changes: 2 additions & 0 deletions src/essence/Basics/Map_/Map_.js
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ let Map_ = {
window.mmgisAPI.provide('map:getBounds', () => engine.getBounds()),
window.mmgisAPI.provide('map:getZoom', () => engine.getZoom()),
window.mmgisAPI.provide('map:getMetersPerPixel', (at) => metersPerPixelAt(engine, at)),
window.mmgisAPI.provide('map:getChromeInsets', () => engine.getChromeInsets()),
window.mmgisAPI.provide('map:setView', ({ center, zoom } = {}) => {
if (!center) return false
engine.setView(center, zoom)
Expand Down Expand Up @@ -409,6 +410,7 @@ let Map_ = {
reEmit('drawcancel', 'map:drawcancel')
reEmit('move', 'map:move')
reEmit('moveend', 'map:moveend')
reEmit('chromeinsetschange', 'map:chromeInsetsChange')
// Pointer streams consumed by plugins (e.g. measure tools, overlays).
reEmit('click', 'map:click')
reEmit('mousemove', 'map:mousemove')
Expand Down
13 changes: 9 additions & 4 deletions src/essence/Basics/UserInterface_/UserInterfaceModern_.css
Original file line number Diff line number Diff line change
Expand Up @@ -555,13 +555,18 @@ body.ui-is-dragging .ui-modern-grid .ui-panel {
padding: var(--ui-float-outer-gap);
}

/* Zone positions */
/* Zone positions. The bottom zones sit above the basemap's logo and
attribution (--ui-map-chrome-*, set by the layout). */
.ui-float-zone-top-left { top: 0; left: 0; }
.ui-float-zone-top-center { top: 0; left: 50%; transform: translateX(-50%); }
.ui-float-zone-top-right { top: 0; right: 0; }
.ui-float-zone-bottom-left { bottom: 0; left: 0; }
.ui-float-zone-bottom-center { bottom: 0; left: 50%; transform: translateX(-50%); }
.ui-float-zone-bottom-right { bottom: 0; right: 0; }
.ui-float-zone-bottom-left { bottom: var(--ui-map-chrome-bottom-left, 0px); left: 0; }
.ui-float-zone-bottom-center {
bottom: max(var(--ui-map-chrome-bottom-left, 0px), var(--ui-map-chrome-bottom-right, 0px));
left: 50%;
transform: translateX(-50%);
}
.ui-float-zone-bottom-right { bottom: var(--ui-map-chrome-bottom-right, 0px); right: 0; }

/* Float panels re-enable pointer events */
.ui-float-panel {
Expand Down
60 changes: 59 additions & 1 deletion src/essence/Basics/UserInterface_/UserInterfaceModern_.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,40 @@ let layoutStyle = ''
let toolLoadQueue = []
let cleanupLayoutListener = null
let _resizeObserver = null
let cleanupChromeListener = null
let mapChromeInsets = null

const CHROME_INSET_VARS = {
bottomLeft: '--ui-map-chrome-bottom-left',
bottomRight: '--ui-map-chrome-bottom-right',
}

// Lifts the bottom float zones above the basemap's logo and attribution. The
// chrome is measured from the map's edges. A zone needs no lift for chrome
// that sits beside it, under a side panel, or the part under a bottom panel.
const _applyMapChromeInsets = () => {
const center = document.getElementById('ui-modern-center')
const map = document.getElementById('map')
const lifts = { bottomLeft: 0, bottomRight: 0 }
if (center && map && mapChromeInsets) {
const m = map.getBoundingClientRect()
const c = center.getBoundingClientRect()
const covered = Math.max(0, m.bottom - c.bottom)
if (c.left < m.left + (mapChromeInsets.bottomLeftWidth || 0))
lifts.bottomLeft = Math.max(0, mapChromeInsets.bottomLeft - covered)
if (c.right > m.right - (mapChromeInsets.bottomRightWidth || 0))
lifts.bottomRight = Math.max(0, mapChromeInsets.bottomRight - covered)
}
Object.entries(CHROME_INSET_VARS).forEach(([side, name]) => {
document.documentElement.style.setProperty(name, `${lifts[side]}px`)
})
}

const _setMapChromeInsets = (insets) => {
if (!insets) return
mapChromeInsets = insets
_applyMapChromeInsets()
}

// --- DOM Builder Helpers ---
const _createPanelIconTray = (panel) => {
Expand Down Expand Up @@ -313,11 +347,27 @@ const UserInterfaceModern_ = {
cleanupLayoutListener = mmgisAPI.on('panels:changed', this.syncDOMState.bind(this))
}

if (!cleanupChromeListener) {
cleanupChromeListener = mmgisAPI.on(
'map:chromeInsetsChange',
_setMapChromeInsets
)
}
// The map may have reported its chrome before this listener existed.
if (mmgisAPI.hasHandler('map:getChromeInsets')) {
mmgisAPI
.request('map:getChromeInsets')
.then(_setMapChromeInsets)
.catch(() => {})
}
_applyMapChromeInsets()

// Set up ResizeObserver to dispatch resize events when the center map area changes size
if (_resizeObserver) {
_resizeObserver.disconnect()
}
_resizeObserver = new ResizeObserver(() => {
_applyMapChromeInsets()
window.dispatchEvent(new Event('resize'))
})
const centerElement = document.getElementById('ui-modern-center')
Expand All @@ -340,6 +390,14 @@ const UserInterfaceModern_ = {
_resizeObserver.disconnect()
_resizeObserver = null
}
if (cleanupChromeListener) {
cleanupChromeListener()
cleanupChromeListener = null
}
mapChromeInsets = null
Object.values(CHROME_INSET_VARS).forEach((name) =>
document.documentElement.style.removeProperty(name)
)
$('#modern-content').empty()
panels = []
mmgisAPI_._pluginController = null
Expand Down Expand Up @@ -870,4 +928,4 @@ const UserInterfaceModern_ = {
}

export default UserInterfaceModern_
export { _createPanelHeader, _createPanelIconTray, _renderRegion }
export { _createPanelHeader, _createPanelIconTray, _renderRegion, _setMapChromeInsets }
Loading