diff --git a/.eslintrc b/.eslintrc index a4ad190b..70133296 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,13 +1,5 @@ { - "extends": [ - "mourner", - "plugin:import/recommended" - ], - "parser": "espree", - "parserOptions": { - "sourceType": "module", - "ecmaVersion": 2020 - }, + "parser": "@typescript-eslint/parser", "plugins": [ "import" ], diff --git a/.gitignore b/.gitignore index 95ab04dc..55540ebd 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,4 @@ dist/mapbox-gl-draw-unminified.js.map dist/bench.js dist/bench.js.map coverage/ -.nyc_output/ debug/access_token_generated.js diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 00000000..72c4429b --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npm test diff --git a/README.md b/README.md index e0282c74..fcaca506 100644 --- a/README.md +++ b/README.md @@ -45,14 +45,6 @@ import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css' ``` -### Typescript - -Typescript definition files are available as part of the [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/mapbox__mapbox-gl-draw) package. - -``` -npm install @types/mapbox__mapbox-gl-draw -``` - ### Example usage ```js diff --git a/dist/mapbox-gl-draw.css b/dist/mapbox-gl-draw.css index 0347a271..fe35d397 100644 --- a/dist/mapbox-gl-draw.css +++ b/dist/mapbox-gl-draw.css @@ -1,26 +1,25 @@ - /* Override default control style */ .mapbox-gl-draw_ctrl-bottom-left, .mapbox-gl-draw_ctrl-top-left { - margin-left:0; - border-radius:0 4px 4px 0; + margin-left: 0; + border-radius: 0 4px 4px 0; } .mapbox-gl-draw_ctrl-top-right, .mapbox-gl-draw_ctrl-bottom-right { - margin-right:0; - border-radius:4px 0 0 4px; + margin-right: 0; + border-radius: 4px 0 0 4px; } .mapbox-gl-draw_ctrl-draw-btn { - border-color:rgba(0,0,0,0.9); - color:rgba(255,255,255,0.5); - width:30px; - height:30px; + border-color: rgba(0, 0, 0, 0.9); + color: rgba(255, 255, 255, 0.5); + width: 30px; + height: 30px; } .mapbox-gl-draw_ctrl-draw-btn.active, .mapbox-gl-draw_ctrl-draw-btn.active:hover { - background-color:rgb(0 0 0/5%); + background-color: rgb(0 0 0/5%); } .mapbox-gl-draw_ctrl-draw-btn { background-repeat: no-repeat; @@ -55,34 +54,39 @@ .mapboxgl-map.mouse-add .mapboxgl-canvas-container.mapboxgl-interactive { cursor: crosshair; } -.mapboxgl-map.mouse-move.mode-direct_select .mapboxgl-canvas-container.mapboxgl-interactive { +.mapboxgl-map.mouse-move.mode-direct_select + .mapboxgl-canvas-container.mapboxgl-interactive { cursor: grab; cursor: -moz-grab; cursor: -webkit-grab; } -.mapboxgl-map.mode-direct_select.feature-vertex.mouse-move .mapboxgl-canvas-container.mapboxgl-interactive { +.mapboxgl-map.mode-direct_select.feature-vertex.mouse-move + .mapboxgl-canvas-container.mapboxgl-interactive { cursor: move; } -.mapboxgl-map.mode-direct_select.feature-midpoint.mouse-pointer .mapboxgl-canvas-container.mapboxgl-interactive { +.mapboxgl-map.mode-direct_select.feature-midpoint.mouse-pointer + .mapboxgl-canvas-container.mapboxgl-interactive { cursor: cell; } -.mapboxgl-map.mode-direct_select.feature-feature.mouse-move .mapboxgl-canvas-container.mapboxgl-interactive { +.mapboxgl-map.mode-direct_select.feature-feature.mouse-move + .mapboxgl-canvas-container.mapboxgl-interactive { cursor: move; } -.mapboxgl-map.mode-static.mouse-pointer .mapboxgl-canvas-container.mapboxgl-interactive { +.mapboxgl-map.mode-static.mouse-pointer + .mapboxgl-canvas-container.mapboxgl-interactive { cursor: grab; cursor: -moz-grab; cursor: -webkit-grab; } .mapbox-gl-draw_boxselect { - pointer-events: none; - position: absolute; - top: 0; - left: 0; - width: 0; - height: 0; - background: rgba(0,0,0,.1); - border: 2px dotted #fff; - opacity: 0.5; + pointer-events: none; + position: absolute; + top: 0; + left: 0; + width: 0; + height: 0; + background: rgba(0, 0, 0, 0.1); + border: 2px dotted #fff; + opacity: 0.5; } diff --git a/index.js b/index.js deleted file mode 100644 index 99b8ba06..00000000 --- a/index.js +++ /dev/null @@ -1,36 +0,0 @@ -import runSetup from './src/setup.js'; -import setupOptions from './src/options.js'; -import setupAPI from './src/api.js'; -import modes from './src/modes/index.js'; -import * as Constants from './src/constants.js'; -import * as lib from './src/lib/index.js'; - -const setupDraw = function(options, api) { - options = setupOptions(options); - - const ctx = { - options - }; - - api = setupAPI(ctx, api); - ctx.api = api; - - const setup = runSetup(ctx); - - api.onAdd = setup.onAdd; - api.onRemove = setup.onRemove; - api.types = Constants.types; - api.options = options; - - return api; -}; - -function MapboxDraw(options) { - setupDraw(options, this); -} - -MapboxDraw.modes = modes; -MapboxDraw.constants = Constants; -MapboxDraw.lib = lib; - -export default MapboxDraw; diff --git a/index.ts b/index.ts new file mode 100644 index 00000000..fd5f2e22 --- /dev/null +++ b/index.ts @@ -0,0 +1,38 @@ +import { spy } from 'sinon'; +import runSetup from './src/setup'; +import { configureOptions } from './src/options'; +import setupAPI from './src/api'; +import * as modes from './src/modes/index'; +import * as Constants from './src/constants'; +import * as lib from './src/lib/index'; +import type { DrawOptions, Draw } from './src/types/types'; + +const setupDraw = (options: DrawOptions, api: Draw) => { + options = configureOptions(options); + + const ctx = { + options + } as spy; + + api = setupAPI(ctx, api); + ctx.api = api; + + const setup = runSetup(ctx); + + api.onAdd = setup.onAdd; + api.onRemove = setup.onRemove; + api.types = Constants.types; + api.options = options; + + return api; +}; + +function MapboxDraw(options?: DrawOptions) { + setupDraw(options, this); +} + +MapboxDraw.modes = modes; +MapboxDraw.constants = Constants; +MapboxDraw.lib = lib; + +export default MapboxDraw; diff --git a/package-lock.json b/package-lock.json index 03b34a05..f92f9b5b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,41 +1,50 @@ { "name": "@mapbox/mapbox-gl-draw", - "version": "1.5.0", + "version": "1.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@mapbox/mapbox-gl-draw", - "version": "1.5.0", + "version": "1.6.0", "license": "ISC", "dependencies": { "@mapbox/geojson-area": "^0.2.2", "@mapbox/geojson-normalize": "^0.0.1", "@mapbox/point-geometry": "^1.1.0", "fast-deep-equal": "^3.1.3", - "nanoid": "^5.0.9" + "nanoid": "^5.1.2" }, "devDependencies": { "@mapbox/cloudfriend": "^8.1.0", "@mapbox/mapbox-gl-draw-static-mode": "^1.0.1", "@mapbox/mapbox-gl-geocoder": "^5.0.2", - "@rollup/plugin-commonjs": "^28.0.0", + "@rollup/plugin-commonjs": "^28.0.2", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.0", "@rollup/plugin-replace": "^6.0.1", "@rollup/plugin-terser": "^0.4.4", + "@rollup/plugin-typescript": "^12.1.2", "@turf/bbox-clip": "^7.0.0", "@turf/centroid": "^7.0.0", - "eslint": "8.57.1", - "eslint-config-mourner": "3.0.0", - "eslint-plugin-import": "^2.28.1", - "mapbox-gl": "^3.4.0", + "@types/geojson": "^7946.0.16", + "@types/node": "^22.13.10", + "@typescript-eslint/parser": "^8.26.0", + "eslint": "^8.57.1", + "eslint-plugin-import": "^2.31.0", + "husky": "^9.1.7", + "lint-staged": "^15.4.3", + "mapbox-gl": "^3.10.0", "mock-browser": "^0.92.14", "npm-run-all": "^4.1.5", + "prettier": "^3.5.3", "rollup": "^4.19.1", "sinon": "^19.0.2", "synthetic-dom-events": "0.3.0", - "vite": "^6.0.2" + "tsx": "^4.19.3", + "typescript": "^5.8.2", + "vite": "^6.0.2", + "vite-plugin-tsconfig-paths": "^1.4.1" }, "engines": { "node": "^18.0.0 || >=20.0.0" @@ -526,9 +535,10 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.8.1", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, - "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } @@ -580,15 +590,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -982,6 +983,7 @@ "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", "dev": true, + "license": "MIT", "dependencies": { "serialize-javascript": "^6.0.1", "smob": "^1.0.0", @@ -999,6 +1001,32 @@ } } }, + "node_modules/@rollup/plugin-typescript": { + "version": "12.1.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-12.1.2.tgz", + "integrity": "sha512-cdtSp154H5sv637uMr1a8OTWB0L1SWDSm1rDGiyfcGcvQ6cuTs4MDk2BVEBGysUWago4OJN4EQZqOTl/QY3Jgg==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.1.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.14.0||^3.0.0||^4.0.0", + "tslib": "*", + "typescript": ">=3.7.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + }, + "tslib": { + "optional": true + } + } + }, "node_modules/@rollup/pluginutils": { "version": "5.1.0", "dev": true, @@ -1464,8 +1492,7 @@ "version": "7946.0.16", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@types/geojson-vt": { "version": "3.2.5", @@ -1484,8 +1511,9 @@ }, "node_modules/@types/json5": { "version": "0.0.29", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true }, "node_modules/@types/keyv": { "version": "3.1.4", @@ -1520,9 +1548,14 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.7.14", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } }, "node_modules/@types/normalize-package-data": { "version": "2.4.4", @@ -1538,8 +1571,9 @@ }, "node_modules/@types/resolve": { "version": "1.20.2", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true }, "node_modules/@types/responselike": { "version": "1.0.3", @@ -1559,6 +1593,151 @@ "@types/geojson": "*" } }, + "node_modules/@typescript-eslint/parser": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.26.1.tgz", + "integrity": "sha512-w6HZUV4NWxqd8BdeFf81t07d7/YV9s7TCWrQQbG5uhuvGUAW+fq1usZ1Hmz9UPNLniFnD8GLSsDpjP0hm1S4lQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.26.1", + "@typescript-eslint/types": "8.26.1", + "@typescript-eslint/typescript-estree": "8.26.1", + "@typescript-eslint/visitor-keys": "8.26.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.26.1.tgz", + "integrity": "sha512-6EIvbE5cNER8sqBu6V7+KeMZIC1664d2Yjt+B9EWUXrsyWpxx4lEZrmvxgSKRC6gX+efDL/UY9OpPZ267io3mg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.26.1", + "@typescript-eslint/visitor-keys": "8.26.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.26.1.tgz", + "integrity": "sha512-n4THUQW27VmQMx+3P+B0Yptl7ydfceUj4ON/AQILAASwgYdZ/2dhfymRMh5egRUrvK5lSmaOm77Ry+lmXPOgBQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.26.1.tgz", + "integrity": "sha512-yUwPpUHDgdrv1QJ7YQal3cMVBGWfnuCdKbXw1yyjArax3353rEJP1ZA+4F8nOlQ3RfS2hUN/wze3nlY+ZOhvoA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.26.1", + "@typescript-eslint/visitor-keys": "8.26.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.26.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.26.1.tgz", + "integrity": "sha512-AjOC3zfnxd6S4Eiy3jwktJPclqhFHNyd8L6Gycf9WUPoKZpgM5PjkxY1X7uSy61xVpiJDhhk7XT2NVsN3ALTWg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.26.1", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", "dev": true, @@ -1603,6 +1782,21 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "dev": true, + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "dev": true, @@ -1631,53 +1825,13 @@ "license": "Python-2.0" }, "node_modules/array-buffer-byte-length": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-buffer-byte-length/node_modules/get-intrinsic": { - "version": "1.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-buffer-byte-length/node_modules/is-array-buffer": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "is-typed-array": "^1.1.10" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-buffer-byte-length/node_modules/is-typed-array": { - "version": "1.1.10", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "dev": true, - "license": "MIT", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" }, "engines": { "node": ">= 0.4" @@ -1693,8 +1847,9 @@ }, "node_modules/array-includes": { "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -1711,16 +1866,18 @@ } }, "node_modules/array.prototype.findlastindex": { - "version": "1.2.5", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", + "es-abstract": "^1.23.9", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -1729,23 +1886,16 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.findlastindex/node_modules/es-shim-unscopables": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.0" - } - }, "node_modules/array.prototype.flat": { - "version": "1.3.2", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -1754,18 +1904,16 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.flat/node_modules/arraybuffer.prototype.slice": { - "version": "1.0.2", + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", "dev": true, - "license": "MIT", "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "is-array-buffer": "^3.0.2", - "is-shared-array-buffer": "^1.0.2" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -1774,50 +1922,19 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.flat/node_modules/es-abstract": { - "version": "1.22.2", + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", "dev": true, - "license": "MIT", "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "arraybuffer.prototype.slice": "^1.0.2", - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "es-set-tostringtag": "^2.0.1", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.1", - "get-symbol-description": "^1.0.0", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.5", - "is-array-buffer": "^3.0.2", - "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.12", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.3", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "safe-array-concat": "^1.0.1", - "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.8", - "string.prototype.trimend": "^1.0.7", - "string.prototype.trimstart": "^1.0.7", - "typed-array-buffer": "^1.0.0", - "typed-array-byte-length": "^1.0.0", - "typed-array-byte-offset": "^1.0.0", - "typed-array-length": "^1.0.4", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.11" + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" }, "engines": { "node": ">= 0.4" @@ -1826,76 +1943,52 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.flat/node_modules/es-set-tostringtag": { - "version": "2.0.1", + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.3", - "has": "^1.0.3", - "has-tostringtag": "^1.0.0" - }, "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" } }, - "node_modules/array.prototype.flat/node_modules/es-set-tostringtag/node_modules/get-intrinsic": { - "version": "1.2.0", + "node_modules/asn1": { + "version": "0.2.6", "dev": true, "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "safer-buffer": "~2.1.0" } }, - "node_modules/array.prototype.flat/node_modules/get-intrinsic": { - "version": "1.2.1", + "node_modules/assert-plus": { + "version": "1.0.0", "dev": true, "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=0.8" } }, - "node_modules/array.prototype.flat/node_modules/internal-slot": { - "version": "1.0.5", + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - }, "engines": { "node": ">= 0.4" } }, - "node_modules/array.prototype.flat/node_modules/is-callable": { - "version": "1.2.7", + "node_modules/asynckit": { + "version": "0.4.0", "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "license": "MIT" }, - "node_modules/array.prototype.flat/node_modules/is-typed-array": { - "version": "1.1.12", + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, - "license": "MIT", "dependencies": { - "which-typed-array": "^1.1.11" + "possible-typed-array-names": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -1904,133 +1997,174 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.flat/node_modules/safe-array-concat": { - "version": "1.0.1", - "dev": true, - "license": "MIT", + "node_modules/aws-sdk": { + "version": "2.1646.0", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "has-symbols": "^1.0.3", - "isarray": "^2.0.5" + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.16.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "util": "^0.12.4", + "uuid": "8.0.0", + "xml2js": "0.6.2" }, "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 10.0.0" } }, - "node_modules/array.prototype.flat/node_modules/safe-regex-test": { - "version": "1.0.0", + "node_modules/aws-sdk/node_modules/events": { + "version": "1.1.1", "dev": true, "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "is-regex": "^1.1.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=0.4.x" } }, - "node_modules/array.prototype.flat/node_modules/safe-regex-test/node_modules/get-intrinsic": { - "version": "1.2.0", + "node_modules/aws-sdk/node_modules/ieee754": { + "version": "1.1.13", "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "license": "BSD-3-Clause" + }, + "node_modules/aws-sdk/node_modules/sax": { + "version": "1.2.1", + "dev": true, + "license": "ISC" + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" } }, - "node_modules/array.prototype.flat/node_modules/string.prototype.trimend": { - "version": "1.0.7", + "node_modules/aws4": { + "version": "1.11.0", "dev": true, - "license": "MIT", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/base-64": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", + "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "tweetnacl": "^0.14.3" } }, - "node_modules/array.prototype.flat/node_modules/string.prototype.trimstart": { - "version": "1.0.7", + "node_modules/brace-expansion": { + "version": "1.1.11", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/array.prototype.flat/node_modules/typed-array-buffer": { - "version": "1.0.0", + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "is-typed-array": "^1.1.10" + "fill-range": "^7.1.1" }, "engines": { - "node": ">= 0.4" + "node": ">=8" } }, - "node_modules/array.prototype.flat/node_modules/typed-array-buffer/node_modules/is-typed-array": { - "version": "1.1.10", + "node_modules/buffer": { + "version": "4.9.2", "dev": true, "license": "MIT", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" } }, - "node_modules/array.prototype.flat/node_modules/typed-array-byte-length": { + "node_modules/buffer-from": { + "version": "1.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/buffer/node_modules/isarray": { "version": "1.0.0", "dev": true, - "license": "MIT", + "license": "MIT" + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, - "node_modules/array.prototype.flat/node_modules/typed-array-byte-length/node_modules/is-typed-array": { - "version": "1.1.10", + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dev": true, - "license": "MIT", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" }, "engines": { "node": ">= 0.4" @@ -2039,34 +2173,27 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.flat/node_modules/typed-array-byte-offset": { - "version": "1.0.0", + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dev": true, - "license": "MIT", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.flat/node_modules/typed-array-byte-offset/node_modules/is-typed-array": { - "version": "1.1.10", + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "dev": true, - "license": "MIT", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -2075,348 +2202,270 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.flat/node_modules/typed-array-length": { - "version": "1.0.4", + "node_modules/callsites": { + "version": "3.1.0", "dev": true, "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "is-typed-array": "^1.1.9" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=6" } }, - "node_modules/array.prototype.flat/node_modules/typed-array-length/node_modules/define-properties": { - "version": "1.1.4", + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true, - "license": "MIT", - "dependencies": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=6" } }, - "node_modules/array.prototype.flat/node_modules/typed-array-length/node_modules/es-abstract": { - "version": "1.20.1", + "node_modules/camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "regexp.prototype.flags": "^1.4.3", - "string.prototype.trimend": "^1.0.5", - "string.prototype.trimstart": "^1.0.5", - "unbox-primitive": "^1.0.2" + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" }, "engines": { - "node": ">= 0.4" + "node": ">=8" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/array.prototype.flat/node_modules/typed-array-length/node_modules/function.prototype.name": { - "version": "1.1.5", + "node_modules/camelcase-keys/node_modules/quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, - "node_modules/array.prototype.flat/node_modules/typed-array-length/node_modules/get-intrinsic": { - "version": "1.1.2", + "node_modules/caseless": { + "version": "0.12.0", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/chalk": { + "version": "2.4.2", "dev": true, "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=4" } }, - "node_modules/array.prototype.flat/node_modules/typed-array-length/node_modules/internal-slot": { - "version": "1.0.3", + "node_modules/chalk/node_modules/ansi-styles": { + "version": "3.2.1", "dev": true, "license": "MIT", "dependencies": { - "get-intrinsic": "^1.1.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" + "color-convert": "^1.9.0" }, "engines": { - "node": ">= 0.4" + "node": ">=4" } }, - "node_modules/array.prototype.flat/node_modules/typed-array-length/node_modules/is-callable": { - "version": "1.2.4", + "node_modules/chalk/node_modules/color-convert": { + "version": "1.9.3", "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "dependencies": { + "color-name": "1.1.3" } }, - "node_modules/array.prototype.flat/node_modules/typed-array-length/node_modules/is-typed-array": { - "version": "1.1.9", + "node_modules/chalk/node_modules/color-name": { + "version": "1.1.3", "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "es-abstract": "^1.20.0", - "for-each": "^0.3.3", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "license": "MIT" }, - "node_modules/array.prototype.flat/node_modules/typed-array-length/node_modules/regexp.prototype.flags": { - "version": "1.4.3", + "node_modules/chalk/node_modules/has-flag": { + "version": "3.0.0", "dev": true, "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=4" } }, - "node_modules/array.prototype.flat/node_modules/typed-array-length/node_modules/string.prototype.trimend": { - "version": "1.0.5", + "node_modules/chalk/node_modules/supports-color": { + "version": "5.5.0", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" + "has-flag": "^3.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=4" } }, - "node_modules/array.prototype.flat/node_modules/typed-array-length/node_modules/string.prototype.trimstart": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "node_modules/cheap-ruler": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cheap-ruler/-/cheap-ruler-4.0.0.tgz", + "integrity": "sha512-0BJa8f4t141BYKQyn9NSQt1PguFQXMXwZiA5shfoaBYHAb2fFk2RAX+tiWMoQU+Agtzt3mdt0JtuyshAXqZ+Vw==", + "dev": true }, - "node_modules/array.prototype.flat/node_modules/which-typed-array": { - "version": "1.1.11", + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", "dev": true, - "license": "MIT", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "restore-cursor": "^5.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.2", + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/array.prototype.flatmap/node_modules/arraybuffer.prototype.slice": { - "version": "1.0.2", + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", "dev": true, - "license": "MIT", "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "is-array-buffer": "^3.0.2", - "is-shared-array-buffer": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" + "mimic-response": "^1.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/array.prototype.flatmap/node_modules/es-abstract": { - "version": "1.22.2", + "node_modules/color-convert": { + "version": "2.0.1", "dev": true, "license": "MIT", "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "arraybuffer.prototype.slice": "^1.0.2", - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "es-set-tostringtag": "^2.0.1", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.1", - "get-symbol-description": "^1.0.0", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.5", - "is-array-buffer": "^3.0.2", - "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.12", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.3", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "safe-array-concat": "^1.0.1", - "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.8", - "string.prototype.trimend": "^1.0.7", - "string.prototype.trimstart": "^1.0.7", - "typed-array-buffer": "^1.0.0", - "typed-array-byte-length": "^1.0.0", - "typed-array-byte-offset": "^1.0.0", - "typed-array-length": "^1.0.4", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.11" + "color-name": "~1.1.4" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=7.0.0" } }, - "node_modules/array.prototype.flatmap/node_modules/es-set-tostringtag": { - "version": "2.0.1", + "node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", "dev": true, "license": "MIT", "dependencies": { - "get-intrinsic": "^1.1.3", - "has": "^1.0.3", - "has-tostringtag": "^1.0.0" + "delayed-stream": "~1.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">= 0.8" } }, - "node_modules/array.prototype.flatmap/node_modules/es-set-tostringtag/node_modules/get-intrinsic": { - "version": "1.2.0", + "node_modules/commander": { + "version": "2.20.3", + "dev": true, + "license": "MIT" + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/content-type-parser": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.3", "dev": true, "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">= 8" } }, - "node_modules/array.prototype.flatmap/node_modules/get-intrinsic": { - "version": "1.2.1", + "node_modules/csscolorparser": { + "version": "1.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/cssom": { + "version": "0.3.8", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "0.2.37", "dev": true, "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "cssom": "0.3.x" } }, - "node_modules/array.prototype.flatmap/node_modules/internal-slot": { - "version": "1.0.5", + "node_modules/dashdash": { + "version": "1.14.1", "dev": true, "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" + "assert-plus": "^1.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=0.10" } }, - "node_modules/array.prototype.flatmap/node_modules/is-callable": { - "version": "1.2.7", + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "dev": true, - "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, "engines": { "node": ">= 0.4" }, @@ -2424,165 +2473,149 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.flatmap/node_modules/is-typed-array": { - "version": "1.1.12", + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", "dev": true, - "license": "MIT", "dependencies": { - "which-typed-array": "^1.1.11" + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" }, "engines": { "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/inspect-js" } }, - "node_modules/array.prototype.flatmap/node_modules/safe-array-concat": { + "node_modules/data-view-byte-offset": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "has-symbols": "^1.0.3", - "isarray": "^2.0.5" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" }, "engines": { - "node": ">=0.4" + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.flatmap/node_modules/safe-regex-test": { - "version": "1.0.0", + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "is-regex": "^1.1.4" + "ms": "^2.1.3" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/array.prototype.flatmap/node_modules/safe-regex-test/node_modules/get-intrinsic": { + "node_modules/decamelize": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/array.prototype.flatmap/node_modules/string.prototype.trimend": { - "version": "1.0.7", + "node_modules/decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/array.prototype.flatmap/node_modules/string.prototype.trimstart": { - "version": "1.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap/node_modules/typed-array-buffer": { - "version": "1.0.0", + "node_modules/decamelize-keys/node_modules/map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "is-typed-array": "^1.1.10" - }, "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" } }, - "node_modules/array.prototype.flatmap/node_modules/typed-array-buffer/node_modules/is-typed-array": { - "version": "1.1.10", + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "dev": true, - "license": "MIT", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "mimic-response": "^3.1.0" }, "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/array.prototype.flatmap/node_modules/typed-array-byte-length": { - "version": "1.0.0", + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" - }, "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/array.prototype.flatmap/node_modules/typed-array-byte-length/node_modules/is-typed-array": { - "version": "1.1.10", + "node_modules/deep-is": { + "version": "0.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, - "node_modules/array.prototype.flatmap/node_modules/typed-array-byte-offset": { - "version": "1.0.0", + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", "dev": true, "license": "MIT", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -2591,16 +2624,14 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.flatmap/node_modules/typed-array-byte-offset/node_modules/is-typed-array": { - "version": "1.1.10", + "node_modules/define-properties": { + "version": "1.2.1", "dev": true, "license": "MIT", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" }, "engines": { "node": ">= 0.4" @@ -2609,150 +2640,156 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.flatmap/node_modules/typed-array-length": { - "version": "1.0.4", + "node_modules/delayed-stream": { + "version": "1.0.0", "dev": true, "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "is-typed-array": "^1.1.9" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=0.4.0" } }, - "node_modules/array.prototype.flatmap/node_modules/typed-array-length/node_modules/define-properties": { - "version": "1.1.4", + "node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", "dev": true, - "license": "MIT", - "dependencies": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.3.1" } }, - "node_modules/array.prototype.flatmap/node_modules/typed-array-length/node_modules/es-abstract": { - "version": "1.20.1", + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "regexp.prototype.flags": "^1.4.3", - "string.prototype.trimend": "^1.0.5", - "string.prototype.trimstart": "^1.0.5", - "unbox-primitive": "^1.0.2" + "esutils": "^2.0.2" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, - "node_modules/array.prototype.flatmap/node_modules/typed-array-length/node_modules/function.prototype.name": { - "version": "1.1.5", + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.flatmap/node_modules/typed-array-length/node_modules/get-intrinsic": { - "version": "1.1.2", + "node_modules/earcut": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.0.tgz", + "integrity": "sha512-41Fs7Q/PLq1SDbqjsgcY7GA42T0jvaCNGXgGtsNdvg+Yv8eIu06bxv4/PoREkZ9nMDNwnUSG9OFB9+yv8eKhDg==", + "dev": true + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", "dev": true, "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" } }, - "node_modules/array.prototype.flatmap/node_modules/typed-array-length/node_modules/internal-slot": { - "version": "1.0.3", + "node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "dev": true, - "license": "MIT", "dependencies": { - "get-intrinsic": "^1.1.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" + "once": "^1.4.0" } }, - "node_modules/array.prototype.flatmap/node_modules/typed-array-length/node_modules/is-callable": { - "version": "1.2.4", + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", "dev": true, - "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/array.prototype.flatmap/node_modules/typed-array-length/node_modules/is-typed-array": { - "version": "1.1.9", + "node_modules/error-ex": { + "version": "1.3.2", "dev": true, "license": "MIT", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "es-abstract": "^1.20.0", - "for-each": "^0.3.3", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "is-arrayish": "^0.2.1" } }, - "node_modules/array.prototype.flatmap/node_modules/typed-array-length/node_modules/regexp.prototype.flags": { - "version": "1.4.3", + "node_modules/es-abstract": { + "version": "1.23.9", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", + "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.0", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-regex": "^1.2.1", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.0", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.3", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.18" }, "engines": { "node": ">= 0.4" @@ -2761,93 +2798,71 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.flatmap/node_modules/typed-array-length/node_modules/string.prototype.trimend": { - "version": "1.0.5", + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">= 0.4" } }, - "node_modules/array.prototype.flatmap/node_modules/typed-array-length/node_modules/string.prototype.trimstart": { - "version": "1.0.5", + "node_modules/es-errors": { + "version": "1.3.0", "dev": true, "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">= 0.4" } }, - "node_modules/array.prototype.flatmap/node_modules/which-typed-array": { - "version": "1.1.11", + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dev": true, - "license": "MIT", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "es-errors": "^1.3.0" }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.3", + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, - "license": "MIT", "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.2.1", - "get-intrinsic": "^1.2.3", - "is-array-buffer": "^3.0.4", - "is-shared-array-buffer": "^1.0.2" + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/arraybuffer.prototype.slice/node_modules/array-buffer-byte-length": { - "version": "1.0.1", + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", - "is-array-buffer": "^3.0.4" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/arraybuffer.prototype.slice/node_modules/is-array-buffer": { - "version": "3.0.4", + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1" + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" }, "engines": { "node": ">= 0.4" @@ -2856,2147 +2871,686 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/arraybuffer.prototype.slice/node_modules/is-array-buffer/node_modules/get-intrinsic": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/asn1": { - "version": "0.2.6", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, - "node_modules/assert-plus": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "dev": true, - "license": "MIT" - }, - "node_modules/available-typed-arrays": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/aws-sdk": { - "version": "2.1646.0", - "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "buffer": "4.9.2", - "events": "1.1.1", - "ieee754": "1.1.13", - "jmespath": "0.16.0", - "querystring": "0.2.0", - "sax": "1.2.1", - "url": "0.10.3", - "util": "^0.12.4", - "uuid": "8.0.0", - "xml2js": "0.6.2" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/aws-sdk/node_modules/events": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/aws-sdk/node_modules/ieee754": { - "version": "1.1.13", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/aws-sdk/node_modules/sax": { - "version": "1.2.1", - "dev": true, - "license": "ISC" - }, - "node_modules/aws-sign2": { - "version": "0.7.0", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "*" - } - }, - "node_modules/aws4": { - "version": "1.11.0", - "dev": true, - "license": "MIT" - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/base-64": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", - "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==", - "dev": true - }, - "node_modules/base64-js": { - "version": "1.5.1", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tweetnacl": "^0.14.3" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/buffer": { - "version": "4.9.2", - "dev": true, - "license": "MIT", - "dependencies": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/buffer/node_modules/isarray": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/cacheable-lookup": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", - "dev": true, - "engines": { - "node": ">=10.6.0" - } - }, - "node_modules/cacheable-request": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", - "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", - "dev": true, - "dependencies": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^4.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^6.0.1", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/call-bind": { - "version": "1.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind/node_modules/function-bind": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase-keys": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", - "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", - "dev": true, - "dependencies": { - "camelcase": "^5.3.1", - "map-obj": "^4.0.0", - "quick-lru": "^4.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/camelcase-keys/node_modules/quick-lru": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", - "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/caseless": { - "version": "0.12.0", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/chalk": { - "version": "2.4.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/chalk/node_modules/ansi-styles": { - "version": "3.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/chalk/node_modules/color-convert": { - "version": "1.9.3", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/chalk/node_modules/color-name": { - "version": "1.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/chalk/node_modules/has-flag": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/chalk/node_modules/supports-color": { - "version": "5.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cheap-ruler": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cheap-ruler/-/cheap-ruler-4.0.0.tgz", - "integrity": "sha512-0BJa8f4t141BYKQyn9NSQt1PguFQXMXwZiA5shfoaBYHAb2fFk2RAX+tiWMoQU+Agtzt3mdt0JtuyshAXqZ+Vw==", - "dev": true - }, - "node_modules/clone-response": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", - "dev": true, - "dependencies": { - "mimic-response": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "2.20.3", - "dev": true, - "license": "MIT" - }, - "node_modules/commondir": { - "version": "1.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/content-type-parser": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/core-util-is": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/csscolorparser": { - "version": "1.0.3", - "dev": true, - "license": "MIT" - }, - "node_modules/cssom": { - "version": "0.3.8", - "dev": true, - "license": "MIT" - }, - "node_modules/cssstyle": { - "version": "0.2.37", - "dev": true, - "license": "MIT", - "dependencies": { - "cssom": "0.3.x" - } - }, - "node_modules/dashdash": { - "version": "1.14.1", - "dev": true, - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/data-view-buffer": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/debug": { - "version": "4.3.5", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decamelize-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", - "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", - "dev": true, - "dependencies": { - "decamelize": "^1.1.0", - "map-obj": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decamelize-keys/node_modules/map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decompress-response/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/deepmerge": { - "version": "4.2.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties/node_modules/define-data-property": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/define-properties/node_modules/get-intrinsic": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/diff": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", - "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", - "dev": true, - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/doctrine": { - "version": "2.1.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/earcut": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.0.tgz", - "integrity": "sha512-41Fs7Q/PLq1SDbqjsgcY7GA42T0jvaCNGXgGtsNdvg+Yv8eIu06bxv4/PoREkZ9nMDNwnUSG9OFB9+yv8eKhDg==", - "dev": true - }, - "node_modules/ecc-jsbn": { - "version": "0.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/error-ex": { - "version": "1.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-abstract": { - "version": "1.23.3", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "arraybuffer.prototype.slice": "^1.0.3", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "data-view-buffer": "^1.0.1", - "data-view-byte-length": "^1.0.1", - "data-view-byte-offset": "^1.0.0", - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-set-tostringtag": "^2.0.3", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.4", - "get-symbol-description": "^1.0.2", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", - "hasown": "^2.0.2", - "internal-slot": "^1.0.7", - "is-array-buffer": "^3.0.4", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.1", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.3", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.13", - "is-weakref": "^1.0.2", - "object-inspect": "^1.13.1", - "object-keys": "^1.1.1", - "object.assign": "^4.1.5", - "regexp.prototype.flags": "^1.5.2", - "safe-array-concat": "^1.1.2", - "safe-regex-test": "^1.0.3", - "string.prototype.trim": "^1.2.9", - "string.prototype.trimend": "^1.0.8", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.2", - "typed-array-byte-length": "^1.0.1", - "typed-array-byte-offset": "^1.0.2", - "typed-array-length": "^1.0.6", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.15" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-abstract/node_modules/array-buffer-byte-length": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.5", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-abstract/node_modules/available-typed-arrays": { - "version": "1.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-abstract/node_modules/get-symbol-description": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-abstract/node_modules/has-property-descriptors": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-abstract/node_modules/has-proto": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-abstract/node_modules/is-array-buffer": { - "version": "3.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-abstract/node_modules/is-array-buffer/node_modules/get-intrinsic": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-abstract/node_modules/is-array-buffer/node_modules/has-proto": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-abstract/node_modules/is-callable": { - "version": "1.2.7", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-abstract/node_modules/is-negative-zero": { - "version": "2.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-abstract/node_modules/is-shared-array-buffer": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-abstract/node_modules/regexp.prototype.flags": { - "version": "1.5.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.6", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "set-function-name": "^2.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-abstract/node_modules/set-function-name": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-define-property": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.4", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag/node_modules/has-tostringtag": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has": "^1.0.3" - } - }, - "node_modules/es-to-primitive": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/esbuild": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", - "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.0", - "@esbuild/android-arm": "0.25.0", - "@esbuild/android-arm64": "0.25.0", - "@esbuild/android-x64": "0.25.0", - "@esbuild/darwin-arm64": "0.25.0", - "@esbuild/darwin-x64": "0.25.0", - "@esbuild/freebsd-arm64": "0.25.0", - "@esbuild/freebsd-x64": "0.25.0", - "@esbuild/linux-arm": "0.25.0", - "@esbuild/linux-arm64": "0.25.0", - "@esbuild/linux-ia32": "0.25.0", - "@esbuild/linux-loong64": "0.25.0", - "@esbuild/linux-mips64el": "0.25.0", - "@esbuild/linux-ppc64": "0.25.0", - "@esbuild/linux-riscv64": "0.25.0", - "@esbuild/linux-s390x": "0.25.0", - "@esbuild/linux-x64": "0.25.0", - "@esbuild/netbsd-arm64": "0.25.0", - "@esbuild/netbsd-x64": "0.25.0", - "@esbuild/openbsd-arm64": "0.25.0", - "@esbuild/openbsd-x64": "0.25.0", - "@esbuild/sunos-x64": "0.25.0", - "@esbuild/win32-arm64": "0.25.0", - "@esbuild/win32-ia32": "0.25.0", - "@esbuild/win32-x64": "0.25.0" - } - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/escodegen": { - "version": "1.14.3", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=4.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/escodegen/node_modules/estraverse": { - "version": "4.3.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/escodegen/node_modules/levn": { - "version": "0.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/escodegen/node_modules/optionator": { - "version": "0.8.3", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", - "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.1", - "@humanwhocodes/config-array": "^0.13.0", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-config-mourner": { - "version": "3.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/ms": { - "version": "2.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint-module-utils": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", - "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", - "dev": true, - "dependencies": { - "debug": "^3.2.7" - }, - "engines": { - "node": ">=4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import": { - "version": "2.31.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", - "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", - "dev": true, - "dependencies": { - "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.8", - "array.prototype.findlastindex": "^1.2.5", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", - "debug": "^3.2.7", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.0", - "hasown": "^2.0.2", - "is-core-module": "^2.15.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "object.groupby": "^1.0.3", - "object.values": "^1.2.0", - "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.8", - "tsconfig-paths": "^3.15.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" - } - }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "3.2.7", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/ms": { - "version": "2.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/eslint/node_modules/doctrine": { - "version": "3.0.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/eslint/node_modules/escape-string-regexp": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/find-up": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/globals": { - "version": "13.19.0", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/locate-path": { - "version": "6.0.0", + "node_modules/esbuild": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", + "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", "dev": true, + "hasInstallScript": true, "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" + "bin": { + "esbuild": "bin/esbuild" }, "engines": { - "node": ">=10" + "node": ">=18" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.0", + "@esbuild/android-arm": "0.25.0", + "@esbuild/android-arm64": "0.25.0", + "@esbuild/android-x64": "0.25.0", + "@esbuild/darwin-arm64": "0.25.0", + "@esbuild/darwin-x64": "0.25.0", + "@esbuild/freebsd-arm64": "0.25.0", + "@esbuild/freebsd-x64": "0.25.0", + "@esbuild/linux-arm": "0.25.0", + "@esbuild/linux-arm64": "0.25.0", + "@esbuild/linux-ia32": "0.25.0", + "@esbuild/linux-loong64": "0.25.0", + "@esbuild/linux-mips64el": "0.25.0", + "@esbuild/linux-ppc64": "0.25.0", + "@esbuild/linux-riscv64": "0.25.0", + "@esbuild/linux-s390x": "0.25.0", + "@esbuild/linux-x64": "0.25.0", + "@esbuild/netbsd-arm64": "0.25.0", + "@esbuild/netbsd-x64": "0.25.0", + "@esbuild/openbsd-arm64": "0.25.0", + "@esbuild/openbsd-x64": "0.25.0", + "@esbuild/sunos-x64": "0.25.0", + "@esbuild/win32-arm64": "0.25.0", + "@esbuild/win32-ia32": "0.25.0", + "@esbuild/win32-x64": "0.25.0" } }, - "node_modules/eslint/node_modules/p-limit": { - "version": "3.1.0", + "node_modules/escape-string-regexp": { + "version": "1.0.5", "dev": true, "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.8.0" } }, - "node_modules/eslint/node_modules/p-locate": { - "version": "5.0.0", + "node_modules/escodegen": { + "version": "1.14.3", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "p-limit": "^3.0.2" + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" }, - "engines": { - "node": ">=10" + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/type-fest": { - "version": "0.20.2", - "dev": true, - "license": "(MIT OR CC0-1.0)", "engines": { - "node": ">=10" + "node": ">=4.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "optionalDependencies": { + "source-map": "~0.6.1" } }, - "node_modules/espree": { - "version": "9.6.1", + "node_modules/escodegen/node_modules/estraverse": { + "version": "4.3.0", "dev": true, "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=4.0" } }, - "node_modules/espree/node_modules/acorn": { - "version": "8.10.0", + "node_modules/escodegen/node_modules/levn": { + "version": "0.3.0", "dev": true, "license": "MIT", - "bin": { - "acorn": "bin/acorn" + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" }, "engines": { - "node": ">=0.4.0" + "node": ">= 0.8.0" } }, - "node_modules/espree/node_modules/acorn-jsx": { - "version": "5.3.2", + "node_modules/escodegen/node_modules/optionator": { + "version": "0.8.3", "dev": true, "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esquery": { - "version": "1.5.0", - "dev": true, - "license": "BSD-3-Clause", "dependencies": { - "estraverse": "^5.1.0" + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" }, "engines": { - "node": ">=0.10" + "node": ">= 0.8.0" } }, - "node_modules/esrecurse": { - "version": "4.3.0", + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, - "license": "BSD-2-Clause", "dependencies": { - "estraverse": "^5.2.0" + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" }, "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "2.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/esutils": { - "version": "2.0.3", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eventemitter3": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", - "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==", - "dev": true - }, - "node_modules/events": { - "version": "3.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.x" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/extend": { - "version": "3.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/extsprintf": { - "version": "1.3.0", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", "dev": true, - "license": "MIT" + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } }, - "node_modules/fastq": { - "version": "1.13.0", + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, - "license": "ISC", "dependencies": { - "reusify": "^1.0.4" + "ms": "^2.1.1" } }, - "node_modules/fdir": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.0.tgz", - "integrity": "sha512-3oB133prH1o4j/L5lLW7uOCF1PlD+/It2L0eL/iAqWMB91RBbqTewABqxhj0ibBd90EEmWZq7ntIWzVaWcXTGQ==", + "node_modules/eslint-module-utils": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", "dev": true, - "peerDependencies": { - "picomatch": "^3 || ^4" + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" }, "peerDependenciesMeta": { - "picomatch": { + "eslint": { "optional": true } } }, - "node_modules/file-entry-cache": { - "version": "6.0.1", + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, - "license": "MIT", "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" + "ms": "^2.1.1" } }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "node_modules/eslint-plugin-import": { + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "dev": true, "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", + "tsconfig-paths": "^3.15.0" }, "engines": { - "node": ">=8" - } - }, - "node_modules/flat-cache": { - "version": "3.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" + "node": ">=4" }, - "engines": { - "node": "^10.12.0 || >=12.0.0" + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, - "node_modules/flatted": { + "node_modules/eslint-plugin-import/node_modules/debug": { "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, - "license": "ISC" - }, - "node_modules/for-each": { - "version": "0.3.3", - "dev": true, - "license": "MIT", "dependencies": { - "is-callable": "^1.1.3" - } - }, - "node_modules/forever-agent": { - "version": "0.6.1", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "*" + "ms": "^2.1.1" } }, - "node_modules/form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "node_modules/eslint-scope": { + "version": "7.2.2", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" }, "engines": { - "node": ">= 6" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.2", + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "license": "Apache-2.0", "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/function-bind": { - "version": "1.1.1", + "node_modules/eslint/node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true, - "license": "MIT" + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } }, - "node_modules/function.prototype.name": { - "version": "1.1.6", + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "functions-have-names": "^1.2.3" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/function.prototype.name/node_modules/arraybuffer.prototype.slice": { - "version": "1.0.2", + "node_modules/eslint/node_modules/doctrine": { + "version": "3.0.0", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "is-array-buffer": "^3.0.2", - "is-shared-array-buffer": "^1.0.2" + "esutils": "^2.0.2" }, "engines": { - "node": ">= 0.4" + "node": ">=6.0.0" + } + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/function.prototype.name/node_modules/es-abstract": { - "version": "1.22.2", + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", "dev": true, "license": "MIT", "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "arraybuffer.prototype.slice": "^1.0.2", - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "es-set-tostringtag": "^2.0.1", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.1", - "get-symbol-description": "^1.0.0", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.5", - "is-array-buffer": "^3.0.2", - "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.12", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.3", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "safe-array-concat": "^1.0.1", - "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.8", - "string.prototype.trimend": "^1.0.7", - "string.prototype.trimstart": "^1.0.7", - "typed-array-buffer": "^1.0.0", - "typed-array-byte-length": "^1.0.0", - "typed-array-byte-offset": "^1.0.0", - "typed-array-length": "^1.0.4", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.11" + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/function.prototype.name/node_modules/es-set-tostringtag": { - "version": "2.0.1", + "node_modules/eslint/node_modules/globals": { + "version": "13.19.0", "dev": true, "license": "MIT", "dependencies": { - "get-intrinsic": "^1.1.3", - "has": "^1.0.3", - "has-tostringtag": "^1.0.0" + "type-fest": "^0.20.2" }, "engines": { - "node": ">= 0.4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/function.prototype.name/node_modules/es-set-tostringtag/node_modules/get-intrinsic": { - "version": "1.2.0", + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", "dev": true, "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/function.prototype.name/node_modules/get-intrinsic": { - "version": "1.2.1", + "node_modules/eslint/node_modules/p-limit": { + "version": "3.1.0", "dev": true, "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/function.prototype.name/node_modules/internal-slot": { - "version": "1.0.5", + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", "dev": true, "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" + "p-limit": "^3.0.2" }, "engines": { - "node": ">= 0.4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/function.prototype.name/node_modules/is-callable": { - "version": "1.2.7", + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", "dev": true, - "license": "MIT", + "license": "(MIT OR CC0-1.0)", "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/function.prototype.name/node_modules/is-typed-array": { - "version": "1.1.12", + "node_modules/espree": { + "version": "9.6.1", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "which-typed-array": "^1.1.11" + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": ">= 0.4" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://opencollective.com/eslint" } }, - "node_modules/function.prototype.name/node_modules/safe-array-concat": { - "version": "1.0.1", + "node_modules/espree/node_modules/acorn": { + "version": "8.10.0", "dev": true, "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "has-symbols": "^1.0.3", - "isarray": "^2.0.5" + "bin": { + "acorn": "bin/acorn" }, "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.4.0" } }, - "node_modules/function.prototype.name/node_modules/safe-regex-test": { - "version": "1.0.0", + "node_modules/espree/node_modules/acorn-jsx": { + "version": "5.3.2", "dev": true, "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "is-regex": "^1.1.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/function.prototype.name/node_modules/safe-regex-test/node_modules/get-intrinsic": { - "version": "1.2.0", + "node_modules/esprima": { + "version": "4.0.1", "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=4" } }, - "node_modules/function.prototype.name/node_modules/string.prototype.trimend": { - "version": "1.0.7", + "node_modules/esquery": { + "version": "1.5.0", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "estraverse": "^5.1.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=0.10" } }, - "node_modules/function.prototype.name/node_modules/string.prototype.trimstart": { - "version": "1.0.7", + "node_modules/esrecurse": { + "version": "4.3.0", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "estraverse": "^5.2.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=4.0" } }, - "node_modules/function.prototype.name/node_modules/typed-array-buffer": { - "version": "1.0.0", + "node_modules/estraverse": { + "version": "5.3.0", "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "is-typed-array": "^1.1.10" - }, + "license": "BSD-2-Clause", "engines": { - "node": ">= 0.4" + "node": ">=4.0" } }, - "node_modules/function.prototype.name/node_modules/typed-array-buffer/node_modules/is-typed-array": { - "version": "1.1.10", + "node_modules/estree-walker": { + "version": "2.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" - }, + "license": "BSD-2-Clause", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, - "node_modules/function.prototype.name/node_modules/typed-array-byte-length": { - "version": "1.0.0", + "node_modules/eventemitter3": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", + "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==", + "dev": true + }, + "node_modules/events": { + "version": "3.3.0", "dev": true, "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.8.x" } }, - "node_modules/function.prototype.name/node_modules/typed-array-byte-length/node_modules/is-typed-array": { - "version": "1.1.10", + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", "dev": true, - "license": "MIT", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=16.17" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/function.prototype.name/node_modules/typed-array-byte-offset": { - "version": "1.0.0", + "node_modules/execa/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" - }, "engines": { - "node": ">= 0.4" + "node": ">=16" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/function.prototype.name/node_modules/typed-array-byte-offset/node_modules/is-typed-array": { - "version": "1.1.10", + "node_modules/extend": { + "version": "3.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, - "license": "MIT", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8.6.0" } }, - "node_modules/function.prototype.name/node_modules/typed-array-length": { - "version": "1.0.4", + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "is-typed-array": "^1.1.9" + "is-glob": "^4.0.1" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">= 6" } }, - "node_modules/function.prototype.name/node_modules/typed-array-length/node_modules/define-properties": { - "version": "1.1.4", + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", "dev": true, - "license": "MIT", + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.13.0", + "dev": true, + "license": "ISC", "dependencies": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", + "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==", + "dev": true, + "peerDependencies": { + "picomatch": "^3 || ^4" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, - "node_modules/function.prototype.name/node_modules/typed-array-length/node_modules/es-abstract": { - "version": "1.20.1", + "node_modules/file-entry-cache": { + "version": "6.0.1", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "regexp.prototype.flags": "^1.4.3", - "string.prototype.trimend": "^1.0.5", - "string.prototype.trimstart": "^1.0.5", - "unbox-primitive": "^1.0.2" + "flat-cache": "^3.0.4" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/function.prototype.name/node_modules/typed-array-length/node_modules/function.prototype.name": { - "version": "1.1.5", + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" + "to-regex-range": "^5.0.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, - "node_modules/function.prototype.name/node_modules/typed-array-length/node_modules/get-intrinsic": { - "version": "1.1.2", + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, - "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=8" } }, - "node_modules/function.prototype.name/node_modules/typed-array-length/node_modules/internal-slot": { - "version": "1.0.3", + "node_modules/flat-cache": { + "version": "3.0.4", "dev": true, "license": "MIT", "dependencies": { - "get-intrinsic": "^1.1.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" + "flatted": "^3.1.0", + "rimraf": "^3.0.2" }, "engines": { - "node": ">= 0.4" + "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/function.prototype.name/node_modules/typed-array-length/node_modules/is-callable": { - "version": "1.2.4", + "node_modules/flatted": { + "version": "3.2.7", "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "license": "ISC" }, - "node_modules/function.prototype.name/node_modules/typed-array-length/node_modules/is-typed-array": { - "version": "1.1.9", + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, - "license": "MIT", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "es-abstract": "^1.20.0", - "for-each": "^0.3.3", - "has-tostringtag": "^1.0.0" + "is-callable": "^1.2.7" }, "engines": { "node": ">= 0.4" @@ -5005,58 +3559,68 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/function.prototype.name/node_modules/typed-array-length/node_modules/regexp.prototype.flags": { - "version": "1.4.3", + "node_modules/forever-agent": { + "version": "0.6.1", "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" - }, + "license": "Apache-2.0", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "*" } }, - "node_modules/function.prototype.name/node_modules/typed-array-length/node_modules/string.prototype.trimend": { - "version": "1.0.5", + "node_modules/form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">= 6" } }, - "node_modules/function.prototype.name/node_modules/typed-array-length/node_modules/string.prototype.trimstart": { - "version": "1.0.5", + "node_modules/fs.realpath": { + "version": "1.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/function.prototype.name/node_modules/which-typed-array": { - "version": "1.1.11", + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", "dev": true, - "license": "MIT", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" }, "engines": { "node": ">= 0.4" @@ -5067,8 +3631,9 @@ }, "node_modules/functions-have-names": { "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true, - "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -5086,16 +3651,34 @@ "integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==", "dev": true }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { - "version": "1.2.4", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, - "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -5104,12 +3687,17 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-intrinsic/node_modules/function-bind": { - "version": "1.1.2", + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/get-stream": { @@ -5128,12 +3716,14 @@ } }, "node_modules/get-symbol-description": { - "version": "1.0.0", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -5142,17 +3732,16 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-symbol-description/node_modules/get-intrinsic": { - "version": "1.1.2", + "node_modules/get-tsconfig": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", + "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", "dev": true, - "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "resolve-pkg-maps": "^1.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, "node_modules/getpass": { @@ -5199,26 +3788,13 @@ } }, "node_modules/globalthis": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/globalthis/node_modules/define-properties": { - "version": "1.1.4", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dev": true, - "license": "MIT", "dependencies": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" + "define-properties": "^1.2.1", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -5228,24 +3804,12 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gopd/node_modules/get-intrinsic": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5320,21 +3884,14 @@ "node": ">=6" } }, - "node_modules/has": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/has-bigints": { - "version": "1.0.2", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", "dev": true, - "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -5348,33 +3905,25 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.0", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, - "license": "MIT", "dependencies": { - "get-intrinsic": "^1.1.1" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-property-descriptors/node_modules/get-intrinsic": { - "version": "1.1.2", + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", "dev": true, - "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "dunder-proto": "^1.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.1", - "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -5383,9 +3932,10 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -5394,11 +3944,12 @@ } }, "node_modules/has-tostringtag": { - "version": "1.0.0", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, - "license": "MIT", "dependencies": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -5418,14 +3969,6 @@ "node": ">= 0.4" } }, - "node_modules/hasown/node_modules/function-bind": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/hosted-git-info": { "version": "2.8.9", "dev": true, @@ -5472,6 +4015,30 @@ "node": ">=10.19.0" } }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "dev": true, @@ -5503,9 +4070,10 @@ "license": "BSD-3-Clause" }, "node_modules/ignore": { - "version": "5.2.0", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, - "license": "MIT", "engines": { "node": ">= 4" } @@ -5565,13 +4133,14 @@ "license": "ISC" }, "node_modules/internal-slot": { - "version": "1.0.7", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "dev": true, - "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" + "hasown": "^2.0.2", + "side-channel": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -5593,42 +4162,38 @@ } }, "node_modules/is-array-buffer": { - "version": "3.0.2", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-array-buffer/node_modules/get-intrinsic": { - "version": "1.2.1", + "node_modules/is-arrayish": { + "version": "0.2.1", "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "license": "MIT" }, - "node_modules/is-array-buffer/node_modules/is-typed-array": { - "version": "1.1.10", + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", "dev": true, - "license": "MIT", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -5637,29 +4202,29 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "dev": true, - "license": "MIT" - }, "node_modules/is-bigint": { - "version": "1.0.4", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "dev": true, - "license": "MIT", "dependencies": { - "has-bigints": "^1.0.1" + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-boolean-object": { - "version": "1.1.2", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -5669,9 +4234,10 @@ } }, "node_modules/is-callable": { - "version": "1.2.4", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -5695,10 +4261,13 @@ } }, "node_modules/is-data-view": { - "version": "1.0.1", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "dev": true, - "license": "MIT", "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" }, "engines": { @@ -5709,11 +4278,13 @@ } }, "node_modules/is-date-object": { - "version": "1.0.5", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "dev": true, - "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -5730,6 +4301,33 @@ "node": ">=0.10.0" } }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-generator-function": { "version": "1.0.10", "dev": true, @@ -5755,15 +4353,11 @@ "node": ">=0.10.0" } }, - "node_modules/is-module": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/is-negative-zero": { - "version": "2.0.2", + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -5771,12 +4365,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-number-object": { - "version": "1.0.7", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "dev": true, - "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -5804,20 +4415,36 @@ }, "node_modules/is-reference": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", "dev": true, - "license": "MIT", "dependencies": { "@types/estree": "*" } }, "node_modules/is-regex": { - "version": "1.1.4", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -5826,22 +4453,40 @@ } }, "node_modules/is-shared-array-buffer": { - "version": "1.0.2", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.2" + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-string": { - "version": "1.0.7", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dev": true, - "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -5851,11 +4496,14 @@ } }, "node_modules/is-symbol": { - "version": "1.0.4", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "dev": true, - "license": "MIT", "dependencies": { - "has-symbols": "^1.0.2" + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -5865,11 +4513,12 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.13", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, - "license": "MIT", "dependencies": { - "which-typed-array": "^1.1.14" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -5883,12 +4532,44 @@ "dev": true, "license": "MIT" }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-weakref": { - "version": "1.0.2", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.2" + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5896,8 +4577,9 @@ }, "node_modules/isarray": { "version": "2.0.5", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true }, "node_modules/isexe": { "version": "2.0.0", @@ -6002,6 +4684,18 @@ "dev": true, "license": "ISC" }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, "node_modules/jsprim": { "version": "1.4.2", "dev": true, @@ -6076,12 +4770,107 @@ "node": ">= 0.8.0" } }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, + "node_modules/lint-staged": { + "version": "15.4.3", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.4.3.tgz", + "integrity": "sha512-FoH1vOeouNh1pw+90S+cnuoFwRfUD9ijY2GKy5h7HS3OR7JVir2N2xrsa0+Twc1B7cW72L+88geG5cW4wIhn7g==", + "dev": true, + "dependencies": { + "chalk": "^5.4.1", + "commander": "^13.1.0", + "debug": "^4.4.0", + "execa": "^8.0.1", + "lilconfig": "^3.1.3", + "listr2": "^8.2.5", + "micromatch": "^4.0.8", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.7.0" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/lint-staged/node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/listr2": { + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.5.tgz", + "integrity": "sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==", + "dev": true, + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/listr2/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true + }, "node_modules/load-json-file": { "version": "4.0.0", "dev": true, @@ -6102,33 +4891,122 @@ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, "dependencies": { - "p-locate": "^4.1.0" + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dev": true, + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/lodash": { - "version": "4.17.21", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", - "dev": true - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "dev": true, - "license": "MIT" - }, "node_modules/lowercase-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", @@ -6180,11 +5058,6 @@ "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.10.0.tgz", "integrity": "sha512-YnQxjlthuv/tidcxGYU2C8nRDVXMlAHa3qFhuOJeX4AfRP72OMRBf9ApL+M+k5VWcAXi2fcNOUVgphknjLumjA==", "dev": true, - "license": "SEE LICENSE IN LICENSE.txt", - "workspaces": [ - "src/style-spec", - "test/build/typings" - ], "dependencies": { "@mapbox/jsonlint-lines-primitives": "^2.0.2", "@mapbox/mapbox-gl-supported": "^3.0.0", @@ -6222,6 +5095,15 @@ "integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==", "dev": true }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/memorystream": { "version": "0.3.1", "dev": true, @@ -6319,6 +5201,46 @@ "node": ">=8" } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime-db": { "version": "1.52.0", "dev": true, @@ -6338,6 +5260,30 @@ "node": ">= 0.6" } }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -6399,9 +5345,10 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "dev": true, - "license": "MIT" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true }, "node_modules/murmurhash-js": { "version": "1.0.0", @@ -6589,6 +5536,33 @@ "which": "bin/which" } }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/nwmatcher": { "version": "1.4.4", "dev": true, @@ -6603,9 +5577,10 @@ } }, "node_modules/object-inspect": { - "version": "1.13.2", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -6622,13 +5597,16 @@ } }, "node_modules/object.assign": { - "version": "4.1.5", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", "object-keys": "^1.1.1" }, "engines": { @@ -6640,8 +5618,9 @@ }, "node_modules/object.fromentries": { "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -6657,8 +5636,9 @@ }, "node_modules/object.groupby": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -6669,11 +5649,13 @@ } }, "node_modules/object.values": { - "version": "1.2.0", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" }, @@ -6692,6 +5674,21 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.3", "dev": true, @@ -6727,6 +5724,23 @@ "node": ">= 0.8.0" } }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/p-cancelable": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", @@ -6904,9 +5918,10 @@ } }, "node_modules/possible-typed-array-names": { - "version": "1.0.0", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" } @@ -6971,6 +5986,21 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/protocol-buffers-schema": { "version": "3.6.0", "dev": true, @@ -7053,6 +6083,8 @@ }, "node_modules/randombytes": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7160,14 +6192,40 @@ "node": ">=4" } }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/regexp.prototype.flags": { - "version": "1.5.1", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "set-function-name": "^2.0.0" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -7249,6 +6307,15 @@ "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", "dev": true }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/resolve-protobuf-schema": { "version": "2.1.0", "dev": true, @@ -7269,6 +6336,37 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/reusify": { "version": "1.0.4", "dev": true, @@ -7278,6 +6376,12 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true + }, "node_modules/rimraf": { "version": "3.0.2", "dev": true, @@ -7354,13 +6458,15 @@ } }, "node_modules/safe-array-concat": { - "version": "1.1.2", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4", - "has-symbols": "^1.0.3", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", "isarray": "^2.0.5" }, "engines": { @@ -7389,14 +6495,31 @@ ], "license": "MIT" }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safe-regex-test": { - "version": "1.0.3", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", + "call-bound": "^1.0.2", "es-errors": "^1.3.0", - "is-regex": "^1.1.4" + "is-regex": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -7417,14 +6540,17 @@ }, "node_modules/semver": { "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "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, "license": "BSD-3-Clause", "dependencies": { @@ -7455,63 +6581,33 @@ "node": ">= 0.4" } }, - "node_modules/set-function-length/node_modules/function-bind": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/set-function-length/node_modules/has-property-descriptors": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/set-function-name": { - "version": "2.0.1", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dev": true, - "license": "MIT", "dependencies": { - "define-data-property": "^1.0.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name/node_modules/define-data-property": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" } }, - "node_modules/set-function-name/node_modules/get-intrinsic": { - "version": "1.2.1", + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", "dev": true, - "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">= 0.4" } }, "node_modules/shebang-command": { @@ -7539,31 +6635,89 @@ "license": "MIT" }, "node_modules/side-channel": { - "version": "1.0.4", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/side-channel/node_modules/get-intrinsic": { - "version": "1.1.2", + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "dev": true, - "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/sinon": { "version": "19.0.2", "resolved": "https://registry.npmjs.org/sinon/-/sinon-19.0.2.tgz", @@ -7582,8 +6736,38 @@ "url": "https://opencollective.com/sinon" } }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/smob": { "version": "1.5.0", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", + "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", "dev": true, "license": "MIT" }, @@ -7666,124 +6850,67 @@ "node": ">=0.10.0" } }, - "node_modules/string.prototype.padend": { - "version": "3.1.3", + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.6.19" } }, - "node_modules/string.prototype.padend/node_modules/define-properties": { - "version": "1.1.4", + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, - "license": "MIT", "dependencies": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">= 0.4" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/string.prototype.padend/node_modules/es-abstract": { - "version": "1.20.1", + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "regexp.prototype.flags": "^1.4.3", - "string.prototype.trimend": "^1.0.5", - "string.prototype.trimstart": "^1.0.5", - "unbox-primitive": "^1.0.2" - }, "engines": { - "node": ">= 0.4" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/string.prototype.padend/node_modules/function.prototype.name": { - "version": "1.1.5", + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" + "ansi-regex": "^6.0.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.padend/node_modules/get-intrinsic": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.padend/node_modules/internal-slot": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/string.prototype.padend/node_modules/regexp.prototype.flags": { - "version": "1.4.3", + "node_modules/string.prototype.padend": { + "version": "3.1.3", "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" + "es-abstract": "^1.19.1" }, "engines": { "node": ">= 0.4" @@ -7792,41 +6919,19 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/string.prototype.padend/node_modules/string.prototype.trimend": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.padend/node_modules/string.prototype.trimstart": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/string.prototype.trim": { - "version": "1.2.9", + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.0", - "es-object-atoms": "^1.0.0" + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -7836,22 +6941,28 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.8", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimstart": { "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -7883,6 +6994,18 @@ "node": ">=4" } }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-indent": { "version": "2.0.0", "dev": true, @@ -7995,6 +7118,18 @@ "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", "dev": true }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/tough-cookie": { "version": "2.5.0", "dev": true, @@ -8021,10 +7156,23 @@ "node": ">=8" } }, + "node_modules/ts-api-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", + "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", + "dev": true, + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/tsconfig-paths": { "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", "dev": true, - "license": "MIT", "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", @@ -8032,23 +7180,31 @@ "strip-bom": "^3.0.0" } }, - "node_modules/tsconfig-paths/node_modules/json5": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true }, + "node_modules/tsx": { + "version": "4.19.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.3.tgz", + "integrity": "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==", + "dev": true, + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/tunnel-agent": { "version": "0.6.0", "dev": true, @@ -8097,73 +7253,31 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/typed-array-buffer": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-length/node_modules/has-proto": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.2", + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, - "license": "MIT", "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/typed-array-byte-offset/node_modules/available-typed-arrays": { - "version": "1.0.7", + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", "dev": true, - "license": "MIT", "dependencies": { - "possible-typed-array-names": "^1.0.0" + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -8172,10 +7286,20 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/typed-array-byte-offset/node_modules/has-proto": { - "version": "1.0.3", + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", "dev": true, - "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, "engines": { "node": ">= 0.4" }, @@ -8184,16 +7308,17 @@ } }, "node_modules/typed-array-length": { - "version": "1.0.6", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-proto": "^1.0.3", "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0" + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" }, "engines": { "node": ">= 0.4" @@ -8202,31 +7327,53 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/typed-array-length/node_modules/has-proto": { - "version": "1.0.3", + "node_modules/typescript": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-paths": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/typescript-paths/-/typescript-paths-1.5.1.tgz", + "integrity": "sha512-lYErSLCON2MSplVV5V/LBgD4UNjMgY3guATdFCZY2q1Nr6OZEu4q6zX/rYMsG1TaWqqQSszg6C9EU7AGWMDrIw==", + "dev": true, + "peerDependencies": { + "typescript": "^4.7.2 || ^5" } }, "node_modules/unbox-primitive": { - "version": "1.0.2", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", + "call-bound": "^1.0.3", "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "node_modules/uri-js": { "version": "4.4.1", "dev": true, @@ -8262,179 +7409,6 @@ "which-typed-array": "^1.1.2" } }, - "node_modules/util/node_modules/define-properties": { - "version": "1.1.4", - "dev": true, - "license": "MIT", - "dependencies": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/util/node_modules/es-abstract": { - "version": "1.20.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "regexp.prototype.flags": "^1.4.3", - "string.prototype.trimend": "^1.0.5", - "string.prototype.trimstart": "^1.0.5", - "unbox-primitive": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/util/node_modules/function.prototype.name": { - "version": "1.1.5", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/util/node_modules/get-intrinsic": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/util/node_modules/internal-slot": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/util/node_modules/is-typed-array": { - "version": "1.1.9", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "es-abstract": "^1.20.0", - "for-each": "^0.3.3", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/util/node_modules/regexp.prototype.flags": { - "version": "1.4.3", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/util/node_modules/string.prototype.trimend": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/util/node_modules/string.prototype.trimstart": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/util/node_modules/which-typed-array": { - "version": "1.1.8", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "es-abstract": "^1.20.0", - "for-each": "^0.3.3", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/uuid": { "version": "8.0.0", "dev": true, @@ -8545,16 +7519,16 @@ } } }, - "node_modules/vite/node_modules/fsevents": { - "version": "2.3.3", + "node_modules/vite-plugin-tsconfig-paths": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/vite-plugin-tsconfig-paths/-/vite-plugin-tsconfig-paths-1.4.1.tgz", + "integrity": "sha512-pGpvsPGDpiM5z7I9ZhBe7H2WAH0gAC2Lh55rM3pE+84V2q7qojNO28wdUl4i/M4XUfJcilhyucmbc9D7IKqpXA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "dependencies": { + "typescript-paths": "^1.5.1" + }, + "peerDependencies": { + "vite": "*" } }, "node_modules/vt-pbf": { @@ -8619,30 +7593,43 @@ } }, "node_modules/which-boxed-primitive": { - "version": "1.0.2", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", "dev": true, - "license": "MIT", "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/which-typed-array": { - "version": "1.1.15", + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", "dev": true, - "license": "MIT", "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.2" + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -8651,12 +7638,16 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/which-typed-array/node_modules/available-typed-arrays": { - "version": "1.0.7", + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "dev": true, - "license": "MIT", "dependencies": { - "possible-typed-array-names": "^1.0.0" + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" }, "engines": { "node": ">= 0.4" @@ -8665,12 +7656,19 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/which-typed-array/node_modules/has-tostringtag": { - "version": "1.0.2", + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", "dev": true, - "license": "MIT", "dependencies": { - "has-symbols": "^1.0.3" + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -8687,6 +7685,62 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "dev": true, @@ -8731,6 +7785,18 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/yaml": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/yargs-parser": { "version": "20.2.9", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", diff --git a/package.json b/package.json index aa232a93..798b170d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@mapbox/mapbox-gl-draw", - "version": "1.5.0", + "version": "1.6.0", "engines": { "node": "^18.0.0 || >=20.0.0" }, @@ -23,7 +23,6 @@ ], "type": "module", "exports": { - ".": "./index.js", "./dist/mapbox-gl-draw-unminified.js": "./dist/mapbox-gl-draw-unminified.js", "./dist/mapbox-gl-draw.js": "./dist/mapbox-gl-draw.js", "./dist/mapbox-gl-draw.css": "./dist/mapbox-gl-draw.css" @@ -33,54 +32,77 @@ "style": "dist/mapbox-gl-draw.css", "scripts": { "docs": "run-s docs-modes-life-cycle docs-modes-get-and-set", - "docs-modes-get-and-set": "documentation readme --readme-file ./docs/MODES.md -s \"Setters and Getters\" src/modes/mode_interface_accessors.js --shallow", - "docs-modes-life-cycle": "documentation readme --readme-file ./docs/MODES.md -s \"Life Cycle Functions\" src/modes/mode_interface.js --shallow", - "lint": "eslint index.js src test bench", + "docs-modes-get-and-set": "documentation readme --readme-file ./docs/MODES.md -s \"Setters and Getters\" src/modes/mode_interface_accessors.ts --shallow", + "docs-modes-life-cycle": "documentation readme --readme-file ./docs/MODES.md -s \"Life Cycle Functions\" src/modes/mode_interface.ts --shallow", + "lint": "eslint **/*.ts", "pretest": "npm run lint", - "test": "node --test --import ./test/mock-browser.js", - "coverage": "node --test --import ./test/mock-browser.js --experimental-test-coverage", + "test": "tsx test/*.test.ts", + "coverage": "tsx test/*.test.ts --experimental-test-coverage", "build-token": "node build/generate-access-token-script.js", "build": "rollup -c", "build-min": "rollup -c --environment MINIFY:true", + "tsc": "tsc", "prepublishOnly": "run-s build build-min", "watch": "rollup -c --watch", "watch-bench": "rollup -c bench/rollup.config.js --watch", "start-server": "vite --config vite.config.js", "start-bench": "run-p build-token watch watch-bench \"start-server --base . .\"", - "start": "run-p build-token start-server" + "start": "run-p build-token start-server", + "prepare": "husky" }, "devDependencies": { "@mapbox/cloudfriend": "^8.1.0", "@mapbox/mapbox-gl-draw-static-mode": "^1.0.1", "@mapbox/mapbox-gl-geocoder": "^5.0.2", - "@rollup/plugin-commonjs": "^28.0.0", + "@rollup/plugin-commonjs": "^28.0.2", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.0", "@rollup/plugin-replace": "^6.0.1", "@rollup/plugin-terser": "^0.4.4", + "@rollup/plugin-typescript": "^12.1.2", "@turf/bbox-clip": "^7.0.0", "@turf/centroid": "^7.0.0", - "eslint": "8.57.1", - "eslint-config-mourner": "3.0.0", - "eslint-plugin-import": "^2.28.1", - "mapbox-gl": "^3.4.0", + "@types/geojson": "^7946.0.16", + "@types/node": "^22.13.10", + "@typescript-eslint/parser": "^8.26.0", + "eslint": "^8.57.1", + "eslint-plugin-import": "^2.31.0", + "husky": "^9.1.7", + "lint-staged": "^15.4.3", + "mapbox-gl": "^3.10.0", "mock-browser": "^0.92.14", "npm-run-all": "^4.1.5", + "prettier": "^3.5.3", "rollup": "^4.19.1", "sinon": "^19.0.2", "synthetic-dom-events": "0.3.0", - "vite": "^6.0.2" + "tsx": "^4.19.3", + "typescript": "^5.8.2", + "vite": "^6.0.2", + "vite-plugin-tsconfig-paths": "^1.4.1" }, "dependencies": { "@mapbox/geojson-area": "^0.2.2", "@mapbox/geojson-normalize": "^0.0.1", "@mapbox/point-geometry": "^1.1.0", "fast-deep-equal": "^3.1.3", - "nanoid": "^5.0.9" + "nanoid": "^5.1.2" }, "files": [ "src", - "index.js", + "index.ts", "dist/mapbox-gl-draw*" - ] + ], + "lint-staged": { + "**/*.{ts}": [ + "eslint", + "prettier --write" + ] + }, + "prettier": { + "singleQuote": true, + "trailingComma": "none", + "arrowParens": "avoid", + "endOfLine": "auto" + } } diff --git a/rollup.config.js b/rollup.config.js index 2a271b7d..dc2b37dd 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,34 +1,27 @@ -const {MINIFY} = process.env; -const minified = MINIFY === 'true'; -const outputFile = minified ? 'dist/mapbox-gl-draw.js' : 'dist/mapbox-gl-draw-unminified.js'; - -import commonjs from '@rollup/plugin-commonjs'; -import resolve from '@rollup/plugin-node-resolve'; +import typescript from '@rollup/plugin-typescript'; import terser from '@rollup/plugin-terser'; +const { MINIFY } = process.env; +const minified = MINIFY === 'true'; +const outputFile = minified + ? 'dist/mapbox-gl-draw.js' + : 'dist/mapbox-gl-draw-unminified.js'; + export default { - input: ['index.js'], + input: 'index.ts', output: { name: 'MapboxDraw', file: outputFile, - format: 'umd', + format: 'esm', sourcemap: true, indent: false }, treeshake: true, plugins: [ + typescript(), minified ? terser({ ecma: 2020, module: true, }) : false, - resolve({ - browser: true, - preferBuiltins: true - }), - commonjs({ - // global keyword handling causes Webpack compatibility issues, so we disabled it: - // https://github.com/mapbox/mapbox-gl-js/pull/6956 - ignoreGlobal: true - }) - ], + ] }; diff --git a/src/api.js b/src/api.ts similarity index 54% rename from src/api.js rename to src/api.ts index ddea6921..6d434305 100644 --- a/src/api.js +++ b/src/api.ts @@ -1,15 +1,17 @@ import isEqual from 'fast-deep-equal'; import normalize from '@mapbox/geojson-normalize'; -import {generateID} from './lib/id.js'; -import featuresAt from './lib/features_at.js'; -import stringSetsAreEqual from './lib/string_sets_are_equal.js'; -import * as Constants from './constants.js'; -import StringSet from './lib/string_set.js'; +import { generateID } from './lib/id'; +import * as featuresAt from './lib/features_at'; +import { stringSetsAreEqual } from './lib/string_sets_are_equal'; +import * as Constants from './constants'; +import StringSet from './lib/string_set'; -import Polygon from './feature_types/polygon.js'; -import LineString from './feature_types/line_string.js'; -import Point from './feature_types/point.js'; -import MultiFeature from './feature_types/multi_feature.js'; +import Polygon from './feature_types/polygon'; +import LineString from './feature_types/line_string'; +import Point from './feature_types/point'; +import MultiFeature from './feature_types/multi_feature'; + +import type { CTX, Draw, StrictFeature, MapMouseEvent } from './types/types'; const featureTypes = { Polygon, @@ -20,31 +22,38 @@ const featureTypes = { MultiPoint: MultiFeature }; -export default function(ctx, api) { +export default function (ctx: CTX, api: Draw) { api.modes = Constants.modes; // API doesn't emit events by default - const silent = ctx.options.suppressAPIEvents !== undefined ? !!ctx.options.suppressAPIEvents : true; - - api.getFeatureIdsAt = function(point) { - const features = featuresAt.click({ point }, null, ctx); + const silent = + ctx.options.suppressAPIEvents !== undefined + ? !!ctx.options.suppressAPIEvents + : true; + + api.getFeatureIdsAt = (point) => { + const event = { point } as MapMouseEvent; + const features = featuresAt.click(event, null, ctx); return features.map(feature => feature.properties.id); }; - api.getSelectedIds = function() { + api.getSelectedIds = () => { return ctx.store.getSelectedIds(); }; - api.getSelected = function() { + api.getSelected = () => { return { - type: Constants.geojsonTypes.FEATURE_COLLECTION, - features: ctx.store.getSelectedIds().map(id => ctx.store.get(id)).map(feature => feature.toGeoJSON()) + type: Constants.geojsonTypes.FEATURE_COLLECTION as 'FeatureCollection', + features: ctx.store + .getSelectedIds() + .map(id => ctx.store.get(id)) + .map(feature => feature.toGeoJSON()) as StrictFeature[] }; }; - api.getSelectedPoints = function() { + api.getSelectedPoints = () => { return { - type: Constants.geojsonTypes.FEATURE_COLLECTION, + type: Constants.geojsonTypes.FEATURE_COLLECTION as 'FeatureCollection', features: ctx.store.getSelectedCoordinates().map(coordinate => ({ type: Constants.geojsonTypes.FEATURE, properties: {}, @@ -52,12 +61,16 @@ export default function(ctx, api) { type: Constants.geojsonTypes.POINT, coordinates: coordinate.coordinates } - })) + })) as StrictFeature[] }; }; - api.set = function(featureCollection) { - if (featureCollection.type === undefined || featureCollection.type !== Constants.geojsonTypes.FEATURE_COLLECTION || !Array.isArray(featureCollection.features)) { + api.set = (featureCollection) => { + if ( + featureCollection.type === undefined || + featureCollection.type !== Constants.geojsonTypes.FEATURE_COLLECTION || + !Array.isArray(featureCollection.features) + ) { throw new Error('Invalid FeatureCollection'); } const renderBatch = ctx.store.createRenderBatch(); @@ -74,17 +87,20 @@ export default function(ctx, api) { return newIds; }; - api.add = function(geojson) { + api.add = (geojson) => { const featureCollection = JSON.parse(JSON.stringify(normalize(geojson))); - const ids = featureCollection.features.map((feature) => { + const ids = featureCollection.features.map(feature => { feature.id = feature.id || generateID(); if (feature.geometry === null) { throw new Error('Invalid geometry: null'); } - if (ctx.store.get(feature.id) === undefined || ctx.store.get(feature.id).type !== feature.geometry.type) { + if ( + ctx.store.get(feature.id) === undefined || + ctx.store.get(feature.id).type !== feature.geometry.type + ) { // If the feature has not yet been created ... const Model = featureTypes[feature.geometry.type]; if (Model === undefined) { @@ -98,9 +114,14 @@ export default function(ctx, api) { const originalProperties = internalFeature.properties; internalFeature.properties = feature.properties; if (!isEqual(originalProperties, feature.properties)) { - ctx.store.featureChanged(internalFeature.id, { silent }); + ctx.store.featureChanged(internalFeature.id as string, { silent }); } - if (!isEqual(internalFeature.getCoordinates(), feature.geometry.coordinates)) { + if ( + !isEqual( + internalFeature.getCoordinates(), + feature.geometry.coordinates + ) + ) { internalFeature.incomingCoords(feature.geometry.coordinates); } } @@ -111,27 +132,31 @@ export default function(ctx, api) { return ids; }; - - api.get = function(id) { + api.get = (id) => { const feature = ctx.store.get(id); if (feature) { - return feature.toGeoJSON(); + return feature.toGeoJSON() as StrictFeature; } }; - api.getAll = function() { + api.getAll = () => { return { - type: Constants.geojsonTypes.FEATURE_COLLECTION, - features: ctx.store.getAll().map(feature => feature.toGeoJSON()) + type: Constants.geojsonTypes.FEATURE_COLLECTION as 'FeatureCollection', + features: ctx.store.getAll().map(feature => feature.toGeoJSON()) as StrictFeature[] }; }; - api.delete = function(featureIds) { + api.delete = (featureIds) => { ctx.store.delete(featureIds, { silent }); // If we were in direct select mode and our selected feature no longer exists // (because it was deleted), we need to get out of that mode. - if (api.getMode() === Constants.modes.DIRECT_SELECT && !ctx.store.getSelectedIds().length) { - ctx.events.changeMode(Constants.modes.SIMPLE_SELECT, undefined, { silent }); + if ( + api.getMode() === Constants.modes.DIRECT_SELECT && + !ctx.store.getSelectedIds().length + ) { + ctx.events.changeMode(Constants.modes.SIMPLE_SELECT, undefined, { + silent + }); } else { ctx.store.render(); } @@ -139,12 +164,14 @@ export default function(ctx, api) { return api; }; - api.deleteAll = function() { + api.deleteAll = () => { ctx.store.delete(ctx.store.getAllIds(), { silent }); // If we were in direct select mode, now our selected feature no longer exists, // so escape that mode. if (api.getMode() === Constants.modes.DIRECT_SELECT) { - ctx.events.changeMode(Constants.modes.SIMPLE_SELECT, undefined, { silent }); + ctx.events.changeMode(Constants.modes.SIMPLE_SELECT, undefined, { + silent + }); } else { ctx.store.render(); } @@ -152,10 +179,19 @@ export default function(ctx, api) { return api; }; - api.changeMode = function(mode, modeOptions = {}) { + api.changeMode = (mode, modeOptions: { featureIds?: string[], featureId?: string } = {}) => { // Avoid changing modes just to re-select what's already selected - if (mode === Constants.modes.SIMPLE_SELECT && api.getMode() === Constants.modes.SIMPLE_SELECT) { - if (stringSetsAreEqual((modeOptions.featureIds || []), ctx.store.getSelectedIds())) return api; + if ( + mode === Constants.modes.SIMPLE_SELECT && + api.getMode() === Constants.modes.SIMPLE_SELECT + ) { + if ( + stringSetsAreEqual( + modeOptions.featureIds || [], + ctx.store.getSelectedIds() + ) + ) + return api; // And if we are changing the selection within simple_select mode, just change the selection, // instead of stopping and re-starting the mode ctx.store.setSelected(modeOptions.featureIds, { silent }); @@ -163,8 +199,11 @@ export default function(ctx, api) { return api; } - if (mode === Constants.modes.DIRECT_SELECT && api.getMode() === Constants.modes.DIRECT_SELECT && - modeOptions.featureId === ctx.store.getSelectedIds()[0]) { + if ( + mode === Constants.modes.DIRECT_SELECT && + api.getMode() === Constants.modes.DIRECT_SELECT && + modeOptions.featureId === ctx.store.getSelectedIds()[0] + ) { return api; } @@ -172,26 +211,26 @@ export default function(ctx, api) { return api; }; - api.getMode = function() { - return ctx.events.getMode(); + api.getMode = () => { + return ctx.events.getMode() as string; }; - api.trash = function() { + api.trash = () => { ctx.events.trash({ silent }); return api; }; - api.combineFeatures = function() { - ctx.events.combineFeatures({ silent }); + api.combineFeatures = () => { + ctx.events.combineFeatures(); return api; }; - api.uncombineFeatures = function() { - ctx.events.uncombineFeatures({ silent }); + api.uncombineFeatures = () => { + ctx.events.uncombineFeatures(); return api; }; - api.setFeatureProperty = function(featureId, property, value) { + api.setFeatureProperty = (featureId, property, value) => { ctx.store.setFeatureProperty(featureId, property, value, { silent }); return api; }; diff --git a/src/constants.js b/src/constants.ts similarity index 100% rename from src/constants.js rename to src/constants.ts diff --git a/src/events.js b/src/events.js deleted file mode 100644 index 8f6c4c6a..00000000 --- a/src/events.js +++ /dev/null @@ -1,268 +0,0 @@ -import setupModeHandler from './lib/mode_handler.js'; -import getFeaturesAndSetCursor from './lib/get_features_and_set_cursor.js'; -import featuresAt from './lib/features_at.js'; -import isClick from './lib/is_click.js'; -import isTap from './lib/is_tap.js'; -import * as Constants from './constants.js'; -import objectToMode from './modes/object_to_mode.js'; - -export default function(ctx) { - - const modes = Object.keys(ctx.options.modes).reduce((m, k) => { - m[k] = objectToMode(ctx.options.modes[k]); - return m; - }, {}); - - let mouseDownInfo = {}; - let touchStartInfo = {}; - const events = {}; - let currentModeName = null; - let currentMode = null; - - events.drag = function(event, isDrag) { - if (isDrag({ - point: event.point, - time: new Date().getTime() - })) { - ctx.ui.queueMapClasses({ mouse: Constants.cursors.DRAG }); - currentMode.drag(event); - } else { - event.originalEvent.stopPropagation(); - } - }; - - events.mousedrag = function(event) { - events.drag(event, endInfo => !isClick(mouseDownInfo, endInfo)); - }; - - events.touchdrag = function(event) { - events.drag(event, endInfo => !isTap(touchStartInfo, endInfo)); - }; - - events.mousemove = function(event) { - const button = event.originalEvent.buttons !== undefined ? event.originalEvent.buttons : event.originalEvent.which; - if (button === 1) { - return events.mousedrag(event); - } - const target = getFeaturesAndSetCursor(event, ctx); - event.featureTarget = target; - currentMode.mousemove(event); - }; - - events.mousedown = function(event) { - mouseDownInfo = { - time: new Date().getTime(), - point: event.point - }; - const target = getFeaturesAndSetCursor(event, ctx); - event.featureTarget = target; - currentMode.mousedown(event); - }; - - events.mouseup = function(event) { - const target = getFeaturesAndSetCursor(event, ctx); - event.featureTarget = target; - - if (isClick(mouseDownInfo, { - point: event.point, - time: new Date().getTime() - })) { - currentMode.click(event); - } else { - currentMode.mouseup(event); - } - }; - - events.mouseout = function(event) { - currentMode.mouseout(event); - }; - - events.touchstart = function(event) { - if (!ctx.options.touchEnabled) { - return; - } - - touchStartInfo = { - time: new Date().getTime(), - point: event.point - }; - const target = featuresAt.touch(event, null, ctx)[0]; - event.featureTarget = target; - currentMode.touchstart(event); - }; - - events.touchmove = function(event) { - if (!ctx.options.touchEnabled) { - return; - } - - currentMode.touchmove(event); - return events.touchdrag(event); - }; - - events.touchend = function(event) { - // Prevent emulated mouse events because we will fully handle the touch here. - // This does not stop the touch events from propogating to mapbox though. - event.originalEvent.preventDefault(); - if (!ctx.options.touchEnabled) { - return; - } - - const target = featuresAt.touch(event, null, ctx)[0]; - event.featureTarget = target; - if (isTap(touchStartInfo, { - time: new Date().getTime(), - point: event.point - })) { - currentMode.tap(event); - } else { - currentMode.touchend(event); - } - }; - - // 8 - Backspace - // 46 - Delete - const isKeyModeValid = code => !(code === 8 || code === 46 || (code >= 48 && code <= 57)); - - events.keydown = function(event) { - const isMapElement = (event.srcElement || event.target).classList.contains(Constants.classes.CANVAS); - if (!isMapElement) return; // we only handle events on the map - - if ((event.keyCode === 8 || event.keyCode === 46) && ctx.options.controls.trash) { - event.preventDefault(); - currentMode.trash(); - } else if (isKeyModeValid(event.keyCode)) { - currentMode.keydown(event); - } else if (event.keyCode === 49 && ctx.options.controls.point) { - changeMode(Constants.modes.DRAW_POINT); - } else if (event.keyCode === 50 && ctx.options.controls.line_string) { - changeMode(Constants.modes.DRAW_LINE_STRING); - } else if (event.keyCode === 51 && ctx.options.controls.polygon) { - changeMode(Constants.modes.DRAW_POLYGON); - } - }; - - events.keyup = function(event) { - if (isKeyModeValid(event.keyCode)) { - currentMode.keyup(event); - } - }; - - events.zoomend = function() { - ctx.store.changeZoom(); - }; - - events.data = function(event) { - if (event.dataType === 'style') { - const { setup, map, options, store } = ctx; - const hasLayers = options.styles.some(style => map.getLayer(style.id)); - if (!hasLayers) { - setup.addLayers(); - store.setDirty(); - store.render(); - } - } - }; - - function changeMode(modename, nextModeOptions, eventOptions = {}) { - currentMode.stop(); - - const modebuilder = modes[modename]; - if (modebuilder === undefined) { - throw new Error(`${modename} is not valid`); - } - currentModeName = modename; - const mode = modebuilder(ctx, nextModeOptions); - currentMode = setupModeHandler(mode, ctx); - - if (!eventOptions.silent) { - ctx.map.fire(Constants.events.MODE_CHANGE, { mode: modename}); - } - - ctx.store.setDirty(); - ctx.store.render(); - } - - const actionState = { - trash: false, - combineFeatures: false, - uncombineFeatures: false - }; - - function actionable(actions) { - let changed = false; - Object.keys(actions).forEach((action) => { - if (actionState[action] === undefined) throw new Error('Invalid action type'); - if (actionState[action] !== actions[action]) changed = true; - actionState[action] = actions[action]; - }); - if (changed) ctx.map.fire(Constants.events.ACTIONABLE, { actions: actionState }); - } - - const api = { - start() { - currentModeName = ctx.options.defaultMode; - currentMode = setupModeHandler(modes[currentModeName](ctx), ctx); - }, - changeMode, - actionable, - currentModeName() { - return currentModeName; - }, - currentModeRender(geojson, push) { - return currentMode.render(geojson, push); - }, - fire(eventName, eventData) { - if (!ctx.map) return; - ctx.map.fire(eventName, eventData); - }, - addEventListeners() { - ctx.map.on('mousemove', events.mousemove); - ctx.map.on('mousedown', events.mousedown); - ctx.map.on('mouseup', events.mouseup); - ctx.map.on('data', events.data); - - ctx.map.on('touchmove', events.touchmove); - ctx.map.on('touchstart', events.touchstart); - ctx.map.on('touchend', events.touchend); - - ctx.container.addEventListener('mouseout', events.mouseout); - - if (ctx.options.keybindings) { - ctx.container.addEventListener('keydown', events.keydown); - ctx.container.addEventListener('keyup', events.keyup); - } - }, - removeEventListeners() { - ctx.map.off('mousemove', events.mousemove); - ctx.map.off('mousedown', events.mousedown); - ctx.map.off('mouseup', events.mouseup); - ctx.map.off('data', events.data); - - ctx.map.off('touchmove', events.touchmove); - ctx.map.off('touchstart', events.touchstart); - ctx.map.off('touchend', events.touchend); - - ctx.container.removeEventListener('mouseout', events.mouseout); - - if (ctx.options.keybindings) { - ctx.container.removeEventListener('keydown', events.keydown); - ctx.container.removeEventListener('keyup', events.keyup); - } - }, - trash(options) { - currentMode.trash(options); - }, - combineFeatures() { - currentMode.combineFeatures(); - }, - uncombineFeatures() { - currentMode.uncombineFeatures(); - }, - getMode() { - return currentModeName; - } - }; - - return api; -} diff --git a/src/events.ts b/src/events.ts new file mode 100644 index 00000000..41b98d42 --- /dev/null +++ b/src/events.ts @@ -0,0 +1,276 @@ +import setupModeHandler from './lib/mode_handler'; +import { getFeatureAtAndSetCursors } from './lib/get_features_at_and_set_cursor'; +import * as featuresAt from './lib/features_at'; +import { isClick } from './lib/is_click'; +import { isTap } from './lib/is_tap'; +import * as Constants from './constants'; +import { objectToMode } from './modes/object_to_mode'; +import type { CTX, Entry } from './types/types'; + +interface EventOptions { + silent?: boolean; +} + +export default function(ctx: CTX) { + const modes = Object.keys(ctx.options.modes).reduce((m, k) => { + m[k] = objectToMode(ctx.options.modes[k]); + return m; + }, {}); + + let mouseDownInfo = {} as Entry; + let touchStartInfo = {} as Entry; + let currentModeName = null; + let currentMode = null; + + // 8 - Backspace + // 46 - Delete + const isKeyModeValid = code => !(code === 8 || code === 46 || (code >= 48 && code <= 57)); + + const events = { + drag: function(event, isDrag) { + if (isDrag) { + ctx.ui.queueMapClasses({ mouse: Constants.cursors.DRAG }); + currentMode.drag(event); + } else { + event.originalEvent.stopPropagation(); + } + }, + + mousedrag: function(event) { + const info = { + point: event.point, + time: new Date().getTime() + }; + + events.drag(event, !isClick(mouseDownInfo, info)); + }, + + touchdrag: function(event) { + const info = { + point: event.point, + time: new Date().getTime() + }; + + events.drag(event, !isTap(touchStartInfo, info)); + }, + + mousemove: function(event) { + const button = event.originalEvent.buttons !== undefined ? event.originalEvent.buttons : event.originalEvent.which; + if (button === 1) { + return events.mousedrag(event); + } + const target = getFeatureAtAndSetCursors(event, ctx); + event.featureTarget = target; + currentMode.mousemove(event); + }, + + mousedown: function(event) { + mouseDownInfo = { + time: new Date().getTime(), + point: event.point + }; + const target = getFeatureAtAndSetCursors(event, ctx); + event.featureTarget = target; + currentMode.mousedown(event); + }, + + mouseup: function(event) { + const target = getFeatureAtAndSetCursors(event, ctx); + event.featureTarget = target; + + if (isClick(mouseDownInfo, { + point: event.point, + time: new Date().getTime() + })) { + currentMode.click(event); + } else { + currentMode.mouseup(event); + } + }, + + mouseout: function(event) { + currentMode.mouseout(event); + }, + + touchstart: function(event) { + if (!ctx.options.touchEnabled) { + return; + } + + touchStartInfo = { + time: new Date().getTime(), + point: event.point + }; + const target = featuresAt.touch(event, null, ctx)[0]; + event.featureTarget = target; + currentMode.touchstart(event); + }, + + touchmove: function(event) { + if (!ctx.options.touchEnabled) { + return; + } + + currentMode.touchmove(event); + return events.touchdrag(event); + }, + + touchend: function(event) { + // Prevent emulated mouse events because we will fully handle the touch here. + // This does not stop the touch events from propogating to mapbox though. + event.originalEvent.preventDefault(); + if (!ctx.options.touchEnabled) { + return; + } + + const target = featuresAt.touch(event, null, ctx)[0]; + event.featureTarget = target; + if (isTap(touchStartInfo, { + time: new Date().getTime(), + point: event.point + })) { + currentMode.tap(event); + } else { + currentMode.touchend(event); + } + }, + + keydown: function(event) { + const isMapElement = (event.srcElement || event.target).classList.contains(Constants.classes.CANVAS); + if (!isMapElement) return; // we only handle events on the map + + if ((event.keyCode === 8 || event.keyCode === 46) && ctx.options.controls.trash) { + event.preventDefault(); + currentMode.trash(); + } else if (isKeyModeValid(event.keyCode)) { + currentMode.keydown(event); + } else if (event.keyCode === 49 && ctx.options.controls.point) { + changeMode(Constants.modes.DRAW_POINT); + } else if (event.keyCode === 50 && ctx.options.controls.line_string) { + changeMode(Constants.modes.DRAW_LINE_STRING); + } else if (event.keyCode === 51 && ctx.options.controls.polygon) { + changeMode(Constants.modes.DRAW_POLYGON); + } + }, + + keyup: function(event) { + if (isKeyModeValid(event.keyCode)) { + currentMode.keyup(event); + } + }, + + data: function(event) { + if (event.dataType === 'style') { + const { setup, map, options, store } = ctx; + const hasLayers = options.styles.some(style => map.getLayer(style.id)); + if (!hasLayers) { + setup.addLayers(); + store.setDirty(); + store.render(); + } + } + } + }; + + function changeMode(modename, nextModeOptions = {}, eventOptions: EventOptions = {}) { + currentMode.stop(); + + const modebuilder = modes[modename]; + if (modebuilder === undefined) { + throw new Error(`${modename} is not valid`); + } + currentModeName = modename; + const mode = modebuilder(ctx, nextModeOptions); + currentMode = setupModeHandler(mode, ctx); + + if (!eventOptions.silent) { + ctx.map.fire(Constants.events.MODE_CHANGE, { mode: modename}); + } + + ctx.store.setDirty(); + ctx.store.render(); + } + + const actionState = { + trash: false, + combineFeatures: false, + uncombineFeatures: false + }; + + function actionable(actions) { + let changed = false; + Object.keys(actions).forEach((action) => { + if (actionState[action] === undefined) throw new Error('Invalid action type'); + if (actionState[action] !== actions[action]) changed = true; + actionState[action] = actions[action]; + }); + if (changed) ctx.map.fire(Constants.events.ACTIONABLE, { actions: actionState }); + } + + const api = { + start() { + currentModeName = ctx.options.defaultMode; + currentMode = setupModeHandler(modes[currentModeName](ctx), ctx); + }, + changeMode, + actionable, + currentModeName() { + return currentModeName; + }, + currentModeRender(geojson, push) { + return currentMode.render(geojson, push); + }, + fire(eventName, eventData) { + if (!ctx.map) return; + ctx.map.fire(eventName, eventData); + }, + addEventListeners() { + ctx.map.on('mousemove', events.mousemove); + ctx.map.on('mousedown', events.mousedown); + ctx.map.on('mouseup', events.mouseup); + ctx.map.on('data', events.data); + + ctx.map.on('touchmove', events.touchmove); + ctx.map.on('touchstart', events.touchstart); + ctx.map.on('touchend', events.touchend); + + ctx.container.addEventListener('mouseout', events.mouseout); + + if (ctx.options.keybindings) { + ctx.container.addEventListener('keydown', events.keydown); + ctx.container.addEventListener('keyup', events.keyup); + } + }, + removeEventListeners() { + ctx.map.off('mousemove', events.mousemove); + ctx.map.off('mousedown', events.mousedown); + ctx.map.off('mouseup', events.mouseup); + ctx.map.off('data', events.data); + + ctx.map.off('touchmove', events.touchmove); + ctx.map.off('touchstart', events.touchstart); + ctx.map.off('touchend', events.touchend); + + ctx.container.removeEventListener('mouseout', events.mouseout); + + if (ctx.options.keybindings) { + ctx.container.removeEventListener('keydown', events.keydown); + ctx.container.removeEventListener('keyup', events.keyup); + } + }, + trash(options?: { silent: boolean }) { + currentMode.trash(options); + }, + combineFeatures() { + currentMode.combineFeatures(); + }, + uncombineFeatures() { + currentMode.uncombineFeatures(); + }, + getMode() { + return currentModeName; + } + }; + + return api; +} diff --git a/src/feature_types/feature.js b/src/feature_types/feature.js deleted file mode 100644 index ee2b92ea..00000000 --- a/src/feature_types/feature.js +++ /dev/null @@ -1,70 +0,0 @@ -import {generateID} from '../lib/id.js'; -import * as Constants from '../constants.js'; - -const Feature = function(ctx, geojson) { - this.ctx = ctx; - this.properties = geojson.properties || {}; - this.coordinates = geojson.geometry.coordinates; - this.id = geojson.id || generateID(); - this.type = geojson.geometry.type; -}; - -Feature.prototype.changed = function() { - this.ctx.store.featureChanged(this.id); -}; - -Feature.prototype.incomingCoords = function(coords) { - this.setCoordinates(coords); -}; - -Feature.prototype.setCoordinates = function(coords) { - this.coordinates = coords; - this.changed(); -}; - -Feature.prototype.getCoordinates = function() { - return JSON.parse(JSON.stringify(this.coordinates)); -}; - -Feature.prototype.setProperty = function(property, value) { - this.properties[property] = value; -}; - -Feature.prototype.toGeoJSON = function() { - return JSON.parse(JSON.stringify({ - id: this.id, - type: Constants.geojsonTypes.FEATURE, - properties: this.properties, - geometry: { - coordinates: this.getCoordinates(), - type: this.type - } - })); -}; - -Feature.prototype.internal = function(mode) { - const properties = { - id: this.id, - meta: Constants.meta.FEATURE, - 'meta:type': this.type, - active: Constants.activeStates.INACTIVE, - mode - }; - - if (this.ctx.options.userProperties) { - for (const name in this.properties) { - properties[`user_${name}`] = this.properties[name]; - } - } - - return { - type: Constants.geojsonTypes.FEATURE, - properties, - geometry: { - coordinates: this.getCoordinates(), - type: this.type - } - }; -}; - -export default Feature; diff --git a/src/feature_types/feature.ts b/src/feature_types/feature.ts new file mode 100644 index 00000000..b819d7ed --- /dev/null +++ b/src/feature_types/feature.ts @@ -0,0 +1,80 @@ +import { generateID } from '../lib/id'; +import * as Constants from '../constants'; +import type { Geometry } from 'geojson'; +import type { StrictFeature, CTX } from '../types/types'; + +class Feature { + ctx: CTX; + properties: Record; + coordinates: any; + id: string; + type: Geometry['type']; + + constructor(ctx: CTX, geojson: StrictFeature) { + this.ctx = ctx; + this.properties = geojson.properties || {}; + this.coordinates = geojson.geometry.coordinates; + this.id = (geojson as any).id || generateID(); + this.type = geojson.geometry.type; + } + + changed(): void { + this.ctx.store.featureChanged(this.id); + } + + incomingCoords(coords: number[][] | number[]): void { + this.setCoordinates(coords); + } + + setCoordinates(coords: unknown): void { + this.coordinates = coords; + this.changed(); + } + + getCoordinates() { + return JSON.parse(JSON.stringify(this.coordinates)); + } + + setProperty(property: string, value: unknown): void { + this.properties[property] = value; + } + + toGeoJSON(): StrictFeature { + return { + id: this.id, + type: Constants.geojsonTypes.FEATURE as 'Feature', + properties: this.properties, + geometry: { + coordinates: this.getCoordinates(), + type: this.type as 'Point' | 'LineString' | 'Polygon' + } + }; + } + + internal(mode: string) { + const properties: Record = { + id: this.id, + meta: Constants.meta.FEATURE, + 'meta:type': this.type, + active: Constants.activeStates.INACTIVE, + mode + }; + + if (this.ctx.options.userProperties) { + for (const name in this.properties) { + properties[`user_${name}`] = this.properties[name]; + } + } + + return { + type: Constants.geojsonTypes.FEATURE, + properties, + geometry: { + coordinates: this.getCoordinates(), + type: this.type + } + }; + } +} + +export default Feature; diff --git a/src/feature_types/line_string.js b/src/feature_types/line_string.js deleted file mode 100644 index 8e23ca8c..00000000 --- a/src/feature_types/line_string.js +++ /dev/null @@ -1,35 +0,0 @@ -import Feature from './feature.js'; - -const LineString = function(ctx, geojson) { - Feature.call(this, ctx, geojson); -}; - -LineString.prototype = Object.create(Feature.prototype); - -LineString.prototype.isValid = function() { - return this.coordinates.length > 1; -}; - -LineString.prototype.addCoordinate = function(path, lng, lat) { - this.changed(); - const id = parseInt(path, 10); - this.coordinates.splice(id, 0, [lng, lat]); -}; - -LineString.prototype.getCoordinate = function(path) { - const id = parseInt(path, 10); - return JSON.parse(JSON.stringify(this.coordinates[id])); -}; - -LineString.prototype.removeCoordinate = function(path) { - this.changed(); - this.coordinates.splice(parseInt(path, 10), 1); -}; - -LineString.prototype.updateCoordinate = function(path, lng, lat) { - const id = parseInt(path, 10); - this.coordinates[id] = [lng, lat]; - this.changed(); -}; - -export default LineString; diff --git a/src/feature_types/line_string.ts b/src/feature_types/line_string.ts new file mode 100644 index 00000000..2f559f71 --- /dev/null +++ b/src/feature_types/line_string.ts @@ -0,0 +1,40 @@ +import Feature from './feature.js'; +import type { StrictFeature, CTX } from '../types/types'; + +type Coordinate = [number, number]; + +class LineString extends Feature { + constructor(ctx: CTX, geojson: StrictFeature) { + super(ctx, geojson); + } + + isValid(): boolean { + return this.coordinates.length > 1; + } + + addCoordinate(path: string | number, lng: number, lat: number): void { + this.changed(); + const id = parseInt(path as string, 10); + this.coordinates.splice(id, 0, [lng, lat]); + } + + getCoordinate(path: string | number): Coordinate | undefined { + const id = parseInt(path as string, 10); + return this.coordinates[id] + ? [...(this.coordinates[id] as Coordinate)] + : undefined; + } + + removeCoordinate(path: string | number): void { + this.changed(); + this.coordinates.splice(parseInt(path as string, 10), 1); + } + + updateCoordinate(path: string | number, lng: number, lat: number): void { + const id = parseInt(path as string, 10); + this.coordinates[id] = [lng, lat]; + this.changed(); + } +} + +export default LineString; diff --git a/src/feature_types/multi_feature.js b/src/feature_types/multi_feature.js deleted file mode 100644 index 752eca00..00000000 --- a/src/feature_types/multi_feature.js +++ /dev/null @@ -1,85 +0,0 @@ -import {generateID} from '../lib/id.js'; -import Feature from './feature.js'; -import * as Constants from '../constants.js'; - -import MultiPoint from './point.js'; -import MultiLineString from './line_string.js'; -import MultiPolygon from './polygon.js'; - -const models = { - MultiPoint, - MultiLineString, - MultiPolygon -}; - -const takeAction = (features, action, path, lng, lat) => { - const parts = path.split('.'); - const idx = parseInt(parts[0], 10); - const tail = (!parts[1]) ? null : parts.slice(1).join('.'); - return features[idx][action](tail, lng, lat); -}; - -const MultiFeature = function(ctx, geojson) { - Feature.call(this, ctx, geojson); - - delete this.coordinates; - this.model = models[geojson.geometry.type]; - if (this.model === undefined) throw new TypeError(`${geojson.geometry.type} is not a valid type`); - this.features = this._coordinatesToFeatures(geojson.geometry.coordinates); -}; - -MultiFeature.prototype = Object.create(Feature.prototype); - -MultiFeature.prototype._coordinatesToFeatures = function(coordinates) { - const Model = this.model.bind(this); - return coordinates.map(coords => new Model(this.ctx, { - id: generateID(), - type: Constants.geojsonTypes.FEATURE, - properties: {}, - geometry: { - coordinates: coords, - type: this.type.replace('Multi', '') - } - })); -}; - -MultiFeature.prototype.isValid = function() { - return this.features.every(f => f.isValid()); -}; - -MultiFeature.prototype.setCoordinates = function(coords) { - this.features = this._coordinatesToFeatures(coords); - this.changed(); -}; - -MultiFeature.prototype.getCoordinate = function(path) { - return takeAction(this.features, 'getCoordinate', path); -}; - -MultiFeature.prototype.getCoordinates = function() { - return JSON.parse(JSON.stringify(this.features.map((f) => { - if (f.type === Constants.geojsonTypes.POLYGON) return f.getCoordinates(); - return f.coordinates; - }))); -}; - -MultiFeature.prototype.updateCoordinate = function(path, lng, lat) { - takeAction(this.features, 'updateCoordinate', path, lng, lat); - this.changed(); -}; - -MultiFeature.prototype.addCoordinate = function(path, lng, lat) { - takeAction(this.features, 'addCoordinate', path, lng, lat); - this.changed(); -}; - -MultiFeature.prototype.removeCoordinate = function(path) { - takeAction(this.features, 'removeCoordinate', path); - this.changed(); -}; - -MultiFeature.prototype.getFeatures = function() { - return this.features; -}; - -export default MultiFeature; diff --git a/src/feature_types/multi_feature.ts b/src/feature_types/multi_feature.ts new file mode 100644 index 00000000..61c4c35b --- /dev/null +++ b/src/feature_types/multi_feature.ts @@ -0,0 +1,116 @@ +import { generateID } from '../lib/id'; +import Feature from './feature'; +import * as Constants from '../constants'; + +import MultiPoint from './point'; +import MultiLineString from './line_string'; +import MultiPolygon from './polygon'; + +const models = { + MultiPoint, + MultiLineString, + MultiPolygon +}; + +type FeatureType = MultiPoint | MultiLineString | MultiPolygon; +type FeatureConstructor = new (...args: any[]) => FeatureType; + +const takeAction = ( + features: FeatureType[], + action: + | 'getCoordinate' + | 'updateCoordinate' + | 'addCoordinate' + | 'removeCoordinate', + path: string, + lng?: number, + lat?: number +): any => { + const parts = path.split('.'); + const idx = parseInt(parts[0], 10); + const tail = !parts[1] ? null : parts.slice(1).join('.'); + return features[idx][action](tail, lng, lat); +}; + +class MultiFeature extends Feature { + model: FeatureConstructor; + features: FeatureType[]; + + constructor(ctx: any, geojson: any) { + super(ctx, geojson); + + delete this.coordinates; + + // Determine the model based on geojson type + this.model = models[geojson.geometry.type]; + if (this.model === undefined) { + throw new TypeError(`${geojson.geometry.type} is not a valid type`); + } + + // Initialize features + this.features = this._coordinatesToFeatures(geojson.geometry.coordinates); + } + + private _coordinatesToFeatures(coordinates: any[]): FeatureType[] { + const Model = this.model.bind(this); + return coordinates.map( + coords => + new Model(this.ctx, { + id: generateID(), + type: Constants.geojsonTypes.FEATURE, + properties: {}, + geometry: { + coordinates: coords, + type: this.type.replace('Multi', '') + } + }) + ); + } + + isValid(): boolean { + return this.features.every(f => f.isValid()); + } + + setCoordinates(coords: any[]): void { + this.features = this._coordinatesToFeatures(coords); + this.changed(); + } + + getCoordinate(path: string): any { + return takeAction(this.features, 'getCoordinate', path); + } + + getCoordinates(): any[] { + return JSON.parse( + JSON.stringify( + this.features.map(f => { + if (f.type === Constants.geojsonTypes.POLYGON) { + return f.getCoordinates(); + } + return f.coordinates; + }) + ) + ); + } + + updateCoordinate(path: string, lng: number, lat: number): void { + takeAction(this.features, 'updateCoordinate', path, lng, lat); + this.changed(); + } + + addCoordinate(path: string, lng: number, lat: number): void { + takeAction(this.features, 'addCoordinate', path, lng, lat); + this.changed(); + } + + removeCoordinate(path: string): void { + takeAction(this.features, 'removeCoordinate', path); + this.changed(); + } + + getFeatures(): FeatureType[] { + return this.features; + } +} + +export default MultiFeature; diff --git a/src/feature_types/point.js b/src/feature_types/point.js deleted file mode 100644 index f0cc3eaf..00000000 --- a/src/feature_types/point.js +++ /dev/null @@ -1,27 +0,0 @@ -import Feature from './feature.js'; - -const Point = function(ctx, geojson) { - Feature.call(this, ctx, geojson); -}; - -Point.prototype = Object.create(Feature.prototype); - -Point.prototype.isValid = function() { - return typeof this.coordinates[0] === 'number' && - typeof this.coordinates[1] === 'number'; -}; - -Point.prototype.updateCoordinate = function(pathOrLng, lngOrLat, lat) { - if (arguments.length === 3) { - this.coordinates = [lngOrLat, lat]; - } else { - this.coordinates = [pathOrLng, lngOrLat]; - } - this.changed(); -}; - -Point.prototype.getCoordinate = function() { - return this.getCoordinates(); -}; - -export default Point; diff --git a/src/feature_types/point.ts b/src/feature_types/point.ts new file mode 100644 index 00000000..a511a956 --- /dev/null +++ b/src/feature_types/point.ts @@ -0,0 +1,31 @@ +import Feature from './feature.js'; +import type { CTX, StrictFeature } from '../types/types'; + +class Point extends Feature { + constructor(ctx: CTX, geojson: StrictFeature) { + super(ctx, geojson); + this.coordinates = geojson.geometry.coordinates; + } + + isValid(): boolean { + return ( + typeof this.coordinates[0] === 'number' && + typeof this.coordinates[1] === 'number' + ); + } + + updateCoordinate(pathOrLng: number, lngOrLat: number, lat?: number): void { + if (lat !== undefined) { + this.coordinates = [lngOrLat, lat]; + } else { + this.coordinates = [pathOrLng, lngOrLat]; + } + this.changed(); + } + + getCoordinate() { + return this.getCoordinates(); + } +} + +export default Point; diff --git a/src/feature_types/polygon.js b/src/feature_types/polygon.js deleted file mode 100644 index 64297887..00000000 --- a/src/feature_types/polygon.js +++ /dev/null @@ -1,71 +0,0 @@ -import Feature from './feature.js'; - -const Polygon = function(ctx, geojson) { - Feature.call(this, ctx, geojson); - this.coordinates = this.coordinates.map(ring => ring.slice(0, -1)); -}; - -Polygon.prototype = Object.create(Feature.prototype); - -Polygon.prototype.isValid = function() { - if (this.coordinates.length === 0) return false; - return this.coordinates.every(ring => ring.length > 2); -}; - -// Expects valid geoJSON polygon geometry: first and last positions must be equivalent. -Polygon.prototype.incomingCoords = function(coords) { - this.coordinates = coords.map(ring => ring.slice(0, -1)); - this.changed(); -}; - -// Does NOT expect valid geoJSON polygon geometry: first and last positions should not be equivalent. -Polygon.prototype.setCoordinates = function(coords) { - this.coordinates = coords; - this.changed(); -}; - -Polygon.prototype.addCoordinate = function(path, lng, lat) { - this.changed(); - const ids = path.split('.').map(x => parseInt(x, 10)); - - const ring = this.coordinates[ids[0]]; - - ring.splice(ids[1], 0, [lng, lat]); -}; - -Polygon.prototype.removeCoordinate = function(path) { - this.changed(); - const ids = path.split('.').map(x => parseInt(x, 10)); - const ring = this.coordinates[ids[0]]; - if (ring) { - ring.splice(ids[1], 1); - if (ring.length < 3) { - this.coordinates.splice(ids[0], 1); - } - } -}; - -Polygon.prototype.getCoordinate = function(path) { - const ids = path.split('.').map(x => parseInt(x, 10)); - const ring = this.coordinates[ids[0]]; - return JSON.parse(JSON.stringify(ring[ids[1]])); -}; - -Polygon.prototype.getCoordinates = function() { - return this.coordinates.map(coords => coords.concat([coords[0]])); -}; - -Polygon.prototype.updateCoordinate = function(path, lng, lat) { - this.changed(); - const parts = path.split('.'); - const ringId = parseInt(parts[0], 10); - const coordId = parseInt(parts[1], 10); - - if (this.coordinates[ringId] === undefined) { - this.coordinates[ringId] = []; - } - - this.coordinates[ringId][coordId] = [lng, lat]; -}; - -export default Polygon; diff --git a/src/feature_types/polygon.ts b/src/feature_types/polygon.ts new file mode 100644 index 00000000..be65c9c8 --- /dev/null +++ b/src/feature_types/polygon.ts @@ -0,0 +1,74 @@ +import Feature from './feature.js'; +import type { CTX, Coords, StrictFeature } from '../types/types'; + +class Polygon extends Feature { + constructor(ctx: CTX, geojson: StrictFeature) { + super(ctx, geojson); + + // Ensure coordinates are properly typed and adjust them. + this.coordinates = this.coordinates.map((ring: Coords) => + ring.slice(0, -1) + ); + } + + isValid(): boolean { + if (this.coordinates.length === 0) return false; + return this.coordinates.every((ring: Coords) => ring.length > 2); + } + + // Expects valid geoJSON polygon geometry: first and last positions must be equivalent. + incomingCoords(coords): void { + this.coordinates = coords.map(ring => ring.slice(0, -1)); + this.changed(); + } + + // Does NOT expect valid geoJSON polygon geometry: first and last positions should not be equivalent. + setCoordinates(coords: Coords[]): void { + this.coordinates = coords; + this.changed(); + } + + addCoordinate(path: string, lng: number, lat: number): void { + this.changed(); + const ids = path.split('.').map((x: string) => parseInt(x, 10)); + const ring = this.coordinates[ids[0]]; + ring.splice(ids[1], 0, [lng, lat]); + } + + removeCoordinate(path: string): void { + this.changed(); + const ids = path.split('.').map((x: string) => parseInt(x, 10)); + const ring = this.coordinates[ids[0]]; + if (ring) { + ring.splice(ids[1], 1); + if (ring.length < 3) { + this.coordinates.splice(ids[0], 1); + } + } + } + + getCoordinate(path: string): Coords { + const ids = path.split('.').map((x: string) => parseInt(x, 10)); + const ring = this.coordinates[ids[0]]; + return JSON.parse(JSON.stringify(ring[ids[1]])); + } + + getCoordinates(): Coords[] { + return this.coordinates.map((coords: Coords) => coords.concat([coords[0]])); + } + + updateCoordinate(path: string, lng: number, lat: number): void { + this.changed(); + const parts = path.split('.'); + const ringId = parseInt(parts[0], 10); + const coordId = parseInt(parts[1], 10); + + if (this.coordinates[ringId] === undefined) { + this.coordinates[ringId] = []; + } + + this.coordinates[ringId][coordId] = [lng, lat]; + } +} + +export default Polygon; diff --git a/src/lib/common_selectors.js b/src/lib/common_selectors.js deleted file mode 100644 index 3339f688..00000000 --- a/src/lib/common_selectors.js +++ /dev/null @@ -1,64 +0,0 @@ -import * as Constants from '../constants.js'; - -export function isOfMetaType(type) { - return function(e) { - const featureTarget = e.featureTarget; - if (!featureTarget) return false; - if (!featureTarget.properties) return false; - return featureTarget.properties.meta === type; - }; -} - -export function isShiftMousedown(e) { - if (!e.originalEvent) return false; - if (!e.originalEvent.shiftKey) return false; - return e.originalEvent.button === 0; -} - -export function isActiveFeature(e) { - if (!e.featureTarget) return false; - if (!e.featureTarget.properties) return false; - return e.featureTarget.properties.active === Constants.activeStates.ACTIVE && - e.featureTarget.properties.meta === Constants.meta.FEATURE; -} - -export function isInactiveFeature(e) { - if (!e.featureTarget) return false; - if (!e.featureTarget.properties) return false; - return e.featureTarget.properties.active === Constants.activeStates.INACTIVE && - e.featureTarget.properties.meta === Constants.meta.FEATURE; -} - -export function noTarget(e) { - return e.featureTarget === undefined; -} - -export function isFeature(e) { - if (!e.featureTarget) return false; - if (!e.featureTarget.properties) return false; - return e.featureTarget.properties.meta === Constants.meta.FEATURE; -} - -export function isVertex(e) { - const featureTarget = e.featureTarget; - if (!featureTarget) return false; - if (!featureTarget.properties) return false; - return featureTarget.properties.meta === Constants.meta.VERTEX; -} - -export function isShiftDown(e) { - if (!e.originalEvent) return false; - return e.originalEvent.shiftKey === true; -} - -export function isEscapeKey(e) { - return e.keyCode === 27; -} - -export function isEnterKey(e) { - return e.keyCode === 13; -} - -export function isTrue() { - return true; -} diff --git a/src/lib/common_selectors.ts b/src/lib/common_selectors.ts new file mode 100644 index 00000000..2135dd5d --- /dev/null +++ b/src/lib/common_selectors.ts @@ -0,0 +1,63 @@ +import * as Constants from '../constants'; +import { MapMouseEvent, MapTouchEvent } from '../types/types'; + +type E = MapMouseEvent | MapTouchEvent; + +export const isOfMetaType = (type: string) => { + return (e: E) => { + const featureTarget = e.featureTarget; + if (!featureTarget) return false; + if (!featureTarget.properties) return false; + return featureTarget.properties.meta === type; + }; +}; + +export const isShiftMousedown = (e: MapMouseEvent) => { + if (!e.originalEvent) return false; + if (!e.originalEvent.shiftKey) return false; + return e.originalEvent.button === 0; +}; + +export const isActiveFeature = (e: E) => { + if (!e.featureTarget) return false; + if (!e.featureTarget.properties) return false; + return ( + e.featureTarget.properties.active === Constants.activeStates.ACTIVE && + e.featureTarget.properties.meta === Constants.meta.FEATURE + ); +}; + +export const isInactiveFeature = (e: E) => { + if (!e.featureTarget) return false; + if (!e.featureTarget.properties) return false; + return ( + e.featureTarget.properties.active === Constants.activeStates.INACTIVE && + e.featureTarget.properties.meta === Constants.meta.FEATURE + ); +} + +export const noTarget = (e: E) => { + return e.featureTarget === undefined; +}; + +export const isFeature = (e: E) => { + if (!e.featureTarget) return false; + if (!e.featureTarget.properties) return false; + return e.featureTarget.properties.meta === Constants.meta.FEATURE; +}; + +export const isVertex = (e: E) => { + const featureTarget = e.featureTarget; + if (!featureTarget) return false; + if (!featureTarget.properties) return false; + return featureTarget.properties.meta === Constants.meta.VERTEX; +}; + +export const isShiftDown = (e: MapMouseEvent) => { + if (!e.originalEvent) return false; + return e.originalEvent.shiftKey === true; +}; + +export const isEscapeKey = (e: KeyboardEvent) => e.key === 'Escape'; +export const isEnterKey = (e: KeyboardEvent) => e.key === 'Enter'; +export const isTrue = () => true; diff --git a/src/lib/constrain_feature_movement.js b/src/lib/constrain_feature_movement.ts similarity index 79% rename from src/lib/constrain_feature_movement.js rename to src/lib/constrain_feature_movement.ts index 64a36379..2948fbb0 100644 --- a/src/lib/constrain_feature_movement.js +++ b/src/lib/constrain_feature_movement.ts @@ -1,4 +1,7 @@ -import * as Constants from '../constants.js'; +import * as Constants from '../constants'; +import type { StrictFeature } from '../types/types'; + +type Delta = { lng: number; lat: number; } const { LAT_MIN, @@ -6,23 +9,23 @@ const { LAT_RENDERED_MIN, LAT_RENDERED_MAX, LNG_MIN, - LNG_MAX, + LNG_MAX } = Constants; -function extent(feature) { +function extent(feature: StrictFeature) { const depth = { Point: 0, LineString: 1, Polygon: 2, MultiPoint: 1, MultiLineString: 2, - MultiPolygon: 3, + MultiPolygon: 3 }[feature.geometry.type]; const coords = [feature.geometry.coordinates].flat(depth); const lngs = coords.map(coord => coord[0]); const lats = coords.map(coord => coord[1]); - const min = vals => Math.min.apply(null, vals); - const max = vals => Math.max.apply(null, vals); + const min = (vals: Array) => Math.min.apply(null, vals); + const max = (vals: Array) => Math.max.apply(null, vals); return [min(lngs), min(lats), max(lngs), max(lats)]; } @@ -30,7 +33,7 @@ function extent(feature) { // - any part of any feature to exceed the poles // - any feature to be completely lost in the space between the projection's // edge and the poles, such that it couldn't be re-selected and moved back -export default function(geojsonFeatures, delta) { +export const constrainFeatureMovement = (geojsonFeatures: Array, delta: Delta) => { // "inner edge" = a feature's latitude closest to the equator let northInnerEdge = LAT_MIN; let southInnerEdge = LAT_MAX; @@ -41,7 +44,7 @@ export default function(geojsonFeatures, delta) { let westEdge = LNG_MAX; let eastEdge = LNG_MIN; - geojsonFeatures.forEach((feature) => { + geojsonFeatures.forEach(feature => { const bounds = extent(feature); const featureSouthEdge = bounds[1]; const featureNorthEdge = bounds[3]; @@ -55,7 +58,6 @@ export default function(geojsonFeatures, delta) { if (featureEastEdge > eastEdge) eastEdge = featureEastEdge; }); - // These changes are not mutually exclusive: we might hit the inner // edge but also have hit the outer edge and therefore need // another readjustment @@ -73,10 +75,12 @@ export default function(geojsonFeatures, delta) { constrainedDelta.lat = LAT_MIN - southOuterEdge; } if (westEdge + constrainedDelta.lng <= LNG_MIN) { - constrainedDelta.lng += Math.ceil(Math.abs(constrainedDelta.lng) / 360) * 360; + constrainedDelta.lng += + Math.ceil(Math.abs(constrainedDelta.lng) / 360) * 360; } if (eastEdge + constrainedDelta.lng >= LNG_MAX) { - constrainedDelta.lng -= Math.ceil(Math.abs(constrainedDelta.lng) / 360) * 360; + constrainedDelta.lng -= + Math.ceil(Math.abs(constrainedDelta.lng) / 360) * 360; } return constrainedDelta; diff --git a/src/lib/create_midpoint.js b/src/lib/create_midpoint.ts similarity index 50% rename from src/lib/create_midpoint.js rename to src/lib/create_midpoint.ts index a6a5e63b..b50e5c84 100644 --- a/src/lib/create_midpoint.js +++ b/src/lib/create_midpoint.ts @@ -1,15 +1,22 @@ -import * as Constants from '../constants.js'; +import * as Constants from '../constants'; +import type { Feature, Point } from 'geojson'; -export default function(parent, startVertex, endVertex) { - const startCoord = startVertex.geometry.coordinates; - const endCoord = endVertex.geometry.coordinates; +export const createMidPoint = ( + parent: string, + startVertex: Feature, + endVertex: Feature +): Feature => { + const startCoord = (startVertex.geometry as Point).coordinates; + const endCoord = (endVertex.geometry as Point).coordinates; // If a coordinate exceeds the projection, we can't calculate a midpoint, // so run away - if (startCoord[1] > Constants.LAT_RENDERED_MAX || + if ( + startCoord[1] > Constants.LAT_RENDERED_MAX || startCoord[1] < Constants.LAT_RENDERED_MIN || endCoord[1] > Constants.LAT_RENDERED_MAX || - endCoord[1] < Constants.LAT_RENDERED_MIN) { + endCoord[1] < Constants.LAT_RENDERED_MIN + ) { return null; } @@ -19,7 +26,7 @@ export default function(parent, startVertex, endVertex) { }; return { - type: Constants.geojsonTypes.FEATURE, + type: Constants.geojsonTypes.FEATURE as 'Feature', properties: { meta: Constants.meta.MIDPOINT, parent, @@ -28,8 +35,8 @@ export default function(parent, startVertex, endVertex) { coord_path: endVertex.properties.coord_path }, geometry: { - type: Constants.geojsonTypes.POINT, + type: Constants.geojsonTypes.POINT as 'Point', coordinates: [mid.lng, mid.lat] } }; -} +}; diff --git a/src/lib/create_supplementary_points.js b/src/lib/create_supplementary_points.ts similarity index 54% rename from src/lib/create_supplementary_points.js rename to src/lib/create_supplementary_points.ts index 2a22567b..0ad8c4ba 100644 --- a/src/lib/create_supplementary_points.js +++ b/src/lib/create_supplementary_points.ts @@ -1,40 +1,63 @@ -import createVertex from './create_vertex.js'; -import createMidpoint from './create_midpoint.js'; -import * as Constants from '../constants.js'; +import { createVertex } from './create_vertex'; +import { createMidPoint } from './create_midpoint'; +import * as Constants from '../constants'; +import type { Coords, StrictFeature } from '../types/types'; +import type { Map } from 'mapbox-gl'; -function createSupplementaryPoints(geojson, options = {}, basePath = null) { +interface Options { + map?: Map; + midpoints?: boolean; + selectedPaths?: string; +} + +export const createSupplementaryPoints = (geojson: StrictFeature, options: Options = {}, basePath = null) => { const { type, coordinates } = geojson.geometry; - const featureId = geojson.properties && geojson.properties.id; + const featureId = geojson.properties && geojson.properties.id as string; let supplementaryPoints = []; if (type === Constants.geojsonTypes.POINT) { // For points, just create a vertex - supplementaryPoints.push(createVertex(featureId, coordinates, basePath, isSelectedPath(basePath))); + supplementaryPoints.push( + createVertex(featureId, coordinates as [number, number], basePath, isSelectedPath(basePath)) + ); } else if (type === Constants.geojsonTypes.POLYGON) { + const lineCoordinates = coordinates as Array; + // Cycle through a Polygon's rings and // process each line - coordinates.forEach((line, lineIndex) => { - processLine(line, (basePath !== null) ? `${basePath}.${lineIndex}` : String(lineIndex)); + lineCoordinates.forEach((line, lineIndex) => { + processLine( + line, + basePath !== null ? `${basePath}.${lineIndex}` : String(lineIndex) + ); }); } else if (type === Constants.geojsonTypes.LINE_STRING) { - processLine(coordinates, basePath); + processLine(coordinates as Coords, basePath); } else if (type.indexOf(Constants.geojsonTypes.MULTI_PREFIX) === 0) { processMultiGeometry(); } - function processLine(line, lineBasePath) { + function processLine(line: Coords, lineBasePath?: string) { let firstPointString = ''; let lastVertex = null; line.forEach((point, pointIndex) => { - const pointPath = (lineBasePath !== undefined && lineBasePath !== null) ? `${lineBasePath}.${pointIndex}` : String(pointIndex); - const vertex = createVertex(featureId, point, pointPath, isSelectedPath(pointPath)); + const pointPath = + lineBasePath !== undefined && lineBasePath !== null + ? `${lineBasePath}.${pointIndex}` + : String(pointIndex); + const vertex = createVertex( + featureId, + point, + pointPath, + isSelectedPath(pointPath) + ); // If we're creating midpoints, check if there was a // vertex before this one. If so, add a midpoint // between that vertex and this one. if (options.midpoints && lastVertex) { - const midpoint = createMidpoint(featureId, lastVertex, vertex); + const midpoint = createMidPoint(featureId, lastVertex, vertex); if (midpoint) { supplementaryPoints.push(midpoint); } @@ -54,7 +77,7 @@ function createSupplementaryPoints(geojson, options = {}, basePath = null) { }); } - function isSelectedPath(path) { + function isSelectedPath(path: string) { if (!options.selectedPaths) return false; return options.selectedPaths.indexOf(path) !== -1; } @@ -63,8 +86,9 @@ function createSupplementaryPoints(geojson, options = {}, basePath = null) { // geometries, and accumulate the supplementary points // for each of those constituents function processMultiGeometry() { + const lineCoordinates = coordinates as Array; const subType = type.replace(Constants.geojsonTypes.MULTI_PREFIX, ''); - coordinates.forEach((subCoordinates, index) => { + lineCoordinates.forEach((subCoordinates, index) => { const subFeature = { type: Constants.geojsonTypes.FEATURE, properties: geojson.properties, @@ -72,12 +96,12 @@ function createSupplementaryPoints(geojson, options = {}, basePath = null) { type: subType, coordinates: subCoordinates } - }; - supplementaryPoints = supplementaryPoints.concat(createSupplementaryPoints(subFeature, options, index)); + } as StrictFeature; + supplementaryPoints = supplementaryPoints.concat( + createSupplementaryPoints(subFeature, options, index) + ); }); } return supplementaryPoints; -} - -export default createSupplementaryPoints; +}; diff --git a/src/lib/create_vertex.js b/src/lib/create_vertex.js deleted file mode 100644 index 048be7ec..00000000 --- a/src/lib/create_vertex.js +++ /dev/null @@ -1,29 +0,0 @@ -import * as Constants from '../constants.js'; - -/** - * Returns GeoJSON for a Point representing the - * vertex of another feature. - * - * @param {string} parentId - * @param {Array} coordinates - * @param {string} path - Dot-separated numbers indicating exactly - * where the point exists within its parent feature's coordinates. - * @param {boolean} selected - * @return {GeoJSON} Point - */ - -export default function(parentId, coordinates, path, selected) { - return { - type: Constants.geojsonTypes.FEATURE, - properties: { - meta: Constants.meta.VERTEX, - parent: parentId, - coord_path: path, - active: (selected) ? Constants.activeStates.ACTIVE : Constants.activeStates.INACTIVE - }, - geometry: { - type: Constants.geojsonTypes.POINT, - coordinates - } - }; -} diff --git a/src/lib/create_vertex.ts b/src/lib/create_vertex.ts new file mode 100644 index 00000000..31b523a7 --- /dev/null +++ b/src/lib/create_vertex.ts @@ -0,0 +1,25 @@ +import * as Constants from '../constants'; +import type { Feature } from 'geojson'; + +export const createVertex = ( + parentId: string, + coordinates: [number, number], + path: string, + selected: boolean +): Feature => { + return { + type: Constants.geojsonTypes.FEATURE as 'Feature', + properties: { + meta: Constants.meta.VERTEX, + parent: parentId, + coord_path: path, + active: selected + ? Constants.activeStates.ACTIVE + : Constants.activeStates.INACTIVE + }, + geometry: { + type: Constants.geojsonTypes.POINT as 'Point', + coordinates + } + }; +}; diff --git a/src/lib/double_click_zoom.js b/src/lib/double_click_zoom.ts similarity index 65% rename from src/lib/double_click_zoom.js rename to src/lib/double_click_zoom.ts index 7bf3ca48..b41704a8 100644 --- a/src/lib/double_click_zoom.js +++ b/src/lib/double_click_zoom.ts @@ -1,14 +1,21 @@ -export default { - enable(ctx) { +export const doubleClickZoom = { + enable: (ctx) => { setTimeout(() => { // First check we've got a map and some context. - if (!ctx.map || !ctx.map.doubleClickZoom || !ctx._ctx || !ctx._ctx.store || !ctx._ctx.store.getInitialConfigValue) return; + if ( + !ctx.map || + !ctx.map.doubleClickZoom || + !ctx._ctx || + !ctx._ctx.store || + !ctx._ctx.store.getInitialConfigValue + ) + return; // Now check initial state wasn't false (we leave it disabled if so) if (!ctx._ctx.store.getInitialConfigValue('doubleClickZoom')) return; ctx.map.doubleClickZoom.enable(); }, 0); }, - disable(ctx) { + disable: (ctx) => { setTimeout(() => { if (!ctx.map || !ctx.map.doubleClickZoom) return; // Always disable here, as it's necessary in some cases. diff --git a/src/lib/euclidean_distance.js b/src/lib/euclidean_distance.js deleted file mode 100644 index 440fd237..00000000 --- a/src/lib/euclidean_distance.js +++ /dev/null @@ -1,5 +0,0 @@ -export default function(a, b) { - const x = a.x - b.x; - const y = a.y - b.y; - return Math.sqrt((x * x) + (y * y)); -} diff --git a/src/lib/euclidean_distance.ts b/src/lib/euclidean_distance.ts new file mode 100644 index 00000000..44f094da --- /dev/null +++ b/src/lib/euclidean_distance.ts @@ -0,0 +1,7 @@ +import type { XY } from '../types/types'; + +export const euclideanDistance = (a: XY, b: XY) => { + const x = a.x - b.x; + const y = a.y - b.y; + return Math.sqrt(x * x + y * y); +}; diff --git a/src/lib/features_at.js b/src/lib/features_at.js deleted file mode 100644 index 1ca36b97..00000000 --- a/src/lib/features_at.js +++ /dev/null @@ -1,48 +0,0 @@ -import sortFeatures from './sort_features.js'; -import mapEventToBoundingBox from './map_event_to_bounding_box.js'; -import * as Constants from '../constants.js'; -import StringSet from './string_set.js'; - -const META_TYPES = [ - Constants.meta.FEATURE, - Constants.meta.MIDPOINT, - Constants.meta.VERTEX -]; - -// Requires either event or bbox -export default { - click: featuresAtClick, - touch: featuresAtTouch -}; - -function featuresAtClick(event, bbox, ctx) { - return featuresAt(event, bbox, ctx, ctx.options.clickBuffer); -} - -function featuresAtTouch(event, bbox, ctx) { - return featuresAt(event, bbox, ctx, ctx.options.touchBuffer); -} - -function featuresAt(event, bbox, ctx, buffer) { - if (ctx.map === null) return []; - - const box = (event) ? mapEventToBoundingBox(event, buffer) : bbox; - - const queryParams = {}; - - if (ctx.options.styles) queryParams.layers = ctx.options.styles.map(s => s.id).filter(id => ctx.map.getLayer(id) != null); - - const features = ctx.map.queryRenderedFeatures(box, queryParams) - .filter(feature => META_TYPES.indexOf(feature.properties.meta) !== -1); - - const featureIds = new StringSet(); - const uniqueFeatures = []; - features.forEach((feature) => { - const featureId = feature.properties.id; - if (featureIds.has(featureId)) return; - featureIds.add(featureId); - uniqueFeatures.push(feature); - }); - - return sortFeatures(uniqueFeatures); -} diff --git a/src/lib/features_at.ts b/src/lib/features_at.ts new file mode 100644 index 00000000..cca2ce8c --- /dev/null +++ b/src/lib/features_at.ts @@ -0,0 +1,56 @@ +import { sortFeatures } from './sort_features'; +import { mapEventToBoundingBox } from './map_event_to_bounding_box'; +import * as Constants from '../constants'; +import StringSet from './string_set'; + +import type { CTX, MapMouseEvent, MapTouchEvent } from '../types/types'; + +type E = MapMouseEvent | MapTouchEvent; + +const META_TYPES = [ + Constants.meta.FEATURE, + Constants.meta.MIDPOINT, + Constants.meta.VERTEX +]; + +const featuresAt = (event: E, bbox, ctx: CTX, buffer: number) => { + if (ctx.map === null) return []; + + const box = event ? mapEventToBoundingBox(event, buffer) : bbox; + + const queryParams: { + layers?: Array; + } = {}; + + if (ctx.options.styles) + queryParams.layers = ctx.options.styles + .map(s => s.id) + .filter(id => ctx.map.getLayer(id) != null); + + const features = ctx.map + .queryRenderedFeatures(box, queryParams) + .filter(feature => META_TYPES.indexOf(feature?.properties?.meta) !== -1); + + const featureIds = new StringSet(); + const uniqueFeatures = []; + features.forEach(feature => { + const featureId = feature.properties?.id; + if (featureIds.has(featureId)) return; + featureIds.add(featureId); + uniqueFeatures.push(feature); + }); + + return sortFeatures(uniqueFeatures); +}; + +function featuresAtClick(event: E, bbox, ctx: CTX) { + return featuresAt(event, bbox, ctx, ctx.options.clickBuffer); +} + +function featuresAtTouch(event: E, bbox, ctx: CTX) { + return featuresAt(event, bbox, ctx, ctx.options.touchBuffer); +} + +// Requires either event or bbox +export const click = featuresAtClick; +export const touch = featuresAtTouch; diff --git a/src/lib/get_features_and_set_cursor.js b/src/lib/get_features_and_set_cursor.js deleted file mode 100644 index f3a4ab61..00000000 --- a/src/lib/get_features_and_set_cursor.js +++ /dev/null @@ -1,22 +0,0 @@ -import featuresAt from './features_at.js'; -import * as Constants from '../constants.js'; - -export default function getFeatureAtAndSetCursors(event, ctx) { - const features = featuresAt.click(event, null, ctx); - const classes = { mouse: Constants.cursors.NONE }; - - if (features[0]) { - classes.mouse = (features[0].properties.active === Constants.activeStates.ACTIVE) ? - Constants.cursors.MOVE : Constants.cursors.POINTER; - classes.feature = features[0].properties.meta; - } - - if (ctx.events.currentModeName().indexOf('draw') !== -1) { - classes.mouse = Constants.cursors.ADD; - } - - ctx.ui.queueMapClasses(classes); - ctx.ui.updateMapClasses(); - - return features[0]; -} diff --git a/src/lib/get_features_at_and_set_cursor.ts b/src/lib/get_features_at_and_set_cursor.ts new file mode 100644 index 00000000..96cacf5c --- /dev/null +++ b/src/lib/get_features_at_and_set_cursor.ts @@ -0,0 +1,27 @@ +import * as featuresAt from './features_at'; +import * as Constants from '../constants'; +import type { CTX, DrawMapClasses, MapMouseEvent, MapTouchEvent } from '../types/types'; + +type E = MapMouseEvent | MapTouchEvent; + +export const getFeatureAtAndSetCursors = (event: E, ctx: CTX) => { + const features = featuresAt.click(event, null, ctx); + const classes: DrawMapClasses = { mouse: Constants.cursors.NONE }; + + if (features[0]) { + classes.mouse = + features[0].properties.active === Constants.activeStates.ACTIVE + ? Constants.cursors.MOVE + : Constants.cursors.POINTER; + classes.feature = features[0].properties.meta; + } + + if (ctx.events.currentModeName().indexOf('draw') !== -1) { + classes.mouse = Constants.cursors.ADD; + } + + ctx.ui.queueMapClasses(classes); + ctx.ui.updateMapClasses(); + + return features[0]; +}; diff --git a/src/lib/id.js b/src/lib/id.js deleted file mode 100644 index eaf080a9..00000000 --- a/src/lib/id.js +++ /dev/null @@ -1,7 +0,0 @@ -import {customAlphabet} from 'nanoid/non-secure'; - -const nanoid = customAlphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', 32); - -export function generateID() { - return nanoid(); -} diff --git a/src/lib/id.ts b/src/lib/id.ts new file mode 100644 index 00000000..f8e6ca33 --- /dev/null +++ b/src/lib/id.ts @@ -0,0 +1,10 @@ +import { customAlphabet } from 'nanoid/non-secure'; + +const nanoid = customAlphabet( + '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', + 32 +); + +export function generateID() { + return nanoid(); +} diff --git a/src/lib/index.js b/src/lib/index.js deleted file mode 100644 index dd881099..00000000 --- a/src/lib/index.js +++ /dev/null @@ -1,43 +0,0 @@ -import * as CommonSelectors from "./common_selectors.js"; -import constrainFeatureMovement from "./constrain_feature_movement.js"; -import createMidPoint from "./create_midpoint.js"; -import createSupplementaryPoints from "./create_supplementary_points.js"; -import createVertex from "./create_vertex.js"; -import doubleClickZoom from "./double_click_zoom.js"; -import euclideanDistance from "./euclidean_distance.js"; -import featuresAt from "./features_at.js"; -import getFeatureAtAndSetCursors from "./get_features_and_set_cursor.js"; -import isClick from "./is_click.js"; -import isEventAtCoordinates from "./is_event_at_coordinates.js"; -import isTap from "./is_tap.js"; -import mapEventToBoundingBox from "./map_event_to_bounding_box.js"; -import ModeHandler from "./mode_handler.js"; -import moveFeatures from "./move_features.js"; -import sortFeatures from "./sort_features.js"; -import StringSet from "./string_set.js"; -import stringSetsAreEqual from "./string_sets_are_equal.js"; -import theme from "./theme.js"; -import toDenseArray from "./to_dense_array.js"; - -export { - CommonSelectors, - constrainFeatureMovement, - createMidPoint, - createSupplementaryPoints, - createVertex, - doubleClickZoom, - euclideanDistance, - featuresAt, - getFeatureAtAndSetCursors, - isClick, - isEventAtCoordinates, - isTap, - mapEventToBoundingBox, - ModeHandler, - moveFeatures, - sortFeatures, - stringSetsAreEqual, - StringSet, - theme, - toDenseArray, -}; diff --git a/src/lib/index.ts b/src/lib/index.ts new file mode 100644 index 00000000..a37cdcbb --- /dev/null +++ b/src/lib/index.ts @@ -0,0 +1,43 @@ +import * as CommonSelectors from './common_selectors'; +import { constrainFeatureMovement } from './constrain_feature_movement'; +import { createMidPoint } from './create_midpoint'; +import { createSupplementaryPoints } from './create_supplementary_points'; +import { createVertex } from './create_vertex'; +import { doubleClickZoom } from './double_click_zoom'; +import { euclideanDistance } from './euclidean_distance'; +import * as featuresAt from './features_at'; +import { getFeatureAtAndSetCursors } from './get_features_at_and_set_cursor'; +import { isClick } from './is_click'; +import { isEventAtCoordinates } from './is_event_at_coordinates'; +import { isTap } from './is_tap'; +import { sortFeatures } from './sort_features'; +import { mapEventToBoundingBox } from './map_event_to_bounding_box'; +import ModeHandler from './mode_handler'; +import { moveFeatures } from './move_features'; +import StringSet from './string_set'; +import { stringSetsAreEqual } from './string_sets_are_equal'; +import theme from './theme'; +import { toDenseArray } from './to_dense_array'; + +export { + CommonSelectors, + constrainFeatureMovement, + createMidPoint, + createSupplementaryPoints, + createVertex, + doubleClickZoom, + euclideanDistance, + featuresAt, + getFeatureAtAndSetCursors, + isClick, + isEventAtCoordinates, + isTap, + mapEventToBoundingBox, + ModeHandler, + moveFeatures, + sortFeatures, + stringSetsAreEqual, + StringSet, + theme, + toDenseArray +}; diff --git a/src/lib/is_click.js b/src/lib/is_click.js deleted file mode 100644 index 06e6a235..00000000 --- a/src/lib/is_click.js +++ /dev/null @@ -1,18 +0,0 @@ -import euclideanDistance from './euclidean_distance.js'; - -const FINE_TOLERANCE = 4; -const GROSS_TOLERANCE = 12; -const INTERVAL = 500; - -export default function isClick(start, end, options = {}) { - const fineTolerance = (options.fineTolerance != null) ? options.fineTolerance : FINE_TOLERANCE; - const grossTolerance = (options.grossTolerance != null) ? options.grossTolerance : GROSS_TOLERANCE; - const interval = (options.interval != null) ? options.interval : INTERVAL; - - start.point = start.point || end.point; - start.time = start.time || end.time; - const moveDistance = euclideanDistance(start.point, end.point); - - return moveDistance < fineTolerance || - (moveDistance < grossTolerance && (end.time - start.time) < interval); -} diff --git a/src/lib/is_click.ts b/src/lib/is_click.ts new file mode 100644 index 00000000..0351c569 --- /dev/null +++ b/src/lib/is_click.ts @@ -0,0 +1,32 @@ +import { euclideanDistance } from './euclidean_distance.js'; +import type { Entry } from '../types/types'; + +interface Options { + fineTolerance: number; + grossTolerance: number; + interval: number; +} + +export const isClick = ( + start: Entry, + end: Entry, + options: Partial = {} +) => { + const { fineTolerance = 4, grossTolerance = 12, interval = 500 } = options; + + const adjustedStart = { + point: start.point || end.point, + time: start.time || end.time + }; + + if (adjustedStart.time === undefined || end.time === undefined) { + return false; + } + + const moveDistance = euclideanDistance(adjustedStart.point, end.point); + + return ( + moveDistance < fineTolerance || + (moveDistance < grossTolerance && end.time - adjustedStart.time < interval) + ); +} diff --git a/src/lib/is_event_at_coordinates.js b/src/lib/is_event_at_coordinates.js deleted file mode 100644 index 790e0a49..00000000 --- a/src/lib/is_event_at_coordinates.js +++ /dev/null @@ -1,6 +0,0 @@ -function isEventAtCoordinates(event, coordinates) { - if (!event.lngLat) return false; - return event.lngLat.lng === coordinates[0] && event.lngLat.lat === coordinates[1]; -} - -export default isEventAtCoordinates; diff --git a/src/lib/is_event_at_coordinates.ts b/src/lib/is_event_at_coordinates.ts new file mode 100644 index 00000000..b7286190 --- /dev/null +++ b/src/lib/is_event_at_coordinates.ts @@ -0,0 +1,10 @@ +export const isEventAtCoordinates = ( + event: { lngLat: { lng: number; lat: number } }, + coordinates: [number, number], + tolerance = 1e-6 // Default precision tolerance +): boolean => { + return ( + Math.abs(event.lngLat.lng - coordinates[0]) < tolerance && + Math.abs(event.lngLat.lat - coordinates[1]) < tolerance + ); +}; diff --git a/src/lib/is_tap.js b/src/lib/is_tap.js deleted file mode 100644 index 55e20c48..00000000 --- a/src/lib/is_tap.js +++ /dev/null @@ -1,15 +0,0 @@ -import euclideanDistance from './euclidean_distance.js'; - -export const TAP_TOLERANCE = 25; -export const TAP_INTERVAL = 250; - -export default function isTap(start, end, options = {}) { - const tolerance = (options.tolerance != null) ? options.tolerance : TAP_TOLERANCE; - const interval = (options.interval != null) ? options.interval : TAP_INTERVAL; - - start.point = start.point || end.point; - start.time = start.time || end.time; - const moveDistance = euclideanDistance(start.point, end.point); - - return moveDistance < tolerance && (end.time - start.time) < interval; -} diff --git a/src/lib/is_tap.ts b/src/lib/is_tap.ts new file mode 100644 index 00000000..2abcfa8c --- /dev/null +++ b/src/lib/is_tap.ts @@ -0,0 +1,25 @@ +import { euclideanDistance } from './euclidean_distance.js'; +import type { Entry } from '../types/types'; + +interface Options { + tolerance?: number; + interval?: number; +} + +export const TAP_TOLERANCE = 25; +export const TAP_INTERVAL = 250; + +export const isTap = ( + start: Entry, + end: Entry, + options: Options = {} +): boolean => { + const { tolerance = TAP_TOLERANCE, interval = TAP_INTERVAL } = options; + + start.point = start.point || end.point; + start.time = start.time || end.time; + const moveDistance = euclideanDistance(start.point, end.point); + + return moveDistance < tolerance && end.time - start.time < interval; +}; + diff --git a/src/lib/map_event_to_bounding_box.js b/src/lib/map_event_to_bounding_box.js deleted file mode 100644 index b4793ae7..00000000 --- a/src/lib/map_event_to_bounding_box.js +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Returns a bounding box representing the event's location. - * - * @param {Event} mapEvent - Mapbox GL JS map event, with a point properties. - * @return {Array>} Bounding box. - */ -function mapEventToBoundingBox(mapEvent, buffer = 0) { - return [ - [mapEvent.point.x - buffer, mapEvent.point.y - buffer], - [mapEvent.point.x + buffer, mapEvent.point.y + buffer] - ]; -} - -export default mapEventToBoundingBox; diff --git a/src/lib/map_event_to_bounding_box.ts b/src/lib/map_event_to_bounding_box.ts new file mode 100644 index 00000000..25f52cb5 --- /dev/null +++ b/src/lib/map_event_to_bounding_box.ts @@ -0,0 +1,12 @@ +import type { Position } from 'geojson'; +import type { MapMouseEvent, MapTouchEvent } from '../types/types'; + +export const mapEventToBoundingBox = ( + mapEvent: MapMouseEvent | MapTouchEvent, + buffer: number = 0 +): Position[] => { + return [ + [mapEvent.point.x - buffer, mapEvent.point.y - buffer], + [mapEvent.point.x + buffer, mapEvent.point.y + buffer] + ]; +} diff --git a/src/lib/mode_handler.js b/src/lib/mode_handler.ts similarity index 97% rename from src/lib/mode_handler.js rename to src/lib/mode_handler.ts index b382471d..7c499f7a 100644 --- a/src/lib/mode_handler.js +++ b/src/lib/mode_handler.ts @@ -1,6 +1,4 @@ - -const ModeHandler = function(mode, DrawContext) { - +const ModeHandler = function (mode, DrawContext) { const handlers = { drag: [], click: [], diff --git a/src/lib/mouse_event_point.js b/src/lib/mouse_event_point.js deleted file mode 100644 index a6b70f4a..00000000 --- a/src/lib/mouse_event_point.js +++ /dev/null @@ -1,19 +0,0 @@ -import Point from '@mapbox/point-geometry'; - -/** - * Returns a Point representing a mouse event's position - * relative to a containing element. - * - * @param {MouseEvent} mouseEvent - * @param {Node} container - * @returns {Point} - */ -function mouseEventPoint(mouseEvent, container) { - const rect = container.getBoundingClientRect(); - return new Point( - mouseEvent.clientX - rect.left - (container.clientLeft || 0), - mouseEvent.clientY - rect.top - (container.clientTop || 0) - ); -} - -export default mouseEventPoint; diff --git a/src/lib/mouse_event_point.ts b/src/lib/mouse_event_point.ts new file mode 100644 index 00000000..fc3a8f1e --- /dev/null +++ b/src/lib/mouse_event_point.ts @@ -0,0 +1,13 @@ +import Point from '@mapbox/point-geometry'; +import type { PointLike } from 'mapbox-gl'; + +export const mouseEventPoint = ( + mouseEvent: MouseEvent, + container: HTMLElement +): PointLike => { + const rect = container.getBoundingClientRect(); + return new Point( + mouseEvent.clientX - rect.left - (container.clientLeft || 0), + mouseEvent.clientY - rect.top - (container.clientTop || 0) + ); +} diff --git a/src/lib/move_features.js b/src/lib/move_features.ts similarity index 56% rename from src/lib/move_features.js rename to src/lib/move_features.ts index 72513b7f..48abea86 100644 --- a/src/lib/move_features.js +++ b/src/lib/move_features.ts @@ -1,13 +1,16 @@ -import constrainFeatureMovement from './constrain_feature_movement.js'; -import * as Constants from '../constants.js'; +import { constrainFeatureMovement } from './constrain_feature_movement'; +import * as Constants from '../constants'; -export default function(features, delta) { - const constrainedDelta = constrainFeatureMovement(features.map(feature => feature.toGeoJSON()), delta); +export const moveFeatures = (features, delta) => { + const constrainedDelta = constrainFeatureMovement( + features.map(feature => feature.toGeoJSON()), + delta + ); - features.forEach((feature) => { + features.forEach(feature => { const currentCoordinates = feature.getCoordinates(); - const moveCoordinate = (coord) => { + const moveCoordinate = coord => { const point = { lng: coord[0] + constrainedDelta.lng, lat: coord[1] + constrainedDelta.lat @@ -20,9 +23,15 @@ export default function(features, delta) { let nextCoordinates; if (feature.type === Constants.geojsonTypes.POINT) { nextCoordinates = moveCoordinate(currentCoordinates); - } else if (feature.type === Constants.geojsonTypes.LINE_STRING || feature.type === Constants.geojsonTypes.MULTI_POINT) { + } else if ( + feature.type === Constants.geojsonTypes.LINE_STRING || + feature.type === Constants.geojsonTypes.MULTI_POINT + ) { nextCoordinates = currentCoordinates.map(moveCoordinate); - } else if (feature.type === Constants.geojsonTypes.POLYGON || feature.type === Constants.geojsonTypes.MULTI_LINE_STRING) { + } else if ( + feature.type === Constants.geojsonTypes.POLYGON || + feature.type === Constants.geojsonTypes.MULTI_LINE_STRING + ) { nextCoordinates = currentCoordinates.map(moveRing); } else if (feature.type === Constants.geojsonTypes.MULTI_POLYGON) { nextCoordinates = currentCoordinates.map(moveMultiPolygon); diff --git a/src/lib/sort_features.js b/src/lib/sort_features.js deleted file mode 100644 index 85229584..00000000 --- a/src/lib/sort_features.js +++ /dev/null @@ -1,38 +0,0 @@ -import area from '@mapbox/geojson-area'; -import * as Constants from '../constants.js'; - -const FEATURE_SORT_RANKS = { - Point: 0, - LineString: 1, - MultiLineString: 1, - Polygon: 2 -}; - -function comparator(a, b) { - const score = FEATURE_SORT_RANKS[a.geometry.type] - FEATURE_SORT_RANKS[b.geometry.type]; - - if (score === 0 && a.geometry.type === Constants.geojsonTypes.POLYGON) { - return a.area - b.area; - } - - return score; -} - -// Sort in the order above, then sort polygons by area ascending. -function sortFeatures(features) { - return features.map((feature) => { - if (feature.geometry.type === Constants.geojsonTypes.POLYGON) { - feature.area = area.geometry({ - type: Constants.geojsonTypes.FEATURE, - property: {}, - geometry: feature.geometry - }); - } - return feature; - }).sort(comparator).map((feature) => { - delete feature.area; - return feature; - }); -} - -export default sortFeatures; diff --git a/src/lib/sort_features.ts b/src/lib/sort_features.ts new file mode 100644 index 00000000..0510a559 --- /dev/null +++ b/src/lib/sort_features.ts @@ -0,0 +1,53 @@ +import area from '@mapbox/geojson-area'; +import * as Constants from '../constants'; +import { Feature } from 'geojson'; + +const FEATURE_SORT_RANKS = { + Point: 0, + MultiPoint: 0, + LineString: 1, + MultiLineString: 1, + MultiPolygon: 2, + Polygon: 2, + GeometryCollection: 2 +}; + +interface DrawFeature extends Feature { + area?: number; +} + +const comparator = (a: DrawFeature, b: DrawFeature) => { + const score = + FEATURE_SORT_RANKS[a.geometry.type] - FEATURE_SORT_RANKS[b.geometry.type]; + + if ( + score === 0 && + a.area && + b.area && + a.geometry.type === Constants.geojsonTypes.POLYGON + ) { + return a.area - b.area; + } + + return score; +} + +// Sort in the order above, then sort polygons by area ascending. +export const sortFeatures = (features: Array) => { + return features + .map(feature => { + if (feature.geometry.type === Constants.geojsonTypes.POLYGON) { + feature.area = area.geometry({ + type: Constants.geojsonTypes.FEATURE, + property: {}, + geometry: feature.geometry + }); + } + return feature; + }) + .sort(comparator) + .map(feature => { + delete feature.area; + return feature; + }); +} diff --git a/src/lib/string_set.js b/src/lib/string_set.js deleted file mode 100644 index d0865779..00000000 --- a/src/lib/string_set.js +++ /dev/null @@ -1,55 +0,0 @@ -function StringSet(items) { - this._items = {}; - this._nums = {}; - this._length = items ? items.length : 0; - if (!items) return; - for (let i = 0, l = items.length; i < l; i++) { - this.add(items[i]); - if (items[i] === undefined) continue; - if (typeof items[i] === 'string') this._items[items[i]] = i; - else this._nums[items[i]] = i; - - } -} - -StringSet.prototype.add = function(x) { - if (this.has(x)) return this; - this._length++; - if (typeof x === 'string') this._items[x] = this._length; - else this._nums[x] = this._length; - return this; -}; - -StringSet.prototype.delete = function(x) { - if (this.has(x) === false) return this; - this._length--; - delete this._items[x]; - delete this._nums[x]; - return this; -}; - -StringSet.prototype.has = function(x) { - if (typeof x !== 'string' && typeof x !== 'number') return false; - return this._items[x] !== undefined || this._nums[x] !== undefined; -}; - -StringSet.prototype.values = function() { - const values = []; - Object.keys(this._items).forEach((k) => { - values.push({ k, v: this._items[k] }); - }); - Object.keys(this._nums).forEach((k) => { - values.push({ k: JSON.parse(k), v: this._nums[k] }); - }); - - return values.sort((a, b) => a.v - b.v).map(a => a.k); -}; - -StringSet.prototype.clear = function() { - this._length = 0; - this._items = {}; - this._nums = {}; - return this; -}; - -export default StringSet; diff --git a/src/lib/string_set.ts b/src/lib/string_set.ts new file mode 100644 index 00000000..67a2cd8a --- /dev/null +++ b/src/lib/string_set.ts @@ -0,0 +1,64 @@ +class StringSet { + private _items: Record; + private _nums: Record; + private _length: number; + + constructor(items?: (string | number)[]) { + this._items = {}; + this._nums = {}; + this._length = items ? items.length : 0; + + if (!items) return; + + for (let i = 0; i < items.length; i++) { + this.add(items[i]); + if (items[i] === undefined) continue; + if (typeof items[i] === 'string') this._items[items[i]] = i; + else this._nums[items[i]] = i; + } + } + + add(x: string | number): this { + if (this.has(x)) return this; + this._length++; + if (typeof x === 'string') this._items[x] = this._length; + else this._nums[x] = this._length; + return this; + } + + delete(x: string | number): this { + if (!this.has(x)) return this; + this._length--; + delete this._items[x as string]; + delete this._nums[x as number]; + return this; + } + + has(x: unknown): boolean { + if (typeof x !== 'string' && typeof x !== 'number') return false; + return this._items[x as string] !== undefined || this._nums[x as number] !== undefined; + } + + values(): (string | number)[] { + const values: { k: string | number; v: number }[] = []; + + Object.keys(this._items).forEach(k => { + values.push({ k, v: this._items[k] }); + }); + + Object.keys(this._nums).forEach(k => { + values.push({ k: JSON.parse(k), v: this._nums[+k] }); + }); + + return values.sort((a, b) => a.v - b.v).map(a => a.k); + } + + clear(): this { + this._length = 0; + this._items = {}; + this._nums = {}; + return this; + } +} + +export default StringSet; diff --git a/src/lib/string_sets_are_equal.js b/src/lib/string_sets_are_equal.js deleted file mode 100644 index 894f5a5d..00000000 --- a/src/lib/string_sets_are_equal.js +++ /dev/null @@ -1,4 +0,0 @@ -export default function(a, b) { - if (a.length !== b.length) return false; - return JSON.stringify(a.map(id => id).sort()) === JSON.stringify(b.map(id => id).sort()); -} diff --git a/src/lib/string_sets_are_equal.ts b/src/lib/string_sets_are_equal.ts new file mode 100644 index 00000000..6e77a92d --- /dev/null +++ b/src/lib/string_sets_are_equal.ts @@ -0,0 +1,7 @@ +export const stringSetsAreEqual = (a: Array, b: Array) => { + if (a.length !== b.length) return false; + return ( + JSON.stringify(a.map(id => id).sort()) === + JSON.stringify(b.map(id => id).sort()) + ); +}; diff --git a/src/lib/theme.js b/src/lib/theme.js deleted file mode 100644 index 0f45b7d1..00000000 --- a/src/lib/theme.js +++ /dev/null @@ -1,153 +0,0 @@ -/* eslint comma-dangle: ["error", "always-multiline"] */ - -const blue = '#3bb2d0'; -const orange = '#fbb03b'; -const white = '#fff'; - -export default [ - // Polygons - // Solid fill - // Active state defines color - { - 'id': 'gl-draw-polygon-fill', - 'type': 'fill', - 'filter': [ - 'all', - ['==', '$type', 'Polygon'], - ], - 'paint': { - 'fill-color': [ - 'case', - ['==', ['get', 'active'], 'true'], orange, - blue, - ], - 'fill-opacity': 0.1, - }, - }, - // Lines - // Polygon - // Matches Lines AND Polygons - // Active state defines color - { - 'id': 'gl-draw-lines', - 'type': 'line', - 'filter': [ - 'any', - ['==', '$type', 'LineString'], - ['==', '$type', 'Polygon'], - ], - 'layout': { - 'line-cap': 'round', - 'line-join': 'round', - }, - 'paint': { - 'line-color': [ - 'case', - ['==', ['get', 'active'], 'true'], orange, - blue, - ], - 'line-dasharray': [ - 'case', - ['==', ['get', 'active'], 'true'], [0.2, 2], - [2, 0], - ], - 'line-width': 2, - }, - }, - // Points - // Circle with an outline - // Active state defines size and color - { - 'id': 'gl-draw-point-outer', - 'type': 'circle', - 'filter': [ - 'all', - ['==', '$type', 'Point'], - ['==', 'meta', 'feature'], - ], - 'paint': { - 'circle-radius': [ - 'case', - ['==', ['get', 'active'], 'true'], 7, - 5, - ], - 'circle-color': white, - }, - }, - { - 'id': 'gl-draw-point-inner', - 'type': 'circle', - 'filter': [ - 'all', - ['==', '$type', 'Point'], - ['==', 'meta', 'feature'], - ], - 'paint': { - 'circle-radius': [ - 'case', - ['==', ['get', 'active'], 'true'], 5, - 3, - ], - 'circle-color': [ - 'case', - ['==', ['get', 'active'], 'true'], orange, - blue, - ], - }, - }, - // Vertex - // Visible when editing polygons and lines - // Similar behaviour to Points - // Active state defines size - { - 'id': 'gl-draw-vertex-outer', - 'type': 'circle', - 'filter': [ - 'all', - ['==', '$type', 'Point'], - ['==', 'meta', 'vertex'], - ['!=', 'mode', 'simple_select'], - ], - 'paint': { - 'circle-radius': [ - 'case', - ['==', ['get', 'active'], 'true'], 7, - 5, - ], - 'circle-color': white, - }, - }, - { - 'id': 'gl-draw-vertex-inner', - 'type': 'circle', - 'filter': [ - 'all', - ['==', '$type', 'Point'], - ['==', 'meta', 'vertex'], - ['!=', 'mode', 'simple_select'], - ], - 'paint': { - 'circle-radius': [ - 'case', - ['==', ['get', 'active'], 'true'], 5, - 3, - ], - 'circle-color': orange, - }, - }, - // Midpoint - // Visible when editing polygons and lines - // Tapping or dragging them adds a new vertex to the feature - { - 'id': 'gl-draw-midpoint', - 'type': 'circle', - 'filter': [ - 'all', - ['==', 'meta', 'midpoint'], - ], - 'paint': { - 'circle-radius': 3, - 'circle-color': orange, - }, - }, -]; diff --git a/src/lib/theme.ts b/src/lib/theme.ts new file mode 100644 index 00000000..c45a9465 --- /dev/null +++ b/src/lib/theme.ts @@ -0,0 +1,106 @@ +const blue = '#3bb2d0'; +const orange = '#fbb03b'; +const white = '#fff'; + +export default [ + // Polygons + // Solid fill + // Active state defines color + { + id: 'gl-draw-polygon-fill', + type: 'fill', + filter: ['all', ['==', '$type', 'Polygon']], + paint: { + 'fill-color': ['case', ['==', ['get', 'active'], 'true'], orange, blue], + 'fill-opacity': 0.1 + } + }, + // Lines + // Polygon + // Matches Lines AND Polygons + // Active state defines color + { + id: 'gl-draw-lines', + type: 'line', + filter: ['any', ['==', '$type', 'LineString'], ['==', '$type', 'Polygon']], + layout: { + 'line-cap': 'round', + 'line-join': 'round' + }, + paint: { + 'line-color': ['case', ['==', ['get', 'active'], 'true'], orange, blue], + 'line-dasharray': [ + 'case', + ['==', ['get', 'active'], 'true'], + [0.2, 2], + [2, 0] + ], + 'line-width': 2 + } + }, + // Points + // Circle with an outline + // Active state defines size and color + { + id: 'gl-draw-point-outer', + type: 'circle', + filter: ['all', ['==', '$type', 'Point'], ['==', 'meta', 'feature']], + paint: { + 'circle-radius': ['case', ['==', ['get', 'active'], 'true'], 7, 5], + 'circle-color': white + } + }, + { + id: 'gl-draw-point-inner', + type: 'circle', + filter: ['all', ['==', '$type', 'Point'], ['==', 'meta', 'feature']], + paint: { + 'circle-radius': ['case', ['==', ['get', 'active'], 'true'], 5, 3], + 'circle-color': ['case', ['==', ['get', 'active'], 'true'], orange, blue] + } + }, + // Vertex + // Visible when editing polygons and lines + // Similar behaviour to Points + // Active state defines size + { + id: 'gl-draw-vertex-outer', + type: 'circle', + filter: [ + 'all', + ['==', '$type', 'Point'], + ['==', 'meta', 'vertex'], + ['!=', 'mode', 'simple_select'] + ], + paint: { + 'circle-radius': ['case', ['==', ['get', 'active'], 'true'], 7, 5], + 'circle-color': white + } + }, + { + id: 'gl-draw-vertex-inner', + type: 'circle', + filter: [ + 'all', + ['==', '$type', 'Point'], + ['==', 'meta', 'vertex'], + ['!=', 'mode', 'simple_select'] + ], + paint: { + 'circle-radius': ['case', ['==', ['get', 'active'], 'true'], 5, 3], + 'circle-color': orange + } + }, + // Midpoint + // Visible when editing polygons and lines + // Tapping or dragging them adds a new vertex to the feature + { + id: 'gl-draw-midpoint', + type: 'circle', + filter: ['all', ['==', 'meta', 'midpoint']], + paint: { + 'circle-radius': 3, + 'circle-color': orange + } + } +]; diff --git a/src/lib/to_dense_array.js b/src/lib/to_dense_array.js deleted file mode 100644 index 6553745f..00000000 --- a/src/lib/to_dense_array.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Derive a dense array (no `undefined`s) from a single value or array. - * - * @param {any} x - * @return {Array} - */ -function toDenseArray(x) { - return [].concat(x).filter(y => y !== undefined); -} - -export default toDenseArray; diff --git a/src/lib/to_dense_array.ts b/src/lib/to_dense_array.ts new file mode 100644 index 00000000..77c76438 --- /dev/null +++ b/src/lib/to_dense_array.ts @@ -0,0 +1,3 @@ +export const toDenseArray = (x: unknown): Array => { + return [].concat(x).filter(y => y !== undefined); +}; diff --git a/src/modes/direct_select.js b/src/modes/direct_select.js deleted file mode 100644 index a27749d4..00000000 --- a/src/modes/direct_select.js +++ /dev/null @@ -1,259 +0,0 @@ -import { noTarget, isOfMetaType, isActiveFeature, isInactiveFeature, isShiftDown } from '../lib/common_selectors.js'; -import createSupplementaryPoints from '../lib/create_supplementary_points.js'; -import constrainFeatureMovement from '../lib/constrain_feature_movement.js'; -import doubleClickZoom from '../lib/double_click_zoom.js'; -import * as Constants from '../constants.js'; -import moveFeatures from '../lib/move_features.js'; - -const isVertex = isOfMetaType(Constants.meta.VERTEX); -const isMidpoint = isOfMetaType(Constants.meta.MIDPOINT); - -const DirectSelect = {}; - -// INTERNAL FUCNTIONS - -DirectSelect.fireUpdate = function() { - this.fire(Constants.events.UPDATE, { - action: Constants.updateActions.CHANGE_COORDINATES, - features: this.getSelected().map(f => f.toGeoJSON()) - }); -}; - -DirectSelect.fireActionable = function(state) { - this.setActionableState({ - combineFeatures: false, - uncombineFeatures: false, - trash: state.selectedCoordPaths.length > 0 - }); -}; - -DirectSelect.startDragging = function(state, e) { - if (state.initialDragPanState == null) { - state.initialDragPanState = this.map.dragPan.isEnabled(); - } - - this.map.dragPan.disable(); - state.canDragMove = true; - state.dragMoveLocation = e.lngLat; -}; - -DirectSelect.stopDragging = function(state) { - if (state.canDragMove && state.initialDragPanState === true) { - this.map.dragPan.enable(); - } - - state.initialDragPanState = null; - state.dragMoving = false; - state.canDragMove = false; - state.dragMoveLocation = null; -}; - -DirectSelect.onVertex = function (state, e) { - this.startDragging(state, e); - const about = e.featureTarget.properties; - const selectedIndex = state.selectedCoordPaths.indexOf(about.coord_path); - if (!isShiftDown(e) && selectedIndex === -1) { - state.selectedCoordPaths = [about.coord_path]; - } else if (isShiftDown(e) && selectedIndex === -1) { - state.selectedCoordPaths.push(about.coord_path); - } - - const selectedCoordinates = this.pathsToCoordinates(state.featureId, state.selectedCoordPaths); - this.setSelectedCoordinates(selectedCoordinates); -}; - -DirectSelect.onMidpoint = function(state, e) { - this.startDragging(state, e); - const about = e.featureTarget.properties; - state.feature.addCoordinate(about.coord_path, about.lng, about.lat); - this.fireUpdate(); - state.selectedCoordPaths = [about.coord_path]; -}; - -DirectSelect.pathsToCoordinates = function(featureId, paths) { - return paths.map(coord_path => ({ feature_id: featureId, coord_path })); -}; - -DirectSelect.onFeature = function(state, e) { - if (state.selectedCoordPaths.length === 0) this.startDragging(state, e); - else this.stopDragging(state); -}; - -DirectSelect.dragFeature = function(state, e, delta) { - moveFeatures(this.getSelected(), delta); - state.dragMoveLocation = e.lngLat; -}; - -DirectSelect.dragVertex = function(state, e, delta) { - const selectedCoords = state.selectedCoordPaths.map(coord_path => state.feature.getCoordinate(coord_path)); - const selectedCoordPoints = selectedCoords.map(coords => ({ - type: Constants.geojsonTypes.FEATURE, - properties: {}, - geometry: { - type: Constants.geojsonTypes.POINT, - coordinates: coords - } - })); - - const constrainedDelta = constrainFeatureMovement(selectedCoordPoints, delta); - for (let i = 0; i < selectedCoords.length; i++) { - const coord = selectedCoords[i]; - state.feature.updateCoordinate(state.selectedCoordPaths[i], coord[0] + constrainedDelta.lng, coord[1] + constrainedDelta.lat); - } -}; - -DirectSelect.clickNoTarget = function () { - this.changeMode(Constants.modes.SIMPLE_SELECT); -}; - -DirectSelect.clickInactive = function () { - this.changeMode(Constants.modes.SIMPLE_SELECT); -}; - -DirectSelect.clickActiveFeature = function (state) { - state.selectedCoordPaths = []; - this.clearSelectedCoordinates(); - state.feature.changed(); -}; - -// EXTERNAL FUNCTIONS - -DirectSelect.onSetup = function(opts) { - const featureId = opts.featureId; - const feature = this.getFeature(featureId); - - if (!feature) { - throw new Error('You must provide a featureId to enter direct_select mode'); - } - - if (feature.type === Constants.geojsonTypes.POINT) { - throw new TypeError('direct_select mode doesn\'t handle point features'); - } - - const state = { - featureId, - feature, - dragMoveLocation: opts.startPos || null, - dragMoving: false, - canDragMove: false, - selectedCoordPaths: opts.coordPath ? [opts.coordPath] : [], - }; - - this.setSelectedCoordinates(this.pathsToCoordinates(featureId, state.selectedCoordPaths)); - this.setSelected(featureId); - doubleClickZoom.disable(this); - - this.setActionableState({ - trash: true - }); - - return state; -}; - -DirectSelect.onStop = function() { - doubleClickZoom.enable(this); - this.clearSelectedCoordinates(); -}; - -DirectSelect.toDisplayFeatures = function(state, geojson, push) { - if (state.featureId === geojson.properties.id) { - geojson.properties.active = Constants.activeStates.ACTIVE; - push(geojson); - createSupplementaryPoints(geojson, { - map: this.map, - midpoints: true, - selectedPaths: state.selectedCoordPaths - }).forEach(push); - } else { - geojson.properties.active = Constants.activeStates.INACTIVE; - push(geojson); - } - this.fireActionable(state); -}; - -DirectSelect.onTrash = function(state) { - // Uses number-aware sorting to make sure '9' < '10'. Comparison is reversed because we want them - // in reverse order so that we can remove by index safely. - state.selectedCoordPaths - .sort((a, b) => b.localeCompare(a, 'en', { numeric: true })) - .forEach(id => state.feature.removeCoordinate(id)); - this.fireUpdate(); - state.selectedCoordPaths = []; - this.clearSelectedCoordinates(); - this.fireActionable(state); - if (state.feature.isValid() === false) { - this.deleteFeature([state.featureId]); - this.changeMode(Constants.modes.SIMPLE_SELECT, {}); - } -}; - -DirectSelect.onMouseMove = function(state, e) { - // On mousemove that is not a drag, stop vertex movement. - const isFeature = isActiveFeature(e); - const onVertex = isVertex(e); - const isMidPoint = isMidpoint(e); - const noCoords = state.selectedCoordPaths.length === 0; - if (isFeature && noCoords) this.updateUIClasses({ mouse: Constants.cursors.MOVE }); - else if (onVertex && !noCoords) this.updateUIClasses({ mouse: Constants.cursors.MOVE }); - else this.updateUIClasses({ mouse: Constants.cursors.NONE }); - - const isDraggableItem = onVertex || isFeature || isMidPoint; - if (isDraggableItem && state.dragMoving) this.fireUpdate(); - - this.stopDragging(state); - - // Skip render - return true; -}; - -DirectSelect.onMouseOut = function(state) { - // As soon as you mouse leaves the canvas, update the feature - if (state.dragMoving) this.fireUpdate(); - - // Skip render - return true; -}; - -DirectSelect.onTouchStart = DirectSelect.onMouseDown = function(state, e) { - if (isVertex(e)) return this.onVertex(state, e); - if (isActiveFeature(e)) return this.onFeature(state, e); - if (isMidpoint(e)) return this.onMidpoint(state, e); -}; - -DirectSelect.onDrag = function(state, e) { - if (state.canDragMove !== true) return; - state.dragMoving = true; - e.originalEvent.stopPropagation(); - - const delta = { - lng: e.lngLat.lng - state.dragMoveLocation.lng, - lat: e.lngLat.lat - state.dragMoveLocation.lat - }; - if (state.selectedCoordPaths.length > 0) this.dragVertex(state, e, delta); - else this.dragFeature(state, e, delta); - - state.dragMoveLocation = e.lngLat; -}; - -DirectSelect.onClick = function(state, e) { - if (noTarget(e)) return this.clickNoTarget(state, e); - if (isActiveFeature(e)) return this.clickActiveFeature(state, e); - if (isInactiveFeature(e)) return this.clickInactive(state, e); - this.stopDragging(state); -}; - -DirectSelect.onTap = function(state, e) { - if (noTarget(e)) return this.clickNoTarget(state, e); - if (isActiveFeature(e)) return this.clickActiveFeature(state, e); - if (isInactiveFeature(e)) return this.clickInactive(state, e); -}; - -DirectSelect.onTouchEnd = DirectSelect.onMouseUp = function(state) { - if (state.dragMoving) { - this.fireUpdate(); - } - this.stopDragging(state); -}; - -export default DirectSelect; - diff --git a/src/modes/direct_select.ts b/src/modes/direct_select.ts new file mode 100644 index 00000000..a53d4d20 --- /dev/null +++ b/src/modes/direct_select.ts @@ -0,0 +1,307 @@ +import { + noTarget, + isOfMetaType, + isActiveFeature, + isInactiveFeature, + isShiftDown +} from '../lib/common_selectors'; +import { createSupplementaryPoints } from '../lib/create_supplementary_points'; +import { constrainFeatureMovement } from '../lib/constrain_feature_movement'; +import { doubleClickZoom } from '../lib/double_click_zoom'; +import { moveFeatures } from '../lib/move_features'; +import * as Constants from '../constants'; + +import type { DirectSelectState, DrawCustomMode, MapMouseEvent, MapTouchEvent, DrawCoords, StrictFeature } from '../types/types'; + +const isVertex = isOfMetaType(Constants.meta.VERTEX); +const isMidpoint = isOfMetaType(Constants.meta.MIDPOINT); + +type Event = MapMouseEvent | MapTouchEvent; + +interface DirectSelectMode extends DrawCustomMode { + fireUpdate(): void; + clickInactive(): void; + fireActionable(state: DirectSelectState): void; + clickNoTarget(state: DirectSelectState, e: Event): void; + startDragging(state: DirectSelectState, e: Event): void; + stopDragging(state: DirectSelectState): void; + onVertex(state: DirectSelectState, e: Event): void; + onMidpoint(state: DirectSelectState, e: Event): void; + onFeature(state: DirectSelectState, e: Event): void; + dragFeature(state: DirectSelectState, e: Event, delta: { lng: number, lat: number }): void; + dragVertex(state: DirectSelectState, e: Event, delta: { lng: number, lat: number }): void; + clickActiveFeature(state: DirectSelectState): void; + pathsToCoordinates(featureId: string, paths: string[]): DrawCoords; + _start(state: DirectSelectState, e: Event): void; + _select(state: DirectSelectState, e: Event): void; + _end(state: DirectSelectState): void; +} + +const DirectSelect: DirectSelectMode = { + + // INTERNAL FUNCTIONS + fireUpdate: function () { + this.fire(Constants.events.UPDATE, { + action: Constants.updateActions.CHANGE_COORDINATES, + features: this.getSelected().map(f => f.toGeoJSON()) + }); + }, + + fireActionable: function (state) { + this.setActionableState({ + combineFeatures: false, + uncombineFeatures: false, + trash: state.selectedCoordPaths.length > 0 + }); + }, + + startDragging: function (state, e) { + if (state.initialDragPanState == null) { + state.initialDragPanState = this.map.dragPan.isEnabled(); + } + + this.map.dragPan.disable(); + state.canDragMove = true; + state.dragMoveLocation = e.lngLat; + }, + + stopDragging: function (state) { + if (state.canDragMove && state.initialDragPanState === true) { + this.map.dragPan.enable(); + } + + state.initialDragPanState = null; + state.dragMoving = false; + state.canDragMove = false; + state.dragMoveLocation = null; + }, + + onVertex: function (state, e) { + this.startDragging(state, e); + const { coord_path } = e.featureTarget.properties; + + const selectedIndex = state.selectedCoordPaths.indexOf(coord_path); + if (!isShiftDown(e as MapMouseEvent) && selectedIndex === -1) { + state.selectedCoordPaths = [coord_path]; + } else if (isShiftDown(e as MapMouseEvent) && selectedIndex === -1) { + state.selectedCoordPaths.push(coord_path); + } + + const selectedCoordinates = this.pathsToCoordinates( + state.featureId, + state.selectedCoordPaths + ); + this.setSelectedCoordinates(selectedCoordinates); + }, + + onMidpoint: function (state, e) { + this.startDragging(state, e); + const about = e.featureTarget.properties; + state.feature.addCoordinate(about.coord_path, about.lng, about.lat); + this.fireUpdate(); + state.selectedCoordPaths = [about.coord_path]; + }, + + pathsToCoordinates: function (featureId, paths) { + return paths.map(coord_path => ({ feature_id: featureId, coord_path })); + }, + + onFeature: function (state, e) { + if (state.selectedCoordPaths.length === 0) this.startDragging(state, e); + else this.stopDragging(state); + }, + + dragFeature: function (state, e, delta) { + moveFeatures(this.getSelected(), delta); + state.dragMoveLocation = e.lngLat; + }, + + dragVertex: function (state, e, delta) { + const selectedCoords = state.selectedCoordPaths.map(coord_path => + state.feature.getCoordinate(coord_path) + ); + const selectedCoordPoints = selectedCoords.map(coords => ({ + type: Constants.geojsonTypes.FEATURE, + properties: {}, + geometry: { + type: Constants.geojsonTypes.POINT, + coordinates: coords + } + })) as Array; + + const constrainedDelta = constrainFeatureMovement(selectedCoordPoints, delta); + for (let i = 0; i < selectedCoords.length; i++) { + const coord = selectedCoords[i]; + state.feature.updateCoordinate( + state.selectedCoordPaths[i], + coord[0] + constrainedDelta.lng, + coord[1] + constrainedDelta.lat + ); + } + }, + + clickNoTarget: function () { + this.changeMode(Constants.modes.SIMPLE_SELECT); + }, + + clickInactive: function () { + this.changeMode(Constants.modes.SIMPLE_SELECT); + }, + + clickActiveFeature: function (state) { + state.selectedCoordPaths = []; + this.clearSelectedCoordinates(); + state.feature.changed(); + }, + + // EXTERNAL FUNCTIONS + onSetup: function (opts) { + const featureId = opts.featureId; + const feature = this.getFeature(featureId); + + if (!feature) { + throw new Error('You must provide a featureId to enter direct_select mode'); + } + + if (feature.type === Constants.geojsonTypes.POINT) { + throw new TypeError("direct_select mode doesn't handle point features"); + } + + const state = { + featureId, + feature, + dragMoveLocation: opts.startPos || null, + dragMoving: false, + canDragMove: false, + selectedCoordPaths: opts.coordPath ? [opts.coordPath] : [] + }; + + + this.setSelectedCoordinates( + this.pathsToCoordinates(featureId, state.selectedCoordPaths) + ); + this.setSelected(featureId); + doubleClickZoom.disable(this); + + this.setActionableState({ + trash: true + }); + + return state; + }, + + onStop: function () { + doubleClickZoom.enable(this); + this.clearSelectedCoordinates(); + }, + + onTrash: function (state) { + // Uses number-aware sorting to make sure '9' < '10'. Comparison is reversed because we want them + // in reverse order so that we can remove by index safely. + state.selectedCoordPaths + .sort((a, b) => b.localeCompare(a, 'en', { numeric: true })) + .forEach(id => state.feature.removeCoordinate(id)); + this.fireUpdate(); + state.selectedCoordPaths = []; + this.clearSelectedCoordinates(); + this.fireActionable(state); + if (state.feature.isValid() === false) { + this.deleteFeature(state.featureId); + this.changeMode(Constants.modes.SIMPLE_SELECT, {}); + } + }, + + onMouseMove: function (state, e) { + // On mousemove that is not a drag, stop vertex movement. + const isFeature = isActiveFeature(e); + const onVertex = isVertex(e); + const isMidPoint = isMidpoint(e); + const noCoords = state.selectedCoordPaths.length === 0; + if (isFeature && noCoords) + this.updateUIClasses({ mouse: Constants.cursors.MOVE }); + else if (onVertex && !noCoords) + this.updateUIClasses({ mouse: Constants.cursors.MOVE }); + else this.updateUIClasses({ mouse: Constants.cursors.NONE }); + + const isDraggableItem = onVertex || isFeature || isMidPoint; + if (isDraggableItem && state.dragMoving) this.fireUpdate(); + + this.stopDragging(state); + + // Skip render + return true; + }, + + onMouseOut: function (state) { + // As soon as you mouse leaves the canvas, update the feature + if (state.dragMoving) this.fireUpdate(); + + // Skip render + return true; + }, + + _start: function (state, e) { + if (isVertex(e)) return this.onVertex(state, e); + if (isActiveFeature(e)) return this.onFeature(state, e); + if (isMidpoint(e)) return this.onMidpoint(state, e); + }, + + onTouchStart: function (state, e) { return this._start(state, e); }, + onMouseDown: function (state, e) { return this._start(state, e); }, + + onDrag: function (state, e) { + if (state.canDragMove !== true) return; + state.dragMoving = true; + e.originalEvent.stopPropagation(); + + const delta = { + lng: e.lngLat.lng - state.dragMoveLocation.lng, + lat: e.lngLat.lat - state.dragMoveLocation.lat + }; + if (state.selectedCoordPaths.length > 0) this.dragVertex(state, e, delta); + else this.dragFeature(state, e, delta); + + state.dragMoveLocation = e.lngLat; + }, + + _select: function (state, e) { + if (noTarget(e)) return this.clickNoTarget(state, e); + if (isActiveFeature(e)) return this.clickActiveFeature(state, e); + if (isInactiveFeature(e)) return this.clickInactive(state, e); + this.stopDragging(state); + }, + + onTap: function (state, e) { return this._select(state, e); }, + onClick: function (state, e) { return this._select(state, e); }, + + _end: function (state) { + if (state.dragMoving) { + this.fireUpdate(); + } + this.stopDragging(state); + }, + + onMouseUp: function (state) { return this._end(state); }, + onTouchEnd: function (state) { return this._end(state); }, + + toDisplayFeatures: function (state, geojson, push) { + if (state.featureId === geojson.properties.id) { + geojson.properties.active = Constants.activeStates.ACTIVE; + push(geojson); + createSupplementaryPoints(geojson, { + map: this.map, + midpoints: true, + selectedPaths: state.selectedCoordPaths + }).forEach(push); + } else { + geojson.properties.active = Constants.activeStates.INACTIVE; + push(geojson); + } + + this.fireActionable(state); + } + +}; + +export default DirectSelect; + diff --git a/src/modes/draw_line_string.js b/src/modes/draw_line_string.js deleted file mode 100644 index ec008061..00000000 --- a/src/modes/draw_line_string.js +++ /dev/null @@ -1,152 +0,0 @@ -import * as CommonSelectors from '../lib/common_selectors.js'; -import isEventAtCoordinates from '../lib/is_event_at_coordinates.js'; -import doubleClickZoom from '../lib/double_click_zoom.js'; -import * as Constants from '../constants.js'; -import createVertex from '../lib/create_vertex.js'; - -const DrawLineString = {}; - -DrawLineString.onSetup = function(opts) { - opts = opts || {}; - const featureId = opts.featureId; - - let line, currentVertexPosition; - let direction = 'forward'; - if (featureId) { - line = this.getFeature(featureId); - if (!line) { - throw new Error('Could not find a feature with the provided featureId'); - } - let from = opts.from; - if (from && from.type === 'Feature' && from.geometry && from.geometry.type === 'Point') { - from = from.geometry; - } - if (from && from.type === 'Point' && from.coordinates && from.coordinates.length === 2) { - from = from.coordinates; - } - if (!from || !Array.isArray(from)) { - throw new Error('Please use the `from` property to indicate which point to continue the line from'); - } - const lastCoord = line.coordinates.length - 1; - if (line.coordinates[lastCoord][0] === from[0] && line.coordinates[lastCoord][1] === from[1]) { - currentVertexPosition = lastCoord + 1; - // add one new coordinate to continue from - line.addCoordinate(currentVertexPosition, ...line.coordinates[lastCoord]); - } else if (line.coordinates[0][0] === from[0] && line.coordinates[0][1] === from[1]) { - direction = 'backwards'; - currentVertexPosition = 0; - // add one new coordinate to continue from - line.addCoordinate(currentVertexPosition, ...line.coordinates[0]); - } else { - throw new Error('`from` should match the point at either the start or the end of the provided LineString'); - } - } else { - line = this.newFeature({ - type: Constants.geojsonTypes.FEATURE, - properties: {}, - geometry: { - type: Constants.geojsonTypes.LINE_STRING, - coordinates: [] - } - }); - currentVertexPosition = 0; - this.addFeature(line); - } - - this.clearSelectedFeatures(); - doubleClickZoom.disable(this); - this.updateUIClasses({ mouse: Constants.cursors.ADD }); - this.activateUIButton(Constants.types.LINE); - this.setActionableState({ - trash: true - }); - - return { - line, - currentVertexPosition, - direction - }; -}; - -DrawLineString.clickAnywhere = function(state, e) { - if (state.currentVertexPosition > 0 && isEventAtCoordinates(e, state.line.coordinates[state.currentVertexPosition - 1]) || - state.direction === 'backwards' && isEventAtCoordinates(e, state.line.coordinates[state.currentVertexPosition + 1])) { - return this.changeMode(Constants.modes.SIMPLE_SELECT, { featureIds: [state.line.id] }); - } - this.updateUIClasses({ mouse: Constants.cursors.ADD }); - state.line.updateCoordinate(state.currentVertexPosition, e.lngLat.lng, e.lngLat.lat); - if (state.direction === 'forward') { - state.currentVertexPosition++; - state.line.updateCoordinate(state.currentVertexPosition, e.lngLat.lng, e.lngLat.lat); - } else { - state.line.addCoordinate(0, e.lngLat.lng, e.lngLat.lat); - } -}; - -DrawLineString.clickOnVertex = function(state) { - return this.changeMode(Constants.modes.SIMPLE_SELECT, { featureIds: [state.line.id] }); -}; - -DrawLineString.onMouseMove = function(state, e) { - state.line.updateCoordinate(state.currentVertexPosition, e.lngLat.lng, e.lngLat.lat); - if (CommonSelectors.isVertex(e)) { - this.updateUIClasses({ mouse: Constants.cursors.POINTER }); - } -}; - -DrawLineString.onTap = DrawLineString.onClick = function(state, e) { - if (CommonSelectors.isVertex(e)) return this.clickOnVertex(state, e); - this.clickAnywhere(state, e); -}; - -DrawLineString.onKeyUp = function(state, e) { - if (CommonSelectors.isEnterKey(e)) { - this.changeMode(Constants.modes.SIMPLE_SELECT, { featureIds: [state.line.id] }); - } else if (CommonSelectors.isEscapeKey(e)) { - this.deleteFeature([state.line.id], { silent: true }); - this.changeMode(Constants.modes.SIMPLE_SELECT); - } -}; - -DrawLineString.onStop = function(state) { - doubleClickZoom.enable(this); - this.activateUIButton(); - - // check to see if we've deleted this feature - if (this.getFeature(state.line.id) === undefined) return; - - //remove last added coordinate - state.line.removeCoordinate(`${state.currentVertexPosition}`); - if (state.line.isValid()) { - this.fire(Constants.events.CREATE, { - features: [state.line.toGeoJSON()] - }); - } else { - this.deleteFeature([state.line.id], { silent: true }); - this.changeMode(Constants.modes.SIMPLE_SELECT, {}, { silent: true }); - } -}; - -DrawLineString.onTrash = function(state) { - this.deleteFeature([state.line.id], { silent: true }); - this.changeMode(Constants.modes.SIMPLE_SELECT); -}; - -DrawLineString.toDisplayFeatures = function(state, geojson, display) { - const isActiveLine = geojson.properties.id === state.line.id; - geojson.properties.active = (isActiveLine) ? Constants.activeStates.ACTIVE : Constants.activeStates.INACTIVE; - if (!isActiveLine) return display(geojson); - // Only render the line if it has at least one real coordinate - if (geojson.geometry.coordinates.length < 2) return; - geojson.properties.meta = Constants.meta.FEATURE; - display(createVertex( - state.line.id, - geojson.geometry.coordinates[state.direction === 'forward' ? geojson.geometry.coordinates.length - 2 : 1], - `${state.direction === 'forward' ? geojson.geometry.coordinates.length - 2 : 1}`, - false - )); - - display(geojson); -}; - -export default DrawLineString; diff --git a/src/modes/draw_line_string.ts b/src/modes/draw_line_string.ts new file mode 100644 index 00000000..5e8c8e4d --- /dev/null +++ b/src/modes/draw_line_string.ts @@ -0,0 +1,211 @@ +import * as CommonSelectors from '../lib/common_selectors'; +import { isEventAtCoordinates } from '../lib/is_event_at_coordinates'; +import { doubleClickZoom } from '../lib/double_click_zoom'; +import * as Constants from '../constants'; +import { createVertex } from '../lib/create_vertex'; + +const DrawLineString = { + onSetup: function (opts) { + opts = opts || {}; + const featureId = opts.featureId; + + let line, currentVertexPosition; + let direction = 'forward'; + if (featureId) { + line = this.getFeature(featureId); + if (!line) { + throw new Error('Could not find a feature with the provided featureId'); + } + let from = opts.from; + if ( + from && + from.type === 'Feature' && + from.geometry && + from.geometry.type === 'Point' + ) { + from = from.geometry; + } + if ( + from && + from.type === 'Point' && + from.coordinates && + from.coordinates.length === 2 + ) { + from = from.coordinates; + } + if (!from || !Array.isArray(from)) { + throw new Error( + 'Please use the `from` property to indicate which point to continue the line from' + ); + } + const lastCoord = line.coordinates.length - 1; + if ( + line.coordinates[lastCoord][0] === from[0] && + line.coordinates[lastCoord][1] === from[1] + ) { + currentVertexPosition = lastCoord + 1; + // add one new coordinate to continue from + line.addCoordinate(currentVertexPosition, ...line.coordinates[lastCoord]); + } else if ( + line.coordinates[0][0] === from[0] && + line.coordinates[0][1] === from[1] + ) { + direction = 'backwards'; + currentVertexPosition = 0; + // add one new coordinate to continue from + line.addCoordinate(currentVertexPosition, ...line.coordinates[0]); + } else { + throw new Error( + '`from` should match the point at either the start or the end of the provided LineString' + ); + } + } else { + line = this.newFeature({ + type: Constants.geojsonTypes.FEATURE, + properties: {}, + geometry: { + type: Constants.geojsonTypes.LINE_STRING, + coordinates: [] + } + }); + currentVertexPosition = 0; + this.addFeature(line); + } + + this.clearSelectedFeatures(); + doubleClickZoom.disable(this); + this.updateUIClasses({ mouse: Constants.cursors.ADD }); + this.activateUIButton(Constants.types.LINE); + this.setActionableState({ + trash: true + }); + + return { + line, + currentVertexPosition, + direction + }; + }, + + clickAnywhere: function (state, e) { + if ( + (state.currentVertexPosition > 0 && + isEventAtCoordinates( + e, + state.line.coordinates[state.currentVertexPosition - 1] + )) || + (state.direction === 'backwards' && + isEventAtCoordinates( + e, + state.line.coordinates[state.currentVertexPosition + 1] + )) + ) { + return this.changeMode(Constants.modes.SIMPLE_SELECT, { + featureIds: [state.line.id] + }); + } + this.updateUIClasses({ mouse: Constants.cursors.ADD }); + state.line.updateCoordinate( + state.currentVertexPosition, + e.lngLat.lng, + e.lngLat.lat + ); + if (state.direction === 'forward') { + state.currentVertexPosition++; + state.line.updateCoordinate( + state.currentVertexPosition, + e.lngLat.lng, + e.lngLat.lat + ); + } else { + state.line.addCoordinate(0, e.lngLat.lng, e.lngLat.lat); + } + }, + + clickOnVertex: function (state) { + return this.changeMode(Constants.modes.SIMPLE_SELECT, { + featureIds: [state.line.id] + }); + }, + + onMouseMove: function (state, e) { + state.line.updateCoordinate( + state.currentVertexPosition, + e.lngLat.lng, + e.lngLat.lat + ); + if (CommonSelectors.isVertex(e)) { + this.updateUIClasses({ mouse: Constants.cursors.POINTER }); + } + }, + + _select: function(state, e) { + if (CommonSelectors.isVertex(e)) return this.clickOnVertex(state, e); + this.clickAnywhere(state, e); + }, + + onKeyUp: function (state, e) { + if (CommonSelectors.isEnterKey(e)) { + this.changeMode(Constants.modes.SIMPLE_SELECT, { + featureIds: [state.line.id] + }); + } else if (CommonSelectors.isEscapeKey(e)) { + this.deleteFeature([state.line.id], { silent: true }); + this.changeMode(Constants.modes.SIMPLE_SELECT); + } + }, + + onStop: function (state) { + doubleClickZoom.enable(this); + this.activateUIButton(); + + // check to see if we've deleted this feature + if (this.getFeature(state.line.id) === undefined) return; + + //remove last added coordinate + state.line.removeCoordinate(`${state.currentVertexPosition}`); + if (state.line.isValid()) { + this.fire(Constants.events.CREATE, { + features: [state.line.toGeoJSON()] + }); + } else { + this.deleteFeature([state.line.id], { silent: true }); + this.changeMode(Constants.modes.SIMPLE_SELECT, {}, { silent: true }); + } + }, + + onTrash: function (state) { + this.deleteFeature([state.line.id], { silent: true }); + this.changeMode(Constants.modes.SIMPLE_SELECT); + }, + + toDisplayFeatures: function (state, geojson, display) { + const isActiveLine = geojson.properties.id === state.line.id; + geojson.properties.active = isActiveLine + ? Constants.activeStates.ACTIVE + : Constants.activeStates.INACTIVE; + if (!isActiveLine) return display(geojson); + // Only render the line if it has at least one real coordinate + if (geojson.geometry.coordinates.length < 2) return; + geojson.properties.meta = Constants.meta.FEATURE; + display( + createVertex( + state.line.id, + geojson.geometry.coordinates[ + state.direction === 'forward' + ? geojson.geometry.coordinates.length - 2 + : 1 + ], + `${state.direction === 'forward' ? geojson.geometry.coordinates.length - 2 : 1}`, + false + ) + ); + + display(geojson); + }, + + onTap: function (state, e) { return this._select(state, e); }, + onClick: function (state, e) { return this._select(state, e); }, +}; + +export default DrawLineString; diff --git a/src/modes/draw_point.js b/src/modes/draw_point.js deleted file mode 100644 index 896187b5..00000000 --- a/src/modes/draw_point.js +++ /dev/null @@ -1,65 +0,0 @@ -import * as CommonSelectors from '../lib/common_selectors.js'; -import * as Constants from '../constants.js'; - -const DrawPoint = {}; - -DrawPoint.onSetup = function() { - const point = this.newFeature({ - type: Constants.geojsonTypes.FEATURE, - properties: {}, - geometry: { - type: Constants.geojsonTypes.POINT, - coordinates: [] - } - }); - - this.addFeature(point); - - this.clearSelectedFeatures(); - this.updateUIClasses({ mouse: Constants.cursors.ADD }); - this.activateUIButton(Constants.types.POINT); - - this.setActionableState({ - trash: true - }); - - return { point }; -}; - -DrawPoint.stopDrawingAndRemove = function(state) { - this.deleteFeature([state.point.id], { silent: true }); - this.changeMode(Constants.modes.SIMPLE_SELECT); -}; - -DrawPoint.onTap = DrawPoint.onClick = function(state, e) { - this.updateUIClasses({ mouse: Constants.cursors.MOVE }); - state.point.updateCoordinate('', e.lngLat.lng, e.lngLat.lat); - this.fire(Constants.events.CREATE, { - features: [state.point.toGeoJSON()] - }); - this.changeMode(Constants.modes.SIMPLE_SELECT, { featureIds: [state.point.id] }); -}; - -DrawPoint.onStop = function(state) { - this.activateUIButton(); - if (!state.point.getCoordinate().length) { - this.deleteFeature([state.point.id], { silent: true }); - } -}; - -DrawPoint.toDisplayFeatures = function(state, geojson, display) { - // Never render the point we're drawing - const isActivePoint = geojson.properties.id === state.point.id; - geojson.properties.active = (isActivePoint) ? Constants.activeStates.ACTIVE : Constants.activeStates.INACTIVE; - if (!isActivePoint) return display(geojson); -}; - -DrawPoint.onTrash = DrawPoint.stopDrawingAndRemove; - -DrawPoint.onKeyUp = function(state, e) { - if (CommonSelectors.isEscapeKey(e) || CommonSelectors.isEnterKey(e)) { - return this.stopDrawingAndRemove(state, e); - } -}; - -export default DrawPoint; diff --git a/src/modes/draw_point.ts b/src/modes/draw_point.ts new file mode 100644 index 00000000..d18aefba --- /dev/null +++ b/src/modes/draw_point.ts @@ -0,0 +1,71 @@ +import * as CommonSelectors from '../lib/common_selectors'; +import * as Constants from '../constants'; +import {StrictFeature} from '../types/types'; + +const DrawPoint = { + onSetup() { + const point = this.newFeature({ + type: Constants.geojsonTypes.FEATURE, + properties: {}, + geometry: { + type: Constants.geojsonTypes.POINT, + coordinates: [] + } + }); + + this.addFeature(point); + this.clearSelectedFeatures(); + this.updateUIClasses({ mouse: Constants.cursors.ADD }); + this.activateUIButton(Constants.types.POINT); + + this.setActionableState({ + trash: true + }); + + return { point }; + }, + + stopDrawingAndRemove(state) { + this.deleteFeature([state.point.id], { silent: true }); + this.changeMode(Constants.modes.SIMPLE_SELECT); + }, + + _select(state, e) { + this.updateUIClasses({ mouse: Constants.cursors.MOVE }); + state.point.updateCoordinate('', e.lngLat.lng, e.lngLat.lat); + this.fire(Constants.events.CREATE, { + features: [state.point.toGeoJSON()] + }); + this.changeMode(Constants.modes.SIMPLE_SELECT, { + featureIds: [state.point.id] + }); + }, + + onStop(state) { + this.activateUIButton(); + if (!state.point.getCoordinate().length) { + this.deleteFeature([state.point.id], { silent: true }); + } + }, + + toDisplayFeatures(state, geojson: StrictFeature, display) { + const isActivePoint = geojson.properties.id === state.point.id; + geojson.properties.active = isActivePoint + ? Constants.activeStates.ACTIVE + : Constants.activeStates.INACTIVE; + if (!isActivePoint) return display(geojson); + }, + + onKeyUp(state, e) { + if (CommonSelectors.isEscapeKey(e) || CommonSelectors.isEnterKey(e)) { + return this.stopDrawingAndRemove(state, e); + } + }, + + onTap: function (state, e) { return this._select(state, e); }, + onClick: function (state, e) { return this._select(state, e); }, + onTrash: function (state, e) { return this.stopDrawingAndRemove(state, e); } +}; + +export default DrawPoint; + diff --git a/src/modes/draw_polygon.js b/src/modes/draw_polygon.js deleted file mode 100644 index a7561404..00000000 --- a/src/modes/draw_polygon.js +++ /dev/null @@ -1,141 +0,0 @@ -import * as CommonSelectors from '../lib/common_selectors.js'; -import doubleClickZoom from '../lib/double_click_zoom.js'; -import * as Constants from '../constants.js'; -import isEventAtCoordinates from '../lib/is_event_at_coordinates.js'; -import createVertex from '../lib/create_vertex.js'; - -const DrawPolygon = {}; - -DrawPolygon.onSetup = function() { - const polygon = this.newFeature({ - type: Constants.geojsonTypes.FEATURE, - properties: {}, - geometry: { - type: Constants.geojsonTypes.POLYGON, - coordinates: [[]] - } - }); - - this.addFeature(polygon); - - this.clearSelectedFeatures(); - doubleClickZoom.disable(this); - this.updateUIClasses({ mouse: Constants.cursors.ADD }); - this.activateUIButton(Constants.types.POLYGON); - this.setActionableState({ - trash: true - }); - - return { - polygon, - currentVertexPosition: 0 - }; -}; - -DrawPolygon.clickAnywhere = function(state, e) { - if (state.currentVertexPosition > 0 && isEventAtCoordinates(e, state.polygon.coordinates[0][state.currentVertexPosition - 1])) { - return this.changeMode(Constants.modes.SIMPLE_SELECT, { featureIds: [state.polygon.id] }); - } - this.updateUIClasses({ mouse: Constants.cursors.ADD }); - state.polygon.updateCoordinate(`0.${state.currentVertexPosition}`, e.lngLat.lng, e.lngLat.lat); - state.currentVertexPosition++; - state.polygon.updateCoordinate(`0.${state.currentVertexPosition}`, e.lngLat.lng, e.lngLat.lat); -}; - -DrawPolygon.clickOnVertex = function(state) { - return this.changeMode(Constants.modes.SIMPLE_SELECT, { featureIds: [state.polygon.id] }); -}; - -DrawPolygon.onMouseMove = function(state, e) { - state.polygon.updateCoordinate(`0.${state.currentVertexPosition}`, e.lngLat.lng, e.lngLat.lat); - if (CommonSelectors.isVertex(e)) { - this.updateUIClasses({ mouse: Constants.cursors.POINTER }); - } -}; - -DrawPolygon.onTap = DrawPolygon.onClick = function(state, e) { - if (CommonSelectors.isVertex(e)) return this.clickOnVertex(state, e); - return this.clickAnywhere(state, e); -}; - -DrawPolygon.onKeyUp = function(state, e) { - if (CommonSelectors.isEscapeKey(e)) { - this.deleteFeature([state.polygon.id], { silent: true }); - this.changeMode(Constants.modes.SIMPLE_SELECT); - } else if (CommonSelectors.isEnterKey(e)) { - this.changeMode(Constants.modes.SIMPLE_SELECT, { featureIds: [state.polygon.id] }); - } -}; - -DrawPolygon.onStop = function(state) { - this.updateUIClasses({ mouse: Constants.cursors.NONE }); - doubleClickZoom.enable(this); - this.activateUIButton(); - - // check to see if we've deleted this feature - if (this.getFeature(state.polygon.id) === undefined) return; - - //remove last added coordinate - state.polygon.removeCoordinate(`0.${state.currentVertexPosition}`); - if (state.polygon.isValid()) { - this.fire(Constants.events.CREATE, { - features: [state.polygon.toGeoJSON()] - }); - } else { - this.deleteFeature([state.polygon.id], { silent: true }); - this.changeMode(Constants.modes.SIMPLE_SELECT, {}, { silent: true }); - } -}; - -DrawPolygon.toDisplayFeatures = function(state, geojson, display) { - const isActivePolygon = geojson.properties.id === state.polygon.id; - geojson.properties.active = (isActivePolygon) ? Constants.activeStates.ACTIVE : Constants.activeStates.INACTIVE; - if (!isActivePolygon) return display(geojson); - - // Don't render a polygon until it has two positions - // (and a 3rd which is just the first repeated) - if (geojson.geometry.coordinates.length === 0) return; - - const coordinateCount = geojson.geometry.coordinates[0].length; - // 2 coordinates after selecting a draw type - // 3 after creating the first point - if (coordinateCount < 3) { - return; - } - geojson.properties.meta = Constants.meta.FEATURE; - display(createVertex(state.polygon.id, geojson.geometry.coordinates[0][0], '0.0', false)); - if (coordinateCount > 3) { - // Add a start position marker to the map, clicking on this will finish the feature - // This should only be shown when we're in a valid spot - const endPos = geojson.geometry.coordinates[0].length - 3; - display(createVertex(state.polygon.id, geojson.geometry.coordinates[0][endPos], `0.${endPos}`, false)); - } - if (coordinateCount <= 4) { - // If we've only drawn two positions (plus the closer), - // make a LineString instead of a Polygon - const lineCoordinates = [ - [geojson.geometry.coordinates[0][0][0], geojson.geometry.coordinates[0][0][1]], [geojson.geometry.coordinates[0][1][0], geojson.geometry.coordinates[0][1][1]] - ]; - // create an initial vertex so that we can track the first point on mobile devices - display({ - type: Constants.geojsonTypes.FEATURE, - properties: geojson.properties, - geometry: { - coordinates: lineCoordinates, - type: Constants.geojsonTypes.LINE_STRING - } - }); - if (coordinateCount === 3) { - return; - } - } - // render the Polygon - return display(geojson); -}; - -DrawPolygon.onTrash = function(state) { - this.deleteFeature([state.polygon.id], { silent: true }); - this.changeMode(Constants.modes.SIMPLE_SELECT); -}; - -export default DrawPolygon; diff --git a/src/modes/draw_polygon.ts b/src/modes/draw_polygon.ts new file mode 100644 index 00000000..b613dff3 --- /dev/null +++ b/src/modes/draw_polygon.ts @@ -0,0 +1,190 @@ +import * as CommonSelectors from '../lib/common_selectors'; +import { doubleClickZoom } from '../lib/double_click_zoom'; +import * as Constants from '../constants'; +import { isEventAtCoordinates } from '../lib/is_event_at_coordinates'; +import { createVertex } from '../lib/create_vertex'; + +const DrawPolygon = { + onSetup: function () { + const polygon = this.newFeature({ + type: Constants.geojsonTypes.FEATURE, + properties: {}, + geometry: { + type: Constants.geojsonTypes.POLYGON, + coordinates: [[]] + } + }); + + this.addFeature(polygon); + + this.clearSelectedFeatures(); + doubleClickZoom.disable(this); + this.updateUIClasses({ mouse: Constants.cursors.ADD }); + this.activateUIButton(Constants.types.POLYGON); + this.setActionableState({ + trash: true + }); + + return { + polygon, + currentVertexPosition: 0 + }; + }, + + clickAnywhere: function (state, e) { + if ( + state.currentVertexPosition > 0 && + isEventAtCoordinates( + e, + state.polygon.coordinates[0][state.currentVertexPosition - 1] + ) + ) { + return this.changeMode(Constants.modes.SIMPLE_SELECT, { + featureIds: [state.polygon.id] + }); + } + this.updateUIClasses({ mouse: Constants.cursors.ADD }); + state.polygon.updateCoordinate( + `0.${state.currentVertexPosition}`, + e.lngLat.lng, + e.lngLat.lat + ); + state.currentVertexPosition++; + state.polygon.updateCoordinate( + `0.${state.currentVertexPosition}`, + e.lngLat.lng, + e.lngLat.lat + ); + }, + + clickOnVertex: function (state) { + return this.changeMode(Constants.modes.SIMPLE_SELECT, { + featureIds: [state.polygon.id] + }); + }, + + onMouseMove: function (state, e) { + state.polygon.updateCoordinate( + `0.${state.currentVertexPosition}`, + e.lngLat.lng, + e.lngLat.lat + ); + if (CommonSelectors.isVertex(e)) { + this.updateUIClasses({ mouse: Constants.cursors.POINTER }); + } + }, + + _select: function (state, e) { + if (CommonSelectors.isVertex(e)) return this.clickOnVertex(state, e); + return this.clickAnywhere(state, e); + }, + + onKeyUp: function (state, e) { + if (CommonSelectors.isEscapeKey(e)) { + this.deleteFeature([state.polygon.id], { silent: true }); + this.changeMode(Constants.modes.SIMPLE_SELECT); + } else if (CommonSelectors.isEnterKey(e)) { + this.changeMode(Constants.modes.SIMPLE_SELECT, { + featureIds: [state.polygon.id] + }); + } + }, + + onStop: function (state) { + this.updateUIClasses({ mouse: Constants.cursors.NONE }); + doubleClickZoom.enable(this); + this.activateUIButton(); + + // check to see if we've deleted this feature + if (this.getFeature(state.polygon.id) === undefined) return; + + //remove last added coordinate + state.polygon.removeCoordinate(`0.${state.currentVertexPosition}`); + if (state.polygon.isValid()) { + this.fire(Constants.events.CREATE, { + features: [state.polygon.toGeoJSON()] + }); + } else { + this.deleteFeature([state.polygon.id], { silent: true }); + this.changeMode(Constants.modes.SIMPLE_SELECT, {}, { silent: true }); + } + }, + + toDisplayFeatures: function (state, geojson, display) { + const isActivePolygon = geojson.properties.id === state.polygon.id; + geojson.properties.active = isActivePolygon + ? Constants.activeStates.ACTIVE + : Constants.activeStates.INACTIVE; + if (!isActivePolygon) return display(geojson); + + // Don't render a polygon until it has two positions + // (and a 3rd which is just the first repeated) + if (geojson.geometry.coordinates.length === 0) return; + + const coordinateCount = geojson.geometry.coordinates[0].length; + // 2 coordinates after selecting a draw type + // 3 after creating the first point + if (coordinateCount < 3) { + return; + } + geojson.properties.meta = Constants.meta.FEATURE; + display( + createVertex( + state.polygon.id, + geojson.geometry.coordinates[0][0], + '0.0', + false + ) + ); + if (coordinateCount > 3) { + // Add a start position marker to the map, clicking on this will finish the feature + // This should only be shown when we're in a valid spot + const endPos = geojson.geometry.coordinates[0].length - 3; + display( + createVertex( + state.polygon.id, + geojson.geometry.coordinates[0][endPos], + `0.${endPos}`, + false + ) + ); + } + if (coordinateCount <= 4) { + // If we've only drawn two positions (plus the closer), + // make a LineString instead of a Polygon + const lineCoordinates = [ + [ + geojson.geometry.coordinates[0][0][0], + geojson.geometry.coordinates[0][0][1] + ], + [ + geojson.geometry.coordinates[0][1][0], + geojson.geometry.coordinates[0][1][1] + ] + ]; + // create an initial vertex so that we can track the first point on mobile devices + display({ + type: Constants.geojsonTypes.FEATURE, + properties: geojson.properties, + geometry: { + coordinates: lineCoordinates, + type: Constants.geojsonTypes.LINE_STRING + } + }); + if (coordinateCount === 3) { + return; + } + } + // render the Polygon + return display(geojson); + }, + + onTap: function (state, e) { return this._select(state, e); }, + onClick: function (state, e) { return this._select(state, e); }, + onTrash: function (state) { + this.deleteFeature([state.polygon.id], { silent: true }); + this.changeMode(Constants.modes.SIMPLE_SELECT); + } +}; + +export default DrawPolygon; diff --git a/src/modes/index.js b/src/modes/index.js deleted file mode 100644 index 38b3ec35..00000000 --- a/src/modes/index.js +++ /dev/null @@ -1,14 +0,0 @@ - -import simple_select from './simple_select.js'; -import direct_select from './direct_select.js'; -import draw_point from './draw_point.js'; -import draw_polygon from './draw_polygon.js'; -import draw_line_string from './draw_line_string.js'; - -export default { - simple_select, - direct_select, - draw_point, - draw_polygon, - draw_line_string, -}; diff --git a/src/modes/index.ts b/src/modes/index.ts new file mode 100644 index 00000000..df91bab3 --- /dev/null +++ b/src/modes/index.ts @@ -0,0 +1,13 @@ +import simple_select from './simple_select'; +import direct_select from './direct_select'; +import draw_point from './draw_point'; +import draw_polygon from './draw_polygon'; +import draw_line_string from './draw_line_string'; + +export { + simple_select, + direct_select, + draw_point, + draw_polygon, + draw_line_string +}; diff --git a/src/modes/mode_interface.js b/src/modes/mode_interface.js deleted file mode 100644 index 3cb2fa11..00000000 --- a/src/modes/mode_interface.js +++ /dev/null @@ -1,147 +0,0 @@ -import ModeInterface from './mode_interface_accessors.js'; -export default ModeInterface; - -/** - * Triggered while a mode is being transitioned into. - * @param opts {Object} - this is the object passed via `draw.changeMode('mode', opts)`; - * @name MODE.onSetup - * @returns {Object} - this object will be passed to all other life cycle functions - */ -ModeInterface.prototype.onSetup = function() {}; - -/** - * Triggered when a drag event is detected on the map - * @name MODE.onDrag - * @param state {Object} - a mutible state object created by onSetup - * @param e {Object} - the captured event that is triggering this life cycle event - */ -ModeInterface.prototype.onDrag = function() {}; - -/** - * Triggered when the mouse is clicked - * @name MODE.onClick - * @param state {Object} - a mutible state object created by onSetup - * @param e {Object} - the captured event that is triggering this life cycle event - */ -ModeInterface.prototype.onClick = function() {}; - -/** - * Triggered with the mouse is moved - * @name MODE.onMouseMove - * @param state {Object} - a mutible state object created by onSetup - * @param e {Object} - the captured event that is triggering this life cycle event - */ -ModeInterface.prototype.onMouseMove = function() {}; - -/** - * Triggered when the mouse button is pressed down - * @name MODE.onMouseDown - * @param state {Object} - a mutible state object created by onSetup - * @param e {Object} - the captured event that is triggering this life cycle event - */ -ModeInterface.prototype.onMouseDown = function() {}; - -/** - * Triggered when the mouse button is released - * @name MODE.onMouseUp - * @param state {Object} - a mutible state object created by onSetup - * @param e {Object} - the captured event that is triggering this life cycle event - */ -ModeInterface.prototype.onMouseUp = function() {}; - -/** - * Triggered when the mouse leaves the map's container - * @name MODE.onMouseOut - * @param state {Object} - a mutible state object created by onSetup - * @param e {Object} - the captured event that is triggering this life cycle event - */ -ModeInterface.prototype.onMouseOut = function() {}; - -/** - * Triggered when a key up event is detected - * @name MODE.onKeyUp - * @param state {Object} - a mutible state object created by onSetup - * @param e {Object} - the captured event that is triggering this life cycle event - */ -ModeInterface.prototype.onKeyUp = function() {}; - -/** - * Triggered when a key down event is detected - * @name MODE.onKeyDown - * @param state {Object} - a mutible state object created by onSetup - * @param e {Object} - the captured event that is triggering this life cycle event - */ -ModeInterface.prototype.onKeyDown = function() {}; - -/** - * Triggered when a touch event is started - * @name MODE.onTouchStart - * @param state {Object} - a mutible state object created by onSetup - * @param e {Object} - the captured event that is triggering this life cycle event - */ -ModeInterface.prototype.onTouchStart = function() {}; - -/** - * Triggered when one drags thier finger on a mobile device - * @name MODE.onTouchMove - * @param state {Object} - a mutible state object created by onSetup - * @param e {Object} - the captured event that is triggering this life cycle event - */ -ModeInterface.prototype.onTouchMove = function() {}; - -/** - * Triggered when one removes their finger from the map - * @name MODE.onTouchEnd - * @param state {Object} - a mutible state object created by onSetup - * @param e {Object} - the captured event that is triggering this life cycle event - */ -ModeInterface.prototype.onTouchEnd = function() {}; - -/** - * Triggered when one quicly taps the map - * @name MODE.onTap - * @param state {Object} - a mutible state object created by onSetup - * @param e {Object} - the captured event that is triggering this life cycle event - */ -ModeInterface.prototype.onTap = function() {}; - -/** - * Triggered when the mode is being exited, to be used for cleaning up artifacts such as invalid features - * @name MODE.onStop - * @param state {Object} - a mutible state object created by onSetup - */ -ModeInterface.prototype.onStop = function() {}; - -/** - * Triggered when [draw.trash()](https://github.com/mapbox/mapbox-gl-draw/blob/main/API.md#trash-draw) is called. - * @name MODE.onTrash - * @param state {Object} - a mutible state object created by onSetup - */ -ModeInterface.prototype.onTrash = function() {}; - -/** - * Triggered when [draw.combineFeatures()](https://github.com/mapbox/mapbox-gl-draw/blob/main/API.md#combinefeatures-draw) is called. - * @name MODE.onCombineFeature - * @param state {Object} - a mutible state object created by onSetup - */ -ModeInterface.prototype.onCombineFeature = function() {}; - -/** - * Triggered when [draw.uncombineFeatures()](https://github.com/mapbox/mapbox-gl-draw/blob/main/API.md#uncombinefeatures-draw) is called. - * @name MODE.onUncombineFeature - * @param state {Object} - a mutible state object created by onSetup - */ -ModeInterface.prototype.onUncombineFeature = function() {}; - -/** - * Triggered per feature on render to convert raw features into set of features for display on the map - * See [styling draw](https://github.com/mapbox/mapbox-gl-draw/blob/main/API.md#styling-draw) for information about what geojson properties Draw uses as part of rendering. - * @name MODE.toDisplayFeatures - * @param state {Object} - a mutible state object created by onSetup - * @param geojson {Object} - a geojson being evaulated. To render, pass to `display`. - * @param display {Function} - all geojson objects passed to this be rendered onto the map - */ -ModeInterface.prototype.toDisplayFeatures = function() { - throw new Error('You must overwrite toDisplayFeatures'); -}; - diff --git a/src/modes/mode_interface.ts b/src/modes/mode_interface.ts new file mode 100644 index 00000000..aebd2ef0 --- /dev/null +++ b/src/modes/mode_interface.ts @@ -0,0 +1,36 @@ +import ModeInterfaceAcessors from './mode_interface_accessors.js'; +import type { DrawCustomMode, StrictFeature } from '../types/types'; + +class ModeInterface extends ModeInterfaceAcessors implements DrawCustomMode { + // Triggered while a mode is being transitioned into. + onSetup: (opts: any) => any = (opts) => {}; + + // Event Handlers + onDrag: (state: any, event: any) => void = (state, event) => {}; + onClick: (state: any, event: any) => void = (state, event) => {}; + onMouseMove: (state: any, event: any) => void = (state, event) => {}; + onMouseDown: (state: any, event: any) => void = (state, event) => {}; + onMouseUp: (state: any, event: any) => void = (state, event) => {}; + onMouseOut: (state: any, event: any) => void = (state, event) => {}; + onKeyUp: (state: any, event: any) => void = (state, event) => {}; + onKeyDown: (state: any, event: any) => void = (state, event) => {}; + onTouchStart: (state: any, event: any) => void = (state, event) => {}; + onTouchMove: (state: any, event: any) => void = (state, event) => {}; + onTouchEnd: (state: any, event: any) => void = (state, event) => {}; + onTap: (state: any, event: any) => void = (state, event) => {}; + + // Lifecycle Events + onStop: (state: any) => void = (state) => {}; + onTrash: (state: any) => void = (state) => {}; + onCombineFeatures: (state: any) => void = (state) => {}; + onUncombineFeatures: (state: any) => void = (state) => {}; + + // Converts raw features into display features + toDisplayFeatures: (state: any, geojson: StrictFeature, push: (feature: StrictFeature) => void) => void = + (state, geojson, push) => { + throw new Error('You must overwrite toDisplayFeatures'); + }; +} + +export default ModeInterface; + diff --git a/src/modes/mode_interface_accessors.js b/src/modes/mode_interface_accessors.js deleted file mode 100644 index ee5eed86..00000000 --- a/src/modes/mode_interface_accessors.js +++ /dev/null @@ -1,231 +0,0 @@ -import * as Constants from '../constants.js'; -import featuresAt from '../lib/features_at.js'; -import Point from '../feature_types/point.js'; -import LineString from '../feature_types/line_string.js'; -import Polygon from '../feature_types/polygon.js'; -import MultiFeature from '../feature_types/multi_feature.js'; - -export default function ModeInterface(ctx) { - this.map = ctx.map; - this.drawConfig = JSON.parse(JSON.stringify(ctx.options || {})); - this._ctx = ctx; -} - -/** - * Sets Draw's interal selected state - * @name this.setSelected - * @param {DrawFeature[]} - whats selected as a [DrawFeature](https://github.com/mapbox/mapbox-gl-draw/blob/main/src/feature_types/feature.js) - */ -ModeInterface.prototype.setSelected = function(features) { - return this._ctx.store.setSelected(features); -}; - -/** - * Sets Draw's internal selected coordinate state - * @name this.setSelectedCoordinates - * @param {Object[]} coords - a array of {coord_path: 'string', feature_id: 'string'} - */ -ModeInterface.prototype.setSelectedCoordinates = function(coords) { - this._ctx.store.setSelectedCoordinates(coords); - coords.reduce((m, c) => { - if (m[c.feature_id] === undefined) { - m[c.feature_id] = true; - this._ctx.store.get(c.feature_id).changed(); - } - return m; - }, {}); -}; - -/** - * Get all selected features as a [DrawFeature](https://github.com/mapbox/mapbox-gl-draw/blob/main/src/feature_types/feature.js) - * @name this.getSelected - * @returns {DrawFeature[]} - */ -ModeInterface.prototype.getSelected = function() { - return this._ctx.store.getSelected(); -}; - -/** - * Get the ids of all currently selected features - * @name this.getSelectedIds - * @returns {String[]} - */ -ModeInterface.prototype.getSelectedIds = function() { - return this._ctx.store.getSelectedIds(); -}; - -/** - * Check if a feature is selected - * @name this.isSelected - * @param {String} id - a feature id - * @returns {Boolean} - */ -ModeInterface.prototype.isSelected = function(id) { - return this._ctx.store.isSelected(id); -}; - -/** - * Get a [DrawFeature](https://github.com/mapbox/mapbox-gl-draw/blob/main/src/feature_types/feature.js) by its id - * @name this.getFeature - * @param {String} id - a feature id - * @returns {DrawFeature} - */ -ModeInterface.prototype.getFeature = function(id) { - return this._ctx.store.get(id); -}; - -/** - * Add a feature to draw's internal selected state - * @name this.select - * @param {String} id - */ -ModeInterface.prototype.select = function(id) { - return this._ctx.store.select(id); -}; - -/** - * Remove a feature from draw's internal selected state - * @name this.delete - * @param {String} id - */ -ModeInterface.prototype.deselect = function(id) { - return this._ctx.store.deselect(id); -}; - -/** - * Delete a feature from draw - * @name this.deleteFeature - * @param {String} id - a feature id - */ -ModeInterface.prototype.deleteFeature = function(id, opts = {}) { - return this._ctx.store.delete(id, opts); -}; - -/** - * Add a [DrawFeature](https://github.com/mapbox/mapbox-gl-draw/blob/main/src/feature_types/feature.js) to draw. - * See `this.newFeature` for converting geojson into a DrawFeature - * @name this.addFeature - * @param {DrawFeature} feature - the feature to add - */ -ModeInterface.prototype.addFeature = function(feature, opts = {}) { - return this._ctx.store.add(feature, opts); -}; - -/** - * Clear all selected features - */ -ModeInterface.prototype.clearSelectedFeatures = function() { - return this._ctx.store.clearSelected(); -}; - -/** - * Clear all selected coordinates - */ -ModeInterface.prototype.clearSelectedCoordinates = function() { - return this._ctx.store.clearSelectedCoordinates(); -}; - -/** - * Indicate if the different action are currently possible with your mode - * See [draw.actionalbe](https://github.com/mapbox/mapbox-gl-draw/blob/main/API.md#drawactionable) for a list of possible actions. All undefined actions are set to **false** by default - * @name this.setActionableState - * @param {Object} actions - */ -ModeInterface.prototype.setActionableState = function(actions = {}) { - const newSet = { - trash: actions.trash || false, - combineFeatures: actions.combineFeatures || false, - uncombineFeatures: actions.uncombineFeatures || false - }; - return this._ctx.events.actionable(newSet); -}; - -/** - * Trigger a mode change - * @name this.changeMode - * @param {String} mode - the mode to transition into - * @param {Object} opts - the options object to pass to the new mode - * @param {Object} eventOpts - used to control what kind of events are emitted. - */ -ModeInterface.prototype.changeMode = function(mode, opts = {}, eventOpts = {}) { - return this._ctx.events.changeMode(mode, opts, eventOpts); -}; - -/** - * Fire a map event - * @name this.fire - * @param {String} eventName - the event name. - * @param {Object} eventData - the event data object. - */ -ModeInterface.prototype.fire = function(eventName, eventData) { - return this._ctx.events.fire(eventName, eventData); -}; - -/** - * Update the state of draw map classes - * @name this.updateUIClasses - * @param {Object} opts - */ -ModeInterface.prototype.updateUIClasses = function(opts) { - return this._ctx.ui.queueMapClasses(opts); -}; - -/** - * If a name is provided it makes that button active, else if makes all buttons inactive - * @name this.activateUIButton - * @param {String?} name - name of the button to make active, leave as undefined to set buttons to be inactive - */ -ModeInterface.prototype.activateUIButton = function(name) { - return this._ctx.ui.setActiveButton(name); -}; - -/** - * Get the features at the location of an event object or in a bbox - * @name this.featuresAt - * @param {Event||NULL} event - a mapbox-gl event object - * @param {BBOX||NULL} bbox - the area to get features from - * @param {String} bufferType - is this `click` or `tap` event, defaults to click - */ -ModeInterface.prototype.featuresAt = function(event, bbox, bufferType = 'click') { - if (bufferType !== 'click' && bufferType !== 'touch') throw new Error('invalid buffer type'); - return featuresAt[bufferType](event, bbox, this._ctx); -}; - -/** - * Create a new [DrawFeature](https://github.com/mapbox/mapbox-gl-draw/blob/main/src/feature_types/feature.js) from geojson - * @name this.newFeature - * @param {GeoJSONFeature} geojson - * @returns {DrawFeature} - */ -ModeInterface.prototype.newFeature = function(geojson) { - const type = geojson.geometry.type; - if (type === Constants.geojsonTypes.POINT) return new Point(this._ctx, geojson); - if (type === Constants.geojsonTypes.LINE_STRING) return new LineString(this._ctx, geojson); - if (type === Constants.geojsonTypes.POLYGON) return new Polygon(this._ctx, geojson); - return new MultiFeature(this._ctx, geojson); -}; - -/** - * Check is an object is an instance of a [DrawFeature](https://github.com/mapbox/mapbox-gl-draw/blob/main/src/feature_types/feature.js) - * @name this.isInstanceOf - * @param {String} type - `Point`, `LineString`, `Polygon`, `MultiFeature` - * @param {Object} feature - the object that needs to be checked - * @returns {Boolean} - */ -ModeInterface.prototype.isInstanceOf = function(type, feature) { - if (type === Constants.geojsonTypes.POINT) return feature instanceof Point; - if (type === Constants.geojsonTypes.LINE_STRING) return feature instanceof LineString; - if (type === Constants.geojsonTypes.POLYGON) return feature instanceof Polygon; - if (type === 'MultiFeature') return feature instanceof MultiFeature; - throw new Error(`Unknown feature class: ${type}`); -}; - -/** - * Force draw to rerender the feature of the provided id - * @name this.doRender - * @param {String} id - a feature id - */ -ModeInterface.prototype.doRender = function(id) { - return this._ctx.store.featureChanged(id); -}; - diff --git a/src/modes/mode_interface_accessors.ts b/src/modes/mode_interface_accessors.ts new file mode 100644 index 00000000..483f89f6 --- /dev/null +++ b/src/modes/mode_interface_accessors.ts @@ -0,0 +1,157 @@ +import * as Constants from '../constants'; +import * as featuresAt from '../lib/features_at'; +import Point from '../feature_types/point'; +import LineString from '../feature_types/line_string'; +import Polygon from '../feature_types/polygon'; +import MultiFeature from '../feature_types/multi_feature'; +import type { CTX, DrawOptions, StrictFeature, DrawFeature } from '../types/types'; +import type { Map } from 'mapbox-gl'; + +interface SelectedCoordinate { + coord_path: string; + feature_id: string; +} + +interface DrawActions { + trash?: boolean; + combineFeatures?: boolean; + uncombineFeatures?: boolean; +} + +export default class ModeInterface { + map: Map; + drawConfig: DrawOptions; + private _ctx: CTX; + + constructor(ctx: CTX) { + this.map = ctx.map; + this.drawConfig = { ...ctx.options }; + this._ctx = ctx; + } + + setSelected(features: string[]): void { + this._ctx.store.setSelected(features); + } + + setSelectedCoordinates(coords: SelectedCoordinate[]): void { + this._ctx.store.setSelectedCoordinates(coords); + coords.reduce( + (m, c) => { + if (!m[c.feature_id]) { + m[c.feature_id] = true; + this._ctx.store.get(c.feature_id)?.changed(); + } + return m; + }, + {} as Record + ); + } + + getSelected(): DrawFeature[] { + return this._ctx.store.getSelected(); + } + + getSelectedIds(): string[] { + return this._ctx.store.getSelectedIds(); + } + + isSelected(id: string): boolean { + return this._ctx.store.isSelected(id); + } + + getFeature(id: string): DrawFeature | undefined { + return this._ctx.store.get(id); + } + + select(id: string): void { + this._ctx.store.select(id); + } + + deselect(id: string): void { + this._ctx.store.deselect(id); + } + + deleteFeature(id: string | string[], opts: Record = {}): void { + this._ctx.store.delete(id, opts); + } + + addFeature(feature: DrawFeature, opts: Record = {}): void { + this._ctx.store.add(feature, opts); + } + + clearSelectedFeatures(): void { + this._ctx.store.clearSelected(); + } + + clearSelectedCoordinates(): void { + this._ctx.store.clearSelectedCoordinates(); + } + + setActionableState({ + trash, + combineFeatures, + uncombineFeatures + }: DrawActions): void { + this._ctx.events.actionable({ + trash: trash || false, + combineFeatures: combineFeatures || false, + uncombineFeatures: uncombineFeatures || false + }); + } + + changeMode( + mode: string, + opts: Record = {}, + eventOpts: Record = {} + ): void { + this._ctx.events.changeMode(mode, opts, eventOpts); + } + + fire(eventName: string, eventData: any): void { + this._ctx.events.fire(eventName, eventData); + } + + updateUIClasses(opts: Record): void { + this._ctx.ui.queueMapClasses(opts); + } + + activateUIButton(name?: string): void { + this._ctx.ui.setActiveButton(name); + } + + featuresAt( + event: any, + bbox: any, + bufferType: 'click' | 'touch' = 'click' + ): any { + if (bufferType !== 'click' && bufferType !== 'touch') { + throw new Error('invalid buffer type'); + } + return featuresAt[bufferType](event, bbox, this._ctx); + } + + newFeature(geojson: StrictFeature) { + const type = geojson.geometry.type; + if (type === Constants.geojsonTypes.POINT) + return new Point(this._ctx, geojson); + if (type === Constants.geojsonTypes.LINE_STRING) + return new LineString(this._ctx, geojson); + if (type === Constants.geojsonTypes.POLYGON) + return new Polygon(this._ctx, geojson); + return new MultiFeature(this._ctx, geojson); + } + + isInstanceOf(type: string, feature: any): boolean { + if (type === Constants.geojsonTypes.POINT) return feature instanceof Point; + if (type === Constants.geojsonTypes.LINE_STRING) + return feature instanceof LineString; + if (type === Constants.geojsonTypes.POLYGON) + return feature instanceof Polygon; + if (type === 'MultiFeature') return feature instanceof MultiFeature; + throw new Error(`Unknown feature class: ${type}`); + } + + doRender(id: string): void { + this._ctx.store.featureChanged(id); + } +} diff --git a/src/modes/object_to_mode.js b/src/modes/object_to_mode.ts similarity index 85% rename from src/modes/object_to_mode.js rename to src/modes/object_to_mode.ts index d71f4c60..2f9a09a2 100644 --- a/src/modes/object_to_mode.js +++ b/src/modes/object_to_mode.ts @@ -1,4 +1,5 @@ import ModeInterface from './mode_interface.js'; +import type { CTX, DrawCustomMode, StrictFeature } from '../types/types'; const eventMapper = { drag: 'onDrag', @@ -17,10 +18,10 @@ const eventMapper = { const eventKeys = Object.keys(eventMapper); -export default function(modeObject) { +export const objectToMode = (modeObject: DrawCustomMode) => { const modeObjectKeys = Object.keys(modeObject); - return function(ctx, startOpts = {}) { + return function (ctx: CTX, startOpts = {}) { let state = {}; const mode = modeObjectKeys.reduce((m, k) => { @@ -41,7 +42,7 @@ export default function(modeObject) { // handlers that are not present in the mode // to reduce on render calls for functions that // have no logic - eventKeys.forEach((key) => { + eventKeys.forEach(key => { const modeHandler = eventMapper[key]; let selector = () => false; if (modeObject[modeHandler]) { @@ -49,7 +50,6 @@ export default function(modeObject) { } this.on(key, selector, wrapper(modeHandler)); }); - }, stop() { mode.onStop(state); @@ -63,9 +63,9 @@ export default function(modeObject) { uncombineFeatures() { mode.onUncombineFeatures(state); }, - render(geojson, push) { + render(geojson: StrictFeature, push) { mode.toDisplayFeatures(state, geojson, push); } }; }; -} +}; diff --git a/src/modes/simple_select.js b/src/modes/simple_select.js deleted file mode 100644 index 62eab178..00000000 --- a/src/modes/simple_select.js +++ /dev/null @@ -1,389 +0,0 @@ -import * as CommonSelectors from '../lib/common_selectors.js'; -import mouseEventPoint from '../lib/mouse_event_point.js'; -import createSupplementaryPoints from '../lib/create_supplementary_points.js'; -import StringSet from '../lib/string_set.js'; -import doubleClickZoom from '../lib/double_click_zoom.js'; -import moveFeatures from '../lib/move_features.js'; -import * as Constants from '../constants.js'; - -const SimpleSelect = {}; - -SimpleSelect.onSetup = function(opts) { - // turn the opts into state. - const state = { - dragMoveLocation: null, - boxSelectStartLocation: null, - boxSelectElement: undefined, - boxSelecting: false, - canBoxSelect: false, - dragMoving: false, - canDragMove: false, - initialDragPanState: this.map.dragPan.isEnabled(), - initiallySelectedFeatureIds: opts.featureIds || [] - }; - - this.setSelected(state.initiallySelectedFeatureIds.filter(id => this.getFeature(id) !== undefined)); - this.fireActionable(); - - this.setActionableState({ - combineFeatures: true, - uncombineFeatures: true, - trash: true - }); - - return state; -}; - -SimpleSelect.fireUpdate = function() { - this.fire(Constants.events.UPDATE, { - action: Constants.updateActions.MOVE, - features: this.getSelected().map(f => f.toGeoJSON()) - }); -}; - -SimpleSelect.fireActionable = function() { - const selectedFeatures = this.getSelected(); - - const multiFeatures = selectedFeatures.filter( - feature => this.isInstanceOf('MultiFeature', feature) - ); - - let combineFeatures = false; - - if (selectedFeatures.length > 1) { - combineFeatures = true; - const featureType = selectedFeatures[0].type.replace('Multi', ''); - selectedFeatures.forEach((feature) => { - if (feature.type.replace('Multi', '') !== featureType) { - combineFeatures = false; - } - }); - } - - const uncombineFeatures = multiFeatures.length > 0; - const trash = selectedFeatures.length > 0; - - this.setActionableState({ - combineFeatures, uncombineFeatures, trash - }); -}; - -SimpleSelect.getUniqueIds = function(allFeatures) { - if (!allFeatures.length) return []; - const ids = allFeatures.map(s => s.properties.id) - .filter(id => id !== undefined) - .reduce((memo, id) => { - memo.add(id); - return memo; - }, new StringSet()); - - return ids.values(); -}; - -SimpleSelect.stopExtendedInteractions = function(state) { - if (state.boxSelectElement) { - if (state.boxSelectElement.parentNode) state.boxSelectElement.parentNode.removeChild(state.boxSelectElement); - state.boxSelectElement = null; - } - - if ((state.canDragMove || state.canBoxSelect) && state.initialDragPanState === true) { - this.map.dragPan.enable(); - } - - state.boxSelecting = false; - state.canBoxSelect = false; - state.dragMoving = false; - state.canDragMove = false; -}; - -SimpleSelect.onStop = function() { - doubleClickZoom.enable(this); -}; - -SimpleSelect.onMouseMove = function(state, e) { - const isFeature = CommonSelectors.isFeature(e); - if (isFeature && state.dragMoving) this.fireUpdate(); - - // On mousemove that is not a drag, stop extended interactions. - // This is useful if you drag off the canvas, release the button, - // then move the mouse back over the canvas --- we don't allow the - // interaction to continue then, but we do let it continue if you held - // the mouse button that whole time - this.stopExtendedInteractions(state); - - // Skip render - return true; -}; - -SimpleSelect.onMouseOut = function(state) { - // As soon as you mouse leaves the canvas, update the feature - if (state.dragMoving) return this.fireUpdate(); - - // Skip render - return true; -}; - -SimpleSelect.onTap = SimpleSelect.onClick = function(state, e) { - // Click (with or without shift) on no feature - if (CommonSelectors.noTarget(e)) return this.clickAnywhere(state, e); // also tap - if (CommonSelectors.isOfMetaType(Constants.meta.VERTEX)(e)) return this.clickOnVertex(state, e); //tap - if (CommonSelectors.isFeature(e)) return this.clickOnFeature(state, e); -}; - -SimpleSelect.clickAnywhere = function (state) { - // Clear the re-render selection - const wasSelected = this.getSelectedIds(); - if (wasSelected.length) { - this.clearSelectedFeatures(); - wasSelected.forEach(id => this.doRender(id)); - } - doubleClickZoom.enable(this); - this.stopExtendedInteractions(state); -}; - -SimpleSelect.clickOnVertex = function(state, e) { - // Enter direct select mode - this.changeMode(Constants.modes.DIRECT_SELECT, { - featureId: e.featureTarget.properties.parent, - coordPath: e.featureTarget.properties.coord_path, - startPos: e.lngLat - }); - this.updateUIClasses({ mouse: Constants.cursors.MOVE }); -}; - -SimpleSelect.startOnActiveFeature = function(state, e) { - // Stop any already-underway extended interactions - this.stopExtendedInteractions(state); - - // Disable map.dragPan immediately so it can't start - this.map.dragPan.disable(); - - // Re-render it and enable drag move - this.doRender(e.featureTarget.properties.id); - - // Set up the state for drag moving - state.canDragMove = true; - state.dragMoveLocation = e.lngLat; -}; - -SimpleSelect.clickOnFeature = function(state, e) { - // Stop everything - doubleClickZoom.disable(this); - this.stopExtendedInteractions(state); - - const isShiftClick = CommonSelectors.isShiftDown(e); - const selectedFeatureIds = this.getSelectedIds(); - const featureId = e.featureTarget.properties.id; - const isFeatureSelected = this.isSelected(featureId); - - // Click (without shift) on any selected feature but a point - if (!isShiftClick && isFeatureSelected && this.getFeature(featureId).type !== Constants.geojsonTypes.POINT) { - // Enter direct select mode - return this.changeMode(Constants.modes.DIRECT_SELECT, { - featureId - }); - } - - // Shift-click on a selected feature - if (isFeatureSelected && isShiftClick) { - // Deselect it - this.deselect(featureId); - this.updateUIClasses({ mouse: Constants.cursors.POINTER }); - if (selectedFeatureIds.length === 1) { - doubleClickZoom.enable(this); - } - // Shift-click on an unselected feature - } else if (!isFeatureSelected && isShiftClick) { - // Add it to the selection - this.select(featureId); - this.updateUIClasses({ mouse: Constants.cursors.MOVE }); - // Click (without shift) on an unselected feature - } else if (!isFeatureSelected && !isShiftClick) { - // Make it the only selected feature - selectedFeatureIds.forEach(id => this.doRender(id)); - this.setSelected(featureId); - this.updateUIClasses({ mouse: Constants.cursors.MOVE }); - } - - // No matter what, re-render the clicked feature - this.doRender(featureId); -}; - -SimpleSelect.onMouseDown = function(state, e) { - state.initialDragPanState = this.map.dragPan.isEnabled(); - if (CommonSelectors.isActiveFeature(e)) return this.startOnActiveFeature(state, e); - if (this.drawConfig.boxSelect && CommonSelectors.isShiftMousedown(e)) return this.startBoxSelect(state, e); -}; - -SimpleSelect.startBoxSelect = function(state, e) { - this.stopExtendedInteractions(state); - this.map.dragPan.disable(); - // Enable box select - state.boxSelectStartLocation = mouseEventPoint(e.originalEvent, this.map.getContainer()); - state.canBoxSelect = true; -}; - -SimpleSelect.onTouchStart = function(state, e) { - if (CommonSelectors.isActiveFeature(e)) return this.startOnActiveFeature(state, e); -}; - -SimpleSelect.onDrag = function(state, e) { - if (state.canDragMove) return this.dragMove(state, e); - if (this.drawConfig.boxSelect && state.canBoxSelect) return this.whileBoxSelect(state, e); -}; - -SimpleSelect.whileBoxSelect = function(state, e) { - state.boxSelecting = true; - this.updateUIClasses({ mouse: Constants.cursors.ADD }); - - // Create the box node if it doesn't exist - if (!state.boxSelectElement) { - state.boxSelectElement = document.createElement('div'); - state.boxSelectElement.classList.add(Constants.classes.BOX_SELECT); - this.map.getContainer().appendChild(state.boxSelectElement); - } - - // Adjust the box node's width and xy position - const current = mouseEventPoint(e.originalEvent, this.map.getContainer()); - const minX = Math.min(state.boxSelectStartLocation.x, current.x); - const maxX = Math.max(state.boxSelectStartLocation.x, current.x); - const minY = Math.min(state.boxSelectStartLocation.y, current.y); - const maxY = Math.max(state.boxSelectStartLocation.y, current.y); - const translateValue = `translate(${minX}px, ${minY}px)`; - state.boxSelectElement.style.transform = translateValue; - state.boxSelectElement.style.WebkitTransform = translateValue; - state.boxSelectElement.style.width = `${maxX - minX}px`; - state.boxSelectElement.style.height = `${maxY - minY}px`; -}; - -SimpleSelect.dragMove = function(state, e) { - // Dragging when drag move is enabled - state.dragMoving = true; - e.originalEvent.stopPropagation(); - - const delta = { - lng: e.lngLat.lng - state.dragMoveLocation.lng, - lat: e.lngLat.lat - state.dragMoveLocation.lat - }; - - moveFeatures(this.getSelected(), delta); - - state.dragMoveLocation = e.lngLat; -}; - -SimpleSelect.onTouchEnd = SimpleSelect.onMouseUp = function(state, e) { - // End any extended interactions - if (state.dragMoving) { - this.fireUpdate(); - } else if (state.boxSelecting) { - const bbox = [ - state.boxSelectStartLocation, - mouseEventPoint(e.originalEvent, this.map.getContainer()) - ]; - const featuresInBox = this.featuresAt(null, bbox, 'click'); - const idsToSelect = this.getUniqueIds(featuresInBox) - .filter(id => !this.isSelected(id)); - - if (idsToSelect.length) { - this.select(idsToSelect); - idsToSelect.forEach(id => this.doRender(id)); - this.updateUIClasses({ mouse: Constants.cursors.MOVE }); - } - } - this.stopExtendedInteractions(state); -}; - -SimpleSelect.toDisplayFeatures = function(state, geojson, display) { - geojson.properties.active = (this.isSelected(geojson.properties.id)) ? - Constants.activeStates.ACTIVE : Constants.activeStates.INACTIVE; - display(geojson); - this.fireActionable(); - if (geojson.properties.active !== Constants.activeStates.ACTIVE || - geojson.geometry.type === Constants.geojsonTypes.POINT) return; - createSupplementaryPoints(geojson).forEach(display); -}; - -SimpleSelect.onTrash = function() { - this.deleteFeature(this.getSelectedIds()); - this.fireActionable(); -}; - -SimpleSelect.onCombineFeatures = function() { - const selectedFeatures = this.getSelected(); - - if (selectedFeatures.length === 0 || selectedFeatures.length < 2) return; - - const coordinates = [], featuresCombined = []; - const featureType = selectedFeatures[0].type.replace('Multi', ''); - - for (let i = 0; i < selectedFeatures.length; i++) { - const feature = selectedFeatures[i]; - - if (feature.type.replace('Multi', '') !== featureType) { - return; - } - if (feature.type.includes('Multi')) { - feature.getCoordinates().forEach((subcoords) => { - coordinates.push(subcoords); - }); - } else { - coordinates.push(feature.getCoordinates()); - } - - featuresCombined.push(feature.toGeoJSON()); - } - - if (featuresCombined.length > 1) { - const multiFeature = this.newFeature({ - type: Constants.geojsonTypes.FEATURE, - properties: featuresCombined[0].properties, - geometry: { - type: `Multi${featureType}`, - coordinates - } - }); - - this.addFeature(multiFeature); - this.deleteFeature(this.getSelectedIds(), { silent: true }); - this.setSelected([multiFeature.id]); - - this.fire(Constants.events.COMBINE_FEATURES, { - createdFeatures: [multiFeature.toGeoJSON()], - deletedFeatures: featuresCombined - }); - } - this.fireActionable(); -}; - -SimpleSelect.onUncombineFeatures = function() { - const selectedFeatures = this.getSelected(); - if (selectedFeatures.length === 0) return; - - const createdFeatures = []; - const featuresUncombined = []; - - for (let i = 0; i < selectedFeatures.length; i++) { - const feature = selectedFeatures[i]; - - if (this.isInstanceOf('MultiFeature', feature)) { - feature.getFeatures().forEach((subFeature) => { - this.addFeature(subFeature); - subFeature.properties = feature.properties; - createdFeatures.push(subFeature.toGeoJSON()); - this.select([subFeature.id]); - }); - this.deleteFeature(feature.id, { silent: true }); - featuresUncombined.push(feature.toGeoJSON()); - } - } - - if (createdFeatures.length > 1) { - this.fire(Constants.events.UNCOMBINE_FEATURES, { - createdFeatures, - deletedFeatures: featuresUncombined - }); - } - this.fireActionable(); -}; - -export default SimpleSelect; diff --git a/src/modes/simple_select.ts b/src/modes/simple_select.ts new file mode 100644 index 00000000..00d800b2 --- /dev/null +++ b/src/modes/simple_select.ts @@ -0,0 +1,425 @@ +import * as CommonSelectors from '../lib/common_selectors'; +import { mouseEventPoint } from '../lib/mouse_event_point'; +import { createSupplementaryPoints } from '../lib/create_supplementary_points'; +import StringSet from '../lib/string_set'; +import { doubleClickZoom } from '../lib/double_click_zoom'; +import { moveFeatures } from '../lib/move_features'; +import * as Constants from '../constants'; + +const SimpleSelect = { + + onSetup: function (opts) { + // turn the opts into state. + const state = { + dragMoveLocation: null, + boxSelectStartLocation: null, + boxSelectElement: undefined, + boxSelecting: false, + canBoxSelect: false, + dragMoving: false, + canDragMove: false, + initialDragPanState: this.map.dragPan.isEnabled(), + initiallySelectedFeatureIds: opts.featureIds || [] + }; + + this.setSelected( + state.initiallySelectedFeatureIds.filter( + id => this.getFeature(id) !== undefined + ) + ); + this.fireActionable(); + + this.setActionableState({ + combineFeatures: true, + uncombineFeatures: true, + trash: true + }); + + return state; + }, + + fireUpdate: function () { + this.fire(Constants.events.UPDATE, { + action: Constants.updateActions.MOVE, + features: this.getSelected().map(f => f.toGeoJSON()) + }); + }, + + fireActionable: function () { + const selectedFeatures = this.getSelected(); + + const multiFeatures = selectedFeatures.filter(feature => + this.isInstanceOf('MultiFeature', feature) + ); + + let combineFeatures = false; + + if (selectedFeatures.length > 1) { + combineFeatures = true; + const featureType = selectedFeatures[0].type.replace('Multi', ''); + selectedFeatures.forEach(feature => { + if (feature.type.replace('Multi', '') !== featureType) { + combineFeatures = false; + } + }); + } + + const uncombineFeatures = multiFeatures.length > 0; + const trash = selectedFeatures.length > 0; + + this.setActionableState({ + combineFeatures, + uncombineFeatures, + trash + }); + }, + + getUniqueIds: function (allFeatures) { + if (!allFeatures.length) return []; + const ids = allFeatures + .map(s => s.properties.id) + .filter(id => id !== undefined) + .reduce((memo, id) => { + memo.add(id); + return memo; + }, new StringSet()); + + return ids.values(); + }, + + stopExtendedInteractions: function (state) { + if (state.boxSelectElement) { + if (state.boxSelectElement.parentNode) + state.boxSelectElement.parentNode.removeChild(state.boxSelectElement); + state.boxSelectElement = null; + } + + if ( + (state.canDragMove || state.canBoxSelect) && + state.initialDragPanState === true + ) { + this.map.dragPan.enable(); + } + + state.boxSelecting = false; + state.canBoxSelect = false; + state.dragMoving = false; + state.canDragMove = false; + }, + + onStop: function () { + doubleClickZoom.enable(this); + }, + + onMouseMove: function (state, e) { + const isFeature = CommonSelectors.isFeature(e); + if (isFeature && state.dragMoving) this.fireUpdate(); + + // On mousemove that is not a drag, stop extended interactions. + // This is useful if you drag off the canvas, release the button, + // then move the mouse back over the canvas --- we don't allow the + // interaction to continue then, but we do let it continue if you held + // the mouse button that whole time + this.stopExtendedInteractions(state); + + // Skip render + return true; + }, + + onMouseOut: function (state) { + // As soon as you mouse leaves the canvas, update the feature + if (state.dragMoving) return this.fireUpdate(); + + // Skip render + return true; + }, + + _select: function (state, e) { + // Click (with or without shift) on no feature + if (CommonSelectors.noTarget(e)) return this.clickAnywhere(state, e); // also tap + if (CommonSelectors.isOfMetaType(Constants.meta.VERTEX)(e)) + return this.clickOnVertex(state, e); //tap + if (CommonSelectors.isFeature(e)) return this.clickOnFeature(state, e); + }, + + clickAnywhere: function (state) { + // Clear the re-render selection + const wasSelected = this.getSelectedIds(); + if (wasSelected.length) { + this.clearSelectedFeatures(); + wasSelected.forEach(id => this.doRender(id)); + } + doubleClickZoom.enable(this); + this.stopExtendedInteractions(state); + }, + + clickOnVertex: function (state, e) { + // Enter direct select mode + this.changeMode(Constants.modes.DIRECT_SELECT, { + featureId: e.featureTarget.properties.parent, + coordPath: e.featureTarget.properties.coord_path, + startPos: e.lngLat + }); + this.updateUIClasses({ mouse: Constants.cursors.MOVE }); + }, + + startOnActiveFeature: function (state, e) { + // Stop any already-underway extended interactions + this.stopExtendedInteractions(state); + + // Disable map.dragPan immediately so it can't start + this.map.dragPan.disable(); + + // Re-render it and enable drag move + this.doRender(e.featureTarget.properties.id); + + // Set up the state for drag moving + state.canDragMove = true; + state.dragMoveLocation = e.lngLat; + }, + + clickOnFeature: function (state, e) { + // Stop everything + doubleClickZoom.disable(this); + this.stopExtendedInteractions(state); + + const isShiftClick = CommonSelectors.isShiftDown(e); + const selectedFeatureIds = this.getSelectedIds(); + const featureId = e.featureTarget.properties.id; + const isFeatureSelected = this.isSelected(featureId); + + // Click (without shift) on any selected feature but a point + if ( + !isShiftClick && + isFeatureSelected && + this.getFeature(featureId).type !== Constants.geojsonTypes.POINT + ) { + // Enter direct select mode + return this.changeMode(Constants.modes.DIRECT_SELECT, { + featureId + }); + } + + // Shift-click on a selected feature + if (isFeatureSelected && isShiftClick) { + // Deselect it + this.deselect(featureId); + this.updateUIClasses({ mouse: Constants.cursors.POINTER }); + if (selectedFeatureIds.length === 1) { + doubleClickZoom.enable(this); + } + // Shift-click on an unselected feature + } else if (!isFeatureSelected && isShiftClick) { + // Add it to the selection + this.select(featureId); + this.updateUIClasses({ mouse: Constants.cursors.MOVE }); + // Click (without shift) on an unselected feature + } else if (!isFeatureSelected && !isShiftClick) { + // Make it the only selected feature + selectedFeatureIds.forEach(id => this.doRender(id)); + this.setSelected(featureId); + this.updateUIClasses({ mouse: Constants.cursors.MOVE }); + } + + // No matter what, re-render the clicked feature + this.doRender(featureId); + }, + + onMouseDown: function (state, e) { + state.initialDragPanState = this.map.dragPan.isEnabled(); + if (CommonSelectors.isActiveFeature(e)) + return this.startOnActiveFeature(state, e); + if (this.drawConfig.boxSelect && CommonSelectors.isShiftMousedown(e)) + return this.startBoxSelect(state, e); + }, + + startBoxSelect: function (state, e) { + this.stopExtendedInteractions(state); + this.map.dragPan.disable(); + // Enable box select + state.boxSelectStartLocation = mouseEventPoint( + e.originalEvent, + this.map.getContainer() + ); + state.canBoxSelect = true; + }, + + onTouchStart: function (state, e) { + if (CommonSelectors.isActiveFeature(e)) + return this.startOnActiveFeature(state, e); + }, + + onDrag: function (state, e) { + if (state.canDragMove) return this.dragMove(state, e); + if (this.drawConfig.boxSelect && state.canBoxSelect) + return this.whileBoxSelect(state, e); + }, + + whileBoxSelect: function (state, e) { + state.boxSelecting = true; + this.updateUIClasses({ mouse: Constants.cursors.ADD }); + + // Create the box node if it doesn't exist + if (!state.boxSelectElement) { + state.boxSelectElement = document.createElement('div'); + state.boxSelectElement.classList.add(Constants.classes.BOX_SELECT); + this.map.getContainer().appendChild(state.boxSelectElement); + } + + // Adjust the box node's width and xy position + const current = mouseEventPoint(e.originalEvent, this.map.getContainer()); + const minX = Math.min(state.boxSelectStartLocation.x, current.x); + const maxX = Math.max(state.boxSelectStartLocation.x, current.x); + const minY = Math.min(state.boxSelectStartLocation.y, current.y); + const maxY = Math.max(state.boxSelectStartLocation.y, current.y); + const translateValue = `translate(${minX}px, ${minY}px)`; + state.boxSelectElement.style.transform = translateValue; + state.boxSelectElement.style.WebkitTransform = translateValue; + state.boxSelectElement.style.width = `${maxX - minX}px`; + state.boxSelectElement.style.height = `${maxY - minY}px`; + }, + + dragMove: function (state, e) { + // Dragging when drag move is enabled + state.dragMoving = true; + e.originalEvent.stopPropagation(); + + const delta = { + lng: e.lngLat.lng - state.dragMoveLocation.lng, + lat: e.lngLat.lat - state.dragMoveLocation.lat + }; + + moveFeatures(this.getSelected(), delta); + + state.dragMoveLocation = e.lngLat; + }, + + _end: function (state, e) { + // End any extended interactions + if (state.dragMoving) { + this.fireUpdate(); + } else if (state.boxSelecting) { + const bbox = [ + state.boxSelectStartLocation, + mouseEventPoint(e.originalEvent, this.map.getContainer()) + ]; + const featuresInBox = this.featuresAt(null, bbox, 'click'); + const idsToSelect = this.getUniqueIds(featuresInBox).filter( + id => !this.isSelected(id) + ); + + if (idsToSelect.length) { + this.select(idsToSelect); + idsToSelect.forEach(id => this.doRender(id)); + this.updateUIClasses({ mouse: Constants.cursors.MOVE }); + } + } + this.stopExtendedInteractions(state); + }, + + toDisplayFeatures: function (state, geojson, display) { + geojson.properties.active = this.isSelected(geojson.properties.id) + ? Constants.activeStates.ACTIVE + : Constants.activeStates.INACTIVE; + display(geojson); + this.fireActionable(); + if ( + geojson.properties.active !== Constants.activeStates.ACTIVE || + geojson.geometry.type === Constants.geojsonTypes.POINT + ) + return; + createSupplementaryPoints(geojson).forEach(display); + }, + + onTrash: function () { + this.deleteFeature(this.getSelectedIds()); + this.fireActionable(); + }, + + onCombineFeatures: function () { + const selectedFeatures = this.getSelected(); + + if (selectedFeatures.length === 0 || selectedFeatures.length < 2) return; + + const coordinates = [], + featuresCombined = []; + const featureType = selectedFeatures[0].type.replace('Multi', ''); + + for (let i = 0; i < selectedFeatures.length; i++) { + const feature = selectedFeatures[i]; + + if (feature.type.replace('Multi', '') !== featureType) { + return; + } + if (feature.type.includes('Multi')) { + feature.getCoordinates().forEach(subcoords => { + coordinates.push(subcoords); + }); + } else { + coordinates.push(feature.getCoordinates()); + } + + featuresCombined.push(feature.toGeoJSON()); + } + + if (featuresCombined.length > 1) { + const multiFeature = this.newFeature({ + type: Constants.geojsonTypes.FEATURE, + properties: featuresCombined[0].properties, + geometry: { + type: `Multi${featureType}`, + coordinates + } + }); + + this.addFeature(multiFeature); + this.deleteFeature(this.getSelectedIds(), { silent: true }); + this.setSelected([multiFeature.id]); + + this.fire(Constants.events.COMBINE_FEATURES, { + createdFeatures: [multiFeature.toGeoJSON()], + deletedFeatures: featuresCombined + }); + } + this.fireActionable(); + }, + + onUncombineFeatures: function () { + const selectedFeatures = this.getSelected(); + if (selectedFeatures.length === 0) return; + + const createdFeatures = []; + const featuresUncombined = []; + + for (let i = 0; i < selectedFeatures.length; i++) { + const feature = selectedFeatures[i]; + + if (this.isInstanceOf('MultiFeature', feature)) { + feature.getFeatures().forEach(subFeature => { + this.addFeature(subFeature); + subFeature.properties = feature.properties; + createdFeatures.push(subFeature.toGeoJSON()); + this.select([subFeature.id]); + }); + this.deleteFeature(feature.id, { silent: true }); + featuresUncombined.push(feature.toGeoJSON()); + } + } + + if (createdFeatures.length > 1) { + this.fire(Constants.events.UNCOMBINE_FEATURES, { + createdFeatures, + deletedFeatures: featuresUncombined + }); + } + this.fireActionable(); + }, + + onTap: function (state, e) { return this._select(state, e); }, + onClick: function (state, e) { return this._select(state, e); }, + onMouseUp: function (state, e) { return this._end(state, e); }, + onTouchEnd: function (state, e) { return this._end(state, e); } + +}; + +export default SimpleSelect; diff --git a/src/options.js b/src/options.js deleted file mode 100644 index 8e88928e..00000000 --- a/src/options.js +++ /dev/null @@ -1,68 +0,0 @@ -import * as Constants from './constants.js'; - -import styles from './lib/theme.js'; -import modes from './modes/index.js'; - -const defaultOptions = { - defaultMode: Constants.modes.SIMPLE_SELECT, - keybindings: true, - touchEnabled: true, - clickBuffer: 2, - touchBuffer: 25, - boxSelect: true, - displayControlsDefault: true, - styles, - modes, - controls: {}, - userProperties: false, - suppressAPIEvents: true -}; - -const showControls = { - point: true, - line_string: true, - polygon: true, - trash: true, - combine_features: true, - uncombine_features: true -}; - -const hideControls = { - point: false, - line_string: false, - polygon: false, - trash: false, - combine_features: false, - uncombine_features: false -}; - -function addSources(styles, sourceBucket) { - return styles.map((style) => { - if (style.source) return style; - return Object.assign({}, style, { - id: `${style.id}.${sourceBucket}`, - source: (sourceBucket === 'hot') ? Constants.sources.HOT : Constants.sources.COLD - }); - }); -} - -export default function(options = {}) { - let withDefaults = Object.assign({}, options); - - if (!options.controls) { - withDefaults.controls = {}; - } - - if (options.displayControlsDefault === false) { - withDefaults.controls = Object.assign({}, hideControls, options.controls); - } else { - withDefaults.controls = Object.assign({}, showControls, options.controls); - } - - withDefaults = Object.assign({}, defaultOptions, withDefaults); - - // Layers with a shared source should be adjacent for performance reasons - withDefaults.styles = addSources(withDefaults.styles, 'cold').concat(addSources(withDefaults.styles, 'hot')); - - return withDefaults; -} diff --git a/src/options.ts b/src/options.ts new file mode 100644 index 00000000..0454079f --- /dev/null +++ b/src/options.ts @@ -0,0 +1,79 @@ +import * as Constants from './constants'; +import * as modes from './modes/index'; +import styles from './lib/theme'; +import { DrawOptions } from './types/types'; + +type Controls = { + point: boolean; + line_string: boolean; + polygon: boolean; + trash: boolean; + combine_features: boolean; + uncombine_features: boolean; + [key: string]: boolean; +}; + +const defaultOptions = { + defaultMode: Constants.modes.SIMPLE_SELECT, + keybindings: true, + touchEnabled: true, + clickBuffer: 2, + touchBuffer: 25, + boxSelect: true, + displayControlsDefault: true, + styles, + modes, + controls: {}, + userProperties: false, + suppressAPIEvents: true +}; + +const showControls: Controls = { + point: true, + line_string: true, + polygon: true, + trash: true, + combine_features: true, + uncombine_features: true +}; + +const hideControls: Controls = { + point: false, + line_string: false, + polygon: false, + trash: false, + combine_features: false, + uncombine_features: false +}; + +function addSources(styles: any[], sourceBucket: 'hot' | 'cold'): any[] { + return styles.map(style => { + if (style.source) return style; + return { + ...style, + id: `${style.id}.${sourceBucket}`, + source: sourceBucket === 'hot' ? Constants.sources.HOT : Constants.sources.COLD + }; + }); +} + +export const configureOptions = (options: DrawOptions = {}): DrawOptions => { + let withDefaults = { ...options }; + + if (!options.controls) { + withDefaults.controls = {}; + } + + withDefaults.controls = options.displayControlsDefault === false + ? { ...hideControls, ...options.controls } + : { ...showControls, ...options.controls }; + + withDefaults = { ...defaultOptions, ...withDefaults } as DrawOptions; + + withDefaults.styles = addSources(withDefaults.styles || [], 'cold').concat( + addSources(withDefaults.styles || [], 'hot') + ); + + return withDefaults; +} + diff --git a/src/render.js b/src/render.ts similarity index 59% rename from src/render.js rename to src/render.ts index ae63efb0..dfded13e 100644 --- a/src/render.js +++ b/src/render.ts @@ -1,9 +1,11 @@ -import * as Constants from './constants.js'; +import * as Constants from './constants'; export default function render() { // eslint-disable-next-line no-invalid-this const store = this; - const mapExists = store.ctx.map && store.ctx.map.getSource(Constants.sources.HOT) !== undefined; + const mapExists = + store.ctx.map && + store.ctx.map.getSource(Constants.sources.HOT) !== undefined; if (!mapExists) return cleanup(); const mode = store.ctx.events.currentModeName(); @@ -17,24 +19,34 @@ export default function render() { newColdIds = store.getAllIds(); } else { newHotIds = store.getChangedIds().filter(id => store.get(id) !== undefined); - newColdIds = store.sources.hot.filter(geojson => geojson.properties.id && newHotIds.indexOf(geojson.properties.id) === -1 && store.get(geojson.properties.id) !== undefined).map(geojson => geojson.properties.id); + newColdIds = store.sources.hot + .filter( + geojson => + geojson.properties.id && + newHotIds.indexOf(geojson.properties.id) === -1 && + store.get(geojson.properties.id) !== undefined + ) + .map(geojson => geojson.properties.id); } store.sources.hot = []; const lastColdCount = store.sources.cold.length; - store.sources.cold = store.isDirty ? [] : store.sources.cold.filter((geojson) => { - const id = geojson.properties.id || geojson.properties.parent; - return newHotIds.indexOf(id) === -1; - }); + store.sources.cold = store.isDirty + ? [] + : store.sources.cold.filter(geojson => { + const id = geojson.properties.id || geojson.properties.parent; + return newHotIds.indexOf(id) === -1; + }); - const coldChanged = lastColdCount !== store.sources.cold.length || newColdIds.length > 0; + const coldChanged = + lastColdCount !== store.sources.cold.length || newColdIds.length > 0; newHotIds.forEach(id => renderFeature(id, 'hot')); newColdIds.forEach(id => renderFeature(id, 'cold')); - function renderFeature(id, source) { + function renderFeature(id: string, source: string) { const feature = store.get(id); const featureInternal = feature.internal(mode); - store.ctx.events.currentModeRender(featureInternal, (geojson) => { + store.ctx.events.currentModeRender(featureInternal, geojson => { geojson.properties.mode = mode; store.sources[source].push(geojson); }); diff --git a/src/setup.js b/src/setup.ts similarity index 85% rename from src/setup.js rename to src/setup.ts index e1524db8..8a5f9909 100644 --- a/src/setup.js +++ b/src/setup.ts @@ -1,10 +1,9 @@ -import events from './events.js'; -import Store from './store.js'; -import ui from './ui.js'; -import * as Constants from './constants.js'; - -export default function(ctx) { +import events from './events'; +import Store from './store'; +import ui from './ui'; +import * as Constants from './constants'; +export default function (ctx) { let controlContainer = null; let mapLoadedInterval = null; @@ -24,7 +23,8 @@ export default function(ctx) { ctx.container = null; ctx.store = null; - if (controlContainer && controlContainer.parentNode) controlContainer.parentNode.removeChild(controlContainer); + if (controlContainer && controlContainer.parentNode) + controlContainer.parentNode.removeChild(controlContainer); controlContainer = null; return this; @@ -43,7 +43,6 @@ export default function(ctx) { ctx.container = map.getContainer(); ctx.store = new Store(ctx); - controlContainer = ctx.ui.addButtons(); if (ctx.options.boxSelect) { @@ -63,7 +62,9 @@ export default function(ctx) { setup.connect(); } else { map.on('load', setup.connect); - mapLoadedInterval = setInterval(() => { if (map.loaded()) setup.connect(); }, 16); + mapLoadedInterval = setInterval(() => { + if (map.loaded()) setup.connect(); + }, 16); } ctx.events.start(); @@ -88,7 +89,7 @@ export default function(ctx) { type: 'geojson' }); - ctx.options.styles.forEach((style) => { + ctx.options.styles.forEach(style => { ctx.map.addLayer(style); }); @@ -98,7 +99,7 @@ export default function(ctx) { // Check for layers and sources before attempting to remove // If user adds draw control and removes it before the map is loaded, layers and sources will be missing removeLayers() { - ctx.options.styles.forEach((style) => { + ctx.options.styles.forEach(style => { if (ctx.map.getLayer(style.id)) { ctx.map.removeLayer(style.id); } diff --git a/src/store.js b/src/store.js deleted file mode 100644 index ed163a74..00000000 --- a/src/store.js +++ /dev/null @@ -1,405 +0,0 @@ -import toDenseArray from './lib/to_dense_array.js'; -import StringSet from './lib/string_set.js'; -import render from './render.js'; -import * as Constants from './constants.js'; - -export default function Store(ctx) { - this._features = {}; - this._featureIds = new StringSet(); - this._selectedFeatureIds = new StringSet(); - this._selectedCoordinates = []; - this._changedFeatureIds = new StringSet(); - this._emitSelectionChange = false; - this._mapInitialConfig = {}; - this.ctx = ctx; - this.sources = { - hot: [], - cold: [] - }; - - // Deduplicate requests to render and tie them to animation frames. - let renderRequest; - this.render = () => { - if (!renderRequest) { - renderRequest = requestAnimationFrame(() => { - renderRequest = null; - render.call(this); - - // Fire deduplicated selection change event - if (this._emitSelectionChange) { - this.ctx.events.fire(Constants.events.SELECTION_CHANGE, { - features: this.getSelected().map(feature => feature.toGeoJSON()), - points: this.getSelectedCoordinates().map(coordinate => ({ - type: Constants.geojsonTypes.FEATURE, - properties: {}, - geometry: { - type: Constants.geojsonTypes.POINT, - coordinates: coordinate.coordinates - } - })) - }); - - this._emitSelectionChange = false; - } - - // Fire render event - this.ctx.events.fire(Constants.events.RENDER, {}); - }); - } - }; - - this.isDirty = false; -} - -/** - * Delays all rendering until the returned function is invoked - * @return {Function} renderBatch - */ -Store.prototype.createRenderBatch = function() { - const holdRender = this.render; - let numRenders = 0; - this.render = function() { - numRenders++; - }; - - return () => { - this.render = holdRender; - if (numRenders > 0) { - this.render(); - } - }; -}; - -/** - * Sets the store's state to dirty. - * @return {Store} this - */ -Store.prototype.setDirty = function() { - this.isDirty = true; - return this; -}; - -/** - * Sets a feature's state to changed. - * @param {string} featureId - * @return {Store} this - */ -Store.prototype.featureCreated = function(featureId, options = {}) { - this._changedFeatureIds.add(featureId); - - const silent = options.silent != null ? options.silent : this.ctx.options.suppressAPIEvents; - if (silent !== true) { - const feature = this.get(featureId); - this.ctx.events.fire(Constants.events.CREATE, { - features: [feature.toGeoJSON()] - }); - } - - return this; -}; - -/** - * Sets a feature's state to changed. - * @param {string} featureId - * @return {Store} this - */ -Store.prototype.featureChanged = function(featureId, options = {}) { - this._changedFeatureIds.add(featureId); - - const silent = options.silent != null ? options.silent : this.ctx.options.suppressAPIEvents; - if (silent !== true) { - this.ctx.events.fire(Constants.events.UPDATE, { - action: options.action ? options.action : Constants.updateActions.CHANGE_COORDINATES, - features: [this.get(featureId).toGeoJSON()] - }); - } - - return this; -}; - -/** - * Gets the ids of all features currently in changed state. - * @return {Store} this - */ -Store.prototype.getChangedIds = function() { - return this._changedFeatureIds.values(); -}; - -/** - * Sets all features to unchanged state. - * @return {Store} this - */ -Store.prototype.clearChangedIds = function() { - this._changedFeatureIds.clear(); - return this; -}; - -/** - * Gets the ids of all features in the store. - * @return {Store} this - */ -Store.prototype.getAllIds = function() { - return this._featureIds.values(); -}; - -/** - * Adds a feature to the store. - * @param {Object} feature - * @param {Object} [options] - * @param {Object} [options.silent] - If `true`, this invocation will not fire an event. - * - * @return {Store} this - */ -Store.prototype.add = function(feature, options = {}) { - this._features[feature.id] = feature; - this._featureIds.add(feature.id); - this.featureCreated(feature.id, {silent: options.silent}); - return this; -}; - -/** - * Deletes a feature or array of features from the store. - * Cleans up after the deletion by deselecting the features. - * If changes were made, sets the state to the dirty - * and fires an event. - * @param {string | Array} featureIds - * @param {Object} [options] - * @param {Object} [options.silent] - If `true`, this invocation will not fire an event. - * @return {Store} this - */ -Store.prototype.delete = function(featureIds, options = {}) { - const deletedFeaturesToEmit = []; - toDenseArray(featureIds).forEach((id) => { - if (!this._featureIds.has(id)) return; - this._featureIds.delete(id); - this._selectedFeatureIds.delete(id); - if (!options.silent) { - if (deletedFeaturesToEmit.indexOf(this._features[id]) === -1) { - deletedFeaturesToEmit.push(this._features[id].toGeoJSON()); - } - } - delete this._features[id]; - this.isDirty = true; - }); - - if (deletedFeaturesToEmit.length) { - this.ctx.events.fire(Constants.events.DELETE, {features: deletedFeaturesToEmit}); - } - - refreshSelectedCoordinates(this, options); - return this; -}; - -/** - * Returns a feature in the store matching the specified value. - * @return {Object | undefined} feature - */ -Store.prototype.get = function(id) { - return this._features[id]; -}; - -/** - * Returns all features in the store. - * @return {Array} - */ -Store.prototype.getAll = function() { - return Object.keys(this._features).map(id => this._features[id]); -}; - -/** - * Adds features to the current selection. - * @param {string | Array} featureIds - * @param {Object} [options] - * @param {Object} [options.silent] - If `true`, this invocation will not fire an event. - * @return {Store} this - */ -Store.prototype.select = function(featureIds, options = {}) { - toDenseArray(featureIds).forEach((id) => { - if (this._selectedFeatureIds.has(id)) return; - this._selectedFeatureIds.add(id); - this._changedFeatureIds.add(id); - if (!options.silent) { - this._emitSelectionChange = true; - } - }); - return this; -}; - -/** - * Deletes features from the current selection. - * @param {string | Array} featureIds - * @param {Object} [options] - * @param {Object} [options.silent] - If `true`, this invocation will not fire an event. - * @return {Store} this - */ -Store.prototype.deselect = function(featureIds, options = {}) { - toDenseArray(featureIds).forEach((id) => { - if (!this._selectedFeatureIds.has(id)) return; - this._selectedFeatureIds.delete(id); - this._changedFeatureIds.add(id); - if (!options.silent) { - this._emitSelectionChange = true; - } - }); - refreshSelectedCoordinates(this, options); - return this; -}; - -/** - * Clears the current selection. - * @param {Object} [options] - * @param {Object} [options.silent] - If `true`, this invocation will not fire an event. - * @return {Store} this - */ -Store.prototype.clearSelected = function(options = {}) { - this.deselect(this._selectedFeatureIds.values(), { silent: options.silent }); - return this; -}; - -/** - * Sets the store's selection, clearing any prior values. - * If no feature ids are passed, the store is just cleared. - * @param {string | Array | undefined} featureIds - * @param {Object} [options] - * @param {Object} [options.silent] - If `true`, this invocation will not fire an event. - * @return {Store} this - */ -Store.prototype.setSelected = function(featureIds, options = {}) { - featureIds = toDenseArray(featureIds); - - // Deselect any features not in the new selection - this.deselect(this._selectedFeatureIds.values().filter(id => featureIds.indexOf(id) === -1), { silent: options.silent }); - - // Select any features in the new selection that were not already selected - this.select(featureIds.filter(id => !this._selectedFeatureIds.has(id)), { silent: options.silent }); - - return this; -}; - -/** - * Sets the store's coordinates selection, clearing any prior values. - * @param {Array>} coordinates - * @return {Store} this - */ -Store.prototype.setSelectedCoordinates = function(coordinates) { - this._selectedCoordinates = coordinates; - this._emitSelectionChange = true; - return this; -}; - -/** - * Clears the current coordinates selection. - * @param {Object} [options] - * @return {Store} this - */ -Store.prototype.clearSelectedCoordinates = function() { - this._selectedCoordinates = []; - this._emitSelectionChange = true; - return this; -}; - -/** - * Returns the ids of features in the current selection. - * @return {Array} Selected feature ids. - */ -Store.prototype.getSelectedIds = function() { - return this._selectedFeatureIds.values(); -}; - -/** - * Returns features in the current selection. - * @return {Array} Selected features. - */ -Store.prototype.getSelected = function() { - return this.getSelectedIds().map(id => this.get(id)); -}; - -/** - * Returns selected coordinates in the currently selected feature. - * @return {Array} Selected coordinates. - */ -Store.prototype.getSelectedCoordinates = function() { - const selected = this._selectedCoordinates.map((coordinate) => { - const feature = this.get(coordinate.feature_id); - return { - coordinates: feature.getCoordinate(coordinate.coord_path) - }; - }); - return selected; -}; - -/** - * Indicates whether a feature is selected. - * @param {string} featureId - * @return {boolean} `true` if the feature is selected, `false` if not. - */ -Store.prototype.isSelected = function(featureId) { - return this._selectedFeatureIds.has(featureId); -}; - -/** - * Sets a property on the given feature - * @param {string} featureId - * @param {string} property property - * @param {string} property value - * @param {Object} [options] - * @param {Object} [options.silent] - If `true`, this invocation will not fire an event. -*/ -Store.prototype.setFeatureProperty = function(featureId, property, value, options = {}) { - this.get(featureId).setProperty(property, value); - - this.featureChanged(featureId, { - silent: options.silent, - action: Constants.updateActions.CHANGE_PROPERTIES - }); -}; - -function refreshSelectedCoordinates(store, options = {}) { - const newSelectedCoordinates = store._selectedCoordinates.filter(point => store._selectedFeatureIds.has(point.feature_id)); - if (store._selectedCoordinates.length !== newSelectedCoordinates.length && !options.silent) { - store._emitSelectionChange = true; - } - store._selectedCoordinates = newSelectedCoordinates; -} - -/** - * Stores the initial config for a map, so that we can set it again after we're done. -*/ -Store.prototype.storeMapConfig = function() { - Constants.interactions.forEach((interaction) => { - const interactionSet = this.ctx.map[interaction]; - if (interactionSet) { - this._mapInitialConfig[interaction] = this.ctx.map[interaction].isEnabled(); - } - }); -}; - -/** - * Restores the initial config for a map, ensuring all is well. -*/ -Store.prototype.restoreMapConfig = function() { - Object.keys(this._mapInitialConfig).forEach((key) => { - const value = this._mapInitialConfig[key]; - if (value) { - this.ctx.map[key].enable(); - } else { - this.ctx.map[key].disable(); - } - }); -}; - -/** - * Returns the initial state of an interaction setting. - * @param {string} interaction - * @return {boolean} `true` if the interaction is enabled, `false` if not. - * Defaults to `true`. (Todo: include defaults.) -*/ -Store.prototype.getInitialConfigValue = function(interaction) { - if (this._mapInitialConfig[interaction] !== undefined) { - return this._mapInitialConfig[interaction]; - } else { - // This needs to be set to whatever the default is for that interaction - // It seems to be true for all cases currently, so let's send back `true`. - return true; - } -}; diff --git a/src/store.ts b/src/store.ts new file mode 100644 index 00000000..686f8378 --- /dev/null +++ b/src/store.ts @@ -0,0 +1,256 @@ +import { toDenseArray } from './lib/to_dense_array'; +import StringSet from './lib/string_set'; +import render from './render'; +import * as Constants from './constants'; +import type { CTX, DrawFeature, DrawCoords, DrawStoreOptions } from './types/types'; + +export default class Store { + private _features: Record = {}; + private _featureIds = new StringSet(); + private _selectedFeatureIds = new StringSet(); + private _selectedCoordinates: DrawCoords = []; + private _changedFeatureIds = new StringSet(); + private _emitSelectionChange = false; + private _mapInitialConfig: Record = {}; + + sources = { hot: [], cold: [] }; + isDirty = false; + ctx: CTX; + renderRequest: number | null = null; + + constructor(ctx: CTX) { + this.ctx = ctx; + } + + render = (): void => { + if (!this.renderRequest) { + this.renderRequest = requestAnimationFrame(() => { + this.renderRequest = null; + render.call(this); + + if (this._emitSelectionChange) { + this.ctx.events.fire(Constants.events.SELECTION_CHANGE, { + features: this.getSelected().map(feature => { + return feature.toGeoJSON() + }), + points: this.getSelectedCoordinates().map(coordinate => ({ + type: Constants.geojsonTypes.FEATURE, + properties: {}, + geometry: { + type: Constants.geojsonTypes.POINT, + coordinates: coordinate.coordinates, + }, + })), + }); + this._emitSelectionChange = false; + } + + this.ctx.events.fire(Constants.events.RENDER, {}); + }); + } + }; + + createRenderBatch(): () => void { + const holdRender = this.render; + let numRenders = 0; + this.render = () => numRenders++; + + return () => { + this.render = holdRender; + if (numRenders > 0) this.render(); + }; + } + + setDirty(): this { + this.isDirty = true; + return this; + } + + featureCreated(featureId, options: DrawStoreOptions = {}): this { + this._changedFeatureIds.add(featureId); + if (!options.silent) { + const feature = this.get(featureId); + this.ctx.events.fire(Constants.events.CREATE, { features: [feature.toGeoJSON()] }); + } + return this; + } + + featureChanged(featureId: string, options: DrawStoreOptions = {}): this { + this._changedFeatureIds.add(featureId); + if (!options.silent) { + this.ctx.events.fire(Constants.events.UPDATE, { + action: options.action ?? Constants.updateActions.CHANGE_COORDINATES, + features: [this.get(featureId).toGeoJSON()], + }); + } + return this; + } + + getChangedIds() { + return this._changedFeatureIds.values(); + } + + clearChangedIds(): this { + this._changedFeatureIds.clear(); + return this; + } + + getAllIds() { + return this._featureIds.values(); + } + + add(feature: DrawFeature, options: DrawStoreOptions = {}): this { + this._features[feature.id] = feature; + this._featureIds.add(feature.id); + this.featureCreated(feature.id, { silent: options.silent }); + return this; + } + + delete(featureIds: string | string[], options: DrawStoreOptions = {}): this { + const deletedFeatures = []; + toDenseArray(featureIds).forEach(id => { + if (!this._featureIds.has(id)) return; + this._featureIds.delete(id); + this._selectedFeatureIds.delete(id); + if (!options.silent) { + deletedFeatures.push(this._features[id].toGeoJSON()); + } + delete this._features[id]; + this.isDirty = true; + }); + + if (deletedFeatures.length) { + this.ctx.events.fire(Constants.events.DELETE, { features: deletedFeatures }); + } + + this.refreshSelectedCoordinates(options); + return this; + } + + get(id: string) { + return this._features[id]; + } + + getAll() { + return Object.values(this._features); + } + + getSelectedIds() { + return this._selectedFeatureIds.values(); + } + + getSelected() { + return this.getSelectedIds().map(id => this.get(id as string)); + } + + getSelectedCoordinates() { + const coordinates = this._selectedCoordinates.map(coordinate => { + const feature = this.get(coordinate.feature_id); + return { coordinates: feature.getCoordinate(coordinate.coord_path) }; + }); + + return coordinates; + } + + select(featureIds: string[], options: DrawStoreOptions = {}): this { + toDenseArray(featureIds).forEach(id => { + if (this._selectedFeatureIds.has(id)) return; + this._selectedFeatureIds.add(id); + this._changedFeatureIds.add(id); + if (!options.silent) this._emitSelectionChange = true; + }); + return this; + } + + deselect(featureIds: (string | number)[], options: DrawStoreOptions = {}): this { + toDenseArray(featureIds).forEach(id => { + if (!this._selectedFeatureIds.has(id)) return; + this._selectedFeatureIds.delete(id); + this._changedFeatureIds.add(id); + if (!options.silent) this._emitSelectionChange = true; + }); + this.refreshSelectedCoordinates(options); + return this; + } + + setSelected(featureIds: string[], options: DrawStoreOptions = {}): this { + featureIds = toDenseArray(featureIds) as string[]; + + // Deselect any features not in the new selection + this.deselect(this._selectedFeatureIds.values().filter(id => featureIds.indexOf(id as string) === -1), { silent: options.silent }); + + // Select any features in the new selection that were not already selected + this.select(featureIds.filter(id => !this._selectedFeatureIds.has(id)), { silent: options.silent }); + + return this; + }; + + setSelectedCoordinates(coordinates) { + this._selectedCoordinates = coordinates; + this._emitSelectionChange = true; + return this; + }; + + clearSelectedCoordinates() { + this._selectedCoordinates = []; + this._emitSelectionChange = true; + return this; + }; + + isSelected(featureId: string): boolean { + return this._selectedFeatureIds.has(featureId); + }; + + clearSelected(options: DrawStoreOptions = {}): this { + this.deselect(this._selectedFeatureIds.values(), { silent: options.silent }); + return this; + }; + + storeMapConfig() { + Constants.interactions.forEach((interaction) => { + const interactionSet = this.ctx.map[interaction]; + if (interactionSet) { + this._mapInitialConfig[interaction] = this.ctx.map[interaction].isEnabled(); + } + }); + } + + restoreMapConfig() { + Object.keys(this._mapInitialConfig).forEach((key) => { + const value = this._mapInitialConfig[key]; + if (value) { + this.ctx.map[key].enable(); + } else { + this.ctx.map[key].disable(); + } + }); + } + + getInitialConfigValue(interaction: string) { + if (this._mapInitialConfig[interaction] !== undefined) { + return this._mapInitialConfig[interaction]; + } else { + // This needs to be set to whatever the default is for that interaction + // It seems to be true for all cases currently, so let's send back `true`. + return true; + } + }; + + setFeatureProperty(featureId, property, value, options: DrawStoreOptions = {}) { + this.get(featureId).setProperty(property, value); + + this.featureChanged(featureId, { + silent: options.silent, + action: Constants.updateActions.CHANGE_PROPERTIES + }); + }; + + private refreshSelectedCoordinates(options: DrawStoreOptions = {}): void { + const newSelectedCoordinates = this._selectedCoordinates.filter(point => this._selectedFeatureIds.has(point.feature_id)); + if (this._selectedCoordinates.length !== newSelectedCoordinates.length && !options.silent) { + this._emitSelectionChange = true; + } + this._selectedCoordinates = newSelectedCoordinates; + } +} + diff --git a/src/types/fast-deep-equal.d.ts b/src/types/fast-deep-equal.d.ts new file mode 100644 index 00000000..f2ed3da5 --- /dev/null +++ b/src/types/fast-deep-equal.d.ts @@ -0,0 +1 @@ +declare module 'fast-deep-equal'; diff --git a/src/types/geojson-area.d.ts b/src/types/geojson-area.d.ts new file mode 100644 index 00000000..2dbcb518 --- /dev/null +++ b/src/types/geojson-area.d.ts @@ -0,0 +1 @@ +declare module '@mapbox/geojson-area'; diff --git a/src/types/geojson-normalize.d.ts b/src/types/geojson-normalize.d.ts new file mode 100644 index 00000000..d8f01ac9 --- /dev/null +++ b/src/types/geojson-normalize.d.ts @@ -0,0 +1 @@ +declare module '@mapbox/geojson-normalize'; diff --git a/src/types/point-geometry.d.ts b/src/types/point-geometry.d.ts new file mode 100644 index 00000000..1dba7786 --- /dev/null +++ b/src/types/point-geometry.d.ts @@ -0,0 +1 @@ +declare module '@mapbox/point-geometry'; diff --git a/src/types/types.ts b/src/types/types.ts new file mode 100644 index 00000000..bc4ec968 --- /dev/null +++ b/src/types/types.ts @@ -0,0 +1,745 @@ +import { + classes, + sources, + cursors, + types, + geojsonTypes, + modes, + events, + updateActions, + meta, + activeStates, + interactions +} from '../constants'; + +import type { + BBox, + Feature, + FeatureCollection, + GeoJSON, + GeoJsonTypes, + Geometry, + Point, + Position +} from 'geojson'; + +import type { + CircleLayerSpecification, + ControlPosition, + FillLayerSpecification, + IControl, + LineLayerSpecification, + Layer, + Map, + MapEvent, + MapMouseEvent as MapboxMapMouseEvent, + MapTouchEvent as MapboxMapTouchEvent +} from 'mapbox-gl'; + +export interface DrawStyleLayer + extends Omit { + id: string; + type: string; +} + +// Extend Feature to require geometry and properties +export interface StrictFeature + extends Omit { + geometry: Geometry & { coordinates: unknown }; + properties: Record; +} + +// Extend FeatureCollection to use StrictFeature +export interface StrictFeatureCollection + extends Omit { + features: StrictFeature[]; +} + +export type XY = { x: number; y: number }; + +export type Coords = Array<[number, number]>; + +export type DrawCoords = Array<{ coord_path: string; feature_id: string }> + +export interface Entry { + point?: XY; + time?: number; +} + +interface Modes { + draw_line_string: DrawCustomMode; + draw_polygon: DrawCustomMode; + draw_point: DrawCustomMode; + simple_select: DrawCustomMode; + direct_select: DrawCustomMode; +} + +interface DrawPoint extends DrawFeatureBase { + readonly type: 'Point'; + getCoordinate(): Position; + updateCoordinate(lng: number, lat: number): void; + updateCoordinate(path: string, lng: number, lat: number): void; +} + +interface DrawLineString extends DrawFeatureBase { + readonly type: 'LineString'; + addCoordinate(path: string | number, lng: number, lat: number): void; + removeCoordinate(path: string | number): void; +} + +interface DrawPolygon extends DrawFeatureBase { + readonly type: 'Polygon'; + addCoordinate(path: string, lng: number, lat: number): void; + removeCoordinate(path: string): void; +} + +interface DrawFeatureBase { + readonly coordinates: Coordinates; + readonly id: NonNullable; + readonly type: GeoJsonTypes; + + changed(): void; + isValid(): boolean; + incomingCoords(coords: number[][] | number[]): void; + setCoordinates(coords: Coordinates): void; + getCoordinates(): Coordinates; + getCoordinate(path: number): Position; + updateCoordinate(path: number, lng: number, lat: number): void; + setProperty(property: string, value: any): void; + properties: Readonly; + toGeoJSON(): GeoJSON; +} + +export interface DrawMapClasses { + mode?: Constants['modes']; + feature?: Constants['meta']; + mouse?: Constants['cursors']; +} + +interface DrawUI { + queueMapClasses: (options: DrawMapClasses) => void; + setActiveButton: (id: string) => void; + updateMapClasses: () => void; + clearMapClasses: () => void; + addButtons: () => void; + removeButtons: () => void; +} + +type DrawMode = DrawModes[keyof DrawModes]; + +interface DrawEvents { + 'draw.create': DrawCreateEvent; + 'draw.delete': DrawDeleteEvent; + 'draw.update': DrawUpdateEvent; + 'draw.selectionchange': DrawSelectionChangeEvent; + 'draw.render': DrawRenderEvent; + 'draw.combine': DrawCombineEvent; + 'draw.uncombine': DrawUncombineEvent; + 'draw.modechange': DrawModeChangeEvent; + 'draw.actionable': DrawActionableEvent; +} + +type DrawEventType = keyof DrawEvents; + +type DrawModes = (typeof modes)[keyof typeof modes]; + +interface DrawControls { + point?: boolean | undefined; + line_string?: boolean | undefined; + polygon?: boolean | undefined; + trash?: boolean | undefined; + combine_features?: boolean | undefined; + uncombine_features?: boolean | undefined; +} + +interface DrawActionableState { + trash?: boolean; + combineFeatures?: boolean; + uncombineFeatures?: boolean; +} + +interface DrawFeatureBase { + readonly coordinates: Coordinates; + readonly id: NonNullable; + readonly type: GeoJsonTypes; + + changed(): void; + isValid(): boolean; + setCoordinates(coords: Coordinates): void; + getCoordinates(): Coordinates; + getCoordinate(path: string): Position; + updateCoordinate(path: string, lng: number, lat: number): void; + setProperty(property: string, value: any): void; + properties: Readonly; + toGeoJSON(): GeoJSON; +} + +interface DrawMultiFeature< + Type extends 'MultiPoint' | 'MultiLineString' | 'MultiPolygon' +> extends Omit< + DrawFeatureBase< + | (Type extends 'MultiPoint' ? Array : never) + | (Type extends 'MultiLineString' + ? Array + : never) + | (Type extends 'MultiPolygon' + ? Array + : never) + >, + 'coordinates' + > { + readonly type: Type; + readonly features: Array< + | (Type extends 'MultiPoint' ? DrawPoint : never) + | (Type extends 'MultiLineString' ? DrawLineString : never) + | (Type extends 'MultiPolygon' ? DrawPolygon : never) + >; + getFeatures(): this['features']; +} + +interface DrawPoint extends DrawFeatureBase { + readonly type: 'Point'; + getCoordinate(): Position; + updateCoordinate(lng: number, lat: number): void; + updateCoordinate(path: string, lng: number, lat: number): void; +} + +interface DrawLineString extends DrawFeatureBase { + readonly type: 'LineString'; + addCoordinate(path: string | number, lng: number, lat: number): void; + removeCoordinate(path: string | number): void; +} + +interface DrawPolygon extends DrawFeatureBase { + readonly type: 'Polygon'; + addCoordinate(path: string, lng: number, lat: number): void; + removeCoordinate(path: string): void; +} + +export interface DirectSelectState { + featureId: string; + canDragMove: boolean; + dragMoveLocation: { lng: number; lat: number; } | undefined + dragMoving: boolean; + feature: DrawPolygon; + initialDragPanState?: boolean; + selectedCoordPaths: string[]; +} + +export type DrawFeature = + | DrawPoint + | DrawLineString + | DrawPolygon + | DrawMultiFeature<'MultiPoint'> + | DrawMultiFeature<'MultiLineString'> + | DrawMultiFeature<'MultiPolygon'>; + +export interface MapMouseEvent extends MapboxMapMouseEvent { + featureTarget: DrawFeature; +} + +export interface MapTouchEvent extends MapboxMapTouchEvent { + featureTarget: DrawFeature; +} + +interface DrawEvent { + target: Map; + type: DrawEventType; +} + +interface DrawCreateEvent extends DrawEvent { + // Array of GeoJSON objects representing the features that were created + features: Feature[]; + type: 'draw.create'; +} + +interface DrawDeleteEvent extends DrawEvent { + // Array of GeoJSON objects representing the features that were delete, featureId?: stringd + features: Feature[]; + type: 'draw.delete'; +} + +interface DrawCombineEvent extends DrawEvent { + deletedFeatures: Feature[]; // Array of deleted features (those incorporated into new multifeatures) + createdFeatures: Feature[]; // Array of created multifeatures + type: 'draw.combine'; +} + +interface DrawUncombineEvent extends DrawEvent { + deletedFeatures: Feature[]; // Array of deleted multifeatures (split into features) + createdFeatures: Feature[]; // Array of created features + type: 'draw.uncombine'; +} + +interface DrawUpdateEvent extends DrawEvent { + features: Feature[]; // Array of features that were updated + action: string; // Name of the action that triggered the update + type: 'draw.update'; +} + +interface DrawSelectionChangeEvent extends DrawEvent { + features: Feature[]; // Array of features that are selected after the change + points: Array>; + type: 'draw.selectionchange'; +} + +interface DrawModeChangeEvent extends DrawEvent { + mode: DrawMode; // The next mode, i.e. the mode that Draw is changing to + type: 'draw.modechange'; +} + +interface DrawRenderEvent extends DrawEvent { + type: 'draw.render'; +} + +interface DrawActionableEvent extends DrawEvent { + actions: DrawActionableState; + type: 'draw.actionable'; +} + +interface DrawCustomModeThis { + map: Map; + _ctx: CTX; + drawConfig: DrawOptions; + setSelected(features?: DrawFeature[]): void; + setSelectedCoordinates( + coords: DrawCoords + ): void; + getSelected(): DrawFeature[]; + getSelectedIds(): string[]; + isSelected(id: string): boolean; + getFeature(id: string): DrawFeature; + select(id: string): void; + delete(id: string): void; + deleteFeature(id: string, opts?: any): void; + addFeature(feature: DrawFeature): void; + clearSelectedFeatures(): void; + clearSelectedCoordinates(): void; + setActionableState(actionableState: DrawActionableState): void; + changeMode(mode: DrawMode, opts?: object, eventOpts?: object): void; + updateUIClasses(opts: object): void; + activateUIButton(name?: string): void; + featuresAt( + event: Event, + bbox: BBox, + bufferType: 'click' | 'tap' + ): DrawFeature[]; + newFeature(geojson: GeoJSON): DrawFeature; + isInstanceOf(type: string, feature: object): boolean; + doRender(id: string): void; +} + +export interface DrawCustomMode { + onSetup?(this: DrawCustomModeThis & this, options: CustomModeOptions): CustomModeState; + onDrag?(this: DrawCustomModeThis & this, state: CustomModeState, e: MapMouseEvent): void; + onClick?( + this: DrawCustomModeThis & this, + state: CustomModeState, + e: MapMouseEvent + ): void; + onMouseMove?( + this: DrawCustomModeThis & this, + state: CustomModeState, + e: MapMouseEvent + ): void; + onMouseDown?( + this: DrawCustomModeThis & this, + state: CustomModeState, + e: MapMouseEvent + ): void; + onMouseUp?( + this: DrawCustomModeThis & this, + state: CustomModeState, + e: MapMouseEvent + ): void; + onMouseOut?( + this: DrawCustomModeThis & this, + state: CustomModeState, + e: MapMouseEvent + ): void; + onKeyUp?( + this: DrawCustomModeThis & this, + state: CustomModeState, + e: KeyboardEvent + ): void; + onKeyDown?( + this: DrawCustomModeThis & this, + state: CustomModeState, + e: KeyboardEvent + ): void; + onTouchStart?( + this: DrawCustomModeThis & this, + state: CustomModeState, + e: MapTouchEvent + ): void; + onTouchMove?( + this: DrawCustomModeThis & this, + state: CustomModeState, + e: MapTouchEvent + ): void; + onTouchEnd?( + this: DrawCustomModeThis & this, + state: CustomModeState, + e: MapTouchEvent + ): void; + onTap?(this: DrawCustomModeThis & this, state: CustomModeState, e: MapTouchEvent): void; + onStop?(this: DrawCustomModeThis & this, state: CustomModeState): void; + onTrash?(this: DrawCustomModeThis & this, state: CustomModeState): void; + onCombineFeature?(this: DrawCustomModeThis & this, state: CustomModeState): void; + onUncombineFeature?(this: DrawCustomModeThis & this, state: CustomModeState): void; + toDisplayFeatures( + this: DrawCustomModeThis & this, + state: CustomModeState, + geojson: StrictFeature, + display: (geojson: StrictFeature) => void + ): void; +} + +interface Modes { + draw_line_string: DrawCustomMode; + draw_polygon: DrawCustomMode; + draw_point: DrawCustomMode; + simple_select: DrawCustomMode; + direct_select: DrawCustomMode; +} + +// Convert these to use imports from constants +interface Constants { + readonly modes: (typeof modes)[keyof typeof modes]; + readonly classes: (typeof classes)[keyof typeof classes]; + readonly sources: (typeof sources)[keyof typeof sources]; + readonly cursors: (typeof cursors)[keyof typeof cursors]; + readonly types: (typeof types)[keyof typeof types]; + readonly geojsonTypes: (typeof geojsonTypes)[keyof typeof geojsonTypes]; + readonly events: (typeof events)[keyof typeof events]; + readonly updateActions: (typeof updateActions)[keyof typeof updateActions]; + readonly meta: (typeof meta)[keyof typeof meta]; + readonly activeStates: (typeof activeStates)[keyof typeof activeStates]; + readonly interactions: (typeof interactions)[keyof typeof interactions]; + readonly LAT_MIN: -90; + readonly LAT_RENDERED_MIN: -85; + readonly LAT_MAX: 90; + readonly LAT_RENDERED_MAX: 85; + readonly LNG_MIN: -270; + readonly LNG_MAX: 270; +} + +interface StringSet { + add(x: string | number): StringSet; + delete(x: string | number): StringSet; + has(x: string | number): boolean; + values(): string | number[]; + clear(): StringSet; +} + +interface Lib { + CommonSelectors: { + isOfMetaType: ( + type: Constants['meta'][keyof Constants['meta']] + ) => (e: MapMouseEvent | MapTouchEvent) => boolean; + isShiftMousedown: (e: MapEvent) => boolean; + isActiveFeature: (e: MapMouseEvent | MapTouchEvent) => boolean; + isInactiveFeature: (e: MapMouseEvent | MapTouchEvent) => boolean; + noTarget: (e: MapMouseEvent | MapTouchEvent) => boolean; + isFeature: (e: MapMouseEvent | MapTouchEvent) => boolean; + isVertex: (e: MapMouseEvent | MapTouchEvent) => boolean; + isShiftDown: (e: MapEvent) => boolean; + isEscapeKey: (e: KeyboardEvent) => boolean; + isEnterKey: (e: KeyboardEvent) => boolean; + isTrue: () => boolean; + }; + + constrainFeatureMovement( + geojsonFeatures: DrawFeature[], + delta: { lng: number; lat: number } + ): { lng: number; lat: number }; + + createMidPoint( + parent: string, + startVertex: Feature, + endVertex: Feature + ): Feature | null; + + createSupplementaryPoints( + geojson: Feature, + options?: { midpoints?: boolean; selectedPaths?: string[] }, + basePath?: string + ): Array>; + + createVertex( + parentId: string, + coordinates: Position, + path: string, + selected: boolean + ): Feature; + + doubleClickZoom: { + enable: (ctx: DrawCustomModeThis) => void; // ?? ctx + disable: (ctx: DrawCustomModeThis) => void; // ?? ctx + }; + + featuresAt: { + click: (event: MapMouseEvent, bbox: BBox, ctx: DrawCustomModeThis) => Feature[]; // ?? ctx + touch: (event: MapTouchEvent, bbox: BBox, ctx: DrawCustomModeThis) => Feature[]; // ?? ctx + }; + + getFeatureAtAndSetCursors(event: MapMouseEvent, ctx: DrawCustomModeThis): Feature; + + euclideanDistance( + a: { x: number; y: number }, + b: { x: number; y: number } + ): number; + + isClick( + start: { point?: Entry }, + end: { point: Entry }, + options?: { + fineTolerance?: number; + grossTolerance?: number; + interval?: number; + } + ): boolean; + + isEventAtCoordinates(event: MapMouseEvent, coordinates: Position[]): boolean; + + isTap( + start: { point?: Entry }, + end: { point: Entry }, + options?: { tolerance?: number; interval?: number } + ): boolean; + + /** + * Returns a bounding box representing the event's location. + * + * @param mapEvent - Mapbox GL JS map event, with a point properties. + * @param [buffer=0] + * @return Bounding box. + */ + mapEventToBoundingBox( + mapEvent: MapMouseEvent | MapTouchEvent, + buffer?: number + ): Position[]; + + ModeHandler: ( + mode: any, + DrawContext: any + ) => { + render: any; + stop: () => void; + trash: () => void; + combineFeatures: () => void; + uncombineFeatures: () => void; + drag: (event: any) => void; + click: (event: any) => void; + mousemove: (event: any) => void; + mousedown: (event: any) => void; + mouseup: (event: any) => void; + mouseout: (event: any) => void; + keydown: (event: any) => void; + keyup: (event: any) => void; + touchstart: (event: any) => void; + touchmove: (event: any) => void; + touchend: (event: any) => void; + tap: (event: any) => void; + }; + + moveFeatures( + features: DrawFeature[], + delta: { lng: number; lat: number } + ): void; + + /** + * Sort features in the following order Point: 0, LineString: 1, MultiLineString: 1, + * Polygon: 2, then sort polygons by area ascending. + * @param features + */ + sortFeatures(features: DrawFeature[]): DrawFeature[]; + + stringSetsAreEqual( + a: Array>, + b: Array> + ): boolean; + + StringSet(items?: Array): StringSet; + + theme: Array< + ( + | FillLayerSpecification + | LineLayerSpecification + | CircleLayerSpecification + ) & { id: ThemeLayerId } + >; + + /** + * Derive a dense array (no `undefined`s) from a single value or array. + */ + toDenseArray(x: any): Array>; +} + +type ThemeLayerId = + | 'gl-draw-polygon-fill-static' + | 'gl-draw-polygon-fill-active' + | 'gl-draw-polygon-fill-inactive' + | 'gl-draw-polygon-stroke-static' + | 'gl-draw-polygon-stroke-active' + | 'gl-draw-polygon-stroke-inactive' + | 'gl-draw-polygon-midpoint' + | 'gl-draw-polygon-and-line-vertex-inactive' + | 'gl-draw-polygon-and-line-vertex-stroke-inactive' + | 'gl-draw-line-static' + | 'gl-draw-line-active' + | 'gl-draw-line-inactive' + | 'gl-draw-point-static' + | 'gl-draw-point-active' + | 'gl-draw-point-inactive' + | 'gl-draw-point-stroke-active' + | 'gl-draw-point-point-stroke-inactive'; + +export interface DrawOptions { + displayControlsDefault?: boolean | undefined; + keybindings?: boolean | undefined; + touchEnabled?: boolean | undefined; + boxSelect?: boolean | undefined; + clickBuffer?: number | undefined; + touchBuffer?: number | undefined; + controls?: DrawControls | undefined; + styles?: Array | undefined; + modes?: { [modeKey: string]: DrawCustomMode } | undefined; + defaultMode?: string | undefined; + userProperties?: boolean | undefined; + suppressAPIEvents?: boolean | undefined; +} + +interface DrawEvents { + actionable(action: DrawActionableState): void; + addEventListeners(): void; + changeMode(mode: string, modeOptions?: {}, eventOptions?: {}): void; + combineFeatures(): void; + currentModeName(): string; + currentModeRender(geojson: StrictFeature, push: (geojson: StrictFeature) => void): void; + fire(eventName: string, eventData: unknown): DrawMode; + getMode(): DrawMode; + removeEventListeners(): void; + start(): void; + trash(options?: { silent: boolean}): void; + uncombineFeatures(): void; +} + +export interface DrawStoreOptions { + silent?: boolean; + action?: string; +} + +export interface DrawStore { + ctx: CTX; + isDirty: boolean; + setDirty(): this; + render(): void; + sources: { + hot: [], + cold: [] + }, + restoreMapConfig(): Record; + getInitialConfigValue(interaction: string): boolean; + featureChanged(id: string, options?: DrawStoreOptions): boolean; + setSelected(features?: string[], options?: { silent: boolean }): void; + setSelectedCoordinates( + coords: DrawCoords + ): void; + getSelectedCoordinates(): { coordinates: Coords[] }[] + getSelected(): DrawFeature[]; + getSelectedIds(): string[]; + isSelected(id: string): boolean; + get(id: string): DrawFeature; + getFeature(id: string): DrawFeature; + select(id: string): void; + deselect(features: string | string[]): void; + delete(id: string | string[], opts: Record): void; + deleteFeature(id: string, opts?: unknown): void; + add(feature: DrawFeature, opts: Record): void; + addFeature(feature: DrawFeature, opts: Record): void; + clearSelected(): void; + clearSelectedFeatures(): void; + clearSelectedCoordinates(): void; + setActionableState(actionableState: DrawActionableState): void; + changeMode(mode: DrawMode, opts?: object, eventOpts?: object): void; + updateUIClasses(opts: object): void; + activateUIButton(name?: string): void; + featuresAt( + event: Event, + bbox: BBox, + bufferType: 'click' | 'tap' + ): DrawFeature[]; + newFeature(geojson: StrictFeatureCollection): DrawFeature; + isInstanceOf(type: string, feature: object): boolean; + doRender(id: string): void; + getAllIds(): Array + getAll(): DrawFeature[]; + createRenderBatch(): () => void; + setFeatureProperty(featureId: string, property: string, value: any, options?: { silent: boolean }): this; +} + +interface DrawSetup { + addLayers(): void; + removeLayers(): void; + onAdd(map: Map): void; + onRemove(): void; + connect(): void; +} + +export interface CTX { + api: Draw; + boxZoomInitial: boolean; + container: HTMLElement; + events: DrawEvents; + map: Map; + options: DrawOptions; + setup: DrawSetup; + store: DrawStore; + ui: DrawUI; +} + +export declare class Draw implements IControl { + static modes: Modes; + static constants: Constants; + static lib: Lib; + modes: typeof modes; + types: typeof types; + options: DrawOptions; + getDefaultPosition: () => ControlPosition; + constructor(options?: DrawOptions); + add(geojson: Feature | StrictFeatureCollection | Geometry): string[]; + get(featureId: string): StrictFeature | undefined; + getFeatureIdsAt(point: { x: number; y: number }): string[]; + getSelectedIds(): string[]; + getSelected(): StrictFeatureCollection; + getSelectedPoints(): StrictFeatureCollection; + getAll(): StrictFeatureCollection; + delete(ids: string | string[]): this; + deleteAll(): this; + set(featureCollection: StrictFeatureCollection): string[]; + trash(): this; + combineFeatures(): this; + uncombineFeatures(): this; + getMode(): (Modes & {}) | string; + changeMode( + mode: (typeof modes)['SIMPLE_SELECT'], + options?: { featureIds: string[] } + ): this; + changeMode( + mode: (typeof modes)['DIRECT_SELECT'], + options: { featureId: string } + ): this; + changeMode( + mode: (typeof modes)['DRAW_LINE_STRING'], + options?: { featureId: string; from: Feature | Point | number[] } + ): this; + changeMode(mode: Modes): this; + changeMode( + mode: T & (T extends Modes ? never : T), + options?: object + ): this; + setFeatureProperty(featureId: string, property: string, value: any): this; + onAdd(map: Map): HTMLElement; + onRemove(map: Map): unknown; +} diff --git a/src/ui.js b/src/ui.ts similarity index 59% rename from src/ui.js rename to src/ui.ts index 82b287a8..fc3466d8 100644 --- a/src/ui.js +++ b/src/ui.ts @@ -1,11 +1,16 @@ -import * as Constants from './constants.js'; +import * as Constants from './constants'; +import type { CTX } from './types/types'; const classTypes = ['mode', 'feature', 'mouse']; -export default function(ctx) { - +interface ButtonElements { + trash?: HTMLButtonElement; + combine_features?: HTMLButtonElement; + uncombine_features?: HTMLButtonElement; +} - const buttonElements = {}; +export default function (ctx: CTX) { + const buttonElements: ButtonElements = {}; let activeButton = null; let currentMapClasses = { @@ -21,7 +26,7 @@ export default function(ctx) { }; function clearMapClasses() { - queueMapClasses({mode:null, feature:null, mouse:null}); + queueMapClasses({ mode: null, feature: null, mouse: null }); updateMapClasses(); } @@ -35,7 +40,7 @@ export default function(ctx) { const classesToRemove = []; const classesToAdd = []; - classTypes.forEach((type) => { + classTypes.forEach(type => { if (nextMapClasses[type] === currentMapClasses[type]) return; classesToRemove.push(`${type}-${currentMapClasses[type]}`); @@ -55,26 +60,30 @@ export default function(ctx) { currentMapClasses = Object.assign(currentMapClasses, nextMapClasses); } - function createControlButton(id, options = {}) { + function createControlButton(id: string, options) { const button = document.createElement('button'); button.className = `${Constants.classes.CONTROL_BUTTON} ${options.className}`; button.setAttribute('title', options.title); options.container.appendChild(button); - button.addEventListener('click', (e) => { - e.preventDefault(); - e.stopPropagation(); - - const clickedButton = e.target; - if (clickedButton === activeButton) { - deactivateButtons(); - options.onDeactivate(); - return; - } + button.addEventListener( + 'click', + e => { + e.preventDefault(); + e.stopPropagation(); + + const clickedButton = e.target; + if (clickedButton === activeButton) { + deactivateButtons(); + options.onDeactivate(); + return; + } - setActiveButton(id); - options.onActivate(); - }, true); + setActiveButton(id); + options.onActivate(); + }, + true + ); return button; } @@ -85,7 +94,7 @@ export default function(ctx) { activeButton = null; } - function setActiveButton(id) { + function setActiveButton(id: string) { deactivateButtons(); const button = buttonElements[id]; @@ -105,34 +114,43 @@ export default function(ctx) { if (!controls) return controlGroup; if (controls[Constants.types.POINT]) { - buttonElements[Constants.types.POINT] = createControlButton(Constants.types.POINT, { - container: controlGroup, - className: Constants.classes.CONTROL_BUTTON_POINT, - title: `Marker tool ${ctx.options.keybindings ? '(1)' : ''}`, - onActivate: () => ctx.events.changeMode(Constants.modes.DRAW_POINT), - onDeactivate: () => ctx.events.trash() - }); + buttonElements[Constants.types.POINT] = createControlButton( + Constants.types.POINT, + { + container: controlGroup, + className: Constants.classes.CONTROL_BUTTON_POINT, + title: `Marker tool ${ctx.options.keybindings ? '(1)' : ''}`, + onActivate: () => ctx.events.changeMode(Constants.modes.DRAW_POINT), + onDeactivate: () => ctx.events.trash() + } + ); } - if (controls[Constants.types.LINE]) { - buttonElements[Constants.types.LINE] = createControlButton(Constants.types.LINE, { - container: controlGroup, - className: Constants.classes.CONTROL_BUTTON_LINE, - title: `LineString tool ${ctx.options.keybindings ? '(2)' : ''}`, - onActivate: () => ctx.events.changeMode(Constants.modes.DRAW_LINE_STRING), - onDeactivate: () => ctx.events.trash() - }); + buttonElements[Constants.types.LINE] = createControlButton( + Constants.types.LINE, + { + container: controlGroup, + className: Constants.classes.CONTROL_BUTTON_LINE, + title: `LineString tool ${ctx.options.keybindings ? '(2)' : ''}`, + onActivate: () => + ctx.events.changeMode(Constants.modes.DRAW_LINE_STRING), + onDeactivate: () => ctx.events.trash() + } + ); } if (controls[Constants.types.POLYGON]) { - buttonElements[Constants.types.POLYGON] = createControlButton(Constants.types.POLYGON, { - container: controlGroup, - className: Constants.classes.CONTROL_BUTTON_POLYGON, - title: `Polygon tool ${ctx.options.keybindings ? '(3)' : ''}`, - onActivate: () => ctx.events.changeMode(Constants.modes.DRAW_POLYGON), - onDeactivate: () => ctx.events.trash() - }); + buttonElements[Constants.types.POLYGON] = createControlButton( + Constants.types.POLYGON, + { + container: controlGroup, + className: Constants.classes.CONTROL_BUTTON_POLYGON, + title: `Polygon tool ${ctx.options.keybindings ? '(3)' : ''}`, + onActivate: () => ctx.events.changeMode(Constants.modes.DRAW_POLYGON), + onDeactivate: () => ctx.events.trash() + } + ); } if (controls.trash) { @@ -158,21 +176,24 @@ export default function(ctx) { } if (controls.uncombine_features) { - buttonElements.uncombine_features = createControlButton('uncombineFeatures', { - container: controlGroup, - className: Constants.classes.CONTROL_BUTTON_UNCOMBINE_FEATURES, - title: 'Uncombine', - onActivate: () => { - ctx.events.uncombineFeatures(); + buttonElements.uncombine_features = createControlButton( + 'uncombineFeatures', + { + container: controlGroup, + className: Constants.classes.CONTROL_BUTTON_UNCOMBINE_FEATURES, + title: 'Uncombine', + onActivate: () => { + ctx.events.uncombineFeatures(); + } } - }); + ); } return controlGroup; } function removeButtons() { - Object.keys(buttonElements).forEach((buttonId) => { + Object.keys(buttonElements).forEach(buttonId => { const button = buttonElements[buttonId]; if (button.parentNode) { button.parentNode.removeChild(button); diff --git a/test/api.test.js b/test/api.test.ts similarity index 59% rename from test/api.test.js rename to test/api.test.ts index 9956bee2..5efb4cdd 100644 --- a/test/api.test.js +++ b/test/api.test.ts @@ -1,14 +1,15 @@ /* eslint no-shadow:[0] */ -import {test, beforeEach, afterEach} from 'node:test'; +import './mock-browser'; +import { test, beforeEach, afterEach } from 'node:test'; +import { spy } from 'sinon'; import assert from 'node:assert/strict'; -import {spy} from 'sinon'; -import * as Constants from '../src/constants.js'; -import MapboxDraw from '../index.js'; -import createMap from './utils/create_map.js'; -import getGeoJSON from './utils/get_geojson.js'; -import {setupAfterNextRender} from './utils/after_next_render.js'; -import getPublicMemberKeys from './utils/get_public_member_keys.js'; +import * as Constants from '../src/constants'; +import MapboxDraw from '../index'; +import createMap from './utils/create_map'; +import getGeoJSON from './utils/get_geojson'; +import { setupAfterNextRender } from './utils/after_next_render'; +import getPublicMemberKeys from './utils/get_public_member_keys'; let map; let afterNextRender; @@ -33,7 +34,7 @@ afterEach(() => { deleteSpy = null; }); -test('Draw.add', async (t) => { +test('Draw.add', async t => { await t.test('should generate unique ID', () => { const [id] = Draw.add(getGeoJSON('point')); @@ -56,7 +57,11 @@ test('Draw.getFeatureIdsAt', async () => { }); assert.equal(featureIds.length, 1, 'should return the added feature'); - assert.equal(featureIds[0], id, 'selected feature should match desired feature'); + assert.equal( + featureIds[0], + id, + 'selected feature should match desired feature' + ); Draw.deleteAll(); }); @@ -66,18 +71,13 @@ test('Draw.getSelectedIds', () => { const [polygonId] = Draw.add(getGeoJSON('polygon')); Draw.changeMode('simple_select', { featureIds: [lineId, pointId] }); const selected = Draw.getSelectedIds(); - assert.equal(selected.length, 2, - 'returns correct number of ids'); - assert.notEqual(selected.indexOf(lineId), -1, - 'result contains line'); - assert.notEqual(selected.indexOf(pointId), -1, - 'result contains point'); + assert.equal(selected.length, 2, 'returns correct number of ids'); + assert.notEqual(selected.indexOf(lineId), -1, 'result contains line'); + assert.notEqual(selected.indexOf(pointId), -1, 'result contains point'); Draw.changeMode('simple_select', { featureIds: [polygonId] }); const nextSelected = Draw.getSelectedIds(); - assert.equal(nextSelected.length, 1, - 'updates length'); - assert.equal(nextSelected[0], polygonId, - 'updates content'); + assert.equal(nextSelected.length, 1, 'updates length'); + assert.equal(nextSelected[0], polygonId, 'updates content'); }); test('Draw.getSelected', () => { @@ -90,18 +90,13 @@ test('Draw.getSelected', () => { assert.equal(typeof fc.features, 'object', 'we have a feature collection'); const selected = fc.features.map(f => f.id); - assert.equal(selected.length, 2, - 'returns correct number of ids'); - assert.notEqual(selected.indexOf(lineId), -1, - 'result contains line'); - assert.notEqual(selected.indexOf(pointId), -1, - 'result contains point'); + assert.equal(selected.length, 2, 'returns correct number of ids'); + assert.notEqual(selected.indexOf(lineId), -1, 'result contains line'); + assert.notEqual(selected.indexOf(pointId), -1, 'result contains point'); Draw.changeMode('simple_select', { featureIds: [polygonId] }); const nextSelected = Draw.getSelected().features.map(f => f.id); - assert.equal(nextSelected.length, 1, - 'updates length'); - assert.equal(nextSelected[0], polygonId, - 'updates content'); + assert.equal(nextSelected.length, 1, 'updates length'); + assert.equal(nextSelected[0], polygonId, 'updates content'); }); test('Draw.set', () => { @@ -115,19 +110,22 @@ test('Draw.set', () => { features: [point, line, polygon] }; const drawInstance = Draw.set(collection); - assert.equal(drawInstance.length, 3, - 'return value is correct length'); + assert.equal(drawInstance.length, 3, 'return value is correct length'); const pointId = drawInstance[0]; const lineId = drawInstance[1]; const polygonId = drawInstance[2]; - assert.equal(Draw.get(pointId).geometry.type, 'Point', - 'point id returned'); - assert.equal(Draw.get(lineId).geometry.type, 'LineString', - 'line id returned'); - assert.equal(Draw.get(polygonId).geometry.type, 'Polygon', - 'polygon id returned'); - assert.equal(Draw.getAll().features.length, 3, - 'all features loaded'); + assert.equal(Draw.get(pointId).geometry.type, 'Point', 'point id returned'); + assert.equal( + Draw.get(lineId).geometry.type, + 'LineString', + 'line id returned' + ); + assert.equal( + Draw.get(polygonId).geometry.type, + 'Polygon', + 'polygon id returned' + ); + assert.equal(Draw.getAll().features.length, 3, 'all features loaded'); // Then set to another addSpy.resetHistory(); @@ -137,22 +135,28 @@ test('Draw.set', () => { features: [polygon] }; const nextDrawInstance = Draw.set(nextCollection); - assert.equal(nextDrawInstance.length, 1, - 'return value is correct length'); + assert.equal(nextDrawInstance.length, 1, 'return value is correct length'); const nextPolygonId = nextDrawInstance[0]; - assert.equal(Draw.get(nextPolygonId).geometry.type, 'Polygon', - 'polygon id returned'); - assert.equal(Draw.getAll().features.length, 1, - 'all features replaced with new ones'); - assert.ok(addSpy.calledWith(nextCollection), - 'Draw.add called with new collection'); - assert.equal(deleteSpy.callCount, 1, - 'Draw.delete called'); - assert.deepEqual(deleteSpy.getCall(0).args[0], [ - pointId, - lineId, - polygonId - ], 'Draw.delete called with old features'); + assert.equal( + Draw.get(nextPolygonId).geometry.type, + 'Polygon', + 'polygon id returned' + ); + assert.equal( + Draw.getAll().features.length, + 1, + 'all features replaced with new ones' + ); + assert.ok( + addSpy.calledWith(nextCollection), + 'Draw.add called with new collection' + ); + assert.equal(deleteSpy.callCount, 1, 'Draw.delete called'); + assert.deepEqual( + deleteSpy.getCall(0).args[0], + [pointId, lineId, polygonId], + 'Draw.delete called with old features' + ); // Then set to another that contains a feature // with an already-used id @@ -166,21 +170,33 @@ test('Draw.set', () => { features: [newLine, overlappingPolygon] }; const overlappingDrawInstance = Draw.set(overlappingCollection); - assert.equal(overlappingDrawInstance.length, 2, - 'return value is correct length'); + assert.equal( + overlappingDrawInstance.length, + 2, + 'return value is correct length' + ); const newLineId = overlappingDrawInstance[0]; const overlappingPolygonId = overlappingDrawInstance[1]; - assert.equal(Draw.get(newLineId).geometry.type, 'LineString', - 'new line id returned'); - assert.equal(Draw.get(overlappingPolygonId).geometry.type, 'Polygon', - 'overlapping polygon id returned'); - assert.equal(overlappingPolygonId, nextPolygonId, - 'overlapping polygon id did not change'); - assert.ok(addSpy.calledWith(overlappingCollection), - 'Draw.add called with overlapping collection'); - assert.equal(deleteSpy.callCount, 0, - 'Draw.delete not called'); - + assert.equal( + Draw.get(newLineId).geometry.type, + 'LineString', + 'new line id returned' + ); + assert.equal( + Draw.get(overlappingPolygonId).geometry.type, + 'Polygon', + 'overlapping polygon id returned' + ); + assert.equal( + overlappingPolygonId, + nextPolygonId, + 'overlapping polygon id did not change' + ); + assert.ok( + addSpy.calledWith(overlappingCollection), + 'Draw.add called with overlapping collection' + ); + assert.equal(deleteSpy.callCount, 0, 'Draw.delete not called'); }); test('Draw.set errors', () => { @@ -202,8 +218,11 @@ test('Draw.add -- point', () => { test('Draw.add -- FeatureCollection', () => { const listOfIds = Draw.add(getGeoJSON('featureCollection')); - assert.equal(listOfIds.length, getGeoJSON('featureCollection').features.length, - 'valid string id returned when adding a featureCollection'); + assert.equal( + listOfIds.length, + getGeoJSON('featureCollection').features.length, + 'valid string id returned when adding a featureCollection' + ); Draw.deleteAll(); }); @@ -255,7 +274,7 @@ test('Draw.add -- existing feature with changed properties', async () => { await afterNextRender(); - point.properties = {'testing': 123}; + point.properties = { testing: 123 }; Draw.add(point); point = Draw.get(id); assert.equal('testing', Object.keys(point.properties)[0]); @@ -272,8 +291,11 @@ test('Draw.get', () => { 'the geometry added is the same returned by Draw.get' ); - assert.equal(Draw.get('foo'), undefined, - 'returned undefined when no feature found'); + assert.equal( + Draw.get('foo'), + undefined, + 'returned undefined when no feature found' + ); Draw.deleteAll(); }); @@ -292,7 +314,11 @@ test('Draw.delete one feature', () => { const id = Draw.add(getGeoJSON('point'))[0]; const drawInstance = Draw.delete(id); assert.equal(drawInstance, Draw, 'returns Draw instance'); - assert.equal(Draw.getAll().features.length, 0, 'can remove a feature by its id'); + assert.equal( + Draw.getAll().features.length, + 0, + 'can remove a feature by its id' + ); }); test('Draw.delete multiple features', () => { @@ -301,9 +327,16 @@ test('Draw.delete multiple features', () => { Draw.add(getGeoJSON('polygon')); const drawInstance = Draw.delete([pointId, lineId]); assert.equal(drawInstance, Draw, 'returns Draw instance'); - assert.equal(Draw.getAll().features.length, 1, 'can remove multiple features by id'); - assert.equal(Draw.getAll().features[0].geometry.type, 'Polygon', - 'the right features were removed'); + assert.equal( + Draw.getAll().features.length, + 1, + 'can remove multiple features by id' + ); + assert.equal( + Draw.getAll().features[0].geometry.type, + 'Polygon', + 'the right features were removed' + ); Draw.deleteAll(); }); @@ -312,14 +345,22 @@ test('Draw.delete a feature that is direct_selected', () => { Draw.changeMode('direct_select', { featureId: id }); Draw.delete([id]); assert.equal(Draw.getAll().features.length, 0, 'removed the feature'); - assert.equal(Draw.getMode(), 'simple_select', 'changed modes to simple_select'); + assert.equal( + Draw.getMode(), + 'simple_select', + 'changed modes to simple_select' + ); }); test('Draw.deleteAll', () => { Draw.add(getGeoJSON('point')); const drawInstance = Draw.deleteAll(); assert.equal(drawInstance, Draw, 'returns Draw instance'); - assert.equal(Draw.getAll().features.length, 0, 'Draw.deleteAll removes all geometries'); + assert.equal( + Draw.getAll().features.length, + 0, + 'Draw.deleteAll removes all geometries' + ); }); test('Draw.deleteAll when in direct_select mode', async () => { @@ -330,10 +371,16 @@ test('Draw.deleteAll when in direct_select mode', async () => { await afterNextRender(); - assert.equal(Draw.getMode(), 'simple_select', - 'switches to simple_select mode'); - assert.equal(Draw.getAll().features.length, 0, - 'removes selected feature along with others'); + assert.equal( + Draw.getMode(), + 'simple_select', + 'switches to simple_select mode' + ); + assert.equal( + Draw.getAll().features.length, + 0, + 'removes selected feature along with others' + ); }); test('Draw.changeMode and Draw.getMode with no pre-existing feature', () => { @@ -341,20 +388,57 @@ test('Draw.changeMode and Draw.getMode with no pre-existing feature', () => { assert.equal(drawInstance, Draw, 'changeMode returns Draw instance'); assert.equal(Draw.getMode(), 'draw_polygon', 'changed to draw_polygon'); assert.equal(Draw.getAll().features.length, 1, 'one feature added'); - assert.equal(Draw.getAll().features[0].geometry.type, 'Polygon', 'and it is a polygon'); - assert.deepEqual(Draw.getAll().features[0].geometry.coordinates, [[null]], 'and it is empty'); + assert.equal( + Draw.getAll().features[0].geometry.type, + 'Polygon', + 'and it is a polygon' + ); + + assert.deepEqual( + Draw.getAll().features[0].geometry.coordinates, + [[undefined]], + 'and it is empty' + ); Draw.changeMode('draw_line_string'); - assert.equal(Draw.getMode(), 'draw_line_string', 'changed to draw_line_string'); - assert.equal(Draw.getAll().features.length, 1, 'still only one feature added'); - assert.equal(Draw.getAll().features[0].geometry.type, 'LineString', 'and it is a line'); - assert.deepEqual(Draw.getAll().features[0].geometry.coordinates, [], 'and it is empty'); + assert.equal( + Draw.getMode(), + 'draw_line_string', + 'changed to draw_line_string' + ); + assert.equal( + Draw.getAll().features.length, + 1, + 'still only one feature added' + ); + assert.equal( + Draw.getAll().features[0].geometry.type, + 'LineString', + 'and it is a line' + ); + assert.deepEqual( + Draw.getAll().features[0].geometry.coordinates, + [], + 'and it is empty' + ); Draw.changeMode('draw_point'); assert.equal(Draw.getMode(), 'draw_point', 'changed to draw_point'); - assert.equal(Draw.getAll().features.length, 1, 'still only one feature added'); - assert.equal(Draw.getAll().features[0].geometry.type, 'Point', 'and it is a point'); - assert.deepEqual(Draw.getAll().features[0].geometry.coordinates, [], 'and it is empty'); + assert.equal( + Draw.getAll().features.length, + 1, + 'still only one feature added' + ); + assert.equal( + Draw.getAll().features[0].geometry.type, + 'Point', + 'and it is a point' + ); + assert.deepEqual( + Draw.getAll().features[0].geometry.coordinates, + [], + 'and it is empty' + ); Draw.changeMode('simple_select'); assert.equal(Draw.getMode(), 'simple_select', 'changed to simple_select'); @@ -363,7 +447,6 @@ test('Draw.changeMode and Draw.getMode with no pre-existing feature', () => { assert.throws(() => { Draw.changeMode('direct_select'); }, 'cannot enter direct_select mode with a featureId'); - }); test('Draw.changeMode to select and de-select pre-existing features', async () => { @@ -371,16 +454,26 @@ test('Draw.changeMode to select and de-select pre-existing features', async () = const [lineId] = Draw.add(getGeoJSON('line')); const [pointId] = Draw.add(getGeoJSON('point')); - const returnA = Draw.changeMode('simple_select', { featureIds: [polygonId, lineId]}); + const returnA = Draw.changeMode('simple_select', { + featureIds: [polygonId, lineId] + }); assert.equal(returnA, Draw, 'returns Draw instance'); assert.equal(Draw.getMode(), 'simple_select', 'changed to simple_select'); - assert.deepEqual(Draw.getSelectedIds(), [polygonId, lineId], - 'polygon and line are selected'); + assert.deepEqual( + Draw.getSelectedIds(), + [polygonId, lineId], + 'polygon and line are selected' + ); - const returnB = Draw.changeMode('simple_select', { featureIds: [polygonId, lineId]}); + const returnB = Draw.changeMode('simple_select', { + featureIds: [polygonId, lineId] + }); assert.equal(returnB, Draw, 'returns Draw instance'); - assert.deepEqual(Draw.getSelectedIds(), [polygonId, lineId], - 'polygon and line are still selected'); + assert.deepEqual( + Draw.getSelectedIds(), + [polygonId, lineId], + 'polygon and line are still selected' + ); const returnC = Draw.changeMode('simple_select', { featureIds: [pointId] }); assert.equal(returnC, Draw, 'returns Draw instance'); @@ -389,112 +482,204 @@ test('Draw.changeMode to select and de-select pre-existing features', async () = assert.ok('a render occurred when selection changed'); - assert.deepEqual(Draw.getSelectedIds(), [pointId], - 'change to simple_select with different featureIds to change selection'); + assert.deepEqual( + Draw.getSelectedIds(), + [pointId], + 'change to simple_select with different featureIds to change selection' + ); const returnD = Draw.changeMode('direct_select', { featureId: polygonId }); assert.equal(returnD, Draw, 'returns Draw instance'); - assert.deepEqual(Draw.getSelectedIds(), [polygonId], - 'change to direct_select changes selection'); + assert.deepEqual( + Draw.getSelectedIds(), + [polygonId], + 'change to direct_select changes selection' + ); const returnE = Draw.changeMode('direct_select', { featureId: polygonId }); assert.equal(returnE, Draw, 'returns Draw instance'); - assert.deepEqual(Draw.getSelectedIds(), [polygonId], - 'changing to direct_select with same selection does nothing'); + assert.deepEqual( + Draw.getSelectedIds(), + [polygonId], + 'changing to direct_select with same selection does nothing' + ); Draw.deleteAll(); }); test('Draw.modes', () => { - assert.equal(Draw.modes.SIMPLE_SELECT, Constants.modes.SIMPLE_SELECT, 'simple_select'); - assert.equal(Draw.modes.DIRECT_SELECT, Constants.modes.DIRECT_SELECT, 'direct_select'); + assert.equal( + Draw.modes.SIMPLE_SELECT, + Constants.modes.SIMPLE_SELECT, + 'simple_select' + ); + assert.equal( + Draw.modes.DIRECT_SELECT, + Constants.modes.DIRECT_SELECT, + 'direct_select' + ); assert.equal(Draw.modes.DRAW_POINT, Constants.modes.DRAW_POINT, 'draw_point'); - assert.equal(Draw.modes.DRAW_LINE_STRING, Constants.modes.DRAW_LINE_STRING, 'draw_line_string'); - assert.equal(Draw.modes.DRAW_POLYGON, Constants.modes.DRAW_POLYGON, 'draw_polygon'); - assert.equal(getPublicMemberKeys(Draw.modes).length, 5, 'no unexpected modes'); + assert.equal( + Draw.modes.DRAW_LINE_STRING, + Constants.modes.DRAW_LINE_STRING, + 'draw_line_string' + ); + assert.equal( + Draw.modes.DRAW_POLYGON, + Constants.modes.DRAW_POLYGON, + 'draw_polygon' + ); + assert.equal( + getPublicMemberKeys(Draw.modes).length, + 5, + 'no unexpected modes' + ); }); test('Draw.combineFeatures -- polygon + polygon = multiploygon', () => { const [polygonId] = Draw.add(getGeoJSON('polygon')); const [polygon2Id] = Draw.add(getGeoJSON('polygon2')); - Draw.changeMode('simple_select', { featureIds: [polygonId, polygon2Id]}); + Draw.changeMode('simple_select', { featureIds: [polygonId, polygon2Id] }); Draw.combineFeatures(); assert.equal(Draw.getAll().features.length, 1, 'can combine two features'); - assert.equal(Draw.getAll().features[0].geometry.type, 'MultiPolygon', 'can combine two polygons into MultiPolygon'); - assert.deepEqual(Draw.getAll().features[0].geometry.coordinates[0], getGeoJSON('polygon').geometry.coordinates, 'first set of coordinates in multipolygon matches with second polygon in selection'); - assert.deepEqual(Draw.getAll().features[0].geometry.coordinates[1], getGeoJSON('polygon2').geometry.coordinates, 'second set of coordinates in multipolygon matches with first polygon in selection'); + assert.equal( + Draw.getAll().features[0].geometry.type, + 'MultiPolygon', + 'can combine two polygons into MultiPolygon' + ); + assert.deepEqual( + Draw.getAll().features[0].geometry.coordinates[0], + getGeoJSON('polygon').geometry.coordinates, + 'first set of coordinates in multipolygon matches with second polygon in selection' + ); + assert.deepEqual( + Draw.getAll().features[0].geometry.coordinates[1], + getGeoJSON('polygon2').geometry.coordinates, + 'second set of coordinates in multipolygon matches with first polygon in selection' + ); Draw.deleteAll(); }); test('Draw.combineFeatures -- point + point = multipoint', () => { const [pointId] = Draw.add(getGeoJSON('point')); const [point2Id] = Draw.add(getGeoJSON('point2')); - Draw.changeMode('simple_select', { featureIds: [pointId, point2Id]}); + Draw.changeMode('simple_select', { featureIds: [pointId, point2Id] }); Draw.combineFeatures(); assert.equal(Draw.getAll().features.length, 1, 'can combine two features'); - assert.equal(Draw.getAll().features[0].geometry.type, 'MultiPoint', 'can combine two points into MultiPoint'); - assert.deepEqual(Draw.getAll().features[0].geometry.coordinates[0], getGeoJSON('point').geometry.coordinates, 'first set of coordinates in multipoint matches with first point in selection'); - assert.deepEqual(Draw.getAll().features[0].geometry.coordinates[1], getGeoJSON('point2').geometry.coordinates, 'second set of coordinates in multipoint matches with second point in selection'); + assert.equal( + Draw.getAll().features[0].geometry.type, + 'MultiPoint', + 'can combine two points into MultiPoint' + ); + assert.deepEqual( + Draw.getAll().features[0].geometry.coordinates[0], + getGeoJSON('point').geometry.coordinates, + 'first set of coordinates in multipoint matches with first point in selection' + ); + assert.deepEqual( + Draw.getAll().features[0].geometry.coordinates[1], + getGeoJSON('point2').geometry.coordinates, + 'second set of coordinates in multipoint matches with second point in selection' + ); Draw.deleteAll(); }); test('Draw.combineFeatures -- linestring + linestring = multilinestring', () => { const [lineId] = Draw.add(getGeoJSON('line')); const [line2Id] = Draw.add(getGeoJSON('line2')); - Draw.changeMode('simple_select', { featureIds: [lineId, line2Id]}); + Draw.changeMode('simple_select', { featureIds: [lineId, line2Id] }); Draw.combineFeatures(); assert.equal(Draw.getAll().features.length, 1, 'can combine two features'); - assert.equal(Draw.getAll().features[0].geometry.type, 'MultiLineString', 'can combine two linestrings into MultiLineString'); - assert.deepEqual(Draw.getAll().features[0].geometry.coordinates[0], getGeoJSON('line').geometry.coordinates, 'first set of coordinates in multilinestring matches with first line in selection'); - assert.deepEqual(Draw.getAll().features[0].geometry.coordinates[1], getGeoJSON('line2').geometry.coordinates, 'second set of coordinates in multilinestring matches with second line selection'); + assert.equal( + Draw.getAll().features[0].geometry.type, + 'MultiLineString', + 'can combine two linestrings into MultiLineString' + ); + assert.deepEqual( + Draw.getAll().features[0].geometry.coordinates[0], + getGeoJSON('line').geometry.coordinates, + 'first set of coordinates in multilinestring matches with first line in selection' + ); + assert.deepEqual( + Draw.getAll().features[0].geometry.coordinates[1], + getGeoJSON('line2').geometry.coordinates, + 'second set of coordinates in multilinestring matches with second line selection' + ); Draw.deleteAll(); }); - test('Draw.combineFeatures -- point + multipoint = multipoint', () => { const [pointId] = Draw.add(getGeoJSON('point')); const [multipointId] = Draw.add(getGeoJSON('multiPoint')); - Draw.changeMode('simple_select', { featureIds: [pointId, multipointId]}); + Draw.changeMode('simple_select', { featureIds: [pointId, multipointId] }); Draw.combineFeatures(); assert.equal(Draw.getAll().features.length, 1, 'can combine two features'); - assert.equal(Draw.getAll().features[0].geometry.type, 'MultiPoint', 'can combine two points into MultiPoint'); - assert.deepEqual(Draw.getAll().features[0].geometry.coordinates[0], getGeoJSON('point').geometry.coordinates, 'first set of coordinates in multipoint matches with first point in selection'); - assert.deepEqual(Draw.getAll().features[0].geometry.coordinates[1], getGeoJSON('multiPoint').geometry.coordinates[0], 'second set of coordinates in multipoint matches with first set of coordinates in multipoint in selection'); - assert.deepEqual(Draw.getAll().features[0].geometry.coordinates[2], getGeoJSON('multiPoint').geometry.coordinates[1], 'third set of coordinates in multipoint matches with second set of coordinates in multipoint in selection'); + assert.equal( + Draw.getAll().features[0].geometry.type, + 'MultiPoint', + 'can combine two points into MultiPoint' + ); + assert.deepEqual( + Draw.getAll().features[0].geometry.coordinates[0], + getGeoJSON('point').geometry.coordinates, + 'first set of coordinates in multipoint matches with first point in selection' + ); + assert.deepEqual( + Draw.getAll().features[0].geometry.coordinates[1], + getGeoJSON('multiPoint').geometry.coordinates[0], + 'second set of coordinates in multipoint matches with first set of coordinates in multipoint in selection' + ); + assert.deepEqual( + Draw.getAll().features[0].geometry.coordinates[2], + getGeoJSON('multiPoint').geometry.coordinates[1], + 'third set of coordinates in multipoint matches with second set of coordinates in multipoint in selection' + ); Draw.deleteAll(); }); test('Draw.combineFeatures -- return on non-similar features', () => { const [lineId] = Draw.add(getGeoJSON('line')); const [polygonId] = Draw.add(getGeoJSON('polygon')); - Draw.changeMode('simple_select', { featureIds: [lineId, polygonId]}); + Draw.changeMode('simple_select', { featureIds: [lineId, polygonId] }); Draw.combineFeatures(); - assert.equal(Draw.getAll().features.length, 2, 'should not combine non similar features'); + assert.equal( + Draw.getAll().features.length, + 2, + 'should not combine non similar features' + ); Draw.deleteAll(); }); test('Draw.combineFeatures -- do nothing on non-similar features', () => { const [lineId] = Draw.add(getGeoJSON('line')); const [polygonId] = Draw.add(getGeoJSON('polygon')); - Draw.changeMode('simple_select', { featureIds: [lineId, polygonId]}); + Draw.changeMode('simple_select', { featureIds: [lineId, polygonId] }); Draw.combineFeatures(); - assert.equal(Draw.getAll().features.length, 2, 'should not combine non similar features'); + assert.equal( + Draw.getAll().features.length, + 2, + 'should not combine non similar features' + ); Draw.deleteAll(); }); test('Draw.combineFeatures -- work for multifeature + feature', () => { const [multipolygonId] = Draw.add(getGeoJSON('multiPolygon')); const [polygonId] = Draw.add(getGeoJSON('polygon')); - Draw.changeMode('simple_select', { featureIds: [polygonId, multipolygonId]}); + Draw.changeMode('simple_select', { featureIds: [polygonId, multipolygonId] }); Draw.combineFeatures(); - assert.equal(Draw.getAll().features.length, 1, 'should work for multifeature + feature'); + assert.equal( + Draw.getAll().features.length, + 1, + 'should work for multifeature + feature' + ); Draw.deleteAll(); }); @@ -504,33 +689,41 @@ test('Draw.combineFeatures -- should do nothing if nothing is selected', () => { Draw.changeMode('simple_select', {}); Draw.combineFeatures(); - assert.equal(Draw.getAll().features.length, 2, 'should do nothing if nothing is selected'); + assert.equal( + Draw.getAll().features.length, + 2, + 'should do nothing if nothing is selected' + ); Draw.deleteAll(); }); test('Draw.uncombineFeatures -- multilinestring', () => { const [multiLineStringId] = Draw.add(getGeoJSON('multiLineString')); - Draw.changeMode('simple_select', { featureIds: [multiLineStringId]}); + Draw.changeMode('simple_select', { featureIds: [multiLineStringId] }); Draw.uncombineFeatures(); const featuresInDraw = Draw.getAll().features; assert.equal(featuresInDraw.length, 2, 'can uncombine multiLineString'); - assert.deepEqual(featuresInDraw[0].geometry.coordinates, + assert.deepEqual( + featuresInDraw[0].geometry.coordinates, getGeoJSON('multiLineString').geometry.coordinates[0], - 'first set of coordinates in multilinestring matches with first lineString in selection'); + 'first set of coordinates in multilinestring matches with first lineString in selection' + ); - assert.deepEqual(featuresInDraw[1].geometry.coordinates, + assert.deepEqual( + featuresInDraw[1].geometry.coordinates, getGeoJSON('multiLineString').geometry.coordinates[1], - 'second set of coordinates in multilinestring matches with second lineString in selection'); + 'second set of coordinates in multilinestring matches with second lineString in selection' + ); Draw.deleteAll(); }); test('Draw.uncombineFeatures -- multipolygon', () => { const [multipolygon2Id] = Draw.add(getGeoJSON('multiPolygon2')); - Draw.changeMode('simple_select', { featureIds: [multipolygon2Id]}); + Draw.changeMode('simple_select', { featureIds: [multipolygon2Id] }); Draw.uncombineFeatures(); @@ -538,20 +731,24 @@ test('Draw.uncombineFeatures -- multipolygon', () => { assert.equal(featuresInDraw.length, 2, 'can uncombine multipolygon'); - assert.deepEqual(featuresInDraw[0].geometry.coordinates, + assert.deepEqual( + featuresInDraw[0].geometry.coordinates, getGeoJSON('multiPolygon2').geometry.coordinates[0], - 'first set of coordinates in multipolygon matches with first polygon in selection'); + 'first set of coordinates in multipolygon matches with first polygon in selection' + ); - assert.deepEqual(featuresInDraw[1].geometry.coordinates, + assert.deepEqual( + featuresInDraw[1].geometry.coordinates, getGeoJSON('multiPolygon2').geometry.coordinates[1], - 'second set of coordinates in multipolygon matches with second polygon in selection'); + 'second set of coordinates in multipolygon matches with second polygon in selection' + ); Draw.deleteAll(); }); test('Draw.uncombineFeatures -- multipoint', () => { const [multipointId] = Draw.add(getGeoJSON('multiPoint')); - Draw.changeMode('simple_select', { featureIds: [multipointId]}); + Draw.changeMode('simple_select', { featureIds: [multipointId] }); Draw.uncombineFeatures(); @@ -559,13 +756,17 @@ test('Draw.uncombineFeatures -- multipoint', () => { assert.equal(featuresInDraw.length, 2, 'can uncombine multipoint'); - assert.deepEqual(featuresInDraw[0].geometry.coordinates, + assert.deepEqual( + featuresInDraw[0].geometry.coordinates, getGeoJSON('multiPoint').geometry.coordinates[0], - 'first set of coordinates in multipoint matches with first point in selection'); + 'first set of coordinates in multipoint matches with first point in selection' + ); - assert.deepEqual(featuresInDraw[1].geometry.coordinates, + assert.deepEqual( + featuresInDraw[1].geometry.coordinates, getGeoJSON('multiPoint').geometry.coordinates[1], - 'second set of coordinates in multipoint matches with second point in selection'); + 'second set of coordinates in multipoint matches with second point in selection' + ); Draw.deleteAll(); }); @@ -576,17 +777,25 @@ test('Draw.uncombineFeatures -- should do nothing if nothing is selected', () => Draw.changeMode('simple_select', {}); Draw.uncombineFeatures(); - assert.equal(Draw.getAll().features.length, 2, 'should do nothing if nothing is selected'); + assert.equal( + Draw.getAll().features.length, + 2, + 'should do nothing if nothing is selected' + ); Draw.deleteAll(); }); test('Draw.uncombineFeatures -- should do nothing if nothing if only non multifeature is selected', () => { const [polygonId] = Draw.add(getGeoJSON('polygon')); const [pointId] = Draw.add(getGeoJSON('point')); - Draw.changeMode('simple_select', { featureIds: [polygonId, pointId]}); + Draw.changeMode('simple_select', { featureIds: [polygonId, pointId] }); Draw.uncombineFeatures(); - assert.equal(Draw.getAll().features.length, 2, 'should do nothing if nothing is selected'); + assert.equal( + Draw.getAll().features.length, + 2, + 'should do nothing if nothing is selected' + ); Draw.deleteAll(); }); @@ -595,7 +804,11 @@ test('Draw.setFeatureProperty', () => { const featureId = Draw.getAll().features[0].id; const drawInstance = Draw.setFeatureProperty(featureId, 'price', 200); assert.equal(drawInstance, Draw, 'returns Draw instance'); - assert.equal(Draw.get(featureId).properties.price, 200, 'Draw.setFeatureProperty adds a property'); + assert.equal( + Draw.get(featureId).properties.price, + 200, + 'Draw.setFeatureProperty adds a property' + ); }); test('Cleanup', () => { diff --git a/test/common_selectors.test.js b/test/common_selectors.test.js deleted file mode 100644 index d4fd1b27..00000000 --- a/test/common_selectors.test.js +++ /dev/null @@ -1,254 +0,0 @@ -import test from 'node:test'; -import assert from 'node:assert/strict'; - -import * as commonSelectors from '../src/lib/common_selectors.js'; - -test('commonSelectors.isOfMetaType', () => { - const isFoo = commonSelectors.isOfMetaType('foo'); - assert.equal(typeof isFoo, 'function'); - assert.ok(isFoo({ - featureTarget: { - properties: { - meta: 'foo' - } - } - })); - assert.equal(isFoo({}), false); - assert.equal(isFoo({ - featureTarget: { - properties: { - meta: 'bar' - } - } - }), false); -}); - -test('commonSelectors.isShiftMousedown', () => { - assert.ok(commonSelectors.isShiftMousedown({ - originalEvent: { - shiftKey: true, - button: 0 - } - })); - - assert.equal(commonSelectors.isShiftMousedown({ - originalEvent: { - shiftKey: false, - button: 0 - } - }), false); - - assert.equal(commonSelectors.isShiftMousedown({ - originalEvent: { - shiftKey: true, - button: 1 - } - }), false); - - assert.equal(commonSelectors.isShiftMousedown({ - nothing: false - }), false); -}); - -test('commonSelectors.isActiveFeature', () => { - assert.ok(commonSelectors.isActiveFeature({ - featureTarget: { - properties: { - active: 'true', - meta: 'feature' - } - } - })); - - assert.equal(commonSelectors.isActiveFeature({ - foo: 'bar' - }), false); - - assert.equal(commonSelectors.isActiveFeature({ - featureTarget: { - properties: { - active: 'false', - meta: 'feature' - } - } - }), false); - - assert.equal(commonSelectors.isActiveFeature({ - featureTarget: { - properties: { - active: 'true', - meta: 'something' - } - } - }), false); - - assert.equal(commonSelectors.isActiveFeature({ - featureTarget: { - properties: { - active: true, - meta: 'Feature' - } - } - }), false); - - assert.equal(commonSelectors.isActiveFeature({ - nothing: false - }), false); - - assert.equal(commonSelectors.isActiveFeature({ - featureTarget: {} - }), false); -}); - -test('commonSelectors.isInactiveFeature', () => { - assert.ok(commonSelectors.isInactiveFeature({ - featureTarget: { - properties: { - active: 'false', - meta: 'feature' - } - } - })); - - assert.equal(commonSelectors.isInactiveFeature({ - foo: 'bar' - }), false); - - assert.equal(commonSelectors.isInactiveFeature({ - featureTarget: { - properties: { - active: 'true', - meta: 'feature' - } - } - }), false); - - assert.equal(commonSelectors.isInactiveFeature({ - featureTarget: { - properties: { - active: 'false', - meta: 'something' - } - } - }), false); - - assert.equal(commonSelectors.isInactiveFeature({ - featureTarget: { - properties: { - active: false, - meta: 'Feature' - } - } - }), false); - - assert.equal(commonSelectors.isInactiveFeature({ - nothing: false - }), false); - - assert.equal(commonSelectors.isInactiveFeature({ - featureTarget: {} - }), false); -}); - -test('commonSelectors.noTarget', () => { - assert.ok(commonSelectors.noTarget({ - something: 1 - })); - - assert.ok(commonSelectors.noTarget({ - FeatureTarget: 1 - })); - - assert.equal(commonSelectors.noTarget({ - featureTarget: {} - }), false); - - assert.equal(commonSelectors.noTarget({ - featureTarget: null - }), false); -}); - -test('commonSelectors.isFeature', () => { - assert.ok(commonSelectors.isFeature({ - featureTarget: { - properties: { - meta: 'feature' - } - } - })); - - assert.equal(commonSelectors.isFeature({ - feee: 2 - }), false); - - assert.equal(commonSelectors.isFeature({ - featureTarget: { - properties: { - meta: 'nonfeature' - } - } - }), false); - - assert.equal(commonSelectors.isFeature({ - nothing: false - }), false); - - assert.equal(commonSelectors.isFeature({ - featureTarget: {} - }), false); -}); - -test('commonSelectors.isShiftDown', () => { - assert.ok(commonSelectors.isShiftDown({ - originalEvent: { - shiftKey: true - } - })); - - assert.equal(commonSelectors.isShiftDown({ - originalEvent: { - shiftKey: false - } - }), false); - - assert.equal(commonSelectors.isShiftDown({ - originalEvent: {} - }), false); - - assert.equal(commonSelectors.isShiftDown({ - nothing: true - }), false); -}); - -test('commonSelectors.isEscapeKey', () => { - assert.ok(commonSelectors.isEscapeKey({ - keyCode: 27 - })); - - assert.equal(commonSelectors.isEscapeKey({ - keyCode: 13 - }), false); - - assert.equal(commonSelectors.isEscapeKey({ - originalEvent: {} - }), false); -}); - -test('commonSelectors.isEnterKey', () => { - assert.ok(commonSelectors.isEnterKey({ - keyCode: 13 - })); - - assert.equal(commonSelectors.isEnterKey({ - keyCode: 27 - }), false); - - assert.equal(commonSelectors.isEnterKey({ - originalEvent: {} - }), false); -}); - -test('commonSelectors.true', () => { - assert.ok(commonSelectors.isTrue()); - assert.ok(commonSelectors.isTrue(false)); -}); diff --git a/test/common_selectors.test.ts b/test/common_selectors.test.ts new file mode 100644 index 00000000..d50d69c6 --- /dev/null +++ b/test/common_selectors.test.ts @@ -0,0 +1,362 @@ +import './mock-browser'; +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import * as commonSelectors from '../src/lib/common_selectors'; +import type { MapMouseEvent } from '../src/types/types'; + +test('commonSelectors.isOfMetaType', () => { + const isFoo = commonSelectors.isOfMetaType('foo'); + assert.equal(typeof isFoo, 'function'); + assert.ok( + isFoo({ + featureTarget: { + properties: { + meta: 'foo' + } + } + } as unknown as MapMouseEvent), + ); + assert.equal(isFoo({} as unknown as MapMouseEvent), false); + assert.equal( + isFoo({ + featureTarget: { + properties: { + meta: 'bar' + } + } + } as unknown as MapMouseEvent), + false + ); +}); + +test('commonSelectors.isShiftMousedown', () => { + assert.ok( + commonSelectors.isShiftMousedown({ + originalEvent: { + shiftKey: true, + button: 0 + } + } as unknown as MapMouseEvent) + ); + + assert.equal( + commonSelectors.isShiftMousedown({ + originalEvent: { + shiftKey: false, + button: 0 + } + } as unknown as MapMouseEvent), + false + ); + + assert.equal( + commonSelectors.isShiftMousedown({ + originalEvent: { + shiftKey: true, + button: 1 + } + } as unknown as MapMouseEvent), + false + ); + + assert.equal( + commonSelectors.isShiftMousedown({ + nothing: false + } as unknown as MapMouseEvent), + false + ); +}); + +test('commonSelectors.isActiveFeature', () => { + assert.ok( + commonSelectors.isActiveFeature({ + featureTarget: { + properties: { + active: 'true', + meta: 'feature' + } + } + } as unknown as MapMouseEvent) + ); + + assert.equal( + commonSelectors.isActiveFeature({ + foo: 'bar' + } as unknown as MapMouseEvent), + false + ); + + assert.equal( + commonSelectors.isActiveFeature({ + featureTarget: { + properties: { + active: 'false', + meta: 'feature' + } + } + } as unknown as MapMouseEvent), + false + ); + + assert.equal( + commonSelectors.isActiveFeature({ + featureTarget: { + properties: { + active: 'true', + meta: 'something' + } + } + } as unknown as MapMouseEvent), + false + ); + + assert.equal( + commonSelectors.isActiveFeature({ + featureTarget: { + properties: { + active: true, + meta: 'Feature' + } + } + } as unknown as MapMouseEvent), + false + ); + + assert.equal( + commonSelectors.isActiveFeature({ + nothing: false + } as unknown as MapMouseEvent), + false + ); + + assert.equal( + commonSelectors.isActiveFeature({ + featureTarget: {} + } as unknown as MapMouseEvent), + false + ); +}); + +test('commonSelectors.isInactiveFeature', () => { + assert.ok( + commonSelectors.isInactiveFeature({ + featureTarget: { + properties: { + active: 'false', + meta: 'feature' + } + } + } as unknown as MapMouseEvent) + ); + + assert.equal( + commonSelectors.isInactiveFeature({ + foo: 'bar' + } as unknown as MapMouseEvent), + false + ); + + assert.equal( + commonSelectors.isInactiveFeature({ + featureTarget: { + properties: { + active: 'true', + meta: 'feature' + } + } + } as unknown as MapMouseEvent), + false + ); + + assert.equal( + commonSelectors.isInactiveFeature({ + featureTarget: { + properties: { + active: 'false', + meta: 'something' + } + } + } as unknown as MapMouseEvent), + false + ); + + assert.equal( + commonSelectors.isInactiveFeature({ + featureTarget: { + properties: { + active: false, + meta: 'Feature' + } + } + } as unknown as MapMouseEvent), + false + ); + + assert.equal( + commonSelectors.isInactiveFeature({ + nothing: false + } as unknown as MapMouseEvent), + false + ); + + assert.equal( + commonSelectors.isInactiveFeature({ + featureTarget: {} + } as unknown as MapMouseEvent), + false + ); +}); + +test('commonSelectors.noTarget', () => { + assert.ok( + commonSelectors.noTarget({ + something: 1 + } as unknown as MapMouseEvent) + ); + + assert.ok( + commonSelectors.noTarget({ + FeatureTarget: 1 + } as unknown as MapMouseEvent) + ); + + assert.equal( + commonSelectors.noTarget({ + featureTarget: {} + } as unknown as MapMouseEvent), + false + ); + + assert.equal( + commonSelectors.noTarget({ + featureTarget: null + } as unknown as MapMouseEvent), + false + ); +}); + +test('commonSelectors.isFeature', () => { + assert.ok( + commonSelectors.isFeature({ + featureTarget: { + properties: { + meta: 'feature' + } + } + } as unknown as MapMouseEvent) + ); + + assert.equal( + commonSelectors.isFeature({ + feee: 2 + } as unknown as MapMouseEvent), + false + ); + + assert.equal( + commonSelectors.isFeature({ + featureTarget: { + properties: { + meta: 'nonfeature' + } + } + } as unknown as MapMouseEvent), + false + ); + + assert.equal( + commonSelectors.isFeature({ + nothing: false + } as unknown as MapMouseEvent), + false + ); + + assert.equal( + commonSelectors.isFeature({ + featureTarget: {} + } as unknown as MapMouseEvent), + false + ); +}); + +test('commonSelectors.isShiftDown', () => { + assert.ok( + commonSelectors.isShiftDown({ + originalEvent: { + shiftKey: true + } + } as unknown as MapMouseEvent) + ); + + assert.equal( + commonSelectors.isShiftDown({ + originalEvent: { + shiftKey: false + } + } as unknown as MapMouseEvent), + false + ); + + assert.equal( + commonSelectors.isShiftDown({ + originalEvent: {} + } as unknown as MapMouseEvent), + false + ); + + assert.equal( + commonSelectors.isShiftDown({ + nothing: true + } as unknown as MapMouseEvent), + false + ); +}); + +test('commonSelectors.isEscapeKey', () => { + assert.ok( + commonSelectors.isEscapeKey({ + keyCode: 27 + } as unknown as KeyboardEvent) + ); + + assert.equal( + commonSelectors.isEscapeKey({ + keyCode: 13 + } as unknown as KeyboardEvent), + false + ); + + assert.equal( + commonSelectors.isEscapeKey({ + originalEvent: {} + } as unknown as KeyboardEvent), + false + ); +}); + +test('commonSelectors.isEnterKey', () => { + assert.ok( + commonSelectors.isEnterKey({ + keyCode: 13 + } as unknown as KeyboardEvent) + ); + + assert.equal( + commonSelectors.isEnterKey({ + keyCode: 27 + } as unknown as KeyboardEvent), + false + ); + + assert.equal( + commonSelectors.isEnterKey({ + originalEvent: {} + } as unknown as KeyboardEvent), + false + ); +}); + +test('commonSelectors.true', () => { + assert.ok(commonSelectors.isTrue()); +}); diff --git a/test/constrain_feature_movement.test.js b/test/constrain_feature_movement.test.ts similarity index 62% rename from test/constrain_feature_movement.test.js rename to test/constrain_feature_movement.test.ts index 5eba5725..bba99d48 100644 --- a/test/constrain_feature_movement.test.js +++ b/test/constrain_feature_movement.test.ts @@ -1,7 +1,8 @@ +import './mock-browser'; import test from 'node:test'; import assert from 'node:assert/strict'; -import getGeoJSON from './utils/get_geojson.js'; -import constrainFeatureMovement from '../src/lib/constrain_feature_movement.js'; +import getGeoJSON from './utils/get_geojson'; +import { constrainFeatureMovement } from '../src/lib/constrain_feature_movement'; test('constrainFeatureMovement point, no constraint', () => { const point = getGeoJSON('point'); @@ -14,7 +15,6 @@ test('constrainFeatureMovement point, no constraint', () => { lat: 13, lng: 0 }); - }); test('constrainFeatureMovement point, requiring northern constraint', () => { @@ -24,11 +24,14 @@ test('constrainFeatureMovement point, requiring northern constraint', () => { lat: 80, lng: 0 }); - assert.deepEqual(constrainedDelta, { - lat: 65, - lng: 0 - }, 'stopped within projection'); - + assert.deepEqual( + constrainedDelta, + { + lat: 65, + lng: 0 + }, + 'stopped within projection' + ); }); test('constrainFeatureMovement point, requiring southern constraint', () => { @@ -38,11 +41,14 @@ test('constrainFeatureMovement point, requiring southern constraint', () => { lat: -80, lng: 0 }); - assert.deepEqual(constrainedDelta, { - lat: -65, - lng: 0 - }, 'stopped within projection'); - + assert.deepEqual( + constrainedDelta, + { + lat: -65, + lng: 0 + }, + 'stopped within projection' + ); }); test('constrainFeatureMovement point, requiring western wrap', () => { @@ -52,11 +58,14 @@ test('constrainFeatureMovement point, requiring western wrap', () => { lat: 10, lng: -300 }); - assert.deepEqual(constrainedDelta, { - lat: 10, - lng: 60 - }, 'stopped within bounds'); - + assert.deepEqual( + constrainedDelta, + { + lat: 10, + lng: 60 + }, + 'stopped within bounds' + ); }); test('constrainFeatureMovement point, requiring eastern wrap', () => { @@ -66,16 +75,23 @@ test('constrainFeatureMovement point, requiring eastern wrap', () => { lat: 10, lng: 300 }); - assert.deepEqual(constrainedDelta, { - lat: 10, - lng: -60 - }, 'stopped within bounds'); - + assert.deepEqual( + constrainedDelta, + { + lat: 10, + lng: -60 + }, + 'stopped within bounds' + ); }); test('constrainFeatureMovement line, no constraint', () => { const line = getGeoJSON('line'); - line.geometry.coordinates = [[0, 0], [10, 10], [20, 20]]; + line.geometry.coordinates = [ + [0, 0], + [10, 10], + [20, 20] + ]; const constrainedDelta = constrainFeatureMovement([line], { lat: 13, lng: 0 @@ -84,68 +100,95 @@ test('constrainFeatureMovement line, no constraint', () => { lat: 13, lng: 0 }); - }); test('constrainFeatureMovement line, requiring northern inner constraint', () => { const line = getGeoJSON('line'); - line.geometry.coordinates = [[80, 80], [81, 81]]; + line.geometry.coordinates = [ + [80, 80], + [81, 81] + ]; const constrainedDelta = constrainFeatureMovement([line], { lat: 7, lng: 0 }); - assert.deepEqual(constrainedDelta, { - lat: 5, - lng: 0 - }, 'stopped within projection'); - + assert.deepEqual( + constrainedDelta, + { + lat: 5, + lng: 0 + }, + 'stopped within projection' + ); }); test('constrainFeatureMovement line, requiring northern outer constraint', () => { const line = getGeoJSON('line'); - line.geometry.coordinates = [[30, 30], [81, 81]]; + line.geometry.coordinates = [ + [30, 30], + [81, 81] + ]; const constrainedDelta = constrainFeatureMovement([line], { lat: 12, lng: 0 }); - assert.deepEqual(constrainedDelta, { - lat: 9, - lng: 0 - }, 'stopped within poles'); - + assert.deepEqual( + constrainedDelta, + { + lat: 9, + lng: 0 + }, + 'stopped within poles' + ); }); test('constrainFeatureMovement line, requiring southern inner constraint', () => { const line = getGeoJSON('line'); - line.geometry.coordinates = [[-80, -80], [-81, -81]]; + line.geometry.coordinates = [ + [-80, -80], + [-81, -81] + ]; const constrainedDelta = constrainFeatureMovement([line], { lat: -7, lng: 0 }); - assert.deepEqual(constrainedDelta, { - lat: -5, - lng: 0 - }, 'stopped within projection'); - + assert.deepEqual( + constrainedDelta, + { + lat: -5, + lng: 0 + }, + 'stopped within projection' + ); }); test('constrainFeatureMovement line, requiring southern outer constraint', () => { const line = getGeoJSON('line'); - line.geometry.coordinates = [[-30, -30], [-81, -81]]; + line.geometry.coordinates = [ + [-30, -30], + [-81, -81] + ]; const constrainedDelta = constrainFeatureMovement([line], { lat: -12, lng: 0 }); - assert.deepEqual(constrainedDelta, { - lat: -9, - lng: 0 - }, 'stopped within poles'); - + assert.deepEqual( + constrainedDelta, + { + lat: -9, + lng: 0 + }, + 'stopped within poles' + ); }); test('constrainFeatureMovement line, requiring western wrap', () => { const line = getGeoJSON('line'); - line.geometry.coordinates = [[0, 0], [10, 10], [20, 20]]; + line.geometry.coordinates = [ + [0, 0], + [10, 10], + [20, 20] + ]; const constrainedDelta = constrainFeatureMovement([line], { lat: 0, lng: -280 @@ -154,12 +197,15 @@ test('constrainFeatureMovement line, requiring western wrap', () => { lat: 0, lng: 80 }); - }); test('constrainFeatureMovement line, requiring eastern wrap', () => { const line = getGeoJSON('line'); - line.geometry.coordinates = [[0, 0], [10, 10], [20, 20]]; + line.geometry.coordinates = [ + [0, 0], + [10, 10], + [20, 20] + ]; const constrainedDelta = constrainFeatureMovement([line], { lat: 0, lng: 255 @@ -168,12 +214,18 @@ test('constrainFeatureMovement line, requiring eastern wrap', () => { lat: 0, lng: -105 }); - }); test('constrainFeatureMovement polygon, no constraint', () => { const polygon = getGeoJSON('polygon'); - polygon.geometry.coordinates = [[[0, 0], [10, 10], [20, 20], [0, 0]]]; + polygon.geometry.coordinates = [ + [ + [0, 0], + [10, 10], + [20, 20], + [0, 0] + ] + ]; const constrainedDelta = constrainFeatureMovement([polygon], { lat: 13, lng: 0 @@ -182,68 +234,114 @@ test('constrainFeatureMovement polygon, no constraint', () => { lat: 13, lng: 0 }); - }); test('constrainFeatureMovement polygon, requiring northern inner constraint', () => { const polygon = getGeoJSON('polygon'); - polygon.geometry.coordinates = [[[80, 80], [81, 81], [81, 82], [80, 80]]]; + polygon.geometry.coordinates = [ + [ + [80, 80], + [81, 81], + [81, 82], + [80, 80] + ] + ]; const constrainedDelta = constrainFeatureMovement([polygon], { lat: 7, lng: 0 }); - assert.deepEqual(constrainedDelta, { - lat: 5, - lng: 0 - }, 'stopped within projection'); - + assert.deepEqual( + constrainedDelta, + { + lat: 5, + lng: 0 + }, + 'stopped within projection' + ); }); test('constrainFeatureMovement polygon, requiring northern outer constraint', () => { const polygon = getGeoJSON('polygon'); - polygon.geometry.coordinates = [[[30, 30], [30, 40], [81, 81], [30, 30]]]; + polygon.geometry.coordinates = [ + [ + [30, 30], + [30, 40], + [81, 81], + [30, 30] + ] + ]; const constrainedDelta = constrainFeatureMovement([polygon], { lat: 12, lng: 0 }); - assert.deepEqual(constrainedDelta, { - lat: 9, - lng: 0 - }, 'stopped within poles'); - + assert.deepEqual( + constrainedDelta, + { + lat: 9, + lng: 0 + }, + 'stopped within poles' + ); }); test('constrainFeatureMovement polygon, requiring southern inner constraint', () => { const polygon = getGeoJSON('polygon'); - polygon.geometry.coordinates = [[[-80, -80], [-81, -81], [-81, -82], [-80, -80]]]; + polygon.geometry.coordinates = [ + [ + [-80, -80], + [-81, -81], + [-81, -82], + [-80, -80] + ] + ]; const constrainedDelta = constrainFeatureMovement([polygon], { lat: -7, lng: 0 }); - assert.deepEqual(constrainedDelta, { - lat: -5, - lng: 0 - }, 'stopped within projection'); - + assert.deepEqual( + constrainedDelta, + { + lat: -5, + lng: 0 + }, + 'stopped within projection' + ); }); test('constrainFeatureMovement polygon, requiring southern outer constraint', () => { const polygon = getGeoJSON('polygon'); - polygon.geometry.coordinates = [[[-30, -30], [-30, -40], [-81, -81], [-30, -30]]]; + polygon.geometry.coordinates = [ + [ + [-30, -30], + [-30, -40], + [-81, -81], + [-30, -30] + ] + ]; const constrainedDelta = constrainFeatureMovement([polygon], { lat: -12, lng: 0 }); - assert.deepEqual(constrainedDelta, { - lat: -9, - lng: 0 - }, 'stopped within poles'); - + assert.deepEqual( + constrainedDelta, + { + lat: -9, + lng: 0 + }, + 'stopped within poles' + ); }); test('constrainFeatureMovement polygon, requiring western wrap', () => { const polygon = getGeoJSON('polygon'); - polygon.geometry.coordinates = [[[0, 0], [10, 10], [20, 20], [0, 0]]]; + polygon.geometry.coordinates = [ + [ + [0, 0], + [10, 10], + [20, 20], + [0, 0] + ] + ]; const constrainedDelta = constrainFeatureMovement([polygon], { lat: 70, lng: -270 @@ -252,12 +350,18 @@ test('constrainFeatureMovement polygon, requiring western wrap', () => { lat: 70, lng: 90 }); - }); test('constrainFeatureMovement polygon, requiring eastern wrap', () => { const polygon = getGeoJSON('polygon'); - polygon.geometry.coordinates = [[[0, 0], [10, 10], [20, 20], [0, 0]]]; + polygon.geometry.coordinates = [ + [ + [0, 0], + [10, 10], + [20, 20], + [0, 0] + ] + ]; const constrainedDelta = constrainFeatureMovement([polygon], { lat: 35, lng: 270 @@ -266,16 +370,25 @@ test('constrainFeatureMovement polygon, requiring eastern wrap', () => { lat: 35, lng: -90 }); - }); test('constrainFeatureMovement many features, no constraint', () => { const polygon = getGeoJSON('polygon'); - polygon.geometry.coordinates = [[[0, 0], [10, 10], [20, 20], [0, 0]]]; + polygon.geometry.coordinates = [ + [ + [0, 0], + [10, 10], + [20, 20], + [0, 0] + ] + ]; const point = getGeoJSON('point'); point.geometry.coordinates = [15, 15]; const line = getGeoJSON('line'); - line.geometry.coordinates = [[15, 15], [25, 25]]; + line.geometry.coordinates = [ + [15, 15], + [25, 25] + ]; const constrainedDelta = constrainFeatureMovement([polygon, point, line], { lat: 13, lng: 0 @@ -284,88 +397,149 @@ test('constrainFeatureMovement many features, no constraint', () => { lat: 13, lng: 0 }); - }); test('constrainFeatureMovement many features, requiring northern inner constraint', () => { const polygon = getGeoJSON('polygon'); - polygon.geometry.coordinates = [[[80, 80], [81, 81], [81, 82], [80, 80]]]; + polygon.geometry.coordinates = [ + [ + [80, 80], + [81, 81], + [81, 82], + [80, 80] + ] + ]; const point = getGeoJSON('point'); point.geometry.coordinates = [15, 15]; const line = getGeoJSON('line'); - line.geometry.coordinates = [[15, 15], [25, 25]]; + line.geometry.coordinates = [ + [15, 15], + [25, 25] + ]; const constrainedDelta = constrainFeatureMovement([polygon, point, line], { lat: 13, lng: 0 }); - assert.deepEqual(constrainedDelta, { - lat: 5, - lng: 0 - }, 'stopped within projection'); - + assert.deepEqual( + constrainedDelta, + { + lat: 5, + lng: 0 + }, + 'stopped within projection' + ); }); test('constrainFeatureMovement many features, requiring northern outer constraint', () => { const polygon = getGeoJSON('polygon'); - polygon.geometry.coordinates = [[[0, 0], [10, 10], [20, 20], [0, 0]]]; + polygon.geometry.coordinates = [ + [ + [0, 0], + [10, 10], + [20, 20], + [0, 0] + ] + ]; const point = getGeoJSON('point'); point.geometry.coordinates = [15, 15]; const line = getGeoJSON('line'); - line.geometry.coordinates = [[15, 15], [25, 25]]; + line.geometry.coordinates = [ + [15, 15], + [25, 25] + ]; const constrainedDelta = constrainFeatureMovement([polygon, point, line], { lat: 100, lng: 0 }); - assert.deepEqual(constrainedDelta, { - lat: 65, - lng: 0 - }, 'stopped within poles'); - + assert.deepEqual( + constrainedDelta, + { + lat: 65, + lng: 0 + }, + 'stopped within poles' + ); }); test('constrainFeatureMovement many features, requiring southern inner constraint', () => { const polygon = getGeoJSON('polygon'); - polygon.geometry.coordinates = [[[-80, -80], [-81, -81], [-81, -82], [-80, -80]]]; + polygon.geometry.coordinates = [ + [ + [-80, -80], + [-81, -81], + [-81, -82], + [-80, -80] + ] + ]; const point = getGeoJSON('point'); point.geometry.coordinates = [15, 15]; const line = getGeoJSON('line'); - line.geometry.coordinates = [[15, 15], [25, 25]]; + line.geometry.coordinates = [ + [15, 15], + [25, 25] + ]; const constrainedDelta = constrainFeatureMovement([polygon, point, line], { lat: -10, lng: 0 }); - assert.deepEqual(constrainedDelta, { - lat: -5, - lng: 0 - }, 'stopped within projection'); - + assert.deepEqual( + constrainedDelta, + { + lat: -5, + lng: 0 + }, + 'stopped within projection' + ); }); test('constrainFeatureMovement many features, requiring southern outer constraint', () => { const polygon = getGeoJSON('polygon'); - polygon.geometry.coordinates = [[[0, 0], [10, 10], [20, 20], [0, 0]]]; + polygon.geometry.coordinates = [ + [ + [0, 0], + [10, 10], + [20, 20], + [0, 0] + ] + ]; const point = getGeoJSON('point'); point.geometry.coordinates = [15, 15]; const line = getGeoJSON('line'); - line.geometry.coordinates = [[15, 15], [25, 25]]; + line.geometry.coordinates = [ + [15, 15], + [25, 25] + ]; const constrainedDelta = constrainFeatureMovement([polygon, point, line], { lat: -200, lng: 0 }); - assert.deepEqual(constrainedDelta, { - lat: -90, - lng: 0 - }, 'stopped within poles'); - + assert.deepEqual( + constrainedDelta, + { + lat: -90, + lng: 0 + }, + 'stopped within poles' + ); }); test('constrainFeatureMovement many features, requiring western wrap', () => { const polygon = getGeoJSON('polygon'); - polygon.geometry.coordinates = [[[0, 0], [10, 10], [20, 20], [0, 0]]]; + polygon.geometry.coordinates = [ + [ + [0, 0], + [10, 10], + [20, 20], + [0, 0] + ] + ]; const point = getGeoJSON('point'); point.geometry.coordinates = [15, 15]; const line = getGeoJSON('line'); - line.geometry.coordinates = [[15, 15], [25, 25]]; + line.geometry.coordinates = [ + [15, 15], + [25, 25] + ]; const constrainedDelta = constrainFeatureMovement([polygon, point, line], { lat: 27, lng: -310 @@ -374,16 +548,25 @@ test('constrainFeatureMovement many features, requiring western wrap', () => { lat: 27, lng: 50 }); - }); test('constrainFeatureMovement many features, requiring eastern wrap', () => { const polygon = getGeoJSON('polygon'); - polygon.geometry.coordinates = [[[0, 0], [10, 10], [20, 20], [0, 0]]]; + polygon.geometry.coordinates = [ + [ + [0, 0], + [10, 10], + [20, 20], + [0, 0] + ] + ]; const point = getGeoJSON('point'); point.geometry.coordinates = [15, 15]; const line = getGeoJSON('line'); - line.geometry.coordinates = [[15, 15], [25, 25]]; + line.geometry.coordinates = [ + [15, 15], + [25, 25] + ]; const constrainedDelta = constrainFeatureMovement([polygon, point, line], { lat: 27, lng: 260 @@ -392,5 +575,4 @@ test('constrainFeatureMovement many features, requiring eastern wrap', () => { lat: 27, lng: -100 }); - }); diff --git a/test/create_supplementary_points.test.js b/test/create_supplementary_points.test.js deleted file mode 100644 index b0b9d363..00000000 --- a/test/create_supplementary_points.test.js +++ /dev/null @@ -1,661 +0,0 @@ -import test from 'node:test'; -import assert from 'node:assert/strict'; -import createMap from './utils/create_map.js'; -import createSupplementaryPoints from '../src/lib/create_supplementary_points.js'; - -test('createSupplementaryPoints with a point', () => { - const point = { - type: 'Point', - properties: { - id: 'foo' - }, - geometry: { - type: 'Point', - coordinates: [10, 15] - } - }; - - const result = createSupplementaryPoints(point); - - assert.deepEqual(result, [{ - geometry: { - coordinates: [10, 15], - type: 'Point' - }, - properties: { - active: 'false', - coord_path: null, - meta: 'vertex', - parent: 'foo' - }, - type: 'Feature' - }]); - - -}); - -test('createSupplementaryPoints with a line, no midpoints', () => { - const line = { - type: 'Feature', - properties: { - id: 'foo' - }, - geometry: { - type: 'LineString', - coordinates: [[0, 0], [4, 4], [8, 8]] - } - }; - - const result = createSupplementaryPoints(line); - - assert.deepEqual(result, [{ - geometry: { - coordinates: [0, 0], - type: 'Point' - }, - properties: { - active: 'false', - coord_path: '0', - meta: 'vertex', - parent: 'foo' - }, - type: 'Feature' - }, { - geometry: { - coordinates: [4, 4], - type: 'Point' - }, - properties: { - active: 'false', - coord_path: '1', - meta: 'vertex', - parent: 'foo' - }, - type: 'Feature' - }, { - geometry: { - coordinates: [8, 8], - type: 'Point' - }, - properties: { - active: 'false', - coord_path: '2', - meta: 'vertex', - parent: 'foo' - }, - type: 'Feature' - }], 'adds vertices'); - - -}); - -test('createSupplementaryPoints with a polygon, no midpoints', () => { - const polygon = { - type: 'Feature', - properties: { - id: 'foo' - }, - geometry: { - type: 'Polygon', - coordinates: [[[1, 1], [2, 2], [3, 3], [4, 4], [1, 1]]] - } - }; - const result = createSupplementaryPoints(polygon); - - assert.deepEqual(result, [{ - geometry: { - coordinates: [1, 1], - type: 'Point' - }, - properties: { - active: 'false', - coord_path: '0.0', - meta: 'vertex', - parent: 'foo' - }, - type: 'Feature' - }, { - geometry: { - coordinates: [2, 2], - type: 'Point' - }, - properties: { - active: 'false', - coord_path: '0.1', - meta: 'vertex', - parent: 'foo' - }, - type: 'Feature' - }, { - geometry: { - coordinates: [3, 3], - type: 'Point' - }, - properties: { - active: 'false', - coord_path: '0.2', - meta: 'vertex', - parent: 'foo' - }, - type: 'Feature' - }, { - geometry: { - coordinates: [4, 4], - type: 'Point' - }, - properties: { - active: 'false', - coord_path: '0.3', - meta: 'vertex', - parent: 'foo' - }, - type: 'Feature' - }], 'adds vertices'); - - -}); - -test('createSupplementaryPoints with line, midpoints, selected coordinate', () => { - const line = { - type: 'Feature', - properties: { - id: 'foo' - }, - geometry: { - type: 'LineString', - coordinates: [[0, 0], [4, 4], [8, 8]] - } - }; - const map = createMap(); - - const results = createSupplementaryPoints(line, { - map, - midpoints: true, - selectedPaths: '1' - }); - - assert.deepEqual(results, [{ - geometry: { - coordinates: [0, 0], - type: 'Point' - }, - properties: { - active: 'false', - coord_path: '0', - meta: 'vertex', - parent: 'foo' - }, - type: 'Feature' - }, { - geometry: { - coordinates: [2, 2], - type: 'Point' - }, - properties: { - coord_path: '1', - lat: 2, - lng: 2, - meta: 'midpoint', - parent: 'foo' - }, - type: 'Feature' - }, { - geometry: { - coordinates: [4, 4], - type: 'Point' - }, - properties: { - active: 'true', - coord_path: '1', - meta: 'vertex', - parent: 'foo' - }, - type: 'Feature' - }, { - geometry: { - coordinates: [6, 6], - type: 'Point' - }, - properties: { - coord_path: '2', - lat: 6, - lng: 6, - meta: 'midpoint', - parent: 'foo' - }, - type: 'Feature' - }, { - geometry: { - coordinates: [8, 8], - type: 'Point' - }, - properties: { - active: 'false', - coord_path: '2', - meta: 'vertex', - parent: 'foo' - }, - type: 'Feature' - }], 'adds vertices and midpoints'); - - -}); - -test('createSupplementaryPoints with polygon, midpoints, selection', () => { - const polygon = { - type: 'Feature', - properties: { - id: 'foo' - }, - geometry: { - type: 'Polygon', - coordinates: [[[1, 1], [2, 2], [3, 3], [4, 4], [1, 1]]] - } - }; - - const map = createMap(); - - const results = createSupplementaryPoints(polygon, { - map, - midpoints: true, - selectedPaths: '0.1' - }); - - assert.deepEqual(results, [{ - geometry: { - coordinates: [1, 1], - type: 'Point' - }, - properties: { - active: 'false', - coord_path: '0.0', - meta: 'vertex', - parent: 'foo' - }, - type: 'Feature' - }, { - geometry: { - coordinates: [1.5, 1.5], - type: 'Point' - }, - properties: { - coord_path: '0.1', - lat: 1.5, - lng: 1.5, - meta: 'midpoint', - parent: 'foo' - }, - type: 'Feature' - }, { - geometry: { - coordinates: [2, 2], - type: 'Point' - }, - properties: { - active: 'true', - coord_path: '0.1', - meta: 'vertex', - parent: 'foo' - }, - type: 'Feature' - }, { - geometry: { - coordinates: [2.5, 2.5], - type: 'Point' - }, - properties: { - coord_path: '0.2', - lat: 2.5, - lng: 2.5, - meta: 'midpoint', - parent: 'foo' - }, - type: 'Feature' - }, { - geometry: { - coordinates: [3, 3], - type: 'Point' - }, - properties: { - active: 'false', - coord_path: '0.2', - meta: 'vertex', - parent: 'foo' - }, - type: 'Feature' - }, { - geometry: { - coordinates: [3.5, 3.5], - type: 'Point' - }, - properties: { - coord_path: '0.3', - lat: 3.5, - lng: 3.5, - meta: 'midpoint', - parent: 'foo' - }, - type: 'Feature' - }, { - geometry: { - coordinates: [4, 4], - type: 'Point' - }, - properties: { - active: 'false', - coord_path: '0.3', - meta: 'vertex', - parent: 'foo' - }, - type: 'Feature' - }, { - geometry: { - coordinates: [2.5, 2.5], - type: 'Point' - }, - properties: { - coord_path: '0.4', - lat: 2.5, - lng: 2.5, - meta: 'midpoint', - parent: 'foo' - }, - type: 'Feature' - }], 'adds vertices and midpoints'); - - -}); - -test('createSupplementaryPoints with MultiLineString, midpoints, selected coordinate', () => { - const line = { - type: 'Feature', - properties: { - id: 'foo' - }, - geometry: { - type: 'MultiLineString', - coordinates: [ - [[0, 0], [4, 4], [8, 8]], - [[20, 20], [24, 24], [28, 28]] - ] - } - }; - const map = createMap(); - - const results = createSupplementaryPoints(line, { - map, - midpoints: true, - selectedPaths: '1.2' - }); - - assert.deepEqual(results, [{ - geometry: { - coordinates: [0, 0], - type: 'Point' - }, - properties: { - active: 'false', - coord_path: '0.0', - meta: 'vertex', - parent: 'foo' - }, - type: 'Feature' - }, { - geometry: { - coordinates: [2, 2], - type: 'Point' - }, - properties: { - coord_path: '0.1', - lat: 2, - lng: 2, - meta: 'midpoint', - parent: 'foo' - }, - type: 'Feature' - }, { - geometry: { - coordinates: [4, 4], - type: 'Point' - }, - properties: { - active: 'false', - coord_path: '0.1', - meta: 'vertex', - parent: 'foo' - }, - type: 'Feature' - }, { - geometry: { - coordinates: [6, 6], - type: 'Point' - }, - properties: { - coord_path: '0.2', - lat: 6, - lng: 6, - meta: 'midpoint', - parent: 'foo' - }, - type: 'Feature' - }, { - geometry: { - coordinates: [8, 8], - type: 'Point' - }, - properties: { - active: 'false', - coord_path: '0.2', - meta: 'vertex', - parent: 'foo' - }, - type: 'Feature' - }, { - geometry: { - coordinates: [20, 20], - type: 'Point' - }, - properties: { - active: 'false', - coord_path: '1.0', - meta: 'vertex', - parent: 'foo' - }, - type: 'Feature' - }, { - geometry: { - coordinates: [22, 22], - type: 'Point' - }, - properties: { - coord_path: '1.1', - lat: 22, - lng: 22, - meta: 'midpoint', - parent: 'foo' - }, - type: 'Feature' - }, { - geometry: { - coordinates: [24, 24], - type: 'Point' - }, - properties: { - active: 'false', - coord_path: '1.1', - meta: 'vertex', - parent: 'foo' - }, - type: 'Feature' - }, { - geometry: { - coordinates: [26, 26], - type: 'Point' - }, - properties: { - coord_path: '1.2', - lat: 26, - lng: 26, - meta: 'midpoint', - parent: 'foo' - }, - type: 'Feature' - }, { - geometry: { - coordinates: [28, 28], - type: 'Point' - }, - properties: { - active: 'true', - coord_path: '1.2', - meta: 'vertex', - parent: 'foo' - }, - type: 'Feature' - }]); - - -}); - -test('createSupplementaryPoints with a line, not all midpoints rendered because of vertex exceeding projection latitude north limit', () => { - const line = { - type: 'Feature', - properties: { - id: 'foo' - }, - geometry: { - type: 'LineString', - coordinates: [[0, 0], [4, 4], [7, 87]] - } - }; - - const result = createSupplementaryPoints(line, { - map: createMap(), - midpoints: true - }); - - assert.deepEqual(result, [{ - geometry: { - coordinates: [0, 0], - type: 'Point' - }, - properties: { - active: 'false', - coord_path: '0', - meta: 'vertex', - parent: 'foo' - }, - type: 'Feature' - }, { - geometry: { - coordinates: [2, 2], - type: 'Point' - }, - properties: { - coord_path: '1', - lat: 2, - lng: 2, - meta: 'midpoint', - parent: 'foo' - }, - type: 'Feature' - }, { - geometry: { - coordinates: [4, 4], - type: 'Point' - }, - properties: { - active: 'false', - coord_path: '1', - meta: 'vertex', - parent: 'foo' - }, - type: 'Feature' - }, { - geometry: { - coordinates: [7, 87], - type: 'Point' - }, - properties: { - active: 'false', - coord_path: '2', - meta: 'vertex', - parent: 'foo' - }, - type: 'Feature' - }], 'adds vertices'); - - -}); - - -test('createSupplementaryPoints with a line, not all midpoints rendered because of vertex exceeding projection latitude south limit', () => { - const line = { - type: 'Feature', - properties: { - id: 'foo' - }, - geometry: { - type: 'LineString', - coordinates: [[0, 0], [4, 4], [7, -87]] - } - }; - - const result = createSupplementaryPoints(line, { - map: createMap(), - midpoints: true - }); - - assert.deepEqual(result, [{ - geometry: { - coordinates: [0, 0], - type: 'Point' - }, - properties: { - active: 'false', - coord_path: '0', - meta: 'vertex', - parent: 'foo' - }, - type: 'Feature' - }, { - geometry: { - coordinates: [2, 2], - type: 'Point' - }, - properties: { - coord_path: '1', - lat: 2, - lng: 2, - meta: 'midpoint', - parent: 'foo' - }, - type: 'Feature' - }, { - geometry: { - coordinates: [4, 4], - type: 'Point' - }, - properties: { - active: 'false', - coord_path: '1', - meta: 'vertex', - parent: 'foo' - }, - type: 'Feature' - }, { - geometry: { - coordinates: [7, -87], - type: 'Point' - }, - properties: { - active: 'false', - coord_path: '2', - meta: 'vertex', - parent: 'foo' - }, - type: 'Feature' - }], 'adds vertices'); - - -}); diff --git a/test/create_supplementary_points.test.ts b/test/create_supplementary_points.test.ts new file mode 100644 index 00000000..39648cda --- /dev/null +++ b/test/create_supplementary_points.test.ts @@ -0,0 +1,765 @@ +import './mock-browser'; +import test from 'node:test'; +import assert from 'node:assert/strict'; +import createMap from './utils/create_map'; +import { createSupplementaryPoints } from '../src/lib/create_supplementary_points'; +import type { StrictFeature } from '../src/types/types'; +import {Map} from 'mapbox-gl'; + +test('createSupplementaryPoints with a point', () => { + const point = { + type: 'Point', + properties: { + id: 'foo' + }, + geometry: { + type: 'Point', + coordinates: [10, 15] + } + } as unknown as StrictFeature; + + const result = createSupplementaryPoints(point); + + assert.deepEqual(result, [ + { + geometry: { + coordinates: [10, 15], + type: 'Point' + }, + properties: { + active: 'false', + coord_path: null, + meta: 'vertex', + parent: 'foo' + }, + type: 'Feature' + } + ]); +}); + +test('createSupplementaryPoints with a line, no midpoints', () => { + const line = { + type: 'Feature', + properties: { + id: 'foo' + }, + geometry: { + type: 'LineString', + coordinates: [ + [0, 0], + [4, 4], + [8, 8] + ] + } + } as unknown as StrictFeature; + + const result = createSupplementaryPoints(line); + + assert.deepEqual( + result, + [ + { + geometry: { + coordinates: [0, 0], + type: 'Point' + }, + properties: { + active: 'false', + coord_path: '0', + meta: 'vertex', + parent: 'foo' + }, + type: 'Feature' + }, + { + geometry: { + coordinates: [4, 4], + type: 'Point' + }, + properties: { + active: 'false', + coord_path: '1', + meta: 'vertex', + parent: 'foo' + }, + type: 'Feature' + }, + { + geometry: { + coordinates: [8, 8], + type: 'Point' + }, + properties: { + active: 'false', + coord_path: '2', + meta: 'vertex', + parent: 'foo' + }, + type: 'Feature' + } + ], + 'adds vertices' + ); +}); + +test('createSupplementaryPoints with a polygon, no midpoints', () => { + const polygon = { + type: 'Feature', + properties: { + id: 'foo' + }, + geometry: { + type: 'Polygon', + coordinates: [ + [ + [1, 1], + [2, 2], + [3, 3], + [4, 4], + [1, 1] + ] + ] + } + } as unknown as StrictFeature; + + const result = createSupplementaryPoints(polygon); + + assert.deepEqual( + result, + [ + { + geometry: { + coordinates: [1, 1], + type: 'Point' + }, + properties: { + active: 'false', + coord_path: '0.0', + meta: 'vertex', + parent: 'foo' + }, + type: 'Feature' + }, + { + geometry: { + coordinates: [2, 2], + type: 'Point' + }, + properties: { + active: 'false', + coord_path: '0.1', + meta: 'vertex', + parent: 'foo' + }, + type: 'Feature' + }, + { + geometry: { + coordinates: [3, 3], + type: 'Point' + }, + properties: { + active: 'false', + coord_path: '0.2', + meta: 'vertex', + parent: 'foo' + }, + type: 'Feature' + }, + { + geometry: { + coordinates: [4, 4], + type: 'Point' + }, + properties: { + active: 'false', + coord_path: '0.3', + meta: 'vertex', + parent: 'foo' + }, + type: 'Feature' + } + ], + 'adds vertices' + ); +}); + +test('createSupplementaryPoints with line, midpoints, selected coordinate', () => { + const line = { + type: 'Feature', + properties: { + id: 'foo' + }, + geometry: { + type: 'LineString', + coordinates: [ + [0, 0], + [4, 4], + [8, 8] + ] + } + } as unknown as StrictFeature; + + const map = createMap() as unknown as Map; + + const results = createSupplementaryPoints(line, { + map, + midpoints: true, + selectedPaths: '1' + }); + + assert.deepEqual( + results, + [ + { + geometry: { + coordinates: [0, 0], + type: 'Point' + }, + properties: { + active: 'false', + coord_path: '0', + meta: 'vertex', + parent: 'foo' + }, + type: 'Feature' + }, + { + geometry: { + coordinates: [2, 2], + type: 'Point' + }, + properties: { + coord_path: '1', + lat: 2, + lng: 2, + meta: 'midpoint', + parent: 'foo' + }, + type: 'Feature' + }, + { + geometry: { + coordinates: [4, 4], + type: 'Point' + }, + properties: { + active: 'true', + coord_path: '1', + meta: 'vertex', + parent: 'foo' + }, + type: 'Feature' + }, + { + geometry: { + coordinates: [6, 6], + type: 'Point' + }, + properties: { + coord_path: '2', + lat: 6, + lng: 6, + meta: 'midpoint', + parent: 'foo' + }, + type: 'Feature' + }, + { + geometry: { + coordinates: [8, 8], + type: 'Point' + }, + properties: { + active: 'false', + coord_path: '2', + meta: 'vertex', + parent: 'foo' + }, + type: 'Feature' + } + ], + 'adds vertices and midpoints' + ); +}); + +test('createSupplementaryPoints with polygon, midpoints, selection', () => { + const polygon = { + type: 'Feature', + properties: { + id: 'foo' + }, + geometry: { + type: 'Polygon', + coordinates: [ + [ + [1, 1], + [2, 2], + [3, 3], + [4, 4], + [1, 1] + ] + ] + } + } as unknown as StrictFeature; + + const map = createMap() as unknown as Map; + + const results = createSupplementaryPoints(polygon, { + map, + midpoints: true, + selectedPaths: '0.1' + }); + + assert.deepEqual( + results, + [ + { + geometry: { + coordinates: [1, 1], + type: 'Point' + }, + properties: { + active: 'false', + coord_path: '0.0', + meta: 'vertex', + parent: 'foo' + }, + type: 'Feature' + }, + { + geometry: { + coordinates: [1.5, 1.5], + type: 'Point' + }, + properties: { + coord_path: '0.1', + lat: 1.5, + lng: 1.5, + meta: 'midpoint', + parent: 'foo' + }, + type: 'Feature' + }, + { + geometry: { + coordinates: [2, 2], + type: 'Point' + }, + properties: { + active: 'true', + coord_path: '0.1', + meta: 'vertex', + parent: 'foo' + }, + type: 'Feature' + }, + { + geometry: { + coordinates: [2.5, 2.5], + type: 'Point' + }, + properties: { + coord_path: '0.2', + lat: 2.5, + lng: 2.5, + meta: 'midpoint', + parent: 'foo' + }, + type: 'Feature' + }, + { + geometry: { + coordinates: [3, 3], + type: 'Point' + }, + properties: { + active: 'false', + coord_path: '0.2', + meta: 'vertex', + parent: 'foo' + }, + type: 'Feature' + }, + { + geometry: { + coordinates: [3.5, 3.5], + type: 'Point' + }, + properties: { + coord_path: '0.3', + lat: 3.5, + lng: 3.5, + meta: 'midpoint', + parent: 'foo' + }, + type: 'Feature' + }, + { + geometry: { + coordinates: [4, 4], + type: 'Point' + }, + properties: { + active: 'false', + coord_path: '0.3', + meta: 'vertex', + parent: 'foo' + }, + type: 'Feature' + }, + { + geometry: { + coordinates: [2.5, 2.5], + type: 'Point' + }, + properties: { + coord_path: '0.4', + lat: 2.5, + lng: 2.5, + meta: 'midpoint', + parent: 'foo' + }, + type: 'Feature' + } + ], + 'adds vertices and midpoints' + ); +}); + +test('createSupplementaryPoints with MultiLineString, midpoints, selected coordinate', () => { + const line = { + type: 'Feature', + properties: { + id: 'foo' + }, + geometry: { + type: 'MultiLineString', + coordinates: [ + [ + [0, 0], + [4, 4], + [8, 8] + ], + [ + [20, 20], + [24, 24], + [28, 28] + ] + ] + } + } as unknown as StrictFeature; + + const map = createMap() as unknown as Map; + + const results = createSupplementaryPoints(line, { + map, + midpoints: true, + selectedPaths: '1.2' + }); + + assert.deepEqual(results, [ + { + geometry: { + coordinates: [0, 0], + type: 'Point' + }, + properties: { + active: 'false', + coord_path: '0.0', + meta: 'vertex', + parent: 'foo' + }, + type: 'Feature' + }, + { + geometry: { + coordinates: [2, 2], + type: 'Point' + }, + properties: { + coord_path: '0.1', + lat: 2, + lng: 2, + meta: 'midpoint', + parent: 'foo' + }, + type: 'Feature' + }, + { + geometry: { + coordinates: [4, 4], + type: 'Point' + }, + properties: { + active: 'false', + coord_path: '0.1', + meta: 'vertex', + parent: 'foo' + }, + type: 'Feature' + }, + { + geometry: { + coordinates: [6, 6], + type: 'Point' + }, + properties: { + coord_path: '0.2', + lat: 6, + lng: 6, + meta: 'midpoint', + parent: 'foo' + }, + type: 'Feature' + }, + { + geometry: { + coordinates: [8, 8], + type: 'Point' + }, + properties: { + active: 'false', + coord_path: '0.2', + meta: 'vertex', + parent: 'foo' + }, + type: 'Feature' + }, + { + geometry: { + coordinates: [20, 20], + type: 'Point' + }, + properties: { + active: 'false', + coord_path: '1.0', + meta: 'vertex', + parent: 'foo' + }, + type: 'Feature' + }, + { + geometry: { + coordinates: [22, 22], + type: 'Point' + }, + properties: { + coord_path: '1.1', + lat: 22, + lng: 22, + meta: 'midpoint', + parent: 'foo' + }, + type: 'Feature' + }, + { + geometry: { + coordinates: [24, 24], + type: 'Point' + }, + properties: { + active: 'false', + coord_path: '1.1', + meta: 'vertex', + parent: 'foo' + }, + type: 'Feature' + }, + { + geometry: { + coordinates: [26, 26], + type: 'Point' + }, + properties: { + coord_path: '1.2', + lat: 26, + lng: 26, + meta: 'midpoint', + parent: 'foo' + }, + type: 'Feature' + }, + { + geometry: { + coordinates: [28, 28], + type: 'Point' + }, + properties: { + active: 'true', + coord_path: '1.2', + meta: 'vertex', + parent: 'foo' + }, + type: 'Feature' + } + ]); +}); + +test('createSupplementaryPoints with a line, not all midpoints rendered because of vertex exceeding projection latitude north limit', () => { + const line = { + type: 'Feature', + properties: { + id: 'foo' + }, + geometry: { + type: 'LineString', + coordinates: [ + [0, 0], + [4, 4], + [7, 87] + ] + } + } as unknown as StrictFeature; + + const map = createMap() as unknown as Map; + + const result = createSupplementaryPoints(line, { + map, + midpoints: true + }); + + assert.deepEqual( + result, + [ + { + geometry: { + coordinates: [0, 0], + type: 'Point' + }, + properties: { + active: 'false', + coord_path: '0', + meta: 'vertex', + parent: 'foo' + }, + type: 'Feature' + }, + { + geometry: { + coordinates: [2, 2], + type: 'Point' + }, + properties: { + coord_path: '1', + lat: 2, + lng: 2, + meta: 'midpoint', + parent: 'foo' + }, + type: 'Feature' + }, + { + geometry: { + coordinates: [4, 4], + type: 'Point' + }, + properties: { + active: 'false', + coord_path: '1', + meta: 'vertex', + parent: 'foo' + }, + type: 'Feature' + }, + { + geometry: { + coordinates: [7, 87], + type: 'Point' + }, + properties: { + active: 'false', + coord_path: '2', + meta: 'vertex', + parent: 'foo' + }, + type: 'Feature' + } + ], + 'adds vertices' + ); +}); + +test('createSupplementaryPoints with a line, not all midpoints rendered because of vertex exceeding projection latitude south limit', () => { + const line = { + type: 'Feature', + properties: { + id: 'foo' + }, + geometry: { + type: 'LineString', + coordinates: [ + [0, 0], + [4, 4], + [7, -87] + ] + } + } as unknown as StrictFeature; + + const map = createMap() as unknown as Map; + + const result = createSupplementaryPoints(line, { + map, + midpoints: true + }); + + assert.deepEqual( + result, + [ + { + geometry: { + coordinates: [0, 0], + type: 'Point' + }, + properties: { + active: 'false', + coord_path: '0', + meta: 'vertex', + parent: 'foo' + }, + type: 'Feature' + }, + { + geometry: { + coordinates: [2, 2], + type: 'Point' + }, + properties: { + coord_path: '1', + lat: 2, + lng: 2, + meta: 'midpoint', + parent: 'foo' + }, + type: 'Feature' + }, + { + geometry: { + coordinates: [4, 4], + type: 'Point' + }, + properties: { + active: 'false', + coord_path: '1', + meta: 'vertex', + parent: 'foo' + }, + type: 'Feature' + }, + { + geometry: { + coordinates: [7, -87], + type: 'Point' + }, + properties: { + active: 'false', + coord_path: '2', + meta: 'vertex', + parent: 'foo' + }, + type: 'Feature' + } + ], + 'adds vertices' + ); +}); diff --git a/test/create_vertex.test.js b/test/create_vertex.test.ts similarity index 88% rename from test/create_vertex.test.js rename to test/create_vertex.test.ts index 0e0ae64f..a8cbe9b3 100644 --- a/test/create_vertex.test.js +++ b/test/create_vertex.test.ts @@ -1,6 +1,7 @@ +import './mock-browser'; import test from 'node:test'; import assert from 'node:assert/strict'; -import createVertex from '../src/lib/create_vertex.js'; +import { createVertex } from '../src/lib/create_vertex'; test('createVertex', () => { assert.deepEqual(createVertex('foo', [1, 2], '3.4.5', true), { @@ -30,6 +31,4 @@ test('createVertex', () => { coordinates: [99, 199] } }); - - }); diff --git a/test/direct_select.test.js b/test/direct_select.test.js deleted file mode 100644 index 6a6ce6d6..00000000 --- a/test/direct_select.test.js +++ /dev/null @@ -1,298 +0,0 @@ -/* eslint no-shadow:[0] */ -import test from 'node:test'; -import assert from 'node:assert/strict'; -import turfCentroid from '@turf/centroid'; -import createSyntheticEvent from 'synthetic-dom-events'; -import {spy} from 'sinon'; - -import MapboxDraw from '../index.js'; -import click from './utils/mouse_click.js'; -import tap from './utils/touch_tap.js'; -import getGeoJSON from './utils/get_geojson.js'; -import createMap from './utils/create_map.js'; -import {setupAfterNextRender} from './utils/after_next_render.js'; -import makeMouseEvent from './utils/make_mouse_event.js'; -import makeTouchEvent from './utils/make_touch_event.js'; -import * as Constants from '../src/constants.js'; - -test('direct_select', async (t) => { - const mapContainer = document.createElement('div'); - document.body.appendChild(mapContainer); - const map = createMap({ container: mapContainer }); - - const Draw = new MapboxDraw(); - map.addControl(Draw); - - spy(map, 'fire'); - - const afterNextRender = setupAfterNextRender(map); - - const cleanUp = async function() { - Draw.deleteAll(); - map.fire.resetHistory(); - await afterNextRender(); - }; - - const getFireArgs = function() { - const args = []; - for (let i = 0; i < map.fire.callCount; i++) { - args.push(map.fire.getCall(i).args); - } - return args; - }; - - t.test('direct_select - init map for tests', () => { - const done = function() { - map.off('load', done); - }; - - if (map.loaded()) { - done(); - } else { - map.on('load', done); - } - }); - - await t.test('direct_select - double click should not disable enabled dragPan', async () => { - const ids = Draw.add(getGeoJSON('polygon')); - - Draw.changeMode(Constants.modes.DIRECT_SELECT, { - featureId: ids[0] - }); - - await afterNextRender(); - - spy(map.dragPan, 'enable'); - - for (let i = 0; i < 2; i++) { - map.fire('mousedown', makeMouseEvent(35, 25)); - await afterNextRender(); - } - - map.fire('mousemove', makeMouseEvent(0, 0)); - - assert.equal(map.dragPan.enable.callCount, 1, 'dragPan.enable called'); - - map.dragPan.enable.restore(); - }); - - - await t.test('direct_select - should fire correct actionable when no vertices selected', async () => { - const ids = Draw.add(getGeoJSON('polygon')); - Draw.changeMode(Constants.modes.SIMPLE_SELECT, { - featureIds: ids - }); - - await afterNextRender(); - - Draw.changeMode(Constants.modes.DIRECT_SELECT, { - featureId: ids[0] - }); - - await afterNextRender(); - - const actionableArgs = getFireArgs().filter(arg => arg[0] === 'draw.actionable'); - assert.ok(actionableArgs.length > 0, 'should have fired an actionable event'); - if (actionableArgs.length > 0) { - const actionable = actionableArgs[actionableArgs.length - 1][1]; - assert.equal(actionable.actions.combineFeatures, false, 'should fire correct combine actionable'); - assert.equal(actionable.actions.uncombineFeatures, false, 'should fire correct uncombine actionable'); - assert.equal(actionable.actions.trash, false, 'should fire correct trash actionable'); - } - - await cleanUp(); - }); - - await t.test('direct_select - should fire correct actionable when a vertex is selected by clicking', async () => { - const ids = Draw.add(getGeoJSON('polygon')); - Draw.changeMode(Constants.modes.DIRECT_SELECT, { - featureId: ids[0] - }); - const clickAt = getGeoJSON('polygon').geometry.coordinates[0][0]; - await afterNextRender(); - - click(map, makeMouseEvent(clickAt[0], clickAt[1])); - - await afterNextRender(); - const actionableArgs = getFireArgs().filter(arg => arg[0] === 'draw.actionable'); - assert.ok(actionableArgs.length > 0, 'should have fired an actionable event'); - if (actionableArgs.length > 0) { - const actionable = actionableArgs[actionableArgs.length - 1][1]; - assert.equal(actionable.actions.combineFeatures, false, 'should fire correct combine actionable'); - assert.equal(actionable.actions.uncombineFeatures, false, 'should fire correct uncombine actionable'); - assert.equal(actionable.actions.trash, true, 'should fire correct trash actionable'); - } - - await cleanUp(); - }); - - await t.test('direct_select - should fire correct actionable when a vertex is selected by tapping', async () => { - const ids = Draw.add(getGeoJSON('polygon')); - Draw.changeMode(Constants.modes.DIRECT_SELECT, { - featureId: ids[0] - }); - const tapAt = getGeoJSON('polygon').geometry.coordinates[0][0]; - await afterNextRender(); - - tap(map, makeTouchEvent(tapAt[0], tapAt[1])); - await afterNextRender(); - - const actionableArgs = getFireArgs().filter(arg => arg[0] === 'draw.actionable'); - assert.ok(actionableArgs.length > 0, 'should have fired an actionable event'); - if (actionableArgs.length > 0) { - const actionable = actionableArgs[actionableArgs.length - 1][1]; - assert.equal(actionable.actions.combineFeatures, false, 'should fire correct combine actionable'); - assert.equal(actionable.actions.uncombineFeatures, false, 'should fire correct uncombine actionable'); - assert.equal(actionable.actions.trash, true, 'should fire correct trash actionable'); - } - - await cleanUp(); - }); - - await t.test('direct_select - trashing vertices should delete the correct ones', async () => { - const longLine = { - type: 'Feature', - properties: {}, - geometry: { - type: 'LineString', - coordinates: [[0, 0], [10, 0], [20, 0], [30, 0], [40, 0], [50, 0], [60, 0], [70, 0], [80, 0], [80, 10], [70, 10], [60, 10], [50, 10]] - } - }; - const ids = Draw.add(longLine); - Draw.changeMode(Constants.modes.DIRECT_SELECT, { - featureId: ids[0] - }); - - await afterNextRender(); - // select multiple nodes at indices 9, 10, 11 - click(map, makeMouseEvent(70, 10, { shiftKey: true })); - click(map, makeMouseEvent(80, 10, { shiftKey: true })); - click(map, makeMouseEvent(60, 10, { shiftKey: true })); - await afterNextRender(); - Draw.trash(); - const afterTrash = Draw.get(ids[0]); - assert.deepEqual(afterTrash.geometry.coordinates, [[0, 0], [10, 0], [20, 0], [30, 0], [40, 0], [50, 0], [60, 0], [70, 0], [80, 0], [50, 10]]); - await cleanUp(); - }); - - await t.test('direct_select - a click on a vertex and than dragging the map shouldn\'t drag the vertex', async () => { - const ids = Draw.add(getGeoJSON('polygon')); - Draw.changeMode(Constants.modes.DIRECT_SELECT, { - featureId: ids[0] - }); - - const clickAt = getGeoJSON('polygon').geometry.coordinates[0][0]; - await afterNextRender(); - click(map, makeMouseEvent(clickAt[0], clickAt[1])); - await afterNextRender(); - map.fire('mousedown', makeMouseEvent(clickAt[0] + 15, clickAt[1] + 15)); - map.fire('mousemove', makeMouseEvent(clickAt[0] + 30, clickAt[1] + 30, { buttons: 1 })); - map.fire('mouseup', makeMouseEvent(clickAt[0] + 30, clickAt[1] + 30)); - const afterMove = Draw.get(ids[0]); - assert.deepEqual(getGeoJSON('polygon').geometry, afterMove.geometry, 'should be the same after the drag'); - await cleanUp(); - }); - - await t.test('direct_select - fire one update when dragging mouse leaves container and button is released outside', async () => { - const ids = Draw.add(getGeoJSON('polygon')); - Draw.changeMode(Constants.modes.DIRECT_SELECT, { - featureId: ids[0] - }); - - const startPosition = getGeoJSON('polygon').geometry.coordinates[0][1]; - await afterNextRender(); - click(map, makeMouseEvent(startPosition[0], startPosition[1])); - await afterNextRender(); - map.fire.resetHistory(); - map.fire('mousedown', makeMouseEvent(startPosition[0], startPosition[1])); - map.fire('mousemove', makeMouseEvent(startPosition[0] + 15, startPosition[1] + 15, { buttons: 1 })); - mapContainer.dispatchEvent(createSyntheticEvent('mouseout')); - map.fire('mousemove', makeMouseEvent(startPosition[0] + 30, startPosition[1] + 30), { buttons: 1 }); - map.fire('mouseup', makeMouseEvent(startPosition[0] + 30, startPosition[1] + 30)); - - const afterMove = Draw.get(ids[0]); - const args = getFireArgs().filter(arg => arg[0] === 'draw.update'); - assert.equal(args.length, 1, 'draw.update called once'); - assert.equal(afterMove.geometry.coordinates[0][1][0], startPosition[0] + 15, 'point lng moved only the first amount'); - assert.equal(afterMove.geometry.coordinates[0][1][1], startPosition[1] + 15, 'point lat moved only the first amount'); - - await cleanUp(); - }); - - await t.test('direct_select - fire two updates when dragging mouse leaves container then returns and button is released inside', async () => { - const ids = Draw.add(getGeoJSON('polygon')); - Draw.changeMode(Constants.modes.DIRECT_SELECT, { - featureId: ids[0] - }); - - const startPosition = getGeoJSON('polygon').geometry.coordinates[0][1]; - await afterNextRender(); - click(map, makeMouseEvent(startPosition[0], startPosition[1])); - await afterNextRender(); - map.fire.resetHistory(); - map.fire('mousedown', makeMouseEvent(startPosition[0], startPosition[1])); - map.fire('mousemove', makeMouseEvent(startPosition[0] + 15, startPosition[1] + 15, { buttons: 1 })); - mapContainer.dispatchEvent(createSyntheticEvent('mouseout')); - map.fire('mousemove', makeMouseEvent(startPosition[0] + 30, startPosition[1] + 30, { buttons: 1 })); - map.fire('mouseup', makeMouseEvent(startPosition[0] + 30, startPosition[1] + 30)); - - const afterMove = Draw.get(ids[0]); - const args = getFireArgs().filter(arg => arg[0] === 'draw.update'); - assert.equal(args.length, 2, 'draw.update called twice'); - assert.equal(afterMove.geometry.coordinates[0][1][0], startPosition[0] + 30, 'point lng moved to the mouseup location'); - assert.equal(afterMove.geometry.coordinates[0][1][1], startPosition[1] + 30, 'point lat moved to the mouseup location'); - - await cleanUp(); - }); - - await t.test('direct_select - drag feature if no vertices are selected', async () => { - const [polygonId] = Draw.add(getGeoJSON('polygon')); - Draw.changeMode(Constants.modes.DIRECT_SELECT, { - featureId: polygonId - }); - - const startPosition = getGeoJSON('polygon').geometry.coordinates[0][1]; - const centroid = turfCentroid(getGeoJSON('polygon')).geometry.coordinates; - await afterNextRender(); - map.fire.resetHistory(); - click(map, makeMouseEvent(centroid[0], centroid[1])); - map.fire('mousedown', makeMouseEvent(centroid[0], centroid[1])); - map.fire('mousemove', makeMouseEvent(centroid[0] + 15, centroid[1] + 15, { buttons: 1 })); - map.fire('mouseup', makeMouseEvent(centroid[0] + 15, centroid[1] + 15)); - - const afterMove = Draw.get(polygonId); - const args = getFireArgs().filter(arg => arg[0] === 'draw.update'); - assert.equal(args.length, 1, 'draw.update called once'); - assert.equal(afterMove.geometry.coordinates[0][1][0], startPosition[0] + 15, 'point lng moved to the mouseup location'); - assert.equal(afterMove.geometry.coordinates[0][1][1], startPosition[1] + 15, 'point lat moved to the mouseup location'); - - await cleanUp(); - }); - - await t.test('direct_select - dragging a selected vertex updates stored coordinates', async () => { - const [lineId] = Draw.add(getGeoJSON('line')); - Draw.changeMode(Constants.modes.DIRECT_SELECT, { - featureId: lineId - }); - - assert.equal(Draw.getSelectedPoints().features[0], undefined, 'no initial selection'); - - const startPosition = getGeoJSON('line').geometry.coordinates[0]; - const endPosition = [startPosition[0] + 10, startPosition[1] + 10]; - await afterNextRender(); - - map.fire.resetHistory(); - click(map, makeMouseEvent(startPosition[0], startPosition[1])); - assert.deepEqual(Draw.getSelectedPoints().features[0].geometry.coordinates, startPosition, 'click saves selection'); - - map.fire('mousedown', makeMouseEvent(startPosition[0], startPosition[1])); - map.fire('mousemove', makeMouseEvent(endPosition[0], endPosition[1], { buttons: 1 })); - map.fire('mouseup', makeMouseEvent(endPosition[0], endPosition[1])); - await afterNextRender(); - - assert.deepEqual(Draw.getSelectedPoints().features[0].geometry.coordinates, endPosition, 'selection is accurate after dragging'); - await cleanUp(); - }); - - document.body.removeChild(mapContainer); -}); diff --git a/test/direct_select.test.ts b/test/direct_select.test.ts new file mode 100644 index 00000000..f83ebb04 --- /dev/null +++ b/test/direct_select.test.ts @@ -0,0 +1,478 @@ +/* eslint no-shadow:[0] */ +import './mock-browser'; +import test from 'node:test'; +import assert from 'node:assert/strict'; +import turfCentroid from '@turf/centroid'; +import createSyntheticEvent from 'synthetic-dom-events'; +import { spy } from 'sinon'; + +import MapboxDraw from '../index'; +import click from './utils/mouse_click'; +import tap from './utils/touch_tap'; +import getGeoJSON from './utils/get_geojson'; +import createMap from './utils/create_map'; +import { setupAfterNextRender } from './utils/after_next_render'; +import makeMouseEvent from './utils/make_mouse_event'; +import makeTouchEvent from './utils/make_touch_event'; +import * as Constants from '../src/constants'; + +test('direct_select', async t => { + const mapContainer = document.createElement('div'); + document.body.appendChild(mapContainer); + const map = createMap({ container: mapContainer }) as spy; + + const Draw = new MapboxDraw(); + map.addControl(Draw); + + spy(map, 'fire'); + + const afterNextRender = setupAfterNextRender(map); + + const cleanUp = async function () { + Draw.deleteAll(); + map.fire.resetHistory(); + await afterNextRender(); + }; + + const getFireArgs = function () { + const args = []; + for (let i = 0; i < map.fire.callCount; i++) { + args.push(map.fire.getCall(i).args); + } + return args; + }; + + t.test('direct_select - init map for tests', () => { + const done = function () { + map.off('load', done); + }; + + if (map.loaded()) { + done(); + } else { + map.on('load', done); + } + }); + + await t.test( + 'direct_select - double click should not disable enabled dragPan', + async () => { + const ids = Draw.add(getGeoJSON('polygon')); + + Draw.changeMode(Constants.modes.DIRECT_SELECT, { + featureId: ids[0] + }); + + await afterNextRender(); + + spy(map.dragPan, 'enable'); + + for (let i = 0; i < 2; i++) { + map.fire('mousedown', makeMouseEvent(35, 25)); + await afterNextRender(); + } + + map.fire('mousemove', makeMouseEvent(0, 0)); + + assert.equal(map.dragPan.enable.callCount, 1, 'dragPan.enable called'); + + map.dragPan.enable.restore(); + } + ); + + await t.test( + 'direct_select - should fire correct actionable when no vertices selected', + async () => { + const ids = Draw.add(getGeoJSON('polygon')); + Draw.changeMode(Constants.modes.SIMPLE_SELECT, { + featureIds: ids + }); + + await afterNextRender(); + + Draw.changeMode(Constants.modes.DIRECT_SELECT, { + featureId: ids[0] + }); + + await afterNextRender(); + + const actionableArgs = getFireArgs().filter( + arg => arg[0] === 'draw.actionable' + ); + assert.ok( + actionableArgs.length > 0, + 'should have fired an actionable event' + ); + if (actionableArgs.length > 0) { + const actionable = actionableArgs[actionableArgs.length - 1][1]; + assert.equal( + actionable.actions.combineFeatures, + false, + 'should fire correct combine actionable' + ); + assert.equal( + actionable.actions.uncombineFeatures, + false, + 'should fire correct uncombine actionable' + ); + assert.equal( + actionable.actions.trash, + false, + 'should fire correct trash actionable' + ); + } + + await cleanUp(); + } + ); + + await t.test( + 'direct_select - should fire correct actionable when a vertex is selected by clicking', + async () => { + const ids = Draw.add(getGeoJSON('polygon')); + Draw.changeMode(Constants.modes.DIRECT_SELECT, { + featureId: ids[0] + }); + const clickAt = getGeoJSON('polygon').geometry.coordinates[0][0]; + await afterNextRender(); + + click(map, makeMouseEvent(clickAt[0], clickAt[1])); + + await afterNextRender(); + const actionableArgs = getFireArgs().filter( + arg => arg[0] === 'draw.actionable' + ); + assert.ok( + actionableArgs.length > 0, + 'should have fired an actionable event' + ); + if (actionableArgs.length > 0) { + const actionable = actionableArgs[actionableArgs.length - 1][1]; + assert.equal( + actionable.actions.combineFeatures, + false, + 'should fire correct combine actionable' + ); + assert.equal( + actionable.actions.uncombineFeatures, + false, + 'should fire correct uncombine actionable' + ); + assert.equal( + actionable.actions.trash, + true, + 'should fire correct trash actionable' + ); + } + + await cleanUp(); + } + ); + + await t.test( + 'direct_select - should fire correct actionable when a vertex is selected by tapping', + async () => { + const ids = Draw.add(getGeoJSON('polygon')); + Draw.changeMode(Constants.modes.DIRECT_SELECT, { + featureId: ids[0] + }); + const tapAt = getGeoJSON('polygon').geometry.coordinates[0][0]; + await afterNextRender(); + + tap(map, makeTouchEvent(tapAt[0], tapAt[1])); + await afterNextRender(); + + const actionableArgs = getFireArgs().filter( + arg => arg[0] === 'draw.actionable' + ); + assert.ok( + actionableArgs.length > 0, + 'should have fired an actionable event' + ); + if (actionableArgs.length > 0) { + const actionable = actionableArgs[actionableArgs.length - 1][1]; + assert.equal( + actionable.actions.combineFeatures, + false, + 'should fire correct combine actionable' + ); + assert.equal( + actionable.actions.uncombineFeatures, + false, + 'should fire correct uncombine actionable' + ); + assert.equal( + actionable.actions.trash, + true, + 'should fire correct trash actionable' + ); + } + + await cleanUp(); + } + ); + + await t.test( + 'direct_select - trashing vertices should delete the correct ones', + async () => { + const longLine = { + type: 'Feature', + properties: {}, + geometry: { + type: 'LineString', + coordinates: [ + [0, 0], + [10, 0], + [20, 0], + [30, 0], + [40, 0], + [50, 0], + [60, 0], + [70, 0], + [80, 0], + [80, 10], + [70, 10], + [60, 10], + [50, 10] + ] + } + }; + const ids = Draw.add(longLine); + Draw.changeMode(Constants.modes.DIRECT_SELECT, { + featureId: ids[0] + }); + + await afterNextRender(); + // select multiple nodes at indices 9, 10, 11 + click(map, makeMouseEvent(70, 10, { shiftKey: true })); + click(map, makeMouseEvent(80, 10, { shiftKey: true })); + click(map, makeMouseEvent(60, 10, { shiftKey: true })); + await afterNextRender(); + Draw.trash(); + const afterTrash = Draw.get(ids[0]); + assert.deepEqual(afterTrash.geometry.coordinates, [ + [0, 0], + [10, 0], + [20, 0], + [30, 0], + [40, 0], + [50, 0], + [60, 0], + [70, 0], + [80, 0], + [50, 10] + ]); + await cleanUp(); + } + ); + + await t.test( + "direct_select - a click on a vertex and than dragging the map shouldn't drag the vertex", + async () => { + const ids = Draw.add(getGeoJSON('polygon')); + Draw.changeMode(Constants.modes.DIRECT_SELECT, { + featureId: ids[0] + }); + + const clickAt = getGeoJSON('polygon').geometry.coordinates[0][0]; + await afterNextRender(); + click(map, makeMouseEvent(clickAt[0], clickAt[1])); + await afterNextRender(); + map.fire('mousedown', makeMouseEvent(clickAt[0] + 15, clickAt[1] + 15)); + map.fire( + 'mousemove', + makeMouseEvent(clickAt[0] + 30, clickAt[1] + 30, { buttons: 1 }) + ); + map.fire('mouseup', makeMouseEvent(clickAt[0] + 30, clickAt[1] + 30)); + const afterMove = Draw.get(ids[0]); + assert.deepEqual( + getGeoJSON('polygon').geometry, + afterMove.geometry, + 'should be the same after the drag' + ); + await cleanUp(); + } + ); + + await t.test( + 'direct_select - fire one update when dragging mouse leaves container and button is released outside', + async () => { + const ids = Draw.add(getGeoJSON('polygon')); + Draw.changeMode(Constants.modes.DIRECT_SELECT, { + featureId: ids[0] + }); + + const startPosition = getGeoJSON('polygon').geometry.coordinates[0][1]; + await afterNextRender(); + click(map, makeMouseEvent(startPosition[0], startPosition[1])); + await afterNextRender(); + map.fire.resetHistory(); + map.fire('mousedown', makeMouseEvent(startPosition[0], startPosition[1])); + map.fire( + 'mousemove', + makeMouseEvent(startPosition[0] + 15, startPosition[1] + 15, { + buttons: 1 + }) + ); + mapContainer.dispatchEvent(createSyntheticEvent('mouseout')); + map.fire( + 'mousemove', + makeMouseEvent(startPosition[0] + 30, startPosition[1] + 30), + { buttons: 1 } + ); + map.fire( + 'mouseup', + makeMouseEvent(startPosition[0] + 30, startPosition[1] + 30) + ); + + const afterMove = Draw.get(ids[0]); + const args = getFireArgs().filter(arg => arg[0] === 'draw.update'); + assert.equal(args.length, 1, 'draw.update called once'); + assert.equal( + afterMove.geometry.coordinates[0][1][0], + startPosition[0] + 15, + 'point lng moved only the first amount' + ); + assert.equal( + afterMove.geometry.coordinates[0][1][1], + startPosition[1] + 15, + 'point lat moved only the first amount' + ); + + await cleanUp(); + } + ); + + await t.test( + 'direct_select - fire two updates when dragging mouse leaves container then returns and button is released inside', + async () => { + const ids = Draw.add(getGeoJSON('polygon')); + Draw.changeMode(Constants.modes.DIRECT_SELECT, { + featureId: ids[0] + }); + + const startPosition = getGeoJSON('polygon').geometry.coordinates[0][1]; + await afterNextRender(); + click(map, makeMouseEvent(startPosition[0], startPosition[1])); + await afterNextRender(); + map.fire.resetHistory(); + map.fire('mousedown', makeMouseEvent(startPosition[0], startPosition[1])); + map.fire( + 'mousemove', + makeMouseEvent(startPosition[0] + 15, startPosition[1] + 15, { + buttons: 1 + }) + ); + mapContainer.dispatchEvent(createSyntheticEvent('mouseout')); + map.fire( + 'mousemove', + makeMouseEvent(startPosition[0] + 30, startPosition[1] + 30, { + buttons: 1 + }) + ); + map.fire( + 'mouseup', + makeMouseEvent(startPosition[0] + 30, startPosition[1] + 30) + ); + + const afterMove = Draw.get(ids[0]); + const args = getFireArgs().filter(arg => arg[0] === 'draw.update'); + assert.equal(args.length, 2, 'draw.update called twice'); + assert.equal( + afterMove.geometry.coordinates[0][1][0], + startPosition[0] + 30, + 'point lng moved to the mouseup location' + ); + assert.equal( + afterMove.geometry.coordinates[0][1][1], + startPosition[1] + 30, + 'point lat moved to the mouseup location' + ); + + await cleanUp(); + } + ); + + await t.test( + 'direct_select - drag feature if no vertices are selected', + async () => { + const [polygonId] = Draw.add(getGeoJSON('polygon')); + Draw.changeMode(Constants.modes.DIRECT_SELECT, { + featureId: polygonId + }); + + const startPosition = getGeoJSON('polygon').geometry.coordinates[0][1]; + const centroid = turfCentroid(getGeoJSON('polygon')).geometry.coordinates; + await afterNextRender(); + map.fire.resetHistory(); + click(map, makeMouseEvent(centroid[0], centroid[1])); + map.fire('mousedown', makeMouseEvent(centroid[0], centroid[1])); + map.fire( + 'mousemove', + makeMouseEvent(centroid[0] + 15, centroid[1] + 15, { buttons: 1 }) + ); + map.fire('mouseup', makeMouseEvent(centroid[0] + 15, centroid[1] + 15)); + + const afterMove = Draw.get(polygonId); + const args = getFireArgs().filter(arg => arg[0] === 'draw.update'); + assert.equal(args.length, 1, 'draw.update called once'); + assert.equal( + afterMove.geometry.coordinates[0][1][0], + startPosition[0] + 15, + 'point lng moved to the mouseup location' + ); + assert.equal( + afterMove.geometry.coordinates[0][1][1], + startPosition[1] + 15, + 'point lat moved to the mouseup location' + ); + + await cleanUp(); + } + ); + + await t.test( + 'direct_select - dragging a selected vertex updates stored coordinates', + async () => { + const [lineId] = Draw.add(getGeoJSON('line')); + Draw.changeMode(Constants.modes.DIRECT_SELECT, { + featureId: lineId + }); + + assert.equal( + Draw.getSelectedPoints().features[0], + undefined, + 'no initial selection' + ); + + const startPosition = getGeoJSON('line').geometry.coordinates[0]; + const endPosition = [startPosition[0] + 10, startPosition[1] + 10]; + await afterNextRender(); + + map.fire.resetHistory(); + click(map, makeMouseEvent(startPosition[0], startPosition[1])); + assert.deepEqual( + Draw.getSelectedPoints().features[0].geometry.coordinates, + startPosition, + 'click saves selection' + ); + + map.fire('mousedown', makeMouseEvent(startPosition[0], startPosition[1])); + map.fire( + 'mousemove', + makeMouseEvent(endPosition[0], endPosition[1], { buttons: 1 }) + ); + map.fire('mouseup', makeMouseEvent(endPosition[0], endPosition[1])); + await afterNextRender(); + + assert.deepEqual( + Draw.getSelectedPoints().features[0].geometry.coordinates, + endPosition, + 'selection is accurate after dragging' + ); + await cleanUp(); + } + ); + + document.body.removeChild(mapContainer); +}); diff --git a/test/draw_line_string.test.js b/test/draw_line_string.test.ts similarity index 56% rename from test/draw_line_string.test.js rename to test/draw_line_string.test.ts index ee0a1fcf..fea38b6d 100644 --- a/test/draw_line_string.test.js +++ b/test/draw_line_string.test.ts @@ -1,17 +1,19 @@ +import './mock-browser'; +import { spy } from 'sinon'; import test from 'node:test'; import assert from 'node:assert/strict'; -import MapboxDraw from '../index.js'; -import mouseClick from './utils/mouse_click.js'; -import touchTap from './utils/touch_tap.js'; -import createMap from './utils/create_map.js'; -import makeMouseEvent from './utils/make_mouse_event.js'; -import makeTouchEvent from './utils/make_touch_event.js'; -import drawLineStringModeObject from '../src/modes/draw_line_string.js'; -import LineString from '../src/feature_types/line_string.js'; -import createMockDrawModeContext from './utils/create_mock_draw_mode_context.js'; -import createMockLifecycleContext from './utils/create_mock_lifecycle_context.js'; -import objectToMode from '../src/modes/object_to_mode.js'; -import {setupAfterNextRender} from './utils/after_next_render.js'; +import MapboxDraw from '../index'; +import mouseClick from './utils/mouse_click'; +import touchTap from './utils/touch_tap'; +import createMap from './utils/create_map'; +import makeMouseEvent from './utils/make_mouse_event'; +import makeTouchEvent from './utils/make_touch_event'; +import drawLineStringModeObject from '../src/modes/draw_line_string'; +import LineString from '../src/feature_types/line_string'; +import createMockDrawModeContext from './utils/create_mock_draw_mode_context'; +import createMockLifecycleContext from './utils/create_mock_lifecycle_context'; +import { objectToMode } from '../src/modes/object_to_mode'; +import { setupAfterNextRender } from './utils/after_next_render'; const drawLineStringMode = objectToMode(drawLineStringModeObject); @@ -21,11 +23,13 @@ import { startLineStringEvent, startPolygonEvent, escapeEvent -} from './utils/key_events.js'; +} from './utils/key_events'; + +import type { StrictFeature } from '../src/types/types'; test('draw_line_string mode initialization', () => { - const context = createMockDrawModeContext(); - const mode = drawLineStringMode(context); + const context = createMockDrawModeContext() as spy; + const mode = drawLineStringMode((context)); const lifecycleContext = createMockLifecycleContext(); mode.start.call(lifecycleContext); @@ -40,32 +44,52 @@ test('draw_line_string mode initialization', () => { } }); // Strip ids for this comparison - assert.deepEqual(Object.assign({}, context.store.add.getCall(0).args[0], { id: null }), - Object.assign({}, emptyLine, { id: null }), 'with a new line'); + assert.deepEqual( + Object.assign({}, context.store.add.getCall(0).args[0], { id: null }), + Object.assign({}, emptyLine, { id: null }), + 'with a new line' + ); }); test('draw_line_string start', () => { - const context = createMockDrawModeContext(); + const context = createMockDrawModeContext() as spy; const mode = drawLineStringMode(context); const lifecycleContext = createMockLifecycleContext(); mode.start.call(lifecycleContext); - assert.equal(context.store.clearSelected.callCount, 1, 'store.clearSelected called'); - assert.equal(context.ui.queueMapClasses.callCount, 1, 'ui.queueMapClasses called'); - assert.deepEqual(context.ui.queueMapClasses.getCall(0).args, [{ mouse: 'add' }], - 'ui.queueMapClasses received correct arguments'); - assert.equal(context.ui.setActiveButton.callCount, 1, 'ui.setActiveButton called'); - assert.deepEqual(context.ui.setActiveButton.getCall(0).args, ['line_string'], - 'ui.setActiveButton received correct arguments'); + assert.equal( + context.store.clearSelected.callCount, + 1, + 'store.clearSelected called' + ); + assert.equal( + context.ui.queueMapClasses.callCount, + 1, + 'ui.queueMapClasses called' + ); + assert.deepEqual( + context.ui.queueMapClasses.getCall(0).args, + [{ mouse: 'add' }], + 'ui.queueMapClasses received correct arguments' + ); + assert.equal( + context.ui.setActiveButton.callCount, + 1, + 'ui.setActiveButton called' + ); + assert.deepEqual( + context.ui.setActiveButton.getCall(0).args, + ['line_string'], + 'ui.setActiveButton received correct arguments' + ); setTimeout(() => { assert.equal(context.map.doubleClickZoom.disable.callCount, 1); - }, 10); }); test('draw_line_string stop with valid line', () => { - const context = createMockDrawModeContext(); + const context = createMockDrawModeContext() as spy; const mode = drawLineStringMode(context); const lifecycleContext = createMockLifecycleContext(); mode.start.call(lifecycleContext); @@ -74,17 +98,22 @@ test('draw_line_string stop with valid line', () => { const line = context.store.get(context.store.getAllIds()[0]); line.isValid = () => true; - mode.stop.call(); - assert.equal(context.ui.setActiveButton.callCount, 2, 'ui.setActiveButton called'); - assert.deepEqual(context.ui.setActiveButton.getCall(1).args, [undefined], - 'ui.setActiveButton received correct arguments'); + (mode.stop as spy).call(); + assert.equal( + context.ui.setActiveButton.callCount, + 2, + 'ui.setActiveButton called' + ); + assert.deepEqual( + context.ui.setActiveButton.getCall(1).args, + [undefined], + 'ui.setActiveButton received correct arguments' + ); assert.equal(context.store.delete.callCount, 0, 'store.delete not called'); - - }); test('draw_line_string stop with invalid line', () => { - const context = createMockDrawModeContext(); + const context = createMockDrawModeContext() as spy; const mode = drawLineStringMode(context); const lifecycleContext = createMockLifecycleContext(); mode.start.call(lifecycleContext); @@ -93,23 +122,29 @@ test('draw_line_string stop with invalid line', () => { const line = context.store.get(context.store.getAllIds()[0]); line.isValid = () => false; - mode.stop.call(); - assert.equal(context.ui.setActiveButton.callCount, 2, 'ui.setActiveButton called'); - assert.deepEqual(context.ui.setActiveButton.getCall(1).args, [undefined], - 'ui.setActiveButton received correct arguments'); + (mode.stop as spy).call(); + assert.equal( + context.ui.setActiveButton.callCount, + 2, + 'ui.setActiveButton called' + ); + assert.deepEqual( + context.ui.setActiveButton.getCall(1).args, + [undefined], + 'ui.setActiveButton received correct arguments' + ); assert.equal(context.store.delete.callCount, 1, 'store.delete called'); if (context.store.delete.callCount > 0) { - assert.deepEqual(context.store.delete.getCall(0).args, [ - [line.id], - { silent: true } - ], 'store.delete received correct arguments'); + assert.deepEqual( + context.store.delete.getCall(0).args, + [[line.id], { silent: true }], + 'store.delete received correct arguments' + ); } - - }); test('draw_line_string render active line with 0 coordinates', () => { - const context = createMockDrawModeContext(); + const context = createMockDrawModeContext() as spy; const mode = drawLineStringMode(context); const lifecycleContext = createMockLifecycleContext(); mode.start.call(lifecycleContext); @@ -127,13 +162,12 @@ test('draw_line_string render active line with 0 coordinates', () => { coordinates: [] } }; - mode.render(geojson, x => memo.push(x)); + mode.render(geojson as StrictFeature, x => memo.push(x)); assert.equal(memo.length, 0, 'does not render'); - }); test('draw_line_string render active line with 1 coordinate', () => { - const context = createMockDrawModeContext(); + const context = createMockDrawModeContext() as spy; const mode = drawLineStringMode(context); const lifecycleContext = createMockLifecycleContext(); mode.start.call(lifecycleContext); @@ -151,13 +185,12 @@ test('draw_line_string render active line with 1 coordinate', () => { coordinates: [[0, 0]] } }; - mode.render(geojson, x => memo.push(x)); + mode.render(geojson as StrictFeature, x => memo.push(x)); assert.equal(memo.length, 0, 'does not render'); - }); test('draw_line_string render active line with 2 coordinates', () => { - const context = createMockDrawModeContext(); + const context = createMockDrawModeContext() as spy; const mode = drawLineStringMode(context); const lifecycleContext = createMockLifecycleContext(); mode.start.call(lifecycleContext); @@ -172,28 +205,37 @@ test('draw_line_string render active line with 2 coordinates', () => { }, geometry: { type: 'LineString', - coordinates: [[0, 0], [10, 10]] + coordinates: [ + [0, 0], + [10, 10] + ] } }; - mode.render(geojson, x => memo.push(x)); + mode.render(geojson as StrictFeature, x => memo.push(x)); assert.equal(memo.length, 2, 'does render'); - assert.deepEqual(memo[1], { - type: 'Feature', - properties: { - id: line.id, - active: 'true', - meta: 'feature' + assert.deepEqual( + memo[1], + { + type: 'Feature', + properties: { + id: line.id, + active: 'true', + meta: 'feature' + }, + geometry: { + type: 'LineString', + coordinates: [ + [0, 0], + [10, 10] + ] + } }, - geometry: { - type: 'LineString', - coordinates: [[0, 0], [10, 10]] - } - }, 'with active: true, meta: feature'); - + 'with active: true, meta: feature' + ); }); test('draw_line_string render inactive feature', () => { - const context = createMockDrawModeContext(); + const context = createMockDrawModeContext() as unknown as spy; const mode = drawLineStringMode(context); const lifecycleContext = createMockLifecycleContext(); mode.start.call(lifecycleContext); @@ -209,23 +251,26 @@ test('draw_line_string render inactive feature', () => { coordinates: [0, 0] } }; - mode.render(geojson, x => memo.push(x)); + mode.render(geojson as StrictFeature, x => memo.push(x)); assert.equal(memo.length, 1, 'does render'); - assert.deepEqual(memo[0], { - type: 'Feature', - properties: { - active: 'false', - meta: 'nothing' + assert.deepEqual( + memo[0], + { + type: 'Feature', + properties: { + active: 'false', + meta: 'nothing' + }, + geometry: { + type: 'Point', + coordinates: [0, 0] + } }, - geometry: { - type: 'Point', - coordinates: [0, 0] - } - }, 'unaltered except active: false'); - + 'unaltered except active: false' + ); }); -test('draw_line_string mouse interaction', async (t) => { +test('draw_line_string mouse interaction', async t => { const container = document.createElement('div'); document.body.appendChild(container); const map = createMap({ container }); @@ -246,25 +291,54 @@ test('draw_line_string mouse interaction', async (t) => { const line = Draw.getAll().features[0]; assert.equal(line.geometry.type, 'LineString'); - assert.deepEqual(line.geometry.coordinates, [[10, 20], [10, 20]], 'starting coordinate added'); + assert.deepEqual( + line.geometry.coordinates, + [ + [10, 20], + [10, 20] + ], + 'starting coordinate added' + ); }); t.test('move mouse', () => { map.fire('mousemove', makeMouseEvent(15, 23)); const line = Draw.getAll().features[0]; - assert.deepEqual(line.geometry.coordinates, [[10, 20], [15, 23]], 'last coordinate added'); + assert.deepEqual( + line.geometry.coordinates, + [ + [10, 20], + [15, 23] + ], + 'last coordinate added' + ); }); t.test('move mouse again', () => { map.fire('mousemove', makeMouseEvent(30, 33)); const line = Draw.getAll().features[0]; - assert.deepEqual(line.geometry.coordinates, [[10, 20], [30, 33]], 'last coordinate replaced'); + assert.deepEqual( + line.geometry.coordinates, + [ + [10, 20], + [30, 33] + ], + 'last coordinate replaced' + ); }); t.test('click to add another vertex', () => { mouseClick(map, makeMouseEvent(35, 35)); const line = Draw.getAll().features[0]; - assert.deepEqual(line.geometry.coordinates, [[10, 20], [35, 35], [35, 35]], 'last coordinate replaced'); + assert.deepEqual( + line.geometry.coordinates, + [ + [10, 20], + [35, 35], + [35, 35] + ], + 'last coordinate replaced' + ); }); t.test('add more points then click on the last vertex to finish', () => { @@ -273,14 +347,30 @@ test('draw_line_string mouse interaction', async (t) => { mouseClick(map, makeMouseEvent(55, 55)); mouseClick(map, makeMouseEvent(55, 55)); const line = Draw.getAll().features[0]; - assert.deepEqual(line.geometry.coordinates, - [[10, 20], [35, 35], [40, 40], [50, 50], [55, 55]], - 'all coordinates in place'); + assert.deepEqual( + line.geometry.coordinates, + [ + [10, 20], + [35, 35], + [40, 40], + [50, 50], + [55, 55] + ], + 'all coordinates in place' + ); mouseClick(map, makeMouseEvent(40, 40)); - assert.deepEqual(line.geometry.coordinates, - [[10, 20], [35, 35], [40, 40], [50, 50], [55, 55]], - 'since we exited draw_line_string mode, another click does not add a coordinate'); + assert.deepEqual( + line.geometry.coordinates, + [ + [10, 20], + [35, 35], + [40, 40], + [50, 50], + [55, 55] + ], + 'since we exited draw_line_string mode, another click does not add a coordinate' + ); }); await t.test('start a line but trash it before completion', () => { @@ -292,7 +382,12 @@ test('draw_line_string mouse interaction', async (t) => { mouseClick(map, makeMouseEvent(3, 3)); const line = Draw.getAll().features[0]; - assert.deepEqual(line.geometry.coordinates, [[1, 1], [2, 2], [3, 3], [3, 3]]); + assert.deepEqual(line.geometry.coordinates, [ + [1, 1], + [2, 2], + [3, 3], + [3, 3] + ]); Draw.trash(); assert.equal(Draw.getAll().features.length, 0, 'no feature added'); @@ -310,7 +405,12 @@ test('draw_line_string mouse interaction', async (t) => { mouseClick(map, makeMouseEvent(3, 3)); const line = Draw.getAll().features[0]; - assert.deepEqual(line.geometry.coordinates, [[1, 1], [2, 2], [3, 3], [3, 3]]); + assert.deepEqual(line.geometry.coordinates, [ + [1, 1], + [2, 2], + [3, 3], + [3, 3] + ]); container.dispatchEvent(escapeEvent); @@ -333,12 +433,25 @@ test('draw_line_string mouse interaction', async (t) => { mouseClick(map, makeMouseEvent(3, 3)); const line = Draw.getAll().features[0]; - assert.deepEqual(line.geometry.coordinates, [[1, 1], [2, 2], [3, 3], [3, 3]]); + assert.deepEqual(line.geometry.coordinates, [ + [1, 1], + [2, 2], + [3, 3], + [3, 3] + ]); container.dispatchEvent(enterEvent); assert.equal(Draw.getAll().features.length, 1, 'the feature was added'); - assert.deepEqual(Draw.getAll().features[0].geometry.coordinates, [[1, 1], [2, 2], [3, 3]], 'the line is correct'); + assert.deepEqual( + Draw.getAll().features[0].geometry.coordinates, + [ + [1, 1], + [2, 2], + [3, 3] + ], + 'the line is correct' + ); mouseClick(map, makeMouseEvent(1, 1)); map.fire('mousemove', makeMouseEvent(16, 16)); @@ -370,59 +483,96 @@ test('draw_line_string mouse interaction', async (t) => { map.fire('mousemove', makeMouseEvent(16, 16)); const line = Draw.getAll().features[0]; - assert.deepEqual(line.geometry.coordinates, [[1, 1], [16, 16]], 'and has right coordinates'); + assert.deepEqual( + line.geometry.coordinates, + [ + [1, 1], + [16, 16] + ], + 'and has right coordinates' + ); container.dispatchEvent(enterEvent); assert.equal(Draw.getAll().features.length, 0, 'line_string was removed'); }); - t.test('start draw_line_string mode then start a point after one click', () => { - Draw.deleteAll(); - assert.equal(Draw.getAll().features.length, 0, 'no features yet'); - - Draw.changeMode('draw_line_string'); - assert.equal(Draw.getAll().features.length, 1, 'line is added'); - mouseClick(map, makeMouseEvent(1, 1)); - map.fire('mousemove', makeMouseEvent(16, 16)); - - const line = Draw.getAll().features[0]; - assert.deepEqual(line.geometry.coordinates, [[1, 1], [16, 16]], 'and has right coordinates'); - - container.dispatchEvent(startPointEvent); - assert.equal(Draw.get(line.id), undefined, 'line_string was removed'); - }); - - t.test('start draw_line_string mode then start a line_string after one click', () => { - Draw.deleteAll(); - assert.equal(Draw.getAll().features.length, 0, 'no features yet'); - - Draw.changeMode('draw_line_string'); - assert.equal(Draw.getAll().features.length, 1, 'line is added'); - mouseClick(map, makeMouseEvent(1, 1)); - map.fire('mousemove', makeMouseEvent(16, 16)); - - const line = Draw.getAll().features[0]; - assert.deepEqual(line.geometry.coordinates, [[1, 1], [16, 16]], 'and has right coordinates'); - - container.dispatchEvent(startLineStringEvent); - assert.equal(Draw.get(line.id), undefined, 'line_string was removed'); - }); - - t.test('start draw_line_string mode then start a polygon after one click', () => { - Draw.deleteAll(); - assert.equal(Draw.getAll().features.length, 0, 'no features yet'); - - Draw.changeMode('draw_line_string'); - assert.equal(Draw.getAll().features.length, 1, 'line is added'); - mouseClick(map, makeMouseEvent(1, 1)); - map.fire('mousemove', makeMouseEvent(16, 16)); + t.test( + 'start draw_line_string mode then start a point after one click', + () => { + Draw.deleteAll(); + assert.equal(Draw.getAll().features.length, 0, 'no features yet'); + + Draw.changeMode('draw_line_string'); + assert.equal(Draw.getAll().features.length, 1, 'line is added'); + mouseClick(map, makeMouseEvent(1, 1)); + map.fire('mousemove', makeMouseEvent(16, 16)); + + const line = Draw.getAll().features[0]; + assert.deepEqual( + line.geometry.coordinates, + [ + [1, 1], + [16, 16] + ], + 'and has right coordinates' + ); + + container.dispatchEvent(startPointEvent); + assert.equal(Draw.get(line.id), undefined, 'line_string was removed'); + } + ); - const line = Draw.getAll().features[0]; - assert.deepEqual(line.geometry.coordinates, [[1, 1], [16, 16]], 'and has right coordinates'); + t.test( + 'start draw_line_string mode then start a line_string after one click', + () => { + Draw.deleteAll(); + assert.equal(Draw.getAll().features.length, 0, 'no features yet'); + + Draw.changeMode('draw_line_string'); + assert.equal(Draw.getAll().features.length, 1, 'line is added'); + mouseClick(map, makeMouseEvent(1, 1)); + map.fire('mousemove', makeMouseEvent(16, 16)); + + const line = Draw.getAll().features[0]; + assert.deepEqual( + line.geometry.coordinates, + [ + [1, 1], + [16, 16] + ], + 'and has right coordinates' + ); + + container.dispatchEvent(startLineStringEvent); + assert.equal(Draw.get(line.id), undefined, 'line_string was removed'); + } + ); - container.dispatchEvent(startPolygonEvent); - assert.equal(Draw.get(line.id), undefined, 'line_string was removed'); - }); + t.test( + 'start draw_line_string mode then start a polygon after one click', + () => { + Draw.deleteAll(); + assert.equal(Draw.getAll().features.length, 0, 'no features yet'); + + Draw.changeMode('draw_line_string'); + assert.equal(Draw.getAll().features.length, 1, 'line is added'); + mouseClick(map, makeMouseEvent(1, 1)); + map.fire('mousemove', makeMouseEvent(16, 16)); + + const line = Draw.getAll().features[0]; + assert.deepEqual( + line.geometry.coordinates, + [ + [1, 1], + [16, 16] + ], + 'and has right coordinates' + ); + + container.dispatchEvent(startPolygonEvent); + assert.equal(Draw.get(line.id), undefined, 'line_string was removed'); + } + ); t.test('start draw_line_string mode then double click', () => { Draw.deleteAll(); @@ -458,21 +608,28 @@ test('draw_line_string mouse interaction', async (t) => { mouseClick(map, makeMouseEvent(16, 16)); lineString = Draw.get(lineString.id); assert.equal(lineString !== undefined, true, 'line_string is here'); - assert.deepEqual(lineString, { - id: lineString.id, - type: 'Feature', - properties: {}, - geometry: { - type: 'LineString', - coordinates: [[0, 0], [15, 15]] - } - }, 'line_string has the right coordinates'); + assert.deepEqual( + lineString, + { + id: lineString.id, + type: 'Feature', + properties: {}, + geometry: { + type: 'LineString', + coordinates: [ + [0, 0], + [15, 15] + ] + } + }, + 'line_string has the right coordinates' + ); }); document.body.removeChild(container); }); -test('draw_line_string touch interaction', async (t) => { +test('draw_line_string touch interaction', async t => { const container = document.createElement('div'); document.body.appendChild(container); const map = createMap({ container }); @@ -492,13 +649,28 @@ test('draw_line_string touch interaction', async (t) => { const line = Draw.getAll().features[0]; assert.equal(line.geometry.type, 'LineString'); - assert.deepEqual(line.geometry.coordinates, [[100, 200], [100, 200]], 'starting coordinate added'); + assert.deepEqual( + line.geometry.coordinates, + [ + [100, 200], + [100, 200] + ], + 'starting coordinate added' + ); }); await t.test('tap to add another vertex', () => { touchTap(map, makeTouchEvent(200, 400)); const line = Draw.getAll().features[0]; - assert.deepEqual(line.geometry.coordinates, [[100, 200], [200, 400], [200, 400]], 'last coordinate replaced'); + assert.deepEqual( + line.geometry.coordinates, + [ + [100, 200], + [200, 400], + [200, 400] + ], + 'last coordinate replaced' + ); }); t.test('add more points then tap on the last vertex to finish', () => { @@ -507,14 +679,30 @@ test('draw_line_string touch interaction', async (t) => { touchTap(map, makeTouchEvent(200, 500)); touchTap(map, makeTouchEvent(200, 500)); const line = Draw.getAll().features[0]; - assert.deepEqual(line.geometry.coordinates, - [[100, 200], [200, 400], [400, 500], [300, 500], [200, 500]], - 'all coordinates in place'); + assert.deepEqual( + line.geometry.coordinates, + [ + [100, 200], + [200, 400], + [400, 500], + [300, 500], + [200, 500] + ], + 'all coordinates in place' + ); touchTap(map, makeTouchEvent(700, 700)); - assert.deepEqual(line.geometry.coordinates, - [[100, 200], [200, 400], [400, 500], [300, 500], [200, 500]], - 'since we exited draw_line_string mode, another tap does not add a coordinate'); + assert.deepEqual( + line.geometry.coordinates, + [ + [100, 200], + [200, 400], + [400, 500], + [300, 500], + [200, 500] + ], + 'since we exited draw_line_string mode, another tap does not add a coordinate' + ); }); await t.test('start a line but trash it before completion', () => { @@ -526,7 +714,12 @@ test('draw_line_string touch interaction', async (t) => { touchTap(map, makeTouchEvent(300, 300)); const line = Draw.getAll().features[0]; - assert.deepEqual(line.geometry.coordinates, [[100, 100], [200, 200], [300, 300], [300, 300]]); + assert.deepEqual(line.geometry.coordinates, [ + [100, 100], + [200, 200], + [300, 300], + [300, 300] + ]); Draw.trash(); assert.equal(Draw.getAll().features.length, 0, 'no feature added'); @@ -539,12 +732,16 @@ test('draw_line_string touch interaction', async (t) => { }); test('draw_line_string continue LineString', () => { - const context = createMockDrawModeContext(); + const context = createMockDrawModeContext() as unknown as spy; const mode = drawLineStringMode(context); const lifecycleContext = createMockLifecycleContext(); mode.start.call(lifecycleContext); - const coordinates = [[0, 0], [5, 5], [10, 10]]; + const coordinates = [ + [0, 0], + [5, 5], + [10, 10] + ]; const geojson = { type: 'Feature', id: 1, @@ -554,55 +751,83 @@ test('draw_line_string continue LineString', () => { coordinates: coordinates.slice(0) } }; - const line = new LineString(context, geojson); + const line = new LineString(context, geojson as StrictFeature); context.store.add(line); assert.throws( - () => drawLineStringMode(context, { featureId: 2 }).start(lifecycleContext), + () => drawLineStringMode(context, { featureId: 2 }).start.call(lifecycleContext), /featureId/, 'wrong feature id' ); assert.throws( - () => drawLineStringMode(context, { featureId: 1 }).start(lifecycleContext), + () => drawLineStringMode(context, { featureId: 1 }).start.call(lifecycleContext), /from.*property/, 'no "from" prop' ); assert.throws( - () => drawLineStringMode(context, { featureId: 1, from: '[0, 0]' }).start.call(lifecycleContext), + () => + drawLineStringMode(context, { featureId: 1, from: '[0, 0]' }).start.call( + lifecycleContext + ), /from.*property/, 'incorrect from prop' ); assert.throws( - () => drawLineStringMode(context, { featureId: 1, from: [-1, -1] }).start.call(lifecycleContext), + () => + drawLineStringMode(context, { featureId: 1, from: [-1, -1] }).start.call( + lifecycleContext + ), /start or the end/, 'not on line' ); assert.throws( - () => drawLineStringMode(context, { featureId: 1, from: [5, 5] }).start.call(lifecycleContext), + () => + drawLineStringMode(context, { featureId: 1, from: [5, 5] }).start.call( + lifecycleContext + ), /start or the end/, 'not at line endpoint' ); - drawLineStringMode(context, { featureId: 1, from: [0, 0] }).start.call(lifecycleContext); + drawLineStringMode(context, { featureId: 1, from: [0, 0] }).start.call( + lifecycleContext + ); let testLine = context.store.get(context.store.getAllIds()[0]); assert.equal(testLine.id, 1, 'initialized with correct line'); - assert.deepEqual(testLine.coordinates, [[0, 0], ...coordinates], - 'added one coordinate at the start endpoint'); + assert.deepEqual( + testLine.coordinates, + [[0, 0], ...coordinates], + 'added one coordinate at the start endpoint' + ); - drawLineStringMode(context, { featureId: 1, from: [10, 10] }).start.call(lifecycleContext); + drawLineStringMode(context, { featureId: 1, from: [10, 10] }).start.call( + lifecycleContext + ); testLine = context.store.get(context.store.getAllIds()[0]); - assert.deepEqual(testLine.coordinates, [[0, 0], ...coordinates, [10, 10]], - 'added one coordinate at the end endpoint'); + assert.deepEqual( + testLine.coordinates, + [[0, 0], ...coordinates, [10, 10]], + 'added one coordinate at the end endpoint' + ); assert.doesNotThrow( - () => drawLineStringMode(context, { featureId: 1, from: { type: 'Point', coordinates: [0, 0] } }).start.call(lifecycleContext), + () => + drawLineStringMode(context, { + featureId: 1, + from: { type: 'Point', coordinates: [0, 0] } + }).start.call(lifecycleContext), 'initializes with Point' ); assert.doesNotThrow( - () => drawLineStringMode(context, { - featureId: 1, - from: { type: 'Feature', geometry: { type: 'Point', coordinates: [0, 0] }, properties: {} } - }).start.call(lifecycleContext), + () => + drawLineStringMode(context, { + featureId: 1, + from: { + type: 'Feature', + geometry: { type: 'Point', coordinates: [0, 0] }, + properties: {} + } + }).start.call(lifecycleContext), 'initializes with a Feature' ); }); @@ -617,7 +842,11 @@ test('draw_line_string continue LineString mouseClick', async () => { await map.on('load'); - const coordinates = [[0, 0], [5, 5], [10, 10]]; + const coordinates = [ + [0, 0], + [5, 5], + [10, 10] + ]; const geojson = { type: 'Feature', id: 1, @@ -635,14 +864,22 @@ test('draw_line_string continue LineString mouseClick', async () => { await afterNextRender(); const line = Draw.getAll().features[0]; - assert.deepEqual(line.geometry.coordinates, [[-1, -1], [-1, -1], ...coordinates], 'line continues from the start'); + assert.deepEqual( + line.geometry.coordinates, + [[-1, -1], [-1, -1], ...coordinates], + 'line continues from the start' + ); Draw.changeMode('draw_line_string', { featureId: 1, from: [10, 10] }); mouseClick(map, makeMouseEvent(12, 12)); await afterNextRender(); const line2 = Draw.getAll().features[0]; - assert.deepEqual(line2.geometry.coordinates, [[-1, -1], ...coordinates, [12, 12], [12, 12]], 'line continues from the end'); + assert.deepEqual( + line2.geometry.coordinates, + [[-1, -1], ...coordinates, [12, 12], [12, 12]], + 'line continues from the end' + ); document.body.removeChild(container); }); diff --git a/test/draw_point.test.js b/test/draw_point.test.ts similarity index 56% rename from test/draw_point.test.js rename to test/draw_point.test.ts index cf6caba9..4d9a232f 100644 --- a/test/draw_point.test.js +++ b/test/draw_point.test.ts @@ -1,21 +1,25 @@ +import './mock-browser'; +import { spy } from 'sinon'; import test from 'node:test'; import assert from 'node:assert/strict'; -import MapboxDraw from '../index.js'; -import mouseClick from './utils/mouse_click.js'; -import touchTap from './utils/touch_tap.js'; -import createMap from './utils/create_map.js'; -import makeMouseEvent from './utils/make_mouse_event.js'; -import makeTouchEvent from './utils/make_touch_event.js'; -import drawPointModeObject from '../src/modes/draw_point.js'; -import Point from '../src/feature_types/point.js'; -import createMockDrawModeContext from './utils/create_mock_draw_mode_context.js'; -import createMockLifecycleContext from './utils/create_mock_lifecycle_context.js'; -import {escapeEvent, enterEvent} from './utils/key_events.js'; -import objectToMode from '../src/modes/object_to_mode.js'; -const drawPointMode = objectToMode(drawPointModeObject); +import MapboxDraw from '../index'; +import mouseClick from './utils/mouse_click'; +import touchTap from './utils/touch_tap'; +import createMap from './utils/create_map'; +import makeMouseEvent from './utils/make_mouse_event'; +import makeTouchEvent from './utils/make_touch_event'; +import drawPointModeObject from '../src/modes/draw_point'; +import Point from '../src/feature_types/point'; +import createMockDrawModeContext from './utils/create_mock_draw_mode_context'; +import createMockLifecycleContext from './utils/create_mock_lifecycle_context'; +import { escapeEvent, enterEvent } from './utils/key_events'; +import { objectToMode } from '../src/modes/object_to_mode'; +import type { StrictFeature } from '../src/types/types'; + +const drawPointMode = objectToMode(drawPointModeObject as spy); test('draw_point mode initialization', () => { - const context = createMockDrawModeContext(); + const context = createMockDrawModeContext() as spy; const lifecycleContext = createMockLifecycleContext(); const mode = drawPointMode(context); mode.start.call(lifecycleContext); @@ -31,29 +35,50 @@ test('draw_point mode initialization', () => { } }); // Strip ids for this comparison - assert.deepEqual(Object.assign({}, context.store.add.getCall(0).args[0], { id: null }), - Object.assign({}, emptypoint, { id: null }), 'with a new line'); + assert.deepEqual( + Object.assign({}, context.store.add.getCall(0).args[0], { id: null }), + Object.assign({}, emptypoint, { id: null }), + 'with a new line' + ); }); test('draw_point start', () => { - const context = createMockDrawModeContext(); + const context = createMockDrawModeContext() as spy; const lifecycleContext = createMockLifecycleContext(); const mode = drawPointMode(context); mode.start.call(lifecycleContext); - assert.equal(context.store.clearSelected.callCount, 1, 'store.clearSelected called'); - assert.equal(context.ui.queueMapClasses.callCount, 1, 'ui.queueMapClasses called'); - assert.deepEqual(context.ui.queueMapClasses.getCall(0).args, [{ mouse: 'add' }], - 'ui.queueMapClasses received correct arguments'); - assert.equal(context.ui.setActiveButton.callCount, 1, 'ui.setActiveButton called'); - assert.deepEqual(context.ui.setActiveButton.getCall(0).args, ['point'], - 'ui.setActiveButton received correct arguments'); + assert.equal( + context.store.clearSelected.callCount, + 1, + 'store.clearSelected called' + ); + assert.equal( + context.ui.queueMapClasses.callCount, + 1, + 'ui.queueMapClasses called' + ); + assert.deepEqual( + context.ui.queueMapClasses.getCall(0).args, + [{ mouse: 'add' }], + 'ui.queueMapClasses received correct arguments' + ); + assert.equal( + context.ui.setActiveButton.callCount, + 1, + 'ui.setActiveButton called' + ); + assert.deepEqual( + context.ui.setActiveButton.getCall(0).args, + ['point'], + 'ui.setActiveButton received correct arguments' + ); assert.equal(lifecycleContext.on.callCount, 12, 'this.on called'); }); test('draw_point stop with point placed', () => { - const context = createMockDrawModeContext(); + const context = createMockDrawModeContext() as spy; const mode = drawPointMode(context); const lifecycleContext = createMockLifecycleContext(); mode.start.call(lifecycleContext); @@ -63,15 +88,22 @@ test('draw_point stop with point placed', () => { const point = context.store.get(id); point.updateCoordinate(10, 20); - mode.stop.call(); - assert.equal(context.ui.setActiveButton.callCount, 2, 'ui.setActiveButton called'); - assert.deepEqual(context.ui.setActiveButton.getCall(1).args, [undefined], - 'ui.setActiveButton received correct arguments'); + (mode.stop as spy).call(); + assert.equal( + context.ui.setActiveButton.callCount, + 2, + 'ui.setActiveButton called' + ); + assert.deepEqual( + context.ui.setActiveButton.getCall(1).args, + [undefined], + 'ui.setActiveButton received correct arguments' + ); assert.equal(context.store.delete.callCount, 0, 'store.delete not called'); }); test('draw_point stop with no point placed', () => { - const context = createMockDrawModeContext(); + const context = createMockDrawModeContext() as spy; const mode = drawPointMode(context); const lifecycleContext = createMockLifecycleContext(); mode.start.call(lifecycleContext); @@ -79,21 +111,28 @@ test('draw_point stop with no point placed', () => { const id = context.store.getAllIds()[0]; const point = context.store.get(id); - mode.stop.call(); - - - assert.equal(context.ui.setActiveButton.callCount, 2, 'ui.setActiveButton called'); - assert.deepEqual(context.ui.setActiveButton.getCall(1).args, [undefined], - 'ui.setActiveButton received correct arguments'); + (mode.stop as spy).call(); + + assert.equal( + context.ui.setActiveButton.callCount, + 2, + 'ui.setActiveButton called' + ); + assert.deepEqual( + context.ui.setActiveButton.getCall(1).args, + [undefined], + 'ui.setActiveButton received correct arguments' + ); assert.equal(context.store.delete.callCount, 1, 'store.delete called'); - assert.deepEqual(context.store.delete.getCall(0).args, [ - [point.id], - { silent: true } - ], 'store.delete received correct arguments'); + assert.deepEqual( + context.store.delete.getCall(0).args, + [[point.id], { silent: true }], + 'store.delete received correct arguments' + ); }); test('draw_point render the active point', () => { - const context = createMockDrawModeContext(); + const context = createMockDrawModeContext() as spy; const mode = drawPointMode(context); const lifecycleContext = createMockLifecycleContext(); mode.start.call(lifecycleContext); @@ -112,13 +151,12 @@ test('draw_point render the active point', () => { coordinates: [10, 10] } }; - mode.render(geojson, x => memo.push(x)); + mode.render(geojson as StrictFeature, x => memo.push(x)); assert.equal(memo.length, 0, 'active point does not render'); - }); test('draw_point render an inactive feature', () => { - const context = createMockDrawModeContext(); + const context = createMockDrawModeContext() as spy; const mode = drawPointMode(context); const lifecycleContext = createMockLifecycleContext(); mode.start.call(lifecycleContext); @@ -131,26 +169,35 @@ test('draw_point render an inactive feature', () => { }, geometry: { type: 'LineString', - coordinates: [[10, 10], [20, 20]] + coordinates: [ + [10, 10], + [20, 20] + ] } }; - mode.render(geojson, x => memo.push(x)); + mode.render(geojson as StrictFeature, x => memo.push(x)); assert.equal(memo.length, 1, 'does render'); - assert.deepEqual(memo[0], { - type: 'Feature', - properties: { - active: 'false', - meta: 'nothing' + assert.deepEqual( + memo[0], + { + type: 'Feature', + properties: { + active: 'false', + meta: 'nothing' + }, + geometry: { + type: 'LineString', + coordinates: [ + [10, 10], + [20, 20] + ] + } }, - geometry: { - type: 'LineString', - coordinates: [[10, 10], [20, 20]] - } - }, 'unaltered except active: false'); - + 'unaltered except active: false' + ); }); -test('draw_point mouse interaction', async (t) => { +test('draw_point mouse interaction', async t => { const container = document.createElement('div'); document.body.appendChild(container); const map = createMap({ container }); @@ -174,7 +221,11 @@ test('draw_point mouse interaction', async (t) => { assert.deepEqual(point.geometry.coordinates, [10, 20], 'coordinate added'); mouseClick(map, makeMouseEvent(30, 30)); - assert.equal(features.length, 1, 'mode has changed, so another click does not create another point'); + assert.equal( + features.length, + 1, + 'mode has changed, so another click does not create another point' + ); }); await t.test('exist before clicking by hitting Escape', () => { @@ -185,7 +236,11 @@ test('draw_point mouse interaction', async (t) => { assert.equal(Draw.getAll().features.length, 0, 'no feature added'); mouseClick(map, makeMouseEvent(30, 30)); - assert.equal(Draw.getAll().features.length, 0, 'mode has changed, so a click does not create another point'); + assert.equal( + Draw.getAll().features.length, + 0, + 'mode has changed, so a click does not create another point' + ); }); await t.test('exist before clicking by hitting Enter', () => { @@ -196,7 +251,11 @@ test('draw_point mouse interaction', async (t) => { assert.equal(Draw.getAll().features.length, 0, 'no feature added'); mouseClick(map, makeMouseEvent(30, 30)); - assert.equal(Draw.getAll().features.length, 0, 'mode has changed, so a click does not create another point'); + assert.equal( + Draw.getAll().features.length, + 0, + 'mode has changed, so a click does not create another point' + ); }); t.test('exist before clicking with Trash', () => { @@ -207,14 +266,17 @@ test('draw_point mouse interaction', async (t) => { assert.equal(Draw.getAll().features.length, 0, 'no feature added'); mouseClick(map, makeMouseEvent(30, 30)); - assert.equal(Draw.getAll().features.length, 0, 'mode has changed, so a click does not create another point'); + assert.equal( + Draw.getAll().features.length, + 0, + 'mode has changed, so a click does not create another point' + ); }); document.body.removeChild(container); }); - -test('draw_point touch interaction', async (t) => { +test('draw_point touch interaction', async t => { const container = document.createElement('div'); document.body.appendChild(container); const map = createMap({ container }); @@ -237,7 +299,11 @@ test('draw_point touch interaction', async (t) => { assert.deepEqual(point.geometry.coordinates, [10, 20], 'coordinate added'); touchTap(map, makeTouchEvent(30, 30)); - assert.equal(features.length, 1, 'mode has changed, so another click does not create another point'); + assert.equal( + features.length, + 1, + 'mode has changed, so another click does not create another point' + ); }); document.body.removeChild(container); diff --git a/test/draw_polygon.test.js b/test/draw_polygon.test.js deleted file mode 100644 index 03b41695..00000000 --- a/test/draw_polygon.test.js +++ /dev/null @@ -1,657 +0,0 @@ -import test from 'node:test'; -import assert from 'node:assert/strict'; -import MapboxDraw from '../index.js'; -import createMap from './utils/create_map.js'; -import mouseClick from './utils/mouse_click.js'; -import touchTap from './utils/touch_tap.js'; -import makeMouseEvent from './utils/make_mouse_event.js'; -import makeTouchEvent from './utils/make_touch_event.js'; -import drawPolygonModeObject from '../src/modes/draw_polygon.js'; -import Polygon from '../src/feature_types/polygon.js'; -import createMockDrawModeContext from './utils/create_mock_draw_mode_context.js'; -import createMockLifecycleContext from './utils/create_mock_lifecycle_context.js'; -import {setupAfterNextRender} from './utils/after_next_render.js'; -import objectToMode from '../src/modes/object_to_mode.js'; -const drawPolygonMode = objectToMode(drawPolygonModeObject); - -import { - enterEvent, - startPointEvent, - startLineStringEvent, - startPolygonEvent, - escapeEvent -} from './utils/key_events.js'; - -test('draw_polygon mode initialization', () => { - const context = createMockDrawModeContext(); - const mode = drawPolygonMode(context); - const lifecycleContext = createMockLifecycleContext(); - mode.start.call(lifecycleContext); - - assert.equal(context.store.add.callCount, 1, 'store.add called'); - - const emptyPolygon = new Polygon(context, { - type: 'Feature', - properties: {}, - geometry: { - type: 'Polygon', - coordinates: [[]] - } - }); - // Strip ids for this comparison - assert.deepEqual(Object.assign({}, context.store.add.getCall(0).args[0], { id: null }), - Object.assign({}, emptyPolygon, { id: null }), 'with a new polygon'); -}); - -test('draw_polygon start', async () => { - const context = createMockDrawModeContext(); - const lifecycleContext = createMockLifecycleContext(); - const mode = drawPolygonMode(context); - - mode.start.call(lifecycleContext); - assert.equal(context.store.clearSelected.callCount, 1, 'store.clearSelected called'); - assert.equal(context.ui.queueMapClasses.callCount, 1, 'ui.queueMapClasses called'); - assert.deepEqual(context.ui.queueMapClasses.getCall(0).args, [{ mouse: 'add' }], - 'ui.queueMapClasses received correct arguments'); - assert.equal(context.ui.setActiveButton.callCount, 1, 'ui.setActiveButton called'); - assert.deepEqual(context.ui.setActiveButton.getCall(0).args, ['polygon'], - 'ui.setActiveButton received correct arguments'); - - await new Promise(resolve => setTimeout(resolve, 10)); - assert.equal(context.map.doubleClickZoom.disable.callCount, 1); -}); - -test('draw_polygon stop with valid polygon', () => { - const context = createMockDrawModeContext(); - const mode = drawPolygonMode(context); - const lifecycleContext = createMockLifecycleContext(); - mode.start.call(lifecycleContext); - - // Fake a valid polygon - const testPolygon = context.store.get(context.store.getAllIds()[0]); - testPolygon.isValid = () => true; - - mode.stop.call(); - assert.equal(context.ui.setActiveButton.callCount, 2, 'ui.setActiveButton called'); - assert.deepEqual(context.ui.setActiveButton.getCall(1).args, [undefined], - 'ui.setActiveButton received correct arguments'); - assert.equal(context.store.delete.callCount, 0, 'store.delete not called'); -}); - -test('draw_polygon stop with invalid polygon', () => { - const context = createMockDrawModeContext(); - const mode = drawPolygonMode(context); - const lifecycleContext = createMockLifecycleContext(); - mode.start.call(lifecycleContext); - - // Fake an invalid polygon - const testPolygon = context.store.get(context.store.getAllIds()[0]); - testPolygon.isValid = () => false; - - mode.stop.call(); - assert.equal(context.ui.setActiveButton.callCount, 2, 'ui.setActiveButton called'); - assert.deepEqual(context.ui.setActiveButton.getCall(1).args, [undefined], - 'ui.setActiveButton received correct arguments'); - assert.equal(context.store.delete.callCount, 1, 'store.delete called'); - assert.deepEqual(context.store.delete.getCall(0).args, [ - [testPolygon.id], - { silent: true } - ], 'store.delete received correct arguments'); -}); - -test('draw_polygon render active polygon with no coordinates', () => { - const context = createMockDrawModeContext(); - const mode = drawPolygonMode(context); - const lifecycleContext = createMockLifecycleContext(); - mode.start.call(lifecycleContext); - const testPolygon = context.store.get(context.store.getAllIds()[0]); - - const memo = []; - const geojson = { - type: 'Feature', - properties: { - id: testPolygon.id - }, - geometry: { - type: 'Polygon', - coordinates: [] - } - }; - mode.render(geojson, x => memo.push(x)); - assert.equal(memo.length, 0, 'does not render'); -}); - -test('draw_polygon render active polygon with 1 coordinate (and closer)', () => { - const context = createMockDrawModeContext(); - const mode = drawPolygonMode(context); - const lifecycleContext = createMockLifecycleContext(); - mode.start.call(lifecycleContext); - const testPolygon = context.store.get(context.store.getAllIds()[0]); - - const memo = []; - const geojson = { - type: 'Feature', - properties: { - id: testPolygon.id - }, - geometry: { - type: 'Polygon', - coordinates: [[[10, 10], [10, 10]]] - } - }; - mode.render(geojson, x => memo.push(x)); - assert.equal(memo.length, 0, 'does not render'); -}); - -test('draw_polygon render active polygon with 2 coordinates (and closer)', () => { - const context = createMockDrawModeContext(); - const mode = drawPolygonMode(context); - const lifecycleContext = createMockLifecycleContext(); - mode.start.call(lifecycleContext); - const testPolygon = context.store.get(context.store.getAllIds()[0]); - - const memo = []; - const geojson = { - type: 'Feature', - properties: { - id: testPolygon.id - }, - geometry: { - type: 'Polygon', - coordinates: [[[0, 0], [0, 10], [0, 0]]] - } - }; - mode.render(geojson, x => memo.push(x)); - assert.equal(memo.length, 2, 'does render'); - assert.deepEqual(memo[1], { - type: 'Feature', - properties: { - id: testPolygon.id, - active: 'true', - meta: 'feature' - }, - geometry: { - type: 'LineString', - coordinates: [[0, 0], [0, 10]] - } - }, 'renders as a LineString with meta: feature, active: true'); - -}); - -test('draw_polygon render active polygon with 3 coordinates (and closer)', () => { - const context = createMockDrawModeContext(); - const mode = drawPolygonMode(context); - const lifecycleContext = createMockLifecycleContext(); - mode.start.call(lifecycleContext); - const testPolygon = context.store.get(context.store.getAllIds()[0]); - - const memo = []; - const geojson = { - type: 'Feature', - properties: { - id: testPolygon.id - }, - geometry: { - type: 'Polygon', - coordinates: [[[0, 0], [0, 10], [10, 10], [10, 0], [0, 0]]] - } - }; - mode.render(geojson, x => memo.push(x)); - assert.equal(memo.length, 3, 'does render'); - assert.deepEqual(memo[0], { - type: 'Feature', - properties: { - parent: testPolygon.id, - meta: 'vertex', - coord_path: '0.0', - active: 'false' - }, - geometry: { - type: 'Point', - coordinates: [0, 0] - } - }, 'renders the start point point with meta: vertex'); - assert.deepEqual(memo[1], { - type: 'Feature', - properties: { - parent: testPolygon.id, - meta: 'vertex', - coord_path: '0.2', - active: 'false' - }, - geometry: { - type: 'Point', - coordinates: [10, 10] - } - }, 'renders end point with meta: vertex'); - assert.deepEqual(memo[2], { - type: 'Feature', - properties: { - id: testPolygon.id, - active: 'true', - meta: 'feature' - }, - geometry: { - type: 'Polygon', - coordinates: [[[0, 0], [0, 10], [10, 10], [10, 0], [0, 0]]] - } - }, 'renders as a polygon with meta: feature, active: true'); - -}); - -test('draw_polygon render inactive feature', () => { - const context = createMockDrawModeContext(); - const mode = drawPolygonMode(context); - const lifecycleContext = createMockLifecycleContext(); - mode.start.call(lifecycleContext); - - const memo = []; - const geojson = { - type: 'Feature', - properties: { - meta: 'meh' - }, - geometry: { - type: 'LineString', - coordinates: [[0, 0], [0, 10]] - } - }; - mode.render(geojson, x => memo.push(x)); - assert.equal(memo.length, 1, 'does render'); - assert.deepEqual(memo[0], { - type: 'Feature', - properties: { - active: 'false', - meta: 'meh' - }, - geometry: { - type: 'LineString', - coordinates: [[0, 0], [0, 10]] - } - }, 'unaltered except active: false'); - -}); - -test('draw_polygon mouse interaction', async (t) => { - const container = document.createElement('div'); - document.body.appendChild(container); - const map = createMap({ container }); - const Draw = new MapboxDraw(); - map.addControl(Draw); - - await map.on('load'); - - // The following sub-tests share state ... - const afterNextRender = setupAfterNextRender(map); - - Draw.changeMode('draw_polygon'); - t.test('first click', () => { - mouseClick(map, makeMouseEvent(10, 20)); - - const { features } = Draw.getAll(); - assert.equal(features.length, 1, 'polygon created'); - const polygon = Draw.getAll().features[0]; - assert.equal(polygon.geometry.type, 'Polygon'); - - assert.deepEqual(polygon.geometry.coordinates, [[[10, 20], [10, 20], [10, 20]]], 'starting coordinate added'); - }); - - await t.test('move mouse', () => { - map.fire('mousemove', makeMouseEvent(15, 23)); - const polygon = Draw.getAll().features[0]; - assert.deepEqual(polygon.geometry.coordinates, [[[10, 20], [15, 23], [10, 20]]], 'middle coordinate added'); - }); - - await t.test('move mouse again', () => { - map.fire('mousemove', makeMouseEvent(30, 33)); - const polygon = Draw.getAll().features[0]; - assert.deepEqual(polygon.geometry.coordinates, [[[10, 20], [30, 33], [10, 20]]], 'middle coordinate replaced'); - }); - - await t.test('click to add another vertex', () => { - mouseClick(map, makeMouseEvent(35, 35)); - const polygon = Draw.getAll().features[0]; - assert.deepEqual(polygon.geometry.coordinates, [[[10, 20], [35, 35], [35, 35], [10, 20]]], 'middle coordinate replaced'); - }); - - t.test('add more points then click on the last vertex to finish', () => { - mouseClick(map, makeMouseEvent(40, 40)); - mouseClick(map, makeMouseEvent(50, 50)); - mouseClick(map, makeMouseEvent(55, 55)); - mouseClick(map, makeMouseEvent(55, 55)); - const polygon = Draw.getAll().features[0]; - assert.deepEqual(polygon.geometry.coordinates, - [[[10, 20], [35, 35], [40, 40], [50, 50], [55, 55], [10, 20]]], - 'all coordinates in place'); - - mouseClick(map, makeMouseEvent(40, 40)); - assert.deepEqual(polygon.geometry.coordinates, - [[[10, 20], [35, 35], [40, 40], [50, 50], [55, 55], [10, 20]]], - 'since we exited draw_polygon mode, another click does not add a coordinate'); - }); - - await t.test('start a polygon but trash it before completion', () => { - // Start a new polygon - Draw.deleteAll(); - Draw.changeMode('draw_polygon'); - mouseClick(map, makeMouseEvent(1, 1)); - mouseClick(map, makeMouseEvent(2, 2)); - mouseClick(map, makeMouseEvent(3, 3)); - - const polygon = Draw.getAll().features[0]; - assert.deepEqual(polygon.geometry.coordinates, [[[1, 1], [2, 2], [3, 3], [3, 3], [1, 1]]]); - - Draw.trash(); - assert.equal(Draw.getAll().features.length, 0, 'no feature added'); - - mouseClick(map, makeMouseEvent(1, 1)); - assert.equal(Draw.getAll().features.length, 0, 'no longer drawing'); - }); - - await t.test('start a polygon but trash it with Escape before completion', () => { - // Start a new polygon - Draw.deleteAll(); - Draw.changeMode('draw_polygon'); - mouseClick(map, makeMouseEvent(1, 1)); - mouseClick(map, makeMouseEvent(2, 2)); - mouseClick(map, makeMouseEvent(3, 3)); - - const polygon = Draw.getAll().features[0]; - assert.deepEqual(polygon.geometry.coordinates, [[[1, 1], [2, 2], [3, 3], [3, 3], [1, 1]]]); - - container.dispatchEvent(escapeEvent); - - assert.equal(Draw.getAll().features.length, 0, 'no feature added'); - - mouseClick(map, makeMouseEvent(1, 1)); - assert.equal(Draw.getAll().features.length, 0, 'no longer drawing'); - }); - - // ZERO CLICK TESTS - - await t.test('start a polygon and end it with Enter', () => { - // Start a new polygon - Draw.deleteAll(); - Draw.changeMode('draw_polygon'); - mouseClick(map, makeMouseEvent(1, 1)); - mouseClick(map, makeMouseEvent(2, 2)); - mouseClick(map, makeMouseEvent(3, 3)); - - const polygon = Draw.getAll().features[0]; - assert.deepEqual(polygon.geometry.coordinates, [[[1, 1], [2, 2], [3, 3], [3, 3], [1, 1]]]); - - container.dispatchEvent(enterEvent); - - assert.equal(Draw.getAll().features.length, 1, 'the feature was added'); - assert.deepEqual(Draw.getAll().features[0].geometry.coordinates, [[[1, 1], [2, 2], [3, 3], [1, 1]]], 'the polygon is correct'); - - mouseClick(map, makeMouseEvent(1, 1)); - assert.equal(Draw.getAll().features.length, 1, 'no longer drawing'); - }); - - await t.test('start draw_polygon mode then changemode before a click', () => { - Draw.deleteAll(); - assert.equal(Draw.getAll().features.length, 0, 'no features yet'); - - Draw.changeMode('draw_polygon'); - assert.equal(Draw.getAll().features.length, 1, 'polygon is added'); - const polygon = Draw.getAll().features[0]; - assert.deepEqual(polygon.geometry.type, 'Polygon'); - - Draw.changeMode('simple_select'); - assert.equal(Draw.getAll().features.length, 0, 'polygon is removed'); - }); - - // ONE CLICK TESTS - - await t.test('start draw_polygon mode then enter after one click', () => { - Draw.deleteAll(); - assert.equal(Draw.getAll().features.length, 0, 'no features yet'); - - Draw.changeMode('draw_polygon'); - assert.equal(Draw.getAll().features.length, 1, 'polygon is added'); - mouseClick(map, makeMouseEvent(1, 1)); - map.fire('mousemove', makeMouseEvent(16, 16)); - - const polygon = Draw.getAll().features[0]; - assert.deepEqual(polygon.geometry.coordinates, [[[1, 1], [16, 16], [1, 1]]], 'and has right coordinates'); - - container.dispatchEvent(enterEvent); - assert.equal(Draw.getAll().features.length, 0, 'polygon was removed'); - }); - - await t.test('start draw_polygon mode then start a point after one click', () => { - Draw.deleteAll(); - assert.equal(Draw.getAll().features.length, 0, 'no features yet'); - - Draw.changeMode('draw_polygon'); - assert.equal(Draw.getAll().features.length, 1, 'polygon is added'); - mouseClick(map, makeMouseEvent(1, 1)); - map.fire('mousemove', makeMouseEvent(16, 16)); - - const polygon = Draw.getAll().features[0]; - assert.deepEqual(polygon.geometry.coordinates, [[[1, 1], [16, 16], [1, 1]]], 'and has right coordinates'); - - container.dispatchEvent(startPointEvent); - assert.equal(Draw.get(polygon.id), undefined, 'polygon was removed'); - }); - - await t.test('start draw_polygon mode then start a line_string after one click', () => { - Draw.deleteAll(); - assert.equal(Draw.getAll().features.length, 0, 'no features yet'); - - Draw.changeMode('draw_polygon'); - assert.equal(Draw.getAll().features.length, 1, 'polygon is added'); - mouseClick(map, makeMouseEvent(1, 1)); - map.fire('mousemove', makeMouseEvent(16, 16)); - - const polygon = Draw.getAll().features[0]; - assert.deepEqual(polygon.geometry.coordinates, [[[1, 1], [16, 16], [1, 1]]], 'and has right coordinates'); - - container.dispatchEvent(startLineStringEvent); - assert.equal(Draw.get(polygon.id), undefined, 'polygon was removed'); - }); - - await t.test('start draw_polygon mode then start a new polygon after one click', () => { - Draw.deleteAll(); - assert.equal(Draw.getAll().features.length, 0, 'no features yet'); - - Draw.changeMode('draw_polygon'); - assert.equal(Draw.getAll().features.length, 1, 'polygon is added'); - mouseClick(map, makeMouseEvent(1, 1)); - map.fire('mousemove', makeMouseEvent(16, 16)); - - const polygon = Draw.getAll().features[0]; - assert.deepEqual(polygon.geometry.coordinates, [[[1, 1], [16, 16], [1, 1]]], 'and has right coordinates'); - - container.dispatchEvent(startPolygonEvent); - assert.equal(Draw.get(polygon.id), undefined, 'polygon was removed'); - }); - - await t.test('start draw_polygon mode then doubleclick', () => { - Draw.deleteAll(); - assert.equal(Draw.getAll().features.length, 0, 'no features yet'); - - Draw.changeMode('draw_polygon'); - assert.equal(Draw.getAll().features.length, 1, 'polygon is added'); - mouseClick(map, makeMouseEvent(1, 1)); - mouseClick(map, makeMouseEvent(1, 1)); - - assert.equal(Draw.getAll().features.length, 0, 'polygon was removed'); - }); - - // TWO CLICK TESTS - - t.test('start draw_polygon mode then enter after two clicks', () => { - Draw.deleteAll(); - assert.equal(Draw.getAll().features.length, 0, 'no features yet'); - - Draw.changeMode('draw_polygon'); - assert.equal(Draw.getAll().features.length, 1, 'polygon is added'); - mouseClick(map, makeMouseEvent(1, 1)); - mouseClick(map, makeMouseEvent(16, 16)); - map.fire('mousemove', makeMouseEvent(8, 0)); - - const polygon = Draw.getAll().features[0]; - assert.deepEqual(polygon.geometry.coordinates, [[[1, 1], [16, 16], [8, 0], [1, 1]]], 'and has right coordinates'); - - container.dispatchEvent(enterEvent); - assert.equal(Draw.getAll().features.length, 0, 'polygon was removed'); - }); - - await t.test('start draw_polygon mode then start a point after two clicks', () => { - Draw.deleteAll(); - assert.equal(Draw.getAll().features.length, 0, 'no features yet'); - - Draw.changeMode('draw_polygon'); - assert.equal(Draw.getAll().features.length, 1, 'polygon is added'); - mouseClick(map, makeMouseEvent(1, 1)); - mouseClick(map, makeMouseEvent(16, 16)); - map.fire('mousemove', makeMouseEvent(8, 0)); - - const polygon = Draw.getAll().features[0]; - assert.deepEqual(polygon.geometry.coordinates, [[[1, 1], [16, 16], [8, 0], [1, 1]]], 'and has right coordinates'); - - container.dispatchEvent(startPointEvent); - assert.equal(Draw.get(polygon.id), undefined, 'polygon was removed'); - }); - - await t.test('start draw_polygon mode then start a line_string after two clicks', () => { - Draw.deleteAll(); - assert.equal(Draw.getAll().features.length, 0, 'no features yet'); - - Draw.changeMode('draw_polygon'); - assert.equal(Draw.getAll().features.length, 1, 'polygon is added'); - mouseClick(map, makeMouseEvent(1, 1)); - mouseClick(map, makeMouseEvent(16, 16)); - map.fire('mousemove', makeMouseEvent(8, 0)); - - const polygon = Draw.getAll().features[0]; - assert.deepEqual(polygon.geometry.coordinates, [[[1, 1], [16, 16], [8, 0], [1, 1]]], 'and has right coordinates'); - - container.dispatchEvent(startLineStringEvent); - assert.equal(Draw.get(polygon.id), undefined, 'polygon was removed'); - }); - - await t.test('start draw_polygon mode then start a new polygon after two clicks', () => { - Draw.deleteAll(); - assert.equal(Draw.getAll().features.length, 0, 'no features yet'); - - Draw.changeMode('draw_polygon'); - assert.equal(Draw.getAll().features.length, 1, 'polygon is added'); - mouseClick(map, makeMouseEvent(1, 1)); - mouseClick(map, makeMouseEvent(16, 16)); - map.fire('mousemove', makeMouseEvent(8, 0)); - - const polygon = Draw.getAll().features[0]; - assert.deepEqual(polygon.geometry.coordinates, [[[1, 1], [16, 16], [8, 0], [1, 1]]], 'and has right coordinates'); - - container.dispatchEvent(startPolygonEvent); - assert.equal(Draw.get(polygon.id), undefined, 'polygon was removed'); - }); - - t.test('start draw_polygon mode then doubleclick', () => { - Draw.deleteAll(); - assert.equal(Draw.getAll().features.length, 0, 'no features yet'); - - Draw.changeMode('draw_polygon'); - assert.equal(Draw.getAll().features.length, 1, 'polygon is added'); - mouseClick(map, makeMouseEvent(1, 1)); - mouseClick(map, makeMouseEvent(16, 16)); - mouseClick(map, makeMouseEvent(16, 16)); - - assert.equal(Draw.getAll().features.length, 0, 'polygon was removed'); - }); - - // FIVE CLICK TEST - - await t.test('end draw_polygon mode by clicking on the start point', async () => { - Draw.deleteAll(); - assert.equal(Draw.getAll().features.length, 0, 'no features yet'); - Draw.changeMode('draw_polygon'); - let polygon = Draw.getAll().features[0]; - assert.equal(polygon !== undefined, true, 'polygon is added'); - mouseClick(map, makeMouseEvent(0, 0)); - mouseClick(map, makeMouseEvent(20, 0)); - mouseClick(map, makeMouseEvent(20, 20)); - mouseClick(map, makeMouseEvent(0, 20)); - - await afterNextRender(); - - map.fire('mousemove', makeMouseEvent(0, 0)); - mouseClick(map, makeMouseEvent(0, 0)); - - polygon = Draw.get(polygon.id); - assert.deepEqual(polygon.geometry.coordinates, [[[0, 0], [20, 0], [20, 20], [0, 20], [0, 0]]], 'and has right coordinates'); - Draw.deleteAll(); - }); - - document.body.removeChild(container); -}); - - -test('draw_polygon touch interaction', async (t) => { - const container = document.createElement('div'); - document.body.appendChild(container); - const map = createMap({ container }); - const Draw = new MapboxDraw(); - map.addControl(Draw); - - await map.on('load'); - - // The following sub-tests share state ... - - Draw.changeMode('draw_polygon'); - t.test('first tap', () => { - touchTap(map, makeTouchEvent(100, 200)); - - const { features } = Draw.getAll(); - assert.equal(features.length, 1, 'polygon created'); - const polygon = Draw.getAll().features[0]; - assert.equal(polygon.geometry.type, 'Polygon'); - - assert.deepEqual(polygon.geometry.coordinates, [[[100, 200], [100, 200], [100, 200]]], 'starting coordinate added'); - }); - - await t.test('tap to add another vertex', () => { - touchTap(map, makeTouchEvent(135, 135)); - const polygon = Draw.getAll().features[0]; - assert.deepEqual(polygon.geometry.coordinates, [[[100, 200], [135, 135], [135, 135], [100, 200]]], 'middle coordinate replaced'); - }); - - await t.test('add more points then tap on the last vertex to finish', () => { - touchTap(map, makeTouchEvent(400, 400)); - touchTap(map, makeTouchEvent(500, 500)); - touchTap(map, makeTouchEvent(550, 550)); - touchTap(map, makeTouchEvent(550, 550)); - const polygon = Draw.getAll().features[0]; - assert.deepEqual(polygon.geometry.coordinates, - [[[100, 200], [135, 135], [400, 400], [500, 500], [550, 550], [100, 200]]], - 'all coordinates in place'); - - touchTap(map, makeTouchEvent(400, 400)); - assert.deepEqual(polygon.geometry.coordinates, - [[[100, 200], [135, 135], [400, 400], [500, 500], [550, 550], [100, 200]]], - 'since we exited draw_polygon mode, another tap does not add a coordinate'); - }); - - await t.test('start a polygon but trash it before completion', () => { - // Start a new polygon - Draw.deleteAll(); - Draw.changeMode('draw_polygon'); - touchTap(map, makeTouchEvent(100, 100)); - touchTap(map, makeTouchEvent(200, 200)); - touchTap(map, makeTouchEvent(300, 300)); - - const polygon = Draw.getAll().features[0]; - assert.deepEqual(polygon.geometry.coordinates, [[[100, 100], [200, 200], [300, 300], [300, 300], [100, 100]]]); - - Draw.trash(); - assert.equal(Draw.getAll().features.length, 0, 'no feature added'); - - touchTap(map, makeTouchEvent(1, 1)); - assert.equal(Draw.getAll().features.length, 0, 'no longer drawing'); - }); - - document.body.removeChild(container); -}); diff --git a/test/draw_polygon.test.ts b/test/draw_polygon.test.ts new file mode 100644 index 00000000..58406ce4 --- /dev/null +++ b/test/draw_polygon.test.ts @@ -0,0 +1,1018 @@ +import './mock-browser'; +import { spy } from 'sinon'; +import test from 'node:test'; +import assert from 'node:assert/strict'; +import MapboxDraw from '../index'; +import createMap from './utils/create_map'; +import mouseClick from './utils/mouse_click'; +import touchTap from './utils/touch_tap'; +import makeMouseEvent from './utils/make_mouse_event'; +import makeTouchEvent from './utils/make_touch_event'; +import drawPolygonModeObject from '../src/modes/draw_polygon'; +import Polygon from '../src/feature_types/polygon'; +import createMockDrawModeContext from './utils/create_mock_draw_mode_context'; +import createMockLifecycleContext from './utils/create_mock_lifecycle_context'; +import { setupAfterNextRender } from './utils/after_next_render'; +import { objectToMode } from '../src/modes/object_to_mode'; +const drawPolygonMode = objectToMode(drawPolygonModeObject); + +import { + enterEvent, + startPointEvent, + startLineStringEvent, + startPolygonEvent, + escapeEvent +} from './utils/key_events'; + +import type { StrictFeature } from '../src/types/types'; + +test('draw_polygon mode initialization', () => { + const context = createMockDrawModeContext() as spy; + const mode = drawPolygonMode(context); + const lifecycleContext = createMockLifecycleContext(); + mode.start.call(lifecycleContext); + + assert.equal(context.store.add.callCount, 1, 'store.add called'); + + const emptyPolygon = new Polygon(context, { + type: 'Feature', + properties: {}, + geometry: { + type: 'Polygon', + coordinates: [[]] + } + }); + // Strip ids for this comparison + assert.deepEqual( + Object.assign({}, context.store.add.getCall(0).args[0], { id: null }), + Object.assign({}, emptyPolygon, { id: null }), + 'with a new polygon' + ); +}); + +test('draw_polygon start', async () => { + const context = createMockDrawModeContext() as spy; + const lifecycleContext = createMockLifecycleContext(); + const mode = drawPolygonMode(context); + + mode.start.call(lifecycleContext); + assert.equal( + context.store.clearSelected.callCount, + 1, + 'store.clearSelected called' + ); + assert.equal( + context.ui.queueMapClasses.callCount, + 1, + 'ui.queueMapClasses called' + ); + assert.deepEqual( + context.ui.queueMapClasses.getCall(0).args, + [{ mouse: 'add' }], + 'ui.queueMapClasses received correct arguments' + ); + assert.equal( + context.ui.setActiveButton.callCount, + 1, + 'ui.setActiveButton called' + ); + assert.deepEqual( + context.ui.setActiveButton.getCall(0).args, + ['polygon'], + 'ui.setActiveButton received correct arguments' + ); + + await new Promise(resolve => setTimeout(resolve, 10)); + assert.equal(context.map.doubleClickZoom.disable.callCount, 1); +}); + +test('draw_polygon stop with valid polygon', () => { + const context = createMockDrawModeContext() as spy; + const mode = drawPolygonMode(context); + const lifecycleContext = createMockLifecycleContext(); + mode.start.call(lifecycleContext); + + // Fake a valid polygon + const testPolygon = context.store.get(context.store.getAllIds()[0]); + testPolygon.isValid = () => true; + + (mode.stop as spy).call(); + assert.equal( + context.ui.setActiveButton.callCount, + 2, + 'ui.setActiveButton called' + ); + assert.deepEqual( + context.ui.setActiveButton.getCall(1).args, + [undefined], + 'ui.setActiveButton received correct arguments' + ); + assert.equal(context.store.delete.callCount, 0, 'store.delete not called'); +}); + +test('draw_polygon stop with invalid polygon', () => { + const context = createMockDrawModeContext() as spy; + const mode = drawPolygonMode(context); + const lifecycleContext = createMockLifecycleContext(); + mode.start.call(lifecycleContext); + + // Fake an invalid polygon + const testPolygon = context.store.get(context.store.getAllIds()[0]); + testPolygon.isValid = () => false; + + (mode.stop as spy).call(); + assert.equal( + context.ui.setActiveButton.callCount, + 2, + 'ui.setActiveButton called' + ); + assert.deepEqual( + context.ui.setActiveButton.getCall(1).args, + [undefined], + 'ui.setActiveButton received correct arguments' + ); + assert.equal(context.store.delete.callCount, 1, 'store.delete called'); + assert.deepEqual( + context.store.delete.getCall(0).args, + [[testPolygon.id], { silent: true }], + 'store.delete received correct arguments' + ); +}); + +test('draw_polygon render active polygon with no coordinates', () => { + const context = createMockDrawModeContext() as spy; + const mode = drawPolygonMode(context); + const lifecycleContext = createMockLifecycleContext(); + mode.start.call(lifecycleContext); + const testPolygon = context.store.get(context.store.getAllIds()[0]); + + const memo = []; + const geojson = { + type: 'Feature', + properties: { + id: testPolygon.id + }, + geometry: { + type: 'Polygon', + coordinates: [] + } + }; + mode.render(geojson as StrictFeature, x => memo.push(x)); + assert.equal(memo.length, 0, 'does not render'); +}); + +test('draw_polygon render active polygon with 1 coordinate (and closer)', () => { + const context = createMockDrawModeContext() as spy; + const mode = drawPolygonMode(context); + const lifecycleContext = createMockLifecycleContext(); + mode.start.call(lifecycleContext); + const testPolygon = context.store.get(context.store.getAllIds()[0]); + + const memo = []; + const geojson = { + type: 'Feature', + properties: { + id: testPolygon.id + }, + geometry: { + type: 'Polygon', + coordinates: [ + [ + [10, 10], + [10, 10] + ] + ] + } + }; + mode.render(geojson as StrictFeature, x => memo.push(x)); + assert.equal(memo.length, 0, 'does not render'); +}); + +test('draw_polygon render active polygon with 2 coordinates (and closer)', () => { + const context = createMockDrawModeContext() as spy; + const mode = drawPolygonMode(context); + const lifecycleContext = createMockLifecycleContext(); + mode.start.call(lifecycleContext); + const testPolygon = context.store.get(context.store.getAllIds()[0]); + + const memo = []; + const geojson = { + type: 'Feature', + properties: { + id: testPolygon.id + }, + geometry: { + type: 'Polygon', + coordinates: [ + [ + [0, 0], + [0, 10], + [0, 0] + ] + ] + } + }; + mode.render(geojson as StrictFeature, x => memo.push(x)); + assert.equal(memo.length, 2, 'does render'); + assert.deepEqual( + memo[1], + { + type: 'Feature', + properties: { + id: testPolygon.id, + active: 'true', + meta: 'feature' + }, + geometry: { + type: 'LineString', + coordinates: [ + [0, 0], + [0, 10] + ] + } + }, + 'renders as a LineString with meta: feature, active: true' + ); +}); + +test('draw_polygon render active polygon with 3 coordinates (and closer)', () => { + const context = createMockDrawModeContext() as spy; + const mode = drawPolygonMode(context); + const lifecycleContext = createMockLifecycleContext(); + mode.start.call(lifecycleContext); + const testPolygon = context.store.get(context.store.getAllIds()[0]); + + const memo = []; + const geojson = { + type: 'Feature', + properties: { + id: testPolygon.id + }, + geometry: { + type: 'Polygon', + coordinates: [ + [ + [0, 0], + [0, 10], + [10, 10], + [10, 0], + [0, 0] + ] + ] + } + }; + mode.render(geojson as StrictFeature, x => memo.push(x)); + assert.equal(memo.length, 3, 'does render'); + assert.deepEqual( + memo[0], + { + type: 'Feature', + properties: { + parent: testPolygon.id, + meta: 'vertex', + coord_path: '0.0', + active: 'false' + }, + geometry: { + type: 'Point', + coordinates: [0, 0] + } + }, + 'renders the start point point with meta: vertex' + ); + assert.deepEqual( + memo[1], + { + type: 'Feature', + properties: { + parent: testPolygon.id, + meta: 'vertex', + coord_path: '0.2', + active: 'false' + }, + geometry: { + type: 'Point', + coordinates: [10, 10] + } + }, + 'renders end point with meta: vertex' + ); + assert.deepEqual( + memo[2], + { + type: 'Feature', + properties: { + id: testPolygon.id, + active: 'true', + meta: 'feature' + }, + geometry: { + type: 'Polygon', + coordinates: [ + [ + [0, 0], + [0, 10], + [10, 10], + [10, 0], + [0, 0] + ] + ] + } + }, + 'renders as a polygon with meta: feature, active: true' + ); +}); + +test('draw_polygon render inactive feature', () => { + const context = createMockDrawModeContext() as spy; + const mode = drawPolygonMode(context); + const lifecycleContext = createMockLifecycleContext(); + mode.start.call(lifecycleContext); + + const memo = []; + const geojson = { + type: 'Feature', + properties: { + meta: 'meh' + }, + geometry: { + type: 'LineString', + coordinates: [ + [0, 0], + [0, 10] + ] + } + }; + mode.render(geojson as StrictFeature, x => memo.push(x)); + assert.equal(memo.length, 1, 'does render'); + assert.deepEqual( + memo[0], + { + type: 'Feature', + properties: { + active: 'false', + meta: 'meh' + }, + geometry: { + type: 'LineString', + coordinates: [ + [0, 0], + [0, 10] + ] + } + }, + 'unaltered except active: false' + ); +}); + +test('draw_polygon mouse interaction', async t => { + const container = document.createElement('div'); + document.body.appendChild(container); + const map = createMap({ container }); + const Draw = new MapboxDraw(); + map.addControl(Draw); + + await map.on('load'); + + // The following sub-tests share state ... + const afterNextRender = setupAfterNextRender(map); + + Draw.changeMode('draw_polygon'); + t.test('first click', () => { + mouseClick(map, makeMouseEvent(10, 20)); + + const { features } = Draw.getAll(); + assert.equal(features.length, 1, 'polygon created'); + const polygon = Draw.getAll().features[0]; + assert.equal(polygon.geometry.type, 'Polygon'); + + assert.deepEqual( + polygon.geometry.coordinates, + [ + [ + [10, 20], + [10, 20], + [10, 20] + ] + ], + 'starting coordinate added' + ); + }); + + await t.test('move mouse', () => { + map.fire('mousemove', makeMouseEvent(15, 23)); + const polygon = Draw.getAll().features[0]; + assert.deepEqual( + polygon.geometry.coordinates, + [ + [ + [10, 20], + [15, 23], + [10, 20] + ] + ], + 'middle coordinate added' + ); + }); + + await t.test('move mouse again', () => { + map.fire('mousemove', makeMouseEvent(30, 33)); + const polygon = Draw.getAll().features[0]; + assert.deepEqual( + polygon.geometry.coordinates, + [ + [ + [10, 20], + [30, 33], + [10, 20] + ] + ], + 'middle coordinate replaced' + ); + }); + + await t.test('click to add another vertex', () => { + mouseClick(map, makeMouseEvent(35, 35)); + const polygon = Draw.getAll().features[0]; + assert.deepEqual( + polygon.geometry.coordinates, + [ + [ + [10, 20], + [35, 35], + [35, 35], + [10, 20] + ] + ], + 'middle coordinate replaced' + ); + }); + + t.test('add more points then click on the last vertex to finish', () => { + mouseClick(map, makeMouseEvent(40, 40)); + mouseClick(map, makeMouseEvent(50, 50)); + mouseClick(map, makeMouseEvent(55, 55)); + mouseClick(map, makeMouseEvent(55, 55)); + const polygon = Draw.getAll().features[0]; + assert.deepEqual( + polygon.geometry.coordinates, + [ + [ + [10, 20], + [35, 35], + [40, 40], + [50, 50], + [55, 55], + [10, 20] + ] + ], + 'all coordinates in place' + ); + + mouseClick(map, makeMouseEvent(40, 40)); + assert.deepEqual( + polygon.geometry.coordinates, + [ + [ + [10, 20], + [35, 35], + [40, 40], + [50, 50], + [55, 55], + [10, 20] + ] + ], + 'since we exited draw_polygon mode, another click does not add a coordinate' + ); + }); + + await t.test('start a polygon but trash it before completion', () => { + // Start a new polygon + Draw.deleteAll(); + Draw.changeMode('draw_polygon'); + mouseClick(map, makeMouseEvent(1, 1)); + mouseClick(map, makeMouseEvent(2, 2)); + mouseClick(map, makeMouseEvent(3, 3)); + + const polygon = Draw.getAll().features[0]; + assert.deepEqual(polygon.geometry.coordinates, [ + [ + [1, 1], + [2, 2], + [3, 3], + [3, 3], + [1, 1] + ] + ]); + + Draw.trash(); + assert.equal(Draw.getAll().features.length, 0, 'no feature added'); + + mouseClick(map, makeMouseEvent(1, 1)); + assert.equal(Draw.getAll().features.length, 0, 'no longer drawing'); + }); + + await t.test( + 'start a polygon but trash it with Escape before completion', + () => { + // Start a new polygon + Draw.deleteAll(); + Draw.changeMode('draw_polygon'); + mouseClick(map, makeMouseEvent(1, 1)); + mouseClick(map, makeMouseEvent(2, 2)); + mouseClick(map, makeMouseEvent(3, 3)); + + const polygon = Draw.getAll().features[0]; + assert.deepEqual(polygon.geometry.coordinates, [ + [ + [1, 1], + [2, 2], + [3, 3], + [3, 3], + [1, 1] + ] + ]); + + container.dispatchEvent(escapeEvent); + + assert.equal(Draw.getAll().features.length, 0, 'no feature added'); + + mouseClick(map, makeMouseEvent(1, 1)); + assert.equal(Draw.getAll().features.length, 0, 'no longer drawing'); + } + ); + + // ZERO CLICK TESTS + + await t.test('start a polygon and end it with Enter', () => { + // Start a new polygon + Draw.deleteAll(); + Draw.changeMode('draw_polygon'); + mouseClick(map, makeMouseEvent(1, 1)); + mouseClick(map, makeMouseEvent(2, 2)); + mouseClick(map, makeMouseEvent(3, 3)); + + const polygon = Draw.getAll().features[0]; + assert.deepEqual(polygon.geometry.coordinates, [ + [ + [1, 1], + [2, 2], + [3, 3], + [3, 3], + [1, 1] + ] + ]); + + container.dispatchEvent(enterEvent); + + assert.equal(Draw.getAll().features.length, 1, 'the feature was added'); + assert.deepEqual( + Draw.getAll().features[0].geometry.coordinates, + [ + [ + [1, 1], + [2, 2], + [3, 3], + [1, 1] + ] + ], + 'the polygon is correct' + ); + + mouseClick(map, makeMouseEvent(1, 1)); + assert.equal(Draw.getAll().features.length, 1, 'no longer drawing'); + }); + + await t.test('start draw_polygon mode then changemode before a click', () => { + Draw.deleteAll(); + assert.equal(Draw.getAll().features.length, 0, 'no features yet'); + + Draw.changeMode('draw_polygon'); + assert.equal(Draw.getAll().features.length, 1, 'polygon is added'); + const polygon = Draw.getAll().features[0]; + assert.deepEqual(polygon.geometry.type, 'Polygon'); + + Draw.changeMode('simple_select'); + assert.equal(Draw.getAll().features.length, 0, 'polygon is removed'); + }); + + // ONE CLICK TESTS + + await t.test('start draw_polygon mode then enter after one click', () => { + Draw.deleteAll(); + assert.equal(Draw.getAll().features.length, 0, 'no features yet'); + + Draw.changeMode('draw_polygon'); + assert.equal(Draw.getAll().features.length, 1, 'polygon is added'); + mouseClick(map, makeMouseEvent(1, 1)); + map.fire('mousemove', makeMouseEvent(16, 16)); + + const polygon = Draw.getAll().features[0]; + assert.deepEqual( + polygon.geometry.coordinates, + [ + [ + [1, 1], + [16, 16], + [1, 1] + ] + ], + 'and has right coordinates' + ); + + container.dispatchEvent(enterEvent); + assert.equal(Draw.getAll().features.length, 0, 'polygon was removed'); + }); + + await t.test( + 'start draw_polygon mode then start a point after one click', + () => { + Draw.deleteAll(); + assert.equal(Draw.getAll().features.length, 0, 'no features yet'); + + Draw.changeMode('draw_polygon'); + assert.equal(Draw.getAll().features.length, 1, 'polygon is added'); + mouseClick(map, makeMouseEvent(1, 1)); + map.fire('mousemove', makeMouseEvent(16, 16)); + + const polygon = Draw.getAll().features[0]; + assert.deepEqual( + polygon.geometry.coordinates, + [ + [ + [1, 1], + [16, 16], + [1, 1] + ] + ], + 'and has right coordinates' + ); + + container.dispatchEvent(startPointEvent); + assert.equal(Draw.get(polygon.id), undefined, 'polygon was removed'); + } + ); + + await t.test( + 'start draw_polygon mode then start a line_string after one click', + () => { + Draw.deleteAll(); + assert.equal(Draw.getAll().features.length, 0, 'no features yet'); + + Draw.changeMode('draw_polygon'); + assert.equal(Draw.getAll().features.length, 1, 'polygon is added'); + mouseClick(map, makeMouseEvent(1, 1)); + map.fire('mousemove', makeMouseEvent(16, 16)); + + const polygon = Draw.getAll().features[0]; + assert.deepEqual( + polygon.geometry.coordinates, + [ + [ + [1, 1], + [16, 16], + [1, 1] + ] + ], + 'and has right coordinates' + ); + + container.dispatchEvent(startLineStringEvent); + assert.equal(Draw.get(polygon.id), undefined, 'polygon was removed'); + } + ); + + await t.test( + 'start draw_polygon mode then start a new polygon after one click', + () => { + Draw.deleteAll(); + assert.equal(Draw.getAll().features.length, 0, 'no features yet'); + + Draw.changeMode('draw_polygon'); + assert.equal(Draw.getAll().features.length, 1, 'polygon is added'); + mouseClick(map, makeMouseEvent(1, 1)); + map.fire('mousemove', makeMouseEvent(16, 16)); + + const polygon = Draw.getAll().features[0]; + assert.deepEqual( + polygon.geometry.coordinates, + [ + [ + [1, 1], + [16, 16], + [1, 1] + ] + ], + 'and has right coordinates' + ); + + container.dispatchEvent(startPolygonEvent); + assert.equal(Draw.get(polygon.id), undefined, 'polygon was removed'); + } + ); + + await t.test('start draw_polygon mode then doubleclick', () => { + Draw.deleteAll(); + assert.equal(Draw.getAll().features.length, 0, 'no features yet'); + + Draw.changeMode('draw_polygon'); + assert.equal(Draw.getAll().features.length, 1, 'polygon is added'); + mouseClick(map, makeMouseEvent(1, 1)); + mouseClick(map, makeMouseEvent(1, 1)); + + assert.equal(Draw.getAll().features.length, 0, 'polygon was removed'); + }); + + // TWO CLICK TESTS + + t.test('start draw_polygon mode then enter after two clicks', () => { + Draw.deleteAll(); + assert.equal(Draw.getAll().features.length, 0, 'no features yet'); + + Draw.changeMode('draw_polygon'); + assert.equal(Draw.getAll().features.length, 1, 'polygon is added'); + mouseClick(map, makeMouseEvent(1, 1)); + mouseClick(map, makeMouseEvent(16, 16)); + map.fire('mousemove', makeMouseEvent(8, 0)); + + const polygon = Draw.getAll().features[0]; + assert.deepEqual( + polygon.geometry.coordinates, + [ + [ + [1, 1], + [16, 16], + [8, 0], + [1, 1] + ] + ], + 'and has right coordinates' + ); + + container.dispatchEvent(enterEvent); + assert.equal(Draw.getAll().features.length, 0, 'polygon was removed'); + }); + + await t.test( + 'start draw_polygon mode then start a point after two clicks', + () => { + Draw.deleteAll(); + assert.equal(Draw.getAll().features.length, 0, 'no features yet'); + + Draw.changeMode('draw_polygon'); + assert.equal(Draw.getAll().features.length, 1, 'polygon is added'); + mouseClick(map, makeMouseEvent(1, 1)); + mouseClick(map, makeMouseEvent(16, 16)); + map.fire('mousemove', makeMouseEvent(8, 0)); + + const polygon = Draw.getAll().features[0]; + assert.deepEqual( + polygon.geometry.coordinates, + [ + [ + [1, 1], + [16, 16], + [8, 0], + [1, 1] + ] + ], + 'and has right coordinates' + ); + + container.dispatchEvent(startPointEvent); + assert.equal(Draw.get(polygon.id), undefined, 'polygon was removed'); + } + ); + + await t.test( + 'start draw_polygon mode then start a line_string after two clicks', + () => { + Draw.deleteAll(); + assert.equal(Draw.getAll().features.length, 0, 'no features yet'); + + Draw.changeMode('draw_polygon'); + assert.equal(Draw.getAll().features.length, 1, 'polygon is added'); + mouseClick(map, makeMouseEvent(1, 1)); + mouseClick(map, makeMouseEvent(16, 16)); + map.fire('mousemove', makeMouseEvent(8, 0)); + + const polygon = Draw.getAll().features[0]; + assert.deepEqual( + polygon.geometry.coordinates, + [ + [ + [1, 1], + [16, 16], + [8, 0], + [1, 1] + ] + ], + 'and has right coordinates' + ); + + container.dispatchEvent(startLineStringEvent); + assert.equal(Draw.get(polygon.id), undefined, 'polygon was removed'); + } + ); + + await t.test( + 'start draw_polygon mode then start a new polygon after two clicks', + () => { + Draw.deleteAll(); + assert.equal(Draw.getAll().features.length, 0, 'no features yet'); + + Draw.changeMode('draw_polygon'); + assert.equal(Draw.getAll().features.length, 1, 'polygon is added'); + mouseClick(map, makeMouseEvent(1, 1)); + mouseClick(map, makeMouseEvent(16, 16)); + map.fire('mousemove', makeMouseEvent(8, 0)); + + const polygon = Draw.getAll().features[0]; + assert.deepEqual( + polygon.geometry.coordinates, + [ + [ + [1, 1], + [16, 16], + [8, 0], + [1, 1] + ] + ], + 'and has right coordinates' + ); + + container.dispatchEvent(startPolygonEvent); + assert.equal(Draw.get(polygon.id), undefined, 'polygon was removed'); + } + ); + + t.test('start draw_polygon mode then doubleclick', () => { + Draw.deleteAll(); + assert.equal(Draw.getAll().features.length, 0, 'no features yet'); + + Draw.changeMode('draw_polygon'); + assert.equal(Draw.getAll().features.length, 1, 'polygon is added'); + mouseClick(map, makeMouseEvent(1, 1)); + mouseClick(map, makeMouseEvent(16, 16)); + mouseClick(map, makeMouseEvent(16, 16)); + + assert.equal(Draw.getAll().features.length, 0, 'polygon was removed'); + }); + + // FIVE CLICK TEST + + await t.test( + 'end draw_polygon mode by clicking on the start point', + async () => { + Draw.deleteAll(); + assert.equal(Draw.getAll().features.length, 0, 'no features yet'); + Draw.changeMode('draw_polygon'); + let polygon = Draw.getAll().features[0]; + assert.equal(polygon !== undefined, true, 'polygon is added'); + mouseClick(map, makeMouseEvent(0, 0)); + mouseClick(map, makeMouseEvent(20, 0)); + mouseClick(map, makeMouseEvent(20, 20)); + mouseClick(map, makeMouseEvent(0, 20)); + + await afterNextRender(); + + map.fire('mousemove', makeMouseEvent(0, 0)); + mouseClick(map, makeMouseEvent(0, 0)); + + polygon = Draw.get(polygon.id); + assert.deepEqual( + polygon.geometry.coordinates, + [ + [ + [0, 0], + [20, 0], + [20, 20], + [0, 20], + [0, 0] + ] + ], + 'and has right coordinates' + ); + Draw.deleteAll(); + } + ); + + document.body.removeChild(container); +}); + +test('draw_polygon touch interaction', async t => { + const container = document.createElement('div'); + document.body.appendChild(container); + const map = createMap({ container }); + const Draw = new MapboxDraw(); + map.addControl(Draw); + + await map.on('load'); + + // The following sub-tests share state ... + + Draw.changeMode('draw_polygon'); + t.test('first tap', () => { + touchTap(map, makeTouchEvent(100, 200)); + + const { features } = Draw.getAll(); + assert.equal(features.length, 1, 'polygon created'); + const polygon = Draw.getAll().features[0]; + assert.equal(polygon.geometry.type, 'Polygon'); + + assert.deepEqual( + polygon.geometry.coordinates, + [ + [ + [100, 200], + [100, 200], + [100, 200] + ] + ], + 'starting coordinate added' + ); + }); + + await t.test('tap to add another vertex', () => { + touchTap(map, makeTouchEvent(135, 135)); + const polygon = Draw.getAll().features[0]; + assert.deepEqual( + polygon.geometry.coordinates, + [ + [ + [100, 200], + [135, 135], + [135, 135], + [100, 200] + ] + ], + 'middle coordinate replaced' + ); + }); + + await t.test('add more points then tap on the last vertex to finish', () => { + touchTap(map, makeTouchEvent(400, 400)); + touchTap(map, makeTouchEvent(500, 500)); + touchTap(map, makeTouchEvent(550, 550)); + touchTap(map, makeTouchEvent(550, 550)); + const polygon = Draw.getAll().features[0]; + assert.deepEqual( + polygon.geometry.coordinates, + [ + [ + [100, 200], + [135, 135], + [400, 400], + [500, 500], + [550, 550], + [100, 200] + ] + ], + 'all coordinates in place' + ); + + touchTap(map, makeTouchEvent(400, 400)); + assert.deepEqual( + polygon.geometry.coordinates, + [ + [ + [100, 200], + [135, 135], + [400, 400], + [500, 500], + [550, 550], + [100, 200] + ] + ], + 'since we exited draw_polygon mode, another tap does not add a coordinate' + ); + }); + + await t.test('start a polygon but trash it before completion', () => { + // Start a new polygon + Draw.deleteAll(); + Draw.changeMode('draw_polygon'); + touchTap(map, makeTouchEvent(100, 100)); + touchTap(map, makeTouchEvent(200, 200)); + touchTap(map, makeTouchEvent(300, 300)); + + const polygon = Draw.getAll().features[0]; + assert.deepEqual(polygon.geometry.coordinates, [ + [ + [100, 100], + [200, 200], + [300, 300], + [300, 300], + [100, 100] + ] + ]); + + Draw.trash(); + assert.equal(Draw.getAll().features.length, 0, 'no feature added'); + + touchTap(map, makeTouchEvent(1, 1)); + assert.equal(Draw.getAll().features.length, 0, 'no longer drawing'); + }); + + document.body.removeChild(container); +}); diff --git a/test/euclidean_distance.test.js b/test/euclidean_distance.test.js deleted file mode 100644 index 461c0d82..00000000 --- a/test/euclidean_distance.test.js +++ /dev/null @@ -1,8 +0,0 @@ -import test from 'node:test'; -import assert from 'node:assert/strict'; -import euclideanDistance from '../src/lib/euclidean_distance.js'; - -test('euclideanDistance', () => { - assert.equal(euclideanDistance({ x: 1, y: 1 }, { x: 4, y: 4 }), 4.242640687119285); - assert.equal(euclideanDistance({ x: -10, y: 99.486354 }, { x: 0, y: -0.324736 }), 100.31078549681536); -}); diff --git a/test/euclidean_distance.test.ts b/test/euclidean_distance.test.ts new file mode 100644 index 00000000..e4cde7ef --- /dev/null +++ b/test/euclidean_distance.test.ts @@ -0,0 +1,15 @@ +import './mock-browser'; +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { euclideanDistance } from '../src/lib/euclidean_distance'; + +test('euclideanDistance', () => { + assert.equal( + euclideanDistance({ x: 1, y: 1 }, { x: 4, y: 4 }), + 4.242640687119285 + ); + assert.equal( + euclideanDistance({ x: -10, y: 99.486354 }, { x: 0, y: -0.324736 }), + 100.31078549681536 + ); +}); diff --git a/test/feature.test.js b/test/feature.test.ts similarity index 64% rename from test/feature.test.js rename to test/feature.test.ts index 01bbfbb7..c4d71c80 100644 --- a/test/feature.test.js +++ b/test/feature.test.ts @@ -1,11 +1,12 @@ +import './mock-browser'; +import { spy } from 'sinon'; import test from 'node:test'; import assert from 'node:assert/strict'; - -import {spy} from 'sinon'; -import Feature from '../src/feature_types/feature.js'; -import createFeature from './utils/create_feature.js'; -import getPublicMemberKeys from './utils/get_public_member_keys.js'; -import createMockCtx from './utils/create_mock_feature_context.js'; +import Feature from '../src/feature_types/feature'; +import createFeature from './utils/create_feature'; +import getPublicMemberKeys from './utils/get_public_member_keys'; +import createMockCtx from './utils/create_mock_feature_context'; +import type { StrictFeature } from '../src/types/types'; test('Feature contrusctor and API', () => { const featureGeoJson = createFeature('line'); @@ -14,21 +15,61 @@ test('Feature contrusctor and API', () => { // Instance members assert.equal(feature.ctx, ctx, 'feature.ctx'); - assert.equal(feature.coordinates, featureGeoJson.geometry.coordinates, 'feature.coordinates'); - assert.equal(feature.properties, featureGeoJson.properties, 'feature.properties'); + assert.equal( + feature.coordinates, + featureGeoJson.geometry.coordinates, + 'feature.coordinates' + ); + assert.equal( + feature.properties, + featureGeoJson.properties, + 'feature.properties' + ); assert.equal(feature.id, featureGeoJson.id, 'feature.id'); assert.equal(feature.type, featureGeoJson.geometry.type, 'feature.type'); - assert.equal(getPublicMemberKeys(feature).length, 5, 'no unexpected instance members'); + assert.equal( + getPublicMemberKeys(feature).length, + 5, + 'no unexpected instance members' + ); // Prototype members assert.equal(typeof Feature.prototype.changed, 'function', 'feature.changed'); - assert.equal(typeof Feature.prototype.incomingCoords, 'function', 'feature.incomingCoords'); - assert.equal(typeof Feature.prototype.setCoordinates, 'function', 'feature.setCoordinates'); - assert.equal(typeof Feature.prototype.getCoordinates, 'function', 'feature.getCoordinates'); - assert.equal(typeof Feature.prototype.toGeoJSON, 'function', 'feature.toGeoJSON'); - assert.equal(typeof Feature.prototype.internal, 'function', 'feature.internal'); - assert.equal(typeof Feature.prototype.setProperty, 'function', 'feature.setProperty'); - assert.equal(getPublicMemberKeys(Feature.prototype).length, 7, 'no unexpected prototype members'); + assert.equal( + typeof Feature.prototype.incomingCoords, + 'function', + 'feature.incomingCoords' + ); + assert.equal( + typeof Feature.prototype.setCoordinates, + 'function', + 'feature.setCoordinates' + ); + assert.equal( + typeof Feature.prototype.getCoordinates, + 'function', + 'feature.getCoordinates' + ); + assert.equal( + typeof Feature.prototype.toGeoJSON, + 'function', + 'feature.toGeoJSON' + ); + assert.equal( + typeof Feature.prototype.internal, + 'function', + 'feature.internal' + ); + assert.equal( + typeof Feature.prototype.setProperty, + 'function', + 'feature.setProperty' + ); + assert.equal( + getPublicMemberKeys(Feature.prototype).length, + 7, + 'no unexpected prototype members' + ); const simpleFeatureGeoJson = { type: 'Feature', @@ -36,9 +77,14 @@ test('Feature contrusctor and API', () => { type: 'Point', coordinates: [0, 0] } - }; + } as StrictFeature; + const featureWithDefaultsOnly = new Feature(ctx, simpleFeatureGeoJson); - assert.deepEqual(featureWithDefaultsOnly.properties, {}, 'feature.properties defaults to {}'); + assert.deepEqual( + featureWithDefaultsOnly.properties, + {}, + 'feature.properties defaults to {}' + ); assert.ok(featureWithDefaultsOnly.id, 'feature.id is provided'); }); @@ -47,10 +93,18 @@ test('Feature#changed', () => { const featureGeoJson = createFeature('point'); const feature = new Feature(ctx, featureGeoJson); - ctx.store.featureChanged.resetHistory(); + (ctx.store.featureChanged as spy).resetHistory(); feature.changed(); - assert.equal(ctx.store.featureChanged.callCount, 1, 'called function on store'); - assert.deepEqual(ctx.store.featureChanged.getCall(0).args[0], featureGeoJson.id, 'with correct args'); + assert.equal( + (ctx.store.featureChanged as spy).callCount, + 1, + 'called function on store' + ); + assert.deepEqual( + (ctx.store.featureChanged as spy).getCall(0).args[0], + featureGeoJson.id, + 'with correct args' + ); }); test('Feature#incomingCoords', () => { @@ -63,7 +117,6 @@ test('Feature#incomingCoords', () => { feature.incomingCoords([1, 2]); assert.deepEqual(feature.coordinates, [1, 2]); assert.equal(changedSpy.callCount, 1); - }); test('Feature#setCoordinates, Feature#setCoordinates', () => { @@ -79,7 +132,6 @@ test('Feature#setCoordinates, Feature#setCoordinates', () => { assert.deepEqual(feature.coordinates, [1, 2]); assert.deepEqual(feature.getCoordinates(), [1, 2]); assert.equal(changedSpy.callCount, 1); - }); test('Feature#toGeoJSON', () => { @@ -95,11 +147,10 @@ test('Feature#toGeoJSON', () => { type: feature.type } }); - }); test('Feature#internal - when userProperties is true', () => { - const ctx = createMockCtx({userProperties: true}); + const ctx = createMockCtx({ userProperties: true }); const polygon = createFeature('polygon'); const feature = new Feature(ctx, polygon); assert.deepEqual(feature.internal('foo'), { @@ -118,11 +169,9 @@ test('Feature#internal - when userProperties is true', () => { type: feature.type } }); - - }); test('Feature#internal - when userProperties is false', () => { - const ctx = createMockCtx({userProperties: false}); + const ctx = createMockCtx({ userProperties: false }); const polygon = createFeature('polygon'); const feature = new Feature(ctx, polygon); assert.deepEqual(feature.internal('foo'), { @@ -139,7 +188,6 @@ test('Feature#internal - when userProperties is false', () => { type: feature.type } }); - }); test('Feature#setProperty', () => { @@ -148,5 +196,4 @@ test('Feature#setProperty', () => { const feature = new Feature(ctx, polygon); feature.setProperty('size', 200); assert.equal(feature.properties.size, 200); - }); diff --git a/test/features_at.test.js b/test/features_at.test.js deleted file mode 100644 index 1209425e..00000000 --- a/test/features_at.test.js +++ /dev/null @@ -1,246 +0,0 @@ -import test from 'node:test'; -import assert from 'node:assert/strict'; -import featuresAt from '../src/lib/features_at.js'; -import styles from '../src/lib/theme.js'; -import * as Constants from '../src/constants.js'; -import setupOptions from '../src/options.js'; - -/** - * Mock of the addLayers function in setup - */ -function addLayers(ctx) { - // drawn features style - ctx.map.addSource(Constants.sources.COLD, { - data: { - type: Constants.geojsonTypes.FEATURE_COLLECTION, - features: [] - }, - type: 'geojson' - }); - - // hot features style - ctx.map.addSource(Constants.sources.HOT, { - data: { - type: Constants.geojsonTypes.FEATURE_COLLECTION, - features: [{ - type: 'Feature', - properties: { - meta: 'feature', - id: 'foo' - }, - geometry: { - type: 'LineString', - coordinates: [[0, 0], [1, 1], [2, 2]] - } - }, { - type: 'Feature', - properties: { - meta: 'nothing', - id: 'bar' - }, - geometry: { - type: 'Polygon', - coordinates: [[[1, 1], [2, 2], [3, 3], [4, 4], [1, 1]]] - } - }, { - type: 'Feature', - properties: { - meta: 'vertex', - id: 'baz' - }, - geometry: { - type: 'Point', - coordinates: [10, 10] - } - }, { - type: 'Feature', - properties: { - meta: 'vertex', - id: 'baz' - }, - geometry: { - type: 'Point', - coordinates: [10, 10] - } - }] - }, - type: 'geojson' - }); - - ctx.options.styles.forEach((style) => { - ctx.map.addLayer(style); - }); -} - -/** - * Mock context with a simplified mapbox-gl-js map (including some source/style/layer interactions) - */ -function createMockContext() { - const _layers = {}; - const sources = {}; - const addSource = (id, source) => { - _layers[id] = source; - }; - const style = { - _layers, - getLayer: id => _layers[id], - addLayer: ((layerObject) => { - addSource(layerObject.id, layerObject); - }), - addSource, - removeSource: (id) => { - delete _layers[id]; - }, - queryRenderedFeatures: (bbox, params) => { - const features = []; - const includedSources = {}; - if (params && params.layers) { - for (const layerId of params.layers) { - const layer = _layers[layerId]; - if (!layer) { - // this layer is not in the style.layers array - throw new ErrorEvent(new Error(`The layer '${layerId}' does not exist in the map's style and cannot be queried for features.`)); - } - includedSources[layer.source] = true; - } - } - Object.keys(includedSources).filter(source => includedSources[source] != null).forEach((source) => { - if (sources[source] && sources[source].data) { - features.push(...sources[source].data.features); - } - }); - return features; - } - }; - - const context = { - options: { - styles - }, - map: { - setStyle: (newStyle) => { - Object.keys(_layers).forEach(key => delete _layers[key]); - Object.values(newStyle).forEach((s) => { - style.addLayer(s); - }); - }, - addSource: (id, source) => { - sources[id] = source; - style.addSource(id, source); - }, - removeSource: (id) => { - style.removeSource(id); - delete sources[id]; - }, - getLayer: id => style.getLayer(id), - addLayer: (layer) => { - style.addLayer(layer); - }, - style, - queryRenderedFeatures: (bbox, params) => style.queryRenderedFeatures(bbox, params) - } - }; - - context.options = setupOptions(context.options); - - addLayers(context); - - return context; -} - -test('featuresAt with click bounding box', () => { - const mockContext = createMockContext(); - const result = featuresAt.click(null, [[10, 10], [20, 20]], mockContext); - - assert.deepEqual(result, [{ - type: 'Feature', - properties: { - meta: 'vertex', - id: 'baz' - }, - geometry: { - type: 'Point', - coordinates: [10, 10] - } - }, { - type: 'Feature', - properties: { - meta: 'feature', - id: 'foo' - }, - geometry: { - type: 'LineString', - coordinates: [[0, 0], [1, 1], [2, 2]] - } - }], 'sorts, filters based on properties.meta, removes duplicates'); - - -}); - -test('featuresAt with touch bounding box', () => { - const mockContext = createMockContext(); - const result = featuresAt.touch(null, [[10, 10], [20, 20]], mockContext); - - assert.deepEqual(result, [{ - type: 'Feature', - properties: { - meta: 'vertex', - id: 'baz' - }, - geometry: { - type: 'Point', - coordinates: [10, 10] - } - }, { - type: 'Feature', - properties: { - meta: 'feature', - id: 'foo' - }, - geometry: { - type: 'LineString', - coordinates: [[0, 0], [1, 1], [2, 2]] - } - }], 'sorts, filters based on properties.meta, removes duplicates'); - - -}); - -test('featuresAt should not include missing style layers', () => { - const mockContext = createMockContext(); - - // mock of map's setStyle, which will remove all mapbox-gl-draw styles until the data event is fired, in which mapbox-gl-draw adds back in the styles. - mockContext.map.setStyle({}); - - // featuresAt should return no features if the styles have not finished adding back in - let result = featuresAt.touch(null, [[10, 10], [20, 20]], mockContext); - assert.deepEqual(result, [], 'sorts, filters based on properties.meta, removes duplicates'); - - // mock adding layers back, similar to data event that fires and mapbox-gl-draw subsequently checks for any missing layers and adds them back in. - addLayers(mockContext); - - result = featuresAt.touch(null, [[10, 10], [20, 20]], mockContext); - assert.deepEqual(result, [{ - type: 'Feature', - properties: { - meta: 'vertex', - id: 'baz' - }, - geometry: { - type: 'Point', - coordinates: [10, 10] - } - }, { - type: 'Feature', - properties: { - meta: 'feature', - id: 'foo' - }, - geometry: { - type: 'LineString', - coordinates: [[0, 0], [1, 1], [2, 2]] - } - }], 'sorts, filters based on properties.meta, removes duplicates'); - - -}); diff --git a/test/features_at.test.ts b/test/features_at.test.ts new file mode 100644 index 00000000..f01c2530 --- /dev/null +++ b/test/features_at.test.ts @@ -0,0 +1,329 @@ +import './mock-browser'; +import { spy } from 'sinon'; +import test from 'node:test'; +import assert from 'node:assert/strict'; +import * as featuresAt from '../src/lib/features_at'; +import styles from '../src/lib/theme'; +import * as Constants from '../src/constants'; +import { configureOptions } from '../src/options'; + +/** + * Mock of the addLayers function in setup + */ +function addLayers(ctx) { + // drawn features style + ctx.map.addSource(Constants.sources.COLD, { + data: { + type: Constants.geojsonTypes.FEATURE_COLLECTION, + features: [] + }, + type: 'geojson' + }); + + // hot features style + ctx.map.addSource(Constants.sources.HOT, { + data: { + type: Constants.geojsonTypes.FEATURE_COLLECTION, + features: [ + { + type: 'Feature', + properties: { + meta: 'feature', + id: 'foo' + }, + geometry: { + type: 'LineString', + coordinates: [ + [0, 0], + [1, 1], + [2, 2] + ] + } + }, + { + type: 'Feature', + properties: { + meta: 'nothing', + id: 'bar' + }, + geometry: { + type: 'Polygon', + coordinates: [ + [ + [1, 1], + [2, 2], + [3, 3], + [4, 4], + [1, 1] + ] + ] + } + }, + { + type: 'Feature', + properties: { + meta: 'vertex', + id: 'baz' + }, + geometry: { + type: 'Point', + coordinates: [10, 10] + } + }, + { + type: 'Feature', + properties: { + meta: 'vertex', + id: 'baz' + }, + geometry: { + type: 'Point', + coordinates: [10, 10] + } + } + ] + }, + type: 'geojson' + }); + + ctx.options.styles.forEach(style => { + ctx.map.addLayer(style); + }); +} + +/** + * Mock context with a simplified mapbox-gl-js map (including some source/style/layer interactions) + */ +function createMockContext() { + const _layers = {}; + const sources = {}; + const addSource = (id, source) => { + _layers[id] = source; + }; + const style = { + _layers, + getLayer: id => _layers[id], + addLayer: layerObject => { + addSource(layerObject.id, layerObject); + }, + addSource, + removeSource: id => { + delete _layers[id]; + }, + queryRenderedFeatures: (bbox, params) => { + const features = []; + const includedSources = {}; + if (params && params.layers) { + for (const layerId of params.layers) { + const layer = _layers[layerId]; + if (!layer) { + // this layer is not in the style.layers array + throw new Error( + `The layer '${layerId}' does not exist in the map's style and cannot be queried for features.` + ); + } + includedSources[layer.source] = true; + } + } + Object.keys(includedSources) + .filter(source => includedSources[source] != null) + .forEach(source => { + if (sources[source] && sources[source].data) { + features.push(...sources[source].data.features); + } + }); + return features; + } + }; + + const context = { + options: { + styles + }, + map: { + setStyle: newStyle => { + Object.keys(_layers).forEach(key => delete _layers[key]); + Object.values(newStyle).forEach(s => { + style.addLayer(s); + }); + }, + addSource: (id, source) => { + sources[id] = source; + style.addSource(id, source); + }, + removeSource: id => { + style.removeSource(id); + delete sources[id]; + }, + getLayer: id => style.getLayer(id), + addLayer: layer => { + style.addLayer(layer); + }, + style, + queryRenderedFeatures: (bbox, params) => + style.queryRenderedFeatures(bbox, params) + } + }; + + context.options = configureOptions(context.options as spy) as spy; + + addLayers(context); + + return context; +} + +test('featuresAt with click bounding box', () => { + const mockContext = createMockContext() as spy; + const result = featuresAt.click( + null, + [ + [10, 10], + [20, 20] + ], + mockContext + ); + + assert.deepEqual( + result, + [ + { + type: 'Feature', + properties: { + meta: 'vertex', + id: 'baz' + }, + geometry: { + type: 'Point', + coordinates: [10, 10] + } + }, + { + type: 'Feature', + properties: { + meta: 'feature', + id: 'foo' + }, + geometry: { + type: 'LineString', + coordinates: [ + [0, 0], + [1, 1], + [2, 2] + ] + } + } + ], + 'sorts, filters based on properties.meta, removes duplicates' + ); +}); + +test('featuresAt with touch bounding box', () => { + const mockContext = createMockContext() as spy; + const result = featuresAt.touch( + null, + [ + [10, 10], + [20, 20] + ], + mockContext + ); + + assert.deepEqual( + result, + [ + { + type: 'Feature', + properties: { + meta: 'vertex', + id: 'baz' + }, + geometry: { + type: 'Point', + coordinates: [10, 10] + } + }, + { + type: 'Feature', + properties: { + meta: 'feature', + id: 'foo' + }, + geometry: { + type: 'LineString', + coordinates: [ + [0, 0], + [1, 1], + [2, 2] + ] + } + } + ], + 'sorts, filters based on properties.meta, removes duplicates' + ); +}); + +test('featuresAt should not include missing style layers', () => { + const mockContext = createMockContext() as spy; + + // mock of map's setStyle, which will remove all mapbox-gl-draw styles until the data event is fired, in which mapbox-gl-draw adds back in the styles. + mockContext.map.setStyle({}); + + // featuresAt should return no features if the styles have not finished adding back in + let result = featuresAt.touch( + null, + [ + [10, 10], + [20, 20] + ], + mockContext + ); + assert.deepEqual( + result, + [], + 'sorts, filters based on properties.meta, removes duplicates' + ); + + // mock adding layers back, similar to data event that fires and mapbox-gl-draw subsequently checks for any missing layers and adds them back in. + addLayers(mockContext); + + result = featuresAt.touch( + null, + [ + [10, 10], + [20, 20] + ], + mockContext + ); + assert.deepEqual( + result, + [ + { + type: 'Feature', + properties: { + meta: 'vertex', + id: 'baz' + }, + geometry: { + type: 'Point', + coordinates: [10, 10] + } + }, + { + type: 'Feature', + properties: { + meta: 'feature', + id: 'foo' + }, + geometry: { + type: 'LineString', + coordinates: [ + [0, 0], + [1, 1], + [2, 2] + ] + } + } + ], + 'sorts, filters based on properties.meta, removes duplicates' + ); +}); diff --git a/test/interaction_events.test.js b/test/interaction_events.test.ts similarity index 60% rename from test/interaction_events.test.js rename to test/interaction_events.test.ts index 4a928394..7d75f8a1 100644 --- a/test/interaction_events.test.js +++ b/test/interaction_events.test.ts @@ -1,21 +1,18 @@ // These tests ensure that user interactions fire the right events +import './mock-browser'; import test from 'node:test'; import assert from 'node:assert/strict'; import { spy } from 'sinon'; -import MapboxDraw from '../index.js'; -import click from './utils/mouse_click.js'; -import createMap from './utils/create_map.js'; -import { setupAfterNextRender } from './utils/after_next_render.js'; -import makeMouseEvent from './utils/make_mouse_event.js'; - -import { - backspaceEvent, - enterEvent, - escapeEvent -} from './utils/key_events.js'; - -test('ensure user interactions fire right events', async (t) => { +import MapboxDraw from '../index'; +import click from './utils/mouse_click'; +import createMap from './utils/create_map'; +import { setupAfterNextRender } from './utils/after_next_render'; +import makeMouseEvent from './utils/make_mouse_event'; + +import { backspaceEvent, enterEvent, escapeEvent } from './utils/key_events'; + +test('ensure user interactions fire right events', async t => { const container = document.createElement('div'); document.body.appendChild(container); const map = createMap({ container }); @@ -40,7 +37,8 @@ test('ensure user interactions fire right events', async (t) => { const drawEvents = []; for (let i = 0; i < fireSpy.callCount; i++) { const eventName = fireSpy.getCall(i).args[0]; - if (typeof eventName !== 'string' || eventName.indexOf('draw.') !== 0) continue; + if (typeof eventName !== 'string' || eventName.indexOf('draw.') !== 0) + continue; // Ignore draw.render events for now if (eventName === 'draw.render') continue; // Ignore draw.actionable events for now @@ -86,10 +84,18 @@ test('ensure user interactions fire right events', async (t) => { } } - const pointButton = controlGroup.getElementsByClassName('mapbox-gl-draw_point')[0]; - const lineButton = controlGroup.getElementsByClassName('mapbox-gl-draw_line')[0]; - const trashButton = controlGroup.getElementsByClassName('mapbox-gl-draw_trash')[0]; - const polygonButton = controlGroup.getElementsByClassName('mapbox-gl-draw_polygon')[0]; + const pointButton = controlGroup.getElementsByClassName( + 'mapbox-gl-draw_point' + )[0]; + const lineButton = controlGroup.getElementsByClassName( + 'mapbox-gl-draw_line' + )[0]; + const trashButton = controlGroup.getElementsByClassName( + 'mapbox-gl-draw_trash' + )[0]; + const polygonButton = controlGroup.getElementsByClassName( + 'mapbox-gl-draw_polygon' + )[0]; // The sequence of these tests matters: each uses state established // in the prior tests. These variables keep track of bits of that state. @@ -102,7 +108,11 @@ test('ensure user interactions fire right events', async (t) => { firedWith('draw.modechange', { mode: 'draw_point' }); - assert.deepEqual(flushDrawEvents(), ['draw.modechange'], 'no unexpected draw events'); + assert.deepEqual( + flushDrawEvents(), + ['draw.modechange'], + 'no unexpected draw events' + ); }); const pointA = { @@ -135,11 +145,11 @@ test('ensure user interactions fire right events', async (t) => { points: [] }); - assert.deepEqual(flushDrawEvents(), [ - 'draw.create', - 'draw.modechange', - 'draw.selectionchange' - ], 'no unexpected draw events'); + assert.deepEqual( + flushDrawEvents(), + ['draw.create', 'draw.modechange', 'draw.selectionchange'], + 'no unexpected draw events' + ); }); await t.test('deselect that point', async () => { @@ -153,9 +163,11 @@ test('ensure user interactions fire right events', async (t) => { features: [], points: [] }); - assert.deepEqual(flushDrawEvents(), [ - 'draw.selectionchange' - ], 'no unexpected draw events'); + assert.deepEqual( + flushDrawEvents(), + ['draw.selectionchange'], + 'no unexpected draw events' + ); }); await t.test('re-select that point', async () => { @@ -169,9 +181,11 @@ test('ensure user interactions fire right events', async (t) => { features: [pointA], points: [] }); - assert.deepEqual(flushDrawEvents(), [ - 'draw.selectionchange' - ], 'no unexpected draw events'); + assert.deepEqual( + flushDrawEvents(), + ['draw.selectionchange'], + 'no unexpected draw events' + ); }); const pointB = { @@ -186,7 +200,7 @@ test('ensure user interactions fire right events', async (t) => { await t.test('drag that point', async () => { // Now in `simple_select` mode ... map.fire('mousedown', makeMouseEvent(25, 25)); - repeat(10, (i) => { + repeat(10, i => { map.fire('mousemove', makeMouseEvent(25 + i, 25 - i, { buttons: 1 })); }); map.fire('mouseup', makeMouseEvent(35, 10)); @@ -195,9 +209,11 @@ test('ensure user interactions fire right events', async (t) => { action: 'move', features: [pointB] }); - assert.deepEqual(flushDrawEvents(), [ - 'draw.update' - ], 'no unexpected draw events'); + assert.deepEqual( + flushDrawEvents(), + ['draw.update'], + 'no unexpected draw events' + ); }); await t.test('delete that point with the Trash button', async () => { @@ -207,9 +223,11 @@ test('ensure user interactions fire right events', async (t) => { firedWith('draw.delete', { features: [pointB] }); - assert.deepEqual(flushDrawEvents(), [ - 'draw.delete' - ], 'no unexpected draw events'); + assert.deepEqual( + flushDrawEvents(), + ['draw.delete'], + 'no unexpected draw events' + ); }); await t.test('enter draw_line_string mode', () => { @@ -218,9 +236,11 @@ test('ensure user interactions fire right events', async (t) => { firedWith('draw.modechange', { mode: 'draw_line_string' }); - assert.deepEqual(flushDrawEvents(), [ - 'draw.modechange' - ], 'no unexpected draw events'); + assert.deepEqual( + flushDrawEvents(), + ['draw.modechange'], + 'no unexpected draw events' + ); }); const lineA = { @@ -228,7 +248,11 @@ test('ensure user interactions fire right events', async (t) => { properties: {}, geometry: { type: 'LineString', - coordinates: [[10, 10], [30, 30], [50, 50]] + coordinates: [ + [10, 10], + [30, 30], + [50, 50] + ] } }; @@ -238,11 +262,11 @@ test('ensure user interactions fire right events', async (t) => { map.fire('mousemove', makeMouseEvent(10, 10)); map.fire('mousemove', makeMouseEvent(20, 20)); click(map, makeMouseEvent(10, 10)); - repeat(20, (i) => { + repeat(20, i => { map.fire('mousemove', makeMouseEvent(10 + i, 10 + i)); }); click(map, makeMouseEvent(30, 30)); - repeat(20, (i) => { + repeat(20, i => { map.fire('mousemove', makeMouseEvent(30 + i, 30 + i)); }); click(map, makeMouseEvent(50, 50)); @@ -263,11 +287,11 @@ test('ensure user interactions fire right events', async (t) => { points: [] }); - assert.deepEqual(flushDrawEvents(), [ - 'draw.create', - 'draw.modechange', - 'draw.selectionchange' - ], 'no unexpected draw events'); + assert.deepEqual( + flushDrawEvents(), + ['draw.create', 'draw.modechange', 'draw.selectionchange'], + 'no unexpected draw events' + ); }); await t.test('deselect that line', async () => { @@ -278,9 +302,11 @@ test('ensure user interactions fire right events', async (t) => { features: [], points: [] }); - assert.deepEqual(flushDrawEvents(), [ - 'draw.selectionchange' - ], 'no unexpected draw events'); + assert.deepEqual( + flushDrawEvents(), + ['draw.selectionchange'], + 'no unexpected draw events' + ); }); await t.test('re-select that line', async () => { @@ -292,9 +318,11 @@ test('ensure user interactions fire right events', async (t) => { features: [lineA], points: [] }); - assert.deepEqual(flushDrawEvents(), [ - 'draw.selectionchange' - ], 'no unexpected draw events'); + assert.deepEqual( + flushDrawEvents(), + ['draw.selectionchange'], + 'no unexpected draw events' + ); }); const lineB = { @@ -302,7 +330,11 @@ test('ensure user interactions fire right events', async (t) => { properties: {}, geometry: { type: 'LineString', - coordinates: [[20, 0], [40, 20], [60, 40]] + coordinates: [ + [20, 0], + [40, 20], + [60, 40] + ] } }; @@ -311,7 +343,7 @@ test('ensure user interactions fire right events', async (t) => { // Mousedown anywhere on the line, not vertex map.fire('mousedown', makeMouseEvent(20, 20)); // Drag it a little bit - repeat(10, (i) => { + repeat(10, i => { map.fire('mousemove', makeMouseEvent(20 + i, 20 - i, { buttons: 1 })); }); // Release the mouse @@ -324,9 +356,11 @@ test('ensure user interactions fire right events', async (t) => { features: [lineB] }); - assert.deepEqual(flushDrawEvents(), [ - 'draw.update' - ], 'no unexpected draw events'); + assert.deepEqual( + flushDrawEvents(), + ['draw.update'], + 'no unexpected draw events' + ); }); await t.test('select a vertex', async () => { @@ -342,20 +376,23 @@ test('ensure user interactions fire right events', async (t) => { firedWith('draw.selectionchange', { features: [lineB], - points: [{ - geometry: { - coordinates: [40, 20], - type: 'Point' - }, - properties: {}, - type: 'Feature' - }] + points: [ + { + geometry: { + coordinates: [40, 20], + type: 'Point' + }, + properties: {}, + type: 'Feature' + } + ] }); - assert.deepEqual(flushDrawEvents(), [ - 'draw.modechange', - 'draw.selectionchange' - ], 'no unexpected draw events'); + assert.deepEqual( + flushDrawEvents(), + ['draw.modechange', 'draw.selectionchange'], + 'no unexpected draw events' + ); }); const lineC = { @@ -363,7 +400,11 @@ test('ensure user interactions fire right events', async (t) => { properties: {}, geometry: { type: 'LineString', - coordinates: [[20, 0], [62, 42], [60, 40]] + coordinates: [ + [20, 0], + [62, 42], + [60, 40] + ] } }; @@ -372,7 +413,7 @@ test('ensure user interactions fire right events', async (t) => { // Click the vertex again map.fire('mousedown', makeMouseEvent(40, 20)); // Drag it a little bit - repeat(22, (i) => { + repeat(22, i => { map.fire('mousemove', makeMouseEvent(40 + i, 20 + i, { buttons: 1 })); }); // Release the mouse @@ -384,10 +425,11 @@ test('ensure user interactions fire right events', async (t) => { action: 'change_coordinates', features: [lineC] }); - assert.deepEqual(flushDrawEvents(), [ - 'draw.update', - 'draw.selectionchange' - ], 'no unexpected draw events'); + assert.deepEqual( + flushDrawEvents(), + ['draw.update', 'draw.selectionchange'], + 'no unexpected draw events' + ); }); const lineD = { @@ -395,7 +437,12 @@ test('ensure user interactions fire right events', async (t) => { properties: {}, geometry: { type: 'LineString', - coordinates: [[20, 0], [41, 21], [62, 42], [60, 40]] + coordinates: [ + [20, 0], + [41, 21], + [62, 42], + [60, 40] + ] } }; @@ -408,9 +455,11 @@ test('ensure user interactions fire right events', async (t) => { action: 'change_coordinates', features: [lineD] }); - assert.deepEqual(flushDrawEvents(), [ - 'draw.update' - ], 'no unexpected draw events'); + assert.deepEqual( + flushDrawEvents(), + ['draw.update'], + 'no unexpected draw events' + ); }); const lineE = { @@ -418,7 +467,11 @@ test('ensure user interactions fire right events', async (t) => { properties: {}, geometry: { type: 'LineString', - coordinates: [[20, 0], [62, 42], [60, 40]] + coordinates: [ + [20, 0], + [62, 42], + [60, 40] + ] } }; @@ -432,10 +485,11 @@ test('ensure user interactions fire right events', async (t) => { action: 'change_coordinates', features: [lineE] }); - assert.deepEqual(flushDrawEvents(), [ - 'draw.update', - 'draw.selectionchange' - ], 'no unexpected draw events'); + assert.deepEqual( + flushDrawEvents(), + ['draw.update', 'draw.selectionchange'], + 'no unexpected draw events' + ); }); // Leaving that line in place while moving on to @@ -456,10 +510,11 @@ test('ensure user interactions fire right events', async (t) => { points: [] }); - assert.deepEqual(flushDrawEvents(), [ - 'draw.modechange', - 'draw.selectionchange' - ], 'no unexpected draw events'); + assert.deepEqual( + flushDrawEvents(), + ['draw.modechange', 'draw.selectionchange'], + 'no unexpected draw events' + ); }); const polygonA = { @@ -467,22 +522,30 @@ test('ensure user interactions fire right events', async (t) => { properties: {}, geometry: { type: 'Polygon', - coordinates: [[[0, 0], [0, 30], [30, 30], [30, 0], [0, 0]]] + coordinates: [ + [ + [0, 0], + [0, 30], + [30, 30], + [30, 0], + [0, 0] + ] + ] } }; await t.test('create a polygon', async () => { // Now in `draw_polygon` mode ... click(map, makeMouseEvent(0, 0)); - repeat(20, (i) => { + repeat(20, i => { map.fire('mousemove', makeMouseEvent(0, 0 + i)); }); click(map, makeMouseEvent(0, 30)); - repeat(20, (i) => { + repeat(20, i => { map.fire('mousemove', makeMouseEvent(0 + i, 30)); }); click(map, makeMouseEvent(30, 30)); - repeat(20, (i) => { + repeat(20, i => { map.fire('mousemove', makeMouseEvent(30, 30 - i)); }); click(map, makeMouseEvent(30, 0)); @@ -503,11 +566,11 @@ test('ensure user interactions fire right events', async (t) => { points: [] }); - assert.deepEqual(flushDrawEvents(), [ - 'draw.create', - 'draw.modechange', - 'draw.selectionchange' - ], 'no unexpected draw events'); + assert.deepEqual( + flushDrawEvents(), + ['draw.create', 'draw.modechange', 'draw.selectionchange'], + 'no unexpected draw events' + ); }); await t.test('deselect the polygon', async () => { @@ -520,17 +583,22 @@ test('ensure user interactions fire right events', async (t) => { features: [], points: [] }); - assert.deepEqual(flushDrawEvents(), [ - 'draw.selectionchange' - ], 'no unexpected draw events'); + assert.deepEqual( + flushDrawEvents(), + ['draw.selectionchange'], + 'no unexpected draw events' + ); }); await t.test('box-select the line and the polygon', async () => { // Now in `simple_select` mode ... // Mouse down with the shift key map.fire('mousedown', makeMouseEvent(200, 200, { shiftKey: true })); - repeat(20, (i) => { - map.fire('mousemove', makeMouseEvent(200 - (10 * i), 200 - (10 * i), { buttons: 1 })); + repeat(20, i => { + map.fire( + 'mousemove', + makeMouseEvent(200 - 10 * i, 200 - 10 * i, { buttons: 1 }) + ); }); map.fire('mouseup', makeMouseEvent(0, 0)); @@ -540,9 +608,11 @@ test('ensure user interactions fire right events', async (t) => { features: [lineE, polygonA], points: [] }); - assert.deepEqual(flushDrawEvents(), [ - 'draw.selectionchange' - ], 'no unexpected draw events'); + assert.deepEqual( + flushDrawEvents(), + ['draw.selectionchange'], + 'no unexpected draw events' + ); }); const lineF = { @@ -550,7 +620,11 @@ test('ensure user interactions fire right events', async (t) => { properties: {}, geometry: { type: 'LineString', - coordinates: [[40, -20], [82, 22], [80, 20]] + coordinates: [ + [40, -20], + [82, 22], + [80, 20] + ] } }; @@ -559,7 +633,15 @@ test('ensure user interactions fire right events', async (t) => { properties: {}, geometry: { type: 'Polygon', - coordinates: [[[20, -20], [20, 10], [50, 10], [50, -20], [20, -20]]] + coordinates: [ + [ + [20, -20], + [20, 10], + [50, 10], + [50, -20], + [20, -20] + ] + ] } }; @@ -568,7 +650,7 @@ test('ensure user interactions fire right events', async (t) => { // Mousedown anywhere on either feature, not a vertex map.fire('mousedown', makeMouseEvent(0, 15)); // Drag it a little bit - repeat(20, (i) => { + repeat(20, i => { map.fire('mousemove', makeMouseEvent(0 + i, 15 - i, { buttons: 1 })); }); // Release the mouse @@ -581,9 +663,11 @@ test('ensure user interactions fire right events', async (t) => { features: [lineF, polygonB] }); - assert.deepEqual(flushDrawEvents(), [ - 'draw.update' - ], 'no unexpected draw events'); + assert.deepEqual( + flushDrawEvents(), + ['draw.update'], + 'no unexpected draw events' + ); }); await t.test('deselect both', async () => { @@ -596,9 +680,11 @@ test('ensure user interactions fire right events', async (t) => { features: [], points: [] }); - assert.deepEqual(flushDrawEvents(), [ - 'draw.selectionchange' - ], 'no unexpected draw events'); + assert.deepEqual( + flushDrawEvents(), + ['draw.selectionchange'], + 'no unexpected draw events' + ); }); await t.test('select the polygon', async () => { @@ -611,9 +697,11 @@ test('ensure user interactions fire right events', async (t) => { features: [polygonB], points: [] }); - assert.deepEqual(flushDrawEvents(), [ - 'draw.selectionchange' - ], 'no unexpected draw events'); + assert.deepEqual( + flushDrawEvents(), + ['draw.selectionchange'], + 'no unexpected draw events' + ); }); await t.test('select a vertex', async () => { @@ -628,27 +716,34 @@ test('ensure user interactions fire right events', async (t) => { firedWith('draw.selectionchange', { features: [polygonB], - points: [{ - geometry: { - coordinates: [20, -20], - type: 'Point' - }, - properties: {}, - type: 'Feature' - }] + points: [ + { + geometry: { + coordinates: [20, -20], + type: 'Point' + }, + properties: {}, + type: 'Feature' + } + ] }); - assert.deepEqual(flushDrawEvents(), [ - 'draw.modechange', - 'draw.selectionchange' - ], 'no unexpected draw events'); + assert.deepEqual( + flushDrawEvents(), + ['draw.modechange', 'draw.selectionchange'], + 'no unexpected draw events' + ); }); await t.test('add another vertex to the selection', async () => { // Now in `simple_select` mode ... click(map, makeMouseEvent(20, 10, { shiftKey: true })); await afterNextRender(); - assert.deepEqual(flushDrawEvents(), ['draw.selectionchange'], 'no unexpected draw events'); + assert.deepEqual( + flushDrawEvents(), + ['draw.selectionchange'], + 'no unexpected draw events' + ); }); const polygonC = { @@ -656,7 +751,15 @@ test('ensure user interactions fire right events', async (t) => { properties: {}, geometry: { type: 'Polygon', - coordinates: [[[0, -20], [0, 10], [50, 10], [50, -20], [0, -20]]] + coordinates: [ + [ + [0, -20], + [0, 10], + [50, 10], + [50, -20], + [0, -20] + ] + ] } }; @@ -665,7 +768,7 @@ test('ensure user interactions fire right events', async (t) => { // Click a vertex again map.fire('mousedown', makeMouseEvent(20, 10)); // Drag it a little bit - repeat(20, (i) => { + repeat(20, i => { map.fire('mousemove', makeMouseEvent(20 - i, 10, { buttons: 1 })); }); // Release the mouse @@ -677,10 +780,11 @@ test('ensure user interactions fire right events', async (t) => { action: 'change_coordinates', features: [polygonC] }); - assert.deepEqual(flushDrawEvents(), [ - 'draw.update', - 'draw.selectionchange' - ], 'no unexpected draw events'); + assert.deepEqual( + flushDrawEvents(), + ['draw.update', 'draw.selectionchange'], + 'no unexpected draw events' + ); }); const polygonD = { @@ -688,7 +792,16 @@ test('ensure user interactions fire right events', async (t) => { properties: {}, geometry: { type: 'Polygon', - coordinates: [[[0, -20], [0, 10], [25, 10], [50, 10], [50, -20], [0, -20]]] + coordinates: [ + [ + [0, -20], + [0, 10], + [25, 10], + [50, 10], + [50, -20], + [0, -20] + ] + ] } }; @@ -703,9 +816,11 @@ test('ensure user interactions fire right events', async (t) => { action: 'change_coordinates', features: [polygonD] }); - assert.deepEqual(flushDrawEvents(), [ - 'draw.update' - ], 'no unexpected draw events'); + assert.deepEqual( + flushDrawEvents(), + ['draw.update'], + 'no unexpected draw events' + ); }); const polygonE = { @@ -713,27 +828,38 @@ test('ensure user interactions fire right events', async (t) => { properties: {}, geometry: { type: 'Polygon', - coordinates: [[[0, 10], [50, 10], [50, -20], [0, 10]]] + coordinates: [ + [ + [0, 10], + [50, 10], + [50, -20], + [0, 10] + ] + ] } }; - await t.test('select then delete two vertices with Draw.trash()', async () => { - // Now in `direct_select` mode ... - click(map, makeMouseEvent(0, -20)); - click(map, makeMouseEvent(25, 10, { shiftKey: true })); - Draw.trash(); - - await afterNextRender(); - - firedWith('draw.update', { - action: 'change_coordinates', - features: [polygonE] - }); - assert.deepEqual(flushDrawEvents(), [ - 'draw.update', - 'draw.selectionchange' - ], 'no unexpected draw events'); - }); + await t.test( + 'select then delete two vertices with Draw.trash()', + async () => { + // Now in `direct_select` mode ... + click(map, makeMouseEvent(0, -20)); + click(map, makeMouseEvent(25, 10, { shiftKey: true })); + Draw.trash(); + + await afterNextRender(); + + firedWith('draw.update', { + action: 'change_coordinates', + features: [polygonE] + }); + assert.deepEqual( + flushDrawEvents(), + ['draw.update', 'draw.selectionchange'], + 'no unexpected draw events' + ); + } + ); await t.test('select the polygon', async () => { // Deselect everything @@ -752,9 +878,11 @@ test('ensure user interactions fire right events', async (t) => { features: [polygonE], points: [] }); - assert.deepEqual(flushDrawEvents(), [ - 'draw.selectionchange' - ], 'no unexpected draw events'); + assert.deepEqual( + flushDrawEvents(), + ['draw.selectionchange'], + 'no unexpected draw events' + ); }); await t.test('add the line to the selection', async () => { @@ -768,9 +896,11 @@ test('ensure user interactions fire right events', async (t) => { features: [polygonE, lineF], points: [] }); - assert.deepEqual(flushDrawEvents(), [ - 'draw.selectionchange' - ], 'no unexpected draw events'); + assert.deepEqual( + flushDrawEvents(), + ['draw.selectionchange'], + 'no unexpected draw events' + ); }); // Below are tests to ensure that API usage to modify data does not @@ -792,7 +922,10 @@ test('ensure user interactions fire right events', async (t) => { properties: {}, geometry: { type: 'LineString', - coordinates: [[10, 10], [20, 20]] + coordinates: [ + [10, 10], + [20, 20] + ] } }); Draw.changeMode('draw_point'); @@ -805,29 +938,38 @@ test('ensure user interactions fire right events', async (t) => { assert.deepEqual(flushDrawEvents(), [], 'no unexpected draw events'); }); - await t.test('except when the API function does not directly correspond to the event', async () => { - const line = { - type: 'Feature', - properties: {}, - geometry: { - type: 'LineString', - coordinates: [[10, 10], [20, 20], [30, 30]] - } - }; - const lineId = Draw.add(line)[0]; - Draw.changeMode('simple_select', { - featureIds: [lineId] - }); - await afterNextRender(); - Draw.trash(); - await afterNextRender(); - firedWith('draw.delete', { - features: [line] - }); - assert.deepEqual(flushDrawEvents(), [ - 'draw.delete' - ], 'no unexpected draw events'); - }); + await t.test( + 'except when the API function does not directly correspond to the event', + async () => { + const line = { + type: 'Feature', + properties: {}, + geometry: { + type: 'LineString', + coordinates: [ + [10, 10], + [20, 20], + [30, 30] + ] + } + }; + const lineId = Draw.add(line)[0]; + Draw.changeMode('simple_select', { + featureIds: [lineId] + }); + await afterNextRender(); + Draw.trash(); + await afterNextRender(); + firedWith('draw.delete', { + features: [line] + }); + assert.deepEqual( + flushDrawEvents(), + ['draw.delete'], + 'no unexpected draw events' + ); + } + ); await t.test('start draw_point mode then exit with Enter', async () => { Draw.deleteAll(); @@ -840,9 +982,11 @@ test('ensure user interactions fire right events', async (t) => { mode: 'simple_select' }); assert.equal(Draw.getAll().features.length, 0, 'no feature created'); - assert.deepEqual(flushDrawEvents(), [ - 'draw.modechange' - ], 'no unexpected draw events'); + assert.deepEqual( + flushDrawEvents(), + ['draw.modechange'], + 'no unexpected draw events' + ); }); await t.test('start draw_point mode then exit with Escape', async () => { @@ -856,104 +1000,127 @@ test('ensure user interactions fire right events', async (t) => { mode: 'simple_select' }); assert.equal(Draw.getAll().features.length, 0, 'no feature created'); - assert.deepEqual(flushDrawEvents(), [ - 'draw.modechange' - ], 'no unexpected draw events'); + assert.deepEqual( + flushDrawEvents(), + ['draw.modechange'], + 'no unexpected draw events' + ); }); - await t.test('start draw_line_string mode and drawing a line then finish with Enter', async () => { - Draw.deleteAll(); - Draw.changeMode('draw_line_string'); - click(map, makeMouseEvent(240, 240)); - click(map, makeMouseEvent(260, 260)); - container.dispatchEvent(enterEvent); + await t.test( + 'start draw_line_string mode and drawing a line then finish with Enter', + async () => { + Draw.deleteAll(); + Draw.changeMode('draw_line_string'); + click(map, makeMouseEvent(240, 240)); + click(map, makeMouseEvent(260, 260)); + container.dispatchEvent(enterEvent); - await afterNextRender(); + await afterNextRender(); - const expectedLine = { - type: 'Feature', - properties: {}, - geometry: { - type: 'LineString', - coordinates: [[240, 240], [260, 260]] - } - }; - - firedWith('draw.create', { - features: [expectedLine] - }); - - firedWith('draw.selectionchange', { - features: [expectedLine], - points: [] - }); - - firedWith('draw.modechange', { - mode: 'simple_select' - }); - - assert.deepEqual(flushDrawEvents(), [ - 'draw.create', - 'draw.modechange', - 'draw.selectionchange' - ], 'no unexpected draw events'); - }); - - await t.test('start draw_line_string mode then exit with Escape', async () => { - Draw.deleteAll(); - Draw.changeMode('draw_line_string'); - click(map, makeMouseEvent(0, 0)); - click(map, makeMouseEvent(20, 20)); - container.dispatchEvent(escapeEvent); - - await afterNextRender(); - - firedWith('draw.modechange', { - mode: 'simple_select' - }); - assert.equal(Draw.getAll().features.length, 0, 'no feature created'); - assert.deepEqual(flushDrawEvents(), [ - 'draw.modechange' - ], 'no unexpected draw events'); - }); - - await t.test('start draw_polygon mode and drawing a polygon then finish with Enter', async () => { - Draw.deleteAll(); - Draw.changeMode('draw_polygon'); - click(map, makeMouseEvent(240, 240)); - click(map, makeMouseEvent(260, 260)); - click(map, makeMouseEvent(300, 200)); - container.dispatchEvent(enterEvent); - - await afterNextRender(); - - const expectedPolygon = { - type: 'Feature', - properties: {}, - geometry: { - type: 'Polygon', - coordinates: [[[240, 240], [260, 260], [300, 200], [240, 240]]] - } - }; - firedWith('draw.create', { - features: [expectedPolygon] - }); - - firedWith('draw.selectionchange', { - features: [expectedPolygon], - points: [] - }); - - firedWith('draw.modechange', { - mode: 'simple_select' - }); - - assert.deepEqual(flushDrawEvents(), [ - 'draw.create', - 'draw.modechange', - 'draw.selectionchange' - ], 'no unexpected draw events'); - }); + const expectedLine = { + type: 'Feature', + properties: {}, + geometry: { + type: 'LineString', + coordinates: [ + [240, 240], + [260, 260] + ] + } + }; + + firedWith('draw.create', { + features: [expectedLine] + }); + + firedWith('draw.selectionchange', { + features: [expectedLine], + points: [] + }); + + firedWith('draw.modechange', { + mode: 'simple_select' + }); + + assert.deepEqual( + flushDrawEvents(), + ['draw.create', 'draw.modechange', 'draw.selectionchange'], + 'no unexpected draw events' + ); + } + ); + + await t.test( + 'start draw_line_string mode then exit with Escape', + async () => { + Draw.deleteAll(); + Draw.changeMode('draw_line_string'); + click(map, makeMouseEvent(0, 0)); + click(map, makeMouseEvent(20, 20)); + container.dispatchEvent(escapeEvent); + + await afterNextRender(); + + firedWith('draw.modechange', { + mode: 'simple_select' + }); + assert.equal(Draw.getAll().features.length, 0, 'no feature created'); + assert.deepEqual( + flushDrawEvents(), + ['draw.modechange'], + 'no unexpected draw events' + ); + } + ); + + await t.test( + 'start draw_polygon mode and drawing a polygon then finish with Enter', + async () => { + Draw.deleteAll(); + Draw.changeMode('draw_polygon'); + click(map, makeMouseEvent(240, 240)); + click(map, makeMouseEvent(260, 260)); + click(map, makeMouseEvent(300, 200)); + container.dispatchEvent(enterEvent); + + await afterNextRender(); + + const expectedPolygon = { + type: 'Feature', + properties: {}, + geometry: { + type: 'Polygon', + coordinates: [ + [ + [240, 240], + [260, 260], + [300, 200], + [240, 240] + ] + ] + } + }; + firedWith('draw.create', { + features: [expectedPolygon] + }); + + firedWith('draw.selectionchange', { + features: [expectedPolygon], + points: [] + }); + + firedWith('draw.modechange', { + mode: 'simple_select' + }); + + assert.deepEqual( + flushDrawEvents(), + ['draw.create', 'draw.modechange', 'draw.selectionchange'], + 'no unexpected draw events' + ); + } + ); await t.test('start draw_polygon mode then exit with Escape', async () => { Draw.deleteAll(); @@ -968,9 +1135,11 @@ test('ensure user interactions fire right events', async (t) => { mode: 'simple_select' }); assert.equal(Draw.getAll().features.length, 0, 'no feature created'); - assert.deepEqual(flushDrawEvents(), [ - 'draw.modechange' - ], 'no unexpected draw events'); + assert.deepEqual( + flushDrawEvents(), + ['draw.modechange'], + 'no unexpected draw events' + ); }); await t.test('box selection includes no features', async () => { @@ -983,10 +1152,10 @@ test('ensure user interactions fire right events', async (t) => { }); }); -test('ensure API fire right events', async (t) => { +test('ensure API fire right events', async t => { const container = document.createElement('div'); document.body.appendChild(container); - const map = createMap({container}); + const map = createMap({ container }); const fireSpy = spy(map, 'fire'); // Explicitly set `suppressAPIEvents` to false to ensure events are fired @@ -1013,34 +1182,33 @@ test('ensure API fire right events', async (t) => { properties: {}, geometry: { type: 'LineString', - coordinates: [[10, 10], [20, 20]] + coordinates: [ + [10, 10], + [20, 20] + ] } }; - t.afterEach(() => { - fireSpy.resetHistory(); - }); - await t.test('Draw#add fires draw.create event', async () => { Draw.add(point); assert.strictEqual(fireSpy.lastCall.firstArg, 'draw.create'); - assert.deepStrictEqual(fireSpy.lastCall.lastArg, {features: [point]}); + assert.deepStrictEqual(fireSpy.lastCall.lastArg, { features: [point] }); Draw.add(line); assert.strictEqual(fireSpy.lastCall.firstArg, 'draw.create'); - assert.deepStrictEqual(fireSpy.lastCall.lastArg, {features: [line]}); + assert.deepStrictEqual(fireSpy.lastCall.lastArg, { features: [line] }); }); await t.test('Draw#delete fires draw.delete event', async () => { Draw.delete(point.id); assert.strictEqual(fireSpy.lastCall.firstArg, 'draw.delete'); - assert.deepStrictEqual(fireSpy.lastCall.lastArg, {features: [point]}); + assert.deepStrictEqual(fireSpy.lastCall.lastArg, { features: [point] }); }); await t.test('Draw#deleteAll fires draw.delete event', async () => { Draw.deleteAll(); assert.strictEqual(fireSpy.lastCall.firstArg, 'draw.delete'); - assert.deepStrictEqual(fireSpy.lastCall.lastArg, {features: [line]}); + assert.deepStrictEqual(fireSpy.lastCall.lastArg, { features: [line] }); }); await t.test('Draw#set fires draw.create event', async () => { @@ -1051,13 +1219,17 @@ test('ensure API fire right events', async (t) => { Draw.set(collection); - assert.strictEqual(fireSpy.callCount, 2, 'fires draw.create event for each feature'); + assert.strictEqual( + fireSpy.callCount, + 2, + 'fires draw.create event for each feature' + ); assert.strictEqual(fireSpy.firstCall.firstArg, 'draw.create'); - assert.deepStrictEqual(fireSpy.firstCall.lastArg, {features: [point]}); + assert.deepStrictEqual(fireSpy.firstCall.lastArg, { features: [point] }); assert.strictEqual(fireSpy.lastCall.firstArg, 'draw.create'); - assert.deepStrictEqual(fireSpy.lastCall.lastArg, {features: [line]}); + assert.deepStrictEqual(fireSpy.lastCall.lastArg, { features: [line] }); }); await t.test('Draw#set fires draw.delete event', async () => { @@ -1068,10 +1240,14 @@ test('ensure API fire right events', async (t) => { Draw.set(collection); - assert.strictEqual(fireSpy.callCount, 1, 'fires draw.delete event for deleted feature'); + assert.strictEqual( + fireSpy.callCount, + 1, + 'fires draw.delete event for deleted feature' + ); assert.strictEqual(fireSpy.lastCall.firstArg, 'draw.delete'); - assert.deepStrictEqual(fireSpy.lastCall.lastArg, {features: [point]}); + assert.deepStrictEqual(fireSpy.lastCall.lastArg, { features: [point] }); }); await t.test('Draw#setFeatureProperty fires draw.update event', () => { @@ -1082,25 +1258,57 @@ test('ensure API fire right events', async (t) => { assert.strictEqual(fireSpy.lastCall.firstArg, 'draw.update'); assert.deepStrictEqual(fireSpy.lastCall.lastArg, { action: 'change_properties', - features: [{...point, properties: {price: 200}}] + features: [{ ...point, properties: { price: 200 } }] }); }); await t.test('Draw#changeMode fires draw.modechange event', async () => { Draw.changeMode('draw_point'); - assert.strictEqual(fireSpy.lastCall.firstArg, 'draw.modechange', 'Draw.changeMode triggers draw.modechange event'); - assert.deepStrictEqual(fireSpy.lastCall.lastArg, { mode: 'draw_point' }, 'Draw.changeMode triggers draw.modechange event with correct data'); + assert.strictEqual( + fireSpy.lastCall.firstArg, + 'draw.modechange', + 'Draw.changeMode triggers draw.modechange event' + ); + assert.deepStrictEqual( + fireSpy.lastCall.lastArg, + { mode: 'draw_point' }, + 'Draw.changeMode triggers draw.modechange event with correct data' + ); Draw.changeMode('draw_line_string'); - assert.strictEqual(fireSpy.lastCall.firstArg, 'draw.modechange', 'Draw.changeMode triggers draw.modechange event'); - assert.deepStrictEqual(fireSpy.lastCall.lastArg, { mode: 'draw_line_string' }, 'Draw.changeMode triggers draw.modechange event with correct data'); + assert.strictEqual( + fireSpy.lastCall.firstArg, + 'draw.modechange', + 'Draw.changeMode triggers draw.modechange event' + ); + assert.deepStrictEqual( + fireSpy.lastCall.lastArg, + { mode: 'draw_line_string' }, + 'Draw.changeMode triggers draw.modechange event with correct data' + ); Draw.changeMode('draw_polygon'); - assert.strictEqual(fireSpy.lastCall.firstArg, 'draw.modechange', 'Draw.changeMode triggers draw.modechange event'); - assert.deepStrictEqual(fireSpy.lastCall.lastArg, { mode: 'draw_polygon' }, 'Draw.changeMode triggers draw.modechange event with correct data'); + assert.strictEqual( + fireSpy.lastCall.firstArg, + 'draw.modechange', + 'Draw.changeMode triggers draw.modechange event' + ); + assert.deepStrictEqual( + fireSpy.lastCall.lastArg, + { mode: 'draw_polygon' }, + 'Draw.changeMode triggers draw.modechange event with correct data' + ); Draw.changeMode('simple_select'); - assert.strictEqual(fireSpy.lastCall.firstArg, 'draw.modechange', 'Draw.changeMode triggers draw.modechange event'); - assert.deepStrictEqual(fireSpy.lastCall.lastArg, { mode: 'simple_select' }, 'Draw.changeMode triggers draw.modechange event with correct data'); + assert.strictEqual( + fireSpy.lastCall.firstArg, + 'draw.modechange', + 'Draw.changeMode triggers draw.modechange event' + ); + assert.deepStrictEqual( + fireSpy.lastCall.lastArg, + { mode: 'simple_select' }, + 'Draw.changeMode triggers draw.modechange event with correct data' + ); }); }); diff --git a/test/is_click.test.js b/test/is_click.test.ts similarity index 82% rename from test/is_click.test.js rename to test/is_click.test.ts index 2ee4ca9f..6bd0a124 100644 --- a/test/is_click.test.js +++ b/test/is_click.test.ts @@ -1,10 +1,11 @@ +import './mock-browser'; import test from 'node:test'; import assert from 'node:assert/strict'; -import isClick from '../src/lib/is_click.js'; +import { isClick } from '../src/lib/is_click'; // By adding these values as options and stating them in the test, // we can know the calculation works from the tests, but tweak -// the actual constants in `is_click.js` without having to +// the actual constants in `is_click` without having to // rewrite tests. const testOptions = { fineTolerance: 4, @@ -18,13 +19,24 @@ test('isClick easy', () => { time: 1 }; const b = { - point: { x: 1, y: 1}, + point: { x: 1, y: 1 }, time: 1 }; - assert.equal(isClick({}, b, testOptions), true, 'true when start is missing point and time'); - assert.equal(isClick({ time: 2000 }, b, testOptions), true, 'true when start has only time'); - assert.equal(isClick(a, b, testOptions), true, 'true when start and end match exactly'); - + assert.equal( + isClick({}, b, testOptions), + true, + 'true when start is missing point and time' + ); + assert.equal( + isClick({ time: 2000 }, b, testOptions), + true, + 'true when start has only time' + ); + assert.equal( + isClick(a, b, testOptions), + true, + 'true when start and end match exactly' + ); }); test('isClick when start/end have same time, very close coordinates', () => { @@ -33,11 +45,10 @@ test('isClick when start/end have same time, very close coordinates', () => { time: 1 }; const b = { - point: { x: 2, y: 1.5}, + point: { x: 2, y: 1.5 }, time: 1 }; assert.equal(isClick(a, b, testOptions), true); - }); test('isClick when start/end have same coordinates, distant times', () => { @@ -46,11 +57,10 @@ test('isClick when start/end have same coordinates, distant times', () => { time: 1 }; const b = { - point: { x: 1, y: 1}, + point: { x: 1, y: 1 }, time: 6000 }; assert.equal(isClick(a, b, testOptions), true); - }); test('isClick when start/end have very close coordinates, distant times', () => { @@ -59,14 +69,12 @@ test('isClick when start/end have very close coordinates, distant times', () => time: 1 }; const b = { - point: { x: 2, y: 1.15}, + point: { x: 2, y: 1.15 }, time: 6000 }; assert.equal(isClick(a, b, testOptions), true); - }); - test('isClick when moving just under 4, same times', () => { const a = { point: { x: 1, y: 1 }, @@ -78,7 +86,6 @@ test('isClick when moving just under 4, same times', () => { }; // Move distance ~3.959798 assert.equal(isClick(a, b, testOptions), true); - }); test('isClick when moving just under 4, distant times', () => { @@ -92,7 +99,6 @@ test('isClick when moving just under 4, distant times', () => { }; // Move distance ~3.959798 assert.equal(isClick(a, b, testOptions), true); - }); test('isClick when moving just above 4, same times', () => { @@ -101,12 +107,11 @@ test('isClick when moving just above 4, same times', () => { time: 1 }; const b = { - point: { x: 3.9, y: 3.9}, + point: { x: 3.9, y: 3.9 }, time: 1 }; // Move distance ~4.101219 assert.equal(isClick(a, b, testOptions), true); - }); test('isClick when moving just above 4, very close times', () => { @@ -115,12 +120,11 @@ test('isClick when moving just above 4, very close times', () => { time: 1 }; const b = { - point: { x: 3.9, y: 3.9}, + point: { x: 3.9, y: 3.9 }, time: 499 }; // Move distance ~4.101219 assert.equal(isClick(a, b, testOptions), true); - }); test('isClick when moving just above 4, distant times', () => { @@ -129,12 +133,11 @@ test('isClick when moving just above 4, distant times', () => { time: 1 }; const b = { - point: { x: 3.9, y: 3.9}, + point: { x: 3.9, y: 3.9 }, time: 6000 }; // Move distance ~4.101219 assert.equal(isClick(a, b, testOptions), false); - }); test('isClick when moving just above 4, barely too distant times', () => { @@ -143,12 +146,11 @@ test('isClick when moving just above 4, barely too distant times', () => { time: 1 }; const b = { - point: { x: 3.9, y: 3.9}, + point: { x: 3.9, y: 3.9 }, time: 501 }; // Move distance ~4.101219 assert.equal(isClick(a, b, testOptions), false); - }); test('isClick when moving just below 12, same times', () => { @@ -157,12 +159,11 @@ test('isClick when moving just below 12, same times', () => { time: 1 }; const b = { - point: { x: 9.2, y: 9.2}, + point: { x: 9.2, y: 9.2 }, time: 1 }; // Move distance ~11.596551 assert.equal(isClick(a, b, testOptions), true); - }); test('isClick when moving just below 12, very close times', () => { @@ -171,12 +172,11 @@ test('isClick when moving just below 12, very close times', () => { time: 1 }; const b = { - point: { x: 9.2, y: 9.2}, + point: { x: 9.2, y: 9.2 }, time: 499 }; // Move distance ~11.596551 assert.equal(isClick(a, b, testOptions), true); - }); test('isClick when moving just below 12, distant times', () => { @@ -185,12 +185,11 @@ test('isClick when moving just below 12, distant times', () => { time: 1 }; const b = { - point: { x: 9.2, y: 9.2}, + point: { x: 9.2, y: 9.2 }, time: 6000 }; // Move distance ~11.596551 assert.equal(isClick(a, b, testOptions), false); - }); test('isClick when moving just below 12, barely too distant times', () => { @@ -199,12 +198,11 @@ test('isClick when moving just below 12, barely too distant times', () => { time: 1 }; const b = { - point: { x: 9.2, y: 9.2}, + point: { x: 9.2, y: 9.2 }, time: 501 }; // Move distance ~11.596551 assert.equal(isClick(a, b, testOptions), false); - }); test('isClick when moving just above 12, same times', () => { @@ -213,7 +211,7 @@ test('isClick when moving just above 12, same times', () => { time: 1 }; const b = { - point: { x: 9.5, y: 9.5}, + point: { x: 9.5, y: 9.5 }, time: 1 }; // Move distance ~12.020815 @@ -226,7 +224,7 @@ test('isClick when moving just above 12, distant times', () => { time: 1 }; const b = { - point: { x: 9.5, y: 9.5}, + point: { x: 9.5, y: 9.5 }, time: 6000 }; // Move distance ~12.020815 diff --git a/test/is_event_at_coordinates.test.js b/test/is_event_at_coordinates.test.js deleted file mode 100644 index 1ddb04b1..00000000 --- a/test/is_event_at_coordinates.test.js +++ /dev/null @@ -1,22 +0,0 @@ -import test from 'node:test'; -import assert from 'node:assert/strict'; -import isEventAtCoordinates from '../src/lib/is_event_at_coordinates.js'; - -test('isEventAtCoordinates', () => { - assert.ok(isEventAtCoordinates({ - lngLat: { - lng: 3, - lat: 29 - } - }, [3, 29])); - assert.equal(isEventAtCoordinates({ - lngLat: { - lng: -3, - lat: 29 - } - }, [3, 29]), false); - - assert.equal(isEventAtCoordinates({ - nothing: true - }, [3, 29]), false); -}); diff --git a/test/is_event_at_coordinates.test.ts b/test/is_event_at_coordinates.test.ts new file mode 100644 index 00000000..f4eece99 --- /dev/null +++ b/test/is_event_at_coordinates.test.ts @@ -0,0 +1,40 @@ +import './mock-browser'; +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { isEventAtCoordinates } from '../src/lib/is_event_at_coordinates'; + +test('isEventAtCoordinates', () => { + assert.ok( + isEventAtCoordinates( + { + lngLat: { + lng: 3, + lat: 29 + } + }, + [3, 29] + ) + ); + assert.equal( + isEventAtCoordinates( + { + lngLat: { + lng: -3, + lat: 29 + } + }, + [3, 29] + ), + false + ); + + assert.equal( + isEventAtCoordinates( + { + nothing: true + } as unknown as { lngLat: { lng: number, lat: number } } , + [3, 29] + ), + false + ); +}); diff --git a/test/is_tap.test.js b/test/is_tap.test.ts similarity index 78% rename from test/is_tap.test.js rename to test/is_tap.test.ts index 2bb10aa4..50f7cfa7 100644 --- a/test/is_tap.test.js +++ b/test/is_tap.test.ts @@ -1,10 +1,11 @@ +import './mock-browser'; import test from 'node:test'; import assert from 'node:assert/strict'; -import isTap from '../src/lib/is_tap.js'; +import { isTap } from '../src/lib/is_tap'; // By adding these values as options and stating them in the test, // we can know the calculation works from the tests, but tweak -// the actual constants in `is_tap.js` without having to +// the actual constants in `is_tap` without having to // rewrite tests. const testOptions = { tolerance: 25, @@ -17,12 +18,24 @@ test('isTap easy', () => { time: 1 }; const b = { - point: { x: 1, y: 1}, + point: { x: 1, y: 1 }, time: 1 }; - assert.equal(isTap({}, b, testOptions), true, 'true when start is missing point and time'); - assert.equal(isTap({ time: 2000 }, b, testOptions), true, 'true when start has only time'); - assert.equal(isTap(a, b, testOptions), true, 'true when start and end match exactly'); + assert.equal( + isTap({}, b, testOptions), + true, + 'true when start is missing point and time' + ); + assert.equal( + isTap({ time: 2000 }, b, testOptions), + true, + 'true when start has only time' + ); + assert.equal( + isTap(a, b, testOptions), + true, + 'true when start and end match exactly' + ); }); test('isTap when moving barely at all, same times', () => { @@ -31,7 +44,7 @@ test('isTap when moving barely at all, same times', () => { time: 1 }; const b = { - point: { x: 2, y: 1.5}, + point: { x: 2, y: 1.5 }, time: 1 }; assert.equal(isTap(a, b, testOptions), true); @@ -69,7 +82,7 @@ test('isTap when moving barely at all, just before the time limit', () => { time: 1 }; const b = { - point: { x: 2, y: 1.5}, + point: { x: 2, y: 1.5 }, time: 250 }; assert.equal(isTap(a, b, testOptions), true); diff --git a/test/line_string.test.js b/test/line_string.test.js deleted file mode 100644 index 9e53b5d1..00000000 --- a/test/line_string.test.js +++ /dev/null @@ -1,115 +0,0 @@ -import test from 'node:test'; -import assert from 'node:assert/strict'; -import {spy} from 'sinon'; -import Feature from '../src/feature_types/feature.js'; -import LineString from '../src/feature_types/line_string.js'; -import MapboxDraw from '../index.js'; -import createFeature from './utils/create_feature.js'; -import getPublicMemberKeys from './utils/get_public_member_keys.js'; -import createMockCtx from './utils/create_mock_feature_context.js'; -import {drawGeometry} from './utils/draw_geometry.js'; -import createMap from './utils/create_map.js'; - -test('LineString constructor and API', () => { - const rawLine = createFeature('line'); - const ctx = createMockCtx(); - const lineString = new LineString(ctx, rawLine); - - // Instance members - assert.equal(lineString.ctx, ctx, 'lineString.ctx'); - assert.equal(lineString.coordinates, rawLine.geometry.coordinates, 'lineString.coordinates'); - assert.equal(lineString.properties, rawLine.properties, 'lineString.properties'); - assert.equal(lineString.id, rawLine.id, 'lineString.id'); - assert.equal(lineString.type, rawLine.geometry.type, 'lineString.type'); - assert.equal(getPublicMemberKeys(lineString).length, 5, 'no unexpected instance members'); - - // Prototype members - assert.equal(typeof LineString.prototype.isValid, 'function', 'lineString.isValid'); - assert.equal(typeof LineString.prototype.addCoordinate, 'function', 'lineString.addCoordinate'); - assert.equal(typeof LineString.prototype.getCoordinate, 'function', 'lineString.getCoordinate'); - assert.equal(typeof LineString.prototype.removeCoordinate, 'function', 'lineString.removeCoordinate'); - assert.equal(typeof LineString.prototype.updateCoordinate, 'function', 'lineString.updateCoordinate'); - assert.equal(getPublicMemberKeys(LineString.prototype).length, 5, 'no unexpected prototype members'); - - assert.ok(LineString.prototype instanceof Feature, 'inherits from Feature'); -}); - -test('LineString#isValid', () => { - const validRawLine = createFeature('line'); - const validLineString = new LineString(createMockCtx(), validRawLine); - assert.equal(validLineString.isValid(), true, 'returns true when valid'); - - const invalidRawLineA = createFeature('line'); - invalidRawLineA.geometry.coordinates = [3]; - const invalidLineStringA = new LineString(createMockCtx(), invalidRawLineA); - assert.equal(invalidLineStringA.isValid(), false, 'returns false when there is one coordinate'); - - const invalidRawLineB = createFeature('line'); - invalidRawLineB.geometry.coordinates = []; - const invalidLineStringB = new LineString(createMockCtx(), invalidRawLineB); - assert.equal(invalidLineStringB.isValid(), false, 'returns false when there are no coordinates'); -}); - -test('LineString#addCoordinate', () => { - const rawLine = createFeature('line'); - rawLine.geometry.coordinates = [[1, 2], [3, 4]]; - const lineString = new LineString(createMockCtx(), rawLine); - const changedSpy = spy(lineString, 'changed'); - - lineString.addCoordinate(1, 5, 6); - assert.equal(changedSpy.callCount, 1, 'called lineString.changed()'); - assert.deepEqual(lineString.getCoordinates(), [[1, 2], [5, 6], [3, 4]], 'new coordinate inserted in correct place'); - - lineString.addCoordinate('0', 7, 8); - assert.deepEqual(lineString.getCoordinates(), [[7, 8], [1, 2], [5, 6], [3, 4]], - 'string path works'); -}); - -test('LineString#getCoordinate', () => { - const rawLine = createFeature('line'); - rawLine.geometry.coordinates = [[1, 2], [3, 4]]; - const lineString = new LineString(createMockCtx(), rawLine); - - assert.deepEqual(lineString.getCoordinate(0), [1, 2], 'number path works'); - assert.deepEqual(lineString.getCoordinate('1'), [3, 4], 'string path works'); -}); - -test('LineString#removeCoordinate', () => { - const rawLine = createFeature('line'); - rawLine.geometry.coordinates = [[1, 2], [3, 4]]; - const lineString = new LineString(createMockCtx(), rawLine); - const changedSpy = spy(lineString, 'changed'); - - lineString.removeCoordinate(1); - assert.equal(changedSpy.callCount, 1, 'called lineString.changed()'); - assert.deepEqual(lineString.getCoordinates(), [[1, 2]], 'coordinate removed from correct place'); -}); - -test('LineString#updateCoordinate', () => { - const rawLine = createFeature('line'); - rawLine.geometry.coordinates = [[1, 2], [3, 4], [5, 6]]; - const lineString = new LineString(createMockCtx(), rawLine); - const changedSpy = spy(lineString, 'changed'); - - lineString.updateCoordinate(1, 7, 8); - assert.equal(changedSpy.callCount, 1, 'called lineString.changed()'); - assert.deepEqual(lineString.getCoordinates(), [[1, 2], [7, 8], [5, 6]], 'coordinate updated at correct place'); -}); - -test('LineString integration', async () => { - const lineStringCoordinates = [[0, 0], [40, 20], [20, 40]]; - const map = createMap(); - const Draw = new MapboxDraw(); - map.addControl(Draw); - - await map.on('load'); - - drawGeometry(map, Draw, 'LineString', lineStringCoordinates, () => { - const feats = Draw.getAll().features; - assert.equal(1, feats.length, 'only one'); - assert.equal('LineString', feats[0].geometry.type, 'of the right type'); - assert.equal(lineStringCoordinates[0].length, feats[0].geometry.coordinates[0].length, 'right number of points'); - assert.deepEqual([...lineStringCoordinates, [20, 40]], feats[0].geometry.coordinates, 'in the right spot'); - Draw.onRemove(); - }); -}); diff --git a/test/line_string.test.ts b/test/line_string.test.ts new file mode 100644 index 00000000..a8bac041 --- /dev/null +++ b/test/line_string.test.ts @@ -0,0 +1,213 @@ +import './mock-browser'; +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { spy } from 'sinon'; +import Feature from '../src/feature_types/feature'; +import LineString from '../src/feature_types/line_string'; +import MapboxDraw from '../index'; +import createFeature from './utils/create_feature'; +import getPublicMemberKeys from './utils/get_public_member_keys'; +import createMockCtx from './utils/create_mock_feature_context'; +import { drawGeometry } from './utils/draw_geometry'; +import createMap from './utils/create_map'; + +test('LineString constructor and API', () => { + const rawLine = createFeature('line'); + const ctx = createMockCtx(); + const lineString = new LineString(ctx, rawLine); + + // Instance members + assert.equal(lineString.ctx, ctx, 'lineString.ctx'); + assert.equal( + lineString.coordinates, + rawLine.geometry.coordinates, + 'lineString.coordinates' + ); + assert.equal( + lineString.properties, + rawLine.properties, + 'lineString.properties' + ); + assert.equal(lineString.id, rawLine.id, 'lineString.id'); + assert.equal(lineString.type, rawLine.geometry.type, 'lineString.type'); + assert.equal( + getPublicMemberKeys(lineString).length, + 5, + 'no unexpected instance members' + ); + + // Prototype members + assert.equal( + typeof LineString.prototype.isValid, + 'function', + 'lineString.isValid' + ); + assert.equal( + typeof LineString.prototype.addCoordinate, + 'function', + 'lineString.addCoordinate' + ); + assert.equal( + typeof LineString.prototype.getCoordinate, + 'function', + 'lineString.getCoordinate' + ); + assert.equal( + typeof LineString.prototype.removeCoordinate, + 'function', + 'lineString.removeCoordinate' + ); + assert.equal( + typeof LineString.prototype.updateCoordinate, + 'function', + 'lineString.updateCoordinate' + ); + assert.equal( + getPublicMemberKeys(LineString.prototype).length, + 5, + 'no unexpected prototype members' + ); + + assert.ok(LineString.prototype instanceof Feature, 'inherits from Feature'); +}); + +test('LineString#isValid', () => { + const validRawLine = createFeature('line'); + const validLineString = new LineString(createMockCtx(), validRawLine); + assert.equal(validLineString.isValid(), true, 'returns true when valid'); + + const invalidRawLineA = createFeature('line'); + invalidRawLineA.geometry.coordinates = [3]; + const invalidLineStringA = new LineString(createMockCtx(), invalidRawLineA); + assert.equal( + invalidLineStringA.isValid(), + false, + 'returns false when there is one coordinate' + ); + + const invalidRawLineB = createFeature('line'); + invalidRawLineB.geometry.coordinates = []; + const invalidLineStringB = new LineString(createMockCtx(), invalidRawLineB); + assert.equal( + invalidLineStringB.isValid(), + false, + 'returns false when there are no coordinates' + ); +}); + +test('LineString#addCoordinate', () => { + const rawLine = createFeature('line'); + rawLine.geometry.coordinates = [ + [1, 2], + [3, 4] + ]; + const lineString = new LineString(createMockCtx(), rawLine); + const changedSpy = spy(lineString, 'changed'); + + lineString.addCoordinate(1, 5, 6); + assert.equal(changedSpy.callCount, 1, 'called lineString.changed()'); + assert.deepEqual( + lineString.getCoordinates(), + [ + [1, 2], + [5, 6], + [3, 4] + ], + 'new coordinate inserted in correct place' + ); + + lineString.addCoordinate('0', 7, 8); + assert.deepEqual( + lineString.getCoordinates(), + [ + [7, 8], + [1, 2], + [5, 6], + [3, 4] + ], + 'string path works' + ); +}); + +test('LineString#getCoordinate', () => { + const rawLine = createFeature('line'); + rawLine.geometry.coordinates = [ + [1, 2], + [3, 4] + ]; + const lineString = new LineString(createMockCtx(), rawLine); + + assert.deepEqual(lineString.getCoordinate(0), [1, 2], 'number path works'); + assert.deepEqual(lineString.getCoordinate('1'), [3, 4], 'string path works'); +}); + +test('LineString#removeCoordinate', () => { + const rawLine = createFeature('line'); + rawLine.geometry.coordinates = [ + [1, 2], + [3, 4] + ]; + const lineString = new LineString(createMockCtx(), rawLine); + const changedSpy = spy(lineString, 'changed'); + + lineString.removeCoordinate(1); + assert.equal(changedSpy.callCount, 1, 'called lineString.changed()'); + assert.deepEqual( + lineString.getCoordinates(), + [[1, 2]], + 'coordinate removed from correct place' + ); +}); + +test('LineString#updateCoordinate', () => { + const rawLine = createFeature('line'); + rawLine.geometry.coordinates = [ + [1, 2], + [3, 4], + [5, 6] + ]; + const lineString = new LineString(createMockCtx(), rawLine); + const changedSpy = spy(lineString, 'changed'); + + lineString.updateCoordinate(1, 7, 8); + assert.equal(changedSpy.callCount, 1, 'called lineString.changed()'); + assert.deepEqual( + lineString.getCoordinates(), + [ + [1, 2], + [7, 8], + [5, 6] + ], + 'coordinate updated at correct place' + ); +}); + +test('LineString integration', async () => { + const lineStringCoordinates = [ + [0, 0], + [40, 20], + [20, 40] + ]; + const map = createMap(); + const Draw = new MapboxDraw(); + map.addControl(Draw); + + await map.on('load'); + + await drawGeometry(map, Draw, 'LineString', lineStringCoordinates); + + const feats = Draw.getAll().features; + assert.equal(1, feats.length, 'only one'); + assert.equal('LineString', feats[0].geometry.type, 'of the right type'); + assert.equal( + lineStringCoordinates[0].length, + feats[0].geometry.coordinates[0].length, + 'right number of points' + ); + assert.deepEqual( + [...lineStringCoordinates, [20, 40]], + feats[0].geometry.coordinates, + 'in the right spot' + ); + Draw.onRemove(); +}); diff --git a/test/map_event_to_bounding_box.test.js b/test/map_event_to_bounding_box.test.js deleted file mode 100644 index 125e536a..00000000 --- a/test/map_event_to_bounding_box.test.js +++ /dev/null @@ -1,26 +0,0 @@ -import test from 'node:test'; -import assert from 'node:assert/strict'; -import mapEventToBoundingBox from '../src/lib/map_event_to_bounding_box.js'; - -test('mapEventToBoundingBox', () => { - assert.deepEqual(mapEventToBoundingBox({ - point: { - x: 1, - y: 2 - } - }), [[1, 2], [1, 2]]); - - assert.deepEqual(mapEventToBoundingBox({ - point: { - x: 1, - y: 2 - } - }, 1), [[0, 1], [2, 3]]); - - assert.deepEqual(mapEventToBoundingBox({ - point: { - x: 10.3, - y: 95674.234 - } - }, 50.5), [[-40.2, 95623.734], [60.8, 95724.734]]); -}); diff --git a/test/map_event_to_bounding_box.test.ts b/test/map_event_to_bounding_box.test.ts new file mode 100644 index 00000000..828d2455 --- /dev/null +++ b/test/map_event_to_bounding_box.test.ts @@ -0,0 +1,52 @@ +import './mock-browser'; +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { mapEventToBoundingBox } from '../src/lib/map_event_to_bounding_box'; +import type { MapMouseEvent } from '../src/types/types'; + +test('mapEventToBoundingBox', () => { + assert.deepEqual( + mapEventToBoundingBox({ + point: { + x: 1, + y: 2 + } + } as unknown as MapMouseEvent), + [ + [1, 2], + [1, 2] + ] + ); + + assert.deepEqual( + mapEventToBoundingBox( + { + point: { + x: 1, + y: 2 + } + } as unknown as MapMouseEvent, + 1 + ), + [ + [0, 1], + [2, 3] + ] + ); + + assert.deepEqual( + mapEventToBoundingBox( + { + point: { + x: 10.3, + y: 95674.234 + } + } as unknown as MapMouseEvent, + 50.5 + ), + [ + [-40.2, 95623.734], + [60.8, 95724.734] + ] + ); +}); diff --git a/test/mock-browser.js b/test/mock-browser.ts similarity index 63% rename from test/mock-browser.js rename to test/mock-browser.ts index c1e18660..fc65f682 100644 --- a/test/mock-browser.js +++ b/test/mock-browser.ts @@ -1,13 +1,14 @@ +import { spy } from 'sinon'; import MockBrowser from 'mock-browser'; const mock = new MockBrowser.mocks.MockBrowser(); global.document = mock.getDocument(); -global.window = {}; +(global.window as spy) = {}; // Polyfill based on https://gist.github.com/paulirish/1579671 let lastTime = 0; -global.requestAnimationFrame = function(fn) { +(global as spy).requestAnimationFrame = fn => { const now = Date.now(); const nextTime = Math.max(lastTime + 16, now); - setTimeout(() => fn(lastTime = nextTime), nextTime - now); + setTimeout(() => fn((lastTime = nextTime)), nextTime - now); }; diff --git a/test/mode_handler.test.js b/test/mode_handler.test.ts similarity index 57% rename from test/mode_handler.test.js rename to test/mode_handler.test.ts index 19051f80..9ad53fee 100644 --- a/test/mode_handler.test.js +++ b/test/mode_handler.test.ts @@ -1,9 +1,10 @@ +import './mock-browser'; import test from 'node:test'; import assert from 'node:assert/strict'; -import {spy} from 'sinon'; -import modeHandler from '../src/lib/mode_handler.js'; -import createMockModeHandlerContext from './utils/create_mock_mode_handler_context.js'; -import createMockMode from './utils/create_mock_mode.js'; +import { spy } from 'sinon'; +import modeHandler from '../src/lib/mode_handler'; +import createMockModeHandlerContext from './utils/create_mock_mode_handler_context'; +import createMockMode from './utils/create_mock_mode'; test('returned API', () => { const mh = modeHandler(createMockMode(), createMockModeHandlerContext()); @@ -22,10 +23,17 @@ test('returned API', () => { assert.equal(typeof mh.touchmove, 'function', 'exposes touchmove'); assert.equal(typeof mh.touchend, 'function', 'exposes touchend'); assert.equal(typeof mh.tap, 'function', 'exposes tap'); - assert.equal(typeof mh.combineFeatures, 'function', 'exposes combineFeatures'); - assert.equal(typeof mh.uncombineFeatures, 'function', 'exposes uncombineFeatures'); + assert.equal( + typeof mh.combineFeatures, + 'function', + 'exposes combineFeatures' + ); + assert.equal( + typeof mh.uncombineFeatures, + 'function', + 'exposes uncombineFeatures' + ); assert.equal(Object.keys(mh).length, 17, 'no unexpected properties'); - }); test('ModeHandler calling mode.start with context, and delegation functionality', () => { @@ -41,35 +49,82 @@ test('ModeHandler calling mode.start with context, and delegation functionality' const drawContext = createMockModeHandlerContext(); const mh = modeHandler(mode, drawContext); - assert.equal(handleStartSpy.callCount, 1, 'start was called on mode handler creation'); + assert.equal( + handleStartSpy.callCount, + 1, + 'start was called on mode handler creation' + ); assert.equal(typeof startContext.on, 'function', 'start context has on()'); - assert.equal(typeof startContext.render, 'function', 'start context has render()'); - assert.equal(Object.keys(startContext).length, 2, 'start context has no unexpected properties'); + assert.equal( + typeof startContext.render, + 'function', + 'start context has render()' + ); + assert.equal( + Object.keys(startContext).length, + 2, + 'start context has no unexpected properties' + ); startContext.render('foo'); - assert.ok(drawContext.store.featureChanged.calledWith('foo'), 'start context render calls store.featureChanged'); + assert.ok( + drawContext.store.featureChanged.calledWith('foo'), + 'start context render calls store.featureChanged' + ); assert.throws(() => { - startContext.on('bar', () => true, () => {}); + startContext.on( + 'bar', + () => true, + () => {} + ); }, 'start context on throws on unknown event type'); mh.mousedown({ one: 1 }); - assert.equal(drawContext.store.render.callCount, 0, 'render not called if no handler fires'); - assert.equal(drawContext.ui.updateMapClasses.callCount, 0, 'updateMapClasses not called if no handler fires'); + assert.equal( + drawContext.store.render.callCount, + 0, + 'render not called if no handler fires' + ); + assert.equal( + drawContext.ui.updateMapClasses.callCount, + 0, + 'updateMapClasses not called if no handler fires' + ); const mousedownSpy = spy(); startContext.on('mousedown', () => true, mousedownSpy); mh.mousedown({ two: 2 }); - assert.equal(mousedownSpy.callCount, 1, 'mousedown callback called via delegation'); - assert.deepEqual(mousedownSpy.getCall(0).args, [{ two: 2 }], 'with correct argument'); - assert.equal(drawContext.store.render.callCount, 1, 'render called if handler fires'); - assert.equal(drawContext.ui.updateMapClasses.callCount, 1, 'updateMapClasses called if handler fires'); + assert.equal( + mousedownSpy.callCount, + 1, + 'mousedown callback called via delegation' + ); + assert.deepEqual( + mousedownSpy.getCall(0).args, + [{ two: 2 }], + 'with correct argument' + ); + assert.equal( + drawContext.store.render.callCount, + 1, + 'render called if handler fires' + ); + assert.equal( + drawContext.ui.updateMapClasses.callCount, + 1, + 'updateMapClasses called if handler fires' + ); const mousedownFailSpy = spy(); mousedownSpy.resetHistory(); startContext.on('mousedown', e => !e.three, mousedownFailSpy); mh.mousedown({ three: 3 }); - assert.equal(mousedownFailSpy.callCount, 0, 'delegation only calls callbacks with selectors returning true'); + assert.equal( + mousedownFailSpy.callCount, + 0, + 'delegation only calls callbacks with selectors returning true' + ); assert.equal(mousedownSpy.callCount, 1); assert.deepEqual(mousedownSpy.getCall(0).args, [{ three: 3 }]); @@ -77,63 +132,129 @@ test('ModeHandler calling mode.start with context, and delegation functionality' startContext.on('drag', () => true, dragSpy); mh.drag({ two: 2 }); assert.equal(dragSpy.callCount, 1, 'drag callback called via delegation'); - assert.deepEqual(dragSpy.getCall(0).args, [{ two: 2 }], 'with correct argument'); + assert.deepEqual( + dragSpy.getCall(0).args, + [{ two: 2 }], + 'with correct argument' + ); const clickSpy = spy(); startContext.on('click', () => true, clickSpy); mh.click({ two: 2 }); assert.equal(clickSpy.callCount, 1, 'click callback called via delegation'); - assert.deepEqual(clickSpy.getCall(0).args, [{ two: 2 }], 'with correct argument'); + assert.deepEqual( + clickSpy.getCall(0).args, + [{ two: 2 }], + 'with correct argument' + ); const mousemoveSpy = spy(); startContext.on('mousemove', () => true, mousemoveSpy); mh.mousemove({ two: 2 }); - assert.equal(mousemoveSpy.callCount, 1, 'mousemove callback called via delegation'); - assert.deepEqual(mousemoveSpy.getCall(0).args, [{ two: 2 }], 'with correct argument'); + assert.equal( + mousemoveSpy.callCount, + 1, + 'mousemove callback called via delegation' + ); + assert.deepEqual( + mousemoveSpy.getCall(0).args, + [{ two: 2 }], + 'with correct argument' + ); const mouseupSpy = spy(); startContext.on('mouseup', () => true, mouseupSpy); mh.mouseup({ two: 2 }); - assert.equal(mouseupSpy.callCount, 1, 'mouseup callback called via delegation'); - assert.deepEqual(mouseupSpy.getCall(0).args, [{ two: 2 }], 'with correct argument'); + assert.equal( + mouseupSpy.callCount, + 1, + 'mouseup callback called via delegation' + ); + assert.deepEqual( + mouseupSpy.getCall(0).args, + [{ two: 2 }], + 'with correct argument' + ); const mouseoutSpy = spy(); startContext.on('mouseout', () => true, mouseoutSpy); mh.mouseout({ two: 2 }); - assert.equal(mouseoutSpy.callCount, 1, 'mouseout callback called via delegation'); - assert.deepEqual(mouseoutSpy.getCall(0).args, [{ two: 2 }], 'with correct argument'); + assert.equal( + mouseoutSpy.callCount, + 1, + 'mouseout callback called via delegation' + ); + assert.deepEqual( + mouseoutSpy.getCall(0).args, + [{ two: 2 }], + 'with correct argument' + ); const keydownSpy = spy(); startContext.on('keydown', () => true, keydownSpy); mh.keydown({ two: 2 }); - assert.equal(keydownSpy.callCount, 1, 'keydown callback called via delegation'); - assert.deepEqual(keydownSpy.getCall(0).args, [{ two: 2 }], 'with correct argument'); + assert.equal( + keydownSpy.callCount, + 1, + 'keydown callback called via delegation' + ); + assert.deepEqual( + keydownSpy.getCall(0).args, + [{ two: 2 }], + 'with correct argument' + ); const keyupSpy = spy(); startContext.on('keyup', () => true, keyupSpy); mh.keyup({ two: 2 }); assert.equal(keyupSpy.callCount, 1, 'keyup callback called via delegation'); - assert.deepEqual(keyupSpy.getCall(0).args, [{ two: 2 }], 'with correct argument'); + assert.deepEqual( + keyupSpy.getCall(0).args, + [{ two: 2 }], + 'with correct argument' + ); const touchstartSpy = spy(); startContext.on('touchstart', () => true, touchstartSpy); mh.touchstart({ two: 2 }); - assert.equal(touchstartSpy.callCount, 1, 'touchstart callback called via delegation'); - assert.deepEqual(touchstartSpy.getCall(0).args, [{ two: 2 }], 'with correct argument'); + assert.equal( + touchstartSpy.callCount, + 1, + 'touchstart callback called via delegation' + ); + assert.deepEqual( + touchstartSpy.getCall(0).args, + [{ two: 2 }], + 'with correct argument' + ); const touchmoveSpy = spy(); startContext.on('touchmove', () => true, touchmoveSpy); mh.touchmove({ two: 2 }); - assert.equal(touchmoveSpy.callCount, 1, 'touchmove callback called via delegation'); - assert.deepEqual(touchmoveSpy.getCall(0).args, [{ two: 2 }], 'with correct argument'); + assert.equal( + touchmoveSpy.callCount, + 1, + 'touchmove callback called via delegation' + ); + assert.deepEqual( + touchmoveSpy.getCall(0).args, + [{ two: 2 }], + 'with correct argument' + ); const touchendSpy = spy(); startContext.on('touchend', () => true, touchendSpy); mh.touchend({ two: 2 }); - assert.equal(touchendSpy.callCount, 1, 'touchend callback called via delegation'); - assert.deepEqual(touchendSpy.getCall(0).args, [{ two: 2 }], 'with correct argument'); - - + assert.equal( + touchendSpy.callCount, + 1, + 'touchend callback called via delegation' + ); + assert.deepEqual( + touchendSpy.getCall(0).args, + [{ two: 2 }], + 'with correct argument' + ); }); test('ModeHandler#stop calling mode.stop', () => { @@ -142,8 +263,6 @@ test('ModeHandler#stop calling mode.stop', () => { mh.stop(); assert.equal(mode.stop.callCount, 1, 'mode.stop called'); - - }); test('ModeHandler#stop not calling nonexistent mode.stop', () => { @@ -154,8 +273,6 @@ test('ModeHandler#stop not calling nonexistent mode.stop', () => { assert.doesNotThrow(() => { mh.stop(); }); - - }); test('Modehandler#trash', () => { @@ -166,8 +283,6 @@ test('Modehandler#trash', () => { mh.trash(); assert.equal(mode.trash.callCount, 1, 'mode.trash called'); assert.equal(drawContext.store.render.callCount, 1, 'store.render called'); - - }); test('Modehandler#trash without a mode.trash', () => { @@ -179,7 +294,9 @@ test('Modehandler#trash without a mode.trash', () => { assert.doesNotThrow(() => { mh.trash(); }); - assert.equal(drawContext.store.render.callCount, 0, 'store.render not called'); - - + assert.equal( + drawContext.store.render.callCount, + 0, + 'store.render not called' + ); }); diff --git a/test/mouse_event_point.test.js b/test/mouse_event_point.test.ts similarity index 80% rename from test/mouse_event_point.test.js rename to test/mouse_event_point.test.ts index a4ab1e1e..bade3ffa 100644 --- a/test/mouse_event_point.test.js +++ b/test/mouse_event_point.test.ts @@ -1,7 +1,8 @@ +import './mock-browser'; import test from 'node:test'; import assert from 'node:assert/strict'; import Point from '@mapbox/point-geometry'; -import mouseEventPoint from '../src/lib/mouse_event_point.js'; +import { mouseEventPoint } from '../src/lib/mouse_event_point'; test('mouseEventPoint', () => { const mockContainer = { @@ -13,12 +14,12 @@ test('mouseEventPoint', () => { top: 20 }; } - }; + } as HTMLElement; const mockEvent = { clientX: 15, clientY: 33 - }; + } as MouseEvent; const result = mouseEventPoint(mockEvent, mockContainer); assert.equal(result instanceof Point, true); diff --git a/test/move_features.test.js b/test/move_features.test.ts similarity index 53% rename from test/move_features.test.js rename to test/move_features.test.ts index c221c15d..e6da7adf 100644 --- a/test/move_features.test.js +++ b/test/move_features.test.ts @@ -1,11 +1,12 @@ +import './mock-browser'; import test from 'node:test'; import assert from 'node:assert/strict'; -import getGeoJSON from './utils/get_geojson.js'; -import createMockFeatureContext from './utils/create_mock_feature_context.js'; -import Point from '../src/feature_types/point.js'; -import LineString from '../src/feature_types/line_string.js'; -import Polygon from '../src/feature_types/polygon.js'; -import moveFeatures from '../src/lib/move_features.js'; +import getGeoJSON from './utils/get_geojson'; +import createMockFeatureContext from './utils/create_mock_feature_context'; +import Point from '../src/feature_types/point'; +import LineString from '../src/feature_types/line_string'; +import Polygon from '../src/feature_types/polygon'; +import { moveFeatures } from '../src/lib/move_features'; const mockFeatureContext = createMockFeatureContext(); @@ -32,58 +33,127 @@ test('moveFeatures point beyond south limit', () => { test('moveFeatures line', () => { const line = new LineString(mockFeatureContext, getGeoJSON('line')); - line.setCoordinates([[10, 15], [-10, -30], [17, 33]]); + line.setCoordinates([ + [10, 15], + [-10, -30], + [17, 33] + ]); moveFeatures([line], { lng: 7, lat: 13 }); - assert.deepEqual(line.getCoordinates(), - [[17, 28], [-3, -17], [24, 46]] - ); + assert.deepEqual(line.getCoordinates(), [ + [17, 28], + [-3, -17], + [24, 46] + ]); }); test('moveFeatures line beyond north limit', () => { const line = new LineString(mockFeatureContext, getGeoJSON('line')); - line.setCoordinates([[10, 15], [-10, -30], [17, 33]]); + line.setCoordinates([ + [10, 15], + [-10, -30], + [17, 33] + ]); moveFeatures([line], { lng: 7, lat: 60 }); - assert.deepEqual(line.getCoordinates(), - [[17, 72], [-3, 27], [24, 90]], + assert.deepEqual( + line.getCoordinates(), + [ + [17, 72], + [-3, 27], + [24, 90] + ], 'lat should only move 57' ); }); test('moveFeatures line beyond south pole', () => { const line = new LineString(mockFeatureContext, getGeoJSON('line')); - line.setCoordinates([[10, 15], [-10, -30], [17, 33]]); + line.setCoordinates([ + [10, 15], + [-10, -30], + [17, 33] + ]); moveFeatures([line], { lng: -7, lat: -100 }); - assert.deepEqual(line.getCoordinates(), - [[3, -45], [-17, -90], [10, -27]], + assert.deepEqual( + line.getCoordinates(), + [ + [3, -45], + [-17, -90], + [10, -27] + ], 'lat should only move -45' ); }); test('moveFeatures polygon', () => { const polygon = new Polygon(mockFeatureContext, getGeoJSON('polygon')); - polygon.setCoordinates([[[0, 0], [0, 10], [10, 10], [10, 0]]]); + polygon.setCoordinates([ + [ + [0, 0], + [0, 10], + [10, 10], + [10, 0] + ] + ]); moveFeatures([polygon], { lng: -23, lat: 31.33 }); - assert.deepEqual(polygon.getCoordinates(), - [[[-23, 31.33], [-23, 41.33], [-13, 41.33], [-13, 31.33], [-23, 31.33]]] - ); + assert.deepEqual(polygon.getCoordinates(), [ + [ + [-23, 31.33], + [-23, 41.33], + [-13, 41.33], + [-13, 31.33], + [-23, 31.33] + ] + ]); }); test('moveFeatures polygon beyond north limit', () => { const polygon = new Polygon(mockFeatureContext, getGeoJSON('polygon')); - polygon.setCoordinates([[[0, 0], [0, 20], [10, 10], [10, 0]]]); + polygon.setCoordinates([ + [ + [0, 0], + [0, 20], + [10, 10], + [10, 0] + ] + ]); moveFeatures([polygon], { lng: -0.5, lat: 100 }); - assert.deepEqual(polygon.getCoordinates(), - [[[-0.5, 70], [-0.5, 90], [9.5, 80], [9.5, 70], [-0.5, 70]]], + assert.deepEqual( + polygon.getCoordinates(), + [ + [ + [-0.5, 70], + [-0.5, 90], + [9.5, 80], + [9.5, 70], + [-0.5, 70] + ] + ], 'lat should only move 70' ); }); test('moveFeatures polygon beyond south pole', () => { const polygon = new Polygon(mockFeatureContext, getGeoJSON('polygon')); - polygon.setCoordinates([[[0, 0], [0, -10.5], [10, -40], [10, 0]]]); + polygon.setCoordinates([ + [ + [0, 0], + [0, -10.5], + [10, -40], + [10, 0] + ] + ]); moveFeatures([polygon], { lng: 1, lat: -80.44 }); - assert.deepEqual(polygon.getCoordinates(), - [[[1, -50], [1, -60.5], [11, -90], [11, -50], [1, -50]]], + assert.deepEqual( + polygon.getCoordinates(), + [ + [ + [1, -50], + [1, -60.5], + [11, -90], + [11, -50], + [1, -50] + ] + ], 'lat should only move -50' ); }); @@ -92,17 +162,42 @@ test('moveFeatures multiple features', () => { const point = new Point(mockFeatureContext, getGeoJSON('point')); point.setCoordinates([10, 20]); const line = new LineString(mockFeatureContext, getGeoJSON('line')); - line.setCoordinates([[10, 15], [-10, -30], [17, 33]]); + line.setCoordinates([ + [10, 15], + [-10, -30], + [17, 33] + ]); const polygon = new Polygon(mockFeatureContext, getGeoJSON('polygon')); - polygon.setCoordinates([[[0, 0], [0, 10], [10, 10], [10, 0]]]); + polygon.setCoordinates([ + [ + [0, 0], + [0, 10], + [10, 10], + [10, 0] + ] + ]); moveFeatures([point, line, polygon], { lng: 5, lat: -7 }); assert.deepEqual(point.getCoordinates(), [15, 13], 'point moved'); - assert.deepEqual(line.getCoordinates(), - [[15, 8], [-5, -37], [22, 26]], + assert.deepEqual( + line.getCoordinates(), + [ + [15, 8], + [-5, -37], + [22, 26] + ], 'line moved' ); - assert.deepEqual(polygon.getCoordinates(), - [[[5, -7], [5, 3], [15, 3], [15, -7], [5, -7]]], + assert.deepEqual( + polygon.getCoordinates(), + [ + [ + [5, -7], + [5, 3], + [15, 3], + [15, -7], + [5, -7] + ] + ], 'polygon moved' ); }); @@ -111,17 +206,42 @@ test('moveFeatures multiple features beyond north limit', () => { const point = new Point(mockFeatureContext, getGeoJSON('point')); point.setCoordinates([10, 45]); const line = new LineString(mockFeatureContext, getGeoJSON('line')); - line.setCoordinates([[10, 15], [-10, -30], [17, 33]]); + line.setCoordinates([ + [10, 15], + [-10, -30], + [17, 33] + ]); const polygon = new Polygon(mockFeatureContext, getGeoJSON('polygon')); - polygon.setCoordinates([[[0, 0], [0, 10], [10, 10], [10, 0]]]); + polygon.setCoordinates([ + [ + [0, 0], + [0, 10], + [10, 10], + [10, 0] + ] + ]); moveFeatures([point, line, polygon], { lng: 5, lat: 200 }); assert.deepEqual(point.getCoordinates(), [15, 85], 'point lat only moved 40'); - assert.deepEqual(line.getCoordinates(), - [[15, 55], [-5, 10], [22, 73]], + assert.deepEqual( + line.getCoordinates(), + [ + [15, 55], + [-5, 10], + [22, 73] + ], 'line lat only moved 40' ); - assert.deepEqual(polygon.getCoordinates(), - [[[5, 40], [5, 50], [15, 50], [15, 40], [5, 40]]], + assert.deepEqual( + polygon.getCoordinates(), + [ + [ + [5, 40], + [5, 50], + [15, 50], + [15, 40], + [5, 40] + ] + ], 'polygon lat only moved 40' ); }); @@ -130,17 +250,46 @@ test('moveFeatures multiple features beyond south limit', () => { const point = new Point(mockFeatureContext, getGeoJSON('point')); point.setCoordinates([10, 20]); const line = new LineString(mockFeatureContext, getGeoJSON('line')); - line.setCoordinates([[10, 15], [-10, -30], [17, 33]]); + line.setCoordinates([ + [10, 15], + [-10, -30], + [17, 33] + ]); const polygon = new Polygon(mockFeatureContext, getGeoJSON('polygon')); - polygon.setCoordinates([[[0, 0], [0, 10], [10, 10], [10, 0]]]); + polygon.setCoordinates([ + [ + [0, 0], + [0, 10], + [10, 10], + [10, 0] + ] + ]); moveFeatures([point, line, polygon], { lng: 5, lat: -120 }); - assert.deepEqual(point.getCoordinates(), [15, -40], 'point lat only moved -60'); - assert.deepEqual(line.getCoordinates(), - [[15, -45], [-5, -90], [22, -27]], + assert.deepEqual( + point.getCoordinates(), + [15, -40], + 'point lat only moved -60' + ); + assert.deepEqual( + line.getCoordinates(), + [ + [15, -45], + [-5, -90], + [22, -27] + ], 'line lat only moved -60' ); - assert.deepEqual(polygon.getCoordinates(), - [[[5, -60], [5, -50], [15, -50], [15, -60], [5, -60]]], + assert.deepEqual( + polygon.getCoordinates(), + [ + [ + [5, -60], + [5, -50], + [15, -50], + [15, -60], + [5, -60] + ] + ], 'polygon moved' ); }); diff --git a/test/multi_feature.test.js b/test/multi_feature.test.js deleted file mode 100644 index da80974a..00000000 --- a/test/multi_feature.test.js +++ /dev/null @@ -1,229 +0,0 @@ -import test from 'node:test'; -import assert from 'node:assert/strict'; -import {spy} from 'sinon'; -import Feature from '../src/feature_types/feature.js'; -import Point from '../src/feature_types/point.js'; -import Polygon from '../src/feature_types/polygon.js'; -import LineString from '../src/feature_types/line_string.js'; -import MultiFeature from '../src/feature_types/multi_feature.js'; -import createMockCtx from './utils/create_mock_feature_context.js'; -import getPublicMemberKeys from './utils/get_public_member_keys.js'; - -test('MultiPoint via MultiFeature', () => { - assert.ok(MultiFeature.prototype instanceof Feature, 'inherits from Feature'); - - // Prototype members - assert.equal(typeof MultiFeature.prototype.isValid, 'function', 'polygon.isValid'); - assert.equal(typeof MultiFeature.prototype.setCoordinates, 'function', 'polygon.setCoordinates'); - assert.equal(typeof MultiFeature.prototype.getCoordinate, 'function', 'polygon.getCoordinate'); - assert.equal(typeof MultiFeature.prototype.getCoordinates, 'function', 'polygon.getCoordinates'); - assert.equal(typeof MultiFeature.prototype.updateCoordinate, 'function', 'polygon.updateCoordinate'); - assert.equal(typeof MultiFeature.prototype.addCoordinate, 'function', 'polygon.addCoordinate'); - assert.equal(typeof MultiFeature.prototype.removeCoordinate, 'function', 'polygon.removeCoordinate'); - assert.equal(typeof MultiFeature.prototype.getFeatures, 'function', 'polygon.getFeatures'); - - assert.equal(getPublicMemberKeys(MultiFeature.prototype).length, 8, 'no unexpected prototype members'); - - -}); - -test('MultiPoint', () => { - const rawMultiPoint = { - type: 'Feature', - id: 'wahoo', - properties: { foo: 'bar' }, - geometry: { - type: 'MultiPoint', - coordinates: [[1, 1], [2, 2], [3, 3]] - } - }; - const ctx = createMockCtx(); - let multiPoint; - assert.doesNotThrow(() => { - multiPoint = new MultiFeature(ctx, rawMultiPoint); - }, 'MultiPoint type does not throw'); - const changedSpy = spy(multiPoint, 'changed'); - - // Instance members - assert.equal(multiPoint.ctx, ctx, 'multiPoint.ctx'); - assert.equal(multiPoint.coordinates, undefined, 'no coordinates'); - assert.deepEqual(multiPoint.properties, { foo: 'bar' }, 'multiPoint.properties'); - assert.equal(multiPoint.id, 'wahoo', 'multiPoint.id'); - assert.equal(multiPoint.type, 'MultiPoint', 'multiPoint.type'); - assert.equal(multiPoint.features.length, 3, 'multiPoint.features'); - // multiPoint.changed gets counted because it's used below - assert.equal(getPublicMemberKeys(multiPoint).length, 7, 'no unexpected instance members'); - - const pointA = multiPoint.features[0]; - const pointB = multiPoint.features[1]; - const pointC = multiPoint.features[2]; - - assert.deepEqual(pointA, new Point(ctx, { - id: pointA.id, - type: 'Feature', - properties: {}, - geometry: { - coordinates: [1, 1], - type: 'Point' - } - })); - assert.deepEqual(pointB, new Point(ctx, { - id: pointB.id, - type: 'Feature', - properties: {}, - geometry: { - coordinates: [2, 2], - type: 'Point' - } - })); - assert.deepEqual(pointC, new Point(ctx, { - id: pointC.id, - type: 'Feature', - properties: {}, - geometry: { - coordinates: [3, 3], - type: 'Point' - } - })); - - const pointAGetCoordinateSpy = spy(pointA, 'getCoordinate'); - const pointBGetCoordinateSpy = spy(pointB, 'getCoordinate'); - const pointCGetCoordinateSpy = spy(pointC, 'getCoordinate'); - const coordinate = multiPoint.getCoordinate('2'); - assert.equal(pointAGetCoordinateSpy.callCount, 0, 'point A getCoordinate not called'); - assert.equal(pointBGetCoordinateSpy.callCount, 0, 'point B getCoordinate not called'); - assert.equal(pointCGetCoordinateSpy.callCount, 1, 'point C getCoordinate'); - assert.deepEqual(coordinate, [3, 3], 'correct coordinate'); - - const pointAUpdateCoordinateSpy = spy(pointA, 'updateCoordinate'); - const pointBUpdateCoordinateSpy = spy(pointB, 'updateCoordinate'); - const pointCUpdateCoordinateSpy = spy(pointC, 'updateCoordinate'); - multiPoint.updateCoordinate('0', 99, 100); - assert.equal(pointAUpdateCoordinateSpy.callCount, 1, 'point A updateCoordinate'); - assert.equal(pointBUpdateCoordinateSpy.callCount, 0, 'point B updateCoordinate not called'); - assert.equal(pointCUpdateCoordinateSpy.callCount, 0, 'point C updateCoordinate not called'); - assert.deepEqual(multiPoint.getCoordinate('0'), [99, 100], 'correct coordinate'); - - assert.deepEqual(multiPoint.getCoordinates(), [[99, 100], [2, 2], [3, 3]], - 'getCoordinates returns the complete multi-coordinates'); - - multiPoint.setCoordinates([[6, 6], [7, 7]]); - assert.equal(changedSpy.callCount, 2, 'changed called by setCoordinates'); - assert.deepEqual(multiPoint.getCoordinates(), [[6, 6], [7, 7]]); - - assert.equal(multiPoint.isValid(), true, 'positive validation works'); - multiPoint.setCoordinates([[1], []]); - assert.equal(multiPoint.isValid(), false, 'negative validation works'); - - -}); - -// Tests below less in depth becuase we know the -// inner-workings are the same -test('MultiPolygon via MultiFeature', () => { - const rawMultiPolygon = { - type: 'Feature', - id: 'zing', - properties: { f: 'a' }, - geometry: { - type: 'MultiPolygon', - coordinates: [ - [[[1, 1], [2, 2], [3, 3], [4, 4], [1, 1]]], - [[[2, 1], [6, 2], [8, 3], [2, 4], [2, 1]], [[1, 1], [2, 2], [3, 3], [1, 1]]] - ] - } - }; - const ctx = createMockCtx(); - let multiPolygon; - assert.doesNotThrow(() => { - multiPolygon = new MultiFeature(ctx, rawMultiPolygon); - }, 'MultiPolygon type does not throw'); - - const polygonA = multiPolygon.features[0]; - const polygonB = multiPolygon.features[1]; - - assert.deepEqual(polygonA, new Polygon(ctx, { - id: polygonA.id, - type: 'Feature', - properties: {}, - geometry: { - coordinates: [[[1, 1], [2, 2], [3, 3], [4, 4], [1, 1]]], - type: 'Polygon' - } - })); - assert.deepEqual(polygonB, new Polygon(ctx, { - id: polygonB.id, - type: 'Feature', - properties: {}, - geometry: { - coordinates: [[[2, 1], [6, 2], [8, 3], [2, 4], [2, 1]], [[1, 1], [2, 2], [3, 3], [1, 1]]], - type: 'Polygon' - } - })); - - -}); - -test('MultiLineString via MultiFeature', () => { - const rawMultiLineString = { - type: 'Feature', - id: 'lineline', - properties: { g: 'h' }, - geometry: { - type: 'MultiLineString', - coordinates: [ - [[1, 1], [2, 2], [3, 3]], - [[4, 4], [5, 5], [6, 6]] - ] - } - }; - const ctx = createMockCtx(); - let multiLineString; - assert.doesNotThrow(() => { - multiLineString = new MultiFeature(ctx, rawMultiLineString); - }, 'MultiLineString type does not throw'); - - const lineStringA = multiLineString.features[0]; - const lineStringB = multiLineString.features[1]; - - assert.deepEqual(lineStringA, new LineString(ctx, { - id: lineStringA.id, - type: 'Feature', - properties: {}, - geometry: { - coordinates: [[1, 1], [2, 2], [3, 3]], - type: 'LineString' - } - })); - assert.deepEqual(lineStringB, new LineString(ctx, { - id: lineStringB.id, - type: 'Feature', - properties: {}, - geometry: { - coordinates: [[4, 4], [5, 5], [6, 6]], - type: 'LineString' - } - })); - - -}); - -test('Invalid MultiFeature type', () => { - const rawThing = { - type: 'Feature', - id: 'blergh', - properties: { g: 'h' }, - geometry: { - type: 'thing', - coordinates: [ - [[1, 1], [2, 2], [3, 3]], - [[4, 4], [5, 5], [6, 6]] - ] - } - }; - let thing; - assert.throws(() => { - thing = new MultiFeature(createMockCtx(), rawThing); - }, 'invalid type throws'); - assert.equal(thing, undefined); -}); diff --git a/test/multi_feature.test.ts b/test/multi_feature.test.ts new file mode 100644 index 00000000..0d48e62c --- /dev/null +++ b/test/multi_feature.test.ts @@ -0,0 +1,396 @@ +import './mock-browser'; +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { spy } from 'sinon'; +import Feature from '../src/feature_types/feature'; +import Point from '../src/feature_types/point'; +import Polygon from '../src/feature_types/polygon'; +import LineString from '../src/feature_types/line_string'; +import MultiFeature from '../src/feature_types/multi_feature'; +import createMockCtx from './utils/create_mock_feature_context'; +import getPublicMemberKeys from './utils/get_public_member_keys'; + +test('MultiPoint via MultiFeature', () => { + assert.ok(MultiFeature.prototype instanceof Feature, 'inherits from Feature'); + + // Prototype members + assert.equal( + typeof MultiFeature.prototype.isValid, + 'function', + 'polygon.isValid' + ); + assert.equal( + typeof MultiFeature.prototype.setCoordinates, + 'function', + 'polygon.setCoordinates' + ); + assert.equal( + typeof MultiFeature.prototype.getCoordinate, + 'function', + 'polygon.getCoordinate' + ); + assert.equal( + typeof MultiFeature.prototype.getCoordinates, + 'function', + 'polygon.getCoordinates' + ); + assert.equal( + typeof MultiFeature.prototype.updateCoordinate, + 'function', + 'polygon.updateCoordinate' + ); + assert.equal( + typeof MultiFeature.prototype.addCoordinate, + 'function', + 'polygon.addCoordinate' + ); + assert.equal( + typeof MultiFeature.prototype.removeCoordinate, + 'function', + 'polygon.removeCoordinate' + ); + assert.equal( + typeof MultiFeature.prototype.getFeatures, + 'function', + 'polygon.getFeatures' + ); + + assert.equal( + getPublicMemberKeys(MultiFeature.prototype).length, + 8, + 'no unexpected prototype members' + ); +}); + +test('MultiPoint', () => { + const rawMultiPoint = { + type: 'Feature', + id: 'wahoo', + properties: { foo: 'bar' }, + geometry: { + type: 'MultiPoint', + coordinates: [ + [1, 1], + [2, 2], + [3, 3] + ] + } + }; + const ctx = createMockCtx(); + let multiPoint; + assert.doesNotThrow(() => { + multiPoint = new MultiFeature(ctx, rawMultiPoint); + }, 'MultiPoint type does not throw'); + const changedSpy = spy(multiPoint, 'changed'); + + // Instance members + assert.equal(multiPoint.ctx, ctx, 'multiPoint.ctx'); + assert.equal(multiPoint.coordinates, undefined, 'no coordinates'); + assert.deepEqual( + multiPoint.properties, + { foo: 'bar' }, + 'multiPoint.properties' + ); + assert.equal(multiPoint.id, 'wahoo', 'multiPoint.id'); + assert.equal(multiPoint.type, 'MultiPoint', 'multiPoint.type'); + assert.equal(multiPoint.features.length, 3, 'multiPoint.features'); + // multiPoint.changed gets counted because it's used below + assert.equal( + getPublicMemberKeys(multiPoint).length, + 7, + 'no unexpected instance members' + ); + + const pointA = multiPoint.features[0]; + const pointB = multiPoint.features[1]; + const pointC = multiPoint.features[2]; + + assert.deepEqual( + pointA, + new Point(ctx, { + id: pointA.id, + type: 'Feature', + properties: {}, + geometry: { + coordinates: [1, 1], + type: 'Point' + } + }) + ); + assert.deepEqual( + pointB, + new Point(ctx, { + id: pointB.id, + type: 'Feature', + properties: {}, + geometry: { + coordinates: [2, 2], + type: 'Point' + } + }) + ); + assert.deepEqual( + pointC, + new Point(ctx, { + id: pointC.id, + type: 'Feature', + properties: {}, + geometry: { + coordinates: [3, 3], + type: 'Point' + } + }) + ); + + const pointAGetCoordinateSpy = spy(pointA, 'getCoordinate'); + const pointBGetCoordinateSpy = spy(pointB, 'getCoordinate'); + const pointCGetCoordinateSpy = spy(pointC, 'getCoordinate'); + const coordinate = multiPoint.getCoordinate('2'); + assert.equal( + pointAGetCoordinateSpy.callCount, + 0, + 'point A getCoordinate not called' + ); + assert.equal( + pointBGetCoordinateSpy.callCount, + 0, + 'point B getCoordinate not called' + ); + assert.equal(pointCGetCoordinateSpy.callCount, 1, 'point C getCoordinate'); + assert.deepEqual(coordinate, [3, 3], 'correct coordinate'); + + const pointAUpdateCoordinateSpy = spy(pointA, 'updateCoordinate'); + const pointBUpdateCoordinateSpy = spy(pointB, 'updateCoordinate'); + const pointCUpdateCoordinateSpy = spy(pointC, 'updateCoordinate'); + multiPoint.updateCoordinate('0', 99, 100); + assert.equal( + pointAUpdateCoordinateSpy.callCount, + 1, + 'point A updateCoordinate' + ); + assert.equal( + pointBUpdateCoordinateSpy.callCount, + 0, + 'point B updateCoordinate not called' + ); + assert.equal( + pointCUpdateCoordinateSpy.callCount, + 0, + 'point C updateCoordinate not called' + ); + assert.deepEqual( + multiPoint.getCoordinate('0'), + [99, 100], + 'correct coordinate' + ); + + assert.deepEqual( + multiPoint.getCoordinates(), + [ + [99, 100], + [2, 2], + [3, 3] + ], + 'getCoordinates returns the complete multi-coordinates' + ); + + multiPoint.setCoordinates([ + [6, 6], + [7, 7] + ]); + assert.equal(changedSpy.callCount, 2, 'changed called by setCoordinates'); + assert.deepEqual(multiPoint.getCoordinates(), [ + [6, 6], + [7, 7] + ]); + + assert.equal(multiPoint.isValid(), true, 'positive validation works'); + multiPoint.setCoordinates([[1], []]); + assert.equal(multiPoint.isValid(), false, 'negative validation works'); +}); + +// Tests below less in depth becuase we know the +// inner-workings are the same +test('MultiPolygon via MultiFeature', () => { + const rawMultiPolygon = { + type: 'Feature', + id: 'zing', + properties: { f: 'a' }, + geometry: { + type: 'MultiPolygon', + coordinates: [ + [ + [ + [1, 1], + [2, 2], + [3, 3], + [4, 4], + [1, 1] + ] + ], + [ + [ + [2, 1], + [6, 2], + [8, 3], + [2, 4], + [2, 1] + ], + [ + [1, 1], + [2, 2], + [3, 3], + [1, 1] + ] + ] + ] + } + }; + const ctx = createMockCtx(); + let multiPolygon; + assert.doesNotThrow(() => { + multiPolygon = new MultiFeature(ctx, rawMultiPolygon); + }, 'MultiPolygon type does not throw'); + + const polygonA = multiPolygon.features[0]; + const polygonB = multiPolygon.features[1]; + + assert.deepEqual( + polygonA, + new Polygon(ctx, { + id: polygonA.id, + type: 'Feature', + properties: {}, + geometry: { + coordinates: [ + [ + [1, 1], + [2, 2], + [3, 3], + [4, 4], + [1, 1] + ] + ], + type: 'Polygon' + } + }) + ); + assert.deepEqual( + polygonB, + new Polygon(ctx, { + id: polygonB.id, + type: 'Feature', + properties: {}, + geometry: { + coordinates: [ + [ + [2, 1], + [6, 2], + [8, 3], + [2, 4], + [2, 1] + ], + [ + [1, 1], + [2, 2], + [3, 3], + [1, 1] + ] + ], + type: 'Polygon' + } + }) + ); +}); + +test('MultiLineString via MultiFeature', () => { + const rawMultiLineString = { + type: 'Feature', + id: 'lineline', + properties: { g: 'h' }, + geometry: { + type: 'MultiLineString', + coordinates: [ + [ + [1, 1], + [2, 2], + [3, 3] + ], + [ + [4, 4], + [5, 5], + [6, 6] + ] + ] + } + }; + const ctx = createMockCtx(); + let multiLineString; + assert.doesNotThrow(() => { + multiLineString = new MultiFeature(ctx, rawMultiLineString); + }, 'MultiLineString type does not throw'); + + const lineStringA = multiLineString.features[0]; + const lineStringB = multiLineString.features[1]; + + assert.deepEqual( + lineStringA, + new LineString(ctx, { + id: lineStringA.id, + type: 'Feature', + properties: {}, + geometry: { + coordinates: [ + [1, 1], + [2, 2], + [3, 3] + ], + type: 'LineString' + } + }) + ); + assert.deepEqual( + lineStringB, + new LineString(ctx, { + id: lineStringB.id, + type: 'Feature', + properties: {}, + geometry: { + coordinates: [ + [4, 4], + [5, 5], + [6, 6] + ], + type: 'LineString' + } + }) + ); +}); + +test('Invalid MultiFeature type', () => { + const rawThing = { + type: 'Feature', + id: 'blergh', + properties: { g: 'h' }, + geometry: { + type: 'thing', + coordinates: [ + [ + [1, 1], + [2, 2], + [3, 3] + ], + [ + [4, 4], + [5, 5], + [6, 6] + ] + ] + } + }; + let thing; + assert.throws(() => { + thing = new MultiFeature(createMockCtx(), rawThing); + }, 'invalid type throws'); + assert.equal(thing, undefined); +}); diff --git a/test/options.test.js b/test/options.test.ts similarity index 73% rename from test/options.test.js rename to test/options.test.ts index aa256964..27d80e01 100644 --- a/test/options.test.js +++ b/test/options.test.ts @@ -1,17 +1,20 @@ /* eslint no-shadow:[0] */ +import './mock-browser'; import fs from 'fs'; import path from 'path'; import test from 'node:test'; import assert from 'node:assert/strict'; -import {fileURLToPath} from 'url'; +import { fileURLToPath } from 'url'; -import MapboxDraw from '../index.js'; -import modes from '../src/modes/index.js'; +import MapboxDraw from '../index'; +import * as modes from '../src/modes/index'; const __dirname = fileURLToPath(new URL('.', import.meta.url)); -const styleWithSourcesFixture = JSON.parse(fs.readFileSync(path.join(__dirname, './fixtures/style_with_sources.json'))); +const styleWithSourcesFixture = JSON.parse( + fs.readFileSync(path.join(__dirname, './fixtures/style_with_sources.json'), 'utf8') +); -test('Options test', async (t) => { +test('Options test', async t => { t.test('no options', () => { const Draw = new MapboxDraw(); const defaultOptions = { @@ -67,7 +70,7 @@ test('Options test', async (t) => { }); t.test('hide all controls', () => { - const Draw = new MapboxDraw({displayControlsDefault: false}); + const Draw = new MapboxDraw({ displayControlsDefault: false }); const defaultOptions = { defaultMode: 'simple_select', modes, @@ -93,7 +96,10 @@ test('Options test', async (t) => { }); await t.test('hide controls but show point', () => { - const Draw = new MapboxDraw({displayControlsDefault: false, controls: {point:true}}); + const Draw = new MapboxDraw({ + displayControlsDefault: false, + controls: { point: true } + }); const defaultOptions = { defaultMode: 'simple_select', modes, @@ -120,7 +126,7 @@ test('Options test', async (t) => { }); t.test('hide only point control', () => { - const Draw = new MapboxDraw({ controls: {point:false}}); + const Draw = new MapboxDraw({ controls: { point: false } }); const defaultOptions = { defaultMode: 'simple_select', modes, @@ -174,56 +180,61 @@ test('Options test', async (t) => { }); await t.test('custom styles', () => { - const Draw = new MapboxDraw({styles: [{ - 'id': 'custom-polygon', - 'type': 'fill', - 'filter': ['all', ['==', '$type', 'Polygon']], - 'paint': { - 'fill-color': '#fff' - } - }, { - 'id': 'custom-point', - 'type': 'circle', - 'filter': ['all', ['==', '$type', 'Point']], - 'paint': { - 'circle-color': '#fff' - } - }]}); + const Draw = new MapboxDraw({ + styles: [ + { + id: 'custom-polygon', + type: 'fill', + filter: ['all', ['==', '$type', 'Polygon']], + paint: { + 'fill-color': '#fff' + } + }, + { + id: 'custom-point', + type: 'circle', + filter: ['all', ['==', '$type', 'Point']], + paint: { + 'circle-color': '#fff' + } + } + ] + }); const styles = [ { - 'id': 'custom-polygon.cold', - 'source': 'mapbox-gl-draw-cold', - 'type': 'fill', - 'filter': ['all', ['==', '$type', 'Polygon']], - 'paint': { + id: 'custom-polygon.cold', + source: 'mapbox-gl-draw-cold', + type: 'fill', + filter: ['all', ['==', '$type', 'Polygon']], + paint: { 'fill-color': '#fff' } }, { - 'id': 'custom-point.cold', - 'source': 'mapbox-gl-draw-cold', - 'type': 'circle', - 'filter': ['all', ['==', '$type', 'Point']], - 'paint': { + id: 'custom-point.cold', + source: 'mapbox-gl-draw-cold', + type: 'circle', + filter: ['all', ['==', '$type', 'Point']], + paint: { 'circle-color': '#fff' } }, { - 'id': 'custom-polygon.hot', - 'source': 'mapbox-gl-draw-hot', - 'type': 'fill', - 'filter': ['all', ['==', '$type', 'Polygon']], - 'paint': { + id: 'custom-polygon.hot', + source: 'mapbox-gl-draw-hot', + type: 'fill', + filter: ['all', ['==', '$type', 'Polygon']], + paint: { 'fill-color': '#fff' } }, { - 'id': 'custom-point.hot', - 'source': 'mapbox-gl-draw-hot', - 'type': 'circle', - 'filter': ['all', ['==', '$type', 'Point']], - 'paint': { + id: 'custom-point.hot', + source: 'mapbox-gl-draw-hot', + type: 'circle', + filter: ['all', ['==', '$type', 'Point']], + paint: { 'circle-color': '#fff' } } diff --git a/test/point.test.js b/test/point.test.ts similarity index 59% rename from test/point.test.js rename to test/point.test.ts index 815824fc..60d5410b 100644 --- a/test/point.test.js +++ b/test/point.test.ts @@ -1,14 +1,15 @@ +import './mock-browser'; import test from 'node:test'; import assert from 'node:assert/strict'; -import {spy} from 'sinon'; -import Feature from '../src/feature_types/feature.js'; -import Point from '../src/feature_types/point.js'; -import MapboxDraw from '../index.js'; -import createFeature from './utils/create_feature.js'; -import getPublicMemberKeys from './utils/get_public_member_keys.js'; -import createMockCtx from './utils/create_mock_feature_context.js'; -import {drawGeometry} from './utils/draw_geometry.js'; -import createMap from './utils/create_map.js'; +import { spy } from 'sinon'; +import Feature from '../src/feature_types/feature'; +import Point from '../src/feature_types/point'; +import MapboxDraw from '../index'; +import createFeature from './utils/create_feature'; +import getPublicMemberKeys from './utils/get_public_member_keys'; +import createMockCtx from './utils/create_mock_feature_context'; +import { drawGeometry } from './utils/draw_geometry'; +import createMap from './utils/create_map'; test('Point constructor and API', () => { const rawPoint = createFeature('point'); @@ -17,17 +18,37 @@ test('Point constructor and API', () => { // Instance members assert.equal(point.ctx, ctx, 'point.ctx'); - assert.equal(point.coordinates, rawPoint.geometry.coordinates, 'point.coordinates'); + assert.equal( + point.coordinates, + rawPoint.geometry.coordinates, + 'point.coordinates' + ); assert.equal(point.properties, rawPoint.properties, 'point.properties'); assert.equal(point.id, rawPoint.id, 'point.id'); assert.equal(point.type, rawPoint.geometry.type, 'point.type'); - assert.equal(getPublicMemberKeys(point).length, 5, 'no unexpected instance members'); + assert.equal( + getPublicMemberKeys(point).length, + 5, + 'no unexpected instance members' + ); // Prototype members assert.equal(typeof Point.prototype.isValid, 'function', 'point.isValid'); - assert.equal(typeof Point.prototype.getCoordinate, 'function', 'point.getCoordinate'); - assert.equal(typeof Point.prototype.updateCoordinate, 'function', 'point.updateCoordinate'); - assert.equal(getPublicMemberKeys(Point.prototype).length, 3, 'no unexpected prototype members'); + assert.equal( + typeof Point.prototype.getCoordinate, + 'function', + 'point.getCoordinate' + ); + assert.equal( + typeof Point.prototype.updateCoordinate, + 'function', + 'point.updateCoordinate' + ); + assert.equal( + getPublicMemberKeys(Point.prototype).length, + 3, + 'no unexpected prototype members' + ); assert.ok(Point.prototype instanceof Feature, 'inherits from Feature'); }); @@ -40,12 +61,20 @@ test('Point#isValid', () => { const invalidRawPointA = createFeature('point'); invalidRawPointA.geometry.coordinates = [0, '1']; const invalidPointA = new Point(createMockCtx(), invalidRawPointA); - assert.equal(invalidPointA.isValid(), false, 'returns false with non-number coordinate'); + assert.equal( + invalidPointA.isValid(), + false, + 'returns false with non-number coordinate' + ); const invalidRawPointB = createFeature('point'); invalidRawPointB.geometry.coordinates = ['1', 0]; const invalidPointB = new Point(createMockCtx(), invalidRawPointA); - assert.equal(invalidPointB.isValid(), false, 'returns false with non-number coordinate, again'); + assert.equal( + invalidPointB.isValid(), + false, + 'returns false with non-number coordinate, again' + ); }); test('Point#updateCoordinate, Point#getCoordinate', () => { @@ -58,7 +87,11 @@ test('Point#updateCoordinate, Point#getCoordinate', () => { point.updateCoordinate(3, 4, 5); assert.equal(changedSpy.callCount, 1); - assert.deepEqual(point.getCoordinate(), [4, 5], 'handles 3 arguments, ignoring the first (as path)'); + assert.deepEqual( + point.getCoordinate(), + [4, 5], + 'handles 3 arguments, ignoring the first (as path)' + ); point.updateCoordinate(6, 7); assert.deepEqual(point.getCoordinate(), [6, 7], 'handles 2 arguments'); @@ -77,7 +110,11 @@ test('Point integration test', async () => { const feats = Draw.getAll().features; assert.equal(1, feats.length, 'only one'); assert.equal('Point', feats[0].geometry.type, 'of the right type'); - assert.deepEqual([10, 10], feats[0].geometry.coordinates, 'in the right spot'); + assert.deepEqual( + [10, 10], + feats[0].geometry.coordinates, + 'in the right spot' + ); Draw.onRemove(); }); diff --git a/test/polygon.test.js b/test/polygon.test.js deleted file mode 100644 index 5ebf2680..00000000 --- a/test/polygon.test.js +++ /dev/null @@ -1,139 +0,0 @@ -import test from 'node:test'; -import assert from 'node:assert/strict'; -import {spy} from 'sinon'; -import Feature from '../src/feature_types/feature.js'; -import Polygon from '../src/feature_types/polygon.js'; -import MapboxDraw from '../index.js'; -import createFeature from './utils/create_feature.js'; -import getPublicMemberKeys from './utils/get_public_member_keys.js'; -import createMockCtx from './utils/create_mock_feature_context.js'; -import {drawGeometry} from './utils/draw_geometry.js'; -import createMap from './utils/create_map.js'; - -test('Polygon constructor and API', () => { - const rawPolygon = createFeature('polygon'); - rawPolygon.geometry.coordinates = [[[1, 2], [3, 4], [5, 6], [7, 8], [1, 2]]]; - const ctx = createMockCtx(); - const polygon = new Polygon(ctx, rawPolygon); - - // Instance members - assert.equal(polygon.ctx, ctx, 'polygon.ctx'); - assert.deepEqual(polygon.coordinates, [[[1, 2], [3, 4], [5, 6], [7, 8]]], - 'polygon.coordinates remove the last coordinate of the ring (which matches the first)'); - assert.equal(polygon.properties, rawPolygon.properties, 'polygon.properties'); - assert.equal(polygon.id, rawPolygon.id, 'polygon.id'); - assert.equal(polygon.type, rawPolygon.geometry.type, 'polygon.type'); - assert.equal(getPublicMemberKeys(polygon).length, 5, 'no unexpected instance members'); - - // Prototype members - assert.equal(typeof Polygon.prototype.isValid, 'function', 'polygon.isValid'); - assert.equal(typeof Polygon.prototype.incomingCoords, 'function', 'polygon.incomingCoords'); - assert.equal(typeof Polygon.prototype.setCoordinates, 'function', 'polygon.setCoordinates'); - assert.equal(typeof Polygon.prototype.addCoordinate, 'function', 'polygon.addCoordinate'); - assert.equal(typeof Polygon.prototype.getCoordinate, 'function', 'polygon.getCoordinate'); - assert.equal(typeof Polygon.prototype.getCoordinates, 'function', 'polygon.getCoordinates'); - assert.equal(typeof Polygon.prototype.removeCoordinate, 'function', 'polygon.removeCoordinate'); - assert.equal(typeof Polygon.prototype.updateCoordinate, 'function', 'polygon.updateCoordinate'); - assert.equal(getPublicMemberKeys(Polygon.prototype).length, 8, 'no unexpected prototype members'); - - assert.ok(Polygon.prototype instanceof Feature, 'inherits from Feature'); -}); - -test('Polygon#isValid', () => { - const validRawPolygon = createFeature('polygon'); - const validPolygon = new Polygon(createMockCtx(), validRawPolygon); - assert.equal(validPolygon.isValid(), true, 'returns true for valid polygons'); - - const invalidRawPolygonA = createFeature('polygon'); - invalidRawPolygonA.geometry.coordinates = [[[1, 2], [3, 4], [5, 6]], [[7, 8], [9, 10]]]; - const invalidPolygonA = new Polygon(createMockCtx(), invalidRawPolygonA); - assert.equal(invalidPolygonA.isValid(), false, 'returns false when a ring has fewer than 3 coordinates'); -}); - -test('Polygon#incomingCoords, Polygon#getCoordinates', () => { - const rawPolygon = createFeature('polygon'); - const polygon = new Polygon(createMockCtx(), rawPolygon); - const changedSpy = spy(polygon, 'changed'); - - polygon.incomingCoords([[[1, 2], [3, 4], [5, 6], [1, 2]]]); - assert.equal(changedSpy.callCount, 1, 'calls polygon.changed'); - assert.deepEqual(polygon.coordinates, [[[1, 2], [3, 4], [5, 6]]], - 'sets new coordinates, eliminating last (closing) one'); - assert.deepEqual(polygon.getCoordinates(), [[[1, 2], [3, 4], [5, 6], [1, 2]]], - 'getCoordinates return closed rings'); -}); - -test('Polygon#setCoordinates', () => { - const rawPolygon = createFeature('polygon'); - const polygon = new Polygon(createMockCtx(), rawPolygon); - const changedSpy = spy(polygon, 'changed'); - - polygon.setCoordinates([[[1, 2], [3, 4], [5, 6]]]); - assert.equal(changedSpy.callCount, 1, 'polygon.changed called'); - assert.deepEqual(polygon.coordinates, [[[1, 2], [3, 4], [5, 6]]], - 'new coordinates set'); -}); - -test('Polygon#addCoordinate, Polygon#removeCoordinate', () => { - const rawPolygon = createFeature('polygon'); - rawPolygon.geometry.coordinates = [ - [[1, 1], [2, 2], [3, 3], [4, 4], [1, 1]], - [[2, 1], [3, 2], [4, 3], [5, 4], [2, 1]] - ]; - const polygon = new Polygon(createMockCtx(), rawPolygon); - const changedSpy = spy(polygon, 'changed'); - - changedSpy.resetHistory(); - polygon.addCoordinate('1.1', 99, 100); - assert.equal(changedSpy.callCount, 1, 'polygon.changed was called'); - assert.deepEqual(polygon.getCoordinates(), [ - [[1, 1], [2, 2], [3, 3], [4, 4], [1, 1]], - [[2, 1], [99, 100], [3, 2], [4, 3], [5, 4], [2, 1]] - ], 'new coordinate added at right place in right ring'); - - changedSpy.resetHistory(); - polygon.removeCoordinate('0.3'); - assert.equal(changedSpy.callCount, 1, 'polygon.changed was called'); - assert.deepEqual(polygon.getCoordinates(), [ - [[1, 1], [2, 2], [3, 3], [1, 1]], - [[2, 1], [99, 100], [3, 2], [4, 3], [5, 4], [2, 1]] - ], 'coordinate removed at right place in right ring'); -}); - -test('Polygon#updateCoordinate, Polygon#getCoordinate', () => { - const rawPolygon = createFeature('polygon'); - rawPolygon.geometry.coordinates = [ - [[1, 1], [2, 2], [3, 3], [4, 4], [1, 1]], - [[2, 1], [3, 2], [4, 3], [5, 4], [2, 1]] - ]; - const polygon = new Polygon(createMockCtx(), rawPolygon); - const changedSpy = spy(polygon, 'changed'); - - changedSpy.resetHistory(); - assert.deepEqual(polygon.getCoordinate('1.2'), [4, 3], 'getCoordinate returns right one'); - polygon.updateCoordinate('1.2', 99, 100); - assert.equal(changedSpy.callCount, 1, 'polygon.changed was called'); - assert.deepEqual(polygon.getCoordinates(), [ - [[1, 1], [2, 2], [3, 3], [4, 4], [1, 1]], - [[2, 1], [3, 2], [99, 100], [5, 4], [2, 1]] - ], 'correct coordinate was changed'); - assert.deepEqual(polygon.getCoordinate('1.2'), [99, 100], 'getCoordinate still works'); -}); - -test('Polygon integration', async () => { - const polygonCoordinates = [[[0, 0], [30, 15], [32, 35], [15, 30], [0, 0]]]; - const map = createMap(); - const Draw = new MapboxDraw(); - map.addControl(Draw); - - await map.on('load'); - - await drawGeometry(map, Draw, 'Polygon', polygonCoordinates); - - const feats = Draw.getAll().features; - assert.equal(1, feats.length, 'only one'); - assert.equal('Polygon', feats[0].geometry.type, 'of the right type'); - assert.equal(feats[0].geometry.coordinates[0].length, polygonCoordinates[0].length, 'right number of points'); - assert.deepEqual(feats[0].geometry.coordinates, polygonCoordinates, 'in the right spot'); - Draw.onRemove(); -}); diff --git a/test/polygon.test.ts b/test/polygon.test.ts new file mode 100644 index 00000000..2c800769 --- /dev/null +++ b/test/polygon.test.ts @@ -0,0 +1,346 @@ +import './mock-browser'; +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { spy } from 'sinon'; +import Feature from '../src/feature_types/feature'; +import Polygon from '../src/feature_types/polygon'; +import MapboxDraw from '../index'; +import createFeature from './utils/create_feature'; +import getPublicMemberKeys from './utils/get_public_member_keys'; +import createMockCtx from './utils/create_mock_feature_context'; +import { drawGeometry } from './utils/draw_geometry'; +import createMap from './utils/create_map'; + +test('Polygon constructor and API', () => { + const rawPolygon = createFeature('polygon'); + rawPolygon.geometry.coordinates = [ + [ + [1, 2], + [3, 4], + [5, 6], + [7, 8], + [1, 2] + ] + ]; + const ctx = createMockCtx(); + const polygon = new Polygon(ctx, rawPolygon); + + // Instance members + assert.equal(polygon.ctx, ctx, 'polygon.ctx'); + assert.deepEqual( + polygon.coordinates, + [ + [ + [1, 2], + [3, 4], + [5, 6], + [7, 8] + ] + ], + 'polygon.coordinates remove the last coordinate of the ring (which matches the first)' + ); + assert.equal(polygon.properties, rawPolygon.properties, 'polygon.properties'); + assert.equal(polygon.id, rawPolygon.id, 'polygon.id'); + assert.equal(polygon.type, rawPolygon.geometry.type, 'polygon.type'); + assert.equal( + getPublicMemberKeys(polygon).length, + 5, + 'no unexpected instance members' + ); + + // Prototype members + assert.equal(typeof Polygon.prototype.isValid, 'function', 'polygon.isValid'); + assert.equal( + typeof Polygon.prototype.incomingCoords, + 'function', + 'polygon.incomingCoords' + ); + assert.equal( + typeof Polygon.prototype.setCoordinates, + 'function', + 'polygon.setCoordinates' + ); + assert.equal( + typeof Polygon.prototype.addCoordinate, + 'function', + 'polygon.addCoordinate' + ); + assert.equal( + typeof Polygon.prototype.getCoordinate, + 'function', + 'polygon.getCoordinate' + ); + assert.equal( + typeof Polygon.prototype.getCoordinates, + 'function', + 'polygon.getCoordinates' + ); + assert.equal( + typeof Polygon.prototype.removeCoordinate, + 'function', + 'polygon.removeCoordinate' + ); + assert.equal( + typeof Polygon.prototype.updateCoordinate, + 'function', + 'polygon.updateCoordinate' + ); + assert.equal( + getPublicMemberKeys(Polygon.prototype).length, + 8, + 'no unexpected prototype members' + ); + + assert.ok(Polygon.prototype instanceof Feature, 'inherits from Feature'); +}); + +test('Polygon#isValid', () => { + const validRawPolygon = createFeature('polygon'); + const validPolygon = new Polygon(createMockCtx(), validRawPolygon); + assert.equal(validPolygon.isValid(), true, 'returns true for valid polygons'); + + const invalidRawPolygonA = createFeature('polygon'); + invalidRawPolygonA.geometry.coordinates = [ + [ + [1, 2], + [3, 4], + [5, 6] + ], + [ + [7, 8], + [9, 10] + ] + ]; + const invalidPolygonA = new Polygon(createMockCtx(), invalidRawPolygonA); + assert.equal( + invalidPolygonA.isValid(), + false, + 'returns false when a ring has fewer than 3 coordinates' + ); +}); + +test('Polygon#incomingCoords, Polygon#getCoordinates', () => { + const rawPolygon = createFeature('polygon'); + const polygon = new Polygon(createMockCtx(), rawPolygon); + const changedSpy = spy(polygon, 'changed'); + + polygon.incomingCoords([ + [ + [1, 2], + [3, 4], + [5, 6], + [1, 2] + ] + ]); + assert.equal(changedSpy.callCount, 1, 'calls polygon.changed'); + assert.deepEqual( + polygon.coordinates, + [ + [ + [1, 2], + [3, 4], + [5, 6] + ] + ], + 'sets new coordinates, eliminating last (closing) one' + ); + assert.deepEqual( + polygon.getCoordinates(), + [ + [ + [1, 2], + [3, 4], + [5, 6], + [1, 2] + ] + ], + 'getCoordinates return closed rings' + ); +}); + +test('Polygon#setCoordinates', () => { + const rawPolygon = createFeature('polygon'); + const polygon = new Polygon(createMockCtx(), rawPolygon); + const changedSpy = spy(polygon, 'changed'); + + polygon.setCoordinates([ + [ + [1, 2], + [3, 4], + [5, 6] + ] + ]); + assert.equal(changedSpy.callCount, 1, 'polygon.changed called'); + assert.deepEqual( + polygon.coordinates, + [ + [ + [1, 2], + [3, 4], + [5, 6] + ] + ], + 'new coordinates set' + ); +}); + +test('Polygon#addCoordinate, Polygon#removeCoordinate', () => { + const rawPolygon = createFeature('polygon'); + rawPolygon.geometry.coordinates = [ + [ + [1, 1], + [2, 2], + [3, 3], + [4, 4], + [1, 1] + ], + [ + [2, 1], + [3, 2], + [4, 3], + [5, 4], + [2, 1] + ] + ]; + const polygon = new Polygon(createMockCtx(), rawPolygon); + const changedSpy = spy(polygon, 'changed'); + + changedSpy.resetHistory(); + polygon.addCoordinate('1.1', 99, 100); + assert.equal(changedSpy.callCount, 1, 'polygon.changed was called'); + assert.deepEqual( + polygon.getCoordinates(), + [ + [ + [1, 1], + [2, 2], + [3, 3], + [4, 4], + [1, 1] + ], + [ + [2, 1], + [99, 100], + [3, 2], + [4, 3], + [5, 4], + [2, 1] + ] + ], + 'new coordinate added at right place in right ring' + ); + + changedSpy.resetHistory(); + polygon.removeCoordinate('0.3'); + assert.equal(changedSpy.callCount, 1, 'polygon.changed was called'); + assert.deepEqual( + polygon.getCoordinates(), + [ + [ + [1, 1], + [2, 2], + [3, 3], + [1, 1] + ], + [ + [2, 1], + [99, 100], + [3, 2], + [4, 3], + [5, 4], + [2, 1] + ] + ], + 'coordinate removed at right place in right ring' + ); +}); + +test('Polygon#updateCoordinate, Polygon#getCoordinate', () => { + const rawPolygon = createFeature('polygon'); + rawPolygon.geometry.coordinates = [ + [ + [1, 1], + [2, 2], + [3, 3], + [4, 4], + [1, 1] + ], + [ + [2, 1], + [3, 2], + [4, 3], + [5, 4], + [2, 1] + ] + ]; + const polygon = new Polygon(createMockCtx(), rawPolygon); + const changedSpy = spy(polygon, 'changed'); + + changedSpy.resetHistory(); + assert.deepEqual( + polygon.getCoordinate('1.2'), + [4, 3], + 'getCoordinate returns right one' + ); + polygon.updateCoordinate('1.2', 99, 100); + assert.equal(changedSpy.callCount, 1, 'polygon.changed was called'); + assert.deepEqual( + polygon.getCoordinates(), + [ + [ + [1, 1], + [2, 2], + [3, 3], + [4, 4], + [1, 1] + ], + [ + [2, 1], + [3, 2], + [99, 100], + [5, 4], + [2, 1] + ] + ], + 'correct coordinate was changed' + ); + assert.deepEqual( + polygon.getCoordinate('1.2'), + [99, 100], + 'getCoordinate still works' + ); +}); + +test('Polygon integration', async () => { + const polygonCoordinates = [ + [ + [0, 0], + [30, 15], + [32, 35], + [15, 30], + [0, 0] + ] + ]; + const map = createMap(); + const Draw = new MapboxDraw(); + map.addControl(Draw); + + await map.on('load'); + + await drawGeometry(map, Draw, 'Polygon', polygonCoordinates); + + const feats = Draw.getAll().features; + assert.equal(1, feats.length, 'only one'); + assert.equal('Polygon', feats[0].geometry.type, 'of the right type'); + assert.equal( + feats[0].geometry.coordinates[0].length, + polygonCoordinates[0].length, + 'right number of points' + ); + assert.deepEqual( + feats[0].geometry.coordinates, + polygonCoordinates, + 'in the right spot' + ); + Draw.onRemove(); +}); diff --git a/test/simple_select.test.js b/test/simple_select.test.js deleted file mode 100644 index b82132c1..00000000 --- a/test/simple_select.test.js +++ /dev/null @@ -1,581 +0,0 @@ -/* eslint no-shadow:[0] */ -import test from 'node:test'; -import assert from 'node:assert/strict'; -import createSyntheticEvent from 'synthetic-dom-events'; -import {spy} from 'sinon'; - -import MapboxDraw from '../index.js'; -import {setupAfterNextRender} from './utils/after_next_render.js'; -import makeMouseEvent from './utils/make_mouse_event.js'; -import mouseClick from './utils/mouse_click.js'; -import makeTouchEvent from './utils/make_touch_event.js'; -import getGeoJSON from './utils/get_geojson.js'; -import createMap from './utils/create_map.js'; -import createMockDrawModeContext from './utils/create_mock_draw_mode_context.js'; -import { TAP_INTERVAL, TAP_TOLERANCE } from '../src/lib/is_tap.js'; - -test('simple_select', async (t) => { - const context = createMockDrawModeContext(); - const mapContainer = document.createElement('div'); - document.body.appendChild(mapContainer); - const map = createMap({ container: mapContainer }); - - const Draw = new MapboxDraw(); - map.addControl(Draw); - - spy(map, 'fire'); - spy(map.doubleClickZoom, 'enable'); - spy(map.doubleClickZoom, 'disable'); - spy(map.dragPan, 'enable'); - spy(map.dragPan, 'disable'); - - const afterNextRender = setupAfterNextRender(map); - - const cleanUp = function() { - Draw.deleteAll(); - map.fire.resetHistory(); - }; - - const getFireArgs = function() { - const args = []; - for (let i = 0; i < map.fire.callCount; i++) { - args.push(map.fire.getCall(i).args); - } - return args; - }; - - t.test('simple_select - init map for tests', () => { - const done = function() { - map.off('load', done); - }; - - if (map.loaded()) { - done(); - } else { - map.on('load', done); - } - }); - - - await t.test('simple_select - box select', async () => { - Draw.add(getGeoJSON('negativePoint')); - const id = Draw.add(getGeoJSON('point'))[0]; - map.fire.resetHistory(); - - await afterNextRender(); - map.dragPan.enable.resetHistory(); - map.dragPan.disable.resetHistory(); - map.fire('mousedown', makeMouseEvent(0, 0, { shiftKey: true })); - assert.equal(map.dragPan.disable.callCount, 1, 'disable dragPan'); - map.fire('mousemove', makeMouseEvent(15, 15, { - shiftKey: true, - buttons: 1 - })); - await afterNextRender(); - assert.equal(map.getContainer().className.indexOf('mouse-add') > -1, true, 'mouse-add class has been set'); - map.fire('mouseup', makeMouseEvent(15, 15, { shiftKey: true })); - - await afterNextRender(); - assert.equal(map.getContainer().className.indexOf('mouse-move') > -1, true, 'mouse-move class has been set'); - const fireArgs = getFireArgs(); - const args = fireArgs.filter(arg => arg[0] === 'draw.selectionchange'); - assert.equal(args.length, 1, 'should have one and only one selectionchange event'); - if (args.length > 0) { - assert.equal(args[0][1].features.length, 1, 'should have one feature selected'); - assert.equal(args[0][1].features[0].id, id, 'should be the feature we expect to be selected'); - } - - const actionableArgs = fireArgs.filter(arg => arg[0] === 'draw.actionable'); - assert.ok(actionableArgs.length > 0, 'should have fired an actionable event'); - if (actionableArgs.length > 0) { - const actionable = actionableArgs[actionableArgs.length - 1][1]; - assert.equal(actionable.actions.combineFeatures, false, 'should fire correct combine actionable'); - assert.equal(actionable.actions.uncombineFeatures, false, 'should fire correct uncombine actionable'); - assert.equal(actionable.actions.trash, true, 'should fire correct trash actionable'); - } - - cleanUp(); - }); - - await t.test('simple_select - box select many features', async () => { - const features = []; - for (let i = 0; i < 5; i++) { - features.push(getGeoJSON('point')); - } - const ids = Draw.add({ - type: 'FeatureCollection', - features - }); - map.fire.resetHistory(); - - await afterNextRender(); - map.dragPan.enable.resetHistory(); - map.fire('mousedown', makeMouseEvent(0, 0, { shiftKey: true })); - map.fire('mousemove', makeMouseEvent(15, 15, { - shiftKey: true, - buttons: 1 - })); - map.fire('mouseup', makeMouseEvent(15, 15, { shiftKey: true })); - - await afterNextRender(); - const fireArgs = getFireArgs(); - const args = fireArgs.filter(arg => arg[0] === 'draw.selectionchange'); - assert.equal(args.length, 1, 'should have one and only one select event'); - if (args.length > 0) { - assert.equal(args[0][1].features.length, ids.length, 'should have all features selected'); - } - - const actionableArgs = fireArgs.filter(arg => arg[0] === 'draw.actionable'); - assert.ok(actionableArgs.length > 0, 'should have fired an actionable event'); - if (actionableArgs.length > 0) { - const actionable = actionableArgs[actionableArgs.length - 1][1]; - assert.equal(actionable.actions.combineFeatures, true, 'should fire correct combine actionable'); - assert.equal(actionable.actions.uncombineFeatures, false, 'should fire correct uncombine actionable'); - assert.equal(actionable.actions.trash, true, 'should fire correct trash actionable'); - } - - cleanUp(); - }); - - await t.test('simple_select - box select over no points', async () => { - Draw.add(getGeoJSON('point')); - map.fire.resetHistory(); - - await afterNextRender(); - map.fire('mousedown', makeMouseEvent(0, 0, { shiftKey: true })); - map.fire('mousemove', makeMouseEvent(-15, -15, { - shiftKey: true, - buttons: 1 - })); - map.fire('mouseup', makeMouseEvent(-15, -15, { shiftKey: true })); - - await afterNextRender(); - assert.equal(getFireArgs().filter(arg => arg[0] === 'draw.selectionchange').length, 0, 'there should be no draw.selectionchange event'); - cleanUp(); - }); - - await t.test('simple_select - box select then mousemove', async () => { - Draw.add(getGeoJSON('point')); - map.fire.resetHistory(); - - await afterNextRender(); - map.fire('mousedown', makeMouseEvent(0, 0, { shiftKey: true })); - map.fire('mousemove', makeMouseEvent(15, 15, { shiftKey: true })); - // This mousemove (not a drag) cancels the box select - map.fire('mousemove', makeMouseEvent(15, 15)); - map.fire('mouseup', makeMouseEvent(15, 15, { shiftKey: true })); - - await afterNextRender(); - assert.equal(getFireArgs().filter(arg => arg[0] === 'draw.selectionchange').length, 0, 'there should be no draw.selectionchange event'); - cleanUp(); - }); - - await t.test('simple_select - deselect', async () => { - const id = Draw.add(getGeoJSON('point'))[0]; - Draw.changeMode('simple_select', { featureIds: [id] }); - - await afterNextRender(); - map.fire.resetHistory(); - map.fire('mousedown', makeMouseEvent(15, 15)); - map.fire('mouseup', makeMouseEvent(15, 15)); - - await afterNextRender(); - const args = getFireArgs().filter(arg => arg[0] === 'draw.selectionchange'); - assert.equal(args.length, 1, 'should have one and only one selectionchange event'); - if (args.length > 0) { - assert.equal(args[0][1].features.length, 0, 'should have no features selected'); - } - - cleanUp(); - }); - - await t.test('simple_select - click on a deselected feature', async () => { - const id = Draw.add(getGeoJSON('polygon'))[0]; - Draw.changeMode('simple_select'); - - await afterNextRender(); - map.fire.resetHistory(); - map.doubleClickZoom.disable.resetHistory(); - map.fire('mousedown', makeMouseEvent(50, 30)); - map.fire('mouseup', makeMouseEvent(50, 30)); - - await afterNextRender(); - assert.equal(map.doubleClickZoom.disable.callCount, 1, 'disable doubleClickZoom'); - let args = getFireArgs(); - args = args.filter(arg => arg[0] === 'draw.selectionchange'); - assert.equal(args.length, 1, 'should have one and only one selectionchange event'); - if (args.length > 0) { - assert.equal(args[0][1].features.length, 1, 'should have only one feature selected'); - assert.equal(args[0][1].features[0].id, id, 'should be the feature we expect to be selected'); - } - cleanUp(); - }); - - await t.test('simple_select - tap on a deselected feature', async () => { - const id = Draw.add(getGeoJSON('polygon'))[0]; - Draw.changeMode('simple_select'); - - await afterNextRender(); - map.fire.resetHistory(); - map.doubleClickZoom.disable.resetHistory(); - map.fire('touchstart', makeTouchEvent(50, 30)); - map.fire('touchend', makeTouchEvent(50, 30)); - - await afterNextRender(); - let args = getFireArgs(); - args = args.filter(arg => arg[0] === 'draw.selectionchange'); - assert.equal(args.length, 1, 'should have one and only one selectionchange event'); - if (args.length > 0) { - assert.equal(args[0][1].features.length, 1, 'should have only one feature selected'); - assert.equal(args[0][1].features[0].id, id, 'should be the feature we expect to be selected'); - } - cleanUp(); - }); - - await t.test('simple_select - click on a selected feature with shift down', async () => { - const id = Draw.add(getGeoJSON('polygon'))[0]; - Draw.changeMode('simple_select', { featureIds: [id] }); - - await afterNextRender(); - map.fire.resetHistory(); - map.doubleClickZoom.disable.resetHistory(); - map.fire('mousedown', makeMouseEvent(50, 30, { shiftKey: true })); - map.fire('mouseup', makeMouseEvent(50, 30, { shiftKey: true })); - - await afterNextRender(); - assert.equal(map.doubleClickZoom.disable.callCount, 1, 'disable doubleClickZoom'); - assert.equal(map.getContainer().className.indexOf('mouse-pointer') > -1, true, 'mouse-pointer class has been set'); - let args = getFireArgs(); - args = args.filter(arg => arg[0] === 'draw.selectionchange'); - assert.equal(args.length, 1, 'should have one and only one selectionchange event'); - if (args.length > 0) { - assert.equal(args[0][1].features.length, 0, 'should have no features selected'); - } - - cleanUp(); - }); - - await t.test('simple_select - delete selected features', async () => { - const id = Draw.add(getGeoJSON('polygon'))[0]; - Draw.changeMode('simple_select', { featureIds: [id] }); - map.fire.resetHistory(); - Draw.trash(); - await afterNextRender(); - let args = getFireArgs(); - args = args.filter(arg => arg[0] === 'draw.delete'); - assert.equal(args.length, 1, 'should have one and only one draw.delete event'); - assert.equal(args[0][1].features.length, 1, 'should delete only one feature'); - assert.equal(args[0][1].features[0].id, id, 'should delete the feature we expect it to delete'); - - const selectedFeatures = Draw.getSelectedIds(); - assert.equal(selectedFeatures.length, 0, 'nothing should be selected anymore'); - cleanUp(); - }); - - await t.test('simple_select - click on a selected feature with shift up to enter direct_select', async () => { - Draw.deleteAll(); - const id = Draw.add(getGeoJSON('polygon'))[0]; - Draw.changeMode('simple_select', { featureIds: [id] }); - - await afterNextRender(); - map.doubleClickZoom.enable.resetHistory(); - map.fire.resetHistory(); - map.doubleClickZoom.disable.resetHistory(); - map.fire('mousedown', makeMouseEvent(50, 30, false)); - map.fire('mouseup', makeMouseEvent(50, 30, false)); - - await afterNextRender(); - assert.equal(map.doubleClickZoom.disable.callCount, 2, 'disable doubleClickZoom. Once for click, once for direct_select'); - assert.equal(map.doubleClickZoom.enable.callCount, 1, 'double click zoom has been enabled'); - assert.equal(map.getContainer().className.indexOf('mouse-move') > -1, true, 'mouse-move class has been set'); - let args = getFireArgs(); - args = args.filter(arg => arg[0] === 'draw.modechange'); - assert.equal(args.length, 1, 'should have one and only one modechange event'); - if (args.length > 0) { - assert.equal(args[0][1].mode, 'direct_select', 'should change to direct select'); - } - cleanUp(); - }); - - await t.test('simple_select - click on a vertex to enter direct_select', async () => { - const id = Draw.add(getGeoJSON('polygon'))[0]; - Draw.changeMode('simple_select', { featureIds: [id] }); - - const clickPosition = getGeoJSON('polygon').geometry.coordinates[0][0]; - - await afterNextRender(); - map.doubleClickZoom.enable.resetHistory(); - map.fire.resetHistory(); - map.fire('mousedown', makeMouseEvent(clickPosition[0], clickPosition[1])); - map.fire('mouseup', makeMouseEvent(clickPosition[0], clickPosition[1])); - - await afterNextRender(); - assert.equal(map.doubleClickZoom.enable.callCount, 1, 'double click zoom has been enabled'); - let args = getFireArgs(); - args = args.filter(arg => arg[0] === 'draw.modechange'); - assert.equal(args.length, 1, 'should have one and only one modechange event'); - if (args.length > 0) { - assert.equal(args[0][1].mode, 'direct_select', 'should change to direct select'); - } - cleanUp(); - }); - - await t.test('simple_select - tap on a vertex to enter direct_select', async () => { - const id = Draw.add(getGeoJSON('polygon'))[0]; - Draw.changeMode('simple_select', { featureIds: [id] }); - - const tapPosition = getGeoJSON('polygon').geometry.coordinates[0][0]; - - await afterNextRender(); - map.doubleClickZoom.enable.resetHistory(); - map.fire.resetHistory(); - map.fire('touchstart', makeTouchEvent(tapPosition[0], tapPosition[1])); - map.fire('touchend', makeTouchEvent(tapPosition[0], tapPosition[1])); - - await afterNextRender(); - let args = getFireArgs(); - args = args.filter(arg => arg[0] === 'draw.modechange'); - assert.equal(args.length, 1, 'should have one and only one modechange event'); - if (args.length > 0) { - assert.equal(args[0][1].mode, 'direct_select', 'should change to direct select'); - } - cleanUp(); - }); - - await t.test('simple_select - tap dragging fires an update event', async () => { - const point = getGeoJSON('point'); - const pointId = Draw.add(point)[0]; - const translatePosition = (point, d) => [point[0] + d, point[1] + d]; - - const startPosition = point.geometry.coordinates; - const endPosition = translatePosition(startPosition, 25); - - Draw.changeMode('simple_select', { - featureIds: [pointId] - }); - - await afterNextRender(); - map.fire.resetHistory(); - map.fire('touchstart', makeTouchEvent(...startPosition)); - map.fire('touchmove', makeTouchEvent(...translatePosition(startPosition, 15))); - map.fire('touchmove', makeTouchEvent(...endPosition)); - map.fire('touchend', makeTouchEvent(...endPosition)); - - const movedPoint = Draw.get(pointId); - const args = getFireArgs().filter(arg => arg[0] === 'draw.update'); - assert.equal(args.length, 1, 'draw.update called once'); - assert.equal(movedPoint.geometry.coordinates[0], endPosition[0], 'point lng moved to touchend location'); - assert.equal(movedPoint.geometry.coordinates[1], endPosition[1], 'point lat moved to touchend location'); - }); - - await t.test('simple_select - click on a deselected feature with shift down while having another feature selected', async () => { - const pointId = Draw.add(getGeoJSON('point'))[0]; - const id = Draw.add(getGeoJSON('polygon'))[0]; - Draw.changeMode('simple_select', { featureIds: [pointId] }); - - await afterNextRender(); - map.fire.resetHistory(); - map.fire('mousedown', makeMouseEvent(50, 30, { shiftKey: true })); - map.fire('mouseup', makeMouseEvent(50, 30, { shiftKey: true })); - - await afterNextRender(); - assert.equal(map.getContainer().className.indexOf('mouse-move') > -1, true, 'mouse-move class has been set'); - assert.equal(Draw.getSelectedIds().indexOf(pointId) !== -1, true, 'point is still selected'); - assert.equal(Draw.getSelectedIds().indexOf(id) !== -1, true, 'polygon is now selected'); - let args = getFireArgs(); - args = args.filter(arg => arg[0] === 'draw.selectionchange'); - assert.equal(args.length, 1, 'should have one and only one selectionchange event'); - if (args.length > 0) { - assert.equal(args[0][1].features.length, 2, 'should have two features selected'); - assert.equal(args[0][1].features[0].id, pointId, 'selection includes point'); - assert.equal(args[0][1].features[1].id, id, 'selection includes polygon'); - } - cleanUp(); - }); - - await t.test('simple_select - click on a deselected feature with shift up, while having another feature selected', async () => { - const pointId = Draw.add(getGeoJSON('point'))[0]; - const id = Draw.add(getGeoJSON('polygon'))[0]; - Draw.changeMode('simple_select', { featureIds: [pointId] }); - - await afterNextRender(); - map.fire.resetHistory(); - map.fire('mousedown', makeMouseEvent(50, 30, false)); - map.fire('mouseup', makeMouseEvent(50, 30, false)); - - await afterNextRender(); - assert.equal(map.getContainer().className.indexOf('mouse-move') > -1, true, 'mouse-move class has been set'); - assert.equal(Draw.getSelectedIds().indexOf(pointId) === -1, true, 'point is no longer selected'); - assert.equal(Draw.getSelectedIds().indexOf(id) !== -1, true, 'polygon is now selected'); - let args = getFireArgs(); - args = args.filter(arg => arg[0] === 'draw.selectionchange'); - assert.equal(args.length, 1, 'should have one and only one selectionchange event'); - if (args.length > 0) { - assert.equal(args[0][1].features.length, 1, 'should have only one feature selected'); - assert.equal(args[0][1].features[0].id, id, 'should be the feature we expect to be selected'); - } - cleanUp(); - }); - - await t.test('simple_select - drag every feature type', async () => { - const pointId = Draw.add(getGeoJSON('point'))[0]; - const multiPointId = Draw.add(getGeoJSON('multiPoint'))[0]; - const lineStringId = Draw.add(getGeoJSON('line'))[0]; - const multiLineStringId = Draw.add(getGeoJSON('multiLineString'))[0]; - const polygonId = Draw.add(getGeoJSON('polygon'))[0]; - const multiPolygonId = Draw.add(getGeoJSON('multiPolygon'))[0]; - - const countPositions = function(feature) { - return feature.geometry.coordinates.join(',').split(',').length; - }; - - const startPosition = getGeoJSON('point').geometry.coordinates; - Draw.changeMode('simple_select', { - featureIds: [pointId, multiPointId, lineStringId, multiLineStringId, polygonId, multiPolygonId] - }); - await afterNextRender(); - map.fire.resetHistory(); - map.fire('mousedown', makeMouseEvent(startPosition[0], startPosition[1])); - map.fire('mousemove', makeMouseEvent(startPosition[0] + 15, startPosition[1] + 15, { buttons: 1 })); - map.fire('mousemove', makeMouseEvent(startPosition[0] + 25, startPosition[1] + 25, { buttons: 1 })); - map.fire('mouseup', makeMouseEvent(startPosition[0] + 25, startPosition[1] + 25)); - - const movedPoint = Draw.get(pointId); - assert.equal(movedPoint.geometry.coordinates[0], startPosition[0] + 25, 'point lng moved'); - assert.equal(movedPoint.geometry.coordinates[1], startPosition[1] + 25, 'point lat moved'); - assert.equal(countPositions(movedPoint), countPositions(getGeoJSON('point')), 'point has same number of postions'); - - const movedMultiPoint = Draw.get(multiPointId); - assert.equal(movedMultiPoint.geometry.coordinates[0][0], getGeoJSON('multiPoint').geometry.coordinates[0][0] + 25, 'multipoint lng moved'); - assert.equal(movedMultiPoint.geometry.coordinates[0][1], getGeoJSON('multiPoint').geometry.coordinates[0][1] + 25, 'multipoint lat moved'); - assert.equal(countPositions(movedMultiPoint), countPositions(getGeoJSON('multiPoint')), 'multiPoint has same number of postions'); - - const movedLineString = Draw.get(lineStringId); - assert.equal(movedLineString.geometry.coordinates[0][0], getGeoJSON('line').geometry.coordinates[0][0] + 25, 'line lng moved'); - assert.equal(movedLineString.geometry.coordinates[0][1], getGeoJSON('line').geometry.coordinates[0][1] + 25, 'line lat moved'); - assert.equal(countPositions(movedLineString), countPositions(getGeoJSON('line')), 'line has same number of postions'); - - const movedMultiLineString = Draw.get(multiLineStringId); - assert.equal(movedMultiLineString.geometry.coordinates[0][0][0], getGeoJSON('multiLineString').geometry.coordinates[0][0][0] + 25, 'multiLineString lng moved'); - assert.equal(movedMultiLineString.geometry.coordinates[0][0][1], getGeoJSON('multiLineString').geometry.coordinates[0][0][1] + 25, 'multiLineString lat moved'); - assert.equal(countPositions(movedMultiLineString), countPositions(getGeoJSON('multiLineString')), 'multiLineString has same number of postions'); - - const movedPolygon = Draw.get(polygonId); - assert.equal(movedPolygon.geometry.coordinates[0][0][0], getGeoJSON('polygon').geometry.coordinates[0][0][0] + 25, 'polygon lng moved'); - assert.equal(movedPolygon.geometry.coordinates[0][0][1], getGeoJSON('polygon').geometry.coordinates[0][0][1] + 25, 'polygon lat moved'); - assert.equal(countPositions(movedPolygon), countPositions(getGeoJSON('polygon')), 'polygon has same number of postions'); - - const movedMultiPolygon = Draw.get(multiPolygonId); - assert.equal(movedMultiPolygon.geometry.coordinates[0][0][0][0], getGeoJSON('multiPolygon').geometry.coordinates[0][0][0][0] + 25, 'multiPolygon lng moved'); - assert.equal(movedMultiPolygon.geometry.coordinates[0][0][0][1], getGeoJSON('multiPolygon').geometry.coordinates[0][0][0][1] + 25, 'multiPolygon lat moved'); - assert.equal(countPositions(movedMultiPolygon), countPositions(getGeoJSON('multiPolygon')), 'multiPolygon has same number of postions'); - - cleanUp(); - }); - - await t.test('simple_select - interrupt drag move with mousemove', async () => { - const pointId = Draw.add(getGeoJSON('point'))[0]; - Draw.changeMode('simple_select', { featureIds: [pointId] }); - const startPosition = getGeoJSON('point').geometry.coordinates; - await afterNextRender(); - map.fire.resetHistory(); - map.fire('mousedown', makeMouseEvent(startPosition[0], startPosition[1])); - // Dragging - map.fire('mousemove', makeMouseEvent(startPosition[0] + 15, startPosition[1] + 15, { buttons: 1 })); - // Not dragging - map.fire('mousemove', makeMouseEvent(startPosition[0] + 25, startPosition[1] + 25)); - map.fire('mouseup', makeMouseEvent(startPosition[0] + 25, startPosition[1] + 25)); - - const movedPoint = Draw.get(pointId); - assert.equal(movedPoint.geometry.coordinates[0], startPosition[0] + 15, 'point lng moved only the first amount'); - assert.equal(movedPoint.geometry.coordinates[1], startPosition[1] + 15, 'point lat moved only the first amount'); - - cleanUp(); - }); - - await t.test('simple_select - fire one update when dragging mouse leaves container and button is released outside', async () => { - const pointId = Draw.add(getGeoJSON('point'))[0]; - Draw.changeMode('simple_select', { featureIds: [pointId] }); - const startPosition = getGeoJSON('point').geometry.coordinates; - await afterNextRender(); - map.fire.resetHistory(); - map.fire('mousedown', makeMouseEvent(startPosition[0], startPosition[1])); - map.fire('mousemove', makeMouseEvent(startPosition[0] + 15, startPosition[1] + 15, { buttons: 1 })); - mapContainer.dispatchEvent(createSyntheticEvent('mouseout')); - map.fire('mousemove', makeMouseEvent(startPosition[0] + 25, startPosition[1] + 25)); - map.fire('mouseup', makeMouseEvent(startPosition[0] + 25, startPosition[1] + 25)); - - const movedPoint = Draw.get(pointId); - const args = getFireArgs().filter(arg => arg[0] === 'draw.update'); - assert.equal(args.length, 1, 'draw.update called once'); - assert.equal(movedPoint.geometry.coordinates[0], startPosition[0] + 15, 'point lng moved only the first amount'); - assert.equal(movedPoint.geometry.coordinates[1], startPosition[1] + 15, 'point lat moved only the first amount'); - - cleanUp(); - }); - - await t.test('simple_select - fire two update when dragging mouse leaves container then returns and button is released inside', async () => { - const pointId = Draw.add(getGeoJSON('point'))[0]; - Draw.changeMode('simple_select', { featureIds: [pointId] }); - const startPosition = getGeoJSON('point').geometry.coordinates; - await afterNextRender(); - map.fire.resetHistory(); - map.fire('mousedown', makeMouseEvent(startPosition[0], startPosition[1])); - map.fire('mousemove', makeMouseEvent(startPosition[0] + 15, startPosition[1] + 15, { buttons: 1 })); - mapContainer.dispatchEvent(createSyntheticEvent('mouseout')); - map.fire('mousemove', makeMouseEvent(startPosition[0] + 25, startPosition[1] + 25, { buttons: 1 })); - map.fire('mouseup', makeMouseEvent(startPosition[0] + 25, startPosition[1] + 25)); - - const movedPoint = Draw.get(pointId); - const args = getFireArgs().filter(arg => arg[0] === 'draw.update'); - assert.equal(args.length, 2, 'draw.update called twice'); - assert.equal(movedPoint.geometry.coordinates[0], startPosition[0] + 25, 'point lng moved to the mouseup location'); - assert.equal(movedPoint.geometry.coordinates[1], startPosition[1] + 25, 'point lat moved to the mouseup location'); - }); - - t.test('simple_select - on closing invalid line', () => { - Draw.changeMode('draw_line_string'); - mouseClick(map, makeMouseEvent(1, 1)); - mouseClick(map, makeMouseEvent(1, 1)); - assert.equal(Draw.getMode(), 'simple_select', 'should be in simple_select mode'); - assert.equal(Draw.getSelected().features.length, 0, 'should not get any selected features'); - assert.equal(context.store._emitSelectionChange, undefined, 'should not emit selection change'); - cleanUp(); - }); - - t.test('simple_select - on closing invalid polygon', () => { - Draw.changeMode('draw_polygon'); - mouseClick(map, makeMouseEvent(1, 1)); - mouseClick(map, makeMouseEvent(16, 16)); - mouseClick(map, makeMouseEvent(16, 16)); - assert.equal(Draw.getMode(), 'simple_select', 'should be in simple_select mode'); - assert.equal(Draw.getSelected().features.length, 0, 'should not get any selected features'); - assert.equal(context.store._emitSelectionChange, undefined, 'should not emit selection change'); - cleanUp(); - }); - - await t.test('simple_select - fire one update after moving point with touch', async () => { - const pointId = Draw.add(getGeoJSON('point'))[0]; - Draw.changeMode('simple_select', { featureIds: [pointId] }); - const startPosition = getGeoJSON('point').geometry.coordinates; - const endPosition = [startPosition[0] + TAP_TOLERANCE, startPosition[1] + TAP_TOLERANCE]; - await afterNextRender(); - map.fire.resetHistory(); - map.doubleClickZoom.disable.resetHistory(); - map.fire('touchstart', makeTouchEvent(startPosition[0], startPosition[1])); - - await new Promise(resolve => setTimeout(resolve, TAP_INTERVAL)); - - map.fire('touchmove', makeTouchEvent(endPosition[0], endPosition[1])); - map.fire('touchend', makeTouchEvent(endPosition[0], endPosition[1])); - - const movedPoint = Draw.get(pointId); - const args = getFireArgs().filter(arg => arg[0] === 'draw.update'); - assert.equal(args.length, 1, 'draw.update called once'); - assert.equal(movedPoint.geometry.coordinates[0], endPosition[0], 'point lng moved to the last touchmove position'); - assert.equal(movedPoint.geometry.coordinates[1], endPosition[1], 'point lat moved the last touchmove position'); - cleanUp(); - }); - - document.body.removeChild(mapContainer); -}); diff --git a/test/simple_select.test.ts b/test/simple_select.test.ts new file mode 100644 index 00000000..d83e0cea --- /dev/null +++ b/test/simple_select.test.ts @@ -0,0 +1,1046 @@ +/* eslint no-shadow:[0] */ +import './mock-browser'; +import test from 'node:test'; +import assert from 'node:assert/strict'; +import createSyntheticEvent from 'synthetic-dom-events'; +import { spy } from 'sinon'; + +import MapboxDraw from '../index'; +import { setupAfterNextRender } from './utils/after_next_render'; +import makeMouseEvent from './utils/make_mouse_event'; +import mouseClick from './utils/mouse_click'; +import makeTouchEvent from './utils/make_touch_event'; +import getGeoJSON from './utils/get_geojson'; +import createMap from './utils/create_map'; +import createMockDrawModeContext from './utils/create_mock_draw_mode_context'; +import { TAP_INTERVAL, TAP_TOLERANCE } from '../src/lib/is_tap'; + +test('simple_select', async t => { + const context = createMockDrawModeContext(); + const mapContainer = document.createElement('div'); + document.body.appendChild(mapContainer); + const map = createMap({ container: mapContainer }) as spy; + + const Draw = new MapboxDraw(); + map.addControl(Draw); + + spy(map, 'fire'); + spy(map.doubleClickZoom, 'enable'); + spy(map.doubleClickZoom, 'disable'); + spy(map.dragPan, 'enable'); + spy(map.dragPan, 'disable'); + + const afterNextRender = setupAfterNextRender(map); + + const cleanUp = function () { + Draw.deleteAll(); + map.fire.resetHistory(); + }; + + const getFireArgs = function () { + const args = []; + for (let i = 0; i < map.fire.callCount; i++) { + args.push(map.fire.getCall(i).args); + } + return args; + }; + + t.test('simple_select - init map for tests', () => { + const done = function () { + map.off('load', done); + }; + + if (map.loaded()) { + done(); + } else { + map.on('load', done); + } + }); + + await t.test('simple_select - box select', async () => { + Draw.add(getGeoJSON('negativePoint')); + const id = Draw.add(getGeoJSON('point'))[0]; + map.fire.resetHistory(); + + await afterNextRender(); + map.dragPan.enable.resetHistory(); + map.dragPan.disable.resetHistory(); + map.fire('mousedown', makeMouseEvent(0, 0, { shiftKey: true })); + assert.equal(map.dragPan.disable.callCount, 1, 'disable dragPan'); + map.fire( + 'mousemove', + makeMouseEvent(15, 15, { + shiftKey: true, + buttons: 1 + }) + ); + await afterNextRender(); + assert.equal( + map.getContainer().className.indexOf('mouse-add') > -1, + true, + 'mouse-add class has been set' + ); + map.fire('mouseup', makeMouseEvent(15, 15, { shiftKey: true })); + + await afterNextRender(); + assert.equal( + map.getContainer().className.indexOf('mouse-move') > -1, + true, + 'mouse-move class has been set' + ); + const fireArgs = getFireArgs(); + const args = fireArgs.filter(arg => arg[0] === 'draw.selectionchange'); + assert.equal( + args.length, + 1, + 'should have one and only one selectionchange event' + ); + if (args.length > 0) { + assert.equal( + args[0][1].features.length, + 1, + 'should have one feature selected' + ); + assert.equal( + args[0][1].features[0].id, + id, + 'should be the feature we expect to be selected' + ); + } + + const actionableArgs = fireArgs.filter(arg => arg[0] === 'draw.actionable'); + assert.ok( + actionableArgs.length > 0, + 'should have fired an actionable event' + ); + if (actionableArgs.length > 0) { + const actionable = actionableArgs[actionableArgs.length - 1][1]; + assert.equal( + actionable.actions.combineFeatures, + false, + 'should fire correct combine actionable' + ); + assert.equal( + actionable.actions.uncombineFeatures, + false, + 'should fire correct uncombine actionable' + ); + assert.equal( + actionable.actions.trash, + true, + 'should fire correct trash actionable' + ); + } + + cleanUp(); + }); + + await t.test('simple_select - box select many features', async () => { + const features = []; + for (let i = 0; i < 5; i++) { + features.push(getGeoJSON('point')); + } + const ids = Draw.add({ + type: 'FeatureCollection', + features + }); + + map.fire.resetHistory(); + + await afterNextRender(); + map.dragPan.enable.resetHistory(); + map.fire('mousedown', makeMouseEvent(0, 0, { shiftKey: true })); + map.fire( + 'mousemove', + makeMouseEvent(15, 15, { + shiftKey: true, + buttons: 1 + }) + ); + map.fire('mouseup', makeMouseEvent(15, 15, { shiftKey: true })); + + await afterNextRender(); + const fireArgs = getFireArgs(); + const args = fireArgs.filter(arg => arg[0] === 'draw.selectionchange'); + assert.equal(args.length, 1, 'should have one and only one select event'); + if (args.length > 0) { + assert.equal( + args[0][1].features.length, + ids.length, + 'should have all features selected' + ); + } + + const actionableArgs = fireArgs.filter(arg => arg[0] === 'draw.actionable'); + assert.ok( + actionableArgs.length > 0, + 'should have fired an actionable event' + ); + if (actionableArgs.length > 0) { + const actionable = actionableArgs[actionableArgs.length - 1][1]; + assert.equal( + actionable.actions.combineFeatures, + true, + 'should fire correct combine actionable' + ); + assert.equal( + actionable.actions.uncombineFeatures, + false, + 'should fire correct uncombine actionable' + ); + assert.equal( + actionable.actions.trash, + true, + 'should fire correct trash actionable' + ); + } + + cleanUp(); + }); + + await t.test('simple_select - box select over no points', async () => { + Draw.add(getGeoJSON('point')); + map.fire.resetHistory(); + + await afterNextRender(); + map.fire('mousedown', makeMouseEvent(0, 0, { shiftKey: true })); + map.fire( + 'mousemove', + makeMouseEvent(-15, -15, { + shiftKey: true, + buttons: 1 + }) + ); + map.fire('mouseup', makeMouseEvent(-15, -15, { shiftKey: true })); + + await afterNextRender(); + assert.equal( + getFireArgs().filter(arg => arg[0] === 'draw.selectionchange').length, + 0, + 'there should be no draw.selectionchange event' + ); + cleanUp(); + }); + + await t.test('simple_select - box select then mousemove', async () => { + Draw.add(getGeoJSON('point')); + map.fire.resetHistory(); + + await afterNextRender(); + map.fire('mousedown', makeMouseEvent(0, 0, { shiftKey: true })); + map.fire('mousemove', makeMouseEvent(15, 15, { shiftKey: true })); + // This mousemove (not a drag) cancels the box select + map.fire('mousemove', makeMouseEvent(15, 15)); + map.fire('mouseup', makeMouseEvent(15, 15, { shiftKey: true })); + + await afterNextRender(); + assert.equal( + getFireArgs().filter(arg => arg[0] === 'draw.selectionchange').length, + 0, + 'there should be no draw.selectionchange event' + ); + cleanUp(); + }); + + await t.test('simple_select - deselect', async () => { + const id = Draw.add(getGeoJSON('point'))[0]; + Draw.changeMode('simple_select', { featureIds: [id] }); + + await afterNextRender(); + map.fire.resetHistory(); + map.fire('mousedown', makeMouseEvent(15, 15)); + map.fire('mouseup', makeMouseEvent(15, 15)); + + await afterNextRender(); + const args = getFireArgs().filter(arg => arg[0] === 'draw.selectionchange'); + assert.equal( + args.length, + 1, + 'should have one and only one selectionchange event' + ); + if (args.length > 0) { + assert.equal( + args[0][1].features.length, + 0, + 'should have no features selected' + ); + } + + cleanUp(); + }); + + await t.test('simple_select - click on a deselected feature', async () => { + const id = Draw.add(getGeoJSON('polygon'))[0]; + Draw.changeMode('simple_select'); + + await afterNextRender(); + map.fire.resetHistory(); + map.doubleClickZoom.disable.resetHistory(); + map.fire('mousedown', makeMouseEvent(50, 30)); + map.fire('mouseup', makeMouseEvent(50, 30)); + + await afterNextRender(); + assert.equal( + map.doubleClickZoom.disable.callCount, + 1, + 'disable doubleClickZoom' + ); + let args = getFireArgs(); + args = args.filter(arg => arg[0] === 'draw.selectionchange'); + assert.equal( + args.length, + 1, + 'should have one and only one selectionchange event' + ); + if (args.length > 0) { + assert.equal( + args[0][1].features.length, + 1, + 'should have only one feature selected' + ); + assert.equal( + args[0][1].features[0].id, + id, + 'should be the feature we expect to be selected' + ); + } + cleanUp(); + }); + + await t.test('simple_select - tap on a deselected feature', async () => { + const id = Draw.add(getGeoJSON('polygon'))[0]; + Draw.changeMode('simple_select'); + + await afterNextRender(); + map.fire.resetHistory(); + map.doubleClickZoom.disable.resetHistory(); + map.fire('touchstart', makeTouchEvent(50, 30)); + map.fire('touchend', makeTouchEvent(50, 30)); + + await afterNextRender(); + let args = getFireArgs(); + args = args.filter(arg => arg[0] === 'draw.selectionchange'); + assert.equal( + args.length, + 1, + 'should have one and only one selectionchange event' + ); + if (args.length > 0) { + assert.equal( + args[0][1].features.length, + 1, + 'should have only one feature selected' + ); + assert.equal( + args[0][1].features[0].id, + id, + 'should be the feature we expect to be selected' + ); + } + cleanUp(); + }); + + await t.test( + 'simple_select - click on a selected feature with shift down', + async () => { + const id = Draw.add(getGeoJSON('polygon'))[0]; + Draw.changeMode('simple_select', { featureIds: [id] }); + + await afterNextRender(); + map.fire.resetHistory(); + map.doubleClickZoom.disable.resetHistory(); + map.fire('mousedown', makeMouseEvent(50, 30, { shiftKey: true })); + map.fire('mouseup', makeMouseEvent(50, 30, { shiftKey: true })); + + await afterNextRender(); + assert.equal( + map.doubleClickZoom.disable.callCount, + 1, + 'disable doubleClickZoom' + ); + assert.equal( + map.getContainer().className.indexOf('mouse-pointer') > -1, + true, + 'mouse-pointer class has been set' + ); + let args = getFireArgs(); + args = args.filter(arg => arg[0] === 'draw.selectionchange'); + assert.equal( + args.length, + 1, + 'should have one and only one selectionchange event' + ); + if (args.length > 0) { + assert.equal( + args[0][1].features.length, + 0, + 'should have no features selected' + ); + } + + cleanUp(); + } + ); + + await t.test('simple_select - delete selected features', async () => { + const id = Draw.add(getGeoJSON('polygon'))[0]; + Draw.changeMode('simple_select', { featureIds: [id] }); + map.fire.resetHistory(); + Draw.trash(); + await afterNextRender(); + let args = getFireArgs(); + args = args.filter(arg => arg[0] === 'draw.delete'); + assert.equal( + args.length, + 1, + 'should have one and only one draw.delete event' + ); + assert.equal( + args[0][1].features.length, + 1, + 'should delete only one feature' + ); + assert.equal( + args[0][1].features[0].id, + id, + 'should delete the feature we expect it to delete' + ); + + const selectedFeatures = Draw.getSelectedIds(); + assert.equal( + selectedFeatures.length, + 0, + 'nothing should be selected anymore' + ); + cleanUp(); + }); + + await t.test( + 'simple_select - click on a selected feature with shift up to enter direct_select', + async () => { + Draw.deleteAll(); + const id = Draw.add(getGeoJSON('polygon'))[0]; + Draw.changeMode('simple_select', { featureIds: [id] }); + + await afterNextRender(); + map.doubleClickZoom.enable.resetHistory(); + map.fire.resetHistory(); + map.doubleClickZoom.disable.resetHistory(); + map.fire('mousedown', makeMouseEvent(50, 30, false)); + map.fire('mouseup', makeMouseEvent(50, 30, false)); + + await afterNextRender(); + assert.equal( + map.doubleClickZoom.disable.callCount, + 2, + 'disable doubleClickZoom. Once for click, once for direct_select' + ); + assert.equal( + map.doubleClickZoom.enable.callCount, + 1, + 'double click zoom has been enabled' + ); + assert.equal( + map.getContainer().className.indexOf('mouse-move') > -1, + true, + 'mouse-move class has been set' + ); + let args = getFireArgs(); + args = args.filter(arg => arg[0] === 'draw.modechange'); + assert.equal( + args.length, + 1, + 'should have one and only one modechange event' + ); + if (args.length > 0) { + assert.equal( + args[0][1].mode, + 'direct_select', + 'should change to direct select' + ); + } + cleanUp(); + } + ); + + await t.test( + 'simple_select - click on a vertex to enter direct_select', + async () => { + const id = Draw.add(getGeoJSON('polygon'))[0]; + Draw.changeMode('simple_select', { featureIds: [id] }); + + const clickPosition = getGeoJSON('polygon').geometry.coordinates[0][0]; + + await afterNextRender(); + map.doubleClickZoom.enable.resetHistory(); + map.fire.resetHistory(); + map.fire('mousedown', makeMouseEvent(clickPosition[0], clickPosition[1])); + map.fire('mouseup', makeMouseEvent(clickPosition[0], clickPosition[1])); + + await afterNextRender(); + assert.equal( + map.doubleClickZoom.enable.callCount, + 1, + 'double click zoom has been enabled' + ); + let args = getFireArgs(); + args = args.filter(arg => arg[0] === 'draw.modechange'); + assert.equal( + args.length, + 1, + 'should have one and only one modechange event' + ); + if (args.length > 0) { + assert.equal( + args[0][1].mode, + 'direct_select', + 'should change to direct select' + ); + } + cleanUp(); + } + ); + + await t.test( + 'simple_select - tap on a vertex to enter direct_select', + async () => { + const id = Draw.add(getGeoJSON('polygon'))[0]; + Draw.changeMode('simple_select', { featureIds: [id] }); + + const tapPosition = getGeoJSON('polygon').geometry.coordinates[0][0]; + + await afterNextRender(); + map.doubleClickZoom.enable.resetHistory(); + map.fire.resetHistory(); + map.fire('touchstart', makeTouchEvent(tapPosition[0], tapPosition[1])); + map.fire('touchend', makeTouchEvent(tapPosition[0], tapPosition[1])); + + await afterNextRender(); + let args = getFireArgs(); + args = args.filter(arg => arg[0] === 'draw.modechange'); + assert.equal( + args.length, + 1, + 'should have one and only one modechange event' + ); + if (args.length > 0) { + assert.equal( + args[0][1].mode, + 'direct_select', + 'should change to direct select' + ); + } + cleanUp(); + } + ); + + await t.test( + 'simple_select - tap dragging fires an update event', + async () => { + const point = getGeoJSON('point'); + const pointId = Draw.add(point)[0]; + const translatePosition = (point, d) => [point[0] + d, point[1] + d]; + + const startPosition = point.geometry.coordinates; + const endPosition = translatePosition(startPosition, 25); + + Draw.changeMode('simple_select', { + featureIds: [pointId] + }); + + await afterNextRender(); + map.fire.resetHistory(); + map.fire('touchstart', makeTouchEvent(...startPosition)); + map.fire( + 'touchmove', + makeTouchEvent(...translatePosition(startPosition, 15)) + ); + map.fire('touchmove', makeTouchEvent(...endPosition)); + map.fire('touchend', makeTouchEvent(...endPosition)); + + const movedPoint = Draw.get(pointId); + const args = getFireArgs().filter(arg => arg[0] === 'draw.update'); + assert.equal(args.length, 1, 'draw.update called once'); + assert.equal( + movedPoint.geometry.coordinates[0], + endPosition[0], + 'point lng moved to touchend location' + ); + assert.equal( + movedPoint.geometry.coordinates[1], + endPosition[1], + 'point lat moved to touchend location' + ); + } + ); + + await t.test( + 'simple_select - click on a deselected feature with shift down while having another feature selected', + async () => { + const pointId = Draw.add(getGeoJSON('point'))[0]; + const id = Draw.add(getGeoJSON('polygon'))[0]; + Draw.changeMode('simple_select', { featureIds: [pointId] }); + + await afterNextRender(); + map.fire.resetHistory(); + map.fire('mousedown', makeMouseEvent(50, 30, { shiftKey: true })); + map.fire('mouseup', makeMouseEvent(50, 30, { shiftKey: true })); + + await afterNextRender(); + assert.equal( + map.getContainer().className.indexOf('mouse-move') > -1, + true, + 'mouse-move class has been set' + ); + assert.equal( + Draw.getSelectedIds().indexOf(pointId) !== -1, + true, + 'point is still selected' + ); + assert.equal( + Draw.getSelectedIds().indexOf(id) !== -1, + true, + 'polygon is now selected' + ); + let args = getFireArgs(); + args = args.filter(arg => arg[0] === 'draw.selectionchange'); + assert.equal( + args.length, + 1, + 'should have one and only one selectionchange event' + ); + if (args.length > 0) { + assert.equal( + args[0][1].features.length, + 2, + 'should have two features selected' + ); + assert.equal( + args[0][1].features[0].id, + pointId, + 'selection includes point' + ); + assert.equal( + args[0][1].features[1].id, + id, + 'selection includes polygon' + ); + } + cleanUp(); + } + ); + + await t.test( + 'simple_select - click on a deselected feature with shift up, while having another feature selected', + async () => { + const pointId = Draw.add(getGeoJSON('point'))[0]; + const id = Draw.add(getGeoJSON('polygon'))[0]; + Draw.changeMode('simple_select', { featureIds: [pointId] }); + + await afterNextRender(); + map.fire.resetHistory(); + map.fire('mousedown', makeMouseEvent(50, 30, false)); + map.fire('mouseup', makeMouseEvent(50, 30, false)); + + await afterNextRender(); + assert.equal( + map.getContainer().className.indexOf('mouse-move') > -1, + true, + 'mouse-move class has been set' + ); + assert.equal( + Draw.getSelectedIds().indexOf(pointId) === -1, + true, + 'point is no longer selected' + ); + assert.equal( + Draw.getSelectedIds().indexOf(id) !== -1, + true, + 'polygon is now selected' + ); + let args = getFireArgs(); + args = args.filter(arg => arg[0] === 'draw.selectionchange'); + assert.equal( + args.length, + 1, + 'should have one and only one selectionchange event' + ); + if (args.length > 0) { + assert.equal( + args[0][1].features.length, + 1, + 'should have only one feature selected' + ); + assert.equal( + args[0][1].features[0].id, + id, + 'should be the feature we expect to be selected' + ); + } + cleanUp(); + } + ); + + await t.test('simple_select - drag every feature type', async () => { + const pointId = Draw.add(getGeoJSON('point'))[0]; + const multiPointId = Draw.add(getGeoJSON('multiPoint'))[0]; + const lineStringId = Draw.add(getGeoJSON('line'))[0]; + const multiLineStringId = Draw.add(getGeoJSON('multiLineString'))[0]; + const polygonId = Draw.add(getGeoJSON('polygon'))[0]; + const multiPolygonId = Draw.add(getGeoJSON('multiPolygon'))[0]; + + const countPositions = function (feature) { + return feature.geometry.coordinates.join(',').split(',').length; + }; + + const startPosition = getGeoJSON('point').geometry.coordinates; + Draw.changeMode('simple_select', { + featureIds: [ + pointId, + multiPointId, + lineStringId, + multiLineStringId, + polygonId, + multiPolygonId + ] + }); + await afterNextRender(); + map.fire.resetHistory(); + map.fire('mousedown', makeMouseEvent(startPosition[0], startPosition[1])); + map.fire( + 'mousemove', + makeMouseEvent(startPosition[0] + 15, startPosition[1] + 15, { + buttons: 1 + }) + ); + map.fire( + 'mousemove', + makeMouseEvent(startPosition[0] + 25, startPosition[1] + 25, { + buttons: 1 + }) + ); + map.fire( + 'mouseup', + makeMouseEvent(startPosition[0] + 25, startPosition[1] + 25) + ); + + const movedPoint = Draw.get(pointId); + assert.equal( + movedPoint.geometry.coordinates[0], + startPosition[0] + 25, + 'point lng moved' + ); + assert.equal( + movedPoint.geometry.coordinates[1], + startPosition[1] + 25, + 'point lat moved' + ); + assert.equal( + countPositions(movedPoint), + countPositions(getGeoJSON('point')), + 'point has same number of postions' + ); + + const movedMultiPoint = Draw.get(multiPointId); + assert.equal( + movedMultiPoint.geometry.coordinates[0][0], + getGeoJSON('multiPoint').geometry.coordinates[0][0] + 25, + 'multipoint lng moved' + ); + assert.equal( + movedMultiPoint.geometry.coordinates[0][1], + getGeoJSON('multiPoint').geometry.coordinates[0][1] + 25, + 'multipoint lat moved' + ); + assert.equal( + countPositions(movedMultiPoint), + countPositions(getGeoJSON('multiPoint')), + 'multiPoint has same number of postions' + ); + + const movedLineString = Draw.get(lineStringId); + assert.equal( + movedLineString.geometry.coordinates[0][0], + getGeoJSON('line').geometry.coordinates[0][0] + 25, + 'line lng moved' + ); + assert.equal( + movedLineString.geometry.coordinates[0][1], + getGeoJSON('line').geometry.coordinates[0][1] + 25, + 'line lat moved' + ); + assert.equal( + countPositions(movedLineString), + countPositions(getGeoJSON('line')), + 'line has same number of postions' + ); + + const movedMultiLineString = Draw.get(multiLineStringId); + assert.equal( + movedMultiLineString.geometry.coordinates[0][0][0], + getGeoJSON('multiLineString').geometry.coordinates[0][0][0] + 25, + 'multiLineString lng moved' + ); + assert.equal( + movedMultiLineString.geometry.coordinates[0][0][1], + getGeoJSON('multiLineString').geometry.coordinates[0][0][1] + 25, + 'multiLineString lat moved' + ); + assert.equal( + countPositions(movedMultiLineString), + countPositions(getGeoJSON('multiLineString')), + 'multiLineString has same number of postions' + ); + + const movedPolygon = Draw.get(polygonId); + assert.equal( + movedPolygon.geometry.coordinates[0][0][0], + getGeoJSON('polygon').geometry.coordinates[0][0][0] + 25, + 'polygon lng moved' + ); + assert.equal( + movedPolygon.geometry.coordinates[0][0][1], + getGeoJSON('polygon').geometry.coordinates[0][0][1] + 25, + 'polygon lat moved' + ); + assert.equal( + countPositions(movedPolygon), + countPositions(getGeoJSON('polygon')), + 'polygon has same number of postions' + ); + + const movedMultiPolygon = Draw.get(multiPolygonId); + assert.equal( + movedMultiPolygon.geometry.coordinates[0][0][0][0], + getGeoJSON('multiPolygon').geometry.coordinates[0][0][0][0] + 25, + 'multiPolygon lng moved' + ); + assert.equal( + movedMultiPolygon.geometry.coordinates[0][0][0][1], + getGeoJSON('multiPolygon').geometry.coordinates[0][0][0][1] + 25, + 'multiPolygon lat moved' + ); + assert.equal( + countPositions(movedMultiPolygon), + countPositions(getGeoJSON('multiPolygon')), + 'multiPolygon has same number of postions' + ); + + cleanUp(); + }); + + await t.test( + 'simple_select - interrupt drag move with mousemove', + async () => { + const pointId = Draw.add(getGeoJSON('point'))[0]; + Draw.changeMode('simple_select', { featureIds: [pointId] }); + const startPosition = getGeoJSON('point').geometry.coordinates; + await afterNextRender(); + map.fire.resetHistory(); + map.fire('mousedown', makeMouseEvent(startPosition[0], startPosition[1])); + // Dragging + map.fire( + 'mousemove', + makeMouseEvent(startPosition[0] + 15, startPosition[1] + 15, { + buttons: 1 + }) + ); + // Not dragging + map.fire( + 'mousemove', + makeMouseEvent(startPosition[0] + 25, startPosition[1] + 25) + ); + map.fire( + 'mouseup', + makeMouseEvent(startPosition[0] + 25, startPosition[1] + 25) + ); + + const movedPoint = Draw.get(pointId); + assert.equal( + movedPoint.geometry.coordinates[0], + startPosition[0] + 15, + 'point lng moved only the first amount' + ); + assert.equal( + movedPoint.geometry.coordinates[1], + startPosition[1] + 15, + 'point lat moved only the first amount' + ); + + cleanUp(); + } + ); + + await t.test( + 'simple_select - fire one update when dragging mouse leaves container and button is released outside', + async () => { + const pointId = Draw.add(getGeoJSON('point'))[0]; + Draw.changeMode('simple_select', { featureIds: [pointId] }); + const startPosition = getGeoJSON('point').geometry.coordinates; + await afterNextRender(); + map.fire.resetHistory(); + map.fire('mousedown', makeMouseEvent(startPosition[0], startPosition[1])); + map.fire( + 'mousemove', + makeMouseEvent(startPosition[0] + 15, startPosition[1] + 15, { + buttons: 1 + }) + ); + mapContainer.dispatchEvent(createSyntheticEvent('mouseout')); + map.fire( + 'mousemove', + makeMouseEvent(startPosition[0] + 25, startPosition[1] + 25) + ); + map.fire( + 'mouseup', + makeMouseEvent(startPosition[0] + 25, startPosition[1] + 25) + ); + + const movedPoint = Draw.get(pointId); + const args = getFireArgs().filter(arg => arg[0] === 'draw.update'); + assert.equal(args.length, 1, 'draw.update called once'); + assert.equal( + movedPoint.geometry.coordinates[0], + startPosition[0] + 15, + 'point lng moved only the first amount' + ); + assert.equal( + movedPoint.geometry.coordinates[1], + startPosition[1] + 15, + 'point lat moved only the first amount' + ); + + cleanUp(); + } + ); + + await t.test( + 'simple_select - fire two update when dragging mouse leaves container then returns and button is released inside', + async () => { + const pointId = Draw.add(getGeoJSON('point'))[0]; + Draw.changeMode('simple_select', { featureIds: [pointId] }); + const startPosition = getGeoJSON('point').geometry.coordinates; + await afterNextRender(); + map.fire.resetHistory(); + map.fire('mousedown', makeMouseEvent(startPosition[0], startPosition[1])); + map.fire( + 'mousemove', + makeMouseEvent(startPosition[0] + 15, startPosition[1] + 15, { + buttons: 1 + }) + ); + mapContainer.dispatchEvent(createSyntheticEvent('mouseout')); + map.fire( + 'mousemove', + makeMouseEvent(startPosition[0] + 25, startPosition[1] + 25, { + buttons: 1 + }) + ); + map.fire( + 'mouseup', + makeMouseEvent(startPosition[0] + 25, startPosition[1] + 25) + ); + + const movedPoint = Draw.get(pointId); + const args = getFireArgs().filter(arg => arg[0] === 'draw.update'); + assert.equal(args.length, 2, 'draw.update called twice'); + assert.equal( + movedPoint.geometry.coordinates[0], + startPosition[0] + 25, + 'point lng moved to the mouseup location' + ); + assert.equal( + movedPoint.geometry.coordinates[1], + startPosition[1] + 25, + 'point lat moved to the mouseup location' + ); + } + ); + + t.test('simple_select - on closing invalid line', () => { + Draw.changeMode('draw_line_string'); + mouseClick(map, makeMouseEvent(1, 1)); + mouseClick(map, makeMouseEvent(1, 1)); + assert.equal( + Draw.getMode(), + 'simple_select', + 'should be in simple_select mode' + ); + assert.equal( + Draw.getSelected().features.length, + 0, + 'should not get any selected features' + ); + assert.equal( + (context.store as spy)._emitSelectionChange, + undefined, + 'should not emit selection change' + ); + cleanUp(); + }); + + t.test('simple_select - on closing invalid polygon', () => { + Draw.changeMode('draw_polygon'); + mouseClick(map, makeMouseEvent(1, 1)); + mouseClick(map, makeMouseEvent(16, 16)); + mouseClick(map, makeMouseEvent(16, 16)); + assert.equal( + Draw.getMode(), + 'simple_select', + 'should be in simple_select mode' + ); + assert.equal( + Draw.getSelected().features.length, + 0, + 'should not get any selected features' + ); + assert.equal( + (context.store as spy)._emitSelectionChange, + undefined, + 'should not emit selection change' + ); + cleanUp(); + }); + + await t.test( + 'simple_select - fire one update after moving point with touch', + async () => { + const pointId = Draw.add(getGeoJSON('point'))[0]; + Draw.changeMode('simple_select', { featureIds: [pointId] }); + const startPosition = getGeoJSON('point').geometry.coordinates; + const endPosition = [ + startPosition[0] + TAP_TOLERANCE, + startPosition[1] + TAP_TOLERANCE + ]; + await afterNextRender(); + map.fire.resetHistory(); + map.doubleClickZoom.disable.resetHistory(); + map.fire( + 'touchstart', + makeTouchEvent(startPosition[0], startPosition[1]) + ); + + await new Promise(resolve => setTimeout(resolve, TAP_INTERVAL)); + + map.fire('touchmove', makeTouchEvent(endPosition[0], endPosition[1])); + map.fire('touchend', makeTouchEvent(endPosition[0], endPosition[1])); + + const movedPoint = Draw.get(pointId); + const args = getFireArgs().filter(arg => arg[0] === 'draw.update'); + assert.equal(args.length, 1, 'draw.update called once'); + assert.equal( + movedPoint.geometry.coordinates[0], + endPosition[0], + 'point lng moved to the last touchmove position' + ); + assert.equal( + movedPoint.geometry.coordinates[1], + endPosition[1], + 'point lat moved the last touchmove position' + ); + cleanUp(); + } + ); + + document.body.removeChild(mapContainer); +}); diff --git a/test/sort_features.test.js b/test/sort_features.test.ts similarity index 59% rename from test/sort_features.test.js rename to test/sort_features.test.ts index 9c17f901..00a8c321 100644 --- a/test/sort_features.test.js +++ b/test/sort_features.test.ts @@ -1,6 +1,8 @@ +import './mock-browser'; import test from 'node:test'; import assert from 'node:assert/strict'; -import sortFeatures from '../src/lib/sort_features.js'; +import { sortFeatures } from '../src/lib/sort_features'; +import {StrictFeature} from '../src/types/types'; test('sortFeatures', () => { const features = [ @@ -12,7 +14,15 @@ test('sortFeatures', () => { id: 'medium-polygon', geometry: { type: 'Polygon', - coordinates: [[[15, 50], [15, 59], [35, 59], [35, 50], [15, 50]]] + coordinates: [ + [ + [15, 50], + [15, 59], + [35, 59], + [35, 50], + [15, 50] + ] + ] } }, { @@ -23,7 +33,15 @@ test('sortFeatures', () => { id: 'huge-polygon', geometry: { type: 'Polygon', - coordinates: [[[58, 27], [58, 68], [101, 68], [101, 27], [58, 27]]] + coordinates: [ + [ + [58, 27], + [58, 68], + [101, 68], + [101, 27], + [58, 27] + ] + ] } }, { @@ -40,7 +58,7 @@ test('sortFeatures', () => { } ]; - assert.deepEqual(sortFeatures(features), [ + assert.deepEqual(sortFeatures(features as unknown as StrictFeature[]), [ { geometry: { type: 'Point' }, properties: { id: 3 } @@ -65,14 +83,30 @@ test('sortFeatures', () => { id: 'medium-polygon', geometry: { type: 'Polygon', - coordinates: [[[15, 50], [15, 59], [35, 59], [35, 50], [15, 50]]] + coordinates: [ + [ + [15, 50], + [15, 59], + [35, 59], + [35, 50], + [15, 50] + ] + ] } }, { id: 'huge-polygon', geometry: { type: 'Polygon', - coordinates: [[[58, 27], [58, 68], [101, 68], [101, 27], [58, 27]]] + coordinates: [ + [ + [58, 27], + [58, 68], + [101, 68], + [101, 27], + [58, 27] + ] + ] } } ]); diff --git a/test/static.test.js b/test/static.test.ts similarity index 79% rename from test/static.test.js rename to test/static.test.ts index a44b4324..7a06b5c4 100644 --- a/test/static.test.js +++ b/test/static.test.ts @@ -1,17 +1,18 @@ /* eslint no-shadow:[0] */ +import './mock-browser'; import test from 'node:test'; import assert from 'node:assert/strict'; import StaticMode from '@mapbox/mapbox-gl-draw-static-mode'; -import {spy} from 'sinon'; +import { spy } from 'sinon'; -import MapboxDraw from '../index.js'; -import {setupAfterNextRender} from './utils/after_next_render.js'; -import makeMouseEvent from './utils/make_mouse_event.js'; -import getGeoJSON from './utils/get_geojson.js'; -import createMap from './utils/create_map.js'; +import MapboxDraw from '../index'; +import { setupAfterNextRender } from './utils/after_next_render'; +import makeMouseEvent from './utils/make_mouse_event'; +import getGeoJSON from './utils/get_geojson'; +import createMap from './utils/create_map'; -test('static', async (t) => { - const map = createMap(); +test('static', async t => { + const map = createMap() as spy; const opts = { modes: { static: StaticMode @@ -27,12 +28,12 @@ test('static', async (t) => { const afterNextRender = setupAfterNextRender(map); - const cleanUp = function() { + const cleanUp = function () { Draw.deleteAll(); map.fire.resetHistory(); }; - const getFireArgs = function() { + const getFireArgs = function () { const args = []; for (let i = 0; i < map.fire.callCount; i++) { args.push(map.fire.getCall(i).args); @@ -41,7 +42,7 @@ test('static', async (t) => { }; t.test('static - init map for tests', () => { - const done = function() { + const done = function () { map.off('load', done); }; @@ -70,7 +71,11 @@ test('static', async (t) => { }); await t.test('static - try clicking many features', async () => { - const features = [getGeoJSON('point'), getGeoJSON('line'), getGeoJSON('square')]; + const features = [ + getGeoJSON('point'), + getGeoJSON('line'), + getGeoJSON('square') + ]; Draw.add({ type: 'FeatureCollection', features diff --git a/test/store.test.js b/test/store.test.ts similarity index 54% rename from test/store.test.js rename to test/store.test.ts index 836f4ea2..7c6d2f43 100644 --- a/test/store.test.js +++ b/test/store.test.ts @@ -1,11 +1,12 @@ +import './mock-browser'; +import { spy } from 'sinon'; import test from 'node:test'; import assert from 'node:assert/strict'; -import {spy} from 'sinon'; -import Store from '../src/store.js'; -import createFeature from './utils/create_feature.js'; -import getPublicMemberKeys from './utils/get_public_member_keys.js'; -import createMap from './utils/create_map.js'; +import Store from '../src/store'; +import createFeature from './utils/create_feature'; +import getPublicMemberKeys from './utils/get_public_member_keys'; +import createMap from './utils/create_map'; function createStore() { const ctx = { @@ -16,7 +17,7 @@ function createStore() { events: { fire: spy() } - }; + } as spy; return new Store(ctx); } @@ -28,49 +29,153 @@ test('Store has correct properties', () => { test('Store constructor and public API', () => { const map = createMap(); - const ctx = { map }; + const ctx = { map } as spy; const store = new Store(ctx); // instance members - assert.deepEqual(store.sources, { - hot: [], - cold: [] - }, 'exposes store.sources'); + assert.deepEqual( + store.sources, + { + hot: [], + cold: [] + }, + 'exposes store.sources' + ); assert.equal(store.ctx, ctx, 'exposes store.ctx'); assert.equal(store.ctx.map, map, 'exposes store.ctx.map'); assert.equal(store.isDirty, false, 'exposes store.isDirty'); assert.equal(typeof store.render, 'function', 'exposes store.render'); - assert.equal(getPublicMemberKeys(store).length, 4, 'no unexpected instance members'); + assert.equal( + getPublicMemberKeys(store).length, + 4, + 'no unexpected instance members' + ); // prototype members - assert.equal(typeof Store.prototype.setDirty, 'function', 'exposes store.setDirty'); - assert.equal(typeof Store.prototype.createRenderBatch, 'function', 'exposes store.createRenderBatch'); - assert.equal(typeof Store.prototype.featureCreated, 'function', 'exposes store.featureCreated'); - assert.equal(typeof Store.prototype.featureChanged, 'function', 'exposes store.featureChanged'); - assert.equal(typeof Store.prototype.getChangedIds, 'function', 'exposes store.getChangedIds'); - assert.equal(typeof Store.prototype.clearChangedIds, 'function', 'exposes store.clearChangedIds'); - assert.equal(typeof Store.prototype.getAllIds, 'function', 'exposes store.getAllIds'); + assert.equal( + typeof Store.prototype.setDirty, + 'function', + 'exposes store.setDirty' + ); + assert.equal( + typeof Store.prototype.createRenderBatch, + 'function', + 'exposes store.createRenderBatch' + ); + assert.equal( + typeof Store.prototype.featureCreated, + 'function', + 'exposes store.featureCreated' + ); + assert.equal( + typeof Store.prototype.featureChanged, + 'function', + 'exposes store.featureChanged' + ); + assert.equal( + typeof Store.prototype.getChangedIds, + 'function', + 'exposes store.getChangedIds' + ); + assert.equal( + typeof Store.prototype.clearChangedIds, + 'function', + 'exposes store.clearChangedIds' + ); + assert.equal( + typeof Store.prototype.getAllIds, + 'function', + 'exposes store.getAllIds' + ); assert.equal(typeof Store.prototype.add, 'function', 'exposes store.add'); assert.equal(typeof Store.prototype.get, 'function', 'exposes store.get'); - assert.equal(typeof Store.prototype.getAll, 'function', 'exposes store.getAll'); - assert.equal(typeof Store.prototype.select, 'function', 'exposes store.select'); - assert.equal(typeof Store.prototype.deselect, 'function', 'exposes store.deselect'); - assert.equal(typeof Store.prototype.clearSelected, 'function', 'exposes store.clearSelected'); - assert.equal(typeof Store.prototype.getSelectedIds, 'function', 'exposes store.getSelectedIds'); - assert.equal(typeof Store.prototype.getSelected, 'function', 'exposes store.getSelected'); - assert.equal(typeof Store.prototype.isSelected, 'function', 'exposes store.isSelected'); - assert.equal(typeof Store.prototype.delete, 'function', 'exposes store.delete'); - assert.equal(typeof Store.prototype.setSelected, 'function', 'exposes store.setSelected'); - assert.equal(typeof Store.prototype.setSelectedCoordinates, 'function', 'exposes store.setSelectedCoordinates'); - assert.equal(typeof Store.prototype.getSelectedCoordinates, 'function', 'exposes store.getSelectedCoordinates'); - assert.equal(typeof Store.prototype.clearSelectedCoordinates, 'function', 'exposes store.clearSelectedCoordinates'); - assert.equal(typeof Store.prototype.setFeatureProperty, 'function', 'exposes store.setFeatureProperty'); - assert.equal(typeof Store.prototype.storeMapConfig, 'function', 'exposes store.storeMapConfig'); - assert.equal(typeof Store.prototype.restoreMapConfig, 'function', 'exposes store.restoreMapConfig'); - assert.equal(typeof Store.prototype.getInitialConfigValue, 'function', 'exposes store.getInitialConfigValue'); - - assert.equal(getPublicMemberKeys(Store.prototype).length, 25, 'no untested prototype members'); + assert.equal( + typeof Store.prototype.getAll, + 'function', + 'exposes store.getAll' + ); + assert.equal( + typeof Store.prototype.select, + 'function', + 'exposes store.select' + ); + assert.equal( + typeof Store.prototype.deselect, + 'function', + 'exposes store.deselect' + ); + assert.equal( + typeof Store.prototype.clearSelected, + 'function', + 'exposes store.clearSelected' + ); + assert.equal( + typeof Store.prototype.getSelectedIds, + 'function', + 'exposes store.getSelectedIds' + ); + assert.equal( + typeof Store.prototype.getSelected, + 'function', + 'exposes store.getSelected' + ); + assert.equal( + typeof Store.prototype.isSelected, + 'function', + 'exposes store.isSelected' + ); + assert.equal( + typeof Store.prototype.delete, + 'function', + 'exposes store.delete' + ); + assert.equal( + typeof Store.prototype.setSelected, + 'function', + 'exposes store.setSelected' + ); + assert.equal( + typeof Store.prototype.setSelectedCoordinates, + 'function', + 'exposes store.setSelectedCoordinates' + ); + assert.equal( + typeof Store.prototype.getSelectedCoordinates, + 'function', + 'exposes store.getSelectedCoordinates' + ); + assert.equal( + typeof Store.prototype.clearSelectedCoordinates, + 'function', + 'exposes store.clearSelectedCoordinates' + ); + assert.equal( + typeof Store.prototype.setFeatureProperty, + 'function', + 'exposes store.setFeatureProperty' + ); + assert.equal( + typeof Store.prototype.storeMapConfig, + 'function', + 'exposes store.storeMapConfig' + ); + assert.equal( + typeof Store.prototype.restoreMapConfig, + 'function', + 'exposes store.restoreMapConfig' + ); + assert.equal( + typeof Store.prototype.getInitialConfigValue, + 'function', + 'exposes store.getInitialConfigValue' + ); + + assert.equal( + getPublicMemberKeys(Store.prototype).length, + 25, + 'no untested prototype members' + ); }); test('Store#setDirty', () => { @@ -83,7 +188,7 @@ test('Store#setDirty', () => { test('Store#createRenderBatch', () => { const store = createStore(); let numRenders = 0; - store.render = function() { + store.render = function () { numRenders++; }; store.render(); @@ -92,13 +197,17 @@ test('Store#createRenderBatch', () => { store.render(); store.render(); store.render(); - assert.equal(numRenders, 1, 'when batching render doesn\'t get incremented'); + assert.equal(numRenders, 1, "when batching render doesn't get incremented"); renderBatch(); assert.equal(numRenders, 2, 'when releasing batch, render only happens once'); renderBatch = store.createRenderBatch(); renderBatch(); - assert.equal(numRenders, 2, 'when releasing batch, render doesn\'t happen if render wasn\'t called'); + assert.equal( + numRenders, + 2, + "when releasing batch, render doesn't happen if render wasn't called" + ); }); test('Store#featureChanged, Store#getChangedIds, Store#clearChangedIds', () => { @@ -116,7 +225,7 @@ test('Store#featureChanged, Store#getChangedIds, Store#clearChangedIds', () => { test('Store#add, Store#get, Store#getAll', () => { const store = createStore(); - assert.equal(store.get(1), undefined); + assert.equal(store.get(1 as unknown as string), undefined); assert.deepEqual(store.getAll(), []); const point = createFeature('point'); const line = createFeature('line'); @@ -133,7 +242,7 @@ test('Store#add, Store#get, Store#getAll', () => { assert.deepEqual(store.getAll(), [point, line]); }); -test('selection methods', async (t) => { +test('selection methods', async t => { const store = createStore(); const f1 = createFeature('point'); store.add(f1); @@ -148,16 +257,32 @@ test('selection methods', async (t) => { t.test('select one feature', () => { store.select(f1.id); - assert.deepEqual(store.getSelectedIds(), [f1.id], 'f1 returns in selected ids array'); - assert.deepEqual(store.getSelected(), [f1.toGeoJSON()], 'f1 returns in selected array'); + assert.deepEqual( + store.getSelectedIds(), + [f1.id], + 'f1 returns in selected ids array' + ); + assert.deepEqual( + store.getSelected(), + [f1.toGeoJSON()], + 'f1 returns in selected array' + ); assert.equal(store.isSelected(f1.id), true, 'isSelected affirms f1'); assert.equal(store.isSelected(f2.id), false, 'isSelected rejects f2'); }); await t.test('select a second feature', () => { store.select(f2.id); - assert.deepEqual(store.getSelectedIds(), [f1.id, f2.id], 'f1 and f2 return in selected ids array'); - assert.deepEqual(store.getSelected(), [f1, f2], 'f1 and f2 return in selected array'); + assert.deepEqual( + store.getSelectedIds(), + [f1.id, f2.id], + 'f1 and f2 return in selected ids array' + ); + assert.deepEqual( + store.getSelected(), + [f1, f2], + 'f1 and f2 return in selected array' + ); assert.equal(store.isSelected(f1.id), true, 'isSelected affirms f1'); assert.equal(store.isSelected(f2.id), true, 'isSelected affirms f2'); }); @@ -168,13 +293,21 @@ test('selection methods', async (t) => { await t.test('deselect a feature', () => { store.deselect(f1.id); - assert.deepEqual(store.getSelectedIds(), [f2.id], 'deselection of f1 clears it from selected array'); + assert.deepEqual( + store.getSelectedIds(), + [f2.id], + 'deselection of f1 clears it from selected array' + ); }); await t.test('serially select more features', () => { store.select(f3.id); store.select(f4.id); - assert.deepEqual(store.getSelectedIds(), [f2.id, f3.id, f4.id], 'serial selection of f3 and f4 reflected in selected array'); + assert.deepEqual( + store.getSelectedIds(), + [f2.id, f3.id, f4.id], + 'serial selection of f3 and f4 reflected in selected array' + ); }); await t.test('clear selection', () => { @@ -222,16 +355,16 @@ test('Store#setSelected', () => { const line = createFeature('line'); const polygon = createFeature('polygon'); - store.setSelected(point.id, {silent: true}); + store.setSelected(point.id, { silent: true }); assert.deepEqual(store.getSelectedIds(), [point.id]); - store.setSelected([line.id, polygon.id], {silent: true}); + store.setSelected([line.id, polygon.id], { silent: true }); assert.deepEqual(store.getSelectedIds(), [line.id, polygon.id]); - store.setSelected(line.id, {silent: true}); + store.setSelected(line.id, { silent: true }); assert.deepEqual(store.getSelectedIds(), [line.id]); - store.setSelected(undefined, {silent: true}); + store.setSelected(undefined, { silent: true }); assert.deepEqual(store.getSelectedIds(), []); }); @@ -243,7 +376,11 @@ test('Store#setFeatureProperty', () => { store.clearChangedIds(); store.setFeatureProperty(point.id, 'size', 200); assert.deepEqual(store.getChangedIds(), [point.id]); - assert.equal(store.get(point.id).properties.size, 200, 'sets the property on the feature'); + assert.equal( + store.get(point.id).properties.size, + 200, + 'sets the property on the feature' + ); }); test('Store#storeAndRestoreMapConfig', () => { @@ -251,14 +388,26 @@ test('Store#storeAndRestoreMapConfig', () => { // Disable doubleClickZoom map.doubleClickZoom.disable(); // Check it's disabled - assert.equal(map.doubleClickZoom.isEnabled(), false, 'Disables doubleClickZoom on the map'); + assert.equal( + map.doubleClickZoom.isEnabled(), + false, + 'Disables doubleClickZoom on the map' + ); const ctx = { map }; - const store = new Store(ctx); + const store = new Store(ctx as spy); store.storeMapConfig(); // Check we can get the initial state of it - assert.equal(store.getInitialConfigValue('doubleClickZoom'), false, 'Retrieves the initial value for the doubleClickZoom'); + assert.equal( + store.getInitialConfigValue('doubleClickZoom'), + false, + 'Retrieves the initial value for the doubleClickZoom' + ); // Enable it again, byt then use restore to reset the initial state map.doubleClickZoom.enable(); store.restoreMapConfig(); - assert.equal(map.doubleClickZoom.isEnabled(), false, 'Restores doubleClickZoom on the map'); + assert.equal( + map.doubleClickZoom.isEnabled(), + false, + 'Restores doubleClickZoom on the map' + ); }); diff --git a/test/string_set.test.js b/test/string_set.test.ts similarity index 67% rename from test/string_set.test.js rename to test/string_set.test.ts index 1ebb711a..5979822f 100644 --- a/test/string_set.test.js +++ b/test/string_set.test.ts @@ -1,21 +1,46 @@ +import './mock-browser'; import test from 'node:test'; import assert from 'node:assert/strict'; -import StringSet from '../src/lib/string_set.js'; +import StringSet from '../src/lib/string_set'; test('StringSet constructor and API', () => { const set = new StringSet(); assert.deepEqual(set.values(), [], 'empty by default'); - assert.equal(Object.keys(set).filter(k => k[0] !== '_').length, 0, 'no unexpected properties'); + assert.equal( + Object.keys(set).filter(k => k[0] !== '_').length, + 0, + 'no unexpected properties' + ); // Methods assert.equal(typeof StringSet.prototype.add, 'function', 'exposes set.add'); - assert.equal(typeof StringSet.prototype.delete, 'function', 'exposes set.delete'); + assert.equal( + typeof StringSet.prototype.delete, + 'function', + 'exposes set.delete' + ); assert.equal(typeof StringSet.prototype.has, 'function', 'exposes set.has'); - assert.equal(typeof StringSet.prototype.values, 'function', 'exposes set.values'); - assert.equal(typeof StringSet.prototype.clear, 'function', 'exposes set.clear'); - assert.equal(Object.keys(StringSet.prototype).filter(k => k[0] !== '_').length, 5, 'no unexpected methods'); + assert.equal( + typeof StringSet.prototype.values, + 'function', + 'exposes set.values' + ); + assert.equal( + typeof StringSet.prototype.clear, + 'function', + 'exposes set.clear' + ); + assert.equal( + Object.keys(StringSet.prototype).filter(k => k[0] !== '_').length, + 5, + 'no unexpected methods' + ); const populatedSet = new StringSet(['a', 4, 'b']); - assert.deepEqual(populatedSet.values(), ['a', 4, 'b'], 'populated by constructor arg'); + assert.deepEqual( + populatedSet.values(), + ['a', 4, 'b'], + 'populated by constructor arg' + ); }); test('StringSet#add', () => { @@ -38,8 +63,6 @@ test('StringSet#delete', () => { assert.deepEqual(set.values(), ['b', 2]); set.delete('a'); assert.deepEqual(set.values(), ['b', 2]); - set.delete(); - assert.deepEqual(set.values(), ['b', 2]); set.delete('b'); assert.deepEqual(set.values(), [2]); set.delete(2); @@ -60,7 +83,11 @@ test('StringSet#values', () => { const subject = ['a', 'b']; const set = new StringSet(subject); assert.deepEqual(set.values(), ['a', 'b']); - assert.notEqual(set.values(), subject, 'array is copied, so source array is not mutable'); + assert.notEqual( + set.values(), + subject, + 'array is copied, so source array is not mutable' + ); }); test('StringSet#clear', () => { diff --git a/test/ui.test.js b/test/ui.test.js deleted file mode 100644 index 49fb57ca..00000000 --- a/test/ui.test.js +++ /dev/null @@ -1,248 +0,0 @@ -import test from 'node:test'; -import assert from 'node:assert/strict'; -import {spy} from 'sinon'; - -import ui from '../src/ui.js'; - -function createMockContext({ position, controls } = {}) { - const container = document.createElement('div'); - document.body.appendChild(container); - - const controlContainer = document.createElement('div'); - controlContainer.className = `mapboxgl-ctrl-${position || 'top-left'}`; - container.appendChild(controlContainer); - - return { - context: { - container, - options: { - controls, - keybindings: true - }, - api: { - trash: spy() - }, - events: { - changeMode: spy() - } - }, - cleanup() { - document.body.removeChild(container); - }, - getControlContainer() { - return controlContainer; - } - }; -} - -function getButtons(div) { - return Array.prototype.slice.call(div.getElementsByClassName('mapbox-gl-draw_ctrl-draw-btn')); -} - -test('ui container classes', async (t) => { - const { context, cleanup } = createMockContext(); - const testUi = ui(context); - - assert.equal(context.container.className, '', 'confirm that class starts empty'); - - // Each sub-test relies on state from the prior sub-tests - - t.test('update all classes', () => { - testUi.queueMapClasses({ - mode: 'direct_select', - feature: 'vertex', - mouse: 'move' - }); - testUi.updateMapClasses(); - assert.ok(context.container.classList.contains('mode-direct_select'), 'mode class set'); - assert.ok(context.container.classList.contains('feature-vertex'), 'feature class set'); - assert.ok(context.container.classList.contains('mouse-move'), 'mouse class set'); - }); - - await t.test('update only feature class', () => { - testUi.queueMapClasses({ - feature: 'midpoint' - }); - testUi.updateMapClasses(); - assert.ok(context.container.classList.contains('mode-direct_select'), 'mode class remains'); - assert.ok(context.container.classList.contains('feature-midpoint'), 'feature class updated'); - assert.ok(context.container.classList.contains('mouse-move'), 'mouse class remains'); - }); - - await t.test('update mode and mouse classes', () => { - testUi.queueMapClasses({ - mode: 'foo', - mouse: 'bar' - }); - testUi.updateMapClasses(); - assert.ok(context.container.classList.contains('mode-foo'), 'mode class updated'); - assert.ok(context.container.classList.contains('feature-midpoint'), 'feature class remains'); - assert.ok(context.container.classList.contains('mouse-bar'), 'mouse class updated'); - }); - - await t.test('remove only feature class', () => { - testUi.queueMapClasses({ - feature: null - }); - testUi.updateMapClasses(); - assert.ok(context.container.classList.contains('mode-foo'), 'mode class remains'); - assert.ok(context.container.className.indexOf('feature-') === -1, 'feature class removed'); - assert.ok(context.container.classList.contains('mouse-bar'), 'mouse class remains'); - }); - - await t.test('remove all classes', () => { - testUi.queueMapClasses({ - feature: null, - mode: null, - mouse: null - }); - testUi.updateMapClasses(); - assert.ok(context.container.className.indexOf('mode-') === -1, 'mode class removed'); - assert.ok(context.container.className.indexOf('feature-') === -1, 'feature class still gone'); - assert.ok(context.container.className.indexOf('mouse-') === -1, 'mouse class removed'); - }); - - cleanup(); -}); - -test('ui buttons with no options.controls', () => { - const { context, cleanup } = createMockContext(); - const testUi = ui(context); - - const div = testUi.addButtons(); - assert.deepEqual(getButtons(div), [], 'still no buttons'); - - cleanup(); -}); - -test('ui buttons with one options.controls', () => { - /* eslint-disable */ - const { context, cleanup } = createMockContext({ - controls: { - line_string: true - } - }); - /* eslint-enable */ - const testUi = ui(context); - - const div = testUi.addButtons(); - const buttons = getButtons(div); - assert.equal(buttons.length, 1, 'one button added'); - assert.ok(buttons[0].classList.contains('mapbox-gl-draw_line'), 'has line class'); - assert.ok(buttons[0].classList.contains('mapbox-gl-draw_ctrl-draw-btn'), 'has control class'); - - cleanup(); -}); - -test('ui buttons control group container inserted above attribution control, in control container, by addButtons', () => { - const { context, cleanup, getControlContainer } = createMockContext({ - controls: { - trash: true - } - }); - - const controlContainer = getControlContainer(); - const testUi = ui(context); - - assert.equal(controlContainer.getElementsByClassName('mapboxgl-ctrl-group').length, 0, - 'confirm control group does not exist at first'); - - const controlGroup = testUi.addButtons(); - assert.ok(controlGroup, 'control group exists'); - - cleanup(); -}); - -test('ui buttons with all options.controls, no attribution control', async (t) => { - /* eslint-disable */ - const { context, cleanup } = createMockContext({ - controls: { - line_string: true, - point: true, - polygon: true, - trash: true - } - }); - /* eslint-enable */ - const testUi = ui(context); - - const controlGroup = testUi.addButtons(); - const buttons = getButtons(controlGroup); - - assert.equal(buttons.length, 4, 'one button added'); - - assert.ok(buttons[0].classList.contains('mapbox-gl-draw_point'), 'first button has point class'); - assert.ok(buttons[0].classList.contains('mapbox-gl-draw_ctrl-draw-btn'), 'first button has control class'); - assert.equal(buttons[0].parentNode, controlGroup, 'first button is in controlGroup'); - assert.ok(buttons[1].classList.contains('mapbox-gl-draw_line'), 'second button has line class'); - assert.ok(buttons[1].classList.contains('mapbox-gl-draw_ctrl-draw-btn'), 'second button has control class'); - assert.equal(buttons[2].parentNode, controlGroup, 'second button is in controlGroup'); - assert.ok(buttons[2].classList.contains('mapbox-gl-draw_polygon'), 'third button has polygon class'); - assert.ok(buttons[2].classList.contains('mapbox-gl-draw_ctrl-draw-btn'), 'third button has control class'); - assert.equal(buttons[1].parentNode, controlGroup, 'third button is in controlGroup'); - assert.ok(buttons[3].classList.contains('mapbox-gl-draw_trash'), 'fourth button has trash class'); - assert.ok(buttons[3].classList.contains('mapbox-gl-draw_ctrl-draw-btn'), 'fourth button has control class'); - assert.equal(buttons[3].parentNode, controlGroup, 'fourth button is in controlGroup'); - - const pointButton = buttons[0]; - const lineButton = buttons[1]; - const polygonButton = buttons[2]; - const trashButton = buttons[3]; - - t.test('click line button', () => { - lineButton.click(); - - assert.ok(lineButton.classList.contains('active'), 'line button is active'); - assert.equal(polygonButton.classList.contains('active'), false, 'polygon button is inactive'); - assert.equal(pointButton.classList.contains('active'), false, 'point button is inactive'); - assert.equal(trashButton.classList.contains('active'), false, 'trash button is inactive'); - - assert.equal(context.events.changeMode.callCount, 1, 'changeMode called'); - assert.deepEqual(context.events.changeMode.getCall(0).args, ['draw_line_string'], 'with correct arguments'); - context.events.changeMode.resetHistory(); - }); - - await t.test('click polygon button', () => { - polygonButton.click(); - - assert.equal(lineButton.classList.contains('active'), false, 'line button is inactive'); - assert.ok(polygonButton.classList.contains('active'), 'polygon button is active'); - assert.equal(pointButton.classList.contains('active'), false, 'point button is inactive'); - assert.equal(trashButton.classList.contains('active'), false, 'trash button is inactive'); - - assert.equal(context.events.changeMode.callCount, 1, 'changeMode called'); - assert.deepEqual(context.events.changeMode.getCall(0).args, ['draw_polygon'], 'with correct arguments'); - context.events.changeMode.resetHistory(); - }); - - await t.test('programmatically activate point button, then programmatically deactivate', () => { - testUi.setActiveButton('point'); - - assert.equal(lineButton.classList.contains('active'), false, 'line button is inactive'); - assert.equal(polygonButton.classList.contains('active'), false, 'polygon button is inactive'); - assert.ok(pointButton.classList.contains('active'), 'point button is active'); - assert.equal(trashButton.classList.contains('active'), false, 'trash button is inactive'); - assert.equal(context.events.changeMode.callCount, 0, 'changeMode not called'); - - testUi.setActiveButton(); - - assert.equal(lineButton.classList.contains('active'), false, 'line button is inactive'); - assert.equal(polygonButton.classList.contains('active'), false, 'polygon button is inactive'); - assert.equal(pointButton.classList.contains('active'), false, 'point button is inactive'); - assert.equal(trashButton.classList.contains('active'), false, 'trash button is inactive'); - assert.equal(context.events.changeMode.callCount, 0, 'changeMode not called'); - }); - - await t.test('click trash button', () => { - trashButton.click(); - - assert.equal(lineButton.classList.contains('active'), false, 'line button is inactive'); - assert.equal(polygonButton.classList.contains('active'), false, 'polygon button is inactive'); - assert.equal(pointButton.classList.contains('active'), false, 'point button is inactive'); - assert.equal(trashButton.classList.contains('active'), false, 'trash button is inactive'); - - assert.equal(context.events.changeMode.callCount, 0, 'changeMode not called'); - }); - - cleanup(); -}); diff --git a/test/ui.test.ts b/test/ui.test.ts new file mode 100644 index 00000000..6a10f431 --- /dev/null +++ b/test/ui.test.ts @@ -0,0 +1,453 @@ +import './mock-browser'; +import { spy } from 'sinon'; +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import ui from '../src/ui'; + +type Options = { + position?: string; + controls?: unknown; +}; + +function createMockContext(options: Options = {}) { + const { position, controls } = options; + + const container = document.createElement('div'); + document.body.appendChild(container); + + const controlContainer = document.createElement('div'); + controlContainer.className = `mapboxgl-ctrl-${position || 'top-left'}`; + container.appendChild(controlContainer); + + return { + context: { + container, + options: { + controls, + keybindings: true + }, + api: { + trash: spy() + }, + events: { + changeMode: spy() + } + }, + cleanup() { + document.body.removeChild(container); + }, + getControlContainer() { + return controlContainer; + } + }; +} + +function getButtons(div) { + return Array.prototype.slice.call( + div.getElementsByClassName('mapbox-gl-draw_ctrl-draw-btn') + ); +} + +test('ui container classes', async t => { + const { context, cleanup } = createMockContext(); + const testUi = ui(context as spy); + + assert.equal( + context.container.className, + '', + 'confirm that class starts empty' + ); + + // Each sub-test relies on state from the prior sub-tests + + t.test('update all classes', () => { + testUi.queueMapClasses({ + mode: 'direct_select', + feature: 'vertex', + mouse: 'move' + }); + testUi.updateMapClasses(); + assert.ok( + context.container.classList.contains('mode-direct_select'), + 'mode class set' + ); + assert.ok( + context.container.classList.contains('feature-vertex'), + 'feature class set' + ); + assert.ok( + context.container.classList.contains('mouse-move'), + 'mouse class set' + ); + }); + + await t.test('update only feature class', () => { + testUi.queueMapClasses({ + feature: 'midpoint' + }); + testUi.updateMapClasses(); + assert.ok( + context.container.classList.contains('mode-direct_select'), + 'mode class remains' + ); + assert.ok( + context.container.classList.contains('feature-midpoint'), + 'feature class updated' + ); + assert.ok( + context.container.classList.contains('mouse-move'), + 'mouse class remains' + ); + }); + + await t.test('update mode and mouse classes', () => { + testUi.queueMapClasses({ + mode: 'foo', + mouse: 'bar' + }); + testUi.updateMapClasses(); + assert.ok( + context.container.classList.contains('mode-foo'), + 'mode class updated' + ); + assert.ok( + context.container.classList.contains('feature-midpoint'), + 'feature class remains' + ); + assert.ok( + context.container.classList.contains('mouse-bar'), + 'mouse class updated' + ); + }); + + await t.test('remove only feature class', () => { + testUi.queueMapClasses({ + feature: null + }); + testUi.updateMapClasses(); + assert.ok( + context.container.classList.contains('mode-foo'), + 'mode class remains' + ); + assert.ok( + context.container.className.indexOf('feature-') === -1, + 'feature class removed' + ); + assert.ok( + context.container.classList.contains('mouse-bar'), + 'mouse class remains' + ); + }); + + await t.test('remove all classes', () => { + testUi.queueMapClasses({ + feature: null, + mode: null, + mouse: null + }); + testUi.updateMapClasses(); + assert.ok( + context.container.className.indexOf('mode-') === -1, + 'mode class removed' + ); + assert.ok( + context.container.className.indexOf('feature-') === -1, + 'feature class still gone' + ); + assert.ok( + context.container.className.indexOf('mouse-') === -1, + 'mouse class removed' + ); + }); + + cleanup(); +}); + +test('ui buttons with no options.controls', () => { + const { context, cleanup } = createMockContext(); + const testUi = ui(context as spy); + + const div = testUi.addButtons(); + assert.deepEqual(getButtons(div), [], 'still no buttons'); + + cleanup(); +}); + +test('ui buttons with one options.controls', () => { + /* eslint-disable */ + const { context, cleanup } = createMockContext({ + controls: { + line_string: true + } + }); + /* eslint-enable */ + const testUi = ui(context as spy); + + const div = testUi.addButtons(); + const buttons = getButtons(div); + assert.equal(buttons.length, 1, 'one button added'); + assert.ok( + buttons[0].classList.contains('mapbox-gl-draw_line'), + 'has line class' + ); + assert.ok( + buttons[0].classList.contains('mapbox-gl-draw_ctrl-draw-btn'), + 'has control class' + ); + + cleanup(); +}); + +test('ui buttons control group container inserted above attribution control, in control container, by addButtons', () => { + const { context, cleanup, getControlContainer } = createMockContext({ + controls: { + trash: true + } + }); + + const controlContainer = getControlContainer(); + const testUi = ui(context as spy); + + assert.equal( + controlContainer.getElementsByClassName('mapboxgl-ctrl-group').length, + 0, + 'confirm control group does not exist at first' + ); + + const controlGroup = testUi.addButtons(); + assert.ok(controlGroup, 'control group exists'); + + cleanup(); +}); + +test('ui buttons with all options.controls, no attribution control', async t => { + /* eslint-disable */ + const { context, cleanup } = createMockContext({ + controls: { + line_string: true, + point: true, + polygon: true, + trash: true + } + }); + /* eslint-enable */ + const testUi = ui(context as spy); + + const controlGroup = testUi.addButtons(); + const buttons = getButtons(controlGroup); + + assert.equal(buttons.length, 4, 'one button added'); + + assert.ok( + buttons[0].classList.contains('mapbox-gl-draw_point'), + 'first button has point class' + ); + assert.ok( + buttons[0].classList.contains('mapbox-gl-draw_ctrl-draw-btn'), + 'first button has control class' + ); + assert.equal( + buttons[0].parentNode, + controlGroup, + 'first button is in controlGroup' + ); + assert.ok( + buttons[1].classList.contains('mapbox-gl-draw_line'), + 'second button has line class' + ); + assert.ok( + buttons[1].classList.contains('mapbox-gl-draw_ctrl-draw-btn'), + 'second button has control class' + ); + assert.equal( + buttons[2].parentNode, + controlGroup, + 'second button is in controlGroup' + ); + assert.ok( + buttons[2].classList.contains('mapbox-gl-draw_polygon'), + 'third button has polygon class' + ); + assert.ok( + buttons[2].classList.contains('mapbox-gl-draw_ctrl-draw-btn'), + 'third button has control class' + ); + assert.equal( + buttons[1].parentNode, + controlGroup, + 'third button is in controlGroup' + ); + assert.ok( + buttons[3].classList.contains('mapbox-gl-draw_trash'), + 'fourth button has trash class' + ); + assert.ok( + buttons[3].classList.contains('mapbox-gl-draw_ctrl-draw-btn'), + 'fourth button has control class' + ); + assert.equal( + buttons[3].parentNode, + controlGroup, + 'fourth button is in controlGroup' + ); + + const pointButton = buttons[0]; + const lineButton = buttons[1]; + const polygonButton = buttons[2]; + const trashButton = buttons[3]; + + t.test('click line button', () => { + lineButton.click(); + + assert.ok(lineButton.classList.contains('active'), 'line button is active'); + assert.equal( + polygonButton.classList.contains('active'), + false, + 'polygon button is inactive' + ); + assert.equal( + pointButton.classList.contains('active'), + false, + 'point button is inactive' + ); + assert.equal( + trashButton.classList.contains('active'), + false, + 'trash button is inactive' + ); + + assert.equal(context.events.changeMode.callCount, 1, 'changeMode called'); + assert.deepEqual( + context.events.changeMode.getCall(0).args, + ['draw_line_string'], + 'with correct arguments' + ); + context.events.changeMode.resetHistory(); + }); + + await t.test('click polygon button', () => { + polygonButton.click(); + + assert.equal( + lineButton.classList.contains('active'), + false, + 'line button is inactive' + ); + assert.ok( + polygonButton.classList.contains('active'), + 'polygon button is active' + ); + assert.equal( + pointButton.classList.contains('active'), + false, + 'point button is inactive' + ); + assert.equal( + trashButton.classList.contains('active'), + false, + 'trash button is inactive' + ); + + assert.equal(context.events.changeMode.callCount, 1, 'changeMode called'); + assert.deepEqual( + context.events.changeMode.getCall(0).args, + ['draw_polygon'], + 'with correct arguments' + ); + context.events.changeMode.resetHistory(); + }); + + await t.test( + 'programmatically activate point button, then programmatically deactivate', + () => { + testUi.setActiveButton('point'); + + assert.equal( + lineButton.classList.contains('active'), + false, + 'line button is inactive' + ); + assert.equal( + polygonButton.classList.contains('active'), + false, + 'polygon button is inactive' + ); + assert.ok( + pointButton.classList.contains('active'), + 'point button is active' + ); + assert.equal( + trashButton.classList.contains('active'), + false, + 'trash button is inactive' + ); + assert.equal( + context.events.changeMode.callCount, + 0, + 'changeMode not called' + ); + + testUi.setActiveButton(''); + + assert.equal( + lineButton.classList.contains('active'), + false, + 'line button is inactive' + ); + assert.equal( + polygonButton.classList.contains('active'), + false, + 'polygon button is inactive' + ); + assert.equal( + pointButton.classList.contains('active'), + false, + 'point button is inactive' + ); + assert.equal( + trashButton.classList.contains('active'), + false, + 'trash button is inactive' + ); + assert.equal( + context.events.changeMode.callCount, + 0, + 'changeMode not called' + ); + } + ); + + await t.test('click trash button', () => { + trashButton.click(); + + assert.equal( + lineButton.classList.contains('active'), + false, + 'line button is inactive' + ); + assert.equal( + polygonButton.classList.contains('active'), + false, + 'polygon button is inactive' + ); + assert.equal( + pointButton.classList.contains('active'), + false, + 'point button is inactive' + ); + assert.equal( + trashButton.classList.contains('active'), + false, + 'trash button is inactive' + ); + + assert.equal( + context.events.changeMode.callCount, + 0, + 'changeMode not called' + ); + }); + + cleanup(); +}); diff --git a/test/utils/after_next_render.js b/test/utils/after_next_render.ts similarity index 84% rename from test/utils/after_next_render.js rename to test/utils/after_next_render.ts index 165f2ab3..7d12b2c2 100644 --- a/test/utils/after_next_render.js +++ b/test/utils/after_next_render.ts @@ -1,6 +1,6 @@ const TIMEOUT = 1000; -export function setupAfterNextRender(map) { +export const setupAfterNextRender = (map) => { let render = 0; map.on('draw.render', () => { render++; @@ -10,7 +10,7 @@ export function setupAfterNextRender(map) { const signal = AbortSignal.timeout(TIMEOUT); signal.addEventListener('abort', () => controller.abort()); - return function afterNextRender(msg) { + return function afterNextRender(msg = '') { return new Promise((resolve, reject) => { const lastRender = render; const id = setInterval(() => { @@ -21,7 +21,7 @@ export function setupAfterNextRender(map) { if (lastRender < render) { clearInterval(id); - resolve(); + resolve(true); } }); }); diff --git a/test/utils/create_feature.js b/test/utils/create_feature.js deleted file mode 100644 index 6c5f5e5e..00000000 --- a/test/utils/create_feature.js +++ /dev/null @@ -1,23 +0,0 @@ -import {generateID} from '../../src/lib/id.js'; -import getGeoJSON from './get_geojson.js'; - -const usedIds = new Set(); - -export function generateUniqueID() { - let id = generateID(); - while (usedIds.has(id)) { - id = generateID(); - } - usedIds.add(id); - return id; -} - -export default function createFeature(featureType) { - const feature = Object.assign({ - id: generateUniqueID(), - properties: {} - }, getGeoJSON(featureType)); - feature.toGeoJSON = () => feature; - feature.setProperty = (property, name) => { feature.properties[property] = name; }; - return feature; -} diff --git a/test/utils/create_feature.ts b/test/utils/create_feature.ts new file mode 100644 index 00000000..d87e474e --- /dev/null +++ b/test/utils/create_feature.ts @@ -0,0 +1,28 @@ +import { generateID } from '../../src/lib/id'; +import getGeoJSON from './get_geojson'; + +const usedIds = new Set(); + +export function generateUniqueID() { + let id = generateID(); + while (usedIds.has(id)) { + id = generateID(); + } + usedIds.add(id); + return id; +} + +export default function createFeature(featureType) { + const feature = Object.assign( + { + id: generateUniqueID(), + properties: {} + }, + getGeoJSON(featureType) + ); + feature.toGeoJSON = () => feature; + feature.setProperty = (property, name) => { + feature.properties[property] = name; + }; + return feature; +} diff --git a/test/utils/create_map.js b/test/utils/create_map.js deleted file mode 100644 index fa96aaa6..00000000 --- a/test/utils/create_map.js +++ /dev/null @@ -1,111 +0,0 @@ -import {bboxClip} from '@turf/bbox-clip'; - -import Evented from '../../bench/lib/evented.js'; -import { interactions } from '../../src/constants.js'; - -class MockMap extends Evented { - constructor(options = {}) { - super(); - - this.sources = {}; - this.style = { - _layers: {}, - getLayer: id => this.style._layers[id], - addSource: (id, source) => { - this.style._layers[id] = source; - }, - removeSource: (id) => { - delete this.style._layers[id]; - }, - }; - this.options = { - container: document.createElement('div'), - ...options - }; - - for (const interaction of interactions) { - this[interaction] = { - enabled: true, - disable() { this.enabled = false; }, - enable() { this.enabled = true; }, - isEnabled() { return this.enabled; }, - }; - } - - setTimeout(() => { - this.fire('load'); - }, 0); - } - - addControl(control) { - control.onAdd(this); - } - - loaded() { - return true; - } - - getLayer(id) { - return this.style.getLayer(id); - } - - getContainer() { - return this.options.container; - } - - addSource(name, source) { - this.style.addSource(name, source); - this.sources[name] = source; - } - removeSource(name) { - delete this.sources[name]; - } - getSource(name) { - const source = this.sources[name]; - return { - ...source, - setData(data) { - source.data = data; - } - }; - } - - addLayer() {} - - queryRenderedFeatures([p0, p1]) { - if (!Array.isArray(p0)) p0 = [p0.x, p0.y]; - if (!Array.isArray(p1)) p1 = [p1.x, p1.y]; - const minX = Math.min(p0[0], p1[0]); - const minY = Math.min(p0[1], p1[1]); - const maxX = Math.max(p0[0], p1[0]); - const maxY = Math.max(p0[1], p1[1]); - const bbox = [minX, minY, maxX, maxY]; - const features = []; - - for (const source of Object.values(this.sources)) { - for (const feature of source.data.features) { - if (feature.geometry.type === 'Point') { - const [x, y] = feature.geometry.coordinates; - if (x >= minX && x <= maxX && y >= minY && y <= maxY) features.push(feature); - - } else if (feature.geometry.type === 'MultiPoint') { - for (const [x, y] of feature.geometry.coordinates) { - if (x >= minX && x <= maxX && y >= minY && y <= maxY) { - features.push(feature); - break; - } - } - - } else { - const clipped = bboxClip(feature, bbox); - if (clipped.geometry.coordinates.length) features.push(feature); - } - } - } - return features; - } -} - -export default function createMap(mapOptions) { - return new MockMap(mapOptions); -} diff --git a/test/utils/create_map.ts b/test/utils/create_map.ts new file mode 100644 index 00000000..ebbcc284 --- /dev/null +++ b/test/utils/create_map.ts @@ -0,0 +1,175 @@ +import { bboxClip } from '@turf/bbox-clip'; +import Evented from '../../bench/lib/evented'; +import { interactions } from '../../src/constants'; +import type { StrictFeature } from '../../src/types/types'; +import type { Polygon } from 'geojson'; + +type Source = { + data: { features: StrictFeature[] }; + setData?: (data: { features: StrictFeature[] }) => void; +}; + +type Layer = Record; +type Coordinates = [number, number]; + +type MapOptions = { + container?: HTMLElement; +} & Record; + +// Define the structure of an interaction +type Interaction = { + enabled: boolean; + disable(): void; + enable(): void; + isEnabled(): boolean; +}; + +class MockMap extends Evented { + sources: Record = {}; + style: { + _layers: Layer; + getLayer: (id: string) => Source | undefined; + addSource: (id: string, source: Source) => void; + removeSource: (id: string) => void; + }; + options: MapOptions; + + // Explicitly declare interactions for TypeScript + scrollZoom: Interaction; + boxZoom: Interaction; + dragRotate: Interaction; + dragPan: Interaction; + keyboard: Interaction; + doubleClickZoom: Interaction; + touchZoomRotate: Interaction; + + constructor(options: MapOptions = {}) { + super(); + + this.style = { + _layers: {}, + getLayer: (id: string) => this.style._layers[id], + addSource: (id: string, source: Source) => { + this.style._layers[id] = source; + }, + removeSource: (id: string) => { + delete this.style._layers[id]; + } + }; + this.options = { + container: document.createElement('div'), + ...options + }; + + // Explicitly define each interaction + this.scrollZoom = this.createInteraction(); + this.boxZoom = this.createInteraction(); + this.dragRotate = this.createInteraction(); + this.dragPan = this.createInteraction(); + this.keyboard = this.createInteraction(); + this.doubleClickZoom = this.createInteraction(); + this.touchZoomRotate = this.createInteraction(); + + // Dynamically add any other interactions + for (const interaction of interactions) { + if (!(interaction in this)) { + (this as any)[interaction] = this.createInteraction(); + } + } + + setTimeout(() => { + this.fire('load'); + }, 0); + } + + // Helper function to create interaction objects + createInteraction(): Interaction { + return { + enabled: true, + disable() { + this.enabled = false; + }, + enable() { + this.enabled = true; + }, + isEnabled() { + return this.enabled; + } + }; + } + + addControl(control: { onAdd: (map: MockMap) => void }) { + control.onAdd(this); + } + + loaded(): boolean { + return true; + } + + getLayer(id: string): Source | undefined { + return this.style.getLayer(id); + } + + getContainer(): HTMLElement { + return this.options.container; + } + + addSource(name: string, source: Source): void { + this.style.addSource(name, source); + this.sources[name] = source; + } + + removeSource(name: string): void { + delete this.sources[name]; + } + + getSource(name: string): Source | undefined { + const source = this.sources[name]; + return source + ? { + ...source, + setData(data: { features: StrictFeature[] }) { + source.data = data; + } + } + : undefined; + } + + addLayer() {} + + queryRenderedFeatures([p0, p1]: [{ x: number; y: number } | Coordinates, { x: number; y: number } | Coordinates]): StrictFeature[] { + if (!Array.isArray(p0)) p0 = [p0.x, p0.y]; + if (!Array.isArray(p1)) p1 = [p1.x, p1.y]; + const minX = Math.min(p0[0], p1[0]); + const minY = Math.min(p0[1], p1[1]); + const maxX = Math.max(p0[0], p1[0]); + const maxY = Math.max(p0[1], p1[1]); + const bbox: [number, number, number, number] = [minX, minY, maxX, maxY]; + const features: StrictFeature[] = []; + + for (const source of Object.values(this.sources)) { + for (const feature of source.data.features) { + if (feature.geometry.type === 'Point') { + const [x, y] = feature.geometry.coordinates as Coordinates; + if (x >= minX && x <= maxX && y >= minY && y <= maxY) + features.push(feature); + } else if (feature.geometry.type === 'MultiPoint') { + for (const [x, y] of feature.geometry.coordinates as Coordinates[]) { + if (x >= minX && x <= maxX && y >= minY && y <= maxY) { + features.push(feature); + break; + } + } + } else { + const clipped = bboxClip(feature as unknown as Polygon, bbox); + if (clipped.geometry.coordinates.length) features.push(feature); + } + } + } + return features; + } +} + +export default function createMap(mapOptions?: MapOptions): MockMap { + return new MockMap(mapOptions); +} diff --git a/test/utils/create_mock_draw_mode_context.js b/test/utils/create_mock_draw_mode_context.ts similarity index 96% rename from test/utils/create_mock_draw_mode_context.js rename to test/utils/create_mock_draw_mode_context.ts index d2657eee..73f6b95b 100644 --- a/test/utils/create_mock_draw_mode_context.js +++ b/test/utils/create_mock_draw_mode_context.ts @@ -1,4 +1,4 @@ -import {spy} from 'sinon'; +import { spy } from 'sinon'; export default function createMockDrawModeContext() { const _store = {}; diff --git a/test/utils/create_mock_feature_context.js b/test/utils/create_mock_feature_context.ts similarity index 63% rename from test/utils/create_mock_feature_context.js rename to test/utils/create_mock_feature_context.ts index 54e1f75d..c756213b 100644 --- a/test/utils/create_mock_feature_context.js +++ b/test/utils/create_mock_feature_context.ts @@ -1,4 +1,4 @@ -import {spy} from 'sinon'; +import { spy } from 'sinon'; /** * Returns an mock ctx object with just those properties a Feature @@ -6,7 +6,9 @@ import {spy} from 'sinon'; * * @return {Object} */ -export default function createMockFeatureContext(opts = { userProperties: false}) { +export default function createMockFeatureContext( + opts = { userProperties: false } +): spy { return { options: { userProperties: opts.userProperties @@ -14,5 +16,5 @@ export default function createMockFeatureContext(opts = { userProperties: false} store: { featureChanged: spy() } - }; + } as spy; } diff --git a/test/utils/create_mock_lifecycle_context.js b/test/utils/create_mock_lifecycle_context.ts similarity index 75% rename from test/utils/create_mock_lifecycle_context.js rename to test/utils/create_mock_lifecycle_context.ts index 4063b36b..0aae8ac5 100644 --- a/test/utils/create_mock_lifecycle_context.js +++ b/test/utils/create_mock_lifecycle_context.ts @@ -1,4 +1,4 @@ -import {spy} from 'sinon'; +import { spy } from 'sinon'; export default function createMockLifecycleContext() { return { diff --git a/test/utils/create_mock_mode.js b/test/utils/create_mock_mode.ts similarity index 83% rename from test/utils/create_mock_mode.js rename to test/utils/create_mock_mode.ts index 05075e05..afb11e37 100644 --- a/test/utils/create_mock_mode.js +++ b/test/utils/create_mock_mode.ts @@ -1,4 +1,4 @@ -import {spy} from 'sinon'; +import { spy } from 'sinon'; export default function createMockModeHandlerContext() { return { diff --git a/test/utils/create_mock_mode_handler_context.js b/test/utils/create_mock_mode_handler_context.ts similarity index 86% rename from test/utils/create_mock_mode_handler_context.js rename to test/utils/create_mock_mode_handler_context.ts index c78183f2..a18aeda0 100644 --- a/test/utils/create_mock_mode_handler_context.js +++ b/test/utils/create_mock_mode_handler_context.ts @@ -1,4 +1,4 @@ -import {spy} from 'sinon'; +import { spy } from 'sinon'; export default function createMockModeHandlerContext() { return { diff --git a/test/utils/draw_geometry.js b/test/utils/draw_geometry.ts similarity index 73% rename from test/utils/draw_geometry.js rename to test/utils/draw_geometry.ts index dc429f01..721146fa 100644 --- a/test/utils/draw_geometry.js +++ b/test/utils/draw_geometry.ts @@ -1,10 +1,6 @@ -import click from './mouse_click.js'; -import {setupAfterNextRender} from './after_next_render.js'; -import makeMouseEvent from './make_mouse_event.js'; - -/** - * Draws a feature on a map. - */ +import click from './mouse_click'; +import { setupAfterNextRender } from './after_next_render'; +import makeMouseEvent from './make_mouse_event'; const mapFeaturesToModes = { Polygon: 'draw_polygon', @@ -23,6 +19,6 @@ export async function drawGeometry(map, draw, type, coordinates) { for (const point of drawCoordinates) { click(map, makeMouseEvent(point[0], point[1], false)); - await afterNextRender(); + await afterNextRender(map); } } diff --git a/test/utils/get_geojson.js b/test/utils/get_geojson.ts similarity index 50% rename from test/utils/get_geojson.js rename to test/utils/get_geojson.ts index 1190231d..5ef17a85 100644 --- a/test/utils/get_geojson.js +++ b/test/utils/get_geojson.ts @@ -1,10 +1,20 @@ const features = { multiPolygon: { type: 'Feature', - properties: {'a':'b', 'c':'d'}, + properties: { a: 'b', c: 'd' }, geometry: { type: 'MultiPolygon', - coordinates: [[[[1, 1], [2, 2], [2, 6], [4, 3], [1, 1]]]] + coordinates: [ + [ + [ + [1, 1], + [2, 2], + [2, 6], + [4, 3], + [1, 1] + ] + ] + ] } }, @@ -15,10 +25,20 @@ const features = { type: 'MultiPolygon', coordinates: [ [ - [[1, 1], [2, 2], [4, 3], [1, 1]] + [ + [1, 1], + [2, 2], + [4, 3], + [1, 1] + ] ], [ - [[30, 20], [50, 40], [70, 30], [30, 20]] + [ + [30, 20], + [50, 40], + [70, 30], + [30, 20] + ] ] ] } @@ -29,7 +49,11 @@ const features = { properties: {}, geometry: { type: 'LineString', - coordinates: [[0, 0], [1, 1], [2, 2]] + coordinates: [ + [0, 0], + [1, 1], + [2, 2] + ] } }, @@ -38,7 +62,11 @@ const features = { properties: {}, geometry: { type: 'LineString', - coordinates: [[3, 3], [5, 5], [7, 7]] + coordinates: [ + [3, 3], + [5, 5], + [7, 7] + ] } }, @@ -47,7 +75,18 @@ const features = { properties: {}, geometry: { type: 'MultiLineString', - coordinates: [[[20, 20], [21, 21], [22, 22]], [[30, 30], [31, 31], [32, 32]]] + coordinates: [ + [ + [20, 20], + [21, 21], + [22, 22] + ], + [ + [30, 30], + [31, 31], + [32, 32] + ] + ] } }, @@ -56,7 +95,10 @@ const features = { properties: {}, geometry: { type: 'MultiPoint', - coordinates: [[-5, -5], [-10, -10]] + coordinates: [ + [-5, -5], + [-10, -10] + ] } }, @@ -89,19 +131,35 @@ const features = { polygon: { type: 'Feature', - properties: {'a':'b', 'c':'d'}, + properties: { a: 'b', c: 'd' }, geometry: { type: 'Polygon', - coordinates: [[[30, 20], [50, 40], [70, 30], [50, 20], [30, 20]]] + coordinates: [ + [ + [30, 20], + [50, 40], + [70, 30], + [50, 20], + [30, 20] + ] + ] } }, polygon2: { type: 'Feature', - properties: {'a2':'b2', 'c2':'d2'}, + properties: { a2: 'b2', c2: 'd2' }, geometry: { type: 'Polygon', - coordinates: [[[40, 30], [60, 50], [90, 90], [100, 80], [40, 30]]] + coordinates: [ + [ + [40, 30], + [60, 50], + [90, 90], + [100, 80], + [40, 30] + ] + ] } }, @@ -110,7 +168,15 @@ const features = { properties: {}, geometry: { type: 'Polygon', - coordinates: [[[1, 1], [1, 2], [2, 2], [2, 1], [1, 1]]] + coordinates: [ + [ + [1, 1], + [1, 2], + [2, 2], + [2, 1], + [1, 1] + ] + ] } }, @@ -125,10 +191,20 @@ const features = { properties: {}, geometry: { type: 'GeometryCollection', - geometries: [{ - type: 'Polygon', - coordinates: [[[30, 20], [50, 40], [70, 30], [50, 20], [30, 20]]] - }] + geometries: [ + { + type: 'Polygon', + coordinates: [ + [ + [30, 20], + [50, 40], + [70, 30], + [50, 20], + [30, 20] + ] + ] + } + ] } }, @@ -140,13 +216,21 @@ const features = { properties: {}, geometry: { type: 'Polygon', - coordinates: [[[1, 1], [2, 2], [3, 3], [4, 4], [1, 1]]] + coordinates: [ + [ + [1, 1], + [2, 2], + [3, 3], + [4, 4], + [1, 1] + ] + ] } } ] } }; -export default function getGeoJSON (type) { +export default function getGeoJSON(type) { return JSON.parse(JSON.stringify(features[type])); } diff --git a/test/utils/get_public_member_keys.js b/test/utils/get_public_member_keys.ts similarity index 100% rename from test/utils/get_public_member_keys.js rename to test/utils/get_public_member_keys.ts diff --git a/test/utils/key_events.js b/test/utils/key_events.ts similarity index 80% rename from test/utils/key_events.js rename to test/utils/key_events.ts index c70fb829..4273fde9 100644 --- a/test/utils/key_events.js +++ b/test/utils/key_events.ts @@ -1,9 +1,10 @@ import createSyntheticEvent from 'synthetic-dom-events'; -import * as Constants from '../../src/constants.js'; +import * as Constants from '../../src/constants'; -const classList = [Constants.classes.CANVAS]; -classList.contains = function(cls) { - return classList.indexOf(cls) >= 0; +const classList = [Constants.classes.CANVAS] as any; + +classList.contains = function (cls: string) { + return this.includes(cls); }; export const enterEvent = createSyntheticEvent('keyup', { diff --git a/test/utils/make_mouse_event.js b/test/utils/make_mouse_event.js deleted file mode 100644 index c7fe104b..00000000 --- a/test/utils/make_mouse_event.js +++ /dev/null @@ -1,14 +0,0 @@ -export default function(lng = 0, lat = 0, eventProperties = {}) { - const e = { - originalEvent: Object.assign({ - stopPropagation() {}, - button: 0, - clientX: lng, - clientY: lat - }, eventProperties), - point: {x: lng, y:lat}, - lngLat: {lng, lat} - }; - - return e; -} diff --git a/test/utils/make_mouse_event.ts b/test/utils/make_mouse_event.ts new file mode 100644 index 00000000..829b6236 --- /dev/null +++ b/test/utils/make_mouse_event.ts @@ -0,0 +1,17 @@ +export default function (lng = 0, lat = 0, eventProperties = {}) { + const e = { + originalEvent: Object.assign( + { + stopPropagation() {}, + button: 0, + clientX: lng, + clientY: lat + }, + eventProperties + ), + point: { x: lng, y: lat }, + lngLat: { lng, lat } + }; + + return e; +} diff --git a/test/utils/make_touch_event.js b/test/utils/make_touch_event.js deleted file mode 100644 index 78e120bf..00000000 --- a/test/utils/make_touch_event.js +++ /dev/null @@ -1,15 +0,0 @@ -export default function(lng = 0, lat = 0, eventProperties = {}) { - const e = { - originalEvent: Object.assign({ - stopPropagation() {}, - preventDefault() {}, - button: 0, - clientX: lng, - clientY: lat - }, eventProperties), - point: {x: lng, y:lat}, - lngLat: {lng, lat} - }; - - return e; -} diff --git a/test/utils/make_touch_event.ts b/test/utils/make_touch_event.ts new file mode 100644 index 00000000..e11e80e8 --- /dev/null +++ b/test/utils/make_touch_event.ts @@ -0,0 +1,18 @@ +export default function (lng = 0, lat = 0, eventProperties = {}) { + const e = { + originalEvent: Object.assign( + { + stopPropagation() {}, + preventDefault() {}, + button: 0, + clientX: lng, + clientY: lat + }, + eventProperties + ), + point: { x: lng, y: lat }, + lngLat: { lng, lat } + }; + + return e; +} diff --git a/test/utils/mouse_click.js b/test/utils/mouse_click.ts similarity index 100% rename from test/utils/mouse_click.js rename to test/utils/mouse_click.ts diff --git a/test/utils/touch_tap.js b/test/utils/touch_tap.ts similarity index 100% rename from test/utils/touch_tap.js rename to test/utils/touch_tap.ts diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..adbc111c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "module": "ESNext", + "target": "ESNext", + "moduleResolution": "Node", + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "allowJs": true, + "noEmit": true, + "types": ["node"] + }, + "include": ["src", "test"] +} + diff --git a/vite.config.js b/vite.config.js index 02991154..f1a0ae34 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,17 +1,9 @@ -export default { - root: 'debug/', - base: '/debug/', - envPrefix: 'MAPBOX_', - server: { - host: '0.0.0.0', - port: 9967, - strictPort: true, - }, - optimizeDeps: { - esbuildOptions: { - define: { - global: 'globalThis' - } - } +import { defineConfig } from 'vite'; +import tsconfigPaths from 'vite-plugin-tsconfig-paths'; + +export default defineConfig({ + plugins: [tsconfigPaths()], + build: { + target: 'esnext' // Ensure compatibility with modern JavaScript } -}; +});