From 58fc382ad26557a0cd18ca7cb9385e50fbb34990 Mon Sep 17 00:00:00 2001 From: Tariq Soliman Date: Wed, 16 Mar 2022 15:57:25 -0700 Subject: [PATCH 1/5] MMGIS 2.6.0 (#160) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added function to set initial layer times based on current time * Remove debug print out (#126) * #127 DrawTool - enable panning around without dropping points while d… (#128) * #127 DrawTool - enable panning around without dropping points while drawing * DrawTool - make active layer deselectable * DrawTool - Fix uploaded file edit panel * #129 BottomBar UI Visibility Modal (#130) * Fix PUBLIC_URL configure urls (#131) * env comment on DB_HOST in docker * Fix controlled layer returning null * Sublayers on Controlled Layers * #134 - Any Projection Image Marker Attachments (#137) * #139 Control Raster Filters in LithoSphere (#140) * Update litho to css friendly filter effects * #138 Projection agnostic Uncertain Ellipses, touch ups (#141) * Minor colorramp dropdown fix * leaflet-imagetransfrom map check * Dockerfile node:16 (#145) * Update README.md Node 10.10 -> Node 14.9.0 * Implement webhooks feature (#146) * First implementation of webhooks * Remove webhooktoken field * Reorganize files and functions * Clean up webhook cards * Add routes for testing webhooks when using development environment * Fix Config subpage scrollbar issue * Fix delete button Co-authored-by: Tariq Soliman * Added docs for how to use remote virtual layers via GDAL - Also updated Measure Tool to support remote DEMs * A bit more info on gdalwmscache directory in docs * Remote_Virtual_Layer typo fixes * Curtain Support (#152) * Curtain part 2 * rgrams styles * Touch ups * Use litho 1.3.0 * #151 Support 3D Uncertainty Ellipses on Point Features (#153) * 3D Uncertainty ellipses * #151 Upgrade lithosphere, more uncertainty ellipse options, docs * Fix minor bugs (#155) * Upgrade litho * Litho 1.3.2 * Litho 1.3.3 * Add LineString functions (#156) * Add function to trim layers containing LineString features * Add appendLineString function and clean up trimLineString function * Remove debug code * Minor fixes * Add args to dockerfile so PUBLIC_URL can be specified at image build time (#157) Co-authored-by: David Lees * Update the globe when vector layers are modified (#158) * Update the globe when vector layers are modified * Remove extraneous code * Remove extra variable * Fixed vector time updates not actually refreshing when told to reload (#159) * Dropdowns can expand up, draw tests * Bump to 2.6.0 Co-authored-by: Joe Roberts Co-authored-by: ac-61 Co-authored-by: dsl3000 Co-authored-by: David Lees --- API/Backend/Config/setup.js | 5 +- API/Backend/Draw/routes/draw.js | 14 + API/Backend/Draw/routes/files.js | 279 +----- API/Backend/Draw/routes/filesutils.js | 263 ++++++ API/Backend/Webhooks/models/webhooks.js | 23 + .../Webhooks/processes/triggerwebhooks.js | 222 +++++ API/Backend/Webhooks/routes/testwebhooks.js | 56 ++ API/Backend/Webhooks/routes/webhooks.js | 50 ++ API/Backend/Webhooks/routes/webhookutils.js | 34 + API/Backend/Webhooks/setup.js | 24 + CHANGELOG.md | 106 +++ Dockerfile | 5 +- README.md | 2 +- config/css/config.css | 25 + config/css/webhooks.css | 24 + config/js/calls.js | 12 + config/js/config.js | 7 +- config/js/datasets.js | 106 +-- config/js/geodatasets.js | 106 +-- config/js/keys.js | 35 +- config/js/webhooks.js | 275 ++++++ docs/docs.js | 1 + docs/pages/markdowns/JavaScript_API.md | 62 ++ docs/pages/markdowns/Measure.md | 2 + docs/pages/markdowns/Remote_Virtual_Layer.md | 79 ++ docs/pages/markdowns/Vector.md | 11 +- package-lock.json | 57 +- package.json | 5 +- sample.env | 1 + src/css/mmgis.css | 30 +- src/css/mmgisUI.css | 101 ++- src/essence/Ancillary/Coordinates.css | 18 +- src/essence/Ancillary/DataShaders.js | 5 +- src/essence/Ancillary/Description.js | 19 +- src/essence/Ancillary/Login/Login.css | 1 + src/essence/Ancillary/Modal.css | 14 +- src/essence/Ancillary/Modal.js | 1 + src/essence/Ancillary/Search.css | 2 + src/essence/Ancillary/TimeControl.js | 20 + src/essence/Basics/Formulae_/Formulae_.js | 85 +- src/essence/Basics/Globe_/Globe_.js | 1 + src/essence/Basics/Layers_/LayerCapturer.js | 76 +- .../Basics/Layers_/LayerConstructors.js | 265 ++++-- src/essence/Basics/Layers_/Layers_.js | 797 +++++++++++++++--- src/essence/Basics/Map_/Map_.js | 103 +-- .../Basics/ToolController_/ToolController_.js | 6 + .../Basics/UserInterface_/BottomBar.css | 68 ++ .../Basics/UserInterface_/BottomBar.js | 443 ++++++++++ .../Basics/UserInterface_/UserInterface_.css | 4 +- .../Basics/UserInterface_/UserInterface_.js | 253 +----- src/essence/Basics/Viewer_/Viewer_.js | 3 - src/essence/Tools/Draw/DrawTool.test.js | 13 +- src/essence/Tools/Draw/DrawTool_Drawing.js | 16 +- src/essence/Tools/Draw/DrawTool_Files.js | 35 +- .../Tools/Draw/DrawTool_SetOperations.js | 14 +- src/essence/Tools/Info/InfoTool.css | 12 +- src/essence/Tools/Kinds/Kinds.js | 6 +- .../Tools/Layers/Filtering/Filtering.js | 3 +- src/essence/Tools/Layers/LayersTool.css | 27 +- src/essence/Tools/Layers/LayersTool.js | 2 +- src/essence/Tools/Measure/MeasureTool.js | 29 +- src/essence/mmgisAPI/mmgisAPI.js | 145 ++-- src/external/Dropy/dropy.css | 7 + src/external/Dropy/dropy.js | 7 +- .../Leaflet/leaflet-imagetransform.js | 43 +- .../Leaflet/leaflet.vectorGrid.bundled.js | 18 +- src/external/OpenSeadragon/fabric.adapted.js | 245 +----- src/external/OpenSeadragon/openseadragon.js | 5 +- src/index.js | 4 + views/configure.pug | 10 +- 70 files changed, 3445 insertions(+), 1402 deletions(-) create mode 100644 API/Backend/Draw/routes/filesutils.js create mode 100644 API/Backend/Webhooks/models/webhooks.js create mode 100644 API/Backend/Webhooks/processes/triggerwebhooks.js create mode 100644 API/Backend/Webhooks/routes/testwebhooks.js create mode 100644 API/Backend/Webhooks/routes/webhooks.js create mode 100644 API/Backend/Webhooks/routes/webhookutils.js create mode 100644 API/Backend/Webhooks/setup.js create mode 100644 config/css/webhooks.css create mode 100644 config/js/webhooks.js create mode 100644 docs/pages/markdowns/Remote_Virtual_Layer.md create mode 100644 src/essence/Basics/UserInterface_/BottomBar.css create mode 100644 src/essence/Basics/UserInterface_/BottomBar.js diff --git a/API/Backend/Config/setup.js b/API/Backend/Config/setup.js index d1fec458..cc71f7a0 100644 --- a/API/Backend/Config/setup.js +++ b/API/Backend/Config/setup.js @@ -1,4 +1,5 @@ const router = require("./routes/configs"); +const triggerWebhooks = require("../Webhooks/processes/triggerwebhooks.js"); let setup = { //Once the app initializes @@ -33,7 +34,9 @@ let setup = { //Once the server starts onceStarted: (s) => {}, //Once all tables sync - onceSynced: (s) => {}, + onceSynced: (s) => { + triggerWebhooks("getConfiguration", {}); + }, }; module.exports = setup; diff --git a/API/Backend/Draw/routes/draw.js b/API/Backend/Draw/routes/draw.js index 8cde694a..ccdab4c5 100644 --- a/API/Backend/Draw/routes/draw.js +++ b/API/Backend/Draw/routes/draw.js @@ -16,6 +16,7 @@ const { sequelize } = require("../../../connection"); const router = express.Router(); const db = database.db; +const triggerWebhooks = require("../../Webhooks/processes/triggerwebhooks"); router.post("/", function (req, res, next) { res.send("test draw"); @@ -41,6 +42,7 @@ const uniqueAcrossArrays = (arr1, arr2) => { const pushToHistory = ( Table, + res, file_id, feature_id, feature_idRemove, @@ -88,6 +90,10 @@ const pushToHistory = ( Table.create(newHistoryEntry) .then((created) => { successCallback(); + triggerWebhooks("drawFileChange", { + id: file_id, + res, + }); return null; }) .catch((err) => { @@ -239,6 +245,7 @@ const clipOver = function ( if (i >= results.length) { pushToHistory( Histories, + res, req.body.file_id, newIds, oldIds, @@ -378,6 +385,7 @@ const clipUnder = function ( if (i >= results.length) { pushToHistory( Histories, + res, req.body.file_id, newIds, oldIds, @@ -566,6 +574,7 @@ const add = function ( } else { pushToHistory( Histories, + res, req.body.file_id, id, null, @@ -733,6 +742,7 @@ const edit = function (req, res, successCallback, failureCallback) { if (req.body.to_history) { pushToHistory( Histories, + res, req.body.file_id, created.id, req.body.feature_id, @@ -844,6 +854,7 @@ router.post("/remove", function (req, res, next) { //Table, file_id, feature_id, feature_idRemove, time, undoToTime, action_index pushToHistory( Histories, + res, req.body.file_id, null, req.body.id, @@ -974,6 +985,7 @@ router.post("/undo", function (req, res, next) { .then((r) => { pushToHistory( Histories, + res, req.body.file_id, null, null, @@ -1112,6 +1124,7 @@ router.post("/merge", function (req, res, next) { if (i >= results.length) { pushToHistory( Histories, + res, req.body.file_id, newIds, oldIds, @@ -1270,6 +1283,7 @@ router.post("/split", function (req, res, next) { if (i >= r.length) { pushToHistory( Histories, + res, req.body.file_id, newIds, oldIds, diff --git a/API/Backend/Draw/routes/files.js b/API/Backend/Draw/routes/files.js index c8779b7c..1fff120a 100644 --- a/API/Backend/Draw/routes/files.js +++ b/API/Backend/Draw/routes/files.js @@ -19,6 +19,9 @@ const PublishedTEST = published.PublishedTEST; const PublishedStore = require("../models/publishedstore"); const draw = require("./draw"); +const filesutils = require("./filesutils"); +const getfile = filesutils.getfile; +const triggerWebhooks = require("../../Webhooks/processes/triggerwebhooks"); const router = express.Router(); const db = database.db; @@ -89,257 +92,7 @@ router.post("/getfiles", function (req, res, next) { * published: (optional) get last published version (makes 'time' ignored) * } */ -router.post("/getfile", function (req, res, next) { - let Table = req.body.test === "true" ? UserfilesTEST : Userfiles; - let Histories = req.body.test === "true" ? FilehistoriesTEST : Filehistories; - - if (req.session.user == "guest" && req.body.quick_published !== "true") { - res.send({ - status: "failure", - message: "Permission denied.", - body: {}, - }); - } - - let published = false; - if (req.body.published === "true") published = true; - if (req.body.quick_published === "true") { - sequelize - .query( - "SELECT " + - "id, intent, parent, children, level, properties, ST_AsGeoJSON(geom)" + - " " + - "FROM " + - (req.body.test === "true" ? "publisheds_test" : "publisheds") + - "" + - (req.body.intent && req.body.intent.length > 0 - ? req.body.intent === "all" - ? " WHERE intent IN ('polygon', 'line', 'point', 'text', 'arrow')" - : " WHERE intent=:intent" - : ""), - { - replacements: { - intent: req.body.intent || "", - }, - } - ) - .spread((results) => { - let geojson = { type: "FeatureCollection", features: [] }; - for (let i = 0; i < results.length; i++) { - let properties = results[i].properties; - let feature = {}; - properties._ = { - id: results[i].id, - intent: results[i].intent, - parent: results[i].parent, - children: results[i].children, - level: results[i].level, - }; - feature.type = "Feature"; - feature.properties = properties; - feature.geometry = JSON.parse(results[i].st_asgeojson); - geojson.features.push(feature); - } - - //Sort features by level - geojson.features.sort((a, b) => - a.properties._.level > b.properties._.level - ? 1 - : b.properties._.level > a.properties._.level - ? -1 - : 0 - ); - - if (req.body.test !== "true") { - //Sort features by geometry type - geojson.features.sort((a, b) => { - if (a.geometry.type == "Point" && b.geometry.type == "Polygon") - return 1; - if (a.geometry.type == "LineString" && b.geometry.type == "Polygon") - return 1; - if (a.geometry.type == "Polygon" && b.geometry.type == "LineString") - return -1; - if (a.geometry.type == "Polygon" && b.geometry.type == "Point") - return -1; - if (a.geometry.type == "LineString" && b.geometry.type == "Point") - return -1; - if (a.geometry.type == b.geometry.type) return 0; - return 0; - }); - } - - res.send({ - status: "success", - message: "Successfully got file.", - body: geojson, - }); - }); - } else { - let idArray = false; - req.body.id = JSON.parse(req.body.id); - if (typeof req.body.id !== "number") idArray = true; - - let atThisTime = published - ? Math.floor(Date.now()) - : req.body.time || Math.floor(Date.now()); - - Table.findAll({ - where: { - id: req.body.id, - //file_owner is req.user or public is '1' - [Sequelize.Op.or]: { - file_owner: req.user, - public: "1", - }, - }, - }) - .then((file) => { - if (!file) { - res.send({ - status: "failure", - message: "Failed to access file.", - body: {}, - }); - } else { - sequelize - .query( - "SELECT history" + - " " + - "FROM file_histories" + - (req.body.test === "true" ? "_tests" : "") + - " " + - "WHERE" + - " " + - (idArray ? "file_id IN (:id)" : "file_id=:id") + - " " + - "AND time<=:time" + - " " + - (published ? "AND action_index=4 " : "") + - "ORDER BY time DESC" + - " " + - "FETCH first " + - (published ? req.body.id.length : "1") + - " rows only", - { - replacements: { - id: req.body.id, - time: atThisTime, - }, - } - ) - .spread((results) => { - let bestHistory = []; - for (let i = 0; i < results.length; i++) { - bestHistory = bestHistory.concat(results[i].history); - } - bestHistory = bestHistory.join(","); - bestHistory = bestHistory || "NULL"; - - //Find best history - sequelize - .query( - "SELECT " + - "id, file_id, level, intent, properties, ST_AsGeoJSON(geom)" + - " " + - "FROM user_features" + - (req.body.test === "true" ? "_tests" : "") + - " " + - "WHERE" + - " " + - (idArray ? "file_id IN (:id)" : "file_id=:id") + - " " + - "AND id IN (" + - bestHistory + - ")", - { - replacements: { - id: req.body.id, - }, - } - ) - .spread((results) => { - let geojson = { type: "FeatureCollection", features: [] }; - for (let i = 0; i < results.length; i++) { - let properties = JSON.parse(results[i].properties); - let feature = {}; - properties._ = { - id: results[i].id, - file_id: results[i].file_id, - level: results[i].level, - intent: results[i].intent, - }; - feature.type = "Feature"; - feature.properties = properties; - feature.geometry = JSON.parse(results[i].st_asgeojson); - geojson.features.push(feature); - } - - //Sort features by level - geojson.features.sort((a, b) => - a.properties._.level > b.properties._.level - ? 1 - : b.properties._.level > a.properties._.level - ? -1 - : 0 - ); - - if (req.body.test !== "true") { - //Sort features by geometry type - geojson.features.sort((a, b) => { - if ( - a.geometry.type == "Point" && - b.geometry.type == "Polygon" - ) - return 1; - if ( - a.geometry.type == "LineString" && - b.geometry.type == "Polygon" - ) - return 1; - if ( - a.geometry.type == "Polygon" && - b.geometry.type == "LineString" - ) - return -1; - if ( - a.geometry.type == "Polygon" && - b.geometry.type == "Point" - ) - return -1; - if ( - a.geometry.type == "LineString" && - b.geometry.type == "Point" - ) - return -1; - if (a.geometry.type == b.geometry.type) return 0; - return 0; - }); - } - - res.send({ - status: "success", - message: "Successfully got file.", - body: { - file: file, - geojson: geojson, - }, - }); - }); - }); - } - - return null; - }) - .catch((err) => { - logger("error", "Failed to get file.", req.originalUrl, req, err); - res.send({ - status: "failure", - message: "Failed to get file.", - body: {}, - }); - }); - } -}); +router.post("/getfile", getfile); /** * Makes a new file @@ -480,6 +233,10 @@ router.post("/make", function (req, res, next) { message: "Successfully made a new file from geojson.", body: {}, }); + triggerWebhooks("drawFileAdd", { + id: created.id, + res, + }); return null; }) .catch((err) => { @@ -522,6 +279,10 @@ router.post("/make", function (req, res, next) { message: "Successfully made a new file.", body: {}, }); + triggerWebhooks("drawFileAdd", { + id: created.id, + res, + }); } return null; @@ -562,6 +323,10 @@ router.post("/remove", function (req, res, next) { message: "File removed.", body: {}, }); + triggerWebhooks("drawFileDelete", { + id: req.body.id, + res, + }); return null; }) @@ -669,6 +434,10 @@ router.post("/change", function (req, res, next) { message: "File edited.", body: {}, }); + triggerWebhooks("drawFileChange", { + id: req.body.id, + res, + }); return null; }) @@ -1482,6 +1251,10 @@ router.post("/publish", function (req, res, next) { message: "Published.", body: {}, }); + triggerWebhooks("drawFileChange", { + id: files[f].dataValues.id, + res, + }); } }, (err) => { @@ -1539,6 +1312,10 @@ router.post("/publish", function (req, res, next) { Table.create(newHistoryEntry) .then((created) => { successCallback(newHistoryEntry); + triggerWebhooks("drawFileAdd", { + id: file_id, + res, + }); return null; }) .catch((err) => { diff --git a/API/Backend/Draw/routes/filesutils.js b/API/Backend/Draw/routes/filesutils.js new file mode 100644 index 00000000..8a966ed8 --- /dev/null +++ b/API/Backend/Draw/routes/filesutils.js @@ -0,0 +1,263 @@ +const logger = require("../../../logger"); +const Sequelize = require("sequelize"); +const { sequelize } = require("../../../connection"); +const fhistories = require("../models/filehistories"); +const Filehistories = fhistories.Filehistories; +const FilehistoriesTEST = fhistories.FilehistoriesTEST; +const ufiles = require("../models/userfiles"); +const Userfiles = ufiles.Userfiles; +const UserfilesTEST = ufiles.UserfilesTEST; + +function getfile(req, res, next) { + let Table = req.body.test === "true" ? UserfilesTEST : Userfiles; + let Histories = req.body.test === "true" ? FilehistoriesTEST : Filehistories; + + if (req.session.user == "guest" && req.body.quick_published !== "true") { + res.send({ + status: "failure", + message: "Permission denied.", + body: {}, + }); + } + + let published = false; + if (req.body.published === "true") published = true; + if (req.body.quick_published === "true") { + sequelize + .query( + "SELECT " + + "id, intent, parent, children, level, properties, ST_AsGeoJSON(geom)" + + " " + + "FROM " + + (req.body.test === "true" ? "publisheds_test" : "publisheds") + + "" + + (req.body.intent && req.body.intent.length > 0 + ? req.body.intent === "all" + ? " WHERE intent IN ('polygon', 'line', 'point', 'text', 'arrow')" + : " WHERE intent=:intent" + : ""), + { + replacements: { + intent: req.body.intent || "", + }, + } + ) + .spread((results) => { + let geojson = { type: "FeatureCollection", features: [] }; + for (let i = 0; i < results.length; i++) { + let properties = results[i].properties; + let feature = {}; + properties._ = { + id: results[i].id, + intent: results[i].intent, + parent: results[i].parent, + children: results[i].children, + level: results[i].level, + }; + feature.type = "Feature"; + feature.properties = properties; + feature.geometry = JSON.parse(results[i].st_asgeojson); + geojson.features.push(feature); + } + + //Sort features by level + geojson.features.sort((a, b) => + a.properties._.level > b.properties._.level + ? 1 + : b.properties._.level > a.properties._.level + ? -1 + : 0 + ); + + if (req.body.test !== "true") { + //Sort features by geometry type + geojson.features.sort((a, b) => { + if (a.geometry.type == "Point" && b.geometry.type == "Polygon") + return 1; + if (a.geometry.type == "LineString" && b.geometry.type == "Polygon") + return 1; + if (a.geometry.type == "Polygon" && b.geometry.type == "LineString") + return -1; + if (a.geometry.type == "Polygon" && b.geometry.type == "Point") + return -1; + if (a.geometry.type == "LineString" && b.geometry.type == "Point") + return -1; + if (a.geometry.type == b.geometry.type) return 0; + return 0; + }); + } + + res.send({ + status: "success", + message: "Successfully got file.", + body: geojson, + }); + }); + } else { + let idArray = false; + req.body.id = JSON.parse(req.body.id); + if (typeof req.body.id !== "number") idArray = true; + + let atThisTime = published + ? Math.floor(Date.now()) + : req.body.time || Math.floor(Date.now()); + + Table.findAll({ + where: { + id: req.body.id, + //file_owner is req.user or public is '1' + [Sequelize.Op.or]: { + file_owner: req.user, + public: "1", + }, + }, + }) + .then((file) => { + if (!file) { + res.send({ + status: "failure", + message: "Failed to access file.", + body: {}, + }); + } else { + sequelize + .query( + "SELECT history" + + " " + + "FROM file_histories" + + (req.body.test === "true" ? "_tests" : "") + + " " + + "WHERE" + + " " + + (idArray ? "file_id IN (:id)" : "file_id=:id") + + " " + + "AND time<=:time" + + " " + + (published ? "AND action_index=4 " : "") + + "ORDER BY time DESC" + + " " + + "FETCH first " + + (published ? req.body.id.length : "1") + + " rows only", + { + replacements: { + id: req.body.id, + time: atThisTime, + }, + } + ) + .spread((results) => { + let bestHistory = []; + for (let i = 0; i < results.length; i++) { + bestHistory = bestHistory.concat(results[i].history); + } + bestHistory = bestHistory.join(","); + bestHistory = bestHistory || "NULL"; + + //Find best history + sequelize + .query( + "SELECT " + + "id, file_id, level, intent, properties, ST_AsGeoJSON(geom)" + + " " + + "FROM user_features" + + (req.body.test === "true" ? "_tests" : "") + + " " + + "WHERE" + + " " + + (idArray ? "file_id IN (:id)" : "file_id=:id") + + " " + + "AND id IN (" + + bestHistory + + ")", + { + replacements: { + id: req.body.id, + }, + } + ) + .spread((results) => { + let geojson = { type: "FeatureCollection", features: [] }; + for (let i = 0; i < results.length; i++) { + let properties = JSON.parse(results[i].properties); + let feature = {}; + properties._ = { + id: results[i].id, + file_id: results[i].file_id, + level: results[i].level, + intent: results[i].intent, + }; + feature.type = "Feature"; + feature.properties = properties; + feature.geometry = JSON.parse(results[i].st_asgeojson); + geojson.features.push(feature); + } + + //Sort features by level + geojson.features.sort((a, b) => + a.properties._.level > b.properties._.level + ? 1 + : b.properties._.level > a.properties._.level + ? -1 + : 0 + ); + + if (req.body.test !== "true") { + //Sort features by geometry type + geojson.features.sort((a, b) => { + if ( + a.geometry.type == "Point" && + b.geometry.type == "Polygon" + ) + return 1; + if ( + a.geometry.type == "LineString" && + b.geometry.type == "Polygon" + ) + return 1; + if ( + a.geometry.type == "Polygon" && + b.geometry.type == "LineString" + ) + return -1; + if ( + a.geometry.type == "Polygon" && + b.geometry.type == "Point" + ) + return -1; + if ( + a.geometry.type == "LineString" && + b.geometry.type == "Point" + ) + return -1; + if (a.geometry.type == b.geometry.type) return 0; + return 0; + }); + } + + res.send({ + status: "success", + message: "Successfully got file.", + body: { + file: file, + geojson: geojson, + }, + }); + }); + }); + } + + return null; + }) + .catch((err) => { + logger("error", "Failed to get file.", req.originalUrl, req, err); + res.send({ + status: "failure", + message: "Failed to get file.", + body: {}, + }); + }); + } +} + +module.exports = { getfile }; diff --git a/API/Backend/Webhooks/models/webhooks.js b/API/Backend/Webhooks/models/webhooks.js new file mode 100644 index 00000000..4a4c406f --- /dev/null +++ b/API/Backend/Webhooks/models/webhooks.js @@ -0,0 +1,23 @@ +/*********************************************************** + * Loading all required dependencies, libraries and packages + **********************************************************/ +const Sequelize = require("sequelize"); +const { sequelize } = require("../../../connection"); + +// setup Webhooks model and its fields. +var Webhooks = sequelize.define( + "webhooks", + { + config: { + type: Sequelize.JSON, + allowNull: true, + defaultValue: {} + }, + }, + { + timestamps: true + } +); + +// export Webhooks model for use in other files. +module.exports = Webhooks; diff --git a/API/Backend/Webhooks/processes/triggerwebhooks.js b/API/Backend/Webhooks/processes/triggerwebhooks.js new file mode 100644 index 00000000..8b1ed548 --- /dev/null +++ b/API/Backend/Webhooks/processes/triggerwebhooks.js @@ -0,0 +1,222 @@ +const logger = require("../../../logger"); +const fetch = require("node-fetch"); + +const filesutils = require("../../Draw/routes/filesutils.js"); +const getfile = filesutils.getfile; + +const webhookutils = require("../../Webhooks/routes/webhookutils.js"); +const webhookEntries = webhookutils.entries; + +const INJECT_REGEX = /{(.*?)}/; + +// Save the webhook config to local memory +var webhooksConfig; + +function getWebhooks() { + var res = {}; + res.send = function (payload) { + if (payload.status == "success") { + if (payload?.body?.entries && payload.body.entries.length > 0) { + var config = JSON.parse(payload.body.entries[0].config); + webhooksConfig = config.webhooks; + } + } else { + logger( + "error", + "Unable to get webhook entries", + "TriggerWebhooks", + null, + "Unable to get webhook entries" + ); + } + }; + + webhookEntries({}, res); +} + +function triggerWebhooks(action, payload) { + if (action === "getConfiguration") { + getWebhooks(); + } + + if (!webhooksConfig) { + return; + } + + webhooksConfig.forEach((wh) => { + switch (wh.action) { + case "DrawFileChange": + if (action === "drawFileChange") { + drawFileUpdate(wh, payload); + } + break; + case "DrawFileAdd": + if (action === "drawFileAdd") { + drawFileUpdate(wh, payload); + } + break; + case "DrawFileDelete": + if (action === "drawFileDelete") { + drawFileDelete(wh, payload); + } + break; + } + }); +} + +function drawFileUpdate(webhook, payload) { + var file_id = payload.id; + var data = { + body: { + id: payload.id, + quick_published: false, + published: false, + }, + user: payload.res.req.user, + session: { + user: payload.res.req.user, + }, + }; + + var response = {}; + response.send = function (res) { + var webhookHeader = JSON.parse(webhook.header); + var webhookBody = JSON.parse(webhook.body); + var file_name = res.body?.file[0]?.file_name || null; + var geojson = res.body.geojson; + + const injectableVariables = { + file_id, + file_name, + geojson, + }; + + // Build the body + buildBody(webhookBody, injectableVariables); + + // Build the url + var url = buildUrl(webhook.url, injectableVariables); + + // Push to the remote webhook + pushToRemote(url, webhook.type, webhookHeader, webhookBody); + }; + + getfile(data, response); +} + +function buildBody(webhookBody, injectableVariables) { + // Fill in the body + for (var i in webhookBody) { + var match = INJECT_REGEX.exec(webhookBody[i]); + // Match for curly braces. If the value contains no curly braces, assume the value is hardcoded so leave the value as is + if (match) { + var variable = match[1]; + if (!injectableVariables[variable]) { + logger( + "error", + "The variable '" + variable + "' is not an injectable variable", + "Webhooks", + null, + "The variable '" + variable + "' is not an injectable variable" + ); + } + webhookBody[i] = injectableVariables[variable]; + } + } +} + +function drawFileDelete(webhook, payload) { + var file_id = payload.id; + var data = { + body: { + id: payload.id, + quick_published: false, + published: false, + }, + user: payload.res.req.user, + session: { + user: payload.res.req.user, + }, + }; + + var response = {}; + response.send = function (res) { + var webhookHeader = JSON.parse(webhook.header); + var geojson = res.body.geojson; + var file_name = res.body?.file[0]?.file_name || null; + + const injectableVariables = { + file_id, + file_name, + geojson, + }; + + // Build the url + var url = buildUrl(webhook.url, injectableVariables); + + // Push to the remote webhook + pushToRemote(url, webhook.type, webhookHeader, {}); + }; + + getfile(data, response); +} + +function buildUrl(url, injectableVariables) { + var updatedUrl = url; + var match; + while (null !== (match = INJECT_REGEX.exec(updatedUrl))) { + var variable = match[1]; + if (!injectableVariables[variable]) { + logger( + "error", + "The variable '" + variable + "' is not an injectable variable", + "Webhooks", + null, + "The variable '" + variable + "' is not an injectable variable" + ); + } + + // Stringify if the injectable variable is an object + var newVariable = injectableVariables[variable]; + if (typeof newVariable === "object" && newVariable !== null) { + newVariable = JSON.stringify(newVariable); + } + + updatedUrl = updatedUrl.replace(match[0], newVariable); + } + return updatedUrl; +} + +function pushToRemote(url, type, header, body) { + fetch(url, { + method: type, + headers: header, + body: JSON.stringify(body), + }) + .then((res) => { + if (!res.ok) { + return res.text().then((text) => { + throw new Error(text); + }); + } else { + return res.json(); + } + }) + .then((json) => { + if (json.status == "success") { + logger("info", "Successful webhook call to " + url, "TriggerWebhooks"); + } + }) + .catch(function (err) { + logger( + "error", + "Failed webhook call to " + url, + "TriggerWebhooks", + null, + err + ); + return null; + }); +} + +module.exports = triggerWebhooks; diff --git a/API/Backend/Webhooks/routes/testwebhooks.js b/API/Backend/Webhooks/routes/testwebhooks.js new file mode 100644 index 00000000..e3d82523 --- /dev/null +++ b/API/Backend/Webhooks/routes/testwebhooks.js @@ -0,0 +1,56 @@ +/*********************************************************** + * JavaScript syntax format: ES5/ES6 - ECMAScript 2015 + * Loading all required dependencies, libraries and packages + **********************************************************/ +const express = require("express"); +const router = express.Router(); + +const logger = require("../../../logger"); + +router.get("/test", function (req, res, next) { + logger("success", "Called /testwebhooks/test API", req.originalUrl, req); + res.send("pong"); +}); + +router.post("/test_webhook_post/", function (req, res, next) { + logger("success", "Called /testwebhooks/test_webhook_post API", req.originalUrl, req); + logger("info", "Body of request\n" + JSON.stringify(req.body, null, 4), req.originalUrl, req); + res.send({ + status: "success", + message: "Received data to test_webhook_post API!", + body: { + input: req.body, + } + }); +}); + +router.delete("/test_webhook_del/:id", function (req, res, next) { + logger("success", "Called /testwebhooks/test_webhook_del API", req.originalUrl, req); + logger("info", "Body of request\n" + JSON.stringify(req.body, null, 4), req.originalUrl, req); + + res.send({ + status: "success", + message: "Received data to test_webhook_del API!", + body: { + params: req.params, + input: req.body, + } + }); + +}); + +router.patch("/test_webhook_patch/:id", function (req, res, next) { + logger("success", "Called /testwebhooks/test_webhook_patch API", req.originalUrl, req); + logger("info", "Body of request\n" + JSON.stringify(req.body, null, 4), req.originalUrl, req); + + res.send({ + status: "success", + message: "Received data to test_webhook_patch API!", + body: { + params: req.params, + input: req.body, + } + }); +}); + +module.exports = router; diff --git a/API/Backend/Webhooks/routes/webhooks.js b/API/Backend/Webhooks/routes/webhooks.js new file mode 100644 index 00000000..109cb36e --- /dev/null +++ b/API/Backend/Webhooks/routes/webhooks.js @@ -0,0 +1,50 @@ +/*********************************************************** + * JavaScript syntax format: ES5/ES6 - ECMAScript 2015 + * Loading all required dependencies, libraries and packages + **********************************************************/ +const express = require("express"); +const router = express.Router(); + +const logger = require("../../../logger"); +const Webhooks = require("../models/webhooks"); + +const webhookutils = require("./webhookutils.js"); +const entries = webhookutils.entries; + +const triggerWebhooks = require("../processes/triggerwebhooks.js"); + +router.post("/save", function (req, res, next) { + let webhookConfig = { + config: req.body.config, + }; + + Webhooks.create(webhookConfig) + .then((created) => { + res.send({ + status: "success", + message: "Successfully saved webhooks config.", + }); + }) + .catch((err) => { + res.send({ + status: "failure", + message: "Failed to save webhooks config!", + body: { err }, + }); + }); +}); + +router.get("/entries", entries); + +router.post("/config", function (req, res, next) { + logger("success", "Called /webhooks/config API", req.originalUrl, req); + + triggerWebhooks("getConfiguration", {}); + // FIXME how do we check the above function ran + res.send({ + status: "success", + message: "Successfully updated webhooks config.", + }); +}); + +module.exports = { router }; diff --git a/API/Backend/Webhooks/routes/webhookutils.js b/API/Backend/Webhooks/routes/webhookutils.js new file mode 100644 index 00000000..53d401bb --- /dev/null +++ b/API/Backend/Webhooks/routes/webhookutils.js @@ -0,0 +1,34 @@ +/*********************************************************** + * JavaScript syntax format: ES5/ES6 - ECMAScript 2015 + * Loading all required dependencies, libraries and packages + **********************************************************/ +const logger = require("../../../logger"); +const Webhooks = require("../models/webhooks"); + +function entries(req, res, next) { + logger("success", "Called /webhooks/entries API", req.originalUrl, req); + Webhooks.findAll({ + order: [["updatedAt", "DESC"]], + }) + .then((sets) => { + if (sets && sets.length > 0) { + let entries = []; + for (let i = 0; i < sets.length; i++) { + entries.push({ config: sets[i].config, updated: sets[i].updatedAt }); + } + + res.send({ + status: "success", + body: { entries: entries }, + }); + } + }) + .catch((err) => { + logger("error", "Failure finding webhooks.", req.originalUrl, req, err); + res.send({ + status: "failure", + }); + }); +} + +module.exports = { entries }; diff --git a/API/Backend/Webhooks/setup.js b/API/Backend/Webhooks/setup.js new file mode 100644 index 00000000..54a8a3c0 --- /dev/null +++ b/API/Backend/Webhooks/setup.js @@ -0,0 +1,24 @@ +const routeWebhooks = require("./routes/webhooks"); +const routerWebhooks = routeWebhooks.router; +const fetch = require("node-fetch"); +const routerTestWebhooks = require("./routes/testwebhooks"); + +let setup = { + //Once the app initializes + onceInit: (s) => { + s.app.use("/API/webhooks", s.checkHeadersCodeInjection, routerWebhooks); + if (process.env.NODE_ENV === "development") { + s.app.use( + "/API/testwebhooks", + s.checkHeadersCodeInjection, + routerTestWebhooks + ); + } + }, + //Once the server starts + onceStarted: (s) => {}, + //Once all tables sync + onceSynced: (s) => {}, +}; + +module.exports = setup; diff --git a/CHANGELOG.md b/CHANGELOG.md index 3aa1b37d..d7d4cf92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,48 @@ # MMGIS Changelog +## 2.6.0 + +_Mar 16, 2022_ + +#### New Requirements + +- Node.js >= v14.9.0 + +#### Summary + +This release adds a webhook manager to the configure page and improves documentation, the mmgisAPI, projection support, as well as synchronicity between the Map and Globe. + +#### Added + +- Configurable webhook manager. +- Access to a settings modal in the bottom left toolbar to toggle various UI elements' visibilities as well as the radius of tiles to query for the 3D Globe +- Raster effects (brightness, contrast, saturation, blend-mode) now apply in 3D as well +- Controlled layers can now utilized sublayers/marker-attachments +- Marker attachments, such as uncertainty ellipses, properly work for any projection +- 3D uncertainty ellipses +- Documentation for using remote virtual layers via GDAL +- PUBLIC_URL can be specified at build now in the Dockerfile +- mmgisAPI functions apply to the 3D Globe too now +- mmgisAPI can trim LineString features at the coordinates level + +#### Changed + +- LithoSphere 1.1.0 => 1.3.0 - [See LithoSphere Releases](https://github.com/NASA-AMMOS/LithoSphere/releases) +- Users can now pan the map while in the DrawTool's draw mode without placing a point +- Time controlled layers can now default to the current time for initial queries + +#### Fixed + +- Some media paths in the /configure path not working when MMGIS is served under a subdomain with PUBLIC_URL + +#### Removed + +- + ## 2.5.0 +_Jan 10, 2022_ + #### Summary This release contains the IsochroneTool, revives the Model layer type and includes a new Query layer type. Each vector layer can now be filtered by the user through the LayersTool, leads in the DrawTool can now draw and publish arrows and annotations, and the MeasureTool finally supports continuous elevation profiles. @@ -52,6 +93,8 @@ This release contains the IsochroneTool, revives the Model layer type and includ ## 2.4.0 +_Aug 06, 2021_ + #### Summary This release adds in the Viewshed Tool, time enabled layers, [LithoSphere](https://github.com/NASA-AMMOS/LithoSphere), WMS support, data layers, a JavaScript API, and more. @@ -95,6 +138,8 @@ This release adds in the Viewshed Tool, time enabled layers, [LithoSphere](https ## 2.3.1 +_Apr 22, 2021_ + #### Summary A point release to address bug fixes. @@ -109,6 +154,8 @@ A point release to address bug fixes. ## 2.3.0 +_Apr 14, 2021_ + #### Summary The Draw Tool gets its own tag filtering system. The Measure Tool now uses great arcs and is way more accurate and the map now fully supports WMS layers! @@ -150,6 +197,8 @@ ALTER TABLE user_files ALTER COLUMN file_description TYPE VARCHAR(10000); ## 2.0.0 +_Jan 14, 2021_ + #### Migration Details - The environment variable `ALLOW_EMBED` has been replaced with `FRAME_ANCESTORS` @@ -196,6 +245,8 @@ ALTER TABLE user_files ALTER COLUMN file_description TYPE VARCHAR(10000); ## 1.3.5 +_Oct 19, 2020_ + #### Added - ALLOW_EMBED environment variable @@ -213,6 +264,8 @@ ALTER TABLE user_files ALTER COLUMN file_description TYPE VARCHAR(10000); ## 1.3.4 +_Oct 06, 2020_ + #### Added: - WMS tile support for the Map (does not yet work on the Globe). @@ -223,6 +276,8 @@ ALTER TABLE user_files ALTER COLUMN file_description TYPE VARCHAR(10000); ## 1.3.3 +_Aug 07, 2020_ + #### Added: - Example docker-compose @@ -242,6 +297,8 @@ ALTER TABLE user_files ALTER COLUMN file_description TYPE VARCHAR(10000); ## 1.3.2 +_Jul 06, 2020_ + #### Fixed - Draw Tool history sql commands assumed rows would be returned in order which could completely break the tool. @@ -252,6 +309,8 @@ ALTER TABLE user_files ALTER COLUMN file_description TYPE VARCHAR(10000); ## 1.3.1 +_May 13, 2020_ + #### Fixed - Additional authorization headers prevented access to the configure login page. @@ -260,6 +319,8 @@ ALTER TABLE user_files ALTER COLUMN file_description TYPE VARCHAR(10000); ## 1.3.0 +_Apr 16, 2020_ + #### New Requirements - Node.js >= v10.10 @@ -295,3 +356,48 @@ ALTER TABLE user_files ALTER COLUMN file_description TYPE VARCHAR(10000); - Infinite login bug - Vectors disappearing with string weights - Some endpoint calls began with home slashes that broke certain setups + +--- + +## 1.2 + +_Nov 06, 2019_ + +#### Added + +- Limit access to the entire site with .env's `AUTH=local` +- Vector Tile Layers +- Store features within Postgres by uploading them with /configure's `Manage Geodatasets`. Point to them by setting the layer URL to `geodatasets:{name}`. Can serve both geojson and vector tiles. + +--- + +## 1.1.1 + +_Oct 25, 2019_ + +#### Fixed + +- Creating a new mission on the 'configure' page failed to make the appropriate mission directories (e.g. Layers). + +--- + +## 1.1 + +_Oct 02, 2019_ + +#### Summary + +MMGIS update with the Campaign Analysis Mapping and Planning (CAMP) tool. The software now runs fully in a node environment. Various other bug fixes and minor updates have been made to the code. + +--- + +## Open Source Release + +_Jun 06, 2019_ + +#### Summary + +This represents the initial release of the Multi-Mission Geographic Information System (MMGIS) software, developed under NASA-AMMOS. + +Dr. Fred J, Calef III & Tariq K. Soliman +NASA-JPL/Caltech diff --git a/Dockerfile b/Dockerfile index cb388939..55c38361 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,7 @@ -FROM node:12 +FROM node:16 + +ARG PUBLIC_URL_ARG= +ENV PUBLIC_URL=$PUBLIC_URL_ARG # Install GDAL with Python bindings RUN apt-get -y update diff --git a/README.md b/README.md index aa84802b..41a05a2a 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ ### System Requirements -1. Install the latest version of [Node.js v10.10+](https://nodejs.org/en/download/). +1. Install the latest version of [Node.js v14.9.0+](https://nodejs.org/en/download/). 1. Install [PostgreSQL v10.14+](https://www.enterprisedb.com/downloads/postgres-postgresql-downloads). Detailed [install instructions](https://www.postgresqltutorial.com/postgresql-getting-started/) for all platforms. 1. Install [PostGIS 2.5+](https://postgis.net/install/). From the above install, you can use the 'Application Stack Builder' to install PostGIS or the default [PostGIS install instructions](https://postgis.net/install/) for all platforms. diff --git a/config/css/config.css b/config/css/config.css index b6e2db74..37134054 100644 --- a/config/css/config.css +++ b/config/css/config.css @@ -113,6 +113,20 @@ body { opacity: 0; transition: opacity 0.2s cubic-bezier(0.39, 0.575, 0.565, 1); } +.container_webhooks { + margin-left: 220px; + width: calc(100% - 220px) !important; + height: 100vh; + max-width: unset !important; + background: white; + position: absolute; + top: 0; + left: 0; + z-index: 200; + pointer-events: none; + opacity: 0; + transition: opacity 0.2s cubic-bezier(0.39, 0.575, 0.565, 1); +} .container { margin-left: 220px; width: 100% !important; @@ -265,6 +279,17 @@ body { #manage_geodatasets:hover { color: #5ea1ed; } +#manage_webhooks { + height: 30px; + width: calc(100% - 16px); + line-height: 30px; + margin: 4px 8px; + font-size: 12px; + color: #ddd; +} +#manage_webhooks:hover { + color: #5ea1ed; +} textarea { resize: vertical; diff --git a/config/css/webhooks.css b/config/css/webhooks.css new file mode 100644 index 00000000..7cda5810 --- /dev/null +++ b/config/css/webhooks.css @@ -0,0 +1,24 @@ +.webhooks { + padding: 20px; + font-family: "Roboto", sans-serif; + display: flex; + flex-flow: column; +} +.webhooks .title { + text-align: center; + font-size: 100px; + color: #ddd; +} +.webhooks .row { + margin-left: 2.5%; + margin-right: 2.5%; +} +.webhooks .CodeMirror { + margin: 0; +} +.webhooks .inject-label{ + margin-top: 0%; +} +.webhooks .card:nth-of-type(even) { + background: rgba(0, 0, 0, 0.03); +} diff --git a/config/js/calls.js b/config/js/calls.js index e52192e7..a4bed70f 100644 --- a/config/js/calls.js +++ b/config/js/calls.js @@ -68,4 +68,16 @@ let calls = { type: "POST", url: "api/longtermtoken/generate", }, + webhooks_save: { + type: "POST", + url: "api/webhooks/save", + }, + webhooks_entries: { + type: "GET", + url: "api/webhooks/entries", + }, + webhooks_config: { + type: "POST", + url: "api/webhooks/config", + }, }; diff --git a/config/js/config.js b/config/js/config.js index 59e7139c..0b25e778 100644 --- a/config/js/config.js +++ b/config/js/config.js @@ -96,6 +96,10 @@ function initialize() { $("#manage_geodatasets").on("click", function () { Geodatasets.make(); }); + //Initial manage webhooks + $("#manage_webhooks").on("click", function () { + Webhooks.make(); + }); $.ajax({ type: calls.missions.type, @@ -216,6 +220,7 @@ function initialize() { Keys.destroy(); Datasets.destroy(); Geodatasets.destroy(); + Webhooks.destroy(); $("#missions li").removeClass("active"); $(this).addClass("active"); @@ -884,7 +889,7 @@ function makeLayerBarAndModal(d, level) { dataSel = "selected"; break; case "query": - barColor = "#0fbd4d"; + barColor = "#62bd0f"; querySel = "selected"; break; case "vector": diff --git a/config/js/datasets.js b/config/js/datasets.js index 31f63ac5..feffdee8 100644 --- a/config/js/datasets.js +++ b/config/js/datasets.js @@ -1,6 +1,6 @@ var Datasets = { csv: null, - init: function() { + init: function () { // prettier-ignore var markup = [ "
", @@ -33,24 +33,24 @@ var Datasets = { Datasets.refreshNames(); //Upload - $(".container_datasets #datasetUploadButton > input").on("change", function( - evt - ) { - var files = evt.target.files; // FileList object + $(".container_datasets #datasetUploadButton > input").on( + "change", + function (evt) { + var files = evt.target.files; // FileList object - // use the 1st file from the list - var f = files[0]; - var ext = Datasets.getExtension(f.name).toLowerCase(); + // use the 1st file from the list + var f = files[0]; + var ext = Datasets.getExtension(f.name).toLowerCase(); - $(".container_datasets .datasetName input").val( - f.name - .split("." + ext)[0] - .replace(/[`~!@#$%^&*()|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, "") - ); + $(".container_datasets .datasetName input").val( + f.name + .split("." + ext)[0] + .replace(/[`~!@#$%^&*()|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, "") + ); - switch (ext) { - case "csv": - /* + switch (ext) { + case "csv": + /* var reader = new FileReader(); // Closure to capture the file information. reader.onload = (function(file) { @@ -62,16 +62,19 @@ var Datasets = { reader.readAsText(f); */ - Datasets.f = f; - $(".container_datasets .datasetUploadFilename").text(Datasets.f.name); - break; - default: - alert("Only .csv files may be uploaded."); + Datasets.f = f; + $(".container_datasets .datasetUploadFilename").text( + Datasets.f.name + ); + break; + default: + alert("Only .csv files may be uploaded."); + } } - }); + ); //Re/create - $(".container_datasets .datasetRecreate > a").on("click", function(evt) { + $(".container_datasets .datasetRecreate > a").on("click", function (evt) { let name = $(".datasetName input").val(); if (Datasets.f == null) { alert("Please upload a .csv file."); @@ -90,7 +93,7 @@ var Datasets = { let cursorSum = 0; let cursorStep = null; Papa.parse(Datasets.f, { - step: function(row, parser) { + step: function (row, parser) { if (firstStep) { header = row.data; firstStep = false; @@ -117,22 +120,22 @@ var Datasets = { name: name, csv: JSON.stringify(currentRows), header: header, - mode: first ? "full" : "append" + mode: first ? "full" : "append", }, - success: function(data) { + success: function (data) { first = false; currentRows = []; parser.resume(); }, - error: function(err) { + error: function (err) { currentRows = []; console.log(err); - } + }, }); } } }, - complete: function() { + complete: function () { if (currentRows.length > 0) { $.ajax({ type: calls.datasets_recreate.type, @@ -141,18 +144,18 @@ var Datasets = { name: name, csv: JSON.stringify(currentRows), header: header, - mode: "append" + mode: "append", }, - success: function(data) { + success: function (data) { Datasets.refreshNames(); $(".datasetRecreate a") .css("pointer-events", "inherit") .text("Re/create"); }, - error: function(err) { + error: function (err) { currentRows = []; console.log(err); - } + }, }); } else { Datasets.refreshNames(); @@ -160,39 +163,48 @@ var Datasets = { .css("pointer-events", "inherit") .text("Re/create"); } - } + }, }); }); }, - make: function() { + make: function () { $(".container_datasets").css({ opacity: 1, - pointerEvents: "inherit" + pointerEvents: "inherit", + display: "block", }); Keys.destroy(); Geodatasets.destroy(); + Webhooks.destroy(); $("#missions li.active").removeClass("active"); + $(".container").css({ + display: "none", + }); }, - destroy: function() { + destroy: function () { $(".container_datasets").css({ opacity: 0, - pointerEvents: "none" + pointerEvents: "none", + display: "none", + }); + $(".container").css({ + display: "block", }); }, - getExtension: function(string) { + getExtension: function (string) { var ex = /(?:\.([^.]+))?$/.exec(string)[1]; return ex || ""; }, - sortArrayOfObjectsByKeyValue: function(arr, key, ascending, stringify) { + sortArrayOfObjectsByKeyValue: function (arr, key, ascending, stringify) { if (arr.constructor !== Array) return arr; const side = ascending ? 1 : -1; - let compareKey = function(a, b) { + let compareKey = function (a, b) { if (a[key] < b[key]) return -1 * side; if (a[key] > b[key]) return side; return 0; }; if (stringify) { - compareKey = function(a, b) { + compareKey = function (a, b) { if (JSON.stringify(a[key]) < JSON.stringify(b[key])) return -1 * side; if (JSON.stringify(a[key]) > JSON.stringify(b[key])) return side; return 0; @@ -201,12 +213,12 @@ var Datasets = { return arr.sort(compareKey); }, - refreshNames: function() { + refreshNames: function () { $.ajax({ type: calls.datasets_entries.type, url: calls.datasets_entries.url, data: {}, - success: function(data) { + success: function (data) { if (data.status == "success") { $(".container_datasets .existing ul").html(""); let entries = Datasets.sortArrayOfObjectsByKeyValue( @@ -223,11 +235,11 @@ var Datasets = { "
" ); } - } + }, }); - } + }, }; -$(document).ready(function() { +$(document).ready(function () { Datasets.init(); }); diff --git a/config/js/geodatasets.js b/config/js/geodatasets.js index 219065be..01a5f1d0 100644 --- a/config/js/geodatasets.js +++ b/config/js/geodatasets.js @@ -1,6 +1,6 @@ var Geodatasets = { geojson: null, - init: function() { + init: function () { // prettier-ignore var markup = [ "
", @@ -35,7 +35,7 @@ var Geodatasets = { //Upload $(".container_geodatasets #geodatasetUploadButton > input").on( "change", - function(evt) { + function (evt) { var files = evt.target.files; // FileList object // use the 1st file from the list @@ -53,8 +53,8 @@ var Geodatasets = { case "geojson": var reader = new FileReader(); // Closure to capture the file information. - reader.onload = (function(file) { - return function(e) { + reader.onload = (function (file) { + return function (e) { $(".container_geodatasets .geodatasetUploadFilename").text( file.name ); @@ -71,68 +71,78 @@ var Geodatasets = { ); //Re/create - $(".container_geodatasets .geodatasetRecreate > a").on("click", function( - evt - ) { - let name = $(".geodatasetName input").val(); - if (Geodatasets.geojson == null) { - alert("Please upload a .geojson file."); - return; - } - if (name == null) { - alert("Please enter a name."); - return; - } - - $.ajax({ - type: calls.geodatasets_recreate.type, - url: calls.geodatasets_recreate.url, - data: { - name: name, - geojson: Geodatasets.geojson - }, - success: function(data) { - Geodatasets.refreshNames(); - $.ajax({ - type: calls.geodatasets_get.type, - url: calls.geodatasets_get.url + "?layer=" + name, - success: function(data) { - console.log(data); - } - }); + $(".container_geodatasets .geodatasetRecreate > a").on( + "click", + function (evt) { + let name = $(".geodatasetName input").val(); + if (Geodatasets.geojson == null) { + alert("Please upload a .geojson file."); + return; } - }); - }); + if (name == null) { + alert("Please enter a name."); + return; + } + + $.ajax({ + type: calls.geodatasets_recreate.type, + url: calls.geodatasets_recreate.url, + data: { + name: name, + geojson: Geodatasets.geojson, + }, + success: function (data) { + Geodatasets.refreshNames(); + $.ajax({ + type: calls.geodatasets_get.type, + url: calls.geodatasets_get.url + "?layer=" + name, + success: function (data) { + console.log(data); + }, + }); + }, + }); + } + ); }, - make: function() { + make: function () { $(".container_geodatasets").css({ opacity: 1, - pointerEvents: "inherit" + pointerEvents: "inherit", + display: "block", }); Keys.destroy(); Datasets.destroy(); + Webhooks.destroy(); $("#missions li.active").removeClass("active"); + $(".container").css({ + display: "none", + }); }, - destroy: function() { + destroy: function () { $(".container_geodatasets").css({ opacity: 0, - pointerEvents: "none" + pointerEvents: "none", + display: "none", + }); + $(".container").css({ + display: "block", }); }, - getExtension: function(string) { + getExtension: function (string) { var ex = /(?:\.([^.]+))?$/.exec(string)[1]; return ex || ""; }, - sortArrayOfObjectsByKeyValue: function(arr, key, ascending, stringify) { + sortArrayOfObjectsByKeyValue: function (arr, key, ascending, stringify) { if (arr.constructor !== Array) return arr; const side = ascending ? 1 : -1; - let compareKey = function(a, b) { + let compareKey = function (a, b) { if (a[key] < b[key]) return -1 * side; if (a[key] > b[key]) return side; return 0; }; if (stringify) { - compareKey = function(a, b) { + compareKey = function (a, b) { if (JSON.stringify(a[key]) < JSON.stringify(b[key])) return -1 * side; if (JSON.stringify(a[key]) > JSON.stringify(b[key])) return side; return 0; @@ -141,12 +151,12 @@ var Geodatasets = { return arr.sort(compareKey); }, - refreshNames: function() { + refreshNames: function () { $.ajax({ type: calls.geodatasets_entries.type, url: calls.geodatasets_entries.url, data: {}, - success: function(data) { + success: function (data) { if (data.status == "success") { $(".container_geodatasets .existing ul").html(""); let entries = Geodatasets.sortArrayOfObjectsByKeyValue( @@ -163,11 +173,11 @@ var Geodatasets = { "
" ); } - } + }, }); - } + }, }; -$(document).ready(function() { +$(document).ready(function () { Geodatasets.init(); }); diff --git a/config/js/keys.js b/config/js/keys.js index 61dad6d1..7641abcb 100644 --- a/config/js/keys.js +++ b/config/js/keys.js @@ -1,6 +1,6 @@ var Keys = { token: null, - init: function() { + init: function () { // prettier-ignore var markup = [ "
", @@ -47,44 +47,53 @@ var Keys = { $(".keys select").material_select(); - $(".keys_generate").on("click", function() { + $(".keys_generate").on("click", function () { $.ajax({ type: calls.longtermtoken_generate.type, url: calls.longtermtoken_generate.url, data: { - period: $("select.keys_generation_period").val() + period: $("select.keys_generation_period").val(), }, - success: function(data) { + success: function (data) { if (data.status === "success") { Keys.token = data.body.token; $("#keys_token").text(Keys.token); $("#keys_examples span").text(Keys.token); } }, - error: function(err) { + error: function (err) { Keys.token = null; console.log(err); - } + }, }); }); - $("#keys_token_copy").on("click", function() { + $("#keys_token_copy").on("click", function () { Keys.copyToClipboard(Keys.token); }); }, - make: function() { + make: function () { $(".container_keys").css({ opacity: 1, - pointerEvents: "inherit" + pointerEvents: "inherit", + display: "block", }); Geodatasets.destroy(); Datasets.destroy(); + Webhooks.destroy(); $("#missions li.active").removeClass("active"); + $(".container").css({ + display: "none", + }); }, - destroy: function() { + destroy: function () { $(".container_keys").css({ opacity: 0, - pointerEvents: "none" + pointerEvents: "none", + display: "none", + }); + $(".container").css({ + display: "block", }); }, copyToClipboard(text) { @@ -106,9 +115,9 @@ var Keys = { document.getSelection().removeAllRanges(); // Unselect everything on the HTML document document.getSelection().addRange(selected); // Restore the original selection } - } + }, }; -$(document).ready(function() { +$(document).ready(function () { Keys.init(); }); diff --git a/config/js/webhooks.js b/config/js/webhooks.js new file mode 100644 index 00000000..01d12855 --- /dev/null +++ b/config/js/webhooks.js @@ -0,0 +1,275 @@ +var webhooksCounter = 0; +var cardEditors = {}; + +var Webhooks = { + geojson: null, + init: function () { + // prettier-ignore + var markup = [ + "
", + "
Webhooks
", + "
", + "
", + "Add Webhook", + "
", + "", + + "
" + ].join('\n'); + + $(".container_webhooks").html(markup); + + $(".webhooks select").material_select(); + + //Add webhook button + $("#addNewWebhook").on("click", function () { + makeWebhookCard(); + refreshWebhooks(); + }); + + //Save changes button + $("#saveWebhookChanges").on("click", saveWebhookChanges); + + Webhooks.refreshNames(); + }, + make: function () { + $(".container_webhooks").css({ + opacity: 1, + pointerEvents: "inherit", + display: "block", + }); + Keys.destroy(); + Datasets.destroy(); + Geodatasets.destroy(); + $("#missions li.active").removeClass("active"); + $(".container").css({ + display: "none", + }); + }, + destroy: function () { + $(".container_webhooks").css({ + opacity: 0, + pointerEvents: "none", + display: "none", + }); + $(".container").css({ + display: "block", + }); + }, + refreshNames: function () { + $.ajax({ + type: calls.webhooks_entries.type, + url: calls.webhooks_entries.url, + data: {}, + success: function (data) { + if (data.status == "success") { + if (data.body && data.body.entries && data.body.entries.length > 0) { + var config = JSON.parse(data.body.entries[0].config); + var webhooks = config.webhooks; + for (let i = 0; i < webhooks.length; i++) { + makeWebhookCard(webhooks[i]); + } + refreshWebhooks(); + } + } + }, + }); + }, +}; + +function makeWebhookCard(data) { + // prettier-ignore + $("#webhooksParent").append( + "
" + + "
    " + + "
  • " + + "
    " + + "" + + "" + + "
    " + + "
    " + + "" + + "" + + "
    " + + "
    " + + "" + + "" + + "
    " + + "
  • " + + "
  • " + + "
    " + + "" + + "" + + "
    " + + "
  • " + + "
  • " + + "
    " + + "" + + "" + + "
    " + + "
  • " + + "
  • " + + "
    " + + "" + + "
    " + + "
    " + + "Delete" + + "
    " + + "
  • " + + "
" + + "
" + ) + + $(".webhooks #webhookUrl_" + webhooksCounter).val( + data && data.url ? data.url : "" + ); + + cardEditors["webhookHeader_" + webhooksCounter] = CodeMirror.fromTextArea( + document.getElementById("webhookHeader_" + webhooksCounter), + { + path: "js/codemirror/codemirror-5.19.0/", + mode: "javascript", + theme: "elegant", + viewportMargin: Infinity, + lineNumbers: true, + autoRefresh: true, + matchBrackets: true, + } + ); + + const headerDefault = { + "Content-Type": "application/json", + }; + + cardEditors["webhookHeader_" + webhooksCounter].setValue( + JSON.stringify( + data && data.header ? JSON.parse(data.header) : headerDefault, + null, + 4 + ) + ); + + cardEditors["webhookBody_" + webhooksCounter] = CodeMirror.fromTextArea( + document.getElementById("webhookBody_" + webhooksCounter), + { + path: "js/codemirror/codemirror-5.19.0/", + mode: "javascript", + theme: "elegant", + viewportMargin: Infinity, + lineNumbers: true, + autoRefresh: true, + matchBrackets: true, + } + ); + + if (data && data.body) { + cardEditors["webhookBody_" + webhooksCounter].setValue( + JSON.stringify(JSON.parse(data.body), null, 4) + ); + } + + //Delete webhook button + $("#deleteWebhook_" + webhooksCounter).on("click", function () { + var deleteThis = $(this).parent().parent().parent(); + deleteThis.remove(); + }); + + webhooksCounter++; +} + +function refreshWebhooks() { + Materialize.updateTextFields(); + $(".webhooks select").material_select(); +} + +function saveWebhookChanges() { + var json = { webhooks: [] }; + + $("#webhooksParent") + .children("div") + .each(function () { + var webhookId = $(this).attr("webhookId"); + var action = $(this).find("#webhookAction").val(); + var type = $(this).find("#webhookType").val(); + var url = $(this) + .find("#webhookUrl_" + webhookId) + .val(); + var header = cardEditors["webhookHeader_" + webhookId] + ? cardEditors["webhookHeader_" + webhookId].getValue() || "{}" + : "{}"; + var body = cardEditors["webhookBody_" + webhookId] + ? cardEditors["webhookBody_" + webhookId].getValue() || "{}" + : "{}"; + + json.webhooks.push({ + action, + type, + url, + header, + body, + }); + }); + + saveWebhookConfig(json); +} + +function saveWebhookConfig(json) { + $.ajax({ + type: calls.webhooks_save.type, + url: calls.webhooks_save.url, + data: { + config: JSON.stringify(json), + }, + success: function (data) { + if (data.status == "success") { + // Update the variable holding the webhooks configuration + updateWebhookConfig(); + + Materialize.toast( + "Save Successful.", + 1600 + ); + $("#toast_success").parent().css("background-color", "#1565C0"); + } else { + Materialize.toast( + "" + data["message"] + "", + 5000 + ); + $("#toast_failure8").parent().css("background-color", "#a11717"); + } + }, + }); +} + +function updateWebhookConfig() { + $.ajax({ + type: calls.webhooks_config.type, + url: calls.webhooks_config.url, + success: function (d) { + if (d.status == "success") { + console.log("Updated webhooks config in backend"); + } + }, + }); +} + +$(document).ready(function () { + Webhooks.init(); +}); diff --git a/docs/docs.js b/docs/docs.js index 56a328e5..05ae684f 100644 --- a/docs/docs.js +++ b/docs/docs.js @@ -23,6 +23,7 @@ let configure = [ "Keys", "Manage_Datasets", "Manage_Geodatasets", + "Remote_Virtual_Layer" ]; let layers = [ "Data", diff --git a/docs/pages/markdowns/JavaScript_API.md b/docs/pages/markdowns/JavaScript_API.md index 81c2f1e8..bf0808f9 100644 --- a/docs/pages/markdowns/JavaScript_API.md +++ b/docs/pages/markdowns/JavaScript_API.md @@ -114,6 +114,68 @@ The following is an example of how to call the `keepLastN` function: window.mmgisAPI.keepLastN('Waypoints', 2) ``` +### trimLineString(layerName, time, timeProp, trimN, startOrEnd) + +This function is used to trim a specified number of vertices on a specified layer containing GeoJson LineString features. This makes the following assumptions: +- If trimming from the beginning of the layer, the time in the `time` parameter must be after the start time of the first feature in the layer +- If trimming from the end of the layer, the time in the `time` parameter must be before the end time of the last feature in the layer + +#### Function parameters + +- `layerName` - name of layer to update +- `time` - absolute time in the format of YYYY-MM-DDThh:mm:ssZ; represents start time if trimming from the beginning, otherwise represents the end time +- `timeProp` - key representing the time property to be updated in the layer +- `trimN` - number of vertices to trim +- `startOrEnd` - direction to trim from; value can only be one of the following options: start, end + +The following are examples of how to call the `trimLineString` function: + +```javascript +window.mmgisAPI.trimLineString('Traverse', '2021-12-01T15:03:00.000Z', 'start_time', 7, 'start') +``` + +```javascript +window.mmgisAPI.trimLineString('Traverse', '2021-12-01T15:13:00.000Z', 'end_time', 7, 'end') +``` + +### appendLineString(layerName, inputData, timeProp) +This function appends input data with a GeoJson Feature that contains a LineString. The LineString vertices from the input data is appended as vertices to the last Feature in the layer. The input data should also contain a property with `timeProp` as the key, representing the new end time for the updated data. + +#### Function parameters + +- `layerName` - name of layer to update +- `inputData` - GeoJson data containing a single Feature containing a LineString +- `timeProp` - key representing the time property to be updated in the layer + +The following is an example of how to call the `appendLineString` function: + +```javascript +window.mmgisAPI.appendLineString( + 'Traverse', + { + 'type':'Feature', + 'properties':{ + 'name': 2, + 'Length_m': 0, + 'COLOR': 0, + 'route': 0, + 'start_time': '2021-12-01T15:10:00.000Z', + 'end_time': '2021-12-01T15:20:00.000Z' + }, + 'geometry':{ + 'type':'LineString', + 'coordinates':[ + [145.862136,-73.208439], + [135.063782,-71.898251], + [130.828697,-75.540527], + [122.767247,-72.658683], + [120.133499,-75.018059] + ] + } + }, + 'end_time'); +``` + ## Time Control ## toggleTimeUI(visibility) diff --git a/docs/pages/markdowns/Measure.md b/docs/pages/markdowns/Measure.md index 5d41610e..894aef0c 100644 --- a/docs/pages/markdowns/Measure.md +++ b/docs/pages/markdowns/Measure.md @@ -14,6 +14,8 @@ On the Configure page, under Tools, you can specify which digital elevation mode At this time, only one DEM can be specified. This DEM should be georeferenced (i.e. have a projection defined). +A remote DEM may be specified using a GDAL XML description file. See [Remote Virtual Layer](?page=Remote_Virtual_Layer) for more information. + ## Tool Use - To make a measurement, left-click on the Measure Tool (graph icon), then left-click on the Map to create an anchor point for the measurement. As you move the mouse, the distance and angle (positive clockwise angle from north (i.e. top of map)) will read out and the distance will "rubber band" as you move. If you left-click again, the tool will display an elevation cross-section/profile. Mousing over the profile will show the raw elevation value at the samples points as well as a small yellow ball displayed between the two points on the map that correspond to where you are on the profile. If you left-click on the map at a new location, a new profile will be created between that point and the previous one. The total distance and section distance will be shown. diff --git a/docs/pages/markdowns/Remote_Virtual_Layer.md b/docs/pages/markdowns/Remote_Virtual_Layer.md new file mode 100644 index 00000000..07551eb0 --- /dev/null +++ b/docs/pages/markdowns/Remote_Virtual_Layer.md @@ -0,0 +1,79 @@ +# Remote Virtual Layers + +A remote virtual layer can be supported via various GDAL drivers. The most common is the [GDAL WMS Driver](https://gdal.org/drivers/raster/wms.html). This allows MMGIS to treat remote datasets as if they were local. + +## GDAL XML Desciption File Template + +Here is a template of a GDAL XML Description file that may be used to access a remote DEM for the [Measure Tool](?page=Measure). + +```xml + + + + 1.1.1 + + http://localhost/map/? + + IAU2000%3A30166%2C31.1492746341015%2C-85.391176037601 + image/tiff + + some_dem_layer + + + + -2091.701 + 3505.141 + 6119.299 + -3638.859 + + 8211 + 7144 + + + +proj=stere +lat_0=-85.391176037601 +lon_0=31.1492746341015 +k=1 +x_0=0 +y_0=0 +a=1737400 +b=1737400 +units=m +no_defs + 1 + + Float32 + + + + 604800 + + ./gdalwmscache + + +``` + +## GDAL Local Caching + +If the `` tag is included in the XML description file, GDAL will by default create a directory named `gdalwmscache` at the root location of MMGIS (directory must have write permissions for this to work). It is highly recommended to include this capability to significantly improve performance. Initial queries to a remote dataset may take several seconds, but subsequent queries that hit the cache are just as fast as accessing a local file. + +## Remote XML Description File + +Typically, an XML description file is generated locally for any dataset that is to be accessed remotely. This file can be place in the mission's Data/ directly for easy access by MMGIS. + +However, it is also possible to access a remote XML description file on another server. This can enable more control for dynamic access. This is accomplished via GDAL's `/vsicurl/` prefix to access network locations. For example, instead of specifying a local XML description file for a DEM like so: + +```javascript +{ + "dem": "data/description.xml" +} +``` + +A remote XML description file can be specified like this: + +```javascript +{ + "dem": "/vsicurl/http://localhost/description.xml" +} +``` + +Other `vsi*` options exist for commercial cloud storage such as S3: `/vsis3/` + +Note: for directly accessing cloud-optimized GeoTIFFs, the XML description file is unnecessary and can be bypassed altogether by using the `/vsis3/` prefix and referencing the remote file path. + +See GDAL documentation for more information about virtual file systems: https://gdal.org/user/virtual_file_systems.html#network-based-file-systems + +## More Information + +For more details about the XML description file, see the official GDAL documentation here: https://gdal.org/drivers/raster/wms.html#xml-description-file diff --git a/docs/pages/markdowns/Vector.md b/docs/pages/markdowns/Vector.md index 52a1c531..044b9541 100644 --- a/docs/pages/markdowns/Vector.md +++ b/docs/pages/markdowns/Vector.md @@ -140,6 +140,8 @@ Example: "angleProp": "path.to.angle.prop", "angleUnit": "deg || rad", "color": "#888888", + "color3d": "#FFFF00", + "depth3d": 8 }, "image": { "initialVisibility": true, @@ -209,7 +211,14 @@ Example: - `axisUnit`: "meters || kilometers", - `angleProp`: Prop path to the rotation of the ellipse. - `angleUnit`: "deg || rad" - - `color`: A css fill color. Will be made more transparent than set. + - `color`: A css fill color. Will be made more transparent than set. Default 'white' + - `fillOpacity`: Map and clamped ellipse fill opacity. 0 to 1. Default 0.25 + - `strokeColor`: Map and clamped ellipse stroke/border color. Default 'black' + - `weight`: Map and clamped ellipse stroke/border weight/thickness. Default 1 + - `opacity`: Overall Map and clamped ellipse opacity. Default 0.8 + - `color3d`: 3d curtain ellipse color. Can be an array for a vertical gradient: ["rgba(0,0,0,0)", "#26A8FF"] + - `depth3d`: Depth in meters for 3d ellipse curve. Default 2 + - `opacity3d`: 3d curtain ellipse opacity - `image`: Places a scaled and orientated image under each marker. A sublayer. - `initialVisibility`: Whether the image sublayer is initially on. Users can toggle sublayers on and off in the layer settings in the LayersTool. - `path`: A url to a (preferably) top-down north-facing orthographic image. diff --git a/package-lock.json b/package-lock.json index 0cac81f5..debb8ff4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mmgis", - "version": "2.4.1", + "version": "2.5.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mmgis", - "version": "2.4.1", + "version": "2.5.0", "dependencies": { "@babel/core": "7.9.0", "@svgr/webpack": "4.3.3", @@ -61,14 +61,13 @@ "jest-resolve": "24.9.0", "jest-watch-typeahead": "0.4.2", "jquery": "^3.5.1", - "lithosphere": "^1.1.0", + "lithosphere": "^1.3.3", "mark.js": "^8.11.1", "memorystore": "^1.6.2", "mini-css-extract-plugin": "0.9.0", "nipplejs": "^0.8.5", "node-fetch": "^2.6.1", "node-schedule": "^1.3.2", - "openseadragon": "^2.4.2", "optimize-css-assets-webpack-plugin": "5.0.3", "pg-promise": "^10.6.1", "png-js": "^1.0.0", @@ -11884,15 +11883,15 @@ "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=" }, "node_modules/lithosphere": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/lithosphere/-/lithosphere-1.1.0.tgz", - "integrity": "sha512-EXdddtN1ModnPN6s3aMSSe9W19qWWt01EGhlGhk1RxAwboXkjIwTeVmIT40PXwIWcjyL7wLH3VHPPoYdnW67eg==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/lithosphere/-/lithosphere-1.3.3.tgz", + "integrity": "sha512-Dc/MubQT3Yfu7MrtK8vfwx66lIvKBCfrkSMCsZlGGOWa9ZiCpg7wS5IubRIrOaAQypy/5hWdNF1nc1J7MWAiZQ==", "dependencies": { "@turf/boolean-intersects": "^6.3.0", "@turf/circle": "^6.3.0", "3d-tiles-renderer": "^0.2.6", "proj4": "^2.7.0", - "three": "^0.125.2" + "three": ">=0.122.0" }, "peerDependencies": { "three": ">=0.122.0" @@ -13187,11 +13186,6 @@ "node": ">=8" } }, - "node_modules/openseadragon": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/openseadragon/-/openseadragon-2.4.2.tgz", - "integrity": "sha512-398KbZwRtOYA6OmeWRY4Q0737NTacQ9Q6whmr9Lp1MNQO3p0eBz5LIASRne+4gwequcSM1vcHcjfy3dIndQziw==" - }, "node_modules/opn": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", @@ -17990,9 +17984,9 @@ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=" }, "node_modules/three": { - "version": "0.125.2", - "resolved": "https://registry.npmjs.org/three/-/three-0.125.2.tgz", - "integrity": "sha512-7rIRO23jVKWcAPFdW/HREU2NZMGWPBZ4XwEMt0Ak0jwLUKVJhcKM55eCBWyGZq/KiQbeo1IeuAoo/9l2dzhTXA==" + "version": "0.138.3", + "resolved": "https://registry.npmjs.org/three/-/three-0.138.3.tgz", + "integrity": "sha512-4t1cKC8gimNyJChJbaklg8W/qj3PpsLJUIFm5LIuAy/hVxxNm1ru2FGTSfbTSsuHmC/7ipsyuGKqrSAKLNtkzg==" }, "node_modules/throat": { "version": "4.1.0", @@ -19936,9 +19930,9 @@ } }, "node_modules/wkt-parser": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/wkt-parser/-/wkt-parser-1.3.1.tgz", - "integrity": "sha512-XK5qV+Y5gsygQfHx2/cS5a7Zxsgleaw8iX5UPC5eOXPc0TgJAu1JB9lr0iYYX3zAnN3p0aNiaN5c+1Bdblxwrg==" + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/wkt-parser/-/wkt-parser-1.3.2.tgz", + "integrity": "sha512-A26BOOo7sHAagyxG7iuRhnKMO7Q3mEOiOT4oGUmohtN/Li5wameeU4S6f8vWw6NADTVKljBs8bzA8JPQgSEMVQ==" }, "node_modules/wkx": { "version": "0.4.8", @@ -30277,15 +30271,15 @@ "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=" }, "lithosphere": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/lithosphere/-/lithosphere-1.1.0.tgz", - "integrity": "sha512-EXdddtN1ModnPN6s3aMSSe9W19qWWt01EGhlGhk1RxAwboXkjIwTeVmIT40PXwIWcjyL7wLH3VHPPoYdnW67eg==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/lithosphere/-/lithosphere-1.3.3.tgz", + "integrity": "sha512-Dc/MubQT3Yfu7MrtK8vfwx66lIvKBCfrkSMCsZlGGOWa9ZiCpg7wS5IubRIrOaAQypy/5hWdNF1nc1J7MWAiZQ==", "requires": { "@turf/boolean-intersects": "^6.3.0", "@turf/circle": "^6.3.0", "3d-tiles-renderer": "^0.2.6", "proj4": "^2.7.0", - "three": "^0.125.2" + "three": ">=0.122.0" } }, "load-json-file": { @@ -31380,11 +31374,6 @@ } } }, - "openseadragon": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/openseadragon/-/openseadragon-2.4.2.tgz", - "integrity": "sha512-398KbZwRtOYA6OmeWRY4Q0737NTacQ9Q6whmr9Lp1MNQO3p0eBz5LIASRne+4gwequcSM1vcHcjfy3dIndQziw==" - }, "opn": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", @@ -35359,9 +35348,9 @@ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=" }, "three": { - "version": "0.125.2", - "resolved": "https://registry.npmjs.org/three/-/three-0.125.2.tgz", - "integrity": "sha512-7rIRO23jVKWcAPFdW/HREU2NZMGWPBZ4XwEMt0Ak0jwLUKVJhcKM55eCBWyGZq/KiQbeo1IeuAoo/9l2dzhTXA==" + "version": "0.138.3", + "resolved": "https://registry.npmjs.org/three/-/three-0.138.3.tgz", + "integrity": "sha512-4t1cKC8gimNyJChJbaklg8W/qj3PpsLJUIFm5LIuAy/hVxxNm1ru2FGTSfbTSsuHmC/7ipsyuGKqrSAKLNtkzg==" }, "throat": { "version": "4.1.0", @@ -37071,9 +37060,9 @@ } }, "wkt-parser": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/wkt-parser/-/wkt-parser-1.3.1.tgz", - "integrity": "sha512-XK5qV+Y5gsygQfHx2/cS5a7Zxsgleaw8iX5UPC5eOXPc0TgJAu1JB9lr0iYYX3zAnN3p0aNiaN5c+1Bdblxwrg==" + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/wkt-parser/-/wkt-parser-1.3.2.tgz", + "integrity": "sha512-A26BOOo7sHAagyxG7iuRhnKMO7Q3mEOiOT4oGUmohtN/Li5wameeU4S6f8vWw6NADTVKljBs8bzA8JPQgSEMVQ==" }, "wkx": { "version": "0.4.8", diff --git a/package.json b/package.json index b036c0eb..225cff5f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mmgis", - "version": "2.5.0", + "version": "2.6.0", "description": "A web-based mapping and localization solution for science operation on planetary missions.", "homepage": "build", "repository": { @@ -95,14 +95,13 @@ "jest-resolve": "24.9.0", "jest-watch-typeahead": "0.4.2", "jquery": "^3.5.1", - "lithosphere": "^1.1.0", + "lithosphere": "^1.3.3", "mark.js": "^8.11.1", "memorystore": "^1.6.2", "mini-css-extract-plugin": "0.9.0", "nipplejs": "^0.8.5", "node-fetch": "^2.6.1", "node-schedule": "^1.3.2", - "openseadragon": "^2.4.2", "optimize-css-assets-webpack-plugin": "5.0.3", "pg-promise": "^10.6.1", "png-js": "^1.0.0", diff --git a/sample.env b/sample.env index e7301549..c9874c52 100644 --- a/sample.env +++ b/sample.env @@ -34,6 +34,7 @@ CLEARANCE_NUMBER DISABLE_LINK_SHORTENER=false #DB +# If using docker, DB_HOST is the database container name DB_HOST=localhost # Postgres' default port is 5432 DB_PORT=5432 diff --git a/src/css/mmgis.css b/src/css/mmgis.css index 8e91552e..8f2ce009 100644 --- a/src/css/mmgis.css +++ b/src/css/mmgis.css @@ -32,11 +32,11 @@ body { /*Color variables*/ :root { - --color-mmgis: #26a8ff; + --color-mmgis: #08aeea; --color-a: #010102; /*#1f1f1f;*/ --color-a1: #1f1f1f; --color-b: #555555; - --color-c: #009eff; + --color-c: #08aeea; --color-d: #2a2a2a; --color-e: #4f4f4f; --color-f: #e1e1e1; @@ -49,6 +49,7 @@ body { --color-m: #c4c4c4; --color-n: #151619; --color-o: #1a628e; + --color-p: #747474; --color-green: #00d200; --color-yellow: #d2b800; @@ -70,6 +71,18 @@ body { --color-r5: #00b7c7; --color-r6: #8be04e; --color-r7: #ebdc78; + + --color-p0: #bdbd0f; + --color-p1: #c0822f; + --color-p2: #ffeaaf; + --color-p3: #62bd0f; + --color-p4: #bd0f32; + --color-p5: #edd49e; + --color-p6: #e7bdcb; + --color-p7: #72d1cb; + --color-p8: #08aeea; + --color-p9: #770fbd; + --color-p10: #bd0f8e; } #nodeenv { @@ -156,6 +169,9 @@ body { justify-content: space-evenly; align-items: center; } +.splitterVInner > div { + border-radius: 3px; +} .splitterVInner i { width: 48px; @@ -295,7 +311,7 @@ body { width: 20px !important; height: 20px !important; color: #ffffff !important; - background: #26a8ff !important; + background: var(--color-mmgis) !important; } .leaflet-popup-content { margin: 1px 7px 1px 7px; @@ -416,11 +432,11 @@ body { color: var(--color-f); width: 30px; height: 30px; - line-height: 29px !important; border-radius: 3px; - margin-bottom: 4px; + margin-bottom: 5px; + font-size: 18px !important; border: none !important; - border-radius: 0 !important; + border-radius: 3px !important; background: var(--color-a) !important; transition: color 0.2s ease-in 0s; } @@ -625,6 +641,8 @@ body { padding: 0px 8px; font-size: 14px; color: var(--color-mw2); + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; flex: 1; transition: background 0.2s ease-out, border 0.2s ease-out; } diff --git a/src/css/mmgisUI.css b/src/css/mmgisUI.css index 5419af12..e2fb46ce 100644 --- a/src/css/mmgisUI.css +++ b/src/css/mmgisUI.css @@ -639,7 +639,7 @@ ::-webkit-scrollbar-thumb { border: 3px solid rgba(0, 0, 0, 0); background-clip: padding-box; - border-radius: 7px; + border-radius: 0px; background-color: #666; box-shadow: inset -1px -1px 0px rgba(0, 0, 0, 0.05), inset 1px 1px 0px rgba(0, 0, 0, 0.05); @@ -911,6 +911,12 @@ blink { opacity: 0.7; transition: opacity 0.4s; } +.slider2.darker { + background: var(--color-a); +} +.slider2.lighter { + background: var(--color-m1); +} .slider2:hover { opacity: 1; @@ -1280,6 +1286,7 @@ input[type='range'].verticalSlider:focus::-ms-fill-upper { } .mmgisHoverBlue { + cursor: pointer; color: var(--color-f); transition: all 0.2s ease-in 0s; } @@ -1292,3 +1299,95 @@ input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } + +/* MMGIS checkbox */ + +/* CHECKBOX */ +/* +
+ + +
+*/ +/* +
+*/ +.mmgis-checkbox { + position: relative; + width: 20px; + height: 20px; + margin: 5px; +} + +.mmgis-checkbox label { + background-color: var(--color-k); + border: 2px solid var(--color-p); + cursor: pointer; + width: 20px; + height: 20px; + position: absolute; + left: 0; + top: 0; + border-radius: 3px; + transition: all 0.2s ease; +} +@media (pointer: fine) and (hover: hover) { + .mmgis-checkbox label:hover { + background-color: var(--color-j); + border: 2px solid var(--color-f); + } +} + +.mmgis-checkbox label::after { + border: 2px solid var(--color-k); + border-top: none; + border-right: none; + content: ''; + width: 9px; + height: 4px; + position: absolute; + left: 4px; + top: 5px; + opacity: 0; + transform: rotate(-45deg); + transition: all 0.2s ease; +} + +.mmgis-checkbox input[type='checkbox'] { + visibility: hidden; +} + +.mmgis-checkbox input[type='checkbox']:checked + label { + background-color: var(--color-f); + border-color: var(--color-f); +} + +.mmgis-checkbox input[type='checkbox']:checked + label::after { + opacity: 1; +} + +.mmgisCheckbox { + margin: 5px; + width: 20px; + height: 20px; + border: 2px solid #777; + border-radius: 3px; + cursor: pointer; + transition: background 0.2s cubic-bezier(0.39, 0.575, 0.565, 1), + border 0.1s cubic-bezier(0.39, 0.575, 0.565, 1), + border-radius 0.1s cubic-bezier(0.39, 0.575, 0.565, 1); +} +.mmgisCheckbox.on { + background: white; + border: 0px solid #777; +} +.mmgisCheckbox:not(.on):hover { + background: rgba(255, 255, 255, 0.1); +} +.mmgisCheckbox.loading { + background: transparent; + border: 3px solid rgba(255, 255, 255, 0.3); + border-radius: 50%; + border-top-color: #fff; + animation: layerLoadingSpin 1s ease-in-out infinite; +} diff --git a/src/essence/Ancillary/Coordinates.css b/src/essence/Ancillary/Coordinates.css index 3bff4a60..c25b6180 100644 --- a/src/essence/Ancillary/Coordinates.css +++ b/src/essence/Ancillary/Coordinates.css @@ -14,7 +14,7 @@ line-height: 30px; padding: 0px 6px; background: var(--color-a); - border-radius: 4px; + border-radius: 3px; display: flex; } #CoordinatesDiv #mouseDesc { @@ -48,7 +48,7 @@ position: absolute; right: 63px; display: flex; - border-radius: 4px; + border-radius: 3px; transition: opacity 0.2s ease-in; } #CoordinatesDiv .mouseLngLatPicking.active { @@ -96,8 +96,8 @@ #CoordinatesDiv #mouseGoPicking { padding: 0px 6px; background: var(--color-h); - border-top-right-radius: 4px; - border-bottom-right-radius: 4px; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; transition: background 0.2s ease-in; cursor: pointer; color: var(--color-a); @@ -112,7 +112,7 @@ color: var(--color-f); text-align: center; cursor: pointer; - border-radius: 4px; + border-radius: 3px; width: 24px; height: 24px; line-height: 24px; @@ -141,8 +141,8 @@ line-height: 24px; margin: 3px 0px 3px 5px; pointer-events: all; - border-top-right-radius: 4px; - border-top-left-radius: 4px; + border-top-right-radius: 3px; + border-top-left-radius: 3px; transition: all 0.2s ease-in; } #CoordinatesDiv #toggleTimeUI > i:before { @@ -156,7 +156,7 @@ #CoordinatesDiv #toggleTimeUI.active { transform: translateY(-10px); color: var(--color-mmgis); - border-top-right-radius: 4px; - border-top-left-radius: 4px; + border-top-right-radius: 3px; + border-top-left-radius: 3px; height: 42px; } diff --git a/src/essence/Ancillary/DataShaders.js b/src/essence/Ancillary/DataShaders.js index 07b7d6f6..7e097500 100644 --- a/src/essence/Ancillary/DataShaders.js +++ b/src/essence/Ancillary/DataShaders.js @@ -369,7 +369,7 @@ let DataShaders = { color === 'transparent' ? 'class="checkeredTransparent"' : '' - } style="width: ${100 / ramp.length}%; height: 22px; ${ + }style="width: ${100 / ramp.length}%; height: 22px; ${ color !== 'transparent' ? `background: ${color};` : '' @@ -380,7 +380,6 @@ let DataShaders = { `
${newRamp.join('\n')}
` ) }) - $(`#dataShader_${cname}_colorize_ramps`).html( Dropy.construct(ramps, null, 0) ) @@ -458,7 +457,7 @@ let DataShaders = { ramp.forEach((color, idx) => { // prettier-ignore legend.push( - `
  • `, + `
  • `, isDiscrete ? [ `
    `, idx === 0 ? `
    ` diff --git a/src/essence/Ancillary/Description.js b/src/essence/Ancillary/Description.js index d91f8350..16364044 100644 --- a/src/essence/Ancillary/Description.js +++ b/src/essence/Ancillary/Description.js @@ -57,21 +57,10 @@ var Description = { .style('margin', '0') Description.descPointInner.on('click', function () { - if ( - Map_.activeLayer.feature.geometry.coordinates[1] && - Map_.activeLayer.feature.geometry.coordinates[0] - ) - if ( - !isNaN(Map_.activeLayer.feature.geometry.coordinates[1]) && - !isNaN(Map_.activeLayer.feature.geometry.coordinates[0]) - ) - Map_.map.setView( - [ - Map_.activeLayer.feature.geometry.coordinates[1], - Map_.activeLayer.feature.geometry.coordinates[0], - ], - Map_.mapScaleZoom || Map_.map.getZoom() - ) + if (typeof Map_.activeLayer.getBounds === 'function') + Map_.map.fitBounds(Map_.activeLayer.getBounds()) + else if (Map_.activeLayer._latlng) + Map_.map.panTo(Map_.activeLayer._latlng) }) this.inited = true diff --git a/src/essence/Ancillary/Login/Login.css b/src/essence/Ancillary/Login/Login.css index faf74867..a7d30e18 100644 --- a/src/essence/Ancillary/Login/Login.css +++ b/src/essence/Ancillary/Login/Login.css @@ -136,6 +136,7 @@ line-height: 28px; margin-left: 5px; padding-left: 2px; + border-radius: 3px; pointer-events: all; transition: color 0.2s ease-in 0s; } diff --git a/src/essence/Ancillary/Modal.css b/src/essence/Ancillary/Modal.css index 458b7c21..d16e183f 100644 --- a/src/essence/Ancillary/Modal.css +++ b/src/essence/Ancillary/Modal.css @@ -16,8 +16,20 @@ right: 0; color: #ddd; cursor: pointer; - margin: 8px 6px; + padding: 16px; transition: color 0.2s ease; + background: var(--color-k); + border-bottom-left-radius: 100%; + box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 1); +} + +#mmgisModalClose i { + position: relative; + top: -5px; + right: -5px; +} +#mmgisModalClose:hover { + color: var(--color-c); } #mmgisModalInner { diff --git a/src/essence/Ancillary/Modal.js b/src/essence/Ancillary/Modal.js index 27a4f628..d5b0c71f 100644 --- a/src/essence/Ancillary/Modal.js +++ b/src/essence/Ancillary/Modal.js @@ -11,6 +11,7 @@ import './Modal.css' const Modal = { _onRemoveCallback: null, set: function (html, onAddCallback, onRemoveCallback) { + if ($('#mmgisModal')) $('#mmgisModal').remove() // prettier-ignore $('body').append([ "
    ", diff --git a/src/essence/Ancillary/Search.css b/src/essence/Ancillary/Search.css index 600b78bf..8bb7c689 100644 --- a/src/essence/Ancillary/Search.css +++ b/src/essence/Ancillary/Search.css @@ -9,6 +9,8 @@ border: none; border-right: 1px solid var(--color-b); cursor: pointer; + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; } #SearchClear { margin-left: 0px; diff --git a/src/essence/Ancillary/TimeControl.js b/src/essence/Ancillary/TimeControl.js index c994fb17..81b9a55b 100644 --- a/src/essence/Ancillary/TimeControl.js +++ b/src/essence/Ancillary/TimeControl.js @@ -71,6 +71,7 @@ var TimeControl = { if (L_.configData.time.visible == false) { TimeControl.toggleTimeUI(false) } + initLayerTimes() }, toggleTimeUI: function (isOn) { d3.select('#timeUI').style('visibility', function () { @@ -291,6 +292,25 @@ var TimeControl = { }, } +function initLayerTimes () { + for (let layerName in L_.layersNamed) { + const layer = L_.layersNamed[layerName] + if ( + layer.time && + layer.time.enabled == true + ) { + layer.time.start = TimeControl.startTime + layer.time.end = TimeControl.endTime + d3.select('.starttime.' + layer.name.replace(/\s/g, '')).text( + layer.time.start + ) + d3.select('.endtime.' + layer.name.replace(/\s/g, '')).text( + layer.time.end + ) + } + } +} + function updateTime() { // Continuously update global time clock and UI elements var now = new Date() diff --git a/src/essence/Basics/Formulae_/Formulae_.js b/src/essence/Basics/Formulae_/Formulae_.js index 5c58877a..5772fb24 100644 --- a/src/essence/Basics/Formulae_/Formulae_.js +++ b/src/essence/Basics/Formulae_/Formulae_.js @@ -23,20 +23,23 @@ var Formulae_ = { radiusOfEarth: 6371000, dam: false, //degrees as meters metersInOneDegree: null, - getBaseGeoJSON: function () { + getBaseGeoJSON: function (featuresArray) { return { type: 'FeatureCollection', crs: { type: 'name', properties: { name: 'urn:ogc:def:crs:OGC:1.3:CRS84' }, }, - features: [], + features: featuresArray || [], } }, getExtension: function (string) { - var ex = /(?:\.([^.]+))?$/.exec(string)[1] + const ex = /(?:\.([^.]+))?$/.exec(string)[1] return ex || '' }, + fileNameFromPath: function (path) { + return path.replace(/^.*[\\\/]/, '').replace(/\.[^/.]+$/, '') + }, pad: function (num, size) { let s = '000000000000000000000000000000' + num return s.substr(s.length - size) @@ -1378,6 +1381,7 @@ var Formulae_ = { return uniqueArray }, sanitize(str) { + if (str == null) return '' return str.replace(/[<>;{}]/g, '') }, doBoundingBoxesIntersect(a, b) { @@ -1740,8 +1744,81 @@ var Formulae_ = { }) return str }, + /** Returns an ellipse with major and minor axes and rotation about a point + // Adapted from turf.js' ellipse function + * @param lnglat {lng: lat:} + * @param axes {x: y:} + * @param crs {object} + * @param options {units: 'meters', steps: 32, angle: 0} + */ + toEllipse(lnglat, axes, crs, options) { + if (crs == null) return null + + let xAxis = axes.x || 0 + let yAxis = axes.y || 0 + // Optional params + options = options || {} + let steps = options.steps || 32 + let units = options.units || 'meters' + let angle = (options.angle || 0) * -1 + const angleRad = angle * (Math.PI / 180) + + if (units === 'kilometers') { + xAxis *= 1000 + yAxis *= 1000 + } + + const centerEN = crs.project(lnglat) + const centerCoordsEN = [centerEN.x, centerEN.y] + + const coordinates = [] + for (let i = 0; i < steps; i += 1) { + let stepAngle = (i * -360) / steps + let x = + (xAxis * yAxis) / + Math.sqrt( + Math.pow(yAxis, 2) + + Math.pow(xAxis, 2) * + Math.pow(Math.tan((stepAngle * Math.PI) / 180), 2) + ) + let y = + (xAxis * yAxis) / + Math.sqrt( + Math.pow(xAxis, 2) + + Math.pow(yAxis, 2) / + Math.pow(Math.tan((stepAngle * Math.PI) / 180), 2) + ) + + if (stepAngle < -90 && stepAngle >= -270) x = -x + if (stepAngle < -180 && stepAngle >= -360) y = -y + + const rot = Formulae_.rotatePoint( + { + x: x, + y: y, + }, + [0, 0], + angleRad + ) + x = rot.x + y = rot.y + + let lnglatCoord = crs.unproject({ + x: x + centerCoordsEN[0], + y: y + centerCoordsEN[1], + }) + coordinates.push([lnglatCoord.lng, lnglatCoord.lat]) + } + coordinates.push(coordinates[0]) + + return { + type: 'Feature', + properties: {}, + geometry: { type: 'Polygon', coordinates: [coordinates] }, + } + }, getCookieValue(a) { - var b = document.cookie.match('(^|[^;]+)\\s*' + a + '\\s*=\\s*([^;]+)') + let b = document.cookie.match('(^|[^;]+)\\s*' + a + '\\s*=\\s*([^;]+)') return b ? b.pop() : '' }, getBrowser() { diff --git a/src/essence/Basics/Globe_/Globe_.js b/src/essence/Basics/Globe_/Globe_.js index 6e7f2f3f..036c0d7f 100644 --- a/src/essence/Basics/Globe_/Globe_.js +++ b/src/essence/Basics/Globe_/Globe_.js @@ -55,6 +55,7 @@ let Globe_ = { tileMapResource: tmr, majorRadius: F_.radiusOfPlanetMajor, minorRadius: F_.radiusOfPlanetMinor, + radiusOfTiles: 5, //renderOnlyWhenOpen: false, //default true //wireframeMode: true, // default false //useLOD: true, // default true diff --git a/src/essence/Basics/Layers_/LayerCapturer.js b/src/essence/Basics/Layers_/LayerCapturer.js index 8c721e68..da0e6104 100644 --- a/src/essence/Basics/Layers_/LayerCapturer.js +++ b/src/essence/Basics/Layers_/LayerCapturer.js @@ -8,26 +8,22 @@ import TimeControl from '../../Ancillary/TimeControl' export const captureVector = (layerObj, options, cb) => { options = options || {} let layerUrl = layerObj.url + const layerData = L_.layersDataByName[layerObj.name] - if ( - options.evenIfOff !== true && - !layerObj.visibility && - !options.useEmptyGeoJSON - ) { - cb('off') + // If there is no url to a JSON file but the "controlled" option is checked in the layer config, + // create the geoJSON layer with empty GeoJSON data + if (options.useEmptyGeoJSON || layerData.controlled) { + cb(F_.getBaseGeoJSON()) return } - if ( - (typeof layerUrl !== 'string' || layerUrl.length === 0) && - !options.useEmptyGeoJSON - ) { - cb(null) + if (options.evenIfOff !== true && !layerObj.visibility) { + cb('off') return } - if (options.useEmptyGeoJSON) { - cb(F_.getBaseGeoJSON()) + if (typeof layerUrl !== 'string' || layerUrl.length === 0) { + cb(null) return } @@ -36,8 +32,15 @@ export const captureVector = (layerObj, options, cb) => { layerObj.time == null ? d3.utcFormat('%Y-%m-%dT%H:%M:%SZ') : d3.utcFormat(layerObj.time.format) - const startTime = layerTimeFormat(Date.parse(TimeControl.getStartTime())) - const endTime = layerTimeFormat(Date.parse(TimeControl.getEndTime())) + const startTime = + layerObj.time == null + ? layerTimeFormat(Date.parse(TimeControl.getStartTime())) + : layerObj.time.start + const endTime = + layerObj.time == null + ? layerTimeFormat(Date.parse(TimeControl.getEndTime())) + : layerObj.time.end + if (typeof layerObj.time != 'undefined') { layerUrl = layerObj.url .replace('{starttime}', startTime) @@ -180,30 +183,23 @@ export const captureVector = (layerObj, options, cb) => { } if (!done) { - // If there is no url to a JSON file but the "controlled" option is checked in the layer config, - // create the geoJSON layer with empty GeoJSON data - const layerData = L_.layersDataByName[layerObj.name] - if (L_.missionPath === layerUrl && layerData.controlled) { - cb(F_.getBaseGeoJSON()) - } else { - $.getJSON(layerUrl, function (data) { - if (data.hasOwnProperty('Features')) { - data.features = data.Features - delete data.Features - } - cb(data) - }).fail(function (jqXHR, textStatus, errorThrown) { - //Tell the console council about what happened - console.warn( - 'ERROR! ' + - textStatus + - ' in ' + - layerUrl + - ' /// ' + - errorThrown - ) - cb(null) - }) - } + $.getJSON(layerUrl, function (data) { + if (data.hasOwnProperty('Features')) { + data.features = data.Features + delete data.Features + } + cb(data) + }).fail(function (jqXHR, textStatus, errorThrown) { + //Tell the console council about what happened + console.warn( + 'ERROR! ' + + textStatus + + ' in ' + + layerUrl + + ' /// ' + + errorThrown + ) + cb(null) + }) } } diff --git a/src/essence/Basics/Layers_/LayerConstructors.js b/src/essence/Basics/Layers_/LayerConstructors.js index 233514ff..e5ba919f 100644 --- a/src/essence/Basics/Layers_/LayerConstructors.js +++ b/src/essence/Basics/Layers_/LayerConstructors.js @@ -4,7 +4,6 @@ import $ from 'jquery' import * as d3 from 'd3' -import { ellipse } from '@turf/turf' import F_ from '../Formulae_/Formulae_' import L_ from '../Layers_/Layers_' @@ -157,8 +156,8 @@ export const constructVectorLayer = ( break case 'directional_circle': svg = [ - `
    `, - ``, + `
    `, + ``, ``, + ``, ``, ``, ].join('\n') break case 'triangle-flipped': svg = [ - ``, + ``, ``, ``, ].join('\n') break case 'square': svg = [ - ``, + ``, ``, @@ -197,42 +196,42 @@ export const constructVectorLayer = ( break case 'diamond': svg = [ - ``, + ``, ``, ``, ].join('\n') break case 'pentagon': svg = [ - ``, + ``, ``, ``, ].join('\n') break case 'hexagon': svg = [ - ``, + ``, ``, ``, ].join('\n') break case 'star': svg = [ - ``, + ``, ``, ``, ].join('\n') break case 'plus': svg = [ - ``, + ``, ``, ``, ].join('\n') break case 'pin': svg = [ - ``, + ``, ``, ``, ].join('\n') @@ -330,6 +329,87 @@ export const constructSublayers = (geojson, layerObj) => { layerObj, 'variables.markerAttachments.uncertainty' ) + let uncertaintyStyle + let curtainUncertaintyOptions + let clampedUncertaintyOptions + if (uncertaintyVar) { + uncertaintyStyle = { + fillOpacity: uncertaintyVar.fillOpacity || 0.25, + fillColor: uncertaintyVar.color || 'white', + color: uncertaintyVar.strokeColor || 'black', + weight: uncertaintyVar.weight || 1, + opacity: uncertaintyVar.opacity || 0.8, + className: 'noPointerEventsImportant', + } + // For Globe Curtains + const uncertaintyEllipseFeatures = [] + const depth3d = uncertaintyVar.depth3d || 2 + geojson.features.forEach((f) => { + let uncertaintyAngle = parseFloat( + F_.getIn(f.properties, uncertaintyVar.angleProp, 0) + ) + if (uncertaintyVar.angleUnit === 'rad') + uncertaintyAngle = uncertaintyAngle * (180 / Math.PI) + + if (f.geometry.type === 'Point') { + const feature = F_.toEllipse( + { + lat: f.geometry.coordinates[1], + lng: f.geometry.coordinates[0], + }, + { + x: F_.getIn(f.properties, uncertaintyVar.xAxisProp, 1), + y: F_.getIn(f.properties, uncertaintyVar.yAxisProp, 1), + }, + window.mmgisglobal.customCRS, + { + units: uncertaintyVar.axisUnits || 'meters', + steps: 32, + angle: uncertaintyAngle, + } + ) + for ( + let i = 0; + i < feature.geometry.coordinates[0].length; + i++ + ) { + feature.geometry.coordinates[0][i][2] = + f.geometry.coordinates[2] + depth3d + } + uncertaintyEllipseFeatures.push(feature) + } + }) + + curtainUncertaintyOptions = { + name: `markerAttachmentUncertainty_${layerObj.name}Curtain`, + on: true, + opacity: uncertaintyVar.opacity3d || 0.5, + imageColor: + uncertaintyVar.color3d || uncertaintyVar.color || '#FFFF00', + depth: depth3d + 1, + geojson: { + type: 'FeatureCollection', + features: uncertaintyEllipseFeatures, + }, + } + clampedUncertaintyOptions = { + name: `markerAttachmentUncertainty_${layerObj.name}Clamped`, + on: true, + order: -9999, + opacity: 1, + minZoom: 0, + maxZoom: 100, + geojson: { + type: 'FeatureCollection', + features: uncertaintyEllipseFeatures, + }, + style: { + default: uncertaintyStyle, + }, + } + } + + // For Leaflet const leafletLayerObjectUncertaintyEllipse = { pointToLayer: (feature, latlong) => { // Marker Attachment Uncertainty @@ -339,33 +419,31 @@ export const constructSublayers = (geojson, layerObj) => { ) if (uncertaintyVar.angleUnit === 'rad') uncertaintyAngle = uncertaintyAngle * (180 / Math.PI) - uncertaintyEllipse = ellipse( - [latlong.lng, latlong.lat], - F_.getIn( - feature.properties, - uncertaintyVar.xAxisProp, - Math.random() + 0.5 - ), - F_.getIn( - feature.properties, - uncertaintyVar.yAxisProp, - Math.random() + 1 - ), + + uncertaintyEllipse = F_.toEllipse( + latlong, + { + x: F_.getIn( + feature.properties, + uncertaintyVar.xAxisProp, + 1 + ), + y: F_.getIn( + feature.properties, + uncertaintyVar.yAxisProp, + 1 + ), + }, + window.mmgisglobal.customCRS, { units: uncertaintyVar.axisUnits || 'meters', steps: 32, angle: uncertaintyAngle, } ) + uncertaintyEllipse = L.geoJSON(uncertaintyEllipse, { - style: { - fillOpacity: 0.25, - fillColor: uncertaintyVar.color || 'white', - color: 'black', - weight: 1, - opacity: 0.8, - className: 'noPointerEventsImportant', - }, + style: uncertaintyStyle, }) return uncertaintyEllipse }, @@ -426,12 +504,9 @@ export const constructSublayers = (geojson, layerObj) => { 'click' ), } - let wm = parseFloat(imageSettings.widthMeters) - let w = parseFloat(imageSettings.widthPixels) - let h = parseFloat(imageSettings.heightPixels) - let lngM = F_.metersToDegrees(wm) / 2 - let latM = lngM * (h / w) - let center = [latlong.lng, latlong.lat] + const wm = parseFloat(imageSettings.widthMeters) + const w = parseFloat(imageSettings.widthPixels) + const h = parseFloat(imageSettings.heightPixels) let angle = -F_.getIn( feature.properties, imageSettings.angleProp, @@ -440,45 +515,62 @@ export const constructSublayers = (geojson, layerObj) => { if (imageSettings.angleProp === 'deg') angle = angle * (Math.PI / 180) - var topLeft = F_.rotatePoint( - { - y: latlong.lat + latM, - x: latlong.lng - lngM, - }, - center, - angle + const crs = window.mmgisglobal.customCRS + const centerEN = crs.project(latlong) + const center = [centerEN.x, centerEN.y] + const xM = wm / 2 + const yM = (wm * (h / w)) / 2 + const topLeft = crs.unproject( + F_.rotatePoint( + { + y: centerEN.y + yM, + x: centerEN.x - xM, + }, + center, + angle + ) ) - var topRight = F_.rotatePoint( - { - y: latlong.lat + latM, - x: latlong.lng + lngM, - }, - center, - angle + + const topRight = crs.unproject( + F_.rotatePoint( + { + y: centerEN.y + yM, + x: centerEN.x + xM, + }, + center, + angle + ) ) - var bottomRight = F_.rotatePoint( - { - y: latlong.lat - latM, - x: latlong.lng + lngM, - }, - center, - angle + + const bottomRight = crs.unproject( + F_.rotatePoint( + { + y: centerEN.y - yM, + x: centerEN.x + xM, + }, + center, + angle + ) ) - var bottomLeft = F_.rotatePoint( - { - y: latlong.lat - latM, - x: latlong.lng - lngM, - }, - center, - angle + + const bottomLeft = crs.unproject( + F_.rotatePoint( + { + y: centerEN.y - yM, + x: centerEN.x - xM, + }, + center, + angle + ) ) - var anchors = [ - [topLeft.y, topLeft.x], - [topRight.y, topRight.x], - [bottomRight.y, bottomRight.x], - [bottomLeft.y, bottomLeft.x], + const anchors = [ + [topLeft.lat, topLeft.lng], + [topRight.lat, topRight.lng], + [bottomRight.lat, bottomRight.lng], + [bottomLeft.lat, bottomLeft.lng], ] + return L.layerGroup([ L.imageTransform(imageSettings.image, anchors, { opacity: 1, @@ -595,18 +687,25 @@ export const constructSublayers = (geojson, layerObj) => { } const sublayers = { - uncertainty_ellipses: uncertaintyVar - ? { - on: - uncertaintyVar.initialVisibility != null - ? uncertaintyVar.initialVisibility - : true, - layer: L.geoJson( - geojson, - leafletLayerObjectUncertaintyEllipse - ), - } - : false, + uncertainty_ellipses: + uncertaintyVar && curtainUncertaintyOptions + ? { + on: + uncertaintyVar.initialVisibility != null + ? uncertaintyVar.initialVisibility + : true, + type: 'uncertainty_ellipses', + curtainLayerId: curtainUncertaintyOptions.name, + curtainOptions: curtainUncertaintyOptions, + clampedLayerId: clampedUncertaintyOptions.name, + clampedOptions: clampedUncertaintyOptions, + geojson: geojson, + layer: L.geoJson( + geojson, + leafletLayerObjectUncertaintyEllipse + ), + } + : false, image_overlays: imageVar && imageShow === 'always' ? { diff --git a/src/essence/Basics/Layers_/Layers_.js b/src/essence/Basics/Layers_/Layers_.js index d7555c19..d7b950c3 100644 --- a/src/essence/Basics/Layers_/Layers_.js +++ b/src/essence/Basics/Layers_/Layers_.js @@ -78,6 +78,7 @@ var L_ = { searchFile: null, toolsLoaded: false, addedfiles: {}, //filename -> null (not null if added) + activeFeature: null, lastActivePoint: { layerName: null, lat: null, @@ -133,6 +134,7 @@ var L_ = { L_.searchStrings = null L_.searchFile = null L_.toolsLoaded = false + L_.activeFeature = null L_.lastActivePoint = { layerName: null, lat: null, @@ -193,17 +195,33 @@ var L_ = { L_.Map_.map.removeLayer(L_.layersGroup[s.name]) if (L_.layersGroupSublayers[s.name]) { for (let sub in L_.layersGroupSublayers[s.name]) { - if ( - L_.layersGroupSublayers[s.name][sub].type === - 'model' - ) { - L_.Globe_.litho.removeLayer( - L_.layersGroupSublayers[s.name][sub].layerId - ) - } else { - L_.Map_.rmNotNull( - L_.layersGroupSublayers[s.name][sub].layer - ) + switch (L_.layersGroupSublayers[s.name][sub].type) { + case 'model': + L_.Globe_.litho.removeLayer( + L_.layersGroupSublayers[s.name][sub] + .layerId + ) + break + case 'uncertainty_ellipses': + L_.Globe_.litho.removeLayer( + L_.layersGroupSublayers[s.name][sub] + .curtainLayerId + ) + L_.Globe_.litho.removeLayer( + L_.layersGroupSublayers[s.name][sub] + .clampedLayerId + ) + L_.Map_.rmNotNull( + L_.layersGroupSublayers[s.name][sub] + .layer + ) + break + default: + L_.Map_.rmNotNull( + L_.layersGroupSublayers[s.name][sub] + .layer + ) + break } } } @@ -216,27 +234,52 @@ var L_ = { if (L_.layersGroupSublayers[s.name]) { for (let sub in L_.layersGroupSublayers[s.name]) { if (L_.layersGroupSublayers[s.name][sub].on) { - if ( - L_.layersGroupSublayers[s.name][sub] - .type === 'model' + switch ( + L_.layersGroupSublayers[s.name][sub].type ) { - L_.Globe_.litho.addLayer( - 'model', - L_.layersGroupSublayers[s.name][sub] - .modelOptions - ) - } else { - L_.Map_.map.addLayer( - L_.layersGroupSublayers[s.name][sub] - .layer - ) - L_.layersGroupSublayers[s.name][ - sub - ].layer.setZIndex( - L_.layersOrdered.length + - 1 - - L_.layersOrdered.indexOf(s.name) - ) + case 'model': + L_.Globe_.litho.addLayer( + 'model', + L_.layersGroupSublayers[s.name][sub] + .modelOptions + ) + break + case 'uncertainty_ellipses': + L_.Globe_.litho.addLayer( + 'curtain', + L_.layersGroupSublayers[s.name][sub] + .curtainOptions + ) + L_.Globe_.litho.addLayer( + 'clamped', + L_.layersGroupSublayers[s.name][sub] + .clampedOptions + ) + L_.Map_.map.addLayer( + L_.layersGroupSublayers[s.name][sub] + .layer + ) + L_.layersGroupSublayers[s.name][ + sub + ].layer.setZIndex( + L_.layersOrdered.length + + 1 - + L_.layersOrdered.indexOf(s.name) + ) + break + default: + L_.Map_.map.addLayer( + L_.layersGroupSublayers[s.name][sub] + .layer + ) + L_.layersGroupSublayers[s.name][ + sub + ].layer.setZIndex( + L_.layersOrdered.length + + 1 - + L_.layersOrdered.indexOf(s.name) + ) + break } } } @@ -387,45 +430,78 @@ var L_ = { const sublayer = sublayers[sublayerName] if (sublayer) { if (sublayer.on === true) { - if (sublayer.type === 'model') { - L_.Globe_.litho.removeLayer(sublayer.layerId) - } else { - L_.Map_.rmNotNull(sublayer.layer) + switch (sublayer.type) { + case 'model': + L_.Globe_.litho.removeLayer(sublayer.layerId) + break + case 'uncertainty_ellipses': + L_.Globe_.litho.removeLayer(sublayer.curtainLayerId) + L_.Globe_.litho.removeLayer(sublayer.clampedLayerId) + L_.Map_.rmNotNull(sublayer.layer) + break + default: + L_.Map_.rmNotNull(sublayer.layer) + break } sublayer.on = false } else { - if (sublayer.type === 'model') { - L_.Globe_.litho.addLayer('model', sublayer.modelOptions) - } else { - L_.Map_.map.addLayer(sublayer.layer) - sublayer.layer.setZIndex( - L_.layersOrdered.length + - 1 - - L_.layersOrdered.indexOf(layerName) - ) + switch (sublayer.type) { + case 'model': + L_.Globe_.litho.addLayer('model', sublayer.modelOptions) + break + case 'uncertainty_ellipses': + L_.Globe_.litho.addLayer( + 'curtain', + sublayer.curtainOptions + ) + L_.Globe_.litho.addLayer( + 'clamped', + sublayer.clampedOptions + ) + L_.Map_.map.addLayer(sublayer.layer) + sublayer.layer.setZIndex( + L_.layersOrdered.length + + 1 - + L_.layersOrdered.indexOf(layerName) + ) + break + default: + L_.Map_.map.addLayer(sublayer.layer) + sublayer.layer.setZIndex( + L_.layersOrdered.length + + 1 - + L_.layersOrdered.indexOf(layerName) + ) + break } sublayer.on = true } } }, - disableAllBut: function (name, skipDisabling) { - if (L_.layersNamed.hasOwnProperty(name)) { - var l + disableAllBut: function (siteName, skipDisabling) { + if (L_.layersNamed.hasOwnProperty(siteName)) { + let l if (skipDisabling !== true) { - for (var i = 0; i < L_.layersData.length; i++) { + for (let i = 0; i < L_.layersData.length; i++) { l = L_.layersData[i] if (L_.toggledArray[l.name] == true) { - L_.toggleLayer(l) + if (l.name != 'Mars Overview') L_.toggleLayer(l) + } + if (L_.toggledArray['Mars Overview'] === false) { + if (l.name === 'Mars Overview') L_.toggleLayer(l) } } } - for (var i = 0; i < L_.layersData.length; i++) { - l = L_.layersData[i] - if (L_.toggledArray[l.name] == false) { - if (l.name == name) L_.toggleLayer(l) - } - if (L_.toggledArray['Mars Overview'] == false) { - if (l.name == 'Mars Overview') L_.toggleLayer(l) + + for (let n in L_.layersParent) { + if (L_.layersParent[n] === siteName && L_.layersDataByName[n]) { + l = L_.layersDataByName[n] + if ( + l.visibility === true && // initial visibility + L_.toggledArray[l.name] === false + ) { + L_.toggleLayer(l) + } } } } @@ -472,13 +548,27 @@ var L_ = { L_.layersData[i].name ][s] if (sublayer.on) { - if (sublayer.type === 'model') { - L_.Globe_.litho.addLayer( - 'model', - sublayer.modelOptions - ) - } else { - map.addLayer(sublayer.layer) + switch (sublayer.type) { + case 'model': + L_.Globe_.litho.addLayer( + 'model', + sublayer.modelOptions + ) + break + case 'uncertainty_ellipses': + L_.Globe_.litho.addLayer( + 'curtain', + sublayer.curtainOptions + ) + L_.Globe_.litho.addLayer( + 'clamped', + sublayer.clampedOptions + ) + map.addLayer(sublayer.layer) + break + default: + map.addLayer(sublayer.layer) + break } } } @@ -564,40 +654,42 @@ var L_ = { }, }) } else if (L_.layersData[i].type != 'header') { - L_.Globe_.litho.addLayer( - L_.layersData[i].type == 'vector' - ? 'clamped' - : L_.layersData[i].type, - { - name: s.name, - order: 1000 - L_.layersIndex[s.name], // Since higher order in litho is on top - on: L_.opacityArray[s.name] ? true : false, - geojson: L_.layersGroup[s.name].toGeoJSON(), - onClick: (feature, lnglat, layer) => { - this.selectFeature(layer.name, feature) - }, - useKeyAsHoverName: s.useKeyAsName, - style: { - // Prefer feature[f].properties.style values - letPropertiesStyleOverride: true, // default false - default: { - fillColor: s.style.fillColor, //Use only rgb and hex. No css color names - fillOpacity: parseFloat( - s.style.fillOpacity - ), - color: s.style.color, - weight: s.style.weight, - radius: s.radius, + if (typeof L_.layersGroup[s.name].toGeoJSON === 'function') + L_.Globe_.litho.addLayer( + L_.layersData[i].type == 'vector' + ? 'clamped' + : L_.layersData[i].type, + { + name: s.name, + order: 1000 - L_.layersIndex[s.name], // Since higher order in litho is on top + on: L_.opacityArray[s.name] ? true : false, + geojson: L_.layersGroup[s.name].toGeoJSON(), + onClick: (feature, lnglat, layer) => { + this.selectFeature(layer.name, feature) }, - bearing: s.variables?.markerAttachments?.bearing - ? s.variables.markerAttachments.bearing - : null, - }, - opacity: L_.opacityArray[s.name], - minZoom: 0, //s.minZoom, - maxZoom: 100, //s.maxNativeZoom, - } - ) + useKeyAsHoverName: s.useKeyAsName, + style: { + // Prefer feature[f].properties.style values + letPropertiesStyleOverride: true, // default false + default: { + fillColor: s.style.fillColor, //Use only rgb and hex. No css color names + fillOpacity: parseFloat( + s.style.fillOpacity + ), + color: s.style.color, + weight: s.style.weight, + radius: s.radius, + }, + bearing: s.variables?.markerAttachments + ?.bearing + ? s.variables.markerAttachments.bearing + : null, + }, + opacity: L_.opacityArray[s.name], + minZoom: 0, //s.minZoom, + maxZoom: 100, //s.maxNativeZoom, + } + ) } } } @@ -607,23 +699,40 @@ var L_ = { layer.setStyle(newStyle) } catch (err) {} }, - select(layer) { + setActiveFeature(layer) { + if (layer && layer.feature && layer.options?.layerName) + L_.activeFeature = { + feature: layer.feature, + layerName: layer.options.layerName, + layer: layer, + } + else L_.activeFeature = null + L_.setLastActivePoint(layer) L_.resetLayerFills() L_.highlight(layer) L_.Map_.activeLayer = layer Description.updatePoint(L_.Map_.activeLayer) - L_.Globe_.highlight( - L_.Globe_.findSpriteObject( - layer.options.layerName, - layer.feature.properties[layer.useKeyAsName] - ), - false - ) - L_.Viewer_.highlight(layer) + if (layer) { + L_.Globe_.highlight( + L_.Globe_.findSpriteObject( + layer.options.layerName, + layer.feature.properties[layer.useKeyAsName] + ), + false + ) + L_.Viewer_.highlight(layer) + } + + ToolController_.notifyActiveTool('setActiveFeature', L_.activeFeature) + + if (!L_.activeFeature) { + L_.clearVectorLayerInfo() + } }, highlight(layer) { + if (layer == null) return const color = (L_.configData.look && L_.configData.look.highlightcolor) || 'red' try { @@ -958,14 +1067,53 @@ var L_ = { return opacity }, setLayerFilter: function (name, filter, value) { - if (filter == 'clear') L_.layerFilters[name] = {} + // Clear + if (filter === 'clear') { + L_.layerFilters[name] = {} + if (L_.Globe_) { + L_.Globe_.litho.setLayerFilterEffect(name, 'brightness', 1) + L_.Globe_.litho.setLayerFilterEffect(name, 'contrast', 1) + L_.Globe_.litho.setLayerFilterEffect(name, 'saturation', 1) + L_.Globe_.litho.setLayerFilterEffect(name, 'blendCode', 0) + } + } + // Create a filters object for the layer if one doesn't exist L_.layerFilters[name] = L_.layerFilters[name] || {} - L_.layerFilters[name][filter] = value + + // Set the new filter (if it's not 'clear') + if (filter !== 'clear') L_.layerFilters[name][filter] = value + + // Mappings because litho names things differently + const lithoBlendMappings = ['none', 'overlay', 'color'] + const lithoFilterMappings = { + brightness: 'brightness', + contrast: 'contrast', + saturate: 'saturation', + } + if (typeof L_.layersGroup[name].updateFilter === 'function') { - var filterArray = [] - for (var f in L_.layerFilters[name]) { + let filterArray = [] + // Apply filter effects + for (let f in L_.layerFilters[name]) { filterArray.push(f + ':' + L_.layerFilters[name][f]) + // For Globe/litho + if (L_.Globe_) { + if (f === 'mix-blend-mode') { + L_.Globe_.litho.setLayerFilterEffect( + name, + 'blendCode', + lithoBlendMappings.indexOf(L_.layerFilters[name][f]) + ) + } else { + L_.Globe_.litho.setLayerFilterEffect( + name, + lithoFilterMappings[f], + parseFloat(L_.layerFilters[name][f]) + ) + } + } } + // For Map L_.layersGroup[name].updateFilter(filterArray) } }, @@ -1100,11 +1248,14 @@ var L_ = { * @param {object} layer - leaflet layer object */ setLastActivePoint: function (layer) { - var layerName = layer.hasOwnProperty('options') - ? layer.options.layerName - : null - var lat = layer.hasOwnProperty('_latlng') ? layer._latlng.lat : null - var lon = layer.hasOwnProperty('_latlng') ? layer._latlng.lng : null + let layerName, lat, lon + if (layer) { + layerName = layer.hasOwnProperty('options') + ? layer.options.layerName + : null + lat = layer.hasOwnProperty('_latlng') ? layer._latlng.lat : null + lon = layer.hasOwnProperty('_latlng') ? layer._latlng.lng : null + } if (layerName != null && lat != null && layerName != null) { L_.lastActivePoint = { @@ -1264,6 +1415,8 @@ var L_ = { try { L_.layersGroup[layerName].clearLayers() L_.clearVectorLayerInfo() + L_.syncSublayerData(layerName, 'clear') + L_.globeLithoLayerHelper(L_.layersNamed[layerName]) } catch (e) { console.log(e) console.warn('Warning: Unable to clear vector layer: ' + layerName) @@ -1280,24 +1433,51 @@ var L_ = { // Remove the layer updateLayer.removeLayer(removeLayer) }, - trimVectorLayerKeepBeforeTime: function (layerName, keepBeforeTime, timePropPath) { - L_.trimVectorLayerHelper(layerName, keepBeforeTime, timePropPath, "before") + trimVectorLayerKeepBeforeTime: function ( + layerName, + keepBeforeTime, + timePropPath + ) { + L_.trimVectorLayerHelper( + layerName, + keepBeforeTime, + timePropPath, + 'before' + ) }, - trimVectorLayerKeepAfterTime: function (layerName, keepAfterTime, timePropPath) { - L_.trimVectorLayerHelper(layerName, keepAfterTime, timePropPath, "after") + trimVectorLayerKeepAfterTime: function ( + layerName, + keepAfterTime, + timePropPath + ) { + L_.trimVectorLayerHelper( + layerName, + keepAfterTime, + timePropPath, + 'after' + ) }, - trimVectorLayerHelper: function (layerName, keepTime, timePropPath, trimType) { + trimVectorLayerHelper: function ( + layerName, + keepTime, + timePropPath, + trimType + ) { // Validate input parameters if (!keepTime) { console.warn( - 'Warning: The input for keep' + trimType.capitalizeFirstLetter() + 'Time is invalid: ' + keepTime + 'Warning: The input for keep' + + trimType.capitalizeFirstLetter() + + 'Time is invalid: ' + + keepTime ) return } if (!timePropPath) { console.warn( - 'Warning: The input for timePropPath is invalid: ' + timePropPath + 'Warning: The input for timePropPath is invalid: ' + + timePropPath ) return } @@ -1306,7 +1486,10 @@ var L_ = { const keepAfterAsDate = new Date(keepTime) if (isNaN(keepAfterAsDate.getTime())) { console.warn( - 'Warning: The input for keep' + trimType.capitalizeFirstLetter() + 'Time is invalid: ' + keepAfterTime + 'Warning: The input for keep' + + trimType.capitalizeFirstLetter() + + 'Time is invalid: ' + + keepTime ) return } @@ -1322,26 +1505,29 @@ var L_ = { for (let i = layers.length - 1; i >= 0; i--) { let layer = layers[i] if (layer.feature.properties[timePropPath]) { - const layerDate = new Date(layer.feature.properties[timePropPath]) + const layerDate = new Date( + layer.feature.properties[timePropPath] + ) if (isNaN(layerDate.getTime())) { console.warn( - 'Warning: The time for the layer is invalid: ' + layer.feature.properties[timePropPath] + 'Warning: The time for the layer is invalid: ' + + layer.feature.properties[timePropPath] ) continue } - if (trimType === "after") { + if (trimType === 'after') { if (layerDate < keepTimeAsDate) { - console.log("if true") L_.removeLayerHelper(updateLayer, layer) } - } else if (trimType === "before") { + } else if (trimType === 'before') { if (layerDate > keepTimeAsDate) { - console.log("if true") L_.removeLayerHelper(updateLayer, layer) } } } } + L_.syncSublayerData(layerName) + L_.globeLithoLayerHelper(L_.layersNamed[layerName]) } } else { console.warn( @@ -1351,10 +1537,10 @@ var L_ = { } }, keepFirstN: function (layerName, keepFirstN) { - L_.keepNHelper(layerName, keepFirstN, "first") + L_.keepNHelper(layerName, keepFirstN, 'first') }, keepLastN: function (layerName, keepLastN) { - L_.keepNHelper(layerName, keepLastN, "last") + L_.keepNHelper(layerName, keepLastN, 'last') }, keepNHelper: function (layerName, keepN, keepType) { // Validate input parameter @@ -1363,7 +1549,9 @@ var L_ = { console.warn( 'Warning: Unable to trim vector layer `' + layerName + - '` as keep' + keepType.capitalizeFirstLetter() + 'N == ' + + '` as keep' + + keepType.capitalizeFirstLetter() + + 'N == ' + keepN + ' and is not a valid integer' ) @@ -1376,19 +1564,21 @@ var L_ = { const updateLayer = L_.layersGroup[layerName] var layers = updateLayer.getLayers() - if (keepType === "last") { + if (keepType === 'last') { while (layers.length > keepN) { const removeLayer = layers[0] L_.removeLayerHelper(updateLayer, removeLayer) layers = updateLayer.getLayers() } - } else if (keepType === "first") { + } else if (keepType === 'first') { while (layers.length > keepN) { const removeLayer = layers[layers.length - 1] L_.removeLayerHelper(updateLayer, removeLayer) layers = updateLayer.getLayers() } } + L_.syncSublayerData(layerName) + L_.globeLithoLayerHelper(L_.layersNamed[layerName]) } } else { console.warn( @@ -1397,6 +1587,306 @@ var L_ = { ) } }, + trimLineString: function (layerName, time, timeProp, trimN, startOrEnd) { + // Validate input parameters + if (!time) { + console.warn( + 'Warning: Unable to trim the LineString in vector layer `' + + layerName + + '` as time === ' + time + ' and is invalid' + ) + return + } + + const timeAsDate = new Date(time) + if (isNaN(timeAsDate.getTime())) { + console.warn( + 'Warning: The input for time is not a valid date' + ) + return + } + + if (!timeProp) { + console.warn( + 'Warning: Unable to trim the LineString in vector layer `' + + layerName + + '` as timeProp === ' + timeProp + ' and is invalid' + ) + return + } + + const trimNum = parseInt(trimN) + if (Number.isNaN(Number(trimNum))) { + console.warn( + 'Warning: Unable to trim the LineString in vector layer `' + + layerName + + '` as trimN == ' + + trimN + + ' and is not a valid integer' + ) + return + } + + const TRIM_DIRECTION = [ 'start', 'end' ] + if (!TRIM_DIRECTION.includes(startOrEnd)) { + console.warn( + 'Warning: Unable to trim the LineString in vector layer `' + + layerName + + '` as startOrEnd == ' + + startOrEnd + + ' and is not a valid input value' + ) + return + } + + if (!time) { + console.warn( + 'Warning: Unable to trim the LineString in vector layer `' + + layerName + + '` as startOrEnd == ' + + startOrEnd + + ' and is not a valid input value' + ) + return + } + + if (layerName in L_.layersGroup) { + const updateLayer = L_.layersGroup[layerName] + + var layers = updateLayer.getLayers() + var layersGeoJSON = updateLayer.toGeoJSON() + var features = layersGeoJSON.features + + // All of the features have to be a LineString + const findNonLineString = features.filter(feature => { + return feature.geometry.type !== 'LineString' + }) + + if (findNonLineString.length > 0) { + console.warn( + 'Warning: Unable to trim the vector layer `' + + layerName + + '` as the features contain geometry that is not LineString' + ) + return + } + + if (features.length > 0) { + // Original layer time + var layerTime; + if (startOrEnd === 'start') { + layerTime = features[0].properties[timeProp] + } else { + layerTime = features[features.length - 1].properties[timeProp] + } + const layerTimeAsDate = new Date(layerTime) + + // Trim only if the new start time is after the layer start time + if (startOrEnd === 'start' && layerTimeAsDate < timeAsDate && trimNum > 0) { + var leftToTrim = trimNum; + var updatedFeatures = []; + // Walk forwards to find the new time + while (features.length > 0) { + const feature = features[0] + // If the feature is missing the key for the time + if (!feature.properties.hasOwnProperty(timeProp)) { + console.warn( + 'Warning: Unable to trim the vector layer `' + + layerName + + '` as the the feature\'s properties object is missing the `' + timeProp + '` key' + ) + return + } + + // If the number to trim is greater than the number of vertices in the current feature, + // trim the entire feature and move on to the next feature + if (leftToTrim >= feature.geometry.coordinates.length) { + leftToTrim -= feature.geometry.coordinates.length + features.shift() + continue + } + + // Trim + if (leftToTrim > 0) { + feature.geometry.coordinates = feature.geometry.coordinates.slice(leftToTrim) + leftToTrim -= trimNum + } + + if (leftToTrim <= 0) { + feature.properties[timeProp] = time + } + + updatedFeatures.push(feature) + features.shift() + } + layersGeoJSON.features = updatedFeatures + } + + // Trim only if the new end time is before the layer end time + if (startOrEnd === 'end' && layerTimeAsDate > timeAsDate && trimNum > 0) { + var leftToTrim = trimNum; + var updatedFeatures = []; + // Walk backwards to find the new time + while (features.length > 0) { + const feature = features[features.length - 1] + // If the feature is missing the key for the end time + if (!feature.properties.hasOwnProperty(timeProp)) { + console.warn( + 'Warning: Unable to trim the vector layer `' + + layerName + + '` as the the feature\'s properties object is missing the key `' + + timeProp + '` for the end time' + ) + return + } + + // If the number to trim is greater than the number of vertices in the current feature, + // trim the entire feature and move on to the next feature + if (leftToTrim >= feature.geometry.coordinates.length) { + leftToTrim -= feature.geometry.coordinates.length + features.pop() + continue + } + + // Trim + if (leftToTrim > 0) { + const length = feature.geometry.coordinates.length + feature.geometry.coordinates = feature.geometry.coordinates.slice(0, length - leftToTrim) + leftToTrim -= trimNum + } + + if (leftToTrim <= 0) { + feature.properties[timeProp] = time + } + + updatedFeatures.unshift(feature) + features.pop() + } + layersGeoJSON.features = updatedFeatures + } + + L_.clearVectorLayerInfo() + updateLayer.clearLayers() + updateLayer.addData(layersGeoJSON) + L_.syncSublayerData(layerName) + L_.globeLithoLayerHelper(L_.layersNamed[layerName]) + } else { + console.warn( + 'Warning: Unable to trim the vector layer `' + + layerName + + '` as the layer contains no features' + ) + return + } + } else { + console.warn( + 'Warning: Unable to trim vector layer as it does not exist: ' + + layerName + ) + } + }, + appendLineString: function (layerName, inputData, timeProp) { + // Validate input parameter + if (!inputData) { + console.warn( + 'Warning: Unable to append to vector layer `' + + layerName + + '` as inputData is invalid: ' + + JSON.stringify(inputData, null, 4) + ) + return + } + + // Make sure the timeProp exists as a property in the updated data + if (!(inputData.properties.hasOwnProperty(timeProp))) { + console.warn( + 'Warning: Unable to append to the vector layer `' + + layerName + + '` as timeProp === ' + timeProp + + ' and does not exist as a property in inputData: ' + + JSON.stringify(lastFeature, null, 4) + ) + return + } + + if (layerName in L_.layersGroup) { + const updateLayer = L_.layersGroup[layerName] + + var layers = updateLayer.getLayers() + var layersGeoJSON = updateLayer.toGeoJSON() + var features = layersGeoJSON.features + + if (features.length > 0) { + var lastFeature = features[features.length - 1] + // Make sure the last feature is a LineString + if (lastFeature.geometry.type !== 'LineString') { + console.warn( + 'Warning: Unable to append to the vector layer `' + + layerName + + '` as the feature is not a LineStringfeature: ' + + JSON.stringify(lastFeature, null, 4) + ) + return + } + + // Make sure the timeProp exists as a property in the feature + if (!(lastFeature.properties.hasOwnProperty(timeProp))) { + console.warn( + 'Warning: Unable to append to the vector layer `' + + layerName + + '` as timeProp === ' + timeProp + + ' and does not exist as a property in the feature: ' + + JSON.stringify(lastFeature, null, 4) + ) + return + } + + if (inputData.type === 'Feature') { + if (inputData.geometry.type !== 'LineString') { + console.warn( + 'Warning: Unable to append to vector layer `' + + layerName + + '` as inputData has the wrong geometry type (must be of type \'LineString\'): ' + + JSON.stringify(inputData, null, 4) + ) + return + } + + // Append new data to the end of the last feature + lastFeature.geometry.coordinates = lastFeature.geometry.coordinates.concat(inputData.geometry.coordinates) + + // Update the time + lastFeature.properties[timeProp] = inputData.properties[timeProp] + } else { + console.warn( + 'Warning: Unable to append to vector layer `' + + layerName + + '` as inputData has the wrong type (must be of type \'Feature\'): ' + + JSON.stringify(inputData, null, 4) + ) + return + } + + L_.clearVectorLayerInfo() + updateLayer.clearLayers() + updateLayer.addData(layersGeoJSON) + L_.syncSublayerData(layerName) + L_.globeLithoLayerHelper(L_.layersNamed[layerName]) + } else { + console.warn( + 'Warning: Unable to append to the vector layer `' + + layerName + + '` as the layer contains no features' + ) + return + } + } else { + console.warn( + 'Warning: Unable to append to vector layer as it does not exist: ' + + layerName + ) + } + }, updateVectorLayer: function (layerName, inputData) { if (layerName in L_.layersGroup) { const updateLayer = L_.layersGroup[layerName] @@ -1410,7 +1900,10 @@ var L_ = { 'Warning: Unable to update vector layer as the input data is invalid: ' + layerName ) + return } + L_.syncSublayerData(layerName) + L_.globeLithoLayerHelper(L_.layersNamed[layerName]) } else { console.warn( 'Warning: Unable to update vector layer as it does not exist: ' + @@ -1418,6 +1911,30 @@ var L_ = { ) } }, + // Make a layer's sublayer match the layers data again + syncSublayerData: function (layerName) { + try { + const geojson = L_.layersGroup[layerName].toGeoJSON() + // Now try the sublayers (if any) + const subUpdateLayers = L_.layersGroupSublayers[layerName] + if (subUpdateLayers) { + for (let sub in subUpdateLayers) { + if ( + subUpdateLayers[sub] !== false && + subUpdateLayers[sub].layer != null + ) { + subUpdateLayers[sub].layer.clearLayers() + subUpdateLayers[sub].layer.addData(geojson) + } + } + } + } catch (e) { + console.log(e) + console.warn( + 'Warning: Failed to update sublayers of layer: ' + layerName + ) + } + }, clearVectorLayerInfo: function () { // Clear the InfoTools data const infoTool = ToolController_.getTool('InfoTool') @@ -1428,6 +1945,20 @@ var L_ = { // Clear the description Description.clearDescription() }, + //Takes in a config layer object + globeLithoLayerHelper: async function(s) { + if (L_.Globe_) { + // Only toggle the layer to reset if the layer is toggled on, + // because if the layer is toggled off, it is not on the globe + if (L_.toggledArray[s.name]) { + // Temporarily set layer to "off" so the layer will be redrawn + L_.toggledArray[s.name] = false + + // Toggle the layer so its drawn in the globe + L_.toggleLayer(s) + } + } + }, } //Takes in a configData object and does a depth-first search through its diff --git a/src/essence/Basics/Map_/Map_.js b/src/essence/Basics/Map_/Map_.js index a12168f2..6e21bf20 100644 --- a/src/essence/Basics/Map_/Map_.js +++ b/src/essence/Basics/Map_/Map_.js @@ -117,47 +117,6 @@ let Map_ = { window.mmgisglobal.customCRS = crs } else { - /* - //Set up leaflet for planet radius only - var r = parseInt(L_.configData.msv.radius.major) - var rFactor = r / 6378137 - - var get_resolution = function() { - level = 30 - var res = [] - res[0] = (Math.PI * 2 * r) / 256 - for (var i = 1; i < level; i++) { - res[i] = (Math.PI * 2 * r) / 256 / Math.pow(2, i) - } - return res - } - var crs = new L.Proj.CRS( - 'EPSG:3857', - '+proj=merc +a=' + - r + - ' +b=' + - r + - ' +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +no_defs', - { - resolutions: get_resolution(), - origin: [ - (-Math.PI * 2 * r) / 2.0, - (Math.PI * 2 * r) / 2.0, - ], - bounds: L.bounds( - [ - -20037508.342789244 * rFactor, - 20037508.342789244 * rFactor, - ], - [ - 20037508.342789244 * rFactor, - -20037508.342789244 * rFactor, - ] - ), - } - ) - */ - //Make the empty map and turn off zoom controls this.map = L.map('map', { zoomControl: hasZoomControl, @@ -168,6 +127,12 @@ let Map_ = { //zoomSnap: 0, //wheelPxPerZoomLevel: 500, }) + + // Default CRS + window.mmgisglobal.customCRS = new L.Proj.CRS( + 'EPSG:3857', + `+proj=merc +lon_0=0 +k=1 +x_0=0 +y_0=0 +a=${F_.radiusOfPlanetMajor} +b=${F_.radiusOfPlanetMinor} +towgs84=0,0,0,0,0,0,0 +units=m +no_defs ` + ) } if (this.map.zoomControl) this.map.zoomControl.setPosition('topright') @@ -223,24 +188,7 @@ let Map_ = { //Add a graticule if (L_.configData.look && L_.configData.look.graticule == true) { - L.latlngGraticule({ - showLabel: true, - color: '#bbb', - weight: 1, - zoomInterval: [ - { start: 2, end: 3, interval: 40 }, - { start: 4, end: 5, interval: 20 }, - { start: 6, end: 7, interval: 10 }, - { start: 8, end: 9, interval: 5 }, - { start: 10, end: 11, interval: 0.4 }, - { start: 12, end: 13, interval: 0.2 }, - { start: 14, end: 15, interval: 0.1 }, - { start: 16, end: 17, interval: 0.01 }, - { start: 18, end: 19, interval: 0.005 }, - { start: 20, end: 21, interval: 0.0025 }, - { start: 21, end: 30, interval: 0.00125 }, - ], - }).addTo(Map_.map) + this.toggleGraticule(true) } //When done zooming, hide the things you're too far out to see/reveal the things you're close enough to see @@ -268,6 +216,31 @@ let Map_ = { //Set the time for any time enabled layers TimeControl.updateLayersTime() }, + toggleGraticule: function (on) { + if (on) + this.graticule = L.latlngGraticule({ + showLabel: true, + color: '#bbb', + weight: 1, + zoomInterval: [ + { start: 2, end: 3, interval: 40 }, + { start: 4, end: 5, interval: 20 }, + { start: 6, end: 7, interval: 10 }, + { start: 8, end: 9, interval: 5 }, + { start: 10, end: 11, interval: 0.4 }, + { start: 12, end: 13, interval: 0.2 }, + { start: 14, end: 15, interval: 0.1 }, + { start: 16, end: 17, interval: 0.01 }, + { start: 18, end: 19, interval: 0.005 }, + { start: 20, end: 21, interval: 0.0025 }, + { start: 21, end: 30, interval: 0.00125 }, + ], + }).addTo(Map_.map) + else { + this.rmNotNull(this.graticule) + this.graticule = null + } + }, clear: function () { this.map.eachLayer(function (layer) { Map_.map.removeLayer(layer) @@ -401,7 +374,7 @@ let Map_ = { } } Map_.allLayersLoadedPassed = false - makeLayer(layerObj) + makeLayer(layerObj, true) allLayersLoaded() }, setPlayerArrow(lng, lat, rot) { @@ -1268,8 +1241,7 @@ function buildToolBar() { function clearOnMapClick(event) { // Skip if there is no actively selected feature - const infoTool = ToolController_.getTool('InfoTool') - if (!infoTool.currentLayer) { + if (!Map_.activeLayer) { return } @@ -1280,7 +1252,8 @@ function clearOnMapClick(event) { let found = false // For all MMGIS layers for (let key in L_.layersGroup) { - if (L_.layersGroup[key] === false) continue + if (L_.layersGroup[key] === false || L_.layersGroup[key] == null) + continue let layers // Layers can be a LayerGroup or an array of LayerGroup @@ -1336,9 +1309,7 @@ function clearOnMapClick(event) { // If no feature was selected by this click event, clear the currently selected item if (!found) { - Map_.activeLayer = null - L_.resetLayerFills() - L_.clearVectorLayerInfo() + L_.setActiveFeature(null) } } } diff --git a/src/essence/Basics/ToolController_/ToolController_.js b/src/essence/Basics/ToolController_/ToolController_.js index 4313baf4..d2f24f20 100644 --- a/src/essence/Basics/ToolController_/ToolController_.js +++ b/src/essence/Basics/ToolController_/ToolController_.js @@ -188,6 +188,12 @@ let ToolController_ = { newWidth = newWidth || 'full' this.UserInterface.setToolWidth(newWidth) }, + notifyActiveTool: function (type, payload) { + if (this.activeTool != null) { + if (typeof this.activeTool.notify === 'function') + this.activeTool.notify(type, payload) + } + }, closeActiveTool: function () { var prevActive = $('#toolcontroller_incdiv .active') prevActive.removeClass('active').css({ diff --git a/src/essence/Basics/UserInterface_/BottomBar.css b/src/essence/Basics/UserInterface_/BottomBar.css new file mode 100644 index 00000000..2918bbf7 --- /dev/null +++ b/src/essence/Basics/UserInterface_/BottomBar.css @@ -0,0 +1,68 @@ +#mainSettingsModal { + background: var(--color-k); + width: 400px; + margin: 40px; + border-radius: 3px; +} +#mainSettingsModalTitle { + padding: 0px 0px 0px 10px; + height: 40px; + line-height: 39px; + font-size: 18px; + background: var(--color-i); + border-top-left-radius: 3px; + border-top-right-radius: 3px; + display: flex; + justify-content: space-between; +} +#mainSettingsModalTitle > div:first-child { + display: flex; +} +#mainSettingsModalTitle > div:first-child > div { + margin-left: 6px; + line-height: 40px; +} +#mainSettingsModalClose { + width: 40px; + height: 40px; + text-align: center; +} +#mainSettingsModalContent { + padding: 10px 20px; +} +.mainSettingsModalSection { + padding: 4px 0px 4px 0px; +} +.mainSettingsModalSectionTitle { +} +.mainSettingsModalSectionOptions { + list-style-type: none; + margin: 8px 0px; + padding: 0; +} +.mainSettingsModalSectionOptions li { + display: flex; + height: 30px; + line-height: 30px; + padding-left: 10px; +} +.mainSettingsModalSectionOptions li > div:last-child { + padding-left: 3px; +} +.mainSettingsModalHR { + width: 100%; + height: 1px; + background: var(--color-b); + margin: 4px 0px; +} + +#mainSettingsModal .slider2 { + margin-top: 6px; + margin-bottom: 6px; +} + +#mainSettingsModal .infoIcon { + padding: 0px 4px; + color: #999; + cursor: help; +} diff --git a/src/essence/Basics/UserInterface_/BottomBar.js b/src/essence/Basics/UserInterface_/BottomBar.js new file mode 100644 index 00000000..0d4d05a5 --- /dev/null +++ b/src/essence/Basics/UserInterface_/BottomBar.js @@ -0,0 +1,443 @@ +import $ from 'jquery' +import * as d3 from 'd3' + +import F_ from '../Formulae_/Formulae_' +import L_ from '../Layers_/Layers_' + +import QueryURL from '../../Ancillary/QueryURL' +import Modal from '../../Ancillary/Modal' +import HTML2Canvas from '../../../external/HTML2Canvas/html2canvas.min' + +import './BottomBar.css' + +let BottomBar = { + UI_: null, + settings: {}, + init: function (containerId, UI) { + this.UI_ = UI + const bottomBar = d3.select(`#${containerId}`) + + // Copy Link + bottomBar + .append('i') + .attr('id', 'topBarLink') + .attr('title', 'Copy Link') + .attr('tabindex', 100) + .attr('class', 'mmgisHoverBlue mdi mdi-open-in-new mdi-18px') + .style('padding', '5px 10px') + .style('width', '40px') + .style('height', '36px') + .style('line-height', '26px') + .style('cursor', 'pointer') + .on('click', function () { + const linkButton = $(this) + QueryURL.writeCoordinateURL(true, function () { + F_.copyToClipboard(L_.url) + + linkButton.removeClass('mdi-open-in-new') + linkButton.addClass('mdi-check-bold') + linkButton.css('color', 'var(--color-green)') + setTimeout(() => { + linkButton.removeClass('mdi-check-bold') + linkButton.css('color', '') + linkButton.addClass('mdi-open-in-new') + }, 3000) + }) + }) + + // Screenshot + bottomBar + .append('i') + .attr('id', 'topBarScreenshot') + .attr('title', 'Screenshot') + .attr('tabindex', 101) + .attr('class', 'mmgisHoverBlue mdi mdi-camera mdi-18px') + .style('padding', '5px 10px') + .style('width', '40px') + .style('height', '36px') + .style('line-height', '26px') + .style('cursor', 'pointer') + .style('opacity', '0.8') + .on('click', function () { + //We need to manually order leaflet z-indices for this to work + let zIndices = [] + $('#mapScreen #map .leaflet-tile-pane') + .children() + .each(function (i, elm) { + zIndices.push($(elm).css('z-index')) + $(elm).css('z-index', i + 1) + }) + $('.leaflet-control-scalefactor').css('display', 'none') + $('.leaflet-control-zoom').css('display', 'none') + $('#topBarScreenshotLoading').css('display', 'block') + HTML2Canvas(document.getElementById('mapScreen'), { + allowTaint: true, + useCORS: true, + }).then(function (canvas) { + canvas.id = 'mmgisScreenshot' + document.body.appendChild(canvas) + F_.downloadCanvas( + canvas.id, + 'mmgis-screenshot', + function () { + canvas.remove() + setTimeout(function () { + $('#topBarScreenshotLoading').css( + 'display', + 'none' + ) + }, 2000) + } + ) + }) + $('#mapScreen #map .leaflet-tile-pane') + .children() + .each(function (i, elm) { + $(elm).css('z-index', zIndices[i]) + }) + $('.leaflet-control-scalefactor').css('display', 'flex') + $('.leaflet-control-zoom').css('display', 'block') + }) + // Screenshot loading + d3.select('#topBarScreenshot') + .append('i') + .attr('id', 'topBarScreenshotLoading') + .attr('title', 'Taking Screenshot') + .attr('tabindex', 102) + .style('display', 'none') + .style('border-radius', '50%') + .style('border', '8px solid #ffe100') + .style('border-right-color', 'transparent') + .style('border-left-color', 'transparent') + .style('position', 'relative') + .style('top', '3px') + .style('left', '-17px') + .style('width', '20px') + .style('height', '20px') + .style('line-height', '26px') + .style('color', '#d2b800') + .style('cursor', 'pointer') + .style('animation-name', 'rotate-forever') + .style('animation-duration', '2s') + .style('animation-iteration-count', 'infinite') + .style('animation-timing', 'linear') + + // Fullscreen + bottomBar + .append('i') + .attr('id', 'topBarFullscreen') + .attr('title', 'Fullscreen') + .attr('tabindex', 103) + .attr('class', 'mmgisHoverBlue mdi mdi-fullscreen mdi-18px') + .style('padding', '5px 10px') + .style('width', '40px') + .style('height', '36px') + .style('line-height', '26px') + .style('cursor', 'pointer') + .on('click', function () { + BottomBar.fullscreen() + if ( + d3.select(this).attr('class') == + 'mmgisHoverBlue mdi mdi-fullscreen mdi-18px' + ) + d3.select(this) + .attr( + 'class', + 'mmgisHoverBlue mdi mdi-fullscreen-exit mdi-18px' + ) + .attr('title', 'Exit Fullscreen') + else + d3.select(this) + .attr( + 'class', + 'mmgisHoverBlue mdi mdi-fullscreen mdi-18px' + ) + .attr('title', 'Fullscreen') + }) + + // Settings + bottomBar + .append('i') + .attr('id', 'bottomBarSettings') + .attr('title', 'Settings') + .attr('tabindex', 104) + .attr('class', 'mmgisHoverBlue mdi mdi-settings mdi-18px') + .style('padding', '5px 10px') + .style('width', '40px') + .style('height', '36px') + .style('line-height', '26px') + .style('cursor', 'pointer') + .on('click', function () { + const that = $('#bottomBarSettings') + const wasOn = that.hasClass('active') + BottomBar.toggleSettings(!wasOn) + }) + + // Help + bottomBar + .append('i') + .attr('id', 'topBarHelp') + .attr('title', 'Help') + .attr('tabindex', 105) + .attr('class', 'mmgisHoverBlue mdi mdi-help mdi-18px') + .style('padding', '5px 10px') + .style('width', '40px') + .style('height', '36px') + .style('line-height', '26px') + .style('cursor', 'pointer') + .on('click', function () { + this.helpOn = !this.helpOn + if (this.helpOn) { + //d3.select('#viewer_Help').style('display', 'inherit') + } else { + d3.select('#viewer_Help').style('display', 'none') + } + }) + }, + toggleSettings: function (on) { + if (on) { + BottomBar.settings.visibility = BottomBar.settings.visibility || { + topbar: true, + toolbars: true, + scalebar: true, + coordinates: true, + graticule: this.UI_.Map_.graticule != null, + miscellaneous: true, + } + // prettier-ignore + const modalContent = [ + `
    `, + `
    `, + `
    Settings
    `, + `
    `, + `
    `, + `
    `, + `
    `, + `
    User Interface Visibility
    `, + `
      `, + `
    • `, + `
      `, + `
      Top Bar
      `, + `
    • `, + /* For now because then we need a way to open the settings modal again + `
    • `, + `
      `, + `
      Toolbars
      `, + `
    • `, + */ + `
    • `, + `
      `, + `
      Scale Bar
      `, + `
    • `, + `
    • `, + `
      `, + `
      Coordinates
      `, + `
    • `, + `
    • `, + `
      `, + `
      Graticule
      `, + `
    • `, + `
    • `, + `
      `, + `
      Miscellaneous
      `, + `
    • `, + `
    `, + `
    `, + (L_.Globe_ ? + [`
    `, + `
    3D Globe
    `, + `
      `, + `
    • `, + `
      Radius of Tiles
      `, + `
      `, + `
      ${L_.Globe_.litho.options.radiusOfTiles}
      `, + ``, + `
      `, + `
    • `, + `
    `, + `
    `].join('') : ''), + `
    `, + `
    ` + ].join('\n') + + Modal.set( + modalContent, + function () { + $('#mainSettingsModalClose').on('click', function () { + Modal.remove() + }) + // UI Visibility + $( + `#mainSettingsModalSectionUIVisibility .mmgis-checkbox > input` + ).on('click', function () { + const checked = $(this).prop('checked') + const value = $(this).attr('value') + + BottomBar.settings.visibility[value] = checked + + if (!checked) { + // now on + switch (value) { + case 'topbar': + $('#topBar').css('display', 'none') + break + case 'toolbars': + $('#mmgislogo').css('display', 'none') + $('#barBottom').css('display', 'none') + $('#toolbar').css({ + display: 'none', + width: '0px', + }) + $('#viewerToolBar').css('display', 'none') + $('#_lithosphere_controls').css( + 'display', + 'none' + ) + $('#splitscreens').css({ + left: '0px', + width: '100%', + }) + $('#mapScreen').css( + 'width', + $('#mapScreen').width() + 40 + 'px' + ) + BottomBar.UI_.topSize = 0 + window.dispatchEvent(new Event('resize')) + break + case 'scalebar': + $('#scaleBarBounds').css('display', 'none') + break + case 'coordinates': + $('#CoordinatesDiv').css('display', 'none') + break + case 'graticule': + BottomBar.UI_.Map_.toggleGraticule(false) + break + case 'miscellaneous': + $('.leaflet-control-container').css( + 'display', + 'none' + ) + $('.splitterVInner').css('display', 'none') + break + default: + break + } + } else { + // now off + switch (value) { + case 'topbar': + $('#topBar').css('display', 'flex') + break + case 'toolbars': + $('#mmgislogo').css('display', 'inherit') + $('#barBottom').css('display', 'flex') + $('#toolbar').css({ + display: 'inherit', + width: '40px', + }) + $('#viewerToolBar').css( + 'display', + 'inherit' + ) + $('#_lithosphere_controls').css( + 'display', + 'inherit' + ) + $('#splitscreens').css({ + left: '40px', + width: 'calc(100% - 40px)', + }) + $('#mapScreen').css( + 'width', + $('#mapScreen').width() - 40 + 'px' + ) + BottomBar.UI_.topSize = 40 + window.dispatchEvent(new Event('resize')) + break + case 'scalebar': + $('#scaleBarBounds').css( + 'display', + 'inherit' + ) + break + case 'coordinates': + $('#CoordinatesDiv').css('display', 'flex') + break + case 'graticule': + BottomBar.UI_.Map_.toggleGraticule(true) + break + case 'miscellaneous': + $('.leaflet-control-container').css( + 'display', + 'block' + ) + $('.splitterVInner').css( + 'display', + 'inline-flex' + ) + break + default: + break + } + } + }) + + // 3d Globe + // Radius of Tiles + $( + '#mainSettingsModalSection3DGlobe #globeSetRadiusOfTiles' + ).on('input', function () { + if (L_.Globe_) { + L_.Globe_.litho.options.radiusOfTiles = parseInt( + $(this).val() + ) + $( + '#mainSettingsModalSection3DGlobe #globeRadiusOfTilesValue' + ).text(L_.Globe_.litho.options.radiusOfTiles) + } + }) + }, + function () {} + ) + } else { + } + }, + toggleHelp: function () {}, + fullscreen: function () { + var isInFullScreen = + (document.fullscreenElement && + document.fullscreenElement !== null) || + (document.webkitFullscreenElement && + document.webkitFullscreenElement !== null) || + (document.mozFullScreenElement && + document.mozFullScreenElement !== null) || + (document.msFullscreenElement && + document.msFullscreenElement !== null) + + var docElm = document.documentElement + if (!isInFullScreen) { + if (docElm.requestFullscreen) { + docElm.requestFullscreen() + } else if (docElm.mozRequestFullScreen) { + docElm.mozRequestFullScreen() + } else if (docElm.webkitRequestFullScreen) { + docElm.webkitRequestFullScreen() + } else if (docElm.msRequestFullscreen) { + docElm.msRequestFullscreen() + } + } else { + if (document.exitFullscreen) { + document.exitFullscreen() + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen() + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen() + } else if (document.msExitFullscreen) { + document.msExitFullscreen() + } + } + }, +} + +export default BottomBar diff --git a/src/essence/Basics/UserInterface_/UserInterface_.css b/src/essence/Basics/UserInterface_/UserInterface_.css index 55a1c938..97a79330 100644 --- a/src/essence/Basics/UserInterface_/UserInterface_.css +++ b/src/essence/Basics/UserInterface_/UserInterface_.css @@ -15,7 +15,6 @@ padding-left: 40px; width: 100%; font-family: sans-serif; - background: rgb(0, 0, 0, 0.15); pointer-events: none; z-index: 2005; transition: all 0.2s cubic-bezier(0.445, 0.05, 0.55, 0.95); @@ -50,6 +49,7 @@ z-index: 1; background: var(--color-a); color: white; + border-radius: 3px; margin: 0px 5px 0px 5px; transition: color 0.2s cubic-bezier(0.445, 0.05, 0.55, 0.95); } @@ -87,6 +87,7 @@ margin-right: 5px; z-index: 20; opacity: 0; + border-radius: 3px; pointer-events: auto; transition: opacity 0.2s ease-out; } @@ -99,6 +100,7 @@ padding-left: 8px; background: var(--color-a); opacity: 0; + border-radius: 3px; pointer-events: auto; transition: opacity 0.2s ease-out; } diff --git a/src/essence/Basics/UserInterface_/UserInterface_.js b/src/essence/Basics/UserInterface_/UserInterface_.js index b56c3766..a9833b4e 100644 --- a/src/essence/Basics/UserInterface_/UserInterface_.js +++ b/src/essence/Basics/UserInterface_/UserInterface_.js @@ -4,8 +4,8 @@ import F_ from '../Formulae_/Formulae_' import L_ from '../Layers_/Layers_' import ToolController_ from '../ToolController_/ToolController_' import Login from '../../Ancillary/Login/Login' -import QueryURL from '../../Ancillary/QueryURL' -import HTML2Canvas from '../../../external/HTML2Canvas/html2canvas.min' + +import BottomBar from './BottomBar' import './UserInterface_.css' @@ -108,213 +108,7 @@ var UserInterface = { .style('flex-flow', 'column') .style('z-index', '1005') - this.barBottom - .append('i') - .attr('id', 'topBarLink') - .attr('title', 'Copy Link') - .attr('tabindex', 100) - .attr('class', 'mmgisHoverBlue mdi mdi-open-in-new mdi-18px') - .style('padding', '5px 10px') - .style('width', '40px') - .style('height', '36px') - .style('line-height', '26px') - .style('cursor', 'pointer') - .on('click', function () { - const linkButton = $(this) - QueryURL.writeCoordinateURL(true, function () { - F_.copyToClipboard(L_.url) - - linkButton.removeClass('mdi-open-in-new') - linkButton.addClass('mdi-check-bold') - linkButton.css('color', 'var(--color-green)') - setTimeout(() => { - linkButton.removeClass('mdi-check-bold') - linkButton.css('color', '') - linkButton.addClass('mdi-open-in-new') - }, 3000) - }) - }) - - this.barBottom - .append('i') - .attr('id', 'topBarScreenshot') - .attr('title', 'Screenshot') - .attr('tabindex', 101) - .attr('class', 'mmgisHoverBlue mdi mdi-camera mdi-18px') - .style('padding', '5px 10px') - .style('width', '40px') - .style('height', '36px') - .style('line-height', '26px') - .style('cursor', 'pointer') - .style('opacity', '0.8') - .on('click', function () { - //We need to manually order leaflet z-indices for this to work - let zIndices = [] - $('#mapScreen #map .leaflet-tile-pane') - .children() - .each(function (i, elm) { - zIndices.push($(elm).css('z-index')) - $(elm).css('z-index', i + 1) - }) - $('.leaflet-control-scalefactor').css('display', 'none') - $('.leaflet-control-zoom').css('display', 'none') - $('#topBarScreenshotLoading').css('display', 'block') - $('#mapToolBar').css('background', 'rgba(0,0,0,0)') - HTML2Canvas(document.getElementById('mapScreen'), { - allowTaint: true, - useCORS: true, - }).then(function (canvas) { - canvas.id = 'mmgisScreenshot' - document.body.appendChild(canvas) - F_.downloadCanvas( - canvas.id, - 'mmgis-screenshot', - function () { - canvas.remove() - setTimeout(function () { - $('#topBarScreenshotLoading').css( - 'display', - 'none' - ) - }, 2000) - } - ) - }) - $('#mapScreen #map .leaflet-tile-pane') - .children() - .each(function (i, elm) { - $(elm).css('z-index', zIndices[i]) - }) - $('.leaflet-control-scalefactor').css('display', 'flex') - $('.leaflet-control-zoom').css('display', 'block') - $('#mapToolBar').css('background', 'rgba(0,0,0,0.15)') - }) - //Screenshot loading - d3.select('#topBarScreenshot') - .append('i') - .attr('id', 'topBarScreenshotLoading') - .attr('title', 'Taking Screenshot') - .attr('tabindex', 102) - .style('display', 'none') - .style('border-radius', '50%') - .style('border', '8px solid #ffe100') - .style('border-right-color', 'transparent') - .style('border-left-color', 'transparent') - .style('position', 'relative') - .style('top', '3px') - .style('left', '-17px') - .style('width', '20px') - .style('height', '20px') - .style('line-height', '26px') - .style('color', '#d2b800') - .style('cursor', 'pointer') - .style('animation-name', 'rotate-forever') - .style('animation-duration', '2s') - .style('animation-iteration-count', 'infinite') - .style('animation-timing', 'linear') - - this.barBottom - .append('i') - .attr('id', 'topBarFullscreen') - .attr('title', 'Fullscreen') - .attr('tabindex', 103) - .attr('class', 'mmgisHoverBlue mdi mdi-fullscreen mdi-18px') - .style('padding', '5px 10px') - .style('width', '40px') - .style('height', '36px') - .style('line-height', '26px') - .style('cursor', 'pointer') - .on('click', function () { - fullscreen() - if ( - d3.select(this).attr('class') == - 'mmgisHoverBlue mdi mdi-fullscreen mdi-18px' - ) - d3.select(this) - .attr( - 'class', - 'mmgisHoverBlue mdi mdi-fullscreen-exit mdi-18px' - ) - .attr('title', 'Exit Fullscreen') - else - d3.select(this) - .attr( - 'class', - 'mmgisHoverBlue mdi mdi-fullscreen mdi-18px' - ) - .attr('title', 'Fullscreen') - }) - - this.barBottom - .append('i') - .attr('id', 'toggleUI') - .attr('title', 'Hide UI') - .attr('tabindex', 104) - .attr('class', 'mmgisHoverBlue mdi mdi-power mdi-18px') - .style('padding', '5px 10px') - .style('width', '40px') - .style('height', '36px') - .style('line-height', '26px') - .style('display', 'none') //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!====== - .on('click', function () { - if (d3.select(this).style('color') == 'rgb(0, 210, 0)') { - d3.select('#topBarRight #loginButton').style( - 'display', - 'none' - ) - d3.select('#topBarRight #loginUsername').style( - 'display', - 'none' - ) - d3.select('#toolbar').style('display', 'none') - d3.select('.mouseLngLat').style('display', 'none') - //d3.select( '.mainDescription' ).style( 'display', 'none' ); - d3.select('#viewerToolBar').style('display', 'none') - d3.select('#mapToolBar').style('display', 'none') - d3.select('#globeToolBar').style('display', 'none') - d3.select(this) - .style('color', 'white') - .attr('title', 'Show UI') - } else { - d3.select('#topBarRight #loginButton').style( - 'display', - 'inherit' - ) - d3.select('#topBarRight #loginUsername').style( - 'display', - 'inherit' - ) - d3.select('#toolbar').style('display', 'inherit') - d3.select('.mouseLngLat').style('display', 'inherit') - //d3.select( '.mainDescription' ).style( 'display', 'inherit' ); - d3.select('#viewerToolBar').style('display', 'inherit') - d3.select('#mapToolBar').style('display', 'inherit') - d3.select('#globeToolBar').style('display', 'inherit') - d3.select(this) - .style('color', 'rgb(0, 210, 0)') - .attr('title', 'Hide UI') - } - }) - - this.barBottom - .append('i') - .attr('id', 'topBarHelp') - .attr('title', 'Help') - .attr('tabindex', 105) - .attr('class', 'mmgisHoverBlue mdi mdi-help mdi-18px') - .style('padding', '5px 10px') - .style('width', '40px') - .style('height', '36px') - .style('line-height', '26px') - .style('cursor', 'pointer') - .on('click', function () { - this.helpOn = !this.helpOn - if (this.helpOn) { - //d3.select('#viewer_Help').style('display', 'inherit') - } else { - d3.select('#viewer_Help').style('display', 'none') - } - }) + BottomBar.init('barBottom', this) this.toolPanel = d3 .select('#main-container') @@ -425,7 +219,6 @@ var UserInterface = { .style('height', '40px') .style('pointer-events', 'none') .style('overflow', 'hidden') - .style('background', 'rgba(0,0,0,0.15)') .style('z-index', '1003') .style('transition', 'bottom 0.2s ease-out') @@ -747,7 +540,7 @@ var UserInterface = { .style('image-rendering', 'pixelated') .html( ` - + ` ) .on('click', F_.toHostForceLanding) @@ -766,6 +559,9 @@ var UserInterface = { shouldRotateSplitterText() }, + resize: function () { + windowresize() + }, hide: function () { d3.select('#main-container').style('opacity', '0') }, @@ -1086,6 +882,7 @@ var UserInterface = { ToolController_.fina(this) Viewer_ = viewer_ Map_ = map_ + this.Map_ = map_ Globe_ = globe_ this.hasViewer = l_.hasViewer this.hasGlobe = l_.hasGlobe @@ -1673,40 +1470,6 @@ function clearUnwantedPanels(hasViewer, hasMap, hasGlobe) { Map_.map.invalidateSize() } -function toggleHelp() {} - -function fullscreen() { - var isInFullScreen = - (document.fullscreenElement && document.fullscreenElement !== null) || - (document.webkitFullscreenElement && - document.webkitFullscreenElement !== null) || - (document.mozFullScreenElement && - document.mozFullScreenElement !== null) || - (document.msFullscreenElement && document.msFullscreenElement !== null) - - var docElm = document.documentElement - if (!isInFullScreen) { - if (docElm.requestFullscreen) { - docElm.requestFullscreen() - } else if (docElm.mozRequestFullScreen) { - docElm.mozRequestFullScreen() - } else if (docElm.webkitRequestFullScreen) { - docElm.webkitRequestFullScreen() - } else if (docElm.msRequestFullscreen) { - docElm.msRequestFullscreen() - } - } else { - if (document.exitFullscreen) { - document.exitFullscreen() - } else if (document.webkitExitFullscreen) { - document.webkitExitFullscreen() - } else if (document.mozCancelFullScreen) { - document.mozCancelFullScreen() - } else if (document.msExitFullscreen) { - document.msExitFullscreen() - } - } -} $(document).ready(function () { UserInterface.init() }) diff --git a/src/essence/Basics/Viewer_/Viewer_.js b/src/essence/Basics/Viewer_/Viewer_.js index 5d4d47a1..168e52a7 100644 --- a/src/essence/Basics/Viewer_/Viewer_.js +++ b/src/essence/Basics/Viewer_/Viewer_.js @@ -3,7 +3,6 @@ import * as d3 from 'd3' import F_ from '../Formulae_/Formulae_' import L_ from '../Layers_/Layers_' -import OpenSeadragon from 'openseadragon' import * as THREE from '../../../external/THREE/three118' import Photosphere from './Photosphere' @@ -13,8 +12,6 @@ import Dropy from '../../../external/Dropy/dropy' import './Viewer_.css' -//import fabricOverlay from '../../../external/OpenSeadragon/openseadragon-fabricjs-overlay' -let fabricOverlay = {} let L = window.L var Viewer_ = { diff --git a/src/essence/Tools/Draw/DrawTool.test.js b/src/essence/Tools/Draw/DrawTool.test.js index 54c63d42..586f7adf 100644 --- a/src/essence/Tools/Draw/DrawTool.test.js +++ b/src/essence/Tools/Draw/DrawTool.test.js @@ -866,11 +866,10 @@ var Test = { ) $('#mmgisModal .drawToolFileSave').click() - $( - '#drawToolDrawFilesList > li:nth-child(2) .drawToolFileEdit' - ).click() - setTimeout(function () { + $( + '#drawToolDrawFilesList > li:nth-child(2) .drawToolFileEdit' + ).click() c( 'File name updates', $( @@ -895,7 +894,7 @@ var Test = { ) $('#mmgisModal .drawToolFileCancel').click() - }, Test.timeout) + }, Test.timeout * 3) }, Test.timeout) }, }, @@ -1714,10 +1713,6 @@ var Test = { name: 'Lead can draw ROIs', subtests: 2, test: function (c) { - $( - '#drawToolDrawFilesListMaster .drawToolDrawFilesListElem[file_id="1"] .drawToolFileSelector' - ).click() - c( 'Starts on polygon drawing', $('.drawToolDrawingTypePolygon').hasClass('active') diff --git a/src/essence/Tools/Draw/DrawTool_Drawing.js b/src/essence/Tools/Draw/DrawTool_Drawing.js index b7223a1c..71311e9c 100644 --- a/src/essence/Tools/Draw/DrawTool_Drawing.js +++ b/src/essence/Tools/Draw/DrawTool_Drawing.js @@ -20,6 +20,8 @@ var Drawing = { DrawTool.setDrawingType = Drawing.setDrawingType DrawTool.switchDrawingType = Drawing.switchDrawingType DrawTool.setDrawing = Drawing.setDrawing + + L.Draw.Polyline.prototype._onTouch = L.Util.falseFn }, drawOver: function (d, clip, callback) { var file_id = @@ -254,6 +256,8 @@ var Drawing = { case 'arrow': DrawTool.drawing.arrow.begin(type) break + default: + break } }, switchDrawingType: function (type) { @@ -294,6 +298,12 @@ var Drawing = { DrawTool.switchDrawingType('Polygon') DrawTool.drawing.polygon.begin('polygon') break + default: + DrawTool.drawing.polygon.end() + DrawTool.drawing.point.end() + DrawTool.drawing.line.end() + DrawTool.drawing.annotation.end() + break } if (DrawTool.intentType != null) { @@ -406,6 +416,7 @@ var drawing = { } d.lastVertex = e.latlng + d.shape = d.drawing._poly }, complete: function () { var d = drawing.polygon @@ -473,7 +484,7 @@ var drawing = { } } - d.shape = d.drawing._poly + d.shape = d.drawing._poly || d.shape }, stop: function () { var d = drawing.polygon @@ -594,6 +605,7 @@ var drawing = { } d.lastVertex = e.latlng + d.shape = d.drawing._poly }, complete: function () { var d = drawing.line @@ -660,7 +672,7 @@ var drawing = { } } - d.shape = d.drawing._poly + d.shape = d.drawing._poly || d.shape }, stop: function () { var d = drawing.line diff --git a/src/essence/Tools/Draw/DrawTool_Files.js b/src/essence/Tools/Draw/DrawTool_Files.js index 8e7ebccc..41a31749 100644 --- a/src/essence/Tools/Draw/DrawTool_Files.js +++ b/src/essence/Tools/Draw/DrawTool_Files.js @@ -1087,9 +1087,8 @@ var Files = { $('.drawToolFileSelector').off('click') $('.drawToolFileSelector').on('click', function () { //Only select files you own - var fileFromId = DrawTool.getFileObjectWithId( - $(this).attr('file_id') - ) + const fileId = $(this).attr('file_id') + var fileFromId = DrawTool.getFileObjectWithId(fileId) if ( mmgisglobal.user !== $(this).attr('file_owner') && fileFromId && @@ -1099,20 +1098,30 @@ var Files = { return var checkbox = $(this).parent().find('.drawToolFileCheckbox') + const wasOn = $(this).parent().parent().hasClass('checked') $('.drawToolFileCheckbox').removeClass('checked') $('.drawToolDrawFilesListElem').removeClass('checked') - checkbox.addClass('checked') - checkbox.parent().parent().parent().addClass('checked') - var intent = $(this).attr('file_intent') - if (DrawTool.intentType != intent) { - DrawTool.intentType = intent - DrawTool.setDrawing(true) - } + if (!wasOn) { + checkbox.addClass('checked') + checkbox.parent().parent().parent().addClass('checked') - DrawTool.currentFileId = parseInt(checkbox.attr('file_id')) - if (DrawTool.filesOn.indexOf(DrawTool.currentFileId) == -1) - checkbox.click() + var intent = $(this).attr('file_intent') + if (DrawTool.intentType != intent) { + DrawTool.intentType = intent + DrawTool.setDrawing(true) + } + + DrawTool.currentFileId = parseInt(checkbox.attr('file_id')) + if (DrawTool.filesOn.indexOf(DrawTool.currentFileId) == -1) + checkbox.click() + } else { + DrawTool.intentType = null + DrawTool.switchDrawingType(null) + DrawTool.setDrawing(false) + DrawTool.currentFileId = null + DrawTool.toggleFile(fileId, 'off') + } }) //Visible File diff --git a/src/essence/Tools/Draw/DrawTool_SetOperations.js b/src/essence/Tools/Draw/DrawTool_SetOperations.js index 4f98b4d0..aaa3207f 100644 --- a/src/essence/Tools/Draw/DrawTool_SetOperations.js +++ b/src/essence/Tools/Draw/DrawTool_SetOperations.js @@ -68,13 +68,13 @@ var SetOperations = { for (var i = 0; i < DrawTool.contextMenuLayers.length; i++) { // prettier-ignore lis.push( - [ - "
  • ", - "
    " + F_.sanitize(DrawTool.contextMenuLayers[i].properties.name) + "
    ", - "
    ", - "
  • " - ].join('\n') - ) + [ + "
  • ", + "
    " + F_.sanitize(DrawTool.contextMenuLayers[i].properties.name || 'No Name') + "
    ", + "
    ", + "
  • " + ].join('\n') + ) } return lis.join('\n') }, diff --git a/src/essence/Tools/Info/InfoTool.css b/src/essence/Tools/Info/InfoTool.css index b53d494b..7912c88b 100644 --- a/src/essence/Tools/Info/InfoTool.css +++ b/src/essence/Tools/Info/InfoTool.css @@ -48,9 +48,9 @@ cursor: pointer; color: #999; transition: color 0.2s cubic-bezier(0.39, 0.575, 0.565, 1); - width: 34px; - height: 34px; - line-height: 34px; + width: 40px; + height: 40px; + line-height: 40px; text-align: center; } #infoToolLocate { @@ -59,9 +59,9 @@ cursor: pointer; color: #999; transition: color 0.2s cubic-bezier(0.39, 0.575, 0.565, 1); - width: 34px; - height: 34px; - line-height: 34px; + width: 40px; + height: 40px; + line-height: 40px; text-align: center; } #infoToolShowHidden { diff --git a/src/essence/Tools/Kinds/Kinds.js b/src/essence/Tools/Kinds/Kinds.js index fa7970c1..1eb41bb5 100644 --- a/src/essence/Tools/Kinds/Kinds.js +++ b/src/essence/Tools/Kinds/Kinds.js @@ -16,10 +16,12 @@ var Kinds = { preFeatures, lastFeatureLayers ) { - L_.select(layer) + L_.setActiveFeature(layer) if (typeof kind !== 'string') return - const layerVar = L_.layersNamed[layer.options.layerName].variables + let layerVar = {} + if (L_.layersNamed[layer.options.layerName]) + layerVar = L_.layersNamed[layer.options.layerName].variables || {} // Remove temp layers Map_.rmNotNull(Map_.tempOverlayImage) diff --git a/src/essence/Tools/Layers/Filtering/Filtering.js b/src/essence/Tools/Layers/Filtering/Filtering.js index 059059d7..fe899e60 100644 --- a/src/essence/Tools/Layers/Filtering/Filtering.js +++ b/src/essence/Tools/Layers/Filtering/Filtering.js @@ -478,7 +478,8 @@ const Filtering = { ``, ], 'op', - opId + opId, + { openUp: true } ) ) Dropy.init($(elmId), function (idx) { diff --git a/src/essence/Tools/Layers/LayersTool.css b/src/essence/Tools/Layers/LayersTool.css index 4f8d74ca..2e3bb319 100644 --- a/src/essence/Tools/Layers/LayersTool.css +++ b/src/essence/Tools/Layers/LayersTool.css @@ -58,22 +58,22 @@ color: #ccc; } #layersTool #filterLayers div.vector.on { - border-bottom: 4px solid rgba(15, 119, 189, 1); + border-bottom: 4px solid var(--color-p8); } #layersTool #filterLayers div.tile.on { - border-bottom: 4px solid rgba(119, 15, 189, 1); + border-bottom: 4px solid var(--color-p9); } #layersTool #filterLayers div.vectortile.on { - border-bottom: 4px solid #bd0f8e; + border-bottom: 4px solid var(--color-p10); } #layersTool #filterLayers div.query.on { - border-bottom: 4px solid rgba(15, 189, 77, 1); + border-bottom: 4px solid var(--color-p3); } #layersTool #filterLayers div.data.on { - border-bottom: 4px solid rgba(189, 15, 50, 1); + border-bottom: 4px solid var(--color-p4); } #layersTool #filterLayers div.model.on { - border-bottom: 4px solid rgba(189, 189, 15, 1); + border-bottom: 4px solid var(--color-p0); } #layersTool #filterLayers div.visible.on { border-bottom: 4px solid rgba(255, 255, 255, 1); @@ -126,6 +126,7 @@ margin: 1px 0px; color: var(--color-f); background: var(--color-j); + overflow: hidden; } #layersToolList > li.forceOff { height: 0 !important; @@ -304,22 +305,22 @@ height: 100%; } .layersToolColor.vector { - background: rgb(15, 119, 189); + background: var(--color-p8); } .layersToolColor.tile { - background: rgb(119, 15, 189); + background: var(--color-p9); } .layersToolColor.vectortile { - background: #bd0f8e; + background: var(--color-p10); } .layersToolColor.query { - background: rgb(15, 189, 77); + background: var(--color-p3); } .layersToolColor.data { - background: rgb(189, 15, 50); + background: var(--color-p4); } .layersToolColor.model { - background: rgb(189, 189, 15); + background: var(--color-p0); } #LayersToolLayer.model { @@ -418,7 +419,7 @@ #LayersToolNav { padding: 0px 10px; text-align: left; - background: #26a8ff; + background: var(--color-mmgis); box-shadow: 0px 4px 2px rgba(0, 0, 0, 0.2); } diff --git a/src/essence/Tools/Layers/LayersTool.js b/src/essence/Tools/Layers/LayersTool.js index 44b78ddc..0e111d97 100644 --- a/src/essence/Tools/Layers/LayersTool.js +++ b/src/essence/Tools/Layers/LayersTool.js @@ -211,7 +211,7 @@ function interfaceWithMMGIS() { '', '
    ', L_.layersGroupSublayers[node[i].name] ? Object.keys(L_.layersGroupSublayers[node[i].name]).map((function(i){return function(s) { - return [ + return L_.layersGroupSublayers[node[i].name][s] === false ? '' : [ '
    ', `
    ${F_.prettifyName(s)}
    `, '
    ', diff --git a/src/essence/Tools/Measure/MeasureTool.js b/src/essence/Tools/Measure/MeasureTool.js index 76a8bc3f..938a92c2 100644 --- a/src/essence/Tools/Measure/MeasureTool.js +++ b/src/essence/Tools/Measure/MeasureTool.js @@ -727,7 +727,12 @@ function makeMeasureToolLayer() { function makeProfile() { var numOfPts = clickedLatLngs.length if (numOfPts > 1 && MeasureTool.vars.dem) { - var pathDEM = 'Missions/' + L_.mission + '/' + MeasureTool.vars.dem + // enable remote access via GDAL Virtual File Systems /vsi* prefix + if (MeasureTool.vars.dem.startsWith('/vsi')) { + var pathDEM = MeasureTool.vars.dem + } else { + var pathDEM = 'Missions/' + L_.mission + '/' + MeasureTool.vars.dem + } //elevPoints.push([{"x": clickedLatLngs[numOfPts - 2].x, "y": clickedLatLngs[numOfPts - 2].y}, {"x": clickedLatLngs[numOfPts - 1].x, "y": clickedLatLngs[numOfPts - 1].y}]); elevPoints = [ { @@ -771,19 +776,23 @@ function makeProfile() { 'Warning: MeasureTool: No elevation data found in ' + pathDEM ) - MeasureTool.reset() - return - } - try { - data = data.replace(/[\n\r]/g, '') - data = JSON.parse(data) - } catch (err) { - console.log(err) - // Fake a line between the most then + // Fake a no data line between them then data = [ [elevPoints[0].y, elevPoints[0].x, 0], [elevPoints[1].y, elevPoints[1].x, 0], ] + } else { + try { + data = data.replace(/[\n\r]/g, '') + data = JSON.parse(data) + } catch (err) { + console.log(err) + // Fake a no data line between them then + data = [ + [elevPoints[0].y, elevPoints[0].x, 0], + [elevPoints[1].y, elevPoints[1].x, 0], + ] + } } if (mode === 'segment') MeasureTool.data = F_.clone(data) diff --git a/src/essence/mmgisAPI/mmgisAPI.js b/src/essence/mmgisAPI/mmgisAPI.js index 9c5cf1f9..9369c398 100644 --- a/src/essence/mmgisAPI/mmgisAPI.js +++ b/src/essence/mmgisAPI/mmgisAPI.js @@ -11,7 +11,8 @@ var mmgisAPI_ = { fina: function (map_) { mmgisAPI_.map = map_.map mmgisAPI.map = map_.map - if( typeof mmgisAPI_.onLoadCallback === 'function' ) mmgisAPI_.onLoadCallback() + if (typeof mmgisAPI_.onLoadCallback === 'function') + mmgisAPI_.onLoadCallback() }, // Returns an array of all features in a given extent featuresContained: function () { @@ -28,42 +29,60 @@ var mmgisAPI_ = { for (let key in L_.layersGroup) { if (L_.layersGroup[key].hasOwnProperty('_layers')) { // For normal layers - const foundFeatures = findFeaturesInLayer(extent, L_.layersGroup[key]) + const foundFeatures = findFeaturesInLayer( + extent, + L_.layersGroup[key] + ) features[key] = foundFeatures - } else if (key.startsWith('DrawTool_') && Array.isArray(L_.layersGroup[key])) { + } else if ( + key.startsWith('DrawTool_') && + Array.isArray(L_.layersGroup[key]) + ) { // If layer is a DrawTool array of layers for (let layer in L_.layersGroup[key]) { let foundFeatures if ('getLayers' in L_.layersGroup[key][layer]) { - if (L_.layersGroup[key][layer]?.feature?.properties?.arrow) { - // If the DrawTool sublayer is an arrow - foundFeatures = findFeaturesInLayer(extent, L_.layersGroup[key][layer]) + if ( + L_.layersGroup[key][layer]?.feature?.properties + ?.arrow + ) { + // If the DrawTool sublayer is an arrow + foundFeatures = findFeaturesInLayer( + extent, + L_.layersGroup[key][layer] + ) // As long as one of the layers of the arrow layer is in the current Map bounds, // return the parent arrow layer's feature if (foundFeatures && foundFeatures.length > 0) { - foundFeatures = L_.layersGroup[key][layer].feature + foundFeatures = + L_.layersGroup[key][layer].feature } } else { // If the DrawTool sublayer is Polygon or Line - foundFeatures = findFeaturesInLayer(extent, L_.layersGroup[key][layer]) + foundFeatures = findFeaturesInLayer( + extent, + L_.layersGroup[key][layer] + ) } - } else if ('getLatLng' in L_.layersGroup[key][layer]) { // If the DrawTool sublayer is a Point if (isLayerInBounds(L_.layersGroup[key][layer])) { foundFeatures = [L_.layersGroup[key][layer].feature] } - } + } if (foundFeatures) { - features[key] = key in features ? features[key].concat(foundFeatures) : foundFeatures + features[key] = + key in features + ? features[key].concat(foundFeatures) + : foundFeatures } } } } - return features; + return features function isLayerInBounds(layer) { // Use the pixel coordinates instead of latlong as latlong does not work well with polar projections @@ -76,8 +95,10 @@ var mmgisAPI_ = { const sw = mmgisAPI_.map.project(extent.getSouthWest()) let _extent - if (Math.abs((Math.abs(nw.x - se.x) - xMapSize)) < epsilon - && Math.abs((Math.abs(nw.y - se.y) - yMapSize)) < epsilon) { + if ( + Math.abs(Math.abs(nw.x - se.x) - xMapSize) < epsilon && + Math.abs(Math.abs(nw.y - se.y) - yMapSize) < epsilon + ) { _extent = L.bounds(nw, se) } else { _extent = L.bounds(ne, sw) @@ -86,8 +107,12 @@ var mmgisAPI_ = { let found = false if ('getBounds' in layer) { const layerBounds = layer.getBounds() - const nwLayer = mmgisAPI_.map.project(layerBounds.getNorthEast()) - const seLayer = mmgisAPI_.map.project(layerBounds.getSouthWest()) + const nwLayer = mmgisAPI_.map.project( + layerBounds.getNorthEast() + ) + const seLayer = mmgisAPI_.map.project( + layerBounds.getSouthWest() + ) const _bounds = L.bounds(nwLayer, seLayer) if (_extent.intersects(_bounds)) { @@ -125,13 +150,15 @@ var mmgisAPI_ = { if (infoTool.currentLayer && infoTool.currentLayer.feature) { const activeFeature = {} - activeFeature[infoTool.currentLayerName] = [infoTool.currentLayer.feature] + activeFeature[infoTool.currentLayerName] = [ + infoTool.currentLayer.feature, + ] return activeFeature } - return null; + return null }, - // Returns an object with the visiblity state of all layers + // Returns an object with the visibility state of all layers getVisibleLayers: function () { // Also return the visibility of the DrawTool layers var drawToolVisibility = {} @@ -140,7 +167,10 @@ var mmgisAPI_ = { var s = l.split('_') var onId = s[1] != 'master' ? parseInt(s[1]) : s[1] if (s[0] == 'DrawTool') { - drawToolVisibility[l] = ToolController_.getTool('DrawTool').filesOn.indexOf(onId) != -1 + drawToolVisibility[l] = + ToolController_.getTool('DrawTool').filesOn.indexOf( + onId + ) != -1 } } } @@ -155,8 +185,7 @@ var mmgisAPI_ = { mmgisAPI_.map.addEventListener(listener, functionReference) } else { console.warn( - 'Warning: Unable to add event listener for ' + - eventName + 'Warning: Unable to add event listener for ' + eventName ) } }, @@ -168,8 +197,7 @@ var mmgisAPI_ = { mmgisAPI_.map.removeEventListener(listener, functionReference) } else { console.warn( - 'Warning: Unable to remove event listener for ' + - eventName + 'Warning: Unable to remove event listener for ' + eventName ) } }, @@ -181,13 +209,13 @@ var mmgisAPI_ = { } else if (eventName === 'onClick') { return 'click' } - return null + return null }, - writeCoordinateURL: function() { - return QueryURL.writeCoordinateURL(false); + writeCoordinateURL: function () { + return QueryURL.writeCoordinateURL(false) }, onLoadCallback: null, - onLoaded: function(onLoadCallback) { + onLoaded: function (onLoadCallback) { mmgisAPI_.onLoadCallback = onLoadCallback }, } @@ -230,6 +258,21 @@ var mmgisAPI = { * @param {keepFirstN} - keepN - number of features to keep from the beginning of the feature list. A value less than or equal to 0 keeps all previous features */ keepFirstN: L_.keepFirstN, + /** + * This function is used to trim a specified number of vertices on a specified layer containing GeoJson LineString features. + * @param {string} - layerName - name of layer to update + * @param {string} - time - absolute time in the format of YYYY-MM-DDThh:mm:ssZ; represents start time if trimming from the beginning, otherwise represents the end time + * @param {number} - trimN - number of vertices to trim + * @param {string} - startOrEnd - direction to trim from; value can only be one of the following options: start, end + */ + trimLineString: L_.trimLineString, + /** + * This function is used to append new LineString data to the last feature (with LineString geometry) in a layer + * @param {string} - layerName - name of layer to update + * @param {object} - inputData - a GeoJson Feature object containing geometry that is a LineString + * @param {string} - timeProp - name of time property in each feature in the layer and in the inputData + */ + appendLineString: L_.appendLineString, // Time Control API functions @@ -260,38 +303,38 @@ var mmgisAPI = { * @returns {boolean} - Whether the time was successfully set */ setLayerTime: TimeControl.setLayerTime, - - /** + + /** * @returns {string} - The current time on the map with offset included */ getTime: TimeControl.getTime, - /** + /** * @returns {string} - The start time on the map with offset included - */ + */ getStartTime: TimeControl.getStartTime, - /** + /** * @returns {string} - The end time on the map with offset included - */ + */ getEndTime: TimeControl.getEndTime, - /** + /** * @param {string} [layerName] * @returns {string} - The start time for an individual layer - */ + */ getLayerStartTime: TimeControl.getLayerStartTime, - - /** + + /** * @param {string} [layerName] * @returns {string} - The end time for an individual layer - */ + */ getLayerEndTime: TimeControl.getLayerEndTime, /** reloadTimeLayers will reload every time enabled layer * @returns {array} - A list of layers that were reloaded */ - reloadTimeLayers: TimeControl.reloadTimeLayers, + reloadTimeLayers: TimeControl.reloadTimeLayers, /** reloadLayer will reload a given time enabled layer * @param {string} [layerName] @@ -302,14 +345,14 @@ var mmgisAPI = { /** setLayersTimeStatus - will set the status color for all global time enabled layers * @param {string} [color] * @returns {array} - A list of layers that were set - */ + */ setLayersTimeStatus: TimeControl.setLayersTimeStatus, /** setLayerTimeStatus - will set the status color for the given layer - * @param {string} [layerName] - * @param {string} [color] - * @returns {boolean} - True if time status was successfully set - */ + * @param {string} [layerName] + * @param {string} [color] + * @returns {boolean} - True if time status was successfully set + */ setLayerTimeStatus: TimeControl.setLayerTimeStatus, /** updateLayersTime - will synchronize every global time enabled layer with global times. @@ -317,24 +360,24 @@ var mmgisAPI = { * may need to be re-synchronized. * @returns {array} - A list of layers that were reloaded */ - updateLayersTime: TimeControl.updateLayersTime, + updateLayersTime: TimeControl.updateLayersTime, /** map - exposes Leaflet map object. - * @returns {object} - The Leaflet map object + * @returns {object} - The Leaflet map object */ - map: null, + map: null, /** featuresContained - returns an array of all features in the current map view. * @returns {object} - An object containing layer names as keys and values as arrays with all features (as GeoJson Feature objects) contained in the current map view */ featuresContained: mmgisAPI_.featuresContained, - /** getActiveFeature - returns the currently active feature (i.e. feature thats clicked and displayed in the InfoTool) + /** getActiveFeature - returns the currently active feature (i.e. feature thats clicked and displayed in the InfoTool) * @returns {object} - The currently selected active feature as an object with the layer name as key and value as an array containing the GeoJson Feature object (MMGIS only allows the section of a single feature). */ getActiveFeature: mmgisAPI_.getActiveFeature, - /** getVisibleLayers - returns an object with the visiblity state of all layers + /** getVisibleLayers - returns an object with the visibility state of all layers * @returns {object} - an object containing the visibility state of each layer */ getVisibleLayers: mmgisAPI_.getVisibleLayers, @@ -346,7 +389,7 @@ var mmgisAPI = { */ addEventListener: mmgisAPI_.addEventListener, - /** removeEventListener - removes map event listener added using the MMGIS API. + /** removeEventListener - removes map event listener added using the MMGIS API. * @param {string} - eventName - name of event to add listener to. Available events: onPan, onZoom, onClick * @param {function} - functionReference - function reference to listener event callback function. null value removes all functions for a given eventName */ diff --git a/src/external/Dropy/dropy.css b/src/external/Dropy/dropy.css index 3933c833..8a25a74b 100644 --- a/src/external/Dropy/dropy.css +++ b/src/external/Dropy/dropy.css @@ -128,3 +128,10 @@ dd { .dropy.dark .dropy__title:hover { border-color: #cccccc; } + +.dropy.openUp .dropy__content { + top: unset; +} +.dropy.openUp .dropy__content > ul { + transform: translateY(-100%); +} diff --git a/src/external/Dropy/dropy.js b/src/external/Dropy/dropy.js index 49b68239..ab4809df 100644 --- a/src/external/Dropy/dropy.js +++ b/src/external/Dropy/dropy.js @@ -12,15 +12,16 @@ export default { // items [] // placeholder // selectedIndex int null for none - construct: function (items, placeholder, selectedIndex) { + construct: function (items, placeholder, selectedIndex, options) { + options = options || {} // prettier-ignore return [ - '
    ', + `
    `, `
    ${items[selectedIndex] || placeholder}
    `, '
    ', '
      ', placeholder ? `
    • ${placeholder}
    • ` : '', - items.map((item, i) => `
    • ${item}
    • `).join('\n'), + items.map((item, i) => `
    • ${item}
    • `).join('\n'), '
    ', '
    ', '', diff --git a/src/external/Leaflet/leaflet-imagetransform.js b/src/external/Leaflet/leaflet-imagetransform.js index 48e4e362..61703848 100644 --- a/src/external/Leaflet/leaflet-imagetransform.js +++ b/src/external/Leaflet/leaflet-imagetransform.js @@ -25,9 +25,7 @@ this._anchors.push(L.latLng(yx)) } - if (this._map) { - this._reset() - } + this._reset() }, _latLngToLayerPoint: function (latlng) { @@ -130,7 +128,7 @@ }, _reset: function () { - if (this.options.clip && !this._imgLoaded) { + if (this._map == null || (this.options.clip && !this._imgLoaded)) { return } var div = this._image, @@ -157,24 +155,25 @@ div.style.width = size.x + 'px' div.style.height = size.y + 'px' - var matrix3d = (this._matrix3d = L.ImageTransform.Utils.general2DProjection( - 0, - 0, - pixels[0].x, - pixels[0].y, - w, - 0, - pixels[1].x, - pixels[1].y, - w, - h, - pixels[2].x, - pixels[2].y, - 0, - h, - pixels[3].x, - pixels[3].y - )) + var matrix3d = (this._matrix3d = + L.ImageTransform.Utils.general2DProjection( + 0, + 0, + pixels[0].x, + pixels[0].y, + w, + 0, + pixels[1].x, + pixels[1].y, + w, + h, + pixels[2].x, + pixels[2].y, + 0, + h, + pixels[3].x, + pixels[3].y + )) //something went wrong (for example, target image size is less then one pixel) if (!matrix3d[8]) { diff --git a/src/external/Leaflet/leaflet.vectorGrid.bundled.js b/src/external/Leaflet/leaflet.vectorGrid.bundled.js index 6e25defe..d06078e0 100644 --- a/src/external/Leaflet/leaflet.vectorGrid.bundled.js +++ b/src/external/Leaflet/leaflet.vectorGrid.bundled.js @@ -722,7 +722,9 @@ readField(tag, result, this$1) if (this$1.pos === startPos) { - this$1.skip(val) + try { + this$1.skip(val) + } catch (err) {} } } return result @@ -2434,9 +2436,8 @@ var styleOptions = layerStyle if (storeFeatures) { id = this.options.getFeatureId(feat) - var styleOverride = this._overriddenStyles[ - id - ] + var styleOverride = + this._overriddenStyles[id] if (styleOverride) { if (styleOverride[layerName]) { styleOptions = @@ -2942,8 +2943,9 @@ for (var lvid in this._map._layers[id]._vectorTiles[ vid ]._layers) { - layer = this._map._layers[id]._vectorTiles[vid] - ._layers[lvid] + layer = + this._map._layers[id]._vectorTiles[vid] + ._layers[lvid] if ( layer._pxBounds.min.x <= point.x && layer._pxBounds.max.x >= point.x && @@ -2952,8 +2954,8 @@ !this._map._draggableMoved(layer) ) { clickedLayer = layer - layerName = this._map._layers[id].options - .layerName + layerName = + this._map._layers[id].options.layerName } } } diff --git a/src/external/OpenSeadragon/fabric.adapted.js b/src/external/OpenSeadragon/fabric.adapted.js index b9e42af7..b3e960da 100644 --- a/src/external/OpenSeadragon/fabric.adapted.js +++ b/src/external/OpenSeadragon/fabric.adapted.js @@ -1,30 +1,15 @@ /* build: `node build.js modules=ALL exclude=json,gestures minifier=uglifyjs` */ /*! Fabric.js Copyright 2008-2015, Printio (Juriy Zaytsev, Maxim Chernyak) */ -var fabric = fabric || { version: "1.7.17" }; +window.fabric = fabric = window.fabric || { version: "1.7.17" }; if (typeof exports !== 'undefined') { exports.fabric = fabric; } -if (typeof document !== 'undefined' && typeof window !== 'undefined') { - fabric.document = document; - fabric.window = window; - // ensure globality even if entire library were function wrapped (as in Meteor.js packaging system) - window.fabric = fabric; -} -else { - // assume we're running under node.js when document/window are not present - fabric.document = require("jsdom") - .jsdom( - decodeURIComponent("%3C!DOCTYPE%20html%3E%3Chtml%3E%3Chead%3E%3C%2Fhead%3E%3Cbody%3E%3C%2Fbody%3E%3C%2Fhtml%3E") - ); - - if (fabric.document.createWindow) { - fabric.window = fabric.document.createWindow(); - } else { - fabric.window = fabric.document.parentWindow; - } -} +fabric.document = document; +fabric.window = window; +// ensure globality even if entire library were function wrapped (as in Meteor.js packaging system) +window.fabric = fabric; /** * True when in environment that supports touch events @@ -986,9 +971,7 @@ fabric.CommonMethods = { * @return {HTMLImageElement} HTML image element */ createImage: function() { - return fabric.isLikelyNode - ? new (require('canvas').Image)() - : fabric.document.createElement('img'); + return fabric.document.createElement('img'); }, /** @@ -26890,219 +26873,3 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot }; })(); - -(function() { - - if (typeof document !== 'undefined' && typeof window !== 'undefined') { - return; - } - - var DOMParser = require('xmldom').DOMParser, - URL = require('url'), - HTTP = require('http'), - HTTPS = require('https'), - - Canvas = require('canvas'), - Image = require('canvas').Image; - - /** @private */ - function request(url, encoding, callback) { - var oURL = URL.parse(url); - - // detect if http or https is used - if ( !oURL.port ) { - oURL.port = ( oURL.protocol.indexOf('https:') === 0 ) ? 443 : 80; - } - - // assign request handler based on protocol - var reqHandler = (oURL.protocol.indexOf('https:') === 0 ) ? HTTPS : HTTP, - req = reqHandler.request({ - hostname: oURL.hostname, - port: oURL.port, - path: oURL.path, - method: 'GET' - }, function(response) { - var body = ''; - if (encoding) { - response.setEncoding(encoding); - } - response.on('end', function () { - callback(body); - }); - response.on('data', function (chunk) { - if (response.statusCode === 200) { - body += chunk; - } - }); - }); - - req.on('error', function(err) { - if (err.errno === process.ECONNREFUSED) { - fabric.log('ECONNREFUSED: connection refused to ' + oURL.hostname + ':' + oURL.port); - } - else { - fabric.log(err.message); - } - callback(null); - }); - - req.end(); - } - - /** @private */ - function requestFs(path, callback) { - var fs = require('fs'); - fs.readFile(path, function (err, data) { - if (err) { - fabric.log(err); - throw err; - } - else { - callback(data); - } - }); - } - - fabric.util.loadImage = function(url, callback, context) { - function createImageAndCallBack(data) { - if (data) { - img.src = new Buffer(data, 'binary'); - // preserving original url, which seems to be lost in node-canvas - img._src = url; - callback && callback.call(context, img); - } - else { - img = null; - callback && callback.call(context, null, true); - } - } - var img = new Image(); - if (url && (url instanceof Buffer || url.indexOf('data') === 0)) { - img.src = img._src = url; - callback && callback.call(context, img); - } - else if (url && url.indexOf('http') !== 0) { - requestFs(url, createImageAndCallBack); - } - else if (url) { - request(url, 'binary', createImageAndCallBack); - } - else { - callback && callback.call(context, url); - } - }; - - fabric.loadSVGFromURL = function(url, callback, reviver) { - url = url.replace(/^\n\s*/, '').replace(/\?.*$/, '').trim(); - if (url.indexOf('http') !== 0) { - requestFs(url, function(body) { - fabric.loadSVGFromString(body.toString(), callback, reviver); - }); - } - else { - request(url, '', function(body) { - fabric.loadSVGFromString(body, callback, reviver); - }); - } - }; - - fabric.loadSVGFromString = function(string, callback, reviver) { - var doc = new DOMParser().parseFromString(string); - fabric.parseSVGDocument(doc.documentElement, function(results, options) { - callback && callback(results, options); - }, reviver); - }; - - fabric.util.getScript = function(url, callback) { - request(url, '', function(body) { - // eslint-disable-next-line no-eval - eval(body); - callback && callback(); - }); - }; - - // fabric.util.createCanvasElement = function(_, width, height) { - // return new Canvas(width, height); - // } - - /** - * Only available when running fabric on node.js - * @param {Number} width Canvas width - * @param {Number} height Canvas height - * @param {Object} [options] Options to pass to FabricCanvas. - * @param {Object} [nodeCanvasOptions] Options to pass to NodeCanvas. - * @return {Object} wrapped canvas instance - */ - fabric.createCanvasForNode = function(width, height, options, nodeCanvasOptions) { - nodeCanvasOptions = nodeCanvasOptions || options; - - var canvasEl = fabric.document.createElement('canvas'), - nodeCanvas = new Canvas(width || 600, height || 600, nodeCanvasOptions), - nodeCacheCanvas = new Canvas(width || 600, height || 600, nodeCanvasOptions); - - // jsdom doesn't create style on canvas element, so here be temp. workaround - canvasEl.style = { }; - - canvasEl.width = nodeCanvas.width; - canvasEl.height = nodeCanvas.height; - options = options || { }; - options.nodeCanvas = nodeCanvas; - options.nodeCacheCanvas = nodeCacheCanvas; - var FabricCanvas = fabric.Canvas || fabric.StaticCanvas, - fabricCanvas = new FabricCanvas(canvasEl, options); - fabricCanvas.nodeCanvas = nodeCanvas; - fabricCanvas.nodeCacheCanvas = nodeCacheCanvas; - fabricCanvas.contextContainer = nodeCanvas.getContext('2d'); - fabricCanvas.contextCache = nodeCacheCanvas.getContext('2d'); - fabricCanvas.Font = Canvas.Font; - return fabricCanvas; - }; - - var originaInitStatic = fabric.StaticCanvas.prototype._initStatic; - fabric.StaticCanvas.prototype._initStatic = function(el, options) { - el = el || fabric.document.createElement('canvas'); - this.nodeCanvas = new Canvas(el.width, el.height); - this.nodeCacheCanvas = new Canvas(el.width, el.height); - originaInitStatic.call(this, el, options); - this.contextContainer = this.nodeCanvas.getContext('2d'); - this.contextCache = this.nodeCacheCanvas.getContext('2d'); - this.Font = Canvas.Font; - }; - - /** @ignore */ - fabric.StaticCanvas.prototype.createPNGStream = function() { - return this.nodeCanvas.createPNGStream(); - }; - - fabric.StaticCanvas.prototype.createJPEGStream = function(opts) { - return this.nodeCanvas.createJPEGStream(opts); - }; - - fabric.StaticCanvas.prototype._initRetinaScaling = function() { - if (!this._isRetinaScaling()) { - return; - } - - this.lowerCanvasEl.setAttribute('width', this.width * fabric.devicePixelRatio); - this.lowerCanvasEl.setAttribute('height', this.height * fabric.devicePixelRatio); - this.nodeCanvas.width = this.width * fabric.devicePixelRatio; - this.nodeCanvas.height = this.height * fabric.devicePixelRatio; - this.contextContainer.scale(fabric.devicePixelRatio, fabric.devicePixelRatio); - return this; - }; - if (fabric.Canvas) { - fabric.Canvas.prototype._initRetinaScaling = fabric.StaticCanvas.prototype._initRetinaScaling; - } - - var origSetBackstoreDimension = fabric.StaticCanvas.prototype._setBackstoreDimension; - fabric.StaticCanvas.prototype._setBackstoreDimension = function(prop, value) { - origSetBackstoreDimension.call(this, prop, value); - this.nodeCanvas[prop] = value; - return this; - }; - if (fabric.Canvas) { - fabric.Canvas.prototype._setBackstoreDimension = fabric.StaticCanvas.prototype._setBackstoreDimension; - } - -})(); - diff --git a/src/external/OpenSeadragon/openseadragon.js b/src/external/OpenSeadragon/openseadragon.js index 78043243..38ac6068 100644 --- a/src/external/OpenSeadragon/openseadragon.js +++ b/src/external/OpenSeadragon/openseadragon.js @@ -2796,8 +2796,11 @@ function OpenSeadragon(options) { // Universal Module Definition, supports CommonJS, AMD and simple script tag ;(function (root, factory) { if (typeof define === 'function' && define.amd) { + // expose as window.OpenSeadragon + window.OpenSeadragon = root.OpenSeadragon = factory() + // expose as amd module - define([], factory) + //define([], factory) } else if (typeof module === 'object' && module.exports) { // expose as commonjs module module.exports = factory() diff --git a/src/index.js b/src/index.js index ab1adcb5..a5ec4758 100644 --- a/src/index.js +++ b/src/index.js @@ -41,6 +41,10 @@ import Detector from './external/THREE/Detector' import VRControls from './external/THREE/VRControls' import ThreeAR from './external/THREE/three.ar' +import OpenSeadragon from './external/OpenSeadragon/openseadragon' +import fabricOverlay from './external/OpenSeadragon/openseadragon-fabricjs-overlay' +import fabricA from './external/OpenSeadragon/fabric.adapted' + import './fonts/materialdesignicons/css/materialdesignicons.min.css' import './external/Leaflet/leaflet1.5.1.css' import './external/Leaflet/leaflet.draw.css' diff --git a/views/configure.pug b/views/configure.pug index 406442e7..2b32b88a 100644 --- a/views/configure.pug +++ b/views/configure.pug @@ -1,7 +1,7 @@ doctype html head title MMGIS Configure - link(rel='shortcut icon' href='../public/images/logos/logo.png') + link(rel='shortcut icon' href='public/images/logos/logo.png') link(type='text/css' rel='stylesheet' href='public/fonts/materialdesignicons/css/materialdesignicons.min.css') // Import materialize.css link(type='text/css' rel='stylesheet' href='config/css/jquery-ui.css') @@ -11,6 +11,7 @@ head link(type='text/css' rel='stylesheet' href='config/css/keys.css') link(type='text/css' rel='stylesheet' href='config/css/datasets.css') link(type='text/css' rel='stylesheet' href='config/css/geodatasets.css') + link(type='text/css' rel='stylesheet' href='config/css/webhooks.css') link(type='text/css' rel='stylesheet' href='config/css/config.css') // Let browser know website is optimized for mobile meta(name='viewport' content='width=device-width, initial-scale=1.0') @@ -34,6 +35,7 @@ script(type='text/javascript' src='config/js/calls.js') script(type='text/javascript' src='config/js/keys.js') script(type='text/javascript' src='config/js/datasets.js') script(type='text/javascript' src='config/js/geodatasets.js') +script(type='text/javascript' src='config/js/webhooks.js') script(type='text/javascript' src='config/js/config.js') script(type='text/javascript' src='src/pre/RefreshAuth.js') @@ -41,13 +43,14 @@ script(type='text/javascript' src='src/pre/RefreshAuth.js') #tbtitle //i.logout#config_logout.mdi.mdi-logout.mdi-24px a.logout#tbmmgis - img(src='../public/images/logos/mmgis.png' height='24px' style='position: relative; top: 13px;') + img(src='public/images/logos/mmgis.png' height='24px' style='position: relative; top: 13px;') span#tbconfig Configuration a#new_mission.btn.waves-effect.waves-light.col.s12.truncate(style='background-color: rgba(255,255,255,0.12);') New Mission ul#missions.mmgisScrollbar a#manage_keys.btn.waves-effect.waves-light.col.s12.truncate(style='background-color: rgba(255,255,255,0.12);') Keys a#manage_datasets.btn.waves-effect.waves-light.col.s12.truncate(style='background-color: rgba(255,255,255,0.12);') Manage Datasets a#manage_geodatasets.btn.waves-effect.waves-light.col.s12.truncate(style='background-color: rgba(255,255,255,0.12);') Manage Geodatasets + a#manage_webhooks.btn.waves-effect.waves-light.col.s12.truncate(style='background-color: rgba(255,255,255,0.12);') Manage Webhooks #logout i.logout#config_logout.mdi.mdi-logout.mdi-24px @@ -56,7 +59,7 @@ script(type='text/javascript' src='src/pre/RefreshAuth.js') #tbtitle i.logout#config_logout.mdi.mdi-logout.mdi-24px a.logout#tbmmgis - img(src='../public/images/logos/mmgis.png' height='24px' style='position: relative; top: 4px;') + img(src='public/images/logos/mmgis.png' height='24px' style='position: relative; top: 4px;') span#tbconfig Configuration #divline #mission_bar.row.z-depth-1 @@ -67,6 +70,7 @@ script(type='text/javascript' src='src/pre/RefreshAuth.js') .container_keys .container_datasets .container_geodatasets +.container_webhooks .container #home_cont(style='display: none') MMGIS Configuration #new_mission_cont(style='display: none; margin-top: 25vh;') From d92cb850daeed78e1eba054b83e2f9dfe2b09aa8 Mon Sep 17 00:00:00 2001 From: David Tsay <3614296+davetsay@users.noreply.github.com> Date: Thu, 26 May 2022 14:44:34 -0700 Subject: [PATCH 2/5] Description should be updated to match example code (#189) --- docs/pages/markdowns/Vector.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/pages/markdowns/Vector.md b/docs/pages/markdowns/Vector.md index 044b9541..160c2407 100644 --- a/docs/pages/markdowns/Vector.md +++ b/docs/pages/markdowns/Vector.md @@ -185,7 +185,7 @@ Example: } ``` -- `useNameAsKey`: 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. +- `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. - `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. - `prop`: This is a property key already within the features properties. It's value will be searched for in the specified dataset column. - `dataset`: The name of a dataset to link to. A list of datasets can be found in the "Manage Datasets" page. From 2396a9abb4b41cb458d152aa7b42032090d5e982 Mon Sep 17 00:00:00 2001 From: James Montalvo Date: Thu, 26 Jan 2023 12:30:23 -0600 Subject: [PATCH 3/5] ci: build docker image and push to GitHub Container Registry (#316) --- .github/workflows/docker-build.yml | 104 +++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 .github/workflows/docker-build.yml diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 00000000..457e87bc --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,104 @@ +name: Docker build and push + +# limit concurrency +# https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#examples-using-concurrency-and-the-default-behavior +concurrency: docker_mmgis_main + +on: + push: + # Only activate for `master` branch + branches: + - master + # Plus for all tags + tags: + - '*' + + # Plus for any pull-requests + pull_request: + +env: + # Will be "NASA-AMMOS/MMGIS" for the main repo, for forks "user-name-of-fork/MMGIS" + # For generating the tag, all will be converted to lowercase + IMAGE_SLUG: ${{ github.repository }} + +jobs: + # Push image to GitHub Container Registry. + # The image tag pattern is: + # for pull-requests: --, eg: 1.35.2-20210125-25 + # for tags: + # for `master` branch: latest,-latest,-latest,-latest,-- + # Version is parsed from package.json + push: + runs-on: ubuntu-latest + if: github.event_name == 'push' || github.event_name == 'pull_request' + steps: + - name: Checkout + uses: actions/checkout@v3 + + - + name: Generate tags + id: generate + run: | + + # Image ID + IMAGE_ID=ghcr.io/$IMAGE_SLUG + + # Date + BDATE=$(date +%Y%m%d) + + # PATCH_VERSION from package.json is like "1.2.3" + # MINOR_VERSION like "1.2", MAJOR_VERSION like "1" + PATCH_VERSION=$(jq .version -r ./package.json) + MINOR_VERSION=${PATCH_VERSION%.*} + MAJOR_VERSION=${MINOR_VERSION%.*} + + # Change all uppercase to lowercase, just in case + IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]') + + # Strip git ref prefix from version and use it as suffix for version + VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') + + # For pull requests just extract the PR number + PR_NUMBER="" + [ "${{ github.event_name }}" == "pull_request" ] && VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\)/merge,\1,') + [ "${{ github.event_name }}" == "pull_request" ] && PR_NUMBER=$VERSION + + # Append version + [ "${{ github.event_name }}" == "pull_request" ] && VERSION=$PATCH_VERSION-$BDATE-$VERSION + + # Strip "v" prefix from tag name if it's a tag + # [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//') + + # Use Docker `latest` tag convention if it's a master branch build + [ "$VERSION" == "master" ] && VERSION=latest + + # Compose REGISTRY_TAGS variable + REGISTRY_TAGS="-t $IMAGE_ID:$VERSION" + + # For master branch also supply an extra tag: -latest,-latest,-latest,-- + [ "$VERSION" == "latest" ] && REGISTRY_TAGS="$REGISTRY_TAGS -t $IMAGE_ID:$PATCH_VERSION-latest -t $IMAGE_ID:$MINOR_VERSION-latest -t $IMAGE_ID:$MAJOR_VERSION-latest -t $IMAGE_ID:$PATCH_VERSION-$BDATE-$(git rev-parse --short HEAD)" + + echo IMAGE_ID=$IMAGE_ID + echo VERSION=$VERSION + echo REGISTRY_TAGS=$REGISTRY_TAGS + echo headref=${{ github.head_ref }} + + SHA_SHORT=${{ github.sha }} + [ "${{ github.event_name }}" == "pull_request" ] && SHA_SHORT=$(echo ${{ github.event.pull_request.head.sha }} | cut -c1-8) + + echo "Final image tag to be pushed:" + echo $REGISTRY_TAGS + echo "REGISTRY_TAGS=$REGISTRY_TAGS" >> $GITHUB_OUTPUT + echo "IMAGE_ID=$IMAGE_ID" >> $GITHUB_OUTPUT + echo "REGISTRY_TAGS_VERSION=$VERSION" >> $GITHUB_OUTPUT + echo "REGISTRY_TAGS_PR_NUMBER=$PR_NUMBER" >> $GITHUB_OUTPUT + echo "SHA_SHORT=$SHA_SHORT" >> $GITHUB_OUTPUT + + - name: Login to GHCR + run: echo ${{ secrets.GITHUB_TOKEN }} | docker login -u ${{ github.actor }} --password-stdin ghcr.io + + - name: Docker build + run: docker build ${{ steps.generate.outputs.REGISTRY_TAGS }} . + + - name: Docker push + run: docker push ${{ steps.generate.outputs.IMAGE_ID }} --all-tags From 3d33ea0bd2be38c7271fa529e977b87e2e47bdc0 Mon Sep 17 00:00:00 2001 From: tariqksoliman Date: Thu, 2 Feb 2023 15:54:05 -0800 Subject: [PATCH 4/5] Update docker-build workflow to trigger on deve branch and releases --- .github/workflows/docker-build.yml | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 457e87bc..4be3ce9e 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -9,12 +9,20 @@ on: # Only activate for `master` branch branches: - master + - development # Plus for all tags tags: - - '*' + - "*" # Plus for any pull-requests pull_request: + branches: + - master + - development + + # And for any final releases + release: + types: [published] env: # Will be "NASA-AMMOS/MMGIS" for the main repo, for forks "user-name-of-fork/MMGIS" @@ -27,16 +35,17 @@ jobs: # for pull-requests: --, eg: 1.35.2-20210125-25 # for tags: # for `master` branch: latest,-latest,-latest,-latest,-- + # for `development` branch: development,-development,-- + # for releases: release,-release,-release,-release,-- # Version is parsed from package.json push: runs-on: ubuntu-latest - if: github.event_name == 'push' || github.event_name == 'pull_request' + if: github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'release' steps: - name: Checkout uses: actions/checkout@v3 - - - name: Generate tags + - name: Generate tags id: generate run: | @@ -71,12 +80,16 @@ jobs: # Use Docker `latest` tag convention if it's a master branch build [ "$VERSION" == "master" ] && VERSION=latest + [ "$VERSION" == "development" ] && VERSION=development + [ "${{ github.event_name }}" == "release" ] && VERSION=release # Compose REGISTRY_TAGS variable REGISTRY_TAGS="-t $IMAGE_ID:$VERSION" # For master branch also supply an extra tag: -latest,-latest,-latest,-- [ "$VERSION" == "latest" ] && REGISTRY_TAGS="$REGISTRY_TAGS -t $IMAGE_ID:$PATCH_VERSION-latest -t $IMAGE_ID:$MINOR_VERSION-latest -t $IMAGE_ID:$MAJOR_VERSION-latest -t $IMAGE_ID:$PATCH_VERSION-$BDATE-$(git rev-parse --short HEAD)" + [ "$VERSION" == "development" ] && REGISTRY_TAGS="$REGISTRY_TAGS -t $IMAGE_ID:$MAJOR_VERSION-development -t $IMAGE_ID:$PATCH_VERSION-$BDATE-$(git rev-parse --short HEAD)" + [ "$VERSION" == "release" ] && REGISTRY_TAGS="$REGISTRY_TAGS -t $IMAGE_ID:$PATCH_VERSION-release -t $IMAGE_ID:$MINOR_VERSION-release -t $IMAGE_ID:$MAJOR_VERSION-release -t $IMAGE_ID:$PATCH_VERSION-$BDATE-$(git rev-parse --short HEAD)" echo IMAGE_ID=$IMAGE_ID echo VERSION=$VERSION From dc4d7ff352c5e1ebc14b4c902ca4ba69dc11f87d Mon Sep 17 00:00:00 2001 From: Tariq Soliman Date: Tue, 21 Feb 2023 16:07:35 -0800 Subject: [PATCH 5/5] #336 Add GitHub Templates (#338) --- .github/ISSUE_TEMPLATE/bug_report.yml | 59 ++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.yml | 48 ++++++++++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 13 +++++ 3 files changed, 120 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..469e2741 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,59 @@ +name: Bug Report Form +description: Report a bug to help us improve +title: "[Bug]: " +labels: "bug" +body: + - type: markdown + attributes: + value: | + > _Thanks for filing a bug ticket. We appreciate your time and effort. Please answer a few questions._ + - type: dropdown + id: checked-for-duplicates + attributes: + label: Checked for duplicates + description: Have you checked for duplicate issue tickets? + multiple: false + options: + - "Yes - I've already checked" + - "No - I haven't checked" + validations: + required: yes + - type: textarea + id: description + attributes: + label: Describe the bug + description: A clear and concise description of what the bug is. Plain-text snippets preferred but screenshots welcome. + placeholder: Tell us what you saw + value: "When I did [...] action, I noticed [...]" + validations: + required: true + - type: textarea + id: expected-behavior + attributes: + label: What did you expect? + description: A clear and concise description of what you expect to happen + placeholder: Tell us what you expected + value: "I expected [...]" + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: Reproducible steps + description: "How would we reproduce this bug? Please walk us through it step by step. Plain-text snippets preferred but screenshots welcome." + value: | + 1. + 2. + 3. + ... + render: bash + - type: textarea + id: environment + attributes: + label: Environment + description: "What is your environment? Include any computer hardware, operating system, framework, browser, time-of-day or other contextual information related to your issue" + value: | + - Version of this software [e.g. vX.Y.Z] + - Operating System: [e.g. MacOSX with Docker Desktop vX.Y] + ... + render: bash diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..4a4c387e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,48 @@ +name: New Feature Form +description: Suggest a new feature for us to implement +title: "[New Feature]: " +labels: "enhancement" +body: + - type: markdown + attributes: + value: | + > _Thanks for filing a new feature request. We appreciate your time and effort. Please answer a few questions._ + - type: dropdown + id: checked-for-duplicates + attributes: + label: Checked for duplicates + description: Have you checked for duplicate issue tickets? + multiple: false + options: + - "Yes - I've already checked" + - "No - I haven't checked" + validations: + required: yes + - type: dropdown + id: checked-alternatives + attributes: + label: Alternatives considered + description: Have you considered alternative solutions to your feature request? + options: + - "Yes - and alternatives don't suffice" + - "No - I haven't considered" + validations: + required: yes + - type: textarea + id: related-problems + attributes: + label: Related problems + description: Is your feature request related to any problems? Please help us understand if so, including linking to any other issue tickets. + placeholder: Tell us the problems + value: "I'm frustrated when [...] happens as documented in issue-XYZ" + validations: + required: false + - type: textarea + id: description + attributes: + label: Describe the feature request + description: A clear and concise description of your request. + placeholder: Tell us what you want + value: "I need or want [...]" + validations: + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..161ffc86 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,13 @@ +## Purpose +- Clear, easy-to-understand sentences outlining the purpose of the PR +## Proposed Changes +- [ADD] ... +- [CHANGE] ... +- [FIX] ... +## Issues +- Links to relevant issues +- Example: issue-XYZ +## Testing +- Provide some proof you've tested your changes +- Example: test results available at ... +- Example: tested on operating system ... \ No newline at end of file