diff --git a/API/Backend/Geodatasets/routes/geodatasets.js b/API/Backend/Geodatasets/routes/geodatasets.js index a86cc7e1..86f62fc9 100644 --- a/API/Backend/Geodatasets/routes/geodatasets.js +++ b/API/Backend/Geodatasets/routes/geodatasets.js @@ -55,9 +55,9 @@ 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 q = `SELECT properties, ST_AsGeoJSON(geom) FROM ${table}`; + let hasBounds = false; if (req.body?.bounds) { const bounds = req.body.bounds; if ( @@ -69,7 +69,12 @@ function get(reqtype, req, res, next) { bounds.northEast.lat != null ) { // ST_MakeEnvelope is (xmin, ymin, xmax, ymax, srid) - q += ` WHERE ST_Intersects(ST_MakeEnvelope(${bounds.southWest.lng}, ${bounds.southWest.lat}, ${bounds.northEast.lng}, ${bounds.northEast.lat}, 4326), geom);`; + q += ` WHERE ST_Intersects(ST_MakeEnvelope(${parseFloat( + bounds.southWest.lng + )}, ${parseFloat(bounds.southWest.lat)}, ${parseFloat( + bounds.northEast.lng + )}, ${parseFloat(bounds.northEast.lat)}, 4326), geom)`; + hasBounds = true; } else { res.send({ status: "failure", @@ -79,6 +84,48 @@ function get(reqtype, req, res, next) { return; } } + if (req.body?.time) { + const time = req.body.time; + const format = req.body.time.format || "YYYY-MM-DDTHH:MI:SSZ"; + let t = ` `; + if (!hasBounds) t += `WHERE `; + else t += `AND `; + + if ( + time.startProp == null || + time.startProp.indexOf(`'`) != -1 || + time.endProp == null || + time.endProp.indexOf(`'`) != -1 || + time.start == null || + time.start.indexOf(`'`) != -1 || + time.end == null || + time.end.indexOf(`'`) != -1 || + format.indexOf(`'`) != -1 + ) { + res.send({ + status: "failure", + message: "Missing inner or malformed 'time' parameters.", + }); + return; + } + + // prettier-ignore + t += [ + `(`, + `properties->>'${time.startProp}' IS NOT NULL AND properties->>'${time.endProp}' IS NOT NULL AND`, + ` date_part('epoch', to_timestamp(properties->>'${time.startProp}', '${format}'::text)) >= date_part('epoch', to_timestamp('${time.start}'::text, '${format}'::text))`, + ` AND date_part('epoch', to_timestamp(properties->>'${time.endProp}', '${format}'::text)) <= date_part('epoch', to_timestamp('${time.end}'::text, '${format}'::text))`, + `)`, + ` OR `, + `(`, + `properties->>'${time.startProp}' IS NULL AND properties->>'${time.endProp}' IS NOT NULL AND`, + ` date_part('epoch', to_timestamp(properties->>'${time.endProp}', '${format}'::text)) >= date_part('epoch', to_timestamp('${time.start}'::text, '${format}'::text))`, + ` AND date_part('epoch', to_timestamp(properties->>'${time.endProp}', '${format}'::text)) >= date_part('epoch', to_timestamp('${time.end}'::text, '${format}'::text))`, + `)` + ].join('') + q += t; + } + q += `;`; sequelize .query(q) 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/Basics/Layers_/LayerCapturer.js b/src/essence/Basics/Layers_/LayerCapturer.js index e9e73e79..6e39d3d7 100644 --- a/src/essence/Basics/Layers_/LayerCapturer.js +++ b/src/essence/Basics/Layers_/LayerCapturer.js @@ -5,6 +5,10 @@ import L_ from '../Layers_/Layers_' import calls from '../../../pre/calls' import TimeControl from '../../Ancillary/TimeControl' +// This is so that an eariler and slower dynamic geodataset request +// does not override an earlier shorter one +// Object of layerName: timestamp +const _geodatasetRequestLastTimestamp = {} export const captureVector = (layerObj, options, cb, dynamicCb) => { options = options || {} let layerUrl = layerObj.url @@ -63,36 +67,71 @@ export const captureVector = (layerObj, options, cb, dynamicCb) => { // Return .on('moveend zoomend') event dynamicCb((e) => { 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', + bounds: { + northEast: { + lat: bounds._northEast.lat, + lng: bounds._northEast.lng, + }, + southWest: { + lat: bounds._southWest.lat, + lng: bounds._southWest.lng, + }, + }, + crsCode: mmgisglobal.customCRS.code.replace( + 'EPSG:', + '' + ), + } + + if (layerData.time?.enabled === true) { + body.time = { + start: layerData.time.start, + startProp: layerData.time.startProp, + end: layerData.time.end, + endProp: layerData.time.endProp, + } + if (e.hasOwnProperty('endTime')) { + // Then this function was being called from timeChange + body.time.start = e.startTime + body.time.end = e.endTime + } + } + + const dateNow = new Date().getTime() + + _geodatasetRequestLastTimestamp[layerObj.name] = + Math.max( + _geodatasetRequestLastTimestamp[ + layerObj.name + ] || 0, + dateNow + ) calls.api( 'geodatasets_get', - { - layer: urlSplitRaw[1], - type: 'geojson', - bounds: { - northEast: { - lat: bounds._northEast.lat, - lng: bounds._northEast.lng, - }, - southWest: { - lat: bounds._southWest.lat, - lng: bounds._southWest.lng, - }, - }, - crsCode: mmgisglobal.customCRS.code.replace( - 'EPSG:', - '' - ), - }, + body, (data) => { - L_.clearVectorLayer(layerObj.name) - L_.updateVectorLayer(layerObj.name, data.body) + if ( + _geodatasetRequestLastTimestamp[ + layerObj.name + ] == dateNow + ) { + L_.clearVectorLayer(layerObj.name) + L_.updateVectorLayer( + layerObj.name, + data.body + ) + } }, (data) => { console.warn( diff --git a/src/essence/Basics/Layers_/Layers_.js b/src/essence/Basics/Layers_/Layers_.js index 6d7d4fa2..56beaa35 100644 --- a/src/essence/Basics/Layers_/Layers_.js +++ b/src/essence/Basics/Layers_/Layers_.js @@ -1034,11 +1034,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 ae6f7de0..dee8e85a 100644 --- a/src/essence/Basics/Map_/Map_.js +++ b/src/essence/Basics/Map_/Map_.js @@ -783,7 +783,11 @@ async function makeVectorLayer( { evenIfOff: evenIfOff, useEmptyGeoJSON: useEmptyGeoJSON }, add, (f) => { - Map_.map.on('moveend zoomend', f) + Map_.map.on('moveend', f) + L_.subscribeTimeChange( + `dynamicgeodataset_${layerObj.name}`, + f + ) } ) diff --git a/src/essence/Tools/Layers/LayersTool.js b/src/essence/Tools/Layers/LayersTool.js index 4e2292ae..3ae49e56 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,109 @@ function interfaceWithMMGIS(fromInit) { ) return } - let geojson = L_.layers.layer[layerUUID].toGeoJSON(L_.GEOJSON_PRECISION) - let filename = layerDisplayName + const download = (geojson) => { + let filename = layerDisplayName - if (coords != 'source') - geojson = L_.convertGeoJSONLngLatsToPrimaryCoordinates(geojson) + 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, + switch (format) { + case 'geojson': + F_.downloadObject(geojson, filename, '.geojson') + break + case 'kml': + const kml = tokml( + F_.geoJSONForceSimpleStyleSpec( + geojson, true, - { x: 385, y: 6 }, - '#e9ff26', - 'black' - ) - } - ) - break - default: + 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(':') + // Only geodatasets work like this so far + if (extent === 'raw' && urlSplit[0] === 'geodatasets') { + calls.api( + 'geodatasets_get', + { + layer: urlSplitRaw[1], + type: 'geojson', + }, + (data) => { + download(data.body) + }, + (data) => { + CursorInfo.update( + `Failed to download ${layerObj.display_name}.`, + 6000, + true, + { x: 385, y: 6 }, + '#e9ff26', + 'black' + ) + console.warn( + 'ERROR: ' + + data.status + + ' in LayersTool geodatasets_get:' + + layerObj.display_name + + ' /// ' + + data.message + ) + return + } + ) + } else { + let geojson = L_.layers.layer[layerUUID].toGeoJSON( + L_.GEOJSON_PRECISION + ) + download(geojson) } })