fix(map): sync deck overlays with maplibre globe
This commit is contained in:
부모
e69ace4434
커밋
b0d51a9490
@ -1,8 +1,10 @@
|
||||
import { HexagonLayer } from "@deck.gl/aggregation-layers";
|
||||
import { IconLayer, GeoJsonLayer, LineLayer, ScatterplotLayer } from "@deck.gl/layers";
|
||||
import { IconLayer, LineLayer, ScatterplotLayer } from "@deck.gl/layers";
|
||||
import { MapboxOverlay } from "@deck.gl/mapbox";
|
||||
import { MapView, _GlobeView as GlobeView, type PickingInfo } from "@deck.gl/core";
|
||||
import { type PickingInfo } from "@deck.gl/core";
|
||||
import maplibregl, {
|
||||
type GeoJSONSource,
|
||||
type GeoJSONSourceSpecification,
|
||||
type LayerSpecification,
|
||||
type RasterDEMSourceSpecification,
|
||||
type StyleSpecification,
|
||||
@ -16,7 +18,7 @@ import type { ZoneId } from "../../entities/zone/model/meta";
|
||||
import { ZONE_META } from "../../entities/zone/model/meta";
|
||||
import type { MapToggleState } from "../../features/mapToggles/MapToggles";
|
||||
import type { FcLink, FleetCircle, PairLink } from "../../features/legacyDashboard/model/types";
|
||||
import { hexToRgb } from "../../shared/lib/color/hexToRgb";
|
||||
import { MaplibreDeckCustomLayer } from "./MaplibreDeckCustomLayer";
|
||||
|
||||
export type Map3DSettings = {
|
||||
showSeamark: boolean;
|
||||
@ -59,6 +61,18 @@ function isFiniteNumber(x: unknown): x is number {
|
||||
return typeof x === "number" && Number.isFinite(x);
|
||||
}
|
||||
|
||||
const EARTH_RADIUS_M = 6371008.8; // MapLibre's `earthRadius`
|
||||
const DEG2RAD = Math.PI / 180;
|
||||
|
||||
function lngLatToUnitSphere(lon: number, lat: number, altitudeMeters = 0): [number, number, number] {
|
||||
const lambda = lon * DEG2RAD;
|
||||
const phi = lat * DEG2RAD;
|
||||
const cosPhi = Math.cos(phi);
|
||||
const s = 1 + altitudeMeters / EARTH_RADIUS_M;
|
||||
// MapLibre globe space: x = east, y = north, z = lon=0 at equator.
|
||||
return [Math.sin(lambda) * cosPhi * s, Math.sin(phi) * s, Math.cos(lambda) * cosPhi * s];
|
||||
}
|
||||
|
||||
const LEGACY_CODE_COLORS: Record<string, [number, number, number]> = {
|
||||
PT: [30, 64, 175], // #1e40af
|
||||
"PT-S": [234, 88, 12], // #ea580c
|
||||
@ -433,10 +447,6 @@ type PairRangeCircle = {
|
||||
|
||||
const DECK_VIEW_ID = "mapbox";
|
||||
|
||||
function getDeckView(projection: MapProjectionId) {
|
||||
return projection === "globe" ? new GlobeView({ id: DECK_VIEW_ID }) : new MapView({ id: DECK_VIEW_ID });
|
||||
}
|
||||
|
||||
export function Map3D({
|
||||
targets,
|
||||
zones,
|
||||
@ -455,6 +465,7 @@ export function Map3D({
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const mapRef = useRef<maplibregl.Map | null>(null);
|
||||
const overlayRef = useRef<MapboxOverlay | null>(null);
|
||||
const globeDeckLayerRef = useRef<MaplibreDeckCustomLayer | null>(null);
|
||||
const showSeamarkRef = useRef(settings.showSeamark);
|
||||
const baseMapRef = useRef<BaseMapId>(baseMap);
|
||||
const projectionRef = useRef<MapProjectionId>(projection);
|
||||
@ -509,13 +520,22 @@ export function Map3D({
|
||||
map.addControl(new maplibregl.NavigationControl({ showZoom: true, showCompass: true }), "top-left");
|
||||
map.addControl(new maplibregl.ScaleControl({ maxWidth: 120, unit: "metric" }), "bottom-left");
|
||||
|
||||
// NOTE: `MapboxOverlayProps`'s TS typing pins `DeckProps` generics to `null`,
|
||||
// which makes `views` type `null`. Runtime supports `views`; cast to keep TS happy.
|
||||
overlay = new MapboxOverlay({ interleaved: true, layers: [], views: getDeckView(projectionRef.current) } as unknown as never);
|
||||
map.addControl(overlay);
|
||||
|
||||
mapRef.current = map;
|
||||
overlayRef.current = overlay;
|
||||
|
||||
// Initial Deck integration:
|
||||
// - mercator: MapboxOverlay interleaved (fast, feature-rich)
|
||||
// - globe: MapLibre custom layer that feeds Deck the globe MVP matrix (keeps basemap+layers aligned)
|
||||
if (projectionRef.current === "mercator") {
|
||||
overlay = new MapboxOverlay({ interleaved: true, layers: [] } as unknown as never);
|
||||
map.addControl(overlay);
|
||||
overlayRef.current = overlay;
|
||||
} else {
|
||||
globeDeckLayerRef.current = new MaplibreDeckCustomLayer({
|
||||
id: "deck-globe",
|
||||
viewId: DECK_VIEW_ID,
|
||||
deckProps: { layers: [] },
|
||||
});
|
||||
}
|
||||
|
||||
function applyProjection() {
|
||||
if (!map) return;
|
||||
@ -533,6 +553,14 @@ export function Map3D({
|
||||
// Ensure the seamark raster overlay exists even when using MapTiler vector styles.
|
||||
map.on("style.load", () => {
|
||||
applyProjection();
|
||||
// Globe deck layer lives inside the style and must be re-added after any style swap.
|
||||
if (projectionRef.current === "globe" && globeDeckLayerRef.current && !map!.getLayer(globeDeckLayerRef.current.id)) {
|
||||
try {
|
||||
map!.addLayer(globeDeckLayerRef.current);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
if (!showSeamarkRef.current) return;
|
||||
try {
|
||||
ensureSeamarkOverlay(map!, "bathymetry-lines");
|
||||
@ -587,6 +615,7 @@ export function Map3D({
|
||||
}
|
||||
|
||||
overlayRef.current = null;
|
||||
globeDeckLayerRef.current = null;
|
||||
mapRef.current = null;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@ -598,23 +627,7 @@ export function Map3D({
|
||||
if (!map) return;
|
||||
let cancelled = false;
|
||||
|
||||
// Recreate the Deck overlay so the Deck instance uses the correct View type
|
||||
// (MapView vs GlobeView) and stays in sync with MapLibre.
|
||||
try {
|
||||
const old = overlayRef.current;
|
||||
if (old) map.removeControl(old);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
const next = new MapboxOverlay({ interleaved: true, layers: [], views: getDeckView(projection) } as unknown as never);
|
||||
map.addControl(next);
|
||||
overlayRef.current = next;
|
||||
} catch (e) {
|
||||
console.warn("Deck overlay re-create failed:", e);
|
||||
}
|
||||
|
||||
const applyProjection = () => {
|
||||
const syncProjectionAndDeck = () => {
|
||||
if (cancelled) return;
|
||||
try {
|
||||
map.setProjection({ type: projection });
|
||||
@ -622,15 +635,66 @@ export function Map3D({
|
||||
} catch (e) {
|
||||
console.warn("Projection switch failed:", e);
|
||||
}
|
||||
|
||||
if (projection === "globe") {
|
||||
// Tear down MapboxOverlay (mercator) and use a MapLibre custom layer that renders Deck
|
||||
// with MapLibre's globe MVP matrix. This avoids the Deck <-> MapLibre globe mismatch.
|
||||
const old = overlayRef.current;
|
||||
if (old) {
|
||||
try {
|
||||
old.finalize();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
overlayRef.current = null;
|
||||
}
|
||||
|
||||
if (!globeDeckLayerRef.current) {
|
||||
globeDeckLayerRef.current = new MaplibreDeckCustomLayer({
|
||||
id: "deck-globe",
|
||||
viewId: DECK_VIEW_ID,
|
||||
deckProps: { layers: [] },
|
||||
});
|
||||
}
|
||||
|
||||
const layer = globeDeckLayerRef.current;
|
||||
if (layer && map.isStyleLoaded() && !map.getLayer(layer.id)) {
|
||||
try {
|
||||
map.addLayer(layer);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Tear down globe custom layer (if present), restore MapboxOverlay interleaved.
|
||||
const globeLayer = globeDeckLayerRef.current;
|
||||
if (globeLayer && map.getLayer(globeLayer.id)) {
|
||||
try {
|
||||
map.removeLayer(globeLayer.id);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
if (!overlayRef.current) {
|
||||
try {
|
||||
const next = new MapboxOverlay({ interleaved: true, layers: [] } as unknown as never);
|
||||
map.addControl(next);
|
||||
overlayRef.current = next;
|
||||
} catch (e) {
|
||||
console.warn("Deck overlay create failed:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (map.isStyleLoaded()) applyProjection();
|
||||
else map.once("style.load", applyProjection);
|
||||
if (map.isStyleLoaded()) syncProjectionAndDeck();
|
||||
else map.once("style.load", syncProjectionAndDeck);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
try {
|
||||
map.off("style.load", applyProjection);
|
||||
map.off("style.load", syncProjectionAndDeck);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@ -691,10 +755,121 @@ export function Map3D({
|
||||
}
|
||||
}, [settings.showSeamark]);
|
||||
|
||||
// Zones (MapLibre-native GeoJSON layers; works in both mercator + globe)
|
||||
useEffect(() => {
|
||||
const map = mapRef.current;
|
||||
if (!map) return;
|
||||
|
||||
const srcId = "zones-src";
|
||||
const fillId = "zones-fill";
|
||||
const lineId = "zones-line";
|
||||
|
||||
const zoneColorExpr: unknown[] = ["match", ["get", "zoneId"]];
|
||||
for (const k of Object.keys(ZONE_META) as ZoneId[]) {
|
||||
zoneColorExpr.push(k, ZONE_META[k].color);
|
||||
}
|
||||
zoneColorExpr.push("#3B82F6");
|
||||
|
||||
const ensure = () => {
|
||||
// Always update visibility if the layers exist.
|
||||
const visibility = overlays.zones ? "visible" : "none";
|
||||
try {
|
||||
if (map.getLayer(fillId)) map.setLayoutProperty(fillId, "visibility", visibility);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
if (map.getLayer(lineId)) map.setLayoutProperty(lineId, "visibility", visibility);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
if (!zones) return;
|
||||
if (!map.isStyleLoaded()) return;
|
||||
|
||||
try {
|
||||
const existing = map.getSource(srcId) as GeoJSONSource | undefined;
|
||||
if (existing) {
|
||||
existing.setData(zones);
|
||||
} else {
|
||||
map.addSource(srcId, { type: "geojson", data: zones } as GeoJSONSourceSpecification);
|
||||
}
|
||||
|
||||
// Keep zones below Deck layers (ships / deck-globe), and below seamarks if enabled.
|
||||
const style = map.getStyle();
|
||||
const firstSymbol = (style.layers || []).find((l) => (l as { type?: string } | undefined)?.type === "symbol") as
|
||||
| { id?: string }
|
||||
| undefined;
|
||||
const before = map.getLayer("deck-globe")
|
||||
? "deck-globe"
|
||||
: map.getLayer("ships")
|
||||
? "ships"
|
||||
: map.getLayer("seamark")
|
||||
? "seamark"
|
||||
: firstSymbol?.id;
|
||||
|
||||
if (!map.getLayer(fillId)) {
|
||||
map.addLayer(
|
||||
{
|
||||
id: fillId,
|
||||
type: "fill",
|
||||
source: srcId,
|
||||
paint: {
|
||||
"fill-color": zoneColorExpr as never,
|
||||
"fill-opacity": 0.12,
|
||||
},
|
||||
layout: { visibility },
|
||||
} as unknown as LayerSpecification,
|
||||
before,
|
||||
);
|
||||
}
|
||||
|
||||
if (!map.getLayer(lineId)) {
|
||||
map.addLayer(
|
||||
{
|
||||
id: lineId,
|
||||
type: "line",
|
||||
source: srcId,
|
||||
paint: {
|
||||
"line-color": zoneColorExpr as never,
|
||||
"line-opacity": 0.85,
|
||||
"line-width": ["interpolate", ["linear"], ["zoom"], 4, 0.8, 10, 1.4, 14, 2.1],
|
||||
},
|
||||
layout: { visibility },
|
||||
} as unknown as LayerSpecification,
|
||||
before,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Zones layer setup failed:", e);
|
||||
}
|
||||
};
|
||||
|
||||
if (map.isStyleLoaded()) ensure();
|
||||
map.on("style.load", ensure);
|
||||
return () => {
|
||||
try {
|
||||
map.off("style.load", ensure);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
}, [zones, overlays.zones]);
|
||||
|
||||
const shipData = useMemo(() => {
|
||||
return targets.filter((t) => isFiniteNumber(t.lat) && isFiniteNumber(t.lon));
|
||||
}, [targets]);
|
||||
|
||||
const globePosByMmsi = useMemo(() => {
|
||||
if (projection !== "globe") return null;
|
||||
const m = new Map<number, [number, number, number]>();
|
||||
for (const t of shipData) {
|
||||
// Slightly above the sea surface to keep the icon readable and avoid depth-fighting.
|
||||
m.set(t.mmsi, lngLatToUnitSphere(t.lon, t.lat, 12));
|
||||
}
|
||||
return m;
|
||||
}, [projection, shipData]);
|
||||
|
||||
const legacyTargets = useMemo(() => {
|
||||
if (!legacyHits) return [];
|
||||
return shipData.filter((t) => legacyHits.has(t.mmsi));
|
||||
@ -727,14 +902,15 @@ export function Map3D({
|
||||
|
||||
// Update Deck.gl layers
|
||||
useEffect(() => {
|
||||
const overlay = overlayRef.current;
|
||||
const map = mapRef.current;
|
||||
if (!overlay || !map) return;
|
||||
if (!map) return;
|
||||
const deckTarget = projection === "globe" ? globeDeckLayerRef.current : overlayRef.current;
|
||||
if (!deckTarget) return;
|
||||
|
||||
const overlayParams = projection === "globe" ? GLOBE_OVERLAY_PARAMS : DEPTH_DISABLED_PARAMS;
|
||||
const layers = [];
|
||||
|
||||
if (settings.showDensity) {
|
||||
if (settings.showDensity && projection !== "globe") {
|
||||
layers.push(
|
||||
new HexagonLayer<AisTarget>({
|
||||
id: "density",
|
||||
@ -750,42 +926,13 @@ export function Map3D({
|
||||
);
|
||||
}
|
||||
|
||||
if (overlays.zones && zones) {
|
||||
layers.push(
|
||||
new GeoJsonLayer({
|
||||
id: "zones",
|
||||
data: zones,
|
||||
pickable: true,
|
||||
// Avoid z-fighting flicker with other layers in the shared MapLibre depth buffer.
|
||||
parameters: overlayParams,
|
||||
stroked: true,
|
||||
filled: true,
|
||||
getFillColor: (f) => {
|
||||
const zoneId = (f.properties as { zoneId?: string } | undefined)?.zoneId as ZoneId | undefined;
|
||||
const col = zoneId ? ZONE_META[zoneId]?.color : "#3B82F6";
|
||||
const [r, g, b] = hexToRgb(col);
|
||||
return [r, g, b, 22];
|
||||
},
|
||||
getLineColor: (f) => {
|
||||
const zoneId = (f.properties as { zoneId?: string } | undefined)?.zoneId as ZoneId | undefined;
|
||||
const col = zoneId ? ZONE_META[zoneId]?.color : "#3B82F6";
|
||||
const [r, g, b] = hexToRgb(col);
|
||||
return [r, g, b, 200];
|
||||
},
|
||||
lineWidthMinPixels: 1,
|
||||
lineWidthMaxPixels: 2,
|
||||
getLineWidth: 1,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (overlays.fleetCircles && (fleetCircles?.length ?? 0) > 0) {
|
||||
if (overlays.fleetCircles && projection !== "globe" && (fleetCircles?.length ?? 0) > 0) {
|
||||
layers.push(
|
||||
new ScatterplotLayer<FleetCircle>({
|
||||
id: "fleet-circles",
|
||||
data: fleetCircles,
|
||||
pickable: false,
|
||||
billboard: projection === "globe",
|
||||
billboard: false,
|
||||
parameters: overlayParams,
|
||||
filled: false,
|
||||
stroked: true,
|
||||
@ -799,13 +946,13 @@ export function Map3D({
|
||||
);
|
||||
}
|
||||
|
||||
if (overlays.pairRange && pairRanges.length > 0) {
|
||||
if (overlays.pairRange && projection !== "globe" && pairRanges.length > 0) {
|
||||
layers.push(
|
||||
new ScatterplotLayer<PairRangeCircle>({
|
||||
id: "pair-range",
|
||||
data: pairRanges,
|
||||
pickable: false,
|
||||
billboard: projection === "globe",
|
||||
billboard: false,
|
||||
parameters: overlayParams,
|
||||
filled: false,
|
||||
stroked: true,
|
||||
@ -827,8 +974,8 @@ export function Map3D({
|
||||
data: pairLinks,
|
||||
pickable: false,
|
||||
parameters: overlayParams,
|
||||
getSourcePosition: (d) => d.from,
|
||||
getTargetPosition: (d) => d.to,
|
||||
getSourcePosition: (d) => (projection === "globe" ? lngLatToUnitSphere(d.from[0], d.from[1]) : d.from),
|
||||
getTargetPosition: (d) => (projection === "globe" ? lngLatToUnitSphere(d.to[0], d.to[1]) : d.to),
|
||||
getColor: (d) => (d.warn ? [245, 158, 11, 220] : [59, 130, 246, 85]),
|
||||
getWidth: (d) => (d.warn ? 2.2 : 1.4),
|
||||
widthUnits: "pixels",
|
||||
@ -843,8 +990,8 @@ export function Map3D({
|
||||
data: fcDashed,
|
||||
pickable: false,
|
||||
parameters: overlayParams,
|
||||
getSourcePosition: (d) => d.from,
|
||||
getTargetPosition: (d) => d.to,
|
||||
getSourcePosition: (d) => (projection === "globe" ? lngLatToUnitSphere(d.from[0], d.from[1]) : d.from),
|
||||
getTargetPosition: (d) => (projection === "globe" ? lngLatToUnitSphere(d.to[0], d.to[1]) : d.to),
|
||||
getColor: (d) => (d.suspicious ? [239, 68, 68, 220] : [217, 119, 6, 200]),
|
||||
getWidth: () => 1.3,
|
||||
widthUnits: "pixels",
|
||||
@ -873,7 +1020,10 @@ export function Map3D({
|
||||
if (!rgb) return [245, 158, 11, 200];
|
||||
return [rgb[0], rgb[1], rgb[2], 200];
|
||||
},
|
||||
getPosition: (d) => [d.lon, d.lat],
|
||||
getPosition: (d) =>
|
||||
projection === "globe"
|
||||
? (globePosByMmsi?.get(d.mmsi) ?? lngLatToUnitSphere(d.lon, d.lat, 12))
|
||||
: ([d.lon, d.lat] as [number, number]),
|
||||
updateTriggers: {
|
||||
getRadius: [selectedMmsi],
|
||||
getLineColor: [legacyHits],
|
||||
@ -895,7 +1045,10 @@ export function Map3D({
|
||||
iconAtlas: "/assets/ship.svg",
|
||||
iconMapping: SHIP_ICON_MAPPING,
|
||||
getIcon: () => "ship",
|
||||
getPosition: (d) => [d.lon, d.lat],
|
||||
getPosition: (d) =>
|
||||
projection === "globe"
|
||||
? (globePosByMmsi?.get(d.mmsi) ?? lngLatToUnitSphere(d.lon, d.lat, 12))
|
||||
: ([d.lon, d.lat] as [number, number]),
|
||||
getAngle: (d) => (isFiniteNumber(d.cog) ? d.cog : 0),
|
||||
sizeUnits: "pixels",
|
||||
getSize: (d) => (selectedMmsi && d.mmsi === selectedMmsi ? 34 : 22),
|
||||
@ -909,7 +1062,7 @@ export function Map3D({
|
||||
);
|
||||
}
|
||||
|
||||
overlay.setProps({
|
||||
const deckProps = {
|
||||
layers,
|
||||
getTooltip: (info: PickingInfo) => {
|
||||
if (!info.object) return null;
|
||||
@ -962,7 +1115,10 @@ export function Map3D({
|
||||
map.easeTo({ center: [t.lon, t.lat], zoom: Math.max(map.getZoom(), 10), duration: 600 });
|
||||
}
|
||||
},
|
||||
});
|
||||
} as const;
|
||||
|
||||
if (projection === "globe") globeDeckLayerRef.current?.setProps(deckProps);
|
||||
else overlayRef.current?.setProps(deckProps as unknown as never);
|
||||
}, [
|
||||
projection,
|
||||
shipData,
|
||||
@ -982,6 +1138,7 @@ export function Map3D({
|
||||
pairRanges,
|
||||
fcDashed,
|
||||
fleetCircles,
|
||||
globePosByMmsi,
|
||||
]);
|
||||
|
||||
return <div ref={containerRef} style={{ width: "100%", height: "100%" }} />;
|
||||
|
||||
130
apps/web/src/widgets/map3d/MaplibreDeckCustomLayer.ts
Normal file
130
apps/web/src/widgets/map3d/MaplibreDeckCustomLayer.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import { Deck, MapController, View, Viewport, type DeckProps } from "@deck.gl/core";
|
||||
import type maplibregl from "maplibre-gl";
|
||||
|
||||
type MatrixViewState = {
|
||||
// MapLibre provides a full world->clip matrix as `modelViewProjectionMatrix`.
|
||||
// We treat it as the viewport's projection matrix and keep viewMatrix identity.
|
||||
projectionMatrix: number[];
|
||||
viewMatrix?: number[];
|
||||
// Deck's View state is constrained to include transition props. We only need one overlapping key
|
||||
// to satisfy TS structural checks without pulling in internal deck.gl types.
|
||||
transitionDuration?: number;
|
||||
};
|
||||
|
||||
class MatrixView extends View<MatrixViewState> {
|
||||
getViewportType(viewState: MatrixViewState): typeof Viewport {
|
||||
void viewState;
|
||||
return Viewport;
|
||||
}
|
||||
|
||||
// Controller isn't used (Deck is created with `controller: false`) but View requires one.
|
||||
protected get ControllerType(): typeof MapController {
|
||||
return MapController;
|
||||
}
|
||||
}
|
||||
|
||||
const IDENTITY_4x4: number[] = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
||||
|
||||
function readMat4(m: ArrayLike<number>): number[] {
|
||||
const out = new Array<number>(16);
|
||||
for (let i = 0; i < 16; i++) out[i] = m[i] as number;
|
||||
return out;
|
||||
}
|
||||
|
||||
function mat4Changed(a: number[] | undefined, b: ArrayLike<number>): boolean {
|
||||
if (!a || a.length !== 16) return true;
|
||||
// The matrix values change on map move/rotate. A strict compare is fine.
|
||||
for (let i = 0; i < 16; i++) {
|
||||
if (a[i] !== (b[i] as number)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export class MaplibreDeckCustomLayer implements maplibregl.CustomLayerInterface {
|
||||
id: string;
|
||||
type = "custom" as const;
|
||||
renderingMode = "3d" as const;
|
||||
|
||||
private _map: maplibregl.Map | null = null;
|
||||
private _deck: Deck<MatrixView[]> | null = null;
|
||||
private _deckProps: Partial<DeckProps<MatrixView[]>> = {};
|
||||
private _viewId: string;
|
||||
private _lastMvp: number[] | undefined;
|
||||
|
||||
constructor(opts: { id: string; viewId: string; deckProps?: Partial<DeckProps<MatrixView[]>> }) {
|
||||
this.id = opts.id;
|
||||
this._viewId = opts.viewId;
|
||||
this._deckProps = opts.deckProps ?? {};
|
||||
}
|
||||
|
||||
get deck(): Deck<MatrixView[]> | null {
|
||||
return this._deck;
|
||||
}
|
||||
|
||||
setProps(next: Partial<DeckProps<MatrixView[]>>) {
|
||||
this._deckProps = { ...this._deckProps, ...next };
|
||||
if (this._deck) this._deck.setProps(this._deckProps as DeckProps<MatrixView[]>);
|
||||
this._map?.triggerRepaint();
|
||||
}
|
||||
|
||||
onAdd(map: maplibregl.Map, gl: WebGLRenderingContext | WebGL2RenderingContext): void {
|
||||
this._map = map;
|
||||
|
||||
const deck = new Deck<MatrixView[]>({
|
||||
...this._deckProps,
|
||||
// Share MapLibre's WebGL context + canvas (single context).
|
||||
gl: gl as WebGL2RenderingContext,
|
||||
canvas: map.getCanvas(),
|
||||
width: null,
|
||||
height: null,
|
||||
// Let MapLibre own pointer/touch behaviors on the shared canvas.
|
||||
touchAction: "none",
|
||||
controller: false,
|
||||
views: this._deckProps.views ?? [new MatrixView({ id: this._viewId })],
|
||||
viewState: this._deckProps.viewState ?? { [this._viewId]: { projectionMatrix: IDENTITY_4x4 } },
|
||||
// Only request a repaint when Deck thinks it needs one. Drawing happens in `render()`.
|
||||
_customRender: () => map.triggerRepaint(),
|
||||
parameters: {
|
||||
// Match @deck.gl/mapbox interleaved defaults (premultiplied blending).
|
||||
depthWriteEnabled: true,
|
||||
depthCompare: "less-equal",
|
||||
depthBias: 0,
|
||||
blend: true,
|
||||
blendColorSrcFactor: "src-alpha",
|
||||
blendColorDstFactor: "one-minus-src-alpha",
|
||||
blendAlphaSrcFactor: "one",
|
||||
blendAlphaDstFactor: "one-minus-src-alpha",
|
||||
blendColorOperation: "add",
|
||||
blendAlphaOperation: "add",
|
||||
...this._deckProps.parameters,
|
||||
},
|
||||
});
|
||||
|
||||
this._deck = deck;
|
||||
}
|
||||
|
||||
onRemove(): void {
|
||||
this._deck?.finalize();
|
||||
this._deck = null;
|
||||
this._map = null;
|
||||
this._lastMvp = undefined;
|
||||
}
|
||||
|
||||
render(_gl: WebGLRenderingContext | WebGL2RenderingContext, options: maplibregl.CustomRenderMethodInput): void {
|
||||
const deck = this._deck;
|
||||
if (!deck) return;
|
||||
|
||||
// MapLibre gives us a world->clip matrix for the current projection (mercator/globe).
|
||||
// For globe, this matrix expects unit-sphere world coordinates (see MapLibre's globe transform).
|
||||
if (mat4Changed(this._lastMvp, options.modelViewProjectionMatrix)) {
|
||||
const projectionMatrix = readMat4(options.modelViewProjectionMatrix);
|
||||
this._lastMvp = projectionMatrix;
|
||||
deck.setProps({ viewState: { [this._viewId]: { projectionMatrix, viewMatrix: IDENTITY_4x4 } } });
|
||||
}
|
||||
|
||||
deck._drawLayers("maplibre-custom", {
|
||||
clearCanvas: false,
|
||||
clearStack: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
불러오는 중...
Reference in New Issue
Block a user