diff --git a/README.md b/README.md index 8a0a551..bbb2311 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ Adds line data passed in `options.data` to the Leaflet map instance passed in `o * `pane` `{String}` optional, default is `overlayPane`. Can be set to a custom pane. --- ### `L.glify.shapes(options: object)` -Adds polygon data passed in `options.data` to the Leaflet map instance passed in `options.map`. +Adds polygon/multipolygon data passed in `options.data` to the Leaflet map instance passed in `options.map`. #### Returns `L.glify.Shapes` instance #### Options diff --git a/example.ts b/example.ts index 1255192..79c5894 100644 --- a/example.ts +++ b/example.ts @@ -1,170 +1,248 @@ -import * as L from 'leaflet'; -import { LeafletMouseEvent } from 'leaflet'; -import { FeatureCollection, LineString } from 'geojson'; -import glify from './src/index'; +import * as L from "leaflet"; +import { LeafletMouseEvent } from "leaflet"; +import { Feature, FeatureCollection, LineString, MultiPolygon } from "geojson"; +import glify from "./src/index"; -const map = L.map('map') - .setView([50.00, 14.44], 7); +const map = L.map("map").setView([50.0, 14.44], 7); -L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png') - .addTo(map); +L.tileLayer( + "https://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png" +).addTo(map); Promise.all([ - wget('data/86T.json'), - wget('data/CZDistricts.json'), - wget>('data/rivers.json') -]) - .then(([points, districts, rivers]) => { - glify.shapes({ - map: map, - click: (e, feature): void => { - L.popup() - .setLatLng(e.latlng) - .setContent(`You clicked on ${feature.properties.NAZKR_ENG}`) - .openOn(map); - - console.log('clicked on Shape', feature, e); - }, - hover: (e: LeafletMouseEvent, feature) => { - console.log('hovered on Shape', feature, e); - }, - data: districts, - border: true, - }); - - glify.lines({ - map, - latitudeKey: 1, - longitudeKey: 0, - weight: 2, - click: (e: LeafletMouseEvent, feature) => { - L.popup() - .setLatLng(e.latlng) - .setContent(`clicked on Line ${feature.properties.name}`) - .openOn(map); - - console.log('clicked on Line', feature, e); - }, - hover: (e: LeafletMouseEvent, feature) => { - console.log('hovered on Line', feature, e); - }, - hoverOff: (e: LeafletMouseEvent, feature) => { - console.log('hovered off Line', feature, e); - }, - data: rivers - }); - - glify.points({ - map: map, - size: function(i) { - return (Math.random() * 17) + 3; - }, - hover: (e: LeafletMouseEvent, feature) => { - console.log('hovered on Point', feature, e); - }, - click: (e: LeafletMouseEvent, feature) => { - //set up a standalone popup (use a popup as a layer) - L.popup() - .setLatLng(feature) - .setContent(`You clicked the point at longitude:${ e.latlng.lng }, latitude:${ e.latlng.lat }`) - .openOn(map); - - console.log('clicked on Point', feature, e); - }, - data: points - }); - - glify.points({ - map, - size: (i) => { - return 20; - }, - color: () => { - return { - r: 1, - g: 0, - b: 0, - }; - }, - click: (e: LeafletMouseEvent, feature) => { - //set up a standalone popup (use a popup as a layer) - L.popup() - .setLatLng(feature) - .setContent(`You clicked the point at longitude:${e.latlng.lng}, latitude:${e.latlng.lat}`) - .openOn(map); - - console.log('clicked on Point', feature, e); - }, - hover: (e: LeafletMouseEvent, feature) => { - console.log('hovered on Point', feature, e); - }, - data: [[50.10164799,14.5]] - }); - - glify.points({ - map, - size: (i) => { - return 20; - }, - color: () => { - return { - r: 0, - g: 0, - b: 1, - }; - }, - hover: (e: LeafletMouseEvent, feature) => { - console.log('hovered on Point', feature, e); - }, - hoverOff: (e: LeafletMouseEvent, feature) => { - - }, - click: (e, feature) => { - //set up a standalone popup (use a popup as a layer) - L.popup() - .setLatLng(feature.geometry.coordinates) - .setContent('You clicked on:' + feature.properties.name) - .openOn(map); - - console.log('clicked on Point', feature, e); - }, - data: { //geojson - 'type': 'FeatureCollection', - 'features':[ - { - 'type':'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [90, 135] - }, - 'properties': { - 'name': 'North Pole', - 'color': 'red' - } + wget("data/86T.json"), + wget("data/CZDistricts.json"), + wget>("data/rivers.json"), + wget>("data/antarctica.geojson"), +]).then(([points, districts, rivers, antarctica]) => { + glify.shapes({ + map: map, + click: (e, feature): void => { + L.popup() + .setLatLng(e.latlng) + .setContent(`You clicked on ${feature.properties.NAZKR_ENG}`) + .openOn(map); + + console.log("clicked on Shape", feature, e); + }, + contextMenu: (e, feature): void => { + e.originalEvent.preventDefault(); // Prevent the default context menu from showing + L.popup() + .setLatLng(e.latlng) + .setContent(`You right clicked on ${feature.properties.NAZKR_ENG}`) + .openOn(map); + + console.log("clicked on Shape", feature, e); + }, + hover: (e: LeafletMouseEvent, feature) => { + console.log("hovered on Shape", feature, e); + }, + data: districts, + border: true, + }); + + glify.lines({ + map, + latitudeKey: 1, + longitudeKey: 0, + weight: 2, + click: (e: LeafletMouseEvent, feature) => { + L.popup() + .setLatLng(e.latlng) + .setContent(`clicked on Line ${feature.properties.name}`) + .openOn(map); + + console.log("clicked on Line", feature, e); + }, + contextMenu: (e: LeafletMouseEvent, feature) => { + e.originalEvent.preventDefault(); // Prevent the default context menu from showing + //set up a standalone popup (use a popup as a layer) + L.popup() + .setLatLng(e.latlng) + .setContent(`right clicked on Line ${feature.properties.name}`) + .openOn(map); + }, + hover: (e: LeafletMouseEvent, feature) => { + console.log("hovered on Line", feature, e); + }, + hoverOff: (e: LeafletMouseEvent, feature) => { + console.log("hovered off Line", feature, e); + }, + data: rivers, + }); + + glify.points({ + map: map, + size: function (i) { + return Math.random() * 17 + 3; + }, + hover: (e: LeafletMouseEvent, feature) => { + console.log("hovered on Point", feature, e); + }, + click: (e: LeafletMouseEvent, feature) => { + //set up a standalone popup (use a popup as a layer) + L.popup() + .setLatLng(feature) + .setContent( + `You clicked the point at longitude:${e.latlng.lng}, latitude:${e.latlng.lat}` + ) + .openOn(map); + + console.log("clicked on Point", feature, e); + }, + contextMenu: (e: LeafletMouseEvent, feature) => { + e.originalEvent.preventDefault(); // Prevent the default context menu from showing + //set up a standalone popup (use a popup as a layer) + L.popup() + .setLatLng(feature) + .setContent( + `You right clicked the point at longitude:${e.latlng.lng}, latitude:${e.latlng.lat}` + ) + .openOn(map); + }, + data: points, + }); + + glify.points({ + map, + size: (i) => { + return 20; + }, + color: () => { + return { + r: 1, + g: 0, + b: 0, + }; + }, + click: (e: LeafletMouseEvent, feature) => { + //set up a standalone popup (use a popup as a layer) + L.popup() + .setLatLng(feature) + .setContent( + `You clicked the point at longitude:${e.latlng.lng}, latitude:${e.latlng.lat}` + ) + .openOn(map); + + console.log("clicked on Point", feature, e); + }, + contextMenu: (e: LeafletMouseEvent, feature) => { + e.originalEvent.preventDefault(); // Prevent the default context menu from showing + //set up a standalone popup (use a popup as a layer) + L.popup() + .setLatLng(feature) + .setContent( + `You right clicked the point at longitude:${e.latlng.lng}, latitude:${e.latlng.lat}` + ) + .openOn(map); + + console.log("clicked on Point", feature, e); + }, + hover: (e: LeafletMouseEvent, feature) => { + console.log("hovered on Point", feature, e); + }, + data: [[50.10164799, 14.5]], + }); + + glify.points({ + map, + size: (i) => { + return 20; + }, + color: () => { + return { + r: 0, + g: 0, + b: 1, + }; + }, + hover: (e: LeafletMouseEvent, feature) => { + console.log("hovered on Point", feature, e); + }, + hoverOff: (e: LeafletMouseEvent, feature) => {}, + click: (e, feature) => { + //set up a standalone popup (use a popup as a layer) + L.popup() + .setLatLng(feature.geometry.coordinates) + .setContent("You clicked on:" + feature.properties.name) + .openOn(map); + + console.log("clicked on Point", feature, e); + }, + contextMenu: (e, feature) => { + e.originalEvent.preventDefault(); // Prevent the default context menu from showing + //set up a standalone popup (use a popup as a layer) + L.popup() + .setLatLng(feature.geometry.coordinates) + .setContent("You right clicked on:" + feature.properties.name) + .openOn(map); + + console.log("clicked on Point", feature, e); + }, + data: { + //geojson + type: "FeatureCollection", + features: [ + { + type: "Feature", + geometry: { + type: "Point", + coordinates: [90, 135], }, - { - 'type':'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [90, 45] - }, - 'properties': { - 'name': 'South Pole', - 'color': 'blue' - } - } - ], - } - }); + properties: { + name: "North Pole", + color: "red", + }, + }, + { + type: "Feature", + geometry: { + type: "Point", + coordinates: [90, 45], + }, + properties: { + name: "South Pole", + color: "blue", + }, + }, + ], + }, + }); + + glify.shapes({ + map, + data: antarctica, + border: true, + click: (e, feature) => { + L.popup() + .setLatLng(e.latlng) + .setContent(`You clicked on ${feature.properties.name}`) + .openOn(map); + + console.log("clicked on Shape", feature, e); + }, + contextMenu: (e, feature) => { + e.originalEvent.preventDefault(); // Prevent the default context menu from showing + L.popup() + .setLatLng(e.latlng) + .setContent(`You right clicked on ${feature.properties.name}`) + .openOn(map); + + console.log("clicked on Shape", feature, e); + }, + hover: (e, feature) => { + console.log("hovered on Shape", feature, e); + }, }); +}); function wget(url: string): Promise { return new Promise((resolve, reject) => { const request = new XMLHttpRequest(); - request.open('GET', url, true); + request.open("GET", url, true); request.onload = () => { if (request.status < 200 && request.status > 400) { - return reject(new Error('failure')); + return reject(new Error("failure")); } resolve(JSON.parse(request.responseText) as T); }; diff --git a/package-lock.json b/package-lock.json index 6267889..c9ea727 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "leaflet.glify", - "version": "3.3.0", + "version": "3.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "leaflet.glify", - "version": "3.3.0", + "version": "3.3.1", "license": "MIT", "dependencies": { "earcut": "^2.2.4", @@ -54,7 +54,7 @@ "ts-loader": "^9.5.1", "ts-shader-loader": "^2.0.2", "typescript": "^5.4.5", - "webpack": "^5.92.0", + "webpack": "^5.94.0", "webpack-cli": "^5.1.4", "webpack-dev-server": "^5.0.4" }, @@ -1675,10 +1675,11 @@ } }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" }, "node_modules/@types/express": { "version": "4.17.21", @@ -2353,10 +2354,11 @@ } }, "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -2383,15 +2385,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-attributes": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", - "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", - "dev": true, - "peerDependencies": { - "acorn": "^8" - } - }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -3026,9 +3019,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", - "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", + "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", "dev": true, "funding": [ { @@ -3044,11 +3037,12 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001629", - "electron-to-chromium": "^1.4.796", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.16" + "caniuse-lite": "^1.0.30001669", + "electron-to-chromium": "^1.5.41", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" @@ -3233,9 +3227,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001633", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001633.tgz", - "integrity": "sha512-6sT0yf/z5jqf8tISAgpJDrmwOpLsrpnyCdD/lOZKvKkkJK4Dn0X5i7KF7THEZhOq+30bmhwBlNEaqPUiHiKtZg==", + "version": "1.0.30001684", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001684.tgz", + "integrity": "sha512-G1LRwLIQjBQoyq0ZJGqGIJUXzJ8irpbjHLpVRXDvBEScFJ9b17sgK6vlx0GAJFE21okD7zXl08rRRUfq6HdoEQ==", "dev": true, "funding": [ { @@ -3250,7 +3244,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/canvas": { "version": "2.11.2", @@ -4272,10 +4267,11 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.802", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.802.tgz", - "integrity": "sha512-TnTMUATbgNdPXVSHsxvNVSG0uEd6cSZsANjm8c9HbvflZVVn1yTRcmVXYT1Ma95/ssB/Dcd30AHweH2TE+dNpA==", - "dev": true + "version": "1.5.67", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.67.tgz", + "integrity": "sha512-nz88NNBsD7kQSAGGJyp8hS6xSPtWwqNogA0mjtc2nUYeEf3nURK9qpV18TuBdDmEDgVWotS8Wkzf+V52dSQ/LQ==", + "dev": true, + "license": "ISC" }, "node_modules/email-addresses": { "version": "5.0.0", @@ -4311,10 +4307,11 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.17.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz", - "integrity": "sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==", + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", "dev": true, + "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -4567,10 +4564,11 @@ } }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -8673,10 +8671,11 @@ "dev": true }, "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", - "dev": true + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true, + "license": "MIT" }, "node_modules/nopt": { "version": "5.0.0", @@ -9309,10 +9308,11 @@ } }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", - "dev": true + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", @@ -11636,9 +11636,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", - "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", "dev": true, "funding": [ { @@ -11654,9 +11654,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.0" }, "bin": { "update-browserslist-db": "cli.js" @@ -11791,21 +11792,21 @@ "dev": true }, "node_modules/webpack": { - "version": "5.92.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.92.0.tgz", - "integrity": "sha512-Bsw2X39MYIgxouNATyVpCNVWBCuUwDgWtN78g6lSdPJRLaQ/PUVm/oXcaRAyY/sMFoKFQrsPeqvTizWtq7QPCA==", + "version": "5.96.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.96.1.tgz", + "integrity": "sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA==", "dev": true, + "license": "MIT", "dependencies": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.5", + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", "@webassemblyjs/ast": "^1.12.1", "@webassemblyjs/wasm-edit": "^1.12.1", "@webassemblyjs/wasm-parser": "^1.12.1", - "acorn": "^8.7.1", - "acorn-import-attributes": "^1.9.5", - "browserslist": "^4.21.10", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.0", + "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", diff --git a/package.json b/package.json index 67a7bd8..f1934e6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "leaflet.glify", - "version": "3.3.0", + "version": "3.3.1", "description": "web gl renderer plugin for leaflet", "main": "dist/glify.js", "browser": "dist/glify-browser.js", @@ -78,7 +78,7 @@ "ts-loader": "^9.5.1", "ts-shader-loader": "^2.0.2", "typescript": "^5.4.5", - "webpack": "^5.92.0", + "webpack": "^5.94.0", "webpack-cli": "^5.1.4", "webpack-dev-server": "^5.0.4" }, diff --git a/src/__mocks__/canvas-overlay.ts b/src/__mocks__/canvas-overlay.ts deleted file mode 100644 index 065a697..0000000 --- a/src/__mocks__/canvas-overlay.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ICanvasOverlayDrawEvent } from "../canvas-overlay"; -import { Map } from "leaflet"; - -export class CanvasOverlay { - _userDrawFunc: (e: ICanvasOverlayDrawEvent) => void; - constructor(userDrawFunc: (e: ICanvasOverlayDrawEvent) => void) { - this._userDrawFunc = userDrawFunc; - } - - canvas: HTMLCanvasElement = (() => { - const canvas = document.createElement("canvas"); - jest.spyOn(canvas, "getContext"); - return canvas; - })(); - - addTo(map: Map): this { - return this; - } - - redraw(): void {} -} diff --git a/src/base-gl-layer.ts b/src/base-gl-layer.ts index 5dd548e..eb7b149 100644 --- a/src/base-gl-layer.ts +++ b/src/base-gl-layer.ts @@ -1,7 +1,7 @@ import { LeafletMouseEvent, Map } from "leaflet"; import { IColor } from "./color"; -import { IPixel } from "./pixel" +import { IPixel } from "./pixel"; import { CanvasOverlay, ICanvasOverlayDrawEvent } from "./canvas-overlay"; import { notProperlyDefined } from "./errors"; import { MapMatrix } from "./map-matrix"; @@ -34,6 +34,7 @@ export interface IBaseGlLayerSettings { [name: string]: IShaderVariable; }; setupClick?: (map: Map) => void; + setupContextMenu?: (map: Map) => void; setupHover?: SetupHoverCallback; sensitivity?: number; sensitivityHover?: number; @@ -41,6 +42,7 @@ export interface IBaseGlLayerSettings { fragmentShaderSource?: (() => string) | string; canvas?: HTMLCanvasElement; click?: EventCallback; + contextMenu?: EventCallback; hover?: EventCallback; hoverOff?: EventCallback; color?: ColorCallback | IColor | null; @@ -59,7 +61,7 @@ export const defaults: Partial = { export type ColorCallback = (featureIndex: number, feature: any) => IColor; export abstract class BaseGlLayer< - T extends IBaseGlLayerSettings = IBaseGlLayerSettings + T extends IBaseGlLayerSettings = IBaseGlLayerSettings, > { bytes = 0; active: boolean; @@ -160,10 +162,10 @@ export abstract class BaseGlLayer< this.matrix = null; this.vertices = null; this.vertexLines = null; - try{ - this.mapCenterPixels = this.map.project(this.map.getCenter(), 0) - } catch(err){ - this.mapCenterPixels = {x:-0,y:-0} + try { + this.mapCenterPixels = this.map.project(this.map.getCenter(), 0); + } catch (err) { + this.mapCenterPixels = { x: -0, y: -0 }; } const preserveDrawingBuffer = Boolean(settings.preserveDrawingBuffer); const layer = (this.layer = new CanvasOverlay( @@ -235,6 +237,9 @@ export abstract class BaseGlLayer< if (settings.click && settings.setupClick) { settings.setupClick(this.map); } + if (settings.contextMenu && settings.setupContextMenu) { + settings.setupContextMenu(this.map); + } if (settings.hover && settings.setupHover) { settings.setupHover(this.map, this.hoverWait); } @@ -417,6 +422,14 @@ export abstract class BaseGlLayer< } } + contextMenu(e: LeafletMouseEvent, feature: any): boolean | undefined { + if (!this.settings.contextMenu) return; + const result = this.settings.contextMenu(e, feature); + if (result !== undefined) { + return result; + } + } + hover(e: LeafletMouseEvent, feature: any): boolean | undefined { if (!this.settings.hover) return; const result = this.settings.hover(e, feature); diff --git a/src/canvas-overlay.ts b/src/canvas-overlay.ts index ed7078a..4035f16 100644 --- a/src/canvas-overlay.ts +++ b/src/canvas-overlay.ts @@ -48,6 +48,14 @@ export class CanvasOverlay extends Layer { _leaflet_id?: number; options: LayerOptions; + get map(): Map { + return this._map; + } + + set map(map: Map) { + this._map = map; + } + constructor(userDrawFunc: IUserDrawFunc, pane: string) { super(); this._userDrawFunc = userDrawFunc; @@ -78,11 +86,11 @@ export class CanvasOverlay extends Layer { } isAnimated(): boolean { - return Boolean(this._map.options.zoomAnimation && Browser.any3d); + return Boolean(this.map.options.zoomAnimation && Browser.any3d); } onAdd(map: Map): this { - this._map = map; + this.map = map; const canvas = (this.canvas = this.canvas ?? document.createElement("canvas")); @@ -136,18 +144,14 @@ export class CanvasOverlay extends Layer { } addTo(map: Map): this { + if (!this.canvas) { + //Resolves an issue where the canvas is not added to the map, discovered in a jsdom testing environment + this.canvas = document.createElement("canvas"); + } map.addLayer(this); return this; } - get map(): Map { - return this._map; - } - - set map(map: Map) { - this._map = map; - } - _resize(resizeEvent: ResizeEvent): void { if (this.canvas) { this.canvas.width = resizeEvent.newSize.x; @@ -157,20 +161,20 @@ export class CanvasOverlay extends Layer { _reset(): void { if (this.canvas) { - const topLeft = this._map.containerPointToLayerPoint([0, 0]); + const topLeft = this.map.containerPointToLayerPoint([0, 0]); DomUtil.setPosition(this.canvas, topLeft); } this._redraw(); } _redraw(): void { - const { _map, canvas } = this; - if (_map) { - const size = _map.getSize(); - const bounds = _map.getBounds(); + const { map, canvas } = this; + if (map) { + const size = map.getSize(); + const bounds = map.getBounds(); const zoomScale = (size.x * 180) / (20037508.34 * (bounds.getEast() - bounds.getWest())); // resolution = 1/zoomScale - const zoom = _map.getZoom(); + const zoom = map.getZoom(); const topLeft = new LatLng(bounds.getNorth(), bounds.getWest()); const offset = this._unclampedProject(topLeft, 0); if (canvas) { @@ -197,10 +201,10 @@ export class CanvasOverlay extends Layer { } _animateZoom(e: ZoomAnimEvent): void { - const { _map, canvas } = this; - const scale = _map.getZoomScale(e.zoom, _map.getZoom()); + const { map, canvas } = this; + const scale = map.getZoomScale(e.zoom, map.getZoom()); const offset = this._unclampedLatLngBoundsToNewLayerBounds( - _map.getBounds(), + map.getBounds(), e.zoom, e.center ).min; @@ -210,15 +214,15 @@ export class CanvasOverlay extends Layer { } _animateZoomNoLayer(e: ZoomAnimEvent): void { - const { _map, canvas } = this; + const { map, canvas } = this; if (canvas) { - const scale = _map.getZoomScale(e.zoom, _map.getZoom()); - const offset = _map + const scale = map.getZoomScale(e.zoom, map.getZoom()); + const offset = map // @ts-expect-error experimental ._getCenterOffset(e.center) ._multiplyBy(-scale) // @ts-expect-error experimental - .subtract(_map._getMapPanePos()); + .subtract(map._getMapPanePos()); DomUtil.setTransform(canvas, offset, scale); } } @@ -226,7 +230,7 @@ export class CanvasOverlay extends Layer { _unclampedProject(latlng: LatLng, zoom: number): Point { // imported partly from https://github.com/Leaflet/Leaflet/blob/1ae785b73092fdb4b97e30f8789345e9f7c7c912/src/geo/projection/Projection.SphericalMercator.js#L21 // used because they clamp the latitude - const { crs } = this._map.options; + const { crs } = this.map.options; // @ts-expect-error experimental const { R } = crs.projection; const d = Math.PI / 180; @@ -249,7 +253,7 @@ export class CanvasOverlay extends Layer { // imported party from https://github.com/Leaflet/Leaflet/blob/84bc05bbb6e4acc41e6f89ff7421dd7c6520d256/src/map/Map.js#L1500 // used because it uses crs.projection.project, which clamp the latitude // @ts-expect-error experimental - const topLeft = this._map._getNewPixelOrigin(center, zoom); + const topLeft = this.map._getNewPixelOrigin(center, zoom); return new Bounds([ this._unclampedProject(latLngBounds.getSouthWest(), zoom).subtract( topLeft diff --git a/src/index.ts b/src/index.ts index 65e7dc8..a8fc471 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,6 +29,7 @@ export class Glify { longitudeKey = 1; latitudeKey = 0; clickSetupMaps: Map[] = []; + contextMenuSetupMaps: Map[] = []; hoverSetupMaps: Map[] = []; shader = shader; @@ -63,6 +64,7 @@ export class Glify { points(settings: Partial): Points { const points = new this.Points({ setupClick: this.setupClick.bind(this), + setupContextMenu: this.setupContextMenu.bind(this), setupHover: this.setupHover.bind(this), latitudeKey: glify.latitudeKey, longitudeKey: glify.longitudeKey, @@ -81,6 +83,7 @@ export class Glify { lines(settings: Partial): Lines { const lines = new this.Lines({ setupClick: this.setupClick.bind(this), + setupContextMenu: this.setupContextMenu.bind(this), setupHover: this.setupHover.bind(this), latitudeKey: this.latitudeKey, longitudeKey: this.longitudeKey, @@ -99,6 +102,7 @@ export class Glify { shapes(settings: Partial): Shapes { const shapes = new this.Shapes({ setupClick: this.setupClick.bind(this), + setupContextMenu: this.setupContextMenu.bind(this), setupHover: this.setupHover.bind(this), latitudeKey: this.latitudeKey, longitudeKey: this.longitudeKey, @@ -130,6 +134,23 @@ export class Glify { }); } + setupContextMenu(map: Map): void { + if (this.contextMenuSetupMaps.includes(map)) return; + this.clickSetupMaps.push(map); + map.on("contextmenu", (e: LeafletMouseEvent) => { + e.originalEvent.preventDefault(); // Prevent the default context menu from showing + let hit; + hit = this.Points.tryContextMenu(e, map, this.pointsInstances); + if (hit !== undefined) return hit; + + hit = this.Lines.tryContextMenu(e, map, this.linesInstances); + if (hit !== undefined) return hit; + + hit = this.Shapes.tryContextMenu(e, map, this.shapesInstances); + if (hit !== undefined) return hit; + }); + } + setupHover(map: Map, hoverWait?: number, immediate?: false): void { if (this.hoverSetupMaps.includes(map)) return; this.hoverSetupMaps.push(map); diff --git a/src/lines.ts b/src/lines.ts index b13eec6..5bf67e1 100644 --- a/src/lines.ts +++ b/src/lines.ts @@ -142,6 +142,7 @@ export class Lines extends BaseGlLayer { let weightFn: WeightCallback | null = null; let chosenColor: color.IColor; let featureIndex = 0; + let coordinates: Position[] | Position[][]; if (typeof color === "function") { colorFn = color; @@ -166,20 +167,29 @@ export class Lines extends BaseGlLayer { ? weightFn(featureIndex, feature) : (weight as number); - const featureVertices = new LineFeatureVertices({ - project, - latitudeKey, - longitudeKey, - color: chosenColor, - weight: chosenWeight, - opacity, - mapCenterPixels, - }); - - featureVertices.fillFromCoordinates(feature.geometry.coordinates); - vertices.push(featureVertices); - if (eachVertex) { - eachVertex(featureVertices); + //coorinates Array Structure depends on whether feature is multipart or not. + //Multi: [ [[],[],[]...], [[],[],[]...], [[],[],[]...]... ], Single: [ [[],[],[]...] ] + //Wrap Single Array to treat two types with same method + coordinates = (feature.geometry || feature).coordinates; + if (feature.geometry.type !== "MultiLineString") { + coordinates = [coordinates as Position[]]; + } + + for (const coordinate of coordinates) { + const featureVertices = new LineFeatureVertices({ + project, + latitudeKey, + longitudeKey, + color: chosenColor, + weight: chosenWeight, + opacity, + mapCenterPixels, + }); + featureVertices.fillFromCoordinates(coordinate as Position[]); + vertices.push(featureVertices); + if (eachVertex) { + eachVertex(featureVertices); + } } } @@ -260,7 +270,10 @@ export class Lines extends BaseGlLayer { gl.vertexAttrib1f(aPointSize, pointSize); mapMatrix.setSize(canvas.width, canvas.height).scaleTo(scale); if (zoom > 18) { - mapMatrix.translateTo((-offset.x + mapCenterPixels.x), (-offset.y + mapCenterPixels.y)); + mapMatrix.translateTo( + -offset.x + mapCenterPixels.x, + -offset.y + mapCenterPixels.y + ); // -- attach matrix value to 'mapMatrix' uniform in shader gl.uniformMatrix4fv(matrix, false, mapMatrix.array); @@ -271,8 +284,8 @@ export class Lines extends BaseGlLayer { for (let xOffset = -weight; xOffset <= weight; xOffset += 0.5) { // -- set base matrix to translate canvas pixel coordinates -> webgl coordinates mapMatrix.translateTo( - (-offset.x + mapCenterPixels.x) + xOffset / scale, - (-offset.y + mapCenterPixels.y) + yOffset / scale + -offset.x + mapCenterPixels.x + xOffset / scale, + -offset.y + mapCenterPixels.y + yOffset / scale ); // -- attach matrix value to 'mapMatrix' uniform in shader gl.uniformMatrix4fv(matrix, false, mapMatrix.array); @@ -300,8 +313,8 @@ export class Lines extends BaseGlLayer { ) { // -- set base matrix to translate canvas pixel coordinates -> webgl coordinates mapMatrix.translateTo( - (-offset.x + mapCenterPixels.x) + xOffset / scale, - (-offset.y + mapCenterPixels.y) + yOffset / scale + -offset.x + mapCenterPixels.x + xOffset / scale, + -offset.y + mapCenterPixels.y + yOffset / scale ); // -- attach matrix value to 'mapMatrix' uniform in shader gl.uniformMatrix4fv(this.matrix, false, mapMatrix.array); @@ -325,14 +338,8 @@ export class Lines extends BaseGlLayer { let foundLines: Lines | null = null; instances.forEach((instance: Lines): void => { - const { - latitudeKey, - longitudeKey, - sensitivity, - weight, - scale, - active, - } = instance; + const { latitudeKey, longitudeKey, sensitivity, weight, scale, active } = + instance; if (!active) return; if (instance.map !== map) return; function checkClick( @@ -404,6 +411,89 @@ export class Lines extends BaseGlLayer { } } + // attempts to click the top-most Lines instance + static tryContextMenu( + e: LeafletMouseEvent, + map: Map, + instances: Lines[] + ): boolean | undefined { + let foundFeature: Feature | null = null; + let foundLines: Lines | null = null; + + instances.forEach((instance: Lines): void => { + const { latitudeKey, longitudeKey, sensitivity, weight, scale, active } = + instance; + if (!active) return; + if (instance.map !== map) return; + function checkContextMenu( + coordinate: Position, + prevCoordinate: Position, + feature: Feature, + chosenWeight: number + ): void { + const distance = latLngDistance( + e.latlng.lng, + e.latlng.lat, + prevCoordinate[longitudeKey], + prevCoordinate[latitudeKey], + coordinate[longitudeKey], + coordinate[latitudeKey] + ); + if (distance <= sensitivity + chosenWeight / scale) { + foundFeature = feature; + foundLines = instance; + } + } + instance.data.features.forEach( + (feature: Feature, i: number): void => { + const chosenWeight = + typeof weight === "function" ? weight(i, feature) : weight; + const { coordinates, type } = feature.geometry; + if (type === "LineString") { + for (let i = 1; i < coordinates.length; i++) { + checkContextMenu( + coordinates[i] as Position, + coordinates[i - 1] as Position, + feature, + chosenWeight + ); + } + } else if (type === "MultiLineString") { + // TODO: Unit test + for (let i = 0; i < coordinates.length; i++) { + const coordinate = coordinates[i]; + for (let j = 0; j < coordinate.length; j++) { + if (j === 0 && i > 0) { + const prevCoordinates = coordinates[i - 1]; + const lastPositions = + prevCoordinates[prevCoordinates.length - 1]; + checkContextMenu( + lastPositions as Position, + coordinates[i][j] as Position, + feature, + chosenWeight + ); + } else if (j > 0) { + checkContextMenu( + coordinates[i][j] as Position, + coordinates[i][j - 1] as Position, + feature, + chosenWeight + ); + } + } + } + } + } + ); + }); + + if (foundLines && foundFeature) { + const result = (foundLines as Lines).contextMenu(e, foundFeature); + return result !== undefined ? result : undefined; + } + } + hoveringFeatures: Array> = []; // hovers all touching Lines instances static tryHover( @@ -450,9 +540,8 @@ export class Lines extends BaseGlLayer { if (!instance.active) return; if (map !== instance.map) return; const oldHoveredFeatures = hoveringFeatures; - const newHoveredFeatures: Array< - Feature - > = []; + const newHoveredFeatures: Array> = + []; instance.hoveringFeatures = newHoveredFeatures; // Check if e.latlng is inside the bbox of the features const bounds = geoJSON(data.features).getBounds(); diff --git a/src/points.ts b/src/points.ts index 9d212a6..a932e10 100644 --- a/src/points.ts +++ b/src/points.ts @@ -156,9 +156,8 @@ export class Points extends BaseGlLayer { mapCenterPixels, } = this; const { eachVertex } = settings; - let colorFn: - | ((i: number, latLng: LatLng | any) => Color.IColor) - | null = null; + let colorFn: ((i: number, latLng: LatLng | any) => Color.IColor) | null = + null; let chosenColor: Color.IColor; let chosenSize: number; let sizeFn; @@ -312,7 +311,15 @@ export class Points extends BaseGlLayer { drawOnCanvas(e: ICanvasOverlayDrawEvent): this { if (!this.gl) return this; - const { gl, canvas, mapMatrix, matrix, map, allLatLngLookup, mapCenterPixels } = this; + const { + gl, + canvas, + mapMatrix, + matrix, + map, + allLatLngLookup, + mapCenterPixels, + } = this; const { offset } = e; const zoom = map.getZoom(); const scale = Math.pow(2, zoom); @@ -320,7 +327,10 @@ export class Points extends BaseGlLayer { mapMatrix .setSize(canvas.width, canvas.height) .scaleTo(scale) - .translateTo(-offset.x + mapCenterPixels.x, -offset.y + mapCenterPixels.y); + .translateTo( + -offset.x + mapCenterPixels.x, + -offset.y + mapCenterPixels.y + ); gl.clear(gl.COLOR_BUFFER_BIT); gl.viewport(0, 0, canvas.width, canvas.height); @@ -423,6 +433,50 @@ export class Points extends BaseGlLayer { } } + // attempts to click the top-most Points instance + static tryContextMenu( + e: LeafletMouseEvent, + map: Map, + instances: Points[] + ): boolean | undefined { + const closestFromEach: IPointVertex[] = []; + const instancesLookup: { [key: string]: Points } = {}; + let result; + let settings: Partial | null = null; + let pointLookup: IPointVertex | null; + + instances.forEach((_instance: Points) => { + settings = _instance.settings; + if (!_instance.active) return; + if (_instance.map !== map) return; + + pointLookup = _instance.lookup(e.latlng); + if (pointLookup === null) return; + instancesLookup[pointLookup.key] = _instance; + closestFromEach.push(pointLookup); + }); + + if (closestFromEach.length < 1) return; + if (!settings) return; + + const found = this.closest(e.latlng, closestFromEach, map); + + if (!found) return; + + const instance = instancesLookup[found.key]; + if (!instance) return; + const { sensitivity } = instance; + const foundLatLng = found.latLng; + const xy = map.latLngToLayerPoint(foundLatLng); + + if ( + pixelInCircle(xy, e.layerPoint, found.chosenSize * (sensitivity ?? 1)) + ) { + result = instance.contextMenu(e, found.feature || found.latLng); + return result !== undefined ? result : true; + } + } + hoveringFeatures: Array> = []; // hovers all touching Points instances static tryHover( diff --git a/src/shapes.ts b/src/shapes.ts index f8ba06b..d23db7e 100644 --- a/src/shapes.ts +++ b/src/shapes.ts @@ -205,61 +205,79 @@ export class Shapes extends BaseGlLayer { const alpha = typeof chosenColor.a === "number" ? chosenColor.a : opacity; coordinates = (feature.geometry || feature).coordinates; - if (!Array.isArray(coordinates[0])) { - continue; - } - flat = earcut.flatten(coordinates); - indices = earcut(flat.vertices, flat.holes, flat.dimensions); - dim = coordinates[0][0].length; - const { longitudeKey, latitudeKey } = this; - for (let i = 0, iMax = indices.length; i < iMax; i++) { - index = indices[i]; - if (typeof flat.vertices[0] === "number") { - triangles.push( - flat.vertices[index * dim + longitudeKey], - flat.vertices[index * dim + latitudeKey] - ); - } else { - throw new Error("unhandled polygon"); - } - } - for (let i = 0, iMax = triangles.length; i < iMax; i) { - pixel = map.project(new LatLng(triangles[i++], triangles[i++]), 0); - vertices.push( - pixel.x - mapCenterPixels.x, - pixel.y - mapCenterPixels.y, - chosenColor.r, - chosenColor.g, - chosenColor.b, - alpha - ); + //coordinates Array Structure depends on whether feature is multipart or not. + //Multi: [ [[],[],[]...], [[],[],[]...], [[],[],[]...]... ], Single: [ [[],[],[]...] ] + //Wrap Single Array to treat two types with same method + if (feature.geometry.type !== "MultiPolygon") { + coordinates = [coordinates]; + } + if ( + coordinates.length == 0 || + !Array.isArray(coordinates[0]) || + !Array.isArray(coordinates[0][0]) + ) { + continue; } - if (border) { - const lines = []; - let holeIndex = 0; - for (let i = 1, iMax = flat.vertices.length - 2; i < iMax; i = i + 2) { - // Skip draw between hole and non-hole vertext - if(((i + 1) / 2) !== flat.holes[holeIndex]) { - lines.push(flat.vertices[i], flat.vertices[i - 1]); - lines.push(flat.vertices[i + 2], flat.vertices[i + 1]); + for (let num in coordinates) { + flat = earcut.flatten(coordinates[num]); + indices = earcut(flat.vertices, flat.holes, flat.dimensions); + dim = coordinates[num][0][0].length; + const { longitudeKey, latitudeKey } = this; + for (let i = 0, iMax = indices.length; i < iMax; i++) { + index = indices[i]; + if (typeof flat.vertices[0] === "number") { + triangles.push( + flat.vertices[index * dim + longitudeKey], + flat.vertices[index * dim + latitudeKey] + ); } else { - holeIndex++; + throw new Error("unhandled polygon"); } } - for (let i = 0, iMax = lines.length; i < iMax; i) { - pixel = latLonToPixel(lines[i++], lines[i++]); - vertexLines.push( + for (let i = 0, iMax = triangles.length; i < iMax; i) { + pixel = map.project(new LatLng(triangles[i++], triangles[i++]), 0); + vertices.push( pixel.x - mapCenterPixels.x, pixel.y - mapCenterPixels.y, chosenColor.r, chosenColor.g, chosenColor.b, - borderOpacity + alpha ); } + + if (border) { + const lines = []; + let holeIndex = 0; + for ( + let i = 1, iMax = flat.vertices.length - 2; + i < iMax; + i = i + 2 + ) { + // Skip draw between hole and non-hole vertext + if ((i + 1) / 2 !== flat.holes[holeIndex]) { + lines.push(flat.vertices[i], flat.vertices[i - 1]); + lines.push(flat.vertices[i + 2], flat.vertices[i + 1]); + } else { + holeIndex++; + } + } + + for (let i = 0, iMax = lines.length; i < iMax; i) { + pixel = latLonToPixel(lines[i++], lines[i++]); + vertexLines.push( + pixel.x - mapCenterPixels.x, + pixel.y - mapCenterPixels.y, + chosenColor.r, + chosenColor.g, + chosenColor.b, + borderOpacity + ); + } + } } } @@ -280,12 +298,23 @@ export class Shapes extends BaseGlLayer { if (!this.gl) return this; const { scale, offset, canvas } = e; - const { mapMatrix, gl, vertices, settings, vertexLines, border, mapCenterPixels } = this; + const { + mapMatrix, + gl, + vertices, + settings, + vertexLines, + border, + mapCenterPixels, + } = this; // -- set base matrix to translate canvas pixel coordinates -> webgl coordinates mapMatrix .setSize(canvas.width, canvas.height) .scaleTo(scale) - .translateTo(-offset.x + mapCenterPixels.x, -offset.y + mapCenterPixels.y); + .translateTo( + -offset.x + mapCenterPixels.x, + -offset.y + mapCenterPixels.y + ); gl.clear(gl.COLOR_BUFFER_BIT); gl.viewport(0, 0, canvas.width, canvas.height); @@ -360,7 +389,39 @@ export class Shapes extends BaseGlLayer { } } - hoveringFeatures: Array | Feature> = []; + // attempts to click the top-most Shapes instance + static tryContextMenu( + e: LeafletMouseEvent, + map: Map, + instances: Shapes[] + ): boolean | undefined { + let foundPolygon: Feature | null = null; + let foundShapes: Shapes | null = null; + instances.forEach(function (_instance: Shapes): void { + if (!_instance.active) return; + if (_instance.map !== map) return; + if (!_instance.polygonLookup) return; + + const polygon = _instance.polygonLookup.search( + e.latlng.lng, + e.latlng.lat + ); + if (polygon) { + foundShapes = _instance; + foundPolygon = polygon; + } + }); + + if (foundShapes && foundPolygon) { + const result = (foundShapes as Shapes).contextMenu(e, foundPolygon); + return result !== undefined ? result : undefined; + } + } + + hoveringFeatures: Array< + | Feature + | Feature + > = []; // hovers all touching Shapes instances static tryHover( e: LeafletMouseEvent, @@ -374,7 +435,10 @@ export class Shapes extends BaseGlLayer { if (instance.map !== map) return; if (!instance.polygonLookup) return; const oldHoveredFeatures = hoveringFeatures; - const newHoveredFeatures: Array | Feature> = []; + const newHoveredFeatures: Array< + | Feature + | Feature + > = []; instance.hoveringFeatures = newHoveredFeatures; const feature = instance.polygonLookup.search(e.latlng.lng, e.latlng.lat); diff --git a/src/tests/base-gl-layer.test.ts b/src/tests/base-gl-layer.test.ts index 3c4d440..9e702b4 100644 --- a/src/tests/base-gl-layer.test.ts +++ b/src/tests/base-gl-layer.test.ts @@ -8,8 +8,6 @@ import { import { ICanvasOverlayDrawEvent } from "../canvas-overlay"; import { LatLng, LatLngBounds, LeafletMouseEvent, Map, Point } from "leaflet"; -jest.mock("../canvas-overlay"); - describe("BaseGlLayer", () => { interface ITestLayerSettings extends IBaseGlLayerSettings {} class TestLayer extends BaseGlLayer { @@ -406,6 +404,18 @@ describe("BaseGlLayer", () => { expect(setupClick).toHaveBeenCalledWith(layer.map); }); }); + describe("when settings.contextMenu and settings.setupContextMenu are truth", () => { + it("calls settings.setupContextMenu with this.map", () => { + const contextMenu = () => {}; + const setupContextMenu = jest.fn(); + const layer = getGlLayer({ + contextMenu, + setupContextMenu, + }); + expect(layer.setup()).toBe(layer); + expect(setupContextMenu).toHaveBeenCalledWith(layer.map); + }); + }); describe("when settings.hover and settings.setupHover are truthy", () => { it("calls settings.setupHover with this.map and settings.hoverWait", () => { const hover: EventCallback = () => {}; diff --git a/src/tests/index.test.ts b/src/tests/index.test.ts index 54ae480..7adaef1 100644 --- a/src/tests/index.test.ts +++ b/src/tests/index.test.ts @@ -5,7 +5,6 @@ import { IShapesSettings, Shapes } from "../shapes"; import { LatLng, LeafletMouseEvent, Map, Point } from "leaflet"; import { FeatureCollection, LineString, MultiPolygon } from "geojson"; -jest.mock("../canvas-overlay"); type mouseEventFunction = (e: LeafletMouseEvent) => void; jest.mock("../utils", () => { return { @@ -91,6 +90,7 @@ describe("glify", () => { size, data, setupClick: points.settings.setupClick, + setupContextMenu: points.settings.setupContextMenu, setupHover: points.settings.setupHover, latitudeKey: 1, longitudeKey: 1, @@ -133,6 +133,7 @@ describe("glify", () => { weight, data, setupClick: lines.settings.setupClick, + setupContextMenu: lines.settings.setupContextMenu, setupHover: lines.settings.setupHover, latitudeKey: 1, longitudeKey: 1, @@ -172,6 +173,7 @@ describe("glify", () => { map, data, setupClick: shapes.settings.setupClick, + setupContextMenu: shapes.settings.setupContextMenu, setupHover: shapes.settings.setupHover, latitudeKey: 2, longitudeKey: 3, @@ -269,10 +271,10 @@ describe("glify", () => { }); describe("when Points.tryClick returns a value", () => { beforeEach(() => { - PointsSpy.tryCLickResult = false; + PointsSpy.tryClickResult = false; }); afterEach(() => { - delete PointsSpy.tryCLickResult; + delete PointsSpy.tryClickResult; }); it("returns early", () => { map.fireEvent("click", { @@ -320,10 +322,10 @@ describe("glify", () => { }); describe("when Lines.tryClick returns a value", () => { beforeEach(() => { - LinesSpy.tryCLickResult = false; + LinesSpy.tryClickResult = false; }); afterEach(() => { - delete LinesSpy.tryCLickResult; + delete LinesSpy.tryClickResult; }); it("returns early", () => { map.fireEvent("click", { @@ -345,6 +347,176 @@ describe("glify", () => { }); }); }); + describe.skip("setupContextMenu", () => { + describe("when this.contextMenuSetupMaps does not include map", () => { + it("pushes it to glify.maps", () => { + const glify = new Glify(); + const element = document.createElement("div"); + const map = new Map(element); + expect(glify.contextMenuSetupMaps.length).toBe(0); + glify.setupContextMenu(map); + expect(glify.contextMenuSetupMaps.length).toBe(1); + }); + }); + describe("when this.contextMenuSetupMaps includes map", () => { + it("returns early", () => { + const glify = new Glify(); + const element = document.createElement("div"); + const map = new Map(element); + glify.contextMenuSetupMaps.push(map); + expect(glify.contextMenuSetupMaps.length).toBe(1); + glify.setupContextMenu(map); + expect(glify.contextMenuSetupMaps.length).toBe(1); + }); + }); + it('calls map.on("contextMenu") correctly', () => { + const glify = new Glify(); + const element = document.createElement("div"); + const map = new Map(element); + jest.spyOn(map, "on"); + glify.setupContextMenu(map); + expect(map.on).toHaveBeenCalled(); + }); + describe("when a contextMenu occurs", () => { + let glify: Glify; + let map: Map; + let element: HTMLElement; + let pointsTryContextMenuSpy: jest.SpyInstance; + let linesTryContextMenuSpy: jest.SpyInstance; + let shapesTryContextMenuSpy: jest.SpyInstance; + let latlng: LatLng; + let layerPoint: Point; + let containerPoint: Point; + + beforeEach(() => { + glify = new Glify(); + glify.Points = PointsSpy; + glify.Lines = LinesSpy; + glify.Shapes = ShapesSpy; + pointsTryContextMenuSpy = jest.spyOn(glify.Points, "tryContextMenu"); + linesTryContextMenuSpy = jest.spyOn(glify.Lines, "tryContextMenu"); + shapesTryContextMenuSpy = jest.spyOn(glify.Shapes, "tryContextMenu"); + element = document.createElement("div"); + map = new Map(element); + glify.setupContextMenu(map); + map.setView([10, 10], 7); + latlng = new LatLng(1, 1); + layerPoint = map.latLngToLayerPoint(latlng); + containerPoint = map.latLngToContainerPoint(latlng); + }); + afterEach(() => { + pointsTryContextMenuSpy.mockRestore(); + linesTryContextMenuSpy.mockRestore(); + shapesTryContextMenuSpy.mockRestore(); + }); + describe("calling Points.tryContextMenu", () => { + describe("when Points.tryContextMenu returns undefined", () => { + it("continues on to Lines", () => { + //TODO: TypeError: Cannot read properties of undefined (reading 'preventDefault') + map.fireEvent("contextmenu", { + latlng, + layerPoint, + containerPoint, + }); + expect(pointsTryContextMenuSpy.mock.calls[0][0]).toEqual({ + containerPoint, + latlng, + layerPoint, + sourceTarget: map, + target: map, + type: "contextmenu", + }); + expect(linesTryContextMenuSpy.mock.calls[0][0]).toEqual({ + containerPoint, + latlng, + layerPoint, + sourceTarget: map, + target: map, + type: "contextmenu", + }); + }); + }); + describe("when Points.tryContextMenu returns a value", () => { + beforeEach(() => { + PointsSpy.tryContextMenuResult = false; + }); + afterEach(() => { + delete PointsSpy.tryContextMenuResult; + }); + it("returns early", () => { + //TODO: TypeError: Cannot read properties of undefined (reading 'preventDefault') + map.fireEvent("contextmenu", { + latlng, + layerPoint, + containerPoint, + }); + expect(pointsTryContextMenuSpy.mock.calls[0][0]).toEqual({ + containerPoint, + latlng, + layerPoint, + sourceTarget: map, + target: map, + type: "click", + }); + expect(linesTryContextMenuSpy).not.toHaveBeenCalled(); + }); + }); + }); + describe("calling Lines.tryContextMenu", () => { + describe("when Lines.tryContextMenu returns undefined", () => { + it("continues on to Shapes", () => { + //TODO: TypeError: Cannot read properties of undefined (reading 'preventDefault') + map.fireEvent("contextmenu", { + latlng, + layerPoint, + containerPoint, + }); + expect(linesTryContextMenuSpy.mock.calls[0][0]).toEqual({ + containerPoint, + latlng, + layerPoint, + sourceTarget: map, + target: map, + type: "contextmenu", + }); + expect(shapesTryContextMenuSpy.mock.calls[0][0]).toEqual({ + containerPoint, + latlng, + layerPoint, + sourceTarget: map, + target: map, + type: "contextmenu", + }); + }); + }); + describe("when Lines.tryContextMenu returns a value", () => { + beforeEach(() => { + LinesSpy.tryContextMenuResult = false; + }); + afterEach(() => { + delete LinesSpy.tryContextMenuResult; + }); + it("returns early", () => { + //TODO: TypeError: Cannot read properties of undefined (reading 'preventDefault') + map.fireEvent("contextmenu", { + latlng, + layerPoint, + containerPoint, + }); + expect(linesTryContextMenuSpy.mock.calls[0][0]).toEqual({ + containerPoint, + latlng, + layerPoint, + sourceTarget: map, + target: map, + type: "contextmenu", + }); + expect(shapesTryContextMenuSpy).not.toHaveBeenCalled(); + }); + }); + }); + }); + }); describe("setupHover", () => { describe("when this.clickSetupMaps does not include map", () => { it("pushes it to glify.maps", () => { @@ -433,13 +605,23 @@ describe("glify", () => { }); class PointsSpy extends Points { - static tryCLickResult: boolean | undefined; + static tryClickResult: boolean | undefined; + static tryContextMenuResult: boolean | undefined; + static tryClick( e: LeafletMouseEvent, map: Map, instances: Points[] ): boolean | undefined { - return this.tryCLickResult; + return this.tryClickResult; + } + + static tryContextMenu( + e: LeafletMouseEvent, + map: Map, + instances: Points[] + ): boolean | undefined { + return this.tryContextMenuResult; } static tryHover( @@ -451,13 +633,22 @@ class PointsSpy extends Points { } } class LinesSpy extends Lines { - static tryCLickResult: boolean | undefined; + static tryClickResult: boolean | undefined; + static tryContextMenuResult: boolean | undefined; static tryClick( e: LeafletMouseEvent, map: Map, instances: Lines[] ): boolean | undefined { - return this.tryCLickResult; + return this.tryClickResult; + } + + static tryContextMenu( + e: LeafletMouseEvent, + map: Map, + instances: Lines[] + ): boolean | undefined { + return this.tryContextMenuResult; } static tryHover( @@ -469,13 +660,22 @@ class LinesSpy extends Lines { } } class ShapesSpy extends Shapes { - static tryCLickResult: boolean | undefined; + static tryClickResult: boolean | undefined; + static tryContextMenuResult: boolean | undefined; static tryClick( e: LeafletMouseEvent, map: Map, instances: Shapes[] ): boolean | undefined { - return this.tryCLickResult; + return this.tryClickResult; + } + + static tryContextMenu( + e: LeafletMouseEvent, + map: Map, + instances: Shapes[] + ): boolean | undefined { + return this.tryContextMenuResult; } static tryHover( diff --git a/src/tests/lines.test.ts b/src/tests/lines.test.ts index 4207837..e55d1e7 100644 --- a/src/tests/lines.test.ts +++ b/src/tests/lines.test.ts @@ -4,7 +4,6 @@ import { MapMatrix } from "../map-matrix"; import { ICanvasOverlayDrawEvent } from "../canvas-overlay"; import { ILinesSettings, Lines, WeightCallback } from "../lines"; -jest.mock("../canvas-overlay"); jest.mock("../utils", () => { return { inBounds: () => true, @@ -279,50 +278,10 @@ describe("Lines", () => { jest.spyOn(lines.map, "project").mockReturnValue(new Point(1, 2)); lines.resetVertices(); const expected = [ - 1, - 2, - 3, - 4, - 5, - 6, - 1, - 2, - 3, - 4, - 5, - 6, - 1, - 2, - 3, - 4, - 5, - 6, - 1, - 2, - 3, - 4, - 5, - 6, + 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, ]; const expectedVerticesArray = [ - 1, - 2, - 3, - 4, - 5, - 6, - 1, - 2, - 3, - 4, - 5, - 6, - 1, - 2, - 3, - 4, - 5, - 6, + 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, ]; expect(lines.vertices.length).toBe(1); expect(lines.vertices[0].array).toEqual(expectedVerticesArray); diff --git a/src/tests/points.test.ts b/src/tests/points.test.ts index 8dcdb9c..eeac563 100644 --- a/src/tests/points.test.ts +++ b/src/tests/points.test.ts @@ -3,8 +3,6 @@ import { FeatureCollection, Point as GeoPoint } from "geojson"; import { IPointVertex, IPointsSettings, Points } from "../points"; import { ICanvasOverlayDrawEvent } from "../canvas-overlay"; -jest.mock("../canvas-overlay"); - function getPoints(settings?: Partial): Points { const element = document.createElement("div"); const map = new Map(element); diff --git a/src/tests/shapes.test.ts b/src/tests/shapes.test.ts index 2bc9f4c..10753cc 100644 --- a/src/tests/shapes.test.ts +++ b/src/tests/shapes.test.ts @@ -7,11 +7,9 @@ import earcut from "earcut"; import { IShapesSettings, Shapes } from "../shapes"; import { notProperlyDefined } from "../errors"; -jest.mock("../canvas-overlay"); jest.mock("geojson-flatten", () => { - const realGeojsonFlatten = jest.requireActual( - "geojson-flatten" - ); + const realGeojsonFlatten = + jest.requireActual("geojson-flatten"); return { __esModule: true, default: jest.fn((v) => realGeojsonFlatten(v)),