diff --git a/.gitignore b/.gitignore index daedb47..64687f9 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ settings-production.json settings-development.json imports/ui/stylesheets/.sass-cache/ private/GeoLite2-City.mmdb* +npm-debug.log diff --git a/.meteor/packages b/.meteor/packages index 256af34..0046334 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -42,3 +42,4 @@ flowkey:raven # js errors vjrj:piwik # Stats mdg:geolocation natestrauser:publish-performant-counts +maximum:server-transform diff --git a/.meteor/versions b/.meteor/versions index 363c32b..a2fb248 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -22,6 +22,8 @@ caching-compiler@1.1.9 caching-html-compiler@1.1.2 callback-hook@1.0.10 check@1.2.5 +coffeescript@1.0.17 +dburles:mongo-collection-instances@0.3.5 ddp@1.3.1 ddp-client@2.1.3 ddp-common@1.2.9 @@ -52,10 +54,13 @@ htmljs@1.0.11 http@1.2.12 id-map@1.0.9 jquery@1.11.10 +lai:collection-extensions@0.2.1_1 launch-screen@1.1.1 livedata@1.0.18 localstorage@1.1.1 logging@1.1.17 +maximum:package-base@1.1.2 +maximum:server-transform@0.5.0 mdg:geolocation@1.3.0 meteor@1.7.2 meteor-base@1.1.0 @@ -78,6 +83,10 @@ oauth2@1.1.11 observe-sequence@1.0.16 ordered-dict@1.0.9 ostrio:cookies@2.1.3 +peerlibrary:assert@0.2.5 +peerlibrary:fiber-utils@0.6.0 +peerlibrary:reactive-mongo@0.1.1 +peerlibrary:server-autorun@0.5.2 promise@0.9.0 raix:eventemitter@0.1.3 random@1.0.10 diff --git a/designs/icons-tcef.svg b/designs/icons-tcef.svg index ce904f3..0b965d0 100644 --- a/designs/icons-tcef.svg +++ b/designs/icons-tcef.svg @@ -245,9 +245,9 @@ borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" - inkscape:zoom="0.49497475" - inkscape:cx="79.419557" - inkscape:cy="458.62586" + inkscape:zoom="0.24748737" + inkscape:cx="680.72767" + inkscape:cy="654.87036" inkscape:document-units="mm" inkscape:current-layer="layer1" showgrid="false" @@ -350,5 +350,24 @@ inkscape:connector-curvature="0" sodipodi:nodetypes="cszzcczcsc" /> + + + + diff --git a/imports/api/Subscriptions/server/publications.js b/imports/api/Subscriptions/server/publications.js index 00ea7c1..508900f 100644 --- a/imports/api/Subscriptions/server/publications.js +++ b/imports/api/Subscriptions/server/publications.js @@ -1,10 +1,14 @@ import { Meteor } from 'meteor/meteor'; import Subscriptions from '../Subscriptions'; +import Perlin from 'loms.perlin'; + +Perlin.seed(Math.random()); Meteor.publishTransformed('userSubsToFires', function() { // 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(doc) { var location = doc.location; /* doc.lat = location.lat; @@ -13,6 +17,14 @@ Meteor.publishTransformed('userSubsToFires', function() { doc.lat = Math.round(location.lat * 10) / 10; doc.lon = Math.round(location.lon * 10) / 10; } + // console.log(`[${doc.lat}, ${doc.lon}]`); + var noiseBase = Perlin.perlin2(doc.lat, doc.lon) + var noise = Math.abs(noiseBase/3); + // console.log(`Noise ${noise}, abs: ${Math.abs(noise)}`); + doc.lat += noise; + doc.lon += noise; + doc.distance += noiseBase; + // console.log(`with noise: [${doc.lat}, ${doc.lon}]`); delete doc.chatId; delete doc.geo; delete doc.location; diff --git a/imports/startup/server/api.js b/imports/startup/server/api.js index fc5ffd2..8b392e6 100644 --- a/imports/startup/server/api.js +++ b/imports/startup/server/api.js @@ -9,4 +9,6 @@ import '../../api/Users/server/publications'; import '../../api/Utility/server/methods'; import '../../api/ActiveFires/server/publications'; +import '../../api/FireAlerts/server/publications'; +import '../../api/Subscriptions/server/publications'; // TODO add rate-limit to these publications diff --git a/imports/startup/server/geolocation.js b/imports/startup/server/geolocation.js index fd74a15..2039b2f 100644 --- a/imports/startup/server/geolocation.js +++ b/imports/startup/server/geolocation.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; - import maxmind from 'maxmind'; + // https://stackoverflow.com/questions/13969655/how-do-you-check-whether-the-given-ip-is-internal-or-not function isPrivateIP(ip) { var parts = ip.split('.'); @@ -39,5 +39,5 @@ Meteor.methods({ }); }) return promise.await(); - } + }, }); diff --git a/imports/ui/pages/FiresMap/FiresMap.js b/imports/ui/pages/FiresMap/FiresMap.js index f3ccef1..28ce1ec 100644 --- a/imports/ui/pages/FiresMap/FiresMap.js +++ b/imports/ui/pages/FiresMap/FiresMap.js @@ -1,15 +1,33 @@ import React, {Component} from 'react'; -import { Row, Button, Checkbox } from 'react-bootstrap'; +import { Row, Col, Button, Checkbox } from 'react-bootstrap'; import PropTypes from 'prop-types'; import { Meteor } from 'meteor/meteor'; import { Trans, Interpolate, translate } from 'react-i18next'; import 'leaflet/dist/leaflet.css'; import { Circle, CircleMarker, Map, Marker, Popup, TileLayer, PropTypes as MapPropTypes } from 'react-leaflet' import ActiveFiresCollection from '../../../api/ActiveFires/ActiveFires'; +import FireAlertsCollection from '../../../api/FireAlerts/FireAlerts'; +import UserSubsToFiresCollection from '../../../api/Subscriptions/Subscriptions'; import { withTracker } from 'meteor/react-meteor-data'; import Loading from '../../components/Loading/Loading'; import './FiresMap.scss'; import Leaflet from 'leaflet' +import LGeo from 'leaflet-geodesy'; +import union from 'turf-union'; +import 'leaflet-graphicscale/dist/Leaflet.GraphicScale.min.css'; +import 'leaflet-graphicscale/dist/Leaflet.GraphicScale.min.js'; + +// https://stackoverflow.com/questions/35394577/leaflet-js-union-merge-circles +function unify(polyList) { + for (var i = 0; i < polyList.length; ++i) { + if (i == 0) { + var unionTemp = polyList[i].toGeoJSON(); + } else { + unionTemp =union(unionTemp, polyList[i].toGeoJSON()); + } + } + return L.geoJson(unionTemp); +} const fireIcon = new Leaflet.Icon({ iconUrl: "/fire-marker.png", @@ -21,42 +39,56 @@ const fireIcon = new Leaflet.Icon({ * popupAnchor: [-3, -76]// point from which the popup should open relative to the iconAnchor*/ }) +const nFireIcon = new Leaflet.Icon({ + iconUrl: "/n-fire-marker.png", + /* shadowUrl: require('../public/marker-shadow.png'), */ + iconSize: [16, 24], // size of the icon + /* shadowSize: [50, 64], // size of the shadow */ + iconAnchor: [8, 26], // point of the icon which will correspond to marker's location + /* shadowAnchor: [4, 62], // the same for the shadow + * popupAnchor: [-3, -76]// point from which the popup should open relative to the iconAnchor*/ +}) + // http://leafletjs.com/reference-1.2.0.html#icon -const MyPopupMarker = ({ children, lat, lon}) => ( -
+const MyPopupMarker = ({ children, lat, lon, nasa}) => ( +
{/* {children} */} - +
) -const FireMark = ({ lat, lon, scan }) => ( +const FireMark = ({ lat, lon, scan, nasa }) => ( ) /* Less acurate (1 pixel per fire) but faster */ -const Fire = ({ lat, lon, scan }) => ( - +const FirePixel = ({ lat, lon, nasa }) => ( + ) MyPopupMarker.propTypes = { + // https://github.com/PaulLeCam/react-leaflet/tree/master/src/propTypes children: MapPropTypes.children, lat: PropTypes.number.isRequired, lon: PropTypes.number.isRequired, + nasa: PropTypes.bool.isRequired, } -Fire.propTypes = { - scan: PropTypes.number.isRequired, +FirePixel.propTypes = { lat: PropTypes.number.isRequired, lon: PropTypes.number.isRequired, + nasa: PropTypes.bool.isRequired, } FireMark.propTypes = { scan: PropTypes.number.isRequired, lat: PropTypes.number.isRequired, lon: PropTypes.number.isRequired, + nasa: PropTypes.bool.isRequired, } const MyMarkersList = ({ markers }) => { @@ -66,10 +98,16 @@ const MyMarkersList = ({ markers }) => { return
{items}
} -const FireList = ({ activefires, scale, useMarkers }) => { - const items = activefires.map(({ _id, ...props }) => ( - useMarkers? : - scale? :)) +const FireList = ({ fires, scale, useMarkers, nasa }) => { + /* if (nasa) { + * console.log(`Scale: ${scale}`); + * for (var i = 0; i < fires.length; i ++) { + * console.log(fires[i].scan); + * } + * }*/ + const items = fires.map(({ _id, ...props }) => ( + useMarkers? : + (!nasa && !scale)? :)) return
{items}
} @@ -91,8 +129,10 @@ class FiresMap extends React.Component { this.state = { viewport: DEFAULT_VIEWPORT, modified: false, - useMarkers: false + useMarkers: false, + showSubsUnion: true } + this.unionGroup = new L.LayerGroup(); } centerOnUserLocation = () => { @@ -122,6 +162,7 @@ class FiresMap extends React.Component { componentDidMount() { height.set(this.divElement.clientHeight); width.set(this.divElement.clientWidth); + this.addScale(); } onViewportChanged = viewport => { @@ -130,7 +171,8 @@ class FiresMap extends React.Component { lat.set(viewport.center[0]); lng.set(viewport.center[1]); this.state.viewport = viewport; - this.state.modified = true + this.state.modified = true; + this.showSubsUnion(this.state.showSubsUnion); } onClickReset = () => { @@ -143,9 +185,57 @@ class FiresMap extends React.Component { this.forceUpdate(); } + getMap = () => { + return this.refs['fireMap'].leafletElement; + } + + showSubsUnion = (show) => { + this.state.showSubsUnion = show; + const map = this.getMap(); + // http://leafletjs.com/reference-1.2.0.html#layergroup + var unionGroup = this.unionGroup; + + if (this.union) { + map.removeLayer(this.union); + } + + if (show) { + // http://leafletjs.com/reference-1.2.0.html#path + var copts = { + parts: 144, + }; + UserSubsToFiresCollection.find().forEach( function(subs){ + var circle = LGeo.circle([subs.lat, subs.lon], subs.distance * 1000, copts) + circle.addTo(unionGroup); + }); + this.union = unify(unionGroup.getLayers()); + this.union.setStyle({ + color: "#145A32", + fillColor: "green", + fillOpacity: .1, + }); + this.union.addTo(map); + } + } + + addScale = () => { + // https://www.npmjs.com/package/leaflet-graphicscale + const map = this.getMap(); + var options = { + fill: 'fill', + showSubunits: true, + } + var graphicScale = L.control.graphicScale([options]).addTo(map); + } + render() { this.state.viewport = !this.state.modified && this.props.viewport && Array.isArray(this.props.viewport.center)? this.props.viewport: this.state.viewport; + if (this.props.subsready && this.refs['fireMap']) { + // Show union of users + this.showSubsUnion(this.state.showSubsUnion); + }; + return ( /* Large number of markers: https://stackoverflow.com/questions/43015854/large-dataset-of-markers-or-dots-in-leaflet/43019740#43019740 */ @@ -159,43 +249,55 @@ class FiresMap extends React.Component { :""}

Fuegos activos

- {this.props.activefires.length === 0? - No hay fuegos activos en esta zona del mapa. Hay un total de {{countTotal: this.props.activefirestotal}} fuegos activos detectados en todo el mundo.: - En rojo, {{count: this.props.activefires.length}} fuegos activos en el mapa. Hay un total de {{countTotal: this.props.activefirestotal}} fuegos activos detectados en todo el mundo. - } - this.useMarkers(e.target.checked)}> - Resaltar los fuegos con un marcador + +

+ {this.props.activefires.length === 0? + No hay fuegos activos en esta zona del mapa. Hay un total de {{countTotal: this.props.activefirestotal}} fuegos activos detectados en todo el mundo.:En rojo, {{count: this.props.activefires.length}} fuegos activos en el mapa. Hay un total de {{countTotal: this.props.activefirestotal}} fuegos activos detectados en todo el mundo por la NASA. + } +

+

En naranja, los fuegos notificados por nuestros usuarios/as recientemente.

+ + + this.showSubsUnion(e.target.checked)}> + Resaltar en verde el área vigilada por nuestros usuarios/as (*) + + this.useMarkers(e.target.checked)}> + Resaltar los fuegos con un marcador + + +
- + {/* http://wiki.openstreetmap.org/wiki/Tile_servers */} 8} + fires={this.props.activefires} + scale={this.state.viewport.zoom > 7} useMarkers={this.state.useMarkers} + nasa={true} + /> + -

- Fuente NASA y alertas vecinales de nuestr@s usuari@s. -

-
- - + (*) Para preservar la privacidad de nuestros usuarios/as, los datos reflejados están aleatoriamente alterados y son solo orientativos.
); @@ -210,6 +312,7 @@ const width = new ReactiveVar(400); FiresMap.propTypes = { loading: PropTypes.bool.isRequired, + subsready: PropTypes.bool.isRequired, activefires: PropTypes.arrayOf(PropTypes.object).isRequired, activefirestotal: PropTypes.number.isRequired, viewport: PropTypes.object.isRequired @@ -236,13 +339,21 @@ export default translate([], { wait: true }) (withTracker(() => { }); Meteor.subscribe('activefirestotal'); + // Right now to all neighborhood alerts + Meteor.subscribe('fireAlerts'); + var userSubs = Meteor.subscribe('userSubsToFires'); // const subscription = Meteor.subscribe('activefiresmyloc', zoom.get()); - console.log(`Active fires ${ActiveFiresCollection.find().fetch().length} of ${Counter.get('countActiveFires')}`); - + // console.log(`Active fires ${ActiveFiresCollection.find().fetch().length} of ${Counter.get('countActiveFires')}`); + // console.log(`Active neighborhood fires ${FireAlertsCollection.find().fetch().length} and users subscribed ${UserSubsToFiresCollection.find().fetch().length}`); + // console.log(UserSubsToFiresCollection.find().fetch()); return { loading: !subscription.ready(), + subsready: userSubs.ready(), activefires: ActiveFiresCollection.find().fetch(), activefirestotal: Counter.get('countActiveFires'), + firealerts: FireAlertsCollection.find().fetch().map( + doc => ( { _id: doc['_id'], lat: doc['location'].lat, lon: doc['location'].lon })), + userSubs: UserSubsToFiresCollection.find().fetch(), viewport: { center: [lat.get(), lng.get()], // a point in the sea zoom: zoom.get(), diff --git a/imports/ui/pages/FiresMap/FiresMap.scss b/imports/ui/pages/FiresMap/FiresMap.scss index d78dd7c..09c46fb 100644 --- a/imports/ui/pages/FiresMap/FiresMap.scss +++ b/imports/ui/pages/FiresMap/FiresMap.scss @@ -2,7 +2,7 @@ .leaflet-container { height: 100%; - min-height: 75vh; + min-height: 70vh; width: 100%; min-width: 75vw; display: flex; @@ -38,3 +38,7 @@ i.location { width: 35px; vertical-align: middle; } + +.mark-checkbox { + margin-left: 5px; +} diff --git a/package.json b/package.json index 43383fe..928f507 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,10 @@ "jquery": "^2.2.4", "jquery-validation": "^1.17.0", "juice": "^4.1.1", + "leaflet-geodesy": "^0.2.1", + "leaflet-graphicscale": "0.0.2", "lodash": "^4.17.4", + "loms.perlin": "^1.0.1", "maxmind": "^2.3.0", "meteor-accounts-t9n": "^2.0.3", "meteor-node-stubs": "^0.2.11", @@ -40,7 +43,8 @@ "react-router-bootstrap": "^0.24.3", "react-router-dom": "^4.2.2", "reactstrap": "^5.0.0-alpha.3", - "simpl-schema": "^0.3.2" + "simpl-schema": "^0.3.2", + "turf-union": "^3.0.12" }, "devDependencies": { "babel-jest": "^20.0.3", diff --git a/public/locales/es/common.json b/public/locales/es/common.json index 0412b2d..fa7ec65 100644 --- a/public/locales/es/common.json +++ b/public/locales/es/common.json @@ -115,9 +115,9 @@ "noActiveFireInMapCount": "No hay fuegos activos en esta zona del mapa. Hay un total de <1><0>{{countTotal}} fuegos activos detectados en todo el mundo.", "activeFireInMapCount": - "En rojo, <1><0>{{count,number}} fuegos activos en el mapa. Hay un total de <3><0>{{countTotal,number}} fuegos activos detectados en todo el mundo.", + "En rojo, <1><0>{{count,number}} fuegos activos en el mapa. Hay un total de <3><0>{{countTotal,number}} fuegos activos detectados en todo el mundo por la NASA.", + "activeNeigFireInMapCount": + "En naranja, los fuegos notificados por nuestros usuarios/as recientemente.", "Centrar el mapa en tu ubicación": - "Centrar el mapa en tu ubicación", - "Fuente NASA y alertas vecinales de nuestr@s usuari@s.": - "Fuente NASA y alertas vecinales de nuestr@s usuari@s." + "Centrar el mapa en tu ubicación" } diff --git a/public/n-fire-marker.png b/public/n-fire-marker.png new file mode 100644 index 0000000..65a115d Binary files /dev/null and b/public/n-fire-marker.png differ