From 7b4d8cf36971f7965aada8d86191a9b11ee8d69d Mon Sep 17 00:00:00 2001 From: Daniel Kastl Date: Fri, 3 Mar 2023 20:08:15 +0900 Subject: [PATCH 001/109] Vector tiles support Signed-off-by: Daniel Kastl --- app/views/gtt_tile_sources/_form.html.erb | 9 +- config/locales/de.yml | 1 + config/locales/en.yml | 1 + config/locales/ja.yml | 1 + package.json | 3 +- src/components/gtt-client.ts | 81 +++++++++++++----- yarn.lock | 100 ++++++++++++---------- 7 files changed, 125 insertions(+), 71 deletions(-) diff --git a/app/views/gtt_tile_sources/_form.html.erb b/app/views/gtt_tile_sources/_form.html.erb index 2e2c72e1..eb448961 100644 --- a/app/views/gtt_tile_sources/_form.html.erb +++ b/app/views/gtt_tile_sources/_form.html.erb @@ -8,7 +8,8 @@ [l(:gtt_tile_sources_select_imagewms), 'ol.source.ImageWMS'], [l(:gtt_tile_sources_select_osm), 'ol.source.OSM'], [l(:gtt_tile_sources_select_tilewms), 'ol.source.TileWMS'], - [l(:gtt_tile_sources_select_xyz), 'ol.source.XYZ'] + [l(:gtt_tile_sources_select_xyz), 'ol.source.XYZ'], + [l(:gtt_tile_sources_select_vectortile), 'ol.source.VectorTile'] ], selected: f.object.type ), { include_blank: l(:gtt_tile_sources_select), required: true } %> @@ -36,7 +37,7 @@ "url": "https://tile.openstreetmap.jp/{z}/{x}/{y}.png", "custom": "19/34.74701/135.35740", "crossOrigin": null, - "attributions": "OpenStreetMap" + "attributions": "OpenStreetMap contributors" }, 'ol.source.TileWMS': { "url": "https://www.example.com/geoserver/wms", @@ -50,6 +51,10 @@ "url": "https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png", "projection": "EPSG:3857", "attributions": "地理院タイル" + }, + 'ol.source.VectorTile': { + "attributions": "OpenMapTiles OpenStreetMap contributors", + "styleUrl": "https://tile.openstreetmap.jp/styles/osm-bright-ja/style.json" } }; diff --git a/config/locales/de.yml b/config/locales/de.yml index 1eafb144..cc9d88c2 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -63,6 +63,7 @@ de: label_baselayer: Basislayer gtt_tile_sources_select_imagewms: Image WMS gtt_tile_sources_select_xyz: XYZ Tiles + gtt_tile_sources_select_vectortile: "Vector Tiles (MVT)" title_geojson_upload: GeoJSON importieren placeholder_geojson_upload: Bitte fügen Sie hier eine GeoJSON-Geometrie ein, oder importieren Sie die GeoJSON-Daten aus einer Datei. diff --git a/config/locales/en.yml b/config/locales/en.yml index e20864ad..e91b089a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -50,6 +50,7 @@ en: gtt_tile_sources_select_osm: "OpenStreetMap" gtt_tile_sources_select_tilewms: "Tiled WMS" gtt_tile_sources_select_xyz: "XYZ Tiles" + gtt_tile_sources_select_vectortile: "Vector Tiles (MVT)" gtt_tile_sources_load_example: "Load example configuration." gtt_map_rotate_label: "Map rotation" diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 63e70ac7..56867f95 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -48,6 +48,7 @@ ja: gtt_tile_sources_select_osm: "OpenStreetMap" gtt_tile_sources_select_tilewms: "Tiled WMS" gtt_tile_sources_select_xyz: "XYZ Tiles" + gtt_tile_sources_select_vectortile: "Vector Tiles (MVT)" gtt_tile_sources_load_example: "サンプル設定をロード。" gtt_settings_general_center_lon: "既定の地図中心経度" diff --git a/package.json b/package.json index d5b19bf7..9748dc59 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "@material-design-icons/font": "^0.14.2", "geojson": "^0.5.0", "ol": "^7.2.2", - "ol-ext": "4.0.4" + "ol-ext": "4.0.6", + "ol-mapbox-style": "^10.1.0" }, "devDependencies": { "@types/geojson": "^7946.0.10", diff --git a/src/components/gtt-client.ts b/src/components/gtt-client.ts index 49256661..2a661222 100644 --- a/src/components/gtt-client.ts +++ b/src/components/gtt-client.ts @@ -1,12 +1,12 @@ import 'ol/ol.css' import 'ol-ext/dist/ol-ext.min.css' -import { Map, Feature, View, Geolocation, Collection } from 'ol' +import { Map, Feature, View, Geolocation } from 'ol' import 'ol-ext/filter/Base' -import { Geometry, GeometryCollection, Point } from 'ol/geom' -import { GeoJSON, WKT } from 'ol/format' -import { Layer, Tile, Image } from 'ol/layer' +import { Geometry, Point } from 'ol/geom' +import { GeoJSON, WKT, MVT } from 'ol/format' +import { Layer, Tile, Image, VectorTile as VTLayer } from 'ol/layer' import VectorLayer from 'ol/layer/Vector' -import { OSM, XYZ, TileWMS, ImageWMS } from 'ol/source' +import { OSM, XYZ, TileWMS, ImageWMS, VectorTile as VTSource } from 'ol/source' import { Style, Fill, Stroke, Circle } from 'ol/style' import { OrderFunction } from 'ol/render' import { @@ -41,9 +41,8 @@ import { FeatureLike } from 'ol/Feature' import TileSource from 'ol/source/Tile' import ImageSource from 'ol/source/Image' import { Options as ImageWMSOptions } from 'ol/source/ImageWMS' -import JSONFeature from 'ol/format/JSONFeature' -import BaseEvent from 'ol/events/Event' -import { CollectionEvent } from 'ol/Collection' +import { Options as VectorTileOptions } from 'ol/source/VectorTile' +import { applyStyle } from 'ol-mapbox-style'; interface GttClientOption { target: HTMLDivElement | null @@ -65,11 +64,19 @@ interface FilterOption { interface TileLayerSource { layer: typeof Tile source: typeof OSM | typeof XYZ | typeof TileWMS + type: string } interface ImageLayerSource { layer: typeof Image source: typeof ImageWMS + type: string +} + +interface VTLayerSource { + layer: typeof VTLayer + source: typeof VTSource + type: string } export class GttClient { @@ -162,7 +169,7 @@ export class GttClient { this.reloadFontSymbol() // TODO: this is only necessary because setting the initial form value - // through the template causes encoding problems + // through the template causes encoding problems this.updateForm(features) this.layerArray = [] @@ -171,11 +178,11 @@ export class GttClient { layers.forEach((layer) => { const s = layer.type.split('.') const layerSource = getLayerSource(s[1], s[2]) - const tileLayerSource = layerSource as TileLayerSource - if (tileLayerSource) { - const l = new (tileLayerSource.layer)({ + if ( layerSource.type === "TileLayerSource") { + const config = layerSource as TileLayerSource + const l = new (config.layer)({ visible: false, - source: new (tileLayerSource.source)(layer.options as any) + source: new (config.source)(layer.options as any) }) l.set('lid', layer.id) @@ -191,11 +198,11 @@ export class GttClient { }) } this.layerArray.push(l) - } else if (layerSource as ImageLayerSource) { - const imageLayerSource = layerSource as ImageLayerSource - const l = new (imageLayerSource.layer)({ + } else if (layerSource.type === "ImageLayerSource") { + const config = layerSource as ImageLayerSource + const l = new (config.layer)({ visible: false, - source: new (imageLayerSource.source)(layer.options as ImageWMSOptions) + source: new (config.source)(layer.options as ImageWMSOptions) }) l.set('lid', layer.id) @@ -211,6 +218,34 @@ export class GttClient { }) } this.layerArray.push(l) + } else if (layerSource.type === "VTLayerSource") { + const config = layerSource as VTLayerSource + const options = layer.options as VectorTileOptions + options.format = new MVT() + const l = new (config.layer)({ + visible: false, + source: new (config.source)(options), + declutter: true + }) as VTLayer + + // Apply style URL if provided + if ("styleUrl" in options) { + applyStyle(l,options.styleUrl) + } + + l.set('lid', layer.id) + l.set('title', layer.name) + l.set('baseLayer', layer.baselayer) + if( layer.baselayer ) { + l.on('change:visible', (e: { target: any }) => { + const target = e.target as any + if (target.getVisible()) { + const lid = target.get('lid') + document.cookie = `_redmine_gtt_basemap=${lid};path=/` + } + }) + } + this.layerArray.push(l) } }, this) @@ -1349,16 +1384,18 @@ export class GttClient { } } -const getLayerSource = (source: string, class_name: string): TileLayerSource | ImageLayerSource | undefined => { +const getLayerSource = (source: string, class_name: string): TileLayerSource | ImageLayerSource | VTLayerSource | undefined => { if (source === 'source') { if (class_name === 'OSM') { - return { layer: Tile, source: OSM } + return { layer: Tile, source: OSM, type: "TileLayerSource" } } else if (class_name === 'XYZ') { - return { layer: Tile, source: XYZ } + return { layer: Tile, source: XYZ, type: "TileLayerSource" } } else if (class_name === 'TileWMS') { - return { layer: Tile, source: TileWMS } + return { layer: Tile, source: TileWMS, type: "TileLayerSource" } } else if (class_name === 'ImageWMS') { - return { layer: Image, source: ImageWMS } + return { layer: Image, source: ImageWMS, type: "ImageLayerSource" } + } else if (class_name === 'VectorTile') { + return { layer: VTLayer, source: VTSource, type: "VTLayerSource" } } } return undefined diff --git a/yarn.lock b/yarn.lock index 7c33cb0b..3a33cc52 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3,9 +3,9 @@ "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0": - version "7.20.13" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.13.tgz#7055ab8a7cff2b8f6058bf6ae45ff84ad2aded4b" - integrity sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA== + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673" + integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw== dependencies: regenerator-runtime "^0.13.11" @@ -89,9 +89,9 @@ integrity sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA== "@material-design-icons/font@^0.14.2": - version "0.14.2" - resolved "https://registry.yarnpkg.com/@material-design-icons/font/-/font-0.14.2.tgz#5b02c3bda37022645cc2df0200be02b6bc91ab4a" - integrity sha512-svLx/Q6WidjiSE1rT9joMy241x+gLb0SfNrVR0Kd3GyPyU5HXRmXvjWDj2h2RHzAsgxFqPGez4hL+EmbjZiYSg== + version "0.14.3" + resolved "https://registry.yarnpkg.com/@material-design-icons/font/-/font-0.14.3.tgz#a3947bc2a6f0aea38ebed29eeb38b5f61be2e438" + integrity sha512-nnxcqi/UZwRnH6IRGAUkjO/pc8jlA+7nEmPBNwWkjtRtKDFw1RiumtS3yR9FvYr4/pylaE4Ewz68t2ROZCvroA== "@petamoriken/float16@^3.4.7": version "3.7.1" @@ -107,9 +107,9 @@ "@types/estree" "*" "@types/eslint@*": - version "8.21.0" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.21.0.tgz#21724cfe12b96696feafab05829695d4d7bd7c48" - integrity sha512-35EhHNOXgxnUgh4XCJsGhE7zdlDhYDN/aMG6UbkByCFFNgQ7b3U+uVoqBpicFydR8JEfgdjCF7SJ7MiJfzuiTA== + version "8.21.1" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.21.1.tgz#110b441a210d53ab47795124dbc3e9bb993d1e7c" + integrity sha512-rc9K8ZpVjNcLs8Fp0dkozd5Pt2Apk1glO4Vgz8ix1u6yFByxfqo5Yavpy65o+93TAe24jr7v+eSBtFLvOQtCRQ== dependencies: "@types/estree" "*" "@types/json-schema" "*" @@ -149,9 +149,9 @@ integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== "@types/node@*": - version "18.11.18" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.18.tgz#8dfb97f0da23c2293e554c5a50d61ef134d7697f" - integrity sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA== + version "18.14.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.14.5.tgz#4a13a6445862159303fc38586598a9396fc408b3" + integrity sha512-CRT4tMK/DHYhw1fcCEBwME9CSaZNclxfzVMe7GsO6ULSwsttbj70wSiX6rZdIjGblu93sTJxLdhNIT85KKI7Qw== "@types/ol-ext@npm:@siedlerchr/types-ol-ext": version "3.0.9" @@ -399,9 +399,9 @@ buffer-from@^1.0.0: integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== caniuse-lite@^1.0.30001449: - version "1.0.30001450" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001450.tgz#022225b91200589196b814b51b1bbe45144cf74f" - integrity sha512-qMBmvmQmFXaSxexkjjfMvD5rnDL0+m+dUMZKoDYsGG8iZN29RuYh9eRoMvKsT6uMAWlyUUGDEQGJJYjzCIO9ew== + version "1.0.30001460" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001460.tgz#31d2e26f0a2309860ed3eff154e03890d9d851a7" + integrity sha512-Bud7abqjvEjipUkpLs4D7gR0l8hBYBHoa+tGtKJHvT2AYzLp1z7EmVkUT4ERpVUfca8S2HGIVs883D8pUH1ZzQ== canvg@^3.0.6: version "3.0.10" @@ -482,9 +482,9 @@ commander@^9.4.1: integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== core-js@^3.6.0, core-js@^3.8.3: - version "3.27.2" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.27.2.tgz#85b35453a424abdcacb97474797815f4d62ebbf7" - integrity sha512-9ashVQskuh5AZEZ1JdQWp1GqSoC1e1G87MzRqg2gIfVAQ7Qn9K+uFj8EcniUFA4P2NLZfV+TOlX1SzoKfo+s7w== + version "3.29.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.29.0.tgz#0273e142b67761058bcde5615c503c7406b572d6" + integrity sha512-VG23vuEisJNkGl6XQmFJd3rEG/so/CNatqeE+7uZAwTSwFeB/qaO0be8xZYUNWprJ/GIwL8aMt9cj1kvbpTZhg== cross-spawn@^7.0.3: version "7.0.3" @@ -527,9 +527,9 @@ cssesc@^3.0.0: integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== dompurify@^2.2.0: - version "2.4.3" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.4.3.tgz#f4133af0e6a50297fc8874e2eaedc13a3c308c03" - integrity sha512-q6QaLcakcRjebxjg8/+NP+h0rPfatOgOzc46Fst9VAA3jF2ApfKBNKMzdP4DYTqtUMXSCd5pRS/8Po/OmoCHZQ== + version "2.4.5" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.4.5.tgz#0e89a27601f0bad978f9a924e7a05d5d2cccdd87" + integrity sha512-jggCCd+8Iqp4Tsz0nIvpcb22InKEBrGz5dw3EQJMs8HPJDsKbFIO3STYtAvCfDx26Muevn1MHVI0XxjgFfmiSA== earcut@^2.2.3: version "2.2.4" @@ -537,9 +537,9 @@ earcut@^2.2.3: integrity sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ== electron-to-chromium@^1.4.284: - version "1.4.284" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz#61046d1e4cab3a25238f6bf7413795270f125592" - integrity sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA== + version "1.4.317" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.317.tgz#9a3d38a1a37f26a417d3d95dafe198ff11ed072b" + integrity sha512-JhCRm9v30FMNzQSsjl4kXaygU+qHBD0Yh7mKxyjmF0V8VwYVB6qpBRX28GyAucrM9wDCpSUctT6FpMUQxbyKuA== enhanced-resolve@^5.0.0, enhanced-resolve@^5.10.0: version "5.12.0" @@ -705,9 +705,9 @@ ieee754@^1.1.12: integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== immutable@^4.0.0: - version "4.2.2" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.2.2.tgz#2da9ff4384a4330c36d4d1bc88e90f9e0b0ccd16" - integrity sha512-fTMKDwtbvO5tldky9QZ2fMX7slR0mYpY5nbnFWYp0fOzDhHqhgIw9KoYgxLWsoNTS9ZHGauHj18DTyEw6BK3Og== + version "4.2.4" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.2.4.tgz#83260d50889526b4b531a5e293709a77f7c55a2a" + integrity sha512-WDxL3Hheb1JkRN3sQkyujNlL/xRjAo3rJtaU5xeufUauG66JdMr32bLj4gF+vWl84DIA3Zxw7tiAjneYzRRw+w== import-local@^3.0.2: version "3.1.0" @@ -874,9 +874,9 @@ mime-types@^2.1.27: mime-db "1.52.0" minimist@^1.2.6: - version "1.2.7" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" - integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== nanoid@^3.3.4: version "3.3.4" @@ -889,24 +889,32 @@ neo-async@^2.6.2: integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== node-releases@^2.0.8: - version "2.0.9" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.9.tgz#fe66405285382b0c4ac6bcfbfbe7e8a510650b4d" - integrity sha512-2xfmOrRkGogbTK9R6Leda0DGiXeY3p2NJpy4+gNCffdUvV6mdEJnaDEic1i3Ec2djAo8jWYoJMR5PB0MSMpxUA== + version "2.0.10" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.10.tgz#c311ebae3b6a148c89b1813fd7c4d3c024ef537f" + integrity sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w== normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -ol-ext@4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/ol-ext/-/ol-ext-4.0.4.tgz#1474253d60c48378f0e7753e3e382835fd2cb21f" - integrity sha512-VMSsBc/vuWgMvk2U8qUNLN0kKjsuPowISmqmMB9Bs8PfyHBNBKCUgMkYOVzO2cX5EQOdVkkcsa0N/Oln5L0oYA== +ol-ext@4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/ol-ext/-/ol-ext-4.0.6.tgz#5c9d8c7042f6b88dd21a725bbb2429162248badd" + integrity sha512-SG90ILjlHT302Zqgr3AR87M4yq4E96g2SJWlkliZDcF++QK4rMja1lId0w/cKMuGSPWSC79lD73eh1vcQlHdGw== + +ol-mapbox-style@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/ol-mapbox-style/-/ol-mapbox-style-10.1.0.tgz#fa2b6d0a9a1ba17b4305b05d38a426a8527e7e4a" + integrity sha512-ujMjK/p/QFGs9ZebkL+x3ceRnzcilHt1Y2/oeMNdvulwjl/zbIG9oHRiwdTW/zHVFpqauTtgpFJFtoqseBMunA== + dependencies: + "@mapbox/mapbox-gl-style-spec" "^13.23.1" + mapbox-to-css-font "^2.4.1" ol-mapbox-style@^9.2.0: - version "9.5.0" - resolved "https://registry.yarnpkg.com/ol-mapbox-style/-/ol-mapbox-style-9.5.0.tgz#94dfa7ade57ee0b442a79162a1d57b70778dda4d" - integrity sha512-ArYzeuZ4dOYZ6UnhXK30kNBdouZay/r6LzLsFGnvJ86lxY6ShJu2FtcKCFGCOZoJZYyoLCStHYOHcRrVLDaJ0Q== + version "9.7.0" + resolved "https://registry.yarnpkg.com/ol-mapbox-style/-/ol-mapbox-style-9.7.0.tgz#38a4f7abc8f0a94f378dcdb7cefdcc69ca3f6287" + integrity sha512-YX3u8FBJHsRHaoGxmd724Mp5WPTuV7wLQW6zZhcihMuInsSdCX1EiZfU+8IAL7jG0pbgl5YgC0aWE/MXJcUXxg== dependencies: "@mapbox/mapbox-gl-style-spec" "^13.23.1" mapbox-to-css-font "^2.4.1" @@ -1158,9 +1166,9 @@ sass-loader@^13.2.0: neo-async "^2.6.2" sass@^1.58.0: - version "1.58.0" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.58.0.tgz#ee8aea3ad5ea5c485c26b3096e2df6087d0bb1cc" - integrity sha512-PiMJcP33DdKtZ/1jSjjqVIKihoDc6yWmYr9K/4r3fVVIEDAluD0q7XZiRKrNJcPK3qkLRF/79DND1H5q1LBjgg== + version "1.58.3" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.58.3.tgz#2348cc052061ba4f00243a208b09c40e031f270d" + integrity sha512-Q7RaEtYf6BflYrQ+buPudKR26/lH+10EmO9bBqbmPh/KeLqv8bjpTNqxe71ocONqXq+jYiCbpPUmQMS+JJPk4A== dependencies: chokidar ">=3.0.0 <4.0.0" immutable "^4.0.0" @@ -1295,9 +1303,9 @@ terser-webpack-plugin@^5.1.3: terser "^5.14.1" terser@^5.14.1: - version "5.16.2" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.16.2.tgz#8f495819439e8b5c150e7530fc434a6e70ea18b2" - integrity sha512-JKuM+KvvWVqT7muHVyrwv7FVRPnmHDwF6XwoIxdbF5Witi0vu99RYpxDexpJndXt3jbZZmmWr2/mQa6HvSNdSg== + version "5.16.5" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.16.5.tgz#1c285ca0655f467f92af1bbab46ab72d1cb08e5a" + integrity sha512-qcwfg4+RZa3YvlFh0qjifnzBHjKGNbtDo9yivMqMFDy9Q6FSaQWSB/j1xKhsoUFJIqDOM3TsN6D5xbrMrFcHbg== dependencies: "@jridgewell/source-map" "^0.3.2" acorn "^8.5.0" From a642ab8ab5b911ccef0b1948dd66a5861e6d46c7 Mon Sep 17 00:00:00 2001 From: Daniel Kastl Date: Mon, 6 Mar 2023 19:19:22 +0900 Subject: [PATCH 002/109] Updates deface ID Signed-off-by: Daniel Kastl --- app/overrides/issues.rb | 2 +- app/overrides/projects.rb | 2 +- app/overrides/users.rb | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/overrides/issues.rb b/app/overrides/issues.rb index ed7595a0..83475e82 100644 --- a/app/overrides/issues.rb +++ b/app/overrides/issues.rb @@ -19,7 +19,7 @@ module Issues :virtual_path => "issues/show", :name => "deface_view_issues_show_map", :insert_before => "div.attributes", - :original => '2042e7171bb2eaa98259e9ccbff8209910565e7a', + :original => 'c56981aa84b0fee66ff43ea773cf1444193a2862', :partial => "issues/show/map" ) diff --git a/app/overrides/projects.rb b/app/overrides/projects.rb index 5c3a6c67..2ec8baa1 100644 --- a/app/overrides/projects.rb +++ b/app/overrides/projects.rb @@ -11,7 +11,7 @@ module Projects :virtual_path => "projects/show", :name => "deface_view_projects_show_other_formats", :insert_bottom => "div.splitcontentright", - :original => '868cdb34ed1c52af47076776295dcba1311914f9', + :original => '1d2f0cb0b1439dddc34ac9c50b6b1b111fe702ce', :partial => "projects/show/other_formats" ) end diff --git a/app/overrides/users.rb b/app/overrides/users.rb index de3b5836..824cd4c1 100644 --- a/app/overrides/users.rb +++ b/app/overrides/users.rb @@ -3,6 +3,7 @@ module Users :virtual_path => "users/show", :name => "deface_view_users_show_other_formats", :insert_bottom => "div.splitcontentleft", + :original => 'abe916df0691ebe8848cfc0dde536abd3bfe39b8', :partial => "users/show/other_formats" ) end From b404692e9394f0c18e582cf6152e871769603f86 Mon Sep 17 00:00:00 2001 From: Daniel Kastl Date: Thu, 16 Feb 2023 09:31:57 +0000 Subject: [PATCH 003/109] Translated using OSGeo Weblate (German) Currently translated at 100.0% (76 of 76 strings) Translation: GTT Project/Redmine GTT core plugin Translate-URL: https://weblate.osgeo.org/projects/gtt-project/redmine_gtt/de/ --- config/locales/de.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/locales/de.yml b/config/locales/de.yml index 1eafb144..4fc61f3f 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -82,3 +82,5 @@ de: modal: load: Laden cancel: Abbrechen + gtt_map_rotate_label: Kartenrotation + gtt_map_rotate_info_html: Hold down Shift+Alt and drag the map to rotate. From 2d6ec7d4dae06dd356f987f2be215bd33fe09a09 Mon Sep 17 00:00:00 2001 From: Daniel Kastl Date: Wed, 8 Mar 2023 10:58:42 +0900 Subject: [PATCH 004/109] Updates API endpoints Signed-off-by: Daniel Kastl --- app/views/issues/index.api.rsb | 7 ++++++- app/views/issues/show.api.rsb | 9 ++++++++- app/views/projects/index.api.rsb | 1 + app/views/projects/show.api.rsb | 4 +++- app/views/users/index.api.rsb | 7 ++++++- app/views/users/show.api.rsb | 9 +++++++-- 6 files changed, 31 insertions(+), 6 deletions(-) diff --git a/app/views/issues/index.api.rsb b/app/views/issues/index.api.rsb index f44e805b..83fd0c4e 100644 --- a/app/views/issues/index.api.rsb +++ b/app/views/issues/index.api.rsb @@ -4,7 +4,7 @@ api.array :issues, api_meta(:total_count => @issue_count, :offset => @offset, :l api.id issue.id api.project(:id => issue.project_id, :name => issue.project.name) unless issue.project.nil? api.tracker(:id => issue.tracker_id, :name => issue.tracker.name) unless issue.tracker.nil? - api.status(:id => issue.status_id, :name => issue.status.name) unless issue.status.nil? + api.status(:id => issue.status_id, :name => issue.status.name, :is_closed => issue.status.is_closed) unless issue.status.nil? api.priority(:id => issue.priority_id, :name => issue.priority.name) unless issue.priority.nil? api.author(:id => issue.author_id, :name => issue.author.name) unless issue.author.nil? api.assigned_to(:id => issue.assigned_to_id, :name => issue.assigned_to.name) unless issue.assigned_to.nil? @@ -19,6 +19,11 @@ api.array :issues, api_meta(:total_count => @issue_count, :offset => @offset, :l api.done_ratio issue.done_ratio api.is_private issue.is_private api.estimated_hours issue.estimated_hours + api.total_estimated_hours issue.total_estimated_hours + if User.current.allowed_to?(:view_time_entries, issue.project) + api.spent_hours(issue.spent_hours) + api.total_spent_hours(issue.total_spent_hours) + end if issue.geom api.geojson (params[:format] == "json") ? issue.geojson : issue.geojson.to_json diff --git a/app/views/issues/show.api.rsb b/app/views/issues/show.api.rsb index da68ee6c..c0da42fb 100644 --- a/app/views/issues/show.api.rsb +++ b/app/views/issues/show.api.rsb @@ -2,7 +2,7 @@ api.issue do api.id @issue.id api.project(:id => @issue.project_id, :name => @issue.project.name) unless @issue.project.nil? api.tracker(:id => @issue.tracker_id, :name => @issue.tracker.name) unless @issue.tracker.nil? - api.status(:id => @issue.status_id, :name => @issue.status.name) unless @issue.status.nil? + api.status(:id => @issue.status_id, :name => @issue.status.name, :is_closed => @issue.status.is_closed) unless @issue.status.nil? api.priority(:id => @issue.priority_id, :name => @issue.priority.name) unless @issue.priority.nil? api.author(:id => @issue.author_id, :name => @issue.author.name) unless @issue.author.nil? api.assigned_to(:id => @issue.assigned_to_id, :name => @issue.assigned_to.name) unless @issue.assigned_to.nil? @@ -36,6 +36,7 @@ api.issue do api.closed_on @issue.closed_on render_api_issue_children(@issue, api) if include_in_api_response?('children') + api.array :attachments do @issue.attachments.each do |attachment| render_api_attachment(attachment, api) @@ -82,4 +83,10 @@ api.issue do api.user :id => user.id, :name => user.name end end if include_in_api_response?('watchers') && User.current.allowed_to?(:view_issue_watchers, @issue.project) + + api.array :allowed_statuses do + @allowed_statuses.each do |status| + api.status :id => status.id, :name => status.name, :is_closed => status.is_closed + end + end if include_in_api_response?('allowed_statuses') end diff --git a/app/views/projects/index.api.rsb b/app/views/projects/index.api.rsb index a57ac6e6..9d517f5a 100644 --- a/app/views/projects/index.api.rsb +++ b/app/views/projects/index.api.rsb @@ -8,6 +8,7 @@ api.array :projects, api_meta(:total_count => @project_count, :offset => @offset api.parent(:id => project.parent.id, :name => project.parent.name) if project.parent && project.parent.visible? api.status project.status api.is_public project.is_public? + api.inherit_members project.inherit_members? api.rotation project.map_rotation if @include_geometry diff --git a/app/views/projects/show.api.rsb b/app/views/projects/show.api.rsb index 89a6e1ad..f67492d7 100644 --- a/app/views/projects/show.api.rsb +++ b/app/views/projects/show.api.rsb @@ -7,7 +7,9 @@ api.project do api.parent(:id => @project.parent.id, :name => @project.parent.name) if @project.parent && @project.parent.visible? api.status @project.status api.is_public @project.is_public? - api.rotation @project.map_rotation + api.inherit_members @project.inherit_members? + api.default_version(:id => @project.default_version.id, :name => @project.default_version.name) if @project.default_version + api.default_assignee(:id => @project.project.default_assigned_to.id, :name => @project.project.default_assigned_to.name) if @project.default_assigned_to if @project.geom api.geojson (params[:format] == "json") ? @project.geojson : @project.geojson.to_json diff --git a/app/views/users/index.api.rsb b/app/views/users/index.api.rsb index d6da6195..2145332e 100644 --- a/app/views/users/index.api.rsb +++ b/app/views/users/index.api.rsb @@ -3,11 +3,16 @@ api.array :users, api_meta(:total_count => @user_count, :offset => @offset, :lim api.user do api.id user.id api.login user.login + api.admin user.admin? api.firstname user.firstname api.lastname user.lastname api.mail user.mail api.created_on user.created_on - api.last_login_on user.last_login_on + api.updated_on user.updated_on + api.last_login_on user.last_login_on + api.passwd_changed_on user.passwd_changed_on + api.avatar_url gravatar_url(user.mail, {rating: nil, size: nil, default: Setting.gravatar_default}) if Setting.gravatar_enabled? + api.twofa_scheme user.twofa_scheme if user.geom api.geojson (params[:format] == "json") ? user.geojson : user.geojson.to_json diff --git a/app/views/users/show.api.rsb b/app/views/users/show.api.rsb index 7941549a..f4f0d564 100644 --- a/app/views/users/show.api.rsb +++ b/app/views/users/show.api.rsb @@ -1,11 +1,16 @@ api.user do api.id @user.id - api.login @user.login if User.current.admin? || (User.current == @user) + api.login @user.login + api.admin @user.admin? if User.current.admin? || (User.current == @user) api.firstname @user.firstname api.lastname @user.lastname api.mail @user.mail if User.current.admin? || !@user.pref.hide_mail api.created_on @user.created_on - api.last_login_on @user.last_login_on + api.updated_on @user.updated_on + api.last_login_on @user.last_login_on + api.passwd_changed_on @user.passwd_changed_on + api.avatar_url gravatar_url(@user.mail, {rating: nil, size: nil, default: Setting.gravatar_default}) if @user.mail && Setting.gravatar_enabled? + api.twofa_scheme @user.twofa_scheme if User.current.admin? || (User.current == @user) api.api_key @user.api_key if User.current.admin? || (User.current == @user) api.status @user.status if User.current.admin? From 1b44fa0c453e5f63555b912bc87d7ed63efe9c56 Mon Sep 17 00:00:00 2001 From: Daniel Kastl Date: Wed, 8 Mar 2023 11:05:47 +0900 Subject: [PATCH 005/109] Re-adds rotation Signed-off-by: Daniel Kastl --- app/views/projects/show.api.rsb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/projects/show.api.rsb b/app/views/projects/show.api.rsb index f67492d7..332e7e33 100644 --- a/app/views/projects/show.api.rsb +++ b/app/views/projects/show.api.rsb @@ -16,6 +16,7 @@ api.project do else api.geojson nil end + api.rotation @project.map_rotation render_api_custom_values @project.visible_custom_field_values, api render_api_includes(@project, api) From 16ce7ff1d1a726c5694e49de1aba0502a67ad3a2 Mon Sep 17 00:00:00 2001 From: Daniel Kastl Date: Tue, 14 Mar 2023 11:14:17 +0900 Subject: [PATCH 006/109] Update test-postgis.yml --- .github/workflows/test-postgis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-postgis.yml b/.github/workflows/test-postgis.yml index 9dad3fb1..ea1a1291 100644 --- a/.github/workflows/test-postgis.yml +++ b/.github/workflows/test-postgis.yml @@ -7,11 +7,11 @@ on: push: branches: - main - - master + - next pull_request: branches: - main - - master + - next jobs: test: From c8ee9f823420aafdc79a170db000d5692f1539ae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Mar 2023 11:14:25 +0000 Subject: [PATCH 007/109] Bump webpack from 5.75.0 to 5.76.0 Bumps [webpack](https://github.com/webpack/webpack) from 5.75.0 to 5.76.0. - [Release notes](https://github.com/webpack/webpack/releases) - [Commits](https://github.com/webpack/webpack/compare/v5.75.0...v5.76.0) --- updated-dependencies: - dependency-name: webpack dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index d5b19bf7..5613c86b 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "style-loader": "^3.3.1", "ts-loader": "^9.4.2", "typescript": "^4.9.5", - "webpack": "^5.75.0", + "webpack": "^5.76.0", "webpack-cli": "^5.0.1" } } diff --git a/yarn.lock b/yarn.lock index 7c33cb0b..fa15d1fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1405,10 +1405,10 @@ webpack-sources@^3.2.3: resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== -webpack@^5.75.0: - version "5.75.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.75.0.tgz#1e440468647b2505860e94c9ff3e44d5b582c152" - integrity sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ== +webpack@^5.76.0: + version "5.76.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.76.0.tgz#f9fb9fb8c4a7dbdcd0d56a98e56b8a942ee2692c" + integrity sha512-l5sOdYBDunyf72HW8dF23rFtWq/7Zgvt/9ftMof71E/yUb1YLOBmTgA2K4vQthB3kotMrSj609txVE0dnr2fjA== dependencies: "@types/eslint-scope" "^3.7.3" "@types/estree" "^0.0.51" From f4c3d6bfba165c5cdfd13cd2b5b1a0c4711078e3 Mon Sep 17 00:00:00 2001 From: Daniel Kastl Date: Thu, 6 Apr 2023 07:51:14 +0900 Subject: [PATCH 008/109] Fetches minor changes from next Signed-off-by: Daniel Kastl --- .gitignore | 11 +++++------ Gemfile | 36 ++++++++++++++++++++++++++++++++---- tsconfig.json | 11 ++++++----- 3 files changed, 43 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index 351b9ab2..caa11f83 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,12 @@ +# Ignore commonly generated/ignored files .DS_Store -node_modules -# webpack generate files -assets/javascripts/main.js -assets/javascripts/main.js.* +node_modules/ + +# Ignore webpack generated files +assets/javascripts/main.js* assets/javascripts/*.png assets/javascripts/*.svg assets/javascripts/*.ttf assets/javascripts/*.eot assets/javascripts/*.woff2 assets/javascripts/*.woff - - diff --git a/Gemfile b/Gemfile index 37706324..375c990e 100644 --- a/Gemfile +++ b/Gemfile @@ -4,7 +4,35 @@ gem 'deface' gem 'immutable-struct' gem "rgeo", "~> 2.4.0" gem "rgeo-geojson" -gem "pg", (ENV['GEM_PG_VERSION'] ? "~> #{ENV['GEM_PG_VERSION']}" : "~> 1.2.2") # make sure we use a version compatible with AR -gem "rgeo-activerecord", (ENV['GEM_RGEO_ACTIVERECORD_VERSION'] ? "~> #{ENV['GEM_RGEO_ACTIVERECORD_VERSION']}" : "~> 7.0.1") # same as above -gem 'activerecord-postgis-adapter', (ENV['GEM_ACTIVERECORD_POSTGIS_ADAPTER_VERSION'] ? "~> #{ENV['GEM_ACTIVERECORD_POSTGIS_ADAPTER_VERSION']}" : "~> 7.1.1") # same as above -gem 'rails-controller-testing' # This gem brings back assigns to your controller tests as well as assert_template to both controller and integration tests. +gem "pg", compatible_pg_version +gem "rgeo-activerecord", compatible_rgeo_activerecord_version +gem 'activerecord-postgis-adapter', compatible_activerecord_postgis_adapter_version +gem 'rails-controller-testing' + +group :development, :test do + gem 'pry' +end + +def compatible_pg_version + if ENV['GEM_PG_VERSION'] + "~> #{ENV['GEM_PG_VERSION']}" + else + "~> 1.2.2" + end +end + +def compatible_rgeo_activerecord_version + if ENV['GEM_RGEO_ACTIVERECORD_VERSION'] + "~> #{ENV['GEM_RGEO_ACTIVERECORD_VERSION']}" + else + "~> 7.0.1" + end +end + +def compatible_activerecord_postgis_adapter_version + if ENV['GEM_ACTIVERECORD_POSTGIS_ADAPTER_VERSION'] + "~> #{ENV['GEM_ACTIVERECORD_POSTGIS_ADAPTER_VERSION']}" + else + "~> 7.1.1" + end +end diff --git a/tsconfig.json b/tsconfig.json index 23bc2bfe..8361195a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,14 +1,15 @@ { "compilerOptions": { - "outDir": "./dist/", - "noImplicitAny": true, - "module": "es6", + "outDir": "./dist", "target": "es5", + "module": "es6", + "moduleResolution": "node", "jsx": "react", "allowJs": true, - "moduleResolution": "node", + "noImplicitAny": true, + "baseUrl": "./src/", "paths": { - "*": ["./src/@types/*"] + "*": ["@types/*"] } } } From bd1c3c2dfd00e076fefa0a05f772939e33f27f5b Mon Sep 17 00:00:00 2001 From: Daniel Kastl Date: Thu, 6 Apr 2023 10:08:57 +0900 Subject: [PATCH 009/109] Revert changes Signed-off-by: Daniel Kastl --- Gemfile | 36 ++++-------------------------------- 1 file changed, 4 insertions(+), 32 deletions(-) diff --git a/Gemfile b/Gemfile index 375c990e..37706324 100644 --- a/Gemfile +++ b/Gemfile @@ -4,35 +4,7 @@ gem 'deface' gem 'immutable-struct' gem "rgeo", "~> 2.4.0" gem "rgeo-geojson" -gem "pg", compatible_pg_version -gem "rgeo-activerecord", compatible_rgeo_activerecord_version -gem 'activerecord-postgis-adapter', compatible_activerecord_postgis_adapter_version -gem 'rails-controller-testing' - -group :development, :test do - gem 'pry' -end - -def compatible_pg_version - if ENV['GEM_PG_VERSION'] - "~> #{ENV['GEM_PG_VERSION']}" - else - "~> 1.2.2" - end -end - -def compatible_rgeo_activerecord_version - if ENV['GEM_RGEO_ACTIVERECORD_VERSION'] - "~> #{ENV['GEM_RGEO_ACTIVERECORD_VERSION']}" - else - "~> 7.0.1" - end -end - -def compatible_activerecord_postgis_adapter_version - if ENV['GEM_ACTIVERECORD_POSTGIS_ADAPTER_VERSION'] - "~> #{ENV['GEM_ACTIVERECORD_POSTGIS_ADAPTER_VERSION']}" - else - "~> 7.1.1" - end -end +gem "pg", (ENV['GEM_PG_VERSION'] ? "~> #{ENV['GEM_PG_VERSION']}" : "~> 1.2.2") # make sure we use a version compatible with AR +gem "rgeo-activerecord", (ENV['GEM_RGEO_ACTIVERECORD_VERSION'] ? "~> #{ENV['GEM_RGEO_ACTIVERECORD_VERSION']}" : "~> 7.0.1") # same as above +gem 'activerecord-postgis-adapter', (ENV['GEM_ACTIVERECORD_POSTGIS_ADAPTER_VERSION'] ? "~> #{ENV['GEM_ACTIVERECORD_POSTGIS_ADAPTER_VERSION']}" : "~> 7.1.1") # same as above +gem 'rails-controller-testing' # This gem brings back assigns to your controller tests as well as assert_template to both controller and integration tests. From 4585e474df304c7fa404b54c4a1d393c846e0a11 Mon Sep 17 00:00:00 2001 From: Daniel Kastl Date: Thu, 6 Apr 2023 10:15:19 +0900 Subject: [PATCH 010/109] Updates requirements for EOL 2023 dependencies Signed-off-by: Daniel Kastl --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f0ae5dbd..9221f6be 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,9 @@ Redmine GTT plugins **require PostgreSQL/PostGIS** and will not work with SQLite or MariaDB/MySQL!!! - Redmine >= 4.2.0 -- PostgreSQL >= 10 +- PostgreSQL >= 12 - PostGIS >= 2.5 -- NodeJS v14 +- NodeJS v18 - yarn ## Installation From 430c562bb2fa585f01f41165f43d31e0f61719ba Mon Sep 17 00:00:00 2001 From: Daniel Kastl Date: Thu, 6 Apr 2023 10:36:14 +0900 Subject: [PATCH 011/109] Updates dependencies Signed-off-by: Daniel Kastl --- package.json | 4 +- yarn.lock | 140 +++++++++++++++++++++++++-------------------------- 2 files changed, 72 insertions(+), 72 deletions(-) diff --git a/package.json b/package.json index c167a69d..398395f1 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "@material-design-icons/font": "^0.14.2", "geojson": "^0.5.0", "ol": "^7.2.2", - "ol-ext": "4.0.6", + "ol-ext": "^4.0.7", "ol-mapbox-style": "^10.1.0" }, "devDependencies": { @@ -37,7 +37,7 @@ "sass-loader": "^13.2.0", "style-loader": "^3.3.1", "ts-loader": "^9.4.2", - "typescript": "^4.9.5", + "typescript": "^5.0.3", "webpack": "^5.76.0", "webpack-cli": "^5.0.1" } diff --git a/yarn.lock b/yarn.lock index 15b42568..5e303bdc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -46,7 +46,7 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== -"@jridgewell/trace-mapping@^0.3.14", "@jridgewell/trace-mapping@^0.3.9": +"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": version "0.3.17" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985" integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g== @@ -89,14 +89,14 @@ integrity sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA== "@material-design-icons/font@^0.14.2": - version "0.14.3" - resolved "https://registry.yarnpkg.com/@material-design-icons/font/-/font-0.14.3.tgz#a3947bc2a6f0aea38ebed29eeb38b5f61be2e438" - integrity sha512-nnxcqi/UZwRnH6IRGAUkjO/pc8jlA+7nEmPBNwWkjtRtKDFw1RiumtS3yR9FvYr4/pylaE4Ewz68t2ROZCvroA== + version "0.14.5" + resolved "https://registry.yarnpkg.com/@material-design-icons/font/-/font-0.14.5.tgz#732dd4dd48a56789aad26d8fb22918dcd5207a38" + integrity sha512-EDk8mOZoOqnAKqAFY5MM5F+7gCclu8sXJsyJhPAgPPyL7DUCJenwOWTywNdlSwX7C+fXgA44aRndIbaTYR3PQg== "@petamoriken/float16@^3.4.7": - version "3.7.1" - resolved "https://registry.yarnpkg.com/@petamoriken/float16/-/float16-3.7.1.tgz#4a0cc0854a3a101cc2d697272f120e1a05975ce5" - integrity sha512-oXZOc+aePd0FnhTWk15pyqK+Do87n0TyLV1nxdEougE95X/WXWDqmQobfhgnSY7QsWn5euZUWuDVeTQvoQ5VNw== + version "3.8.0" + resolved "https://registry.yarnpkg.com/@petamoriken/float16/-/float16-3.8.0.tgz#3a48b7938e1a62188a61ec02d5b12630f671401f" + integrity sha512-AhVAm6SQ+zgxIiOzwVdUcDmKlu/qU39FiYD2UD6kQQaVenrn0dGZewIghWAENGQsvC+1avLCuT+T2/3Gsp/W3w== "@types/eslint-scope@^3.7.3": version "3.7.4" @@ -107,9 +107,9 @@ "@types/estree" "*" "@types/eslint@*": - version "8.21.1" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.21.1.tgz#110b441a210d53ab47795124dbc3e9bb993d1e7c" - integrity sha512-rc9K8ZpVjNcLs8Fp0dkozd5Pt2Apk1glO4Vgz8ix1u6yFByxfqo5Yavpy65o+93TAe24jr7v+eSBtFLvOQtCRQ== + version "8.37.0" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.37.0.tgz#29cebc6c2a3ac7fea7113207bf5a828fdf4d7ef1" + integrity sha512-Piet7dG2JBuDIfohBngQ3rCt7MgO9xCO4xIMKxBThCq5PNRB91IjlJ10eJVwfoNtvTErmxLzwBZ7rHZtbOMmFQ== dependencies: "@types/estree" "*" "@types/json-schema" "*" @@ -149,14 +149,14 @@ integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== "@types/node@*": - version "18.14.5" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.14.5.tgz#4a13a6445862159303fc38586598a9396fc408b3" - integrity sha512-CRT4tMK/DHYhw1fcCEBwME9CSaZNclxfzVMe7GsO6ULSwsttbj70wSiX6rZdIjGblu93sTJxLdhNIT85KKI7Qw== + version "18.15.11" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.11.tgz#b3b790f09cb1696cffcec605de025b088fa4225f" + integrity sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q== "@types/ol-ext@npm:@siedlerchr/types-ol-ext": - version "3.0.9" - resolved "https://registry.yarnpkg.com/@siedlerchr/types-ol-ext/-/types-ol-ext-3.0.9.tgz#7e45ab0e212a46f0b16622aa008ef0b37157f160" - integrity sha512-OwQovO8A+CtIqBr4s8r6hn9sN2uR3nB+z/MiLOYLS5YPPaJ1h0nyJXxmsofjcmtedGLkqwLcAl1YfAKxjG3bAg== + version "3.1.0" + resolved "https://registry.yarnpkg.com/@siedlerchr/types-ol-ext/-/types-ol-ext-3.1.0.tgz#4ddb002a0996a6fb34f4f351780ac05f70291438" + integrity sha512-QN7zJR/x/B0Q0dqYThp7QPhU0MDP6OzkZlGLYrCLLocIfHRs7/orWrCxfR1pAxDSRYghjDC/xMoiHNRpykn/rQ== dependencies: jspdf "^2.5.1" @@ -399,9 +399,9 @@ buffer-from@^1.0.0: integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== caniuse-lite@^1.0.30001449: - version "1.0.30001460" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001460.tgz#31d2e26f0a2309860ed3eff154e03890d9d851a7" - integrity sha512-Bud7abqjvEjipUkpLs4D7gR0l8hBYBHoa+tGtKJHvT2AYzLp1z7EmVkUT4ERpVUfca8S2HGIVs883D8pUH1ZzQ== + version "1.0.30001474" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001474.tgz#13b6fe301a831fe666cce8ca4ef89352334133d5" + integrity sha512-iaIZ8gVrWfemh5DG3T9/YqarVZoYf0r188IjaGwx68j4Pf0SGY6CQkmJUIE+NZHkkecQGohzXmBGEwWDr9aM3Q== canvg@^3.0.6: version "3.0.10" @@ -482,9 +482,9 @@ commander@^9.4.1: integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== core-js@^3.6.0, core-js@^3.8.3: - version "3.29.0" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.29.0.tgz#0273e142b67761058bcde5615c503c7406b572d6" - integrity sha512-VG23vuEisJNkGl6XQmFJd3rEG/so/CNatqeE+7uZAwTSwFeB/qaO0be8xZYUNWprJ/GIwL8aMt9cj1kvbpTZhg== + version "3.30.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.30.0.tgz#64ac6f83bc7a49fd42807327051701d4b1478dea" + integrity sha512-hQotSSARoNh1mYPi9O2YaWeiq/cEB95kOrFb4NCrO4RIFt1qqNpKsaE+vy/L3oiqvND5cThqXzUU3r9F7Efztg== cross-spawn@^7.0.3: version "7.0.3" @@ -537,9 +537,9 @@ earcut@^2.2.3: integrity sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ== electron-to-chromium@^1.4.284: - version "1.4.317" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.317.tgz#9a3d38a1a37f26a417d3d95dafe198ff11ed072b" - integrity sha512-JhCRm9v30FMNzQSsjl4kXaygU+qHBD0Yh7mKxyjmF0V8VwYVB6qpBRX28GyAucrM9wDCpSUctT6FpMUQxbyKuA== + version "1.4.353" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.353.tgz#20e9cb4c83a08e35b3314d3fa8988764c105e6b7" + integrity sha512-IdJVpMHJoBT/nn0GQ02wPfbhogDVpd1ud95lP//FTf5l35wzxKJwibB4HBdY7Q+xKPA1nkZ0UDLOMyRj5U5IAQ== enhanced-resolve@^5.0.0, enhanced-resolve@^5.10.0: version "5.12.0" @@ -670,9 +670,9 @@ glob-to-regexp@^0.4.1: integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== graceful-fs@^4.1.2, graceful-fs@^4.2.4, graceful-fs@^4.2.9: - version "4.2.10" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" - integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== has-flag@^4.0.0: version "4.0.0" @@ -705,9 +705,9 @@ ieee754@^1.1.12: integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== immutable@^4.0.0: - version "4.2.4" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.2.4.tgz#83260d50889526b4b531a5e293709a77f7c55a2a" - integrity sha512-WDxL3Hheb1JkRN3sQkyujNlL/xRjAo3rJtaU5xeufUauG66JdMr32bLj4gF+vWl84DIA3Zxw7tiAjneYzRRw+w== + version "4.3.0" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.0.tgz#eb1738f14ffb39fd068b1dbe1296117484dd34be" + integrity sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg== import-local@^3.0.2: version "3.1.0" @@ -814,7 +814,7 @@ kind-of@^6.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== -klona@^2.0.4: +klona@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.6.tgz#85bffbf819c03b2f53270412420a4555ef882e22" integrity sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA== @@ -898,15 +898,15 @@ normalize-path@^3.0.0, normalize-path@~3.0.0: resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -ol-ext@4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/ol-ext/-/ol-ext-4.0.6.tgz#5c9d8c7042f6b88dd21a725bbb2429162248badd" - integrity sha512-SG90ILjlHT302Zqgr3AR87M4yq4E96g2SJWlkliZDcF++QK4rMja1lId0w/cKMuGSPWSC79lD73eh1vcQlHdGw== +ol-ext@^4.0.7: + version "4.0.7" + resolved "https://registry.yarnpkg.com/ol-ext/-/ol-ext-4.0.7.tgz#de1cd75c8fa8278e5dae27b11478505d98d18476" + integrity sha512-7ctK7nxR/F27bh11Y+WEVuq8xdkLRTutVxQTov/jg89s9yyXgNFtswaJsREtckhYhbMFP5aXyVNvLkAcPOzguA== ol-mapbox-style@^10.1.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/ol-mapbox-style/-/ol-mapbox-style-10.1.0.tgz#fa2b6d0a9a1ba17b4305b05d38a426a8527e7e4a" - integrity sha512-ujMjK/p/QFGs9ZebkL+x3ceRnzcilHt1Y2/oeMNdvulwjl/zbIG9oHRiwdTW/zHVFpqauTtgpFJFtoqseBMunA== + version "10.4.0" + resolved "https://registry.yarnpkg.com/ol-mapbox-style/-/ol-mapbox-style-10.4.0.tgz#3f4336c016c96a9b93e75e4fe5d689fdc9250fc4" + integrity sha512-p6OOvUPtBmMcjct6bh9pbWWx/jT/8WACN+25QArUrH4HizjViHTN3C5GUqDOSqITTqtjyQWnvxcXYpgBqkK1Hg== dependencies: "@mapbox/mapbox-gl-style-spec" "^13.23.1" mapbox-to-css-font "^2.4.1" @@ -920,9 +920,9 @@ ol-mapbox-style@^9.2.0: mapbox-to-css-font "^2.4.1" ol@^7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/ol/-/ol-7.2.2.tgz#d675a1525fd995a29a70a9a9fa9c3a9bc827aa39" - integrity sha512-eqJ1hhVQQ3Ap4OhYq9DRu5pz9RMpLhmoTauDoIqpn7logVi1AJE+lXjEHrPrTSuZYjtFbMgqr07sxoLNR65nrw== + version "7.3.0" + resolved "https://registry.yarnpkg.com/ol/-/ol-7.3.0.tgz#7ffb5f258dafa4a3e218208aad9054d61f6fe786" + integrity sha512-08vJE4xITKPazQ9qJjeqYjRngnM9s+1eSv219Pdlrjj3LpLqjEH386ncq+76Dw1oGPGR8eLVEePk7FEd9XqqMw== dependencies: earcut "^2.2.3" geotiff "^2.0.7" @@ -1158,17 +1158,17 @@ safe-buffer@^5.1.0: integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== sass-loader@^13.2.0: - version "13.2.0" - resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-13.2.0.tgz#80195050f58c9aac63b792fa52acb6f5e0f6bdc3" - integrity sha512-JWEp48djQA4nbZxmgC02/Wh0eroSUutulROUusYJO9P9zltRbNN80JCBHqRGzjd4cmZCa/r88xgfkjGD0TXsHg== + version "13.2.2" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-13.2.2.tgz#f97e803993b24012c10d7ba9676548bf7a6b18b9" + integrity sha512-nrIdVAAte3B9icfBiGWvmMhT/D+eCDwnk+yA7VE/76dp/WkHX+i44Q/pfo71NYbwj0Ap+PGsn0ekOuU1WFJ2AA== dependencies: - klona "^2.0.4" + klona "^2.0.6" neo-async "^2.6.2" sass@^1.58.0: - version "1.58.3" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.58.3.tgz#2348cc052061ba4f00243a208b09c40e031f270d" - integrity sha512-Q7RaEtYf6BflYrQ+buPudKR26/lH+10EmO9bBqbmPh/KeLqv8bjpTNqxe71ocONqXq+jYiCbpPUmQMS+JJPk4A== + version "1.60.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.60.0.tgz#657f0c23a302ac494b09a5ba8497b739fb5b5a81" + integrity sha512-updbwW6fNb5gGm8qMXzVO7V4sWf7LMXnMly/JEyfbfERbVH46Fn6q02BX7/eHTdKpE7d+oTkMMQpFWNUMfFbgQ== dependencies: chokidar ">=3.0.0 <4.0.0" immutable "^4.0.0" @@ -1190,7 +1190,7 @@ semver@^7.3.4, semver@^7.3.8: dependencies: lru-cache "^6.0.0" -serialize-javascript@^6.0.0: +serialize-javascript@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz#b206efb27c3da0b0ab6b52f48d170b7996458e5c" integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w== @@ -1258,9 +1258,9 @@ stackblur-canvas@^2.0.0: integrity sha512-EeNzTVfj+1In7aSLPKDD03F/ly4RxEuF/EX0YcOG0cKoPXs+SLZxDawQbexQDBzwROs4VKLWTOaZQlZkGBFEIQ== style-loader@^3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.1.tgz#057dfa6b3d4d7c7064462830f9113ed417d38575" - integrity sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ== + version "3.3.2" + resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.2.tgz#eaebca714d9e462c19aa1e3599057bc363924899" + integrity sha512-RHs/vcrKdQK8wZliteNK4NKzxvLBzpuHMqYmUVWeKa6MkaIQ97ZTOS0b+zapZhy6GcrgWnvWYCMHRirC3FsUmw== supports-color@^7.1.0: version "7.2.0" @@ -1292,20 +1292,20 @@ tapable@^2.1.1, tapable@^2.2.0: integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== terser-webpack-plugin@^5.1.3: - version "5.3.6" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz#5590aec31aa3c6f771ce1b1acca60639eab3195c" - integrity sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ== + version "5.3.7" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.7.tgz#ef760632d24991760f339fe9290deb936ad1ffc7" + integrity sha512-AfKwIktyP7Cu50xNjXF/6Qb5lBNzYaWpU6YfoX3uZicTx0zTy0stDDCsvjDapKsSDvOeWo5MEq4TmdBy2cNoHw== dependencies: - "@jridgewell/trace-mapping" "^0.3.14" + "@jridgewell/trace-mapping" "^0.3.17" jest-worker "^27.4.5" schema-utils "^3.1.1" - serialize-javascript "^6.0.0" - terser "^5.14.1" + serialize-javascript "^6.0.1" + terser "^5.16.5" -terser@^5.14.1: - version "5.16.5" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.16.5.tgz#1c285ca0655f467f92af1bbab46ab72d1cb08e5a" - integrity sha512-qcwfg4+RZa3YvlFh0qjifnzBHjKGNbtDo9yivMqMFDy9Q6FSaQWSB/j1xKhsoUFJIqDOM3TsN6D5xbrMrFcHbg== +terser@^5.16.5: + version "5.16.8" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.16.8.tgz#ccde583dabe71df3f4ed02b65eb6532e0fae15d5" + integrity sha512-QI5g1E/ef7d+PsDifb+a6nnVgC4F22Bg6T0xrBrz6iloVB4PUkkunp6V8nzoOOZJIzjWVdAGqCdlKlhLq/TbIA== dependencies: "@jridgewell/source-map" "^0.3.2" acorn "^8.5.0" @@ -1336,10 +1336,10 @@ ts-loader@^9.4.2: micromatch "^4.0.0" semver "^7.3.4" -typescript@^4.9.5: - version "4.9.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" - integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== +typescript@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.3.tgz#fe976f0c826a88d0a382007681cbb2da44afdedf" + integrity sha512-xv8mOEDnigb/tN9PSMTwSEqAnUvkoXMQlicOb0IUVDBSQCgBSaAAROUZYy2IcUy5qU6XajK5jjjO7TMWqBTKZA== update-browserslist-db@^1.0.10: version "1.0.10" @@ -1414,9 +1414,9 @@ webpack-sources@^3.2.3: integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== webpack@^5.76.0: - version "5.76.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.76.0.tgz#f9fb9fb8c4a7dbdcd0d56a98e56b8a942ee2692c" - integrity sha512-l5sOdYBDunyf72HW8dF23rFtWq/7Zgvt/9ftMof71E/yUb1YLOBmTgA2K4vQthB3kotMrSj609txVE0dnr2fjA== + version "5.78.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.78.0.tgz#836452a12416af2a7beae906b31644cb2562f9e6" + integrity sha512-gT5DP72KInmE/3azEaQrISjTvLYlSM0j1Ezhht/KLVkrqtv10JoP/RXhwmX/frrutOPuSq3o5Vq0ehR/4Vmd1g== dependencies: "@types/eslint-scope" "^3.7.3" "@types/estree" "^0.0.51" From d29e38de60173f0b009f33338a1ba9590f7eb5f9 Mon Sep 17 00:00:00 2001 From: Daniel Kastl Date: Thu, 6 Apr 2023 11:14:50 +0900 Subject: [PATCH 012/109] Better readablity and comments Signed-off-by: Daniel Kastl --- Gemfile | 16 +++++++--- tsconfig.json | 4 +-- webpack.config.js | 80 ++++++++++++++++++++++++++--------------------- 3 files changed, 58 insertions(+), 42 deletions(-) diff --git a/Gemfile b/Gemfile index 37706324..b3c730ca 100644 --- a/Gemfile +++ b/Gemfile @@ -1,10 +1,18 @@ source 'https://rubygems.org' +# Define gem versions with environment variables or default versions +gem_versions = { + pg: ENV['GEM_PG_VERSION'] || '1.2.2', + rgeo: ENV['GEM_RGEO_VERSION'] || '2.4.0', + rgeo_activerecord: ENV['GEM_RGEO_ACTIVERECORD_VERSION'] || '7.0.1', + activerecord_postgis_adapter: ENV['GEM_ACTIVERECORD_POSTGIS_ADAPTER_VERSION'] || '7.1.1' +} + gem 'deface' gem 'immutable-struct' -gem "rgeo", "~> 2.4.0" +gem "rgeo", "~> #{gem_versions[:rgeo]}" gem "rgeo-geojson" -gem "pg", (ENV['GEM_PG_VERSION'] ? "~> #{ENV['GEM_PG_VERSION']}" : "~> 1.2.2") # make sure we use a version compatible with AR -gem "rgeo-activerecord", (ENV['GEM_RGEO_ACTIVERECORD_VERSION'] ? "~> #{ENV['GEM_RGEO_ACTIVERECORD_VERSION']}" : "~> 7.0.1") # same as above -gem 'activerecord-postgis-adapter', (ENV['GEM_ACTIVERECORD_POSTGIS_ADAPTER_VERSION'] ? "~> #{ENV['GEM_ACTIVERECORD_POSTGIS_ADAPTER_VERSION']}" : "~> 7.1.1") # same as above +gem "pg", "~> #{gem_versions[:pg]}" +gem "rgeo-activerecord", "~> #{gem_versions[:rgeo_activerecord]}" +gem 'activerecord-postgis-adapter', "~> #{gem_versions[:activerecord_postgis_adapter]}" gem 'rails-controller-testing' # This gem brings back assigns to your controller tests as well as assert_template to both controller and integration tests. diff --git a/tsconfig.json b/tsconfig.json index 8361195a..c78fe4fe 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,13 +1,13 @@ { "compilerOptions": { - "outDir": "./dist", "target": "es5", "module": "es6", "moduleResolution": "node", "jsx": "react", + "baseUrl": "./src/", + "outDir": "./dist", "allowJs": true, "noImplicitAny": true, - "baseUrl": "./src/", "paths": { "*": ["@types/*"] } diff --git a/webpack.config.js b/webpack.config.js index 2bd336bd..51476d11 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,45 +1,53 @@ -const path = require('path') +const path = require('path'); + +// Define loaders +// Loaders for processing Sass files +const sassLoaders = [ + 'style-loader', // Creates `style` nodes from JS strings + 'css-loader', // Translates CSS into CommonJS + 'sass-loader', // Compiles Sass to CSS +]; + +// Loaders for processing CSS files +const cssLoaders = ['style-loader', 'css-loader']; + +// Loaders for processing image files +const imageLoaders = { + test: /\.(png|svg|jpg|jpeg|gif)$/i, + type: 'asset/resource', +}; + +// Loaders for processing font files +const fontLoaders = { + test: /\.(woff|woff2|eot|ttf|otf)$/i, + type: 'asset/resource', +}; + +// Loaders for processing TypeScript files +const tsLoaders = { + test: /\.ts$/i, + use: 'ts-loader', +}; module.exports = { - mode: 'production', - entry: path.resolve(__dirname, 'src', 'index.ts'), + mode: 'production', // Set build mode to production + entry: path.join(__dirname, 'src', 'index.ts'), // Specify entry point module: { rules: [ - { - test: /\.s[ac]ss$/i, - use: [ - // Creates `style` nodes from JS strings - "style-loader", - // Translates CSS into CommonJS - "css-loader", - // Compiles Sass to CSS - "sass-loader", - ], - }, - { - test: /\.css$/i, - use: ['style-loader', 'css-loader'], - }, - { - test: /\.(png|svg|jpg|jpeg|gif)$/i, - type: 'asset/resource', - }, - { - test: /\.(woff|woff2|eot|ttf|otf)$/i, - type: 'asset/resource', - }, - { - test: /\.ts$/i, - use: 'ts-loader', - }, + // Apply loaders + { test: /\.s[ac]ss$/i, use: sassLoaders }, + { test: /\.css$/i, use: cssLoaders }, + imageLoaders, + fontLoaders, + tsLoaders, ], }, - devtool: false, + devtool: false, // Disable source maps resolve: { - extensions: ['.ts', '.js'] + extensions: ['.ts', '.js'], // Specify file extensions to resolve }, output: { - filename: 'main.js', - path: path.resolve(__dirname, 'assets', 'javascripts') - } -} + filename: 'main.js', // Set output filename + path: path.join(__dirname, 'assets', 'javascripts'), // Set output directory + }, +}; From 32a1d6dcf2e8e7878b049d991d814293be3d8387 Mon Sep 17 00:00:00 2001 From: Daniel Kastl Date: Thu, 6 Apr 2023 11:16:44 +0900 Subject: [PATCH 013/109] Refactors settings Signed-off-by: Daniel Kastl --- src/components/gtt-setting.ts | 134 +++++++++++++++------------------- 1 file changed, 57 insertions(+), 77 deletions(-) diff --git a/src/components/gtt-setting.ts b/src/components/gtt-setting.ts index b02e9bd3..1dd92e8f 100644 --- a/src/components/gtt-setting.ts +++ b/src/components/gtt-setting.ts @@ -1,95 +1,75 @@ -import 'ol/ol.css' -import 'ol-ext/dist/ol-ext.min.css' -import FontSymbol from 'ol-ext/style/FontSymbol' - -export const gtt_setting = ():void => { +import 'ol/ol.css'; +import 'ol-ext/dist/ol-ext.min.css'; +import FontSymbol from 'ol-ext/style/FontSymbol'; +export const gtt_setting = (): void => { // Override jQuery UI select menu - $.widget("ui.selectmenu", $.ui.selectmenu, { - _renderItem: function( ul: any, item: any ) { - const li = $('
  • ') - const wrapper = $('
    ', { - text: '' - }) - const style = item.optgroup.toLowerCase().split(' ').join('-') - switch (style) { - case 'material-icons': - $('', { - class: 'ui-icons ' + style, - title: item.label, - text: item.value - }).prependTo(wrapper) - break; + $.widget('ui.selectmenu', $.ui.selectmenu, { + _renderItem: function (ul: JQuery, item: any) { + const li = $('
  • '); + const wrapper = $('
    ', { text: '' }); + const style = item.optgroup.toLowerCase().split(' ').join('-'); - default: - $('', { - class: 'ui-icons ' + style + ' icon-' + item.value, - title: item.label, - text: '' - }).prependTo(wrapper) - break; - } - return li.append(wrapper).appendTo(ul) - } + const iconConfig = { + class: `ui-icons ${style}${style === 'material-icons' ? '' : ' icon-' + item.value}`, + title: item.label, + text: style === 'material-icons' ? item.value : '', + }; + + $('', iconConfig).prependTo(wrapper); + + return li.append(wrapper).appendTo(ul); + }, }); - const glyph = FontSymbol.defs.glyphs - document.querySelectorAll("[id^='settings_tracker_']").forEach((element: HTMLSelectElement) => { - const selectedValue = element.value - if (element.length === 1 && selectedValue !== "") { - element.remove(0) - // element.append(new Option("", "", false, false)) + const glyph = FontSymbol.defs.glyphs; + const trackerElements = document.querySelectorAll("[id^='settings_tracker_']"); + + const processElement = (element: HTMLSelectElement) => { + const selectedValue = element.value; + + if (element.length === 1 && selectedValue !== '') { + element.remove(0); } - for (let font in FontSymbol.defs.fonts) { - const optgroup = document.createElement('optgroup') - optgroup.label = FontSymbol.defs.fonts[font].name - for (let i in glyph) { - if (glyph[i].font == font) { - const selected = selectedValue === i - const words = i.split('_') - const text = words.map((word) => { - return word[0].toUpperCase() + word.substring(1) - }).join(' ') - optgroup.appendChild(new Option(text, i, selected, selected)) - if (selected) { - const style = font.toLowerCase().split(' ').join('-') - switch (style) { - case 'material-icons': - element.nextElementSibling.className = style - element.nextElementSibling.textContent = i - break; - default: - element.nextElementSibling.className = style + ' icon-' + i - element.nextElementSibling.textContent = '' - break; - } + for (const font in FontSymbol.defs.fonts) { + const optgroup = document.createElement('optgroup'); + optgroup.label = FontSymbol.defs.fonts[font].name; + + for (const i in glyph) { + if (glyph[i].font === font) { + const selected = selectedValue === i; + const words = i.split('_'); + const text = words.map((word) => word[0].toUpperCase() + word.substring(1)).join(' '); + + optgroup.appendChild(new Option(text, i, selected, selected)); + + if (selected) { + const style = font.toLowerCase().split(' ').join('-'); + const icon = element.nextElementSibling; + icon.className = `${style}${style === 'material-icons' ? '' : ' icon-' + i}`; + icon.textContent = style === 'material-icons' ? i : ''; } } } - element.append(optgroup) + + element.append(optgroup); } // Apply better Selector styling with jQuery UI (available in Redmine) $(element) .selectmenu({ - change: function(event: any, data: any) { - const style = data.item.optgroup.toLowerCase().split(' ').join('-') - switch (style) { - case 'material-icons': - document.querySelector(`#icon_${element.id}`).className = style - document.querySelector(`#icon_${element.id}`).textContent = data.item.value - break; - - default: - document.querySelector(`#icon_${element.id}`).className = style + ' icon-' + data.item.value - document.querySelector(`#icon_${element.id}`).textContent = '' - break; - } - } + change: function (event: any, data: any) { + const style = data.item.optgroup.toLowerCase().split(' ').join('-'); + const icon = document.querySelector(`#icon_${element.id}`); + icon.className = `${style}${style === 'material-icons' ? '' : ' icon-' + data.item.value}`; + icon.textContent = style === 'material-icons' ? data.item.value : ''; + }, }) .selectmenu('menuWidget') .addClass('select-overflow') - .addClass('ui-menu-icons customicons') - }) -} + .addClass('ui-menu-icons customicons'); + }; + + trackerElements.forEach(processElement); +}; From c22838197e03a03e01e773732989f2b945ea4df9 Mon Sep 17 00:00:00 2001 From: Daniel Kastl Date: Thu, 6 Apr 2023 13:21:42 +0900 Subject: [PATCH 014/109] Restructure basics Signed-off-by: Daniel Kastl --- .../quick_hack.ts => @types/constants.ts} | 5 +- src/@types/types.ts | 38 +++ src/@types/window.d.ts | 34 ++- src/components/gtt-client.ts | 277 +++++++----------- src/index.ts | 33 +-- 5 files changed, 187 insertions(+), 200 deletions(-) rename src/{components/quick_hack.ts => @types/constants.ts} (75%) create mode 100644 src/@types/types.ts diff --git a/src/components/quick_hack.ts b/src/@types/constants.ts similarity index 75% rename from src/components/quick_hack.ts rename to src/@types/constants.ts index 931c9bca..ad986c23 100644 --- a/src/components/quick_hack.ts +++ b/src/@types/constants.ts @@ -1,8 +1,9 @@ +// constants.ts export const quick_hack = { lon: 139.691706, lat: 35.689524, zoom: 13, maxzoom: 19, fitMaxzoom: 17, - geocoder: {} -} + geocoder: {}, +}; diff --git a/src/@types/types.ts b/src/@types/types.ts new file mode 100644 index 00000000..30f4b907 --- /dev/null +++ b/src/@types/types.ts @@ -0,0 +1,38 @@ +// types.ts +import { Tile, Image, VectorTile as VTLayer } from 'ol/layer'; +import { OSM, XYZ, TileWMS, ImageWMS, VectorTile as VTSource } from 'ol/source'; + +export interface GttClientOption { + target: HTMLDivElement | null; +} + +export interface LayerObject { + type: string; + id: number; + name: string; + baselayer: boolean; + options: object; +} + +export interface FilterOption { + location: boolean; + distance: boolean; +} + +export interface TileLayerSource { + layer: typeof Tile; + source: typeof OSM | typeof XYZ | typeof TileWMS; + type: string; +} + +export interface ImageLayerSource { + layer: typeof Image; + source: typeof ImageWMS; + type: string; +} + +export interface VTLayerSource { + layer: typeof VTLayer; + source: typeof VTSource; + type: string; +} diff --git a/src/@types/window.d.ts b/src/@types/window.d.ts index 7af2aa74..75ea3623 100644 --- a/src/@types/window.d.ts +++ b/src/@types/window.d.ts @@ -1,12 +1,24 @@ -// Redmine function -interface Window { - availableFilters: any - operatorByType: any - operatorLabels: any - toggleOperator(filed: any): void - showModal(id: string, width: string, title?: string): void - buildFilterRowWithoutDistanceFilter(field: any, operator: any, values: any): void - buildFilterRow(field: any, operator: any, values: any): void - replaceIssueFormWith(html: any): void - replaceIssueFormWithInitMap(html: any): void +declare global { + interface Window { + // Redmine functions + availableFilters: any; + operatorByType: any; + operatorLabels: any; + toggleOperator(field: any): void; + showModal(id: string, width: string, title?: string): void; + buildFilterRowWithoutDistanceFilter( + field: any, + operator: any, + values: any + ): void; + buildFilterRow(field: any, operator: any, values: any): void; + replaceIssueFormWith(html: any): void; + replaceIssueFormWithInitMap(html: any): void; + + // Gtt functions + createGttClient(target: HTMLDivElement): void; + gtt_setting(): void; + } } + +export {}; // This ensures this file is treated as a module diff --git a/src/components/gtt-client.ts b/src/components/gtt-client.ts index 2a661222..21f69c1a 100644 --- a/src/components/gtt-client.ts +++ b/src/components/gtt-client.ts @@ -1,83 +1,65 @@ -import 'ol/ol.css' -import 'ol-ext/dist/ol-ext.min.css' -import { Map, Feature, View, Geolocation } from 'ol' -import 'ol-ext/filter/Base' -import { Geometry, Point } from 'ol/geom' -import { GeoJSON, WKT, MVT } from 'ol/format' -import { Layer, Tile, Image, VectorTile as VTLayer } from 'ol/layer' -import VectorLayer from 'ol/layer/Vector' -import { OSM, XYZ, TileWMS, ImageWMS, VectorTile as VTSource } from 'ol/source' -import { Style, Fill, Stroke, Circle } from 'ol/style' -import { OrderFunction } from 'ol/render' +// OpenLayers core imports +import 'ol/ol.css'; +import { Map, Feature, View, Geolocation } from 'ol'; +import { Geometry, Point } from 'ol/geom'; +import { GeoJSON, WKT, MVT } from 'ol/format'; +import { Layer, Tile, Image, VectorTile as VTLayer } from 'ol/layer'; +import VectorLayer from 'ol/layer/Vector'; +import { OSM, XYZ, TileWMS, ImageWMS, VectorTile as VTSource } from 'ol/source'; +import { Style, Fill, Stroke, Circle } from 'ol/style'; +import { OrderFunction } from 'ol/render'; import { defaults as interactions_defaults, MouseWheelZoom, Modify, Draw, Select, -} from 'ol/interaction' -import { focus as events_condifition_focus } from 'ol/events/condition' -import { defaults as control_defaults, FullScreen, Rotate } from 'ol/control' -import { transform, fromLonLat, transformExtent } from 'ol/proj' -import { createEmpty, extend, getCenter, containsCoordinate } from 'ol/extent' -import { FeatureCollection } from 'geojson' -import { quick_hack } from './quick_hack' -import Vector from 'ol/source/Vector' -import Ordering from 'ol-ext/render/Ordering' -import Shadow from 'ol-ext/style/Shadow' -import FontSymbol from 'ol-ext/style/FontSymbol' -import Mask from 'ol-ext/filter/Mask' -import Bar from 'ol-ext/control/Bar' -import Toggle from 'ol-ext/control/Toggle' -import Button from 'ol-ext/control/Button' -import TextButton from 'ol-ext/control/TextButton' -import LayerPopup from 'ol-ext/control/LayerPopup' -import LayerSwitcher from 'ol-ext/control/LayerSwitcher' -import Popup from 'ol-ext/overlay/Popup' -import { position } from 'ol-ext/control/control' -import { ResizeObserver } from '@juggle/resize-observer' -import VectorSource from 'ol/source/Vector' -import { FeatureLike } from 'ol/Feature' -import TileSource from 'ol/source/Tile' -import ImageSource from 'ol/source/Image' -import { Options as ImageWMSOptions } from 'ol/source/ImageWMS' -import { Options as VectorTileOptions } from 'ol/source/VectorTile' +} from 'ol/interaction'; +import { focus as events_condifition_focus } from 'ol/events/condition'; +import { defaults as control_defaults, FullScreen, Rotate } from 'ol/control'; +import { transform, fromLonLat, transformExtent } from 'ol/proj'; +import { createEmpty, extend, getCenter, containsCoordinate } from 'ol/extent'; +import Vector from 'ol/source/Vector'; +import VectorSource from 'ol/source/Vector'; +import { FeatureLike } from 'ol/Feature'; +import TileSource from 'ol/source/Tile'; +import ImageSource from 'ol/source/Image'; +import { Options as ImageWMSOptions } from 'ol/source/ImageWMS'; +import { Options as VectorTileOptions } from 'ol/source/VectorTile'; import { applyStyle } from 'ol-mapbox-style'; -interface GttClientOption { - target: HTMLDivElement | null -} - -interface LayerObject { - type: string - id: number - name: string - baselayer: boolean - options: object -} - -interface FilterOption { - location: boolean - distance: boolean -} - -interface TileLayerSource { - layer: typeof Tile - source: typeof OSM | typeof XYZ | typeof TileWMS - type: string -} - -interface ImageLayerSource { - layer: typeof Image - source: typeof ImageWMS - type: string -} +// OpenLayers extension imports +import 'ol-ext/dist/ol-ext.min.css'; +import 'ol-ext/filter/Base'; +import Ordering from 'ol-ext/render/Ordering'; +import Shadow from 'ol-ext/style/Shadow'; +import FontSymbol from 'ol-ext/style/FontSymbol'; +import Mask from 'ol-ext/filter/Mask'; +import Bar from 'ol-ext/control/Bar'; +import Toggle from 'ol-ext/control/Toggle'; +import Button from 'ol-ext/control/Button'; +import TextButton from 'ol-ext/control/TextButton'; +import LayerPopup from 'ol-ext/control/LayerPopup'; +import LayerSwitcher from 'ol-ext/control/LayerSwitcher'; +import Popup from 'ol-ext/overlay/Popup'; +import { position } from 'ol-ext/control/control'; + +// Other imports +import { FeatureCollection } from 'geojson'; +import { ResizeObserver } from '@juggle/resize-observer'; + +// Import types +import { + GttClientOption, + LayerObject, + FilterOption, + TileLayerSource, + ImageLayerSource, + VTLayerSource, +} from '../@types/types'; -interface VTLayerSource { - layer: typeof VTLayer - source: typeof VTSource - type: string -} +// Import constants +import { quick_hack } from '../@types/constants'; export class GttClient { readonly map: Map @@ -96,73 +78,58 @@ export class GttClient { this.filters = { location: false, distance: false - } - this.maps = [] - this.geolocations = [] + }; + this.maps = []; + this.geolocations = []; // needs target if (!options.target) { - return + return; } - const gtt_defaults = document.querySelector('#gtt-defaults') as HTMLDivElement + const gtt_defaults = document.querySelector('#gtt-defaults') as HTMLDivElement; if (!gtt_defaults) { - return + return; } - this.defaults = gtt_defaults.dataset + this.defaults = gtt_defaults.dataset; - if (this.defaults.lon === null || this.defaults.lon === undefined) { - this.defaults.lon = quick_hack.lon.toString() - } - if (this.defaults.lat === null || this.defaults.lat === undefined) { - this.defaults.lat = quick_hack.lat.toString() - } - if (this.defaults.zoom === null || this.defaults.zoom === undefined) { - this.defaults.zoom = quick_hack.zoom.toString() - } - if (this.defaults.maxzoom === null || this.defaults.maxzoom === undefined) { - this.defaults.maxzoom = quick_hack.maxzoom.toString() - } - if (this.defaults.fitMaxzoom === null || this.defaults.fitMaxzoom === undefined) { - this.defaults.fitMaxzoom = quick_hack.fitMaxzoom.toString() - } - if (this.defaults.geocoder === null || this.defaults.geocoder === undefined) { - this.defaults.geocoder = JSON.stringify(quick_hack.geocoder) + const keysToCheck = ['lon', 'lat', 'zoom', 'maxzoom', 'fitMaxzoom', 'geocoder']; + + for (const key of keysToCheck) { + if (this.defaults[key] == null) { + this.defaults[key] = quick_hack[key as keyof typeof quick_hack]?.toString() ?? ''; + } } - this.contents = options.target.dataset - this.i18n = JSON.parse(this.defaults.i18n) + this.contents = options.target.dataset; + this.i18n = JSON.parse(this.defaults.i18n); // create map at first + const { zoomInTipLabel, zoomOutTipLabel } = this.i18n.control; this.map = new Map({ target: options.target, - //layers: this.layerArray, interactions: interactions_defaults({mouseWheelZoom: false}).extend([ new MouseWheelZoom({ - constrainResolution: true, // force zooming to a integer zoom - condition: events_condifition_focus // only wheel/trackpad zoom when the map has the focus + constrainResolution: true, + condition: events_condifition_focus }) ]), controls: control_defaults({ rotateOptions: {}, - attributionOptions: { - collapsible: false - }, - zoomOptions: { - zoomInTipLabel: this.i18n.control.zoom_in, - zoomOutTipLabel: this.i18n.control.zoom_out - } + attributionOptions: { collapsible: false }, + zoomOptions: { zoomInTipLabel, zoomOutTipLabel } }) - }) + }); - let features: Feature[] | null = null - if (this.contents.geom && this.contents.geom !== null && this.contents.geom !== 'null') { - features = new GeoJSON().readFeatures( - JSON.parse(this.contents.geom), { - featureProjection: 'EPSG:3857' - } - ) + // Use optional chaining to simplify null checks + // Use optional chaining to simplify null checks + const geom = this.contents?.geom; + let features: Feature[] = []; // Set a default value for features array + if (geom) { + // Destructure object and rename variable for better readability + const { featureProjection: proj } = { featureProjection: 'EPSG:3857' }; + features = new GeoJSON().readFeatures(JSON.parse(geom), { featureProjection: proj }); } // Fix FireFox unloaded font issue @@ -1401,42 +1368,39 @@ const getLayerSource = (source: string, class_name: string): TileLayerSource | I return undefined } -const getCookie = (cname:string):string => { - var name = cname + '=' - var ca = document.cookie.split(';') - for(var i = 0; i < ca.length; i++) { - var c = ca[i] - while (c.charAt(0) == ' ') { - c = c.substring(1) +const getCookie = (cname: string): string => { + const name = cname + '='; + const decodedCookie = decodeURIComponent(document.cookie); + const ca = decodedCookie.split(';'); + + for(let i = 0; i { - let degrees = radians * (180 / Math.PI) - degrees = (degrees % 360 + 360) % 360 - return degrees +const radiansToDegrees = (radians: number): number => { + const degrees = radians * (180 / Math.PI); + return (degrees + 360) % 360; } -const degreesToRadians = (degrees: number) => { - return degrees * (Math.PI / 180) -} +const degreesToRadians = (degrees: number): number => degrees * (Math.PI / 180); + +const getMapSize = (map: Map): number[] => { + const [width, height] = map.getSize(); -const getMapSize = (map: Map) => { - let size = map.getSize() - if (size.length === 2 && size[0] <= 0 && size[1] <= 0) { - const target = map.getTarget() as HTMLElement - const target_obj = document.querySelector(`div#${target.id}`) - size = [ - target_obj.clientWidth, - target_obj.clientHeight - ] + if (width <= 0 || height <= 0) { + const target = map.getTarget() as HTMLElement; + return [target.clientWidth, target.clientHeight]; } - return size + + return [width, height]; } const evaluateComparison = (left: any, operator: any, right: any): any => { @@ -1448,32 +1412,9 @@ const evaluateComparison = (left: any, operator: any, right: any): any => { } } -const getObjectPathValue = (obj: any, path: any, def: any = null) => { - const stringToPath = function (path: any) { - if (typeof path !== 'string') { - return path - } - var output: Array = [] - path.split('.').forEach(item => { - item.split(/\[([^}]+)\]/g).forEach(key => { - if (key.length > 0) { - output.push(key) - } - }) - }) - return output - } - - path = stringToPath(path) - let current = obj - for (var i = 0; i < path.length; i++) { - if (!current[path[i]]) { - return def - } - current = current[path[i]] - } - - return current +const getObjectPathValue = (obj: any, path: string | Array, def: any = null) => { + const pathArr = Array.isArray(path) ? path : path.split(".").flatMap((key) => key.split(/\[([^}]+)\]/g).filter(Boolean)); + return pathArr.reduce((acc, key) => acc?.[key], obj) ?? def; } /** diff --git a/src/index.ts b/src/index.ts index 92050e76..881cae5a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,27 +1,22 @@ -import 'ol/ol.css' -import 'ol-ext/dist/ol-ext.min.css' -import './stylesheets/app.scss' +import 'ol/ol.css'; +import 'ol-ext/dist/ol-ext.min.css'; +import './stylesheets/app.scss'; // Custom Icons -import './stylesheets/custom-icons.css' -import './stylesheets/CustomIconsDef.js' +import './stylesheets/custom-icons.css'; +import './stylesheets/CustomIconsDef.js'; // Material Design Icons // https://github.com/marella/material-design-icons/tree/main/font#readme -import '@material-design-icons/font/filled.css' -import './stylesheets/MaterialDesignDef.js' +import '@material-design-icons/font/filled.css'; +import './stylesheets/MaterialDesignDef.js'; -import { GttClient } from './components/gtt-client' -import { gtt_setting } from './components/gtt-setting' +import { GttClient } from './components/gtt-client'; +import { gtt_setting } from './components/gtt-setting'; -interface Window { - createGttClient(target: HTMLDivElement): void - gtt_setting(): void -} -declare var window: Window -window.createGttClient = (target: HTMLDivElement):void => { - new GttClient({target: target}) -} +window.createGttClient = (target: HTMLDivElement): void => { + new GttClient({ target: target }); +}; window.gtt_setting = (): void => { - gtt_setting() -} + gtt_setting(); +}; From b7455a040cb6dbc3fa9a9415efeaabcc6dd6e684 Mon Sep 17 00:00:00 2001 From: Daniel Kastl Date: Thu, 6 Apr 2023 13:25:49 +0900 Subject: [PATCH 015/109] Improves history parser Signed-off-by: Daniel Kastl --- src/components/gtt-client.ts | 55 +++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/src/components/gtt-client.ts b/src/components/gtt-client.ts index 21f69c1a..ba8851c9 100644 --- a/src/components/gtt-client.ts +++ b/src/components/gtt-client.ts @@ -957,31 +957,40 @@ export class GttClient { * Parse page for WKT strings in history */ parseHistory() { - document.querySelectorAll('div#history ul.details i').forEach((item: Element) => { - const regex = new RegExp(/\b(?:POINT|LINESTRING|POLYGON)\b\s?(\({1,}[-]?\d+([,. ]\s?[-]?\d+)*\){1,})/gi) - const match = item.innerHTML.match(regex) + const historyItems = document.querySelectorAll('div#history ul.details i'); + + const regex = /\b(?:POINT|LINESTRING|POLYGON)\b\s?\({1,}[-]?\d+([,. ]\s?[-]?\d+)*\){1,}/gi; + const dataProjection = 'EPSG:4326'; + const featureProjection = 'EPSG:3857'; + + const parseAndFormatWKT = (wkt: string) => { + const feature = new WKT().readFeature(wkt, { dataProjection, featureProjection }); + let formattedWKT = new WKT().writeFeature(feature, { dataProjection, featureProjection, decimals: 5 }); + + if (formattedWKT.length > 30) { + const parts = formattedWKT.split(' '); + formattedWKT = `${parts[0]}...${parts[parts.length - 1]}`; + } + + return formattedWKT; + }; + + historyItems.forEach((item: Element) => { + const match = item.innerHTML.match(regex); + if (match !== null) { - const feature = new WKT().readFeature( - match.join(''), { - dataProjection: 'EPSG:4326', - featureProjection: 'EPSG:3857' - } - ) - let wkt = new WKT().writeFeature( - feature, { - dataProjection: 'EPSG:4326', - featureProjection: 'EPSG:3857', - decimals: 5 - } - ) - // Shorten long WKT's - if (wkt.length > 30) { - const parts = wkt.split(' ') - wkt = parts[0] + '...' + parts[parts.length - 1] - } - item.innerHTML = `${wkt}` + const wkt = parseAndFormatWKT(match.join('')); + + const link = document.createElement('a'); + link.href = '#'; + link.classList.add('wkt'); + link.dataset.feature = match.join(''); + link.textContent = wkt; + + // Replace current node with new link. + item.replaceWith(link); } - }) + }); } /** From 02e8368f0346a49279a04c4b133ae185e8c9d051 Mon Sep 17 00:00:00 2001 From: Daniel Kastl Date: Fri, 7 Apr 2023 23:15:26 +0900 Subject: [PATCH 016/109] Refactor - 1st iteration Signed-off-by: Daniel Kastl --- src/components/gtt-client.ts | 1525 ----------------- .../gtt-client}/constants.ts | 2 +- src/components/gtt-client/geocoding.ts | 250 +++ src/components/gtt-client/gtt-client-class.ts | 465 +++++ src/components/gtt-client/helpers.ts | 262 +++ src/components/gtt-client/index.ts | 4 + .../gtt-client/interfaces.ts} | 0 src/components/gtt-client/openlayers.ts | 505 ++++++ src/components/gtt-client/redmine.ts | 103 ++ src/index.ts | 4 +- ...{CustomIconsDef.js => custom-icons-def.js} | 0 ...ialDesignDef.js => material-design-def.js} | 0 webpack.config.js | 1 + 13 files changed, 1593 insertions(+), 1528 deletions(-) delete mode 100644 src/components/gtt-client.ts rename src/{@types => components/gtt-client}/constants.ts (80%) create mode 100644 src/components/gtt-client/geocoding.ts create mode 100644 src/components/gtt-client/gtt-client-class.ts create mode 100644 src/components/gtt-client/helpers.ts create mode 100644 src/components/gtt-client/index.ts rename src/{@types/types.ts => components/gtt-client/interfaces.ts} (100%) create mode 100644 src/components/gtt-client/openlayers.ts create mode 100644 src/components/gtt-client/redmine.ts rename src/stylesheets/{CustomIconsDef.js => custom-icons-def.js} (100%) rename src/stylesheets/{MaterialDesignDef.js => material-design-def.js} (100%) diff --git a/src/components/gtt-client.ts b/src/components/gtt-client.ts deleted file mode 100644 index ba8851c9..00000000 --- a/src/components/gtt-client.ts +++ /dev/null @@ -1,1525 +0,0 @@ -// OpenLayers core imports -import 'ol/ol.css'; -import { Map, Feature, View, Geolocation } from 'ol'; -import { Geometry, Point } from 'ol/geom'; -import { GeoJSON, WKT, MVT } from 'ol/format'; -import { Layer, Tile, Image, VectorTile as VTLayer } from 'ol/layer'; -import VectorLayer from 'ol/layer/Vector'; -import { OSM, XYZ, TileWMS, ImageWMS, VectorTile as VTSource } from 'ol/source'; -import { Style, Fill, Stroke, Circle } from 'ol/style'; -import { OrderFunction } from 'ol/render'; -import { - defaults as interactions_defaults, - MouseWheelZoom, - Modify, - Draw, - Select, -} from 'ol/interaction'; -import { focus as events_condifition_focus } from 'ol/events/condition'; -import { defaults as control_defaults, FullScreen, Rotate } from 'ol/control'; -import { transform, fromLonLat, transformExtent } from 'ol/proj'; -import { createEmpty, extend, getCenter, containsCoordinate } from 'ol/extent'; -import Vector from 'ol/source/Vector'; -import VectorSource from 'ol/source/Vector'; -import { FeatureLike } from 'ol/Feature'; -import TileSource from 'ol/source/Tile'; -import ImageSource from 'ol/source/Image'; -import { Options as ImageWMSOptions } from 'ol/source/ImageWMS'; -import { Options as VectorTileOptions } from 'ol/source/VectorTile'; -import { applyStyle } from 'ol-mapbox-style'; - -// OpenLayers extension imports -import 'ol-ext/dist/ol-ext.min.css'; -import 'ol-ext/filter/Base'; -import Ordering from 'ol-ext/render/Ordering'; -import Shadow from 'ol-ext/style/Shadow'; -import FontSymbol from 'ol-ext/style/FontSymbol'; -import Mask from 'ol-ext/filter/Mask'; -import Bar from 'ol-ext/control/Bar'; -import Toggle from 'ol-ext/control/Toggle'; -import Button from 'ol-ext/control/Button'; -import TextButton from 'ol-ext/control/TextButton'; -import LayerPopup from 'ol-ext/control/LayerPopup'; -import LayerSwitcher from 'ol-ext/control/LayerSwitcher'; -import Popup from 'ol-ext/overlay/Popup'; -import { position } from 'ol-ext/control/control'; - -// Other imports -import { FeatureCollection } from 'geojson'; -import { ResizeObserver } from '@juggle/resize-observer'; - -// Import types -import { - GttClientOption, - LayerObject, - FilterOption, - TileLayerSource, - ImageLayerSource, - VTLayerSource, -} from '../@types/types'; - -// Import constants -import { quick_hack } from '../@types/constants'; - -export class GttClient { - readonly map: Map - maps: Array - layerArray: Layer[] - defaults: DOMStringMap - contents: DOMStringMap - i18n: any - toolbar: Bar - filters: FilterOption - vector: VectorLayer> - bounds: VectorLayer> - geolocations: Array - - constructor(options: GttClientOption) { - this.filters = { - location: false, - distance: false - }; - this.maps = []; - this.geolocations = []; - - // needs target - if (!options.target) { - return; - } - - const gtt_defaults = document.querySelector('#gtt-defaults') as HTMLDivElement; - if (!gtt_defaults) { - return; - } - - this.defaults = gtt_defaults.dataset; - - const keysToCheck = ['lon', 'lat', 'zoom', 'maxzoom', 'fitMaxzoom', 'geocoder']; - - for (const key of keysToCheck) { - if (this.defaults[key] == null) { - this.defaults[key] = quick_hack[key as keyof typeof quick_hack]?.toString() ?? ''; - } - } - - this.contents = options.target.dataset; - this.i18n = JSON.parse(this.defaults.i18n); - - // create map at first - const { zoomInTipLabel, zoomOutTipLabel } = this.i18n.control; - this.map = new Map({ - target: options.target, - interactions: interactions_defaults({mouseWheelZoom: false}).extend([ - new MouseWheelZoom({ - constrainResolution: true, - condition: events_condifition_focus - }) - ]), - controls: control_defaults({ - rotateOptions: {}, - attributionOptions: { collapsible: false }, - zoomOptions: { zoomInTipLabel, zoomOutTipLabel } - }) - }); - - // Use optional chaining to simplify null checks - // Use optional chaining to simplify null checks - const geom = this.contents?.geom; - let features: Feature[] = []; // Set a default value for features array - if (geom) { - // Destructure object and rename variable for better readability - const { featureProjection: proj } = { featureProjection: 'EPSG:3857' }; - features = new GeoJSON().readFeatures(JSON.parse(geom), { featureProjection: proj }); - } - - // Fix FireFox unloaded font issue - this.reloadFontSymbol() - - // TODO: this is only necessary because setting the initial form value - // through the template causes encoding problems - this.updateForm(features) - this.layerArray = [] - - if (this.contents.layers) { - const layers = JSON.parse(this.contents.layers) as [LayerObject] - layers.forEach((layer) => { - const s = layer.type.split('.') - const layerSource = getLayerSource(s[1], s[2]) - if ( layerSource.type === "TileLayerSource") { - const config = layerSource as TileLayerSource - const l = new (config.layer)({ - visible: false, - source: new (config.source)(layer.options as any) - }) - - l.set('lid', layer.id) - l.set('title', layer.name) - l.set('baseLayer', layer.baselayer) - if( layer.baselayer ) { - l.on('change:visible', e => { - const target = e.target as Tile - if (target.getVisible()) { - const lid = target.get('lid') - document.cookie = `_redmine_gtt_basemap=${lid};path=/` - } - }) - } - this.layerArray.push(l) - } else if (layerSource.type === "ImageLayerSource") { - const config = layerSource as ImageLayerSource - const l = new (config.layer)({ - visible: false, - source: new (config.source)(layer.options as ImageWMSOptions) - }) - - l.set('lid', layer.id) - l.set('title', layer.name) - l.set('baseLayer', layer.baselayer) - if( layer.baselayer ) { - l.on('change:visible', e => { - const target = e.target as Image - if (target.getVisible()) { - const lid = target.get('lid') - document.cookie = `_redmine_gtt_basemap=${lid};path=/` - } - }) - } - this.layerArray.push(l) - } else if (layerSource.type === "VTLayerSource") { - const config = layerSource as VTLayerSource - const options = layer.options as VectorTileOptions - options.format = new MVT() - const l = new (config.layer)({ - visible: false, - source: new (config.source)(options), - declutter: true - }) as VTLayer - - // Apply style URL if provided - if ("styleUrl" in options) { - applyStyle(l,options.styleUrl) - } - - l.set('lid', layer.id) - l.set('title', layer.name) - l.set('baseLayer', layer.baselayer) - if( layer.baselayer ) { - l.on('change:visible', (e: { target: any }) => { - const target = e.target as any - if (target.getVisible()) { - const lid = target.get('lid') - document.cookie = `_redmine_gtt_basemap=${lid};path=/` - } - }) - } - this.layerArray.push(l) - } - }, this) - - /** - * Ordering the Layers for the LayerSwitcher Control. - * BaseLayers are added first. - */ - this.layerArray.forEach( (l:Layer) => { - if( l.get("baseLayer") ) { - this.map.addLayer(l) - } - } - ) - - var containsOverlay = false; - - this.layerArray.forEach( (l:Layer) => { - if( !l.get("baseLayer") ) { - this.map.addLayer(l) - containsOverlay = true - } - } - ) - } - - this.setBasemap() - - // Layer for project boundary - this.bounds = new VectorLayer({ - source: new Vector(), - style: new Style({ - fill: new Fill({ - color: 'rgba(255,255,255,0.0)' - }), - stroke: new Stroke({ - color: 'rgba(220,26,26,0.7)', - // lineDash: [12,1,12], - width: 1 - }) - }) - }) - this.bounds.set('title', 'Boundaries') - this.bounds.set('displayInLayerSwitcher', false) - this.layerArray.push(this.bounds) - this.map.addLayer(this.bounds) - - const yOrdering: unknown = Ordering.yOrdering() - - this.vector = new VectorLayer>({ - source: new Vector({ - 'features': features, - 'useSpatialIndex': false - }), - renderOrder: yOrdering as OrderFunction, - style: this.getStyle.bind(this) - }) - this.vector.set('title', 'Features') - this.vector.set('displayInLayerSwitcher', false) - this.layerArray.push(this.vector) - this.map.addLayer(this.vector) - - // Render project boundary if bounds are available - if (this.contents.bounds && this.contents.bounds !== null) { - const boundary = new GeoJSON().readFeature( - this.contents.bounds, { - featureProjection: 'EPSG:3857' - } - ) - this.bounds.getSource().addFeature(boundary) - if (this.contents.bounds === this.contents.geom) { - this.vector.setVisible(false) - } - this.layerArray.forEach((layer:Layer) => { - if (layer.get('baseLayer')) { - layer.addFilter(new Mask({ - feature: boundary, - inner: false, - fill: new Fill({ - color: [220,26,26,.1] - }) - })) - } - }) - } - - // For map div focus settings - if (options.target) { - if (options.target.getAttribute('tabindex') == null) { - options.target.setAttribute('tabindex', '0') - } - } - - // Fix empty map issue - this.map.once('postrender', e => { - this.zoomToExtent(true); - }) - - // Add Toolbar - this.toolbar = new Bar() - this.toolbar.setPosition('bottom-left' as position) - this.map.addControl(this.toolbar) - this.setView() - this.setGeocoding(this.map) - this.setGeolocation(this.map) - this.parseHistory() - - this.map.addControl (new FullScreen({ - tipLabel: this.i18n.control.fullscreen - })) - this.map.addControl (new Rotate({ - tipLabel: this.i18n.control.rotate - })) - - // Control button - const maximizeCtrl = new Button({ - html: 'zoom_out_map', - title: this.i18n.control.maximize, - handleClick: () => { - this.zoomToExtent(true); - } - }) - this.toolbar.addControl(maximizeCtrl) - - // Map rotation - const rotation_field = document.querySelector('#gtt_configuration_map_rotation') as HTMLInputElement - if (rotation_field !== null) { - this.map.getView().on('change:rotation', (evt) => { - rotation_field.value = String(Math.round(radiansToDegrees(evt.target.getRotation()))) - }) - - rotation_field.addEventListener("input", (evt) => { - const { target } = evt; - if (!(target instanceof HTMLInputElement)) { - return; - } - const value = target.value; - this.map.getView().setRotation(degreesToRadians(parseInt(value))) - }) - } - - if (this.contents.edit) { - this.setControls(this.contents.edit.split(' ')) - } else if (this.contents.popup) { - this.setPopover() - } - - // Zoom to extent when map collapsed => expended - if (this.contents.collapsed) { - const self = this - const collapsedObserver = new MutationObserver((mutations) => { - // const currentMap = this.map - mutations.forEach(function(mutation) { - if (mutation.attributeName !== 'style') { - return - } - const mapDiv = mutation.target as HTMLDivElement - if (mapDiv && mapDiv.style.display === 'block') { - self.zoomToExtent(true) - collapsedObserver.disconnect() - } - }) - }) - collapsedObserver.observe(self.map.getTargetElement(), { attributes: true, attributeFilter: ['style'] }) - } - - // Sidebar hack - const resizeObserver = new ResizeObserver((entries, observer) => { - this.maps.forEach(m => { - m.updateSize() - }) - }) - resizeObserver.observe(this.map.getTargetElement()) - - // When one or more issues is selected, zoom to selected map features - document.querySelectorAll('table.issues tbody tr').forEach((element: HTMLTableRowElement) => { - element.addEventListener('click', (evt) => { - const currentTarget = evt.currentTarget as HTMLTableRowElement - const id = currentTarget.id.split('-')[1] - const feature = this.vector.getSource().getFeatureById(id) - this.map.getView().fit(feature.getGeometry().getExtent(), { - size: this.map.getSize() - }) - }) - }) - - // Need to update size of an invisible map, when the editable form is made - // visible. This doesn't look like a good way to do it, but this is more of - // a Redmine problem - document.querySelectorAll('div.contextual a.icon-edit').forEach((element: HTMLAnchorElement) => { - element.addEventListener('click', () => { - setTimeout(() => { - this.maps.forEach(m => { - m.updateSize() - }) - this.zoomToExtent() - }, 200) - }) - }) - - // Redraw the map, when a GTT Tab gets activated - document.querySelectorAll('#tab-gtt').forEach((element) => { - element.addEventListener('click', () => { - this.maps.forEach(m => { - m.updateSize() - }) - this.zoomToExtent() - }) - }) - - // Add LayerSwitcher Image Toolbar - if( containsOverlay) { - this.map.addControl(new LayerSwitcher({ - reordering: false - })) - } - else { - this.map.addControl(new LayerPopup()) - } - - - // Because Redmine filter functions are applied later, the Window onload - // event provides a workaround to have filters loaded before executing - // the following code - window.addEventListener('load', () => { - if (document.querySelectorAll('tr#tr_bbox').length > 0) { - this.filters.location = true - } - if (document.querySelectorAll('tr#tr_distance').length > 0) { - this.filters.distance = true - } - const legend = document.querySelector('fieldset#location legend') as HTMLLegendElement - if (legend) { - legend.addEventListener('click', (evt) => { - const element = evt.currentTarget as HTMLLegendElement - this.toggleAndLoadMap(element) - }) - } - this.zoomToExtent() - this.map.on('moveend', this.updateFilter.bind(this)) - }) - - // Handle multiple maps per page - this.maps.push(this.map) - } - - /** - * Add editing tools - */ - setControls(types: Array) { - // Make vector features editable - const modify = new Modify({ - features: this.vector.getSource().getFeaturesCollection() - }) - - modify.on('modifyend', evt => { - this.updateForm(evt.features.getArray(), true) - }) - - this.map.addInteraction(modify) - - const mainbar = new Bar() - mainbar.setPosition("top-left" as position) - this.map.addControl(mainbar) - - const editbar = new Bar({ - toggleOne: true, // one control active at the same time - group: true // group controls together - }) - mainbar.addControl(editbar) - - types.forEach((type: any, idx) => { - const draw = new Draw({ - type: type, - source: this.vector.getSource() - }) - - draw.on('drawend', evt => { - this.vector.getSource().clear() - this.updateForm([evt.feature], true) - }) - - // Material design icon - let mdi = 'place' - - switch (type.toLowerCase()) { - case 'linestring': - mdi = 'polyline' - break; - - case 'polygon': - mdi = 'format_shapes' - break; - } - - const control = new Toggle({ - html: `${mdi}`, - title: this.i18n.control[type.toLowerCase()], - interaction: draw, - active: (idx === 0) - }) - editbar.addControl(control) - }) - - // Uses jQuery UI for GeoJSON Upload modal window - const mapObj = this - const dialog = $("#dialog-geojson-upload").dialog({ - autoOpen: false, - resizable: true, - height: 'auto', - width: 380, - modal: true, - buttons: { - [mapObj.i18n.modal.load]: function() { - const geojson_input = document.querySelector('#dialog-geojson-upload textarea') as HTMLInputElement - const data = geojson_input.value - if (data !== null) { - const features = new GeoJSON().readFeatures( - JSON.parse(data), { - featureProjection: 'EPSG:3857' - } - ) - mapObj.vector.getSource().clear() - mapObj.vector.getSource().addFeatures(features) - mapObj.updateForm(features) - mapObj.zoomToExtent() - } - $(this).dialog('close') - }, - [mapObj.i18n.modal.cancel]: function() { - $(this).dialog('close') - } - } - }); - - // Upload button - if (this.contents.upload === "true") { - - const fileSelector = document.getElementById('file-selector') - fileSelector.addEventListener('change', (event: any) => { - const file = event.target.files[0] - // Check if the file is GeoJSON. - if (file.type && !file.type.startsWith('application/geo')) { - console.log('File is not a GeoJSON document.', file.type, file); - return; - } - const fileReader = new FileReader(); - fileReader.addEventListener('load', (event: any) => { - const geojson_input = document.querySelector('#dialog-geojson-upload textarea') as HTMLInputElement - geojson_input.value = JSON.stringify(event.target.result, null, 2) - }); - fileReader.readAsText(file); - }); - - editbar.addControl(new Button({ - html: 'file_upload', - title: this.i18n.control.upload, - handleClick: () => { - dialog.dialog('open') - } - })) - } - } - - setPopover() { - const popup = new Popup({ - popupClass: 'default', - closeBox: false, - onclose: () => {}, - positioning: 'auto', - anim: true - }) - this.map.addOverlay(popup) - - // Control Select - const select = new Select({ - layers: [this.vector], - style: null, - multi: false - }) - this.map.addInteraction(select) - - // On selected => show/hide popup - select.getFeatures().on(['add'], (evt: any) => { - const feature = evt.element - - const content: Array = [] - content.push(`${feature.get('subject')}
    `) - // content.push('Starts at: ' + feature.get("start_date") + ' |'); - - const popup_contents = JSON.parse(this.contents.popup) - const url = popup_contents.href.replace(/\[(.+?)\]/g, feature.get('id')) - content.push(`Edit`) - - popup.show(feature.getGeometry().getFirstCoordinate(), content.join('') as any) - }) - - select.getFeatures().on(['remove'], _ => { - popup.hide() - }) - - // change mouse cursor when over marker - this.map.on('pointermove', evt => { - if (evt.dragging) return - const hit = this.map.hasFeatureAtPixel(evt.pixel, { - layerFilter: (layer) => { - return layer === this.vector - } - }) - this.map.getTargetElement().style.cursor = hit ? 'pointer' : '' - }) - } - - updateForm(features: FeatureLike[] | null, updateAddressFlag: boolean = false):void { - if (features == null) { - return - } - const geom = document.querySelector('#geom') as HTMLInputElement - if (!geom) { - return - } - - const writer = new GeoJSON() - // Convert to Feature type for GeoJSON writer - const new_features: Feature[] = features.map((feature => { - return new Feature({geometry: feature.getGeometry() as Geometry}) - })) - const geojson_str = writer.writeFeatures(new_features, { - featureProjection: 'EPSG:3857', - dataProjection: 'EPSG:4326' - }) - const geojson = JSON.parse(geojson_str) as FeatureCollection - geom.value = JSON.stringify(geojson.features[0]) - - const geocoder = JSON.parse(this.defaults.geocoder) - if (updateAddressFlag && geocoder.address_field_name && features && features.length > 0) { - let addressInput: HTMLInputElement = null - document.querySelectorAll(`#issue-form #attributes label`).forEach(element => { - if (element.innerHTML.includes(geocoder.address_field_name)) { - addressInput = element.parentNode.querySelector('p').querySelector('input') as HTMLInputElement - } - }) - if (addressInput) { - // Todo: only works with point geometries for now for the last geometry - const geom = features[features.length - 1].getGeometry() as Point - if (geom === null) { - return - } - let coords = geom.getCoordinates() - coords = transform(coords, 'EPSG:3857', 'EPSG:4326') - const reverse_geocode_url = geocoder.reverse_geocode_url.replace("{lon}", coords[0].toString()).replace("{lat}", coords[1].toString()) - fetch(reverse_geocode_url) - .then(response => response.json()) - .then(data => { - const check = evaluateComparison(getObjectPathValue(data, geocoder.reverse_geocode_result_check_path), - geocoder.reverse_geocode_result_check_operator, - geocoder.reverse_geocode_result_check_value) - let districtInput: HTMLInputElement = null - document.querySelectorAll(`#issue-form #attributes label`).forEach(element => { - if (element.innerHTML.includes(geocoder.district_field_name)) { - districtInput = element.parentNode.querySelector('p').querySelector('input') as HTMLInputElement - } - }) - const address = getObjectPathValue(data, geocoder.reverse_geocode_result_address_path) - let foundDistrict = false - if (check && address) { - addressInput.value = address - if (districtInput) { - const district = getObjectPathValue(data, geocoder.reverse_geocode_result_district_path) - if (district) { - const regexp = new RegExp(geocoder.reverse_geocode_result_district_regexp) - const match = regexp.exec(district) - if (match && match.length === 2) { - districtInput.value = match[1] - foundDistrict = true - } - } - } - } else { - addressInput.value = geocoder.empty_field_value - } - if (!foundDistrict) { - if (districtInput) { - districtInput.value = '' - } - } - }) - } - } - - } - - /** - * Decide which baselayer to show - */ - setBasemap(): void { - if (this.layerArray.length == 0) { - console.error("There is no baselayer available!") - return - } - - let index = 0 - const cookie = parseInt(getCookie('_redmine_gtt_basemap')) - if (cookie) { - let lid = 0 - // Check if layer ID exists in available layers - this.layerArray.forEach((layer) => { - if (cookie === layer.get("lid")) { - lid = cookie - } - }) - - // Set selected layer visible - this.layerArray.forEach((layer, idx) => { - if (lid === layer.get("lid")) { - index = idx - } - }) - } - - // Set layer visible - this.layerArray[index].setVisible(true) - } - - getColor(feature: Feature, isFill: boolean = false): string { - let color = '#000000' - if (feature.getGeometry().getType() !== 'Point') { - color = '#FFD700' - } - const plugin_settings = JSON.parse(this.defaults.pluginSettings) - const status = document.querySelector('#issue_status_id') as HTMLInputElement - - let status_id = feature.get('status_id') - if (!status_id && status) { - status_id = status.value - } - if (status_id) { - const key = `status_${status_id}` - if (key in plugin_settings) { - color = plugin_settings[key] - } - } - if (isFill && color !== null && color.length === 7) { - color = color + '33' // Add alpha: 0.2 - } - return color - } - - getFontColor(_: unknown): string { - const color = "#FFFFFF" - return color - } - - getSymbol(feature: Feature) { - let symbol = 'home' - - const plugin_settings = JSON.parse(this.defaults.pluginSettings) - const issue_tracker = document.querySelector('#issue_tracker_id') as HTMLInputElement - let tracker_id = feature.get('tracker_id') - if (!tracker_id && issue_tracker) { - tracker_id = issue_tracker.value - } - if (tracker_id) { - const key = `tracker_${tracker_id}` - if (key in plugin_settings) { - symbol = plugin_settings[key] - } - } - return symbol - } - - getStyle(feature: Feature, _: unknown):Style[] { - const styles: Style[] = [] - - // Apply Shadow - styles.push( - new Style({ - image: new Shadow({ - radius: 15, - blur: 5, - offsetX: 0, - offsetY: 0, - fill: new Fill({ - color: 'rgba(0,0,0,0.5)' - }) - }) - }) - ) - - const self = this - - // Apply Font Style - styles.push( - new Style({ - image: new FontSymbol({ - form: 'blazon', - gradient: false, - glyph: self.getSymbol(feature), - fontSize: 0.7, - radius: 18, - offsetY: -18, - rotation: 0, - rotateWithView: false, - color: self.getFontColor(feature), - fill: new Fill({ - color: self.getColor(feature) - }), - stroke: new Stroke({ - color: '#333333', - width: 1 - }), - opacity: 1, - }), - stroke: new Stroke({ - width: 4, - color: self.getColor(feature) - }), - fill: new Fill({ - color: self.getColor(feature, true), - }) - }) - ) - - return styles - } - - /** - * - */ - setView() { - const center = fromLonLat([parseFloat(this.defaults.lon), parseFloat(this.defaults.lat)]) - const view = new View({ - // Avoid flicker (map move) - center: center, - zoom: parseInt(this.defaults.zoom), - maxZoom: parseInt(this.defaults.maxzoom), // applies for Mierune Tiles - rotation: degreesToRadians(parseInt(this.map.getTargetElement().getAttribute("data-rotation"))) - }) - this.map.setView(view) - } - - /** - * - */ - zoomToExtent(force: boolean = true) { - if (!force && (this.filters.distance || this.filters.location)) { - // Do not zoom to extent but show the previous extent stored as cookie - const parts = (getCookie("_redmine_gtt_permalink")).split("/"); - this.maps.forEach(m => { - m.getView().setZoom(parseInt(parts[0], 10)) - m.getView().setCenter(transform([ - parseFloat(parts[1]), - parseFloat(parts[2]) - ],'EPSG:4326','EPSG:3857')) - m.getView().setRotation(parseFloat(parts[3])) - }) - } else if (this.vector.getSource().getFeatures().length > 0) { - let extent = createEmpty() - // Because the vector layer is set to "useSpatialIndex": false, we cannot - // make use of "vector.getSource().getExtent()" - this.vector.getSource().getFeatures().forEach(feature => { - extend(extent, feature.getGeometry().getExtent()) - }) - this.maps.forEach(m => { - m.getView().fit(extent, { - size: getMapSize(m), - maxZoom: parseInt(this.defaults.fitMaxzoom) - }) - }) - } else if (this.bounds.getSource().getFeatures().length > 0) { - this.maps.forEach(m => { - m.getView().fit(this.bounds.getSource().getExtent(), { - size: getMapSize(m), - maxZoom: parseInt(this.defaults.fitMaxzoom) - }) - }) - } else { - // Set default center, once - this.maps.forEach(m => { - m.getView().setCenter(transform([parseFloat(this.defaults.lon), parseFloat(this.defaults.lat)], - 'EPSG:4326', 'EPSG:3857')); - }) - this.geolocations.forEach(g => { - g.once('change:position', (evt) => { - this.maps.forEach(m => { - m.getView().setCenter(g.getPosition()) - }) - }) - }) - } - } - - /** - * Updates map settings for Redmine filter - */ - updateFilter() { - let center = this.map.getView().getCenter() - let extent = this.map.getView().calculateExtent(this.map.getSize()) - - center = transform(center,'EPSG:3857','EPSG:4326') - // console.log("Map Center (WGS84): ", center); - const fieldset = document.querySelector('fieldset#location') as HTMLFieldSetElement - if (fieldset) { - fieldset.dataset.center = JSON.stringify(center) - } - const value_distance_3 = document.querySelector('#tr_distance #values_distance_3') as HTMLInputElement - if (value_distance_3) { - value_distance_3.value = center[0].toString() - } - const value_distance_4 = document.querySelector('#tr_distance #values_distance_4') as HTMLInputElement - if (value_distance_4) { - value_distance_4.value = center[1].toString() - } - - // Set Permalink as Cookie - const cookie = [] - const hash = this.map.getView().getZoom() + '/' + - Math.round(center[0] * 1000000) / 1000000 + '/' + - Math.round(center[1] * 1000000) / 1000000 + '/' + - this.map.getView().getRotation() - cookie.push("_redmine_gtt_permalink=" + hash) - cookie.push("path=" + window.location.pathname) - document.cookie = cookie.join(";") - - const extent_str = transformExtent(extent,'EPSG:3857','EPSG:4326').join('|') - // console.log("Map Extent (WGS84): ",extent); - const bbox = document.querySelector('select[name="v[bbox][]"]') - if (bbox) { - const option = bbox.querySelector('option') as HTMLOptionElement - option.value = extent_str - } - // adjust the value of the 'On map' option tag - // Also adjust the JSON data that's the basis for building the filter row - // html (this is relevant if the map is moved first and then the filter is - // added.) - if(window.availableFilters && window.availableFilters.bbox) { - window.availableFilters.bbox.values = [['On map', extent]] - } - } - - - /** - * Parse page for WKT strings in history - */ - parseHistory() { - const historyItems = document.querySelectorAll('div#history ul.details i'); - - const regex = /\b(?:POINT|LINESTRING|POLYGON)\b\s?\({1,}[-]?\d+([,. ]\s?[-]?\d+)*\){1,}/gi; - const dataProjection = 'EPSG:4326'; - const featureProjection = 'EPSG:3857'; - - const parseAndFormatWKT = (wkt: string) => { - const feature = new WKT().readFeature(wkt, { dataProjection, featureProjection }); - let formattedWKT = new WKT().writeFeature(feature, { dataProjection, featureProjection, decimals: 5 }); - - if (formattedWKT.length > 30) { - const parts = formattedWKT.split(' '); - formattedWKT = `${parts[0]}...${parts[parts.length - 1]}`; - } - - return formattedWKT; - }; - - historyItems.forEach((item: Element) => { - const match = item.innerHTML.match(regex); - - if (match !== null) { - const wkt = parseAndFormatWKT(match.join('')); - - const link = document.createElement('a'); - link.href = '#'; - link.classList.add('wkt'); - link.dataset.feature = match.join(''); - link.textContent = wkt; - - // Replace current node with new link. - item.replaceWith(link); - } - }); - } - - /** - * Add Geolocation functionality - */ - setGeolocation(currentMap: Map) { - const geolocation = new Geolocation({ - tracking: false, - projection: currentMap.getView().getProjection() - }) - this.geolocations.push(geolocation) - - geolocation.on('change', (evt) => { - // console.log({ - // accuracy: geolocation.getAccuracy(), - // altitude: geolocation.getAltitude(), - // altitudeAccuracy: geolocation.getAltitudeAccuracy(), - // heading: geolocation.getHeading(), - // speed: geolocation.getSpeed() - // }) - }) - geolocation.on('error', (error) => { - // TBD - console.error(error) - }) - - const accuracyFeature = new Feature() - geolocation.on('change:accuracyGeometry', (evt) => { - accuracyFeature.setGeometry(geolocation.getAccuracyGeometry()) - }) - - const positionFeature = new Feature() - positionFeature.setStyle(new Style({ - image: new Circle({ - radius: 6, - fill: new Fill({ - color: '#3399CC' - }), - stroke: new Stroke({ - color: '#fff', - width: 2 - }) - }) - })) - - geolocation.on('change:position', (evt) => { - const position = geolocation.getPosition() - positionFeature.setGeometry(position ? new Point(position) : null) - - const extent = currentMap.getView().calculateExtent(currentMap.getSize()) - if (!containsCoordinate(extent, position)) { - currentMap.getView().setCenter(position) - } - }) - - const geolocationLayer = new VectorLayer({ - source: new Vector({ - features: [accuracyFeature, positionFeature] - }) - }) - geolocationLayer.set('displayInLayerSwitcher', false) - currentMap.addLayer(geolocationLayer) - - // Control button - const geolocationCtrl = new Toggle({ - html: 'my_location', - title: this.i18n.control.geolocation, - active: false, - onToggle: (active: boolean) => { - geolocation.setTracking(active) - geolocationLayer.setVisible(active) - } - }) - this.toolbar.addControl(geolocationCtrl) - } - - /** - * Add Geocoding functionality - */ - setGeocoding(currentMap: Map):void { - - // Hack to add Geocoding buttons to text fields - // There should be a better way to do this - const geocoder = JSON.parse(this.defaults.geocoder) - if (geocoder.geocode_url && - geocoder.address_field_name && - document.querySelectorAll("#issue-form #attributes button.btn-geocode").length == 0) - { - document.querySelectorAll(`#issue-form #attributes label`).forEach(element => { - if (element.textContent.includes(geocoder.address_field_name)) { - element.querySelectorAll('p').forEach(p_element => { - const button = document.createElement('button') as HTMLButtonElement - button.name = 'button' - button.type = 'button' - button.className = 'btn-geocode' - button.appendChild(document.createTextNode(geocoder.address_field_name)) - p_element.appendChild(button) - }) - } - }) - - document.querySelectorAll('button.btn-geocode').forEach(element => { - element.addEventListener('click', (evt) => { - // Geocode address and add/update icon on map - const button = evt.currentTarget as HTMLButtonElement - if (button.previousElementSibling.querySelector('input').value != '') { - const address = button.previousElementSibling.querySelector('input').value - const geocode_url = geocoder.geocode_url.replace("{address}", encodeURIComponent(address)) - fetch(geocode_url) - .then(response => response.json()) - .then(data => { - const check = evaluateComparison(getObjectPathValue(data, geocoder.geocode_result_check_path), - geocoder.geocode_result_check_operator, - geocoder.geocode_result_check_value - ) - if (check) { - const lon = getObjectPathValue(data, geocoder.geocode_result_lon_path) - const lat = getObjectPathValue(data, geocoder.geocode_result_lat_path) - const coords = [lon, lat] - const geom = new Point(fromLonLat(coords, 'EPSG:3857')) - const features = this.vector.getSource().getFeatures() - if (features.length > 0) { - features[features.length - 1].setGeometry(geom) - } else { - const feature = new Feature(geom) - this.vector.getSource().addFeatures([feature]) - } - this.updateForm(this.vector.getSource().getFeatures()) - this.zoomToExtent(true) - - const _districtInput = document.querySelectorAll(`#issue-form #attributes label`) - let districtInput: HTMLInputElement = null - _districtInput.forEach(element => { - if (element.innerHTML.includes(geocoder.district_field_name)) { - districtInput = element.parentNode.querySelector('p').querySelector('input') - } - }) - let foundDistrict = false - if (districtInput) { - const district = getObjectPathValue(data, geocoder.geocode_result_district_path) - if (district) { - const regexp = new RegExp(geocoder.geocode_result_district_regexp) - const match = regexp.exec(district) - if (match && match.length === 2) { - districtInput.value = match[1] - foundDistrict = true - } - } - if (!foundDistrict) { - if (districtInput) { - districtInput.value = "" - } - } - } - } - }) - } - }) - }) - } - - if (geocoder.place_search_url && - geocoder.place_search_field_name && - document.querySelectorAll("#issue-form #attributes button.btn-placesearch").length == 0 ) - { - document.querySelectorAll(`#issue-form #attributes label`).forEach(element => { - if (element.innerHTML.includes(geocoder.place_search_field_name)) { - element.querySelectorAll('p').forEach(p_element => { - const button = document.createElement('button') as HTMLButtonElement - button.name = 'button' - button.type = 'button' - button.className = 'btn-placesearch' - button.appendChild(document.createTextNode(geocoder.place_search_field_name)) - p_element.appendChild(button) - }) - } - }) - - document.querySelectorAll("button.btn-placesearch").forEach(element => { - element.addEventListener('click', () => { - if (this.vector.getSource().getFeatures().length > 0) { - let coords = null - this.vector.getSource().getFeatures().forEach((feature) => { - // Todo: only works with point geometries for now for the last geometry - coords = getCenter(feature.getGeometry().getExtent()) - }) - coords = transform(coords, 'EPSG:3857', 'EPSG:4326') - const place_search_url = geocoder.place_search_url.replace("{lon}", coords[0].toString()).replace("{lat}", coords[1].toString()) - fetch(place_search_url) - .then(response => response.json()) - .then(data => { - const list:Array = getObjectPathValue(data, geocoder.place_search_result_list_path) - if (list.length > 0) { - const modal = document.querySelector('#ajax-modal') as HTMLDivElement - modal.innerHTML = ` -

    ${geocoder.place_search_result_ui_title}

    -
    -

    - -

    - ` - modal.classList.add('place_search_results') - list.forEach(item => { - const display = getObjectPathValue(item, geocoder.place_search_result_display_path) - const value = getObjectPathValue(item, geocoder.place_search_result_value_path) - if (display && value) { - const places = document.querySelector('div#places') as HTMLDivElement - const input = document.createElement('input') as HTMLInputElement - input.type = 'radio' - input.name = 'places' - input.value = value - input.appendChild(document.createTextNode(display)) - places.appendChild(input) - places.appendChild(document.createElement('br')) - } - }) - window.showModal('ajax-model', '400px') - document.querySelector("p.buttons input[type='submit']").addEventListener('click', () => { - let input: HTMLInputElement = null - document.querySelectorAll(`#issue-form #attributes label`).forEach(element => { - if (element.innerHTML.includes(geocoder.place_search_field_name)) { - input = element.parentNode.querySelector('p').querySelector('input') as HTMLInputElement - } - }) - if (input) { - input.value = (document.querySelector("div#places input[type='radio']:checked") as HTMLInputElement).value - } - }) - } else { - let input: HTMLInputElement = null - document.querySelectorAll(`#issue-form #attributes label`).forEach(element => { - if (element.innerHTML.includes(geocoder.place_search_field_name)) { - input = element.parentNode.querySelector('p').querySelector('input') as HTMLInputElement - } - }) - if (input) { - input.value = geocoder.empty_field_value - } - } - }) - } - }) - }) - } - - // disable geocoding control if plugin setting is not true - if (this.contents.geocoding !== "true") { - return - } - - const mapId = currentMap.getTargetElement().getAttribute("id") - - // Control button - const geocodingCtrl = new Toggle({ - html: 'manage_search', - title: this.i18n.control.geocoding, - className: "ctl-geocoding", - onToggle: (active: boolean) => { - const text = (document.querySelector("div#" + mapId + " .ctl-geocoding div input") as HTMLInputElement) - if (active) { - text.focus() - } else { - text.blur() - const button = document.querySelector("div#" + mapId + " .ctl-geocoding button") - button.blur() - } - }, - bar: new Bar({ - controls: [ - new TextButton({ - html: '
    ' - }) - ] - }) - }) - this.toolbar.addControl(geocodingCtrl) - - // Make Geocoding API request - document.querySelector("div#" + mapId + " .ctl-geocoding div input").addEventListener('keydown', (evt) => { - if (evt.keyCode === 13) { - evt.preventDefault() - evt.stopPropagation() - } else { - return true - } - - if (!geocoder.geocode_url) { - throw new Error ("No Geocoding service configured!") - } - - const url = geocoder.geocode_url.replace("{address}", encodeURIComponent( - (document.querySelector("div#" + mapId + " .ctl-geocoding form input[name=address]") as HTMLInputElement).value) - ) - - fetch(url) - .then(response => response.json()) - .then(data => { - const check = evaluateComparison(getObjectPathValue(data, geocoder.geocode_result_check_path), - geocoder.geocode_result_check_operator, - geocoder.geocode_result_check_value - ) - if (check) { - const lon = getObjectPathValue(data, geocoder.geocode_result_lon_path) - const lat = getObjectPathValue(data, geocoder.geocode_result_lat_path) - const coords = [lon, lat] - const geom = new Point(fromLonLat(coords, 'EPSG:3857')) - currentMap.getView().fit(geom.getExtent(), { - size: currentMap.getSize(), - maxZoom: parseInt(this.defaults.fitMaxzoom) - }) - } - }) - - return false - }) - } - - reloadFontSymbol() { - if ('fonts' in document) { - const symbolFonts: Array = [] - for (const font in FontSymbol.defs.fonts) { - symbolFonts.push(font) - } - if (symbolFonts.length > 0) { - (document as any).fonts.addEventListener('loadingdone', (e: any) => { - const fontIndex = e.fontfaces.findIndex((font: any) => { - return symbolFonts.indexOf(font.family) >= 0 - }) - if (fontIndex >= 0) { - this.maps.forEach(m => { - const layers = m.getLayers() - layers.forEach(layer => { - if (layer instanceof VectorLayer && - layer.getKeys().indexOf("title") >= 0 && - layer.get("title") === "Features") { - const features = layer.getSource().getFeatures() - const pointIndex = features.findIndex((feature: Feature) => { - return feature.getGeometry().getType() === "Point" - }) - if (pointIndex >= 0) { - // console.log("Reloading Features layer") - layer.changed() - } - } - }) - }) - } - }) - } - } - } - - toggleAndLoadMap(el: HTMLLegendElement) { - const fieldset = el.parentElement - fieldset.classList.toggle('collapsed') - el.classList.toggle('icon-expended') - el.classList.toggle('icon-collapsed') - const div = fieldset.querySelector('div') - if (div.style.display === 'none') { - div.style.display = 'block' - } else { - div.style.display = 'none' - } - this.maps.forEach(function (m) { - m.updateSize() - }) - } -} - -const getLayerSource = (source: string, class_name: string): TileLayerSource | ImageLayerSource | VTLayerSource | undefined => { - if (source === 'source') { - if (class_name === 'OSM') { - return { layer: Tile, source: OSM, type: "TileLayerSource" } - } else if (class_name === 'XYZ') { - return { layer: Tile, source: XYZ, type: "TileLayerSource" } - } else if (class_name === 'TileWMS') { - return { layer: Tile, source: TileWMS, type: "TileLayerSource" } - } else if (class_name === 'ImageWMS') { - return { layer: Image, source: ImageWMS, type: "ImageLayerSource" } - } else if (class_name === 'VectorTile') { - return { layer: VTLayer, source: VTSource, type: "VTLayerSource" } - } - } - return undefined -} - -const getCookie = (cname: string): string => { - const name = cname + '='; - const decodedCookie = decodeURIComponent(document.cookie); - const ca = decodedCookie.split(';'); - - for(let i = 0; i { - const degrees = radians * (180 / Math.PI); - return (degrees + 360) % 360; -} - -const degreesToRadians = (degrees: number): number => degrees * (Math.PI / 180); - -const getMapSize = (map: Map): number[] => { - const [width, height] = map.getSize(); - - if (width <= 0 || height <= 0) { - const target = map.getTarget() as HTMLElement; - return [target.clientWidth, target.clientHeight]; - } - - return [width, height]; -} - -const evaluateComparison = (left: any, operator: any, right: any): any => { - if (typeof left == 'object') { - left = JSON.stringify(left) - return Function('"use strict";return (JSON.parse(\'' + left + '\')' + operator + right + ')')() - } else { - return Function('"use strict";return (' + left + operator + right + ')')() - } -} - -const getObjectPathValue = (obj: any, path: string | Array, def: any = null) => { - const pathArr = Array.isArray(path) ? path : path.split(".").flatMap((key) => key.split(/\[([^}]+)\]/g).filter(Boolean)); - return pathArr.reduce((acc, key) => acc?.[key], obj) ?? def; -} - -/** - * Extend core Redmine's buildFilterRow method - */ -window.buildFilterRowWithoutDistanceFilter = window.buildFilterRow; -window.buildFilterRow = function(field, operator, values) { - if (field == 'distance') { - buildDistanceFilterRow(operator, values) - } else { - window.buildFilterRowWithoutDistanceFilter(field, operator, values) - } -} - -const buildDistanceFilterRow = (operator: any, values: any):void => { - const field = 'distance' - const fieldId = field - const filterTable = document.querySelector('#filters-table') as HTMLTableElement - const filterOptions = window.availableFilters[field] - if (!filterOptions) { - return - } - const operators = window.operatorByType[filterOptions['type']] - const filterValues = filterOptions['values'] - - const tr = document.createElement('tr') as HTMLTableRowElement - tr.className = 'filter' - tr.id = `tr_${fieldId}` - tr.innerHTML = ` - - - - - - - - - - - - - `; - (document.querySelector(`#values_${fieldId}_1`) as HTMLInputElement).value = values[0] - let base_idx = 1 - if (values.length == 2 || values.length == 4) { - // upper bound for 'between' operator - (document.querySelector(`#values_${fieldId}_2`) as HTMLInputElement).value = values[1] - base_idx = 2 - } - let x, y - if (values.length > 2) { - // console.log('distance center point from values: ', values[base_idx], values[base_idx+1]); - x = values[base_idx] - y = values[base_idx+1] - } else { - // console.log('taking distance from map fieldset: ', $('fieldset#location').data('center')); - const fieldset = document.querySelector('fieldset#location') as HTMLFieldSetElement - if (!fieldset.dataset.center) { - return - } - const xy = JSON.parse(fieldset.dataset.center) - x = xy[0] - y = xy[1] - } - (document.querySelector(`#values_${fieldId}_3`) as HTMLInputElement).value = x; - (document.querySelector(`#values_${fieldId}_4`) as HTMLInputElement).value = y; -} - -window.replaceIssueFormWithInitMap = window.replaceIssueFormWith -window.replaceIssueFormWith = (html) => { - window.replaceIssueFormWithInitMap(html) - const ol_maps = document.querySelector("form[class$='_issue'] div.ol-map") as HTMLDivElement - if (ol_maps) { - new GttClient({target: ol_maps}) - } -} diff --git a/src/@types/constants.ts b/src/components/gtt-client/constants.ts similarity index 80% rename from src/@types/constants.ts rename to src/components/gtt-client/constants.ts index ad986c23..c531df85 100644 --- a/src/@types/constants.ts +++ b/src/components/gtt-client/constants.ts @@ -1,5 +1,5 @@ // constants.ts -export const quick_hack = { +export const constants = { lon: 139.691706, lat: 35.689524, zoom: 13, diff --git a/src/components/gtt-client/geocoding.ts b/src/components/gtt-client/geocoding.ts new file mode 100644 index 00000000..fda5cab5 --- /dev/null +++ b/src/components/gtt-client/geocoding.ts @@ -0,0 +1,250 @@ +import { Map, Feature } from 'ol'; +import { Point } from 'ol/geom'; +import { transform, fromLonLat } from 'ol/proj'; +import { getCenter } from 'ol/extent'; +import Bar from 'ol-ext/control/Bar'; +import Toggle from 'ol-ext/control/Toggle'; +import TextButton from 'ol-ext/control/TextButton'; + +import { evaluateComparison, getObjectPathValue } from "./helpers"; + +/** + * Add Geocoding functionality + */ +export function setGeocoding(currentMap: Map):void { + + // Hack to add Geocoding buttons to text fields + // There should be a better way to do this + const geocoder = JSON.parse(this.defaults.geocoder) + if (geocoder.geocode_url && + geocoder.address_field_name && + document.querySelectorAll("#issue-form #attributes button.btn-geocode").length == 0) + { + document.querySelectorAll(`#issue-form #attributes label`).forEach(element => { + if (element.textContent.includes(geocoder.address_field_name)) { + element.querySelectorAll('p').forEach(p_element => { + const button = document.createElement('button') as HTMLButtonElement + button.name = 'button' + button.type = 'button' + button.className = 'btn-geocode' + button.appendChild(document.createTextNode(geocoder.address_field_name)) + p_element.appendChild(button) + }) + } + }) + + document.querySelectorAll('button.btn-geocode').forEach(element => { + element.addEventListener('click', (evt) => { + // Geocode address and add/update icon on map + const button = evt.currentTarget as HTMLButtonElement + if (button.previousElementSibling.querySelector('input').value != '') { + const address = button.previousElementSibling.querySelector('input').value + const geocode_url = geocoder.geocode_url.replace("{address}", encodeURIComponent(address)) + fetch(geocode_url) + .then(response => response.json()) + .then(data => { + const check = evaluateComparison(getObjectPathValue(data, geocoder.geocode_result_check_path), + geocoder.geocode_result_check_operator, + geocoder.geocode_result_check_value + ) + if (check) { + const lon = getObjectPathValue(data, geocoder.geocode_result_lon_path) + const lat = getObjectPathValue(data, geocoder.geocode_result_lat_path) + const coords = [lon, lat] + const geom = new Point(fromLonLat(coords, 'EPSG:3857')) + const features = this.vector.getSource().getFeatures() + if (features.length > 0) { + features[features.length - 1].setGeometry(geom) + } else { + const feature = new Feature(geom) + this.vector.getSource().addFeatures([feature]) + } + this.updateForm(this.vector.getSource().getFeatures()) + this.zoomToExtent(true) + + const _districtInput = document.querySelectorAll(`#issue-form #attributes label`) + let districtInput: HTMLInputElement = null + _districtInput.forEach(element => { + if (element.innerHTML.includes(geocoder.district_field_name)) { + districtInput = element.parentNode.querySelector('p').querySelector('input') + } + }) + let foundDistrict = false + if (districtInput) { + const district = getObjectPathValue(data, geocoder.geocode_result_district_path) + if (district) { + const regexp = new RegExp(geocoder.geocode_result_district_regexp) + const match = regexp.exec(district) + if (match && match.length === 2) { + districtInput.value = match[1] + foundDistrict = true + } + } + if (!foundDistrict) { + if (districtInput) { + districtInput.value = "" + } + } + } + } + }) + } + }) + }) + } + + if (geocoder.place_search_url && + geocoder.place_search_field_name && + document.querySelectorAll("#issue-form #attributes button.btn-placesearch").length == 0 ) + { + document.querySelectorAll(`#issue-form #attributes label`).forEach(element => { + if (element.innerHTML.includes(geocoder.place_search_field_name)) { + element.querySelectorAll('p').forEach(p_element => { + const button = document.createElement('button') as HTMLButtonElement + button.name = 'button' + button.type = 'button' + button.className = 'btn-placesearch' + button.appendChild(document.createTextNode(geocoder.place_search_field_name)) + p_element.appendChild(button) + }) + } + }) + + document.querySelectorAll("button.btn-placesearch").forEach(element => { + element.addEventListener('click', () => { + if (this.vector.getSource().getFeatures().length > 0) { + let coords = null + this.vector.getSource().getFeatures().forEach((feature: any) => { + // Todo: only works with point geometries for now for the last geometry + coords = getCenter(feature.getGeometry().getExtent()) + }) + coords = transform(coords, 'EPSG:3857', 'EPSG:4326') + const place_search_url = geocoder.place_search_url.replace("{lon}", coords[0].toString()).replace("{lat}", coords[1].toString()) + fetch(place_search_url) + .then(response => response.json()) + .then(data => { + const list:Array = getObjectPathValue(data, geocoder.place_search_result_list_path) + if (list.length > 0) { + const modal = document.querySelector('#ajax-modal') as HTMLDivElement + modal.innerHTML = ` +

    ${geocoder.place_search_result_ui_title}

    +
    +

    + +

    + ` + modal.classList.add('place_search_results') + list.forEach(item => { + const display = getObjectPathValue(item, geocoder.place_search_result_display_path) + const value = getObjectPathValue(item, geocoder.place_search_result_value_path) + if (display && value) { + const places = document.querySelector('div#places') as HTMLDivElement + const input = document.createElement('input') as HTMLInputElement + input.type = 'radio' + input.name = 'places' + input.value = value + input.appendChild(document.createTextNode(display)) + places.appendChild(input) + places.appendChild(document.createElement('br')) + } + }) + window.showModal('ajax-model', '400px') + document.querySelector("p.buttons input[type='submit']").addEventListener('click', () => { + let input: HTMLInputElement = null + document.querySelectorAll(`#issue-form #attributes label`).forEach(element => { + if (element.innerHTML.includes(geocoder.place_search_field_name)) { + input = element.parentNode.querySelector('p').querySelector('input') as HTMLInputElement + } + }) + if (input) { + input.value = (document.querySelector("div#places input[type='radio']:checked") as HTMLInputElement).value + } + }) + } else { + let input: HTMLInputElement = null + document.querySelectorAll(`#issue-form #attributes label`).forEach(element => { + if (element.innerHTML.includes(geocoder.place_search_field_name)) { + input = element.parentNode.querySelector('p').querySelector('input') as HTMLInputElement + } + }) + if (input) { + input.value = geocoder.empty_field_value + } + } + }) + } + }) + }) + } + + // disable geocoding control if plugin setting is not true + if (this.contents.geocoding !== "true") { + return + } + + const mapId = currentMap.getTargetElement().getAttribute("id") + + // Control button + const geocodingCtrl = new Toggle({ + html: 'manage_search', + title: this.i18n.control.geocoding, + className: "ctl-geocoding", + onToggle: (active: boolean) => { + const text = (document.querySelector("div#" + mapId + " .ctl-geocoding div input") as HTMLInputElement) + if (active) { + text.focus() + } else { + text.blur() + const button = document.querySelector("div#" + mapId + " .ctl-geocoding button") + button.blur() + } + }, + bar: new Bar({ + controls: [ + new TextButton({ + html: '
    ' + }) + ] + }) + }) + this.toolbar.addControl(geocodingCtrl) + + // Make Geocoding API request + document.querySelector("div#" + mapId + " .ctl-geocoding div input").addEventListener('keydown', (evt) => { + if (evt.keyCode === 13) { + evt.preventDefault() + evt.stopPropagation() + } else { + return true + } + + if (!geocoder.geocode_url) { + throw new Error ("No Geocoding service configured!") + } + + const url = geocoder.geocode_url.replace("{address}", encodeURIComponent( + (document.querySelector("div#" + mapId + " .ctl-geocoding form input[name=address]") as HTMLInputElement).value) + ) + + fetch(url) + .then(response => response.json()) + .then(data => { + const check = evaluateComparison(getObjectPathValue(data, geocoder.geocode_result_check_path), + geocoder.geocode_result_check_operator, + geocoder.geocode_result_check_value + ) + if (check) { + const lon = getObjectPathValue(data, geocoder.geocode_result_lon_path) + const lat = getObjectPathValue(data, geocoder.geocode_result_lat_path) + const coords = [lon, lat] + const geom = new Point(fromLonLat(coords, 'EPSG:3857')) + currentMap.getView().fit(geom.getExtent(), { + size: currentMap.getSize(), + maxZoom: parseInt(this.defaults.fitMaxzoom) + }) + } + }) + + return false + }) +} diff --git a/src/components/gtt-client/gtt-client-class.ts b/src/components/gtt-client/gtt-client-class.ts new file mode 100644 index 00000000..bdaf4be6 --- /dev/null +++ b/src/components/gtt-client/gtt-client-class.ts @@ -0,0 +1,465 @@ +/// OpenLayers core imports +import 'ol/ol.css'; +import { Map, Feature, Geolocation } from 'ol'; +import { Geometry } from 'ol/geom'; +import { GeoJSON, MVT } from 'ol/format'; +import { Layer, Tile, Image, VectorTile as VTLayer } from 'ol/layer'; +import VectorLayer from 'ol/layer/Vector'; +import { Style, Fill, Stroke } from 'ol/style'; +import { OrderFunction } from 'ol/render'; +import { + defaults as interactions_defaults, + MouseWheelZoom, +} from 'ol/interaction'; +import { focus as events_condifition_focus } from 'ol/events/condition'; +import { defaults as control_defaults, FullScreen, Rotate } from 'ol/control'; +import Vector from 'ol/source/Vector'; +import VectorSource from 'ol/source/Vector'; +import TileSource from 'ol/source/Tile'; +import ImageSource from 'ol/source/Image'; +import { Options as ImageWMSOptions } from 'ol/source/ImageWMS'; +import { Options as VectorTileOptions } from 'ol/source/VectorTile'; +import { applyStyle } from 'ol-mapbox-style'; + +// OpenLayers extension imports +import 'ol-ext/dist/ol-ext.min.css'; +import 'ol-ext/filter/Base'; +import Ordering from 'ol-ext/render/Ordering'; +import Mask from 'ol-ext/filter/Mask'; +import Bar from 'ol-ext/control/Bar'; +import Button from 'ol-ext/control/Button'; +import LayerPopup from 'ol-ext/control/LayerPopup'; +import LayerSwitcher from 'ol-ext/control/LayerSwitcher'; +import { position } from 'ol-ext/control/control'; + +// Other imports +import { ResizeObserver } from '@juggle/resize-observer'; + +// Import types +import { + GttClientOption, + LayerObject, + FilterOption, + TileLayerSource, + ImageLayerSource, + VTLayerSource, +} from './interfaces'; + +import { constants as quick_hack } from './constants'; +import { radiansToDegrees, degreesToRadians, reloadFontSymbol, updateForm, updateFilter, parseHistory } from "./helpers"; +import { getLayerSource, setBasemap, zoomToExtent, toggleAndLoadMap, setGeolocation, setView, getStyle, setControls, setPopover } from "./openlayers"; +import { setGeocoding } from "./geocoding"; + +export default class GttClient { + readonly map: Map + maps: Array + layerArray: Layer[] + defaults: DOMStringMap + contents: DOMStringMap + i18n: any + toolbar: Bar + filters: FilterOption + vector: VectorLayer> + bounds: VectorLayer> + geolocations: Array + + constructor(options: GttClientOption) { + this.filters = { + location: false, + distance: false + } + this.maps = [] + this.geolocations = [] + + // needs target + if (!options.target) { + return + } + + const gtt_defaults = document.querySelector('#gtt-defaults') as HTMLDivElement + if (!gtt_defaults) { + return + } + + this.defaults = gtt_defaults.dataset + + if (this.defaults.lon === null || this.defaults.lon === undefined) { + this.defaults.lon = quick_hack.lon.toString() + } + if (this.defaults.lat === null || this.defaults.lat === undefined) { + this.defaults.lat = quick_hack.lat.toString() + } + if (this.defaults.zoom === null || this.defaults.zoom === undefined) { + this.defaults.zoom = quick_hack.zoom.toString() + } + if (this.defaults.maxzoom === null || this.defaults.maxzoom === undefined) { + this.defaults.maxzoom = quick_hack.maxzoom.toString() + } + if (this.defaults.fitMaxzoom === null || this.defaults.fitMaxzoom === undefined) { + this.defaults.fitMaxzoom = quick_hack.fitMaxzoom.toString() + } + if (this.defaults.geocoder === null || this.defaults.geocoder === undefined) { + this.defaults.geocoder = JSON.stringify(quick_hack.geocoder) + } + + this.contents = options.target.dataset + this.i18n = JSON.parse(this.defaults.i18n) + + // create map at first + this.map = new Map({ + target: options.target, + //layers: this.layerArray, + interactions: interactions_defaults({mouseWheelZoom: false}).extend([ + new MouseWheelZoom({ + constrainResolution: true, // force zooming to a integer zoom + condition: events_condifition_focus // only wheel/trackpad zoom when the map has the focus + }) + ]), + controls: control_defaults({ + rotateOptions: {}, + attributionOptions: { + collapsible: false + }, + zoomOptions: { + zoomInTipLabel: this.i18n.control.zoom_in, + zoomOutTipLabel: this.i18n.control.zoom_out + } + }) + }) + + let features: Feature[] | null = null + if (this.contents.geom && this.contents.geom !== null && this.contents.geom !== 'null') { + features = new GeoJSON().readFeatures( + JSON.parse(this.contents.geom), { + featureProjection: 'EPSG:3857' + } + ) + } + + // Fix FireFox unloaded font issue + reloadFontSymbol.call(this) + + // TODO: this is only necessary because setting the initial form value + // through the template causes encoding problems + updateForm(this, features) + this.layerArray = [] + + if (this.contents.layers) { + const layers = JSON.parse(this.contents.layers) as [LayerObject] + layers.forEach((layer) => { + const s = layer.type.split('.') + const layerSource = getLayerSource(s[1], s[2]) + if ( layerSource.type === "TileLayerSource") { + const config = layerSource as TileLayerSource + const l = new (config.layer)({ + visible: false, + source: new (config.source)(layer.options as any) + }) + + l.set('lid', layer.id) + l.set('title', layer.name) + l.set('baseLayer', layer.baselayer) + if( layer.baselayer ) { + l.on('change:visible', e => { + const target = e.target as Tile + if (target.getVisible()) { + const lid = target.get('lid') + document.cookie = `_redmine_gtt_basemap=${lid};path=/` + } + }) + } + this.layerArray.push(l) + } else if (layerSource.type === "ImageLayerSource") { + const config = layerSource as ImageLayerSource + const l = new (config.layer)({ + visible: false, + source: new (config.source)(layer.options as ImageWMSOptions) + }) + + l.set('lid', layer.id) + l.set('title', layer.name) + l.set('baseLayer', layer.baselayer) + if( layer.baselayer ) { + l.on('change:visible', e => { + const target = e.target as Image + if (target.getVisible()) { + const lid = target.get('lid') + document.cookie = `_redmine_gtt_basemap=${lid};path=/` + } + }) + } + this.layerArray.push(l) + } else if (layerSource.type === "VTLayerSource") { + const config = layerSource as VTLayerSource + const options = layer.options as VectorTileOptions + options.format = new MVT() + const l = new (config.layer)({ + visible: false, + source: new (config.source)(options), + declutter: true + }) as VTLayer + + // Apply style URL if provided + if ("styleUrl" in options) { + applyStyle(l,options.styleUrl) + } + + l.set('lid', layer.id) + l.set('title', layer.name) + l.set('baseLayer', layer.baselayer) + if( layer.baselayer ) { + l.on('change:visible', (e: { target: any }) => { + const target = e.target as any + if (target.getVisible()) { + const lid = target.get('lid') + document.cookie = `_redmine_gtt_basemap=${lid};path=/` + } + }) + } + this.layerArray.push(l) + } + }, this) + + /** + * Ordering the Layers for the LayerSwitcher Control. + * BaseLayers are added first. + */ + this.layerArray.forEach( (l:Layer) => { + if( l.get("baseLayer") ) { + this.map.addLayer(l) + } + } + ) + + var containsOverlay = false; + + this.layerArray.forEach( (l:Layer) => { + if( !l.get("baseLayer") ) { + this.map.addLayer(l) + containsOverlay = true + } + } + ) + } + + setBasemap.call(this) + + // Layer for project boundary + this.bounds = new VectorLayer({ + source: new Vector(), + style: new Style({ + fill: new Fill({ + color: 'rgba(255,255,255,0.0)' + }), + stroke: new Stroke({ + color: 'rgba(220,26,26,0.7)', + // lineDash: [12,1,12], + width: 1 + }) + }) + }) + this.bounds.set('title', 'Boundaries') + this.bounds.set('displayInLayerSwitcher', false) + this.layerArray.push(this.bounds) + this.map.addLayer(this.bounds) + + const yOrdering: unknown = Ordering.yOrdering() + + this.vector = new VectorLayer>({ + source: new Vector({ + 'features': features, + 'useSpatialIndex': false + }), + renderOrder: yOrdering as OrderFunction, + style: getStyle.bind(this) + }) + this.vector.set('title', 'Features') + this.vector.set('displayInLayerSwitcher', false) + this.layerArray.push(this.vector) + this.map.addLayer(this.vector) + + // Render project boundary if bounds are available + if (this.contents.bounds && this.contents.bounds !== null) { + const boundary = new GeoJSON().readFeature( + this.contents.bounds, { + featureProjection: 'EPSG:3857' + } + ) + this.bounds.getSource().addFeature(boundary) + if (this.contents.bounds === this.contents.geom) { + this.vector.setVisible(false) + } + this.layerArray.forEach((layer:Layer) => { + if (layer.get('baseLayer')) { + layer.addFilter(new Mask({ + feature: boundary, + inner: false, + fill: new Fill({ + color: [220,26,26,.1] + }) + })) + } + }) + } + + // For map div focus settings + if (options.target) { + if (options.target.getAttribute('tabindex') == null) { + options.target.setAttribute('tabindex', '0') + } + } + + // Fix empty map issue + this.map.once('postrender', e => { + zoomToExtent.call(this, true) + }) + + // Add Toolbar + this.toolbar = new Bar() + this.toolbar.setPosition('bottom-left' as position) + this.map.addControl(this.toolbar) + setView.call(this) + setGeocoding.call(this, this.map) + setGeolocation.call(this, this.map) + parseHistory.call(this) + + this.map.addControl (new FullScreen({ + tipLabel: this.i18n.control.fullscreen + })) + this.map.addControl (new Rotate({ + tipLabel: this.i18n.control.rotate + })) + + // Control button + const maximizeCtrl = new Button({ + html: 'zoom_out_map', + title: this.i18n.control.maximize, + handleClick: () => { + zoomToExtent.call(this, true); + } + }) + this.toolbar.addControl(maximizeCtrl) + + // Map rotation + const rotation_field = document.querySelector('#gtt_configuration_map_rotation') as HTMLInputElement + if (rotation_field !== null) { + this.map.getView().on('change:rotation', (evt) => { + rotation_field.value = String(Math.round(radiansToDegrees(evt.target.getRotation()))) + }) + + rotation_field.addEventListener("input", (evt) => { + const { target } = evt; + if (!(target instanceof HTMLInputElement)) { + return; + } + const value = target.value; + this.map.getView().setRotation(degreesToRadians(parseInt(value))) + }) + } + + if (this.contents.edit) { + setControls.call(this, this.contents.edit.split(' ')) + } else if (this.contents.popup) { + setPopover.call(this) + } + + // Zoom to extent when map collapsed => expended + if (this.contents.collapsed) { + const self = this + const collapsedObserver = new MutationObserver((mutations) => { + // const currentMap = this.map + mutations.forEach(function(mutation) { + if (mutation.attributeName !== 'style') { + return + } + const mapDiv = mutation.target as HTMLDivElement + if (mapDiv && mapDiv.style.display === 'block') { + zoomToExtent.call(this, true) + collapsedObserver.disconnect() + } + }) + }) + collapsedObserver.observe(self.map.getTargetElement(), { attributes: true, attributeFilter: ['style'] }) + } + + // Sidebar hack + const resizeObserver = new ResizeObserver((entries, observer) => { + this.maps.forEach(m => { + m.updateSize() + }) + }) + resizeObserver.observe(this.map.getTargetElement()) + + // When one or more issues is selected, zoom to selected map features + document.querySelectorAll('table.issues tbody tr').forEach((element: HTMLTableRowElement) => { + element.addEventListener('click', (evt) => { + const currentTarget = evt.currentTarget as HTMLTableRowElement + const id = currentTarget.id.split('-')[1] + const feature = this.vector.getSource().getFeatureById(id) + this.map.getView().fit(feature.getGeometry().getExtent(), { + size: this.map.getSize() + }) + }) + }) + + // Need to update size of an invisible map, when the editable form is made + // visible. This doesn't look like a good way to do it, but this is more of + // a Redmine problem + document.querySelectorAll('div.contextual a.icon-edit').forEach((element: HTMLAnchorElement) => { + element.addEventListener('click', () => { + setTimeout(() => { + this.maps.forEach(m => { + m.updateSize() + }) + zoomToExtent.call(this) + }, 200) + }) + }) + + // Redraw the map, when a GTT Tab gets activated + document.querySelectorAll('#tab-gtt').forEach((element) => { + element.addEventListener('click', () => { + this.maps.forEach(m => { + m.updateSize() + }) + zoomToExtent.call(this) + }) + }) + + // Add LayerSwitcher Image Toolbar + if( containsOverlay) { + this.map.addControl(new LayerSwitcher({ + reordering: false + })) + } + else { + this.map.addControl(new LayerPopup()) + } + + + // Because Redmine filter functions are applied later, the Window onload + // event provides a workaround to have filters loaded before executing + // the following code + window.addEventListener('load', () => { + if (document.querySelectorAll('tr#tr_bbox').length > 0) { + this.filters.location = true + } + if (document.querySelectorAll('tr#tr_distance').length > 0) { + this.filters.distance = true + } + const legend = document.querySelector('fieldset#location legend') as HTMLLegendElement + if (legend) { + legend.addEventListener('click', (evt) => { + const element = evt.currentTarget as HTMLLegendElement + toggleAndLoadMap(element) + }) + } + zoomToExtent.call(this) + this.map.on('moveend', updateFilter.bind(this)) + }) + + // Handle multiple maps per page + this.maps.push(this.map) + } + +} diff --git a/src/components/gtt-client/helpers.ts b/src/components/gtt-client/helpers.ts new file mode 100644 index 00000000..70328730 --- /dev/null +++ b/src/components/gtt-client/helpers.ts @@ -0,0 +1,262 @@ +import { Map, Feature } from 'ol'; +import { Geometry, Point } from 'ol/geom'; +import { GeoJSON, WKT } from 'ol/format'; +import { FeatureCollection } from 'geojson'; +import VectorLayer from 'ol/layer/Vector'; +import { FeatureLike } from 'ol/Feature'; +import FontSymbol from 'ol-ext/style/FontSymbol'; +import { transform, transformExtent } from 'ol/proj'; + +export const getCookie = (cname: string): string => { + const name = cname + '='; + const decodedCookie = decodeURIComponent(document.cookie); + const ca = decodedCookie.split(';'); + + for (let i = 0; i < ca.length; i++) { + let c = ca[i]; + while (c.charAt(0) === ' ') { + c = c.substring(1); + } + if (c.indexOf(name) === 0) { + return c.substring(name.length, c.length); + } + } + return ''; +}; + +export const radiansToDegrees = (radians: number): number => { + const degrees = radians * (180 / Math.PI); + return (degrees + 360) % 360; +}; + +export const degreesToRadians = (degrees: number): number => degrees * (Math.PI / 180); + +export const getMapSize = (map: Map): number[] => { + const [width, height] = map.getSize(); + + if (width <= 0 || height <= 0) { + const target = map.getTarget() as HTMLElement; + return [target.clientWidth, target.clientHeight]; + } + + return [width, height]; +}; + +export const evaluateComparison = (left: any, operator: any, right: any): any => { + if (typeof left == 'object') { + left = JSON.stringify(left); + return Function('"use strict";return (JSON.parse(\'' + left + '\')' + operator + right + ')')(); + } else { + return Function('"use strict";return (' + left + operator + right + ')')(); + } +}; + +export const getObjectPathValue = (obj: any, path: string | Array, def: any = null) => { + const pathArr = Array.isArray(path) + ? path + : path.split('.').flatMap((key) => key.split(/\[([^}]+)\]/g).filter(Boolean)); + return pathArr.reduce((acc, key) => acc?.[key], obj) ?? def; +}; + +export function reloadFontSymbol() { + if ('fonts' in document) { + const symbolFonts: Array = [] + for (const font in FontSymbol.defs.fonts) { + symbolFonts.push(font) + } + if (symbolFonts.length > 0) { + (document as any).fonts.addEventListener('loadingdone', (e: any) => { + const fontIndex = e.fontfaces.findIndex((font: any) => { + return symbolFonts.indexOf(font.family) >= 0 + }) + if (fontIndex >= 0) { + this.maps.forEach((m: any) => { + const layers = m.getLayers() + layers.forEach((layer: any) => { + if (layer instanceof VectorLayer && + layer.getKeys().indexOf("title") >= 0 && + layer.get("title") === "Features") { + const features = layer.getSource().getFeatures() + const pointIndex = features.findIndex((feature: Feature) => { + return feature.getGeometry().getType() === "Point" + }) + if (pointIndex >= 0) { + // console.log("Reloading Features layer") + layer.changed() + } + } + }) + }) + } + }) + } + } +} + +export function updateForm(mapObj: any, features: FeatureLike[] | null, updateAddressFlag: boolean = false):void { + if (features == null) { + return + } + const geom = document.querySelector('#geom') as HTMLInputElement + if (!geom) { + return + } + + const writer = new GeoJSON() + // Convert to Feature type for GeoJSON writer + const new_features: Feature[] = features.map((feature => { + return new Feature({geometry: feature.getGeometry() as Geometry}) + })) + const geojson_str = writer.writeFeatures(new_features, { + featureProjection: 'EPSG:3857', + dataProjection: 'EPSG:4326' + }) + const geojson = JSON.parse(geojson_str) as FeatureCollection + geom.value = JSON.stringify(geojson.features[0]) + + const geocoder = JSON.parse(mapObj.defaults.geocoder) + if (updateAddressFlag && geocoder.address_field_name && features && features.length > 0) { + let addressInput: HTMLInputElement = null + document.querySelectorAll(`#issue-form #attributes label`).forEach(element => { + if (element.innerHTML.includes(geocoder.address_field_name)) { + addressInput = element.parentNode.querySelector('p').querySelector('input') as HTMLInputElement + } + }) + if (addressInput) { + // Todo: only works with point geometries for now for the last geometry + const geom = features[features.length - 1].getGeometry() as Point + if (geom === null) { + return + } + let coords = geom.getCoordinates() + coords = transform(coords, 'EPSG:3857', 'EPSG:4326') + const reverse_geocode_url = geocoder.reverse_geocode_url.replace("{lon}", coords[0].toString()).replace("{lat}", coords[1].toString()) + fetch(reverse_geocode_url) + .then(response => response.json()) + .then(data => { + const check = evaluateComparison(getObjectPathValue(data, geocoder.reverse_geocode_result_check_path), + geocoder.reverse_geocode_result_check_operator, + geocoder.reverse_geocode_result_check_value) + let districtInput: HTMLInputElement = null + document.querySelectorAll(`#issue-form #attributes label`).forEach(element => { + if (element.innerHTML.includes(geocoder.district_field_name)) { + districtInput = element.parentNode.querySelector('p').querySelector('input') as HTMLInputElement + } + }) + const address = getObjectPathValue(data, geocoder.reverse_geocode_result_address_path) + let foundDistrict = false + if (check && address) { + addressInput.value = address + if (districtInput) { + const district = getObjectPathValue(data, geocoder.reverse_geocode_result_district_path) + if (district) { + const regexp = new RegExp(geocoder.reverse_geocode_result_district_regexp) + const match = regexp.exec(district) + if (match && match.length === 2) { + districtInput.value = match[1] + foundDistrict = true + } + } + } + } else { + addressInput.value = geocoder.empty_field_value + } + if (!foundDistrict) { + if (districtInput) { + districtInput.value = '' + } + } + }) + } + } + +} + +/** + * Updates map settings for Redmine filter + */ +export function updateFilter() { + let center = this.map.getView().getCenter() + let extent = this.map.getView().calculateExtent(this.map.getSize()) + + center = transform(center,'EPSG:3857','EPSG:4326') + // console.log("Map Center (WGS84): ", center); + const fieldset = document.querySelector('fieldset#location') as HTMLFieldSetElement + if (fieldset) { + fieldset.dataset.center = JSON.stringify(center) + } + const value_distance_3 = document.querySelector('#tr_distance #values_distance_3') as HTMLInputElement + if (value_distance_3) { + value_distance_3.value = center[0].toString() + } + const value_distance_4 = document.querySelector('#tr_distance #values_distance_4') as HTMLInputElement + if (value_distance_4) { + value_distance_4.value = center[1].toString() + } + + // Set Permalink as Cookie + const cookie = [] + const hash = this.map.getView().getZoom() + '/' + + Math.round(center[0] * 1000000) / 1000000 + '/' + + Math.round(center[1] * 1000000) / 1000000 + '/' + + this.map.getView().getRotation() + cookie.push("_redmine_gtt_permalink=" + hash) + cookie.push("path=" + window.location.pathname) + document.cookie = cookie.join(";") + + const extent_str = transformExtent(extent,'EPSG:3857','EPSG:4326').join('|') + // console.log("Map Extent (WGS84): ",extent); + const bbox = document.querySelector('select[name="v[bbox][]"]') + if (bbox) { + const option = bbox.querySelector('option') as HTMLOptionElement + option.value = extent_str + } + // adjust the value of the 'On map' option tag + // Also adjust the JSON data that's the basis for building the filter row + // html (this is relevant if the map is moved first and then the filter is + // added.) + if(window.availableFilters && window.availableFilters.bbox) { + window.availableFilters.bbox.values = [['On map', extent]] + } +} + +/** + * Parse page for WKT strings in history + */ +export function parseHistory() { + const historyItems = document.querySelectorAll('div#history ul.details i'); + + const regex = /\b(?:POINT|LINESTRING|POLYGON)\b\s?\({1,}[-]?\d+([,. ]\s?[-]?\d+)*\){1,}/gi; + const dataProjection = 'EPSG:4326'; + const featureProjection = 'EPSG:3857'; + + const parseAndFormatWKT = (wkt: string) => { + const feature = new WKT().readFeature(wkt, { dataProjection, featureProjection }); + let formattedWKT = new WKT().writeFeature(feature, { dataProjection, featureProjection, decimals: 5 }); + + if (formattedWKT.length > 30) { + const parts = formattedWKT.split(' '); + formattedWKT = `${parts[0]}...${parts[parts.length - 1]}`; + } + + return formattedWKT; + }; + + historyItems.forEach((item: Element) => { + const match = item.innerHTML.match(regex); + + if (match !== null) { + const wkt = parseAndFormatWKT(match.join('')); + + const link = document.createElement('a'); + link.href = '#'; + link.classList.add('wkt'); + link.dataset.feature = match.join(''); + link.textContent = wkt; + + // Replace current node with new link. + item.replaceWith(link); + } + }); +} + diff --git a/src/components/gtt-client/index.ts b/src/components/gtt-client/index.ts new file mode 100644 index 00000000..3c4ed860 --- /dev/null +++ b/src/components/gtt-client/index.ts @@ -0,0 +1,4 @@ +export * from './redmine'; + +import GttClient from './gtt-client-class'; +export { GttClient }; diff --git a/src/@types/types.ts b/src/components/gtt-client/interfaces.ts similarity index 100% rename from src/@types/types.ts rename to src/components/gtt-client/interfaces.ts diff --git a/src/components/gtt-client/openlayers.ts b/src/components/gtt-client/openlayers.ts new file mode 100644 index 00000000..c723656a --- /dev/null +++ b/src/components/gtt-client/openlayers.ts @@ -0,0 +1,505 @@ +import { Map, Feature, View, Geolocation } from 'ol'; +import { Geometry, Point } from 'ol/geom'; +import { Image as ImageLayer, Tile as TileLayer, VectorTile as VTLayer } from 'ol/layer'; +import { OSM, XYZ, TileWMS, ImageWMS, VectorTile as VTSource } from 'ol/source'; +import Vector from 'ol/source/Vector' +import VectorLayer from 'ol/layer/Vector'; +import { Style, Fill, Stroke, Circle } from 'ol/style'; +import { createEmpty, extend, containsCoordinate } from 'ol/extent'; +import { transform, fromLonLat } from 'ol/proj'; +import Shadow from 'ol-ext/style/Shadow'; +import FontSymbol from 'ol-ext/style/FontSymbol'; + +import { Modify, Draw, Select } from 'ol/interaction' +import Bar from 'ol-ext/control/Bar'; +import Button from 'ol-ext/control/Button'; +import Toggle from 'ol-ext/control/Toggle'; +import Popup from 'ol-ext/overlay/Popup'; +import { position } from 'ol-ext/control/control'; +import { GeoJSON } from 'ol/format'; + +import { TileLayerSource, ImageLayerSource, VTLayerSource } from './interfaces'; +import { getCookie, getMapSize, degreesToRadians, updateForm } from "./helpers"; + +/** + * Add editing tools + */ +export function setControls(types: Array) { + // Make vector features editable + const modify = new Modify({ + features: this.vector.getSource().getFeaturesCollection() + }) + + modify.on('modifyend', evt => { + updateForm(this, evt.features.getArray(), true) + }) + + this.map.addInteraction(modify) + + const mainbar = new Bar() + mainbar.setPosition("top-left" as position) + this.map.addControl(mainbar) + + const editbar = new Bar({ + toggleOne: true, // one control active at the same time + group: true // group controls together + }) + mainbar.addControl(editbar) + + types.forEach((type: any, idx) => { + const draw = new Draw({ + type: type, + source: this.vector.getSource() + }) + + draw.on('drawend', evt => { + this.vector.getSource().clear() + updateForm(this, [evt.feature], true) + }) + + // Material design icon + let mdi = 'place' + + switch (type.toLowerCase()) { + case 'linestring': + mdi = 'polyline' + break; + + case 'polygon': + mdi = 'format_shapes' + break; + } + + const control = new Toggle({ + html: `${mdi}`, + title: this.i18n.control[type.toLowerCase()], + interaction: draw, + active: (idx === 0) + }) + editbar.addControl(control) + }) + + // Uses jQuery UI for GeoJSON Upload modal window + const mapObj = this + const dialog = $("#dialog-geojson-upload").dialog({ + autoOpen: false, + resizable: true, + height: 'auto', + width: 380, + modal: true, + buttons: { + [mapObj.i18n.modal.load]: function() { + const geojson_input = document.querySelector('#dialog-geojson-upload textarea') as HTMLInputElement + const data = geojson_input.value + if (data !== null) { + const features = new GeoJSON().readFeatures( + JSON.parse(data), { + featureProjection: 'EPSG:3857' + } + ) + mapObj.vector.getSource().clear() + mapObj.vector.getSource().addFeatures(features) + updateForm(mapObj, features) + zoomToExtent.call(mapObj) + } + $(this).dialog('close') + }, + [mapObj.i18n.modal.cancel]: function() { + $(this).dialog('close') + } + } + }); + + // Upload button + if (this.contents.upload === "true") { + + const fileSelector = document.getElementById('file-selector') + fileSelector.addEventListener('change', (event: any) => { + const file = event.target.files[0] + // Check if the file is GeoJSON. + if (file.type && !file.type.startsWith('application/geo')) { + console.log('File is not a GeoJSON document.', file.type, file); + return; + } + const fileReader = new FileReader(); + fileReader.addEventListener('load', (event: any) => { + const geojson_input = document.querySelector('#dialog-geojson-upload textarea') as HTMLInputElement + geojson_input.value = JSON.stringify(event.target.result, null, 2) + }); + fileReader.readAsText(file); + }); + + editbar.addControl(new Button({ + html: 'file_upload', + title: this.i18n.control.upload, + handleClick: () => { + dialog.dialog('open') + } + })) + } +} + +/** + * Add popup + */ +export function setPopover() { + const popup = new Popup({ + popupClass: 'default', + closeBox: false, + onclose: () => {}, + positioning: 'auto', + anim: true + }) + this.map.addOverlay(popup) + + // Control Select + const select = new Select({ + layers: [this.vector], + style: null, + multi: false + }) + this.map.addInteraction(select) + + // On selected => show/hide popup + select.getFeatures().on(['add'], (evt: any) => { + const feature = evt.element + + const content: Array = [] + content.push(`${feature.get('subject')}
    `) + // content.push('Starts at: ' + feature.get("start_date") + ' |'); + + const popup_contents = JSON.parse(this.contents.popup) + const url = popup_contents.href.replace(/\[(.+?)\]/g, feature.get('id')) + content.push(`Edit`) + + popup.show(feature.getGeometry().getFirstCoordinate(), content.join('') as any) + }) + + select.getFeatures().on(['remove'], _ => { + popup.hide() + }) + + // change mouse cursor when over marker + this.map.on('pointermove', (evt: any) => { + if (evt.dragging) return + const hit = this.map.hasFeatureAtPixel(evt.pixel, { + layerFilter: (layer: any) => { + return layer === this.vector + } + }) + this.map.getTargetElement().style.cursor = hit ? 'pointer' : '' + }) +} + +export const getLayerSource = ( + source: string, + class_name: string, +): TileLayerSource | ImageLayerSource | VTLayerSource | undefined => { + if (source === 'source') { + if (class_name === 'OSM') { + return { layer: TileLayer, source: OSM, type: 'TileLayerSource' }; + } else if (class_name === 'XYZ') { + return { layer: TileLayer, source: XYZ, type: 'TileLayerSource' }; + } else if (class_name === 'TileWMS') { + return { layer: TileLayer, source: TileWMS, type: 'TileLayerSource' }; + } else if (class_name === 'ImageWMS') { + return { layer: ImageLayer, source: ImageWMS, type: 'ImageLayerSource' }; + } else if (class_name === 'VectorTile') { + return { layer: VTLayer, source: VTSource, type: 'VTLayerSource' }; + } + } + return undefined; +}; + +/** +* Decide which baselayer to show +*/ +export function setBasemap(): void { + if (this.layerArray.length == 0) { + console.error("There is no baselayer available!") + return + } + + let index = 0 + const cookie = parseInt(getCookie('_redmine_gtt_basemap')) + if (cookie) { + let lid = 0 + // Check if layer ID exists in available layers + this.layerArray.forEach((layer: any) => { + if (cookie === layer.get("lid")) { + lid = cookie + } + }) + + // Set selected layer visible + this.layerArray.forEach((layer: any, idx: number) => { + if (lid === layer.get("lid")) { + index = idx + } + }) + } + + // Set layer visible + this.layerArray[index].setVisible(true) +} + +export function zoomToExtent(force: boolean = true) { + if (!force && (this.filters.distance || this.filters.location)) { + // Do not zoom to extent but show the previous extent stored as cookie + const parts = (getCookie("_redmine_gtt_permalink")).split("/"); + this.maps.forEach((m: any) => { + m.getView().setZoom(parseInt(parts[0], 10)) + m.getView().setCenter(transform([ + parseFloat(parts[1]), + parseFloat(parts[2]) + ],'EPSG:4326','EPSG:3857')) + m.getView().setRotation(parseFloat(parts[3])) + }) + } else if (this.vector.getSource().getFeatures().length > 0) { + let extent = createEmpty() + // Because the vector layer is set to "useSpatialIndex": false, we cannot + // make use of "vector.getSource().getExtent()" + this.vector.getSource().getFeatures().forEach((feature: any) => { + extend(extent, feature.getGeometry().getExtent()) + }) + this.maps.forEach((m: any) => { + m.getView().fit(extent, { + size: getMapSize(m), + maxZoom: parseInt(this.defaults.fitMaxzoom) + }) + }) + } else if (this.bounds.getSource().getFeatures().length > 0) { + this.maps.forEach((m: any) => { + m.getView().fit(this.bounds.getSource().getExtent(), { + size: getMapSize(m), + maxZoom: parseInt(this.defaults.fitMaxzoom) + }) + }) + } else { + // Set default center, once + this.maps.forEach((m: any) => { + m.getView().setCenter(transform([parseFloat(this.defaults.lon), parseFloat(this.defaults.lat)], + 'EPSG:4326', 'EPSG:3857')); + }) + this.geolocations.forEach((g: any) => { + g.once('change:position', (evt: any) => { + this.maps.forEach((m: any) => { + m.getView().setCenter(g.getPosition()) + }) + }) + }) + } +} + +/** + * Add Geolocation functionality + */ +export function setGeolocation(currentMap: Map) { + const geolocation = new Geolocation({ + tracking: false, + projection: currentMap.getView().getProjection() + }) + this.geolocations.push(geolocation) + + geolocation.on('change', (evt) => { + // console.log({ + // accuracy: geolocation.getAccuracy(), + // altitude: geolocation.getAltitude(), + // altitudeAccuracy: geolocation.getAltitudeAccuracy(), + // heading: geolocation.getHeading(), + // speed: geolocation.getSpeed() + // }) + }) + geolocation.on('error', (error) => { + // TBD + console.error(error) + }) + + const accuracyFeature = new Feature() + geolocation.on('change:accuracyGeometry', (evt) => { + accuracyFeature.setGeometry(geolocation.getAccuracyGeometry()) + }) + + const positionFeature = new Feature() + positionFeature.setStyle(new Style({ + image: new Circle({ + radius: 6, + fill: new Fill({ + color: '#3399CC' + }), + stroke: new Stroke({ + color: '#fff', + width: 2 + }) + }) + })) + + geolocation.on('change:position', (evt) => { + const position = geolocation.getPosition() + positionFeature.setGeometry(position ? new Point(position) : null) + + const extent = currentMap.getView().calculateExtent(currentMap.getSize()) + if (!containsCoordinate(extent, position)) { + currentMap.getView().setCenter(position) + } + }) + + const geolocationLayer = new VectorLayer({ + source: new Vector({ + features: [accuracyFeature, positionFeature] + }) + }) + geolocationLayer.set('displayInLayerSwitcher', false) + currentMap.addLayer(geolocationLayer) + + // Control button + const geolocationCtrl = new Toggle({ + html: 'my_location', + title: this.i18n.control.geolocation, + active: false, + onToggle: (active: boolean) => { + geolocation.setTracking(active) + geolocationLayer.setVisible(active) + } + }) + this.toolbar.addControl(geolocationCtrl) +} + +export function toggleAndLoadMap(el: HTMLLegendElement) { + const fieldset = el.parentElement + fieldset.classList.toggle('collapsed') + el.classList.toggle('icon-expended') + el.classList.toggle('icon-collapsed') + const div = fieldset.querySelector('div') + if (div.style.display === 'none') { + div.style.display = 'block' + } else { + div.style.display = 'none' + } + this.maps.forEach(function (m: any) { + m.updateSize() + }) +} + +export function setView() { + const center = fromLonLat([parseFloat(this.defaults.lon), parseFloat(this.defaults.lat)]) + const view = new View({ + // Avoid flicker (map move) + center: center, + zoom: parseInt(this.defaults.zoom), + maxZoom: parseInt(this.defaults.maxzoom), // applies for Mierune Tiles + rotation: degreesToRadians(parseInt(this.map.getTargetElement().getAttribute("data-rotation"))) + }) + this.map.setView(view) +} + +export function getStyle(feature: Feature, _: unknown):Style[] { + const styles: Style[] = [] + + // Apply Shadow + styles.push( + new Style({ + image: new Shadow({ + radius: 15, + blur: 5, + offsetX: 0, + offsetY: 0, + fill: new Fill({ + color: 'rgba(0,0,0,0.5)' + }) + }) + }) + ) + + const self = this + + // Apply Font Style + styles.push( + new Style({ + image: new FontSymbol({ + form: 'blazon', + gradient: false, + glyph: getSymbol(self, feature), + fontSize: 0.7, + radius: 18, + offsetY: -18, + rotation: 0, + rotateWithView: false, + color: getFontColor(feature), + fill: new Fill({ + color: getColor(self, feature) + }), + stroke: new Stroke({ + color: '#333333', + width: 1 + }), + opacity: 1, + }), + stroke: new Stroke({ + width: 4, + color: getColor(self, feature) + }), + fill: new Fill({ + color: getColor(self, feature, true), + }) + }) + ) + + return styles +} + +/** + * TODO: check if this is acually used + */ +export function getColor(mapObj: any, feature: Feature, isFill: boolean = false): string { + let color = '#000000' + if (feature.getGeometry().getType() !== 'Point') { + color = '#FFD700' + } + const plugin_settings = JSON.parse(mapObj.defaults.pluginSettings) + const status = document.querySelector('#issue_status_id') as HTMLInputElement + + let status_id = feature.get('status_id') + if (!status_id && status) { + status_id = status.value + } + if (status_id) { + const key = `status_${status_id}` + if (key in plugin_settings) { + color = plugin_settings[key] + } + } + if (isFill && color !== null && color.length === 7) { + color = color + '33' // Add alpha: 0.2 + } + return color +} + +/** + * TODO: check if this is acually used + */ +export function getFontColor(_: unknown): string { + const color = "#FFFFFF" + return color +} + +/** + * TODO: check if this is acually used + */ +export function getSymbol(mapObj: any, feature: Feature) { + let symbol = 'home' + + const plugin_settings = JSON.parse(mapObj.defaults.pluginSettings) + const issue_tracker = document.querySelector('#issue_tracker_id') as HTMLInputElement + let tracker_id = feature.get('tracker_id') + if (!tracker_id && issue_tracker) { + tracker_id = issue_tracker.value + } + if (tracker_id) { + const key = `tracker_${tracker_id}` + if (key in plugin_settings) { + symbol = plugin_settings[key] + } + } + return symbol +} diff --git a/src/components/gtt-client/redmine.ts b/src/components/gtt-client/redmine.ts new file mode 100644 index 00000000..eb1880f8 --- /dev/null +++ b/src/components/gtt-client/redmine.ts @@ -0,0 +1,103 @@ +import GttClient from './gtt-client-class'; + +/** + * Extend core Redmine's buildFilterRow method + */ +window.buildFilterRowWithoutDistanceFilter = window.buildFilterRow; +window.buildFilterRow = function (field, operator, values) { + if (field == 'distance') { + buildDistanceFilterRow(operator, values); + } else { + window.buildFilterRowWithoutDistanceFilter(field, operator, values); + } +}; + +export const buildDistanceFilterRow = (operator: any, values: any): void => { + const field = 'distance' + const fieldId = field + const filterTable = document.querySelector('#filters-table') as HTMLTableElement + const filterOptions = window.availableFilters[field] + if (!filterOptions) { + return + } + const operators = window.operatorByType[filterOptions['type']] + const filterValues = filterOptions['values'] + + const tr = document.createElement('tr') as HTMLTableRowElement + tr.className = 'filter' + tr.id = `tr_${fieldId}` + tr.innerHTML = ` + + + + + + + + + + + + + `; + (document.querySelector(`#values_${fieldId}_1`) as HTMLInputElement).value = values[0] + let base_idx = 1 + if (values.length == 2 || values.length == 4) { + // upper bound for 'between' operator + (document.querySelector(`#values_${fieldId}_2`) as HTMLInputElement).value = values[1] + base_idx = 2 + } + let x, y + if (values.length > 2) { + // console.log('distance center point from values: ', values[base_idx], values[base_idx+1]); + x = values[base_idx] + y = values[base_idx+1] + } else { + // console.log('taking distance from map fieldset: ', $('fieldset#location').data('center')); + const fieldset = document.querySelector('fieldset#location') as HTMLFieldSetElement + if (!fieldset.dataset.center) { + return + } + const xy = JSON.parse(fieldset.dataset.center) + x = xy[0] + y = xy[1] + } + (document.querySelector(`#values_${fieldId}_3`) as HTMLInputElement).value = x; + (document.querySelector(`#values_${fieldId}_4`) as HTMLInputElement).value = y; +}; + +window.replaceIssueFormWithInitMap = window.replaceIssueFormWith; +export const replaceIssueFormWithInitMap = window.replaceIssueFormWith; + +export const replaceIssueFormWith = (html: any): void => { + window.replaceIssueFormWithInitMap(html); + const ol_maps = document.querySelector( + "form[class$='_issue'] div.ol-map" + ) as HTMLDivElement; + if (ol_maps) { + new GttClient({ target: ol_maps }); + } +}; diff --git a/src/index.ts b/src/index.ts index 881cae5a..3de219dc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,12 +4,12 @@ import './stylesheets/app.scss'; // Custom Icons import './stylesheets/custom-icons.css'; -import './stylesheets/CustomIconsDef.js'; +import './stylesheets/custom-icons-def.js'; // Material Design Icons // https://github.com/marella/material-design-icons/tree/main/font#readme import '@material-design-icons/font/filled.css'; -import './stylesheets/MaterialDesignDef.js'; +import './stylesheets/material-design-def.js'; import { GttClient } from './components/gtt-client'; import { gtt_setting } from './components/gtt-setting'; diff --git a/src/stylesheets/CustomIconsDef.js b/src/stylesheets/custom-icons-def.js similarity index 100% rename from src/stylesheets/CustomIconsDef.js rename to src/stylesheets/custom-icons-def.js diff --git a/src/stylesheets/MaterialDesignDef.js b/src/stylesheets/material-design-def.js similarity index 100% rename from src/stylesheets/MaterialDesignDef.js rename to src/stylesheets/material-design-def.js diff --git a/webpack.config.js b/webpack.config.js index 51476d11..d40a045a 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -43,6 +43,7 @@ module.exports = { ], }, devtool: false, // Disable source maps + // devtool: 'source-map', // Generate source maps for easier debugging resolve: { extensions: ['.ts', '.js'], // Specify file extensions to resolve }, From b5a96dd389d13132d5d0df6736e15f7ab61ff814 Mon Sep 17 00:00:00 2001 From: Daniel Kastl Date: Fri, 7 Apr 2023 23:38:33 +0900 Subject: [PATCH 017/109] Adds comments Signed-off-by: Daniel Kastl --- src/@types/window.d.ts | 26 +++++++++++++++++++++++++ src/components/gtt-client/constants.ts | 14 +++++++++++-- src/components/gtt-client/index.ts | 2 ++ src/components/gtt-client/interfaces.ts | 6 ++++++ src/index.ts | 23 ++++++++++++---------- 5 files changed, 59 insertions(+), 12 deletions(-) diff --git a/src/@types/window.d.ts b/src/@types/window.d.ts index 75ea3623..c1c302f6 100644 --- a/src/@types/window.d.ts +++ b/src/@types/window.d.ts @@ -22,3 +22,29 @@ declare global { } export {}; // This ensures this file is treated as a module +/** + * Redmine functions + */ +interface Window { + availableFilters: any; + buildFilterRow(field: any, operator: any, values: any): void; + buildFilterRowWithoutDistanceFilter( + field: any, + operator: any, + values: any + ): void; + operatorByType: any; + operatorLabels: any; + replaceIssueFormWith(html: any): void; + replaceIssueFormWithInitMap(html: any): void; + showModal(id: string, width: string, title?: string): void; + toggleOperator(field: any): void; +} + +/** + * Gtt functions + */ +interface Window { + createGttClient(target: HTMLDivElement): void; + gtt_setting(): void; +} diff --git a/src/components/gtt-client/constants.ts b/src/components/gtt-client/constants.ts index c531df85..f98fd326 100644 --- a/src/components/gtt-client/constants.ts +++ b/src/components/gtt-client/constants.ts @@ -1,5 +1,15 @@ -// constants.ts -export const constants = { +// Define an interface named Constants which enforces readonly properties for lon, lat, zoom, maxzoom, fitMaxzoom and geocoder. +export interface Constants { + readonly lon: number; + readonly lat: number; + readonly zoom: number; + readonly maxzoom: number; + readonly fitMaxzoom: number; + readonly geocoder: Record; +} + +// Define a constant object named constants of type Constants and specify its values. +export const constants: Constants = { lon: 139.691706, lat: 35.689524, zoom: 13, diff --git a/src/components/gtt-client/index.ts b/src/components/gtt-client/index.ts index 3c4ed860..d09b3a0a 100644 --- a/src/components/gtt-client/index.ts +++ b/src/components/gtt-client/index.ts @@ -1,4 +1,6 @@ +// This line exports all the members from the redmine module file. export * from './redmine'; +// This line imports the GttClient class from the gtt-client-class module file and then re-exports it as the default export of this module file. import GttClient from './gtt-client-class'; export { GttClient }; diff --git a/src/components/gtt-client/interfaces.ts b/src/components/gtt-client/interfaces.ts index 30f4b907..a1ae99a2 100644 --- a/src/components/gtt-client/interfaces.ts +++ b/src/components/gtt-client/interfaces.ts @@ -2,10 +2,12 @@ import { Tile, Image, VectorTile as VTLayer } from 'ol/layer'; import { OSM, XYZ, TileWMS, ImageWMS, VectorTile as VTSource } from 'ol/source'; +// Interface for options used in creating a new instance of GttClient export interface GttClientOption { target: HTMLDivElement | null; } +// Interface describing a layer object used in GttClient export interface LayerObject { type: string; id: number; @@ -14,23 +16,27 @@ export interface LayerObject { options: object; } +// Interface for filtering options used in GttClient export interface FilterOption { location: boolean; distance: boolean; } +// Interface for describing a tile layer source used in GttClient export interface TileLayerSource { layer: typeof Tile; source: typeof OSM | typeof XYZ | typeof TileWMS; type: string; } +// Interface for describing an image layer source used in GttClient export interface ImageLayerSource { layer: typeof Image; source: typeof ImageWMS; type: string; } +// Interface for describing a vector tile layer source used in GttClient export interface VTLayerSource { layer: typeof VTLayer; source: typeof VTSource; diff --git a/src/index.ts b/src/index.ts index 3de219dc..20551e50 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,22 +1,25 @@ +// Import stylesheets import 'ol/ol.css'; import 'ol-ext/dist/ol-ext.min.css'; import './stylesheets/app.scss'; - -// Custom Icons import './stylesheets/custom-icons.css'; import './stylesheets/custom-icons-def.js'; - -// Material Design Icons -// https://github.com/marella/material-design-icons/tree/main/font#readme import '@material-design-icons/font/filled.css'; import './stylesheets/material-design-def.js'; +// Import components import { GttClient } from './components/gtt-client'; import { gtt_setting } from './components/gtt-setting'; -window.createGttClient = (target: HTMLDivElement): void => { - new GttClient({ target: target }); -}; -window.gtt_setting = (): void => { +// Define functions to attach to the global window object +function createGttClient(target: HTMLDivElement) { + new GttClient({ target }); +} + +function attachGttSetting() { gtt_setting(); -}; +} + +// Attach functions to global window object +window.createGttClient = createGttClient; +window.gtt_setting = attachGttSetting; From 34f2520287c5e86691ff5b7bd435a624f1424ca4 Mon Sep 17 00:00:00 2001 From: Daniel Kastl Date: Mon, 10 Apr 2023 00:40:17 +0900 Subject: [PATCH 018/109] Improves interfaces's names Signed-off-by: Daniel Kastl --- src/components/gtt-client/gtt-client-class.ts | 24 +++++++++---------- src/components/gtt-client/interfaces.ts | 12 +++++----- src/components/gtt-client/openlayers.ts | 4 ++-- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/components/gtt-client/gtt-client-class.ts b/src/components/gtt-client/gtt-client-class.ts index bdaf4be6..c0e5ccfd 100644 --- a/src/components/gtt-client/gtt-client-class.ts +++ b/src/components/gtt-client/gtt-client-class.ts @@ -37,12 +37,12 @@ import { ResizeObserver } from '@juggle/resize-observer'; // Import types import { - GttClientOption, - LayerObject, - FilterOption, - TileLayerSource, - ImageLayerSource, - VTLayerSource, + IGttClientOption, + ILayerObject, + IFilterOption, + ITileLayerSource, + IImageLayerSource, + IVTLayerSource, } from './interfaces'; import { constants as quick_hack } from './constants'; @@ -58,12 +58,12 @@ export default class GttClient { contents: DOMStringMap i18n: any toolbar: Bar - filters: FilterOption + filters: IFilterOption vector: VectorLayer> bounds: VectorLayer> geolocations: Array - constructor(options: GttClientOption) { + constructor(options: IGttClientOption) { this.filters = { location: false, distance: false @@ -145,12 +145,12 @@ export default class GttClient { this.layerArray = [] if (this.contents.layers) { - const layers = JSON.parse(this.contents.layers) as [LayerObject] + const layers = JSON.parse(this.contents.layers) as [ILayerObject] layers.forEach((layer) => { const s = layer.type.split('.') const layerSource = getLayerSource(s[1], s[2]) if ( layerSource.type === "TileLayerSource") { - const config = layerSource as TileLayerSource + const config = layerSource as ITileLayerSource const l = new (config.layer)({ visible: false, source: new (config.source)(layer.options as any) @@ -170,7 +170,7 @@ export default class GttClient { } this.layerArray.push(l) } else if (layerSource.type === "ImageLayerSource") { - const config = layerSource as ImageLayerSource + const config = layerSource as IImageLayerSource const l = new (config.layer)({ visible: false, source: new (config.source)(layer.options as ImageWMSOptions) @@ -190,7 +190,7 @@ export default class GttClient { } this.layerArray.push(l) } else if (layerSource.type === "VTLayerSource") { - const config = layerSource as VTLayerSource + const config = layerSource as IVTLayerSource const options = layer.options as VectorTileOptions options.format = new MVT() const l = new (config.layer)({ diff --git a/src/components/gtt-client/interfaces.ts b/src/components/gtt-client/interfaces.ts index a1ae99a2..95d559fb 100644 --- a/src/components/gtt-client/interfaces.ts +++ b/src/components/gtt-client/interfaces.ts @@ -3,12 +3,12 @@ import { Tile, Image, VectorTile as VTLayer } from 'ol/layer'; import { OSM, XYZ, TileWMS, ImageWMS, VectorTile as VTSource } from 'ol/source'; // Interface for options used in creating a new instance of GttClient -export interface GttClientOption { +export interface IGttClientOption { target: HTMLDivElement | null; } // Interface describing a layer object used in GttClient -export interface LayerObject { +export interface ILayerObject { type: string; id: number; name: string; @@ -17,27 +17,27 @@ export interface LayerObject { } // Interface for filtering options used in GttClient -export interface FilterOption { +export interface IFilterOption { location: boolean; distance: boolean; } // Interface for describing a tile layer source used in GttClient -export interface TileLayerSource { +export interface ITileLayerSource { layer: typeof Tile; source: typeof OSM | typeof XYZ | typeof TileWMS; type: string; } // Interface for describing an image layer source used in GttClient -export interface ImageLayerSource { +export interface IImageLayerSource { layer: typeof Image; source: typeof ImageWMS; type: string; } // Interface for describing a vector tile layer source used in GttClient -export interface VTLayerSource { +export interface IVTLayerSource { layer: typeof VTLayer; source: typeof VTSource; type: string; diff --git a/src/components/gtt-client/openlayers.ts b/src/components/gtt-client/openlayers.ts index c723656a..d5ddc1a3 100644 --- a/src/components/gtt-client/openlayers.ts +++ b/src/components/gtt-client/openlayers.ts @@ -18,7 +18,7 @@ import Popup from 'ol-ext/overlay/Popup'; import { position } from 'ol-ext/control/control'; import { GeoJSON } from 'ol/format'; -import { TileLayerSource, ImageLayerSource, VTLayerSource } from './interfaces'; +import { ITileLayerSource, IImageLayerSource, IVTLayerSource } from './interfaces'; import { getCookie, getMapSize, degreesToRadians, updateForm } from "./helpers"; /** @@ -194,7 +194,7 @@ export function setPopover() { export const getLayerSource = ( source: string, class_name: string, -): TileLayerSource | ImageLayerSource | VTLayerSource | undefined => { +): ITileLayerSource | IImageLayerSource | IVTLayerSource | undefined => { if (source === 'source') { if (class_name === 'OSM') { return { layer: TileLayer, source: OSM, type: 'TileLayerSource' }; From 6531319ec78f55ed6e978dc8faffdfbc78801494 Mon Sep 17 00:00:00 2001 From: Daniel Kastl Date: Mon, 10 Apr 2023 15:41:37 +0900 Subject: [PATCH 019/109] 2nd iteration Signed-off-by: Daniel Kastl --- src/components/gtt-client/GttClient.ts | 63 +++ .../{geocoding.ts => geocoding/index.ts} | 2 +- src/components/gtt-client/gtt-client-class.ts | 465 ------------------ .../{helpers.ts => helpers/index.ts} | 0 src/components/gtt-client/index.ts | 6 +- src/components/gtt-client/init/contents.ts | 3 + src/components/gtt-client/init/controls.ts | 72 +++ src/components/gtt-client/init/defaults.ts | 19 + src/components/gtt-client/init/events.ts | 96 ++++ src/components/gtt-client/init/layers.ts | 195 ++++++++ src/components/gtt-client/init/map.ts | 28 ++ .../{openlayers.ts => openlayers/index.ts} | 4 +- .../{redmine.ts => redmine/index.ts} | 2 +- 13 files changed, 485 insertions(+), 470 deletions(-) create mode 100644 src/components/gtt-client/GttClient.ts rename src/components/gtt-client/{geocoding.ts => geocoding/index.ts} (99%) delete mode 100644 src/components/gtt-client/gtt-client-class.ts rename src/components/gtt-client/{helpers.ts => helpers/index.ts} (100%) create mode 100644 src/components/gtt-client/init/contents.ts create mode 100644 src/components/gtt-client/init/controls.ts create mode 100644 src/components/gtt-client/init/defaults.ts create mode 100644 src/components/gtt-client/init/events.ts create mode 100644 src/components/gtt-client/init/layers.ts create mode 100644 src/components/gtt-client/init/map.ts rename src/components/gtt-client/{openlayers.ts => openlayers/index.ts} (99%) rename src/components/gtt-client/{redmine.ts => redmine/index.ts} (98%) diff --git a/src/components/gtt-client/GttClient.ts b/src/components/gtt-client/GttClient.ts new file mode 100644 index 00000000..aa5ca4b0 --- /dev/null +++ b/src/components/gtt-client/GttClient.ts @@ -0,0 +1,63 @@ +import { Map, Geolocation } from 'ol'; +import { Geometry } from 'ol/geom'; +import { Layer, Vector as VectorLayer } from 'ol/layer'; +import { Vector as VectorSource } from 'ol/source'; +import Bar from 'ol-ext/control/Bar'; + +import { IGttClientOption, IFilterOption } from './interfaces'; + +import { initDefaults } from './init/defaults'; +import { initContents } from './init/contents'; +import { initMap } from './init/map'; +import { initLayers } from './init/layers'; +import { initControls } from './init/controls'; +import { initEventListeners } from './init/events'; + +export default class GttClient { + readonly map: Map; + maps: Array; + layerArray: Layer[]; + defaults: DOMStringMap; + contents: DOMStringMap; + i18n: any; + toolbar: Bar; + filters: IFilterOption; + vector: VectorLayer>; + bounds: VectorLayer>; + geolocations: Array; + + constructor(options: IGttClientOption) { + if (!options.target) { + return; + } + + // For map div focus settings + if (options.target) { + if (options.target.getAttribute('tabindex') == null) { + options.target.setAttribute('tabindex', '0') + } + } + + this.filters = { + location: false, + distance: false, + }; + this.maps = []; + this.geolocations = []; + + this.defaults = initDefaults(); + this.contents = initContents(options.target); + this.i18n = JSON.parse(this.defaults.i18n); + + this.map = initMap(options.target, this.i18n); + + this.layerArray = initLayers.call(this); + + initControls.call(this); + + initEventListeners.call(this); + + // Handle multiple maps per page + this.maps.push(this.map) + } +} diff --git a/src/components/gtt-client/geocoding.ts b/src/components/gtt-client/geocoding/index.ts similarity index 99% rename from src/components/gtt-client/geocoding.ts rename to src/components/gtt-client/geocoding/index.ts index fda5cab5..78545f9b 100644 --- a/src/components/gtt-client/geocoding.ts +++ b/src/components/gtt-client/geocoding/index.ts @@ -6,7 +6,7 @@ import Bar from 'ol-ext/control/Bar'; import Toggle from 'ol-ext/control/Toggle'; import TextButton from 'ol-ext/control/TextButton'; -import { evaluateComparison, getObjectPathValue } from "./helpers"; +import { evaluateComparison, getObjectPathValue } from '../helpers'; /** * Add Geocoding functionality diff --git a/src/components/gtt-client/gtt-client-class.ts b/src/components/gtt-client/gtt-client-class.ts deleted file mode 100644 index c0e5ccfd..00000000 --- a/src/components/gtt-client/gtt-client-class.ts +++ /dev/null @@ -1,465 +0,0 @@ -/// OpenLayers core imports -import 'ol/ol.css'; -import { Map, Feature, Geolocation } from 'ol'; -import { Geometry } from 'ol/geom'; -import { GeoJSON, MVT } from 'ol/format'; -import { Layer, Tile, Image, VectorTile as VTLayer } from 'ol/layer'; -import VectorLayer from 'ol/layer/Vector'; -import { Style, Fill, Stroke } from 'ol/style'; -import { OrderFunction } from 'ol/render'; -import { - defaults as interactions_defaults, - MouseWheelZoom, -} from 'ol/interaction'; -import { focus as events_condifition_focus } from 'ol/events/condition'; -import { defaults as control_defaults, FullScreen, Rotate } from 'ol/control'; -import Vector from 'ol/source/Vector'; -import VectorSource from 'ol/source/Vector'; -import TileSource from 'ol/source/Tile'; -import ImageSource from 'ol/source/Image'; -import { Options as ImageWMSOptions } from 'ol/source/ImageWMS'; -import { Options as VectorTileOptions } from 'ol/source/VectorTile'; -import { applyStyle } from 'ol-mapbox-style'; - -// OpenLayers extension imports -import 'ol-ext/dist/ol-ext.min.css'; -import 'ol-ext/filter/Base'; -import Ordering from 'ol-ext/render/Ordering'; -import Mask from 'ol-ext/filter/Mask'; -import Bar from 'ol-ext/control/Bar'; -import Button from 'ol-ext/control/Button'; -import LayerPopup from 'ol-ext/control/LayerPopup'; -import LayerSwitcher from 'ol-ext/control/LayerSwitcher'; -import { position } from 'ol-ext/control/control'; - -// Other imports -import { ResizeObserver } from '@juggle/resize-observer'; - -// Import types -import { - IGttClientOption, - ILayerObject, - IFilterOption, - ITileLayerSource, - IImageLayerSource, - IVTLayerSource, -} from './interfaces'; - -import { constants as quick_hack } from './constants'; -import { radiansToDegrees, degreesToRadians, reloadFontSymbol, updateForm, updateFilter, parseHistory } from "./helpers"; -import { getLayerSource, setBasemap, zoomToExtent, toggleAndLoadMap, setGeolocation, setView, getStyle, setControls, setPopover } from "./openlayers"; -import { setGeocoding } from "./geocoding"; - -export default class GttClient { - readonly map: Map - maps: Array - layerArray: Layer[] - defaults: DOMStringMap - contents: DOMStringMap - i18n: any - toolbar: Bar - filters: IFilterOption - vector: VectorLayer> - bounds: VectorLayer> - geolocations: Array - - constructor(options: IGttClientOption) { - this.filters = { - location: false, - distance: false - } - this.maps = [] - this.geolocations = [] - - // needs target - if (!options.target) { - return - } - - const gtt_defaults = document.querySelector('#gtt-defaults') as HTMLDivElement - if (!gtt_defaults) { - return - } - - this.defaults = gtt_defaults.dataset - - if (this.defaults.lon === null || this.defaults.lon === undefined) { - this.defaults.lon = quick_hack.lon.toString() - } - if (this.defaults.lat === null || this.defaults.lat === undefined) { - this.defaults.lat = quick_hack.lat.toString() - } - if (this.defaults.zoom === null || this.defaults.zoom === undefined) { - this.defaults.zoom = quick_hack.zoom.toString() - } - if (this.defaults.maxzoom === null || this.defaults.maxzoom === undefined) { - this.defaults.maxzoom = quick_hack.maxzoom.toString() - } - if (this.defaults.fitMaxzoom === null || this.defaults.fitMaxzoom === undefined) { - this.defaults.fitMaxzoom = quick_hack.fitMaxzoom.toString() - } - if (this.defaults.geocoder === null || this.defaults.geocoder === undefined) { - this.defaults.geocoder = JSON.stringify(quick_hack.geocoder) - } - - this.contents = options.target.dataset - this.i18n = JSON.parse(this.defaults.i18n) - - // create map at first - this.map = new Map({ - target: options.target, - //layers: this.layerArray, - interactions: interactions_defaults({mouseWheelZoom: false}).extend([ - new MouseWheelZoom({ - constrainResolution: true, // force zooming to a integer zoom - condition: events_condifition_focus // only wheel/trackpad zoom when the map has the focus - }) - ]), - controls: control_defaults({ - rotateOptions: {}, - attributionOptions: { - collapsible: false - }, - zoomOptions: { - zoomInTipLabel: this.i18n.control.zoom_in, - zoomOutTipLabel: this.i18n.control.zoom_out - } - }) - }) - - let features: Feature[] | null = null - if (this.contents.geom && this.contents.geom !== null && this.contents.geom !== 'null') { - features = new GeoJSON().readFeatures( - JSON.parse(this.contents.geom), { - featureProjection: 'EPSG:3857' - } - ) - } - - // Fix FireFox unloaded font issue - reloadFontSymbol.call(this) - - // TODO: this is only necessary because setting the initial form value - // through the template causes encoding problems - updateForm(this, features) - this.layerArray = [] - - if (this.contents.layers) { - const layers = JSON.parse(this.contents.layers) as [ILayerObject] - layers.forEach((layer) => { - const s = layer.type.split('.') - const layerSource = getLayerSource(s[1], s[2]) - if ( layerSource.type === "TileLayerSource") { - const config = layerSource as ITileLayerSource - const l = new (config.layer)({ - visible: false, - source: new (config.source)(layer.options as any) - }) - - l.set('lid', layer.id) - l.set('title', layer.name) - l.set('baseLayer', layer.baselayer) - if( layer.baselayer ) { - l.on('change:visible', e => { - const target = e.target as Tile - if (target.getVisible()) { - const lid = target.get('lid') - document.cookie = `_redmine_gtt_basemap=${lid};path=/` - } - }) - } - this.layerArray.push(l) - } else if (layerSource.type === "ImageLayerSource") { - const config = layerSource as IImageLayerSource - const l = new (config.layer)({ - visible: false, - source: new (config.source)(layer.options as ImageWMSOptions) - }) - - l.set('lid', layer.id) - l.set('title', layer.name) - l.set('baseLayer', layer.baselayer) - if( layer.baselayer ) { - l.on('change:visible', e => { - const target = e.target as Image - if (target.getVisible()) { - const lid = target.get('lid') - document.cookie = `_redmine_gtt_basemap=${lid};path=/` - } - }) - } - this.layerArray.push(l) - } else if (layerSource.type === "VTLayerSource") { - const config = layerSource as IVTLayerSource - const options = layer.options as VectorTileOptions - options.format = new MVT() - const l = new (config.layer)({ - visible: false, - source: new (config.source)(options), - declutter: true - }) as VTLayer - - // Apply style URL if provided - if ("styleUrl" in options) { - applyStyle(l,options.styleUrl) - } - - l.set('lid', layer.id) - l.set('title', layer.name) - l.set('baseLayer', layer.baselayer) - if( layer.baselayer ) { - l.on('change:visible', (e: { target: any }) => { - const target = e.target as any - if (target.getVisible()) { - const lid = target.get('lid') - document.cookie = `_redmine_gtt_basemap=${lid};path=/` - } - }) - } - this.layerArray.push(l) - } - }, this) - - /** - * Ordering the Layers for the LayerSwitcher Control. - * BaseLayers are added first. - */ - this.layerArray.forEach( (l:Layer) => { - if( l.get("baseLayer") ) { - this.map.addLayer(l) - } - } - ) - - var containsOverlay = false; - - this.layerArray.forEach( (l:Layer) => { - if( !l.get("baseLayer") ) { - this.map.addLayer(l) - containsOverlay = true - } - } - ) - } - - setBasemap.call(this) - - // Layer for project boundary - this.bounds = new VectorLayer({ - source: new Vector(), - style: new Style({ - fill: new Fill({ - color: 'rgba(255,255,255,0.0)' - }), - stroke: new Stroke({ - color: 'rgba(220,26,26,0.7)', - // lineDash: [12,1,12], - width: 1 - }) - }) - }) - this.bounds.set('title', 'Boundaries') - this.bounds.set('displayInLayerSwitcher', false) - this.layerArray.push(this.bounds) - this.map.addLayer(this.bounds) - - const yOrdering: unknown = Ordering.yOrdering() - - this.vector = new VectorLayer>({ - source: new Vector({ - 'features': features, - 'useSpatialIndex': false - }), - renderOrder: yOrdering as OrderFunction, - style: getStyle.bind(this) - }) - this.vector.set('title', 'Features') - this.vector.set('displayInLayerSwitcher', false) - this.layerArray.push(this.vector) - this.map.addLayer(this.vector) - - // Render project boundary if bounds are available - if (this.contents.bounds && this.contents.bounds !== null) { - const boundary = new GeoJSON().readFeature( - this.contents.bounds, { - featureProjection: 'EPSG:3857' - } - ) - this.bounds.getSource().addFeature(boundary) - if (this.contents.bounds === this.contents.geom) { - this.vector.setVisible(false) - } - this.layerArray.forEach((layer:Layer) => { - if (layer.get('baseLayer')) { - layer.addFilter(new Mask({ - feature: boundary, - inner: false, - fill: new Fill({ - color: [220,26,26,.1] - }) - })) - } - }) - } - - // For map div focus settings - if (options.target) { - if (options.target.getAttribute('tabindex') == null) { - options.target.setAttribute('tabindex', '0') - } - } - - // Fix empty map issue - this.map.once('postrender', e => { - zoomToExtent.call(this, true) - }) - - // Add Toolbar - this.toolbar = new Bar() - this.toolbar.setPosition('bottom-left' as position) - this.map.addControl(this.toolbar) - setView.call(this) - setGeocoding.call(this, this.map) - setGeolocation.call(this, this.map) - parseHistory.call(this) - - this.map.addControl (new FullScreen({ - tipLabel: this.i18n.control.fullscreen - })) - this.map.addControl (new Rotate({ - tipLabel: this.i18n.control.rotate - })) - - // Control button - const maximizeCtrl = new Button({ - html: 'zoom_out_map', - title: this.i18n.control.maximize, - handleClick: () => { - zoomToExtent.call(this, true); - } - }) - this.toolbar.addControl(maximizeCtrl) - - // Map rotation - const rotation_field = document.querySelector('#gtt_configuration_map_rotation') as HTMLInputElement - if (rotation_field !== null) { - this.map.getView().on('change:rotation', (evt) => { - rotation_field.value = String(Math.round(radiansToDegrees(evt.target.getRotation()))) - }) - - rotation_field.addEventListener("input", (evt) => { - const { target } = evt; - if (!(target instanceof HTMLInputElement)) { - return; - } - const value = target.value; - this.map.getView().setRotation(degreesToRadians(parseInt(value))) - }) - } - - if (this.contents.edit) { - setControls.call(this, this.contents.edit.split(' ')) - } else if (this.contents.popup) { - setPopover.call(this) - } - - // Zoom to extent when map collapsed => expended - if (this.contents.collapsed) { - const self = this - const collapsedObserver = new MutationObserver((mutations) => { - // const currentMap = this.map - mutations.forEach(function(mutation) { - if (mutation.attributeName !== 'style') { - return - } - const mapDiv = mutation.target as HTMLDivElement - if (mapDiv && mapDiv.style.display === 'block') { - zoomToExtent.call(this, true) - collapsedObserver.disconnect() - } - }) - }) - collapsedObserver.observe(self.map.getTargetElement(), { attributes: true, attributeFilter: ['style'] }) - } - - // Sidebar hack - const resizeObserver = new ResizeObserver((entries, observer) => { - this.maps.forEach(m => { - m.updateSize() - }) - }) - resizeObserver.observe(this.map.getTargetElement()) - - // When one or more issues is selected, zoom to selected map features - document.querySelectorAll('table.issues tbody tr').forEach((element: HTMLTableRowElement) => { - element.addEventListener('click', (evt) => { - const currentTarget = evt.currentTarget as HTMLTableRowElement - const id = currentTarget.id.split('-')[1] - const feature = this.vector.getSource().getFeatureById(id) - this.map.getView().fit(feature.getGeometry().getExtent(), { - size: this.map.getSize() - }) - }) - }) - - // Need to update size of an invisible map, when the editable form is made - // visible. This doesn't look like a good way to do it, but this is more of - // a Redmine problem - document.querySelectorAll('div.contextual a.icon-edit').forEach((element: HTMLAnchorElement) => { - element.addEventListener('click', () => { - setTimeout(() => { - this.maps.forEach(m => { - m.updateSize() - }) - zoomToExtent.call(this) - }, 200) - }) - }) - - // Redraw the map, when a GTT Tab gets activated - document.querySelectorAll('#tab-gtt').forEach((element) => { - element.addEventListener('click', () => { - this.maps.forEach(m => { - m.updateSize() - }) - zoomToExtent.call(this) - }) - }) - - // Add LayerSwitcher Image Toolbar - if( containsOverlay) { - this.map.addControl(new LayerSwitcher({ - reordering: false - })) - } - else { - this.map.addControl(new LayerPopup()) - } - - - // Because Redmine filter functions are applied later, the Window onload - // event provides a workaround to have filters loaded before executing - // the following code - window.addEventListener('load', () => { - if (document.querySelectorAll('tr#tr_bbox').length > 0) { - this.filters.location = true - } - if (document.querySelectorAll('tr#tr_distance').length > 0) { - this.filters.distance = true - } - const legend = document.querySelector('fieldset#location legend') as HTMLLegendElement - if (legend) { - legend.addEventListener('click', (evt) => { - const element = evt.currentTarget as HTMLLegendElement - toggleAndLoadMap(element) - }) - } - zoomToExtent.call(this) - this.map.on('moveend', updateFilter.bind(this)) - }) - - // Handle multiple maps per page - this.maps.push(this.map) - } - -} diff --git a/src/components/gtt-client/helpers.ts b/src/components/gtt-client/helpers/index.ts similarity index 100% rename from src/components/gtt-client/helpers.ts rename to src/components/gtt-client/helpers/index.ts diff --git a/src/components/gtt-client/index.ts b/src/components/gtt-client/index.ts index d09b3a0a..d4e9745b 100644 --- a/src/components/gtt-client/index.ts +++ b/src/components/gtt-client/index.ts @@ -1,6 +1,10 @@ // This line exports all the members from the redmine module file. export * from './redmine'; +// import 'ol/ol.css'; +// import 'ol-ext/dist/ol-ext.min.css'; +// import 'ol-ext/filter/Base'; + // This line imports the GttClient class from the gtt-client-class module file and then re-exports it as the default export of this module file. -import GttClient from './gtt-client-class'; +import GttClient from './GttClient'; export { GttClient }; diff --git a/src/components/gtt-client/init/contents.ts b/src/components/gtt-client/init/contents.ts new file mode 100644 index 00000000..1de18a10 --- /dev/null +++ b/src/components/gtt-client/init/contents.ts @@ -0,0 +1,3 @@ +export function initContents(target: HTMLElement): DOMStringMap { + return target.dataset; +} diff --git a/src/components/gtt-client/init/controls.ts b/src/components/gtt-client/init/controls.ts new file mode 100644 index 00000000..fb754124 --- /dev/null +++ b/src/components/gtt-client/init/controls.ts @@ -0,0 +1,72 @@ +import { FullScreen, Rotate } from 'ol/control'; +import Bar from 'ol-ext/control/Bar'; +import Button from 'ol-ext/control/Button'; +import LayerPopup from 'ol-ext/control/LayerPopup'; +import LayerSwitcher from 'ol-ext/control/LayerSwitcher'; +import { position } from 'ol-ext/control/control'; + +import { setGeocoding } from "../geocoding"; +import { radiansToDegrees, degreesToRadians, parseHistory } from "../helpers"; +import { zoomToExtent, setGeolocation, setView, setControls, setPopover } from "../openlayers"; + +export function initControls(this: any): void { + + // Add Toolbar + this.toolbar = new Bar() + this.toolbar.setPosition('bottom-left' as position) + this.map.addControl(this.toolbar) + setView.call(this) + setGeocoding.call(this, this.map) + setGeolocation.call(this, this.map) + parseHistory.call(this) + + this.map.addControl (new FullScreen({ + tipLabel: this.i18n.control.fullscreen + })) + this.map.addControl (new Rotate({ + tipLabel: this.i18n.control.rotate + })) + + // Control button + const maximizeCtrl = new Button({ + html: 'zoom_out_map', + title: this.i18n.control.maximize, + handleClick: () => { + zoomToExtent.call(this, true); + } + }) + this.toolbar.addControl(maximizeCtrl) + + // Map rotation + const rotation_field = document.querySelector('#gtt_configuration_map_rotation') as HTMLInputElement + if (rotation_field !== null) { + this.map.getView().on('change:rotation', (evt: any) => { + rotation_field.value = String(Math.round(radiansToDegrees(evt.target.getRotation()))) + }) + + rotation_field.addEventListener("input", (evt: any) => { + const { target } = evt; + if (!(target instanceof HTMLInputElement)) { + return; + } + const value = target.value; + this.map.getView().setRotation(degreesToRadians(parseInt(value))) + }) + } + + if (this.contents.edit) { + setControls.call(this, this.contents.edit.split(' ')) + } else if (this.contents.popup) { + setPopover.call(this) + } + + // Add LayerSwitcher Image Toolbar + if( this.containsOverlay) { + this.map.addControl(new LayerSwitcher({ + reordering: false + })) + } + else { + this.map.addControl(new LayerPopup()) + } +} diff --git a/src/components/gtt-client/init/defaults.ts b/src/components/gtt-client/init/defaults.ts new file mode 100644 index 00000000..fd038ac7 --- /dev/null +++ b/src/components/gtt-client/init/defaults.ts @@ -0,0 +1,19 @@ +import { constants } from '../constants'; + +export function initDefaults(): DOMStringMap { + const gtt_defaults = document.querySelector('#gtt-defaults') as HTMLDivElement; + if (!gtt_defaults) { + return {}; + } + + const defaults = gtt_defaults.dataset; + + // Set default values for missing properties + Object.entries(constants).forEach(([key, value]) => { + if (defaults[key] === null || defaults[key] === undefined) { + defaults[key] = value.toString(); + } + }); + + return defaults; +} diff --git a/src/components/gtt-client/init/events.ts b/src/components/gtt-client/init/events.ts new file mode 100644 index 00000000..f00d5516 --- /dev/null +++ b/src/components/gtt-client/init/events.ts @@ -0,0 +1,96 @@ +import { ResizeObserver } from '@juggle/resize-observer'; + +import { updateFilter } from "../helpers"; +import { zoomToExtent, toggleAndLoadMap } from "../openlayers"; + +export function initEventListeners(this: any): void { + + // Fix empty map issue + this.map.once('postrender', (evt: any) => { + zoomToExtent.call(this, true) + }) + + // Zoom to extent when map collapsed => expended + if (this.contents.collapsed) { + const self = this + const collapsedObserver = new MutationObserver((mutations) => { + // const currentMap = this.map + mutations.forEach(function(mutation) { + if (mutation.attributeName !== 'style') { + return + } + const mapDiv = mutation.target as HTMLDivElement + if (mapDiv && mapDiv.style.display === 'block') { + zoomToExtent.call(this, true) + collapsedObserver.disconnect() + } + }) + }) + collapsedObserver.observe(self.map.getTargetElement(), { attributes: true, attributeFilter: ['style'] }) + } + + // Sidebar hack + const resizeObserver = new ResizeObserver((entries, observer) => { + this.maps.forEach((m: any) => { + m.updateSize() + }) + }) + resizeObserver.observe(this.map.getTargetElement()) + + // When one or more issues is selected, zoom to selected map features + document.querySelectorAll('table.issues tbody tr').forEach((element: HTMLTableRowElement) => { + element.addEventListener('click', (evt) => { + const currentTarget = evt.currentTarget as HTMLTableRowElement + const id = currentTarget.id.split('-')[1] + const feature = this.vector.getSource().getFeatureById(id) + this.map.getView().fit(feature.getGeometry().getExtent(), { + size: this.map.getSize() + }) + }) + }) + + // Need to update size of an invisible map, when the editable form is made + // visible. This doesn't look like a good way to do it, but this is more of + // a Redmine problem + document.querySelectorAll('div.contextual a.icon-edit').forEach((element: HTMLAnchorElement) => { + element.addEventListener('click', () => { + setTimeout(() => { + this.maps.forEach((m: any) => { + m.updateSize() + }) + zoomToExtent.call(this) + }, 200) + }) + }) + + // Redraw the map, when a GTT Tab gets activated + document.querySelectorAll('#tab-gtt').forEach((element) => { + element.addEventListener('click', () => { + this.maps.forEach((m: any) => { + m.updateSize() + }) + zoomToExtent.call(this) + }) + }) + + // Because Redmine filter functions are applied later, the Window onload + // event provides a workaround to have filters loaded before executing + // the following code + window.addEventListener('load', () => { + if (document.querySelectorAll('tr#tr_bbox').length > 0) { + this.filters.location = true + } + if (document.querySelectorAll('tr#tr_distance').length > 0) { + this.filters.distance = true + } + const legend = document.querySelector('fieldset#location legend') as HTMLLegendElement + if (legend) { + legend.addEventListener('click', (evt) => { + const element = evt.currentTarget as HTMLLegendElement + toggleAndLoadMap(element) + }) + } + zoomToExtent.call(this) + this.map.on('moveend', updateFilter.bind(this)) + }) +} diff --git a/src/components/gtt-client/init/layers.ts b/src/components/gtt-client/init/layers.ts new file mode 100644 index 00000000..0ccb12d1 --- /dev/null +++ b/src/components/gtt-client/init/layers.ts @@ -0,0 +1,195 @@ +import { Feature } from 'ol'; +import { Layer, Tile, Image, Vector as VectorLayer, VectorTile as VTLayer } from 'ol/layer'; +import { Vector as VectorSource, Image as ImageSource, Tile as TileSource } from 'ol/source'; +import { GeoJSON, MVT } from 'ol/format'; +import { Geometry } from 'ol/geom'; +import { Options as ImageWMSOptions } from 'ol/source/ImageWMS'; +import { Options as VectorTileOptions } from 'ol/source/VectorTile'; +import { Style, Fill, Stroke } from 'ol/style'; +import { OrderFunction } from 'ol/render'; + +import Ordering from 'ol-ext/render/Ordering'; +import Mask from 'ol-ext/filter/Mask'; +import { applyStyle } from 'ol-mapbox-style'; + +import { ILayerObject, ITileLayerSource, IImageLayerSource, IVTLayerSource } from '../interfaces'; +import { updateForm, reloadFontSymbol } from "../helpers"; +import { getLayerSource, setBasemap, getStyle } from "../openlayers"; + +export function initLayers(this: any): Layer[] { + this.layerArray = []; + + let features: Feature[] | null = null + if (this.contents.geom && this.contents.geom !== null && this.contents.geom !== 'null') { + features = new GeoJSON().readFeatures( + JSON.parse(this.contents.geom), { + featureProjection: 'EPSG:3857' + } + ) + } + + // Fix FireFox unloaded font issue + reloadFontSymbol.call(this) + + // TODO: this is only necessary because setting the initial form value + // through the template causes encoding problems + updateForm(this, features) + + if (this.contents.layers) { + const layers = JSON.parse(this.contents.layers) as [ILayerObject] + layers.forEach((layer) => { + const s = layer.type.split('.') + const layerSource = getLayerSource(s[1], s[2]) + if ( layerSource.type === "TileLayerSource") { + const config = layerSource as ITileLayerSource + const l = new (config.layer)({ + visible: false, + source: new (config.source)(layer.options as any) + }) + + l.set('lid', layer.id) + l.set('title', layer.name) + l.set('baseLayer', layer.baselayer) + if( layer.baselayer ) { + l.on('change:visible', e => { + const target = e.target as Tile + if (target.getVisible()) { + const lid = target.get('lid') + document.cookie = `_redmine_gtt_basemap=${lid};path=/` + } + }) + } + this.layerArray.push(l) + } else if (layerSource.type === "ImageLayerSource") { + const config = layerSource as IImageLayerSource + const l = new (config.layer)({ + visible: false, + source: new (config.source)(layer.options as ImageWMSOptions) + }) + + l.set('lid', layer.id) + l.set('title', layer.name) + l.set('baseLayer', layer.baselayer) + if( layer.baselayer ) { + l.on('change:visible', e => { + const target = e.target as Image + if (target.getVisible()) { + const lid = target.get('lid') + document.cookie = `_redmine_gtt_basemap=${lid};path=/` + } + }) + } + this.layerArray.push(l) + } else if (layerSource.type === "VTLayerSource") { + const config = layerSource as IVTLayerSource + const options = layer.options as VectorTileOptions + options.format = new MVT() + const l = new (config.layer)({ + visible: false, + source: new (config.source)(options), + declutter: true + }) as VTLayer + + // Apply style URL if provided + if ("styleUrl" in options) { + applyStyle(l,options.styleUrl) + } + + l.set('lid', layer.id) + l.set('title', layer.name) + l.set('baseLayer', layer.baselayer) + if( layer.baselayer ) { + l.on('change:visible', (e: { target: any }) => { + const target = e.target as any + if (target.getVisible()) { + const lid = target.get('lid') + document.cookie = `_redmine_gtt_basemap=${lid};path=/` + } + }) + } + this.layerArray.push(l) + } + }, this) + + /** + * Ordering the Layers for the LayerSwitcher Control. + * BaseLayers are added first. + */ + this.layerArray.forEach( (l:Layer) => { + if( l.get("baseLayer") ) { + this.map.addLayer(l) + } + }) + + this.containsOverlay = false; + + this.layerArray.forEach( (l:Layer) => { + if( !l.get("baseLayer") ) { + this.map.addLayer(l) + this.containsOverlay = true + } + }) + } + + setBasemap.call(this) + + // Layer for project boundary + this.bounds = new VectorLayer({ + source: new VectorSource(), + style: new Style({ + fill: new Fill({ + color: 'rgba(255,255,255,0.0)' + }), + stroke: new Stroke({ + color: 'rgba(220,26,26,0.7)', + // lineDash: [12,1,12], + width: 1 + }) + }) + }) + this.bounds.set('title', 'Boundaries') + this.bounds.set('displayInLayerSwitcher', false) + this.layerArray.push(this.bounds) + this.map.addLayer(this.bounds) + + const yOrdering: unknown = Ordering.yOrdering() + + this.vector = new VectorLayer>({ + source: new VectorSource({ + 'features': features, + 'useSpatialIndex': false + }), + renderOrder: yOrdering as OrderFunction, + style: getStyle.bind(this) + }) + this.vector.set('title', 'Features') + this.vector.set('displayInLayerSwitcher', false) + this.layerArray.push(this.vector) + this.map.addLayer(this.vector) + + // Render project boundary if bounds are available + if (this.contents.bounds && this.contents.bounds !== null) { + const boundary = new GeoJSON().readFeature( + this.contents.bounds, { + featureProjection: 'EPSG:3857' + } + ) + this.bounds.getSource().addFeature(boundary) + if (this.contents.bounds === this.contents.geom) { + this.vector.setVisible(false) + } + this.layerArray.forEach((layer:Layer) => { + if (layer.get('baseLayer')) { + layer.addFilter(new Mask({ + feature: boundary, + inner: false, + fill: new Fill({ + color: [220,26,26,.1] + }) + })) + } + }) + } + + return this.layerArray; +} diff --git a/src/components/gtt-client/init/map.ts b/src/components/gtt-client/init/map.ts new file mode 100644 index 00000000..56bc9cab --- /dev/null +++ b/src/components/gtt-client/init/map.ts @@ -0,0 +1,28 @@ +import { Map } from 'ol'; +import { defaults as interactions_defaults, MouseWheelZoom } from 'ol/interaction'; +import { focus as events_condifition_focus } from 'ol/events/condition'; +import { defaults as control_defaults } from 'ol/control'; + +export function initMap(target: HTMLElement, i18n: any): Map { + const map = new Map({ + target, + interactions: interactions_defaults({ mouseWheelZoom: false }).extend([ + new MouseWheelZoom({ + constrainResolution: true, // force zooming to a integer zoom + condition: events_condifition_focus, // only wheel/trackpad zoom when the map has the focus + }), + ]), + controls: control_defaults({ + rotateOptions: {}, + attributionOptions: { + collapsible: false, + }, + zoomOptions: { + zoomInTipLabel: i18n.control.zoom_in, + zoomOutTipLabel: i18n.control.zoom_out, + }, + }), + }); + + return map; +} diff --git a/src/components/gtt-client/openlayers.ts b/src/components/gtt-client/openlayers/index.ts similarity index 99% rename from src/components/gtt-client/openlayers.ts rename to src/components/gtt-client/openlayers/index.ts index d5ddc1a3..6a071a7e 100644 --- a/src/components/gtt-client/openlayers.ts +++ b/src/components/gtt-client/openlayers/index.ts @@ -18,8 +18,8 @@ import Popup from 'ol-ext/overlay/Popup'; import { position } from 'ol-ext/control/control'; import { GeoJSON } from 'ol/format'; -import { ITileLayerSource, IImageLayerSource, IVTLayerSource } from './interfaces'; -import { getCookie, getMapSize, degreesToRadians, updateForm } from "./helpers"; +import { ITileLayerSource, IImageLayerSource, IVTLayerSource } from '../interfaces'; +import { getCookie, getMapSize, degreesToRadians, updateForm } from "../helpers"; /** * Add editing tools diff --git a/src/components/gtt-client/redmine.ts b/src/components/gtt-client/redmine/index.ts similarity index 98% rename from src/components/gtt-client/redmine.ts rename to src/components/gtt-client/redmine/index.ts index eb1880f8..c7e66687 100644 --- a/src/components/gtt-client/redmine.ts +++ b/src/components/gtt-client/redmine/index.ts @@ -1,4 +1,4 @@ -import GttClient from './gtt-client-class'; +import GttClient from '../GttClient'; /** * Extend core Redmine's buildFilterRow method From a60fb4f06d8d91f88e00c07539c252b74bea7cd8 Mon Sep 17 00:00:00 2001 From: Daniel Kastl Date: Mon, 10 Apr 2023 15:47:18 +0900 Subject: [PATCH 020/109] Cleanup tile source columns Signed-off-by: Daniel Kastl --- app/views/gtt_tile_sources/_tile_source.html.erb | 4 ++-- app/views/gtt_tile_sources/index.html.erb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/gtt_tile_sources/_tile_source.html.erb b/app/views/gtt_tile_sources/_tile_source.html.erb index 2c2fc0f3..12e17260 100644 --- a/app/views/gtt_tile_sources/_tile_source.html.erb +++ b/app/views/gtt_tile_sources/_tile_source.html.erb @@ -1,10 +1,10 @@ "> - <%= tile_source.type %> <%= link_to tile_source.name, edit_gtt_tile_source_path(tile_source) %> + <%= tile_source.type %> <%= checked_image tile_source.baselayer? %> <%= checked_image tile_source.global? %> <%= checked_image tile_source.default? %> - <%= tile_source_options tile_source %> + <%= reorder_handle(tile_source, url: gtt_tile_source_path(tile_source), param: 'tile_source') %> <%= delete_link gtt_tile_source_path(tile_source) %> diff --git a/app/views/gtt_tile_sources/index.html.erb b/app/views/gtt_tile_sources/index.html.erb index 7427e3c2..55804bae 100644 --- a/app/views/gtt_tile_sources/index.html.erb +++ b/app/views/gtt_tile_sources/index.html.erb @@ -8,12 +8,12 @@ - + - + From ec7cdf5337b4ed33810a6c1d11371ac4f53427f7 Mon Sep 17 00:00:00 2001 From: Daniel Kastl Date: Mon, 10 Apr 2023 17:23:44 +0900 Subject: [PATCH 021/109] Tabs for GTT settings Signed-off-by: Daniel Kastl --- app/views/settings/gtt/_general.html.erb | 50 ++++++++-- app/views/settings/gtt/_geocoder.html.erb | 17 ++++ app/views/settings/gtt/_main.html.erb | 3 - app/views/settings/gtt/_map.html.erb | 58 ------------ app/views/settings/gtt/_settings.html.erb | 107 ++-------------------- app/views/settings/gtt/_styling.html.erb | 22 +++++ config/locales/de.yml | 3 + config/locales/en.yml | 4 + config/locales/ja.yml | 4 + 9 files changed, 97 insertions(+), 171 deletions(-) create mode 100644 app/views/settings/gtt/_geocoder.html.erb delete mode 100644 app/views/settings/gtt/_main.html.erb delete mode 100644 app/views/settings/gtt/_map.html.erb create mode 100644 app/views/settings/gtt/_styling.html.erb diff --git a/app/views/settings/gtt/_general.html.erb b/app/views/settings/gtt/_general.html.erb index e6ebf742..e1f015c7 100644 --- a/app/views/settings/gtt/_general.html.erb +++ b/app/views/settings/gtt/_general.html.erb @@ -1,17 +1,15 @@ -

    <%= content_tag(:label, l(:gtt_settings_general_center_lon)) %> <%= text_field_tag('settings[default_map_center_longitude]', @settings['default_map_center_longitude'], - #:readonly => true, :size => 10) %>

    @@ -19,7 +17,6 @@ <%= content_tag(:label, l(:gtt_settings_general_center_lat)) %> <%= text_field_tag('settings[default_map_center_latitude]', @settings['default_map_center_latitude'], - #:readonly => true, :size => 10) %>

    @@ -27,6 +24,41 @@ <%= content_tag(:label, l(:gtt_settings_general_zoom_level)) %> <%= text_field_tag('settings[default_map_zoom_level]', @settings['default_map_zoom_level'], - #:readonly => true, :size => 10) %>

    + +

    + <%= content_tag(:label, l(:gtt_settings_general_maxzoom_level)) %> + <%= text_field_tag('settings[default_map_maxzoom_level]', + @settings['default_map_maxzoom_level'], + :size => 10) %> +

    + +

    + <%= content_tag(:label, l(:gtt_settings_general_fit_maxzoom_level)) %> + <%= text_field_tag('settings[default_map_fit_maxzoom_level]', + @settings['default_map_fit_maxzoom_level'], + :size => 10) %> +

    + + +
    +

    <%= l(:select_edit_geometry_settings) %>

    + +

    + <%= content_tag(:label, l(:label_editable_geometry_types_on_issue_map)) %> + + <% ["Point", "LineString", "Polygon"].each do |g| %> + + <% end %> +

    + +

    + <%= content_tag(:label, l(:label_enable_geojson_upload_on_issue_map)) %> + <%= check_box_tag 'settings[enable_geojson_upload_on_issue_map]', true, @settings[:enable_geojson_upload_on_issue_map] %> +

    +
    diff --git a/app/views/settings/gtt/_geocoder.html.erb b/app/views/settings/gtt/_geocoder.html.erb new file mode 100644 index 00000000..c0c6c6db --- /dev/null +++ b/app/views/settings/gtt/_geocoder.html.erb @@ -0,0 +1,17 @@ +
    +

    <%= l(:select_default_geocoder_settings) %>

    + +

    + <%= content_tag(:label, l(:label_enable_geocoding_on_map)) %> + <%= check_box_tag 'settings[enable_geocoding_on_map]', true, @settings[:enable_geocoding_on_map] %> +

    + +

    + <%= content_tag(:label, l(:geocoder_options)) %> + <%= text_area_tag('settings[default_geocoder_options]', + @settings['default_geocoder_options'], + :escape => false, + :rows => 10, + :cols => 100) %> +

    +
    diff --git a/app/views/settings/gtt/_main.html.erb b/app/views/settings/gtt/_main.html.erb deleted file mode 100644 index 6df63ce1..00000000 --- a/app/views/settings/gtt/_main.html.erb +++ /dev/null @@ -1,3 +0,0 @@ -<%= render_tabs RedmineGtt::SETTINGS %> - -<% html_title(l(:label_settings), l(:label_gtt)) -%> diff --git a/app/views/settings/gtt/_map.html.erb b/app/views/settings/gtt/_map.html.erb deleted file mode 100644 index 55fe9080..00000000 --- a/app/views/settings/gtt/_map.html.erb +++ /dev/null @@ -1,58 +0,0 @@ -
    -<%= link_to l(:label_gtt_basemap_new), new_gtt_basemap_path, :class => 'icon icon-add' %> -
    - -

    <%=l(:label_gtt_basemap_plural)%>

    - -
    <%= l :label_type %> <%= l :label_name %><%= l :label_type %> <%= l :label_baselayer %> <%= l :label_global %> <%= l :label_default %><%= l :label_config %>
    - - - - - - -<% for basemap in GttBasemap.all %> - "> - - - - -<% end %> - -
    <%=l(:label_gtt_basemap)%><%=l(:label_gtt_basemap_url)%>
    <%= basemap.name %><%= basemap.url %> - <%= delete_link gtt_basemap_path(status) %> -
    - - - -

    - <%= content_tag(:label, l(:gtt_settings_map_url)) %> - <%= text_field_tag('settings[default_map_url]', - @settings['default_map_url'], - :size => 80) %> -

    - -

    - <%= content_tag(:label, l(:gtt_settings_map_apikey)) %> - <%= text_field_tag('settings[default_map_apikey]', - @settings['default_map_apikey'], - :size => 80) %> -

    - -

    - <%= content_tag(:label, l(:gtt_settings_map_maxzoom_level)) %> - <%= text_field_tag('settings[default_map_maxzoom_level]', - @settings['default_map_maxzoom_level'], - :size => 5) %> -

    diff --git a/app/views/settings/gtt/_settings.html.erb b/app/views/settings/gtt/_settings.html.erb index 28707e71..8f24cee3 100644 --- a/app/views/settings/gtt/_settings.html.erb +++ b/app/views/settings/gtt/_settings.html.erb @@ -1,106 +1,11 @@ -

    <%= l(:select_default_tracker_icon) %>

    +<% plugin_tabs = [ + { :name => :general, :partial => 'settings/gtt/general', :label => :gtt_settings_label_general }, + { :name => :styling, :partial => 'settings/gtt/styling', :label => :gtt_settings_label_styling }, + { :name => :geocoder, :partial => 'settings/gtt/geocoder', :label => :gtt_settings_label_geocoder } +] %> -<% Tracker.sorted.each do |t| %> -

    - <%= content_tag :label, t.name %> - <%= select_tag "settings[tracker_#{t.id}]", "".html_safe %> - -

    -<% end %> -
    -
    -

    <%= l(:select_default_status_color) %>

    - -<% IssueStatus.sorted.each do |t| %> -

    - <%= content_tag :label, t.name %> - <%= color_field_tag "settings[status_#{t.id}]", @settings["status_#{t.id}"] %> -

    -<% end %> -
    - -
    -

    <%= l(:select_default_map_settings) %>

    - -

    - <%= content_tag(:label, l(:label_default_collapsed_issues_page_map)) %> - <%= check_box_tag 'settings[default_collapsed_issues_page_map]', true, @settings[:default_collapsed_issues_page_map] %> -

    - -

    - <%= content_tag(:label, l(:gtt_settings_general_center_lon)) %> - <%= text_field_tag('settings[default_map_center_longitude]', - @settings['default_map_center_longitude'], - :size => 10) %> -

    - -

    - <%= content_tag(:label, l(:gtt_settings_general_center_lat)) %> - <%= text_field_tag('settings[default_map_center_latitude]', - @settings['default_map_center_latitude'], - :size => 10) %> -

    - -

    - <%= content_tag(:label, l(:gtt_settings_general_zoom_level)) %> - <%= text_field_tag('settings[default_map_zoom_level]', - @settings['default_map_zoom_level'], - :size => 10) %> -

    - -

    - <%= content_tag(:label, l(:gtt_settings_general_maxzoom_level)) %> - <%= text_field_tag('settings[default_map_maxzoom_level]', - @settings['default_map_maxzoom_level'], - :size => 10) %> -

    - -

    - <%= content_tag(:label, l(:gtt_settings_general_fit_maxzoom_level)) %> - <%= text_field_tag('settings[default_map_fit_maxzoom_level]', - @settings['default_map_fit_maxzoom_level'], - :size => 10) %> -

    -
    - -
    -

    <%= l(:select_edit_geometry_settings) %>

    - -

    - <%= content_tag(:label, l(:label_editable_geometry_types_on_issue_map)) %> - - <% ["Point", "LineString", "Polygon"].each do |g| %> - - <% end %> -

    - -

    - <%= content_tag(:label, l(:label_enable_geojson_upload_on_issue_map)) %> - <%= check_box_tag 'settings[enable_geojson_upload_on_issue_map]', true, @settings[:enable_geojson_upload_on_issue_map] %> -

    -
    - -
    -

    <%= l(:select_default_geocoder_settings) %>

    - -

    - <%= content_tag(:label, l(:label_enable_geocoding_on_map)) %> - <%= check_box_tag 'settings[enable_geocoding_on_map]', true, @settings[:enable_geocoding_on_map] %> -

    -

    - <%= content_tag(:label, l(:geocoder_options)) %> - <%= text_area_tag('settings[default_geocoder_options]', - @settings['default_geocoder_options'], - :escape => false, - :rows => 10, - :cols => 100) %> -

    -
    +<%= render_tabs plugin_tabs %> <%= javascript_tag do %> window.gtt_setting() diff --git a/app/views/settings/gtt/_styling.html.erb b/app/views/settings/gtt/_styling.html.erb new file mode 100644 index 00000000..e7294673 --- /dev/null +++ b/app/views/settings/gtt/_styling.html.erb @@ -0,0 +1,22 @@ +
    +

    <%= l(:select_default_tracker_icon) %>

    + +<% Tracker.sorted.each do |t| %> +

    + <%= content_tag :label, t.name %> + <%= select_tag "settings[tracker_#{t.id}]", "".html_safe %> + +

    +<% end %> +
    + +
    +

    <%= l(:select_default_status_color) %>

    + +<% IssueStatus.sorted.each do |t| %> +

    + <%= content_tag :label, t.name %> + <%= color_field_tag "settings[status_#{t.id}]", @settings["status_#{t.id}"] %> +

    +<% end %> +
    diff --git a/config/locales/de.yml b/config/locales/de.yml index 4382e299..3b654be0 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -85,3 +85,6 @@ de: cancel: Abbrechen gtt_map_rotate_label: Kartenrotation gtt_map_rotate_info_html: Hold down Shift+Alt and drag the map to rotate. + gtt_settings_label_general: "General" + gtt_settings_label_styling: "Styling" + gtt_settings_label_geocoder: "Geocoder" diff --git a/config/locales/en.yml b/config/locales/en.yml index e91b089a..5f84495e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -62,6 +62,10 @@ en: gtt_settings_general_maxzoom_level: "Default map maximum zoom level" gtt_settings_general_fit_maxzoom_level: "Default map fit maximum zoom level" + gtt_settings_label_general: "General" + gtt_settings_label_styling: "Styling" + gtt_settings_label_geocoder: "Geocoder" + # gtt_settings_map_url: "API Service URL" # gtt_settings_map_apikey: "API Key" # gtt_settings_map_maxzoom_level: "Maximum map zoom level" diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 56867f95..34d29503 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -57,6 +57,10 @@ ja: gtt_settings_general_maxzoom_level: "既定の最大地図ズームレベル" gtt_settings_general_fit_maxzoom_level: "フィット時最大地図ズームレベル" + gtt_settings_label_general: "General" + gtt_settings_label_styling: "Styling" + gtt_settings_label_geocoder: "Geocoder" + # gtt_settings_map_url: "地図APIサービスのURL" # gtt_settings_map_apikey: "地図APIキー" # gtt_settings_map_maxzoom_level: "最大地図ズームレベル" From 49242fe13168c1ef938a26c37596951126ca6986 Mon Sep 17 00:00:00 2001 From: Daniel Kastl Date: Mon, 10 Apr 2023 22:10:54 +0900 Subject: [PATCH 022/109] Small improvements, adds documentation Signed-off-by: Daniel Kastl --- src/@types/window.d.ts | 58 +++++++++++++++++++++- src/components/gtt-client/GttClient.ts | 57 ++++++++++----------- src/components/gtt-client/constants.ts | 8 ++- src/components/gtt-client/index.ts | 12 +++-- src/components/gtt-client/init/contents.ts | 15 ++++++ src/components/gtt-client/init/defaults.ts | 17 ++++++- src/components/gtt-client/init/map.ts | 51 ++++++++++++------- src/components/gtt-client/interfaces.ts | 19 ++++--- src/index.ts | 17 +++++++ 9 files changed, 187 insertions(+), 67 deletions(-) diff --git a/src/@types/window.d.ts b/src/@types/window.d.ts index c1c302f6..672a2e0a 100644 --- a/src/@types/window.d.ts +++ b/src/@types/window.d.ts @@ -1,27 +1,81 @@ declare global { interface Window { - // Redmine functions + /** + * Redmine functions + */ + + // An object containing available filters. availableFilters: any; + + // An object containing operators by type. operatorByType: any; + + // An object containing operator labels. operatorLabels: any; + + /** + * Toggles the operator for a given field. + * @param field - The field for which the operator will be toggled. + */ toggleOperator(field: any): void; + + /** + * Shows a modal with the given ID, width, and optional title. + * @param id - The ID of the modal to be shown. + * @param width - The width of the modal. + * @param title - The optional title of the modal. + */ showModal(id: string, width: string, title?: string): void; + + /** + * Builds a filter row without distance filter. + * @param field - The field for the filter row. + * @param operator - The operator for the filter row. + * @param values - The values for the filter row. + */ buildFilterRowWithoutDistanceFilter( field: any, operator: any, values: any ): void; + + /** + * Builds a filter row. + * @param field - The field for the filter row. + * @param operator - The operator for the filter row. + * @param values - The values for the filter row. + */ buildFilterRow(field: any, operator: any, values: any): void; + + /** + * Replaces the issue form with the given HTML. + * @param html - The HTML to replace the issue form with. + */ replaceIssueFormWith(html: any): void; + + /** + * Replaces the issue form with the given HTML and initializes the map. + * @param html - The HTML to replace the issue form with. + */ replaceIssueFormWithInitMap(html: any): void; - // Gtt functions + /** + * Gtt functions + */ + + /** + * Creates a GttClient instance for the given target. + * @param target - The HTMLDivElement for which the GttClient will be created. + */ createGttClient(target: HTMLDivElement): void; + + // A function to handle GTT settings. gtt_setting(): void; } } export {}; // This ensures this file is treated as a module + /** * Redmine functions */ diff --git a/src/components/gtt-client/GttClient.ts b/src/components/gtt-client/GttClient.ts index aa5ca4b0..184ed5f5 100644 --- a/src/components/gtt-client/GttClient.ts +++ b/src/components/gtt-client/GttClient.ts @@ -1,63 +1,58 @@ import { Map, Geolocation } from 'ol'; import { Geometry } from 'ol/geom'; -import { Layer, Vector as VectorLayer } from 'ol/layer'; +import { Vector as VectorLayer } from 'ol/layer'; import { Vector as VectorSource } from 'ol/source'; -import Bar from 'ol-ext/control/Bar'; import { IGttClientOption, IFilterOption } from './interfaces'; -import { initDefaults } from './init/defaults'; -import { initContents } from './init/contents'; +import { initDefaults, initFilters } from './init/defaults'; +import { initContents, setTabIndex } from './init/contents'; import { initMap } from './init/map'; import { initLayers } from './init/layers'; import { initControls } from './init/controls'; import { initEventListeners } from './init/events'; +/** + * GttClient is a class representing a geospatial application client. + * It initializes and manages map instances, map-related settings, + * layers, controls, and event listeners. + */ export default class GttClient { readonly map: Map; maps: Array; - layerArray: Layer[]; defaults: DOMStringMap; contents: DOMStringMap; i18n: any; - toolbar: Bar; filters: IFilterOption; vector: VectorLayer>; bounds: VectorLayer>; geolocations: Array; - constructor(options: IGttClientOption) { - if (!options.target) { - return; - } - - // For map div focus settings - if (options.target) { - if (options.target.getAttribute('tabindex') == null) { - options.target.setAttribute('tabindex', '0') - } - } - - this.filters = { - location: false, - distance: false, - }; + /** + * Constructs a new GttClient instance. + * @param target - The HTMLElement where the map will be rendered. + */ + constructor({ target }: IGttClientOption) { + if (!target) return; + + // Set tabindex for map div focus settings + setTabIndex(target); + + // Initialize class properties this.maps = []; this.geolocations = []; - this.defaults = initDefaults(); - this.contents = initContents(options.target); + this.filters = initFilters(); + this.contents = initContents(target); this.i18n = JSON.parse(this.defaults.i18n); - this.map = initMap(options.target, this.i18n); - - this.layerArray = initLayers.call(this); - + // Initialize map, layers, controls, and event listeners + this.map = initMap(target, this.i18n); + initLayers.call(this); initControls.call(this); - initEventListeners.call(this); - // Handle multiple maps per page - this.maps.push(this.map) + // Add the initialized map to the maps array + this.maps.push(this.map); } } diff --git a/src/components/gtt-client/constants.ts b/src/components/gtt-client/constants.ts index f98fd326..87a9013a 100644 --- a/src/components/gtt-client/constants.ts +++ b/src/components/gtt-client/constants.ts @@ -1,4 +1,6 @@ -// Define an interface named Constants which enforces readonly properties for lon, lat, zoom, maxzoom, fitMaxzoom and geocoder. +// Define an interface named Constants which enforces readonly properties for +// lon, lat, zoom, maxzoom, fitMaxzoom, and geocoder. This interface is used to +// ensure that the 'constants' object conforms to a specific structure. export interface Constants { readonly lon: number; readonly lat: number; @@ -8,7 +10,9 @@ export interface Constants { readonly geocoder: Record; } -// Define a constant object named constants of type Constants and specify its values. +// Define a constant object named 'constants' of type Constants and specify its values. +// This object holds default values for map-related settings, such as longitude, +// latitude, zoom levels, and geocoder settings. export const constants: Constants = { lon: 139.691706, lat: 35.689524, diff --git a/src/components/gtt-client/index.ts b/src/components/gtt-client/index.ts index d4e9745b..66a5ce0d 100644 --- a/src/components/gtt-client/index.ts +++ b/src/components/gtt-client/index.ts @@ -1,10 +1,12 @@ -// This line exports all the members from the redmine module file. +// Export all members from the 'redmine' module file. export * from './redmine'; -// import 'ol/ol.css'; -// import 'ol-ext/dist/ol-ext.min.css'; -// import 'ol-ext/filter/Base'; +// Import OpenLayers and OpenLayers-Extensions styles +import 'ol/ol.css'; +import 'ol-ext/dist/ol-ext.min.css'; -// This line imports the GttClient class from the gtt-client-class module file and then re-exports it as the default export of this module file. +// Import the GttClient class from the 'GttClient' module file and re-export +// it as the default export of this module file. This allows other modules +// to import the GttClient class directly from this file. import GttClient from './GttClient'; export { GttClient }; diff --git a/src/components/gtt-client/init/contents.ts b/src/components/gtt-client/init/contents.ts index 1de18a10..15d6b1d8 100644 --- a/src/components/gtt-client/init/contents.ts +++ b/src/components/gtt-client/init/contents.ts @@ -1,3 +1,18 @@ +/** + * Retrieves the dataset of the given target HTMLElement. + * @param target - The HTMLElement for which the dataset will be returned. + * @returns - The dataset of the given target HTMLElement. + */ export function initContents(target: HTMLElement): DOMStringMap { return target.dataset; } + +/** + * Sets the tabindex attribute of the given target HTMLElement to '0' if it's not already set. + * @param target - The HTMLElement for which the tabindex attribute will be set. + */ +export function setTabIndex(target: HTMLElement): void { + if (target.getAttribute('tabindex') === null) { + target.setAttribute('tabindex', '0'); + } +} diff --git a/src/components/gtt-client/init/defaults.ts b/src/components/gtt-client/init/defaults.ts index fd038ac7..64a4fb9c 100644 --- a/src/components/gtt-client/init/defaults.ts +++ b/src/components/gtt-client/init/defaults.ts @@ -1,5 +1,12 @@ import { constants } from '../constants'; +import { IFilterOption } from '../interfaces'; +/** + * Initializes default settings by retrieving the dataset from the element with the + * ID 'gtt-defaults'. If the dataset doesn't have a value for a property, a default + * value from the 'constants' object is used. + * @returns - An object containing the initialized default settings. + */ export function initDefaults(): DOMStringMap { const gtt_defaults = document.querySelector('#gtt-defaults') as HTMLDivElement; if (!gtt_defaults) { @@ -10,10 +17,18 @@ export function initDefaults(): DOMStringMap { // Set default values for missing properties Object.entries(constants).forEach(([key, value]) => { - if (defaults[key] === null || defaults[key] === undefined) { + if (!defaults[key]) { defaults[key] = value.toString(); } }); return defaults; } + +/** + * Initializes filter options with default values. + * @returns - An object containing the initialized filter options. + */ +export function initFilters(): IFilterOption { + return { location: false, distance: false }; +} diff --git a/src/components/gtt-client/init/map.ts b/src/components/gtt-client/init/map.ts index 56bc9cab..5594d06e 100644 --- a/src/components/gtt-client/init/map.ts +++ b/src/components/gtt-client/init/map.ts @@ -1,27 +1,40 @@ import { Map } from 'ol'; -import { defaults as interactions_defaults, MouseWheelZoom } from 'ol/interaction'; -import { focus as events_condifition_focus } from 'ol/events/condition'; -import { defaults as control_defaults } from 'ol/control'; +import { defaults as interactionsDefaults, MouseWheelZoom } from 'ol/interaction'; +import { focus as eventsConditionFocus } from 'ol/events/condition'; +import { defaults as controlDefaults } from 'ol/control'; +/** + * Initializes a new OpenLayers Map instance with custom interactions and controls. + * @param target - The HTMLElement where the map will be rendered. + * @param i18n - An object containing translations for user interface elements. + * @returns - The initialized OpenLayers Map instance. + */ export function initMap(target: HTMLElement, i18n: any): Map { + // Define custom interactions + const interactions = interactionsDefaults({ mouseWheelZoom: false }).extend([ + new MouseWheelZoom({ + constrainResolution: true, // force zooming to an integer zoom + condition: eventsConditionFocus, // only wheel/trackpad zoom when the map has the focus + }), + ]); + + // Define custom controls + const controls = controlDefaults({ + rotateOptions: {}, + attributionOptions: { + collapsible: false, + }, + zoomOptions: { + zoomInTipLabel: i18n.control.zoom_in, + zoomOutTipLabel: i18n.control.zoom_out, + }, + }); + + // Initialize the map with custom interactions and controls const map = new Map({ target, - interactions: interactions_defaults({ mouseWheelZoom: false }).extend([ - new MouseWheelZoom({ - constrainResolution: true, // force zooming to a integer zoom - condition: events_condifition_focus, // only wheel/trackpad zoom when the map has the focus - }), - ]), - controls: control_defaults({ - rotateOptions: {}, - attributionOptions: { - collapsible: false, - }, - zoomOptions: { - zoomInTipLabel: i18n.control.zoom_in, - zoomOutTipLabel: i18n.control.zoom_out, - }, - }), + interactions, + controls, }); return map; diff --git a/src/components/gtt-client/interfaces.ts b/src/components/gtt-client/interfaces.ts index 95d559fb..8871feb3 100644 --- a/src/components/gtt-client/interfaces.ts +++ b/src/components/gtt-client/interfaces.ts @@ -1,13 +1,14 @@ -// types.ts import { Tile, Image, VectorTile as VTLayer } from 'ol/layer'; import { OSM, XYZ, TileWMS, ImageWMS, VectorTile as VTSource } from 'ol/source'; -// Interface for options used in creating a new instance of GttClient +// Interface for options used when creating a new instance of GttClient. +// Specifies the target HTML element for the map. export interface IGttClientOption { target: HTMLDivElement | null; } -// Interface describing a layer object used in GttClient +// Interface describing a layer object used in GttClient. +// Contains information about the layer type, id, name, whether it's a base layer, and additional options. export interface ILayerObject { type: string; id: number; @@ -16,27 +17,31 @@ export interface ILayerObject { options: object; } -// Interface for filtering options used in GttClient +// Interface for filtering options used in GttClient. +// Specifies whether location and distance filters are enabled. export interface IFilterOption { location: boolean; distance: boolean; } -// Interface for describing a tile layer source used in GttClient +// Interface for describing a tile layer source used in GttClient. +// Defines the layer and source types for tile-based layers (e.g., OSM, XYZ, WMS). export interface ITileLayerSource { layer: typeof Tile; source: typeof OSM | typeof XYZ | typeof TileWMS; type: string; } -// Interface for describing an image layer source used in GttClient +// Interface for describing an image layer source used in GttClient. +// Defines the layer and source types for image-based layers (e.g., ImageWMS). export interface IImageLayerSource { layer: typeof Image; source: typeof ImageWMS; type: string; } -// Interface for describing a vector tile layer source used in GttClient +// Interface for describing a vector tile layer source used in GttClient. +// Defines the layer and source types for vector tile layers (e.g., VectorTile). export interface IVTLayerSource { layer: typeof VTLayer; source: typeof VTSource; diff --git a/src/index.ts b/src/index.ts index 20551e50..7b293d1e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,18 @@ // Import stylesheets +// OpenLayers core stylesheet import 'ol/ol.css'; + +// OpenLayers extensions stylesheet import 'ol-ext/dist/ol-ext.min.css'; + +// Custom app styles import './stylesheets/app.scss'; + +// Custom icons import './stylesheets/custom-icons.css'; import './stylesheets/custom-icons-def.js'; + +// Material Design icons import '@material-design-icons/font/filled.css'; import './stylesheets/material-design-def.js'; @@ -12,10 +21,18 @@ import { GttClient } from './components/gtt-client'; import { gtt_setting } from './components/gtt-setting'; // Define functions to attach to the global window object + +/** + * Creates a GttClient instance for the given target. + * @param target - The HTMLDivElement for which the GttClient will be created. + */ function createGttClient(target: HTMLDivElement) { new GttClient({ target }); } +/** + * Attaches GTT settings. + */ function attachGttSetting() { gtt_setting(); } From a65d9d6aa43541d291cb3a00b25c645f98e96cf5 Mon Sep 17 00:00:00 2001 From: Daniel Kastl Date: Mon, 10 Apr 2023 22:28:40 +0900 Subject: [PATCH 023/109] breaks initialization into smaller functions, adds documentation Signed-off-by: Daniel Kastl --- src/components/gtt-client/init/controls.ts | 121 ++++++++++++------- src/components/gtt-client/init/events.ts | 130 ++++++++++++--------- 2 files changed, 156 insertions(+), 95 deletions(-) diff --git a/src/components/gtt-client/init/controls.ts b/src/components/gtt-client/init/controls.ts index fb754124..767cb9f1 100644 --- a/src/components/gtt-client/init/controls.ts +++ b/src/components/gtt-client/init/controls.ts @@ -9,64 +9,103 @@ import { setGeocoding } from "../geocoding"; import { radiansToDegrees, degreesToRadians, parseHistory } from "../helpers"; import { zoomToExtent, setGeolocation, setView, setControls, setPopover } from "../openlayers"; -export function initControls(this: any): void { +/** + * Adds the toolbar and basic controls to the map instance. + * @param {any} instance - The GttClient instance. + */ +function addToolbarAndControls(instance: any): void { + instance.toolbar = new Bar(); + instance.toolbar.setPosition('bottom-left' as position); + instance.map.addControl(instance.toolbar); - // Add Toolbar - this.toolbar = new Bar() - this.toolbar.setPosition('bottom-left' as position) - this.map.addControl(this.toolbar) - setView.call(this) - setGeocoding.call(this, this.map) - setGeolocation.call(this, this.map) - parseHistory.call(this) + setView.call(instance); + setGeocoding.call(instance, instance.map); + setGeolocation.call(instance, instance.map); + parseHistory.call(instance); +} - this.map.addControl (new FullScreen({ - tipLabel: this.i18n.control.fullscreen - })) - this.map.addControl (new Rotate({ - tipLabel: this.i18n.control.rotate - })) +/** + * Adds the FullScreen and Rotate controls to the map instance. + * @param {any} instance - The GttClient instance. + */ +function addFullScreenAndRotateControls(instance: any): void { + instance.map.addControl(new FullScreen({ + tipLabel: instance.i18n.control.fullscreen + })); - // Control button + instance.map.addControl(new Rotate({ + tipLabel: instance.i18n.control.rotate + })); +} + +/** + * Adds the maximize control button to the toolbar. + * @param {any} instance - The GttClient instance. + */ +function addMaximizeControl(instance: any): void { const maximizeCtrl = new Button({ - html: 'zoom_out_map', - title: this.i18n.control.maximize, + html: 'zoom_out_map', + title: instance.i18n.control.maximize, handleClick: () => { - zoomToExtent.call(this, true); + zoomToExtent.call(instance, true); } - }) - this.toolbar.addControl(maximizeCtrl) + }); - // Map rotation - const rotation_field = document.querySelector('#gtt_configuration_map_rotation') as HTMLInputElement - if (rotation_field !== null) { - this.map.getView().on('change:rotation', (evt: any) => { - rotation_field.value = String(Math.round(radiansToDegrees(evt.target.getRotation()))) - }) + instance.toolbar.addControl(maximizeCtrl); +} - rotation_field.addEventListener("input", (evt: any) => { +/** + * Handles the map rotation functionality. + * @param {any} instance - The GttClient instance. + */ +function handleMapRotation(instance: any): void { + const rotationField = document.querySelector('#gtt_configuration_map_rotation') as HTMLInputElement; + + if (rotationField !== null) { + instance.map.getView().on('change:rotation', (evt: any) => { + rotationField.value = String(Math.round(radiansToDegrees(evt.target.getRotation()))); + }); + + rotationField.addEventListener("input", (evt: any) => { const { target } = evt; if (!(target instanceof HTMLInputElement)) { return; } const value = target.value; - this.map.getView().setRotation(degreesToRadians(parseInt(value))) - }) + instance.map.getView().setRotation(degreesToRadians(parseInt(value))); + }); } +} + +/** + * Adds either a LayerSwitcher or LayerPopup control to the map instance. + * @param {any} instance - The GttClient instance. + */ +function addLayerSwitcherOrPopup(instance: any): void { + if (instance.containsOverlay) { + instance.map.addControl(new LayerSwitcher({ + reordering: false + })); + } else { + instance.map.addControl(new LayerPopup()); + } +} + +/** + * Initializes the controls for the GttClient instance. + * @this {any} - The GttClient instance. + */ +export function initControls(this: any): void { + addToolbarAndControls(this); + addFullScreenAndRotateControls(this); + addMaximizeControl(this); + handleMapRotation(this); if (this.contents.edit) { - setControls.call(this, this.contents.edit.split(' ')) + setControls.call(this, this.contents.edit.split(' ')); } else if (this.contents.popup) { - setPopover.call(this) + setPopover.call(this); } - // Add LayerSwitcher Image Toolbar - if( this.containsOverlay) { - this.map.addControl(new LayerSwitcher({ - reordering: false - })) - } - else { - this.map.addControl(new LayerPopup()) - } + addLayerSwitcherOrPopup(this); } diff --git a/src/components/gtt-client/init/events.ts b/src/components/gtt-client/init/events.ts index f00d5516..bd22da18 100644 --- a/src/components/gtt-client/init/events.ts +++ b/src/components/gtt-client/init/events.ts @@ -3,94 +3,116 @@ import { ResizeObserver } from '@juggle/resize-observer'; import { updateFilter } from "../helpers"; import { zoomToExtent, toggleAndLoadMap } from "../openlayers"; +/** + * Initialize event listeners for the GttClient instance. + */ export function initEventListeners(this: any): void { + handlePostRender.call(this); + handleCollapsed.call(this); + handleResize.call(this); + handleIssueSelection.call(this); + handleEditIcon.call(this); + handleGttTabActivation.call(this); + handleFilters.call(this); +} - // Fix empty map issue +// Handle postrender event to fix empty map issue +function handlePostRender(this: any): void { this.map.once('postrender', (evt: any) => { - zoomToExtent.call(this, true) - }) + zoomToExtent.call(this, true); + }); +} - // Zoom to extent when map collapsed => expended +// Observe map element to zoom to extent when map collapsed => expended +function handleCollapsed(this: any): void { if (this.contents.collapsed) { - const self = this const collapsedObserver = new MutationObserver((mutations) => { - // const currentMap = this.map - mutations.forEach(function(mutation) { + mutations.forEach((mutation) => { if (mutation.attributeName !== 'style') { - return + return; } - const mapDiv = mutation.target as HTMLDivElement + const mapDiv = mutation.target as HTMLDivElement; if (mapDiv && mapDiv.style.display === 'block') { - zoomToExtent.call(this, true) - collapsedObserver.disconnect() + zoomToExtent.call(this, true); + collapsedObserver.disconnect(); } - }) - }) - collapsedObserver.observe(self.map.getTargetElement(), { attributes: true, attributeFilter: ['style'] }) + }); + }); + collapsedObserver.observe(this.map.getTargetElement(), { attributes: true, attributeFilter: ['style'] }); } +} - // Sidebar hack +// Handle map resizing for multiple maps +function handleResize(this: any): void { const resizeObserver = new ResizeObserver((entries, observer) => { this.maps.forEach((m: any) => { - m.updateSize() - }) - }) - resizeObserver.observe(this.map.getTargetElement()) + m.updateSize(); + }); + }); + resizeObserver.observe(this.map.getTargetElement()); +} - // When one or more issues is selected, zoom to selected map features +// Handle issue selection to zoom to selected map features +function handleIssueSelection(this: any): void { document.querySelectorAll('table.issues tbody tr').forEach((element: HTMLTableRowElement) => { element.addEventListener('click', (evt) => { - const currentTarget = evt.currentTarget as HTMLTableRowElement - const id = currentTarget.id.split('-')[1] - const feature = this.vector.getSource().getFeatureById(id) + const currentTarget = evt.currentTarget as HTMLTableRowElement; + const id = currentTarget.id.split('-')[1]; + const feature = this.vector.getSource().getFeatureById(id); this.map.getView().fit(feature.getGeometry().getExtent(), { - size: this.map.getSize() - }) - }) - }) + size: this.map.getSize(), + }); + }); + }); +} - // Need to update size of an invisible map, when the editable form is made - // visible. This doesn't look like a good way to do it, but this is more of - // a Redmine problem +// Handle edit icon click to update map size when the editable form is made visible +function handleEditIcon(this: any): void { document.querySelectorAll('div.contextual a.icon-edit').forEach((element: HTMLAnchorElement) => { element.addEventListener('click', () => { setTimeout(() => { this.maps.forEach((m: any) => { - m.updateSize() - }) - zoomToExtent.call(this) - }, 200) - }) - }) + m.updateSize(); + }); + zoomToExtent.call(this); + }, 200); + }); + }); +} - // Redraw the map, when a GTT Tab gets activated +// Handle GTT tab activation to redraw the map +function handleGttTabActivation(this: any): void { document.querySelectorAll('#tab-gtt').forEach((element) => { element.addEventListener('click', () => { this.maps.forEach((m: any) => { - m.updateSize() - }) - zoomToExtent.call(this) - }) - }) + m.updateSize(); + }); + zoomToExtent.call(this); + }); + }); +} - // Because Redmine filter functions are applied later, the Window onload - // event provides a workaround to have filters loaded before executing - // the following code +// Handle map filters and load event listeners +function handleFilters(this: any): void { window.addEventListener('load', () => { + // Check if location filter is available if (document.querySelectorAll('tr#tr_bbox').length > 0) { - this.filters.location = true + this.filters.location = true; } + // Check if distance filter is available if (document.querySelectorAll('tr#tr_distance').length > 0) { - this.filters.distance = true + this.filters.distance = true; } - const legend = document.querySelector('fieldset#location legend') as HTMLLegendElement + // Set up click event listener for location filter legend + const legend = document.querySelector('fieldset#location legend') as HTMLLegendElement; if (legend) { legend.addEventListener('click', (evt) => { - const element = evt.currentTarget as HTMLLegendElement - toggleAndLoadMap(element) - }) + const element = evt.currentTarget as HTMLLegendElement; + toggleAndLoadMap(element); + }); } - zoomToExtent.call(this) - this.map.on('moveend', updateFilter.bind(this)) - }) + // Call zoomToExtent and updateFilter functions + zoomToExtent.call(this); + this.map.on('moveend', updateFilter.bind(this)); + }); } From d72ee8f8d826d239a47aea783dcb3144563bbe9f Mon Sep 17 00:00:00 2001 From: Daniel Kastl Date: Mon, 10 Apr 2023 23:10:30 +0900 Subject: [PATCH 024/109] Breaks layer initialisation into functions Signed-off-by: Daniel Kastl --- src/components/gtt-client/init/layers.ts | 255 ++++++++++++----------- 1 file changed, 131 insertions(+), 124 deletions(-) diff --git a/src/components/gtt-client/init/layers.ts b/src/components/gtt-client/init/layers.ts index 0ccb12d1..10cd04d3 100644 --- a/src/components/gtt-client/init/layers.ts +++ b/src/components/gtt-client/init/layers.ts @@ -19,121 +19,129 @@ import { getLayerSource, setBasemap, getStyle } from "../openlayers"; export function initLayers(this: any): Layer[] { this.layerArray = []; - let features: Feature[] | null = null + const features = readGeoJSONFeatures.call(this); + reloadFontSymbol.call(this); + updateForm(this, features); + + if (this.contents.layers) { + createLayers.call(this); + addLayersToMap.call(this); + } + + setBasemap.call(this); + addBoundsLayer.call(this); + addVectorLayer.call(this, features); + renderProjectBoundary.call(this); + + return this.layerArray; +} + +function readGeoJSONFeatures(this: any): Feature[] | null { if (this.contents.geom && this.contents.geom !== null && this.contents.geom !== 'null') { - features = new GeoJSON().readFeatures( + return new GeoJSON().readFeatures( JSON.parse(this.contents.geom), { featureProjection: 'EPSG:3857' } - ) + ); } + return null; +} - // Fix FireFox unloaded font issue - reloadFontSymbol.call(this) +function createLayers(this: any): void { + const layers = JSON.parse(this.contents.layers) as [ILayerObject]; + layers.forEach((layer) => { + const s = layer.type.split('.'); + const layerSource = getLayerSource(s[1], s[2]); + const l = createLayer(layer, layerSource); + + if (l) { + setLayerProperties(l, layer); + handleLayerVisibilityChange(l, layer); + this.layerArray.push(l); + } + }, this); +} - // TODO: this is only necessary because setting the initial form value - // through the template causes encoding problems - updateForm(this, features) +function createLayer(layer: ILayerObject, layerSource: ITileLayerSource | IImageLayerSource | IVTLayerSource): Layer | null { + switch (layerSource.type) { + case "TileLayerSource": + return createTileLayer(layer, layerSource as ITileLayerSource); + case "ImageLayerSource": + return createImageLayer(layer, layerSource as IImageLayerSource); + case "VTLayerSource": + return createVTLayer(layer, layerSource as IVTLayerSource); + default: + return null; + } +} - if (this.contents.layers) { - const layers = JSON.parse(this.contents.layers) as [ILayerObject] - layers.forEach((layer) => { - const s = layer.type.split('.') - const layerSource = getLayerSource(s[1], s[2]) - if ( layerSource.type === "TileLayerSource") { - const config = layerSource as ITileLayerSource - const l = new (config.layer)({ - visible: false, - source: new (config.source)(layer.options as any) - }) - - l.set('lid', layer.id) - l.set('title', layer.name) - l.set('baseLayer', layer.baselayer) - if( layer.baselayer ) { - l.on('change:visible', e => { - const target = e.target as Tile - if (target.getVisible()) { - const lid = target.get('lid') - document.cookie = `_redmine_gtt_basemap=${lid};path=/` - } - }) - } - this.layerArray.push(l) - } else if (layerSource.type === "ImageLayerSource") { - const config = layerSource as IImageLayerSource - const l = new (config.layer)({ - visible: false, - source: new (config.source)(layer.options as ImageWMSOptions) - }) - - l.set('lid', layer.id) - l.set('title', layer.name) - l.set('baseLayer', layer.baselayer) - if( layer.baselayer ) { - l.on('change:visible', e => { - const target = e.target as Image - if (target.getVisible()) { - const lid = target.get('lid') - document.cookie = `_redmine_gtt_basemap=${lid};path=/` - } - }) - } - this.layerArray.push(l) - } else if (layerSource.type === "VTLayerSource") { - const config = layerSource as IVTLayerSource - const options = layer.options as VectorTileOptions - options.format = new MVT() - const l = new (config.layer)({ - visible: false, - source: new (config.source)(options), - declutter: true - }) as VTLayer - - // Apply style URL if provided - if ("styleUrl" in options) { - applyStyle(l,options.styleUrl) - } - - l.set('lid', layer.id) - l.set('title', layer.name) - l.set('baseLayer', layer.baselayer) - if( layer.baselayer ) { - l.on('change:visible', (e: { target: any }) => { - const target = e.target as any - if (target.getVisible()) { - const lid = target.get('lid') - document.cookie = `_redmine_gtt_basemap=${lid};path=/` - } - }) - } - this.layerArray.push(l) - } - }, this) - - /** - * Ordering the Layers for the LayerSwitcher Control. - * BaseLayers are added first. - */ - this.layerArray.forEach( (l:Layer) => { - if( l.get("baseLayer") ) { - this.map.addLayer(l) - } - }) +function createTileLayer(layer: ILayerObject, config: ITileLayerSource): Tile { + return new (config.layer)({ + visible: false, + source: new (config.source)(layer.options as any) + }); +} - this.containsOverlay = false; +function createImageLayer(layer: ILayerObject, config: IImageLayerSource): Image { + return new (config.layer)({ + visible: false, + source: new (config.source)(layer.options as ImageWMSOptions) + }); +} + +function createVTLayer(layer: ILayerObject, config: IVTLayerSource): VTLayer { + const options = layer.options as VectorTileOptions; + options.format = new MVT(); + const l = new (config.layer)({ + visible: false, + source: new (config.source)(options), + declutter: true + }) as VTLayer; + + // Apply style URL if provided + if ("styleUrl" in options) { + applyStyle(l, options.styleUrl); + } + + return l; +} - this.layerArray.forEach( (l:Layer) => { - if( !l.get("baseLayer") ) { - this.map.addLayer(l) - this.containsOverlay = true +function setLayerProperties(layer: Layer, layerObject: ILayerObject): void { + layer.set('lid', layerObject.id); + layer.set('title', layerObject.name); + layer.set('baseLayer', layerObject.baselayer); +} + +function handleLayerVisibilityChange(layer: Layer, layerObject: ILayerObject): void { + if (layerObject.baselayer) { + layer.on('change:visible', e => { + const target = e.target as Layer; + if (target.getVisible()) { + const lid = target.get('lid'); + document.cookie = `_redmine_gtt_basemap=${lid};path=/`; } - }) + }); } +} + +function addLayersToMap(this: any): void { + this.layerArray.forEach((l: Layer) => { + if (l.get("baseLayer")) { + this.map.addLayer(l); + } + }); - setBasemap.call(this) + this.containsOverlay = false; - // Layer for project boundary + this.layerArray.forEach((l: Layer) => { + if (!l.get("baseLayer")) { + this.map.addLayer(l); + this.containsOverlay = true; + } + }); +} + +function addBoundsLayer(this: any): void { this.bounds = new VectorLayer({ source: new VectorSource(), style: new Style({ @@ -142,18 +150,18 @@ export function initLayers(this: any): Layer[] { }), stroke: new Stroke({ color: 'rgba(220,26,26,0.7)', - // lineDash: [12,1,12], width: 1 }) }) - }) - this.bounds.set('title', 'Boundaries') - this.bounds.set('displayInLayerSwitcher', false) - this.layerArray.push(this.bounds) - this.map.addLayer(this.bounds) - - const yOrdering: unknown = Ordering.yOrdering() + }); + this.bounds.set('title', 'Boundaries'); + this.bounds.set('displayInLayerSwitcher', false); + this.layerArray.push(this.bounds); + this.map.addLayer(this.bounds); +} +function addVectorLayer(this: any, features: Feature[] | null): void { + const yOrdering: unknown = Ordering.yOrdering(); this.vector = new VectorLayer>({ source: new VectorSource({ 'features': features, @@ -161,35 +169,34 @@ export function initLayers(this: any): Layer[] { }), renderOrder: yOrdering as OrderFunction, style: getStyle.bind(this) - }) - this.vector.set('title', 'Features') - this.vector.set('displayInLayerSwitcher', false) - this.layerArray.push(this.vector) - this.map.addLayer(this.vector) + }); + this.vector.set('title', 'Features'); + this.vector.set('displayInLayerSwitcher', false); + this.layerArray.push(this.vector); + this.map.addLayer(this.vector); +} - // Render project boundary if bounds are available +function renderProjectBoundary(this: any): void { if (this.contents.bounds && this.contents.bounds !== null) { const boundary = new GeoJSON().readFeature( this.contents.bounds, { featureProjection: 'EPSG:3857' } - ) - this.bounds.getSource().addFeature(boundary) + ); + this.bounds.getSource().addFeature(boundary); if (this.contents.bounds === this.contents.geom) { - this.vector.setVisible(false) + this.vector.setVisible(false); } - this.layerArray.forEach((layer:Layer) => { + this.layerArray.forEach((layer: Layer) => { if (layer.get('baseLayer')) { layer.addFilter(new Mask({ feature: boundary, inner: false, fill: new Fill({ - color: [220,26,26,.1] + color: [220, 26, 26, 0.1] }) - })) + })); } - }) + }); } - - return this.layerArray; } From 816b2db9de0f63ea715dbe1ded2c5b14598d1df0 Mon Sep 17 00:00:00 2001 From: Daniel Kastl Date: Mon, 10 Apr 2023 23:36:25 +0900 Subject: [PATCH 025/109] Updates dependencies Signed-off-by: Daniel Kastl --- package.json | 4 ++-- yarn.lock | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 398395f1..352afaf5 100644 --- a/package.json +++ b/package.json @@ -33,11 +33,11 @@ "@types/jqueryui": "^1.12.16", "@types/ol-ext": "npm:@siedlerchr/types-ol-ext", "css-loader": "^6.7.3", - "sass": "^1.58.0", + "sass": "^1.61.0", "sass-loader": "^13.2.0", "style-loader": "^3.3.1", "ts-loader": "^9.4.2", - "typescript": "^5.0.3", + "typescript": "^5.0.4", "webpack": "^5.76.0", "webpack-cli": "^5.0.1" } diff --git a/yarn.lock b/yarn.lock index 5e303bdc..b8964675 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1165,10 +1165,10 @@ sass-loader@^13.2.0: klona "^2.0.6" neo-async "^2.6.2" -sass@^1.58.0: - version "1.60.0" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.60.0.tgz#657f0c23a302ac494b09a5ba8497b739fb5b5a81" - integrity sha512-updbwW6fNb5gGm8qMXzVO7V4sWf7LMXnMly/JEyfbfERbVH46Fn6q02BX7/eHTdKpE7d+oTkMMQpFWNUMfFbgQ== +sass@^1.61.0: + version "1.61.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.61.0.tgz#d1f6761bb833887b8fdab32a24e052c40531d02b" + integrity sha512-PDsN7BrVkNZK2+dj/dpKQAWZavbAQ87IXqVvw2+oEYI+GwlTWkvbQtL7F2cCNbMqJEYKPh1EcjSxsnqIb/kyaQ== dependencies: chokidar ">=3.0.0 <4.0.0" immutable "^4.0.0" @@ -1336,10 +1336,10 @@ ts-loader@^9.4.2: micromatch "^4.0.0" semver "^7.3.4" -typescript@^5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.3.tgz#fe976f0c826a88d0a382007681cbb2da44afdedf" - integrity sha512-xv8mOEDnigb/tN9PSMTwSEqAnUvkoXMQlicOb0IUVDBSQCgBSaAAROUZYy2IcUy5qU6XajK5jjjO7TMWqBTKZA== +typescript@^5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.4.tgz#b217fd20119bd61a94d4011274e0ab369058da3b" + integrity sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw== update-browserslist-db@^1.0.10: version "1.0.10" From 7be16181d8f5a6252d37b64a4aba47792c01a5c0 Mon Sep 17 00:00:00 2001 From: Daniel Kastl Date: Tue, 11 Apr 2023 08:12:39 +0900 Subject: [PATCH 026/109] Adds documentation Signed-off-by: Daniel Kastl --- src/components/gtt-client/helpers/index.ts | 54 +++++++++++++++++++- src/components/gtt-client/init/events.ts | 28 ++++++++--- src/components/gtt-client/init/layers.ts | 58 ++++++++++++++++++++++ 3 files changed, 131 insertions(+), 9 deletions(-) diff --git a/src/components/gtt-client/helpers/index.ts b/src/components/gtt-client/helpers/index.ts index 70328730..176a1d94 100644 --- a/src/components/gtt-client/helpers/index.ts +++ b/src/components/gtt-client/helpers/index.ts @@ -7,6 +7,12 @@ import { FeatureLike } from 'ol/Feature'; import FontSymbol from 'ol-ext/style/FontSymbol'; import { transform, transformExtent } from 'ol/proj'; +/** + * Get the value of a cookie by its name. + * + * @param cname - The name of the cookie. + * @returns The value of the cookie or an empty string if not found. + */ export const getCookie = (cname: string): string => { const name = cname + '='; const decodedCookie = decodeURIComponent(document.cookie); @@ -24,13 +30,31 @@ export const getCookie = (cname: string): string => { return ''; }; +/** + * Convert radians to degrees. + * + * @param radians - The value in radians to convert. + * @returns The value in degrees. + */ export const radiansToDegrees = (radians: number): number => { const degrees = radians * (180 / Math.PI); return (degrees + 360) % 360; }; +/** + * Convert degrees to radians. + * + * @param degrees - The value in degrees to convert. + * @returns The value in radians. + */ export const degreesToRadians = (degrees: number): number => degrees * (Math.PI / 180); +/** + * Get the map size, taking into account the possibility of width or height being 0. + * + * @param map - The OpenLayers Map object. + * @returns An array containing the width and height of the map. + */ export const getMapSize = (map: Map): number[] => { const [width, height] = map.getSize(); @@ -42,6 +66,14 @@ export const getMapSize = (map: Map): number[] => { return [width, height]; }; +/** + * Evaluate a comparison between two values with a specified operator. + * + * @param left - The left-hand side value of the comparison. + * @param operator - The operator to use in the comparison. + * @param right - The right-hand side value of the comparison. + * @returns The result of the comparison. + */ export const evaluateComparison = (left: any, operator: any, right: any): any => { if (typeof left == 'object') { left = JSON.stringify(left); @@ -51,6 +83,14 @@ export const evaluateComparison = (left: any, operator: any, right: any): any => } }; +/** + * Get the value of a nested property in an object using a path. + * + * @param obj - The object to get the value from. + * @param path - The path to the property in the object, either as a string or an array of strings. + * @param def - An optional default value to return if the property is not found. + * @returns The value of the property or the default value if not found. + */ export const getObjectPathValue = (obj: any, path: string | Array, def: any = null) => { const pathArr = Array.isArray(path) ? path @@ -58,6 +98,9 @@ export const getObjectPathValue = (obj: any, path: string | Array, def: return pathArr.reduce((acc, key) => acc?.[key], obj) ?? def; }; +/** + * Reload FontSymbol styles on the map. + */ export function reloadFontSymbol() { if ('fonts' in document) { const symbolFonts: Array = [] @@ -93,6 +136,13 @@ export function reloadFontSymbol() { } } +/** + * Update the form with the provided feature data. + * + * @param mapObj - The map object containing settings. + * @param features - The features to update the form with. + * @param updateAddressFlag - A flag to update the address field with reverse geocoding, default is false. + */ export function updateForm(mapObj: any, features: FeatureLike[] | null, updateAddressFlag: boolean = false):void { if (features == null) { return @@ -173,7 +223,7 @@ export function updateForm(mapObj: any, features: FeatureLike[] | null, updateAd } /** - * Updates map settings for Redmine filter + * Update the map settings for the Redmine filter. */ export function updateFilter() { let center = this.map.getView().getCenter() @@ -221,7 +271,7 @@ export function updateFilter() { } /** - * Parse page for WKT strings in history + * Parse the history of the page for WKT strings and replace them with formatted links. */ export function parseHistory() { const historyItems = document.querySelectorAll('div#history ul.details i'); diff --git a/src/components/gtt-client/init/events.ts b/src/components/gtt-client/init/events.ts index bd22da18..82c88a9b 100644 --- a/src/components/gtt-client/init/events.ts +++ b/src/components/gtt-client/init/events.ts @@ -16,14 +16,18 @@ export function initEventListeners(this: any): void { handleFilters.call(this); } -// Handle postrender event to fix empty map issue +/** + * Handles 'postrender' event to fix empty map issue by zooming to extent. + */ function handlePostRender(this: any): void { this.map.once('postrender', (evt: any) => { zoomToExtent.call(this, true); }); } -// Observe map element to zoom to extent when map collapsed => expended +/** + * Observes map element to zoom to extent when map is expanded from a collapsed state. + */ function handleCollapsed(this: any): void { if (this.contents.collapsed) { const collapsedObserver = new MutationObserver((mutations) => { @@ -42,7 +46,9 @@ function handleCollapsed(this: any): void { } } -// Handle map resizing for multiple maps +/** + * Handles map resizing for multiple maps by observing the map target element. + */ function handleResize(this: any): void { const resizeObserver = new ResizeObserver((entries, observer) => { this.maps.forEach((m: any) => { @@ -52,7 +58,9 @@ function handleResize(this: any): void { resizeObserver.observe(this.map.getTargetElement()); } -// Handle issue selection to zoom to selected map features +/** + * Handles issue selection to zoom to selected map features when a table row is clicked. + */ function handleIssueSelection(this: any): void { document.querySelectorAll('table.issues tbody tr').forEach((element: HTMLTableRowElement) => { element.addEventListener('click', (evt) => { @@ -66,7 +74,9 @@ function handleIssueSelection(this: any): void { }); } -// Handle edit icon click to update map size when the editable form is made visible +/** + * Handles the click event on the edit icon to update the map size when the editable form is made visible. + */ function handleEditIcon(this: any): void { document.querySelectorAll('div.contextual a.icon-edit').forEach((element: HTMLAnchorElement) => { element.addEventListener('click', () => { @@ -80,7 +90,9 @@ function handleEditIcon(this: any): void { }); } -// Handle GTT tab activation to redraw the map +/** + * Handles GTT tab activation to redraw the map when the tab is clicked. + */ function handleGttTabActivation(this: any): void { document.querySelectorAll('#tab-gtt').forEach((element) => { element.addEventListener('click', () => { @@ -92,7 +104,9 @@ function handleGttTabActivation(this: any): void { }); } -// Handle map filters and load event listeners +/** + * Handles map filters and load event listeners for updating the map view. + */ function handleFilters(this: any): void { window.addEventListener('load', () => { // Check if location filter is available diff --git a/src/components/gtt-client/init/layers.ts b/src/components/gtt-client/init/layers.ts index 10cd04d3..a5eec168 100644 --- a/src/components/gtt-client/init/layers.ts +++ b/src/components/gtt-client/init/layers.ts @@ -16,6 +16,10 @@ import { ILayerObject, ITileLayerSource, IImageLayerSource, IVTLayerSource } fro import { updateForm, reloadFontSymbol } from "../helpers"; import { getLayerSource, setBasemap, getStyle } from "../openlayers"; +/** + * Initializes layers for the OpenLayers map and adds them to the layerArray. + * @returns {Layer[]} Array of layers added to the map. + */ export function initLayers(this: any): Layer[] { this.layerArray = []; @@ -36,6 +40,10 @@ export function initLayers(this: any): Layer[] { return this.layerArray; } +/** + * Reads GeoJSON features from the provided input. + * @returns {Feature[] | null} Array of GeoJSON features or null if no features found. + */ function readGeoJSONFeatures(this: any): Feature[] | null { if (this.contents.geom && this.contents.geom !== null && this.contents.geom !== 'null') { return new GeoJSON().readFeatures( @@ -47,6 +55,9 @@ function readGeoJSONFeatures(this: any): Feature[] | null { return null; } +/** + * Creates layers based on the input data and adds them to the layerArray. + */ function createLayers(this: any): void { const layers = JSON.parse(this.contents.layers) as [ILayerObject]; layers.forEach((layer) => { @@ -62,6 +73,12 @@ function createLayers(this: any): void { }, this); } +/** + * Creates a Layer object based on the input data and layer source. + * @param {ILayerObject} layer - Layer object data. + * @param {ITileLayerSource | IImageLayerSource | IVTLayerSource} layerSource - Layer source object. + * @returns {Layer | null} Created layer or null if the layer source type is not supported. + */ function createLayer(layer: ILayerObject, layerSource: ITileLayerSource | IImageLayerSource | IVTLayerSource): Layer | null { switch (layerSource.type) { case "TileLayerSource": @@ -75,6 +92,12 @@ function createLayer(layer: ILayerObject, layerSource: ITileLayerSource | IImage } } +/** + * Creates a Tile layer based on the input data and tile layer source configuration. + * @param {ILayerObject} layer - Layer object data. + * @param {ITileLayerSource} config - Tile layer source configuration. + * @returns {Tile} Created Tile layer. + */ function createTileLayer(layer: ILayerObject, config: ITileLayerSource): Tile { return new (config.layer)({ visible: false, @@ -82,6 +105,12 @@ function createTileLayer(layer: ILayerObject, config: ITileLayerSource): Tile} Created Image layer. + */ function createImageLayer(layer: ILayerObject, config: IImageLayerSource): Image { return new (config.layer)({ visible: false, @@ -89,6 +118,12 @@ function createImageLayer(layer: ILayerObject, config: IImageLayerSource): Image }); } +/** + * Creates a VectorTile layer based on the input data and vector tile layer source configuration. + * @param {ILayerObject} layer - Layer object data. + * @param {IVTLayerSource} config - Vector tile layer source configuration. + * @returns {VTLayer} Created VectorTile layer. + */ function createVTLayer(layer: ILayerObject, config: IVTLayerSource): VTLayer { const options = layer.options as VectorTileOptions; options.format = new MVT(); @@ -106,12 +141,22 @@ function createVTLayer(layer: ILayerObject, config: IVTLayerSource): VTLayer { return l; } +/** + * Sets properties for a layer based on the input layer object. + * @param {Layer} layer - Layer object. + * @param {ILayerObject} layerObject - Input layer object data. + */ function setLayerProperties(layer: Layer, layerObject: ILayerObject): void { layer.set('lid', layerObject.id); layer.set('title', layerObject.name); layer.set('baseLayer', layerObject.baselayer); } +/** + * Handles visibility change for a layer and updates the cookie accordingly. + * @param {Layer} layer - Layer object. + * @param {ILayerObject} layerObject - Input layer object data. + */ function handleLayerVisibilityChange(layer: Layer, layerObject: ILayerObject): void { if (layerObject.baselayer) { layer.on('change:visible', e => { @@ -124,6 +169,9 @@ function handleLayerVisibilityChange(layer: Layer, layerObject: ILayerObject): v } } +/** + * Adds layers to the map based on their properties. + */ function addLayersToMap(this: any): void { this.layerArray.forEach((l: Layer) => { if (l.get("baseLayer")) { @@ -141,6 +189,9 @@ function addLayersToMap(this: any): void { }); } +/** + * Adds a bounds layer to the map for rendering boundaries. + */ function addBoundsLayer(this: any): void { this.bounds = new VectorLayer({ source: new VectorSource(), @@ -160,6 +211,10 @@ function addBoundsLayer(this: any): void { this.map.addLayer(this.bounds); } +/** + * Adds a vector layer to the map for rendering GeoJSON features. + * @param {Feature[] | null} features - Array of GeoJSON features or null if no features found. + */ function addVectorLayer(this: any, features: Feature[] | null): void { const yOrdering: unknown = Ordering.yOrdering(); this.vector = new VectorLayer>({ @@ -176,6 +231,9 @@ function addVectorLayer(this: any, features: Feature[] | null): void { this.map.addLayer(this.vector); } +/** + * Renders the project boundary on the map by adding a boundary feature and applying a mask to the base layers. + */ function renderProjectBoundary(this: any): void { if (this.contents.bounds && this.contents.bounds !== null) { const boundary = new GeoJSON().readFeature( From 95ad2c39e02633b5b4e1255e3e1ed2013746b2e0 Mon Sep 17 00:00:00 2001 From: Daniel Kastl Date: Wed, 12 Apr 2023 10:06:39 +0900 Subject: [PATCH 027/109] Removes unused folder Signed-off-by: Daniel Kastl --- lib/tasks/.keep | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 lib/tasks/.keep diff --git a/lib/tasks/.keep b/lib/tasks/.keep deleted file mode 100644 index e69de29b..00000000 From 20e0a5ee35a7be9aa8490a71a2bead55be814744 Mon Sep 17 00:00:00 2001 From: Daniel Kastl Date: Wed, 12 Apr 2023 10:06:53 +0900 Subject: [PATCH 028/109] Updates dependencies Signed-off-by: Daniel Kastl --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 352afaf5..b2e56d32 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "@types/jqueryui": "^1.12.16", "@types/ol-ext": "npm:@siedlerchr/types-ol-ext", "css-loader": "^6.7.3", - "sass": "^1.61.0", + "sass": "^1.62.0", "sass-loader": "^13.2.0", "style-loader": "^3.3.1", "ts-loader": "^9.4.2", diff --git a/yarn.lock b/yarn.lock index b8964675..7b54de8b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1165,10 +1165,10 @@ sass-loader@^13.2.0: klona "^2.0.6" neo-async "^2.6.2" -sass@^1.61.0: - version "1.61.0" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.61.0.tgz#d1f6761bb833887b8fdab32a24e052c40531d02b" - integrity sha512-PDsN7BrVkNZK2+dj/dpKQAWZavbAQ87IXqVvw2+oEYI+GwlTWkvbQtL7F2cCNbMqJEYKPh1EcjSxsnqIb/kyaQ== +sass@^1.62.0: + version "1.62.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.62.0.tgz#3686b2195b93295d20765135e562366b33ece37d" + integrity sha512-Q4USplo4pLYgCi+XlipZCWUQz5pkg/ruSSgJ0WRDSb/+3z9tXUOkQ7QPYn4XrhZKYAK4HlpaQecRwKLJX6+DBg== dependencies: chokidar ">=3.0.0 <4.0.0" immutable "^4.0.0" From 7edc01ae0173b0cf1e316a7908a98112087e4f28 Mon Sep 17 00:00:00 2001 From: Daniel Kastl Date: Thu, 13 Apr 2023 01:10:53 +0900 Subject: [PATCH 029/109] Refactors map_layer Signed-off-by: Daniel Kastl --- .../gtt_configuration_controller.rb | 126 +++++----- app/controllers/gtt_map_layers_controller.rb | 70 ++++++ .../gtt_tile_sources_controller.rb | 67 ------ app/helpers/gtt_map_layers_helper.rb | 13 + app/helpers/gtt_tile_sources_helper.rb | 13 - app/models/gtt_configuration.rb | 8 +- app/models/gtt_map_layer.rb | 59 +++++ app/models/gtt_tile_source.rb | 34 --- app/views/gtt_map_layers/_form.html.erb | 225 ++++++++++++++++++ app/views/gtt_map_layers/_map_layer.html.erb | 12 + app/views/gtt_map_layers/edit.html.erb | 10 + app/views/gtt_map_layers/index.html.erb | 30 +++ app/views/gtt_map_layers/new.html.erb | 10 + app/views/gtt_map_layers/new.js.erb | 1 + app/views/gtt_tile_sources/_form.html.erb | 67 ------ .../gtt_tile_sources/_tile_source.html.erb | 13 - app/views/gtt_tile_sources/edit.html.erb | 10 - app/views/gtt_tile_sources/index.html.erb | 31 --- app/views/gtt_tile_sources/new.html.erb | 10 - app/views/gtt_tile_sources/new.js.erb | 2 - config/locales/en.yml | 98 +++++--- config/routes.rb | 2 +- .../20230411003927_create_gtt_map_layers.rb | 22 ++ ...31_create_gtt_map_layers_projects_table.rb | 8 + init.rb | 6 +- lib/redmine_gtt/actions/create_map_layer.rb | 18 ++ lib/redmine_gtt/actions/create_tile_source.rb | 18 -- lib/redmine_gtt/actions/update_map_layer.rb | 20 ++ .../actions/update_project_settings.rb | 4 +- lib/redmine_gtt/actions/update_tile_source.rb | 20 -- lib/redmine_gtt/patches/issue_patch.rb | 3 +- lib/redmine_gtt/patches/project_patch.rb | 16 +- .../patches/projects_helper_patch.rb | 4 +- lib/redmine_gtt/patches/user_patch.rb | 2 +- 34 files changed, 643 insertions(+), 409 deletions(-) create mode 100644 app/controllers/gtt_map_layers_controller.rb delete mode 100644 app/controllers/gtt_tile_sources_controller.rb create mode 100644 app/helpers/gtt_map_layers_helper.rb delete mode 100644 app/helpers/gtt_tile_sources_helper.rb create mode 100644 app/models/gtt_map_layer.rb delete mode 100644 app/models/gtt_tile_source.rb create mode 100644 app/views/gtt_map_layers/_form.html.erb create mode 100644 app/views/gtt_map_layers/_map_layer.html.erb create mode 100644 app/views/gtt_map_layers/edit.html.erb create mode 100644 app/views/gtt_map_layers/index.html.erb create mode 100644 app/views/gtt_map_layers/new.html.erb create mode 100644 app/views/gtt_map_layers/new.js.erb delete mode 100644 app/views/gtt_tile_sources/_form.html.erb delete mode 100644 app/views/gtt_tile_sources/_tile_source.html.erb delete mode 100644 app/views/gtt_tile_sources/edit.html.erb delete mode 100644 app/views/gtt_tile_sources/index.html.erb delete mode 100644 app/views/gtt_tile_sources/new.html.erb delete mode 100644 app/views/gtt_tile_sources/new.js.erb create mode 100644 db/migrate/20230411003927_create_gtt_map_layers.rb create mode 100644 db/migrate/20230411004231_create_gtt_map_layers_projects_table.rb create mode 100644 lib/redmine_gtt/actions/create_map_layer.rb delete mode 100644 lib/redmine_gtt/actions/create_tile_source.rb create mode 100644 lib/redmine_gtt/actions/update_map_layer.rb delete mode 100644 lib/redmine_gtt/actions/update_tile_source.rb diff --git a/app/controllers/gtt_configuration_controller.rb b/app/controllers/gtt_configuration_controller.rb index 540436e6..74901afa 100644 --- a/app/controllers/gtt_configuration_controller.rb +++ b/app/controllers/gtt_configuration_controller.rb @@ -1,74 +1,76 @@ class GttConfigurationController < ApplicationController - before_action :find_optional_project_and_authorize + before_action :find_optional_project_and_authorize - accept_api_auth :default_setting_configuration + accept_api_auth :default_setting_configuration - def default_setting_configuration - gtt_map_config = build_default_setting_config - respond_to do |format| - format.api { render json: build_default_setting_config} - end + def default_setting_configuration + gtt_map_config = build_default_setting_config + respond_to do |format| + format.api { render json: build_default_setting_config} end + end - def build_default_setting_config - default_tracker_icon = [] - default_status_color = [] - gtt_tile_source = [] + def build_default_setting_config + default_tracker_icon = [] + default_status_color = [] + gtt_tile_source = [] - Tracker.all.sort.each {|tracker| - default_tracker_icon.append({ - trackerID: tracker.id, - trackerName: tracker.name, - icon: Setting.plugin_redmine_gtt['tracker_'+tracker.id.to_s] - }) - } - IssueStatus.all.sort.each {|status| - default_status_color.append({ - statusID: status.id, - statusName: status.name, - color: Setting.plugin_redmine_gtt['status_'+status.id.to_s] - }) - } - GttTileSource.where(global: true).sort.each {|tileSource| - gtt_tile_source.append({ - id: tileSource.id, - name: tileSource.name, - type: tileSource.type, - options: tileSource.options - }) - } + Tracker.all.sort.each {|tracker| + default_tracker_icon.append({ + trackerID: tracker.id, + trackerName: tracker.name, + icon: Setting.plugin_redmine_gtt['tracker_'+tracker.id.to_s] + }) + } - mapConfig = { - gttDefaultSetting: { - defaultTrackerIcon: default_tracker_icon, - defaultStatusColor: default_status_color, - defaultMapSetting: { - centerLng: Setting.plugin_redmine_gtt['default_map_center_longitude'], - centerLat: Setting.plugin_redmine_gtt['default_map_center_latitude'] - }, - geometrySetting: { - geometryTypes: Setting.plugin_redmine_gtt['editable_geometry_types_on_issue_map'], - GeoJsonUpload: (Setting.plugin_redmine_gtt['enable_geojson_upload_on_issue_map'] == 'true'), - }, - geocoderSetting: { - enableGeocodingOnMap: (Setting.plugin_redmine_gtt['enable_geocoding_on_map'] == 'true'), - geocoderOptions: Setting.plugin_redmine_gtt['default_geocoder_options'] - }, - }, - gttLayer: gtt_tile_source, - } - return mapConfig - end + IssueStatus.all.sort.each {|status| + default_status_color.append({ + statusID: status.id, + statusName: status.name, + color: Setting.plugin_redmine_gtt['status_'+status.id.to_s] + }) + } + + GttMapLayer.where(global: true).sort.each {|mapLayer| + gtt_map_layer.append({ + id: mapLayer.id, + name: mapLayer.name, + type: mapLayer.type, + options: mapLayer.options + }) + } + + mapConfig = { + gttDefaultSetting: { + defaultTrackerIcon: default_tracker_icon, + defaultStatusColor: default_status_color, + defaultMapSetting: { + centerLng: Setting.plugin_redmine_gtt['default_map_center_longitude'], + centerLat: Setting.plugin_redmine_gtt['default_map_center_latitude'] + }, + geometrySetting: { + geometryTypes: Setting.plugin_redmine_gtt['editable_geometry_types_on_issue_map'], + GeoJsonUpload: (Setting.plugin_redmine_gtt['enable_geojson_upload_on_issue_map'] == 'true'), + }, + geocoderSetting: { + enableGeocodingOnMap: (Setting.plugin_redmine_gtt['enable_geocoding_on_map'] == 'true'), + geocoderOptions: Setting.plugin_redmine_gtt['default_geocoder_options'] + }, + }, + gttLayer: gtt_map_layer, + } + return mapConfig + end - private + private - def find_optional_project_and_authorize - if params[:project_id] - @project = Project.find params[:project_id] - authorize - else - authorize_global - end + def find_optional_project_and_authorize + if params[:project_id] + @project = Project.find(params[:project_id]) + authorize + else + authorize_global end + end end diff --git a/app/controllers/gtt_map_layers_controller.rb b/app/controllers/gtt_map_layers_controller.rb new file mode 100644 index 00000000..20fe42d1 --- /dev/null +++ b/app/controllers/gtt_map_layers_controller.rb @@ -0,0 +1,70 @@ +class GttMapLayersController < ApplicationController + layout 'admin' + + before_action :require_admin + + self.main_menu = false + + def index + @map_layers = GttMapLayer.sorted + end + + def new + @map_layer = GttMapLayer.new + end + + def create + r = RedmineGtt::Actions::CreateMapLayer.(map_layer_params) + if r.map_layer_created? + redirect_to(params[:continue] ? new_gtt_map_layer_path : gtt_map_layers_path) + else + @map_layer = r.map_layer + render 'new' + end + end + + def edit + @map_layer = GttMapLayer.find(params[:id]) + end + + def update + ml = GttMapLayer.find(params[:id]) + r = RedmineGtt::Actions::UpdateMapLayer.(ml, map_layer_params) + if r.map_layer_updated? + respond_to do |format| + format.html { + flash[:notice] = l(:notice_successful_update) + redirect_to gtt_map_layers_path + } + format.js { head 200 } + end + else + respond_to do |format| + format.html { + @map_layer = r.map_layer + render 'edit' + } + format.js { head 422 } + end + end + end + + def destroy + ml = GttMapLayer.find(params[:id]) + ml.destroy + redirect_to gtt_map_layers_path + end + + + private + + def map_layer_params + return {} unless params[:map_layer] + + params[:map_layer].permit( + :name, :default, :global, :baselayer, :position, + :layer, :layer_options, :source, :source_options, + :format, :format_options, :styles + ) + end + end diff --git a/app/controllers/gtt_tile_sources_controller.rb b/app/controllers/gtt_tile_sources_controller.rb deleted file mode 100644 index e75539ce..00000000 --- a/app/controllers/gtt_tile_sources_controller.rb +++ /dev/null @@ -1,67 +0,0 @@ -class GttTileSourcesController < ApplicationController - layout 'admin' - - before_action :require_admin - - self.main_menu = false - - def index - @tile_sources = GttTileSource.sorted - end - - def new - @tile_source = GttTileSource.new - end - - def create - r = RedmineGtt::Actions::CreateTileSource.(tile_source_params) - if r.tile_source_created? - redirect_to(params[:continue] ? new_gtt_tile_source_path : gtt_tile_sources_path) - else - @tile_source = r.tile_source - render 'new' - end - end - - def edit - @tile_source = GttTileSource.find params[:id] - end - - def update - ts = GttTileSource.find params[:id] - r = RedmineGtt::Actions::UpdateTileSource.(ts, tile_source_params) - if r.tile_source_updated? - respond_to do |format| - format.html { - flash[:notice] = l(:notice_successful_update) - redirect_to gtt_tile_sources_path - } - format.js { head 200 } - end - else - respond_to do |format| - format.html { - @tile_source = r.tile_source - render 'edit' - } - format.js { head 422 } - end - end - end - - def destroy - ts = GttTileSource.find params[:id] - ts.destroy - redirect_to gtt_tile_sources_path - end - - - private - - def tile_source_params - return {} unless params[:tile_source] - - params[:tile_source].permit( :name, :type, :baselayer, :global, :position, - :default, :options_string ) - end -end diff --git a/app/helpers/gtt_map_layers_helper.rb b/app/helpers/gtt_map_layers_helper.rb new file mode 100644 index 00000000..13d87718 --- /dev/null +++ b/app/helpers/gtt_map_layers_helper.rb @@ -0,0 +1,13 @@ +module GttMapLayersHelper + def pretty_map_layer_layer_options(map_layer) + JSON.pretty_generate map_layer.layer_options if Hash === map_layer.layer_options + end + + def map_layer_layer_options(map_layer) + safe_join map_layer.layer_options.to_a.select{ + |k,v| v.present? + }.map{|k,v| + "#{k}: #{v}" + }, "
    ".html_safe + end +end diff --git a/app/helpers/gtt_tile_sources_helper.rb b/app/helpers/gtt_tile_sources_helper.rb deleted file mode 100644 index adfbdd65..00000000 --- a/app/helpers/gtt_tile_sources_helper.rb +++ /dev/null @@ -1,13 +0,0 @@ -module GttTileSourcesHelper - def pretty_tile_source_options(tile_source) - JSON.pretty_generate tile_source.options if Hash === tile_source.options - end - - def tile_source_options(tile_source) - safe_join tile_source.options.to_a.select{ - |k,v| v.present? - }.map{|k,v| - "#{k}: #{v}" - }, "
    ".html_safe - end -end diff --git a/app/models/gtt_configuration.rb b/app/models/gtt_configuration.rb index bdfca763..f369748e 100644 --- a/app/models/gtt_configuration.rb +++ b/app/models/gtt_configuration.rb @@ -1,25 +1,25 @@ class GttConfiguration include ActiveModel::Model - attr_accessor :project, :geojson, :gtt_tile_source_ids, :map_rotation + attr_accessor :project, :geojson, :gtt_map_layer_ids, :map_rotation def self.for(project) new( project: project, geojson: project.geojson, map_rotation: project.map_rotation, - gtt_tile_source_ids: project.gtt_tile_source_ids + gtt_map_layer_ids: project.gtt_map_layer_ids ) end def self.from_params(params) new geojson: params[:geojson], map_rotation: params[:map_rotation], - gtt_tile_source_ids: params[:gtt_tile_source_ids] + gtt_map_layer_ids: params[:gtt_map_layer_ids] end def map - GttMap.new json: geojson, layers: project.gtt_tile_sources.sorted, rotation: project.map_rotation + GttMap.new json: geojson, layers: project.gtt_map_layers.sorted, rotation: project.map_rotation end end diff --git a/app/models/gtt_map_layer.rb b/app/models/gtt_map_layer.rb new file mode 100644 index 00000000..b7095430 --- /dev/null +++ b/app/models/gtt_map_layer.rb @@ -0,0 +1,59 @@ +# Map layer model +# +# Configuration is stored as json +class GttMapLayer < ActiveRecord::Base + self.inheritance_column = 'none' + + validates :name, presence: true + validates :layer, presence: true + + validate :take_json_layer_options + validate :take_json_source_options + validate :take_json_format_options + + acts_as_positioned + scope :sorted, ->{ order :position } + + # globally available map layers + scope :global, ->{ where global: true } + + # default map layers for new projects + scope :default, ->{ where default: true } + + attr_writer :layer_options_jsonb + def layer_options_jsonb + @layer_options ||= JSON.pretty_generate(layer_options || {}) + end + + attr_writer :source_options_jsonb + def source_options_jsonb + @source_options ||= JSON.pretty_generate(source_options || {}) + end + + attr_writer :format_options_jsonb + def format_options_jsonb + @format_options ||= JSON.pretty_generate(format_options || {}) + end + + private + + def take_json_layer_options + self.layer_options = JSON.parse(layer_options_jsonb) + rescue JSON::ParserError + errors.add :layer_options, I18n.t(:error_invalid_json) + end + + def take_json_source_options + self.source_options = JSON.parse(source_options_jsonb) + rescue JSON::ParserError + errors.add :source_options, I18n.t(:error_invalid_json) + end + + def take_json_format_options + self.format_options = JSON.parse(format_options_jsonb) + rescue JSON::ParserError + errors.add :format_options, I18n.t(:error_invalid_json) + end + +end + diff --git a/app/models/gtt_tile_source.rb b/app/models/gtt_tile_source.rb deleted file mode 100644 index cf133422..00000000 --- a/app/models/gtt_tile_source.rb +++ /dev/null @@ -1,34 +0,0 @@ -# Tile source model -# -# Configuration is stored as json -class GttTileSource < ActiveRecord::Base - self.inheritance_column = 'none' - - validates :name, presence: true - validates :type, presence: true - validate :take_json_options - - acts_as_positioned - scope :sorted, ->{ order :position } - - # globally available tile sources - scope :global, ->{ where global: true } - - # default tile sources for new projects - scope :default, ->{ where default: true } - - attr_writer :options_string - def options_string - @options_string ||= JSON.pretty_generate(options || {}) - end - - private - - def take_json_options - self.options = JSON.parse options_string - rescue JSON::ParserError - errors.add :options_string, I18n.t(:error_invalid_json) - end - -end - diff --git a/app/views/gtt_map_layers/_form.html.erb b/app/views/gtt_map_layers/_form.html.erb new file mode 100644 index 00000000..0880e089 --- /dev/null +++ b/app/views/gtt_map_layers/_form.html.erb @@ -0,0 +1,225 @@ +<%= error_messages_for 'map_layer' %> + +
    +

    <%= f.text_field :name, required: true, size: 25 %>

    +

    <%= f.check_box :baselayer %>

    +

    <%= f.check_box :global %>

    +

    <%= f.check_box :default %>

    +

    + <%= f.select :layer, + options_for_select([ + [t('map_layer.layer_options.select_image'), 'ol.layer.Image'], + [t('map_layer.layer_options.select_tile'), 'ol.layer.Tile'], + [t('map_layer.layer_options.select_mapboxvector'), 'ol.layer.MapboxVector'], + [t('map_layer.layer_options.select_vector'), 'ol.layer.Vector'], + [t('map_layer.layer_options.select_vectortile'), 'ol.layer.VectorTile'], + ], selected: f.object.layer ), + { include_blank: t('map_layer.layer_options.select'), required: true } + %> + <%= t('map_layer.load_example') %> +

    +

    + <%= f.text_area :layer_options, rows: 4, cols: 80 %> +

    +

    + <%= f.select :source, + options_for_select([ + [t('map_layer.source_options.select_bingmaps'), 'ol.source.BingMaps'], + [t('map_layer.source_options.select_cartodb'), 'ol.source.CartoDB'], + [t('map_layer.source_options.select_imagestatic'), 'ol.source.ImageStatic'], + [t('map_layer.source_options.select_imagewms'), 'ol.source.ImageWMS'], + [t('map_layer.source_options.select_osm'), 'ol.source.OSM'], + [t('map_layer.source_options.select_raster'), 'ol.source.Raster'], + [t('map_layer.source_options.select_stamen'), 'ol.source.Stamen'], + [t('map_layer.source_options.select_tilejson'), 'ol.source.TileJSON'], + [t('map_layer.source_options.select_tilewms'), 'ol.source.TileWMS'], + [t('map_layer.source_options.select_utfgrid'), 'ol.source.UTFGrid'], + [t('map_layer.source_options.select_vector'), 'ol.source.Vector'], + [t('map_layer.source_options.select_vectortile'), 'ol.source.VectorTile'], + [t('map_layer.source_options.select_wmts'), 'ol.source.WMTS'], + [t('map_layer.source_options.select_xyz'), 'ol.source.XYZ'], + ], selected: f.object.source ), + { include_blank: t('map_layer.source_options.select') } + %> +

    +

    + <%= f.text_area :source_options, rows: 6, cols: 80 %> +

    +

    + <%= f.select :format, + options_for_select([ + [t('map_layer.format_options.select_geojson'), 'ol.format.GeoJSON'], + [t('map_layer.format_options.select_gpx'), 'ol.format.GPX'], + [t('map_layer.format_options.select_kml'), 'ol.format.KML'], + [t('map_layer.format_options.select_mvt'), 'ol.format.MVT'], + [t('map_layer.format_options.select_topojson'), 'ol.format.TopoJSON'], + [t('map_layer.format_options.select_wfs'), 'ol.format.WFS'], + [t('map_layer.format_options.select_wkb'), 'ol.format.WKB'], + [t('map_layer.format_options.select_wkt'), 'ol.format.WKT'], + ], selected: f.object.format ), + { include_blank: t('map_layer.format_options.select') } + %> +

    +

    + <%= f.text_area :format_options, rows: 4, cols: 80 %> +

    +

    + <%= f.text_area :styles, rows: 4, cols: 80 %> +

    +
    + + diff --git a/app/views/gtt_map_layers/_map_layer.html.erb b/app/views/gtt_map_layers/_map_layer.html.erb new file mode 100644 index 00000000..f994f371 --- /dev/null +++ b/app/views/gtt_map_layers/_map_layer.html.erb @@ -0,0 +1,12 @@ +"> + <%= link_to map_layer.name, edit_gtt_map_layer_path(map_layer) %> + <%= map_layer.layer %> + <%= checked_image map_layer.baselayer? %> + <%= checked_image map_layer.global? %> + <%= checked_image map_layer.default? %> + + <%= reorder_handle(map_layer, url: gtt_map_layer_path(map_layer), param: 'map_layer') %> + <%= delete_link gtt_map_layer_path(map_layer) %> + + + diff --git a/app/views/gtt_map_layers/edit.html.erb b/app/views/gtt_map_layers/edit.html.erb new file mode 100644 index 00000000..0a9c9a6a --- /dev/null +++ b/app/views/gtt_map_layers/edit.html.erb @@ -0,0 +1,10 @@ +<%= title [t('map_layer.plural'), gtt_map_layers_path], @map_layer.name %> + +<%= labelled_form_for :map_layer, @map_layer, url: gtt_map_layer_path(@map_layer), method: :patch do |f| %> + <%= render partial: 'form', locals: { f: f } %> +

    + <%= submit_tag l :button_save %> +

    +<% end %> + + diff --git a/app/views/gtt_map_layers/index.html.erb b/app/views/gtt_map_layers/index.html.erb new file mode 100644 index 00000000..eeb901fb --- /dev/null +++ b/app/views/gtt_map_layers/index.html.erb @@ -0,0 +1,30 @@ +
    +<%= link_to t('map_layer.new'), new_gtt_map_layer_path, :class => 'icon icon-add' %> +
    + +<%= title t('map_layer.plural') %> + +<% if @map_layers.any? %> + + + + + + + + + + + + + <%= render collection: @map_layers, partial: 'map_layer' %> + +
    <%= l :label_name %><%= l :label_layer %><%= l :label_baselayer %><%= l :label_global %><%= l :label_default %>
    + +<% else %> +

    <%= l :label_no_data %>

    +<% end %> + +<%= javascript_tag do %> + $(function() { $("table.map_layers tbody").positionedItems(); }); +<% end %> diff --git a/app/views/gtt_map_layers/new.html.erb b/app/views/gtt_map_layers/new.html.erb new file mode 100644 index 00000000..d0162b7d --- /dev/null +++ b/app/views/gtt_map_layers/new.html.erb @@ -0,0 +1,10 @@ +<%= title [t('map_layer.plural'), gtt_map_layers_path], t('map_layer.new') %> + +<%= labelled_form_for :map_layer, @map_layer, url: gtt_map_layers_path do |f| %> + <%= render partial: 'form', locals: { f: f } %> +

    + <%= submit_tag t(:button_create) %> + <%= submit_tag t(:button_create_and_continue), name: 'continue' %> +

    +<% end %> + diff --git a/app/views/gtt_map_layers/new.js.erb b/app/views/gtt_map_layers/new.js.erb new file mode 100644 index 00000000..5a611017 --- /dev/null +++ b/app/views/gtt_map_layers/new.js.erb @@ -0,0 +1 @@ +$('#content').html('<%= j render template: 'gtt_map_layers/new', formats: [:html] %>'); diff --git a/app/views/gtt_tile_sources/_form.html.erb b/app/views/gtt_tile_sources/_form.html.erb deleted file mode 100644 index eb448961..00000000 --- a/app/views/gtt_tile_sources/_form.html.erb +++ /dev/null @@ -1,67 +0,0 @@ -<%= error_messages_for 'tile_source' %> - -
    -

    <%= f.text_field :name, required: true, size: 25 %>

    -

    - <%= f.select :type, - options_for_select([ - [l(:gtt_tile_sources_select_imagewms), 'ol.source.ImageWMS'], - [l(:gtt_tile_sources_select_osm), 'ol.source.OSM'], - [l(:gtt_tile_sources_select_tilewms), 'ol.source.TileWMS'], - [l(:gtt_tile_sources_select_xyz), 'ol.source.XYZ'], - [l(:gtt_tile_sources_select_vectortile), 'ol.source.VectorTile'] - ], selected: f.object.type ), - { include_blank: l(:gtt_tile_sources_select), required: true } - %> -

    -

    <%= f.check_box :baselayer %>

    -

    <%= f.check_box :global %>

    -

    <%= f.check_box :default %>

    -

    - <%= f.text_area :options_string, rows: 10, cols: 80 %> - <%= l(:gtt_tile_sources_load_example) %> -

    -
    - - diff --git a/app/views/gtt_tile_sources/_tile_source.html.erb b/app/views/gtt_tile_sources/_tile_source.html.erb deleted file mode 100644 index 12e17260..00000000 --- a/app/views/gtt_tile_sources/_tile_source.html.erb +++ /dev/null @@ -1,13 +0,0 @@ -"> - <%= link_to tile_source.name, edit_gtt_tile_source_path(tile_source) %> - <%= tile_source.type %> - <%= checked_image tile_source.baselayer? %> - <%= checked_image tile_source.global? %> - <%= checked_image tile_source.default? %> - - - <%= reorder_handle(tile_source, url: gtt_tile_source_path(tile_source), param: 'tile_source') %> - <%= delete_link gtt_tile_source_path(tile_source) %> - - - diff --git a/app/views/gtt_tile_sources/edit.html.erb b/app/views/gtt_tile_sources/edit.html.erb deleted file mode 100644 index 424251b4..00000000 --- a/app/views/gtt_tile_sources/edit.html.erb +++ /dev/null @@ -1,10 +0,0 @@ -<%= title [l(:label_gtt_tile_source_plural), gtt_tile_sources_path], @tile_source.name %> - -<%= labelled_form_for :tile_source, @tile_source, url: gtt_tile_source_path(@tile_source), method: :patch do |f| %> - <%= render partial: 'form', locals: { f: f } %> -

    - <%= submit_tag l :button_save %> -

    -<% end %> - - diff --git a/app/views/gtt_tile_sources/index.html.erb b/app/views/gtt_tile_sources/index.html.erb deleted file mode 100644 index 55804bae..00000000 --- a/app/views/gtt_tile_sources/index.html.erb +++ /dev/null @@ -1,31 +0,0 @@ -
    -<%= link_to l(:label_gtt_tile_source_new), new_gtt_tile_source_path, :class => 'icon icon-add' %> -
    - -<%= title l :label_gtt_tile_source_plural %> - -<% if @tile_sources.any? %> - - - - - - - - - - - - - - <%= render collection: @tile_sources, partial: 'tile_source' %> - -
    <%= l :label_name %><%= l :label_type %><%= l :label_baselayer %><%= l :label_global %><%= l :label_default %>
    - -<% else %> -

    <%= l :label_no_data %>

    -<% end %> - -<%= javascript_tag do %> - $(function() { $("table.tile_sources tbody").positionedItems(); }); -<% end %> diff --git a/app/views/gtt_tile_sources/new.html.erb b/app/views/gtt_tile_sources/new.html.erb deleted file mode 100644 index 25417995..00000000 --- a/app/views/gtt_tile_sources/new.html.erb +++ /dev/null @@ -1,10 +0,0 @@ -<%= title [l(:label_gtt_tile_source_plural), gtt_tile_sources_path], l(:label_gtt_tile_source_new) %> - -<%= labelled_form_for :tile_source, @tile_source, url: gtt_tile_sources_path do |f| %> - <%= render partial: 'form', locals: { f: f } %> -

    - <%= submit_tag l :button_create %> - <%= submit_tag l(:button_create_and_continue), name: 'continue' %> -

    -<% end %> - diff --git a/app/views/gtt_tile_sources/new.js.erb b/app/views/gtt_tile_sources/new.js.erb deleted file mode 100644 index 38046d35..00000000 --- a/app/views/gtt_tile_sources/new.js.erb +++ /dev/null @@ -1,2 +0,0 @@ -$('#content').html('<%= j render template: 'gtt_tile_sources/new', formats: [:html] %>'); - diff --git a/config/locales/en.yml b/config/locales/en.yml index 5f84495e..33fec331 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,17 +1,11 @@ # English strings go here for Rails i18n en: error_invalid_json: Must be valid JSON - error_unable_to_update_project_gtt_settings: "Unable to update project GTT settings\ - \ (%{value})" + error_unable_to_update_project_gtt_settings: "Unable to update project GTT settings (%{value})" - field_default: Default for new projects field_geometry: "Geometry" field_location: "Location" field_geom: "Geometry" - field_global: Globally available - field_baselayer: BaseLayer - field_options_string: Options - field_gtt_tile_source_ids: Tile Sources label_geotask_map: "Geotask Map" label_global: Global @@ -20,39 +14,21 @@ en: label_name: Name label_baselayer: BaseLayer label_nearby: "nearby(lat,lng)" - label_type: Type + label_layer: "Layer Type" label_config: Configuration label_gtt_settings: GTT label_gtt_settings_headline: GTT Settings - # gtt_label_geometry: "API Key" - # gtt_text_settings_help: "Enter the API key. Leave empty if no key is required." - # gtt_text_settings_geometry_example: "2747491302261fbc47967ba62621af22" - label_gtt: "GTT" label_gtt_bbox_filter: Location label_gtt_distance: Distance label_gtt_select_icon: Select icon - label_gtt_tile_source: Tile Source - label_gtt_tile_source_new: New Tile Source - label_gtt_tile_source_plural: Tile Sources label_parameters: Parameters label_tab_general: "General" label_tab_geocoder: "Geocoder" - label_tab_map: "Tile Sources" geocoder_options: "Geocoder Options" - gtt_tile_sources_info: "Select the tile sources that should be available in this\ - \ project." - gtt_tile_sources_select: "-- Select type --" - gtt_tile_sources_select_imagewms: "Image WMS" - gtt_tile_sources_select_osm: "OpenStreetMap" - gtt_tile_sources_select_tilewms: "Tiled WMS" - gtt_tile_sources_select_xyz: "XYZ Tiles" - gtt_tile_sources_select_vectortile: "Vector Tiles (MVT)" - gtt_tile_sources_load_example: "Load example configuration." - gtt_map_rotate_label: "Map rotation" gtt_map_rotate_info_html: "Hold down Shift+Alt and drag the map to rotate." @@ -66,16 +42,6 @@ en: gtt_settings_label_styling: "Styling" gtt_settings_label_geocoder: "Geocoder" - # gtt_settings_map_url: "API Service URL" - # gtt_settings_map_apikey: "API Key" - # gtt_settings_map_maxzoom_level: "Maximum map zoom level" - - # gtt_settings_geocoder_url: "API Service URL" - # gtt_settings_geocoder_apikey: "API Key" - # gtt_settings_geocoder_address_field_name: "Address field name" - # gtt_settings_geocoder_district_field_name: "District field name" - # gtt_park_search_field_name: "Park search field name" - label_default_collapsed_issues_page_map: "Default collapsed issues page map" label_gtt_point: "Point" label_gtt_linestring: "LineString" @@ -90,15 +56,69 @@ en: select_edit_geometry_settings: "Set edit geometry settings:" select_default_geocoder_settings: "Set Geocoder settings:" - # text_osm_url_sample: For example https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png - project_module_gtt: "GTT" permission_manage_gtt_settings: "Manage GTT settings" title_geojson_upload: "Import GeoJSON" placeholder_geojson_upload: "Please paste a GeoJSON geometry here, or import the GeoJSON data from a file." - # Localize for use in Javascript + map_layer: + title: "Map Layers" + plural: "Map Layers" + new: "New Map Layer" + ids: "Map Layers" + project: + info: "Select the map layers that should be available in this project." + layer_options: + select: "-- Select layer type --" + select_image: "Image" + select_tile: "Tile" + select_mapboxvector: "Mapbox Vector" + select_vector: "Vector" + select_vectortile: "Vector Tile" + source_options: + select: "-- Select source type --" + select_bingmaps: "Bing Maps" + select_cartodb: "CartoDB" + select_imagestatic: "Static Image" + select_imagewms: "Image WMS" + select_osm: "OpenStreetMap" + select_raster: "Raster" + select_stamen: "Stamen" + select_tilejson: "Tiled JSON" + select_tilewms: "Tiled WMS" + select_utfgrid: "UTF Grid" + select_vector: "Vector" + select_vectortile: "Vector Tiles" + select_wmts: "WMTS Tiles" + select_xyz: "XYZ Tiles" + format_options: + select: "-- Select format type --" + select_geojson: "GeoJSON" + select_gpx: "GPX" + select_kml: "KML" + select_mvt: "MVT" + select_topojson: "TopoJSON" + select_wfs: "WFS" + select_wkb: "WKB" + select_wkt: "WKT" + load_example: "Load example configuration." + + # Workaround to easily customize field labels + field_name: "Name" + field_layer: "Layer type" + field_layer_options: "Layer options" + field_baselayer: "BaseLayer" + field_global: "Globally available" + field_default: "Default for new projects" + field_source: "Source type" + field_source_options: "Source options" + field_format: "Format type" + field_format_options: "Format options" + field_styles: "Style settings" + + # Localize for use in Javascriptccccccevihnjbenrlkjhjegkudjdckklnbkvgfkcnrkh + gtt_js: control: geocoding: "Location search" diff --git a/config/routes.rb b/config/routes.rb index 6d4b87c5..74f38675 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,7 +1,7 @@ # Plugin's routes # See: http://guides.rubyonrails.org/routing.html -resources :gtt_tile_sources +resources :gtt_map_layers put 'projects/:id/settings/gtt', to: 'projects#update_gtt_configuration', as: :update_gtt_configuration diff --git a/db/migrate/20230411003927_create_gtt_map_layers.rb b/db/migrate/20230411003927_create_gtt_map_layers.rb new file mode 100644 index 00000000..12583de5 --- /dev/null +++ b/db/migrate/20230411003927_create_gtt_map_layers.rb @@ -0,0 +1,22 @@ +class CreateGttMapLayers < ActiveRecord::Migration[6.1] + def change + create_table :gtt_map_layers do |t| + t.string :name, null: false + + t.string :layer, null: false + t.jsonb :layer_options + t.string :source + t.jsonb :source_options + t.string :format + t.jsonb :format_options + t.text :styles + + t.boolean :global, default: false + t.boolean :default, default: false + t.boolean :baselayer, default: true + + t.integer :position, default: 0 + t.timestamps null: false + end + end +end diff --git a/db/migrate/20230411004231_create_gtt_map_layers_projects_table.rb b/db/migrate/20230411004231_create_gtt_map_layers_projects_table.rb new file mode 100644 index 00000000..b3ada6aa --- /dev/null +++ b/db/migrate/20230411004231_create_gtt_map_layers_projects_table.rb @@ -0,0 +1,8 @@ +class CreateGttMapLayersProjectsTable < ActiveRecord::Migration[6.1] + def change + create_join_table :gtt_map_layers, :projects do |t| + t.index :project_id + t.index :gtt_map_layer_id + end + end +end diff --git a/init.rb b/init.rb index 30aa72ab..601cc753 100644 --- a/init.rb +++ b/init.rb @@ -38,9 +38,9 @@ ) menu :admin_menu, - :gtt_tile_sources, - { controller: 'gtt_tile_sources', action: 'index' }, - caption: :label_gtt_tile_source_plural, :html => {:class => 'icon icon-gtt-map'} + :gtt_map_layers, + { controller: 'gtt_map_layers', action: 'index' }, + caption: :'map_layer.plural', html: { class: 'icon icon-gtt-map' } end # Register MIME Types diff --git a/lib/redmine_gtt/actions/create_map_layer.rb b/lib/redmine_gtt/actions/create_map_layer.rb new file mode 100644 index 00000000..f21ed8a9 --- /dev/null +++ b/lib/redmine_gtt/actions/create_map_layer.rb @@ -0,0 +1,18 @@ +module RedmineGtt + module Actions + class CreateMapLayer < Base + + Result = ImmutableStruct.new(:map_layer_created?, :map_layer) + + def initialize(parameters) + @params = parameters + end + + def call + ml = GttMapLayer.new @params + Result.new map_layer_created: ml.save, map_layer: ml + end + + end + end +end diff --git a/lib/redmine_gtt/actions/create_tile_source.rb b/lib/redmine_gtt/actions/create_tile_source.rb deleted file mode 100644 index fcd44482..00000000 --- a/lib/redmine_gtt/actions/create_tile_source.rb +++ /dev/null @@ -1,18 +0,0 @@ -module RedmineGtt - module Actions - class CreateTileSource < Base - - Result = ImmutableStruct.new(:tile_source_created?, :tile_source) - - def initialize(parameters) - @params = parameters - end - - def call - ts = GttTileSource.new @params - Result.new tile_source_created: ts.save, tile_source: ts - end - - end - end -end diff --git a/lib/redmine_gtt/actions/update_map_layer.rb b/lib/redmine_gtt/actions/update_map_layer.rb new file mode 100644 index 00000000..4a08bb43 --- /dev/null +++ b/lib/redmine_gtt/actions/update_map_layer.rb @@ -0,0 +1,20 @@ +module RedmineGtt + module Actions + class UpdateMapLayer < Base + + Result = ImmutableStruct.new(:map_layer_updated?, :map_layer) + + def initialize(map_layer, parameters) + @ml = map_layer + @params = parameters + end + + def call + @ml.attributes = @params + Result.new map_layer_updated: @ml.save, map_layer: @ml + end + + end + end +end + diff --git a/lib/redmine_gtt/actions/update_project_settings.rb b/lib/redmine_gtt/actions/update_project_settings.rb index dc11021f..650238f1 100644 --- a/lib/redmine_gtt/actions/update_project_settings.rb +++ b/lib/redmine_gtt/actions/update_project_settings.rb @@ -23,8 +23,8 @@ def call private def update_project - if tile_source_ids = @form.gtt_tile_source_ids - @project.gtt_tile_source_ids = tile_source_ids + if map_layer_ids = @form.gtt_map_layer_ids + @project.gtt_map_layer_ids = map_layer_ids end begin diff --git a/lib/redmine_gtt/actions/update_tile_source.rb b/lib/redmine_gtt/actions/update_tile_source.rb deleted file mode 100644 index 1e3628e9..00000000 --- a/lib/redmine_gtt/actions/update_tile_source.rb +++ /dev/null @@ -1,20 +0,0 @@ -module RedmineGtt - module Actions - class UpdateTileSource < Base - - Result = ImmutableStruct.new(:tile_source_updated?, :tile_source) - - def initialize(tile_source, parameters) - @ts = tile_source - @params = parameters - end - - def call - @ts.attributes = @params - Result.new tile_source_updated: @ts.save, tile_source: @ts - end - - end - end -end - diff --git a/lib/redmine_gtt/patches/issue_patch.rb b/lib/redmine_gtt/patches/issue_patch.rb index 128189df..8b67a38a 100644 --- a/lib/redmine_gtt/patches/issue_patch.rb +++ b/lib/redmine_gtt/patches/issue_patch.rb @@ -22,8 +22,7 @@ def self.apply def map json = as_geojson - GttMap.new json: json, layers: project.gtt_tile_sources.sorted, - bounds: project.map.json + GttMap.new json: json, layers: project.gtt_map_layers.sorted, bounds: project.map.json end # Check if geometry change aren't small and ignore it diff --git a/lib/redmine_gtt/patches/project_patch.rb b/lib/redmine_gtt/patches/project_patch.rb index 53ee307a..86009264 100644 --- a/lib/redmine_gtt/patches/project_patch.rb +++ b/lib/redmine_gtt/patches/project_patch.rb @@ -9,27 +9,27 @@ def self.apply Project.prepend self Project.class_eval do safe_attributes :geojson, :map_rotation - has_and_belongs_to_many :gtt_tile_sources - after_create :set_default_tile_sources + has_and_belongs_to_many :gtt_map_layers + after_create :set_default_map_layers end end end def map - GttMap.new json: as_geojson, layers: gtt_tile_sources.sorted + GttMap.new json: as_geojson, layers: gtt_map_layers.sorted end def enabled_module_names=(*_) super - set_default_tile_sources + set_default_map_layers end - def set_default_tile_sources - if gtt_tile_sources.none? and module_enabled?(:gtt) - self.gtt_tile_sources = GttTileSource.default.sorted.to_a + def set_default_map_layers + if gtt_map_layers.none? and module_enabled?(:gtt) + self.gtt_map_layers = GttMapLayer.default.sorted.to_a end end - private :set_default_tile_sources + private :set_default_map_layers end diff --git a/lib/redmine_gtt/patches/projects_helper_patch.rb b/lib/redmine_gtt/patches/projects_helper_patch.rb index e5c4346f..5db3abb0 100644 --- a/lib/redmine_gtt/patches/projects_helper_patch.rb +++ b/lib/redmine_gtt/patches/projects_helper_patch.rb @@ -17,8 +17,8 @@ def self.apply def render_api_includes(project, api) super api.array :layers do - project.gtt_tile_sources.each do |gtt_tile_source| - api.layer(gtt_tile_source.attributes) + project.gtt_map_layers.each do |gtt_map_layer| + api.layer(gtt_map_layer.attributes) end end if include_in_api_response?('layers') end diff --git a/lib/redmine_gtt/patches/user_patch.rb b/lib/redmine_gtt/patches/user_patch.rb index ec331eb3..db13dd3b 100644 --- a/lib/redmine_gtt/patches/user_patch.rb +++ b/lib/redmine_gtt/patches/user_patch.rb @@ -13,7 +13,7 @@ def self.apply end def map - GttMap.new json: as_geojson, layers: GttTileSource.global.sorted + GttMap.new json: as_geojson, layers: GttMapLayer.global.sorted end def geojson_additional_properties(include_properties) From 515cf26e6f169695565504ceb1e29f02a74581a0 Mon Sep 17 00:00:00 2001 From: Daniel Kastl Date: Thu, 13 Apr 2023 10:48:31 +0900 Subject: [PATCH 030/109] Updates helper functions Signed-off-by: Daniel Kastl --- app/models/gtt_map_layer.rb | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/app/models/gtt_map_layer.rb b/app/models/gtt_map_layer.rb index b7095430..fad7056e 100644 --- a/app/models/gtt_map_layer.rb +++ b/app/models/gtt_map_layer.rb @@ -20,39 +20,39 @@ class GttMapLayer < ActiveRecord::Base # default map layers for new projects scope :default, ->{ where default: true } - attr_writer :layer_options_jsonb - def layer_options_jsonb - @layer_options ||= JSON.pretty_generate(layer_options || {}) + attr_writer :layer_options_string + def layer_options_string + @layer_options_string ||= JSON.pretty_generate(layer_options || {}) end - attr_writer :source_options_jsonb - def source_options_jsonb - @source_options ||= JSON.pretty_generate(source_options || {}) + attr_writer :source_options_string + def source_options_string + @source_options_string ||= JSON.pretty_generate(source_options || {}) end - attr_writer :format_options_jsonb - def format_options_jsonb - @format_options ||= JSON.pretty_generate(format_options || {}) + attr_writer :format_options_string + def format_options_string + @format_options_string ||= JSON.pretty_generate(format_options || {}) end private def take_json_layer_options - self.layer_options = JSON.parse(layer_options_jsonb) + self.layer_options = JSON.parse(layer_options_string) rescue JSON::ParserError - errors.add :layer_options, I18n.t(:error_invalid_json) + errors.add :layer_options_string, I18n.t(:error_invalid_json) end def take_json_source_options - self.source_options = JSON.parse(source_options_jsonb) + self.source_options = JSON.parse(source_options_string) rescue JSON::ParserError - errors.add :source_options, I18n.t(:error_invalid_json) + errors.add :source_options_string, I18n.t(:error_invalid_json) end def take_json_format_options - self.format_options = JSON.parse(format_options_jsonb) + self.format_options = JSON.parse(format_options_string) rescue JSON::ParserError - errors.add :format_options, I18n.t(:error_invalid_json) + errors.add :format_options_string, I18n.t(:error_invalid_json) end end From 3c07859529a3d563962a7a96cce941d0a389af30 Mon Sep 17 00:00:00 2001 From: Daniel Kastl Date: Thu, 13 Apr 2023 13:50:57 +0900 Subject: [PATCH 031/109] Map layer admin Signed-off-by: Daniel Kastl --- app/controllers/gtt_map_layers_controller.rb | 6 +- app/views/gtt_map_layers/_form.html.erb | 140 ++++++++++--------- app/views/gtt_map_layers/_map_layer.html.erb | 1 + app/views/gtt_map_layers/index.html.erb | 1 + config/locales/en.yml | 3 +- 5 files changed, 85 insertions(+), 66 deletions(-) diff --git a/app/controllers/gtt_map_layers_controller.rb b/app/controllers/gtt_map_layers_controller.rb index 20fe42d1..83627646 100644 --- a/app/controllers/gtt_map_layers_controller.rb +++ b/app/controllers/gtt_map_layers_controller.rb @@ -63,8 +63,10 @@ def map_layer_params params[:map_layer].permit( :name, :default, :global, :baselayer, :position, - :layer, :layer_options, :source, :source_options, - :format, :format_options, :styles + :layer, :layer_options_string, + :source, :source_options_string, + :format, :format_options_string, + :styles ) end end diff --git a/app/views/gtt_map_layers/_form.html.erb b/app/views/gtt_map_layers/_form.html.erb index 0880e089..207284c1 100644 --- a/app/views/gtt_map_layers/_form.html.erb +++ b/app/views/gtt_map_layers/_form.html.erb @@ -1,7 +1,10 @@ <%= error_messages_for 'map_layer' %>
    -

    <%= f.text_field :name, required: true, size: 25 %>

    +

    + <%= f.text_field :name, required: true, size: 25 %> + <%= t('map_layer.load_example') %> +

    <%= f.check_box :baselayer %>

    <%= f.check_box :global %>

    <%= f.check_box :default %>

    @@ -16,10 +19,9 @@ ], selected: f.object.layer ), { include_blank: t('map_layer.layer_options.select'), required: true } %> - <%= t('map_layer.load_example') %>

    - <%= f.text_area :layer_options, rows: 4, cols: 80 %> + <%= f.text_area :layer_options_string, rows: 4, cols: 80 %>

    <%= f.select :source, @@ -43,7 +45,7 @@ %>

    - <%= f.text_area :source_options, rows: 6, cols: 80 %> + <%= f.text_area :source_options_string, rows: 8, cols: 80 %>

    <%= f.select :format, @@ -61,11 +63,11 @@ %>

    - <%= f.text_area :format_options, rows: 4, cols: 80 %> + <%= f.text_area :format_options_string, rows: 4, cols: 80 %>

    -

    +

    diff --git a/app/views/gtt_map_layers/_map_layer.html.erb b/app/views/gtt_map_layers/_map_layer.html.erb index f994f371..3b63d438 100644 --- a/app/views/gtt_map_layers/_map_layer.html.erb +++ b/app/views/gtt_map_layers/_map_layer.html.erb @@ -1,6 +1,7 @@ "> <%= link_to map_layer.name, edit_gtt_map_layer_path(map_layer) %> <%= map_layer.layer %> + <%= map_layer.source %> <%= checked_image map_layer.baselayer? %> <%= checked_image map_layer.global? %> <%= checked_image map_layer.default? %> diff --git a/app/views/gtt_map_layers/index.html.erb b/app/views/gtt_map_layers/index.html.erb index eeb901fb..b839220f 100644 --- a/app/views/gtt_map_layers/index.html.erb +++ b/app/views/gtt_map_layers/index.html.erb @@ -10,6 +10,7 @@ <%= l :label_name %> <%= l :label_layer %> + <%= l :label_source %> <%= l :label_baselayer %> <%= l :label_global %> <%= l :label_default %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 33fec331..3c4fa86b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -15,6 +15,7 @@ en: label_baselayer: BaseLayer label_nearby: "nearby(lat,lng)" label_layer: "Layer Type" + label_source: "Source Type" label_config: Configuration label_gtt_settings: GTT label_gtt_settings_headline: GTT Settings @@ -102,7 +103,7 @@ en: select_wfs: "WFS" select_wkb: "WKB" select_wkt: "WKT" - load_example: "Load example configuration." + load_example: "Examples: " # Workaround to easily customize field labels field_name: "Name" From d9d545c2169c7fd4ab5bf807f135f589d6ee5aff Mon Sep 17 00:00:00 2001 From: Daniel Kastl Date: Thu, 13 Apr 2023 16:31:03 +0900 Subject: [PATCH 032/109] Refactors adding of layers Signed-off-by: Daniel Kastl --- .gitignore | 1 + app/views/gtt_map_layers/_form.html.erb | 81 ++++++------ package.json | 2 +- src/components/gtt-client/init/layers.ts | 116 ++++++------------ src/components/gtt-client/interfaces.ts | 32 +---- src/components/gtt-client/openlayers/index.ts | 23 ---- yarn.lock | 31 ++--- 7 files changed, 98 insertions(+), 188 deletions(-) diff --git a/.gitignore b/.gitignore index caa11f83..e853e01f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ node_modules/ # Ignore webpack generated files assets/javascripts/main.js* +assets/javascripts/*.main.js* assets/javascripts/*.png assets/javascripts/*.svg assets/javascripts/*.ttf diff --git a/app/views/gtt_map_layers/_form.html.erb b/app/views/gtt_map_layers/_form.html.erb index 207284c1..ccbe2b93 100644 --- a/app/views/gtt_map_layers/_form.html.erb +++ b/app/views/gtt_map_layers/_form.html.erb @@ -11,11 +11,11 @@

    <%= f.select :layer, options_for_select([ - [t('map_layer.layer_options.select_image'), 'ol.layer.Image'], - [t('map_layer.layer_options.select_tile'), 'ol.layer.Tile'], - [t('map_layer.layer_options.select_mapboxvector'), 'ol.layer.MapboxVector'], - [t('map_layer.layer_options.select_vector'), 'ol.layer.Vector'], - [t('map_layer.layer_options.select_vectortile'), 'ol.layer.VectorTile'], + [t('map_layer.layer_options.select_image'), 'Image'], + [t('map_layer.layer_options.select_tile'), 'Tile'], + [t('map_layer.layer_options.select_mapboxvector'), 'MapboxVector'], + [t('map_layer.layer_options.select_vector'), 'Vector'], + [t('map_layer.layer_options.select_vectortile'), 'VectorTile'], ], selected: f.object.layer ), { include_blank: t('map_layer.layer_options.select'), required: true } %> @@ -26,20 +26,20 @@

    <%= f.select :source, options_for_select([ - [t('map_layer.source_options.select_bingmaps'), 'ol.source.BingMaps'], - [t('map_layer.source_options.select_cartodb'), 'ol.source.CartoDB'], - [t('map_layer.source_options.select_imagestatic'), 'ol.source.ImageStatic'], - [t('map_layer.source_options.select_imagewms'), 'ol.source.ImageWMS'], - [t('map_layer.source_options.select_osm'), 'ol.source.OSM'], - [t('map_layer.source_options.select_raster'), 'ol.source.Raster'], - [t('map_layer.source_options.select_stamen'), 'ol.source.Stamen'], - [t('map_layer.source_options.select_tilejson'), 'ol.source.TileJSON'], - [t('map_layer.source_options.select_tilewms'), 'ol.source.TileWMS'], - [t('map_layer.source_options.select_utfgrid'), 'ol.source.UTFGrid'], - [t('map_layer.source_options.select_vector'), 'ol.source.Vector'], - [t('map_layer.source_options.select_vectortile'), 'ol.source.VectorTile'], - [t('map_layer.source_options.select_wmts'), 'ol.source.WMTS'], - [t('map_layer.source_options.select_xyz'), 'ol.source.XYZ'], + [t('map_layer.source_options.select_bingmaps'), 'BingMaps'], + [t('map_layer.source_options.select_cartodb'), 'CartoDB'], + [t('map_layer.source_options.select_imagestatic'), 'ImageStatic'], + [t('map_layer.source_options.select_imagewms'), 'ImageWMS'], + [t('map_layer.source_options.select_osm'), 'OSM'], + [t('map_layer.source_options.select_raster'), 'Raster'], + [t('map_layer.source_options.select_stamen'), 'Stamen'], + [t('map_layer.source_options.select_tilejson'), 'TileJSON'], + [t('map_layer.source_options.select_tilewms'), 'TileWMS'], + [t('map_layer.source_options.select_utfgrid'), 'UTFGrid'], + [t('map_layer.source_options.select_vector'), 'Vector'], + [t('map_layer.source_options.select_vectortile'), 'VectorTile'], + [t('map_layer.source_options.select_wmts'), 'WMTS'], + [t('map_layer.source_options.select_xyz'), 'XYZ'], ], selected: f.object.source ), { include_blank: t('map_layer.source_options.select') } %> @@ -50,14 +50,14 @@

    <%= f.select :format, options_for_select([ - [t('map_layer.format_options.select_geojson'), 'ol.format.GeoJSON'], - [t('map_layer.format_options.select_gpx'), 'ol.format.GPX'], - [t('map_layer.format_options.select_kml'), 'ol.format.KML'], - [t('map_layer.format_options.select_mvt'), 'ol.format.MVT'], - [t('map_layer.format_options.select_topojson'), 'ol.format.TopoJSON'], - [t('map_layer.format_options.select_wfs'), 'ol.format.WFS'], - [t('map_layer.format_options.select_wkb'), 'ol.format.WKB'], - [t('map_layer.format_options.select_wkt'), 'ol.format.WKT'], + [t('map_layer.format_options.select_geojson'), 'GeoJSON'], + [t('map_layer.format_options.select_gpx'), 'GPX'], + [t('map_layer.format_options.select_kml'), 'KML'], + [t('map_layer.format_options.select_mvt'), 'MVT'], + [t('map_layer.format_options.select_topojson'), 'TopoJSON'], + [t('map_layer.format_options.select_wfs'), 'WFS'], + [t('map_layer.format_options.select_wkb'), 'WKB'], + [t('map_layer.format_options.select_wkt'), 'WKT'], ], selected: f.object.format ), { include_blank: t('map_layer.format_options.select') } %> @@ -73,19 +73,19 @@