diff --git a/configure/src/components/SaveBar/SaveBar.js b/configure/src/components/SaveBar/SaveBar.js
index 074b57f1e..4721a1fce 100644
--- a/configure/src/components/SaveBar/SaveBar.js
+++ b/configure/src/components/SaveBar/SaveBar.js
@@ -51,6 +51,14 @@ const useStyles = makeStyles((theme) => ({
cursor: "not-allowed !important",
background: `${theme.palette.swatches.red[500]} !important`,
},
+ errorIndicator: {
+ width: "10px",
+ height: "10px",
+ borderRadius: "50%",
+ backgroundColor: theme.palette.error.main,
+ marginLeft: "6px",
+ display: "inline-block",
+ },
}));
export default function SaveBar() {
@@ -60,6 +68,8 @@ export default function SaveBar() {
const mission = useSelector((state) => state.core.mission);
const lockConfig = useSelector((state) => state.core.lockConfig);
+ const validationErrors = useSelector((state) => state.core.validationErrors);
+ const hasValidationErrors = validationErrors && validationErrors.length > 0;
return (
<>
@@ -77,6 +87,7 @@ export default function SaveBar() {
: null}
endIcon={}
onClick={() => {
dispatch(
diff --git a/configure/src/core/ConfigureStore.js b/configure/src/core/ConfigureStore.js
index 2fbc1789b..55a752a08 100644
--- a/configure/src/core/ConfigureStore.js
+++ b/configure/src/core/ConfigureStore.js
@@ -1,6 +1,7 @@
import { createSlice } from "@reduxjs/toolkit";
import { calls } from "./calls";
+import { getAllValidationErrors } from "./validators";
window.newUUIDCount = 0;
window.configId = parseInt(Math.random() * 100000);
@@ -17,6 +18,7 @@ export const ConfigureStore = createSlice({
stacCollections: [],
userEntries: [],
page: null,
+ validationErrors: [],
modal: {
newMission: false,
layer: false,
@@ -64,6 +66,8 @@ export const ConfigureStore = createSlice({
},
setConfiguration: (state, action) => {
state.configuration = action.payload;
+ // Update validation errors whenever configuration changes
+ state.validationErrors = getAllValidationErrors(action.payload);
},
setToolConfiguration: (state, action) => {
state.toolConfiguration = action.payload;
@@ -106,6 +110,9 @@ export const ConfigureStore = createSlice({
severity: action.payload.severity,
};
},
+ setValidationErrors: (state, action) => {
+ state.validationErrors = action.payload;
+ },
clearLockConfig: (state, action) => {
state.lockConfigTypes[action.payload.type || "main"] = false;
@@ -210,6 +217,7 @@ export const {
setPage,
setModal,
setSnackBarText,
+ setValidationErrors,
saveConfiguration,
clearLockConfig,
setLockConfig,
diff --git a/configure/src/core/Maker.js b/configure/src/core/Maker.js
index 6dea942ba..0689e39b7 100644
--- a/configure/src/core/Maker.js
+++ b/configure/src/core/Maker.js
@@ -24,6 +24,7 @@ import FormControl from "@mui/material/FormControl";
import Select from "@mui/material/Select";
import IconButton from "@mui/material/IconButton";
import Slider from "@mui/material/Slider";
+import FormHelperText from "@mui/material/FormHelperText";
import AddIcon from "@mui/icons-material/Add";
import CloseIcon from "@mui/icons-material/Close";
@@ -39,6 +40,7 @@ import {
getLayerByUUID,
isUrlAbsolute,
} from "./utils";
+import { isFieldRequired } from "./validators";
import Map from "../components/Map/Map";
import ColorButton from "../components/ColorButton/ColorButton";
@@ -237,6 +239,9 @@ const useStyles = makeStyles((theme) => ({
marginBottom: "-13px !important",
fontSize: "14px !important",
},
+ noMarginHelperText: {
+ marginLeft: "0px !important",
+ },
}));
const getComponent = (
@@ -269,6 +274,10 @@ const getComponent = (
}
disabled = !switchVal;
}
+ const isRequired = isFieldRequired(com, layer, configuration);
+ const fieldValue = value != null ? value : getIn(directConf, com.field, "");
+ const hasError = isRequired && (fieldValue === "" || fieldValue == null);
+
switch (com.type) {
case "gap":
return (
@@ -286,10 +295,16 @@ const getComponent = (
variant="filled"
size="small"
disabled={disabled}
+ required={isRequired}
+ error={hasError}
+ helperText={hasError ? "This field is required" : ""}
+ FormHelperTextProps={{
+ className: c.noMarginHelperText
+ }}
inputProps={{
autoComplete: "off",
}}
- value={value != null ? value : getIn(directConf, com.field, "")}
+ value={fieldValue}
onChange={(e) => {
updateConfiguration(forceField || com.field, e.target.value, layer);
}}
@@ -326,10 +341,16 @@ const getComponent = (
variant="filled"
size="small"
disabled={disabled}
+ required={isRequired}
+ error={hasError}
+ helperText={hasError ? "This field is required" : ""}
+ FormHelperTextProps={{
+ className: c.noMarginHelperText
+ }}
inputProps={{
autoComplete: "off",
}}
- value={value != null ? value : getIn(directConf, com.field, "")}
+ value={fieldValue}
onChange={(e) => {
updateConfiguration(forceField || com.field, e.target.value, layer);
}}
@@ -599,6 +620,8 @@ const getComponent = (
);
case "number":
+ const numberValue = value != null ? value : getIn(directConf, com.field, "");
+ const numberHasError = isRequired && (numberValue === "" || numberValue == null || isNaN(numberValue));
inner = (
{
let v = e.target.value;
if (v != "") {
diff --git a/configure/src/core/validators.js b/configure/src/core/validators.js
new file mode 100644
index 000000000..5cac17e7b
--- /dev/null
+++ b/configure/src/core/validators.js
@@ -0,0 +1,216 @@
+// Validation functions that mirror backend validation rules
+
+export const validateConfiguration = (configuration) => {
+ const errors = [];
+
+ if (!configuration) {
+ errors.push({ field: null, message: "Configuration is missing" });
+ return errors;
+ }
+
+ // Check top-level required fields
+ if (!configuration.msv) {
+ errors.push({ field: "msv", message: "Mission-Site-View (msv) is required" });
+ } else {
+ // Check msv.view array
+ if (!configuration.msv.view || !Array.isArray(configuration.msv.view)) {
+ errors.push({ field: "msv.view", message: "Initial view is required" });
+ } else {
+ if (configuration.msv.view[0] == null || configuration.msv.view[0] === "") {
+ errors.push({ field: "msv.view.0", message: "Initial Latitude is required" });
+ }
+ if (configuration.msv.view[1] == null || configuration.msv.view[1] === "") {
+ errors.push({ field: "msv.view.1", message: "Initial Longitude is required" });
+ }
+ if (configuration.msv.view[2] == null || configuration.msv.view[2] === "") {
+ errors.push({ field: "msv.view.2", message: "Initial Zoom Level is required" });
+ }
+ }
+ }
+
+ if (!configuration.layers) {
+ errors.push({ field: "layers", message: "Layers array is required" });
+ }
+
+ if (!configuration.tools) {
+ errors.push({ field: "tools", message: "Tools object is required" });
+ }
+
+ return errors;
+};
+
+export const validateLayer = (layer) => {
+ const errors = [];
+
+ if (!layer) {
+ errors.push({ field: null, message: "Layer is missing" });
+ return errors;
+ }
+
+ // Check layer name
+ if (layer.name == null || layer.name === "" || layer.name === "undefined") {
+ errors.push({ field: "name", message: "Layer name is required" });
+ }
+
+ // Check by layer type
+ switch (layer.type) {
+ case "tile":
+ case "image":
+ if (!layer.url || layer.url === "" || layer.url === "undefined") {
+ errors.push({ field: "url", message: "URL is required" });
+ }
+ if (layer.minZoom == null || isNaN(layer.minZoom)) {
+ errors.push({ field: "minZoom", message: "Minimum Zoom is required" });
+ } else if (layer.minZoom < 0) {
+ errors.push({ field: "minZoom", message: "Minimum Zoom must be >= 0" });
+ }
+ if (layer.maxNativeZoom == null || isNaN(layer.maxNativeZoom)) {
+ errors.push({ field: "maxNativeZoom", message: "Maximum Native Zoom is required" });
+ }
+ if (layer.maxZoom == null || isNaN(layer.maxZoom)) {
+ errors.push({ field: "maxZoom", message: "Maximum Zoom is required" });
+ }
+ if (!isNaN(layer.minZoom) && !isNaN(layer.maxNativeZoom) && layer.minZoom > layer.maxNativeZoom) {
+ errors.push({ field: "minZoom", message: "Minimum Zoom cannot be greater than Maximum Native Zoom" });
+ }
+ break;
+
+ case "vectortile":
+ if (!layer.url || layer.url === "" || layer.url === "undefined") {
+ errors.push({ field: "url", message: "URL is required" });
+ }
+ if (layer.minZoom == null || isNaN(layer.minZoom)) {
+ errors.push({ field: "minZoom", message: "Minimum Zoom is required" });
+ }
+ if (layer.maxNativeZoom == null || isNaN(layer.maxNativeZoom)) {
+ errors.push({ field: "maxNativeZoom", message: "Maximum Native Zoom is required" });
+ }
+ if (layer.maxZoom == null || isNaN(layer.maxZoom)) {
+ errors.push({ field: "maxZoom", message: "Maximum Zoom is required" });
+ }
+ break;
+
+ case "data":
+ if (!layer.demtileurl || layer.demtileurl === "" || layer.demtileurl === "undefined") {
+ errors.push({ field: "demtileurl", message: "DEM Tile URL is required" });
+ }
+ if (layer.minZoom == null || isNaN(layer.minZoom)) {
+ errors.push({ field: "minZoom", message: "Minimum Zoom is required" });
+ }
+ if (layer.maxNativeZoom == null || isNaN(layer.maxNativeZoom)) {
+ errors.push({ field: "maxNativeZoom", message: "Maximum Native Zoom is required" });
+ }
+ if (layer.maxZoom == null || isNaN(layer.maxZoom)) {
+ errors.push({ field: "maxZoom", message: "Maximum Zoom is required" });
+ }
+ break;
+
+ case "query":
+ if (!layer.query?.endpoint || layer.query.endpoint === "" || layer.query.endpoint === "undefined") {
+ errors.push({ field: "query.endpoint", message: "Query Endpoint is required" });
+ }
+ break;
+
+ case "vector":
+ case "velocity":
+ if (layer.controlled !== true) {
+ if (!layer.url || layer.url === "" || layer.url === "undefined") {
+ errors.push({ field: "url", message: "URL is required (unless layer is controlled)" });
+ }
+ }
+ break;
+
+ case "model":
+ if (!layer.url || layer.url === "" || layer.url === "undefined") {
+ errors.push({ field: "url", message: "URL is required" });
+ }
+ if (layer.position?.longitude == null || isNaN(layer.position?.longitude)) {
+ errors.push({ field: "position.longitude", message: "Longitude is required" });
+ }
+ if (layer.position?.latitude == null || isNaN(layer.position?.latitude)) {
+ errors.push({ field: "position.latitude", message: "Latitude is required" });
+ }
+ if (layer.position?.elevation == null || isNaN(layer.position?.elevation)) {
+ errors.push({ field: "position.elevation", message: "Elevation is required" });
+ }
+ if (layer.rotation?.x == null || isNaN(layer.rotation?.x)) {
+ errors.push({ field: "rotation.x", message: "Rotation X is required" });
+ }
+ if (layer.rotation?.y == null || isNaN(layer.rotation?.y)) {
+ errors.push({ field: "rotation.y", message: "Rotation Y is required" });
+ }
+ if (layer.rotation?.z == null || isNaN(layer.rotation?.z)) {
+ errors.push({ field: "rotation.z", message: "Rotation Z is required" });
+ }
+ if (layer.scale == null || isNaN(layer.scale)) {
+ errors.push({ field: "scale", message: "Scale is required" });
+ }
+ break;
+
+ case "header":
+ // No additional required fields for header
+ break;
+
+ default:
+ if (layer.type) {
+ errors.push({ field: "type", message: `Unknown layer type: ${layer.type}` });
+ }
+ }
+
+ return errors;
+};
+
+// Check if a field should be required based on conditional logic
+export const isFieldRequired = (component, layer, configuration) => {
+ if (!component.required) return false;
+
+ // Check conditional requirements
+ if (component.conditionalRequired) {
+ const condition = component.conditionalRequired;
+ const fieldValue = layer?.[condition.field];
+
+ // If the condition field equals the condition value, field is NOT required
+ if (fieldValue === condition.value) {
+ return false;
+ }
+ }
+
+ return true;
+};
+
+// Get all validation errors for current configuration
+export const getAllValidationErrors = (configuration) => {
+ const errors = [];
+
+ // Validate top-level configuration
+ const configErrors = validateConfiguration(configuration);
+ errors.push(...configErrors);
+
+ // Validate each layer
+ if (configuration?.layers && Array.isArray(configuration.layers)) {
+ const traverseLayers = (layers, path = []) => {
+ layers.forEach((layer, index) => {
+ const layerPath = [...path, index];
+ const layerErrors = validateLayer(layer);
+
+ // Add layer path to errors
+ layerErrors.forEach(error => {
+ errors.push({
+ ...error,
+ layerPath,
+ layerName: layer.name || `Layer ${layerPath.join('.')}`
+ });
+ });
+
+ // Traverse sublayers
+ if (layer.sublayers && Array.isArray(layer.sublayers)) {
+ traverseLayers(layer.sublayers, layerPath);
+ }
+ });
+ };
+
+ traverseLayers(configuration.layers);
+ }
+
+ return errors;
+};
\ No newline at end of file
diff --git a/configure/src/metaconfigs/layer-data-config.json b/configure/src/metaconfigs/layer-data-config.json
index ff00f3ff3..e70dbcca6 100644
--- a/configure/src/metaconfigs/layer-data-config.json
+++ b/configure/src/metaconfigs/layer-data-config.json
@@ -29,7 +29,8 @@
"name": "Layer Name",
"description": "",
"type": "textnotrim",
- "width": 8
+ "width": 8,
+ "required": true
},
{
"field": "visibility",
@@ -49,7 +50,8 @@
"name": "Data Tile URL",
"description": "",
"type": "text",
- "width": 10
+ "width": 10,
+ "required": true
},
{
"field": "demparser",
@@ -98,7 +100,8 @@
"type": "number",
"min": 0,
"step": 1,
- "width": 3
+ "width": 3,
+ "required": true
},
{
"field": "maxNativeZoom",
@@ -107,7 +110,8 @@
"type": "number",
"min": 0,
"step": 1,
- "width": 2
+ "width": 2,
+ "required": true
},
{
"field": "maxZoom",
@@ -116,7 +120,8 @@
"type": "number",
"min": 0,
"step": 1,
- "width": 3
+ "width": 3,
+ "required": true
}
]
},
diff --git a/configure/src/metaconfigs/layer-header-config.json b/configure/src/metaconfigs/layer-header-config.json
index fc63441a2..0a0817d2a 100644
--- a/configure/src/metaconfigs/layer-header-config.json
+++ b/configure/src/metaconfigs/layer-header-config.json
@@ -29,7 +29,8 @@
"name": "Layer Name",
"description": "",
"type": "textnotrim",
- "width": 8
+ "width": 8,
+ "required": true
},
{
"field": "expanded",
diff --git a/configure/src/metaconfigs/layer-image-config.json b/configure/src/metaconfigs/layer-image-config.json
index 7003832ad..a05a7f25c 100644
--- a/configure/src/metaconfigs/layer-image-config.json
+++ b/configure/src/metaconfigs/layer-image-config.json
@@ -28,7 +28,8 @@
"name": "Layer Name",
"description": "",
"type": "textnotrim",
- "width": 8
+ "width": 8,
+ "required": true
},
{
"field": "visibility",
@@ -48,7 +49,8 @@
"name": "URL",
"description": "",
"type": "text",
- "width": 12
+ "width": 12,
+ "required": true
},
{
"field": "initialOpacity",
@@ -69,7 +71,8 @@
"type": "number",
"min": 0,
"step": 1,
- "width": 3
+ "width": 3,
+ "required": true
},
{
"field": "maxNativeZoom",
@@ -78,7 +81,8 @@
"type": "number",
"min": 0,
"step": 1,
- "width": 2
+ "width": 2,
+ "required": true
},
{
"field": "maxZoom",
@@ -87,7 +91,8 @@
"type": "number",
"min": 0,
"step": 1,
- "width": 3
+ "width": 3,
+ "required": true
}
]
},
diff --git a/configure/src/metaconfigs/layer-model-config.json b/configure/src/metaconfigs/layer-model-config.json
index 64cf18e99..c718e7606 100644
--- a/configure/src/metaconfigs/layer-model-config.json
+++ b/configure/src/metaconfigs/layer-model-config.json
@@ -29,7 +29,8 @@
"name": "Layer Name",
"description": "A display name for the layer.",
"type": "textnotrim",
- "width": 6
+ "width": 6,
+ "required": true
},
{
"field": "visibility",
@@ -48,7 +49,8 @@
"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
+ "width": 12,
+ "required": true
}
]
},
@@ -61,7 +63,8 @@
"type": "number",
"min": -180,
"max": 180,
- "width": 4
+ "width": 4,
+ "required": true
},
{
"field": "position.latitude",
@@ -70,14 +73,16 @@
"type": "number",
"min": -90,
"max": 90,
- "width": 4
+ "width": 4,
+ "required": true
},
{
"field": "position.elevation",
"name": "Elevation",
"description": "(Required) The elevation in meters at which to place the model.",
"type": "number",
- "width": 4
+ "width": 4,
+ "required": true
}
]
},
@@ -88,21 +93,24 @@
"name": "Rotation X (radians)",
"description": "(Optional) An x-axis rotation in radians to orient the model.",
"type": "number",
- "width": 4
+ "width": 4,
+ "required": true
},
{
"field": "rotation.y",
"name": "Rotation Y (radians)",
"description": "(Optional) An y-axis rotation in radians to orient the model.",
"type": "number",
- "width": 4
+ "width": 4,
+ "required": true
},
{
"field": "rotation.z",
"name": "Rotation Z (radians)",
"description": "(Optional) An z-axis rotation in radians to orient the model.",
"type": "number",
- "width": 4
+ "width": 4,
+ "required": true
}
]
},
@@ -113,7 +121,8 @@
"name": "Scale",
"description": "(Optional) A scaling factor to resize the model.",
"type": "number",
- "width": 4
+ "width": 4,
+ "required": true
}
]
}
diff --git a/configure/src/metaconfigs/layer-query-config.json b/configure/src/metaconfigs/layer-query-config.json
index 5c4430582..3b56c6dab 100644
--- a/configure/src/metaconfigs/layer-query-config.json
+++ b/configure/src/metaconfigs/layer-query-config.json
@@ -29,7 +29,8 @@
"name": "Layer Name",
"description": "A display name for the layer.",
"type": "textnotrim",
- "width": 6
+ "width": 6,
+ "required": true
},
{
"field": "kind",
@@ -63,7 +64,8 @@
"name": "Query Endpoint",
"description": "A file path that points to a search endpoint.",
"type": "text",
- "width": 10
+ "width": 10,
+ "required": true
},
{
"field": "query.type",
diff --git a/configure/src/metaconfigs/layer-tile-config.json b/configure/src/metaconfigs/layer-tile-config.json
index 3c9f065bc..c71cc918f 100644
--- a/configure/src/metaconfigs/layer-tile-config.json
+++ b/configure/src/metaconfigs/layer-tile-config.json
@@ -29,7 +29,8 @@
"name": "Layer Name",
"description": "",
"type": "textnotrim",
- "width": 4
+ "width": 4,
+ "required": true
},
{
"field": "visibility",
@@ -62,7 +63,8 @@
"name": "Source URL",
"description": "• URL: '{url}' A path that points to a tileset, service, or file. If the path is relative, it will be relative to the mission's /Missions/{mission} directory.\n\t• URL (TMS): Tile Map Service tiles are 256x256 sized images hierarchically organized by zoom level and referenced with x and y coordinates. These are the standard format for web tiles and are the format that MMGIS's auxiliary tiling scripts output. Append /{z}/{x}/{y}.png to your URL.\n\t• URL (WMTS): Web Map Tile Service is the same exact concept as TMS but it has an inverted Y-axis. Just like TMS, append /{z}/{x}/{y}.png to your URL.\n\t• URL (WMS): Web Map Service tiles are a popular way of publishing maps by professional GIS software. This format is similar to the previous two formats, but more generic and not so well optimized for use in web maps. A WMS image is defined by the coordinates of its corners. A layer (or list of layers) should be provided as an options by appending ?layers=<,another_if_you _want> to your URL. To override WMS parameters append &= again to the URL after the 'layers' parameters. If desired, use &TILESIZE= to change the tile size of the request and layer away from the default of 256. Example URL: http://ows.mundialis.de/services/service?layers=TOPO-WMS,OSM-Overlay-WMS\n\t• URL (GeoTiff): '{url_to_tif.tif}' A URL to the .tif. Note that this is not tiled and clients will download the entire file.\n• COG: '{url_to_cog.tif}' A URL to the .tif and make sure the 'Use TiTiler' option is enabled.\n• 'STAC-COLLECTION: '{name_of_an_mmgis_stac_collection}' - If titiler-pgstac is available, the tileset is the mosaicked COGs within the specified STAC collection.",
"type": "text",
- "width": 6
+ "width": 6,
+ "required": true
}
]
},
@@ -103,7 +105,8 @@
"type": "number",
"min": 0,
"step": 1,
- "width": 3
+ "width": 3,
+ "required": true
},
{
"field": "maxNativeZoom",
@@ -112,7 +115,8 @@
"type": "number",
"min": 0,
"step": 1,
- "width": 2
+ "width": 2,
+ "required": true
},
{
"field": "maxZoom",
@@ -121,7 +125,8 @@
"type": "number",
"min": 0,
"step": 1,
- "width": 3
+ "width": 3,
+ "required": true
}
]
},
diff --git a/configure/src/metaconfigs/layer-vector-config.json b/configure/src/metaconfigs/layer-vector-config.json
index 8b91cbd2d..f32207ece 100644
--- a/configure/src/metaconfigs/layer-vector-config.json
+++ b/configure/src/metaconfigs/layer-vector-config.json
@@ -29,7 +29,8 @@
"name": "Layer Name",
"description": "A display name for the layer.",
"type": "textnotrim",
- "width": 6
+ "width": 6,
+ "required": true
},
{
"field": "kind",
@@ -80,7 +81,9 @@
"name": "Source URL",
"description": "\t• URL: '{url}' - A file path that points to a geojson. If the path is relative, it will be relative to the mission's directory.\n\t• GeoDatasets: '{geodataset_name}' - Simply go to 'Manage Geodatasets' at the bottom left, upload a geojson and link to it in this URL field with '{geodataset_name}'\n\t• API: 'publishedall' - Grabs all features published via the DrawTool.\n\t• API: 'published:{file_intent}' - Grabs all features published via the DrawTool of a certain intent. Possible values are: roi, campaign, campsite, signpost, trail, all\n\t• API: 'drawn:{file_id}' - Grabs a user drawn file from the DrawTool. The file_id is an integer and can be found by hovering over the desired file in the DrawTool. Note that if the file chosen is still private, the file owner will be the only user who can view it.\n\t• STAC: '{url_to_stac}' - Point to a STAC catalog/collection/item of GeoJSON features.\n\t• STAC-CATALOG: '{url_to_stac_catalog}' - Use this prefix if the URL is specifically to a Catalog object.\n\t• STAC-COLLECTION: '{url_to_stac_collection}' - Use this prefix if the URL is specifically to a Collection object.\n\t• STAC-ITEM: '{url_to_stac_item}' - Use this prefix if the URL is to an Item object\n\t\t• If the underlying data is a COG or similar data product, use the Tile layer type instead.",
"type": "text",
- "width": 10
+ "width": 10,
+ "required": true,
+ "conditionalRequired": {"field": "controlled", "value": false}
}
]
},
diff --git a/configure/src/metaconfigs/layer-vectortile-config.json b/configure/src/metaconfigs/layer-vectortile-config.json
index 34b71611f..953f13921 100644
--- a/configure/src/metaconfigs/layer-vectortile-config.json
+++ b/configure/src/metaconfigs/layer-vectortile-config.json
@@ -29,7 +29,8 @@
"name": "Layer Name",
"description": "A display name for the layer.",
"type": "textnotrim",
- "width": 6
+ "width": 6,
+ "required": true
},
{
"field": "kind",
@@ -63,7 +64,8 @@
"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
+ "width": 12,
+ "required": true
}
]
},
@@ -76,7 +78,18 @@
"type": "number",
"min": 0,
"step": 1,
- "width": 2
+ "width": 2,
+ "required": true
+ },
+ {
+ "field": "maxNativeZoom",
+ "name": "Maximum Native Zoom",
+ "description": "The highest (largest number) zoom level of the tile set.",
+ "type": "number",
+ "min": 0,
+ "step": 1,
+ "width": 2,
+ "required": true
},
{
"field": "maxZoom",
@@ -85,7 +98,8 @@
"type": "number",
"min": 0,
"step": 1,
- "width": 2
+ "width": 2,
+ "required": true
},
{
"field": "layer3dType",
diff --git a/configure/src/metaconfigs/layer-velocity-config.json b/configure/src/metaconfigs/layer-velocity-config.json
index eef793007..0b3c2d36f 100644
--- a/configure/src/metaconfigs/layer-velocity-config.json
+++ b/configure/src/metaconfigs/layer-velocity-config.json
@@ -28,7 +28,8 @@
"name": "Layer Name",
"description": "A display name for the layer.",
"type": "textnotrim",
- "width": 6
+ "width": 6,
+ "required": true
},
{
"field": "kind",
@@ -61,7 +62,9 @@
"name": "URL",
"description": "A file path that points to a geojson, gribjson (streamlines only), or geotiff (arrows only). 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.",
"type": "text",
- "width": 12
+ "width": 12,
+ "required": true,
+ "conditionalRequired": {"field": "controlled", "value": false}
}
]
},
diff --git a/configure/src/metaconfigs/tab-home-config.json b/configure/src/metaconfigs/tab-home-config.json
index 5ba918686..854ea90e3 100644
--- a/configure/src/metaconfigs/tab-home-config.json
+++ b/configure/src/metaconfigs/tab-home-config.json
@@ -10,7 +10,8 @@
"type": "number",
"min": -90,
"max": 90,
- "width": 4
+ "width": 4,
+ "required": true
},
{
"field": "msv.view.1",
@@ -19,7 +20,8 @@
"type": "number",
"min": -180,
"max": 180,
- "width": 4
+ "width": 4,
+ "required": true
},
{
"field": "msv.view.2",
@@ -28,7 +30,8 @@
"type": "number",
"min": 0,
"max": 40,
- "width": 4
+ "width": 4,
+ "required": true
}
]
},