UNPKG

@availabs/avl-map

Version:

AVAILabs map library, using mapbox-gl

428 lines (355 loc) 11.3 kB
import mapboxgl from "mapbox-gl" import { hasValue } from "@availabs/avl-components" import DefaultHoverComp from "./components/DefaultHoverComp" import get from "lodash.get" let id = -1; const getLayerId = () => `avl-layer-${ ++id }`; const DefaultCallback = () => null; const DefaultOptions = { setActive: true, isDynamic: false, filters: {}, modals: {}, mapActions: [], sources: [], layers: [], isVisible: true, toolbar: ["toggleVisibility"], legend: null, infoBoxes: [], state: {}, mapboxMap: null, onHover: false, onClick: false, onBoxSelect: false } class LayerContainer { constructor(options = {}) { const Options = { ...DefaultOptions, ...options }; for (const key in Options) { this[key] = Options[key]; } this.id = getLayerId(); this.layerVisibility = {}; this.needsRender = this.setActive; this.callbacks = []; this.hoveredFeatures = new Map(); this.dispatchUpdate = () => {}; this.updateState = this.updateState.bind(this); } _init(mapboxMap, falcor) { this.mapboxMap = mapboxMap; this.falcor = falcor; return this.init(mapboxMap, falcor); } init(mapboxMap, falcor) { return Promise.resolve(); } // Don't attach any state directly to layers. // Use this function to update layer state. // This function causes an update in React through the map component. // The React update will cause rerenders in layer components. // Layer components, modals, infoboxes, etc., should pull from layer.state. updateState(newState) { if (typeof newState === "function") { this.state = newState(this.state); } else { this.state = { ...this.state, ...newState }; } this.dispatchUpdate(this, this.state); } _onAdd(mapboxMap, falcor, updateHover) { this.sources.forEach(({ id, source }) => { if (!mapboxMap.getSource(id)) { mapboxMap.addSource(id, source); } }); this.layers.forEach(layer => { if (!mapboxMap.getLayer(layer.id)) { mapboxMap.addLayer(layer, layer.beneath); if (!this.isVisible) { this._setVisibilityNone(mapboxMap, layer.id); } else { this.layerVisibility[layer.id] = mapboxMap.getLayoutProperty(layer.id, "visibility"); } } }); if (this.onHover) { this.addHover(mapboxMap, updateHover); } if (this.onClick) { this.addClick(mapboxMap); } if (this.onBoxSelect) { this.state = { ...this.state, selection: [] }; this.addBoxSelect(mapboxMap); } return this.onAdd(mapboxMap, falcor); } onAdd(mapboxMap, falcor) { return Promise.resolve(); } addClick(mapboxMap) { function click(layerId, { point, features, lngLat }) { this.onClick.callback.call(this, layerId, features, lngLat, point); }; this.onClick.layers.forEach(layerId => { const callback = click.bind(this, layerId); this.callbacks.push({ action: "click", callback, layerId }); mapboxMap.on("click", layerId, callback); }); } hoverLeave(mapboxMap, layerId) { if (!this.hoveredFeatures.has(layerId)) return; this.hoveredFeatures.get(layerId).forEach(value => { mapboxMap.setFeatureState(value, { hover: false }); }); this.hoveredFeatures.delete(layerId); } addHover(mapboxMap, updateHover) { const callback = get(this, ["onHover", "callback"], DefaultCallback).bind(this), HoverComp = get(this, ["onHover", "HoverComp"], DefaultHoverComp), property = get(this, ["onHover", "property"], null), filterFunc = get(this, ["onHover", "filterFunc"], null); const mousemove = (layerId, { point, features, lngLat }) => { const hoveredFeatures = this.hoveredFeatures.get(layerId) || new Map(); this.hoveredFeatures.set(layerId, new Map()); const hoverFeatures = features => { features.forEach(({ id, source, sourceLayer }) => { if ((id === undefined) || (id === null)) return; if (hoveredFeatures.has(id)) { this.hoveredFeatures.get(layerId).set(id, hoveredFeatures.get(id)); hoveredFeatures.delete(id); } else { const value = { id, source, sourceLayer }; this.hoveredFeatures.get(layerId).set(id, value); mapboxMap.setFeatureState(value, { hover: true }); } }); } if (property) { const properties = features.reduce((a, c) => { const prop = get(c, ["properties", property], null); if (prop) { a[prop] = true; } return a; }, {}) hoverFeatures( mapboxMap.queryRenderedFeatures({ layers: [layerId], filter: ["in", ["get", property], ["literal", Object.keys(properties)]] }) ); } if (filterFunc) { const filter = filterFunc.call(this, layerId, features, lngLat, point); if (filter) { hoverFeatures( mapboxMap.queryRenderedFeatures({ layers: [layerId], filter }) ); } } hoverFeatures(features); hoveredFeatures.forEach(value => { mapboxMap.setFeatureState(value, { hover: false }); }) const data = callback(layerId, features, lngLat, point); if (hasValue(data)) { updateHover({ pos: [point.x, point.y], type: "hover-layer-move", HoverComp, layer: this, lngLat, data }); } }; const mouseleave = (layerId, e) => { this.hoverLeave(mapboxMap, layerId); updateHover({ type: "hover-layer-leave", layer: this }); }; this.onHover.layers.forEach(layerId => { let callback = mousemove.bind(this, layerId); this.callbacks.push({ action: "mousemove", callback, layerId }); mapboxMap.on("mousemove", layerId, callback); callback = mouseleave.bind(this, layerId); this.callbacks.push({ action: "mouseleave", callback, layerId }); mapboxMap.on("mouseleave", layerId, callback); }, this); } addBoxSelect(mapboxMap) { let start, current, box; const canvasContainer = mapboxMap.getCanvasContainer(); const getPos = e => { const rect = canvasContainer.getBoundingClientRect(); return new mapboxgl.Point( e.clientX - rect.left - canvasContainer.clientLeft, e.clientY - rect.top - canvasContainer.clientTop ) } const mousemove = e => { e.preventDefault(); current = getPos(e); if (!box) { const className = get(this, ["onBoxSelect", "className"], "bg-black bg-opacity-50 border-2 border-black"); box = document.createElement("div"); box.className = "absolute top-0 left-0 w-0 h-0 " + className; canvasContainer.appendChild(box); } var minX = Math.min(start.x, current.x), maxX = Math.max(start.x, current.x), minY = Math.min(start.y, current.y), maxY = Math.max(start.y, current.y); box.style.transform = `translate( ${ minX }px, ${ minY }px)`; box.style.width = `${ maxX - minX }px`; box.style.height = `${ maxY - minY }px`; } const mouseup = e => { finish([start, getPos(e)]); } const keyup = e => { if ((e.keyCode === 27) || (e.which === 27) || (e.code === 'Escape')) { finish(); } } const finish = bbox => { document.removeEventListener('mousemove', mousemove); document.removeEventListener('mouseup', mouseup); document.removeEventListener('keydown', keyup); mapboxMap.dragPan.enable(); if (box) { box.parentNode.removeChild(box); box = null; } if (bbox) { const queriedFeatures = mapboxMap.queryRenderedFeatures(bbox, { layers: get(this, ["onBoxSelect", "layers"]), filter: get(this, ["onBoxSelect", "filter"]) }) const featureMap = queriedFeatures.reduce((a, c) => { a[c.id] = c; return a; }, {}); const features = Object.values(featureMap); const values = []; features.forEach(feature => { values.push({ id: feature.id, source: feature.source, sourceLayer: feature.sourceLayer }); }); get(this, ["onBoxSelect", "selectedValues"], []) .forEach(value => { mapboxMap.setFeatureState(value, { select: false }); }); this.onBoxSelect.selectedValues = values; values.forEach(value => { mapboxMap.setFeatureState(value, { select: true }); }); this.updateState({ selection: features }); } } const mousedown = e => { if (!(e.shiftKey && e.button === 0)) return; document.addEventListener('mousemove', mousemove); document.addEventListener('mouseup', mouseup); document.addEventListener('keydown', keyup); mapboxMap.dragPan.disable(); start = getPos(e); } this.callbacks.push({ action: "mousemove", callback: mousemove, element: canvasContainer }); canvasContainer.addEventListener("mousedown", mousedown, true); mapboxMap.boxZoom.disable(); this.onBoxSelect.selectedValues = []; } _onRemove(mapboxMap) { while (this.callbacks.length) { const { action, layerId, callback, element } = this.callbacks.pop(); if (element) { element.removeEventListener(action, callback); } else { mapboxMap.off(action, layerId, callback); } } this.layers.forEach(({ id }) => { mapboxMap.removeLayer(id); }); this.onRemove(mapboxMap); } onRemove(mapboxMap) { } fetchData(falcor) { return Promise.resolve(); } render(mapboxMap, falcor) { } receiveProps(props, mapboxMap, falcor) { } toggleVisibility(mapboxMap) { this.isVisible = !this.isVisible; this.layers.forEach(({ id }) => { if (this.isVisible) { this._setVisibilityVisible(mapboxMap, id); } else { this._setVisibilityNone(mapboxMap, id); } }); } _setVisibilityVisible(mapboxMap, layerId) { if (this.layerVisibility[layerId] !== "none") { mapboxMap.setLayoutProperty(layerId, "visibility", "visible"); } } _setVisibilityNone(mapboxMap, layerId) { const visibility = mapboxMap.getLayoutProperty(layerId, "visibility"); if (visibility === "none") { this.layerVisibility[layerId] = "none"; } else { mapboxMap.setLayoutProperty(layerId, "visibility", "none"); } } setLayerVisibility(mapboxMap, layer, visibility) { const isVisible = this.isVisible && (visibility === "visible"); this.layerVisibility[layer.id] = visibility; visibility = isVisible ? "visible" : "none"; mapboxMap.setLayoutProperty(layer.id, "visibility", visibility); } onFilterChange(filterName, newValue, prevValue) { } onMapStyleChange(mapboxMap, falcor, updateHover) { this._onAdd(mapboxMap, falcor, updateHover) .then(() => this.render(mapboxMap, falcor)) } } export { LayerContainer };