Subs union to server side

This commit is contained in:
vjrj 2018-02-14 18:21:50 +01:00
parent ed7c89476f
commit ac4331edbd
10 changed files with 299 additions and 162 deletions

View file

@ -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'
});

View file

@ -9,3 +9,4 @@ import './notificationsObserver';
import './facts';
import '../common/comments';
import './sitemaps';
import './subsUnion';

View file

@ -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 } });
});

View file

@ -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();
}
});
});

View file

@ -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;

View file

@ -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;

View file

@ -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,

View file

@ -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 (
<Fragment>
{ !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));