From 5554fd23fff07424da27f607aab1407366640be4 Mon Sep 17 00:00:00 2001 From: Tariq Soliman Date: Mon, 6 Jul 2020 18:36:43 -0700 Subject: [PATCH] MMGIS-1.3.2 --- API/Backend/Config/routes/configs.js | 896 +- API/Backend/Draw/routes/draw.js | 2994 +++---- API/Backend/Draw/routes/files.js | 3316 ++++--- API/templates/config_template.js | 149 +- README.md | 2 +- auxiliary/populateMosaics/populateMosaics.js | 63 + config/configconfig.json | 151 + config/css/config.css | 1 + config/js/config.js | 677 +- css/mmgis.css | 1432 +-- css/mmgisUI.css | 9 +- docs/docs.js | 336 +- docs/images/atlas_wget_script.bat | 0 docs/images/draw_panel1.jpg | Bin 0 -> 53681 bytes docs/images/draw_panel2.jpg | Bin 0 -> 47988 bytes docs/images/draw_panel3.jpg | Bin 0 -> 59585 bytes docs/pages/markdowns/Draw.md | 17 + docs/pages/markdowns/Layers_Tab.md | 901 +- docs/pages/markdowns/Viewshed.md | 80 + package.json | 2 +- .../Test/Layers/Waypoints/waypoints.json | 75 +- scripts/configure.js | 452 +- scripts/essence/Ancillary/DataShaders.js | 13 +- scripts/essence/Ancillary/Description.js | 463 +- scripts/essence/Ancillary/Login/Login.js | 1064 ++- scripts/essence/Ancillary/QueryURL.js | 777 +- scripts/essence/Ancillary/Search.js | 1028 +-- scripts/essence/Basics/Formulae_/Formulae_.js | 3456 ++++---- .../Globe_/Addons/Globe_VectorsAsTiles.js | 698 +- .../Basics/Globe_/Addons/Globe_Walk.js | 828 +- scripts/essence/Basics/Globe_/Cameras.js | 760 +- scripts/essence/Basics/Globe_/Globe_.js | 7735 +++++++++-------- scripts/essence/Basics/Globe_/projection.js | 53 +- scripts/essence/Basics/Layers_/Layers_.js | 1568 ++-- scripts/essence/Basics/Map_/Map_.js | 2853 +++--- scripts/essence/Basics/Test_/Test_.js | 420 +- .../Basics/UserInterface_/UserInterface_.js | 3755 ++++---- scripts/essence/Tools/Draw/DrawTool.css | 5124 +++++------ scripts/essence/Tools/Draw/DrawTool.js | 2604 +++--- scripts/essence/Tools/Draw/DrawTool.test.js | 4296 ++++----- .../essence/Tools/Draw/DrawTool_Drawing.js | 3162 +++---- scripts/essence/Tools/Draw/DrawTool_Files.js | 2519 +++--- .../essence/Tools/Draw/DrawTool_History.js | 280 +- scripts/essence/Tools/Draw/DrawTool_Shapes.js | 1327 +-- scripts/essence/Tools/Info/InfoTool.css | 16 + scripts/essence/Tools/Info/InfoTool.js | 47 +- scripts/essence/essence.js | 751 +- scripts/external/Arc/arc.js | 302 + .../external/Leaflet/leaflet.tilelayer.gl.js | 76 +- views/index.pug | 106 +- views/login.pug | 53 +- 51 files changed, 29493 insertions(+), 28194 deletions(-) create mode 100644 auxiliary/populateMosaics/populateMosaics.js create mode 100644 config/configconfig.json create mode 100644 docs/images/atlas_wget_script.bat create mode 100644 docs/images/draw_panel1.jpg create mode 100644 docs/images/draw_panel2.jpg create mode 100644 docs/images/draw_panel3.jpg create mode 100644 docs/pages/markdowns/Viewshed.md create mode 100644 scripts/external/Arc/arc.js diff --git a/API/Backend/Config/routes/configs.js b/API/Backend/Config/routes/configs.js index d7621b21..3e2bd41d 100644 --- a/API/Backend/Config/routes/configs.js +++ b/API/Backend/Config/routes/configs.js @@ -1,448 +1,448 @@ -/*********************************************************** - * JavaScript syntax format: ES5/ES6 - ECMAScript 2015 - * Loading all required dependencies, libraries and packages - **********************************************************/ -const express = require("express"); -const router = express.Router(); -const execFile = require("child_process").execFile; - -const logger = require("../../../logger"); -const Config = require("../models/config"); -const config_template = require("../../../templates/config_template"); - -const fs = require("fs"); - -function get(req, res, next, cb) { - Config.findAll({ - where: { - mission: req.query.mission - } - }) - .then(missions => { - let maxVersion = -Infinity; - if (missions && missions.length > 0) { - for (let i = 0; i < missions.length; i++) { - maxVersion = Math.max(missions[i].version, maxVersion); - } - return maxVersion; - } else return 0; - }) - .then(version => { - if (req.query.version) version = req.query.version; - - if (version < 0) { - //mission doesn't exist - if (cb) cb({ status: "failure", message: "Mission not found." }); - else res.send({ status: "failure", message: "Mission not found." }); - return null; - } else { - Config.findOne({ - where: { - mission: req.query.mission, - version: version - } - }) - .then(mission => { - if (req.query.full) { - if (cb) - cb({ - status: "success", - mission: mission.mission, - config: mission.config, - version: mission.version - }); - else - res.send({ - status: "success", - mission: mission.mission, - config: mission.config, - version: mission.version - }); - } else res.send(mission.config); - return null; - }) - .catch(err => { - if (cb) cb({ status: "failure", message: "Mission not found." }); - else res.send({ status: "failure", message: "Mission not found." }); - return null; - }); - } - return null; - }) - .catch(err => { - if (cb) cb({ status: "failure", message: "Mission not found." }); - else res.send({ status: "failure", message: "Mission not found." }); - return null; - }); - return null; -} -router.get("/get", function(req, res, next) { - get(req, res, next); -}); - -function add(req, res, next, cb) { - let configTemplate = JSON.parse(JSON.stringify(config_template)); - configTemplate = req.body.config || configTemplate; - configTemplate.msv.mission = req.body.mission; - - if ( - req.body.mission !== - req.body.mission.replace( - /[`~!@#$%^&*()|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, - "" - ) && - req.body.mission.length === 0 && - !isNaN(req.body.mission[0]) - ) { - logger("error", "Attempted to add bad mission name.", req.originalUrl, req); - res.send({ status: "failure", message: "Bad mission name." }); - return; - } - - let newConfig = { - mission: req.body.mission, - config: configTemplate, - version: 0 - }; - - //Make sure the mission doesn't already exist - Config.findOne({ - where: { - mission: req.body.mission - } - }) - .then(mission => { - if (!mission) { - Config.create(newConfig) - .then(created => { - if (req.body.makedir === "true") { - let dir = "./Missions/" + created.mission; - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir); - let dir2 = dir + "/Layers"; - if (!fs.existsSync(dir2)) { - fs.mkdirSync(dir2); - } - let dir3 = dir + "/Data"; - if (!fs.existsSync(dir3)) { - fs.mkdirSync(dir3); - } - } - } - - logger( - "info", - "Successfully created mission: " + created.mission, - req.originalUrl, - req - ); - if (cb) - cb({ - status: "success", - mission: created.mission, - version: created.version - }); - else - res.send({ - status: "success", - mission: created.mission, - version: created.version - }); - return null; - }) - .catch(err => { - logger( - "error", - "Failed to create new mission.", - req.originalUrl, - req, - err - ); - if (cb) - cb({ - status: "failure", - message: "Failed to create new mission." - }); - else - res.send({ - status: "failure", - message: "Failed to create new mission." - }); - return null; - }); - } else { - logger("error", "Mission already exists.", req.originalUrl, req); - if (cb) cb({ status: "failure", message: "Mission already exists." }); - else - res.send({ status: "failure", message: "Mission already exists." }); - } - return null; - }) - .catch(err => { - logger( - "error", - "Failed to check if mission already exists.", - req.originalUrl, - req, - err - ); - if (cb) - cb({ - status: "failure", - message: "Failed to check if mission already exists." - }); - else - res.send({ - status: "failure", - message: "Failed to check if mission already exists." - }); - return null; - }); - return null; -} -router.post("/add", function(req, res, next) { - add(req, res, next); -}); - -function upsert(req, res, next, cb) { - let hasVersion = false; - if (req.body.version != null) hasVersion = true; - let versionConfig = null; - - Config.findAll({ - where: { - mission: req.body.mission - } - }) - .then(missions => { - let maxVersion = -Infinity; - if (missions && missions.length > 0) { - for (let i = 0; i < missions.length; i++) { - maxVersion = Math.max(missions[i].version, maxVersion); - if (hasVersion && missions[i].version == req.body.version) - versionConfig = missions[i].config; - } - return maxVersion; - } else return -1; //will get incremented to 0 - }) - .then(version => { - Config.create({ - mission: req.body.mission, - config: versionConfig || JSON.parse(req.body.config), - version: version + 1 - }) - .then(created => { - logger( - "info", - "Successfully updated mission: " + - created.mission + - " v" + - created.version, - req.originalUrl, - req - ); - if (cb) - cb({ - status: "success", - mission: created.mission, - version: created.version - }); - else - res.send({ - status: "success", - mission: created.mission, - version: created.version - }); - return null; - }) - .catch(err => { - logger( - "error", - "Failed to update mission.", - req.originalUrl, - req, - err - ); - if (cb) - cb({ status: "failure", message: "Failed to update mission." }); - else - res.send({ - status: "failure", - message: "Failed to update mission." - }); - return null; - }); - return null; - }) - .catch(err => { - logger("error", "Failed to update mission.", req.originalUrl, req, err); - if (cb) cb({ status: "failure", message: "Failed to update mission." }); - else res.send({ status: "failure", message: "Failed to find mission." }); - return null; - }); - return null; -} -router.post("/upsert", function(req, res, next) { - upsert(req, res, next); -}); - -router.post("/missions", function(req, res, next) { - Config.aggregate("mission", "DISTINCT", { plain: false }) - .then(missions => { - let allMissions = []; - for (let i = 0; i < missions.length; i++) - allMissions.push(missions[i].DISTINCT); - allMissions.sort(); - res.send({ status: "success", missions: allMissions }); - return null; - }) - .catch(err => { - logger("error", "Failed to find missions.", req.originalUrl, req, err); - res.send({ status: "failure", message: "Failed to find missions." }); - return null; - }); - return null; -}); - -router.post("/versions", function(req, res, next) { - Config.findAll({ - where: { - mission: req.body.mission - }, - attributes: ["mission", "version", "createdAt"] - }) - .then(missions => { - res.send({ status: "success", versions: missions }); - return null; - }) - .catch(err => { - logger("error", "Failed to find versions.", req.originalUrl, req, err); - res.send({ status: "failure", message: "Failed to find versions." }); - return null; - }); - return null; -}); - -function relativizePaths(config, mission) { - let relConfig = JSON.parse(JSON.stringify(config)); - - setAllKeys(relConfig, "../" + mission + "/"); - - function setAllKeys(data, prepend) { - if (typeof data === "object" && data !== null) { - for (let k in data) { - if (typeof data[k] === "object" && data[k] !== null) - setAllKeys(data[k], prepend); - else if (Array.isArray(data[k])) setAllKeys(data[k], prepend); - else if (k == "url" || k == "demtileurl" || k == "legend") - if (data[k].indexOf("://") == -1) data[k] = prepend + "" + data[k]; - } - } else if (Array.isArray(data)) { - for (let i = 0; i < data.length; i++) { - if (typeof data[i] === "object" && data[i] !== null) - setAllKeys(data[i], prepend); - else if (Array.isArray(data[i])) setAllKeys(data[i], prepend); - } - } - } - - return relConfig; -} - -//existingMission -//cloneMission -//hasPaths -router.post("/clone", function(req, res, next) { - req.query.full = true; - req.query.mission = req.body.existingMission; - - get(req, res, next, function(r) { - if (r.status == "success") { - r.config.msv.mission = req.body.cloneMission; - req.body.config = - req.body.hasPaths == "true" - ? relativizePaths(r.config, req.body.existingMission) - : r.config; - req.body.mission = req.body.cloneMission; - execFile( - "php", - [ - "private/api/create_mission.php", - encodeURIComponent(req.body.cloneMission) - ], - function(error, stdout, stderr) { - stdout = JSON.parse(stdout); - if (stdout.status == "success") { - add(req, res, next, function(r2) { - if (r2.status == "success") { - res.send(r2); - } else { - res.send(r2); - } - }); - } else res.send(stdout); - } - ); - } else { - res.send(r); - } - }); -}); - -router.post("/rename", function(req, res, next) {}); -router.post("/destroy", function(req, res, next) { - Config.destroy({ - where: { - mission: req.body.mission - } - }) - .then(mission => { - logger( - "info", - "Deleted Mission: " + req.body.mission, - req.originalUrl, - req - ); - - const dir = "./Missions/" + req.body.mission; - if (fs.existsSync(dir)) { - fs.rename(dir, dir + "_deleted_", err => { - if (err) - res.send({ - status: "success", - message: - "Successfully Deleted Mission: " + - req.body.mission + - " but couldn't rename its Missions directory." - }); - else - res.send({ - status: "success", - message: "Successfully Deleted Mission: " + req.body.mission - }); - }); - } else { - res.send({ - status: "success", - message: "Successfully Deleted Mission: " + req.body.mission - }); - } - }) - .catch(err => { - logger( - "error", - "Failed to delete mission: " + req.body.mission, - req.originalUrl, - req, - err - ); - res.send({ - status: "failure", - message: "Failed to delete mission " + req.body.mission + "." - }); - return null; - }); - return null; -}); - -module.exports = router; +/*********************************************************** + * JavaScript syntax format: ES5/ES6 - ECMAScript 2015 + * Loading all required dependencies, libraries and packages + **********************************************************/ +const express = require("express"); +const router = express.Router(); +const execFile = require("child_process").execFile; + +const logger = require("../../../logger"); +const Config = require("../models/config"); +const config_template = require("../../../templates/config_template"); + +const fs = require("fs"); + +function get(req, res, next, cb) { + Config.findAll({ + where: { + mission: req.query.mission, + }, + }) + .then((missions) => { + let maxVersion = -Infinity; + if (missions && missions.length > 0) { + for (let i = 0; i < missions.length; i++) { + maxVersion = Math.max(missions[i].version, maxVersion); + } + return maxVersion; + } else return 0; + }) + .then((version) => { + if (req.query.version) version = req.query.version; + + if (version < 0) { + //mission doesn't exist + if (cb) cb({ status: "failure", message: "Mission not found." }); + else res.send({ status: "failure", message: "Mission not found." }); + return null; + } else { + Config.findOne({ + where: { + mission: req.query.mission, + version: version, + }, + }) + .then((mission) => { + if (req.query.full) { + if (cb) + cb({ + status: "success", + mission: mission.mission, + config: mission.config, + version: mission.version, + }); + else + res.send({ + status: "success", + mission: mission.mission, + config: mission.config, + version: mission.version, + }); + } else res.send(mission.config); + return null; + }) + .catch((err) => { + if (cb) cb({ status: "failure", message: "Mission not found." }); + else res.send({ status: "failure", message: "Mission not found." }); + return null; + }); + } + return null; + }) + .catch((err) => { + if (cb) cb({ status: "failure", message: "Mission not found." }); + else res.send({ status: "failure", message: "Mission not found." }); + return null; + }); + return null; +} +router.get("/get", function (req, res, next) { + get(req, res, next); +}); + +function add(req, res, next, cb) { + let configTemplate = JSON.parse(JSON.stringify(config_template)); + configTemplate = req.body.config || configTemplate; + configTemplate.msv.mission = req.body.mission; + + if ( + req.body.mission !== + req.body.mission.replace( + /[`~!@#$%^&*()|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, + "" + ) && + req.body.mission.length === 0 && + !isNaN(req.body.mission[0]) + ) { + logger("error", "Attempted to add bad mission name.", req.originalUrl, req); + res.send({ status: "failure", message: "Bad mission name." }); + return; + } + + let newConfig = { + mission: req.body.mission, + config: configTemplate, + version: 0, + }; + + //Make sure the mission doesn't already exist + Config.findOne({ + where: { + mission: req.body.mission, + }, + }) + .then((mission) => { + if (!mission) { + Config.create(newConfig) + .then((created) => { + if (req.body.makedir === "true") { + let dir = "./Missions/" + created.mission; + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir); + let dir2 = dir + "/Layers"; + if (!fs.existsSync(dir2)) { + fs.mkdirSync(dir2); + } + let dir3 = dir + "/Data"; + if (!fs.existsSync(dir3)) { + fs.mkdirSync(dir3); + } + } + } + + logger( + "info", + "Successfully created mission: " + created.mission, + req.originalUrl, + req + ); + if (cb) + cb({ + status: "success", + mission: created.mission, + version: created.version, + }); + else + res.send({ + status: "success", + mission: created.mission, + version: created.version, + }); + return null; + }) + .catch((err) => { + logger( + "error", + "Failed to create new mission.", + req.originalUrl, + req, + err + ); + if (cb) + cb({ + status: "failure", + message: "Failed to create new mission.", + }); + else + res.send({ + status: "failure", + message: "Failed to create new mission.", + }); + return null; + }); + } else { + logger("error", "Mission already exists.", req.originalUrl, req); + if (cb) cb({ status: "failure", message: "Mission already exists." }); + else + res.send({ status: "failure", message: "Mission already exists." }); + } + return null; + }) + .catch((err) => { + logger( + "error", + "Failed to check if mission already exists.", + req.originalUrl, + req, + err + ); + if (cb) + cb({ + status: "failure", + message: "Failed to check if mission already exists.", + }); + else + res.send({ + status: "failure", + message: "Failed to check if mission already exists.", + }); + return null; + }); + return null; +} +router.post("/add", function (req, res, next) { + add(req, res, next); +}); + +function upsert(req, res, next, cb) { + let hasVersion = false; + if (req.body.version != null) hasVersion = true; + let versionConfig = null; + + Config.findAll({ + where: { + mission: req.body.mission, + }, + }) + .then((missions) => { + let maxVersion = -Infinity; + if (missions && missions.length > 0) { + for (let i = 0; i < missions.length; i++) { + maxVersion = Math.max(missions[i].version, maxVersion); + if (hasVersion && missions[i].version == req.body.version) + versionConfig = missions[i].config; + } + return maxVersion; + } else return -1; //will get incremented to 0 + }) + .then((version) => { + Config.create({ + mission: req.body.mission, + config: versionConfig || JSON.parse(req.body.config), + version: version + 1, + }) + .then((created) => { + logger( + "info", + "Successfully updated mission: " + + created.mission + + " v" + + created.version, + req.originalUrl, + req + ); + if (cb) + cb({ + status: "success", + mission: created.mission, + version: created.version, + }); + else + res.send({ + status: "success", + mission: created.mission, + version: created.version, + }); + return null; + }) + .catch((err) => { + logger( + "error", + "Failed to update mission.", + req.originalUrl, + req, + err + ); + if (cb) + cb({ status: "failure", message: "Failed to update mission." }); + else + res.send({ + status: "failure", + message: "Failed to update mission.", + }); + return null; + }); + return null; + }) + .catch((err) => { + logger("error", "Failed to update mission.", req.originalUrl, req, err); + if (cb) cb({ status: "failure", message: "Failed to update mission." }); + else res.send({ status: "failure", message: "Failed to find mission." }); + return null; + }); + return null; +} +router.post("/upsert", function (req, res, next) { + upsert(req, res, next); +}); + +router.post("/missions", function (req, res, next) { + Config.aggregate("mission", "DISTINCT", { plain: false }) + .then((missions) => { + let allMissions = []; + for (let i = 0; i < missions.length; i++) + allMissions.push(missions[i].DISTINCT); + allMissions.sort(); + res.send({ status: "success", missions: allMissions }); + return null; + }) + .catch((err) => { + logger("error", "Failed to find missions.", req.originalUrl, req, err); + res.send({ status: "failure", message: "Failed to find missions." }); + return null; + }); + return null; +}); + +router.post("/versions", function (req, res, next) { + Config.findAll({ + where: { + mission: req.body.mission, + }, + attributes: ["mission", "version", "createdAt"], + }) + .then((missions) => { + res.send({ status: "success", versions: missions }); + return null; + }) + .catch((err) => { + logger("error", "Failed to find versions.", req.originalUrl, req, err); + res.send({ status: "failure", message: "Failed to find versions." }); + return null; + }); + return null; +}); + +function relativizePaths(config, mission) { + let relConfig = JSON.parse(JSON.stringify(config)); + + setAllKeys(relConfig, "../" + mission + "/"); + + function setAllKeys(data, prepend) { + if (typeof data === "object" && data !== null) { + for (let k in data) { + if (typeof data[k] === "object" && data[k] !== null) + setAllKeys(data[k], prepend); + else if (Array.isArray(data[k])) setAllKeys(data[k], prepend); + else if (k == "url" || k == "demtileurl" || k == "legend") + if (data[k].indexOf("://") == -1) data[k] = prepend + "" + data[k]; + } + } else if (Array.isArray(data)) { + for (let i = 0; i < data.length; i++) { + if (typeof data[i] === "object" && data[i] !== null) + setAllKeys(data[i], prepend); + else if (Array.isArray(data[i])) setAllKeys(data[i], prepend); + } + } + } + + return relConfig; +} + +//existingMission +//cloneMission +//hasPaths +router.post("/clone", function (req, res, next) { + req.query.full = true; + req.query.mission = req.body.existingMission; + + get(req, res, next, function (r) { + if (r.status == "success") { + r.config.msv.mission = req.body.cloneMission; + req.body.config = + req.body.hasPaths == "true" + ? relativizePaths(r.config, req.body.existingMission) + : r.config; + req.body.mission = req.body.cloneMission; + execFile( + "php", + [ + "private/api/create_mission.php", + encodeURIComponent(req.body.cloneMission), + ], + function (error, stdout, stderr) { + stdout = JSON.parse(stdout); + if (stdout.status == "success") { + add(req, res, next, function (r2) { + if (r2.status == "success") { + res.send(r2); + } else { + res.send(r2); + } + }); + } else res.send(stdout); + } + ); + } else { + res.send(r); + } + }); +}); + +router.post("/rename", function (req, res, next) {}); +router.post("/destroy", function (req, res, next) { + Config.destroy({ + where: { + mission: req.body.mission, + }, + }) + .then((mission) => { + logger( + "info", + "Deleted Mission: " + req.body.mission, + req.originalUrl, + req + ); + + const dir = "./Missions/" + req.body.mission; + if (fs.existsSync(dir)) { + fs.rename(dir, dir + "_deleted_", (err) => { + if (err) + res.send({ + status: "success", + message: + "Successfully Deleted Mission: " + + req.body.mission + + " but couldn't rename its Missions directory.", + }); + else + res.send({ + status: "success", + message: "Successfully Deleted Mission: " + req.body.mission, + }); + }); + } else { + res.send({ + status: "success", + message: "Successfully Deleted Mission: " + req.body.mission, + }); + } + }) + .catch((err) => { + logger( + "error", + "Failed to delete mission: " + req.body.mission, + req.originalUrl, + req, + err + ); + res.send({ + status: "failure", + message: "Failed to delete mission " + req.body.mission + ".", + }); + return null; + }); + return null; +}); + +module.exports = router; diff --git a/API/Backend/Draw/routes/draw.js b/API/Backend/Draw/routes/draw.js index 4c3c86d3..134694fe 100644 --- a/API/Backend/Draw/routes/draw.js +++ b/API/Backend/Draw/routes/draw.js @@ -1,1497 +1,1497 @@ -const express = require("express"); -const logger = require("../../../logger"); -const database = require("../../../database"); -const Sequelize = require("sequelize"); -const uuidv4 = require("uuid/v4"); -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; -const uf = require("../models/userfeatures"); -const Userfeatures = uf.Userfeatures; -const UserfeaturesTEST = uf.UserfeaturesTEST; -const { sequelize } = require("../../../connection"); - -const router = express.Router(); -const db = database.db; - -router.post("/", function(req, res, next) { - res.send("test draw"); -}); - -/** - * Crops out duplicate array elements between arrays - * Ex. - * arr1=['a','b'], arr2=['b'] -> ['a'] - * - * @param {[]} arr1 - * @param {[]} arr2 - * @return {[]} arr1 without any elements of arr2 - */ -const uniqueAcrossArrays = (arr1, arr2) => { - let uniqueArr = Object.assign([], arr1); - for (let i = uniqueArr.length - 1; i >= 0; i--) { - if (arr2.indexOf(arr1[i]) != -1) uniqueArr.splice(i, 1); - } - - return uniqueArr; -}; - -const pushToHistory = ( - Table, - file_id, - feature_id, - feature_idRemove, - time, - undoToTime, - action_index, - successCallback, - failureCallback -) => { - Table.findAll({ - where: { - file_id: file_id - } - }) - .then(histories => { - let maxHistoryId = -Infinity; - if (histories && histories.length > 0) { - for (let i = 0; i < histories.length; i++) { - maxHistoryId = Math.max(histories[i].history_id, maxHistoryId); - } - return { - historyIndex: maxHistoryId + 1, - history: histories[maxHistoryId].history - }; - } else return { historyIndex: 0, history: [] }; - }) - .then(historyObj => { - getNextHistory( - Table, - historyObj.history, - action_index, - feature_id, - feature_idRemove, - file_id, - undoToTime, - h => { - let newHistoryEntry = { - file_id: file_id, - history_id: historyObj.historyIndex, - time: time, - action_index: action_index, - history: h - }; - // Insert new entry into the history table - Table.create(newHistoryEntry) - .then(created => { - successCallback(); - return null; - }) - .catch(err => { - failureCallback(err); - }); - }, - err => { - failureCallback(err); - } - ); - return null; - }); -}; - -const getNextHistory = ( - Table, - history, - action_index, - feature_idAdd, - feature_idRemove, - file_id, - undoToTime, - successCallback, - failureCallback -) => { - switch (action_index) { - case 0: //add - history.push(feature_idAdd); - if (Array.isArray(feature_idAdd)) history = feature_idAdd; - successCallback(history); - return; - case 1: //edit - history.splice(history.indexOf(parseInt(feature_idRemove)), 1); - history.push(feature_idAdd); - successCallback(history); - return; - case 2: //delete - history.splice(history.indexOf(parseInt(feature_idRemove)), 1); - successCallback(history); - return; - case 3: //undo - //Here we do want to use the last history, we want to use the history at undo to time - Table.findOne({ - where: { - file_id: file_id, - time: undoToTime - } - }) - .then(history => { - successCallback(history.history); - return null; - }) - .catch(err => { - failureCallback(err); - return null; - }); - break; - case 5: //Clip add over - case 6: //Merge add array of add ids and remove array of remove ids - case 7: //Clip add under - case 8: //Split - //add - history = history.concat(feature_idAdd); - //remove - history = uniqueAcrossArrays(history, feature_idRemove); - successCallback(history); - return; - default: - failureCallback("Unknown action_index: " + action_index); - } -}; - -/** - * - * @param {number} file_id - * @param {number} added_id - */ -const clipOver = function( - req, - res, - file_id, - added_id, - time, - successCallback, - failureCallback -) { - let Histories = req.body.test === "true" ? FilehistoriesTEST : Filehistories; - - //CLIP OVER - Histories.findAll({ - where: { - file_id: file_id - } - }) - .then(histories => { - let maxHistoryId = -Infinity; - if (histories && histories.length > 0) { - for (let i = 0; i < histories.length; i++) { - maxHistoryId = Math.max(histories[i].history_id, maxHistoryId); - } - return { - historyIndex: maxHistoryId + 1, - history: histories[maxHistoryId].history - }; - } else return { historyIndex: 0, history: [] }; - }) - .then(historyObj => { - let history = historyObj.history; - history = history.join(","); - history = history || "NULL"; - //RETURN ALL THE CHANGED SHAPE IDs AND GEOMETRIES - let q = [ - "SELECT clipped.id, ST_AsGeoJSON( (ST_Dump(clipped.newgeom)).geom ) AS newgeom FROM", - "(", - "SELECT data.id, data.newgeom", - "FROM (", - "SELECT r.id, ST_DIFFERENCE(ST_MakeValid(r.geom),", - "ST_MakeValid((", - "SELECT a.geom", - "FROM user_features" + - (req.body.test === "true" ? "_tests" : "") + - " AS a", - "WHERE a.id = :added_id AND ST_INTERSECTS(a.geom, r.geom)", - "))", - ") AS newgeom", - "FROM user_features" + - (req.body.test === "true" ? "_tests" : "") + - " AS r", - "WHERE r.file_id = :file_id AND r.id != :added_id AND r.id IN (" + - history + - ")", - ") data", - "WHERE data.newgeom IS NOT NULL", - ") AS clipped" - ].join(" "); - sequelize - .query(q, { - replacements: { - file_id: file_id, - added_id: added_id - } - }) - .spread(results => { - let oldIds = []; - let newIds = [added_id]; - - editLoop(0); - function editLoop(i) { - if (i >= results.length) { - pushToHistory( - Histories, - req.body.file_id, - newIds, - oldIds, - time, - null, - 5, - () => { - if (typeof successCallback === "function") successCallback(); - }, - err => { - if (typeof failureCallback === "function") - failureCallback(err); - }, - "addandremove" - ); - return; - } - let newReq = Object.assign({}, req); - results[i].newgeom.crs = { - type: "name", - properties: { name: "EPSG:4326" } - }; - newReq.body = { - file_id: file_id, - feature_id: results[i].id, - geometry: results[i].newgeom, - to_history: false, - test: req.body.test - }; - - if (oldIds.indexOf(results[i].id) == -1) oldIds.push(results[i].id); - edit( - newReq, - res, - newId => { - newIds.push(newId); - editLoop(i + 1); - }, - () => { - editLoop(i + 1); - } - ); - } - - return null; - }) - .catch(err => { - failureCallback(err); - }); - - return null; - }) - .catch(err => { - failureCallback(err); - }); -}; - -const clipUnder = function( - req, - res, - newFeature, - time, - successCallback, - failureCallback -) { - let Features = req.body.test === "true" ? UserfeaturesTEST : Userfeatures; - let Histories = req.body.test === "true" ? FilehistoriesTEST : Filehistories; - - Histories.findAll({ - where: { - file_id: newFeature.file_id - } - }) - .then(histories => { - let maxHistoryId = -Infinity; - if (histories && histories.length > 0) { - for (let i = 0; i < histories.length; i++) { - maxHistoryId = Math.max(histories[i].history_id, maxHistoryId); - } - return { - historyIndex: maxHistoryId + 1, - history: histories[maxHistoryId].history - }; - } else return { historyIndex: 0, history: [] }; - }) - .then(historyObj => { - let history = historyObj.history; - history = history.join(","); - history = history || "NULL"; - - //Continually clip the added feature with the other features of the file - let q = [ - "WITH RECURSIVE clipper (n, clippedgeom) AS (", - "SELECT 0 n, ST_MakeValid(ST_GeomFromGeoJSON(:geom)) clippedgeom", - "UNION ALL", - "SELECT n+1, ST_DIFFERENCE(", - "clippedgeom,", - "(", - "SELECT ST_BUFFER(", - "ST_UNION(", - "ARRAY((", - "SELECT ST_BUFFER(a.geom, 0.00001, 'join=mitre')", - "FROM user_features" + - (req.body.test === "true" ? "_tests" : "") + - " AS a", - "WHERE a.id IN (" + - history + - ") AND ST_INTERSECTS(a.geom, clippedgeom)", - "))", - "),", - "-0.00001,'join=mitre')", - ")", - ")", - "FROM clipper", - "WHERE n < 1", - ")", - "SELECT ST_AsGeoJSON( (ST_Dump(clipped.clippedgeom)).geom ) as geom FROM", - "(", - "SELECT c.n, c.clippedgeom as clippedgeom FROM clipper c", - "WHERE c.clippedgeom IS NOT NULL", - "ORDER by c.n DESC LIMIT 1", - ") AS clipped" - ].join(" "); - - sequelize - .query(q, { - replacements: { - geom: JSON.stringify(newFeature.geom) - } - }) - .spread(results => { - let oldIds = []; - let newIds = []; - - addLoop(0); - function addLoop(i) { - if (i >= results.length) { - pushToHistory( - Histories, - req.body.file_id, - newIds, - oldIds, - time, - null, - 7, - () => { - if (typeof successCallback === "function") successCallback(); - }, - err => { - if (typeof failureCallback === "function") - failureCallback(err); - } - ); - return null; - } - let clippedFeature = Object.assign({}, newFeature); - clippedFeature.properties = JSON.parse(newFeature.properties); - clippedFeature.geom = JSON.parse(results[i].geom); - clippedFeature.geom.crs = { - type: "name", - properties: { name: "EPSG:4326" } - }; - clippedFeature.properties.uuid = uuidv4(); - clippedFeature.properties = JSON.stringify( - clippedFeature.properties - ); - - Features.create(clippedFeature) - .then(created => { - newIds.push(created.id); - //now update the - addLoop(i + 1); - return null; - }) - .catch(err => { - addLoop(i + 1); - return null; - //failureCallback(); - }); - } - - return null; - }) - .catch(err => { - failureCallback(err); - }); - - return null; - }) - .catch(err => { - failureCallback(err); - }); -}; -/** - * Adds a feature - * { - * file_id: (required) - * parent: (optional) - * order: <'min' || 'max' || int> (optional) - * 'min' and < 0 adds behind all features - * keywords: (optional) - * intent: (optional) - * properties: (optional) - * geometry: (required) - * } - */ -const add = function( - req, - res, - successCallback, - failureCallback1, - failureCallback2 -) { - let Files = req.body.test === "true" ? UserfilesTEST : Userfiles; - let Features = req.body.test === "true" ? UserfeaturesTEST : Userfeatures; - let Histories = req.body.test === "true" ? FilehistoriesTEST : Filehistories; - - let time = Math.floor(Date.now()); - - let groups = []; - if (req.groups) groups = Object.keys(req.groups); - - if (req.body.to_history == null) req.body.to_history = true; - - //Check that the provided file_id is an id that belongs to the current user - Files.findOne({ - where: { - id: req.body.file_id, - [Sequelize.Op.or]: { - file_owner: req.user, - [Sequelize.Op.and]: { - file_owner: "group", - file_owner_group: { [Sequelize.Op.overlap]: groups } - } - } - } - }).then(file => { - if (!file) { - if (typeof failureCallback1 === "function") failureCallback1(); - } else { - //Find the next level - let order = "max"; - if (req.body.order == "min" || req.body.order < 0) order = "min"; - - Features.findAll({ - where: { - file_id: req.body.file_id - } - }) - .then(features => { - let maxLevel = -Infinity; - let minLevel = Infinity; - if (features && features.length > 0) { - for (let i = 0; i < features.length; i++) { - maxLevel = Math.max(features[i].level, maxLevel); - minLevel = Math.min(features[i].level, minLevel); - } - if (order === "max") return maxLevel + 1; - else return minLevel - 1; - } else return 0; - }) - .then(level => { - let properties = req.body.properties || {}; - //Remove _ from properties if it has it. This is because the server returns metadata - // under _ and we don't want it to potentially nest - delete properties["_"]; - - let geom = JSON.parse(req.body.geometry); - //Geometry needs this for the spatialiness to work - geom.crs = { type: "name", properties: { name: "EPSG:4326" } }; - - let newFeature = { - file_id: req.body.file_id, - level: level, - intent: req.body.intent, - elevated: "0", - properties: properties, - geom: geom - }; - - if (req.body.clip === "under") { - clipUnder( - req, - res, - newFeature, - time, - (createdId, createdIntent) => { - if (typeof successCallback === "function") - successCallback(createdId, createdIntent); - }, - err => { - if (typeof failureCallback2 === "function") - failureCallback2(err); - } - ); - } else { - newFeature.properties = JSON.parse(newFeature.properties); - newFeature.properties.uuid = uuidv4(); - newFeature.properties = JSON.stringify(newFeature.properties); - // Insert new feature into the feature table - Features.create(newFeature) - .then(created => { - if (req.body.to_history) { - let id = created.id; - if (req.body.bulk_ids != null) { - id = req.body.bulk_ids; - id.push(created.id); - } - if (req.body.clip === "over") { - clipOver( - req, - res, - newFeature.file_id, - id, - time, - () => { - if (typeof successCallback === "function") - successCallback(created.id, created.intent); - }, - err => { - if (typeof failureCallback2 === "function") - failureCallback2(err); - } - ); - } else { - pushToHistory( - Histories, - req.body.file_id, - id, - null, - time, - null, - 0, - () => { - if (typeof successCallback === "function") - successCallback(created.id, created.intent); - }, - err => { - if (typeof failureCallback2 === "function") - failureCallback2(err); - } - ); - } - } else { - if (typeof successCallback === "function") - successCallback(created.id, created.intent); - } - return null; - }) - .catch(err => { - if (typeof failureCallback2 === "function") - failureCallback2(err); - }); - } - }); - return null; - } - }); -}; -router.post("/add", function(req, res, next) { - add( - req, - res, - (id, intent) => { - logger("info", "Successfully added a new feature.", req.originalUrl, req); - res.send({ - status: "success", - message: "Successfully added a new feature.", - body: { id: id, intent: intent } - }); - }, - err => { - logger("error", "Failed to access file.", req.originalUrl, req, err); - res.send({ - status: "failure", - message: "Failed to access file.", - body: {} - }); - }, - err => { - logger("error", "Failed to add new feature.", req.originalUrl, req, err); - res.send({ - status: "failure", - message: "Failed to add new feature.", - body: {} - }); - } - ); -}); - -/** - * Edits a feature - * { - * file_id: (required) - * feature_id: (required) - * parent: (optional) - * keywords: (optional) - * intent: (optional) - * properties: (optional) - * geometry: (optional) - * } - */ -const edit = function(req, res, successCallback, failureCallback) { - let Files = req.body.test === "true" ? UserfilesTEST : Userfiles; - let Features = req.body.test === "true" ? UserfeaturesTEST : Userfeatures; - let Histories = req.body.test === "true" ? FilehistoriesTEST : Filehistories; - - let time = Math.floor(Date.now()); - - let groups = []; - if (req.groups) groups = Object.keys(req.groups); - - if (req.body.to_history == null) req.body.to_history = true; - - Files.findOne({ - where: { - id: req.body.file_id, - [Sequelize.Op.or]: { - file_owner: req.user, - [Sequelize.Op.and]: { - file_owner: "group", - file_owner_group: { [Sequelize.Op.overlap]: groups } - } - } - } - }) - .then(file => { - if (!file) { - failureCallback(); - } else { - Features.findOne({ - where: { - id: req.body.feature_id, - file_id: req.body.file_id - }, - attributes: { - include: [ - [ - Sequelize.fn("ST_AsGeoJSON", Sequelize.col("geom")), - "geojson_geom" - ] - ] - } - }) - .then(feature => { - if (!feature && !req.body.addIfNotFound) { - failureCallback(); - } else { - var newAttributes = feature.dataValues; - - delete newAttributes["id"]; - delete newAttributes.properties["_"]; - newAttributes.extant_start = time; - - if (req.body.hasOwnProperty("parent")) - newAttributes.parent = req.body.parent; - if (req.body.hasOwnProperty("keywords")) - newAttributes.keywords = req.body.keywords; - if (req.body.hasOwnProperty("intent")) - newAttributes.intent = req.body.intent; - if (req.body.hasOwnProperty("properties")) - newAttributes.properties = req.body.properties; - if (req.body.hasOwnProperty("geometry")) { - newAttributes.geom = JSON.parse(req.body.geometry); - } else { - newAttributes.geom = JSON.parse( - feature.dataValues.geojson_geom - ); - } - if ( - req.body.hasOwnProperty("reassignUUID") && - req.body.reassignUUID == "true" - ) { - newAttributes.properties = JSON.parse(newAttributes.properties); - newAttributes.properties.uuid = uuidv4(); - newAttributes.properties = JSON.stringify( - newAttributes.properties - ); - } - - newAttributes.geom.crs = { - type: "name", - properties: { name: "EPSG:4326" } - }; - - Features.create(newAttributes) - .then(created => { - let createdId = created.id; - let createdUUID = JSON.parse(created.properties).uuid; - let createdIntent = created.intent; - - if (req.body.to_history) { - pushToHistory( - Histories, - req.body.file_id, - created.id, - req.body.feature_id, - time, - null, - 1, - () => { - successCallback(createdId, createdUUID, createdIntent); - }, - err => { - failureCallback(err); - } - ); - } else { - successCallback(createdId, createdUUID, createdIntent); - } - return null; - }) - .catch(err => { - failureCallback(err); - }); - } - return null; - }) - .catch(err => { - failureCallback(err); - }); - } - - return null; - }) - .catch(err => { - failureCallback(err); - }); -}; - -router.post("/edit", function(req, res) { - edit( - req, - res, - (createdId, createdUUID, createdIntent) => { - logger("info", "Successfully edited feature.", req.originalUrl, req); - res.send({ - status: "success", - message: "Successfully edited feature.", - body: { id: createdId, uuid: createdUUID, intent: createdIntent } - }); - }, - err => { - logger("error", "Failed to edit feature.", req.originalUrl, req, err); - res.send({ - status: "failure", - message: "Failed to edit feature.", - body: {} - }); - } - ); -}); - -/** - * Hides a feature - * { - * file_id: (required) - * feature_id: (required) - * } - */ -router.post("/remove", function(req, res, next) { - let Files = req.body.test === "true" ? UserfilesTEST : Userfiles; - let Features = req.body.test === "true" ? UserfeaturesTEST : Userfeatures; - let Histories = req.body.test === "true" ? FilehistoriesTEST : Filehistories; - - let time = Math.floor(Date.now()); - - let groups = []; - if (req.groups) groups = Object.keys(req.groups); - - Files.findOne({ - where: { - id: req.body.file_id, - [Sequelize.Op.or]: { - file_owner: req.user, - [Sequelize.Op.and]: { - file_owner: "group", - file_owner_group: { [Sequelize.Op.overlap]: groups } - } - } - } - }).then(file => { - if (!file) { - logger("error", "Failed to access file.", req.originalUrl, req); - res.send({ - status: "failure", - message: "Failed to access file.", - body: {} - }); - } else { - Features.update( - { - extant_end: time - }, - { - where: { - file_id: req.body.file_id, - id: req.body.id - } - } - ) - .then(() => { - //Table, file_id, feature_id, feature_idRemove, time, undoToTime, action_index - pushToHistory( - Histories, - req.body.file_id, - null, - req.body.id, - time, - null, - 2, - () => { - logger("info", "Feature removed.", req.originalUrl, req); - res.send({ - status: "success", - message: "Feature removed.", - body: {} - }); - }, - err => { - logger( - "error", - "Failed to remove feature.", - req.originalUrl, - req, - err - ); - res.send({ - status: "failure", - message: "Failed to remove feature.", - body: {} - }); - } - ); - - return null; - }) - .catch(err => { - logger( - "error", - "Failed to find and remove feature.", - req.originalUrl, - req, - err - ); - res.send({ - status: "failure", - message: "Failed to find and remove feature.", - body: {} - }); - }); - } - - return null; - }); -}); - -/** - * Undoes drawings - * { - * file_id: (required) - * undo_time: (required) - * } - */ -router.post("/undo", function(req, res, next) { - let Files = req.body.test === "true" ? UserfilesTEST : Userfiles; - let Features = req.body.test === "true" ? UserfeaturesTEST : Userfeatures; - let Histories = req.body.test === "true" ? FilehistoriesTEST : Filehistories; - - let time = Math.floor(Date.now()); - - let groups = []; - if (req.groups) groups = Object.keys(req.groups); - - Files.findOne({ - where: { - id: req.body.file_id, - [Sequelize.Op.or]: { - file_owner: req.user, - [Sequelize.Op.and]: { - file_owner: "group", - file_owner_group: { [Sequelize.Op.overlap]: groups } - } - } - } - }).then(file => { - if (!file) { - logger("error", "Failed to access file.", req.originalUrl, req); - res.send({ - status: "failure", - message: "Failed to access file.", - body: {} - }); - } else { - Features.update( - { - trimmed_start: Sequelize.fn( - "array_append", - Sequelize.col("trimmed_start"), - req.body.undo_time - ), - trimmed_at: Sequelize.fn( - "array_append", - Sequelize.col("trimmed_at"), - String(time) - ), - trimmed_at_final: time - }, - { - where: { - file_id: req.body.file_id, - - [Sequelize.Op.or]: { - [Sequelize.Op.and]: { - extant_start: { - [Sequelize.Op.gt]: req.body.undo_time - }, - [Sequelize.Op.or]: { - extant_end: { - [Sequelize.Op.gt]: req.body.undo_time - }, - extant_end: null - } - }, - trimmed_at_final: { - //undo time less than any trimmed end value - [Sequelize.Op.lte]: time - } - } - } - } - ) - .then(r => { - pushToHistory( - Histories, - req.body.file_id, - null, - null, - time, - req.body.undo_time, - 3, - () => { - logger("info", "Undo successful.", req.originalUrl, req); - res.send({ - status: "success", - message: "Undo successful.", - body: {} - }); - }, - err => { - logger("error", "Failed to undo.", req.originalUrl, req, err); - res.send({ - status: "failure", - message: "Failed to undo.", - body: {} - }); - } - ); - - return null; - }) - .catch(err => { - logger("error", "Failed to undo file.", req.originalUrl, req, err); - res.send({ - status: "failure", - message: "Failed to undo file.", - body: {} - }); - }); - } - - return null; - }); -}); - -/** - * Merge features - * { - * file_id: (required) - * prop_id: - feature id whose properties will be copied (required) - * ids: - of all the ids to merge together - * } - */ -router.post("/merge", function(req, res, next) { - let Files = req.body.test === "true" ? UserfilesTEST : Userfiles; - let Features = req.body.test === "true" ? UserfeaturesTEST : Userfeatures; - let Histories = req.body.test === "true" ? FilehistoriesTEST : Filehistories; - - let time = Math.floor(Date.now()); - - let groups = []; - if (req.groups) groups = Object.keys(req.groups); - - //Add prop_ids to ids if it's not already there - if (req.body.ids.indexOf(req.body.prop_id) == -1) - req.body.ids.push(req.body.prop_id); - - ///Check that the provided file_id is an id that belongs to the current user - Files.findOne({ - where: { - id: req.body.file_id, - [Sequelize.Op.or]: { - file_owner: req.user, - [Sequelize.Op.and]: { - file_owner: "group", - file_owner_group: { [Sequelize.Op.overlap]: groups } - } - } - } - }).then(file => { - if (!file) { - logger("error", "Failed to access file.", req.originalUrl, req); - res.send({ - status: "failure", - message: "Failed to access file.", - body: {} - }); - } else { - Features.findOne({ - where: { - id: req.body.prop_id - } - }).then(feature => { - let ids = req.body.ids; - ids = ids.join(","); - ids = ids || "NULL"; - - let q; - if (feature.geom.type == "LineString") { - q = [ - "SELECT ST_AsGeoJSON( (ST_Dump(mergedgeom.geom)).geom ) AS merged FROM", - "(", - "SELECT ST_LineMerge(ST_Union(geom)) AS geom", - "FROM user_features" + - (req.body.test === "true" ? "_tests" : "") + - " AS a", - "WHERE a.id IN (" + ids + ") AND a.file_id = :file_id", - ") AS mergedgeom" - ].join(" "); - } else { - q = [ - "SELECT ST_AsGeoJSON( (ST_Dump(mergedgeom.geom)).geom ) as merged FROM", - "(", - "SELECT ST_BUFFER(ST_UNION(", - "ARRAY((", - "SELECT ST_BUFFER(geom, 0.00001, 'join=mitre')", - "FROM user_features" + - (req.body.test === "true" ? "_tests" : "") + - " AS a", - "WHERE a.id IN (" + ids + ")", - "))", - "), -0.00001,'join=mitre') AS geom", - ") AS mergedgeom" - ].join(" "); - } - sequelize - .query(q, { - replacements: { - file_id: req.body.file_id - } - }) - .spread(results => { - let oldIds = req.body.ids.map(function(id) { - return parseInt(id, 10); - }); - - let newIds = []; - - addLoop(0); - function addLoop(i) { - if (i >= results.length) { - pushToHistory( - Histories, - req.body.file_id, - newIds, - oldIds, - time, - null, - 6, - () => { - logger( - "info", - "Successfully merged " + - req.body.ids.length + - " features.", - req.originalUrl, - req - ); - res.send({ - status: "success", - message: - "Successfully merged " + - req.body.ids.length + - " features.", - body: { ids: newIds } - }); - }, - err => { - logger( - "error", - "Merge failure.", - req.originalUrl, - req, - err - ); - res.send({ - status: "failure", - message: "Merge failure.", - body: {} - }); - } - ); - return null; - } - let mergedFeature = JSON.parse(JSON.stringify(feature)); - mergedFeature.geom = JSON.parse(results[i].merged); - mergedFeature.geom.crs = { - type: "name", - properties: { name: "EPSG:4326" } - }; - delete mergedFeature.id; - - Features.create(mergedFeature) - .then(created => { - newIds.push(created.id); - addLoop(i + 1); - return null; - }) - .catch(err => { - addLoop(i + 1); - return null; - //failureCallback(); - }); - } - }); - }); - } - }); -}); - -/** - * split features - * { - * file_id: (required) - * split: - feature to split against (required) - * ids: - of all the ids to perform the split against - * } - */ -router.post("/split", function(req, res, next) { - let Files = req.body.test === "true" ? UserfilesTEST : Userfiles; - let Features = req.body.test === "true" ? UserfeaturesTEST : Userfeatures; - let Histories = req.body.test === "true" ? FilehistoriesTEST : Filehistories; - - let time = Math.floor(Date.now()); - - let groups = []; - if (req.groups) groups = Object.keys(req.groups); - - let splitFeature = JSON.parse(req.body.split); - - ///Check that the provided file_id is an id that belongs to the current user - Files.findOne({ - where: { - id: req.body.file_id, - [Sequelize.Op.or]: { - file_owner: req.user, - [Sequelize.Op.and]: { - file_owner: "group", - file_owner_group: { [Sequelize.Op.overlap]: groups } - } - } - } - }) - .then(file => { - if (!file) { - logger("error", "Failed to access file.", req.originalUrl, req); - res.send({ - status: "failure", - message: "Failed to access file.", - body: {} - }); - } else { - let ids = req.body.ids; - ids = ids.join(","); - ids = ids || "NULL"; - - let geom = splitFeature.geometry; - geom.crs = { type: "name", properties: { name: "EPSG:4326" } }; - - let q = [ - "SELECT g.id, g.file_id, g.level, g.intent, g.properties, ST_SPLIT(ST_SetSRID(g.geom, 4326), ST_GeomFromGeoJSON(:geom)) FROM", - "(", - "SELECT id, file_id, level, intent, properties, geom", - "FROM user_features AS a", - "WHERE a.id IN (" + ids + ") AND a.file_id = :file_id", - ") AS g" - ].join(" "); - sequelize - .query(q, { - replacements: { - file_id: parseInt(req.body.file_id), - geom: JSON.stringify(geom) - } - }) - .spread(results => { - //reformat results - let r = []; - for (var i = 0; i < results.length; i++) { - for (var j = 0; j < results[i].st_split.geometries.length; j++) { - //If the length is 1, then no split occurred - if (results[i].st_split.geometries.length > 1) - r.push({ - file_id: results[i].file_id, - intent: results[i].intent, - level: results[i].level, - properties: results[i].properties, - geom: results[i].st_split.geometries[j] - }); - } - } - - let oldIds = req.body.ids.map(function(id) { - return parseInt(id, 10); - }); - - let newIds = []; - addLoop(0); - function addLoop(i) { - if (i >= r.length) { - pushToHistory( - Histories, - req.body.file_id, - newIds, - oldIds, - time, - null, - 8, - () => { - res.send({ - status: "success", - message: - "Successfully split " + - req.body.ids.length + - " features.", - body: { ids: newIds } - }); - }, - err => { - logger( - "error", - "Split failure.", - req.originalUrl, - req, - err - ); - res.send({ - status: "failure", - message: "Split failure.", - body: {} - }); - } - ); - return null; - } - - Features.create(r[i]) - .then(created => { - newIds.push(created.id); - addLoop(i + 1); - return null; - }) - .catch(err => { - console.log(err); - addLoop(i + 1); - return null; - }); - } - - return null; - }) - .catch(err => { - logger("error", "Failed to split.", req.originalUrl, req, err); - res.send({ - status: "failure", - message: "Failed to split.", - body: {} - }); - - return null; - }); - } - return null; - }) - .catch(err => { - logger("error", "Failed to split.", req.originalUrl, req, err); - res.send({ - status: "failure", - message: "Failed to split.", - body: {} - }); - - return null; - }); -}); - -router.post("/replace", function(req, res, next) { - res.send("test draw replace"); -}); - -router.post("/sendtofront", function(req, res, next) { - res.send("test draw front"); -}); - -router.post("/sendtoback", function(req, res, next) { - res.send("test draw back"); -}); - -/** - * Clears out testing tables - */ -router.post("/clear_test", function(req, res, next) { - sequelize - .query('TRUNCATE TABLE "user_features_tests" RESTART IDENTITY') - .then(() => { - sequelize - .query('TRUNCATE TABLE "publisheds_tests" RESTART IDENTITY') - .then(() => { - sequelize - .query('TRUNCATE TABLE "file_histories_tests" RESTART IDENTITY') - .then(() => { - sequelize - .query('TRUNCATE TABLE "user_files_tests" RESTART IDENTITY') - .then(() => { - //Add back sel files - makeMasterFilesTEST(req.leadGroupName, () => { - logger( - "info", - "Successfully cleared test tables.", - req.originalUrl, - req - ); - res.send({ - status: "success", - message: "Successfully cleared tables.", - body: {} - }); - - return null; - }); - - return null; - }) - .catch(err => { - logger( - "error", - "Failed to clear 1 test table. (C)", - req.originalUrl, - req, - err - ); - res.send({ - status: "failure", - message: "Failed to clear 1 table.", - body: {} - }); - - return null; - }); - return null; - }) - .catch(err => { - logger( - "error", - "Failed to clear 1 test table. (B)", - req.originalUrl, - req, - err - ); - res.send({ - status: "failure", - message: "Failed to clear 1 table.", - body: {} - }); - return null; - }); - - return null; - }) - .catch(err => { - logger( - "error", - "Failed to clear 1 test table. (A)", - req.originalUrl, - req, - err - ); - res.send({ - status: "failure", - message: "Failed to clear 1 table.", - body: {} - }); - return null; - }); - - return null; - }) - .catch(err => { - logger( - "error", - "Failed to clear both test tables.", - req.originalUrl, - req, - err - ); - res.send({ - status: "failure", - message: "Failed to clear both tables", - body: {} - }); - - return null; - }); -}); - -const makeMasterFilesTEST = (leadGroupName, callback) => { - let intents = ["roi", "campaign", "campsite", "trail", "signpost"]; - makeMasterFileTEST(0, UserfilesTEST); - - function makeMasterFileTEST(i, Table) { - let intent = intents[i]; - if (intent == null) { - callback(); - return null; - } - - Table.findOrCreate({ - where: { - file_owner: "group", - file_owner_group: [leadGroupName], - file_name: intent.toUpperCase(), - file_description: "Lead composed " + intent.toUpperCase() + "s.", - is_master: true, - intent: intent, - public: "1", - hidden: "0" - } - }).spread(function(userResult, created) { - makeMasterFileTEST(i + 1, Table); - return null; - }); - - return null; - } -}; - -module.exports = { router, add }; +const express = require("express"); +const logger = require("../../../logger"); +const database = require("../../../database"); +const Sequelize = require("sequelize"); +const uuidv4 = require("uuid/v4"); +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; +const uf = require("../models/userfeatures"); +const Userfeatures = uf.Userfeatures; +const UserfeaturesTEST = uf.UserfeaturesTEST; +const { sequelize } = require("../../../connection"); + +const router = express.Router(); +const db = database.db; + +router.post("/", function (req, res, next) { + res.send("test draw"); +}); + +/** + * Crops out duplicate array elements between arrays + * Ex. + * arr1=['a','b'], arr2=['b'] -> ['a'] + * + * @param {[]} arr1 + * @param {[]} arr2 + * @return {[]} arr1 without any elements of arr2 + */ +const uniqueAcrossArrays = (arr1, arr2) => { + let uniqueArr = Object.assign([], arr1); + for (let i = uniqueArr.length - 1; i >= 0; i--) { + if (arr2.indexOf(arr1[i]) != -1) uniqueArr.splice(i, 1); + } + + return uniqueArr; +}; + +const pushToHistory = ( + Table, + file_id, + feature_id, + feature_idRemove, + time, + undoToTime, + action_index, + successCallback, + failureCallback +) => { + Table.findAll({ + limit: 1, + where: { + file_id: file_id, + }, + order: [["history_id", "DESC"]], + }) + .then((lastHistory) => { + let maxHistoryId = -Infinity; + let bestI = -1; + if (lastHistory && lastHistory.length > 0) { + return { + historyIndex: lastHistory[0].history_id + 1, + history: lastHistory[0].history, + }; + } else return { historyIndex: 0, history: [] }; + }) + .then((historyObj) => { + getNextHistory( + Table, + historyObj.history, + action_index, + feature_id, + feature_idRemove, + file_id, + undoToTime, + (h) => { + let newHistoryEntry = { + file_id: file_id, + history_id: historyObj.historyIndex, + time: time, + action_index: action_index, + history: h, + }; + // Insert new entry into the history table + Table.create(newHistoryEntry) + .then((created) => { + successCallback(); + return null; + }) + .catch((err) => { + failureCallback(err); + }); + }, + (err) => { + failureCallback(err); + } + ); + return null; + }); +}; + +const getNextHistory = ( + Table, + history, + action_index, + feature_idAdd, + feature_idRemove, + file_id, + undoToTime, + successCallback, + failureCallback +) => { + switch (action_index) { + case 0: //add + history.push(feature_idAdd); + if (Array.isArray(feature_idAdd)) history = feature_idAdd; + successCallback(history); + return; + case 1: //edit + history.splice(history.indexOf(parseInt(feature_idRemove)), 1); + history.push(feature_idAdd); + successCallback(history); + return; + case 2: //delete + history.splice(history.indexOf(parseInt(feature_idRemove)), 1); + successCallback(history); + return; + case 3: //undo + //Here we do want to use the last history, we want to use the history at undo to time + Table.findOne({ + where: { + file_id: file_id, + time: undoToTime, + }, + }) + .then((history) => { + successCallback(history.history); + return null; + }) + .catch((err) => { + failureCallback(err); + return null; + }); + break; + case 5: //Clip add over + case 6: //Merge add array of add ids and remove array of remove ids + case 7: //Clip add under + case 8: //Split + //add + history = history.concat(feature_idAdd); + //remove + history = uniqueAcrossArrays(history, feature_idRemove); + successCallback(history); + return; + default: + failureCallback("Unknown action_index: " + action_index); + } +}; + +/** + * + * @param {number} file_id + * @param {number} added_id + */ +const clipOver = function ( + req, + res, + file_id, + added_id, + time, + successCallback, + failureCallback +) { + let Histories = req.body.test === "true" ? FilehistoriesTEST : Filehistories; + + //CLIP OVER + Histories.findAll({ + limit: 1, + where: { + file_id: file_id, + }, + order: [["history_id", "DESC"]], + }) + .then((lastHistory) => { + let maxHistoryId = -Infinity; + let bestI = -1; + if (lastHistory && lastHistory.length > 0) { + return { + historyIndex: lastHistory[0].history_id + 1, + history: lastHistory[0].history, + }; + } else return { historyIndex: 0, history: [] }; + }) + .then((historyObj) => { + let history = historyObj.history; + history = history.join(","); + history = history || "NULL"; + //RETURN ALL THE CHANGED SHAPE IDs AND GEOMETRIES + let q = [ + "SELECT clipped.id, ST_AsGeoJSON( (ST_Dump(clipped.newgeom)).geom ) AS newgeom FROM", + "(", + "SELECT data.id, data.newgeom", + "FROM (", + "SELECT r.id, ST_DIFFERENCE(ST_MakeValid(r.geom),", + "ST_MakeValid((", + "SELECT a.geom", + "FROM user_features" + + (req.body.test === "true" ? "_tests" : "") + + " AS a", + "WHERE a.id = :added_id AND ST_INTERSECTS(a.geom, r.geom)", + "))", + ") AS newgeom", + "FROM user_features" + + (req.body.test === "true" ? "_tests" : "") + + " AS r", + "WHERE r.file_id = :file_id AND r.id != :added_id AND r.id IN (" + + history + + ")", + ") data", + "WHERE data.newgeom IS NOT NULL", + ") AS clipped", + ].join(" "); + sequelize + .query(q, { + replacements: { + file_id: file_id, + added_id: added_id, + }, + }) + .spread((results) => { + let oldIds = []; + let newIds = [added_id]; + + editLoop(0); + function editLoop(i) { + if (i >= results.length) { + pushToHistory( + Histories, + req.body.file_id, + newIds, + oldIds, + time, + null, + 5, + () => { + if (typeof successCallback === "function") successCallback(); + }, + (err) => { + if (typeof failureCallback === "function") + failureCallback(err); + }, + "addandremove" + ); + return; + } + let newReq = Object.assign({}, req); + results[i].newgeom.crs = { + type: "name", + properties: { name: "EPSG:4326" }, + }; + newReq.body = { + file_id: file_id, + feature_id: results[i].id, + geometry: results[i].newgeom, + to_history: false, + test: req.body.test, + }; + + if (oldIds.indexOf(results[i].id) == -1) oldIds.push(results[i].id); + edit( + newReq, + res, + (newId) => { + newIds.push(newId); + editLoop(i + 1); + }, + () => { + editLoop(i + 1); + } + ); + } + + return null; + }) + .catch((err) => { + failureCallback(err); + }); + + return null; + }) + .catch((err) => { + failureCallback(err); + }); +}; + +const clipUnder = function ( + req, + res, + newFeature, + time, + successCallback, + failureCallback +) { + let Features = req.body.test === "true" ? UserfeaturesTEST : Userfeatures; + let Histories = req.body.test === "true" ? FilehistoriesTEST : Filehistories; + + Histories.findAll({ + limit: 1, + where: { + file_id: newFeature.file_id, + }, + order: [["history_id", "DESC"]], + }) + .then((lastHistory) => { + let maxHistoryId = -Infinity; + let bestI = -1; + if (lastHistory && lastHistory.length > 0) { + return { + historyIndex: lastHistory[0].history_id + 1, + history: lastHistory[0].history, + }; + } else return { historyIndex: 0, history: [] }; + }) + .then((historyObj) => { + let history = historyObj.history; + history = history.join(","); + history = history || "NULL"; + + //Continually clip the added feature with the other features of the file + let q = [ + "WITH RECURSIVE clipper (n, clippedgeom) AS (", + "SELECT 0 n, ST_MakeValid(ST_GeomFromGeoJSON(:geom)) clippedgeom", + "UNION ALL", + "SELECT n+1, ST_DIFFERENCE(", + "clippedgeom,", + "(", + "SELECT ST_BUFFER(", + "ST_UNION(", + "ARRAY((", + "SELECT ST_BUFFER(a.geom, 0.00001, 'join=mitre')", + "FROM user_features" + + (req.body.test === "true" ? "_tests" : "") + + " AS a", + "WHERE a.id IN (" + + history + + ") AND ST_INTERSECTS(a.geom, clippedgeom)", + "))", + "),", + "-0.00001,'join=mitre')", + ")", + ")", + "FROM clipper", + "WHERE n < 1", + ")", + "SELECT ST_AsGeoJSON( (ST_Dump(clipped.clippedgeom)).geom ) as geom FROM", + "(", + "SELECT c.n, c.clippedgeom as clippedgeom FROM clipper c", + "WHERE c.clippedgeom IS NOT NULL", + "ORDER by c.n DESC LIMIT 1", + ") AS clipped", + ].join(" "); + + sequelize + .query(q, { + replacements: { + geom: JSON.stringify(newFeature.geom), + }, + }) + .spread((results) => { + let oldIds = []; + let newIds = []; + + addLoop(0); + function addLoop(i) { + if (i >= results.length) { + pushToHistory( + Histories, + req.body.file_id, + newIds, + oldIds, + time, + null, + 7, + () => { + if (typeof successCallback === "function") successCallback(); + }, + (err) => { + if (typeof failureCallback === "function") + failureCallback(err); + } + ); + return null; + } + let clippedFeature = Object.assign({}, newFeature); + clippedFeature.properties = JSON.parse(newFeature.properties); + clippedFeature.geom = JSON.parse(results[i].geom); + clippedFeature.geom.crs = { + type: "name", + properties: { name: "EPSG:4326" }, + }; + clippedFeature.properties.uuid = uuidv4(); + clippedFeature.properties = JSON.stringify( + clippedFeature.properties + ); + + Features.create(clippedFeature) + .then((created) => { + newIds.push(created.id); + //now update the + addLoop(i + 1); + return null; + }) + .catch((err) => { + addLoop(i + 1); + return null; + //failureCallback(); + }); + } + + return null; + }) + .catch((err) => { + failureCallback(err); + }); + + return null; + }) + .catch((err) => { + failureCallback(err); + }); +}; +/** + * Adds a feature + * { + * file_id: (required) + * parent: (optional) + * order: <'min' || 'max' || int> (optional) + * 'min' and < 0 adds behind all features + * keywords: (optional) + * intent: (optional) + * properties: (optional) + * geometry: (required) + * } + */ +const add = function ( + req, + res, + successCallback, + failureCallback1, + failureCallback2 +) { + let Files = req.body.test === "true" ? UserfilesTEST : Userfiles; + let Features = req.body.test === "true" ? UserfeaturesTEST : Userfeatures; + let Histories = req.body.test === "true" ? FilehistoriesTEST : Filehistories; + + let time = Math.floor(Date.now()); + + let groups = []; + if (req.groups) groups = Object.keys(req.groups); + + if (req.body.to_history == null) req.body.to_history = true; + + //Check that the provided file_id is an id that belongs to the current user + Files.findOne({ + where: { + id: req.body.file_id, + [Sequelize.Op.or]: { + file_owner: req.user, + [Sequelize.Op.and]: { + file_owner: "group", + file_owner_group: { [Sequelize.Op.overlap]: groups }, + }, + }, + }, + }).then((file) => { + if (!file) { + if (typeof failureCallback1 === "function") failureCallback1(); + } else { + //Find the next level + let order = "max"; + if (req.body.order == "min" || req.body.order < 0) order = "min"; + + Features.findAll({ + where: { + file_id: req.body.file_id, + }, + }) + .then((features) => { + let maxLevel = -Infinity; + let minLevel = Infinity; + if (features && features.length > 0) { + for (let i = 0; i < features.length; i++) { + maxLevel = Math.max(features[i].level, maxLevel); + minLevel = Math.min(features[i].level, minLevel); + } + if (order === "max") return maxLevel + 1; + else return minLevel - 1; + } else return 0; + }) + .then((level) => { + let properties = req.body.properties || {}; + //Remove _ from properties if it has it. This is because the server returns metadata + // under _ and we don't want it to potentially nest + delete properties["_"]; + + let geom = JSON.parse(req.body.geometry); + //Geometry needs this for the spatialiness to work + geom.crs = { type: "name", properties: { name: "EPSG:4326" } }; + + let newFeature = { + file_id: req.body.file_id, + level: level, + intent: req.body.intent, + elevated: "0", + properties: properties, + geom: geom, + }; + + if (req.body.clip === "under") { + clipUnder( + req, + res, + newFeature, + time, + (createdId, createdIntent) => { + if (typeof successCallback === "function") + successCallback(createdId, createdIntent); + }, + (err) => { + if (typeof failureCallback2 === "function") + failureCallback2(err); + } + ); + } else { + newFeature.properties = JSON.parse(newFeature.properties); + newFeature.properties.uuid = uuidv4(); + newFeature.properties = JSON.stringify(newFeature.properties); + // Insert new feature into the feature table + Features.create(newFeature) + .then((created) => { + if (req.body.to_history) { + let id = created.id; + if (req.body.bulk_ids != null) { + id = req.body.bulk_ids; + id.push(created.id); + } + if (req.body.clip === "over") { + clipOver( + req, + res, + newFeature.file_id, + id, + time, + () => { + if (typeof successCallback === "function") + successCallback(created.id, created.intent); + }, + (err) => { + if (typeof failureCallback2 === "function") + failureCallback2(err); + } + ); + } else { + pushToHistory( + Histories, + req.body.file_id, + id, + null, + time, + null, + 0, + () => { + if (typeof successCallback === "function") + successCallback(created.id, created.intent); + }, + (err) => { + if (typeof failureCallback2 === "function") + failureCallback2(err); + } + ); + } + } else { + if (typeof successCallback === "function") + successCallback(created.id, created.intent); + } + return null; + }) + .catch((err) => { + if (typeof failureCallback2 === "function") + failureCallback2(err); + }); + } + }); + return null; + } + }); +}; +router.post("/add", function (req, res, next) { + add( + req, + res, + (id, intent) => { + logger("info", "Successfully added a new feature.", req.originalUrl, req); + res.send({ + status: "success", + message: "Successfully added a new feature.", + body: { id: id, intent: intent }, + }); + }, + (err) => { + logger("error", "Failed to access file.", req.originalUrl, req, err); + res.send({ + status: "failure", + message: "Failed to access file.", + body: {}, + }); + }, + (err) => { + logger("error", "Failed to add new feature.", req.originalUrl, req, err); + res.send({ + status: "failure", + message: "Failed to add new feature.", + body: {}, + }); + } + ); +}); + +/** + * Edits a feature + * { + * file_id: (required) + * feature_id: (required) + * parent: (optional) + * keywords: (optional) + * intent: (optional) + * properties: (optional) + * geometry: (optional) + * } + */ +const edit = function (req, res, successCallback, failureCallback) { + let Files = req.body.test === "true" ? UserfilesTEST : Userfiles; + let Features = req.body.test === "true" ? UserfeaturesTEST : Userfeatures; + let Histories = req.body.test === "true" ? FilehistoriesTEST : Filehistories; + + let time = Math.floor(Date.now()); + + let groups = []; + if (req.groups) groups = Object.keys(req.groups); + + if (req.body.to_history == null) req.body.to_history = true; + + Files.findOne({ + where: { + id: req.body.file_id, + [Sequelize.Op.or]: { + file_owner: req.user, + [Sequelize.Op.and]: { + file_owner: "group", + file_owner_group: { [Sequelize.Op.overlap]: groups }, + }, + }, + }, + }) + .then((file) => { + if (!file) { + failureCallback(); + } else { + Features.findOne({ + where: { + id: req.body.feature_id, + file_id: req.body.file_id, + }, + attributes: { + include: [ + [ + Sequelize.fn("ST_AsGeoJSON", Sequelize.col("geom")), + "geojson_geom", + ], + ], + }, + }) + .then((feature) => { + if (!feature && !req.body.addIfNotFound) { + failureCallback(); + } else { + var newAttributes = feature.dataValues; + + delete newAttributes["id"]; + delete newAttributes.properties["_"]; + newAttributes.extant_start = time; + + if (req.body.hasOwnProperty("parent")) + newAttributes.parent = req.body.parent; + if (req.body.hasOwnProperty("keywords")) + newAttributes.keywords = req.body.keywords; + if (req.body.hasOwnProperty("intent")) + newAttributes.intent = req.body.intent; + if (req.body.hasOwnProperty("properties")) + newAttributes.properties = req.body.properties; + if (req.body.hasOwnProperty("geometry")) { + newAttributes.geom = JSON.parse(req.body.geometry); + } else { + newAttributes.geom = JSON.parse( + feature.dataValues.geojson_geom + ); + } + if ( + req.body.hasOwnProperty("reassignUUID") && + req.body.reassignUUID == "true" + ) { + newAttributes.properties = JSON.parse(newAttributes.properties); + newAttributes.properties.uuid = uuidv4(); + newAttributes.properties = JSON.stringify( + newAttributes.properties + ); + } + + newAttributes.geom.crs = { + type: "name", + properties: { name: "EPSG:4326" }, + }; + + Features.create(newAttributes) + .then((created) => { + let createdId = created.id; + let createdUUID = JSON.parse(created.properties).uuid; + let createdIntent = created.intent; + + if (req.body.to_history) { + pushToHistory( + Histories, + req.body.file_id, + created.id, + req.body.feature_id, + time, + null, + 1, + () => { + successCallback(createdId, createdUUID, createdIntent); + }, + (err) => { + failureCallback(err); + } + ); + } else { + successCallback(createdId, createdUUID, createdIntent); + } + return null; + }) + .catch((err) => { + failureCallback(err); + }); + } + return null; + }) + .catch((err) => { + failureCallback(err); + }); + } + + return null; + }) + .catch((err) => { + failureCallback(err); + }); +}; + +router.post("/edit", function (req, res) { + edit( + req, + res, + (createdId, createdUUID, createdIntent) => { + logger("info", "Successfully edited feature.", req.originalUrl, req); + res.send({ + status: "success", + message: "Successfully edited feature.", + body: { id: createdId, uuid: createdUUID, intent: createdIntent }, + }); + }, + (err) => { + logger("error", "Failed to edit feature.", req.originalUrl, req, err); + res.send({ + status: "failure", + message: "Failed to edit feature.", + body: {}, + }); + } + ); +}); + +/** + * Hides a feature + * { + * file_id: (required) + * feature_id: (required) + * } + */ +router.post("/remove", function (req, res, next) { + let Files = req.body.test === "true" ? UserfilesTEST : Userfiles; + let Features = req.body.test === "true" ? UserfeaturesTEST : Userfeatures; + let Histories = req.body.test === "true" ? FilehistoriesTEST : Filehistories; + + let time = Math.floor(Date.now()); + + let groups = []; + if (req.groups) groups = Object.keys(req.groups); + + Files.findOne({ + where: { + id: req.body.file_id, + [Sequelize.Op.or]: { + file_owner: req.user, + [Sequelize.Op.and]: { + file_owner: "group", + file_owner_group: { [Sequelize.Op.overlap]: groups }, + }, + }, + }, + }).then((file) => { + if (!file) { + logger("error", "Failed to access file.", req.originalUrl, req); + res.send({ + status: "failure", + message: "Failed to access file.", + body: {}, + }); + } else { + Features.update( + { + extant_end: time, + }, + { + where: { + file_id: req.body.file_id, + id: req.body.id, + }, + } + ) + .then(() => { + //Table, file_id, feature_id, feature_idRemove, time, undoToTime, action_index + pushToHistory( + Histories, + req.body.file_id, + null, + req.body.id, + time, + null, + 2, + () => { + logger("info", "Feature removed.", req.originalUrl, req); + res.send({ + status: "success", + message: "Feature removed.", + body: {}, + }); + }, + (err) => { + logger( + "error", + "Failed to remove feature.", + req.originalUrl, + req, + err + ); + res.send({ + status: "failure", + message: "Failed to remove feature.", + body: {}, + }); + } + ); + + return null; + }) + .catch((err) => { + logger( + "error", + "Failed to find and remove feature.", + req.originalUrl, + req, + err + ); + res.send({ + status: "failure", + message: "Failed to find and remove feature.", + body: {}, + }); + }); + } + + return null; + }); +}); + +/** + * Undoes drawings + * { + * file_id: (required) + * undo_time: (required) + * } + */ +router.post("/undo", function (req, res, next) { + let Files = req.body.test === "true" ? UserfilesTEST : Userfiles; + let Features = req.body.test === "true" ? UserfeaturesTEST : Userfeatures; + let Histories = req.body.test === "true" ? FilehistoriesTEST : Filehistories; + + let time = Math.floor(Date.now()); + + let groups = []; + if (req.groups) groups = Object.keys(req.groups); + + Files.findOne({ + where: { + id: req.body.file_id, + [Sequelize.Op.or]: { + file_owner: req.user, + [Sequelize.Op.and]: { + file_owner: "group", + file_owner_group: { [Sequelize.Op.overlap]: groups }, + }, + }, + }, + }).then((file) => { + if (!file) { + logger("error", "Failed to access file.", req.originalUrl, req); + res.send({ + status: "failure", + message: "Failed to access file.", + body: {}, + }); + } else { + Features.update( + { + trimmed_start: Sequelize.fn( + "array_append", + Sequelize.col("trimmed_start"), + req.body.undo_time + ), + trimmed_at: Sequelize.fn( + "array_append", + Sequelize.col("trimmed_at"), + String(time) + ), + trimmed_at_final: time, + }, + { + where: { + file_id: req.body.file_id, + + [Sequelize.Op.or]: { + [Sequelize.Op.and]: { + extant_start: { + [Sequelize.Op.gt]: req.body.undo_time, + }, + [Sequelize.Op.or]: { + extant_end: { + [Sequelize.Op.gt]: req.body.undo_time, + }, + extant_end: null, + }, + }, + trimmed_at_final: { + //undo time less than any trimmed end value + [Sequelize.Op.lte]: time, + }, + }, + }, + } + ) + .then((r) => { + pushToHistory( + Histories, + req.body.file_id, + null, + null, + time, + req.body.undo_time, + 3, + () => { + logger("info", "Undo successful.", req.originalUrl, req); + res.send({ + status: "success", + message: "Undo successful.", + body: {}, + }); + }, + (err) => { + logger("error", "Failed to undo.", req.originalUrl, req, err); + res.send({ + status: "failure", + message: "Failed to undo.", + body: {}, + }); + } + ); + + return null; + }) + .catch((err) => { + logger("error", "Failed to undo file.", req.originalUrl, req, err); + res.send({ + status: "failure", + message: "Failed to undo file.", + body: {}, + }); + }); + } + + return null; + }); +}); + +/** + * Merge features + * { + * file_id: (required) + * prop_id: - feature id whose properties will be copied (required) + * ids: - of all the ids to merge together + * } + */ +router.post("/merge", function (req, res, next) { + let Files = req.body.test === "true" ? UserfilesTEST : Userfiles; + let Features = req.body.test === "true" ? UserfeaturesTEST : Userfeatures; + let Histories = req.body.test === "true" ? FilehistoriesTEST : Filehistories; + + let time = Math.floor(Date.now()); + + let groups = []; + if (req.groups) groups = Object.keys(req.groups); + + //Add prop_ids to ids if it's not already there + if (req.body.ids.indexOf(req.body.prop_id) == -1) + req.body.ids.push(req.body.prop_id); + + ///Check that the provided file_id is an id that belongs to the current user + Files.findOne({ + where: { + id: req.body.file_id, + [Sequelize.Op.or]: { + file_owner: req.user, + [Sequelize.Op.and]: { + file_owner: "group", + file_owner_group: { [Sequelize.Op.overlap]: groups }, + }, + }, + }, + }).then((file) => { + if (!file) { + logger("error", "Failed to access file.", req.originalUrl, req); + res.send({ + status: "failure", + message: "Failed to access file.", + body: {}, + }); + } else { + Features.findOne({ + where: { + id: req.body.prop_id, + }, + }).then((feature) => { + let ids = req.body.ids; + ids = ids.join(","); + ids = ids || "NULL"; + + let q; + if (feature.geom.type == "LineString") { + q = [ + "SELECT ST_AsGeoJSON( (ST_Dump(mergedgeom.geom)).geom ) AS merged FROM", + "(", + "SELECT ST_LineMerge(ST_Union(geom)) AS geom", + "FROM user_features" + + (req.body.test === "true" ? "_tests" : "") + + " AS a", + "WHERE a.id IN (" + ids + ") AND a.file_id = :file_id", + ") AS mergedgeom", + ].join(" "); + } else { + q = [ + "SELECT ST_AsGeoJSON( (ST_Dump(mergedgeom.geom)).geom ) as merged FROM", + "(", + "SELECT ST_BUFFER(ST_UNION(", + "ARRAY((", + "SELECT ST_BUFFER(geom, 0.00001, 'join=mitre')", + "FROM user_features" + + (req.body.test === "true" ? "_tests" : "") + + " AS a", + "WHERE a.id IN (" + ids + ")", + "))", + "), -0.00001,'join=mitre') AS geom", + ") AS mergedgeom", + ].join(" "); + } + sequelize + .query(q, { + replacements: { + file_id: req.body.file_id, + }, + }) + .spread((results) => { + let oldIds = req.body.ids.map(function (id) { + return parseInt(id, 10); + }); + + let newIds = []; + + addLoop(0); + function addLoop(i) { + if (i >= results.length) { + pushToHistory( + Histories, + req.body.file_id, + newIds, + oldIds, + time, + null, + 6, + () => { + logger( + "info", + "Successfully merged " + + req.body.ids.length + + " features.", + req.originalUrl, + req + ); + res.send({ + status: "success", + message: + "Successfully merged " + + req.body.ids.length + + " features.", + body: { ids: newIds }, + }); + }, + (err) => { + logger( + "error", + "Merge failure.", + req.originalUrl, + req, + err + ); + res.send({ + status: "failure", + message: "Merge failure.", + body: {}, + }); + } + ); + return null; + } + let mergedFeature = JSON.parse(JSON.stringify(feature)); + mergedFeature.geom = JSON.parse(results[i].merged); + mergedFeature.geom.crs = { + type: "name", + properties: { name: "EPSG:4326" }, + }; + delete mergedFeature.id; + + Features.create(mergedFeature) + .then((created) => { + newIds.push(created.id); + addLoop(i + 1); + return null; + }) + .catch((err) => { + addLoop(i + 1); + return null; + //failureCallback(); + }); + } + }); + }); + } + }); +}); + +/** + * split features + * { + * file_id: (required) + * split: - feature to split against (required) + * ids: - of all the ids to perform the split against + * } + */ +router.post("/split", function (req, res, next) { + let Files = req.body.test === "true" ? UserfilesTEST : Userfiles; + let Features = req.body.test === "true" ? UserfeaturesTEST : Userfeatures; + let Histories = req.body.test === "true" ? FilehistoriesTEST : Filehistories; + + let time = Math.floor(Date.now()); + + let groups = []; + if (req.groups) groups = Object.keys(req.groups); + + let splitFeature = JSON.parse(req.body.split); + + ///Check that the provided file_id is an id that belongs to the current user + Files.findOne({ + where: { + id: req.body.file_id, + [Sequelize.Op.or]: { + file_owner: req.user, + [Sequelize.Op.and]: { + file_owner: "group", + file_owner_group: { [Sequelize.Op.overlap]: groups }, + }, + }, + }, + }) + .then((file) => { + if (!file) { + logger("error", "Failed to access file.", req.originalUrl, req); + res.send({ + status: "failure", + message: "Failed to access file.", + body: {}, + }); + } else { + let ids = req.body.ids; + ids = ids.join(","); + ids = ids || "NULL"; + + let geom = splitFeature.geometry; + geom.crs = { type: "name", properties: { name: "EPSG:4326" } }; + + let q = [ + "SELECT g.id, g.file_id, g.level, g.intent, g.properties, ST_SPLIT(ST_SetSRID(g.geom, 4326), ST_GeomFromGeoJSON(:geom)) FROM", + "(", + "SELECT id, file_id, level, intent, properties, geom", + "FROM user_features AS a", + "WHERE a.id IN (" + ids + ") AND a.file_id = :file_id", + ") AS g", + ].join(" "); + sequelize + .query(q, { + replacements: { + file_id: parseInt(req.body.file_id), + geom: JSON.stringify(geom), + }, + }) + .spread((results) => { + //reformat results + let r = []; + for (var i = 0; i < results.length; i++) { + for (var j = 0; j < results[i].st_split.geometries.length; j++) { + //If the length is 1, then no split occurred + if (results[i].st_split.geometries.length > 1) + r.push({ + file_id: results[i].file_id, + intent: results[i].intent, + level: results[i].level, + properties: results[i].properties, + geom: results[i].st_split.geometries[j], + }); + } + } + + let oldIds = req.body.ids.map(function (id) { + return parseInt(id, 10); + }); + + let newIds = []; + addLoop(0); + function addLoop(i) { + if (i >= r.length) { + pushToHistory( + Histories, + req.body.file_id, + newIds, + oldIds, + time, + null, + 8, + () => { + res.send({ + status: "success", + message: + "Successfully split " + + req.body.ids.length + + " features.", + body: { ids: newIds }, + }); + }, + (err) => { + logger( + "error", + "Split failure.", + req.originalUrl, + req, + err + ); + res.send({ + status: "failure", + message: "Split failure.", + body: {}, + }); + } + ); + return null; + } + + Features.create(r[i]) + .then((created) => { + newIds.push(created.id); + addLoop(i + 1); + return null; + }) + .catch((err) => { + console.log(err); + addLoop(i + 1); + return null; + }); + } + + return null; + }) + .catch((err) => { + logger("error", "Failed to split.", req.originalUrl, req, err); + res.send({ + status: "failure", + message: "Failed to split.", + body: {}, + }); + + return null; + }); + } + return null; + }) + .catch((err) => { + logger("error", "Failed to split.", req.originalUrl, req, err); + res.send({ + status: "failure", + message: "Failed to split.", + body: {}, + }); + + return null; + }); +}); + +router.post("/replace", function (req, res, next) { + res.send("test draw replace"); +}); + +router.post("/sendtofront", function (req, res, next) { + res.send("test draw front"); +}); + +router.post("/sendtoback", function (req, res, next) { + res.send("test draw back"); +}); + +/** + * Clears out testing tables + */ +router.post("/clear_test", function (req, res, next) { + sequelize + .query('TRUNCATE TABLE "user_features_tests" RESTART IDENTITY') + .then(() => { + sequelize + .query('TRUNCATE TABLE "publisheds_tests" RESTART IDENTITY') + .then(() => { + sequelize + .query('TRUNCATE TABLE "file_histories_tests" RESTART IDENTITY') + .then(() => { + sequelize + .query('TRUNCATE TABLE "user_files_tests" RESTART IDENTITY') + .then(() => { + //Add back sel files + makeMasterFilesTEST(req.leadGroupName, () => { + logger( + "info", + "Successfully cleared test tables.", + req.originalUrl, + req + ); + res.send({ + status: "success", + message: "Successfully cleared tables.", + body: {}, + }); + + return null; + }); + + return null; + }) + .catch((err) => { + logger( + "error", + "Failed to clear 1 test table. (C)", + req.originalUrl, + req, + err + ); + res.send({ + status: "failure", + message: "Failed to clear 1 table.", + body: {}, + }); + + return null; + }); + return null; + }) + .catch((err) => { + logger( + "error", + "Failed to clear 1 test table. (B)", + req.originalUrl, + req, + err + ); + res.send({ + status: "failure", + message: "Failed to clear 1 table.", + body: {}, + }); + return null; + }); + + return null; + }) + .catch((err) => { + logger( + "error", + "Failed to clear 1 test table. (A)", + req.originalUrl, + req, + err + ); + res.send({ + status: "failure", + message: "Failed to clear 1 table.", + body: {}, + }); + return null; + }); + + return null; + }) + .catch((err) => { + logger( + "error", + "Failed to clear both test tables.", + req.originalUrl, + req, + err + ); + res.send({ + status: "failure", + message: "Failed to clear both tables", + body: {}, + }); + + return null; + }); +}); + +const makeMasterFilesTEST = (leadGroupName, callback) => { + let intents = ["roi", "campaign", "campsite", "trail", "signpost"]; + makeMasterFileTEST(0, UserfilesTEST); + + function makeMasterFileTEST(i, Table) { + let intent = intents[i]; + if (intent == null) { + callback(); + return null; + } + + Table.findOrCreate({ + where: { + file_owner: "group", + file_owner_group: [leadGroupName], + file_name: intent.toUpperCase(), + file_description: "Lead composed " + intent.toUpperCase() + "s.", + is_master: true, + intent: intent, + public: "1", + hidden: "0", + }, + }).spread(function (userResult, created) { + makeMasterFileTEST(i + 1, Table); + return null; + }); + + return null; + } +}; + +module.exports = { router, add }; diff --git a/API/Backend/Draw/routes/files.js b/API/Backend/Draw/routes/files.js index 182e699d..79c9baf2 100644 --- a/API/Backend/Draw/routes/files.js +++ b/API/Backend/Draw/routes/files.js @@ -1,1660 +1,1656 @@ -const express = require("express"); -const logger = require("../../../logger"); -const database = require("../../../database"); -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; -const makeMasterFiles = ufiles.makeMasterFiles; -const ufeatures = require("../models/userfeatures"); -const Userfeatures = ufeatures.Userfeatures; -const UserfeaturesTEST = ufeatures.UserfeaturesTEST; -const published = require("../models/published"); -const Published = published.Published; -const PublishedTEST = published.PublishedTEST; -const PublishedStore = require("../models/publishedstore"); - -const draw = require("./draw"); - -const router = express.Router(); -const db = database.db; - -const historyKey = { - 0: "Add", - 1: "Edit", - 2: "Delete", - 3: "Undo", - 4: "Publish", - 5: "Add (over)", - 6: "Merge", - 7: "Add (under)", - 8: "Split" -}; - -router.post("/", function(req, res, next) { - res.send("test files"); -}); - -/** - * Gets all owned or public files - */ -router.post("/getfiles", function(req, res, next) { - let Table = req.body.test === "true" ? UserfilesTEST : Userfiles; - - Table.findAll({ - where: { - //file_owner is req.user or public is '0' - hidden: "0", - [Sequelize.Op.or]: { - file_owner: req.user, - public: "1" - } - } - }) - .then(files => { - if (!files) { - res.send({ - status: "failure", - message: "Failed to get files.", - body: {} - }); - } else { - files.sort((a, b) => (a.id > b.id ? 1 : -1)); - res.send({ - status: "success", - message: "Successfully got files.", - body: files - }); - } - }) - .catch(err => { - logger("error", "Failed to get files.", req.originalUrl, req, err); - res.send({ - status: "failure", - message: "Failed to get files.", - body: {} - }); - }); -}); - -/** - * Returns a geojson of a file - * { - * id: (required) - * time: (optional) - * 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 - ? " 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: {} - }); - }); - } -}); - -/** - * Makes a new file - * { - * file_owner: (required) - * file_name: (required) - * file_description: (optional) - * intent: (optional) - * geojson: (optional) -- geojson to initialize file from - * } - */ -router.post("/make", function(req, res, next) { - let Table = req.body.test === "true" ? UserfilesTEST : Userfiles; - - //group is a reserved keyword - if (req.user === "group") { - logger( - "error", - 'Failed to make a new file. Owner can\'t be "group".', - req.originalUrl, - req - ); - res.send({ - status: "failure", - message: 'Failed to make a new file. Owner can\'t be "group".', - body: {} - }); - return; - } - - let time = Math.floor(Date.now()); - - let newUserfile = { - file_owner: req.user, - file_name: req.body.file_name, - file_description: req.body.file_description, - intent: req.body.intent, - public: "1", - hidden: "0" - }; - - // Insert new userfile into the user_files table - Table.create(newUserfile) - .then(created => { - let geojson = req.body.geojson ? JSON.parse(req.body.geojson) : null; - if ( - geojson && - geojson.features && - geojson.features.length > 0 && - req.body.test !== "true" - ) { - let features = geojson.features; - - let rows = []; - for (var i = 0; i < features.length; i++) { - let intent = null; - if ( - features[i].properties && - features[i].properties._ && - features[i].properties._.intent - ) - intent = features[i].properties._.intent; - else { - switch (features[i].geometry.type.toLowerCase()) { - case "point": - case "multipoint": - intent = "point"; - break; - case "linestring": - case "multilinestring": - intent = "line"; - break; - default: - intent = "polygon"; - break; - } - if ( - features[i].properties && - features[i].properties.arrow === true - ) { - intent = "arrow"; - } - if ( - features[i].properties && - features[i].properties.annotation === true - ) { - intent = "note"; - } - } - let geom = features[i].geometry; - geom.crs = { type: "name", properties: { name: "EPSG:4326" } }; - - rows.push({ - file_id: created.id, - level: "0", - intent: intent, - elevated: "0", - properties: JSON.stringify(features[i].properties), - geom: geom - }); - } - - Userfeatures.bulkCreate(rows, { returning: true }) - .then(function(response) { - let ids = []; - for (let i = 0; i < response.length; i++) { - ids.push(response[i].id); - } - Filehistories.findAll({ - where: { - file_id: created.id - } - }) - .then(histories => { - let maxHistoryId = -Infinity; - if (histories && histories.length > 0) { - for (let i = 0; i < histories.length; i++) { - maxHistoryId = Math.max( - histories[i].history_id, - maxHistoryId - ); - } - return { - historyIndex: maxHistoryId + 1, - history: histories[maxHistoryId].history - }; - } else return { historyIndex: 0, history: [] }; - }) - .then(historyObj => { - let history = historyObj.history.concat(ids); - let newHistoryEntry = { - file_id: created.id, - history_id: historyObj.historyIndex, - time: time, - action_index: 0, - history: history - }; - // Insert new entry into the history table - Filehistories.create(newHistoryEntry) - .then(created => { - res.send({ - status: "success", - message: "Successfully made a new file from geojson.", - body: {} - }); - return null; - }) - .catch(err => { - logger( - "error", - "Upload GeoJSON but failed to update history!", - req.originalUrl, - req, - err - ); - res.send({ - status: "failure", - message: "Upload GeoJSON but failed to update history!", - body: {} - }); - }); - - return null; - }); - return null; - }) - .catch(function(err) { - logger( - "error", - "Failed to upload GeoJSON!", - req.originalUrl, - req, - err - ); - res.send({ - status: "failure", - message: "Failed to upload GeoJSON!", - body: {} - }); - return null; - }); - } else { - res.send({ - status: "success", - message: "Successfully made a new file.", - body: {} - }); - } - - return null; - }) - .catch(err => { - logger("error", "Failed to make a new file.", req.originalUrl, req, err); - res.send({ - status: "failure", - message: "Failed to make a new file.", - body: {} - }); - }); -}); - -/** - * Removes/Hides a file - * { - * id: (required) - * } - */ -router.post("/remove", function(req, res, next) { - let Table = req.body.test === "true" ? UserfilesTEST : Userfiles; - - Table.update( - { - hidden: "1" - }, - { - where: { - id: req.body.id, - file_owner: req.user - } - } - ) - .then(() => { - res.send({ - status: "success", - message: "File removed.", - body: {} - }); - - return null; - }) - .catch(err => { - logger( - "error", - "Failed to find and remove file.", - req.originalUrl, - req, - err - ); - res.send({ - status: "failure", - message: "Failed to find and remove file.", - body: {} - }); - }); -}); - -/** - * Restores/Unhides a file - * { - * id: (required) - * } - */ -router.post("/restore", function(req, res, next) { - let Table = req.body.test === "true" ? UserfilesTEST : Userfiles; - - Table.update( - { - hidden: "0" - }, - { - where: { - id: req.body.id, - file_owner: req.user - } - } - ) - .then(() => { - res.send({ - status: "success", - message: "File restored.", - body: {} - }); - - return null; - }) - .catch(err => { - logger( - "error", - "Failed to find and restore file.", - req.originalUrl, - req, - err - ); - res.send({ - status: "failure", - message: "Failed to find and restore file.", - body: {} - }); - }); -}); - -/** - * Update a file's name and/or description - * { - * id: - * file_name: (optional) - * file_description: (optional) - * public: <0|1> (optional) - * } - */ -router.post("/change", function(req, res, next) { - let Table = req.body.test === "true" ? UserfilesTEST : Userfiles; - - //Form update object - let toUpdateTo = {}; - if (req.body.hasOwnProperty("file_name") && req.body.file_name != null) { - toUpdateTo.file_name = req.body.file_name; - } - if ( - req.body.hasOwnProperty("file_description") && - req.body.file_description != null - ) { - toUpdateTo.file_description = req.body.file_description; - } - if ( - req.body.hasOwnProperty("public") && - (req.body.public == 0 || req.body.public == 1) - ) { - toUpdateTo.public = req.body.public; - } - - Table.update(toUpdateTo, { - where: { - id: req.body.id, - file_owner: req.user, - is_master: false //No editing these - } - }) - .then(() => { - res.send({ - status: "success", - message: "File edited.", - body: {} - }); - - return null; - }) - .catch(err => { - logger("error", "Failed to edit file.", req.originalUrl, req, err); - res.send({ - status: "failure", - message: "Failed to edit file.", - body: {} - }); - }); -}); - -/** - * compile sel file - * { - * time: int - * verbose: bool - * test: bool - * } - */ -const compile = function(req, res, callback) { - let Table = req.query.test === "true" ? UserfilesTEST : Userfiles; - - let atThisTime = req.query.time || Math.floor(Date.now()); - - Table.findAll({ - where: { - is_master: true, - intent: { - [Sequelize.Op.in]: ["roi", "campaign", "campsite", "trail", "signpost"] - } - } - }).then(files => { - let featureIds = []; - let finished = 0; - for (let f = 0; f < files.length; f++) { - sequelize - .query( - "SELECT history" + - " " + - "FROM file_histories" + - (req.query.test === "true" ? "_tests" : "") + - " " + - "WHERE file_id=" + - files[f].dataValues.id + - " " + - "AND time<=" + - atThisTime + - " " + - "ORDER BY time DESC" + - " " + - "FETCH first 1 rows only" - ) - .spread(results => { - let bestHistory = results.length > 0 ? results[0].history : []; - featureIds = featureIds.concat(bestHistory); - finished++; - tryProcessFeatures(finished); - }); - } - function tryProcessFeatures(finished) { - if (finished == files.length) { - featureIds = featureIds.join(",") || "NULL"; - //get all features - sequelize - .query( - "SELECT " + - "id, file_id, level, intent, properties, ST_AsGeoJSON(geom)" + - " " + - "FROM user_features" + - (req.query.test === "true" ? "_tests" : "") + - " " + - "WHERE id IN (" + - featureIds + - ")" - ) - .spread(features => { - processFeatures(features); - }); - } - } - function processFeatures(features) { - sequelize - .query( - "SELECT" + - " " + - '\'intersects\' as "association", a.id, a.intent, b.id AS "associated_id", b.intent AS "associated_intent", b.properties AS "associated_properties"' + - " " + - "FROM user_features" + - (req.query.test === "true" ? "_tests" : "") + - " a," + - " " + - "user_features" + - (req.query.test === "true" ? "_tests" : "") + - " b" + - " " + - "WHERE a.id IN (" + - featureIds + - ")" + - " " + - "AND b.id IN (" + - featureIds + - ")" + - " " + - "AND a.id != b.id" + - " " + - "AND ((ST_OVERLAPS(a.geom, b.geom)" + - " " + - "AND NOT ST_Touches(a.geom, b.geom))" + - " " + - "OR ST_CROSSES(a.geom, b.geom))" + - " " + - "UNION ALL" + - " " + - "SELECT" + - " " + - '\'contains\' as "association", a.id, a.intent, b.id AS "associated_id", b.intent AS "associated_intent", b.properties AS "associated_properties"' + - " " + - "FROM user_features" + - (req.query.test === "true" ? "_tests" : "") + - " a," + - " " + - "user_features" + - (req.query.test === "true" ? "_tests" : "") + - " b" + - " " + - "WHERE a.id IN (" + - featureIds + - ")" + - " " + - "AND b.id IN (" + - featureIds + - ")" + - " " + - "AND a.id != b.id" + - " " + - "AND ST_Contains(a.geom, b.geom)" - ) - .spread(results => { - let hierarchy = []; - let intentOrder = ["roi", "campaign", "campsite", "signpost"]; - let flatHierarchy = []; - let issues = []; - let changes = []; - - //Get all immediate children of everything - for (let f = 0; f < features.length; f++) { - let intersects = []; - let contains = []; - let children = []; - for (let r = 0; r < results.length; r++) { - if (results[r].id == features[f].id) { - let childProps = JSON.parse(results[r].associated_properties); - if (results[r].association === "intersects") { - intersects.push({ - name: childProps.name, - uuid: childProps.uuid, - id: results[r].associated_id, - intent: results[r].associated_intent - }); - } else if (results[r].association === "contains") { - contains.push({ - name: childProps.name, - uuid: childProps.uuid, - id: results[r].associated_id, - intent: results[r].associated_intent - }); - children.push({ - name: childProps.name, - uuid: childProps.uuid, - id: results[r].associated_id, - intent: results[r].associated_intent - }); - } - } - } - let featureProps = JSON.parse(features[f].properties); - flatHierarchy.push({ - feature: features[f], - id: features[f].id, - name: featureProps.name, - uuid: featureProps.uuid, - intent: features[f].intent, - children: children, - possibleChildren: { - intersects: intersects, - contains: contains, - directIntersects: [] - } - }); - } - //Now attach parents to flatHierarchy - for (let i = 0; i < flatHierarchy.length; i++) { - flatHierarchy[i].parent = {}; - flatHierarchy[i].possibleParents = []; - for (let j = 0; j < flatHierarchy.length; j++) { - if (i != j) { - for ( - let k = 0; - k < flatHierarchy[j].possibleChildren.contains.length; - k++ - ) { - if ( - flatHierarchy[i].id == - flatHierarchy[j].possibleChildren.contains[k].id - ) { - flatHierarchy[i].possibleParents.push({ - name: flatHierarchy[j].name, - uuid: flatHierarchy[j].uuid, - id: flatHierarchy[j].id, - intent: flatHierarchy[j].intent - }); - } - } - } - } - } - removeIndirectChildren(); - function removeIndirectChildren() { - for (let i = 0; i < flatHierarchy.length; i++) { - let node = flatHierarchy[i]; - let intent = node.intent; - if (intentOrder.indexOf(intent) === -1) continue; - let associationIntent = - intentOrder[intentOrder.indexOf(intent) + 1]; - if (associationIntent == null) { - node.children = []; - } else { - for (let j = node.children.length - 1; j >= 0; j--) { - if (node.children[j].intent != associationIntent) { - node.children.splice(j, 1); - } - } - node.possibleChildren.directIntersects = JSON.parse( - JSON.stringify(node.possibleChildren.intersects) - ); - for ( - let i = node.possibleChildren.directIntersects.length - 1; - i >= 0; - i-- - ) - if ( - node.possibleChildren.directIntersects[i].intent != - associationIntent && - node.possibleChildren.directIntersects[i].intent != intent - ) - node.possibleChildren.directIntersects.splice(i, 1); - } - } - } - addParents(); - function addParents() { - for (let i = 0; i < flatHierarchy.length; i++) { - for (let j = 0; j < flatHierarchy[i].children.length; j++) { - //Each child - //Iterate back through to child and add this flatHierarchy[i] as parent - for (let k = 0; k < flatHierarchy.length; k++) - if (flatHierarchy[k].id === flatHierarchy[i].children[j].id) - flatHierarchy[k].parent = { - name: flatHierarchy[i].name, - uuid: flatHierarchy[i].uuid, - id: flatHierarchy[i].id, - intent: flatHierarchy[i].intent - }; - } - - //If no parents at this point try to find the best possible parent - if ( - Object.keys(flatHierarchy[i].parent).length === 0 && - flatHierarchy[i].possibleParents.length > 0 - ) { - let intentOrderReversed = JSON.parse( - JSON.stringify(intentOrder) - ); - intentOrderReversed.reverse(); - let intentId = intentOrderReversed.indexOf( - flatHierarchy[i].intent - ); - if (intentId != -1) { - for ( - let l = intentId + 1; - l < intentOrderReversed.length; - l++ - ) { - for ( - let m = 0; - m < flatHierarchy[i].possibleParents.length; - m++ - ) { - if ( - Object.keys(flatHierarchy[i].parent).length === 0 && - flatHierarchy[i].possibleParents[m].intent === - intentOrderReversed[l] - ) { - flatHierarchy[i].parent = - flatHierarchy[i].possibleParents[m]; - } - } - } - } - } - } - } - - //Build the root of the trees - for (let f = 0; f < features.length; f++) { - let isCovered = false; - for (let r = 0; r < results.length; r++) { - if ( - results[r].association === "contains" && - results[r].associated_id == features[f].id - ) { - isCovered = true; - break; - } - } - if (!isCovered) { - let featureProps = JSON.parse(features[f].properties); - hierarchy.push({ - intent: features[f].intent, - id: features[f].id, - name: featureProps.name, - uuid: featureProps.uuid, - children: { - intersects: [], - contains: [] - } - }); - continue; - } - } - - //From those roots do a depth traversal, adding the flat children each time - depthTraversal(hierarchy, 0); - function depthTraversal(node, depth) { - for (var i = 0; i < node.length; i++) { - //Add other feature information while we're at it - addFeatureData(node[i], depth); - - addRelationships(node[i]); - if (node[i].children.length > 0) - depthTraversal(node[i].children, depth + 1); - } - } - function addRelationships(node) { - for (let i = 0; i < flatHierarchy.length; i++) - if (node.id == flatHierarchy[i].id) { - node.parent = JSON.parse( - JSON.stringify(flatHierarchy[i].parent) - ); - node.children = JSON.parse( - JSON.stringify(flatHierarchy[i].children) - ); - return; - } - } - function addFeatureData(node, depth) { - for (let i = 0; i < features.length; i++) { - let f = features[i]; - if (node.id == f.id) { - let properties = JSON.parse(f.properties); - let feature = {}; - properties._ = { - id: f.id, - file_id: f.file_id, - level: f.level, - intent: f.intent - }; - feature.type = "Feature"; - feature.properties = properties; - feature.geometry = JSON.parse(f.st_asgeojson); - //id, file_id, level, intent, properties, ST_AsGeoJSON(geom)' + ' ' + - node.file_id = f.file_id; - node.level = f.level; - node.depth = depth; - node.intent = f.intent; - node.name = properties.name; - node.uuid = properties.uuid; - node.properties = JSON.parse(f.properties); - node.geometry = JSON.parse(f.st_asgeojson); - node.feature = feature; - return; - } - } - } - - let saviors = {}; - //Not always do all features fit in the hierarchy at this point, one last chance to fit them in - addOutcasts(); - function addOutcasts() { - let includedIds = []; - let allIds = []; - let outcastIds = []; - - //populate includedIds - depthTraversalA(hierarchy, 0); - function depthTraversalA(node, depth) { - for (let i = 0; i < node.length; i++) { - includedIds.push(node[i].id); - if (node[i].children.length > 0) { - depthTraversalA(node[i].children, depth + 1); - } - } - } - - //populate allIds - for (let i = 0; i < flatHierarchy.length; i++) { - allIds.push(flatHierarchy[i].id); - } - - //populate outcasts - for (let i = 0; i < allIds.length; i++) { - if (includedIds.indexOf(allIds[i]) == -1) - outcastIds.push(allIds[i]); - } - - // parentId: child - //let saviors = {} - for (let i = 0; i < flatHierarchy.length; i++) { - if (outcastIds.indexOf(flatHierarchy[i].id) != -1) { - if ( - flatHierarchy[i].parent && - flatHierarchy[i].parent.id != null - ) { - let outcast = JSON.parse(JSON.stringify(flatHierarchy[i])); - saviors[flatHierarchy[i].parent.id] = outcast; - } - } - } - - //The Saviorng - depthTraversalB(hierarchy, 0); - function depthTraversalB(node, depth) { - for (let i = 0; i < node.length; i++) { - if (saviors[node[i].id] != null) { - node[i].children = Array.isArray(node[i].children) - ? node[i].children - : []; - for (let j = 0; j < features.length; j++) { - let f = features[j]; - if (saviors[node[i].id].id == f.id) { - let outcast = {}; - let properties = JSON.parse(f.properties); - let feature = {}; - properties._ = { - id: f.id, - file_id: f.file_id, - level: f.level, - intent: f.intent - }; - feature.type = "Feature"; - feature.properties = properties; - feature.geometry = JSON.parse(f.st_asgeojson); - - outcast.name = properties.name; - outcast.uuid = properties.uuid; - outcast.id = f.id; - outcast.intent = f.intent; - outcast.file_id = f.file_id; - outcast.level = f.level; - outcast.depth = depth + 1; - outcast.properties = JSON.parse(f.properties); - outcast.geometry = JSON.parse(f.st_asgeojson); - outcast.feature = feature; - outcast.children = saviors[node[i].id] || []; - outcast.parent = saviors[node[i].id].parent || {}; - node[i].children.push(outcast); - } - } - } - if (node[i].children && node[i].children.length > 0) { - depthTraversalB(node[i].children, depth + 1); - } - } - } - } - - findIssues(); - function findIssues() { - let uuidsFound = {}; - let namesFound = {}; - - for (let i = 0; i < flatHierarchy.length; i++) { - let node = flatHierarchy[i]; - let intent = node.intent; - let props = JSON.parse(node.feature.properties); - - //Check for duplicate uuids - if (props.uuid == null) { - issues.push({ - severity: "error", - antecedent: { - id: node.id, - intent: node.intent - }, - message: "{antecedent} is missing a uuid." - }); - } else { - let uuidKeys = Object.keys(uuidsFound); - let uuidI = uuidKeys.indexOf(props.uuid); - if (uuidI >= 0) { - issues.push({ - severity: "error", - antecedent: { - id: node.id, - intent: node.intent - }, - message: "{antecedent} has the same uuid as {consequent}", - consequent: { - id: uuidsFound[uuidKeys[uuidI]].id, - intent: uuidsFound[uuidKeys[uuidI]].intent - } - }); - } else { - uuidsFound[props.uuid] = { - id: node.id, - intent: node.intent - }; - } - } - - //Check for duplicate names - if (props.name == null) { - issues.push({ - severity: "error", - antecedent: { - id: node.id, - intent: node.intent - }, - message: "{antecedent} is missing a name." - }); - } else { - let nameKeys = Object.keys(namesFound); - let nameI = nameKeys.indexOf(props.name); - if (nameI >= 0) { - issues.push({ - severity: "error", - antecedent: { - id: node.id, - intent: node.intent - }, - message: "{antecedent} has the same name as {consequent}", - consequent: { - id: namesFound[nameKeys[nameI]].id, - intent: namesFound[nameKeys[nameI]].intent - } - }); - } else { - namesFound[props.name] = { - id: node.id, - intent: node.intent - }; - } - } - - if (intentOrder.indexOf(intent) === -1) continue; - let parentIntent = intentOrder[intentOrder.indexOf(intent) - 1]; - if (parentIntent != null && intent != "signpost") { - //Check that it has a valid parent - if (node.parent.intent != parentIntent) { - issues.push({ - severity: "error", - antecedent: { - id: node.id, - intent: node.intent - }, - message: - "{antecedent} does not have a parent of type: " + - parentIntent + - "." - }); - } else if (Object.keys(node.parent).length === 0) { - issues.push({ - severity: "error", - antecedent: { - id: node.id, - intent: node.intent - }, - message: "{antecedent} does not have a parent." - }); - } - } - - let ints = node.possibleChildren.directIntersects; - for (let j = 0; j < ints.length; j++) { - if (node.intent == "trail") { - } else if (node.intent != ints[j].intent) - issues.push({ - severity: "error", - antecedent: { - id: node.id, - intent: node.intent - }, - message: - "{antecedent} does not fully contain possible child {consequent}", - consequent: { - id: ints[j].id, - intent: ints[j].intent - } - }); - else - issues.push({ - severity: "error", - antecedent: { - id: node.id, - intent: node.intent - }, - message: - "{antecedent} intersects {consequent} of same intent.", - consequent: { - id: ints[j].id, - intent: ints[j].intent - } - }); - } - } - } - - function findChanges(cb) { - //Get published_family_tree from our store - sequelize - .query( - "SELECT value" + - " " + - "FROM published_stores" + - " " + - "WHERE time<=:time" + - " " + - "ORDER BY time DESC" + - " " + - "FETCH first 1 rows only", - { - replacements: { - time: Math.floor(Date.now()) - } - } - ) - .spread(published_family_tree => { - if ( - !published_family_tree || - !published_family_tree[0] || - !published_family_tree[0].value - ) { - cb(false); - return; - } else { - let tree = JSON.parse(published_family_tree[0].value); - let fh = tree.flatHierarchy; - let oldFeatures = {}; - let newFeatures = {}; - let added = []; - let changed = []; - let removed = []; - - //Find all the old and new uuids and names first - for (let i = 0; i < fh.length; i++) { - let node = fh[i]; - let props = JSON.parse(node.feature.properties); - oldFeatures[props.uuid] = { name: props.name, id: node.id }; - } - for (let i = 0; i < flatHierarchy.length; i++) { - let node = flatHierarchy[i]; - let props = JSON.parse(node.feature.properties); - newFeatures[props.uuid] = { name: props.name, id: node.id }; - } - let newFeatureUUIDs = Object.keys(newFeatures); - let oldFeatureUUIDs = Object.keys(oldFeatures); - - //Added - for (let i = 0; i < newFeatureUUIDs.length; i++) { - if (oldFeatureUUIDs.indexOf(newFeatureUUIDs[i]) == -1) - added.push({ - uuid: newFeatureUUIDs[i], - name: newFeatures[newFeatureUUIDs[i]].name, - id: newFeatures[newFeatureUUIDs[i]].id - }); - } - //Removed - for (let i = 0; i < oldFeatureUUIDs.length; i++) { - if (newFeatureUUIDs.indexOf(oldFeatureUUIDs[i]) == -1) - removed.push({ - uuid: oldFeatureUUIDs[i], - name: oldFeatures[oldFeatureUUIDs[i]].name, - id: oldFeatures[oldFeatureUUIDs[i]].id - }); - } - //Changed - for (let i = 0; i < newFeatureUUIDs.length; i++) { - if (oldFeatureUUIDs.indexOf(newFeatureUUIDs[i]) != -1) { - if ( - oldFeatures[newFeatureUUIDs[i]].name != - newFeatures[newFeatureUUIDs[i]].name - ) { - changed.push({ - uuid: newFeatureUUIDs[i], - old_name: oldFeatures[newFeatureUUIDs[i]].name, - new_name: newFeatures[newFeatureUUIDs[i]].name, - id: newFeatures[newFeatureUUIDs[i]].id - }); - } - } - } - - cb({ added, changed, removed }); - } - }); - } - - findChanges(function(changes) { - let body = { - hierarchy: hierarchy, - issues: issues, - changes: changes - }; - if (req.query.verbose) { - body = { - hierarchy: hierarchy, - flatHierarchy: flatHierarchy, - issues: issues, - changes: changes, - saviors: saviors - }; - } - callback(body); - }); - }); - } - }); -}; -router.get("/compile", function(req, res, next) { - compile(req, res, body => { - if (body == null) { - logger("error", "Failed compile file.", req.originalUrl, req); - } - res.send({ - status: body != null ? "success" : "failed", - message: "File compiled.", - body: body - }); - }); -}); - -/** - * publish sel file - * { - * } - */ -router.post("/publish", function(req, res, next) { - let Table = req.body.test === "true" ? UserfilesTEST : Userfiles; - let Histories = req.body.test === "true" ? FilehistoriesTEST : Filehistories; - - let time = Math.floor(Date.now()); - - //Check that user belongs to sel group - if (req.groups[req.leadGroupName] != true) { - logger("info", "Unauthorized to publish.", req.originalUrl, req); - res.send({ - status: "failure", - message: "Unauthorized to publish.", - body: {} - }); - return null; - } - - let groups = []; - if (req.groups) groups = Object.keys(req.groups); - - Table.findAll({ - where: { - is_master: true, - [Sequelize.Op.or]: { - file_owner: req.user, - [Sequelize.Op.and]: { - file_owner: "group", - file_owner_group: { [Sequelize.Op.overlap]: groups } - } - } - } - }).then(files => { - publishToPublished(function(pass, message) { - if (pass) { - for (let f = 0; f < files.length; f++) { - publishToHistory( - Histories, - files[f].dataValues.id, - time, - () => { - if (f === files.length - 1) { - res.send({ - status: "success", - message: "Published.", - body: {} - }); - } - }, - err => { - logger("error", "Failed to publish.", req.originalUrl, req, err); - res.send({ - status: "failure", - message: "Failed to publish.", - body: {} - }); - } - ); - } - } else { - logger("error", "Failed to publish. " + message, req.originalUrl, req); - res.send({ - status: "failure", - message: "Failed to publish." + message, - body: {} - }); - } - }); - }); - - function publishToHistory( - Table, - file_id, - time, - successCallback, - failureCallback - ) { - Table.findAll({ - where: { - file_id: file_id - } - }) - .then(histories => { - let maxHistoryId = -Infinity; - if (histories && histories.length > 0) { - for (let i = 0; i < histories.length; i++) { - maxHistoryId = Math.max(histories[i].history_id, maxHistoryId); - } - return { - historyIndex: maxHistoryId + 1, - history: histories[maxHistoryId].history - }; - } else return { historyIndex: 0, history: [] }; - }) - .then(historyObj => { - let newHistoryEntry = { - file_id: file_id, - history_id: historyObj.historyIndex, - time: time, - action_index: 4, - history: historyObj.history - }; - // Insert new entry into the history table - Table.create(newHistoryEntry) - .then(created => { - successCallback(newHistoryEntry); - return null; - }) - .catch(err => { - failureCallback(newHistoryEntry); - }); - return null; - }); - } - - function publishToPublished(cb) { - let Publisheds = req.body.test === "true" ? PublishedTEST : Published; - req.query.verbose = true; - compile(req, res, body => { - if (body.issues.length > 0) { - cb(false, " File has unresolved issues."); - } else if (req.body.test === "true") { - cb(true); - return null; - } else { - PublishedStore.create({ - name: "published_family_tree", - value: JSON.stringify(body), - time: time - }) - .then(() => { - Publisheds.destroy({ - where: {} - }).then(del => { - let fH = body.flatHierarchy; - - let rows = []; - for (let i = 0; i < fH.length; i++) { - let feature = { - id: fH[i].id, - intent: fH[i].intent, - parent: fH[i].parent.hasOwnProperty("id") - ? fH[i].parent.id - : null, - children: fH[i].children.map(v => { - return v.id; - }), - level: fH[i].feature.level, - properties: JSON.parse(fH[i].feature.properties), - geom: JSON.parse(fH[i].feature.st_asgeojson) - }; - delete feature.properties._; - feature.geom.crs = { - type: "name", - properties: { name: "EPSG:4326" } - }; - - rows.push(feature); - } - - Publisheds.bulkCreate(rows, { returning: true }) - .then(function(response) { - cb(true); - return null; - }) - .catch(function(error) { - cb(false); - return null; - }); - - return null; - }); - - return null; - }) - .catch(function(err) { - logger( - "error", - "Error adding published tree.", - req.originalUrl, - req, - err - ); - cb(false); - return null; - }); - } - }); - } -}); - -/** - * Get a file's history - * { - * id: - * } - */ -router.post("/gethistory", function(req, res, next) { - let Table = req.body.test === "true" ? FilehistoriesTEST : Filehistories; - - Table.findAll({ - where: { - file_id: req.body.id - } - }) - .then(histories => { - if (!histories) { - res.send({ - status: "failure", - message: "Failed to get history.", - body: {} - }); - } else { - //Newest first - histories.sort((a, b) => (a.history_id < b.history_id ? 1 : -1)); - for (let i = 0; i < histories.length; i++) - histories[i].dataValues.message = - historyKey[histories[i].dataValues.action_index]; - - res.send({ - status: "success", - message: "Successfully got history.", - body: histories - }); - } - - return null; - }) - .catch(err => { - logger("error", "Failed to get history.", req.originalUrl, req, err); - res.send({ - status: "failure", - message: "Failed to get history.", - body: {} - }); - }); -}); - -module.exports = { router, makeMasterFiles }; +const express = require("express"); +const logger = require("../../../logger"); +const database = require("../../../database"); +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; +const makeMasterFiles = ufiles.makeMasterFiles; +const ufeatures = require("../models/userfeatures"); +const Userfeatures = ufeatures.Userfeatures; +const UserfeaturesTEST = ufeatures.UserfeaturesTEST; +const published = require("../models/published"); +const Published = published.Published; +const PublishedTEST = published.PublishedTEST; +const PublishedStore = require("../models/publishedstore"); + +const draw = require("./draw"); + +const router = express.Router(); +const db = database.db; + +const historyKey = { + 0: "Add", + 1: "Edit", + 2: "Delete", + 3: "Undo", + 4: "Publish", + 5: "Add (over)", + 6: "Merge", + 7: "Add (under)", + 8: "Split", +}; + +router.post("/", function (req, res, next) { + res.send("test files"); +}); + +/** + * Gets all owned or public files + */ +router.post("/getfiles", function (req, res, next) { + let Table = req.body.test === "true" ? UserfilesTEST : Userfiles; + + Table.findAll({ + where: { + //file_owner is req.user or public is '0' + hidden: "0", + [Sequelize.Op.or]: { + file_owner: req.user, + public: "1", + }, + }, + }) + .then((files) => { + if (!files) { + res.send({ + status: "failure", + message: "Failed to get files.", + body: {}, + }); + } else { + files.sort((a, b) => (a.id > b.id ? 1 : -1)); + res.send({ + status: "success", + message: "Successfully got files.", + body: files, + }); + } + }) + .catch((err) => { + logger("error", "Failed to get files.", req.originalUrl, req, err); + res.send({ + status: "failure", + message: "Failed to get files.", + body: {}, + }); + }); +}); + +/** + * Returns a geojson of a file + * { + * id: (required) + * time: (optional) + * 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 + ? " 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: {}, + }); + }); + } +}); + +/** + * Makes a new file + * { + * file_owner: (required) + * file_name: (required) + * file_description: (optional) + * intent: (optional) + * geojson: (optional) -- geojson to initialize file from + * } + */ +router.post("/make", function (req, res, next) { + let Table = req.body.test === "true" ? UserfilesTEST : Userfiles; + + //group is a reserved keyword + if (req.user === "group") { + logger( + "error", + 'Failed to make a new file. Owner can\'t be "group".', + req.originalUrl, + req + ); + res.send({ + status: "failure", + message: 'Failed to make a new file. Owner can\'t be "group".', + body: {}, + }); + return; + } + + let time = Math.floor(Date.now()); + + let newUserfile = { + file_owner: req.user, + file_name: req.body.file_name, + file_description: req.body.file_description, + intent: req.body.intent, + public: "1", + hidden: "0", + }; + + // Insert new userfile into the user_files table + Table.create(newUserfile) + .then((created) => { + let geojson = req.body.geojson ? JSON.parse(req.body.geojson) : null; + if ( + geojson && + geojson.features && + geojson.features.length > 0 && + req.body.test !== "true" + ) { + let features = geojson.features; + + let rows = []; + for (var i = 0; i < features.length; i++) { + let intent = null; + if ( + features[i].properties && + features[i].properties._ && + features[i].properties._.intent + ) + intent = features[i].properties._.intent; + else { + switch (features[i].geometry.type.toLowerCase()) { + case "point": + case "multipoint": + intent = "point"; + break; + case "linestring": + case "multilinestring": + intent = "line"; + break; + default: + intent = "polygon"; + break; + } + if ( + features[i].properties && + features[i].properties.arrow === true + ) { + intent = "arrow"; + } + if ( + features[i].properties && + features[i].properties.annotation === true + ) { + intent = "note"; + } + } + let geom = features[i].geometry; + geom.crs = { type: "name", properties: { name: "EPSG:4326" } }; + + rows.push({ + file_id: created.id, + level: "0", + intent: intent, + elevated: "0", + properties: JSON.stringify(features[i].properties), + geom: geom, + }); + } + + Userfeatures.bulkCreate(rows, { returning: true }) + .then(function (response) { + let ids = []; + for (let i = 0; i < response.length; i++) { + ids.push(response[i].id); + } + Filehistories.findAll({ + limit: 1, + where: { + file_id: created.id, + }, + order: [["history_id", "DESC"]], + }) + .then((lastHistory) => { + if (lastHistory && lastHistory.length > 0) { + return { + historyIndex: lastHistory[0].history_id + 1, + history: lastHistory[0].history, + }; + } else return { historyIndex: 0, history: [] }; + }) + .then((historyObj) => { + let history = historyObj.history.concat(ids); + let newHistoryEntry = { + file_id: created.id, + history_id: historyObj.historyIndex, + time: time, + action_index: 0, + history: history, + }; + // Insert new entry into the history table + Filehistories.create(newHistoryEntry) + .then((created) => { + res.send({ + status: "success", + message: "Successfully made a new file from geojson.", + body: {}, + }); + return null; + }) + .catch((err) => { + logger( + "error", + "Upload GeoJSON but failed to update history!", + req.originalUrl, + req, + err + ); + res.send({ + status: "failure", + message: "Upload GeoJSON but failed to update history!", + body: {}, + }); + }); + + return null; + }); + return null; + }) + .catch(function (err) { + logger( + "error", + "Failed to upload GeoJSON!", + req.originalUrl, + req, + err + ); + res.send({ + status: "failure", + message: "Failed to upload GeoJSON!", + body: {}, + }); + return null; + }); + } else { + res.send({ + status: "success", + message: "Successfully made a new file.", + body: {}, + }); + } + + return null; + }) + .catch((err) => { + logger("error", "Failed to make a new file.", req.originalUrl, req, err); + res.send({ + status: "failure", + message: "Failed to make a new file.", + body: {}, + }); + }); +}); + +/** + * Removes/Hides a file + * { + * id: (required) + * } + */ +router.post("/remove", function (req, res, next) { + let Table = req.body.test === "true" ? UserfilesTEST : Userfiles; + + Table.update( + { + hidden: "1", + }, + { + where: { + id: req.body.id, + file_owner: req.user, + }, + } + ) + .then(() => { + res.send({ + status: "success", + message: "File removed.", + body: {}, + }); + + return null; + }) + .catch((err) => { + logger( + "error", + "Failed to find and remove file.", + req.originalUrl, + req, + err + ); + res.send({ + status: "failure", + message: "Failed to find and remove file.", + body: {}, + }); + }); +}); + +/** + * Restores/Unhides a file + * { + * id: (required) + * } + */ +router.post("/restore", function (req, res, next) { + let Table = req.body.test === "true" ? UserfilesTEST : Userfiles; + + Table.update( + { + hidden: "0", + }, + { + where: { + id: req.body.id, + file_owner: req.user, + }, + } + ) + .then(() => { + res.send({ + status: "success", + message: "File restored.", + body: {}, + }); + + return null; + }) + .catch((err) => { + logger( + "error", + "Failed to find and restore file.", + req.originalUrl, + req, + err + ); + res.send({ + status: "failure", + message: "Failed to find and restore file.", + body: {}, + }); + }); +}); + +/** + * Update a file's name and/or description + * { + * id: + * file_name: (optional) + * file_description: (optional) + * public: <0|1> (optional) + * } + */ +router.post("/change", function (req, res, next) { + let Table = req.body.test === "true" ? UserfilesTEST : Userfiles; + + //Form update object + let toUpdateTo = {}; + if (req.body.hasOwnProperty("file_name") && req.body.file_name != null) { + toUpdateTo.file_name = req.body.file_name; + } + if ( + req.body.hasOwnProperty("file_description") && + req.body.file_description != null + ) { + toUpdateTo.file_description = req.body.file_description; + } + if ( + req.body.hasOwnProperty("public") && + (req.body.public == 0 || req.body.public == 1) + ) { + toUpdateTo.public = req.body.public; + } + + Table.update(toUpdateTo, { + where: { + id: req.body.id, + file_owner: req.user, + is_master: false, //No editing these + }, + }) + .then(() => { + res.send({ + status: "success", + message: "File edited.", + body: {}, + }); + + return null; + }) + .catch((err) => { + logger("error", "Failed to edit file.", req.originalUrl, req, err); + res.send({ + status: "failure", + message: "Failed to edit file.", + body: {}, + }); + }); +}); + +/** + * compile sel file + * { + * time: int + * verbose: bool + * test: bool + * } + */ +const compile = function (req, res, callback) { + const isTest = req.query.test === "true" || req.body.test === "true"; + let Table = isTest ? UserfilesTEST : Userfiles; + + let atThisTime = req.query.time || Math.floor(Date.now()); + + Table.findAll({ + where: { + is_master: true, + intent: { + [Sequelize.Op.in]: ["roi", "campaign", "campsite", "trail", "signpost"], + }, + }, + }).then((files) => { + let featureIds = []; + let finished = 0; + for (let f = 0; f < files.length; f++) { + sequelize + .query( + "SELECT history" + + " " + + "FROM file_histories" + + (isTest ? "_tests" : "") + + " " + + "WHERE file_id=" + + files[f].dataValues.id + + " " + + "AND time<=" + + atThisTime + + " " + + "ORDER BY time DESC" + + " " + + "FETCH first 1 rows only" + ) + .spread((results) => { + let bestHistory = results.length > 0 ? results[0].history : []; + featureIds = featureIds.concat(bestHistory); + finished++; + tryProcessFeatures(finished); + }); + } + function tryProcessFeatures(finished) { + if (finished == files.length) { + featureIds = featureIds.join(",") || "NULL"; + //get all features + sequelize + .query( + "SELECT " + + "id, file_id, level, intent, properties, ST_AsGeoJSON(geom)" + + " " + + "FROM user_features" + + (isTest ? "_tests" : "") + + " " + + "WHERE id IN (" + + featureIds + + ")" + ) + .spread((features) => { + processFeatures(features); + }); + } + } + function processFeatures(features) { + sequelize + .query( + "SELECT" + + " " + + '\'intersects\' as "association", a.id, a.intent, b.id AS "associated_id", b.intent AS "associated_intent", b.properties AS "associated_properties"' + + " " + + "FROM user_features" + + (isTest ? "_tests" : "") + + " a," + + " " + + "user_features" + + (isTest ? "_tests" : "") + + " b" + + " " + + "WHERE a.id IN (" + + featureIds + + ")" + + " " + + "AND b.id IN (" + + featureIds + + ")" + + " " + + "AND a.id != b.id" + + " " + + "AND ((ST_OVERLAPS(a.geom, b.geom)" + + " " + + "AND NOT ST_Touches(a.geom, b.geom))" + + " " + + "OR ST_CROSSES(a.geom, b.geom))" + + " " + + "UNION ALL" + + " " + + "SELECT" + + " " + + '\'contains\' as "association", a.id, a.intent, b.id AS "associated_id", b.intent AS "associated_intent", b.properties AS "associated_properties"' + + " " + + "FROM user_features" + + (isTest ? "_tests" : "") + + " a," + + " " + + "user_features" + + (isTest ? "_tests" : "") + + " b" + + " " + + "WHERE a.id IN (" + + featureIds + + ")" + + " " + + "AND b.id IN (" + + featureIds + + ")" + + " " + + "AND a.id != b.id" + + " " + + "AND ST_Contains(a.geom, b.geom)" + ) + .spread((results) => { + let hierarchy = []; + let intentOrder = ["roi", "campaign", "campsite", "signpost"]; + let flatHierarchy = []; + let issues = []; + let changes = []; + + //Get all immediate children of everything + for (let f = 0; f < features.length; f++) { + let intersects = []; + let contains = []; + let children = []; + for (let r = 0; r < results.length; r++) { + if (results[r].id == features[f].id) { + let childProps = JSON.parse(results[r].associated_properties); + if (results[r].association === "intersects") { + intersects.push({ + name: childProps.name, + uuid: childProps.uuid, + id: results[r].associated_id, + intent: results[r].associated_intent, + }); + } else if (results[r].association === "contains") { + contains.push({ + name: childProps.name, + uuid: childProps.uuid, + id: results[r].associated_id, + intent: results[r].associated_intent, + }); + children.push({ + name: childProps.name, + uuid: childProps.uuid, + id: results[r].associated_id, + intent: results[r].associated_intent, + }); + } + } + } + let featureProps = JSON.parse(features[f].properties); + flatHierarchy.push({ + feature: features[f], + id: features[f].id, + name: featureProps.name, + uuid: featureProps.uuid, + intent: features[f].intent, + children: children, + possibleChildren: { + intersects: intersects, + contains: contains, + directIntersects: [], + }, + }); + } + //Now attach parents to flatHierarchy + for (let i = 0; i < flatHierarchy.length; i++) { + flatHierarchy[i].parent = {}; + flatHierarchy[i].possibleParents = []; + for (let j = 0; j < flatHierarchy.length; j++) { + if (i != j) { + for ( + let k = 0; + k < flatHierarchy[j].possibleChildren.contains.length; + k++ + ) { + if ( + flatHierarchy[i].id == + flatHierarchy[j].possibleChildren.contains[k].id + ) { + flatHierarchy[i].possibleParents.push({ + name: flatHierarchy[j].name, + uuid: flatHierarchy[j].uuid, + id: flatHierarchy[j].id, + intent: flatHierarchy[j].intent, + }); + } + } + } + } + } + removeIndirectChildren(); + function removeIndirectChildren() { + for (let i = 0; i < flatHierarchy.length; i++) { + let node = flatHierarchy[i]; + let intent = node.intent; + if (intentOrder.indexOf(intent) === -1) continue; + let associationIntent = + intentOrder[intentOrder.indexOf(intent) + 1]; + if (associationIntent == null) { + node.children = []; + } else { + for (let j = node.children.length - 1; j >= 0; j--) { + if (node.children[j].intent != associationIntent) { + node.children.splice(j, 1); + } + } + node.possibleChildren.directIntersects = JSON.parse( + JSON.stringify(node.possibleChildren.intersects) + ); + for ( + let i = node.possibleChildren.directIntersects.length - 1; + i >= 0; + i-- + ) + if ( + node.possibleChildren.directIntersects[i].intent != + associationIntent && + node.possibleChildren.directIntersects[i].intent != intent + ) + node.possibleChildren.directIntersects.splice(i, 1); + } + } + } + addParents(); + function addParents() { + for (let i = 0; i < flatHierarchy.length; i++) { + for (let j = 0; j < flatHierarchy[i].children.length; j++) { + //Each child + //Iterate back through to child and add this flatHierarchy[i] as parent + for (let k = 0; k < flatHierarchy.length; k++) + if (flatHierarchy[k].id === flatHierarchy[i].children[j].id) + flatHierarchy[k].parent = { + name: flatHierarchy[i].name, + uuid: flatHierarchy[i].uuid, + id: flatHierarchy[i].id, + intent: flatHierarchy[i].intent, + }; + } + + //If no parents at this point try to find the best possible parent + if ( + Object.keys(flatHierarchy[i].parent).length === 0 && + flatHierarchy[i].possibleParents.length > 0 + ) { + let intentOrderReversed = JSON.parse( + JSON.stringify(intentOrder) + ); + intentOrderReversed.reverse(); + let intentId = intentOrderReversed.indexOf( + flatHierarchy[i].intent + ); + if (intentId != -1) { + for ( + let l = intentId + 1; + l < intentOrderReversed.length; + l++ + ) { + for ( + let m = 0; + m < flatHierarchy[i].possibleParents.length; + m++ + ) { + if ( + Object.keys(flatHierarchy[i].parent).length === 0 && + flatHierarchy[i].possibleParents[m].intent === + intentOrderReversed[l] + ) { + flatHierarchy[i].parent = + flatHierarchy[i].possibleParents[m]; + } + } + } + } + } + } + } + + //Build the root of the trees + for (let f = 0; f < features.length; f++) { + let isCovered = false; + for (let r = 0; r < results.length; r++) { + if ( + results[r].association === "contains" && + results[r].associated_id == features[f].id + ) { + isCovered = true; + break; + } + } + if (!isCovered) { + let featureProps = JSON.parse(features[f].properties); + hierarchy.push({ + intent: features[f].intent, + id: features[f].id, + name: featureProps.name, + uuid: featureProps.uuid, + children: { + intersects: [], + contains: [], + }, + }); + continue; + } + } + + //From those roots do a depth traversal, adding the flat children each time + depthTraversal(hierarchy, 0); + function depthTraversal(node, depth) { + for (var i = 0; i < node.length; i++) { + //Add other feature information while we're at it + addFeatureData(node[i], depth); + + addRelationships(node[i]); + if (node[i].children.length > 0) + depthTraversal(node[i].children, depth + 1); + } + } + function addRelationships(node) { + for (let i = 0; i < flatHierarchy.length; i++) + if (node.id == flatHierarchy[i].id) { + node.parent = JSON.parse( + JSON.stringify(flatHierarchy[i].parent) + ); + node.children = JSON.parse( + JSON.stringify(flatHierarchy[i].children) + ); + return; + } + } + function addFeatureData(node, depth) { + for (let i = 0; i < features.length; i++) { + let f = features[i]; + if (node.id == f.id) { + let properties = JSON.parse(f.properties); + let feature = {}; + properties._ = { + id: f.id, + file_id: f.file_id, + level: f.level, + intent: f.intent, + }; + feature.type = "Feature"; + feature.properties = properties; + feature.geometry = JSON.parse(f.st_asgeojson); + //id, file_id, level, intent, properties, ST_AsGeoJSON(geom)' + ' ' + + node.file_id = f.file_id; + node.level = f.level; + node.depth = depth; + node.intent = f.intent; + node.name = properties.name; + node.uuid = properties.uuid; + node.properties = JSON.parse(f.properties); + node.geometry = JSON.parse(f.st_asgeojson); + node.feature = feature; + return; + } + } + } + + let saviors = {}; + //Not always do all features fit in the hierarchy at this point, one last chance to fit them in + addOutcasts(); + function addOutcasts() { + let includedIds = []; + let allIds = []; + let outcastIds = []; + + //populate includedIds + depthTraversalA(hierarchy, 0); + function depthTraversalA(node, depth) { + for (let i = 0; i < node.length; i++) { + includedIds.push(node[i].id); + if (node[i].children.length > 0) { + depthTraversalA(node[i].children, depth + 1); + } + } + } + + //populate allIds + for (let i = 0; i < flatHierarchy.length; i++) { + allIds.push(flatHierarchy[i].id); + } + + //populate outcasts + for (let i = 0; i < allIds.length; i++) { + if (includedIds.indexOf(allIds[i]) == -1) + outcastIds.push(allIds[i]); + } + + // parentId: child + //let saviors = {} + for (let i = 0; i < flatHierarchy.length; i++) { + if (outcastIds.indexOf(flatHierarchy[i].id) != -1) { + if ( + flatHierarchy[i].parent && + flatHierarchy[i].parent.id != null + ) { + let outcast = JSON.parse(JSON.stringify(flatHierarchy[i])); + saviors[flatHierarchy[i].parent.id] = outcast; + } + } + } + + //The Saviorng + depthTraversalB(hierarchy, 0); + function depthTraversalB(node, depth) { + for (let i = 0; i < node.length; i++) { + if (saviors[node[i].id] != null) { + node[i].children = Array.isArray(node[i].children) + ? node[i].children + : []; + for (let j = 0; j < features.length; j++) { + let f = features[j]; + if (saviors[node[i].id].id == f.id) { + let outcast = {}; + let properties = JSON.parse(f.properties); + let feature = {}; + properties._ = { + id: f.id, + file_id: f.file_id, + level: f.level, + intent: f.intent, + }; + feature.type = "Feature"; + feature.properties = properties; + feature.geometry = JSON.parse(f.st_asgeojson); + + outcast.name = properties.name; + outcast.uuid = properties.uuid; + outcast.id = f.id; + outcast.intent = f.intent; + outcast.file_id = f.file_id; + outcast.level = f.level; + outcast.depth = depth + 1; + outcast.properties = JSON.parse(f.properties); + outcast.geometry = JSON.parse(f.st_asgeojson); + outcast.feature = feature; + outcast.children = saviors[node[i].id] || []; + outcast.parent = saviors[node[i].id].parent || {}; + node[i].children.push(outcast); + } + } + } + if (node[i].children && node[i].children.length > 0) { + depthTraversalB(node[i].children, depth + 1); + } + } + } + } + + findIssues(); + function findIssues() { + let uuidsFound = {}; + let namesFound = {}; + + for (let i = 0; i < flatHierarchy.length; i++) { + let node = flatHierarchy[i]; + let intent = node.intent; + let props = JSON.parse(node.feature.properties); + + //Check for duplicate uuids + if (props.uuid == null) { + issues.push({ + severity: "error", + antecedent: { + id: node.id, + intent: node.intent, + }, + message: "{antecedent} is missing a uuid.", + }); + } else { + let uuidKeys = Object.keys(uuidsFound); + let uuidI = uuidKeys.indexOf(props.uuid); + if (uuidI >= 0) { + issues.push({ + severity: "error", + antecedent: { + id: node.id, + intent: node.intent, + }, + message: "{antecedent} has the same uuid as {consequent}", + consequent: { + id: uuidsFound[uuidKeys[uuidI]].id, + intent: uuidsFound[uuidKeys[uuidI]].intent, + }, + }); + } else { + uuidsFound[props.uuid] = { + id: node.id, + intent: node.intent, + }; + } + } + + //Check for duplicate names + if (props.name == null) { + issues.push({ + severity: "error", + antecedent: { + id: node.id, + intent: node.intent, + }, + message: "{antecedent} is missing a name.", + }); + } else { + let nameKeys = Object.keys(namesFound); + let nameI = nameKeys.indexOf(props.name); + if (nameI >= 0) { + issues.push({ + severity: "error", + antecedent: { + id: node.id, + intent: node.intent, + }, + message: "{antecedent} has the same name as {consequent}", + consequent: { + id: namesFound[nameKeys[nameI]].id, + intent: namesFound[nameKeys[nameI]].intent, + }, + }); + } else { + namesFound[props.name] = { + id: node.id, + intent: node.intent, + }; + } + } + + if (intentOrder.indexOf(intent) === -1) continue; + let parentIntent = intentOrder[intentOrder.indexOf(intent) - 1]; + if (parentIntent != null && intent != "signpost") { + //Check that it has a valid parent + if (node.parent.intent != parentIntent) { + issues.push({ + severity: "error", + antecedent: { + id: node.id, + intent: node.intent, + }, + message: + "{antecedent} does not have a parent of type: " + + parentIntent + + ".", + }); + } else if (Object.keys(node.parent).length === 0) { + issues.push({ + severity: "error", + antecedent: { + id: node.id, + intent: node.intent, + }, + message: "{antecedent} does not have a parent.", + }); + } + } + + let ints = node.possibleChildren.directIntersects; + for (let j = 0; j < ints.length; j++) { + if (node.intent == "trail") { + } else if (node.intent != ints[j].intent) + issues.push({ + severity: "error", + antecedent: { + id: node.id, + intent: node.intent, + }, + message: + "{antecedent} does not fully contain possible child {consequent}", + consequent: { + id: ints[j].id, + intent: ints[j].intent, + }, + }); + else + issues.push({ + severity: "error", + antecedent: { + id: node.id, + intent: node.intent, + }, + message: + "{antecedent} intersects {consequent} of same intent.", + consequent: { + id: ints[j].id, + intent: ints[j].intent, + }, + }); + } + } + } + + function findChanges(cb) { + //Get published_family_tree from our store + sequelize + .query( + "SELECT value" + + " " + + "FROM published_stores" + + " " + + "WHERE time<=:time" + + " " + + "ORDER BY time DESC" + + " " + + "FETCH first 1 rows only", + { + replacements: { + time: Math.floor(Date.now()), + }, + } + ) + .spread((published_family_tree) => { + if ( + !published_family_tree || + !published_family_tree[0] || + !published_family_tree[0].value + ) { + cb(false); + return; + } else { + let tree = JSON.parse(published_family_tree[0].value); + let fh = tree.flatHierarchy; + let oldFeatures = {}; + let newFeatures = {}; + let added = []; + let changed = []; + let removed = []; + + //Find all the old and new uuids and names first + for (let i = 0; i < fh.length; i++) { + let node = fh[i]; + let props = JSON.parse(node.feature.properties); + oldFeatures[props.uuid] = { name: props.name, id: node.id }; + } + for (let i = 0; i < flatHierarchy.length; i++) { + let node = flatHierarchy[i]; + let props = JSON.parse(node.feature.properties); + newFeatures[props.uuid] = { name: props.name, id: node.id }; + } + let newFeatureUUIDs = Object.keys(newFeatures); + let oldFeatureUUIDs = Object.keys(oldFeatures); + + //Added + for (let i = 0; i < newFeatureUUIDs.length; i++) { + if (oldFeatureUUIDs.indexOf(newFeatureUUIDs[i]) == -1) + added.push({ + uuid: newFeatureUUIDs[i], + name: newFeatures[newFeatureUUIDs[i]].name, + id: newFeatures[newFeatureUUIDs[i]].id, + }); + } + //Removed + for (let i = 0; i < oldFeatureUUIDs.length; i++) { + if (newFeatureUUIDs.indexOf(oldFeatureUUIDs[i]) == -1) + removed.push({ + uuid: oldFeatureUUIDs[i], + name: oldFeatures[oldFeatureUUIDs[i]].name, + id: oldFeatures[oldFeatureUUIDs[i]].id, + }); + } + //Changed + for (let i = 0; i < newFeatureUUIDs.length; i++) { + if (oldFeatureUUIDs.indexOf(newFeatureUUIDs[i]) != -1) { + if ( + oldFeatures[newFeatureUUIDs[i]].name != + newFeatures[newFeatureUUIDs[i]].name + ) { + changed.push({ + uuid: newFeatureUUIDs[i], + old_name: oldFeatures[newFeatureUUIDs[i]].name, + new_name: newFeatures[newFeatureUUIDs[i]].name, + id: newFeatures[newFeatureUUIDs[i]].id, + }); + } + } + } + + cb({ added, changed, removed }); + } + }); + } + + findChanges(function (changes) { + let body = { + hierarchy: hierarchy, + issues: issues, + changes: changes, + }; + if (req.query.verbose) { + body = { + hierarchy: hierarchy, + flatHierarchy: flatHierarchy, + issues: issues, + changes: changes, + saviors: saviors, + }; + } + callback(body); + }); + }); + } + }); +}; +router.get("/compile", function (req, res, next) { + compile(req, res, (body) => { + if (body == null) { + logger("error", "Failed compile file.", req.originalUrl, req); + } + res.send({ + status: body != null ? "success" : "failed", + message: "File compiled.", + body: body, + }); + }); +}); + +/** + * publish sel file + * { + * } + */ +router.post("/publish", function (req, res, next) { + let Table = req.body.test === "true" ? UserfilesTEST : Userfiles; + let Histories = req.body.test === "true" ? FilehistoriesTEST : Filehistories; + + let time = Math.floor(Date.now()); + + //Check that user belongs to sel group + if (req.groups[req.leadGroupName] != true) { + logger("info", "Unauthorized to publish.", req.originalUrl, req); + res.send({ + status: "failure", + message: "Unauthorized to publish.", + body: {}, + }); + return null; + } + + let groups = []; + if (req.groups) groups = Object.keys(req.groups); + + Table.findAll({ + where: { + is_master: true, + [Sequelize.Op.or]: { + file_owner: req.user, + [Sequelize.Op.and]: { + file_owner: "group", + file_owner_group: { [Sequelize.Op.overlap]: groups }, + }, + }, + }, + }).then((files) => { + publishToPublished(function (pass, message) { + if (pass) { + for (let f = 0; f < files.length; f++) { + publishToHistory( + Histories, + files[f].dataValues.id, + time, + () => { + if (f === files.length - 1) { + res.send({ + status: "success", + message: "Published.", + body: {}, + }); + } + }, + (err) => { + logger("error", "Failed to publish.", req.originalUrl, req, err); + res.send({ + status: "failure", + message: "Failed to publish.", + body: {}, + }); + } + ); + } + } else { + logger("error", "Failed to publish. " + message, req.originalUrl, req); + res.send({ + status: "failure", + message: "Failed to publish." + message, + body: {}, + }); + } + }); + }); + + function publishToHistory( + Table, + file_id, + time, + successCallback, + failureCallback + ) { + Table.findAll({ + where: { + file_id: file_id, + }, + }) + .then((histories) => { + let maxHistoryId = -Infinity; + if (histories && histories.length > 0) { + for (let i = 0; i < histories.length; i++) { + maxHistoryId = Math.max(histories[i].history_id, maxHistoryId); + } + return { + historyIndex: maxHistoryId + 1, + history: histories[maxHistoryId].history, + }; + } else return { historyIndex: 0, history: [] }; + }) + .then((historyObj) => { + let newHistoryEntry = { + file_id: file_id, + history_id: historyObj.historyIndex, + time: time, + action_index: 4, + history: historyObj.history, + }; + // Insert new entry into the history table + Table.create(newHistoryEntry) + .then((created) => { + successCallback(newHistoryEntry); + return null; + }) + .catch((err) => { + failureCallback(newHistoryEntry); + }); + return null; + }); + } + + function publishToPublished(cb) { + let Publisheds = req.body.test === "true" ? PublishedTEST : Published; + req.query.verbose = true; + compile(req, res, (body) => { + if (body.issues.length > 0) { + cb(false, " File has unresolved issues."); + } else if (req.body.test === "true") { + cb(true); + return null; + } else { + PublishedStore.create({ + name: "published_family_tree", + value: JSON.stringify(body), + time: time, + }) + .then(() => { + Publisheds.destroy({ + where: {}, + }).then((del) => { + let fH = body.flatHierarchy; + + let rows = []; + for (let i = 0; i < fH.length; i++) { + let feature = { + id: fH[i].id, + intent: fH[i].intent, + parent: fH[i].parent.hasOwnProperty("id") + ? fH[i].parent.id + : null, + children: fH[i].children.map((v) => { + return v.id; + }), + level: fH[i].feature.level, + properties: JSON.parse(fH[i].feature.properties), + geom: JSON.parse(fH[i].feature.st_asgeojson), + }; + delete feature.properties._; + feature.geom.crs = { + type: "name", + properties: { name: "EPSG:4326" }, + }; + + rows.push(feature); + } + + Publisheds.bulkCreate(rows, { returning: true }) + .then(function (response) { + cb(true); + return null; + }) + .catch(function (error) { + cb(false); + return null; + }); + + return null; + }); + + return null; + }) + .catch(function (err) { + logger( + "error", + "Error adding published tree.", + req.originalUrl, + req, + err + ); + cb(false); + return null; + }); + } + }); + } +}); + +/** + * Get a file's history + * { + * id: + * } + */ +router.post("/gethistory", function (req, res, next) { + let Table = req.body.test === "true" ? FilehistoriesTEST : Filehistories; + + Table.findAll({ + where: { + file_id: req.body.id, + }, + }) + .then((histories) => { + if (!histories) { + res.send({ + status: "failure", + message: "Failed to get history.", + body: {}, + }); + } else { + //Newest first + histories.sort((a, b) => (a.history_id < b.history_id ? 1 : -1)); + for (let i = 0; i < histories.length; i++) + histories[i].dataValues.message = + historyKey[histories[i].dataValues.action_index]; + + res.send({ + status: "success", + message: "Successfully got history.", + body: histories, + }); + } + + return null; + }) + .catch((err) => { + logger("error", "Failed to get history.", req.originalUrl, req, err); + res.send({ + status: "failure", + message: "Failed to get history.", + body: {}, + }); + }); +}); + +module.exports = { router, makeMasterFiles }; diff --git a/API/templates/config_template.js b/API/templates/config_template.js index d85a108f..a6728f93 100644 --- a/API/templates/config_template.js +++ b/API/templates/config_template.js @@ -1,13 +1,14 @@ module.exports = { msv: { - mission: "TEMPLATE", + mission: "Test", site: "", masterdb: false, view: ["0", "0", "0"], radius: { major: "3396190", - minor: "3396190" - } + minor: "3396190", + }, + mapscale: "", }, projection: { custom: false, @@ -18,33 +19,41 @@ module.exports = { bounds: ["", "", "", ""], origin: ["", ""], reszoomlevel: "", - resunitsperpixel: "" + resunitsperpixel: "", }, look: { pagename: "MMGIS", + minimalist: false, zoomcontrol: false, graticule: false, bodycolor: "", topbarcolor: "", toolbarcolor: "", - mapcolor: "" + mapcolor: "", + swap: true, + copylink: true, + screenshot: true, + fullscreen: true, + help: true, + logourl: "", + helpurl: "", }, panels: ["viewer", "map", "globe"], tools: [ { name: "Layers", icon: "buffer", - js: "LayersTool" + js: "LayersTool", }, { name: "Legend", icon: "format-list-bulleted-type", - js: "LegendTool" + js: "LegendTool", }, { name: "Info", icon: "information-variant", - js: "InfoTool" + js: "InfoTool", }, { name: "Sites", @@ -55,49 +64,30 @@ module.exports = { { name: "Site1", code: "S1", - view: [-4.667975771815966, 137.370253354311, 16] + view: [-4.667975771815966, 137.370253354311, 16], }, { name: "Site2", code: "S2", - view: [-4.667985128408622, 137.3702734708786, 20] - } - ] - } - }, - { - name: "FileManager", - icon: "folder-multiple", - js: "FileManagerTool" + view: [-4.667985128408622, 137.3702734708786, 20], + }, + ], + }, }, { - name: "Measure", - icon: "chart-areaspline", - js: "MeasureTool", - variables: { - dem: "Data/missionDEM.tif" - } + name: "Chemistry", + icon: "flask", + js: "ChemistryTool", }, { name: "Draw", icon: "lead-pencil", - js: "DrawTool" - }, - { - name: "Chemistry", - icon: "flask", - js: "ChemistryTool" + js: "DrawTool", }, { - name: "Search", - icon: "eye", - js: "SearchTool", - variables: { - searchfields: { - ChemCam: "(TARGET) round(Sol)", - Waypoints: "round(sol)" - } - } + name: "FileManager", + icon: "folder-multiple", + js: "FileManagerTool", }, { name: "Identifier", @@ -106,21 +96,33 @@ module.exports = { variables: { "Tile with DEM": { url: "Data/missionDEM.tif", - unit: "m" - } - } - } + unit: "m", + }, + }, + }, + { + name: "Measure", + icon: "chart-areaspline", + js: "MeasureTool", + variables: { + dem: "Data/missionDEM.tif", + }, + }, ], layers: [ { name: "A Header", type: "header", + initialOpacity: 1, sublayers: [ { name: "S1 Drawings", + kind: "none", type: "vector", url: "Drawn/S1_speDrawings.geojson", + tms: true, visibility: false, + initialOpacity: 1, togglesWithHeader: true, style: { className: "s1drawings", @@ -128,15 +130,19 @@ module.exports = { fillColor: "undefined", weight: null, fillOpacity: 1, - opacity: 1 + opacity: 1, }, - radius: 1 + variables: {}, + radius: 1, }, { name: "S2 Drawings", + kind: "none", type: "vector", url: "Drawn/S2_speDrawings.geojson", + tms: true, visibility: false, + initialOpacity: 1, togglesWithHeader: true, style: { className: "s2drawings", @@ -144,16 +150,20 @@ module.exports = { fillColor: "undefined", weight: null, fillOpacity: 1, - opacity: 1 + opacity: 1, }, - radius: 1 + variables: {}, + radius: 1, }, { name: "ChemCam", + kind: "none", type: "vector", url: "Layers/ChemCam/chemcam.json", + tms: true, visibility: true, visibilitycutoff: 17, + initialOpacity: 1, togglesWithHeader: true, style: { className: "chemcam", @@ -161,7 +171,7 @@ module.exports = { fillColor: "prop:color3", weight: 2, fillOpacity: 1, - opacity: 1 + opacity: 1, }, variables: { useKeyAsName: "TARGET", @@ -173,17 +183,21 @@ module.exports = { "MgO", "Na2O", "SiO2", - "TiO2" - ] + "TiO2", + ], + search: "(TARGET)", }, - radius: 5 + radius: 5, }, { name: "Waypoints", + kind: "none", type: "vector", url: "Layers/Waypoints/waypoints.json", legend: "Layers/Waypoints/legend.csv", + tms: true, visibility: true, + initialOpacity: 1, togglesWithHeader: true, style: { className: "waypoints", @@ -191,15 +205,19 @@ module.exports = { fillColor: "#000", weight: 2, fillOpacity: 1, - opacity: 1 + opacity: 1, }, - radius: 8 + variables: {}, + radius: 8, }, { name: "Polygon", + kind: "none", type: "vector", url: "Layers/Polygon/polygon.geojson", + tms: true, visibility: false, + initialOpacity: 1, togglesWithHeader: true, style: { className: "polygon", @@ -207,15 +225,19 @@ module.exports = { fillColor: "prop:fill", weight: 2, fillOpacity: 0.7, - opacity: 1 + opacity: 1, }, - radius: 4 + variables: {}, + radius: 4, }, { name: "Line", + kind: "none", type: "vector", url: "Layers/Line/line.json", + tms: true, visibility: false, + initialOpacity: 1, togglesWithHeader: true, style: { className: "line", @@ -223,22 +245,25 @@ module.exports = { fillColor: "white", weight: 5, fillOpacity: 1, - opacity: 1 + opacity: 1, }, - radius: 1 + variables: {}, + radius: 1, }, { name: "Tile with DEM", type: "tile", url: "Layers/TilewithDEM/Gale_HiRISE/{z}/{x}/{y}.png", demtileurl: "Layers/TilewithDEM/Gale_HiRISE_DEM/{z}/{x}/{y}.png", + tms: true, visibility: true, + initialOpacity: 1, togglesWithHeader: true, minZoom: 16, maxNativeZoom: 17, - maxZoom: 22 - } - ] - } - ] + maxZoom: 22, + }, + ], + }, + ], }; diff --git a/README.md b/README.md index 1bb03ef6..aa6fa58a 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Spatial Data Infrastructure for Planetary Missions 1. Run `install.sh` within `/` `./install.sh` - (If you can't run install, just copy `/prepare/base/Missions` to `/Missions`) + (If you can't run install, just copy `/prepare/base/Missions` to `/Missions` and copy `/prepare/base/config/configconfig.json` to `/config/configconfig.json`) 1. Copy `/sample.env` to `.env` `cp sample.env .env` diff --git a/auxiliary/populateMosaics/populateMosaics.js b/auxiliary/populateMosaics/populateMosaics.js new file mode 100644 index 00000000..3a0d073e --- /dev/null +++ b/auxiliary/populateMosaics/populateMosaics.js @@ -0,0 +1,63 @@ +// node populateMosaics.js [input json] [mosaic parameters csv] [url prefix] [output json] +// [input json] is a geojson where each feature has the properties "sol", "site" and "pos" +// [mosaic parameters csv] has the following header: mos_name,rows,columns,azmin,azmax,elmin,elmax,elzero +// [url prefix] +// [output json] writes the mosaic features[i].properties.images +// +// use npm to install csv-parser + +const csv = require("csv-parser"); +const fs = require("fs"); + +const input = process.argv[2]; +const parameters = process.argv[3]; +const prefix = process.argv[4]; +const output = process.argv[5]; + +let csvrows = {}; + +fs.createReadStream(parameters) + .pipe(csv()) + .on("data", (row) => { + const sol = parseInt(row.mos_name.substr(7, 4)); + const site = parseInt(row.mos_name.substr(15, 3)); + const pos = parseInt(row.mos_name.substr(24, 4)); + const id = sol + "_" + site + "_" + pos; + + csvrows[id] = row; + }) + .on("end", () => { + let geojson = JSON.parse(fs.readFileSync(input, "utf8")); + + geojson.features.forEach((element) => { + const p = element.properties; + + const sol = parseInt(p.sol); + const site = parseInt(p.site); + const pos = parseInt(p.pos); + const id = sol + "_" + site + "_" + pos; + + p.images = p.images || []; + + if (csvrows[id]) { + const c = csvrows[id]; + // delete the existing image with same url if any + p.images = p.images.filter((v) => v.name != c.mos_name); + p.images.push({ + name: c.mos_name, + isPanoramic: true, + url: prefix + c.mos_name + ".jpg", + rows: c.rows, + columns: c.columns, + azmin: c.azmin, + azmax: c.azmax, + elmin: c.elmin, + elmax: c.elmax, + elzero: c.elzero, + }); + } + }); + fs.writeFile(output, JSON.stringify(geojson), "utf8", () => { + console.log("Successfully populated mosaic images into " + output + "!"); + }); + }); diff --git a/config/configconfig.json b/config/configconfig.json new file mode 100644 index 00000000..afe036ee --- /dev/null +++ b/config/configconfig.json @@ -0,0 +1,151 @@ +{ + "missions": [], + "tools": [ + { + "defaultIcon": "buffer", + "description": "Hierarchically toggle layers on and off and alter their opacities.", + "descriptionFull": "", + "hasVars": false, + "name": "Layers" + }, + { + "defaultIcon": "format-list-bulleted-type", + "description": "Show a chart mapping colors and symbols to meaning.", + "descriptionFull": "", + "hasVars": false, + "name": "Legend" + }, + { + "defaultIcon": "information-variant", + "description": "Display the geojson properties field of a clicked point.", + "descriptionFull": "", + "hasVars": false, + "name": "Info" + }, + { + "defaultIcon": "pin", + "description": "A button bar to navigate between various map locations.", + "descriptionFull": { + "title": "A button bar to quickly navigate between preset map locations.", + "example": { + "sites": [ + { + "name": "(str) Name of site", + "code": "(str) Unique identifier. Can match header layer to toggle it's sublayers", + "view": ["(num) Latitude", "(num) Longitude", "(num) Zoom level"] + }, + { + "...": "..." + } + ] + } + }, + "hasVars": true, + "name": "Sites" + }, + { + "defaultIcon": "folder-multiple", + "description": "A file finder/explorer to access and display other users' drawings.", + "descriptionFull": "", + "hasVars": false, + "name": "FileManager" + }, + { + "defaultIcon": "chart-areaspline", + "description": "Measure distances and generates elevation profiles.", + "descriptionFull": { + "title": "Specify a path to a Digital Elevation Model (dem) .tif. Measure distances and generates elevation profiles. Can also query specific bands at specific points of images and generate profiles of them.", + "example": { + "dem": "(str) path to Data/missionDEM.tif" + } + }, + "hasVars": true, + "name": "Measure" + }, + { + "defaultIcon": "lead-pencil", + "description": "Draw polygons with colors, names and descriptions.", + "descriptionFull": { + "title": "Please specify a color legend. Draw polygons with colors, names and descriptions. Polygons of the same file never overlap each other and there are options to draw, erase, delete, draw over, draw under, change the color, name, description, download and choose which file you're drawing to.", + "example": { + "colorlegend": [ + { + "name": "Smooth Regolith", + "color": "#1E5CB3", + "value": "0", + "invertTextColor": true + }, + { + "...": "..." + } + ], + "demtilesets": { + "path_to_DEM_tiles/{z}/{x}/{y}.png": { + "dim": "(int) dimension of dem tiles used. 32 for 32x32 tiles", + "z": "(int) zoom level to query elevation data off of dem tiles" + }, + "...": { + "...": "..." + } + } + } + }, + "hasVars": true, + "name": "Sketch" + }, + { + "defaultIcon": "flask", + "description": "Display chemistry percentages via graphs of a clicked point.", + "descriptionFull": "", + "hasVars": false, + "name": "Chemistry" + }, + { + "defaultIcon": "eye", + "description": "Search a layer for a string.", + "descriptionFull": { + "title": "Please specify search fields. Search a layer for a string and either go to it and/or select it. The strings are built from the layer elements' properties object. Which properties it searches through depends on the defined searchfields for each layer. Try to choose a combination that's unique. propN is a geojson property key of that layer. All propNs are placed between parentheses and separated by a space. Place 'round' or 'rmunder' before a prop to round it or remove its underscores.", + "example": { + "searchfields": { + "[Layer_Name]": "(prop1) round(prop3)", + "...": "..." + } + } + }, + "hasVars": true, + "name": "Search" + }, + { + "defaultIcon": "map-marker", + "description": "Mouse over the map for a by-pixel legend of a raster.", + "descriptionFull": { + "title": "Mouse over to query underlying datasets.", + "example": { + "[Layer_Name]": { + "url": "(str) path_to_data/data.tif", + "bands": "(int) how many bands to query from", + "sigfigs": "(int) how many digits after the decimal", + "unit": "(str) whatever string unit" + }, + "...": {} + } + }, + "hasVars": true, + "name": "Identifier" + }, + { + "defaultIcon": "magnify", + "description": "Spatially query layer data", + "descriptionFull": "A more complete description that you see when you click on the description", + "hasVars": true, + "name": "Query" + }, + { + "defaultIcon": "tent", + "description": "Advanced drawing", + "descriptionFull": "", + "hasVars": true, + "name": "Draw" + } + ] +} diff --git a/config/css/config.css b/config/css/config.css index 606fea4b..f719f147 100644 --- a/config/css/config.css +++ b/config/css/config.css @@ -399,6 +399,7 @@ textarea { color: #ddd; } +#variableEl > .CodeMirror, #vtLayerEl > .CodeMirror { margin: 0; } diff --git a/config/js/config.js b/config/js/config.js index 4fd4fe61..a79cbbbe 100644 --- a/config/js/config.js +++ b/config/js/config.js @@ -11,28 +11,28 @@ var availableKinds = []; var dataOfLastUsedLayerSlot = {}; -setInterval(function() { +setInterval(function () { mmgisglobal.lastInteraction = Date.now(); }, 60000 * 5); //$( 'body' ).on( 'mousemove', function() { mmgisglobal.lastInteraction = Date.now(); } ); -$(document).ready(function() { +$(document).ready(function () { initialize(); }); - + function initialize() { - $(".logout").on("click", function() { + $(".logout").on("click", function () { $.ajax({ type: calls.logout.type, url: calls.logout.url, data: {}, - success: function(data) { + success: function (data) { window.location = "/"; - } + }, }); }); //initialize new mission button - $("#new_mission").on("click", function() { + $("#new_mission").on("click", function () { $("#missions li").removeClass("active"); $("#new_mission").css({ "background-color": "#1565C0" }); $(".container #existing_mission_cont").css({ display: "none" }); @@ -44,15 +44,15 @@ function initialize() { $("body").attr("class", "mmgisScrollbar"); - $("#upload_config_input").on("change", function(evt) { + $("#upload_config_input").on("change", function (evt) { var files = evt.target.files; // FileList object // use the 1st file from the list var f = files[0]; var reader = new FileReader(); // Closure to capture the file information. - reader.onload = (function(file) { - return function(e) { + reader.onload = (function (file) { + return function (e) { let config; try { config = JSON.parse(e.target.result); @@ -61,9 +61,7 @@ function initialize() { "Bad JSON.", 4000 ); - $("#toast_failure80") - .parent() - .css("background-color", "#a11717"); + $("#toast_failure80").parent().css("background-color", "#a11717"); return; } if ( @@ -77,9 +75,7 @@ function initialize() { "Bad config.", 4000 ); - $("#toast_failure81") - .parent() - .css("background-color", "#a11717"); + $("#toast_failure81").parent().css("background-color", "#a11717"); } }; })(f); @@ -89,15 +85,15 @@ function initialize() { }); //Initial keys - $("#manage_keys").on("click", function() { + $("#manage_keys").on("click", function () { Keys.make(); }); //Initial manage datasets - $("#manage_datasets").on("click", function() { + $("#manage_datasets").on("click", function () { Datasets.make(); }); //Initial manage geodatasets - $("#manage_geodatasets").on("click", function() { + $("#manage_geodatasets").on("click", function () { Geodatasets.make(); }); @@ -105,7 +101,7 @@ function initialize() { type: calls.missions.type, url: calls.missions.url, data: {}, - success: function(data) { + success: function (data) { if (data.status == "success") { var mData = data.missions; for (var i = 0; i < mData.length; i++) { @@ -117,11 +113,9 @@ function initialize() { "Error loading available mission.", 500000 ); - $("#toast_failure8") - .parent() - .css("background-color", "#a11717"); + $("#toast_failure8").parent().css("background-color", "#a11717"); } - } + }, }); function getConfigConfig() { @@ -129,22 +123,22 @@ function initialize() { type: calls.getToolConfig.type, url: calls.getToolConfig.url, data: {}, - success: function(ccData) { + success: function (ccData) { if (ccData.status != "success") { console.warn("Failure getting tools configurations."); return; } - tData = Object.keys(ccData.tools).map(function(key) { + tData = Object.keys(ccData.tools).map(function (key) { return ccData.tools[key]; }); //Populate available Kinds - let kinds = tData.filter(t => t.name === "Kinds"); + let kinds = tData.filter((t) => t.name === "Kinds"); if (kinds[0]) { availableKinds = kinds[0].kinds; //then remove Kinds - tData = tData.filter(t => t.name !== "Kinds"); + tData = tData.filter((t) => t.name !== "Kinds"); } editors = {}; @@ -171,8 +165,8 @@ function initialize() { ); $("#t" + tData[i].name + "_info").on( "click", - (function(name, descriptionFull) { - return function() { + (function (name, descriptionFull) { + return function () { if (descriptionFull == "") descriptionFull = { title: "No further description." }; $("#info_modal div.modal-content h4").html(name); @@ -186,10 +180,8 @@ function initialize() { })(tData[i].name, tData[i].descriptionFull) ); - $("#t" + tData[i].name + "_icon").on("input", function() { - var newIcon = $(this) - .val() - .replace(/ /g, "_"); + $("#t" + tData[i].name + "_icon").on("input", function () { + var newIcon = $(this).val().replace(/ /g, "_"); $(this) .parent() .find("i") @@ -211,7 +203,7 @@ function initialize() { viewportMargin: Infinity, lineNumbers: true, autoRefresh: true, - matchBrackets: true + matchBrackets: true, } ); editors[tData[i].name] = codeeditor; @@ -223,7 +215,7 @@ function initialize() { $("ul.tabs#missions .indicator").css({ display: "none" }); - $("#missions li").on("click", function() { + $("#missions li").on("click", function () { layerEditors = {}; Keys.destroy(); @@ -240,15 +232,13 @@ function initialize() { $("#tab_layers_rows").empty(); $("#new_mission").css({ - "background-color": "rgba(255,255,255,0.12)" + "background-color": "rgba(255,255,255,0.12)", }); $(".container #existing_mission_cont").css({ display: "inherit" }); $(".container #new_mission_cont").css({ display: "none" }); $("ul.tabs .indicator").css({ "background-color": "#1565C0" }); - mission = $(this) - .find("a") - .html(); + mission = $(this).find("a").html(); missionPath = calls.missionPath + mission + "/config.json"; $.ajax({ @@ -256,9 +246,9 @@ function initialize() { url: calls.get.url, data: { mission: mission, - full: true + full: true, }, - success: function(data) { + success: function (data) { if (data.status == "success") { var cData = data.config; @@ -412,9 +402,7 @@ function initialize() { //tools //uncheck all tools - $("#tab_tools") - .find(":checkbox") - .prop("checked", false); + $("#tab_tools").find(":checkbox").prop("checked", false); //clear all editors for (var e in editors) { editors[e].setValue(""); @@ -456,13 +444,13 @@ function initialize() { type: calls.versions.type, url: calls.versions.url, data: { - mission: mission + mission: mission, }, - success: function(data) { + success: function (data) { if (data.status == "success") { populateVersions(data.versions); } - } + }, }); } else { Materialize.toast( @@ -475,25 +463,25 @@ function initialize() { .parent() .css("background-color", "#a11717"); } - } + }, }); }); }, - error: function(jqXHR, textStatus, error) { + error: function (jqXHR, textStatus, error) { console.warn("Error getting tools configurations."); - } + }, }); } //Add layer button - $("#add_new_layer").on("click", function() { + $("#add_new_layer").on("click", function () { var madeUpData = { name: "New Layer", type: "header", visibility: "false" }; makeLayerBarAndModal(madeUpData, 0); refresh(); }); //Clone Button and Modal - $("#clone_mission").on("click", function() { + $("#clone_mission").on("click", function () { //Clear passwords $("#cloneName").val(""); $("#clonePassword").val(""); @@ -505,14 +493,14 @@ function initialize() { mission + "" ); - setTimeout(function() { + setTimeout(function () { $(".lean-overlay").css({ transition: "background-color 0.5s", - "background-color": "#1565c0" + "background-color": "#1565c0", }); }, 150); }); - $("#clone_modal #clone_mission_clone").on("click", function() { + $("#clone_modal #clone_mission_clone").on("click", function () { let cName = $("#cloneName").val(); let hasPaths = $("#clonePaths").prop("checked"); @@ -522,17 +510,15 @@ function initialize() { data: { existingMission: mission, cloneMission: cName, - hasPaths: hasPaths + hasPaths: hasPaths, }, - success: function(data) { + success: function (data) { if (data.status == "success") { Materialize.toast( "Mission Clone Successful.", 3000 ); - $("#toast_success_clone") - .parent() - .css("background-color", "#1565C0"); + $("#toast_success_clone").parent().css("background-color", "#1565C0"); Materialize.toast( "Page will now reload...", 3000 @@ -540,7 +526,7 @@ function initialize() { $("#toast_success_cloner") .parent() .css("background-color", "#1565C0"); - setTimeout(function() { + setTimeout(function () { location.reload(); }, 3000); } else { @@ -548,11 +534,9 @@ function initialize() { "" + data.message + "", 5000 ); - $("#toast_bad_clone") - .parent() - .css("background-color", "#a11717"); + $("#toast_bad_clone").parent().css("background-color", "#a11717"); } - } + }, }); //Clear again @@ -560,7 +544,7 @@ function initialize() { }); //Delete Button and Modal - $("#delete_mission").on("click", function() { + $("#delete_mission").on("click", function () { //Clear passwords $("#deleteMissionName").val(""); @@ -571,14 +555,14 @@ function initialize() { mission + "" ); - setTimeout(function() { + setTimeout(function () { $(".lean-overlay").css({ transition: "background-color 0.5s", - "background-color": "red" + "background-color": "red", }); }, 150); }); - $("#delete_modal #delete_mission_delete").on("click", function() { + $("#delete_modal #delete_mission_delete").on("click", function () { var name = $("#deleteMissionName").val(); if (name != mission) { @@ -586,9 +570,7 @@ function initialize() { "Confirmation mission name didn't match.", 7000 ); - $("#toast_delete_failure2") - .parent() - .css("background-color", "#a11717"); + $("#toast_delete_failure2").parent().css("background-color", "#a11717"); return; } @@ -596,25 +578,21 @@ function initialize() { type: calls.destroy.type, url: calls.destroy.url, data: { - mission: mission + mission: mission, }, - success: function(data) { + success: function (data) { if (data.status == "success") { Materialize.toast( "Mission Removal Successful.", 4000 ); - $("#toast_success4") - .parent() - .css("background-color", "#1565C0"); + $("#toast_success4").parent().css("background-color", "#1565C0"); Materialize.toast( "Page will now reload...", 4000 ); - $("#toast_success5") - .parent() - .css("background-color", "#1565C0"); - setTimeout(function() { + $("#toast_success5").parent().css("background-color", "#1565C0"); + setTimeout(function () { location.reload(); }, 4000); } else { @@ -626,7 +604,7 @@ function initialize() { .parent() .css("background-color", "#a11717"); } - } + }, }); $("#deleteMissionName").val(""); @@ -677,12 +655,32 @@ function makeLayerBarAndModal(d, level) { // prettier-ignore var nameEl = "block", kindEl = "block", typeEl = "block", urlEl = "block", demtileurlEl = "block", legendEl = "block", visEl = "block", viscutEl = "block", initOpacEl = "block", togwheadEl = "block", minzEl = "block", - tmsEl = "none"; visEl = "block", viscutEl = "block", togwheadEl = "block", minzEl = "block", - modelLonEl = "block", modelLatEl = "block", modelElevEl = "block", - modelRotXEl = "block", modelRotYEl = "block", modelRotZEl = "block", modelScaleEl = "block", - maxnzEl = "block", maxzEl = "block", strcolEl = "block", filcolEl = "block", - weightEl = "block", opacityEl = "block", radiusEl = "block", variableEl = "block", - xmlEl = "block", bbEl = "block", vtLayerEl = "block", vtIdEl = "block", vtKeyEl = "block", vtLayerSetStylesEl = "block"; + tmsEl = "none", + visEl = "block", + viscutEl = "block", + togwheadEl = "block", + minzEl = "block", + modelLonEl = "block", + modelLatEl = "block", + modelElevEl = "block", + modelRotXEl = "block", + modelRotYEl = "block", + modelRotZEl = "block", + modelScaleEl = "block", + maxnzEl = "block", + maxzEl = "block", + strcolEl = "block", + filcolEl = "block", + weightEl = "block", + opacityEl = "block", + radiusEl = "block", + variableEl = "block", + xmlEl = "block", + bbEl = "block", + vtLayerEl = "block", + vtIdEl = "block", + vtKeyEl = "block", + vtLayerSetStylesEl = "block"; // prettier-ignore switch( d.type ) { @@ -713,11 +711,11 @@ function makeLayerBarAndModal(d, level) { modelLonEl = "none"; modelLatEl = "none"; modelElevEl = "none"; modelRotXEl = "none"; modelRotYEl = "none"; modelRotZEl = "none"; modelScaleEl = "none"; maxnzEl = "block"; maxzEl = "block"; strcolEl = "none"; filcolEl = "none"; - weightEl = "none"; opacityEl = "none"; radiusEl = "none"; variableEl = "none"; + weightEl = "none"; opacityEl = "none"; radiusEl = "none"; variableEl = "block"; xmlEl = "none"; bbEl = "none"; vtLayerEl = "block"; vtIdEl = "block"; vtKeyEl = "block"; vtLayerSetStylesEl = "block"; break; case "data": - nameEl = "block"; kindEl = "none"; typeEl = "block"; urlEl = "block"; demtileurlEl = "none"; legendEl = "block"; + nameEl = "block"; kindEl = "none"; typeEl = "block"; urlEl = "block"; demtileurlEl = "black"; legendEl = "block"; visEl = "block"; viscutEl = "none"; initOpacEl = "none"; togwheadEl = "block"; minzEl = "block"; tmsEl = "none"; modelLonEl = "none"; modelLatEl = "none"; modelElevEl = "none"; @@ -797,16 +795,19 @@ function makeLayerBarAndModal(d, level) { modelSel = "selected"; } - var tmsTrueSel = "", tmsFalseSel = ""; - switch( d.tms ) { + var tmsTrueSel = "", + tmsFalseSel = ""; + switch (d.tms) { case true: - case "true": tmsTrueSel = "selected"; + case "true": + tmsTrueSel = "selected"; break; case false: - case "false": tmsFalseSel = "selected"; + case "false": + tmsFalseSel = "selected"; break; } - + var visTrueSel = "", visFalseSel = ""; var visIcon = "inherit"; @@ -1030,6 +1031,7 @@ function makeLayerBarAndModal(d, level) { "
" + "
" + + "Vector Tile Stylings:" + "" + "
" + "
" + @@ -1059,7 +1061,8 @@ function makeLayerBarAndModal(d, level) { "" + "
" + - "
" + + "
" + + "Raw Variables:" + "" + "
" + "
" + @@ -1087,7 +1090,7 @@ function makeLayerBarAndModal(d, level) { viewportMargin: Infinity, lineNumbers: true, autoRefresh: true, - matchBrackets: true + matchBrackets: true, } ); if (dStyle.vtLayer) @@ -1103,7 +1106,7 @@ function makeLayerBarAndModal(d, level) { viewportMargin: Infinity, lineNumbers: true, autoRefresh: true, - matchBrackets: true + matchBrackets: true, } ); if (d.variables) @@ -1113,33 +1116,25 @@ function makeLayerBarAndModal(d, level) { //Extend jQuery functionality to allow for an x-axis draggable that snaps with // materialize rows // offs to avoid duplicates $.fn.extend({ - mmgisLinkModalsToLayers: function() { + mmgisLinkModalsToLayers: function () { $(this) .children(".modal") - .each(function() { + .each(function () { //Link Name $(this) .find("#nameEl") .off("change", mmgisLinkModalsToLayersNameChange); - $(this) - .find("#nameEl") - .on("change", mmgisLinkModalsToLayersNameChange); + $(this).find("#nameEl").on("change", mmgisLinkModalsToLayersNameChange); //Link Type with color and available fields $(this) .find("#typeEl") .off("change", mmgisLinkModalsToLayersTypeChange); - $(this) - .find("#typeEl") - .on("change", mmgisLinkModalsToLayersTypeChange); + $(this).find("#typeEl").on("change", mmgisLinkModalsToLayersTypeChange); //Link visibility with icon - $(this) - .find("#visEl") - .off("change", mmgisLinkModalsToLayersVisChange); - $(this) - .find("#visEl") - .on("change", mmgisLinkModalsToLayersVisChange); + $(this).find("#visEl").off("change", mmgisLinkModalsToLayersVisChange); + $(this).find("#visEl").on("change", mmgisLinkModalsToLayersVisChange); //Make delete delete $(this) @@ -1149,53 +1144,33 @@ $.fn.extend({ .find("#delete_layer") .on("click", mmgisLinkModalsToLayersDeleteClick); - $(this) - .find(".clone") - .off("click", mmgisLinkModalsToLayersCloneClick); - $(this) - .find(".clone") - .on("click", mmgisLinkModalsToLayersCloneClick); + $(this).find(".clone").off("click", mmgisLinkModalsToLayersCloneClick); + $(this).find(".clone").on("click", mmgisLinkModalsToLayersCloneClick); }); }, - materializeDraggable: function() { + materializeDraggable: function () { $(this) .children("li") - .each(function() { - $(this) - .children("a") - .off("mouseup", materializeDraggableMouseUp); - $(this) - .children("a") - .on("mouseup", materializeDraggableMouseUp); + .each(function () { + $(this).children("a").off("mouseup", materializeDraggableMouseUp); + $(this).children("a").on("mouseup", materializeDraggableMouseUp); }); - } + }, }); function mmgisLinkModalsToLayersNameChange(e) { - var mainThis = $(this) - .parent() - .parent() - .parent(); + var mainThis = $(this).parent().parent().parent(); var mainId = mainThis.attr("id"); mainId = mainId.substring(mainId.indexOf("_") + 1); //Change modal title name - mainThis.find("#modal_name").html( - $(this) - .children("input") - .val() - ); + mainThis.find("#modal_name").html($(this).children("input").val()); //Change layer bar name $("#layers_rows_" + mainId + " .l_title").html( - $(this) - .children("input") - .val() + $(this).children("input").val() ); } function mmgisLinkModalsToLayersTypeChange(e) { - var mainThis = $(this) - .parent() - .parent() - .parent(); + var mainThis = $(this).parent().parent().parent(); var mainId = mainThis.attr("id"); mainId = mainId.substring(mainId.indexOf("_") + 1); @@ -1237,11 +1212,11 @@ function mmgisLinkModalsToLayersTypeChange(e) { modelLonEl = "none"; modelLatEl = "none"; modelElevEl = "none"; modelRotXEl = "none"; modelRotYEl = "none"; modelRotZEl = "none"; modelScaleEl = "none"; maxzEl = "block"; strcolEl = "none"; filcolEl = "none"; weightEl = "none"; - opacityEl = "none"; radiusEl = "none"; variableEl = "none"; + opacityEl = "none"; radiusEl = "none"; variableEl = "block"; xmlEl = "none"; bbEl = "none"; vtLayerEl = "block"; vtIdEl = "block"; vtKeyEl = "block"; vtLayerSetStylesEl = "block"; break; case "data": barColor = "rgb(189, 15, 50)"; - nameEl = "block"; kindEl = "none"; typeEl = "block"; urlEl = "block"; demtileurlEl = "none"; legendEl = "block"; + nameEl = "block"; kindEl = "none"; typeEl = "block"; urlEl = "block"; demtileurlEl = "block"; legendEl = "block"; tmsEl = "none"; visEl = "block"; viscutEl = "none"; initOpacEl = "none"; togwheadEl = "block"; minzEl = "block"; maxnzEl = "block"; modelLonEl = "none"; modelLatEl = "none"; modelElevEl = "none"; modelRotXEl = "none"; modelRotYEl = "none"; modelRotZEl = "none"; modelScaleEl = "none"; @@ -1288,19 +1263,12 @@ function mmgisLinkModalsToLayersTypeChange(e) { mainThis.find("#typeEl").css("display", typeEl); mainThis.find("#kindEl").css("display", kindEl); if (kindEl == "none") - mainThis - .find("#nameEl") - .removeClass("s3") - .addClass("s5"); - else - mainThis - .find("#nameEl") - .removeClass("s5") - .addClass("s3"); + mainThis.find("#nameEl").removeClass("s3").addClass("s5"); + else mainThis.find("#nameEl").removeClass("s5").addClass("s3"); mainThis.find("#urlEl").css("display", urlEl); mainThis.find("#demtileurlEl").css("display", demtileurlEl); mainThis.find("#legendEl").css("display", legendEl); - mainThis.find("#tmsEl").css( "display", tmsEl ); + mainThis.find("#tmsEl").css("display", tmsEl); mainThis.find("#visEl").css("display", visEl); mainThis.find("#viscutEl").css("display", viscutEl); mainThis.find("#initOpacEl").css("display", initOpacEl); @@ -1329,28 +1297,18 @@ function mmgisLinkModalsToLayersTypeChange(e) { mainThis.find("#vtLayerSetStylesEl").css("display", vtLayerSetStylesEl); } function mmgisLinkModalsToLayersVisChange(e) { - var mainThis = $(this) - .parent() - .parent() - .parent(); + var mainThis = $(this).parent().parent().parent(); var mainId = mainThis.attr("id"); mainId = mainId.substring(mainId.indexOf("_") + 1); - if ( - $(this) - .find("select option:selected") - .text() - .toLowerCase() == "true" - ) { + if ($(this).find("select option:selected").text().toLowerCase() == "true") { $("#layers_rows_" + mainId + " .l_icon").css({ display: "inherit" }); } else { $("#layers_rows_" + mainId + " .l_icon").css({ display: "none" }); } } function mmgisLinkModalsToLayersDeleteClick(e) { - var mainThis = $(this) - .parent() - .parent(); + var mainThis = $(this).parent().parent(); var mainId = mainThis.attr("id"); mainId = mainId.substring(mainId.indexOf("_") + 1); @@ -1362,10 +1320,7 @@ function mmgisLinkModalsToLayersDeleteClick(e) { } function mmgisLinkModalsToLayersCloneClick(e) { - var mainThis = $(this) - .parent() - .parent() - .parent(); + var mainThis = $(this).parent().parent().parent(); var mainId = mainThis.attr("id"); mainId = mainId.substring(mainId.indexOf("_") + 1); makeLayerBarAndModal( @@ -1380,28 +1335,15 @@ function materializeDraggableMouseUp(e) { //Find out where the left edge of the bar lands relative to the layer tab //12 because materialize uses a 12 col system //console.log( $(this).parent().parent().width() ) - var colWidth = - ($(this) - .parent() - .parent() - .width() - - 304) / - 12; + var colWidth = ($(this).parent().parent().width() - 304) / 12; var layerBarLoc = - $(this).offset().left - - 150 - - $(this) - .parent() - .parent() - .offset().left; + $(this).offset().left - 150 - $(this).parent().parent().offset().left; var bestColumn = parseInt(layerBarLoc / colWidth); if (bestColumn < 1) bestColumn = 1; if (bestColumn > 10) bestColumn = 10; - var classString = $(this) - .attr("class") - .split(" "); + var classString = $(this).attr("class").split(" "); var classS = classString[classString.length - 2]; var classPush = classString[classString.length - 1]; $(this).removeClass(classS); @@ -1420,7 +1362,7 @@ function save() { look: {}, panels: [], tools: [], - layers: [] + layers: [], }; var prevIndentations = []; var prevLayerObjects = []; @@ -1437,7 +1379,7 @@ function save() { json.msv["view"] = [ $("#tab_initial_rows #ilat").val(), $("#tab_initial_rows #ilon").val(), - $("#tab_initial_rows #izoom").val() + $("#tab_initial_rows #izoom").val(), ]; json.msv["radius"] = {}; json.msv["radius"]["major"] = $("#tab_initial_rows #iradMaj").val(); @@ -1455,11 +1397,11 @@ function save() { $("#tab_projection #projection_boundsMinX").val(), $("#tab_projection #projection_boundsMinY").val(), $("#tab_projection #projection_boundsMaxX").val(), - $("#tab_projection #projection_boundsMaxY").val() + $("#tab_projection #projection_boundsMaxY").val(), ]; json.projection["origin"] = [ $("#tab_projection #projection_originX").val(), - $("#tab_projection #projection_originY").val() + $("#tab_projection #projection_originY").val(), ]; json.projection["reszoomlevel"] = $( "#tab_projection #projection_resZ" @@ -1527,7 +1469,7 @@ function save() { // because modals aren't ordered. $("#tab_layers_rows") .children("li") - .each(function() { + .each(function () { var layerObject = {}; //Get layer row identation var layerRow = $(this).find("a"); @@ -1535,11 +1477,7 @@ function save() { indentation = indentation[indentation.length - 1]; indentation = parseInt(indentation.substring(6)); //Find corresponding modal - var modal = $( - $(this) - .find("a") - .attr("href") - ); + var modal = $($(this).find("a").attr("href")); var modalId = modal.attr("modalId"); var modalName = modal.find("#nameEl input").val(); @@ -1554,11 +1492,12 @@ function save() { var modalUrl = modal.find("#urlEl input").val(); var modaldemtileUrl = modal.find("#demtileurlEl input").val(); var modalLegend = modal.find("#legendEl input").val(); - var modalTms = modal.find( "#tmsEl select option:selected" ).text().toLowerCase(); - if( modalTms == "true") - modalTms = true; - else - modalTms = false; + var modalTms = modal + .find("#tmsEl select option:selected") + .text() + .toLowerCase(); + if (modalTms == "true") modalTms = true; + else modalTms = false; var modalVis = modal .find("#visEl select option:selected") .text() @@ -1616,7 +1555,7 @@ function save() { layerObject.demtileurl = modaldemtileUrl; if (modalLegend != "undefined" && modalLegend != "") layerObject.legend = modalLegend; - if( modalType != "header" ) layerObject.tms = modalTms; + if (modalType != "header") layerObject.tms = modalTms; if (modalType != "header") layerObject.visibility = modalVis; if (!isNaN(modalViscut)) layerObject.visibilitycutoff = modalViscut; if (!isNaN(modalInitOpac)) layerObject.initialOpacity = modalInitOpac; @@ -1647,14 +1586,18 @@ function save() { layerObject.rotation.z = !isNaN(modalModelRotZ) ? modalModelRotZ : 0; layerObject.scale = !isNaN(modalModelScale) ? modalModelScale : 1; } - if (modalType == "point" || modalType == "vector") { + if ( + modalType == "point" || + modalType == "vector" || + modalType == "vectortile" + ) { layerObject.style = { className: styleName, color: styleStrcol, fillColor: styleFilcol, weight: styleWeight, fillOpacity: styleOpacity, - opacity: 1 + opacity: 1, }; if (modalVariable != "undefined") { try { @@ -1698,16 +1641,15 @@ function save() { } } - - if( !validName(modalName) ) { + if (!validName(modalName)) { isInvalidData = true; Materialize.toast( - "WARNING: Invalid layer name - " + modalName + "", - 5000 - ); - $("#toast_warningov1") - .parent() - .css("background-color", "#a11717"); + "WARNING: Invalid layer name - " + + modalName + + "", + 5000 + ); + $("#toast_warningov1").parent().css("background-color", "#a11717"); } //Check if data is properly filled out @@ -1719,9 +1661,7 @@ function save() { "WARNING: header with no name.", 5000 ); - $("#toast_warningh1") - .parent() - .css("background-color", "#a11717"); + $("#toast_warningh1").parent().css("background-color", "#a11717"); } break; case "tile": @@ -1731,18 +1671,14 @@ function save() { "WARNING: tile with undefined name.", 5000 ); - $("#toast_warningt1") - .parent() - .css("background-color", "#a11717"); + $("#toast_warningt1").parent().css("background-color", "#a11717"); } else if (modalName.length < 1) { isInvalidData = true; Materialize.toast( "WARNING: tile with no name.", 5000 ); - $("#toast_warningt2") - .parent() - .css("background-color", "#a11717"); + $("#toast_warningt2").parent().css("background-color", "#a11717"); } if (modalUrl == "undefined" || modalUrl == "") { isInvalidData = true; @@ -1752,9 +1688,7 @@ function save() { " has undefined url.", 5000 ); - $("#toast_warningt3") - .parent() - .css("background-color", "#a11717"); + $("#toast_warningt3").parent().css("background-color", "#a11717"); } if (isNaN(modalMinz)) { isInvalidData = true; @@ -1764,9 +1698,7 @@ function save() { " has undefined minz.", 5000 ); - $("#toast_warningt4") - .parent() - .css("background-color", "#a11717"); + $("#toast_warningt4").parent().css("background-color", "#a11717"); } else if (modalMinz < 0) { isInvalidData = true; Materialize.toast( @@ -1775,9 +1707,7 @@ function save() { " has minz under 0.", 5000 ); - $("#toast_warningt5") - .parent() - .css("background-color", "#a11717"); + $("#toast_warningt5").parent().css("background-color", "#a11717"); } if (isNaN(modalMaxnz)) { isInvalidData = true; @@ -1787,9 +1717,7 @@ function save() { " has undefined maxnz.", 5000 ); - $("#toast_warningt6") - .parent() - .css("background-color", "#a11717"); + $("#toast_warningt6").parent().css("background-color", "#a11717"); } if (isNaN(modalMaxz)) { isInvalidData = true; @@ -1799,9 +1727,7 @@ function save() { " has undefined maxz.", 5000 ); - $("#toast_warningt7") - .parent() - .css("background-color", "#a11717"); + $("#toast_warningt7").parent().css("background-color", "#a11717"); } if ( !isNaN(modalMinz) && @@ -1816,9 +1742,7 @@ function save() { " has minz larger than maxnz.", 5000 ); - $("#toast_warningt8") - .parent() - .css("background-color", "#a11717"); + $("#toast_warningt8").parent().css("background-color", "#a11717"); } break; case "vectortile": @@ -1925,9 +1849,7 @@ function save() { "WARNING: data with undefined name.", 5000 ); - $("#toast_warningt1") - .parent() - .css("background-color", "#a11717"); + $("#toast_warningt1").parent().css("background-color", "#a11717"); } else if (modalName.length < 1) { isInvalidData = true; Materialize.toast( @@ -2022,18 +1944,14 @@ function save() { "WARNING: point with undefined name.", 5000 ); - $("#toast_warningp1") - .parent() - .css("background-color", "#a11717"); + $("#toast_warningp1").parent().css("background-color", "#a11717"); } else if (modalName.length < 1) { isInvalidData = true; Materialize.toast( "WARNING: point with no name.", 5000 ); - $("#toast_warningp2") - .parent() - .css("background-color", "#a11717"); + $("#toast_warningp2").parent().css("background-color", "#a11717"); } if (modalUrl == "undefined" || modalUrl == "") { isInvalidData = true; @@ -2043,9 +1961,7 @@ function save() { " has undefined url.", 5000 ); - $("#toast_warningp3") - .parent() - .css("background-color", "#a11717"); + $("#toast_warningp3").parent().css("background-color", "#a11717"); } break; case "vector": @@ -2055,18 +1971,14 @@ function save() { "WARNING: vector with undefined name.", 5000 ); - $("#toast_warningv1") - .parent() - .css("background-color", "#a11717"); + $("#toast_warningv1").parent().css("background-color", "#a11717"); } else if (modalName.length < 1) { isInvalidData = true; Materialize.toast( "WARNING: vector with no name.", 5000 ); - $("#toast_warningv2") - .parent() - .css("background-color", "#a11717"); + $("#toast_warningv2").parent().css("background-color", "#a11717"); } if (modalUrl == "undefined" || modalUrl == "") { isInvalidData = true; @@ -2076,9 +1988,7 @@ function save() { " has undefined url.", 5000 ); - $("#toast_warningv3") - .parent() - .css("background-color", "#a11717"); + $("#toast_warningv3").parent().css("background-color", "#a11717"); } break; case "model": @@ -2088,18 +1998,14 @@ function save() { "WARNING: model with undefined name.", 5000 ); - $("#toast_warningm1") - .parent() - .css("background-color", "#a11717"); + $("#toast_warningm1").parent().css("background-color", "#a11717"); } else if (modalName.length < 1) { isInvalidData = true; Materialize.toast( "WARNING: model with no name.", 5000 ); - $("#toast_warningm2") - .parent() - .css("background-color", "#a11717"); + $("#toast_warningm2").parent().css("background-color", "#a11717"); } if (modalUrl == "undefined" || modalUrl == "") { isInvalidData = true; @@ -2109,9 +2015,7 @@ function save() { " has undefined url.", 5000 ); - $("#toast_warningm3") - .parent() - .css("background-color", "#a11717"); + $("#toast_warningm3").parent().css("background-color", "#a11717"); } if ( isNaN(modalModelLon) || @@ -2124,9 +2028,7 @@ function save() { " has invalid Lon, Lat, or Elev. Defaulting to 0.", 5000 ); - $("#toast_warningm4") - .parent() - .css("background-color", "#aeae09"); + $("#toast_warningm4").parent().css("background-color", "#aeae09"); } if ( isNaN(modalModelRotX) || @@ -2139,9 +2041,7 @@ function save() { " has invalid Rotation X, Y or Z. Defaulting to 0.", 5000 ); - $("#toast_warningm5") - .parent() - .css("background-color", "#aeae09"); + $("#toast_warningm5").parent().css("background-color", "#aeae09"); } if (isNaN(modalModelScale)) { Materialize.toast( @@ -2150,9 +2050,7 @@ function save() { " has invalid Scale. Defaulting to 0.", 5000 ); - $("#toast_warningm6") - .parent() - .css("background-color", "#aeae09"); + $("#toast_warningm6").parent().css("background-color", "#aeae09"); } break; } @@ -2194,9 +2092,7 @@ function save() { "WARNING: non-header(s).", 5000 ); - $("#toast_warning") - .parent() - .css("background-color", "#a11717"); + $("#toast_warning").parent().css("background-color", "#a11717"); } //SAVE HERE @@ -2205,18 +2101,14 @@ function save() { saveConfig(json); } else { Materialize.toast("Save Failed.", 5000); - $("#toast_failure") - .parent() - .css("background-color", "#a11717"); + $("#toast_failure").parent().css("background-color", "#a11717"); } } else { Materialize.toast( "No mission selected.", 5000 ); - $("#toast_warning") - .parent() - .css("background-color", "#aeae09"); + $("#toast_warning").parent().css("background-color", "#aeae09"); } } @@ -2227,9 +2119,9 @@ function passwordMakeMission() { url: calls.verify.url, data: { m: "ADMIN", - p: pass + p: pass, }, - success: function(data) { + success: function (data) { if (data == "success") { var missionname = $("#tab_new_mission_rows #nmmission").val(); var missionpassword = $("#tab_new_mission_rows #nmpassword").val(); @@ -2243,9 +2135,7 @@ function passwordMakeMission() { "Don't use special characters in the mission name.", 5000 ); - $("#toast_failure6") - .parent() - .css("background-color", "#a11717"); + $("#toast_failure6").parent().css("background-color", "#a11717"); return; } if (missionpassword == missionretypepassword) { @@ -2255,36 +2145,28 @@ function passwordMakeMission() { "Mission passwords don't match.", 5000 ); - $("#toast_failure3") - .parent() - .css("background-color", "#a11717"); + $("#toast_failure3").parent().css("background-color", "#a11717"); } } else { Materialize.toast( "At least one field is empty.", 5000 ); - $("#toast_failure4") - .parent() - .css("background-color", "#a11717"); + $("#toast_failure4").parent().css("background-color", "#a11717"); } } else { Materialize.toast( "Invalid Password", 5000 ); - $("#toast_bad_password2") - .parent() - .css("background-color", "#a11717"); + $("#toast_bad_password2").parent().css("background-color", "#a11717"); Materialize.toast( "Launch Failed.", 5000 ); - $("#toast_failure5") - .parent() - .css("background-color", "#a11717"); + $("#toast_failure5").parent().css("background-color", "#a11717"); } - } + }, }); } @@ -2294,9 +2176,9 @@ function makeMission(missionname, missionpassword) { url: calls.make_mission.url, data: { missionname: missionname, - password: missionpassword + password: missionpassword, }, - success: function(data) { + success: function (data) { data = JSON.parse(data); if (data["status"] == "success") { Materialize.toast( @@ -2305,10 +2187,8 @@ function makeMission(missionname, missionpassword) { " Created. Page will now reload...", 5000 ); - $("#toast_success1") - .parent() - .css("background-color", "#1565C0"); - setTimeout(function() { + $("#toast_success1").parent().css("background-color", "#1565C0"); + setTimeout(function () { location.reload(); }, 3000); } else { @@ -2316,11 +2196,9 @@ function makeMission(missionname, missionpassword) { "" + data["message"] + "", 5000 ); - $("#toast_failure7") - .parent() - .css("background-color", "#a11717"); + $("#toast_failure7").parent().css("background-color", "#a11717"); } - } + }, }); } @@ -2333,9 +2211,7 @@ function addMission() { "Don't use special characters in the mission name.", 5000 ); - $("#toast_failure6") - .parent() - .css("background-color", "#a11717"); + $("#toast_failure6").parent().css("background-color", "#a11717"); return; } @@ -2346,9 +2222,9 @@ function addMission() { url: calls.add.url, data: { mission: missionname, - makedir: makedir + makedir: makedir, }, - success: function(data) { + success: function (data) { if (data.status == "success") { Materialize.toast( "Mission: " + @@ -2356,10 +2232,8 @@ function addMission() { " Created. Page will now reload...", 4000 ); - $("#toast_success1") - .parent() - .css("background-color", "#1565C0"); - setTimeout(function() { + $("#toast_success1").parent().css("background-color", "#1565C0"); + setTimeout(function () { location.reload(); }, 4000); } else { @@ -2367,20 +2241,16 @@ function addMission() { "" + data["message"] + "", 5000 ); - $("#toast_failure7") - .parent() - .css("background-color", "#a11717"); + $("#toast_failure7").parent().css("background-color", "#a11717"); } - } + }, }); } else { Materialize.toast( "Please enter a new mission name.", 5000 ); - $("#toast_failure4") - .parent() - .css("background-color", "#a11717"); + $("#toast_failure4").parent().css("background-color", "#a11717"); } } @@ -2390,17 +2260,15 @@ function saveConfig(json) { url: calls.upsert.url, data: { mission: mission, - config: JSON.stringify(json) + config: JSON.stringify(json), }, - success: function(data) { + success: function (data) { if (data.status == "success") { Materialize.toast( "Save Successful.", 1600 ); - $("#toast_success") - .parent() - .css("background-color", "#1565C0"); + $("#toast_success").parent().css("background-color", "#1565C0"); /* Materialize.toast( "Page will now reload..." , 4000); $( "#toast_success3" ).parent().css("background-color", "#1565C0"); @@ -2411,11 +2279,9 @@ function saveConfig(json) { "" + data["message"] + "", 5000 ); - $("#toast_failure8") - .parent() - .css("background-color", "#a11717"); + $("#toast_failure8").parent().css("background-color", "#a11717"); } - } + }, }); } @@ -2426,9 +2292,9 @@ function passwordWriteJSON(filename, json) { url: calls.verify.url, data: { m: mission.toLowerCase(), - p: pass + p: pass, }, - success: function(data) { + success: function (data) { if (data == "success") { //Try changing the mission name if it was changed if (mission.toLowerCase() != json.msv.mission.toLowerCase()) { @@ -2437,9 +2303,9 @@ function passwordWriteJSON(filename, json) { url: calls.rename_mission.url, data: { mission: mission.toLowerCase(), - tomission: json.msv.mission + tomission: json.msv.mission, }, - success: function(data) { + success: function (data) { data = JSON.parse(data); if (data["status"] == "success") { Materialize.toast( @@ -2453,7 +2319,7 @@ function passwordWriteJSON(filename, json) { filename = calls.missionPath + "" + json.msv.mission + "/config.json"; - writeJSON(filename, json, function() { + writeJSON(filename, json, function () { Materialize.toast( "Page will now reload...", 3000 @@ -2461,7 +2327,7 @@ function passwordWriteJSON(filename, json) { $("#toast_success3") .parent() .css("background-color", "#1565C0"); - setTimeout(function() { + setTimeout(function () { location.reload(); }, 3000); }); @@ -2474,7 +2340,7 @@ function passwordWriteJSON(filename, json) { .parent() .css("background-color", "#a11717"); } - } + }, }); } else { writeJSON(filename, json); @@ -2484,18 +2350,14 @@ function passwordWriteJSON(filename, json) { "Invalid Password", 5000 ); - $("#toast_bad_password") - .parent() - .css("background-color", "#a11717"); + $("#toast_bad_password").parent().css("background-color", "#a11717"); Materialize.toast( "Save Failed.", 5000 ); - $("#toast_failure2") - .parent() - .css("background-color", "#a11717"); + $("#toast_failure2").parent().css("background-color", "#a11717"); } - } + }, }); } function writeJSON(filename, json, callback) { @@ -2508,19 +2370,17 @@ function writeJSON(filename, json, callback) { data: { filename: calls.write_json.pathprefix + filename, mission: mission, - json: json + json: json, }, - success: function(data) { + success: function (data) { Materialize.toast( "Save Successful.", 5000 ); - $("#toast_success") - .parent() - .css("background-color", "#1565C0"); + $("#toast_success").parent().css("background-color", "#1565C0"); if (typeof callback === "function") callback(); - } + }, }); } @@ -2530,7 +2390,7 @@ function projectionPopulateFromXML() { type: "GET", url: xmlPath, dataType: "xml", - success: function(xml) { + success: function (xml) { try { $("#tab_projection #projection_boundsMinX").val( $(xml).find("BoundingBox")[0].attributes["minx"].value @@ -2565,7 +2425,7 @@ function projectionPopulateFromXML() { } Materialize.updateTextFields(); }, - error: function(XMLHttpRequest, textStatus, errorThrown) { + error: function (XMLHttpRequest, textStatus, errorThrown) { Materialize.toast( "Failed to Populate From XML", 5000 @@ -2573,7 +2433,7 @@ function projectionPopulateFromXML() { $("#toast_failure_populateXML") .parent() .css("background-color", "#a11717"); - } + }, }); } function projectionToggleCustom(force) { @@ -2615,7 +2475,7 @@ function tilelayerPopulateFromXML(modalId) { type: "GET", url: xmlPath, dataType: "xml", - success: function(xml) { + success: function (xml) { try { var tLen = $(xml).find("TileSet").length; var minzValue = $(xml).find("TileSet")[0].attributes["order"].value; @@ -2642,7 +2502,7 @@ function tilelayerPopulateFromXML(modalId) { } Materialize.updateTextFields(); }, - error: function(XMLHttpRequest, textStatus, errorThrown) { + error: function (XMLHttpRequest, textStatus, errorThrown) { Materialize.toast( "Failed to Populate From " + xmlPath + @@ -2652,7 +2512,7 @@ function tilelayerPopulateFromXML(modalId) { $("#toast_failure_populateXMLtilelayer") .parent() .css("background-color", "#a11717"); - } + }, }); } @@ -2667,25 +2527,26 @@ function layerPopulateVariable(modalId) { prop: "{prop}", dataset: "{dataset}", column: "{column}", - type: "{none || images}" - } + type: "{none || images}", + }, ]; currentLayerVars.links = currentLayerVars.links || [ { name: "example", - link: "url/?param={prop}" - } + link: "url/?param={prop}", + }, ]; currentLayerVars.info = currentLayerVars.info || [ { which: "last", icon: "material design icon", - value: "Prop: {prop}" - } + value: "Prop: {prop}", + }, ]; - currentLayerVars.search = currentLayerVars.search || "(prop1) round(prop2.1) rmunder(prop_3)" + currentLayerVars.search = + currentLayerVars.search || "(prop1) round(prop2.1) rmunder(prop_3)"; layerEditors[modalId].setValue(JSON.stringify(currentLayerVars, null, 4)); } @@ -2701,7 +2562,7 @@ function vtlayerPopulateStyle(modalId) { type: "GET", url: metadatajsonPath, dataType: "json", - success: function(json) { + success: function (json) { var layers = JSON.parse(json.json).vector_layers; var newLayerStyles = {}; @@ -2713,7 +2574,7 @@ function vtlayerPopulateStyle(modalId) { fillOpacity: 0.5, opacity: 1, radius: 4, - weight: 2 + weight: 2, }; } if (layerEditors[modalId]) { @@ -2748,7 +2609,7 @@ function vtlayerPopulateStyle(modalId) { ); } }, - error: function(XMLHttpRequest, textStatus, errorThrown) { + error: function (XMLHttpRequest, textStatus, errorThrown) { Materialize.toast( "Failed to Populate From " + metadatajsonPath + @@ -2758,7 +2619,7 @@ function vtlayerPopulateStyle(modalId) { $("#toast_failure_populatemetajsonvtlayer") .parent() .css("background-color", "#a11717"); - } + }, }); } @@ -2786,31 +2647,27 @@ function populateVersions(versions) { $("#tab_overall_versions").append(li); } - $(".version_set").on("click", function() { + $(".version_set").on("click", function () { $.ajax({ type: calls.upsert.type, url: calls.upsert.url, data: { mission: $(this).attr("mission"), - version: $(this).attr("version") + version: $(this).attr("version"), }, - success: function(data) { + success: function (data) { if (data.status == "success") { Materialize.toast( "Save Successful.", 4000 ); - $("#toast_success") - .parent() - .css("background-color", "#1565C0"); + $("#toast_success").parent().css("background-color", "#1565C0"); Materialize.toast( "Page will now reload...", 4000 ); - $("#toast_success3") - .parent() - .css("background-color", "#1565C0"); - setTimeout(function() { + $("#toast_success3").parent().css("background-color", "#1565C0"); + setTimeout(function () { location.reload(); }, 4000); } else { @@ -2818,15 +2675,13 @@ function populateVersions(versions) { "" + data["message"] + "", 5000 ); - $("#toast_failure8") - .parent() - .css("background-color", "#a11717"); + $("#toast_failure8").parent().css("background-color", "#a11717"); } - } + }, }); }); - $(".version_download").on("click", function() { + $(".version_download").on("click", function () { let downloadMission = $(this).attr("mission"); let downloadVersion = $(this).attr("version"); @@ -2836,9 +2691,9 @@ function populateVersions(versions) { data: { mission: downloadMission, version: downloadVersion, - full: true + full: true, }, - success: function(data) { + success: function (data) { if (data.status == "success") { downloadObject( data.config, @@ -2850,19 +2705,15 @@ function populateVersions(versions) { "Download Successful.", 4000 ); - $("#toast_success") - .parent() - .css("background-color", "#1565C0"); + $("#toast_success").parent().css("background-color", "#1565C0"); } else { Materialize.toast( "" + data["message"] + "", 5000 ); - $("#toast_failure8") - .parent() - .css("background-color", "#a11717"); + $("#toast_failure8").parent().css("background-color", "#a11717"); } - } + }, }); }); } @@ -2893,20 +2744,24 @@ function downloadObject(exportObj, exportName, exportExt, prettify) { } function toTitleCase(str) { - return str.replace(/\w\S*/g, function(txt) { + return str.replace(/\w\S*/g, function (txt) { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); }); } function validName(name) { - if( name.length > 0 && name.length == name.replace(/[`~!@#$%^&*|+\=?;:'",.<>\{\}\[\]\\\/]/gi, "").length && /^\d+$/.test(name[0]) == false ) { + if ( + name.length > 0 && + name.length == + name.replace(/[`~!@#$%^&*|+\=?;:'",.<>\{\}\[\]\\\/]/gi, "").length && + /^\d+$/.test(name[0]) == false + ) { try { - $( '.' + name.replace(/\s/g, '').toLowerCase() ); - return true; - } - catch (e) { + $("." + name.replace(/\s/g, "").toLowerCase()); + return true; + } catch (e) { return false; } } return false; -} \ No newline at end of file +} diff --git a/css/mmgis.css b/css/mmgis.css index f7c8480a..636edb24 100644 --- a/css/mmgis.css +++ b/css/mmgis.css @@ -1,703 +1,729 @@ -@font-face { - font-family: lato; - src: url(../../../public/fonts/lato/Lato-Regular.ttf), - url(../public/fonts/lato/Lato-Regular.ttf); -} -@font-face { - font-family: lato-light; - src: url(../../../public/fonts/lato/Lato-Light.ttf), - url(../public/fonts/lato/Lato-Light.ttf); -} -@font-face { - font-family: roboto; - src: url(../../../public/fonts/roboto/Roboto-Regular.ttf), - url(../public/fonts/roboto/Roboto-Regular.ttf); -} -@font-face { - font-family: venus; - src: url(../../../public/fonts/taxidriver/TaxiDriver.ttf), - url(../public/fonts/taxidriver/TaxiDriver.ttf); -} - -html, -body { - width: 100%; - height: 100%; - margin: 0; -} -body { - background-color: var(--color-a); - /*background: linear-gradient(to top, #0F1111, #1E2021);*/ - - overflow: hidden !important; - color: #fff; - font-family: roboto; -} - -/*Color variables*/ -:root { - --color-mmgis: #26a8ff; - --color-a: #1f1f1f; - --color-b: #555555; - --color-c: #009eff; - --color-d: #2a2a2a; - --color-e: #4f4f4f; - --color-f: #e1e1e1; - --color-h: #fff800; - --color-g: #121218; - --color-i: #363636; - - --color-m1: #394044; - --color-m2: #222829; - --color-m3: #1b1d1e; - --color-mh: #ffdd5c; - --color-mw: #f8f8f8; - --color-mw2: #efefef; -} -#mmgislogo { - position: relative; - top: 2px; -} - -#nodeenv { - display: none; - position: absolute; - width: 36px; - height: 1px; - left: 0; - bottom: 0; - overflow: hidden; - pointer-events: none; - background: #333; - z-index: 10000; -} - -#topBar { -} - -#topBarHelp { - color: #999; - opacity: 0.8; - transition: color 0.3s; - width: 24px; - height: 24px; -} -#topBarHelp:hover { - color: #fff; -} - -#topBarLink { - color: #999; - opacity: 0.8; - transition: color 0.3s; - width: 24px; - height: 24px; -} -#topBarLink:hover { - color: #fff; -} - -#topBarFullscreen { - color: #999; - opacity: 0.8; - transition: color 0.3s; - width: 24px; - height: 24px; -} -#topBarFullscreen:hover { - color: #fff; -} - -#toggleUI { - opacity: 0.8; - cursor: pointer; - transition: color 0.3s; - width: 24px; - height: 24px; -} - -#splitscreens { - width: 100%; - height: 100%; - position: absolute; - left: 0px; - top: 0px; -} -#vmgScreen { - width: 100%; - height: 100%; - position: absolute; -} -.splitterV { - height: 100%; - position: absolute; - cursor: col-resize; - display: flex; - align-items: center; - z-index: 500; - transition: opacity 0.3s; -} -.splitterV:hover { - opacity: 0.9; -} - -.splitterVInner { - position: absolute; - left: -50%; - height: 100vh; - background: linear-gradient( - to right, - rgba(0, 0, 0, 0), - rgba(0, 0, 0, 0.2), - rgba(0, 0, 0, 0) - ); - text-align: center; - display: inline-flex; - justify-content: space-evenly; - align-items: center; -} - -.splitterVInner i { - width: 48px; - height: 30px; - line-height: 30px; - cursor: col-resize; - color: #fff; -} -.splitterVInner i:hover { - color: var(--color-mmgis); - cursor: pointer; -} -#mapSplitInnerViewerInfo { - position: absolute; - left: 61px; - background: var(--color-a); - height: 30px; - line-height: 30px; - padding: 0px 8px; - border-radius: 3px; - pointer-events: none; - opacity: 0; - transition: opacity 0.2s ease-out; -} -.splitterVInner i:hover ~ #mapSplitInnerViewerInfo { - opacity: 1; -} - -#mapSplitInnerGlobeInfo { - position: absolute; - left: -82px; - background: var(--color-a); - height: 30px; - line-height: 30px; - padding: 0px 8px; - border-radius: 3px; - pointer-events: none; - opacity: 0; - transition: opacity 0.2s ease-out; -} -.splitterVInner i:hover ~ #mapSplitInnerGlobeInfo { - opacity: 1; -} - -.splitterH { - /*background-color: #1A1A1A;*/ - width: 100%; - position: absolute; - /*border-top: 1px solid #333;*/ - /*border-bottom: 1px solid rgba(0,0,0,0.95);*/ - cursor: row-resize; - z-index: 6; -} - -.splitterText { - -webkit-transition: all 0.25s ease-in; - transition: all 0.25s ease-in; - position: absolute; - font-weight: bolder; - transform: rotate(90deg); - margin-left: 0px; - font-size: 18px; - color: #555; - transform-origin: bottom left 0; -} -.splitterText.active { - margin-left: 24px; - color: #999; - font-size: 14px !important; - transform: rotate(0deg) !important; -} - -#tScreen { - width: 100%; - position: absolute; - bottom: 0px; -} -#toolsSplit { - left: 0px; -} -#toolsWrapper { - /*background-color: #071a21;*/ - height: 0%; - width: 100%; - position: absolute; -} - -#toolbar { - width: auto; - background: var(--color-a); - height: 30px; - position: absolute; - left: 0px; - bottom: 0px; -} - -#logoGoBack { - transition: 0.25s ease-in; - position: absolute; - bottom: 1px; - left: 24px; - opacity: 0.2; - z-index: 103; -} -#logoGoBack:hover { - opacity: 1; -} - -.leaflet-container { - background: black; -} -.leaflet-popup-content-wrapper { - background: var(--color-a); - color: #e1e1e1; - font-size: 14px; - line-height: 24px; - border-radius: 0; - max-height: 50vh; - overflow-y: auto; - overflow-x: hidden; -} -.leaflet-popup-tip { -} -.leaflet-popup-close-button { - margin-right: 0px; - right: -20px !important; - padding: 3px 0px 0 0 !important; - width: 20px !important; - height: 20px !important; - color: #ffffff !important; - background: #26a8ff !important; -} -.leaflet-popup-content { - margin: 1px 7px 1px 7px; -} - -.leaflet-marker-pane.hideDivIcons .leaflet-div-icon { - opacity: 0 !important; - pointer-events: none; -} - -.noPointerEvents { - pointer-events: none; -} -.noPointerEventsImportant { - pointer-events: none !important; -} - -.mouseLngLat { - -webkit-transition: all 0.25s ease-in; - transition: all 0.25s ease-in; - opacity: 0.9; -} -.mouseLngLat:hover { - opacity: 1; -} - -#topBarTitle { - -webkit-transition: color 0.25s ease-in; - transition: color 0.25s ease-in; - color: var(--color-f); -} -#topBarTitle:hover { - color: #bbb; -} - -.topHome { - display: flex; - padding: 0px 5px; - border: 1px solid var(--color-m1); - border-radius: 3px; - color: rgb(170, 170, 170); - line-height: 24px; - margin-left: 8px; - opacity: 0.8; - transition: opacity 0.25s ease-out; -} -.topHome:hover { - cursor: pointer; - opacity: 1; -} - -.mainInfo > div { - display: flex; - padding: 0px 8px; - border: 1px solid var(--color-m1); - color: var(--color-mw2); - margin-left: 8px; - opacity: 0.8; - transition: opacity 0.25s ease-out; -} -.mainInfo > div:hover { - cursor: pointer; - opacity: 1; -} -.mainInfo > div:first-child { - margin-left: 0; -} - -.mainInfo > div > div { - display: flex; - white-space: nowrap; - line-height: 29px; - font-size: 14px; - font-weight: bold; - cursor: pointer; - margin: 0px; -} - -.mainInfo > div > i { - line-height: 26px; - margin-right: 5px; -} - -#mainDescMission { - -webkit-transition: all 0.25s ease-in; - transition: all 0.25s ease-in; - opacity: 0.8; -} -#mainDescMission:hover { - opacity: 1; -} -#mainDescSite { - -webkit-transition: all 0.25s ease-in; - transition: all 0.25s ease-in; - opacity: 0.8; -} -#mainDescSite:hover { - opacity: 1; -} -#mainDescPoint { - -webkit-transition: all 0.25s ease-in; - transition: all 0.25s ease-in; - opacity: 0.8; -} -#mainDescPoint:hover { - opacity: 1; -} -#mainDescPoint a { - height: 100%; - line-height: 28px; - padding: 0px 8px; - font-size: 11px; - background: var(--color-e); - color: white; - border-left: 1px solid var(--color-b); - opacity: 0.8; - transition: opacity 0.25s ease-out, background 0.25s ease-out; -} -#mainDescPoint a > i { - margin-left: 3px; -} -#mainDescPoint a:first-child { - margin-left: 4px; - border-left: none; -} -#mainDescPoint a:last-child { -} -#mainDescPoint a:hover { - opacity: 1; - background: var(--color-mmgis); -} - -.ui.selection.list > .item.customColor1:hover { - background: rgba(18, 66, 84, 0.5) !important; -} - -.leaflet-tile-pane { - image-rendering: pixelated; -} - -.leaflet-control-zoom { - border: none !important; - margin-top: 5px !important; - margin-right: 5px !important; -} -.leaflet-control-zoom a { - color: #aaa; - width: 30px; - height: 30px; - line-height: 29px !important; - border-radius: 3px; - margin-bottom: 4px; - border: 1px solid #000000 !important; - background: var(--color-a) !important; - transition: color 0.2s ease-in 0s; -} -.leaflet-control-zoom a:hover { - color: #fff; -} -#loginoutButton, -#forceSignupButton { - transition: color 0.2s ease-in 0s; -} -#loginoutButton:hover, -#forceSignupButton:hover { - color: #fff; -} - -.leaflet-popup-content-wrapper::-webkit-scrollbar-track { - -webkit-box-shadow: inset 0px 0px 6px rgba(0, 0, 0, 0.3); - background-color: Transparent; -} - -.leaflet-popup-content-wrapper::-webkit-scrollbar { - width: 2px; - height: 2px; - background-color: var(--color-a); -} - -.leaflet-popup-content-wrapper::-webkit-scrollbar-thumb { - border-radius: 0px; - background-color: #26a8ff; -} - -.leaflet-popup-annotation { - left: 0 !important; - top: -10px !important; -} -.leaflet-popup-annotation > .leaflet-popup-content-wrapper { - background: transparent; - box-shadow: none; - max-height: none !important; -} -.leaflet-popup-annotation .leaflet-popup-content { - margin: 0; - width: auto !important; -} -.leaflet-popup-annotation textarea { - background: rgba(0, 0, 0, 0.4); - color: white; - border: none; - padding: 3px; -} -.leaflet-popup-annotation .leaflet-popup-tip-container { - display: none; -} - -/*Leaflet fix that prevents shifting at higher zooms on single images*/ -.leaflet-container img.leaflet-image-layer { - max-width: none !important; -} - -/*Openseadragon navigator border color*/ -.displayregion { - border: 2px solid var(--color-mh) !important; -} -.openseadragon-container .navigator { - border: 1px solid var(--color-a) !important; -} -.osd-custom-buttons > div { - width: 30px; - height: 30px; - background: var(--color-a); - line-height: 31px; - text-align: center; - margin-right: 9px; - cursor: pointer; - border-radius: 3px; - color: #bbb; - display: inline-block; - position: relative; - transition: color 0.2s ease-out; -} -.osd-custom-buttons > div:hover { - color: white; -} - -.cplot_axis path { - fill: none; - stroke: #161616; -} -.cplot_axis line { - fill: none; - stroke: #fff; -} - -#toolcontroller_incdiv > div { - color: #bbb; -} -#toolcontroller_incdiv > div:hover { - color: white; -} - -.ui-menu .ui-menu-item { - color: rgba(255, 255, 255, 0.87); - border: 1px solid transparent; - border-top: 1px solid var(--color-b); - transition: background 0.2s cubic-bezier(0.445, 0.05, 0.55, 0.95), - color 0.2s cubic-bezier(0.445, 0.05, 0.55, 0.95); -} -.ui-menu .ui-menu-item.ui-state-focus { - margin: 0; - background: var(--color-g); - color: white; -} -.ui-menu .ui-menu-item:hover { - background: var(--color-g); - color: white; -} -#SearchClear { - margin-left: 0px; - display: inherit; - position: absolute; - right: 36px; - width: 30px; - height: 30px; - padding-left: 7px; - line-height: 28px; - color: var(--color-f); - cursor: pointer; -} -#SearchClear:hover { - color: white; -} -#SearchBoth { - margin-left: 0px; - display: inherit; - position: absolute; - right: 6px; - width: 30px; - height: 30px; - padding-left: 7px; - line-height: 28px; - color: var(--color-f); - cursor: pointer; -} -#SearchBoth:hover { - color: white; -} - -#scaleBar { - margin: 0; - pointer-events: none; - z-index: 1001; - opacity: 0.75; -} - -.autoWidth { - width: auto !important; -} - -.childpointerevents > * { - pointer-events: auto; -} - -*:focus { - outline: none; -} -/*Clear white highlighting*/ -::selection { - background-color: Transparent; - color: #888; -} - -.flexbetween { - display: flex; - justify-content: space-between; -} - -.leaflet-control-scalefactor { - position: absolute; - bottom: 25px; - left: 10px; - display: flex; - margin-left: 2px !important; -} -.leaflet-control-scalefactor-line { - background: #161616; - box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.2); - padding: 0px 4px; - color: #dcdcdc; - cursor: default; - opacity: 0.85; - user-select: none; - white-space: nowrap; -} -.leaflet-control-scalefactor-goto { - white-space: nowrap; - padding: 0px 4px; - background: #161616; - box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.2); - padding: 0px 4px 0px 0px; - color: #dcdcdc; - cursor: pointer; - opacity: 0.85; - user-select: none; -} - -.cp-color-picker { - background: var(--color-a); - border-radius: 0px !important; - margin-left: -1px; - margin-top: 1px; -} - -.leaflet-tile-pane svg.leaflet-tile { - pointer-events: none !important; -} -.leaflet-tile-pane svg.leaflet-tile path.leaflet-interactive { - pointer-events: auto; -} - -.kindsWedgeLayer { - pointer-events: none !important; -} - -.topBarSearch { - height: 100%; - max-width: 380px; - width: 100vw; - background: var(--color-i); - border: 1px solid var(--color-e); - padding: 0px 8px; - font-size: 14px; - color: var(--color-f); - flex: 1; - transition: background 0.2s ease-out, border 0.2s ease-out; -} -.topBarSearch:hover { - background: var(--color-a); -} -.topBarSearch:focus { - background: var(--color-a); - border: 1px solid var(--color-mmgis); -} -.topBarSearch::placeholder { - color: #aaa; -} - -.hoverOpensPanel .hoverOpeningPanel { - opacity: 0; - pointer-events: none; - transition: opacity 0.2s cubic-bezier(0.445, 0.05, 0.55, 0.95), - background 0.2s cubic-bezier(0.445, 0.05, 0.55, 0.95); -} -.hoverOpensPanel .hoverOpeningPanel div:hover { - cursor: pointer; - color: white; - background: #151515; -} -.hoverOpensPanel:hover .hoverOpeningPanel { - opacity: 1; - pointer-events: initial; -} +@font-face { + font-family: lato; + src: url(../../../public/fonts/lato/Lato-Regular.ttf), + url(../public/fonts/lato/Lato-Regular.ttf); +} +@font-face { + font-family: lato-light; + src: url(../../../public/fonts/lato/Lato-Light.ttf), + url(../public/fonts/lato/Lato-Light.ttf); +} +@font-face { + font-family: roboto; + src: url(../../../public/fonts/roboto/Roboto-Regular.ttf), + url(../public/fonts/roboto/Roboto-Regular.ttf); +} +@font-face { + font-family: venus; + src: url(../../../public/fonts/taxidriver/TaxiDriver.ttf), + url(../public/fonts/taxidriver/TaxiDriver.ttf); +} + +html, +body { + width: 100%; + height: 100%; + margin: 0; +} +body { + background-color: var(--color-a); + /*background: linear-gradient(to top, #0F1111, #1E2021);*/ + + overflow: hidden !important; + color: #fff; + font-family: roboto; +} + +/*Color variables*/ +:root { + --color-mmgis: #26a8ff; + --color-a: #0f0f0f; /*#1f1f1f;*/ + --color-a1: #1f1f1f; + --color-b: #555555; + --color-c: #009eff; + --color-d: #2a2a2a; + --color-e: #4f4f4f; + --color-f: #e1e1e1; + --color-h: #fff800; + --color-g: #121218; + --color-i: #363636; + + --color-m1: #394044; + --color-m2: #222829; + --color-m3: #1b1d1e; + --color-mh: #ffdd5c; + --color-mw: #f8f8f8; + --color-mw2: #efefef; +} +#mmgislogo { + position: relative; + top: 2px; +} + +#nodeenv { + display: none; + position: absolute; + width: 36px; + height: 1px; + left: 0; + bottom: 0; + overflow: hidden; + pointer-events: none; + background: #333; + z-index: 10000; +} + +#topBar { +} + +#topBarHelp { + color: #999; + opacity: 0.8; + transition: color 0.3s; + width: 24px; + height: 24px; +} +#topBarHelp:hover { + color: #fff; +} + +#topBarLink { + color: #999; + opacity: 0.8; + transition: color 0.3s; + width: 24px; + height: 24px; +} +#topBarLink:hover { + color: #fff; +} + +#topBarFullscreen { + color: #999; + opacity: 0.8; + transition: color 0.3s; + width: 24px; + height: 24px; +} +#topBarFullscreen:hover { + color: #fff; +} + +#toggleUI { + opacity: 0.8; + cursor: pointer; + transition: color 0.3s; + width: 24px; + height: 24px; +} + +#splitscreens { + width: 100%; + height: 100%; + position: absolute; + left: 0px; + top: 0px; +} +#vmgScreen { + width: 100%; + height: 100%; + position: absolute; +} +.splitterV { + height: 100%; + position: absolute; + cursor: col-resize; + display: flex; + align-items: center; + z-index: 500; + transition: opacity 0.3s; +} +.splitterV:hover { + opacity: 0.9; +} + +.splitterVInner { + position: absolute; + left: -50%; + height: 100vh; + background: linear-gradient( + to right, + rgba(0, 0, 0, 0), + rgba(0, 0, 0, 0.2), + rgba(0, 0, 0, 0) + ); + text-align: center; + display: inline-flex; + justify-content: space-evenly; + align-items: center; +} + +.splitterVInner i { + width: 48px; + height: 30px; + line-height: 30px; + cursor: col-resize; + color: #fff; +} +.splitterVInner i:hover { + color: var(--color-mmgis); + cursor: pointer; +} +#mapSplitInnerViewerInfo { + position: absolute; + left: 61px; + background: var(--color-a); + height: 30px; + line-height: 30px; + padding: 0px 8px; + border-radius: 3px; + pointer-events: none; + opacity: 0; + transition: opacity 0.2s ease-out; +} +.splitterVInner i:hover ~ #mapSplitInnerViewerInfo { + opacity: 1; +} + +#mapSplitInnerGlobeInfo { + position: absolute; + left: -82px; + background: var(--color-a); + height: 30px; + line-height: 30px; + padding: 0px 8px; + border-radius: 3px; + pointer-events: none; + opacity: 0; + transition: opacity 0.2s ease-out; +} +.splitterVInner i:hover ~ #mapSplitInnerGlobeInfo { + opacity: 1; +} + +.splitterH { + /*background-color: #1A1A1A;*/ + width: 100%; + position: absolute; + /*border-top: 1px solid #333;*/ + /*border-bottom: 1px solid rgba(0,0,0,0.95);*/ + cursor: row-resize; + z-index: 6; +} + +.splitterText { + -webkit-transition: all 0.25s ease-in; + transition: all 0.25s ease-in; + position: absolute; + font-weight: bolder; + transform: rotate(90deg); + margin-left: 0px; + font-size: 18px; + color: #555; + transform-origin: bottom left 0; +} +.splitterText.active { + margin-left: 24px; + color: #999; + font-size: 14px !important; + transform: rotate(0deg) !important; +} + +#tScreen { + width: 100%; + position: absolute; + bottom: 0px; +} +#toolsSplit { + left: 0px; +} +#toolsWrapper { + /*background-color: #071a21;*/ + height: 0%; + width: 100%; + position: absolute; +} + +#toolbar { + width: auto; + background: var(--color-a); + height: 30px; + position: absolute; + left: 0px; + bottom: 0px; +} + +#logoGoBack { + transition: 0.25s ease-in; + position: absolute; + bottom: 1px; + left: 24px; + opacity: 0.2; + z-index: 103; +} +#logoGoBack:hover { + opacity: 1; +} + +.leaflet-container { + background: black; +} +.leaflet-popup-content-wrapper { + background: var(--color-a); + color: #e1e1e1; + font-size: 14px; + line-height: 24px; + border-radius: 0; + max-height: 50vh; + overflow-y: auto; + overflow-x: hidden; +} +.leaflet-popup-tip { +} +.leaflet-popup-close-button { + margin-right: 0px; + right: -20px !important; + padding: 3px 0px 0 0 !important; + width: 20px !important; + height: 20px !important; + color: #ffffff !important; + background: #26a8ff !important; +} +.leaflet-popup-content { + margin: 1px 7px 1px 7px; +} + +.leaflet-marker-pane.hideDivIcons .leaflet-div-icon { + opacity: 0 !important; + pointer-events: none; +} + +.noPointerEvents { + pointer-events: none; +} +.noPointerEventsImportant { + pointer-events: none !important; +} + +.mouseLngLat { + -webkit-transition: all 0.25s ease-in; + transition: all 0.25s ease-in; + opacity: 0.9; +} +.mouseLngLat:hover { + opacity: 1; +} + +#topBarTitle { + -webkit-transition: color 0.25s ease-in; + transition: color 0.25s ease-in; + color: var(--color-f); +} +#topBarTitle:hover { + color: #bbb; +} + +.topHome { + display: flex; + padding: 0px 5px; + border: 1px solid var(--color-m1); + border-radius: 3px; + color: rgb(170, 170, 170); + line-height: 24px; + margin-left: 8px; + opacity: 0.8; + transition: opacity 0.25s ease-out; +} +.topHome:hover { + cursor: pointer; + opacity: 1; +} + +.mainInfo > div { + display: flex; + padding: 0px 8px; + border: 1px solid var(--color-b); + background: var(--color-i); + color: var(--color-mw2); + margin-left: 8px; + opacity: 0.8; + transition: opacity 0.25s ease-out; +} +.mainInfo > div:hover { + cursor: pointer; + opacity: 1; +} +.mainInfo > div:first-child { + margin-left: 0; +} + +.mainInfo > div > div { + display: flex; + white-space: nowrap; + line-height: 29px; + font-size: 14px; + font-weight: bold; + cursor: pointer; + margin: 0px; +} + +.mainInfo > div > i { + line-height: 26px; + margin-right: 5px; +} + +#mainDescMission { + -webkit-transition: all 0.25s ease-in; + transition: all 0.25s ease-in; + opacity: 0.8; +} +#mainDescMission:hover { + opacity: 1; +} +#mainDescSite { + -webkit-transition: all 0.25s ease-in; + transition: all 0.25s ease-in; + opacity: 0.8; +} +#mainDescSite:hover { + opacity: 1; +} +#mainDescPoint { + -webkit-transition: all 0.25s ease-in; + transition: all 0.25s ease-in; + opacity: 0.8; +} +#mainDescPoint:hover { + opacity: 1; +} +#mainDescPoint a { + height: 100%; + line-height: 28px; + padding: 0px 8px; + font-size: 11px; + background: var(--color-e); + color: white; + border-left: 1px solid var(--color-b); + opacity: 0.8; + transition: opacity 0.25s ease-out, background 0.25s ease-out; +} +#mainDescPoint a > i { + margin-left: 3px; +} +#mainDescPoint a:first-child { + margin-left: 4px; + border-left: none; +} +#mainDescPoint a:last-child { +} +#mainDescPoint a:hover { + opacity: 1; + background: var(--color-mmgis); +} + +.ui.selection.list > .item.customColor1:hover { + background: rgba(18, 66, 84, 0.5) !important; +} + +.leaflet-tile-pane { + image-rendering: pixelated; +} + +.leaflet-control-zoom { + border: none !important; + margin-top: 5px !important; + margin-right: 5px !important; +} +.leaflet-control-zoom a { + color: #aaa; + width: 30px; + height: 30px; + line-height: 29px !important; + border-radius: 3px; + margin-bottom: 4px; + border: 1px solid #000000 !important; + background: var(--color-a) !important; + transition: color 0.2s ease-in 0s; +} +.leaflet-control-zoom a:hover { + color: #fff; +} +#loginoutButton, +#forceSignupButton { + transition: color 0.2s ease-in 0s; +} +#loginoutButton:hover, +#forceSignupButton:hover { + color: #fff; +} + +.leaflet-popup-content-wrapper::-webkit-scrollbar-track { + -webkit-box-shadow: inset 0px 0px 6px rgba(0, 0, 0, 0.3); + background-color: Transparent; +} + +.leaflet-popup-content-wrapper::-webkit-scrollbar { + width: 2px; + height: 2px; + background-color: var(--color-a); +} + +.leaflet-popup-content-wrapper::-webkit-scrollbar-thumb { + border-radius: 0px; + background-color: #26a8ff; +} + +.leaflet-popup-annotation { + left: 0 !important; + top: -10px !important; +} +.leaflet-popup-annotation > .leaflet-popup-content-wrapper { + background: transparent; + box-shadow: none; + max-height: none !important; +} +.leaflet-popup-annotation .leaflet-popup-content { + margin: 0; + width: auto !important; +} +.leaflet-popup-annotation textarea { + background: rgba(0, 0, 0, 0.4); + color: white; + border: none; + padding: 3px; +} +.leaflet-popup-annotation .leaflet-popup-tip-container { + display: none; +} + +/*Leaflet fix that prevents shifting at higher zooms on single images*/ +.leaflet-container img.leaflet-image-layer { + max-width: none !important; +} + +/*Openseadragon navigator border color*/ +.displayregion { + border: 2px solid var(--color-mh) !important; +} +.openseadragon-container .navigator { + border: 1px solid var(--color-a) !important; +} +.osd-custom-buttons > div { + width: 30px; + height: 30px; + background: var(--color-a); + line-height: 31px; + text-align: center; + margin-right: 9px; + cursor: pointer; + border-radius: 3px; + color: #bbb; + display: inline-block; + position: relative; + transition: color 0.2s ease-out; +} +.osd-custom-buttons > div:hover { + color: white; +} + +.cplot_axis path { + fill: none; + stroke: #161616; +} +.cplot_axis line { + fill: none; + stroke: #fff; +} + +#toolcontroller_incdiv > div { + color: #bbb; +} +#toolcontroller_incdiv > div:hover { + color: white; +} + +.ui-menu .ui-menu-item { + color: rgba(255, 255, 255, 0.87); + border: 1px solid transparent; + border-top: 1px solid var(--color-b); + transition: background 0.2s cubic-bezier(0.445, 0.05, 0.55, 0.95), + color 0.2s cubic-bezier(0.445, 0.05, 0.55, 0.95); +} +.ui-menu .ui-menu-item.ui-state-focus { + margin: 0; + background: var(--color-g); + color: white; +} +.ui-menu .ui-menu-item:hover { + background: var(--color-g); + color: white; +} +#SearchClear { + margin-left: 0px; + display: inherit; + position: absolute; + right: 36px; + width: 30px; + height: 30px; + padding-left: 7px; + line-height: 28px; + color: var(--color-f); + cursor: pointer; +} +#SearchClear:hover { + color: white; +} +#SearchBoth { + margin-left: 0px; + display: inherit; + position: absolute; + right: 6px; + width: 30px; + height: 30px; + padding-left: 7px; + line-height: 28px; + color: var(--color-f); + cursor: pointer; +} +#SearchBoth:hover { + color: white; +} + +#scaleBar { + margin: 0; + pointer-events: none; + z-index: 1001; + opacity: 0.75; +} + +.autoWidth { + width: auto !important; +} + +.childpointerevents > * { + pointer-events: auto; +} + +*:focus { + outline: none; +} +/*Clear white highlighting*/ +::selection { + background-color: rgb(0, 158, 255) !important; + color: rgb(255, 255, 255) !important; +} + +.flexbetween { + display: flex; + justify-content: space-between; +} + +.leaflet-control-scalefactor { + position: absolute; + bottom: 25px; + left: 10px; + display: flex; + margin-left: 2px !important; +} +.leaflet-control-scalefactor-line { + background: #161616; + box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.2); + padding: 0px 4px; + color: #dcdcdc; + cursor: default; + opacity: 0.85; + user-select: none; + white-space: nowrap; +} +.leaflet-control-scalefactor-goto { + white-space: nowrap; + padding: 0px 4px; + background: #161616; + box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.2); + padding: 0px 4px 0px 0px; + color: #dcdcdc; + cursor: pointer; + opacity: 0.85; + user-select: none; +} + +.cp-color-picker { + background: var(--color-a); + border-radius: 0px !important; + margin-left: -1px; + margin-top: 1px; +} + +.leaflet-tile-pane svg.leaflet-tile { + pointer-events: none !important; +} +.leaflet-tile-pane svg.leaflet-tile path.leaflet-interactive { + pointer-events: auto; +} + +.leaflet-tile-256 { + width: 256px !important; + height: 256px !important; +} + +.kindsWedgeLayer { + pointer-events: none !important; +} + +.topBarSearch { + height: 100%; + max-width: 380px; + width: 100vw; + background: var(--color-i); + border: 1px solid var(--color-e); + padding: 0px 8px; + font-size: 14px; + color: var(--color-f); + flex: 1; + transition: background 0.2s ease-out, border 0.2s ease-out; +} +.topBarSearch:hover { + background: var(--color-a); +} +.topBarSearch:focus { + background: var(--color-a); + border: 1px solid var(--color-mmgis); +} +.topBarSearch::placeholder { + color: #aaa; +} + +.hoverOpensPanel .hoverOpeningPanel { + opacity: 0; + pointer-events: none; + transition: opacity 0.2s cubic-bezier(0.445, 0.05, 0.55, 0.95), + background 0.2s cubic-bezier(0.445, 0.05, 0.55, 0.95); +} +.hoverOpensPanel .hoverOpeningPanel div:hover { + cursor: pointer; + color: white; + background: #151515; +} +.hoverOpensPanel:hover .hoverOpeningPanel { + opacity: 1; + pointer-events: initial; +} + +.highlightAnim1 { + animation-name: highlightAnim1; + animation-duration: 1.2s; + animation-iteration-count: infinite; + animation-timing-function: ease-in-out; + color: white; +} +@-webkit-keyframes highlightAnim1 { + 0% { + background-color: #008321; + } + 50% { + background-color: #00c932; + } + 100% { + background-color: #008321; + } +} diff --git a/css/mmgisUI.css b/css/mmgisUI.css index a03f2b42..4acf49fa 100644 --- a/css/mmgisUI.css +++ b/css/mmgisUI.css @@ -396,14 +396,15 @@ padding: 0.5em 0.6em 0.5em 0.6em !important; min-height: 0; } -.ui.selection.active.dropdown, .ui.selection.active.dropdown .menu { +.ui.selection.active.dropdown, +.ui.selection.active.dropdown .menu { border-color: var(--color-mmgis) !important; } .ui.dropdown.searchSelect { font-size: 14px; width: 160px; -} +} .ui.dropdown.searchSelect .menu { background: var(--color-a); color: white; @@ -612,10 +613,10 @@ height: 14px; } ::-webkit-scrollbar-thumb { - border: 4px solid rgba(0, 0, 0, 0); + border: 3px solid rgba(0, 0, 0, 0); background-clip: padding-box; border-radius: 7px; - background-color: rgba(69, 90, 100, 0.4); + 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); } diff --git a/docs/docs.js b/docs/docs.js index 86f61bcb..9e587388 100644 --- a/docs/docs.js +++ b/docs/docs.js @@ -1,166 +1,170 @@ -//Form nav -let pages = ["README", "AR_and_VR", "Deep_Linking", "Development", "ENVs"]; -let configure = [ - "Configure", - "Overall_Tab", - "Initial_Tab", - "Layers_Tab", - "Tools_Tab", - "Projection_Tab", - "Look_Tab", - "Panels_Tab", - "Kinds", - "Vector_Styling" -]; -let tools = [ - "Chemistry", - "Draw", - "Identifier", - "Info", - "Layers", - "Legend", - "Measure", - "Sites" -]; -let apis = [ - { name: "Main", path: "/api/docs/main" }, - { name: "Spatial", path: "/api/docs/spatial/" } -]; - -pages.forEach(v => { - let node = document.createElement("li"); - node.setAttribute("id", v); - node.setAttribute("class", "page"); - node.addEventListener( - "click", - (function(page) { - return function() { - let pageElms = document.getElementsByClassName("page"); - for (let i = 0; i < pageElms.length; i++) { - pageElms[i].setAttribute("class", "page"); - } - document.getElementById(page).setAttribute("class", "page active"); - setPage(page); - }; - })(v) - ); - - let text = v.replace(/_/g, " "); - if (text == "README") text = "Home"; - let textnode = document.createTextNode(text); - node.appendChild(textnode); - document.getElementById("nav").appendChild(node); -}); - -configure.forEach(v => { - let node = document.createElement("li"); - node.setAttribute("id", v); - node.setAttribute("class", "page"); - node.addEventListener( - "click", - (function(page) { - return function() { - let pageElms = document.getElementsByClassName("page"); - for (let i = 0; i < pageElms.length; i++) { - pageElms[i].setAttribute("class", "page"); - } - document.getElementById(page).setAttribute("class", "page active"); - setPage(page); - }; - })(v) - ); - - let text = v.replace(/_/g, " "); - if (text == "README") text = "Home"; - let textnode = document.createTextNode(text); - node.appendChild(textnode); - document.getElementById("navconfigure").appendChild(node); -}); - -tools.forEach(v => { - let node = document.createElement("li"); - node.setAttribute("id", v); - node.setAttribute("class", "page"); - node.addEventListener( - "click", - (function(page) { - return function() { - let pageElms = document.getElementsByClassName("page"); - for (let i = 0; i < pageElms.length; i++) { - pageElms[i].setAttribute("class", "page"); - } - document.getElementById(page).setAttribute("class", "page active"); - setPage(page); - }; - })(v) - ); - - let text = v.replace(/_/g, " "); - if (text == "README") text = "Home"; - let textnode = document.createTextNode(text); - node.appendChild(textnode); - document.getElementById("navtools").appendChild(node); -}); - -apis.forEach(v => { - let node = document.createElement("li"); - node.setAttribute("id", v.name); - node.addEventListener( - "click", - (function(api) { - return function() { - window.location.href = api.path; - }; - })(v) - ); - - let text = v.name.replace(/_/g, " "); - let textnode = document.createTextNode(text); - node.appendChild(textnode); - document.getElementById("navapi").appendChild(node); -}); - -function setPage(page) { - let xhr = new XMLHttpRequest(); - xhr.addEventListener("load", function() { - let url = window.location.href.split("?")[0] + "?page=" + page; - window.history.replaceState("", "", url); - - let options = { - highlight: code => hljs.highlightAuto(code).value - }; - document.getElementById("markdown").innerHTML = marked( - this.responseText, - options - ); - }); - let path = "/docs/pages/markdowns/" + page + ".md"; - if (page == "README") path = "/README.md"; - xhr.open("GET", path); - xhr.send(); -} - -function getSingleQueryVariable(variable) { - var query = window.location.search.substring(1); - var vars = query.split("&"); - for (var i = 0; i < vars.length; i++) { - var pair = vars[i].split("="); - if (pair[0] == variable) { - return decodeURIComponent(pair[1]); - } - } - - return false; -} - -document.getElementById("tbtitle").addEventListener("click", function() { - window.location.href = "/"; -}); - -setTimeout(function() { - let page = getSingleQueryVariable("page") || "README"; - - let pageElm = document.getElementById(page); - - if (pageElm) pageElm.dispatchEvent(new CustomEvent("click")); -}, 100); +//Form nav +let pages = ["README", "AR_and_VR", "Deep_Linking", "Development", "ENVs"]; +let configure = [ + "Configure", + "Overall_Tab", + "Initial_Tab", + "Layers_Tab", + "Tools_Tab", + "Projection_Tab", + "Look_Tab", + "Panels_Tab", + "Kinds", + "Vector_Styling", + "Keys", + "Manage_Datasets", + "Manage_Geodatasets", +]; +let tools = [ + "Chemistry", + "Draw", + "Identifier", + "Info", + "Layers", + "Legend", + "Measure", + "Sites", + "Viewshed", +]; +let apis = [ + { name: "Main", path: "/api/docs/main" }, + { name: "Spatial", path: "/api/docs/spatial/" }, +]; + +pages.forEach((v) => { + let node = document.createElement("li"); + node.setAttribute("id", v); + node.setAttribute("class", "page"); + node.addEventListener( + "click", + (function (page) { + return function () { + let pageElms = document.getElementsByClassName("page"); + for (let i = 0; i < pageElms.length; i++) { + pageElms[i].setAttribute("class", "page"); + } + document.getElementById(page).setAttribute("class", "page active"); + setPage(page); + }; + })(v) + ); + + let text = v.replace(/_/g, " "); + if (text == "README") text = "Home"; + let textnode = document.createTextNode(text); + node.appendChild(textnode); + document.getElementById("nav").appendChild(node); +}); + +configure.forEach((v) => { + let node = document.createElement("li"); + node.setAttribute("id", v); + node.setAttribute("class", "page"); + node.addEventListener( + "click", + (function (page) { + return function () { + let pageElms = document.getElementsByClassName("page"); + for (let i = 0; i < pageElms.length; i++) { + pageElms[i].setAttribute("class", "page"); + } + document.getElementById(page).setAttribute("class", "page active"); + setPage(page); + }; + })(v) + ); + + let text = v.replace(/_/g, " "); + if (text == "README") text = "Home"; + let textnode = document.createTextNode(text); + node.appendChild(textnode); + document.getElementById("navconfigure").appendChild(node); +}); + +tools.forEach((v) => { + let node = document.createElement("li"); + node.setAttribute("id", v); + node.setAttribute("class", "page"); + node.addEventListener( + "click", + (function (page) { + return function () { + let pageElms = document.getElementsByClassName("page"); + for (let i = 0; i < pageElms.length; i++) { + pageElms[i].setAttribute("class", "page"); + } + document.getElementById(page).setAttribute("class", "page active"); + setPage(page); + }; + })(v) + ); + + let text = v.replace(/_/g, " "); + if (text == "README") text = "Home"; + let textnode = document.createTextNode(text); + node.appendChild(textnode); + document.getElementById("navtools").appendChild(node); +}); + +apis.forEach((v) => { + let node = document.createElement("li"); + node.setAttribute("id", v.name); + node.addEventListener( + "click", + (function (api) { + return function () { + window.location.href = api.path; + }; + })(v) + ); + + let text = v.name.replace(/_/g, " "); + let textnode = document.createTextNode(text); + node.appendChild(textnode); + document.getElementById("navapi").appendChild(node); +}); + +function setPage(page) { + let xhr = new XMLHttpRequest(); + xhr.addEventListener("load", function () { + let url = window.location.href.split("?")[0] + "?page=" + page; + window.history.replaceState("", "", url); + + let options = { + highlight: (code) => hljs.highlightAuto(code).value, + }; + document.getElementById("markdown").innerHTML = marked( + this.responseText, + options + ); + }); + let path = "/docs/pages/markdowns/" + page + ".md"; + if (page == "README") path = "/README.md"; + xhr.open("GET", path); + xhr.send(); +} + +function getSingleQueryVariable(variable) { + var query = window.location.search.substring(1); + var vars = query.split("&"); + for (var i = 0; i < vars.length; i++) { + var pair = vars[i].split("="); + if (pair[0] == variable) { + return decodeURIComponent(pair[1]); + } + } + + return false; +} + +document.getElementById("tbtitle").addEventListener("click", function () { + window.location.href = "/"; +}); + +setTimeout(function () { + let page = getSingleQueryVariable("page") || "README"; + + let pageElm = document.getElementById(page); + + if (pageElm) pageElm.dispatchEvent(new CustomEvent("click")); +}, 100); diff --git a/docs/images/atlas_wget_script.bat b/docs/images/atlas_wget_script.bat new file mode 100644 index 00000000..e69de29b diff --git a/docs/images/draw_panel1.jpg b/docs/images/draw_panel1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..388ff7ee6e68e1a92a2cdc69659cf1f0ea65ed30 GIT binary patch literal 53681 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

D%>%#ily5-7z9ucI0&WRwmHbIK!E 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 K@-y7u=l(Ac": { - "color": "#FFFFFF", - "fill": true, - "fillColor": "rgb(0, 125, 200)", - "fillOpacity": 0.5, - "opacity": 1, - "radius": 4, - "weight": 2 - } -} -``` - -# Data - -An experimental layer type. Uses WebGL shaders to generate tiles on the fly. Only a flood shader with hardcoded values is supported at this moment. The flood height can be adjusted in real time under the layer's setting in MMGIS. - -#### Layer Name - -_type:_ string -The unique display name and identifier of the layer. It must be unique and contain no special characters. - -#### URL - -_type:_ string -A relative to the mission directoty or absolute file path to a Digital Elevation Map tileset generated by `auxillary/1bto4b/rasterstotiles1bto4b.py` - -#### Legend - -_type:_ string -An absolute or relative file path pointing to a `legend.csv` that describes the symbology of the layer. Please see the Legend Tool to see how to form a `legend.csv`. - -#### Visibility - -_type:_ bool -Whether the layer is on initially. - -#### Minimum Zoom - -_type:_ integer -The lowest (smallest number) zoom level of the tile set. -_Note: This field can be automatically populate with "Populate from XML". "Populate from XML" uses looks for a `tilemapresource.xml` in the tileset directory specified by the URL field._ - -#### Maximum Native Zoom - -_type:_ integer -The highest (largest number) zoom level of the tile set. -_Note: This field can be automatically populate with "Populate from XML". "Populate from XML" uses looks for a `tilemapresource.xml` in the tileset directory specified by the URL field._ - -#### Maximum Zoom - -_type:_ integer -The highest (largest number) zoom level to see in MMGIS. This value is at least as high as Maximum Native Zoom. This allows zooms level higher than that of the tileset. Instead of rendering new tile image, it scales them in instead. - -#### Bounding Box - -_type:_ string _optional_ -A comma separated string defining the tileset's `minimumLonDeg,minimumLatDeg,maximumLonDeg,maximumLatDeg`. Setting a bounding box improves performance by limiting requests for tiles to only those that fit the bounds. -_Note: This field can be automatically populate with "Populate from XML". "Populate from XML" uses looks for a `tilemapresource.xml` in the tileset directory specified by the URL field._ - -# Vector - -A [geojson](https://geojson.org/) layer. - -#### Layer Name - -_type:_ string -The unique display name and identifier of the layer. It must be unique and contain no special characters. - -#### Kind of Layer - -_type:_ enum -A special kind of interaction for the layer. Please see the Kinds page for more. - -#### URL - -_type:_ string -A file path that points to a geojson. If the path is relative, it will be relative to the mission's directory. The URL must contain a proper placeholder ending such as: `{z}/{x}/{y}.png`. Alternatively vectors can be served with Geodatasets. Simply go to "Manage Geodatasets" at the bottom left, upload a geojson and link to it in this URL field with "geodatasets:{geodataset\*name}" - -#### Legend - -_type:_ string -An absolute or relative file path pointing to a `legend.csv` that describes the symbology of the layer. Please see the Legend Tool to see how to form a `legend.csv`. - -#### Visibility - -_type:_ bool -Whether the layer is on initially. - -#### Visibility Cutoff - -_type:_ integer _optional_ -If set, this vector layer will be hidden if the current zoom level is less than or equal to that of the visibility cutoff. `Visibility Cutoff * -1` will invert its visibility condition. This is useful when the dataset is dense, local to a one region, or irrelevant when far away and the desired range of zoom levels is large. - -#### Initial Opacity - -_type:_ float -A value from 0 to 1 of the layer's initial opacity. 1 is fully opaque. - -#### Stroke Color - -_type:_ CSS color string or a prop _optional_ -The border color of each feature. If the feature is a line, this field is the color of the line. See the Vector Styling page for more. Colors can be as follows: - -- A named color - - crimson, blue, rebeccapurple -- A hex color - - #FFF - - #A58101 -- An rgb color - - rgb(255,89,45) -- An hsl color - - hsl(130, 26%, 34%) -- Based on a feature's color property - - `prop:geojson_property_key` will set the feature's color to the values of `features[i].properties.geojson_property_key` - -#### Fill Color - -_type:_ CSS color string or a prop _optional_ -The fill color of each feature. See Stroke Color for color options. See the Vector Styling page for more. - -#### Stroke Weight - -_type:_ positive integer _optional_ -The thickness of the stroke/border in pixels. See the Vector Styling page for more. - -#### Fill Opacity - -_type:_ float _optional_ -A value from 0 to 1 of Fill Color's opacity. 1 is fully opaque. See the Vector Styling page for more. -_Note: It's also possible to set the opacities of colors directly with #CCDDEEFF, rgba() and hsla()._ - -#### Radius - -_type:_ positive integer _optional_ -When a point feature is encountered, this value will be it's radius in pixels. - -#### Raw Variables - -Clicking "Set Default Variables" will add a template of all possible raw variables (without overwriting ones that are already set). All raw variables are optional. - -Example: - -```javascript -{ - "useKeyAsName": "name", - "datasetLinks": [ - { - "prop": "{prop}", - "dataset": "{dataset}", - "column": "{column}", - "type": "{none || images}" - } - ], - "links": [ - { - "name": "example", - "link": "url/?param={prop}" - } - ], - "info": [ - { - "which": "last", - "icon": "material design icon", - "value": "Prop: {prop}" - } - ], - "search": "(prop1) round(prop2.1) rmunder(prop_3)" -} -``` - -- `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. -- `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. - - `column`: This is a column/csv header name within the dataset. If the value of the prop key matches the value in this column, the entire row will be return. All rows that match are returned. - - `type`: Unused. -- `links`: Configure deep links to other sites based on the properties on a selected feature. This requires the "Minimalist" option in the Look Tab to be unchecked. Upon clicking a feature, a list of deep links are put into the top bar and can be clicked on to navigate to any other page. - - `name`: The name of the deep link. It should be unique. - - `link`: A url template. Curly brackets are included. On feature click, all `{prop}` are replaced with the corresponding `features[i].properties.prop` value. Multiple `{prop}` are supported as are access to nested props using dot notation `{stores.food.candy}`. -- `info`: Creates an informational record at the top of the page. The first use case was showing the value of the latest sol. Clicking this record pans to the feature specified by `which`. This requires the "Minimalist" option in the Look Tab to be unchecked. This is used on startup and not when a user selects a feature in this layer. - - `which`: This only supports the value `last` at this point. - - `icon`: Any [Material Design Icon](http://materialdesignicons.com/) name - - `value`: A name to display. All `{prop}`s will be replaced by their corresponding `features[which].properties[prop]` value. -- `search`: This requires the "Minimalist" option in the Look Tab to be unchecked. When set, this layer will become searchable through the search bar at the top. The search will look for and autocomplete on the properties specified. All properties are enclosed by parentheses and space-separated. `round` can be used like a function to round the property beforehand. `rmunder` works similarly but removes all underscores instead. - -# Model - -#### Layer Name - -_type:_ string -The unique display name and identifier of the layer. It must be unique and contain no special characters. - -#### URL - -_type:_ string -A file path that points to a .dae or .obj. If the path is relative, it will be relative to the mission's directory. - -#### Longitude - -_type:_ float -The longitude in decimal degrees at which to place the model. - -#### Latitude - -_type:_ float -The latitude in decimal degrees at which to place the model. - -#### Elevation - -_type:_ float -The elevation in meters at which to place the model. - -#### Rotation X - -_type:_ float _optional_ -An x-axis rotation in radians to orient the model. - -#### Rotation Y - -_type:_ float _optional_ -A y-axis rotation in radians to orient the model. - -#### Rotation Z - -_type:_ float _optional_ -A z-axis rotation in radians to orient the model. - -#### Scale - -_type:_ float _optional_ -A scaling factor to resize the model. - -#### Visibility - -_type:_ bool -Whether the layer is on initially. - -#### Initial Opacity - -_type:_ float -A value from 0 to 1 of the layer's initial opacity. 1 is fully opaque. - ---- - -_Note:_ Additional vector layer stylings can be found on the [Meaningful GeoJSON Styles](Meaningful-GeoJSON-Styles) page. +# Layers Tab + +Add, alter and remove Layers: + +"Saving Changes" will catch most structural mistakes made here and will inform you of what's wrong. It will not verify anything further such as whether a layer's data exists or whether the raw variables entered are valid. + +##### Contents + +- [Adding Layers](#adding-layers) +- [Cloning Layers](#cloning-layers) +- [Removing Layers](#removing-layers) +- [Layer Structure](#layer-structure) +- [Configuring Individual Layers](#configuring-individual-layers) +- [Layer Types](#layer-types) + - [Header](#header) + - [Tile](#tile) + - [Vector Tile](#vector-tile) + - [Data](#data) + - [Vector](#vector) + - [Model](#model) + +## Adding Layers + +- Scroll to the bottom of the page and click "Add Layer +" + - This will add a blank `header` layer named "Name Layer" to the bottom of the layers list. + +## Cloning Layers + +- Clicking a layer brings up its form. At the top right of this form there is a button to clone the layer. Clicking it clones the layer and adds it to the bottom of the layers list. Because layer names must be unique, the layer name of the newly cloned layer must be changed before saving changes can be successful. + +## Removing Layers + +- Clicking a layer brings up its form. A the bottom left of this form there is a button to delete the layer. There is no additional warning prompt. + +## Layer Structure + +- The layer at the top of the layers list renders on top of all other layers. The layer second from the top of the list of layers renders on top of all other layers except the first one. And so on. +- Layers can be reordered by clicking and dragging them vertically to a new position. If you're dealing with a long list of layers, use the mouse-wheel while holding a layer to move it more quickly. +- Layers can also be clicked and dragged horizontally. If a layer is indented, it will be grouped under the header above it. Headers can be indented/nested within other headers too. + +## Configuring Individual Layers + +- Clicking a layer brings of a form to change its parameters. +- Layers can be broken up by 6 types and can be set with the "Layer Type" field. + +--- + +## Layer Types + +# Header + +Header layers contain no content but allows other layers to be grouped underneath it much like a directory. + +#### Layer Name + +_type:_ string +The unique display name and identifier of the layer. It must be unique and contain no special characters. + +#### Visibility + +_type:_ bool +Whether the contents of this header are possibly visible initially. + +# Tile + +Tile layers are hierarchical raster imagery. + +#### Layer Name + +_type:_ string +The unique display name and identifier of the layer. It must be unique and contain no special characters. + +#### URL + +_type:_ string +A file path that points to a tileset's directory. If the path is relative, it will be relative to the mission's directory. The URL must contain a proper placeholder ending such as: `{z}/{x}/{y}.png` + +#### DEM Tile URL + +_type:_ string _optional_ +A file path like URL but pointing to a Digital Elevation Map tileset generated by `auxillary/1bto4b/rasterstotiles1bto4b.py` This is responsible for 3D data in the globe. It would be ideal if this tileset can match the extents of its corresponding raster and has either no nodata or has nodata far lower than that of its lowest point. + +#### Legend + +_type:_ string +An absolute or relative file path pointing to a `legend.csv` that describes the symbology of the layer. Please see the Legend Tool to see how to form a `legend.csv`. + +#### TMS + +_type:_ bool +The format of the tiles. If TMS is false, it assumes WMS tiles. The main difference between TMS and WMS is that their Y-axes are inverted. + +#### Visibility + +_type:_ bool +Whether the layer is on initially. + +#### Initial Opacity + +_type:_ float +A value from 0 to 1 of the layer's initial opacity. 1 is fully opaque. + +#### Minimum Zoom + +_type:_ integer +The lowest (smallest number) zoom level of the tile set. +_Note: This field can be automatically populate with "Populate from XML". "Populate from XML" uses looks for a `tilemapresource.xml` in the tileset directory specified by the URL field._ + +#### Maximum Native Zoom + +_type:_ integer +The highest (largest number) zoom level of the tile set. +_Note: This field can be automatically populate with "Populate from XML". "Populate from XML" uses looks for a `tilemapresource.xml` in the tileset directory specified by the URL field._ + +#### Maximum Zoom + +_type:_ integer +The highest (largest number) zoom level to see in MMGIS. This value is at least as high as Maximum Native Zoom. This allows zooms level higher than that of the tileset. Instead of rendering new tile image, it scales them in instead. + +#### Bounding Box + +_type:_ string _optional_ +A comma separated string defining the tileset's `minimumLonDeg,minimumLatDeg,maximumLonDeg,maximumLatDeg`. Setting a bounding box improves performance by limiting requests for tiles to only those that fit the bounds. +_Note: This field can be automatically populate with "Populate from XML". "Populate from XML" uses looks for a `tilemapresource.xml` in the tileset directory specified by the URL field._ + +# Vector Tile + +A mix between Tile and Vector. Useful when rendering tons of features since features are rendered based on viewport instead of all at once at the start. + +#### Layer Name + +_type:_ string +The unique display name and identifier of the layer. It must be unique and contain no special characters. + +#### Kind of Layer + +_type:_ enum +A special kind of interaction for the layer. Please see the Kinds page for more. + +#### URL + +_type:_ string +A file path that points to a vector tileset's directory. If the path is relative, it will be relative to the mission's directory. The URL must contain a proper placeholder ending such as: `{z}/{x}/{y}.png`. Alternatively vector tiles can be generated and served in real time with Geodatasets. Simply go to "Manage Geodatasets" at the bottom left, upload a geojson and link to it in this URL field with "geodatasets:{geodataset*name}" +\_Note: If the vector tile layer contains points, the layer will break. A known workaround is to buffer all points.* + +#### DEM Tile URL + +_type:_ string _optional_ +A file path like URL but pointing to a Digital Elevation Map tileset generated by `auxillary/1bto4b/rasterstotiles1bto4b.py` This is responsible for 3D data in the globe. It would be ideal if this tileset can match the extents of its corresponding raster and has either no nodata or has nodata far lower than that of its lowest point. + +#### Legend + +_type:_ string +An absolute or relative file path pointing to a `legend.csv` that describes the symbology of the layer. Please see the Legend Tool to see how to form a `legend.csv`. + +#### TMS + +_type:_ bool +The format of the tiles. If TMS is false, it assumes WMS tiles. The main difference between TMS and WMS is that their Y-axes are inverted. + +#### Visibility + +_type:_ bool +Whether the layer is on initially. + +#### Initial Opacity + +_type:_ float +A value from 0 to 1 of the layer's initial opacity. 1 is fully opaque. + +#### Minimum Zoom + +_type:_ integer +The lowest (smallest number) zoom level of the tile set. +_Note: This field can be automatically populate with "Populate from XML". "Populate from XML" uses looks for a `tilemapresource.xml` in the tileset directory specified by the URL field._ + +#### Maximum Native Zoom + +_type:_ integer +The highest (largest number) zoom level of the tile set. +_Note: This field can be automatically populate with "Populate from XML". "Populate from XML" uses looks for a `tilemapresource.xml` in the tileset directory specified by the URL field._ + +#### Maximum Zoom + +_type:_ integer +The highest (largest number) zoom level to see in MMGIS. This value is at least as high as Maximum Native Zoom. This allows zooms level higher than that of the tileset. Instead of rendering new tile image, it scales them in instead. + +#### Vector Tile Feature Unique Id Key + +_type:_ string +Each feature of the vector tileset needs a property with a unique value to identify it. This required field is the name of that property. + +#### Use Key as Name + +_type:_ string _optional_ +The property key whose value should be the hover text of each feature. This is the same as the `useKeyAsName` raw variable within Vector layers + +#### Raw Variables + +Vector Tiles are styled differently than Vectors. Raw variables here takes an object that maps internal vector tile layer names to styles. All raw variables are optional. + +Example: + +```javascript +{ + "": { + "color": "#FFFFFF", + "fill": true, + "fillColor": "rgb(0, 125, 200)", + "fillOpacity": 0.5, + "opacity": 1, + "radius": 4, + "weight": 2 + } +} +``` + +# Data + +An experimental layer type. Uses WebGL shaders to generate tiles on the fly. Only a flood shader with hardcoded values is supported at this moment. The flood height can be adjusted in real time under the layer's setting in MMGIS. + +#### Layer Name + +_type:_ string +The unique display name and identifier of the layer. It must be unique and contain no special characters. + +#### URL + +_type:_ string +A relative to the mission directoty or absolute file path to a Digital Elevation Map tileset generated by `auxillary/1bto4b/rasterstotiles1bto4b.py` + +#### Legend + +_type:_ string +An absolute or relative file path pointing to a `legend.csv` that describes the symbology of the layer. Please see the Legend Tool to see how to form a `legend.csv`. + +#### Visibility + +_type:_ bool +Whether the layer is on initially. + +#### Minimum Zoom + +_type:_ integer +The lowest (smallest number) zoom level of the tile set. +_Note: This field can be automatically populate with "Populate from XML". "Populate from XML" uses looks for a `tilemapresource.xml` in the tileset directory specified by the URL field._ + +#### Maximum Native Zoom + +_type:_ integer +The highest (largest number) zoom level of the tile set. +_Note: This field can be automatically populate with "Populate from XML". "Populate from XML" uses looks for a `tilemapresource.xml` in the tileset directory specified by the URL field._ + +#### Maximum Zoom + +_type:_ integer +The highest (largest number) zoom level to see in MMGIS. This value is at least as high as Maximum Native Zoom. This allows zooms level higher than that of the tileset. Instead of rendering new tile image, it scales them in instead. + +#### Bounding Box + +_type:_ string _optional_ +A comma separated string defining the tileset's `minimumLonDeg,minimumLatDeg,maximumLonDeg,maximumLatDeg`. Setting a bounding box improves performance by limiting requests for tiles to only those that fit the bounds. +_Note: This field can be automatically populate with "Populate from XML". "Populate from XML" uses looks for a `tilemapresource.xml` in the tileset directory specified by the URL field._ + +# Vector + +A [geojson](https://geojson.org/) layer. + +#### Layer Name + +_type:_ string +The unique display name and identifier of the layer. It must be unique and contain no special characters. + +#### Kind of Layer + +_type:_ enum +A special kind of interaction for the layer. Please see the Kinds page for more. + +#### URL + +_type:_ string +A file path that points to a geojson. If the path is relative, it will be relative to the mission's directory. The URL must contain a proper placeholder ending such as: `{z}/{x}/{y}.png`. Alternatively vectors can be served with Geodatasets. Simply go to "Manage Geodatasets" at the bottom left, upload a geojson and link to it in this URL field with "geodatasets:{geodataset\*name}" + +#### Legend + +_type:_ string +An absolute or relative file path pointing to a `legend.csv` that describes the symbology of the layer. Please see the Legend Tool to see how to form a `legend.csv`. + +#### Visibility + +_type:_ bool +Whether the layer is on initially. + +#### Visibility Cutoff + +_type:_ integer _optional_ +If set, this vector layer will be hidden if the current zoom level is less than or equal to that of the visibility cutoff. `Visibility Cutoff * -1` will invert its visibility condition. This is useful when the dataset is dense, local to a one region, or irrelevant when far away and the desired range of zoom levels is large. + +#### Initial Opacity + +_type:_ float +A value from 0 to 1 of the layer's initial opacity. 1 is fully opaque. + +#### Stroke Color + +_type:_ CSS color string or a prop _optional_ +The border color of each feature. If the feature is a line, this field is the color of the line. See the Vector Styling page for more. Colors can be as follows: + +- A named color + - crimson, blue, rebeccapurple +- A hex color + - #FFF + - #A58101 +- An rgb color + - rgb(255,89,45) +- An hsl color + - hsl(130, 26%, 34%) +- Based on a feature's color property + - `prop:geojson_property_key` will set the feature's color to the values of `features[i].properties.geojson_property_key` + - If that property is not a valid CSS color and is a string, it will use a random and consistent color based on its hash. + +#### Fill Color + +_type:_ CSS color string or a prop _optional_ +The fill color of each feature. See Stroke Color for color options. See the Vector Styling page for more. + +#### Stroke Weight + +_type:_ positive integer _optional_ +The thickness of the stroke/border in pixels. See the Vector Styling page for more. + +#### Fill Opacity + +_type:_ float _optional_ +A value from 0 to 1 of Fill Color's opacity. 1 is fully opaque. See the Vector Styling page for more. +_Note: It's also possible to set the opacities of colors directly with #CCDDEEFF, rgba() and hsla()._ + +#### Radius + +_type:_ positive integer _optional_ +When a point feature is encountered, this value will be it's radius in pixels. + +#### Raw Variables + +Clicking "Set Default Variables" will add a template of all possible raw variables (without overwriting ones that are already set). All raw variables are optional. + +Example: + +```javascript +{ + "useKeyAsName": "name", + "datasetLinks": [ + { + "prop": "{prop}", + "dataset": "{dataset}", + "column": "{column}", + "type": "{none || images}" + } + ], + "links": [ + { + "name": "example", + "link": "url/?param={prop}" + } + ], + "info": [ + { + "which": "last", + "icon": "material design icon", + "value": "Prop: {prop}" + } + ], + "search": "(prop1) round(prop2.1) rmunder(prop_3)" +} +``` + +- `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. +- `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. + - `column`: This is a column/csv header name within the dataset. If the value of the prop key matches the value in this column, the entire row will be return. All rows that match are returned. + - `type`: Unused. +- `links`: Configure deep links to other sites based on the properties on a selected feature. This requires the "Minimalist" option in the Look Tab to be unchecked. Upon clicking a feature, a list of deep links are put into the top bar and can be clicked on to navigate to any other page. + - `name`: The name of the deep link. It should be unique. + - `link`: A url template. Curly brackets are included. On feature click, all `{prop}` are replaced with the corresponding `features[i].properties.prop` value. Multiple `{prop}` are supported as are access to nested props using dot notation `{stores.food.candy}`. +- `info`: Creates an informational record at the top of the page. The first use case was showing the value of the latest sol. Clicking this record pans to the feature specified by `which`. This requires the "Minimalist" option in the Look Tab to be unchecked. This is used on startup and not when a user selects a feature in this layer. + - `which`: This only supports the value `last` at this point. + - `icon`: Any [Material Design Icon](http://materialdesignicons.com/) name + - `value`: A name to display. All `{prop}`s will be replaced by their corresponding `features[which].properties[prop]` value. +- `search`: This requires the "Minimalist" option in the Look Tab to be unchecked. When set, this layer will become searchable through the search bar at the top. The search will look for and autocomplete on the properties specified. All properties are enclosed by parentheses and space-separated. `round` can be used like a function to round the property beforehand. `rmunder` works similarly but removes all underscores instead. + +# Model + +#### Layer Name + +_type:_ string +The unique display name and identifier of the layer. It must be unique and contain no special characters. + +#### URL + +_type:_ string +A file path that points to a .dae or .obj. If the path is relative, it will be relative to the mission's directory. + +#### Longitude + +_type:_ float +The longitude in decimal degrees at which to place the model. + +#### Latitude + +_type:_ float +The latitude in decimal degrees at which to place the model. + +#### Elevation + +_type:_ float +The elevation in meters at which to place the model. + +#### Rotation X + +_type:_ float _optional_ +An x-axis rotation in radians to orient the model. + +#### Rotation Y + +_type:_ float _optional_ +A y-axis rotation in radians to orient the model. + +#### Rotation Z + +_type:_ float _optional_ +A z-axis rotation in radians to orient the model. + +#### Scale + +_type:_ float _optional_ +A scaling factor to resize the model. + +#### Visibility + +_type:_ bool +Whether the layer is on initially. + +#### Initial Opacity + +_type:_ float +A value from 0 to 1 of the layer's initial opacity. 1 is fully opaque. + +--- + +_Note:_ Additional vector layer stylings can be found on the [Meaningful GeoJSON Styles](Meaningful-GeoJSON-Styles) page. diff --git a/docs/pages/markdowns/Viewshed.md b/docs/pages/markdowns/Viewshed.md new file mode 100644 index 00000000..2e51fd72 --- /dev/null +++ b/docs/pages/markdowns/Viewshed.md @@ -0,0 +1,80 @@ +# Viewshed + +The Viewshed tool renders dynamic tilesets based on line-of-sight visibilities from user defined source points. Users can change viewshed colors, opacities, source heights, fields of views and visualize multiple viewsheds simultaneously. + +## Tool Configuration + +### Example + +```javascript +{ + "data": [ + { + "name": "Unique Name 1", + "demtileurl": "Layers/Example/demtileset/{z}/{x}/{y}.png", + "minZoom": 8, + "maxNativeZoom": 18 + }, + { "...": "..." } + ], + "curvature": false, + "cameraPresets": [ + { + "name": "CAM A", + "height": 2, + "azCenter": 0, + "azFOV": 70, + "elCenter": -10, + "elFOV": 30 + }, + { "...": "..." } + ] +} +``` + +_**data**_ - At minimum, the Viewshed tool requires at least one "data" source. A data source describes a DEM tileset (see /auxiliary/1bto4b) and allows users to select it by name to generate viewsheds over. + +_**curvature**_ - Optionally setting this to false disables the account of drop-off based on planetary curvature when calculating viewsheds. If unset or set to true, the configured Major Radius will be used during generations. + +_**cameraPresets**_ - Are optional but, if set, require only "name" to be defined. Setting these allows users to quickly navigate to preset camera parameters. "height", "azCenter", "azFOV", "elCenter" and "elFOV" are all optional and are all of type number. + +## Tool Use + +### Options + +Users can modify the following parameters per viewshed: + +_**Data**_ - Changes the dataset to perform the viewshed on. + +_**Color**_ - Changes the color of the viewshed. + +_**Opacity**_ - Changes the opacity of the viewshed. + +_**Resolution**_ - Because viewshedding requires a lot of data which can slow things down, four resolutions of data are provided. Ultra, the highest resolution, matches the resolution of the current raster tiles based on zoom. For example if the raster basemap is at 200 meters/pixel, an ultra viewshed would be generated with 200 meters/pixel (or highest available) digital elevation map (DEM) data. The resolution of the DEM impacts the accuracy of the generated viewshed. High is half the resolution of ultra, Medium, the default, is half the resolution of high, and low is half the resolution of medium. Due to performance issues both ultra and high resolutions require manual regeneration upon changes to viewshed parameters or regions. + +_**Reverse**_ - Normally viewsheds color the visible regions. Reversing the viewshed colors everything except the visible regions. + +_**Camera Preset**_ - Using values configured for the Viewshed Tool, sets other parameters to mock a camera. + +_**Height**_ - The distance, in meters, that the viewshed source point sits above the surface. + +_**FOV (Az)**_ - The azimuthal (horizontal) field of view. + +_**FOV (El)**_ - The elevational (vertical) field of view. + +_**Center Azimuth**_ - The azimuthal look-at angle. 0deg (North) -> 360deg (North) increasing clockwise. The generated viewshed will encompass the range such that half its FOV falls on either half of its angle. + +_**Center Elevation**_ - The elevational look-at angle. -90deg(Down) -> 90deg (Up). The generated viewshed will encompass the range such that half its FOV falls on both top and bottom halves of its angle. + +_**Latitude**_ - The latitude of the viewshed's source point. + +_**Longitude**_ - The longitude of the viewshed's source point. + +## Technical + +- The accuracy of the Viewshed tool has been independently verified within ArcGIS. +- Viewsheds are generated entirely in-browser in JavaScript using tiled data. + - The algorithm is derived from ["Generating Viewsheds without Using Sightlines. Wang, Jianjun, Robinson, Gary J., and White, Kevin. Photogrammetric Engineering and Remote Sensing. p81"](https://www.asprs.org/wp-content/uploads/pers/2000journal/january/2000_jan_87-90.pdf) with implementation guidance from [gdal_viewshed](https://github.com/OSGeo/gdal/blob/master/gdal/alg/viewshed.cpp). + - The algorithm is somewhat special to our DEM tile format in how it deal seam boundaries and stitches tiles together. + - An additional data manager was written to query for all tiles necessary to render the viewshed for the current screen. + - Azimuth and elevation fields-of-views and look-ats are new. diff --git a/package.json b/package.json index 1b0adb7b..8033f7a4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mmgis", - "version": "1.3.0", + "version": "1.3.2", "description": "A web-based mapping and localization solution for science operation on planetary missions.", "main": "server.js", "scripts": { diff --git a/prepare/base/Missions/Test/Layers/Waypoints/waypoints.json b/prepare/base/Missions/Test/Layers/Waypoints/waypoints.json index cb539abe..98dbf879 100644 --- a/prepare/base/Missions/Test/Layers/Waypoints/waypoints.json +++ b/prepare/base/Missions/Test/Layers/Waypoints/waypoints.json @@ -1,6 +1,73 @@ { -"type": "FeatureCollection", -"features": [ -{ "type": "Feature", "properties": { "testObj": "Data/models/Askival_v2/Askival_v2.obj", "testTexture": "Data/models/Askival_v2/Askival_v2.jpg", "sol": 1051.000000, "site": 48.000000, "pos": 2470.000000, "Ls": 16.300000, "sampleurl1": "http://www.jpl.nasa.gov/assets/images/logo_nasa_trio_black@2x.png", "sampleurl2": "http://mars.jpl.nasa.gov/msl-raw-images/msss/00582/mrdi/0582MD0002120000101703E01_DXXX.jpg", "SCLK_START": 490798305.117000, "SCLK_END": 490799311.058000, "lat_dd": -4.667984, "lon_dd": 137.370240, "easting_m": 8142579.427000, "northing_m": -276693.318000, "elev_m": -4446.783203, "pitch_rad": 0.018910, "roll_rad": -0.112010, "tilt_rad": 0.113590, "yaw_rad": 1.487740, "pitch_deg": 1.083463, "roll_deg": -6.417700, "tilt_deg": 6.508228, "yaw_deg": 85.241223, "mobtrav_id": 6.018533, "mobtrav_to": 10915.484142, "drive_type": "final", "site_pos": "48_2470", "sol_site_p": "1051_48_2470" }, "geometry": { "type": "Point", "coordinates": [ 137.37024003873523, -4.667983634858819, -4446.7832031300004 ] } } -] + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "sol": 1051.0, + "images": [ + { + "name": "Panorama Example", + "url": "Data/Mosaics/N_L000_1051_ILT048CYL_S_2470_UNCORM1.jpg", + "isPanoramic": true, + "rows": 1800, + "columns": 7696, + "azmin": 0.0, + "azmax": 360.0, + "elmin": -66.5118, + "elmax": 17.7171, + "elzero": 379.759 + }, + { + "name": "Model Example", + "url": "Data/models/Askival_v2/Askival_v2.obj", + "texture": "Data/models/Askival_v2/Askival_v2.jpg", + "type": "image", + "isModel": true + }, + { + "name": "Image Example 1", + "url": "http://www.jpl.nasa.gov/assets/images/logo_nasa_trio_black@2x.png", + "type": "image" + }, + { + "name": "Image Example 2", + "url": "http://mars.jpl.nasa.gov/msl-raw-images/msss/00582/mrdi/0582MD0002120000101703E01_DXXX.jpg", + "type": "image" + } + ], + "site": 48.0, + "pos": 2470.0, + "Ls": 16.3, + "SCLK_START": 490798305.117, + "SCLK_END": 490799311.058, + "lat_dd": -4.667984, + "lon_dd": 137.37024, + "easting_m": 8142579.427, + "northing_m": -276693.318, + "elev_m": -4446.783203, + "pitch_rad": 0.01891, + "roll_rad": -0.11201, + "tilt_rad": 0.11359, + "yaw_rad": 1.48774, + "pitch_deg": 1.083463, + "roll_deg": -6.4177, + "tilt_deg": 6.508228, + "yaw_deg": 85.241223, + "mobtrav_id": 6.018533, + "mobtrav_to": 10915.484142, + "drive_type": "final", + "site_pos": "48_2470", + "sol_site_p": "1051_48_2470" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 137.37024003873523, + -4.667983634858819, + -4446.7832031300004 + ] + } + } + ] } diff --git a/scripts/configure.js b/scripts/configure.js index e047bb69..32c67b90 100644 --- a/scripts/configure.js +++ b/scripts/configure.js @@ -1,224 +1,228 @@ -// Configure Require.js -var require = { - //relative to the index calling it - baseUrl: 'scripts', - //Some browsers suspend script loading on inactive tabs so disable script load timeouts - waitSeconds: 0, - paths: { - //core - loading: 'pre/loading/loading', - landingPage: 'essence/LandingPage/LandingPage', - essence: 'essence/essence', - - //externals - attributions: 'external/attributions', - - colorPicker: 'external/ColorPicker/jqColorPicker.min', - - d3: 'external/D3/d3.v4.min', - d33: 'external/D3/d3.v3.min', //!!!chemistryplot.js still uses - - jquery: 'external/JQuery/jquery.min', - jqueryUI: 'external/JQuery/jquery-ui', - mark: 'external/JQuery/jquery.mark.min', - - leaflet: 'external/Leaflet/leaflet1.5.1', - leafletDraw: 'external/Leaflet/leaflet.draw', - leafletGeometryUtil: 'external/Leaflet/leaflet.geometryutil', - leafletSnap: 'external/Leaflet/leaflet.snap', - leafletCorridor: 'external/Leaflet/leaflet-corridor', - leafletPip: 'external/Leaflet/leaflet-pip', - leafletImageTransform: 'external/Leaflet/leaflet-imagetransform', - proj4: 'external/Leaflet/proj4-compressed', - proj4leaflet: 'external/Leaflet/proj4leaflet', - leafletEditable: 'external/Leaflet/leaflet-editable', - leafletHotline: 'external/Leaflet/leaflet.hotline.min', - leafletPolylineDecorator: 'external/Leaflet/leaflet.polylineDecorator', - leafletScaleFactor: 'external/Leaflet/leaflet.scalefactor.min', - leafletColorFilter: 'external/Leaflet/leaflet-tilelayer-colorfilter', - leafletTileLayerGL: 'external/Leaflet/leaflet.tilelayer.gl', - leafletVectorGrid: 'external/Leaflet/leaflet.vectorGrid.bundled', - - metricsGraphics: 'external/MetricsGraphics/metricsgraphics.min', - openSeadragon: 'external/OpenSeadragon/openseadragon.min', - fabricA: 'external/OpenSeadragon/fabric.adapted', - fabricOverlay: 'external/OpenSeadragon/openseadragon-fabricjs-overlay', - dataTables: 'external/DataTables/datatables.min', - nipple: 'external/NippleJS/nipplejs.min', - multiRange: 'external/MultiRange/multirange', - jsonViewer: 'external/JSONViewer/jquery.json-viewer', - fileSaver: 'external/FileSaver/FileSaver.min', - - Hammer: 'external/Hammer/hammer.min', - - HTML2Canvas: 'external/HTML2Canvas/html2canvas.min', - - png: 'external/PNG/png', - zlib: 'external/PNG/zlib', - - semantic: 'external/SemanticUI/semantic.min', - - shp: 'external/shpjs/shapefile', - shpwrite: 'external/SHPWrite/shpwrite', - - three: 'external/THREE/three', - threeWindow: 'external/THREE/threeWindow', - threeCore: 'external/THREE/three112.min', - OrbitControls: 'external/THREE/OrbitControls', - PointerLockControls: 'external/THREE/PointerLockControls', - DeviceOrientationControls: 'external/THREE/DeviceOrientationControls', - ThreeSky: 'external/THREE/ThreeSky', - MeshLine: 'external/THREE/MeshLine', - OBJLoader: 'external/THREE/OBJLoader', - MTLLoader: 'external/THREE/MTLLoader', - ColladaLoader: 'external/THREE/ColladaLoader', - LineWidthPR: 'external/THREE/LineWidthPR', - WebGLWireframes: 'external/THREE/WebGLWireframes', - WebVR: 'external/THREE/WebVR', - VRController: 'external/THREE/VRController', - Detector: 'external/THREE/Detector', - VRControls: 'external/THREE/VRControls', - ThreeAR: 'external/THREE/three.ar', - - turf: 'external/Turf/turf5.1.6.min', - turfLegacy: 'external/Turf/turf.min', - highcharts: 'external/Highcharts', - //essences - //basics - Layers_: 'essence/Basics/Layers_/Layers_', - //viewer - Viewer_: 'essence/Basics/Viewer_/Viewer_', - Photosphere: 'essence/Basics/Viewer_/Photosphere', - ModelViewer: 'essence/Basics/Viewer_/ModelViewer', - //map - Map_: 'essence/Basics/Map_/Map_', - //globe - Globe_: 'essence/Basics/Globe_/Globe_', - Cameras: 'essence/Basics/Globe_/Cameras', - container: 'essence/Basics/Globe_/container', - projection: 'essence/Basics/Globe_/projection', - renderer: 'essence/Basics/Globe_/renderer', - scene: 'essence/Basics/Globe_/scene', - shaders: 'essence/Basics/Globe_/shaders', - Globe_AR: 'essence/Basics/Globe_/Addons/Globe_AR', - Globe_Compass: 'essence/Basics/Globe_/Addons/Globe_Compass', - Globe_Walk: 'essence/Basics/Globe_/Addons/Globe_Walk', - Globe_VectorsAsTiles: - 'essence/Basics/Globe_/Addons/Globe_VectorsAsTiles', - Globe_Radargrams: 'essence/Basics/Globe_/Addons/Globe_Radargrams', - //other - Formulae_: 'essence/Basics/Formulae_/Formulae_', - ToolController_: 'essence/Basics/ToolController_/ToolController_', - UserInterface_: 'essence/Basics/UserInterface_/UserInterface_', - Test_: 'essence/Basics/Test_/Test_', - - //ancillary - CursorInfo: 'essence/Ancillary/CursorInfo', - ContextMenu: 'essence/Ancillary/ContextMenu', - Coordinates: 'essence/Ancillary/Coordinates', - DataShaders: 'essence/Ancillary/DataShaders', - Description: 'essence/Ancillary/Description', - Login: 'essence/Ancillary/Login/Login', - PanelChanger: 'essence/Ancillary/PanelChanger', - ScaleBar: 'essence/Ancillary/ScaleBar', - ScaleBox: 'essence/Ancillary/ScaleBox', - Swap: 'essence/Ancillary/Swap', - Search: 'essence/Ancillary/Search', - QueryURL: 'essence/Ancillary/QueryURL', - SiteChanger: 'essence/Ancillary/SiteChanger', - Sprites: 'essence/Ancillary/Sprites', - }, - shim: { - //externals - jqueryUI: { deps: ['jquery'], exports: '$' }, - mark: { deps: ['jquery'], exports: '$' }, - - leaflet: { exports: 'L' }, - leafletDraw: { deps: ['leaflet'] }, - leafletGeometryUtil: { deps: ['leaflet'] }, - leafletSnap: { - deps: ['leaflet', 'leafletDraw', 'leafletGeometryUtil'], - }, - leafletCorridor: { deps: ['leaflet'] }, - leafletPip: { deps: ['leaflet'] }, - leafletImageTransform: { deps: ['leaflet'] }, - proj4: { deps: ['leaflet'] }, - proj4leaflet: { deps: ['leaflet', 'proj4'] }, - leafletEditable: { deps: ['leaflet'] }, - leafletPolylineDecorator: { deps: ['leaflet'] }, - leafletScaleFactor: { deps: ['leaflet'] }, - leafletColorFilter: { deps: ['leaflet'] }, - leafletTileLayerGL: { deps: ['leaflet'] }, - leafletVectorGrid: { deps: ['leaflet'] }, - - metricsGraphics: { deps: ['jquery', 'd3'] }, - dataTables: { deps: ['jquery'] }, - - fabricOverlay: { deps: ['openSeadragon'] }, - fabricA: { exports: 'fabric' }, - - png: { deps: ['zlib'] }, - - semantic: { deps: ['jquery'] }, - - threeCore: { exports: 'THREE' }, - OrbitControls: { deps: ['threeWindow'], exports: 'THREE' }, - PointerLockControls: { deps: ['threeCore'], exports: 'THREE' }, - DeviceOrientationControls: { deps: ['threeCore'], exports: 'THREE' }, - ThreeSky: { deps: ['threeCore'], exports: 'THREE' }, - Photosphere: { deps: ['threeCore'], exports: 'THREE' }, - ModelViewer: { deps: ['threeCore'], exports: 'THREE' }, - MeshLine: { deps: ['threeCore'], exports: 'THREE' }, - OBJLoader: { deps: ['threeCore'], exports: 'THREE' }, - MTLLoader: { deps: ['threeCore'], exports: 'THREE' }, - ColladaLoader: { deps: ['threeCore'], exports: 'THREE' }, - LineWidthPR: { deps: ['threeCore'], exports: 'THREE' }, - WebGLWireframes: { deps: ['threeCore'], exports: 'THREE' }, - WebVR: { deps: ['threeCore'], exports: 'THREE' }, - VRController: { deps: ['threeCore'], exports: 'THREE' }, - VRControls: { deps: ['threeCore'], exports: 'THREE' }, - ThreeAR: { deps: ['threeCore'], exports: 'THREE' }, - 'highcharts': { deps: ['jquery'], exports: 'Highcharts'}, - }, - wrapShim: true, - map: { - '*': { - css: 'css.min', // or whatever the path to require-css is - }, - }, - findNestedDependencies: false, - packages: [{ - name: 'highcharts', - main: 'highcharts' - }] -} - -if (!mmgisglobal.toolConfigs.hasOwnProperty('Kinds')) { - console.warn('Error: Kinds tool not found. Are you missing a config.js?') -} - -//Now add toolConfigs -for (let c in mmgisglobal.toolConfigs) { - //First add paths - for (let p in mmgisglobal.toolConfigs[c].paths) { - if (!require.paths.hasOwnProperty(p)) { - require.paths[p] = mmgisglobal.toolConfigs[c].paths[p] - } else { - console.warn( - 'Failed to add tool to configuration as path already exists: ' + - p - ) - } - } - //Then add shim - for (let s in mmgisglobal.toolConfigs[c].shim) { - if (!require.shim.hasOwnProperty(s)) { - require.shim[s] = mmgisglobal.toolConfigs[c].shim[s] - } else { - console.warn( - 'Failed to add tool to configuration as shim already exists: ' + - s - ) - } - } -} +// Configure Require.js +var require = { + //relative to the index calling it + baseUrl: 'scripts', + //Some browsers suspend script loading on inactive tabs so disable script load timeouts + waitSeconds: 0, + paths: { + //core + loading: 'pre/loading/loading', + landingPage: 'essence/LandingPage/LandingPage', + essence: 'essence/essence', + + //externals + attributions: 'external/attributions', + + arc: 'external/Arc/arc', + + colorPicker: 'external/ColorPicker/jqColorPicker.min', + + d3: 'external/D3/d3.v4.min', + d33: 'external/D3/d3.v3.min', //!!!chemistryplot.js still uses + + jquery: 'external/JQuery/jquery.min', + jqueryUI: 'external/JQuery/jquery-ui', + mark: 'external/JQuery/jquery.mark.min', + + leaflet: 'external/Leaflet/leaflet1.5.1', + leafletDraw: 'external/Leaflet/leaflet.draw', + leafletGeometryUtil: 'external/Leaflet/leaflet.geometryutil', + leafletSnap: 'external/Leaflet/leaflet.snap', + leafletCorridor: 'external/Leaflet/leaflet-corridor', + leafletPip: 'external/Leaflet/leaflet-pip', + leafletImageTransform: 'external/Leaflet/leaflet-imagetransform', + proj4: 'external/Leaflet/proj4-compressed', + proj4leaflet: 'external/Leaflet/proj4leaflet', + leafletEditable: 'external/Leaflet/leaflet-editable', + leafletHotline: 'external/Leaflet/leaflet.hotline.min', + leafletPolylineDecorator: 'external/Leaflet/leaflet.polylineDecorator', + leafletScaleFactor: 'external/Leaflet/leaflet.scalefactor.min', + leafletColorFilter: 'external/Leaflet/leaflet-tilelayer-colorfilter', + leafletTileLayerGL: 'external/Leaflet/leaflet.tilelayer.gl', + leafletVectorGrid: 'external/Leaflet/leaflet.vectorGrid.bundled', + + metricsGraphics: 'external/MetricsGraphics/metricsgraphics.min', + openSeadragon: 'external/OpenSeadragon/openseadragon.min', + fabricA: 'external/OpenSeadragon/fabric.adapted', + fabricOverlay: 'external/OpenSeadragon/openseadragon-fabricjs-overlay', + dataTables: 'external/DataTables/datatables.min', + nipple: 'external/NippleJS/nipplejs.min', + multiRange: 'external/MultiRange/multirange', + jsonViewer: 'external/JSONViewer/jquery.json-viewer', + fileSaver: 'external/FileSaver/FileSaver.min', + + Hammer: 'external/Hammer/hammer.min', + + HTML2Canvas: 'external/HTML2Canvas/html2canvas.min', + + png: 'external/PNG/png', + zlib: 'external/PNG/zlib', + + semantic: 'external/SemanticUI/semantic.min', + + shp: 'external/shpjs/shapefile', + shpwrite: 'external/SHPWrite/shpwrite', + + three: 'external/THREE/three', + threeWindow: 'external/THREE/threeWindow', + threeCore: 'external/THREE/three112.min', + OrbitControls: 'external/THREE/OrbitControls', + PointerLockControls: 'external/THREE/PointerLockControls', + DeviceOrientationControls: 'external/THREE/DeviceOrientationControls', + ThreeSky: 'external/THREE/ThreeSky', + MeshLine: 'external/THREE/MeshLine', + OBJLoader: 'external/THREE/OBJLoader', + MTLLoader: 'external/THREE/MTLLoader', + ColladaLoader: 'external/THREE/ColladaLoader', + LineWidthPR: 'external/THREE/LineWidthPR', + WebGLWireframes: 'external/THREE/WebGLWireframes', + WebVR: 'external/THREE/WebVR', + VRController: 'external/THREE/VRController', + Detector: 'external/THREE/Detector', + VRControls: 'external/THREE/VRControls', + ThreeAR: 'external/THREE/three.ar', + + turf: 'external/Turf/turf5.1.6.min', + turfLegacy: 'external/Turf/turf.min', + highcharts: 'external/Highcharts', + //essences + //basics + Layers_: 'essence/Basics/Layers_/Layers_', + //viewer + Viewer_: 'essence/Basics/Viewer_/Viewer_', + Photosphere: 'essence/Basics/Viewer_/Photosphere', + ModelViewer: 'essence/Basics/Viewer_/ModelViewer', + //map + Map_: 'essence/Basics/Map_/Map_', + //globe + Globe_: 'essence/Basics/Globe_/Globe_', + Cameras: 'essence/Basics/Globe_/Cameras', + container: 'essence/Basics/Globe_/container', + projection: 'essence/Basics/Globe_/projection', + renderer: 'essence/Basics/Globe_/renderer', + scene: 'essence/Basics/Globe_/scene', + shaders: 'essence/Basics/Globe_/shaders', + Globe_AR: 'essence/Basics/Globe_/Addons/Globe_AR', + Globe_Compass: 'essence/Basics/Globe_/Addons/Globe_Compass', + Globe_Walk: 'essence/Basics/Globe_/Addons/Globe_Walk', + Globe_VectorsAsTiles: + 'essence/Basics/Globe_/Addons/Globe_VectorsAsTiles', + Globe_Radargrams: 'essence/Basics/Globe_/Addons/Globe_Radargrams', + //other + Formulae_: 'essence/Basics/Formulae_/Formulae_', + ToolController_: 'essence/Basics/ToolController_/ToolController_', + UserInterface_: 'essence/Basics/UserInterface_/UserInterface_', + Test_: 'essence/Basics/Test_/Test_', + + //ancillary + CursorInfo: 'essence/Ancillary/CursorInfo', + ContextMenu: 'essence/Ancillary/ContextMenu', + Coordinates: 'essence/Ancillary/Coordinates', + DataShaders: 'essence/Ancillary/DataShaders', + Description: 'essence/Ancillary/Description', + Login: 'essence/Ancillary/Login/Login', + PanelChanger: 'essence/Ancillary/PanelChanger', + ScaleBar: 'essence/Ancillary/ScaleBar', + ScaleBox: 'essence/Ancillary/ScaleBox', + Swap: 'essence/Ancillary/Swap', + Search: 'essence/Ancillary/Search', + QueryURL: 'essence/Ancillary/QueryURL', + SiteChanger: 'essence/Ancillary/SiteChanger', + Sprites: 'essence/Ancillary/Sprites', + }, + shim: { + //externals + jqueryUI: { deps: ['jquery'], exports: '$' }, + mark: { deps: ['jquery'], exports: '$' }, + + leaflet: { exports: 'L' }, + leafletDraw: { deps: ['leaflet'] }, + leafletGeometryUtil: { deps: ['leaflet'] }, + leafletSnap: { + deps: ['leaflet', 'leafletDraw', 'leafletGeometryUtil'], + }, + leafletCorridor: { deps: ['leaflet'] }, + leafletPip: { deps: ['leaflet'] }, + leafletImageTransform: { deps: ['leaflet'] }, + proj4: { deps: ['leaflet'] }, + proj4leaflet: { deps: ['leaflet', 'proj4'] }, + leafletEditable: { deps: ['leaflet'] }, + leafletPolylineDecorator: { deps: ['leaflet'] }, + leafletScaleFactor: { deps: ['leaflet'] }, + leafletColorFilter: { deps: ['leaflet'] }, + leafletTileLayerGL: { deps: ['leaflet'] }, + leafletVectorGrid: { deps: ['leaflet'] }, + + metricsGraphics: { deps: ['jquery', 'd3'] }, + dataTables: { deps: ['jquery'] }, + + fabricOverlay: { deps: ['openSeadragon'] }, + fabricA: { exports: 'fabric' }, + + png: { deps: ['zlib'] }, + + semantic: { deps: ['jquery'] }, + + threeCore: { exports: 'THREE' }, + OrbitControls: { deps: ['threeWindow'], exports: 'THREE' }, + PointerLockControls: { deps: ['threeCore'], exports: 'THREE' }, + DeviceOrientationControls: { deps: ['threeCore'], exports: 'THREE' }, + ThreeSky: { deps: ['threeCore'], exports: 'THREE' }, + Photosphere: { deps: ['threeCore'], exports: 'THREE' }, + ModelViewer: { deps: ['threeCore'], exports: 'THREE' }, + MeshLine: { deps: ['threeCore'], exports: 'THREE' }, + OBJLoader: { deps: ['threeCore'], exports: 'THREE' }, + MTLLoader: { deps: ['threeCore'], exports: 'THREE' }, + ColladaLoader: { deps: ['threeCore'], exports: 'THREE' }, + LineWidthPR: { deps: ['threeCore'], exports: 'THREE' }, + WebGLWireframes: { deps: ['threeCore'], exports: 'THREE' }, + WebVR: { deps: ['threeCore'], exports: 'THREE' }, + VRController: { deps: ['threeCore'], exports: 'THREE' }, + VRControls: { deps: ['threeCore'], exports: 'THREE' }, + ThreeAR: { deps: ['threeCore'], exports: 'THREE' }, + highcharts: { deps: ['jquery'], exports: 'Highcharts' }, + }, + wrapShim: true, + map: { + '*': { + css: 'css.min', // or whatever the path to require-css is + }, + }, + findNestedDependencies: false, + packages: [ + { + name: 'highcharts', + main: 'highcharts', + }, + ], +} + +if (!mmgisglobal.toolConfigs.hasOwnProperty('Kinds')) { + console.warn('Error: Kinds tool not found. Are you missing a config.js?') +} + +//Now add toolConfigs +for (let c in mmgisglobal.toolConfigs) { + //First add paths + for (let p in mmgisglobal.toolConfigs[c].paths) { + if (!require.paths.hasOwnProperty(p)) { + require.paths[p] = mmgisglobal.toolConfigs[c].paths[p] + } else { + console.warn( + 'Failed to add tool to configuration as path already exists: ' + + p + ) + } + } + //Then add shim + for (let s in mmgisglobal.toolConfigs[c].shim) { + if (!require.shim.hasOwnProperty(s)) { + require.shim[s] = mmgisglobal.toolConfigs[c].shim[s] + } else { + console.warn( + 'Failed to add tool to configuration as shim already exists: ' + + s + ) + } + } +} diff --git a/scripts/essence/Ancillary/DataShaders.js b/scripts/essence/Ancillary/DataShaders.js index 4b0291e3..4939b941 100644 --- a/scripts/essence/Ancillary/DataShaders.js +++ b/scripts/essence/Ancillary/DataShaders.js @@ -1,4 +1,4 @@ -define([], function() { +define([], function () { /* var rgba = { r: 92, g: 244, b: 12, a: 247 } @@ -32,6 +32,17 @@ define([], function() { console.log(encodeRGBA(d)) */ return (DataShaders = { + image: { + // prettier-ignore + frag: [ + 'void main(void) {', + // Fetch color from texture + 'highp vec4 texelColour = texture2D(uTexture0, vec2(vTextureCoords.s, vTextureCoords.t));', + 'gl_FragColor = texelColour;', + '}' + ].join('\n'), + settings: [], + }, flood: { // prettier-ignore frag: [ diff --git a/scripts/essence/Ancillary/Description.js b/scripts/essence/Ancillary/Description.js index 48a26685..a2bba9d9 100644 --- a/scripts/essence/Ancillary/Description.js +++ b/scripts/essence/Ancillary/Description.js @@ -1,234 +1,229 @@ -//The bottom left text that describes to the user the basic mmgis state - -define(['jquery', 'd3', 'Formulae_'], function($, d3, F_) { - var Description = { - descCont: null, - descMission: null, - descSite: null, - descPoint: null, - L_: null, - init: function(mission, site, Map_, L_) { - this.L_ = L_ - this.Map_ = Map_ - this.descCont = d3.select( - '#main-container > #topBar .mainDescription' - ) - this.descInfoCont = d3.select('#main-container > #topBar .mainInfo') - /* - this.descMission = descCont - .append('div') - .attr('id', 'mainDescMission') - .style('line-height', '32px') - .style('padding-left', '8px') - .style('color', '#EEE') - .style('font-size', '22px') - .style('margin', '0') - .style('cursor', 'default') - .style('text-align', 'center') - .style('cursor', 'pointer') - .html(mission) - var missionWidth = $('#mainDescMission').width() + 3 - */ - - this.descSite = this.descCont - .append('p') - .attr('id', 'mainDescSite') - .style('display', 'none') //!!!!!!!!!!!! - .style('line-height', '29px') - .style('color', '#CCC') - .style('font-size', '16px') - .style('font-weight', 'bold') - .style('cursor', 'pointer') - .style('margin', '0') - - this.descPoint = this.descCont - .append('p') - .attr('id', 'mainDescPoint') - .style('display', 'flex') - .style('white-space', 'nowrap') - .style('line-height', '29px') - .style('font-size', '14px') - .style('color', 'var(--color-mw2)') - .style('font-weight', 'bold') - .style('cursor', 'pointer') - .style('margin', '0') - - this.descPointInner = this.descPoint - .append('div') - .attr('id', 'mainDescPointInner') - .style('display', 'flex') - .style('white-space', 'nowrap') - .style('line-height', '29px') - .style('font-size', '14px') - .style('color', 'var(--color-mw2)') - .style('font-weight', 'bold') - .style('cursor', 'pointer') - .style('margin', '0') - this.descPointLinks = this.descPoint - .append('div') - .attr('id', 'mainDescPointLinks') - .style('display', 'flex') - .style('white-space', 'nowrap') - .style('line-height', '29px') - .style('font-size', '14px') - .style('color', '#AAA') - .style('font-weight', 'bold') - .style('cursor', 'pointer') - .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() - ) - }) - }, - updateInfo() { - let infos = [] - - for (let layer in this.L_.layersNamed) { - let l = this.L_.layersNamed[layer] - if ( - l.hasOwnProperty('variables') && - l.variables.hasOwnProperty('info') && - l.variables.info.hasOwnProperty('length') - ) { - let layers = this.L_.layersGroup[layer]._layers - let newInfo = '' - for (let i = 0; i < l.variables.info.length; i++) { - let which = - l.variables.info[i].which != null && - !isNaN(l.variables.info[i].which) - ? Math.max( - Math.min( - which, - Object.keys(layers).length - 1 - ), - 0 - ) - : Object.keys(layers).length - 1 - let feature = layers[Object.keys(layers)[which]].feature - let infoText = F_.bracketReplace( - l.variables.info[i].value, - feature.properties - ) - let lat = !isNaN(feature.geometry.coordinates[1]) - ? feature.geometry.coordinates[1] - : null - let lng = !isNaN(feature.geometry.coordinates[0]) - ? feature.geometry.coordinates[0] - : null - - newInfo += '

' - if (l.variables.info[i].icon) - newInfo += - "" - newInfo += '
' + infoText + '
' - } - if (newInfo.length > 0) infos.push(newInfo) - } - } - this.descInfoCont.html(infos.join('\n')) - - this.descInfoCont.style('display', 'flex') - $('#main-container > #topBar .mainInfo').animate( - { - opacity: 1, - }, - 80 - ) - - d3.select('#main-container > #topBar .mainInfo > div').on( - 'click', - function() { - let lat = d3.select(this).attr('lat') - let lng = d3.select(this).attr('lng') - - if (lat != null && lng != null) { - Description.Map_.map.setView( - [lat, lng], - Description.Map_.mapScaleZoom || - Description.Map_.map.getZoom() - ) - } - } - ) - }, - updateSite: function(site) { - if (site != null) { - Description.descSite.html(site) - } - }, - updatePoint: function(activeLayer) { - this.descCont.style('display', 'flex') - $('#main-container > #topBar .mainDescription').animate( - { - opacity: 1, - }, - 80 - ) - if (activeLayer != null && activeLayer.hasOwnProperty('options')) { - var keyAsName - var links = "" - - if ( - this.L_.layersNamed[activeLayer.options.layerName] && - this.L_.layersNamed[activeLayer.options.layerName].variables - ) { - let v = this.L_.layersNamed[activeLayer.options.layerName] - .variables - if (v.links) { - links = '' - for (let i = 0; i < v.links.length; i++) { - links += - "" + - v.links[i].name + - "" + - '' - } - } - } - - let key = - activeLayer.useKeyAsName || - Object.keys(activeLayer.feature.properties)[0] - keyAsName = - activeLayer.feature.properties[key] + - '
(' + - key + - ')
' - - Description.descPointInner.html( - activeLayer.options.layerName + ': ' + keyAsName - ) - Description.descPointLinks.html(links) - } - }, - } - - return Description -}) +//The bottom left text that describes to the user the basic mmgis state + +define(['jquery', 'd3', 'Formulae_'], function ($, d3, F_) { + var Description = { + descCont: null, + descMission: null, + descSite: null, + descPoint: null, + L_: null, + init: function (mission, site, Map_, L_) { + this.L_ = L_ + this.Map_ = Map_ + this.descCont = d3.select('.mainDescription') + this.descInfoCont = d3.select('.mainInfo') + /* + this.descMission = descCont + .append('div') + .attr('id', 'mainDescMission') + .style('line-height', '32px') + .style('padding-left', '8px') + .style('color', '#EEE') + .style('font-size', '22px') + .style('margin', '0') + .style('cursor', 'default') + .style('text-align', 'center') + .style('cursor', 'pointer') + .html(mission) + var missionWidth = $('#mainDescMission').width() + 3 + */ + + this.descSite = this.descCont + .append('p') + .attr('id', 'mainDescSite') + .style('display', 'none') //!!!!!!!!!!!! + .style('line-height', '29px') + .style('color', '#CCC') + .style('font-size', '16px') + .style('font-weight', 'bold') + .style('cursor', 'pointer') + .style('margin', '0') + + this.descPoint = this.descCont + .append('p') + .attr('id', 'mainDescPoint') + .style('display', 'flex') + .style('white-space', 'nowrap') + .style('line-height', '29px') + .style('font-size', '14px') + .style('color', 'var(--color-mw2)') + .style('font-weight', 'bold') + .style('cursor', 'pointer') + .style('margin', '0') + + this.descPointInner = this.descPoint + .append('div') + .attr('id', 'mainDescPointInner') + .style('display', 'flex') + .style('white-space', 'nowrap') + .style('line-height', '29px') + .style('font-size', '14px') + .style('color', 'var(--color-mw2)') + .style('font-weight', 'bold') + .style('cursor', 'pointer') + .style('margin', '0') + this.descPointLinks = this.descPoint + .append('div') + .attr('id', 'mainDescPointLinks') + .style('display', 'flex') + .style('white-space', 'nowrap') + .style('line-height', '29px') + .style('font-size', '14px') + .style('color', '#AAA') + .style('font-weight', 'bold') + .style('cursor', 'pointer') + .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() + ) + }) + }, + updateInfo() { + let infos = [] + + for (let layer in this.L_.layersNamed) { + let l = this.L_.layersNamed[layer] + if ( + l.hasOwnProperty('variables') && + l.variables.hasOwnProperty('info') && + l.variables.info.hasOwnProperty('length') + ) { + let layers = this.L_.layersGroup[layer]._layers + let newInfo = '' + for (let i = 0; i < l.variables.info.length; i++) { + let which = + l.variables.info[i].which != null && + !isNaN(l.variables.info[i].which) + ? Math.max( + Math.min( + which, + Object.keys(layers).length - 1 + ), + 0 + ) + : Object.keys(layers).length - 1 + let feature = layers[Object.keys(layers)[which]].feature + let infoText = F_.bracketReplace( + l.variables.info[i].value, + feature.properties + ) + let lat = !isNaN(feature.geometry.coordinates[1]) + ? feature.geometry.coordinates[1] + : null + let lng = !isNaN(feature.geometry.coordinates[0]) + ? feature.geometry.coordinates[0] + : null + + newInfo += '
' + if (l.variables.info[i].icon) + newInfo += + "" + newInfo += '
' + infoText + '
' + } + if (newInfo.length > 0) infos.push(newInfo) + } + } + this.descInfoCont.html(infos.join('\n')) + + this.descInfoCont.style('display', 'flex') + $('.mainInfo').animate( + { + opacity: 1, + }, + 80 + ) + + d3.select('.mainInfo > div').on('click', function () { + let lat = d3.select(this).attr('lat') + let lng = d3.select(this).attr('lng') + + if (lat != null && lng != null) { + Description.Map_.map.setView( + [lat, lng], + Description.Map_.mapScaleZoom || + Description.Map_.map.getZoom() + ) + } + }) + }, + updateSite: function (site) { + if (site != null) { + Description.descSite.html(site) + } + }, + updatePoint: function (activeLayer) { + this.descCont.style('display', 'flex') + $('.mainDescription').animate( + { + opacity: 1, + }, + 80 + ) + if (activeLayer != null && activeLayer.hasOwnProperty('options')) { + var keyAsName + var links = "" + + if ( + this.L_.layersNamed[activeLayer.options.layerName] && + this.L_.layersNamed[activeLayer.options.layerName].variables + ) { + let v = this.L_.layersNamed[activeLayer.options.layerName] + .variables + if (v.links) { + links = '' + for (let i = 0; i < v.links.length; i++) { + links += + "" + + v.links[i].name + + "" + + '' + } + } + } + + let key = + activeLayer.useKeyAsName || + Object.keys(activeLayer.feature.properties)[0] + keyAsName = + activeLayer.feature.properties[key] + + '
(' + + key + + ')
' + + Description.descPointInner.html( + activeLayer.options.layerName + ': ' + keyAsName + ) + Description.descPointLinks.html(links) + } + }, + } + + return Description +}) diff --git a/scripts/essence/Ancillary/Login/Login.js b/scripts/essence/Ancillary/Login/Login.js index 13437574..f7ddb1ca 100644 --- a/scripts/essence/Ancillary/Login/Login.js +++ b/scripts/essence/Ancillary/Login/Login.js @@ -1,534 +1,530 @@ -//Depends on a div with id 'topBar' -define([ - 'jquery', - 'd3', - 'Formulae_', - 'Layers_', - 'ToolController_', - 'semantic', -], function($, d3, F_, L_, ToolController_) { - var emailSuffix = '@domain.com' - // prettier-ignore - var modalFormSignIn = - "" + - "
" + - "
" + - "" + - "
" + - "" + - "" + - "
" + - "
" + - "" + - "
" + - "" + - "
" + - "" + - "" + - "
" + - "
" + - "" + - "" + - "
or sign up
" + - "
" + - "
Invalid username or password
" + - "





"; - - var validate = { - username: false, - email: false, - password: false, - retypepassword: false, - } - - var Login = { - loginBar: null, - loggedIn: false, - username: null, - signUp: false, - beganLoggedIn: false, - init: function() { - if ( - mmgisglobal.AUTH == 'csso' && - mmgisglobal.hasOwnProperty('user') - ) { - this.loggedIn = true - this.username = mmgisglobal.user - this.beganLoggedIn = true - } - - Login.loginBar = d3 - .select('#main-container') - .append('div') - .attr('id', 'loginDiv') - .style('display', 'flex') - .style('position', 'absolute') - .style('top', '0px') - .style('right', '0px') - .style('z-index', '2006') - .style('margin', '5px') - .style('color', '#aaa') - - d3.select('#topBar') - .append('div') - .attr('id', 'loginModal') - .attr('class', 'ui small basic modal') - .style('width', '300px') - .style('margin', '0 0 0 -150px') - .html(modalFormSignIn) - $('#loginModal #loginInUpToggle').on('click', function() { - $('#loginErrorMessage').animate({ opacity: '0' }, 500) - if (!Login.signUp) { - Login.signUp = true - $('#loginEmail').css({ display: 'inherit' }) - $('#loginRetypePassword').css({ display: 'inherit' }) - $('#loginSubmit').val('Sign Up') - $('#loginInUpToggle').html('or sign in') - } else { - Login.signUp = false - $('#loginEmail').css({ display: 'none' }) - $('#loginRetypePassword').css({ display: 'none' }) - $('#loginSubmit').val('Sign In') - $('#loginInUpToggle').html('or sign up') - } - }) - - $('#loginUsernameInput').on('change paste keyup', function() { - $('#loginUsernameInput').css({ 'background-color': 'white' }) - var value = $(this).val() - if (value.length > 0) { - $('#loginUsernameInputIcon').animate({ opacity: '1' }, 80) - $('#loginEmailInputIcon').animate({ opacity: '1' }, 80) - validate.username = true - validate.email = true - } else { - $('#loginUsernameInputIcon').animate({ opacity: '0' }, 80) - validate.username = false - } - $('#loginEmailInput').val(value + emailSuffix) - }) - - $('#loginEmailInput').on('change paste keyup', function() { - var value = $(this).val() - if (value.length > 0) { - $('#loginEmailInputIcon').animate({ opacity: '1' }, 80) - validate.email = true - } else { - $('#loginEmailInputIcon').animate({ opacity: '0' }, 80) - validate.email = false - } - }) - - $('#loginPasswordInput').on('change paste keyup', function() { - var value = $(this).val() - var retypePass = $('#loginRetypePasswordInput').val() - if (value.length > 0) { - $('#loginPasswordInputIcon').animate({ opacity: '1' }, 80) - validate.password = true - if (value == retypePass) { - $('#loginRetypePasswordInputIcon') - .removeClass('red') - .removeClass('remove') - $('#loginRetypePasswordInputIcon') - .addClass('blue') - .addClass('checkmark') - } else { - $('#loginRetypePasswordInputIcon') - .removeClass('blue') - .removeClass('checkmark') - $('#loginRetypePasswordInputIcon') - .addClass('red') - .addClass('remove') - } - } else { - $('#loginPasswordInputIcon').animate({ opacity: '0' }, 80) - validate.password = false - } - }) - - $('#loginRetypePasswordInput').on('change paste keyup', function() { - var value = $(this).val() - var actualPass = $('#loginPasswordInput').val() - if (value.length > 0) { - $('#loginRetypePasswordInputIcon').animate( - { opacity: '1' }, - 80 - ) - if (actualPass == value) { - $('#loginRetypePasswordInputIcon') - .removeClass('red') - .removeClass('remove') - $('#loginRetypePasswordInputIcon') - .addClass('blue') - .addClass('checkmark') - validate.retypepassword = true - } else { - $('#loginRetypePasswordInputIcon') - .removeClass('blue') - .removeClass('checkmark') - $('#loginRetypePasswordInputIcon') - .addClass('red') - .addClass('remove') - validate.retypepassword = false - } - } else { - $('#loginRetypePasswordInputIcon').animate( - { opacity: '0' }, - 80 - ) - validate.retypepassword = false - } - }) - - $('#loginForm').submit(function(e) { - e.preventDefault() - var values = {} - $.each($(this).serializeArray(), function(i, field) { - values[field.name] = field.value - }) - - values['mission'] = L_.mission - values['master'] = L_.masterdb - - if (!Login.signUp) { - if (validate.username && validate.password) { - calls.api( - 'login', - values, - function(d) { - Login.username = values.username - mmgisglobal.user = Login.username - mmgisglobal.groups = d.groups - loginSuccess(d) - }, - function(d) { - alert(d.message) - } - ) - } else { - var errorMessage = '' - if (!validate.username) - errorMessage += 'Please enter a username.
' - if (!validate.password) - errorMessage += 'Please enter a password.
' - $('#loginErrorMessage') - .html(errorMessage) - .animate({ opacity: '1' }, 80) - } - } else { - if ( - validate.username && - validate.email && - validate.password && - validate.retypepassword - ) { - calls.api( - 'signup', - values, - function(d) { - //This automatically signed a new user in - if (mmgisglobal.AUTH === 'local') { - //This just flashes blue to show it worked - // prettier-ignore - $('#loginErrorMessage').animate({ opacity: '0' }, 500) - // prettier-ignore - $('#loginModal').parent() - .animate({'background-color': 'rgba(46,180,255,0.6)'}, 1000, - function() { - setTimeout(function() { - $('#loginModal').parent().css({'background-color':'rgba(0,0,0,0.6)'}) - }, 2000 ) - } - ) - } else { - Login.username = d.username - mmgisglobal.user = Login.username - mmgisglobal.groups = d.groups - loginSuccess(d) - } - }, - function(d) { - if (mmgisglobal.AUTH === 'local') { - $('#loginErrorMessage') - .html(d.message) - .animate({ opacity: '1' }, 80) - } else { - loginSuccess(d) - } - } - ) - } else { - var errorMessage = '' - if (!validate.username) - errorMessage += 'Please enter a username.
' - if (!validate.email) - errorMessage += 'Please enter an email.
' - if (!validate.password) - errorMessage += 'Please enter a password.
' - if (!validate.retypepassword) - errorMessage += 'Please retype password.
' - $('#loginErrorMessage') - .html(errorMessage) - .animate({ opacity: '1' }, 80) - } - } - return false - }) - - Login.loginBar - .append('div') - .attr('id', 'loginUser') - .attr('title', Login.loggedIn ? Login.username : '') - .style('text-align', 'center') - .style('font-size', '12px') - .style('font-weight', 'bold') - .style('font-family', 'sans-serif') - .style('margin-right', '5px') - .style('cursor', 'pointer') - .style('width', '30px') - .style('height', '30px') - .style('line-height', '30px') - .style('color', 'white') - .style( - 'background', - Login.loggedIn - ? F_.getColorScale( - F_.ASCIIProduct(Login.username) / - Login.username.length - ) - : 'transparent' - ) - .style('opacity', Login.loggedIn ? 1 : 0) - .style('text-transform', 'uppercase') - .style('transition', 'opacity 0.2s ease-out') - .html(Login.loggedIn ? Login.username[0] : '') - - //Show signup for admins - if ( - mmgisglobal.AUTH === 'local' && - mmgisglobal.permission === '111' - ) { - Login.loginBar - .append('div') - .attr('id', 'forceSignupButton') - .style('text-align', 'center') - .style('cursor', 'pointer') - .style('width', '30px') - .style('height', '30px') - .style('line-height', '26px') - .style('margin-right', '4px') - .style('background', 'var(--color-a)') - .style('pointer-events', 'all') - .on('click', function() { - //Open login - //default to signup - Login.signUp = true - $('#loginEmail').css({ display: 'inherit' }) - $('#loginRetypePassword').css({ display: 'inherit' }) - $('#loginSubmit').val('Sign Up') - $('#loginInUpToggle').html('or sign in') - //and open modal - $('#loginModal').modal('show') - }) - .append('i') - .attr('id', 'forceSignupButtonIcon') - .attr('class', 'mdi mdi-account-multiple mdi-18px') - } - - Login.loginBar - .append('div') - .attr('id', 'loginoutButton') - .attr('title', Login.loggedIn ? 'Logout' : 'Login') - .style('text-align', 'center') - .style('cursor', 'pointer') - .style('width', '30px') - .style('height', '30px') - .style('line-height', '27px') - .style('padding-left', '2px') - .style('border', '1px solid var(--color-e)') - .style('background', 'var(--color-i)') - .style('pointer-events', 'all') - .on('click', function() { - if (Login.loggedIn) { - //Then Logout - if (Login.beganLoggedIn) { - Login.loggedIn = false - window.location.href = '/ssologoutredirect' - } else { - calls.api( - 'logout', - { username: Login.username }, - function(d) { - ToolController_.closeActiveTool() - mmgisglobal.user = 'guest' - mmgisglobal.groups = [] - - Login.username = null - Login.loggedIn = false - d3.select('#loginUser') - .style('opacity', 0) - .html('') - d3.select('#loginoutButton').attr( - 'title', - 'Login' - ) - d3.select('#loginoutButtonIcon').attr( - 'class', - 'mdi mdi-login mdi-18px' - ) - // Destroy the cookie session here - var decodedCookie = decodeURIComponent( - document.cookie - ) - var cookies = decodedCookie.split(';') - var MMGISUser = cookies[0].split('=') - MMGISUser = JSON.parse(MMGISUser[1]) - MMGISUser.username = '' - MMGISUser.token = '' - - if (mmgisglobal.AUTH === 'local') { - location.reload() - } - }, - function(d) {} - ) - } - } else { - //Open login - //default to login - Login.signUp = false - $('#loginEmail').css({ display: 'none' }) - $('#loginRetypePassword').css({ display: 'none' }) - $('#loginSubmit').val('Sign In') - $('#loginInUpToggle').html('or sign up') - //and open modal - $('#loginModal').modal('show') - } - }) - .append('i') - .attr('id', 'loginoutButtonIcon') - .attr( - 'class', - Login.loggedIn - ? 'mdi mdi-logout mdi-18px' - : 'mdi mdi-login mdi-18px' - ) - - $('#loginModal').modal({ - blurring: true, - }) - - //Sign in at page load from cookie if possible - if (mmgisglobal.AUTH !== 'csso') { - calls.api( - 'login', - { - useToken: true, - }, - function(d) { - Login.username = d.username - mmgisglobal.user = Login.username - mmgisglobal.groups = d.groups - loginSuccess(d) - }, - function(d) { - loginSuccess(d, true) - } - ) - } - }, - } - - function loginSuccess(data, ignoreError) { - if (data.status == 'success') { - document.cookie = - 'MMGISUser=' + - JSON.stringify({ - username: data.username, - token: data.token, - }) - - Login.loggedIn = true - $('#loginErrorMessage').animate({ opacity: '0' }, 500) - $('#loginModal') - .parent() - .animate( - { 'background-color': 'rgba(46,180,255,0.6)' }, - 1000, - function() { - ToolController_.closeActiveTool() - setTimeout(function() { - $('#loginModal').modal('hide') - $('#loginForm').trigger('reset') - $('#loginModal') - .parent() - .css({ 'background-color': 'rgba(0,0,0,0.6)' }) - $('#loginUsernameInputIcon').css({ opacity: '0' }) - $('#loginEmailInputIcon').css({ opacity: '0' }) - $('#loginPasswordInputIcon').css({ opacity: '0' }) - $('#loginRetypePasswordInputIcon').css({ - opacity: '0', - }) - validate.username = false - validate.email = false - validate.password = false - validate.retypepassword = false - $('#loginButton').html('logout') - d3.select('#loginoutButton').attr('title', 'Logout') - d3.select('#loginoutButtonIcon').attr( - 'class', - 'mdi mdi-logout mdi-18px' - ) - - d3.select('#loginUser').attr( - 'title', - Login.loggedIn ? Login.username : '' - ) - - $('#loginUser') - .css({ - opacity: 1, - background: Login.loggedIn - ? F_.getColorScale( - F_.ASCIIProduct(Login.username) / - Login.username.length - ) - : 'transparent', - }) - .html(Login.username[0]) - }, 600) - } - ) - } else { - document.cookie = - 'MMGISUser=' + - JSON.stringify({ - username: '', - token: '', - }) - - if (mmgisglobal.AUTH === 'local') { - location.reload() - } - if (ignoreError) return - - $('#loginErrorMessage') - .html(data.message) - .animate({ opacity: '1' }, 80) - } - } - - return Login -}) +//Depends on a div with id 'topBar' +define([ + 'jquery', + 'd3', + 'Formulae_', + 'Layers_', + 'ToolController_', + 'semantic', +], function ($, d3, F_, L_, ToolController_) { + var emailSuffix = '@domain.com' + // prettier-ignore + var modalFormSignIn = + "" + + "
" + + "
" + + "" + + "
" + + "" + + "" + + "
" + + "
" + + "" + + "
" + + "" + + "
" + + "" + + "" + + "
" + + "
" + + "" + + "" + + "
or sign up
" + + "
" + + "
Invalid username or password
" + + "





"; + + var validate = { + username: false, + email: false, + password: false, + retypepassword: false, + } + + var Login = { + loginBar: null, + loggedIn: false, + username: null, + signUp: false, + beganLoggedIn: false, + init: function () { + if ( + mmgisglobal.AUTH == 'csso' && + mmgisglobal.hasOwnProperty('user') + ) { + this.loggedIn = true + this.username = mmgisglobal.user + this.beganLoggedIn = true + } + + Login.loginBar = d3 + .select('#main-container') + .append('div') + .attr('id', 'loginDiv') + .style('display', 'flex') + .style('position', 'absolute') + .style('top', '0px') + .style('right', '0px') + .style('z-index', '2006') + .style('margin', '5px') + .style('color', '#aaa') + .style('mix-blend-mode', 'luminosity') + + d3.select('#topBar') + .append('div') + .attr('id', 'loginModal') + .attr('class', 'ui small basic modal') + .style('width', '300px') + .style('margin', '0 0 0 -150px') + .html(modalFormSignIn) + $('#loginModal #loginInUpToggle').on('click', function () { + $('#loginErrorMessage').animate({ opacity: '0' }, 500) + if (!Login.signUp) { + Login.signUp = true + $('#loginEmail').css({ display: 'inherit' }) + $('#loginRetypePassword').css({ display: 'inherit' }) + $('#loginSubmit').val('Sign Up') + $('#loginInUpToggle').html('or sign in') + } else { + Login.signUp = false + $('#loginEmail').css({ display: 'none' }) + $('#loginRetypePassword').css({ display: 'none' }) + $('#loginSubmit').val('Sign In') + $('#loginInUpToggle').html('or sign up') + } + }) + + $('#loginUsernameInput').on('change paste keyup', function () { + $('#loginUsernameInput').css({ 'background-color': 'white' }) + var value = $(this).val() + if (value.length > 0) { + $('#loginUsernameInputIcon').animate({ opacity: '1' }, 80) + $('#loginEmailInputIcon').animate({ opacity: '1' }, 80) + validate.username = true + validate.email = true + } else { + $('#loginUsernameInputIcon').animate({ opacity: '0' }, 80) + validate.username = false + } + $('#loginEmailInput').val(value + emailSuffix) + }) + + $('#loginEmailInput').on('change paste keyup', function () { + var value = $(this).val() + if (value.length > 0) { + $('#loginEmailInputIcon').animate({ opacity: '1' }, 80) + validate.email = true + } else { + $('#loginEmailInputIcon').animate({ opacity: '0' }, 80) + validate.email = false + } + }) + + $('#loginPasswordInput').on('change paste keyup', function () { + var value = $(this).val() + var retypePass = $('#loginRetypePasswordInput').val() + if (value.length > 0) { + $('#loginPasswordInputIcon').animate({ opacity: '1' }, 80) + validate.password = true + if (value == retypePass) { + $('#loginRetypePasswordInputIcon') + .removeClass('red') + .removeClass('remove') + $('#loginRetypePasswordInputIcon') + .addClass('blue') + .addClass('checkmark') + } else { + $('#loginRetypePasswordInputIcon') + .removeClass('blue') + .removeClass('checkmark') + $('#loginRetypePasswordInputIcon') + .addClass('red') + .addClass('remove') + } + } else { + $('#loginPasswordInputIcon').animate({ opacity: '0' }, 80) + validate.password = false + } + }) + + $('#loginRetypePasswordInput').on( + 'change paste keyup', + function () { + var value = $(this).val() + var actualPass = $('#loginPasswordInput').val() + if (value.length > 0) { + $('#loginRetypePasswordInputIcon').animate( + { opacity: '1' }, + 80 + ) + if (actualPass == value) { + $('#loginRetypePasswordInputIcon') + .removeClass('red') + .removeClass('remove') + $('#loginRetypePasswordInputIcon') + .addClass('blue') + .addClass('checkmark') + validate.retypepassword = true + } else { + $('#loginRetypePasswordInputIcon') + .removeClass('blue') + .removeClass('checkmark') + $('#loginRetypePasswordInputIcon') + .addClass('red') + .addClass('remove') + validate.retypepassword = false + } + } else { + $('#loginRetypePasswordInputIcon').animate( + { opacity: '0' }, + 80 + ) + validate.retypepassword = false + } + } + ) + + $('#loginForm').submit(function (e) { + e.preventDefault() + var values = {} + $.each($(this).serializeArray(), function (i, field) { + values[field.name] = field.value + }) + + values['mission'] = L_.mission + values['master'] = L_.masterdb + + if (!Login.signUp) { + if (validate.username && validate.password) { + calls.api( + 'login', + values, + function (d) { + Login.username = values.username + mmgisglobal.user = Login.username + mmgisglobal.groups = d.groups + loginSuccess(d) + }, + function (d) { + alert(d.message) + } + ) + } else { + var errorMessage = '' + if (!validate.username) + errorMessage += 'Please enter a username.
' + if (!validate.password) + errorMessage += 'Please enter a password.
' + $('#loginErrorMessage') + .html(errorMessage) + .animate({ opacity: '1' }, 80) + } + } else { + if ( + validate.username && + validate.email && + validate.password && + validate.retypepassword + ) { + calls.api( + 'signup', + values, + function (d) { + //This automatically signed a new user in + if (mmgisglobal.AUTH === 'local') { + //This just flashes blue to show it worked + // prettier-ignore + $('#loginErrorMessage').animate({ opacity: '0' }, 500) + // prettier-ignore + $('#loginModal').parent() + .animate({'background-color': 'rgba(46,180,255,0.6)'}, 1000, + function() { + setTimeout(function() { + $('#loginModal').parent().css({'background-color':'rgba(0,0,0,0.6)'}) + }, 2000 ) + } + ) + } else { + Login.username = d.username + mmgisglobal.user = Login.username + mmgisglobal.groups = d.groups + loginSuccess(d) + } + }, + function (d) { + if (mmgisglobal.AUTH === 'local') { + $('#loginErrorMessage') + .html(d.message) + .animate({ opacity: '1' }, 80) + } else { + loginSuccess(d) + } + } + ) + } else { + var errorMessage = '' + if (!validate.username) + errorMessage += 'Please enter a username.
' + if (!validate.email) + errorMessage += 'Please enter an email.
' + if (!validate.password) + errorMessage += 'Please enter a password.
' + if (!validate.retypepassword) + errorMessage += 'Please retype password.
' + $('#loginErrorMessage') + .html(errorMessage) + .animate({ opacity: '1' }, 80) + } + } + return false + }) + + Login.loginBar + .append('div') + .attr('id', 'loginUser') + .attr('title', Login.loggedIn ? Login.username : '') + .style('text-align', 'center') + .style('font-size', '12px') + .style('font-weight', 'bold') + .style('font-family', 'sans-serif') + .style('margin-right', '5px') + .style('cursor', 'pointer') + .style('width', '30px') + .style('height', '30px') + .style('line-height', '30px') + .style('color', 'white') + .style( + 'background', + Login.loggedIn ? 'var(--color-i)' : 'transparent' + ) + .style('opacity', Login.loggedIn ? 1 : 0) + .style('text-transform', 'uppercase') + .style('transition', 'opacity 0.2s ease-out') + .html(Login.loggedIn ? Login.username[0] : '') + + //Show signup for admins + if ( + mmgisglobal.AUTH === 'local' && + mmgisglobal.permission === '111' + ) { + Login.loginBar + .append('div') + .attr('id', 'forceSignupButton') + .style('text-align', 'center') + .style('cursor', 'pointer') + .style('width', '30px') + .style('height', '30px') + .style('line-height', '26px') + .style('margin-right', '4px') + .style('background', 'var(--color-a)') + .style('pointer-events', 'all') + .on('click', function () { + //Open login + //default to signup + Login.signUp = true + $('#loginEmail').css({ display: 'inherit' }) + $('#loginRetypePassword').css({ display: 'inherit' }) + $('#loginSubmit').val('Sign Up') + $('#loginInUpToggle').html('or sign in') + //and open modal + $('#loginModal').modal('show') + }) + .append('i') + .attr('id', 'forceSignupButtonIcon') + .attr('class', 'mdi mdi-account-multiple mdi-18px') + } + + Login.loginBar + .append('div') + .attr('id', 'loginoutButton') + .attr('title', Login.loggedIn ? 'Logout' : 'Login') + .style('text-align', 'center') + .style('cursor', 'pointer') + .style('width', '30px') + .style('height', '30px') + .style('line-height', '27px') + .style('padding-left', '2px') + .style('border', '1px solid var(--color-e)') + .style('background', 'var(--color-a)') + .style('pointer-events', 'all') + .on('click', function () { + if (Login.loggedIn) { + //Then Logout + if (Login.beganLoggedIn) { + Login.loggedIn = false + window.location.href = '/ssologoutredirect' + } else { + calls.api( + 'logout', + { username: Login.username }, + function (d) { + ToolController_.closeActiveTool() + mmgisglobal.user = 'guest' + mmgisglobal.groups = [] + + Login.username = null + Login.loggedIn = false + d3.select('#loginUser') + .style('opacity', 0) + .html('') + d3.select('#loginoutButton').attr( + 'title', + 'Login' + ) + d3.select('#loginoutButtonIcon').attr( + 'class', + 'mdi mdi-login mdi-18px' + ) + // Destroy the cookie session here + var decodedCookie = decodeURIComponent( + document.cookie + ) + var cookies = decodedCookie.split(';') + var MMGISUser = cookies[0].split('=') + MMGISUser = JSON.parse(MMGISUser[1]) + MMGISUser.username = '' + MMGISUser.token = '' + + if (mmgisglobal.AUTH === 'local') { + location.reload() + } + }, + function (d) {} + ) + } + } else { + //Open login + //default to login + Login.signUp = false + $('#loginEmail').css({ display: 'none' }) + $('#loginRetypePassword').css({ display: 'none' }) + $('#loginSubmit').val('Sign In') + $('#loginInUpToggle').html('or sign up') + //and open modal + $('#loginModal').modal('show') + } + }) + .append('i') + .attr('id', 'loginoutButtonIcon') + .attr( + 'class', + Login.loggedIn + ? 'mdi mdi-logout mdi-18px' + : 'mdi mdi-login mdi-18px' + ) + + $('#loginModal').modal({ + blurring: true, + }) + + //Sign in at page load from cookie if possible + if (mmgisglobal.AUTH !== 'csso') { + calls.api( + 'login', + { + useToken: true, + }, + function (d) { + Login.username = d.username + mmgisglobal.user = Login.username + mmgisglobal.groups = d.groups + loginSuccess(d) + }, + function (d) { + loginSuccess(d, true) + } + ) + } + }, + } + + function loginSuccess(data, ignoreError) { + if (data.status == 'success') { + document.cookie = + 'MMGISUser=' + + JSON.stringify({ + username: data.username, + token: data.token, + }) + + Login.loggedIn = true + $('#loginErrorMessage').animate({ opacity: '0' }, 500) + $('#loginModal') + .parent() + .animate( + { 'background-color': 'rgba(46,180,255,0.6)' }, + 1000, + function () { + ToolController_.closeActiveTool() + setTimeout(function () { + $('#loginModal').modal('hide') + $('#loginForm').trigger('reset') + $('#loginModal') + .parent() + .css({ 'background-color': 'rgba(0,0,0,0.6)' }) + $('#loginUsernameInputIcon').css({ opacity: '0' }) + $('#loginEmailInputIcon').css({ opacity: '0' }) + $('#loginPasswordInputIcon').css({ opacity: '0' }) + $('#loginRetypePasswordInputIcon').css({ + opacity: '0', + }) + validate.username = false + validate.email = false + validate.password = false + validate.retypepassword = false + $('#loginButton').html('logout') + d3.select('#loginoutButton').attr('title', 'Logout') + d3.select('#loginoutButtonIcon').attr( + 'class', + 'mdi mdi-logout mdi-18px' + ) + + d3.select('#loginUser').attr( + 'title', + Login.loggedIn ? Login.username : '' + ) + + $('#loginUser') + .css({ + opacity: 1, + background: Login.loggedIn + ? 'var(--color-i)' + : 'transparent', + }) + .html(Login.username[0]) + }, 600) + } + ) + } else { + document.cookie = + 'MMGISUser=' + + JSON.stringify({ + username: '', + token: '', + }) + + if (mmgisglobal.AUTH === 'local') { + location.reload() + } + if (ignoreError) return + + $('#loginErrorMessage') + .html(data.message) + .animate({ opacity: '1' }, 80) + } + } + + return Login +}) diff --git a/scripts/essence/Ancillary/QueryURL.js b/scripts/essence/Ancillary/QueryURL.js index 008aa705..7f02ddd1 100644 --- a/scripts/essence/Ancillary/QueryURL.js +++ b/scripts/essence/Ancillary/QueryURL.js @@ -1,387 +1,390 @@ -define(['Layers_', 'ToolController_'], function(L_, T_) { - var QueryURL = { - checkIfMission: function() { - return this.getSingleQueryVariable('mission') - }, - queryURL: function() { - //Set the site and view if specified in the url - var urlSite = this.getSingleQueryVariable('site') - var urlMapLat = this.getSingleQueryVariable('mapLat') - var urlMapLon = this.getSingleQueryVariable('mapLon') - var urlMapZoom = this.getSingleQueryVariable('mapZoom') - var urlGlobeLat = this.getSingleQueryVariable('globeLat') - var urlGlobeLon = this.getSingleQueryVariable('globeLon') - var urlGlobeZoom = this.getSingleQueryVariable('globeZoom') - var urlGlobeCamera = this.getSingleQueryVariable('globeCamera') - var urlPanePercents = this.getSingleQueryVariable('panePercents') - var urlToolsObj = this.getSingleQueryVariable('tools') - - var searchFile = this.getSingleQueryVariable('searchFile') - var searchStrings = this.getMultipleQueryVariable('searchstr') - var layersOn = this.getSingleQueryVariable('on') - var selected = this.getSingleQueryVariable('selected') - - var viewerImg = this.getSingleQueryVariable('viewerImg') - var viewerLoc = this.getSingleQueryVariable('viewerLoc') - - var rmcxyzoom = this.getSingleQueryVariable('rmcxyzoom') - - if (urlSite !== false) { - L_.FUTURES.site = urlSite - } - - if ( - urlMapLat !== false && - urlMapLon !== false && - urlMapZoom !== false - ) { - // lat, lon, zoom - L_.FUTURES.mapView = [ - parseFloat(urlMapLat), - parseFloat(urlMapLon), - parseInt(urlMapZoom), - ] - } - - if ( - urlGlobeLat !== false && - urlGlobeLon != false && - urlGlobeZoom != false - ) { - // lat, lon, zoom - L_.FUTURES.globeView = [ - parseFloat(urlGlobeLat), - parseFloat(urlGlobeLon), - parseInt(urlGlobeZoom), - ] - } - - if (urlGlobeCamera !== false) { - var c = urlGlobeCamera.split(',') - // posX, posY, posZ, targetX, targetY, targetZ - L_.FUTURES.globeCamera = [ - parseFloat(c[0]), - parseFloat(c[1]), - parseInt(c[2]), - parseFloat(c[3]), - parseFloat(c[4]), - parseInt(c[5]), - ] - } - - if (urlPanePercents !== false) { - // viewerPercent, mapPercent, globePercent - // sum == 100 - L_.FUTURES.panelPercents = urlPanePercents.split(',') - } - - if (urlToolsObj !== false) { - L_.FUTURES.tools = urlToolsObj.split(',') - } - - if (searchFile !== false) { - L_.searchFile = searchFile - } - - if (searchStrings !== false) { - L_.searchStrings = searchStrings - } - - if (selected !== false) { - var s = selected.split(',') - //1 and 2 could be either lat, lng or key, value - let isKeyValue = - isNaN(parseFloat(s[1])) || isNaN(parseFloat(s[2])) - if (isKeyValue) { - L_.FUTURES.activePoint = { - layerName: s[0], - key: s[1], - value: s[2], - view: s[3], - zoom: s[4], - } - } else { - L_.FUTURES.activePoint = { - layerName: s[0], - lat: parseFloat(s[1]), - lon: parseFloat(s[2]), - view: s[3], - zoom: s[4], - } - } - } - - if (viewerImg !== false) { - L_.FUTURES.viewerImg = viewerImg - } - - if (viewerLoc !== false) { - var l = viewerLoc.split(',') - for (var i = 0; i < l.length; i++) l[i] = parseFloat(l[i]) - L_.FUTURES.viewerLoc = l - } - - if (rmcxyzoom) { - let s = rmcxyzoom.split(',') - if (s.length == 5) { - calls.api( - 'spatial_published', - { - rmc: s[0] + ',' + s[1], - x: s[2], - y: s[3], - query: 'self', - }, - function(d) { - console.log(d) - }, - function(d) { - console.warn(d) - } - ) - } - } - - if (layersOn !== false || selected !== false) { - L_.FUTURES.customOn = true - // lists all the on layers - // if the url has the on parameter and a layer is not listed in that url, the layer is off - // on layers are split into $, $, ... - var onLayers = {} - //'replace' makes it so that onLayers are the only ones on, - //'add' makes it so that onLayers and union of default are on - var method = 'replace' - - if (layersOn !== false) { - L_.initialLayersOn = layersOn - var arr = layersOn.split(',') - for (var l of arr) { - let s = l.split('$') - onLayers[s[0]] = { opacity: parseFloat(s[1]) } - } - } - //Turn the selected layer on too - if (selected !== false) { - let s = selected.split(',') - onLayers[s[0]] = { opacity: 1 } - } - - //This is so that when preselecting data the layer can turn on along with all default layers - if (layersOn === false && selected !== false) method = 'add' - - return { onLayers: onLayers, method: method } - } - - return null - }, - getSingleQueryVariable: function(variable) { - var query = window.location.search.substring(1) - var vars = query.split('&') - for (var i = 0; i < vars.length; i++) { - var pair = vars[i].split('=') - if (pair[0] == variable) { - return decodeURIComponent(pair[1]) - } - } - - return false - }, - getMultipleQueryVariable: function(variable) { - var parameterList = [] - var query = window.location.search.substring(1) - var vars = query.split('&') - for (var i = 0; i < vars.length; i++) { - var pair = vars[i].split('=') - if (pair[0].toLowerCase() == variable) { - parameterList.push(decodeURIComponent(pair[1])) - } - } - - if (parameterList.length == 0) { - return false - } else { - return parameterList - } - }, - /* - mission - site - mapLon - mapLat - mapZoom - globeLon - globeLat - globeZoom - globeCamera posX,posY,posZ,tarX,tarY,tarZ - panePercents - on name$opacity, - selected name,lat,lon - viewerImg - viewerLoc - image posX,posY,w,h - photosphere az,el,fov - model posX,posY,posZ,tarX,tarY,tarZ - tools - "tools=camp$1.3.4," - */ - writeCoordinateURL: function( - mapLon, - mapLat, - mapZoom, - globeLon, - globeLat, - globeZoom - ) { - L_.Viewer_.getLocation() - - var callback - if (typeof mapLon === 'function') { - callback = mapLon - mapLon = undefined - } - - //Defaults - if (mapLon === undefined) mapLon = L_.Map_.map.getCenter().lng - if (mapLat === undefined) mapLat = L_.Map_.map.getCenter().lat - if (mapZoom === undefined) mapZoom = L_.Map_.map.getZoom() - - var globeCenter = L_.Globe_.getCenter() - if (globeLon === undefined) globeLon = globeCenter.lon - if (globeLat === undefined) globeLat = globeCenter.lat - if (globeZoom === undefined) globeZoom = L_.Globe_.zoom - - var viewerImg = L_.Viewer_.getLastImageId() - var viewerLoc = L_.Viewer_.getLocation() - - //mission - var urlAppendage = '?mission=' + L_.mission - - //site - if (L_.site) urlAppendage += '&site=' + L_.site - - //mapLon - urlAppendage += '&mapLon=' + mapLon - - //mapLat - urlAppendage += '&mapLat=' + mapLat - - //mapZoom - urlAppendage += '&mapZoom=' + mapZoom - - //globeLon - urlAppendage += '&globeLon=' + globeLon - - //globeLat - urlAppendage += '&globeLat=' + globeLat - - //globeZoom - urlAppendage += '&globeZoom=' + globeZoom - - //globeCamera - var orbit = L_.Globe_.getCameras().orbit - var cam = orbit.camera - var con = orbit.controls - - var pos = cam.position - var tar = con.target - var globeCamera = - pos.x + - ',' + - pos.y + - ',' + - pos.z + - ',' + - tar.x + - ',' + - tar.y + - ',' + - tar.z - urlAppendage += '&globeCamera=' + globeCamera - - //panePercents - var pP = L_.UserInterface_.getPanelPercents() - var panePercents = pP.viewer + ',' + pP.map + ',' + pP.globe - urlAppendage += '&panePercents=' + panePercents - - //on - var layersOnString = '' - for (var l in L_.toggledArray) { - if ( - L_.toggledArray[l] && - L_.layersDataByName[l].type !== 'header' - ) - layersOnString += - l + - '$' + - parseFloat(L_.opacityArray[l]).toFixed(2) + - ',' - } - layersOnString = layersOnString.substring( - 0, - layersOnString.length - 1 - ) - if (layersOnString.length > 0) - urlAppendage += '&on=' + layersOnString - - //selected - if (L_.lastActivePoint.layerName != null) { - if (L_.toggledArray[L_.lastActivePoint.layerName]) - urlAppendage += - '&selected=' + - L_.lastActivePoint.layerName + - ',' + - L_.lastActivePoint.lat + - ',' + - L_.lastActivePoint.lon - } - - //viewer - if (viewerImg !== false) urlAppendage += '&viewerImg=' + viewerImg - if (viewerImg !== false && viewerLoc !== false) - urlAppendage += '&viewerLoc=' + viewerLoc - - //tools - var urlTools = T_.getToolsUrl() - if (urlTools !== false) urlAppendage += '&tools=' + urlTools - - var url = urlAppendage - - calls.api( - 'shortener_shorten', - { - url: url, - }, - function(s) { - //Set and update the short url - L_.url = - window.location.href.split('?')[0] + '?s=' + s.body.url - window.history.replaceState('', '', L_.url) - if (typeof callback === 'function') callback() - }, - function(e) { - //Set and update the full url - L_.url = window.location.href.split('?')[0] + url - window.history.replaceState('', '', L_.url) - if (typeof callback === 'function') callback() - } - ) - }, - writeSearchURL: function(searchStrs, searchFile) { - return //!!!!!!!!!!!!!!!! - var url = - window.location.href.split('?')[0] + - '?mission=' + - L_.mission + - '&site=' + - L_.site - for (var i = 0; i < searchStrs.length; i++) { - url = url + '&searchStr=' + searchStrs[i] - } - url = url + '&searchFile=' + searchFile - - window.history.replaceState('', '', url) - }, - } - - return QueryURL -}) +define(['Layers_', 'ToolController_'], function (L_, T_) { + var QueryURL = { + checkIfMission: function () { + return this.getSingleQueryVariable('mission') + }, + queryURL: function () { + //Set the site and view if specified in the url + var urlSite = this.getSingleQueryVariable('site') + var urlMapLat = this.getSingleQueryVariable('mapLat') + var urlMapLon = this.getSingleQueryVariable('mapLon') + var urlMapZoom = this.getSingleQueryVariable('mapZoom') + var urlGlobeLat = this.getSingleQueryVariable('globeLat') + var urlGlobeLon = this.getSingleQueryVariable('globeLon') + var urlGlobeZoom = this.getSingleQueryVariable('globeZoom') + var urlGlobeCamera = this.getSingleQueryVariable('globeCamera') + var urlPanePercents = this.getSingleQueryVariable('panePercents') + var urlToolsObj = this.getSingleQueryVariable('tools') + + var searchFile = this.getSingleQueryVariable('searchFile') + var searchStrings = this.getMultipleQueryVariable('searchstr') + var layersOn = this.getSingleQueryVariable('on') + var selected = this.getSingleQueryVariable('selected') + + var viewerImg = this.getSingleQueryVariable('viewerImg') + var viewerLoc = this.getSingleQueryVariable('viewerLoc') + + var rmcxyzoom = this.getSingleQueryVariable('rmcxyzoom') + + if (urlSite !== false) { + L_.FUTURES.site = urlSite + } + + if ( + urlMapLat !== false && + urlMapLon !== false && + urlMapZoom !== false + ) { + // lat, lon, zoom + L_.FUTURES.mapView = [ + parseFloat(urlMapLat), + parseFloat(urlMapLon), + parseInt(urlMapZoom), + ] + } + + if ( + urlGlobeLat !== false && + urlGlobeLon != false && + urlGlobeZoom != false + ) { + // lat, lon, zoom + L_.FUTURES.globeView = [ + parseFloat(urlGlobeLat), + parseFloat(urlGlobeLon), + parseInt(urlGlobeZoom), + ] + } + + if (urlGlobeCamera !== false) { + var c = urlGlobeCamera.split(',') + // posX, posY, posZ, targetX, targetY, targetZ + L_.FUTURES.globeCamera = [ + parseFloat(c[0]), + parseFloat(c[1]), + parseInt(c[2]), + parseFloat(c[3]), + parseFloat(c[4]), + parseInt(c[5]), + ] + } + + if (urlPanePercents !== false) { + // viewerPercent, mapPercent, globePercent + // sum == 100 + L_.FUTURES.panelPercents = urlPanePercents.split(',') + } + + if (urlToolsObj !== false) { + L_.FUTURES.tools = urlToolsObj.split(',') + } + + if (searchFile !== false) { + L_.searchFile = searchFile + } + + if (searchStrings !== false) { + L_.searchStrings = searchStrings + } + + if (selected !== false) { + var s = selected.split(',') + //1 and 2 could be either lat, lng or key, value + let isKeyValue = + isNaN(parseFloat(s[1])) || isNaN(parseFloat(s[2])) + if (isKeyValue) { + L_.FUTURES.activePoint = { + layerName: s[0], + key: s[1], + value: s[2], + view: s[3], + zoom: s[4], + } + } else { + L_.FUTURES.activePoint = { + layerName: s[0], + lat: parseFloat(s[1]), + lon: parseFloat(s[2]), + view: s[3], + zoom: s[4], + } + } + } + + if (viewerImg !== false) { + L_.FUTURES.viewerImg = viewerImg + } + + if (viewerLoc !== false) { + var l = viewerLoc.split(',') + for (var i = 0; i < l.length; i++) l[i] = parseFloat(l[i]) + L_.FUTURES.viewerLoc = l + } + + if (rmcxyzoom) { + let s = rmcxyzoom.split(',') + if (s.length == 5) { + calls.api( + 'spatial_published', + { + rmc: s[0] + ',' + s[1], + x: s[2], + y: s[3], + query: 'self', + }, + function (d) { + console.log(d) + }, + function (d) { + console.warn(d) + } + ) + } + } + + if (layersOn !== false || selected !== false) { + L_.FUTURES.customOn = true + // lists all the on layers + // if the url has the on parameter and a layer is not listed in that url, the layer is off + // on layers are split into $, $, ... + var onLayers = {} + //'replace' makes it so that onLayers are the only ones on, + //'add' makes it so that onLayers and union of default are on + var method = 'replace' + + if (layersOn !== false) { + L_.initialLayersOn = layersOn + var arr = layersOn.split(',') + for (var l of arr) { + let s = l.split('$') + onLayers[s[0]] = { opacity: parseFloat(s[1]) } + } + } + //Turn the selected layer on too + if (selected !== false) { + let s = selected.split(',') + onLayers[s[0]] = { opacity: 1 } + } + + //This is so that when preselecting data the layer can turn on along with all default layers + if (layersOn == false && selected != false) method = 'add' + + return { + onLayers: onLayers, + method: method, + } + } + + return null + }, + getSingleQueryVariable: function (variable) { + var query = window.location.search.substring(1) + var vars = query.split('&') + for (var i = 0; i < vars.length; i++) { + var pair = vars[i].split('=') + if (pair[0] == variable) { + return decodeURIComponent(pair[1]) + } + } + + return false + }, + getMultipleQueryVariable: function (variable) { + var parameterList = [] + var query = window.location.search.substring(1) + var vars = query.split('&') + for (var i = 0; i < vars.length; i++) { + var pair = vars[i].split('=') + if (pair[0].toLowerCase() == variable) { + parameterList.push(decodeURIComponent(pair[1])) + } + } + + if (parameterList.length == 0) { + return false + } else { + return parameterList + } + }, + /* + mission + site + mapLon + mapLat + mapZoom + globeLon + globeLat + globeZoom + globeCamera posX,posY,posZ,tarX,tarY,tarZ + panePercents + on name$opacity, + selected name,lat,lon + viewerImg + viewerLoc + image posX,posY,w,h + photosphere az,el,fov + model posX,posY,posZ,tarX,tarY,tarZ + tools + "tools=camp$1.3.4," + */ + writeCoordinateURL: function ( + mapLon, + mapLat, + mapZoom, + globeLon, + globeLat, + globeZoom + ) { + L_.Viewer_.getLocation() + + var callback + if (typeof mapLon === 'function') { + callback = mapLon + mapLon = undefined + } + + //Defaults + if (mapLon === undefined) mapLon = L_.Map_.map.getCenter().lng + if (mapLat === undefined) mapLat = L_.Map_.map.getCenter().lat + if (mapZoom === undefined) mapZoom = L_.Map_.map.getZoom() + + var globeCenter = L_.Globe_.getCenter() + if (globeLon === undefined) globeLon = globeCenter.lon + if (globeLat === undefined) globeLat = globeCenter.lat + if (globeZoom === undefined) globeZoom = L_.Globe_.zoom + + var viewerImg = L_.Viewer_.getLastImageId() + var viewerLoc = L_.Viewer_.getLocation() + + //mission + var urlAppendage = '?mission=' + L_.mission + + //site + if (L_.site) urlAppendage += '&site=' + L_.site + + //mapLon + urlAppendage += '&mapLon=' + mapLon + + //mapLat + urlAppendage += '&mapLat=' + mapLat + + //mapZoom + urlAppendage += '&mapZoom=' + mapZoom + + //globeLon + urlAppendage += '&globeLon=' + globeLon + + //globeLat + urlAppendage += '&globeLat=' + globeLat + + //globeZoom + urlAppendage += '&globeZoom=' + globeZoom + + //globeCamera + var orbit = L_.Globe_.getCameras().orbit + var cam = orbit.camera + var con = orbit.controls + + var pos = cam.position + var tar = con.target + var globeCamera = + pos.x + + ',' + + pos.y + + ',' + + pos.z + + ',' + + tar.x + + ',' + + tar.y + + ',' + + tar.z + urlAppendage += '&globeCamera=' + globeCamera + + //panePercents + var pP = L_.UserInterface_.getPanelPercents() + var panePercents = pP.viewer + ',' + pP.map + ',' + pP.globe + urlAppendage += '&panePercents=' + panePercents + + //on + var layersOnString = '' + for (var l in L_.toggledArray) { + if ( + L_.toggledArray[l] && + L_.layersDataByName[l].type !== 'header' + ) + layersOnString += + l + + '$' + + parseFloat(L_.opacityArray[l]).toFixed(2) + + ',' + } + layersOnString = layersOnString.substring( + 0, + layersOnString.length - 1 + ) + if (layersOnString.length > 0) + urlAppendage += '&on=' + layersOnString + + //selected + if (L_.lastActivePoint.layerName != null) { + if (L_.toggledArray[L_.lastActivePoint.layerName]) + urlAppendage += + '&selected=' + + L_.lastActivePoint.layerName + + ',' + + L_.lastActivePoint.lat + + ',' + + L_.lastActivePoint.lon + } + + //viewer + if (viewerImg !== false) urlAppendage += '&viewerImg=' + viewerImg + if (viewerImg !== false && viewerLoc !== false) + urlAppendage += '&viewerLoc=' + viewerLoc + + //tools + var urlTools = T_.getToolsUrl() + if (urlTools !== false) urlAppendage += '&tools=' + urlTools + + var url = urlAppendage + + calls.api( + 'shortener_shorten', + { + url: url, + }, + function (s) { + //Set and update the short url + L_.url = + window.location.href.split('?')[0] + '?s=' + s.body.url + window.history.replaceState('', '', L_.url) + if (typeof callback === 'function') callback() + }, + function (e) { + //Set and update the full url + L_.url = window.location.href.split('?')[0] + url + window.history.replaceState('', '', L_.url) + if (typeof callback === 'function') callback() + } + ) + }, + writeSearchURL: function (searchStrs, searchFile) { + return //!!!!!!!!!!!!!!!! + var url = + window.location.href.split('?')[0] + + '?mission=' + + L_.mission + + '&site=' + + L_.site + for (var i = 0; i < searchStrs.length; i++) { + url = url + '&searchStr=' + searchStrs[i] + } + url = url + '&searchFile=' + searchFile + + window.history.replaceState('', '', url) + }, + } + + return QueryURL +}) diff --git a/scripts/essence/Ancillary/Search.js b/scripts/essence/Ancillary/Search.js index bcb9b375..3d0a30c8 100644 --- a/scripts/essence/Ancillary/Search.js +++ b/scripts/essence/Ancillary/Search.js @@ -1,490 +1,538 @@ -define(['jquery', 'jqueryUI', 'd3', 'Formulae_', 'Description'], function( - $, - jqueryUI, - d3, - F_, - Description -) { - // prettier-ignore - var markup = [ - "" - ].join('\n'); - - let L_ = null - let Viewer_ = null - let Map_ = null - let Globe_ = null - - var Search = { - height: 43, - width: 700, - lname: null, - arrayToSearch: null, - MMWebGISInterface: null, - searchvars: {}, - searchFields: {}, - type: 'geojson', - lastGeodatasetLayerName: null, - init: function(classSel, l_, v_, m_, g_) { - L_ = l_ - Viewer_ = v_ - Map_ = m_ - Globe_ = g_ - - //Get search variables - this.searchvars = {} - for (let l in L_.layersNamed) { - if ( - L_.layersNamed[l].variables && - L_.layersNamed[l].variables.search - ) - this.searchvars[l] = L_.layersNamed[l].variables.search - } - - // Nothing configured so don't even render it - if (Object.keys(this.searchvars).length == 0) return - - this.searchFields = makeSearchFields(this.searchvars) - if ( - L_.searchStrings != null && - L_.searchStrings.length > 0 && - L_.searchFile != null - ) { - searchWithURLParams() - } - this.MMWebGISInterface = new interfaceWithMMWebGIS(classSel) - }, - } - - function interfaceWithMMWebGIS(classSel) { - this.separateFromMMWebGIS = function() { - separateFromMMWebGIS() - } - - Search.lname = null - Search.arrayToSearch = [] - - var cont = d3.select(classSel) - if (cont == null) return - cont.selectAll('*').remove() - cont.html(markup) - - var first = true - for (l in Search.searchFields) { - if ( - L_.layersNamed[l] && - (L_.layersNamed[l].type == 'vector' || - L_.layersNamed[l].type == 'vectortile') - ) { - d3.select('#SearchType') - .append('option') - .attr('value', l) - .html(l) - if (first) { - changeSearchField(l) - first = false - } - } - } - $('#SearchType').dropdown({ - onChange: function(val) { - changeSearchField(val) - }, - }) - - d3.select('#SearchGo').on('click', searchGo) - d3.select('#SearchSelect').on('click', searchSelect) - d3.select('#SearchClear').on('click', function() { - $('#auto_search').val('') - }) - d3.select('#SearchBoth').on('click', searchBoth) - - function separateFromMMWebGIS() { - d3.select('#SearchGo').on('click', null) - d3.select('#SearchSelect').on('click', null) - d3.select('#SearchBoth').on('click', null) - if ($('#auto_search').hasClass('ui-autocomplete-input')) { - $('#auto_search').autocomplete('destroy') - } - } - } - - function initializeSearch() { - $(function() { - $('#auto_search').autocomplete({ - source: function(request, response) { - var re = $.ui.autocomplete.escapeRegex(request.term) - var matcher = new RegExp('\\b' + re, 'i') - var a = $.grep(Search.arrayToSearch, function(item, index) { - return matcher.test(item) - }) - response(a) - }, - select: function(event, ui) { - searchBoth(ui.item.value) - }, - }) - $('.ui-autocomplete') - .css({ - 'max-height': '60vh', - 'overflow-y': 'auto', - 'overflow-x': 'hidden', - border: '1px solid var(--color-mmgis)', - 'border-top': 'none', - 'background-color': 'var(--color-a)', - }) - .addClass('mmgisScrollbar') - }) - } - - function changeSearchField(val) { - if (Map_ != null) { - Search.lname = val - - let urlSplit = L_.layersNamed[Search.lname].url.split(':') - - if (urlSplit[0] == 'geodatasets' && urlSplit[1] != null) { - Search.type = 'geodatasets' - Search.lastGeodatasetLayerName = urlSplit[1] - $('#SearchSelect').css({ display: 'none' }) - $('#SearchBoth').css({ display: 'none' }) - } else { - Search.type = 'geojson' - $('#SearchSelect').css({ display: 'inherit' }) - $('#SearchBoth').css({ display: 'inherit' }) - - var searchFile = L_.layersNamed[Search.lname].url - - $.getJSON(L_.missionPath + searchFile, function(data) { - Search.arrayToSearch = [] - var props - for (var i = 0; i < data.features.length; i++) { - props = data.features[i].properties - Search.arrayToSearch.push( - getSearchFieldStringForFeature(Search.lname, props) - ) - } - if (Search.arrayToSearch[0]) { - if (!isNaN(Search.arrayToSearch[0])) - Search.arrayToSearch.sort(function(a, b) { - return a - b - }) - else Search.arrayToSearch.sort() - } - if (document.getElementById('auto_search') != null) { - document.getElementById( - 'auto_search' - ).placeholder = getSearchFieldKeys(Search.lname) - } - }) - } - initializeSearch() - } - } - - function searchGo() { - switch (Search.type) { - case 'geodatasets': - searchGeodatasets() - break - default: - doWithSearch('goto', 'false', 'false', false) - } - } - function searchSelect() { - doWithSearch('select', 'false', 'false', false) - } - function searchBoth(value) { - doWithSearch('both', 'false', 'false', false, value) - } - - function searchGeodatasets() { - let value = document.getElementById('auto_search').value - let key = - Search.searchFields[Search.lname] && - Search.searchFields[Search.lname][0] - ? Search.searchFields[Search.lname][0][1] - : null - if (key == null) return - - calls.api( - 'geodatasets_search', - { - layer: Search.lastGeodatasetLayerName, - key: key, - value: value, - }, - function(d) { - var r = d.body[0] - Map_.map.setView( - [r.coordinates[1], r.coordinates[0]], - Map_.map.getZoom() - ) - setTimeout(function() { - var vts = L_.layersGroup[Search.lname]._vectorTiles - for (var i in vts) { - for (var j in vts[i]._features) { - var feature = vts[i]._features[j].feature - if (feature.properties[key] == value) { - L_.layersGroup[ - Search.lname - ]._events.click[0].fn({ layer: feature }) - break - } - } - } - }, 2000) - }, - function(d) {} - ) - } - - //doX is either "goto", "select" or "both" - //forceX overrides searchbar entry, "false" for default - //forceSTS overrides dropdown, "false" for default - //function doWithSearch( doX, forceX, forceSTS ) { - function doWithSearch(doX, forceX, forceSTS, isURLSearch, value) { - var x - var sTS - - if (forceX == 'false' && !isURLSearch) { - x = - value != null - ? [value] - : [document.getElementById('auto_search').value] //what the user entered in search field - } else if (forceX == 'false' && isURLSearch) { - x = L_.searchStrings - } else x = forceX - - if (forceSTS == 'false') sTS = Search.lname - else sTS = forceSTS - - var markers = L_.layersGroup[Search.lname] - var selectLayers = [] - var gotoLayers = [] - var targetsID - - // Turn the layer on if it's off - if (!L_.toggledArray[Search.lname]) - L_.toggleLayer(L_.layersNamed[Search.lname]) - - if (doX == 'both' || doX == 'select') { - L_.resetLayerFills() - } - if (markers != undefined) { - markers.eachLayer(function(layer) { - var props = layer.feature.properties - var clickI = 0 - var shouldSearch = false - var comparer = getSearchFieldStringForFeature( - Search.lname, - props - ) - - for (var i = 0; i < x.length; i++) { - if ( - x.length == 1 - ? x[i].toLowerCase() == comparer.toLowerCase() - : x[i] - .toLowerCase() - .indexOf(comparer.toLowerCase()) > -1 || - comparer - .toLowerCase() - .indexOf(x[i].toLowerCase()) > -1 - ) { - shouldSearch = true - break - } - } - - if (shouldSearch) { - if (doX == 'both' || doX == 'select') { - selectLayers.push(layer) - } - if (doX == 'both' || doX == 'goto') { - gotoLayers.push(layer) - } - } - }) - - if (selectLayers.length == 1) { - selectLayers[0].fireEvent('click') - } else if (selectLayers.length > 1) { - for (var i = 0; i < selectLayers.length; i++) { - selectLayers[i].setStyle({ fillColor: 'red' }) - selectLayers[i].bringToFront() - } - } - - if (gotoLayers.length > 0) { - var coordinate = getMapZoomCoordinate(gotoLayers) - Map_.map.setView( - [coordinate.latitude, coordinate.longitude], - coordinate.zoomLevel - ) - } - } - } - - //Probably better to use a grammar - function makeSearchFields(vars) { - searchfields = {} - for (layerfield in vars) { - var fieldString = vars[layerfield] - fieldString = fieldString.split(')') - for (var i = 0; i < fieldString.length; i++) { - fieldString[i] = fieldString[i].split('(') - var li = fieldString[i][0].lastIndexOf(' ') - if (li != -1) { - fieldString[i][0] = fieldString[i][0].substring(li + 1) - } - } - fieldString.pop() - //0 is function, 1 is parameter - searchfields[layerfield] = fieldString - } - return searchfields - } - - function getSearchFieldStringForFeature(name, props) { - var str = '' - if (Search.searchFields.hasOwnProperty(name)) { - var sf = Search.searchFields[name] //sf for search field - for (var i = 0; i < sf.length; i++) { - switch (sf[i][0].toLowerCase()) { - case '': //no function - str += props[sf[i][1]] - break - case 'round': - str += Math.round(props[sf[i][1]]) - break - case 'rmunder': - str += props[sf[i][1]].replace('_', ' ') - break - } - if (i != sf.length - 1) str += ' ' - } - } - return str - } - - function getSearchFieldKeys(name) { - var str = '' - if (Search.searchFields.hasOwnProperty(name)) { - var sf = Search.searchFields[name] //sf for search field - for (var i = 0; i < sf.length; i++) { - str += sf[i][1] - str += ' ' - } - } - return str.substring(0, str.length - 1) - } - - function searchWithURLParams() { - changeSearchField(L_.searchFile) - doWithSearch('both', 'false', 'false', true) - } - - function getMapZoomCoordinate(layers) { - //The zoom levels are defined at http://wiki.openstreetmap.org/wiki/Zoom_levels - var zoomLevels = [ - 360, - 180, - 90, - 45, - 22.5, - 11.25, - 5.625, - 2.813, - 1.406, - 0.703, - 0.352, - 0.176, - 0.088, - 0.044, - 0.022, - 0.011, - 0.005, - 0.003, - 0.001, - 0.0005, - 0.0003, - 0.0001, - ] - var boundingBoxNorth = 90 - var boundingBoxSouth = -90 - var boundingBoxEast = 180 - var boundingBoxWest = -180 - var latitudeValidRange = [-90, 90] - var longitudeValidRange = [-180, 180] - - for (var i = 0; i < layers.length; i++) { - var latitude = layers[i].feature.geometry.coordinates[1] - var longitude = layers[i].feature.geometry.coordinates[0] - - //make sure latitude and longitude are in [-90, 90] and [-180, 180] - if ( - latitude < latitudeValidRange[0] || - latitude > latitudeValidRange[1] || - longitude < longitudeValidRange[0] || - longitude > longitudeValidRange[1] - ) { - continue - } - - if (latitude <= boundingBoxNorth) { - boundingBoxNorth = latitude - } - if (latitude >= boundingBoxSouth) { - boundingBoxSouth = latitude - } - if (longitude <= boundingBoxEast) { - boundingBoxEast = longitude - } - if (longitude >= boundingBoxWest) { - boundingBoxWest = longitude - } - } - - var latitudeDiff = Math.abs(boundingBoxNorth - boundingBoxSouth) - var longitudeDiff = Math.abs(boundingBoxEast - boundingBoxWest) - if (latitudeDiff == 0 && longitudeDiff == 0) { - return { - latitude: boundingBoxNorth, - longitude: boundingBoxEast, - zoomLevel: 21, - } - } else { - var maxDiff = - latitudeDiff >= longitudeDiff ? latitudeDiff : longitudeDiff - for (var i = 0; i < zoomLevels.length; i++) { - if (maxDiff < zoomLevels[i] && i != zoomLevels.length - 1) { - continue - } - - return { - latitude: - boundingBoxSouth + - (boundingBoxNorth - boundingBoxSouth) / 2, - longitude: - boundingBoxWest + - (boundingBoxEast - boundingBoxWest) / 2, - zoomLevel: i, - } - } - } - } - - return Search -}) +define(['jquery', 'jqueryUI', 'd3', 'Formulae_', 'Description'], function ( + $, + jqueryUI, + d3, + F_, + Description +) { + // prettier-ignore + var markup = [ + "" + ].join('\n'); + + let L_ = null + let Viewer_ = null + let Map_ = null + let Globe_ = null + + var Search = { + height: 43, + width: 700, + lname: null, + arrayToSearch: null, + MMWebGISInterface: null, + searchvars: {}, + searchFields: {}, + type: 'geojson', + lastGeodatasetLayerName: null, + init: function (classSel, l_, v_, m_, g_) { + L_ = l_ + Viewer_ = v_ + Map_ = m_ + Globe_ = g_ + + //Get search variables + this.searchvars = {} + for (let l in L_.layersNamed) { + if ( + L_.layersNamed[l].variables && + L_.layersNamed[l].variables.search + ) + this.searchvars[l] = L_.layersNamed[l].variables.search + } + + // Nothing configured so don't even render it + if (Object.keys(this.searchvars).length == 0) return + + this.searchFields = makeSearchFields(this.searchvars) + if ( + L_.searchStrings != null && + L_.searchStrings.length > 0 && + L_.searchFile != null + ) { + searchWithURLParams() + } + this.MMWebGISInterface = new interfaceWithMMWebGIS(classSel) + }, + } + + function interfaceWithMMWebGIS(classSel) { + this.separateFromMMWebGIS = function () { + separateFromMMWebGIS() + } + + Search.lname = null + Search.arrayToSearch = [] + + var cont = d3.select(classSel) + if (cont == null) return + cont.selectAll('*').remove() + cont.html(markup) + + var first = true + for (l in Search.searchFields) { + if ( + L_.layersNamed[l] && + (L_.layersNamed[l].type == 'vector' || + L_.layersNamed[l].type == 'vectortile') + ) { + d3.select('#SearchType') + .append('option') + .attr('value', l) + .html(l) + if (first) { + changeSearchField(l) + first = false + } + } + } + $('#SearchType').dropdown({ + onChange: function (val) { + changeSearchField(val) + }, + }) + + d3.select('#SearchGo').on('click', searchGo) + d3.select('#SearchSelect').on('click', searchSelect) + d3.select('#SearchClear').on('click', function () { + $('#auto_search').val('') + }) + d3.select('#SearchBoth').on('click', searchBoth) + + function separateFromMMWebGIS() { + d3.select('#SearchGo').on('click', null) + d3.select('#SearchSelect').on('click', null) + d3.select('#SearchBoth').on('click', null) + if ($('#auto_search').hasClass('ui-autocomplete-input')) { + $('#auto_search').autocomplete('destroy') + } + } + } + + function initializeSearch() { + $(function () { + $('#auto_search').autocomplete({ + source: function (request, response) { + var re = $.ui.autocomplete.escapeRegex(request.term) + var matcher = new RegExp('\\b' + re, 'i') + var a = $.grep(Search.arrayToSearch, function ( + item, + index + ) { + return matcher.test(item) + }) + response(a) + }, + select: function (event, ui) { + searchBoth(ui.item.value) + }, + }) + $('#auto_search').on('keydown', function (e) { + if (e.keyCode == 13) searchBoth() + }) + + $('.ui-autocomplete') + .css({ + 'max-height': '60vh', + 'overflow-y': 'auto', + 'overflow-x': 'hidden', + border: '1px solid var(--color-mmgis)', + 'border-top': 'none', + 'background-color': 'var(--color-a)', + }) + .addClass('mmgisScrollbar') + }) + } + + function changeSearchField(val) { + if (Map_ != null) { + Search.lname = val + + let urlSplit = L_.layersNamed[Search.lname].url.split(':') + + Search.layerType = L_.layersNamed[Search.lname].type + if (urlSplit[0] == 'geodatasets' && urlSplit[1] != null) { + Search.type = 'geodatasets' + Search.lastGeodatasetLayerName = urlSplit[1] + $('#SearchSelect').css({ display: 'inherit' }) + $('#SearchBoth').css({ display: 'inherit' }) + if (document.getElementById('auto_search') != null) { + document.getElementById( + 'auto_search' + ).placeholder = getSearchFieldKeys(Search.lname) + } + } else { + Search.type = 'geojson' + $('#SearchSelect').css({ display: 'inherit' }) + $('#SearchBoth').css({ display: 'inherit' }) + + var searchFile = L_.layersNamed[Search.lname].url + + $.getJSON(L_.missionPath + searchFile, function (data) { + Search.arrayToSearch = [] + var props + for (var i = 0; i < data.features.length; i++) { + props = data.features[i].properties + Search.arrayToSearch.push( + getSearchFieldStringForFeature(Search.lname, props) + ) + } + if (Search.arrayToSearch[0]) { + if (!isNaN(Search.arrayToSearch[0])) + Search.arrayToSearch.sort(function (a, b) { + return a - b + }) + else Search.arrayToSearch.sort() + } + if (document.getElementById('auto_search') != null) { + document.getElementById( + 'auto_search' + ).placeholder = getSearchFieldKeys(Search.lname) + } + }) + } + + initializeSearch() + } + } + + function searchGo() { + switch (Search.type) { + case 'geodatasets': + searchGeodatasets() + break + default: + doWithSearch('goto', 'false', 'false', false) + } + } + function searchSelect() { + doWithSearch('select', 'false', 'false', false) + } + function searchBoth(value) { + switch (Search.layerType) { + case 'vectortile': + searchGeodatasets() + break + default: + doWithSearch('both', 'false', 'false', false, value) + } + } + + function searchGeodatasets() { + let value = document.getElementById('auto_search').value + + let key = + Search.searchFields[Search.lname] && + Search.searchFields[Search.lname][0] + ? Search.searchFields[Search.lname][0][1] + : null + if (key == null) return + + let wasOff = false + // Turn the layer on if it's off + + calls.api( + 'geodatasets_search', + { + layer: Search.lastGeodatasetLayerName, + key: key, + value: value, + }, + function (d) { + var r = d.body[0] + + let selectTimeout = setTimeout(() => { + L_.layersGroup[Search.lname].off('load') + selectFeature() + }, 1500) + + L_.layersGroup[Search.lname].on('load', function (event) { + L_.layersGroup[Search.lname].off('load') + clearTimeout(selectTimeout) + selectFeature() + }) + Map_.map.setView( + [r.coordinates[1], r.coordinates[0]], + Map_.mapScaleZoom || Map_.map.getZoom() + ) + if (!L_.toggledArray[Search.lname]) { + wasOff = true + L_.toggleLayer(L_.layersNamed[Search.lname]) + } + + function selectFeature() { + var vts = L_.layersGroup[Search.lname]._vectorTiles + for (var i in vts) { + for (var j in vts[i]._features) { + var feature = vts[i]._features[j].feature + if (feature.properties[key] == value) { + feature._layerName = vts[i].options.layerName + feature._layer = feature + L_.layersGroup[ + Search.lname + ]._events.click[0].fn({ + layer: feature, + sourceTarget: feature, + }) + return + } + } + } + } + }, + function (d) {} + ) + } + + //doX is either "goto", "select" or "both" + //forceX overrides searchbar entry, "false" for default + //forceSTS overrides dropdown, "false" for default + //function doWithSearch( doX, forceX, forceSTS ) { + function doWithSearch(doX, forceX, forceSTS, isURLSearch, value) { + var x + var sTS + + if (forceX == 'false' && !isURLSearch) { + x = + value != null + ? [value] + : [document.getElementById('auto_search').value] //what the user entered in search field + } else if (forceX == 'false' && isURLSearch) { + x = L_.searchStrings + } else x = forceX + + if (forceSTS == 'false') sTS = Search.lname + else sTS = forceSTS + + var markers = L_.layersGroup[Search.lname] + var selectLayers = [] + var gotoLayers = [] + var targetsID + + // Turn the layer on if it's off + if (!L_.toggledArray[Search.lname]) + L_.toggleLayer(L_.layersNamed[Search.lname]) + + if (doX == 'both' || doX == 'select') { + L_.resetLayerFills() + } + + if (markers != undefined && typeof markers.eachLayer === 'function') { + markers.eachLayer(function (layer) { + var props = layer.feature.properties + var clickI = 0 + var shouldSearch = false + var comparer = getSearchFieldStringForFeature( + Search.lname, + props + ) + + for (var i = 0; i < x.length; i++) { + if ( + x.length == 1 + ? x[i].toLowerCase() == comparer.toLowerCase() + : x[i] + .toLowerCase() + .indexOf(comparer.toLowerCase()) > -1 || + comparer + .toLowerCase() + .indexOf(x[i].toLowerCase()) > -1 + ) { + shouldSearch = true + break + } + } + + if (shouldSearch) { + if (doX == 'both' || doX == 'select') { + selectLayers.push(layer) + } + if (doX == 'both' || doX == 'goto') { + gotoLayers.push(layer) + } + } + }) + + if (selectLayers.length == 1) { + selectLayers[0].setStyle({ fillColor: 'red' }) + selectLayers[0].fireEvent('click') + selectLayers[0].bringToFront() + } else if (selectLayers.length > 1) { + for (var i = 0; i < selectLayers.length; i++) { + selectLayers[i].setStyle({ fillColor: 'red' }) + selectLayers[i].bringToFront() + } + } + + if (gotoLayers.length > 0) { + var coordinate = getMapZoomCoordinate(gotoLayers) + Map_.map.setView( + [coordinate.latitude, coordinate.longitude], + Map_.mapScaleZoom || Map_.map.getZoom() + ) + } + } + } + + //Probably better to use a grammar + function makeSearchFields(vars) { + searchfields = {} + for (layerfield in vars) { + var fieldString = vars[layerfield] + fieldString = fieldString.split(')') + for (var i = 0; i < fieldString.length; i++) { + fieldString[i] = fieldString[i].split('(') + var li = fieldString[i][0].lastIndexOf(' ') + if (li != -1) { + fieldString[i][0] = fieldString[i][0].substring(li + 1) + } + } + fieldString.pop() + //0 is function, 1 is parameter + searchfields[layerfield] = fieldString + } + return searchfields + } + + function getSearchFieldStringForFeature(name, props) { + var str = '' + if (Search.searchFields.hasOwnProperty(name)) { + var sf = Search.searchFields[name] //sf for search field + for (var i = 0; i < sf.length; i++) { + switch (sf[i][0].toLowerCase()) { + case '': //no function + str += props[sf[i][1]] + break + case 'round': + str += Math.round(props[sf[i][1]]) + break + case 'rmunder': + str += props[sf[i][1]].replace('_', ' ') + break + } + if (i != sf.length - 1) str += ' ' + } + } + return str + } + + function getSearchFieldKeys(name) { + var str = '' + if (Search.searchFields.hasOwnProperty(name)) { + var sf = Search.searchFields[name] //sf for search field + for (var i = 0; i < sf.length; i++) { + str += sf[i][1] + str += ' ' + } + } + return str.substring(0, str.length - 1) + } + + function searchWithURLParams() { + changeSearchField(L_.searchFile) + doWithSearch('both', 'false', 'false', true) + } + + function getMapZoomCoordinate(layers) { + //The zoom levels are defined at http://wiki.openstreetmap.org/wiki/Zoom_levels + var zoomLevels = [ + 360, + 180, + 90, + 45, + 22.5, + 11.25, + 5.625, + 2.813, + 1.406, + 0.703, + 0.352, + 0.176, + 0.088, + 0.044, + 0.022, + 0.011, + 0.005, + 0.003, + 0.001, + 0.0005, + 0.0003, + 0.0001, + ] + var boundingBoxNorth = 90 + var boundingBoxSouth = -90 + var boundingBoxEast = 180 + var boundingBoxWest = -180 + var latitudeValidRange = [-90, 90] + var longitudeValidRange = [-180, 180] + + for (var i = 0; i < layers.length; i++) { + var latitude = layers[i].feature.geometry.coordinates[1] + var longitude = layers[i].feature.geometry.coordinates[0] + + //make sure latitude and longitude are in [-90, 90] and [-180, 180] + if ( + latitude < latitudeValidRange[0] || + latitude > latitudeValidRange[1] || + longitude < longitudeValidRange[0] || + longitude > longitudeValidRange[1] + ) { + continue + } + + if (latitude <= boundingBoxNorth) { + boundingBoxNorth = latitude + } + if (latitude >= boundingBoxSouth) { + boundingBoxSouth = latitude + } + if (longitude <= boundingBoxEast) { + boundingBoxEast = longitude + } + if (longitude >= boundingBoxWest) { + boundingBoxWest = longitude + } + } + + var latitudeDiff = Math.abs(boundingBoxNorth - boundingBoxSouth) + var longitudeDiff = Math.abs(boundingBoxEast - boundingBoxWest) + if (latitudeDiff == 0 && longitudeDiff == 0) { + return { + latitude: boundingBoxNorth, + longitude: boundingBoxEast, + zoomLevel: 21, + } + } else { + var maxDiff = + latitudeDiff >= longitudeDiff ? latitudeDiff : longitudeDiff + for (var i = 0; i < zoomLevels.length; i++) { + if (maxDiff < zoomLevels[i] && i != zoomLevels.length - 1) { + continue + } + + return { + latitude: + boundingBoxSouth + + (boundingBoxNorth - boundingBoxSouth) / 2, + longitude: + boundingBoxWest + + (boundingBoxEast - boundingBoxWest) / 2, + zoomLevel: i, + } + } + } + } + + return Search +}) diff --git a/scripts/essence/Basics/Formulae_/Formulae_.js b/scripts/essence/Basics/Formulae_/Formulae_.js index 753d0010..2945af74 100644 --- a/scripts/essence/Basics/Formulae_/Formulae_.js +++ b/scripts/essence/Basics/Formulae_/Formulae_.js @@ -1,1631 +1,1825 @@ -//Holds a bunch of reusable mathy formulas and variables -// often referred to as F_ -define(['turf', 'fileSaver'], function(turf) { - var temp = new Float32Array(1) - - Object.defineProperty(Object.prototype, 'getFirst', { - value: function() { - return this[Object.keys(this)[0]] - }, - writable: true, - configurable: true, - enumerable: false, - }) - - var Formulae_ = { - radiusOfPlanetMajor: 3396190, //(m) Defaults to Mars - radiusOfPlanetMinor: 3396190, - radiusOfEarth: 6371000, - dam: false, //degrees as meters - metersInOneDegree: null, - getBaseGeoJSON: function() { - return { - type: 'FeatureCollection', - crs: { - type: 'name', - properties: { name: 'urn:ogc:def:crs:OGC:1.3:CRS84' }, - }, - features: [], - } - }, - getExtension: function(string) { - var ex = /(?:\.([^.]+))?$/.exec(string)[1] - return ex || '' - }, - setRadius: function(which, radius) { - if (which.toLowerCase() == 'major') - this.radiusOfPlanetMajor = parseFloat(radius) - else if (which.toLowerCase() == 'minor') - this.radiusOfPlanetMinor = parseFloat(radius) - }, - useDegreesAsMeters: function(use) { - if (use === true || use === false) Formulae_.dam = use - }, - getEarthToPlanetRatio: function() { - return this.radiusOfEarth / this.radiusOfPlanetMajor - }, - linearScale: function(domain, range, value) { - return ( - ((range[1] - range[0]) * (value - domain[0])) / - (domain[1] - domain[0]) + - range[0] - ) - }, - //Uses haversine to calculate distances over arcs - lngLatDistBetween: function(lon1, lat1, lon2, lat2) { - var R = this.radiusOfPlanetMajor - var φ1 = lat1 * (Math.PI / 180) - var φ2 = lat2 * (Math.PI / 180) - var Δφ = (lat2 - lat1) * (Math.PI / 180) - var Δλ = (lon2 - lon1) * (Math.PI / 180) - - var a = - Math.sin(Δφ / 2) * Math.sin(Δφ / 2) + - Math.cos(φ1) * - Math.cos(φ2) * - Math.sin(Δλ / 2) * - Math.sin(Δλ / 2) - var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) - - return R * c - }, - metersToDegrees: function(meters) { - return (meters / this.radiusOfPlanetMajor) * (180 / Math.PI) - }, - degreesToMeters: function(degrees) { - return degrees * (Math.PI / 180) * this.radiusOfPlanetMajor - }, - //2D - distanceFormula: function(x1, y1, x2, y2) { - return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)) - }, - //2D - areaOfTriangle: function(aX, aY, bX, bY, cX, cY) { - return Math.abs( - (aX * (bY - cY) + bX * (cY - aY) + cX * (aY - bY)) / 2 - ) - }, - //from point p, finds the closest point in a series of lines - //p is point {x: X, y: y} - //pts is an array of points [[[x, y],[x, y]]...] - //return index of closest point in pts to p as [i,j] - //var testPts = [[[0, 5], [1, 2], [4, 4]], [[8, 17], [7, 14]]]; - //var clP = closestPoint({x: 0, y: 2}, testPts); - //console.log(testPts[clP[0]][clP[1]]); - closestPoint: function(p, pts) { - var closestI = 0 - var closestJ = 0 - var closestIDist = Infinity - var d - for (var i = 0; i < pts.length; i++) { - for (var j = 0; j < pts[i].length; j++) { - d = this.distanceFormula( - p.x, - p.y, - pts[i][j][0], - pts[i][j][1] - ) - if (d < closestIDist) { - closestI = i - closestJ = j - closestIDist = d - } - } - } - return [closestI, closestJ] - }, - //a mod that works with negatives. a true modulo and not remainder - mod: function(n, m) { - var remain = n % m - return Math.floor(remain >= 0 ? remain : remain + m) - }, - //2D rotate a point about another point a certain angle - //pt is {x: ,y: } center is [x,y] angle in radians - rotatePoint: function(pt, center, angle) { - var cosAngle = Math.cos(angle) - var sinAngle = Math.sin(angle) - var dx = pt.x - center[0] - var dy = pt.y - center[1] - var newPt = {} - newPt['x'] = center[0] + dx * cosAngle - dy * sinAngle - newPt['y'] = center[1] + dx * sinAngle + dy * cosAngle - - return newPt - }, - //Rotates X then Z then Y ? - //all are of form {x: , y: , z: } - //angle is in radians - //if center undefined, then 0 0 0 - rotatePoint3D: function(pt, angle, center) { - if (center == undefined) center = { x: 0, y: 0, z: 0 } - //Offset - var dx = pt.x - center.x - var dy = pt.y - center.y - var dz = pt.z - center.z - - var sx = Math.sin(angle.x) - var cx = Math.cos(angle.x) - var sy = Math.sin(angle.y) - var cy = Math.cos(angle.y) - var sz = Math.sin(angle.z) - var cz = Math.cos(angle.z) - - var x = center.x + dx * (cy * cz) + dy * (-cy * sz) + dz * sy - var y = - center.y + - dx * (cx * sz + sx * sy * cz) + - dy * (cx * cz - sx * sy * sz) + - dz * (-sx * cy) - var z = - center.z + - dx * (sx * sz - cx * sy * cz) + - dy * (sx * cz + cx * sy * sz) + - dz * (cx * cy) - - return { x: x, y: y, z: z } - }, - bearingBetweenTwoLatLngs: function(lat1, lng1, lat2, lng2) { - lat1 *= Math.PI / 180 - lng1 *= Math.PI / 180 - lat2 *= Math.PI / 180 - lng2 *= Math.PI / 180 - - var y = Math.sin(lng2 - lng1) * Math.cos(lat2) - var x = - Math.cos(lat1) * Math.sin(lat2) - - Math.sin(lat1) * Math.cos(lat2) * Math.cos(lng2 - lng1) - - return (Math.atan2(y, x) * (180 / Math.PI) + 360) % 360 - }, - inclinationBetweenTwoLatLngs: function( - lat1, - lng1, - elev1, - lat2, - lng2, - elev2 - ) { - //distance between - var x = this.lngLatDistBetween(lng1, lat1, lng2, lat2) - //y difference in Elevation - var y = elev2 - elev1 - var incline = Math.atan(y / x) * (180 / Math.PI) - return incline - }, - //closest point on line from point - //all of form {x: X, y: Y} - //p point, v and w line endpoints - //returns: - // [closest point on line to point, closest distance from point to line] - // as [{x: X, y: Y}, float] - closestToSegment: function(p, v, w) { - function dist2(v, w) { - return Math.pow(v.x - w.x, 2) + Math.pow(v.y - w.y, 2) - } - var l2 = dist2(v, w) - if (l2 == 0) return [p, Math.sqrt(dist2(p, v))] - var t = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / l2 - t = Math.max(0, Math.min(1, t)) - var ptLine = { x: v.x + t * (w.x - v.x), y: v.y + t * (w.y - v.y) } - return [ptLine, Math.sqrt(dist2(p, ptLine))] - }, - //lines of form [[[x1, y1], [x2, y2]], [[x1, y1],[x2, y2]], ... ] - //returns only point {x: X, y: Y} - closestToSegments: function(p, lines) { - var shortestDist = Infinity - var nearestPoint = { x: 0, y: 0 } - var v - var w - var cts - for (var pg = 0; pg < lines.length; pg++) { - for (var i = 0; i < lines[pg].length; i++) { - v = { x: lines[pg][i][0][0], y: lines[pg][i][0][1] } - w = { x: lines[pg][i][1][0], y: lines[pg][i][1][1] } - cts = this.closestToSegment(p, v, w) - if (cts[1] < shortestDist) { - shortestDist = cts[1] - nearestPoint = cts[0] - } - } - } - return nearestPoint - }, - rgb2hex: function(rgb) { - if (rgb.search('rgb') == -1) { - return rgb - } else { - rgb = rgb.match( - /^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+))?\)$/ - ) - function hex(x) { - return ('0' + parseInt(x).toString(16)).slice(-2) - } - return '#' + hex(rgb[1]) + hex(rgb[2]) + hex(rgb[3]) - } - }, - //From: http://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb Tim Down - hexToRGB: function(hex) { - // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") - var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i - hex = hex.replace(shorthandRegex, function(m, r, g, b) { - return r + r + g + g + b + b - }) - - var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) - return result - ? { - r: parseInt(result[1], 16), - g: parseInt(result[2], 16), - b: parseInt(result[3], 16), - } - : null - }, - rgbToArray: function(rgb) { - return rgb.match(/\d+/g) - }, - //From: http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#ECMAScript_.28JavaScript.2FActionScript.2C_etc..29 - lon2tileUnfloored: function(lon, zoom) { - return ((lon + 180) / 360) * Math.pow(2, zoom) - }, - lat2tileUnfloored: function(lat, zoom) { - return ( - ((1 - - Math.log( - Math.tan((lat * Math.PI) / 180) + - 1 / Math.cos((lat * Math.PI) / 180) - ) / - Math.PI) / - 2) * - Math.pow(2, zoom) - ) - }, - //no radius - lonLatToVector3nr: function(lon, lat, height) { - var phi = lat * (Math.PI / 180) - var theta = (lon - 180) * (Math.PI / 180) - - var x = height * Math.cos(phi) * Math.sin(theta) - var y = -height * Math.sin(phi) - var z = -height * Math.cos(phi) * Math.cos(theta) - - return { x: x, y: y, z: z } - }, - //From: https://github.com/mrdoob/three.js/issues/758 mrdoob - getImageData: function(image) { - if (image.width == 0) return - var canvas = document.createElement('canvas') - canvas.width = image.width - canvas.height = image.height - - var context = canvas.getContext('2d') - context.drawImage(image, 0, 0) - - return context.getImageData(0, 0, image.width, image.height) - }, - getPixel: function(imagedata, x, y) { - var position = (x + imagedata.width * y) * 4, - data = imagedata.data - return { - r: data[position], - g: data[position + 1], - b: data[position + 2], - a: data[position + 3], - } - }, - /** - * Traverses an object with an array of keys - * @param {*} obj - * @param {*} keyArray - */ - getIn: function(obj, keyArray) { - if (keyArray == null) return null - let object = Object.assign({}, obj) - for (let i = 0; i < keyArray.length; i++) { - if (object.hasOwnProperty(keyArray[i])) - object = object[keyArray[i]] - else return null - } - return object - }, - getKeyByValue: function(obj, value) { - return Object.keys(obj).filter(function(key) { - return obj[key] === value - })[0] - }, - getValueByKeyCaseInsensitive: function(key, obj) { - key = (key + '').toLowerCase() - for (var p in obj) { - if (obj.hasOwnProperty(p) && key == (p + '').toLowerCase()) { - return obj[p] - } - } - }, - removeDuplicatesInArrayOfObjects(arr) { - let stringedArr = arr - stringedArr.forEach((el, i) => { - stringedArr[i] = JSON.stringify(el) - }) - let uniqueArr = [] - for (let i = stringedArr.length - 1; i >= 0; i--) { - if (uniqueArr.indexOf(stringedArr[i]) == -1) - uniqueArr.push(stringedArr[i]) - } - uniqueArr.forEach((el, i) => { - uniqueArr[i] = JSON.parse(el) - }) - return uniqueArr - }, - //Get index of array of objects with key value pair (-1 if not found) - objectArrayIndexOfKeyWithValue(objectArray, key, value) { - var index = -1 - for (let i in objectArray) { - if (objectArray[i]) { - if ( - objectArray[i].hasOwnProperty(key) && - objectArray[i][key] === value - ) { - index = i - break - } - } - } - return index - }, - //Returns the line with points no greater than meters apart - subdivideLine(line, meters) { - let subdividedLine = [] - for (var i = 0; i < line.length; i++) { - subdividedLine.push([line[i][0], line[i][1]]) - if (i != line.length - 1) { - var length = Formulae_.lngLatDistBetween( - line[i][0], - line[i][1], - line[i + 1][0], - line[i + 1][1] - ) - var spacing = meters / length - for (var s = spacing; s < 1; s += spacing) { - var newPt = Formulae_.interpolatePointsPerun( - { x: line[i][0], y: line[i][1] }, - { x: line[i + 1][0], y: line[i + 1][1] }, - s - ) - subdividedLine.push([newPt.x, newPt.y]) - } - } - } - return subdividedLine - }, - //Helper to make an array or object an enumerated array - enumerate: function(obj) { - var arr = [] - var keys = Object.keys(obj) - for (var k = 0; k < keys.length; k++) { - arr[k] = obj[keys[k]] - } - return arr - }, - //Return a clone of the object to avoid pass by reference issues - clone: function(obj) { - var copy - // Handle the 3 simple types, and null or undefined - if (null == obj || 'object' != typeof obj) return obj - - // Handle Date - if (obj instanceof Date) { - copy = new Date() - copy.setTime(obj.getTime()) - return copy - } - - // Handle Array - if (obj instanceof Array) { - copy = [] - for (var i = 0, len = obj.length; i < len; i++) { - copy[i] = this.clone(obj[i]) - } - return copy - } - - // Handle Object - if (obj instanceof Object) { - copy = {} - for (var attr in obj) { - if (obj.hasOwnProperty(attr)) - copy[attr] = this.clone(obj[attr]) - } - return copy - } - throw new Error("Unable to copy obj! Its type isn't supported.") - }, - //Returns an array of ints from a to b inclusively - range: function(a, b) { - a = b - a + 1 - var c = [] - while (a--) c[a] = b-- - return c - }, - //simple and only works from 0 to 16 - numberToWords: function(n) { - switch (n) { - case 0: - return 'zero' - case 1: - return 'one' - case 2: - return 'two' - case 3: - return 'three' - case 4: - return 'four' - case 5: - return 'five' - case 6: - return 'six' - case 7: - return 'seven' - case 8: - return 'eight' - case 9: - return 'nine' - case 10: - return 'ten' - case 11: - return 'eleven' - case 12: - return 'twelve' - case 13: - return 'thirteen' - case 14: - return 'fourteen' - case 15: - return 'fifteen' - case 16: - return 'sixteen' - } - return 'zero' - }, - isUrlAbsolute: function(url) { - var r = new RegExp('^(?:[a-z]+:)?//', 'i') - return r.test(url) - }, - csvToJSON: function(csv) { - var lines = csv.split('\n') - var result = [] - var headers = lines[0].split(',') - for (var i = 1; i < lines.length; i++) { - var obj = {} - var currentline = lines[i].split(',') - for (var j = 0; j < headers.length; j++) { - obj[headers[j]] = currentline[j] - } - result.push(obj) - } - return JSON.parse(JSON.stringify(result).replace(/\\r/g, '')) - }, - latlonzoomToTileCoords: function(lat, lon, zoom) { - var xtile = parseInt(Math.floor(((lon + 180) / 360) * (1 << zoom))) - var ytile = parseInt( - Math.floor( - ((1 - - Math.log( - Math.tan(lat * (Math.PI / 180)) + - 1 / Math.cos(lat * (Math.PI / 180)) - ) / - Math.PI) / - 2) * - (1 << zoom) - ) - ) - return { - x: xtile, - y: ytile, - z: zoom, - } - }, - noNullLength: function(arr) { - let len = 0 - for (let i = 0; i < arr.length; i++) if (arr[i] != null) len++ - return len - }, - isEmpty: function(obj) { - if (obj === undefined) return true - for (var prop in obj) { - if (obj.hasOwnProperty(prop)) return false - } - return JSON.stringify(obj) === JSON.stringify({}) - }, - //Returns true if all elements of the array are the same (empty is false) - identicalElements(arr) { - if (arr.length === 0) return false - var elm = arr[0] - for (var i = 0; i < arr.length; i++) { - if (elm !== arr[i]) return false - } - return true - }, - cleanString(str) { - return str.replace(/[`~!@#$%^&*|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, '') - }, - invertGeoJSONLatLngs(geojson) { - geojson = this.clone(geojson) - var coords = geojson.geometry.coordinates - - if ( - coords.constructor === Array && - coords[0].constructor !== Array - ) { - var newCoords = Object.assign([], coords) - coords[0] = newCoords[1] - coords[1] = newCoords[0] - } else { - for (var i = 0; i < coords.length; i++) { - if (coords[i][0].constructor === Array) { - for (var j = 0; j < coords[i].length; j++) { - if (coords[i][j][0].constructor === Array) { - for (var k = 0; k < coords[i][j].length; k++) { - if ( - coords[i][j][k][0].constructor === Array - ) { - for ( - var l = 0; - l < coords[i][j][k].length; - l++ - ) { - if ( - coords[i][j][k][0] - .constructor === Array - ) { - for ( - var m = 0; - m < - coords[i][j][k][l].length; - m++ - ) { - if ( - coords[i][j][k][l][0] - .constructor === - Array - ) { - console.log( - 'Lazy depth traversal failed' - ) - } else { - var newCoords = Object.assign( - [], - coords[i][j][k][l][ - m - ] - ) - var swap = newCoords[0] - newCoords[0] = - newCoords[1] - newCoords[1] = swap - coords[i][j][k][l][ - m - ] = newCoords - } - } - } else { - var newCoords = Object.assign( - [], - coords[i][j][k][l] - ) - var swap = newCoords[0] - newCoords[0] = newCoords[1] - newCoords[1] = swap - coords[i][j][k][l] = newCoords - } - } - } else { - var newCoords = Object.assign( - [], - coords[i][j][k] - ) - var swap = newCoords[0] - newCoords[0] = newCoords[1] - newCoords[1] = swap - coords[i][j][k] = newCoords - } - } - } else { - var newCoords = Object.assign([], coords[i][j]) - var swap = newCoords[0] - newCoords[0] = newCoords[1] - newCoords[1] = swap - coords[i][j] = newCoords - } - } - } else { - var newCoords = Object.assign([], coords[i]) - var swap = newCoords[0] - newCoords[0] = newCoords[1] - newCoords[1] = swap - coords[i] = newCoords - } - } - } - - geojson.geometry.coordinates = coords - return geojson - }, - //By geometry type: polygon -> multilinestring -> point - sortGeoJSONFeatures(geojson) { - var featuresKey - if (geojson.hasOwnProperty('features')) featuresKey = 'features' - else if (geojson.hasOwnProperty('Features')) - featuresKey = 'Features' - else return - - var oldFeatures = geojson[featuresKey] - var newFeatures = [] - - var sortOrder = [ - 'multipolygon', - 'polygon', - 'multilinestring', - 'linestring', - 'multipoint', - 'point', - ] - - for (var i = 0; i < sortOrder.length; i++) { - for (var j = 0; j < oldFeatures.length; j++) { - if ( - oldFeatures[j].geometry.type - .toLowerCase() - .includes(sortOrder[i]) - ) { - newFeatures.push(oldFeatures[j]) - } - } - } - geojson.features = newFeatures - delete geojson.Features - //no return as pass by reference - }, - geoJSONFeatureMetersToDegrees(feature) { - switch (feature.geometry.type.toLowerCase()) { - case 'point': - feature.geometry.coordinates[0] = - (feature.geometry.coordinates[0] * (180 / Math.PI)) / - this.radiusOfPlanetMajor - feature.geometry.coordinates[1] = - (feature.geometry.coordinates[1] * (180 / Math.PI)) / - this.radiusOfPlanetMajor - break - case 'linestring': - for ( - var i = 0; - i < feature.geometry.coordinates.length; - i++ - ) { - feature.geometry.coordinates[i][0] = - (feature.geometry.coordinates[i][0] * - (180 / Math.PI)) / - this.radiusOfPlanetMajor - feature.geometry.coordinates[i][1] = - (feature.geometry.coordinates[i][1] * - (180 / Math.PI)) / - this.radiusOfPlanetMajor - } - break - case 'polygon': - case 'multilinestring': - for ( - var i = 0; - i < feature.geometry.coordinates.length; - i++ - ) { - for ( - var j = 0; - j < feature.geometry.coordinates[i].length; - j++ - ) { - feature.geometry.coordinates[i][j][0] = - (feature.geometry.coordinates[i][j][0] * - (180 / Math.PI)) / - this.radiusOfPlanetMajor - feature.geometry.coordinates[i][j][1] = - (feature.geometry.coordinates[i][j][1] * - (180 / Math.PI)) / - this.radiusOfPlanetMajor - } - } - break - case 'multipolygon': - for ( - var i = 0; - i < feature.geometry.coordinates.length; - i++ - ) { - for ( - var j = 0; - j < feature.geometry.coordinates[i].length; - j++ - ) { - for ( - var k = 0; - k < feature.geometry.coordinates[i][j].length; - k++ - ) { - feature.geometry.coordinates[i][j][k][0] = - (feature.geometry.coordinates[i][j][k][0] * - (180 / Math.PI)) / - this.radiusOfPlanetMajor - feature.geometry.coordinates[i][j][k][1] = - (feature.geometry.coordinates[i][j][k][1] * - (180 / Math.PI)) / - this.radiusOfPlanetMajor - } - } - } - break - } - return feature - }, - lnglatsToDemtileElevs(lnglats, demtilesets, callback) { - $.ajax({ - type: calls.lnglatsToDemtileElevs.type, - url: calls.lnglatsToDemtileElevs.url, - data: { - lnglats: JSON.stringify(lnglats), - demtilesets: JSON.stringify(demtilesets), - }, - success: function(data) { - if (typeof callback == 'function') - callback(JSON.parse(data)) - }, - }) - }, - marsEarthSurfaceAreaRatio() { - return ( - (4 * Math.PI * Math.pow(this.radiusOfPlanetMajor, 2)) / - (4 * Math.PI * Math.pow(6378137, 2)) - ) - }, - //Current only supports a single feature: {type:"feature", ...} - geojsonAddSpatialProperties(geojson) { - var g = geojson.geometry.coordinates[0] - - switch (geojson.geometry.type.toLowerCase()) { - case 'multilinestring': - //length2D - var length2D = 0 - for (var i = 1; i < g.length; i++) { - length2D += this.lngLatDistBetween( - g[i - 1][0], - g[i - 1][1], - g[i][0], - g[i][1] - ) - } - geojson.properties.length2D = length2D - break - case 'polygon': - //perimeter2D - var perimeter2D = 0 - for (var i = 1; i < g.length; i++) { - perimeter2D += this.lngLatDistBetween( - g[i - 1][0], - g[i - 1][1], - g[i][0], - g[i][1] - ) - } - - geojson.properties.perimeter2D = perimeter2D - //area2D - var area2D = this.geoJSONArea(geojson.geometry) - geojson.properties.area2D = area2D - break - } - return geojson - }, - /** - * Function to sort alphabetically an array of objects by some specific key. - * - * @param {String} property Key of the object to sort. - */ - dynamicSort(property) { - var sortOrder = 1 - - if (property[0] === '-') { - sortOrder = -1 - property = property.substr(1) - } - - return function(a, b) { - if (sortOrder == -1) { - return b[property].localeCompare(a[property]) - } else { - return a[property].localeCompare(b[property]) - } - } - }, - //colors are evenly spaced rgb: [ [r,g,b], [r,g,b], [r,g,b] ] - //percent is 0 to 1 - getColorFromRangeByPercent(colors, percent, asRGBString) { - if (percent > 1 || percent < 0 || colors.length < 2) - return colors[0] - else if (percent == 1) { - var c = colors[colors.length - 1] - if (asRGBString) - return 'rgb(' + c[0] + ',' + c[1] + ',' + c[2] + ')' - return c - } - - //Find the two colors the percent will fall into - var startIndex = parseInt((colors.length - 1) * percent) - if (startIndex > colors.length - 1) startIndex = 0 - var color1 = colors[startIndex] - var color2 = colors[startIndex + 1] - var min = startIndex / (colors.length - 1) - var max = (startIndex + 1) / (colors.length - 1) - var ratio = (percent - min) / (max - min) - - var r = Math.ceil(color2[0] * ratio + color1[0] * (1 - ratio)) - var g = Math.ceil(color2[1] * ratio + color1[1] * (1 - ratio)) - var b = Math.ceil(color2[2] * ratio + color1[2] * (1 - ratio)) - - if (asRGBString) return 'rgb(' + r + ',' + g + ',' + b + ')' - return [r, g, b] - }, - interpolatePointsPerun(p1, p2, p) { - return { - x: p1.x + p * (p2.x - p1.x), - y: p1.y + p * (p2.y - p1.y), - z: p1.z && p2.z ? p1.z + p * (p2.z - p1.z) : 0, - } - }, - //https://github.com/mapbox/geojson-area/blob/master/index.js - geoJSONArea(g) { - return geometry(g) - - function geometry(_) { - var area = 0, - i - switch (_.type) { - case 'Polygon': - return polygonArea(_.coordinates) - case 'MultiPolygon': - for (i = 0; i < _.coordinates.length; i++) { - area += polygonArea(_.coordinates[i]) - } - return area - case 'Point': - case 'MultiPoint': - case 'LineString': - case 'MultiLineString': - return 0 - case 'GeometryCollection': - for (i = 0; i < _.geometries.length; i++) { - area += geometry(_.geometries[i]) - } - return area - } - } - function polygonArea(coords) { - var area = 0 - if (coords && coords.length > 0) { - area += Math.abs(ringArea(coords[0])) - for (var i = 1; i < coords.length; i++) { - area -= Math.abs(ringArea(coords[i])) - } - } - return area - } - function ringArea(coords) { - coords = Object.assign([], coords) - for (var c = 0; c < coords.length; c++) { - coords[c] = [coords[c][0], coords[c][1]] - } - - var p1, - p2, - p3, - lowerIndex, - middleIndex, - upperIndex, - i, - area = 0, - coordsLength = coords.length - if (coordsLength > 2) { - for (i = 0; i < coordsLength; i++) { - if (i === coordsLength - 2) { - // i = N-2 - lowerIndex = coordsLength - 2 - middleIndex = coordsLength - 1 - upperIndex = 0 - } else if (i === coordsLength - 1) { - // i = N-1 - lowerIndex = coordsLength - 1 - middleIndex = 0 - upperIndex = 1 - } else { - // i = 0 to N-3 - lowerIndex = i - middleIndex = i + 1 - upperIndex = i + 2 - } - p1 = coords[lowerIndex] - p2 = coords[middleIndex] - p3 = coords[upperIndex] - area += (rad(p3[0]) - rad(p1[0])) * Math.sin(rad(p2[1])) - } - area = - (area * Math.pow(Formulae_.radiusOfPlanetMajor, 2)) / 2 - } - - return area - } - - function rad(_) { - return (_ * Math.PI) / 180 - } - }, - calcPolygonArea(vertices) { - var total = 0 - - for (var i = 0, l = vertices.length; i < l; i++) { - var addX = vertices[i][0] - var addY = vertices[i == vertices.length - 1 ? 0 : i + 1][1] - var subX = vertices[i == vertices.length - 1 ? 0 : i + 1][0] - var subY = vertices[i][1] - - total += addX * addY * 0.5 - total -= subX * subY * 0.5 - } - - return Math.abs(total) - }, - //if array is an array of objects, - // the optional key can be set to say which key to average - arrayAverage(array, key) { - var total = 0 - for (var i = 0; i < array.length; i++) { - if (key != null) total += array[i][key] - else total += array[i] - } - return total / array.length - }, - doubleToTwoFloats(double) { - if (double >= 0) { - var high = Math.floor(double / 65536) * 65536 - return [this.f32round(high), this.f32round(double - high)] - } else { - var high = Math.floor(-double / 65536) * 65536 - return [this.f32round(-high), this.f32round(double + high)] - } - }, - f32round(x) { - temp[0] = +x - return temp[0] - }, - toEllipsisString(str, length) { - return str.length > length ? str.substr(0, length - 3) + '...' : str - }, - GeoJSONStringify(geojson) { - var featuresKey - if (geojson.hasOwnProperty('features')) featuresKey = 'features' - else if (geojson.hasOwnProperty('Features')) - featuresKey = 'Features' - else return - - var savedFeatures = Object.assign([], geojson[featuresKey]) - delete geojson[featuresKey] - var string = JSON.stringify(geojson) - var featuresString = '' - for (var i = 0; i < savedFeatures.length; i++) { - if (i != 0) { - featuresString += '\n,' - savedFeatures[i].properties['boundingbox'] = turf.bbox( - savedFeatures[i] - ) - } - featuresString += JSON.stringify(savedFeatures[i]) - } - string = string.substring(0, string.length - 1) - string += ',"features":[' + featuresString - string += ']}' - return string - }, - /** - * Given an xyz and z, gets all tiles on zoom level z that are contained in xyz - * @param {[x,y,z]} xyz - the tile to get the contents of - * @param {number} z - the zoom level of tiles to get - * @param {boolean} useLast - use lastTileContains - * return arrays of [x,y,z]s contained - */ - //For use with tileContains. Stores last three calls and results to speed up performance - lastTileContains: [], - tileContains(xyz, z, useLast) { - if (useLast) { - for (var i = 0; i < this.lastTileContains.length; i++) { - var lastxyz = this.lastTileContains[i].call.xyz - if ( - lastxyz[0] == xyz[0] && - lastxyz[1] == xyz[1] && - lastxyz[2] == xyz[2] && - this.lastTileContains[i].call.z == z - ) { - return this.lastTileContains[i].result - } - } - } - var contained = [] - const zoomRatio = Math.pow(2, z) / Math.pow(2, xyz[2]) - const max = [ - (xyz[0] + 1) * zoomRatio - 1, - (xyz[1] + 1) * zoomRatio - 1, - ] - const min = [max[0] - zoomRatio + 1, max[1] - zoomRatio + 1] - for (var x = min[0]; x <= max[0]; x++) { - for (var y = min[1]; y <= max[1]; y++) { - contained.push([x, y, z]) - } - } - this.lastTileContains.unshift({ - call: { xyz: xyz, z: z }, - result: contained, - }) - if (this.lastTileContains.length > 3) this.lastTileContains.pop() - return contained - }, - /** - * Returns true if tile xyzContainer contains the tile xyzContained - * @param {[x,y,z]} xyzContainer - * @param {[x,y,z]} xyzContained - * return bool - */ - tileIsContained(xyzContainer, xyzContained, useLast) { - var contains = this.tileContains( - xyzContainer, - xyzContained[2], - useLast - ) - for (var i = 0; i < contains.length; i++) { - if ( - contains[i][0] == xyzContained[0] && - contains[i][1] == xyzContained[1] - ) - return true - } - return false - }, - scaleImageInHalf(image, width, height) { - var newWidth = Math.floor(width / 2) - var newHeight = Math.floor(height / 2) - var halfWidth = Math.floor(newWidth / 1) - var halfHeight = Math.floor(newHeight / 1) - - var cvTopLeft = document.createElement('canvas') - var cvTopRight = document.createElement('canvas') - var cvBottomLeft = document.createElement('canvas') - var cvBottomRight = document.createElement('canvas') - - cvTopLeft.width = newWidth - cvTopLeft.height = newHeight - cvTopRight.width = newWidth - cvTopRight.height = newHeight - cvBottomLeft.width = newWidth - cvBottomLeft.height = newHeight - cvBottomRight.width = newWidth - cvBottomRight.height = newHeight - - var ctxTopLeft = cvTopLeft.getContext('2d') - var ctxTopRight = cvTopRight.getContext('2d') - var ctxBottomLeft = cvBottomLeft.getContext('2d') - var ctxBottomRight = cvBottomRight.getContext('2d') - - ctxTopLeft.drawImage(image, 0, 0) - ctxTopRight.drawImage(image, -newWidth, 0) - ctxBottomLeft.drawImage(image, 0, -newHeight) - ctxBottomRight.drawImage(image, -newWidth, -newHeight) - - /* - cvTopLeft.width = halfWidth; - cvTopLeft.height = halfHeight; - cvTopRight.width = halfWidth; - cvTopRight.height = halfHeight; - cvBottomLeft.width = halfWidth; - cvBottomLeft.height = halfHeight; - cvBottomRight.width = halfWidth; - cvBottomRight.height = halfHeight; - */ - - var cv = document.createElement('canvas') - cv.id = 'cv' - cv.width = width - cv.height = height - var ctx = cv.getContext('2d') - - ctx.drawImage(cvTopLeft, 0, 0) - ctx.drawImage(cvTopRight, newWidth, 0) - ctx.drawImage(cvBottomLeft, 0, newHeight) - ctx.drawImage(cvBottomRight, newWidth, newHeight) - - var cv1 = document.body.appendChild(cvTopLeft) - var cv2 = document.body.appendChild(cvTopRight) - var cv3 = document.body.appendChild(cvBottomLeft) - var cv4 = document.body.appendChild(cvBottomRight) - - var cvd = document.body.appendChild(cv) - return cv.toDataURL() - }, - //A out of little place - download: function(filepath) { - window.open(filepath + '?nocache=' + new Date().getTime()) - }, - downloadObject(exportObj, exportName, exportExt) { - var strung - if (exportExt && exportExt == '.geojson') { - //pretty print geojson - let features = [] - for (var i = 0; i < exportObj.features.length; i++) - features.push(JSON.stringify(exportObj.features[i])) - features = '[\n' + features.join(',\n') + '\n]' - exportObj.features = '__FEATURES_PLACEHOLDER__' - strung = JSON.stringify(exportObj, null, 2) - strung = strung.replace('"__FEATURES_PLACEHOLDER__"', features) - } else strung = JSON.stringify(exportObj) - - var fileName = exportName + (exportExt || '.json') - - try { - // Create a blob of the data - var fileToSave = new Blob([strung], { - type: 'application/json', - name: fileName, - }) - // Save the file //from FileSaver - saveAs(fileToSave, fileName) - } catch (err) { - //https://stackoverflow.com/questions/19721439/download-json-object-as-a-file-from-browser#answer-30800715 - var dataStr = - 'data:text/json;charset=utf-8,' + encodeURIComponent(strung) - var downloadAnchorNode = document.createElement('a') - downloadAnchorNode.setAttribute('href', dataStr) - downloadAnchorNode.setAttribute('download', fileName) - document.body.appendChild(downloadAnchorNode) // required for firefox - downloadAnchorNode.click() - downloadAnchorNode.remove() - } - }, - //headers: ['x','y','z'] - //array: [[0,1,2],[3,4,5],...] - downloadArrayAsCSV(headers, array, exportName) { - var csv = '' - csv = headers.join(',') + '\n' - for (var i = 0; i < array.length; i++) { - csv += array[i].join(',') + '\n' - } - var dataStr = - 'data:text/csv;charset=utf-8,' + encodeURIComponent(csv) - var downloadAnchorNode = document.createElement('a') - downloadAnchorNode.setAttribute('href', dataStr) - downloadAnchorNode.setAttribute('download', exportName + '.csv') - document.body.appendChild(downloadAnchorNode) // required for firefox - downloadAnchorNode.click() - downloadAnchorNode.remove() - }, - downloadCanvas(canvasId, name, callback) { - var link = document.createElement('a') - name = name ? name + '.png' : 'mmgis.png' - link.setAttribute('download', name) - document.getElementById(canvasId).toBlob(function(blob) { - var objUrl = URL.createObjectURL(blob) - link.setAttribute('href', objUrl) - document.body.appendChild(link) - link.click() - link.remove() - if (typeof callback === 'function') callback() - }) - }, - uniqueArray(arr) { - var uniqueArray = [] - for (var i in arr) { - if (uniqueArray.indexOf(arr[i]) === -1) uniqueArray.push(arr[i]) - } - return uniqueArray - }, - doBoundingBoxesIntersect(a, b) { - return a[1] <= b[3] && a[3] >= b[1] && a[0] <= b[2] && a[2] >= b[0] - }, - pointsInPoint(point, layers) { - var points = [] - - var l = layers._layers - - if (l == null) return points - - for (var i in l) { - if ( - l[i].feature.geometry.coordinates[0] == point[0] && - l[i].feature.geometry.coordinates[1] == point[1] - ) - points.push(l[i].feature) - } - - return points - }, - validTextColour(stringToTest) { - //Alter the following conditions according to your need. - if (stringToTest === '') { - return false - } - if (stringToTest === 'inherit') { - return false - } - if (stringToTest === 'transparent') { - return false - } - - var image = document.createElement('img') - image.style.color = 'rgb(0, 0, 0)' - image.style.color = stringToTest - if (image.style.color !== 'rgb(0, 0, 0)') { - return true - } - image.style.color = 'rgb(255, 255, 255)' - image.style.color = stringToTest - return image.style.color !== 'rgb(255, 255, 255)' - }, - timestampToDate(timestamp) { - var a = new Date(timestamp * 1000) - var months = [ - 'Jan', - 'Feb', - 'Mar', - 'Apr', - 'May', - 'Jun', - 'Jul', - 'Aug', - 'Sep', - 'Oct', - 'Nov', - 'Dec', - ] - var year = a.getUTCFullYear() - var month = a.getUTCMonth() + 1 - var monthName = months[month - 1] - var date = a.getUTCDate() - var hour = - a.getUTCHours() < 10 ? '0' + a.getUTCHours() : a.getUTCHours() - var min = - a.getUTCMinutes() < 10 - ? '0' + a.getUTCMinutes() - : a.getUTCMinutes() - var sec = - a.getUTCSeconds() < 10 - ? '0' + a.getUTCSeconds() - : a.getUTCSeconds() - - return ( - monthName + - ' ' + - date + - ', ' + - year + - ' ' + - hour + - ':' + - min + - ':' + - sec - ) - }, - /** - * Returns an array of only the matching elements between two arrays - * @param {[]} arr1 - * @param {[]} arr2 - */ - diff(arr1, arr2) { - if (arr1 == null || arr2 == null) return [] - return arr1.filter(e => arr2.indexOf(e) !== -1) - }, - /** - * Copies input to user's clipboard - * @param {string} text - text to copy to clipboard - * @credit https://hackernoon.com/copying-text-to-clipboard-with-javascript-df4d4988697f - */ - copyToClipboard(text) { - const el = document.createElement('textarea') // Create a ", - "
", - "", - "
", - "
", - "", - "
" + ( (file.public == 1) ? 'Public' : 'Private' ) + "
", - "
", - "", - "
", - "
", - "
Save Changes
", - "
", - "" - ].join('\n'); - if (file.is_master) { - d3.select('#drawToolDrawFilesListMaster') - .append('li') - .attr('class', 'drawToolDrawFilesListElem') - .attr('file_id', file.id) - .attr('file_name', file.file_name) - .attr('file_owner', file.file_owner) - .html(markup) - - var lastMasterName = $( - '#drawToolDrawFilesListMaster li:last-child .drawToolFileName' - ).text() - $( - '#drawToolDrawFilesListMaster li:last-child .drawToolFileName' - ).text( - DrawTool.intentNameMapping[lastMasterName.toLowerCase()] - ? DrawTool.intentNameMapping[ - lastMasterName.toLowerCase() - ] + 's' - : lastMasterName - ) - } else { - d3.select('#drawToolDrawFilesList') - .append('li') - .attr('class', 'drawToolDrawFilesListElem') - .attr('file_id', file.id) - .attr('file_name', file.file_name) - .attr('file_owner', file.file_owner) - .html(markup) - } - } - - $('.drawToolDrawFilesListElem').on('mouseover', function() { - var that = this - clearTimeout(DrawTool.fileTooltipTimeout) - DrawTool.fileTooltipTimeout = setTimeout(function() { - $('.drawToolFileTooltip').removeClass('active') - var tt = $(that).find('.drawToolFileTooltip') - tt.addClass('active') - tt.css('top', $(that).offset().top + 'px') - clearTimeout(DrawTool.fileTooltipTimeout2) - DrawTool.fileTooltipTimeout2 = setTimeout(function() { - $(that) - .find('.drawToolFileDescriptionTooltip') - .addClass('active') - }, 1000) - }, 400) - }) - $('.drawToolDrawFilesListElem').on('mouseout', function() { - clearTimeout(DrawTool.fileTooltipTimeout) - clearTimeout(DrawTool.fileTooltipTimeout2) - $('.drawToolFileTooltip').removeClass('active') - $('.drawToolFileDescriptionTooltip').removeClass('active') - }) - - //Li Elem Context Menu - $('#drawToolDrawPublished').off('contextmenu') - $('.drawToolDrawFilesListElem, #drawToolDrawPublished').on( - 'contextmenu', - function(e) { - e.preventDefault() - var elm = $(this) - var isPub = elm.attr('id') === 'drawToolDrawPublished' - hideContextMenu(true) - elm.css('background', '#e8e8e8') - elm.find('.drawToolIntentColor').css({ - width: '17px', - }) - var rect = $(this) - .get(0) - .getBoundingClientRect() - - var markup = [ - "
", - '
    ', - "
  • Export as .geojson
  • ", - //"
  • Export as .shp
  • ", - "
  • Toggle Labels
  • ', - '
', - '
', - ].join('\n') - - $('body').append(markup) - - var body = { - id: elm.attr('file_id'), - } - if (isPub) { - body = { - id: '[1,2,3,4,5]', - published: true, - } - } - $('#cmExportGeoJSON').on( - 'click', - (function(body, isPub) { - return function() { - DrawTool.getFile(body, function(d) { - let geojson = d.geojson - let filename = '' - if (isPub) { - filename = 'CAMP_Latest_Map' - geojson._metadata = d.file - } else { - filename = - d.file[0].file_name + - '_' + - d.file[0].id + - '_' + - d.file[0].file_owner - geojson._metadata = [d.file[0]] - } - - //Genericize it to a map/all type - if (geojson._metadata[0].intent != 'all') { - for ( - var i = 0; - i < geojson.features.length; - i++ - ) { - var newIntent = null - var t = geojson.features[ - i - ].geometry.type.toLowerCase() - if ( - t == 'polygon' || - t == 'multipolygon' - ) - newIntent = 'polygon' - else if ( - t == 'linestring' || - t == 'multilinestring' - ) - newIntent = 'line' - else newIntent = 'point' - geojson.features[ - i - ].properties._.intent = newIntent - } - geojson._metadata[0].intent = 'all' - } - - DrawTool.expandPointprops(geojson) - F_.downloadObject( - geojson, - filename, - '.geojson' - ) - }) - } - })(body, isPub) - ) - - $('#cmExportShp').on( - 'click', - (function(body, isPub) { - return function() { - DrawTool.getFile(body, function(d) { - let geojson = d.geojson - ///geojson._metadata = d.file[0]; - shpwrite.download(geojson, { - folder: - d.file[0].file_name + - '_' + - d.file[0].id + - '_' + - d.file[0].file_owner, - types: {}, - }) - }) - } - })(body, isPub) - ) - - $( - '#drawToolDrawFilesListElemContextMenu #cmToggleLabels' - ).on( - 'click', - (function(isPub) { - return function() { - if (isPub) return - DrawTool.toggleLabels(elm.attr('file_id')) - } - })(isPub) - ) - - var count = 1 //It has to start in one - $('#drawToolDrawFilesListElemContextMenu').on( - 'mouseenter', - function() { - count++ - } - ) - $('#drawToolDrawFilesListElemContextMenu').on( - 'mouseleave', - function() { - count-- - setTimeout(function() { - if (count <= 0) hideContextMenu() - }, 50) - } - ) - function enter() { - count++ - } - function leave() { - count-- - setTimeout(function() { - if (count <= 0) { - hideContextMenu() - elm.off('mouseenter', enter) - elm.off('mouseleave', leave) - } - }, 50) - } - elm.on('mouseenter', enter) - elm.on('mouseleave', leave) - - function hideContextMenu(immediately) { - $('.drawToolDrawFilesListElem').css('background', '') - $('.drawToolIntentColor').css({ - width: '7px', - }) - if (immediately) { - $('#drawToolDrawFilesListElemContextMenu').remove() - } else - $('#drawToolDrawFilesListElemContextMenu').animate( - { - opacity: 0, - }, - 250, - function() { - $( - '#drawToolDrawFilesListElemContextMenu' - ).remove() - } - ) - } - } - ) - - $('.drawToolFileSave').on('click', function() { - var elm = $(this) - .parent() - .parent() - .parent() - - //Only select files you own - if (mmgisglobal.user !== elm.attr('file_owner')) return - - var fileid = elm.attr('file_id') - var filename = elm.find('.drawToolFileNameInput').val() - var body = { - id: fileid, - file_name: filename, - file_description: elm.find('.drawToolFileDesc').val(), - public: - elm - .find('.drawToolFilePublic') - .find('i') - .attr('public') == '1' - ? 1 - : 0, - } - - DrawTool.changeFile(body, function(d) { - elm.find('.drawToolFileName').text(filename) - var files_i = F_.objectArrayIndexOfKeyWithValue( - DrawTool.files, - 'id', - parseInt(fileid) - ) - if (files_i !== -1) - DrawTool.files[files_i].file_name = filename - - DrawTool.getFiles(function() { - DrawTool.populateFiles() - }) - }) - }) - - $('.drawToolFilePublic').off('click') - $('.drawToolFilePublic').on('click', function() { - var icon = $(this).find('i') - var public = icon.attr('public') - if (public == '0') { - icon.removeClass('mdi-shield') - icon.addClass('mdi-shield-outline') - icon.attr('public', '1') - $(this) - .find('.drawToolFilePublicName') - .text('Public') - } else { - icon.removeClass('mdi-shield-outline') - icon.addClass('mdi-shield') - icon.attr('public', '0') - $(this) - .find('.drawToolFilePublicName') - .text('Private') - } - }) - - $('.drawToolFileDelete').off('click') - $('.drawToolFileDelete').on('click', function() { - var filenameToDelete = $(this) - .parent() - .parent() - .parent() - .attr('file_name') - var response = prompt( - 'Are you sure you want to delete ' + - filenameToDelete + - ' (Y/N)?' - ) - response = response.toLowerCase() - if (!(response == 'yes' || response == 'y')) return - - var body = { - id: $(this) - .parent() - .parent() - .parent() - .attr('file_id'), - } - var layerId = 'DrawTool_' + body.id - - DrawTool.removeFile( - body, - (function(layerId, id) { - return function(d) { - //Remove each feature in its group - if (L_.layersGroup.hasOwnProperty(layerId)) { - for ( - var i = 0; - i < L_.layersGroup[layerId].length; - i++ - ) { - Map_.rmNotNull(L_.layersGroup[layerId][i]) - //And from the Globe - Globe_.removeVectorTileLayer( - 'camptool_' + layerId + '_' + i - ) - } - } - //Remove from filesOn - let f = DrawTool.filesOn.indexOf(parseInt(id)) - if (f != -1) DrawTool.filesOn.splice(f, 1) - } - })(layerId, body.id) - ) - - $(this) - .parent() - .parent() - .parent() - .remove() - }) - - $('.drawToolFileEdit').off('click') - $('.drawToolFileEdit').on('click', function() { - var elm = $(this) - .parent() - .parent() - .parent() - - //Only select files you own - if (mmgisglobal.user !== elm.attr('file_owner')) return - - var top = elm.offset().top + 22 + 'px' - elm = elm.find('.drawToolFileEditOn') - elm.css('top', top) - var display = elm.css('display') - $('.drawToolFileEditOn').css('display', 'none') - if (display == 'none') elm.css('display', 'inherit') - return false - }) - - //Highlight layer if on - $('.drawToolDrawFilesListElem').off('mouseenter') - $('.drawToolDrawFilesListElem').on('mouseenter', function() { - $(this) - .find('.drawToolFileEdit') - .addClass('shown') - var fileId = parseInt($(this).attr('file_id')) - var l = L_.layersGroup['DrawTool_' + fileId] - if (!l) return - for (var i = 0; i < l.length; i++) { - if (l[i] != null) { - if (typeof l[i].setStyle === 'function') - l[i].setStyle({ color: '#7fff00' }) - else if (l[i].hasOwnProperty('_layers')) { - //Arrow - var layers = l[i]._layers - layers[Object.keys(layers)[0]].setStyle({ - color: '#7fff00', - }) - layers[Object.keys(layers)[1]].setStyle({ - color: '#7fff00', - }) - } else - $('.DrawToolAnnotation_' + fileId).addClass( - 'highlight' - ) - } - } - }) - $('.drawToolDrawFilesListElem').off('mouseleave') - $('.drawToolDrawFilesListElem').on('mouseleave', function() { - $(this) - .find('.drawToolFileEdit') - .removeClass('shown') - var fileId = parseInt($(this).attr('file_id')) - var l = L_.layersGroup['DrawTool_' + fileId] - if (!l) return - for (var i = 0; i < l.length; i++) { - var style - if (l[i] != null) { - if ( - !l[i].hasOwnProperty('feature') && - l[i].hasOwnProperty('_layers') - ) - style = - l[i]._layers[Object.keys(l[i]._layers)[0]] - .feature.properties.style - else style = l[i].feature.properties.style - - if (typeof l[i].setStyle === 'function') - l[i].setStyle(style) - else if (l[i].hasOwnProperty('_layers')) { - //Arrow - var layers = l[i]._layers - layers[Object.keys(layers)[0]].setStyle({ - color: style.color, - }) - layers[Object.keys(layers)[1]].setStyle({ - color: style.color, - }) - } else - $('.DrawToolAnnotation_' + fileId).removeClass( - 'highlight' - ) - } - } - }) - //Select file - $('.drawToolFileSelector').off('click') - $('.drawToolFileSelector').on('click', function() { - //Only select files you own - var fileFromId = DrawTool.getFileObjectWithId( - $(this).attr('file_id') - ) - if ( - mmgisglobal.user !== $(this).attr('file_owner') && - (fileFromId && - F_.diff( - fileFromId.file_owner_group, - DrawTool.userGroups - ).length == 0) - ) - return - - var checkbox = $(this) - .parent() - .find('.drawToolFileCheckbox') - $('.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) - } - - DrawTool.currentFileId = parseInt(checkbox.attr('file_id')) - if (DrawTool.filesOn.indexOf(DrawTool.currentFileId) == -1) - checkbox.click() - }) - - //Visible File - $('.drawToolFileCheckbox').off('click') - $('.drawToolFileCheckbox').on('click', DrawTool.toggleFile) - }, - refreshFile: function( - id, - time, - populateShapesAfter, - selectedFeatureIds, - asPublished, - cb - ) { - let parsedId = - typeof parseInt(id) === 'number' && !Array.isArray(id) - ? parseInt(id) - : 'master' - //Can't refresh what isn't there - if ( - parsedId != 'master' && - L_.layersGroup.hasOwnProperty('DrawTool_' + parsedId) == false - ) - return - - var body = { - id: JSON.stringify(id), - time: time, - } - if (asPublished == true) body.published = true - - DrawTool.getFile( - body, - (function(index, selectedFeatureIds) { - return function(data) { - var layerId = 'DrawTool_' + index - //Remove it first - if (L_.layersGroup.hasOwnProperty(layerId)) { - for ( - var i = 0; - i < L_.layersGroup[layerId].length; - i++ - ) { - //Close any popups/labels - var popupLayer = L_.layersGroup[layerId][i] - DrawTool.removePopupsFromLayer(popupLayer) - - Map_.rmNotNull(L_.layersGroup[layerId][i]) - L_.layersGroup[layerId][i] = null - //And from the Globe - Globe_.removeVectorTileLayer( - 'camptool_' + layerId + '_' + i - ) - } - } - - var features = data.geojson.features - for (var i = 0; i < features.length; i++) { - if ( - !features[i].properties.hasOwnProperty('style') - ) { - features[i].properties.style = F_.clone( - DrawTool.defaultStyle - ) - if ( - features[i].geometry.type.toLowerCase() == - 'point' - ) - features[i].properties.style.fillOpacity = 1 - } - if (features[i].properties.arrow === true) { - var c = features[i].geometry.coordinates - var start = new L.LatLng(c[0][1], c[0][0]) - var end = new L.LatLng(c[1][1], c[1][0]) - - DrawTool.addArrowToMap( - layerId, - start, - end, - features[i].properties.style, - features[i] - ) - } else if ( - features[i].properties.annotation === true - ) { - //Remove previous annotation if any - $( - '#DrawToolAnnotation_' + - id + - '_' + - features[i].properties._.id - ) - .parent() - .parent() - .parent() - .parent() - .remove() - - var s = features[i].properties.style - var styleString = - (s.color - ? 'text-shadow: ' + - F_.getTextShadowString( - s.color, - s.strokeOpacity, - s.weight - ) + - '; ' - : '') + - (s.fillColor - ? 'color: ' + s.fillColor + '; ' - : '') + - (s.fontSize - ? 'font-size: ' + s.fontSize + '; ' - : '') - L_.layersGroup[layerId].push( - L.popup({ - className: 'leaflet-popup-annotation', - closeButton: false, - autoClose: false, - closeOnEscapeKey: false, - closeOnClick: false, - autoPan: false, - offset: new L.point(0, 3), - }) - .setLatLng( - new L.LatLng( - features[ - i - ].geometry.coordinates[1], - features[ - i - ].geometry.coordinates[0] - ) - ) - .setContent( - '
' + - "
" + - '
' + - '
' - ) - .addTo(Map_.map) - ) - L_.layersGroup[layerId][ - L_.layersGroup[layerId].length - 1 - ].feature = features[i] - $( - '#DrawToolAnnotation_' + - id + - '_' + - features[i].properties._.id - ).text(features[i].properties.name) - - DrawTool.refreshNoteEvents() - } else if (features[i].geometry.type === 'Point') { - L_.layersGroup[layerId].push( - L.circleMarker( - new L.LatLng( - features[i].geometry.coordinates[1], - features[i].geometry.coordinates[0] - ), - features[i].properties.style - ).addTo(Map_.map) - ) - L_.layersGroup[layerId][ - L_.layersGroup[layerId].length - 1 - ].feature = features[i] - } else { - L_.layersGroup[layerId].push( - L.geoJson( - { - type: 'FeatureCollection', - features: [features[i]], - }, - { - style: function(feature) { - return feature.properties.style - }, - } - ).addTo(Map_.map) - ) - } - - if ( - features[i].properties.annotation !== true && - features[i].properties.arrow !== true - ) { - var last = L_.layersGroup[layerId].length - 1 - var llast = L_.layersGroup[layerId][last] - var layer - - if (llast.hasOwnProperty('_layers')) - layer = - llast._layers[ - Object.keys(llast._layers)[0] - ] - else { - layer = Object.assign({}, llast) - layer.feature.geometry.coordinates = [ - layer.feature.geometry.coordinates[1], - layer.feature.geometry.coordinates[0], - ] - } - - Globe_.addVectorTileLayer( - { - id: 'camptool_' + layerId + '_' + last, - on: true, - layers: [layer], - }, - true - ) - } - } - if (populateShapesAfter) - DrawTool.populateShapes(id, selectedFeatureIds) - - DrawTool.maintainLayerOrder() - - DrawTool.refreshMasterCheckbox() - - //Keep labels on if they were on before - let indexOf = DrawTool.labelsOn.indexOf(index + '') - if (indexOf != -1) { - DrawTool.labelsOn.splice(indexOf, 1) - DrawTool.toggleLabels(index + '') - } - - if (typeof cb === 'function') { - cb() - } - } - })(parsedId, selectedFeatureIds) - ) - }, - /** - * Adds or removes a file - * if fileId is not define, expects an element with a file_id attr - * @param {int} fileId *optional* - * @param {'on' || 'off'} forceToggle *optional* - */ - toggleFile: function( - fileId, - forceToggle, - populateShapesAfter, - asPublished - ) { - var argumented = typeof fileId === 'number' || fileId === 'master' - - var id = parseInt($(this).attr('file_id')) - if (argumented) id = fileId - - var layerId = 'DrawTool_' + id - - if ( - forceToggle == 'off' || - (forceToggle != 'on' && DrawTool.filesOn.indexOf(id) != -1) - ) { - //OFF - DrawTool.filesOn = DrawTool.filesOn.filter(function(v) { - return v !== id - }) - - if (!argumented) { - DrawTool.populateShapes() - //Change icon - $(this).removeClass('on') - $(this) - .parent() - .parent() - .parent() - .removeClass('on') - } - //Remove each feature in its group - if (L_.layersGroup.hasOwnProperty(layerId)) { - for (var i = 0; i < L_.layersGroup[layerId].length; i++) { - Map_.rmNotNull(L_.layersGroup[layerId][i]) - //And from the Globe - Globe_.removeVectorTileLayer( - 'camptool_' + layerId + '_' + i - ) - } - } - - DrawTool.refreshMasterCheckbox() - } else { - //ON - DrawTool.filesOn.push(id) - - if (!argumented) { - //Change icon - $(this).addClass('on') - $(this) - .parent() - .parent() - .parent() - .addClass('on') - } - //Get the file if we don't already have it - L_.layersGroup[layerId] = [] - DrawTool.refreshFile( - id == 'master' ? DrawTool.masterFileIds : id, - null, - populateShapesAfter != null - ? populateShapesAfter - : !argumented, - null, - asPublished - ) - } - }, - toggleLabels: function(file_id) { - var l = L_.layersGroup['DrawTool_' + file_id] - let indexOf = DrawTool.labelsOn.indexOf(file_id) - var isOn = indexOf != -1 - if (isOn) DrawTool.labelsOn.splice(indexOf, 1) - else DrawTool.labelsOn.push(file_id) - - if (l) { - for (var i = 0; i < l.length; i++) { - if (l[i] != null) { - if (l[i]._layers) { - var p = l[i]._layers[Object.keys(l[i]._layers)[0]] - if (isOn) p.closePopup() - else p.openPopup() - } else if (l[i].feature.properties.annotation != true) { - var p = l[i] - if (isOn) p.closePopup() - else p.openPopup() - } - } - } - } - }, - maintainLayerOrder: function() { - for (var i = 0; i < DrawTool.intentOrder.length; i++) { - for (var j = 0; j < DrawTool.filesOn.length; j++) { - var file = DrawTool.getFileObjectWithId(DrawTool.filesOn[j]) - if (file.intent === DrawTool.intentOrder[i]) { - for (var e of L_.layersGroup[ - 'DrawTool_' + DrawTool.filesOn[j] - ]) - if ( - e != null && - typeof e.bringToFront === 'function' - ) - e.bringToFront() - } - } - } - }, - removePopupsFromLayer: function(popupLayer) { - if (popupLayer != null) { - if (popupLayer._layers) { - var p = - popupLayer._layers[Object.keys(popupLayer._layers)[0]] - - let wasOpen = p.getPopup() ? p.getPopup().isOpen() : false - if (wasOpen) return wasOpen - p.closePopup() - p.unbindPopup() - } else if ( - !popupLayer.feature || - popupLayer.feature.properties.annotation != true - ) { - let wasOpen = popupLayer.getPopup() - ? popupLayer.getPopup().isOpen() - : false - if (wasOpen) return wasOpen - popupLayer.closePopup() - popupLayer.unbindPopup() - } - } - return false - }, - refreshNoteEvents() { - $('.drawToolAnnotation').off('mouseover') - $('.drawToolAnnotation').on('mouseover', function() { - var layer = 'DrawTool_' + $(this).attr('layer') - var index = $(this).attr('index') - $('.drawToolShapeLi').removeClass('hovered') - $('.drawToolShapeLi .drawToolShapeLiItem').mouseleave() - $('#drawToolShapeLiItem_' + layer + '_' + index).addClass( - 'hovered' - ) - $( - '#drawToolShapeLiItem_' + - layer + - '_' + - index + - ' .drawToolShapeLiItem' - ).mouseenter() - }) - $('.drawToolAnnotation').off('mouseout') - $('.drawToolAnnotation').on('mouseout', function() { - $('.drawToolShapeLi').removeClass('hovered') - $('.drawToolShapeLi .drawToolShapeLiItem').mouseleave() - }) - $('.drawToolAnnotation').off('click') - $('.drawToolAnnotation').on('click', function() { - var layer = 'DrawTool_' + $(this).attr('layer') - var index = $(this).attr('index') - var shape = L_.layersGroup[layer][index] - if (!mmgisglobal.shiftDown) { - if (typeof shape.getBounds === 'function') - Map_.map.fitBounds(shape.getBounds()) - else Map_.map.panTo(shape._latlng) - } - - shape.fireEvent('click') - }) - }, - refreshMasterCheckbox: function() { - //Have master file checkbox on only when all master files are on too - var masterCheckShouldBeOn = true - for (var f in DrawTool.files) { - if ( - DrawTool.files[f].is_master && - DrawTool.filesOn.indexOf(DrawTool.files[f].id) == -1 - ) { - masterCheckShouldBeOn = false - break - } - } - if (masterCheckShouldBeOn) - $('.drawToolFileMasterCheckbox').addClass('on') - else $('.drawToolFileMasterCheckbox').removeClass('on') - }, - } - - return Files -}) +define([ + 'jquery', + 'd3', + 'Formulae_', + 'Layers_', + 'Globe_', + 'Map_', + 'Viewer_', + 'UserInterface_', + 'CursorInfo', + 'leafletDraw', + 'turf', + 'leafletPolylineDecorator', + 'leafletSnap', + 'colorPicker', + 'shp', + 'shpwrite', +], function ( + $, + d3, + F_, + L_, + Globe_, + Map_, + Viewer_, + UserInterface_, + CursorInfo, + leafletDraw, + turf, + leafletPolylineDecorator, + leafletSnap, + colorPicker, + shp, + shpwrite +) { + var DrawTool = null + var Files = { + init: function (tool) { + DrawTool = tool + DrawTool.populateFiles = Files.populateFiles + DrawTool.refreshFile = Files.refreshFile + DrawTool.toggleFile = Files.toggleFile + DrawTool.toggleLabels = Files.toggleLabels + DrawTool.maintainLayerOrder = Files.maintainLayerOrder + DrawTool.removePopupsFromLayer = Files.removePopupsFromLayer + DrawTool.refreshNoteEvents = Files.refreshNoteEvents + DrawTool.refreshMasterCheckbox = Files.refreshMasterCheckbox + }, + populateFiles: function () { + $('#drawToolDrawFilesListMaster *').remove() + $('#drawToolDrawFilesList *').remove() + + for (var i = 0; i < DrawTool.files.length; i++) { + addFileToList(DrawTool.files[i]) + } + + //Master Header + $('.drawToolMasterHeaderLeftLeft').off('click') + $('.drawToolMasterHeaderLeftLeft').on('click', function () { + $('#drawToolDrawFilesListMaster').toggleClass('active') + var isActive = $('#drawToolDrawFilesListMaster').hasClass( + 'active' + ) + if (isActive) { + $('.drawToolMasterHeaderLeftLeft i').removeClass( + 'mdi-chevron-right' + ) + $('.drawToolMasterHeaderLeftLeft i').addClass( + 'mdi-chevron-down' + ) + } else { + $('.drawToolMasterHeaderLeftLeft i').removeClass( + 'mdi-chevron-down' + ) + $('.drawToolMasterHeaderLeftLeft i').addClass( + 'mdi-chevron-right' + ) + } + }) + $('.drawToolFileMasterCheckbox').off('click') + $('.drawToolFileMasterCheckbox').on('click', function () { + $('.drawToolFileMasterCheckbox').toggleClass('on') + var isActive = $('.drawToolFileMasterCheckbox').hasClass('on') + if (isActive) { + //Turn on all master files + for (var f in DrawTool.files) { + var id = DrawTool.files[f].id + if ( + DrawTool.files[f].is_master && + DrawTool.filesOn.indexOf(id) == -1 + ) { + DrawTool.toggleFile(id) + $( + '.drawToolFileCheckbox[file_id="' + id + '" ]' + ).addClass('on') + } + } + } else { + //Turn off all master files + for (var f in DrawTool.files) { + var id = DrawTool.files[f].id + if ( + DrawTool.files[f].is_master && + DrawTool.filesOn.indexOf(id) != -1 + ) { + DrawTool.toggleFile(id) + $( + '.drawToolFileCheckbox[file_id="' + id + '" ]' + ).removeClass('on') + } + } + } + }) + + //Draw type + $('#drawToolDrawingTypeDiv > div').off('click') + $('#drawToolDrawingTypeDiv > div').on('click', function (e) { + if (DrawTool.intentType == 'all') { + $('#drawToolDrawingTypeDiv > div').removeClass('active') + $('#drawToolDrawingTypeDiv > div').css('border-radius', 0) + $('#drawToolDrawingTypeDiv > div').css( + 'background', + 'var(--color-a)' + ) + $(this).addClass('active') + $(this).css( + 'background', + $('#drawToolDrawingTypeDiv').css('background') + ) + $(this).prev().css({ + 'border-top-right-radius': '10px', + 'border-bottom-right-radius': '10px', + }) + $(this).next().css({ + 'border-top-left-radius': '10px', + 'border-bottom-left-radius': '10px', + }) + + DrawTool.setDrawingType($(this).attr('draw')) + } + }) + + //Filter + $('#drawToolDrawFilterClear').off('click') + $('#drawToolDrawFilterClear').on('click', function () { + $('#drawToolDrawFilter').val('') + fileFilter() + }) + $('#drawToolDrawFilter').off('input') + $('#drawToolDrawFilter').on('input', fileFilter) + $('#drawToolDrawSortDiv > div').off('click') + $('#drawToolDrawSortDiv > div').on('click', function () { + $(this).toggleClass('active') + fileFilter() + }) + $('.drawToolFilterDropdown li').off('click') + $('.drawToolFilterDropdown li').on('click', function () { + $(this).toggleClass('active') + $(this).find('.drawToolFilterCheckbox').toggleClass('on') + fileFilter() + }) + $('#drawToolDrawIntentFilterDiv > div').off('mouseenter') + $('#drawToolDrawIntentFilterDiv > div').on( + 'mouseenter', + function () { + var that = this + clearTimeout(DrawTool.tooltipTimeout1) + DrawTool.tooltipTimeout1 = setTimeout(function () { + $('#drawToolDrawFilterDivToolTip').css( + 'background', + DrawTool.categoryStyles[$(that).attr('intent')] + .color + ) + $('#drawToolDrawFilterDivToolTip').addClass('active') + $('#drawToolDrawFilterDivToolTip').text( + $(that).attr('tooltip') + ) + }, 500) + } + ) + $('#drawToolDrawIntentFilterDiv > div').off('mouseleave') + $('#drawToolDrawIntentFilterDiv > div').on( + 'mouseleave', + function () { + clearTimeout(DrawTool.tooltipTimeout1) + $('#drawToolDrawFilterDivToolTip').css( + 'background', + 'rgba(255,255,255,0)' + ) + $('#drawToolDrawFilterDivToolTip').removeClass('active') + $('#drawToolDrawFilterDivToolTip').text('') + } + ) + + fileFilter() + function fileFilter() { + //filter over name, intent and id for now + var on = 0 + var off = 0 + + var string = $('#drawToolDrawFilter').val() + if (string != null && string != '') + string = string.toLowerCase() + /* + else { + $( '.drawToolDrawFilesListElem' ).css( 'display', 'list-item' ); + $( '#drawToolDrawFilterCount' ).text( '' ); + $( '#drawToolDrawFilterCount' ).css( 'padding-right', '0px' ); + return; + } + */ + + var intents = [] + $('.drawToolFilterDropdown .active').each(function () { + intents.push($(this).attr('intent')) + }) + + var sorts = [] + $('#drawToolDrawSortDiv .active').each(function () { + sorts.push($(this).attr('type')) + }) + + $('.drawToolDrawFilesListElem').each(function () { + var fileId = parseInt($(this).attr('file_id')) + var file = F_.objectArrayIndexOfKeyWithValue( + DrawTool.files, + 'id', + parseInt(fileId) + ) + if (file != null) file = DrawTool.files[file] + + var show = false + if ( + (string == null || + $(this) + .attr('file_name') + .toLowerCase() + .indexOf(string) != -1 || + string == null || + $(this) + .attr('file_owner') + .toLowerCase() + .indexOf(string) != -1) && + (sorts.indexOf('on') == -1 || + DrawTool.filesOn.indexOf(fileId) != -1) && + (sorts.indexOf('owned') == -1 || + (file != null && + file.file_owner === mmgisglobal.user)) && + (sorts.indexOf('public') == -1 || + (file != null && file.public == '1')) && + (intents.length == 0 || + (file != null && + intents.indexOf(file.intent) != -1)) + ) + show = true + + if (file.is_master) show = true + + if (show) { + $(this).css('opacity', 1) + $(this).css('height', '30px') + $(this).css( + 'border-bottom', + '1px solid rgba(171, 171, 171, 0.25);' + ) + on++ + } else { + $(this).css('opacity', 0) + $(this).css('height', '0px') + $(this).css('border-bottom', 'none') + off++ + } + }) + + $('#drawToolDrawFilterCount').text(on + '/' + (on + off)) + $('#drawToolDrawFilterCount').css('padding-right', '7px') + } + + function addFileToList(file) { + var checkState = '-blank-outline' + var onState = ' on' + var shieldState = '' + var ownedByUser = false + + if (DrawTool.currentFileId == file.id) + checkState = '-intermediate' + + if (DrawTool.filesOn.indexOf(file.id) == -1) onState = '' + + if (file.public == 1) shieldState = '-outline' + + if ( + mmgisglobal.user == file.file_owner || + (file.file_owner_group && + F_.diff(file.file_owner_group, DrawTool.userGroups) + .length > 0) + ) + ownedByUser = true + + // prettier-ignore + var markup = [ + "
", + "
", + "
", + "
", + "
" + file.file_name + "
", + //"
" + ( (ownedByUser) ? '' : file.file_owner ) + "
", + "
", + "
", + "
", + "", + "", + "
", + "
", + "
", + "
", + "
", + "
" + file.file_name + " by " + ( (ownedByUser) ? ((file.is_master) ? 'you! (Lead)' : 'you!') : ((file.is_master) ? 'Lead!' : file.file_owner) ) + "
", + "
" + ( (file.file_description != null && file.file_description.length > 0 ) ? file.file_description : 'No file description.' ) + "
", + //"
(Right-click to toggle labels)
", + "
", + "
", + "
", + "", + "
", + "
", + "", + "
", + "
", + "
", + "
", + "", + "
" + ( (file.public == 1) ? 'Public' : 'Private' ) + "
", + "
", + "", + "
", + "
", + "
Save Changes
", + "
", + "
" + ].join('\n'); + if (file.is_master) { + d3.select('#drawToolDrawFilesListMaster') + .append('li') + .attr('class', 'drawToolDrawFilesListElem') + .attr('file_id', file.id) + .attr('file_name', file.file_name) + .attr('file_owner', file.file_owner) + .html(markup) + + var lastMasterName = $( + '#drawToolDrawFilesListMaster li:last-child .drawToolFileName' + ).text() + $( + '#drawToolDrawFilesListMaster li:last-child .drawToolFileName' + ).text( + DrawTool.intentNameMapping[lastMasterName.toLowerCase()] + ? DrawTool.intentNameMapping[ + lastMasterName.toLowerCase() + ] + 's' + : lastMasterName + ) + } else { + d3.select('#drawToolDrawFilesList') + .append('li') + .attr('class', 'drawToolDrawFilesListElem') + .attr('file_id', file.id) + .attr('file_name', file.file_name) + .attr('file_owner', file.file_owner) + .html(markup) + } + } + + $('.drawToolDrawFilesListElem').on('mouseover', function () { + var that = this + clearTimeout(DrawTool.fileTooltipTimeout) + DrawTool.fileTooltipTimeout = setTimeout(function () { + $('.drawToolFileTooltip').removeClass('active') + var tt = $(that).find('.drawToolFileTooltip') + tt.addClass('active') + tt.css('top', $(that).offset().top + 'px') + clearTimeout(DrawTool.fileTooltipTimeout2) + DrawTool.fileTooltipTimeout2 = setTimeout(function () { + $(that) + .find('.drawToolFileDescriptionTooltip') + .addClass('active') + }, 1000) + }, 400) + }) + $('.drawToolDrawFilesListElem').on('mouseout', function () { + clearTimeout(DrawTool.fileTooltipTimeout) + clearTimeout(DrawTool.fileTooltipTimeout2) + $('.drawToolFileTooltip').removeClass('active') + $('.drawToolFileDescriptionTooltip').removeClass('active') + }) + + //Li Elem Context Menu + $('#drawToolDrawPublished').off('contextmenu') + $('.drawToolDrawFilesListElem, #drawToolDrawPublished').on( + 'contextmenu', + function (e) { + e.preventDefault() + var elm = $(this) + var isPub = elm.attr('id') === 'drawToolDrawPublished' + hideContextMenu(true) + elm.css('background', '#e8e8e8') + elm.find('.drawToolIntentColor').css({ + width: '17px', + }) + var rect = $(this).get(0).getBoundingClientRect() + + var markup = [ + "
", + '
    ', + "
  • Export as .geojson
  • ", + //"
  • Export as .shp
  • ", + "
  • Toggle Labels
  • ', + '
', + '
', + ].join('\n') + + $('body').append(markup) + + var body = { + id: elm.attr('file_id'), + } + if (isPub) { + body = { + id: '[1,2,3,4,5]', + published: true, + } + } + $('#cmExportGeoJSON').on( + 'click', + (function (body, isPub) { + return function () { + DrawTool.getFile(body, function (d) { + let geojson = d.geojson + let filename = '' + if (isPub) { + filename = 'CAMP_Latest_Map' + geojson._metadata = d.file + } else { + filename = + d.file[0].file_name + + '_' + + d.file[0].id + + '_' + + d.file[0].file_owner + geojson._metadata = [d.file[0]] + } + + //Genericize it to a map/all type + if (geojson._metadata[0].intent != 'all') { + for ( + var i = 0; + i < geojson.features.length; + i++ + ) { + var newIntent = null + var t = geojson.features[ + i + ].geometry.type.toLowerCase() + if ( + t == 'polygon' || + t == 'multipolygon' + ) + newIntent = 'polygon' + else if ( + t == 'linestring' || + t == 'multilinestring' + ) + newIntent = 'line' + else newIntent = 'point' + geojson.features[ + i + ].properties._.intent = newIntent + } + geojson._metadata[0].intent = 'all' + } + + DrawTool.expandPointprops(geojson) + F_.downloadObject( + geojson, + filename, + '.geojson' + ) + }) + } + })(body, isPub) + ) + + $('#cmExportShp').on( + 'click', + (function (body, isPub) { + return function () { + DrawTool.getFile(body, function (d) { + let geojson = d.geojson + ///geojson._metadata = d.file[0]; + shpwrite.download(geojson, { + folder: + d.file[0].file_name + + '_' + + d.file[0].id + + '_' + + d.file[0].file_owner, + types: {}, + }) + }) + } + })(body, isPub) + ) + + $( + '#drawToolDrawFilesListElemContextMenu #cmToggleLabels' + ).on( + 'click', + (function (isPub) { + return function () { + if (isPub) return + DrawTool.toggleLabels(elm.attr('file_id')) + } + })(isPub) + ) + + var count = 1 //It has to start in one + $('#drawToolDrawFilesListElemContextMenu').on( + 'mouseenter', + function () { + count++ + } + ) + $('#drawToolDrawFilesListElemContextMenu').on( + 'mouseleave', + function () { + count-- + setTimeout(function () { + if (count <= 0) hideContextMenu() + }, 50) + } + ) + function enter() { + count++ + } + function leave() { + count-- + setTimeout(function () { + if (count <= 0) { + hideContextMenu() + elm.off('mouseenter', enter) + elm.off('mouseleave', leave) + } + }, 50) + } + elm.on('mouseenter', enter) + elm.on('mouseleave', leave) + + function hideContextMenu(immediately) { + $('.drawToolDrawFilesListElem').css('background', '') + $('.drawToolIntentColor').css({ + width: '7px', + }) + if (immediately) { + $('#drawToolDrawFilesListElemContextMenu').remove() + } else + $('#drawToolDrawFilesListElemContextMenu').animate( + { + opacity: 0, + }, + 250, + function () { + $( + '#drawToolDrawFilesListElemContextMenu' + ).remove() + } + ) + } + } + ) + + $('.drawToolFileSave').on('click', function () { + var elm = $(this).parent().parent().parent() + + //Only select files you own + if (mmgisglobal.user !== elm.attr('file_owner')) return + + var fileid = elm.attr('file_id') + var filename = elm.find('.drawToolFileNameInput').val() + var body = { + id: fileid, + file_name: filename, + file_description: elm.find('.drawToolFileDesc').val(), + public: + elm + .find('.drawToolFilePublic') + .find('i') + .attr('public') == '1' + ? 1 + : 0, + } + + DrawTool.changeFile(body, function (d) { + elm.find('.drawToolFileName').text(filename) + var files_i = F_.objectArrayIndexOfKeyWithValue( + DrawTool.files, + 'id', + parseInt(fileid) + ) + if (files_i !== -1) + DrawTool.files[files_i].file_name = filename + + DrawTool.getFiles(function () { + DrawTool.populateFiles() + }) + }) + }) + + $('.drawToolFilePublic').off('click') + $('.drawToolFilePublic').on('click', function () { + var icon = $(this).find('i') + var public = icon.attr('public') + if (public == '0') { + icon.removeClass('mdi-shield') + icon.addClass('mdi-shield-outline') + icon.attr('public', '1') + $(this).find('.drawToolFilePublicName').text('Public') + } else { + icon.removeClass('mdi-shield-outline') + icon.addClass('mdi-shield') + icon.attr('public', '0') + $(this).find('.drawToolFilePublicName').text('Private') + } + }) + + $('.drawToolFileDelete').off('click') + $('.drawToolFileDelete').on('click', function () { + var filenameToDelete = $(this) + .parent() + .parent() + .parent() + .attr('file_name') + var response = prompt( + 'Are you sure you want to delete ' + + filenameToDelete + + ' (Y/N)?' + ) + response = response.toLowerCase() + if (!(response == 'yes' || response == 'y')) return + + var body = { + id: $(this).parent().parent().parent().attr('file_id'), + } + var layerId = 'DrawTool_' + body.id + + DrawTool.removeFile( + body, + (function (layerId, id) { + return function (d) { + //Remove each feature in its group + if (L_.layersGroup.hasOwnProperty(layerId)) { + for ( + var i = 0; + i < L_.layersGroup[layerId].length; + i++ + ) { + Map_.rmNotNull(L_.layersGroup[layerId][i]) + //And from the Globe + Globe_.removeVectorTileLayer( + 'camptool_' + layerId + '_' + i + ) + } + } + //Remove from filesOn + let f = DrawTool.filesOn.indexOf(parseInt(id)) + if (f != -1) DrawTool.filesOn.splice(f, 1) + } + })(layerId, body.id) + ) + + $(this).parent().parent().parent().remove() + }) + + $('.drawToolFileEdit').off('click') + $('.drawToolFileEdit').on('click', function () { + var elm = $(this).parent().parent().parent() + + //Only select files you own + if (mmgisglobal.user !== elm.attr('file_owner')) return + + var top = elm.offset().top + 22 + 'px' + elm = elm.find('.drawToolFileEditOn') + elm.css('top', top) + var display = elm.css('display') + $('.drawToolFileEditOn').css('display', 'none') + if (display == 'none') elm.css('display', 'inherit') + return false + }) + + //Highlight layer if on + $('.drawToolDrawFilesListElem').off('mouseenter') + $('.drawToolDrawFilesListElem').on('mouseenter', function () { + $(this).find('.drawToolFileEdit').addClass('shown') + var fileId = parseInt($(this).attr('file_id')) + var l = L_.layersGroup['DrawTool_' + fileId] + if (!l) return + for (var i = 0; i < l.length; i++) { + if (l[i] != null) { + if (typeof l[i].setStyle === 'function') + l[i].setStyle({ color: '#7fff00' }) + else if (l[i].hasOwnProperty('_layers')) { + //Arrow + var layers = l[i]._layers + layers[Object.keys(layers)[0]].setStyle({ + color: '#7fff00', + }) + layers[Object.keys(layers)[1]].setStyle({ + color: '#7fff00', + }) + } else + $('.DrawToolAnnotation_' + fileId).addClass( + 'highlight' + ) + } + } + }) + $('.drawToolDrawFilesListElem').off('mouseleave') + $('.drawToolDrawFilesListElem').on('mouseleave', function () { + $(this).find('.drawToolFileEdit').removeClass('shown') + var fileId = parseInt($(this).attr('file_id')) + var l = L_.layersGroup['DrawTool_' + fileId] + if (!l) return + for (var i = 0; i < l.length; i++) { + var style + if (l[i] != null) { + if ( + !l[i].hasOwnProperty('feature') && + l[i].hasOwnProperty('_layers') + ) + style = + l[i]._layers[Object.keys(l[i]._layers)[0]] + .feature.properties.style + else style = l[i].feature.properties.style + + if (typeof l[i].setStyle === 'function') + l[i].setStyle(style) + else if (l[i].hasOwnProperty('_layers')) { + //Arrow + var layers = l[i]._layers + layers[Object.keys(layers)[0]].setStyle({ + color: style.color, + }) + layers[Object.keys(layers)[1]].setStyle({ + color: style.color, + }) + } else + $('.DrawToolAnnotation_' + fileId).removeClass( + 'highlight' + ) + } + } + }) + //Select file + $('.drawToolFileSelector').off('click') + $('.drawToolFileSelector').on('click', function () { + //Only select files you own + var fileFromId = DrawTool.getFileObjectWithId( + $(this).attr('file_id') + ) + if ( + mmgisglobal.user !== $(this).attr('file_owner') && + fileFromId && + F_.diff(fileFromId.file_owner_group, DrawTool.userGroups) + .length == 0 + ) + return + + var checkbox = $(this).parent().find('.drawToolFileCheckbox') + $('.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) + } + + DrawTool.currentFileId = parseInt(checkbox.attr('file_id')) + if (DrawTool.filesOn.indexOf(DrawTool.currentFileId) == -1) + checkbox.click() + }) + + //Visible File + $('.drawToolFileCheckbox').off('click') + $('.drawToolFileCheckbox').on('click', DrawTool.toggleFile) + }, + refreshFile: function ( + id, + time, + populateShapesAfter, + selectedFeatureIds, + asPublished, + cb + ) { + let parsedId = + typeof parseInt(id) === 'number' && !Array.isArray(id) + ? parseInt(id) + : 'master' + //Can't refresh what isn't there + if ( + parsedId != 'master' && + L_.layersGroup.hasOwnProperty('DrawTool_' + parsedId) == false + ) + return + + var body = { + id: JSON.stringify(id), + time: time, + } + if (asPublished == true) body.published = true + + DrawTool.getFile( + body, + (function (index, selectedFeatureIds) { + return function (data) { + var layerId = 'DrawTool_' + index + //Remove it first + if (L_.layersGroup.hasOwnProperty(layerId)) { + for ( + var i = 0; + i < L_.layersGroup[layerId].length; + i++ + ) { + //Close any popups/labels + var popupLayer = L_.layersGroup[layerId][i] + DrawTool.removePopupsFromLayer(popupLayer) + + Map_.rmNotNull(L_.layersGroup[layerId][i]) + L_.layersGroup[layerId][i] = null + //And from the Globe + Globe_.removeVectorTileLayer( + 'camptool_' + layerId + '_' + i + ) + } + } + + var features = data.geojson.features + for (var i = 0; i < features.length; i++) { + if ( + !features[i].properties.hasOwnProperty('style') + ) { + features[i].properties.style = F_.clone( + DrawTool.defaultStyle + ) + if ( + features[i].geometry.type.toLowerCase() == + 'point' + ) + features[i].properties.style.fillOpacity = 1 + } + if (features[i].properties.arrow === true) { + var c = features[i].geometry.coordinates + var start = new L.LatLng(c[0][1], c[0][0]) + var end = new L.LatLng(c[1][1], c[1][0]) + + DrawTool.addArrowToMap( + layerId, + start, + end, + features[i].properties.style, + features[i] + ) + } else if ( + features[i].properties.annotation === true + ) { + //Remove previous annotation if any + $( + '#DrawToolAnnotation_' + + id + + '_' + + features[i].properties._.id + ) + .parent() + .parent() + .parent() + .parent() + .remove() + + var s = features[i].properties.style + var styleString = + (s.color + ? 'text-shadow: ' + + F_.getTextShadowString( + s.color, + s.strokeOpacity, + s.weight + ) + + '; ' + : '') + + (s.fillColor + ? 'color: ' + s.fillColor + '; ' + : '') + + (s.fontSize + ? 'font-size: ' + s.fontSize + '; ' + : '') + L_.layersGroup[layerId].push( + L.popup({ + className: 'leaflet-popup-annotation', + closeButton: false, + autoClose: false, + closeOnEscapeKey: false, + closeOnClick: false, + autoPan: false, + offset: new L.point(0, 3), + }) + .setLatLng( + new L.LatLng( + features[ + i + ].geometry.coordinates[1], + features[ + i + ].geometry.coordinates[0] + ) + ) + .setContent( + '
' + + "
" + + '
' + + '
' + ) + .addTo(Map_.map) + ) + L_.layersGroup[layerId][ + L_.layersGroup[layerId].length - 1 + ].feature = features[i] + $( + '#DrawToolAnnotation_' + + id + + '_' + + features[i].properties._.id + ).text(features[i].properties.name) + + DrawTool.refreshNoteEvents() + } else if (features[i].geometry.type === 'Point') { + L_.layersGroup[layerId].push( + L.circleMarker( + new L.LatLng( + features[i].geometry.coordinates[1], + features[i].geometry.coordinates[0] + ), + features[i].properties.style + ).addTo(Map_.map) + ) + L_.layersGroup[layerId][ + L_.layersGroup[layerId].length - 1 + ].feature = features[i] + } else { + L_.layersGroup[layerId].push( + L.geoJson( + { + type: 'FeatureCollection', + features: [features[i]], + }, + { + style: function (feature) { + return feature.properties.style + }, + } + ).addTo(Map_.map) + ) + } + + if ( + features[i].properties.annotation !== true && + features[i].properties.arrow !== true + ) { + var last = L_.layersGroup[layerId].length - 1 + var llast = L_.layersGroup[layerId][last] + var layer + + if (llast.hasOwnProperty('_layers')) + layer = + llast._layers[ + Object.keys(llast._layers)[0] + ] + else { + layer = Object.assign({}, llast) + layer.feature.geometry.coordinates = [ + layer.feature.geometry.coordinates[1], + layer.feature.geometry.coordinates[0], + ] + } + + Globe_.addVectorTileLayer( + { + id: 'camptool_' + layerId + '_' + last, + on: true, + layers: [layer], + }, + true + ) + } + } + if (populateShapesAfter) + DrawTool.populateShapes(id, selectedFeatureIds) + + DrawTool.maintainLayerOrder() + + DrawTool.refreshMasterCheckbox() + + //Keep labels on if they were on before + let indexOf = DrawTool.labelsOn.indexOf(index + '') + if (indexOf != -1) { + DrawTool.labelsOn.splice(indexOf, 1) + DrawTool.toggleLabels(index + '') + } + + if (typeof cb === 'function') { + cb() + } + } + })(parsedId, selectedFeatureIds) + ) + }, + /** + * Adds or removes a file + * if fileId is not define, expects an element with a file_id attr + * @param {int} fileId *optional* + * @param {'on' || 'off'} forceToggle *optional* + */ + toggleFile: function ( + fileId, + forceToggle, + populateShapesAfter, + asPublished + ) { + var argumented = typeof fileId === 'number' || fileId === 'master' + + var id = parseInt($(this).attr('file_id')) + if (argumented) id = fileId + + var layerId = 'DrawTool_' + id + + if ( + forceToggle == 'off' || + (forceToggle != 'on' && DrawTool.filesOn.indexOf(id) != -1) + ) { + //OFF + // Don't allow turning files off that are being drawn in + if (DrawTool.currentFileId == id) return + + DrawTool.filesOn = DrawTool.filesOn.filter(function (v) { + return v !== id + }) + + if (!argumented) { + DrawTool.populateShapes() + //Change icon + $(this).removeClass('on') + $(this).parent().parent().parent().removeClass('on') + } + //Remove each feature in its group + if (L_.layersGroup.hasOwnProperty(layerId)) { + for (var i = 0; i < L_.layersGroup[layerId].length; i++) { + Map_.rmNotNull(L_.layersGroup[layerId][i]) + //And from the Globe + Globe_.removeVectorTileLayer( + 'camptool_' + layerId + '_' + i + ) + } + } + + DrawTool.refreshMasterCheckbox() + } else { + //ON + DrawTool.filesOn.push(id) + + if (!argumented) { + //Change icon + $(this).addClass('on') + $(this).parent().parent().parent().addClass('on') + } + //Get the file if we don't already have it + L_.layersGroup[layerId] = [] + DrawTool.refreshFile( + id == 'master' ? DrawTool.masterFileIds : id, + null, + populateShapesAfter != null + ? populateShapesAfter + : !argumented, + null, + asPublished + ) + } + }, + toggleLabels: function (file_id) { + var l = L_.layersGroup['DrawTool_' + file_id] + let indexOf = DrawTool.labelsOn.indexOf(file_id) + var isOn = indexOf != -1 + if (isOn) DrawTool.labelsOn.splice(indexOf, 1) + else DrawTool.labelsOn.push(file_id) + + if (l) { + for (var i = 0; i < l.length; i++) { + if (l[i] != null) { + if (l[i]._layers) { + var p = l[i]._layers[Object.keys(l[i]._layers)[0]] + if (isOn) p.closePopup() + else p.openPopup() + } else if (l[i].feature.properties.annotation != true) { + var p = l[i] + if (isOn) p.closePopup() + else p.openPopup() + } + } + } + } + }, + maintainLayerOrder: function () { + for (var i = 0; i < DrawTool.intentOrder.length; i++) { + for (var j = 0; j < DrawTool.filesOn.length; j++) { + var file = DrawTool.getFileObjectWithId(DrawTool.filesOn[j]) + if (file.intent === DrawTool.intentOrder[i]) { + for (var e of L_.layersGroup[ + 'DrawTool_' + DrawTool.filesOn[j] + ]) + if ( + e != null && + typeof e.bringToFront === 'function' + ) + e.bringToFront() + } + } + } + }, + removePopupsFromLayer: function (popupLayer) { + if (popupLayer != null) { + if (popupLayer._layers) { + var p = + popupLayer._layers[Object.keys(popupLayer._layers)[0]] + + let wasOpen = p.getPopup() ? p.getPopup().isOpen() : false + if (wasOpen) return wasOpen + p.closePopup() + p.unbindPopup() + } else if ( + !popupLayer.feature || + popupLayer.feature.properties.annotation != true + ) { + let wasOpen = popupLayer.getPopup() + ? popupLayer.getPopup().isOpen() + : false + if (wasOpen) return wasOpen + popupLayer.closePopup() + popupLayer.unbindPopup() + } + } + return false + }, + refreshNoteEvents() { + $('.drawToolAnnotation').off('mouseover') + $('.drawToolAnnotation').on('mouseover', function () { + var layer = 'DrawTool_' + $(this).attr('layer') + var index = $(this).attr('index') + $('.drawToolShapeLi').removeClass('hovered') + $('.drawToolShapeLi .drawToolShapeLiItem').mouseleave() + $('#drawToolShapeLiItem_' + layer + '_' + index).addClass( + 'hovered' + ) + $( + '#drawToolShapeLiItem_' + + layer + + '_' + + index + + ' .drawToolShapeLiItem' + ).mouseenter() + }) + $('.drawToolAnnotation').off('mouseout') + $('.drawToolAnnotation').on('mouseout', function () { + $('.drawToolShapeLi').removeClass('hovered') + $('.drawToolShapeLi .drawToolShapeLiItem').mouseleave() + }) + $('.drawToolAnnotation').off('click') + $('.drawToolAnnotation').on('click', function () { + var layer = 'DrawTool_' + $(this).attr('layer') + var index = $(this).attr('index') + var shape = L_.layersGroup[layer][index] + if (!mmgisglobal.shiftDown) { + if (typeof shape.getBounds === 'function') + Map_.map.fitBounds(shape.getBounds()) + else Map_.map.panTo(shape._latlng) + } + + shape.fireEvent('click') + }) + }, + refreshMasterCheckbox: function () { + //Have master file checkbox on only when all master files are on too + var masterCheckShouldBeOn = true + for (var f in DrawTool.files) { + if ( + DrawTool.files[f].is_master && + DrawTool.filesOn.indexOf(DrawTool.files[f].id) == -1 + ) { + masterCheckShouldBeOn = false + break + } + } + if (masterCheckShouldBeOn) + $('.drawToolFileMasterCheckbox').addClass('on') + else $('.drawToolFileMasterCheckbox').removeClass('on') + }, + } + + return Files +}) diff --git a/scripts/essence/Tools/Draw/DrawTool_History.js b/scripts/essence/Tools/Draw/DrawTool_History.js index 2311a8a8..ffa67ecc 100644 --- a/scripts/essence/Tools/Draw/DrawTool_History.js +++ b/scripts/essence/Tools/Draw/DrawTool_History.js @@ -1,138 +1,142 @@ -define([ - 'jquery', - 'd3', - 'Formulae_', - 'Layers_', - 'Globe_', - 'Map_', - 'Viewer_', - 'UserInterface_', - 'CursorInfo', - 'leafletDraw', - 'turf', - 'leafletPolylineDecorator', - 'leafletSnap', - 'colorPicker', - 'shp', - 'shpwrite', -], function( - $, - d3, - F_, - L_, - Globe_, - Map_, - Viewer_, - UserInterface_, - CursorInfo, - leafletDraw, - turf, - leafletPolylineDecorator, - leafletSnap, - colorPicker, - shp, - shpwrite -) { - var DrawTool = null - var History = { - init: function(tool) { - DrawTool = tool - DrawTool.populateHistory = History.populateHistory - }, - populateHistory() { - clearInterval(DrawTool.historyTimeout) - DrawTool.historyTimeout = setInterval(function() { - $('#drawToolHistoryTime').val( - F_.timestampToDate(Date.now() / 1000) - ) - }, 1000) - - $('#drawToolHistorySequenceList ul *').remove() - - var file = DrawTool.getFileObjectWithId(DrawTool.currentFileId) - if (file == null) { - $('#drawToolHistoryFile') - .css({ background: '#ac0404', color: 'white' }) - .text('No File Selected!') - return - } - $('#drawToolHistoryFile') - .css({ background: 'white', color: 'black' }) - .text(file.file_name) - - DrawTool.getHistoryFile( - file.id, - function(history) { - DrawTool.currentHistory = history - DrawTool.timeInHistory = F_.timestampToDate( - Date.now() / 1000 - ) - for (var i = 0; i < history.length; i++) { - addHistoryToList(history[i]) - } - if (history[i - 1]) - addHistoryToList({ - time: history[i - 1].time - 1, - message: 'Begin', - }) - $('#drawToolHistorySequenceList > ul > li').on( - 'click', - function() { - var time = parseInt( - $(this) - .find('div') - .attr('time') - ) - DrawTool.timeInHistory = time - $(this) - .prevAll() - .addClass('inactive') - $(this).removeClass('inactive') - $(this) - .nextAll() - .removeClass('inactive') - $('#drawToolHistoryTime').val( - F_.timestampToDate(time / 1000) - ) - - var toUndo = $(this).prevAll().length - $('#drawToolHistorySave').removeClass('active') - if (toUndo > 0) - $('#drawToolHistorySave').addClass('active') - $('#drawToolHistorySave').text( - toUndo === 0 - ? 'Nothing to Undo' - : 'Undo ' + toUndo + ' Actions' - ) - - clearInterval(DrawTool.historyTimeout) - DrawTool.refreshFile(file.id, time, false) - } - ) - $('#drawToolHistoryNow').on('click', function() { - $( - '#drawToolHistorySequenceList > ul > li:first-child' - ).click() - }) - }, - function() {} - ) - - function addHistoryToList(h) { - // prettier-ignore - var markup = [ - "
", - "" + h.message + "", - "" + F_.timestampToDate(h.time / 1000) + "", - "
" - ].join('\n'); - - d3.select('#drawToolHistorySequenceList ul') - .append('li') - .html(markup) - } - }, - } - - return History -}) +define([ + 'jquery', + 'd3', + 'Formulae_', + 'Layers_', + 'Globe_', + 'Map_', + 'Viewer_', + 'UserInterface_', + 'CursorInfo', + 'leafletDraw', + 'turf', + 'leafletPolylineDecorator', + 'leafletSnap', + 'colorPicker', + 'shp', + 'shpwrite', +], function ( + $, + d3, + F_, + L_, + Globe_, + Map_, + Viewer_, + UserInterface_, + CursorInfo, + leafletDraw, + turf, + leafletPolylineDecorator, + leafletSnap, + colorPicker, + shp, + shpwrite +) { + var DrawTool = null + var History = { + init: function (tool) { + DrawTool = tool + DrawTool.populateHistory = History.populateHistory + }, + populateHistory() { + clearInterval(DrawTool.historyTimeout) + DrawTool.historyTimeout = setInterval(function () { + $('#drawToolHistoryTime').val( + F_.timestampToDate(Date.now() / 1000) + ) + }, 1000) + + $('#drawToolHistorySequenceList ul *').remove() + + var file = DrawTool.getFileObjectWithId(DrawTool.currentFileId) + if (file == null) { + $('#drawToolHistoryFile') + .css({ background: '#ac0404', color: 'white' }) + .text('No File Selected!') + return + } + $('#drawToolHistoryFile') + .css({ background: 'white', color: 'black' }) + .text(file.file_name) + + DrawTool.getHistoryFile( + file.id, + function (history) { + DrawTool.currentHistory = history + DrawTool.timeInHistory = F_.timestampToDate( + Date.now() / 1000 + ) + for (var i = 0; i < history.length; i++) { + addHistoryToList(history[i]) + } + if (history[i - 1]) + addHistoryToList({ + time: history[i - 1].time - 1, + message: 'Begin', + }) + + var toUndo = $(this).prevAll().length + $('#drawToolHistorySave').removeClass('active') + if (toUndo > 0) $('#drawToolHistorySave').addClass('active') + $('#drawToolHistorySave').text( + toUndo === 0 + ? 'Nothing to Undo' + : 'Undo ' + toUndo + ' Actions' + ) + + $('#drawToolHistorySequenceList > ul > li').on( + 'click', + function () { + var time = parseInt( + $(this).find('div').attr('time') + ) + DrawTool.timeInHistory = time + $(this).prevAll().addClass('inactive') + $(this).removeClass('inactive') + $(this).nextAll().removeClass('inactive') + $('#drawToolHistoryTime').val( + F_.timestampToDate(time / 1000) + ) + + var toUndo = $(this).prevAll().length + $('#drawToolHistorySave').removeClass('active') + if (toUndo > 0) + $('#drawToolHistorySave').addClass('active') + $('#drawToolHistorySave').text( + toUndo === 0 + ? 'Nothing to Undo' + : 'Undo ' + toUndo + ' Actions' + ) + + clearInterval(DrawTool.historyTimeout) + DrawTool.refreshFile(file.id, time, false) + } + ) + $('#drawToolHistoryNow').on('click', function () { + $( + '#drawToolHistorySequenceList > ul > li:first-child' + ).click() + }) + }, + function () {} + ) + + function addHistoryToList(h) { + // prettier-ignore + var markup = [ + "
", + "" + h.message + "", + "" + F_.timestampToDate(h.time / 1000) + "", + "
" + ].join('\n'); + + d3.select('#drawToolHistorySequenceList ul') + .append('li') + .html(markup) + } + }, + } + + return History +}) diff --git a/scripts/essence/Tools/Draw/DrawTool_Shapes.js b/scripts/essence/Tools/Draw/DrawTool_Shapes.js index 414fcd1d..6e6e6103 100644 --- a/scripts/essence/Tools/Draw/DrawTool_Shapes.js +++ b/scripts/essence/Tools/Draw/DrawTool_Shapes.js @@ -1,652 +1,675 @@ -define([ - 'jquery', - 'd3', - 'Formulae_', - 'Layers_', - 'Globe_', - 'Map_', - 'Viewer_', - 'UserInterface_', - 'CursorInfo', - 'leafletDraw', - 'turf', - 'leafletPolylineDecorator', - 'leafletSnap', - 'colorPicker', - 'shp', - 'shpwrite', -], function( - $, - d3, - F_, - L_, - Globe_, - Map_, - Viewer_, - UserInterface_, - CursorInfo, - leafletDraw, - turf, - leafletPolylineDecorator, - leafletSnap, - colorPicker, - shp, - shpwrite -) { - var DrawTool = null - var Shapes = { - init: function(tool) { - DrawTool = tool - DrawTool.populateShapes = Shapes.populateShapes - DrawTool.updateCopyTo = Shapes.updateCopyTo - }, - populateShapes: function(fileId, selectedFeatureIds) { - //If we get an array of fileIds, split them - if (Array.isArray(fileId)) { - /* - for (var i = 0; i < fileId.length; i++) { - DrawTool.populateShapes(fileId[i], selectedFeatureIds) - } - return - */ - fileId = 'master' - } - - //Find all the active shapes on the last list so that repopulating doesn't change this - var stillActive = [] - $('#drawToolShapesFeaturesList .drawToolShapeLi.active').each( - function() { - stillActive.push($(this).attr('id')) - } - ) - - //Populate shapes - $('#drawToolShapesFeaturesList *').remove() - for (var l in L_.layersGroup) { - var s = l.split('_') - var onId = s[1] != 'master' ? parseInt(s[1]) : s[1] - if ( - s[0] == 'DrawTool' && - DrawTool.filesOn.indexOf(onId) != -1 - ) { - for (var i = 0; i < L_.layersGroup[l].length; i++) { - var file = DrawTool.getFileObjectWithId(s[1]) - addShapeToList(L_.layersGroup[l][i], file, l, i, s[1]) - } - } - } - - $('#drawToolShapesFilterClear').off('click') - $('#drawToolShapesFilterClear').on('click', function() { - $('#drawToolShapesFilter').val('') - shapeFilter() - }) - $('#drawToolShapesFilter').off('input') - $('#drawToolShapesFilter').on('input', shapeFilter) - shapeFilter() - function shapeFilter() { - //filter over name, intent and id for now - var on = 0 - var off = 0 - var v = $('#drawToolShapesFilter').val() - - if (v != null && v != '') v = v.toLowerCase() - else { - //not filtering - $('.drawToolShapeLi').css('display', 'list-item') - $('#drawToolShapesFilterCount').text('') - $('#drawToolShapesFilterCount').css('padding-right', '0px') - return - } - - $('.drawToolShapeLi').each(function() { - var l = - L_.layersGroup[$(this).attr('layer')][ - $(this).attr('index') - ] - if (l.hasOwnProperty('_layers')) - l = l._layers[Object.keys(l._layers)[0]] - - var show = false - if ( - l.feature.properties.name && - l.feature.properties.name.toLowerCase().indexOf(v) != -1 - ) - show = true - if ( - l.feature.properties._.intent && - l.feature.properties._.intent - .toLowerCase() - .indexOf(v) != -1 - ) - show = true - if (l.feature.properties._.id.toString().indexOf(v) != -1) - show = true - - if (show) { - $(this).css('display', 'list-item') - on++ - } else { - $(this).css('display', 'none') - off++ - } - }) - - $('#drawToolShapesFilterCount').text(on + '/' + (on + off)) - $('#drawToolShapesFilterCount').css('padding-right', '7px') - } - - function addShapeToList(shape, file, layer, index, layerId) { - if (shape == null) return - - var f = shape - - if ( - !shape.hasOwnProperty('feature') && - shape.hasOwnProperty('_layers') - ) - //if it's a non point layer - f = shape._layers[Object.keys(shape._layers)[0]] - - var properties = f.feature.properties - - if (f.hasOwnProperty('_layers')) f = f._layers - else f = { layer: f } - - var shieldState = '' - if (file.public == 1) shieldState = '-outline' - - var ownedByUser = false - if ( - mmgisglobal.user == file.file_owner || - (file.file_owner_group && - F_.diff(file.file_owner_group, DrawTool.userGroups) - .length > 0) - ) - ownedByUser = true - - // prettier-ignore - var markup = [ - "
", - "
", - "
", - "
", - "
" + properties.name + "
", - "
", - "
", - "
", - "", - "", - "
", - "
" - ].join('\n'); - - let shapeLiId = 'drawToolShapeLiItem_' + layer + '_' + index - let activeClass = '' - if (stillActive.indexOf(shapeLiId) != -1) - activeClass = ' active' - - d3.select('#drawToolShapesFeaturesList') - .append('li') - .attr('id', 'drawToolShapeLiItem_' + layer + '_' + index) - .attr('class', 'drawToolShapeLi' + activeClass) - .attr('layer', layer) - .attr('layer_id', layerId) - .attr('index', index) - .attr('file_id', file.id) - .attr('file_owner', file.file_owner) - .attr('file_name', file.file_name) - .attr('intent', properties._.intent) - .attr('shape_id', properties._.id) - .html(markup) - - $('#drawToolShapeLiItem_' + layer + '_' + index) - .find('.drawToolShapeLiItemB') - .attr('title', properties.name || 'No Name') - .text(properties.name || 'No Name') - - for (var elayer in f) { - var e = f[elayer] - - var pUpfeature = e.feature - if (e.feature == null && shape.feature != null) - pUpfeature = shape.feature - // create popup contents - var customPopup = - "
" + - pUpfeature.properties.name + - '
' - - // specify popup options - var customOptions = { - autoPan: false, - autoClose: false, - closeButton: false, - closeOnClick: false, - //offset: L.Point(0,0), - className: 'drawToolLabel', - } - - let p = DrawTool.removePopupsFromLayer(e) - if (!p) e.bindPopup(customPopup, customOptions) - - e.off('mousemove') - e.on( - 'mousemove', - (function(layer, index) { - return function(event) { - if ( - DrawTool.contextMenuLayer && - DrawTool.contextMenuLayer.dragging - ) - return - var l = L_.layersGroup[layer][index] - var p - if ( - !l.hasOwnProperty('feature') && - l.hasOwnProperty('_layers') - ) - p = - l._layers[Object.keys(l._layers)[0]] - .feature.properties - else p = l.feature.properties - var centerPx = event.containerPoint - - $('#drawToolMouseoverText').text(p.name) - $('#drawToolMouseoverText').addClass('active') - $('#drawToolMouseoverText').css({ - top: centerPx.y, - left: centerPx.x + 280, - }) - } - })(layer, index) - ) - e.off('mouseover') - e.on( - 'mouseover', - (function(layer, index) { - return function() { - if ( - DrawTool.contextMenuLayer && - DrawTool.contextMenuLayer.dragging - ) - return - $('.drawToolShapeLi').removeClass('hovered') - $( - '.drawToolShapeLi .drawToolShapeLiItem' - ).mouseleave() - $( - '#drawToolShapeLiItem_' + - layer + - '_' + - index - ).addClass('hovered') - $( - '#drawToolShapeLiItem_' + - layer + - '_' + - index + - ' .drawToolShapeLiItem' - ).mouseenter() - } - })(layer, index) - ) - e.off('mouseout') - e.on('mouseout', function() { - if ( - DrawTool.contextMenuLayer && - DrawTool.contextMenuLayer.dragging - ) - return - $('.drawToolShapeLi').removeClass('hovered') - $('.drawToolShapeLi .drawToolShapeLiItem').mouseleave() - $('#drawToolMouseoverText').removeClass('active') - }) - e.off('click') - e.on( - 'click', - (function(layer, index, fileid) { - return function(event) { - if (DrawTool.activeContent != 'shapes') - DrawTool.showContent('shapes') - - var ctrl = mmgisglobal.ctrlDown - var elm = $( - '#drawToolShapeLiItem_' + - layer + - '_' + - index - ) - var intent = elm.attr('intent') - - //No mismatched intents - if ( - ctrl && - DrawTool.lastShapeIntent !== intent && - DrawTool.contextMenuLayer != null && - DrawTool.contextMenuLayers.length > 0 - ) { - CursorInfo.update( - 'Grouped shapes must share intent.', - 6000, - true, - { x: 268, y: 6 }, - '#e9ff26', - 'black' - ) - return - } - - //if we click on a different feature we ignore drag - //This is so features can be selected after point drags - if ( - DrawTool.contextMenuLayer && - !( - DrawTool.lastContextLayerIndexFileId - .layer == layer && - DrawTool.lastContextLayerIndexFileId - .index == index && - DrawTool.lastContextLayerIndexFileId - .fileid == fileid - ) - ) { - DrawTool.contextMenuLayer.justDragged = false - } - var liIndex = $( - '#drawToolShapesFeaturesList li' - ).index(elm) - if ( - mmgisglobal.shiftDown && - DrawTool.lastShapeIndex != null - ) { - var curI = liIndex - var curLi = elm - while (curI != DrawTool.lastShapeIndex) { - if ( - !curLi.hasClass('active') && - DrawTool.lastShapeIntent === - curLi.attr('intent') - ) { - curLi.addClass('active') - curLi - .find( - '.drawToolShapeLiItemCheck' - ) - .addClass('checked') - DrawTool.showContextMenu( - 0, - 0, - curLi.attr('layer'), - curLi.attr('index'), - curLi.attr('file_id'), - true - ) - } - if (liIndex > DrawTool.lastShapeIndex) { - //activate upwards - curI-- - curLi = curLi.prev() - } else { - //downwards - curI++ - curLi = curLi.next() - } - } - return - } - DrawTool.lastShapeIndex = liIndex - //Clear all active if ctrl not held or intent types differ - if ( - !ctrl || - (DrawTool.lastShapeIntent !== null && - DrawTool.lastShapeIntent !== intent) - ) { - $('.drawToolShapeLi').removeClass('active') - $('.drawToolShapeLi') - .find('.drawToolShapeLiItemCheck') - .removeClass('checked') - } - elm.addClass('active') - elm.find('.drawToolShapeLiItemCheck').addClass( - 'checked' - ) - - var copyToIntent = intent - if ( - copyToIntent == 'polygon' || - copyToIntent == 'line' || - copyToIntent == 'point' || - copyToIntent == 'text' || - copyToIntent == 'arrow' - ) - copyToIntent = 'all' - - DrawTool.updateCopyTo(copyToIntent, intent) - - DrawTool.lastShapeIntent = intent - - DrawTool.showContextMenu( - 0, - 0, - layer, - index, - fileid, - ctrl - ) - } - })(layer, index, file.id) - ) - } - } - - //Hover li item to highlight shape - $('.drawToolShapeLi').on('mouseenter', function() { - if (DrawTool.activeContent === 'history') return - var layer = $(this) - .find('.drawToolShapeLiItem') - .attr('layer') - var index = $(this) - .find('.drawToolShapeLiItem') - .attr('index') - - if (typeof L_.layersGroup[layer][index].setStyle === 'function') - L_.layersGroup[layer][index].setStyle({ color: '#7fff00' }) - else if ( - L_.layersGroup[layer][index].hasOwnProperty('_layers') - ) { - //Arrow - var layers = L_.layersGroup[layer][index]._layers - layers[Object.keys(layers)[0]].setStyle({ - color: '#7fff00', - }) - layers[Object.keys(layers)[1]].setStyle({ - color: '#7fff00', - }) - } else { - $( - '#DrawToolAnnotation_' + - $(this).attr('layer_id') + - '_' + - $(this).attr('shape_id') - ).addClass('highlight') - } - }) - $('.drawToolShapeLi').on('mouseleave', function() { - if (DrawTool.activeContent === 'history') return - var layer = $(this) - .find('.drawToolShapeLiItem') - .attr('layer') - var index = $(this) - .find('.drawToolShapeLiItem') - .attr('index') - var shapeId = $(this).attr('shape_id') - var shape = L_.layersGroup[layer][index] - - var style - if ( - !shape.hasOwnProperty('feature') && - shape.hasOwnProperty('_layers') - ) - style = - shape._layers[Object.keys(shape._layers)[0]].feature - .properties.style - else style = shape.feature.properties.style - if (style == null) style = shape.options - - if (typeof shape.setStyle === 'function') - shape.setStyle({ color: style.color }) - else if (shape.hasOwnProperty('_layers')) { - //Arrow - var layers = shape._layers - layers[Object.keys(layers)[0]].setStyle({ - color: style.color, - }) - layers[Object.keys(layers)[1]].setStyle({ - color: style.color, - }) - } else - $( - '#DrawToolAnnotation_' + - $(this).attr('layer_id') + - '_' + - $(this).attr('shape_id') - ).removeClass('highlight') - }) - $('.drawToolShapeLiItem').on('click', function(e) { - var layer = $(this).attr('layer') - var index = $(this).attr('index') - var shape = L_.layersGroup[layer][index] - if (!mmgisglobal.shiftDown) { - if (typeof shape.getBounds === 'function') - Map_.map.panTo(shape.getBounds().getCenter()) - else if (shape.hasOwnProperty('_latlng')) - Map_.map.panTo(shape._latlng) - else if (shape.hasOwnProperty('_layers')) { - //Arrow - var layers = shape._layers - var pos = DrawTool.getInnerLayers( - layers[Object.keys(layers)[1]], - 3 - ) - if (pos) { - pos = pos._latlngs[1] - Map_.map.panTo(pos) - } - } - } - - if (shape.hasOwnProperty('_layers')) - shape._layers[Object.keys(shape._layers)[0]].fireEvent( - 'click' - ) - else shape.fireEvent('click') - }) - - if ( - fileId != null && - selectedFeatureIds != null && - !( - selectedFeatureIds.length == 1 && - selectedFeatureIds[0] == null - ) - ) { - mmgisglobal.ctrlDown = false - for (var i = 0; i < selectedFeatureIds.length; i++) { - var item = $( - '.drawToolShapeLi[file_id="' + - fileId + - '"][shape_id="' + - selectedFeatureIds[i] + - '"] > div' - ) - if (item.length > 0) { - var shape = - L_.layersGroup[item.attr('layer')][ - item.attr('index') - ] - if (shape.hasOwnProperty('_layers')) - shape._layers[ - Object.keys(shape._layers)[0] - ].fireEvent('click') - else shape.fireEvent('click') - } - mmgisglobal.ctrlDown = true - } - mmgisglobal.ctrlDown = false - } - }, - updateCopyTo: function(intent, subintent) { - //if( intent === DrawTool.lastShapeIntent ) return; - //Update copy to dropdown - var defaultOpt = 'File...' - $('#drawToolShapesCopyDropdown *').remove() - $('#drawToolShapesCopyDropdown').html( - "" - ) - d3.select('#drawToolShapesCopySelect').html( - "' - ) - - if (intent) { - //Don't allow copies to same file - var filenames = [] - $('.drawToolShapeLi').each(function(i, elm) { - if ($(elm).hasClass('active')) { - filenames.push($(elm).attr('file_name')) - } - }) - - for (var i = 0; i < DrawTool.files.length; i++) { - //Lead Files - if ( - DrawTool.userGroups.indexOf('mmgis-group') != -1 && - DrawTool.files[i].file_owner == 'group' && - filenames.indexOf(DrawTool.files[i].file_name) == -1 && - DrawTool.files[i].hidden == '0' && - ((intent == 'all' && - subintent == 'polygon' && - (DrawTool.files[i].intent == 'roi' || - DrawTool.files[i].intent == 'campaign' || - DrawTool.files[i].intent == 'campsite')) || - (intent == 'all' && - subintent == 'line' && - DrawTool.files[i].intent == 'trail') || - (intent == 'all' && - subintent == 'point' && - DrawTool.files[i].intent == 'signpost')) - ) { - d3.select('#drawToolShapesCopySelect') - .append('option') - .attr('value', DrawTool.files[i].id) - .text(DrawTool.files[i].file_name + ' [Lead]') - } else if ( - mmgisglobal.user == DrawTool.files[i].file_owner && - filenames.indexOf(DrawTool.files[i].file_name) == -1 && - intent == DrawTool.files[i].intent && - DrawTool.files[i].hidden == '0' - ) { - d3.select('#drawToolShapesCopySelect') - .append('option') - .attr('value', DrawTool.files[i].id) - .text(DrawTool.files[i].file_name) - } - } - } - - DrawTool.copyFileId = null - $('#drawToolShapesCopySelect').dropdown({ - onChange: function(val, name) { - DrawTool.copyFileId = val - DrawTool.copyFilename = name - }, - }) - - //$( '#drawToolShapesCopySelect' ).dropdown( 'set selected', DrawTool.copyFilename || defaultOpt ); - }, - } - - return Shapes -}) +define([ + 'jquery', + 'd3', + 'Formulae_', + 'Layers_', + 'Globe_', + 'Map_', + 'Viewer_', + 'UserInterface_', + 'CursorInfo', + 'leafletDraw', + 'turf', + 'leafletPolylineDecorator', + 'leafletSnap', + 'colorPicker', + 'shp', + 'shpwrite', +], function ( + $, + d3, + F_, + L_, + Globe_, + Map_, + Viewer_, + UserInterface_, + CursorInfo, + leafletDraw, + turf, + leafletPolylineDecorator, + leafletSnap, + colorPicker, + shp, + shpwrite +) { + var DrawTool = null + var Shapes = { + init: function (tool) { + DrawTool = tool + DrawTool.populateShapes = Shapes.populateShapes + DrawTool.updateCopyTo = Shapes.updateCopyTo + }, + populateShapes: function (fileId, selectedFeatureIds) { + //If we get an array of fileIds, split them + if (Array.isArray(fileId)) { + /* + for (var i = 0; i < fileId.length; i++) { + DrawTool.populateShapes(fileId[i], selectedFeatureIds) + } + return + */ + fileId = 'master' + } + + //Find all the active shapes on the last list so that repopulating doesn't change this + var stillActive = [] + $('#drawToolShapesFeaturesList .drawToolShapeLi.active').each( + function () { + stillActive.push($(this).attr('id')) + } + ) + + //Populate shapes + $('#drawToolShapesFeaturesList *').remove() + for (var l in L_.layersGroup) { + var s = l.split('_') + var onId = s[1] != 'master' ? parseInt(s[1]) : s[1] + if ( + s[0] == 'DrawTool' && + DrawTool.filesOn.indexOf(onId) != -1 + ) { + var file = DrawTool.getFileObjectWithId(s[1]) + if (L_.layersGroup[l].length > 0) + d3.select('#drawToolShapesFeaturesList') + .append('li') + .attr( + 'class', + 'drawToolShapesFeaturesListFileHeader' + ) + .style( + 'background', + DrawTool.categoryStyles[file.intent].color + ) + .style( + 'color', + file.intent == 'campaign' || + file.intent == 'campsite' || + file.intent == 'trail' + ? 'black' + : 'white' + ) + .html(file.file_name) + for (var i = 0; i < L_.layersGroup[l].length; i++) { + addShapeToList(L_.layersGroup[l][i], file, l, i, s[1]) + } + } + } + + $('#drawToolShapesFilterClear').off('click') + $('#drawToolShapesFilterClear').on('click', function () { + $('#drawToolShapesFilter').val('') + shapeFilter() + }) + $('#drawToolShapesFilter').off('input') + $('#drawToolShapesFilter').on('input', shapeFilter) + shapeFilter() + function shapeFilter() { + //filter over name, intent and id for now + var on = 0 + var off = 0 + var v = $('#drawToolShapesFilter').val() + + if (v != null && v != '') v = v.toLowerCase() + else { + //not filtering + $('.drawToolShapeLi').css('display', 'list-item') + $('#drawToolShapesFilterCount').text('') + $('#drawToolShapesFilterCount').css('padding-right', '0px') + return + } + + $('.drawToolShapeLi').each(function () { + var l = + L_.layersGroup[$(this).attr('layer')][ + $(this).attr('index') + ] + if (l.hasOwnProperty('_layers')) + l = l._layers[Object.keys(l._layers)[0]] + + var show = false + if ( + l.feature.properties.name && + l.feature.properties.name.toLowerCase().indexOf(v) != -1 + ) + show = true + if ( + l.feature.properties._.intent && + l.feature.properties._.intent + .toLowerCase() + .indexOf(v) != -1 + ) + show = true + if (l.feature.properties._.id.toString().indexOf(v) != -1) + show = true + + const fileObj = DrawTool.getFileObjectWithId( + l.feature.properties._.file_id + ) + if ( + fileObj && + fileObj.file_name != null && + fileObj.file_name.toLowerCase().indexOf(v) != -1 + ) + show = true + + if (show) { + $(this).css('display', 'list-item') + on++ + } else { + $(this).css('display', 'none') + off++ + } + }) + + $('#drawToolShapesFilterCount').text(on + '/' + (on + off)) + $('#drawToolShapesFilterCount').css('padding-right', '7px') + } + + function addShapeToList(shape, file, layer, index, layerId) { + if (shape == null) return + + var f = shape + + if ( + !shape.hasOwnProperty('feature') && + shape.hasOwnProperty('_layers') + ) + //if it's a non point layer + f = shape._layers[Object.keys(shape._layers)[0]] + + var properties = f.feature.properties + + if (f.hasOwnProperty('_layers')) f = f._layers + else f = { layer: f } + + var shieldState = '' + if (file.public == 1) shieldState = '-outline' + + var ownedByUser = false + if ( + mmgisglobal.user == file.file_owner || + (file.file_owner_group && + F_.diff(file.file_owner_group, DrawTool.userGroups) + .length > 0) + ) + ownedByUser = true + + // prettier-ignore + var markup = [ + "
", + "
", + "
", + "
", + "
" + properties.name + "
", + "
", + "
", + "
", + "", + "", + "
", + "
" + ].join('\n'); + + let shapeLiId = 'drawToolShapeLiItem_' + layer + '_' + index + let activeClass = '' + if (stillActive.indexOf(shapeLiId) != -1) + activeClass = ' active' + + d3.select('#drawToolShapesFeaturesList') + .append('li') + .attr('id', 'drawToolShapeLiItem_' + layer + '_' + index) + .attr('class', 'drawToolShapeLi' + activeClass) + .attr('layer', layer) + .attr('layer_id', layerId) + .attr('index', index) + .attr('file_id', file.id) + .attr('file_owner', file.file_owner) + .attr('file_name', file.file_name) + .attr('intent', properties._.intent) + .attr('shape_id', properties._.id) + .html(markup) + + $('#drawToolShapeLiItem_' + layer + '_' + index) + .find('.drawToolShapeLiItemB') + .attr('title', properties.name || 'No Name') + .text(properties.name || 'No Name') + + for (var elayer in f) { + var e = f[elayer] + + var pUpfeature = e.feature + if (e.feature == null && shape.feature != null) + pUpfeature = shape.feature + // create popup contents + var customPopup = + "
" + + pUpfeature.properties.name + + '
' + + // specify popup options + var customOptions = { + autoPan: false, + autoClose: false, + closeButton: false, + closeOnClick: false, + //offset: L.Point(0,0), + className: 'drawToolLabel', + } + + let p = DrawTool.removePopupsFromLayer(e) + if (!p) e.bindPopup(customPopup, customOptions) + + e.off('mousemove') + e.on( + 'mousemove', + (function (layer, index) { + return function (event) { + if ( + DrawTool.contextMenuLayer && + DrawTool.contextMenuLayer.dragging + ) + return + var l = L_.layersGroup[layer][index] + var p + if ( + !l.hasOwnProperty('feature') && + l.hasOwnProperty('_layers') + ) + p = + l._layers[Object.keys(l._layers)[0]] + .feature.properties + else p = l.feature.properties + var centerPx = event.containerPoint + + $('#drawToolMouseoverText').text(p.name) + $('#drawToolMouseoverText').addClass('active') + $('#drawToolMouseoverText').css({ + top: centerPx.y, + left: centerPx.x + 280, + }) + } + })(layer, index) + ) + e.off('mouseover') + e.on( + 'mouseover', + (function (layer, index) { + return function () { + if ( + DrawTool.contextMenuLayer && + DrawTool.contextMenuLayer.dragging + ) + return + $('.drawToolShapeLi').removeClass('hovered') + $( + '.drawToolShapeLi .drawToolShapeLiItem' + ).mouseleave() + $( + '#drawToolShapeLiItem_' + + layer + + '_' + + index + ).addClass('hovered') + $( + '#drawToolShapeLiItem_' + + layer + + '_' + + index + + ' .drawToolShapeLiItem' + ).mouseenter() + } + })(layer, index) + ) + e.off('mouseout') + e.on('mouseout', function () { + if ( + DrawTool.contextMenuLayer && + DrawTool.contextMenuLayer.dragging + ) + return + $('.drawToolShapeLi').removeClass('hovered') + $('.drawToolShapeLi .drawToolShapeLiItem').mouseleave() + $('#drawToolMouseoverText').removeClass('active') + }) + e.off('click') + e.on( + 'click', + (function (layer, index, fileid) { + return function (event) { + if (DrawTool.activeContent != 'shapes') + DrawTool.showContent('shapes') + + var ctrl = mmgisglobal.ctrlDown + var elm = $( + '#drawToolShapeLiItem_' + + layer + + '_' + + index + ) + var intent = elm.attr('intent') + + //No mismatched intents + if ( + ctrl && + DrawTool.lastShapeIntent !== intent && + DrawTool.contextMenuLayer != null && + DrawTool.contextMenuLayers.length > 0 + ) { + CursorInfo.update( + 'Grouped shapes must share intent.', + 6000, + true, + { x: 268, y: 6 }, + '#e9ff26', + 'black' + ) + return + } + + //if we click on a different feature we ignore drag + //This is so features can be selected after point drags + if ( + DrawTool.contextMenuLayer && + !( + DrawTool.lastContextLayerIndexFileId + .layer == layer && + DrawTool.lastContextLayerIndexFileId + .index == index && + DrawTool.lastContextLayerIndexFileId + .fileid == fileid + ) + ) { + DrawTool.contextMenuLayer.justDragged = false + } + var liIndex = $( + '#drawToolShapesFeaturesList li' + ).index(elm) + if ( + mmgisglobal.shiftDown && + DrawTool.lastShapeIndex != null + ) { + var curI = liIndex + var curLi = elm + while (curI != DrawTool.lastShapeIndex) { + if ( + !curLi.hasClass('active') && + DrawTool.lastShapeIntent === + curLi.attr('intent') + ) { + curLi.addClass('active') + curLi + .find( + '.drawToolShapeLiItemCheck' + ) + .addClass('checked') + DrawTool.showContextMenu( + 0, + 0, + curLi.attr('layer'), + curLi.attr('index'), + curLi.attr('file_id'), + true + ) + } + if (liIndex > DrawTool.lastShapeIndex) { + //activate upwards + curI-- + curLi = curLi.prev() + } else { + //downwards + curI++ + curLi = curLi.next() + } + } + return + } + DrawTool.lastShapeIndex = liIndex + //Clear all active if ctrl not held or intent types differ + if ( + !ctrl || + (DrawTool.lastShapeIntent !== null && + DrawTool.lastShapeIntent !== intent) + ) { + $('.drawToolShapeLi').removeClass('active') + $('.drawToolShapeLi') + .find('.drawToolShapeLiItemCheck') + .removeClass('checked') + } + elm.addClass('active') + elm.find('.drawToolShapeLiItemCheck').addClass( + 'checked' + ) + + var copyToIntent = intent + if ( + copyToIntent == 'polygon' || + copyToIntent == 'line' || + copyToIntent == 'point' || + copyToIntent == 'text' || + copyToIntent == 'arrow' + ) + copyToIntent = 'all' + + DrawTool.updateCopyTo(copyToIntent, intent) + + DrawTool.lastShapeIntent = intent + + DrawTool.showContextMenu( + 0, + 0, + layer, + index, + fileid, + ctrl + ) + } + })(layer, index, file.id) + ) + } + } + + //Hover li item to highlight shape + $('.drawToolShapeLi').on('mouseenter', function () { + if (DrawTool.activeContent === 'history') return + var layer = $(this).find('.drawToolShapeLiItem').attr('layer') + var index = $(this).find('.drawToolShapeLiItem').attr('index') + + if (typeof L_.layersGroup[layer][index].setStyle === 'function') + L_.layersGroup[layer][index].setStyle({ color: '#7fff00' }) + else if ( + L_.layersGroup[layer][index].hasOwnProperty('_layers') + ) { + //Arrow + var layers = L_.layersGroup[layer][index]._layers + layers[Object.keys(layers)[0]].setStyle({ + color: '#7fff00', + }) + layers[Object.keys(layers)[1]].setStyle({ + color: '#7fff00', + }) + } else { + $( + '#DrawToolAnnotation_' + + $(this).attr('layer_id') + + '_' + + $(this).attr('shape_id') + ).addClass('highlight') + } + }) + $('.drawToolShapeLi').on('mouseleave', function () { + if (DrawTool.activeContent === 'history') return + var layer = $(this).find('.drawToolShapeLiItem').attr('layer') + var index = $(this).find('.drawToolShapeLiItem').attr('index') + var shapeId = $(this).attr('shape_id') + var shape = L_.layersGroup[layer][index] + + var style + if ( + !shape.hasOwnProperty('feature') && + shape.hasOwnProperty('_layers') + ) + style = + shape._layers[Object.keys(shape._layers)[0]].feature + .properties.style + else style = shape.feature.properties.style + if (style == null) style = shape.options + + if (typeof shape.setStyle === 'function') + shape.setStyle({ color: style.color }) + else if (shape.hasOwnProperty('_layers')) { + //Arrow + var layers = shape._layers + layers[Object.keys(layers)[0]].setStyle({ + color: style.color, + }) + layers[Object.keys(layers)[1]].setStyle({ + color: style.color, + }) + } else + $( + '#DrawToolAnnotation_' + + $(this).attr('layer_id') + + '_' + + $(this).attr('shape_id') + ).removeClass('highlight') + }) + $('.drawToolShapeLiItem').on('click', function (e) { + var layer = $(this).attr('layer') + var index = $(this).attr('index') + var shape = L_.layersGroup[layer][index] + if (!mmgisglobal.shiftDown) { + if (typeof shape.getBounds === 'function') + Map_.map.panTo(shape.getBounds().getCenter()) + else if (shape.hasOwnProperty('_latlng')) + Map_.map.panTo(shape._latlng) + else if (shape.hasOwnProperty('_layers')) { + //Arrow + var layers = shape._layers + var pos = DrawTool.getInnerLayers( + layers[Object.keys(layers)[1]], + 3 + ) + if (pos) { + pos = pos._latlngs[1] + Map_.map.panTo(pos) + } + } + } + + if (shape.hasOwnProperty('_layers')) + shape._layers[Object.keys(shape._layers)[0]].fireEvent( + 'click' + ) + else shape.fireEvent('click') + }) + + if ( + fileId != null && + selectedFeatureIds != null && + !( + selectedFeatureIds.length == 1 && + selectedFeatureIds[0] == null + ) + ) { + mmgisglobal.ctrlDown = false + for (var i = 0; i < selectedFeatureIds.length; i++) { + var item = $( + '.drawToolShapeLi[file_id="' + + fileId + + '"][shape_id="' + + selectedFeatureIds[i] + + '"] > div' + ) + if (item.length > 0) { + var shape = + L_.layersGroup[item.attr('layer')][ + item.attr('index') + ] + if (shape.hasOwnProperty('_layers')) + shape._layers[ + Object.keys(shape._layers)[0] + ].fireEvent('click') + else shape.fireEvent('click') + } + mmgisglobal.ctrlDown = true + } + mmgisglobal.ctrlDown = false + } + }, + updateCopyTo: function (intent, subintent) { + //if( intent === DrawTool.lastShapeIntent ) return; + //Update copy to dropdown + var defaultOpt = 'File...' + $('#drawToolShapesCopyDropdown *').remove() + $('#drawToolShapesCopyDropdown').html( + "" + ) + d3.select('#drawToolShapesCopySelect').html( + "' + ) + + if (intent) { + //Don't allow copies to same file + var filenames = [] + $('.drawToolShapeLi').each(function (i, elm) { + if ($(elm).hasClass('active')) { + filenames.push($(elm).attr('file_name')) + } + }) + + for (var i = 0; i < DrawTool.files.length; i++) { + //Lead Files + if ( + DrawTool.userGroups.indexOf('mmgis-group') != -1 && + DrawTool.files[i].file_owner == 'group' && + filenames.indexOf(DrawTool.files[i].file_name) == -1 && + DrawTool.files[i].hidden == '0' && + ((intent == 'all' && + subintent == 'polygon' && + (DrawTool.files[i].intent == 'roi' || + DrawTool.files[i].intent == 'campaign' || + DrawTool.files[i].intent == 'campsite')) || + (intent == 'all' && + subintent == 'line' && + DrawTool.files[i].intent == 'trail') || + (intent == 'all' && + subintent == 'point' && + DrawTool.files[i].intent == 'signpost') || + intent == DrawTool.files[i].intent) + ) { + d3.select('#drawToolShapesCopySelect') + .append('option') + .attr('value', DrawTool.files[i].id) + .text(DrawTool.files[i].file_name + ' [Lead]') + } else if ( + mmgisglobal.user == DrawTool.files[i].file_owner && + filenames.indexOf(DrawTool.files[i].file_name) == -1 && + intent == DrawTool.files[i].intent && + DrawTool.files[i].hidden == '0' + ) { + d3.select('#drawToolShapesCopySelect') + .append('option') + .attr('value', DrawTool.files[i].id) + .text(DrawTool.files[i].file_name) + } + } + } + + DrawTool.copyFileId = null + $('#drawToolShapesCopySelect').dropdown({ + onChange: function (val, name) { + DrawTool.copyFileId = val + DrawTool.copyFilename = name + }, + }) + + //$( '#drawToolShapesCopySelect' ).dropdown( 'set selected', DrawTool.copyFilename || defaultOpt ); + }, + } + + return Shapes +}) diff --git a/scripts/essence/Tools/Info/InfoTool.css b/scripts/essence/Tools/Info/InfoTool.css index 3004be67..af7df80e 100644 --- a/scripts/essence/Tools/Info/InfoTool.css +++ b/scripts/essence/Tools/Info/InfoTool.css @@ -13,11 +13,22 @@ color: var(--color-mmgis); } +#infoToolOverlaps { + width: 100%; + text-align: center; + display: block; + color: #98ff26; + position: absolute; + line-height: 30px; + font-size: 13px; +} + #infoToolContents { width: 100%; flex: 1; overflow-y: auto; font-size: 12px; + border-top: 1px solid var(--color-b); } #infoToolSelections { @@ -33,11 +44,16 @@ text-align: center; padding: 3px 0px; margin: 0px; + line-height: 18px; width: 100% !important; } #infoToolSelections li.active { background: var(--color-e); border-radius: 2px; + color: white; +} +#infoToolSelections li:hover { + color: white; } #infoTool .collapsed > ul { diff --git a/scripts/essence/Tools/Info/InfoTool.js b/scripts/essence/Tools/Info/InfoTool.js index d032c306..5f8777b3 100644 --- a/scripts/essence/Tools/Info/InfoTool.js +++ b/scripts/essence/Tools/Info/InfoTool.js @@ -12,13 +12,14 @@ define([ 'Kinds', 'jsonViewer', 'css!InfoTool', -], function($, d3, F_, L_, Globe_, Map_, Viewer_, Kinds, jsonViewer) { +], function ($, d3, F_, L_, Globe_, Map_, Viewer_, Kinds, jsonViewer) { //Add the tool markup if you want to do it this way // prettier-ignore // prettier-ignore var markup = [ "
", "
Info
", + "
", "
    ", "
", "
", @@ -37,17 +38,17 @@ define([ geoOpen: false, vars: {}, MMGISInterface: null, - make: function() { + make: function () { this.MMGISInterface = new interfaceWithMMGIS() }, - destroy: function() { + destroy: function () { this.MMGISInterface.separateFromMMGIS() }, - getUrlString: function() { + getUrlString: function () { return '' }, //We might get multiple features if vector layers overlap - use: function( + use: function ( currentLayer, currentLayerName, features, @@ -73,7 +74,12 @@ define([ this.currentLayerName = currentLayerName this.info = features this.variables = variables - this.activeFeatureI = activeI || 0 + let activeIndex = activeI + if (activeI == null) { + let foundI = this.findFeature(currentLayer, features) + activeIndex = foundI != -1 ? foundI : 0 + } + this.activeFeatureI = activeIndex this.initialEvent = initialEvent } @@ -90,9 +96,13 @@ define([ if (this.info == null || this.info.length == 0) return - d3.select('#infoToolSelections') - .selectAll('*') - .remove() + d3.select('#infoToolSelections').selectAll('*').remove() + if (this.info.length > 1) { + $('#infoToolOverlaps').text( + this.info.length + ' Features Overlap' + ) + $('#infoToolOverlaps').css({ display: 'block' }) + } else $('#infoToolOverlaps').css({ display: 'none' }) for (var i = 0; i < this.info.length; i++) { if (!this.info[i].properties) { if (this.info[i].feature) @@ -115,8 +125,8 @@ define([ .attr('class', i == this.activeFeatureI ? 'active' : '') .on( 'click', - (function(idx) { - return function() { + (function (idx) { + return function () { let e = JSON.parse( JSON.stringify(InfoTool.initialEvent) ) @@ -138,17 +148,30 @@ define([ .html(name) } + $('#infoToolSelections').scrollTop((this.activeFeatureI - 1) * 24) + $('#json-renderer').jsonViewer(this.info[this.activeFeatureI], { collapsed: false, withQuotes: false, withLinks: true, }) }, + findFeature: function (l, featureArray) { + if (l.feature && featureArray) { + let f = JSON.stringify(l.feature) + for (let i = 0; i < featureArray.length; i++) { + if (JSON.stringify(featureArray[i]) == f) { + return i + } + } + } + return -1 + }, } // function interfaceWithMMGIS() { - this.separateFromMMGIS = function() { + this.separateFromMMGIS = function () { separateFromMMGIS() } diff --git a/scripts/essence/essence.js b/scripts/essence/essence.js index 387dbb0a..6478d7ad 100644 --- a/scripts/essence/essence.js +++ b/scripts/essence/essence.js @@ -1,368 +1,383 @@ -/* - Copyright 2019 NASA/JPL-Caltech - - Tariq Soliman - Fred Calef III - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -define([ - 'jquery', - 'd3', - 'Formulae_', - 'Test_', - 'Layers_', - 'Map_', - 'Viewer_', - 'UserInterface_', - 'ToolController_', - 'CursorInfo', - 'ContextMenu', - 'Coordinates', - 'Description', - 'ScaleBar', - 'ScaleBox', - 'Swap', - 'QueryURL', - 'three', - 'Globe_', -], function( - $, - d3, - F_, - T_, - L_, - Map_, - Viewer_, - UserInterface_, - ToolController_, - CursorInfo, - ContextMenu, - Coordinates, - Description, - ScaleBar, - ScaleBox, - Swap, - QueryURL, - THREE, - Globe_ -) { - //Requiring UserInterface_ initializes itself - - //Say it's development version - if (mmgisglobal.NODE_ENV === 'development') { - d3.select('body') - .append('div') - .attr('id', 'nodeenv') - .html('DEVELOPMENT') - } - if (typeof mmgisglobal.groups === 'string') { - mmgisglobal.groups = mmgisglobal.groups.replace(/"/g, '"') - try { - mmgisglobal.groups = JSON.parse(mmgisglobal.groups) - } catch (err) { - console.warn('User groups failed to parse.') - } - } - if (typeof mmgisglobal.HOSTS === 'string') { - try { - mmgisglobal.HOSTS = JSON.parse( - mmgisglobal.HOSTS.replace(/"/gi, '"') - ) - } catch (err) { - mmgisglobal.HOSTS = {} - } - } else { - mmgisglobal.HOSTS = {} - } - - mmgisglobal.lastInteraction = Date.now() - $('body').on('mousemove', function() { - mmgisglobal.lastInteraction = Math.floor(Date.now() / 1000) - }) - - mmgisglobal.ctrlDown = false - mmgisglobal.shiftDown = false - // Check whether control button and shift is pressed - //17 is ctrl, 91, 93, and 224 are MAC metakeys - $(document).keydown(function(e) { - if ( - e.which == '17' || - e.which == '91' || - e.which == '93' || - e.which == '224' - ) - mmgisglobal.ctrlDown = true - if (e.which == '16') mmgisglobal.shiftDown = true - }) - $(document).keyup(function(e) { - if ( - e.which == '17' || - e.which == '91' || - e.which == '93' || - e.which == '224' - ) - mmgisglobal.ctrlDown = false - if (e.which == '16') mmgisglobal.shiftDown = false - }) - - $(document.body).keydown(function(e) { - if ( - ToolController_.activeTool == null && - !$('#loginModal').hasClass('active') && - e.shiftKey && - e.keyCode === 84 - ) { - T_.toggle() - } - }) - - var essence = { - configData: null, - hasSwapped: false, - init: function(config, missionsList, swapping, hasDAMTool) { - //Save the config data - this.configData = config - - //Make sure url matches mission - var urlSplit = window.location.href.split('?') - var url = urlSplit[0] - - if ( - urlSplit.length == 1 || - swapping || - (urlSplit[1] && urlSplit[1].split('=')[0] == 'forcelanding') - ) { - //then no parameters or old ones - url = - window.location.href.split('?')[0] + - '?mission=' + - config.msv.mission - window.history.replaceState('', '', url) - L_.url = window.location.href - } - - if (swapping) { - this.hasSwapped = true - L_.clear() - //UserInterface_.refresh(); - } - - //Try querying the urlSite - var urlOnLayers = null - if (!swapping) urlOnLayers = QueryURL.queryURL() - - //Parse all the configData - L_.init(this.configData, missionsList, urlOnLayers) - - if (swapping) { - ToolController_.clear() - Viewer_.clearImage() - //ToolController_.init( L_.tools ); - } - //Update mission title - d3.select('title').html(mmgisglobal.name + ' - ' + L_.mission) - //Set radii - F_.setRadius('major', L_.radius.major) - F_.setRadius('minor', L_.radius.minor) - //Initialize CursorInfo - if (!swapping) CursorInfo.init() - - //Make the globe - if (!swapping) Globe_.init() - - //Make the viewer - if (!swapping) Viewer_.init() - - //Make the map - if (swapping) Map_.clear() - - Map_.init(this.fina) - - //Now that the map is made - Coordinates.init() - ContextMenu.init() - - if (!swapping) { - Description.init(L_.mission, L_.site, Map_, L_) - - ScaleBar.init(ScaleBox) - } else { - if (!hasDAMTool) { - F_.useDegreesAsMeters(false) - Coordinates.setDamCoordSwap(false) - } else { - F_.useDegreesAsMeters(true) - Coordinates.setDamCoordSwap(true) - } - Coordinates.refresh() - ScaleBar.refresh() - } - - Swap.make(this) - }, - swapMission(to) { - //console.log( to ); - //Close all tools since they only update when reopened - ToolController_.closeActiveTool() - - if (mmgisglobal.SERVER == 'node') { - calls.api( - 'get', - { - mission: to, - }, - function(data) { - makeMission(data) - }, - function(e) { - console.log( - "Warning: Couldn't load: " + - missionName + - ' configuration.' - ) - makeMissionNotFoundDiv() - } - ) - } else { - $.getJSON( - 'Missions/' + - to + - '/' + - 'config.json' + - '?nocache=' + - new Date().getTime(), - function(data) { - makeMission(data) - } - ).error(function() { - console.log( - "Warning: Couldn't load: " + - 'Missions/' + - to + - '/' + - 'config.json' - ) - makeMissionNotFoundDiv() - }) - } - - function makeMission(data) { - var toolsThatUseDegreesAsMeters = ['InSight'] - var hasDAMTool = false - //Remove swap tool from data.tools - for (var i = data.tools.length - 1; i > 0; i--) { - if ( - toolsThatUseDegreesAsMeters.indexOf( - data.tools[i].name - ) !== -1 - ) { - hasDAMTool = true - } - if (data.tools[i].name === 'Swap') { - data.tools.splice(i, 1) - } - } - //Add swap to data.tools - for (var i in essence.configData.tools) { - if (essence.configData.tools[i].name === 'Swap') { - data.tools.push(essence.configData.tools[i]) - break - } - } - - if ( - JSON.stringify(essence.configData.panels) !== - JSON.stringify(data.panels) - ) { - data.panels = ['viewer', 'map', 'globe'] - } - - essence.init(data, L_.missionsList, true, hasDAMTool) - } - }, - fina: function() { - if (essence.hasSwapped) Globe_.reset() - - //FinalizeGlobe - Globe_.fina(Coordinates) - //Finalize Layers_ - L_.fina(Viewer_, Map_, Globe_, UserInterface_) - //Finalize the interface - UserInterface_.fina(L_, Viewer_, Map_, Globe_) - //Finalize the Viewer - Viewer_.fina(Map_) - - stylize() - }, - } - - return essence - - //Move this somewhere else later - function stylize() { - if (L_.configData.look) { - if( L_.configData.look.pagename && - L_.configData.look.pagename != '') - d3.select('title').html(L_.configData.look.pagename + ' - ' + L_.mission) - if ( - L_.configData.look.bodycolor && - L_.configData.look.bodycolor != '' - ) - $('body').css({ background: L_.configData.look.bodycolor }) - if ( - L_.configData.look.topbarcolor && - L_.configData.look.topbarcolor != '' - ) - $('#topBar').css({ background: L_.configData.look.topbarcolor }) - if ( - L_.configData.look.toolbarcolor && - L_.configData.look.toolbarcolor != '' - ) { - $('#toolbar').css({ - background: L_.configData.look.toolbarcolor, - }) - var bodyRGB = $('#toolbar').css('backgroundColor') - var bodyHEX = F_.rgb2hex(bodyRGB) - bodyRGB = F_.rgbToArray(bodyRGB) - var c = - 'rgba(' + - bodyRGB[0] + - ',' + - bodyRGB[1] + - ',' + - bodyRGB[2] + - ')' - $('#toolsWrapper').css({ background: c }) - } - if ( - L_.configData.look.mapcolor && - L_.configData.look.mapcolor != '' - ) - $('#map').css({ background: L_.configData.look.mapcolor }) - if( L_.configData.look.logourl && - L_.configData.look.logourl != '' ) { - d3.select('#mmgislogo img').attr('src', L_.configData.look.logourl) - $('#favicon').attr('href', L_.configData.look.logourl); - } - if( L_.configData.look.helpurl && - L_.configData.look.helpurl != '' ) { - $('#topBarHelp').on('click', function() { - let win = window.open(L_.configData.look.helpurl, '_mmgishelp'); - win.focus(); - }) - } - } - } -}) +/* + Copyright 2019 NASA/JPL-Caltech + + Tariq Soliman + Fred Calef III + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +define([ + 'jquery', + 'd3', + 'Formulae_', + 'Test_', + 'Layers_', + 'Map_', + 'Viewer_', + 'UserInterface_', + 'ToolController_', + 'CursorInfo', + 'ContextMenu', + 'Coordinates', + 'Description', + 'ScaleBar', + 'ScaleBox', + 'Swap', + 'QueryURL', + 'three', + 'Globe_', +], function ( + $, + d3, + F_, + T_, + L_, + Map_, + Viewer_, + UserInterface_, + ToolController_, + CursorInfo, + ContextMenu, + Coordinates, + Description, + ScaleBar, + ScaleBox, + Swap, + QueryURL, + THREE, + Globe_ +) { + //Requiring UserInterface_ initializes itself + + //Say it's development version + if (mmgisglobal.NODE_ENV === 'development') { + d3.select('body') + .append('div') + .attr('id', 'nodeenv') + .html('DEVELOPMENT') + } + if (typeof mmgisglobal.groups === 'string') { + mmgisglobal.groups = mmgisglobal.groups.replace(/"/g, '"') + try { + mmgisglobal.groups = JSON.parse(mmgisglobal.groups) + } catch (err) { + console.warn('User groups failed to parse.') + } + } + if (typeof mmgisglobal.HOSTS === 'string') { + try { + mmgisglobal.HOSTS = JSON.parse( + mmgisglobal.HOSTS.replace(/"/gi, '"') + ) + } catch (err) { + mmgisglobal.HOSTS = {} + } + } else { + mmgisglobal.HOSTS = {} + } + + mmgisglobal.lastInteraction = Date.now() + $('body').on('mousemove', function () { + mmgisglobal.lastInteraction = Math.floor(Date.now() / 1000) + }) + + mmgisglobal.ctrlDown = false + mmgisglobal.shiftDown = false + // Check whether control button and shift is pressed + //17 is ctrl, 91, 93, and 224 are MAC metakeys + $(document).keydown(function (e) { + if ( + e.which == '17' || + e.which == '91' || + e.which == '93' || + e.which == '224' + ) + mmgisglobal.ctrlDown = true + if (e.which == '16') mmgisglobal.shiftDown = true + }) + $(document).keyup(function (e) { + if ( + e.which == '17' || + e.which == '91' || + e.which == '93' || + e.which == '224' + ) + mmgisglobal.ctrlDown = false + if (e.which == '16') mmgisglobal.shiftDown = false + }) + + $(document.body).keydown(function (e) { + if ( + ToolController_.activeTool == null && + !$('#loginModal').hasClass('active') && + UserInterface_.getPanelPercents().globe == 0 && + e.shiftKey && + e.keyCode === 84 + ) { + T_.toggle() + } + }) + + var essence = { + configData: null, + hasSwapped: false, + init: function (config, missionsList, swapping, hasDAMTool) { + //Save the config data + this.configData = config + + //Make sure url matches mission + var urlSplit = window.location.href.split('?') + var url = urlSplit[0] + + if ( + urlSplit.length == 1 || + swapping || + (urlSplit[1] && urlSplit[1].split('=')[0] == 'forcelanding') + ) { + //then no parameters or old ones + url = + window.location.href.split('?')[0] + + '?mission=' + + config.msv.mission + window.history.replaceState('', '', url) + L_.url = window.location.href + } + + if (swapping) { + this.hasSwapped = true + L_.clear() + //UserInterface_.refresh(); + } + + //Try querying the urlSite + var urlOnLayers = null + if (!swapping) urlOnLayers = QueryURL.queryURL() + + //Parse all the configData + L_.init(this.configData, missionsList, urlOnLayers) + + if (swapping) { + ToolController_.clear() + Viewer_.clearImage() + //ToolController_.init( L_.tools ); + } + //Update mission title + d3.select('title').html(mmgisglobal.name + ' - ' + L_.mission) + //Set radii + F_.setRadius('major', L_.radius.major) + F_.setRadius('minor', L_.radius.minor) + //Initialize CursorInfo + if (!swapping) CursorInfo.init() + + //Make the globe + if (!swapping) Globe_.init() + + //Make the viewer + if (!swapping) Viewer_.init() + + //Make the map + if (swapping) Map_.clear() + + Map_.init(this.fina) + + //Now that the map is made + Coordinates.init() + ContextMenu.init() + + if (!swapping) { + Description.init(L_.mission, L_.site, Map_, L_) + + ScaleBar.init(ScaleBox) + } else { + if (!hasDAMTool) { + F_.useDegreesAsMeters(false) + Coordinates.setDamCoordSwap(false) + } else { + F_.useDegreesAsMeters(true) + Coordinates.setDamCoordSwap(true) + } + Coordinates.refresh() + ScaleBar.refresh() + } + + Swap.make(this) + }, + swapMission(to) { + //console.log( to ); + //Close all tools since they only update when reopened + ToolController_.closeActiveTool() + + if (mmgisglobal.SERVER == 'node') { + calls.api( + 'get', + { + mission: to, + }, + function (data) { + makeMission(data) + }, + function (e) { + console.log( + "Warning: Couldn't load: " + + missionName + + ' configuration.' + ) + makeMissionNotFoundDiv() + } + ) + } else { + $.getJSON( + 'Missions/' + + to + + '/' + + 'config.json' + + '?nocache=' + + new Date().getTime(), + function (data) { + makeMission(data) + } + ).error(function () { + console.log( + "Warning: Couldn't load: " + + 'Missions/' + + to + + '/' + + 'config.json' + ) + makeMissionNotFoundDiv() + }) + } + + function makeMission(data) { + var toolsThatUseDegreesAsMeters = ['InSight'] + var hasDAMTool = false + //Remove swap tool from data.tools + for (var i = data.tools.length - 1; i > 0; i--) { + if ( + toolsThatUseDegreesAsMeters.indexOf( + data.tools[i].name + ) !== -1 + ) { + hasDAMTool = true + } + if (data.tools[i].name === 'Swap') { + data.tools.splice(i, 1) + } + } + //Add swap to data.tools + for (var i in essence.configData.tools) { + if (essence.configData.tools[i].name === 'Swap') { + data.tools.push(essence.configData.tools[i]) + break + } + } + + if ( + JSON.stringify(essence.configData.panels) !== + JSON.stringify(data.panels) + ) { + data.panels = ['viewer', 'map', 'globe'] + } + + essence.init(data, L_.missionsList, true, hasDAMTool) + } + }, + fina: function () { + if (essence.hasSwapped) Globe_.reset() + + //FinalizeGlobe + Globe_.fina(Coordinates) + //Finalize Layers_ + L_.fina(Viewer_, Map_, Globe_, UserInterface_) + //Finalize the interface + UserInterface_.fina(L_, Viewer_, Map_, Globe_) + //Finalize the Viewer + Viewer_.fina(Map_) + + stylize() + }, + } + + return essence + + //Move this somewhere else later + function stylize() { + if (L_.configData.look) { + if ( + L_.configData.look.pagename && + L_.configData.look.pagename != '' + ) + d3.select('title').html( + L_.configData.look.pagename + ' - ' + L_.mission + ) + if ( + L_.configData.look.bodycolor && + L_.configData.look.bodycolor != '' + ) + $('body').css({ background: L_.configData.look.bodycolor }) + if ( + L_.configData.look.topbarcolor && + L_.configData.look.topbarcolor != '' + ) + $('#topBar').css({ background: L_.configData.look.topbarcolor }) + if ( + L_.configData.look.toolbarcolor && + L_.configData.look.toolbarcolor != '' + ) { + $('#toolbar').css({ + background: L_.configData.look.toolbarcolor, + }) + var bodyRGB = $('#toolbar').css('backgroundColor') + var bodyHEX = F_.rgb2hex(bodyRGB) + bodyRGB = F_.rgbToArray(bodyRGB) + var c = + 'rgba(' + + bodyRGB[0] + + ',' + + bodyRGB[1] + + ',' + + bodyRGB[2] + + ')' + $('#toolsWrapper').css({ background: c }) + } + if ( + L_.configData.look.mapcolor && + L_.configData.look.mapcolor != '' + ) + $('#map').css({ background: L_.configData.look.mapcolor }) + if ( + L_.configData.look.logourl && + L_.configData.look.logourl != '' + ) { + d3.select('#mmgislogo img').attr( + 'src', + L_.configData.look.logourl + ) + $('#favicon').attr('href', L_.configData.look.logourl) + } + if ( + L_.configData.look.helpurl && + L_.configData.look.helpurl != '' + ) { + $('#topBarHelp').on('click', function () { + let win = window.open( + L_.configData.look.helpurl, + '_mmgishelp' + ) + win.focus() + }) + } + } + } +}) diff --git a/scripts/external/Arc/arc.js b/scripts/external/Arc/arc.js new file mode 100644 index 00000000..04423756 --- /dev/null +++ b/scripts/external/Arc/arc.js @@ -0,0 +1,302 @@ +define([], function () { + 'use strict' + + var D2R = Math.PI / 180 + var R2D = 180 / Math.PI + + var Coord = function (lon, lat) { + this.lon = lon + this.lat = lat + this.x = D2R * lon + this.y = D2R * lat + } + + Coord.prototype.view = function () { + return String(this.lon).slice(0, 4) + ',' + String(this.lat).slice(0, 4) + } + + Coord.prototype.antipode = function () { + var anti_lat = -1 * this.lat + var anti_lon = this.lon < 0 ? 180 + this.lon : (180 - this.lon) * -1 + return new Coord(anti_lon, anti_lat) + } + + var LineString = function () { + this.coords = [] + this.length = 0 + } + + LineString.prototype.move_to = function (coord) { + this.length++ + this.coords.push(coord) + } + + var Arc = function (properties) { + this.properties = properties || {} + this.geometries = [] + } + + Arc.prototype.json = function () { + if (this.geometries.length <= 0) { + return { + geometry: { type: 'LineString', coordinates: null }, + type: 'Feature', + properties: this.properties, + } + } else if (this.geometries.length == 1) { + return { + geometry: { + type: 'LineString', + coordinates: this.geometries[0].coords, + }, + type: 'Feature', + properties: this.properties, + } + } else { + var multiline = [] + for (var i = 0; i < this.geometries.length; i++) { + multiline.push(this.geometries[i].coords) + } + return { + geometry: { type: 'MultiLineString', coordinates: multiline }, + type: 'Feature', + properties: this.properties, + } + } + } + + // TODO - output proper multilinestring + Arc.prototype.wkt = function () { + var wkt_string = '' + var wkt = 'LINESTRING(' + var collect = function (c) { + wkt += c[0] + ' ' + c[1] + ',' + } + for (var i = 0; i < this.geometries.length; i++) { + if (this.geometries[i].coords.length === 0) { + return 'LINESTRING(empty)' + } else { + var coords = this.geometries[i].coords + coords.forEach(collect) + wkt_string += wkt.substring(0, wkt.length - 1) + ')' + } + } + return wkt_string + } + + /* + * http://en.wikipedia.org/wiki/Great-circle_distance + * + */ + var GreatCircle = function (start, end, properties) { + if (!start || start.x === undefined || start.y === undefined) { + throw new Error( + 'GreatCircle constructor expects two args: start and end objects with x and y properties' + ) + } + if (!end || end.x === undefined || end.y === undefined) { + throw new Error( + 'GreatCircle constructor expects two args: start and end objects with x and y properties' + ) + } + this.start = new Coord(start.x, start.y) + this.end = new Coord(end.x, end.y) + this.properties = properties || {} + + var w = this.start.x - this.end.x + var h = this.start.y - this.end.y + var z = + Math.pow(Math.sin(h / 2.0), 2) + + Math.cos(this.start.y) * + Math.cos(this.end.y) * + Math.pow(Math.sin(w / 2.0), 2) + this.g = 2.0 * Math.asin(Math.sqrt(z)) + + if (this.g == Math.PI) { + throw new Error( + 'it appears ' + + start.view() + + ' and ' + + end.view() + + " are 'antipodal', e.g diametrically opposite, thus there is no single route but rather infinite" + ) + } else if (isNaN(this.g)) { + throw new Error( + 'could not calculate great circle between ' + + start + + ' and ' + + end + ) + } + } + + /* + * http://williams.best.vwh.net/avform.htm#Intermediate + */ + GreatCircle.prototype.interpolate = function (f) { + var A = Math.sin((1 - f) * this.g) / Math.sin(this.g) + var B = Math.sin(f * this.g) / Math.sin(this.g) + var x = + A * Math.cos(this.start.y) * Math.cos(this.start.x) + + B * Math.cos(this.end.y) * Math.cos(this.end.x) + var y = + A * Math.cos(this.start.y) * Math.sin(this.start.x) + + B * Math.cos(this.end.y) * Math.sin(this.end.x) + var z = A * Math.sin(this.start.y) + B * Math.sin(this.end.y) + var lat = + R2D * Math.atan2(z, Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2))) + var lon = R2D * Math.atan2(y, x) + return [lon, lat] + } + + /* + * Generate points along the great circle + */ + GreatCircle.prototype.Arc = function (npoints, options) { + var first_pass = [] + if (!npoints || npoints <= 2) { + first_pass.push([this.start.lon, this.start.lat]) + first_pass.push([this.end.lon, this.end.lat]) + } else { + var delta = 1.0 / (npoints - 1) + for (var i = 0; i < npoints; ++i) { + var step = delta * i + var pair = this.interpolate(step) + first_pass.push(pair) + } + } + /* partial port of dateline handling from: + gdal/ogr/ogrgeometryfactory.cpp + + TODO - does not handle all wrapping scenarios yet + */ + var bHasBigDiff = false + var dfMaxSmallDiffLong = 0 + // from http://www.gdal.org/ogr2ogr.html + // -datelineoffset: + // (starting with GDAL 1.10) offset from dateline in degrees (default long. = +/- 10deg, geometries within 170deg to -170deg will be splited) + var dfDateLineOffset = options && options.offset ? options.offset : 10 + var dfLeftBorderX = 180 - dfDateLineOffset + var dfRightBorderX = -180 + dfDateLineOffset + var dfDiffSpace = 360 - dfDateLineOffset + + // https://github.com/OSGeo/gdal/blob/7bfb9c452a59aac958bff0c8386b891edf8154ca/gdal/ogr/ogrgeometryfactory.cpp#L2342 + for (var j = 1; j < first_pass.length; ++j) { + var dfPrevX = first_pass[j - 1][0] + var dfX = first_pass[j][0] + var dfDiffLong = Math.abs(dfX - dfPrevX) + if ( + dfDiffLong > dfDiffSpace && + ((dfX > dfLeftBorderX && dfPrevX < dfRightBorderX) || + (dfPrevX > dfLeftBorderX && dfX < dfRightBorderX)) + ) { + bHasBigDiff = true + } else if (dfDiffLong > dfMaxSmallDiffLong) { + dfMaxSmallDiffLong = dfDiffLong + } + } + + var poMulti = [] + if (bHasBigDiff && dfMaxSmallDiffLong < dfDateLineOffset) { + var poNewLS = [] + poMulti.push(poNewLS) + for (var k = 0; k < first_pass.length; ++k) { + var dfX0 = parseFloat(first_pass[k][0]) + if ( + k > 0 && + Math.abs(dfX0 - first_pass[k - 1][0]) > dfDiffSpace + ) { + var dfX1 = parseFloat(first_pass[k - 1][0]) + var dfY1 = parseFloat(first_pass[k - 1][1]) + var dfX2 = parseFloat(first_pass[k][0]) + var dfY2 = parseFloat(first_pass[k][1]) + if ( + dfX1 > -180 && + dfX1 < dfRightBorderX && + dfX2 == 180 && + k + 1 < first_pass.length && + first_pass[k - 1][0] > -180 && + first_pass[k - 1][0] < dfRightBorderX + ) { + poNewLS.push([-180, first_pass[k][1]]) + k++ + poNewLS.push([first_pass[k][0], first_pass[k][1]]) + continue + } else if ( + dfX1 > dfLeftBorderX && + dfX1 < 180 && + dfX2 == -180 && + k + 1 < first_pass.length && + first_pass[k - 1][0] > dfLeftBorderX && + first_pass[k - 1][0] < 180 + ) { + poNewLS.push([180, first_pass[k][1]]) + k++ + poNewLS.push([first_pass[k][0], first_pass[k][1]]) + continue + } + + if (dfX1 < dfRightBorderX && dfX2 > dfLeftBorderX) { + // swap dfX1, dfX2 + var tmpX = dfX1 + dfX1 = dfX2 + dfX2 = tmpX + // swap dfY1, dfY2 + var tmpY = dfY1 + dfY1 = dfY2 + dfY2 = tmpY + } + if (dfX1 > dfLeftBorderX && dfX2 < dfRightBorderX) { + dfX2 += 360 + } + + if (dfX1 <= 180 && dfX2 >= 180 && dfX1 < dfX2) { + var dfRatio = (180 - dfX1) / (dfX2 - dfX1) + var dfY = dfRatio * dfY2 + (1 - dfRatio) * dfY1 + poNewLS.push([ + first_pass[k - 1][0] > dfLeftBorderX ? 180 : -180, + dfY, + ]) + poNewLS = [] + poNewLS.push([ + first_pass[k - 1][0] > dfLeftBorderX ? -180 : 180, + dfY, + ]) + poMulti.push(poNewLS) + } else { + poNewLS = [] + poMulti.push(poNewLS) + } + poNewLS.push([dfX0, first_pass[k][1]]) + } else { + poNewLS.push([first_pass[k][0], first_pass[k][1]]) + } + } + } else { + // add normally + var poNewLS0 = [] + poMulti.push(poNewLS0) + for (var l = 0; l < first_pass.length; ++l) { + poNewLS0.push([first_pass[l][0], first_pass[l][1]]) + } + } + + var arc = new Arc(this.properties) + for (var m = 0; m < poMulti.length; ++m) { + var line = new LineString() + arc.geometries.push(line) + var points = poMulti[m] + for (var j0 = 0; j0 < points.length; ++j0) { + line.move_to(points[j0]) + } + } + return arc + } + + var arc = {} + arc.Coord = Coord + arc.Arc = Arc + arc.GreatCircle = GreatCircle + + return arc +}) diff --git a/scripts/external/Leaflet/leaflet.tilelayer.gl.js b/scripts/external/Leaflet/leaflet.tilelayer.gl.js index 08e59d1e..9e65d83b 100644 --- a/scripts/external/Leaflet/leaflet.tilelayer.gl.js +++ b/scripts/external/Leaflet/leaflet.tilelayer.gl.js @@ -22,6 +22,10 @@ L.TileLayer.GL = L.GridLayer.extend({ // Array of tile URL templates (as in `L.TileLayer`), between zero and 8 elements. Each URL template will be converted into a plain `L.TileLayer` and pushed in the `tileLayers` option. tileUrls: [], + // @option tileUrlsAsDataUrls: Boolean + // Boolean that if true treats tileUrls as an array of image objects with data urls. { : { : { : dataURL }, : {} } } + tileUrlsAsDataUrls: false, + // @option tileLayers: Array // Array of instances of `L.TileLayer` (or its subclasses, like `L.TileLayer.WMS`), between zero and 8 elements. tileLayers: [], @@ -45,7 +49,7 @@ L.TileLayer.GL = L.GridLayer.extend({ // On instantiating the layer, it will initialize all the GL context // and upload the shaders to the GPU, along with the vertex buffer // (the vertices will stay the same for all tiles). - initialize: function(options) { + initialize: function (options) { options = L.setOptions(this, options) this._renderer = L.DomUtil.create('canvas') @@ -65,9 +69,17 @@ L.TileLayer.GL = L.GridLayer.extend({ // Create `TileLayer`s from the tileUrls option. this._tileLayers = Array.from(options.tileLayers) for (var i = 0; i < options.tileUrls.length && i < 8; i++) { - this._tileLayers.push( - L.tileLayer(options.tileUrls[i], options.options || {}) - ) + if (options.tileUrlsAsDataUrls) { + this._tileLayers.push( + L.tileLayer('{z},{x},{y}', options.options || {}) + ) + this._tileLayers[this._tileLayers.length - 1].dataUrls = + options.tileUrls[i] + } else { + this._tileLayers.push( + L.tileLayer(options.tileUrls[i], options.options || {}) + ) + } } this._loadGLProgram() @@ -86,11 +98,11 @@ L.TileLayer.GL = L.GridLayer.extend({ // @method getGlError(): String|undefined // If there was any error compiling/linking the shaders, returns a string // with information about that error. If there was no error, returns `undefined`. - getGlError: function() { + getGlError: function () { return this._glError }, - _loadGLProgram: function() { + _loadGLProgram: function () { var gl = this._gl // Force using this vertex shader. @@ -301,7 +313,7 @@ L.TileLayer.GL = L.GridLayer.extend({ // render a tile, passing the complex space coordinates to the // GPU, and asking to render the vertexes (as triangles) again. // Every pixel will be opaque, so there is no need to clear the scene. - _render: function(coords) { + _render: function (coords) { var gl = this._gl gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight) gl.clearColor(0, 0, 0, 0) @@ -369,7 +381,7 @@ L.TileLayer.GL = L.GridLayer.extend({ gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4) }, - _bindTexture: function(index, imageData) { + _bindTexture: function (index, imageData) { // Helper function. Binds a ImageData (HTMLImageElement, HTMLCanvasElement or // ImageBitmap) to a texture, given its index (0 to 7). // The image data is assumed to be in RGBA format. @@ -390,7 +402,7 @@ L.TileLayer.GL = L.GridLayer.extend({ gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_NEAREST ) - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) gl.generateMipmap(gl.TEXTURE_2D) @@ -404,12 +416,14 @@ L.TileLayer.GL = L.GridLayer.extend({ L.GridLayer.prototype._addTile.call(this, coords, container) }, - createTile: function(coords, done) { + createTile: function (coords, done) { var tile = L.DomUtil.create('canvas', 'leaflet-tile') tile.width = tile.height = this.options.tileSize tile.onselectstart = tile.onmousemove = L.Util.falseFn var ctx = tile.getContext('2d') + ctx.imageSmoothingEnabled = false + var unwrappedKey = this._unwrappedKey var texFetches = [] for (var i = 0; i < this._tileLayers.length && i < 8; i++) { @@ -418,7 +432,7 @@ L.TileLayer.GL = L.GridLayer.extend({ } Promise.all(texFetches).then( - function(textureImages) { + function (textureImages) { if (!this._map) { return } @@ -436,10 +450,11 @@ L.TileLayer.GL = L.GridLayer.extend({ } this._render(coords) + ctx.drawImage(this._renderer, 0, 0) done() }.bind(this), - function(err) { + function (err) { L.TileLayer.prototype._tileOnError.call(this, done, tile, err) }.bind(this) ) @@ -447,7 +462,7 @@ L.TileLayer.GL = L.GridLayer.extend({ return tile }, - _removeTile: function(key) { + _removeTile: function (key) { if (this._isReRenderable) { delete this._fetchedTextures[key] delete this._2dContexts[key] @@ -455,7 +470,7 @@ L.TileLayer.GL = L.GridLayer.extend({ L.TileLayer.prototype._removeTile.call(this, key) }, - onAdd: function() { + onAdd: function () { // If the shader is time-dependent (i.e. animated), start an animation loop. if (this._uNowPosition) { L.Util.cancelAnimFrame(this._animFrame) @@ -464,13 +479,13 @@ L.TileLayer.GL = L.GridLayer.extend({ L.TileLayer.prototype.onAdd.call(this) }, - onRemove: function(map) { + onRemove: function (map) { // Stop the animation loop, if any. L.Util.cancelAnimFrame(this._animFrame) L.TileLayer.prototype.onRemove.call(this, map) }, - _onFrame: function() { + _onFrame: function () { if (this._uNowPosition && this._map) { this.reRender() this._animFrame = L.Util.requestAnimFrame( @@ -480,12 +495,12 @@ L.TileLayer.GL = L.GridLayer.extend({ ) } }, - clear: function() { + clear: function () { var gl = this._gl gl.clear(gl.DEPTH_BUFFER_BIT | gl.COLOR_BUFFER_BIT) }, // Runs the shader (again) on all tiles - reRender: function() { + reRender: function () { if (!this._isReRenderable) { return } @@ -538,7 +553,7 @@ L.TileLayer.GL = L.GridLayer.extend({ // Gets the tile for the Nth `TileLayer` in `this._tileLayers`, // for the given tile coords, returns a promise to the tile. - _getNthTile: function(n, coords) { + _getNthTile: function (n, coords) { var layer = this._tileLayers[n] // Monkey-patch a few things, both for TileLayer and TileLayer.WMS layer._tileZoom = this._tileZoom @@ -546,17 +561,30 @@ L.TileLayer.GL = L.GridLayer.extend({ layer._crs = this._map.options.crs layer._globalTileRange = this._globalTileRange return new Promise( - function(resolve, reject) { + function (resolve, reject) { var tile = document.createElement('img') tile.crossOrigin = '' - tile.src = layer.getTileUrl(coords) - L.DomEvent.on(tile, 'load', resolve.bind(this, tile)) - L.DomEvent.on(tile, 'error', reject.bind(this, tile)) + if (this.options.tileUrlsAsDataUrls) { + if ( + layer.dataUrls[coords.z] && + layer.dataUrls[coords.z][coords.x] && + layer.dataUrls[coords.z][coords.x][coords.y] + ) { + tile.src = layer.dataUrls[coords.z][coords.x][coords.y] + resolve(tile) + } else { + reject(tile) + } + } else { + tile.src = layer.getTileUrl(coords) + L.DomEvent.on(tile, 'load', resolve.bind(this, tile)) + L.DomEvent.on(tile, 'error', reject.bind(this, tile)) + } }.bind(this) ) }, }) -L.tileLayer.gl = function(opts) { +L.tileLayer.gl = function (opts) { return new L.TileLayer.GL(opts) } diff --git a/views/index.pug b/views/index.pug index 671bbc88..9ecf4e26 100644 --- a/views/index.pug +++ b/views/index.pug @@ -1,54 +1,54 @@ -doctype html -head - title MMGIS - meta(charset='utf-8') - meta(name='viewport' content='width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0') - link(id='favicon' rel='shortcut icon' href='../resources/logo.png') - link(type='text/css' rel='stylesheet' href='../public/fonts/materialdesignicons/css/materialdesignicons.min.css') - link(href='../css/external/jquery-ui.css' rel='stylesheet' type='text/css') - link(href='../scripts/external/SemanticUI/semantic.min.css' rel='stylesheet' type='text/css') - link(href='../scripts/external/Leaflet/leaflet1.5.1.css' rel='stylesheet' type='text/css') - link(href='../scripts/external/Leaflet/leaflet.draw.css' rel='stylesheet' type='text/css') - link(href='../css/external/leaflet.label.css' rel='stylesheet' type='text/css') - link(href='../css/external/multirange.css' rel='stylesheet' type='text/css') - link(href='../scripts/external/MetricsGraphics/metricsgraphics.css' rel='stylesheet' type='text/css') - link(href='../scripts/external/MetricsGraphics/metricsgraphics-dark.css' rel='stylesheet' type='text/css') - link(href='../scripts/external/JSONViewer/jquery.json-viewer.css' rel='stylesheet' type='text/css') - link(href='../scripts/external/DataTables/datatables.css' rel='stylesheet' type='text/css') - link(href='../scripts/essence/LandingPage/LandingPage.css' rel='stylesheet' type='text/css') - link(href='../css/mmgis.css' rel='stylesheet' type='text/css') - link(href='../css/mmgisUI.css' rel='stylesheet' type='text/css') - link(href='../css/tools.css' rel='stylesheet' type='text/css') - link(href='../scripts/pre/loading/loading.css' rel='stylesheet' type='text/css') -#main-container -footer - script. - var mmgisglobal = {}; - mmgisglobal.name = 'MMGIS'; - mmgisglobal.version = '1.3.0'; - mmgisglobal.SERVER = 'node'; - mmgisglobal.AUTH = '#{AUTH}'; - mmgisglobal.user = '#{user}'; - mmgisglobal.permission = '#{permission}'; - mmgisglobal.groups = '#{groups}'; - mmgisglobal.NODE_ENV = '#{NODE_ENV}'; - mmgisglobal.CONFIGCONFIG_PATH = '#{CONFIGCONFIG_PATH}'; - mmgisglobal.FORCE_CONFIG_PATH = '#{FORCE_CONFIG_PATH}'; - mmgisglobal.HOSTS = '#{HOSTS}' - console.info( - '███╗ ███╗███╗ ███╗ ██████╗ ██╗███████╗' + ' v' + mmgisglobal.version + '\n' - + '████╗ ████║████╗ ████║██╔════╝ ██║██╔════╝' + ' Leaflet 1.5.1\n' - + '██╔████╔██║██╔████╔██║██║ ███╗██║███████╗' + ' THREE r112\n' - + '██║╚██╔╝██║██║╚██╔╝██║██║ ██║██║╚════██║' + '\n' - + '██║ ╚═╝ ██║██║ ╚═╝ ██║╚██████╔╝██║███████║' + '\n' - + '╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝╚══════╝' ); - //Still some problems loading tools - //link(href='dist/mmgis.min.css' rel='stylesheet' type='text/css') - //script(src='scripts/require.js') - //script(src='dist/mmgis.min.js') - script(src='../scripts/pre/toolConfigs.js') - script(src='../scripts/pre/loading/loading.js') - script(src='../scripts/pre/calls.js') - script(src='../scripts/configure.js') - script(data-main='main' src='../scripts/require.js') +doctype html +head + title MMGIS + meta(charset='utf-8') + meta(name='viewport' content='width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0') + link(id='favicon' rel='shortcut icon' href='../resources/logo.png') + link(type='text/css' rel='stylesheet' href='../public/fonts/materialdesignicons/css/materialdesignicons.min.css') + link(href='../css/external/jquery-ui.css' rel='stylesheet' type='text/css') + link(href='../scripts/external/SemanticUI/semantic.min.css' rel='stylesheet' type='text/css') + link(href='../scripts/external/Leaflet/leaflet1.5.1.css' rel='stylesheet' type='text/css') + link(href='../scripts/external/Leaflet/leaflet.draw.css' rel='stylesheet' type='text/css') + link(href='../css/external/leaflet.label.css' rel='stylesheet' type='text/css') + link(href='../css/external/multirange.css' rel='stylesheet' type='text/css') + link(href='../scripts/external/MetricsGraphics/metricsgraphics.css' rel='stylesheet' type='text/css') + link(href='../scripts/external/MetricsGraphics/metricsgraphics-dark.css' rel='stylesheet' type='text/css') + link(href='../scripts/external/JSONViewer/jquery.json-viewer.css' rel='stylesheet' type='text/css') + link(href='../scripts/external/DataTables/datatables.css' rel='stylesheet' type='text/css') + link(href='../scripts/essence/LandingPage/LandingPage.css' rel='stylesheet' type='text/css') + link(href='../css/mmgis.css' rel='stylesheet' type='text/css') + link(href='../css/mmgisUI.css' rel='stylesheet' type='text/css') + link(href='../css/tools.css' rel='stylesheet' type='text/css') + link(href='../scripts/pre/loading/loading.css' rel='stylesheet' type='text/css') +#main-container +footer + script. + var mmgisglobal = {}; + mmgisglobal.name = 'MMGIS'; + mmgisglobal.version = '1.3.2'; + mmgisglobal.SERVER = 'node'; + mmgisglobal.AUTH = '#{AUTH}'; + mmgisglobal.user = '#{user}'; + mmgisglobal.permission = '#{permission}'; + mmgisglobal.groups = '#{groups}'; + mmgisglobal.NODE_ENV = '#{NODE_ENV}'; + mmgisglobal.CONFIGCONFIG_PATH = '#{CONFIGCONFIG_PATH}'; + mmgisglobal.FORCE_CONFIG_PATH = '#{FORCE_CONFIG_PATH}'; + mmgisglobal.HOSTS = '#{HOSTS}' + console.info( + '███╗ ███╗███╗ ███╗ ██████╗ ██╗███████╗' + ' v' + mmgisglobal.version + '\n' + + '████╗ ████║████╗ ████║██╔════╝ ██║██╔════╝' + ' Leaflet 1.5.1\n' + + '██╔████╔██║██╔████╔██║██║ ███╗██║███████╗' + ' THREE r112\n' + + '██║╚██╔╝██║██║╚██╔╝██║██║ ██║██║╚════██║' + '\n' + + '██║ ╚═╝ ██║██║ ╚═╝ ██║╚██████╔╝██║███████║' + '\n' + + '╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝╚══════╝' ); + //Still some problems loading tools + //link(href='dist/mmgis.min.css' rel='stylesheet' type='text/css') + //script(src='scripts/require.js') + //script(src='dist/mmgis.min.js') + script(src='../scripts/pre/toolConfigs.js') + script(src='../scripts/pre/loading/loading.js') + script(src='../scripts/pre/calls.js') + script(src='../scripts/configure.js') + script(data-main='main' src='../scripts/require.js') script(src='scripts/pre/RefreshAuth.js') \ No newline at end of file diff --git a/views/login.pug b/views/login.pug index 56494bad..c33f56ee 100644 --- a/views/login.pug +++ b/views/login.pug @@ -1,20 +1,35 @@ doctype html -head - title MMGIS / Login - link(rel='shortcut icon' href='resources/logo.png') - link(type='text/css' rel='stylesheet' href='public/login.css') - script(type='text/javascript' src='public/jquery.min.js') - script(type='text/javascript' src='public/login.js') -body.mmgisScrollbar - .container - img(src='resources/mmgis.png' alt='MMGIS') - span#msg.error - form.box(name='form1') - h4 - | Sign in - span to your account. - h5 If you do not have an account,
request one from your administrator. - input#username(type='text' name='username' placeholder='Username' autocomplete='off') - input#pwd(type='password' name='password' placeholder='Password' autocomplete='off') - input#pwd_retype(type='password' name='password' placeholder='Retype Password' autocomplete='off') - div.btn1(onClick='login()') Sign in \ No newline at end of file +html(lang='en') + head + title MMGIS / Login + link(rel='shortcut icon' href='resources/logo.png') + link(type='text/css' rel='stylesheet' href='public/login.css') + script(type='text/javascript' src='public/jquery.min.js') + script(type='text/javascript' src='public/login.js') + body.mmgisScrollbar + .header + img(src='https://graphics.jpl.nasa.gov/img/logos/Tribrand_WhiteText_CMYK_022615-354x67.png' alt='Jet Propulsion Laboratory, California Institute of Technology' style='left: 44px; transform: unset;') + .container(style='z-index: 1;') + img(src='resources/mmgis.png' alt='MMGIS') + span#msg.error + form.box(name='form1' title='Log in to MMGIS!') + h4 + | Sign in + span to your account. + h5 If you do not have an account,
request one from your administrator. + label(for="username" style="position: absolute; color: #a1a4ad; top: 202px; left: 50%; transform: translateX(-50%); background: hsl(226, 32%, 15%); border-radius: 10px; font-size: 14px; width: 100px; text-align: center;") Username + input#username(type='text' name='username' placeholder='Username' autocomplete='off') + label(for="password" style="position: absolute; color: #a1a4ad; top: 269px; left: 50%; transform: translateX(-50%); background: hsl(226, 32%, 15%); border-radius: 10px; font-size: 14px; width: 100px; text-align: center;") Password + input#pwd(type='password' name='password' placeholder='Password' autocomplete='off') + input#pwd_retype(type='password' name='password' placeholder='Retype Password' autocomplete='off') + div.btn1(onClick='login()') Sign in + .footer(style='position: absolute; bottom: 0; padding: 44px; box-sizing: border-box; width: 100%; color: #adadad; font-size: 14px; display: flex; justify-content: flex-end; text-align: end;') + + div + div Contact: fred.calef@jpl.nasa.gov + div Clearance Number: CL##-####div + div(style='display: flex;') + a(href='http://www.jpl.nasa.gov/copyrights.cfm' style='margin-right: 10px;') PRIVACY + a(href='http://www.jpl.nasa.gov/imagepolicy/') IMAGE POLICY + footer + script(id="_fed_an_ua_tag" type="text/javascript" src="https://dap.digitalgov.gov/Universal-Federated-Analytics-Min.js?agency=NASA&subagency=MMGIS&dclink=true&sp=search,s") \ No newline at end of file