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
+
+
+ /* : */
+ );
+ }
+}
+
+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/ */}
-
-
-
-
-
-
-
-
-
-
+ {/* https://v4-alpha.getbootstrap.com/components/carousel/ */}
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
Elige un radio de vigilancia
-
-
-
-
-
-
-
-
Recibe alertas de fuegos en esa zona
-
-
-
-
-
-
-
-
Alerta cuando hay un fuego
-
+
+
+
+
+
+
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",