diff --git a/imports/api/ActiveFires/ActiveFires.js b/imports/api/ActiveFires/ActiveFires.js new file mode 100644 index 0000000..e751d3d --- /dev/null +++ b/imports/api/ActiveFires/ActiveFires.js @@ -0,0 +1,65 @@ +/* eslint-disable consistent-return */ + +import { Mongo } from 'meteor/mongo'; +import SimpleSchema from 'simpl-schema'; + +const ActiveFires = new Mongo.Collection('activefires'); + +ActiveFires.allow({ + insert: () => false, + update: () => false, + remove: () => false, +}); + +ActiveFires.deny({ + insert: () => true, + update: () => true, + remove: () => true, +}); + +/* Sample: + * { + * "_id" : ObjectId("5a208d7579095a1adba48191"), + * "ourid" : { + * "type" : "Point", + * "coordinates" : [ + * -42.84192, + * -25.54453 + * ] + * }, + * "lat" : 9.743, + * "lon" : -63.483, + * "updatedAt" : ISODate("2017-11-30T22:02:39.644Z"), + * "type" : "viirs", + * "when" : ISODate("2017-11-30T05:30:00Z"), + * "acq_date" : "2017-11-30", + * "acq_time" : "04:12", + * "scan" : 0.5, + * "track" : 0.4, + * "satellite" : "N", + * "confidence" : "nominal", + * "version" : "1.0NRT", + * "frp" : 4.3, + * "daynight" : "N", + * "bright_ti4" : 330.2, + * "bright_ti5" : 291, + * "createdAt" : ISODate("2017-11-30T08:07:33.720Z") + * } + * */ + +ActiveFires.schema = new SimpleSchema({ + lat: SimpleSchema.Integer, + lon: SimpleSchema.Integer, + scan: Number, + type: String, + acq_date: String, + acq_time: String, + when: Date, + createdAt: Date, + updatedAt: Date + } +); + +ActiveFires.attachSchema(ActiveFires.schema); + +export default ActiveFires; diff --git a/imports/api/ActiveFires/server/publications.js b/imports/api/ActiveFires/server/publications.js new file mode 100644 index 0000000..36cd08b --- /dev/null +++ b/imports/api/ActiveFires/server/publications.js @@ -0,0 +1,74 @@ +import { Meteor } from 'meteor/meteor'; +import { check, Match } from 'meteor/check'; +import ActiveFires from '../ActiveFires'; + +Meteor.publish('activefires', function activefires() { + return ActiveFires.find(); +}); + +const validZoom = Match.Where((zoom) => { + // http://wiki.openstreetmap.org/wiki/Zoom_levels + check(zoom, Number); + return zoom >= 0 && zoom <= 19; +}); + +// https://github.com/3stack-software/meteor-match-library +var NumberBetween = function (min, max) { + return Match.Where(function (x) { + check(x, Number); + return min <= x && x <= max; + }); +}; + +var NullOr = function (type) { + return Match.Where(function (x) { + if (x === null) { + return true; + } + check(x, type); + return true; + }); +}; + + +const zoomMetersPerPixel = [156412, 78206, 39103, 19551, 9776, 4888, 2444, 1222, 610.984, 305.492, 152.746, 76.373, 38.187, 19.093, 9.547, 4.773, 2.387, 1.193, 0.596, 0.298]; + +var activefires = function(zoom, lat, lng) { + // latitude -90 and 90 and the longitude between -180 and 180 + check(lat, NumberBetween(-90, 90)); + check(lng, NumberBetween(-180, 180)); + + // console.log("Zoom: " + zoom + " lat: " + lat + " lng: " + lng); + // console.log("Meters per pixel: " + zoomMetersPerPixel[zoom]); + + // console.log(geoData); + return ActiveFires.find({ + ourid: { + $near : { + $geometry: { + type: "Point", + coordinates: [ lng, lat] + }, + $minDistance: 0, + $maxDistance: zoomMetersPerPixel[zoom] * 1000 + } + } + }); +} + +Meteor.publish('activefiresmyloc', function(zoom, lat, lng) { + check(zoom, validZoom); + check(lat, NullOr(Number)); + check(lng, NullOr(Number)); + if (lat === null || lng === null) { + var clientIP = this.connection.clientAddress; + if (clientIP === '127.0.0.1') { + clientIP = '80.58.61.250' // Some Spain IP address + } + var geoData = IPGeocoder.geocode(clientIP); + var location = geoData.location; + lat = location.latitude; + lng = location.longitude; + } + return activefires(zoom, lat, lng); +}); diff --git a/imports/startup/server/api.js b/imports/startup/server/api.js index e351317..fc5ffd2 100644 --- a/imports/startup/server/api.js +++ b/imports/startup/server/api.js @@ -7,3 +7,6 @@ import '../../api/Users/server/methods'; import '../../api/Users/server/publications'; import '../../api/Utility/server/methods'; + +import '../../api/ActiveFires/server/publications'; +// TODO add rate-limit to these publications diff --git a/imports/ui/pages/Index/FiresMap.js b/imports/ui/pages/Index/FiresMap.js new file mode 100644 index 0000000..dec4e39 --- /dev/null +++ b/imports/ui/pages/Index/FiresMap.js @@ -0,0 +1,208 @@ +import React, {Component} from 'react'; +import { Row } from 'react-bootstrap'; +import PropTypes from 'prop-types'; +import { Meteor } from 'meteor/meteor'; +import { 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 { withTracker } from 'meteor/react-meteor-data'; +import Loading from '../../components/Loading/Loading'; + +const MyPopupMarker = ({ children, position }) => ( + + + {children} + + +) + +const MyCircle = ({ radius, position }) => ( + +) + +const FireMark = ({ lat, lon, scan }) => ( + +) + +/* Less acurate (1 pixel per fire) but faster */ +const Fire = ({ lat, lon, scan }) => ( + +) + +MyPopupMarker.propTypes = { + children: MapPropTypes.children, + position: MapPropTypes.latlng, +} + +MyCircle.propTypes = { + radius: PropTypes.number.isRequired, + position: MapPropTypes.latlng, +} + +Fire.propTypes = { + scan: PropTypes.number.isRequired, + lat: PropTypes.number.isRequired, + lon: PropTypes.number.isRequired, +} + +FireMark.propTypes = { + scan: PropTypes.number.isRequired, + lat: PropTypes.number.isRequired, + lon: PropTypes.number.isRequired, +} + +const MyMarkersList = ({ markers }) => { + const items = markers.map(({ key, ...props }) => ( + + )) + return
{items}
+} + +const MyCirclesList = ({ circles }) => { + const items = circles.map(({ key, ...props }) => ( + + )) + return
{items}
+} + +const FireList = ({ activefires, scale }) => { + // console.log("Scaling? :" + scale); + const items = activefires.map(({ _id, ...props }) => ( + scale? : + + )) + return
{items}
+} + +MyMarkersList.propTypes = { + markers: PropTypes.array.isRequired, +} + +MyCirclesList.propTypes = { + circles: PropTypes.array.isRequired, +} + +const DEF_LAT = 35.159028; +const DEF_LNG = -46.738057; +const DEFAULT_VIEWPORT = { + center: [DEF_LAT, DEF_LNG], // a point in the sea + zoom: 8, +} + +class FiresMap extends React.Component { + + constructor(props) { + super(props); + this.state = { + viewport: DEFAULT_VIEWPORT, + modified: false + } + } + + onViewportChanged = viewport => { + // console.log(this.state.viewport); + zoom.set(viewport.zoom); + lat.set(viewport.center[0]); + lng.set(viewport.center[1]); + this.state = { viewport: viewport, modified: true }; + } + + onClickReset = () => { + // console.log("onclick"); + // this.setState({ viewport: DEFAULT_VIEWPORT }) + } + + render() { + // const position = [this.default.lat, this.default.lng]; + // const position = this.props.geoip || [this.default.lat, this.default.lng]; + this.state = { + viewport: !this.state.modified && this.props.viewport && Array.isArray(this.props.viewport.center)? this.props.viewport: this.state.viewport, + modified: this.state.modified + } + + return ( + /* !this.props.loading ?*/ + /* Large number of markers: + https://stackoverflow.com/questions/43015854/large-dataset-of-markers-or-dots-in-leaflet/43019740#43019740 */ +
+ Fires active in this map: {this.props.activefires.length} + Mostrar fábricas + + {/* http://wiki.openstreetmap.org/wiki/Tile_servers */} + {/* */} + {/* */} + + {/* + + */} + + 8} /> + + {/* */} + +
+ /* : */ + ); + } +} + +const geoip = new ReactiveVar(''); +const zoom = new ReactiveVar(8); +const lat = new ReactiveVar(DEF_LAT); +const lng = new ReactiveVar(DEF_LNG); + +FiresMap.propTypes = { + loading: PropTypes.bool.isRequired, + activefires: PropTypes.arrayOf(PropTypes.object).isRequired, +}; + +export default withTracker(() => { + var subscription; + Meteor.autorun(function() { + // Subscribe for the current templateId (only if one is selected). Note this + // will automatically clean up any previously subscribed data and it will + // also stop all subscriptions when this template is destroyed. + if (zoom.get()) + // TODO select position + subscription = Meteor.subscribe('activefiresmyloc', zoom.get(), lat.get(), lng.get()); + }); + + // const subscription = Meteor.subscribe('activefiresmyloc', zoom.get()); + Meteor.call("geo", function (error, response) { + if (error) { + console.warn(error); + } else { + geoip.set([response.location.latitude, response.location.longitude] ); + } + }); + + return { + loading: !subscription.ready(), + activefires: ActiveFiresCollection.find().fetch(), + geoip: geoip.get(), + zoom: zoom.get(), + viewport: { + center: geoip.get(), // a point in the sea + zoom: zoom.get(), + } + }; +})(FiresMap); diff --git a/imports/ui/pages/Index/Index-custom.scss b/imports/ui/pages/Index/Index-custom.scss index 53bc0af..087a9c9 100644 --- a/imports/ui/pages/Index/Index-custom.scss +++ b/imports/ui/pages/Index/Index-custom.scss @@ -140,3 +140,11 @@ color: white; text-transform: uppercase; } + + +/* https://github.com/PaulLeCam/react-leaflet/issues/108 */ +.leaflet-container { + height: 500px; + width: 85%; + margin: 0 auto; +} diff --git a/imports/ui/pages/Index/Index.js b/imports/ui/pages/Index/Index.js index f935a62..8294ae4 100644 --- a/imports/ui/pages/Index/Index.js +++ b/imports/ui/pages/Index/Index.js @@ -3,17 +3,22 @@ import { Button } from 'react-bootstrap'; import { translate } from 'react-i18next'; import {render} from 'react-dom'; import { Link } from 'react-router-dom'; - // https://www.npmjs.com/package/react-resize-detector import ReactResizeDetector from 'react-resize-detector'; +import FiresMap from './FiresMap'; import './Index.scss'; import './Index-custom.scss'; + class Index extends Component { // Debounce by David Walsch // https://davidwalsh.name/javascript-debounce-function + constructor(props) { + super(props); + } + debounce = (func, wait, immediate) => { var timeout; return function() { @@ -51,72 +56,77 @@ class Index extends Component { render() { return (
- {/* https://v4-alpha.getbootstrap.com/components/carousel/ */} -
- -
-
    -
  1. -
  2. -
  3. -
  4. -
-
+ {/* https://v4-alpha.getbootstrap.com/components/carousel/ */} +
+ +
+
    +
  1. +
  2. +
  3. +
  4. +
+
-
-
-
-

Elige un lugar

-

-
-
- -
-
-
-

Elige un radio de vigilancia

-

-
-
- -
-
-
-

Recibe alertas de fuegos en esa zona

-

-
-
- -
-
-
-

Alerta cuando hay un fuego

-

+
+
+
+

Elige un lugar

+

+
+
+ +
+
+
+

Elige un radio de vigilancia

+

+
+
+ +
+
+
+

Recibe alertas de fuegos en esa zona

+

+
+
+ +
+
+
+

Alerta cuando hay un fuego

+

+
+ + + Previous + + + + Next +
- - - Previous - - - - Next - -
-
+
-
-
-
-

{this.props.t('AppName')}

+
+
+
+

{this.props.t('AppName')}

+
+

Siempre alerta a los fuegos en nuestro vecindario

+ {this.props.t('Participa')}
-

Siempre alerta a los fuegos en nuestro vecindario

- {this.props.t('Participa')} -
-
-
- ); + + +
+ + +
+ + ); }; } diff --git a/package.json b/package.json index a0dc066..b1aa096 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "react-bootstrap": "^0.31.2", "react-dom": "^15.6.1", "react-i18next": "^6.1.0", + "react-leaflet": "^1.7.7", "react-resize-detector": "^1.1.0", "react-router-bootstrap": "^0.24.3", "react-router-dom": "^4.2.2",