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,
};