diff --git a/.gitignore b/.gitignore index ee3a868..180354f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules/ test.js *.svg -dist/ \ No newline at end of file +/index.js +/without-countries.js diff --git a/.prettierrc.json b/.prettierrc.json index 28920ea..2db7aff 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -5,5 +5,5 @@ "singleQuote": true, "arrowParens": "always", "bracketSpacing": true, - "printWidth": 120 + "printWidth": 80 } diff --git a/index.d.ts b/index.d.ts index 0a9d28c..6a8558f 100644 --- a/index.d.ts +++ b/index.d.ts @@ -4,12 +4,15 @@ namespace DottedMapLib { lng: { min: number; max: number }; } - interface Settings { + interface MapSettings { height?: number; width?: number; countries?: string[]; region?: Region; grid?: 'vertical' | 'diagonal'; + } + + interface Settings extends MapSettings { avoidOuterPins?: false | true; } @@ -40,6 +43,8 @@ namespace DottedMapLib { }; } +export const getMapJSON: (settings: DottedMapLib.MapSettings) => string; + export default class DottedMap { constructor(settings: DottedMapLib.Settings); diff --git a/package-lock.json b/package-lock.json index 505e62f..093fc5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { "name": "dotted-map", - "version": "2.0.0", + "version": "2.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "version": "2.0.0", + "version": "2.1.0", "license": "MIT", "dependencies": { "@turf/boolean-point-in-polygon": "^6.0.1", @@ -69,9 +69,9 @@ } }, "node_modules/@types/estree": { - "version": "0.0.46", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.46.tgz", - "integrity": "sha512-laIjwTQaD+5DukBZaygQ79K1Z0jb1bPEMRrkXSLjtCcZm+abyp5YbrqpSLzD42FwWW6gK/aS4NYpJ804nG2brg==", + "version": "0.0.47", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.47.tgz", + "integrity": "sha512-c5ciR06jK8u9BstrmJyO97m+klJrrhCf9u3rLu3DEAJBirxRqSCvDQoYKmxuYwQI5SZChAWu+tq9oVlGRuzPAg==", "dev": true }, "node_modules/@types/json-schema": { @@ -327,14 +327,14 @@ } }, "node_modules/browserslist": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.4.tgz", - "integrity": "sha512-d7rCxYV8I9kj41RH8UKYnvDYCRENUlHRgyXy/Rhr/1BaeLGfiCptEdFE8MIrvGfWbBFNjVYx76SQWvNX1j+/cQ==", + "version": "4.16.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.5.tgz", + "integrity": "sha512-C2HAjrM1AI/djrpAUU/tr4pml1DqLIzJKSLDBXBrNErl9ZCCTXdhwxdJjYc16953+mBWf7Lw+uUJgpgb8cN71A==", "dev": true, "dependencies": { - "caniuse-lite": "^1.0.30001208", + "caniuse-lite": "^1.0.30001214", "colorette": "^1.2.2", - "electron-to-chromium": "^1.3.712", + "electron-to-chromium": "^1.3.719", "escalade": "^3.1.1", "node-releases": "^1.1.71" }, @@ -356,9 +356,9 @@ "dev": true }, "node_modules/caniuse-lite": { - "version": "1.0.30001209", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001209.tgz", - "integrity": "sha512-2Ktt4OeRM7EM/JaOZjuLzPYAIqmbwQMNnYbgooT+icoRGrKOyAxA1xhlnotBD1KArRSPsuJp3TdYcZYrL7qNxA==", + "version": "1.0.30001214", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001214.tgz", + "integrity": "sha512-O2/SCpuaU3eASWVaesQirZv1MSjUNOvmugaD8zNSJqw6Vv5SGwoOpA9LJs3pNPfM745nxqPvfZY3MQKY4AKHYg==", "dev": true }, "node_modules/chrome-trace-event": { @@ -411,15 +411,15 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.3.717", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.717.tgz", - "integrity": "sha512-OfzVPIqD1MkJ7fX+yTl2nKyOE4FReeVfMCzzxQS+Kp43hZYwHwThlGP+EGIZRXJsxCM7dqo8Y65NOX/HP12iXQ==", + "version": "1.3.720", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.720.tgz", + "integrity": "sha512-B6zLTxxaOFP4WZm6DrvgRk8kLFYWNhQ5TrHMC0l5WtkMXhU5UbnvWoTfeEwqOruUSlNMhVLfYak7REX6oC5Yfw==", "dev": true }, "node_modules/enhanced-resolve": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.7.0.tgz", - "integrity": "sha512-6njwt/NsZFUKhM6j9U8hzVyD4E4r0x7NQzhTCbcWOJ0IQjNSAoalWmb0AE51Wn+fwan5qVESWi7t2ToBxs9vrw==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.0.tgz", + "integrity": "sha512-Sl3KRpJA8OpprrtaIswVki3cWPiPKxXuFxJXBp+zNb6s6VwNWwFRUdtmzd2ReUut8n+sCPx7QCtQ7w5wfJhSgQ==", "dev": true, "dependencies": { "graceful-fs": "^4.2.4", @@ -1235,20 +1235,20 @@ } }, "node_modules/webpack": { - "version": "5.33.2", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.33.2.tgz", - "integrity": "sha512-X4b7F1sYBmJx8mlh2B7mV5szEkE0jYNJ2y3akgAP0ERi0vLCG1VvdsIxt8lFd4st6SUy0lf7W0CCQS566MBpJg==", + "version": "5.35.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.35.0.tgz", + "integrity": "sha512-au3gu55yYF/h6NXFr0KZPZAYxS6Nlc595BzYPke8n0CSff5WXcoixtjh5LC/8mXunkRKxhymhXmBY0+kEbR6jg==", "dev": true, "dependencies": { "@types/eslint-scope": "^3.7.0", - "@types/estree": "^0.0.46", + "@types/estree": "^0.0.47", "@webassemblyjs/ast": "1.11.0", "@webassemblyjs/wasm-edit": "1.11.0", "@webassemblyjs/wasm-parser": "1.11.0", "acorn": "^8.0.4", "browserslist": "^4.14.5", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.7.0", + "enhanced-resolve": "^5.8.0", "es-module-lexer": "^0.4.0", "eslint-scope": "^5.1.1", "events": "^3.2.0", @@ -1449,9 +1449,9 @@ } }, "@types/estree": { - "version": "0.0.46", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.46.tgz", - "integrity": "sha512-laIjwTQaD+5DukBZaygQ79K1Z0jb1bPEMRrkXSLjtCcZm+abyp5YbrqpSLzD42FwWW6gK/aS4NYpJ804nG2brg==", + "version": "0.0.47", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.47.tgz", + "integrity": "sha512-c5ciR06jK8u9BstrmJyO97m+klJrrhCf9u3rLu3DEAJBirxRqSCvDQoYKmxuYwQI5SZChAWu+tq9oVlGRuzPAg==", "dev": true }, "@types/json-schema": { @@ -1679,14 +1679,14 @@ "dev": true }, "browserslist": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.4.tgz", - "integrity": "sha512-d7rCxYV8I9kj41RH8UKYnvDYCRENUlHRgyXy/Rhr/1BaeLGfiCptEdFE8MIrvGfWbBFNjVYx76SQWvNX1j+/cQ==", + "version": "4.16.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.5.tgz", + "integrity": "sha512-C2HAjrM1AI/djrpAUU/tr4pml1DqLIzJKSLDBXBrNErl9ZCCTXdhwxdJjYc16953+mBWf7Lw+uUJgpgb8cN71A==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001208", + "caniuse-lite": "^1.0.30001214", "colorette": "^1.2.2", - "electron-to-chromium": "^1.3.712", + "electron-to-chromium": "^1.3.719", "escalade": "^3.1.1", "node-releases": "^1.1.71" } @@ -1698,9 +1698,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001209", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001209.tgz", - "integrity": "sha512-2Ktt4OeRM7EM/JaOZjuLzPYAIqmbwQMNnYbgooT+icoRGrKOyAxA1xhlnotBD1KArRSPsuJp3TdYcZYrL7qNxA==", + "version": "1.0.30001214", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001214.tgz", + "integrity": "sha512-O2/SCpuaU3eASWVaesQirZv1MSjUNOvmugaD8zNSJqw6Vv5SGwoOpA9LJs3pNPfM745nxqPvfZY3MQKY4AKHYg==", "dev": true }, "chrome-trace-event": { @@ -1744,15 +1744,15 @@ } }, "electron-to-chromium": { - "version": "1.3.717", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.717.tgz", - "integrity": "sha512-OfzVPIqD1MkJ7fX+yTl2nKyOE4FReeVfMCzzxQS+Kp43hZYwHwThlGP+EGIZRXJsxCM7dqo8Y65NOX/HP12iXQ==", + "version": "1.3.720", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.720.tgz", + "integrity": "sha512-B6zLTxxaOFP4WZm6DrvgRk8kLFYWNhQ5TrHMC0l5WtkMXhU5UbnvWoTfeEwqOruUSlNMhVLfYak7REX6oC5Yfw==", "dev": true }, "enhanced-resolve": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.7.0.tgz", - "integrity": "sha512-6njwt/NsZFUKhM6j9U8hzVyD4E4r0x7NQzhTCbcWOJ0IQjNSAoalWmb0AE51Wn+fwan5qVESWi7t2ToBxs9vrw==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.0.tgz", + "integrity": "sha512-Sl3KRpJA8OpprrtaIswVki3cWPiPKxXuFxJXBp+zNb6s6VwNWwFRUdtmzd2ReUut8n+sCPx7QCtQ7w5wfJhSgQ==", "dev": true, "requires": { "graceful-fs": "^4.2.4", @@ -2357,20 +2357,20 @@ } }, "webpack": { - "version": "5.33.2", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.33.2.tgz", - "integrity": "sha512-X4b7F1sYBmJx8mlh2B7mV5szEkE0jYNJ2y3akgAP0ERi0vLCG1VvdsIxt8lFd4st6SUy0lf7W0CCQS566MBpJg==", + "version": "5.35.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.35.0.tgz", + "integrity": "sha512-au3gu55yYF/h6NXFr0KZPZAYxS6Nlc595BzYPke8n0CSff5WXcoixtjh5LC/8mXunkRKxhymhXmBY0+kEbR6jg==", "dev": true, "requires": { "@types/eslint-scope": "^3.7.0", - "@types/estree": "^0.0.46", + "@types/estree": "^0.0.47", "@webassemblyjs/ast": "1.11.0", "@webassemblyjs/wasm-edit": "1.11.0", "@webassemblyjs/wasm-parser": "1.11.0", "acorn": "^8.0.4", "browserslist": "^4.14.5", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.7.0", + "enhanced-resolve": "^5.8.0", "es-module-lexer": "^0.4.0", "eslint-scope": "^5.1.1", "events": "^3.2.0", diff --git a/package.json b/package.json index 5d4e478..182ef67 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,13 @@ { "name": "dotted-map", - "version": "2.0.0", + "version": "2.1.1", "description": "Create a SVG map filled with dots for the world or countries", - "main": "dist/dotted-map.js", - "module": "src/index.js", + "main": "./index.js", + "exports": { + ".": "./index.js", + "./without-countries": "./without-countries.js" + }, + "module": "./src/with-countries.js", "scripts": { "build": "webpack" }, diff --git a/src/index-with-countries.js b/src/index-with-countries.js deleted file mode 100644 index e69de29..0000000 diff --git a/src/index.js b/src/index.js deleted file mode 100644 index 6263f55..0000000 --- a/src/index.js +++ /dev/null @@ -1,186 +0,0 @@ -import proj4 from 'proj4'; -import inside from '@turf/boolean-point-in-polygon'; -import geojsonWorld from './countries.geo.json'; - -// TODO -// [ ] Handle prefill cache → -// [ ] Method to export a cache -// [ ] Method to import a cache -// [ ] Make two exports: one with countries, one without (reduce bundle size) - -const geojsonByCountry = geojsonWorld.features.reduce((countries, feature) => { - countries[feature.id] = feature; - return countries; -}, {}); - -const geojsonToMultiPolygons = (geojson) => { - const coordinates = geojson.features.reduce( - (poly, feature) => - poly.concat(feature.geometry.type === 'Polygon' ? [feature.geometry.coordinates] : feature.geometry.coordinates), - [], - ); - return { type: 'Feature', geometry: { type: 'MultiPolygon', coordinates } }; -}; - -const CACHE = {}; - -const DEFAULT_WORLD_REGION = { - lat: { min: -56, max: 71 }, - lng: { min: -179, max: 179 }, -}; - -const computeGeojsonBox = (geojson) => { - const { type, features, geometry, coordinates } = geojson; - if (type === 'FeatureCollection') { - const boxes = features.map(computeGeojsonBox); - return { - lat: { - min: Math.min(...boxes.map((box) => box.lat.min)), - max: Math.max(...boxes.map((box) => box.lat.max)), - }, - lng: { - min: Math.min(...boxes.map((box) => box.lng.min)), - max: Math.max(...boxes.map((box) => box.lng.max)), - }, - }; - } else if (type == 'Feature') { - return computeGeojsonBox(geometry); - } else if (type === 'MultiPolygon') { - return computeGeojsonBox({ type: 'Polygon', coordinates: coordinates.flat() }); - } else if (type == 'Polygon') { - const coords = coordinates.flat(); - const latitudes = coords.map(([_lng, lat]) => lat); - const longitudes = coords.map(([lng, _lat]) => lng); - - return { - lat: { - min: Math.min(...latitudes), - max: Math.max(...latitudes), - }, - lng: { - min: Math.min(...longitudes), - max: Math.max(...longitudes), - }, - }; - } else { - throw new Error(`Unknown geojson type ${type}`); - } -}; - -function DottedMap({ height = 0, width = 0, countries = [], region, grid = 'vertical', avoidOuterPins = false }) { - if (height <= 0 && width <= 0) { - throw new Error('height or width is required'); - } - - let geojson = geojsonWorld; - if (countries.length > 0) { - geojson = { - type: 'FeatureCollection', - features: countries.map((country) => geojsonByCountry[country]), - }; - if (!region) { - region = computeGeojsonBox(geojson); - } - } else if (!region) { - region = DEFAULT_WORLD_REGION; - } - - const poly = geojsonToMultiPolygons(geojson); - - const cacheKey = [JSON.stringify(region), grid, height, width, JSON.stringify(countries)].join(' '); - - const [X_MIN, Y_MIN] = proj4(proj4.defs('GOOGLE'), [region.lng.min, region.lat.min]); - const [X_MAX, Y_MAX] = proj4(proj4.defs('GOOGLE'), [region.lng.max, region.lat.max]); - const X_RANGE = X_MAX - X_MIN; - const Y_RANGE = Y_MAX - Y_MIN; - - if (width <= 0) { - width = Math.round((height * X_RANGE) / Y_RANGE); - } else if (height <= 0) { - height = Math.round((width * Y_RANGE) / X_RANGE); - } - - const points = { ...CACHE[cacheKey] } || {}; - const ystep = grid === 'diagonal' ? Math.sqrt(3) / 2 : 1; - - if (!CACHE[cacheKey]) { - for (let y = 0; y * ystep < height; y += 1) { - for (let x = 0; x < width; x += 1) { - const localx = y % 2 === 0 && grid === 'diagonal' ? x + 0.5 : x; - const localy = y * ystep; - - const pointGoogle = [(localx / width) * X_RANGE + X_MIN, Y_MAX - (localy / height) * Y_RANGE]; - const wgs84Point = proj4(proj4.defs('GOOGLE'), proj4.defs('WGS84'), pointGoogle); - - if (inside(wgs84Point, poly)) { - points[[x, y].join(';')] = { x: localx, y: localy }; - } - } - } - - CACHE[cacheKey] = { ...points }; - } - - return { - addPin({ lat, lng, data, svgOptions }) { - const [googleX, googleY] = proj4(proj4.defs('GOOGLE'), [lng, lat]); - if (avoidOuterPins) { - const wgs84Point = proj4(proj4.defs('GOOGLE'), proj4.defs('WGS84'), [googleX, googleY]); - if (!inside(wgs84Point, poly)) return; - } - let [rawX, rawY] = [(width * (googleX - X_MIN)) / X_RANGE, (height * (Y_MAX - googleY)) / Y_RANGE]; - const y = Math.round(rawY / ystep); - if (y % 2 === 0 && grid === 'diagonal') { - rawX -= 0.5; - } - const x = Math.round(rawX); - let [localx, localy] = [x, Math.round(y) * ystep]; - if (y % 2 === 0 && grid === 'diagonal') { - localx += 0.5; - } - - const point = { x: localx, y: localy, data, svgOptions }; - - points[[x, y].join(';')] = point; - - return point; - }, - getPoints() { - return Object.values(points); - }, - getSVG({ shape = 'circle', color = 'current', backgroundColor = 'transparent', radius = 0.5 }) { - const getPoint = ({ x, y, svgOptions = {} }) => { - const pointRadius = svgOptions.radius || radius; - if (shape === 'circle') { - return ``; - } else if (shape === 'hexagon') { - const sqrt3radius = Math.sqrt(3) * pointRadius; - - const polyPoints = [ - [x + sqrt3radius, y - pointRadius], - [x + sqrt3radius, y + pointRadius], - [x, y + 2 * pointRadius], - [x - sqrt3radius, y + pointRadius], - [x - sqrt3radius, y - pointRadius], - [x, y - 2 * pointRadius], - ]; - - return ``; - } - }; - - return ` - ${Object.values(points).map(getPoint).join('\n')} - `; - }, - image: { - region, - width, - height, - }, - }; -} - -export default DottedMap; diff --git a/src/with-countries.js b/src/with-countries.js new file mode 100644 index 0000000..c0a5032 --- /dev/null +++ b/src/with-countries.js @@ -0,0 +1,180 @@ +import proj4 from 'proj4'; +import inside from '@turf/boolean-point-in-polygon'; +import geojsonWorld from './countries.geo.json'; +import DottedMapWithoutCountries from './without-countries'; + +const geojsonByCountry = geojsonWorld.features.reduce((countries, feature) => { + countries[feature.id] = feature; + return countries; +}, {}); + +const geojsonToMultiPolygons = (geojson) => { + const coordinates = geojson.features.reduce( + (poly, feature) => + poly.concat( + feature.geometry.type === 'Polygon' + ? [feature.geometry.coordinates] + : feature.geometry.coordinates, + ), + [], + ); + return { type: 'Feature', geometry: { type: 'MultiPolygon', coordinates } }; +}; + +const CACHE = {}; + +const DEFAULT_WORLD_REGION = { + lat: { min: -56, max: 71 }, + lng: { min: -179, max: 179 }, +}; + +const computeGeojsonBox = (geojson) => { + const { type, features, geometry, coordinates } = geojson; + if (type === 'FeatureCollection') { + const boxes = features.map(computeGeojsonBox); + return { + lat: { + min: Math.min(...boxes.map((box) => box.lat.min)), + max: Math.max(...boxes.map((box) => box.lat.max)), + }, + lng: { + min: Math.min(...boxes.map((box) => box.lng.min)), + max: Math.max(...boxes.map((box) => box.lng.max)), + }, + }; + } else if (type == 'Feature') { + return computeGeojsonBox(geometry); + } else if (type === 'MultiPolygon') { + return computeGeojsonBox({ + type: 'Polygon', + coordinates: coordinates.flat(), + }); + } else if (type == 'Polygon') { + const coords = coordinates.flat(); + const latitudes = coords.map(([_lng, lat]) => lat); + const longitudes = coords.map(([lng, _lat]) => lng); + + return { + lat: { + min: Math.min(...latitudes), + max: Math.max(...latitudes), + }, + lng: { + min: Math.min(...longitudes), + max: Math.max(...longitudes), + }, + }; + } else { + throw new Error(`Unknown geojson type ${type}`); + } +}; + +const getMap = ({ + height = 0, + width = 0, + countries = [], + region, + grid = 'vertical', +}) => { + if (height <= 0 && width <= 0) { + throw new Error('height or width is required'); + } + + let geojson = geojsonWorld; + if (countries.length > 0) { + geojson = { + type: 'FeatureCollection', + features: countries.map((country) => geojsonByCountry[country]), + }; + if (!region) { + region = computeGeojsonBox(geojson); + } + } else if (!region) { + region = DEFAULT_WORLD_REGION; + } + + const poly = geojsonToMultiPolygons(geojson); + + const [X_MIN, Y_MIN] = proj4(proj4.defs('GOOGLE'), [ + region.lng.min, + region.lat.min, + ]); + const [X_MAX, Y_MAX] = proj4(proj4.defs('GOOGLE'), [ + region.lng.max, + region.lat.max, + ]); + const X_RANGE = X_MAX - X_MIN; + const Y_RANGE = Y_MAX - Y_MIN; + + if (width <= 0) { + width = Math.round((height * X_RANGE) / Y_RANGE); + } else if (height <= 0) { + height = Math.round((width * Y_RANGE) / X_RANGE); + } + + const points = {}; + const ystep = grid === 'diagonal' ? Math.sqrt(3) / 2 : 1; + + for (let y = 0; y * ystep < height; y += 1) { + for (let x = 0; x < width; x += 1) { + const localx = y % 2 === 0 && grid === 'diagonal' ? x + 0.5 : x; + const localy = y * ystep; + + const pointGoogle = [ + (localx / width) * X_RANGE + X_MIN, + Y_MAX - (localy / height) * Y_RANGE, + ]; + const wgs84Point = proj4( + proj4.defs('GOOGLE'), + proj4.defs('WGS84'), + pointGoogle, + ); + + if (inside(wgs84Point, poly)) { + points[[x, y].join(';')] = { x: localx, y: localy }; + } + } + } + + return { + points, + X_MIN, + Y_MIN, + X_MAX, + Y_MAX, + X_RANGE, + Y_RANGE, + region, + grid, + height, + width, + ystep, + }; +}; + +export const getMapJSON = (props) => JSON.stringify(getMap(props)); + +const getCacheKey = ({ + height = 0, + width = 0, + countries = [], + region, + grid = 'vertical', +}) => { + return [ + JSON.stringify(region), + grid, + height, + width, + JSON.stringify(countries), + ].join(' '); +}; + +function DottedMap({ avoidOuterPins = false, ...args }) { + const cacheKey = getCacheKey(args); + const map = CACHE[cacheKey] || getMap(args); + + return new DottedMapWithoutCountries({ avoidOuterPins, map }); +} + +export default DottedMap; diff --git a/src/without-countries.js b/src/without-countries.js new file mode 100644 index 0000000..55b1351 --- /dev/null +++ b/src/without-countries.js @@ -0,0 +1,93 @@ +import proj4 from 'proj4'; +import inside from '@turf/boolean-point-in-polygon'; + +function DottedMapWithoutCountries({ map, avoidOuterPins = false }) { + const { + points, + X_MIN, + Y_MAX, + X_RANGE, + Y_RANGE, + region, + grid, + width, + height, + ystep, + } = map; + + return { + addPin({ lat, lng, data, svgOptions }) { + const [googleX, googleY] = proj4(proj4.defs('GOOGLE'), [lng, lat]); + if (avoidOuterPins) { + const wgs84Point = proj4(proj4.defs('GOOGLE'), proj4.defs('WGS84'), [ + googleX, + googleY, + ]); + if (!inside(wgs84Point, poly)) return; + } + let [rawX, rawY] = [ + (width * (googleX - X_MIN)) / X_RANGE, + (height * (Y_MAX - googleY)) / Y_RANGE, + ]; + const y = Math.round(rawY / ystep); + if (y % 2 === 0 && grid === 'diagonal') { + rawX -= 0.5; + } + const x = Math.round(rawX); + let [localx, localy] = [x, Math.round(y) * ystep]; + if (y % 2 === 0 && grid === 'diagonal') { + localx += 0.5; + } + + const point = { x: localx, y: localy, data, svgOptions }; + + points[[x, y].join(';')] = point; + + return point; + }, + getPoints() { + return Object.values(points); + }, + getSVG({ + shape = 'circle', + color = 'current', + backgroundColor = 'transparent', + radius = 0.5, + }) { + const getPoint = ({ x, y, svgOptions = {} }) => { + const pointRadius = svgOptions.radius || radius; + if (shape === 'circle') { + return ``; + } else if (shape === 'hexagon') { + const sqrt3radius = Math.sqrt(3) * pointRadius; + + const polyPoints = [ + [x + sqrt3radius, y - pointRadius], + [x + sqrt3radius, y + pointRadius], + [x, y + 2 * pointRadius], + [x - sqrt3radius, y + pointRadius], + [x - sqrt3radius, y - pointRadius], + [x, y - 2 * pointRadius], + ]; + + return ``; + } + }; + + return ` + ${Object.values(points).map(getPoint).join('\n')} + `; + }, + image: { + region, + width, + height, + }, + }; +} + +export default DottedMapWithoutCountries; diff --git a/webpack.config.js b/webpack.config.js index 18fe99d..cdfaa5f 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,16 +1,37 @@ const path = require('path'); +const fs = require('fs'); + +class CleanOutput { + apply(compiler) { + compiler.hooks.thisCompilation.tap('CleanOutput', () => { + if (fs.existsSync('./index.js')) { + fs.rmSync('./index.js'); + } + if (fs.existsSync('./without-countries.js')) { + fs.rmSync('./without-countries.js'); + } + }); + } +} module.exports = { - entry: './src/index.js', + entry: { + index: './src/with-countries.js', + 'without-countries': './src/without-countries.js', + }, mode: 'production', output: { - path: path.resolve(__dirname, 'dist'), - filename: 'dotted-map.js', + path: path.resolve(__dirname), + filename: '[name].js', library: { name: 'dotted-map', type: 'umd', }, globalObject: 'this', - clean: true, }, + externals: { + '@turf/boolean-point-in-polygon': '@turf/boolean-point-in-polygon', + proj4: 'proj4', + }, + plugins: [new CleanOutput()], }; diff --git a/without-countries.d.ts b/without-countries.d.ts new file mode 100644 index 0000000..6fdb6ce --- /dev/null +++ b/without-countries.d.ts @@ -0,0 +1,65 @@ +namespace DottedMapWithoutCountriesLib { + interface Region { + lat: { min: number; max: number }; + lng: { min: number; max: number }; + } + + interface Map { + points: Point[]; + X_MIN: number; + Y_MAX: number; + X_RANGE: number; + Y_RANGE: number; + region: Region; + grid: 'vertical' | 'diagonal'; + width: number; + height: number; + ystep: number; + } + + interface Settings extends MapSettings { + map: Map; + avoidOuterPins?: false | true; + } + + interface SvgOptions { + color?: string; + radius?: number; + } + + interface Pin { + lat: number; + lng: number; + data?: any; + svgOptions?: SvgOptions; + } + + interface SvgSettings { + shape?: 'circle' | 'hexagon'; + color?: string; + backgroundColor?: string; + radius?: number; + } + + type Point = { + x: number; + y: number; + data?: any; + svgOptions?: SvgOptions; + }; +} + +export default class DottedMapWithoutCountries { + constructor(settings: DottedMapWithoutCountriesLib.Settings); + + addPin( + pin: DottedMapWithoutCountriesLib.Pin, + ): DottedMapWithoutCountriesLib.Point; + getPoints(): DottedMapWithoutCountriesLib.Point[]; + getSVG(settings: DottedMapWithoutCountriesLib.SvgSettings): string; + image: { + region: DottedMap.Region; + width: number; + height: number; + }; +}