Expose a new export: without-countries
This commit is contained in:
+2
-1
@@ -1,4 +1,5 @@
|
||||
node_modules/
|
||||
test.js
|
||||
*.svg
|
||||
dist/
|
||||
/index.js
|
||||
/without-countries.js
|
||||
|
||||
+1
-1
@@ -5,5 +5,5 @@
|
||||
"singleQuote": true,
|
||||
"arrowParens": "always",
|
||||
"bracketSpacing": true,
|
||||
"printWidth": 120
|
||||
"printWidth": 80
|
||||
}
|
||||
|
||||
Vendored
+6
-1
@@ -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);
|
||||
|
||||
|
||||
Generated
+46
-46
@@ -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",
|
||||
|
||||
+7
-3
@@ -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"
|
||||
},
|
||||
|
||||
-186
@@ -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 `<circle cx="${x}" cy="${y}" r="${pointRadius}" fill="${svgOptions.color || color}" />`;
|
||||
} 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 `<polyline points="${polyPoints.map((point) => point.join(',')).join(' ')}" fill="${
|
||||
svgOptions.color || color
|
||||
}" />`;
|
||||
}
|
||||
};
|
||||
|
||||
return `<svg viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg" style="background-color: ${backgroundColor}">
|
||||
${Object.values(points).map(getPoint).join('\n')}
|
||||
</svg>`;
|
||||
},
|
||||
image: {
|
||||
region,
|
||||
width,
|
||||
height,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default DottedMap;
|
||||
@@ -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;
|
||||
@@ -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 `<circle cx="${x}" cy="${y}" r="${pointRadius}" fill="${
|
||||
svgOptions.color || color
|
||||
}" />`;
|
||||
} 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 `<polyline points="${polyPoints
|
||||
.map((point) => point.join(','))
|
||||
.join(' ')}" fill="${svgOptions.color || color}" />`;
|
||||
}
|
||||
};
|
||||
|
||||
return `<svg viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg" style="background-color: ${backgroundColor}">
|
||||
${Object.values(points).map(getPoint).join('\n')}
|
||||
</svg>`;
|
||||
},
|
||||
image: {
|
||||
region,
|
||||
width,
|
||||
height,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default DottedMapWithoutCountries;
|
||||
+25
-4
@@ -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()],
|
||||
};
|
||||
|
||||
Vendored
+65
@@ -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;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user