From 2f847c0ae6ce1872adfcb35760cc64f520b674d8 Mon Sep 17 00:00:00 2001 From: tariqksoliman Date: Tue, 4 Jun 2024 18:05:15 -0700 Subject: [PATCH] #464 Home page among many other things --- API/Backend/Config/uuids.js | 8 +- configure/src/components/Main/Main.js | 4 + .../Modals/NewMissionModal/NewMissionModal.js | 6 +- configure/src/components/Panel/Panel.js | 11 +- .../Modals/PreviewModal/PreviewModal.js | 8 +- configure/src/components/Tabs/Home/Home.js | 167 +- .../CloneConfigModal/CloneConfigModal.js | 321 ++++ .../DeleteConfigModal/DeleteConfigModal.js} | 123 +- .../UploadConfigModal/UploadConfigModal.js} | 168 +- .../src/components/Tabs/Home/Versions.js | 493 +++++ .../src/components/Tabs/Layers/Layers.js | 48 +- .../Layers/Modals/LayerModal/LayerModal.js | 118 +- .../Tabs/Tools/Modals/ToolModal/ToolModal.js | 3 +- configure/src/core/ConfigureStore.js | 18 +- configure/src/core/Maker.js | 2 +- configure/src/core/utils.js | 8 +- .../src/metaconfigs/layer-model-config.json | 1580 +++++++++++++++- .../src/metaconfigs/layer-vector-config.json | 25 + .../metaconfigs/layer-vectortile-config.json | 1637 ++++++++++++++++- configure/src/metaconfigs/meta-config.json | 0 configure/src/pages/Datasets/Datasets.js | 36 +- .../AppendGeoDatasetModal.js | 1 - .../DeleteGeoDatasetModal.js | 1 - .../Modals/NewDatasetModal/NewDatasetModal.js | 1 - .../UpdateGeoDatasetModal.js | 1 - .../src/pages/GeoDatasets/GeoDatasets.js | 36 +- .../AppendGeoDatasetModal.js | 1 - .../DeleteGeoDatasetModal.js | 1 - .../NewGeoDatasetModal/NewGeoDatasetModal.js | 1 - .../UpdateGeoDatasetModal.js | 1 - .../AppendGeoDatasetModal.js | 368 ---- .../LayersUsedByModal/LayersUsedByModal.js | 228 --- .../NewGeoDatasetModal/NewGeoDatasetModal.js | 389 ---- .../PreviewGeoDatasetModal.js | 160 -- configure/src/pages/WebHooks/WebHooks.js | 681 +------ .../Basics/Layers_/LayerConstructors.js | 2 +- 36 files changed, 4615 insertions(+), 2041 deletions(-) create mode 100644 configure/src/components/Tabs/Home/Modals/CloneConfigModal/CloneConfigModal.js rename configure/src/{pages/WebHooks/Modals/DeleteGeoDatasetModal/DeleteGeoDatasetModal.js => components/Tabs/Home/Modals/DeleteConfigModal/DeleteConfigModal.js} (70%) rename configure/src/{pages/WebHooks/Modals/UpdateGeoDatasetModal/UpdateGeoDatasetModal.js => components/Tabs/Home/Modals/UploadConfigModal/UploadConfigModal.js} (62%) create mode 100644 configure/src/components/Tabs/Home/Versions.js delete mode 100644 configure/src/metaconfigs/meta-config.json delete mode 100644 configure/src/pages/WebHooks/Modals/AppendGeoDatasetModal/AppendGeoDatasetModal.js delete mode 100644 configure/src/pages/WebHooks/Modals/LayersUsedByModal/LayersUsedByModal.js delete mode 100644 configure/src/pages/WebHooks/Modals/NewGeoDatasetModal/NewGeoDatasetModal.js delete mode 100644 configure/src/pages/WebHooks/Modals/PreviewGeoDatasetModal/PreviewGeoDatasetModal.js diff --git a/API/Backend/Config/uuids.js b/API/Backend/Config/uuids.js index 0d761abf..87af4590 100644 --- a/API/Backend/Config/uuids.js +++ b/API/Backend/Config/uuids.js @@ -9,13 +9,17 @@ const populateUUIDs = (config) => { // Track of all of the previously defined UUIDs (i.e. ignore the UUIDs of the newly added layers) Utils.traverseLayers(config.layers, (layer) => { - if (layer.uuid != null && !layer.proposed_uuid) { + if ( + layer.uuid != null && + typeof layer.uuid !== "number" && + !layer.proposed_uuid + ) { definedUUIDs.push(layer.uuid); } }); Utils.traverseLayers(config.layers, (layer) => { - if (layer.uuid == null) { + if (layer.uuid == null || typeof layer.uuid === "number") { layer.uuid = uuidv4(); newlyAddedUUIDs.push({ name: layer.name, diff --git a/configure/src/components/Main/Main.js b/configure/src/components/Main/Main.js index 06246844..8db523f0 100644 --- a/configure/src/components/Main/Main.js +++ b/configure/src/components/Main/Main.js @@ -37,6 +37,7 @@ import UserInterface from "../Tabs/UserInterface/UserInterface"; import APITokens from "../../pages/APITokens/APITokens"; import GeoDatasets from "../../pages/GeoDatasets/GeoDatasets"; import Datasets from "../../pages/Datasets/Datasets"; +import WebHooks from "../../pages/WebHooks/WebHooks"; const useStyles = makeStyles((theme) => ({ Main: { @@ -163,6 +164,9 @@ export default function Main() { case "api_tokens": Page = ; break; + case "webhooks": + Page = ; + break; default: } diff --git a/configure/src/components/Panel/Modals/NewMissionModal/NewMissionModal.js b/configure/src/components/Panel/Modals/NewMissionModal/NewMissionModal.js index 1a3cbb8a..bb42b56c 100644 --- a/configure/src/components/Panel/Modals/NewMissionModal/NewMissionModal.js +++ b/configure/src/components/Panel/Modals/NewMissionModal/NewMissionModal.js @@ -76,7 +76,6 @@ const useStyles = makeStyles((theme) => ({ subtitle: { fontSize: "14px !important", width: "100%", - textAlign: "right", marginBottom: "8px !important", color: theme.palette.swatches.grey[300], letterSpacing: "0.2px", @@ -129,7 +128,10 @@ const NewMissionModal = (props) => { calls.api( "add", - null, + { + mission: missionName, + makedir: createDir, + }, (res) => { calls.api( "missions", diff --git a/configure/src/components/Panel/Panel.js b/configure/src/components/Panel/Panel.js index 325c8b44..56a8a867 100644 --- a/configure/src/components/Panel/Panel.js +++ b/configure/src/components/Panel/Panel.js @@ -6,7 +6,12 @@ import mmgisLogo from "../../images/mmgis.png"; import clsx from "clsx"; -import { setMission, setModal, setPage } from "../../core/ConfigureStore"; +import { + setConfiguration, + setMission, + setModal, + setPage, +} from "../../core/ConfigureStore"; import NewMissionModal from "./Modals/NewMissionModal/NewMissionModal"; @@ -177,6 +182,7 @@ export default function Panel() { disableElevation startIcon={} onClick={() => { + dispatch(setMission(null)); dispatch(setPage({ page: "geodatasets" })); }} > @@ -188,6 +194,7 @@ export default function Panel() { disableElevation startIcon={} onClick={() => { + dispatch(setMission(null)); dispatch(setPage({ page: "datasets" })); }} > @@ -199,6 +206,7 @@ export default function Panel() { disableElevation startIcon={} onClick={() => { + dispatch(setMission(null)); dispatch(setPage({ page: "api_tokens" })); }} > @@ -210,6 +218,7 @@ export default function Panel() { disableElevation startIcon={} onClick={() => { + dispatch(setMission(null)); dispatch(setPage({ page: "webhooks" })); }} > diff --git a/configure/src/components/SaveBar/Modals/PreviewModal/PreviewModal.js b/configure/src/components/SaveBar/Modals/PreviewModal/PreviewModal.js index 323cd32f..22c6e3a6 100644 --- a/configure/src/components/SaveBar/Modals/PreviewModal/PreviewModal.js +++ b/configure/src/components/SaveBar/Modals/PreviewModal/PreviewModal.js @@ -154,7 +154,11 @@ const PreviewModal = (props) => {
-
Previewing Changes
+
+ {modal?.customConfig != null + ? `Previewing Configuration v${modal.version}` + : "Previewing Changes"} +
{
- + ); diff --git a/configure/src/components/Tabs/Home/Home.js b/configure/src/components/Tabs/Home/Home.js index 52613fb8..32398123 100644 --- a/configure/src/components/Tabs/Home/Home.js +++ b/configure/src/components/Tabs/Home/Home.js @@ -4,20 +4,73 @@ import { setVersions } from "./HomeSlice"; import { makeStyles } from "@mui/styles"; import { calls } from "../../../core/calls"; +import { downloadObject } from "../../../core/utils"; import Maker from "../../../core/Maker"; -import { setSnackBarText } from "../../../core/ConfigureStore"; +import { setSnackBarText, setModal } from "../../../core/ConfigureStore"; + +import Versions from "./Versions"; + +import IconButton from "@mui/material/IconButton"; +import Tooltip from "@mui/material/Tooltip"; + +import BrowserUpdatedIcon from "@mui/icons-material/BrowserUpdated"; +import ContentCopyIcon from "@mui/icons-material/ContentCopy"; +import UploadIcon from "@mui/icons-material/Upload"; +import DeleteForeverIcon from "@mui/icons-material/DeleteForever"; import config from "../../../metaconfigs/tab-home-config.json"; +import UploadConfigModal from "./Modals/UploadConfigModal/UploadConfigModal"; +import CloneConfigModal from "./Modals/CloneConfigModal/CloneConfigModal"; +import DeleteConfigModal from "./Modals/DeleteConfigModal/DeleteConfigModal"; const useStyles = makeStyles((theme) => ({ Home: { width: "100%", display: "flex", + flexFlow: "column", background: theme.palette.swatches.grey[1000], padding: "0px 32px 64px 32px", boxSizing: "border-box", backgroundImage: "url(configure/build/gridlines.png)", }, + top: { + display: "flex", + justifyContent: "space-between", + margin: "20px 0px 8px 0px", + }, + title: { + letterSpacing: "2px", + color: theme.palette.swatches.p[0], + textShadow: `0px 2px 1px ${theme.palette.swatches.grey[300]}`, + fontSize: "48px", + margin: 0, + }, + right: { + display: "flex", + }, + exportIcon: { + color: `${theme.palette.swatches.p[11]} !important`, + width: "40px", + height: "40px", + margin: "9px !important", + }, + uploadIcon: { + width: "40px", + height: "40px", + margin: "9px !important", + }, + cloneIcon: { + color: `${theme.palette.accent.main} !important`, + width: "40px", + height: "40px", + margin: "9px !important", + }, + deleteIcon: { + color: `${theme.palette.swatches.red[500]} !important`, + width: "40px", + height: "40px", + margin: "9px !important", + }, })); export default function Home() { @@ -25,39 +78,119 @@ export default function Home() { const dispatch = useDispatch(); const mission = useSelector((state) => state.core.mission); - const versions = useSelector((state) => state.home.versions); + const configuration = useSelector((state) => state.core.configuration); - useEffect(() => { + const queryVersions = () => { if (mission != null) calls.api( "versions", { mission: mission }, (res) => { + const v = res?.versions || []; + if (v.length > 0) v[v.length - 1].current = true; dispatch(setVersions(res?.versions || [])); }, (res) => { dispatch( setSnackBarText({ - text: res?.message || "Failed to get history for mission.", + text: + res?.message || "Failed to get the history for the mission.", severity: "error", }) ); } ); - }, [dispatch, mission]); + }; + + const handleExport = () => { + downloadObject(configuration, `${mission}_WORKING_config`, ".json"); + dispatch( + setSnackBarText({ + text: "Successfully exported working Configuration JSON.", + severity: "success", + }) + ); + }; + const handleUpload = () => { + dispatch( + setModal({ + name: "uploadConfig", + }) + ); + }; + const handleClone = () => { + dispatch( + setModal({ + name: "cloneConfig", + }) + ); + }; + const handleDelete = () => { + dispatch( + setModal({ + name: "deleteConfig", + }) + ); + }; return ( -
-
    - {versions.map((v) => { - return ( -
  • - {v.version} - {v.createdAt} -
  • - ); - })} -
- -
+ <> +
+
+

{mission}

+
+ + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + + + ); } diff --git a/configure/src/components/Tabs/Home/Modals/CloneConfigModal/CloneConfigModal.js b/configure/src/components/Tabs/Home/Modals/CloneConfigModal/CloneConfigModal.js new file mode 100644 index 00000000..ec3f282d --- /dev/null +++ b/configure/src/components/Tabs/Home/Modals/CloneConfigModal/CloneConfigModal.js @@ -0,0 +1,321 @@ +import React, { useState } from "react"; +import { useSelector, useDispatch } from "react-redux"; + +import { calls } from "../../../../../core/calls"; + +import { + setModal, + setSnackBarText, + setMissions, +} from "../../../../../core/ConfigureStore"; + +import Typography from "@mui/material/Typography"; +import Button from "@mui/material/Button"; +import Dialog from "@mui/material/Dialog"; +import DialogActions from "@mui/material/DialogActions"; +import DialogContent from "@mui/material/DialogContent"; +import DialogTitle from "@mui/material/DialogTitle"; +import IconButton from "@mui/material/IconButton"; +import FormGroup from "@mui/material/FormGroup"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import Checkbox from "@mui/material/Checkbox"; + +import CloseSharpIcon from "@mui/icons-material/CloseSharp"; +import ContentCopyIcon from "@mui/icons-material/ContentCopy"; + +import TextField from "@mui/material/TextField"; + +import { makeStyles, useTheme } from "@mui/styles"; +import useMediaQuery from "@mui/material/useMediaQuery"; + +const useStyles = makeStyles((theme) => ({ + Modal: { + margin: theme.headHeights[1], + [theme.breakpoints.down("xs")]: { + margin: "6px", + }, + "& .MuiDialog-container": { + height: "unset !important", + transform: "translateX(-50%) translateY(-50%)", + left: "50%", + top: "50%", + position: "absolute", + }, + }, + contents: { + background: theme.palette.primary.main, + height: "100%", + width: "500px", + }, + heading: { + height: theme.headHeights[2], + boxSizing: "border-box", + background: theme.palette.swatches.p[0], + borderBottom: `1px solid ${theme.palette.swatches.grey[800]}`, + padding: `4px ${theme.spacing(2)} 4px ${theme.spacing(4)} !important`, + }, + title: { + padding: `8px 0px`, + fontSize: theme.typography.pxToRem(16), + fontWeight: "bold", + color: theme.palette.swatches.grey[0], + textTransform: "uppercase", + }, + content: { + padding: "8px 16px 16px 16px !important", + height: `calc(100% - ${theme.headHeights[2]}px)`, + }, + closeIcon: { + padding: theme.spacing(1.5), + height: "100%", + margin: "4px 0px", + }, + flexBetween: { + display: "flex", + justifyContent: "space-between", + }, + subtitle: { + fontSize: "14px !important", + width: "100%", + marginBottom: "8px !important", + color: theme.palette.swatches.grey[300], + letterSpacing: "0.2px", + }, + subtitle2: { + fontSize: "12px !important", + fontStyle: "italic", + width: "100%", + marginBottom: "8px !important", + color: theme.palette.swatches.grey[400], + }, + confirmInput: { + width: "100%", + margin: "10px 0px 4px 0px !important", + borderTop: `1px solid ${theme.palette.swatches.grey[500]}`, + }, + backgroundIcon: { + margin: "7px 8px 0px 0px", + }, + + layerName: { + textAlign: "center", + fontSize: "24px !important", + letterSpacing: "1px !important", + color: theme.palette.swatches.grey[150], + fontWeight: "bold !important", + margin: "10px !important", + borderBottom: `1px solid ${theme.palette.swatches.grey[100]}`, + paddingBottom: "10px", + }, + hasOccurrencesTitle: { + margin: "10px", + display: "flex", + }, + hasOccurrences: { + fontStyle: "italic", + }, + mission: { + background: theme.palette.swatches.p[11], + color: theme.palette.swatches.grey[900], + height: "24px", + lineHeight: "24px", + padding: "0px 5px", + borderRadius: "3px", + display: "inline-block", + letterSpacing: "1px", + marginLeft: "20px", + }, + pathName: { + display: "flex", + marginLeft: "40px", + marginTop: "4px", + height: "24px", + lineHeight: "24px", + }, + path: { + color: theme.palette.swatches.grey[500], + }, + name: { + color: theme.palette.swatches.grey[100], + fontWeight: "bold", + }, + confirmMessage: { + fontStyle: "italic", + fontSize: "15px !important", + }, + dialogActions: { + display: "flex !important", + justifyContent: "space-between !important", + }, + submit: { + background: `${theme.palette.swatches.p[0]} !important`, + color: `${theme.palette.swatches.grey[1000]} !important`, + "&:hover": { + background: `${theme.palette.swatches.grey[0]} !important`, + }, + }, + cancel: {}, +})); + +const MODAL_NAME = "cloneConfig"; +const CloneConfigModal = (props) => { + const { queryGeoDatasets } = props; + const c = useStyles(); + + const modal = useSelector((state) => state.core.modal[MODAL_NAME]); + const mission = useSelector((state) => state.core.mission); + const missions = useSelector((state) => state.core.missions); + + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("md")); + + const dispatch = useDispatch(); + + const [newMissionName, setNewMissionName] = useState(null); + const [hasPaths, setHasPaths] = useState(false); + + const handleClose = () => { + // close modal + dispatch(setModal({ name: MODAL_NAME, on: false })); + }; + const handleSubmit = () => { + if (newMissionName == "" || newMissionName == null) { + dispatch( + setSnackBarText({ + text: "A new mission name to clone into needs to be specified.", + severity: "error", + }) + ); + return; + } + + for (let i = 0; i < missions.length; i++) { + if (newMissionName.toLowerCase() === missions[i].toLowerCase()) { + dispatch( + setSnackBarText({ + text: "Must clone into a new mission name!", + severity: "error", + }) + ); + return; + } + } + + calls.api( + "clone", + { + existingMission: mission, + cloneMission: newMissionName, + hasPaths: hasPaths, + }, + (res) => { + dispatch( + setSnackBarText({ + text: `Successfully cloned this mission into '${newMissionName}'.`, + severity: "success", + }) + ); + calls.api( + "missions", + null, + (res) => { + dispatch(setMissions(res.missions)); + }, + (res) => { + dispatch( + setSnackBarText({ + text: res?.message || "Failed to get available missions.", + severity: "error", + }) + ); + } + ); + handleClose(); + }, + (res) => { + dispatch( + setSnackBarText({ + text: `Failed to clone this mission.`, + severity: "error", + }) + ); + } + ); + }; + + return ( + + +
+
+ +
Clone a Mission
+
+ + + +
+
+ + {`Cloning: ${mission}`} + { + setNewMissionName(e.target.value); + }} + /> + {`Enter a new mission name above and click 'Clone' to clone this mission.`} + + { + setHasPaths(e.target.checked); + }} + /> + } + label={"Adjust Paths"} + /> + + {`Adjust new paths so that they still point to the same data (../{mission})`} + + + + + +
+ ); +}; + +export default CloneConfigModal; diff --git a/configure/src/pages/WebHooks/Modals/DeleteGeoDatasetModal/DeleteGeoDatasetModal.js b/configure/src/components/Tabs/Home/Modals/DeleteConfigModal/DeleteConfigModal.js similarity index 70% rename from configure/src/pages/WebHooks/Modals/DeleteGeoDatasetModal/DeleteGeoDatasetModal.js rename to configure/src/components/Tabs/Home/Modals/DeleteConfigModal/DeleteConfigModal.js index e77ed875..276e0b2e 100644 --- a/configure/src/pages/WebHooks/Modals/DeleteGeoDatasetModal/DeleteGeoDatasetModal.js +++ b/configure/src/components/Tabs/Home/Modals/DeleteConfigModal/DeleteConfigModal.js @@ -1,9 +1,14 @@ import React, { useState } from "react"; import { useSelector, useDispatch } from "react-redux"; -import { calls } from "../../../../core/calls"; +import { calls } from "../../../../../core/calls"; -import { setModal, setSnackBarText } from "../../../../core/ConfigureStore"; +import { + setModal, + setMission, + setMissions, + setSnackBarText, +} from "../../../../../core/ConfigureStore"; import Typography from "@mui/material/Typography"; import Button from "@mui/material/Button"; @@ -72,7 +77,6 @@ const useStyles = makeStyles((theme) => ({ subtitle: { fontSize: "14px !important", width: "100%", - textAlign: "right", marginBottom: "8px !important", color: theme.palette.swatches.grey[300], letterSpacing: "0.2px", @@ -162,40 +166,40 @@ const useStyles = makeStyles((theme) => ({ cancel: {}, })); -const MODAL_NAME = "deleteGeoDataset"; -const DeleteGeoDatasetModal = (props) => { +const MODAL_NAME = "deleteConfig"; +const DeleteConfigModal = (props) => { const { queryGeoDatasets } = props; const c = useStyles(); const modal = useSelector((state) => state.core.modal[MODAL_NAME]); - const geodatasets = useSelector((state) => state.core.geodatasets); + const mission = useSelector((state) => state.core.mission); const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down("md")); const dispatch = useDispatch(); - const [geoDatasetName, setGeoDatasetName] = useState(null); + const [missionName, setMissionName] = useState(null); const handleClose = () => { // close modal dispatch(setModal({ name: MODAL_NAME, on: false })); }; const handleSubmit = () => { - if (!modal?.geoDataset?.name) { + if (!mission) { dispatch( setSnackBarText({ - text: "Cannot delete undefined GeoDataset.", + text: "Cannot delete undefined Mission.", severity: "error", }) ); return; } - if (geoDatasetName !== modal.geoDataset.name) { + if (missionName !== mission) { dispatch( setSnackBarText({ - text: "Confirmation GeoDataset name does not match.", + text: "Confirmation Mission name does not match.", severity: "error", }) ); @@ -203,35 +207,39 @@ const DeleteGeoDatasetModal = (props) => { } calls.api( - "geodatasets_remove", + "destroy", { - urlReplacements: { - name: modal.geoDataset.name, - }, + mission: mission, }, (res) => { - if (res.status === "success") { - dispatch( - setSnackBarText({ - text: `Successfully deleted the '${modal.geoDataset.name}' GeoDataset.`, - severity: "success", - }) - ); - queryGeoDatasets(); - handleClose(); - } else { - dispatch( - setSnackBarText({ - text: `Failed to delete the '${modal.geoDataset.name}' GeoDataset.`, - severity: "error", - }) - ); - } + dispatch( + setSnackBarText({ + text: `Successfully deleted the '${mission}' Mission.`, + severity: "success", + }) + ); + dispatch(setMission(null)); + calls.api( + "missions", + null, + (res) => { + dispatch(setMissions(res.missions)); + }, + (res) => { + dispatch( + setSnackBarText({ + text: res?.message || "Failed to get available missions.", + severity: "error", + }) + ); + } + ); + handleClose(); }, (res) => { dispatch( setSnackBarText({ - text: `Failed to delete the '${modal.geoDataset.name}' GeoDataset.`, + text: `Failed to delete the '${mission}' Mission.`, severity: "error", }) ); @@ -239,30 +247,6 @@ const DeleteGeoDatasetModal = (props) => { ); }; - let occurrences = []; - - if (modal?.geoDataset?.occurrences) - occurrences = Object.keys(modal?.geoDataset?.occurrences) - .map((mission) => { - const m = modal?.geoDataset?.occurrences[mission]; - if (m.length == 0) return null; - else { - const items = [
{mission}
]; - m.forEach((n) => { - items.push( -
-
- {`${n.path}.`.replaceAll(".", " ➔ ")} -
-
{n.name}
-
- ); - }); - return items; - } - }) - .filter(Boolean); - return ( {
-
Delete a GeoDataset
+
Delete a Mission
{ {`Deleting: ${modal?.geoDataset?.name}`} - {occurrences.length > 0 && ( - <> -
- - - {`This GeoDataset is currently in use in the following layers:`} - -
-
{occurrences}
- - )} + >{`Deleting: ${mission}`} { - setGeoDatasetName(e.target.value); + setMissionName(e.target.value); }} /> {`Enter '${modal?.geoDataset?.name}' above and click 'Delete' to confirm the permanent deletion of this GeoDataset.`} + >{`Enter '${mission}' above and click 'Delete' to confirm the permanent deletion of this Mission. None of the mission's data files in /Missions will be deleted.`}
); }; -export default UpdateGeoDatasetModal; +export default UploadConfigModal; diff --git a/configure/src/components/Tabs/Home/Versions.js b/configure/src/components/Tabs/Home/Versions.js new file mode 100644 index 00000000..0eb464b9 --- /dev/null +++ b/configure/src/components/Tabs/Home/Versions.js @@ -0,0 +1,493 @@ +import React, { useEffect, useState } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { makeStyles } from "@mui/styles"; + +import clsx from "clsx"; + +import { calls } from "../../../core/calls"; +import { downloadObject } from "../../../core/utils"; +import { + setSnackBarText, + setModal, + setConfiguration, + clearLockConfig, +} from "../../../core/ConfigureStore"; +import { setVersions } from "./HomeSlice"; + +import PropTypes from "prop-types"; +import { alpha } from "@mui/material/styles"; +import Box from "@mui/material/Box"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableHead from "@mui/material/TableHead"; +import TablePagination from "@mui/material/TablePagination"; +import TableRow from "@mui/material/TableRow"; +import TableSortLabel from "@mui/material/TableSortLabel"; +import Toolbar from "@mui/material/Toolbar"; +import Typography from "@mui/material/Typography"; +import Paper from "@mui/material/Paper"; +import Checkbox from "@mui/material/Checkbox"; +import IconButton from "@mui/material/IconButton"; +import Button from "@mui/material/Button"; +import Tooltip from "@mui/material/Tooltip"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import Switch from "@mui/material/Switch"; +import DeleteIcon from "@mui/icons-material/Delete"; +import FilterListIcon from "@mui/icons-material/FilterList"; +import Divider from "@mui/material/Divider"; +import Badge from "@mui/material/Badge"; +import { visuallyHidden } from "@mui/utils"; + +import InventoryIcon from "@mui/icons-material/Inventory"; +import PreviewIcon from "@mui/icons-material/Preview"; +import DownloadIcon from "@mui/icons-material/Download"; +import UploadIcon from "@mui/icons-material/Upload"; +import DriveFileRenameOutlineIcon from "@mui/icons-material/DriveFileRenameOutline"; +import DeleteForeverIcon from "@mui/icons-material/DeleteForever"; +import AddIcon from "@mui/icons-material/Add"; +import ShapeLineIcon from "@mui/icons-material/ShapeLine"; +import LowPriorityIcon from "@mui/icons-material/LowPriority"; +import ControlPointDuplicateIcon from "@mui/icons-material/ControlPointDuplicate"; + +function descendingComparator(a, b, orderBy) { + if (b[orderBy] < a[orderBy]) { + return -1; + } + if (b[orderBy] > a[orderBy]) { + return 1; + } + return 0; +} + +function getComparator(order, orderBy) { + return order === "desc" + ? (a, b) => descendingComparator(a, b, orderBy) + : (a, b) => -descendingComparator(a, b, orderBy); +} + +// Since 2020 all major browsers ensure sort stability with Array.prototype.sort(). +// stableSort() brings sort stability to non-modern browsers (notably IE11). If you +// only support modern browsers you can replace stableSort(exampleArray, exampleComparator) +// with exampleArray.slice().sort(exampleComparator) +function stableSort(array, comparator) { + const stabilizedThis = array.map((el, index) => [el, index]); + stabilizedThis.sort((a, b) => { + const order = comparator(a[0], b[0]); + if (order !== 0) { + return order; + } + return a[1] - b[1]; + }); + return stabilizedThis.map((el) => el[0]); +} + +const useStyles = makeStyles((theme) => ({ + Versions: { width: "100%", height: "100%" }, + VersionsInner: { + width: "100%", + height: "100%", + display: "flex", + flexFlow: "column", + backgroundImage: "url(configure/build/gridlines.png)", + }, + table: { + flex: 1, + overflowY: "auto", + "& tr": { + background: theme.palette.swatches.grey[850], + }, + "& td": { + borderRight: `1px solid ${theme.palette.swatches.grey[800]}`, + borderBottom: `1px solid ${theme.palette.swatches.grey[700]} !important`, + }, + "& td:first-child": { + fontWeight: "bold", + letterSpacing: "1px", + fontSize: "16px", + color: `${theme.palette.swatches.p[13]}`, + }, + }, + tableInner: { + width: "100% !important", + boxShadow: "0px 1px 7px 0px rgba(0, 0, 0, 0.2)", + }, + flex: { + display: "flex", + "& > svg": { + margin: "3px 10px 0px 2px", + }, + }, + actions: { + display: "flex", + justifyContent: "right", + }, + previewIcon: { + width: "40px !important", + height: "40px !important", + }, + downloadIcon: { + marginRight: "4px !important", + width: "40px !important", + height: "40px !important", + }, + setIcon: { + marginLeft: "4px !important", + width: "40px !important", + height: "40px !important", + transform: "rotateZ(180deg)", + }, + th: { + fontWeight: "bold !important", + textTransform: "uppercase", + letterSpacing: "1px !important", + color: `${theme.palette.accent.main} !important`, + backgroundColor: `${theme.palette.swatches.grey[1000]} !important`, + borderRight: `1px solid ${theme.palette.swatches.grey[900]}`, + }, + bottomBar: { + background: theme.palette.swatches.grey[1000], + }, + versionCell: { + display: "flex", + }, + current: { + background: theme.palette.swatches.p[11], + borderRadius: "3px", + color: theme.palette.swatches.grey[1000], + margin: "0px 6px", + padding: "2px 6px", + fontSize: "12px", + textTransform: "uppercase", + lineHeight: "18px", + }, +})); + +const headCells = [ + { + id: "version", + label: "Version", + }, + { + id: "createdAt", + label: "Date", + }, + { + id: "actions", + label: "", + }, +]; + +function EnhancedTableHead(props) { + const { order, orderBy, rowCount, onRequestSort } = props; + const createSortHandler = (property) => (event) => { + onRequestSort(event, property); + }; + + const c = useStyles(); + + return ( + + + {headCells.map((headCell, idx) => ( + + + {headCell.label} + {orderBy === headCell.id ? ( + + {order === "desc" ? "sorted descending" : "sorted ascending"} + + ) : null} + + + ))} + + + ); +} + +EnhancedTableHead.propTypes = { + onRequestSort: PropTypes.func.isRequired, + onSelectAllClick: PropTypes.func.isRequired, + order: PropTypes.oneOf(["asc", "desc"]).isRequired, + orderBy: PropTypes.string.isRequired, + rowCount: PropTypes.number.isRequired, +}; + +export default function Versions(props) { + const { queryVersions } = props; + const [order, setOrder] = React.useState("desc"); + const [orderBy, setOrderBy] = React.useState("version"); + const [page, setPage] = React.useState(0); + const [rowsPerPage, setRowsPerPage] = React.useState(5); + + const c = useStyles(); + + const dispatch = useDispatch(); + const mission = useSelector((state) => state.core.mission); + const versions = useSelector((state) => state.home.versions); + + useEffect(() => { + queryVersions(); + }, [mission]); + + const handleRequestSort = (event, property) => { + const isAsc = orderBy === property && order === "asc"; + setOrder(isAsc ? "desc" : "asc"); + setOrderBy(property); + }; + + const handleChangePage = (event, newPage) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event) => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); + }; + + // Avoid a layout jump when reaching the last page with empty rows. + const emptyRows = + page > 0 ? Math.max(0, (1 + page) * rowsPerPage - versions.length) : 0; + + const visibleRows = React.useMemo( + () => + stableSort(versions, getComparator(order, orderBy)).slice( + page * rowsPerPage, + page * rowsPerPage + rowsPerPage + ), + [order, orderBy, page, rowsPerPage, versions] + ); + + return ( + <> + + + + + + + {visibleRows.map((row, index) => { + let numOccurrences = 0; + if (row.occurrences) { + Object.keys(row.occurrences).forEach((m) => { + numOccurrences += row.occurrences[m].length; + }); + } + + return ( + + +
+
{`v${row.version}`}
+ {row.current ? ( +
current
+ ) : null} +
+
+ + {row.createdAt + ? new Date(row.createdAt).toLocaleString() + : row.createdAt} + + +
+ + { + if (row.version) + calls.api( + "get", + { + mission: row.mission, + version: row.version, + id: window.configId, + }, + (res) => { + dispatch( + setModal({ + name: "preview", + customConfig: res, + version: row.version, + }) + ); + }, + (res) => { + dispatch( + setSnackBarText({ + text: "Failed to download Configuration JSON.", + severity: "error", + }) + ); + } + ); + }} + > + + + + + { + if (row.version) + calls.api( + "get", + { + mission: row.mission, + version: row.version, + id: window.configId, + }, + (res) => { + downloadObject( + res.config, + `${row.mission}_v${row.version}_config`, + ".json" + ); + dispatch( + setSnackBarText({ + text: "Successfully downloaded Configuration JSON.", + severity: "success", + }) + ); + }, + (res) => { + dispatch( + setSnackBarText({ + text: "Failed to download Configuration JSON.", + severity: "error", + }) + ); + } + ); + }} + > + + + + + + + + { + if (row.version) + calls.api( + "upsert", + { + mission: row.mission, + version: row.version, + id: window.configId, + }, + (res) => { + dispatch( + setSnackBarText({ + text: "Successfully set Configuration JSON to this version.", + severity: "success", + }) + ); + queryVersions(); + if (res.status === "success") + if (mission != null) + calls.api( + "get", + { mission: mission }, + (res) => { + dispatch(setConfiguration(res)); + dispatch(clearLockConfig({})); + }, + (res) => { + dispatch( + setSnackBarText({ + text: + res?.message || + "Failed to get configuration for mission.", + severity: "error", + }) + ); + } + ); + }, + (res) => { + dispatch( + setSnackBarText({ + text: "Failed to set Configuration JSON to this version.", + severity: "error", + }) + ); + } + ); + }} + > + + + +
+
+
+ ); + })} + {emptyRows > 0 && ( + + + + )} +
+
+
+ +
+
+ + ); +} diff --git a/configure/src/components/Tabs/Layers/Layers.js b/configure/src/components/Tabs/Layers/Layers.js index a82333e0..705bc6a9 100644 --- a/configure/src/components/Tabs/Layers/Layers.js +++ b/configure/src/components/Tabs/Layers/Layers.js @@ -21,6 +21,7 @@ import ListItemButton from "@mui/material/ListItemButton"; import ListItemIcon from "@mui/material/ListItemIcon"; import ListItemText from "@mui/material/ListItemText"; import IconButton from "@mui/material/IconButton"; +import Button from "@mui/material/Button"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; // Header import StorageIcon from "@mui/icons-material/Storage"; // Data @@ -29,6 +30,7 @@ import TravelExploreIcon from "@mui/icons-material/TravelExplore"; // Query import LanguageIcon from "@mui/icons-material/Language"; // Tile import GridViewIcon from "@mui/icons-material/GridView"; // Vector tile import ViewInArIcon from "@mui/icons-material/ViewInAr"; // Model +import AddIcon from "@mui/icons-material/Add"; import VisibilityIcon from "@mui/icons-material/Visibility"; import AccessTimeFilledIcon from "@mui/icons-material/AccessTimeFilled"; @@ -49,11 +51,11 @@ const useStyles = makeStyles((theme) => ({ }, verticalLines: { display: "flex", - height: "calc(100% - 72px)", + height: "calc(100% - 128px)", position: "absolute", top: "0px", left: "0px", - margin: "4px 20%", + margin: "32px 18%", "& > div": { width: `${INDENT_WIDTH - 1}px`, height: "100%", @@ -62,7 +64,7 @@ const useStyles = makeStyles((theme) => ({ }, layersList: { width: "100%", - margin: "0px 20%", + margin: "28px 18%", }, layersListItem: { height: "28px", @@ -131,6 +133,15 @@ const useStyles = makeStyles((theme) => ({ dragHandle: { padding: "4px", }, + addLayer: { + position: "absolute !important", + top: "32px", + right: "18%", + transform: "translateX(calc(100% + 5px))", + borderRadius: "3px !important", + backgroundColor: `${theme.palette.swatches.p[0]} !important`, + outline: "none !important", + }, })); let savedLayersConfiguration = ""; @@ -155,9 +166,7 @@ export default function Layers() { name: "layer", on: true, layerUUID: layer.uuid, - onClose: () => { - console.log("closed"); - }, + onClose: () => {}, }) ); }; @@ -252,9 +261,30 @@ export default function Layers() { dispatch(setConfiguration(nextConfiguration)); }; + console.log(flatLayers); + return ( <>
+
@@ -315,8 +345,10 @@ export default function Layers() { return ( {(provided, snapshot) => ( diff --git a/configure/src/components/Tabs/Layers/Modals/LayerModal/LayerModal.js b/configure/src/components/Tabs/Layers/Modals/LayerModal/LayerModal.js index b0cf8f98..ff0666a1 100644 --- a/configure/src/components/Tabs/Layers/Modals/LayerModal/LayerModal.js +++ b/configure/src/components/Tabs/Layers/Modals/LayerModal/LayerModal.js @@ -1,30 +1,28 @@ import React, { useState } from "react"; import { useSelector, useDispatch } from "react-redux"; -import { calls } from "../../../../../core/calls"; -import { getLayerByUUID } from "../../../../../core/utils"; +import { getLayerByUUID, traverseLayers } from "../../../../../core/utils"; import { - setMissions, setModal, setSnackBarText, + setConfiguration, } from "../../../../../core/ConfigureStore"; -import Typography from "@mui/material/Typography"; import Button from "@mui/material/Button"; import Dialog from "@mui/material/Dialog"; import DialogActions from "@mui/material/DialogActions"; import DialogContent from "@mui/material/DialogContent"; import DialogTitle from "@mui/material/DialogTitle"; +import Tooltip from "@mui/material/Tooltip"; import IconButton from "@mui/material/IconButton"; +import Divider from "@mui/material/Divider"; +import DeleteForeverIcon from "@mui/icons-material/DeleteForever"; import CloseSharpIcon from "@mui/icons-material/CloseSharp"; import LayersIcon from "@mui/icons-material/Layers"; -import TextField from "@mui/material/TextField"; -import FormGroup from "@mui/material/FormGroup"; -import FormControlLabel from "@mui/material/FormControlLabel"; -import Checkbox from "@mui/material/Checkbox"; +import ContentCopyIcon from "@mui/icons-material/ContentCopy"; import { makeStyles, useTheme } from "@mui/styles"; import useMediaQuery from "@mui/material/useMediaQuery"; @@ -89,7 +87,6 @@ const useStyles = makeStyles((theme) => ({ subtitle: { fontSize: "14px !important", width: "100%", - textAlign: "right", marginBottom: "8px !important", color: theme.palette.swatches.grey[300], letterSpacing: "0.2px", @@ -108,18 +105,43 @@ const useStyles = makeStyles((theme) => ({ backgroundIcon: { margin: "7px 8px 0px 0px", }, + dialogActions: { + display: "flex !important", + justifyContent: "space-between !important", + background: `${theme.palette.swatches.grey[150]} !important`, + padding: "8px 14px !important", + }, + removeButton: { + background: `${theme.palette.swatches.red[500]} !important`, + color: `${theme.palette.swatches.grey[1000]} !important`, + border: "none !important", + }, + actionsRight: { + display: "flex", + }, + cloneButton: { + color: `${theme.palette.swatches.grey[900]} !important`, + }, + divider: { + borderColor: `${theme.palette.swatches.grey[300]} !important`, + margin: "0px 10px !important", + }, + doneButton: { + background: `${theme.palette.swatches.p[0]} !important`, + color: `${theme.palette.swatches.grey[150]} !important`, + border: "none !important", + width: "100px", + }, })); const MODAL_NAME = "layer"; const LayerModal = (props) => { - const {} = props; const c = useStyles(); const modal = useSelector((state) => state.core.modal[MODAL_NAME]); const configuration = useSelector((state) => state.core.configuration); const layerUUID = modal && modal.layerUUID ? modal.layerUUID : null; - const layer = getLayerByUUID(configuration.layers, layerUUID) || {}; const theme = useTheme(); @@ -196,15 +218,71 @@ const LayerModal = (props) => { - - + +
+ +
+
+ + { + const nextConfiguration = JSON.parse( + JSON.stringify(configuration) + ); + const clonedLayer = JSON.parse(JSON.stringify(layer)); + window.newUUIDCount++; + const uuid = window.newUUIDCount; + clonedLayer.uuid = uuid; + nextConfiguration.layers.unshift(clonedLayer); + dispatch(setConfiguration(nextConfiguration)); + dispatch( + setSnackBarText({ + text: `Cloned '${layer.name}'.`, + severity: "success", + }) + ); + }} + > + + + + + + + +
); diff --git a/configure/src/components/Tabs/Tools/Modals/ToolModal/ToolModal.js b/configure/src/components/Tabs/Tools/Modals/ToolModal/ToolModal.js index 3962cbf1..bdee5389 100644 --- a/configure/src/components/Tabs/Tools/Modals/ToolModal/ToolModal.js +++ b/configure/src/components/Tabs/Tools/Modals/ToolModal/ToolModal.js @@ -87,7 +87,6 @@ const useStyles = makeStyles((theme) => ({ subtitle: { fontSize: "14px !important", width: "100%", - textAlign: "right", marginBottom: "8px !important", color: theme.palette.swatches.grey[300], letterSpacing: "0.2px", @@ -244,7 +243,7 @@ const ToolModal = (props) => { const nextConfiguration = JSON.parse( JSON.stringify(configuration) ); - if (tool != null) { + if (tool != null && tool.name != null) { updateToolInConfiguration( tool.name, nextConfiguration, diff --git a/configure/src/core/ConfigureStore.js b/configure/src/core/ConfigureStore.js index 1da24719..60ec2f97 100644 --- a/configure/src/core/ConfigureStore.js +++ b/configure/src/core/ConfigureStore.js @@ -2,14 +2,15 @@ import { createSlice } from "@reduxjs/toolkit"; import { calls } from "./calls"; -const configId = parseInt(Math.random() * 100000); +window.newUUIDCount = 0; +window.configId = parseInt(Math.random() * 100000); export const ConfigureStore = createSlice({ name: "core", initialState: { missions: [], mission: null, - configuration: "{}", + configuration: {}, toolConfiguration: {}, geodatasets: [], datasets: [], @@ -26,6 +27,9 @@ export const ConfigureStore = createSlice({ appendGeoDataset: false, updateGeoDataset: false, newDataset: false, + uploadConfig: false, + cloneConfig: false, + deleteConfig: false, }, snackBarText: false, lockConfig: false, @@ -149,12 +153,18 @@ export const ConfigureStore = createSlice({ } return; } + + let finalConfig = action.payload.configuration + ? JSON.parse(JSON.stringify(action.payload.configuration)) + : JSON.parse(JSON.stringify(state.configuration)); + if (finalConfig.temp) delete finalConfig.temp; + calls.api( "upsert", { mission: state.mission, - config: JSON.stringify(state.configuration), - id: configId, + config: JSON.stringify(finalConfig), + id: window.configId, }, (res) => { action.payload.cb("success", res); diff --git a/configure/src/core/Maker.js b/configure/src/core/Maker.js index a86fba59..15abd1b7 100644 --- a/configure/src/core/Maker.js +++ b/configure/src/core/Maker.js @@ -413,7 +413,7 @@ const getComponent = ( {com.name} - + { diff --git a/configure/src/core/utils.js b/configure/src/core/utils.js index c7a54299..5fad6be4 100644 --- a/configure/src/core/utils.js +++ b/configure/src/core/utils.js @@ -126,7 +126,11 @@ export const downloadObject = ( if (typeof exportObj === "string") { strung = exportObj; } else { - if (exportExt && (exportExt === ".json" || exportExt === ".geojson")) { + if ( + exportExt && + (exportExt === ".json" || exportExt === ".geojson") && + exportObj.features + ) { //pretty print geojson let features = []; for (let i = 0; i < exportObj.features.length; i++) @@ -135,7 +139,7 @@ export const downloadObject = ( exportObj.features = "__FEATURES_PLACEHOLDER__"; strung = JSON.stringify(exportObj, null, 2); strung = strung.replace('"__FEATURES_PLACEHOLDER__"', features); - } else strung = JSON.stringify(exportObj); + } else strung = JSON.stringify(exportObj, null, 2); } let fileName = exportName + (exportExt || ".json"); diff --git a/configure/src/metaconfigs/layer-model-config.json b/configure/src/metaconfigs/layer-model-config.json index 0967ef42..00f95758 100644 --- a/configure/src/metaconfigs/layer-model-config.json +++ b/configure/src/metaconfigs/layer-model-config.json @@ -1 +1,1579 @@ -{} +{ + "tabs": [ + { + "name": "Core", + "rows": [ + { + "name": "Core", + "components": [ + { + "field": "type", + "name": "Layer Type", + "description": "", + "type": "dropdown", + "width": 2, + "options": [ + "data", + "header", + "model", + "query", + "tile", + "vector", + "vectortile" + ] + }, + { + "field": "name", + "name": "Layer Name", + "description": "A display name for the layer.", + "type": "text", + "width": 6 + }, + { + "field": "visibility", + "name": "Initial Visibility", + "description": "Whether the layer is on initially.", + "type": "checkbox", + "width": 2, + "defaultChecked": false + } + ] + }, + { + "components": [ + { + "field": "url", + "name": "URL", + "description": "A file path that points to a .dae or .obj. If the path is relative, it will be relative to the mission’s directory.", + "type": "text", + "width": 12 + } + ] + }, + { + "components": [ + { + "field": "position.longitude", + "name": "Longitude", + "description": "(Required) The longitude in decimal degrees at which to place the model.", + "type": "number", + "min": -180, + "max": 180, + "width": 4 + }, + { + "field": "position.latitude", + "name": "Latitude", + "description": "(Required) The latitude in decimal degrees at which to place the model.", + "type": "number", + "min": -90, + "max": 90, + "width": 4 + }, + { + "field": "position.elevation", + "name": "Elevation", + "description": "(Required) The elevation in meters at which to place the model.", + "type": "number", + "width": 4 + } + ] + }, + { + "components": [ + { + "field": "rotation.x", + "name": "Rotation X (radians)", + "description": "(Optional) An x-axis rotation in radians to orient the model.", + "type": "number", + "width": 4 + }, + { + "field": "rotation.y", + "name": "Rotation Y (radians)", + "description": "(Optional) An y-axis rotation in radians to orient the model.", + "type": "number", + "width": 4 + }, + { + "field": "rotation.z", + "name": "Rotation Z (radians)", + "description": "(Optional) An z-axis rotation in radians to orient the model.", + "type": "number", + "width": 4 + } + ] + }, + { + "components": [ + { + "field": "scale", + "name": "Scale", + "description": "(Optional) A scaling factor to resize the model.", + "type": "number", + "width": 4 + } + ] + } + ] + }, + { + "name": "Style", + "rows": [ + { + "name": "Style", + "components": [ + { + "field": "style.color", + "name": "Color", + "description": "The border color of each feature. If the feature is a line, this field is the color of the line.", + "type": "colorpicker", + "width": 2 + }, + { + "new": true, + "field": "style.colorProp", + "name": "Color From Property", + "description": "Color but taken from each individual feature's property. For example 'color' or 'path.to.prop.in.properties.object'. Takes priority over the value in the 'Color' field.", + "type": "text", + "width": 4 + }, + { + "field": "style.opacity", + "name": "Opacity", + "description": "Stroke Opacity", + "type": "slider", + "min": 0, + "max": 1, + "step": 0.01, + "width": 2 + }, + { + "new": true, + "field": "style.opacityProp", + "name": "Opacity From Property", + "description": "Opacity but taken from each individual feature's property. For example 'opacity' or 'path.to.prop.in.properties.object'. Takes priority over the value in the 'Opacity' field.", + "type": "text", + "width": 4 + } + ] + }, + { + "components": [ + { + "field": "style.fillColor", + "name": "Fill Color", + "description": "Fill Color", + "type": "colorpicker", + "width": 2 + }, + { + "new": true, + "field": "style.fillColorProp", + "name": "Fill Color From Property", + "description": "Fill Color but taken from each individual feature's property. For example 'fill_color' or 'path.to.prop.in.properties.object'. Takes priority over the value in the 'Fill Color' field.", + "type": "text", + "width": 4 + }, + { + "field": "style.fillOpacity", + "name": "Fill Opacity", + "description": "Fill Opacity", + "type": "slider", + "min": 0, + "max": 1, + "step": 0.01, + "width": 2 + }, + { + "new": true, + "field": "style.fillOpacityProp", + "name": "Fill Opacity From Property", + "description": "Fill Opacity but taken from each individual feature's property. For example 'fill_opacity' or 'path.to.prop.in.properties.object'. Takes priority over the value in the 'Fill Opacity' field.", + "type": "text", + "width": 4 + } + ] + }, + { + "components": [ + { + "field": "style.weight", + "name": "Weight", + "description": "Stroke weight in pixels of point features.", + "type": "slider", + "min": 0, + "max": 16, + "step": 1, + "width": 2 + }, + { + "new": true, + "field": "style.weightProp", + "name": "Weight From Property", + "description": "Weight but taken from each individual feature's property. For example 'weight' or 'path.to.prop.in.properties.object'. Takes priority over the value in the 'Weight' field.", + "type": "text", + "width": 4 + }, + { + "field": "style.radius", + "name": "Radius", + "description": "Radius in pixels of point features.", + "type": "slider", + "min": 0, + "max": 24, + "step": 1, + "width": 2 + }, + { + "new": true, + "field": "style.radiusProp", + "name": "Radius From Property", + "description": "Radius but taken from each individual feature's property. For example 'radius' or 'path.to.prop.in.properties.object'. Takes priority over the value in the 'Radius' field.", + "type": "text", + "width": 4 + } + ] + }, + { + "components": [ + { + "field": "shape", + "name": "Shape", + "description": "A marker symbol for point features for this layer. When set to 'None', a simple circle is used. Marker icons and other attachments may override this setting.", + "type": "dropdown", + "width": 4, + "options": [ + "none", + "circle", + "directional-circle", + "triangle", + "triangle-flipped", + "square", + "diamond", + "pentagon", + "hexagon", + "star", + "plus", + "pin" + ] + } + ] + }, + { + "subname": "Marker Icons", + "subdescription": "Uses an icon image instead of an svg for all of the layer's point markers. If you're using this as a bearing marker, make sure the base icon is pointing north.", + "components": [ + { + "field": "variables.markerIcon.iconUrl", + "name": "Icon URL", + "description": "A URL to an image to use as a marker icon.", + "type": "text", + "width": 12 + } + ] + }, + { + "components": [ + { + "field": "variables.markerIcon.shadowUrl", + "name": "Shadow URL", + "description": "An optional URL to an image to use as a marker icon's shadow..", + "type": "text", + "width": 12 + } + ] + }, + { + "components": [ + { + "field": "variables.markerIcon.iconSize.0", + "name": "Icon X Size", + "description": "Pixel width of the icon's image.", + "type": "number", + "min": 0, + "step": 1, + "width": 3 + }, + { + "field": "variables.markerIcon.iconSize.1", + "name": "Icon Y Size", + "description": "Pixel height of the icon's image.", + "type": "number", + "min": 0, + "step": 1, + "width": 3 + }, + { + "field": "variables.markerIcon.shadowSize.0", + "name": "Shadow X Size", + "description": "Pixel width of the icon's shadow.", + "type": "number", + "min": 0, + "step": 1, + "width": 3 + }, + { + "field": "variables.markerIcon.shadowSize.1", + "name": "Shadow Y Size", + "description": "Pixel height of the icon's shadow.", + "type": "number", + "min": 0, + "step": 1, + "width": 3 + } + ] + }, + { + "components": [ + { + "field": "variables.markerIcon.iconAnchor.0", + "name": "Icon X Anchor", + "description": "X point of the icon which will correspond to marker's center location.", + "type": "number", + "width": 3 + }, + { + "field": "variables.markerIcon.iconAnchor.1", + "name": "Icon Y Anchor", + "description": "Y point of the icon which will correspond to marker's center location.", + "type": "number", + "width": 3 + }, + { + "field": "variables.markerIcon.shadowAnchor.0", + "name": "Shadow X Anchor", + "description": "X point of the shadow which will correspond to marker's center location.", + "type": "number", + "width": 3 + }, + { + "field": "variables.markerIcon.shadowAnchor.1", + "name": "Shadow Y Anchor", + "description": "Y point of the shadow which will correspond to marker's center location.", + "type": "number", + "width": 3 + } + ] + } + ] + }, + { + "name": "Time", + "rows": [ + { + "name": "Time", + "components": [ + { + "field": "time.enabled", + "name": "Time Enabled", + "description": "True if the layer is time enabled. URLs that contain {starttime} or {endtime} will be dynamically replaced by their set values when the layer is fetched. If true and a URL is set and Controlled is true, only the initial url query will be performed.", + "type": "switch", + "width": 3, + "defaultChecked": false + } + ] + }, + { + "components": [ + { + "field": "time.type", + "name": "Time Type", + "description": "When the time changes, whether the layer should requery the source or filter the layer locally based on feature properties.", + "type": "dropdown", + "width": 3, + "options": ["requery", "local"] + } + ] + }, + { + "components": [ + { + "field": "time.format", + "name": "Time Format", + "description": "The string format to be used in the URL for {starttime} and {endtime}. Uses D3 time format specifiers: https://github.com/d3/d3-time-format. Default: %Y-%m-%dT%H:%M:%SZ", + "type": "text", + "width": 6 + } + ] + }, + { + "components": [ + { + "field": "time.startProp", + "name": "Start Time Property Name", + "description": "Optional and only in use if Time Enabled = true and Time Type = Local. The starting time property path. Setting this is addition to Main Time Property Name casts the feature's time over a range instead of as a single point in time. Can use dot-notation for nested path. Can be a unix timestamp or an ISO time (end the ISO with a Z to designate that it should be treated as a UTC time).", + "type": "text", + "width": 6 + }, + { + "field": "time.endProp", + "name": "Main/End Time Property Name", + "description": "Required in Time Enabled = true and Time Type = Local. The main time property path. Can use dot-notation for nested path. Can be a unix timestamp or an ISO time (end the ISO with a Z to designate that it should be treated as a UTC time).", + "type": "text", + "width": 6 + } + ] + } + ] + }, + { + "name": "Legend", + "rows": [ + { + "name": "Legend", + "components": [ + { + "field": "variables.legend", + "name": "Legend", + "description": "Configures a legend for the layer. The Legend Tool renders symbologies and gradient scales for any properly configured layer that is on.", + "type": "objectarray", + "width": 12, + "object": [ + { + "field": "color", + "name": "Fill Color", + "description": "A color for the main fill of the symbol.", + "type": "colorpicker", + "width": 3 + }, + { + "field": "strokecolor", + "name": "Border Color", + "description": "A stroke/border color. Note that 'discreet' and 'continuous' shapes have no borders.", + "type": "colorpicker", + "width": 3 + }, + { + "field": "shape", + "name": "Shape", + "description": "The symbol for which to us for this legend entry. Discreet and continuous describe scales. These scales are broken into groups by a change in shape value. For instance, 'discreet, discreet, discreet, circle, discreet, discreet' represents a discreet scales of three colors, a circle and then a discreet scale of two colors.", + "type": "dropdown", + "width": 3, + "options": [ + "circle", + "square", + "rect", + "triangle", + "continuous", + "discreet" + ] + }, + { + "field": "value", + "name": "Label", + "description": "A label description for this legend entry.", + "type": "text", + "width": 3 + } + ] + } + ] + } + ] + }, + { + "name": "Interface", + "rows": [ + { + "name": "Interface", + "subname": "Hover Feature Label", + "subdescription": "The property key whose value should be the hover text of each feature. If left unset, the hover key and value will be the first one listed in the feature's properties.", + "components": [ + { + "new": true, + "field": "variables.useKeyAsName.0", + "name": "Property 1", + "description": "", + "type": "text", + "width": 2 + }, + { + "new": true, + "field": "variables.useKeyAsName.1", + "name": "Property 2", + "description": "", + "type": "text", + "width": 2 + }, + { + "new": true, + "field": "variables.useKeyAsName.2", + "name": "Property 3", + "description": "", + "type": "text", + "width": 2 + }, + { + "new": true, + "field": "variables.useKeyAsName.3", + "name": "Property 4", + "description": "", + "type": "text", + "width": 2 + }, + { + "new": true, + "field": "variables.useKeyAsName.4", + "name": "Property 5", + "description": "", + "type": "text", + "width": 2 + }, + { + "new": true, + "field": "variables.useKeyAsName.5", + "name": "Property 6", + "description": "", + "type": "text", + "width": 2 + } + ] + }, + { + "components": [ + { + "new": true, + "field": "variables.useKeyAsName.6", + "name": "Property 7", + "description": "", + "type": "text", + "width": 2 + }, + { + "new": true, + "field": "variables.useKeyAsName.7", + "name": "Property 8", + "description": "", + "type": "text", + "width": 2 + }, + { + "new": true, + "field": "variables.useKeyAsName.8", + "name": "Property 9", + "description": "", + "type": "text", + "width": 2 + }, + { + "new": true, + "field": "variables.useKeyAsName.9", + "name": "Property 10", + "description": "", + "type": "text", + "width": 2 + }, + { + "new": true, + "field": "variables.useKeyAsName.10", + "name": "Property 11", + "description": "", + "type": "text", + "width": 2 + }, + { + "new": true, + "field": "variables.useKeyAsName.11", + "name": "Property 12", + "description": "", + "type": "text", + "width": 2 + } + ] + }, + { + "subname": "Key Bindings", + "components": [ + { + "field": "variables.shortcutSuffix", + "name": "Alt + {letter} Toggle Shortcut", + "description": "A single letter to 'ALT + {letter}' toggle the layer on and off. Please verify that your chosen shortcut does not conflict with other system or browser-level keyboard shortcuts.", + "type": "text", + "width": 6 + } + ] + }, + { + "subname": "Search", + "components": [ + { + "field": "variables.search", + "name": "Search Construct", + "description": "When set, this layer will become searchable through the search bar at the top. The search will look for and autocomplete on the properties specified. All properties are enclosed by parentheses and space-separated. 'round()' can be used like a function to round the property beforehand. 'rmunder()' works similarly but removes all underscores instead. For example: '(RMC) rmunder(sol)'", + "type": "text", + "width": 12 + } + ] + }, + { + "subname": "Links", + "components": [ + { + "field": "variables.links", + "name": "External Links", + "description": "Configure deep links to other sites based on the properties on a selected feature. Upon clicking a feature, a list of deep links are put into the top bar and can be clicked on to navigate to any other page.", + "type": "objectarray", + "width": 12, + "object": [ + { + "field": "name", + "name": "Display Name", + "description": "The name of the deep link. It should be unique.", + "type": "text", + "width": 3 + }, + { + "field": "link", + "name": "URL", + "description": "A url template. Curly brackets are included. On feature click, all '{prop}' are replaced with the corresponding features[i].properties.prop value. Multiple '{prop}' are supported as an access to nested props using dot notation '{stores.food.candy}'.", + "type": "text", + "width": 9 + } + ] + } + ] + }, + { + "subname": "Information", + "components": [ + { + "field": "variables.info", + "name": "TopBar Information", + "description": "Creates an informational record at the top of the page. The first use case was showing the value of the latest sol. Clicking this record pans to the feature specified by 'which'. This is used on startup and not when a user selects a feature in this layer.", + "type": "objectarray", + "width": 12, + "object": [ + { + "field": "value", + "name": "Information Construct", + "description": "A name to display. All '{prop}'s will be replaced by their corresponding features[which].properties[prop] value.", + "type": "text", + "width": 4 + }, + { + "field": "icon", + "name": "Icon Name", + "description": "Any Material Design Icon name: https://pictogrammers.com/library/mdi/", + "type": "text", + "width": 3 + }, + { + "field": "which", + "name": "From Feature", + "description": "This only supports the value 'last' at this point meaning that the properties from the last feature in the layer while be used to populate the information in the TopBar. ", + "type": "dropdown", + "width": 3, + "options": ["last"] + }, + { + "field": "go", + "name": "Go", + "description": "if true, pans and zooms to the feature of which on initial load. The zoom used is Map Scale Zoom or the current zoom. In the case of multiple layers configured with 'Go', only the first feature with info.go is gone to.", + "type": "checkbox", + "width": 2, + "defaultChecked": false + } + ] + } + ] + } + ] + }, + { + "name": "Information", + "rows": [ + { + "name": "Information", + "subname": "Layer Tags", + "subdescription": "Assign tags to this layer so that they may be searched upon through the LayersTool.", + "components": [ + { + "new": true, + "field": "variables.tags.0", + "name": "Tag", + "description": "", + "type": "text", + "width": 2 + }, + { + "field": "variables.tags.1", + "name": "Tag", + "description": "", + "type": "text", + "width": 2 + }, + { + "field": "variables.tags.2", + "name": "Tag", + "description": "", + "type": "text", + "width": 2 + }, + { + "field": "variables.tags.3", + "name": "Tag", + "description": "", + "type": "text", + "width": 2 + }, + { + "field": "variables.tags.4", + "name": "Tag", + "description": "", + "type": "text", + "width": 2 + }, + { + "field": "variables.tags.5", + "name": "Tag", + "description": "", + "type": "text", + "width": 2 + } + ] + }, + { + "components": [ + { + "field": "variables.tags.6", + "name": "Tag", + "description": "", + "type": "text", + "width": 2 + }, + { + "field": "variables.tags.7", + "name": "Tag", + "description": "", + "type": "text", + "width": 2 + }, + { + "field": "variables.tags.8", + "name": "Tag", + "description": "", + "type": "text", + "width": 2 + }, + { + "field": "variables.tags.9", + "name": "Tag", + "description": "", + "type": "text", + "width": 2 + }, + { + "field": "variables.tags.10", + "name": "Tag", + "description": "", + "type": "text", + "width": 2 + }, + { + "field": "variables.tags.11", + "name": "Tag", + "description": "", + "type": "text", + "width": 2 + } + ] + }, + { + "subname": "Description", + "subdescription": "A freeform markdown description of the layer. In the LayersTool, users may click the information icon beside the layer's name to view this information.", + "components": [ + { + "field": "description", + "name": "Description", + "description": "", + "type": "markdown", + "width": 12 + } + ] + } + ] + }, + { + "name": "Datasets", + "rows": [ + { + "name": "Datasets", + "subname": "Dataset Connections", + "components": [ + { + "field": "variables.datasetLinks", + "name": "Connections", + "description": "Datasets are csvs uploaded from the 'Manage Datasets' page accessible on the lower left. Every time a feature from this layer is clicked with datasetLinks configured, it will request the data from the server and include it with it's regular geojson properties. This is especially useful when single features need a lot of metadata to perform a task as it loads it only as needed. A unique value needs to be present in both the feature's 'prop'erties and in the dataset's 'column' in order to combine the metadata.", + "type": "objectarray", + "width": 12, + "object": [ + { + "field": "dataset", + "name": "Dataset Name", + "description": "The name of a dataset to link to. A list of datasets can be found in the 'Manage Datasets' page.", + "type": "text", + "width": 3 + }, + { + "field": "column", + "name": "Connecting Dataset Column", + "description": "This is a column/csv header name within the dataset. If the value of the prop key matches the value in this column, the entire row will be return. All rows that match are returned.", + "type": "text", + "width": 3 + }, + { + "field": "prop", + "name": "Connecting Feature Property", + "description": "This is a property key already within the features properties. It's value will be searched for in the specified dataset column.", + "type": "text", + "width": 3 + } + ] + } + ] + } + ] + }, + { + "name": "Attachment - Layers", + "rows": [ + { + "name": "Attachment - Layers", + "subname": "Labels", + "components": [ + { + "new": true, + "field": "variables.layerAttachments.labels.enabled", + "name": "Enabled", + "description": "Place a label beside each feature. Also applies to 'Coordinate Attachments -> Marker' features.", + "type": "switch", + "width": 3, + "defaultChecked": false + } + ] + }, + { + "components": [ + { + "field": "variables.layerAttachments.labels.initialVisibility", + "name": "Initial Visibility", + "description": "Whether the label sublayer is initially on. Users can toggle sublayers on and off in the layer settings in the LayersTool.", + "type": "checkbox", + "width": 2, + "defaultChecked": false + }, + { + "field": "variables.layerAttachments.labels.theme", + "name": "Theme", + "description": "Label theme. Either default or solid. Default is white text with a black border. Solid is white text with a dark-grey background box.", + "type": "dropdown", + "width": 2, + "options": ["default", "solid"] + }, + { + "field": "variables.layerAttachments.labels.size", + "name": "Size", + "description": "Label size. Either default or large. Default is 14px, large is 16px.", + "type": "dropdown", + "width": 2, + "options": ["default", "large"] + } + ] + }, + { + "subname": "Pairings", + "components": [ + { + "field": "variables.layerAttachments.pairings.enabled", + "name": "Enabled", + "description": "Links cross-layer features together. Features paired to this layer will attempt to compute the azimuth-elevation relationship between the two to draw in the Viewer's PhotoSphere. Additionally, on the Map, a line will be drawn between the two features.", + "type": "switch", + "width": 3, + "defaultChecked": false + } + ] + }, + { + "components": [ + { + "field": "variables.layerAttachments.pairings.initialVisibility", + "name": "Initial Visibility", + "description": "Whether the pairing line sublayer is initially on. Users can toggle sublayers on and off in the layer settings in the LayersTool.", + "type": "checkbox", + "width": 2, + "defaultChecked": false + }, + { + "field": "variables.layerAttachments.pairings.layers", + "name": "Layers with which to Pair", + "description": "An comma-separated array of names or UUIDs of other layers.", + "type": "textarray", + "width": 6 + }, + { + "field": "variables.layerAttachments.pairings.pairProp", + "name": "Pair Property", + "description": "The dot.notated path to the feature properties that contains the property to pair on. This layer and all paired layers need this property to properly pair up. A feature in this layer is said to be paired with a feature of one of the other specified layers, if and only if the values of this property in both features matches.", + "type": "text", + "width": 4 + } + ] + }, + { + "components": [ + { + "field": "variables.layerAttachments.pairings.layerAzProp", + "name": "Layer Azimuth Property", + "description": "The dot.notated path to the feature properties that contains the features' azimuth. If unset, the azimuth will be calculated through the feature's longitude, latitude, elevation coordinates.", + "type": "text", + "width": 4 + }, + { + "field": "variables.layerAttachments.pairings.layerElProp", + "name": "Layer Elevation Property", + "description": "The dot notated path to the feature properties that contains the features' elevation. If unset, the elevation will be calculated through the feature's longitude, latitude, elevation coordinates.", + "type": "text", + "width": 4 + }, + { + "field": "variables.layerAttachments.pairings.originOffsetOrder", + "name": "Origin Offset Order", + "description": "In many cases, a marker's center is not the camera's center. Within a feature's 'properties.images' objects, 'originOffset' can be defined. 'originOffsetOrder' describes the XYZ order and signage that 'originOffset' should be read in. Possible values are X, -X, Y, -Y, Z, -Z. Example value: 'X,Y,-Z'", + "type": "textarray", + "width": 4 + } + ] + }, + { + "components": [ + { + "field": "variables.layerAttachments.pairings.style.color", + "name": "Pairing Line Color", + "description": "The color of the line to drawn between paired features that indicate their connectivity.", + "type": "colorpicker", + "width": 2 + }, + { + "field": "variables.layerAttachments.pairings.style.weight", + "name": "Pairing Line Weight", + "description": "The weight (thickness) of the line to drawn between paired features that indicate their connectivity.", + "type": "number", + "min": 1, + "width": 3 + } + ] + } + ] + }, + { + "name": "Attachment - Coordinates", + "rows": [ + { + "name": "Attachment - Coordinates", + "subname": "Marker", + "components": [ + { + "new": true, + "field": "variables.coordinateAttachments.marker.enabled", + "name": "Enabled", + "description": "Place a marker at every coordinate of every feature.", + "type": "switch", + "width": 3, + "defaultChecked": false + } + ] + }, + { + "components": [ + { + "field": "variables.coordinateAttachments.marker.initialVisibility", + "name": "Initial Visibility", + "description": "Whether the coordinate marker sublayer is initially on. Users can toggle sublayers on and off in the layer settings in the LayersTool.", + "type": "checkbox", + "width": 2, + "defaultChecked": false + }, + { + "field": "variables.coordinateAttachments.marker.color", + "name": "Color", + "description": "A stroke/border color for the markers.", + "type": "colorpicker", + "width": 2 + }, + { + "field": "variables.coordinateAttachments.marker.opacity", + "name": "Opacity", + "description": "A stroke/border opacity for the markers.", + "type": "number", + "min": 0, + "max": 1, + "width": 2 + }, + { + "field": "variables.coordinateAttachments.marker.fillColor", + "name": "Fill Color", + "description": "A fill color for the markers.", + "type": "colorpicker", + "width": 2 + }, + { + "field": "variables.coordinateAttachments.marker.fillOpacity", + "name": "Fill Opacity", + "description": "A fill opacity for the markers.", + "type": "number", + "min": 0, + "max": 1, + "width": 2 + } + ] + }, + { + "components": [ + { + "field": "variables.coordinateAttachments.marker.weight", + "name": "Weight", + "description": "A stroke thickness in pixels for the markers.", + "type": "number", + "min": 1, + "step": 1, + "width": 3 + }, + { + "field": "variables.coordinateAttachments.marker.radius", + "name": "Radius", + "description": "A radius in pixels for the markers.", + "type": "number", + "min": 1, + "step": 1, + "width": 3 + } + ] + } + ] + }, + { + "name": "Attachment - Markers", + "rows": [ + { + "name": "Attachment - Markers", + "subname": "Bearings", + "components": [ + { + "new": true, + "field": "variables.markerAttachments.bearing.enabled", + "name": "Enabled", + "description": "Sets a bearing direction (clockwise from north) of this layer's point markers (or markerIcons if set). Overrides the layer's shape dropdown value.", + "type": "switch", + "width": 3, + "defaultChecked": false + } + ] + }, + { + "components": [ + { + "field": "variables.markerAttachments.bearing.angleProp", + "name": "Angle Property", + "description": "The dot.notated path to the feature properties that contains the desired rotation angle. Ex. headings.yaw.", + "type": "text", + "width": 6 + }, + { + "field": "variables.markerAttachments.bearing.angleUnit", + "name": "Angle Unit", + "description": "Unit of the value of 'angleProp'.", + "type": "dropdown", + "width": 2, + "options": ["deg", "rad"] + }, + { + "field": "variables.markerAttachments.bearing.color", + "name": "Color", + "description": "A color for the directional arrow for non-markerIcon bearings.", + "type": "colorpicker", + "width": 4 + } + ] + }, + { + "subname": "Image", + "components": [ + { + "new": true, + "field": "variables.markerAttachments.image.enabled", + "name": "Enabled", + "description": "Places a scaled and orientated image under each marker.", + "type": "switch", + "width": 3, + "defaultChecked": false + } + ] + }, + { + "components": [ + { + "field": "variables.markerAttachments.image.path", + "name": "Image URL", + "description": "A url to a (preferably) top-down north-facing orthographic image.", + "type": "text", + "width": 9 + }, + { + "field": "variables.markerAttachments.image.pathProp", + "name": "Image URL Property", + "description": "A prop.path to an image url. This takes priority over 'Image URL' and is useful if the path is feature specific.", + "type": "text", + "width": 3 + } + ] + }, + { + "components": [ + { + "field": "variables.markerAttachments.image.initialVisibility", + "name": "Initial Visibility", + "description": "Whether the image sublayer is initially on. Users can toggle sublayers on and off in the layer settings in the LayersTool.", + "type": "checkbox", + "width": 2, + "defaultChecked": false + }, + { + "field": "variables.markerAttachments.image.initialOpacity", + "name": "Initial Opacity", + "description": "The initial image opacity. Users can change sublayer opacity n the layer settings in the LayersTool. From 0 to 1.", + "type": "number", + "min": 0, + "max": 1, + "width": 2 + }, + { + "field": "variables.markerAttachments.image.widthMeters", + "name": "Width in Meters", + "description": "Width of image in meters in order to calculate scale.", + "type": "number", + "min": 0, + "max": 1, + "width": 2 + }, + { + "field": "variables.markerAttachments.image.widthPixels", + "name": "Pixels Wide", + "description": "Image width in pixels.", + "type": "number", + "min": 0, + "max": 1, + "width": 2 + }, + { + "field": "variables.markerAttachments.image.heightPixels", + "name": "Pixels High", + "description": "Image height in pixels.", + "type": "number", + "min": 0, + "max": 1, + "width": 2 + } + ] + }, + { + "components": [ + { + "field": "variables.markerAttachments.image.angleProp", + "name": "Angle Property", + "description": "Prop path to the rotation of the image", + "type": "text", + "width": 6 + }, + { + "field": "variables.markerAttachments.image.angleUnit", + "name": "Angle Unit", + "description": "The units of 'Angle Property'.", + "type": "dropdown", + "width": 2, + "options": ["deg", "rad"] + }, + { + "field": "variables.markerAttachments.image.show", + "name": "Shown", + "description": "If set to 'always', overrides the Waypoints Kind (if set) and always renders the image under the marker. 'click' just shows the image on click and requires the layer to have the Waypoints Kind.", + "type": "dropdown", + "width": 2, + "options": ["click", "always"] + } + ] + }, + { + "subname": "Model", + "components": [ + { + "field": "variables.markerAttachments.model.enabled", + "name": "Enabled", + "description": "Render a model in the 3D Globe for the point features of this layer.", + "type": "switch", + "width": 3, + "defaultChecked": false + } + ] + }, + { + "components": [ + { + "field": "variables.markerAttachments.model.path", + "name": "Model URL", + "description": "A path to model. The following model formats are supported: .dae, .glb, .gltf, and .obj.", + "type": "text", + "width": 9 + }, + { + "field": "variables.markerAttachments.model.pathProp", + "name": "Model URL Property", + "description": "A prop.path to a model. This takes priority over 'Model URL' and is useful if model is feature specific.", + "type": "text", + "width": 3 + } + ] + }, + { + "components": [ + { + "field": "variables.markerAttachments.model.yawProp", + "name": "Yaw Property", + "description": "Prop path to the model's yaw. If this value is a number, uses it directly.", + "type": "text", + "width": 8 + }, + { + "field": "variables.markerAttachments.model.yawUnit", + "name": "Yaw Unit", + "description": "The units of 'Yaw Property'.", + "type": "dropdown", + "width": 2, + "options": ["deg", "rad"] + }, + { + "field": "variables.markerAttachments.model.invertYaw", + "name": "Invert Yaw", + "description": "If true, multiplies yaw by -1.", + "type": "checkbox", + "width": 2, + "defaultChecked": false + } + ] + }, + { + "components": [ + { + "field": "variables.markerAttachments.model.pitchProp", + "name": "Pitch Property", + "description": "Prop path to the model's pitch. If this value is a number, uses it directly.", + "type": "text", + "width": 8 + }, + { + "field": "variables.markerAttachments.model.pitchUnit", + "name": "Pitch Unit", + "description": "The units of 'Pitch Property'.", + "type": "dropdown", + "width": 2, + "options": ["deg", "rad"] + }, + { + "field": "variables.markerAttachments.model.invertPitch", + "name": "Invert Pitch", + "description": "If true, multiplies pitch by -1.", + "type": "checkbox", + "width": 2, + "defaultChecked": false + } + ] + }, + { + "components": [ + { + "field": "variables.markerAttachments.model.rollProp", + "name": "Roll Property", + "description": "Prop path to the model's roll. If this value is a number, uses it directly.", + "type": "text", + "width": 8 + }, + { + "field": "variables.markerAttachments.model.rollUnit", + "name": "Roll Unit", + "description": "The units of 'Roll Property'.", + "type": "dropdown", + "width": 2, + "options": ["deg", "rad"] + }, + { + "field": "variables.markerAttachments.model.invertRoll", + "name": "Invert Roll", + "description": "If true, multiplies roll by -1.", + "type": "checkbox", + "width": 2, + "defaultChecked": false + } + ] + }, + { + "components": [ + { + "field": "variables.markerAttachments.model.elevationProp", + "name": "Elevation Property", + "description": "Prop path to the model's elevation (in meters). If this value is a number, uses it directly. Default 0.", + "type": "text", + "width": 4 + }, + { + "field": "variables.markerAttachments.model.scaleProp", + "name": "Scale Property", + "description": "Prop path to the model's scale. If this value is a number, uses it directly. Default 1.", + "type": "text", + "width": 4 + }, + { + "field": "variables.markerAttachments.model.show", + "name": "Shown", + "description": "If set to 'always', always renders the model at the marker. 'click' just shows the model on-click", + "type": "dropdown", + "width": 2, + "options": ["click", "always"] + }, + { + "field": "variables.markerAttachments.model.onlyLastN", + "name": "Only on Last N", + "description": "If 0, shows models at all points. If a number, only shows models for the last n points of the layer.", + "type": "number", + "min": 0, + "width": 2 + } + ] + }, + { + "subname": "Uncertainty Ellipses", + "components": [ + { + "field": "variables.markerAttachments.uncertainty.enabled", + "name": "Enabled", + "description": "Turns on a sublayer feature that places ellipses about point features in order to indicate positional uncertainties.", + "type": "switch", + "width": 3, + "defaultChecked": false + } + ] + }, + { + "components": [ + { + "field": "variables.markerAttachments.uncertainty.initialVisibility", + "name": "Initial Visibility", + "description": "Whether the uncertainty sublayer is initially on. Users can toggle sublayers on and off in the layer settings in the LayersTool.", + "type": "checkbox", + "width": 2, + "defaultChecked": false + }, + { + "field": "variables.markerAttachments.uncertainty.strokeColor", + "name": "Stroke Color", + "description": "A stroke/border color for the uncertainty ellipse. Defaults to 'black'.", + "type": "colorpicker", + "width": 2 + }, + { + "field": "variables.markerAttachments.uncertainty.opacity", + "name": "Stroke Opacity", + "description": "A stroke/border opacity for the uncertainty ellipse. 0 (transparent) to 1 (opaque).", + "type": "number", + "min": 0, + "max": 1, + "width": 2 + }, + { + "field": "variables.markerAttachments.uncertainty.color", + "name": "Fill Color", + "description": "A fill color for the uncertainty ellipse. It will appear more transparent than set. Defaults to 'white'.", + "type": "colorpicker", + "width": 2 + }, + { + "field": "variables.markerAttachments.uncertainty.fillOpacity", + "name": "Fill Opacity", + "description": "A fill opacity for the uncertainty ellipse. 0 (transparent) to 1 (opaque).", + "type": "number", + "min": 0, + "max": 1, + "width": 2 + } + ] + }, + { + "components": [ + { + "field": "variables.markerAttachments.uncertainty.weight", + "name": "Weight", + "description": "A stroke/border weight for the uncertainty ellipse.", + "type": "number", + "min": 1, + "width": 2 + }, + { + "field": "variables.markerAttachments.uncertainty.color3d", + "name": "3D Color", + "description": "A color for the ellipse's vertical curtain in the 3D Globe. Can be an array for a vertical gradient: 'rgba(0,0,0,0), #26A8FF'.", + "type": "textarray", + "width": 4 + }, + { + "field": "variables.markerAttachments.uncertainty.opacity3d", + "name": "3D Opacity", + "description": "A fill opacity for the ellipse's vertical curtain in the 3D Globe. 0 (transparent) to 1 (opaque).", + "type": "number", + "min": 0, + "max": 1, + "width": 2 + }, + { + "field": "variables.markerAttachments.uncertainty.depth3d", + "name": "3D Depth", + "description": "A depth, in meters, for the ellipse's vertical curtain in the 3D Globe. Defaults to 2.", + "type": "number", + "min": 0, + "width": 2 + } + ] + }, + { + "components": [ + { + "field": "variables.markerAttachments.uncertainty.xAxisProp", + "name": "X-Axis Property", + "description": "Prop path to the x-axis radius value of the ellipse.", + "type": "text", + "width": 5 + }, + { + "field": "variables.markerAttachments.uncertainty.yAxisProp", + "name": "Y-Axis Property", + "description": "Prop path to the y-axis radius value of the ellipse.", + "type": "text", + "width": 5 + }, + { + "field": "variables.markerAttachments.uncertainty.axisUnit", + "name": "Axis Unit", + "description": "The units of 'X-Axis Property' and 'Y-Axis Property'.", + "type": "dropdown", + "width": 2, + "options": ["meters", "kilometers"] + } + ] + }, + { + "components": [ + { + "field": "variables.markerAttachments.uncertainty.angleProp", + "name": "Angle Property", + "description": "Prop path to the rotation of the ellipse.", + "type": "text", + "width": 5 + }, + { + "field": "variables.markerAttachments.uncertainty.angleUnit", + "name": "Angle Unit", + "description": "The units of 'Angle Property'.", + "type": "dropdown", + "width": 2, + "options": ["deg", "rad"] + } + ] + } + ] + }, + { + "name": "Attachment - Paths", + "rows": [ + { + "name": "Attachment - Paths", + "subname": "Gradient", + "components": [ + { + "field": "variables.pathAttachments.gradient.enabled", + "name": "Enabled", + "description": "Renders a 'hotline' gradient for visualizing value changes along a path. Useful with 'Miscellaneous -> Hide Main Features'. Look into 'Enhanced GeoJSON' in the docs on how to format layers with properties-per-coordinate.", + "type": "switch", + "width": 3, + "defaultChecked": false + } + ] + }, + { + "components": [ + { + "field": "variables.pathAttachments.gradient.colorWithProp", + "name": "Color With Property", + "description": "Prop path to the value's key to visualize.", + "type": "text", + "width": 3 + }, + { + "field": "variables.pathAttachments.gradient.dropdownColorWithProp", + "name": "Dropdown Color With Properties", + "description": "Array of additional prop paths. Users can toggle between value properties in the LayersTool. 'Color With Property's path is automatically added here if not already included.", + "type": "textarray", + "width": 9 + } + ] + }, + { + "components": [ + { + "field": "variables.pathAttachments.gradient.colorRamp", + "name": "Color Ramp", + "description": "Array of css colors indicating the color ramp. The first color represents the min value and the last color represents the max value in the layer.", + "type": "textarray", + "width": 10 + }, + { + "field": "variables.pathAttachments.gradient.weight", + "name": "Weight", + "description": "Line thickness in pixels.", + "type": "number", + "min": 1, + "width": 2 + } + ] + } + ] + }, + { + "name": "Miscellaneous", + "rows": [ + { + "name": "Miscellaneous", + "subname": "Chemistry", + "components": [ + { + "field": "variables.chemistry", + "name": "Chemistry Values", + "description": "Comma-separated chemistry columns to use as percentages and it what order. Should be used with a Dataset. Look into the docs for the ChemistryTool for more information.", + "type": "textarray", + "width": 12 + } + ] + }, + { + "components": [ + { + "field": "variables.hideMainFeature", + "name": "Hide Main Features", + "description": " If true, hides all typically rendered features. This is useful if showing only the 'Attachments' of layers is desired and not their features themselves. ", + "type": "checkbox", + "width": 3, + "defaultChecked": false + } + ] + } + ] + } + ] +} diff --git a/configure/src/metaconfigs/layer-vector-config.json b/configure/src/metaconfigs/layer-vector-config.json index 36cb416e..51b42ceb 100644 --- a/configure/src/metaconfigs/layer-vector-config.json +++ b/configure/src/metaconfigs/layer-vector-config.json @@ -232,6 +232,31 @@ } ] }, + { + "components": [ + { + "field": "shape", + "name": "Shape", + "description": "A marker symbol for point features for this layer. When set to 'None', a simple circle is used. Marker icons and other attachments may override this setting.", + "type": "dropdown", + "width": 4, + "options": [ + "none", + "circle", + "directional-circle", + "triangle", + "triangle-flipped", + "square", + "diamond", + "pentagon", + "hexagon", + "star", + "plus", + "pin" + ] + } + ] + }, { "subname": "Marker Icons", "subdescription": "Uses an icon image instead of an svg for all of the layer's point markers. If you're using this as a bearing marker, make sure the base icon is pointing north.", diff --git a/configure/src/metaconfigs/layer-vectortile-config.json b/configure/src/metaconfigs/layer-vectortile-config.json index 0967ef42..073035b9 100644 --- a/configure/src/metaconfigs/layer-vectortile-config.json +++ b/configure/src/metaconfigs/layer-vectortile-config.json @@ -1 +1,1636 @@ -{} +{ + "tabs": [ + { + "name": "Core", + "rows": [ + { + "name": "Core", + "components": [ + { + "field": "type", + "name": "Layer Type", + "description": "", + "type": "dropdown", + "width": 2, + "options": [ + "data", + "header", + "model", + "query", + "tile", + "vector", + "vectortile" + ] + }, + { + "field": "name", + "name": "Layer Name", + "description": "A display name for the layer.", + "type": "text", + "width": 6 + }, + { + "field": "kind", + "name": "Kind of Layer", + "description": "A special kind of interaction for the layer. Please see the Kinds page in the documentation for more.", + "type": "dropdown", + "width": 2, + "options": [ + "none", + "info", + "waypoint", + "chemistry_tool", + "draw_tool", + "edl_wedge" + ] + }, + { + "field": "visibility", + "name": "Initial Visibility", + "description": "Whether the layer is on initially.", + "type": "checkbox", + "width": 2, + "defaultChecked": false + } + ] + }, + { + "components": [ + { + "field": "url", + "name": "URL", + "description": "A file path that points to a geojson. If the path is relative, it will be relative to the mission's directory. The URL must contain a proper placeholder ending such as: {z}/{x}/{y}.png. Alternatively vectors can be served with Geodatasets. Simply go to 'Manage Geodatasets' at the bottom left, upload a geojson and link to it in this URL field with 'geodatasets:{geodataset*name}'", + "type": "text", + "width": 12 + } + ] + }, + { + "components": [ + { + "field": "minZoom", + "name": "Minimum Zoom", + "description": "The lowest (smallest number) zoom level for which to show this layer. If the current Map's zoom level is less than this, the layer will not be rendered even if the layer is still on.", + "type": "number", + "min": 0, + "step": 1, + "width": 2 + }, + { + "field": "maxZoom", + "name": "Maximum Zoom", + "description": "The highest (greatest number) zoom level for which to show this layer. If the current Map's zoom level is higher/deeper than this, the layer will not be rendered even if the layer is still on.", + "type": "number", + "min": 0, + "step": 1, + "width": 2 + }, + { + "field": "layer3dType", + "name": "3D Layer Type", + "description": "Whether, in the Globe, to clamp this layer onto the ground textures or to represent with sprites and line objects.", + "type": "dropdown", + "width": 2, + "options": ["clamped", "vector"] + }, + { + "field": "initialOpacity", + "name": "Initial Opacity", + "description": "A value from 0 (transparent) to 1 (opaque) of the layer's initial opacity.", + "type": "number", + "min": 0, + "max": 1, + "width": 2 + } + ] + } + ] + }, + { + "name": "Style", + "rows": [ + { + "name": "Style", + "components": [ + { + "field": "style.color", + "name": "Color", + "description": "The border color of each feature. If the feature is a line, this field is the color of the line.", + "type": "colorpicker", + "width": 2 + }, + { + "new": true, + "field": "style.colorProp", + "name": "Color From Property", + "description": "Color but taken from each individual feature's property. For example 'color' or 'path.to.prop.in.properties.object'. Takes priority over the value in the 'Color' field.", + "type": "text", + "width": 4 + }, + { + "field": "style.opacity", + "name": "Opacity", + "description": "Stroke Opacity", + "type": "slider", + "min": 0, + "max": 1, + "step": 0.01, + "width": 2 + }, + { + "new": true, + "field": "style.opacityProp", + "name": "Opacity From Property", + "description": "Opacity but taken from each individual feature's property. For example 'opacity' or 'path.to.prop.in.properties.object'. Takes priority over the value in the 'Opacity' field.", + "type": "text", + "width": 4 + } + ] + }, + { + "components": [ + { + "field": "style.fillColor", + "name": "Fill Color", + "description": "Fill Color", + "type": "colorpicker", + "width": 2 + }, + { + "new": true, + "field": "style.fillColorProp", + "name": "Fill Color From Property", + "description": "Fill Color but taken from each individual feature's property. For example 'fill_color' or 'path.to.prop.in.properties.object'. Takes priority over the value in the 'Fill Color' field.", + "type": "text", + "width": 4 + }, + { + "field": "style.fillOpacity", + "name": "Fill Opacity", + "description": "Fill Opacity", + "type": "slider", + "min": 0, + "max": 1, + "step": 0.01, + "width": 2 + }, + { + "new": true, + "field": "style.fillOpacityProp", + "name": "Fill Opacity From Property", + "description": "Fill Opacity but taken from each individual feature's property. For example 'fill_opacity' or 'path.to.prop.in.properties.object'. Takes priority over the value in the 'Fill Opacity' field.", + "type": "text", + "width": 4 + } + ] + }, + { + "components": [ + { + "field": "style.weight", + "name": "Weight", + "description": "Stroke weight in pixels of point features.", + "type": "slider", + "min": 0, + "max": 16, + "step": 1, + "width": 2 + }, + { + "new": true, + "field": "style.weightProp", + "name": "Weight From Property", + "description": "Weight but taken from each individual feature's property. For example 'weight' or 'path.to.prop.in.properties.object'. Takes priority over the value in the 'Weight' field.", + "type": "text", + "width": 4 + }, + { + "field": "style.radius", + "name": "Radius", + "description": "Radius in pixels of point features.", + "type": "slider", + "min": 0, + "max": 24, + "step": 1, + "width": 2 + }, + { + "new": true, + "field": "style.radiusProp", + "name": "Radius From Property", + "description": "Radius but taken from each individual feature's property. For example 'radius' or 'path.to.prop.in.properties.object'. Takes priority over the value in the 'Radius' field.", + "type": "text", + "width": 4 + } + ] + }, + { + "components": [ + { + "field": "style.vtId", + "name": "Vector Tile Feature Unique Id Key", + "description": "Each feature of the vector tileset needs a property with a unique value to identify it. This required field is the name of that property.", + "type": "text", + "width": 4 + }, + { + "field": "style.vtKey", + "name": "Vector Tile Use Key as Name", + "description": "(Optional) The property key whose value should be the hover text of each feature.", + "type": "text", + "width": 4 + }, + { + "field": "shape", + "name": "Shape", + "description": "A marker symbol for point features for this layer. When set to 'None', a simple circle is used. Marker icons and other attachments may override this setting.", + "type": "dropdown", + "width": 4, + "options": [ + "none", + "circle", + "directional-circle", + "triangle", + "triangle-flipped", + "square", + "diamond", + "pentagon", + "hexagon", + "star", + "plus", + "pin" + ] + } + ] + }, + + { + "subname": "Vector Tile Stylings", + "subdescription": "Like: { \"\": { \"color\": \"#FFFFFF\", \"fill\": true, \"fillColor\": \"rgb(0, 125, 200)\", \"fillOpacity\": 0.5, \"opacity\": 1,\"radius\": 4,\"weight\": 2} }", + "components": [ + { + "field": "style.vtLayer", + "name": "Vector Tile Style", + "description": "Vector Tiles are styled differently than Vectors. Raw variables here takes an object that maps internal vector tile layer names to styles. All raw variables are optional.", + "type": "json", + "width": 12 + } + ] + }, + { + "subname": "Marker Icons", + "subdescription": "Uses an icon image instead of an svg for all of the layer's point markers. If you're using this as a bearing marker, make sure the base icon is pointing north.", + "components": [ + { + "field": "variables.markerIcon.iconUrl", + "name": "Icon URL", + "description": "A URL to an image to use as a marker icon.", + "type": "text", + "width": 12 + } + ] + }, + { + "components": [ + { + "field": "variables.markerIcon.shadowUrl", + "name": "Shadow URL", + "description": "An optional URL to an image to use as a marker icon's shadow..", + "type": "text", + "width": 12 + } + ] + }, + { + "components": [ + { + "field": "variables.markerIcon.iconSize.0", + "name": "Icon X Size", + "description": "Pixel width of the icon's image.", + "type": "number", + "min": 0, + "step": 1, + "width": 3 + }, + { + "field": "variables.markerIcon.iconSize.1", + "name": "Icon Y Size", + "description": "Pixel height of the icon's image.", + "type": "number", + "min": 0, + "step": 1, + "width": 3 + }, + { + "field": "variables.markerIcon.shadowSize.0", + "name": "Shadow X Size", + "description": "Pixel width of the icon's shadow.", + "type": "number", + "min": 0, + "step": 1, + "width": 3 + }, + { + "field": "variables.markerIcon.shadowSize.1", + "name": "Shadow Y Size", + "description": "Pixel height of the icon's shadow.", + "type": "number", + "min": 0, + "step": 1, + "width": 3 + } + ] + }, + { + "components": [ + { + "field": "variables.markerIcon.iconAnchor.0", + "name": "Icon X Anchor", + "description": "X point of the icon which will correspond to marker's center location.", + "type": "number", + "width": 3 + }, + { + "field": "variables.markerIcon.iconAnchor.1", + "name": "Icon Y Anchor", + "description": "Y point of the icon which will correspond to marker's center location.", + "type": "number", + "width": 3 + }, + { + "field": "variables.markerIcon.shadowAnchor.0", + "name": "Shadow X Anchor", + "description": "X point of the shadow which will correspond to marker's center location.", + "type": "number", + "width": 3 + }, + { + "field": "variables.markerIcon.shadowAnchor.1", + "name": "Shadow Y Anchor", + "description": "Y point of the shadow which will correspond to marker's center location.", + "type": "number", + "width": 3 + } + ] + } + ] + }, + { + "name": "Time", + "rows": [ + { + "name": "Time", + "components": [ + { + "field": "time.enabled", + "name": "Time Enabled", + "description": "True if the layer is time enabled. URLs that contain {starttime} or {endtime} will be dynamically replaced by their set values when the layer is fetched. If true and a URL is set and Controlled is true, only the initial url query will be performed.", + "type": "switch", + "width": 3, + "defaultChecked": false + } + ] + }, + { + "components": [ + { + "field": "time.type", + "name": "Time Type", + "description": "When the time changes, whether the layer should requery the source or filter the layer locally based on feature properties.", + "type": "dropdown", + "width": 3, + "options": ["requery", "local"] + } + ] + }, + { + "components": [ + { + "field": "time.format", + "name": "Time Format", + "description": "The string format to be used in the URL for {starttime} and {endtime}. Uses D3 time format specifiers: https://github.com/d3/d3-time-format. Default: %Y-%m-%dT%H:%M:%SZ", + "type": "text", + "width": 6 + } + ] + }, + { + "components": [ + { + "field": "time.startProp", + "name": "Start Time Property Name", + "description": "Optional and only in use if Time Enabled = true and Time Type = Local. The starting time property path. Setting this is addition to Main Time Property Name casts the feature's time over a range instead of as a single point in time. Can use dot-notation for nested path. Can be a unix timestamp or an ISO time (end the ISO with a Z to designate that it should be treated as a UTC time).", + "type": "text", + "width": 6 + }, + { + "field": "time.endProp", + "name": "Main/End Time Property Name", + "description": "Required in Time Enabled = true and Time Type = Local. The main time property path. Can use dot-notation for nested path. Can be a unix timestamp or an ISO time (end the ISO with a Z to designate that it should be treated as a UTC time).", + "type": "text", + "width": 6 + } + ] + } + ] + }, + { + "name": "Legend", + "rows": [ + { + "name": "Legend", + "components": [ + { + "field": "variables.legend", + "name": "Legend", + "description": "Configures a legend for the layer. The Legend Tool renders symbologies and gradient scales for any properly configured layer that is on.", + "type": "objectarray", + "width": 12, + "object": [ + { + "field": "color", + "name": "Fill Color", + "description": "A color for the main fill of the symbol.", + "type": "colorpicker", + "width": 3 + }, + { + "field": "strokecolor", + "name": "Border Color", + "description": "A stroke/border color. Note that 'discreet' and 'continuous' shapes have no borders.", + "type": "colorpicker", + "width": 3 + }, + { + "field": "shape", + "name": "Shape", + "description": "The symbol for which to us for this legend entry. Discreet and continuous describe scales. These scales are broken into groups by a change in shape value. For instance, 'discreet, discreet, discreet, circle, discreet, discreet' represents a discreet scales of three colors, a circle and then a discreet scale of two colors.", + "type": "dropdown", + "width": 3, + "options": [ + "circle", + "square", + "rect", + "triangle", + "continuous", + "discreet" + ] + }, + { + "field": "value", + "name": "Label", + "description": "A label description for this legend entry.", + "type": "text", + "width": 3 + } + ] + } + ] + } + ] + }, + { + "name": "Interface", + "rows": [ + { + "name": "Interface", + "subname": "Hover Feature Label", + "subdescription": "The property key whose value should be the hover text of each feature. If left unset, the hover key and value will be the first one listed in the feature's properties.", + "components": [ + { + "new": true, + "field": "variables.useKeyAsName.0", + "name": "Property 1", + "description": "", + "type": "text", + "width": 2 + }, + { + "new": true, + "field": "variables.useKeyAsName.1", + "name": "Property 2", + "description": "", + "type": "text", + "width": 2 + }, + { + "new": true, + "field": "variables.useKeyAsName.2", + "name": "Property 3", + "description": "", + "type": "text", + "width": 2 + }, + { + "new": true, + "field": "variables.useKeyAsName.3", + "name": "Property 4", + "description": "", + "type": "text", + "width": 2 + }, + { + "new": true, + "field": "variables.useKeyAsName.4", + "name": "Property 5", + "description": "", + "type": "text", + "width": 2 + }, + { + "new": true, + "field": "variables.useKeyAsName.5", + "name": "Property 6", + "description": "", + "type": "text", + "width": 2 + } + ] + }, + { + "components": [ + { + "new": true, + "field": "variables.useKeyAsName.6", + "name": "Property 7", + "description": "", + "type": "text", + "width": 2 + }, + { + "new": true, + "field": "variables.useKeyAsName.7", + "name": "Property 8", + "description": "", + "type": "text", + "width": 2 + }, + { + "new": true, + "field": "variables.useKeyAsName.8", + "name": "Property 9", + "description": "", + "type": "text", + "width": 2 + }, + { + "new": true, + "field": "variables.useKeyAsName.9", + "name": "Property 10", + "description": "", + "type": "text", + "width": 2 + }, + { + "new": true, + "field": "variables.useKeyAsName.10", + "name": "Property 11", + "description": "", + "type": "text", + "width": 2 + }, + { + "new": true, + "field": "variables.useKeyAsName.11", + "name": "Property 12", + "description": "", + "type": "text", + "width": 2 + } + ] + }, + { + "subname": "Key Bindings", + "components": [ + { + "field": "variables.shortcutSuffix", + "name": "Alt + {letter} Toggle Shortcut", + "description": "A single letter to 'ALT + {letter}' toggle the layer on and off. Please verify that your chosen shortcut does not conflict with other system or browser-level keyboard shortcuts.", + "type": "text", + "width": 6 + } + ] + }, + { + "subname": "Search", + "components": [ + { + "field": "variables.search", + "name": "Search Construct", + "description": "When set, this layer will become searchable through the search bar at the top. The search will look for and autocomplete on the properties specified. All properties are enclosed by parentheses and space-separated. 'round()' can be used like a function to round the property beforehand. 'rmunder()' works similarly but removes all underscores instead. For example: '(RMC) rmunder(sol)'", + "type": "text", + "width": 12 + } + ] + }, + { + "subname": "Links", + "components": [ + { + "field": "variables.links", + "name": "External Links", + "description": "Configure deep links to other sites based on the properties on a selected feature. Upon clicking a feature, a list of deep links are put into the top bar and can be clicked on to navigate to any other page.", + "type": "objectarray", + "width": 12, + "object": [ + { + "field": "name", + "name": "Display Name", + "description": "The name of the deep link. It should be unique.", + "type": "text", + "width": 3 + }, + { + "field": "link", + "name": "URL", + "description": "A url template. Curly brackets are included. On feature click, all '{prop}' are replaced with the corresponding features[i].properties.prop value. Multiple '{prop}' are supported as an access to nested props using dot notation '{stores.food.candy}'.", + "type": "text", + "width": 9 + } + ] + } + ] + }, + { + "subname": "Information", + "components": [ + { + "field": "variables.info", + "name": "TopBar Information", + "description": "Creates an informational record at the top of the page. The first use case was showing the value of the latest sol. Clicking this record pans to the feature specified by 'which'. This is used on startup and not when a user selects a feature in this layer.", + "type": "objectarray", + "width": 12, + "object": [ + { + "field": "value", + "name": "Information Construct", + "description": "A name to display. All '{prop}'s will be replaced by their corresponding features[which].properties[prop] value.", + "type": "text", + "width": 4 + }, + { + "field": "icon", + "name": "Icon Name", + "description": "Any Material Design Icon name: https://pictogrammers.com/library/mdi/", + "type": "text", + "width": 3 + }, + { + "field": "which", + "name": "From Feature", + "description": "This only supports the value 'last' at this point meaning that the properties from the last feature in the layer while be used to populate the information in the TopBar. ", + "type": "dropdown", + "width": 3, + "options": ["last"] + }, + { + "field": "go", + "name": "Go", + "description": "if true, pans and zooms to the feature of which on initial load. The zoom used is Map Scale Zoom or the current zoom. In the case of multiple layers configured with 'Go', only the first feature with info.go is gone to.", + "type": "checkbox", + "width": 2, + "defaultChecked": false + } + ] + } + ] + } + ] + }, + { + "name": "Information", + "rows": [ + { + "name": "Information", + "subname": "Layer Tags", + "subdescription": "Assign tags to this layer so that they may be searched upon through the LayersTool.", + "components": [ + { + "new": true, + "field": "variables.tags.0", + "name": "Tag", + "description": "", + "type": "text", + "width": 2 + }, + { + "field": "variables.tags.1", + "name": "Tag", + "description": "", + "type": "text", + "width": 2 + }, + { + "field": "variables.tags.2", + "name": "Tag", + "description": "", + "type": "text", + "width": 2 + }, + { + "field": "variables.tags.3", + "name": "Tag", + "description": "", + "type": "text", + "width": 2 + }, + { + "field": "variables.tags.4", + "name": "Tag", + "description": "", + "type": "text", + "width": 2 + }, + { + "field": "variables.tags.5", + "name": "Tag", + "description": "", + "type": "text", + "width": 2 + } + ] + }, + { + "components": [ + { + "field": "variables.tags.6", + "name": "Tag", + "description": "", + "type": "text", + "width": 2 + }, + { + "field": "variables.tags.7", + "name": "Tag", + "description": "", + "type": "text", + "width": 2 + }, + { + "field": "variables.tags.8", + "name": "Tag", + "description": "", + "type": "text", + "width": 2 + }, + { + "field": "variables.tags.9", + "name": "Tag", + "description": "", + "type": "text", + "width": 2 + }, + { + "field": "variables.tags.10", + "name": "Tag", + "description": "", + "type": "text", + "width": 2 + }, + { + "field": "variables.tags.11", + "name": "Tag", + "description": "", + "type": "text", + "width": 2 + } + ] + }, + { + "subname": "Description", + "subdescription": "A freeform markdown description of the layer. In the LayersTool, users may click the information icon beside the layer's name to view this information.", + "components": [ + { + "field": "description", + "name": "Description", + "description": "", + "type": "markdown", + "width": 12 + } + ] + } + ] + }, + + { + "name": "Behavior", + "rows": [ + { + "name": "Behavior", + "subname": "Dynamic Extent", + "components": [ + { + "field": "variables.dynamicExtent", + "name": "Enabled", + "description": " If true, tries to only query the vector features present in the user's current map viewport. This can be very performant for large vector datasets especially if minimum and maximum zooms are set for the layer. Pan and zooming causes requeries. If used with a geodataset, the time and extent queries will work out-of-the-box. Otherwise, if using an external server, the following parameters in {} will be automatically replaced on query in the url: 'starttime={starttime}&endtime={endtime}&startprop={startprop}&endprop={endprop}&crscode={crscode}&zoom={zoom}&minx={minx}&miny={miny}&maxx={maxx}&maxy={maxy}'", + "type": "switch", + "width": 3, + "defaultChecked": false + }, + { + "field": "variables.dynamicExtentThreshold", + "name": "Threshold", + "description": "If dynamicExtent is true, only requery if the map was panned past the stated threshold. Unit is in meters. If a zoom-dependent threshold is desired, set this value to a string ending in '/z'. This will then internally use 'dynamicExtentMoveThreshold / Math.pow(2, zoom)' as the threshold value.", + "type": "text", + "width": 3 + } + ] + }, + { + "subname": "Miscellaneous", + "components": [ + { + "field": "variables.style.nointeraction", + "name": "Disable Interactivity", + "description": "If checked, no mouse events will trigger on features of this layer.", + "type": "checkbox", + "width": 2, + "defaultChecked": false + } + ] + } + ] + }, + { + "name": "Datasets", + "rows": [ + { + "name": "Datasets", + "subname": "Dataset Connections", + "components": [ + { + "field": "variables.datasetLinks", + "name": "Connections", + "description": "Datasets are csvs uploaded from the 'Manage Datasets' page accessible on the lower left. Every time a feature from this layer is clicked with datasetLinks configured, it will request the data from the server and include it with it's regular geojson properties. This is especially useful when single features need a lot of metadata to perform a task as it loads it only as needed. A unique value needs to be present in both the feature's 'prop'erties and in the dataset's 'column' in order to combine the metadata.", + "type": "objectarray", + "width": 12, + "object": [ + { + "field": "dataset", + "name": "Dataset Name", + "description": "The name of a dataset to link to. A list of datasets can be found in the 'Manage Datasets' page.", + "type": "text", + "width": 3 + }, + { + "field": "column", + "name": "Connecting Dataset Column", + "description": "This is a column/csv header name within the dataset. If the value of the prop key matches the value in this column, the entire row will be return. All rows that match are returned.", + "type": "text", + "width": 3 + }, + { + "field": "prop", + "name": "Connecting Feature Property", + "description": "This is a property key already within the features properties. It's value will be searched for in the specified dataset column.", + "type": "text", + "width": 3 + } + ] + } + ] + } + ] + }, + { + "name": "Attachment - Layers", + "rows": [ + { + "name": "Attachment - Layers", + "subname": "Labels", + "components": [ + { + "new": true, + "field": "variables.layerAttachments.labels.enabled", + "name": "Enabled", + "description": "Place a label beside each feature. Also applies to 'Coordinate Attachments -> Marker' features.", + "type": "switch", + "width": 3, + "defaultChecked": false + } + ] + }, + { + "components": [ + { + "field": "variables.layerAttachments.labels.initialVisibility", + "name": "Initial Visibility", + "description": "Whether the label sublayer is initially on. Users can toggle sublayers on and off in the layer settings in the LayersTool.", + "type": "checkbox", + "width": 2, + "defaultChecked": false + }, + { + "field": "variables.layerAttachments.labels.theme", + "name": "Theme", + "description": "Label theme. Either default or solid. Default is white text with a black border. Solid is white text with a dark-grey background box.", + "type": "dropdown", + "width": 2, + "options": ["default", "solid"] + }, + { + "field": "variables.layerAttachments.labels.size", + "name": "Size", + "description": "Label size. Either default or large. Default is 14px, large is 16px.", + "type": "dropdown", + "width": 2, + "options": ["default", "large"] + } + ] + }, + { + "subname": "Pairings", + "components": [ + { + "field": "variables.layerAttachments.pairings.enabled", + "name": "Enabled", + "description": "Links cross-layer features together. Features paired to this layer will attempt to compute the azimuth-elevation relationship between the two to draw in the Viewer's PhotoSphere. Additionally, on the Map, a line will be drawn between the two features.", + "type": "switch", + "width": 3, + "defaultChecked": false + } + ] + }, + { + "components": [ + { + "field": "variables.layerAttachments.pairings.initialVisibility", + "name": "Initial Visibility", + "description": "Whether the pairing line sublayer is initially on. Users can toggle sublayers on and off in the layer settings in the LayersTool.", + "type": "checkbox", + "width": 2, + "defaultChecked": false + }, + { + "field": "variables.layerAttachments.pairings.layers", + "name": "Layers with which to Pair", + "description": "An comma-separated array of names or UUIDs of other layers.", + "type": "textarray", + "width": 6 + }, + { + "field": "variables.layerAttachments.pairings.pairProp", + "name": "Pair Property", + "description": "The dot.notated path to the feature properties that contains the property to pair on. This layer and all paired layers need this property to properly pair up. A feature in this layer is said to be paired with a feature of one of the other specified layers, if and only if the values of this property in both features matches.", + "type": "text", + "width": 4 + } + ] + }, + { + "components": [ + { + "field": "variables.layerAttachments.pairings.layerAzProp", + "name": "Layer Azimuth Property", + "description": "The dot.notated path to the feature properties that contains the features' azimuth. If unset, the azimuth will be calculated through the feature's longitude, latitude, elevation coordinates.", + "type": "text", + "width": 4 + }, + { + "field": "variables.layerAttachments.pairings.layerElProp", + "name": "Layer Elevation Property", + "description": "The dot notated path to the feature properties that contains the features' elevation. If unset, the elevation will be calculated through the feature's longitude, latitude, elevation coordinates.", + "type": "text", + "width": 4 + }, + { + "field": "variables.layerAttachments.pairings.originOffsetOrder", + "name": "Origin Offset Order", + "description": "In many cases, a marker's center is not the camera's center. Within a feature's 'properties.images' objects, 'originOffset' can be defined. 'originOffsetOrder' describes the XYZ order and signage that 'originOffset' should be read in. Possible values are X, -X, Y, -Y, Z, -Z. Example value: 'X,Y,-Z'", + "type": "textarray", + "width": 4 + } + ] + }, + { + "components": [ + { + "field": "variables.layerAttachments.pairings.style.color", + "name": "Pairing Line Color", + "description": "The color of the line to drawn between paired features that indicate their connectivity.", + "type": "colorpicker", + "width": 2 + }, + { + "field": "variables.layerAttachments.pairings.style.weight", + "name": "Pairing Line Weight", + "description": "The weight (thickness) of the line to drawn between paired features that indicate their connectivity.", + "type": "number", + "min": 1, + "width": 3 + } + ] + } + ] + }, + { + "name": "Attachment - Coordinates", + "rows": [ + { + "name": "Attachment - Coordinates", + "subname": "Marker", + "components": [ + { + "new": true, + "field": "variables.coordinateAttachments.marker.enabled", + "name": "Enabled", + "description": "Place a marker at every coordinate of every feature.", + "type": "switch", + "width": 3, + "defaultChecked": false + } + ] + }, + { + "components": [ + { + "field": "variables.coordinateAttachments.marker.initialVisibility", + "name": "Initial Visibility", + "description": "Whether the coordinate marker sublayer is initially on. Users can toggle sublayers on and off in the layer settings in the LayersTool.", + "type": "checkbox", + "width": 2, + "defaultChecked": false + }, + { + "field": "variables.coordinateAttachments.marker.color", + "name": "Color", + "description": "A stroke/border color for the markers.", + "type": "colorpicker", + "width": 2 + }, + { + "field": "variables.coordinateAttachments.marker.opacity", + "name": "Opacity", + "description": "A stroke/border opacity for the markers.", + "type": "number", + "min": 0, + "max": 1, + "width": 2 + }, + { + "field": "variables.coordinateAttachments.marker.fillColor", + "name": "Fill Color", + "description": "A fill color for the markers.", + "type": "colorpicker", + "width": 2 + }, + { + "field": "variables.coordinateAttachments.marker.fillOpacity", + "name": "Fill Opacity", + "description": "A fill opacity for the markers.", + "type": "number", + "min": 0, + "max": 1, + "width": 2 + } + ] + }, + { + "components": [ + { + "field": "variables.coordinateAttachments.marker.weight", + "name": "Weight", + "description": "A stroke thickness in pixels for the markers.", + "type": "number", + "min": 1, + "step": 1, + "width": 3 + }, + { + "field": "variables.coordinateAttachments.marker.radius", + "name": "Radius", + "description": "A radius in pixels for the markers.", + "type": "number", + "min": 1, + "step": 1, + "width": 3 + } + ] + } + ] + }, + { + "name": "Attachment - Markers", + "rows": [ + { + "name": "Attachment - Markers", + "subname": "Bearings", + "components": [ + { + "new": true, + "field": "variables.markerAttachments.bearing.enabled", + "name": "Enabled", + "description": "Sets a bearing direction (clockwise from north) of this layer's point markers (or markerIcons if set). Overrides the layer's shape dropdown value.", + "type": "switch", + "width": 3, + "defaultChecked": false + } + ] + }, + { + "components": [ + { + "field": "variables.markerAttachments.bearing.angleProp", + "name": "Angle Property", + "description": "The dot.notated path to the feature properties that contains the desired rotation angle. Ex. headings.yaw.", + "type": "text", + "width": 6 + }, + { + "field": "variables.markerAttachments.bearing.angleUnit", + "name": "Angle Unit", + "description": "Unit of the value of 'angleProp'.", + "type": "dropdown", + "width": 2, + "options": ["deg", "rad"] + }, + { + "field": "variables.markerAttachments.bearing.color", + "name": "Color", + "description": "A color for the directional arrow for non-markerIcon bearings.", + "type": "colorpicker", + "width": 4 + } + ] + }, + { + "subname": "Image", + "components": [ + { + "new": true, + "field": "variables.markerAttachments.image.enabled", + "name": "Enabled", + "description": "Places a scaled and orientated image under each marker.", + "type": "switch", + "width": 3, + "defaultChecked": false + } + ] + }, + { + "components": [ + { + "field": "variables.markerAttachments.image.path", + "name": "Image URL", + "description": "A url to a (preferably) top-down north-facing orthographic image.", + "type": "text", + "width": 9 + }, + { + "field": "variables.markerAttachments.image.pathProp", + "name": "Image URL Property", + "description": "A prop.path to an image url. This takes priority over 'Image URL' and is useful if the path is feature specific.", + "type": "text", + "width": 3 + } + ] + }, + { + "components": [ + { + "field": "variables.markerAttachments.image.initialVisibility", + "name": "Initial Visibility", + "description": "Whether the image sublayer is initially on. Users can toggle sublayers on and off in the layer settings in the LayersTool.", + "type": "checkbox", + "width": 2, + "defaultChecked": false + }, + { + "field": "variables.markerAttachments.image.initialOpacity", + "name": "Initial Opacity", + "description": "The initial image opacity. Users can change sublayer opacity n the layer settings in the LayersTool. From 0 to 1.", + "type": "number", + "min": 0, + "max": 1, + "width": 2 + }, + { + "field": "variables.markerAttachments.image.widthMeters", + "name": "Width in Meters", + "description": "Width of image in meters in order to calculate scale.", + "type": "number", + "min": 0, + "max": 1, + "width": 2 + }, + { + "field": "variables.markerAttachments.image.widthPixels", + "name": "Pixels Wide", + "description": "Image width in pixels.", + "type": "number", + "min": 0, + "max": 1, + "width": 2 + }, + { + "field": "variables.markerAttachments.image.heightPixels", + "name": "Pixels High", + "description": "Image height in pixels.", + "type": "number", + "min": 0, + "max": 1, + "width": 2 + } + ] + }, + { + "components": [ + { + "field": "variables.markerAttachments.image.angleProp", + "name": "Angle Property", + "description": "Prop path to the rotation of the image", + "type": "text", + "width": 6 + }, + { + "field": "variables.markerAttachments.image.angleUnit", + "name": "Angle Unit", + "description": "The units of 'Angle Property'.", + "type": "dropdown", + "width": 2, + "options": ["deg", "rad"] + }, + { + "field": "variables.markerAttachments.image.show", + "name": "Shown", + "description": "If set to 'always', overrides the Waypoints Kind (if set) and always renders the image under the marker. 'click' just shows the image on click and requires the layer to have the Waypoints Kind.", + "type": "dropdown", + "width": 2, + "options": ["click", "always"] + } + ] + }, + { + "subname": "Model", + "components": [ + { + "field": "variables.markerAttachments.model.enabled", + "name": "Enabled", + "description": "Render a model in the 3D Globe for the point features of this layer.", + "type": "switch", + "width": 3, + "defaultChecked": false + } + ] + }, + { + "components": [ + { + "field": "variables.markerAttachments.model.path", + "name": "Model URL", + "description": "A path to model. The following model formats are supported: .dae, .glb, .gltf, and .obj.", + "type": "text", + "width": 9 + }, + { + "field": "variables.markerAttachments.model.pathProp", + "name": "Model URL Property", + "description": "A prop.path to a model. This takes priority over 'Model URL' and is useful if model is feature specific.", + "type": "text", + "width": 3 + } + ] + }, + { + "components": [ + { + "field": "variables.markerAttachments.model.yawProp", + "name": "Yaw Property", + "description": "Prop path to the model's yaw. If this value is a number, uses it directly.", + "type": "text", + "width": 8 + }, + { + "field": "variables.markerAttachments.model.yawUnit", + "name": "Yaw Unit", + "description": "The units of 'Yaw Property'.", + "type": "dropdown", + "width": 2, + "options": ["deg", "rad"] + }, + { + "field": "variables.markerAttachments.model.invertYaw", + "name": "Invert Yaw", + "description": "If true, multiplies yaw by -1.", + "type": "checkbox", + "width": 2, + "defaultChecked": false + } + ] + }, + { + "components": [ + { + "field": "variables.markerAttachments.model.pitchProp", + "name": "Pitch Property", + "description": "Prop path to the model's pitch. If this value is a number, uses it directly.", + "type": "text", + "width": 8 + }, + { + "field": "variables.markerAttachments.model.pitchUnit", + "name": "Pitch Unit", + "description": "The units of 'Pitch Property'.", + "type": "dropdown", + "width": 2, + "options": ["deg", "rad"] + }, + { + "field": "variables.markerAttachments.model.invertPitch", + "name": "Invert Pitch", + "description": "If true, multiplies pitch by -1.", + "type": "checkbox", + "width": 2, + "defaultChecked": false + } + ] + }, + { + "components": [ + { + "field": "variables.markerAttachments.model.rollProp", + "name": "Roll Property", + "description": "Prop path to the model's roll. If this value is a number, uses it directly.", + "type": "text", + "width": 8 + }, + { + "field": "variables.markerAttachments.model.rollUnit", + "name": "Roll Unit", + "description": "The units of 'Roll Property'.", + "type": "dropdown", + "width": 2, + "options": ["deg", "rad"] + }, + { + "field": "variables.markerAttachments.model.invertRoll", + "name": "Invert Roll", + "description": "If true, multiplies roll by -1.", + "type": "checkbox", + "width": 2, + "defaultChecked": false + } + ] + }, + { + "components": [ + { + "field": "variables.markerAttachments.model.elevationProp", + "name": "Elevation Property", + "description": "Prop path to the model's elevation (in meters). If this value is a number, uses it directly. Default 0.", + "type": "text", + "width": 4 + }, + { + "field": "variables.markerAttachments.model.scaleProp", + "name": "Scale Property", + "description": "Prop path to the model's scale. If this value is a number, uses it directly. Default 1.", + "type": "text", + "width": 4 + }, + { + "field": "variables.markerAttachments.model.show", + "name": "Shown", + "description": "If set to 'always', always renders the model at the marker. 'click' just shows the model on-click", + "type": "dropdown", + "width": 2, + "options": ["click", "always"] + }, + { + "field": "variables.markerAttachments.model.onlyLastN", + "name": "Only on Last N", + "description": "If 0, shows models at all points. If a number, only shows models for the last n points of the layer.", + "type": "number", + "min": 0, + "width": 2 + } + ] + }, + { + "subname": "Uncertainty Ellipses", + "components": [ + { + "field": "variables.markerAttachments.uncertainty.enabled", + "name": "Enabled", + "description": "Turns on a sublayer feature that places ellipses about point features in order to indicate positional uncertainties.", + "type": "switch", + "width": 3, + "defaultChecked": false + } + ] + }, + { + "components": [ + { + "field": "variables.markerAttachments.uncertainty.initialVisibility", + "name": "Initial Visibility", + "description": "Whether the uncertainty sublayer is initially on. Users can toggle sublayers on and off in the layer settings in the LayersTool.", + "type": "checkbox", + "width": 2, + "defaultChecked": false + }, + { + "field": "variables.markerAttachments.uncertainty.strokeColor", + "name": "Stroke Color", + "description": "A stroke/border color for the uncertainty ellipse. Defaults to 'black'.", + "type": "colorpicker", + "width": 2 + }, + { + "field": "variables.markerAttachments.uncertainty.opacity", + "name": "Stroke Opacity", + "description": "A stroke/border opacity for the uncertainty ellipse. 0 (transparent) to 1 (opaque).", + "type": "number", + "min": 0, + "max": 1, + "width": 2 + }, + { + "field": "variables.markerAttachments.uncertainty.color", + "name": "Fill Color", + "description": "A fill color for the uncertainty ellipse. It will appear more transparent than set. Defaults to 'white'.", + "type": "colorpicker", + "width": 2 + }, + { + "field": "variables.markerAttachments.uncertainty.fillOpacity", + "name": "Fill Opacity", + "description": "A fill opacity for the uncertainty ellipse. 0 (transparent) to 1 (opaque).", + "type": "number", + "min": 0, + "max": 1, + "width": 2 + } + ] + }, + { + "components": [ + { + "field": "variables.markerAttachments.uncertainty.weight", + "name": "Weight", + "description": "A stroke/border weight for the uncertainty ellipse.", + "type": "number", + "min": 1, + "width": 2 + }, + { + "field": "variables.markerAttachments.uncertainty.color3d", + "name": "3D Color", + "description": "A color for the ellipse's vertical curtain in the 3D Globe. Can be an array for a vertical gradient: 'rgba(0,0,0,0), #26A8FF'.", + "type": "textarray", + "width": 4 + }, + { + "field": "variables.markerAttachments.uncertainty.opacity3d", + "name": "3D Opacity", + "description": "A fill opacity for the ellipse's vertical curtain in the 3D Globe. 0 (transparent) to 1 (opaque).", + "type": "number", + "min": 0, + "max": 1, + "width": 2 + }, + { + "field": "variables.markerAttachments.uncertainty.depth3d", + "name": "3D Depth", + "description": "A depth, in meters, for the ellipse's vertical curtain in the 3D Globe. Defaults to 2.", + "type": "number", + "min": 0, + "width": 2 + } + ] + }, + { + "components": [ + { + "field": "variables.markerAttachments.uncertainty.xAxisProp", + "name": "X-Axis Property", + "description": "Prop path to the x-axis radius value of the ellipse.", + "type": "text", + "width": 5 + }, + { + "field": "variables.markerAttachments.uncertainty.yAxisProp", + "name": "Y-Axis Property", + "description": "Prop path to the y-axis radius value of the ellipse.", + "type": "text", + "width": 5 + }, + { + "field": "variables.markerAttachments.uncertainty.axisUnit", + "name": "Axis Unit", + "description": "The units of 'X-Axis Property' and 'Y-Axis Property'.", + "type": "dropdown", + "width": 2, + "options": ["meters", "kilometers"] + } + ] + }, + { + "components": [ + { + "field": "variables.markerAttachments.uncertainty.angleProp", + "name": "Angle Property", + "description": "Prop path to the rotation of the ellipse.", + "type": "text", + "width": 5 + }, + { + "field": "variables.markerAttachments.uncertainty.angleUnit", + "name": "Angle Unit", + "description": "The units of 'Angle Property'.", + "type": "dropdown", + "width": 2, + "options": ["deg", "rad"] + } + ] + } + ] + }, + { + "name": "Attachment - Paths", + "rows": [ + { + "name": "Attachment - Paths", + "subname": "Gradient", + "components": [ + { + "field": "variables.pathAttachments.gradient.enabled", + "name": "Enabled", + "description": "Renders a 'hotline' gradient for visualizing value changes along a path. Useful with 'Miscellaneous -> Hide Main Features'. Look into 'Enhanced GeoJSON' in the docs on how to format layers with properties-per-coordinate.", + "type": "switch", + "width": 3, + "defaultChecked": false + } + ] + }, + { + "components": [ + { + "field": "variables.pathAttachments.gradient.colorWithProp", + "name": "Color With Property", + "description": "Prop path to the value's key to visualize.", + "type": "text", + "width": 3 + }, + { + "field": "variables.pathAttachments.gradient.dropdownColorWithProp", + "name": "Dropdown Color With Properties", + "description": "Array of additional prop paths. Users can toggle between value properties in the LayersTool. 'Color With Property's path is automatically added here if not already included.", + "type": "textarray", + "width": 9 + } + ] + }, + { + "components": [ + { + "field": "variables.pathAttachments.gradient.colorRamp", + "name": "Color Ramp", + "description": "Array of css colors indicating the color ramp. The first color represents the min value and the last color represents the max value in the layer.", + "type": "textarray", + "width": 10 + }, + { + "field": "variables.pathAttachments.gradient.weight", + "name": "Weight", + "description": "Line thickness in pixels.", + "type": "number", + "min": 1, + "width": 2 + } + ] + } + ] + }, + { + "name": "Miscellaneous", + "rows": [ + { + "name": "Miscellaneous", + "subname": "Chemistry", + "components": [ + { + "field": "variables.chemistry", + "name": "Chemistry Values", + "description": "Comma-separated chemistry columns to use as percentages and it what order. Should be used with a Dataset. Look into the docs for the ChemistryTool for more information.", + "type": "textarray", + "width": 12 + } + ] + }, + { + "components": [ + { + "field": "variables.hideMainFeature", + "name": "Hide Main Features", + "description": " If true, hides all typically rendered features. This is useful if showing only the 'Attachments' of layers is desired and not their features themselves. ", + "type": "checkbox", + "width": 3, + "defaultChecked": false + } + ] + } + ] + } + ] +} diff --git a/configure/src/metaconfigs/meta-config.json b/configure/src/metaconfigs/meta-config.json deleted file mode 100644 index e69de29b..00000000 diff --git a/configure/src/pages/Datasets/Datasets.js b/configure/src/pages/Datasets/Datasets.js index 3b3231e7..5894c1d0 100644 --- a/configure/src/pages/Datasets/Datasets.js +++ b/configure/src/pages/Datasets/Datasets.js @@ -109,16 +109,10 @@ const useStyles = makeStyles((theme) => ({ }, }, tableInner: { - margin: "53px", - width: "calc(100% - 106px) !important", + margin: "32px", + width: "calc(100% - 64px) !important", boxShadow: "0px 1px 7px 0px rgba(0, 0, 0, 0.2)", }, - flex: { - display: "flex", - "& > svg": { - margin: "3px 10px 0px 2px", - }, - }, actions: { display: "flex", justifyContent: "right", @@ -172,15 +166,23 @@ const useStyles = makeStyles((theme) => ({ topbar: { width: "100%", height: "48px", - minHeight: "48px", + minHeight: "48px !important", display: "flex", justifyContent: "space-between", - background: theme.palette.swatches.grey[850], + background: theme.palette.swatches.grey[1000], boxShadow: `inset 10px 0px 10px -5px rgba(0,0,0,0.3)`, - borderBottom: `1px solid ${theme.palette.swatches.grey[900]} !important`, + borderBottom: `2px solid ${theme.palette.swatches.grey[800]} !important`, padding: `0px 20px`, boxSizing: `border-box !important`, }, + topbarTitle: { + display: "flex", + color: theme.palette.accent.main, + "& > svg": { + color: theme.palette.swatches.grey[150], + margin: "3px 10px 0px 2px", + }, + }, bottomBar: { background: theme.palette.swatches.grey[850], boxShadow: "inset 10px 0px 10px -5px rgba(0,0,0,0.3)", @@ -189,9 +191,9 @@ const useStyles = makeStyles((theme) => ({ fontWeight: "bold !important", textTransform: "uppercase", letterSpacing: "1px !important", - color: `${theme.palette.swatches.grey[850]} !important`, - backgroundColor: `${theme.palette.swatches.grey[150]} !important`, - borderRight: `1px solid ${theme.palette.swatches.grey[400]}`, + color: `${theme.palette.accent.main} !important`, + backgroundColor: `${theme.palette.swatches.grey[1000]} !important`, + borderRight: `1px solid ${theme.palette.swatches.grey[900]}`, }, })); @@ -262,12 +264,12 @@ function EnhancedTableToolbar(props) { return ( -
+
DATASETS @@ -294,7 +296,7 @@ EnhancedTableToolbar.propTypes = { export default function Datasets() { const [order, setOrder] = React.useState("asc"); - const [orderBy, setOrderBy] = React.useState("calories"); + const [orderBy, setOrderBy] = React.useState("name"); const [page, setPage] = React.useState(0); const [rowsPerPage, setRowsPerPage] = React.useState(25); diff --git a/configure/src/pages/Datasets/Modals/AppendGeoDatasetModal/AppendGeoDatasetModal.js b/configure/src/pages/Datasets/Modals/AppendGeoDatasetModal/AppendGeoDatasetModal.js index 2c5dd2d3..1a52f3e4 100644 --- a/configure/src/pages/Datasets/Modals/AppendGeoDatasetModal/AppendGeoDatasetModal.js +++ b/configure/src/pages/Datasets/Modals/AppendGeoDatasetModal/AppendGeoDatasetModal.js @@ -79,7 +79,6 @@ const useStyles = makeStyles((theme) => ({ subtitle: { fontSize: "14px !important", width: "100%", - textAlign: "right", marginBottom: "8px !important", color: theme.palette.swatches.grey[300], letterSpacing: "0.2px", diff --git a/configure/src/pages/Datasets/Modals/DeleteGeoDatasetModal/DeleteGeoDatasetModal.js b/configure/src/pages/Datasets/Modals/DeleteGeoDatasetModal/DeleteGeoDatasetModal.js index e77ed875..16133650 100644 --- a/configure/src/pages/Datasets/Modals/DeleteGeoDatasetModal/DeleteGeoDatasetModal.js +++ b/configure/src/pages/Datasets/Modals/DeleteGeoDatasetModal/DeleteGeoDatasetModal.js @@ -72,7 +72,6 @@ const useStyles = makeStyles((theme) => ({ subtitle: { fontSize: "14px !important", width: "100%", - textAlign: "right", marginBottom: "8px !important", color: theme.palette.swatches.grey[300], letterSpacing: "0.2px", diff --git a/configure/src/pages/Datasets/Modals/NewDatasetModal/NewDatasetModal.js b/configure/src/pages/Datasets/Modals/NewDatasetModal/NewDatasetModal.js index 6c0edb03..4c57ba9d 100644 --- a/configure/src/pages/Datasets/Modals/NewDatasetModal/NewDatasetModal.js +++ b/configure/src/pages/Datasets/Modals/NewDatasetModal/NewDatasetModal.js @@ -82,7 +82,6 @@ const useStyles = makeStyles((theme) => ({ subtitle: { fontSize: "14px !important", width: "100%", - textAlign: "right", marginBottom: "8px !important", color: theme.palette.swatches.grey[300], letterSpacing: "0.2px", diff --git a/configure/src/pages/Datasets/Modals/UpdateGeoDatasetModal/UpdateGeoDatasetModal.js b/configure/src/pages/Datasets/Modals/UpdateGeoDatasetModal/UpdateGeoDatasetModal.js index fc50a8a8..49905d69 100644 --- a/configure/src/pages/Datasets/Modals/UpdateGeoDatasetModal/UpdateGeoDatasetModal.js +++ b/configure/src/pages/Datasets/Modals/UpdateGeoDatasetModal/UpdateGeoDatasetModal.js @@ -79,7 +79,6 @@ const useStyles = makeStyles((theme) => ({ subtitle: { fontSize: "14px !important", width: "100%", - textAlign: "right", marginBottom: "8px !important", color: theme.palette.swatches.grey[300], letterSpacing: "0.2px", diff --git a/configure/src/pages/GeoDatasets/GeoDatasets.js b/configure/src/pages/GeoDatasets/GeoDatasets.js index c8033b75..dee4d482 100644 --- a/configure/src/pages/GeoDatasets/GeoDatasets.js +++ b/configure/src/pages/GeoDatasets/GeoDatasets.js @@ -114,16 +114,10 @@ const useStyles = makeStyles((theme) => ({ }, }, tableInner: { - margin: "53px", - width: "calc(100% - 106px) !important", + margin: "32px", + width: "calc(100% - 64px) !important", boxShadow: "0px 1px 7px 0px rgba(0, 0, 0, 0.2)", }, - flex: { - display: "flex", - "& > svg": { - margin: "3px 10px 0px 2px", - }, - }, actions: { display: "flex", justifyContent: "right", @@ -177,15 +171,23 @@ const useStyles = makeStyles((theme) => ({ topbar: { width: "100%", height: "48px", - minHeight: "48px", + minHeight: "48px !important", display: "flex", justifyContent: "space-between", - background: theme.palette.swatches.grey[850], + background: theme.palette.swatches.grey[1000], boxShadow: `inset 10px 0px 10px -5px rgba(0,0,0,0.3)`, - borderBottom: `1px solid ${theme.palette.swatches.grey[900]} !important`, + borderBottom: `2px solid ${theme.palette.swatches.grey[800]} !important`, padding: `0px 20px`, boxSizing: `border-box !important`, }, + topbarTitle: { + display: "flex", + color: theme.palette.accent.main, + "& > svg": { + color: theme.palette.swatches.grey[150], + margin: "3px 10px 0px 2px", + }, + }, bottomBar: { background: theme.palette.swatches.grey[850], boxShadow: "inset 10px 0px 10px -5px rgba(0,0,0,0.3)", @@ -194,9 +196,9 @@ const useStyles = makeStyles((theme) => ({ fontWeight: "bold !important", textTransform: "uppercase", letterSpacing: "1px !important", - color: `${theme.palette.swatches.grey[850]} !important`, - backgroundColor: `${theme.palette.swatches.grey[150]} !important`, - borderRight: `1px solid ${theme.palette.swatches.grey[400]}`, + color: `${theme.palette.accent.main} !important`, + backgroundColor: `${theme.palette.swatches.grey[1000]} !important`, + borderRight: `1px solid ${theme.palette.swatches.grey[900]}`, }, })); @@ -283,12 +285,12 @@ function EnhancedTableToolbar(props) { return ( -
+
GEODATASETS @@ -315,7 +317,7 @@ EnhancedTableToolbar.propTypes = { export default function GeoDatasets() { const [order, setOrder] = React.useState("asc"); - const [orderBy, setOrderBy] = React.useState("calories"); + const [orderBy, setOrderBy] = React.useState("name"); const [page, setPage] = React.useState(0); const [rowsPerPage, setRowsPerPage] = React.useState(25); diff --git a/configure/src/pages/GeoDatasets/Modals/AppendGeoDatasetModal/AppendGeoDatasetModal.js b/configure/src/pages/GeoDatasets/Modals/AppendGeoDatasetModal/AppendGeoDatasetModal.js index 2c5dd2d3..1a52f3e4 100644 --- a/configure/src/pages/GeoDatasets/Modals/AppendGeoDatasetModal/AppendGeoDatasetModal.js +++ b/configure/src/pages/GeoDatasets/Modals/AppendGeoDatasetModal/AppendGeoDatasetModal.js @@ -79,7 +79,6 @@ const useStyles = makeStyles((theme) => ({ subtitle: { fontSize: "14px !important", width: "100%", - textAlign: "right", marginBottom: "8px !important", color: theme.palette.swatches.grey[300], letterSpacing: "0.2px", diff --git a/configure/src/pages/GeoDatasets/Modals/DeleteGeoDatasetModal/DeleteGeoDatasetModal.js b/configure/src/pages/GeoDatasets/Modals/DeleteGeoDatasetModal/DeleteGeoDatasetModal.js index e77ed875..16133650 100644 --- a/configure/src/pages/GeoDatasets/Modals/DeleteGeoDatasetModal/DeleteGeoDatasetModal.js +++ b/configure/src/pages/GeoDatasets/Modals/DeleteGeoDatasetModal/DeleteGeoDatasetModal.js @@ -72,7 +72,6 @@ const useStyles = makeStyles((theme) => ({ subtitle: { fontSize: "14px !important", width: "100%", - textAlign: "right", marginBottom: "8px !important", color: theme.palette.swatches.grey[300], letterSpacing: "0.2px", diff --git a/configure/src/pages/GeoDatasets/Modals/NewGeoDatasetModal/NewGeoDatasetModal.js b/configure/src/pages/GeoDatasets/Modals/NewGeoDatasetModal/NewGeoDatasetModal.js index 848810f2..3b6d267f 100644 --- a/configure/src/pages/GeoDatasets/Modals/NewGeoDatasetModal/NewGeoDatasetModal.js +++ b/configure/src/pages/GeoDatasets/Modals/NewGeoDatasetModal/NewGeoDatasetModal.js @@ -79,7 +79,6 @@ const useStyles = makeStyles((theme) => ({ subtitle: { fontSize: "14px !important", width: "100%", - textAlign: "right", marginBottom: "8px !important", color: theme.palette.swatches.grey[300], letterSpacing: "0.2px", diff --git a/configure/src/pages/GeoDatasets/Modals/UpdateGeoDatasetModal/UpdateGeoDatasetModal.js b/configure/src/pages/GeoDatasets/Modals/UpdateGeoDatasetModal/UpdateGeoDatasetModal.js index fc50a8a8..49905d69 100644 --- a/configure/src/pages/GeoDatasets/Modals/UpdateGeoDatasetModal/UpdateGeoDatasetModal.js +++ b/configure/src/pages/GeoDatasets/Modals/UpdateGeoDatasetModal/UpdateGeoDatasetModal.js @@ -79,7 +79,6 @@ const useStyles = makeStyles((theme) => ({ subtitle: { fontSize: "14px !important", width: "100%", - textAlign: "right", marginBottom: "8px !important", color: theme.palette.swatches.grey[300], letterSpacing: "0.2px", diff --git a/configure/src/pages/WebHooks/Modals/AppendGeoDatasetModal/AppendGeoDatasetModal.js b/configure/src/pages/WebHooks/Modals/AppendGeoDatasetModal/AppendGeoDatasetModal.js deleted file mode 100644 index 2c5dd2d3..00000000 --- a/configure/src/pages/WebHooks/Modals/AppendGeoDatasetModal/AppendGeoDatasetModal.js +++ /dev/null @@ -1,368 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { useSelector, useDispatch } from "react-redux"; - -import { calls } from "../../../../core/calls"; - -import { - setMissions, - setModal, - setSnackBarText, -} from "../../../../core/ConfigureStore"; - -import Typography from "@mui/material/Typography"; -import Button from "@mui/material/Button"; -import Dialog from "@mui/material/Dialog"; -import DialogActions from "@mui/material/DialogActions"; -import DialogContent from "@mui/material/DialogContent"; -import DialogTitle from "@mui/material/DialogTitle"; -import IconButton from "@mui/material/IconButton"; - -import CloseSharpIcon from "@mui/icons-material/CloseSharp"; -import ControlPointDuplicateIcon from "@mui/icons-material/ControlPointDuplicate"; -import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile"; - -import { useDropzone } from "react-dropzone"; - -import TextField from "@mui/material/TextField"; -import FormGroup from "@mui/material/FormGroup"; -import FormControlLabel from "@mui/material/FormControlLabel"; -import Checkbox from "@mui/material/Checkbox"; - -import { makeStyles, useTheme } from "@mui/styles"; -import useMediaQuery from "@mui/material/useMediaQuery"; - -const useStyles = makeStyles((theme) => ({ - Modal: { - margin: theme.headHeights[1], - [theme.breakpoints.down("xs")]: { - margin: "6px", - }, - "& .MuiDialog-container": { - height: "unset !important", - transform: "translateX(-50%) translateY(-50%)", - left: "50%", - top: "50%", - position: "absolute", - }, - }, - contents: { - background: theme.palette.primary.main, - height: "100%", - width: "600px", - }, - heading: { - height: theme.headHeights[2], - boxSizing: "border-box", - background: theme.palette.swatches.p[0], - borderBottom: `1px solid ${theme.palette.swatches.grey[800]}`, - padding: `4px ${theme.spacing(2)} 4px ${theme.spacing(4)} !important`, - }, - title: { - padding: `8px 0px`, - fontSize: theme.typography.pxToRem(16), - fontWeight: "bold", - textTransform: "uppercase", - }, - content: { - padding: "8px 16px 16px 16px !important", - height: `calc(100% - ${theme.headHeights[2]}px)`, - }, - closeIcon: { - padding: theme.spacing(1.5), - height: "100%", - margin: "4px 0px", - }, - flexBetween: { - display: "flex", - justifyContent: "space-between", - }, - subtitle: { - fontSize: "14px !important", - width: "100%", - textAlign: "right", - marginBottom: "8px !important", - color: theme.palette.swatches.grey[300], - letterSpacing: "0.2px", - }, - subtitle2: { - fontSize: "12px !important", - fontStyle: "italic", - width: "100%", - marginBottom: "8px !important", - color: theme.palette.swatches.grey[400], - }, - missionNameInput: { - width: "100%", - margin: "8px 0px 4px 0px !important", - }, - backgroundIcon: { - margin: "7px 8px 0px 0px", - }, - - fileName: { - textAlign: "center", - fontWeight: "bold", - letterSpacing: "1px", - marginBottom: "10px", - paddingBottom: "10px", - borderBottom: `1px solid ${theme.palette.swatches.grey[500]}`, - }, - dropzone: { - width: "100%", - minHeight: "100px", - margin: "16px 0px", - "& > div": { - flex: "1 1 0%", - display: "flex", - flexDirection: "column", - alignItems: "center", - padding: "20px", - borderWidth: "2px", - borderRadius: "2px", - borderColor: theme.palette.swatches.grey[300], - borderStyle: "dashed", - backgroundColor: theme.palette.swatches.grey[900], - color: theme.palette.swatches.grey[200], - outline: "none", - transition: "border 0.24s ease-in-out 0s", - "&:hover": { - borderColor: theme.palette.swatches.p[11], - }, - }, - }, - dropzoneMessage: { - textAlign: "center", - color: theme.palette.swatches.p[11], - "& > p:first-child": { fontWeight: "bold", letterSpacing: "1px" }, - "& > p:last-child": { fontSize: "14px", fontStyle: "italic" }, - }, - timeFields: { - display: "flex", - "& > div:first-child": { - marginRight: "5px", - }, - "& > div:last-child": { - marginLeft: "5px", - }, - }, -})); - -const MODAL_NAME = "appendGeoDataset"; -const AppendGeoDatasetModal = (props) => { - const { queryGeoDatasets } = props; - const c = useStyles(); - - const modal = useSelector((state) => state.core.modal[MODAL_NAME]); - - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down("md")); - - const dispatch = useDispatch(); - - const [geojson, setGeojson] = useState(null); - const [fileName, setFileName] = useState(null); - const [startTimeField, setStartTimeField] = useState(null); - const [endTimeField, setEndTimeField] = useState(null); - - useEffect(() => { - setStartTimeField(modal?.geoDataset?.start_time_field); - setEndTimeField(modal?.geoDataset?.end_time_field); - }, [JSON.stringify(modal)]); - - const handleClose = () => { - // close modal - dispatch(setModal({ name: MODAL_NAME, on: false })); - }; - const handleSubmit = () => { - const geoDatasetName = modal?.geoDataset?.name; - - if (geojson == null || fileName === null) { - dispatch( - setSnackBarText({ - text: "Please upload a file.", - severity: "error", - }) - ); - return; - } - - if (geoDatasetName === null) { - dispatch( - setSnackBarText({ - text: "No GeoDataset found to append to.", - severity: "error", - }) - ); - return; - } - const forceParams = { - filename: fileName, - }; - if (startTimeField) forceParams.start_prop = startTimeField; - if (endTimeField) forceParams.end_prop = endTimeField; - - calls.api( - "geodatasets_append", - { - urlReplacements: { - name: geoDatasetName, - }, - forceParams, - type: geojson.type, - features: geojson.features, - }, - (res) => { - if (res.status === "success") { - dispatch( - setSnackBarText({ - text: "Successfully appended to GeoDataset.", - severity: "success", - }) - ); - queryGeoDatasets(); - handleClose(); - } else { - dispatch( - setSnackBarText({ - text: res?.message || "Failed to append to GeoDataset.", - severity: "error", - }) - ); - } - }, - (res) => { - dispatch( - setSnackBarText({ - text: res?.message || "Failed to append to GeoDataset.", - severity: "error", - }) - ); - } - ); - }; - - // Dropzone initialization - const { - getRootProps, - getInputProps, - isDragActive, - isDragAccept, - isDragReject, - } = useDropzone({ - maxFiles: 1, - accept: { - "application/json": [".json", ".geojson"], - }, - onDropAccepted: (files) => { - const file = files[0]; - setFileName(file.name); - - const reader = new FileReader(); - reader.onload = (e) => { - setGeojson(JSON.parse(e.target.result)); - }; - reader.readAsText(file); - }, - onDropRejected: () => { - setFileName(null); - setGeojson(null); - }, - }); - - return ( - - -
-
- -
{`Append features to this GeoDataset: ${modal?.geoDataset?.name}`}
-
- - - -
-
- - - {`Appends the features of the uploaded file to the GeoDataset`} - -
-
- - {isDragAccept &&

All files will be accepted

} - {isDragReject &&

Some files will be rejected

} - {!isDragActive && ( -
-

Drag 'n' drop or click to select files...

-

Only *.json and *.geojson files are accepted.

-
- )} -
-
- -
- -
{fileName || "No File Selected"}
-
- -
-
- { - setStartTimeField(e.target.value); - }} - /> - - {`If this GeoDataset already has a Start Time Field attached, the name of that start time field inside each feature's "properties" object for which to create a temporal index for the geodataset. Take care in using time field names for the appended GeoJSON features that are different from that of the existing features.`} - -
-
- { - setEndTimeField(e.target.value); - }} - /> - - {`If this GeoDataset already has a End Time Field attached, the name of that end time field inside each feature's "properties" object for which to create a temporal index for the geodataset. Take care in using time field names for the appended GeoJSON features that are different from that of the existing features.`} - -
-
-
- - - -
- ); -}; - -export default AppendGeoDatasetModal; diff --git a/configure/src/pages/WebHooks/Modals/LayersUsedByModal/LayersUsedByModal.js b/configure/src/pages/WebHooks/Modals/LayersUsedByModal/LayersUsedByModal.js deleted file mode 100644 index 558bd613..00000000 --- a/configure/src/pages/WebHooks/Modals/LayersUsedByModal/LayersUsedByModal.js +++ /dev/null @@ -1,228 +0,0 @@ -import React, { useState } from "react"; -import { useSelector, useDispatch } from "react-redux"; - -import { calls } from "../../../../core/calls"; - -import { setModal, setSnackBarText } from "../../../../core/ConfigureStore"; - -import Typography from "@mui/material/Typography"; -import Button from "@mui/material/Button"; -import Dialog from "@mui/material/Dialog"; -import DialogActions from "@mui/material/DialogActions"; -import DialogContent from "@mui/material/DialogContent"; -import DialogTitle from "@mui/material/DialogTitle"; -import IconButton from "@mui/material/IconButton"; - -import CloseSharpIcon from "@mui/icons-material/CloseSharp"; -import ShapeLineIcon from "@mui/icons-material/ShapeLine"; -import WarningIcon from "@mui/icons-material/Warning"; -import DeleteForeverIcon from "@mui/icons-material/DeleteForever"; - -import TextField from "@mui/material/TextField"; - -import { makeStyles, useTheme } from "@mui/styles"; -import useMediaQuery from "@mui/material/useMediaQuery"; - -const useStyles = makeStyles((theme) => ({ - Modal: { - margin: theme.headHeights[1], - [theme.breakpoints.down("xs")]: { - margin: "6px", - }, - "& .MuiDialog-container": { - height: "unset !important", - transform: "translateX(-50%) translateY(-50%)", - left: "50%", - top: "50%", - position: "absolute", - }, - }, - contents: { - background: theme.palette.primary.main, - height: "100%", - width: "500px", - }, - heading: { - height: theme.headHeights[2], - boxSizing: "border-box", - background: theme.palette.swatches.p[0], - borderBottom: `1px solid ${theme.palette.swatches.grey[800]}`, - padding: `4px ${theme.spacing(2)} 4px ${theme.spacing(4)} !important`, - }, - title: { - padding: `8px 0px`, - fontSize: theme.typography.pxToRem(16), - fontWeight: "bold", - color: theme.palette.swatches.grey[0], - textTransform: "uppercase", - }, - content: { - padding: "8px 16px 16px 16px !important", - height: `calc(100% - ${theme.headHeights[2]}px)`, - }, - closeIcon: { - padding: theme.spacing(1.5), - height: "100%", - margin: "4px 0px", - }, - flexBetween: { - display: "flex", - justifyContent: "space-between", - }, - backgroundIcon: { - margin: "7px 8px 0px 0px", - }, - - fileName: { - textAlign: "center", - fontWeight: "bold", - letterSpacing: "1px", - marginBottom: "10px", - borderBottom: `1px solid ${theme.palette.swatches.grey[500]}`, - paddingBottom: "10px", - }, - - layerName: { - textAlign: "center", - fontSize: "24px !important", - letterSpacing: "1px !important", - color: theme.palette.swatches.grey[100], - fontWeight: "bold !important", - margin: "10px !important", - borderBottom: `1px solid ${theme.palette.swatches.grey[100]}`, - paddingBottom: "10px", - }, - hasOccurrencesTitle: { - margin: "10px", - display: "flex", - }, - hasOccurrences: { - fontStyle: "italic", - }, - mission: { - background: theme.palette.swatches.p[11], - color: theme.palette.swatches.grey[900], - height: "24px", - lineHeight: "24px", - padding: "0px 5px", - borderRadius: "3px", - display: "inline-block", - letterSpacing: "1px", - marginLeft: "20px", - }, - pathName: { - display: "flex", - marginLeft: "40px", - marginTop: "4px", - height: "24px", - lineHeight: "24px", - }, - path: { - color: theme.palette.swatches.grey[500], - }, - name: { - color: theme.palette.swatches.grey[100], - fontWeight: "bold", - }, - close: {}, -})); - -const MODAL_NAME = "layersUsedByGeoDataset"; -const LayersUsedByModal = (props) => { - const {} = props; - const c = useStyles(); - - const modal = useSelector((state) => state.core.modal[MODAL_NAME]); - - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down("md")); - - const dispatch = useDispatch(); - - const handleClose = () => { - // close modal - dispatch(setModal({ name: MODAL_NAME, on: false })); - }; - - let occurrences = []; - - if (modal?.geoDataset?.occurrences) - occurrences = Object.keys(modal?.geoDataset?.occurrences) - .map((mission) => { - const m = modal?.geoDataset?.occurrences[mission]; - if (m.length == 0) return null; - else { - const items = [
{mission}
]; - m.forEach((n) => { - items.push( -
-
- {`${n.path}.`.replaceAll(".", " ➔ ")} -
-
{n.name}
-
- ); - }); - return items; - } - }) - .filter(Boolean); - - return ( - - -
-
- -
GeoDataset is Used By
-
- - - -
-
- - {`${modal?.geoDataset?.name}`} - {occurrences.length > 0 ? ( - <> -
- - {`This GeoDataset is currently in use in the following layers:`} - -
-
{occurrences}
- - ) : ( -
- - {`This GeoDataset is not in use.`} - -
- )} -
- - - -
- ); -}; - -export default LayersUsedByModal; diff --git a/configure/src/pages/WebHooks/Modals/NewGeoDatasetModal/NewGeoDatasetModal.js b/configure/src/pages/WebHooks/Modals/NewGeoDatasetModal/NewGeoDatasetModal.js deleted file mode 100644 index 848810f2..00000000 --- a/configure/src/pages/WebHooks/Modals/NewGeoDatasetModal/NewGeoDatasetModal.js +++ /dev/null @@ -1,389 +0,0 @@ -import React, { useState } from "react"; -import { useSelector, useDispatch } from "react-redux"; - -import { calls } from "../../../../core/calls"; - -import { - setMissions, - setModal, - setSnackBarText, -} from "../../../../core/ConfigureStore"; - -import Typography from "@mui/material/Typography"; -import Button from "@mui/material/Button"; -import Dialog from "@mui/material/Dialog"; -import DialogActions from "@mui/material/DialogActions"; -import DialogContent from "@mui/material/DialogContent"; -import DialogTitle from "@mui/material/DialogTitle"; -import IconButton from "@mui/material/IconButton"; - -import CloseSharpIcon from "@mui/icons-material/CloseSharp"; -import ShapeLineIcon from "@mui/icons-material/ShapeLine"; -import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile"; - -import { useDropzone } from "react-dropzone"; - -import TextField from "@mui/material/TextField"; -import FormGroup from "@mui/material/FormGroup"; -import FormControlLabel from "@mui/material/FormControlLabel"; -import Checkbox from "@mui/material/Checkbox"; - -import { makeStyles, useTheme } from "@mui/styles"; -import useMediaQuery from "@mui/material/useMediaQuery"; - -const useStyles = makeStyles((theme) => ({ - Modal: { - margin: theme.headHeights[1], - [theme.breakpoints.down("xs")]: { - margin: "6px", - }, - "& .MuiDialog-container": { - height: "unset !important", - transform: "translateX(-50%) translateY(-50%)", - left: "50%", - top: "50%", - position: "absolute", - }, - }, - contents: { - background: theme.palette.primary.main, - height: "100%", - width: "600px", - }, - heading: { - height: theme.headHeights[2], - boxSizing: "border-box", - background: theme.palette.swatches.p[0], - borderBottom: `1px solid ${theme.palette.swatches.grey[800]}`, - padding: `4px ${theme.spacing(2)} 4px ${theme.spacing(4)} !important`, - }, - title: { - padding: `8px 0px`, - fontSize: theme.typography.pxToRem(16), - fontWeight: "bold", - textTransform: "uppercase", - }, - content: { - padding: "8px 16px 16px 16px !important", - height: `calc(100% - ${theme.headHeights[2]}px)`, - }, - closeIcon: { - padding: theme.spacing(1.5), - height: "100%", - margin: "4px 0px", - }, - flexBetween: { - display: "flex", - justifyContent: "space-between", - }, - subtitle: { - fontSize: "14px !important", - width: "100%", - textAlign: "right", - marginBottom: "8px !important", - color: theme.palette.swatches.grey[300], - letterSpacing: "0.2px", - }, - subtitle2: { - fontSize: "12px !important", - fontStyle: "italic", - width: "100%", - marginBottom: "8px !important", - color: theme.palette.swatches.grey[400], - }, - missionNameInput: { - width: "100%", - margin: "8px 0px 4px 0px !important", - }, - backgroundIcon: { - margin: "7px 8px 0px 0px", - }, - - fileName: { - textAlign: "center", - fontWeight: "bold", - letterSpacing: "1px", - marginBottom: "10px", - borderBottom: `1px solid ${theme.palette.swatches.grey[500]}`, - paddingBottom: "10px", - }, - dropzone: { - width: "100%", - minHeight: "100px", - margin: "16px 0px", - "& > div": { - flex: "1 1 0%", - display: "flex", - flexDirection: "column", - alignItems: "center", - padding: "20px", - borderWidth: "2px", - borderRadius: "2px", - borderColor: theme.palette.swatches.grey[300], - borderStyle: "dashed", - backgroundColor: theme.palette.swatches.grey[900], - color: theme.palette.swatches.grey[200], - outline: "none", - transition: "border 0.24s ease-in-out 0s", - "&:hover": { - borderColor: theme.palette.swatches.p[11], - }, - }, - }, - dropzoneMessage: { - textAlign: "center", - color: theme.palette.swatches.p[11], - "& > p:first-child": { fontWeight: "bold", letterSpacing: "1px" }, - "& > p:last-child": { fontSize: "14px", fontStyle: "italic" }, - }, - timeFields: { - display: "flex", - "& > div:first-child": { - marginRight: "5px", - }, - "& > div:last-child": { - marginLeft: "5px", - }, - }, -})); - -const MODAL_NAME = "newGeoDataset"; -const NewGeoDatasetModal = (props) => { - const { queryGeoDatasets } = props; - const c = useStyles(); - - const modal = useSelector((state) => state.core.modal[MODAL_NAME]); - const geodatasets = useSelector((state) => state.core.geodatasets); - - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down("md")); - - const dispatch = useDispatch(); - - const [geoDatasetName, setGeoDatasetName] = useState(null); - const [startTimeField, setStartTimeField] = useState(null); - const [endTimeField, setEndTimeField] = useState(null); - const [fileName, setFileName] = useState(null); - const [geojson, setGeojson] = useState(null); - - const handleClose = () => { - // close modal - dispatch(setModal({ name: MODAL_NAME, on: false })); - }; - const handleSubmit = () => { - if (geojson == null || fileName === null) { - dispatch( - setSnackBarText({ - text: "Please upload a file.", - severity: "error", - }) - ); - return; - } - - if (geoDatasetName == null || geoDatasetName == "") { - dispatch( - setSnackBarText({ - text: "Please enter a name for the new GeoDataset.", - severity: "error", - }) - ); - return; - } - - for (let i = 0; i < geodatasets.length; i++) { - if (geodatasets[i].name === geoDatasetName) { - dispatch( - setSnackBarText({ - text: "GeoDataset name already exists.", - severity: "error", - }) - ); - return; - } - } - - if (geoDatasetName.match(/[|\\/~^:,;?!&%$@#*+\{\}\[\]<>]/)) { - dispatch( - setSnackBarText({ - text: "GeoDataset names cannot contain the following symbols: |\\/~^:,;?!&%$@#*+.{}[]<>", - severity: "error", - }) - ); - return; - } - - calls.api( - "geodatasets_recreate", - { - name: geoDatasetName, - startProp: startTimeField, - endProp: endTimeField, - geojson: geojson, - filename: fileName, - }, - (res) => { - if (res.status === "success") { - dispatch( - setSnackBarText({ - text: "Successfully created a new GeoDataset.", - severity: "success", - }) - ); - queryGeoDatasets(); - handleClose(); - } else { - dispatch( - setSnackBarText({ - text: res?.message || "Failed to create a GeoDataset.", - severity: "error", - }) - ); - } - }, - (res) => { - dispatch( - setSnackBarText({ - text: res?.message || "Failed to create a GeoDataset.", - severity: "error", - }) - ); - } - ); - }; - - // Dropzone initialization - const { - getRootProps, - getInputProps, - isDragActive, - isDragAccept, - isDragReject, - } = useDropzone({ - maxFiles: 1, - accept: { - "application/json": [".json", ".geojson"], - }, - onDropAccepted: (files) => { - const file = files[0]; - setFileName(file.name); - - const reader = new FileReader(); - reader.onload = (e) => { - setGeojson(e.target.result); - }; - reader.readAsText(file); - }, - onDropRejected: () => { - setFileName(null); - setGeojson(null); - }, - }); - - return ( - - -
-
- -
Make a New GeoDataset
-
- - - -
-
- - - {`GeoDatasets are .geojson/.json files that get stored in MMGIS' spatial database. Set a layer's "URL" to "geodatasets:{geodataset_name}" to use it.`} - -
-
- - {isDragAccept &&

All files will be accepted

} - {isDragReject &&

Some files will be rejected

} - {!isDragActive && ( -
-

Drag 'n' drop or click to select files...

-

Only *.json and *.geojson files are accepted.

-
- )} -
-
- -
- -
{fileName || "No File Selected"}
-
- { - setGeoDatasetName(e.target.value); - }} - /> - - {`A new and unique name for a GeoDataset. No special characters allowed.`} - - -
-
- { - setStartTimeField(e.target.value); - }} - /> - - {`(Optional) The name of a start time field inside each feature's "properties" object for which to create a temporal index for the geodataset. This enables time queries on GeoDatasets. The value here can use dot.notation in the case the time property is nested in the properties object. This field cannot be changed after the GeoDataset is created. If there is only one time to query upon, and not a time range, use End Time Field for that main time.`} - -
-
- { - setEndTimeField(e.target.value); - }} - /> - - {`(Optional) The name of an end time field inside each feature's "properties" object for which to create a temporal index for the geodataset. This enables time queries on GeoDatasets. The value here can use dot.notation in the case the time property is nested in the properties object. This field cannot be changed after the GeoDataset is created. If there is only one time to query upon, and not a time range, use End Time Field for that main time.`} - -
-
-
- - - -
- ); -}; - -export default NewGeoDatasetModal; diff --git a/configure/src/pages/WebHooks/Modals/PreviewGeoDatasetModal/PreviewGeoDatasetModal.js b/configure/src/pages/WebHooks/Modals/PreviewGeoDatasetModal/PreviewGeoDatasetModal.js deleted file mode 100644 index 61c09848..00000000 --- a/configure/src/pages/WebHooks/Modals/PreviewGeoDatasetModal/PreviewGeoDatasetModal.js +++ /dev/null @@ -1,160 +0,0 @@ -import React, { useState, useEffect } from "react"; -import { useSelector, useDispatch } from "react-redux"; - -import { calls } from "../../../../core/calls"; - -import { setModal, setSnackBarText } from "../../../../core/ConfigureStore"; - -import Typography from "@mui/material/Typography"; -import Button from "@mui/material/Button"; -import Dialog from "@mui/material/Dialog"; -import DialogActions from "@mui/material/DialogActions"; -import DialogContent from "@mui/material/DialogContent"; -import DialogTitle from "@mui/material/DialogTitle"; -import IconButton from "@mui/material/IconButton"; - -import CloseSharpIcon from "@mui/icons-material/CloseSharp"; -import PreviewIcon from "@mui/icons-material/Preview"; - -import { makeStyles, useTheme } from "@mui/styles"; -import useMediaQuery from "@mui/material/useMediaQuery"; -import Map from "../../../../components/Map/Map"; - -const useStyles = makeStyles((theme) => ({ - Modal: { - [theme.breakpoints.down("xs")]: { - margin: "6px", - }, - "& .MuiDialog-container": { - width: "100%", - transform: "translateX(-50%) translateY(-50%)", - left: "50%", - top: "50%", - position: "absolute", - }, - "& .MuiPaper-root": { - margin: "0px", - }, - }, - contents: { - height: "100%", - width: "100%", - maxWidth: "calc(100vw - 32px) !important", - maxHeight: "calc(100vh - 32px) !important", - }, - heading: { - height: theme.headHeights[2], - boxSizing: "border-box", - background: theme.palette.swatches.p[0], - padding: `4px ${theme.spacing(2)} 4px ${theme.spacing(4)} !important`, - }, - title: { - padding: `8px 0px`, - fontSize: theme.typography.pxToRem(16), - fontWeight: "bold", - textTransform: "uppercase", - }, - content: { - padding: "0px !important", - height: `calc(100vh - 32px)`, - background: theme.palette.swatches.grey[100], - }, - closeIcon: { - padding: theme.spacing(1.5), - height: "100%", - margin: "4px 0px", - }, - flexBetween: { - display: "flex", - justifyContent: "space-between", - }, - backgroundIcon: { - margin: "7px 8px 0px 0px", - }, -})); - -const MODAL_NAME = "previewGeoDataset"; -const PreviewGeoDatasetModal = (props) => { - const {} = props; - const c = useStyles(); - - const modal = useSelector((state) => state.core.modal[MODAL_NAME]); - const [geoDataset, setGeoDataset] = useState(null); - - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down("md")); - - const dispatch = useDispatch(); - - const handleClose = () => { - // close modal - dispatch(setModal({ name: MODAL_NAME, on: false })); - }; - - const queryGeoDataset = () => { - if (modal?.geoDataset?.name) - calls.api( - "geodatasets_get", - { - layer: modal.geoDataset.name, - }, - (res) => { - setGeoDataset(res); - }, - (res) => { - dispatch( - setSnackBarText({ - text: res?.message || "Failed to get geodataset.", - severity: "error", - }) - ); - } - ); - }; - useEffect(() => { - queryGeoDataset(); - }, [modal?.geoDataset?.name]); - - return ( - - -
-
- -
{`Previewing GeoDataset${ - modal?.geoDataset?.name ? `: ${modal.geoDataset.name}` : "" - }`}
-
- - - -
-
- - - -
- ); -}; - -export default PreviewGeoDatasetModal; diff --git a/configure/src/pages/WebHooks/WebHooks.js b/configure/src/pages/WebHooks/WebHooks.js index 79f358e4..221faef6 100644 --- a/configure/src/pages/WebHooks/WebHooks.js +++ b/configure/src/pages/WebHooks/WebHooks.js @@ -2,626 +2,95 @@ import React, { useEffect, useState } from "react"; import { useSelector, useDispatch } from "react-redux"; import { makeStyles } from "@mui/styles"; -import clsx from "clsx"; - import { calls } from "../../core/calls"; -import { downloadObject } from "../../core/utils"; -import { - setSnackBarText, - setGeodatasets, - setModal, -} from "../../core/ConfigureStore"; - -import PropTypes from "prop-types"; -import { alpha } from "@mui/material/styles"; -import Box from "@mui/material/Box"; -import Table from "@mui/material/Table"; -import TableBody from "@mui/material/TableBody"; -import TableCell from "@mui/material/TableCell"; -import TableContainer from "@mui/material/TableContainer"; -import TableHead from "@mui/material/TableHead"; -import TablePagination from "@mui/material/TablePagination"; -import TableRow from "@mui/material/TableRow"; -import TableSortLabel from "@mui/material/TableSortLabel"; -import Toolbar from "@mui/material/Toolbar"; -import Typography from "@mui/material/Typography"; -import Paper from "@mui/material/Paper"; -import Checkbox from "@mui/material/Checkbox"; -import IconButton from "@mui/material/IconButton"; -import Button from "@mui/material/Button"; -import Tooltip from "@mui/material/Tooltip"; -import FormControlLabel from "@mui/material/FormControlLabel"; -import Switch from "@mui/material/Switch"; -import DeleteIcon from "@mui/icons-material/Delete"; -import FilterListIcon from "@mui/icons-material/FilterList"; -import Divider from "@mui/material/Divider"; -import Badge from "@mui/material/Badge"; -import { visuallyHidden } from "@mui/utils"; - -import InventoryIcon from "@mui/icons-material/Inventory"; -import PreviewIcon from "@mui/icons-material/Preview"; -import DownloadIcon from "@mui/icons-material/Download"; -import UploadIcon from "@mui/icons-material/Upload"; -import DriveFileRenameOutlineIcon from "@mui/icons-material/DriveFileRenameOutline"; -import DeleteForeverIcon from "@mui/icons-material/DeleteForever"; -import AddIcon from "@mui/icons-material/Add"; -import ShapeLineIcon from "@mui/icons-material/ShapeLine"; -import ControlPointDuplicateIcon from "@mui/icons-material/ControlPointDuplicate"; - -import NewGeoDatasetModal from "./Modals/NewGeoDatasetModal/NewGeoDatasetModal"; -import DeleteGeoDatasetModal from "./Modals/DeleteGeoDatasetModal/DeleteGeoDatasetModal"; -import LayersUsedByModal from "./Modals/LayersUsedByModal/LayersUsedByModal"; -import PreviewGeoDatasetModal from "./Modals/PreviewGeoDatasetModal/PreviewGeoDatasetModal"; -import AppendGeoDatasetModal from "./Modals/AppendGeoDatasetModal/AppendGeoDatasetModal"; -import UpdateGeoDatasetModal from "./Modals/UpdateGeoDatasetModal/UpdateGeoDatasetModal"; - -function descendingComparator(a, b, orderBy) { - if (b[orderBy] < a[orderBy]) { - return -1; - } - if (b[orderBy] > a[orderBy]) { - return 1; - } - return 0; -} - -function getComparator(order, orderBy) { - return order === "desc" - ? (a, b) => descendingComparator(a, b, orderBy) - : (a, b) => -descendingComparator(a, b, orderBy); -} - -// Since 2020 all major browsers ensure sort stability with Array.prototype.sort(). -// stableSort() brings sort stability to non-modern browsers (notably IE11). If you -// only support modern browsers you can replace stableSort(exampleArray, exampleComparator) -// with exampleArray.slice().sort(exampleComparator) -function stableSort(array, comparator) { - const stabilizedThis = array.map((el, index) => [el, index]); - stabilizedThis.sort((a, b) => { - const order = comparator(a[0], b[0]); - if (order !== 0) { - return order; - } - return a[1] - b[1]; - }); - return stabilizedThis.map((el) => el[0]); -} +import Maker from "../../core/Maker"; +import { setSnackBarText } from "../../core/ConfigureStore"; + +const config = { + rows: [ + { + name: "WebHooks", + description: + "Configures the available functionalities of the Map's and Globe's right-click context menu.", + components: [ + { + field: "webhooks", + name: "Context Menu Actions", + description: + "When right-clicking on the Map or Globe, a custom context-menu appears. By default it only offers 'Copy Coordinates'. By adding objects to the rightClickMenuActions array, entries can be added to the context-menu to send users to links with parameters populated with the current coordinates.", + type: "objectarray", + width: 12, + object: [ + { + field: "action", + name: "Action", + description: + "Optionally restrict some right-click menu actions to only be supported when clicking on a polygon.", + type: "dropdown", + width: 2, + options: ["DrawFileAdd", "DrawFileChange", "DrawFileDelete"], + }, + { + field: "type", + name: "HTTP Method", + description: + "Optionally restrict some right-click menu actions to only be supported when clicking on a polygon.", + type: "dropdown", + width: 2, + options: ["GET", "POST", "PUT", "DELETE", "PATCH"], + }, + { + field: "url", + name: "URL", + description: + "The text for this menu entry when users right-click.", + type: "text", + width: 2, + }, + { + field: "header", + name: "Header JSON", + description: + "Vector Tiles are styled differently than Vectors. Raw variables here takes an object that maps internal vector tile layer names to styles. All raw variables are optional.", + type: "json", + width: 12, + }, + { + field: "body", + name: "Body JSON", + description: + "Vector Tiles are styled differently than Vectors. Raw variables here takes an object that maps internal vector tile layer names to styles. All raw variables are optional.", + type: "json", + width: 12, + }, + ], + }, + ], + }, + ], +}; const useStyles = makeStyles((theme) => ({ - GeoDatasets: { width: "100%", height: "100%" }, - GeoDatasetsInner: { + WebHooks: { width: "100%", height: "100%", - display: "flex", - flexFlow: "column", - }, - table: { - flex: 1, overflowY: "auto", - "& tr": { - background: theme.palette.swatches.grey[850], - }, - "& td": { - borderRight: `1px solid ${theme.palette.swatches.grey[800]}`, - borderBottom: `1px solid ${theme.palette.swatches.grey[700]} !important`, - }, - "& td:first-child": { - fontWeight: "bold", - letterSpacing: "1px", - fontSize: "16px", - color: `${theme.palette.swatches.p[13]}`, - }, - }, - tableInner: { - margin: "53px", - width: "calc(100% - 106px) !important", - boxShadow: "0px 1px 7px 0px rgba(0, 0, 0, 0.2)", - }, - flex: { display: "flex", - "& > svg": { - margin: "3px 10px 0px 2px", - }, - }, - actions: { - display: "flex", - justifyContent: "right", - }, - inIcon: { - width: "40px !important", - height: "40px !important", - }, - previewIcon: { - width: "40px !important", - height: "40px !important", - }, - downloadIcon: { - marginRight: "4px !important", - width: "40px !important", - height: "40px !important", - }, - appendIcon: { - marginLeft: "4px !important", - width: "40px !important", - height: "40px !important", - }, - renameIcon: { - width: "40px !important", - height: "40px !important", - }, - updateIcon: { - marginRight: "4px !important", - width: "40px !important", - height: "40px !important", - }, - deleteIcon: { - marginLeft: "4px !important", - width: "40px !important", - height: "40px !important", - "&:hover": { - background: "#c43541 !important", - color: `${theme.palette.swatches.grey[900]} !important`, - }, - }, - addButton: { - whiteSpace: "nowrap", - padding: "5px 20px !important", - margin: "0px 10px !important", - }, - badge: { - "& > span": { - backgroundColor: `${theme.palette.swatches.p[11]} !important`, - }, - }, - topbar: { - width: "100%", - height: "48px", - minHeight: "48px", - display: "flex", - justifyContent: "space-between", - background: theme.palette.swatches.grey[850], - boxShadow: `inset 10px 0px 10px -5px rgba(0,0,0,0.3)`, - borderBottom: `1px solid ${theme.palette.swatches.grey[900]} !important`, - padding: `0px 20px`, - boxSizing: `border-box !important`, - }, - bottomBar: { - background: theme.palette.swatches.grey[850], - boxShadow: "inset 10px 0px 10px -5px rgba(0,0,0,0.3)", - }, - th: { - fontWeight: "bold !important", - textTransform: "uppercase", - letterSpacing: "1px !important", - color: `${theme.palette.swatches.grey[850]} !important`, - backgroundColor: `${theme.palette.swatches.grey[150]} !important`, - borderRight: `1px solid ${theme.palette.swatches.grey[400]}`, + background: theme.palette.swatches.grey[1000], + padding: "0px 32px 64px 32px", + boxSizing: "border-box", + backgroundImage: "url(configure/build/gridlines.png)", }, })); -const headCells = [ - { - id: "name", - label: "Name", - }, - { - id: "updated", - label: "Last Updated", - }, - { - id: "filename", - label: "Uploaded From", - }, - { - id: "num_features", - label: "# of Features", - }, - { - id: "start_time_field", - label: "Start Time Field", - }, - { - id: "end_time_field", - label: "End Time Field", - }, - { - id: "actions", - label: "", - }, -]; - -function EnhancedTableHead(props) { - const { order, orderBy, rowCount, onRequestSort } = props; - const createSortHandler = (property) => (event) => { - onRequestSort(event, property); - }; - +export default function WebHooks() { const c = useStyles(); - return ( - - - {headCells.map((headCell, idx) => ( - - - {headCell.label} - {orderBy === headCell.id ? ( - - {order === "desc" ? "sorted descending" : "sorted ascending"} - - ) : null} - - - ))} - - - ); -} - -EnhancedTableHead.propTypes = { - onRequestSort: PropTypes.func.isRequired, - onSelectAllClick: PropTypes.func.isRequired, - order: PropTypes.oneOf(["asc", "desc"]).isRequired, - orderBy: PropTypes.string.isRequired, - rowCount: PropTypes.number.isRequired, -}; - -function EnhancedTableToolbar(props) { - const c = useStyles(); const dispatch = useDispatch(); return ( - -
- - - GEODATASETS - -
- - -
- ); -} - -EnhancedTableToolbar.propTypes = { - numSelected: PropTypes.number.isRequired, -}; - -export default function GeoDatasets() { - const [order, setOrder] = React.useState("asc"); - const [orderBy, setOrderBy] = React.useState("calories"); - const [page, setPage] = React.useState(0); - const [rowsPerPage, setRowsPerPage] = React.useState(25); - - const c = useStyles(); - - const dispatch = useDispatch(); - const geodatasets = useSelector((state) => state.core.geodatasets); - - const queryGeoDatasets = () => { - calls.api( - "geodatasets_entries", - {}, - (res) => { - if (res.status === "success") - dispatch( - setGeodatasets( - res.body.entries.map((en, idx) => { - en.id = idx; - return en; - }) - ) - ); - else - dispatch( - setSnackBarText({ - text: res?.message || "Failed to get geodatasets.", - severity: "error", - }) - ); - }, - (res) => { - dispatch( - setSnackBarText({ - text: res?.message || "Failed to get geodatasets.", - severity: "error", - }) - ); - } - ); - }; - useEffect(() => { - queryGeoDatasets(); - }, []); - - const handleRequestSort = (event, property) => { - const isAsc = orderBy === property && order === "asc"; - setOrder(isAsc ? "desc" : "asc"); - setOrderBy(property); - }; - - const handleChangePage = (event, newPage) => { - setPage(newPage); - }; - - const handleChangeRowsPerPage = (event) => { - setRowsPerPage(parseInt(event.target.value, 10)); - setPage(0); - }; - - // Avoid a layout jump when reaching the last page with empty rows. - const emptyRows = - page > 0 ? Math.max(0, (1 + page) * rowsPerPage - geodatasets.length) : 0; - - const visibleRows = React.useMemo( - () => - stableSort(geodatasets, getComparator(order, orderBy)).slice( - page * rowsPerPage, - page * rowsPerPage + rowsPerPage - ), - [order, orderBy, page, rowsPerPage, geodatasets] - ); - - return ( - <> - - - - - - - - {visibleRows.map((row, index) => { - let numOccurrences = 0; - if (row.occurrences) { - Object.keys(row.occurrences).forEach((m) => { - numOccurrences += row.occurrences[m].length; - }); - } - - return ( - - {row.name} - - {row.updated - ? new Date(row.updated).toLocaleString() - : row.updated} - - {row.filename} - {row.num_features} - - {row.start_time_field} - - {row.end_time_field} - -
- - { - dispatch( - setModal({ - name: "layersUsedByGeoDataset", - geoDataset: row, - }) - ); - }} - > - - - - - - - { - dispatch( - setModal({ - name: "previewGeoDataset", - geoDataset: row, - }) - ); - }} - > - - - - - { - if (row.name) - calls.api( - "geodatasets_get", - { - layer: row.name, - }, - (res) => { - downloadObject( - res, - `${row.name}-geodataset`, - ".geojson" - ); - dispatch( - setSnackBarText({ - text: - res?.message || - "Successfully downloaded GeoDataset.", - severity: "success", - }) - ); - }, - (res) => { - dispatch( - setSnackBarText({ - text: - res?.message || - "Failed to download GeoDataset.", - severity: "error", - }) - ); - } - ); - }} - > - - - - - - { - dispatch( - setModal({ - name: "appendGeoDataset", - geoDataset: row, - }) - ); - }} - > - - - - {/* - - {}} - > - - - - */} - - { - dispatch( - setModal({ - name: "updateGeoDataset", - geoDataset: row, - }) - ); - }} - > - - - - - - - { - dispatch( - setModal({ - name: "deleteGeoDataset", - geoDataset: row, - }) - ); - }} - > - - - -
-
-
- ); - })} - {emptyRows > 0 && ( - - - - )} -
-
-
- -
-
- - - - - - - +
+ +
); } diff --git a/src/essence/Basics/Layers_/LayerConstructors.js b/src/essence/Basics/Layers_/LayerConstructors.js index 1ac42c40..c9c7af3b 100644 --- a/src/essence/Basics/Layers_/LayerConstructors.js +++ b/src/essence/Basics/Layers_/LayerConstructors.js @@ -280,7 +280,7 @@ export const constructVectorLayer = ( ``, ].join('\n') break - case 'directional_circle': + case 'directional-circle': svg = [ `
`, ``,