From c47c2d8124df1659701d4c6e2f9f9c2b131711bf Mon Sep 17 00:00:00 2001 From: Tariq Soliman Date: Mon, 4 Mar 2024 18:44:49 -0800 Subject: [PATCH] #505 Dynamic Vector Layers (#506) * #505 Dynamic Vector Layer querying 1 * #505 Time queries, reselect improvement, download extent or full * #505 Dynamic vector layers --- API/Backend/Geodatasets/routes/geodatasets.js | 158 ++++-- config/js/config.js | 3 + config/js/keys.js | 12 + docs/pages/Configure/Layers/Vector/Vector.md | 6 +- src/essence/Ancillary/Description.js | 5 + src/essence/Ancillary/TimeControl.js | 6 +- src/essence/Ancillary/TimeUI.js | 1 + src/essence/Basics/Layers_/LayerCapturer.js | 475 +++++++++++++----- src/essence/Basics/Layers_/Layers_.js | 33 ++ src/essence/Basics/Map_/Map_.js | 19 +- src/essence/Tools/Layers/LayersTool.js | 171 +++++-- src/pre/calls.js | 2 +- 12 files changed, 671 insertions(+), 220 deletions(-) diff --git a/API/Backend/Geodatasets/routes/geodatasets.js b/API/Backend/Geodatasets/routes/geodatasets.js index 94364ad5..5ae4d38c 100644 --- a/API/Backend/Geodatasets/routes/geodatasets.js +++ b/API/Backend/Geodatasets/routes/geodatasets.js @@ -13,8 +13,9 @@ const Geodatasets = geodatasets.Geodatasets; const makeNewGeodatasetTable = geodatasets.makeNewGeodatasetTable; //Returns a geodataset table as a geojson -router.post("/get", function (req, res, next) { - get("post", req, res, next); +router.get("/get/:layer", function (req, res, next) { + req.query.layer = req.params.layer; + get("get", req, res, next); }); router.get("/get", function (req, res, next) { get("get", req, res, next); @@ -51,10 +52,66 @@ function get(reqtype, req, res, next) { if (result) { let table = result.dataValues.table; if (type == "geojson") { + let q = `SELECT properties, ST_AsGeoJSON(geom) FROM ${table}`; + + let hasBounds = false; + let minx = req.query?.minx; + let miny = req.query?.miny; + let maxx = req.query?.maxx; + let maxy = req.query?.maxy; + if (minx != null && miny != null && maxx != null && maxy != null) { + // ST_MakeEnvelope is (xmin, ymin, xmax, ymax, srid) + q += ` WHERE ST_Intersects(ST_MakeEnvelope(${parseFloat( + minx + )}, ${parseFloat(miny)}, ${parseFloat(maxx)}, ${parseFloat( + maxy + )}, 4326), geom)`; + hasBounds = true; + } + if (req.query?.endtime != null) { + const format = req.query?.format || "YYYY-MM-DDTHH:MI:SSZ"; + let t = ` `; + if (!hasBounds) t += `WHERE `; + else t += `AND `; + + if ( + req.query?.startProp == null || + req.query?.startProp.indexOf(`'`) != -1 || + req.query?.endProp == null || + req.query?.endProp.indexOf(`'`) != -1 || + req.query?.starttime == null || + req.query?.starttime.indexOf(`'`) != -1 || + req.query?.endtime == null || + req.query?.endtime.indexOf(`'`) != -1 || + format.indexOf(`'`) != -1 + ) { + res.send({ + status: "failure", + message: "Missing inner or malformed 'time' parameters.", + }); + return; + } + + // prettier-ignore + t += [ + `(`, + `properties->>'${req.query.startProp}' IS NOT NULL AND properties->>'${req.query.endProp}' IS NOT NULL AND`, + ` date_part('epoch', to_timestamp(properties->>'${req.query.startProp}', '${format}'::text)) >= date_part('epoch', to_timestamp('${req.query.starttime}'::text, '${format}'::text))`, + ` AND date_part('epoch', to_timestamp(properties->>'${req.query.endProp}', '${format}'::text)) <= date_part('epoch', to_timestamp('${req.query.endtime}'::text, '${format}'::text))`, + `)`, + ` OR `, + `(`, + `properties->>'${req.query.startProp}' IS NULL AND properties->>'${req.query.endProp}' IS NOT NULL AND`, + ` date_part('epoch', to_timestamp(properties->>'${req.query.endProp}', '${format}'::text)) >= date_part('epoch', to_timestamp('${req.query.starttime}'::text, '${format}'::text))`, + ` AND date_part('epoch', to_timestamp(properties->>'${req.query.endProp}', '${format}'::text)) >= date_part('epoch', to_timestamp('${req.query.endtime}'::text, '${format}'::text))`, + `)` + ].join('') + q += t; + } + q += `;`; + sequelize - .query( - "SELECT properties, ST_AsGeoJSON(geom)" + " " + "FROM " + table - ) + .query(q) .then(([results]) => { let geojson = { type: "FeatureCollection", features: [] }; for (let i = 0; i < results.length; i++) { @@ -299,10 +356,35 @@ router.post("/search", function (req, res, next) { }); }); +router.post("/append/:name", function (req, res, next) { + req.body = { + name: req.params.name, + geojson: req.body, + action: "append", + }; + recreate(req, res, next); +}); + +router.post("/recreate/:name", function (req, res, next) { + req.body = { + name: req.params.name, + geojson: req.body, + action: "recreate", + }; + recreate(req, res, next); +}); + router.post("/recreate", function (req, res, next) { + recreate(req, res, next); +}); + +function recreate(req, res, next) { let features = null; try { - features = JSON.parse(req.body.geojson).features; + features = + typeof req.body.geojson === "string" + ? JSON.parse(req.body.geojson).features + : req.body.geojson.features; } catch (err) { logger("error", "Failure: Malformed file.", req.originalUrl, req, err); res.send({ @@ -330,7 +412,7 @@ router.post("/recreate", function (req, res, next) { } let drop_qry = "TRUNCATE TABLE " + result.table + " RESTART IDENTITY"; - if (req.body.hasOwnProperty("action") && req.body.action=="append") { + if (req.body.hasOwnProperty("action") && req.body.action == "append") { drop_qry = ""; } @@ -360,40 +442,40 @@ router.post("/recreate", function (req, res, next) { res.send(result); } ); +} - function populateGeodatasetTable(Table, features, cb) { - let rows = []; - - for (var i = 0; i < features.length; i++) { - rows.push({ - properties: features[i].properties, - geometry_type: features[i].geometry.type, - geom: { - crs: { type: "name", properties: { name: "EPSG:4326" } }, - type: features[i].geometry.type, - coordinates: features[i].geometry.coordinates, - }, - }); - } +function populateGeodatasetTable(Table, features, cb) { + let rows = []; - Table.bulkCreate(rows, { returning: true }) - .then(function (response) { - cb(true); - return null; - }) - .catch(function (err) { - logger( - "error", - "Geodatasets: Failed to populate a geodataset table!", - req.originalUrl, - req, - err - ); - cb(false); - return null; - }); + for (var i = 0; i < features.length; i++) { + rows.push({ + properties: features[i].properties, + geometry_type: features[i].geometry.type, + geom: { + crs: { type: "name", properties: { name: "EPSG:4326" } }, + type: features[i].geometry.type, + coordinates: features[i].geometry.coordinates, + }, + }); } -}); + + Table.bulkCreate(rows, { returning: true }) + .then(function (response) { + cb(true); + return null; + }) + .catch(function (err) { + logger( + "error", + "Geodatasets: Failed to populate a geodataset table!", + req.originalUrl, + req, + err + ); + cb(false); + return null; + }); +} function tile2Lng(x, z) { return (x / Math.pow(2, z)) * 360 - 180; diff --git a/config/js/config.js b/config/js/config.js index 985f9dfb..7dbf76ca 100644 --- a/config/js/config.js +++ b/config/js/config.js @@ -2796,6 +2796,9 @@ function layerPopulateVariable(modalId, layerType) { value: "Example", }, ]; + currentLayerVars.dynamicExtent = currentLayerVars.dynamicExtent || false; + currentLayerVars.dynamicExtentMoveThreshold = + currentLayerVars.dynamicExtentMoveThreshold || null; currentLayerVars.shortcutSuffix = currentLayerVars.shortcutSuffix || null; if (layerType == "tile") { diff --git a/config/js/keys.js b/config/js/keys.js index 2d237f69..34ae72f4 100644 --- a/config/js/keys.js +++ b/config/js/keys.js @@ -42,6 +42,18 @@ var Keys = { "
Uploading CSVs
", '
curl -i -X POST -H "Authorization:Bearer <token>" -F "name={dataset_name}" -F "upsert=true" -F "header=[\"File\",\"Target\",\"ShotNumber\",\"Distance(m)\",\"LaserPower\",\"SpectrumTotal\",\"SiO2\",\"TiO2\",\"Al2O3\",\"FeOT\",\"MgO\",\"CaO\",\"Na2O\",\"K2O\",\"Total\",\"SiO2_RMSEP\",\"TiO2_RMSEP\",\"Al2O3_RMSEP\",\"FeOT_RMSEP\",\"MgO_RMSEP\",\"CaO_RMSEP\",\"Na2O_RMSEP\",\"K2O_RMSEP\"]" -F "data=@{path/to.csv};type=text/csv" ' + location.origin + '/api/datasets/upload
', "", + "
  • ", + "
    Upserting a Geodataset
    ", + '
    curl -i -X POST -H "Authorization:Bearer <token>" -H "Content-Type: application/json" --data-binary "@{path_to_your_file}.json" ' + location.origin + '/api/geodatasets/recreate/{geodataset_name}
    ', + "
  • ", + "
  • ", + "
    Appending to an existing Geodataset
    ", + '
    curl -i -X POST -H "Authorization:Bearer <token>" -H "Content-Type: application/json" --data-binary "@{path_to_your_file_to_append}.json" ' + location.origin + '/api/geodatasets/append/{geodataset_name}
    ', + "
  • ", + "
  • ", + "
    Get an existing Geodataset
    ", + '
    curl -i -X GET -H "Authorization:Bearer <token>" -H "Content-Type: application/json" ' + location.origin + '/api/geodatasets/get/{geodataset_name}
    ', + "
  • ", "", "", "" diff --git a/docs/pages/Configure/Layers/Vector/Vector.md b/docs/pages/Configure/Layers/Vector/Vector.md index 73b84990..428cc5cd 100644 --- a/docs/pages/Configure/Layers/Vector/Vector.md +++ b/docs/pages/Configure/Layers/Vector/Vector.md @@ -27,7 +27,7 @@ A file path that points to a geojson. If the path is relative, it will be relati #### Controlled -_type:_ bool +_type:_ bool Whether the layer can be dynamically updated or not. If true, the layer can be dynamically updated and the URL is not required. If true and a URL is set and Time Enabled is true, the initial url query will be performed. @@ -127,6 +127,8 @@ Example: ```javascript { "useKeyAsName": "propKey || [propKey1, propKey2, ...]", + "dynamicExtent": false, + "dynamicExtentMoveThreshold": "100000000/z", "shortcutSuffix": "single letter to 'ATL + {letter}' toggle the layer on and off", "hideMainFeature": false, "datasetLinks": [ @@ -254,6 +256,8 @@ Example: ``` - `useKeyAsName`: 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. This may also be an array of keys. +- `dynamicExtent`: If true, tries to only query the vector features present in the user's current map viewport. 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}` +- `dynamicExtentMoveThreshold`: 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. - `shortcutSuffix`: 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. - `hideMainFeature`: If true, hides all typically rendered features. This is useful if showing only `*Attachments` sublayers is desired. Default false - `datasetLinks`: 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. diff --git a/src/essence/Ancillary/Description.js b/src/essence/Ancillary/Description.js index c5722c53..40425ef7 100644 --- a/src/essence/Ancillary/Description.js +++ b/src/essence/Ancillary/Description.js @@ -70,6 +70,11 @@ const Description = { this.inited = true if (this.waitingOnUpdate) this.updateInfo() + + $(window).on('resize', () => { + $('#mainDescPointLinks > dl.dropy').removeClass('open') + $(`#mainDescPointLinks_global`).empty() + }) }, updateInfo(force) { if (force !== true) { diff --git a/src/essence/Ancillary/TimeControl.js b/src/essence/Ancillary/TimeControl.js index 3c14b7e2..0c4d7389 100644 --- a/src/essence/Ancillary/TimeControl.js +++ b/src/essence/Ancillary/TimeControl.js @@ -292,7 +292,11 @@ var TimeControl = { var reloadedLayers = [] for (let layerName in L_.layers.data) { const layer = L_.layers.data[layerName] - if (layer.time && layer.time.enabled === true) { + if ( + layer.time && + layer.time.enabled === true && + layer.variables?.dynamicExtent != true + ) { TimeControl.reloadLayer(layer) reloadedLayers.push(layer.name) } diff --git a/src/essence/Ancillary/TimeUI.js b/src/essence/Ancillary/TimeUI.js index 285276d2..ea07bfaf 100644 --- a/src/essence/Ancillary/TimeUI.js +++ b/src/essence/Ancillary/TimeUI.js @@ -400,6 +400,7 @@ const TimeUI = { ) { const dateStaged = new Date(L_.configData.time.initialwindowend) if (dateStaged == 'Invalid Date') { + TimeUI._timelineEndTimestamp = new Date() console.warn( "Invalid 'Initial Window End Time' provided. Defaulting to 'now'." ) diff --git a/src/essence/Basics/Layers_/LayerCapturer.js b/src/essence/Basics/Layers_/LayerCapturer.js index a71d43a4..ad2e42b7 100644 --- a/src/essence/Basics/Layers_/LayerCapturer.js +++ b/src/essence/Basics/Layers_/LayerCapturer.js @@ -5,7 +5,14 @@ import L_ from '../Layers_/Layers_' import calls from '../../../pre/calls' import TimeControl from '../../Ancillary/TimeControl' -export const captureVector = (layerObj, options, cb) => { +// This is so that an eariler and slower dynamic geodataset request +// does not override an earlier shorter one +// Object of layerName: timestamp +const _geodatasetRequestLastTimestamp = {} +const _geodatasetRequestLastLoc = {} +const _layerRequestLastTimestamp = {} +const _layerRequestLastLoc = {} +export const captureVector = (layerObj, options, cb, dynamicCb) => { options = options || {} let layerUrl = layerObj.url const layerData = L_.layers.data[layerObj.name] @@ -57,110 +64,277 @@ export const captureVector = (layerObj, options, cb) => { let urlSplitRaw = layerObj.url.split(':') let urlSplit = layerObj.url.toLowerCase().split(':') - switch (urlSplit[0]) { - case 'geodatasets': - calls.api( - 'geodatasets_get', - { - layer: urlSplitRaw[1], - type: 'geojson', - }, - function (data) { - cb(data.body) - }, - function (data) { - console.warn( - 'ERROR: ' + - data.status + - ' in ' + - layerUrl + - ' /// ' + - data.message - ) - cb(null) - } - ) - break - case 'api': - switch (urlSplit[1]) { - case 'publishedall': - calls.api( - 'files_getfile', - { - quick_published: true, - }, - function (data) { - data.body.features.sort((a, b) => { - let intentOrder = [ - 'polygon', - 'roi', - 'campaign', - 'campsite', - 'all', - 'line', - 'trail', - 'point', - 'signpost', - 'arrow', - 'text', - 'note', - 'master', - ] - let ai = intentOrder.indexOf( - a.properties._.intent - ) - let bi = intentOrder.indexOf( - b.properties._.intent - ) - return ai - bi - }) - cb(data.body) - }, - function (data) { - console.warn( - 'ERROR! ' + - data.status + - ' in ' + - layerUrl + - ' /// ' + - data.message + if (layerData?.variables?.dynamicExtent === true) { + switch (urlSplit[0]) { + case 'geodatasets': + // Return .on('moveend zoomend') event + dynamicCb((e) => { + console.log() + // Don't query if layer is off + if (L_.layers.on[layerObj.name] !== true) return + + const zoom = L_.Map_.map.getZoom() + + if ( + zoom >= (layerData.minZoom || 0) && + (zoom <= layerData.maxZoom || 100) + ) { + // Then query, delete existing and remake + const bounds = L_.Map_.map.getBounds() + const body = { + layer: urlSplitRaw[1], + type: 'geojson', + maxy: bounds._northEast.lat, + maxx: bounds._northEast.lng, + miny: bounds._southWest.lat, + minx: bounds._southWest.lng, + crsCode: mmgisglobal.customCRS.code.replace( + 'EPSG:', + '' + ), + zoom: zoom, + } + + if (layerData.time?.enabled === true) { + body.starttime = layerData.time.start + body.startProp = layerData.time.startProp + body.endtime = layerData.time.end + body.endProp = layerData.time.endProp + + if (e.hasOwnProperty('endTime')) { + // Then this function was being called from timeChange + body.starttime = e.startTime + body.endtime = e.endTime + } + } + + const dateNow = new Date().getTime() + + _geodatasetRequestLastTimestamp[layerObj.name] = + Math.max( + _geodatasetRequestLastTimestamp[ + layerObj.name + ] || 0, + dateNow ) - cb(null) + calls.api( + 'geodatasets_get', + body, + (data) => { + const lastLoc = + _geodatasetRequestLastLoc[layerObj.name] + const nowLoc = L_.Map_.map.getCenter() + + if ( + _geodatasetRequestLastTimestamp[ + layerObj.name + ] == dateNow && + (lastLoc == null || + layerData?.variables + ?.dynamicExtentMoveThreshold == + null || + F_.lngLatDistBetween( + lastLoc.lng, + lastLoc.lat, + nowLoc.lng, + nowLoc.lat + ) > + parseFloat( + layerData?.variables + ?.dynamicExtentMoveThreshold + ) / + (layerData?.variables + ?.dynamicExtentMoveThreshold && + layerData?.variables?.dynamicExtentMoveThreshold.indexOf( + '/z' + ) > -1 + ? Math.pow( + 2, + L_.Map_.map.getZoom() + ) + : 1)) + ) { + L_.clearVectorLayer(layerObj.name) + L_.updateVectorLayer(layerObj.name, data) + _geodatasetRequestLastLoc[layerObj.name] = + nowLoc + } + }, + (data) => { + console.warn( + 'ERROR: ' + + data.status + + ' in geodatasets_get:' + + layerObj.display_name + + ' /// ' + + data.message + ) + } + ) + } else { + // Just delete existing + L_.clearVectorLayer(layerObj.name) + } + }) + cb({ type: 'FeatureCollection', features: [] }, true) + break + case 'api': + break + default: + // Return .on('moveend zoomend') event + dynamicCb((e) => { + // Don't query if layer is off + if (L_.layers.on[layerObj.name] !== true) return + + const zoom = L_.Map_.map.getZoom() + + if ( + zoom >= (layerData.minZoom || 0) && + (zoom <= layerData.maxZoom || 100) + ) { + // Then query, delete existing and remake + const bounds = L_.Map_.map.getBounds() + const body = { + type: 'geojson', + maxy: bounds._northEast.lat, + maxx: bounds._northEast.lng, + miny: bounds._southWest.lat, + minx: bounds._southWest.lng, + crsCode: mmgisglobal.customCRS.code.replace( + 'EPSG:', + '' + ), + zoom: zoom, + } + + if (layerData.time?.enabled === true) { + body.starttime = layerData.time.start + body.startProp = layerData.time.startProp + body.endtime = layerData.time.end + body.endProp = layerData.time.endProp + + if (e.hasOwnProperty('endTime')) { + // Then this function was being called from timeChange + body.starttime = e.startTime + body.endtime = e.endTime + } } - ) - break - case 'published': - calls.api( - 'files_getfile', - { - intent: urlSplit[2], - quick_published: true, - }, - function (data) { - cb(data.body) - }, - function (data) { + + const dateNow = new Date().getTime() + + _layerRequestLastTimestamp[layerObj.name] = Math.max( + _layerRequestLastTimestamp[layerObj.name] || 0, + dateNow + ) + + let dynamicLayerUrl = layerObj.url + .replace(/{starttime}/g, body.starttime) + .replace(/{endtime}/g, body.endtime) + .replace(/{time}/g, body.endtime) + .replace(/{startprop}/g, body.startProp) + .replace(/{endprop}/g, body.endProp) + .replace(/{crscode}/g, body.crsCode) + .replace(/{zoom}/g, body.zoom) + .replace(/{minx}/g, body.minx) + .replace(/{miny}/g, body.miny) + .replace(/{maxx}/g, body.maxx) + .replace(/{maxy}/g, body.maxy) + + if (!F_.isUrlAbsolute(dynamicLayerUrl)) + dynamicLayerUrl = L_.missionPath + dynamicLayerUrl + + $.getJSON(dynamicLayerUrl, function (data) { + if (data.hasOwnProperty('Features')) { + data.features = data.Features + delete data.Features + } + + if ( + _layerRequestLastTimestamp[layerObj.name] == + dateNow + ) { + L_.clearVectorLayer(layerObj.name) + L_.updateVectorLayer(layerObj.name, data) + } + }).fail(function (jqXHR, textStatus, errorThrown) { + //Tell the console council about what happened console.warn( 'ERROR! ' + - data.status + + textStatus + ' in ' + layerUrl + ' /// ' + - data.message + errorThrown ) - cb(null) - } - ) - break - case 'tacticaltargets': - calls.api( - 'tactical_targets', - {}, - function (data) { - cb(data.body) - }, - function (data) { - if (data) { + }) + } else { + // Just delete existing + L_.clearVectorLayer(layerObj.name) + } + }) + cb({ type: 'FeatureCollection', features: [] }, true) + } + } else { + switch (urlSplit[0]) { + case 'geodatasets': + calls.api( + 'geodatasets_get', + { + layer: urlSplitRaw[1], + type: 'geojson', + }, + (data) => { + cb(data) + }, + (data) => { + console.warn( + 'ERROR: ' + + data.status + + ' in ' + + layerUrl + + ' /// ' + + data.message + ) + cb(null) + } + ) + break + case 'api': + switch (urlSplit[1]) { + case 'publishedall': + calls.api( + 'files_getfile', + { + quick_published: true, + }, + function (data) { + data.body.features.sort((a, b) => { + let intentOrder = [ + 'polygon', + 'roi', + 'campaign', + 'campsite', + 'all', + 'line', + 'trail', + 'point', + 'signpost', + 'arrow', + 'text', + 'note', + 'master', + ] + let ai = intentOrder.indexOf( + a.properties._.intent + ) + let bi = intentOrder.indexOf( + b.properties._.intent + ) + return ai - bi + }) + cb(data.body) + }, + function (data) { console.warn( 'ERROR! ' + data.status + @@ -169,22 +343,21 @@ export const captureVector = (layerObj, options, cb) => { ' /// ' + data.message ) + cb(null) } - cb(null) - } - ) - break - case 'drawn': - calls.api( - 'files_getfile', - { - id: urlSplit[2], - }, - function (data) { - cb(data.body.geojson) - }, - function (data) { - if (data) { + ) + break + case 'published': + calls.api( + 'files_getfile', + { + intent: urlSplit[2], + quick_published: true, + }, + function (data) { + cb(data.body) + }, + function (data) { console.warn( 'ERROR! ' + data.status + @@ -193,31 +366,77 @@ export const captureVector = (layerObj, options, cb) => { ' /// ' + data.message ) + cb(null) } - cb(null) - } - ) - break - default: - console.warn( - `Unknown layer URL ${layerUrl} in layer ${layerObj.name}` - ) - cb(null) - break - } - break - default: - done = false + ) + break + case 'tacticaltargets': + calls.api( + 'tactical_targets', + {}, + function (data) { + cb(data.body) + }, + function (data) { + if (data) { + console.warn( + 'ERROR! ' + + data.status + + ' in ' + + layerUrl + + ' /// ' + + data.message + ) + } + cb(null) + } + ) + break + case 'drawn': + calls.api( + 'files_getfile', + { + id: urlSplit[2], + }, + function (data) { + cb(data.body.geojson) + }, + function (data) { + if (data) { + console.warn( + 'ERROR! ' + + data.status + + ' in ' + + layerUrl + + ' /// ' + + data.message + ) + } + cb(null) + } + ) + break + default: + console.warn( + `Unknown layer URL ${layerUrl} in layer ${layerObj.name}` + ) + cb(null) + break + } + break + default: + done = false + } } if (!done) { - $.getJSON(layerUrl, function (data) { + $.getJSON(layerUrl, (data) => { if (data.hasOwnProperty('Features')) { data.features = data.Features delete data.Features } cb(data) - }).fail(function (jqXHR, textStatus, errorThrown) { + }).fail((jqXHR, textStatus, errorThrown) => { //Tell the console council about what happened console.warn( 'ERROR! ' + diff --git a/src/essence/Basics/Layers_/Layers_.js b/src/essence/Basics/Layers_/Layers_.js index 6d7d4fa2..f093d19d 100644 --- a/src/essence/Basics/Layers_/Layers_.js +++ b/src/essence/Basics/Layers_/Layers_.js @@ -215,6 +215,18 @@ const L_ = { if (L_._onLayerToggleSubscriptions[fid] != null) delete L_._onLayerToggleSubscriptions[fid] }, + _onSpecificLayerToggleSubscriptions: {}, + subscribeOnSpecificLayerToggle: function (fid, layerId, func) { + if (typeof func === 'function') + L_._onSpecificLayerToggleSubscriptions[fid] = { + layer: layerId, + func: func, + } + }, + unsubscribeOnSpecificLayerToggle: function (fid) { + if (L_._onSpecificLayerToggleSubscriptions[fid] != null) + delete L_._onSpecificLayerToggleSubscriptions[fid] + }, //Takes in config layer obj //Toggles a layer on and off and accounts for sublayers //Takes in a config layer object @@ -233,6 +245,11 @@ const L_ = { L_._onLayerToggleSubscriptions[k](s.name, !on) }) + Object.keys(L_._onSpecificLayerToggleSubscriptions).forEach((k) => { + const subs = L_._onSpecificLayerToggleSubscriptions[k] + if (subs.layer === s.name) subs.func(s.name, !on) + }) + // Always reupdate layer infos at the end to keep them in sync Description.updateInfo() @@ -1034,11 +1051,27 @@ const L_ = { ) } else { const savedOptions = JSON.parse(JSON.stringify(layer.options)) + layer.setStyle({ color: color, stroke: color, }) layer.options = savedOptions + + // For some odd reason sometimes the first style does not work + // This makes sure it does + setTimeout(() => { + if ( + layer.options.color != color && + layer.options.stroke != color + ) { + layer.setStyle({ + color: color, + stroke: color, + }) + layer.options = savedOptions + } + }, 1) } } catch (err) { if (layer._icon) diff --git a/src/essence/Basics/Map_/Map_.js b/src/essence/Basics/Map_/Map_.js index 38674296..2997f048 100644 --- a/src/essence/Basics/Map_/Map_.js +++ b/src/essence/Basics/Map_/Map_.js @@ -781,10 +781,22 @@ async function makeVectorLayer( captureVector( layerObj, { evenIfOff: evenIfOff, useEmptyGeoJSON: useEmptyGeoJSON }, - add + add, + (f) => { + Map_.map.on('moveend', f) + L_.subscribeTimeChange( + `dynamicgeodataset_${layerObj.name}`, + f + ) + L_.subscribeOnSpecificLayerToggle( + `dynamicgeodataset_${layerObj.name}`, + layerObj.name, + f + ) + } ) - function add(data) { + function add(data, allowInvalid) { // [] if (Array.isArray(data) && data.length === 0) { data = { type: 'FeatureCollection', features: [] } @@ -807,6 +819,7 @@ async function makeVectorLayer( let invalidGeoJSONTrace = gjv.valid(data, true) const allowableErrors = [`position must only contain numbers`] + invalidGeoJSONTrace = invalidGeoJSONTrace.filter((t) => { if (typeof t !== 'string') return false for (let i = 0; i < allowableErrors.length; i++) { @@ -818,7 +831,7 @@ async function makeVectorLayer( if ( data == null || data === 'off' || - invalidGeoJSONTrace.length > 0 + (invalidGeoJSONTrace.length > 0 && allowInvalid !== true) ) { if (data != null && data != 'off') { data = null diff --git a/src/essence/Tools/Layers/LayersTool.js b/src/essence/Tools/Layers/LayersTool.js index 4e2292ae..713d99e6 100644 --- a/src/essence/Tools/Layers/LayersTool.js +++ b/src/essence/Tools/Layers/LayersTool.js @@ -188,7 +188,6 @@ function interfaceWithMMGIS(fromInit) { let currentContrast let currentSaturation let currentBlend - //Build layerExport var layerExport switch (node[i].type) { @@ -231,6 +230,13 @@ function interfaceWithMMGIS(fromInit) { '', '', '', + node[i]?.variables?.dynamicExtent === true ? ['
    ', + '
    Extent
    ', + '', + '
    ',].join('\n') : '', L_.Coordinates.mainType != 'll' ? [ '
    ', '
    Coords
    ', @@ -768,6 +774,9 @@ function interfaceWithMMGIS(fromInit) { let format = li.find('.layersToolExportFormat') if (format) format = format.val() || 'geojson' else format = 'geojson' + let extent = li.find('.layersToolExportExtent') + if (extent) extent = extent.val() || 'local' + else extent = 'local' let coords = li.find('.layersToolExportCoords') if (coords) coords = coords.val() || 'source' else coords = 'source' @@ -783,68 +792,134 @@ function interfaceWithMMGIS(fromInit) { ) return } - let geojson = L_.layers.layer[layerUUID].toGeoJSON(L_.GEOJSON_PRECISION) - - let filename = layerDisplayName - - if (coords != 'source') - geojson = L_.convertGeoJSONLngLatsToPrimaryCoordinates(geojson) - - switch (format) { - case 'geojson': - F_.downloadObject(geojson, filename, '.geojson') - break - case 'kml': - const kml = tokml( - F_.geoJSONForceSimpleStyleSpec( - geojson, - true, - L_.layers.data[layerUUID]?.style, - layerData.useKeyAsName - ), - { - name: layerData.useKeyAsName || false, - description: 'Generated by MMGIS', - timestamp: - layerData.time?.enabled === true - ? layerData.time.endProp || null - : null, - simplestyle: true, - } - ) - F_.downloadObject(kml, filename, '.kml', 'xml') - break - case 'shp': - const folder = filename + const download = (geojson) => { + let filename = layerDisplayName + + if (coords != 'source') + geojson = L_.convertGeoJSONLngLatsToPrimaryCoordinates(geojson) + + switch (format) { + case 'geojson': + F_.downloadObject(geojson, filename, '.geojson') + break + case 'kml': + const kml = tokml( + F_.geoJSONForceSimpleStyleSpec( + geojson, + true, + L_.layers.data[layerUUID]?.style, + layerData.useKeyAsName + ), + { + name: layerData.useKeyAsName || false, + description: 'Generated by MMGIS', + timestamp: + layerData.time?.enabled === true + ? layerData.time.endProp || null + : null, + simplestyle: true, + } + ) + F_.downloadObject(kml, filename, '.kml', 'xml') + break + case 'shp': + const folder = filename + + calls.api( + 'proj42wkt', + { + proj4: window.mmgisglobal.customCRS.projString, + }, + (data) => { + shpwrite + .zip(geojson, { + outputType: 'blob', + prj: data, + }) + .then((content) => { + saveAs(content, `${folder}.zip`) + }) + }, + function (err) { + CursorInfo.update( + `Failed to generate shapefile's .prj.`, + 6000, + true, + { x: 385, y: 6 }, + '#e9ff26', + 'black' + ) + } + ) + break + default: + } + } + + const urlSplitRaw = (layerData.url || '').split(':') + const urlSplit = (layerData.url || '').toLowerCase().split(':') + + if (extent === 'raw') { + if (urlSplit[0] === 'geodatasets') { calls.api( - 'proj42wkt', + 'geodatasets_get', { - proj4: window.mmgisglobal.customCRS.projString, + layer: urlSplitRaw[1], + type: 'geojson', }, (data) => { - shpwrite - .zip(geojson, { - outputType: 'blob', - prj: data, - }) - .then((content) => { - saveAs(content, `${folder}.zip`) - }) + download(data.body) }, - function (err) { + (data) => { CursorInfo.update( - `Failed to generate shapefile's .prj.`, + `Failed to download ${layerDisplayName}.`, 6000, true, { x: 385, y: 6 }, '#e9ff26', 'black' ) + console.warn( + 'ERROR: ' + + data.status + + ' in LayersTool geodatasets_get:' + + layerDisplayName + + ' /// ' + + data.message + ) + return } ) - break - default: + } else { + let layerUrl = layerData.url + + if (!F_.isUrlAbsolute(layerUrl)) + layerUrl = L_.missionPath + layerUrl + $.getJSON(layerUrl, function (data) { + if (data.hasOwnProperty('Features')) { + data.features = data.Features + delete data.Features + } + + download(data) + }).fail(function (jqXHR, textStatus, errorThrown) { + //Tell the console council about what happened + console.warn( + 'ERROR! ' + + textStatus + + ' in ' + + layerUrl + + ' /// ' + + errorThrown + ) + }) + } + } else { + let geojson = L_.layers.layer[layerUUID].toGeoJSON( + L_.GEOJSON_PRECISION + ) + download(geojson) } }) diff --git a/src/pre/calls.js b/src/pre/calls.js index b365c0c0..9a197b42 100644 --- a/src/pre/calls.js +++ b/src/pre/calls.js @@ -131,7 +131,7 @@ const c = { url: 'API/datasets/get', }, geodatasets_get: { - type: 'POST', + type: 'GET', url: 'API/geodatasets/get', }, geodatasets_search: {