diff --git a/electron-builder.yml b/electron-builder.yml index 75c5fe00..2eeb465f 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -35,4 +35,4 @@ publish: - provider: github releaseType: release -electronVersion: 26.6.0 +electronVersion: 31.2.1 diff --git a/package-lock.json b/package-lock.json index 73ce0f0d..9ec519e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,12 +12,12 @@ "dependencies": { "@mdi/js": "^7.0.96", "@mdi/react": "^1.6.0", - "@most/scheduler": "^1.3.0", - "@syncpoint/signs": "^1.0.1", + "@syncpoint/signal": "^1.2.0", + "@syncpoint/signs": "^1.1.0", "@syncpoint/wkx": "^0.5.2", "abstract-leveldown": "^7.2.0", "color": "^4.2.3", - "fuse.js": "^6.6.2", + "fuse.js": "^7.0.0", "geo-coordinates-parser": "^1.7.3", "geodesy": "^2.4.0", "jexl": "^2.3.0", @@ -27,52 +27,52 @@ "leveldown": "^6.1.1", "levelup": "^5.0.1", "luxon": "^3.1.0", - "minisearch": "^6.0.1", - "most-subject": "^6.0.0", + "minisearch": "^7.0.1", "mousetrap": "^1.6.5", "mousetrap-global-bind": "^1.1.0", "ol": "^9.2.4", - "path-to-regexp": "^6.2.1", + "path-to-regexp": "^7.1.0", "proj4": "^2.8.0", "ramda": "^0.30.1", - "rbush": "^3.0.1", + "rbush": "^4.0.0", "react": "^18.2.0", "react-cool-virtual": "^0.7.0", "react-dom": "^18.2.0", "react-easy-sort": "^1.5.1", "react-fast-compare": "^3.2.0", - "react-tooltip": "^5.27.0", + "react-tooltip": "^5.27.1", "reproject": "^1.2.7", "sanitize-filename": "^1.6.3", "subleveldown": "^6.0.1", "throttle-debounce": "^5.0.2", - "typeface-roboto": "^1.1.13" + "typeface-roboto": "^1.1.13", + "uniqolor": "^1.1.1" }, "devDependencies": { - "@babel/core": "^7.21.4", - "@babel/eslint-parser": "^7.18.2", - "@babel/preset-env": "^7.21.4", + "@babel/core": "^7.24.9", + "@babel/eslint-parser": "^7.24.8", + "@babel/preset-env": "^7.24.8", "@babel/preset-react": "^7.17.12", "@babel/register": "^7.17.7", "babel-loader": "^9.1.0", "c8": "^10.1.2", "css-loader": "^7.1.2", - "electron": "^26.6.0", + "electron": "^31.2.1", "electron-builder": "^24.6.4", "electron-updater": "^6.1.4", - "eslint": "^8.57.0", "eslint-config-standard": "^17.0.0", - "eslint-plugin-react": "^7.34.3", + "eslint-plugin-react": "^7.34.4", "eslint-plugin-react-hooks": "^4.6.0", "file-loader": "^6.2.0", "html-webpack-plugin": "^5.5.0", "jsdoc": "^4.0.0", "memdown": "^6.1.1", - "mocha": "^10.5.2", - "sass": "^1.77.6", + "mocha": "^10.6.0", + "sass": "^1.77.8", "sass-loader": "^14.2.1", + "source-map-loader": "^5.0.0", "style-loader": "^4.0.0", - "webpack": "^5.92.1", + "webpack": "^5.93.0", "webpack-cli": "^5.0.0", "webpack-dev-server": "^5.0.4", "yaml": "^2.1.3" @@ -105,30 +105,30 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", - "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", + "version": "7.24.9", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.9.tgz", + "integrity": "sha512-e701mcfApCJqMMueQI0Fb68Amflj83+dvAvHawoBpAz+GDjCIyGHzNwnefjsWJ3xiYAqqiQFoWbspGYBdb2/ng==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", - "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", + "version": "7.24.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz", + "integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.7", - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helpers": "^7.24.7", - "@babel/parser": "^7.24.7", + "@babel/generator": "^7.24.9", + "@babel/helper-compilation-targets": "^7.24.8", + "@babel/helper-module-transforms": "^7.24.9", + "@babel/helpers": "^7.24.8", + "@babel/parser": "^7.24.8", "@babel/template": "^7.24.7", - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7", + "@babel/traverse": "^7.24.8", + "@babel/types": "^7.24.9", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -144,9 +144,9 @@ } }, "node_modules/@babel/eslint-parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.24.7.tgz", - "integrity": "sha512-SO5E3bVxDuxyNxM5agFv480YA2HO6ohZbGxbazZdIk3KQOPOGVNw6q78I9/lbviIf95eq6tPozeYnJLbjnC8IA==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.24.8.tgz", + "integrity": "sha512-nYAikI4XTGokU2QX7Jx+v4rxZKhKivaQaREZjuW3mrJrbdWJ5yUfohnoUULge+zEEaKjPYNxhoRgUKktjXtbwA==", "dev": true, "dependencies": { "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", @@ -162,12 +162,12 @@ } }, "node_modules/@babel/generator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", - "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", + "version": "7.24.9", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.9.tgz", + "integrity": "sha512-G8v3jRg+z8IwY1jHFxvCNhOPYPterE4XljNgdGTYfSTtzzwjIswIzIaSPSLs3R7yFuqnqNeay5rjICfqVr+/6A==", "dev": true, "dependencies": { - "@babel/types": "^7.24.7", + "@babel/types": "^7.24.9", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" @@ -202,14 +202,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", - "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.8.tgz", + "integrity": "sha512-oU+UoqCHdp+nWVDkpldqIQL/i/bvAv53tRqLG/s+cOXxe66zOYLU7ar/Xs3LdmBihrUMEUhwu6dMZwbNOYDwvw==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.24.7", - "@babel/helper-validator-option": "^7.24.7", - "browserslist": "^4.22.2", + "@babel/compat-data": "^7.24.8", + "@babel/helper-validator-option": "^7.24.8", + "browserslist": "^4.23.1", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -337,9 +337,9 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", - "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", + "version": "7.24.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.9.tgz", + "integrity": "sha512-oYbh+rtFKj/HwBQkFlUzvcybzklmVdVV3UU+mN7n2t/q3yGHbuVdNxyFvSBO1tfvjyArpHNcWMAzsSPdyI46hw==", "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.24.7", @@ -368,9 +368,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", - "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", "dev": true, "engines": { "node": ">=6.9.0" @@ -449,9 +449,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", - "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", "dev": true, "engines": { "node": ">=6.9.0" @@ -467,9 +467,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", - "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", "dev": true, "engines": { "node": ">=6.9.0" @@ -491,13 +491,13 @@ } }, "node_modules/@babel/helpers": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", - "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.8.tgz", + "integrity": "sha512-gV2265Nkcz7weJJfvDoAEVzC1e2OTDpkGbEsebse8koXUJUXPsCMi7sRo/+SPMuMZ9MtUPnGwITTnQnU5YjyaQ==", "dev": true, "dependencies": { "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/types": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -519,9 +519,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", - "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.8.tgz", + "integrity": "sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -970,16 +970,16 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.7.tgz", - "integrity": "sha512-CFbbBigp8ln4FU6Bpy6g7sE8B/WmCmzvivzUC6xDAdWVsjYTXijpuuGJmYkAaoWAzcItGKT3IOAbxRItZ5HTjw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.8.tgz", + "integrity": "sha512-VXy91c47uujj758ud9wx+OMgheXm4qJfyhj1P18YvlrQkNOSrwsteHk+EFS3OMGfhMhpZa0A+81eE7G4QC+3CA==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.8", "@babel/helper-environment-visitor": "^7.24.7", "@babel/helper-function-name": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.8", "@babel/helper-replace-supers": "^7.24.7", "@babel/helper-split-export-declaration": "^7.24.7", "globals": "^11.1.0" @@ -1008,12 +1008,12 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.7.tgz", - "integrity": "sha512-19eJO/8kdCQ9zISOf+SEUJM/bAUIsvY3YDnXZTupUCQ8LgrWnsG/gFB9dvXqdXnRXMAM8fvt7b0CBKQHNGy1mw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.8.tgz", + "integrity": "sha512-36e87mfY8TnRxc7yc6M9g9gOB7rKgSahqkIKwLpz4Ppk2+zC2Cy1is0uwtuSG6AE4zlTOUa+7JGz9jCJGLqQFQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -1213,13 +1213,13 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.7.tgz", - "integrity": "sha512-iFI8GDxtevHJ/Z22J5xQpVqFLlMNstcLXh994xifFwxxGslr2ZXXLWgtBeLctOD63UFDArdvN6Tg8RFw+aEmjQ==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.8.tgz", + "integrity": "sha512-WHsk9H8XxRs3JXKWFiqtQebdh9b/pTk4EgueygFzYlTKAg0Ud985mSevdNjdXdFBATSKVJGQXP1tv6aGbssLKA==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.8", + "@babel/helper-plugin-utils": "^7.24.8", "@babel/helper-simple-access": "^7.24.7" }, "engines": { @@ -1377,12 +1377,12 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.7.tgz", - "integrity": "sha512-tK+0N9yd4j+x/4hxF3F0e0fu/VdcxU18y5SevtyM/PCFlQvXbR0Zmlo2eBrKtVipGNFzpq56o8WsIIKcJFUCRQ==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.8.tgz", + "integrity": "sha512-5cTOLSMs9eypEy8JUVvIKOu6NgvbJMnpG62VpIHrTmROdQ+L5mDAaI40g25k5vXti55JWNX5jCkq3HZxXBQANw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.8", "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", "@babel/plugin-syntax-optional-chaining": "^7.8.3" }, @@ -1615,12 +1615,12 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.7.tgz", - "integrity": "sha512-VtR8hDy7YLB7+Pet9IarXjg/zgCMSF+1mNS/EQEiEaUPoFXCVsHG64SIxcaaI2zJgRiv+YmgaQESUfWAdbjzgg==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.8.tgz", + "integrity": "sha512-adNTUpDCVnmAE58VEqKlAA6ZBlNkMnWD0ZcW76lyNFN3MJniyGFZfNwERVk8Ap56MCnXztmDr19T4mPTztcuaw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -1693,15 +1693,15 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.7.tgz", - "integrity": "sha512-1YZNsc+y6cTvWlDHidMBsQZrZfEFjRIo/BZCT906PMdzOyXtSLTgqGdrpcuTDCXyd11Am5uQULtDIcCfnTc8fQ==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.8.tgz", + "integrity": "sha512-vObvMZB6hNWuDxhSaEPTKCwcqkAIuDtE+bQGn4XMXne1DSLzFVY8Vmj1bm+mUQXYNN8NmaQEO+r8MMbzPr1jBQ==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.24.7", - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-validator-option": "^7.24.7", + "@babel/compat-data": "^7.24.8", + "@babel/helper-compilation-targets": "^7.24.8", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-validator-option": "^7.24.8", "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.24.7", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.24.7", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.7", @@ -1732,9 +1732,9 @@ "@babel/plugin-transform-block-scoping": "^7.24.7", "@babel/plugin-transform-class-properties": "^7.24.7", "@babel/plugin-transform-class-static-block": "^7.24.7", - "@babel/plugin-transform-classes": "^7.24.7", + "@babel/plugin-transform-classes": "^7.24.8", "@babel/plugin-transform-computed-properties": "^7.24.7", - "@babel/plugin-transform-destructuring": "^7.24.7", + "@babel/plugin-transform-destructuring": "^7.24.8", "@babel/plugin-transform-dotall-regex": "^7.24.7", "@babel/plugin-transform-duplicate-keys": "^7.24.7", "@babel/plugin-transform-dynamic-import": "^7.24.7", @@ -1747,7 +1747,7 @@ "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", "@babel/plugin-transform-member-expression-literals": "^7.24.7", "@babel/plugin-transform-modules-amd": "^7.24.7", - "@babel/plugin-transform-modules-commonjs": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-modules-systemjs": "^7.24.7", "@babel/plugin-transform-modules-umd": "^7.24.7", "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", @@ -1757,7 +1757,7 @@ "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-object-super": "^7.24.7", "@babel/plugin-transform-optional-catch-binding": "^7.24.7", - "@babel/plugin-transform-optional-chaining": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.8", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", @@ -1768,7 +1768,7 @@ "@babel/plugin-transform-spread": "^7.24.7", "@babel/plugin-transform-sticky-regex": "^7.24.7", "@babel/plugin-transform-template-literals": "^7.24.7", - "@babel/plugin-transform-typeof-symbol": "^7.24.7", + "@babel/plugin-transform-typeof-symbol": "^7.24.8", "@babel/plugin-transform-unicode-escapes": "^7.24.7", "@babel/plugin-transform-unicode-property-regex": "^7.24.7", "@babel/plugin-transform-unicode-regex": "^7.24.7", @@ -1777,7 +1777,7 @@ "babel-plugin-polyfill-corejs2": "^0.4.10", "babel-plugin-polyfill-corejs3": "^0.10.4", "babel-plugin-polyfill-regenerator": "^0.6.1", - "core-js-compat": "^3.31.0", + "core-js-compat": "^3.37.1", "semver": "^6.3.1" }, "engines": { @@ -1872,19 +1872,19 @@ } }, "node_modules/@babel/traverse": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", - "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.8.tgz", + "integrity": "sha512-t0P1xxAPzEDcEPmjprAQq19NWum4K0EQPjMwZQZbHt+GiZqvjCHjj755Weq1YRPVzBI+3zSfvScfpnuIecVFJQ==", "dev": true, "dependencies": { "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.7", + "@babel/generator": "^7.24.8", "@babel/helper-environment-visitor": "^7.24.7", "@babel/helper-function-name": "^7.24.7", "@babel/helper-hoist-variables": "^7.24.7", "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7", + "@babel/parser": "^7.24.8", + "@babel/types": "^7.24.8", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -1893,12 +1893,12 @@ } }, "node_modules/@babel/types": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", - "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", + "version": "7.24.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.9.tgz", + "integrity": "sha512-xm8XrMKz0IlUdocVbYJe0Z9xEgidU7msskG8BbhnTPK/HZ2z/7FP7ykqPgrUH+C+r414mNfNWam1f2vqOjqjYQ==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-string-parser": "^7.24.8", "@babel/helper-validator-identifier": "^7.24.7", "to-fast-properties": "^2.0.0" }, @@ -2197,6 +2197,7 @@ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", "dev": true, + "peer": true, "dependencies": { "eslint-visitor-keys": "^3.3.0" }, @@ -2212,6 +2213,7 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, + "peer": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -2224,6 +2226,7 @@ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.1.tgz", "integrity": "sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==", "dev": true, + "peer": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } @@ -2233,6 +2236,7 @@ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, + "peer": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -2256,6 +2260,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, + "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2266,6 +2271,7 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, + "peer": true, "dependencies": { "type-fest": "^0.20.2" }, @@ -2281,6 +2287,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2293,6 +2300,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, + "peer": true, "engines": { "node": ">=10" }, @@ -2305,6 +2313,7 @@ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", "dev": true, + "peer": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } @@ -2345,6 +2354,7 @@ "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", "deprecated": "Use @eslint/config-array instead", "dev": true, + "peer": true, "dependencies": { "@humanwhocodes/object-schema": "^2.0.2", "debug": "^4.3.1", @@ -2359,6 +2369,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, + "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2369,6 +2380,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2381,6 +2393,7 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, + "peer": true, "engines": { "node": ">=12.22" }, @@ -2394,7 +2407,8 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "deprecated": "Use @eslint/object-schema instead", - "dev": true + "dev": true, + "peer": true }, "node_modules/@isaacs/cliui": { "version": "8.0.2", @@ -2717,64 +2731,6 @@ "prop-types": "^15.7.2" } }, - "node_modules/@most/core": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@most/core/-/core-0.14.0.tgz", - "integrity": "sha512-Ns8ckDZ7iGKfUgM9Ljp+fR2gnq71j5cAGXTWEDCSZH2KaLRfxs7pp0/oxljNN3aIvcQQcK1MD1Jf9mGo+aUiUA==", - "dependencies": { - "@most/disposable": "^0.13.1", - "@most/prelude": "^1.6.4", - "@most/scheduler": "^0.13.1", - "@most/types": "^0.11.1" - } - }, - "node_modules/@most/core/node_modules/@most/scheduler": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/@most/scheduler/-/scheduler-0.13.1.tgz", - "integrity": "sha512-t14TLvEfUYPSLJQSLT/V5bQYxvKdLib8KFrz0hniOHZRoqyBgt6zhHKP1zywrEYQd9+sdhS5SZDnX9wAJ+xJxg==", - "dependencies": { - "@most/prelude": "^1.6.4", - "@most/types": "^0.11.1" - } - }, - "node_modules/@most/core/node_modules/@most/types": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/@most/types/-/types-0.11.1.tgz", - "integrity": "sha512-acP+xy7F4W50GAbpOmfdNgATPwCfMUSLmWEhUb4MHli9k0qAtN828LTVPgK3AsTBh/Fkx0TulbgW+H2t06BsKA==" - }, - "node_modules/@most/disposable": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/@most/disposable/-/disposable-0.13.1.tgz", - "integrity": "sha512-2Z89nEjdEJ6RvCzwyWZK6xv2avt2gY2NSkceemOQ869ggGDB2ZFA/ZMuyZag2KVoCyjb9zMOhfOgQw1gD/m9QA==", - "dependencies": { - "@most/prelude": "^1.6.4", - "@most/types": "^0.11.1" - } - }, - "node_modules/@most/disposable/node_modules/@most/types": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/@most/types/-/types-0.11.1.tgz", - "integrity": "sha512-acP+xy7F4W50GAbpOmfdNgATPwCfMUSLmWEhUb4MHli9k0qAtN828LTVPgK3AsTBh/Fkx0TulbgW+H2t06BsKA==" - }, - "node_modules/@most/prelude": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@most/prelude/-/prelude-1.8.0.tgz", - "integrity": "sha512-t1CcURpZzfmBA6fEWwqmCqeNzWAj1w2WqEmCk/2yXMe/p8Ut000wFmVKMy8A1Rl9VVxZEZ5nBHd/pU0dR4bv/w==" - }, - "node_modules/@most/scheduler": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@most/scheduler/-/scheduler-1.3.0.tgz", - "integrity": "sha512-bbJGyhbZxNqlkVjP8+YT9wIVMvYnpzWOzV8jZueqlTH2PJWewH2f54YziZn7/wWa6AJdN03H1vb8Tbi9GcA/cw==", - "dependencies": { - "@most/prelude": "^1.8.0", - "@most/types": "^1.1.0" - } - }, - "node_modules/@most/types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@most/types/-/types-1.1.0.tgz", - "integrity": "sha512-v2trqAWu1jqP4Yd/CyI1O6mAeJyygK1uJOrFRpNPkPZIaYw4khA4EQe4WzcyOFKuXdiP8qAqaxGtXXJJ2LZdXg==" - }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -2789,6 +2745,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, + "peer": true, "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -2802,6 +2759,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, + "peer": true, "engines": { "node": ">= 8" } @@ -2811,6 +2769,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, + "peer": true, "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -2977,22 +2936,18 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "node_modules/@syncpoint/signal": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@syncpoint/signal/-/signal-1.2.0.tgz", + "integrity": "sha512-h4pEjSOBdDzl2ICog5TlPpy++uLRXE3pgC9mfy6ivfH8gZRA4earsKIq7wNMBBamqHZGQ3RU59HC9oaravP40g==" + }, "node_modules/@syncpoint/signs": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@syncpoint/signs/-/signs-1.0.1.tgz", - "integrity": "sha512-HrVbTs6UHgUvx4rc3Q5gDe3KPGQmaRtXe4a+jPvw9hp1M/C02Y4/18BbnggvmTRjD1WRg3+ff4vBFS2rI3gYzA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@syncpoint/signs/-/signs-1.1.0.tgz", + "integrity": "sha512-SOXi5y1znjQ77hjxrYEudy76K6CC1NNkzqqTu4gj1nFMu3qzAdsK5KD9xH5e8jEfaEO9YtVYVGnBlmfUhQOq2w==", "dependencies": { - "ramda": "^0.28.0", - "svg-path-bbox": "^1.2.4" - } - }, - "node_modules/@syncpoint/signs/node_modules/ramda": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.28.0.tgz", - "integrity": "sha512-9QnLuG/kPVgWvMQ4aODhsBUFKOUmnbUnsSXACv+NCQZcHbeb+v8Lodp8OVxtRULN1/xOyYLLaL6npE6dMq5QTA==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda" + "ramda": "^0.30.1", + "svg-path-bbox": "^2.0.0" } }, "node_modules/@syncpoint/wkx": { @@ -3374,7 +3329,8 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@webassemblyjs/ast": { "version": "1.12.1", @@ -3656,6 +3612,7 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, + "peer": true, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -3766,9 +3723,9 @@ } }, "node_modules/ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true, "engines": { "node": ">=6" @@ -5179,6 +5136,7 @@ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, + "peer": true, "engines": { "node": ">=6" } @@ -6156,7 +6114,8 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true + "dev": true, + "peer": true }, "node_modules/default-browser": { "version": "5.2.1", @@ -6313,9 +6272,9 @@ "dev": true }, "node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "dev": true, "engines": { "node": ">=0.3.1" @@ -6448,6 +6407,7 @@ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, + "peer": true, "dependencies": { "esutils": "^2.0.2" }, @@ -6597,14 +6557,14 @@ } }, "node_modules/electron": { - "version": "26.6.10", - "resolved": "https://registry.npmjs.org/electron/-/electron-26.6.10.tgz", - "integrity": "sha512-pV2SD0RXzAiNRb/2yZrsVmVkBOMrf+DVsPulIgRjlL0+My9BL5spFuhHVMQO9yHl9tFpWtuRpQv0ofM/i9P8xg==", + "version": "31.2.1", + "resolved": "https://registry.npmjs.org/electron/-/electron-31.2.1.tgz", + "integrity": "sha512-g3CLKjl4yuXt6VWm/KpgEjYYhFiCl19RgUn8lOC8zV/56ZXAS3+mqV4wWzicE/7vSYXs6GRO7vkYRwrwhX3Gaw==", "dev": true, "hasInstallScript": true, "dependencies": { "@electron/get": "^2.0.0", - "@types/node": "^18.11.18", + "@types/node": "^20.9.0", "extract-zip": "^2.0.1" }, "bin": { @@ -6964,12 +6924,12 @@ "dev": true }, "node_modules/electron-updater": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.2.1.tgz", - "integrity": "sha512-83eKIPW14qwZqUUM6wdsIRwVKZyjmHxQ4/8G+1C6iS5PdDt7b1umYQyj1/qPpH510GmHEQe4q0kCPe3qmb3a0Q==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.3.0.tgz", + "integrity": "sha512-3Xlezhk+dKaSQrOnkQNqCGiuGSSUPO9BV9TQZ4Iig6AyTJ4FzJONE5gFFc382sY53Sh9dwJfzKsA3DxRHt2btw==", "dev": true, "dependencies": { - "builder-util-runtime": "9.2.4", + "builder-util-runtime": "9.2.5", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", @@ -6979,6 +6939,19 @@ "tiny-typed-emitter": "^2.1.0" } }, + "node_modules/electron-updater/node_modules/builder-util-runtime": { + "version": "9.2.5", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.5.tgz", + "integrity": "sha512-HjIDfhvqx/8B3TDN4GbABQcgpewTU4LMRTQPkVpKYV3lsuxEJoIfvg09GyWTNmfVNSUAYf+fbTN//JX4TH20pg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/electron-updater/node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -7027,9 +7000,9 @@ } }, "node_modules/electron/node_modules/@types/node": { - "version": "18.19.34", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.34.tgz", - "integrity": "sha512-eXF4pfBNV5DAMKGbI02NnDtWrQ40hAN558/2vvS4gMpMIxaf6JmD7YjnZbq0Q9TDSSkKBamime8ewRoomHdt4g==", + "version": "20.14.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.9.tgz", + "integrity": "sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -7356,6 +7329,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -7727,9 +7701,9 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.34.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.3.tgz", - "integrity": "sha512-aoW4MV891jkUulwDApQbPYTVZmeuSyFrudpbTAQuj5Fv8VL+o6df2xIGpw8B0hPjAaih1/Fb0om9grCdyFYemA==", + "version": "7.34.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.4.tgz", + "integrity": "sha512-Np+jo9bUwJNxCsT12pXtrGhJgT3T44T1sHhn1Ssr42XFn8TES0267wPGo5nNrMHi8qkyimDAX2BUmkf9pSaVzA==", "dev": true, "dependencies": { "array-includes": "^3.1.8", @@ -7740,16 +7714,17 @@ "doctrine": "^2.1.0", "es-iterator-helpers": "^1.0.19", "estraverse": "^5.3.0", + "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.8", "object.fromentries": "^2.0.8", - "object.hasown": "^1.1.4", "object.values": "^1.2.0", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.11" + "string.prototype.matchall": "^4.0.11", + "string.prototype.repeat": "^1.0.0" }, "engines": { "node": ">=4" @@ -7857,6 +7832,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -7872,6 +7848,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, + "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -7882,6 +7859,7 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -7898,6 +7876,7 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -7909,13 +7888,15 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "peer": true }, "node_modules/eslint/node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, + "peer": true, "engines": { "node": ">=10" }, @@ -7928,6 +7909,7 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, + "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -7944,6 +7926,7 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, + "peer": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -7956,6 +7939,7 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, + "peer": true, "dependencies": { "type-fest": "^0.20.2" }, @@ -7971,6 +7955,7 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "peer": true, "engines": { "node": ">=8" } @@ -7980,6 +7965,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -7992,6 +7978,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -8004,6 +7991,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, + "peer": true, "engines": { "node": ">=10" }, @@ -8016,6 +8004,7 @@ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, + "peer": true, "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", @@ -8033,6 +8022,7 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, + "peer": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -8045,6 +8035,7 @@ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", "dev": true, + "peer": true, "dependencies": { "estraverse": "^5.1.0" }, @@ -8269,7 +8260,8 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/fastest-levenshtein": { "version": "1.0.16", @@ -8290,6 +8282,7 @@ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dev": true, + "peer": true, "dependencies": { "reusify": "^1.0.4" } @@ -8325,6 +8318,7 @@ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, + "peer": true, "dependencies": { "flat-cache": "^3.0.4" }, @@ -8468,6 +8462,7 @@ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, + "peer": true, "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", @@ -8481,7 +8476,8 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/follow-redirects": { "version": "1.15.6", @@ -8673,9 +8669,9 @@ } }, "node_modules/fuse.js": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-6.6.2.tgz", - "integrity": "sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.0.0.tgz", + "integrity": "sha512-14F4hBIxqKvD4Zz/XjDc3y94mNZN6pRv3U13Udo0lNLCWRBUsrMv2xwcF/y/Z5sV6+FQW+/ow68cHpm4sunt8Q==", "engines": { "node": ">=10" } @@ -8886,6 +8882,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, + "peer": true, "dependencies": { "is-glob": "^4.0.3" }, @@ -9089,7 +9086,8 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true + "dev": true, + "peer": true }, "node_modules/handle-thing": { "version": "2.0.1", @@ -9627,6 +9625,7 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", "dev": true, + "peer": true, "engines": { "node": ">= 4" } @@ -9642,6 +9641,7 @@ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "dev": true, + "peer": true, "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -9741,6 +9741,7 @@ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, + "peer": true, "engines": { "node": ">=0.8.19" } @@ -10173,6 +10174,7 @@ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true, + "peer": true, "engines": { "node": ">=8" } @@ -10785,7 +10787,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/json-stringify-safe": { "version": "5.0.1", @@ -10897,6 +10900,14 @@ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/kbar/node_modules/fuse.js": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-6.6.2.tgz", + "integrity": "sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==", + "engines": { + "node": ">=10" + } + }, "node_modules/kbar/node_modules/react-virtual": { "version": "2.10.4", "resolved": "https://registry.npmjs.org/react-virtual/-/react-virtual-2.10.4.tgz", @@ -11105,6 +11116,7 @@ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, + "peer": true, "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -11224,7 +11236,8 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true + "dev": true, + "peer": true }, "node_modules/lodash.union": { "version": "4.6.0", @@ -11832,9 +11845,9 @@ "dev": true }, "node_modules/minisearch": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-6.3.0.tgz", - "integrity": "sha512-ihFnidEeU8iXzcVHy74dhkxh/dn8Dc08ERl0xwoMMGqp4+LvRSCgicb+zGqWthVokQKvCSxITlh3P08OzdTYCQ==" + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.0.1.tgz", + "integrity": "sha512-xLeX/AwTJLzgBF2/bdUI7MEePwXtzaLExkRwu8YFGfLDwSe06KYkplqPodLANsqvfc5Ks/r5ItFUSjIp7+9xtw==" }, "node_modules/minizlib": { "version": "2.1.2", @@ -11868,31 +11881,31 @@ } }, "node_modules/mocha": { - "version": "10.5.2", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.5.2.tgz", - "integrity": "sha512-9btlN3JKCefPf+vKd/kcKz2SXxi12z6JswkGfaAF0saQvnsqLJk504ZmbxhSoENge08E9dsymozKgFMTl5PQsA==", + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.6.0.tgz", + "integrity": "sha512-hxjt4+EEB0SA0ZDygSS015t65lJw/I2yRCS3Ae+SJ5FrbzrXgfYwJr96f0OvIXdj7h4lv/vLCrH3rkiuizFSvw==", "dev": true, "dependencies": { - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", "chokidar": "^3.5.3", - "debug": "4.3.4", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "8.1.0", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "5.0.1", - "ms": "2.1.3", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "workerpool": "6.2.1", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" }, "bin": { "_mocha": "bin/_mocha", @@ -11902,29 +11915,6 @@ "node": ">= 14.0.0" } }, - "node_modules/mocha/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/mocha/node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/mocha/node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -11966,18 +11956,6 @@ "node": ">=8" } }, - "node_modules/mocha/node_modules/minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/mocha/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -11999,35 +11977,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/mocha/node_modules/yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/most-subject": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/most-subject/-/most-subject-6.0.0.tgz", - "integrity": "sha512-Fg55kwepLJ4l6tI/iOWaKDV0pNpsv0KMprr4skdZlKu292bN0xkT8zE12CybkVhivWlQP3QrweSk8eOKoJUMmA==", - "dependencies": { - "@most/core": "0.14.0", - "@most/prelude": "1.6.4", - "@most/types": "0.11.1" - } - }, - "node_modules/most-subject/node_modules/@most/prelude": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/@most/prelude/-/prelude-1.6.4.tgz", - "integrity": "sha512-RsT1xRIEc+rCCTZPL3v/tAC+dX1qt1q00ZofEtCJEMEsVg7zT+WLXiVQdhcRqqh4baQTYmDPArXrQWyd7GkqAA==" - }, - "node_modules/most-subject/node_modules/@most/types": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/@most/types/-/types-0.11.1.tgz", - "integrity": "sha512-acP+xy7F4W50GAbpOmfdNgATPwCfMUSLmWEhUb4MHli9k0qAtN828LTVPgK3AsTBh/Fkx0TulbgW+H2t06BsKA==" - }, "node_modules/mousetrap": { "version": "1.6.5", "resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.5.tgz", @@ -12095,7 +12044,8 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/negotiator": { "version": "0.6.3", @@ -12717,23 +12667,6 @@ "node": ">= 0.4" } }, - "node_modules/object.hasown": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.4.tgz", - "integrity": "sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg==", - "dev": true, - "dependencies": { - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/object.values": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", @@ -12774,6 +12707,14 @@ "url": "https://opencollective.com/openlayers" } }, + "node_modules/ol/node_modules/rbush": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/rbush/-/rbush-3.0.1.tgz", + "integrity": "sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==", + "dependencies": { + "quickselect": "^2.0.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -12842,6 +12783,7 @@ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, + "peer": true, "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", @@ -12965,6 +12907,7 @@ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, + "peer": true, "dependencies": { "callsites": "^3.0.0" }, @@ -13084,9 +13027,12 @@ } }, "node_modules/path-to-regexp": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", - "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==" + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-7.1.0.tgz", + "integrity": "sha512-ZToe+MbUF4lBqk6dV8GKot4DKfzrxXsplOddH8zN3YK+qw9/McvP7+4ICjZvOne0jQhN4eJwHsX6tT0Ns19fvw==", + "engines": { + "node": ">=16" + } }, "node_modules/pause-stream": { "version": "0.0.11", @@ -13363,6 +13309,7 @@ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, + "peer": true, "engines": { "node": ">= 0.8.0" } @@ -13611,9 +13558,9 @@ } }, "node_modules/rbush": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/rbush/-/rbush-3.0.1.tgz", - "integrity": "sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/rbush/-/rbush-4.0.0.tgz", + "integrity": "sha512-F5xw+166FYDZI6jEcz+sWEHL5/J+du3kQWkwqWrPKb6iVoLPZh+2KhTS4OoYqrw1v/RO1xQe6WsLwBvrUAlvXw==", "dependencies": { "quickselect": "^2.0.0" } @@ -13692,9 +13639,9 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/react-tooltip": { - "version": "5.27.0", - "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.27.0.tgz", - "integrity": "sha512-JXROcdfCEbCqkAkh8LyTSP3guQ0dG53iY2E2o4fw3D8clKzziMpE6QG6CclDaHELEKTzpMSeAOsdtg0ahoQosw==", + "version": "5.27.1", + "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.27.1.tgz", + "integrity": "sha512-a+micPXcMOMt11CYlwJD4XShcqGziasHco4NPe1OFw298WBTILMyzUgNC1LAFViAe791JdHNVSJIpzhZm2MvDA==", "dependencies": { "@floating-ui/dom": "^1.6.1", "classnames": "^2.3.0" @@ -14162,6 +14109,7 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, + "peer": true, "engines": { "node": ">=4" } @@ -14210,6 +14158,7 @@ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true, + "peer": true, "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -14230,6 +14179,7 @@ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, + "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -14289,6 +14239,7 @@ "url": "https://feross.org/support" } ], + "peer": true, "dependencies": { "queue-microtask": "^1.2.2" } @@ -14362,9 +14313,9 @@ } }, "node_modules/sass": { - "version": "1.77.6", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.6.tgz", - "integrity": "sha512-ByXE1oLD79GVq9Ht1PeHWCPMPB8XHpBuz1r85oByKHjZY6qV6rWnQovQzXJXuQ/XyE1Oj3iPk3lo28uzaRA2/Q==", + "version": "1.77.8", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.8.tgz", + "integrity": "sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -14697,9 +14648,9 @@ } }, "node_modules/serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "dependencies": { "randombytes": "^2.1.0" @@ -15089,6 +15040,26 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz", + "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==", + "dev": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.72.1" + } + }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -15355,6 +15326,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, "node_modules/string.prototype.trim": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", @@ -15543,9 +15524,9 @@ } }, "node_modules/svg-path-bbox": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/svg-path-bbox/-/svg-path-bbox-1.2.6.tgz", - "integrity": "sha512-jKkQcvBrjUdzDFOIas2E1g5ICVdZ/YdjDjhvwbfXRYmodN3qIR6pBAzQhLEpn11cb1Q2Hn+ufFqER5wDHr6osg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/svg-path-bbox/-/svg-path-bbox-2.0.0.tgz", + "integrity": "sha512-DP/dcKuwjfJ2GXiM1RsIKcWv+aGazBXTYPuAH9pWYZVm5+pZ6ho70BeLB0inqUGDCCHDmcUlQ2OcLlGuwhmkKQ==", "dependencies": { "svgpath": "^2.6.0" }, @@ -15746,15 +15727,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/terser-webpack-plugin/node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -15834,7 +15806,8 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/thingies": { "version": "1.21.0", @@ -16006,6 +15979,7 @@ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, + "peer": true, "dependencies": { "prelude-ls": "^1.2.1" }, @@ -16209,6 +16183,11 @@ "node": ">=4" } }, + "node_modules/uniqolor": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/uniqolor/-/uniqolor-1.1.1.tgz", + "integrity": "sha512-HUwezlXCwm5bzsEXW7AP7ybezH13uWENRgYT+3dOdhJPvpYucSqvIGckMiLn+Uy2j0NVf3fPp43uZ4aun3t4Ww==" + }, "node_modules/unique-filename": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", @@ -16415,9 +16394,9 @@ "integrity": "sha512-BSR9wyRsy/KOValMgd5kMyr3JzpdeoR9KVId8u5GVlTTAtNChlsE4yTxeY7zMdNSyOmoKBv8NH2qeRY9Tg+IaA==" }, "node_modules/webpack": { - "version": "5.92.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.92.1.tgz", - "integrity": "sha512-JECQ7IwJb+7fgUFBlrJzbyu3GEuNBcdqr1LD7IbSzwkSmIevTm8PF+wej3Oxuz/JFBUZ6O1o43zsPkwm1C4TmA==", + "version": "5.93.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz", + "integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==", "dev": true, "dependencies": { "@types/eslint-scope": "^3.7.3", @@ -16888,14 +16867,15 @@ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, + "peer": true, "engines": { "node": ">=0.10.0" } }, "node_modules/workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", "dev": true }, "node_modules/wrap-ansi": { @@ -17006,9 +16986,9 @@ "dev": true }, "node_modules/ws": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", - "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "dev": true, "engines": { "node": ">=10.0.0" diff --git a/package.json b/package.json index ca141a01..0a895c2e 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "description": "Open Source Command and Control Information System (C2IS)", "main": "dist/main.js", "scripts": { - "start": "electron . --cold", + "start": "electron . --cold --trace-warnings", "hot": "webpack serve", "webpack": "webpack", "webpack:production": "webpack --mode=production", @@ -28,30 +28,30 @@ }, "license": "AGPLv3", "devDependencies": { - "@babel/core": "^7.21.4", - "@babel/eslint-parser": "^7.18.2", - "@babel/preset-env": "^7.21.4", + "@babel/core": "^7.24.9", + "@babel/eslint-parser": "^7.24.8", + "@babel/preset-env": "^7.24.8", "@babel/preset-react": "^7.17.12", "@babel/register": "^7.17.7", "babel-loader": "^9.1.0", "c8": "^10.1.2", "css-loader": "^7.1.2", - "electron": "^26.6.0", + "electron": "^31.2.1", "electron-builder": "^24.6.4", "electron-updater": "^6.1.4", - "eslint": "^8.57.0", "eslint-config-standard": "^17.0.0", - "eslint-plugin-react": "^7.34.3", + "eslint-plugin-react": "^7.34.4", "eslint-plugin-react-hooks": "^4.6.0", "file-loader": "^6.2.0", "html-webpack-plugin": "^5.5.0", "jsdoc": "^4.0.0", "memdown": "^6.1.1", - "mocha": "^10.5.2", - "sass": "^1.77.6", + "mocha": "^10.6.0", + "sass": "^1.77.8", "sass-loader": "^14.2.1", + "source-map-loader": "^5.0.0", "style-loader": "^4.0.0", - "webpack": "^5.92.1", + "webpack": "^5.93.0", "webpack-cli": "^5.0.0", "webpack-dev-server": "^5.0.4", "yaml": "^2.1.3" @@ -64,12 +64,12 @@ "dependencies": { "@mdi/js": "^7.0.96", "@mdi/react": "^1.6.0", - "@most/scheduler": "^1.3.0", - "@syncpoint/signs": "^1.0.1", + "@syncpoint/signal": "^1.2.0", + "@syncpoint/signs": "^1.1.0", "@syncpoint/wkx": "^0.5.2", "abstract-leveldown": "^7.2.0", "color": "^4.2.3", - "fuse.js": "^6.6.2", + "fuse.js": "^7.0.0", "geo-coordinates-parser": "^1.7.3", "geodesy": "^2.4.0", "jexl": "^2.3.0", @@ -79,25 +79,25 @@ "leveldown": "^6.1.1", "levelup": "^5.0.1", "luxon": "^3.1.0", - "minisearch": "^6.0.1", - "most-subject": "^6.0.0", + "minisearch": "^7.0.1", "mousetrap": "^1.6.5", "mousetrap-global-bind": "^1.1.0", "ol": "^9.2.4", - "path-to-regexp": "^6.2.1", + "path-to-regexp": "^7.1.0", "proj4": "^2.8.0", "ramda": "^0.30.1", - "rbush": "^3.0.1", + "rbush": "^4.0.0", "react": "^18.2.0", "react-cool-virtual": "^0.7.0", "react-dom": "^18.2.0", "react-easy-sort": "^1.5.1", "react-fast-compare": "^3.2.0", - "react-tooltip": "^5.27.0", + "react-tooltip": "^5.27.1", "reproject": "^1.2.7", "sanitize-filename": "^1.6.3", "subleveldown": "^6.0.1", "throttle-debounce": "^5.0.2", - "typeface-roboto": "^1.1.13" + "typeface-roboto": "^1.1.13", + "uniqolor": "^1.1.1" } } diff --git a/paperwork/polygon-styles.md b/paperwork/polygon-styles.md new file mode 100644 index 00000000..0204dcb3 --- /dev/null +++ b/paperwork/polygon-styles.md @@ -0,0 +1,80 @@ +```mermaid +graph LR; + +classDef common fill:#ff9; +classDef graphics fill:#f99; +classDef polygon fill:#efe; + +%% Inputs +properties[[properties]]; +geometry[[geometry]]; +globalStyle[[globalStyle]]; +layerStyle[[layerStyle]]; +featureStyle[[featureStyle]]; +centerResolution[[centerResolution]]; +selectionMode[[selectionMode]]; + +styleFactory[[styleFactory]]; + +%% Common +properties --> sidc:::common; +sidc --> parameterizedSIDC:::common; +globalStyle --> colorScheme:::common; +layerStyle --> colorScheme; +featureStyle --> colorScheme; +sidc --> schemeStyle:::common; +colorScheme --> schemeStyle; +globalStyle --> effectiveStyle:::common; +schemeStyle --> effectiveStyle; +layerStyle --> effectiveStyle; +featureStyle --> effectiveStyle; +effectiveStyle --> styleRegistry:::common; + +%% Graphics +geometry --> read:::graphics; +geometry --> rewrite:::graphics; +geometry --> pointResolution:::graphics; +centerResolution --> resolution:::graphics; +pointResolution --> resolution; +geometry --> jtsGeometry:::graphics; +read --> jtsGeometry; +resolution --> clip:::graphics; +sidc --> specialization:::graphics; +specialization --> geometryProperties:::graphics; +jtsGeometry --> geometryProperties; +sidc --> evalSync:::graphics; +properties --> evalSync; +geometryProperties --> evalSync; + +%% Polygon +geometry --> simplifiedGeometry:::polygon; +centerResolution --> simplifiedGeometry; +simplifiedGeometry --> jtsSimplifiedGeometry:::polygon; +read --> jtsSimplifiedGeometry; +effectiveStyle --> lineSmoothing:::polygon; +simplifiedGeometry --> smoothenedGeometry:::polygon; +lineSmoothing --> smoothenedGeometry; +smoothenedGeometry --> jtsSmoothenedGeometry:::polygon; +read --> jtsSmoothenedGeometry; +jtsSmoothenedGeometry --> context:::polygon; +resolution --> context; +jtsSmoothenedGeometry --> placement:::polygon; +context --> shape:::polygon; +parameterizedSIDC --> shape; +selectionMode --> selection:::polygon; +jtsSimplifiedGeometry --> selection; +parameterizedSIDC --> labels:::polygon; +placement --> labels; +shape --> styles:::polygon; +labels --> styles; +selection --> styles; + +%% Final +A{{style}} +styles --> A; +styleRegistry --> A; +evalSync --> A; +clip --> A; +rewrite --> A; +styleFactory --> A; +``` \ No newline at end of file diff --git a/paperwork/rules.dot b/paperwork/rules.dot deleted file mode 100644 index b44ae688..00000000 --- a/paperwork/rules.dot +++ /dev/null @@ -1,101 +0,0 @@ -digraph structs { - ranksep=1; - { - node [shape=plaintext, fontsize=16]; - Input -> I -> II -> III -> IV -> V -> VI; - } - - { rank = same; - Input; - node [shape=record]; - properties [color=red]; - }; - - { rank = same; - I; - node [shape=record]; - INPUT_A [color=red,label="{{ - globalStyle| - layerStyle| - featureStyle - }}"]; - - PROPERTIES [label="{{ - sidc| - parameterized\nSIDC| - modifiers - }}"]; - }; - - { rank = same; - II; - node [shape=record]; - INPUT_B [color=red,label="{{ - key| - centerResolution - }}"]; - - EFFECTIVE [label="{{ - effectiveStyle| - smoothen - }}"]; - - evalSync; - - node [shape=record]; - STYLES [color=blue,label="{{ - dynamicStyle| - staticStyles - }}"]; - }; - - { rank = same; - III; - node [shape=record]; - GEOMETRY [label="{{ - simplifiedGeometry| - geometry - }}" - ]; - }; - - { rank = same; - IV; - node [shape=box]; - mode [color=red]; - placement [color=blue]; - }; - - { rank = same; - V; - node [shape=box]; - styles; selectedStyles; - }; - - { rank = same; - VI; - node [shape=box]; - style [color=green]; - }; - - properties -> PROPERTIES; - PROPERTIES:sidc:s -> EFFECTIVE:ne; - INPUT_A:globalStyle -> EFFECTIVE:n; - INPUT_A:layerStyle -> EFFECTIVE:n; - INPUT_A:featureStyle -> EFFECTIVE:n; - PROPERTIES:parameterizedSIDC -> STYLES:n; - PROPERTIES:modifiers -> evalSync:n; - EFFECTIVE:smoothen:s -> GEOMETRY:n; - INPUT_B:key -> GEOMETRY:n; - INPUT_B:centerResolution -> GEOMETRY:n; - GEOMETRY:geometry:s -> placement:n; - STYLES:dynamicStyle -> styles; - STYLES:staticStyles -> styles; - mode -> selectedStyles; - GEOMETRY:simplifiedGeometry -> selectedStyles:n; - placement -> styles; - evalSync:s -> styles; - styles -> style; - EFFECTIVE:effectiveStyle:s -> style; - selectedStyles -> style; -} diff --git a/paperwork/rules.pdf b/paperwork/rules.pdf deleted file mode 100644 index d921ddf6..00000000 Binary files a/paperwork/rules.pdf and /dev/null differ diff --git a/src/renderer/components/OSD.js b/src/renderer/components/OSD.js index 38f977eb..97154616 100644 --- a/src/renderer/components/OSD.js +++ b/src/renderer/components/OSD.js @@ -20,6 +20,7 @@ export const OSD = () => { React.useEffect(() => { emitter.on('osd', dispatch) + emitter.emit('osd-mounted') }, [emitter]) return
@@ -31,6 +32,6 @@ export const OSD = () => {
{ state.C2 }
-
+
{ state.C3 }
} diff --git a/src/renderer/components/Project-services.js b/src/renderer/components/Project-services.js index d696ee91..066ae0e4 100644 --- a/src/renderer/components/Project-services.js +++ b/src/renderer/components/Project-services.js @@ -62,7 +62,7 @@ export default async projectUUID => { const coordinatesFormat = new CoordinatesFormat(emitter, preferencesStore) const optionStore = new OptionStore(coordinatesFormat, store, sessionStore) const nominatim = new Nominatim(store) - const featureStore = new FeatureStore(store, selection) + const featureStore = new FeatureStore(store) const searchIndex = new SearchIndex(jsonDB, documentStore, optionStore, emitter, nominatim, sessionStore, spatialIndex) // Key bindings. @@ -128,7 +128,6 @@ export default async projectUUID => { await schema.bootstrap() await tileLayerStore.bootstrap() await searchIndex.bootstrap() - await featureStore.bootstrap() await spatialIndex.bootstrap() return services diff --git a/src/renderer/components/map/eventHandlers.js b/src/renderer/components/map/eventHandlers.js index 391b5f72..a757cb78 100644 --- a/src/renderer/components/map/eventHandlers.js +++ b/src/renderer/components/map/eventHandlers.js @@ -68,7 +68,7 @@ const sendPreview = (services, map) => { * */ const mapHandlers = (services, map) => { - const { selection, osdDriver, dragAndDrop } = services + const { selection, osdDriver, dragAndDrop, emitter } = services map.addEventListener('keydown', event => { const { key } = event.originalEvent @@ -85,6 +85,15 @@ const mapHandlers = (services, map) => { if (deselect.length) selection.deselect(deselect) }) + let resolution + map.on('moveend', () => { + const updated = map.getView().getResolution() + if (updated !== resolution) { + resolution = updated + emitter.emit('view/resolution', { resolution }) + } + }) + // Note: Neither dragstart nor dragend events are fired when dragging // a file into the browser from the OS. const target = document.getElementById('map') diff --git a/src/renderer/components/map/vectorLayers.js b/src/renderer/components/map/vectorLayers.js index 33f30705..ef69055e 100644 --- a/src/renderer/components/map/vectorLayers.js +++ b/src/renderer/components/map/vectorLayers.js @@ -25,7 +25,7 @@ const highlightLayer = (sources, styles) => { export default (sources, styles) => { - const { deselectedSource, selectedSource } = sources + const { deselectedSource, selectedSource, featureSource } = sources const declutter = false const vectorLayer = source => new VectorLayer({ source, @@ -35,6 +35,7 @@ export default (sources, styles) => { return { featureLayer: vectorLayer(deselectedSource), + // featureLayer: vectorLayer(featureSource), highlightLayer: highlightLayer(sources, styles), selectedLayer: vectorLayer(selectedSource) } diff --git a/src/renderer/components/map/vectorSources.js b/src/renderer/components/map/vectorSources.js index 93a43eac..abac667d 100644 --- a/src/renderer/components/map/vectorSources.js +++ b/src/renderer/components/map/vectorSources.js @@ -1,23 +1,32 @@ -import * as Sources from '../../model/Sources' import * as ID from '../../ids' +import { featureSource } from '../../model/sources/featureSource' +import { highlightTracker } from '../../model/sources/highlightTracker' +import { lockedTracker } from '../../model/sources/lockedTracker' +import { visibilityTracker } from '../../model/sources/visibilityTracker' +import { selectionTracker } from '../../model/sources/selectionTracker' +import { intersect } from '../../model/sources/intersect' +import { union } from '../../model/sources/union' export default async services => { const { store, featureStore, emitter, sessionStore, selection } = services - const featureSource = Sources.union( - Sources.featureSource(featureStore, ID.FEATURE_SCOPE), - Sources.featureSource(featureStore, ID.MARKER_SCOPE), - Sources.featureSource(featureStore, ID.MEASURE_SCOPE) - ) - const { visibleSource } = await Sources.visibilityTracker(featureSource, store, emitter) - const { unlockedSource } = Sources.lockedTracker(featureSource, store) + // const featureSource = Sources.union( + // Sources.featureSource(featureStore, ID.FEATURE_SCOPE), + // Sources.featureSource(featureStore, ID.MARKER_SCOPE), + // Sources.featureSource(featureStore, ID.MEASURE_SCOPE) + // ) + + const features = featureSource(services) + + const { visibleSource } = await visibilityTracker(features, store, emitter) + const { unlockedSource } = lockedTracker(features, store) const selectableSource = visibleSource // alias: visible features are selectable - const { selectedSource, deselectedSource } = Sources.selectionTracker(selectableSource, selection) - const highlightSource = Sources.highlightTracker(emitter, store, sessionStore) - const modifiableSource = Sources.intersect(unlockedSource, selectedSource) + const { selectedSource, deselectedSource } = selectionTracker(selectableSource, selection) + const highlightSource = highlightTracker(emitter, store, sessionStore) + const modifiableSource = intersect(unlockedSource, selectedSource) return { - featureSource, + featureSource: features, highlightSource, selectedSource, deselectedSource, diff --git a/src/renderer/components/properties/Attitude.js b/src/renderer/components/properties/Attitude.js index 92791c4e..91b0d88d 100644 --- a/src/renderer/components/properties/Attitude.js +++ b/src/renderer/components/properties/Attitude.js @@ -1,7 +1,7 @@ /* eslint-disable react/prop-types */ import React from 'react' import textProperty from './textProperty' -import { readGeometry, writeGeometryObject } from '../../store/FeatureStore' +import { readGeometry, writeGeometryObject } from '../../ol/format' import * as geom from './geometries' const TextProperty = textProperty({ diff --git a/src/renderer/components/properties/CorridorWidth.js b/src/renderer/components/properties/CorridorWidth.js index b87cc79e..cc366688 100644 --- a/src/renderer/components/properties/CorridorWidth.js +++ b/src/renderer/components/properties/CorridorWidth.js @@ -1,7 +1,7 @@ /* eslint-disable react/prop-types */ import React from 'react' import textProperty from './textProperty' -import { readGeometry, writeGeometryObject } from '../../store/FeatureStore' +import { readGeometry, writeGeometryObject } from '../../ol/format' import * as geom from './geometries' const TextProperty = textProperty({ diff --git a/src/renderer/components/properties/GraphicsProperties.js b/src/renderer/components/properties/GraphicsProperties.js index c30e3330..cccb884a 100644 --- a/src/renderer/components/properties/GraphicsProperties.js +++ b/src/renderer/components/properties/GraphicsProperties.js @@ -19,7 +19,7 @@ import GridCols2 from './GridCols2' import CorridorWidth from './CorridorWidth' import * as MILSTD from '../../symbology/2525c' import * as GEOM from '../../model/geometry' -import { readGeometry } from '../../store/FeatureStore' +import { readGeometry } from '../../ol/format' import KProperty from './KProperty' export default props => { diff --git a/src/renderer/components/properties/Length.js b/src/renderer/components/properties/Length.js index 2ee06aaa..7cad2e52 100644 --- a/src/renderer/components/properties/Length.js +++ b/src/renderer/components/properties/Length.js @@ -1,7 +1,7 @@ /* eslint-disable react/prop-types */ import React from 'react' import textProperty from './textProperty' -import { readGeometry, writeGeometryObject } from '../../store/FeatureStore' +import { readGeometry, writeGeometryObject } from '../../ol/format' import * as geom from './geometries' const TextProperty = textProperty({ diff --git a/src/renderer/components/properties/Radius.js b/src/renderer/components/properties/Radius.js index 74cac609..e526676f 100644 --- a/src/renderer/components/properties/Radius.js +++ b/src/renderer/components/properties/Radius.js @@ -1,7 +1,7 @@ /* eslint-disable react/prop-types */ import React from 'react' import textProperty from './textProperty' -import { readGeometry, writeGeometryObject } from '../../store/FeatureStore' +import { readGeometry, writeGeometryObject } from '../../ol/format' import * as geom from './geometries' const TextProperty = textProperty({ @@ -20,10 +20,11 @@ const TextProperty = textProperty({ const properties = geom.circleProperties(jtsGeometry) properties.am = value const circle = geom.circle(jtsGeometry, properties) + const geometry = writeGeometryObject(write(circle)) return { ...feature, - geometry: writeGeometryObject(write(circle)), + geometry, properties: { ...feature.properties, am: value diff --git a/src/renderer/components/properties/RectangleWidth.js b/src/renderer/components/properties/RectangleWidth.js index 7ac873b7..393812b3 100644 --- a/src/renderer/components/properties/RectangleWidth.js +++ b/src/renderer/components/properties/RectangleWidth.js @@ -1,7 +1,7 @@ /* eslint-disable react/prop-types */ import React from 'react' import textProperty from './textProperty' -import { readGeometry, writeGeometryObject } from '../../store/FeatureStore' +import { readGeometry, writeGeometryObject } from '../../ol/format' import * as geom from './geometries' const TextProperty = textProperty({ diff --git a/src/renderer/components/properties/TilePresetProperties.js b/src/renderer/components/properties/TilePresetProperties.js index 69a5dd23..c4b99ce5 100644 --- a/src/renderer/components/properties/TilePresetProperties.js +++ b/src/renderer/components/properties/TilePresetProperties.js @@ -2,7 +2,7 @@ import React from 'react' import SortableList, { SortableItem } from 'react-easy-sort' import Icon from '@mdi/react' -import { mdiDrag, mdiEyeOutline, mdiEyeOff, mdiSquareOpacity } from '@mdi/js' +import { mdiDrag, mdiEyeOutline, mdiEyeOff, mdiSquareOpacity, mdiTerrain } from '@mdi/js' import { useServices, useList } from '../hooks' import Range from './Range' import { Tooltip } from 'react-tooltip' @@ -41,23 +41,22 @@ const Opacity = props => { /** * */ -const Layer = props => ( - - - {/* react-easy-sort kills list style => add necessary margins. */} -
-
-
- - {props.name} - { + + // terrain data layers must not be invisible, thus we hide controls + const icons = props.contentType?.includes('terrain') + ? + : <> + ( onClick={props.onToggleVisible} className='tt-tile-preset-visibility' /> + + + return ( + + {/* react-easy-sort kills list style => add necessary margins. */} +
+
+
+ + {props.name} + { icons } +
(
-) + ) +} /** @@ -118,16 +133,17 @@ const LayerList = props => { tileLayerStore.toggleVisible(props.preset, id) } - const layers = list.entries.map(entry => - - ) + const layers = list.entries + .map(entry => + + ) // There seems to be no way to have decent CSS on list container. // We workaround this by applying ad-hoc styles. @@ -148,8 +164,10 @@ const LayerList = props => { /** * */ -const TilePresetProperties = props => ( +const TilePresetProperties = props => { + return ( -) + ) +} export default TilePresetProperties diff --git a/src/renderer/components/properties/TileServiceProperties.js b/src/renderer/components/properties/TileServiceProperties.js index c6c968ce..4e135c77 100644 --- a/src/renderer/components/properties/TileServiceProperties.js +++ b/src/renderer/components/properties/TileServiceProperties.js @@ -17,6 +17,7 @@ import { useList, useServices } from '../hooks' import * as TileService from '../../store/tileServiceAdapters' import './TileServiceProperties.css' import { Tooltip } from 'react-tooltip' +import Checkbox from './Checkbox' /** @@ -55,7 +56,7 @@ const fuseOptions = { * */ const TileServiceProperties = props => { - const { tileLayerStore, sessionStore } = useServices() + const { tileLayerStore, sessionStore, store } = useServices() const [key, service] = (Object.entries(props.features))[0] const [url, setUrl] = React.useState({ dirty: false, value: service.url || '' }) const [entries, setEntries] = React.useState([]) @@ -127,13 +128,27 @@ const TileServiceProperties = props => { } const handleZoomChange = ({ maxZoom }) => { - tileLayerStore.updateService(key, { ...service, ...{ capabilities: { maxZoom } } }) + const updatedService = { ...service } + updatedService.capabilities.maxZoom = maxZoom + tileLayerStore.updateService(key, updatedService /* { ...service, ...{ capabilities: { maxZoom } } } */) } const handleEntryChange = async id => tileLayerStore.toggleActiveLayer(key, id) const handleEntryClick = id => dispatch({ type: 'select', id }) const handleFilterChange = ({ target }) => setFilter(target.value) + const handleRGBTerrain = async ({ target }) => { + const updatedService = { ...service } + updatedService.capabilities.contentType = target.checked ? 'terrain/mapbox-rgb' : undefined + tileLayerStore.updateService(key, updatedService) + + if (target.checked) { + store.addTag(key, 'TERRAIN') + } else { + store.removeTag(key, 'TERRAIN') + } + } + const layerList = ['WMS', 'WMTS'].includes(service.type) ?
{ @@ -159,6 +174,10 @@ const TileServiceProperties = props => { ? : null + const terrainSelector = (service.type === 'XYZ') + ? + : null + return ( @@ -169,6 +188,7 @@ const TileServiceProperties = props => { { layerList }
{ zoomSliders } + { terrainSelector }
) diff --git a/src/renderer/components/properties/geometries.js b/src/renderer/components/properties/geometries.js index 1ab60f25..59ba9193 100644 --- a/src/renderer/components/properties/geometries.js +++ b/src/renderer/components/properties/geometries.js @@ -40,6 +40,12 @@ export const corridorProperties = geometry => { } } +export const GeometryProperties = { + RECTANGLE: rectangleProperties, + CIRCLE: circleProperties, + CORRIDOR: corridorProperties +} + const unitSquare = TS.polygon([ [-1, 1], [1, 1], [1, -1], [-1, -1], [-1, 1] ].map(TS.coordinate)) diff --git a/src/renderer/ids.js b/src/renderer/ids.js index cc5f1180..852fcda8 100644 --- a/src/renderer/ids.js +++ b/src/renderer/ids.js @@ -96,7 +96,7 @@ export const isMeasureId = isId(MEASURE_SCOPE) export const isStylableId = R.anyPass([isLayerId, isFeatureId]) export const isDeletableId = id => !isSymbolId(id) -export const isTaggableId = id => !isViewId(id) +export const isTaggableId = id => (!isViewId(id) && !isTagsId(id)) export const isAssociatedId = R.anyPass([isHiddenId, isLockedId, isDefaultId, isTagsId]) export const layerUUID = R.cond([ diff --git a/src/renderer/model/OSDDriver.js b/src/renderer/model/OSDDriver.js index d5b8bc40..6c60f4b0 100644 --- a/src/renderer/model/OSDDriver.js +++ b/src/renderer/model/OSDDriver.js @@ -2,7 +2,7 @@ import { toLonLat } from 'ol/proj' import { LatLon } from 'geodesy/mgrs.js' import Dms from 'geodesy/dms.js' import { militaryFormat } from '../../shared/datetime' -import { isDefaultId } from '../ids' +import { isDefaultId, isLayerId } from '../ids' Dms.separator = ' ' @@ -25,6 +25,13 @@ const formats = { UTM: ([lng, lat]) => new LatLon(lat, lng).toUtm().toString() } +const elevation = rgb => { + if (!rgb) return null + const value = -10000 + (((rgb[0] << 16) + (rgb[1] << 8) + rgb[2]) * 0.1) + if (value === -10000) return null + return value +} + export const OSDDriver = function (projectUUID, emitter, preferencesStore, projectStore, store) { this.projectUUID = projectUUID this.emitter = emitter @@ -32,16 +39,16 @@ export const OSDDriver = function (projectUUID, emitter, preferencesStore, proje this.projectStore = projectStore this.store = store - ;(async () => { + emitter.on('osd-mounted', async () => { this.coordinatesFormat = await preferencesStore.get('coordinates-format', 'MGRS') this.updateProjectName() this.updateDefaultLayer() - })() + }) setInterval(this.updateDateTime.bind(this), 1000) store.on('batch', ({ operations }) => { - const update = operations.some(({ key }) => isDefaultId(key)) + const update = operations.some(({ key }) => isDefaultId(key) || isLayerId(key)) if (update) this.updateDefaultLayer() }) @@ -51,13 +58,36 @@ export const OSDDriver = function (projectUUID, emitter, preferencesStore, proje }) } -OSDDriver.prototype.pointermove = function ({ coordinate }) { +OSDDriver.prototype.pointermove = function ({ coordinate, map, pixel }) { this.lastCoordinate = coordinate if (!this.coordinatesFormat) return const lonLat = toLonLat(coordinate) const message = formats[this.coordinatesFormat](lonLat) this.emitter.emit('osd', { message, cell: 'C2' }) + + const getElevation = async () => { + const candids = map?.getLayerGroup().getLayersArray() + const terrainLayers = candids.filter(l => l.get('contentType') === 'terrain/mapbox-rgb') + if (terrainLayers.length === 0) { + return '' + } + + const data = terrainLayers + .map(l => l.getData(pixel)) + .map(d => elevation(d)) + .filter(Boolean) + + if (data.length === 0) { + return '' + } + + const elevationMessage = `${data[0].toFixed(1)}m` + return elevationMessage + } + + getElevation().then(message => this.emitter.emit('osd', { message, cell: 'C3' })) + } OSDDriver.prototype.updateDateTime = function () { @@ -72,11 +102,9 @@ OSDDriver.prototype.updateProjectName = async function () { } OSDDriver.prototype.updateDefaultLayer = async function () { - const { store } = this - - const layerId = await store.defaultLayerId() + const layerId = await this.store.defaultLayerId() if (layerId) { - const layer = await store.value(layerId) + const layer = await this.store.value(layerId) this.emitter.emit('osd', { message: layer.name, cell: 'A2' }) } else { this.emitter.emit('osd', { message: '', cell: 'A2' }) diff --git a/src/renderer/model/Sources.js b/src/renderer/model/Sources.js deleted file mode 100644 index 4c806f36..00000000 --- a/src/renderer/model/Sources.js +++ /dev/null @@ -1,270 +0,0 @@ -import * as R from 'ramda' -import Collection from 'ol/Collection' -import VectorSource from 'ol/source/Vector' -import Feature from 'ol/Feature' -import Event from 'ol/events/Event' -import * as ID from '../ids' - - -/** - * - */ -export const featureSource = (featureStore, scope) => { - const matchesScope = feature => feature - ? ID.isId(scope)(feature.getId()) - : false - - const features = Object.values(featureStore.features).filter(matchesScope) - const source = new VectorSource({ features }) - - featureStore.on('addfeatures', ({ features }) => { - source.addFeatures(features.filter(matchesScope)) - }) - - featureStore.on('removefeatures', ({ features }) => features - .filter(matchesScope) - .forEach(feature => source.removeFeature(feature)) - ) - - return source -} - - -/** - * - */ -export class TouchFeaturesEvent extends Event { - constructor (keys) { - super('touchfeatures') - this.keys = keys - } -} - - -/** - * - */ -export const filter = predicate => source => { - - // Supply empty collection which will be kept in sync with source. - // This is neccessary for interactions which only accept feature collections - // instead of sources (e.g. translate, clone). - const destination = new VectorSource({ features: new Collection() }) - - source.on('addfeature', ({ feature: addition }) => { - if (destination.getFeatureById(addition.getId())) return - if (!predicate(addition.getId())) return - destination.addFeature(addition) - }) - - source.on('removefeature', ({ feature: removal }) => { - const feature = destination.getFeatureById(removal.getId()) - if (feature) destination.removeFeature(feature) - }) - - source.on('touchfeatures', ({ keys }) => { - keys.forEach(key => { - if (predicate(key)) { - // Note: Re-adding would actually remove the feature! - if (destination.getFeatureById(key)) return - const feature = source.getFeatureById(key) - if (feature) destination.addFeature(feature) - } else { - const feature = destination.getFeatureById(key) - if (feature) destination.removeFeature(feature) - } - }) - }) - - const additions = source.getFeatures().filter(feature => predicate(feature.getId())) - destination.addFeatures(additions) - - return destination -} - -/** - * intersect :: ol/source/Vector S => S -> S -> S - */ -export const intersect = (a, b) => { - const intersection = new VectorSource({ features: new Collection() }) - - a.on('addfeature', ({ feature: addition }) => { - if (!b.getFeatureById(addition.getId())) return - intersection.addFeature(addition) - }) - - a.on('removefeature', ({ feature: removal }) => { - const feature = intersection.getFeatureById(removal.getId()) - if (feature) intersection.removeFeature(feature) - }) - - b.on('addfeature', ({ feature: addition }) => { - if (!a.getFeatureById(addition.getId())) return - intersection.addFeature(addition) - }) - - b.on('removefeature', ({ feature: removal }) => { - const feature = intersection.getFeatureById(removal.getId()) - if (feature) intersection.removeFeature(feature) - }) - - return intersection -} - -/** - * union :: ol/source/Vector S => [S] -> S - */ -export const union = (...sources) => { - const union = new VectorSource({ features: new Collection() }) - - sources.forEach(source => { - union.addFeatures(source.getFeatures()) - source.on('addfeature', ({ feature }) => union.addFeature(feature)) - source.on('removefeature', ({ feature }) => union.removeFeature(union.getFeatureById(feature.getId()))) - }) - - return union -} - -/** - * - */ -export const selectionTracker = (source, selection) => { - const keySet = new Set() - - const selected = key => keySet.has(key) - const deselected = key => !keySet.has(key) - - selection.on('selection', ({ selected, deselected }) => { - selected.forEach(key => keySet.add(key)) - deselected.forEach(key => keySet.delete(key)) - const keys = [...selected, ...deselected] - source.dispatchEvent(new TouchFeaturesEvent(keys)) - }) - - return { - selectedSource: filter(selected)(source), - deselectedSource: filter(deselected)(source) - } -} - - -/** - * - */ -export const visibilityTracker = async (source, store, emitter) => { - const keySet = new Set() - const hidden = key => keySet.has(key) - const visible = key => !keySet.has(key) - - await (async () => { - emitter.on('feature/show', ({ ids }) => { - const keys = ids.map(ID.associatedId) - keys.forEach(key => keySet.delete(key)) - source.dispatchEvent(new TouchFeaturesEvent(keys)) - }) - - emitter.on('feature/hide', ({ ids }) => { - const keys = ids.map(ID.associatedId) - keys.forEach(key => keySet.add(key)) - source.dispatchEvent(new TouchFeaturesEvent(keys)) - }) - - store.on('batch', ({ operations }) => { - const candidates = operations - .filter(({ key }) => ID.isHiddenId(key)) - .map(({ type, key }) => ({ type, key: ID.associatedId(key) })) - - const [additions, removals] = R.partition(({ type }) => type === 'put', candidates) - additions.forEach(({ key }) => keySet.add(key)) - removals.forEach(({ key }) => keySet.delete(key)) - - const keys = candidates.map(({ key }) => key) - source.dispatchEvent(new TouchFeaturesEvent(keys)) - }) - - const keys = await store.keys(ID.hiddenId()) - keys.forEach(key => keySet.add(ID.associatedId(key))) - })() - - return { - visibleSource: filter(visible)(source), - hiddenSource: filter(hidden)(source) - } -} - - -/** - * - */ -export const lockedTracker = (source, store) => { - const keySet = new Set() - const locked = key => keySet.has(key) - const unlocked = key => !keySet.has(key) - - ;(async () => { - store.on('batch', ({ operations }) => { - const candidates = operations - .filter(({ key }) => ID.isLockedId(key)) - .map(({ type, key }) => ({ type, key: ID.associatedId(key) })) - - const [additions, removals] = R.partition(({ type }) => type === 'put', candidates) - additions.forEach(({ key }) => keySet.add(key)) - removals.forEach(({ key }) => keySet.delete(key)) - - const keys = candidates.map(({ key }) => key) - source.dispatchEvent(new TouchFeaturesEvent(keys)) - }) - - const keys = await store.keys(ID.lockedId()) - keys.forEach(key => keySet.add(ID.associatedId(key))) - })() - - return { - unlockedSource: filter(unlocked)(source), - lockedSource: filter(locked)(source) - } -} - - -export const highlightTracker = (emitter, store, sessionStore) => { - const source = new VectorSource({ features: new Collection() }) - let timeout - let hiddenIds = [] - - const handleTimeout = () => { - source.clear() - emitter.emit('feature/hide', { ids: hiddenIds }) - } - - emitter.on('highlight/on', async ({ ids }) => { - const viewport = await sessionStore.get('viewport') - const geometries = await store.geometryBounds(ids, viewport.resolution) - const features = geometries.map(geometry => new Feature(geometry)) - source.addFeatures(features) - // Temporarily show hidden feature. - const isHidable = id => ID.isFeatureId(id) || ID.isMarkerId(id) || ID.isMeasureId(id) - - const keys = await store.collectKeys(ids) - const featureIds = keys.filter(isHidable) - const tuples = await store.tuples(featureIds.map(ID.hiddenId)) - - hiddenIds = tuples - .filter(([_, value]) => value) - .map(([key]) => key) - - emitter.emit('feature/show', { ids: hiddenIds }) - - if (timeout) clearTimeout(timeout) - timeout = setTimeout(handleTimeout, 5000) - }) - - emitter.on('highlight/off', () => { - if (!timeout) return - clearTimeout(timeout) - handleTimeout() - timeout = null - }) - - return source -} diff --git a/src/renderer/model/sources/TouchFeaturesEvent.js b/src/renderer/model/sources/TouchFeaturesEvent.js new file mode 100644 index 00000000..03477738 --- /dev/null +++ b/src/renderer/model/sources/TouchFeaturesEvent.js @@ -0,0 +1,11 @@ +import Event from 'ol/events/Event' + +/** + * + */ +export class TouchFeaturesEvent extends Event { + constructor (keys) { + super('touchfeatures') + this.keys = keys + } +} diff --git a/src/renderer/model/sources/featureSource.js b/src/renderer/model/sources/featureSource.js new file mode 100644 index 00000000..94754eae --- /dev/null +++ b/src/renderer/model/sources/featureSource.js @@ -0,0 +1,187 @@ +import * as R from 'ramda' +import Signal from '@syncpoint/signal' +import VectorSource from 'ol/source/Vector' +import * as ID from '../../ids' +import styles from '../../ol/style/styles' +import { format } from '../../ol/format' +import { flat, select } from '../../../shared/signal' +import { setCoordinates } from '../geometry' +import keyequals from '../../ol/style/keyequals' +import isEqual from 'react-fast-compare' + +/** + * Read features from GeoJSON to ol/Feature and + * create input signals for style calculation. + */ +const readFeature = R.curry((state, source) => { + const feature = format.readFeature(source) + const featureId = feature.getId() + const layerId = ID.layerId(featureId) + const { geometry, ...properties } = feature.getProperties() + + feature.$ = { + + // A word of caution: It is strongly adviced to NOT use feature signal + // DIRECTLY to derive style! Setting the featues style will update the + // feature's revision and thus lead to an infinite loop. + // Always make sure to extract relevant information from feature into + // new signals which conversely are only updated when this information + // has actually changed. + // + properties: Signal.of(properties, { equals: isEqual }), + geometry: Signal.of(geometry, { equals: keyequals() }), + globalStyle: Signal.of(state.styles[ID.defaultStyleId]), + layerStyle: Signal.of(state.styles[ID.styleId(layerId)] ?? {}), + featureStyle: Signal.of(state.styles[ID.styleId(featureId)] ?? {}), + centerResolution: Signal.of(state.resolution), + selectionMode: Signal.of('default') + } + + feature.$.style = styles(feature) + feature.setStyle(feature => feature.$.style()) + + // Use dedicated function to update feature coordinates from within + // modify interaction. Such internal changes must not trigger ModifyEvent. + + feature.internalChange = Signal.of(false) + + feature.updateCoordinates = coordinates => { + feature.internalChange(true) + setCoordinates(feature.getGeometry(), coordinates) + feature.internalChange(false) + } + + feature.commit = () => { + // Event must be deferred so that event handler has a chance + // to update to a new state (drag -> selected). + setTimeout(() => feature.dispatchEvent({ type: 'change', target: feature })) + } + + feature.on('change', ({ target }) => { + const { geometry, ...properties } = target.getProperties() + target.$.properties(properties) + target.$.geometry(geometry) + }) + + return feature +}) + +// Batch operations order: +// 0 - (del, style+) +// 1 - (del, feature) +// 2 - (put, style+) +// 3 - (put, feature) +// 4 - other +const ord = R.cond([ + [R.both(R.propEq('del', 'type'), R.compose(R.startsWith('style+'), R.prop('key'))), R.always(0)], + [R.both(R.propEq('del', 'type'), R.compose(R.startsWith(ID.FEATURE_SCOPE), R.prop('key'))), R.always(1)], + [R.both(R.propEq('put', 'type'), R.compose(R.startsWith('style+'), R.prop('key'))), R.always(2)], + [R.both(R.propEq('put', 'type'), R.compose(R.startsWith(ID.FEATURE_SCOPE), R.prop('key'))), R.always(3)], + [R.T, R.always(4)] +]) + +const isCandidateId = id => ID.isFeatureId(id) || ID.isMarkerId(id) || ID.isMeasureId(id) + +const operations = R.compose( + flat, + R.map(R.sort((a, b) => ord(a) - ord(b))), + R.map(R.prop('operations')) +) + +const selectEvent = select([ + R.propEq(ID.defaultStyleId, 'key'), + R.compose(ID.isLayerStyleId, R.prop('key')), + R.compose(ID.isFeatureStyleId, R.prop('key')), + R.compose(isCandidateId, R.prop('key')) +]) + +/** + * + */ +export const featureSource = services => { + const { store, selection } = services + const state = {} + + const source = new VectorSource({ + features: [], + useSpatialIndex: false, + strategy: function (extent, resolution) { + if (state.resolution !== resolution) { + state.resolution = resolution + this.getFeatures().map(feature => feature.$.centerResolution(resolution)) + } + return [extent] + } + }) + + const getFeatureById = source.getFeatureById.bind(source) + const getFeaturesById = ids => ids.map(getFeatureById).filter(Boolean) + + // Load styles and features. + ;(async () => { + state.styles = await store.dictionary('style+') + const tuples = [ + ...await store.tuples(ID.FEATURE_SCOPE), + ...await store.tuples(ID.MARKER_SCOPE), + ...await store.tuples(ID.MEASURE_SCOPE) + ] + + const features = tuples + .map(([id, value]) => ({ id, ...value })) + .map(readFeature(state)) + source.addFeatures(features) + })() + + // ==> batch event handling + + const events = operations(Signal.fromListeners(['batch'], store)) + const [globalStyle, layerStyle, featureStyle, feature] = selectEvent(events) + + globalStyle.on(({ value }) => { + source.getFeatures().forEach(feature => feature.$.globalStyle(value)) + }) + + layerStyle.on(({ type, key, value }) => { + const layerId = ID.layerId(key) + if (type === 'del') delete state.styles[key] + source.getFeatures() + .filter(feature => ID.layerId(feature.getId()) === layerId) + .forEach(feature => feature.$.layerStyle(type === 'put' ? value : {})) + }) + + featureStyle.on(({ type, key, value }) => { + const featureId = ID.featureId(key) + const feature = getFeatureById(featureId) + if (type === 'del') delete state.styles[key] + if (feature) feature.$.featureStyle(type === 'put' ? value : {}) + }) + + feature.on(({ type, key, value }) => { + let feature = getFeatureById(key) + if (type === 'del') source.removeFeature(feature) + else if (feature) { + feature.setProperties(value.properties) + // It is possible that only properties have changed. + // Don't set null/undefined geometry! + const geometry = format.readGeometry(value.geometry) + if (geometry) feature.setGeometry(geometry) + } else { + feature = readFeature(state, { id: key, ...value }) + source.addFeature(feature) + } + }) + + // <== batch event handling + + selection.on('selection', ({ deselected }) => { + const mode = selection.selected().length > 1 + ? 'multiselect' + : 'singleselect' + + const apply = mode => feature => feature.$.selectionMode(mode) + getFeaturesById(deselected).map(apply('default')) + getFeaturesById(selection.selected()).map(apply(mode)) + }) + + return source +} diff --git a/src/renderer/model/sources/filter.js b/src/renderer/model/sources/filter.js new file mode 100644 index 00000000..d84844f6 --- /dev/null +++ b/src/renderer/model/sources/filter.js @@ -0,0 +1,47 @@ +import Collection from 'ol/Collection' +import VectorSource from 'ol/source/Vector' + +/** + * + */ +export const filter = predicate => source => { + + // Supply empty collection which will be kept in sync with source. + // This is neccessary for interactions which only accept feature collections + // instead of sources (e.g. translate, clone). + const destination = new VectorSource({ + features: new Collection(), + // Hack to delegate load request upstream: + strategy: (extent, resolution) => source.strategy_(extent, resolution) + }) + + source.on('addfeature', ({ feature: addition }) => { + if (destination.getFeatureById(addition.getId())) return + if (!predicate(addition.getId())) return + destination.addFeature(addition) + }) + + source.on('removefeature', ({ feature: removal }) => { + const feature = destination.getFeatureById(removal.getId()) + if (feature) destination.removeFeature(feature) + }) + + source.on('touchfeatures', ({ keys }) => { + keys.forEach(key => { + if (predicate(key)) { + // Note: Re-adding would actually remove the feature! + if (destination.getFeatureById(key)) return + const feature = source.getFeatureById(key) + if (feature) destination.addFeature(feature) + } else { + const feature = destination.getFeatureById(key) + if (feature) destination.removeFeature(feature) + } + }) + }) + + const additions = source.getFeatures().filter(feature => predicate(feature.getId())) + destination.addFeatures(additions) + + return destination +} diff --git a/src/renderer/model/sources/highlightTracker.js b/src/renderer/model/sources/highlightTracker.js new file mode 100644 index 00000000..f0980c78 --- /dev/null +++ b/src/renderer/model/sources/highlightTracker.js @@ -0,0 +1,46 @@ +import Collection from 'ol/Collection' +import VectorSource from 'ol/source/Vector' +import Feature from 'ol/Feature' +import * as ID from '../../ids' + +export const highlightTracker = (emitter, store, sessionStore) => { + const source = new VectorSource({ features: new Collection() }) + let timeout + let hiddenIds = [] + + const handleTimeout = () => { + source.clear() + emitter.emit('feature/hide', { ids: hiddenIds }) + } + + emitter.on('highlight/on', async ({ ids }) => { + const viewport = await sessionStore.get('viewport') + const geometries = await store.geometryBounds(ids, viewport.resolution) + const features = geometries.map(geometry => new Feature(geometry)) + source.addFeatures(features) + // Temporarily show hidden feature. + const isHidable = id => ID.isFeatureId(id) || ID.isMarkerId(id) || ID.isMeasureId(id) + + const keys = await store.collectKeys(ids) + const featureIds = keys.filter(isHidable) + const tuples = await store.tuples(featureIds.map(ID.hiddenId)) + + hiddenIds = tuples + .filter(([_, value]) => value) + .map(([key]) => key) + + emitter.emit('feature/show', { ids: hiddenIds }) + + if (timeout) clearTimeout(timeout) + timeout = setTimeout(handleTimeout, 5000) + }) + + emitter.on('highlight/off', () => { + if (!timeout) return + clearTimeout(timeout) + handleTimeout() + timeout = null + }) + + return source +} diff --git a/src/renderer/model/sources/intersect.js b/src/renderer/model/sources/intersect.js new file mode 100644 index 00000000..8173d5c5 --- /dev/null +++ b/src/renderer/model/sources/intersect.js @@ -0,0 +1,31 @@ +import Collection from 'ol/Collection' +import VectorSource from 'ol/source/Vector' + +/** + * intersect :: ol/source/Vector S => S -> S -> S + */ +export const intersect = (a, b) => { + const intersection = new VectorSource({ features: new Collection() }) + + a.on('addfeature', ({ feature: addition }) => { + if (!b.getFeatureById(addition.getId())) return + intersection.addFeature(addition) + }) + + a.on('removefeature', ({ feature: removal }) => { + const feature = intersection.getFeatureById(removal.getId()) + if (feature) intersection.removeFeature(feature) + }) + + b.on('addfeature', ({ feature: addition }) => { + if (!a.getFeatureById(addition.getId())) return + intersection.addFeature(addition) + }) + + b.on('removefeature', ({ feature: removal }) => { + const feature = intersection.getFeatureById(removal.getId()) + if (feature) intersection.removeFeature(feature) + }) + + return intersection +} diff --git a/src/renderer/model/sources/lockedTracker.js b/src/renderer/model/sources/lockedTracker.js new file mode 100644 index 00000000..9e4213ba --- /dev/null +++ b/src/renderer/model/sources/lockedTracker.js @@ -0,0 +1,36 @@ +import * as R from 'ramda' +import * as ID from '../../ids' +import { TouchFeaturesEvent } from './TouchFeaturesEvent' +import { filter } from './filter' + +/** + * + */ +export const lockedTracker = (source, store) => { + const keySet = new Set() + const locked = key => keySet.has(key) + const unlocked = key => !keySet.has(key) + + ;(async () => { + store.on('batch', ({ operations }) => { + const candidates = operations + .filter(({ key }) => ID.isLockedId(key)) + .map(({ type, key }) => ({ type, key: ID.associatedId(key) })) + + const [additions, removals] = R.partition(({ type }) => type === 'put', candidates) + additions.forEach(({ key }) => keySet.add(key)) + removals.forEach(({ key }) => keySet.delete(key)) + + const keys = candidates.map(({ key }) => key) + source.dispatchEvent(new TouchFeaturesEvent(keys)) + }) + + const keys = await store.keys(ID.lockedId()) + keys.forEach(key => keySet.add(ID.associatedId(key))) + })() + + return { + unlockedSource: filter(unlocked)(source), + lockedSource: filter(locked)(source) + } +} diff --git a/src/renderer/model/sources/selectionTracker.js b/src/renderer/model/sources/selectionTracker.js new file mode 100644 index 00000000..e1132458 --- /dev/null +++ b/src/renderer/model/sources/selectionTracker.js @@ -0,0 +1,24 @@ +import { TouchFeaturesEvent } from './TouchFeaturesEvent' +import { filter } from './filter' + +/** + * + */ +export const selectionTracker = (source, selection) => { + const keySet = new Set() + + const selected = key => keySet.has(key) + const deselected = key => !keySet.has(key) + + selection.on('selection', ({ selected, deselected }) => { + selected.forEach(key => keySet.add(key)) + deselected.forEach(key => keySet.delete(key)) + const keys = [...selected, ...deselected] + source.dispatchEvent(new TouchFeaturesEvent(keys)) + }) + + return { + selectedSource: filter(selected)(source), + deselectedSource: filter(deselected)(source) + } +} diff --git a/src/renderer/model/sources/union.js b/src/renderer/model/sources/union.js new file mode 100644 index 00000000..0e410992 --- /dev/null +++ b/src/renderer/model/sources/union.js @@ -0,0 +1,17 @@ +import Collection from 'ol/Collection' +import VectorSource from 'ol/source/Vector' + +/** + * union :: ol/source/Vector S => [S] -> S + */ +export const union = (...sources) => { + const union = new VectorSource({ features: new Collection() }) + + sources.forEach(source => { + union.addFeatures(source.getFeatures()) + source.on('addfeature', ({ feature }) => union.addFeature(feature)) + source.on('removefeature', ({ feature }) => union.removeFeature(union.getFeatureById(feature.getId()))) + }) + + return union +} diff --git a/src/renderer/model/sources/visibilityTracker.js b/src/renderer/model/sources/visibilityTracker.js new file mode 100644 index 00000000..ad9e91cf --- /dev/null +++ b/src/renderer/model/sources/visibilityTracker.js @@ -0,0 +1,47 @@ +import * as R from 'ramda' +import * as ID from '../../ids' +import { TouchFeaturesEvent } from './TouchFeaturesEvent' +import { filter } from './filter' + +/** + * + */ +export const visibilityTracker = async (source, store, emitter) => { + const keySet = new Set() + const hidden = key => keySet.has(key) + const visible = key => !keySet.has(key) + + await (async () => { + emitter.on('feature/show', ({ ids }) => { + const keys = ids.map(ID.associatedId) + keys.forEach(key => keySet.delete(key)) + source.dispatchEvent(new TouchFeaturesEvent(keys)) + }) + + emitter.on('feature/hide', ({ ids }) => { + const keys = ids.map(ID.associatedId) + keys.forEach(key => keySet.add(key)) + source.dispatchEvent(new TouchFeaturesEvent(keys)) + }) + + store.on('batch', ({ operations }) => { + const candidates = operations + .filter(({ key }) => ID.isHiddenId(key)) + .map(({ type, key }) => ({ type, key: ID.associatedId(key) })) + + const [additions, removals] = R.partition(({ type }) => type === 'put', candidates) + additions.forEach(({ key }) => keySet.add(key)) + removals.forEach(({ key }) => keySet.delete(key)) + const keys = candidates.map(({ key }) => key) + source.dispatchEvent(new TouchFeaturesEvent(keys)) + }) + + const keys = await store.keys(ID.hiddenId()) + keys.forEach(key => keySet.add(ID.associatedId(key))) + })() + + return { + visibleSource: filter(visible)(source), + hiddenSource: filter(hidden)(source) + } +} diff --git a/src/renderer/ol/.gitignore b/src/renderer/ol/.gitignore new file mode 100644 index 00000000..92c252a4 --- /dev/null +++ b/src/renderer/ol/.gitignore @@ -0,0 +1 @@ +style.backup diff --git a/src/renderer/ol/format.js b/src/renderer/ol/format.js new file mode 100644 index 00000000..1ef10c85 --- /dev/null +++ b/src/renderer/ol/format.js @@ -0,0 +1,19 @@ +import GeoJSON from 'ol/format/GeoJSON' + +export const format = new GeoJSON({ + dataProjection: 'EPSG:3857', + featureProjection: 'EPSG:3857' +}) + +/** + * Note: If source does not include a geometry, geometry of resulting feature is `null`. + */ +export const readFeature = format.readFeature.bind(format) +export const readFeatures = format.readFeatures.bind(format) +export const readGeometry = format.readGeometry.bind(format) +export const writeGeometry = format.writeGeometry.bind(format) +export const writeGeometryObject = format.writeGeometryObject.bind(format) + +// writeFeatureCollection :: [ol/Feature] -> GeoJSON/FeatureCollection +export const writeFeatureCollection = format.writeFeaturesObject.bind(format) +export const writeFeatureObject = format.writeFeatureObject.bind(format) diff --git a/src/renderer/ol/interaction/measure/GeometryType.js b/src/renderer/ol/interaction/GeometryType.js similarity index 100% rename from src/renderer/ol/interaction/measure/GeometryType.js rename to src/renderer/ol/interaction/GeometryType.js diff --git a/src/renderer/ol/interaction/clone-interaction.js b/src/renderer/ol/interaction/clone-interaction.js index b502dd35..144df2cb 100644 --- a/src/renderer/ol/interaction/clone-interaction.js +++ b/src/renderer/ol/interaction/clone-interaction.js @@ -1,7 +1,7 @@ import uuid from '../../../shared/uuid' import { Translate } from 'ol/interaction' import { MAC } from 'ol/has' -import { writeGeometryObject } from '../../store/FeatureStore' +import { writeGeometryObject } from '../../ol/format' import * as ID from '../../ids' diff --git a/src/renderer/ol/interaction/draw-interaction.js b/src/renderer/ol/interaction/draw-interaction.js index 1a60934a..2e58ebf2 100644 --- a/src/renderer/ol/interaction/draw-interaction.js +++ b/src/renderer/ol/interaction/draw-interaction.js @@ -2,22 +2,11 @@ import * as R from 'ramda' import Draw from 'ol/interaction/Draw' import uuid from '../../../shared/uuid' import * as MILSTD from '../../symbology/2525c' -import { writeFeatureObject } from '../../store/FeatureStore' +import { writeFeatureObject } from '../../ol/format' import * as TS from '../ts' import * as EPSG from '../../epsg' import { PI_OVER_2, PI_OVER_4, SQRT_2 } from '../../../shared/Math' - -const GeometryType = { - POINT: 'Point', - LINE_STRING: 'LineString', - LINEAR_RING: 'LinearRing', - POLYGON: 'Polygon', - MULTI_POINT: 'MultiPoint', - MULTI_LINE_STRING: 'MultiLineString', - MULTI_POLYGON: 'MultiPolygon', - GEOMETRY_COLLECTION: 'GeometryCollection', - CIRCLE: 'Circle' -} +import GeometryType from './GeometryType' export default options => { const { services, map } = options diff --git a/src/renderer/ol/interaction/measure/LineStringStyle.js b/src/renderer/ol/interaction/measure/LineStringStyle.js new file mode 100644 index 00000000..d865ab01 --- /dev/null +++ b/src/renderer/ol/interaction/measure/LineStringStyle.js @@ -0,0 +1,80 @@ +import * as geom from 'ol/geom' +import { Circle, Fill, Stroke, Style, Text } from 'ol/style' +import { FONT } from './baseStyle' +import { angle, radiansAngle, length, getLastSegmentCoordinates } from './tools' + +export const LineString = geometry => { + const styles = [] + let numberOfSegments = 0 + + geometry.forEachSegment((start, end) => { + const segment = new geom.LineString([start, end]) + numberOfSegments++ + styles.push(new Style({ + geometry: segment, + text: new Text({ + text: `${length(segment)}\n\n${angle(segment)}`, + font: FONT, + fill: new Fill({ + color: 'black' + }), + stroke: new Stroke({ + color: 'white', + width: 5 + }), + placement: 'line', + overflow: true, + textBaseline: 'middle' + }) + })) + }) + + /* first point of the linestring */ + styles.push(new Style({ + geometry: new geom.Point(geometry.getFirstCoordinate()), + image: new Circle({ + radius: 6, + fill: new Fill({ + color: 'green' + }) + }) + })) + + const lastSegment = new geom.LineString(getLastSegmentCoordinates(geometry)) + const alpha = radiansAngle(lastSegment) + + /* set style and label for last point but only if we have more than one segment */ + + styles.push( + new Style({ + geometry: new geom.Point(geometry.getLastCoordinate()), + image: new Circle({ + radius: 6, + fill: new Fill({ + color: 'red' + }) + }), + text: (numberOfSegments === 1) + ? '' + : new Text({ + text: length(geometry), + font: FONT, + fill: new Fill({ + color: 'black' + }), + stroke: new Stroke({ + color: 'white', + width: 5 + }), + offsetX: 25 * Math.cos(alpha), + offsetY: -25 * Math.sin(alpha), + placement: 'point', + textAlign: Math.abs(alpha) < Math.PI / 2 ? 'left' : 'right', + overflow: true, + textBaseline: 'ideographic' + }) + }) + ) + + return styles +} diff --git a/src/renderer/ol/interaction/measure/PolygonStyle.js b/src/renderer/ol/interaction/measure/PolygonStyle.js new file mode 100644 index 00000000..1a4469c0 --- /dev/null +++ b/src/renderer/ol/interaction/measure/PolygonStyle.js @@ -0,0 +1,54 @@ +import * as geom from 'ol/geom' +import { Fill, Stroke, Style, Text } from 'ol/style' +import { FONT } from './baseStyle' +import { length, area } from './tools' + +export const Polygon = geometry => { + + const styles = [] + const coordinates = geometry.getCoordinates()[0] + const numberOfSegments = coordinates.length - 1 + + for (let i = 0; i < numberOfSegments; i++) { + const segment = new geom.LineString([coordinates[i], coordinates[i + 1]]) + styles.push(new Style({ + geometry: segment, + text: new Text({ + text: `${length(segment)}\n\n`, + font: FONT, + fill: new Fill({ + color: 'black' + }), + stroke: new Stroke({ + color: 'white', + width: 5 + }), + placement: 'line', + overflow: true, + textBaseline: 'middle' + }) + })) + } + + styles.push( + new Style({ + geometry: geometry.getInteriorPoint(), + text: new Text({ + text: `${area(geometry)}\n${length(geometry)}`, + font: FONT, + fill: new Fill({ + color: 'black' + }), + stroke: new Stroke({ + color: 'white', + width: 5 + }), + placement: 'point', + overflow: true, + textBaseline: 'ideographic' + }) + }) + ) + + return styles +} diff --git a/src/renderer/ol/interaction/measure/baseStyle.js b/src/renderer/ol/interaction/measure/baseStyle.js new file mode 100644 index 00000000..7f6d4dff --- /dev/null +++ b/src/renderer/ol/interaction/measure/baseStyle.js @@ -0,0 +1,26 @@ +import { Circle, Fill, Stroke, Style } from 'ol/style' +export const FONT = '12px sans-serif' + +export const baseStyle = selected => [ + new Style({ + stroke: new Stroke({ + color: selected ? 'blue' : 'red', + width: 4 + }) + }), + new Style({ + stroke: new Stroke({ + color: 'white', + lineDash: [15, 15], + width: 4 + }) + }), + new Style({ + image: new Circle({ + radius: 4, + fill: new Fill({ + color: 'blue' + }) + }) + }) +] diff --git a/src/renderer/ol/interaction/measure/index.js b/src/renderer/ol/interaction/measure/index.js index 59e633cd..650d32d2 100644 --- a/src/renderer/ol/interaction/measure/index.js +++ b/src/renderer/ol/interaction/measure/index.js @@ -5,12 +5,13 @@ import { Vector as VectorSource } from 'ol/source' import { Vector as VectorLayer } from 'ol/layer' import Circle from 'ol/geom/Circle' import uuid from '../../../../shared/uuid' -import GeometryType from './GeometryType' -import { baseStyle, stylefunctionForGeometryType } from './style' +import GeometryType from '../GeometryType' +import { baseStyle } from './baseStyle' +import { styleFN } from './style' import { getLastSegmentCoordinates } from './tools' import { militaryFormat } from '../../../../shared/datetime' import * as ID from '../../../ids' -import { writeFeatureObject } from '../../../store/FeatureStore' +import { writeFeatureObject } from '../../../ol/format' export default ({ map, services }) => { @@ -55,7 +56,7 @@ export default ({ map, services }) => { }) drawInteraction.once('drawstart', ({ feature }) => { - feature.setStyle(stylefunctionForGeometryType(geometryType, () => true)) + feature.setStyle(styleFN(() => true)) if (geometryType !== GeometryType.LINE_STRING) return /* circle helper is only supported when measuring distances */ diff --git a/src/renderer/ol/interaction/measure/style.js b/src/renderer/ol/interaction/measure/style.js index c33aa2d4..f0d2f94f 100644 --- a/src/renderer/ol/interaction/measure/style.js +++ b/src/renderer/ol/interaction/measure/style.js @@ -1,189 +1,19 @@ -import { Circle as CircleStyle, Fill, Stroke, Style, Text as TextStyle } from 'ol/style' -import Point from 'ol/geom/Point' -import LineString from 'ol/geom/LineString' -import GeometryType from './GeometryType' +import { Polygon } from './PolygonStyle' +import { LineString } from './LineStringStyle' +import { baseStyle } from './baseStyle' -import { angle, radiansAngle, length, getLastSegmentCoordinates, area } from './tools' - -const FONT = '12px sans-serif' - -export const baseStyle = isSelected => [ - new Style({ - stroke: new Stroke({ - color: isSelected ? 'blue' : 'red', - width: 4 - }) - }), - new Style({ - stroke: new Stroke({ - color: 'white', - lineDash: [15, 15], - width: 4 - }) - }), - new Style({ - image: new CircleStyle({ - radius: 4, - fill: new Fill({ - color: 'blue' - }) - }) - }) -] - -/* style function for POLYGON */ -const polygonStyle = feature => { - - const styles = [] - const geometry = feature.getGeometry() - - const coordinates = geometry.getCoordinates()[0] - const numberOfSegments = coordinates.length - 1 - - for (let i = 0; i < numberOfSegments; i++) { - const segment = new LineString([coordinates[i], coordinates[i + 1]]) - styles.push(new Style({ - geometry: segment, - text: new TextStyle({ - text: `${length(segment)}\n\n`, - font: FONT, - fill: new Fill({ - color: 'black' - }), - stroke: new Stroke({ - color: 'white', - width: 5 - }), - placement: 'line', - overflow: true, - textBaseline: 'middle' - }) - })) - } - - styles.push( - new Style({ - geometry: geometry.getInteriorPoint(), - text: new TextStyle({ - text: `${area(geometry)}\n${length(geometry)}`, - font: FONT, - fill: new Fill({ - color: 'black' - }), - stroke: new Stroke({ - color: 'white', - width: 5 - }), - placement: 'point', - overflow: true, - textBaseline: 'ideographic' - }) - }) - ) - - return styles -} - -/* style function for LINE_STRING */ -const linestringStyle = feature => { - const styles = [] - const geometry = feature.getGeometry() - - let numberOfSegments = 0 - - geometry.forEachSegment((start, end) => { - const segment = new LineString([start, end]) - numberOfSegments++ - styles.push(new Style({ - geometry: segment, - text: new TextStyle({ - text: `${length(segment)}\n\n${angle(segment)}`, - font: FONT, - fill: new Fill({ - color: 'black' - }), - stroke: new Stroke({ - color: 'white', - width: 5 - }), - placement: 'line', - overflow: true, - textBaseline: 'middle' - }) - })) - }) - - /* first point of the linestring */ - styles.push(new Style({ - geometry: new Point(geometry.getFirstCoordinate()), - image: new CircleStyle({ - radius: 6, - fill: new Fill({ - color: 'green' - }) - }) - })) - - const lastSegment = new LineString(getLastSegmentCoordinates(geometry)) - const alpha = radiansAngle(lastSegment) - - /* set style and label for last point but only if we have more than one segment */ - - styles.push( - new Style({ - geometry: new Point(geometry.getLastCoordinate()), - image: new CircleStyle({ - radius: 6, - fill: new Fill({ - color: 'red' - }) - }), - text: (numberOfSegments === 1) - ? '' - : new TextStyle({ - text: length(geometry), - font: FONT, - fill: new Fill({ - color: 'black' - }), - stroke: new Stroke({ - color: 'white', - width: 5 - }), - offsetX: 25 * Math.cos(alpha), - offsetY: -25 * Math.sin(alpha), - placement: 'point', - textAlign: Math.abs(alpha) < Math.PI / 2 ? 'left' : 'right', - overflow: true, - textBaseline: 'ideographic' - }) - }) - ) - - - return styles -} - -/*** - * - * isSelected: function (feature) => Boolean - */ -export const stylist = (isSelected) => (feature) => { - const geometry = feature.getGeometry() - return stylefunctionForGeometryType(geometry.getType(), isSelected)(feature) +export const STYLES = { + Polygon, + LineString } -/* returns a style function for the given geometry type and selection state */ -export const stylefunctionForGeometryType = (geometryType, isSelected) => { - // const styles = - - if (geometryType === GeometryType.POLYGON) { - return feature => [...(baseStyle(isSelected(feature))), ...polygonStyle(feature)] - } else if (geometryType === GeometryType.LINE_STRING) { - return feature => [...(baseStyle(isSelected(feature))), ...linestringStyle(feature)] - } - - return () => { - baseStyle(() => false) +export const styleFN = (isSelected) => { + return feature => { + const geometry = feature.getGeometry() + const geometryType = geometry.getType() + return [ + ...baseStyle(isSelected(feature)), + ...STYLES[geometryType](geometry) + ] } } diff --git a/src/renderer/ol/interaction/measure/tools.js b/src/renderer/ol/interaction/measure/tools.js index 09485a86..98721ecd 100644 --- a/src/renderer/ol/interaction/measure/tools.js +++ b/src/renderer/ol/interaction/measure/tools.js @@ -1,4 +1,4 @@ -import GeometryType from './GeometryType' +import GeometryType from '../GeometryType' import { getArea, getLength } from 'ol/sphere' const meterFormatter = new Intl.NumberFormat(window.navigator.userLanguage || window.navigator.language, { maximumFractionDigits: 1, style: 'unit', unit: 'meter' }) diff --git a/src/renderer/ol/interaction/modify-interaction.js b/src/renderer/ol/interaction/modify-interaction.js index f8b172b6..d322842f 100644 --- a/src/renderer/ol/interaction/modify-interaction.js +++ b/src/renderer/ol/interaction/modify-interaction.js @@ -1,5 +1,5 @@ import { Modify } from './modify' -import { writeGeometryObject } from '../../store/FeatureStore' +import { writeGeometryObject } from '../../ol/format' /** * @param {*} store diff --git a/src/renderer/ol/interaction/modify/events.js b/src/renderer/ol/interaction/modify/events.js index 4ca94b0d..34b60958 100644 --- a/src/renderer/ol/interaction/modify/events.js +++ b/src/renderer/ol/interaction/modify/events.js @@ -1,7 +1,5 @@ import * as R from 'ramda' import Event from 'ol/events/Event' -import { currentTime } from '@most/scheduler' -import { map } from '@most/core' import { Coordinate } from './coordinate' /** @@ -121,95 +119,14 @@ export const pointer = (options, rbush, event) => { pointer.pick = () => { const [segment] = sortedSegments() const [coordinate, index] = vertex(segment) - return { segment, coordinate, index } - } - - return pointer -} - -/** - * fromListeners :: [string] -> EventTarget -> Stream - */ -export const fromListeners = (types, target) => ({ - run: (sink, scheduler) => { - const push = event => sink.event(currentTime(scheduler), event) - types.forEach(type => target.addEventListener(type, push)) - - return { - dispose: () => types.forEach(type => target.removeEventListener(type, push)) - } - } -}) - -export class Pipe { - constructor (sink) { this.sink = sink } - end (time) { this.sink.end(time) } - error (time, err) { this.sink.end(time, err) } -} - -class Op { - constructor (sink, stream) { - this.sink = sink - this.stream = stream - } - run (sink, scheduler) { - return this.stream.run(this.sink(sink), scheduler) - } -} + // Coordinate must never be undefined in order + // to be handled correctly as a signal. + return coordinate === undefined + ? ({ segment, coordinate: null, index }) + : ({ segment, coordinate, index }) -class Replace { - constructor (stream) { - this.stream = stream } - run (sink, scheduler) { - return this.stream.run(new ReplaceSink(sink, scheduler), scheduler) - } -} - -class ReplaceSink extends Pipe { - constructor (sink, scheduler) { - super(sink) - this.scheduler = scheduler - this.disposable = { - dispose: () => {} - } - } - - event (time, stream) { - this.disposable.dispose() - this.disposable = stream.run(this.sink, this.scheduler) - } -} - -/** - * Flatten stream of arrays with depth = 1. - */ -class Flat { - constructor (stream) { - this.stream = stream - } - - run (sink, scheduler) { - this.stream.run(new FlatSink(sink), scheduler) - } -} - -class FlatSink extends Pipe { - event (time, xs) { - if (!Array.isArray(xs)) this.sink.event(time, xs) - else xs.forEach(x => this.sink.event(time, x)) - } + return pointer } - -export const flat = stream => new Flat(stream) -export const flatN = n => Array(n).fill(flat).reduce((f, g) => R.compose(g, f)) - -/** - * replace, aka SwitchAll (RxJS) - */ -export const replace = stream => new Replace(stream) -export const orElse = value => map(that => that || value) -export const op = sink => stream => new Op(sink, stream) -export const pipe = ops => stream => ops.reduce((acc, op) => op(acc), stream) diff --git a/src/renderer/ol/interaction/modify/index.js b/src/renderer/ol/interaction/modify/index.js index 13ab3d18..f3b7cbea 100644 --- a/src/renderer/ol/interaction/modify/index.js +++ b/src/renderer/ol/interaction/modify/index.js @@ -5,57 +5,70 @@ import VectorLayer from 'ol/layer/Vector' import VectorSource from 'ol/source/Vector' import Point from 'ol/geom/Point' import * as style from 'ol/style' -import * as Subject from 'most-subject' -import * as M from '@most/core' -import { runEffects } from '@most/core' -import { newDefaultScheduler, currentTime } from '@most/scheduler' -import { writeIndex } from './writers' -import { setCoordinates } from '../../../model/geometry' +import Signal from '@syncpoint/signal' import * as Events from './events' -import { ModifyEvent, pipe, fromListeners, replace, orElse, op, flat } from './events' import { selected } from './states' +import { ModifyEvent } from './events' +import { writeIndex } from './writers' +import { flat } from '../../../../shared/signal' /** * Sink for vertex feature overlay. * Accept coordinate events and update feature accordingly. */ -export class OverlaySink { - constructor (style, options) { +class OverlaySink { + constructor (style, options, coordinate) { const source = new VectorSource({ useSpatialIndex: false, wrapX: !!options.wrapX }) - this.style = style this.layer = new VectorLayer({ source, updateWhileAnimating: true, updateWhileInteracting: true }) + + this.dispose = Signal.on(coordinate => { + const feature = source.getFeatureById('feature:pointer') + if (coordinate && !feature) { + const pointer = new Feature(new Point(coordinate)) + pointer.setId('feature:pointer') + pointer.setStyle(style) + source.addFeature(pointer) + } else if (coordinate && feature) { + const geometry = feature.getGeometry() + geometry.setCoordinates(coordinate) + } else if (!coordinate && feature) { + source.removeFeature(feature) + } + }, coordinate) } - setMap (map) { this.layer.setMap(map) } - end (time) { console.log('[OverlaySink] end') } - error (time, err) { console.error('[OverlaySink] error', err) } - - event (time, coordinate) { - const source = this.layer.getSource() - const feature = source.getFeatureById('feature:pointer') - - if (coordinate && !feature) { - const pointer = new Feature(new Point(coordinate)) - pointer.setId('feature:pointer') - pointer.setStyle(this.style) - source.addFeature(pointer) - } else if (coordinate && feature) { - const geometry = feature.getGeometry() - geometry.setCoordinates(coordinate) - } else if (!coordinate && feature) { - source.removeFeature(feature) - } + setMap (map) { + if (!map) this.dispose() + this.layer.setMap(map) } } +/** + * rbush :: ol/Feature => Signal ol/structs/RBush + */ +const rbush = (() => { + let changeEvent + + return feature => { + if (changeEvent) changeEvent.dispose() + changeEvent = Signal.fromListeners(['change'], feature) + return R.compose( + Signal.startWith(() => writeIndex(feature)), + R.map(({ target }) => writeIndex(target)), + R.reject(({ target }) => target.internalChange()) + )(changeEvent) + } +})() + + /** * */ @@ -64,150 +77,80 @@ export class Modify extends Interaction { constructor (options) { super(options) - this.overlay = new OverlaySink(pointerStyles.DEFAULT, options) - - // Setup subject to receive map browser events: - const [sink, event$] = Subject.create() - this.next = event => Subject.event(currentTime(scheduler), event, sink) - - // Apply (rbush, event) to current state and update state accordingly. - const eventLoop = (state, [rbush, event]) => { - + // this.mapEvent :: Signal ol/MapBrowserEvent + // Receive map browser (mouse, keyboard) events. + this.mapEvent = Signal.of() + + // feature :: [ol/Feature] => ol/Feature + // Single feature or placeholder feature without geometry. + const feature = features => + features.length === 1 + ? features[0] + : new Feature() + + // isSymbol :: ol/Feature => Boolean + const isSymbol = feature => feature?.getGeometry()?.getType() === 'Point' + + // sourceEvent :: Signal ol/source/Vector.VectorSourceEvent + // Track feature add/remove events to update spatial index. + const sourceEvent = Signal.fromListeners(['addfeature', 'removefeature'], options.source) + + // spatialIndex :: Signal ol/structs/RBush + const spatialIndex = R.compose( + R.chain(rbush), + R.reject(isSymbol), + R.map(feature), + R.map(({ target }) => target.getFeatures()) + )(sourceEvent) + + // spatialEvent :: Signal [ol/structs/RBush, ol/MapBrowserEvent] + // Map browser event in context of current spatial index. + const spatialEvent = + Signal.lift((rbush, event) => [rbush, event], spatialIndex, this.mapEvent) + + // eventHandler :: Coordinate [Number, Number] => + // State -> [ol/structs/RBush, ol/MapBrowserEvent] -> [State, Coordinate] + const eventHandler = (state, [rbush, event]) => { // For empty index, reset to loaded state: - if (rbush.isEmpty()) return { seed: selected(true), value: Events.coordinate(null) } - + if (rbush.isEmpty()) return [selected(true), Events.coordinate(null)] const pointer = Events.pointer(options, rbush, event) const handler = state[event.type] - const [seed, value] = (handler && handler(pointer)) || [state, null] - return { seed, value } + return (handler && handler(pointer)) || [state, undefined] } - // Setup RBush stream from vector source add/remove - // feature and feature change events: - const rbush$ = Modify.rbush(options.source) - - const pipeline$ = pipe([ - - // combine :: (a -> b -> c) -> Stream a -> Stream b -> Stream c - // Apply a function to the most recent event from each Stream - // when a new event arrives on any Stream. - M.combine((rbush, event) => [rbush, event], rbush$), - M.loop(eventLoop, selected(true)), - M.filter(R.identity), + // stateLoop :: Signal { type: 'coordinate, ... } | ModifyEvent + const stateLoop = R.compose( + R.reject(R.isNil), flat, - M.multicast - ])(event$) - - // Coordinate path. Pipe coordinate events to overlay. - const coordinate$ = pipe([ - M.filter(event => event.type === 'coordinate'), - M.map(({ coordinate }) => coordinate), - op(() => this.overlay) - ])(pipeline$) - - // Update path. Pipe update events to store (indirectly). - const update$ = pipe([ - M.filter(event => event instanceof ModifyEvent), - M.tap(event => this.dispatchEvent(event)) - ])(pipeline$) - - const scheduler = newDefaultScheduler() - runEffects(coordinate$, scheduler) - runEffects(update$, scheduler) + Signal.loop(eventHandler, selected(true)) + )(spatialEvent) + + // coordinate :: Coordinate [Number, Number] => Signal Coordinate + // Filter coordinates from state loop. + const coordinate = R.compose( + R.map(({ coordinate }) => coordinate), + R.filter(event => event.type === 'coordinate') + )(stateLoop) + + // modifyEvent :: Signal + // Filter ModifyEvent to be dispatched from state loop. + const modifyEvent = R.compose( + Signal.filter(event => event instanceof ModifyEvent) + )(stateLoop) + + // Effects: Update overlay feature coordinate, dispatch modify event + this.overlay = new OverlaySink(pointerStyles.DEFAULT, options, coordinate) + Signal.link(event => this.dispatchEvent(event), modifyEvent) } handleEvent (event) { - this.next(event) + this.mapEvent(event) // Returning true will propagate event to next interaction, // unless stopPropagation() was called on event. return true } - /** - * rbush :: RBush a => ol/VectorSource -> Stream a - */ - static rbush (source) { - - const isSymbol = feature => - feature.getGeometry() && - feature.getGeometry().getType() === 'Point' - - const pipeline = pipe([ - M.map(({ target }) => target.getFeatures()), - - // Only one selected feature allowed: - M.map(features => features.length === 1 ? features[0] : null), - - // Replace null with dummy feature without geometry: - orElse(new Feature()), - - // Exclude 1-point symbols from modify: - M.filter(feature => R.not(isSymbol(feature))), - - M.skipRepeats, - - // map :: RBush a => ol.Feature -> Stream a - // Higher-order stream of RBush events. - M.map(Modify.featureProxy), - - // Switch to new RBush stream as it arrives, - // ending the previous stream. RBush events - // are piped/flattened to output stream. - replace - ]) - - const source$ = fromListeners(['addfeature', 'removefeature'], source) - return pipeline(source$) - } - - /** - * createProxy :: RBush a => ol/Feature -> Stream a - * Feature proxy with - * @property coordinates - writable, geometry coordinates - * @property commit - emit change event for feature - */ - static featureProxy (feature) { - // feature change event: false -> external, true -> internal - let internalChange = false - - const handlers = { - set (target, property, value) { - // coordinates setter on feature proxy: - if (property === 'coordinates') { - internalChange = true - setCoordinates(target.getGeometry(), value) - internalChange = false - return true - } else { - return Reflect.set(target, property, value) - } - } - } - - const proxy = new Proxy(feature, handlers) - - // Simulate external change by emitting change event explicitly. - proxy.commit = () => { - // Must not happen inside this stack frame: - const event = { type: 'change', target: feature } - const dispatch = () => feature.dispatchEvent(event) - setTimeout(dispatch, 0) - } - - // Create new spatial index for each external change event: - const pipeline = pipe([ - M.filter(() => !internalChange), - M.map(() => writeIndex(proxy)), - - // Initial spatial index: - M.startWith(writeIndex(proxy)) - ]) - - const change$ = fromListeners(['change'], feature) - return pipeline(change$) - } - setMap (map) { super.setMap(map) this.overlay.setMap(map) diff --git a/src/renderer/ol/interaction/modify/states.js b/src/renderer/ol/interaction/modify/states.js index f11e3e1f..d5150f86 100644 --- a/src/renderer/ol/interaction/modify/states.js +++ b/src/renderer/ol/interaction/modify/states.js @@ -72,7 +72,7 @@ export const selected = (handleClick = false) => ({ const { segment, index } = pick const { feature } = segment - feature.coordinates = removeVertex(segment, index) + feature.updateCoordinates(removeVertex(segment, index)) feature.commit() return [selected(), [ @@ -92,7 +92,7 @@ const drag = (feature, update) => ({ const [coordinates, coordinate] = update(pointer.coordinate, pointer.condition) // Side-effect: Update feature coordinates and thus geometry. - feature.coordinates = coordinates + feature.updateCoordinates(coordinates) return [drag(feature, update), Events.coordinate(coordinate)] }, @@ -119,7 +119,7 @@ const insert = pick => { const coordinate = pointer.coordinate const feature = segment.feature const [coordinates, update] = insertVertex(segment, coordinate) - feature.coordinates = coordinates + feature.updateCoordinates(coordinates) return [drag(feature, update), [Events.coordinate(coordinate)]] } }, @@ -138,7 +138,7 @@ const remove = pick => { return { pointerup: () => { const feature = segment.feature - feature.coordinates = removeVertex(segment, index) + feature.updateCoordinates(removeVertex(segment, index)) feature.commit() // Remain in REMOVE state and wait for next click event: diff --git a/src/renderer/ol/interaction/modify/writers.js b/src/renderer/ol/interaction/modify/writers.js index 28935709..b836847d 100644 --- a/src/renderer/ol/interaction/modify/writers.js +++ b/src/renderer/ol/interaction/modify/writers.js @@ -6,7 +6,7 @@ import { geometryType } from '../../../model/geometry' import { Hooks } from './hooks' /** - * signature :: ol.Feature -> String + * signature :: ol/Feature -> String * Geometry type plus optional layout from feature descriptors. */ const signature = feature => { @@ -19,7 +19,7 @@ const signature = feature => { } /** - * writeIndex :: ol.Feature => ol.structs.RBush + * writeIndex :: ol/Feature => ol/structs/RBush * Create new spatial index (R-Bush) from feature. */ export const writeIndex = feature => { diff --git a/src/renderer/ol/interaction/translate-interaction.js b/src/renderer/ol/interaction/translate-interaction.js index 7c5cf4bf..0ec06177 100644 --- a/src/renderer/ol/interaction/translate-interaction.js +++ b/src/renderer/ol/interaction/translate-interaction.js @@ -1,6 +1,6 @@ import * as R from 'ramda' import { Translate } from 'ol/interaction' -import { writeFeatureCollection } from '../../store/FeatureStore' +import { writeFeatureCollection } from '../../ol/format' import { noModifierKeys, shiftKeyOnly } from 'ol/events/condition' /** diff --git a/src/renderer/ol/style/labels.js b/src/renderer/ol/style/_bbox.js similarity index 55% rename from src/renderer/ol/style/labels.js rename to src/renderer/ol/style/_bbox.js index f14db774..1cb04a32 100644 --- a/src/renderer/ol/style/labels.js +++ b/src/renderer/ol/style/_bbox.js @@ -1,20 +1,21 @@ -import * as R from 'ramda' -import { Jexl } from 'jexl' +import * as TS from '../ts' +import { PI_OVER_2 } from '../../../shared/Math' + const canvas = document.createElement('canvas') const context = canvas.getContext('2d') /** * */ -const textBoundingBox = ({ TS, PI_OVER_2, resolution }, props) => { - const textField = props['text-field'] +const textBoundingBox = (resolution, style) => { + const textField = style['text-field'] if (!textField) return null - if (props['text-clipping'] === 'none') return null + if (style['text-clipping'] === 'none') return null // Prepare bounding box geometry (dimensions only, including padding). const lines = textField.split('\n') const [maxWidthPx, maxHeightPx] = lines.reduce((acc, line) => { - context.font = props['text-font'] + context.font = style['text-font'] const metrics = context.measureText(line) const width = metrics.width const height = 1.2 * lines.length * ((metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent)) @@ -23,8 +24,8 @@ const textBoundingBox = ({ TS, PI_OVER_2, resolution }, props) => { return acc }, [0, 0]) - const { x, y } = props.geometry.getCoordinates()[0] - const padding = props['text-padding'] || 0 + const { x, y } = style.geometry.getCoordinates()[0] + const padding = style['text-padding'] || 0 const dx = (maxWidthPx / 2 + padding) * resolution const dy = (maxHeightPx / 2 + padding) * resolution @@ -38,9 +39,9 @@ const textBoundingBox = ({ TS, PI_OVER_2, resolution }, props) => { // Transform geometry (rotate/translate) to match // label options offset, justify and rotate. - const rotate = props['text-rotate'] || 0 - const justify = props['text-justify'] || 'center' - const [offsetX, offsetY] = props['text-offset'] || [0, 0] + const rotate = style['text-rotate'] || 0 + const justify = style['text-justify'] || 'center' + const [offsetX, offsetY] = style['text-offset'] || [0, 0] const flipX = { start: -1, end: 1, center: 0 } const flipY = rotate < -PI_OVER_2 || rotate > PI_OVER_2 ? -1 : 1 @@ -59,15 +60,15 @@ const textBoundingBox = ({ TS, PI_OVER_2, resolution }, props) => { /** * */ -const iconBoundingBox = ({ TS, resolution }, props) => { - const scale = props['icon-scale'] +const iconBoundingBox = (resolution, style) => { + const scale = style['icon-scale'] if (!scale) return null - const width = props['icon-width'] * scale / 4 - const height = props['icon-height'] * scale / 4 - const rotate = props['icon-rotate'] || 0 - const padding = props['icon-padding'] || 0 - const { x, y } = props.geometry.getCoordinates()[0] + const width = style['icon-width'] * scale / 4 + const height = style['icon-height'] * scale / 4 + const rotate = style['icon-rotate'] || 0 + const padding = style['icon-padding'] || 0 + const { x, y } = style.geometry.getCoordinates()[0] const x1 = x - (width + padding) * resolution const x2 = x + (width + padding) * resolution @@ -80,37 +81,15 @@ const iconBoundingBox = ({ TS, resolution }, props) => { return rotation.transform(geometry) } - -/** - * - */ -export const boundingBox = R.curry((context, style) => { - if (style['text-field']) return textBoundingBox(context, style) - else if (style['icon-image']) return iconBoundingBox(context, style) +const bbox = resolution => style => { + if (style['text-field']) return textBoundingBox(resolution, style) + else if (style['icon-image']) return iconBoundingBox(resolution, style) else return null -}) - -const jexl = new Jexl() +} /** * */ -export const evalSync = context => { - - const evalSync = textField => Array.isArray(textField) - ? textField.map(evalSync).filter(Boolean).join('\n') - : jexl.evalSync(textField, context) - - return props => { - props = Array.isArray(props) ? props : [props] - return props.reduce((acc, spec) => { - if (!spec['text-field']) acc.push(spec) - else { - const textField = evalSync(spec['text-field']) - if (textField) acc.push({ ...spec, 'text-field': textField }) - } - - return acc - }, []) - } -} +export default (resolution, styles) => styles + .map(bbox(resolution)) + .filter(Boolean) diff --git a/src/renderer/ol/style/_clip.js b/src/renderer/ol/style/_clip.js new file mode 100644 index 00000000..4e6eeae4 --- /dev/null +++ b/src/renderer/ol/style/_clip.js @@ -0,0 +1,23 @@ +import * as TS from '../ts' +import _bboxes from './_bbox' + +/** + * + */ +export default resolution => { + return styles => { + const bboxes = _bboxes(resolution, styles) + if (bboxes.length === 0) return styles + + const clip = geometry => TS.difference([geometry, ...bboxes]) + const lineString = geometry => TS.lineString(geometry.getCoordinates()) + const geometry = styles.some(props => props['text-clipping'] === 'line') + ? lineString(styles[0].geometry) + : styles[0].geometry + + // Replace primary geometry with clipped geometry: + styles[0].geometry = clip(geometry) + + return styles + } +} diff --git a/src/renderer/ol/style/_colorScheme.js b/src/renderer/ol/style/_colorScheme.js new file mode 100644 index 00000000..3f84cdd2 --- /dev/null +++ b/src/renderer/ol/style/_colorScheme.js @@ -0,0 +1,10 @@ + +/** + * + */ +export default (globalStyle, layerStyle, featureStyle) => { + return featureStyle?.['color-scheme'] || + layerStyle?.['color-scheme'] || + globalStyle?.['color-scheme'] || + 'medium' +} diff --git a/src/renderer/ol/style/_context.js b/src/renderer/ol/style/_context.js new file mode 100644 index 00000000..fcf363f5 --- /dev/null +++ b/src/renderer/ol/style/_context.js @@ -0,0 +1,4 @@ +import * as TS from '../ts' +import * as Math from '../../../shared/Math' + +export default (geometry, resolution) => ({ TS, ...Math, geometry, resolution }) diff --git a/src/renderer/ol/style/_effectiveStyle.js b/src/renderer/ol/style/_effectiveStyle.js new file mode 100644 index 00000000..6df90970 --- /dev/null +++ b/src/renderer/ol/style/_effectiveStyle.js @@ -0,0 +1,14 @@ + +/** + * + */ +export default (globalStyle, schemeStyle, layerStyle, featureStyle) => { + if (!layerStyle['line-color']) delete layerStyle['line-color'] + if (!layerStyle['line-halo-color']) delete layerStyle['line-halo-color'] + return { + ...globalStyle, + ...schemeStyle, + ...layerStyle, + ...featureStyle + } +} diff --git a/src/renderer/ol/style/_evalSync.js b/src/renderer/ol/style/_evalSync.js new file mode 100644 index 00000000..689e0012 --- /dev/null +++ b/src/renderer/ol/style/_evalSync.js @@ -0,0 +1,47 @@ +import { echelonCode } from '../../symbology/2525c' +import { echelons } from './echelon' +import { Jexl } from 'jexl' + +const jexl = new Jexl() + +/** + * + */ +const evalSync = context => { + const evalSync = textField => Array.isArray(textField) + ? textField.map(evalSync).filter(Boolean).join('\n') + : jexl.evalSync(textField, context) + + const replaceOne = properties => { + properties = Array.isArray(properties) ? properties : [properties] + return properties.reduce((acc, spec) => { + if (!spec['text-field']) acc.push(spec) + else { + const textField = evalSync(spec['text-field']) + if (textField) acc.push({ ...spec, 'text-field': textField }) + } + + return acc + }, []) + } + + const replaceAll = arg => { + if (!Array.isArray(arg)) return replaceAll(arg) + return arg.flatMap(replaceOne) + } + + return replaceAll +} + +export default (sidc, props1, props2) => { + const code = echelonCode(sidc) + const echelon = + (code === '*' || code === '-') + ? '' + : echelons[code]?.text + + return evalSync({ + modifiers: { ...props1, ...props2 }, + echelon + }) +} diff --git a/src/renderer/ol/style/_labels.js b/src/renderer/ol/style/_labels.js new file mode 100644 index 00000000..e03f8724 --- /dev/null +++ b/src/renderer/ol/style/_labels.js @@ -0,0 +1,5 @@ + +/** + * + */ +export default labels => sidc => (labels[sidc] || []).flat() diff --git a/src/renderer/ol/style/_lineSmoothing.js b/src/renderer/ol/style/_lineSmoothing.js new file mode 100644 index 00000000..d5163a5c --- /dev/null +++ b/src/renderer/ol/style/_lineSmoothing.js @@ -0,0 +1,5 @@ + +/** + * + */ +export default style => style['line-smooth'] || false diff --git a/src/renderer/ol/style/_rewrite.js b/src/renderer/ol/style/_rewrite.js new file mode 100644 index 00000000..372ba5a0 --- /dev/null +++ b/src/renderer/ol/style/_rewrite.js @@ -0,0 +1,8 @@ + +/** + * + */ +export default fn => ({ geometry, ...rest }) => + geometry + ? ({ geometry: fn(geometry), ...rest }) + : rest diff --git a/src/renderer/ol/style/_schemeStyle.js b/src/renderer/ol/style/_schemeStyle.js new file mode 100644 index 00000000..9a2afbd9 --- /dev/null +++ b/src/renderer/ol/style/_schemeStyle.js @@ -0,0 +1,22 @@ +import * as Colors from './color-schemes' +import { identityCode, statusCode } from '../../symbology/2525c' + +/** + * + */ +export default (sidc, colorScheme) => { + const status = statusCode(sidc) + const identity = identityCode(sidc) + const simpleIdentity = identity === 'H' || identity === 'S' + ? 'H' + : '-' + + return { + 'binary-color': Colors.lineColor(colorScheme)(simpleIdentity), // black or red + 'line-color': Colors.lineColor(colorScheme)(identity), + 'fill-color': Colors.lineColor(colorScheme)(identity), + 'line-dash-array': status === 'A' ? [20, 10] : null, + 'line-halo-color': Colors.lineHaloColor(identity), + 'line-halo-dash-array': status === 'A' ? [20, 10] : null + } +} diff --git a/src/renderer/ol/style/_selection.js b/src/renderer/ol/style/_selection.js new file mode 100644 index 00000000..568d1ddc --- /dev/null +++ b/src/renderer/ol/style/_selection.js @@ -0,0 +1,22 @@ +import * as R from 'ramda' +import * as TS from '../ts' + +export default (mode, geometry) => { + const selection = [] + const guideline = mode === 'singleselect' + ? { id: 'style:guide-stroke', geometry } + : null + + const points = () => TS.points(geometry) + + const handles = R.cond([ + [R.equals('default'), R.always(null)], + [R.equals('singleselect'), R.always({ id: 'style:circle-handle', geometry: TS.multiPoint(points()) })], + [R.equals('multiselect'), R.always({ id: 'style:rectangle-handle', geometry: points()[0] })] + ])(mode) + + guideline && selection.push(guideline) + handles && selection.push(handles) + + return selection +} diff --git a/src/renderer/ol/style/_shape.js b/src/renderer/ol/style/_shape.js new file mode 100644 index 00000000..ae87976e --- /dev/null +++ b/src/renderer/ol/style/_shape.js @@ -0,0 +1,10 @@ +import * as R from 'ramda' + +/** + * + */ +export default styles => sidc => { + const tryer = (styles[sidc] || styles.DEFAULT) + const catcher = (_, context) => (styles.ERROR || styles.DEFAULT)(context) + return R.tryCatch(tryer, catcher) +} diff --git a/src/renderer/ol/style/_simplifiedGeometry.js b/src/renderer/ol/style/_simplifiedGeometry.js new file mode 100644 index 00000000..5eee0998 --- /dev/null +++ b/src/renderer/ol/style/_simplifiedGeometry.js @@ -0,0 +1,15 @@ + +/** + * + */ +export default (geometry, resolution) => { + const geometryType = geometry.getType() + const coordinates = geometry.getCoordinates() + const simplify = + (geometryType === 'Polygon' && coordinates[0].length > 50) || + (geometryType === 'LineString' && coordinates.length > 50) + + return simplify + ? geometry.simplify(resolution) + : geometry +} diff --git a/src/renderer/ol/style/_smoothenedGeometry.js b/src/renderer/ol/style/_smoothenedGeometry.js new file mode 100644 index 00000000..5f461f8f --- /dev/null +++ b/src/renderer/ol/style/_smoothenedGeometry.js @@ -0,0 +1,8 @@ +import { smooth } from './chaikin' + +/** + * + */ +export default (simplifiedGeometry, lineSmoothing) => lineSmoothing + ? smooth(simplifiedGeometry) + : simplifiedGeometry diff --git a/src/renderer/ol/style/_transform.js b/src/renderer/ol/style/_transform.js new file mode 100644 index 00000000..874297fc --- /dev/null +++ b/src/renderer/ol/style/_transform.js @@ -0,0 +1,8 @@ +import * as R from 'ramda' +import { transform } from '../../model/geometry' +import { destructure } from '../../../shared/signal' + +export default R.compose( + destructure(['read', 'write', 'pointResolution']), + R.map(transform) +) diff --git a/src/renderer/ol/style/corridor.js b/src/renderer/ol/style/corridor.js index 910e99e8..a2537afa 100644 --- a/src/renderer/ol/style/corridor.js +++ b/src/renderer/ol/style/corridor.js @@ -1,55 +1,16 @@ -/* eslint-disable camelcase */ -import * as shared from './shared' -import styles from './corridor-styles' -import { transform } from '../../model/geometry' - -const collectStyles = [next => { - const { parameterizedSIDC: sidc } = next - const dynamicStyle = (styles[sidc] || styles.DEFAULT) - const staticStyles = [] - return { dynamicStyle, staticStyles } -}, ['parameterizedSIDC']] - -/** - * geometry :: jsts/geom/geometry - * write :: jsts/geom/geometry -> ol/geom/geometry - * resolution :: Number - */ -const geometry = [next => { - const { definingGeometry, centerResolution } = next - - // Transform (TS/UTM). - // - const { read, write, pointResolution } = transform(definingGeometry) - const geometry = read(definingGeometry) - const simplifiedGeometry = geometry - const resolution = pointResolution(centerResolution) - const rewrite = ({ geometry, ...props }) => ({ geometry: write(geometry), ...props }) - return { geometry, simplifiedGeometry, rewrite, resolution } -}, ['mode', 'smoothen', 'geometryKey', 'centerResolution']] - - -const labelPlacement = [() => { - return { placement: x => x } -}, ['geometry']] - - -/** - * style :: [ol/style/Style] - */ -const error = [next => { - return { styles: styles.ERROR(next) } -}, ['err']] - -export default [ - shared.sidc, - shared.evalSync, - collectStyles, - shared.effectiveStyle, - geometry, - labelPlacement, - shared.selectedStyles, - shared.styles, - error, - shared.style -] +import Signal from '@syncpoint/signal' +import styles from './corridor-styles/index' +import graphics from './graphics' + +import _context from './_context' +import _shape from './_shape' +import _selection from './_selection' + +const specifics = $ => { + $.context = Signal.link(_context, [$.jtsGeometry, $.resolution]) + $.shape = $.context.ap($.parameterizedSIDC.map(_shape(styles))) + $.selection = Signal.link(_selection, [$.selectionMode, $.jtsGeometry]) + $.labels = Signal.of([]) +} + +export default graphics(specifics) diff --git a/src/renderer/ol/style/defaultStyle.js b/src/renderer/ol/style/defaultStyle.js new file mode 100644 index 00000000..9bbd6c24 --- /dev/null +++ b/src/renderer/ol/style/defaultStyle.js @@ -0,0 +1,13 @@ +import { Circle, Fill, Stroke, Style } from 'ol/style' + +export default (options = {}) => { + const fill = new Fill({ color: 'rgba(255,255,255,0.3)' }) + const strokeColor = options.strokeColor ?? '#888' + const stroke = new Stroke({ color: strokeColor, width: 1.25 }) + return new Style({ + geometry: options.geometry, + image: new Circle({ fill, stroke, radius: 5 }), + fill, + stroke + }) +} diff --git a/src/renderer/ol/style/fallback.js b/src/renderer/ol/style/fallback.js new file mode 100644 index 00000000..8a8d6f12 --- /dev/null +++ b/src/renderer/ol/style/fallback.js @@ -0,0 +1,4 @@ +import Signal from '@syncpoint/signal' +import defaultStyle from './defaultStyle' + +export default () => Signal.of([defaultStyle()]) diff --git a/src/renderer/ol/style/graphics.js b/src/renderer/ol/style/graphics.js new file mode 100644 index 00000000..7730e47d --- /dev/null +++ b/src/renderer/ol/style/graphics.js @@ -0,0 +1,47 @@ +import * as R from 'ramda' +import Signal from '@syncpoint/signal' +import transform from './_transform' +import { specialization } from '../../symbology/2525c' +import { GeometryProperties } from '../../components/properties/geometries' + +import _rewrite from './_rewrite' +import _evalSync from './_evalSync' +import _clip from './_clip' + +const EMPTY_OBJECT = {} +export default specifics => $ => { + const [read, write, pointResolution] = transform($.geometry) + $.read = read + $.rewrite = write.map(fn => xs => xs.map(_rewrite(fn))) + $.pointResolution = pointResolution + $.resolution = $.centerResolution.ap($.pointResolution) + $.jtsGeometry = $.geometry.ap($.read) + $.clip = $.resolution.map(_clip) + + // Derive additional properties from geometry, + // which might be used in labels (an, am). + $.specialization = $.sidc.map(sidc => specialization(sidc) || null) + $.geometryProperties = Signal.link((specialization, geometry) => { + const fn = GeometryProperties[specialization] + return fn ? fn(geometry) : EMPTY_OBJECT + }, [$.specialization, $.jtsGeometry]) + $.evalSync = Signal.link(_evalSync, [$.sidc, $.properties, $.geometryProperties]) + + specifics($) + + $.styles = Signal.link( + (...styles) => styles.reduce(R.concat), + [ + $.shape, + $.labels, + $.selection + ] + ) + + return $.styles + .ap($.styleRegistry) + .ap($.evalSync) + .ap($.clip) + .ap($.rewrite) + .ap($.styleFactory) +} diff --git a/src/renderer/ol/style/keyequals.js b/src/renderer/ol/style/keyequals.js new file mode 100644 index 00000000..0d845a9d --- /dev/null +++ b/src/renderer/ol/style/keyequals.js @@ -0,0 +1,14 @@ +/** + * Stateful equality function to compare old/new keys of OL features/geometries. + */ +const keyequals = () => { + const key = target => `${target.ol_uid}:${target.getRevision()}` + let last + return (_, b) => { + if (last === key(b)) return true + else last = key(b) + return false + } +} + +export default keyequals diff --git a/src/renderer/ol/style/linestring-styles/index.js b/src/renderer/ol/style/linestring-styles/index.js index 0b9d5e6d..58511160 100644 --- a/src/renderer/ol/style/linestring-styles/index.js +++ b/src/renderer/ol/style/linestring-styles/index.js @@ -36,8 +36,12 @@ import G_T_A from './G_T_A' import G_T_AS from './G_T_AS' import G_T_F from './G_T_F' +const DEFAULT = ({ geometry }) => [{ id: 'style:2525c/default-stroke', geometry }] +const ERROR = ({ geometry }) => [{ id: 'style:wasp-stroke', geometry }] + export default { - DEFAULT: ({ geometry }) => [{ id: 'style:2525c/default-stroke', geometry }], + DEFAULT, + ERROR, 'G*F*LT----': G_F_LT, // LINEAR TARGET 'G*F*LTF---': G_F_LT, // FINAL PROTECTIVE FIRE (FPF) 'G*F*LTS---': G_F_LT, // LINEAR SMOKE TARGET diff --git a/src/renderer/ol/style/linestring-styles/labels.js b/src/renderer/ol/style/linestring-styles/labels.js index 06cf3826..6a0538fc 100644 --- a/src/renderer/ol/style/linestring-styles/labels.js +++ b/src/renderer/ol/style/linestring-styles/labels.js @@ -46,7 +46,7 @@ const BND_3 = { id: 'style:default-text', 'text-field': 'echelon', 'text-anchor' const BND = [BND_1, BND_2, BND_3] -export const labels = {} +const labels = {} labels['G*T*A-----'] = [{ 'text-field': 'modifiers.t', 'text-anchor': 0.15, 'text-clipping': 'none' }] // FOLLOW AND ASSUME labels['G*T*AS----'] = [{ 'text-field': 'modifiers.t', 'text-anchor': 0.15, 'text-clipping': 'none' }] // FOLLOW AND SUPPORT labels['G*G*GLB---'] = BND // BOUNDARIES @@ -80,3 +80,5 @@ labels['G*O*BE----'] = MM('"E"') // BEARING LINE / ELECTRONIC labels['G*O*BA----'] = MM('"A"') // BEARING LINE / ACOUSTIC labels['G*O*BT----'] = MM('"T"') // BEARING LINE / TORPEDO labels['G*O*BO----'] = MM('"O"') // BEARING LINE / ELECTRO-OPTICAL INTERCEPT + +export default labels diff --git a/src/renderer/ol/style/linestring-placement.js b/src/renderer/ol/style/linestring-styles/placement.js similarity index 65% rename from src/renderer/ol/style/linestring-placement.js rename to src/renderer/ol/style/linestring-styles/placement.js index c2a8e7a0..090b06d8 100644 --- a/src/renderer/ol/style/linestring-placement.js +++ b/src/renderer/ol/style/linestring-styles/placement.js @@ -1,6 +1,10 @@ import * as R from 'ramda' +import * as TS from '../../ts' -export const placement = ({ TS, geometry }) => { +/** + * placement :: jts/geom/Geometry => Style => Style + */ +const placement = geometry => { const segments = TS.segments(geometry) const line = TS.lengthIndexedLine(geometry) const endIndex = line.getEndIndex() @@ -33,17 +37,28 @@ export const placement = ({ TS, geometry }) => { const normalize = angle => TS.Angle.normalize(TS.Angle.PI_TIMES_2 - angle) - return props => { - const rotate = props['text-field'] ? 'text-rotate' : 'icon-rotate' - const anchor = props['text-anchor'] || - props['icon-anchor'] || - props['symbol-anchor'] || - (props['text-field'] ? 'center' : null) + const tryer = properties => { + const rotate = properties['text-field'] ? 'text-rotate' : 'icon-rotate' + const anchor = properties['text-anchor'] || + properties['icon-anchor'] || + properties['symbol-anchor'] || + (properties['text-field'] ? 'center' : null) return { geometry: anchors(anchor), - ...props, + ...properties, [rotate]: normalize(angle(anchor)) } } -} \ No newline at end of file + + const catcher = (err, properties) => console.warn(err, properties) + + const calculate = arg => { + if (!Array.isArray(arg)) return calculate([arg]) + else return arg.map(R.tryCatch(tryer, catcher)).filter(Boolean) + } + + return calculate +} + +export default placement diff --git a/src/renderer/ol/style/linestring.js b/src/renderer/ol/style/linestring.js index 3597d6bc..a5018844 100644 --- a/src/renderer/ol/style/linestring.js +++ b/src/renderer/ol/style/linestring.js @@ -1,42 +1,34 @@ -import * as shared from './shared' -import styles from './linestring-styles' -import { placement } from './linestring-placement' -import { labels } from './linestring-styles/labels' - -/** - * dynamicStyle - * staticStyles - */ -const collectStyles = [next => { - const { parameterizedSIDC: sidc } = next - const dynamicStyle = (styles[sidc] || styles.DEFAULT) - const staticStyles = (labels[sidc] || []) - return { dynamicStyle, staticStyles } -}, ['parameterizedSIDC']] - - -/** - * placement - */ -const labelPlacement = [next => { - return { placement: placement(next) } -}, ['geometry']] - - -export default [ - shared.sidc, - shared.evalSync, - collectStyles, - shared.effectiveStyle, - shared.geometry, - labelPlacement, - shared.selectedStyles, - shared.styles, - shared.style -] - - -// ==> label specifications and placement - - - +import Signal from '@syncpoint/signal' +import labels from './linestring-styles/labels' +import styles from './linestring-styles/index' +import placement from './linestring-styles/placement' +import graphics from './graphics' +import keyequals from './keyequals' + +import _smoothenedGeometry from './_smoothenedGeometry' +import _simplifiedGeometry from './_simplifiedGeometry' +import _context from './_context' +import _labels from './_labels' +import _shape from './_shape' +import _lineSmoothing from './_lineSmoothing' +import _selection from './_selection' + +const specifics = $ => { + $.simplifiedGeometry = Signal.link(_simplifiedGeometry, [$.geometry, $.centerResolution], { equals: keyequals() }) + $.jtsSimplifiedGeometry = $.simplifiedGeometry.ap($.read) + $.lineSmoothing = $.effectiveStyle.map(_lineSmoothing) + $.smoothenedGeometry = Signal.link(_smoothenedGeometry, [$.simplifiedGeometry, $.lineSmoothing]) + $.jtsSmoothenedGeometry = $.smoothenedGeometry.ap($.read) + $.context = Signal.link(_context, [$.jtsSmoothenedGeometry, $.resolution]) + $.placement = $.jtsSmoothenedGeometry.map(placement) + + $.shape = $.context.ap($.parameterizedSIDC.map(_shape(styles))) + $.selection = Signal.link(_selection, [$.selectionMode, $.jtsSimplifiedGeometry]) + $.labels = $.parameterizedSIDC + .map(_labels(labels)) + .ap($.placement) + + return graphics($) +} + +export default graphics(specifics) diff --git a/src/renderer/ol/style/crosshair.js b/src/renderer/ol/style/marker.js similarity index 76% rename from src/renderer/ol/style/crosshair.js rename to src/renderer/ol/style/marker.js index 6f1c25cb..b83f2ebd 100644 --- a/src/renderer/ol/style/crosshair.js +++ b/src/renderer/ol/style/marker.js @@ -1,6 +1,6 @@ import { Stroke, Circle, RegularShape, Style } from 'ol/style' -export default (color, radius = 30) => { +const crosshair = (color, radius = 30) => { const stroke = new Stroke({ color, width: 2 }) const bigCircle = new Circle({ stroke, radius: 30 }) const smallCircle = new Circle({ stroke, radius: radius / 15 }) @@ -18,3 +18,10 @@ export default (color, radius = 30) => { }) }))] } + +export default $ => + $.selectionMode.map(mode => + mode === 'default' + ? crosshair('black') + : crosshair('red') +) diff --git a/src/renderer/ol/style/measure.js b/src/renderer/ol/style/measure.js new file mode 100644 index 00000000..7cd6e201 --- /dev/null +++ b/src/renderer/ol/style/measure.js @@ -0,0 +1,20 @@ +import * as R from 'ramda' +import Signal from '@syncpoint/signal' +import { STYLES } from '../interaction/measure/style' +import { baseStyle } from '../interaction/measure/baseStyle' + +export default $ => { + $.geometryType = $.geometry.map(geometry => geometry.getType()) + $.selected = $.selectionMode.map(mode => mode !== 'default') + $.baseStyle = $.selected.map(baseStyle) + $.styleFN = $.geometryType.map(type => STYLES[type]) + $.geometryStyle = $.geometry.ap($.styleFN) + + return Signal.link( + (...styles) => styles.reduce(R.concat), + [ + $.baseStyle, + $.geometryStyle + ] + ) +} diff --git a/src/renderer/ol/style/multipoint-styles/index.js b/src/renderer/ol/style/multipoint-styles/index.js index ba6183da..1558ce4b 100644 --- a/src/renderer/ol/style/multipoint-styles/index.js +++ b/src/renderer/ol/style/multipoint-styles/index.js @@ -17,11 +17,12 @@ const circle = id => ({ TS, geometry }) => { return [{ id, geometry: buffer }] } +const DEFAULT = ({ geometry }) => [{ id: 'style:2525c/default-stroke', geometry }] const CIRCLE = circle('style:2525c/default-stroke') const FILLED_CIRCLE = circle('style:2525c/hatch-fill') export default { - DEFAULT: ({ geometry }) => [{ id: 'style:2525c/default-stroke', geometry }], + DEFAULT, CIRCLE, FILLED_CIRCLE, 'G*F*AXC---': G_F_AXC, // SENSOR RANGE FAN diff --git a/src/renderer/ol/style/multipoint-styles/labels.js b/src/renderer/ol/style/multipoint-styles/labels.js index a3668fc9..63160789 100644 --- a/src/renderer/ol/style/multipoint-styles/labels.js +++ b/src/renderer/ol/style/multipoint-styles/labels.js @@ -7,7 +7,7 @@ const ALL_LINES = title => title ? [`"${title}"`, 'modifiers.t', 'modifiers.h', ALT_LINE, DTG_LINE] : ['modifiers.t', 'modifiers.h', ALT_LINE, DTG_LINE] -export const labels = { +const labels = { 'G*F*ATC---': C(ALL_LINES()), // CIRCULAR TARGET 'G*F*ACSC--': C(ALL_LINES('FSA')), // FIRE SUPPORT AREA (FSA) CIRCULAR 'G*F*ACAC--': C(ALL_LINES('ACA')), // AIRSPACE COORDINATION AREA (ACA) CIRCULAR @@ -23,3 +23,5 @@ export const labels = { 'G*F*AKBC--': C(ALL_LINES('BKB'), HALO), // KILL BOX BLUE CIRCULAR 'G*F*AKPC--': C(ALL_LINES('PKB'), HALO) // KILL BOX PURPLE CIRCULAR } + +export default labels diff --git a/src/renderer/ol/style/multipoint.js b/src/renderer/ol/style/multipoint.js index 05de8182..31492b09 100644 --- a/src/renderer/ol/style/multipoint.js +++ b/src/renderer/ol/style/multipoint.js @@ -1,67 +1,31 @@ -/* eslint-disable no-multi-spaces */ -/* eslint-disable camelcase */ - -import * as shared from './shared' -import styles from './multipoint-styles' -import { transform } from '../../model/geometry' -import { placement } from './polygon-placement' -import { labels } from './multipoint-styles/labels' - -/** - * dynamicStyle - * staticStyles - */ -const collectStyles = [next => { - const { parameterizedSIDC: sidc } = next - const dynamicStyle = (styles[sidc] || styles.DEFAULT) - const staticStyles = (labels[sidc] || []) - return { dynamicStyle, staticStyles } -}, ['parameterizedSIDC']] - - -/** - * geometry :: jsts/geom/geometry - * rewrite :: ... - * resolution :: Number - */ -const geometry = [next => { - const { definingGeometry, centerResolution } = next - - // Transform (TS/UTM). - // - const { read, write, pointResolution } = transform(definingGeometry) - const geometry = read(definingGeometry) - const simplifiedGeometry = geometry - const resolution = pointResolution(centerResolution) - const rewrite = ({ geometry, ...props }) => ({ geometry: write(geometry), ...props }) - return { geometry, simplifiedGeometry, rewrite, resolution } -}, ['mode', 'smoothen', 'geometryKey', 'centerResolution']] - - -/** - * placement - */ -const labelPlacement = [next => { - // Explicit labels are exclusively for circular features. - // We use polygon placement to place these labels based on - // the point buffer around the features center. - // - const { TS, geometry } = next +import Signal from '@syncpoint/signal' +import * as TS from '../ts' +import labels from './multipoint-styles/labels' +import styles from './multipoint-styles/index' +import placement from './polygon-styles/placement' +import graphics from './graphics' + +import _context from './_context' +import _labels from './_labels' +import _shape from './_shape' +import _selection from './_selection' + +const _pointBuffer = geometry => { const [C, A] = TS.coordinates(geometry) const segment = TS.segment([C, A]) - const buffer = TS.pointBuffer(TS.point(C))(segment.getLength()) - return { placement: placement({ TS, geometry: buffer }) } -}, ['geometry']] - - -export default [ - shared.sidc, - shared.evalSync, - collectStyles, - shared.effectiveStyle, - geometry, - labelPlacement, - shared.selectedStyles, - shared.styles, - shared.style -] + return TS.pointBuffer(TS.point(C))(segment.getLength()) +} + +const specifics = $ => { + $.context = Signal.link(_context, [$.jtsGeometry, $.resolution]) + $.placement = $.jtsGeometry.map(_pointBuffer).map(placement) + $.shape = $.context.ap($.parameterizedSIDC.map(_shape(styles))) + $.selection = Signal.link(_selection, [$.selectionMode, $.jtsGeometry]) + $.labels = $.parameterizedSIDC + .map(_labels(labels)) + .ap($.placement) + + return graphics($) +} + +export default graphics(specifics) diff --git a/src/renderer/ol/style/point.js b/src/renderer/ol/style/point.js deleted file mode 100644 index 0c4372e2..00000000 --- a/src/renderer/ol/style/point.js +++ /dev/null @@ -1,40 +0,0 @@ -/* eslint-disable camelcase */ -import * as R from 'ramda' -import * as shared from './shared' -import { styleFactory } from './styleFactory' -import { MODIFIERS } from '../../symbology/2525c' - -const symbolModifiers = properties => Object.entries(properties) - .filter(([key, value]) => MODIFIERS[key] && value) - .reduce((acc, [key, value]) => R.tap(acc => (acc[MODIFIERS[key]] = value), acc), {}) - -/** - * style - */ -const style = [next => { - const { mode, definingGeometry, sidc, modifiers, effectiveStyle } = next - - // symbol-text-color - const selected = mode === 'singleselect' - ? { 'symbol-halo-color': 'white', 'symbol-halo-width': 6, 'symbol-fill-opacity': 1 } - : {} - - const style = [{ - id: 'style:2525c/symbol', - geometry: definingGeometry, - 'symbol-code': sidc, - 'symbol-modifiers': symbolModifiers(modifiers), - ...selected - }] - .map(effectiveStyle) - .flatMap(styleFactory) - - return { style } -}, ['mode', 'sidc', 'modifiers', 'geometryKey', 'effectiveStyle']] - - -export default [ - shared.sidc, - shared.effectiveStyle, - style -] diff --git a/src/renderer/ol/style/polygon-styles/labels.js b/src/renderer/ol/style/polygon-styles/labels.js index b397b544..c0e6b31a 100644 --- a/src/renderer/ol/style/polygon-styles/labels.js +++ b/src/renderer/ol/style/polygon-styles/labels.js @@ -18,7 +18,7 @@ const G_G_PM = [ { 'symbol-code': 'GFGPPD----', 'symbol-anchor': 'center', 'symbol-size': 100 } ] -export const labels = {} +const labels = {} labels['G*G*GAG---'] = C(ALL_LINES()) // GENERAL AREA labels['G*G*GAA---'] = C(ALL_LINES('AA')) // ASSEMBLY AREA labels['G*G*GAE---'] = C(ALL_LINES('EA')) // ENGAGEMENT AREA @@ -109,3 +109,5 @@ labels['G*S*ASR---'] = C(ALL_LINES('RSA')) // SUPPORT AREAS / REGIMENTAL (DSA) labels['G*M*NR----'] = [{ 'symbol-code': 'GFMPNZ----', 'symbol-anchor': 'center' }] // RADIOACTIVE AREA labels['G*M*NB----'] = [{ 'symbol-code': 'GFMPNEB---', 'symbol-anchor': 'center' }] // BIOLOGICALLY CONTAMINATED AREA labels['G*M*NC----'] = [{ 'symbol-code': 'GFMPNEC---', 'symbol-anchor': 'center' }] // CHEMICALLY CONTAMINATED AREA + +export default labels diff --git a/src/renderer/ol/style/polygon-placement.js b/src/renderer/ol/style/polygon-styles/placement.js similarity index 73% rename from src/renderer/ol/style/polygon-placement.js rename to src/renderer/ol/style/polygon-styles/placement.js index f8b5a449..de1c6b94 100644 --- a/src/renderer/ol/style/polygon-placement.js +++ b/src/renderer/ol/style/polygon-styles/placement.js @@ -1,3 +1,5 @@ +import * as R from 'ramda' +import * as TS from '../../ts' const lazy = function (fn) { let evaluated = false @@ -11,7 +13,10 @@ const lazy = function (fn) { } } -export const placement = ({ TS, geometry }) => { +/** + * placement :: jts/geom/Geometry => Style => Style + */ +const placement = geometry => { const ring = geometry.getExteriorRing() const envelope = ring.getEnvelopeInternal() const centroid = TS.centroid(ring) @@ -45,12 +50,22 @@ export const placement = ({ TS, geometry }) => { right: lazy(() => TS.point(xIntersection()[1])) } - return props => { - const anchor = props['text-anchor'] + const tryer = properties => { + const anchor = properties['text-anchor'] const geometry = Number.isFinite(anchor) ? fraction(anchor) : anchors[anchor || 'center']() + return { geometry, ...properties } + } + + const catcher = (err, properties) => console.warn(err, properties) - return { geometry, ...props } + const calculate = arg => { + if (!Array.isArray(arg)) return calculate([arg]) + else return arg.map(R.tryCatch(tryer, catcher)).filter(Boolean) } + + return calculate } + +export default placement diff --git a/src/renderer/ol/style/polygon.js b/src/renderer/ol/style/polygon.js index 632ca65d..3ac15426 100644 --- a/src/renderer/ol/style/polygon.js +++ b/src/renderer/ol/style/polygon.js @@ -1,45 +1,32 @@ -/* eslint-disable camelcase */ -import * as shared from './shared' -import styles from './polygon-styles' -import { placement } from './polygon-placement' -import { labels } from './polygon-styles/labels' +import Signal from '@syncpoint/signal' +import labels from './polygon-styles/labels' +import styles from './polygon-styles/index' +import placement from './polygon-styles/placement' +import graphics from './graphics' +import keyequals from './keyequals' -/** - * dynamicStyle - * staticStyles - */ -const collectStyles = [next => { - const { parameterizedSIDC: sidc } = next - const dynamicStyle = (styles[sidc] || styles.DEFAULT) - const staticStyles = (labels[sidc] || []) - return { dynamicStyle, staticStyles } -}, ['parameterizedSIDC']] +import _smoothenedGeometry from './_smoothenedGeometry' +import _simplifiedGeometry from './_simplifiedGeometry' +import _context from './_context' +import _labels from './_labels' +import _shape from './_shape' +import _lineSmoothing from './_lineSmoothing' +import _selection from './_selection' +const specifics = $ => { + $.simplifiedGeometry = Signal.link(_simplifiedGeometry, [$.geometry, $.centerResolution], { equals: keyequals() }) + $.jtsSimplifiedGeometry = $.simplifiedGeometry.ap($.read) + $.lineSmoothing = $.effectiveStyle.map(_lineSmoothing) + $.smoothenedGeometry = Signal.link(_smoothenedGeometry, [$.simplifiedGeometry, $.lineSmoothing]) + $.jtsSmoothenedGeometry = $.smoothenedGeometry.ap($.read) + $.context = Signal.link(_context, [$.jtsSmoothenedGeometry, $.resolution]) + $.placement = $.jtsSmoothenedGeometry.map(placement) -/** - * placement - */ -const labelPlacement = [next => { - return { placement: placement(next) } -}, ['geometry']] + $.shape = $.context.ap($.parameterizedSIDC.map(_shape(styles))) + $.selection = Signal.link(_selection, [$.selectionMode, $.jtsSimplifiedGeometry]) + $.labels = $.parameterizedSIDC + .map(_labels(labels)) + .ap($.placement) +} - -/** - * style :: [ol/style/Style] - */ -const error = [next => { - return { styles: styles.ERROR(next) } -}, ['err']] - -export default [ - shared.sidc, - shared.evalSync, - collectStyles, - shared.effectiveStyle, - shared.geometry, - labelPlacement, - shared.selectedStyles, - shared.styles, - error, - shared.style -] +export default graphics(specifics) diff --git a/src/renderer/ol/style/rules.js b/src/renderer/ol/style/rules.js deleted file mode 100644 index 3bfc372f..00000000 --- a/src/renderer/ol/style/rules.js +++ /dev/null @@ -1,56 +0,0 @@ -import * as R from 'ramda' -import isEqual from 'react-fast-compare' -import Point from './point' -import LineString from './linestring' -import Polygon from './polygon' -import Corridor from './corridor' -import MultiPoint from './multipoint' - -export const rules = { - Point, - LineString, - Polygon, - 'LineString:Point': Corridor, - MultiPoint -} - -const notEqual = (state, obj, key) => !isEqual(state[key], obj[key]) - -const comparators = { - globalStyle: notEqual, - layerStyle: notEqual, - featureStyle: notEqual, - properties: notEqual, - modifiers: notEqual -} - -const fn = rule => rule[0] -const deps = rule => rule[1] - - -/** - * - */ -export const reduce = (state, facts, rank = 0) => { - const next = { ...state, ...facts } - const different = key => comparators[key] - ? comparators[key](state, facts, key) - : state[key] !== facts[key] - - if (rank >= state.rules.length) return next - const changed = Object.keys(facts).filter(different) - if (changed.length === 0) return state - - const isStale = rule => deps(rule).some(key => changed.includes(key)) - const isFulfilled = rule => deps(rule).every(key => !R.isNil(next[key])) - const head = state.rules[rank] - const catcher = (err, next) => { - console.error(err) - return reduce(state, { ...next, err }, ++rank) - } - const tryer = next => isStale(head) && isFulfilled(head) - ? reduce(state, { ...next, ...fn(head)(next) }, ++rank) - : reduce(state, next, ++rank) - - return R.tryCatch(tryer, catcher)(next) -} diff --git a/src/renderer/ol/style/shared.js b/src/renderer/ol/style/shared.js deleted file mode 100644 index 72c66f0b..00000000 --- a/src/renderer/ol/style/shared.js +++ /dev/null @@ -1,188 +0,0 @@ -import * as Colors from './color-schemes' -import { echelonCode, identityCode, statusCode, parameterized } from '../../symbology/2525c' -import { smooth } from './chaikin' -import { transform } from '../../model/geometry' -import { styleFactory } from './styleFactory' -import echelon, { echelons } from './echelon' -import * as Labels from './labels' -import styleRegistry from './styleRegistry' - -const rules = [] -export default rules - - -/** - * sidc :: String - * parameterizedSIDC :: String - * modifiers :: [String] - */ -export const sidc = [next => { - const { properties } = next - const { sidc, ...modifiers } = properties - const parameterizedSIDC = parameterized(sidc) - return { sidc, parameterizedSIDC, modifiers } -}, ['properties']] - - -export const evalSync = [next => { - const { modifiers, sidc } = next - const sizeCode = echelonCode(sidc) - const echelonText = (sizeCode === '*' || sizeCode === '-') ? '' : echelons[sizeCode]?.text - return { evalSync: Labels.evalSync({ modifiers, echelon: echelonText }) } -}, ['modifiers', 'sidc']] - - -/** - * effectiveStyle :: ... - * smoothen :: boolean - */ -export const effectiveStyle = [next => { - const global = next.globalStyle || {} - const layer = next.layerStyle - const feature = next.featureStyle || {} - const { sidc } = next - const status = statusCode(sidc) - const identity = identityCode(sidc) - const simpleIdentity = identity === 'H' || identity === 'S' - ? 'H' - : '-' - - const colorScheme = feature?.['color-scheme'] || - layer?.['color-scheme'] || - global?.['color-scheme'] || - 'medium' - - const scheme = { - 'binary-color': Colors.lineColor(colorScheme)(simpleIdentity), // black or red - 'line-color': Colors.lineColor(colorScheme)(identity), - 'fill-color': Colors.lineColor(colorScheme)(identity), - 'line-dash-array': status === 'A' ? [20, 10] : null, - 'line-halo-color': Colors.lineHaloColor(identity), - 'line-halo-dash-array': status === 'A' ? [20, 10] : null - } - - if (!layer['line-color']) delete layer['line-color'] - if (!layer['line-halo-color']) delete layer['line-halo-color'] - - // Split `smoothen` from rest. - // We don't want to calculate new geometries on color change. - const merged = { ...global, ...scheme, ...layer, ...feature } - const { 'line-smooth': smoothen, ...props } = merged - - return { - smoothen: !!smoothen, - effectiveStyle: styleRegistry(next, props) - } -}, ['sidc', 'globalStyle', 'layerStyle', 'featureStyle']] - - -/** - * geometry :: jsts/geom/geometry - * rewrite :: ... - * resolution :: Number - */ -export const geometry = [next => { - const { smoothen, definingGeometry, centerResolution, geometryType } = next - - // Simplify. - // Never simplify current selection. - const coordinates = definingGeometry.getCoordinates() - const simplified = - (geometryType === 'Polygon' && coordinates[0].length > 50) || - (geometryType === 'LineString' && coordinates.length > 50) - - const simplifiedGeometry = simplified - ? definingGeometry.simplify(centerResolution) - : definingGeometry - - // Smoothen. - // - const smoothenedGeometry = smoothen - ? smooth(simplifiedGeometry) - : simplifiedGeometry - - // Transform (TS/UTM). - // - const { read, write, pointResolution } = transform(smoothenedGeometry) - const geometry = read(smoothenedGeometry) - const resolution = pointResolution(centerResolution) - const rewrite = ({ geometry, ...props }) => ({ geometry: write(geometry), ...props }) - - return { - simplified, - geometry, - simplifiedGeometry: read(simplifiedGeometry), - rewrite, - resolution - } -}, ['mode', 'smoothen', 'geometryKey', 'centerResolution']] - - -/** - * styles :: ... - */ -export const styles = [next => { - const { dynamicStyle, staticStyles, evalSync, placement } = next - const styles = [ - ...dynamicStyle(next), - ...staticStyles - ] - .flatMap(evalSync) - .flatMap(placement) - - return { styles } -}, ['dynamicStyle', 'staticStyles', 'evalSync', 'placement']] - - -/** - * - */ -export const selectedStyles = [next => { - const { TS, mode, simplifiedGeometry, geometryType } = next - const selectedStyles = [] - - const guideline = mode === 'singleselect' - ? { id: 'style:guide-stroke', geometry: simplifiedGeometry } - : null - - const points = () => TS.points(simplifiedGeometry) - const handles = geometryType !== 'point' && mode !== 'default' - ? mode === 'singleselect' - ? { id: 'style:circle-handle', geometry: TS.multiPoint(points()) } - : { id: 'style:rectangle-handle', geometry: points()[0] } - : null - - guideline && selectedStyles.push(guideline) - handles && selectedStyles.push(handles) - - return { selectedStyles } -}, ['mode', 'simplifiedGeometry']] - - -/** - * style :: [ol/style/Style] - */ -export const style = [next => { - const { TS, styles, selectedStyles, effectiveStyle, rewrite } = next - if (styles.length === 0) return { style: [] } - const effectiveStyles = [...styles, ...selectedStyles] - .map(echelon(next)) - .map(effectiveStyle) - - const bboxes = effectiveStyles.map(Labels.boundingBox(next)).filter(Boolean) - const clipLine = effectiveStyles.some(props => props['text-clipping'] === 'line') - const lineString = geometry => TS.lineString(geometry.getCoordinates()) - const clip = geometry => TS.difference([geometry, ...bboxes]) - const geometry = clipLine - ? lineString(effectiveStyles[0].geometry) - : effectiveStyles[0].geometry - - // Replace primary geometry with clipped geometry: - effectiveStyles[0].geometry = clip(geometry) - - const style = effectiveStyles - .map(rewrite) - .flatMap(styleFactory) - - return { style } -}, ['styles', 'effectiveStyle', 'selectedStyles']] diff --git a/src/renderer/ol/style/styleRegistry.js b/src/renderer/ol/style/styleRegistry.js index 9d61bd60..ee087ce5 100644 --- a/src/renderer/ol/style/styleRegistry.js +++ b/src/renderer/ol/style/styleRegistry.js @@ -1,3 +1,5 @@ +import { PI_OVER_4 } from '../../../shared/Math' + const COLOR_WHITE = 'white' const COLOR_BLACK = 'black' const COLOR_YELLOW = 'yellow' @@ -6,85 +8,88 @@ const DASH_ARRAY_10_10 = [10, 10] const DASH_ARRAY_14_6 = [14, 6] const DASH_ARRAY_20_8_2_8 = [20, 8, 2, 8] -export default ({ PI_OVER_4 }, props) => { - - const font = props['text-font'] || [ - props['text-font-style'], - props['text-font-variant'], - props['text-font-weight'], - props['text-font-size'], - props['text-font-family'] +/** + * Registry of predefined styles. + */ +export default (options) => { + + const font = options['text-font'] || [ + options['text-font-style'], + options['text-font-variant'], + options['text-font-weight'], + options['text-font-size'], + options['text-font-family'] ].filter(Boolean).join(' ') const registry = {} registry['style:2525c/symbol'] = { - 'color-scheme': props['color-scheme'], - 'symbol-color': props['symbol-color'], - 'symbol-halo-color': props['symbol-halo-color'], - 'symbol-halo-width': props['symbol-halo-width'], - 'symbol-text-color': props['symbol-text-color'], - 'symbol-text-halo-color': props['symbol-text-halo-color'], - 'symbol-text-halo-width': props['symbol-text-halo-width'], - 'symbol-text-size': props['symbol-text-size'], - 'symbol-text': props['symbol-text'], - 'symbol-fill': props['symbol-fill'], - 'symbol-fill-opacity': props['symbol-fill-opacity'], - 'symbol-frame': props['symbol-frame'], - 'symbol-icon': props['symbol-icon'], - 'symbol-line-width': props['symbol-line-width'], - 'symbol-size': props['symbol-size'], - 'icon-scale': props['icon-scale'] + 'color-scheme': options['color-scheme'], + 'symbol-color': options['symbol-color'], + 'symbol-halo-color': options['symbol-halo-color'], + 'symbol-halo-width': options['symbol-halo-width'], + 'symbol-text-color': options['symbol-text-color'], + 'symbol-text-halo-color': options['symbol-text-halo-color'], + 'symbol-text-halo-width': options['symbol-text-halo-width'], + 'symbol-text-size': options['symbol-text-size'], + 'symbol-text': options['symbol-text'], + 'symbol-fill': options['symbol-fill'], + 'symbol-fill-opacity': options['symbol-fill-opacity'], + 'symbol-frame': options['symbol-frame'], + 'symbol-icon': options['symbol-icon'], + 'symbol-line-width': options['symbol-line-width'], + 'symbol-size': options['symbol-size'], + 'icon-scale': options['icon-scale'] } registry['style:2525c/default-stroke'] = { - 'line-cap': props['line-cap'], - 'line-join': props['line-join'], - 'line-color': props['line-color'], - 'line-width': props['line-width'], - 'line-dash-array': props['line-dash-array'], - 'line-halo-color': props['line-halo-color'], - 'line-halo-width': props['line-halo-width'], - 'line-halo-dash-array': props['line-halo-dash-array'] + 'line-cap': options['line-cap'], + 'line-join': options['line-join'], + 'line-color': options['line-color'], + 'line-width': options['line-width'], + 'line-dash-array': options['line-dash-array'], + 'line-halo-color': options['line-halo-color'], + 'line-halo-width': options['line-halo-width'], + 'line-halo-dash-array': options['line-halo-dash-array'] } registry['style:2525c/solid-stroke'] = { - 'line-cap': props['line-cap'], - 'line-join': props['line-join'], - 'line-color': props['line-color'], - 'line-width': props['line-width'], - 'line-halo-color': props['line-halo-color'], - 'line-halo-width': props['line-halo-width'] + 'line-cap': options['line-cap'], + 'line-join': options['line-join'], + 'line-color': options['line-color'], + 'line-width': options['line-width'], + 'line-halo-color': options['line-halo-color'], + 'line-halo-width': options['line-halo-width'] } registry['style:2525c/dashed-stroke'] = { - 'line-cap': props['line-cap'], - 'line-join': props['line-join'], - 'line-color': props['line-color'], - 'line-width': props['line-width'], + 'line-cap': options['line-cap'], + 'line-join': options['line-join'], + 'line-color': options['line-color'], + 'line-width': options['line-width'], 'line-dash-array': DASH_ARRAY_14_6, - 'line-halo-color': props['line-halo-color'], - 'line-halo-width': props['line-halo-width'], + 'line-halo-color': options['line-halo-color'], + 'line-halo-width': options['line-halo-width'], 'line-halo-dash-array': DASH_ARRAY_14_6 } registry['style:2525c/solid-fill'] = { - 'line-cap': props['line-cap'], - 'line-join': props['line-join'], - 'line-color': props['line-color'], - 'line-width': props['line-width'], - 'line-halo-color': props['line-halo-color'], - 'line-halo-width': props['line-halo-width'], - 'fill-color': props['fill-color'] + 'line-cap': options['line-cap'], + 'line-join': options['line-join'], + 'line-color': options['line-color'], + 'line-width': options['line-width'], + 'line-halo-color': options['line-halo-color'], + 'line-halo-width': options['line-halo-width'], + 'fill-color': options['fill-color'] } registry['style:2525c/hatch-fill'] = { - 'line-cap': props['line-cap'], - 'line-join': props['line-join'], - 'line-color': props['line-color'], - 'line-width': props['line-width'], - 'line-halo-color': props['line-halo-color'], - 'line-halo-width': props['line-halo-width'], + 'line-cap': options['line-cap'], + 'line-join': options['line-join'], + 'line-color': options['line-color'], + 'line-width': options['line-width'], + 'line-halo-color': options['line-halo-color'], + 'line-halo-width': options['line-halo-width'], 'fill-pattern': 'hatch', 'fill-pattern-angle': 45, 'fill-pattern-size': 2, @@ -93,24 +98,24 @@ export default ({ PI_OVER_4 }, props) => { registry['style:default-text'] = { 'text-font': font, - 'text-color': props['text-color'], - 'text-fill-color': props['text-fill-color'], - 'text-line-color': props['text-line-color'], - 'text-line-width': props['text-line-width'], - 'text-halo-color': props['text-halo-color'], - 'text-halo-width': props['text-halo-width'], + 'text-color': options['text-color'], + 'text-fill-color': options['text-fill-color'], + 'text-line-color': options['text-line-color'], + 'text-line-width': options['text-line-width'], + 'text-halo-color': options['text-halo-color'], + 'text-halo-width': options['text-halo-width'], 'text-justify': 'center', 'text-rotation-anchor': 'auto' } registry['style:2525c/fence-stroke'] = { 'line-cap': 'square', - 'line-color': props['binary-color'], + 'line-color': options['binary-color'], 'line-width': 2 } registry['style:2525c/fence-o'] = { - 'shape-line-color': props['binary-color'], + 'shape-line-color': options['binary-color'], 'shape-line-width': 2, 'shape-points': 8, 'shape-radius': 8, @@ -120,7 +125,7 @@ export default ({ PI_OVER_4 }, props) => { } registry['style:2525c/fence-x'] = { - 'shape-line-color': props['binary-color'], + 'shape-line-color': options['binary-color'], 'shape-line-width': 2, 'shape-points': 4, 'shape-radius': 8, @@ -131,10 +136,10 @@ export default ({ PI_OVER_4 }, props) => { registry['style:wasp-stroke'] = { 'line-color': COLOR_YELLOW, - 'line-width': props['line-width'], + 'line-width': options['line-width'], 'line-dash-array': DASH_ARRAY_10_10, 'line-halo-color': COLOR_BLACK, - 'line-halo-width': props['line-halo-width'], + 'line-halo-width': options['line-halo-width'], 'line-halo-dash-array': null } diff --git a/src/renderer/ol/style/styles.js b/src/renderer/ol/style/styles.js new file mode 100644 index 00000000..5c35d714 --- /dev/null +++ b/src/renderer/ol/style/styles.js @@ -0,0 +1,45 @@ +import * as R from 'ramda' +import { parameterized } from '../../symbology/2525c' +import Signal from '@syncpoint/signal' +import * as Geometry from '../../model/geometry' +import styleRegistry from './styleRegistry' +import symbol from './symbol' +import polygon from './polygon' +import linestring from './linestring' +import multipoint from './multipoint' +import corridor from './corridor' +import marker from './marker' +import measure from './measure' +import fallback from './fallback' +import { styleFactory } from './styleFactory' +import * as ID from '../../ids' + +import _colorScheme from './_colorScheme' +import _schemeStyle from './_schemeStyle' +import _effectiveStyle from './_effectiveStyle' + +export default feature => { + const { $ } = feature + + $.sidc = $.properties.map(R.prop('sidc')) + $.parameterizedSIDC = $.sidc.map(parameterized) + $.colorScheme = Signal.link(_colorScheme, [$.globalStyle, $.layerStyle, $.featureStyle]) + $.schemeStyle = Signal.link(_schemeStyle, [$.sidc, $.colorScheme]) + $.effectiveStyle = Signal.link(_effectiveStyle, [$.globalStyle, $.schemeStyle, $.layerStyle, $.featureStyle]) + $.styleRegistry = $.effectiveStyle + .map(styleRegistry) + .map(fn => xs => xs.map(fn)) + $.styleFactory = Signal.of(xs => xs.flatMap(styleFactory)) + + const featureId = feature.getId() + const geometryType = Geometry.geometryType(feature.getGeometry()) + + if (ID.isMarkerId(featureId)) return marker($) + else if (ID.isMeasureId(featureId)) return measure($) + else if (geometryType === 'Point') return symbol($) + else if (geometryType === 'Polygon') return polygon($) + else if (geometryType === 'LineString') return linestring($) + else if (geometryType === 'MultiPoint') return multipoint($) + else if (geometryType === 'LineString:Point') return corridor($) + else return fallback($) +} diff --git a/src/renderer/ol/style/symbol.js b/src/renderer/ol/style/symbol.js new file mode 100644 index 00000000..d8ac5fa2 --- /dev/null +++ b/src/renderer/ol/style/symbol.js @@ -0,0 +1,39 @@ +import * as R from 'ramda' +import Signal from '@syncpoint/signal' +import { MODIFIERS } from '../../symbology/2525c' + +/** + * + */ +export default $ => { + $.shape = $.properties.map(properties => { + const sidc = properties.sidc + const modifiers = Object.entries(properties) + .filter(([key, value]) => MODIFIERS[key] && value) + .reduce((acc, [key, value]) => R.tap(acc => (acc[MODIFIERS[key]] = value), acc), {}) + + return [{ + id: 'style:2525c/symbol', + 'symbol-code': sidc, + 'symbol-modifiers': modifiers + }] + }, []) + + $.selection = $.selectionMode.map(mode => + mode === 'multiselect' + ? [{ id: 'style:rectangle-handle' }] + : [] + ) + + $.styles = Signal.link( + (...styles) => styles.reduce(R.concat), + [ + $.shape, + $.selection + ] + ) + + return $.styles + .ap($.styleRegistry) + .ap($.styleFactory) +} diff --git a/src/renderer/store/FeatureStore.js b/src/renderer/store/FeatureStore.js index 8b1609b7..54a3eb91 100644 --- a/src/renderer/store/FeatureStore.js +++ b/src/renderer/store/FeatureStore.js @@ -1,213 +1,12 @@ /* eslint-disable camelcase */ -import * as R from 'ramda' -import util from 'util' -import GeoJSON from 'ol/format/GeoJSON' import * as Extent from 'ol/extent' -import Emitter from '../../shared/emitter' -import { debounce, batch } from '../../shared/debounce' -import { geometryType } from '../model/geometry' -import * as ID from '../ids' -import { reduce, rules } from '../ol/style/rules' -import crosshair from '../ol/style/crosshair' -import { stylist as measurementStyler } from '../ol/interaction/measure/style' -import * as TS from '../ol/ts' -import * as Math from '../../shared/Math' - -const format = new GeoJSON({ - dataProjection: 'EPSG:3857', - featureProjection: 'EPSG:3857' -}) - -/** - * Note: If source does not include a geometry, geometry of resulting feature is `null`. - */ -export const readFeature = source => format.readFeature(source) -export const readFeatures = source => format.readFeatures(source) -export const readGeometry = source => format.readGeometry(source) -export const writeGeometry = geometry => format.writeGeometry(geometry) -export const writeGeometryObject = geometry => format.writeGeometryObject(geometry) - -// writeFeatureCollection :: [ol/Feature] -> GeoJSON/FeatureCollection -export const writeFeatureCollection = features => format.writeFeaturesObject(features) -export const writeFeatureObject = feature => format.writeFeatureObject(feature) - +import { readFeature } from '../ol/format' /** * */ -export function FeatureStore (store, selection) { +export function FeatureStore (store) { this.store = store - this.selection = selection - this.features = {} - this.styleProps = {} - - const debouncedHandler = batch(debounce(32), this.batch.bind(this)) - store.on('batch', ({ operations }) => debouncedHandler(operations)) - - selection.on('selection', ({ deselected, selected }) => { - deselected.forEach(key => { - if (this.features[key]) this.features[key].apply({ mode: 'default' }) - }) - - const mode = selection.selected().length > 1 - ? 'multiselect' - : 'singleselect' - - selected.forEach(key => { - if (this.features[key]) this.features[key].apply({ mode }) - }) - }) -} - -util.inherits(FeatureStore, Emitter) - - -/** - * - */ -FeatureStore.prototype.bootstrap = async function () { - // On startup: load all features: - // - this.styleProps = Object.fromEntries(await this.store.tuples('style+')) - await this.loadFeatures(ID.FEATURE_SCOPE) - await this.loadFeatures(ID.MARKER_SCOPE) - await this.loadFeatures(ID.MEASURE_SCOPE) -} - -/** - * - */ -FeatureStore.prototype.loadFeatures = async function (scope) { - const tuples = await this.store.tuples(scope) - const geoJSON = tuples.map(([id, feature]) => ({ id, ...feature })) - const isValid = feature => feature?.type === 'Feature' && feature.geometry - const [valid, invalid] = R.partition(isValid, geoJSON) - if (invalid.length) console.warn('invalid features', invalid) - this.addFeatures(readFeatures({ type: 'FeatureCollection', features: valid })) -} - - -/** - * - */ -FeatureStore.prototype.batch = function (operations) { - const features = Object.values(this.features) - const apply = obj => feature => feature.apply(obj, true) - - operations - .filter(({ key }) => key === ID.defaultStyleId) - .forEach(({ value }) => features.forEach(apply({ globalStyle: value }))) - - operations - .filter(({ key }) => ID.isFeatureStyleId(key)) - .forEach(({ key, value }) => apply({ featureStyle: value })(this.features[ID.featureId(key)])) - - // Apply new/updated layer styles: - // - operations - .filter(({ type }) => type === 'put') - .filter(({ key }) => ID.isLayerStyleId(key)) - .forEach(({ key, value }) => { - this.styleProps[key] = value - const layerId = ID.layerId(key) - Object - .keys(this.features) - .filter(key => ID.layerId(key) === layerId) - .forEach(key => apply({ layerStyle: value })(this.features[key])) - }) - - // Remove deleted layer styles: - // - operations - .filter(({ type }) => type === 'del') - .filter(({ key }) => ID.isLayerStyleId(key)) - .forEach(({ key }) => { - delete this.styleProps[key] - const layerId = ID.layerId(key) - Object - .keys(this.features) - .filter(key => ID.layerId(key) === layerId) - .forEach(key => apply({ layerStyle: {} })(this.features[key])) - }) - - const isCandidateId = id => ID.isFeatureId(id) || ID.isMarkerId(id) || ID.isMeasureId(id) - const candidates = operations.filter(({ key }) => isCandidateId(key)) - const [removals, other] = R.partition(({ type }) => type === 'del', candidates) - const [updates, additions] = R.partition(({ key }) => this.features[key], other) - this.handleRemovals(removals) - this.handleAdditions(additions) - this.handleUpdates(updates) -} - - -/** - * - */ -FeatureStore.prototype.handleAdditions = function (additions) { - if (additions.length === 0) return - const isValid = feature => feature?.type === 'Feature' && feature.geometry - const features = additions - .filter(({ value }) => isValid(value)) - .map(({ key, value }) => readFeature({ id: key, ...value })) - - this.addFeatures(features) -} - - -/** - * - */ -FeatureStore.prototype.handleRemovals = function (removals) { - if (removals.length === 0) return - const features = removals.map(({ key }) => this.features[key]) - removals.forEach(({ key }) => delete this.features[key]) - this.emit('removefeatures', ({ features })) -} - - -const isGeometry = value => { - if (!value) return false - else if (typeof value !== 'object') return false - else { - if (!value.type) return false - else if (!value.coordinates && !value.geometries) return false - return true - } -} - - -/** - * - */ -FeatureStore.prototype.handleUpdates = function (updates) { - - // We don't want null geometry overwrite existing one - // in case only feature properties were updated (JSON). - // - const trim = properties => { - const { geometry, ...rest } = properties - return geometry - ? properties - : rest - } - - updates.forEach(({ key, value }) => { - const properties = isGeometry(value) - ? { geometry: readGeometry(value) } - : trim(readFeature(value).getProperties()) - - const feature = this.features[key] - feature.setProperties({ ...feature.getProperties(), ...properties }) - }) -} - -/** - * - */ -FeatureStore.prototype.addFeatures = function (features) { - const wrap = this.wrap.bind(this) - features.map(wrap).forEach(feature => (this.features[feature.getId()] = feature)) - this.emit('addfeatures', ({ features })) } @@ -221,99 +20,3 @@ FeatureStore.prototype.center = async function (key) { const extent = feature.getGeometry()?.getExtent() return Extent.getCenter(extent) } - - -/** - * - */ -FeatureStore.prototype.feature = function (key) { - return this.features[key] -} - - -/** - * - */ -FeatureStore.prototype.wrap = function (feature) { - const id = feature.getId() - if (ID.isFeatureId(id)) return this.wrapFeature(feature) - else if (ID.isMarkerId(id)) return this.wrapMarker(feature) - else if (ID.isMeasureId(id)) return this.wrapMeasurement(feature) - else return feature -} - - -/** - * - */ -FeatureStore.prototype.wrapFeature = function (feature) { - const type = geometryType(feature.getGeometry()) - if (!rules[type]) console.warn('[style] unsupported geometry', type) - - let state = { - TS, - ...Math, - mode: 'default', - rules: rules[type] || [], - layerStyle: {}, - featureStyle: {} - } - - const featureId = feature.getId() - const layerId = ID.layerId(featureId) - const set = key => props => (state[key] = props) - R.when(Boolean, set('globalStyle'))(this.styleProps[ID.defaultStyleId]) - R.when(Boolean, set('layerStyle'))(this.styleProps['style+' + layerId]) - R.when(Boolean, set('featureStyle'))(this.styleProps['style+' + featureId]) - - feature.setStyle((feature, resolution) => { - const { geometry: definingGeometry, ...properties } = feature.getProperties() - state = reduce(state, { - definingGeometry, - properties, - centerResolution: resolution, - geometryKey: `${definingGeometry.ol_uid}:${definingGeometry.getRevision()}`, - geometryType: type - }) - - return state.style - }) - - feature.apply = (obj, forceUpdate) => { - state = reduce(state, obj) - if (forceUpdate) feature.changed() - } - - return feature -} - - -/** - * - */ -FeatureStore.prototype.wrapMarker = function (feature) { - const defaultStyle = crosshair('black') - const selectedStyle = crosshair('red') - - feature.apply = () => {} - feature.setStyle(feature => { - return this.selection.isSelected(feature.getId()) - ? selectedStyle - : defaultStyle - }) - - return feature -} - - -/** - * - */ -FeatureStore.prototype.wrapMeasurement = function (feature) { - const isSelected = feature => this.selection.isSelected(feature.getId()) - - feature.apply = () => {} - feature.setStyle(measurementStyler(isSelected)) - - return feature -} diff --git a/src/renderer/store/SpatialIndex.js b/src/renderer/store/SpatialIndex.js index 75d937bf..37c90fbe 100644 --- a/src/renderer/store/SpatialIndex.js +++ b/src/renderer/store/SpatialIndex.js @@ -4,18 +4,15 @@ import * as L from '../../shared/level' import { bbox } from './geometry' import * as TS from '../ol/ts' +/** + * Spatial index for point geometries only. + */ export function SpatialIndex (wkbDB) { this.wkbDB = wkbDB this.tree = new RBush() this.geoJSONReader = new TS.GeoJSONReader() wkbDB.on('batch', this.update.bind(this)) - // wkbDB.on('put', (key, value) => console.log('[SpatialIndex/put]', key, value)) - // wkbDB.on('del', key => console.log('[SpatialIndex/del]', key)) - - // Import symbols once for each fresh project database. - window.requestIdleCallback(async () => { - }, { timeout: 2000 }) } diff --git a/src/renderer/store/Store.js b/src/renderer/store/Store.js index cf39dd83..8a48352b 100644 --- a/src/renderer/store/Store.js +++ b/src/renderer/store/Store.js @@ -6,7 +6,7 @@ import * as L from '../../shared/level' import { PartitionDOWN } from '../../shared/level/PartitionDOWN' import * as TS from '../ol/ts' import { transform, geometryType } from '../model/geometry' -import { readGeometry } from '../store/FeatureStore' +import { readGeometry } from '../ol/format' import { bbox } from './geometry' /** diff --git a/src/renderer/store/TileLayerStore.js b/src/renderer/store/TileLayerStore.js index 0762bc19..3aa99be2 100644 --- a/src/renderer/store/TileLayerStore.js +++ b/src/renderer/store/TileLayerStore.js @@ -1,5 +1,5 @@ import * as R from 'ramda' -import { Tile as TileLayer } from 'ol/layer' +import WebGLTileLayer from 'ol/layer/WebGLTile.js' import Collection from 'ol/Collection' import LayerGroup from 'ol/layer/Group' import * as ID from '../ids' @@ -21,7 +21,8 @@ const fetchCapabilities = async service => { } catch (err) { return TileService.adapters.XYZ({ url: service.url, - maxZoom: service.capabilities?.maxZoom || 24 + maxZoom: service.capabilities?.maxZoom || 24, + contentType: service.capabilities?.contentType }) } } @@ -40,7 +41,17 @@ TileLayerStore.tileLayer = function (services) { const { type, capabilities } = services[ID.tileServiceId(id)] const adapter = TileService.adapters[type](capabilities) const source = adapter.source(ID.containedId(id)) - return new TileLayer({ source, id, opacity, visible, zIndex: 0 - index }) + const layerProps = { + source, + id, + opacity: capabilities?.contentType?.includes('terrain') ? 0 : opacity, + visible: capabilities?.contentType?.includes('terrain') ? true : visible, + zIndex: 0 - index, + contentType: capabilities?.contentType + } + + const layer = new WebGLTileLayer(layerProps) + return layer } } @@ -112,7 +123,6 @@ TileLayerStore.prototype.updateService = async function (key, service) { const name = service.name || title return { ...service, type, name, capabilities } } - const newValue = await capabilities(service) this.store.update([key], [newValue], [service]) } @@ -216,15 +226,26 @@ TileLayerStore.prototype.updatePreset = async function () { : adapters[key].layerName(ID.containedId(id)) } + const contentType = id => { + // eslint-disable-next-line no-unused-vars + const [key, service] = findService(ID.tileServiceId(id)) + return ['XYZ'].includes(service.type) + ? service.capabilities.contentType + : undefined + } + const layer = id => ({ id, opacity: 1.0, visible: false }) const additions = activeLayerIds.filter(x => !currentLayers.includes(x)).map(layer) // Propagate name changes from service to preset (only for OSM, XYZ.) - // const propagateName = layer => ({ ...layer, name: layerName(layer.id) }) + // Propagate content type (either undefined or terrain/mapbox-rgb) + const propagateContentType = layer => ({ ...layer, contentType: contentType(layer.id) }) + const preset = (currentPreset.concat(additions)) .filter(layer => !removals.includes(layer.id)) .map(propagateName) + .map(propagateContentType) this.store.update([ID.tilePresetId()], [preset]) } @@ -237,11 +258,12 @@ TileLayerStore.prototype.updateLayers = async function (preset) { const currentLayers = this.layerCollection.getArray() const findLayer = id => currentLayers.find(layer => layer.get('id') === id) const services = Object.fromEntries(await this.store.tuples(ID.TILE_SERVICE_SCOPE)) - + const updateLayer = (layer, properties, index) => { - layer.setOpacity(properties.opacity) - layer.setVisible(properties.visible) + layer.setOpacity(properties.contentType ? 0 : properties.opacity) + layer.setVisible(properties.contentType ? 1 : properties.visible) layer.setZIndex(0 - index) + layer.set('contentType', properties.contentType) return layer } diff --git a/src/renderer/store/documents/tile-service.js b/src/renderer/store/documents/tile-service.js index eff280f0..d473b7c1 100644 --- a/src/renderer/store/documents/tile-service.js +++ b/src/renderer/store/documents/tile-service.js @@ -5,10 +5,12 @@ export default async function (id) { const keys = [R.identity, ID.tagsId] const [service, tags] = await this.store.collect(id, keys) + if (!service) return null + const document = { id, scope: ID.TILE_SERVICE, - text: service.name, + text: service.name || '', tags: [...(tags || []), service.type] } diff --git a/src/renderer/store/options/feature.js b/src/renderer/store/options/feature.js index 0503c5da..f7f97c2c 100644 --- a/src/renderer/store/options/feature.js +++ b/src/renderer/store/options/feature.js @@ -32,18 +32,11 @@ export default async function (id) { ? `SYSTEM:${geometryType.toLowerCase()}` : `SYSTEM:${geometryType.toLowerCase()}:NONE` - let icon - try { - icon = svg(sidc) - } catch (err) { - console.error(err) - } - return { id, title: feature.name || properties.t || null, // might be undefined description, - svg: icon, + svg: svg(sidc), tags: [ 'SCOPE:FEATURE', hidden ? 'SYSTEM:HIDDEN::mdiEyeOff' : 'SYSTEM:VISIBLE::mdiEyeOutline', diff --git a/src/renderer/store/options/place.js b/src/renderer/store/options/place.js index b84bf077..d8339b78 100644 --- a/src/renderer/store/options/place.js +++ b/src/renderer/store/options/place.js @@ -3,7 +3,7 @@ import * as Extent from 'ol/extent' import * as Sphere from 'ol/sphere' import { toLonLat } from 'ol/proj' import * as ID from '../../ids' -import { readGeometry } from '../../store/FeatureStore' +import { readGeometry } from '../../ol/format' export default async function (id) { const keys = [R.identity, ID.tagsId] diff --git a/src/renderer/store/options/tile-service.js b/src/renderer/store/options/tile-service.js index 123e2c04..aea07917 100644 --- a/src/renderer/store/options/tile-service.js +++ b/src/renderer/store/options/tile-service.js @@ -11,9 +11,12 @@ export default async function (id) { scope: service.type, tags: [ `SCOPE:${service.type}:NONE`, - ...((tags || [])).map(label => `USER:${label}:NONE`), + (tags || []).some(t => t === 'TERRAIN') ? 'SYSTEM:TERRAIN::mdiTerrain' : null, + ...((tags || [])) + .filter(Boolean) + .filter(t => t !== 'TERRAIN').map(label => `USER:${label}:NONE`), 'PLUS' - ].join(' '), + ].filter(Boolean).join(' '), capabilities: 'TAG|RENAME' } diff --git a/src/renderer/symbology/2525c.js b/src/renderer/symbology/2525c.js index 0f990844..93eb22d0 100644 --- a/src/renderer/symbology/2525c.js +++ b/src/renderer/symbology/2525c.js @@ -177,5 +177,5 @@ export const specialization = sidc => { else if (geometry && geometry.layout === 'rectangle') return 'RECTANGLE' else if (geometry && geometry.layout === 'circle') return 'CIRCLE' else if (geometry && geometry.layout === 'corridor') return 'CORRIDOR' - else return null + else return } diff --git a/src/shared/signal.js b/src/shared/signal.js new file mode 100644 index 00000000..d7ca5177 --- /dev/null +++ b/src/shared/signal.js @@ -0,0 +1,71 @@ +import * as R from 'ramda' +import Signal from '@syncpoint/signal' + +/** + * select :: Signal S => [a -> Boolean] -> S a -> [S a] + * + * Split one input signal into multiply output signals based on predicates. + * Each input value is either forwarded to one output signal or dropped if + * no condition matches. + */ +export const select = R.curry((predicates, signal) => { + const outputs = predicates.map(() => Signal.of()) + signal.on(value => { + const match = condition => condition(value) + outputs[predicates.findIndex(match)]?.(value) + }) + return outputs +}) + +export const flat = signal => { + const output = Signal.of() + signal.on(v => (Array.isArray(v) ? v : [v]).forEach(output)) + return output +} + +/** + * split :: Signal S => [a -> b] -> S a -> [S b] + * + * Split one input signal into multiple output signals based on mapping + * functions. Input values are mapped and forwarded to the function's + * respective output signal. + */ +export const split = R.curry((fns, signal) => { + const outputs = fns.map(() => Signal.of()) + signal.on(value => fns.forEach((fn, i) => outputs[i](fn(value)))) + return outputs +}) + +/** + * destructure :: Signal S => [String] -> S { k: v } -> [S Any] + * + * Split input object signal into ordered list of value signals + * based on entry keys. + */ +export const destructure = R.curry((keys, signal) => { + const outputs = keys.map(() => Signal.of()) + signal.on(object => keys.forEach((key, i) => outputs[i](object[key]))) + return outputs +}) + +export const once = (fn, signal) => { + const dispose = signal.on(value => { + setImmediate(() => dispose()) + fn(value) + }) +} + +export const circuitBreaker = (input) => { + let last = 0 + let count = 0 + + return input.map(x => { + if ((Date.now() - last) < 10) count++ + last = Date.now() + + if (count > 10) { + console.warn('frequency too high', x) + return undefined + } else return x + }) +} diff --git a/webpack.config.js b/webpack.config.js index 8091e387..a88236fa 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -47,6 +47,23 @@ const RULES = { font: { test: /\.(eot|svg|ttf|woff|woff2)$/, type: 'asset/resource' + }, + + sourcemap: { + test: /\.js$/, + enforce: "pre", + use: [ + { + loader: "source-map-loader", + options: { + filterSourceMappingUrl: (url, resourcePath) => { + // Consume own (@syncpoint) sourcemaps; remove others. + if (/@syncpoint/g.test(resourcePath)) return 'consume' + else return 'remove' + } + } + } + ] } } @@ -123,10 +140,8 @@ const devServer = env => { } const devtool = env => { - if (env.production) return ({}) // no source maps for production - return ({ - devtool: 'cheap-source-map' - }) + if (env.production) return ({ devtool: 'source-map' }) + else return ({ devtool: 'eval-source-map' }) } module.exports = (env, argv) => {