diff --git a/.gitignore b/.gitignore index eeb3622..603fb12 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,8 @@ public/locales/undefined/ .screenshots .vscode output.json + +# fase-0 higiene: variantes de settings con secretos (no versionar) +settings-production-*.json +settings-tests.json +*.json~ diff --git a/README.md b/README.md index e686902..4417536 100644 --- a/README.md +++ b/README.md @@ -33,14 +33,19 @@ TEST_PORT=3000 TEST_WATCH=1 TEST_CLIENT=0 MONGO_URL=mongodb://localhost:27017/fu # and -chimp --watch --ddp=http://localhost:3000 --path=cucumber +node_modules/.bin/chimp --watch --ddp=http://localhost:3000 --path=cucumber # and -chimp --ddp=http://localhost:3000 --path=cucumber +node_modules/.bin/chimp --ddp=http://localhost:3000 --path=cucumber ``` +### FAQ & Troubleshooting + +**Q** - I get something like `(...) /node_modules/fibers/future.js:280 (...) Error: Could not locate the bindings file.` What can I do? +**A** - Try `meteor npm rebuild` + ## Data source acknowledgements *We acknowledge the use of data and imagery from LANCE FIRMS operated by the NASA/GSFC/Earth Science Data and Information System (ESDIS) with funding provided by NASA/HQ*. @@ -51,4 +56,5 @@ GNU APLv3. See our [LICENSE](https://github.com/comunes/todos-contra-el-fuego-we ## Thanks & other acknowlegments -(...) +Thanks indeed to: +- Lupe Bao Reixa for Galician translation diff --git a/cucumber/chimp-config.js b/cucumber/chimp-config.js new file mode 100644 index 0000000..7ec19dc --- /dev/null +++ b/cucumber/chimp-config.js @@ -0,0 +1,29 @@ +module.exports = { + // - - - - CHIMP - - - - + /* watch: false, + * watchTags: '@watch', + * offline: false, + */ + // - - - - CUCUMBER - - - - + /* path: './features', + * jsonOutput: 'output.json', */ + + // '- - - - DEBUGGING - - - - + log: 'info', + debug: false, + seleniumDebug: false, + webdriverLogLevel: false, + // debugBrkCucumber: 5858, + // - - - - WEBDRIVER-IO - - - - + webdriverio: { + waitforTimeout: 10000, + waitforInterval: 250, // KEEP SMALL (!!!) this is the INTERVAL in which waitFor* is looped fo + desiredCapabilities: { + chromeOptions: { + // args: ["headless", "disable-gpu"] + args: ['--disable-gpu', '--no-sandbox', '--headless'] + }, + isHeadless: true + } + } +}; diff --git a/cucumber/features/pages.feature b/cucumber/features/pages.feature index fd2193b..0807c2c 100644 --- a/cucumber/features/pages.feature +++ b/cucumber/features/pages.feature @@ -40,7 +40,6 @@ Feature: Test all secundary pages | fire/Fe26.2**1a0361ed0384f741403682e26b4bbc3850bee24e775da13c9782b22365ea895f*r_3gmVad5vzkeyqPpo6UcA*1s5fFz3iDGKTYP2RCvXMshof00QCHf4ErDl9K2dxoX0u-J-t6scyOWG8pGp3ehg_FfEtyR_kYcEKU3rE0jaSlZbD09TIvhiIJeS3C6Uc8YD-rit0XBrgsVKfYSxKzTRoOYiYFJ8JYd298hMtfiASePjS05Z58hhicyCcJYYRlarqDScG3LiVY3lL5y2nfcdIMNuSjCiKOJWuMkxwd9nR1UHMudLl0hEoy56mPdnHpDYtP9IYUlIOk1LlWBxcmHKifbXeqHu94p8j13Kk20dh2R49Hw3KsSoE9UbWmGQA9wAZXT82301i3rGF5GPAKjlTlRYcWisQurnPwHSVmx3DhUdiYwKGxt4KeaM5QVI4BE9octvE41OOprB_-Il105diQEh2Y9vdvX51ZVWIRfCboICPM6rJb0Oin7U7F1iM-oD_5s3DGnelfM5LGBcKwiB5paMo5M5vdBMaO-zR216cW9yGVXw9IZqHx8xDQWnoHAZjt8NLHeiGF2QOmIGtEUH7qnwhGpkcvszajmAZzR8saZgoH1qfBfvpVA41YfV14gU**4d030f05e23ad75409cebc609107467fc60be5077d52cf041087cd024fc4dc45*GY97aGFc1MyAsoO3Qxqtgwk9j-MbAPdEGBmEHq6r8VU | Additional information | false | Then I check that all page urls works properly - @watch Scenario: Check that other non visible pages work well Given I logout if logged And a list of non visible pages ids and contents diff --git a/imports/api/ActiveFires/ActiveFires.js b/imports/api/ActiveFires/ActiveFires.js index 79437af..16e2760 100644 --- a/imports/api/ActiveFires/ActiveFires.js +++ b/imports/api/ActiveFires/ActiveFires.js @@ -1,5 +1,6 @@ /* eslint-disable consistent-return */ +import { Meteor } from 'meteor/meteor'; import { Mongo } from 'meteor/mongo'; import SimpleSchema from 'simpl-schema'; import firesCommonSchema from '../Common/FiresSchema'; @@ -18,8 +19,11 @@ ActiveFires.deny({ remove: () => true }); +const activeFiresSchema = firesCommonSchema; -ActiveFires.schema = new SimpleSchema(firesCommonSchema); +activeFiresSchema.fireUnion = { type: Meteor.Collection.ObjectID, optional: true, blackbox: true }; + +ActiveFires.schema = new SimpleSchema(activeFiresSchema); ActiveFires.attachSchema(ActiveFires.schema); diff --git a/imports/api/ActiveFires/server/countFires.js b/imports/api/ActiveFires/server/countFires.js index 39008da..b3b8511 100644 --- a/imports/api/ActiveFires/server/countFires.js +++ b/imports/api/ActiveFires/server/countFires.js @@ -32,7 +32,9 @@ const findFiresInRegion = (zone) => { export const countRealFires = (firesCursor) => { const realFires = []; firesCursor.forEach((fire) => { + if (debug) console.log(`${JSON.stringify(fire)} -----`); const union = firesUnion([fire]); + if (debug) console.log(`${JSON.stringify(union)} -----`); const falsePos = whichAreFalsePositives(FalsePositives, union); const industries = whichAreFalsePositives(Industries, union); if (falsePos.count() === 0 && industries.count() === 0) { diff --git a/imports/api/ActiveFires/server/publications.js b/imports/api/ActiveFires/server/publications.js index 21e4e5a..03878b3 100644 --- a/imports/api/ActiveFires/server/publications.js +++ b/imports/api/ActiveFires/server/publications.js @@ -31,7 +31,8 @@ const activefires = (northEastLng, northEastLat, southWestLng, southWestLat, wit lat: 1, lon: 1, when: 1, - scan: 1 + scan: 1, + track: 1 } }); diff --git a/imports/api/ActiveFiresUnion/ActiveFiresUnion.js b/imports/api/ActiveFiresUnion/ActiveFiresUnion.js new file mode 100644 index 0000000..2437e54 --- /dev/null +++ b/imports/api/ActiveFiresUnion/ActiveFiresUnion.js @@ -0,0 +1,71 @@ +/* eslint-disable consistent-return */ +/* eslint-disable import/no-absolute-path */ + +import { Mongo } from 'meteor/mongo'; +import SimpleSchema from 'simpl-schema'; +import LocationSchema from '/imports/api/Utility/LocationSchema.js'; +import { defaultCreatedAt, defaultUpdateAt } from '/imports/api/Utility/Utils.js'; + +const ActiveFiresUnion = new Mongo.Collection('activefiresunion', { idGeneration: 'MONGO' }); + +/* Sample: + * + * TODO + * + * */ + +// We have to store polygons and their centroids +// {"type":"Polygon","coordinates":[[[-5.8645,43.5524],[-5.8621,43.5524],[-5.8621,43.5546],[-5.8645,43.5546],[-5.8645,43.5524]]]} + +ActiveFiresUnion.allow({ + insert: () => false, + update: () => false, + remove: () => false +}); + +ActiveFiresUnion.deny({ + insert: () => true, + update: () => true, + remove: () => true +}); + +// https://www.npmjs.com/package/simpl-schema +const GeoJsonSchema = new SimpleSchema({ + type: { + type: String, + allowedValues: ['Polygon'] + }, + coordinates: { + type: Array + }, + 'coordinates.$': { + type: Array + }, + 'coordinates.$.$': { + type: Array + }, + 'coordinates.$.$.$': { + type: Number + } +}); + +const firesUnionSchema = { + centerid: LocationSchema, + + // https://docs.mongodb.com/manual/reference/geojson/ + // : { type: , coordinates: } + shape: GeoJsonSchema, + history: { type: Array, optional: true }, + 'history.$': GeoJsonSchema, + + when: Date, // First date + + createdAt: defaultCreatedAt, + updatedAt: defaultUpdateAt +}; + +ActiveFiresUnion.schema = new SimpleSchema(firesUnionSchema); + +ActiveFiresUnion.attachSchema(ActiveFiresUnion.schema); + +export default ActiveFiresUnion; diff --git a/imports/api/ActiveFiresUnion/server/publications.js b/imports/api/ActiveFiresUnion/server/publications.js new file mode 100644 index 0000000..6a126d8 --- /dev/null +++ b/imports/api/ActiveFiresUnion/server/publications.js @@ -0,0 +1,80 @@ +/* global Counter */ +/* eslint-disable import/no-absolute-path */ +/* eslint-disable prefer-arrow-callback */ + +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; +import { NumberBetween } from '/imports/modules/server/other-checks'; +// import { whichAreFalsePositives, firesUnion } from '/imports/api/FalsePositives/server/publications'; +// import FalsePositives from '/imports/api/FalsePositives/FalsePositives'; +// import Industries from '/imports/api/Industries/Industries'; +import ActiveFiresUnion from '../ActiveFiresUnion'; + +const counter = new Counter('countActiveFiresUnion', ActiveFiresUnion.find({})); + +Meteor.publish('activefiresuniontotal', function total() { + return counter; +}); + +const activeFiresUnion = (b1, b2, c1, c2, withMarks) => { + // a --- b + // c --- d + const geometry = { + $geometry: { + type: 'Polygon', + coordinates: [[ + [c1, c2], + [c1, b2], + [b1, b2], + [b1, c2], + [c1, c2] + ]] + } + }; + // console.log(JSON.stringify(geometry)); + const fires = ActiveFiresUnion.find({ + shape: { + $geoIntersects: geometry + } + }, { + fields: { + shape: 1, + centerid: 1, + when: 1, + createdAt: 1, + updatedAt: 1 + } + }); + + if (Meteor.isDevelopment) console.log(`Active fires union total: ${fires.count()}`); + + /* + if (withMarks && fires.fetch().length > 0) { + const union = firesUnion(fires); + const falsePos = whichAreFalsePositives(FalsePositives, union); + const industries = whichAreFalsePositives(Industries, union); + return [fires, falsePos, industries]; + } */ + + return fires; +}; + +Meteor.publish('activefiresunionmyloc', function activeInMyLoc(northEastLng, northEastLat, southWestLng, southWestLat, withMarks) { + // latitude -90 and 90 and the longitude between -180 and 180 + check(northEastLng, NumberBetween(-180, 180)); + check(southWestLat, NumberBetween(-90, 90)); + check(southWestLng, NumberBetween(-180, 180)); + check(northEastLat, NumberBetween(-90, 90)); + check(withMarks, Boolean); + + // I use this for fire stats + // if (!Meteor.isDevelopment) return this.ready(); // empty + return activeFiresUnion(northEastLng, northEastLat, southWestLng, southWestLat, withMarks); +}); + +// Warning: this increase always by one the fire stats +Meteor.publish('lastFireUnionDetected', function lastFireDetected() { + // I use this for fire stats + // if (!Meteor.isDevelopment) return this.ready(); // empty + return ActiveFiresUnion.find({}, { limit: 1, sort: { when: -1 } }); +}); diff --git a/imports/api/FalsePositives/server/publications.js b/imports/api/FalsePositives/server/publications.js index 1816344..a79ae1a 100644 --- a/imports/api/FalsePositives/server/publications.js +++ b/imports/api/FalsePositives/server/publications.js @@ -5,7 +5,8 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; import { NumberBetween } from '/imports/modules/server/other-checks'; -import L from 'leaflet-headless'; +import '/imports/startup/server/leaflet-workaround.js'; +import L from 'leaflet'; import calcUnion from '/imports/ui/components/Maps/SubsUnion/Unify'; import Industries from '/imports/api/Industries/Industries'; import FalsePositives from '../FalsePositives'; @@ -17,20 +18,26 @@ Meteor.publish('falsePositivesTotal', function total() { }); export const firesUnion = (fires) => { - const group = new L.FeatureGroup(); const firesArray = Array.isArray(fires) ? fires : fires.fetch(); // if not is a cursor const remap = firesArray.map(function remap(doc) { - // default scan: 1 for neightbor alerts - return { location: { lat: doc.lat, lon: doc.lon }, distance: doc.scan || 1 }; + const isNASA = doc.type === 'modis' || doc.type === 'viirs'; + const pixelSize = doc.type === 'viirs' ? 0.375 : 1; // viirs has 375m pixel size, modis 1000m + // default 1 km for neightbor alerts + const map = { + location: { lat: doc.lat, lon: doc.lon }, + distance: isNASA ? doc.scan * pixelSize : 1, + distanceY: isNASA ? doc.track * pixelSize : 1 + }; + // console.log(map); + return map; }); - const union = calcUnion(remap, group, sub => sub); + const union = calcUnion(L, remap, sub => sub, false); return union; }; export const zoneToUnion = (lat, lon, distance) => { - const group = new L.FeatureGroup(); const remap = [{ location: { lat, lon }, distance }]; - const union = calcUnion(remap, group, sub => sub); + const union = calcUnion(L, remap, sub => sub, true); return union; }; diff --git a/imports/api/FireAlerts/server/publications.js b/imports/api/FireAlerts/server/publications.js index b0e9412..6642f35 100644 --- a/imports/api/FireAlerts/server/publications.js +++ b/imports/api/FireAlerts/server/publications.js @@ -33,7 +33,8 @@ Meteor.publish('fireAlerts', function fireAlerts(northEastLng, northEastLat, sou fields: { lat: 1, lon: 1, - scan: 1 + scan: 1, + track: 1 } }); if (Meteor.isDevelopment) console.log(`Neighbour alerts total: ${fires.count()}`); diff --git a/imports/api/Fires/server/publications.js b/imports/api/Fires/server/publications.js index acb4d1e..3862beb 100644 --- a/imports/api/Fires/server/publications.js +++ b/imports/api/Fires/server/publications.js @@ -147,7 +147,7 @@ export function fireFromHash(fireEnc, params) { // const unsealed = Promise.await(urlEnc.decrypt(fireEnc)); const unsealed = Promise.await(unsealW(fireEnc)); if (unsealed === undefined) { - throw Error('Undefined unsealed'); + throw Error(`Fail to unseal '${fireEnc}'`); } const w = unsealed.when; unsealed.when = new Date(w); diff --git a/imports/api/Rest/Rest.js b/imports/api/Rest/Rest.js index dc47750..ae5e383 100644 --- a/imports/api/Rest/Rest.js +++ b/imports/api/Rest/Rest.js @@ -18,6 +18,7 @@ import { Random } from 'meteor/random'; import { subscriptionsInsert } from '/imports/api/Subscriptions/methods.js'; import { Mongo } from 'meteor/mongo'; import { upsertFalsePositive } from '/imports/api/FalsePositives/methods.js'; + const debug = false; const uptime = new Date(); @@ -156,11 +157,14 @@ if (!Meteor.settings.private.internalApiToken) { lat: 1, lon: 1, when: 1, - scan: 1 + type: 1, // modis, viirs, vecinal + scan: 1, + track: 1 } }); const result = { total: fires.count(), real: countRealFires(fires) }; + if (debug) console.log(`Query for fires in ${lat}, ${lng} in ${km} km radius ${result}`); if (!full) { return result; @@ -439,11 +443,15 @@ if (!Meteor.settings.private.internalApiToken) { const user = Meteor.users.findOne({ fireBaseToken: mobileToken }); if (!user) return failMsg('User not found'); - // TODO - const fire = fireFromHash(sealed, {}); - console.log(`Marking fire as false positive: ${JSON.stringify(fire)}`); - const result = upsertFalsePositive(type, user._id, fire); - return jsend.success({ upsert: result }); + console.log(`Marking hash fire as '${type}' false positive: ${sealed}`); + const fireC = fireFromHash(sealed, { id: sealed }); + if (fireC && typeof fireC === 'object') { + const fire = fireC.fetch()[0]; + console.log(`Marking fire as false positive: ${fire._id}`); + const result = upsertFalsePositive(type, user._id, fire); + return jsend.success({ upsert: result }); + } + return failMsg('Cannot mark fire as false positive'); } }); } diff --git a/imports/modules/server/notificationsProcess.js b/imports/modules/server/notificationsProcess.js new file mode 100644 index 0000000..657aa2a --- /dev/null +++ b/imports/modules/server/notificationsProcess.js @@ -0,0 +1,153 @@ +/* eslint-disable import/no-absolute-path */ + +import { Meteor } from 'meteor/meteor'; +import Notifications from '/imports/api/Notifications/Notifications'; +import i18n from 'i18next'; +import moment from 'moment'; +import { dateLongFormat } from '/imports/api/Common/dates'; +// import sendMail from '/imports/startup/server/email'; +import sendEmail, { subjectTruncate } from '/imports/modules/server/send-email'; +import { isMailServerMaster } from '/imports/startup/server/email'; +// import { hr } from '/imports/startup/server/email'; +import getEmailOf from '/imports/modules/get-email-of-user'; +import image from 'google-maps-image-api-url'; +import gcm from 'node-gcm'; +import { trim } from '/imports/ui/components/NotificationsObserver/util.js'; +import ravenLogger from '/imports/startup/server/ravenLogger'; + +let validFcmSender = true; + +if (!(Meteor.settings.private.fcmApiToken && Meteor.settings.private.fcmApiToken.length > 0)) { + console.warn('Missing settings.private.fcmApiToken key, mobile notifications will not work'); + validFcmSender = false; +} + +// https://www.npmjs.com/package/google-maps-image-api-url +// https://stackoverflow.com/questions/24355007/is-there-no-way-to-embed-a-google-map-into-an-html-email +// https://developers.google.com/maps/documentation/static-maps/intro +function imgUrl(lat, lng) { + return image({ + key: Meteor.settings.gmaps.key, + type: 'staticmap', + center: `${lat},${lng}`, + size: '640x480', + zoom: 16, + maptype: 'hybrid', + language: 'es', + markers: `icon: ${Meteor.settings.private.fireIconUrl}|${lat},${lng}` + }); +} + +/* + function imgEl(lat, lng) { + return ``; + } */ + +// Cutover flag (fase 1a): channels listed in +// Meteor.settings.private.notifDisabledChannels (e.g. ['mobile','web']) are +// handled by the tcef-notifications microservice, so the old observer/cron must +// NOT send them. Mutual exclusion per channel — reversible by editing settings, +// no code deploy needed. ES5-compatible on purpose (Meteor 1.6 / Node 8). +const notifDisabledChannels = (Meteor.settings.private && Meteor.settings.private.notifDisabledChannels) || []; + +const processNotif = (notif) => { + if (notifDisabledChannels.indexOf(notif.type) !== -1) { + // This channel was migrated to tcef-notifications; do nothing here. + return; + } + if (isMailServerMaster && validFcmSender && notif.type === 'mobile' && notif.notified !== true) { + const fcmSender = new gcm.Sender(Meteor.settings.private.fcmApiToken); + const user = Meteor.users.findOne({ _id: notif.userId }); + moment.locale(user.lang); + // duplicate code below + const body = `${trim(notif.content)}`; + + // https://firebase.google.com/docs/cloud-messaging/concept-options + const msg = new gcm.Message(); + + msg.addNotification({ + title: i18n.t('Alerta de fuego'), + body, + click_action: 'FLUTTER_NOTIFICATION_CLICK', + tag: notif._id, // prevent duplication of fire notifications + sound: 'default', // Indicates sound to be played. Supports only default currently. + icon: 'launch_image' // 'ic_launcher' + }); + + msg.addData('id', notif._id._str); + msg.addData('description', body); + msg.addData('lat', notif.geo.coordinates[1]); + msg.addData('lon', notif.geo.coordinates[0]); + msg.addData('when', notif.when); + msg.addData('subsId', notif.subsId._str); + msg.addData('sealed', notif.sealed); + + const registrationTokens = []; + if (!user.fireBaseToken) { + console.warn('This mobile user doesn\'t have a firebase registration token'); + } else { + registrationTokens.push(user.fireBaseToken); + // FIXME: better join users + if (validFcmSender) { + fcmSender.send(msg, { registrationTokens }, Meteor.bindEnvironment(function processResult(err, response) { + if (err) { + console.error(`FCM error: ${err}`); + // FIXME send to sentry + ravenLogger.log(err); + } else { + // console.log(`FCM response: ${response}`); + Notifications.update(notif._id, { $set: { notified: true, notifiedAt: new Date() } }); + } + })); + } + } + } + if (isMailServerMaster && notif.type === 'web' && notif.emailNotified !== true) { + const user = Meteor.users.findOne({ _id: notif.userId }); + const { firstName, emailAddress } = getEmailOf(user); + + if (emailAddress) { + const img = imgUrl(notif.geo.coordinates[1], notif.geo.coordinates[0]); + // const url = imgUrl(notif.geo.coordinates[1], notif.geo.coordinates[0]); + const fireUrl = `${Meteor.absoluteUrl('fire/')}${notif.sealed}`; + // const fireHtmlUrl = `${i18n.t('Más información sobre este fuego')}`; + // TODO get _id of fire + // const fireTextUrl = `${i18n.t('Más información sobre este fuego')}:\n${fireUrl}`; + // FIXME use our map as url and static map as img + moment.locale(user.lang); + // moment user tz ? + const message = `${trim(notif.content)} (${i18n.t('fireDetectedAt', { when: dateLongFormat(notif.when) })}).`; + + // TODO Comunes Address + + const emailOpts = { + to: emailAddress, + // userName: firstName, + // sendAt: new Date(), + subject: subjectTruncate.apply(message), + // text: `${message}\n\n${fireTextUrl}\n\n`, + // template: '

