Fire popupsand active/archive urls

This commit is contained in:
vjrj 2018-02-08 11:15:23 +01:00
parent 6870de4c7b
commit eca58df6c2
15 changed files with 222 additions and 49 deletions

View file

@ -27,6 +27,7 @@ const activefires = (northEastLng, northEastLat, southWestLng, southWestLat) =>
fields: { fields: {
lat: 1, lat: 1,
lon: 1, lon: 1,
when: 1,
scan: 1 scan: 1
} }
}); });

View file

@ -7,6 +7,7 @@ import urlEnc from '/imports/modules/url-encode';
import { Promise } from 'meteor/promise'; import { Promise } from 'meteor/promise';
import NodeGeocoder from 'node-geocoder'; import NodeGeocoder from 'node-geocoder';
import { gmapServerKey } from '/imports/startup/server/IPGeocoder'; import { gmapServerKey } from '/imports/startup/server/IPGeocoder';
import ActiveFiresCollection from '/imports/api/ActiveFires/ActiveFires';
import FiresCollection from '../Fires'; import FiresCollection from '../Fires';
function findFire(unsealed) { function findFire(unsealed) {
@ -36,6 +37,75 @@ const unseal = async (obj) => {
const unsealW = obj => unseal(obj); const unsealW = obj => unseal(obj);
const fixConfidence = (obj) => {
if (typeof obj.confidence === 'string') {
obj.confidence = Number.parseInt(obj.confidence, 10);
}
if (typeof obj.confidence === 'undefined' || isNaN(obj.confidence)) {
delete obj.confidence;
}
return obj;
};
const findOrCreateFire = (obj) => {
const fire = findFire(obj);
// console.log(`Found: ${fire.count()}`);
if (!obj.address) {
try {
const rev = Promise.await(geocoder.reverse({ lat: obj.lat, lon: obj.lon }));
if (rev[0]) {
obj.address = rev[0].formattedAddress;
}
// console.log(obj.address);
} catch (reve) {
console.warn(reve);
}
}
if (fire.count() === 0) {
// const result =
// console.log('Creating new fire');
FiresCollection.upsert({ ourid: obj.ourid, when: obj.when, type: obj.type }, { $set: obj }, { multi: false, upsert: true });
// console.log(JSON.stringify(result));
}
return findFire(obj);
};
Meteor.publish('fireFromActiveId', function fireFromActiveId(_id) {
try {
check(_id, String);
// console.log(`Looking for active fire ${_id}`);
const fire = ActiveFiresCollection.findOne(new Meteor.Collection.ObjectID(_id));
if (fire) {
// console.info(`Active fire found: ${_id}`);
return findOrCreateFire(fixConfidence(fire));
}
console.info(`Active fire not found: ${_id}`);
// Not found in active fires!
return this.ready();
} catch (e) {
console.info(`Active fire not found (with error): ${_id}`);
return this.ready();
}
});
Meteor.publish('fireFromId', function fireFromId(_id) {
try {
check(_id, String);
// console.log(`Looking for archive fire ${_id}`);
const fire = FiresCollection.find(new Meteor.Collection.ObjectID(_id));
if (fire.count() !== 0) {
// console.info(`Archive fire found: ${_id}`);
return fire;
}
console.info(`Fire not found: ${_id}`);
// Not found in active fires!
return this.ready();
} catch (e) {
console.info(`Active fire not found (with error): ${_id}`);
return this.ready();
}
});
Meteor.publish('fireFromHash', function fireFromHash(fireEnc) { Meteor.publish('fireFromHash', function fireFromHash(fireEnc) {
check(fireEnc, String); check(fireEnc, String);
try { try {
@ -55,32 +125,9 @@ Meteor.publish('fireFromHash', function fireFromHash(fireEnc) {
unsealed.updatedAt = !u ? new Date() : new Date(u); unsealed.updatedAt = !u ? new Date() : new Date(u);
// console.log(unsealed); // console.log(unsealed);
// FIXME: // FIXME:
if (typeof unsealed.confidence === 'string') { const unsealedFix = fixConfidence(unsealed);
unsealed.confidence = Number.parseInt(unsealed.confidence, 10); FiresCollection.schema.validate(unsealedFix);
} return findOrCreateFire(unsealedFix);
if (typeof unsealed.confidence === 'undefined' || isNaN(unsealed.confidence)) {
delete unsealed.confidence;
}
FiresCollection.schema.validate(unsealed);
const fire = findFire(unsealed);
// console.log(`Found: ${fire.count()}`);
if (!unsealed.address) {
try {
const rev = Promise.await(geocoder.reverse({ lat: unsealed.lat, lon: unsealed.lon }));
if (rev[0]) {
unsealed.address = rev[0].formattedAddress;
}
// console.log(unsealed.address);
} catch (reve) {
console.warn(reve);
}
}
if (fire.count() === 0) {
// const result =
FiresCollection.upsert({ ourid: unsealed.ourid, when: unsealed.when, type: unsealed.type }, { $set: unsealed }, { multi: false, upsert: true });
// console.log(JSON.stringify(result));
}
return findFire(unsealed);
/* console.log(`fires: ${fire.count()}`); /* console.log(`fires: ${fire.count()}`);
* return fire; */ * return fire; */
} catch (e) { } catch (e) {

View file

@ -53,7 +53,7 @@ i18nOpts.react = {
nsMode: 'default' */ nsMode: 'default' */
}; };
const sendMissing = true; const sendMissing = Meteor.isDevelopment;
if (sendMissing && !Meteor.isProduction) { if (sendMissing && !Meteor.isProduction) {
i18nOpts.sendMissing = true; i18nOpts.sendMissing = true;
i18nOpts.sendMissingTo = 'fallback'; i18nOpts.sendMissingTo = 'fallback';

View file

@ -11,7 +11,7 @@ i18nOpts.backend.addPath = `${Meteor.rootPath}/../web.browser/app/${i18nOpts.bac
// console.log(i18nOpts.backend.loadPath); // console.log(i18nOpts.backend.loadPath);
i18nOpts.debug = false; i18nOpts.debug = false;
i18nOpts.saveMissing = true; i18nOpts.saveMissing = Meteor.isDevelopment;
i18nOpts.initImmediate = false; i18nOpts.initImmediate = false;
i18n i18n

View file

@ -1,19 +1,32 @@
import React from 'react'; import React from 'react';
import { Circle } from 'react-leaflet'; import { Circle } from 'react-leaflet';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import FirePopup from './FirePopup';
const FireCircleMark = ({ const FireCircleMark = ({
lat, lat,
lon, lon,
scan nasa,
scan,
id,
when,
history,
t
}) => ( }) => (
<Circle center={[lat, lon]} color="red" stroke={false} fillOpacity="1" fill radius={scan * 1000} /> <Circle center={[lat, lon]} color="red" stroke={false} fillOpacity="1" fill radius={scan * 1000}>
<FirePopup t={t} history={history} id={id} nasa={nasa} lat={lat} lon={lon} when={when} />
</Circle>
); );
FireCircleMark.propTypes = { FireCircleMark.propTypes = {
scan: PropTypes.number.isRequired, scan: PropTypes.number.isRequired,
lat: PropTypes.number.isRequired, lat: PropTypes.number.isRequired,
lon: PropTypes.number.isRequired lon: PropTypes.number.isRequired,
nasa: PropTypes.bool.isRequired,
id: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
when: PropTypes.instanceOf(Date).isRequired,
t: PropTypes.func.isRequired
}; };
export default FireCircleMark; export default FireCircleMark;

View file

@ -3,15 +3,25 @@ import React from 'react';
import { CircleMarker, Marker } from 'react-leaflet'; import { CircleMarker, Marker } from 'react-leaflet';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { fireIcon, nFireIcon } from '/imports/ui/components/Maps/Icons'; import { fireIcon, nFireIcon } from '/imports/ui/components/Maps/Icons';
import { translate } from 'react-i18next';
import FirePopup from './FirePopup';
const FireIconMark = ({ const FireIconMark = ({
lat, lat,
lon, lon,
nasa nasa,
id,
history,
when,
t
}) => ( }) => (
<div> <div>
<Marker position={[lat, lon]} icon={nasa ? fireIcon : nFireIcon} /> <Marker position={[lat, lon]} icon={nasa ? fireIcon : nFireIcon}>
<CircleMarker center={[lat, lon]} color={nasa ? 'red' : '#D35400'} stroke={false} fillOpacity="1" fill radius={1} /> <FirePopup t={t} history={history} id={id} nasa={nasa} lat={lat} lon={lon} when={when} />
</Marker>
<CircleMarker center={[lat, lon]} color={nasa ? 'red' : '#D35400'} stroke={false} fillOpacity="1" fill radius={1}>
<FirePopup t={t} history={history} id={id} nasa={nasa} lat={lat} lon={lon} when={when} />
</CircleMarker>
</div> </div>
); );
@ -19,7 +29,11 @@ FireIconMark.propTypes = {
// https://github.com/PaulLeCam/react-leaflet/tree/master/src/propTypes // https://github.com/PaulLeCam/react-leaflet/tree/master/src/propTypes
lat: PropTypes.number.isRequired, lat: PropTypes.number.isRequired,
lon: PropTypes.number.isRequired, lon: PropTypes.number.isRequired,
nasa: PropTypes.bool.isRequired nasa: PropTypes.bool.isRequired,
id: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
when: PropTypes.instanceOf(Date).isRequired,
t: PropTypes.func.isRequired
}; };
export default FireIconMark; export default translate([], { wait: true })(FireIconMark);

View file

@ -9,18 +9,18 @@ import FirePixel from '/imports/ui/components/Maps/FirePixel';
export default function FireList(props) { export default function FireList(props) {
const { const {
fires, scale, useMarkers, nasa fires, scale, useMarkers, nasa, t, history
} = props; } = props;
const useMarks = useMarkers && scale; const useMarks = useMarkers && scale;
const usePixel = !nasa || !scale; const usePixel = !nasa || !scale;
/* console.log(`Using marks: ${useMarks}, using pixels: ${usePixel}`); */ /* console.log(`Using marks: ${useMarks}, using pixels: ${usePixel}`); */
let items; let items;
if (useMarks) { if (useMarks) {
items = fires.map(({ _id, ...otherProps }) => (<FireIconMark key={_id} nasa={nasa} {...otherProps} />)); items = fires.map(({ _id, ...otherProps }) => (<FireIconMark t={t} history={history} id={_id} key={_id} nasa={nasa} {...otherProps} />));
} else if (usePixel) { } else if (usePixel) {
items = fires.map(({ _id, ...otherProps }) => (<FirePixel key={_id} nasa={nasa} {...otherProps} />)); items = fires.map(({ _id, ...otherProps }) => (<FirePixel key={_id} nasa={nasa} {...otherProps} />));
} else { } else {
items = fires.map(({ _id, ...otherProps }) => (<FireCircleMark key={_id} nasa={nasa} {...otherProps} />)); items = fires.map(({ _id, ...otherProps }) => (<FireCircleMark t={t} history={history} id={_id} key={_id} nasa={nasa} {...otherProps} />));
} }
return (<div style={{ display: 'none' }}>{items}</div>); return (<div style={{ display: 'none' }}>{items}</div>);
} }
@ -29,5 +29,7 @@ FireList.propTypes = {
fires: PropTypes.array.isRequired, fires: PropTypes.array.isRequired,
scale: PropTypes.bool.isRequired, scale: PropTypes.bool.isRequired,
useMarkers: PropTypes.bool.isRequired, useMarkers: PropTypes.bool.isRequired,
nasa: PropTypes.bool.isRequired nasa: PropTypes.bool.isRequired,
history: PropTypes.object.isRequired,
t: PropTypes.func.isRequired
}; };

View file

@ -0,0 +1,43 @@
/* eslint-disable import/no-absolute-path */
import React, { Fragment } from 'react';
import { Popup, Tooltip } from 'react-leaflet';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import moment from 'moment';
const FirePopup = ({
lat,
lon,
nasa,
id,
history,
when,
t
}) => (
<Fragment>
<Popup className="fire-popup">
<Fragment>
<span>{t('Coordenadas:')} {lat}, {lon}</span><br />
<span>{t('Fuente')}: {t(nasa ? 'NASA' : 'nuestros usuarios/as')}</span><br />
<span>{t('Detectado')}: {moment(when).fromNow()}</span><br />
<span>
<a href="#" onClick={() => history.push(`/fire/active/${id}`)}>{t('Más información sobre este fuego')}</a>
</span>
</Fragment>
</Popup>
<Tooltip><Fragment>{t('Pulsa para más información')}</Fragment></Tooltip>
</Fragment>
);
FirePopup.propTypes = {
// https://github.com/PaulLeCam/react-leaflet/tree/master/src/propTypes
lat: PropTypes.number.isRequired,
lon: PropTypes.number.isRequired,
nasa: PropTypes.bool.isRequired,
id: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
when: PropTypes.instanceOf(Date).isRequired,
t: PropTypes.func.isRequired
};
export default translate([], { wait: true })(FirePopup);

View file

@ -83,6 +83,7 @@ const App = props => (
<Authenticated exact path="/profile" component={Profile} {...props} /> <Authenticated exact path="/profile" component={Profile} {...props} />
<Authenticated exact path="/status" component={Status} {...props} /> <Authenticated exact path="/status" component={Status} {...props} />
<Route path="/fires" component={FiresMap} {...props} /> <Route path="/fires" component={FiresMap} {...props} />
<Route path="/fire/:type(active|archive)/:id" component={Fires} {...props} />
<Route path="/fire/:id" component={Fires} {...props} /> <Route path="/fire/:id" component={Fires} {...props} />
<Public path="/auth/:token" component={Auth} {...props} /> <Public path="/auth/:token" component={Auth} {...props} />
<Public path="/signup" component={Signup} {...props} /> <Public path="/signup" component={Signup} {...props} />

View file

@ -34,6 +34,10 @@ class Fire extends React.Component {
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
if (this.props.when !== nextProps.when || this.props.loading !== nextProps.loading || this.props.notfound !== nextProps.notfound) { if (this.props.when !== nextProps.when || this.props.loading !== nextProps.loading || this.props.notfound !== nextProps.notfound) {
// console.log(`Next when ${nextProps.when}`); // console.log(`Next when ${nextProps.when}`);
if (nextProps.fire && (nextProps.active || nextProps.fromHash)) {
// change url to archive with new _id
nextProps.history.replace(`/fire/archive/${nextProps.fire._id}`);
}
this.setState({ this.setState({
loading: nextProps.loading, loading: nextProps.loading,
notfound: nextProps.notfound, notfound: nextProps.notfound,
@ -124,7 +128,7 @@ class Fire extends React.Component {
{Object.keys(FalsePositiveTypes).map(key => ( {Object.keys(FalsePositiveTypes).map(key => (
<button <button
className="dropdown-item" className="dropdown-item"
onClick={() => this.onTypeSelect(key)} onClick={() => this.onTypeSelect(key)}
key={key} key={key}
type="button" type="button"
> >
@ -161,8 +165,11 @@ class Fire extends React.Component {
Fire.propTypes = { Fire.propTypes = {
t: PropTypes.func.isRequired, t: PropTypes.func.isRequired,
history: PropTypes.object.isRequired,
loading: PropTypes.bool.isRequired, loading: PropTypes.bool.isRequired,
notfound: PropTypes.bool.isRequired, notfound: PropTypes.bool.isRequired,
fromHash: PropTypes.bool.isRequired,
active: PropTypes.bool.isRequired,
when: PropTypes.instanceOf(Date), when: PropTypes.instanceOf(Date),
fire: PropTypes.object fire: PropTypes.object
}; };
@ -173,8 +180,24 @@ Fire.defaultProps = {
// export default translate([], { wait: true })(withTracker((props) => { // export default translate([], { wait: true })(withTracker((props) => {
const FireContainer = withTracker(({ match }) => { const FireContainer = withTracker(({ match }) => {
const fireEncrypt = match.params.id; const id = match.params.id;
const subscription = Meteor.subscribe('fireFromHash', fireEncrypt); const fireType = match.params.type;
let subscription;
const active = fireType === 'active';
const archive = fireType === 'archive';
let fromHash = false;
if (active) {
subscription = Meteor.subscribe('fireFromActiveId', id);
} else if (archive) {
subscription = Meteor.subscribe('fireFromId', id);
} else {
console.log('Seems a fire from enc hash');
fromHash = true;
subscription = Meteor.subscribe('fireFromHash', id);
}
// console.log(`Type of '${fireType}' fire, active: ${active}, archive: ${archive}, fromHash: ${fromHash}`);
// console.log(`Subs ready: ${subscription.ready()}, fire: ${JSON.stringify(FiresCollection.findOne())}`); // console.log(`Subs ready: ${subscription.ready()}, fire: ${JSON.stringify(FiresCollection.findOne())}`);
const loading = !subscription.ready(); const loading = !subscription.ready();
const notfound = !loading && FiresCollection.find().count() === 0; const notfound = !loading && FiresCollection.find().count() === 0;
@ -182,6 +205,8 @@ const FireContainer = withTracker(({ match }) => {
* console.log(`Not found fire: ${notfound}`); */ * console.log(`Not found fire: ${notfound}`); */
return { return {
loading, loading,
active,
fromHash,
fire: FiresCollection.findOne(), fire: FiresCollection.findOne(),
notfound, notfound,
when: subscription.ready() && FiresCollection.findOne() ? FiresCollection.findOne().when : null when: subscription.ready() && FiresCollection.findOne() ? FiresCollection.findOne().when : null

View file

@ -211,12 +211,16 @@ class FiresMap extends React.Component {
{!this.props.loading && {!this.props.loading &&
<Fragment> <Fragment>
<FireList <FireList
t={t}
history={this.props.history}
fires={this.props.activefires} fires={this.props.activefires}
scale={this.state.viewport.zoom >= MAXZOOM} scale={this.state.viewport.zoom >= MAXZOOM}
useMarkers={this.state.useMarkers} useMarkers={this.state.useMarkers}
nasa nasa
/> />
<FireList <FireList
t={t}
history={this.props.history}
fires={this.props.firealerts} fires={this.props.firealerts}
scale={false} scale={false}
useMarkers={this.state.useMarkers} useMarkers={this.state.useMarkers}
@ -250,6 +254,7 @@ FiresMap.propTypes = {
activefirestotal: PropTypes.number.isRequired, activefirestotal: PropTypes.number.isRequired,
center: PropTypes.arrayOf(PropTypes.number), center: PropTypes.arrayOf(PropTypes.number),
zoom: PropTypes.number, zoom: PropTypes.number,
history: PropTypes.object.isRequired,
t: PropTypes.func.isRequired t: PropTypes.func.isRequired
}; };

View file

@ -42,12 +42,12 @@ class Index extends Component {
this.myScaleFunction(); this.myScaleFunction();
} }
getMap(isMobile) { getMap(isMobile, props) {
if (!isMobile) { if (!isMobile) {
return ( return (
<section className="sect5"> <section className="sect5">
<div className="container"> <div className="container">
<FiresMap /> <FiresMap {...props} />
</div> </div>
</section>); </section>);
} }
@ -233,7 +233,7 @@ class Index extends Component {
</div> </div>
</section> </section>
{this.getMap(isAnyMobile)} {this.getMap(isAnyMobile, this.props)}
<section className="platf sect6"> <section className="platf sect6">
<div className="container"> <div className="container">

View file

@ -40,3 +40,13 @@ h4.page-header {
#react-root > div > div.container > div > div.alert { #react-root > div > div.container > div > div.alert {
margin-top: 20px; margin-top: 20px;
} }
div.leaflet-pane.leaflet-popup-pane > div > div.leaflet-popup-content-wrapper > div > p,
div.leaflet-pane.leaflet-popup-pane > div > div.leaflet-popup-content-wrapper > div > p > a {
font-size: 12px;
}
div.leaflet-pane.leaflet-popup-pane > div > div.leaflet-popup-content-wrapper > div > p > a:hover,
div.leaflet-pane.leaflet-popup-pane > div > div.leaflet-popup-content-wrapper > div > p > a {
// color: $todos-palette2;
}

View file

@ -169,5 +169,11 @@
"Fuego en La Tuna, Los Ángeles, Estados Unidos, septiembre de 2017": "Fuego en La Tuna, Los Ángeles, Estados Unidos, septiembre de 2017":
"La Tuna fire, LA, United States, September 2017", "La Tuna fire, LA, United States, September 2017",
"Polución en la ciudad de Almaty, Kazakhstan, enero de 2014": "Polución en la ciudad de Almaty, Kazakhstan, enero de 2014":
"Smog over Almaty city, Kazakhstan, January 2014" "Smog over Almaty city, Kazakhstan, January 2014",
"Fuente": "Source",
"NASA": "NASA",
"nuestros usuarios/as": "our users",
"Pulsa para más información": "Click for more information",
"Detectado":
"Detected"
} }

View file

@ -261,5 +261,11 @@
"Fuego en La Tuna, Los Ángeles, Estados Unidos, septiembre de 2017": "Fuego en La Tuna, Los Ángeles, Estados Unidos, septiembre de 2017":
"Fuego en La Tuna, Los Ángeles, Estados Unidos, septiembre de 2017", "Fuego en La Tuna, Los Ángeles, Estados Unidos, septiembre de 2017",
"Polución en la ciudad de Almaty, Kazakhstan, enero de 2014": "Polución en la ciudad de Almaty, Kazakhstan, enero de 2014":
"Polución en la ciudad de Almaty, Kazakhstan, enero de 2014" "Polución en la ciudad de Almaty, Kazakhstan, enero de 2014",
"Fuente": "Fuente",
"NASA": "NASA",
"nuestros usuarios/as": "nuestros usuarios/as",
"Pulsa para más información": "Pulsa para más información",
"Detectado":
"Detectado"
} }