diff --git a/packages/client/src/admin/data/glStyleUtils.ts b/packages/client/src/admin/data/glStyleUtils.ts new file mode 100644 index 00000000..066caf8c --- /dev/null +++ b/packages/client/src/admin/data/glStyleUtils.ts @@ -0,0 +1,415 @@ +import cloneDeep from "lodash.clonedeep"; +import { + AnyLayer, + CircleLayer, + Expression, + FillLayer, + LineLayer, + SymbolLayer, +} from "mapbox-gl"; +import { isExpression } from "../../dataLayers/legends/utils"; +import { colord } from "colord"; + +type HighlightableLayer = FillLayer | LineLayer | SymbolLayer | CircleLayer; +export function isHighlightableLayer( + layer: AnyLayer +): layer is HighlightableLayer { + return ( + layer.type === "fill" || + layer.type === "line" || + layer.type === "symbol" || + layer.type === "circle" + ); +} + +/** + * Converts legacy mapbox-gl-style filter expressions to the modern form which + * uses the `["get", "property"]` syntax. + * + * https://docs.mapbox.com/style-spec/reference/other/#other-filter + * + * Should be able to run this against even modern filter expressions without + * any problems. + * + * @param expression + * @returns mapboxgl.Expression + */ +export function upgradeLegacyFilterExpressions( + expression: Expression +): Expression { + const [operator] = expression; + switch (operator) { + case "==": + case "!=": + case ">": + case ">=": + case "<": + case "<=": + return upgradeComparisonFilter(expression); + // @ts-ignore + case "!has": + case "has": + return upgradeExistentialFilter(expression as [string, string]); + //@ts-ignore + case "!in": + case "in": + //@ts-ignore + return upgradeSetMembershipFilter(expression); + // @ts-ignore + case "none": + case "all": + case "any": + return upgradeCombinationFilter(expression); + default: + return [...expression]; + } +} + +function hasLegacyFilterArgs(expression: Expression): boolean { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [operator, left, right] = expression; + return !Array.isArray(left) && !Array.isArray(right); +} + +// https://docs.mapbox.com/style-spec/reference/other/#comparison-filters +function upgradeComparisonFilter(expression: Expression): Expression { + if (hasLegacyFilterArgs(expression)) { + return [expression[0], ["get", expression[1]], expression[2]]; + } else { + return expression; + } +} + +// https://docs.mapbox.com/style-spec/reference/other/#existential-filters +function upgradeExistentialFilter(expression: [string, string]): Expression { + if (typeof expression[1] === "string") { + if (expression[0] === "!has") { + return ["!", ["has", expression[1]]]; + } else { + return [...expression] as Expression; + } + } else { + return expression as Expression; + } +} + +// https://docs.mapbox.com/style-spec/reference/other/#set-membership-filters +function upgradeSetMembershipFilter( + expression: ["in" | "!in", string, ...any] +): Expression { + const [operator, property, ...values] = expression; + if (Array.isArray(property)) { + return expression as Expression; + } + const propertyExpression = + property === "$type" ? ["geometry-type"] : ["get", property]; + if (operator === "!in") { + return ["!", ["in", propertyExpression, values]]; + } else { + return ["in", propertyExpression, values]; + } +} + +// https://docs.mapbox.com/style-spec/reference/other/#combining-filters +function upgradeCombinationFilter(expression: Expression): Expression { + const [operator, ...args] = expression; + // @ts-ignore + if (operator === "none") { + return ["!", [operator, ...args.map(upgradeLegacyFilterExpressions)]]; + } else { + return [operator, ...args.map(upgradeLegacyFilterExpressions)]; + } +} + +/** + * Generates appropriate styles for hovering or selecting features within a set + * of layers. If the layers already contain feature-state expressions which + * access "selected" or "hovered", this function will not modify the layers. + * + * Supports fill, line, circle, and text label layers. All other layer types + * will be returned unmodified. + * + * Unfortunately "marker layers" (symbols with an icon-image) cannot be + * supported since layout properties cannot be modified by feature-state. + * + * @param layers + * @returns + */ +export function addInteractivityExpressions(layers: AnyLayer[]) { + // First check to see if the author of the styles for this layer already + // added interactivity expressions. If so, we don't want to add generated + // styles. + if ( + layers.find( + (layer) => isHighlightableLayer(layer) && hasFeatureStateExpression(layer) + ) + ) { + return layers; + } + if (!layers.find((l) => isHighlightableLayer(l))) { + return layers; + } + const lineLayers = layers.filter( + (layer) => layer.type === "line" + ) as LineLayer[]; + const fillLayers = layers.filter( + (layer) => layer.type === "fill" + ) as FillLayer[]; + const circleLayers = layers.filter( + (layer) => layer.type === "circle" + ) as CircleLayer[]; + const markerLayers = layers.filter( + (layer) => layer.type === "symbol" && layer.layout?.["icon-image"] + ) as SymbolLayer[]; + const labelLayers = layers.filter( + (layer) => + layer.type === "symbol" && + layer.layout?.["text-field"] && + !layer.layout?.["icon-image"] + ) as SymbolLayer[]; + const newLayers: AnyLayer[] = []; + if (lineLayers.length > 0) { + // this is the easiest. just increase widths of existing lines + for (const l of layers) { + if (l.type !== "line") { + newLayers.push(l); + continue; + } + const layer = cloneDeep(l); + if (layer.paint) { + const lineWidthValue = layer.paint["line-width"]; + const lineOpacity = layer.paint["line-opacity"]; + const lineColor = layer.paint["line-color"]; + layer.paint = { + ...layer.paint, + "line-width": [ + "case", + ["boolean", ["feature-state", "selected"], false], + ["max", ["*", lineWidthValue || 1, 1.5], 3], + ["boolean", ["feature-state", "hovered"], false], + ["max", ["*", lineWidthValue || 1, 1.25], 2], + lineWidthValue || 1, + ], + "line-opacity": [ + "case", + ["boolean", ["feature-state", "selected"], false], + 1, + ["boolean", ["feature-state", "hovered"], false], + 1, + lineOpacity || 1, + ], + "line-color": [ + "let", + "c", + ["to-rgba", lineColor || "#000"], + [ + "rgba", + ["at", 0, ["var", "c"]], + ["at", 1, ["var", "c"]], + ["at", 2, ["var", "c"]], + [ + "case", + ["boolean", ["feature-state", "selected"], false], + 1, + ["boolean", ["feature-state", "hovered"], false], + 1, + 0, + ], + ], + ], + }; + } + newLayers.push(layer); + } + } else if (fillLayers.length > 0) { + // uh oh. There are fill layers but no line layers. We'll need to create + // line layers for each fill layer. If there are multiple fill layers, + // it's likely they are using filters to show different colors for + // different feature classes. We'll need to create a line layer for each + // fill layer. + for (const l of layers) { + if (l.type !== "fill") { + newLayers.push(l); + continue; + } + newLayers.push(l); + if (l.paint) { + let fillColorValue = l.paint["fill-color"]; + if (!isExpression(fillColorValue)) { + fillColorValue = colord(fillColorValue as string) + .alpha(1) + .toHex(); + } + let fillOutlineColorValue = l.paint["fill-outline-color"]; + if (!isExpression(fillOutlineColorValue)) { + fillOutlineColorValue = colord(fillOutlineColorValue as string) + .alpha(1) + .toHex(); + } + newLayers.push({ + type: "line", + // eslint-disable-next-line i18next/no-literal-string + id: `${l.id}-outline`, + source: l.source, + ...(l["source-layer"] ? { "source-layer": l["source-layer"] } : {}), + ...(l.filter ? { filter: l.filter } : {}), + layout: { + visibility: "visible", + }, + paint: { + "line-color": [ + "let", + "c", + ["to-rgba", fillOutlineColorValue || fillColorValue || "#000"], + [ + "rgba", + ["at", 0, ["var", "c"]], + ["at", 1, ["var", "c"]], + ["at", 2, ["var", "c"]], + [ + "case", + ["boolean", ["feature-state", "selected"], false], + 1, + ["boolean", ["feature-state", "hovered"], false], + 1, + 0, + ], + ], + ], + "line-width": [ + "case", + ["boolean", ["feature-state", "selected"], false], + 2, + ["boolean", ["feature-state", "hovered"], false], + 1, + 0, + ], + }, + } as LineLayer); + } + } + } else if (circleLayers.length > 0) { + for (const l of layers) { + if (l.type !== "circle") { + newLayers.push(l); + continue; + } + const layer = cloneDeep(l); + // Slightly increase the size of the circle if hovered or selected + if (layer.paint) { + const circleRadius = layer.paint["circle-radius"]; + const circleStrokeWidth = layer.paint["circle-stroke-width"]; + const circleStrokeOpacity = layer.paint["circle-stroke-opacity"]; + layer.paint = { + ...layer.paint, + ...(circleStrokeWidth + ? { + "circle-stroke-width": [ + "case", + ["boolean", ["feature-state", "selected"], false], + ["*", circleStrokeWidth, 2], + ["boolean", ["feature-state", "hovered"], false], + ["*", circleStrokeWidth, 1.5], + circleStrokeWidth, + ], + } + : {}), + ...(isExpression(circleRadius) && /interpolate/.test(circleRadius[0]) + ? {} + : { + "circle-radius": [ + "case", + ["boolean", ["feature-state", "selected"], false], + ["*", circleRadius || 1, 1.5], + ["boolean", ["feature-state", "hovered"], false], + ["*", circleRadius || 1, 1.25], + circleRadius || 1, + ], + }), + ...(circleStrokeOpacity + ? { + "circle-stroke-opacity": [ + "case", + ["boolean", ["feature-state", "selected"], false], + 1, + ["boolean", ["feature-state", "hovered"], false], + 1, + circleStrokeOpacity, + ], + } + : {}), + }; + newLayers.push(layer); + } + } + } else if (markerLayers.length > 0) { + return layers; + // Can't really do anything useful here since we can't alter layout + // properties. :( + } else if (labelLayers.length > 0) { + // Increase the size of the halo. Can't do much else because feature-state + // can only change paint properties, not layout properties. + for (const l of layers) { + if (l.type !== "symbol" || !("text-field" in (l.layout || {}))) { + newLayers.push(l); + continue; + } + const layer = cloneDeep(l); + if (layer.layout) { + const textHaloWidth = (layer.paint || { "text-halo-width": 0 })[ + "text-halo-width" + ]; + layer.paint = { + ...layer.paint, + "text-halo-width": [ + "case", + ["boolean", ["feature-state", "selected"], false], + ["*", textHaloWidth || 1, 2], + ["boolean", ["feature-state", "hovered"], false], + ["*", textHaloWidth || 1, 1.5], + textHaloWidth || 0, + ], + }; + } + newLayers.push(layer); + } + } else { + return layers; + } + return newLayers; +} + +/** + * Determines if the given layer uses feature-state expressions to highlight + * hovered or selected features. + * + * @param layer + * @returns + */ +function hasFeatureStateExpression(layer: HighlightableLayer): boolean { + for (const value of [ + ...Object.values(layer.paint || {}), + Object.values(layer.layout || {}), + ]) { + if (isExpression(value)) { + if (usesFeatureState(value, ["hovered", "selected"])) { + return true; + } + } + } + return false; +} + +function usesFeatureState(expression: Expression, states: string[]): boolean { + if (expression[0] === "feature-state") { + return states.includes(expression[1]); + } + for (const arg of expression.slice(1)) { + if (isExpression(arg)) { + if (usesFeatureState(arg, states)) { + return true; + } + } + } + return false; +} diff --git a/packages/client/src/dataLayers/LayerInteractivityManager.ts b/packages/client/src/dataLayers/LayerInteractivityManager.ts index c42693e2..431e3c84 100644 --- a/packages/client/src/dataLayers/LayerInteractivityManager.ts +++ b/packages/client/src/dataLayers/LayerInteractivityManager.ts @@ -1,6 +1,4 @@ -import { - FilterOptions, - GeoJSONSource, +import mapboxgl, { Layer, Map, MapboxGeoJSONFeature, @@ -8,12 +6,10 @@ import { Popup, } from "mapbox-gl"; import { - CROSSHAIR_IMAGE_ID, idForLayer, isSeaSketchLayerId, layerIdFromStyleLayerId, MapContextInterface, - POPUP_CLICK_LOCATION_SOURCE, Tooltip, } from "./MapContextManager"; import Mustache from "mustache"; @@ -25,14 +21,9 @@ import { DataSourceTypes, InteractivityType, } from "../generated/graphql"; -import // getDynamicArcGISStyle, -// identifyLayers, -"../admin/data/arcgis/arcgis"; import { EventEmitter } from "eventemitter3"; import { CustomGLSource } from "@seasketch/mapbox-gl-esri-sources"; import { identifyLayers } from "../admin/data/arcgis/arcgis"; -import { EMPTY_FEATURE_COLLECTION } from "../draw/useMapboxGLDraw"; -import { GeoJsonProperties } from "geojson"; const PopupNumberFormatter = Intl.NumberFormat(undefined, { maximumFractionDigits: 2, @@ -48,10 +39,6 @@ const PopupNumberFormatter = Intl.NumberFormat(undefined, { export default class LayerInteractivityManager extends EventEmitter { private map: Map; private _setState: Dispatch>; - private _setHighlightedLayer: ( - layerId: string | undefined, - filter?: FilterOptions - ) => void; private previousState?: MapContextInterface; /** List of interactive layers that are currently shown on the map. Update with setVisibleLayers() */ @@ -71,6 +58,8 @@ export default class LayerInteractivityManager extends EventEmitter { private focusedSketchId?: number; private customSources: { [sourceId: string]: CustomGLSource } = {}; private tocItemLabels: { [stableId: string]: string } = {}; + private selectedFeature?: mapboxgl.FeatureIdentifier; + private hoveredFeature?: mapboxgl.FeatureIdentifier; /** * @@ -80,17 +69,12 @@ export default class LayerInteractivityManager extends EventEmitter { */ constructor( map: Map, - setState: Dispatch>, - setHighlightedLayer: ( - layerId: string | undefined, - filter?: FilterOptions - ) => void + setState: Dispatch> ) { super(); this.map = map; this.registerEventListeners(map); this._setState = setState; - this._setHighlightedLayer = setHighlightedLayer; } /** @@ -312,6 +296,17 @@ export default class LayerInteractivityManager extends EventEmitter { if (this.paused) { return; } + if (this.hoveredFeature) { + this.map.setFeatureState(this.hoveredFeature, { + hovered: false, + selected: + this.selectedFeature && + this.selectedFeature?.id === this.hoveredFeature?.id && + this.selectedFeature?.source === this.hoveredFeature?.source && + this.selectedFeature?.sourceLayer === + this.hoveredFeature?.sourceLayer, + }); + } setTimeout(() => { delete this.previousInteractionTarget; this.setState((prev) => ({ @@ -374,10 +369,6 @@ export default class LayerInteractivityManager extends EventEmitter { } ); if (interactivitySetting.type === InteractivityType.Popup) { - const popupSource = this.map.getSource( - POPUP_CLICK_LOCATION_SOURCE - ) as GeoJSONSource; - popupSource?.setData(EMPTY_FEATURE_COLLECTION); this.setState((prev) => ({ ...prev, sidebarPopupContent: undefined, @@ -388,19 +379,6 @@ export default class LayerInteractivityManager extends EventEmitter { .setHTML(content) .addTo(this.map!); } else { - if (this.map) { - const popupSource = this.map.getSource( - POPUP_CLICK_LOCATION_SOURCE - ) as GeoJSONSource; - popupSource.setData({ - type: "Feature", - properties: {}, - geometry: { - type: "Point", - coordinates: [e.lngLat.lng, e.lngLat.lat], - }, - }); - } const titleContent = Mustache.render( interactivitySetting.title || "", { @@ -413,16 +391,7 @@ export default class LayerInteractivityManager extends EventEmitter { sidebarPopupContent: content, sidebarPopupTitle: titleContent, })); - const idProperty = getIdProperty(top.properties); - if (idProperty) { - this._setHighlightedLayer(top.layer.id, [ - "==", - ["get", idProperty], - top.properties![idProperty], - ] as FilterOptions); - } else { - this._setHighlightedLayer(undefined); - } + this.setSelectedFeature(top); } } else if ( interactivitySetting && @@ -433,17 +402,6 @@ export default class LayerInteractivityManager extends EventEmitter { sidebarPopupContent: undefined, sidebarPopupTitle: undefined, })); - const popupSource = this.map.getSource( - POPUP_CLICK_LOCATION_SOURCE - ) as GeoJSONSource; - popupSource.setData({ - type: "Feature", - properties: {}, - geometry: { - type: "Point", - coordinates: [e.lngLat.lng, e.lngLat.lat], - }, - }); const lyr = this.layers[top.layer.id]; // @ts-ignore const layerLabel = (this.tocItemLabels || {})[lyr?.tocId]; @@ -489,18 +447,12 @@ export default class LayerInteractivityManager extends EventEmitter { vectorPopupOpened = true; } } else { - const popupSource = this.map?.getSource(POPUP_CLICK_LOCATION_SOURCE) as - | GeoJSONSource - | undefined; - if (popupSource) { - popupSource.setData(EMPTY_FEATURE_COLLECTION); - } this.setState((prev) => ({ ...prev, sidebarPopupContent: undefined, sidebarPopupTitle: undefined, })); - this._setHighlightedLayer(undefined); + this.setSelectedFeature(undefined); } if (!vectorPopupOpened) { // Are any image layers active that support identify tools? @@ -518,19 +470,69 @@ export default class LayerInteractivityManager extends EventEmitter { } }; + setSelectedFeature(feature?: mapboxgl.FeatureIdentifier) { + if (this.selectedFeature?.id === feature?.id) { + return; + } + if (this.selectedFeature) { + this.map.setFeatureState(this.selectedFeature, { + selected: false, + hovered: + this.hoveredFeature && + this.hoveredFeature?.id === this.selectedFeature?.id && + this.hoveredFeature?.source === this.selectedFeature?.source && + this.hoveredFeature?.sourceLayer === + this.selectedFeature?.sourceLayer, + }); + } + if (feature) { + this.map.setFeatureState(feature, { + selected: true, + hovered: + this.hoveredFeature && + this.hoveredFeature?.id === feature?.id && + this.hoveredFeature?.source === feature?.source && + this.hoveredFeature?.sourceLayer === feature?.sourceLayer, + }); + } + this.selectedFeature = feature; + } + + setHoveredFeature(feature?: mapboxgl.FeatureIdentifier) { + if (this.hoveredFeature?.id === feature?.id) { + return; + } + if (this.hoveredFeature) { + this.map.setFeatureState(this.hoveredFeature, { + hovered: false, + selected: + this.selectedFeature && + this.selectedFeature?.id === this.hoveredFeature?.id && + this.selectedFeature?.source === this.hoveredFeature?.source && + this.selectedFeature?.sourceLayer === + this.hoveredFeature?.sourceLayer, + }); + } + if (feature) { + this.map.setFeatureState(feature, { + hovered: true, + selected: + this.selectedFeature && + this.selectedFeature?.id === feature?.id && + this.selectedFeature?.source === feature?.source && + this.selectedFeature?.sourceLayer === feature?.sourceLayer, + }); + } + this.hoveredFeature = feature; + } + clearSidebarPopup = () => { this.setState((prev) => ({ ...prev, sidebarPopupContent: undefined, sidebarPopupTitle: undefined, })); - this._setHighlightedLayer(undefined); - const popupSource = this.map?.getSource(POPUP_CLICK_LOCATION_SOURCE) as - | GeoJSONSource - | undefined; - if (popupSource) { - popupSource.setData(EMPTY_FEATURE_COLLECTION); - } + this.setSelectedFeature(undefined); }; // Note, this will only work with ArcGIS Server @@ -727,6 +729,7 @@ export default class LayerInteractivityManager extends EventEmitter { }); if (features.length && layerIds.indexOf(features[0].layer.id) > -1) { const top = features[0]; + this.setHoveredFeature(top); const interactivitySetting = this.getInteractivitySettingForFeature(top); if (interactivitySetting) { let cursor = ""; @@ -817,6 +820,7 @@ export default class LayerInteractivityManager extends EventEmitter { clear(); } } else { + this.setHoveredFeature(undefined); clear(); } }; @@ -859,28 +863,3 @@ const mustacheHelpers = { return `${Math.round(parseFloat(render(text)) * 100) / 100}`; }, }; - -const orderedPotentialIdProperties = [ - "id", - "ID", - "Id", - "OBJECTID", - "FID", - "fid", - "OBJECT_ID", - "ISO_SOV1", -]; - -function getIdProperty(properties: GeoJsonProperties) { - if (properties === null) { - return undefined; - } - let idProperty: string | undefined; - for (const potentialIdProperty of orderedPotentialIdProperties) { - if (properties[potentialIdProperty]) { - idProperty = potentialIdProperty; - break; - } - } - return idProperty; -} diff --git a/packages/client/src/dataLayers/MapContextManager.ts b/packages/client/src/dataLayers/MapContextManager.ts index ea5e68fd..cf961e86 100644 --- a/packages/client/src/dataLayers/MapContextManager.ts +++ b/packages/client/src/dataLayers/MapContextManager.ts @@ -12,8 +12,6 @@ import mapboxgl, { AnyLayer, Sources, GeoJSONSource, - FilterOptions, - LineLayer, } from "mapbox-gl"; import { createContext, @@ -30,6 +28,7 @@ import { DataLayerDetailsFragment, DataSourceDetailsFragment, DataSourceTypes, + InteractivityType, MapBookmarkDetailsFragment, OptionalBasemapLayer, OptionalBasemapLayersGroupType, @@ -67,9 +66,7 @@ import { import { OrderedLayerSettings } from "@seasketch/mapbox-gl-esri-sources/dist/src/CustomGLSource"; import { isArcGISDynamicMapService } from "@seasketch/mapbox-gl-esri-sources/dist/src/ArcGISDynamicMapService"; import { isArcgisFeatureLayerSource } from "@seasketch/mapbox-gl-esri-sources/dist/src/ArcGISFeatureLayerSource"; - -const CROSSHAIR_IMAGE = `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAYAAAA71pVKAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAEhJREFUKJHlkLENwEAMAiH7T+AMe2k+LX75ixRBokInbAxoqmtMdrDtink62zaAR82dvoMFaP1dkthwvcwfB+vgO4VxsNPmqAd8/ytkrmseVgAAAABJRU5ErkJggg==`; -export const CROSSHAIR_IMAGE_ID = "_ssn_crosshair"; +import { addInteractivityExpressions } from "../admin/data/glStyleUtils"; export const MeasureEventTypes = { Started: "measure_started", @@ -153,7 +150,6 @@ export interface LayerState { export interface SketchLayerState extends LayerState { sketchClassId?: number; } -export const POPUP_CLICK_LOCATION_SOURCE = "popup-click-location"; class MapContextManager extends EventEmitter { map?: Map; interactivityManager?: LayerInteractivityManager; @@ -417,8 +413,7 @@ class MapContextManager extends EventEmitter { this.interactivityManager = new LayerInteractivityManager( this.map, - this.setState, - this.setHighlightedLayer + this.setState ); this.interactivityManager.setVisibleLayers( @@ -440,13 +435,6 @@ class MapContextManager extends EventEmitter { this.map.on("moveend", this.onMapMove); this.map.on("styleimagemissing", this.onStyleImageMissing); this.map.on("load", () => { - this.map?.loadImage(CROSSHAIR_IMAGE, (error, image) => { - if (error) { - console.error(error); - } else if (image && this.map) { - this.map.addImage(CROSSHAIR_IMAGE_ID, image, { pixelRatio: 2 }); - } - }); this.mapIsLoaded = true; // Use to trigger changes to mapContextManager.map this.setState((prev) => ({ ...prev })); @@ -1275,21 +1263,6 @@ class MapContextManager extends EventEmitter { ); baseStyle.sources = baseStyle.sources || {}; - let existingPopupClickLocationSource = - existingStyle?.sources[POPUP_CLICK_LOCATION_SOURCE]; - if (existingPopupClickLocationSource) { - baseStyle.sources[POPUP_CLICK_LOCATION_SOURCE] = - existingPopupClickLocationSource as GeoJSONSource; - } else { - baseStyle.sources[POPUP_CLICK_LOCATION_SOURCE] = { - type: "geojson", - data: { - type: "FeatureCollection", - features: [], - }, - }; - } - baseStyle.layers = baseStyle.layers || []; if (labelsLayerIndex === -1) { labelsLayerIndex = baseStyle.layers.length; @@ -1453,7 +1426,19 @@ class MapContextManager extends EventEmitter { styleData.imageList.addToMap(this.map); } const layers = isUnderLabels ? underLabels : overLabels; - layers.push(...styleData.layers); + if ( + layer.interactivitySettings && + layer.interactivitySettings.type === + InteractivityType.SidebarOverlay + ) { + layers.push( + ...addInteractivityExpressions( + styleData.layers as AnyLayer[] + ) + ); + } else { + layers.push(...styleData.layers); + } } else { setTimeout(() => { this.debouncedUpdateStyle(); @@ -1480,27 +1465,28 @@ class MapContextManager extends EventEmitter { source.type === DataSourceTypes.SeasketchMvt) && layer.mapboxGlStyles?.length ) { - for (let i = 0; i < layer.mapboxGlStyles.length; i++) { - const layers = isUnderLabels ? underLabels : overLabels; - if ( - source.type === DataSourceTypes.SeasketchMvt || - source.type === DataSourceTypes.Vector || - source.type === DataSourceTypes.SeasketchRaster - ) { - layers.push({ - ...layer.mapboxGlStyles[i], - source: source.id.toString(), - id: idForLayer(layer, i), - "source-layer": layer.sourceLayer, - }); - } else { - layers.push({ - ...layer.mapboxGlStyles[i], - source: source.id.toString(), - id: idForLayer(layer, i), - }); - } + const shouldHaveSourceLayer = + source.type === DataSourceTypes.SeasketchMvt || + source.type === DataSourceTypes.Vector || + source.type === DataSourceTypes.SeasketchRaster; + let glLayers = (layer.mapboxGlStyles as any[]).map((lyr, i) => { + return { + ...lyr, + source: source.id.toString(), + id: idForLayer(layer, i), + ...(shouldHaveSourceLayer + ? { "source-layer": layer.sourceLayer } + : {}), + } as AnyLayer; + }); + const layers = isUnderLabels ? underLabels : overLabels; + if ( + layer.interactivitySettings?.type === + InteractivityType.SidebarOverlay + ) { + glLayers = addInteractivityExpressions(glLayers); } + layers.push(...glLayers); } else if (isCustomSourceType(source.type) && layer.sublayer) { // Add sublayer info if needed if (!Array.isArray(this.customSources[source.id].sublayers)) { @@ -1554,82 +1540,8 @@ class MapContextManager extends EventEmitter { ...overLabels, ...this.dynamicLayers, ...glDrawLayers, - { - id: "sidebar-popup-click-location", - type: "symbol", - source: POPUP_CLICK_LOCATION_SOURCE, - paint: { - "icon-translate-transition": { - duration: 0, - }, - "icon-opacity-transition": { - duration: 0, - delay: 0, - }, - }, - layout: { - "icon-image": CROSSHAIR_IMAGE_ID, - "icon-size": 2, - "icon-ignore-placement": true, - "icon-allow-overlap": true, - }, - }, ]; - // TODO: implement layer highlighting in a more general way - // console.log("highlighted layer", this.highlightedLayer); - // if (this.highlightedLayer?.layerId && this.highlightedLayer.filter) { - // // highlight a feature in this layer - // const layer = baseStyle.layers.find( - // (l) => l.id === this.highlightedLayer!.layerId - // ); - // if (layer) { - // switch (layer.type) { - // case "fill": - // case "line": - // // console.log("found layer to highlight", layer); - // const h = { - // id: "highlighted-feature", - // type: "line", - // source: layer.source, - // ...(layer["source-layer"] - // ? { "source-layer": layer["source-layer"] } - // : {}), - // // @ts-ignore - // filter: this.highlightedLayer.filter as FilterOptions, - // paint: { - // "line-color": "rgba(255, 255,255, 1.0)", - // "line-width": 1, - // // "line-gap-width": 2, - // "line-offset": -1, - // }, - // layout: { - // "line-join": "round", - // }, - // }; - // // console.log(h); - // baseStyle.layers.push(h as LineLayer); - // break; - // case "symbol": - // // in the original layer, set the visibility to none where the - // // filter matches, then - // // layer.layout = { - // // ...layer.layout, - // // // @ts-ignore - // // visibility: [ - // // this.highlightedLayer.filter as FilterOptions, - // // "none", - // // layer.layout?.visibility || "visible", - // // ], - // // }; - // // console.log(layer); - // break; - // default: { - // } - // } - // } - // } - // Evaluate any basemap optional layers // value is whether to toggle // const stylesSubjectToToggle: { [id: string]: boolean } = {}; @@ -2035,21 +1947,6 @@ class MapContextManager extends EventEmitter { maxWait: 100, }); - private highlightedLayer: { layerId: string; filter?: FilterOptions } | null = - null; - - setHighlightedLayer = ( - layerId: string | undefined, - filter?: FilterOptions - ) => { - if (!layerId) { - this.highlightedLayer = null; - } else { - this.highlightedLayer = { layerId, filter }; - } - this.debouncedUpdateStyle(); - }; - private tocItemLabels: { [id: string]: string } = {}; reset( diff --git a/packages/client/src/dataLayers/legends/utils.ts b/packages/client/src/dataLayers/legends/utils.ts index 42ff4169..10293920 100644 --- a/packages/client/src/dataLayers/legends/utils.ts +++ b/packages/client/src/dataLayers/legends/utils.ts @@ -179,28 +179,29 @@ export function findGetExpression( if (!isExpression(expression)) { return null; } - if (isFilter && !parent) { - // check for legacy filter type - if ( - typeof expression[1] === "string" && - !/\$/.test(expression[1]) && - expression[1] !== "zoom" - ) { - return { - type: "legacy", - property: expression[1], - }; - } - } if (expression[0] === "get") { return { type: "get", property: expression[1] }; } else { - for (const arg of expression.slice(1)) { - if (isExpression(arg)) { - const found = findGetExpression(arg, isFilter, expression); - if (found !== null) { - return found; - } + if (isFilter) { + // check for legacy filter type + if ( + typeof expression[1] === "string" && + !/\$/.test(expression[1]) && + expression[1] !== "zoom" + ) { + return { + type: "legacy", + property: expression[1], + }; + } + } + } + + for (const arg of expression.slice(1)) { + if (isExpression(arg)) { + const found = findGetExpression(arg, isFilter, expression); + if (found !== null) { + return found; } } }