{{appName}}

{{{html}}}', + lang: user.lang, + template: 'new-fire', + templateVars: { + applicationName: i18n.t('AppName'), + firstName, + message, + fireUrl, + img, + subsUrl: Meteor.absoluteUrl('subscriptions') + } + }; + sendEmail(emailOpts).catch((error) => { + throw new Meteor.Error('500', `${error}`); + }); + // sendMail(emailOpts, true); + Notifications.update(notif._id, { $set: { emailNotified: true, emailNotifiedAt: new Date() } }); + } else { + // Not email or not verified -> remove notif so we don't retry to send it + Notifications.remove({ _id: notif._id }); + } + } +}; + +export default processNotif; diff --git a/imports/startup/client/i18n.js b/imports/startup/client/i18n.js index 2fdc6b4..384053f 100644 --- a/imports/startup/client/i18n.js +++ b/imports/startup/client/i18n.js @@ -17,7 +17,7 @@ import i18nOpts from '../common/i18n'; const detectorOptions = { // order and from where user language should be detected - order: ['querystring', 'cookie', 'localStorage', 'navigator', 'htmlTag'], + order: ['querystring', 'navigator', 'cookie', 'localStorage', 'htmlTag'], // keys or params to lookup language from lookupQuerystring: 'lng', @@ -61,6 +61,14 @@ if (sendMissing && Meteor.isDevelopment) { }; } +function setT9(lang) { + if (lang === 'gl') { + T9n.setLanguage('es'); + } else { + T9n.setLanguage(lang); + } +} + i18n.use(backend) .use(LngDetector) .use(Cache) @@ -75,7 +83,7 @@ i18n.use(backend) // document.title = t('AppName'); // Accounts translation // https://github.com/softwarerero/meteor-accounts-t9n - T9n.setLanguage(i18n.language); + setT9(i18n.language); // console.log(T9n.get('error.accounts.User not found')); moment.locale(i18n.language); @@ -104,7 +112,8 @@ Meteor.subscribe('userData'); // lang is there i18n.on('languageChanged', (lng) => { moment.locale(lng); - T9n.setLanguage(lng); + // TODO fix this when gl is translated + setT9(lng); Tracker.autorun((computation) => { if (Meteor.userId()) { // logged diff --git a/imports/startup/common/i18n.js b/imports/startup/common/i18n.js index 9be09fd..218e46d 100644 --- a/imports/startup/common/i18n.js +++ b/imports/startup/common/i18n.js @@ -3,6 +3,8 @@ import moment from 'moment'; // Load the js langs import es from 'meteor-accounts-t9n/build/es'; import en from 'meteor-accounts-t9n/build/en'; +// TODO ask for translation of this +// import gl from 'meteor-accounts-t9n/build/gl'; const backOpts = { // path where resources get loaded from @@ -21,7 +23,10 @@ const shouldDebug = (forceDebug && !Meteor.isProduction); const i18nOpts = { backend: backOpts, // lng: 'es', - fallbackLng: ['es', 'en'], + fallbackLng: { + gl: ['es'], + default: ['en'] + }, sendMissingTo: 'fallback', interpolation: { escapeValue: false, // not needed for react!! @@ -35,7 +40,7 @@ const i18nOpts = { return value; } }, - whitelist: ['es', 'en'], // allowed languages + whitelist: ['es', 'en', 'gl'], // allowed languages load: 'languageOnly', // 'es' o 'en', previously: 'all', // es-ES -> es, en-US -> en debug: shouldDebug, ns: 'common', diff --git a/imports/startup/server/api.js b/imports/startup/server/api.js index 73f1fa2..a6044b4 100644 --- a/imports/startup/server/api.js +++ b/imports/startup/server/api.js @@ -9,6 +9,7 @@ import '../../api/Users/server/publications'; import '../../api/Utility/server/methods'; import '../../api/ActiveFires/server/publications'; +import '../../api/ActiveFiresUnion/server/publications'; import '../../api/FireAlerts/server/publications'; import '../../api/Subscriptions/methods'; diff --git a/imports/startup/server/cron.js b/imports/startup/server/cron.js index 9b0fa77..843e55b 100644 --- a/imports/startup/server/cron.js +++ b/imports/startup/server/cron.js @@ -4,6 +4,8 @@ import { Meteor } from 'meteor/meteor'; import { tweetIberiaFires, tweetEuropeFires } from '/imports/api/ActiveFires/server/tweetFiresInZone'; import { isMailServerMaster, sendEmailsFromQueue } from '/imports/startup/server/email'; +import Notifications from '/imports/api/Notifications/Notifications'; +import processNotif from '/imports/modules/server/notificationsProcess.js'; // https://github.com/thesaucecode/meteor-synced-cron/ @@ -44,7 +46,7 @@ Meteor.startup(() => { timezone: 'Europe/Madrid', schedule: (parser) => { // http://bunkat.github.io/later/ - const sched = parser.text('every 1 min'); + const sched = parser.text('every 10 min'); if (sched.error !== -1) { console.error(`Mail cron 'when' field parsed with errors: ${sched.error}`); } @@ -52,6 +54,30 @@ Meteor.startup(() => { }, job: () => sendEmailsFromQueue() }); + + SyncedCron.add({ + name: 'Process pending notif', + timezone: 'Europe/Madrid', + schedule: (parser) => { + // http://bunkat.github.io/later/ + const sched = parser.text('every 15 min'); + if (sched.error !== -1) { + console.error(`Mail cron 'when' field parsed with errors: ${sched.error}`); + } + return sched; + }, + job: () => { + const mobileNotif = Notifications.find({ nofitied: null, type: 'mobile' }); + mobileNotif.forEach((notif) => { + processNotif(notif); + }); + const emailNotif = Notifications.find({ emailNofitied: null, type: 'web' }); + emailNotif.forEach((notif) => { + processNotif(notif); + }); + return { mobile: mobileNotif.count(), web: emailNotif.count() }; + } + }); } const esEn = Meteor.settings.private.twitter.es.enabled; diff --git a/imports/startup/server/index.js b/imports/startup/server/index.js index dd4f913..7f84dde 100644 --- a/imports/startup/server/index.js +++ b/imports/startup/server/index.js @@ -17,5 +17,4 @@ import './prerender'; import './feedback'; import './cron'; import './isMaster'; -import './segfaults'; import './rest'; diff --git a/imports/startup/server/leaflet-workaround.js b/imports/startup/server/leaflet-workaround.js new file mode 100644 index 0000000..740aa3b --- /dev/null +++ b/imports/startup/server/leaflet-workaround.js @@ -0,0 +1,19 @@ +// imports/startup/server/leaflet-workaround.js +global.window = { + screen: { + deviceXDPI: 1, + logicalXDPI: 1 + } +}; +global.document = { + documentElement: { + style: {} + }, + createElement: () => { + return {}; + } +}; +global.navigator = { + userAgent: 'foo', + platform: 'bar' +}; diff --git a/imports/startup/server/migrations.js b/imports/startup/server/migrations.js index defc7c1..e3bfac9 100644 --- a/imports/startup/server/migrations.js +++ b/imports/startup/server/migrations.js @@ -10,6 +10,7 @@ import FalsePositives from '/imports/api/FalsePositives/FalsePositives'; import Industries from '/imports/api/Industries/Industries'; import IndustryRegistries from '/imports/api/Industries/IndustryRegistries'; import Notifications from '/imports/api/Notifications/Notifications'; +import ActiveFiresUnion from '/imports/api/ActiveFiresUnion/ActiveFiresUnion'; import { Mongo } from 'meteor/mongo'; Meteor.startup(() => { @@ -240,6 +241,18 @@ Meteor.startup(() => { } }); + Migrations.add({ + version: 18, + up: () => { + const raw = ActiveFiresUnion.rawCollection(); + raw.createIndex({ centerid: '2dsphere' }); + raw.createIndex({ shape: '2dsphere' }); + raw.createIndex({ when: 1 }); + raw.createIndex({ createdAt: 1 }); + raw.createIndex({ updatedAt: 1 }); + } + }); + // Set createdAt in users & subs Migrations.migrateTo('latest'); diff --git a/imports/startup/server/notificationsObserver.js b/imports/startup/server/notificationsObserver.js index 0065e15..70be8e0 100644 --- a/imports/startup/server/notificationsObserver.js +++ b/imports/startup/server/notificationsObserver.js @@ -1,145 +1,19 @@ /* eslint-disable import/no-absolute-path */ -import i18n from 'i18next'; import { Meteor } from 'meteor/meteor'; -import moment from 'moment'; -import { dateLongFormat } from '/imports/api/Common/dates'; import Notifications from '/imports/api/Notifications/Notifications'; -// import sendMail from '/imports/startup/server/email'; -import sendEmail, { subjectTruncate } from '/imports/modules/server/send-email'; +import processNotif from '/imports/modules/server/notificationsProcess.js'; import { isMailServerMaster } from '/imports/startup/server/email'; -// import { hr } from '/imports/startup/server/email'; -import getEmailOf from '/imports/modules/get-email-of-user'; -import image from 'google-maps-image-api-url'; -import gcm from 'node-gcm'; -import { trim } from '/imports/ui/components/NotificationsObserver/util.js'; Meteor.startup(() => { - let validFcmSender = true; - - if (!(Meteor.settings.private.fcmApiToken && Meteor.settings.private.fcmApiToken.length > 0)) { - console.warn('Missing settings.private.fcmApiToken key, mobile notifications will not work'); - validFcmSender = false; - } - - const fcmSender = new gcm.Sender(Meteor.settings.private.fcmApiToken); - - // https://www.npmjs.com/package/google-maps-image-api-url - // https://stackoverflow.com/questions/24355007/is-there-no-way-to-embed-a-google-map-into-an-html-email - // https://developers.google.com/maps/documentation/static-maps/intro - function imgUrl(lat, lng) { - return image({ - key: Meteor.settings.gmaps.key, - type: 'staticmap', - center: `${lat},${lng}`, - size: '640x480', - zoom: 16, - maptype: 'hybrid', - language: 'es', - markers: `icon: ${Meteor.settings.private.fireIconUrl}|${lat},${lng}` + if (isMailServerMaster) { + Notifications.find().observe({ + added: function notifAdded(notif) { + processNotif(notif); + }, + changed: function notifChanged(updatedNotif) { // , oldNotif) { + processNotif(updatedNotif); + } }); } - - /* - function imgEl(lat, lng) { - return ``; - } */ - - function process(notif) { - if (isMailServerMaster && notif.type === 'mobile' && !notif.notified) { - const user = Meteor.users.findOne({ _id: notif.userId }); - moment.locale(user.lang); - // duplicate code below - const body = `${trim(notif.content)}`; - - // https://firebase.google.com/docs/cloud-messaging/concept-options - const msg = new gcm.Message(); - - msg.addNotification({ - title: i18n.t('Alerta de fuego'), - body, - click_action: 'FLUTTER_NOTIFICATION_CLICK', - tag: notif._id, // prevent duplication of fire notifications - sound: 'default', // Indicates sound to be played. Supports only default currently. - icon: 'launch_image' // 'ic_launcher' - }); - - msg.addData('id', notif._id._str); - msg.addData('description', body); - msg.addData('lat', notif.geo.coordinates[1]); - msg.addData('lon', notif.geo.coordinates[0]); - msg.addData('when', notif.when); - msg.addData('subsId', notif.subsId._str); - msg.addData('sealed', notif.sealed); - - const registrationTokens = []; - if (!user.fireBaseToken) { - console.warn('This mobile user doesn\'t have a firebase registration token'); - } else { - registrationTokens.push(user.fireBaseToken); - // FIXME: better join users - if (validFcmSender) { - fcmSender.send(msg, { registrationTokens }, Meteor.bindEnvironment(function processResult(err, response) { - if (err) { - console.error(`FCM error: ${err}`); - } else { - // console.log(`FCM response: ${response}`); - Notifications.update(notif._id, { $set: { notified: true, notifiedAt: new Date() } }); - } - })); - } - } - } else if (notif.type === 'web' && !notif.emailNotified) { - const user = Meteor.users.findOne({ _id: notif.userId }); - const { firstName, emailAddress } = getEmailOf(user); - - if (emailAddress) { - const img = imgUrl(notif.geo.coordinates[1], notif.geo.coordinates[0]); - // const url = imgUrl(notif.geo.coordinates[1], notif.geo.coordinates[0]); - const fireUrl = `${Meteor.absoluteUrl('fire/')}${notif.sealed}`; - // const fireHtmlUrl = `${i18n.t('Más información sobre este fuego')}`; - // TODO get _id of fire - // const fireTextUrl = `${i18n.t('Más información sobre este fuego')}:\n${fireUrl}`; - // FIXME use our map as url and static map as img - moment.locale(user.lang); - // moment user tz ? - const message = `${trim(notif.content)} (${i18n.t('fireDetectedAt', { when: dateLongFormat(notif.when) })}).`; - - // TODO Comunes Address - - const emailOpts = { - to: emailAddress, - // userName: firstName, - // sendAt: new Date(), - subject: subjectTruncate.apply(message), - // text: `${message}\n\n${fireTextUrl}\n\n`, - // template: '

{{appName}}

{{{html}}}', - lang: user.lang, - template: 'new-fire', - templateVars: { - applicationName: i18n.t('AppName'), - firstName, - message, - fireUrl, - img, - subsUrl: Meteor.absoluteUrl('subscriptions') - } - }; - sendEmail(emailOpts).catch((error) => { - throw new Meteor.Error('500', `${error}`); - }); - // sendMail(emailOpts, true); - Notifications.update(notif._id, { $set: { emailNotified: true, emailNotifiedAt: new Date() } }); - } - } - } - - Notifications.find().observe({ - added: function notifAdded(notif) { - process(notif); - }, - changed: function notifChanged(updatedNotif, oldNotif) { - process(updatedNotif); - } - }); }); diff --git a/imports/startup/server/segfaults.js b/imports/startup/server/segfaults.js deleted file mode 100644 index b976f0b..0000000 --- a/imports/startup/server/segfaults.js +++ /dev/null @@ -1,25 +0,0 @@ -import SegfaultHandler from 'segfault-handler'; -import moment from 'moment'; -// import ravenLogger from './ravenLogger'; - -// https://github.com/ddopson/node-segfault-handler - -const dateFileFormat = moment().format('YYYYMMDD_HH:mm:ss'); - -SegfaultHandler.registerHandler(`/var/tmp/tcef-stacktrace-${dateFileFormat}.log`); - - -// This callaback does not work: -// https://github.com/ddopson/node-segfault-handler/issues/49 - -/* SegfaultHandler.registerHandler('/var/tmp/tcef-crash.log', (signal, address, stack) => { - * // Do what you want with the signal, address, or stack (array) - * // This callback will execute before the signal is forwarded on. - * ravenLogger.log(stack, signal); - * }); */ - -// Only uncomment for segv tests -// SegfaultHandler.causeSegfault(); - -// Or this for normal error (should go to raven in configured) -// throw new Error('This is just a test of error in server'); diff --git a/imports/startup/server/sitemaps.js b/imports/startup/server/sitemaps.js index c1fe573..8026449 100644 --- a/imports/startup/server/sitemaps.js +++ b/imports/startup/server/sitemaps.js @@ -4,6 +4,8 @@ import Fires from '/imports/api/Fires/Fires'; // import Comments from '/imports/api/Comments/Comments'; +const firesMapEnabled = false; + sitemaps.add('/sitemap.xml', () => { const today = new Date(); @@ -20,21 +22,23 @@ sitemaps.add('/sitemap.xml', () => { // When user has public page /* const users = Meteor.users.find().fetch(); - _.each(users, function(user) { - out.push({ - page: '/persona/' + user.username, - lastmod: user.updatedAt - }); + _.each(users, function(user) { + out.push({ + page: '/persona/' + user.username, + lastmod: user.updatedAt + }); }); */ - Fires.find({}, { limit: 100, sort: { createdAt: -1 } }).fetch().forEach((fire) => { - // Search the last comment of tha fire - const lastComment = Comments.getCollection().findOne({ referenceId: `fire-${fire._id}` }, { sort: { createdAt: -1 } }); - out.push({ - page: `/fire/archive/${fire._id._str}`, - lastmod: lastComment ? lastComment.createdAt : fire.updatedAt + if (firesMapEnabled) { + Fires.find({}, { limit: 100, sort: { createdAt: -1 } }).fetch().forEach((fire) => { + // Search the last comment of tha fire + const lastComment = Comments.getCollection().findOne({ referenceId: `fire-${fire._id}` }, { sort: { createdAt: -1 } }); + out.push({ + page: `/fire/archive/${fire._id._str}`, + lastmod: lastComment ? lastComment.createdAt : fire.updatedAt + }); }); - }); + } return out; }); diff --git a/imports/startup/server/subsUnion.js b/imports/startup/server/subsUnion.js index 96c6c20..2643359 100644 --- a/imports/startup/server/subsUnion.js +++ b/imports/startup/server/subsUnion.js @@ -4,7 +4,8 @@ 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 './leaflet-workaround'; +import L from 'leaflet'; import calcUnion from '/imports/ui/components/Maps/SubsUnion/Unify'; import { isMailServerMaster } from '/imports/startup/server/email'; @@ -35,9 +36,8 @@ Meteor.startup(() => { const noNoisy = sub => sub; const process = (isPublic) => { - const group = new L.FeatureGroup(); const subscribers = Subscriptions.find().fetch(); - const result = calcUnion(subscribers, group, isPublic ? addNoisy : noNoisy); + const result = calcUnion(L, subscribers, isPublic ? addNoisy : noNoisy, true); const union = result[0]; const bounds = result[1]; diff --git a/imports/ui/components/Maps/DefMapLayers.js b/imports/ui/components/Maps/DefMapLayers.js index 713b499..6e3ed56 100644 --- a/imports/ui/components/Maps/DefMapLayers.js +++ b/imports/ui/components/Maps/DefMapLayers.js @@ -35,7 +35,8 @@ class DefMapLayers extends Component { ); const osmlayer = ( diff --git a/imports/ui/components/Maps/FireCircleMark.js b/imports/ui/components/Maps/FireCircleMark.js index e4a3460..80b9826 100644 --- a/imports/ui/components/Maps/FireCircleMark.js +++ b/imports/ui/components/Maps/FireCircleMark.js @@ -1,10 +1,10 @@ import React, { Component } from 'react'; -import { Circle } from 'react-leaflet'; +import { GeoJSON } from 'react-leaflet'; import PropTypes from 'prop-types'; +import { rectangleAround } from 'map-common-utils'; import { onMarkClick } from './MarkListeners'; import FirePopup from './FirePopup'; - class FireCircleMark extends Component { constructor(props) { super(props); @@ -22,22 +22,26 @@ class FireCircleMark extends Component { lat, lon, scan, + track, nasa, id, history, when, t } = this.props; + const rect = rectangleAround({ lat, lon }, track, track); return ( - + - +
); + /* */ } } FireCircleMark.propTypes = { scan: PropTypes.number.isRequired, + track: PropTypes.number.isRequired, lat: PropTypes.number.isRequired, lon: PropTypes.number.isRequired, nasa: PropTypes.bool.isRequired, diff --git a/imports/ui/components/Maps/FireIconMark.js b/imports/ui/components/Maps/FireIconMark.js index 94d93a0..ab24da4 100644 --- a/imports/ui/components/Maps/FireIconMark.js +++ b/imports/ui/components/Maps/FireIconMark.js @@ -3,8 +3,8 @@ import React, { Component, Fragment } from 'react'; import { CircleMarker, Marker, Tooltip } from 'react-leaflet'; import PropTypes from 'prop-types'; import { fireIconS, fireIconM, fireIconL, nFireIcon, industryIcon, regIndustryIcon } from '/imports/ui/components/Maps/Icons'; -import { onMarkClick } from './MarkListeners'; import { translate } from 'react-i18next'; +import { onMarkClick } from './MarkListeners'; import FirePopup from './FirePopup'; class FireIconMark extends Component { @@ -30,17 +30,7 @@ class FireIconMark extends Component { render() { const { - lat, - lon, - scan, - nasa, - id, - history, - falsePositives, - industries, - neighbour, - when, - t + lat, lon, scan, track, nasa, id, history, falsePositives, industries, neighbour, when, t } = this.props; return (
@@ -79,6 +69,7 @@ FireIconMark.propTypes = { lat: PropTypes.number.isRequired, lon: PropTypes.number.isRequired, scan: PropTypes.number, + track: PropTypes.number, nasa: PropTypes.bool, falsePositives: PropTypes.bool.isRequired, industries: PropTypes.bool.isRequired, diff --git a/imports/ui/components/Maps/FireListUnion.js b/imports/ui/components/Maps/FireListUnion.js new file mode 100644 index 0000000..3b094c0 --- /dev/null +++ b/imports/ui/components/Maps/FireListUnion.js @@ -0,0 +1,22 @@ +/* eslint-disable import/no-absolute-path */ +/* eslint-disable react/jsx-indent-props */ +/* eslint-disable react/jsx-indent */ +import React from 'react'; +import PropTypes from 'prop-types'; +import FirePolygonMark from '/imports/ui/components/Maps/FirePolygonMark'; + +export default function FireListUnion(props) { + const { + firesUnion, nasa, t, history + } = props; + /* console.log(`Using marks: ${useMarks}, using pixels: ${usePixel}`); */ + const items = firesUnion.map(({ _id, ...otherProps }) => ()); + return (
{items}
); +} + +FireListUnion.propTypes = { + firesUnion: PropTypes.array.isRequired, + nasa: PropTypes.bool.isRequired, + history: PropTypes.object.isRequired, + t: PropTypes.func.isRequired +}; diff --git a/imports/ui/components/Maps/FirePixel.js b/imports/ui/components/Maps/FirePixel.js index c81b547..4efcabe 100644 --- a/imports/ui/components/Maps/FirePixel.js +++ b/imports/ui/components/Maps/FirePixel.js @@ -3,31 +3,32 @@ import React from 'react'; import { CircleMarker } from 'react-leaflet'; import PropTypes from 'prop-types'; -import FirePopup from './FirePopup'; import { translate } from 'react-i18next'; +import FirePopup from './FirePopup'; /* Less acurate (1 pixel per fire) but faster */ const FirePixel = ({ - lat, lon, nasa, id, when, t, history + lat, lon, nasa, id, when, t, history, track }) => ( ); - FirePixel.propTypes = { lat: PropTypes.number.isRequired, lon: PropTypes.number.isRequired, id: PropTypes.object.isRequired, history: PropTypes.object.isRequired, + scan: PropTypes.number, + track: PropTypes.number, when: PropTypes.instanceOf(Date), nasa: PropTypes.bool.isRequired, t: PropTypes.func.isRequired diff --git a/imports/ui/components/Maps/FirePolygonMark.js b/imports/ui/components/Maps/FirePolygonMark.js new file mode 100644 index 0000000..92e3640 --- /dev/null +++ b/imports/ui/components/Maps/FirePolygonMark.js @@ -0,0 +1,48 @@ +import React, { Component } from 'react'; +import { GeoJSON } from 'react-leaflet'; +import PropTypes from 'prop-types'; +import { onMarkClick } from './MarkListeners'; +import FirePopup from './FirePopup'; + +class FirePolygonMark extends Component { + constructor(props) { + super(props); + this.t = props.t; + this.onClick = this.onClick.bind(this); + } + + onClick() { + const { history, nasa, id } = this.props; + onMarkClick(history, nasa, id); + } + + render() { + const { + nasa, + id, + shape, + centerid, + history, + when, + t + } = this.props; + const lon = centerid.coordinates[0]; + const lat = centerid.coordinates[1]; + return ( + + ); + /* */ + } +} + +FirePolygonMark.propTypes = { + id: PropTypes.object.isRequired, + nasa: PropTypes.bool.isRequired, + shape: PropTypes.object.isRequired, + centerid: PropTypes.object.isRequired, + history: PropTypes.object.isRequired, + when: PropTypes.instanceOf(Date).isRequired, + t: PropTypes.func.isRequired +}; + +export default FirePolygonMark; diff --git a/imports/ui/components/Maps/SubsUnion/SubsUnion.js b/imports/ui/components/Maps/SubsUnion/SubsUnion.js index dab127a..41a4d81 100644 --- a/imports/ui/components/Maps/SubsUnion/SubsUnion.js +++ b/imports/ui/components/Maps/SubsUnion/SubsUnion.js @@ -33,8 +33,7 @@ const subsUnion = (union, options) => { 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 result = calcUnion(L, options.subs, sub => sub, true); const unionJson = result[0]; const bounds = result[1]; diff --git a/imports/ui/components/Maps/SubsUnion/Unify.js b/imports/ui/components/Maps/SubsUnion/Unify.js index 46586b7..873dcbf 100644 --- a/imports/ui/components/Maps/SubsUnion/Unify.js +++ b/imports/ui/components/Maps/SubsUnion/Unify.js @@ -1,48 +1,9 @@ -import { check } from 'meteor/check'; -import LGeo from 'leaflet-geodesy'; -import tunion from '@turf/union'; -import ttrunc from '@turf/truncate'; +import { calcUnion as calcUnionWoBounds } from 'map-common-utils'; -// 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.info(`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()]; +const calcUnion = (L, subs, decorated, typeCircle) => { + const unionJson = calcUnionWoBounds(subs, decorated, typeCircle); + // return [unionJson, L.geoJSON(unionJson).getBounds()]; + return [unionJson, unionJson === null ? null : L.geoJSON(unionJson).getBounds()]; }; export default calcUnion; diff --git a/imports/ui/layouts/App/App.js b/imports/ui/layouts/App/App.js index d991262..4aeef3a 100644 --- a/imports/ui/layouts/App/App.js +++ b/imports/ui/layouts/App/App.js @@ -98,66 +98,66 @@ const App = props => ( /* https://react.i18next.com/components/i18nextprovider.html */
{props.i18nReady.get() && - - - - - - { !props.loading && -
- - - {i18n.t('AppName')} - - - - - - + + + + + + { !props.loading && +
+ + + {i18n.t('AppName')} + + + + + + - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - + + + + + + + + - - - - - - - - + + + + + + + + - - - -
- - - {props.i18nReady.get() && } -
} -
-
-
-
-
} + + + +
+ + + {props.i18nReady.get() && } +
} +
+
+
+
+
}
); @@ -186,7 +186,9 @@ export default withTracker(() => { const loading = !Roles.subscription.ready() || !i18nReady.get(); const name = user && user.profile && user.profile.name && getUserName(user.profile.name); const emailAddress = user && user.emails && user.emails[0].address; - // console.log(`i18n ready?: ${i18nReady.get()}`); + Meteor.autorun(() => { + console.log(`i18n ready?: ${i18nReady.get()}`); + }); return { loading, loggingIn, diff --git a/imports/ui/pages/Fires/Fires.js b/imports/ui/pages/Fires/Fires.js index 0e4271a..2647992 100644 --- a/imports/ui/pages/Fires/Fires.js +++ b/imports/ui/pages/Fires/Fires.js @@ -10,7 +10,8 @@ import { Row, Col, Alert, FormGroup } from 'react-bootstrap'; import { Meteor } from 'meteor/meteor'; import { Bert } from 'meteor/themeteorchef:bert'; import { Helmet } from 'react-helmet'; -import { Map, Circle } from 'react-leaflet'; +import { Map, GeoJSON } from 'react-leaflet'; +import { rectangleAround } from 'map-common-utils'; import Blaze from 'meteor/gadicc:blaze-react-component'; import DefMapLayers from '/imports/ui/components/Maps/DefMapLayers'; import NotFound from '/imports/ui/pages/NotFound/NotFound'; @@ -115,8 +116,9 @@ class Fire extends React.Component { t('Información adicional sobre fuego detectado en {{where}} el {{when}}', { where: fire.address, when: this.dateLongFormat }) : t('Información adicional sobre fuego detectado el {{when}}', { when: this.dateLongFormat }); } - - return (fire && !loading ? ( + const ready = fire && !loading; + const rect = ready ? rectangleAround({ lat: fire.lat, lon: fire.lon }, fire.track, fire.track) : null; + return (ready ? (
{t('AppName')}: {t('Información adicional sobre fuego')} @@ -137,14 +139,13 @@ class Fire extends React.Component { zoom={13} > - { this.circle = circle; this.handleLeafletCircleLoad(circle); }} + data={rect} + color="red" + stroke + width="1" + fillOpacity="0.0" /> { (this.props.activefires.length + this.props.firealerts.length) === 0 ? No hay fuegos activos en esta zona del mapa. {{ countTotal: this.props.activefirestotal }} fuegos activos en el mundo. : - En rojo, {{ count: this.props.activefires.length + this.props.firealerts.length }} fuegos activos. {{ countTotal: this.props.activefirestotal }} fuegos activos en el mundo. + En rojo, {{ count: this.props.activefiresunion.length + this.props.firealerts.length }} fuegos activos. {{ countTotal: this.props.activefirestotal }} fuegos activos en el mundo. }

{isNotHomeAndMobile() && this.props.firealerts.length > 0 && @@ -307,6 +297,14 @@ class FiresMap extends React.Component { neighbour={false} industries={false} /> + { Meteor.isDevelopment && + + } { // console.log(`With industries: ${withIndustries}`); let subscription; + let subscriptionUnion; let alertSubscription; const centerStored = store.get('firesmap_center'); @@ -422,6 +423,14 @@ export default translate([], { wait: true })(withTracker(() => { mapSize.get()[1].lat, marks.get() && zoom.get() >= MAXZOOM ); + subscriptionUnion = Meteor.subscribe( + 'activefiresunionmyloc', + mapSize.get()[0].lng, + mapSize.get()[0].lat, + mapSize.get()[1].lng, + mapSize.get()[1].lat, + false + ); alertSubscription = Meteor.subscribe( 'fireAlerts', mapSize.get()[0].lng, @@ -442,6 +451,7 @@ export default translate([], { wait: true })(withTracker(() => { }); Meteor.subscribe('activefirestotal'); + Meteor.subscribe('activefiresuniontotal'); const settingsSubs = Meteor.subscribe('settings'); const lastCheck = SiteSettings.findOne({ name: 'last-fire-check' }); @@ -453,13 +463,15 @@ export default translate([], { wait: true })(withTracker(() => { const lastFireDetected = ActiveFiresCollection.findOne({}, { sort: { when: -1 } }); return { - loading: Meteor.status().status !== 'connected' || !subscription ? true : !(subscription.ready() && settingsSubs.ready() && alertSubscription.ready() && settingsSubs.ready()), + loading: Meteor.status().status !== 'connected' || !subscription ? true : !(subscription.ready() && settingsSubs.ready() && alertSubscription.ready() && settingsSubs.ready() && subscriptionUnion.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(), + activefiresunion: ActiveFiresUnion.find({}, { reactive: zoom.get() >= MAXZOOMREACTIVE }).fetch(), activefirestotal: Counter.get('countActiveFires') + fireAlerts.length, + activefiresuniontotal: Counter.get('countActiveFiresUnion') + fireAlerts.length, firealerts: fireAlerts, falsePositives, industries, diff --git a/imports/ui/pages/Index/Index.js b/imports/ui/pages/Index/Index.js index 5d6b130..9986fba 100644 --- a/imports/ui/pages/Index/Index.js +++ b/imports/ui/pages/Index/Index.js @@ -11,6 +11,7 @@ import PropTypes from 'prop-types'; // import { Link } from 'react-router-dom'; // https://www.npmjs.com/package/react-resize-detector import ReactResizeDetector from 'react-resize-detector'; +import MobileStoreButton from 'react-mobile-store-button'; import _ from 'lodash'; import 'html5-device-mockups/dist/device-mockups.min.css'; import 'bootstrap-carousel-swipe/carousel-swipe'; @@ -20,7 +21,6 @@ import SubscriptionsMap from '/imports/ui/pages/Subscriptions/SubscriptionsMap'; import ShareIt from '/imports/ui/components/ShareIt/ShareIt'; import getFallbackLang from '/imports/modules/lang-fallback'; import FiresMap from '../FiresMap/FiresMap'; - import './Index.scss'; import './Index-custom.scss'; @@ -102,6 +102,7 @@ class Index extends Component { render() { const { t } = this.props; const title = `${t('AppName')}: ${t('Inicio')}`; + const androidUrl = 'https://play.google.com/store/apps/details?id=org.comunes.fires'; return (
@@ -181,6 +182,9 @@ class Index extends Component {

Siempre alerta a los fuegos en nuestro vecindario

{/* {this.props.t('Participa')} */} +
+ +
diff --git a/imports/ui/pages/Index/Index.scss b/imports/ui/pages/Index/Index.scss index 3fb18b4..527aa93 100644 --- a/imports/ui/pages/Index/Index.scss +++ b/imports/ui/pages/Index/Index.scss @@ -77,3 +77,7 @@ } } } + +.android-btn > div { + height: 50px !important; +} diff --git a/imports/ui/pages/Profile/Profile.js b/imports/ui/pages/Profile/Profile.js index 665e9d7..6d9b24b 100644 --- a/imports/ui/pages/Profile/Profile.js +++ b/imports/ui/pages/Profile/Profile.js @@ -9,15 +9,17 @@ import { Meteor } from 'meteor/meteor'; import { Accounts } from 'meteor/accounts-base'; import { Bert } from 'meteor/themeteorchef:bert'; import { testId } from '/imports/ui/components/Utils/TestUtils'; -import Col from '../../components/Col/Col'; -import { withTracker } from 'meteor/react-meteor-data'; -import InputHint from '../../components/InputHint/InputHint'; -import validate from '../../../modules/validate'; import { translate } from 'react-i18next'; import { T9n } from 'meteor-accounts-t9n'; +import { withTracker } from 'meteor/react-meteor-data'; +import Col from '../../components/Col/Col'; +import InputHint from '../../components/InputHint/InputHint'; +import validate from '../../../modules/validate'; import './Profile.scss'; +// List of languages: https://github.com/i18next/i18next/issues/1068 + class Profile extends React.Component { constructor(props) { super(props); @@ -155,6 +157,7 @@ class Profile extends React.Component { renderPasswordUser(loading, user) { const { t, i18n } = this.props; + const enabledLangs = ['en', 'es', 'gl']; const langName = { en: 'English', es: 'Español', gl: 'Galego', ast: 'Asturianu', ca: 'Català' }; @@ -202,7 +205,7 @@ class Profile extends React.Component { {langName[i18n.language]}
- {i18n.languages.map(lang => ( + {enabledLangs.map(lang => (