From ac4331edbd8477cebfb728353043f8380fc480c3 Mon Sep 17 00:00:00 2001 From: vjrj Date: Wed, 14 Feb 2018 18:21:50 +0100 Subject: [PATCH] Subs union to server side --- .../api/Subscriptions/server/publications.js | 35 ---- imports/startup/server/index.js | 1 + imports/startup/server/migrations.js | 4 +- imports/startup/server/subsUnion.js | 84 ++++++++++ .../ui/components/Maps/SubsUnion/SubsUnion.js | 92 +++------- imports/ui/components/Maps/SubsUnion/Unify.js | 48 ++++++ imports/ui/pages/FiresMap/FiresMap.js | 20 ++- .../pages/Subscriptions/SubscriptionsMap.js | 18 +- package-lock.json | 158 +++++++++++++----- package.json | 1 + 10 files changed, 299 insertions(+), 162 deletions(-) create mode 100644 imports/startup/server/subsUnion.js create mode 100644 imports/ui/components/Maps/SubsUnion/Unify.js diff --git a/imports/api/Subscriptions/server/publications.js b/imports/api/Subscriptions/server/publications.js index f67fc3a..adf6e13 100644 --- a/imports/api/Subscriptions/server/publications.js +++ b/imports/api/Subscriptions/server/publications.js @@ -3,43 +3,8 @@ import { Meteor } from 'meteor/meteor'; import { Mongo } from 'meteor/mongo'; import { check } from 'meteor/check'; -import Perlin from 'loms.perlin'; import Subscriptions from '../Subscriptions'; -Perlin.seed(Math.random()); - -Meteor.publishTransformed('userSubsToFires', function transform() { - // https://en.wikipedia.org/wiki/Location_obfuscation - // https://en.wikipedia.org/wiki/Decimal_degrees#Precision - // https://gis.stackexchange.com/questions/27792/what-simple-effective-techniques-for-obfuscating-points-are-available - return Subscriptions.find().serverTransform(function transformDoc(odoc) { - const doc = odoc; - // Destructuring gives me an error: "Cannot destructure property `location` of 'undefined'" - const location = doc.location; - /* doc.lat = location.lat; - * doc.lon = location.lon; */ - let lat; - let lon; - if (location) { - lat = Math.round(location.lat * 10) / 10; - lon = Math.round(location.lon * 10) / 10; - // console.log(`[${lat}, ${lon}]`); - const noiseBase = Perlin.perlin2(lat, lon); - const noise = Math.abs(noiseBase / 3); - // console.log(`Noise ${noise}, abs: ${Math.abs(noise)}`); - lat += noise; - lon += noise; - doc.location.lat = lat; - doc.location.lon = lon; - doc.distance += noiseBase; - } - // console.log(`with noise: [${doc.lat}, ${doc.lon}]`); - delete doc.chatId; - delete doc.geo; - return doc; - }); -}); - Meteor.publish('mysubscriptions', function subscriptions() { return Subscriptions.find({ owner: this.userId }); // type: 'web' }); diff --git a/imports/startup/server/index.js b/imports/startup/server/index.js index aa32e9d..b331b73 100644 --- a/imports/startup/server/index.js +++ b/imports/startup/server/index.js @@ -9,3 +9,4 @@ import './notificationsObserver'; import './facts'; import '../common/comments'; import './sitemaps'; +import './subsUnion'; diff --git a/imports/startup/server/migrations.js b/imports/startup/server/migrations.js index e58605f..f420f82 100644 --- a/imports/startup/server/migrations.js +++ b/imports/startup/server/migrations.js @@ -13,7 +13,7 @@ Meteor.startup(() => { Migrations.config({ // Log job run details to console - log: true + log: Meteor.isProduction }); Migrations.add({ @@ -120,7 +120,7 @@ Meteor.startup(() => { Migrations.add({ version: 8, up: function siteSettingsAddIndex() { - SiteSettings._ensureIndex({ isPublic: 1 }, { unique: 1 }); + SiteSettings._ensureIndex({ isPublic: 1 }); SiteSettings.find({ isPublic: null }).forEach((setting) => { SiteSettings.update({ _id: setting._id }, { $set: { isPublic: true } }); }); diff --git a/imports/startup/server/subsUnion.js b/imports/startup/server/subsUnion.js new file mode 100644 index 0000000..d527f50 --- /dev/null +++ b/imports/startup/server/subsUnion.js @@ -0,0 +1,84 @@ +/* eslint-disable import/no-absolute-path */ + +import { Meteor } from 'meteor/meteor'; +import Subscriptions from '/imports/api/Subscriptions/Subscriptions'; +import SiteSettings from '/imports/api/SiteSettings/SiteSettings'; +import Perlin from 'loms.perlin'; +import L from 'leaflet-headless'; +import calcUnion from '/imports/ui/components/Maps/SubsUnion/Unify'; + +// sudo apt-get install libcairo2-dev libjpeg-dev libgif-dev + +Meteor.startup(() => { + Perlin.seed(Math.random()); + + const addNoisy = (osub) => { + const sub = osub; + let lat = Math.round(sub.location.lat * 10) / 10; + let lon = Math.round(sub.location.lon * 10) / 10; + const noiseBase = Perlin.perlin2(lat, lon); + const noise = Math.abs(noiseBase / 3); + lat += noise; + lon += noise; + sub.location.lat = lat; + sub.location.lon = lon; + sub.distance += noiseBase; + return sub; + }; + + const process = () => { + const group = new L.FeatureGroup(); + const result = calcUnion(Subscriptions.find().fetch(), group, addNoisy); + const union = result[0]; + const bounds = result[1]; + + if (typeof union === 'object') { + const unionSet = { + $set: { + name: 'subs-public-union', + value: JSON.stringify(union), + isPublic: true, + description: 'Public subscriptions union', + type: 'string' + } + }; + const boundsSet = { + $set: { + name: 'subs-public-union-bounds', + value: JSON.stringify(bounds), + isPublic: true, + description: 'Public subscriptions union bounds', + type: 'string' + } + }; + // FIXME, take care of object size: + // https://stackoverflow.com/questions/10827812/what-is-the-length-maximum-for-a-string-data-type-in-mongodb-used-with-ruby + SiteSettings.upsert({ name: 'subs-public-union' }, unionSet, { multi: false }); + SiteSettings.upsert({ name: 'subs-public-union-bounds' }, boundsSet, { multi: false }); + if (Meteor.isDevelopment) console.log('Subscription union calculated'); + } else { + console.log('Subscription union failed!'); + } + }; + + // At startup + process(); + + Subscriptions.find({ createdAt: { $gt: new Date() } }).observe({ + added: function newSubAdded() { // doc) { + if (Meteor.isDevelopment) console.log('Subs added so recreate union'); + process(); + } + }); + + Subscriptions.find().observe({ + changed: function subsChanged() { // updatedDoc, oldDoc) { + if (Meteor.isDevelopment) console.log('Subs changed so recreate union'); + process(); + }, + removed: function subsRemoved() { // oldDoc) { + if (Meteor.isDevelopment) console.log('Subs removed so recreate union'); + process(); + } + }); +}); diff --git a/imports/ui/components/Maps/SubsUnion/SubsUnion.js b/imports/ui/components/Maps/SubsUnion/SubsUnion.js index 60cbd26..c624759 100644 --- a/imports/ui/components/Maps/SubsUnion/SubsUnion.js +++ b/imports/ui/components/Maps/SubsUnion/SubsUnion.js @@ -3,89 +3,43 @@ /* eslint-disable import/no-absolute-path */ /* global L */ -import { Map } from 'react-leaflet'; -import LGeo from 'leaflet-geodesy'; -import tunion from '@turf/union'; -import ttrunc from '@turf/truncate'; -import { check, Match } from 'meteor/check'; - -// https://stackoverflow.com/questions/35394577/leaflet-js-union-merge-circles -const truncOptions = { precision: 6, coordinates: 2 }; - -function unify(polyList) { - let unionTemp; - for (let i = 0; i < polyList.length; i += 1) { - const pol = polyList[i].toGeoJSON(); - const cleanPol = ttrunc(pol, truncOptions); - if (i === 0) { - unionTemp = cleanPol; - } else { - unionTemp = ttrunc(tunion(unionTemp, cleanPol), truncOptions); - } - } - return unionTemp; -} +import calcUnion from '/imports/ui/components/Maps/SubsUnion/Unify'; const subsUnion = (union, options) => { - // check(union, Match.Optional(Object)); - check(options, { - map: Map, - show: Boolean, - subs: [Object], - color: Match.Optional(String), - fillcolor: Match.Optional(String), - opacity: Match.Optional(Number), - fit: Boolean - }); - const color = options.color || '#145A32'; const fillColor = options.fillColor || 'green'; const opacity = options.options || 0.1; if (options.subs) { const lmap = options.map.leafletElement; - // http://leafletjs.com/reference-1.2.0.html#layergroup - // FeatureGroup has getBounds - const unionGroup = new L.FeatureGroup(); - if (union) { lmap.removeLayer(union); } union = null; - - if (options.subs.length > 0 && options.show) { - // http://leafletjs.com/reference-1.2.0.html#path - const copts = { - parts: 144 - }; - options.subs.forEach((sub) => { - try { - if (sub.location && sub.location.lat && sub.location.lon && sub.distance) { - check(sub.location.lon, Number); - check(sub.location.lat, Number); - check(sub.distance, Number); - const circle = LGeo.circle([sub.location.lat, sub.location.lon], sub.distance * 1000, copts); - circle.addTo(unionGroup); - } else { - console.error(`Wrong subscription ${JSON.stringify(sub)}`); - } - } catch (e) { - console.error(e); - console.error(`Wrong subscription trying to make union ${JSON.stringify(sub)}`); + if (options.show) { + if (options.fromServer) { + // We get the json from server side + union = L.geoJson(JSON.parse(options.subs)); + union.setStyle({ color, fillColor, fillOpacity: opacity }); + union.addTo(lmap); + if (options.fit && options.bounds) { + // console.log(options.bounds); + const bounds = JSON.parse(options.bounds); + options.map.leafletElement.fitBounds(L.latLngBounds(bounds._northEast, bounds._southWest)); + } + } else if (options.subs.length > 0) { + const unionGroup = new L.FeatureGroup(); + const result = calcUnion(options.subs, unionGroup, sub => sub); + const unionJson = result[0]; + const bounds = result[1]; + + union = L.geoJson(unionJson); + union.setStyle({ color, fillColor, fillOpacity: opacity }); + union.addTo(lmap); + if (options.fit) { + options.map.leafletElement.fitBounds(bounds); } - }); - const unionJson = unify(unionGroup.getLayers()); - union = L.geoJson(unionJson); - union.setStyle({ - color, - fillColor, - fillOpacity: opacity - }); - union.addTo(lmap); - if (options.fit) { - options.map.leafletElement.fitBounds(unionGroup.getBounds()); } - return union; } } return union; diff --git a/imports/ui/components/Maps/SubsUnion/Unify.js b/imports/ui/components/Maps/SubsUnion/Unify.js new file mode 100644 index 0000000..68f602a --- /dev/null +++ b/imports/ui/components/Maps/SubsUnion/Unify.js @@ -0,0 +1,48 @@ +import { check } from 'meteor/check'; +import LGeo from 'leaflet-geodesy'; +import tunion from '@turf/union'; +import ttrunc from '@turf/truncate'; + +// https://stackoverflow.com/questions/35394577/leaflet-js-union-merge-circles +const truncOptions = { precision: 6, coordinates: 2 }; + +const unify = (polyList) => { + let unionTemp; + for (let i = 0; i < polyList.length; i += 1) { + const pol = polyList[i].toGeoJSON(); + const cleanPol = ttrunc(pol, truncOptions); + if (i === 0) { + unionTemp = cleanPol; + } else { + unionTemp = ttrunc(tunion(unionTemp, cleanPol), truncOptions); + } + } + return unionTemp; +}; + +const calcUnion = (subs, group, decorated) => { + const unionGroup = group; + const copts = { + parts: 144 + }; + subs.forEach((osub) => { + try { + if (osub.location && osub.location.lat && osub.location.lon && osub.distance) { + check(osub.location.lon, Number); + check(osub.location.lat, Number); + check(osub.distance, Number); + const dsub = decorated(osub); + const circle = LGeo.circle([dsub.location.lat, dsub.location.lon], dsub.distance * 1000, copts); + circle.addTo(unionGroup); + } else { + console.error(`Wrong subscription ${JSON.stringify(osub)}`); + } + } catch (e) { + console.error(e, `Wrong subscription trying to make union ${JSON.stringify(osub)}`); + } + }); + const unionJson = unify(unionGroup.getLayers()); + return [unionJson, unionGroup.getBounds()]; +}; + +export default calcUnion; diff --git a/imports/ui/pages/FiresMap/FiresMap.js b/imports/ui/pages/FiresMap/FiresMap.js index b75d26b..f577e08 100644 --- a/imports/ui/pages/FiresMap/FiresMap.js +++ b/imports/ui/pages/FiresMap/FiresMap.js @@ -29,7 +29,6 @@ import ActiveFiresCollection from '/imports/api/ActiveFires/ActiveFires'; import FireAlertsCollection from '/imports/api/FireAlerts/FireAlerts'; import FalsePositivesCollection from '/imports/api/FalsePositives/FalsePositives'; import SiteSettings from '/imports/api/SiteSettings/SiteSettings'; -import UserSubsToFiresCollection from '/imports/api/Subscriptions/Subscriptions'; import { isNotHomeAndMobile, isChrome } from '/imports/ui/components/Utils/isMobile'; import { isHome } from '/imports/ui/components/Utils/location'; import ShareIt from '/imports/ui/components/ShareIt/ShareIt'; @@ -174,6 +173,8 @@ class FiresMap extends React.Component { map, subs: this.props.userSubs, show: this.state.showSubsUnion, + bounds: this.props.userSubsBounds, + fromServer: true, fit: false }); } @@ -181,7 +182,7 @@ class FiresMap extends React.Component { render() { const { t } = this.props; - console.log(`Rendering ${this.props.loading ? 'loading' : 'LOADED'} map ${this.props.activefires.length + this.props.firealerts.length} of ${this.props.activefirestotal} total. False positives: ${this.props.falsePositives.length}. Subs users ready ${this.props.subsready} (${this.props.userSubs.length}), reactive ${this.state.viewport.zoom >= MAXZOOMREACTIVE}`); + console.log(`Rendering ${this.props.loading ? 'loading' : 'LOADED'} map ${this.props.activefires.length + this.props.firealerts.length} of ${this.props.activefirestotal} total. False positives: ${this.props.falsePositives.length}. Reactive ${this.state.viewport.zoom >= MAXZOOMREACTIVE}`); const title = `${t('AppName')}: ${t('Fuegos activos')}`; if (Meteor.isDevelopment) { console.log(`False positives total: ${this.props.falsePositivesTotal}`); @@ -315,7 +316,8 @@ class FiresMap extends React.Component { FiresMap.propTypes = { loading: PropTypes.bool.isRequired, subsready: PropTypes.bool.isRequired, - userSubs: PropTypes.arrayOf(PropTypes.object).isRequired, + userSubs: PropTypes.string, + userSubsBounds: PropTypes.string, activefires: PropTypes.arrayOf(PropTypes.object).isRequired, firealerts: PropTypes.arrayOf(PropTypes.object).isRequired, falsePositives: PropTypes.arrayOf(PropTypes.object).isRequired, @@ -379,15 +381,17 @@ export default translate([], { wait: true })(withTracker(() => { Meteor.subscribe('activefirestotal'); Meteor.subscribe('falsePositivesTotal'); - Meteor.subscribe('settings'); - const userSubs = Meteor.subscribe('userSubsToFires'); + const settingsSubs = Meteor.subscribe('settings'); const lastCheck = SiteSettings.findOne({ name: 'last-fire-check' }); + const userSubs = SiteSettings.findOne({ name: 'subs-public-union' }); + const userSubsBounds = SiteSettings.findOne({ name: 'subs-public-union-bounds' }); const fireAlerts = FireAlertsCollection.find().fetch(); const falsePositives = FalsePositivesCollection.find().fetch(); return { - loading: !subscription ? true : !subscription.ready(), - userSubs: UserSubsToFiresCollection.find().fetch(), - subsready: userSubs.ready(), + loading: !subscription ? true : !(subscription.ready() && settingsSubs.ready()), + userSubs: userSubs ? userSubs.value : null, + userSubsBounds: userSubs ? userSubsBounds.value : null, + subsready: settingsSubs.ready(), // Not reactive query depending on zoom level activefires: ActiveFiresCollection.find({}, { reactive: zoom.get() >= MAXZOOMREACTIVE }).fetch(), activefirestotal: Counter.get('countActiveFires') + fireAlerts.length, diff --git a/imports/ui/pages/Subscriptions/SubscriptionsMap.js b/imports/ui/pages/Subscriptions/SubscriptionsMap.js index ce675fd..5d162a9 100644 --- a/imports/ui/pages/Subscriptions/SubscriptionsMap.js +++ b/imports/ui/pages/Subscriptions/SubscriptionsMap.js @@ -19,7 +19,7 @@ import CenterInMyPosition from '/imports/ui/components/CenterInMyPosition/Center import subsUnion from '/imports/ui/components/Maps/SubsUnion/SubsUnion'; import DefMapLayers from '/imports/ui/components/Maps/DefMapLayers'; import Loading from '/imports/ui/components/Loading/Loading'; -import UserSubsToFiresCollection from '/imports/api/Subscriptions/Subscriptions'; +import SiteSettings from '/imports/api/SiteSettings/SiteSettings'; import { isChrome } from '/imports/ui/components/Utils/isMobile'; import { isHome } from '/imports/ui/components/Utils/location'; import ShareIt from '/imports/ui/components/ShareIt/ShareIt'; @@ -66,6 +66,8 @@ class SubscriptionsMap extends React.Component { this.state.union = subsUnion(this.state.union, { map, subs: this.props.userSubs, + bounds: this.props.userSubsBounds, + fromServer: true, show: true, fit: this.state.init }); @@ -89,7 +91,7 @@ class SubscriptionsMap extends React.Component { render() { const { t } = this.props; const title = `${t('AppName')}: ${t('Zonas vigiladas')}`; - console.log(`Rendering Subs users ready ${this.props.subsready} subs: ${this.props.userSubs.length} viewport: ${JSON.stringify(this.state.viewport)}`); + console.log(`Rendering Subs users ready ${this.props.subsready} viewport: ${JSON.stringify(this.state.viewport)}`); return ( { !isHome() && @@ -153,16 +155,20 @@ class SubscriptionsMap extends React.Component { } SubscriptionsMap.propTypes = { + userSubs: PropTypes.string, + userSubsBounds: PropTypes.string, subsready: PropTypes.bool.isRequired, - userSubs: PropTypes.arrayOf(PropTypes.object).isRequired, history: PropTypes.object.isRequired, t: PropTypes.func.isRequired }; export default translate([], { wait: true })(withTracker(() => { - const userSubs = Meteor.subscribe('userSubsToFires'); + const settingsSubs = Meteor.subscribe('settings'); + const userSubs = SiteSettings.findOne({ name: 'subs-public-union' }); + const userSubsBounds = SiteSettings.findOne({ name: 'subs-public-union-bounds' }); return { - userSubs: UserSubsToFiresCollection.find().fetch(), - subsready: userSubs.ready() + userSubs: userSubs ? userSubs.value : null, + userSubsBounds: userSubs ? userSubsBounds.value : null, + subsready: settingsSubs.ready() }; })(SubscriptionsMap)); diff --git a/package-lock.json b/package-lock.json index 5581dfe..174ad76 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,8 +60,7 @@ "abab": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz", - "integrity": "sha1-X6rZwsB/YN12dw9xzwJbYqY8/U4=", - "dev": true + "integrity": "sha1-X6rZwsB/YN12dw9xzwJbYqY8/U4=" }, "acorn": { "version": "5.2.1", @@ -282,8 +281,7 @@ "array-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", - "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", - "dev": true + "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=" }, "array-includes": { "version": "https://registry.npmjs.org/array-includes/-/array-includes-3.0.3.tgz", @@ -2247,6 +2245,14 @@ "integrity": "sha1-H5CV8u/UlA4LpsWZKreptkzDW6Q=", "dev": true }, + "canvas": { + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-1.6.9.tgz", + "integrity": "sha1-4/lc7HsWvy1vP8clwC2UDTJY9ps=", + "requires": { + "nan": "2.6.2" + } + }, "caseless": { "version": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" @@ -2778,8 +2784,7 @@ "content-type-parser": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/content-type-parser/-/content-type-parser-1.0.2.tgz", - "integrity": "sha512-lM4l4CnMEwOLHAHr/P6MEZwZFPJFtAAKgL6pogbXmVZggIqXhdB6RbBtPOTsw2FcXwYhehRGERJmRrjOiIB8pQ==", - "dev": true + "integrity": "sha512-lM4l4CnMEwOLHAHr/P6MEZwZFPJFtAAKgL6pogbXmVZggIqXhdB6RbBtPOTsw2FcXwYhehRGERJmRrjOiIB8pQ==" }, "convert-source-map": { "version": "1.5.1", @@ -2909,14 +2914,12 @@ "cssom": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.2.tgz", - "integrity": "sha1-uANhcMefB6kP8vFuIihAJ6JDhIs=", - "dev": true + "integrity": "sha1-uANhcMefB6kP8vFuIihAJ6JDhIs=" }, "cssstyle": { "version": "0.2.37", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-0.2.37.tgz", "integrity": "sha1-VBCXI0yyUTyDzu06zdwn/yeYfVQ=", - "dev": true, "requires": { "cssom": "0.3.2" } @@ -2930,6 +2933,11 @@ "es5-ext": "0.10.37" } }, + "d3-queue": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/d3-queue/-/d3-queue-2.0.3.tgz", + "integrity": "sha1-B/vaOsrlNYqcUpmq+ICt8JU+0sI=" + }, "damerau-levenshtein": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.4.tgz", @@ -3331,7 +3339,6 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.9.0.tgz", "integrity": "sha512-v0MYvNQ32bzwoG2OSFzWAkuahDQHK92JBN0pTAALJ4RIxEZe766QJPDR8Hqy7XNUy5K3fnVL76OqYAdc4TZEIw==", - "dev": true, "requires": { "esprima": "3.1.3", "estraverse": "4.2.0", @@ -3343,38 +3350,32 @@ "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" }, "esprima": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", - "dev": true + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=" }, "estraverse": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", - "dev": true + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=" }, "esutils": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" }, "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" }, "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, "requires": { "prelude-ls": "1.1.2", "type-check": "0.3.2" @@ -3384,7 +3385,6 @@ "version": "0.8.2", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", - "dev": true, "requires": { "deep-is": "0.1.3", "fast-levenshtein": "2.0.6", @@ -3397,21 +3397,18 @@ "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true, "optional": true }, "type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, "requires": { "prelude-ls": "1.1.2" } @@ -3419,8 +3416,7 @@ "wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", - "dev": true + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" } } }, @@ -4941,7 +4937,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", - "dev": true, "requires": { "whatwg-encoding": "1.0.3" } @@ -7451,6 +7446,92 @@ "resolved": "https://registry.npmjs.org/leaflet-graphicscale/-/leaflet-graphicscale-0.0.2.tgz", "integrity": "sha1-q2INKJ6acETC9RB8g74XhDrsUwM=" }, + "leaflet-headless": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/leaflet-headless/-/leaflet-headless-0.2.6.tgz", + "integrity": "sha1-Hqh4c/fuJj/JyI3xXsx4qZUdcsk=", + "requires": { + "canvas": "1.6.9", + "jsdom": "9.8.3", + "leaflet": "1.3.1", + "leaflet-image": "0.4.0", + "request": "2.83.0" + }, + "dependencies": { + "acorn": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz", + "integrity": "sha1-q259nYhqrKiwhbwzEreaGYQz8Oc=" + }, + "acorn-globals": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-1.0.9.tgz", + "integrity": "sha1-VbtemGkVB7dFedBRNBMhfDgMVM8=", + "requires": { + "acorn": "2.7.0" + } + }, + "iconv-lite": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" + }, + "jsdom": { + "version": "9.8.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-9.8.3.tgz", + "integrity": "sha1-/eKcEJwyoRMeC2xlkU5kGY+Xw3A=", + "requires": { + "abab": "1.0.4", + "acorn": "2.7.0", + "acorn-globals": "1.0.9", + "array-equal": "1.0.0", + "content-type-parser": "1.0.2", + "cssom": "0.3.2", + "cssstyle": "0.2.37", + "escodegen": "1.9.0", + "html-encoding-sniffer": "1.0.2", + "iconv-lite": "0.4.19", + "nwmatcher": "1.4.3", + "parse5": "1.5.1", + "request": "2.83.0", + "sax": "1.2.4", + "symbol-tree": "3.2.2", + "tough-cookie": "2.3.3", + "webidl-conversions": "3.0.1", + "whatwg-encoding": "1.0.3", + "whatwg-url": "3.1.0", + "xml-name-validator": "2.0.1" + } + }, + "parse5": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-1.5.1.tgz", + "integrity": "sha1-m387DeMr543CQBsXVzzK8Pb1nZQ=" + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + }, + "whatwg-url": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-3.1.0.tgz", + "integrity": "sha1-e9yuSQ+SGu9kUftnOexrvY6Qe/Y=", + "requires": { + "tr46": "0.0.3", + "webidl-conversions": "3.0.1" + } + } + } + }, + "leaflet-image": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/leaflet-image/-/leaflet-image-0.4.0.tgz", + "integrity": "sha1-6E8i/2KI8JubDi9RpIUKnweWjME=", + "requires": { + "d3-queue": "2.0.3" + } + }, "leaflet-sleep": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/leaflet-sleep/-/leaflet-sleep-0.5.1.tgz", @@ -9028,8 +9109,7 @@ "nwmatcher": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/nwmatcher/-/nwmatcher-1.4.3.tgz", - "integrity": "sha512-IKdSTiDWCarf2JTS5e9e2+5tPZGdkRJ79XjYV0pzK8Q9BpsFyBq1RGKxzs7Q8UBushGw7m6TzVKz6fcY99iSWw==", - "dev": true + "integrity": "sha512-IKdSTiDWCarf2JTS5e9e2+5tPZGdkRJ79XjYV0pzK8Q9BpsFyBq1RGKxzs7Q8UBushGw7m6TzVKz6fcY99iSWw==" }, "oauth-sign": { "version": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", @@ -10887,8 +10967,7 @@ "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "semver": { "version": "5.4.1", @@ -11291,8 +11370,7 @@ "symbol-tree": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz", - "integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=", - "dev": true + "integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=" }, "table": { "version": "4.0.2", @@ -11479,8 +11557,7 @@ "tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", - "dev": true + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" }, "trim-right": { "version": "1.0.1", @@ -11688,7 +11765,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.3.tgz", "integrity": "sha512-jLBwwKUhi8WtBfsMQlL4bUUcT8sMkAtQinscJAe/M4KHCkHuUJAF6vuB0tueNIw4c8ziO6AkRmgY+jL3a0iiPw==", - "dev": true, "requires": { "iconv-lite": "0.4.19" }, @@ -11696,8 +11772,7 @@ "iconv-lite": { "version": "0.4.19", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", - "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==", - "dev": true + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" } } }, @@ -11865,8 +11940,7 @@ "xml-name-validator": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-2.0.1.tgz", - "integrity": "sha1-TYuPHszTQZqjYgYb7O9RXh5VljU=", - "dev": true + "integrity": "sha1-TYuPHszTQZqjYgYb7O9RXh5VljU=" }, "xtend": { "version": "4.0.1", diff --git a/package.json b/package.json index 3fd1c7b..43ffaf6 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "leaflet": "^1.3.1", "leaflet-geodesy": "^0.2.1", "leaflet-graphicscale": "0.0.2", + "leaflet-headless": "^0.2.6", "leaflet-sleep": "^0.5.1", "lodash": "^4.17.4", "loms.perlin": "^1.0.1",