From 884922066c1857d487864256a560cca69a17bf09 Mon Sep 17 00:00:00 2001 From: "S. Andrew Sheppard" Date: Fri, 9 Feb 2024 21:18:05 +0000 Subject: [PATCH] update VectorTile to support wq.db's MVT server --- .../map-gl-web/src/overlays/VectorTile.js | 27 ++- packages/map/src/components/AutoMap.js | 36 +++- packages/map/src/components/MapLayers.js | 3 + packages/map/src/components/OverlayToggle.js | 4 +- packages/map/src/hooks.js | 174 ++++++++++++++++++ packages/map/src/index.js | 9 +- packages/map/src/reducer.js | 3 + packages/map/src/views/DefaultDetail.js | 9 +- packages/map/src/views/DefaultPopup.js | 68 ++++--- 9 files changed, 282 insertions(+), 51 deletions(-) diff --git a/packages/map-gl-web/src/overlays/VectorTile.js b/packages/map-gl-web/src/overlays/VectorTile.js index 8f0d5f7a..ff3204a9 100644 --- a/packages/map-gl-web/src/overlays/VectorTile.js +++ b/packages/map-gl-web/src/overlays/VectorTile.js @@ -1,5 +1,6 @@ import React, { useMemo, useState, useEffect } from "react"; import PropTypes from "prop-types"; +import { useStyleProp } from "@wq/map"; import { Source, Layer } from "react-map-gl"; function AutoLayer({ id, active, before, layout = {}, paint = {}, ...rest }) { @@ -15,19 +16,17 @@ function AutoLayer({ id, active, before, layout = {}, paint = {}, ...rest }) { return ; } -export default function VectorTile({ name, active, before, url, style }) { - if (url) { - return ( - - ); +export default function VectorTile(props) { + if (props.url) { + return ; + } else { + return ; } +} + +function StyleVectorTile(props) { + const { sources, layers } = useStyleProp(props); - const { sources, layers } = style; if (!sources || !layers) { return null; } @@ -40,8 +39,8 @@ export default function VectorTile({ name, active, before, url, style }) { {layers.map((layer) => ( ))} @@ -62,7 +61,7 @@ function UrlVectorTile({ name, active, before, url }) { if (style) { return ( - { + if (!tiles) { + return null; + } + const origin = tiles.startsWith("/") ? window.location.origin : ""; + return { + name: "Default Tile Source", + type: "vector-tile", + style: { + sources: { + _default: { + type: "vector", + tiles: [origin + tiles], + }, + }, + layers: [], + }, + }; + }, [tiles]); const identify = overlays.some((overlay) => !!overlay.popup); @@ -82,6 +109,9 @@ export default function AutoMap({ )} + {defaultTileSource && ( + + )} {basemaps.map((conf) => ( ))} diff --git a/packages/map/src/components/MapLayers.js b/packages/map/src/components/MapLayers.js index beae29cf..2732fb4b 100644 --- a/packages/map/src/components/MapLayers.js +++ b/packages/map/src/components/MapLayers.js @@ -25,6 +25,9 @@ MapLayers.propTypes = { }; export function MapLayer({ element }) { + if (!element || !element.type) { + return null; + } const type = element.type.isAutoBasemap ? "Basemap" : "Overlay", { name, active } = element.props; return ( diff --git a/packages/map/src/components/OverlayToggle.js b/packages/map/src/components/OverlayToggle.js index b68b7a15..fea27d96 100644 --- a/packages/map/src/components/OverlayToggle.js +++ b/packages/map/src/components/OverlayToggle.js @@ -18,9 +18,7 @@ export default function OverlayToggle({ name, legend, active, setActive }) { onValueChange={setActive} /> )} - description={ - active && legend ? () => : null - } + description={active && legend ? : null} > {name} diff --git a/packages/map/src/hooks.js b/packages/map/src/hooks.js index 3fb37e88..c94fbf09 100644 --- a/packages/map/src/hooks.js +++ b/packages/map/src/hooks.js @@ -8,6 +8,8 @@ import { useApp, useComponents, usePluginComponentMap, + useModel, + useReverse, } from "@wq/react"; import Mustache from "mustache"; @@ -168,6 +170,7 @@ export function routeMapConf(config, routeInfo, context = {}) { ...((conf[mode] || { maps: {} }).maps[mapname] || {}), basemaps: config.basemaps.map(checkGroupLayers), bounds: config.bounds, + tiles: config.tiles, }; if (config.mapProps) { @@ -576,3 +579,174 @@ export function useGeolocation() { }, }; } + +export function useStyleProp({ name, style, layer, color, icon }) { + return useMemo(() => { + if (!style && !layer) { + console.warn(`Specify style or layer for "${name}"`); + return { sources: {}, layers: [] }; + } + if (typeof layer === "string") { + layer = { id: layer, "source-layer": layer }; + } + if (style) { + return style; + } else if (icon) { + return { + sources: {}, + layers: makeSymbolLayers(layer, icon), + }; + } else if (color) { + return { + sources: {}, + layers: makeColorLayers(layer, color), + }; + } else { + return { + sources: {}, + layers: makeColorLayers(layer, "#3388ff", "#3086cc"), + }; + } + }, [name, style, layer, color, icon]); +} + +function makeSymbolLayers(layer, icon) { + const { id, ["source-layer"]: sourceLayer, ...rest } = layer; + return [ + { + id: id, + source: "_default", + "source-layer": sourceLayer || id, + type: "symbol", + layout: { + "icon-image": icon, + "icon-allow-overlap": true, + }, + ...rest, + }, + ]; +} + +function makeColorLayers(layer, color, pointColor = color) { + const { id, ["source-layer"]: sourceLayer, ...rest } = layer; + return [ + { + id: `${id}-fill`, + source: "_default", + "source-layer": sourceLayer || id, + type: "fill", + paint: { + "fill-color": color, + "fill-opacity": [ + "match", + ["geometry-type"], + ["Polygon", "MultiPolygon"], + 0.2, + 0, + ], + }, + ...rest, + }, + { + id: `${id}-line`, + source: "_default", + "source-layer": sourceLayer || id, + type: "line", + paint: { + "line-width": 3, + "line-color": color, + "line-opacity": 1, + }, + ...rest, + }, + { + id: `${id}-circle`, + source: "_default", + "source-layer": sourceLayer || id, + type: "circle", + paint: { + "circle-color": "white", + "circle-radius": [ + "match", + ["geometry-type"], + ["Point", "MultiPoint"], + 3, + 0, + ], + "circle-stroke-color": pointColor, + "circle-stroke-width": [ + "match", + ["geometry-type"], + ["Point", "MultiPoint"], + 3, + 0, + ], + "circle-opacity": [ + "match", + ["geometry-type"], + ["Point", "MultiPoint"], + 1, + 0, + ], + }, + ...rest, + }, + ]; +} + +export function useFeatureValues(feature, modelConf) { + const slug = feature.properties[modelConf.lookup] || feature.id, + form = modelConf.form || [{ name: "label" }], + emptyForm = makeEmptyForm(form), + app = useApp(), + modelData = useModel(modelConf.name, slug), + [fetchData, setFetchData] = useState({}); + + useEffect(() => { + loadData(); + async function loadData() { + const data = await app.models[modelConf.name].find(slug); + if (data) { + setFetchData(data); + } + } + }, [app, modelConf, slug]); + + return { + ...emptyForm, + ...feature.properties, + ...fetchData, + ...modelData, + }; +} + +function makeEmptyForm(form) { + const values = {}; + for (const field of form) { + if (field.name === "" && field.type === "group") { + Object.assign(values, makeEmptyForm(field.children)); + } else if (field.type === "group") { + values[field.name] = makeEmptyForm(field.children); + } else { + values[field.name] = "-"; + } + } + return values; +} + +export function useFeatureUrl(feature, modelConf, mode = "edit") { + const slug = feature.properties[modelConf.lookup] || feature.id, + reverse = useReverse(), + authState = usePluginState("auth"), + perms = + authState && + authState.config && + authState.config.pages && + authState.config.pages[modelConf.name]; + + if ((perms && perms.can_change) || mode !== "edit") { + return reverse(`${modelConf.name}_${mode}`, slug); + } else { + return null; + } +} diff --git a/packages/map/src/index.js b/packages/map/src/index.js index e98afcc8..1cc5fad6 100644 --- a/packages/map/src/index.js +++ b/packages/map/src/index.js @@ -12,6 +12,9 @@ import { asFeatureCollection, computeBounds, useGeolocation, + useStyleProp, + useFeatureValues, + useFeatureUrl, } from "./hooks.js"; import { AutoMap, @@ -27,7 +30,7 @@ import { } from "./components/index.js"; import { Geo } from "./inputs/index.js"; import { GeoHelp, GeoLocate, GeoCode, GeoCoords } from "./geotools/index.js"; -import { DefaultList, DefaultDetail } from "./views/index.js"; +import { DefaultList, DefaultDetail, DefaultPopup } from "./views/index.js"; export default map; @@ -44,6 +47,9 @@ export { asFeatureCollection, computeBounds, useGeolocation, + useStyleProp, + useFeatureValues, + useFeatureUrl, AutoMap, AutoBasemap, AutoOverlay, @@ -61,4 +67,5 @@ export { GeoCoords, DefaultList, DefaultDetail, + DefaultPopup, }; diff --git a/packages/map/src/reducer.js b/packages/map/src/reducer.js index ec9b0178..c76f2b76 100644 --- a/packages/map/src/reducer.js +++ b/packages/map/src/reducer.js @@ -57,6 +57,7 @@ export default function reducer(state = {}, action, config) { ), viewState, initBounds: conf.bounds, + tiles: conf.tiles, autoZoom: conf.autoZoom, mapProps: conf.mapProps, highlight: isSameView ? highlight : null, @@ -304,6 +305,7 @@ function reduceMapState(state) { overlays, viewState, initBounds, + tiles, autoZoom, mapProps, highlight, @@ -320,6 +322,7 @@ function reduceMapState(state) { overlays, viewState, initBounds, + tiles, autoZoom, mapProps, highlight, diff --git a/packages/map/src/views/DefaultDetail.js b/packages/map/src/views/DefaultDetail.js index 03c7c678..62e1f9ba 100644 --- a/packages/map/src/views/DefaultDetail.js +++ b/packages/map/src/views/DefaultDetail.js @@ -5,7 +5,8 @@ import { useMinWidth } from "@wq/material"; export default function DefaultDetailWithMap() { const mapState = useMapState(), - { MapProvider, AutoMap, Divider, TabGroup, TabItem } = useComponents(), + { MapProvider, AutoMap, HighlightPopup, Divider, TabGroup, TabItem } = + useComponents(), splitScreen = useMinWidth(900), context = useRenderContext(); if (mapState) { @@ -15,7 +16,10 @@ export default function DefaultDetailWithMap() { - + + + + ); } else { @@ -27,6 +31,7 @@ export default function DefaultDetailWithMap() { + diff --git a/packages/map/src/views/DefaultPopup.js b/packages/map/src/views/DefaultPopup.js index 31560faf..8d4defd2 100644 --- a/packages/map/src/views/DefaultPopup.js +++ b/packages/map/src/views/DefaultPopup.js @@ -1,51 +1,63 @@ import React from "react"; -import { - useComponents, - useReverse, - useConfig, - usePluginState, -} from "@wq/react"; +import { useComponents, useConfig } from "@wq/react"; +import { useFeatureValues, useFeatureUrl } from "../hooks.js"; import PropTypes from "prop-types"; -export default function DefaultPopup({ feature }) { - const reverse = useReverse(), - { PropertyTable, View, Fab } = useComponents(), - config = useConfig(), - authState = usePluginState("auth"), - page_config = feature.popup ? config.pages[feature.popup] : null, - perms = - page_config && - authState && - authState.config && - authState.config.pages && - authState.config.pages[feature.popup]; +export default function DefaultPopup({ feature, sx }) { + const config = useConfig(), + modelConf = feature.popup ? config.pages[feature.popup] : null, + { PropertyTable } = useComponents(); - let form, editUrl; - if (page_config) { - form = page_config.form || [{ name: "label" }]; - if (perms && perms.can_change) { - editUrl = reverse(`${feature.popup}_edit`, feature.id); - } + if (modelConf) { + return ( + + ); } else { - form = Object.keys(feature.properties).map((name) => ({ + const form = Object.keys(feature.properties).map((name) => ({ name, })); + return ( + + + + ); } +} + +function ModelFeaturePopup({ feature, modelConf, sx }) { + const values = useFeatureValues(feature, modelConf), + editUrl = useFeatureUrl(feature, modelConf, "edit"), + { PropertyTable } = useComponents(); + return ( + + + + ); +} + +export function FeaturePopup({ children, actionIcon = "edit", actionUrl, sx }) { + const { View, Fab } = useComponents(); return ( - - {editUrl && } + {children} + {actionUrl && } ); } DefaultPopup.propTypes = { feature: PropTypes.object, + sx: PropTypes.object, };