From 34d3ae683616be09ca11dc1e5bd61220dfe63b0f Mon Sep 17 00:00:00 2001 From: matafokka Date: Tue, 19 Apr 2022 15:43:04 +0300 Subject: [PATCH 01/48] Now area is calculated for each individual zone and put to the paths' widgets. The calculations are also more precise. --- SynthBase/Hull.js | 12 ++-- SynthBase/SynthBaseLayer.js | 23 ++++--- SynthPolygonLayer/SynthPolygonLayer.js | 1 - SynthPolygonLayer/drawPaths.js | 2 + SynthPolygonLayer/polygons.js | 4 -- package-lock.json | 84 +++++++++++++++++--------- package.json | 1 + 7 files changed, 83 insertions(+), 44 deletions(-) diff --git a/SynthBase/Hull.js b/SynthBase/Hull.js index 22e912e3..08705cc3 100644 --- a/SynthBase/Hull.js +++ b/SynthBase/Hull.js @@ -275,12 +275,16 @@ L.ALS.SynthBaseLayer.prototype.connectHullToAirport = function () { */ L.ALS.SynthBaseLayer.prototype.connectHull = function () { for (let i = 0; i < this.paths.length; i++) { - let path = this.paths[i]; + let path = this.paths[i], selectedArea = 0; path.hullLength = 0; - let layers = path.pathGroup.getLayers(); - for (let layer of layers) + + path.pathGroup.eachLayer((layer) => { path.hullLength += this.getPathLength(layer); - this._createPathWidget(path, 1, path.toUpdateColors); + if (layer.selectedArea) + selectedArea += layer.selectedArea; + }); + + this._createPathWidget(path, 1, path.toUpdateColors, selectedArea); this.buildHull(path, this.getWidgetById(`color${i}`).getValue()); } this.connectHullToAirport(); diff --git a/SynthBase/SynthBaseLayer.js b/SynthBase/SynthBaseLayer.js index 3cd40fbe..676b842e 100644 --- a/SynthBase/SynthBaseLayer.js +++ b/SynthBase/SynthBaseLayer.js @@ -129,7 +129,7 @@ L.ALS.SynthBaseLayer = L.ALS.Layer.extend(/** @lends L.ALS.SynthBaseLayer.protot * Properties to copy to GeoJSON when exporting * @type {string[]} */ - this.propertiesToExport = ["cameraWidth", "cameraHeight", "pixelWidth", "focalLength", "flightHeight", "overlayBetweenPaths", "overlayBetweenImages", "imageScale", "ly", "Ly", "By", "lx", "Lx", "Bx", "GSI", "IFOV", "GIFOV", "FOV", "GFOV", "selectedArea", "timeBetweenCaptures"]; + this.propertiesToExport = ["cameraWidth", "cameraHeight", "pixelWidth", "focalLength", "flightHeight", "overlayBetweenPaths", "overlayBetweenImages", "imageScale", "ly", "Ly", "By", "lx", "Lx", "Bx", "GSI", "IFOV", "GIFOV", "FOV", "GFOV", "timeBetweenCaptures"]; // Add airport let icon = L.divIcon({ @@ -316,7 +316,7 @@ L.ALS.SynthBaseLayer = L.ALS.Layer.extend(/** @lends L.ALS.SynthBaseLayer.protot this.connectHull(); }, - _createPathWidget: function (layer, length, toFlash) { + _createPathWidget: function (layer, length, toFlash, selectedArea = 0) { let id = L.ALS.Helpers.generateID(), button = new L.ALS.Widgets.Button("flashPath" + id, "flashPath", this, "flashPath"), lengthWidget = new L.ALS.Widgets.ValueLabel("pathLength" + id, "pathLength", "m").setFormatNumbers(true).setNumberOfDigitsAfterPoint(0), @@ -329,11 +329,20 @@ L.ALS.SynthBaseLayer = L.ALS.Layer.extend(/** @lends L.ALS.SynthBaseLayer.protot timeWidget.setValue(time.formatted); warning.setValue(time.number > 4 ? "flightTimeWarning" : ""); } + this.pathsDetailsSpoiler.addWidgets( new L.ALS.Widgets.SimpleLabel("pathLabel" + id, `${L.ALS.locale.pathTitle} ${this._pathsWidgetsNumber}`, "center", "message"), - button, lengthWidget, timeWidget, warning, + button ); + if (selectedArea) { + this.pathsDetailsSpoiler.addWidget( + new L.ALS.Widgets.ValueLabel("selectedArea" + id, "selectedArea", "sq.m.").setNumberOfDigitsAfterPoint(0).setFormatNumbers(true).setValue(selectedArea) + ); + } + + this.pathsDetailsSpoiler.addWidgets(lengthWidget, timeWidget, warning); + button.toFlash = toFlash; this._pathsWidgetsNumber++; layer.updateWidgets(length); @@ -449,12 +458,12 @@ L.ALS.SynthBaseLayer = L.ALS.Layer.extend(/** @lends L.ALS.SynthBaseLayer.protot connectOnePerFlight: function () { for (let i = 0; i < this.paths.length; i++) { - let path = this.paths[i], {connectionsGroup, pathGroup} = path, layers = pathGroup.getLayers(), + let path = this.paths[i], {connectionsGroup, pathGroup} = path, lineOptions = this.getConnectionLineOptions(this.getWidgetById(`color${i}`).getValue()); connectionsGroup.clearLayers(); - for (let layer of layers) { + pathGroup.eachLayer((layer) => { layer.pathLength = this.getPathLength(layer); let latLngs = layer.getLatLngs(), @@ -464,9 +473,9 @@ L.ALS.SynthBaseLayer = L.ALS.Layer.extend(/** @lends L.ALS.SynthBaseLayer.protot if (layer.actualPaths) toFlash.push(...layer.actualPaths); - this._createPathWidget(connectionLine, 1, toFlash); + this._createPathWidget(connectionLine, 1, toFlash, layer.selectedArea); connectionsGroup.addLayer(connectionLine); - } + }); } this.connectOnePerFlightToAirport(); // So we'll end up with only one place that updates widgets }, diff --git a/SynthPolygonLayer/SynthPolygonLayer.js b/SynthPolygonLayer/SynthPolygonLayer.js index d3ce5353..3f60f238 100644 --- a/SynthPolygonLayer/SynthPolygonLayer.js +++ b/SynthPolygonLayer/SynthPolygonLayer.js @@ -102,7 +102,6 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthBaseLayer.extend( /** @lends L.ALS.SynthPol this.addWidgets( new L.ALS.Widgets.File("DEMFiles", DEMFilesLabel, this, "onDEMLoad").setMultiple(true), new L.ALS.Widgets.Divider("div3"), - new L.ALS.Widgets.ValueLabel("selectedArea", "selectedArea", "sq.m.").setNumberOfDigitsAfterPoint(0).setFormatNumbers(true), ); this.addBaseParametersOutputSection(); diff --git a/SynthPolygonLayer/drawPaths.js b/SynthPolygonLayer/drawPaths.js index 69da725a..69c9de61 100644 --- a/SynthPolygonLayer/drawPaths.js +++ b/SynthPolygonLayer/drawPaths.js @@ -1,4 +1,5 @@ const bbox = require("@turf/bbox").default; +const turfArea = require("@turf/area").default; const MathTools = require("../MathTools.js"); const turfHelpers = require("@turf/helpers"); @@ -93,6 +94,7 @@ L.ALS.SynthPolygonLayer.prototype._drawPathsWorker = function (isParallels) { prevLine, shouldDraw = true; connectionLine.actualPaths = []; + connectionLine.selectedArea = turfArea(turfPolygon); while (shouldDraw) { let lineCoordinates; diff --git a/SynthPolygonLayer/polygons.js b/SynthPolygonLayer/polygons.js index 1791435e..8b6cd780 100644 --- a/SynthPolygonLayer/polygons.js +++ b/SynthPolygonLayer/polygons.js @@ -58,7 +58,6 @@ L.ALS.SynthPolygonLayer.prototype.removePolygon = function (polygon, removeFromO } L.ALS.SynthPolygonLayer.prototype._calculatePolygonParameters = function (widget) { - this.selectedArea = 0; for (let name in this.polygons) { if (!this.polygons.hasOwnProperty(name)) continue; @@ -69,8 +68,6 @@ L.ALS.SynthPolygonLayer.prototype._calculatePolygonParameters = function (widget layer.lngCellSizeInMeters = this.getParallelOrMeridianLineLength(latLngs[0], latLngs[1], false); layer.latCellSizeInMeters = this.getParallelOrMeridianLineLength(latLngs[1], latLngs[2], false); - this.selectedArea += layer.lngCellSizeInMeters * layer.latCellSizeInMeters; - widgetContainer.getWidgetById("lngCellSizeInMeters").setValue(layer.lngCellSizeInMeters); widgetContainer.getWidgetById("latCellSizeInMeters").setValue(layer.latCellSizeInMeters); @@ -101,7 +98,6 @@ L.ALS.SynthPolygonLayer.prototype._calculatePolygonParameters = function (widget widgetContainer.getWidgetById(name).setValue(value); } } - this.getWidgetById("selectedArea").setValue(this.selectedArea); // Draw thick borders around selected polygons this.mergePolygons(); diff --git a/package-lock.json b/package-lock.json index 27738ee7..97f986f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "synthflight", - "version": "0.1.0-beta", + "version": "0.2.0-beta", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "synthflight", - "version": "0.1.0-beta", + "version": "0.2.0-beta", "hasInstallScript": true, "license": "GPL-3.0-or-later", "dependencies": { @@ -20,6 +20,7 @@ "@babel/preset-env": "^7.12.1", "@babel/runtime-corejs3": "^7.12.5", "@mapbox/geojson-merge": "^1.1.1", + "@turf/area": "^6.5.0", "@turf/bbox": "^6.0.1", "@turf/bbox-polygon": "^6.4.0", "@turf/helpers": "^6.1.4", @@ -1543,6 +1544,19 @@ "node": ">=6" } }, + "node_modules/@turf/area": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/area/-/area-6.5.0.tgz", + "integrity": "sha512-xCZdiuojokLbQ+29qR6qoMD89hv+JAgWjLrwSEWL+3JV8IXKeNFl6XkEJz9HGkVpnXvQKJoRz4/liT+8ZZ5Jyg==", + "dev": true, + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/meta": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, "node_modules/@turf/bbox": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@turf/bbox/-/bbox-6.0.1.tgz", @@ -1562,21 +1576,15 @@ "@turf/helpers": "^6.4.0" } }, - "node_modules/@turf/bbox/node_modules/@turf/meta": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@turf/meta/-/meta-6.0.2.tgz", - "integrity": "sha512-VA7HJkx7qF1l3+GNGkDVn2oXy4+QoLP6LktXAaZKjuT1JI0YESat7quUkbCMy4zP9lAUuvS4YMslLyTtr919FA==", + "node_modules/@turf/helpers": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-6.5.0.tgz", + "integrity": "sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw==", "dev": true, - "dependencies": { - "@turf/helpers": "6.x" + "funding": { + "url": "https://opencollective.com/turf" } }, - "node_modules/@turf/helpers": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-6.4.0.tgz", - "integrity": "sha512-7vVpWZwHP0Qn8DDSlM++nhs3/6zfPt+GODjvLVZ+sWIG4S3vOtUUOfO5eIjRzxsUHHqhgiIL0QA17u79uLM+mQ==", - "dev": true - }, "node_modules/@turf/intersect": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/@turf/intersect/-/intersect-6.4.0.tgz", @@ -1597,6 +1605,18 @@ "@turf/helpers": "^6.4.0" } }, + "node_modules/@turf/meta": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/meta/-/meta-6.5.0.tgz", + "integrity": "sha512-RrArvtsV0vdsCBegoBtOalgdSOfkBrTJ07VkpiCnq/491W67hnMWmDu7e6Ztw0C3WldRYTXkg3SumfdzZxLBHA==", + "dev": true, + "dependencies": { + "@turf/helpers": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, "node_modules/@types/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", @@ -13935,6 +13955,16 @@ "defer-to-connect": "^1.0.1" } }, + "@turf/area": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/area/-/area-6.5.0.tgz", + "integrity": "sha512-xCZdiuojokLbQ+29qR6qoMD89hv+JAgWjLrwSEWL+3JV8IXKeNFl6XkEJz9HGkVpnXvQKJoRz4/liT+8ZZ5Jyg==", + "dev": true, + "requires": { + "@turf/helpers": "^6.5.0", + "@turf/meta": "^6.5.0" + } + }, "@turf/bbox": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@turf/bbox/-/bbox-6.0.1.tgz", @@ -13943,17 +13973,6 @@ "requires": { "@turf/helpers": "6.x", "@turf/meta": "6.x" - }, - "dependencies": { - "@turf/meta": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@turf/meta/-/meta-6.0.2.tgz", - "integrity": "sha512-VA7HJkx7qF1l3+GNGkDVn2oXy4+QoLP6LktXAaZKjuT1JI0YESat7quUkbCMy4zP9lAUuvS4YMslLyTtr919FA==", - "dev": true, - "requires": { - "@turf/helpers": "6.x" - } - } } }, "@turf/bbox-polygon": { @@ -13966,9 +13985,9 @@ } }, "@turf/helpers": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-6.4.0.tgz", - "integrity": "sha512-7vVpWZwHP0Qn8DDSlM++nhs3/6zfPt+GODjvLVZ+sWIG4S3vOtUUOfO5eIjRzxsUHHqhgiIL0QA17u79uLM+mQ==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-6.5.0.tgz", + "integrity": "sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw==", "dev": true }, "@turf/intersect": { @@ -13991,6 +14010,15 @@ "@turf/helpers": "^6.4.0" } }, + "@turf/meta": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/meta/-/meta-6.5.0.tgz", + "integrity": "sha512-RrArvtsV0vdsCBegoBtOalgdSOfkBrTJ07VkpiCnq/491W67hnMWmDu7e6Ztw0C3WldRYTXkg3SumfdzZxLBHA==", + "dev": true, + "requires": { + "@turf/helpers": "^6.5.0" + } + }, "@types/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", diff --git a/package.json b/package.json index aa740616..7cbfcb6d 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "@babel/preset-env": "^7.12.1", "@babel/runtime-corejs3": "^7.12.5", "@mapbox/geojson-merge": "^1.1.1", + "@turf/area": "^6.5.0", "@turf/bbox": "^6.0.1", "@turf/bbox-polygon": "^6.4.0", "@turf/helpers": "^6.1.4", From 6a45cd676817aa1a736a4b736ae034ccd81a194b Mon Sep 17 00:00:00 2001 From: matafokka Date: Tue, 19 Apr 2022 15:46:40 +0300 Subject: [PATCH 02/48] Added info that user can search by semantics in Geometry Layers --- locales/English.js | 2 +- locales/Russian.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/English.js b/locales/English.js index aae3c7c8..f112c384 100644 --- a/locales/English.js +++ b/locales/English.js @@ -123,7 +123,7 @@ L.ALS.Locales.addLocaleProperties("English", { geometryDisplayName: "Geometry Layer", geometryFileLabel: "Zipped shapefile or GeoJSON:", - geometryNotification: "Click/tap on features to see the semantics", // TODO: Add tip for searching by semantics when search will be added + geometryNotification: "Click/tap on features to see the semantics. You cam search by semantics later by clicking \"Search\" button on the program's panel.", // SynthGeometryLayer diff --git a/locales/Russian.js b/locales/Russian.js index b578edaa..88dba001 100644 --- a/locales/Russian.js +++ b/locales/Russian.js @@ -118,7 +118,7 @@ L.ALS.Locales.addLocaleProperties("Русский", { geometryDisplayName: "Слой Геометрии", geometryFileLabel: "Сжатый shapefile (zip-архив) или GeoJSON:", - geometryNotification: "Чтобы просмотреть семантику объекта, нажмите на него", // TODO: Add tip for searching by semantics when search will be added + geometryNotification: "Чтобы просмотреть семантику объекта, нажмите на него. Позже вы можете выполнить поиск по семантике, нажав кнопку поиска на панели программы.", // SynthShapefileLayer From 6520a3b49b7b9949abd6095e602d826c145108e8 Mon Sep 17 00:00:00 2001 From: matafokka Date: Thu, 21 Apr 2022 20:48:26 +0300 Subject: [PATCH 03/48] - Removed redundant code in onMapPan.js. - Added space before brackets for 1:5 000 and 1:2 000 standard scales. --- SynthGridLayer/onMapPan.js | 54 ++++++++------------------------------ 1 file changed, 11 insertions(+), 43 deletions(-) diff --git a/SynthGridLayer/onMapPan.js b/SynthGridLayer/onMapPan.js index 63911f29..7e1c5d72 100644 --- a/SynthGridLayer/onMapPan.js +++ b/SynthGridLayer/onMapPan.js @@ -16,38 +16,14 @@ L.ALS.SynthGridLayer.prototype._onMapPan = function () { this.labelsGroup.deleteLabel(id); this._namesIDs = []; - // Get viewport bounds - let bounds = this.map.getBounds(); - let topLeft = bounds.getNorthWest(), - topRight = bounds.getNorthEast(), - bottomLeft = bounds.getSouthWest(), - bottomRight = bounds.getSouthEast(); - - // Determine the longest sides of the window - let latFrom, latTo, lngFrom, lngTo; - - if (topLeft.lat > topRight.lat) { - latFrom = bottomLeft.lat; - latTo = topLeft.lat; - } else { - latFrom = bottomRight.lat; - latTo = topRight.lat; - } - - if (topRight.lng > bottomRight.lng) { - lngFrom = topLeft.lng; - lngTo = topRight.lng; - } else { - lngFrom = bottomLeft.lng; - lngTo = bottomRight.lng; - } + // Get viewport bounds and calculate correct start and end coords for lng and lat + let bounds = this.map.getBounds(), north = bounds.getNorth(), west = bounds.getWest(), + lngFrom = this._closestLess(west, this.lngDistance), + lngTo = this._closestGreater(bounds.getEast(), this.lngDistance), + latFrom = this._closestLess(bounds.getSouth(), this.latDistance), + latTo = this._closestGreater(north, this.latDistance); - // Calculate correct start and end points for given lat - latFrom = this._closestLess(latFrom, this.latDistance); - latTo = this._closestGreater(latTo, this.latDistance); - - let mapLatLng = this.map.getBounds().getNorthWest(), - isFirstIteration = true; + let isFirstIteration = true; let createLabel = (latLng, content, origin = "center", colorful = false) => { let id = L.ALS.Helpers.generateID(); @@ -61,7 +37,7 @@ L.ALS.SynthGridLayer.prototype._onMapPan = function () { for (let lat = latFrom; lat <= latTo; lat += this.latDistance) { // From bottom (South) to top (North) let absLat = this.toFixed(lat > 0 ? lat - this.latDistance : lat); - createLabel([lat, mapLatLng.lng], absLat, "leftCenter", true); + createLabel([lat, west], absLat, "leftCenter", true); // Merge sheets when lat exceeds certain value. Implemented as specified by this document: // https://docs.cntd.ru/document/456074853 @@ -75,16 +51,12 @@ L.ALS.SynthGridLayer.prototype._onMapPan = function () { } let lngDistance = this.lngDistance * mergedSheetsCount; - // Calculate correct start and end points for given lng - lngFrom = this._closestLess(lngFrom, lngDistance) - lngTo = this._closestGreater(lngTo, lngDistance); - for (let lng = lngFrom; lng <= lngTo; lng += lngDistance) { // From left (West) to right (East) if (lng < -180 || lng > 180 - lngDistance) continue; if (isFirstIteration) - createLabel([mapLatLng.lat, lng], this.toFixed(lng), "topCenter", true); + createLabel([north, lng], this.toFixed(lng), "topCenter", true); let polygon = L.polygon([ [lat, lng], @@ -150,7 +122,7 @@ L.ALS.SynthGridLayer.prototype._onMapPan = function () { let fixedLatScale = this.toFixed(sheetLat); // Truncate sheet sizes to avoid floating point errors. let fixedLngScale = this.toFixed(sheetLng); - // Ok, imagine a ruler. It looks like |...|...|...|. In our case, | is sheet's border. Our point lies between these borders. + // Ok, imagine a ruler which looks like this: |...|...|...|. In our case, | is sheet's border. Our point lies between these borders. // We need to find how much borders we need to reach our point. We do that for both lat and lng. // Here we're finding coordinates of these borders let bottomLat = this.toFixed(this._closestLess(fixedLat, fixedLatScale)); @@ -187,8 +159,6 @@ L.ALS.SynthGridLayer.prototype._onMapPan = function () { } return toReturn; - //return " | Row: " + row + " Col: " + col; - } if (this._currentStandardScale === 500000) // 1:500 000 @@ -207,9 +177,7 @@ L.ALS.SynthGridLayer.prototype._onMapPan = function () { if (this._currentStandardScale <= 10000) polygonName += "-" + sheetNumber(2, "numbers", 5 / 60, 7.5 / 60); } else if (this._currentStandardScale <= 5000) { - polygonName += "(" - if (this._currentStandardScale <= 5000) - polygonName += sheetNumber(16, this._currentStandardScale === 5000 ? "numbers" : "none", 2 / 6, 3 / 6); + polygonName += " (" + sheetNumber(16, this._currentStandardScale === 5000 ? "numbers" : "none", 2 / 6, 3 / 6); if (this._currentStandardScale === 2000) polygonName += "-" + sheetNumber(3, "alphabet", (1 + 15 / 60) / 60, (1 + 52.5 / 60) / 60).toLowerCase(); polygonName += ")"; From f28fed279dbc4fc8a88169d4934e8a4702a314ba Mon Sep 17 00:00:00 2001 From: matafokka Date: Mon, 2 May 2022 12:11:40 +0300 Subject: [PATCH 04/48] Started working on polygon layer In this layer, the capturing area is selected using a polygon. For finding optimal paths we need to use heuristics. The idea behind heuristics is an assumption that the shortest path will always be parallel to the edge of the polygon. The paths themselves are built using gnomonic projection which allows us to work with geodesics as with vectors. The current implementation fails in some cases (I'm not sure which ones, it seem to fail at random), and it's incredibly slow. I'm about to try to do all calculations in gnomonic projection but not sure if it'll work. Anyway, this commit also introduces massive refactoring for the new layer to work. Some misc changes: - Added polygons support to the DEM parsers. - GeoTIFFParser now uses point-in-polygon instead of point-in-rectangle in projected coordinates. - Localized a notification when exporting a layer with no paths. The text now also fits all polygon-based layers. --- ESRIGridParser.js | 61 ++- GeoTIFFParser.js | 27 +- MathTools.js | 59 +-- {SynthBase => SynthBaseLayer}/Hull.js | 4 +- .../SynthBaseLayer.js | 64 +-- .../SynthBaseSettings.js | 0 .../calculateParameters.js | 0 {SynthBase => SynthBaseLayer}/draw.js | 0 {SynthBase => SynthBaseLayer}/toGeoJSON.js | 0 SynthGeometryLayer/SynthGeometryLayer.js | 13 +- SynthGridLayer/SynthGridLayer.js | 10 +- SynthGridLayer/SynthGridSettings.js | 4 +- SynthGridLayer/mergePolygons.js | 2 +- SynthLineLayer/SynthLineLayer.js | 17 +- .../DEM.js | 32 +- .../SynthPolygonBaseLayer.js | 270 +++++++++++++ .../SynthPolygonBaseSettings.js | 39 ++ .../polygons.js | 66 ++-- SynthPolygonLayer.js | 2 +- SynthPolygonLayer/SynthPolygonLayer.js | 364 +++++++++++------- SynthPolygonLayer/SynthPolygonSettings.js | 32 +- SynthPolygonLayer/SynthPolygonWizard.js | 3 + SynthPolygonLayer/misc.js | 151 -------- SynthPolygonLayer/toGeoJSON.js | 47 --- .../SynthRectangleBaseLayer.js | 124 ++++++ .../drawPaths.js | 21 +- SynthRectangleBaseLayer/misc.js | 78 ++++ .../serialization.js | 10 +- SynthRectangleBaseLayer/toGeoJSON.js | 31 ++ SynthRectangleLayer/SynthRectangleLayer.js | 43 +-- SynthRectangleLayer/SynthRectangleSettings.js | 4 +- locales/English.js | 7 + locales/Russian.js | 14 +- main.js | 9 +- 34 files changed, 1015 insertions(+), 593 deletions(-) rename {SynthBase => SynthBaseLayer}/Hull.js (100%) rename {SynthBase => SynthBaseLayer}/SynthBaseLayer.js (95%) rename {SynthBase => SynthBaseLayer}/SynthBaseSettings.js (100%) rename {SynthBase => SynthBaseLayer}/calculateParameters.js (100%) rename {SynthBase => SynthBaseLayer}/draw.js (100%) rename {SynthBase => SynthBaseLayer}/toGeoJSON.js (100%) rename {SynthPolygonLayer => SynthPolygonBaseLayer}/DEM.js (81%) create mode 100644 SynthPolygonBaseLayer/SynthPolygonBaseLayer.js create mode 100644 SynthPolygonBaseLayer/SynthPolygonBaseSettings.js rename {SynthPolygonLayer => SynthPolygonBaseLayer}/polygons.js (59%) create mode 100644 SynthPolygonLayer/SynthPolygonWizard.js delete mode 100644 SynthPolygonLayer/misc.js delete mode 100644 SynthPolygonLayer/toGeoJSON.js create mode 100644 SynthRectangleBaseLayer/SynthRectangleBaseLayer.js rename {SynthPolygonLayer => SynthRectangleBaseLayer}/drawPaths.js (89%) create mode 100644 SynthRectangleBaseLayer/misc.js rename {SynthPolygonLayer => SynthRectangleBaseLayer}/serialization.js (75%) create mode 100644 SynthRectangleBaseLayer/toGeoJSON.js diff --git a/ESRIGridParser.js b/ESRIGridParser.js index bac36132..7b5f5e19 100644 --- a/ESRIGridParser.js +++ b/ESRIGridParser.js @@ -2,21 +2,21 @@ const MathTools = require("./MathTools.js"); const proj4 = require("proj4"); /** - * A stateful ESRI Grid parser. Calculates min and max values for selected polygons in {@link L.ALS.SynthPolygonLayer}. Can parse huge files by chunks. + * A stateful ESRI Grid parser. Calculates min and max values for selected polygons in {@link L.ALS.SynthRectangleBaseLayer}. Can parse huge files by chunks. */ class ESRIGridParser { /** * Constructs a ESRI Grid parser. * - * @param layer {L.ALS.SynthPolygonLayer} A layer to apply parsed values to. + * @param layer {L.ALS.SynthRectangleBaseLayer} A layer to apply parsed values to. * @param projectionString {string} Proj4 projection string. If not given, WGS84 assumed. */ constructor(layer = undefined, projectionString = "") { /** * Layer to apply parsed values to - * @type {L.ALS.SynthPolygonLayer} + * @type {L.ALS.SynthRectangleBaseLayer} */ this.layer = layer @@ -199,12 +199,6 @@ class ESRIGridParser { if (!this.DEMParams.nodata_value) this.DEMParams.nodata_value = -9999; - /*let rect = L.rectangle([ - [this.y, this.x], - [this.y - this.DEMParams.nrows * this.DEMParams.cellsize, this.x + this.DEMParams.ncols * this.DEMParams.cellsize] - ], {color: "#ff7800", weight: 2}); - this.layer.addLayers(rect);*/ - this.DEMParamsCalculated = true; } @@ -222,7 +216,18 @@ class ESRIGridParser { let oldPoint = [this.x, this.y]; let point = (this.hasProj) ? this.projectionFromWGS.inverse(oldPoint) : oldPoint; for (let name in this.polygonsCoordinates) { - if (!MathTools.isPointInRectangle(point, this.polygonsCoordinates[name])) + let poly = this.polygonsCoordinates[name], fn, projPoint; + + if (poly.length > 2) { + fn = MathTools.isPointInPolygon; + let {x, y} = this.layer.map.project([point[0], point[1]], 0); + projPoint = [x, y]; + } else { + fn = MathTools.isPointInRectangle; + projPoint = point; + } + + if (!fn(projPoint, poly)) continue; if (!this.polygonsStats[name]) @@ -260,7 +265,7 @@ class ESRIGridParser { * * This method is NOT thread-safe! Call it outside of your WebWorker and pass your layer as an argument. * - * @param layer {L.ALS.SynthPolygonLayer} If you're not using it in a WebWorker, don't pass anything. Otherwise, pass your layer. + * @param layer {L.ALS.SynthRectangleBaseLayer} If you're not using it in a WebWorker, don't pass anything. Otherwise, pass your layer. */ copyStats(layer = undefined) { let l = this.layer || layer; @@ -311,26 +316,42 @@ class ESRIGridParser { /** * Generates initial parameters for the layer. - * @param layer {L.ALS.SynthPolygonLayer} Layer to copy data from + * @param layer {L.ALS.SynthRectangleBaseLayer} Layer to copy data from + * @param project {boolean} Whether polygon coordinates should be reprojected, if `layer.useRect` is `false` */ - static getInitialData(layer) { + static getInitialData(layer, project = true) { let polygonsCoordinates = {}; for (let name in layer.polygons) { if (!layer.polygons.hasOwnProperty(name)) continue; - let rect = layer.polygons[name].getBounds(); - polygonsCoordinates[name] = [ - [rect.getWest(), rect.getNorth()], - [rect.getEast(), rect.getSouth()] - ]; + let polygon = layer.polygons[name]; + if (layer.useRect) { + let rect = polygon.getBounds(); + polygonsCoordinates[name] = [ + [rect.getWest(), rect.getNorth()], + [rect.getEast(), rect.getSouth()] + ]; + } else { + let coords = polygon.getLatLngs(); + polygonsCoordinates[name] = []; + for (let coord of coords) { + if (!project) { + polygonsCoordinates.push([coord.lng, coord.lat]); + continue; + } + + let {x, y} = layer.map.project(coord, 0); + polygonsCoordinates[name].push([x, y]); + } + } } return polygonsCoordinates; } /** * Copies stats from any ESRIGridParser to a given layer - * @param layer {L.ALS.SynthPolygonLayer} Layer to copy stats to + * @param layer {L.ALS.SynthRectangleBaseLayer} Layer to copy stats to * @param stats {Object} Stats from any ESRIGridParser */ static copyStats(layer, stats) { @@ -342,7 +363,7 @@ class ESRIGridParser { widgetable.getWidgetById("maxHeight").setValue(s.max); widgetable.getWidgetById("meanHeight").setValue(s.mean); } - layer.updateAll(); + layer.calculateParameters(); } static createStatsObject() { diff --git a/GeoTIFFParser.js b/GeoTIFFParser.js index 4f0d57bd..06c6548d 100644 --- a/GeoTIFFParser.js +++ b/GeoTIFFParser.js @@ -42,23 +42,22 @@ module.exports = async function (file, projectionString, initialData) { let projectionFromWGS = proj4("WGS84", projectionString); for (let name in initialData) { - // Let's project each polygon to the image, get their intersection part and calculate statistics for it + // Project each polygon to the image, get their intersection part and calculate statistics for it let polygon = initialData[name], - oldBbox = [ - polygon[0], [polygon[1][0], polygon[0][1]], polygon[1], [polygon[0][0], polygon[1][1]], polygon[0] - ], - newBbox = []; + coords = polygon.length > 2 ? polygon : [ + polygon[0], [polygon[1][0], polygon[0][1]], + polygon[1], [polygon[0][0], polygon[1][1]], polygon[0] + ], + projPolygon = []; - for (let point of oldBbox) - newBbox.push(projectionFromWGS.forward(point)); + for (let coord of coords) + projPolygon.push(projectionFromWGS.forward(coord)); - newBbox = bboxPolygon( - bbox( - turfHelpers.polygon([newBbox]) - ) - ); + let polygonBbox = bboxPolygon(bbox( + turfHelpers.polygon([projPolygon]) + )); - let intersection = intersect(imagePolygon, newBbox); + let intersection = intersect(imagePolygon, polygonBbox); if (!intersection) continue; @@ -98,7 +97,7 @@ module.exports = async function (file, projectionString, initialData) { currentX++; // So we can continue without incrementing index++; - if (!MathTools.isPointInRectangle(point, polygon)) + if (!MathTools.isPointInPolygon(point, projPolygon)) continue; let value = 0; diff --git a/MathTools.js b/MathTools.js index 134551f7..fa8d46b1 100644 --- a/MathTools.js +++ b/MathTools.js @@ -23,8 +23,7 @@ class MathTools { * @return {boolean} true, if does. False otherwise. */ static isPointOnLine(point, line) { - let p1 = line[0], p2 = line[1]; - let p1x = p1[0], p1y = p1[1], p2x = p2[0], p2y = p2[1], px = point[0], py = point[1]; + let [p1, p2] = line, [p1x, p1y] = p1, [p2x, p2y] = p2, [px, py] = point; // Determine if px and py is between line's points. If not, the point is not on the line. let minX = Math.min(p1x, p2x); @@ -134,7 +133,7 @@ class MathTools { if (notOnVertex) intersections++; } - //console.log(point, intersections); + return (intersections % 2 !== 0); } @@ -180,39 +179,49 @@ class MathTools { * @return {*[]|undefined} Clipped line where points are sorted from left to right or, if x coordinates are equal, from top to bottom. Or undefined if line doesn't intersect the polygon. */ static clipLineByPolygon(line, polygon) { - if (line.length !== 2) + if (line.length < 2) return undefined; - // Find intersection of each edge with the line and put it to the array + // Find intersection of each edge with each segment of the line and put it to the array let intersections = []; for (let i = 0; i < polygon.length - 1; i++) { let edge = [polygon[i], polygon[i + 1]]; - let intersection = this.linesIntersection(line, edge); - if (intersection === undefined) - continue; - for (let point of intersection) - intersections.push(point); + for (let j = 0; j < line.length - 1; j++) { + let intersection = this.linesIntersection([line[j], line[j + 1]], edge); + + if (intersection === undefined) + continue; + + for (let point of intersection) + intersections.push(point); + } + } + if (intersections.length < 2) + return undefined; + // Find two points that will produce greatest length. It will yield the segment inside the whole polygon. - let point1, point2, previousLength; + let point1, point2, previousLength = 0; for (let i = 0; i < intersections.length; i++) { + let p1 = intersections[i]; + for (let j = i + 1; j < intersections.length; j++) { - let p1 = intersections[i], p2 = intersections[j]; - let length = this.distanceBetweenPoints(p1, p2); - if (previousLength !== undefined && this.isLessThanOrEqualTo(length, previousLength)) + let p2 = intersections[j], length = this.distanceBetweenPoints(p1, p2); + + if (length <= previousLength) continue; + point1 = p1; point2 = p2; previousLength = length; } + } - if (previousLength === undefined) - return undefined; // Sort points from left to right and, if x coordinates are equal, from top to bottom. - let x1 = point1[0], x2 = point2[0], y1 = point1[1], y2 = point2[1]; + let [x1, y1] = point1, [x2, y2] = point2; if (this.isEqual(x1, x2)) { if (this.isGreaterThanOrEqualTo(y1, y2)) return [point1, point2]; @@ -242,15 +251,13 @@ class MathTools { * @return {*[]|undefined} One of: Array of intersections or, if lines doesn't intersect, undefined */ static linesIntersection(line1, line2) { - let p11 = line1[0], p21 = line2[0]; - let x1 = p11[0], x3 = p21[0]; - - let params1 = this.getSlopeAndIntercept(line1); - let params2 = this.getSlopeAndIntercept(line2); + let x1 = line1[0][0], x3 = line2[0][0], + params1 = this.getSlopeAndIntercept(line1), + params2 = this.getSlopeAndIntercept(line2); // Case when only one of the lines is vertical - let x, verticalLine, otherLine; - let shouldIterateOverPoints = false; // In case of parallel and overlapping lines we should use another method of detection intersections + let x, verticalLine, otherLine, + shouldIterateOverPoints = false; // In case of parallel and overlapping lines we should use another method of detection intersections if (params1 === undefined && params2 !== undefined) { x = x1; verticalLine = line1; @@ -271,8 +278,8 @@ class MathTools { } if (!shouldIterateOverPoints) { - let slopeDiff = params1.slope - params2.slope; - let interceptDiff = params2.intercept - params1.intercept; + let slopeDiff = params1.slope - params2.slope, + interceptDiff = params2.intercept - params1.intercept; // Case when lines are parallel... if (this.isEqual(slopeDiff, 0)) { diff --git a/SynthBase/Hull.js b/SynthBaseLayer/Hull.js similarity index 100% rename from SynthBase/Hull.js rename to SynthBaseLayer/Hull.js index 08705cc3..6ae46be7 100644 --- a/SynthBase/Hull.js +++ b/SynthBaseLayer/Hull.js @@ -53,6 +53,8 @@ L.ALS.SynthBaseLayer.prototype.buildHull = function (path, color) { } let {lower, upper} = this.getConvexHull(points); + lower.pop(); + upper.pop(); // Normalize path-connection order by appending other point of the first path, if it's not connected already. let [p1, p2] = lower; @@ -189,8 +191,6 @@ L.ALS.SynthBaseLayer.prototype.getConvexHull = function (points) { upper.push(point); } - lower.pop(); - upper.pop(); return {upper, lower} } diff --git a/SynthBase/SynthBaseLayer.js b/SynthBaseLayer/SynthBaseLayer.js similarity index 95% rename from SynthBase/SynthBaseLayer.js rename to SynthBaseLayer/SynthBaseLayer.js index 676b842e..7245118b 100644 --- a/SynthBase/SynthBaseLayer.js +++ b/SynthBaseLayer/SynthBaseLayer.js @@ -39,7 +39,26 @@ L.ALS.SynthBaseLayer = L.ALS.Layer.extend(/** @lends L.ALS.SynthBaseLayer.protot isAfterDeserialization: false, - init: function (settings, pathGroup1, connectionsGroup1, colorLabel1, path1AdditionalLayers = [], pathGroup2 = undefined, connectionsGroup2 = undefined, colorLabel2 = undefined, path2AdditionalLayers = []) { + /** + * Indicates whether this layer has Y overlay, i.e. if it has parallel paths + * @type {boolean} + */ + hasYOverlay: true, + + init: function ( + settings, + // Path 1 args + pathGroup1, + connectionsGroup1, + colorLabel1, + path1AdditionalLayers = [], + + // Path 2 args + pathGroup2 = undefined, + connectionsGroup2 = undefined, + colorLabel2 = undefined, + path2AdditionalLayers = [] + ) { /** * {@link L.ALS.Layer#writeToHistory} but debounced for use in repeated calls @@ -86,19 +105,13 @@ L.ALS.SynthBaseLayer = L.ALS.Layer.extend(/** @lends L.ALS.SynthBaseLayer.protot toUpdateColors: [pathGroup2, connectionsGroup2, ...path2AdditionalLayers] } : undefined; - /** - * Indicates whether this layer has Y overlay, i.e. if it has parallel paths - * @type {boolean} - */ - this.hasYOverlay = !!this.path2; - /** * Array of paths to work with * @type {PathData[]} */ this.paths = [this.path1]; - if (this.hasYOverlay) + if (this.path2) this.paths.push(this.path2); /** @@ -250,7 +263,8 @@ L.ALS.SynthBaseLayer = L.ALS.Layer.extend(/** @lends L.ALS.SynthBaseLayer.protot */ _setPathsColor: function () { for (let i = 0; i < this.paths.length; i++) { - let style = {color: this.getWidgetById(`color${i}`).getValue()}, + let color = this.getWidgetById(`color${i}`).getValue(), + style = {fillColor: color, color}, path = this.paths[i]; for (let group of path.toUpdateColors) group.setStyle(style); @@ -375,7 +389,7 @@ L.ALS.SynthBaseLayer = L.ALS.Layer.extend(/** @lends L.ALS.SynthBaseLayer.protot * @return {number} Line length */ getLineLengthMeters: function (line, useFlightHeight = true) { - let r = this._getEarthRadius(useFlightHeight), points = line instanceof Array ? line : line.getLatLngs(), distance = 0; + let r = this.getEarthRadius(useFlightHeight), points = line instanceof Array ? line : line.getLatLngs(), distance = 0; if (points.length === 0) return 0; @@ -404,7 +418,7 @@ L.ALS.SynthBaseLayer = L.ALS.Layer.extend(/** @lends L.ALS.SynthBaseLayer.protot * @return {number} By how much you should modify (add or remove to) lng or lat to get a line of given length */ getArcAngleByLength: function (startingPoint, length, isVertical, useFlightHeight = false) { - let r = this._getEarthRadius(useFlightHeight); + let r = this.getEarthRadius(useFlightHeight); // For vertical lines, we can simply use arc length since any two equal angles will form two equal arcs along any meridian. if (isVertical) @@ -420,7 +434,7 @@ L.ALS.SynthBaseLayer = L.ALS.Layer.extend(/** @lends L.ALS.SynthBaseLayer.protot return turfHelpers.radiansToDegrees(length / newR); }, - _getEarthRadius: function (useFlightHeight = false) { + getEarthRadius: function (useFlightHeight = false) { return 6378137 + (useFlightHeight ? this.flightHeight : 0); }, @@ -440,12 +454,11 @@ L.ALS.SynthBaseLayer = L.ALS.Layer.extend(/** @lends L.ALS.SynthBaseLayer.protot let airportPos = this._airportMarker.getLatLng(); for (let path of this.paths) { - let layers = path.connectionsGroup.getLayers(); - for (let layer of layers) { + path.connectionsGroup.eachLayer((layer) => { layer.getLatLngs()[1] = airportPos; L.redrawLayer(layer); layer.updateWidgets(layer.pathLength + this.getLineLengthMeters(layer)); - } + }); } }, @@ -486,18 +499,17 @@ L.ALS.SynthBaseLayer = L.ALS.Layer.extend(/** @lends L.ALS.SynthBaseLayer.protot * @return {LatLng[][]} Cycles */ onePerFlightToCycles: function (path) { - let layers = path.pathGroup.getLayers(); - - if (layers.length === 0) - return undefined; - let cycles = [], airportPos = this._airportMarker.getLatLng(); - for (let layer of layers) { + + path.pathGroup.eachLayer((layer) => { let latLngs = layer.getLatLngs(), toPush = [airportPos, ...latLngs, airportPos]; toPush.pathLength = layer.pathLength + this.getLineLengthMeters([latLngs[0], airportPos]) + this.getLineLengthMeters([latLngs[latLngs.length - 1], airportPos]); cycles.push(toPush); - } + }) + + if (cycles.length === 0) + return undefined; return cycles; }, @@ -522,9 +534,11 @@ L.ALS.SynthBaseLayer = L.ALS.Layer.extend(/** @lends L.ALS.SynthBaseLayer.protot this.layerSystem.clickOnMenu(); for (let group of widget.toFlash) { - let layers = group instanceof L.FeatureGroup ? group.getLayers() : [group]; - for (let layer of layers) - this.flashLine(layer); + if (!group instanceof L.FeatureGroup) { + this.flashLine(group); + continue; + } + group.eachLayer(layer => this.flashLine(layer)); } }, diff --git a/SynthBase/SynthBaseSettings.js b/SynthBaseLayer/SynthBaseSettings.js similarity index 100% rename from SynthBase/SynthBaseSettings.js rename to SynthBaseLayer/SynthBaseSettings.js diff --git a/SynthBase/calculateParameters.js b/SynthBaseLayer/calculateParameters.js similarity index 100% rename from SynthBase/calculateParameters.js rename to SynthBaseLayer/calculateParameters.js diff --git a/SynthBase/draw.js b/SynthBaseLayer/draw.js similarity index 100% rename from SynthBase/draw.js rename to SynthBaseLayer/draw.js diff --git a/SynthBase/toGeoJSON.js b/SynthBaseLayer/toGeoJSON.js similarity index 100% rename from SynthBase/toGeoJSON.js rename to SynthBaseLayer/toGeoJSON.js diff --git a/SynthGeometryLayer/SynthGeometryLayer.js b/SynthGeometryLayer/SynthGeometryLayer.js index e5f87aab..46ee81c3 100644 --- a/SynthGeometryLayer/SynthGeometryLayer.js +++ b/SynthGeometryLayer/SynthGeometryLayer.js @@ -71,8 +71,8 @@ L.ALS.SynthGeometryLayer = L.ALS.Layer.extend( /** @lends L.ALS.SynthGeometryLay }, _displayFile: function (geoJson) { - let borderColor = new L.ALS.Widgets.Color("borderColor", "geometryBorderColor", this, "_setColor").setValue(this.borderColor), - fillColor = new L.ALS.Widgets.Color("fillColor", "geometryFillColor", this, "_setColor").setValue(this.fillColor), + let borderColor = new L.ALS.Widgets.Color("borderColor", "geometryBorderColor", this, "setColor").setValue(this.borderColor), + fillColor = new L.ALS.Widgets.Color("fillColor", "geometryFillColor", this, "setColor").setValue(this.fillColor), menu = [borderColor, fillColor], popupOptions = { maxWidth: 500, @@ -142,23 +142,22 @@ L.ALS.SynthGeometryLayer = L.ALS.Layer.extend( /** @lends L.ALS.SynthGeometryLay this.deleteLayer(); }, - _setColor(widget) { + setColor(widget) { this[widget.id] = widget.getValue(); this._setLayerColors(); }, _setLayerColors() { - let layers = this._layer.getLayers(); - for (let layer of layers) { + this._layer.eachLayer((layer) => { if (!layer.setStyle) - continue; + return; layer.setStyle({ color: this.borderColor, fillColor: this.fillColor, fill: layer instanceof L.Polygon }); - } + }); }, onDelete: function () { diff --git a/SynthGridLayer/SynthGridLayer.js b/SynthGridLayer/SynthGridLayer.js index 3c27c390..141c72d8 100644 --- a/SynthGridLayer/SynthGridLayer.js +++ b/SynthGridLayer/SynthGridLayer.js @@ -6,7 +6,7 @@ require("./SynthGridWizard.js"); * @class * @extends L.ALS.Layer */ -L.ALS.SynthGridLayer = L.ALS.SynthPolygonLayer.extend(/** @lends L.ALS.SynthGridLayer.prototype */{ +L.ALS.SynthGridLayer = L.ALS.SynthRectangleBaseLayer.extend(/** @lends L.ALS.SynthGridLayer.prototype */{ defaultName: "Grid Layer", useZoneNumbers: true, @@ -22,7 +22,7 @@ L.ALS.SynthGridLayer = L.ALS.SynthPolygonLayer.extend(/** @lends L.ALS.SynthGrid */ this._namesIDs = []; - L.ALS.SynthPolygonLayer.prototype.init.call(this, wizardResults, settings); + L.ALS.SynthRectangleBaseLayer.prototype.init.call(this, wizardResults, settings); /** * Whether or not cells above 60 lat should be merged @@ -47,13 +47,13 @@ L.ALS.SynthGridLayer = L.ALS.SynthPolygonLayer.extend(/** @lends L.ALS.SynthGrid } else this.addPolygon(polygon); - this.updateAll(); + this.calculateParameters(); this.writeToHistoryDebounced(); }, - updateAll: function () { + calculateParameters: function () { this._onMapZoom(); - L.ALS.SynthPolygonLayer.prototype.updateAll.call(this); + L.ALS.SynthRectangleBaseLayer.prototype.calculateParameters.call(this); }, statics: { diff --git a/SynthGridLayer/SynthGridSettings.js b/SynthGridLayer/SynthGridSettings.js index dfd09efc..0b358c26 100644 --- a/SynthGridLayer/SynthGridSettings.js +++ b/SynthGridLayer/SynthGridSettings.js @@ -4,10 +4,10 @@ * @class * @extends L.ALS.Settings */ -L.ALS.SynthGridSettings = L.ALS.SynthPolygonSettings.extend( /** @lends L.ALS.SynthGridSettings.prototype */ { +L.ALS.SynthGridSettings = L.ALS.SynthPolygonBaseSettings.extend( /** @lends L.ALS.SynthGridSettings.prototype */ { initialize: function () { - L.ALS.SynthPolygonSettings.prototype.initialize.call(this); + L.ALS.SynthPolygonBaseSettings.prototype.initialize.call(this); this.addColorWidgets("defaultGridBorderColor", "defaultGridFillColor"); this.addWidget(new L.ALS.Widgets.Number("gridHidingFactor", "gridHidingFactor").setMin(1).setMax(10).setValue(5), 5); } diff --git a/SynthGridLayer/mergePolygons.js b/SynthGridLayer/mergePolygons.js index a19073a0..c9915591 100644 --- a/SynthGridLayer/mergePolygons.js +++ b/SynthGridLayer/mergePolygons.js @@ -2,7 +2,7 @@ const polybool = require("polybooljs"); const MathTools = require("../MathTools.js"); L.ALS.SynthGridLayer.prototype.mergePolygons = function () { - L.ALS.SynthPolygonLayer.prototype.mergePolygons.call(this); + L.ALS.SynthRectangleBaseLayer.prototype.mergePolygons.call(this); // Until there's no adjacent polygons, compare each polygon to each and try to find adjacent one. Then merge it. while (true) { diff --git a/SynthLineLayer/SynthLineLayer.js b/SynthLineLayer/SynthLineLayer.js index 98fc0873..7ed20348 100644 --- a/SynthLineLayer/SynthLineLayer.js +++ b/SynthLineLayer/SynthLineLayer.js @@ -62,11 +62,11 @@ L.ALS.SynthLineLayer = L.ALS.SynthBaseLayer.extend(/** @lends L.ALS.SynthLineLay this.pathsGroup.clearLayers(); this.pointsGroup.clearLayers(); - let layers = this.drawingGroup.getLayers(), color = this.getWidgetById("color0").getValue(), lineOptions = { + let color = this.getWidgetById("color0").getValue(), lineOptions = { color, thickness: this.lineThicknessValue, segmentsNumber: L.GEODESIC_SEGMENTS, }; - for (let layer of layers) { + this.drawingGroup.eachLayer((layer) => { let latLngs = layer.getLatLngs(); for (let i = 1; i < latLngs.length; i++) { let extendedGeodesic = new L.Geodesic([latLngs[i - 1], latLngs[i]], lineOptions), @@ -78,14 +78,13 @@ L.ALS.SynthLineLayer = L.ALS.SynthBaseLayer.extend(/** @lends L.ALS.SynthLineLay // Capture points made by constructing a line with segments number equal to the number of images let points = new L.Geodesic(extendedGeodesic.getLatLngs(), { - ...lineOptions, - segmentsNumber: numberOfImages + ...lineOptions, segmentsNumber: numberOfImages }).getActualLatLngs()[0]; for (let point of points) - this.pointsGroup.addLayer(this.createCapturePoint([point.lat, point.lng], color)); + this.pointsGroup.addLayer(this.createCapturePoint(point, color)); } - } + }); this.updatePathsMeta(); @@ -120,10 +119,8 @@ L.ALS.SynthLineLayer = L.ALS.SynthBaseLayer.extend(/** @lends L.ALS.SynthLineLay }, serialize: function (seenObjects) { - let layers = this.drawingGroup.getLayers(), lines = []; - - for (let layer of layers) - lines.push(layer.getLatLngs()); + let lines = []; + this.drawingGroup.eachLayer(layer => lines.push(layer.getLatLngs())); let serialized = this.getObjectToSerializeTo(seenObjects); serialized.lines = L.ALS.Serializable.serializeAnyObject(lines, seenObjects); diff --git a/SynthPolygonLayer/DEM.js b/SynthPolygonBaseLayer/DEM.js similarity index 81% rename from SynthPolygonLayer/DEM.js rename to SynthPolygonBaseLayer/DEM.js index b1980aa4..72c3dc64 100644 --- a/SynthPolygonLayer/DEM.js +++ b/SynthPolygonBaseLayer/DEM.js @@ -8,7 +8,7 @@ try { } catch (e) {} const work = require("webworkify"); -L.ALS.SynthPolygonLayer.prototype.onDEMLoad = async function (widget) { +L.ALS.SynthPolygonBaseLayer.prototype.onDEMLoad = async function (widget) { let clear = () => { L.ALS.operationsWindow.removeOperation("dem"); widget.clearFileArea(); @@ -48,7 +48,7 @@ L.ALS.SynthPolygonLayer.prototype.onDEMLoad = async function (widget) { * Being called upon DEM load * @param widget {L.ALS.Widgets.File} */ -L.ALS.SynthPolygonLayer.prototype.onDEMLoadWorker = async function (widget) { +L.ALS.SynthPolygonBaseLayer.prototype.onDEMLoadWorker = async function (widget) { let files = widget.getValue(); let parser = new ESRIGridParser(this); let fileReader = new FileReader(); @@ -65,7 +65,14 @@ L.ALS.SynthPolygonLayer.prototype.onDEMLoadWorker = async function (widget) { continue; // Try to find aux or prj file for current file and get projection string from it - let baseName = this.getFileBaseName(file.name), projectionString = ""; + let baseName = "", projectionString = ""; + + for (let symbol of file.name) { + if (symbol === ".") + break; + baseName += symbol; + } + for (let file2 of files) { let ext2 = L.ALS.Helpers.getFileExtension(file2.name).toLowerCase(); let isPrj = (ext2 === "prj"); @@ -92,7 +99,7 @@ L.ALS.SynthPolygonLayer.prototype.onDEMLoadWorker = async function (widget) { let startIndex = text.indexOf(start) + start.length; let endIndex = text.indexOf(end); if (startIndex === start.length - 1 || endIndex === -1) - continue; // Continue in hope of finding not broken xml or prj file. + continue; // Continue in hope of finding correct xml or prj file. projectionString = text.substring(startIndex, endIndex); break; } @@ -100,21 +107,18 @@ L.ALS.SynthPolygonLayer.prototype.onDEMLoadWorker = async function (widget) { if (isTiff) { if (!GeoTIFFParser) continue; - let stats = await GeoTIFFParser(file, projectionString, ESRIGridParser.getInitialData(this)); + let stats = await GeoTIFFParser(file, projectionString, ESRIGridParser.getInitialData(this, false)); ESRIGridParser.copyStats(this, stats); continue; } if (!supportsWorker) { await new Promise((resolve) => { - ESRIGridParser.parseFile(file, parser, fileReader, () => { - resolve(); - }) + ESRIGridParser.parseFile(file, parser, fileReader, () => resolve()) }); continue; } - //let workerFn = isTiff ? GeoTIFFParserWorker : ESRIGridParserWorker; // In case we'll define another parser let worker = work(ESRIGridParserWorker); await new Promise(resolve => { worker.addEventListener("message", (e) => { @@ -129,14 +133,4 @@ L.ALS.SynthPolygonLayer.prototype.onDEMLoadWorker = async function (widget) { }); }); } -}; - -L.ALS.SynthPolygonLayer.prototype.getFileBaseName = function (filename) { - let baseName = ""; - for (let symbol of filename) { - if (symbol === ".") - return baseName; - baseName += symbol; - } - return baseName; }; \ No newline at end of file diff --git a/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js b/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js new file mode 100644 index 00000000..a59d0595 --- /dev/null +++ b/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js @@ -0,0 +1,270 @@ +let GeoTIFFParser; +try { + GeoTIFFParser = require("../GeoTIFFParser.js"); +} catch (e) { +} + +/** + * Contains common logic for rectangle and polygon layers + * + * @class + * @extends L.ALS.SynthBaseLayer + */ +L.ALS.SynthPolygonBaseLayer = L.ALS.SynthBaseLayer.extend( /** @lends L.ALS.SynthPolygonBaseLayer.prototype */ { + useZoneNumbers: false, + useRect: false, + calculateCellSizeForPolygons: true, + + /** + * Indicates whether the grid is displayed or not. + * @type {boolean} + */ + isDisplayed: true, + + _doHidePolygonWidgets: false, + _doHidePathsNumbers: false, + + init: function ( + settings, + // Path 1 args + path1InternalConnections, + path1ExternalConnections, + path1ActualPathGroup, + pointsGroup1 = undefined, + colorLabel1 = "parallelsColor", + hidePaths1WidgetId = "hidePathsByParallels", + // Path 2 args + path2InternalConnections = undefined, + path2ExternalConnections = undefined, + path2ActualPathGroup = undefined, + pointsGroup2 = undefined, + colorLabel2 = "meridiansColor", + hidePaths2WidgetId = "hidePathsByMeridians", + ) { + this.polygons = {}; + this.polygonsWidgets = {}; + this.serializationIgnoreList.push("polygons", "lngDistance", "latDistance", "_currentStandardScale"); + + this.polygonGroup = L.featureGroup(); + this.widgetsGroup = L.featureGroup(); + this.bordersGroup = L.featureGroup(); + this.bordersGroup.thicknessMultiplier = 4; + this.labelsGroup = new L.LabelLayer(false); + + L.ALS.SynthBaseLayer.prototype.init.call(this, settings, + path1InternalConnections, path1ExternalConnections, colorLabel1, [path1ActualPathGroup], + path2InternalConnections, path2ExternalConnections, colorLabel2, [path2ActualPathGroup] + ); + + this.path1.pointsGroup = pointsGroup1; + this.path1.hidePathsWidgetId = hidePaths1WidgetId; + this.path1.actualPathGroup = path1ActualPathGroup; + this.path1.toUpdateColors.push(pointsGroup1); + + if (this.path2) { + this.path2.pointsGroup = pointsGroup2; + this.path2.hidePathsWidgetId = hidePaths2WidgetId; + this.path2.actualPathGroup = path2ActualPathGroup; + this.path2.toUpdateColors.push(pointsGroup2); + } + + this.groupsToHideOnEditStart = [ + path1InternalConnections, path1ExternalConnections, path1ActualPathGroup, pointsGroup1, + path2InternalConnections, path2ExternalConnections, path2ActualPathGroup, pointsGroup2, + this.widgetsGroup, this.labelsGroup, + ]; + + // TODO: Fix thickness + this.addLayers(this.polygonGroup, this.widgetsGroup, this.bordersGroup, pointsGroup1, path1ActualPathGroup, this.labelsGroup); + this.toUpdateThickness.push(this.polygonGroup, this.bordersGroup, pointsGroup1, path1ActualPathGroup); + + if (this.path2) { + this.addLayers(pointsGroup2, path2ActualPathGroup); + this.toUpdateThickness.push(pointsGroup2, path2ActualPathGroup); + } + + this.addWidgets( + new L.ALS.Widgets.Checkbox("hidePolygonWidgets", "hidePolygonWidgets", this, "updateLayersVisibility"), + new L.ALS.Widgets.Checkbox("hideNumbers", "hideNumbers", this, "updateLayersVisibility"), + new L.ALS.Widgets.Checkbox("hideCapturePoints", "hideCapturePoints", this, "updateLayersVisibility").setValue(true), + new L.ALS.Widgets.Checkbox("hidePathsConnections", "hidePathsConnections", this, "updateLayersVisibility"), + new L.ALS.Widgets.Checkbox(this.path1.hidePathsWidgetId, this.path1.hidePathsWidgetId, this, "updateLayersVisibility"), + ); + + if (this.path2) { + this.addWidget( + new L.ALS.Widgets.Checkbox(this.path2.hidePathsWidgetId, this.path2.hidePathsWidgetId, this, "updateLayersVisibility")); + } + + this.addWidgets( + new L.ALS.Widgets.Color("borderColor", this.borderColorLabel, this, "setColor").setValue(this.borderColor), + new L.ALS.Widgets.Color("fillColor", this.fillColorLabel, this, "setColor").setValue(this.fillColor), + ); + + this.addBaseParametersInputSection(); + + let DEMFilesLabel = "DEMFiles"; + if (!GeoTIFFParser) + DEMFilesLabel = "DEMFilesWhenGeoTIFFNotSupported"; + if (L.ALS.Helpers.isIElte9) + DEMFilesLabel = "DEMFilesIE9"; + + this.addWidgets( + new L.ALS.Widgets.File("DEMFiles", DEMFilesLabel, this, "onDEMLoad").setMultiple(true), + new L.ALS.Widgets.Divider("div3"), + ); + + this.addBaseParametersOutputSection(); + }, + + /** + * Calculates grid hiding threshold + * @param settings {SettingsObject} Settings to calculate threshold from + */ + calculateThreshold: function (settings) { + let multiplier = (settings.gridHidingFactor - 5) / 5; // Factor is in range [1..10]. Let's make it [-1...1] + this.minThreshold = 15 + 10 * multiplier; + this.maxThreshold = 60 + 60 * multiplier; + + // If grid will have labels, on lower zoom levels map will become both messy and unusably slow. So we have to set higher hiding threshold. + this.hidingThreshold = this._currentStandardScale === Infinity ? this.minThreshold : this.maxThreshold; + }, + + applyNewSettings: function (settings) { + this.calculateThreshold(settings); + this.calculateParameters(); + }, + + onHide: function () { + for (let path of this.paths) + path.pathGroup.remove(); + }, + + /** + * Generates polygon name for adding into this.polygons + * @param polygon Polygon to generate name for + * @return {string} Name for given polygon + * @protected + */ + _generatePolygonName: function (polygon) { + let firstPoint = polygon.getLatLngs()[0][0]; + return "p_" + this.toFixed(firstPoint.lat) + "_" + this.toFixed(firstPoint.lng); + }, + + onShow: function () { + this.updateLayersVisibility(); + }, + + onDelete: function () { + this.onHide(); + }, + + updateLayersVisibility: function () { + let hideCapturePoints = this.getWidgetById("hideCapturePoints").getValue(), + hidePathsConnections = this.getWidgetById("hidePathsConnections").getValue(); + + for (let path of this.paths) { + let hidePaths = this.getWidgetById(path.hidePathsWidgetId).getValue(); + + if (hidePathsConnections) { + path.pathGroup.remove(); + path.connectionsGroup.remove(); + } else { + this.hideOrShowLayer(hidePaths, path.pathGroup); + this.hideOrShowLayer(hidePaths, path.actualPathGroup); + this.hideOrShowLayer(hidePaths, path.connectionsGroup); + } + + if (hideCapturePoints) + path.pointsGroup.remove(); + else + this.hideOrShowLayer(hidePaths, path.pointsGroup); + + this.hideOrShowLayer(hidePaths, path.actualPathGroup); + } + + this._doHidePolygonWidgets = this.getWidgetById("hidePolygonWidgets").getValue(); + this.hideOrShowLayer(this._doHidePolygonWidgets || this._shouldHideEverything, this.widgetsGroup); + this._doHidePathsNumbers = this.getWidgetById("hideNumbers").getValue(); + }, + + setColor: function (widget) { + this[widget.id] = widget.getValue(); + this.calculateParameters(); + this.updatePolygonsColors(); + }, + + updatePolygonsColors: function () { + let color = this.getWidgetById("borderColor").getValue(), + fillColor = this.getWidgetById("fillColor").getValue(); + for (let id in this.polygons) + this.polygons[id].setStyle({color, fillColor}); + }, + + clearPaths: function () { + for (let path of this.paths) { + let groups = [path.pathGroup, path.connectionsGroup, path.pointsGroup, path.actualPathGroup]; + for (let group of groups) + group.clearLayers(); + } + }, + + onEditStart: function () { + for (let group of this.groupsToHideOnEditStart) { + if (group) + this.hideOrShowLayer(true, group); + } + }, + + /** + * Copies polygons and airport to the array for merging it to collection later + * @returns {Object[]} Array of features to merge + */ + baseFeaturesToGeoJSON: function () { + let jsons = []; + + for (let name in this.polygons) { + if (!this.polygons.hasOwnProperty(name)) + continue; + let polygon = this.polygons[name], + polygonJson = polygon.toGeoJSON(), + props = ["polygonName", "minHeight", "maxHeight", "meanHeight", "absoluteHeight", "reliefType", "elevationDifference", "latCellSizeInMeters", "lngCellSizeInMeters"]; + for (let prop of props) { + let value = polygon[prop]; + if (value !== undefined) + polygonJson.properties[prop] = value; + } + polygonJson.properties.name = "Selected cell"; + jsons.push(polygonJson); + } + + let airport = this._airportMarker.toGeoJSON(); + airport.name = "Airport"; + jsons.push(airport); + + return jsons; + }, + + /** + * Truncates argument to fifth number after point. + * @param n {number} Number to truncate + * @return {number} Truncated number + */ + toFixed: function (n) { + return parseFloat(n.toFixed(5)); + }, + + statics: { + deserialize: function (serialized, layerSystem, settings, seenObjects) { + let deserialized = L.ALS.SynthRectangleBaseLayer.deserialize(serialized, layerSystem, settings, seenObjects); + for (let id in deserialized.polygons) + deserialized.polygonGroup.addLayer(deserialized.polygons[id]); + deserialized.updatePolygonsColors(); + return deserialized; + }, + } + +}); + +require("./DEM.js"); +require("./polygons.js"); \ No newline at end of file diff --git a/SynthPolygonBaseLayer/SynthPolygonBaseSettings.js b/SynthPolygonBaseLayer/SynthPolygonBaseSettings.js new file mode 100644 index 00000000..4762a880 --- /dev/null +++ b/SynthPolygonBaseLayer/SynthPolygonBaseSettings.js @@ -0,0 +1,39 @@ +/** + * Settings for SynthRectangleBaseLayer + * + * @class + * @extends L.ALS.Settings + */ +L.ALS.SynthPolygonBaseSettings = L.ALS.SynthBaseSettings.extend( /** @lends L.ALS.SynthPolygonBaseSettings.prototype */ { + + borderColor: "#6495ed", + fillColor: "#6495ed", + meridiansColor: "#ad0000", + parallelsColor: "#007800", + + addColorWidgets: function (borderLabel, fillLabel, useTwoPaths = true) { + this.addWidget( + new L.ALS.Widgets.Color("borderColor", borderLabel).setValue(this.borderColor), + this.borderColor + ); + + this.addWidget( + new L.ALS.Widgets.Color("fillColor", fillLabel).setValue(this.fillColor), + this.fillColor + ); + + this.addWidget( + new L.ALS.Widgets.Color("color0", "defaultParallelsColor").setValue(this.parallelsColor), + this.parallelsColor + ); + + if (!useTwoPaths) + return; + + this.addWidget( + new L.ALS.Widgets.Color("color1", "defaultMeridiansColor").setValue(this.meridiansColor), + this.meridiansColor + ); + } + +}); \ No newline at end of file diff --git a/SynthPolygonLayer/polygons.js b/SynthPolygonBaseLayer/polygons.js similarity index 59% rename from SynthPolygonLayer/polygons.js rename to SynthPolygonBaseLayer/polygons.js index 8b6cd780..b20bc54c 100644 --- a/SynthPolygonLayer/polygons.js +++ b/SynthPolygonBaseLayer/polygons.js @@ -17,7 +17,7 @@ L.ALS.MeanHeightButtonHandler = L.ALS.Serializable.extend( /**@lends L.ALS.MeanH } }) -L.ALS.SynthPolygonLayer.prototype.addPolygon = function (polygon) { +L.ALS.SynthPolygonBaseLayer.prototype.addPolygon = function (polygon) { polygon._intName = this._generatePolygonName(polygon); polygon.setStyle({fill: true}); @@ -26,30 +26,38 @@ L.ALS.SynthPolygonLayer.prototype.addPolygon = function (polygon) { let controlsContainer = new L.WidgetLayer(polygon.getLatLngs()[0][1], "topLeft"), handler = new L.ALS.MeanHeightButtonHandler(controlsContainer); if (this.useZoneNumbers) - controlsContainer.addWidget(new L.ALS.Widgets.Number("zoneNumber", "zoneNumber", this, "_calculatePolygonParameters").setMin(1).setValue(1)); + controlsContainer.addWidget(new L.ALS.Widgets.Number("zoneNumber", "zoneNumber", this, "calculatePolygonParameters").setMin(1).setValue(1)); controlsContainer.addWidgets( - new L.ALS.Widgets.Number("minHeight", "minHeight", this, "_calculatePolygonParameters").setMin(1).setValue(1), - new L.ALS.Widgets.Number("maxHeight", "maxHeight", this, "_calculatePolygonParameters").setMin(1).setValue(1), - new L.ALS.Widgets.Number("meanHeight", "meanHeight", this, "_calculatePolygonParameters").setMin(1).setValue(1), + new L.ALS.Widgets.Number("minHeight", "minHeight", this, "calculatePolygonParameters").setMin(1).setValue(1), + new L.ALS.Widgets.Number("maxHeight", "maxHeight", this, "calculatePolygonParameters").setMin(1).setValue(1), + new L.ALS.Widgets.Number("meanHeight", "meanHeight", this, "calculatePolygonParameters").setMin(1).setValue(1), new L.ALS.Widgets.Button("meanFromMinMax", "meanFromMinMax", handler, "handle"), new L.ALS.Widgets.ValueLabel("absoluteHeight", "absoluteHeight", "m"), new L.ALS.Widgets.ValueLabel("elevationDifference", "elevationDifference"), new L.ALS.Widgets.ValueLabel("reliefType", "reliefType"), new L.ALS.Widgets.SimpleLabel("error").setStyle("error"), - new L.ALS.Widgets.ValueLabel("lngCellSizeInMeters", "lngCellSizeInMeters", "m").setNumberOfDigitsAfterPoint(0), - new L.ALS.Widgets.ValueLabel("latCellSizeInMeters", "latCellSizeInMeters", "m").setNumberOfDigitsAfterPoint(0), ); + if (this.calculateCellSizeForPolygons) { + controlsContainer.addWidgets( + new L.ALS.Widgets.ValueLabel("lngCellSizeInMeters", "lngCellSizeInMeters", "m").setNumberOfDigitsAfterPoint(0), + new L.ALS.Widgets.ValueLabel("latCellSizeInMeters", "latCellSizeInMeters", "m").setNumberOfDigitsAfterPoint(0), + ) + } + let toFormatNumbers = ["absoluteHeight", "elevationDifference", "lngCellSizeInMeters", "latCellSizeInMeters"]; - for (let id of toFormatNumbers) - controlsContainer.getWidgetById(id).setFormatNumbers(true); + for (let id of toFormatNumbers) { + let widget = controlsContainer.getWidgetById(id); + if (widget) + widget.setFormatNumbers(true); + } this.polygonsWidgets[polygon._intName] = controlsContainer; this.widgetsGroup.addLayer(controlsContainer); } -L.ALS.SynthPolygonLayer.prototype.removePolygon = function (polygon, removeFromObject = true) { +L.ALS.SynthPolygonBaseLayer.prototype.removePolygon = function (polygon, removeFromObject = true) { let name = polygon._intName || this._generatePolygonName(polygon); if (removeFromObject) delete this.polygons[name]; @@ -57,7 +65,7 @@ L.ALS.SynthPolygonLayer.prototype.removePolygon = function (polygon, removeFromO delete this.polygonsWidgets[name]; } -L.ALS.SynthPolygonLayer.prototype._calculatePolygonParameters = function (widget) { +L.ALS.SynthPolygonBaseLayer.prototype.calculatePolygonParameters = function (widget) { for (let name in this.polygons) { if (!this.polygons.hasOwnProperty(name)) continue; @@ -65,11 +73,13 @@ L.ALS.SynthPolygonLayer.prototype._calculatePolygonParameters = function (widget let layer = this.polygons[name], latLngs = layer.getLatLngs()[0]; let widgetContainer = this.polygonsWidgets[name]; - layer.lngCellSizeInMeters = this.getParallelOrMeridianLineLength(latLngs[0], latLngs[1], false); - layer.latCellSizeInMeters = this.getParallelOrMeridianLineLength(latLngs[1], latLngs[2], false); + if (this.calculateCellSizeForPolygons) { + layer.lngCellSizeInMeters = this.getParallelOrMeridianLineLength(latLngs[0], latLngs[1], false); + layer.latCellSizeInMeters = this.getParallelOrMeridianLineLength(latLngs[1], latLngs[2], false); - widgetContainer.getWidgetById("lngCellSizeInMeters").setValue(layer.lngCellSizeInMeters); - widgetContainer.getWidgetById("latCellSizeInMeters").setValue(layer.latCellSizeInMeters); + widgetContainer.getWidgetById("lngCellSizeInMeters").setValue(layer.lngCellSizeInMeters); + widgetContainer.getWidgetById("latCellSizeInMeters").setValue(layer.latCellSizeInMeters); + } layer.minHeight = widgetContainer.getWidgetById("minHeight").getValue(); layer.maxHeight = widgetContainer.getWidgetById("maxHeight").getValue(); @@ -98,30 +108,4 @@ L.ALS.SynthPolygonLayer.prototype._calculatePolygonParameters = function (widget widgetContainer.getWidgetById(name).setValue(value); } } - - // Draw thick borders around selected polygons - this.mergePolygons(); - this.bordersGroup.clearLayers(); - if (this.mergedPolygons.length === 0) { - this._clearPaths(); - return; - } - - for (let polygon of this.mergedPolygons) { - let latLngs = []; - for (let p of polygon) - latLngs.push([p[1], p[0]]); - - if (!this.useZoneNumbers) - continue; - - this.bordersGroup.addLayer(L.polyline(latLngs, { - weight: this.lineThicknessValue * this.bordersGroup.thicknessMultiplier, - color: this.getWidgetById("borderColor").getValue() - } - )); - } - this._drawPaths(); - - this.writeToHistoryDebounced(); } \ No newline at end of file diff --git a/SynthPolygonLayer.js b/SynthPolygonLayer.js index 749a2823..0f686077 100644 --- a/SynthPolygonLayer.js +++ b/SynthPolygonLayer.js @@ -1,4 +1,4 @@ -L.ALS.SynthPolygonLayer = L.ALS.SynthBaseDrawLayer.extend({ +L.ALS.SynthRectangleBaseLayer = L.ALS.SynthBaseDrawLayer.extend({ defaultName: "Polygon Layer", drawControls: { polygon: { diff --git a/SynthPolygonLayer/SynthPolygonLayer.js b/SynthPolygonLayer/SynthPolygonLayer.js index 3f60f238..341f82b7 100644 --- a/SynthPolygonLayer/SynthPolygonLayer.js +++ b/SynthPolygonLayer/SynthPolygonLayer.js @@ -1,169 +1,261 @@ -// This file contains class definitions and menu. For other stuff, see other files in this directory. - -const MathTools = require("../MathTools.js"); -const turfHelpers = require("@turf/helpers"); +require("./SynthPolygonWizard.js"); require("./SynthPolygonSettings.js"); -let GeoTIFFParser; -try { - GeoTIFFParser = require("../GeoTIFFParser.js"); -} catch (e) { -} +const MathTools = require("../MathTools.js"); +const proj4 = require("proj4"); -/** - * Base layer for rectangle-based planning - * - * @class - * @extends L.ALS.SynthBaseLayer - */ -L.ALS.SynthPolygonLayer = L.ALS.SynthBaseLayer.extend( /** @lends L.ALS.SynthPolygonLayer.prototype */ { +L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend({ - _currentStandardScale: -1, + calculateCellSizeForPolygons: false, + defaultName: "Polygon Layer", + borderColorLabel: "rectangleBorderColor", + fillColorLabel: "rectangleFillColor", - borderColorLabel: "", - fillColorLabel: "", + init: function (wizardResults, settings) { + this.copySettingsToThis(settings); - useZoneNumbers: false, + this.internalConnections = L.featureGroup(); + this.externalConnections = L.featureGroup(); + this.pathGroup = L.featureGroup(); + this.pointsGroup = L.featureGroup(); + + L.ALS.SynthPolygonBaseLayer.prototype.init.call(this, settings, + this.internalConnections, + this.externalConnections, + this.pathGroup, + this.pointsGroup, + "polygonPathsColor", + "polygonHidePaths", + ); - /** - * Indicates whether the grid is displayed or not. - * @type {boolean} - */ - isDisplayed: true, + this.enableDraw({ + polygon: { + shapeOptions: { + color: "#ff0000", + weight: this.lineThicknessValue + } + } + }, this.polygonGroup); - _doHidePolygonWidgets: false, - _doHidePathsNumbers: false, + this.calculateThreshold(settings); // Update hiding threshold + this.calculateParameters(); + this.updateLayersVisibility(); + }, - init: function (wizardResults, settings) { - this.copySettingsToThis(settings); + onEditEnd: function () { + let color = this.getWidgetById("color0").getValue(), + lineOptions = { + color, thickness: this.lineThicknessValue, segmentsNumber: L.GEODESIC_SEGMENTS, + } - this.polygons = {}; - this.polygonsWidgets = {}; - this.serializationIgnoreList.push("polygons", "lngDistance", "latDistance", "_currentStandardScale"); + for (let name in this.polygons) + this.removePolygon(this.polygons[name], false); + this.polygons = {} + this.clearPaths(); - // To optimize the grid and reduce visual clutter, let's: - // 1. Display only visible polygons. If we'll render the whole thing, user will need from couple of MBs to TBs of RAM. - // 2. Hide grid when it'll contain a lot of polygons and becomes messy - // Additional redrawing actually won't introduce any noticeable delay. + let layersWereRemoved = false; - // Create empty groups containing our stuff. Yeah, I hate copying too, but I want code completion :D + // Build paths for each polygon. - this.polygonGroup = L.featureGroup(); - this.widgetsGroup = L.featureGroup(); - this.bordersGroup = L.featureGroup(); - this.bordersGroup.thicknessMultiplier = 4; - this.latPointsGroup = L.featureGroup(); - this.lngPointsGroup = L.featureGroup(); - this.labelsGroup = new L.LabelLayer(false); + // The heuristics follows an assumption that the shortest path will always be parallel + // to the edge of the polygon. - this.pathsByParallels = L.featureGroup(); - this.parallelsInternalConnections = L.featureGroup(); - this.parallelsExternalConnections = L.featureGroup(); + // To build parallel paths, first, we build a line that is perpendicular to the edge (let's call it directional). + // Then, for each intermediate point (distances between points are equal to By) of the directional line, + // we build a line that is perpendicular to the directional line - a path. + // Then we crop the path by polygon by projecting both path and polygon to the WebMercator. - this.pathsByMeridians = L.featureGroup(); - this.meridiansInternalConnections = L.featureGroup(); - this.meridiansExternalConnections = L.featureGroup(); + // Sometimes we can't use an edge (i.e. when polygon is star-shaped). + // To fix that, we'll build paths using convex hull which'll allow us to get rid of two problems: - this.addLayers(this.polygonGroup, this.widgetsGroup, this.bordersGroup, this.latPointsGroup, this.lngPointsGroup, this.labelsGroup, this.pathsByParallels, this.pathsByMeridians); + // 1. Star-shaped polygons. + // 2. Determining where directional line should be headed. For upper part of the convex hull the direction + // is downwards. For lower, upwards. - L.ALS.SynthBaseLayer.prototype.init.call(this, settings, - this.parallelsInternalConnections, this.parallelsExternalConnections, "parallelsColor", [this.pathsByParallels], - this.meridiansInternalConnections, this.meridiansExternalConnections, "meridiansColor", [this.pathsByMeridians] - ); + // For building perpendicular lines, we'll use gnomonic projection to which we can transfer some + // properties of Euclidean 2D space. + this.polygonGroup.eachLayer((layer) => { + let latLngs = layer.getLatLngs()[0], + {upper, lower} = this.getConvexHull(L.LatLngUtil.cloneLatLngs(latLngs)), + projectedPolygon = [], minLength = Infinity, shortestPath, shortestPathConnections, shortestPathPoints; - this.toUpdateThickness.push(this.polygonGroup, this.bordersGroup, this.latPointsGroup, this.lngPointsGroup); - - /** - * Contains paths' labels' IDs - * @type {string[]} - * @private - */ - this._pathsLabelsIDs = []; - - let DEMFilesLabel = "DEMFiles"; - if (!GeoTIFFParser) - DEMFilesLabel = "DEMFilesWhenGeoTIFFNotSupported"; - if (L.ALS.Helpers.isIElte9) - DEMFilesLabel = "DEMFilesIE9"; - - this.addWidgets( - new L.ALS.Widgets.Checkbox("hidePolygonWidgets", "hidePolygonWidgets", this, "_updateLayersVisibility"), - new L.ALS.Widgets.Checkbox("hideNumbers", "hideNumbers", this, "_updateLayersVisibility"), - new L.ALS.Widgets.Checkbox("hideCapturePoints", "hideCapturePoints", this, "_updateLayersVisibility").setValue(true), - new L.ALS.Widgets.Checkbox("hidePathsConnections", "hidePathsConnections", this, "_updateLayersVisibility"), - new L.ALS.Widgets.Checkbox("hidePathsByMeridians", "hidePathsByMeridians", this, "_updateLayersVisibility"), - new L.ALS.Widgets.Checkbox("hidePathsByParallels", "hidePathsByParallels", this, "_updateLayersVisibility"), - new L.ALS.Widgets.Color("borderColor", this.borderColorLabel, this, "_setColor").setValue(this.borderColor), - new L.ALS.Widgets.Color("fillColor", this.fillColorLabel, this, "_setColor").setValue(this.fillColor), - ); + // Convert polygon coords to layer points, so we can use MathTools + for (let coord of latLngs) { + let {x, y} = this.map.project(coord, 0); + projectedPolygon.push([x, y]); + } + projectedPolygon.push(projectedPolygon[0]); + latLngs.push(latLngs[0]); + + // For upper part, perpendiculars should be headed downwards, for lower, upwards + upper.direction = -1; + lower.direction = 1; + + for (let part of [upper, lower]) { + for (let i = 0; i < part.length - 1; i++) { + // Get a directional line + let line = this.perpendicularLine(part[i], part[i + 1], this.By, "end", part.direction), + perpLinePoints = line.getActualLatLngs()[0], + currentPath = [], currentConnections = [], currentLength = 0, currentPoints = [], + shouldSwapPoints = false, lineAfterPolygonAdded = false; + + // For each point in line (points lying somewhere on path's geodesic) build a path + for (let j = 0; j < perpLinePoints.length - 1; j++) { + if (lineAfterPolygonAdded) + break; + + let p1 = perpLinePoints[j], p2 = perpLinePoints[j + 1], + line = this.perpendicularLine(p1, p2, this.Bx, "both"), + linePoints = line.getActualLatLngs()[0], + projectedLine = []; + + // Project perpendicular line. Also find indices of two closest points to the + // current vertex. + let indexP1, indexP2, lengthP1 = Infinity, lengthP2 = Infinity; + for (let k = 0; k < linePoints.length - 1; k++) { + let coord = linePoints[k], + {x, y} = this.map.project(coord, 0); + projectedLine.push([x, y]); + + if (j !== 0) + continue; + + let length = this.getLineLengthMeters(L.polyline([p1, coord])); + + if (coord.lng < p1.lng && length < lengthP1) { + lengthP1 = length; + indexP1 = k; + } + + if (coord.lng > p1.lng && length < lengthP2) { + lengthP2 = length; + indexP2 = k; + } + } + + let intersection = MathTools.clipLineByPolygon(projectedLine, projectedPolygon); + + if (!intersection) { + // If it's the first point, and there's no intersection, use closest points as + // an intersection with two bases in mind. This doesn't seem to happen, but let's be + // cautious anyway + if (j === 0) { + intersection = [ + projectedLine[indexP1 - 2], + projectedLine[indexP1 + 2] + ]; + } else { + let [newP1, newP2] = currentPath[currentPath.length - 1].getLatLngs(), + interP1 = this.map.project([p1.lat, newP1.lng], 0), + interP2 = this.map.project([p1.lat, newP2.lng], 0); + intersection = [[interP1.x, interP1.y], [interP2.x, interP2.y]]; + lineAfterPolygonAdded = true; + } + } + + let [pathP1, pathP2] = intersection, + pathPoints = shouldSwapPoints ? [pathP2, pathP1] : [pathP1, pathP2]; + + shouldSwapPoints = !shouldSwapPoints; + + let path = L.geodesic([ + this.map.unproject(pathPoints[0], 0), + this.map.unproject(pathPoints[1], 0), + ], lineOptions), + length = path.statistics.sphericalLengthMeters, + numberOfImages = Math.ceil(length / this.Bx) + 4, + extendBy = (this.Bx * numberOfImages - length) / 2 / length; + path.changeLength("both", extendBy); + + currentPath.push(path); + currentLength += path.statistics.sphericalLengthMeters; + currentConnections.push(...path.getLatLngs()); + + let capturePoints = L.geodesic(path.getLatLngs(), {segmentsNumber: numberOfImages}).getActualLatLngs()[0]; + for (let point of capturePoints) + currentPoints.push(this.createCapturePoint(point, color)); + } + + if (currentLength >= minLength) + continue; + + minLength = currentLength; + shortestPath = currentPath; + shortestPathConnections = currentConnections; + shortestPathPoints = currentPoints; + } + } - this.addBaseParametersInputSection(); + // Limit polygon size by limiting total approximate paths count. This is not 100% accurate but close enough. + /*if (!shortestPath) { + layersWereRemoved = true; + this.polygonGroup.removeLayer(layer); + return; + } - this.addWidgets( - new L.ALS.Widgets.File("DEMFiles", DEMFilesLabel, this, "onDEMLoad").setMultiple(true), - new L.ALS.Widgets.Divider("div3"), - ); + this.pathsGroup.addLayer(shortestPath);*/ + this.addPolygon(layer); - this.addBaseParametersOutputSection(); + this.internalConnections.addLayer(L.geodesic(shortestPathConnections, { + ...lineOptions, dashArray: this.dashedLine + })); - this.lngDistance = parseFloat(wizardResults["gridLngDistance"]); - this.latDistance = parseFloat(wizardResults["gridLatDistance"]); + for (let path of shortestPath) + this.pathGroup.addLayer(path); - // Determine whether this grid uses standard scale or not - let scale = wizardResults.gridStandardScales; - if (scale && scale !== "Custom") { - let scaleWithoutSpaces = ""; - for (let i = 2; i < scale.length; i++) { - let char = scale[i]; - if (char === " ") - continue; - scaleWithoutSpaces += char; - } - this._currentStandardScale = parseInt(scaleWithoutSpaces); - this.setName(`${this.defaultName}, ${scale}`); - } else - this._currentStandardScale = Infinity; - this.calculateThreshold(settings); // Update hiding threshold + for (let marker of shortestPathPoints) + this.pointsGroup.addLayer(marker); - this.updateAll(); - this.getWidgetById("hideCapturePoints").callCallback(); - }, - // It overrides parent method, my IDE can't see it - getPathLength: function (layer) { - // Basically, inverse of L.ALS.SynthBaseLayer#getArcAngleByLength - let latLngs = layer instanceof Array ? layer : layer.getLatLngs(), length = 0; + }); + this.updatePathsMeta(); + this.updateLayersVisibility(); + this.calculateParameters(); + return; - for (let i = 0; i < latLngs.length - 1; i += 2) { - // Path length - let p1 = latLngs[i], p2 = latLngs[i + 1], connP = latLngs[i + 2]; - length += this.getParallelOrMeridianLineLength(p1, p2); + if (layersWereRemoved) + window.alert(L.ALS.locale.rectangleLayersRemoved); - // Connection length - if (connP) - length += this.getParallelOrMeridianLineLength(p2, connP); - } - return length; + this.map.addLayer(this.labelsGroup); // Nothing in the base layer hides or shows it, so it's only hidden in code above + this.updateLayersVisibility(); + this.calculateParameters(); + this.writeToHistory(); }, - getParallelOrMeridianLineLength: function (p1, p2, useFlightHeight = true) { - let r = this._getEarthRadius(useFlightHeight), {x, y} = MathTools.getXYPropertiesForPoint(p1), - p1Y = p1[y], lngDiff = Math.abs(p1[x] - p2[x]); + perpendicularLine: function (p1, p2, basis, extendFrom = "end", direction = 1) { + // Project coordinates to the gnomonic projection and work with lines as with vectors. + let proj = this.getProjFromWgs(p1.lng, p1.lat), + [p2x, p2y] = proj.forward([p2.lng, p2.lat]), + // Find an orthogonal vector + perpX = 1000, + perpY = -perpX * p2x / p2y, + [perpLng, perpLat] = proj.inverse([perpX, perpY]); + + // Check if orthogonal vector in correct direction. If not, reverse it. + if (Math.sign(perpLat - p1.lat) !== direction) { + perpX = -perpX; + perpY = -perpY; + [perpLng, perpLat] = proj.inverse([perpX, perpY]); + } - // By meridians - if (lngDiff <= MathTools.precision) - return r * turfHelpers.degreesToRadians(Math.abs(p1Y - p2[y])); + // Build 175 degrees long geodesic + // TODO: Make it be less than 180 after extending + let targetLength = 175 / 180 * this.getEarthRadius(false), + segmentsNumber = Math.ceil(targetLength / basis), + geodesic = new L.Geodesic([p1, [perpLat, perpLng]], {segmentsNumber}), + length = geodesic.statistics.sphericalLengthMeters, + extendBy = (segmentsNumber * basis - length) / length; - // By parallels - let angle = turfHelpers.degreesToRadians(90 - Math.abs(p1Y)); - return turfHelpers.degreesToRadians(lngDiff) * Math.sin(angle) * r; - } + geodesic.changeLength(extendFrom, extendBy); + return geodesic; + }, -}); + getProjFromWgs: function (lng0, lat0) { + return proj4("+proj=longlat +ellps=sphere +no_defs", `+proj=gnom +lat_0=${lat0} +lon_0=${lng0} +x_0=0 +y_0=0 +ellps=sphere +datum=WGS84 +units=m +no_defs`); + }, -require("./DEM.js"); -require("./drawPaths.js"); -require("./misc.js"); -require("./polygons.js"); -require("./serialization.js"); -require("./toGeoJSON.js"); \ No newline at end of file + statics: { + wizard: L.ALS.SynthPolygonWizard, + settings: new L.ALS.SynthPolygonSettings(), + } +}); \ No newline at end of file diff --git a/SynthPolygonLayer/SynthPolygonSettings.js b/SynthPolygonLayer/SynthPolygonSettings.js index c3f0812d..0aa015ce 100644 --- a/SynthPolygonLayer/SynthPolygonSettings.js +++ b/SynthPolygonLayer/SynthPolygonSettings.js @@ -1,36 +1,14 @@ /** - * Settings for SynthPolygonLayer + * Settings for SynthGridLayer * * @class * @extends L.ALS.Settings */ -L.ALS.SynthPolygonSettings = L.ALS.SynthBaseSettings.extend( /** @lends L.ALS.SynthPolygonSettings.prototype */ { +L.ALS.SynthPolygonSettings = L.ALS.SynthPolygonBaseSettings.extend( /** @lends L.ALS.SynthRectangleSettings.prototype */ { - borderColor: "#6495ed", - fillColor: "#6495ed", - meridiansColor: "#ad0000", - parallelsColor: "#007800", - - addColorWidgets: function (borderLabel, fillLabel) { - this.addWidget( - new L.ALS.Widgets.Color("borderColor", borderLabel).setValue(this.borderColor), - this.borderColor - ); - - this.addWidget( - new L.ALS.Widgets.Color("fillColor", fillLabel).setValue(this.fillColor), - this.fillColor - ); - - this.addWidget( - new L.ALS.Widgets.Color("color0", "defaultParallelsColor").setValue(this.parallelsColor), - this.parallelsColor - ); - - this.addWidget( - new L.ALS.Widgets.Color("color1", "defaultMeridiansColor").setValue(this.meridiansColor), - this.meridiansColor - ); + initialize: function () { + L.ALS.SynthPolygonBaseSettings.prototype.initialize.call(this); + this.addColorWidgets("defaultRectangleBorderColor", "defaultRectangleFillColor", false); } }); \ No newline at end of file diff --git a/SynthPolygonLayer/SynthPolygonWizard.js b/SynthPolygonLayer/SynthPolygonWizard.js new file mode 100644 index 00000000..4a113fc1 --- /dev/null +++ b/SynthPolygonLayer/SynthPolygonWizard.js @@ -0,0 +1,3 @@ +L.ALS.SynthPolygonWizard = L.ALS.EmptyWizard.extend({ + displayName: "polygonLayerName" +}); \ No newline at end of file diff --git a/SynthPolygonLayer/misc.js b/SynthPolygonLayer/misc.js deleted file mode 100644 index 6e07428f..00000000 --- a/SynthPolygonLayer/misc.js +++ /dev/null @@ -1,151 +0,0 @@ -// Misc methods, event handlers, etc which most likely won't change in future - -const turfHelpers = require("@turf/helpers"); - -L.ALS.SynthPolygonLayer.prototype._setColor = function (widget) { - this[widget.id] = widget.getValue(); - this.updateAll(); -} - -L.ALS.SynthPolygonLayer.prototype.calculateParameters = function () { - L.ALS.SynthBaseLayer.prototype.calculateParameters.call(this); - - // Calculate estimated paths count for a polygon. Values are somewhat true for equatorial regions. - // We'll check if it's too small (in near-polar regions, there'll be only one path when value is 2) or too big. - let latLngs = ["lat", "lng"]; - for (let name of latLngs) { - let cellSize = Math.round(turfHelpers.radiansToLength(turfHelpers.degreesToRadians(this[name + "Distance"]), "meters")); - this[name + "FakePathsCount"] = Math.ceil(cellSize / this.By); - } - - this._calculatePolygonParameters(); -} - -L.ALS.SynthPolygonLayer.prototype._updateLayersVisibility = function () { - let hidePathsByMeridians = this.getWidgetById("hidePathsByMeridians").getValue(), - hidePathsByParallels = this.getWidgetById("hidePathsByParallels").getValue(); - - if (this.getWidgetById("hidePathsConnections").getValue()) { - this.parallelsInternalConnections.remove(); - this.parallelsExternalConnections.remove(); - this.meridiansInternalConnections.remove(); - this.meridiansExternalConnections.remove(); - } else { - this.hideOrShowLayer(hidePathsByParallels, this.parallelsInternalConnections); - this.hideOrShowLayer(hidePathsByParallels, this.parallelsExternalConnections); - this.hideOrShowLayer(hidePathsByMeridians, this.meridiansInternalConnections); - this.hideOrShowLayer(hidePathsByMeridians, this.meridiansExternalConnections); - } - - if (this.getWidgetById("hideCapturePoints").getValue()) { - this.latPointsGroup.remove(); - this.lngPointsGroup.remove(); - } else { - this.hideOrShowLayer(hidePathsByParallels, this.lngPointsGroup); - this.hideOrShowLayer(hidePathsByMeridians, this.latPointsGroup); - } - - this.hideOrShowLayer(hidePathsByParallels, this.pathsByParallels); - this.hideOrShowLayer(hidePathsByMeridians, this.pathsByMeridians); - - this._doHidePolygonWidgets = this.getWidgetById("hidePolygonWidgets").getValue(); - this.hideOrShowLayer(this._doHidePolygonWidgets || this._shouldHideEverything, this.widgetsGroup); - this._doHidePathsNumbers = this.getWidgetById("hideNumbers").getValue(); - this._drawPaths(); // We have to redraw paths both for hiding one of the paths and hiding numbers -} - -/** - * Updates grid by redrawing all polygons, recalculating stuff, etc - */ -L.ALS.SynthPolygonLayer.prototype.updateAll = function () { - // Legacy code, though, this might be used for something else later - this.calculateParameters(); -} - -/** - * Generates polygon name for adding into this.polygons - * @param polygon Polygon to generate name for - * @return {string} Name for given polygon - * @protected - */ -L.ALS.SynthPolygonLayer.prototype._generatePolygonName = function (polygon) { - let firstPoint = polygon.getLatLngs()[0][0]; - return "p_" + this.toFixed(firstPoint.lat) + "_" + this.toFixed(firstPoint.lng); -} - -/** - * Loops over pathsByParallels and pathsByMeridians and calls callback - * @param callback {function(Polyline)} Callback function that accepts polyline (path) - */ -L.ALS.SynthPolygonLayer.prototype.forEachPath = function (callback) { - let groups = ["pathsByParallels", "pathsByMeridians"]; - for (let group of groups) - callback(this[group]); -} - -L.ALS.SynthPolygonLayer.prototype.onHide = function () { - this.forEachPath((path) => { - path.remove(); - }); -} - -L.ALS.SynthPolygonLayer.prototype.onShow = function () { - this._updateLayersVisibility(); -} - -L.ALS.SynthPolygonLayer.prototype.onDelete = function () { - this.onHide(); -} - -/** - * Truncates argument to fifth number after point. - * @param n Number to truncate - * @return {number} Truncated number - */ -L.ALS.SynthPolygonLayer.prototype.toFixed = function (n) { - return parseFloat(n.toFixed(5)); -} - -L.ALS.SynthPolygonLayer.prototype._closestGreater = function (current, divider) { - return Math.ceil(current / divider) * divider; -} - -L.ALS.SynthPolygonLayer.prototype._closestLess = function (current, divider) { - return Math.floor(current / divider) * divider; -} - -L.ALS.SynthPolygonLayer.prototype.applyNewSettings = function (settings) { - this.calculateThreshold(settings); - this.updateAll(); -} - -/** - * Calculates grid hiding threshold - * @param settings {SettingsObject} Settings to calculate threshold from - */ -L.ALS.SynthPolygonLayer.prototype.calculateThreshold = function (settings) { - let multiplier = (settings.gridHidingFactor - 5) / 5; // Factor is in range [1..10]. Let's make it [-1...1] - this.minThreshold = 15 + 10 * multiplier; - this.maxThreshold = 60 + 60 * multiplier; - - // If grid will have labels, on lower zoom levels map will become both messy and unusably slow. So we have to set higher hiding threshold. - this.hidingThreshold = this._currentStandardScale === Infinity ? this.minThreshold : this.maxThreshold; -} - -/** - * Merges selected polygon into one GeoJSON feature. - * @return number[][][] Merged feature - */ -L.ALS.SynthPolygonLayer.prototype.mergePolygons = function () { - // Convert object with polygons to an array and push edges instead of points here - this.mergedPolygons = []; - for (let id in this.polygons) { - let latLngs = this.polygons[id].getLatLngs()[0], poly = []; - for (let p of latLngs) - poly.push([p.lng, p.lat]); - poly.push(poly[0]); // We need to close the polygons to use MathTools stuff - if (this.useZoneNumbers) - poly.zoneNumber = this.polygonsWidgets[id].getWidgetById("zoneNumber").getValue(); - this.mergedPolygons.push(poly); - } -} \ No newline at end of file diff --git a/SynthPolygonLayer/toGeoJSON.js b/SynthPolygonLayer/toGeoJSON.js deleted file mode 100644 index 7aac2549..00000000 --- a/SynthPolygonLayer/toGeoJSON.js +++ /dev/null @@ -1,47 +0,0 @@ -const geojsonMerge = require("@mapbox/geojson-merge"); // Using this since turfHelpers.featureCollection() discards previously defined properties. - -L.ALS.SynthPolygonLayer.prototype.toGeoJSON = function () { - let jsons = []; - - for (let name in this.polygons) { - if (!this.polygons.hasOwnProperty(name)) - continue; - let polygon = this.polygons[name], - polygonJson = polygon.toGeoJSON(), - props = ["polygonName", "minHeight", "maxHeight", "meanHeight", "absoluteHeight", "reliefType", "elevationDifference", "latCellSizeInMeters", "lngCellSizeInMeters"]; - for (let prop of props) - polygonJson.properties[prop] = polygon[prop]; - polygonJson.properties.name = "Selected cell"; - jsons.push(polygonJson); - } - - let airport = this._airportMarker.toGeoJSON(); - airport.name = "Airport"; - jsons.push(airport); - - if (this.pathsByMeridians.getLayers().length === 0 || this.pathsByParallels.getLayers().length === 0) { - window.alert(`No paths has been drawn in layer "${this.getName()}"! You'll get only selected gird cells and airport position.`); - return geojsonMerge.merge(jsons); - } - - // See _calculateParameters - let parallelsProps = {name: "Flight paths by parallels"}, meridiansProps = {name: "Flight paths by meridians"}; - - for (let prop of [parallelsProps, meridiansProps]) { - for (let param of this.propertiesToExport) - prop[param] = this[param]; - } - - jsons.push(L.ALS.SynthBaseLayer.prototype.toGeoJSON.call(this, parallelsProps, meridiansProps)); - - let pointsParams = [["capturePointsByMeridians", this.latPointsGroup.getLayers()], ["capturePointsByParallels", this.lngPointsGroup.getLayers()]]; - for (let param of pointsParams) { - for (let layer of param[1]) { - let pointsJson = layer.toGeoJSON(); - pointsJson.name = param[0]; - jsons.push(pointsJson); - } - } - - return geojsonMerge.merge(jsons); -} \ No newline at end of file diff --git a/SynthRectangleBaseLayer/SynthRectangleBaseLayer.js b/SynthRectangleBaseLayer/SynthRectangleBaseLayer.js new file mode 100644 index 00000000..8048066e --- /dev/null +++ b/SynthRectangleBaseLayer/SynthRectangleBaseLayer.js @@ -0,0 +1,124 @@ +// This file contains class definitions and menu. For other stuff, see other files in this directory. + +const MathTools = require("../MathTools.js"); +const turfHelpers = require("@turf/helpers"); +require("../SynthPolygonBaseLayer/SynthPolygonBaseSettings.js"); + +/** + * Base layer for rectangle-based planning + * + * @class + * @extends L.ALS.SynthPolygonBaseLayer + */ +L.ALS.SynthRectangleBaseLayer = L.ALS.SynthPolygonBaseLayer.extend( /** @lends L.ALS.SynthRectangleBaseLayer.prototype */ { + + _currentStandardScale: -1, + useRect: true, + borderColorLabel: "", + fillColorLabel: "", + + init: function (wizardResults, settings) { + this.copySettingsToThis(settings); + + + // To optimize the grid and reduce visual clutter, let's: + // 1. Display only visible polygons. If we'll render the whole thing, user will need from couple of MBs to TBs of RAM. + // 2. Hide grid when it'll contain a lot of polygons and becomes messy + // Additional redrawing actually won't introduce any noticeable delay. + + // Create empty groups containing our stuff. Yeah, I hate copying too, but I want code completion :D + this.latPointsGroup = L.featureGroup(); + this.lngPointsGroup = L.featureGroup(); + + this.pathsByParallels = L.featureGroup(); + this.parallelsInternalConnections = L.featureGroup(); + this.parallelsExternalConnections = L.featureGroup(); + + this.pathsByMeridians = L.featureGroup(); + this.meridiansInternalConnections = L.featureGroup(); + this.meridiansExternalConnections = L.featureGroup(); + + L.ALS.SynthPolygonBaseLayer.prototype.init.call(this, settings, + // Parallels args + this.parallelsInternalConnections, + this.parallelsExternalConnections, + this.pathsByParallels, + this.lngPointsGroup, + "parallelsColor", + "hidePathsByParallels", + + // Meridians args + this.meridiansInternalConnections, + this.meridiansExternalConnections, + this.pathsByMeridians, + this.latPointsGroup, + "meridiansColor", + "hidePathsByMeridians", + ); + + /** + * Contains paths' labels' IDs + * @type {string[]} + * @private + */ + this._pathsLabelsIDs = []; + + this.lngDistance = parseFloat(wizardResults["gridLngDistance"]); + this.latDistance = parseFloat(wizardResults["gridLatDistance"]); + + // Determine whether this grid uses standard scale or not + let scale = wizardResults.gridStandardScales; + if (scale && scale !== "Custom") { + let scaleWithoutSpaces = ""; + for (let i = 2; i < scale.length; i++) { + let char = scale[i]; + if (char === " ") + continue; + scaleWithoutSpaces += char; + } + this._currentStandardScale = parseInt(scaleWithoutSpaces); + this.setName(`${this.defaultName}, ${scale}`); + } else + this._currentStandardScale = Infinity; + this.calculateThreshold(settings); // Update hiding threshold + + this.calculateParameters(); + this.getWidgetById("hideCapturePoints").callCallback(); + }, + + // It overrides parent method, my IDE can't see it + getPathLength: function (layer) { + // Basically, inverse of L.ALS.SynthBaseLayer#getArcAngleByLength + let latLngs = layer instanceof Array ? layer : layer.getLatLngs(), length = 0; + + for (let i = 0; i < latLngs.length - 1; i += 2) { + // Path length + let p1 = latLngs[i], p2 = latLngs[i + 1], connP = latLngs[i + 2]; + length += this.getParallelOrMeridianLineLength(p1, p2); + + // Connection length + if (connP) + length += this.getParallelOrMeridianLineLength(p2, connP); + } + return length; + }, + + getParallelOrMeridianLineLength: function (p1, p2, useFlightHeight = true) { + let r = this.getEarthRadius(useFlightHeight), {x, y} = MathTools.getXYPropertiesForPoint(p1), + p1Y = p1[y], lngDiff = Math.abs(p1[x] - p2[x]); + + // By meridians + if (lngDiff <= MathTools.precision) + return r * turfHelpers.degreesToRadians(Math.abs(p1Y - p2[y])); + + // By parallels + let angle = turfHelpers.degreesToRadians(90 - Math.abs(p1Y)); + return turfHelpers.degreesToRadians(lngDiff) * Math.sin(angle) * r; + } + +}); + +require("./drawPaths.js"); +require("./misc.js"); +require("./serialization.js"); +require("./toGeoJSON.js"); \ No newline at end of file diff --git a/SynthPolygonLayer/drawPaths.js b/SynthRectangleBaseLayer/drawPaths.js similarity index 89% rename from SynthPolygonLayer/drawPaths.js rename to SynthRectangleBaseLayer/drawPaths.js index 69c9de61..8eacfc1d 100644 --- a/SynthPolygonLayer/drawPaths.js +++ b/SynthRectangleBaseLayer/drawPaths.js @@ -3,18 +3,16 @@ const turfArea = require("@turf/area").default; const MathTools = require("../MathTools.js"); const turfHelpers = require("@turf/helpers"); -L.ALS.SynthPolygonLayer.prototype._clearPaths = function () { - let groupsToClear = [this.pathsByParallels, this.pathsByMeridians, this.meridiansExternalConnections, this.meridiansInternalConnections, this.parallelsExternalConnections, this.parallelsInternalConnections, this.latPointsGroup, this.lngPointsGroup]; - for (let group of groupsToClear) - group.clearLayers(); +L.ALS.SynthRectangleBaseLayer.prototype.clearPaths = function () { + L.ALS.SynthPolygonBaseLayer.prototype.clearPaths.call(this); for (let id of this._pathsLabelsIDs) this.labelsGroup.deleteLabel(id); this._pathsLabelsIDs = []; } -L.ALS.SynthPolygonLayer.prototype._drawPaths = function () { - this._clearPaths(); +L.ALS.SynthRectangleBaseLayer.prototype._drawPaths = function () { + this.clearPaths(); // Validate estimated paths count @@ -51,26 +49,25 @@ L.ALS.SynthPolygonLayer.prototype._drawPaths = function () { * Draws flight paths. Use _drawPaths wrapper to draw paths instead of this. * @private */ -L.ALS.SynthPolygonLayer.prototype._drawPathsWorker = function (isParallels) { +L.ALS.SynthRectangleBaseLayer.prototype._drawPathsWorker = function (isParallels) { - let pathName, nameForOutput, color, connectionsGroup, widgetId, extensionIndex; + let pathGroup, nameForOutput, color, connectionsGroup, widgetId, extensionIndex; if (isParallels) { - pathName = "pathsByParallels"; + pathGroup = this.pathsByParallels; connectionsGroup = this.parallelsInternalConnections; nameForOutput = "lng"; color = this["color0"]; widgetId = "hidePathsByParallels"; extensionIndex = 0; } else { - pathName = "pathsByMeridians"; + pathGroup = this.pathsByMeridians; connectionsGroup = this.meridiansInternalConnections; nameForOutput = "lat"; color = this["color1"]; widgetId = "hidePathsByMeridians"; extensionIndex = 1; } - let pathGroup = this[pathName], - pointsName = nameForOutput + "PointsGroup", + let pointsName = nameForOutput + "PointsGroup", lineOptions = { color, weight: this.lineThicknessValue diff --git a/SynthRectangleBaseLayer/misc.js b/SynthRectangleBaseLayer/misc.js new file mode 100644 index 00000000..e78991df --- /dev/null +++ b/SynthRectangleBaseLayer/misc.js @@ -0,0 +1,78 @@ +// Misc methods, event handlers, etc which most likely won't change in future + +const turfHelpers = require("@turf/helpers"); + +L.ALS.SynthRectangleBaseLayer.prototype.calculateParameters = function () { + L.ALS.SynthBaseLayer.prototype.calculateParameters.call(this); + + // Calculate estimated paths count for a polygon. Values are somewhat true for equatorial regions. + // We'll check if it's too small (in near-polar regions, there'll be only one path when value is 2) or too big. + let latLngs = ["lat", "lng"]; + for (let name of latLngs) { + let cellSize = Math.round(turfHelpers.radiansToLength(turfHelpers.degreesToRadians(this[name + "Distance"]), "meters")); + this[name + "FakePathsCount"] = Math.ceil(cellSize / this.By); + } + + this.calculatePolygonParameters(); +} + +L.ALS.SynthRectangleBaseLayer.prototype.updateLayersVisibility = function () { + L.ALS.SynthPolygonBaseLayer.prototype.updateLayersVisibility.call(this); + this._drawPaths(); // We have to redraw paths both for hiding one of the paths and hiding numbers +} + +L.ALS.SynthRectangleBaseLayer.prototype._closestGreater = function (current, divider) { + return Math.ceil(current / divider) * divider; +} + +L.ALS.SynthRectangleBaseLayer.prototype._closestLess = function (current, divider) { + return Math.floor(current / divider) * divider; +} + +L.ALS.SynthRectangleBaseLayer.prototype.calculatePolygonParameters = function (widget) { + L.ALS.SynthPolygonBaseLayer.prototype.calculatePolygonParameters.call(this, widget); + + // Draw thick borders around selected polygons + this.mergePolygons(); + this.bordersGroup.clearLayers(); + if (this.mergedPolygons.length === 0) { + this.clearPaths(); + return; + } + + for (let polygon of this.mergedPolygons) { + let latLngs = []; + for (let p of polygon) + latLngs.push([p[1], p[0]]); + + if (!this.useZoneNumbers) + continue; + + this.bordersGroup.addLayer(L.polyline(latLngs, { + weight: this.lineThicknessValue * this.bordersGroup.thicknessMultiplier, + color: this.getWidgetById("borderColor").getValue() + } + )); + } + this._drawPaths(); + + this.writeToHistoryDebounced(); +} + +/** + * Merges selected polygon into one GeoJSON feature. + * @return {number[][][]} Merged feature + */ +L.ALS.SynthRectangleBaseLayer.prototype.mergePolygons = function () { + // Convert object with polygons to an array and push edges instead of points here + this.mergedPolygons = []; + for (let id in this.polygons) { + let latLngs = this.polygons[id].getLatLngs()[0], poly = []; + for (let p of latLngs) + poly.push([p.lng, p.lat]); + poly.push(poly[0]); // We need to close the polygons to use MathTools stuff + if (this.useZoneNumbers) + poly.zoneNumber = this.polygonsWidgets[id].getWidgetById("zoneNumber").getValue(); + this.mergedPolygons.push(poly); + } +} \ No newline at end of file diff --git a/SynthPolygonLayer/serialization.js b/SynthRectangleBaseLayer/serialization.js similarity index 75% rename from SynthPolygonLayer/serialization.js rename to SynthRectangleBaseLayer/serialization.js index 9edcd047..f4702f17 100644 --- a/SynthPolygonLayer/serialization.js +++ b/SynthRectangleBaseLayer/serialization.js @@ -1,4 +1,4 @@ -L.ALS.SynthPolygonLayer.prototype.serialize = function (seenObjects) { +L.ALS.SynthRectangleBaseLayer.prototype.serialize = function (seenObjects) { let serialized = this.getObjectToSerializeTo(seenObjects); serialized.polygonsWidgets = L.ALS.Serializable.serializeAnyObject(this.polygonsWidgets, seenObjects); @@ -16,9 +16,9 @@ L.ALS.SynthPolygonLayer.prototype.serialize = function (seenObjects) { return serialized; } -L.ALS.SynthPolygonLayer._toUpdateColors = ["borderColor", "fillColor", "color0", "color1"]; +L.ALS.SynthRectangleBaseLayer._toUpdateColors = ["borderColor", "fillColor", "color0", "color1"]; -L.ALS.SynthPolygonLayer.deserialize = function (serialized, layerSystem, settings, seenObjects) { +L.ALS.SynthRectangleBaseLayer.deserialize = function (serialized, layerSystem, settings, seenObjects) { let object = L.ALS.Layer.deserialize(serialized, layerSystem, settings, seenObjects); object.isAfterDeserialization = true; @@ -34,10 +34,10 @@ L.ALS.SynthPolygonLayer.deserialize = function (serialized, layerSystem, setting } for (let color of this._toUpdateColors) - object._setColor(object.getWidgetById(color)); + object.setColor(object.getWidgetById(color)); object.setAirportLatLng(); - object.updateAll(); + object.calculateParameters(); return object; } \ No newline at end of file diff --git a/SynthRectangleBaseLayer/toGeoJSON.js b/SynthRectangleBaseLayer/toGeoJSON.js new file mode 100644 index 00000000..a90c583b --- /dev/null +++ b/SynthRectangleBaseLayer/toGeoJSON.js @@ -0,0 +1,31 @@ +const geojsonMerge = require("@mapbox/geojson-merge"); // Using this since turfHelpers.featureCollection() discards previously defined properties. + +L.ALS.SynthRectangleBaseLayer.prototype.toGeoJSON = function () { + let jsons = this.baseFeaturesToGeoJSON(); + + if (this.pathsByMeridians.getLayers().length === 0 || this.pathsByParallels.getLayers().length === 0) { + window.alert(`${L.ALS.locale.jsonNoPaths1} "${this.getName()}"! ${L.ALS.locale.jsonNoPaths2}`); + return geojsonMerge.merge(jsons); + } + + // See _calculateParameters + let parallelsProps = {name: "Flight paths by parallels"}, meridiansProps = {name: "Flight paths by meridians"}; + + for (let prop of [parallelsProps, meridiansProps]) { + for (let param of this.propertiesToExport) + prop[param] = this[param]; + } + + jsons.push(L.ALS.SynthBaseLayer.prototype.toGeoJSON.call(this, parallelsProps, meridiansProps)); + + let pointsParams = [["capturePointsByMeridians", this.latPointsGroup], ["capturePointsByParallels", this.lngPointsGroup]]; + for (let param of pointsParams) { + param[1].eachLayer((layer) => { + let pointsJson = layer.toGeoJSON(); + pointsJson.name = param[0]; + jsons.push(pointsJson); + }); + } + + return geojsonMerge.merge(jsons); +} \ No newline at end of file diff --git a/SynthRectangleLayer/SynthRectangleLayer.js b/SynthRectangleLayer/SynthRectangleLayer.js index af362cd7..6e0d6414 100644 --- a/SynthRectangleLayer/SynthRectangleLayer.js +++ b/SynthRectangleLayer/SynthRectangleLayer.js @@ -1,14 +1,14 @@ require("./SynthRectangleWizard.js"); require("./SynthRectangleSettings.js"); -L.ALS.SynthRectangleLayer = L.ALS.SynthPolygonLayer.extend({ +L.ALS.SynthRectangleLayer = L.ALS.SynthRectangleBaseLayer.extend({ defaultName: "Rectangle Layer", borderColorLabel: "rectangleBorderColor", fillColorLabel: "rectangleFillColor", init: function (wizardResults, settings) { - L.ALS.SynthPolygonLayer.prototype.init.call(this, wizardResults, settings); + L.ALS.SynthRectangleBaseLayer.prototype.init.call(this, wizardResults, settings); this.enableDraw({ rectangle: { @@ -20,21 +20,15 @@ L.ALS.SynthRectangleLayer = L.ALS.SynthPolygonLayer.extend({ }, this.polygonGroup); }, - onEditStart: function () { - let groups = ["labelsGroup", "widgetsGroup", "pathsByParallels", "pathsByMeridians", "parallelsInternalConnections", "parallelsExternalConnections", "meridiansInternalConnections", "meridiansExternalConnections", "latPointsGroup", "lngPointsGroup"]; - for (let group of groups) - this.hideOrShowLayer(true, this[group]); - }, - onEditEnd: function () { for (let name in this.polygons) this.removePolygon(this.polygons[name], false); this.polygons = {} - let layers = this.polygonGroup.getLayers(), layersWereRemoved = false; + let layersWereRemoved = false; - for (let i = 0; i < layers.length; i++) { - let layer = layers[i], bounds = layer.getBounds(), topLeft = bounds.getNorthWest(), + this.polygonGroup.eachLayer((layer) => { + let bounds = layer.getBounds(), topLeft = bounds.getNorthWest(), arrTopLeft = [topLeft.lng, topLeft.lat], lngDiff = Math.abs(bounds.getWest() - bounds.getEast()), latDiff = Math.abs(bounds.getNorth() - bounds.getSouth()), @@ -46,42 +40,23 @@ L.ALS.SynthRectangleLayer = L.ALS.SynthPolygonLayer.extend({ if (lngPathsCount + latPathsCount > 150) { layersWereRemoved = true; this.polygonGroup.removeLayer(layer); - continue; + return; } this.addPolygon(layer); - } + }); if (layersWereRemoved) window.alert(L.ALS.locale.rectangleLayersRemoved); this.map.addLayer(this.labelsGroup); // Nothing in the base layer hides or shows it, so it's only hidden in code above - this._updateLayersVisibility(); - this.updateAll(); + this.updateLayersVisibility(); + this.calculateParameters(); this.writeToHistory(); }, - _setColor: function (widget) { - L.ALS.SynthPolygonLayer.prototype._setColor.call(this, widget); - this.updateRectanglesColors(); - }, - - updateRectanglesColors: function () { - let color = this.getWidgetById("borderColor").getValue(), - fillColor = this.getWidgetById("fillColor").getValue(); - for (let id in this.polygons) - this.polygons[id].setStyle({color, fillColor}); - }, - statics: { wizard: L.ALS.SynthRectangleWizard, settings: new L.ALS.SynthRectangleSettings(), - deserialize: function (serialized, layerSystem, settings, seenObjects) { - let deserialized = L.ALS.SynthPolygonLayer.deserialize(serialized, layerSystem, settings, seenObjects); - for (let id in deserialized.polygons) - deserialized.polygonGroup.addLayer(deserialized.polygons[id]); - deserialized.updateRectanglesColors(); - return deserialized; - } } }); \ No newline at end of file diff --git a/SynthRectangleLayer/SynthRectangleSettings.js b/SynthRectangleLayer/SynthRectangleSettings.js index 26198012..8812a6f6 100644 --- a/SynthRectangleLayer/SynthRectangleSettings.js +++ b/SynthRectangleLayer/SynthRectangleSettings.js @@ -4,10 +4,10 @@ * @class * @extends L.ALS.Settings */ -L.ALS.SynthRectangleSettings = L.ALS.SynthPolygonSettings.extend( /** @lends L.ALS.SynthRectangleSettings.prototype */ { +L.ALS.SynthRectangleSettings = L.ALS.SynthPolygonBaseSettings.extend( /** @lends L.ALS.SynthRectangleSettings.prototype */ { initialize: function () { - L.ALS.SynthPolygonSettings.prototype.initialize.call(this); + L.ALS.SynthPolygonBaseSettings.prototype.initialize.call(this); this.addColorWidgets("defaultRectangleBorderColor", "defaultRectangleFillColor"); } diff --git a/locales/English.js b/locales/English.js index f112c384..cf5eef7c 100644 --- a/locales/English.js +++ b/locales/English.js @@ -102,6 +102,9 @@ L.ALS.Locales.addLocaleProperties("English", { notGridNotSupported: "Sorry, your browser doesn't support anything other than ASCII Grid. Please, select a valid ASCII Grid file.", DEMError: "Sorry, an error occurred while loading one of your files", //TODO: Add file name + jsonNoPaths1: "No paths has been drawn in layer", + jsonNoPaths2: "only selected geometry and airport position will be exported.", + // SynthGridSettings defaultGridBorderColor: "Default grid border color:", @@ -140,6 +143,10 @@ L.ALS.Locales.addLocaleProperties("English", { geometryDefaultFillColor: "Default fill color:", geometryDefaultBorderColor: "Default border color:", + // SynthPolygonLayer + polygonPathsColor: "Paths color:", + polygonHidePaths: "Hide paths", + // Search searchButtonTitle: "Search Geometry Layers or OSM", searchPlaceholder: "Type to search...", diff --git a/locales/Russian.js b/locales/Russian.js index 88dba001..8b78ba71 100644 --- a/locales/Russian.js +++ b/locales/Russian.js @@ -97,6 +97,9 @@ L.ALS.Locales.addLocaleProperties("Русский", { notGridNotSupported: "Извините, ваш браузер не поддерживает ничего, кроме ASCII Grid. Пожалуйста, выберете файл ASCII Grid.", DEMError: "Извините, во время загрузки одного из ваших файлов произошла ошибка", + jsonNoPaths1: "Маршруты не были добавлены в слое", + jsonNoPaths2: "будет экспортирована только геометрия и положение аэропорта.", + // SynthGridSettings defaultGridBorderColor: "Цвет обводки сетки по умолчанию:", @@ -114,13 +117,13 @@ L.ALS.Locales.addLocaleProperties("Русский", { rectangleFillColor: "Цвет заливки прямоугольников:", rectangleLayersRemoved: "Один или несколько прямоугольников были удалены, так как они слишком большие", - // SynthShapefileWizard + // SynthGeometryWizard geometryDisplayName: "Слой Геометрии", geometryFileLabel: "Сжатый shapefile (zip-архив) или GeoJSON:", geometryNotification: "Чтобы просмотреть семантику объекта, нажмите на него. Позже вы можете выполнить поиск по семантике, нажав кнопку поиска на панели программы.", - // SynthShapefileLayer + // SynthGeometryLayer geometryOutOfBounds: "Объекты в выбранном файле выходят за границы видимой области. Пожалуйста, проверьте проекцию и/или добавьте в архив файл .prj", geometryInvalidFile: "Этот файл не является shapefile-ом или файлом GeoJSON", @@ -130,11 +133,16 @@ L.ALS.Locales.addLocaleProperties("Русский", { geometryBrowserNotSupported: "Ваш браузер не поддерживает добавление данного слоя, но вы можете открывать проекты, использующие этот слой.", geometryNoFileSelected: "Файл не был выбран. Пожалуйста, выберете файл, который хотите добавить, и попробуйте снова.", - // Shapefile settings + // SynthGeometrySettings geometryDefaultFillColor: "Цвет заливки по умолчанию:", geometryDefaultBorderColor: "Цвет обводки по умолчанию:", + // SynthPolygonLayer + + polygonPathsColor: "Цвет маршрутов:", + polygonHidePaths: "Скрыть маршруты", + // Search searchButtonTitle: "Поиск в Слоях Геометрии и OSM", searchPlaceholder: "Начните вводить для поиска...", diff --git a/main.js b/main.js index 2b55cd9d..3d33f688 100644 --- a/main.js +++ b/main.js @@ -14,7 +14,7 @@ window.L = require("leaflet"); * Segments number to use when displaying L.Geodesic * @type {number} */ -L.GEODESIC_SEGMENTS = 1000; +L.GEODESIC_SEGMENTS = 500; L.Geodesic = require("leaflet.geodesic").GeodesicLine; require("leaflet-draw"); @@ -25,11 +25,13 @@ require("./node_modules/leaflet.coordinates/dist/Leaflet.Coordinates-0.1.5.min.j require("./locales/English.js"); require("./locales/Russian.js"); require("./SynthGeometryLayer/SynthGeometryLayer.js"); -require("./SynthBase/SynthBaseLayer.js"); -require("./SynthPolygonLayer/SynthPolygonLayer.js"); +require("./SynthBaseLayer/SynthBaseLayer.js"); +require("./SynthPolygonBaseLayer/SynthPolygonBaseLayer.js"); +require("./SynthRectangleBaseLayer/SynthRectangleBaseLayer.js"); require("./SynthGridLayer/SynthGridLayer.js"); require("./SynthRectangleLayer/SynthRectangleLayer.js"); require("./SynthLineLayer/SynthLineLayer.js"); +require("./SynthPolygonLayer/SynthPolygonLayer.js"); require("./SearchControl.js"); L.ALS.System.initializeSystem(); @@ -127,6 +129,7 @@ for (let country of countries) { layerSystem.addBaseLayer(L.tileLayer(""), "Empty"); // Add layer types +layerSystem.addLayerType(L.ALS.SynthPolygonLayer); layerSystem.addLayerType(L.ALS.SynthGridLayer); layerSystem.addLayerType(L.ALS.SynthRectangleLayer); layerSystem.addLayerType(L.ALS.SynthLineLayer); From 9e0d2924a1a2ca603dd2ee03598f5bccfd476b85 Mon Sep 17 00:00:00 2001 From: matafokka Date: Tue, 3 May 2022 17:54:13 +0300 Subject: [PATCH 05/48] Reimplemented PolygonLayer with all calculations in gnomonic projection. This stuff works and greatly improves performance. I've also discovered that GeodesicLine#changeLength() doesn't work for really short lines. I have to do something about it and also about some other stuff like too large polygons. --- MathTools.js | 2 +- SynthLineLayer/SynthLineLayer.js | 5 + SynthPolygonLayer/SynthPolygonLayer.js | 175 ++++++++++++++----------- 3 files changed, 103 insertions(+), 79 deletions(-) diff --git a/MathTools.js b/MathTools.js index fa8d46b1..3cf95299 100644 --- a/MathTools.js +++ b/MathTools.js @@ -203,7 +203,7 @@ class MathTools { return undefined; // Find two points that will produce greatest length. It will yield the segment inside the whole polygon. - let point1, point2, previousLength = 0; + let point1, point2, previousLength = -1; for (let i = 0; i < intersections.length; i++) { let p1 = intersections[i]; diff --git a/SynthLineLayer/SynthLineLayer.js b/SynthLineLayer/SynthLineLayer.js index 7ed20348..b8ea9a9a 100644 --- a/SynthLineLayer/SynthLineLayer.js +++ b/SynthLineLayer/SynthLineLayer.js @@ -74,6 +74,11 @@ L.ALS.SynthLineLayer = L.ALS.SynthBaseLayer.extend(/** @lends L.ALS.SynthLineLay numberOfImages = Math.ceil(length / this.Bx) + 4, extendBy = (this.Bx * numberOfImages - length) / 2 / length; extendedGeodesic.changeLength("both", extendBy); + + if (MathTools.isEqual(length, extendedGeodesic.statistics.sphericalLengthMeters)) { + // TODO: Do something about really short lines + } + this.pathsGroup.addLayer(extendedGeodesic); // Capture points made by constructing a line with segments number equal to the number of images diff --git a/SynthPolygonLayer/SynthPolygonLayer.js b/SynthPolygonLayer/SynthPolygonLayer.js index 341f82b7..a3d7a464 100644 --- a/SynthPolygonLayer/SynthPolygonLayer.js +++ b/SynthPolygonLayer/SynthPolygonLayer.js @@ -13,6 +13,12 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend({ init: function (wizardResults, settings) { this.copySettingsToThis(settings); + /** + * 60 degrees geodesic line length in meters + * @type {number} + */ + this.maxGeodesicLengthMeters = this.getEarthRadius() * 60; + this.internalConnections = L.featureGroup(); this.externalConnections = L.featureGroup(); this.pathGroup = L.featureGroup(); @@ -75,17 +81,18 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend({ // For building perpendicular lines, we'll use gnomonic projection to which we can transfer some // properties of Euclidean 2D space. this.polygonGroup.eachLayer((layer) => { - let latLngs = layer.getLatLngs()[0], + let center = layer.getBounds().getCenter(), + proj = proj4("+proj=longlat +ellps=sphere +no_defs", `+proj=gnom +lat_0=${center.lat} +lon_0=${center.lng} +x_0=0 +y_0=0 +ellps=sphere +datum=WGS84 +units=m +no_defs`), + latLngs = layer.getLatLngs()[0], {upper, lower} = this.getConvexHull(L.LatLngUtil.cloneLatLngs(latLngs)), - projectedPolygon = [], minLength = Infinity, shortestPath, shortestPathConnections, shortestPathPoints; + projectedPolygon = [], + minLength = Infinity, shortestPath, shortestPathConnections, shortestPathPoints; // Convert polygon coords to layer points, so we can use MathTools - for (let coord of latLngs) { - let {x, y} = this.map.project(coord, 0); - projectedPolygon.push([x, y]); - } + for (let coord of latLngs) + projectedPolygon.push(proj.forward([coord.lng, coord.lat])); + projectedPolygon.push(projectedPolygon[0]); - latLngs.push(latLngs[0]); // For upper part, perpendiculars should be headed downwards, for lower, upwards upper.direction = -1; @@ -94,78 +101,85 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend({ for (let part of [upper, lower]) { for (let i = 0; i < part.length - 1; i++) { // Get a directional line - let line = this.perpendicularLine(part[i], part[i + 1], this.By, "end", part.direction), - perpLinePoints = line.getActualLatLngs()[0], + let origP1 = part[i], origP2 = part[i + 1], + edgeP1 = proj.forward([origP1.lng, origP1.lat]), edgeP2 = proj.forward([origP2.lng, origP2.lat]), + directionalLine = this.perpendicularLine(proj, edgeP1, edgeP2, "end", part.direction), currentPath = [], currentConnections = [], currentLength = 0, currentPoints = [], - shouldSwapPoints = false, lineAfterPolygonAdded = false; + shouldSwapPoints = false, lineAfterPolygonAdded = false, + length = MathTools.distanceBetweenPoints(...directionalLine); - // For each point in line (points lying somewhere on path's geodesic) build a path - for (let j = 0; j < perpLinePoints.length - 1; j++) { + // Move along the line by By + for (let deltaBy = 0; deltaBy < length; deltaBy += this.By) { if (lineAfterPolygonAdded) break; - let p1 = perpLinePoints[j], p2 = perpLinePoints[j + 1], - line = this.perpendicularLine(p1, p2, this.Bx, "both"), - linePoints = line.getActualLatLngs()[0], - projectedLine = []; - - // Project perpendicular line. Also find indices of two closest points to the - // current vertex. - let indexP1, indexP2, lengthP1 = Infinity, lengthP2 = Infinity; - for (let k = 0; k < linePoints.length - 1; k++) { - let coord = linePoints[k], - {x, y} = this.map.project(coord, 0); - projectedLine.push([x, y]); - - if (j !== 0) - continue; - - let length = this.getLineLengthMeters(L.polyline([p1, coord])); - - if (coord.lng < p1.lng && length < lengthP1) { - lengthP1 = length; - indexP1 = k; - } - - if (coord.lng > p1.lng && length < lengthP2) { - lengthP2 = length; - indexP2 = k; - } - } - - let intersection = MathTools.clipLineByPolygon(projectedLine, projectedPolygon); + let p1 = this.scaleLine(directionalLine, deltaBy)[1], + p2 = this.scaleLine(directionalLine, deltaBy + this.By)[1], + line = this.perpendicularLine(proj, p1, p2, "both"), + intersection = MathTools.clipLineByPolygon(line, projectedPolygon); if (!intersection) { - // If it's the first point, and there's no intersection, use closest points as - // an intersection with two bases in mind. This doesn't seem to happen, but let's be - // cautious anyway - if (j === 0) { + // If it's the first point, use an edge as intersection + if (deltaBy === 0) + intersection = [edgeP1, edgeP2]; + else { + // Move line along perpendicular. See details here: https://math.stackexchange.com/questions/2593627/i-have-a-line-i-want-to-move-the-line-a-certain-distance-away-parallelly + let [p1, p2] = currentPath[currentPath.length - 1].getLatLngs(), + [p1x, p1y] = proj.forward([p1.lng, p1.lat]), + [p2x, p2y] = proj.forward([p2.lng, p2.lat]), + dx = p2x - p1x, dy = p2y - p1y, + dr = this.By / Math.sqrt(dx ** 2 + dy ** 2), + xMod = dr * (p1y - p2y), yMod = dr * (p2x - p1x); + + if (Math.sign(yMod) !== part.direction) { + xMod = -xMod; + yMod = -yMod; + } + intersection = [ - projectedLine[indexP1 - 2], - projectedLine[indexP1 + 2] + [p1x + xMod, p1y + yMod], + [p2x + xMod, p2y + yMod], ]; - } else { - let [newP1, newP2] = currentPath[currentPath.length - 1].getLatLngs(), - interP1 = this.map.project([p1.lat, newP1.lng], 0), - interP2 = this.map.project([p1.lat, newP2.lng], 0); - intersection = [[interP1.x, interP1.y], [interP2.x, interP2.y]]; lineAfterPolygonAdded = true; } } - let [pathP1, pathP2] = intersection, - pathPoints = shouldSwapPoints ? [pathP2, pathP1] : [pathP1, pathP2]; + if (shouldSwapPoints) + intersection.reverse(); shouldSwapPoints = !shouldSwapPoints; let path = L.geodesic([ - this.map.unproject(pathPoints[0], 0), - this.map.unproject(pathPoints[1], 0), - ], lineOptions), + proj.inverse(intersection[0]).reverse(), + proj.inverse(intersection[1]).reverse(), + ], { + ...lineOptions, color: lineAfterPolygonAdded ? "red" : "blue", + }), length = path.statistics.sphericalLengthMeters, - numberOfImages = Math.ceil(length / this.Bx) + 4, + numberOfImages = Math.ceil(length / this.Bx), extendBy; + + // Don't extend copied line + if (lineAfterPolygonAdded) { + extendBy = 0; + } else { + numberOfImages += 4; extendBy = (this.Bx * numberOfImages - length) / 2 / length; - path.changeLength("both", extendBy); + } + + // If current length is already greater than previous, break loop and save some time + let tempLength = currentLength + length + length * extendBy; + if (tempLength >= minLength) { + currentLength = tempLength; + break; + } + + if (!lineAfterPolygonAdded) { + path.changeLength("both", extendBy); + + if (MathTools.isEqual(length, path.statistics.sphericalLengthMeters)) { + // TODO: Do something about really short lines + } + } currentPath.push(path); currentLength += path.statistics.sphericalLengthMeters; @@ -222,36 +236,41 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend({ this.writeToHistory(); }, - perpendicularLine: function (p1, p2, basis, extendFrom = "end", direction = 1) { + perpendicularLine: function (proj, p1, p2, extendFrom = "end", direction = 1) { // Project coordinates to the gnomonic projection and work with lines as with vectors. - let proj = this.getProjFromWgs(p1.lng, p1.lat), - [p2x, p2y] = proj.forward([p2.lng, p2.lat]), + let [p1x, p1y] = p1, [p2x, p2y] = p2, + x = p2x - p1x, y = p2y - p1y, // Find an orthogonal vector perpX = 1000, - perpY = -perpX * p2x / p2y, - [perpLng, perpLat] = proj.inverse([perpX, perpY]); + perpY = -perpX * x / y; // Check if orthogonal vector in correct direction. If not, reverse it. - if (Math.sign(perpLat - p1.lat) !== direction) { + if (Math.sign(perpY) !== direction) { perpX = -perpX; perpY = -perpY; - [perpLng, perpLat] = proj.inverse([perpX, perpY]); } - // Build 175 degrees long geodesic - // TODO: Make it be less than 180 after extending - let targetLength = 175 / 180 * this.getEarthRadius(false), - segmentsNumber = Math.ceil(targetLength / basis), - geodesic = new L.Geodesic([p1, [perpLat, perpLng]], {segmentsNumber}), - length = geodesic.statistics.sphericalLengthMeters, - extendBy = (segmentsNumber * basis - length) / length; + // Move vector back + perpX += p1x; + perpY += p1y; + + // Scale line + + let line = this.scaleLine([[p1x, p1y], [perpX, perpY]], this.maxGeodesicLengthMeters); + + if (extendFrom !== "both") + return line; - geodesic.changeLength(extendFrom, extendBy); - return geodesic; + return [this.perpendicularLine(proj, p1, p2, "end", -direction)[1], line[1]]; }, - getProjFromWgs: function (lng0, lat0) { - return proj4("+proj=longlat +ellps=sphere +no_defs", `+proj=gnom +lat_0=${lat0} +lon_0=${lng0} +x_0=0 +y_0=0 +ellps=sphere +datum=WGS84 +units=m +no_defs`); + scaleLine: function (line, targetLength) { + let [p1, p2] = line, [p1x, p1y] = p1, [p2x, p2y] = p2, + dx = p2x - p1x, dy = p2y - p1y, + lengthModifier = targetLength / Math.sqrt(dx ** 2 + dy ** 2); + dx *= lengthModifier; + dy *= lengthModifier; + return [[p1x, p1y], [dx + p1x, dy + p1y]]; }, statics: { From 094f66194eede7a73c413e7f609d624c82bb2057 Mon Sep 17 00:00:00 2001 From: matafokka Date: Wed, 4 May 2022 14:44:30 +0300 Subject: [PATCH 06/48] Small improvements God, I hate naming commits. Anyway, changes: - Big rectangles in Rectangle Layer are no longer removed. Instead, they just have the red color and error message inside of them. The same logic will be applied to the upcoming Polygon Layer. - Now Polygon Layer calculates exact paths count for limiting the size. - Moved paths count estimation logic to the Grid Layer. It's the only layer that needs estimation. Also made the whole thing look better. - Did minor refactoring. --- SynthGridLayer/SynthGridLayer.js | 44 ++++++++++++++++++- SynthGridLayer/onMapPan.js | 7 +-- .../SynthPolygonBaseLayer.js | 11 +++++ SynthPolygonBaseLayer/polygons.js | 8 ++++ SynthPolygonLayer/SynthPolygonLayer.js | 3 +- .../SynthRectangleBaseLayer.js | 2 +- SynthRectangleBaseLayer/drawPaths.js | 39 +++------------- SynthRectangleBaseLayer/misc.js | 13 +----- SynthRectangleLayer/SynthRectangleLayer.js | 16 +++---- locales/English.js | 2 +- locales/Russian.js | 2 +- 11 files changed, 84 insertions(+), 63 deletions(-) diff --git a/SynthGridLayer/SynthGridLayer.js b/SynthGridLayer/SynthGridLayer.js index 141c72d8..194d362e 100644 --- a/SynthGridLayer/SynthGridLayer.js +++ b/SynthGridLayer/SynthGridLayer.js @@ -1,5 +1,6 @@ require("./SynthGridSettings.js"); require("./SynthGridWizard.js"); +const turfHelpers = require("@turf/helpers"); /** * Layer that allows users to plan aerial photography using grid @@ -20,7 +21,7 @@ L.ALS.SynthGridLayer = L.ALS.SynthRectangleBaseLayer.extend(/** @lends L.ALS.Syn * @type {string[]} * @private */ - this._namesIDs = []; + this.gridLabelsIDs = []; L.ALS.SynthRectangleBaseLayer.prototype.init.call(this, wizardResults, settings); @@ -56,6 +57,47 @@ L.ALS.SynthGridLayer = L.ALS.SynthRectangleBaseLayer.extend(/** @lends L.ALS.Syn L.ALS.SynthRectangleBaseLayer.prototype.calculateParameters.call(this); }, + /** + * Estimates paths count based on given cell size in degrees + * @param cellSizeDeg + * @returns {number} + */ + estimatePathsCount: function (cellSizeDeg) { + return Math.ceil( + turfHelpers.radiansToLength(turfHelpers.degreesToRadians(cellSizeDeg), "meters") / + this.By + ); + }, + + drawPaths: function () { + this.clearPaths(); + + // Calculate estimated paths count for a polygon. Values are somewhat true for equatorial regions. + // We'll check if it's too small (in near-polar regions, there'll be only one path when value is 2) or too big. + + let errorLabel = this.getWidgetById("calculateParametersError"), + parallelsPathsCount = this.estimatePathsCount(this.lngPathsCount), + meridiansPathsCount = this.estimatePathsCount(this.latPathsCount); + + if (parallelsPathsCount === undefined) { + errorLabel.setValue("errorDistanceHasNotBeenCalculated"); + return; + } + + if (parallelsPathsCount >= 20 || meridiansPathsCount >= 20) { + errorLabel.setValue("errorPathsCountTooBig"); + return; + } + + if (parallelsPathsCount <= 2 || meridiansPathsCount <= 2) { + errorLabel.setValue("errorPathsCountTooSmall"); + return; + } + errorLabel.setValue(""); + + L.ALS.SynthRectangleBaseLayer.prototype.drawPaths.call(this); + }, + statics: { wizard: L.ALS.SynthGridWizard, settings: new L.ALS.SynthGridSettings(), diff --git a/SynthGridLayer/onMapPan.js b/SynthGridLayer/onMapPan.js index 7e1c5d72..d610b371 100644 --- a/SynthGridLayer/onMapPan.js +++ b/SynthGridLayer/onMapPan.js @@ -11,10 +11,7 @@ L.ALS.SynthGridLayer.prototype._onMapPan = function () { return; this.polygonGroup.clearLayers(); - - for (let id of this._namesIDs) - this.labelsGroup.deleteLabel(id); - this._namesIDs = []; + this.clearLabels("gridLabelsIDs"); // Get viewport bounds and calculate correct start and end coords for lng and lat let bounds = this.map.getBounds(), north = bounds.getNorth(), west = bounds.getWest(), @@ -27,7 +24,7 @@ L.ALS.SynthGridLayer.prototype._onMapPan = function () { let createLabel = (latLng, content, origin = "center", colorful = false) => { let id = L.ALS.Helpers.generateID(); - this._namesIDs.push(id); + this.gridLabelsIDs.push(id); this.labelsGroup.addLabel(id, latLng, content, {origin: origin}); if (colorful) this.labelsGroup.setLabelDisplayOptions(id, L.LabelLayer.DefaultDisplayOptions.Success); diff --git a/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js b/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js index a59d0595..d0892774 100644 --- a/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js +++ b/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js @@ -50,6 +50,7 @@ L.ALS.SynthPolygonBaseLayer = L.ALS.SynthBaseLayer.extend( /** @lends L.ALS.Synt this.bordersGroup = L.featureGroup(); this.bordersGroup.thicknessMultiplier = 4; this.labelsGroup = new L.LabelLayer(false); + this.polygonErrorsLabelsIDs = []; L.ALS.SynthBaseLayer.prototype.init.call(this, settings, path1InternalConnections, path1ExternalConnections, colorLabel1, [path1ActualPathGroup], @@ -216,6 +217,16 @@ L.ALS.SynthPolygonBaseLayer = L.ALS.SynthBaseLayer.extend( /** @lends L.ALS.Synt } }, + /** + * Clears labels which IDs are contained in `arrayProperty`. + * @param arrayProperty {string} A property of `this` that contains labels' IDs to clear + */ + clearLabels: function (arrayProperty) { + for (let id of this[arrayProperty]) + this.labelsGroup.deleteLabel(id); + this[arrayProperty] = []; + }, + /** * Copies polygons and airport to the array for merging it to collection later * @returns {Object[]} Array of features to merge diff --git a/SynthPolygonBaseLayer/polygons.js b/SynthPolygonBaseLayer/polygons.js index b20bc54c..e8383833 100644 --- a/SynthPolygonBaseLayer/polygons.js +++ b/SynthPolygonBaseLayer/polygons.js @@ -108,4 +108,12 @@ L.ALS.SynthPolygonBaseLayer.prototype.calculatePolygonParameters = function (wid widgetContainer.getWidgetById(name).setValue(value); } } +} + +L.ALS.SynthPolygonBaseLayer.prototype.denyPolygon = function (polygon, reason) { + polygon.setStyle({color: "red", fillColor: "red"}); + let {lat, lng} = polygon.getBounds().getCenter(), + id = L.ALS.Helpers.generateID(); + this.labelsGroup.addLabel(id, [lat, lng], reason, L.ALS.LeafletLayers.LabelLayer.DefaultDisplayOptions.Error); + this.polygonErrorsLabelsIDs.push(id); } \ No newline at end of file diff --git a/SynthPolygonLayer/SynthPolygonLayer.js b/SynthPolygonLayer/SynthPolygonLayer.js index a3d7a464..162a0e38 100644 --- a/SynthPolygonLayer/SynthPolygonLayer.js +++ b/SynthPolygonLayer/SynthPolygonLayer.js @@ -228,7 +228,7 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend({ return; if (layersWereRemoved) - window.alert(L.ALS.locale.rectangleLayersRemoved); + window.alert(L.ALS.locale.rectangleLayersSkipped); this.map.addLayer(this.labelsGroup); // Nothing in the base layer hides or shows it, so it's only hidden in code above this.updateLayersVisibility(); @@ -255,7 +255,6 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend({ perpY += p1y; // Scale line - let line = this.scaleLine([[p1x, p1y], [perpX, perpY]], this.maxGeodesicLengthMeters); if (extendFrom !== "both") diff --git a/SynthRectangleBaseLayer/SynthRectangleBaseLayer.js b/SynthRectangleBaseLayer/SynthRectangleBaseLayer.js index 8048066e..05b89994 100644 --- a/SynthRectangleBaseLayer/SynthRectangleBaseLayer.js +++ b/SynthRectangleBaseLayer/SynthRectangleBaseLayer.js @@ -61,7 +61,7 @@ L.ALS.SynthRectangleBaseLayer = L.ALS.SynthPolygonBaseLayer.extend( /** @lends L * @type {string[]} * @private */ - this._pathsLabelsIDs = []; + this.pathsLabelsIds = []; this.lngDistance = parseFloat(wizardResults["gridLngDistance"]); this.latDistance = parseFloat(wizardResults["gridLatDistance"]); diff --git a/SynthRectangleBaseLayer/drawPaths.js b/SynthRectangleBaseLayer/drawPaths.js index 8eacfc1d..990910c3 100644 --- a/SynthRectangleBaseLayer/drawPaths.js +++ b/SynthRectangleBaseLayer/drawPaths.js @@ -5,51 +5,26 @@ const turfHelpers = require("@turf/helpers"); L.ALS.SynthRectangleBaseLayer.prototype.clearPaths = function () { L.ALS.SynthPolygonBaseLayer.prototype.clearPaths.call(this); - - for (let id of this._pathsLabelsIDs) - this.labelsGroup.deleteLabel(id); - this._pathsLabelsIDs = []; + this.clearLabels("pathsLabelsIds"); } -L.ALS.SynthRectangleBaseLayer.prototype._drawPaths = function () { +L.ALS.SynthRectangleBaseLayer.prototype.drawPaths = function () { this.clearPaths(); - // Validate estimated paths count - - let errorLabel = this.getWidgetById("calculateParametersError"), - parallelsPathsCount = this["lngFakePathsCount"], - meridiansPathsCount = this["latFakePathsCount"]; - - if (parallelsPathsCount === undefined) { - errorLabel.setValue("errorDistanceHasNotBeenCalculated"); - return; - } - - if (parallelsPathsCount >= 20 || meridiansPathsCount >= 20) { - errorLabel.setValue("errorPathsCountTooBig"); - return; - } - - if (parallelsPathsCount <= 2 || meridiansPathsCount <= 2) { - errorLabel.setValue("errorPathsCountTooSmall"); - return; - } - errorLabel.setValue(""); - if (this.mergedPolygons.length === 0) return; - this._drawPathsWorker(true); - this._drawPathsWorker(false); + this.drawPathsWorker(true); + this.drawPathsWorker(false); this.updatePathsMeta(); this.labelsGroup.redraw(); } /** - * Draws flight paths. Use _drawPaths wrapper to draw paths instead of this. + * Draws flight paths. Use drawPaths wrapper to draw paths instead of this. * @private */ -L.ALS.SynthRectangleBaseLayer.prototype._drawPathsWorker = function (isParallels) { +L.ALS.SynthRectangleBaseLayer.prototype.drawPathsWorker = function (isParallels) { let pathGroup, nameForOutput, color, connectionsGroup, widgetId, extensionIndex; if (isParallels) { @@ -171,7 +146,7 @@ L.ALS.SynthRectangleBaseLayer.prototype._drawPathsWorker = function (isParallels continue; let labelId = L.ALS.Helpers.generateID(); - this._pathsLabelsIDs.push(labelId); + this.pathsLabelsIds.push(labelId); this.labelsGroup.addLabel(labelId, coord, number, L.LabelLayer.DefaultDisplayOptions[isParallels ? "Message" : "Error"]); number++; } diff --git a/SynthRectangleBaseLayer/misc.js b/SynthRectangleBaseLayer/misc.js index e78991df..e415670c 100644 --- a/SynthRectangleBaseLayer/misc.js +++ b/SynthRectangleBaseLayer/misc.js @@ -4,21 +4,12 @@ const turfHelpers = require("@turf/helpers"); L.ALS.SynthRectangleBaseLayer.prototype.calculateParameters = function () { L.ALS.SynthBaseLayer.prototype.calculateParameters.call(this); - - // Calculate estimated paths count for a polygon. Values are somewhat true for equatorial regions. - // We'll check if it's too small (in near-polar regions, there'll be only one path when value is 2) or too big. - let latLngs = ["lat", "lng"]; - for (let name of latLngs) { - let cellSize = Math.round(turfHelpers.radiansToLength(turfHelpers.degreesToRadians(this[name + "Distance"]), "meters")); - this[name + "FakePathsCount"] = Math.ceil(cellSize / this.By); - } - this.calculatePolygonParameters(); } L.ALS.SynthRectangleBaseLayer.prototype.updateLayersVisibility = function () { L.ALS.SynthPolygonBaseLayer.prototype.updateLayersVisibility.call(this); - this._drawPaths(); // We have to redraw paths both for hiding one of the paths and hiding numbers + this.drawPaths(); // We have to redraw paths both for hiding one of the paths and hiding numbers } L.ALS.SynthRectangleBaseLayer.prototype._closestGreater = function (current, divider) { @@ -54,7 +45,7 @@ L.ALS.SynthRectangleBaseLayer.prototype.calculatePolygonParameters = function (w } )); } - this._drawPaths(); + this.drawPaths(); this.writeToHistoryDebounced(); } diff --git a/SynthRectangleLayer/SynthRectangleLayer.js b/SynthRectangleLayer/SynthRectangleLayer.js index 6e0d6414..ed924776 100644 --- a/SynthRectangleLayer/SynthRectangleLayer.js +++ b/SynthRectangleLayer/SynthRectangleLayer.js @@ -25,21 +25,19 @@ L.ALS.SynthRectangleLayer = L.ALS.SynthRectangleBaseLayer.extend({ this.removePolygon(this.polygons[name], false); this.polygons = {} + this.clearLabels("polygonErrorsLabelsIDs"); + let layersWereRemoved = false; this.polygonGroup.eachLayer((layer) => { let bounds = layer.getBounds(), topLeft = bounds.getNorthWest(), - arrTopLeft = [topLeft.lng, topLeft.lat], - lngDiff = Math.abs(bounds.getWest() - bounds.getEast()), - latDiff = Math.abs(bounds.getNorth() - bounds.getSouth()), - lngLength = this.getArcAngleByLength(arrTopLeft, this.By, false), - latLength = this.getArcAngleByLength(arrTopLeft, this.By, true), - lngPathsCount = Math.round(lngDiff / lngLength), latPathsCount = Math.round(latDiff / latLength); + parallelsPathsCount = Math.ceil(this.getParallelOrMeridianLineLength(topLeft, bounds.getSouthWest()) / this.By) + 1, + meridiansPathsCount = Math.ceil(this.getParallelOrMeridianLineLength(topLeft, bounds.getNorthEast()) / this.By) + 1; // Limit polygon size by limiting total approximate paths count. This is not 100% accurate but close enough. - if (lngPathsCount + latPathsCount > 150) { + if (meridiansPathsCount + parallelsPathsCount > 150) { layersWereRemoved = true; - this.polygonGroup.removeLayer(layer); + this.denyPolygon(layer, "polygonTooBig"); return; } @@ -47,7 +45,7 @@ L.ALS.SynthRectangleLayer = L.ALS.SynthRectangleBaseLayer.extend({ }); if (layersWereRemoved) - window.alert(L.ALS.locale.rectangleLayersRemoved); + window.alert(L.ALS.locale.rectangleLayersSkipped); this.map.addLayer(this.labelsGroup); // Nothing in the base layer hides or shows it, so it's only hidden in code above this.updateLayersVisibility(); diff --git a/locales/English.js b/locales/English.js index cf5eef7c..0121f30e 100644 --- a/locales/English.js +++ b/locales/English.js @@ -120,7 +120,7 @@ L.ALS.Locales.addLocaleProperties("English", { defaultRectangleFillColor: "Default rectangle fill color:", rectangleBorderColor: "Rectangle border color:", rectangleFillColor: "Rectangle fill color:", - rectangleLayersRemoved: "One or more rectangles has been removed because they're too big", + rectangleLayersSkipped: "One or more rectangles has been skipped because they're too big. These rectangles have red color.", // SynthGeometryWizard diff --git a/locales/Russian.js b/locales/Russian.js index 8b78ba71..62b285d8 100644 --- a/locales/Russian.js +++ b/locales/Russian.js @@ -115,7 +115,7 @@ L.ALS.Locales.addLocaleProperties("Русский", { defaultRectangleFillColor: "Цвет заливки прямоугольников по умолчанию:", rectangleBorderColor: "Цвет обводки прямоугольников:", rectangleFillColor: "Цвет заливки прямоугольников:", - rectangleLayersRemoved: "Один или несколько прямоугольников были удалены, так как они слишком большие", + rectangleLayersSkipped: "Один или несколько прямоугольников были пропущены, так как они слишком большие. Данные прямоугольники имеют красный цвет.", // SynthGeometryWizard From dd42e5a74e4b581ea394a6ce9cfce22ea7d820f7 Mon Sep 17 00:00:00 2001 From: matafokka Date: Sun, 8 May 2022 14:58:30 +0300 Subject: [PATCH 07/48] Polygon Layer rework, fixes for big polygons and lines - Reworked the whole Polygon Layer, the algorithms seems to work fine. - Dealt with short geodesics by fixing my fork of Leaflet.Geodesic. - Dealt with long geodesics and big polygons. - Added missing locales for the Polygon Layer. --- SynthLineLayer/SynthLineLayer.js | 32 +- .../SynthPolygonBaseLayer.js | 1 - SynthPolygonBaseLayer/polygons.js | 6 +- SynthPolygonLayer/SynthPolygonLayer.js | 353 +++++++++++------- SynthRectangleLayer/SynthRectangleLayer.js | 12 +- locales/English.js | 4 +- locales/Russian.js | 4 +- main.js | 2 +- package-lock.json | 4 +- 9 files changed, 248 insertions(+), 170 deletions(-) diff --git a/SynthLineLayer/SynthLineLayer.js b/SynthLineLayer/SynthLineLayer.js index b8ea9a9a..54157bfe 100644 --- a/SynthLineLayer/SynthLineLayer.js +++ b/SynthLineLayer/SynthLineLayer.js @@ -1,6 +1,7 @@ require("./SynthLineWizard.js"); require("./SynthLineSettings.js"); const geojsonMerge = require("@mapbox/geojson-merge"); // Using this since turfHelpers.featureCollection() discards previously defined properties. +const MathTools = require("../MathTools.js"); /** * Geodesic line layer @@ -18,8 +19,10 @@ L.ALS.SynthLineLayer = L.ALS.SynthBaseLayer.extend(/** @lends L.ALS.SynthLineLay this.pathsGroup = L.featureGroup(); this.drawingGroup = L.featureGroup(); this.connectionsGroup = L.featureGroup(); + this.errorGroup = L.featureGroup(); L.ALS.SynthBaseLayer.prototype.init.call(this, settings, this.pathsGroup, this.connectionsGroup, "lineLayerColor"); + this.addLayers(this.errorGroup); this.enableDraw({ polyline: { @@ -61,10 +64,13 @@ L.ALS.SynthLineLayer = L.ALS.SynthBaseLayer.extend(/** @lends L.ALS.SynthLineLay onEditEnd: function () { this.pathsGroup.clearLayers(); this.pointsGroup.clearLayers(); + this.errorGroup.clearLayers(); - let color = this.getWidgetById("color0").getValue(), lineOptions = { - color, thickness: this.lineThicknessValue, segmentsNumber: L.GEODESIC_SEGMENTS, - }; + let color = this.getWidgetById("color0").getValue(), + lineOptions = { + color, thickness: this.lineThicknessValue, segmentsNumber: L.GEODESIC_SEGMENTS, + }, + linesWereInvalidated = false; this.drawingGroup.eachLayer((layer) => { let latLngs = layer.getLatLngs(); @@ -72,11 +78,20 @@ L.ALS.SynthLineLayer = L.ALS.SynthBaseLayer.extend(/** @lends L.ALS.SynthLineLay let extendedGeodesic = new L.Geodesic([latLngs[i - 1], latLngs[i]], lineOptions), length = extendedGeodesic.statistics.sphericalLengthMeters, numberOfImages = Math.ceil(length / this.Bx) + 4, - extendBy = (this.Bx * numberOfImages - length) / 2 / length; - extendedGeodesic.changeLength("both", extendBy); + shouldInvalidateLine = numberOfImages > 10000; // Line is way too long for calculated Bx - if (MathTools.isEqual(length, extendedGeodesic.statistics.sphericalLengthMeters)) { - // TODO: Do something about really short lines + // This will throw an error when new length exceeds 180 degrees + try { + extendedGeodesic.changeLength("both", (this.Bx * numberOfImages - length) / 2 / length); + } catch (e) { + shouldInvalidateLine = true; + } + + if (shouldInvalidateLine) { + extendedGeodesic.setStyle({color: "red"}); + this.errorGroup.addLayer(extendedGeodesic); + linesWereInvalidated = true; + continue; } this.pathsGroup.addLayer(extendedGeodesic); @@ -102,6 +117,9 @@ L.ALS.SynthLineLayer = L.ALS.SynthBaseLayer.extend(/** @lends L.ALS.SynthLineLay this.map.removeLayer(this.drawingGroup); this.map.addLayer(this.pathsGroup); + if (linesWereInvalidated) + window.alert(L.ALS.locale.lineLayersSkipped); + this.writeToHistory(); }, diff --git a/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js b/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js index d0892774..d8b1bcc0 100644 --- a/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js +++ b/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js @@ -50,7 +50,6 @@ L.ALS.SynthPolygonBaseLayer = L.ALS.SynthBaseLayer.extend( /** @lends L.ALS.Synt this.bordersGroup = L.featureGroup(); this.bordersGroup.thicknessMultiplier = 4; this.labelsGroup = new L.LabelLayer(false); - this.polygonErrorsLabelsIDs = []; L.ALS.SynthBaseLayer.prototype.init.call(this, settings, path1InternalConnections, path1ExternalConnections, colorLabel1, [path1ActualPathGroup], diff --git a/SynthPolygonBaseLayer/polygons.js b/SynthPolygonBaseLayer/polygons.js index e8383833..aff4c214 100644 --- a/SynthPolygonBaseLayer/polygons.js +++ b/SynthPolygonBaseLayer/polygons.js @@ -110,10 +110,6 @@ L.ALS.SynthPolygonBaseLayer.prototype.calculatePolygonParameters = function (wid } } -L.ALS.SynthPolygonBaseLayer.prototype.denyPolygon = function (polygon, reason) { +L.ALS.SynthPolygonBaseLayer.prototype.invalidatePolygon = function (polygon) { polygon.setStyle({color: "red", fillColor: "red"}); - let {lat, lng} = polygon.getBounds().getCenter(), - id = L.ALS.Helpers.generateID(); - this.labelsGroup.addLabel(id, [lat, lng], reason, L.ALS.LeafletLayers.LabelLayer.DefaultDisplayOptions.Error); - this.polygonErrorsLabelsIDs.push(id); } \ No newline at end of file diff --git a/SynthPolygonLayer/SynthPolygonLayer.js b/SynthPolygonLayer/SynthPolygonLayer.js index 162a0e38..597bcab2 100644 --- a/SynthPolygonLayer/SynthPolygonLayer.js +++ b/SynthPolygonLayer/SynthPolygonLayer.js @@ -14,10 +14,10 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend({ this.copySettingsToThis(settings); /** - * 60 degrees geodesic line length in meters + * 89 degrees geodesic line length in meters. Gnomonic projection can't display points starting from 90 deg from the center. * @type {number} */ - this.maxGeodesicLengthMeters = this.getEarthRadius() * 60; + this.maxGnomonicPointDistance = this.getEarthRadius() * 89 * Math.PI / 180; this.internalConnections = L.featureGroup(); this.externalConnections = L.featureGroup(); @@ -39,6 +39,13 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend({ color: "#ff0000", weight: this.lineThicknessValue } + }, + + rectangle: { + shapeOptions: { + color: "#ff0000", + weight: this.lineThicknessValue + } } }, this.polygonGroup); @@ -59,7 +66,7 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend({ this.clearPaths(); - let layersWereRemoved = false; + let layersWereInvalidated = false; // Build paths for each polygon. @@ -69,145 +76,167 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend({ // To build parallel paths, first, we build a line that is perpendicular to the edge (let's call it directional). // Then, for each intermediate point (distances between points are equal to By) of the directional line, // we build a line that is perpendicular to the directional line - a path. - // Then we crop the path by polygon by projecting both path and polygon to the WebMercator. - // Sometimes we can't use an edge (i.e. when polygon is star-shaped). - // To fix that, we'll build paths using convex hull which'll allow us to get rid of two problems: + // Sometimes we can't use an edge (i.e. when polygon is star-shaped) because directional line won't cover the + // whole polygon. To fix that, we'll build paths using convex hull which'll allow us to get rid of two problems: // 1. Star-shaped polygons. - // 2. Determining where directional line should be headed. For upper part of the convex hull the direction - // is downwards. For lower, upwards. + // 2. Crop the line by polygon to determine correct perpendicular direction. To do so, we'll draw a directional + // line from the center of the edge. I haven't found any other way of determining direction when drawing from + // the first point of the edge, there's just no common properties of polygon's configurations I've tested. + + // To work with geodesics as vectors and lines, we'll use gnomonic projection. + // We'll also crop the paths by the hull, so there won't be empty space along the paths. - // For building perpendicular lines, we'll use gnomonic projection to which we can transfer some - // properties of Euclidean 2D space. this.polygonGroup.eachLayer((layer) => { + // Build projection. The center will be the center of the polygon. It doesn't matter that much, but center + // Allows us to expand polygon size a bit when it's close to the projection's coordinates limits. let center = layer.getBounds().getCenter(), - proj = proj4("+proj=longlat +ellps=sphere +no_defs", `+proj=gnom +lat_0=${center.lat} +lon_0=${center.lng} +x_0=0 +y_0=0 +ellps=sphere +datum=WGS84 +units=m +no_defs`), - latLngs = layer.getLatLngs()[0], - {upper, lower} = this.getConvexHull(L.LatLngUtil.cloneLatLngs(latLngs)), + proj = proj4("+proj=longlat +ellps=sphere +no_defs", `+proj=gnom +lat_0=${center.lat} +lon_0=${center.lng} +x_0=0 +y_0=0 +ellps=sphere +R=${this.getEarthRadius()} +units=m +no_defs`), + + // Get convex hull and initialize variables for the shortest path + {upper, lower} = this.getConvexHull(L.LatLngUtil.cloneLatLngs(layer.getLatLngs()[0])), projectedPolygon = [], minLength = Infinity, shortestPath, shortestPathConnections, shortestPathPoints; - // Convert polygon coords to layer points, so we can use MathTools - for (let coord of latLngs) - projectedPolygon.push(proj.forward([coord.lng, coord.lat])); - - projectedPolygon.push(projectedPolygon[0]); - - // For upper part, perpendiculars should be headed downwards, for lower, upwards - upper.direction = -1; - lower.direction = 1; - - for (let part of [upper, lower]) { - for (let i = 0; i < part.length - 1; i++) { - // Get a directional line - let origP1 = part[i], origP2 = part[i + 1], - edgeP1 = proj.forward([origP1.lng, origP1.lat]), edgeP2 = proj.forward([origP2.lng, origP2.lat]), - directionalLine = this.perpendicularLine(proj, edgeP1, edgeP2, "end", part.direction), - currentPath = [], currentConnections = [], currentLength = 0, currentPoints = [], - shouldSwapPoints = false, lineAfterPolygonAdded = false, - length = MathTools.distanceBetweenPoints(...directionalLine); - - // Move along the line by By - for (let deltaBy = 0; deltaBy < length; deltaBy += this.By) { - if (lineAfterPolygonAdded) - break; - - let p1 = this.scaleLine(directionalLine, deltaBy)[1], - p2 = this.scaleLine(directionalLine, deltaBy + this.By)[1], - line = this.perpendicularLine(proj, p1, p2, "both"), - intersection = MathTools.clipLineByPolygon(line, projectedPolygon); - - if (!intersection) { - // If it's the first point, use an edge as intersection - if (deltaBy === 0) - intersection = [edgeP1, edgeP2]; - else { - // Move line along perpendicular. See details here: https://math.stackexchange.com/questions/2593627/i-have-a-line-i-want-to-move-the-line-a-certain-distance-away-parallelly - let [p1, p2] = currentPath[currentPath.length - 1].getLatLngs(), - [p1x, p1y] = proj.forward([p1.lng, p1.lat]), - [p2x, p2y] = proj.forward([p2.lng, p2.lat]), - dx = p2x - p1x, dy = p2y - p1y, - dr = this.By / Math.sqrt(dx ** 2 + dy ** 2), - xMod = dr * (p1y - p2y), yMod = dr * (p2x - p1x); - - if (Math.sign(yMod) !== part.direction) { - xMod = -xMod; - yMod = -yMod; - } - - intersection = [ - [p1x + xMod, p1y + yMod], - [p2x + xMod, p2y + yMod], - ]; - lineAfterPolygonAdded = true; - } - } + // Remove same points from the hull + upper.pop(); + lower.pop(); + + // Project the hull + for (let part of [lower, upper]) { + for (let coord of part) { + let point = proj.forward([coord.lng, coord.lat]); + if (!this.isPointValid(point)) { + layersWereInvalidated = true; + this.invalidatePolygon(layer); + return; + } + projectedPolygon.push(point); + } + } - if (shouldSwapPoints) - intersection.reverse(); - - shouldSwapPoints = !shouldSwapPoints; - - let path = L.geodesic([ - proj.inverse(intersection[0]).reverse(), - proj.inverse(intersection[1]).reverse(), - ], { - ...lineOptions, color: lineAfterPolygonAdded ? "red" : "blue", - }), - length = path.statistics.sphericalLengthMeters, - numberOfImages = Math.ceil(length / this.Bx), extendBy; - - // Don't extend copied line - if (lineAfterPolygonAdded) { - extendBy = 0; - } else { - numberOfImages += 4; - extendBy = (this.Bx * numberOfImages - length) / 2 / length; - } + projectedPolygon.push(projectedPolygon[0]); // Close the polygon + + // For each edge + for (let i = 0; i < projectedPolygon.length - 1; i++) { + // Build a directional line + let edgeP1 = projectedPolygon[i], edgeP2 = projectedPolygon[i + 1], + directionalLine = this.perpendicularLine(edgeP1, edgeP2, projectedPolygon, true), + // Initialize variables for the current path + currentPath = [], currentConnections = [], currentLength = 0, currentPoints = [], + shouldSwapPoints = false, lineAfterPolygonAdded = false; + + // Move along the line by By until we reach the end of the polygon and add an additional line + // or exceed the limit of 300 paths + let deltaBy = 0; + while (true) { + if (lineAfterPolygonAdded) + break; + + if (deltaBy / this.By > 300) { + layersWereInvalidated = true; + this.invalidatePolygon(layer); + return; + } - // If current length is already greater than previous, break loop and save some time - let tempLength = currentLength + length + length * extendBy; - if (tempLength >= minLength) { - currentLength = tempLength; - break; + // Scale the directional line, so endpoints of scaled line will be the start and end points of the + // line that we'll build a perpendicular (a path) to. The distance between these points is By. + let p1 = this.scaleLine(directionalLine, deltaBy)[1], + p2 = this.scaleLine(directionalLine, deltaBy + this.By)[1], + line = this.perpendicularLine(p1, p2, projectedPolygon); + + if (!line) { + // If it's the first point, use an edge as an intersection. This shouldn't happen but I like to + // be extra careful. + if (deltaBy === 0) + line = [edgeP1, edgeP2]; + else { + // Otherwise, we've passed the polygon and we have to add another line, i.e. move it along + // directional line. To do so, get x and y differences between current path and previous + // path. Then move cloned path by these differences. + let dx = p2[0] - p1[0], + dy = p2[1] - p1[1], + [prevPathP1, prevPathP2] = currentPath[currentPath.length - 1].getLatLngs(), + [p1x, p1y] = proj.forward([prevPathP1.lng, prevPathP1.lat]), + [p2x, p2y] = proj.forward([prevPathP2.lng, prevPathP2.lat]); + p1x += dx; + p2x += dx; + p1y += dy; + p2y += dy; + + line = [[p1x, p1y], [p2x, p2y]]; + lineAfterPolygonAdded = true; } + } - if (!lineAfterPolygonAdded) { - path.changeLength("both", extendBy); + // Swap points of each odd path + if (shouldSwapPoints) + line.reverse(); + + shouldSwapPoints = !shouldSwapPoints; + + // Build a path + let path = L.geodesic([ + proj.inverse(line[0]).reverse(), + proj.inverse(line[1]).reverse(), + ], lineOptions), + length = path.statistics.sphericalLengthMeters, + numberOfImages = Math.ceil(length / this.Bx), extendBy; + + if (lineAfterPolygonAdded) { + extendBy = 0; // Don't extend copied line + } else { + numberOfImages += 4; + extendBy = (this.Bx * numberOfImages - length) / 2 / length; + } - if (MathTools.isEqual(length, path.statistics.sphericalLengthMeters)) { - // TODO: Do something about really short lines - } - } + if (numberOfImages > 100) { + layersWereInvalidated = true; + this.invalidatePolygon(layer); + return; + } - currentPath.push(path); - currentLength += path.statistics.sphericalLengthMeters; - currentConnections.push(...path.getLatLngs()); + // If current length is already greater than previous, break loop and save some time + if (currentLength + length + length * extendBy >= minLength) { + currentLength = Infinity; + break; + } - let capturePoints = L.geodesic(path.getLatLngs(), {segmentsNumber: numberOfImages}).getActualLatLngs()[0]; - for (let point of capturePoints) - currentPoints.push(this.createCapturePoint(point, color)); + // Try change length to fit new basis. GeodesicLine will throw when new length exceeds 180 degrees. + // In this case, invalidate current polygon. + if (!lineAfterPolygonAdded) { + try { + path.changeLength("both", extendBy); + } catch (e) { + layersWereInvalidated = true; + this.invalidatePolygon(layer); + return; + } } - if (currentLength >= minLength) - continue; + // Push the stuff related to the current paths to the arrays + currentPath.push(path); + currentLength += path.statistics.sphericalLengthMeters; + currentConnections.push(...path.getLatLngs()); + + // Fill in capture points. + let capturePoints = L.geodesic(path.getLatLngs(), {segmentsNumber: numberOfImages}).getActualLatLngs()[0]; + for (let point of capturePoints) + currentPoints.push(this.createCapturePoint(point, color)); - minLength = currentLength; - shortestPath = currentPath; - shortestPathConnections = currentConnections; - shortestPathPoints = currentPoints; + deltaBy += this.By; } - } - // Limit polygon size by limiting total approximate paths count. This is not 100% accurate but close enough. - /*if (!shortestPath) { - layersWereRemoved = true; - this.polygonGroup.removeLayer(layer); - return; + if (currentLength >= minLength) + continue; + + minLength = currentLength; + shortestPath = currentPath; + shortestPathConnections = currentConnections; + shortestPathPoints = currentPoints; } - this.pathsGroup.addLayer(shortestPath);*/ this.addPolygon(layer); this.internalConnections.addLayer(L.geodesic(shortestPathConnections, { @@ -219,50 +248,80 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend({ for (let marker of shortestPathPoints) this.pointsGroup.addLayer(marker); - - }); - this.updatePathsMeta(); - this.updateLayersVisibility(); - this.calculateParameters(); - return; - if (layersWereRemoved) - window.alert(L.ALS.locale.rectangleLayersSkipped); + if (layersWereInvalidated) + window.alert(L.ALS.locale.polygonLayersSkipped); this.map.addLayer(this.labelsGroup); // Nothing in the base layer hides or shows it, so it's only hidden in code above + this.updatePathsMeta(); this.updateLayersVisibility(); this.calculateParameters(); - this.writeToHistory(); + this.writeToHistoryDebounced(); }, - perpendicularLine: function (proj, p1, p2, extendFrom = "end", direction = 1) { - // Project coordinates to the gnomonic projection and work with lines as with vectors. + /** + * Builds an "infinite" perpendicular line to the given line defined by p1 and p2 (let's call it reference line). + * Then crops the perpendicular by the given polygon and returns it. + * + * First point of the perpendicular always lies on the reference line. + * + * @param p1 {number[]} First point of the reference line + * @param p2 {number[]} Second point of the reference line + * @param polygon {number[][]} Polygon to crop perpendicular with. + * @param moveToCenter {boolean} If true, perpendicular will be drawn from the center of the reference line. Otherwise, perpendicular will be drawn from p1. + * @returns {number[][]|undefined} Perpendicular or undefined, if line doesn't intersect the polygon. + */ + perpendicularLine: function (p1, p2, polygon, moveToCenter = false) { let [p1x, p1y] = p1, [p2x, p2y] = p2, x = p2x - p1x, y = p2y - p1y, - // Find an orthogonal vector - perpX = 1000, + perpX, perpY; + + // Find an orthogonal vector + if (y === 0) { + // Build vertical line for horizontal reference lines + perpX = 0; + perpY = 1000; + } else { + // Build orthogonal vectors for other lines + perpX = 1000; perpY = -perpX * x / y; - - // Check if orthogonal vector in correct direction. If not, reverse it. - if (Math.sign(perpY) !== direction) { - perpX = -perpX; - perpY = -perpY; } - // Move vector back - perpX += p1x; - perpY += p1y; + // For each negative and positive directions + let line = []; + for (let sign of [1, -1]) { + // Scale the perpendicular to the maximum distance in gnomonic projection + let [px, py] = this.scaleLine([[0, 0], [sign * perpX, sign * perpY]], this.maxGnomonicPointDistance)[1]; - // Scale line - let line = this.scaleLine([[p1x, p1y], [perpX, perpY]], this.maxGeodesicLengthMeters); + // Move perpendicular back + px += p1x; + py += p1y; - if (extendFrom !== "both") - return line; + if (moveToCenter) { + // Move perpendicular by the half of the original vector + px += x / 2; + py += y / 2; + } + + line.push([px, py]); + } - return [this.perpendicularLine(proj, p1, p2, "end", -direction)[1], line[1]]; + let clippedLine = MathTools.clipLineByPolygon(line, polygon); + + if (!clippedLine) + return; + + return MathTools.isPointOnLine(clippedLine[0], [p1, p2]) ? clippedLine : clippedLine.reverse(); }, + /** + * Scales the line from the first point to the target length + * + * @param line {number[][]} Line to scale + * @param targetLength {number} Target line length in meters + * @returns {number[][]} Scaled line + */ scaleLine: function (line, targetLength) { let [p1, p2] = line, [p1x, p1y] = p1, [p2x, p2y] = p2, dx = p2x - p1x, dy = p2y - p1y, @@ -272,6 +331,10 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend({ return [[p1x, p1y], [dx + p1x, dy + p1y]]; }, + isPointValid: function (point) { + return Math.sqrt(point[0] ** 2 + point[1] ** 2) <= this.maxGnomonicPointDistance; + }, + statics: { wizard: L.ALS.SynthPolygonWizard, settings: new L.ALS.SynthPolygonSettings(), diff --git a/SynthRectangleLayer/SynthRectangleLayer.js b/SynthRectangleLayer/SynthRectangleLayer.js index ed924776..b8986071 100644 --- a/SynthRectangleLayer/SynthRectangleLayer.js +++ b/SynthRectangleLayer/SynthRectangleLayer.js @@ -25,26 +25,24 @@ L.ALS.SynthRectangleLayer = L.ALS.SynthRectangleBaseLayer.extend({ this.removePolygon(this.polygons[name], false); this.polygons = {} - this.clearLabels("polygonErrorsLabelsIDs"); - - let layersWereRemoved = false; + let layersWereInvalidated = false; this.polygonGroup.eachLayer((layer) => { + // Limit polygon size by limiting total paths count let bounds = layer.getBounds(), topLeft = bounds.getNorthWest(), parallelsPathsCount = Math.ceil(this.getParallelOrMeridianLineLength(topLeft, bounds.getSouthWest()) / this.By) + 1, meridiansPathsCount = Math.ceil(this.getParallelOrMeridianLineLength(topLeft, bounds.getNorthEast()) / this.By) + 1; - // Limit polygon size by limiting total approximate paths count. This is not 100% accurate but close enough. if (meridiansPathsCount + parallelsPathsCount > 150) { - layersWereRemoved = true; - this.denyPolygon(layer, "polygonTooBig"); + layersWereInvalidated = true; + this.invalidatePolygon(layer); return; } this.addPolygon(layer); }); - if (layersWereRemoved) + if (layersWereInvalidated) window.alert(L.ALS.locale.rectangleLayersSkipped); this.map.addLayer(this.labelsGroup); // Nothing in the base layer hides or shows it, so it's only hidden in code above diff --git a/locales/English.js b/locales/English.js index 0121f30e..8c530514 100644 --- a/locales/English.js +++ b/locales/English.js @@ -20,9 +20,9 @@ L.ALS.Locales.addLocaleProperties("English", { // SynthLineLayer lineLayerColor: "Line color:", settingsLineLayerColor: "Default line color:", + lineLayersSkipped: "One or more lines has been skipped because they're too long. These lines have red color.", // SynthGridWizard - gridWizardDisplayName: "Grid Layer", gridWizardNotification: `If map scale is too low, grid will be hidden. Please, zoom in to see it. @@ -144,8 +144,10 @@ L.ALS.Locales.addLocaleProperties("English", { geometryDefaultBorderColor: "Default border color:", // SynthPolygonLayer + polygonLayerName: "Polygon Layer", polygonPathsColor: "Paths color:", polygonHidePaths: "Hide paths", + polygonLayersSkipped: "One or more polygons has been skipped because they're too big. These polygons have red color.", // Search searchButtonTitle: "Search Geometry Layers or OSM", diff --git a/locales/Russian.js b/locales/Russian.js index 62b285d8..05b0eb98 100644 --- a/locales/Russian.js +++ b/locales/Russian.js @@ -20,6 +20,7 @@ L.ALS.Locales.addLocaleProperties("Русский", { // SynthLineLayer lineLayerColor: "Цвет линий:", settingsLineLayerColor: "Цвет линий по умолчанию:", + lineLayersSkipped: "Одна или несколько линий были пропущены, так как они слишком длинные. Данные линии имеют красный цвет.", // SynthGridWizard @@ -139,9 +140,10 @@ L.ALS.Locales.addLocaleProperties("Русский", { geometryDefaultBorderColor: "Цвет обводки по умолчанию:", // SynthPolygonLayer - + polygonLayerName: "Слой Полигонов", polygonPathsColor: "Цвет маршрутов:", polygonHidePaths: "Скрыть маршруты", + polygonLayersSkipped: "Один или несколько полигонов были пропущены, так как они слишком большие. Данные полигоны имеют красный цвет.", // Search searchButtonTitle: "Поиск в Слоях Геометрии и OSM", diff --git a/main.js b/main.js index 3d33f688..b28297a1 100644 --- a/main.js +++ b/main.js @@ -14,7 +14,7 @@ window.L = require("leaflet"); * Segments number to use when displaying L.Geodesic * @type {number} */ -L.GEODESIC_SEGMENTS = 500; +L.GEODESIC_SEGMENTS = 1000; L.Geodesic = require("leaflet.geodesic").GeodesicLine; require("leaflet-draw"); diff --git a/package-lock.json b/package-lock.json index 97f986f4..afd64dd7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6078,7 +6078,7 @@ }, "node_modules/leaflet.geodesic": { "version": "2.6.1", - "resolved": "git+ssh://git@github.com/matafokka/Leaflet.Geodesic.git#ff58fda7783f91c9c4b2595903b73c5359f66703", + "resolved": "git+ssh://git@github.com/matafokka/Leaflet.Geodesic.git#fa63ecc51ce77256d1ca2cb96b139e0040b6a6da", "dev": true, "license": "GPL-3.0", "peerDependencies": { @@ -17617,7 +17617,7 @@ "dev": true }, "leaflet.geodesic": { - "version": "git+ssh://git@github.com/matafokka/Leaflet.Geodesic.git#ff58fda7783f91c9c4b2595903b73c5359f66703", + "version": "git+ssh://git@github.com/matafokka/Leaflet.Geodesic.git#fa63ecc51ce77256d1ca2cb96b139e0040b6a6da", "dev": true, "from": "leaflet.geodesic@github:matafokka/Leaflet.Geodesic", "requires": {} From 99c201a29e22cd3b8cf0231c69df554a148849b3 Mon Sep 17 00:00:00 2001 From: matafokka Date: Sun, 8 May 2022 15:47:12 +0300 Subject: [PATCH 08/48] Fixed changing thickness not working --- SynthBaseLayer/SynthBaseLayer.js | 9 ++++++++- SynthPolygonBaseLayer/SynthPolygonBaseLayer.js | 1 - 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/SynthBaseLayer/SynthBaseLayer.js b/SynthBaseLayer/SynthBaseLayer.js index 7245118b..c47338c2 100644 --- a/SynthBaseLayer/SynthBaseLayer.js +++ b/SynthBaseLayer/SynthBaseLayer.js @@ -127,7 +127,14 @@ L.ALS.SynthBaseLayer = L.ALS.Layer.extend(/** @lends L.ALS.SynthBaseLayer.protot * * @type {L.FeatureGroup[]} */ - this.toUpdateThickness = [...path1AdditionalLayers, ...path2AdditionalLayers]; + this.toUpdateThickness = []; + + for (let arr of [path1AdditionalLayers, path2AdditionalLayers]) { + for (let item of arr) { + if (item !== undefined) + this.toUpdateThickness.push(item); + } + } for (let i = 0; i < this.paths.length; i++) { let path = this.paths[i]; diff --git a/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js b/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js index d8b1bcc0..f3ee08d6 100644 --- a/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js +++ b/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js @@ -74,7 +74,6 @@ L.ALS.SynthPolygonBaseLayer = L.ALS.SynthBaseLayer.extend( /** @lends L.ALS.Synt this.widgetsGroup, this.labelsGroup, ]; - // TODO: Fix thickness this.addLayers(this.polygonGroup, this.widgetsGroup, this.bordersGroup, pointsGroup1, path1ActualPathGroup, this.labelsGroup); this.toUpdateThickness.push(this.polygonGroup, this.bordersGroup, pointsGroup1, path1ActualPathGroup); From e50b0af7ce516d37087f1802a0173c9c500bc40a Mon Sep 17 00:00:00 2001 From: matafokka Date: Sun, 8 May 2022 15:59:55 +0300 Subject: [PATCH 09/48] Fixed handlers of inactive layers being called on edit start and end --- SynthBaseLayer/draw.js | 3 +++ SynthLineLayer/SynthLineLayer.js | 6 ++++++ SynthPolygonBaseLayer/SynthPolygonBaseLayer.js | 3 +++ SynthPolygonLayer/SynthPolygonLayer.js | 3 +++ SynthRectangleLayer/SynthRectangleLayer.js | 3 +++ 5 files changed, 18 insertions(+) diff --git a/SynthBaseLayer/draw.js b/SynthBaseLayer/draw.js index 5475a145..33e1b46c 100644 --- a/SynthBaseLayer/draw.js +++ b/SynthBaseLayer/draw.js @@ -38,6 +38,9 @@ L.ALS.SynthBaseLayer.prototype.enableDraw = function (drawControls, drawingGroup } L.ALS.SynthBaseLayer.prototype.onDraw = function (e) { + if (!this.isSelected) + return; + this._drawingGroup.addLayer(e.layer); let borderColorId, fillColor; diff --git a/SynthLineLayer/SynthLineLayer.js b/SynthLineLayer/SynthLineLayer.js index 54157bfe..12556735 100644 --- a/SynthLineLayer/SynthLineLayer.js +++ b/SynthLineLayer/SynthLineLayer.js @@ -55,6 +55,9 @@ L.ALS.SynthLineLayer = L.ALS.SynthBaseLayer.extend(/** @lends L.ALS.SynthLineLay }, onEditStart: function () { + if (!this.isSelected) + return; + this.map.removeLayer(this.pathsGroup); this.map.removeLayer(this.connectionsGroup); this.map.removeLayer(this.pointsGroup); @@ -62,6 +65,9 @@ L.ALS.SynthLineLayer = L.ALS.SynthBaseLayer.extend(/** @lends L.ALS.SynthLineLay }, onEditEnd: function () { + if (!this.isSelected) + return; + this.pathsGroup.clearLayers(); this.pointsGroup.clearLayers(); this.errorGroup.clearLayers(); diff --git a/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js b/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js index f3ee08d6..7853a6ae 100644 --- a/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js +++ b/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js @@ -209,6 +209,9 @@ L.ALS.SynthPolygonBaseLayer = L.ALS.SynthBaseLayer.extend( /** @lends L.ALS.Synt }, onEditStart: function () { + if (!this.isSelected) + return; + for (let group of this.groupsToHideOnEditStart) { if (group) this.hideOrShowLayer(true, group); diff --git a/SynthPolygonLayer/SynthPolygonLayer.js b/SynthPolygonLayer/SynthPolygonLayer.js index 597bcab2..57268e4e 100644 --- a/SynthPolygonLayer/SynthPolygonLayer.js +++ b/SynthPolygonLayer/SynthPolygonLayer.js @@ -55,6 +55,9 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend({ }, onEditEnd: function () { + if (!this.isSelected) + return; + let color = this.getWidgetById("color0").getValue(), lineOptions = { color, thickness: this.lineThicknessValue, segmentsNumber: L.GEODESIC_SEGMENTS, diff --git a/SynthRectangleLayer/SynthRectangleLayer.js b/SynthRectangleLayer/SynthRectangleLayer.js index b8986071..5b694073 100644 --- a/SynthRectangleLayer/SynthRectangleLayer.js +++ b/SynthRectangleLayer/SynthRectangleLayer.js @@ -21,6 +21,9 @@ L.ALS.SynthRectangleLayer = L.ALS.SynthRectangleBaseLayer.extend({ }, onEditEnd: function () { + if (!this.isSelected) + return; + for (let name in this.polygons) this.removePolygon(this.polygons[name], false); this.polygons = {} From 1ba62abf183f3ca76ed308223d82311211ab2390 Mon Sep 17 00:00:00 2001 From: matafokka Date: Sun, 8 May 2022 17:28:00 +0300 Subject: [PATCH 10/48] Fixed broken serialization after refactoring Polygon Layer is still in development, so I haven't tested it yet --- SynthLineLayer/SynthLineLayer.js | 7 +- .../SynthPolygonBaseLayer.js | 16 ++--- SynthPolygonBaseLayer/polygons.js | 2 + SynthPolygonBaseLayer/serialization.js | 65 +++++++++++++++++++ SynthPolygonLayer/SynthPolygonLayer.js | 4 ++ .../SynthRectangleBaseLayer.js | 1 - SynthRectangleBaseLayer/serialization.js | 43 ------------ SynthRectangleLayer/SynthRectangleLayer.js | 5 +- 8 files changed, 84 insertions(+), 59 deletions(-) create mode 100644 SynthPolygonBaseLayer/serialization.js delete mode 100644 SynthRectangleBaseLayer/serialization.js diff --git a/SynthLineLayer/SynthLineLayer.js b/SynthLineLayer/SynthLineLayer.js index 12556735..148f45e4 100644 --- a/SynthLineLayer/SynthLineLayer.js +++ b/SynthLineLayer/SynthLineLayer.js @@ -44,6 +44,8 @@ L.ALS.SynthLineLayer = L.ALS.SynthBaseLayer.extend(/** @lends L.ALS.SynthLineLay this.pointsGroup = L.featureGroup(); this.calculateParameters(); + + this.isAfterDeserialization = false; }, _hideCapturePoints: function (widget) { @@ -126,7 +128,7 @@ L.ALS.SynthLineLayer = L.ALS.SynthBaseLayer.extend(/** @lends L.ALS.SynthLineLay if (linesWereInvalidated) window.alert(L.ALS.locale.lineLayersSkipped); - this.writeToHistory(); + this.writeToHistoryDebounced(); }, calculateParameters: function () { @@ -161,12 +163,13 @@ L.ALS.SynthLineLayer = L.ALS.SynthBaseLayer.extend(/** @lends L.ALS.SynthLineLay settings: new L.ALS.SynthLineSettings(), deserialize: function (serialized, layerSystem, settings, seenObjects) { - let object = L.ALS.Layer.deserialize(serialized, layerSystem, settings, seenObjects), + let object = L.ALS.SynthBaseLayer.deserialize(serialized, layerSystem, settings, seenObjects), lines = L.ALS.Serializable.deserialize(serialized.lines, seenObjects); for (let line of lines) object.drawingGroup.addLayer(new L.Geodesic(line, object.drawControls.polyline.shapeOptions)); + object.isAfterDeserialization = true; object.onEditEnd(); delete object.lines; diff --git a/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js b/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js index 7853a6ae..fa0f00ba 100644 --- a/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js +++ b/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js @@ -42,8 +42,9 @@ L.ALS.SynthPolygonBaseLayer = L.ALS.SynthBaseLayer.extend( /** @lends L.ALS.Synt hidePaths2WidgetId = "hidePathsByMeridians", ) { this.polygons = {}; + this.invalidPolygons = {}; this.polygonsWidgets = {}; - this.serializationIgnoreList.push("polygons", "lngDistance", "latDistance", "_currentStandardScale"); + this.serializationIgnoreList.push("polygons", "invalidPolygons", "lngDistance", "latDistance", "_currentStandardScale"); this.polygonGroup = L.featureGroup(); this.widgetsGroup = L.featureGroup(); @@ -266,17 +267,8 @@ L.ALS.SynthPolygonBaseLayer = L.ALS.SynthBaseLayer.extend( /** @lends L.ALS.Synt return parseFloat(n.toFixed(5)); }, - statics: { - deserialize: function (serialized, layerSystem, settings, seenObjects) { - let deserialized = L.ALS.SynthRectangleBaseLayer.deserialize(serialized, layerSystem, settings, seenObjects); - for (let id in deserialized.polygons) - deserialized.polygonGroup.addLayer(deserialized.polygons[id]); - deserialized.updatePolygonsColors(); - return deserialized; - }, - } - }); require("./DEM.js"); -require("./polygons.js"); \ No newline at end of file +require("./polygons.js"); +require("./serialization.js"); \ No newline at end of file diff --git a/SynthPolygonBaseLayer/polygons.js b/SynthPolygonBaseLayer/polygons.js index aff4c214..03d68c0c 100644 --- a/SynthPolygonBaseLayer/polygons.js +++ b/SynthPolygonBaseLayer/polygons.js @@ -111,5 +111,7 @@ L.ALS.SynthPolygonBaseLayer.prototype.calculatePolygonParameters = function (wid } L.ALS.SynthPolygonBaseLayer.prototype.invalidatePolygon = function (polygon) { + polygon._intName = this._generatePolygonName(polygon); polygon.setStyle({color: "red", fillColor: "red"}); + this.invalidPolygons[polygon._intName] = polygon; } \ No newline at end of file diff --git a/SynthPolygonBaseLayer/serialization.js b/SynthPolygonBaseLayer/serialization.js new file mode 100644 index 00000000..58fa7f34 --- /dev/null +++ b/SynthPolygonBaseLayer/serialization.js @@ -0,0 +1,65 @@ +L.ALS.SynthPolygonBaseLayer.prototype.serialize = function (seenObjects) { + let serialized = this.getObjectToSerializeTo(seenObjects); + + serialized.polygonsWidgets = L.ALS.Serializable.serializeAnyObject(this.polygonsWidgets, seenObjects); + + // Gather selected polygons' coordinates + + for (let objName of ["polygons", "invalidPolygons"]) { + let polyObject = this[objName]; + serialized[objName] = {}; + + for (let name in polyObject) { + if (!polyObject.hasOwnProperty(name)) + continue; + + let poly = polyObject[name]; + serialized[objName][name] = poly[poly instanceof L.Rectangle ? "getBounds" : "getLatLngs"](); + } + } + + this.clearSerializedPathsWidgets(serialized); + return serialized; +} + +L.ALS.SynthPolygonBaseLayer._toUpdateColors = ["borderColor", "fillColor", "color0", "color1"]; + +L.ALS.SynthPolygonBaseLayer.deserialize = function (serialized, layerSystem, settings, seenObjects) { + let object = L.ALS.SynthBaseLayer.deserialize(serialized, layerSystem, settings, seenObjects); + object.isAfterDeserialization = true; + + for (let objName of ["polygons", "invalidPolygons"]) { + let serializedPolyObj = serialized[objName], + deserializedPolyObj = object[objName]; + + for (let prop in serializedPolyObj) { + if (!serializedPolyObj.hasOwnProperty(prop)) + continue; + + let value = serializedPolyObj[prop], + newPoly = L[value.serializableClassName === "L.LatLngBounds" ? "rectangle" : "polygon"](value); + + if (objName === "invalidPolygons") + object.invalidatePolygon(newPoly) + else + deserializedPolyObj[prop] = newPoly; + + object.polygonGroup.addLayer(newPoly); + } + } + + for (let prop in object.polygonsWidgets) { + let widget = object.polygonsWidgets[prop]; + if (widget.addTo) + object.widgetsGroup.addLayer(widget); + } + + for (let color of this._toUpdateColors) + object.setColor(object.getWidgetById(color)); + + object.setAirportLatLng(); + object.calculateParameters(); + object.updatePolygonsColors(); + + return object; +} \ No newline at end of file diff --git a/SynthPolygonLayer/SynthPolygonLayer.js b/SynthPolygonLayer/SynthPolygonLayer.js index 57268e4e..4c7991d5 100644 --- a/SynthPolygonLayer/SynthPolygonLayer.js +++ b/SynthPolygonLayer/SynthPolygonLayer.js @@ -52,6 +52,8 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend({ this.calculateThreshold(settings); // Update hiding threshold this.calculateParameters(); this.updateLayersVisibility(); + + this.isAfterDeserialization = false; }, onEditEnd: function () { @@ -65,7 +67,9 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend({ for (let name in this.polygons) this.removePolygon(this.polygons[name], false); + this.polygons = {} + this.invalidPolygons = {}; this.clearPaths(); diff --git a/SynthRectangleBaseLayer/SynthRectangleBaseLayer.js b/SynthRectangleBaseLayer/SynthRectangleBaseLayer.js index 05b89994..43de56ec 100644 --- a/SynthRectangleBaseLayer/SynthRectangleBaseLayer.js +++ b/SynthRectangleBaseLayer/SynthRectangleBaseLayer.js @@ -120,5 +120,4 @@ L.ALS.SynthRectangleBaseLayer = L.ALS.SynthPolygonBaseLayer.extend( /** @lends L require("./drawPaths.js"); require("./misc.js"); -require("./serialization.js"); require("./toGeoJSON.js"); \ No newline at end of file diff --git a/SynthRectangleBaseLayer/serialization.js b/SynthRectangleBaseLayer/serialization.js deleted file mode 100644 index f4702f17..00000000 --- a/SynthRectangleBaseLayer/serialization.js +++ /dev/null @@ -1,43 +0,0 @@ -L.ALS.SynthRectangleBaseLayer.prototype.serialize = function (seenObjects) { - let serialized = this.getObjectToSerializeTo(seenObjects); - - serialized.polygonsWidgets = L.ALS.Serializable.serializeAnyObject(this.polygonsWidgets, seenObjects); - serialized.polygons = {}; - - // Gather selected polygons' coordinates - for (let name in this.polygons) { - if (!this.polygons.hasOwnProperty(name)) - continue; - let poly = this.polygons[name]; - serialized.polygons[name] = poly[poly instanceof L.Rectangle ? "getBounds" : "getLatLngs"](); - } - - this.clearSerializedPathsWidgets(serialized); - return serialized; -} - -L.ALS.SynthRectangleBaseLayer._toUpdateColors = ["borderColor", "fillColor", "color0", "color1"]; - -L.ALS.SynthRectangleBaseLayer.deserialize = function (serialized, layerSystem, settings, seenObjects) { - let object = L.ALS.Layer.deserialize(serialized, layerSystem, settings, seenObjects); - object.isAfterDeserialization = true; - - for (let prop in serialized.polygons) { - let value = serialized.polygons[prop]; - object.polygons[prop] = L[value.serializableClassName === "L.LatLngBounds" ? "rectangle" : "polygon"](value); - } - - for (let prop in object.polygonsWidgets) { - let widget = object.polygonsWidgets[prop]; - if (widget.addTo) - object.widgetsGroup.addLayer(widget); - } - - for (let color of this._toUpdateColors) - object.setColor(object.getWidgetById(color)); - - object.setAirportLatLng(); - object.calculateParameters(); - - return object; -} \ No newline at end of file diff --git a/SynthRectangleLayer/SynthRectangleLayer.js b/SynthRectangleLayer/SynthRectangleLayer.js index 5b694073..a9a7d8fa 100644 --- a/SynthRectangleLayer/SynthRectangleLayer.js +++ b/SynthRectangleLayer/SynthRectangleLayer.js @@ -18,6 +18,7 @@ L.ALS.SynthRectangleLayer = L.ALS.SynthRectangleBaseLayer.extend({ } } }, this.polygonGroup); + this.isAfterDeserialization = false; }, onEditEnd: function () { @@ -26,7 +27,9 @@ L.ALS.SynthRectangleLayer = L.ALS.SynthRectangleBaseLayer.extend({ for (let name in this.polygons) this.removePolygon(this.polygons[name], false); + this.polygons = {} + this.invalidPolygons = {}; let layersWereInvalidated = false; @@ -51,7 +54,7 @@ L.ALS.SynthRectangleLayer = L.ALS.SynthRectangleBaseLayer.extend({ this.map.addLayer(this.labelsGroup); // Nothing in the base layer hides or shows it, so it's only hidden in code above this.updateLayersVisibility(); this.calculateParameters(); - this.writeToHistory(); + this.writeToHistoryDebounced(); }, statics: { From c26db4cdfdee9507178ec2ab3351557c001efec1 Mon Sep 17 00:00:00 2001 From: matafokka Date: Sun, 8 May 2022 17:53:52 +0300 Subject: [PATCH 11/48] Added paths' numbers to the Polygon Layer --- SynthPolygonBaseLayer/serialization.js | 2 +- SynthPolygonLayer/SynthPolygonLayer.js | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/SynthPolygonBaseLayer/serialization.js b/SynthPolygonBaseLayer/serialization.js index 58fa7f34..f91d90c7 100644 --- a/SynthPolygonBaseLayer/serialization.js +++ b/SynthPolygonBaseLayer/serialization.js @@ -40,7 +40,7 @@ L.ALS.SynthPolygonBaseLayer.deserialize = function (serialized, layerSystem, set newPoly = L[value.serializableClassName === "L.LatLngBounds" ? "rectangle" : "polygon"](value); if (objName === "invalidPolygons") - object.invalidatePolygon(newPoly) + object.invalidatePolygon(newPoly); else deserializedPolyObj[prop] = newPoly; diff --git a/SynthPolygonLayer/SynthPolygonLayer.js b/SynthPolygonLayer/SynthPolygonLayer.js index 4c7991d5..4828ea88 100644 --- a/SynthPolygonLayer/SynthPolygonLayer.js +++ b/SynthPolygonLayer/SynthPolygonLayer.js @@ -68,6 +68,7 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend({ for (let name in this.polygons) this.removePolygon(this.polygons[name], false); + this.labelsGroup.deleteAllLabels(); this.polygons = {} this.invalidPolygons = {}; @@ -250,9 +251,19 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend({ ...lineOptions, dashArray: this.dashedLine })); - for (let path of shortestPath) + let number = 0; + for (let path of shortestPath) { this.pathGroup.addLayer(path); + // Add numbers + let latLngs = path.getLatLngs(); + + for (let point of latLngs) { + this.labelsGroup.addLabel("", [point.lat, point.lng], number, L.LabelLayer.DefaultDisplayOptions.Message); + number++; + } + } + for (let marker of shortestPathPoints) this.pointsGroup.addLayer(marker); }); @@ -267,6 +278,11 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend({ this.writeToHistoryDebounced(); }, + updateLayersVisibility: function () { + L.ALS.SynthPolygonBaseLayer.prototype.updateLayersVisibility.call(this); + this.hideOrShowLayer(this._doHidePathsNumbers || this.getWidgetById("polygonHidePaths").getValue(), this.labelsGroup); + }, + /** * Builds an "infinite" perpendicular line to the given line defined by p1 and p2 (let's call it reference line). * Then crops the perpendicular by the given polygon and returns it. From 6a62eea68c26a1f5900894a944687ac1b35a42eb Mon Sep 17 00:00:00 2001 From: matafokka Date: Sun, 8 May 2022 18:20:31 +0300 Subject: [PATCH 12/48] Polygon Layer fixes - Fixed deserialization. - Fixed paths not redrawing after parameters update. - Also fixed the whole data flow logic. --- SynthPolygonBaseLayer/serialization.js | 7 +++++-- SynthPolygonLayer/SynthPolygonLayer.js | 9 +++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/SynthPolygonBaseLayer/serialization.js b/SynthPolygonBaseLayer/serialization.js index f91d90c7..ae668294 100644 --- a/SynthPolygonBaseLayer/serialization.js +++ b/SynthPolygonBaseLayer/serialization.js @@ -54,8 +54,11 @@ L.ALS.SynthPolygonBaseLayer.deserialize = function (serialized, layerSystem, set object.widgetsGroup.addLayer(widget); } - for (let color of this._toUpdateColors) - object.setColor(object.getWidgetById(color)); + for (let color of this._toUpdateColors) { + let widget = object.getWidgetById(color); + if (widget) + object.setColor(widget); + } object.setAirportLatLng(); object.calculateParameters(); diff --git a/SynthPolygonLayer/SynthPolygonLayer.js b/SynthPolygonLayer/SynthPolygonLayer.js index 4828ea88..17cb130e 100644 --- a/SynthPolygonLayer/SynthPolygonLayer.js +++ b/SynthPolygonLayer/SynthPolygonLayer.js @@ -2,6 +2,7 @@ require("./SynthPolygonWizard.js"); require("./SynthPolygonSettings.js"); const MathTools = require("../MathTools.js"); const proj4 = require("proj4"); +const debounce = require("debounce") L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend({ @@ -50,8 +51,8 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend({ }, this.polygonGroup); this.calculateThreshold(settings); // Update hiding threshold + this.onEditEndDebounced = debounce(() => this.onEditEnd(), 300); // Math operations are too slow for immediate update this.calculateParameters(); - this.updateLayersVisibility(); this.isAfterDeserialization = false; }, @@ -274,10 +275,14 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend({ this.map.addLayer(this.labelsGroup); // Nothing in the base layer hides or shows it, so it's only hidden in code above this.updatePathsMeta(); this.updateLayersVisibility(); - this.calculateParameters(); this.writeToHistoryDebounced(); }, + calculateParameters: function () { + L.ALS.SynthPolygonBaseLayer.prototype.calculateParameters.call(this); + this.onEditEndDebounced(); + }, + updateLayersVisibility: function () { L.ALS.SynthPolygonBaseLayer.prototype.updateLayersVisibility.call(this); this.hideOrShowLayer(this._doHidePathsNumbers || this.getWidgetById("polygonHidePaths").getValue(), this.labelsGroup); From 62c73925e16f04c9e2cfba7aa50244e1f739b018 Mon Sep 17 00:00:00 2001 From: matafokka Date: Sun, 8 May 2022 19:53:14 +0300 Subject: [PATCH 13/48] Localized draw controls --- SynthBaseLayer/draw.js | 8 +++++++- main.js | 10 ++++++++++ package-lock.json | 17 +++++++++++++++++ package.json | 1 + 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/SynthBaseLayer/draw.js b/SynthBaseLayer/draw.js index 33e1b46c..ca0c3896 100644 --- a/SynthBaseLayer/draw.js +++ b/SynthBaseLayer/draw.js @@ -34,7 +34,13 @@ L.ALS.SynthBaseLayer.prototype.enableDraw = function (drawControls, drawingGroup this.addEventListenerTo(this.map, "draw:created", "onDraw"); this.addEventListenerTo(this.map, "draw:drawstart draw:editstart draw:deletestart", "onEditStart"); this.addEventListenerTo(this.map, "draw:drawstop draw:editstop draw:deletestop", "onEditEnd"); - this.addControl(this.drawControl, "top", "follow-menu"); + let addDrawControl = () => this.addControl(this.drawControl, "top", "follow-menu"); + addDrawControl(); + + document.body.addEventListener("synthflight-locale-changed", () => { + this.removeControl(this.drawControl); + addDrawControl(); + }); } L.ALS.SynthBaseLayer.prototype.onDraw = function (e) { diff --git a/main.js b/main.js index b28297a1..e72fab7e 100644 --- a/main.js +++ b/main.js @@ -33,6 +33,16 @@ require("./SynthRectangleLayer/SynthRectangleLayer.js"); require("./SynthLineLayer/SynthLineLayer.js"); require("./SynthPolygonLayer/SynthPolygonLayer.js"); require("./SearchControl.js"); +const drawLocales = require("leaflet-draw-locales").default; + +// Update L.Draw locale on ALS locale change +let oldChangeLocale = L.ALS.Locales.changeLocale; + +L.ALS.Locales.changeLocale = function (locale) { + oldChangeLocale.call(this, locale); + L.drawLocal = drawLocales(L.ALS.locale.language); + L.ALS.Helpers.dispatchEvent(document.body, "synthflight-locale-changed"); +} L.ALS.System.initializeSystem(); diff --git a/package-lock.json b/package-lock.json index afd64dd7..9b92c7a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,7 @@ "keyboardevent-key-polyfill": "^1.1.0", "leaflet": "^1.7.1", "leaflet-draw": "^1.0.4", + "leaflet-draw-locales": "^1.2.1", "leaflet.coordinates": "~0.1.5", "leaflet.geodesic": "github:matafokka/Leaflet.Geodesic", "minisearch": "^4.0.3", @@ -6070,6 +6071,16 @@ "integrity": "sha512-rsQ6saQO5ST5Aj6XRFylr5zvarWgzWnrg46zQ1MEOEIHsppdC/8hnN8qMoFvACsPvTioAuysya/TVtog15tyAQ==", "dev": true }, + "node_modules/leaflet-draw-locales": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/leaflet-draw-locales/-/leaflet-draw-locales-1.2.1.tgz", + "integrity": "sha512-pT7qoUF9cod4B5q9RO4jKF6tLH+sVXhq2Xw34NAWV83w+ne2+ZqWARR5Kis9bREunznQRXNIq4UR9prib1waBw==", + "dev": true, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, "node_modules/leaflet.coordinates": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/leaflet.coordinates/-/leaflet.coordinates-0.1.5.tgz", @@ -17610,6 +17621,12 @@ "integrity": "sha512-rsQ6saQO5ST5Aj6XRFylr5zvarWgzWnrg46zQ1MEOEIHsppdC/8hnN8qMoFvACsPvTioAuysya/TVtog15tyAQ==", "dev": true }, + "leaflet-draw-locales": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/leaflet-draw-locales/-/leaflet-draw-locales-1.2.1.tgz", + "integrity": "sha512-pT7qoUF9cod4B5q9RO4jKF6tLH+sVXhq2Xw34NAWV83w+ne2+ZqWARR5Kis9bREunznQRXNIq4UR9prib1waBw==", + "dev": true + }, "leaflet.coordinates": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/leaflet.coordinates/-/leaflet.coordinates-0.1.5.tgz", diff --git a/package.json b/package.json index 7cbfcb6d..57e2c1af 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "keyboardevent-key-polyfill": "^1.1.0", "leaflet": "^1.7.1", "leaflet-draw": "^1.0.4", + "leaflet-draw-locales": "^1.2.1", "leaflet.coordinates": "~0.1.5", "leaflet.geodesic": "github:matafokka/Leaflet.Geodesic", "minisearch": "^4.0.3", From 03a64e2b7eb06b13181b0954905af4c48382bde2 Mon Sep 17 00:00:00 2001 From: matafokka Date: Mon, 9 May 2022 17:35:36 +0300 Subject: [PATCH 14/48] Polygon Layer features - Added paths numbers. - Added workaround with layer cloning for antimeridians. This workaround feels kinda janky but it works. It also needs to be ported to the Rectangle Layer. --- SynthPolygonBaseLayer/polygons.js | 70 +++++++++++++++++++++++++- SynthPolygonLayer/SynthPolygonLayer.js | 16 +++++- 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/SynthPolygonBaseLayer/polygons.js b/SynthPolygonBaseLayer/polygons.js index 03d68c0c..f1d48f21 100644 --- a/SynthPolygonBaseLayer/polygons.js +++ b/SynthPolygonBaseLayer/polygons.js @@ -19,6 +19,7 @@ L.ALS.MeanHeightButtonHandler = L.ALS.Serializable.extend( /**@lends L.ALS.MeanH L.ALS.SynthPolygonBaseLayer.prototype.addPolygon = function (polygon) { polygon._intName = this._generatePolygonName(polygon); + delete this.invalidPolygons[polygon._intName]; polygon.setStyle({fill: true}); this.polygons[polygon._intName] = polygon; @@ -55,6 +56,9 @@ L.ALS.SynthPolygonBaseLayer.prototype.addPolygon = function (polygon) { this.polygonsWidgets[polygon._intName] = controlsContainer; this.widgetsGroup.addLayer(controlsContainer); + + if (polygon.linkedLayer) + polygon.linkedLayer.setStyle(polygon.options); } L.ALS.SynthPolygonBaseLayer.prototype.removePolygon = function (polygon, removeFromObject = true) { @@ -112,6 +116,70 @@ L.ALS.SynthPolygonBaseLayer.prototype.calculatePolygonParameters = function (wid L.ALS.SynthPolygonBaseLayer.prototype.invalidatePolygon = function (polygon) { polygon._intName = this._generatePolygonName(polygon); - polygon.setStyle({color: "red", fillColor: "red"}); + + for (let poly of [polygon, polygon.linkedLayer]) { + if (poly) + poly.setStyle({color: "red", fillColor: "red"}); + } this.invalidPolygons[polygon._intName] = polygon; + delete this.polygons[polygon._intName]; +} + +L.ALS.SynthPolygonBaseLayer.prototype._getRectOrPolyCoords = function (layer) { + if (layer instanceof L.Rectangle) { + let {_northEast, _southWest} = layer.getBounds(); + return [_northEast, _southWest]; + } + return layer.getLatLngs()[0]; +} + +L.ALS.SynthPolygonBaseLayer.prototype._setRectOrPolyCoords = function (layer, coords) { + if (layer instanceof L.Rectangle) + layer.setBounds(coords); + else + layer.setLatLngs(coords); +} + +L.ALS.SynthPolygonBaseLayer.prototype.cloneLayerIfNeeded = function (layer) { + // Clone layers that crosses antimeridians + let bounds = layer.getBounds(), + crossingWest = bounds.getWest() < -180, + crossingEast = bounds.getEast() > 180, + crossingOne = crossingWest || crossingEast, + crossingBoth = crossingEast && crossingWest + + if (!layer.linkedLayer && crossingOne && !crossingBoth) { + let clonedLayer = layer instanceof L.Rectangle ? L.rectangle(bounds) : L.polygon([]), + moveTo = crossingWest ? 1 : -1, + setLinkedLatLngs = (editedLayer) => { + let latlngs = this._getRectOrPolyCoords(editedLayer), newLatLngs = []; + + for (let coord of latlngs) + newLatLngs.push([coord.lat, coord.lng + (editedLayer.linkedLayer.isCloned ? 1 : -1) * moveTo * 360]); + + this._setRectOrPolyCoords(editedLayer.linkedLayer, newLatLngs); + } + + clonedLayer.isCloned = true; + clonedLayer.linkedLayer = layer; + layer.linkedLayer = clonedLayer; + this.polygonGroup.addLayer(clonedLayer); + + for (let lyr of [layer, clonedLayer]) { + let editHandler = () => { + setLinkedLatLngs(lyr); + lyr.linkedLayer.editing.updateMarkers(); + } + + lyr.on("editdrag", editHandler); + lyr.editHandler = editHandler; + } + + setLinkedLatLngs(layer); + } else if (layer.linkedLayer && (!crossingOne || crossingBoth)) { + layer.off("editdrag", layer.editHandler); + this.polygonGroup.removeLayer(layer.linkedLayer); + delete layer.editHandler; + delete layer.linkedLayer; + } } \ No newline at end of file diff --git a/SynthPolygonLayer/SynthPolygonLayer.js b/SynthPolygonLayer/SynthPolygonLayer.js index 17cb130e..e47b86e0 100644 --- a/SynthPolygonLayer/SynthPolygonLayer.js +++ b/SynthPolygonLayer/SynthPolygonLayer.js @@ -98,6 +98,19 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend({ // We'll also crop the paths by the hull, so there won't be empty space along the paths. this.polygonGroup.eachLayer((layer) => { + // Remove a linked layer when a layer either original or cloned has been removed + if (layer.linkedLayer && !this.polygonGroup.hasLayer(layer.linkedLayer)) { + this.polygonGroup.removeLayer(layer); + return; + } + + // Skip cloned layers + if (layer.isCloned) { + return; + } + + this.cloneLayerIfNeeded(layer); + // Build projection. The center will be the center of the polygon. It doesn't matter that much, but center // Allows us to expand polygon size a bit when it's close to the projection's coordinates limits. let center = layer.getBounds().getCenter(), @@ -260,7 +273,8 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend({ let latLngs = path.getLatLngs(); for (let point of latLngs) { - this.labelsGroup.addLabel("", [point.lat, point.lng], number, L.LabelLayer.DefaultDisplayOptions.Message); + let {lat, lng} = point.wrap(); + this.labelsGroup.addLabel("", [lat, lng], number, L.LabelLayer.DefaultDisplayOptions.Message); number++; } } From 371945be058d93c8a4db3e87a6a0d3645ce11d22 Mon Sep 17 00:00:00 2001 From: matafokka Date: Tue, 10 May 2022 19:12:29 +0300 Subject: [PATCH 15/48] Now Polygon Layer and Polyline Layer can load initial geometry from zipped shapefile or GeoJSON Other minor changes in this commit: - Changed style in some locale entries. - Now polygon size limit by path's count is precalculated. - Reduced maximum number of paths in Polygon Layer to 50. - Once again reduced geodesic segments number to 500. I'm not sure what value to keep, gotta think about it. --- SynthGeometryBaseWizard.js | 139 ++++++++++++++++++++++ SynthGeometryLayer/SynthGeometryLayer.js | 72 +++++------ SynthGeometryLayer/SynthGeometryWizard.js | 14 +-- SynthLineLayer/SynthLineLayer.js | 4 +- SynthLineLayer/SynthLineWizard.js | 3 +- SynthPolygonLayer/SynthPolygonLayer.js | 18 +-- SynthPolygonLayer/SynthPolygonWizard.js | 5 +- locales/English.js | 11 +- locales/Russian.js | 11 +- main.js | 3 +- package-lock.json | 14 +-- package.json | 2 +- 12 files changed, 212 insertions(+), 84 deletions(-) create mode 100644 SynthGeometryBaseWizard.js diff --git a/SynthGeometryBaseWizard.js b/SynthGeometryBaseWizard.js new file mode 100644 index 00000000..20ca1989 --- /dev/null +++ b/SynthGeometryBaseWizard.js @@ -0,0 +1,139 @@ +const shp = require("shpjs"); + +L.ALS.SynthGeometryBaseWizard = L.ALS.Wizard.extend({ + + fileLabel: "geometryFileLabel", + + initialize: function () { + + L.ALS.Wizard.prototype.initialize.call(this); + if (!window.FileReader) { + this.addWidget(new L.ALS.Widgets.SimpleLabel("lbl", "geometryBrowserNotSupported", "center", "error")); + return; + } + + this.addWidget( + new L.ALS.Widgets.File("file", this.fileLabel) + ); + }, + + statics: { + /** + * Callback to pass to {@link L.ALS.SynthGeometryBaseWizard.getGeoJSON}. + * + * @callback getGeoJSONCallback + * @param {Object|"NoFileSelected"|"NoFeatures"|"InvalidFileType"} geoJson GeoJSON or an error message + * @param {string|undefined} [fileName=undefined] Name of the loaded file + */ + + /** + * Reads GeoJSON or ShapeFile and calls a callback with the content as GeoJSON and filename. + * + * If an error occurred, the first argument will be an error text, and filename will be `undefined`. + * + * @param wizardResults Wizard results + * @param callback {getGeoJSONCallback} + */ + getGeoJSON: function (wizardResults, callback) { + let file = wizardResults["file"][0], + fileReader = new FileReader(); + + if (!file) { + callback("NoFileSelected"); + return; + } + + // Try to read as shapefile + fileReader.addEventListener("load", (event) => { + shp(event.target.result).then((geoJson) => { + if (geoJson.features.length === 0) { + callback("NoFeatures"); + return; + } + + callback(geoJson, file.name); + + }).catch((reason) => { + console.log(reason); + + // If reading as shapefile fails, try to read as GeoJSON. + // We won't check bounds because we assume GeoJSON being in WGS84. + let fileReader2 = new FileReader(); + fileReader2.addEventListener("load", (event) => { + let json; + + try { + json = JSON.parse(event.target.result); + } catch (e) { + console.log(e); + callback("InvalidFileType"); + return; + } + + callback(json, file.name); + }); + fileReader2.readAsText(file); + }); + }); + + try {fileReader.readAsArrayBuffer(file);} + catch (e) {} + }, + + /** + * Adds initial shapefile or GeoJSON file to the {@link L.ALS.SynthPolygonLayer} or {@link L.ALS.SynthLineLayer} and updates layer parameters + * + * @param synthLayer {L.ALS.SynthPolygonLayer|L.ALS.SynthLineLayer} Pass `this` here + * @param wizardResults Wizard results + */ + initializePolygonOrPolylineLayer: function (synthLayer, wizardResults) { + let groupToAdd, layerType, CastTo, + finishLoading = () => { + synthLayer.calculateParameters(); + synthLayer.isAfterDeserialization = false; + } + + if (synthLayer instanceof L.ALS.SynthPolygonLayer) { + groupToAdd = synthLayer.polygonGroup; + layerType = L.Polygon; + CastTo = L.Polygon; + } else { + groupToAdd = synthLayer.drawingGroup; + layerType = L.Polyline; + CastTo = L.Geodesic; + } + + this.getGeoJSON(wizardResults, geoJson => { + switch (geoJson) { + case "NoFileSelected": + finishLoading(); + return; + case "NoFeatures": + window.alert(L.ALS.locale.geometryNoFeatures); + finishLoading(); + return; + case "InvalidFileType": + window.alert(L.ALS.locale.geometryInvalidFile); + finishLoading(); + return; + } + + let layersAdded = false; + L.geoJson(geoJson, { + onEachFeature: (feature, layer) => { + if (!(layer instanceof layerType)) + return; + + groupToAdd.addLayer(new CastTo(layer.getLatLngs())); + layersAdded = true; + } + }); + + if (!layersAdded) + window.alert(L.ALS.locale.initialFeaturesNoFeatures); + + finishLoading(); + }); + } + } +}) \ No newline at end of file diff --git a/SynthGeometryLayer/SynthGeometryLayer.js b/SynthGeometryLayer/SynthGeometryLayer.js index 46ee81c3..889d09fd 100644 --- a/SynthGeometryLayer/SynthGeometryLayer.js +++ b/SynthGeometryLayer/SynthGeometryLayer.js @@ -1,6 +1,5 @@ require("./SynthGeometryWizard.js"); require("./SynthGeometrySettings.js"); -const shp = require("shpjs"); /** * Layer with geometry from shapefile or GeoJSON @@ -25,52 +24,25 @@ L.ALS.SynthGeometryLayer = L.ALS.Layer.extend( /** @lends L.ALS.SynthGeometryLay return; } - let file = wizardResults["geometryFileLabel"][0], fileReader = new FileReader(); - - if (!file) { - this._deleteInvalidLayer(L.ALS.locale.geometryNoFileSelected); - return; - } - - this.setName(file.name); - - // Try to read as shapefile - fileReader.addEventListener("load", (event) => { - this.isShapefile = true; // Will hide unneeded widgets using this - shp(event.target.result).then((geoJson) => { - if (geoJson.features.length === 0) { - this._deleteInvalidLayer(L.ALS.locale.geometryNoFeatures); - return; - } + L.ALS.SynthGeometryBaseWizard.getGeoJSON(wizardResults, (geoJson, name) => this._displayFile(geoJson, name)); + }, - this._displayFile(geoJson); - // Check if bounds are valid - let bounds = this._layer.getBounds(); - if (bounds._northEast.lng > 180 || bounds._northEast.lat > 90 || bounds._southWest.lng < -180 || bounds._southWest.lat < -90) - window.alert(L.ALS.locale.geometryOutOfBounds); - - }).catch((reason) => { - console.log(reason); - - // If reading as shapefile fails, try to read as GeoJSON. - // We won't check bounds because we assume GeoJSON being in WGS84. - let fileReader2 = new FileReader(); - fileReader2.addEventListener("load", (event) => { - try {this._displayFile(JSON.parse(event.target.result));} - catch (e) { - console.log(e); - this._deleteInvalidLayer(); - } - }); - fileReader2.readAsText(file); - }); - }); + _displayFile: function (geoJson, fileName) { + if (fileName) + this.setName(fileName); - try {fileReader.readAsArrayBuffer(file);} - catch (e) {} - }, + switch (geoJson) { + case "NoFileSelected": + this._deleteInvalidLayer(L.ALS.locale.geometryNoFileSelected); + return; + case "NoFeatures": + this._deleteInvalidLayer(L.ALS.locale.geometryNoFeatures); + return; + case "InvalidFileType": + this._deleteInvalidLayer(L.ALS.locale.geometryInvalidFile); + return; + } - _displayFile: function (geoJson) { let borderColor = new L.ALS.Widgets.Color("borderColor", "geometryBorderColor", this, "setColor").setValue(this.borderColor), fillColor = new L.ALS.Widgets.Color("fillColor", "geometryFillColor", this, "setColor").setValue(this.fillColor), menu = [borderColor, fillColor], @@ -130,6 +102,16 @@ L.ALS.SynthGeometryLayer = L.ALS.Layer.extend( /** @lends L.ALS.SynthGeometryLay } }); + // Check if bounds are valid + let bounds = this._layer.getBounds(); + if ( + bounds._northEast.lng > 180 || + bounds._northEast.lat > 90 || + bounds._southWest.lng < -180 || + bounds._southWest.lat < -90 + ) + window.alert(L.ALS.locale.geometryOutOfBounds); + if (L.ALS.searchWindow) L.ALS.searchWindow.addToSearch(this.id, docs, fields); // Add GeoJSON to search this.addLayers(this._layer); @@ -137,7 +119,7 @@ L.ALS.SynthGeometryLayer = L.ALS.Layer.extend( /** @lends L.ALS.SynthGeometryLay this.writeToHistory(); }, - _deleteInvalidLayer: function (message = L.ALS.locale.geometryInvalidFile) { + _deleteInvalidLayer: function (message) { window.alert(message); this.deleteLayer(); }, diff --git a/SynthGeometryLayer/SynthGeometryWizard.js b/SynthGeometryLayer/SynthGeometryWizard.js index c2ce8faf..5da6cd9d 100644 --- a/SynthGeometryLayer/SynthGeometryWizard.js +++ b/SynthGeometryLayer/SynthGeometryWizard.js @@ -1,20 +1,16 @@ /** - * Wizard for SynthShapefileLayer + * Wizard for SynthGeometryLayer * @class * @extends L.ALS.Wizard */ -L.ALS.SynthGeometryWizard = L.ALS.Wizard.extend( /** @lends L.ALS.SynthGeometryWizard.prototype */ { +L.ALS.SynthGeometryWizard = L.ALS.SynthGeometryBaseWizard.extend( /** @lends L.ALS.SynthGeometryWizard.prototype */ { displayName: "geometryDisplayName", initialize: function () { - L.ALS.Wizard.prototype.initialize.call(this); - if (!window.FileReader) { - this.addWidget(new L.ALS.Widgets.SimpleLabel("lbl", "geometryBrowserNotSupported", "center", "error")); - return; - } - this.addWidgets( - new L.ALS.Widgets.File("geometryFileLabel", "geometryFileLabel"), + L.ALS.SynthGeometryBaseWizard.prototype.initialize.call(this); + + this.addWidget( new L.ALS.Widgets.SimpleLabel("geometryNotification", "geometryNotification", "center", "message") ); } diff --git a/SynthLineLayer/SynthLineLayer.js b/SynthLineLayer/SynthLineLayer.js index 148f45e4..cf005af3 100644 --- a/SynthLineLayer/SynthLineLayer.js +++ b/SynthLineLayer/SynthLineLayer.js @@ -43,9 +43,7 @@ L.ALS.SynthLineLayer = L.ALS.SynthBaseLayer.extend(/** @lends L.ALS.SynthLineLay this.addBaseParametersOutputSection(); this.pointsGroup = L.featureGroup(); - this.calculateParameters(); - - this.isAfterDeserialization = false; + L.ALS.SynthGeometryBaseWizard.initializePolygonOrPolylineLayer(this, wizardResults); }, _hideCapturePoints: function (widget) { diff --git a/SynthLineLayer/SynthLineWizard.js b/SynthLineLayer/SynthLineWizard.js index 35e42a24..6fdc1ccb 100644 --- a/SynthLineLayer/SynthLineWizard.js +++ b/SynthLineLayer/SynthLineWizard.js @@ -1,3 +1,4 @@ -L.ALS.SynthLineWizard = L.ALS.EmptyWizard.extend({ +L.ALS.SynthLineWizard = L.ALS.SynthGeometryBaseWizard.extend({ displayName: "lineLayerName", + fileLabel: "initialFeaturesFileLabelLine", }); \ No newline at end of file diff --git a/SynthPolygonLayer/SynthPolygonLayer.js b/SynthPolygonLayer/SynthPolygonLayer.js index e47b86e0..2c2041b3 100644 --- a/SynthPolygonLayer/SynthPolygonLayer.js +++ b/SynthPolygonLayer/SynthPolygonLayer.js @@ -52,9 +52,8 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend({ this.calculateThreshold(settings); // Update hiding threshold this.onEditEndDebounced = debounce(() => this.onEditEnd(), 300); // Math operations are too slow for immediate update - this.calculateParameters(); - this.isAfterDeserialization = false; + L.ALS.SynthGeometryBaseWizard.initializePolygonOrPolylineLayer(this, wizardResults); }, onEditEnd: function () { @@ -149,6 +148,13 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend({ currentPath = [], currentConnections = [], currentLength = 0, currentPoints = [], shouldSwapPoints = false, lineAfterPolygonAdded = false; + // Precalculate paths' count and invalidate layer if this count is too high + if (MathTools.distanceBetweenPoints(...directionalLine) / this.By > 50) { + layersWereInvalidated = true; + this.invalidatePolygon(layer); + return; + } + // Move along the line by By until we reach the end of the polygon and add an additional line // or exceed the limit of 300 paths let deltaBy = 0; @@ -156,12 +162,6 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend({ if (lineAfterPolygonAdded) break; - if (deltaBy / this.By > 300) { - layersWereInvalidated = true; - this.invalidatePolygon(layer); - return; - } - // Scale the directional line, so endpoints of scaled line will be the start and end points of the // line that we'll build a perpendicular (a path) to. The distance between these points is By. let p1 = this.scaleLine(directionalLine, deltaBy)[1], @@ -265,7 +265,7 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend({ ...lineOptions, dashArray: this.dashedLine })); - let number = 0; + let number = 1; for (let path of shortestPath) { this.pathGroup.addLayer(path); diff --git a/SynthPolygonLayer/SynthPolygonWizard.js b/SynthPolygonLayer/SynthPolygonWizard.js index 4a113fc1..29f1657f 100644 --- a/SynthPolygonLayer/SynthPolygonWizard.js +++ b/SynthPolygonLayer/SynthPolygonWizard.js @@ -1,3 +1,4 @@ -L.ALS.SynthPolygonWizard = L.ALS.EmptyWizard.extend({ - displayName: "polygonLayerName" +L.ALS.SynthPolygonWizard = L.ALS.SynthGeometryBaseWizard.extend({ + displayName: "polygonLayerName", + fileLabel: "initialFeaturesFileLabelPolygon", }); \ No newline at end of file diff --git a/locales/English.js b/locales/English.js index 8c530514..24220453 100644 --- a/locales/English.js +++ b/locales/English.js @@ -31,7 +31,7 @@ L.ALS.Locales.addLocaleProperties("English", { gridStandardScales: "Grid scale:", gridLngDistance: "Distance between parallels:", gridLatDistance: "Distance between meridians:", - gridShouldMergeCells: "Merge couples of adjacent cells when latitude exceeds 60° and merge again when it exceeds 76° (except 1:1 000 000 and 1:2 000 scales when cells above 76° triple-merged instead of quadruple-merged)", + gridShouldMergeCells: "Merge adjacent cells when latitude exceeds 60°", // SynthGridLayer @@ -131,8 +131,8 @@ L.ALS.Locales.addLocaleProperties("English", { // SynthGeometryLayer geometryOutOfBounds: "Features in selected file are out of visible area. Please, check projection and/or add .prj file to the archive.", - geometryInvalidFile: "This file is not valid zipped shapefile or GeoJSON file", - geometryNoFeatures: "This file doesn't contain any features, so it won't be added", + geometryInvalidFile: "Selected file is not valid zipped shapefile or GeoJSON file", + geometryNoFeatures: "Selected file doesn't contain any features, so it won't be added", geometryBorderColor: "Border color:", geometryFillColor: "Fill color:", geometryBrowserNotSupported: "Your browser doesn't support adding this layer. You still can open projects with this layer though.", @@ -149,6 +149,11 @@ L.ALS.Locales.addLocaleProperties("English", { polygonHidePaths: "Hide paths", polygonLayersSkipped: "One or more polygons has been skipped because they're too big. These polygons have red color.", + // GeoJSON initial features + initialFeaturesFileLabelPolygon: "Load initial polygons from zipped shapefile or GeoJSON (non-polygon features will be skipped):", + initialFeaturesFileLabelLine: "Load initial polylines from zipped shapefile or GeoJSON (non-polyline features will be skipped):", + initialFeaturesNoFeatures: "Selected file doesn't contain any features supported by the added layer", + // Search searchButtonTitle: "Search Geometry Layers or OSM", searchPlaceholder: "Type to search...", diff --git a/locales/Russian.js b/locales/Russian.js index 05b0eb98..0015c891 100644 --- a/locales/Russian.js +++ b/locales/Russian.js @@ -32,7 +32,7 @@ L.ALS.Locales.addLocaleProperties("Русский", { gridStandardScales: "Масштаб сетки:", gridLngDistance: "Расстояние между параллелями:", gridLatDistance: "Расстояние между меридианами:", - gridShouldMergeCells: "Объединять пары соседних трапеций при широте выше 60° и снова объединять при широте выше 76° (кроме масштабов 1:1 000 000 и 1:2 000, при которых трапеции с широтой больше 76° объединяются по 3, а не по 4)", + gridShouldMergeCells: "Объединять соседние трапеции при широте выше 60°", // SynthGridLayer @@ -127,8 +127,8 @@ L.ALS.Locales.addLocaleProperties("Русский", { // SynthGeometryLayer geometryOutOfBounds: "Объекты в выбранном файле выходят за границы видимой области. Пожалуйста, проверьте проекцию и/или добавьте в архив файл .prj", - geometryInvalidFile: "Этот файл не является shapefile-ом или файлом GeoJSON", - geometryNoFeatures: "Этот файл не содержит объектов, поэтому не будет добавлен", + geometryInvalidFile: "Выбранный файл не является shapefile-ом или файлом GeoJSON", + geometryNoFeatures: "Выбранный файл не содержит объектов, поэтому не будет добавлен", geometryBorderColor: "Цвет обводки:", geometryFillColor: "Цвет заливки:", geometryBrowserNotSupported: "Ваш браузер не поддерживает добавление данного слоя, но вы можете открывать проекты, использующие этот слой.", @@ -145,6 +145,11 @@ L.ALS.Locales.addLocaleProperties("Русский", { polygonHidePaths: "Скрыть маршруты", polygonLayersSkipped: "Один или несколько полигонов были пропущены, так как они слишком большие. Данные полигоны имеют красный цвет.", + // GeoJSON initial features + initialFeaturesFileLabelPolygon: "Загрузить исходные полигоны из сжатого shapefile (zip-архива) или GeoJSON (типы, отличные от полигона, будут пропущены):", + initialFeaturesFileLabelLine: "Загрузить исходные полилинии из сжатого shapefile (zip-архива) или GeoJSON (типы, отличные от пололинии, будут пропущены):", + initialFeaturesNoFeatures: "Выбранный файл не содержит ни одного объекта, поддерживаемого добавляемым слоем", + // Search searchButtonTitle: "Поиск в Слоях Геометрии и OSM", searchPlaceholder: "Начните вводить для поиска...", diff --git a/main.js b/main.js index e72fab7e..fa8e9de8 100644 --- a/main.js +++ b/main.js @@ -14,7 +14,7 @@ window.L = require("leaflet"); * Segments number to use when displaying L.Geodesic * @type {number} */ -L.GEODESIC_SEGMENTS = 1000; +L.GEODESIC_SEGMENTS = 500; L.Geodesic = require("leaflet.geodesic").GeodesicLine; require("leaflet-draw"); @@ -24,6 +24,7 @@ L.ALS.Locales.AdditionalLocales.Russian(); require("./node_modules/leaflet.coordinates/dist/Leaflet.Coordinates-0.1.5.min.js"); require("./locales/English.js"); require("./locales/Russian.js"); +require("./SynthGeometryBaseWizard.js"); require("./SynthGeometryLayer/SynthGeometryLayer.js"); require("./SynthBaseLayer/SynthBaseLayer.js"); require("./SynthPolygonBaseLayer/SynthPolygonBaseLayer.js"); diff --git a/package-lock.json b/package-lock.json index 9b92c7a3..4ae077cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "license": "GPL-3.0-or-later", "dependencies": { "@electron/remote": "^2.0.1", - "leaflet-advanced-layer-system": "^2.2.1" + "leaflet-advanced-layer-system": "^2.2.2" }, "devDependencies": { "@babel/core": "^7.12.3", @@ -6061,9 +6061,9 @@ "dev": true }, "node_modules/leaflet-advanced-layer-system": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/leaflet-advanced-layer-system/-/leaflet-advanced-layer-system-2.2.1.tgz", - "integrity": "sha512-F010VJWIlrqoYZjXH5BtpKr5zVHA8sxchYnxGgN84xa/gCPrJ5RZx/6qHpsa7A6eLqsqXWFpezOvU5vY3Nw4ZQ==" + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/leaflet-advanced-layer-system/-/leaflet-advanced-layer-system-2.2.2.tgz", + "integrity": "sha512-GWi+31uWgzaGnmVd4oU17s87trLk+6oVV6rGLFJ/XvWpd8u+AbuHJXTA6PLhYWOzlPWWDWG1fORLb13tPDs+Ww==" }, "node_modules/leaflet-draw": { "version": "1.0.4", @@ -17611,9 +17611,9 @@ "dev": true }, "leaflet-advanced-layer-system": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/leaflet-advanced-layer-system/-/leaflet-advanced-layer-system-2.2.1.tgz", - "integrity": "sha512-F010VJWIlrqoYZjXH5BtpKr5zVHA8sxchYnxGgN84xa/gCPrJ5RZx/6qHpsa7A6eLqsqXWFpezOvU5vY3Nw4ZQ==" + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/leaflet-advanced-layer-system/-/leaflet-advanced-layer-system-2.2.2.tgz", + "integrity": "sha512-GWi+31uWgzaGnmVd4oU17s87trLk+6oVV6rGLFJ/XvWpd8u+AbuHJXTA6PLhYWOzlPWWDWG1fORLb13tPDs+Ww==" }, "leaflet-draw": { "version": "1.0.4", diff --git a/package.json b/package.json index 57e2c1af..fb960b5f 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,6 @@ }, "dependencies": { "@electron/remote": "^2.0.1", - "leaflet-advanced-layer-system": "^2.2.1" + "leaflet-advanced-layer-system": "^2.2.2" } } From f14cae2e01241307544bd9e1f60baeca36fc7129 Mon Sep 17 00:00:00 2001 From: matafokka Date: Tue, 10 May 2022 20:51:35 +0300 Subject: [PATCH 16/48] Bug fixes - Fixed notifications about skipped layer appearing on parameters change. Damn that was annoying. - Fixed points not drawing on split geodesic line. --- SynthGeometryBaseWizard.js | 22 ++++++++++++++++++---- SynthGeometryLayer/SynthGeometryLayer.js | 9 +-------- SynthLineLayer/SynthLineLayer.js | 20 ++++++++++++-------- SynthPolygonLayer/SynthPolygonLayer.js | 20 ++++++++++++-------- 4 files changed, 43 insertions(+), 28 deletions(-) diff --git a/SynthGeometryBaseWizard.js b/SynthGeometryBaseWizard.js index 20ca1989..16c113ea 100644 --- a/SynthGeometryBaseWizard.js +++ b/SynthGeometryBaseWizard.js @@ -89,7 +89,7 @@ L.ALS.SynthGeometryBaseWizard = L.ALS.Wizard.extend({ initializePolygonOrPolylineLayer: function (synthLayer, wizardResults) { let groupToAdd, layerType, CastTo, finishLoading = () => { - synthLayer.calculateParameters(); + synthLayer.calculateParameters(true); synthLayer.isAfterDeserialization = false; } @@ -119,7 +119,7 @@ L.ALS.SynthGeometryBaseWizard = L.ALS.Wizard.extend({ } let layersAdded = false; - L.geoJson(geoJson, { + let geoJsonLayer = L.geoJson(geoJson, { onEachFeature: (feature, layer) => { if (!(layer instanceof layerType)) return; @@ -129,11 +129,25 @@ L.ALS.SynthGeometryBaseWizard = L.ALS.Wizard.extend({ } }); - if (!layersAdded) + if(layersAdded) + this.checkGeoJSONBounds(geoJson); + else window.alert(L.ALS.locale.initialFeaturesNoFeatures); finishLoading(); }); - } + }, + + checkGeoJSONBounds: function (layer) { + // TODO: When wrapping will be done, expand checking to [-360; 360] range + let {_northEast, _southWest} = layer.getBounds(); + if ( + _northEast.lng > 180 || + _northEast.lat > 90 || + _southWest.lng < -180 || + _southWest.lat < -90 + ) + window.alert(L.ALS.locale.geometryOutOfBounds); + }, } }) \ No newline at end of file diff --git a/SynthGeometryLayer/SynthGeometryLayer.js b/SynthGeometryLayer/SynthGeometryLayer.js index 889d09fd..610d7073 100644 --- a/SynthGeometryLayer/SynthGeometryLayer.js +++ b/SynthGeometryLayer/SynthGeometryLayer.js @@ -103,14 +103,7 @@ L.ALS.SynthGeometryLayer = L.ALS.Layer.extend( /** @lends L.ALS.SynthGeometryLay }); // Check if bounds are valid - let bounds = this._layer.getBounds(); - if ( - bounds._northEast.lng > 180 || - bounds._northEast.lat > 90 || - bounds._southWest.lng < -180 || - bounds._southWest.lat < -90 - ) - window.alert(L.ALS.locale.geometryOutOfBounds); + L.ALS.SynthGeometryBaseWizard.checkGeoJSONBounds(this._layer); if (L.ALS.searchWindow) L.ALS.searchWindow.addToSearch(this.id, docs, fields); // Add GeoJSON to search diff --git a/SynthLineLayer/SynthLineLayer.js b/SynthLineLayer/SynthLineLayer.js index cf005af3..9ab9f341 100644 --- a/SynthLineLayer/SynthLineLayer.js +++ b/SynthLineLayer/SynthLineLayer.js @@ -64,7 +64,7 @@ L.ALS.SynthLineLayer = L.ALS.SynthBaseLayer.extend(/** @lends L.ALS.SynthLineLay this.map.addLayer(this.drawingGroup); }, - onEditEnd: function () { + onEditEnd: function (notifyIfLayersSkipped = true) { if (!this.isSelected) return; @@ -78,6 +78,8 @@ L.ALS.SynthLineLayer = L.ALS.SynthBaseLayer.extend(/** @lends L.ALS.SynthLineLay }, linesWereInvalidated = false; + notifyIfLayersSkipped = typeof notifyIfLayersSkipped === "boolean" ? notifyIfLayersSkipped : true; + this.drawingGroup.eachLayer((layer) => { let latLngs = layer.getLatLngs(); for (let i = 1; i < latLngs.length; i++) { @@ -103,12 +105,14 @@ L.ALS.SynthLineLayer = L.ALS.SynthBaseLayer.extend(/** @lends L.ALS.SynthLineLay this.pathsGroup.addLayer(extendedGeodesic); // Capture points made by constructing a line with segments number equal to the number of images - let points = new L.Geodesic(extendedGeodesic.getLatLngs(), { + let pointsArrays = new L.Geodesic(extendedGeodesic.getLatLngs(), { ...lineOptions, segmentsNumber: numberOfImages - }).getActualLatLngs()[0]; + }).getActualLatLngs(); - for (let point of points) - this.pointsGroup.addLayer(this.createCapturePoint(point, color)); + for (let array of pointsArrays) { + for (let point of array) + this.pointsGroup.addLayer(this.createCapturePoint(point, color)); + } } }); @@ -123,15 +127,15 @@ L.ALS.SynthLineLayer = L.ALS.SynthBaseLayer.extend(/** @lends L.ALS.SynthLineLay this.map.removeLayer(this.drawingGroup); this.map.addLayer(this.pathsGroup); - if (linesWereInvalidated) + if (linesWereInvalidated && notifyIfLayersSkipped) window.alert(L.ALS.locale.lineLayersSkipped); this.writeToHistoryDebounced(); }, - calculateParameters: function () { + calculateParameters: function (notifyIfLayersSkipped = false) { L.ALS.SynthBaseLayer.prototype.calculateParameters.call(this); - this.onEditEnd(); + this.onEditEnd(typeof notifyIfLayersSkipped === "boolean" ? notifyIfLayersSkipped : false); }, toGeoJSON: function () { diff --git a/SynthPolygonLayer/SynthPolygonLayer.js b/SynthPolygonLayer/SynthPolygonLayer.js index 2c2041b3..58b767bf 100644 --- a/SynthPolygonLayer/SynthPolygonLayer.js +++ b/SynthPolygonLayer/SynthPolygonLayer.js @@ -51,12 +51,12 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend({ }, this.polygonGroup); this.calculateThreshold(settings); // Update hiding threshold - this.onEditEndDebounced = debounce(() => this.onEditEnd(), 300); // Math operations are too slow for immediate update + this.onEditEndDebounced = debounce((notifyIfLayersSkipped = false) => this.onEditEnd(notifyIfLayersSkipped), 300); // Math operations are too slow for immediate update L.ALS.SynthGeometryBaseWizard.initializePolygonOrPolylineLayer(this, wizardResults); }, - onEditEnd: function () { + onEditEnd: function (notifyIfLayersSkipped = true) { if (!this.isSelected) return; @@ -75,6 +75,7 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend({ this.clearPaths(); let layersWereInvalidated = false; + notifyIfLayersSkipped = typeof notifyIfLayersSkipped === "boolean" ? notifyIfLayersSkipped : true; // Build paths for each polygon. @@ -243,9 +244,12 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend({ currentConnections.push(...path.getLatLngs()); // Fill in capture points. - let capturePoints = L.geodesic(path.getLatLngs(), {segmentsNumber: numberOfImages}).getActualLatLngs()[0]; - for (let point of capturePoints) - currentPoints.push(this.createCapturePoint(point, color)); + let pointsArrays = L.geodesic(path.getLatLngs(), {segmentsNumber: numberOfImages}).getActualLatLngs(); + + for (let array of pointsArrays) { + for (let point of array) + currentPoints.push(this.createCapturePoint(point, color)); + } deltaBy += this.By; } @@ -283,7 +287,7 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend({ this.pointsGroup.addLayer(marker); }); - if (layersWereInvalidated) + if (layersWereInvalidated && notifyIfLayersSkipped) window.alert(L.ALS.locale.polygonLayersSkipped); this.map.addLayer(this.labelsGroup); // Nothing in the base layer hides or shows it, so it's only hidden in code above @@ -292,9 +296,9 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend({ this.writeToHistoryDebounced(); }, - calculateParameters: function () { + calculateParameters: function (notifyIfLayersSkipped = false) { L.ALS.SynthPolygonBaseLayer.prototype.calculateParameters.call(this); - this.onEditEndDebounced(); + this.onEditEndDebounced(typeof notifyIfLayersSkipped === "boolean" ? notifyIfLayersSkipped : false); }, updateLayersVisibility: function () { From 397a36c11c0460944d53d8e06ea169a40110a189 Mon Sep 17 00:00:00 2001 From: matafokka Date: Wed, 11 May 2022 13:37:34 +0300 Subject: [PATCH 17/48] Removed leftover file from when I started working on Polygon Layer. How I event missed it? :D --- SynthPolygonLayer.js | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 SynthPolygonLayer.js diff --git a/SynthPolygonLayer.js b/SynthPolygonLayer.js deleted file mode 100644 index 0f686077..00000000 --- a/SynthPolygonLayer.js +++ /dev/null @@ -1,10 +0,0 @@ -L.ALS.SynthRectangleBaseLayer = L.ALS.SynthBaseDrawLayer.extend({ - defaultName: "Polygon Layer", - drawControls: { - polygon: { - shapeOptions: { - color: "#ff0000" - } - } - }, -}); \ No newline at end of file From 5a86b62f0d8849b001a60200e23d83fe9a11f267 Mon Sep 17 00:00:00 2001 From: matafokka Date: Wed, 11 May 2022 17:28:32 +0300 Subject: [PATCH 18/48] Made working near antimeridians less janky Now black overlay covers longitudes out of [-180; 180] range. The page body now is also black despite what theme is set. When editing starts, black overlay and notifications hides, but red datelines and map copies appear. I had to disable tile animations to remove some of overlay "glitches" which actually made map feel more snappy. The whole thing still feels janky, but there's no other way I can think of to make it better. --- css/styles.css | 12 ++++++++++ main.js | 64 ++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 72 insertions(+), 4 deletions(-) diff --git a/css/styles.css b/css/styles.css index 90cf8b2b..34952f5b 100644 --- a/css/styles.css +++ b/css/styles.css @@ -1,3 +1,15 @@ +#map { + background: black; +} + +.leaflet-pane.leaflet-mapLabels-pane { + z-index: 600; +} + +.leaflet-pane.leaflet-blackOverlay-pane { + z-index: 500; +} + .leaflet-control-coordinates .uiElement { font-size: 0.75rem !important; margin-top: 0; diff --git a/main.js b/main.js index fa8e9de8..cf98d1ee 100644 --- a/main.js +++ b/main.js @@ -60,11 +60,13 @@ let map = L.map("map", { preferCanvas: true, // Canvas is faster than SVG renderer keyboard: false, worldCopyJump: true, + fadeAnimation: false }).setView([51.505, -0.09], 13); map.doubleClickZoom.disable(); // Display a notification that users can move the map farther to jump to the other side of the world + let labelLayer = new L.ALS.LeafletLayers.LabelLayer(false), maxLabelWidth = 3, labelOpts = { maxWidth: 10, @@ -72,6 +74,10 @@ let labelLayer = new L.ALS.LeafletLayers.LabelLayer(false), maxLabelWidth = 3, }, westOpts = {origin: "rightCenter", ...labelOpts}, eastOpts = {origin: "leftCenter", ...labelOpts}; + +let labelsPaneElement = map.createPane("mapLabelsPane"); + +labelLayer.options.pane = "mapLabelsPane"; labelLayer.addTo(map); map.on("moveend zoomend resize", () => { @@ -92,7 +98,61 @@ map.on("moveend zoomend resize", () => { labelLayer.redraw(); }); +// Add black overlay to hide polygons when editing is not active + +L.BlackOverlayLayer = L.GridLayer.extend({ + createTile: function(coords) { + let tile = L.DomUtil.create("canvas", "leaflet-tile"), + {_northEast, _southWest} = this._tileCoordsToBounds(coords); + + if (_southWest.lng >= -180 && _northEast.lng <= 180) + return tile; + + let ctx = tile.getContext("2d"), + {x, y} = this.getTileSize(); + tile.width = x; + tile.height = y; + + ctx.fillStyle = "black"; + ctx.fillRect(0, 0, x, y); + + return tile; + } +}); + +map.createPane("blackOverlayPane"); + +let overlayLayer = new L.BlackOverlayLayer({ + noWrap: true, + pane: "blackOverlayPane", +}).addTo(map); + + +// When drawing starts, hide notifications and black overlay, but add red datelines + +let datelines = L.featureGroup(); +for (let sign of [1, -1]) { + let lng = 180 * sign; + datelines.addLayer(L.polyline([[90, lng], [-90, lng]], { + color: "red", + weight: 1, + })); +} + +map.on("draw:drawstart draw:editstart draw:deletestart", () => { + overlayLayer.setOpacity(0); + labelsPaneElement.style.opacity = "0"; + datelines.addTo(map); +}); + +map.on("draw:drawstop draw:editstop draw:deletestop", () => { + overlayLayer.setOpacity(1); + labelsPaneElement.style.opacity = "1"; + datelines.remove(); +}); + // Initialize layer system. Create and add base layers. + let layerSystem = new L.ALS.System(map, { aboutHTML: require("./about.js"), filePrefix: "SynthFlight", @@ -106,13 +166,11 @@ let layerSystem = new L.ALS.System(map, { // CartoDB layerSystem.addBaseLayer(L.tileLayer("http://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png", { maxZoom: 19, - noWrap: true, }), "CartoDB"); // OSM layerSystem.addBaseLayer(L.tileLayer("http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { maxZoom: 19, - noWrap: true, }), "Open Street Maps"); // Google maps @@ -120,7 +178,6 @@ let letters = [["m", "Streets"], ["s", "Satellite"], ["p", "Terrain"], ["s,h", " for (let letter of letters) { layerSystem.addBaseLayer(L.tileLayer("http://{s}.google.com/vt/lyrs=" + letter[0] + "&x={x}&y={y}&z={z}", { maxZoom: 20, - noWrap: true, subdomains: ['mt0', 'mt1', 'mt2', 'mt3'] }), "Google " + letter[1]); } @@ -132,7 +189,6 @@ for (let country of countries) { subdomains: ['01', '02', '03', '04'], reuseTiles: true, updateWhenIdle: false, - noWrap: true, }), "Yandex " + country[3] + country[4]); } From 104cd87552a51dae8ebd279bd23162359676c7f2 Mon Sep 17 00:00:00 2001 From: matafokka Date: Wed, 11 May 2022 17:54:46 +0300 Subject: [PATCH 19/48] Changed options of the black overlay layer --- main.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/main.js b/main.js index cf98d1ee..5460e2fc 100644 --- a/main.js +++ b/main.js @@ -125,9 +125,10 @@ map.createPane("blackOverlayPane"); let overlayLayer = new L.BlackOverlayLayer({ noWrap: true, pane: "blackOverlayPane", + updateWhenIdle: false, + updateWhenZooming: false, }).addTo(map); - // When drawing starts, hide notifications and black overlay, but add red datelines let datelines = L.featureGroup(); From 6fb8d99cf37ca73b554ef8e20a188989d33cd9e7 Mon Sep 17 00:00:00 2001 From: matafokka Date: Thu, 12 May 2022 20:28:44 +0300 Subject: [PATCH 20/48] Added dateline wrapping to the Rectangle Layer --- SynthPolygonLayer/SynthPolygonLayer.js | 3 +- SynthRectangleBaseLayer/drawPaths.js | 6 +-- SynthRectangleLayer/SynthRectangleLayer.js | 12 +++++ WrappedPolyline.js | 55 ++++++++++++++++++++++ main.js | 4 +- 5 files changed, 73 insertions(+), 7 deletions(-) create mode 100644 WrappedPolyline.js diff --git a/SynthPolygonLayer/SynthPolygonLayer.js b/SynthPolygonLayer/SynthPolygonLayer.js index 58b767bf..64c938a8 100644 --- a/SynthPolygonLayer/SynthPolygonLayer.js +++ b/SynthPolygonLayer/SynthPolygonLayer.js @@ -105,9 +105,8 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend({ } // Skip cloned layers - if (layer.isCloned) { + if (layer.isCloned) return; - } this.cloneLayerIfNeeded(layer); diff --git a/SynthRectangleBaseLayer/drawPaths.js b/SynthRectangleBaseLayer/drawPaths.js index 990910c3..775976b0 100644 --- a/SynthRectangleBaseLayer/drawPaths.js +++ b/SynthRectangleBaseLayer/drawPaths.js @@ -62,7 +62,7 @@ L.ALS.SynthRectangleBaseLayer.prototype.drawPathsWorker = function (isParallels) lat = startLat, lng = startLng, turfPolygonCoordinates = turfPolygon.geometry.coordinates[0], // MathTools accepts coordinates of the polygon, not polygon itself - number = 1, connectionLine = L.polyline([], connLineOptions), + number = 1, connectionLine = new L.WrappedPolyline([], connLineOptions), prevLine, shouldDraw = true; connectionLine.actualPaths = []; @@ -133,7 +133,7 @@ L.ALS.SynthRectangleBaseLayer.prototype.drawPathsWorker = function (isParallels) secondPoint = endPoint; } - let line = L.polyline([], lineOptions); // Contains paths with turns, i.e. internal connections + let line = new L.WrappedPolyline([], lineOptions); // Contains paths with turns, i.e. internal connections for (let point of [firstPoint, secondPoint]) { // Add points to the path @@ -147,7 +147,7 @@ L.ALS.SynthRectangleBaseLayer.prototype.drawPathsWorker = function (isParallels) let labelId = L.ALS.Helpers.generateID(); this.pathsLabelsIds.push(labelId); - this.labelsGroup.addLabel(labelId, coord, number, L.LabelLayer.DefaultDisplayOptions[isParallels ? "Message" : "Error"]); + this.labelsGroup.addLabel(labelId, L.latLng(coord).wrap(), number, L.LabelLayer.DefaultDisplayOptions[isParallels ? "Message" : "Error"]); number++; } diff --git a/SynthRectangleLayer/SynthRectangleLayer.js b/SynthRectangleLayer/SynthRectangleLayer.js index a9a7d8fa..d5e3c027 100644 --- a/SynthRectangleLayer/SynthRectangleLayer.js +++ b/SynthRectangleLayer/SynthRectangleLayer.js @@ -34,6 +34,18 @@ L.ALS.SynthRectangleLayer = L.ALS.SynthRectangleBaseLayer.extend({ let layersWereInvalidated = false; this.polygonGroup.eachLayer((layer) => { + // Remove a linked layer when a layer either original or cloned has been removed + if (layer.linkedLayer && !this.polygonGroup.hasLayer(layer.linkedLayer)) { + this.polygonGroup.removeLayer(layer); + return; + } + + // Skip cloned layers + if (layer.isCloned) + return; + + this.cloneLayerIfNeeded(layer); + // Limit polygon size by limiting total paths count let bounds = layer.getBounds(), topLeft = bounds.getNorthWest(), parallelsPathsCount = Math.ceil(this.getParallelOrMeridianLineLength(topLeft, bounds.getSouthWest()) / this.By) + 1, diff --git a/WrappedPolyline.js b/WrappedPolyline.js new file mode 100644 index 00000000..2e99a59a --- /dev/null +++ b/WrappedPolyline.js @@ -0,0 +1,55 @@ +/** + * A class for displaying paths by parallels and meridians. Shouldn't be using for anything else. + * + * @class + * @extends L.Polyline + */ +L.WrappedPolyline = L.Polyline.extend(/** @lends L.WrappedPolyline.prototype */{ + initialize: function (latlngs, options) { + L.Util.setOptions(this, options); + this.setLatLngs(latlngs); + }, + + setLatLngs: function (latlngs) { + this._originalLatLngs = []; + + if (latlngs.length === 0) { + L.Polyline.prototype.setLatLngs.call(this, this._originalLatLngs); + return this; + } + + let segments = [this._originalLatLngs, []], moveBy = 0, isFirstIteration = true; + + for (let segment of segments) { + for (let i = 0; i < latlngs.length; i++) { + let point = L.latLng(latlngs[i]).clone(); + + if (isFirstIteration && !moveBy) { + if (point.lng < -180) + moveBy = 360; + else if (point.lng > 180) + moveBy = -360; + } else if (!isFirstIteration) + point.lng += moveBy; + + segment.push(point); + } + isFirstIteration = false; + } + + L.Polyline.prototype.setLatLngs.call(this, moveBy === 0 ? this._originalLatLngs : segments); + return this; + }, + + addLatLng: function (latLng) { + return this.setLatLngs([...this._originalLatLngs, latLng]); + }, + + getLatLngs: function () { + return this._originalLatLngs; + }, + + toGeoJSON: function (precision) { + return new L.Polyline(this._originalLatLngs).toGeoJSON(precision); + } +}); \ No newline at end of file diff --git a/main.js b/main.js index 5460e2fc..4ee2abf0 100644 --- a/main.js +++ b/main.js @@ -17,6 +17,7 @@ window.L = require("leaflet"); L.GEODESIC_SEGMENTS = 500; L.Geodesic = require("leaflet.geodesic").GeodesicLine; +require("./WrappedPolyline.js"); require("leaflet-draw"); require("./DrawGeodesic.js"); require("leaflet-advanced-layer-system"); @@ -132,8 +133,7 @@ let overlayLayer = new L.BlackOverlayLayer({ // When drawing starts, hide notifications and black overlay, but add red datelines let datelines = L.featureGroup(); -for (let sign of [1, -1]) { - let lng = 180 * sign; +for (let lng of [180, -180]) { datelines.addLayer(L.polyline([[90, lng], [-90, lng]], { color: "red", weight: 1, From 7bbf5f4c1a6de33dfd9dd8b4339a636fc2edbb7d Mon Sep 17 00:00:00 2001 From: matafokka Date: Fri, 13 May 2022 17:14:20 +0300 Subject: [PATCH 21/48] Now distances in Grid Layer must divide Earth into the whole number of segments. If user's distances doesn't do so, a prompt to use corrected distances is displayed. If user refuses to use suggested distances, layer is deleted. Also bumped ALS version which allows for safe layer deletion. --- SynthGridLayer/SynthGridLayer.js | 37 ++++++++++++++++++- .../SynthPolygonBaseLayer.js | 3 +- .../SynthRectangleBaseLayer.js | 4 +- locales/English.js | 10 +++++ locales/Russian.js | 10 +++++ package-lock.json | 14 +++---- package.json | 2 +- 7 files changed, 67 insertions(+), 13 deletions(-) diff --git a/SynthGridLayer/SynthGridLayer.js b/SynthGridLayer/SynthGridLayer.js index 194d362e..5cd5b34b 100644 --- a/SynthGridLayer/SynthGridLayer.js +++ b/SynthGridLayer/SynthGridLayer.js @@ -1,11 +1,12 @@ require("./SynthGridSettings.js"); require("./SynthGridWizard.js"); +const MathTools = require("../MathTools.js"); const turfHelpers = require("@turf/helpers"); /** * Layer that allows users to plan aerial photography using grid * @class - * @extends L.ALS.Layer + * @extends L.ALS.SynthRectangleBaseLayer */ L.ALS.SynthGridLayer = L.ALS.SynthRectangleBaseLayer.extend(/** @lends L.ALS.SynthGridLayer.prototype */{ @@ -13,8 +14,40 @@ L.ALS.SynthGridLayer = L.ALS.SynthRectangleBaseLayer.extend(/** @lends L.ALS.Syn useZoneNumbers: true, borderColorLabel: "gridBorderColor", fillColorLabel: "gridFillColor", + writeToHistoryOnInit: false, - init: function (wizardResults, settings) { + init: function (wizardResults, settings, fn) { + // Verify that distances split map into whole number of segments. If not, ask user if they want to use corrected + // distances. + + // Distances don't divide Earth into the whole number of segments. + // Would you like to use for parallels and for meridians? + let confirmText = L.ALS.locale.gridCorrectDistancesMain1 + "\n\n" + L.ALS.locale.gridCorrectDistancesMain2, + shouldAlert = false; + + for (let name of ["Lat", "Lng"]) { + let wizardDistanceName = `grid${name}Distance`, + userDistance = parseFloat(wizardResults[wizardDistanceName]), + correctedDistance = 360 / Math.round(360 / userDistance); + + if (MathTools.isEqual((360 / userDistance) % 1, 0)) + continue; + + shouldAlert = true; + wizardResults[wizardDistanceName] = correctedDistance; + confirmText += ` ${correctedDistance}° ` + + L.ALS.locale[`gridCorrectDistances${name}`] + ` ${L.ALS.locale.gridCorrectDistancesAnd}`; + } + + if (shouldAlert) { + confirmText = confirmText.substring(0, confirmText.lastIndexOf(" " + L.ALS.locale.gridCorrectDistancesAnd)) + "?" + + "\n\n" + L.ALS.locale.gridCorrectDistancesMain3; + + if (!window.confirm(confirmText)) { + fn(); + return; + } + } /** * Contains polygons' names' IDs diff --git a/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js b/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js index fa0f00ba..77475f7a 100644 --- a/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js +++ b/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js @@ -156,7 +156,8 @@ L.ALS.SynthPolygonBaseLayer = L.ALS.SynthBaseLayer.extend( /** @lends L.ALS.Synt }, onDelete: function () { - this.onHide(); + if (!this.creationCancelled) + this.onHide(); }, updateLayersVisibility: function () { diff --git a/SynthRectangleBaseLayer/SynthRectangleBaseLayer.js b/SynthRectangleBaseLayer/SynthRectangleBaseLayer.js index 43de56ec..1d4c8e69 100644 --- a/SynthRectangleBaseLayer/SynthRectangleBaseLayer.js +++ b/SynthRectangleBaseLayer/SynthRectangleBaseLayer.js @@ -63,8 +63,8 @@ L.ALS.SynthRectangleBaseLayer = L.ALS.SynthPolygonBaseLayer.extend( /** @lends L */ this.pathsLabelsIds = []; - this.lngDistance = parseFloat(wizardResults["gridLngDistance"]); - this.latDistance = parseFloat(wizardResults["gridLatDistance"]); + this.lngDistance = parseFloat(wizardResults.gridLngDistance); + this.latDistance = parseFloat(wizardResults.gridLatDistance); // Determine whether this grid uses standard scale or not let scale = wizardResults.gridStandardScales; diff --git a/locales/English.js b/locales/English.js index 24220453..2eea647c 100644 --- a/locales/English.js +++ b/locales/English.js @@ -35,6 +35,16 @@ L.ALS.Locales.addLocaleProperties("English", { // SynthGridLayer + // Confirmation text when distances don't divide map into the whole number of cells + gridCorrectDistancesMain1: "Distances don't divide Earth into the whole number of segments.", + gridCorrectDistancesMain2: "Would you like to use distance in", + gridCorrectDistancesLat: "for parallels", + gridCorrectDistancesAnd: "and", + gridCorrectDistancesLng: "for meridians", + gridCorrectDistancesMain3: "Click \"OK\" to use the suggested distances. Click \"Cancel\" to cancel adding this layer.", + + // Main stuff + alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZ", // Don't add it, if you don't need different symbols in cells' names gridLayerDefaultName: "Grid Layer", hidePolygonWidgets: "Hide widgets on the map", diff --git a/locales/Russian.js b/locales/Russian.js index 0015c891..4868894b 100644 --- a/locales/Russian.js +++ b/locales/Russian.js @@ -36,6 +36,16 @@ L.ALS.Locales.addLocaleProperties("Русский", { // SynthGridLayer + // Confirmation text when distances don't divide map into the whole number of cells + gridCorrectDistancesMain1: "Расстояния не разделяют Землю на равное число сегментов.", + gridCorrectDistancesMain2: "Хотите ли вы использовать расстояние в", + gridCorrectDistancesLat: "для параллелей", + gridCorrectDistancesAnd: "и", + gridCorrectDistancesLng: "для меридианов", + gridCorrectDistancesMain3: "Нажмите \"Ок\", чтобы использовать предложенные расстояния. Нажмите \"Отмена\", чтобы отменить создание данного слоя.", + + // Main stuff + alphabet: "АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ", // Don't add it, if you don't need different symbols in cells' names gridLayerDefaultName: "Слой Сетки", hidePolygonWidgets: "Скрыть виджеты на карте", diff --git a/package-lock.json b/package-lock.json index 4ae077cc..16dbb3fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "license": "GPL-3.0-or-later", "dependencies": { "@electron/remote": "^2.0.1", - "leaflet-advanced-layer-system": "^2.2.2" + "leaflet-advanced-layer-system": "^2.2.3" }, "devDependencies": { "@babel/core": "^7.12.3", @@ -6061,9 +6061,9 @@ "dev": true }, "node_modules/leaflet-advanced-layer-system": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/leaflet-advanced-layer-system/-/leaflet-advanced-layer-system-2.2.2.tgz", - "integrity": "sha512-GWi+31uWgzaGnmVd4oU17s87trLk+6oVV6rGLFJ/XvWpd8u+AbuHJXTA6PLhYWOzlPWWDWG1fORLb13tPDs+Ww==" + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/leaflet-advanced-layer-system/-/leaflet-advanced-layer-system-2.2.3.tgz", + "integrity": "sha512-8hdbqkYyJyPVP+CN2rfNuY4OPzhu186fntgLXo2pXRjFoSgEAVfmQt7IRHPqJSSfQDbRJe2HiomPEb0bU2IVhg==" }, "node_modules/leaflet-draw": { "version": "1.0.4", @@ -17611,9 +17611,9 @@ "dev": true }, "leaflet-advanced-layer-system": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/leaflet-advanced-layer-system/-/leaflet-advanced-layer-system-2.2.2.tgz", - "integrity": "sha512-GWi+31uWgzaGnmVd4oU17s87trLk+6oVV6rGLFJ/XvWpd8u+AbuHJXTA6PLhYWOzlPWWDWG1fORLb13tPDs+Ww==" + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/leaflet-advanced-layer-system/-/leaflet-advanced-layer-system-2.2.3.tgz", + "integrity": "sha512-8hdbqkYyJyPVP+CN2rfNuY4OPzhu186fntgLXo2pXRjFoSgEAVfmQt7IRHPqJSSfQDbRJe2HiomPEb0bU2IVhg==" }, "leaflet-draw": { "version": "1.0.4", diff --git a/package.json b/package.json index fb960b5f..8d52a65a 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,6 @@ }, "dependencies": { "@electron/remote": "^2.0.1", - "leaflet-advanced-layer-system": "^2.2.2" + "leaflet-advanced-layer-system": "^2.2.3" } } From 495d387b6deabab699f0846e1b2c11031591522e Mon Sep 17 00:00:00 2001 From: matafokka Date: Fri, 13 May 2022 17:19:01 +0300 Subject: [PATCH 22/48] Fixed paths' count estimation in Grid Layer not working --- SynthGridLayer/SynthGridLayer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SynthGridLayer/SynthGridLayer.js b/SynthGridLayer/SynthGridLayer.js index 5cd5b34b..60926d42 100644 --- a/SynthGridLayer/SynthGridLayer.js +++ b/SynthGridLayer/SynthGridLayer.js @@ -109,8 +109,8 @@ L.ALS.SynthGridLayer = L.ALS.SynthRectangleBaseLayer.extend(/** @lends L.ALS.Syn // We'll check if it's too small (in near-polar regions, there'll be only one path when value is 2) or too big. let errorLabel = this.getWidgetById("calculateParametersError"), - parallelsPathsCount = this.estimatePathsCount(this.lngPathsCount), - meridiansPathsCount = this.estimatePathsCount(this.latPathsCount); + parallelsPathsCount = this.estimatePathsCount(this.lngDistance), + meridiansPathsCount = this.estimatePathsCount(this.latDistance); if (parallelsPathsCount === undefined) { errorLabel.setValue("errorDistanceHasNotBeenCalculated"); From bac37522ab1406f29d8f6d8f7e4782c3888c107d Mon Sep 17 00:00:00 2001 From: matafokka Date: Fri, 13 May 2022 17:23:30 +0300 Subject: [PATCH 23/48] Made deleting Geometry Layer a bit safer. There still might be race conditions but they won't affect anything and I'm too lazy to deal with them. --- SynthGeometryLayer/SynthGeometryLayer.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/SynthGeometryLayer/SynthGeometryLayer.js b/SynthGeometryLayer/SynthGeometryLayer.js index 610d7073..b9397c05 100644 --- a/SynthGeometryLayer/SynthGeometryLayer.js +++ b/SynthGeometryLayer/SynthGeometryLayer.js @@ -12,7 +12,7 @@ L.ALS.SynthGeometryLayer = L.ALS.Layer.extend( /** @lends L.ALS.SynthGeometryLay isShapeFile: false, writeToHistoryOnInit: false, - init: function (wizardResults, settings) { + init: function (wizardResults, settings, cancelCreation) { this.copySettingsToThis(settings); this.setConstructorArguments(["deserialized"]); @@ -20,7 +20,8 @@ L.ALS.SynthGeometryLayer = L.ALS.Layer.extend( /** @lends L.ALS.SynthGeometryLay return; if (!window.FileReader) { - this._deleteInvalidLayer(L.ALS.locale.geometryBrowserNotSupported); + window.alert(L.ALS.locale.geometryBrowserNotSupported); + cancelCreation(); return; } From 00b06ea1fab71a0c4f40ecb4ffcf97e06573ef35 Mon Sep 17 00:00:00 2001 From: matafokka Date: Fri, 13 May 2022 17:24:57 +0300 Subject: [PATCH 24/48] Moved Polygon Layer down in list before I forget :p --- main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.js b/main.js index 4ee2abf0..b2aeaa0b 100644 --- a/main.js +++ b/main.js @@ -197,9 +197,9 @@ for (let country of countries) { layerSystem.addBaseLayer(L.tileLayer(""), "Empty"); // Add layer types -layerSystem.addLayerType(L.ALS.SynthPolygonLayer); layerSystem.addLayerType(L.ALS.SynthGridLayer); layerSystem.addLayerType(L.ALS.SynthRectangleLayer); +layerSystem.addLayerType(L.ALS.SynthPolygonLayer); layerSystem.addLayerType(L.ALS.SynthLineLayer); layerSystem.addLayerType(L.ALS.SynthGeometryLayer); From ba0ee8d6ed06db273bf5ba6e161268418a02ba46 Mon Sep 17 00:00:00 2001 From: matafokka Date: Fri, 13 May 2022 19:50:06 +0300 Subject: [PATCH 25/48] Added wrapping to the Geometry Layer And also expanded bounds checking to [-360; 360] when loading GeoJSON --- SynthGeometryBaseWizard.js | 12 ++-- SynthGeometryLayer/SynthGeometryLayer.js | 78 ++++++++++++++++++++---- 2 files changed, 71 insertions(+), 19 deletions(-) diff --git a/SynthGeometryBaseWizard.js b/SynthGeometryBaseWizard.js index 16c113ea..6d79fc71 100644 --- a/SynthGeometryBaseWizard.js +++ b/SynthGeometryBaseWizard.js @@ -24,6 +24,7 @@ L.ALS.SynthGeometryBaseWizard = L.ALS.Wizard.extend({ * @callback getGeoJSONCallback * @param {Object|"NoFileSelected"|"NoFeatures"|"InvalidFileType"} geoJson GeoJSON or an error message * @param {string|undefined} [fileName=undefined] Name of the loaded file + * @param {boolean|undefined} [isShapefile=undefined] If selected file is shapefile */ /** @@ -51,7 +52,7 @@ L.ALS.SynthGeometryBaseWizard = L.ALS.Wizard.extend({ return; } - callback(geoJson, file.name); + callback(geoJson, file.name, true); }).catch((reason) => { console.log(reason); @@ -70,7 +71,7 @@ L.ALS.SynthGeometryBaseWizard = L.ALS.Wizard.extend({ return; } - callback(json, file.name); + callback(json, file.name, false); }); fileReader2.readAsText(file); }); @@ -119,7 +120,7 @@ L.ALS.SynthGeometryBaseWizard = L.ALS.Wizard.extend({ } let layersAdded = false; - let geoJsonLayer = L.geoJson(geoJson, { + L.geoJson(geoJson, { onEachFeature: (feature, layer) => { if (!(layer instanceof layerType)) return; @@ -139,12 +140,11 @@ L.ALS.SynthGeometryBaseWizard = L.ALS.Wizard.extend({ }, checkGeoJSONBounds: function (layer) { - // TODO: When wrapping will be done, expand checking to [-360; 360] range let {_northEast, _southWest} = layer.getBounds(); if ( - _northEast.lng > 180 || + _northEast.lng > 360 || _northEast.lat > 90 || - _southWest.lng < -180 || + _southWest.lng < -360 || _southWest.lat < -90 ) window.alert(L.ALS.locale.geometryOutOfBounds); diff --git a/SynthGeometryLayer/SynthGeometryLayer.js b/SynthGeometryLayer/SynthGeometryLayer.js index b9397c05..1da76987 100644 --- a/SynthGeometryLayer/SynthGeometryLayer.js +++ b/SynthGeometryLayer/SynthGeometryLayer.js @@ -28,7 +28,7 @@ L.ALS.SynthGeometryLayer = L.ALS.Layer.extend( /** @lends L.ALS.SynthGeometryLay L.ALS.SynthGeometryBaseWizard.getGeoJSON(wizardResults, (geoJson, name) => this._displayFile(geoJson, name)); }, - _displayFile: function (geoJson, fileName) { + _displayFile: function (geoJson, fileName, isShapefile) { if (fileName) this.setName(fileName); @@ -44,6 +44,8 @@ L.ALS.SynthGeometryLayer = L.ALS.Layer.extend( /** @lends L.ALS.SynthGeometryLay return; } + this.originalGeoJson = geoJson; + let borderColor = new L.ALS.Widgets.Color("borderColor", "geometryBorderColor", this, "setColor").setValue(this.borderColor), fillColor = new L.ALS.Widgets.Color("fillColor", "geometryFillColor", this, "setColor").setValue(this.fillColor), menu = [borderColor, fillColor], @@ -52,6 +54,8 @@ L.ALS.SynthGeometryLayer = L.ALS.Layer.extend( /** @lends L.ALS.SynthGeometryLay maxHeight: 500, }; + this.isShapefile = this.isShapefile || isShapefile; + if (this.isShapefile) { let type = geoJson.features[0].geometry.type; if (type === "LineString") @@ -63,21 +67,60 @@ L.ALS.SynthGeometryLayer = L.ALS.Layer.extend( /** @lends L.ALS.SynthGeometryLay for (let widget of menu) this.addWidget(widget); - let docs = [], fields = []; // Search documents + let docs = [], fields = [], clonedLayers = []; // Search documents this._layer = L.geoJSON(geoJson, { onEachFeature: (feature, layer) => { - let popup = "", doc = {}; + let popup = "", doc = {}, bbox; // Calculate bbox for zooming - if (!feature.geometry.bbox) { - if (layer.getBounds) { - let bounds = layer.getBounds(); - feature.geometry.bbox = [bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()]; - } else { - let latLng = layer.getLatLng(), size = 0.008; - feature.geometry.bbox = [latLng.lng - size, latLng.lat - size, latLng.lng + size, latLng.lat + size]; + if (layer.getBounds) { + let bounds = layer.getBounds(), west = bounds.getWest(), east = bounds.getEast(); + bbox = [west, bounds.getSouth(), east, bounds.getNorth()]; + + // Check if layer crosses one of antimeridians + let moveBy = 0, crossesLeft = west < -180, crossesRight = east > 180; + + if (crossesLeft && crossesRight) + moveBy = 0; + else if (crossesLeft) + moveBy = 360; + else if (crossesRight) + moveBy = -360; + + // Clone layer + if (moveBy) { + // Move bbox + for (let i = 0; i <= 2; i += 2) + bbox += moveBy; + + // Clone coordinates + let latLngs = layer.getLatLngs(), clonedLatLngs = []; + + if (latLngs.length === 0 || latLngs[0] instanceof L.LatLng) + latLngs = [latLngs]; + + for (let array of latLngs) { + let clonedArray = []; + console.log(array) + for (let coord of array) { + let clonedCoord = coord.clone(); + clonedCoord.lng += moveBy; + clonedArray.push(clonedCoord); + } + clonedLatLngs.push(clonedArray); + } + + // Create cloned layer + let clonedLayer = layer instanceof L.Polygon ? new L.Polygon(clonedLatLngs) : + new L.Polyline(clonedLatLngs); + layer.clone = clonedLayer; + clonedLayers.push(clonedLayer); } + } else { + let latLng = layer.getLatLng().wrap(), size = 0.008; + layer.setLatLng(latLng); // Wrap points + bbox = [latLng.lng - size, latLng.lat - size, latLng.lng + size, latLng.lat + size]; } // Copy properties to the popup and search doc @@ -94,15 +137,24 @@ L.ALS.SynthGeometryLayer = L.ALS.Layer.extend( /** @lends L.ALS.SynthGeometryLay fields.push(name); } - layer.bindPopup(`
${popup}
`, popupOptions); + if (!popup) + popup = "
No data
"; + + for (let lyr of [layer, layer.clone]) { + if (lyr) + lyr.bindPopup(`
${popup}
`, popupOptions); + } doc._miniSearchId = L.ALS.Helpers.generateID(); - doc.bbox = feature.geometry.bbox; + doc.bbox = bbox; doc.properties = feature.properties; docs.push(doc); } }); + for (let layer of clonedLayers) + this._layer.addLayer(layer); + // Check if bounds are valid L.ALS.SynthGeometryBaseWizard.checkGeoJSONBounds(this._layer); @@ -150,7 +202,7 @@ L.ALS.SynthGeometryLayer = L.ALS.Layer.extend( /** @lends L.ALS.SynthGeometryLay let json = { widgets: this.serializeWidgets(seenObjects), name: this.getName(), - geoJson: this._layer.toGeoJSON(), + geoJson: this.originalGeoJson, serializationID: this.serializationID }; From d8c38b2f99f4f5defb177121fa67bb11ab14a8ce Mon Sep 17 00:00:00 2001 From: matafokka Date: Fri, 13 May 2022 19:50:57 +0300 Subject: [PATCH 26/48] Removed console.log(), haha --- SynthGeometryLayer/SynthGeometryLayer.js | 1 - 1 file changed, 1 deletion(-) diff --git a/SynthGeometryLayer/SynthGeometryLayer.js b/SynthGeometryLayer/SynthGeometryLayer.js index 1da76987..77388ca3 100644 --- a/SynthGeometryLayer/SynthGeometryLayer.js +++ b/SynthGeometryLayer/SynthGeometryLayer.js @@ -102,7 +102,6 @@ L.ALS.SynthGeometryLayer = L.ALS.Layer.extend( /** @lends L.ALS.SynthGeometryLay for (let array of latLngs) { let clonedArray = []; - console.log(array) for (let coord of array) { let clonedCoord = coord.clone(); clonedCoord.lng += moveBy; From b7fe740fcac1d94f4786c0b3f164592ffa34b641 Mon Sep 17 00:00:00 2001 From: matafokka Date: Fri, 13 May 2022 20:12:17 +0300 Subject: [PATCH 27/48] Aligned flight time warning to the left --- SynthBaseLayer/SynthBaseLayer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SynthBaseLayer/SynthBaseLayer.js b/SynthBaseLayer/SynthBaseLayer.js index c47338c2..b2bbb3f1 100644 --- a/SynthBaseLayer/SynthBaseLayer.js +++ b/SynthBaseLayer/SynthBaseLayer.js @@ -342,7 +342,7 @@ L.ALS.SynthBaseLayer = L.ALS.Layer.extend(/** @lends L.ALS.SynthBaseLayer.protot button = new L.ALS.Widgets.Button("flashPath" + id, "flashPath", this, "flashPath"), lengthWidget = new L.ALS.Widgets.ValueLabel("pathLength" + id, "pathLength", "m").setFormatNumbers(true).setNumberOfDigitsAfterPoint(0), timeWidget = new L.ALS.Widgets.ValueLabel("flightTime" + id, "flightTime", "h:mm"), - warning = new L.ALS.Widgets.SimpleLabel("warning" + id, "", "center", "warning"); + warning = new L.ALS.Widgets.SimpleLabel("warning" + id, "", "left", "warning"); layer.updateWidgets = (length) => { lengthWidget.setValue(length); From 5411ea808baa7cb00a2436602ec1e30c746e4a37 Mon Sep 17 00:00:00 2001 From: matafokka Date: Sat, 14 May 2022 13:09:30 +0300 Subject: [PATCH 28/48] Fixed typos, added link to the upcoming wiki page --- about.js | 12 +++++++----- locales/English.js | 19 ++++++++++--------- locales/Russian.js | 19 ++++++++++--------- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/about.js b/about.js index 54f18dff..70319894 100644 --- a/about.js +++ b/about.js @@ -46,13 +46,15 @@ module.exports = `

LogoSynthFlight ${version}

-

+

-

+

-

+

-

+

+ +

-

Nominatim API

+

Nominatim API

`; \ No newline at end of file diff --git a/locales/English.js b/locales/English.js index 2eea647c..7e978a82 100644 --- a/locales/English.js +++ b/locales/English.js @@ -26,7 +26,7 @@ L.ALS.Locales.addLocaleProperties("English", { gridWizardDisplayName: "Grid Layer", gridWizardNotification: `If map scale is too low, grid will be hidden. Please, zoom in to see it. - To select a polygon, either click right mouse button (or tap and hold) or double-click (or double-tap) on it.`, + To select a polygon, either click right mouse button (or tap and hold on sensor display) or double-click (or double-tap) on it.`, gridStandardScales: "Grid scale:", gridLngDistance: "Distance between parallels:", @@ -184,16 +184,17 @@ L.ALS.Locales.addLocaleProperties("English", { // About - firstParagraph: "SynthFlight is a fully client-side software for planning aerial photography. This is a beta version so bugs, huge API changes and lack of backwards compatibility are to be expected.", + about1: "SynthFlight is a fully client-side software for planning aerial photography.", - secondParagraphPart1: "Visit project's", - secondParagraphPart2: "GitHub page", - secondParagraphPart3: "for more information.", + about2Part1: "Visit project's", + about2Part2: "GitHub page", + about2Part3: "for more information.", - thirdParagraph: "Developing SynthFlight is possible thanks to various open-source software.", + about3Part1: "User guide is located in", + about3Part2: "SynthFlight Wiki.", - fourthParagraph: "Using maps is possible thanks to following geoservices:", - - fifthParagraph: "Web search is powered by OpenStreetMaps and", // ... Nominatim API + about4: "Developing SynthFlight is possible thanks to various open-source software.", + about5: "Using maps is possible thanks to following geoservices:", + about6: "Web search is powered by OpenStreetMaps and", // ... Nominatim API }); \ No newline at end of file diff --git a/locales/Russian.js b/locales/Russian.js index 4868894b..92d9a60b 100644 --- a/locales/Russian.js +++ b/locales/Russian.js @@ -27,7 +27,7 @@ L.ALS.Locales.addLocaleProperties("Русский", { gridWizardDisplayName: "Слой Сетки", gridWizardNotification: `Если масштаб карты слишком мелкий, сетка будет скрыта. Пожалуйста, увеличьте масштаб карты, чтобы ее увидеть. - Чтобы выделить трапецию, либо нажмите на него правой кнопкой мыши (или тапните и задержите палец) или два раза кликните (тапните) на него.`, + Чтобы выделить трапецию, или нажмите на него правой кнопкой мыши (или задержите палец на сенсорном экране), или два раза кликните (тапните) на него.`, gridStandardScales: "Масштаб сетки:", gridLngDistance: "Расстояние между параллелями:", @@ -180,16 +180,17 @@ L.ALS.Locales.addLocaleProperties("Русский", { // About - firstParagraph: "SynthFlight – это полностью клиентское программное обеспечение для проектирования аэрофотосъемочных работ. Это beta-версия, поэтому ожидаемы баги, большие изменения API, отсутствие обратной совместимости и т.д.", + about1: "SynthFlight – это полностью клиентское программное обеспечение для проектирования аэрофотосъемочных работ.", - secondParagraphPart1: "Посетите", - secondParagraphPart2: "страницу проекта на GitHub", - secondParagraphPart3: "для дополнительной информации (на английском языке).", + about2Part1: "Посетите", + about2Part2: "страницу проекта на GitHub", + about2Part3: "для дополнительной информации (на английском языке).", - thirdParagraph: "Разработка SynthFlight возможна, благодаря различному свободному ПО.", + about3Part1: "Руководство пользователя на английском языке находится на", + about3Part2: "странице SynthFlight Wiki.", - fourthParagraph: "Использование карт возможно, благодаря следующим геосервисам:", - - fifthParagraph: "Поиск по Интернету осуществляется через OpenStreetMaps при помощи", // ... Nominatim API + about4: "Разработка SynthFlight возможна, благодаря различному свободному ПО.", + about5: "Использование карт возможно, благодаря следующим геосервисам:", + about6: "Поиск по Интернету осуществляется через OpenStreetMaps при помощи", // ... Nominatim API }); \ No newline at end of file From 04b9a181d561f9350e911714abedd268cc8d3da0 Mon Sep 17 00:00:00 2001 From: matafokka Date: Sat, 14 May 2022 14:15:30 +0300 Subject: [PATCH 29/48] Better error reporting when loading DEM files --- SynthPolygonBaseLayer/DEM.js | 95 ++++++++++++++++++++++++++---------- locales/English.js | 3 +- locales/Russian.js | 3 +- 3 files changed, 72 insertions(+), 29 deletions(-) diff --git a/SynthPolygonBaseLayer/DEM.js b/SynthPolygonBaseLayer/DEM.js index 72c3dc64..3271e687 100644 --- a/SynthPolygonBaseLayer/DEM.js +++ b/SynthPolygonBaseLayer/DEM.js @@ -2,6 +2,7 @@ const ESRIGridParser = require("../ESRIGridParser.js"); const ESRIGridParserWorker = require("../ESRIGridParserWorker.js"); +const proj4 = require("proj4"); let GeoTIFFParser; try { GeoTIFFParser = require("../GeoTIFFParser.js"); @@ -26,7 +27,12 @@ L.ALS.SynthPolygonBaseLayer.prototype.onDEMLoad = async function (widget) { if (!window.FileReader) { L.ALS.Helpers.readTextFile(widget.input, L.ALS.locale.notGridNotSupported, (grid) => { let parser = new ESRIGridParser(this); - parser.readChunk(grid); + try { + parser.readChunk(grid); + } catch (e) { + this.showDEMError([widget.input.files[0].name]); + return; + } parser.copyStats(); clear(); }); @@ -34,35 +40,53 @@ L.ALS.SynthPolygonBaseLayer.prototype.onDEMLoad = async function (widget) { } // For normal browsers - try { - await this.onDEMLoadWorker(widget); - } catch (e) { - console.error(e); - window.alert(L.ALS.locale.DEMError); - } + let {invalidFiles, invalidProjectionFiles} = await this.onDEMLoadWorker(widget); clear(); + if (invalidFiles.length !== 0) + this.showDEMError(invalidFiles, invalidProjectionFiles); }; +L.ALS.SynthPolygonBaseLayer.prototype.showDEMError = function (invalidFiles, invalidProjectionFiles = []) { + let errorMessage = L.ALS.locale.DEMError + " " + invalidFiles.join(", "); + + if (invalidProjectionFiles.length !== 0) + errorMessage += "\n\n" + L.ALS.locale.DEMErrorProjFiles + " " + invalidProjectionFiles.join(", "); + + window.alert(errorMessage + "."); +} + +L.ALS.SynthPolygonBaseLayer.prototype._tryProjectionString = function (string) { + try { + proj4(string, "WGS84"); + return true; + } catch (e) { + return false; + } +} + /** * Being called upon DEM load * @param widget {L.ALS.Widgets.File} */ L.ALS.SynthPolygonBaseLayer.prototype.onDEMLoadWorker = async function (widget) { - let files = widget.getValue(); - let parser = new ESRIGridParser(this); - let fileReader = new FileReader(); - // noinspection JSUnresolvedVariable - let supportsWorker = (window.Worker && process.browser); // We're using webworkify which relies on browserify-specific stuff which isn't available in dev environment + let files = widget.getValue(), + parser = new ESRIGridParser(this), + fileReader = new FileReader(), + supportsWorker = (window.Worker && process.browser), // We're using webworkify which relies on browserify-specific stuff which isn't available in dev environment + invalidFiles = [], invalidProjectionFiles = []; for (let file of files) { let ext = L.ALS.Helpers.getFileExtension(file.name).toLowerCase(); - let isTiff = (ext === "tif" || ext === "tiff" || ext === "geotif" || ext === "geotiff"); - let isGrid = (ext === "asc" || ext === "grd"); + let isTiff = (ext === "tif" || ext === "tiff" || ext === "geotif" || ext === "geotiff"), + isGrid = (ext === "asc" || ext === "grd"); - if (!isTiff && !isGrid) + if (!isTiff && !isGrid) { + if (ext !== "prj" && ext !== "xml") + invalidFiles.push(file.name); continue; + } // Try to find aux or prj file for current file and get projection string from it let baseName = "", projectionString = ""; @@ -74,8 +98,9 @@ L.ALS.SynthPolygonBaseLayer.prototype.onDEMLoadWorker = async function (widget) } for (let file2 of files) { - let ext2 = L.ALS.Helpers.getFileExtension(file2.name).toLowerCase(); - let isPrj = (ext2 === "prj"); + let ext2 = L.ALS.Helpers.getFileExtension(file2.name).toLowerCase(), + isPrj = (ext2 === "prj"); + if ((ext2 !== "xml" && !isPrj) || !file2.name.startsWith(baseName)) continue; @@ -91,31 +116,45 @@ L.ALS.SynthPolygonBaseLayer.prototype.onDEMLoadWorker = async function (widget) // prj contains only projection string if (isPrj) { projectionString = text; + + if (!this._tryProjectionString(projectionString)) { + invalidProjectionFiles.push(file2.name); + projectionString = ""; + } + break; } // Parse XML - let start = "", end = ""; - let startIndex = text.indexOf(start) + start.length; - let endIndex = text.indexOf(end); - if (startIndex === start.length - 1 || endIndex === -1) - continue; // Continue in hope of finding correct xml or prj file. + let start = "", end = "", + startIndex = text.indexOf(start) + start.length, + endIndex = text.indexOf(end); + projectionString = text.substring(startIndex, endIndex); - break; + + if (projectionString.length !== 0 && this._tryProjectionString(projectionString)) + break; + + invalidProjectionFiles.push(file2.name); + projectionString = ""; } if (isTiff) { if (!GeoTIFFParser) continue; - let stats = await GeoTIFFParser(file, projectionString, ESRIGridParser.getInitialData(this, false)); - ESRIGridParser.copyStats(this, stats); + try { + let stats = await GeoTIFFParser(file, projectionString, ESRIGridParser.getInitialData(this, false)); + ESRIGridParser.copyStats(this, stats); + } catch (e) { + invalidFiles.push(file.name); + } continue; } if (!supportsWorker) { await new Promise((resolve) => { ESRIGridParser.parseFile(file, parser, fileReader, () => resolve()) - }); + }).catch(() => invalidFiles.push(file.name)); continue; } @@ -131,6 +170,8 @@ L.ALS.SynthPolygonBaseLayer.prototype.onDEMLoadWorker = async function (widget) projectionString: projectionString, file: file, }); - }); + }).catch(() => invalidFiles.push(file.name)); } + + return {invalidFiles, invalidProjectionFiles}; }; \ No newline at end of file diff --git a/locales/English.js b/locales/English.js index 7e978a82..9e12d2ec 100644 --- a/locales/English.js +++ b/locales/English.js @@ -110,7 +110,8 @@ L.ALS.Locales.addLocaleProperties("English", { confirmDEMLoading: "Are you sure you want to load DEMs? It will override current statistics and take some time.", loadingDEM: "Loading selected DEM files, it might take a while...", notGridNotSupported: "Sorry, your browser doesn't support anything other than ASCII Grid. Please, select a valid ASCII Grid file.", - DEMError: "Sorry, an error occurred while loading one of your files", //TODO: Add file name + DEMError: "Following files are not valid DEM files:", + DEMErrorProjFiles: "Following files are not valid projection files:", jsonNoPaths1: "No paths has been drawn in layer", jsonNoPaths2: "only selected geometry and airport position will be exported.", diff --git a/locales/Russian.js b/locales/Russian.js index 92d9a60b..b9212e9c 100644 --- a/locales/Russian.js +++ b/locales/Russian.js @@ -106,7 +106,8 @@ L.ALS.Locales.addLocaleProperties("Русский", { confirmDEMLoading: "Вы уверены, что хотите загрузить файлы ЦМР? Это перезапишет текущую статистику и займет некоторое время.", loadingDEM: "Выбранные вами файлы ЦМР загружаются, это может занять некоторое время...", notGridNotSupported: "Извините, ваш браузер не поддерживает ничего, кроме ASCII Grid. Пожалуйста, выберете файл ASCII Grid.", - DEMError: "Извините, во время загрузки одного из ваших файлов произошла ошибка", + DEMError: "Следующие файлы не являются файлами ЦМР:", + DEMErrorProjFiles: "Следующие файлы не являются файлами проекции:", jsonNoPaths1: "Маршруты не были добавлены в слое", jsonNoPaths2: "будет экспортирована только геометрия и положение аэропорта.", From 29242b8dab35e7270a7bd9bbffa5a569a269ff11 Mon Sep 17 00:00:00 2001 From: matafokka Date: Sat, 14 May 2022 14:45:46 +0300 Subject: [PATCH 30/48] Implemented draw tolerance --- SynthBaseLayer/draw.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/SynthBaseLayer/draw.js b/SynthBaseLayer/draw.js index ca0c3896..f1334f68 100644 --- a/SynthBaseLayer/draw.js +++ b/SynthBaseLayer/draw.js @@ -47,6 +47,17 @@ L.ALS.SynthBaseLayer.prototype.onDraw = function (e) { if (!this.isSelected) return; + // Don't add layers of size less than 3x3 px + if (e.layer.getBounds) { + let {_northEast, _southWest} = e.layer.getBounds(), + zoom = this.map.getZoom(), + min = this.map.project(_northEast, zoom), + max = this.map.project(_southWest, zoom); + + if (Math.abs(max.x - min.x) <= 3 && Math.abs(max.y - min.y) <= 3) + return; + } + this._drawingGroup.addLayer(e.layer); let borderColorId, fillColor; From 8fd8a6521f1f8208e568690909e4610cecd91245 Mon Sep 17 00:00:00 2001 From: matafokka Date: Sat, 14 May 2022 17:00:45 +0300 Subject: [PATCH 31/48] Added error reporting for unsupported Shapefile projection --- SynthGeometryBaseWizard.js | 19 ++++++++++++++++--- SynthGeometryLayer/SynthGeometryLayer.js | 3 +++ locales/English.js | 1 + locales/Russian.js | 1 + package-lock.json | 13 ------------- package.json | 1 - 6 files changed, 21 insertions(+), 17 deletions(-) diff --git a/SynthGeometryBaseWizard.js b/SynthGeometryBaseWizard.js index 6d79fc71..120d1e70 100644 --- a/SynthGeometryBaseWizard.js +++ b/SynthGeometryBaseWizard.js @@ -1,6 +1,11 @@ const shp = require("shpjs"); -L.ALS.SynthGeometryBaseWizard = L.ALS.Wizard.extend({ +/** + * Wizard with file reader and Shapefile and GeoJSON parser + * @class + * @extends L.ALS.Wizard + */ +L.ALS.SynthGeometryBaseWizard = L.ALS.Wizard.extend(/** @lends L.ALS.SynthGeometryBaseWizard.prototype */{ fileLabel: "geometryFileLabel", @@ -22,7 +27,7 @@ L.ALS.SynthGeometryBaseWizard = L.ALS.Wizard.extend({ * Callback to pass to {@link L.ALS.SynthGeometryBaseWizard.getGeoJSON}. * * @callback getGeoJSONCallback - * @param {Object|"NoFileSelected"|"NoFeatures"|"InvalidFileType"} geoJson GeoJSON or an error message + * @param {Object|"NoFileSelected"|"NoFeatures"|"InvalidFileType"|"ProjectionNotSupported"} geoJson GeoJSON or an error message * @param {string|undefined} [fileName=undefined] Name of the loaded file * @param {boolean|undefined} [isShapefile=undefined] If selected file is shapefile */ @@ -55,7 +60,11 @@ L.ALS.SynthGeometryBaseWizard = L.ALS.Wizard.extend({ callback(geoJson, file.name, true); }).catch((reason) => { - console.log(reason); + // If reason is string, proj4js doesn't support file projection + if (typeof reason === "string") { + callback("ProjectionNotSupported"); + return; + } // If reading as shapefile fails, try to read as GeoJSON. // We won't check bounds because we assume GeoJSON being in WGS84. @@ -117,6 +126,10 @@ L.ALS.SynthGeometryBaseWizard = L.ALS.Wizard.extend({ window.alert(L.ALS.locale.geometryInvalidFile); finishLoading(); return; + case "ProjectionNotSupported": + window.alert(L.ALS.locale.geometryProjectionNotSupported); + finishLoading(); + return; } let layersAdded = false; diff --git a/SynthGeometryLayer/SynthGeometryLayer.js b/SynthGeometryLayer/SynthGeometryLayer.js index 77388ca3..758c2916 100644 --- a/SynthGeometryLayer/SynthGeometryLayer.js +++ b/SynthGeometryLayer/SynthGeometryLayer.js @@ -42,6 +42,9 @@ L.ALS.SynthGeometryLayer = L.ALS.Layer.extend( /** @lends L.ALS.SynthGeometryLay case "InvalidFileType": this._deleteInvalidLayer(L.ALS.locale.geometryInvalidFile); return; + case "ProjectionNotSupported": + this._deleteInvalidLayer(L.ALS.locale.geometryProjectionNotSupported); + return; } this.originalGeoJson = geoJson; diff --git a/locales/English.js b/locales/English.js index 9e12d2ec..de1fd0e5 100644 --- a/locales/English.js +++ b/locales/English.js @@ -148,6 +148,7 @@ L.ALS.Locales.addLocaleProperties("English", { geometryFillColor: "Fill color:", geometryBrowserNotSupported: "Your browser doesn't support adding this layer. You still can open projects with this layer though.", geometryNoFileSelected: "No file has been selected. Please, select a file that you want to add and try again.", + geometryProjectionNotSupported: "Sorry, projection of selected file is not supported. Please, convert your file to another projection, preferably, WebMercator.", // SynthGeometrySettings diff --git a/locales/Russian.js b/locales/Russian.js index b9212e9c..f14361e5 100644 --- a/locales/Russian.js +++ b/locales/Russian.js @@ -144,6 +144,7 @@ L.ALS.Locales.addLocaleProperties("Русский", { geometryFillColor: "Цвет заливки:", geometryBrowserNotSupported: "Ваш браузер не поддерживает добавление данного слоя, но вы можете открывать проекты, использующие этот слой.", geometryNoFileSelected: "Файл не был выбран. Пожалуйста, выберете файл, который хотите добавить, и попробуйте снова.", + geometryProjectionNotSupported: "Извините проекция выбранного файла не поддерживается. Пожалуйста, переконвертируйте файл в другую проекцию, предпочтительно, в WebMercator.", // SynthGeometrySettings diff --git a/package-lock.json b/package-lock.json index 16dbb3fa..846458f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,7 +34,6 @@ "debounce": "^1.2.1", "electron": "^16.0.3", "electron-packager": "^15.4.0", - "fastestsmallesttextencoderdecoder": "^1.0.22", "fs-extra": "^9.0.1", "geotiff": "^1.0.4", "geotiff-geokeys-to-proj4": "^2021.10.31", @@ -4581,12 +4580,6 @@ "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==", "dev": true }, - "node_modules/fastestsmallesttextencoderdecoder": { - "version": "1.0.22", - "resolved": "https://registry.npmjs.org/fastestsmallesttextencoderdecoder/-/fastestsmallesttextencoderdecoder-1.0.22.tgz", - "integrity": "sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==", - "dev": true - }, "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -16452,12 +16445,6 @@ "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==", "dev": true }, - "fastestsmallesttextencoderdecoder": { - "version": "1.0.22", - "resolved": "https://registry.npmjs.org/fastestsmallesttextencoderdecoder/-/fastestsmallesttextencoderdecoder-1.0.22.tgz", - "integrity": "sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==", - "dev": true - }, "fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", diff --git a/package.json b/package.json index 8d52a65a..6d1280d4 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,6 @@ "debounce": "^1.2.1", "electron": "^16.0.3", "electron-packager": "^15.4.0", - "fastestsmallesttextencoderdecoder": "^1.0.22", "fs-extra": "^9.0.1", "geotiff": "^1.0.4", "geotiff-geokeys-to-proj4": "^2021.10.31", From 44dbde4376fd4f73bfec1ad14095ffa123e590d7 Mon Sep 17 00:00:00 2001 From: matafokka Date: Sat, 14 May 2022 17:20:51 +0300 Subject: [PATCH 32/48] Small code refactoring - Added missing class annotations. - Replaced factories with instantiation which seems to finally enable code completion. --- DrawGeodesic.js | 2 +- SynthBaseLayer/SynthBaseLayer.js | 4 ++-- SynthGeometryLayer/SynthGeometryLayer.js | 2 +- SynthGridLayer/onMapPan.js | 2 +- SynthLineLayer/SynthLineLayer.js | 11 +++++------ SynthPolygonBaseLayer/SynthPolygonBaseLayer.js | 6 +++--- SynthPolygonBaseLayer/polygons.js | 2 +- SynthPolygonLayer/SynthPolygonLayer.js | 16 +++++++++++----- .../SynthRectangleBaseLayer.js | 16 ++++++++-------- SynthRectangleBaseLayer/misc.js | 2 +- SynthRectangleLayer/SynthRectangleLayer.js | 8 +++++++- main.js | 4 ++-- 12 files changed, 43 insertions(+), 32 deletions(-) diff --git a/DrawGeodesic.js b/DrawGeodesic.js index a255753e..78a9af31 100644 --- a/DrawGeodesic.js +++ b/DrawGeodesic.js @@ -125,7 +125,7 @@ L.Draw.Polyline = L.Draw.Feature.extend({ // also do not want to trigger any click handlers of objects we are clicking on // while drawing. if (!this._mouseMarker) { - this._mouseMarker = L.marker(this._map.getCenter(), { + this._mouseMarker = new L.Marker(this._map.getCenter(), { icon: L.divIcon({ className: 'leaflet-mouse-marker', iconAnchor: [20, 20], diff --git a/SynthBaseLayer/SynthBaseLayer.js b/SynthBaseLayer/SynthBaseLayer.js index b2bbb3f1..a6b2c722 100644 --- a/SynthBaseLayer/SynthBaseLayer.js +++ b/SynthBaseLayer/SynthBaseLayer.js @@ -162,7 +162,7 @@ L.ALS.SynthBaseLayer = L.ALS.Layer.extend(/** @lends L.ALS.SynthBaseLayer.protot * Airport marker * @protected */ - this._airportMarker = L.marker(this.map.getCenter(), { + this._airportMarker = new L.Marker(this.map.getCenter(), { icon: icon, draggable: true }); @@ -564,7 +564,7 @@ L.ALS.SynthBaseLayer = L.ALS.Layer.extend(/** @lends L.ALS.SynthBaseLayer.protot }, createCapturePoint: function (coord, color) { - return L.circleMarker(coord, { + return new L.CircleMarker(coord, { radius: this.lineThicknessValue * 2, stroke: false, fillOpacity: 1, diff --git a/SynthGeometryLayer/SynthGeometryLayer.js b/SynthGeometryLayer/SynthGeometryLayer.js index 758c2916..9a6a5477 100644 --- a/SynthGeometryLayer/SynthGeometryLayer.js +++ b/SynthGeometryLayer/SynthGeometryLayer.js @@ -72,7 +72,7 @@ L.ALS.SynthGeometryLayer = L.ALS.Layer.extend( /** @lends L.ALS.SynthGeometryLay let docs = [], fields = [], clonedLayers = []; // Search documents - this._layer = L.geoJSON(geoJson, { + this._layer = new L.GeoJSON(geoJson, { onEachFeature: (feature, layer) => { let popup = "", doc = {}, bbox; diff --git a/SynthGridLayer/onMapPan.js b/SynthGridLayer/onMapPan.js index d610b371..fc66ab45 100644 --- a/SynthGridLayer/onMapPan.js +++ b/SynthGridLayer/onMapPan.js @@ -55,7 +55,7 @@ L.ALS.SynthGridLayer.prototype._onMapPan = function () { if (isFirstIteration) createLabel([north, lng], this.toFixed(lng), "topCenter", true); - let polygon = L.polygon([ + let polygon = new L.Polygon([ [lat, lng], [lat + this.latDistance, lng], [lat + this.latDistance, lng + lngDistance], diff --git a/SynthLineLayer/SynthLineLayer.js b/SynthLineLayer/SynthLineLayer.js index 9ab9f341..6f0e7198 100644 --- a/SynthLineLayer/SynthLineLayer.js +++ b/SynthLineLayer/SynthLineLayer.js @@ -1,7 +1,6 @@ require("./SynthLineWizard.js"); require("./SynthLineSettings.js"); const geojsonMerge = require("@mapbox/geojson-merge"); // Using this since turfHelpers.featureCollection() discards previously defined properties. -const MathTools = require("../MathTools.js"); /** * Geodesic line layer @@ -16,10 +15,10 @@ L.ALS.SynthLineLayer = L.ALS.SynthBaseLayer.extend(/** @lends L.ALS.SynthLineLay hasYOverlay: false, init: function (wizardResults, settings) { - this.pathsGroup = L.featureGroup(); - this.drawingGroup = L.featureGroup(); - this.connectionsGroup = L.featureGroup(); - this.errorGroup = L.featureGroup(); + this.pathsGroup = new L.FeatureGroup(); + this.drawingGroup = new L.FeatureGroup(); + this.connectionsGroup = new L.FeatureGroup(); + this.errorGroup = new L.FeatureGroup(); L.ALS.SynthBaseLayer.prototype.init.call(this, settings, this.pathsGroup, this.connectionsGroup, "lineLayerColor"); this.addLayers(this.errorGroup); @@ -42,7 +41,7 @@ L.ALS.SynthLineLayer = L.ALS.SynthBaseLayer.extend(/** @lends L.ALS.SynthLineLay this.addBaseParametersInputSection(); this.addBaseParametersOutputSection(); - this.pointsGroup = L.featureGroup(); + this.pointsGroup = new L.FeatureGroup(); L.ALS.SynthGeometryBaseWizard.initializePolygonOrPolylineLayer(this, wizardResults); }, diff --git a/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js b/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js index 77475f7a..ee6f2b5a 100644 --- a/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js +++ b/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js @@ -46,9 +46,9 @@ L.ALS.SynthPolygonBaseLayer = L.ALS.SynthBaseLayer.extend( /** @lends L.ALS.Synt this.polygonsWidgets = {}; this.serializationIgnoreList.push("polygons", "invalidPolygons", "lngDistance", "latDistance", "_currentStandardScale"); - this.polygonGroup = L.featureGroup(); - this.widgetsGroup = L.featureGroup(); - this.bordersGroup = L.featureGroup(); + this.polygonGroup = new L.FeatureGroup(); + this.widgetsGroup = new L.FeatureGroup(); + this.bordersGroup = new L.FeatureGroup(); this.bordersGroup.thicknessMultiplier = 4; this.labelsGroup = new L.LabelLayer(false); diff --git a/SynthPolygonBaseLayer/polygons.js b/SynthPolygonBaseLayer/polygons.js index f1d48f21..bbd49c39 100644 --- a/SynthPolygonBaseLayer/polygons.js +++ b/SynthPolygonBaseLayer/polygons.js @@ -149,7 +149,7 @@ L.ALS.SynthPolygonBaseLayer.prototype.cloneLayerIfNeeded = function (layer) { crossingBoth = crossingEast && crossingWest if (!layer.linkedLayer && crossingOne && !crossingBoth) { - let clonedLayer = layer instanceof L.Rectangle ? L.rectangle(bounds) : L.polygon([]), + let clonedLayer = layer instanceof L.Rectangle ? L.rectangle(bounds) : new L.Polygon([]), moveTo = crossingWest ? 1 : -1, setLinkedLatLngs = (editedLayer) => { let latlngs = this._getRectOrPolyCoords(editedLayer), newLatLngs = []; diff --git a/SynthPolygonLayer/SynthPolygonLayer.js b/SynthPolygonLayer/SynthPolygonLayer.js index 64c938a8..3f35ca8d 100644 --- a/SynthPolygonLayer/SynthPolygonLayer.js +++ b/SynthPolygonLayer/SynthPolygonLayer.js @@ -4,7 +4,13 @@ const MathTools = require("../MathTools.js"); const proj4 = require("proj4"); const debounce = require("debounce") -L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend({ +/** + * Polygon layer + * + * @class + * @extends L.ALS.SynthPolygonBaseLayer + */ +L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend(/** @lends L.ALS.SynthPolygonLayer.prototype */{ calculateCellSizeForPolygons: false, defaultName: "Polygon Layer", @@ -20,10 +26,10 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend({ */ this.maxGnomonicPointDistance = this.getEarthRadius() * 89 * Math.PI / 180; - this.internalConnections = L.featureGroup(); - this.externalConnections = L.featureGroup(); - this.pathGroup = L.featureGroup(); - this.pointsGroup = L.featureGroup(); + this.internalConnections = new L.FeatureGroup(); + this.externalConnections = new L.FeatureGroup(); + this.pathGroup = new L.FeatureGroup(); + this.pointsGroup = new L.FeatureGroup(); L.ALS.SynthPolygonBaseLayer.prototype.init.call(this, settings, this.internalConnections, diff --git a/SynthRectangleBaseLayer/SynthRectangleBaseLayer.js b/SynthRectangleBaseLayer/SynthRectangleBaseLayer.js index 1d4c8e69..ca989c53 100644 --- a/SynthRectangleBaseLayer/SynthRectangleBaseLayer.js +++ b/SynthRectangleBaseLayer/SynthRectangleBaseLayer.js @@ -27,16 +27,16 @@ L.ALS.SynthRectangleBaseLayer = L.ALS.SynthPolygonBaseLayer.extend( /** @lends L // Additional redrawing actually won't introduce any noticeable delay. // Create empty groups containing our stuff. Yeah, I hate copying too, but I want code completion :D - this.latPointsGroup = L.featureGroup(); - this.lngPointsGroup = L.featureGroup(); + this.latPointsGroup = new L.FeatureGroup(); + this.lngPointsGroup = new L.FeatureGroup(); - this.pathsByParallels = L.featureGroup(); - this.parallelsInternalConnections = L.featureGroup(); - this.parallelsExternalConnections = L.featureGroup(); + this.pathsByParallels = new L.FeatureGroup(); + this.parallelsInternalConnections = new L.FeatureGroup(); + this.parallelsExternalConnections = new L.FeatureGroup(); - this.pathsByMeridians = L.featureGroup(); - this.meridiansInternalConnections = L.featureGroup(); - this.meridiansExternalConnections = L.featureGroup(); + this.pathsByMeridians = new L.FeatureGroup(); + this.meridiansInternalConnections = new L.FeatureGroup(); + this.meridiansExternalConnections = new L.FeatureGroup(); L.ALS.SynthPolygonBaseLayer.prototype.init.call(this, settings, // Parallels args diff --git a/SynthRectangleBaseLayer/misc.js b/SynthRectangleBaseLayer/misc.js index e415670c..7447c585 100644 --- a/SynthRectangleBaseLayer/misc.js +++ b/SynthRectangleBaseLayer/misc.js @@ -39,7 +39,7 @@ L.ALS.SynthRectangleBaseLayer.prototype.calculatePolygonParameters = function (w if (!this.useZoneNumbers) continue; - this.bordersGroup.addLayer(L.polyline(latLngs, { + this.bordersGroup.addLayer(new L.Polyline(latLngs, { weight: this.lineThicknessValue * this.bordersGroup.thicknessMultiplier, color: this.getWidgetById("borderColor").getValue() } diff --git a/SynthRectangleLayer/SynthRectangleLayer.js b/SynthRectangleLayer/SynthRectangleLayer.js index d5e3c027..cbcba7af 100644 --- a/SynthRectangleLayer/SynthRectangleLayer.js +++ b/SynthRectangleLayer/SynthRectangleLayer.js @@ -1,7 +1,13 @@ require("./SynthRectangleWizard.js"); require("./SynthRectangleSettings.js"); -L.ALS.SynthRectangleLayer = L.ALS.SynthRectangleBaseLayer.extend({ +/** + * Rectangle layer + * + * @class + * @extends L.ALS.SynthRectangleBaseLayer + */ +L.ALS.SynthRectangleLayer = L.ALS.SynthRectangleBaseLayer.extend(/** @lends L.ALS.SynthRectangleLayer.prototype */{ defaultName: "Rectangle Layer", borderColorLabel: "rectangleBorderColor", diff --git a/main.js b/main.js index b2aeaa0b..86564715 100644 --- a/main.js +++ b/main.js @@ -132,9 +132,9 @@ let overlayLayer = new L.BlackOverlayLayer({ // When drawing starts, hide notifications and black overlay, but add red datelines -let datelines = L.featureGroup(); +let datelines = new L.FeatureGroup(); for (let lng of [180, -180]) { - datelines.addLayer(L.polyline([[90, lng], [-90, lng]], { + datelines.addLayer(new L.Polyline([[90, lng], [-90, lng]], { color: "red", weight: 1, })); From 4688763dc88bd47bb424c2394d2c798e0f1a863b Mon Sep 17 00:00:00 2001 From: matafokka Date: Sat, 14 May 2022 19:01:57 +0300 Subject: [PATCH 33/48] Geodesics fixes - Optimized geodesics in Polygon Layer - Now Line Layer checks if number of images is greater than global number of segments. This prevents capture points not lying on the line. - Replaced L.Geodesic factories with instantiations. Forgot to do so in one of previous commits. --- DrawGeodesic.js | 2 +- SynthBaseLayer/Hull.js | 4 ++-- SynthBaseLayer/SynthBaseLayer.js | 4 ++-- SynthLineLayer/SynthLineLayer.js | 19 +++++++++++-------- SynthPolygonLayer/SynthPolygonLayer.js | 22 ++++++++++++---------- 5 files changed, 28 insertions(+), 23 deletions(-) diff --git a/DrawGeodesic.js b/DrawGeodesic.js index 78a9af31..fd9e9792 100644 --- a/DrawGeodesic.js +++ b/DrawGeodesic.js @@ -15,7 +15,7 @@ L.Geodesic.prototype.getActualLatLngs = function () { // Writing these functions as class methods will result in an error being thrown. I don't know why. function _createGeodesic (coords, opts = {}) { - let geodesic = L.geodesic(coords, {...opts, wrap: true}); + let geodesic = new L.Geodesic(coords, {...opts, wrap: true}); geodesic.geom.geodesic.ellipsoid.a = 6378137; geodesic.geom.geodesic.ellipsoid.b = 6378137; geodesic.geom.geodesic.ellipsoid.f = 0; diff --git a/SynthBaseLayer/Hull.js b/SynthBaseLayer/Hull.js index 6ae46be7..e9fba659 100644 --- a/SynthBaseLayer/Hull.js +++ b/SynthBaseLayer/Hull.js @@ -31,7 +31,7 @@ L.ALS.SynthBaseLayer.prototype.buildHull = function (path, color) { if (layers.length === 1) { const latLngs = layers[0].getLatLngs(); - connectionsGroup.addLayer(L.geodesic([latLngs[0], latLngs[latLngs.length - 1]], lineOptions)); + connectionsGroup.addLayer(new L.Geodesic([latLngs[0], latLngs[latLngs.length - 1]], lineOptions)); connectionsGroup.addLayer(hullConnection); path.hullConnections = []; return; @@ -160,7 +160,7 @@ L.ALS.SynthBaseLayer.prototype.buildHull = function (path, color) { } for (let pair of optConnections) - connectionsGroup.addLayer(L.geodesic(pair, lineOptions)); + connectionsGroup.addLayer(new L.Geodesic(pair, lineOptions)); connectionsGroup.addLayer(hullConnection); path.hullConnections = optConnections; } diff --git a/SynthBaseLayer/SynthBaseLayer.js b/SynthBaseLayer/SynthBaseLayer.js index a6b2c722..ae72eb23 100644 --- a/SynthBaseLayer/SynthBaseLayer.js +++ b/SynthBaseLayer/SynthBaseLayer.js @@ -138,7 +138,7 @@ L.ALS.SynthBaseLayer = L.ALS.Layer.extend(/** @lends L.ALS.SynthBaseLayer.protot for (let i = 0; i < this.paths.length; i++) { let path = this.paths[i]; - path.hullConnection = L.geodesic([[0, 0], [0, 0]], this.getConnectionLineOptions(settings[`color${i}`])); + path.hullConnection = new L.Geodesic([[0, 0], [0, 0]], this.getConnectionLineOptions(settings[`color${i}`])); this.addLayers(path.pathGroup, path.connectionsGroup); this.toUpdateThickness.push(path.pathGroup, path.connectionsGroup); } @@ -487,7 +487,7 @@ L.ALS.SynthBaseLayer = L.ALS.Layer.extend(/** @lends L.ALS.SynthBaseLayer.protot layer.pathLength = this.getPathLength(layer); let latLngs = layer.getLatLngs(), - connectionLine = L.geodesic([latLngs[0], [0, 0], latLngs[latLngs.length - 1]], lineOptions); + connectionLine = new L.Geodesic([latLngs[0], [0, 0], latLngs[latLngs.length - 1]], lineOptions); connectionLine.pathLength = layer.pathLength; let toFlash = [layer, connectionLine]; if (layer.actualPaths) diff --git a/SynthLineLayer/SynthLineLayer.js b/SynthLineLayer/SynthLineLayer.js index 6f0e7198..bd141adc 100644 --- a/SynthLineLayer/SynthLineLayer.js +++ b/SynthLineLayer/SynthLineLayer.js @@ -72,9 +72,7 @@ L.ALS.SynthLineLayer = L.ALS.SynthBaseLayer.extend(/** @lends L.ALS.SynthLineLay this.errorGroup.clearLayers(); let color = this.getWidgetById("color0").getValue(), - lineOptions = { - color, thickness: this.lineThicknessValue, segmentsNumber: L.GEODESIC_SEGMENTS, - }, + lineOptions = {color, thickness: this.lineThicknessValue}, linesWereInvalidated = false; notifyIfLayersSkipped = typeof notifyIfLayersSkipped === "boolean" ? notifyIfLayersSkipped : true; @@ -82,7 +80,7 @@ L.ALS.SynthLineLayer = L.ALS.SynthBaseLayer.extend(/** @lends L.ALS.SynthLineLay this.drawingGroup.eachLayer((layer) => { let latLngs = layer.getLatLngs(); for (let i = 1; i < latLngs.length; i++) { - let extendedGeodesic = new L.Geodesic([latLngs[i - 1], latLngs[i]], lineOptions), + let extendedGeodesic = new L.Geodesic([latLngs[i - 1], latLngs[i]], {segmentsNumber: 2}), length = extendedGeodesic.statistics.sphericalLengthMeters, numberOfImages = Math.ceil(length / this.Bx) + 4, shouldInvalidateLine = numberOfImages > 10000; // Line is way too long for calculated Bx @@ -94,17 +92,22 @@ L.ALS.SynthLineLayer = L.ALS.SynthBaseLayer.extend(/** @lends L.ALS.SynthLineLay shouldInvalidateLine = true; } + let displayGeodesic = new L.Geodesic(extendedGeodesic.getLatLngs(), { + ...lineOptions, + segmentsNumber: Math.max(numberOfImages, L.GEODESIC_SEGMENTS) + }); + if (shouldInvalidateLine) { - extendedGeodesic.setStyle({color: "red"}); - this.errorGroup.addLayer(extendedGeodesic); + displayGeodesic.setStyle({color: "red"}); + this.errorGroup.addLayer(displayGeodesic); linesWereInvalidated = true; continue; } - this.pathsGroup.addLayer(extendedGeodesic); + this.pathsGroup.addLayer(displayGeodesic); // Capture points made by constructing a line with segments number equal to the number of images - let pointsArrays = new L.Geodesic(extendedGeodesic.getLatLngs(), { + let pointsArrays = new L.Geodesic(displayGeodesic.getLatLngs(), { ...lineOptions, segmentsNumber: numberOfImages }).getActualLatLngs(); diff --git a/SynthPolygonLayer/SynthPolygonLayer.js b/SynthPolygonLayer/SynthPolygonLayer.js index 3f35ca8d..f871acf2 100644 --- a/SynthPolygonLayer/SynthPolygonLayer.js +++ b/SynthPolygonLayer/SynthPolygonLayer.js @@ -67,9 +67,8 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend(/** @lends L.ALS.Sy return; let color = this.getWidgetById("color0").getValue(), - lineOptions = { - color, thickness: this.lineThicknessValue, segmentsNumber: L.GEODESIC_SEGMENTS, - } + lineOptions = {color, thickness: this.lineThicknessValue, segmentsNumber: L.GEODESIC_SEGMENTS}, + calculationsLineOptions = {segmentsNumber: 2} for (let name in this.polygons) this.removePolygon(this.polygons[name], false); @@ -205,10 +204,10 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend(/** @lends L.ALS.Sy shouldSwapPoints = !shouldSwapPoints; // Build a path - let path = L.geodesic([ + let path = new L.Geodesic([ proj.inverse(line[0]).reverse(), proj.inverse(line[1]).reverse(), - ], lineOptions), + ], calculationsLineOptions), length = path.statistics.sphericalLengthMeters, numberOfImages = Math.ceil(length / this.Bx), extendBy; @@ -244,12 +243,14 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend(/** @lends L.ALS.Sy } // Push the stuff related to the current paths to the arrays + let pathEndPoints = path.getLatLngs(); + currentPath.push(path); currentLength += path.statistics.sphericalLengthMeters; - currentConnections.push(...path.getLatLngs()); + currentConnections.push(...pathEndPoints); // Fill in capture points. - let pointsArrays = L.geodesic(path.getLatLngs(), {segmentsNumber: numberOfImages}).getActualLatLngs(); + let pointsArrays = new L.Geodesic(pathEndPoints, {segmentsNumber: numberOfImages}).getActualLatLngs(); for (let array of pointsArrays) { for (let point of array) @@ -270,13 +271,14 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend(/** @lends L.ALS.Sy this.addPolygon(layer); - this.internalConnections.addLayer(L.geodesic(shortestPathConnections, { - ...lineOptions, dashArray: this.dashedLine + this.internalConnections.addLayer(new L.Geodesic(shortestPathConnections, { + ...lineOptions, + dashArray: this.dashedLine, })); let number = 1; for (let path of shortestPath) { - this.pathGroup.addLayer(path); + this.pathGroup.addLayer(new L.Geodesic(path.getLatLngs(), lineOptions)); // Add numbers let latLngs = path.getLatLngs(); From 6afe09e1d58d4b5628f4b0ce9eb493eaffe0fce7 Mon Sep 17 00:00:00 2001 From: matafokka Date: Sat, 14 May 2022 19:12:39 +0300 Subject: [PATCH 34/48] Now widgets in Polygon Layer are aligned to the polygon's center --- SynthPolygonBaseLayer/polygons.js | 13 +++++++++++-- SynthPolygonLayer/SynthPolygonLayer.js | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/SynthPolygonBaseLayer/polygons.js b/SynthPolygonBaseLayer/polygons.js index bbd49c39..d85fcf42 100644 --- a/SynthPolygonBaseLayer/polygons.js +++ b/SynthPolygonBaseLayer/polygons.js @@ -17,14 +17,23 @@ L.ALS.MeanHeightButtonHandler = L.ALS.Serializable.extend( /**@lends L.ALS.MeanH } }) -L.ALS.SynthPolygonBaseLayer.prototype.addPolygon = function (polygon) { +L.ALS.SynthPolygonBaseLayer.prototype.addPolygon = function (polygon, alignToCenter = false) { polygon._intName = this._generatePolygonName(polygon); delete this.invalidPolygons[polygon._intName]; polygon.setStyle({fill: true}); this.polygons[polygon._intName] = polygon; - let controlsContainer = new L.WidgetLayer(polygon.getLatLngs()[0][1], "topLeft"), handler = new L.ALS.MeanHeightButtonHandler(controlsContainer); + let anchorPoint, anchor; + if (alignToCenter) { + anchorPoint = polygon.getBounds().getCenter(); + anchor = "center"; + } else { + anchorPoint = polygon.getLatLngs()[0][1]; + anchor = "topLeft"; + } + + let controlsContainer = new L.WidgetLayer(anchorPoint, anchor), handler = new L.ALS.MeanHeightButtonHandler(controlsContainer); if (this.useZoneNumbers) controlsContainer.addWidget(new L.ALS.Widgets.Number("zoneNumber", "zoneNumber", this, "calculatePolygonParameters").setMin(1).setValue(1)); diff --git a/SynthPolygonLayer/SynthPolygonLayer.js b/SynthPolygonLayer/SynthPolygonLayer.js index f871acf2..8adf4561 100644 --- a/SynthPolygonLayer/SynthPolygonLayer.js +++ b/SynthPolygonLayer/SynthPolygonLayer.js @@ -269,7 +269,7 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend(/** @lends L.ALS.Sy shortestPathPoints = currentPoints; } - this.addPolygon(layer); + this.addPolygon(layer, center); this.internalConnections.addLayer(new L.Geodesic(shortestPathConnections, { ...lineOptions, From 5bfc03ff726b7c6100693ad180a9acaf6ec10515 Mon Sep 17 00:00:00 2001 From: matafokka Date: Sun, 15 May 2022 13:08:26 +0300 Subject: [PATCH 35/48] Fixed polygon parameters not calculating after polygon has been added --- SynthPolygonLayer/SynthPolygonLayer.js | 1 + SynthRectangleBaseLayer/misc.js | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/SynthPolygonLayer/SynthPolygonLayer.js b/SynthPolygonLayer/SynthPolygonLayer.js index 8adf4561..494b64a3 100644 --- a/SynthPolygonLayer/SynthPolygonLayer.js +++ b/SynthPolygonLayer/SynthPolygonLayer.js @@ -298,6 +298,7 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend(/** @lends L.ALS.Sy window.alert(L.ALS.locale.polygonLayersSkipped); this.map.addLayer(this.labelsGroup); // Nothing in the base layer hides or shows it, so it's only hidden in code above + this.calculatePolygonParameters(); this.updatePathsMeta(); this.updateLayersVisibility(); this.writeToHistoryDebounced(); diff --git a/SynthRectangleBaseLayer/misc.js b/SynthRectangleBaseLayer/misc.js index 7447c585..1f0da9d1 100644 --- a/SynthRectangleBaseLayer/misc.js +++ b/SynthRectangleBaseLayer/misc.js @@ -1,7 +1,5 @@ // Misc methods, event handlers, etc which most likely won't change in future -const turfHelpers = require("@turf/helpers"); - L.ALS.SynthRectangleBaseLayer.prototype.calculateParameters = function () { L.ALS.SynthBaseLayer.prototype.calculateParameters.call(this); this.calculatePolygonParameters(); From 73c5bbb2ede5e89da00cff545ba02063f9410e5a Mon Sep 17 00:00:00 2001 From: matafokka Date: Sun, 15 May 2022 13:59:46 +0300 Subject: [PATCH 36/48] Started fixing DEM loading --- ESRIGridParser.js | 4 ++-- GeoTIFFParser.js | 4 +++- SynthPolygonBaseLayer/DEM.js | 12 ++++++++++-- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/ESRIGridParser.js b/ESRIGridParser.js index 7b5f5e19..d116c04b 100644 --- a/ESRIGridParser.js +++ b/ESRIGridParser.js @@ -333,11 +333,11 @@ class ESRIGridParser { [rect.getEast(), rect.getSouth()] ]; } else { - let coords = polygon.getLatLngs(); + let coords = polygon.getLatLngs()[0]; polygonsCoordinates[name] = []; for (let coord of coords) { if (!project) { - polygonsCoordinates.push([coord.lng, coord.lat]); + polygonsCoordinates[name].push([coord.lng, coord.lat]); continue; } diff --git a/GeoTIFFParser.js b/GeoTIFFParser.js index 06c6548d..16c60c58 100644 --- a/GeoTIFFParser.js +++ b/GeoTIFFParser.js @@ -46,13 +46,15 @@ module.exports = async function (file, projectionString, initialData) { let polygon = initialData[name], coords = polygon.length > 2 ? polygon : [ polygon[0], [polygon[1][0], polygon[0][1]], - polygon[1], [polygon[0][0], polygon[1][1]], polygon[0] + polygon[1], [polygon[0][0], polygon[1][1]] ], projPolygon = []; for (let coord of coords) projPolygon.push(projectionFromWGS.forward(coord)); + projPolygon.push([...projPolygon[0]]); // Clone first coordinate to avoid floating point errors + let polygonBbox = bboxPolygon(bbox( turfHelpers.polygon([projPolygon]) )); diff --git a/SynthPolygonBaseLayer/DEM.js b/SynthPolygonBaseLayer/DEM.js index 3271e687..5273f124 100644 --- a/SynthPolygonBaseLayer/DEM.js +++ b/SynthPolygonBaseLayer/DEM.js @@ -30,6 +30,7 @@ L.ALS.SynthPolygonBaseLayer.prototype.onDEMLoad = async function (widget) { try { parser.readChunk(grid); } catch (e) { + console.log(e); this.showDEMError([widget.input.files[0].name]); return; } @@ -146,6 +147,7 @@ L.ALS.SynthPolygonBaseLayer.prototype.onDEMLoadWorker = async function (widget) let stats = await GeoTIFFParser(file, projectionString, ESRIGridParser.getInitialData(this, false)); ESRIGridParser.copyStats(this, stats); } catch (e) { + console.log(e); invalidFiles.push(file.name); } continue; @@ -154,7 +156,10 @@ L.ALS.SynthPolygonBaseLayer.prototype.onDEMLoadWorker = async function (widget) if (!supportsWorker) { await new Promise((resolve) => { ESRIGridParser.parseFile(file, parser, fileReader, () => resolve()) - }).catch(() => invalidFiles.push(file.name)); + }).catch(e => { + console.log(e); + invalidFiles.push(file.name); + }); continue; } @@ -170,7 +175,10 @@ L.ALS.SynthPolygonBaseLayer.prototype.onDEMLoadWorker = async function (widget) projectionString: projectionString, file: file, }); - }).catch(() => invalidFiles.push(file.name)); + }).catch(e => { + console.log(e); + invalidFiles.push(file.name); + }); } return {invalidFiles, invalidProjectionFiles}; From 83817b1b743a4a388a72717bff5c3f581551936d Mon Sep 17 00:00:00 2001 From: matafokka Date: Mon, 16 May 2022 18:55:39 +0300 Subject: [PATCH 37/48] Refactoring and bug fixes - To fix DEMs, I had to remove a good chunk of legacy code. - Fixed GeoTIFF, still working on ESRI Grid. - Added configuration for annoying notifications. - Fixed Rectangle Layer not updating paths after changing parameters. - Fixed Line Layer leaving capture points after deleting. - Hell if I know, this commit grew bigger than I wanted. --- ESRIGridParser.js | 71 ++++----- SynthBaseLayer/SynthBaseLayer.js | 26 ++++ SynthBaseLayer/calculateParameters.js | 7 +- SynthBaseLayer/draw.js | 9 +- SynthGeneralSettings.js | 19 +++ SynthGridLayer/SynthGridLayer.js | 15 +- SynthGridLayer/onMapPan.js | 33 +++-- SynthLineLayer/SynthLineLayer.js | 15 +- SynthLineLayer/SynthLineSettings.js | 2 +- .../SynthPolygonBaseLayer.js | 15 +- SynthPolygonBaseLayer/polygons.js | 135 ++++++++++++------ SynthPolygonBaseLayer/serialization.js | 1 + SynthPolygonLayer/SynthPolygonLayer.js | 38 ++--- SynthRectangleBaseLayer/drawPaths.js | 3 - SynthRectangleBaseLayer/misc.js | 20 ++- SynthRectangleLayer/SynthRectangleLayer.js | 20 +-- locales/English.js | 5 + locales/Russian.js | 5 + main.js | 4 +- 19 files changed, 271 insertions(+), 172 deletions(-) create mode 100644 SynthGeneralSettings.js diff --git a/ESRIGridParser.js b/ESRIGridParser.js index d116c04b..167e797f 100644 --- a/ESRIGridParser.js +++ b/ESRIGridParser.js @@ -119,15 +119,15 @@ class ESRIGridParser { this.polygonsCoordinates = ESRIGridParser.getInitialData(this.layer); } - /** * Reads a chunk * @param chunk {string} A chunk to read */ readChunk(chunk) { for (let i = 0; i < chunk.length; i++) { - let symbol = chunk[i].toLowerCase(); - let isSpace = (symbol === " "), isLineBreak = (symbol === "\n" || symbol === "\r"); + let symbol = chunk[i].toLowerCase(), + isSpace = (symbol === " "), + isLineBreak = (symbol === "\n" || symbol === "\r"); // Skip multiple spaces and line breaks let nameMap = [ @@ -151,8 +151,11 @@ class ESRIGridParser { if (!this.allDEMParamsRead) { // Read param name until we hit space if (this.readingDEMParamName) { - if (symbol === " ") + if (isSpace) { + if (this.param === "") + continue; this.readingDEMParamName = false; + } // Stop reading when we hit minus or digit else if (symbol === "-" || !isNaN(parseInt(symbol))) { this.value = symbol; @@ -219,15 +222,15 @@ class ESRIGridParser { let poly = this.polygonsCoordinates[name], fn, projPoint; if (poly.length > 2) { - fn = MathTools.isPointInPolygon; + fn = "isPointInPolygon"; let {x, y} = this.layer.map.project([point[0], point[1]], 0); projPoint = [x, y]; } else { - fn = MathTools.isPointInRectangle; + fn = "isPointInRectangle"; projPoint = point; } - if (!fn(projPoint, poly)) + if (!MathTools[fn](projPoint, poly)) continue; if (!this.polygonsStats[name]) @@ -235,6 +238,8 @@ class ESRIGridParser { let stats = this.polygonsStats[name]; ESRIGridParser.addToStats(pixelValue, stats); + + L.circleMarker([...point].reverse(), {color: `rgb(${pixelValue},${pixelValue},${pixelValue})`, fillOpacity: 1, stroke: false}).addTo(map); } } else if (!isSpace && !isLineBreak) this.value += symbol; @@ -316,36 +321,36 @@ class ESRIGridParser { /** * Generates initial parameters for the layer. - * @param layer {L.ALS.SynthRectangleBaseLayer} Layer to copy data from + * @param layer {L.ALS.SynthPolygonBaseLayer} Layer to copy data from * @param project {boolean} Whether polygon coordinates should be reprojected, if `layer.useRect` is `false` */ static getInitialData(layer, project = true) { let polygonsCoordinates = {}; - for (let name in layer.polygons) { - if (!layer.polygons.hasOwnProperty(name)) - continue; + layer.forEachValidPolygon(polygon => { + polygon.tempDemName = L.ALS.Helpers.generateID(); - let polygon = layer.polygons[name]; if (layer.useRect) { let rect = polygon.getBounds(); - polygonsCoordinates[name] = [ + polygonsCoordinates[polygon.tempDemName] = [ [rect.getWest(), rect.getNorth()], [rect.getEast(), rect.getSouth()] ]; - } else { - let coords = polygon.getLatLngs()[0]; - polygonsCoordinates[name] = []; - for (let coord of coords) { - if (!project) { - polygonsCoordinates[name].push([coord.lng, coord.lat]); - continue; - } + return; + } - let {x, y} = layer.map.project(coord, 0); - polygonsCoordinates[name].push([x, y]); + let coords = polygon.getLatLngs()[0], coordsCopy = []; + polygonsCoordinates[polygon.tempDemName] = coordsCopy; + for (let coord of coords) { + if (!project) { + coordsCopy.push([coord.lng, coord.lat]); + continue; } + + let {x, y} = layer.map.project(coord, 0); + coordsCopy.push([x, y]); } - } + }); + return polygonsCoordinates; } @@ -355,14 +360,16 @@ class ESRIGridParser { * @param stats {Object} Stats from any ESRIGridParser */ static copyStats(layer, stats) { - for (let name in stats) { - let widgetable = layer.polygonsWidgets[name]; - let s = stats[name]; - s.mean = s.sum / s.count; - widgetable.getWidgetById("minHeight").setValue(s.min); - widgetable.getWidgetById("maxHeight").setValue(s.max); - widgetable.getWidgetById("meanHeight").setValue(s.mean); - } + layer.forEachValidPolygon(polygon => { + let entry = stats[polygon.tempDemName]; + if (!entry) + return; + + entry.mean = entry.sum / entry.count; + polygon.widgetable.getWidgetById("minHeight").setValue(entry.min); + polygon.widgetable.getWidgetById("maxHeight").setValue(entry.max); + polygon.widgetable.getWidgetById("meanHeight").setValue(entry.mean); + }); layer.calculateParameters(); } diff --git a/SynthBaseLayer/SynthBaseLayer.js b/SynthBaseLayer/SynthBaseLayer.js index ae72eb23..57012224 100644 --- a/SynthBaseLayer/SynthBaseLayer.js +++ b/SynthBaseLayer/SynthBaseLayer.js @@ -587,6 +587,32 @@ L.ALS.SynthBaseLayer = L.ALS.Layer.extend(/** @lends L.ALS.SynthBaseLayer.protot return hide; }, + /** + * Displays a notification after layers has been edited + * @param invalidLayersMessage {string} Message to display when layers has been invalidated + * @param layersInvalidated {boolean} Whether layers has been invalidated + * @param e {Event|undefined} Received L.Draw event, if present + * @param shouldJustReturn {boolean} If this function should just return instead of displaying a notification + */ + notifyAfterEditing: function (invalidLayersMessage, layersInvalidated, e = undefined, shouldJustReturn = false) { + // The whole thing makes no sense when polygons are invalidated when user edits parameters. + // TODO: Somehow fix this? + + if (!L.ALS.generalSettings.notificationsEnabled || shouldJustReturn) + return; + + let notification = ""; + + if (layersInvalidated) + notification += invalidLayersMessage + "\n\n"; + + if (e && e.type === "draw:editstop") + notification += L.ALS.locale.afterEditingInvalidDEMValues + "\n\n"; + + if (notification !== "") + window.alert(notification + L.ALS.locale.afterEditingToDisableNotifications); + }, + clearSerializedPathsWidgets: function (serialized) { for (let i = 1; i <= this._pathsWidgetsNumber; i++) delete serialized._widgets["pathWidget" + i]; diff --git a/SynthBaseLayer/calculateParameters.js b/SynthBaseLayer/calculateParameters.js index 5b9799df..889292ab 100644 --- a/SynthBaseLayer/calculateParameters.js +++ b/SynthBaseLayer/calculateParameters.js @@ -1,6 +1,6 @@ const turfHelpers = require("@turf/helpers"); -L.ALS.SynthBaseLayer.prototype.calculateParameters = function () { +L.ALS.SynthBaseLayer.prototype.calculateParameters = function (notifyIfLayersSkipped = false) { let parameters = ["cameraWidth", "cameraHeight", "pixelWidth", "focalLength", "imageScale", "overlayBetweenPaths", "overlayBetweenImages", "aircraftSpeed"]; for (let param of parameters) @@ -54,5 +54,8 @@ L.ALS.SynthBaseLayer.prototype.calculateParameters = function () { this.getWidgetById(name).setValue(value); } - this.writeToHistoryDebounced(); + if (this.onEditEndDebounced) + this.onEditEndDebounced(typeof notifyIfLayersSkipped === "boolean" ? notifyIfLayersSkipped : false); + else + this.writeToHistoryDebounced(); } \ No newline at end of file diff --git a/SynthBaseLayer/draw.js b/SynthBaseLayer/draw.js index f1334f68..de62506c 100644 --- a/SynthBaseLayer/draw.js +++ b/SynthBaseLayer/draw.js @@ -1,3 +1,5 @@ +const debounce = require("debounce"); + /** * Enables L.Draw on this layer * @param drawControls {Object} L.Draw controls to use @@ -22,6 +24,8 @@ L.ALS.SynthBaseLayer.prototype.enableDraw = function (drawControls, drawingGroup } } + this.onEditEndDebounced = debounce((notifyIfLayersSkipped = false) => this.onEditEnd(undefined, notifyIfLayersSkipped), 300); // Math operations are too slow for immediate update + for (let control of this._drawTypes) this._drawOptions.draw[control] = false; @@ -47,7 +51,10 @@ L.ALS.SynthBaseLayer.prototype.onDraw = function (e) { if (!this.isSelected) return; - // Don't add layers of size less than 3x3 px + // Don't add layers of size less than 3x3 px and don't add geodesics with one point + if (e.layer instanceof L.Geodesic && e.layer.getLatLngs().length < 2) + return; + if (e.layer.getBounds) { let {_northEast, _southWest} = e.layer.getBounds(), zoom = this.map.getZoom(), diff --git a/SynthGeneralSettings.js b/SynthGeneralSettings.js new file mode 100644 index 00000000..353e0044 --- /dev/null +++ b/SynthGeneralSettings.js @@ -0,0 +1,19 @@ +/** + * Whether SynthFlight should notify user when DEM is loaded or map objects are edited + * @type {boolean} + */ +L.ALS.generalSettings.notificationsEnabled = true; + +L.ALS.SynthGeneralSettings = L.ALS.GeneralSettings.extend({ + initialize: function (defaultLocale) { + L.ALS.GeneralSettings.prototype.initialize.call(this, defaultLocale); + this.removeWidget("notify"); + this.addWidget(new L.ALS.Widgets.Checkbox("notify", "generalSettingsDisableAnnoyingNotification", this, "_changeNotifications"), false); + }, + + _changeNotifications: function (widget) { + let value = !widget.getValue(); + L.ALS.generalSettings.notifyWhenLongRunningOperationComplete = value; + L.ALS.generalSettings.notificationsEnabled = value; + } +}) \ No newline at end of file diff --git a/SynthGridLayer/SynthGridLayer.js b/SynthGridLayer/SynthGridLayer.js index 60926d42..6726a99d 100644 --- a/SynthGridLayer/SynthGridLayer.js +++ b/SynthGridLayer/SynthGridLayer.js @@ -56,6 +56,8 @@ L.ALS.SynthGridLayer = L.ALS.SynthRectangleBaseLayer.extend(/** @lends L.ALS.Syn */ this.gridLabelsIDs = []; + this.polygons = {}; + L.ALS.SynthRectangleBaseLayer.prototype.init.call(this, wizardResults, settings); /** @@ -77,9 +79,13 @@ L.ALS.SynthGridLayer = L.ALS.SynthRectangleBaseLayer.extend(/** @lends L.ALS.Syn if (this.polygons[name]) { polygon.setStyle({fill: false}); + delete this.polygons[name]; this.removePolygon(polygon); - } else + } else { + polygon.setStyle({fill: true}); + this.polygons[name] = polygon; this.addPolygon(polygon); + } this.calculateParameters(); this.writeToHistoryDebounced(); @@ -129,6 +135,13 @@ L.ALS.SynthGridLayer = L.ALS.SynthRectangleBaseLayer.extend(/** @lends L.ALS.Syn errorLabel.setValue(""); L.ALS.SynthRectangleBaseLayer.prototype.drawPaths.call(this); + + this.updatePathsMeta(); + }, + + forEachValidPolygon: function (cb) { + for (let id in this.polygons) + cb(this.polygons[id]); }, statics: { diff --git a/SynthGridLayer/onMapPan.js b/SynthGridLayer/onMapPan.js index fc66ab45..786198c2 100644 --- a/SynthGridLayer/onMapPan.js +++ b/SynthGridLayer/onMapPan.js @@ -10,7 +10,11 @@ L.ALS.SynthGridLayer.prototype._onMapPan = function () { if (!this.isShown || !this.isDisplayed) return; - this.polygonGroup.clearLayers(); + this.polygonGroup.eachLayer(polygon => { + if (!polygon.options.fill) + this.polygonGroup.removeLayer(polygon); + }); + this.clearLabels("gridLabelsIDs"); // Get viewport bounds and calculate correct start and end coords for lng and lat @@ -65,20 +69,19 @@ L.ALS.SynthGridLayer.prototype._onMapPan = function () { // If this polygon has been selected, we should fill it and replace it in the array. // Because fill will be changed, we can't keep old polygon, it's easier to just replace it let name = this._generatePolygonName(polygon); - let isSelected = this.polygons[name] !== undefined; - polygon.setStyle({ - color: this.borderColor, - fillColor: this.fillColor, - fill: isSelected, - weight: this.lineThicknessValue - }); - - // We should select or deselect polygons upon double click - this.addEventListenerTo(polygon, "dblclick contextmenu", "_selectOrDeselectPolygon"); - this.polygonGroup.addLayer(polygon); - - if (isSelected) - this.polygons[name] = polygon; + + if (!this.polygons[name]) { + polygon.setStyle({ + color: this.borderColor, + fillColor: this.fillColor, + fill: false, + weight: this.lineThicknessValue + }); + + // We should select or deselect polygons upon double click + this.addEventListenerTo(polygon, "dblclick contextmenu", "_selectOrDeselectPolygon"); + this.polygonGroup.addLayer(polygon); + } // Generate current polygon's name if grid uses one of standard scales if (this._currentStandardScale === Infinity) { diff --git a/SynthLineLayer/SynthLineLayer.js b/SynthLineLayer/SynthLineLayer.js index bd141adc..5be20b3a 100644 --- a/SynthLineLayer/SynthLineLayer.js +++ b/SynthLineLayer/SynthLineLayer.js @@ -19,9 +19,10 @@ L.ALS.SynthLineLayer = L.ALS.SynthBaseLayer.extend(/** @lends L.ALS.SynthLineLay this.drawingGroup = new L.FeatureGroup(); this.connectionsGroup = new L.FeatureGroup(); this.errorGroup = new L.FeatureGroup(); + this.pointsGroup = new L.FeatureGroup(); L.ALS.SynthBaseLayer.prototype.init.call(this, settings, this.pathsGroup, this.connectionsGroup, "lineLayerColor"); - this.addLayers(this.errorGroup); + this.addLayers(this.errorGroup, this.pointsGroup); this.enableDraw({ polyline: { @@ -40,8 +41,6 @@ L.ALS.SynthLineLayer = L.ALS.SynthBaseLayer.extend(/** @lends L.ALS.SynthLineLay this.addBaseParametersInputSection(); this.addBaseParametersOutputSection(); - - this.pointsGroup = new L.FeatureGroup(); L.ALS.SynthGeometryBaseWizard.initializePolygonOrPolylineLayer(this, wizardResults); }, @@ -63,7 +62,7 @@ L.ALS.SynthLineLayer = L.ALS.SynthBaseLayer.extend(/** @lends L.ALS.SynthLineLay this.map.addLayer(this.drawingGroup); }, - onEditEnd: function (notifyIfLayersSkipped = true) { + onEditEnd: function (event, notifyIfLayersSkipped = true) { if (!this.isSelected) return; @@ -129,17 +128,11 @@ L.ALS.SynthLineLayer = L.ALS.SynthBaseLayer.extend(/** @lends L.ALS.SynthLineLay this.map.removeLayer(this.drawingGroup); this.map.addLayer(this.pathsGroup); - if (linesWereInvalidated && notifyIfLayersSkipped) - window.alert(L.ALS.locale.lineLayersSkipped); + this.notifyAfterEditing(L.ALS.locale.lineLayersSkipped, linesWereInvalidated, undefined, !notifyIfLayersSkipped); this.writeToHistoryDebounced(); }, - calculateParameters: function (notifyIfLayersSkipped = false) { - L.ALS.SynthBaseLayer.prototype.calculateParameters.call(this); - this.onEditEnd(typeof notifyIfLayersSkipped === "boolean" ? notifyIfLayersSkipped : false); - }, - toGeoJSON: function () { let pathsMeta = {}; for (let prop of this.propertiesToExport) { diff --git a/SynthLineLayer/SynthLineSettings.js b/SynthLineLayer/SynthLineSettings.js index 9d12dd44..09bee9b7 100644 --- a/SynthLineLayer/SynthLineSettings.js +++ b/SynthLineLayer/SynthLineSettings.js @@ -2,7 +2,7 @@ L.ALS.SynthLineSettings = L.ALS.SynthBaseSettings.extend({ initialize: function () { L.ALS.SynthBaseSettings.prototype.initialize.call(this); - const color = "#ff0000"; + const color = "#d900ff"; this.addWidget(new L.ALS.Widgets.Color("color0", "settingsLineLayerColor").setValue(color), color); } diff --git a/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js b/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js index ee6f2b5a..381ef5cc 100644 --- a/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js +++ b/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js @@ -41,9 +41,6 @@ L.ALS.SynthPolygonBaseLayer = L.ALS.SynthBaseLayer.extend( /** @lends L.ALS.Synt colorLabel2 = "meridiansColor", hidePaths2WidgetId = "hidePathsByMeridians", ) { - this.polygons = {}; - this.invalidPolygons = {}; - this.polygonsWidgets = {}; this.serializationIgnoreList.push("polygons", "invalidPolygons", "lngDistance", "latDistance", "_currentStandardScale"); this.polygonGroup = new L.FeatureGroup(); @@ -198,8 +195,7 @@ L.ALS.SynthPolygonBaseLayer = L.ALS.SynthBaseLayer.extend( /** @lends L.ALS.Synt updatePolygonsColors: function () { let color = this.getWidgetById("borderColor").getValue(), fillColor = this.getWidgetById("fillColor").getValue(); - for (let id in this.polygons) - this.polygons[id].setStyle({color, fillColor}); + this.forEachValidPolygon(polygon => polygon.setStyle({color, fillColor})); }, clearPaths: function () { @@ -237,11 +233,8 @@ L.ALS.SynthPolygonBaseLayer = L.ALS.SynthBaseLayer.extend( /** @lends L.ALS.Synt baseFeaturesToGeoJSON: function () { let jsons = []; - for (let name in this.polygons) { - if (!this.polygons.hasOwnProperty(name)) - continue; - let polygon = this.polygons[name], - polygonJson = polygon.toGeoJSON(), + this.forEachValidPolygon(polygon => { + let polygonJson = polygon.toGeoJSON(), props = ["polygonName", "minHeight", "maxHeight", "meanHeight", "absoluteHeight", "reliefType", "elevationDifference", "latCellSizeInMeters", "lngCellSizeInMeters"]; for (let prop of props) { let value = polygon[prop]; @@ -250,7 +243,7 @@ L.ALS.SynthPolygonBaseLayer = L.ALS.SynthBaseLayer.extend( /** @lends L.ALS.Synt } polygonJson.properties.name = "Selected cell"; jsons.push(polygonJson); - } + }); let airport = this._airportMarker.toGeoJSON(); airport.name = "Airport"; diff --git a/SynthPolygonBaseLayer/polygons.js b/SynthPolygonBaseLayer/polygons.js index d85fcf42..a5c3d9fb 100644 --- a/SynthPolygonBaseLayer/polygons.js +++ b/SynthPolygonBaseLayer/polygons.js @@ -6,8 +6,8 @@ */ L.ALS.MeanHeightButtonHandler = L.ALS.Serializable.extend( /**@lends L.ALS.MeanHeightButtonHandler.prototype */ { - initialize: function (controlsContainer) { - this._widgetable = controlsContainer; + initialize: function (widgetable) { + this._widgetable = widgetable; }, handle: function () { @@ -18,12 +18,11 @@ L.ALS.MeanHeightButtonHandler = L.ALS.Serializable.extend( /**@lends L.ALS.MeanH }) L.ALS.SynthPolygonBaseLayer.prototype.addPolygon = function (polygon, alignToCenter = false) { - polygon._intName = this._generatePolygonName(polygon); - delete this.invalidPolygons[polygon._intName]; - + // Make polygon valid polygon.setStyle({fill: true}); - this.polygons[polygon._intName] = polygon; + polygon.isValid = true; + // Get anchor and anchor coordinates let anchorPoint, anchor; if (alignToCenter) { anchorPoint = polygon.getBounds().getCenter(); @@ -33,12 +32,20 @@ L.ALS.SynthPolygonBaseLayer.prototype.addPolygon = function (polygon, alignToCen anchor = "topLeft"; } - let controlsContainer = new L.WidgetLayer(anchorPoint, anchor), handler = new L.ALS.MeanHeightButtonHandler(controlsContainer); + if (polygon.widgetable) { + polygon.widgetable.setLatLng(anchorPoint); + return; + } + + polygon.widgetable = new L.WidgetLayer(anchorPoint, anchor); + polygon.widgetable.polygon = polygon; + + let handler = new L.ALS.MeanHeightButtonHandler(polygon.widgetable); if (this.useZoneNumbers) - controlsContainer.addWidget(new L.ALS.Widgets.Number("zoneNumber", "zoneNumber", this, "calculatePolygonParameters").setMin(1).setValue(1)); + polygon.widgetable.addWidget(new L.ALS.Widgets.Number("zoneNumber", "zoneNumber", this, "calculatePolygonParameters").setMin(1).setValue(1)); - controlsContainer.addWidgets( + polygon.widgetable.addWidgets( new L.ALS.Widgets.Number("minHeight", "minHeight", this, "calculatePolygonParameters").setMin(1).setValue(1), new L.ALS.Widgets.Number("maxHeight", "maxHeight", this, "calculatePolygonParameters").setMin(1).setValue(1), new L.ALS.Widgets.Number("meanHeight", "meanHeight", this, "calculatePolygonParameters").setMin(1).setValue(1), @@ -50,7 +57,7 @@ L.ALS.SynthPolygonBaseLayer.prototype.addPolygon = function (polygon, alignToCen ); if (this.calculateCellSizeForPolygons) { - controlsContainer.addWidgets( + polygon.widgetable.addWidgets( new L.ALS.Widgets.ValueLabel("lngCellSizeInMeters", "lngCellSizeInMeters", "m").setNumberOfDigitsAfterPoint(0), new L.ALS.Widgets.ValueLabel("latCellSizeInMeters", "latCellSizeInMeters", "m").setNumberOfDigitsAfterPoint(0), ) @@ -58,53 +65,101 @@ L.ALS.SynthPolygonBaseLayer.prototype.addPolygon = function (polygon, alignToCen let toFormatNumbers = ["absoluteHeight", "elevationDifference", "lngCellSizeInMeters", "latCellSizeInMeters"]; for (let id of toFormatNumbers) { - let widget = controlsContainer.getWidgetById(id); + let widget = polygon.widgetable.getWidgetById(id); if (widget) widget.setFormatNumbers(true); } - this.polygonsWidgets[polygon._intName] = controlsContainer; - this.widgetsGroup.addLayer(controlsContainer); + this.widgetsGroup.addLayer(polygon.widgetable); if (polygon.linkedLayer) polygon.linkedLayer.setStyle(polygon.options); } -L.ALS.SynthPolygonBaseLayer.prototype.removePolygon = function (polygon, removeFromObject = true) { - let name = polygon._intName || this._generatePolygonName(polygon); - if (removeFromObject) - delete this.polygons[name]; - this.widgetsGroup.removeLayer(this.polygonsWidgets[name]); - delete this.polygonsWidgets[name]; +/** + * Removes polygon, its widget and linked polygon from the map + * @param polygon {L.Layer} Polygon to remove + */ +L.ALS.SynthPolygonBaseLayer.prototype.removePolygon = function (polygon) { + for (let layer of [polygon, polygon.linkedLayer]) { + if (!layer) + continue; + + this.polygonGroup.removeLayer(layer); + if (!layer.widgetable) + continue; + + this._removePolygonWidget(layer); + } } -L.ALS.SynthPolygonBaseLayer.prototype.calculatePolygonParameters = function (widget) { - for (let name in this.polygons) { - if (!this.polygons.hasOwnProperty(name)) +L.ALS.SynthPolygonBaseLayer.prototype.invalidatePolygon = function (polygon) { + for (let layer of [polygon, polygon.linkedLayer]) { + if (!layer) continue; - let layer = this.polygons[name], latLngs = layer.getLatLngs()[0]; - let widgetContainer = this.polygonsWidgets[name]; + layer.setStyle({color: "red", fillColor: "red"}); + layer.isValid = false; + + this._removePolygonWidget(layer); + } +} + +L.ALS.SynthPolygonBaseLayer.prototype._removePolygonWidget = function (polygon) { + if (!polygon.widgetable) + return; + + this.widgetsGroup.removeLayer(polygon.widgetable); + polygon.widgetable.remove(); + delete polygon.widgetable; +} + +/** + * Removes widgets that are hanging on the map after polygons have been removed + */ +L.ALS.SynthPolygonBaseLayer.prototype.removeLeftoverWidgets = function () { + this.widgetsGroup.eachLayer(layer => { + if (layer.polygon && !this.polygonGroup.hasLayer(layer.polygon)) + this.widgetsGroup.removeLayer(layer); + }); +} + +L.ALS.SynthPolygonBaseLayer.prototype.afterEditEnd = function (invalidLayersMessage, layersInvalidated, e = undefined, shouldJustReturn = false) { + this.notifyAfterEditing(invalidLayersMessage, layersInvalidated, e, shouldJustReturn); + + this.removeLeftoverWidgets(); + this.map.addLayer(this.labelsGroup); // Nothing in the base layer hides or shows it, so it's only hidden in code above + this.updatePolygonsColors(); + this.calculatePolygonParameters(); + this.updatePathsMeta(); + this.updateLayersVisibility(); + this.writeToHistoryDebounced(); +} + +L.ALS.SynthPolygonBaseLayer.prototype.calculatePolygonParameters = function () { + this.forEachValidPolygon(layer => { + + let latLngs = layer.getLatLngs()[0]; if (this.calculateCellSizeForPolygons) { layer.lngCellSizeInMeters = this.getParallelOrMeridianLineLength(latLngs[0], latLngs[1], false); layer.latCellSizeInMeters = this.getParallelOrMeridianLineLength(latLngs[1], latLngs[2], false); - widgetContainer.getWidgetById("lngCellSizeInMeters").setValue(layer.lngCellSizeInMeters); - widgetContainer.getWidgetById("latCellSizeInMeters").setValue(layer.latCellSizeInMeters); + layer.widgetable.getWidgetById("lngCellSizeInMeters").setValue(layer.lngCellSizeInMeters); + layer.widgetable.getWidgetById("latCellSizeInMeters").setValue(layer.latCellSizeInMeters); } - layer.minHeight = widgetContainer.getWidgetById("minHeight").getValue(); - layer.maxHeight = widgetContainer.getWidgetById("maxHeight").getValue(); + layer.minHeight = layer.widgetable.getWidgetById("minHeight").getValue(); + layer.maxHeight = layer.widgetable.getWidgetById("maxHeight").getValue(); - let errorLabel = widgetContainer.getWidgetById("error"); + let errorLabel = layer.widgetable.getWidgetById("error"); if (layer.minHeight > layer.maxHeight) { errorLabel.setValue("errorMinHeightBiggerThanMaxHeight"); - continue; + return; } errorLabel.setValue(""); - layer.meanHeight = widgetContainer.getWidgetById("meanHeight").getValue(); + layer.meanHeight = layer.widgetable.getWidgetById("meanHeight").getValue(); layer.absoluteHeight = this["flightHeight"] + layer.meanHeight; layer.elevationDifference = (layer.maxHeight - layer.minHeight) / this["flightHeight"]; @@ -118,20 +173,16 @@ L.ALS.SynthPolygonBaseLayer.prototype.calculatePolygonParameters = function (wid } catch (e) { value = layer[name]; } - widgetContainer.getWidgetById(name).setValue(value); + layer.widgetable.getWidgetById(name).setValue(value); } - } + }) } -L.ALS.SynthPolygonBaseLayer.prototype.invalidatePolygon = function (polygon) { - polygon._intName = this._generatePolygonName(polygon); - - for (let poly of [polygon, polygon.linkedLayer]) { - if (poly) - poly.setStyle({color: "red", fillColor: "red"}); - } - this.invalidPolygons[polygon._intName] = polygon; - delete this.polygons[polygon._intName]; +L.ALS.SynthPolygonBaseLayer.prototype.forEachValidPolygon = function (cb) { + this.polygonGroup.eachLayer(poly => { + if (!poly.isCloned && poly.isValid) + cb(poly); + }) } L.ALS.SynthPolygonBaseLayer.prototype._getRectOrPolyCoords = function (layer) { diff --git a/SynthPolygonBaseLayer/serialization.js b/SynthPolygonBaseLayer/serialization.js index ae668294..03d07510 100644 --- a/SynthPolygonBaseLayer/serialization.js +++ b/SynthPolygonBaseLayer/serialization.js @@ -1,6 +1,7 @@ L.ALS.SynthPolygonBaseLayer.prototype.serialize = function (seenObjects) { let serialized = this.getObjectToSerializeTo(seenObjects); + // TODO: Fix serialization serialized.polygonsWidgets = L.ALS.Serializable.serializeAnyObject(this.polygonsWidgets, seenObjects); // Gather selected polygons' coordinates diff --git a/SynthPolygonLayer/SynthPolygonLayer.js b/SynthPolygonLayer/SynthPolygonLayer.js index 494b64a3..ea371fc3 100644 --- a/SynthPolygonLayer/SynthPolygonLayer.js +++ b/SynthPolygonLayer/SynthPolygonLayer.js @@ -2,7 +2,6 @@ require("./SynthPolygonWizard.js"); require("./SynthPolygonSettings.js"); const MathTools = require("../MathTools.js"); const proj4 = require("proj4"); -const debounce = require("debounce") /** * Polygon layer @@ -57,30 +56,21 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend(/** @lends L.ALS.Sy }, this.polygonGroup); this.calculateThreshold(settings); // Update hiding threshold - this.onEditEndDebounced = debounce((notifyIfLayersSkipped = false) => this.onEditEnd(notifyIfLayersSkipped), 300); // Math operations are too slow for immediate update L.ALS.SynthGeometryBaseWizard.initializePolygonOrPolylineLayer(this, wizardResults); }, - onEditEnd: function (notifyIfLayersSkipped = true) { + onEditEnd: function (e, notifyIfLayersSkipped = true) { if (!this.isSelected) return; - let color = this.getWidgetById("color0").getValue(), - lineOptions = {color, thickness: this.lineThicknessValue, segmentsNumber: L.GEODESIC_SEGMENTS}, - calculationsLineOptions = {segmentsNumber: 2} - - for (let name in this.polygons) - this.removePolygon(this.polygons[name], false); - this.labelsGroup.deleteAllLabels(); - this.polygons = {} - this.invalidPolygons = {}; - this.clearPaths(); - let layersWereInvalidated = false; - notifyIfLayersSkipped = typeof notifyIfLayersSkipped === "boolean" ? notifyIfLayersSkipped : true; + let color = this.getWidgetById("color0").getValue(), + lineOptions = {color, thickness: this.lineThicknessValue, segmentsNumber: L.GEODESIC_SEGMENTS}, + calculationsLineOptions = {segmentsNumber: 2}, + layersWereInvalidated = false; // Build paths for each polygon. @@ -102,10 +92,10 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend(/** @lends L.ALS.Sy // To work with geodesics as vectors and lines, we'll use gnomonic projection. // We'll also crop the paths by the hull, so there won't be empty space along the paths. - this.polygonGroup.eachLayer((layer) => { + this.polygonGroup.eachLayer(layer => { // Remove a linked layer when a layer either original or cloned has been removed if (layer.linkedLayer && !this.polygonGroup.hasLayer(layer.linkedLayer)) { - this.polygonGroup.removeLayer(layer); + this.removePolygon(layer); return; } @@ -294,19 +284,7 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend(/** @lends L.ALS.Sy this.pointsGroup.addLayer(marker); }); - if (layersWereInvalidated && notifyIfLayersSkipped) - window.alert(L.ALS.locale.polygonLayersSkipped); - - this.map.addLayer(this.labelsGroup); // Nothing in the base layer hides or shows it, so it's only hidden in code above - this.calculatePolygonParameters(); - this.updatePathsMeta(); - this.updateLayersVisibility(); - this.writeToHistoryDebounced(); - }, - - calculateParameters: function (notifyIfLayersSkipped = false) { - L.ALS.SynthPolygonBaseLayer.prototype.calculateParameters.call(this); - this.onEditEndDebounced(typeof notifyIfLayersSkipped === "boolean" ? notifyIfLayersSkipped : false); + this.afterEditEnd(L.ALS.locale.polygonLayersSkipped, layersWereInvalidated, e, !notifyIfLayersSkipped); }, updateLayersVisibility: function () { diff --git a/SynthRectangleBaseLayer/drawPaths.js b/SynthRectangleBaseLayer/drawPaths.js index 775976b0..b31f1597 100644 --- a/SynthRectangleBaseLayer/drawPaths.js +++ b/SynthRectangleBaseLayer/drawPaths.js @@ -9,14 +9,11 @@ L.ALS.SynthRectangleBaseLayer.prototype.clearPaths = function () { } L.ALS.SynthRectangleBaseLayer.prototype.drawPaths = function () { - this.clearPaths(); - if (this.mergedPolygons.length === 0) return; this.drawPathsWorker(true); this.drawPathsWorker(false); - this.updatePathsMeta(); this.labelsGroup.redraw(); } diff --git a/SynthRectangleBaseLayer/misc.js b/SynthRectangleBaseLayer/misc.js index 1f0da9d1..c471f4e9 100644 --- a/SynthRectangleBaseLayer/misc.js +++ b/SynthRectangleBaseLayer/misc.js @@ -1,8 +1,10 @@ // Misc methods, event handlers, etc which most likely won't change in future -L.ALS.SynthRectangleBaseLayer.prototype.calculateParameters = function () { - L.ALS.SynthBaseLayer.prototype.calculateParameters.call(this); - this.calculatePolygonParameters(); +L.ALS.SynthRectangleBaseLayer.prototype.calculateParameters = function (notifyIfLayersSkipped = false) { + L.ALS.SynthBaseLayer.prototype.calculateParameters.call(this, notifyIfLayersSkipped); + + if (!this.onEditEndDebounced) + this.calculatePolygonParameters(); } L.ALS.SynthRectangleBaseLayer.prototype.updateLayersVisibility = function () { @@ -55,13 +57,17 @@ L.ALS.SynthRectangleBaseLayer.prototype.calculatePolygonParameters = function (w L.ALS.SynthRectangleBaseLayer.prototype.mergePolygons = function () { // Convert object with polygons to an array and push edges instead of points here this.mergedPolygons = []; - for (let id in this.polygons) { - let latLngs = this.polygons[id].getLatLngs()[0], poly = []; + + this.forEachValidPolygon(polygon => { + let latLngs = polygon.getLatLngs()[0], poly = []; for (let p of latLngs) poly.push([p.lng, p.lat]); + poly.push(poly[0]); // We need to close the polygons to use MathTools stuff + if (this.useZoneNumbers) - poly.zoneNumber = this.polygonsWidgets[id].getWidgetById("zoneNumber").getValue(); + poly.zoneNumber = polygon.widgetable.getWidgetById("zoneNumber").getValue(); + this.mergedPolygons.push(poly); - } + }); } \ No newline at end of file diff --git a/SynthRectangleLayer/SynthRectangleLayer.js b/SynthRectangleLayer/SynthRectangleLayer.js index cbcba7af..a0199d33 100644 --- a/SynthRectangleLayer/SynthRectangleLayer.js +++ b/SynthRectangleLayer/SynthRectangleLayer.js @@ -27,22 +27,18 @@ L.ALS.SynthRectangleLayer = L.ALS.SynthRectangleBaseLayer.extend(/** @lends L.AL this.isAfterDeserialization = false; }, - onEditEnd: function () { + onEditEnd: function (e, notifyIfLayersSkipped = true) { if (!this.isSelected) return; - for (let name in this.polygons) - this.removePolygon(this.polygons[name], false); - - this.polygons = {} - this.invalidPolygons = {}; + this.clearPaths(); let layersWereInvalidated = false; - this.polygonGroup.eachLayer((layer) => { + this.polygonGroup.eachLayer(layer => { // Remove a linked layer when a layer either original or cloned has been removed if (layer.linkedLayer && !this.polygonGroup.hasLayer(layer.linkedLayer)) { - this.polygonGroup.removeLayer(layer); + this.removePolygon(layer); return; } @@ -66,13 +62,7 @@ L.ALS.SynthRectangleLayer = L.ALS.SynthRectangleBaseLayer.extend(/** @lends L.AL this.addPolygon(layer); }); - if (layersWereInvalidated) - window.alert(L.ALS.locale.rectangleLayersSkipped); - - this.map.addLayer(this.labelsGroup); // Nothing in the base layer hides or shows it, so it's only hidden in code above - this.updateLayersVisibility(); - this.calculateParameters(); - this.writeToHistoryDebounced(); + this.afterEditEnd(L.ALS.locale.rectangleLayersSkipped, layersWereInvalidated, e, !notifyIfLayersSkipped); }, statics: { diff --git a/locales/English.js b/locales/English.js index de1fd0e5..f3c1a119 100644 --- a/locales/English.js +++ b/locales/English.js @@ -161,6 +161,11 @@ L.ALS.Locales.addLocaleProperties("English", { polygonHidePaths: "Hide paths", polygonLayersSkipped: "One or more polygons has been skipped because they're too big. These polygons have red color.", + // Notifications after editing + afterEditingInvalidDEMValues: "Height values might be invalid because map objects has been edited. Please, reload DEM or edit height values manually.", + afterEditingToDisableNotifications: "To disable these notification, go to Settings - General Settings - Disable all annoying notifications after editing and DEM loading.", + generalSettingsDisableAnnoyingNotification: "Disable all annoying notifications after editing and DEM loading", + // GeoJSON initial features initialFeaturesFileLabelPolygon: "Load initial polygons from zipped shapefile or GeoJSON (non-polygon features will be skipped):", initialFeaturesFileLabelLine: "Load initial polylines from zipped shapefile or GeoJSON (non-polyline features will be skipped):", diff --git a/locales/Russian.js b/locales/Russian.js index f14361e5..929d6163 100644 --- a/locales/Russian.js +++ b/locales/Russian.js @@ -155,7 +155,12 @@ L.ALS.Locales.addLocaleProperties("Русский", { polygonLayerName: "Слой Полигонов", polygonPathsColor: "Цвет маршрутов:", polygonHidePaths: "Скрыть маршруты", + + // Notifications after editing polygonLayersSkipped: "Один или несколько полигонов были пропущены, так как они слишком большие. Данные полигоны имеют красный цвет.", + afterEditingInvalidDEMValues: "Значения высот могут быть неправильными, поскольку объекты были отредактированы. Пожалуйста, заново загрузите ЦМР или вручную отредактируйте значения высот.", + afterEditingToDisableNotifications: "Чтобы убрать данные уведомления, перейдите в Настройки - Общие настройки - Отключить все надоедливые уведомления после редактирования и загрузки ЦМР.", + generalSettingsDisableAnnoyingNotification: "Отключить все надоедливые уведомления после редактирования и загрузки ЦМР", // GeoJSON initial features initialFeaturesFileLabelPolygon: "Загрузить исходные полигоны из сжатого shapefile (zip-архива) или GeoJSON (типы, отличные от полигона, будут пропущены):", diff --git a/main.js b/main.js index 86564715..05146d88 100644 --- a/main.js +++ b/main.js @@ -25,6 +25,7 @@ L.ALS.Locales.AdditionalLocales.Russian(); require("./node_modules/leaflet.coordinates/dist/Leaflet.Coordinates-0.1.5.min.js"); require("./locales/English.js"); require("./locales/Russian.js"); +require("./SynthGeneralSettings.js"); require("./SynthGeometryBaseWizard.js"); require("./SynthGeometryLayer/SynthGeometryLayer.js"); require("./SynthBaseLayer/SynthBaseLayer.js"); @@ -62,7 +63,7 @@ let map = L.map("map", { keyboard: false, worldCopyJump: true, fadeAnimation: false -}).setView([51.505, -0.09], 13); +}).setView([55.75, 37.61], 13); map.doubleClickZoom.disable(); @@ -162,6 +163,7 @@ let layerSystem = new L.ALS.System(map, { makeMapFullscreen: true, historySize: L.ALS.Helpers.supportsFlexbox ? 40 : 20, // Old browsers might have lower RAM limits toolbarZoomControl: new L.ALS.ControlZoom({vertical: true}), + generalSettings: L.ALS.SynthGeneralSettings }); // CartoDB From 36c5d72b327e2c08055d7ed0bf1bef142604e21f Mon Sep 17 00:00:00 2001 From: matafokka Date: Tue, 17 May 2022 14:30:18 +0300 Subject: [PATCH 38/48] Just saving progress on DEM fixes - Removed unnecessary code. - Optimized rectangles in Polygon Layer and when loading GeoTIFF. - Now all point in polygon checks are done in WebMercator. I'm actually planning to move the whole thing to use WebMercator which will lead to some performance improvements and reduce code complexity. --- ESRIGridParser.js | 32 ++++++++-------- GeoTIFFParser.js | 38 ++++++++++++++----- .../SynthPolygonBaseLayer.js | 1 - .../SynthRectangleBaseLayer.js | 1 - 4 files changed, 45 insertions(+), 27 deletions(-) diff --git a/ESRIGridParser.js b/ESRIGridParser.js index 167e797f..285f6dd3 100644 --- a/ESRIGridParser.js +++ b/ESRIGridParser.js @@ -1,5 +1,6 @@ const MathTools = require("./MathTools.js"); const proj4 = require("proj4"); +const crs = L.CRS.EPSG3857; /** * A stateful ESRI Grid parser. Calculates min and max values for selected polygons in {@link L.ALS.SynthRectangleBaseLayer}. Can parse huge files by chunks. @@ -216,30 +217,28 @@ class ESRIGridParser { continue; } - let oldPoint = [this.x, this.y]; - let point = (this.hasProj) ? this.projectionFromWGS.inverse(oldPoint) : oldPoint; + let oldPoint = [this.x, this.y], + point = (this.hasProj) ? this.projectionFromWGS.inverse(oldPoint) : oldPoint; + for (let name in this.polygonsCoordinates) { - let poly = this.polygonsCoordinates[name], fn, projPoint; + let poly = this.polygonsCoordinates[name]; if (poly.length > 2) { - fn = "isPointInPolygon"; - let {x, y} = this.layer.map.project([point[0], point[1]], 0); - projPoint = [x, y]; + let {x, y} = crs.project(L.latLng(point[1], point[0])); + if (!MathTools.isPointInPolygon([x, y], poly)) + continue; } else { - fn = "isPointInRectangle"; - projPoint = point; + if (!MathTools.isPointInRectangle(point, poly)) + continue; } - if (!MathTools[fn](projPoint, poly)) - continue; - if (!this.polygonsStats[name]) this.polygonsStats[name] = ESRIGridParser.createStatsObject(); let stats = this.polygonsStats[name]; ESRIGridParser.addToStats(pixelValue, stats); - L.circleMarker([...point].reverse(), {color: `rgb(${pixelValue},${pixelValue},${pixelValue})`, fillOpacity: 1, stroke: false}).addTo(map); + new L.CircleMarker([...point].reverse(), {color: `rgb(${pixelValue},${pixelValue},${pixelValue})`, fillOpacity: 1, stroke: false}).addTo(map); } } else if (!isSpace && !isLineBreak) this.value += symbol; @@ -322,14 +321,14 @@ class ESRIGridParser { /** * Generates initial parameters for the layer. * @param layer {L.ALS.SynthPolygonBaseLayer} Layer to copy data from - * @param project {boolean} Whether polygon coordinates should be reprojected, if `layer.useRect` is `false` + * @param project {boolean} Whether polygon coordinates should be reprojected, if polygon is not a rectangle */ static getInitialData(layer, project = true) { let polygonsCoordinates = {}; layer.forEachValidPolygon(polygon => { polygon.tempDemName = L.ALS.Helpers.generateID(); - if (layer.useRect) { + if (polygon instanceof L.Rectangle) { let rect = polygon.getBounds(); polygonsCoordinates[polygon.tempDemName] = [ [rect.getWest(), rect.getNorth()], @@ -341,12 +340,15 @@ class ESRIGridParser { let coords = polygon.getLatLngs()[0], coordsCopy = []; polygonsCoordinates[polygon.tempDemName] = coordsCopy; for (let coord of coords) { + // GeoTIFFParser reprojects polygon from WGS84 to image's projection if (!project) { coordsCopy.push([coord.lng, coord.lat]); continue; } - let {x, y} = layer.map.project(coord, 0); + // ESRIGridParser reprojects point to the WebMercator to save a lot of time on projecting + // polygon to the image's projection + let {x, y} = crs.project(coord); coordsCopy.push([x, y]); } }); diff --git a/GeoTIFFParser.js b/GeoTIFFParser.js index 16c60c58..e6e28242 100644 --- a/GeoTIFFParser.js +++ b/GeoTIFFParser.js @@ -7,6 +7,7 @@ const toProj4 = require("geotiff-geokeys-to-proj4"); const proj4 = require("proj4"); const MathTools = require("./MathTools.js"); const ESRIGridParser = require("./ESRIGridParser.js"); +const crs = L.CRS.EPSG3857; /** * Parses GeoTIFF files @@ -44,16 +45,23 @@ module.exports = async function (file, projectionString, initialData) { for (let name in initialData) { // Project each polygon to the image, get their intersection part and calculate statistics for it let polygon = initialData[name], - coords = polygon.length > 2 ? polygon : [ + isRect = polygon.length === 2, + coords = isRect ? [ polygon[0], [polygon[1][0], polygon[0][1]], polygon[1], [polygon[0][0], polygon[1][1]] - ], - projPolygon = []; + ] : polygon, + projPolygon = [], mercPolygon = []; - for (let coord of coords) + for (let coord of coords) { projPolygon.push(projectionFromWGS.forward(coord)); + if (isRect) + continue; + let {x, y} = crs.project(L.latLng(coord[1], coord[0])); + mercPolygon.push([x, y]); + } projPolygon.push([...projPolygon[0]]); // Clone first coordinate to avoid floating point errors + mercPolygon.push(mercPolygon[0]); let polygonBbox = bboxPolygon(bbox( turfHelpers.polygon([projPolygon]) @@ -84,10 +92,11 @@ module.exports = async function (file, projectionString, initialData) { stats = ESRIGridParser.createStatsObject(); // Stats for current polygon for (currentY; currentY <= maxY; currentY++) { - let currentX = minX; - let raster = await image.readRasters({window: [minX, currentY, maxX, currentY + 1]}); - let color0 = raster[0], // Raster is a TypedArray where elements are colors and their elements are pixel values of that color. + let currentX = minX, + raster = await image.readRasters({window: [minX, currentY, maxX, currentY + 1]}), + color0 = raster[0], // Raster is a TypedArray where elements are colors and their elements are pixel values of that color. index = -1; + for (let pixel of color0) { let crsX = leftX + currentX * xSize, crsY = topY + currentY * ySize; if (projInformation) { @@ -95,12 +104,18 @@ module.exports = async function (file, projectionString, initialData) { crsY *= projInformation.coordinatesConversionParameters.y; } - let point = projectionFromWGS.inverse([crsX, crsY]); currentX++; // So we can continue without incrementing index++; - if (!MathTools.isPointInPolygon(point, projPolygon)) - continue; + let point = projectionFromWGS.inverse([crsX, crsY]); + if (isRect) { + if (!MathTools.isPointInRectangle(point, polygon)) + continue; + } else { + let {x, y} = crs.project(L.latLng(point[1], point[0])); + if (!MathTools.isPointInPolygon([x, y], mercPolygon)) + continue; + } let value = 0; for (let color of raster) @@ -109,6 +124,9 @@ module.exports = async function (file, projectionString, initialData) { let multipliedValue = value * zScale; if (value === nodata || multipliedValue === nodata) continue; + + new L.CircleMarker([...point].reverse(), {color: `rgb(${value},${value},${value})`, fillOpacity: 1, stroke: false}).addTo(map); + ESRIGridParser.addToStats(multipliedValue, stats); } } diff --git a/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js b/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js index 381ef5cc..f929837f 100644 --- a/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js +++ b/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js @@ -12,7 +12,6 @@ try { */ L.ALS.SynthPolygonBaseLayer = L.ALS.SynthBaseLayer.extend( /** @lends L.ALS.SynthPolygonBaseLayer.prototype */ { useZoneNumbers: false, - useRect: false, calculateCellSizeForPolygons: true, /** diff --git a/SynthRectangleBaseLayer/SynthRectangleBaseLayer.js b/SynthRectangleBaseLayer/SynthRectangleBaseLayer.js index ca989c53..a8ffc515 100644 --- a/SynthRectangleBaseLayer/SynthRectangleBaseLayer.js +++ b/SynthRectangleBaseLayer/SynthRectangleBaseLayer.js @@ -13,7 +13,6 @@ require("../SynthPolygonBaseLayer/SynthPolygonBaseSettings.js"); L.ALS.SynthRectangleBaseLayer = L.ALS.SynthPolygonBaseLayer.extend( /** @lends L.ALS.SynthRectangleBaseLayer.prototype */ { _currentStandardScale: -1, - useRect: true, borderColorLabel: "", fillColorLabel: "", From 1da6ac7a686dfb2d997c8174f39f3ba6beceb69e Mon Sep 17 00:00:00 2001 From: matafokka Date: Tue, 17 May 2022 16:04:47 +0300 Subject: [PATCH 39/48] Finally fixed DEM Also polished the whole Mercator thing --- ESRIGridParser.js | 63 +++++++++++++----------------------- GeoTIFFParser.js | 35 ++++++++------------ MathTools.js | 17 ++++++++-- SynthPolygonBaseLayer/DEM.js | 2 +- 4 files changed, 52 insertions(+), 65 deletions(-) diff --git a/ESRIGridParser.js b/ESRIGridParser.js index 285f6dd3..38fa8a9c 100644 --- a/ESRIGridParser.js +++ b/ESRIGridParser.js @@ -19,20 +19,16 @@ class ESRIGridParser { * Layer to apply parsed values to * @type {L.ALS.SynthRectangleBaseLayer} */ - this.layer = layer + this.layer = layer; + + if (projectionString === "") + projectionString = "WGS84"; /** - * Proj4 projection string - * @type {boolean} + * Proj4 projection object. Projects coordinates from WGS84 to grids projection. + * @type {Object|undefined} */ - this.hasProj = (projectionString !== ""); - - if (this.hasProj) - /** - * Proj4 projection object. Projects coordinates from WGS84 to grids projection. - * @type {Object|undefined} - */ - this.projectionFromWGS = proj4("WGS84", projectionString); + this.projectionFromMerc = proj4("EPSG:3857", projectionString); this.clearState(); } @@ -217,20 +213,13 @@ class ESRIGridParser { continue; } - let oldPoint = [this.x, this.y], - point = (this.hasProj) ? this.projectionFromWGS.inverse(oldPoint) : oldPoint; + let point = this.projectionFromMerc.inverse([this.x, this.y]); for (let name in this.polygonsCoordinates) { let poly = this.polygonsCoordinates[name]; - if (poly.length > 2) { - let {x, y} = crs.project(L.latLng(point[1], point[0])); - if (!MathTools.isPointInPolygon([x, y], poly)) - continue; - } else { - if (!MathTools.isPointInRectangle(point, poly)) - continue; - } + if (!MathTools[poly.length > 2 ? "isPointInPolygon" : "isPointInRectangle"](point, poly)) + continue; if (!this.polygonsStats[name]) this.polygonsStats[name] = ESRIGridParser.createStatsObject(); @@ -238,7 +227,10 @@ class ESRIGridParser { let stats = this.polygonsStats[name]; ESRIGridParser.addToStats(pixelValue, stats); - new L.CircleMarker([...point].reverse(), {color: `rgb(${pixelValue},${pixelValue},${pixelValue})`, fillOpacity: 1, stroke: false}).addTo(map); + /*new L.CircleMarker( + crs.unproject(L.point(...point)), + {color: `rgb(${pixelValue},${pixelValue},${pixelValue})`, fillOpacity: 1, stroke: false} + ).addTo(map);*/ } } else if (!isSpace && !isLineBreak) this.value += symbol; @@ -321,36 +313,27 @@ class ESRIGridParser { /** * Generates initial parameters for the layer. * @param layer {L.ALS.SynthPolygonBaseLayer} Layer to copy data from - * @param project {boolean} Whether polygon coordinates should be reprojected, if polygon is not a rectangle */ - static getInitialData(layer, project = true) { + static getInitialData(layer) { let polygonsCoordinates = {}; layer.forEachValidPolygon(polygon => { polygon.tempDemName = L.ALS.Helpers.generateID(); + let coords; + if (polygon instanceof L.Rectangle) { let rect = polygon.getBounds(); - polygonsCoordinates[polygon.tempDemName] = [ - [rect.getWest(), rect.getNorth()], - [rect.getEast(), rect.getSouth()] - ]; - return; - } + coords = [rect.getNorthWest(), rect.getSouthEast()]; + } else + coords = polygon.getLatLngs()[0]; - let coords = polygon.getLatLngs()[0], coordsCopy = []; - polygonsCoordinates[polygon.tempDemName] = coordsCopy; + let coordsCopy = []; for (let coord of coords) { - // GeoTIFFParser reprojects polygon from WGS84 to image's projection - if (!project) { - coordsCopy.push([coord.lng, coord.lat]); - continue; - } - - // ESRIGridParser reprojects point to the WebMercator to save a lot of time on projecting - // polygon to the image's projection let {x, y} = crs.project(coord); coordsCopy.push([x, y]); } + + polygonsCoordinates[polygon.tempDemName] = coordsCopy; }); return polygonsCoordinates; diff --git a/GeoTIFFParser.js b/GeoTIFFParser.js index e6e28242..d2c28e24 100644 --- a/GeoTIFFParser.js +++ b/GeoTIFFParser.js @@ -7,7 +7,6 @@ const toProj4 = require("geotiff-geokeys-to-proj4"); const proj4 = require("proj4"); const MathTools = require("./MathTools.js"); const ESRIGridParser = require("./ESRIGridParser.js"); -const crs = L.CRS.EPSG3857; /** * Parses GeoTIFF files @@ -40,7 +39,7 @@ module.exports = async function (file, projectionString, initialData) { projectionString = projInformation.proj4; } - let projectionFromWGS = proj4("WGS84", projectionString); + let projectionFromMerc = proj4("EPSG:3857", projectionString); for (let name in initialData) { // Project each polygon to the image, get their intersection part and calculate statistics for it @@ -50,18 +49,12 @@ module.exports = async function (file, projectionString, initialData) { polygon[0], [polygon[1][0], polygon[0][1]], polygon[1], [polygon[0][0], polygon[1][1]] ] : polygon, - projPolygon = [], mercPolygon = []; - - for (let coord of coords) { - projPolygon.push(projectionFromWGS.forward(coord)); - if (isRect) - continue; - let {x, y} = crs.project(L.latLng(coord[1], coord[0])); - mercPolygon.push([x, y]); - } + projPolygon = []; + + for (let coord of coords) + projPolygon.push(projectionFromMerc.forward(coord)); projPolygon.push([...projPolygon[0]]); // Clone first coordinate to avoid floating point errors - mercPolygon.push(mercPolygon[0]); let polygonBbox = bboxPolygon(bbox( turfHelpers.polygon([projPolygon]) @@ -107,15 +100,10 @@ module.exports = async function (file, projectionString, initialData) { currentX++; // So we can continue without incrementing index++; - let point = projectionFromWGS.inverse([crsX, crsY]); - if (isRect) { - if (!MathTools.isPointInRectangle(point, polygon)) - continue; - } else { - let {x, y} = crs.project(L.latLng(point[1], point[0])); - if (!MathTools.isPointInPolygon([x, y], mercPolygon)) - continue; - } + let point = projectionFromMerc.inverse([crsX, crsY]); + + if (!MathTools[isRect ? "isPointInRectangle" : "isPointInPolygon"](point, polygon)) + continue; let value = 0; for (let color of raster) @@ -125,7 +113,10 @@ module.exports = async function (file, projectionString, initialData) { if (value === nodata || multipliedValue === nodata) continue; - new L.CircleMarker([...point].reverse(), {color: `rgb(${value},${value},${value})`, fillOpacity: 1, stroke: false}).addTo(map); + /*new L.CircleMarker( + L.CRS.EPSG3857.unproject(L.point(...point)), + {color: `rgb(${value},${value},${value})`, fillOpacity: 1, stroke: false} + ).addTo(map);*/ ESRIGridParser.addToStats(multipliedValue, stats); } diff --git a/MathTools.js b/MathTools.js index 3cf95299..3ec75dae 100644 --- a/MathTools.js +++ b/MathTools.js @@ -98,12 +98,22 @@ class MathTools { * @return {boolean} True, if point lies in polygon or on one of its edges. */ static isPointInPolygon(point, polygon) { - let intersections = 0, ray = [point, [Infinity, point[1]]]; + let intersections = 0, ray = [point, [Infinity, point[1]]], + polygonNotClosed = !MathTools.arePointsEqual(polygon[0], polygon[polygon.length - 1]); + + // If polygon is not closed, algorithm will return wrong value, if point intersects with the last edge + if (polygonNotClosed) + polygon.push([...polygon[0]]); + for (let i = 0; i < polygon.length - 1; i++) { let edge = [polygon[i], polygon[i + 1]], isPointOnEdge = MathTools.isPointOnLine(point, edge); - if (isPointOnEdge) + if (isPointOnEdge) { + if (polygonNotClosed) + polygon.pop(); + return true; + } let intersection = MathTools.linesIntersection(edge, ray); if (!intersection) @@ -134,6 +144,9 @@ class MathTools { intersections++; } + if (polygonNotClosed) + polygon.pop(); + return (intersections % 2 !== 0); } diff --git a/SynthPolygonBaseLayer/DEM.js b/SynthPolygonBaseLayer/DEM.js index 5273f124..2959d547 100644 --- a/SynthPolygonBaseLayer/DEM.js +++ b/SynthPolygonBaseLayer/DEM.js @@ -144,7 +144,7 @@ L.ALS.SynthPolygonBaseLayer.prototype.onDEMLoadWorker = async function (widget) if (!GeoTIFFParser) continue; try { - let stats = await GeoTIFFParser(file, projectionString, ESRIGridParser.getInitialData(this, false)); + let stats = await GeoTIFFParser(file, projectionString, ESRIGridParser.getInitialData(this)); ESRIGridParser.copyStats(this, stats); } catch (e) { console.log(e); From 96433ccac48afeb4010ecf49f548656a82de6cbd Mon Sep 17 00:00:00 2001 From: matafokka Date: Tue, 17 May 2022 17:19:04 +0300 Subject: [PATCH 40/48] Fixes for legacy browsers - Fixed parameters not being calculated in browsers without FileReader. - Fixed controls position in IE9 by bumping ALS version. --- SynthGeometryBaseWizard.js | 5 +++++ css/styles.css | 5 +++++ package-lock.json | 14 +++++++------- package.json | 2 +- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/SynthGeometryBaseWizard.js b/SynthGeometryBaseWizard.js index 120d1e70..43cf1d5d 100644 --- a/SynthGeometryBaseWizard.js +++ b/SynthGeometryBaseWizard.js @@ -103,6 +103,11 @@ L.ALS.SynthGeometryBaseWizard = L.ALS.Wizard.extend(/** @lends L.ALS.SynthGeomet synthLayer.isAfterDeserialization = false; } + if (!window.FileReader) { + finishLoading(); + return; + } + if (synthLayer instanceof L.ALS.SynthPolygonLayer) { groupToAdd = synthLayer.polygonGroup; layerType = L.Polygon; diff --git a/css/styles.css b/css/styles.css index 34952f5b..f8e8ffc1 100644 --- a/css/styles.css +++ b/css/styles.css @@ -10,6 +10,11 @@ z-index: 500; } +.ie-lte-9 .leaflet-pane.leaflet-mapLabels-pane, +.ie-lte-9 .leaflet-pane.leaflet-blackOverlay-pane { + z-index: 400; +} + .leaflet-control-coordinates .uiElement { font-size: 0.75rem !important; margin-top: 0; diff --git a/package-lock.json b/package-lock.json index 846458f8..e1e40946 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "license": "GPL-3.0-or-later", "dependencies": { "@electron/remote": "^2.0.1", - "leaflet-advanced-layer-system": "^2.2.3" + "leaflet-advanced-layer-system": "^2.2.4" }, "devDependencies": { "@babel/core": "^7.12.3", @@ -6054,9 +6054,9 @@ "dev": true }, "node_modules/leaflet-advanced-layer-system": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/leaflet-advanced-layer-system/-/leaflet-advanced-layer-system-2.2.3.tgz", - "integrity": "sha512-8hdbqkYyJyPVP+CN2rfNuY4OPzhu186fntgLXo2pXRjFoSgEAVfmQt7IRHPqJSSfQDbRJe2HiomPEb0bU2IVhg==" + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/leaflet-advanced-layer-system/-/leaflet-advanced-layer-system-2.2.4.tgz", + "integrity": "sha512-NL+lMosKSq5W25lOWa+NFpFY6D/PPQQ4TxwpyJ5eLFrRMablS2i/0JAIbfVcbbzh8I+rNisNF8AoqMIT0JWzIg==" }, "node_modules/leaflet-draw": { "version": "1.0.4", @@ -17598,9 +17598,9 @@ "dev": true }, "leaflet-advanced-layer-system": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/leaflet-advanced-layer-system/-/leaflet-advanced-layer-system-2.2.3.tgz", - "integrity": "sha512-8hdbqkYyJyPVP+CN2rfNuY4OPzhu186fntgLXo2pXRjFoSgEAVfmQt7IRHPqJSSfQDbRJe2HiomPEb0bU2IVhg==" + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/leaflet-advanced-layer-system/-/leaflet-advanced-layer-system-2.2.4.tgz", + "integrity": "sha512-NL+lMosKSq5W25lOWa+NFpFY6D/PPQQ4TxwpyJ5eLFrRMablS2i/0JAIbfVcbbzh8I+rNisNF8AoqMIT0JWzIg==" }, "leaflet-draw": { "version": "1.0.4", diff --git a/package.json b/package.json index 6d1280d4..c8de6628 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,6 @@ }, "dependencies": { "@electron/remote": "^2.0.1", - "leaflet-advanced-layer-system": "^2.2.3" + "leaflet-advanced-layer-system": "^2.2.4" } } From 62003d0daff6f3c4079698faf6a6e3a7713b458d Mon Sep 17 00:00:00 2001 From: matafokka Date: Tue, 17 May 2022 18:08:08 +0300 Subject: [PATCH 41/48] Started fixing serialization Fixed Rectangle, Polygon and Line Layer serialization. Grid Layer serialization is still broken. --- SynthLineLayer/SynthLineLayer.js | 7 +--- SynthPolygonBaseLayer/serialization.js | 56 ++++++++++++-------------- 2 files changed, 27 insertions(+), 36 deletions(-) diff --git a/SynthLineLayer/SynthLineLayer.js b/SynthLineLayer/SynthLineLayer.js index 5be20b3a..eb8f2370 100644 --- a/SynthLineLayer/SynthLineLayer.js +++ b/SynthLineLayer/SynthLineLayer.js @@ -119,11 +119,8 @@ L.ALS.SynthLineLayer = L.ALS.SynthBaseLayer.extend(/** @lends L.ALS.SynthLineLay this.updatePathsMeta(); - if (!this.getWidgetById("hidePathsConnections").getValue()) - this.map.addLayer(this.connectionsGroup); - - if (!this.getWidgetById("hideCapturePoints").getValue()) - this.map.addLayer(this.pointsGroup); + this.hideOrShowLayer(this.getWidgetById("hidePathsConnections").getValue(), this.connectionsGroup); + this.hideOrShowLayer(this.getWidgetById("hideCapturePoints").getValue(), this.pointsGroup); this.map.removeLayer(this.drawingGroup); this.map.addLayer(this.pathsGroup); diff --git a/SynthPolygonBaseLayer/serialization.js b/SynthPolygonBaseLayer/serialization.js index 03d07510..965725ab 100644 --- a/SynthPolygonBaseLayer/serialization.js +++ b/SynthPolygonBaseLayer/serialization.js @@ -1,25 +1,27 @@ L.ALS.SynthPolygonBaseLayer.prototype.serialize = function (seenObjects) { let serialized = this.getObjectToSerializeTo(seenObjects); - // TODO: Fix serialization - serialized.polygonsWidgets = L.ALS.Serializable.serializeAnyObject(this.polygonsWidgets, seenObjects); + serialized.polygons = []; + serialized.polygonsWidgets = {}; // Gather selected polygons' coordinates - for (let objName of ["polygons", "invalidPolygons"]) { - let polyObject = this[objName]; - serialized[objName] = {}; + this.polygonGroup.eachLayer(poly => { + if (poly.isCloned) + return; - for (let name in polyObject) { - if (!polyObject.hasOwnProperty(name)) - continue; + let serializedPoly = poly[poly instanceof L.Rectangle ? "getBounds" : "getLatLngs"](); + serializedPoly.widgetLinkId = L.ALS.Helpers.generateID(); + serialized.polygons.push(serializedPoly); - let poly = polyObject[name]; - serialized[objName][name] = poly[poly instanceof L.Rectangle ? "getBounds" : "getLatLngs"](); - } - } + if (!poly.widgetable) + return; + + serialized.polygonsWidgets[serializedPoly.widgetLinkId] = poly.widgetable.serialize(seenObjects); + }); this.clearSerializedPathsWidgets(serialized); + console.log(serialized); return serialized; } @@ -28,31 +30,23 @@ L.ALS.SynthPolygonBaseLayer._toUpdateColors = ["borderColor", "fillColor", "colo L.ALS.SynthPolygonBaseLayer.deserialize = function (serialized, layerSystem, settings, seenObjects) { let object = L.ALS.SynthBaseLayer.deserialize(serialized, layerSystem, settings, seenObjects); object.isAfterDeserialization = true; + console.log(serialized); - for (let objName of ["polygons", "invalidPolygons"]) { - let serializedPolyObj = serialized[objName], - deserializedPolyObj = object[objName]; - for (let prop in serializedPolyObj) { - if (!serializedPolyObj.hasOwnProperty(prop)) - continue; + for (let poly of serialized.polygons) { + let newPoly = L[poly.serializableClassName === "L.LatLngBounds" ? "rectangle" : "polygon"](poly), + widget = serialized.polygonsWidgets[poly.widgetLinkId]; + object.polygonGroup.addLayer(newPoly); - let value = serializedPolyObj[prop], - newPoly = L[value.serializableClassName === "L.LatLngBounds" ? "rectangle" : "polygon"](value); + if (!widget) + continue; - if (objName === "invalidPolygons") - object.invalidatePolygon(newPoly); - else - deserializedPolyObj[prop] = newPoly; + let newWidget = L.ALS.LeafletLayers.WidgetLayer.deserialize(widget, seenObjects); - object.polygonGroup.addLayer(newPoly); - } - } + newPoly.widgetable = newWidget; + newWidget.polygon = newPoly; - for (let prop in object.polygonsWidgets) { - let widget = object.polygonsWidgets[prop]; - if (widget.addTo) - object.widgetsGroup.addLayer(widget); + object.widgetsGroup.addLayer(newWidget); } for (let color of this._toUpdateColors) { From f3975b3ea97e0dd3cfd8d27d5f7a791b7372a966 Mon Sep 17 00:00:00 2001 From: matafokka Date: Tue, 17 May 2022 18:08:33 +0300 Subject: [PATCH 42/48] Removed console.log() --- SynthPolygonBaseLayer/serialization.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/SynthPolygonBaseLayer/serialization.js b/SynthPolygonBaseLayer/serialization.js index 965725ab..de4d557b 100644 --- a/SynthPolygonBaseLayer/serialization.js +++ b/SynthPolygonBaseLayer/serialization.js @@ -21,7 +21,6 @@ L.ALS.SynthPolygonBaseLayer.prototype.serialize = function (seenObjects) { }); this.clearSerializedPathsWidgets(serialized); - console.log(serialized); return serialized; } @@ -30,8 +29,6 @@ L.ALS.SynthPolygonBaseLayer._toUpdateColors = ["borderColor", "fillColor", "colo L.ALS.SynthPolygonBaseLayer.deserialize = function (serialized, layerSystem, settings, seenObjects) { let object = L.ALS.SynthBaseLayer.deserialize(serialized, layerSystem, settings, seenObjects); object.isAfterDeserialization = true; - console.log(serialized); - for (let poly of serialized.polygons) { let newPoly = L[poly.serializableClassName === "L.LatLngBounds" ? "rectangle" : "polygon"](poly), From 88f48b44e8003496e6d1bbdd9d415926045c42ad Mon Sep 17 00:00:00 2001 From: matafokka Date: Tue, 17 May 2022 19:59:25 +0300 Subject: [PATCH 43/48] Seems like serialization is fine Also added better structure for serializing. --- SynthGridLayer/SynthGridLayer.js | 81 ++++++++++++++----- SynthGridLayer/onMapPan.js | 24 +----- SynthGridLayer/serialization.js | 40 +++++++++ .../SynthPolygonBaseLayer.js | 11 --- SynthPolygonBaseLayer/serialization.js | 32 ++++---- 5 files changed, 117 insertions(+), 71 deletions(-) create mode 100644 SynthGridLayer/serialization.js diff --git a/SynthGridLayer/SynthGridLayer.js b/SynthGridLayer/SynthGridLayer.js index 6726a99d..d83cacf2 100644 --- a/SynthGridLayer/SynthGridLayer.js +++ b/SynthGridLayer/SynthGridLayer.js @@ -70,27 +70,6 @@ L.ALS.SynthGridLayer = L.ALS.SynthRectangleBaseLayer.extend(/** @lends L.ALS.Syn this.addEventListenerTo(this.map, "moveend resize", "_onMapPan"); }, - /** - * Selects or deselects polygon upon double click and redraws flight paths - * @param event - */ - _selectOrDeselectPolygon: function (event) { - let polygon = event.target, name = this._generatePolygonName(polygon); - - if (this.polygons[name]) { - polygon.setStyle({fill: false}); - delete this.polygons[name]; - this.removePolygon(polygon); - } else { - polygon.setStyle({fill: true}); - this.polygons[name] = polygon; - this.addPolygon(polygon); - } - - this.calculateParameters(); - this.writeToHistoryDebounced(); - }, - calculateParameters: function () { this._onMapZoom(); L.ALS.SynthRectangleBaseLayer.prototype.calculateParameters.call(this); @@ -144,6 +123,63 @@ L.ALS.SynthGridLayer = L.ALS.SynthRectangleBaseLayer.extend(/** @lends L.ALS.Syn cb(this.polygons[id]); }, + initPolygon: function (lat, lng, lngDistance) { + let polygon = new L.Rectangle([ + [lat, lng], + [lat + this.latDistance, lng + lngDistance], + ]), + name = this.generatePolygonName(polygon); + + return this.polygons[name] ? this.polygons[name] : this.initPolygonStyleAndEvents(polygon); + }, + + initPolygonStyleAndEvents: function (polygon, isSelected = false) { + let name = this.generatePolygonName(polygon); + + polygon.setStyle({ + color: this.borderColor, + fillColor: this.fillColor, + fill: false, + weight: this.lineThicknessValue + }); + + let select = () => { + polygon.setStyle({fill: true}); + this.polygons[name] = polygon; + this.addPolygon(polygon); + } + + polygon.on("dblclick contextmenu", () => { + if (this.polygons[name]) { + polygon.setStyle({fill: false}); + delete this.polygons[name]; + this.removePolygon(polygon); + } else + select(); + + this.calculateParameters(); + this.writeToHistoryDebounced(); + }); + + this.polygonGroup.addLayer(polygon); + + if (isSelected) + select(); + + return polygon; + }, + + /** + * Generates polygon name for adding into this.polygons + * @param polygon Polygon to generate name for + * @return {string} Name for given polygon + * @protected + */ + generatePolygonName: function (polygon) { + let {lat, lng} = polygon.getBounds().getNorthWest(); + return "p_" + this.toFixed(lat) + "_" + this.toFixed(lng); + }, + statics: { wizard: L.ALS.SynthGridWizard, settings: new L.ALS.SynthGridSettings(), @@ -152,4 +188,5 @@ L.ALS.SynthGridLayer = L.ALS.SynthRectangleBaseLayer.extend(/** @lends L.ALS.Syn require("./onMapPan.js"); require("./onMapZoom.js"); -require("./mergePolygons.js"); \ No newline at end of file +require("./mergePolygons.js"); +require("./serialization.js"); \ No newline at end of file diff --git a/SynthGridLayer/onMapPan.js b/SynthGridLayer/onMapPan.js index 786198c2..1ada8371 100644 --- a/SynthGridLayer/onMapPan.js +++ b/SynthGridLayer/onMapPan.js @@ -59,29 +59,7 @@ L.ALS.SynthGridLayer.prototype._onMapPan = function () { if (isFirstIteration) createLabel([north, lng], this.toFixed(lng), "topCenter", true); - let polygon = new L.Polygon([ - [lat, lng], - [lat + this.latDistance, lng], - [lat + this.latDistance, lng + lngDistance], - [lat, lng + lngDistance], - ]); - - // If this polygon has been selected, we should fill it and replace it in the array. - // Because fill will be changed, we can't keep old polygon, it's easier to just replace it - let name = this._generatePolygonName(polygon); - - if (!this.polygons[name]) { - polygon.setStyle({ - color: this.borderColor, - fillColor: this.fillColor, - fill: false, - weight: this.lineThicknessValue - }); - - // We should select or deselect polygons upon double click - this.addEventListenerTo(polygon, "dblclick contextmenu", "_selectOrDeselectPolygon"); - this.polygonGroup.addLayer(polygon); - } + let polygon = this.initPolygon(lat, lng, lngDistance); // Generate current polygon's name if grid uses one of standard scales if (this._currentStandardScale === Infinity) { diff --git a/SynthGridLayer/serialization.js b/SynthGridLayer/serialization.js new file mode 100644 index 00000000..2261ee2a --- /dev/null +++ b/SynthGridLayer/serialization.js @@ -0,0 +1,40 @@ +L.ALS.SynthGridLayer.prototype.serialize = function (seenObjects) { + let serialized = this.getObjectToSerializeTo(seenObjects); + + serialized.polygons = []; + + // Gather selected polygons' coordinates + + for (let id in this.polygons) { + let poly = this.polygons[id]; + + serialized.polygons.push({ + polygon: poly.getBounds(), + widget: poly.widgetable.serialize(seenObjects), + }); + } + + this.clearSerializedPathsWidgets(serialized); + return serialized; +} + +L.ALS.SynthGridLayer.deserialize = function (serialized, layerSystem, settings, seenObjects) { + let object = L.ALS.SynthBaseLayer.deserialize(serialized, layerSystem, settings, seenObjects); + object.isAfterDeserialization = true; + + for (let struct of serialized.polygons) { + let {polygon, widget} = struct, + newPoly = new L.Rectangle(polygon), + newWidget = L.ALS.LeafletLayers.WidgetLayer.deserialize(widget, seenObjects); + + newPoly.widgetable = newWidget; + newWidget.polygon = newPoly; + + object.initPolygonStyleAndEvents(newPoly, true); + object.widgetsGroup.addLayer(newWidget); + } + + this.afterDeserialization(object); + object._onMapPan(); + return object; +} \ No newline at end of file diff --git a/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js b/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js index f929837f..cbaf5f55 100644 --- a/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js +++ b/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js @@ -136,17 +136,6 @@ L.ALS.SynthPolygonBaseLayer = L.ALS.SynthBaseLayer.extend( /** @lends L.ALS.Synt path.pathGroup.remove(); }, - /** - * Generates polygon name for adding into this.polygons - * @param polygon Polygon to generate name for - * @return {string} Name for given polygon - * @protected - */ - _generatePolygonName: function (polygon) { - let firstPoint = polygon.getLatLngs()[0][0]; - return "p_" + this.toFixed(firstPoint.lat) + "_" + this.toFixed(firstPoint.lng); - }, - onShow: function () { this.updateLayersVisibility(); }, diff --git a/SynthPolygonBaseLayer/serialization.js b/SynthPolygonBaseLayer/serialization.js index de4d557b..65c0919d 100644 --- a/SynthPolygonBaseLayer/serialization.js +++ b/SynthPolygonBaseLayer/serialization.js @@ -2,7 +2,6 @@ L.ALS.SynthPolygonBaseLayer.prototype.serialize = function (seenObjects) { let serialized = this.getObjectToSerializeTo(seenObjects); serialized.polygons = []; - serialized.polygonsWidgets = {}; // Gather selected polygons' coordinates @@ -10,14 +9,14 @@ L.ALS.SynthPolygonBaseLayer.prototype.serialize = function (seenObjects) { if (poly.isCloned) return; - let serializedPoly = poly[poly instanceof L.Rectangle ? "getBounds" : "getLatLngs"](); - serializedPoly.widgetLinkId = L.ALS.Helpers.generateID(); - serialized.polygons.push(serializedPoly); + let serializedPoly = poly[poly instanceof L.Rectangle ? "getBounds" : "getLatLngs"](), + serializedStruct = {polygon: serializedPoly} + serialized.polygons.push(serializedStruct); if (!poly.widgetable) return; - serialized.polygonsWidgets[serializedPoly.widgetLinkId] = poly.widgetable.serialize(seenObjects); + serializedStruct.widget = poly.widgetable.serialize(seenObjects); }); this.clearSerializedPathsWidgets(serialized); @@ -30,9 +29,9 @@ L.ALS.SynthPolygonBaseLayer.deserialize = function (serialized, layerSystem, set let object = L.ALS.SynthBaseLayer.deserialize(serialized, layerSystem, settings, seenObjects); object.isAfterDeserialization = true; - for (let poly of serialized.polygons) { - let newPoly = L[poly.serializableClassName === "L.LatLngBounds" ? "rectangle" : "polygon"](poly), - widget = serialized.polygonsWidgets[poly.widgetLinkId]; + for (let struct of serialized.polygons) { + let {polygon, widget} = struct, + newPoly = L[polygon.serializableClassName === "L.LatLngBounds" ? "rectangle" : "polygon"](polygon); object.polygonGroup.addLayer(newPoly); if (!widget) @@ -46,15 +45,18 @@ L.ALS.SynthPolygonBaseLayer.deserialize = function (serialized, layerSystem, set object.widgetsGroup.addLayer(newWidget); } + this.afterDeserialization(object); + return object; +} + +L.ALS.SynthPolygonBaseLayer.afterDeserialization = function (deserialized) { for (let color of this._toUpdateColors) { - let widget = object.getWidgetById(color); + let widget = deserialized.getWidgetById(color); if (widget) - object.setColor(widget); + deserialized.setColor(widget); } - object.setAirportLatLng(); - object.calculateParameters(); - object.updatePolygonsColors(); - - return object; + deserialized.setAirportLatLng(); + deserialized.calculateParameters(); + deserialized.updatePolygonsColors(); } \ No newline at end of file From 429514a4b808f02c1d7e3d8a7f7c2e761d02a5ef Mon Sep 17 00:00:00 2001 From: matafokka Date: Tue, 17 May 2022 20:17:39 +0300 Subject: [PATCH 44/48] Better labels for when loading geometry is not supported Also restored initial map view back to London --- SynthGeometryBaseWizard.js | 3 ++- SynthGeometryLayer/SynthGeometryWizard.js | 3 +++ locales/English.js | 3 ++- locales/Russian.js | 3 ++- main.js | 4 ++-- 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/SynthGeometryBaseWizard.js b/SynthGeometryBaseWizard.js index 43cf1d5d..1f7e79f2 100644 --- a/SynthGeometryBaseWizard.js +++ b/SynthGeometryBaseWizard.js @@ -8,12 +8,13 @@ const shp = require("shpjs"); L.ALS.SynthGeometryBaseWizard = L.ALS.Wizard.extend(/** @lends L.ALS.SynthGeometryBaseWizard.prototype */{ fileLabel: "geometryFileLabel", + browserNotSupportedLabel: "initialFeaturesBrowserNotSupported", initialize: function () { L.ALS.Wizard.prototype.initialize.call(this); if (!window.FileReader) { - this.addWidget(new L.ALS.Widgets.SimpleLabel("lbl", "geometryBrowserNotSupported", "center", "error")); + this.addWidget(new L.ALS.Widgets.SimpleLabel("lbl", this.browserNotSupportedLabel, "center", "error")); return; } diff --git a/SynthGeometryLayer/SynthGeometryWizard.js b/SynthGeometryLayer/SynthGeometryWizard.js index 5da6cd9d..4734f381 100644 --- a/SynthGeometryLayer/SynthGeometryWizard.js +++ b/SynthGeometryLayer/SynthGeometryWizard.js @@ -6,9 +6,12 @@ L.ALS.SynthGeometryWizard = L.ALS.SynthGeometryBaseWizard.extend( /** @lends L.ALS.SynthGeometryWizard.prototype */ { displayName: "geometryDisplayName", + browserNotSupportedLabel: "geometryBrowserNotSupported", initialize: function () { L.ALS.SynthGeometryBaseWizard.prototype.initialize.call(this); + if (!window.FileReader) + return; this.addWidget( new L.ALS.Widgets.SimpleLabel("geometryNotification", "geometryNotification", "center", "message") diff --git a/locales/English.js b/locales/English.js index f3c1a119..5bfa2bda 100644 --- a/locales/English.js +++ b/locales/English.js @@ -146,7 +146,7 @@ L.ALS.Locales.addLocaleProperties("English", { geometryNoFeatures: "Selected file doesn't contain any features, so it won't be added", geometryBorderColor: "Border color:", geometryFillColor: "Fill color:", - geometryBrowserNotSupported: "Your browser doesn't support adding this layer. You still can open projects with this layer though.", + geometryBrowserNotSupported: "Sorry, your browser doesn't support adding this layer. You still can open projects with this layer though.", geometryNoFileSelected: "No file has been selected. Please, select a file that you want to add and try again.", geometryProjectionNotSupported: "Sorry, projection of selected file is not supported. Please, convert your file to another projection, preferably, WebMercator.", @@ -167,6 +167,7 @@ L.ALS.Locales.addLocaleProperties("English", { generalSettingsDisableAnnoyingNotification: "Disable all annoying notifications after editing and DEM loading", // GeoJSON initial features + initialFeaturesBrowserNotSupported: "Sorry, your browser doesn't support loading initial geometry for this layer. You still can draw geometry yourself though.", initialFeaturesFileLabelPolygon: "Load initial polygons from zipped shapefile or GeoJSON (non-polygon features will be skipped):", initialFeaturesFileLabelLine: "Load initial polylines from zipped shapefile or GeoJSON (non-polyline features will be skipped):", initialFeaturesNoFeatures: "Selected file doesn't contain any features supported by the added layer", diff --git a/locales/Russian.js b/locales/Russian.js index 929d6163..3289145e 100644 --- a/locales/Russian.js +++ b/locales/Russian.js @@ -142,7 +142,7 @@ L.ALS.Locales.addLocaleProperties("Русский", { geometryNoFeatures: "Выбранный файл не содержит объектов, поэтому не будет добавлен", geometryBorderColor: "Цвет обводки:", geometryFillColor: "Цвет заливки:", - geometryBrowserNotSupported: "Ваш браузер не поддерживает добавление данного слоя, но вы можете открывать проекты, использующие этот слой.", + geometryBrowserNotSupported: "Извините, ваш браузер не поддерживает добавление данного слоя, но вы можете открывать проекты, использующие этот слой.", geometryNoFileSelected: "Файл не был выбран. Пожалуйста, выберете файл, который хотите добавить, и попробуйте снова.", geometryProjectionNotSupported: "Извините проекция выбранного файла не поддерживается. Пожалуйста, переконвертируйте файл в другую проекцию, предпочтительно, в WebMercator.", @@ -163,6 +163,7 @@ L.ALS.Locales.addLocaleProperties("Русский", { generalSettingsDisableAnnoyingNotification: "Отключить все надоедливые уведомления после редактирования и загрузки ЦМР", // GeoJSON initial features + initialFeaturesBrowserNotSupported: "Извините, ваш браузер не поддерживает загрузку исходной геометрии для данного слоя, но вы все равно можете нарисовать геомтрию вручную.", initialFeaturesFileLabelPolygon: "Загрузить исходные полигоны из сжатого shapefile (zip-архива) или GeoJSON (типы, отличные от полигона, будут пропущены):", initialFeaturesFileLabelLine: "Загрузить исходные полилинии из сжатого shapefile (zip-архива) или GeoJSON (типы, отличные от пололинии, будут пропущены):", initialFeaturesNoFeatures: "Выбранный файл не содержит ни одного объекта, поддерживаемого добавляемым слоем", diff --git a/main.js b/main.js index 05146d88..14299228 100644 --- a/main.js +++ b/main.js @@ -63,13 +63,13 @@ let map = L.map("map", { keyboard: false, worldCopyJump: true, fadeAnimation: false -}).setView([55.75, 37.61], 13); +}).setView([51.505, -0.09], 13); map.doubleClickZoom.disable(); // Display a notification that users can move the map farther to jump to the other side of the world -let labelLayer = new L.ALS.LeafletLayers.LabelLayer(false), maxLabelWidth = 3, +let labelLayer = new L.ALS.LeafletLayers.LabelLayer(false), labelOpts = { maxWidth: 10, breakWords: false, From 0b31ef267fccc8ac2bdae3d411bc7ab1f6773346 Mon Sep 17 00:00:00 2001 From: matafokka Date: Thu, 19 May 2022 15:47:10 +0300 Subject: [PATCH 45/48] Fixing serialization again... - Did small refactoring. - Fixed paths' metadata not updating after deserialization. - Bumped ALS version which introduces an important fix. --- SynthBaseLayer/Hull.js | 4 +- SynthBaseLayer/SynthBaseLayer.js | 48 ++++++++++++------- SynthGridLayer/serialization.js | 1 - SynthLineLayer/SynthLineLayer.js | 6 +-- .../SynthPolygonBaseLayer.js | 2 +- SynthPolygonBaseLayer/serialization.js | 1 - package-lock.json | 14 +++--- package.json | 2 +- 8 files changed, 45 insertions(+), 33 deletions(-) diff --git a/SynthBaseLayer/Hull.js b/SynthBaseLayer/Hull.js index e9fba659..fc178c54 100644 --- a/SynthBaseLayer/Hull.js +++ b/SynthBaseLayer/Hull.js @@ -222,7 +222,7 @@ L.ALS.SynthBaseLayer.prototype.getHullOptimalConnection = function (pathP1, path } L.ALS.SynthBaseLayer.prototype.connectHullToAirport = function () { - let airportPos = this._airportMarker.getLatLng(); + let airportPos = this.airportMarker.getLatLng(); for (let i = 0; i < this.paths.length; i++) { @@ -301,7 +301,7 @@ L.ALS.SynthBaseLayer.prototype.hullToCycles = function (path) { return undefined; if (path.hullConnections.length === 0) { - let airportPos = this._airportMarker.getLatLng(); + let airportPos = this.airportMarker.getLatLng(); return [[ airportPos, ...path.pathGroup.getLayers()[0].getLatLngs(), diff --git a/SynthBaseLayer/SynthBaseLayer.js b/SynthBaseLayer/SynthBaseLayer.js index 57012224..b0bc0522 100644 --- a/SynthBaseLayer/SynthBaseLayer.js +++ b/SynthBaseLayer/SynthBaseLayer.js @@ -143,7 +143,7 @@ L.ALS.SynthBaseLayer = L.ALS.Layer.extend(/** @lends L.ALS.SynthBaseLayer.protot this.toUpdateThickness.push(path.pathGroup, path.connectionsGroup); } - this.serializationIgnoreList.push("_airportMarker", "toUpdateThickness", "writeToHistoryDebounced"); + this.serializationIgnoreList.push("airportMarker", "toUpdateThickness", "writeToHistoryDebounced", "pathsDetailsSpoiler"); /** * Properties to copy to GeoJSON when exporting @@ -162,14 +162,14 @@ L.ALS.SynthBaseLayer = L.ALS.Layer.extend(/** @lends L.ALS.SynthBaseLayer.protot * Airport marker * @protected */ - this._airportMarker = new L.Marker(this.map.getCenter(), { + this.airportMarker = new L.Marker(this.map.getCenter(), { icon: icon, draggable: true }); // Set inputs' values to new ones on drag - this.addEventListenerTo(this._airportMarker, "drag", "onMarkerDrag"); - this.addLayers(this._airportMarker); + this.addEventListenerTo(this.airportMarker, "drag", "onMarkerDrag"); + this.addLayers(this.airportMarker); }, /** @@ -210,7 +210,7 @@ L.ALS.SynthBaseLayer = L.ALS.Layer.extend(/** @lends L.ALS.SynthBaseLayer.protot new L.ALS.Widgets.Divider("div2"), ); - this._airportMarker.fire("drag"); // Just to set values + this.airportMarker.fire("drag"); // Just to set values }, /** @@ -284,14 +284,14 @@ L.ALS.SynthBaseLayer = L.ALS.Layer.extend(/** @lends L.ALS.SynthBaseLayer.protot latWidget.setValue(fixedLatLng.lat); lngWidget.setValue(fixedLatLng.lng); - this._airportMarker.setLatLng(fixedLatLng); + this.airportMarker.setLatLng(fixedLatLng); this.connectToAirport(); }, onMarkerDrag: function () { - let latLng = this._airportMarker.getLatLng(), + let latLng = this.airportMarker.getLatLng(), fixedLatLng = this._limitAirportPos(latLng.lat, latLng.lng); - this._airportMarker.setLatLng(fixedLatLng); + this.airportMarker.setLatLng(fixedLatLng); this.getWidgetById("airportLat").setValue(fixedLatLng.lat.toFixed(5)); this.getWidgetById("airportLng").setValue(fixedLatLng.lng.toFixed(5)); this.connectToAirport(); @@ -314,7 +314,7 @@ L.ALS.SynthBaseLayer = L.ALS.Layer.extend(/** @lends L.ALS.SynthBaseLayer.protot let popup = document.createElement("div"); L.ALS.Locales.localizeElement(popup, "airportForLayer", "innerText"); popup.innerText += " " + this.getName(); - this._airportMarker.bindPopup(popup); + this.airportMarker.bindPopup(popup); }, connectToAirport: function () { @@ -337,14 +337,14 @@ L.ALS.SynthBaseLayer = L.ALS.Layer.extend(/** @lends L.ALS.SynthBaseLayer.protot this.connectHull(); }, - _createPathWidget: function (layer, length, toFlash, selectedArea = 0) { + _createPathWidget: function (object, length, toFlash, selectedArea = 0) { let id = L.ALS.Helpers.generateID(), button = new L.ALS.Widgets.Button("flashPath" + id, "flashPath", this, "flashPath"), lengthWidget = new L.ALS.Widgets.ValueLabel("pathLength" + id, "pathLength", "m").setFormatNumbers(true).setNumberOfDigitsAfterPoint(0), timeWidget = new L.ALS.Widgets.ValueLabel("flightTime" + id, "flightTime", "h:mm"), warning = new L.ALS.Widgets.SimpleLabel("warning" + id, "", "left", "warning"); - layer.updateWidgets = (length) => { + object.updateWidgets = (length) => { lengthWidget.setValue(length); let time = this.getFlightTime(length); timeWidget.setValue(time.formatted); @@ -366,7 +366,7 @@ L.ALS.SynthBaseLayer = L.ALS.Layer.extend(/** @lends L.ALS.SynthBaseLayer.protot button.toFlash = toFlash; this._pathsWidgetsNumber++; - layer.updateWidgets(length); + object.updateWidgets(length); }, /** @@ -458,7 +458,7 @@ L.ALS.SynthBaseLayer = L.ALS.Layer.extend(/** @lends L.ALS.SynthBaseLayer.protot * Called when there's one flight per each path. You should call {@link L.ALS.SynthBaseLayer#connectOnePerFlight} here. */ connectOnePerFlightToAirport: function () { - let airportPos = this._airportMarker.getLatLng(); + let airportPos = this.airportMarker.getLatLng(); for (let path of this.paths) { path.connectionsGroup.eachLayer((layer) => { @@ -506,7 +506,7 @@ L.ALS.SynthBaseLayer = L.ALS.Layer.extend(/** @lends L.ALS.SynthBaseLayer.protot * @return {LatLng[][]} Cycles */ onePerFlightToCycles: function (path) { - let cycles = [], airportPos = this._airportMarker.getLatLng(); + let cycles = [], airportPos = this.airportMarker.getLatLng(); path.pathGroup.eachLayer((layer) => { let latLngs = layer.getLatLngs(), toPush = [airportPos, ...latLngs, airportPos]; @@ -613,9 +613,23 @@ L.ALS.SynthBaseLayer = L.ALS.Layer.extend(/** @lends L.ALS.SynthBaseLayer.protot window.alert(notification + L.ALS.locale.afterEditingToDisableNotifications); }, - clearSerializedPathsWidgets: function (serialized) { - for (let i = 1; i <= this._pathsWidgetsNumber; i++) - delete serialized._widgets["pathWidget" + i]; + getObjectToSerializeTo: function (seenObjects) { + let object = L.ALS.Layer.prototype.getObjectToSerializeTo.call(this, seenObjects), + {lat, lng} = this.airportMarker.getLatLng(); + object.airportPos = {lat, lng}; + + delete object._widgets.pathsDetails; + + return object; + }, + + statics: { + deserialize: function (serialized, layerSystem, settings, seenObjects) { + let object = L.ALS.Layer.deserialize(serialized, layerSystem, settings, seenObjects); + object.airportMarker.setLatLng(L.latLng(serialized.airportPos)); + object.addWidget(object.pathsDetailsSpoiler); + return object; + } } }); diff --git a/SynthGridLayer/serialization.js b/SynthGridLayer/serialization.js index 2261ee2a..d3cf70a2 100644 --- a/SynthGridLayer/serialization.js +++ b/SynthGridLayer/serialization.js @@ -14,7 +14,6 @@ L.ALS.SynthGridLayer.prototype.serialize = function (seenObjects) { }); } - this.clearSerializedPathsWidgets(serialized); return serialized; } diff --git a/SynthLineLayer/SynthLineLayer.js b/SynthLineLayer/SynthLineLayer.js index eb8f2370..e71c606d 100644 --- a/SynthLineLayer/SynthLineLayer.js +++ b/SynthLineLayer/SynthLineLayer.js @@ -144,10 +144,10 @@ L.ALS.SynthLineLayer = L.ALS.SynthBaseLayer.extend(/** @lends L.ALS.SynthLineLay }, serialize: function (seenObjects) { - let lines = []; - this.drawingGroup.eachLayer(layer => lines.push(layer.getLatLngs())); + let serialized = this.getObjectToSerializeTo(seenObjects), + lines = []; - let serialized = this.getObjectToSerializeTo(seenObjects); + this.drawingGroup.eachLayer(layer => lines.push(layer.getLatLngs())); serialized.lines = L.ALS.Serializable.serializeAnyObject(lines, seenObjects); return serialized; }, diff --git a/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js b/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js index cbaf5f55..3aac58cc 100644 --- a/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js +++ b/SynthPolygonBaseLayer/SynthPolygonBaseLayer.js @@ -233,7 +233,7 @@ L.ALS.SynthPolygonBaseLayer = L.ALS.SynthBaseLayer.extend( /** @lends L.ALS.Synt jsons.push(polygonJson); }); - let airport = this._airportMarker.toGeoJSON(); + let airport = this.airportMarker.toGeoJSON(); airport.name = "Airport"; jsons.push(airport); diff --git a/SynthPolygonBaseLayer/serialization.js b/SynthPolygonBaseLayer/serialization.js index 65c0919d..347c22ed 100644 --- a/SynthPolygonBaseLayer/serialization.js +++ b/SynthPolygonBaseLayer/serialization.js @@ -19,7 +19,6 @@ L.ALS.SynthPolygonBaseLayer.prototype.serialize = function (seenObjects) { serializedStruct.widget = poly.widgetable.serialize(seenObjects); }); - this.clearSerializedPathsWidgets(serialized); return serialized; } diff --git a/package-lock.json b/package-lock.json index e1e40946..fe52b862 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "license": "GPL-3.0-or-later", "dependencies": { "@electron/remote": "^2.0.1", - "leaflet-advanced-layer-system": "^2.2.4" + "leaflet-advanced-layer-system": "^2.2.5" }, "devDependencies": { "@babel/core": "^7.12.3", @@ -6054,9 +6054,9 @@ "dev": true }, "node_modules/leaflet-advanced-layer-system": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/leaflet-advanced-layer-system/-/leaflet-advanced-layer-system-2.2.4.tgz", - "integrity": "sha512-NL+lMosKSq5W25lOWa+NFpFY6D/PPQQ4TxwpyJ5eLFrRMablS2i/0JAIbfVcbbzh8I+rNisNF8AoqMIT0JWzIg==" + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/leaflet-advanced-layer-system/-/leaflet-advanced-layer-system-2.2.5.tgz", + "integrity": "sha512-W5RsUI94IIkCJnF6SKhLfIFdXPalzc9I/HKutxIzXgiUSqQBcFA74LFIXa9DrHdwU/OuguWj9v+NVifxEL/nWg==" }, "node_modules/leaflet-draw": { "version": "1.0.4", @@ -17598,9 +17598,9 @@ "dev": true }, "leaflet-advanced-layer-system": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/leaflet-advanced-layer-system/-/leaflet-advanced-layer-system-2.2.4.tgz", - "integrity": "sha512-NL+lMosKSq5W25lOWa+NFpFY6D/PPQQ4TxwpyJ5eLFrRMablS2i/0JAIbfVcbbzh8I+rNisNF8AoqMIT0JWzIg==" + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/leaflet-advanced-layer-system/-/leaflet-advanced-layer-system-2.2.5.tgz", + "integrity": "sha512-W5RsUI94IIkCJnF6SKhLfIFdXPalzc9I/HKutxIzXgiUSqQBcFA74LFIXa9DrHdwU/OuguWj9v+NVifxEL/nWg==" }, "leaflet-draw": { "version": "1.0.4", diff --git a/package.json b/package.json index c8de6628..77358565 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,6 @@ }, "dependencies": { "@electron/remote": "^2.0.1", - "leaflet-advanced-layer-system": "^2.2.4" + "leaflet-advanced-layer-system": "^2.2.5" } } From 6fe7b79c838cf5f64158b0699603a631338ae765 Mon Sep 17 00:00:00 2001 From: matafokka Date: Thu, 19 May 2022 17:22:52 +0300 Subject: [PATCH 46/48] Fixed serialization of rectangle-based layers Also bumpled ALS version to fix map widgets covering the map --- SynthGridLayer/serialization.js | 2 +- SynthPolygonBaseLayer/serialization.js | 9 +++++++-- package-lock.json | 14 +++++++------- package.json | 2 +- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/SynthGridLayer/serialization.js b/SynthGridLayer/serialization.js index d3cf70a2..9ebb97c8 100644 --- a/SynthGridLayer/serialization.js +++ b/SynthGridLayer/serialization.js @@ -9,7 +9,7 @@ L.ALS.SynthGridLayer.prototype.serialize = function (seenObjects) { let poly = this.polygons[id]; serialized.polygons.push({ - polygon: poly.getBounds(), + polygon: this.serializeRect(poly), widget: poly.widgetable.serialize(seenObjects), }); } diff --git a/SynthPolygonBaseLayer/serialization.js b/SynthPolygonBaseLayer/serialization.js index 347c22ed..50d66d8d 100644 --- a/SynthPolygonBaseLayer/serialization.js +++ b/SynthPolygonBaseLayer/serialization.js @@ -9,7 +9,7 @@ L.ALS.SynthPolygonBaseLayer.prototype.serialize = function (seenObjects) { if (poly.isCloned) return; - let serializedPoly = poly[poly instanceof L.Rectangle ? "getBounds" : "getLatLngs"](), + let serializedPoly = poly instanceof L.Rectangle ? this.serializeRect(poly) : poly.getLatLngs(), serializedStruct = {polygon: serializedPoly} serialized.polygons.push(serializedStruct); @@ -22,6 +22,11 @@ L.ALS.SynthPolygonBaseLayer.prototype.serialize = function (seenObjects) { return serialized; } +L.ALS.SynthPolygonBaseLayer.prototype.serializeRect = function (rect) { + let {_northEast, _southWest} = rect.getBounds(); + return [[_northEast.lat, _northEast.lng], [_southWest.lat, _southWest.lng]]; +} + L.ALS.SynthPolygonBaseLayer._toUpdateColors = ["borderColor", "fillColor", "color0", "color1"]; L.ALS.SynthPolygonBaseLayer.deserialize = function (serialized, layerSystem, settings, seenObjects) { @@ -30,7 +35,7 @@ L.ALS.SynthPolygonBaseLayer.deserialize = function (serialized, layerSystem, set for (let struct of serialized.polygons) { let {polygon, widget} = struct, - newPoly = L[polygon.serializableClassName === "L.LatLngBounds" ? "rectangle" : "polygon"](polygon); + newPoly = new L[polygon.length === 2 ? "Rectangle" : "Polygon"](polygon); object.polygonGroup.addLayer(newPoly); if (!widget) diff --git a/package-lock.json b/package-lock.json index fe52b862..6ba7f47b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "license": "GPL-3.0-or-later", "dependencies": { "@electron/remote": "^2.0.1", - "leaflet-advanced-layer-system": "^2.2.5" + "leaflet-advanced-layer-system": "^2.2.6" }, "devDependencies": { "@babel/core": "^7.12.3", @@ -6054,9 +6054,9 @@ "dev": true }, "node_modules/leaflet-advanced-layer-system": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/leaflet-advanced-layer-system/-/leaflet-advanced-layer-system-2.2.5.tgz", - "integrity": "sha512-W5RsUI94IIkCJnF6SKhLfIFdXPalzc9I/HKutxIzXgiUSqQBcFA74LFIXa9DrHdwU/OuguWj9v+NVifxEL/nWg==" + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/leaflet-advanced-layer-system/-/leaflet-advanced-layer-system-2.2.6.tgz", + "integrity": "sha512-QG5VcQJmP9NLZ5PhBbXBPXJ5gRpdlg2c44b8BORRhTxVJWQ//wSo4l1sjjGvYk+oZAvmJA26FyGBAqXNpz/0HQ==" }, "node_modules/leaflet-draw": { "version": "1.0.4", @@ -17598,9 +17598,9 @@ "dev": true }, "leaflet-advanced-layer-system": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/leaflet-advanced-layer-system/-/leaflet-advanced-layer-system-2.2.5.tgz", - "integrity": "sha512-W5RsUI94IIkCJnF6SKhLfIFdXPalzc9I/HKutxIzXgiUSqQBcFA74LFIXa9DrHdwU/OuguWj9v+NVifxEL/nWg==" + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/leaflet-advanced-layer-system/-/leaflet-advanced-layer-system-2.2.6.tgz", + "integrity": "sha512-QG5VcQJmP9NLZ5PhBbXBPXJ5gRpdlg2c44b8BORRhTxVJWQ//wSo4l1sjjGvYk+oZAvmJA26FyGBAqXNpz/0HQ==" }, "leaflet-draw": { "version": "1.0.4", diff --git a/package.json b/package.json index 77358565..66125ed2 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,6 @@ }, "dependencies": { "@electron/remote": "^2.0.1", - "leaflet-advanced-layer-system": "^2.2.5" + "leaflet-advanced-layer-system": "^2.2.6" } } From fcf048c360336e2c9c0a22178c515f6f9290ded0 Mon Sep 17 00:00:00 2001 From: matafokka Date: Thu, 19 May 2022 17:36:04 +0300 Subject: [PATCH 47/48] Added proper export to GeoJSON to the Polygon Layer --- SynthPolygonLayer/SynthPolygonLayer.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/SynthPolygonLayer/SynthPolygonLayer.js b/SynthPolygonLayer/SynthPolygonLayer.js index ea371fc3..ff8023df 100644 --- a/SynthPolygonLayer/SynthPolygonLayer.js +++ b/SynthPolygonLayer/SynthPolygonLayer.js @@ -2,6 +2,7 @@ require("./SynthPolygonWizard.js"); require("./SynthPolygonSettings.js"); const MathTools = require("../MathTools.js"); const proj4 = require("proj4"); +const geojsonMerge = require("@mapbox/geojson-merge"); // Using this since turfHelpers.featureCollection() discards previously defined properties. /** * Polygon layer @@ -367,6 +368,24 @@ L.ALS.SynthPolygonLayer = L.ALS.SynthPolygonBaseLayer.extend(/** @lends L.ALS.Sy return Math.sqrt(point[0] ** 2 + point[1] ** 2) <= this.maxGnomonicPointDistance; }, + toGeoJSON: function () { + let jsons = this.baseFeaturesToGeoJSON(); + + this.pointsGroup.eachLayer(layer => { + let pointsJson = layer.toGeoJSON(); + pointsJson.name = "capturePoint"; + jsons.push(pointsJson); + }); + + let props = {} + for (let param of this.propertiesToExport) + props[param] = this[param]; + + jsons.push(L.ALS.SynthBaseLayer.prototype.toGeoJSON.call(this, props)); + + return geojsonMerge.merge(jsons); + }, + statics: { wizard: L.ALS.SynthPolygonWizard, settings: new L.ALS.SynthPolygonSettings(), From eebd463e1be380791dd38a2b19d814a9bba53a1f Mon Sep 17 00:00:00 2001 From: matafokka Date: Thu, 19 May 2022 19:15:22 +0300 Subject: [PATCH 48/48] Preparing SynthFlight for the stable release God, I hope I fixed everything. Or at least everything major :p --- README.md | 42 +++++++++++++++++++++--------------------- package.json | 2 +- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 58d63671..f542ca8f 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,8 @@ -# SynthFlight Beta +# SynthFlight -SynthFlight is a fully client-side software for planning aerial photography. Run it either on the desktop or in a [browser online](https://matafokka.github.io/SynthFlight/). +SynthFlight is a fully client-side software for planning aerial photography. -This is a beta version, so bugs, huge API changes and lack of backwards compatibility are to be expected. - -Most of the planned functionality is here, however, a number of small changes will be introduced. - -A stable version will be released in May or June 2022. +Run it either on the desktop or in a [browser online](https://matafokka.github.io/SynthFlight/). # Setup @@ -26,17 +22,15 @@ There are numerous ways to set up SynthFlight, listed from most to least preferr 1. Extract the downloaded archive wherever you want. 1. Navigate to the extracted folder, open it and run `SynthFlight` executable file. -***Warning 1:** only Windows x64 builds has been tested so far.* +***Warning:** macOS builds are not signed, so you need to configure your system to run unsigned apps.* -***Warning 2:** macOS builds are not signed, thus require disabling the Gatekeeper or something.* +## System requirements -# System requirements - -## For PWA +### For PWA A browser that supports it. -## For browser +### For browser One of: @@ -55,8 +49,7 @@ One of: Of course, requirements for TLS might change in future with the new TLS versions coming out and GitHub and OSM changing their policies. You can't prevent this from happening, the only thing you can do is using an evergreen browser. - -## For desktop builds +### For desktop builds * **Operating system** - one of: * **Windows 7** or later. ARM64, x86 and x64 platforms are supported. @@ -65,6 +58,10 @@ Of course, requirements for TLS might change in future with the new TLS versions * **CPU**: One that can handle web surfing. If you can browse the internet, SynthFlight will work fine. * **RAM**: 1 GB or more. +# User guide + +Please, refer to the [Wiki page](https://github.com/matafokka/SynthFlight/wiki) for the user guide. + # Building If you want to build SynthFlight yourself, do the following: @@ -75,7 +72,7 @@ If you want to build SynthFlight yourself, do the following: 1. Build by running `node build.js`. There are additional options, to see them, run `node build.js -h`. 1. When build will be finished, in project root will be `dist` directory containing builds for different OSs and platforms. -***Warning:** To build for macOS, you may need to build on an actual macOS.* +***Warning:** To build for macOS, you may need to run everything on an actual macOS. I don't have macOS, so I can't test if builds in this repo actually work.* # Hosting @@ -90,16 +87,19 @@ There's a [development](https://github.com/matafokka/SynthFlight/tree/developmen Translating this app will be much appreciated. SynthFlight locales can be found in [`locales`](https://github.com/matafokka/SynthFlight/tree/development/locales) directory and ALS locales are available [here](https://github.com/matafokka/leaflet-advanced-layer-system/tree/master/locales). Both of these needs to be translated. Locales are plain JS objects where key is being used in the program itself and value is a string that's being added to the page. Only values needs translation. Copy one of the locales to a new file, change locale name and translate all the values. -You can also contribute by reporting bugs, requesting API changes, new functionality or something else. Please, create an issue and describe your request. +You can also contribute by reporting bugs or requesting new functionality. To do so, please, create an issue and describe your request. # FAQ ## Can a local copy work offline? Yes. -## Projects compatibility? +## Will my old projects be compatible with the new ALS version? + +Backwards compatibility is preserved unless noted otherwise in the release notes. However, this is unlikely to happen unless required for fixing a critical bug. + +## My plane crashed, images got ruined, and my dog died! Who should I blame?! -There will be no compatibility between SynthFlight versions until first stable release. +As license states, yourself :p -## When a stable release will be available? -May or June 2022 +However, if you've encountered a some kind of error, please, report it by creating an issue. diff --git a/package.json b/package.json index 66125ed2..e8d20061 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "synthflight", "productName": "SynthFlight", - "version": "0.2.0-beta", + "version": "1.0.0", "description": "A fully client-side software for planning aerial photography", "main": "electronApp.js", "browser": "index.html",