Compare commits

...

214 commits

Author SHA1 Message Date
39415808bf Fase 1a: flag de cutover notifDisabledChannels en processNotif
Guard reversible al inicio de processNotif: los canales listados en
settings.private.notifDisabledChannels (p.ej. ['mobile','web']) los gestiona el
microservicio tcef-notifications, y el observer/cron viejos los ignoran.
Exclusion mutua por canal sin desplegar codigo (solo editar settings).
ES5-compatible (Meteor 1.6 / Node 8).
2026-07-13 00:10:39 +02:00
9d92ec4f88 deps: regenera package-lock.json y actualiza puntero de meteor-comments-ui 2026-07-12 23:26:17 +02:00
2a31435135 Refactor server startup: elimina segfaults, añade leaflet-workaround
Retira import './segfaults' (workaround de crash obsoleto) y añade
leaflet-workaround.js (shims global window/document/navigator para
render de Leaflet en servidor). Ajustes menores en publications de
FalsePositives y subsUnion.
2026-07-12 23:26:16 +02:00
8281fa3427 chore: ignorar settings-production-*.json y settings-tests.json (secretos) 2026-07-12 23:26:16 +02:00
vjrj
ee7af168d6 Update meteor dev user/pass 2022-08-14 21:53:41 +02:00
vjrj
4cbbe65fa3 Fix wrong grey tiles 2022-08-13 19:37:56 +02:00
vjrj
b9457fe141 Minor changes in fires union 2018-12-23 13:31:41 +01:00
vjrj
c4cb52abaf Improved chimp command in README 2018-11-18 18:01:38 +01:00
vjrj
423dbc5ed9 Added optional chimp configuration 2018-11-18 18:01:17 +01:00
vjrj
1fdf86dd47 Union of subs 'null' when app is initialized 2018-11-18 16:24:49 +01:00
vjrj
5f9a84dfd7 Unify don't calc boundaries of empty union 2018-11-18 15:45:14 +01:00
vjrj
564b9e8ead Trying to fix race condition in i18n load 2018-11-18 15:43:48 +01:00
vjrj
df51368d50 Better log in error 2018-11-18 15:05:04 +01:00
vjrj
0f6dd21541 Removed watch to test 2018-11-18 15:04:46 +01:00
vjrj
8903c765e3 Minor refactor 2018-11-18 14:27:51 +01:00
vjrj
266707052c Better error message 2018-11-18 14:27:34 +01:00
vjrj
3f8bc3666b Fixed index DOM warning. Google Play btn height 2018-11-18 13:08:36 +01:00
vjrj
6abe668740 Added fires union (wip) 2018-11-18 11:48:03 +01:00
vjrj
7b5f17fbe8 Added activeFiresUnion (wip) 2018-10-28 20:27:38 +01:00
vjrj
8b598fab7c Disabled fires render 2018-10-15 06:02:05 +02:00
vjrj
cb7aeda4b8 Create a sender on each message 2018-09-20 15:21:38 +02:00
vjrj
181f488263 New map libs 2018-09-20 15:21:17 +02:00
vjrj
e96e6ee8f7 Union of polygon to lib 2018-09-16 17:06:23 +02:00
vjrj
df47bef794 Fix API now using track 2018-09-15 10:27:03 +02:00
vjrj
3ea653552e Removed typo 2018-09-13 10:13:47 +02:00
vjrj
6063437df0 Remove email notifications from not validated emails 2018-09-13 10:09:09 +02:00
vjrj
cdc3bbdd5b Notif stats to cron process 2018-09-13 10:08:26 +02:00
vjrj
33c68001c2 _str ignore 2018-09-13 10:08:13 +02:00
vjrj
53a7c3be21 Process also pending emailNotifications 2018-09-11 15:27:10 +02:00
vjrj
3d059287ae Notif process also in cron 2018-09-11 14:42:31 +02:00
vjrj
a208eb7ea1 More work with unify, circle/square 2018-09-11 11:40:17 +02:00
vjrj
c4b2f8cd73 Update README with FAQ 2018-08-28 13:15:56 +02:00
vjrj
f9febb4415 Added fire type to REST call 2018-08-22 08:10:35 +02:00
Vicente J. Ruiz Jurado
f5665a56a8 Hardcode available list of langs in profile 2018-08-17 11:33:29 +02:00
Vicente J. Ruiz Jurado
4d7938220b Added comment 2018-08-17 06:13:57 +02:00
Vicente J. Ruiz Jurado
829fae4f07 Trying to get available list of langs in profile 2018-08-17 05:39:03 +02:00
Vicente J. Ruiz Jurado
e223ddb643 Preload langs 2018-08-16 22:44:38 +02:00
Vicente J. Ruiz Jurado
dc5e38f8d5 gl fallback to es in T9 2018-08-16 19:04:46 +02:00
Vicente J. Ruiz Jurado
ed08563cad Thanks for translation 2018-08-16 17:48:58 +02:00
Vicente J. Ruiz Jurado
9a31ab2457 Commenting gl in t9 accounts 2018-08-16 17:47:00 +02:00
Vicente J. Ruiz Jurado
f608fc4d28 Enabling Galego in interface 2018-08-16 17:32:45 +02:00
Weblate
6f683ebaa2 Merge remote-tracking branch 'origin/tcef-master' into tcef-master 2018-08-16 09:03:55 +00:00
lupe bao reixa
8e729a6939 Translated using Weblate (Galician)
Currently translated at 100.0% (221 of 221 strings)
2018-08-16 09:03:51 +00:00
Vicente J. Ruiz Jurado
ffef42d19a Added play button 2018-08-15 07:26:39 +02:00
Vicente J. Ruiz Jurado
3defbb01a2 Fix for false positive rest call 2018-08-15 06:18:51 +02:00
Vicente J. Ruiz Jurado
bbdc53bda1 More logs in Rest 2018-08-11 08:15:04 +02:00
Vicente J. Ruiz Jurado
8a741dc729 Adding log to false positive 2018-08-10 11:12:33 +02:00
Vicente J. Ruiz Jurado
15ed7e917a Adding log to false positive 2018-08-10 09:07:05 +02:00
Vicente J. Ruiz Jurado
759797e412 Fix for rest method for false positive stats when no fires 2018-08-08 08:42:52 +02:00
Vicente J. Ruiz Jurado
5e6271bb88 Trying to fix fireToHash func export 2018-08-05 10:58:35 +02:00
Vicente J. Ruiz Jurado
5429dfbc04 bind publish 2018-08-05 10:44:17 +02:00
Vicente J. Ruiz Jurado
9556bf4e60 New rest methods (subs-union, falsePositive) for mobile 2018-08-05 09:36:18 +02:00
Vicente J. Ruiz Jurado
d4be8c7c46 Privacy updated 2018-08-04 18:42:37 +02:00
Vicente J. Ruiz Jurado
cfe6c2f570 Better mobile notification desc 2018-08-03 09:05:14 +02:00
Vicente J. Ruiz Jurado
20359a79e3 FCM error handling with Meteor.bind 2018-08-03 08:20:20 +02:00
Vicente J. Ruiz Jurado
8780edc832 Correct ObjectId in fcm message 2018-08-03 08:04:43 +02:00
Vicente J. Ruiz Jurado
dc0df7cf32 Master sends only to mobiles 2018-08-02 23:13:15 +02:00
Vicente J. Ruiz Jurado
bf926514c9 Mobile notifications 2018-08-02 12:42:19 +02:00
vjrj
47e9c18607 More work in REST api deletion 2018-06-19 10:49:53 +02:00
vjrj
5642b106d4 Improved rest api calls 2018-06-19 00:18:18 +02:00
vjrj
5b3d86e6a5 Changed rest params in subs 2018-06-18 11:58:04 +02:00
vjrj
b8ab47e867 Improvements in REST API 2018-06-15 06:39:09 +02:00
vjrj
d980a31c05 Improved test lauch for ci. Doc 2018-06-14 21:24:41 +02:00
vjrj
714cc8610a Improved test lauch for ci 2018-06-14 21:24:29 +02:00
vjrj
a48da85ebe More REST calls 2018-06-14 20:35:41 +02:00
vjrj
1de7e4a7b3 Proper error codes 2018-06-14 19:48:48 +02:00
vjrj
eebbdda064 Rest api updated 2018-06-14 17:55:38 +02:00
vjrj
d551dea425 Added done() to test 2018-06-14 17:54:51 +02:00
vjrj
9dbd97009b Improved rest methods 2018-06-12 10:21:55 +02:00
vjrj
ec0edf4475 More rest methods 2018-06-12 08:05:44 +02:00
vjrj
5e5ecb69b8 Wait for settings 2018-06-12 06:10:19 +02:00
vjrj
e28886e031 Update .gitignore 2018-06-12 06:09:55 +02:00
vjrj
eefa586007 Return unauth 2018-06-12 06:08:25 +02:00
vjrj
3e5700f648 Greater icon 2018-06-12 06:08:11 +02:00
vjrj
37e42b70f1 FireStats null when empty 2018-05-23 11:33:54 +02:00
vjrj
12bf1ef09d RavenLogger in dev if enabled 2018-05-23 11:05:30 +02:00
vjrj
783967c0e1 Log console errors in tests. .meteorignore 2018-05-23 10:53:35 +02:00
vjrj
b54707f2dc Improved cucumber tests 2018-05-23 06:29:31 +02:00
vjrj
b53e5d0e36 Log ravenLogger enabled/disabled 2018-05-22 18:19:37 +02:00
vjrj
245619f4e4 Tests more robusts 2018-05-21 17:43:17 +02:00
vjrj
391b8935be Formatted messages 2018-05-21 17:43:05 +02:00
vjrj
d30385b49e Disable raven when not configured 2018-05-21 17:42:37 +02:00
vjrj
bea2703f2f Better stats in FiresMap 2018-05-21 15:43:43 +02:00
vjrj
e36dda74db Better api logs 2018-05-21 15:43:07 +02:00
vjrj
f144c986b7 Added fire stats in zone to API 2018-05-20 17:45:25 +02:00
vjrj
9502ea3604 Remove blur on first home slide 2018-05-10 22:43:14 +02:00
vjrj
22534ddb7e Subs union only in master 2018-05-10 22:39:50 +02:00
vjrj
0f0983bcf5 Removed unused dep 2018-05-10 22:32:30 +02:00
vjrj
b7ebb9a28c Better loading in FiresMap 2018-05-10 11:47:45 +02:00
vjrj
f65c1ee614 Added raven logger to wrong files 2018-05-10 11:06:41 +02:00
vjrj
37a24d288a Check url in encoded fire 2018-05-10 09:52:54 +02:00
vjrj
b7930eeb0e More logs when fire decoding fails 2018-05-10 09:52:30 +02:00
vjrj
2d19778553 Fallback moved. Index points to telegran en bot 2018-05-10 09:00:05 +02:00
vjrj
0a8d59bc53 Minor fix in test times 2018-05-10 08:58:03 +02:00
vjrj
9d68b1e6c1 Trying to add blur and lazy to slides 2018-05-08 12:31:30 +02:00
vjrj
53c81e4f15 Reorder imports 2018-05-08 11:20:48 +02:00
vjrj
b4afa820ab Added webp images for faster home load 2018-05-08 11:12:48 +02:00
vjrj
6e82e2813b Added rendertron flag 2018-05-07 23:56:10 +02:00
vjrj
03acf8ba65 Fix for firestats increased by one 2018-05-05 09:01:34 +02:00
vjrj
88a3eb9639 Adjusts in tweet texts 2018-05-04 11:27:12 +02:00
vjrj
032f6939f6 Remove tweet sample in dev 2018-05-03 20:33:02 +02:00
vjrj
984ce8531d Uptime in status api 2018-05-03 20:32:43 +02:00
vjrj
2d0c8f664b Do subsUnion at startup only if necessary 2018-05-03 16:45:01 +02:00
vjrj
430ec1fbaa Revert mongo test version 2018-05-03 16:42:51 +02:00
vjrj
b35ac6d461 Seeder added to fire enconde tests 2018-05-03 12:08:31 +02:00
vjrj
56eccab054 Fix LocationSchema checks 2018-05-03 12:08:00 +02:00
vjrj
8cabf9b32b Removed kadira 2018-05-03 03:11:05 +02:00
vjrj
2d4f4f4d6e Testing again mongo 1.4.6 with this PR: https://github.com/meteor/meteor/pull/9739 2018-05-03 02:49:05 +02:00
vjrj
08abdffd69 Added fibers workaround 2018-05-03 02:20:33 +02:00
vjrj
2d083d52ba Revert mongo test version 2018-05-03 01:56:29 +02:00
vjrj
b5fc6b396c Again satellite maps in fires 2018-05-03 01:39:01 +02:00
vjrj
9961e56ddc Removed some coments 2018-05-03 01:37:22 +02:00
vjrj
a118b91b79 Added better server log errors 2018-05-03 01:15:33 +02:00
vjrj
9f63552b81 Testing mongo 1.4.6 with this PR: https://github.com/meteor/meteor/pull/9739 2018-05-02 20:30:10 +02:00
vjrj
3eb2e4ff28 Fix tweet tests 2018-05-02 19:04:00 +02:00
vjrj
7bb004cdd2 Improved tweets when many fires 2018-05-02 19:03:30 +02:00
vjrj
eddf89013c Better doc about mongo integration 2018-05-02 19:02:50 +02:00
vjrj
cd63f88e72 Improved tweet sizes 2018-05-02 17:44:33 +02:00
vjrj
4d0911d529 Return empty in FireStats when not data 2018-05-02 11:15:03 +02:00
vjrj
4ca553ff92 Updated kadira agent 2018-05-01 20:50:24 +02:00
vjrj
1d0ad9c41b FireStats class 2018-05-01 17:42:13 +02:00
vjrj
395ee855dd Reconnect only for non authenticated 2018-05-01 17:41:50 +02:00
vjrj
a1c7e0d6e9 Added dev dep 2018-04-27 20:14:46 +02:00
vjrj
e60adc574b Fix server import 2018-04-27 20:05:38 +02:00
vjrj
a4053e6dcd Added debug to countFires 2018-04-27 17:32:03 +02:00
vjrj
4bdebc0af4 Added raven to countFires 2018-04-27 17:31:53 +02:00
vjrj
ed42d3d5ac Dev log 2018-04-19 12:52:45 +02:00
vjrj
7b056ae754 Terrain instead of satellite in Fires page 2018-04-19 12:52:25 +02:00
vjrj
1360f742f3 Fix test for CI 2018-04-12 20:54:34 +02:00
vjrj
8941d6e54e Users schema roles fix after update 2018-04-12 17:42:06 +02:00
vjrj
eddc97fe98 meteor 1.6.1.1 2018-04-12 16:53:06 +02:00
vjrj
ebb3fd2796 Wait for logout in test 2018-04-12 16:52:41 +02:00
vjrj
aa91984afb meteor 1.6.1.1 (wip) 2018-04-12 16:27:47 +02:00
vjrj
ec5969b898 API doc 2018-04-12 10:53:43 +02:00
vjrj
8dc1ec63f2 Added basic rest api 2018-04-12 10:45:20 +02:00
vjrj
903375a102 Added kadira-zones 2018-04-10 11:53:35 +02:00
vjrj
e9248c72a1 Added kadira 2018-04-10 10:42:31 +02:00
vjrj
09e78a0b8f Segfauls for debug server issues 2018-04-09 19:24:08 +02:00
vjrj
2c7dc8e737 Cron log enable/disable 2018-04-09 13:32:34 +02:00
vjrj
61339f4eb2 Email sending by our cron 2018-04-06 17:20:35 +02:00
vjrj
1570ef7d44 email job update only in server 2018-04-06 13:01:12 +02:00
vjrj
e6a84852fa Enabled cron 2018-04-05 13:14:10 +02:00
vjrj
dcab7912c2 Refactor subsUnion 2018-04-04 18:14:32 +02:00
vjrj
a511d7192a Disabled cron 2018-04-04 17:17:21 +02:00
vjrj
3311637c28 isMaster is a function 2018-04-04 09:19:20 +02:00
vjrj
a6bd1095bf Added master/worker pm2 detection and mail configuration 2018-04-04 08:42:02 +02:00
vjrj
f042beceb1 Update raven configuration in server 2018-04-04 08:32:45 +02:00
vjrj
ea50b17df3 Added isMailServer setting for Mail-Time server/client configuration 2018-04-04 08:31:02 +02:00
vjrj
0026b498eb SubsUnion debug 2018-04-02 21:38:51 +02:00
vjrj
ce23ae58e6 Readded reconnect 2018-04-01 16:58:08 +02:00
vjrj
40f79c6aa3 Removed unused string 2018-04-01 16:40:28 +02:00
vjrj
4dfdd48e6a Cron tweet tasks enabled/disabled 2018-04-01 16:37:59 +02:00
Vicente J. Ruiz Jurado
21531d1fd2 falsepositives migration (5rd round) 2018-03-26 17:55:43 +02:00
Vicente J. Ruiz Jurado
d538f8cd8e falsepositives migration (4rd round) 2018-03-26 16:48:51 +02:00
Vicente J. Ruiz Jurado
1504fc1ab1 falsepositives migration (3rd round) 2018-03-26 15:44:41 +02:00
Vicente J. Ruiz Jurado
b6accbadef falsepositives migration (2nd round) 2018-03-26 15:05:10 +02:00
Vicente J. Ruiz Jurado
7886c1227e falsepositives migration 2018-03-26 14:17:13 +02:00
vjrj
1eb6e45dfe Added EU fire stats tweets 2018-03-22 17:42:13 +01:00
vjrj
452f6baf45 Last fire detected 2018-03-22 16:38:27 +01:00
vjrj
b794538ed2 New def values for FiresMap 2018-03-22 16:01:02 +01:00
vjrj
9ee01a6ef8 Improved autonomies names, cron job return 2018-03-22 09:38:40 +01:00
vjrj
de7c56d99b Improved autonomies names 2018-03-22 09:09:53 +01:00
vjrj
681c70f80c Better calculation of fires in tweets 2018-03-22 08:47:24 +01:00
vjrj
a326000474 Remove space in tweets 2018-03-18 20:31:20 +01:00
vjrj
0c56bcf7e0 Added cron tweets for active fires in 'es' and 'pt' 2018-03-17 20:06:00 +01:00
vjrj
c99559d442 Updated settings sample 2018-03-17 17:22:57 +01:00
vjrj
b69202c789 Updated settings sample 2018-03-17 17:21:16 +01:00
vjrj
77f0f159ab Added cron (wp) 2018-03-17 02:03:26 +01:00
vjrj
4d036939f0 Added comment 2018-03-17 01:46:11 +01:00
vjrj
37928238df Added twitter tweet support 2018-03-17 01:42:48 +01:00
vjrj
29a9b328a0 Added TODO 2018-03-17 01:37:04 +01:00
vjrj
a83e2de12f Added priv subsUnion 2018-03-17 00:38:09 +01:00
vjrj
5febb231be NASA map link improved (again) 2018-03-15 10:14:18 +01:00
vjrj
20d5c2b74f NASA map link improved 2018-03-15 09:32:56 +01:00
vjrj
bf757a7f84 href lang for different pages 2018-03-14 10:35:07 +01:00
vjrj
6e3d23e740 Add bot id to subscriptions 2018-03-14 09:01:19 +01:00
vjrj
b92e3c9b30 Added from now title 2018-03-13 01:46:46 +01:00
vjrj
886af7b824 Added more layers to NASA link 2018-03-13 01:17:31 +01:00
vjrj
47838fb0a7 Added build badget 2018-03-13 00:56:43 +01:00
vjrj
8b58e8fc7d Updated settings sample 2018-03-12 13:25:57 +01:00
vjrj
219dc6763f Removed log 2018-03-12 11:00:06 +01:00
vjrj
23a9049abb Added nasa link 2018-03-12 10:24:46 +01:00
vjrj
d2d9b2cf05 Fire neighbour icon 2018-03-10 17:05:16 +01:00
vjrj
72f4ee7fd2 Added version to icons 2018-03-10 13:51:07 +01:00
vjrj
182dc4d878 Fire icons resized 2018-03-10 13:09:41 +01:00
vjrj
4954b907de Autoreconnect 2018-03-07 11:44:56 +01:00
vjrj
4fafaf8ed5 Added telegramBot to user info 2018-03-06 17:58:25 +01:00
vjrj
f7d118de27 Some translation work 2018-03-06 15:55:37 +01:00
vjrj
6e0282d679 FiresMap refactor 2018-03-06 15:55:27 +01:00
vjrj
c025ec5ce8 Removing user-status because not in use 2018-03-06 09:22:40 +01:00
vjrj
edfdbee6d4 Fix translation typo 2018-03-06 09:10:24 +01:00
vjrj
af6676cd8c Against the Fire 2018-03-05 12:04:55 +01:00
vjrj
8c91011dc7 Missing translation 2018-03-05 10:02:42 +01:00
vjrj
a8a1c43f2c Fire comments email notifications 2018-03-05 09:59:55 +01:00
vjrj
90f1e5ba9c More styling in mobile 2018-03-04 13:38:17 +01:00
vjrj
24e8351ba5 Removed BettaRibbon 2018-03-04 13:38:05 +01:00
vjrj
23ee271d64 Fires maps simplified. Better mobile style 2018-03-04 13:31:41 +01:00
vjrj
db964d247f Disabled raven in dev 2018-03-04 12:41:46 +01:00
vjrj
f85771f05a Missing translation 2018-03-04 12:30:26 +01:00
vjrj
2e09100bdc Cancel btn in SelectionMap only in subscriptions. Added title 2018-03-04 10:55:14 +01:00
vjrj
815bbb48fe Added opacity to maps 2018-03-04 10:44:26 +01:00
vjrj
965a80fb3f ie9 gfys 2018-03-03 11:43:17 +01:00
vjrj
dfd31e69ca Refactor App for better ErrorHandling when i18next is not ready 2018-03-03 11:41:43 +01:00
vjrj
e175762c4e ZonesMap refactor 2018-03-03 11:41:12 +01:00
vjrj
cbc3d07f4a Removed fragment in SubsMap 2018-03-03 11:38:49 +01:00
vjrj
337931b6c2 Updated i18next 2018-03-03 11:38:09 +01:00
vjrj
83f5048ff8 Refactor Feedback 2018-03-03 11:26:19 +01:00
vjrj
8b0577b46f Refactor Reconnect 2018-03-03 10:54:53 +01:00
vjrj
64b2affee4 Clean in Profile 2018-03-03 09:05:55 +01:00
vjrj
5996f3a74a Simplify Test page 2018-03-03 08:41:53 +01:00
vjrj
fa4af5e5fe Refactor i18n client code 2018-03-03 08:41:23 +01:00
vjrj
71c6cf20d7 Removed fragment, added key to Index 2018-03-03 08:40:58 +01:00
vjrj
48dab4bb0d Refactor piwik startup 2018-03-02 21:58:14 +01:00
vjrj
38b4769806 Added subscriptions cancel 2018-03-02 12:53:03 +01:00
165 changed files with 14489 additions and 2456 deletions

8
.gitignore vendored
View file

@ -9,3 +9,11 @@ npm-debug.log
.deploy/ .deploy/
public/locales/undefined/ public/locales/undefined/
.deploy-staging .deploy-staging
.screenshots
.vscode
output.json
# fase-0 higiene: variantes de settings con secretos (no versionar)
settings-production-*.json
settings-tests.json
*.json~

View file

@ -4,33 +4,33 @@
# 'meteor add' and 'meteor remove' will edit this file for you, # 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand. # but you can also edit it by hand.
meteor-base@1.2.0 # Packages every Meteor app needs to have meteor-base@1.3.0 # Packages every Meteor app needs to have
mobile-experience@1.0.5 # Packages for a great mobile UX mobile-experience@1.0.5 # Packages for a great mobile UX
mongo@1.3.0 # The database Meteor supports right now mongo@1.4.7 # The database Meteor supports right now
reactive-var@1.0.11 # Reactive variable for tracker reactive-var@1.0.11 # Reactive variable for tracker
tracker@1.1.3 # Meteor's client-side reactive programming library tracker@1.1.3 # Meteor's client-side reactive programming library
standard-minifier-css@1.3.5 # CSS minifier run for production mode standard-minifier-css@1.4.0 # CSS minifier run for production mode
standard-minifier-js@2.2.0 # JS minifier run for production mode standard-minifier-js@2.3.1 # JS minifier run for production mode
es5-shim@4.6.15 # ECMAScript 5 compatibility for older browsers. es5-shim@4.7.0 # ECMAScript 5 compatibility for older browsers.
ecmascript@0.9.0 # Enable ECMAScript2015+ syntax in app code ecmascript@0.10.6 # Enable ECMAScript2015+ syntax in app code
shell-server@0.3.0 # Server-side component of the `meteor shell` command shell-server@0.3.1 # Server-side component of the `meteor shell` command
react-meteor-data react-meteor-data
alanning:roles alanning:roles
fourseven:scss fourseven:scss
accounts-base@1.4.0 accounts-base@1.4.2
accounts-password@1.5.0 accounts-password@1.5.0
service-configuration@1.0.11 service-configuration@1.0.11
accounts-facebook@1.3.0 accounts-facebook@1.3.1
accounts-github@1.4.0 accounts-github@1.4.1
accounts-google@1.3.0 accounts-google@1.3.1
themeteorchef:bert themeteorchef:bert
fortawesome:fontawesome fortawesome:fontawesome
aldeed:collection2-core@2.0.1 aldeed:collection2-core@2.0.1
audit-argument-checks@1.0.7 audit-argument-checks@1.0.7
ddp-rate-limiter@1.0.7 ddp-rate-limiter@1.0.7
dynamic-import@0.2.0 dynamic-import@0.3.0
static-html static-html
alexwine:bootstrap-4 alexwine:bootstrap-4
@ -45,7 +45,6 @@ natestrauser:publish-performant-counts
maximum:server-transform maximum:server-transform
mys:fonts # fonst in npm mys:fonts # fonst in npm
ostrio:mailer # mailer ostrio:mailer # mailer
mizzao:user-status # user status (online, etc)
# babrahams:constellation # dev utility # babrahams:constellation # dev utility
percolate:migrations # db migrations percolate:migrations # db migrations
ostrio:meteor-root ostrio:meteor-root
@ -54,11 +53,17 @@ practicalmeteor:chai
aldeed:schema-deny aldeed:schema-deny
aldeed:template-extension aldeed:template-extension
dburles:collection-helpers dburles:collection-helpers
less less@2.7.11
markdown markdown@1.0.12
mongo-livedata mongo-livedata@1.0.12
reywood:publish-composite reywood:publish-composite
barbatus:stars-rating barbatus:stars-rating
arkham:comments-ui arkham:comments-ui
facts facts@1.0.9
gadicohen:sitemaps gadicohen:sitemaps
nspangler:autoreconnect
saucecode:timezoned-synced-cron
# lmachens:kadira
meteorhacks:zones
nimble:restivus
xolvio:cleaner

View file

@ -1 +1 @@
METEOR@1.6 METEOR@1.6.1.1

View file

@ -1,52 +1,56 @@
255kb:meteor-status@1.5.0 255kb:meteor-status@1.5.0
accounts-base@1.4.0 accounts-base@1.4.2
accounts-facebook@1.3.0 accounts-facebook@1.3.1
accounts-github@1.4.0 accounts-github@1.4.1
accounts-google@1.3.0 accounts-google@1.3.1
accounts-oauth@1.1.15 accounts-oauth@1.1.15
accounts-password@1.5.0 accounts-password@1.5.1
alanning:roles@1.2.16 alanning:roles@1.2.16
aldeed:collection2-core@2.0.1 aldeed:collection2-core@2.1.2
aldeed:schema-deny@2.0.1 aldeed:schema-deny@2.0.1
aldeed:template-extension@4.1.0 aldeed:template-extension@4.1.0
alexwine:bootstrap-4@4.0.0-beta.2 alexwine:bootstrap-4@4.1.0
allow-deny@1.1.0 allow-deny@1.1.0
arkham:comments-ui@1.4.4 arkham:comments-ui@1.4.4
audit-argument-checks@1.0.7 audit-argument-checks@1.0.7
autoupdate@1.3.12 autoupdate@1.4.0
babel-compiler@6.24.7 babel-compiler@7.0.7
babel-runtime@1.1.1 babel-runtime@1.2.2
barbatus:stars-rating@1.1.1 barbatus:stars-rating@1.1.1
base64@1.0.10 base64@1.0.11
binary-heap@1.0.10 binary-heap@1.0.10
blaze@2.3.2 blaze@2.3.2
blaze-tools@1.0.10 blaze-tools@1.0.10
boilerplate-generator@1.3.0 boilerplate-generator@1.4.0
caching-compiler@1.1.9 browser-policy@1.1.0
browser-policy-common@1.0.11
browser-policy-content@1.1.0
browser-policy-framing@1.1.0
caching-compiler@1.1.11
caching-html-compiler@1.1.2 caching-html-compiler@1.1.2
callback-hook@1.0.10 callback-hook@1.1.0
check@1.2.5 check@1.3.1
coffeescript@1.0.17 coffeescript@1.0.17
dburles:collection-helpers@1.1.0 dburles:collection-helpers@1.1.0
dburles:mongo-collection-instances@0.3.5 dburles:mongo-collection-instances@0.3.5
ddp@1.4.0 ddp@1.4.0
ddp-client@2.2.0 ddp-client@2.3.2
ddp-common@1.3.0 ddp-common@1.4.0
ddp-rate-limiter@1.0.7 ddp-rate-limiter@1.0.7
ddp-server@2.1.0 ddp-server@2.1.2
deps@1.0.12 deps@1.0.12
diff-sequence@1.0.7 diff-sequence@1.1.0
dynamic-import@0.2.0 dynamic-import@0.3.0
ecmascript@0.9.0 ecmascript@0.10.7
ecmascript-runtime@0.5.0 ecmascript-runtime@0.5.0
ecmascript-runtime-client@0.5.0 ecmascript-runtime-client@0.6.2
ecmascript-runtime-server@0.5.0 ecmascript-runtime-server@0.5.0
ejson@1.1.0 ejson@1.1.0
email@1.2.3 email@1.2.3
es5-shim@4.6.15 es5-shim@4.7.3
facebook-oauth@1.4.0 facebook-oauth@1.4.0
facts@1.0.9 facts@1.0.9
flowkey:raven@1.0.0 flowkey:raven@1.1.0
fortawesome:fontawesome@4.7.0 fortawesome:fontawesome@4.7.0
fourseven:scss@4.5.4 fourseven:scss@4.5.4
gadicc:blaze-react-component@1.4.0 gadicc:blaze-react-component@1.4.0
@ -54,51 +58,53 @@ gadicohen:robots-txt@0.0.10
gadicohen:sitemaps@0.0.26 gadicohen:sitemaps@0.0.26
geojson-utils@1.0.10 geojson-utils@1.0.10
github-oauth@1.2.0 github-oauth@1.2.0
google-oauth@1.2.4 google-oauth@1.2.5
hot-code-push@1.0.4 hot-code-push@1.0.4
html-tools@1.0.11 html-tools@1.0.11
htmljs@1.0.11 htmljs@1.0.11
http@1.3.0 http@1.4.0
id-map@1.0.9 id-map@1.1.0
jquery@1.11.10 jquery@1.11.11
lai:collection-extensions@0.2.1_1 lai:collection-extensions@0.2.1_1
launch-screen@1.1.1 launch-screen@1.1.1
less@2.7.11 less@2.7.12
livedata@1.0.18 livedata@1.0.18
localstorage@1.2.0 localstorage@1.2.0
logging@1.1.19 logging@1.1.20
markdown@1.0.12 markdown@1.0.12
maximum:package-base@1.1.2 maximum:package-base@1.1.2
maximum:server-transform@0.5.0 maximum:server-transform@0.5.0
mdg:geolocation@1.3.0 mdg:geolocation@1.3.0
meteor@1.8.0 meteor@1.8.6
meteor-base@1.2.0 meteor-base@1.3.0
meteortesting:browser-tests@0.1.2 meteorhacks:inject-initial@1.0.4
meteortesting:mocha@0.5.0 meteorhacks:zones@1.6.0
minifier-css@1.2.16 meteortesting:browser-tests@0.2.0
minifier-js@2.2.1 meteortesting:mocha@0.5.1
minimongo@1.4.0 minifier-css@1.3.1
mizzao:timesync@0.3.4 minifier-js@2.3.4
mizzao:user-status@0.6.7 minimongo@1.4.4
mobile-experience@1.0.5 mobile-experience@1.0.5
mobile-status-bar@1.0.14 mobile-status-bar@1.0.14
modules@0.11.0 modules@0.11.6
modules-runtime@0.9.0 modules-runtime@0.9.2
mongo@1.3.0 mongo@1.4.7
mongo-dev-server@1.1.0 mongo-dev-server@1.1.0
mongo-id@1.0.6 mongo-id@1.0.7
mongo-livedata@1.0.12 mongo-livedata@1.0.12
mys:fonts@0.0.2 mys:fonts@0.0.2
natestrauser:publish-performant-counts@0.1.2 natestrauser:publish-performant-counts@0.1.2
nimble:restivus@0.8.12
npm-bcrypt@0.9.3 npm-bcrypt@0.9.3
npm-mongo@2.2.34 npm-mongo@2.2.34
oauth@1.2.0 nspangler:autoreconnect@0.0.1
oauth@1.2.3
oauth2@1.2.0 oauth2@1.2.0
observe-sequence@1.0.16 observe-sequence@1.0.16
ordered-dict@1.0.9 ordered-dict@1.1.0
ostrio:cookies@2.1.3 ostrio:cookies@2.1.3
ostrio:mailer@2.1.0 ostrio:mailer@2.1.0
ostrio:meteor-root@1.0.6 ostrio:meteor-root@1.0.7
peerlibrary:assert@0.2.5 peerlibrary:assert@0.2.5
peerlibrary:fiber-utils@0.6.0 peerlibrary:fiber-utils@0.6.0
peerlibrary:reactive-mongo@0.1.1 peerlibrary:reactive-mongo@0.1.1
@ -106,37 +112,44 @@ peerlibrary:server-autorun@0.5.2
percolate:migrations@1.0.2 percolate:migrations@1.0.2
practicalmeteor:chai@2.1.0_1 practicalmeteor:chai@2.1.0_1
practicalmeteor:mocha-core@1.0.1 practicalmeteor:mocha-core@1.0.1
promise@0.10.0 promise@0.10.2
raix:eventemitter@0.1.3 raix:eventemitter@0.1.3
random@1.0.10 random@1.1.0
rate-limit@1.0.8 rate-limit@1.0.9
react-meteor-data@0.2.15 react-meteor-data@0.2.16
reactive-dict@1.2.0 reactive-dict@1.2.0
reactive-var@1.0.11 reactive-var@1.0.11
reload@1.1.11 reload@1.2.0
retry@1.0.9 retry@1.1.0
reywood:publish-composite@1.5.2 reywood:publish-composite@1.6.0
routepolicy@1.0.12 routepolicy@1.0.13
saucecode:timezoned-synced-cron@1.2.11
selaias:cookie-consent@0.4.0 selaias:cookie-consent@0.4.0
server-render@0.3.0
service-configuration@1.0.11 service-configuration@1.0.11
session@1.1.7 session@1.1.7
sha@1.0.9 sha@1.0.9
shell-server@0.3.0 shell-server@0.3.1
shim-common@0.1.0
simple:json-routes@2.1.0
socket-stream-client@0.1.0
spacebars@1.0.15 spacebars@1.0.15
spacebars-compiler@1.1.3 spacebars-compiler@1.1.3
srp@1.0.10 srp@1.0.10
standard-minifier-css@1.3.5 standard-minifier-css@1.4.1
standard-minifier-js@2.2.1 standard-minifier-js@2.3.3
static-html@1.2.2 static-html@1.2.2
templating@1.3.2 templating@1.3.2
templating-compiler@1.3.3 templating-compiler@1.3.3
templating-runtime@1.3.2 templating-runtime@1.3.2
templating-tools@1.1.2 templating-tools@1.1.2
themeteorchef:bert@2.1.3 themeteorchef:bert@2.1.3
tmeasday:check-npm-versions@0.3.1 tmeasday:check-npm-versions@0.3.2
tracker@1.1.3 tracker@1.1.3
ui@1.0.13
underscore@1.0.10 underscore@1.0.10
url@1.1.0 url@1.2.0
vjrj:piwik@0.3.1 vjrj:piwik@0.3.1
webapp@1.4.0 webapp@1.5.0
webapp-hashing@1.0.9 webapp-hashing@1.0.9
xolvio:cleaner@0.3.3

3
.meteorignore Normal file
View file

@ -0,0 +1,3 @@
# Don't reload meteor server when we modify or do test
cucumber
output.json

7
API.md Normal file
View file

@ -0,0 +1,7 @@
Basic API:
```bash
curl -X GET http://localhost:3000/api/v1/status/last-fire-detected
curl -X GET http://localhost:3000/api/v1/status/last-fire-check
curl -X GET http://localhost:3000/api/v1/status/active-fires-count
```

View file

@ -1,10 +1,19 @@
# Tod@s contra el Fuego (All Against Fire) # Tod@s contra el Fuego (All Against Fire)
[![Build Status](http://ci.comunes.org/buildStatus/icon?job=todos-contra-el-fuego-web)](http://ci.comunes.org/job/todos-contra-el-fuego-web/)
This is web service that notifies about fires detected in an area of your interest. It helps the early detection of fires and facilitates local mobilization, while the professional extinction services arrive. This is web service that notifies about fires detected in an area of your interest. It helps the early detection of fires and facilitates local mobilization, while the professional extinction services arrive.
## Install ## Install
Install `meteor` and run `npm start` Prerrequisites: a running meteor (we share the `fuegos` database with the telegram bot).
Install `meteor` and run `meteor npm install` and `npm start`
Some other development deps (in debian & ubuntu):
```
apt-get install libcairo2-dev libjpeg-dev libgif-dev pkg-config
```
### GeoIP ### GeoIP
@ -20,18 +29,23 @@ More platforms and services in the future...
We do tests via: We do tests via:
``` ```
TEST_WATCH=1 MONGO_URL=mongodb://localhost:27017/fuegos meteor --settings settings-development.json test --driver-package meteortesting:mocha --port 3010 TEST_PORT=3000 TEST_WATCH=1 TEST_CLIENT=0 MONGO_URL=mongodb://localhost:27017/fuegostest meteor --settings settings-development.json test --driver-package meteortesting:mocha --port 3010
# and # and
chimp --watch --ddp=http://localhost:3000 --path=cucumber node_modules/.bin/chimp --watch --ddp=http://localhost:3000 --path=cucumber
# and # 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 ## 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*. *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*.
@ -42,4 +56,5 @@ GNU APLv3. See our [LICENSE](https://github.com/comunes/todos-contra-el-fuego-we
## Thanks & other acknowlegments ## Thanks & other acknowlegments
(...) Thanks indeed to:
- Lupe Bao Reixa for Galician translation

View file

@ -1,7 +1,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="fragment" content="!"> <meta name="fragment" content="!">
<title>All against Fire | Tod@s contra el Fuego</title> <title>All Against the Fire | Tod@s contra el Fuego</title>
<meta name="description" content="Crowdsourcing against wildfires | Colaboración masiva contra los incendios"> <meta name="description" content="Crowdsourcing against wildfires | Colaboración masiva contra los incendios">
<meta name="viewport" content="initial-scale=1, minimal-ui, maximum-scale=1, minimum-scale=1" /> <meta name="viewport" content="initial-scale=1, minimal-ui, maximum-scale=1, minimum-scale=1" />

View file

@ -1 +1,4 @@
import '../imports/startup/client'; import '../imports/startup/client';
// https://github.com/GoogleChrome/rendertron#rendering-budget-timeout
window.renderComplete = false;

29
cucumber/chimp-config.js Normal file
View file

@ -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
}
}
};

View file

@ -1,4 +1,3 @@
@watch
Feature: Allow users to give feedback Feature: Allow users to give feedback
As a user (authenticated or not) As a user (authenticated or not)
@ -10,6 +9,7 @@ Feature: Allow users to give feedback
# Because is not verified # Because is not verified
Then I can send feedback with email Then I can send feedback with email
When I sign out When I sign out
Given I'm not logged
Then I can send feedback with email Then I can send feedback with email
Scenario: A anon user can give feedback Scenario: A anon user can give feedback

View file

@ -1,5 +1,8 @@
Feature: Test all secundary pages Feature: Test all secundary pages
Background:
Given I am on the home page
Scenario: Check that all secondary pages work well Scenario: Check that all secondary pages work well
Given a list of page ids and contents Given a list of page ids and contents
| license | License | | license | License |
@ -10,8 +13,9 @@ Feature: Test all secundary pages
| about | About | | about | About |
Then I check that all pages works properly Then I check that all pages works properly
Scenario: Check that all secondary pages work well Scenario: Check that all secondary pages work well when not logged
Given a list of page urls and contents Given I logout if logged
And a list of page urls and contents
| login | Login | true | | login | Login | true |
| signup | Sign Up | true | | signup | Sign Up | true |
| fires | Fires | true | | fires | Fires | true |
@ -21,10 +25,27 @@ Feature: Test all secundary pages
| fire/inexistent | This page doesn't exist | false | | fire/inexistent | This page doesn't exist | false |
| fire/Fe26.2**1a0361ed0384f741403682e26b4bbc3850bee24e775da13c9782b22365ea895f*r_3gmVad5vzkeyqPpo6UcA*1s5fFz3iDGKTYP2RCvXMshof00QCHf4ErDl9K2dxoX0u-J-t6scyOWG8pGp3ehg_FfEtyR_kYcEKU3rE0jaSlZbD09TIvhiIJeS3C6Uc8YD-rit0XBrgsVKfYSxKzTRoOYiYFJ8JYd298hMtfiASePjS05Z58hhicyCcJYYRlarqDScG3LiVY3lL5y2nfcdIMNuSjCiKOJWuMkxwd9nR1UHMudLl0hEoy56mPdnHpDYtP9IYUlIOk1LlWBxcmHKifbXeqHu94p8j13Kk20dh2R49Hw3KsSoE9UbWmGQA9wAZXT82301i3rGF5GPAKjlTlRYcWisQurnPwHSVmx3DhUdiYwKGxt4KeaM5QVI4BE9octvE41OOprB_-Il105diQEh2Y9vdvX51ZVWIRfCboICPM6rJb0Oin7U7F1iM-oD_5s3DGnelfM5LGBcKwiB5paMo5M5vdBMaO-zR216cW9yGVXw9IZqHx8xDQWnoHAZjt8NLHeiGF2QOmIGtEUH7qnwhGpkcvszajmAZzR8saZgoH1qfBfvpVA41YfV14gU**4d030f05e23ad75409cebc609107467fc60be5077d52cf041087cd024fc4dc45*GY97aGFc1MyAsoO3Qxqtgwk9j-MbAPdEGBmEHq6r8VU | Additional information | false | | 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 Then I check that all page urls works properly
# And they are spiderable # And they are spiderable
@watch
Scenario: Check that all secondary pages work well when logged
Given I have an account and I logged in
And a list of page urls and contents
| status | Status | false |
| fires | Fires | true |
| zones | Monitored | true |
| verify-email/something | Verify | true |
| fire/inexistent | This page doesn't exist | false |
| 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
Scenario: Check that other non visible pages work well Scenario: Check that other non visible pages work well
Given a list of non visible pages ids and contents Given I logout if logged
| status | Status | And a list of non visible pages ids and contents
| error | Upps | | error | Upppps | false |
# FIXME: find a way to monitor json pages
# | api/v1/status/last-fire-detected | updateAt |
# | api/v1/status/last-fire-check | description |
# | api/v1/status/active-fires-count | total |
Then I check that all non visible pages works properly Then I check that all non visible pages works properly

View file

@ -24,6 +24,7 @@ module.exports = function doSteps() {
client.waitForVisible('#feedback-tab', 10000); client.waitForVisible('#feedback-tab', 10000);
client.click('#feedback-tab'); client.click('#feedback-tab');
client.waitForVisible('#feedback-form', 10000); client.waitForVisible('#feedback-form', 10000);
client.waitForEnabled('input[name="email"]');
client.setValue('input[name="email"]', randomEmail()); client.setValue('input[name="email"]', randomEmail());
client.setValue('#feedbackTextarea', randomText(500)); client.setValue('#feedbackTextarea', randomText(500));
client.click('#sendFeedbackBtn'); client.click('#sendFeedbackBtn');

View file

@ -2,17 +2,50 @@
(function () { (function () {
module.exports = function () { module.exports = function () {
const printLog = (type) => {
client.log(type).value.forEach((logItem) => {
const { source, message } = logItem;
if (type === 'browser' && source === 'console-api') {
// Clean messages of type:
// http://localhost:3000/app/app.js?hash=34da4258bd2caa4e14c7c0c20ba6396f309e7236 13325:18 "GMaps script just loaded"
// to get the last part
const cleanMessage = message.replace(/[^ ]{1,} [^ ]{1,} "(.*)"$/g, '$1');
console.log(`${logItem.level}: ${cleanMessage}`);
} else {
// Right now we don't need logs like:
// %cDownload the React DevTools for a better development experience: https://fb.me/react-devtools
// console.log(`${logItem.level}: ${message}`);
}
});
};
this.StepResult((stepResult) => {
if (stepResult.getStatus() === 'failed') {
// http://webdriver.io/api/protocol/log.html
console.log(`Step result: ${stepResult.getStatus()}`);
printLog('browser');
}
});
this.Before(() => { this.Before(() => {
// global.expect = require('@xolvio/jasmine-expect').expect; // global.expect = require('@xolvio/jasmine-expect').expect;
/* Not used // https://github.com/webdriverio/webdriverio/issues/1145
if (!this.initMyCmds) { if (!this.initMyCmds) {
client.addCommand('waitForClickable', function elementClickable(selector, timeout) { client.addCommand('waitForClickable', function elementClickable(selector, timeout) {
this.waitForVisible(selector, timeout); this.waitForVisible(selector, timeout);
this.waitForEnabled(selector, timeout); this.waitForEnabled(selector, timeout);
}); });
client.addCommand('waitUntilText', function waitUntilText(selector, text, ms) {
ms = ms || 5000;
this.waitForVisible(selector);
const self = this;
this.waitUntil(() => self.getText(selector).includes(text), ms, `expected text in ${selector} be different after ${ms / 1000}s. Obtained: '${self.getText(selector)}' expected: '${text}'`);
});
this.initMyCmds = true; this.initMyCmds = true;
} */ }
client.windowHandleSize({ width: 1500, height: 1000 }); client.windowHandleSize({ width: 1500, height: 1000 });

View file

@ -10,7 +10,7 @@ let pass;
let email; let email;
const setUserValues = (isEdit) => { const setUserValues = (isEdit) => {
client.waitForVisible('input[name="firstName"]', 5000); client.waitForVisible('input[name="firstName"]', 10000);
firstName = randomUsername(); firstName = randomUsername();
lastName = randomUsername(); lastName = randomUsername();
email = randomEmail(); email = randomEmail();
@ -32,31 +32,54 @@ module.exports = function doSteps(notos) {
goHome(client); goHome(client);
}); });
function register() { function waitNoBert() {
client.pause(2000);
if (client.isVisible('.bert-container')) {
// console.log('Removing bert alert');
client.click('.bert-container');
client.waitForVisible('.bert-container', 10000, true);
} else {
// console.log('No bert alert');
}
}
function logoutIfLogged() {
if (client.isVisible('#logout')) { if (client.isVisible('#logout')) {
client.click('#logout'); client.click('#logout');
} }
client.waitForVisible('#signup', 5000); client.waitForVisible('#signup', 10000);
}
function register() {
logoutIfLogged();
client.waitForVisible('#signup', 10000);
client.click('#signup'); client.click('#signup');
setUserValues(); setUserValues();
if (!notos) { if (!notos) {
client.click('input[name="tos"]'); client.click('input[name="tos"]');
} }
client.click('#signUpSubmit'); client.click('#signUpSubmit');
client.waitForVisible('#logout', 10000);
} }
this.Given(/^I logout if logged$/, () => {
waitNoBert();
logoutIfLogged();
});
this.Given(/^I have an account and I logged in$/, () => { this.Given(/^I have an account and I logged in$/, () => {
register(); register();
}); });
function checkName() { function checkName() {
client.waitForVisible('#profile', 5000); client.waitForVisible('#profile', 5000);
client.waitForText('#profile', firstName); client.waitUntilText('#profile', firstName);
client.waitForText('#profile', lastName); client.waitUntilText('#profile', lastName);
} }
this.Then(/^I should be logged in$/, () => { this.Then(/^I should be logged in$/, () => {
client.waitUntil(() => client.isVisible('#logout') === true, 10000); client.waitUntil(() => client.isVisible('#logout') === true, 10000);
client.waitUntil(() => client.isVisible('#profile') === true, 10000);
}); });
/* this.Given(/^I am signed out$/, () => { /* this.Given(/^I am signed out$/, () => {
@ -64,29 +87,24 @@ module.exports = function doSteps(notos) {
* }); * });
*/ */
function closeAlert() {
// client.waitForVisible('.bert-alert', 5000);
// client.click('.bert-content');
client.waitUntil(() => client.isVisible('.bert-alert') === false, 10000);
}
this.When(/^I sign out$/, () => { this.When(/^I sign out$/, () => {
if (client.isVisible('.bert-alert')) { waitNoBert();
client.waitUntil(() => client.isVisible('.bert-alert') === false, 10000); client.waitForClickable('#logout', 10000);
} // client.waitForVisible('#logout', 10000);
client.waitForVisible('#logout', 5000);
client.click('#logout'); client.click('#logout');
client.waitForVisible('#login', 10000);
}); });
function login(badpass) { function login(badpass) {
waitNoBert();
client.waitForVisible('#login', 10000); client.waitForVisible('#login', 10000);
client.waitUntil(() => client.isVisible('.bert-alert') === false, 10000);
client.click('#login'); client.click('#login');
client.waitForVisible('#loginSubmit', 5000); client.waitForVisible('#loginSubmit', 5000);
client.setValue('input[name="emailAddress"]', email); client.setValue('input[name="emailAddress"]', email);
client.setValue('input[name="password"]', badpass ? randomPassword() : pass); client.setValue('input[name="password"]', badpass ? randomPassword() : pass);
client.click('#loginSubmit'); client.click('#loginSubmit');
closeAlert(); waitNoBert();
} }
this.When(/^I enter my email and password$/, () => { this.When(/^I enter my email and password$/, () => {
@ -94,11 +112,13 @@ module.exports = function doSteps(notos) {
}); });
this.Then(/^I can edit my profile$/, () => { this.Then(/^I can edit my profile$/, () => {
client.waitForVisible('#profile', 5000); waitNoBert();
client.waitForClickable('#profile', 10000);
client.click('#profile'); client.click('#profile');
client.pause(2000);
setUserValues(true); setUserValues(true);
client.click('#profileSubmit'); client.click('#profileSubmit');
closeAlert(); waitNoBert();
checkName(); checkName();
}); });
@ -116,7 +136,7 @@ module.exports = function doSteps(notos) {
this.Then(/^I should be registered$/, () => { this.Then(/^I should be registered$/, () => {
client.waitForVisible('.bert-alert', 10000, true); client.waitForVisible('.bert-alert', 10000, true);
closeAlert(); waitNoBert();
}); });
this.Given(/^I register with some name, password and email but without accept conditions$/, () => { this.Given(/^I register with some name, password and email but without accept conditions$/, () => {

View file

@ -16,7 +16,7 @@ module.exports = function () {
const content = pages[i][1]; const content = pages[i][1];
client.waitForVisible(id, 10000); client.waitForVisible(id, 10000);
client.click(id); client.click(id);
client.waitForText('#react-root', content); client.waitUntilText('#react-root', content);
// https://jasmine.github.io/2.3/introduction.html#section-Expectations // https://jasmine.github.io/2.3/introduction.html#section-Expectations
expect(client.getTitle()).toContain(pages[i][1]); expect(client.getTitle()).toContain(pages[i][1]);
} }
@ -26,7 +26,7 @@ module.exports = function () {
const content = pages[i][1]; const content = pages[i][1];
client.url(`${process.env.ROOT_URL}/${url}`); client.url(`${process.env.ROOT_URL}/${url}`);
client.waitForVisible('#react-root', 10000); client.waitForVisible('#react-root', 10000);
client.waitForText('#react-root', content); client.waitUntilText('#react-root', content);
const checkTitle = pages[i][2] === 'true'; const checkTitle = pages[i][2] === 'true';
// https://jasmine.github.io/2.3/introduction.html#section-Expectations // https://jasmine.github.io/2.3/introduction.html#section-Expectations
if (checkTitle) { if (checkTitle) {
@ -41,10 +41,7 @@ module.exports = function () {
this.Then(/^I check that all non visible pages works properly$/, (callback) => { this.Then(/^I check that all non visible pages works properly$/, (callback) => {
for (let i = 0; i < pages.length; i += 1) { for (let i = 0; i < pages.length; i += 1) {
client.url(`${process.env.ROOT_URL}/${pages[i][0]}`); processPageUrl(i);
const content = pages[i][1];
client.waitForVisible('#react-root', 10000);
client.waitForText('#react-root', content);
} }
callback(); callback();
}); });
@ -77,7 +74,7 @@ module.exports = function () {
// Write code here that turns the phrase above into concrete actions // Write code here that turns the phrase above into concrete actions
for (let i = 0; i < pages.length; i += 1) { for (let i = 0; i < pages.length; i += 1) {
client.url(`${process.env.ROOT_URL}/${pages[i][0]}?_escaped_fragment_=`); client.url(`${process.env.ROOT_URL}/${pages[i][0]}?_escaped_fragment_=`);
client.waitForText('#react-root', pages[i][1]); client.waitUntilText('#react-root', pages[i][1]);
} }
callback(); callback();
}); });

View file

@ -7,6 +7,7 @@
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="210mm" width="210mm"
@ -245,9 +246,9 @@
borderopacity="1.0" borderopacity="1.0"
inkscape:pageopacity="0.0" inkscape:pageopacity="0.0"
inkscape:pageshadow="2" inkscape:pageshadow="2"
inkscape:zoom="0.25" inkscape:zoom="0.23559314"
inkscape:cx="-324.9908" inkscape:cx="-2890.4884"
inkscape:cy="997.3585" inkscape:cy="-486.23766"
inkscape:document-units="mm" inkscape:document-units="mm"
inkscape:current-layer="layer1" inkscape:current-layer="layer1"
showgrid="false" showgrid="false"
@ -448,11 +449,18 @@
inkscape:connector-curvature="0" /> inkscape:connector-curvature="0" />
</g> </g>
</g> </g>
<rect
style="opacity:1;vector-effect:none;fill:#c8b7b7;fill-opacity:1;stroke:#c8b7b7;stroke-width:0.26458332;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect8537"
width="368.82184"
height="657.66956"
x="-971.73767"
y="74.954208" />
<g <g
id="g17717" id="g8543"
inkscape:export-filename="/mnt/md1/proyectos/dev/todos-contra-el-fuego-web/public/favicon-200.png" inkscape:export-filename="/mnt/md1/proyectos/dev/todos-contra-el-fuego-web/public/fire-marker-l.png"
inkscape:export-xdpi="28.790001" inkscape:export-xdpi="4"
inkscape:export-ydpi="28.790001"> inkscape:export-ydpi="4">
<rect <rect
y="108.84225" y="108.84225"
x="-419.07861" x="-419.07861"
@ -499,61 +507,66 @@
style="font-size:192px;fill:#ff0000" /></flowRegion><flowPara style="font-size:192px;fill:#ff0000" /></flowRegion><flowPara
id="flowPara8488"></flowPara></flowRoot> <g id="flowPara8488"></flowPara></flowRoot> <g
id="g22554-45" id="g22554-45"
transform="rotate(-104.78334,-231.82318,51.6182)" transform="matrix(-0.04534342,-0.17182024,0.17182024,-0.04534342,-93.960693,-204.49561)"
inkscape:export-filename="/mnt/md1/proyectos/dev/todos-contra-el-fuego-web/public/fire-marker.png" inkscape:export-filename="/mnt/md1/proyectos/dev/todos-contra-el-fuego-web/public/fire-marker.png"
inkscape:export-xdpi="4.0856881" inkscape:export-xdpi="4.0856881"
inkscape:export-ydpi="4.0856881"> inkscape:export-ydpi="4.0856881"
style="stroke-width:5.62737894">
<g <g
id="g8559"> id="g8559"
style="stroke-width:5.62737894">
<g <g
inkscape:export-ydpi="4.0856881" inkscape:export-ydpi="4.0856881"
inkscape:export-xdpi="4.0856881" inkscape:export-xdpi="4.0856881"
inkscape:export-filename="/mnt/md1/proyectos/dev/todos-contra-el-fuego-web/public/fire-marker.png" inkscape:export-filename="/mnt/md1/proyectos/dev/todos-contra-el-fuego-web/public/fire-marker.png"
transform="rotate(-144.74008,-66.772616,119.34544)" transform="rotate(-144.74008,-66.772616,119.34544)"
id="g22554-2"> id="g22554-2"
style="stroke-width:5.62737894">
<path <path
sodipodi:nodetypes="cszzcczasc" sodipodi:nodetypes="cszzcczasc"
inkscape:connector-curvature="0" inkscape:connector-curvature="0"
id="path5329-8" id="path5329-8"
d="m -25.638382,38.048346 c 11.566313,13.887908 17.1215844,22.244854 13.751242,33.868326 -2.465387,8.50265 -8.895424,18.626534 -18.100172,25.532106 -9.204776,6.905572 -20.549951,13.517692 -24.263404,17.728802 -3.71348,4.21108 -9.835779,11.6046 -10.976769,21.82688 -2.725208,24.7073 15.038044,45.63926 39.702238,48.35952 11.289242,1.2311 26.6324564,-5.2645 34.2331684,-10.31973 C 16.308659,169.98901 28.520768,152.76263 31.570884,138.81099 35.854436,119.21732 35.065926,96.385127 23.626002,79.16853 12.946123,63.095807 3.8673424,50.775037 -25.638382,38.048346 Z" d="m -25.638382,38.048346 c 11.566313,13.887908 17.1215844,22.244854 13.751242,33.868326 -2.465387,8.50265 -8.895424,18.626534 -18.100172,25.532106 -9.204776,6.905572 -20.549951,13.517692 -24.263404,17.728802 -3.71348,4.21108 -9.835779,11.6046 -10.976769,21.82688 -2.725208,24.7073 15.038044,45.63926 39.702238,48.35952 11.289242,1.2311 26.6324564,-5.2645 34.2331684,-10.31973 C 16.308659,169.98901 28.520768,152.76263 31.570884,138.81099 35.854436,119.21732 35.065926,96.385127 23.626002,79.16853 12.946123,63.095807 3.8673424,50.775037 -25.638382,38.048346 Z"
style="fill:#d41300;fill-opacity:1;stroke:none;stroke-width:10.58333302;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> style="fill:#d41300;fill-opacity:1;stroke:none;stroke-width:59.556427;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path <path
sodipodi:nodetypes="cszzcczcsc" sodipodi:nodetypes="cszzcczcsc"
inkscape:connector-curvature="0" inkscape:connector-curvature="0"
id="path5331-30" id="path5331-30"
d="m -7.5198746,101.09834 c 4.018254,8.70077 3.821166,14.69136 0.49964,20.18233 -2.429696,4.01667 -7.1125824,8.31964 -12.7749834,10.59336 -5.662401,2.27373 -12.38086,4.1034 -14.860826,5.75067 -2.479966,1.64727 -6.630512,4.59399 -8.617877,9.67322 -4.786736,12.28103 1.27127,26.11038 13.530846,30.8887 5.613373,2.17993 11.515354,2.13146 17.442629,-0.0849 5.9272234,-2.21636 14.0405914,-9.44377 18.0234704,-15.42084 6.3490206,-9.59829 8.9531296,-23.76395 5.4565016,-34.15229 -3.2643496,-9.69812 -4.6224286,-12.3603 -18.6994006,-27.43025 z" d="m -7.5198746,101.09834 c 4.018254,8.70077 3.821166,14.69136 0.49964,20.18233 -2.429696,4.01667 -7.1125824,8.31964 -12.7749834,10.59336 -5.662401,2.27373 -12.38086,4.1034 -14.860826,5.75067 -2.479966,1.64727 -6.630512,4.59399 -8.617877,9.67322 -4.786736,12.28103 1.27127,26.11038 13.530846,30.8887 5.613373,2.17993 11.515354,2.13146 17.442629,-0.0849 5.9272234,-2.21636 14.0405914,-9.44377 18.0234704,-15.42084 6.3490206,-9.59829 8.9531296,-23.76395 5.4565016,-34.15229 -3.2643496,-9.69812 -4.6224286,-12.3603 -18.6994006,-27.43025 z"
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:10.58333302;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:59.556427;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g> </g>
<g <g
inkscape:export-ydpi="4.0856881" inkscape:export-ydpi="4.0856881"
inkscape:export-xdpi="4.0856881" inkscape:export-xdpi="4.0856881"
inkscape:export-filename="/mnt/md1/proyectos/dev/todos-contra-el-fuego-web/public/fire-marker.png" inkscape:export-filename="/mnt/md1/proyectos/dev/todos-contra-el-fuego-web/public/fire-marker.png"
transform="rotate(96.689458,-75.070103,121.00438)" transform="rotate(96.689458,-75.070103,121.00438)"
id="g22554-5"> id="g22554-5"
style="stroke-width:5.62737894">
<path <path
sodipodi:nodetypes="cszzcczasc" sodipodi:nodetypes="cszzcczasc"
inkscape:connector-curvature="0" inkscape:connector-curvature="0"
id="path5329-4" id="path5329-4"
d="m -25.638382,38.048346 c 11.566313,13.887908 17.1215844,22.244854 13.751242,33.868326 -2.465387,8.50265 -8.895424,18.626534 -18.100172,25.532106 -9.204776,6.905572 -20.549951,13.517692 -24.263404,17.728802 -3.71348,4.21108 -9.835779,11.6046 -10.976769,21.82688 -2.725208,24.7073 15.038044,45.63926 39.702238,48.35952 11.289242,1.2311 26.6324564,-5.2645 34.2331684,-10.31973 C 16.308659,169.98901 28.520768,152.76263 31.570884,138.81099 35.854436,119.21732 35.065926,96.385127 23.626002,79.16853 12.946123,63.095807 3.8673424,50.775037 -25.638382,38.048346 Z" d="m -25.638382,38.048346 c 11.566313,13.887908 17.1215844,22.244854 13.751242,33.868326 -2.465387,8.50265 -8.895424,18.626534 -18.100172,25.532106 -9.204776,6.905572 -20.549951,13.517692 -24.263404,17.728802 -3.71348,4.21108 -9.835779,11.6046 -10.976769,21.82688 -2.725208,24.7073 15.038044,45.63926 39.702238,48.35952 11.289242,1.2311 26.6324564,-5.2645 34.2331684,-10.31973 C 16.308659,169.98901 28.520768,152.76263 31.570884,138.81099 35.854436,119.21732 35.065926,96.385127 23.626002,79.16853 12.946123,63.095807 3.8673424,50.775037 -25.638382,38.048346 Z"
style="fill:#d41300;fill-opacity:1;stroke:none;stroke-width:10.58333302;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> style="fill:#d41300;fill-opacity:1;stroke:none;stroke-width:59.556427;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path <path
sodipodi:nodetypes="cszzcczcsc" sodipodi:nodetypes="cszzcczcsc"
inkscape:connector-curvature="0" inkscape:connector-curvature="0"
id="path5331-9" id="path5331-9"
d="m -7.5198746,101.09834 c 4.018254,8.70077 3.821166,14.69136 0.49964,20.18233 -2.429696,4.01667 -7.1125824,8.31964 -12.7749834,10.59336 -5.662401,2.27373 -12.38086,4.1034 -14.860826,5.75067 -2.479966,1.64727 -6.630512,4.59399 -8.617877,9.67322 -4.786736,12.28103 1.27127,26.11038 13.530846,30.8887 5.613373,2.17993 11.515354,2.13146 17.442629,-0.0849 5.9272234,-2.21636 14.0405914,-9.44377 18.0234704,-15.42084 6.3490206,-9.59829 8.9531296,-23.76395 5.4565016,-34.15229 -3.2643496,-9.69812 -4.6224286,-12.3603 -18.6994006,-27.43025 z" d="m -7.5198746,101.09834 c 4.018254,8.70077 3.821166,14.69136 0.49964,20.18233 -2.429696,4.01667 -7.1125824,8.31964 -12.7749834,10.59336 -5.662401,2.27373 -12.38086,4.1034 -14.860826,5.75067 -2.479966,1.64727 -6.630512,4.59399 -8.617877,9.67322 -4.786736,12.28103 1.27127,26.11038 13.530846,30.8887 5.613373,2.17993 11.515354,2.13146 17.442629,-0.0849 5.9272234,-2.21636 14.0405914,-9.44377 18.0234704,-15.42084 6.3490206,-9.59829 8.9531296,-23.76395 5.4565016,-34.15229 -3.2643496,-9.69812 -4.6224286,-12.3603 -18.6994006,-27.43025 z"
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:10.58333302;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:59.556427;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g> </g>
<g <g
transform="rotate(-22.690691,-35.704234,120.074)" transform="rotate(-22.690691,-35.704234,120.074)"
id="g8542"> id="g8542"
style="stroke-width:5.62737894">
<path <path
style="fill:#d41300;fill-opacity:1;stroke:none;stroke-width:10.58333302;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" style="fill:#d41300;fill-opacity:1;stroke:none;stroke-width:59.556427;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m -25.638382,38.048346 c 11.566313,13.887908 17.1215844,22.244854 13.751242,33.868326 -2.465387,8.50265 -8.895424,18.626534 -18.100172,25.532106 -9.204776,6.905572 -20.549951,13.517692 -24.263404,17.728802 -3.71348,4.21108 -9.835779,11.6046 -10.976769,21.82688 -2.725208,24.7073 15.038044,45.63926 39.702238,48.35952 11.289242,1.2311 26.6324564,-5.2645 34.2331684,-10.31973 C 16.308659,169.98901 28.520768,152.76263 31.570884,138.81099 35.854436,119.21732 35.065926,96.385127 23.626002,79.16853 12.946123,63.095807 3.8673424,50.775037 -25.638382,38.048346 Z" d="m -25.638382,38.048346 c 11.566313,13.887908 17.1215844,22.244854 13.751242,33.868326 -2.465387,8.50265 -8.895424,18.626534 -18.100172,25.532106 -9.204776,6.905572 -20.549951,13.517692 -24.263404,17.728802 -3.71348,4.21108 -9.835779,11.6046 -10.976769,21.82688 -2.725208,24.7073 15.038044,45.63926 39.702238,48.35952 11.289242,1.2311 26.6324564,-5.2645 34.2331684,-10.31973 C 16.308659,169.98901 28.520768,152.76263 31.570884,138.81099 35.854436,119.21732 35.065926,96.385127 23.626002,79.16853 12.946123,63.095807 3.8673424,50.775037 -25.638382,38.048346 Z"
id="path5329-9" id="path5329-9"
inkscape:connector-curvature="0" inkscape:connector-curvature="0"
sodipodi:nodetypes="cszzcczasc" /> sodipodi:nodetypes="cszzcczasc" />
<path <path
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:10.58333302;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:59.556427;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m -7.5198746,101.09834 c 4.018254,8.70077 3.821166,14.69136 0.49964,20.18233 -2.429696,4.01667 -7.1125824,8.31964 -12.7749834,10.59336 -5.662401,2.27373 -12.38086,4.1034 -14.860826,5.75067 -2.479966,1.64727 -6.630512,4.59399 -8.617877,9.67322 -4.786736,12.28103 1.27127,26.11038 13.530846,30.8887 5.613373,2.17993 11.515354,2.13146 17.442629,-0.0849 5.9272234,-2.21636 14.0405914,-9.44377 18.0234704,-15.42084 6.3490206,-9.59829 8.9531296,-23.76395 5.4565016,-34.15229 -3.2643496,-9.69812 -4.6224286,-12.3603 -18.6994006,-27.43025 z" d="m -7.5198746,101.09834 c 4.018254,8.70077 3.821166,14.69136 0.49964,20.18233 -2.429696,4.01667 -7.1125824,8.31964 -12.7749834,10.59336 -5.662401,2.27373 -12.38086,4.1034 -14.860826,5.75067 -2.479966,1.64727 -6.630512,4.59399 -8.617877,9.67322 -4.786736,12.28103 1.27127,26.11038 13.530846,30.8887 5.613373,2.17993 11.515354,2.13146 17.442629,-0.0849 5.9272234,-2.21636 14.0405914,-9.44377 18.0234704,-15.42084 6.3490206,-9.59829 8.9531296,-23.76395 5.4565016,-34.15229 -3.2643496,-9.69812 -4.6224286,-12.3603 -18.6994006,-27.43025 z"
id="path5331-35" id="path5331-35"
inkscape:connector-curvature="0" inkscape:connector-curvature="0"
@ -561,5 +574,86 @@
</g> </g>
</g> </g>
</g> </g>
<g
id="g8551"
inkscape:export-filename="/mnt/md1/proyectos/dev/todos-contra-el-fuego-web/public/fire-marker-s.png"
inkscape:export-xdpi="4"
inkscape:export-ydpi="4">
<use
transform="translate(0,404.8127)"
height="100%"
width="100%"
id="use8529"
xlink:href="#rect17701"
y="0"
x="0" />
<use
style="stroke-width:2"
transform="matrix(0.5,0,0,0.5,-165.41924,545.68726)"
height="100%"
width="100%"
id="use8531"
xlink:href="#g22554-4"
y="0"
x="0" />
</g>
<g
id="g8547"
transform="translate(0,-14.019767)"
inkscape:export-filename="/mnt/md1/proyectos/dev/todos-contra-el-fuego-web/public/fire-marker-m.png"
inkscape:export-xdpi="4"
inkscape:export-ydpi="4">
<use
transform="translate(0,190.50006)"
height="100%"
width="100%"
id="use8533"
xlink:href="#rect17701"
y="0"
x="0" />
<use
style="stroke-width:1.33333337"
transform="matrix(0.75,0,0,0.75,-82.709618,260.93734)"
height="100%"
width="100%"
id="use8535"
xlink:href="#g22554-4"
y="0"
x="0" />
</g>
<g
transform="translate(176.48029,228.33241)"
id="g8543-0"
inkscape:export-filename="/home/vjrj/proyectos/dev/fires_flutter/images/logo-1024.png"
inkscape:export-xdpi="147.38"
inkscape:export-ydpi="147.38">
<rect
y="108.84225"
x="-419.07861"
height="176.48029"
width="176.48029"
id="rect17701-2"
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:none;stroke-width:0.26458332;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<g
style="stroke-width:0.88457036"
inkscape:export-ydpi="4.0856881"
inkscape:export-xdpi="4.0856881"
inkscape:export-filename="/mnt/md1/proyectos/dev/todos-contra-el-fuego-web/public/fire-marker.png"
transform="matrix(1.1304923,0,0,1.1304923,-313.01185,70.713452)"
id="g22554-4-3">
<path
sodipodi:nodetypes="cszzcczasc"
inkscape:connector-curvature="0"
id="path5329-5-7"
d="m -25.638382,38.048346 c 11.566313,13.887908 17.1215844,22.244854 13.751242,33.868326 -2.465387,8.50265 -8.895424,18.626534 -18.100172,25.532106 -9.204776,6.905572 -20.549951,13.517692 -24.263404,17.728802 -3.71348,4.21108 -9.835779,11.6046 -10.976769,21.82688 -2.725208,24.7073 15.038044,45.63926 39.702238,48.35952 11.289242,1.2311 26.6324564,-5.2645 34.2331684,-10.31973 C 16.308659,169.98901 28.520768,152.76263 31.570884,138.81099 35.854436,119.21732 35.065926,96.385127 23.626002,79.16853 12.946123,63.095807 3.8673424,50.775037 -25.638382,38.048346 Z"
style="fill:#d41300;fill-opacity:1;stroke:none;stroke-width:9.36170292;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="cszzcczcsc"
inkscape:connector-curvature="0"
id="path5331-3-5"
d="m -7.5198746,101.09834 c 4.018254,8.70077 3.821166,14.69136 0.49964,20.18233 -2.429696,4.01667 -7.1125824,8.31964 -12.7749834,10.59336 -5.662401,2.27373 -12.38086,4.1034 -14.860826,5.75067 -2.479966,1.64727 -6.630512,4.59399 -8.617877,9.67322 -4.786736,12.28103 1.27127,26.11038 13.530846,30.8887 5.613373,2.17993 11.515354,2.13146 17.442629,-0.0849 5.9272234,-2.21636 14.0405914,-9.44377 18.0234704,-15.42084 6.3490206,-9.59829 8.9531296,-23.76395 5.4565016,-34.15229 -3.2643496,-9.69812 -4.6224286,-12.3603 -18.6994006,-27.43025 z"
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:9.36170292;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
</g>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Before After
Before After

View file

@ -1,5 +1,6 @@
/* eslint-disable consistent-return */ /* eslint-disable consistent-return */
import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo'; import { Mongo } from 'meteor/mongo';
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import firesCommonSchema from '../Common/FiresSchema'; import firesCommonSchema from '../Common/FiresSchema';
@ -18,8 +19,11 @@ ActiveFires.deny({
remove: () => true 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); ActiveFires.attachSchema(ActiveFires.schema);

View file

@ -0,0 +1,79 @@
/* eslint-disable import/no-absolute-path */
import { whichAreFalsePositives, firesUnion } from '/imports/api/FalsePositives/server/publications';
import FalsePositives from '/imports/api/FalsePositives/FalsePositives';
import Industries from '/imports/api/Industries/Industries';
import ravenLogger from '/imports/startup/server/ravenLogger';
import ActiveFires from '../ActiveFires';
const debug = 0;
const cleanProv = (prov, stringsToRemove) => {
let lprov = prov;
stringsToRemove.forEach((st) => {
lprov = lprov.replace(st[0], st[1]);
});
return lprov;
};
const findFiresInRegion = (zone) => {
const result = ActiveFires.find({
ourid: {
$geoWithin: {
$geometry: zone.geometry
}
}
});
/* console.log(`False positive total: ${falsePos.count()}`);
console.log(`False positives: ${JSON.stringify(falsePos.fetch())}`); */
return result;
};
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) {
realFires.push(fire);
}
});
// group fires
const realUnion = firesUnion(realFires);
const unionCount = realUnion[0] &&
realUnion[0].geometry &&
realUnion[0].geometry.coordinates ? realUnion[0].geometry.coordinates.length : 0;
return unionCount;
};
const countFiresInRegions = (regions, stringsToRemove) => {
let total = 0;
const fireStats = {};
regions.features.forEach((region) => {
const regionName = cleanProv(region.properties.name, stringsToRemove);
const regionCode = region.properties.iso_a2;
if (debug) console.log(`${regionName} -----`);
try {
const fires = findFiresInRegion(region);
// TODO Also check neighbour fires (when better implementation of that part)
const initialCount = fires.count();
if (initialCount > 0) {
const unionCount = countRealFires(fires);
if (debug) console.log(`${regionName} initial: ${initialCount}, union calc: ${unionCount}`);
if (unionCount > 0) {
total += unionCount;
fireStats[regionName] = { count: unionCount, code: regionCode };
}
}
} catch (e) {
ravenLogger.log(e);
}
});
return { total, fires: fireStats };
};
export default countFiresInRegions;

View file

@ -31,12 +31,12 @@ const activefires = (northEastLng, northEastLat, southWestLng, southWestLat, wit
lat: 1, lat: 1,
lon: 1, lon: 1,
when: 1, when: 1,
scan: 1 scan: 1,
track: 1
} }
}); });
// console.log(JSON.stringify(result)); if (Meteor.isDevelopment) console.log(`Active fires total: ${fires.count()}`);
// console.log(`Fires total: ${fires.count()}`);
if (withMarks && fires.fetch().length > 0) { if (withMarks && fires.fetch().length > 0) {
const union = firesUnion(fires); const union = firesUnion(fires);
@ -58,3 +58,8 @@ Meteor.publish('activefiresmyloc', function activeInMyLoc(northEastLng, northEas
return activefires(northEastLng, northEastLat, southWestLng, southWestLat, withMarks); return activefires(northEastLng, northEastLat, southWestLng, southWestLat, withMarks);
}); });
// Warning: this increase always by one the fire stats
Meteor.publish('lastFireDetected', function lastFireDetected() {
return ActiveFires.find({}, { limit: 1, sort: { when: -1 } });
});

View file

@ -0,0 +1,155 @@
/* eslint-disable import/no-absolute-path */
import getPrivateFile from '/imports/modules/server/get-private-file';
import { Meteor } from 'meteor/meteor';
import tbuffer from '@turf/buffer';
import emojiFlags from 'emoji-flags';
import tweet from '/imports/modules/server/twitter';
import countFiresInRegions from './countFires';
const debug = false;
const autonomies = tbuffer(JSON.parse(getPrivateFile('data/es-atlas-autonomies.json')), 0);
const portugal = tbuffer(JSON.parse(getPrivateFile('data/pt-atlas.json')), 0);
const europe = tbuffer(JSON.parse(getPrivateFile('data/europe-atlas.json')), 0);
const stringsToRemoveIberia = [
['Ciudad Autónoma de', ''],
['País Vasco/', ''],
['Pais Vasco', 'Euskadi'],
['Comunidad Foral de', ''],
['Región de', ''],
['Valencia', 'CValenciana'],
['Comunidad de Madrid', 'CMadrid'],
['León', 'CyLeón'],
['Leon', 'CyLeón'],
['Castilla-', ''],
['Cataluña/', ''],
['Cataluña', 'Catalunya'],
['La Mancha', 'CLMancha'],
['Baleares', 'IllesBalears'],
['Galicia', 'Galiza'],
['Principado de', ''],
[' ', '']
];
const emoji = (key, name, fires) => {
const emo = emojiFlags.countryCode(key.code);
if (!emo) {
if (Meteor.isDevelopment) console.log(`Cannot find flag for country ${name}: ${JSON.stringify(key)}`);
return '';
}
return `${emo.emoji} ${fires}`;
};
const composeEuropeTweet = (num, stats, abrev) => {
if (num <= 0) {
return '';
}
const keys = Object.keys(stats);
const numZones = keys.length;
let text = '';
keys.map((key, index) => { // , index
const zoneFires = stats[key].count;
const zoneFiresText = zoneFires > 1 ? index === 0 ? zoneFires : `${zoneFires}` : index === 0 ? 'One' : '1';
const what = index > 0 ? ' ' : zoneFires > 1 ? ' active #wildfires ' : ' active #wildfire ';
const subText = abrev ? emoji(stats[key], key, zoneFires) :
`${zoneFiresText}${what}in #{key}`;
let delimiter;
if (!abrev) {
delimiter = index === numZones - 1 ? ' and' : ',';
} else {
delimiter = ',';
}
const subTexts = index === 0 ? subText : `${delimiter} ${subText}`;
text += subTexts;
return '';
});
return `${text}`;
};
const composeIberiaTweet = (num, stats) => {
if (num <= 0) {
return '';
}
const keys = Object.keys(stats);
const numZones = keys.length;
let text = '';
keys.map((key, index) => { // , index
const zoneFires = stats[key].count;
const zoneFiresText = zoneFires > 1 ? index === 0 ? zoneFires : `otros ${zoneFires}` : index === 0 ? 'Un' : 'otro';
const what = index > 0 ? ' ' : zoneFires > 1 ? ' #fuegos activos ' : ' #fuego activo ';
const subText = `${zoneFiresText}${what}en #${key}`;
const delimiter = index === numZones - 1 ? ' y' : ',';
const subTexts = index === 0 ? subText : `${delimiter} ${subText}`;
text += subTexts;
return '';
});
return `${text}`;
};
const tweetHeaders = ['🔥'];
const tweetFooters = ['Más info en: https://fuegos.comunes.org/fires'];
const correctTweetSize = (tweetText) => {
const length = tweetText.length;
if (length > 280) {
if (Meteor.isDevelopment) console.log(`Tweet is too big (${length}): ${tweetText}`);
return false;
}
return true;
};
const tweetEuropeFires = () => {
const resultEu = countFiresInRegions(europe, [[' ', '']]);
if (resultEu.total > 0) {
let tweetText = `${tweetHeaders[0].trim()} ${composeEuropeTweet(resultEu.total, resultEu.fires, false)}. More info: https://fires.comunes.org/fires`;
if (!correctTweetSize(tweetText)) {
tweetText = `${tweetHeaders[0].trim()}Active #wildfires in #Europe:\n${composeEuropeTweet(resultEu.total, resultEu.fires, true)}. More info: https://fires.comunes.org/fires`;
}
if (!correctTweetSize(tweetText)) {
tweetText = `${tweetHeaders[0].trim()}Active #wildfires in #Europe:\n${composeEuropeTweet(resultEu.total, resultEu.fires, true)}`;
}
if (!correctTweetSize(tweetText)) {
tweetText = `${tweetHeaders[0].trim()} There are ${resultEu.total} active #wildfires in #Europe. More info: https://fires.comunes.org/fires`;
}
if (Meteor.isDevelopment) {
console.log(tweetText);
} else {
tweet(tweetText, 'en');
}
return tweetText;
}
};
const tweetIberiaFires = () => {
const resultEs = countFiresInRegions(autonomies, stringsToRemoveIberia);
const resultPt = countFiresInRegions(portugal, stringsToRemoveIberia);
const fires = Object.assign(resultEs.fires, resultPt.fires);
const total = resultEs.total + resultPt.total;
// console.log(`Total fires ${total}, ${JSON.stringify(fires)}`);
if (total > 0) {
const tweetText = `${tweetHeaders[0].trim()} ${composeIberiaTweet(total, fires)}. ${tweetFooters[0]}`;
if (correctTweetSize(tweetText)) {
if (Meteor.isDevelopment) {
console.log(tweetText);
} else {
tweet(tweetText, 'es');
}
}
return tweetText;
}
return '';
};
if (Meteor.isDevelopment && debug) {
tweetIberiaFires();
tweetEuropeFires();
}
export { composeIberiaTweet, tweetHeaders, tweetFooters, tweetIberiaFires, tweetEuropeFires };

View file

@ -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/
// <field>: { type: <GeoJSON type> , coordinates: <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;

View file

@ -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 } });
});

View file

@ -3,4 +3,5 @@ import moment from 'moment-timezone';
export const momentTz = date => moment.tz(date, moment.tz.guess()); export const momentTz = date => moment.tz(date, moment.tz.guess());
export const dateLongFormat = date => momentTz(date).format('LLLL (z)'); export const dateLongFormat = date => momentTz(date).format('LLLL (z)');
export const dateParseShortFormat = date => moment(date, 'YYYY-MM-DD').format('LL'); export const dateParseShortFormat = date => moment(date, 'YYYY-MM-DD').format('LL');
export const dateYYYYMMDD = date => moment(date).format('YYYY-MM-DD');
export const dateFromNow = date => momentTz(date).fromNow(); export const dateFromNow = date => momentTz(date).fromNow();

View file

@ -2,6 +2,7 @@
const types = { const types = {
industry: 'Es una industria', industry: 'Es una industria',
// TODO volcano: 'Es un volcán',
controled: 'Es una quema controlada', controled: 'Es una quema controlada',
falsealarm: 'Parece una falsa alarma' falsealarm: 'Parece una falsa alarma'
}; };

View file

@ -6,6 +6,29 @@ import FalsePositiveTypes from './FalsePositiveTypes';
import Fires from '../Fires/Fires'; import Fires from '../Fires/Fires';
import rateLimit from '../../modules/rate-limit'; import rateLimit from '../../modules/rate-limit';
export function upsertFalsePositive(keyType, owner, fire) {
const fireType = fire.type;
if (fireType === 'vecinal') {
throw new Meteor.Error('500', 'No se puede marcar este tipo de fuego');
}
const date = new Date();
const formated = moment(date).format('YYYYMMDD');
try {
const set = {
owner,
type: keyType,
when: date,
whendateformat: formated,
fireId: fire._id,
geo: fire.ourid
};
return FalsePositives.upsert({ geo: fire.ourid, owner }, { $set: set }, { multi: false, upsert: true });
} catch (exception) {
console.log(exception);
throw new Meteor.Error('500', exception);
}
}
Meteor.methods({ Meteor.methods({
'falsePositives.insert': function falsePositivesInsert(fireId, keyType) { 'falsePositives.insert': function falsePositivesInsert(fireId, keyType) {
check(fireId, Meteor.Collection.ObjectID); check(fireId, Meteor.Collection.ObjectID);
@ -17,27 +40,9 @@ Meteor.methods({
} }
check(this.userId, String); check(this.userId, String);
const fire = Fires.findOne(fireId); const fire = Fires.findOne(fireId);
const owner = this.userId;
if (fire) { if (fire) {
const fireType = fire.type; upsertFalsePositive(keyType, owner, fire);
if (fireType === 'vecinal') {
throw new Meteor.Error('500', 'No se puede marcar este tipo de fuego');
}
const date = new Date();
const formated = moment(date).format('YYYYMMDD');
try {
const set = {
owner: this.userId,
type: keyType,
when: date,
whendateformat: formated,
fireId,
geo: fire.ourid
};
return FalsePositives.upsert({ geo: fire.ourid, owner: this.userId }, { $set: set }, { multi: false, upsert: true });
} catch (exception) {
console.log(exception);
throw new Meteor.Error('500', exception);
}
} else { } else {
throw new Meteor.Error('500', 'Fuego no encontrado'); throw new Meteor.Error('500', 'Fuego no encontrado');
} }

View file

@ -5,7 +5,8 @@
import { Meteor } from 'meteor/meteor'; import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check'; import { check } from 'meteor/check';
import { NumberBetween } from '/imports/modules/server/other-checks'; 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 calcUnion from '/imports/ui/components/Maps/SubsUnion/Unify';
import Industries from '/imports/api/Industries/Industries'; import Industries from '/imports/api/Industries/Industries';
import FalsePositives from '../FalsePositives'; import FalsePositives from '../FalsePositives';
@ -17,12 +18,26 @@ Meteor.publish('falsePositivesTotal', function total() {
}); });
export const firesUnion = (fires) => { export const firesUnion = (fires) => {
const group = new L.FeatureGroup(); const firesArray = Array.isArray(fires) ? fires : fires.fetch(); // if not is a cursor
const remap = fires.fetch().map(function remap(doc) { const remap = firesArray.map(function remap(doc) {
// default scan: 1 for neightbor alerts const isNASA = doc.type === 'modis' || doc.type === 'viirs';
return { location: { lat: doc.lat, lon: doc.lon }, distance: doc.scan || 1 }; 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 remap = [{ location: { lat, lon }, distance }];
const union = calcUnion(L, remap, sub => sub, true);
return union; return union;
}; };

View file

@ -33,9 +33,10 @@ Meteor.publish('fireAlerts', function fireAlerts(northEastLng, northEastLat, sou
fields: { fields: {
lat: 1, lat: 1,
lon: 1, lon: 1,
scan: 1 scan: 1,
track: 1
} }
}); });
// console.log(`Fires total: ${fires.count()}`); if (Meteor.isDevelopment) console.log(`Neighbour alerts total: ${fires.count()}`);
return fires; return fires;
}); });

View file

@ -12,6 +12,7 @@ import FireAlertsCollection from '/imports/api/FireAlerts/FireAlerts';
import { whichAreFalsePositives, firesUnion } from '/imports/api/FalsePositives/server/publications'; import { whichAreFalsePositives, firesUnion } from '/imports/api/FalsePositives/server/publications';
import FalsePositives from '/imports/api/FalsePositives/FalsePositives'; import FalsePositives from '/imports/api/FalsePositives/FalsePositives';
import Industries from '/imports/api/Industries/Industries'; import Industries from '/imports/api/Industries/Industries';
import ravenLogger from '/imports/startup/server/ravenLogger';
import FiresCollection from '../Fires'; import FiresCollection from '../Fires';
@ -132,32 +133,46 @@ Meteor.publish('fireFromId', function fireFromId(_id) {
} }
}); });
Meteor.publish('fireFromHash', function fireFromHash(fireEnc) { function logUrl(fireEnc, params) {
const message = `Wrong fire: ${fireEnc}. Params received in url: ${JSON.stringify(params)}`;
console.log(message);
ravenLogger.log(message);
}
export function fireFromHash(fireEnc, params) {
check(fireEnc, String); check(fireEnc, String);
check(params, Object);
// console.log(fireEnc);
// const unsealed = Promise.await(urlEnc.decrypt(fireEnc));
const unsealed = Promise.await(unsealW(fireEnc));
if (unsealed === undefined) {
throw Error(`Fail to unseal '${fireEnc}'`);
}
const w = unsealed.when;
unsealed.when = new Date(w);
const c = unsealed.createdAt;
unsealed.createdAt = !c ? new Date() : new Date(c);
const u = unsealed.updatedAt;
unsealed.updatedAt = !u ? new Date() : new Date(u);
// console.log(unsealed);
// FIXME:
const unsealedFix = fixConfidence(unsealed);
FiresCollection.schema.validate(unsealedFix);
return findOrCreateFire(unsealedFix);
/* console.log(`fires: ${fire.count()}`);
* return fire; */
}
Meteor.publish('fireFromHash', function fireFromHashFunc(fireEnc, params) {
check(fireEnc, String);
check(params, Object);
try { try {
// console.log(fireEnc); return fireFromHash(fireEnc, params);
// const unsealed = Promise.await(urlEnc.decrypt(fireEnc));
const unsealed = Promise.await(unsealW(fireEnc));
if (unsealed === undefined) {
console.info(`Wrong fire: ${fireEnc}`);
// https://guide.meteor.com/data-loading.html
return this.ready();
}
const w = unsealed.when;
unsealed.when = new Date(w);
const c = unsealed.createdAt;
unsealed.createdAt = !c ? new Date() : new Date(c);
const u = unsealed.updatedAt;
unsealed.updatedAt = !u ? new Date() : new Date(u);
// console.log(unsealed);
// FIXME:
const unsealedFix = fixConfidence(unsealed);
FiresCollection.schema.validate(unsealedFix);
return findOrCreateFire(unsealedFix);
/* console.log(`fires: ${fire.count()}`);
* return fire; */
} catch (e) { } catch (e) {
console.warn(e); console.warn(e);
logUrl(fireEnc, params);
// https://guide.meteor.com/data-loading.html
return this.ready(); return this.ready();
} }
}); });

View file

@ -2,6 +2,7 @@
/* eslint-disable import/no-absolute-path */ /* eslint-disable import/no-absolute-path */
import { Mongo } from 'meteor/mongo'; import { Mongo } from 'meteor/mongo';
import { Meteor } from 'meteor/meteor';
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import { defaultCreatedAt, defaultUpdateAt } from '/imports/api/Utility/Utils.js'; import { defaultCreatedAt, defaultUpdateAt } from '/imports/api/Utility/Utils.js';
import LocationSchema from '/imports/api/Utility/LocationSchema.js'; import LocationSchema from '/imports/api/Utility/LocationSchema.js';
@ -22,11 +23,12 @@ Notifications.deny({
Notifications.schema = new SimpleSchema({ Notifications.schema = new SimpleSchema({
userId: String, userId: String,
subsId: { type: Meteor.Collection.ObjectID, optional: true, blackbox: true },
content: String, content: String,
geo: LocationSchema, geo: LocationSchema,
type: String, type: String,
webNotified: { type: Boolean, optional: true }, notified: { type: Boolean, optional: true },
webNotifiedAt: { type: Date, optional: true }, notifiedAt: { type: Date, optional: true },
emailNotified: { type: Boolean, optional: true }, emailNotified: { type: Boolean, optional: true },
emailNotifiedAt: { type: Date, optional: true }, emailNotifiedAt: { type: Date, optional: true },
when: Date, when: Date,

View file

@ -8,7 +8,7 @@ Meteor.methods({
check(notifId, Meteor.Collection.ObjectID); check(notifId, Meteor.Collection.ObjectID);
try { try {
Notifications.update(notifId, { $set: { webNotified: true, webNotifiedAt: new Date() } }); Notifications.update(notifId, { $set: { notified: true, notifiedAt: new Date() } });
return notifId; return notifId;
} catch (exception) { } catch (exception) {
throw new Meteor.Error('500', exception); throw new Meteor.Error('500', exception);

View file

@ -4,7 +4,7 @@ import { Meteor } from 'meteor/meteor';
import Notifications from '../Notifications'; import Notifications from '../Notifications';
Meteor.publish('mynotifications', function notifications() { Meteor.publish('mynotifications', function notifications() {
const notif = Notifications.find({ userId: this.userId, type: 'web', webNotified: null }); const notif = Notifications.find({ userId: this.userId, type: 'web', notified: null });
// console.log(`Notifications for user ${this.userId}: ${notif.count()}`); // console.log(`Notifications for user ${this.userId}: ${notif.count()}`);
return notif; return notif;
}); });

457
imports/api/Rest/Rest.js Normal file
View file

@ -0,0 +1,457 @@
/* global Restivus */
/* eslint-disable import/no-absolute-path */
import { Meteor } from 'meteor/meteor';
import { NumberBetween } from '/imports/modules/server/other-checks';
import Fires from '/imports/api/Fires/Fires';
import { check } from 'meteor/check';
import SiteSettings from '/imports/api/SiteSettings/SiteSettings';
import ActiveFiresCollection from '/imports/api/ActiveFires/ActiveFires';
import { countRealFires } from '/imports/api/ActiveFires/server/countFires';
import { whichAreFalsePositives, firesUnion, zoneToUnion } from '/imports/api/FalsePositives/server/publications';
import { fireFromHash } from '/imports/api/Fires/server/publications.js';
import FalsePositives from '/imports/api/FalsePositives/FalsePositives';
import Industries from '/imports/api/Industries/Industries';
import Subscriptions from '/imports/api/Subscriptions/Subscriptions';
import jsend from 'jsend';
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();
const restivusError = (code, message) => ({ status: 'error', statusCode: code, body: message });
function failMsg(msg) {
return restivusError(500, `Unexpected error in REST call: ${msg}`);
}
function fail(e) {
return restivusError(500, `Unexpected error in REST call: ${e}`);
}
function defaultFailParams(e) {
return restivusError(400, `Wrong REST params: ${e}`);
}
function checkAuthToken(token) {
if (!Meteor.settings.private.internalApiToken || token !== Meteor.settings.private.internalApiToken) {
const message = 'Unauthorized auth token in REST API';
console.warn(message);
return restivusError(401, message);
}
return undefined;
}
function checkLatLonDist(km, lat, lng) {
check(lng, NumberBetween(-180, 180));
check(lat, NumberBetween(-90, 90));
check(km, NumberBetween(0, Meteor.isDevelopment ? 1000 : 100));
}
if (!Meteor.settings.private.internalApiToken) {
console.warn('Meteor.settings.private.internalApiToken is not configured so we don\'t enable our REST API');
} else {
// export
const apiV1 = new Restivus({
useDefaultAuth: true,
apiPath: 'api',
version: 'v1',
prettyJson: true
});
// Generates: POST on /api/users and GET, DELETE /api/users/:id for
// Meteor.users collection
/* apiV1.addCollection(Meteor.users, {
* excludedEndpoints: ['getAll', 'put', 'delete', 'patch', 'get'],
* routeOptions: {
* authRequired: true
* },
* endpoints: {
* post: {
* authRequired: false
* },
* delete: {
* roleRequired: 'admin'
* }
* }
* }); */
apiV1.addCollection(Fires, {
excludedEndpoints: ['put', 'post', 'patch', 'delete'],
// excludedEndpoints: ['getAll', 'put', 'post', 'patch', 'delete'],
routeOptions: {
authRequired: false
},
endpoints: {
get: {
action: function getFire() {
return Fires.findOne(new Meteor.Collection.ObjectID(this.urlParams.id));
}
}
}
});
// Maps to: /api/v1/status/last-fire-check
apiV1.addRoute('status/last-fire-check', { authRequired: false }, {
get: function get() {
return SiteSettings.findOne({ name: 'last-fire-check' });
}
});
// Maps to: /api/v1/status/last-fire-detected
apiV1.addRoute('status/last-fire-detected', { authRequired: false }, {
get: function get() {
return ActiveFiresCollection.findOne({}, { sort: { when: -1 } });
}
});
// Maps to: /api/v1/status/active-fires-count
apiV1.addRoute('status/active-fires-count', { authRequired: false }, {
get: function get() {
return { total: ActiveFiresCollection.find({}).count() };
}
});
// Maps to: /api/v1/status/uptime
apiV1.addRoute('status/uptime', { authRequired: false }, {
get: function get() {
return { ms: new Date() - uptime };
}
});
function getFires(route, full) {
const lat = Number(route.urlParams.lat);
const lng = Number(route.urlParams.lng);
const km = Number(route.urlParams.km);
const { token } = route.urlParams;
try {
checkLatLonDist(km, lat, lng);
check(token, String);
} catch (e) {
return defaultFailParams(e);
}
const failed = checkAuthToken(token);
if (failed) return failed;
if (debug) console.log(`Query for fires in ${lat}, ${lng} in ${km} km radius`);
const fires = ActiveFiresCollection.find({
ourid: {
$near: {
$geometry: {
type: 'Point',
coordinates: [lng, lat]
},
$maxDistance: km * 1000,
$minDistance: 0
}
}
}, {
fields: {
lat: 1,
lon: 1,
when: 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;
}
let union;
// TODO only get real
const firesA = fires.fetch();
if (firesA.length > 0) {
union = firesUnion(fires);
} else {
union = zoneToUnion(lat, lng, km);
}
const falsePos = whichAreFalsePositives(FalsePositives, union);
const industries = whichAreFalsePositives(Industries, union);
result.fires = firesA;
result.industries = industries.fetch();
result.falsePos = falsePos.fetch();
return result;
}
// Maps to: /api/v1/fires-in/:lat/:lng/:km
// 100 km max
// Ex: http://127.0.0.1:3000/api/v1/fires-in/token/38.736946/-9.142685/100
apiV1.addRoute('fires-in/:token/:lat/:lng/:km', { authRequired: false }, {
get: function get() {
return getFires(this, false);
}
});
apiV1.addRoute('fires-in-full/:token/:lat/:lng/:km', { authRequired: false }, {
get: function get() {
return getFires(this, true);
}
});
// Add mobile user:
// curl -X POST http://localhost:3000/api/v1/users/mobile -d "token: thisAppAutToken" -d "mobileToken: user-mobile-firebase-token"
// Response:
//
// https://docs.meteor.com/api/passwords.html#Accounts-createUser
apiV1.addRoute('mobile/users', { authRequired: false }, {
post: function post() {
const { token, mobileToken, lang } = this.bodyParams;
try {
check(token, String);
check(lang, String);
check(mobileToken, String);
} catch (e) {
return defaultFailParams(e);
}
const failed = checkAuthToken(token);
if (failed) return failed;
let username;
const already = Meteor.users.find({ fireBaseToken: mobileToken });
if (already.count() > 1) {
return restivusError(500, 'Unexpected error in REST call: several users with that mobile token?');
} else if (already.count() === 1) {
username = already.fetch()[0].username;
} else {
do {
username = Random.id(15);
} while (Meteor.users.find({ username }).count() !== 0);
}
// FIXME check valid lang
const now = new Date();
const result = Meteor.users.upsert({
fireBaseToken: mobileToken
}, {
$set: {
username,
fireBaseToken: mobileToken,
lang,
profile: { },
createdAt: now,
updatedAt: now
}
});
if (debug) {
console.log(this.bodyParams);
console.log(this.urlParams);
console.log(this.queryParams);
}
const upsertUser = Meteor.users.findOne({ username });
if (debug) console.log(upsertUser);
return jsend.success({
upsertResult: result,
username,
userId: upsertUser._id,
lang: upsertUser.lang,
mobileToken: upsertUser.fireBaseToken
});
}
});
// max sitance: 100km
apiV1.addRoute('mobile/subscriptions', { authRequired: false }, {
post: function post() {
const {
token,
mobileToken,
id,
lat,
lon,
distance
} = this.bodyParams;
let oid;
try {
check(token, String);
check(mobileToken, String);
check(id, String);
oid = new Mongo.ObjectID(id);
check(oid, Meteor.Collection.ObjectID);
check(lat, Number);
check(lon, Number);
check(distance, Number);
checkLatLonDist(distance, lat, lon);
} catch (e) {
if (debug) console.log(e);
return defaultFailParams(e);
}
const failed = checkAuthToken(token);
if (failed) return failed;
const user = Meteor.users.findOne({ fireBaseToken: mobileToken });
if (!user) return failMsg('User not found');
const newSubs = {};
newSubs._id = new Meteor.Collection.ObjectID(id);
newSubs.location = {};
newSubs.location.lat = lat;
newSubs.location.lon = lon;
newSubs.distance = distance;
let result;
try {
result = subscriptionsInsert(newSubs, user._id, 'mobile');
} catch (e) {
return fail(e);
}
return jsend.success({ subsId: result._str });
}
});
apiV1.addRoute('mobile/subscriptions/:token/:mobileToken/:subsId', { authRequired: false }, {
delete: function del() {
const {
token,
mobileToken,
subsId
} = this.urlParams;
try {
check(token, String);
check(mobileToken, String);
check(subsId, String);
} catch (e) {
return defaultFailParams(e);
}
const failed = checkAuthToken(token);
if (failed) return failed;
const user = Meteor.users.findOne({ fireBaseToken: mobileToken });
if (!user) return failMsg('User not found');
try {
Subscriptions.remove({ owner: user._id, _id: new Meteor.Collection.ObjectID(subsId) });
} catch (e) {
return fail(e);
}
return jsend.success({});
}
});
apiV1.addRoute('mobile/subscriptions/all/:token/:mobileToken', { authRequired: false }, {
get: function get() {
const { token, mobileToken } = this.urlParams;
try {
check(token, String);
check(mobileToken, String);
} catch (e) {
return defaultFailParams(e);
}
const failed = checkAuthToken(token);
if (failed) return failed;
const user = Meteor.users.findOne({ fireBaseToken: mobileToken });
if (!user) return failMsg('User not found');
const result = Subscriptions.find({ owner: user._id });
return jsend.success({ subscriptions: result.fetch(), count: result.count() });
},
delete: function delAll() {
const { token, mobileToken } = this.urlParams;
try {
check(token, String);
check(mobileToken, String);
} catch (e) {
return defaultFailParams(e);
}
const failed = checkAuthToken(token);
if (failed) return failed('Auth api check failed');
if (Meteor.users.find({ fireBaseToken: mobileToken }).count() !== 1) return fail;
const user = Meteor.users.findOne({ fireBaseToken: mobileToken });
if (!user) return failMsg('User not found');
const toRemove = Subscriptions.find({ owner: user._id }).count();
Subscriptions.remove({ owner: user._id });
return jsend.success({ count: toRemove });
}
});
apiV1.addRoute('status/subs-public-union/:token', { authRequired: false }, {
get: function get() {
const { token } = this.urlParams;
try {
check(token, String);
} catch (e) {
return defaultFailParams(e);
}
const failed = checkAuthToken(token);
if (failed) return failed;
const currentUnion = SiteSettings.findOne({ name: 'subs-public-union' });
const userSubsBounds = SiteSettings.findOne({ name: 'subs-public-union-bounds' });
return jsend.success({ union: currentUnion, bounds: userSubsBounds });
}
});
apiV1.addRoute('mobile/falsepositive', { authRequired: false }, {
post: function post() {
const {
token,
mobileToken,
sealed,
type
} = this.bodyParams;
try {
check(token, String);
check(mobileToken, String);
check(sealed, String);
check(type, String);
} catch (e) {
if (debug) console.log(e);
return defaultFailParams(e);
}
const failed = checkAuthToken(token);
if (failed) return failed;
const user = Meteor.users.findOne({ fireBaseToken: mobileToken });
if (!user) return failMsg('User not found');
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');
}
});
}

View file

@ -1,6 +1,7 @@
/* eslint-disable consistent-return */ /* eslint-disable consistent-return */
/* eslint-disable import/no-absolute-path */ /* eslint-disable import/no-absolute-path */
import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo'; import { Mongo } from 'meteor/mongo';
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import { defaultCreatedAt, defaultUpdateAt } from '/imports/api/Utility/Utils.js'; import { defaultCreatedAt, defaultUpdateAt } from '/imports/api/Utility/Utils.js';
@ -42,12 +43,14 @@ Subscriptions.deny({
Subscriptions.schema = new SimpleSchema({ Subscriptions.schema = new SimpleSchema({
_id: { type: Meteor.Collection.ObjectID, optional: true, blackbox: true },
location: Object, location: Object,
'location.lat': Number, 'location.lat': Number,
'location.lon': Number, 'location.lon': Number,
geo: LocationSchema, geo: LocationSchema,
distance: Number, distance: Number,
chatId: { type: Number, optional: true }, // only in 'telegram' type chatId: { type: Number, optional: true }, // only in 'telegram' type
telegramBot: { type: String, optional: true }, // only in 'telegram' type
owner: String, owner: String,
type: String, type: String,
createdAt: defaultCreatedAt, createdAt: defaultCreatedAt,

View file

@ -10,30 +10,49 @@ function geo(doc) {
}; };
} }
export function subscriptionsInsert(doc, userId, type) {
check(doc, {
_id: Match.Maybe(Meteor.Collection.ObjectID),
location: Match.ObjectIncluding({ lat: Number, lon: Number }),
distance: Number
});
const newDoc = {
owner: userId,
type,
geo: geo(doc),
...doc
};
// console.log(newDoc);
const already = Subscriptions.findOne(newDoc);
if (already) {
throw new Meteor.Error('on-already-subscribed', 'The user is already subscribed to this area');
}
try {
return Subscriptions.insert(newDoc);
} catch (exception) {
// console.error(exception);
throw new Meteor.Error('500', exception);
}
}
export function subscriptionsRemove(subscriptionId) {
check(subscriptionId, Meteor.Collection.ObjectID);
try {
return Subscriptions.remove(subscriptionId);
} catch (exception) {
console.error(exception);
throw new Meteor.Error('500', exception);
}
}
Meteor.methods({ Meteor.methods({
'subscriptions.insert': function subscriptionsInsert(doc) { 'subscriptions.insert': function subscriptionInsertMethod(doc) {
check(doc, { check(doc, {
location: Match.ObjectIncluding({ lat: Number, lon: Number }), location: Match.ObjectIncluding({ lat: Number, lon: Number }),
distance: Number distance: Number
}); });
const type = 'web'; return subscriptionsInsert(doc, this.userId, 'web');
const newDoc = {
owner: this.userId,
type,
geo: geo(doc),
...doc
};
// console.log(newDoc);
const already = Subscriptions.findOne(newDoc);
if (already) {
throw new Meteor.Error('on-already-subscribed', 'The user is already subscribed to this area');
}
try {
return Subscriptions.insert(newDoc);
} catch (exception) {
// console.error(exception);
throw new Meteor.Error('500', exception);
}
}, },
'subscriptions.update': function subscriptionsUpdate(doc) { 'subscriptions.update': function subscriptionsUpdate(doc) {
check(doc, { check(doc, {
@ -52,15 +71,9 @@ Meteor.methods({
throw new Meteor.Error('500', exception); throw new Meteor.Error('500', exception);
} }
}, },
'subscriptions.remove': function subscriptionsRemove(subscriptionId) { 'subscriptions.remove': function subscriptionsRemoveMethod(subscriptionId) {
check(subscriptionId, Meteor.Collection.ObjectID); check(subscriptionId, Meteor.Collection.ObjectID);
return subscriptionsRemove(subscriptionId);
try {
return Subscriptions.remove(subscriptionId);
} catch (exception) {
console.error(exception);
throw new Meteor.Error('500', exception);
}
} }
}); });

View file

@ -78,11 +78,8 @@ const schemaUser = new SimpleSchema({
// Option 2: [String] type // Option 2: [String] type
// If you are sure you will never need to use role groups, then // If you are sure you will never need to use role groups, then
// you can specify [String] as the type // you can specify [String] as the type
roles: { roles: { type: Array, optional: true },
type: Array, 'roles.$': { type: String, optional: true },
// autoform: { type: 'hidden' },
optional: true
},
// https://github.com/todda00/meteor-friendly-slugs/issues/1 // https://github.com/todda00/meteor-friendly-slugs/issues/1
friendlySlugs: { friendlySlugs: {
type: Object, type: Object,
@ -110,6 +107,7 @@ const schemaUser = new SimpleSchema({
telegramUsername: { type: String, optional: true }, telegramUsername: { type: String, optional: true },
telegramFirstName: { type: String, optional: true }, telegramFirstName: { type: String, optional: true },
telegramLanguageCode: { type: String, optional: true }, telegramLanguageCode: { type: String, optional: true },
fireBaseToken: { type: String, optional: true },
createdAt: defaultCreatedAt, createdAt: defaultCreatedAt,
updatedAt: defaultUpdateAt updatedAt: defaultUpdateAt
}); });

View file

@ -12,10 +12,10 @@ const LocationSchema = new SimpleSchema({
minCount: 2, minCount: 2,
maxCount: 2, maxCount: 2,
custom: function custom() { custom: function custom() {
if (!(this.value[0] >= -90 && this.value[0] <= 90)) { if (!(this.value[0] >= -180 && this.value[0] <= 180)) {
return 'lngOutOfRange'; return 'lngOutOfRange';
} }
if (!(this.value[1] >= -180 && this.value[1] <= 180)) { if (!(this.value[1] >= -90 && this.value[1] <= 90)) {
return 'latOutOfRange'; return 'latOutOfRange';
} }
return true; return true;
@ -27,8 +27,8 @@ const LocationSchema = new SimpleSchema({
}); });
LocationSchema.messageBox.messages({ LocationSchema.messageBox.messages({
lonOutOfRange: '[label] longitude should be between -90 and 90', latOutOfRange: '[label] latitude should be between -90 and 90',
latOutOfRange: '[label] latitude should be between -180 and 180' lngOutOfRange: '[label] longitude should be between -180 and 180'
}); });
export default LocationSchema; export default LocationSchema;

View file

@ -1,5 +1,7 @@
/* eslint-disable import/no-absolute-path */
import fs from 'fs'; import fs from 'fs';
import getFallbackLang from '/imports/startup/server/accounts/lang-fallback'; import getFallbackLang from '/imports/modules/lang-fallback';
const as = f => `assets/app/${f}`; const as = f => `assets/app/${f}`;

View file

@ -0,0 +1,19 @@
/* eslint-disable import/no-absolute-path */
import getOAuthProfile from '/imports/modules/get-oauth-profile';
const getEmailOf = (user) => {
const hasPassword = user.services && user.services.password && user.services.password.bcrypt;
// console.log(`Has password: ${hasPassword}`);
const OAuthProfile = getOAuthProfile({
password: hasPassword,
profile: user.profile
}, user);
const firstName = OAuthProfile ? OAuthProfile.name.first : user.profile.name.first;
const emailAddress = OAuthProfile ? OAuthProfile.email :
user && user.emails[0] && user.emails[0].verified ? user.emails[0].address : null;
return { emailAddress, firstName };
};
export default getEmailOf;

View file

@ -1,5 +1,6 @@
const getFallbackLang = (lang) => { const getFallbackLang = (lang) => {
if (lang && (lang === 'ast' || lang === 'gl' || lang === 'eu' || lang === 'ca' || lang.match(/^es-/))) { if (lang && (lang === 'ast' || lang === 'gl' || lang === 'eu' ||
lang === 'es' || lang === 'ca' || lang.match(/^es-/))) {
return 'es'; return 'es';
} }
return 'en'; return 'en';

View file

@ -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 `<img src="${imgUrl(lat, lng)}" width="640" height="480"/>`;
} */
// 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 = `<a href="${fireUrl}">${i18n.t('Más información sobre este fuego')}</a>`;
// 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: '<body><h2>{{appName}}</h2>{{{html}}}</body>',
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;

View file

@ -1,11 +1,21 @@
/* eslint-disable import/no-absolute-path */
import { Meteor } from 'meteor/meteor'; import { Meteor } from 'meteor/meteor';
// import { Email } from 'meteor/email'; import i18n from 'i18next';
import { getFileNameOfLang } from '/imports/api/Utility/server/files.js';
import sendMail from '/imports/startup/server/email';
import getPrivateFile from './get-private-file'; import getPrivateFile from './get-private-file';
import templateToText from './handlebars-email-to-text'; import templateToText from './handlebars-email-to-text';
import templateToHTML from './handlebars-email-to-html'; import templateToHTML from './handlebars-email-to-html';
import { getFileNameOfLang } from '/imports/api/Utility/server/files.js';
import sendMail from '/imports/startup/server/email'; export function subjectTruncate(n = 70, useWordBoundary = true) {
import i18n from 'i18next'; // https://stackoverflow.com/questions/1199352/smart-way-to-shorten-long-strings-with-javascript
if (this.length <= n) {
return `${this}`;
}
const subString = this.substr(0, n - 1);
return `${useWordBoundary ? subString.substr(0, subString.lastIndexOf(' ')) : subString}...`;
}
const sendEmail = (options, { resolve, reject }) => { const sendEmail = (options, { resolve, reject }) => {
try { try {

View file

@ -0,0 +1,27 @@
import { Meteor } from 'meteor/meteor';
import Twit from 'twit';
// https://www.npmjs.com/package/twit
const confTwitter = (lang) => {
const tConf = Meteor.settings.private.twitter[lang];
const T = new Twit({
consumer_key: tConf.consumer_key,
consumer_secret: tConf.consumer_secret,
access_token: tConf.access_token,
access_token_secret: tConf.access_token_secret
// timeout_ms: 60*1000, // optional HTTP request timeout to apply to all requests.
});
return T;
};
const tweet = (message, lang) => {
const T = confTwitter(lang);
T.post('statuses/update', { status: message }, (err, data, response) => {
// console.log(data);
// console.log(response);
});
};
export default tweet;

View file

@ -17,7 +17,7 @@ import i18nOpts from '../common/i18n';
const detectorOptions = { const detectorOptions = {
// order and from where user language should be detected // 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 // keys or params to lookup language from
lookupQuerystring: 'lng', 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) i18n.use(backend)
.use(LngDetector) .use(LngDetector)
.use(Cache) .use(Cache)
@ -70,16 +78,17 @@ i18n.use(backend)
console.error(err); console.error(err);
return; return;
} }
i18nReady.set(true); console.log(`Language initialized: ${i18n.language}`);
// document.title = t('AppName'); // document.title = t('AppName');
// Accounts translation // Accounts translation
// https://github.com/softwarerero/meteor-accounts-t9n // https://github.com/softwarerero/meteor-accounts-t9n
// console.log("Language: " + i18n.language); setT9(i18n.language);
T9n.setLanguage(i18n.language);
// console.log(T9n.get('error.accounts.User not found')); // console.log(T9n.get('error.accounts.User not found'));
moment.locale(i18n.language); moment.locale(i18n.language);
i18nReady.set(true);
// cookies eu consent // cookies eu consent
const cookiesOpt = { const cookiesOpt = {
cookieTitle: t('Uso de Cookies'), cookieTitle: t('Uso de Cookies'),
@ -103,7 +112,8 @@ Meteor.subscribe('userData'); // lang is there
i18n.on('languageChanged', (lng) => { i18n.on('languageChanged', (lng) => {
moment.locale(lng); moment.locale(lng);
T9n.setLanguage(lng); // TODO fix this when gl is translated
setT9(lng);
Tracker.autorun((computation) => { Tracker.autorun((computation) => {
if (Meteor.userId()) { if (Meteor.userId()) {
// logged // logged

View file

@ -1,8 +1,19 @@
/* global */
import React from 'react'; import React from 'react';
import { render } from 'react-dom'; import { render } from 'react-dom';
import { Meteor } from 'meteor/meteor'; import { Meteor } from 'meteor/meteor';
import '/imports/startup/client/modernizr';
import App from '../../ui/layouts/App/App'; import App from '../../ui/layouts/App/App';
import '../../ui/stylesheets/app.scss'; import '../../ui/stylesheets/app.scss';
Meteor.startup(() => render(<App />, document.getElementById('react-root'))); // https://stackoverflow.com/questions/19562207/jquery-detect-browser-ie9-and-below-and-throw-up-a-modal-to-upgrade
const isIE9OrBelow = () => /MSIE\s/.test(navigator.userAgent) && parseFloat(navigator.appVersion.split('MSIE')[1]) < 10;
if (isIE9OrBelow()) window.alert('You are using an outdated browser. Please use Chrome or Firefox to display this site.');
else {
Modernizr.on('webp', (result) => {
if (Meteor.isDevelopment) console.log(`webp ${result ? '' : 'not '}supported`);
});
Meteor.startup(() => render(<App />, document.getElementById('react-root')));
}

File diff suppressed because one or more lines are too long

View file

@ -1,8 +1,7 @@
Meteor.startup(function() { import { Meteor } from 'meteor/meteor';
return Tracker.autorun(function() { import { Tracker } from 'meteor/tracker';
var userId;
userId = Meteor.userId(); Meteor.startup(() => Tracker.autorun(() => {
// console.log(userId); const userId = Meteor.userId();
Meteor.Piwik.setUserInfo(userId); Meteor.Piwik.setUserInfo(userId);
}); }));
});

View file

@ -2,11 +2,15 @@ import RavenLogger from 'meteor/flowkey:raven';
import { Meteor } from 'meteor/meteor'; import { Meteor } from 'meteor/meteor';
const ravenOptions = {}; const ravenOptions = {};
const publicDSN = Meteor.settings.public.sentryPublicDSN;
const enabled = !!publicDSN;
const ravenLogger = new RavenLogger({ const ravenLogger = enabled ? new RavenLogger({
publicDSN: Meteor.settings.public.sentryPublicDSN, // will be used on the client publicDSN, // will be used on the client
shouldCatchConsoleError: true, // default shouldCatchConsoleError: true, // default
trackUser: true // default trackUser: true // default
}, ravenOptions); }, ravenOptions) : { log: (error) => { console.log(error); } };
console.log(`ravenLogger ${enabled ? 'enabled' : 'disabled'}`);
export default ravenLogger; export default ravenLogger;

View file

@ -0,0 +1,8 @@
/* global reconnecToServer */
import { Meteor } from 'meteor/meteor';
Meteor.startup(() => {
// Reconnect Function
reconnectToServer(5000, true);
});

View file

@ -3,7 +3,8 @@
// Client and Server // Client and Server
Comments.config({ Comments.config({
rating: 'likes-and-dislikes', rating: 'likes-and-dislikes',
allowAnonymous: () => true, allowAnonymous: () => false,
allowReplies: () => false, // disabled right now: I don't know hot to get referenceId of replies
anonymousSalt: 'klasddl3lala0l3lasdlas0ol3lasdlao3lasdoaslaldal3lasdclasdlal3lasdladlaq', anonymousSalt: 'klasddl3lala0l3lasdlas0ol3lasdlao3lasdoaslaldal3lasdclasdlal3lasdladlaq',
publishUserFields: { publishUserFields: {
profile: 1 profile: 1

View file

@ -3,6 +3,8 @@ import moment from 'moment';
// Load the js langs // Load the js langs
import es from 'meteor-accounts-t9n/build/es'; import es from 'meteor-accounts-t9n/build/es';
import en from 'meteor-accounts-t9n/build/en'; import en from 'meteor-accounts-t9n/build/en';
// TODO ask for translation of this
// import gl from 'meteor-accounts-t9n/build/gl';
const backOpts = { const backOpts = {
// path where resources get loaded from // path where resources get loaded from
@ -21,7 +23,10 @@ const shouldDebug = (forceDebug && !Meteor.isProduction);
const i18nOpts = { const i18nOpts = {
backend: backOpts, backend: backOpts,
// lng: 'es', // lng: 'es',
fallbackLng: ['es', 'en'], fallbackLng: {
gl: ['es'],
default: ['en']
},
sendMissingTo: 'fallback', sendMissingTo: 'fallback',
interpolation: { interpolation: {
escapeValue: false, // not needed for react!! escapeValue: false, // not needed for react!!
@ -35,7 +40,7 @@ const i18nOpts = {
return value; 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 load: 'languageOnly', // 'es' o 'en', previously: 'all', // es-ES -> es, en-US -> en
debug: shouldDebug, debug: shouldDebug,
ns: 'common', ns: 'common',

View file

@ -1,7 +1,9 @@
/* eslint-disable import/no-absolute-path */
import { Accounts } from 'meteor/accounts-base'; import { Accounts } from 'meteor/accounts-base';
import sendWelcomeEmail from '../../../api/Users/server/send-welcome-email'; import getFallbackLang from '/imports/modules/lang-fallback';
import getOAuthProfile from '../../../modules/get-oauth-profile'; import sendWelcomeEmail from '/imports/api/Users/server/send-welcome-email';
import getFallbackLang from './lang-fallback'; import getOAuthProfile from '/imports/modules/get-oauth-profile';
Accounts.onCreateUser((options, user) => { Accounts.onCreateUser((options, user) => {
const userToCreate = user; const userToCreate = user;

View file

@ -9,6 +9,7 @@ import '../../api/Users/server/publications';
import '../../api/Utility/server/methods'; import '../../api/Utility/server/methods';
import '../../api/ActiveFires/server/publications'; import '../../api/ActiveFires/server/publications';
import '../../api/ActiveFiresUnion/server/publications';
import '../../api/FireAlerts/server/publications'; import '../../api/FireAlerts/server/publications';
import '../../api/Subscriptions/methods'; import '../../api/Subscriptions/methods';

View file

@ -0,0 +1,19 @@
/* eslint-disable import/no-absolute-path */
import { Meteor } from 'meteor/meteor';
import ravenLogger from '/imports/startup/server/ravenLogger';
Meteor.startup(() => {
const bound = Meteor.bindEnvironment((callback) => { callback(); });
process.on('uncaughtException', (err) => {
bound(() => {
console.error(err);
// https://github.com/flowkey/meteor-raven
ravenLogger.log(err);
// https://github.com/nodejs/node-v0.x-archive/blob/master/doc/api/process.markdown#exit-codes
process.exit(7);
});
});
});

View file

@ -0,0 +1,80 @@
/* global Comments */
/* eslint-disable import/no-absolute-path */
import { Meteor } from 'meteor/meteor';
import i18n from 'i18next';
import sendEmail, { subjectTruncate } from '/imports/modules/server/send-email';
import getEmailOf from '/imports/modules/get-email-of-user';
import '../common/comments';
/* import i18n from 'i18next';
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 { hr } from '/imports/startup/server/email';
import getOAuthProfile from '/imports/modules/get-oauth-profile';
import image from 'google-maps-image-api-url';
import { trim } from '/imports/ui/components/NotificationsObserver/util.js'; */
Meteor.startup(() => {
// On Server or Client (preferably on Server)
Comments.config({
onEvent: (name, action, payload) => {
// e.g send a mail
// console.log(`name: ${name}, action: ${action}, payload: ${JSON.stringify(payload)}`);
// Samples:
// name: comment, action: add, payload: {"referenceId":"fire-56a9cc5a5586879eb9cc6dd7","content":"asdfasdf asd fasd fas","userId":"tdK4e43ikjDXct7Gj","isAnonymous":false,"createdAt":"2018-03-04T14:53:40.319Z","likes":[],"dislikes":[],"replies":[],"media":{},"status":"approved","starRatings":[],"ratingScore":0,"_id":"mfAe3HL9qNofhiAnh"}
// name: reply, action: add, payload: {"replyId":"teSLaz4f8RxHjptBm","content":"sas fasdfasdasdf","userId":"tdK4e43ikjDXct7Gj","createdAt":"2018-03-04T14:54:56.965Z","replies":[],"likes":[],"lastUpdatedAt":"2018-03-04T14:54:56.965Z","isAnonymous":false,"status":"approved","media":{},"ratingScore":0,"_id":"DyHt28rF6rTDnC5fP","rootUserId":"tdK4e43ikjDXct7Gj"}
// name: reply, action: like, payload: {"replyId":"teSLaz4f8RxHjptBm","content":"sas fasdfasdasdf","userId":"tdK4e43ikjDXct7Gj","createdAt":"2018-03-04T14:54:56.965Z","replies":[],"likes":["tdK4e43ikjDXct7Gj"],"lastUpdatedAt":"2018-03-04T14:54:56.965Z","isAnonymous":false,"status":"approved","media":{},"ratingScore":1,"_id":"DyHt28rF6rTDnC5fP","ratedUserId":"tdK4e43ikjDXct7Gj","rootUserId":"tdK4e43ikjDXct7Gj"}
// name: comment, action: like, payload: {"_id":"mfAe3HL9qNofhiAnh","ratedUserId":"tdK4e43ikjDXct7Gj"}
// name: reply, action: edit, payload: {"_id":"DyHt28rF6rTDnC5fP","replyId":"teSLaz4f8RxHjptBm","content":"sas fasdfasdasdf dasd fadsfa","userId":"tdK4e43ikjDXct7Gj","createdAt":"2018-03-04T14:54:56.965Z","replies":[],"likes":[],"lastUpdatedAt":"2018-03-04T14:54:56.965Z","isAnonymous":false,"status":"approved","media":{},"ratingScore":0,"starRatings":[],"ratedUserId":"tdK4e43ikjDXct7Gj","rootUserId":"tdK4e43ikjDXct7Gj"}
// name: reply, action: remove, payload: {"_id":"DyHt28rF6rTDnC5fP","rootUserId":"tdK4e43ikjDXct7Gj"}
// name: (comment|reply)
// action: (add|like|edit|remove)
if (name === 'comment' && action.match(/add|like|edit/)) {
if (action === 'add') {
// Check for other users that did comments and send an email (uniq users, and not this user)
// console.log(payload.referenceId);
const query = payload.isAnonymous ?
{ referenceId: payload.referenceId } :
{ referenceId: payload.referenceId, userId: { $ne: payload.userId } };
Comments.getCollection().rawCollection().distinct('userId', query).then((users) => {
// console.log(users);
const path = payload.referenceId.replace(/fire-/, 'fire/archive/');
const fireUrl = Meteor.absoluteUrl(path);
// console.log(fireUrl);
Meteor.users.find({ _id: { $in: users } }).forEach((user) => {
const { firstName, emailAddress } = getEmailOf(user);
// console.log(JSON.stringify(user));
if (emailAddress) {
const emailOpts = {
to: emailAddress,
subject: subjectTruncate.apply(i18n.t('Hay más información sobre un fuego')),
lang: user.lang,
template: 'new-fire-comment',
templateVars: {
applicationName: i18n.t('AppName'),
firstName,
fireUrl
}
};
sendEmail(emailOpts).catch((error) => {
throw new Meteor.Error('500', `${error}`);
});
}
});
});
}
}
if (name === 'reply' && action.match(/add|like|edit/)) {
// Check for other previous users to this thread and send an email
// console.log('Reply op');
}
}
});
});

View file

@ -0,0 +1,135 @@
/* global SyncedCron */
/* eslint-disable import/no-absolute-path */
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/
Meteor.startup(() => {
SyncedCron.config({
// Log job run details to console
log: Meteor.settings.private.cron.debug || false,
// Use a custom logger function (defaults to Meteor's logging package)
// workaround to disable logging succesfully
logger: Meteor.settings.private.cron.debug ? null : () => {}
// Name of collection to use for synchronisation and logging
// collectionName: 'cronHistory',
// Default to localTime
// Options: 'utc', 'localtime', or specific timezones 'America/New_York'
// Will be applied to jobs with no timezone defined
// timezone: 'utc',
/*
TTL in seconds for history records in collection to expire
NOTE: Unset to remove expiry but ensure you remove the index from
mongo by hand
ALSO: SyncedCron can't use the `_ensureIndex` command to modify
the TTL index. The best way to modify the default value of
`collectionTTL` is to remove the index by hand (in the mongo shell
run `db.cronHistory.dropIndex({startedAt: 1})`) and re-run your
project. SyncedCron will recreate the index with the updated TTL.
*/
// collectionTTL: 172800
});
if (isMailServerMaster) {
SyncedCron.add({
name: 'Send emails',
timezone: 'Europe/Madrid',
schedule: (parser) => {
// http://bunkat.github.io/later/
const sched = parser.text('every 10 min');
if (sched.error !== -1) {
console.error(`Mail cron 'when' field parsed with errors: ${sched.error}`);
}
return sched;
},
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;
const enEn = Meteor.settings.private.twitter.en.enabled;
if (esEn) {
SyncedCron.add({
name: 'Check for fires stats in Spain and tweet about',
timezone: 'Europe/Madrid',
// Optionally set a positive offset if you wish to 'snooze' a schedule
// offset: 30 * 60 * 100,
// context: {
// userID: 'xyz'
// },
schedule: (parser) => {
// this.magic = true; // Context is accesible here as this context.
// parser is a later.parse object
// return parser.text('every 2 minutes');
// http://bunkat.github.io/later/
const sched = parser.text(Meteor.settings.private.twitter.es.when);
if (sched.error !== -1) {
console.error(`Twitter cron 'when' field parsed with errors: ${sched.error}`);
}
return sched;
},
job: () => tweetIberiaFires()
/* console.log('cron is working');
* console.log(this.userID) // Context Object becomes this argument
* console.log(this.magic) /
* var numbersCrunched = CrushSomeNumbers();
* return numbersCrunched;
return tweetFires(); */
});
}
if (enEn) {
SyncedCron.add({
name: 'Check for fires stats in Europe and tweet about',
timezone: 'Europe/Madrid',
schedule: (parser) => {
const sched = parser.text(Meteor.settings.private.twitter.en.when);
if (sched.error !== -1) {
console.error(`Twitter cron 'when' field parsed with errors: ${sched.error}`);
}
return sched;
},
job: () => tweetEuropeFires()
});
}
if (isMailServerMaster || esEn || enEn) {
SyncedCron.start();
}
});

View file

@ -1,8 +1,8 @@
import { Meteor } from 'meteor/meteor'; import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import nodemailer from 'nodemailer'; import nodemailer from 'nodemailer';
import { MailTime } from 'meteor/ostrio:mailer'; import { MailTime } from 'meteor/ostrio:mailer';
import i18n from 'i18next'; import i18n from 'i18next';
import isMaster from './isMaster';
const transports = []; const transports = [];
@ -20,38 +20,46 @@ export const hr = `<table cellspacing="0" cellpadding="0" border="0" width="100%
</tr> </tr>
</table>`; </table>`;
const MailQueue = new MailTime({ let MailQueue;
db,
type: 'server',
strategy: 'balancer', // Transports will be used in round robin chain
transports,
from(transport) {
// To pass spam-filters `from` field should be correctly set
// for each transport, check `transport` object for more options
if (!Meteor.isProduction) {
// only for test purposes
return `${i18n.t('AppName')} <notify@example.org>`;
}
return `${i18n.t('AppName')} <${transport.options.auth.user}>`;
},
debug: Meteor.settings.private.debugMailer,
concatEmails: true, // Concatenate emails to the same addressee
concatSubject: `${i18n.t('Nuevas notificaciones de {{app}}', { app: i18n.t('AppName') })}`,
/* eslint-disable */
concatDelimiter: hr + '<h2>{{{subject}}}</h2>', // Start each concatenated email with it's own subject
/* eslint-enable */
// concatThrottling: 30,
template: MailTime.Template // Use default template
});
// A Client (not used yet) export const isMailServerMaster = Meteor.settings.private.isMailServer && isMaster();
// const MailQueueClient = new MailTime({
// db, if (isMailServerMaster) {
// type: 'client', console.log('I\'m the mail server');
// debug: true, MailQueue = new MailTime({
// strategy: 'balancer', // Transports will be used in round robin chain db,
// concatEmails: true // Concatenate emails to the same address type: 'client', // instead of 'server' for now to avoid use the JosK task manager at 250ms, we use our cron
// }); // see https://github.com/VeliovGroup/Mail-Time/issues/5
strategy: 'balancer', // Transports will be used in round robin chain
transports,
from(transport) {
// To pass spam-filters `from` field should be correctly set
// for each transport, check `transport` object for more options
if (!Meteor.isProduction) {
// only for test purposes
return `${i18n.t('AppName')} <notify@example.org>`;
}
return `${i18n.t('AppName')} <${transport.options.auth.user}>`;
},
debug: Meteor.settings.private.debugMailer,
concatEmails: true, // Concatenate emails to the same addressee
concatSubject: `${i18n.t('Nuevas notificaciones de {{app}}', { app: i18n.t('AppName') })}`,
/* eslint-disable */
concatDelimiter: hr + '<h2>{{{subject}}}</h2>', // Start each concatenated email with it's own subject
/* eslint-enable */
// concatThrottling: 30,
template: MailTime.Template // Use default template
});
} else {
console.log('I\'m a mail client');
MailQueue = new MailTime({
db,
type: 'client',
debug: Meteor.settings.private.debugMailer,
strategy: 'balancer', // Transports will be used in round robin chain
concatEmails: true // Concatenate emails to the same address
});
}
export default function sendMail(opts, debug) { export default function sendMail(opts, debug) {
if (debug) { if (debug) {
@ -78,27 +86,6 @@ if (Meteor.settings.private.testMailer) {
sendMail(emailOpts, true); sendMail(emailOpts, true);
} }
export const sendEmailsFromQueue = () => {
// Set interval to greater than 256 MailQueue.___send(() => { });
// https://github.com/VeliovGroup/Mail-Time/issues/5
const MailJobs = new Mongo.Collection('__JobTasks__mailTimeQueue', { idGeneration: 'MONGO' });
const updateJob = (mailJob) => {
MailJobs.update(mailJob._id, { $set: { delay: 30000 } });
// console.log(MailJobs.findOne());
}; };
const mailJob = MailJobs.findOne();
if (mailJob) {
updateJob(mailJob);
}
MailJobs.find().observe({
added: function notifAdded(item) {
updateJob(item);
},
changed: function notifChanged(item) {
updateJob(item);
}
});

View file

@ -0,0 +1,7 @@
// Workaround for: https://github.com/meteor/meteor/issues/9796
// https://github.com/meteor/meteor/issues/9796#issuecomment-381676326
// https://github.com/sandstorm-io/sandstorm/blob/0f1fec013fe7208ed0fd97eb88b31b77e3c61f42/shell/server/00-startup.js#L99-L129
import Fiber from 'fibers';
Fiber.poolSize = 1e9;

View file

@ -1,4 +1,6 @@
import './ravenLogger'; import './ravenLogger';
import './catchExceptions';
import './fibers';
import './i18n'; import './i18n';
import './accounts'; import './accounts';
import './api'; import './api';
@ -8,8 +10,11 @@ import './IPGeocoder';
import './migrations'; import './migrations';
import './notificationsObserver'; import './notificationsObserver';
import './facts'; import './facts';
import '../common/comments'; import './comments';
import './sitemaps'; import './sitemaps';
import './subsUnion'; import './subsUnion';
import './prerender'; import './prerender';
import './feedback'; import './feedback';
import './cron';
import './isMaster';
import './rest';

View file

@ -0,0 +1,16 @@
/* eslint-disable import/no-absolute-path */
import pm2Master from 'pm2-master';
import { Meteor } from 'meteor/meteor';
Meteor.startup(() => {
if (pm2Master.isMaster()) {
console.log('This is the master process');
} else {
console.log('This is a worker process');
}
});
const isMaster = () => pm2Master.isMaster();
export default isMaster;

View file

@ -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'
};

View file

@ -1,4 +1,4 @@
/* global Migrations */ /* global Migrations, Comments */
/* eslint-disable import/no-absolute-path */ /* eslint-disable import/no-absolute-path */
import { Meteor } from 'meteor/meteor'; import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base'; import { Accounts } from 'meteor/accounts-base';
@ -9,6 +9,9 @@ import SiteSettings from '/imports/api/SiteSettings/SiteSettings';
import FalsePositives from '/imports/api/FalsePositives/FalsePositives'; import FalsePositives from '/imports/api/FalsePositives/FalsePositives';
import Industries from '/imports/api/Industries/Industries'; import Industries from '/imports/api/Industries/Industries';
import IndustryRegistries from '/imports/api/Industries/IndustryRegistries'; 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(() => { Meteor.startup(() => {
// https://github.com/percolatestudio/meteor-migrations // https://github.com/percolatestudio/meteor-migrations
@ -157,9 +160,101 @@ Meteor.startup(() => {
} }
}); });
Migrations.add({
version: 11,
up: function noAnonComments() {
Comments.getCollection().remove({ isAnonymous: true });
}
});
Migrations.add({
version: 12,
up: function setTelegramUsersBotId() {
Meteor.users.update({ telegramChatId: { $ne: null }, telegramBot: null }, {
$set: {
telegramBot: 'es'
}
}, { multi: true });
}
});
Migrations.add({
version: 13,
up: function removeTelegramBotFromUsersId() {
Meteor.users.update({}, {
$unset: {
telegramBot: ''
}
}, { multi: true });
}
});
Migrations.add({
version: 14,
up: function setTelegramUsersBotId() {
UserSubsToFiresCollection.update({ chatId: { $ne: null }, telegramBot: null }, {
$set: {
telegramBot: 'es'
}
}, { multi: true });
}
});
Migrations.add({
version: 15,
up: function moveToFalsePositivesUppercase() {
/* const falsepositiveslower = new Mongo.Collection('falsepositives', { idGeneration: 'MONGO' });
* falsepositiveslower.find({}).forEach((falseDoc) => {
* FalsePositives.insert(falseDoc);
* });*/
// TODO remove falsepositives lowercase collection
}
});
Migrations.add({
version: 16,
up: function moveToFalsePositivesUppercaseWithUser() {
const falsepositiveslower = new Mongo.Collection('falsepositives', { idGeneration: 'MONGO' });
const now = new Date();
falsepositiveslower.find({}).forEach((falseDoc) => {
const user = Meteor.users.findOne({ telegramChatId: falseDoc.chatId });
if (user) {
falseDoc.owner= user._id;
}
falseDoc.type = 'industry';
falseDoc.createdAt = now;
falseDoc.updatedAt = now;
FalsePositives.insert(falseDoc);
});
}
});
Migrations.add({
version: 17,
up: function renameWebNotifiedField() {
Notifications.update({ webNotified: { $exists: true } }, {
$rename: { webNotifiedAt: 'notifiedAt' }
}, { upsert: false, multi: true });
Notifications.update({ webNotified: { $exists: true } }, {
$rename: { webNotified: 'notified' }
}, { upsert: false, multi: true });
}
});
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 // Set createdAt in users & subs
Migrations.migrateTo('latest'); Migrations.migrateTo('latest');
// Migrations.migrateTo('7,rerun'); // Migrations.migrateTo('14,rerun');
}); });

View file

@ -1,111 +1,19 @@
/* eslint-disable import/no-absolute-path */ /* eslint-disable import/no-absolute-path */
import i18n from 'i18next';
import { Meteor } from 'meteor/meteor'; import { Meteor } from 'meteor/meteor';
import moment from 'moment';
import { dateLongFormat } from '/imports/api/Common/dates';
import Notifications from '/imports/api/Notifications/Notifications'; import Notifications from '/imports/api/Notifications/Notifications';
// import sendMail from '/imports/startup/server/email'; import processNotif from '/imports/modules/server/notificationsProcess.js';
import sendEmail from '/imports/modules/server/send-email'; import { isMailServerMaster } from '/imports/startup/server/email';
// import { hr } from '/imports/startup/server/email';
import getOAuthProfile from '/imports/modules/get-oauth-profile';
import image from 'google-maps-image-api-url';
import { trim } from '/imports/ui/components/NotificationsObserver/util.js';
Meteor.startup(() => { Meteor.startup(() => {
// https://stackoverflow.com/questions/1199352/smart-way-to-shorten-long-strings-with-javascript if (isMailServerMaster) {
function truncate(n, useWordBoundary) { Notifications.find().observe({
if (this.length <= n) { added: function notifAdded(notif) {
return this; processNotif(notif);
} },
const subString = this.substr(0, n - 1); changed: function notifChanged(updatedNotif) { // , oldNotif) {
return `${useWordBoundary ? processNotif(updatedNotif);
subString.substr(0, subString.lastIndexOf(' ')) : }
subString}...`;
}
// 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 `<img src="${imgUrl(lat, lng)}" width="640" height="480"/>`;
} */
function process(notif) {
if (notif.type === 'web' && !notif.emailNotified) {
const user = Meteor.users.findOne({ _id: notif.userId });
const hasPassword = user.services && user.services.password && user.services.password.bcrypt;
// console.log(`Has password: ${hasPassword}`);
const OAuthProfile = getOAuthProfile({
password: hasPassword,
profile: user.profile
}, user);
const firstName = OAuthProfile ? OAuthProfile.name.first : user.profile.name.first;
const emailAddress = OAuthProfile ? OAuthProfile.email :
user && user.emails[0] && user.emails[0].verified ? user.emails[0].address : null;
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 = `<a href="${fireUrl}">${i18n.t('Más información sobre este fuego')}</a>`;
// 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: truncate.apply(message, [50, true]),
// text: `${message}\n\n${fireTextUrl}\n\n`,
// template: '<body><h2>{{appName}}</h2>{{{html}}}</body>',
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);
}
});
}); });

View file

@ -2,12 +2,17 @@ import RavenLogger from 'meteor/flowkey:raven';
import { Meteor } from 'meteor/meteor'; import { Meteor } from 'meteor/meteor';
const ravenOptions = {}; const ravenOptions = {};
const publicDSN = Meteor.settings.public.sentryPublicDSN;
const privateDSN = Meteor.settings.sentryPrivateDSN;
const enabled = publicDSN && privateDSN;
const ravenLogger = new RavenLogger({ const ravenLogger = enabled ? new RavenLogger({
publicDSN: Meteor.settings.public.sentryPublicDSN, publicDSN,
privateDSN: Meteor.settings.sentryPrivateDSN, privateDSN,
shouldCatchConsoleError: true, // default shouldCatchConsoleError: true, // default true
trackUser: true // default trackUser: true // default false
}, ravenOptions); }, ravenOptions) : { log: (error) => { console.log(error); } };
console.log(`ravenLogger ${enabled ? 'enabled' : 'disabled'}`);
export default ravenLogger; export default ravenLogger;

View file

@ -0,0 +1 @@
import '../../api/Rest/Rest.js';

View file

@ -4,6 +4,8 @@
import Fires from '/imports/api/Fires/Fires'; import Fires from '/imports/api/Fires/Fires';
// import Comments from '/imports/api/Comments/Comments'; // import Comments from '/imports/api/Comments/Comments';
const firesMapEnabled = false;
sitemaps.add('/sitemap.xml', () => { sitemaps.add('/sitemap.xml', () => {
const today = new Date(); const today = new Date();
@ -20,21 +22,23 @@ sitemaps.add('/sitemap.xml', () => {
// When user has public page // When user has public page
/* const users = Meteor.users.find().fetch(); /* const users = Meteor.users.find().fetch();
_.each(users, function(user) { _.each(users, function(user) {
out.push({ out.push({
page: '/persona/' + user.username, page: '/persona/' + user.username,
lastmod: user.updatedAt lastmod: user.updatedAt
}); });
}); */ }); */
Fires.find({}, { limit: 100, sort: { createdAt: -1 } }).fetch().forEach((fire) => { if (firesMapEnabled) {
// Search the last comment of tha fire Fires.find({}, { limit: 100, sort: { createdAt: -1 } }).fetch().forEach((fire) => {
const lastComment = Comments.getCollection().findOne({ referenceId: `fire-${fire._id}` }, { sort: { createdAt: -1 } }); // Search the last comment of tha fire
out.push({ const lastComment = Comments.getCollection().findOne({ referenceId: `fire-${fire._id}` }, { sort: { createdAt: -1 } });
page: `/fire/archive/${fire._id._str}`, out.push({
lastmod: lastComment ? lastComment.createdAt : fire.updatedAt page: `/fire/archive/${fire._id._str}`,
lastmod: lastComment ? lastComment.createdAt : fire.updatedAt
});
}); });
}); }
return out; return out;
}); });

View file

@ -4,12 +4,19 @@ import { Meteor } from 'meteor/meteor';
import Subscriptions from '/imports/api/Subscriptions/Subscriptions'; import Subscriptions from '/imports/api/Subscriptions/Subscriptions';
import SiteSettings from '/imports/api/SiteSettings/SiteSettings'; import SiteSettings from '/imports/api/SiteSettings/SiteSettings';
import Perlin from 'loms.perlin'; 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 calcUnion from '/imports/ui/components/Maps/SubsUnion/Unify';
import { isMailServerMaster } from '/imports/startup/server/email';
// sudo apt-get install libcairo2-dev libjpeg-dev libgif-dev // sudo apt-get install libcairo2-dev libjpeg-dev libgif-dev
Meteor.startup(() => { Meteor.startup(() => {
if (!isMailServerMaster) {
console.log('We only process subsUnion in master');
return;
}
const debug = true; // Meteor.isDevelopment;
Perlin.seed(Math.random()); Perlin.seed(Math.random());
const addNoisy = (osub) => { const addNoisy = (osub) => {
@ -26,59 +33,91 @@ Meteor.startup(() => {
return sub; return sub;
}; };
const process = () => { const noNoisy = sub => sub;
const group = new L.FeatureGroup();
const result = calcUnion(Subscriptions.find().fetch(), group, addNoisy); const process = (isPublic) => {
const subscribers = Subscriptions.find().fetch();
const result = calcUnion(L, subscribers, isPublic ? addNoisy : noNoisy, true);
const union = result[0]; const union = result[0];
const bounds = result[1]; const bounds = result[1];
const publicl = isPublic ? 'public' : 'private';
const Publicl = publicl.replace(/\b\w/g, l => l.toUpperCase());
if (typeof union === 'object') { if (typeof union === 'object') {
const unionSet = { const unionSet = {
$set: { $set: {
name: 'subs-public-union', name: `subs-${publicl}-union`,
value: JSON.stringify(union), value: JSON.stringify(union),
isPublic: true, isPublic,
description: 'Public subscriptions union', description: `${Publicl} subscriptions union`,
type: 'string' type: 'string'
} }
}; };
const boundsSet = { const boundsSet = {
$set: { $set: {
name: 'subs-public-union-bounds', name: `subs-${publicl}-union-bounds`,
value: JSON.stringify(bounds), value: JSON.stringify(bounds),
isPublic: true, isPublic,
description: 'Public subscriptions union bounds', description: `${Publicl} subscriptions union bounds`,
type: 'string' type: 'string'
} }
}; };
const sizeSet = {
$set: {
name: 'subs-union-count',
value: subscribers.length,
isPublic: false,
description: 'Subscriptions count',
type: 'number'
}
};
// FIXME, take care of object size: // FIXME, take care of object size:
// https://stackoverflow.com/questions/10827812/what-is-the-length-maximum-for-a-string-data-type-in-mongodb-used-with-ruby // https://stackoverflow.com/questions/10827812/what-is-the-length-maximum-for-a-string-data-type-in-mongodb-used-with-ruby
SiteSettings.upsert({ name: 'subs-public-union' }, unionSet, { multi: false }); SiteSettings.upsert({ name: `subs-${publicl}-union` }, unionSet, { multi: false });
SiteSettings.upsert({ name: 'subs-public-union-bounds' }, boundsSet, { multi: false }); SiteSettings.upsert({ name: `subs-${publicl}-union-bounds` }, boundsSet, { multi: false });
if (Meteor.isDevelopment) console.log('Subscription union calculated'); SiteSettings.upsert({ name: 'subs-union-count' }, sizeSet, { multi: false });
if (debug) console.log(`${Publicl} subscription union calculated`);
} else { } else {
console.log('Subscription union failed!'); console.log('Subscription union failed!');
} }
}; };
// At startup // At startup, we check if it's necessary to calc subscriptions union again
process(); const currentUnion = SiteSettings.findOne({ name: 'subs-public-union' });
const lastSubs = Subscriptions.findOne({}, { sort: { updatedAt: -1 } });
const countUnionSubs = SiteSettings.findOne({ name: 'subs-union-count' });
const countSubs = Subscriptions.find({}).count();
if (currentUnion && lastSubs) {
const lastUnionUpdated = currentUnion.updatedAt;
const lastSubsUpdated = lastSubs.updatedAt;
if (lastUnionUpdated > lastSubsUpdated || !countUnionSubs || countSubs !== countUnionSubs.value) {
console.log('Subs union outdated');
process(true);
process(false);
} else {
console.log('Subs union up-to-date');
}
}
Subscriptions.find({ createdAt: { $gt: new Date() } }).observe({ Subscriptions.find({ createdAt: { $gt: new Date() } }).observe({
added: function newSubAdded() { // doc) { added: function newSubAdded() { // doc) {
if (Meteor.isDevelopment) console.log('Subs added so recreate union'); if (debug) console.log('Subs added so recreate union');
process(); process(true);
process(false);
} }
}); });
Subscriptions.find().observe({ Subscriptions.find().observe({
changed: function subsChanged() { // updatedDoc, oldDoc) { changed: function subsChanged() { // updatedDoc, oldDoc) {
if (Meteor.isDevelopment) console.log('Subs changed so recreate union'); if (debug) console.log('Subs changed so recreate union');
process(); process(true);
process(false);
}, },
removed: function subsRemoved() { // oldDoc) { removed: function subsRemoved() { // oldDoc) {
if (Meteor.isDevelopment) console.log('Subs removed so recreate union'); if (debug) console.log('Subs removed so recreate union');
process(); process(true);
process(false);
} }
}); });
}); });

View file

@ -4,9 +4,7 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { Row, Col } from 'react-bootstrap'; import { Row, Col } from 'react-bootstrap';
import { withTracker } from 'meteor/react-meteor-data';
import ravenLogger from '/imports/startup/client/ravenLogger'; import ravenLogger from '/imports/startup/client/ravenLogger';
import './ErrorBoundary.scss'; import './ErrorBoundary.scss';
@ -14,7 +12,6 @@ import './ErrorBoundary.scss';
class ErrorBoundary extends Component { class ErrorBoundary extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.t = props.t;
this.state = { hasError: false }; this.state = { hasError: false };
} }
@ -29,33 +26,41 @@ class ErrorBoundary extends Component {
} }
render() { render() {
const {
appName, title, subTitle, children
} = this.props;
if (this.state.hasError) { if (this.state.hasError) {
// You can render any custom fallback UI // You can render any custom fallback UI
return ( return (
<div className="error-boundary"> <div className="error-boundary">
<Row className="align-items-center justify-content-center"> <Row className="align-items-center justify-content-center">
<Col xs={12} sm={6} md={5} lg={4}> <Col xs={12} sm={6} md={5} lg={4}>
<h1 className="page-header">{this.t('AppNameFull')}</h1> <h1 className="page-header">{appName}</h1>
<h4 className="page-header">{this.t('general-error-title')}</h4> <h4 className="page-header">{title}</h4>
<p> <p>{subTitle}</p>
{this.t('general-error-description')}
</p>
</Col> </Col>
</Row> </Row>
</div> </div>
); );
} }
return this.props.children; return children;
} }
} }
ErrorBoundary.propTypes = { ErrorBoundary.propTypes = {
t: PropTypes.func.isRequired appName: PropTypes.string,
title: PropTypes.string,
subTitle: PropTypes.string,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired
}; };
ErrorBoundary.defaultProps = { ErrorBoundary.defaultProps = {
appName: 'All Against the Fire!',
title: 'Upppps: Something has gone wrong',
subTitle: 'We are investigating the problem, try again in a while'
}; };
export default translate([], { wait: true })(withTracker(props => ({ export default ErrorBoundary;
// props.something
}))(ErrorBoundary));

View file

@ -2,7 +2,7 @@
/* eslint-disable import/no-absolute-path */ /* eslint-disable import/no-absolute-path */
/* eslint-disable import/no-absolute-path */ /* eslint-disable import/no-absolute-path */
import React, { Component, Fragment } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Meteor } from 'meteor/meteor'; import { Meteor } from 'meteor/meteor';
import { translate } from 'react-i18next'; import { translate } from 'react-i18next';
@ -18,13 +18,13 @@ import './Feedback.scss';
class Feedback extends Component { class Feedback extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.t = props.t;
this.handleSubmit = this.handleSubmit.bind(this); this.handleSubmit = this.handleSubmit.bind(this);
this.onTabClick = this.onTabClick.bind(this); this.onTabClick = this.onTabClick.bind(this);
} }
componentDidMount() { componentDidMount() {
const component = this; const component = this;
const { t } = this.props;
validate(component.form, { validate(component.form, {
rules: { rules: {
@ -38,11 +38,11 @@ class Feedback extends Component {
}, },
messages: { messages: {
feedbackText: { feedbackText: {
required: this.t('Por favor, escribe aquí tu feedback...') required: t('Por favor, escribe aquí tu feedback...')
}, },
email: { email: {
required: this.t('Tu correo'), required: t('Tu correo'),
email: this.t('¿Es correcto este correo?') email: t('¿Es correcto este correo?')
} }
}, },
submitHandler() { component.handleSubmit(); } submitHandler() { component.handleSubmit(); }
@ -71,8 +71,9 @@ class Feedback extends Component {
render() { render() {
// console.log(`Render Feedback because isHome ${this.props.isHome}, email: '${this.props.emailAddress}'`); // console.log(`Render Feedback because isHome ${this.props.isHome}, email: '${this.props.emailAddress}'`);
const disabled = this.props.emailVerified && this.props.emailAddress; const disabled = this.props.emailVerified && this.props.emailAddress;
const { t } = this.props;
return ( return (
<Fragment> <div>
{ !this.props.isHome && { !this.props.isHome &&
<div id="feedback"> <div id="feedback">
<div id="feedback-form" ref={formdiv => (this.formdiv = formdiv)} style={{ display: 'none' }} className="card"> <div id="feedback-form" ref={formdiv => (this.formdiv = formdiv)} style={{ display: 'none' }} className="card">
@ -88,7 +89,7 @@ class Feedback extends Component {
name="email" name="email"
className="form-control" className="form-control"
ref={email => (this.email = email)} ref={email => (this.email = email)}
placeholder={this.t('Tu correo')} placeholder={t('Tu correo')}
key={disabled ? 'disabledEmail' : 'enabledEmail'} key={disabled ? 'disabledEmail' : 'enabledEmail'}
disabled={disabled} disabled={disabled}
defaultValue={disabled ? this.props.emailAddress : ''} defaultValue={disabled ? this.props.emailAddress : ''}
@ -103,13 +104,13 @@ class Feedback extends Component {
className="form-control" className="form-control"
name="feedbackText" name="feedbackText"
ref={feedbackText => (this.feedbackText = feedbackText)} ref={feedbackText => (this.feedbackText = feedbackText)}
placeholder={this.t('Por favor, escribe aquí tu feedback...')} placeholder={t('Por favor, escribe aquí tu feedback...')}
rows="6" rows="6"
/> />
<FormControl.Feedback /> <FormControl.Feedback />
</FormGroup> </FormGroup>
<Button type="submit" bsStyle="success" className="float-right" id={testId('sendFeedbackBtn')} > <Button type="submit" bsStyle="success" className="float-right" id={testId('sendFeedbackBtn')} >
{this.t('Enviar')} {t('Enviar')}
</Button> </Button>
</form> </form>
</div> </div>
@ -117,11 +118,11 @@ class Feedback extends Component {
id="feedback-tab" id="feedback-tab"
onClick={(event) => { this.onTabClick(event); }} onClick={(event) => { this.onTabClick(event); }}
> >
{this.t('Feedback')} {t('Feedback')}
</div> </div>
</div> </div>
} }
</Fragment> </div>
); );
} }
} }
@ -133,7 +134,7 @@ Feedback.propTypes = {
isHome: PropTypes.bool.isRequired isHome: PropTypes.bool.isRequired
}; };
export default translate([], { wait: true })(withTracker(props => ({ export default translate()(withTracker(props => ({
emailAddress: props.emailAddress, emailAddress: props.emailAddress,
emailVerified: props.emailVerified, emailVerified: props.emailVerified,
isHome: isHome() isHome: isHome()

View file

@ -5,6 +5,10 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { withTracker } from 'meteor/react-meteor-data'; import { withTracker } from 'meteor/react-meteor-data';
import Chronos from '/imports/ui/components/Chronos/Chronos'; import Chronos from '/imports/ui/components/Chronos/Chronos';
import { translate } from 'react-i18next';
import { dateLongFormat } from '/imports/api/Common/dates';
import './FromNow.scss';
class FromNow extends Component { class FromNow extends Component {
constructor(props) { constructor(props) {
@ -27,26 +31,39 @@ class FromNow extends Component {
return !(nextState.when === this.state.when); return !(nextState.when === this.state.when);
} }
thatIs() {
return this.props.t('esDecirTalDia', { date: dateLongFormat(this.props.whenDate) });
}
render() { render() {
console.log(`Render from now ${this.state.when}`); console.log(`Render from now ${this.state.when}`);
return ( return (
<span>{this.state.when}</span> <span data-toggle="tooltip" className="from-now" title={this.thatIs()}>{this.state.when}</span>
); );
} }
} }
FromNow.propTypes = { FromNow.propTypes = {
when: PropTypes.string t: PropTypes.func.isRequired,
when: PropTypes.string,
whenDate: PropTypes.instanceOf(Date)
}; };
FromNow.defaultProps = { FromNow.defaultProps = {
}; };
const FromNowContainer = withTracker((props) => { /*
* const FromNowContainer = withTracker((props) => {
*
* })(FromNow);
*
* export default FromNowContainer;
* */
export default translate([], { wait: true })(withTracker((props) => {
const whenDate = props.when; const whenDate = props.when;
return { return {
when: whenDate ? Chronos.moment(whenDate).fromNow() : null when: whenDate ? Chronos.moment(whenDate).fromNow() : null,
whenDate
}; };
})(FromNow); })(FromNow));
export default FromNowContainer;

View file

@ -0,0 +1,4 @@
.from-now {
border-bottom: 1px gray dashed;
cursor: pointer;
}

View file

@ -2,7 +2,8 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Line } from 'react-progressbar.js'; import { Meteor } from 'meteor/meteor';
import { Line } from 'react-progress-bar.js';
import './LoadingBar.scss'; import './LoadingBar.scss';
@ -10,7 +11,7 @@ import './LoadingBar.scss';
const LoadingBar = ({ progress }) => ( const LoadingBar = ({ progress }) => (
<div className="loading-bar"> <div className="loading-bar">
<Line <Line
progress={progress} progress={Meteor.status().status !== 'connected' ? Meteor.status().retryCount / 10 : progress}
options={{ strokeWidth: 2, color: '#5A7636' }} options={{ strokeWidth: 2, color: '#5A7636' }}
initialAnimate initialAnimate
/> />

View file

@ -11,6 +11,8 @@ import { TileLayer, LayersControl } from 'react-leaflet';
const { BaseLayer } = LayersControl; const { BaseLayer } = LayersControl;
const defOpacity = 0.7;
class DefMapLayers extends Component { class DefMapLayers extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -31,13 +33,16 @@ class DefMapLayers extends Component {
const osmgraylayer = ( const osmgraylayer = (
<BaseLayer checked={this.props.gray} name={t('Mapa gris de OpenStreetMap')}> <BaseLayer checked={this.props.gray} name={t('Mapa gris de OpenStreetMap')}>
<TileLayer <TileLayer
opacity={defOpacity}
attribution="&copy; <a href=&quot;http://osm.org/copyright&quot;>OpenStreetMap</a> contributors" attribution="&copy; <a href=&quot;http://osm.org/copyright&quot;>OpenStreetMap</a> contributors"
url="https://tiles.wmflabs.org/bw-mapnik/{z}/{x}/{y}.png" // url="https://tiles.wmflabs.org/bw-mapnik/{z}/{x}/{y}.png"
url="https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png"
/> />
</BaseLayer>); </BaseLayer>);
const osmlayer = ( const osmlayer = (
<BaseLayer checked={this.props.osmcolor} name={t('Mapa color de OpenStreetMap')}> <BaseLayer checked={this.props.osmcolor} name={t('Mapa color de OpenStreetMap')}>
<TileLayer <TileLayer
opacity={defOpacity}
attribution="&amp;copy <a href=&quot;http://osm.org/copyright&quot;>OpenStreetMap</a> contributors" attribution="&amp;copy <a href=&quot;http://osm.org/copyright&quot;>OpenStreetMap</a> contributors"
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/> />
@ -49,15 +54,15 @@ class DefMapLayers extends Component {
{/* React.Fragment does not work here */} {/* React.Fragment does not work here */}
{ this.state.gkey && { this.state.gkey &&
<BaseLayer name={t('Mapa de carreteras de Google')}> <BaseLayer name={t('Mapa de carreteras de Google')}>
<GoogleLayer googlekey={this.state.gkey} maptype="ROADMAP" /> <GoogleLayer opacity={defOpacity} googlekey={this.state.gkey} maptype="ROADMAP" />
</BaseLayer> } </BaseLayer> }
{ this.state.gkey && { this.state.gkey &&
<BaseLayer name={t('Mapa de terreno de Google')}> <BaseLayer name={t('Mapa de terreno de Google')} checked={this.props.terrain}>
<GoogleLayer googlekey={this.state.gkey} maptype="TERRAIN" /> <GoogleLayer opacity={defOpacity} googlekey={this.state.gkey} maptype="TERRAIN" />
</BaseLayer> } </BaseLayer> }
{ this.state.gkey && { this.state.gkey &&
<BaseLayer name={t('Mapa de satélite de Google')} checked={this.props.satellite}> <BaseLayer name={t('Mapa de satélite de Google')} checked={this.props.satellite}>
<GoogleLayer googlekey={this.state.gkey} maptype="SATELLITE" /> <GoogleLayer opacity={defOpacity} googlekey={this.state.gkey} maptype="SATELLITE" />
</BaseLayer> } </BaseLayer> }
</LayersControl> </LayersControl>
); );
@ -68,6 +73,7 @@ DefMapLayers.propTypes = {
t: PropTypes.func.isRequired, t: PropTypes.func.isRequired,
gray: PropTypes.bool, gray: PropTypes.bool,
osmcolor: PropTypes.bool, osmcolor: PropTypes.bool,
terrain: PropTypes.bool,
satellite: PropTypes.bool satellite: PropTypes.bool
}; };
@ -75,6 +81,7 @@ DefMapLayers.propTypes = {
DefMapLayers.defaultProps = { DefMapLayers.defaultProps = {
gray: false, gray: false,
osmcolor: false, osmcolor: false,
terrain: false,
satellite: false satellite: false
}; };

View file

@ -1,10 +1,10 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Circle } from 'react-leaflet'; import { GeoJSON } from 'react-leaflet';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { rectangleAround } from 'map-common-utils';
import { onMarkClick } from './MarkListeners'; import { onMarkClick } from './MarkListeners';
import FirePopup from './FirePopup'; import FirePopup from './FirePopup';
class FireCircleMark extends Component { class FireCircleMark extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -22,22 +22,26 @@ class FireCircleMark extends Component {
lat, lat,
lon, lon,
scan, scan,
track,
nasa, nasa,
id, id,
history, history,
when, when,
t t
} = this.props; } = this.props;
const rect = rectangleAround({ lat, lon }, track, track);
return ( return (
<Circle center={[lat, lon]} color="red" stroke={false} fillOpacity="1" fill radius={scan * 500} onClick={this.onClick}> <GeoJSON data={rect} color="red" stroke width="1" opacity=".4" fillOpacity="1">
<FirePopup t={t} history={history} id={id} nasa={nasa} lat={lat} lon={lon} when={when} /> <FirePopup t={t} history={history} id={id} nasa={nasa} lat={lat} lon={lon} when={when} />
</Circle> </GeoJSON>
); );
/* <Circle center={[lat, lon]} color="red" stroke={false} fillOpacity="1" fill radius={scan * 500} onClick={this.onClick}> */
} }
} }
FireCircleMark.propTypes = { FireCircleMark.propTypes = {
scan: PropTypes.number.isRequired, scan: PropTypes.number.isRequired,
track: PropTypes.number.isRequired,
lat: PropTypes.number.isRequired, lat: PropTypes.number.isRequired,
lon: PropTypes.number.isRequired, lon: PropTypes.number.isRequired,
nasa: PropTypes.bool.isRequired, nasa: PropTypes.bool.isRequired,

View file

@ -3,8 +3,8 @@ import React, { Component, Fragment } from 'react';
import { CircleMarker, Marker, Tooltip } from 'react-leaflet'; import { CircleMarker, Marker, Tooltip } from 'react-leaflet';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { fireIconS, fireIconM, fireIconL, nFireIcon, industryIcon, regIndustryIcon } from '/imports/ui/components/Maps/Icons'; import { fireIconS, fireIconM, fireIconL, nFireIcon, industryIcon, regIndustryIcon } from '/imports/ui/components/Maps/Icons';
import { onMarkClick } from './MarkListeners';
import { translate } from 'react-i18next'; import { translate } from 'react-i18next';
import { onMarkClick } from './MarkListeners';
import FirePopup from './FirePopup'; import FirePopup from './FirePopup';
class FireIconMark extends Component { class FireIconMark extends Component {
@ -30,16 +30,7 @@ class FireIconMark extends Component {
render() { render() {
const { const {
lat, lat, lon, scan, track, nasa, id, history, falsePositives, industries, neighbour, when, t
lon,
scan,
nasa,
id,
history,
falsePositives,
industries,
when,
t
} = this.props; } = this.props;
return ( return (
<div> <div>
@ -78,9 +69,11 @@ FireIconMark.propTypes = {
lat: PropTypes.number.isRequired, lat: PropTypes.number.isRequired,
lon: PropTypes.number.isRequired, lon: PropTypes.number.isRequired,
scan: PropTypes.number, scan: PropTypes.number,
track: PropTypes.number,
nasa: PropTypes.bool, nasa: PropTypes.bool,
falsePositives: PropTypes.bool.isRequired, falsePositives: PropTypes.bool.isRequired,
industries: PropTypes.bool.isRequired, industries: PropTypes.bool.isRequired,
neighbour: PropTypes.bool.isRequired,
id: PropTypes.object.isRequired, id: PropTypes.object.isRequired,
history: PropTypes.object.isRequired, history: PropTypes.object.isRequired,
when: PropTypes.instanceOf(Date), when: PropTypes.instanceOf(Date),

View file

@ -9,14 +9,14 @@ import FirePixel from '/imports/ui/components/Maps/FirePixel';
export default function FireList(props) { export default function FireList(props) {
const { const {
fires, scale, useMarkers, nasa, t, history, falsePositives, industries fires, scale, useMarkers, nasa, t, history, falsePositives, industries, neighbour
} = 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 t={t} history={history} id={_id} key={_id} nasa={nasa} falsePositives={falsePositives} industries={industries} {...otherProps} />)); items = fires.map(({ _id, ...otherProps }) => (<FireIconMark t={t} history={history} id={_id} key={_id} nasa={nasa} neighbour={neighbour} falsePositives={falsePositives} industries={industries} {...otherProps} />));
} else if (usePixel && !falsePositives && !industries) { } else if (usePixel && !falsePositives && !industries) {
items = fires.map(({ _id, ...otherProps }) => (<FirePixel t={t} key={_id} id={_id} history={history} nasa={nasa} {...otherProps} />)); items = fires.map(({ _id, ...otherProps }) => (<FirePixel t={t} key={_id} id={_id} history={history} nasa={nasa} {...otherProps} />));
} else if (!falsePositives && !industries) { } else if (!falsePositives && !industries) {
@ -32,6 +32,7 @@ FireList.propTypes = {
nasa: PropTypes.bool.isRequired, nasa: PropTypes.bool.isRequired,
falsePositives: PropTypes.bool.isRequired, falsePositives: PropTypes.bool.isRequired,
industries: PropTypes.bool.isRequired, industries: PropTypes.bool.isRequired,
neighbour: PropTypes.bool.isRequired,
history: PropTypes.object.isRequired, history: PropTypes.object.isRequired,
t: PropTypes.func.isRequired t: PropTypes.func.isRequired
}; };

View file

@ -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 }) => (<FirePolygonMark t={t} history={history} id={_id} key={_id} nasa={nasa} {...otherProps} />));
return (<div style={{ display: 'none' }}>{items}</div>);
}
FireListUnion.propTypes = {
firesUnion: PropTypes.array.isRequired,
nasa: PropTypes.bool.isRequired,
history: PropTypes.object.isRequired,
t: PropTypes.func.isRequired
};

View file

@ -3,31 +3,32 @@
import React from 'react'; import React from 'react';
import { CircleMarker } from 'react-leaflet'; import { CircleMarker } from 'react-leaflet';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import FirePopup from './FirePopup';
import { translate } from 'react-i18next'; import { translate } from 'react-i18next';
import FirePopup from './FirePopup';
/* Less acurate (1 pixel per fire) but faster */ /* Less acurate (1 pixel per fire) but faster */
const FirePixel = ({ const FirePixel = ({
lat, lon, nasa, id, when, t, history lat, lon, nasa, id, when, t, history, track
}) => ( }) => (
<CircleMarker <CircleMarker
center={[lat, lon]} center={[lat, lon]}
color={nasa ? 'red' : '#D35400'} color={nasa ? 'red' : '#D35400'}
stroke={false} stroke={false}
fillOpacity="1" fillOpacity="1"
fill fill
radius={nasa ? 1 : 2} radius={nasa ? 1 : 2}
> >
<FirePopup t={t} history={history} id={id} nasa={nasa} lat={lat} lon={lon} when={when} /> <FirePopup t={t} history={history} id={id} nasa={nasa} lat={lat} lon={lon} when={when} />
</CircleMarker> </CircleMarker>
); );
FirePixel.propTypes = { FirePixel.propTypes = {
lat: PropTypes.number.isRequired, lat: PropTypes.number.isRequired,
lon: PropTypes.number.isRequired, lon: PropTypes.number.isRequired,
id: PropTypes.object.isRequired, id: PropTypes.object.isRequired,
history: PropTypes.object.isRequired, history: PropTypes.object.isRequired,
scan: PropTypes.number,
track: PropTypes.number,
when: PropTypes.instanceOf(Date), when: PropTypes.instanceOf(Date),
nasa: PropTypes.bool.isRequired, nasa: PropTypes.bool.isRequired,
t: PropTypes.func.isRequired t: PropTypes.func.isRequired

View file

@ -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 (
<GeoJSON data={shape} color="orange" stroke width="1" opacity=".2" fillOpacity=".0" />
);
/* <FirePopup t={t} history={history} id={id} nasa={nasa} lat={lat} lon={lon} when={when} /> */
}
}
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;

View file

@ -13,21 +13,21 @@ export const fireIcon = new Leaflet.Icon({
}); });
export const fireIconL = new Leaflet.Icon({ export const fireIconL = new Leaflet.Icon({
iconUrl: '/fire-marker-l.png', iconUrl: '/fire-marker-l.png?v2',
iconSize: [16, 24], iconSize: [28, 28],
iconAnchor: [8, 26] iconAnchor: [14, 30]
}); });
export const fireIconM = new Leaflet.Icon({ export const fireIconM = new Leaflet.Icon({
iconUrl: '/fire-marker-m.png', iconUrl: '/fire-marker-m.png?v2',
iconSize: [12, 17], iconSize: [28, 28],
iconAnchor: [6, 19] iconAnchor: [14, 30]
}); });
export const fireIconS = new Leaflet.Icon({ export const fireIconS = new Leaflet.Icon({
iconUrl: '/fire-marker-s.png', iconUrl: '/fire-marker-s.png?v2',
iconSize: [8, 12], iconSize: [28, 28],
iconAnchor: [4, 14] iconAnchor: [14, 30]
}); });
export const industryIcon = new Leaflet.Icon({ export const industryIcon = new Leaflet.Icon({

View file

@ -33,8 +33,7 @@ const subsUnion = (union, options) => {
options.map.leafletElement.fitBounds(L.latLngBounds(bounds._northEast, bounds._southWest)); options.map.leafletElement.fitBounds(L.latLngBounds(bounds._northEast, bounds._southWest));
} }
} else if (options.subs.length > 0) { } else if (options.subs.length > 0) {
const unionGroup = new L.FeatureGroup(); const result = calcUnion(L, options.subs, sub => sub, true);
const result = calcUnion(options.subs, unionGroup, sub => sub);
const unionJson = result[0]; const unionJson = result[0];
const bounds = result[1]; const bounds = result[1];

View file

@ -1,48 +1,9 @@
import { check } from 'meteor/check'; import { calcUnion as calcUnionWoBounds } from 'map-common-utils';
import LGeo from 'leaflet-geodesy';
import tunion from '@turf/union';
import ttrunc from '@turf/truncate';
// https://stackoverflow.com/questions/35394577/leaflet-js-union-merge-circles const calcUnion = (L, subs, decorated, typeCircle) => {
const truncOptions = { precision: 6, coordinates: 2 }; const unionJson = calcUnionWoBounds(subs, decorated, typeCircle);
// return [unionJson, L.geoJSON(unionJson).getBounds()];
const unify = (polyList) => { return [unionJson, unionJson === null ? null : L.geoJSON(unionJson).getBounds()];
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()];
}; };
export default calcUnion; export default calcUnion;

View file

@ -7,7 +7,7 @@ import { Link } from 'react-router-dom';
import { LinkContainer } from 'react-router-bootstrap'; import { LinkContainer } from 'react-router-bootstrap';
import { Trans, translate } from 'react-i18next'; import { Trans, translate } from 'react-i18next';
import { testId } from '/imports/ui/components/Utils/TestUtils'; import { testId } from '/imports/ui/components/Utils/TestUtils';
import BetaRibbon from '../../components/BetaRibbon/BetaRibbon'; /* import BetaRibbon from '../../components/BetaRibbon/BetaRibbon'; */
import PublicNavigation from '../PublicNavigation/PublicNavigation'; import PublicNavigation from '../PublicNavigation/PublicNavigation';
import AuthenticatedNavigation from '../AuthenticatedNavigation/AuthenticatedNavigation'; import AuthenticatedNavigation from '../AuthenticatedNavigation/AuthenticatedNavigation';
import NavItem from '../NavItem/NavItem'; import NavItem from '../NavItem/NavItem';
@ -19,7 +19,7 @@ import './Navigation.scss';
const Navigation = props => ( const Navigation = props => (
<nav className="navbar navbar-expand-lg navbar-dark bg-dark"> <nav className="navbar navbar-expand-lg navbar-dark bg-dark">
<div style={{ overflow: 'hidden' } /* for ribbon */} className="container"> <div style={{ overflow: 'hidden' } /* for ribbon */} className="container">
<BetaRibbon /> {/* <BetaRibbon /> */}
{/* <Navbar bsClass="navbar navbar-dark bg-dark"> */} {/* <Navbar bsClass="navbar navbar-dark bg-dark"> */}
{/* https://github.com/react-bootstrap/react-bootstrap/blob/master/src/Navbar.js */} {/* https://github.com/react-bootstrap/react-bootstrap/blob/master/src/Navbar.js */}
<Navbar.Header> <Navbar.Header>

View file

@ -12,7 +12,7 @@ import { trim } from './util.js';
function process(notif) { function process(notif) {
// No already notified // No already notified
if (Push.Permission.has()) { if (Push.Permission.has()) {
if (!notif.webNotified) { if (!notif.notified && notif.type === 'web') {
Push.create(i18n.t('AppName'), { Push.create(i18n.t('AppName'), {
body: `${trim(notif.content)} (${i18n.t('fireDetected', { when: dateFromNow(notif.when) })})`, body: `${trim(notif.content)} (${i18n.t('fireDetected', { when: dateFromNow(notif.when) })})`,
icon: '/n-fire-marker.png', icon: '/n-fire-marker.png',
@ -47,7 +47,7 @@ Meteor.startup(() => {
if (Meteor.userId()) { if (Meteor.userId()) {
Meteor.subscribe('mynotifications'); Meteor.subscribe('mynotifications');
// Check for notifications not processed at startup // Check for notifications not processed at startup
Notifications.find({ webNotified: null }).forEach((notif) => { Notifications.find({ notified: null, type: 'web' }).forEach((notif) => {
process(notif); process(notif);
}); });
} }

View file

@ -1,30 +1,24 @@
/* eslint-disable react/jsx-indent-props */ /* eslint-disable react/jsx-indent-props */
/* eslint-disable import/no-absolute-path */ /* eslint-disable import/no-absolute-path */
import React, { Fragment, Component } from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { translate } from 'react-i18next'; import { translate } from 'react-i18next';
import { Meteor } from 'meteor/meteor'; /* import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker'; import { Tracker } from 'meteor/tracker'; */
import Blaze from 'meteor/gadicc:blaze-react-component'; import Blaze from 'meteor/gadicc:blaze-react-component';
class Reconnect extends Component { const Reconnect = ({ t, authenticated }) => (
render() { <div>
const { t, authenticated } = this.props; { true && /* !authenticated && */
// console.log(`Reconnect ${!authenticated ? 'enabled' : 'disabled'}`); <Blaze
return (
<Fragment>
{ !authenticated &&
<Blaze
template="meteorStatus" template="meteorStatus"
textDisconnect={t('Desconectado del servidor, reconectando en %delay% segundos.')} textDisconnect={t('Desconectado del servidor, reconectando en %delay% segundos.')}
textConnecting={t('Desconectado del servidor, reconectando...')} textConnecting={t('Desconectado del servidor, reconectando...')}
linkText={t('Reintentar ahora')} linkText={t('Reintentar ahora')}
/> } /> }
</Fragment> </div>
); );
}
}
Reconnect.propTypes = { Reconnect.propTypes = {
t: PropTypes.func.isRequired, t: PropTypes.func.isRequired,
@ -36,11 +30,12 @@ Reconnect.defaultProps = {
export default translate([], { wait: true })(Reconnect); export default translate([], { wait: true })(Reconnect);
/*
if (!Meteor.isProduction) { if (!Meteor.isProduction) {
// We clear the console on disconnect during development // We clear the console on disconnect during development
Tracker.autorun(() => { Tracker.autorun(() => {
if (Meteor.status().status === 'waiting') { if (Meteor.status().status === 'waiting') {
// console.clear(); console.clear();
} }
}); });
} } */

View file

@ -242,9 +242,10 @@ class SelectionMap extends Component {
{ this.props.sndBtn && this.props.onSndBtn && { this.props.sndBtn && this.props.onSndBtn &&
<Button <Button
bsStyle="warning" bsStyle="warning"
title={this.props.sndBtnTitle}
onClick={event => this.onSndBtn(event)} onClick={event => this.onSndBtn(event)}
> >
{this.props.sndBtn} {this.props.sndBtn.match(/^fa-/) ? <i className={`fa ${this.props.sndBtn}`} /> : this.props.sndBtn }
</Button> </Button>
} }
<Button <Button
@ -266,7 +267,8 @@ class SelectionMap extends Component {
} }
SelectionMap.defaultProps = { SelectionMap.defaultProps = {
zoom: 11 zoom: 11,
sndBtnTitle: ''
}; };
SelectionMap.propTypes = { SelectionMap.propTypes = {
@ -280,6 +282,7 @@ SelectionMap.propTypes = {
onFstBtn: PropTypes.func.isRequired, onFstBtn: PropTypes.func.isRequired,
sndBtn: PropTypes.string, sndBtn: PropTypes.string,
onSndBtn: PropTypes.func, onSndBtn: PropTypes.func,
sndBtnTitle: PropTypes.string,
onRemove: PropTypes.func, onRemove: PropTypes.func,
action: PropTypes.number.isRequired, action: PropTypes.number.isRequired,
loadingSubs: PropTypes.bool.isRequired, loadingSubs: PropTypes.bool.isRequired,

View file

@ -68,6 +68,7 @@ class SubscriptionEditor extends React.Component {
zoom={this.state.zoom} zoom={this.state.zoom}
distance={doc.distance} distance={doc.distance}
focusInput={focus} focusInput={focus}
history={this.props.history}
disableFstBtn={this.state.disableFstBtn} disableFstBtn={this.state.disableFstBtn}
subsBtn={isEdit ? t('Actualizar') : t(isAnyMobile ? 'Suscribirme a este radio' : 'Suscribirme a fuegos en este radio')} subsBtn={isEdit ? t('Actualizar') : t(isAnyMobile ? 'Suscribirme a este radio' : 'Suscribirme a fuegos en este radio')}
onSubs={state => this.onSubs(state)} onSubs={state => this.onSubs(state)}

View file

@ -18,6 +18,7 @@ import '/imports/startup/client/meta';
import '/imports/startup/client/ravenLogger'; import '/imports/startup/client/ravenLogger';
import '/imports/startup/client/geolocation'; import '/imports/startup/client/geolocation';
import '/imports/startup/client/piwik-start.js'; import '/imports/startup/client/piwik-start.js';
import '/imports/startup/client/reconnect.js';
import Reconnect from '../../components/Reconnect/Reconnect'; import Reconnect from '../../components/Reconnect/Reconnect';
import Navigation from '../../components/Navigation/Navigation'; import Navigation from '../../components/Navigation/Navigation';
import Authenticated from '../../components/Authenticated/Authenticated'; import Authenticated from '../../components/Authenticated/Authenticated';
@ -37,16 +38,16 @@ import VerifyEmail from '../../pages/VerifyEmail/VerifyEmail';
import RecoverPassword from '../../pages/RecoverPassword/RecoverPassword'; import RecoverPassword from '../../pages/RecoverPassword/RecoverPassword';
import ResetPassword from '../../pages/ResetPassword/ResetPassword'; import ResetPassword from '../../pages/ResetPassword/ResetPassword';
import Profile from '../../pages/Profile/Profile'; import Profile from '../../pages/Profile/Profile';
import NotFound from '../../pages/NotFound/NotFound';
import FiresMap from '../../pages/FiresMap/FiresMap';
import Fires from '../../pages/Fires/Fires'; import Fires from '../../pages/Fires/Fires';
import Sandbox from '../../pages/Sandbox/Sandbox'; import Sandbox from '../../pages/Sandbox/Sandbox';
import ZonesMap from '../../pages/ZonesMap/ZonesMap';
import FiresMap from '../../pages/FiresMap/FiresMap';
import NotFound from '../../pages/NotFound/NotFound';
import Terms from '../../pages/Terms/Terms'; import Terms from '../../pages/Terms/Terms';
import About from '../../pages/About/About'; import About from '../../pages/About/About';
import Privacy from '../../pages/Privacy/Privacy'; import Privacy from '../../pages/Privacy/Privacy';
import License from '../../pages/License/License'; import License from '../../pages/License/License';
import Credits from '../../pages/Credits/Credits'; import Credits from '../../pages/Credits/Credits';
import ZonesMap from '../../pages/ZonesMap/ZonesMap';
import Footer from '../../components/Footer/Footer'; import Footer from '../../components/Footer/Footer';
import Feedback from '../../components/Feedback/Feedback'; import Feedback from '../../components/Feedback/Feedback';
import ReSendEmail from '../../components/ReSendEmail/ReSendEmail'; import ReSendEmail from '../../components/ReSendEmail/ReSendEmail';
@ -57,11 +58,10 @@ import './App.scss';
class LocationListener extends Component { class LocationListener extends Component {
// https://stackoverflow.com/questions/43512450/react-router-v4-route-onchange-event // https://stackoverflow.com/questions/43512450/react-router-v4-route-onchange-event
static contextTypes = {
router: PropTypes.object
};
componentDidMount() { componentDidMount() {
// https://github.com/GoogleChrome/rendertron#rendering-budget-timeout
window.renderComplete = true;
this.handleLocationChange(this.context.router.history.location); this.handleLocationChange(this.context.router.history.location);
this.unlisten = this.unlisten =
this.context.router.history.listen(this.handleLocationChange); this.context.router.history.listen(this.handleLocationChange);
@ -75,82 +75,90 @@ class LocationListener extends Component {
// your staff here // your staff here
console.log(`----- location: '${location.pathname}'`); console.log(`----- location: '${location.pathname}'`);
Meteor.Piwik.trackPage(location.pathname); Meteor.Piwik.trackPage(location.pathname);
// Meteor.isReadyForSpiderable = true;
} }
// https://stackoverflow.com/questions/39133797/react-only-return-props-children
render() { render() {
return this.props.children; return this.props.children;
} }
} }
LocationListener.contextTypes = {
router: PropTypes.object
};
LocationListener.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired
};
const App = props => ( const App = props => (
/* https://react.i18next.com/components/i18nextprovider.html */ /* https://react.i18next.com/components/i18nextprovider.html */
<I18nextProvider i18n={i18n}> <div>
{props.i18nReady.get() &&
<ErrorBoundary> <ErrorBoundary>
<Router history={history}> <I18nextProvider i18n={i18n}>
<LocationListener> <ErrorBoundary appName={i18n.t('AppNameFull')} title={i18n.t('general-error-title')} subTitle={i18n.t('general-error-description')}>
{ !props.loading ? <Router history={history}>
<div className="App"> <LocationListener>
<Helmet> { !props.loading &&
<meta charSet="utf-8" /> <div className="App">
<title>{i18n.t('AppName')}</title> <Helmet>
<meta name="description" content={`${i18n.t('AppDescrip')}: ${i18n.t('AppDescripLong')}`} /> <meta charSet="utf-8" />
<link rel="alternate" href="https://fires.comunes.org/" hrefLang="en" /> <title>{i18n.t('AppName')}</title>
<link rel="alternate" href="https://fuegos.comunes.org/" hrefLang="es" /> <meta name="description" content={`${i18n.t('AppDescrip')}: ${i18n.t('AppDescripLong')}`} />
</Helmet> <link rel="alternate" href={`https://fires.comunes.org${history.location.pathname}`} hrefLang="en" />
<Navigation {...props} /> <link rel="alternate" href={`https://fuegos.comunes.org${history.location.pathname}`} hrefLang="es" />
<ReSendEmail {...props} /> </Helmet>
<Navigation {...props} />
<ReSendEmail {...props} />
<Grid> <Grid>
<Switch> <Switch>
<Route exact name="index" path="/" component={Index} /> <Route exact name="index" path="/" component={Index} />
{/* <Authenticated exact path="/documents" component={Documents} {...props} /> <Authenticated exact path="/subscriptions" component={Subscriptions} {...props} />
<Authenticated exact path="/documents/new" component={NewDocument} {...props} /> <Authenticated exact path="/subscriptions/new" component={NewSubscription} {...props} />
<Authenticated exact path="/documents/:_id" component={ViewDocument} {...props} /> <Authenticated exact path="/subscriptions/:_id" component={ViewSubscription} {...props} />
<Authenticated exact path="/documents/:_id/edit" component={EditDocument} {...props} /> <Authenticated exact path="/subscriptions/:_id/edit" component={EditSubscription} {...props} />
*/} <Authenticated exact path="/profile" component={Profile} {...props} />
<Authenticated exact path="/subscriptions" component={Subscriptions} {...props} /> <Authenticated exact path="/status" component={Status} {...props} />
<Authenticated exact path="/subscriptions/new" component={NewSubscription} {...props} /> <Route path="/fires" component={FiresMap} industries={false} {...props} />
<Authenticated exact path="/subscriptions/:_id" component={ViewSubscription} {...props} /> <Route path="/zones" component={ZonesMap} {...props} />
<Authenticated exact path="/subscriptions/:_id/edit" component={EditSubscription} {...props} />
<Authenticated exact path="/profile" component={Profile} {...props} />
<Authenticated exact path="/status" component={Status} {...props} />
{/* <Route path="/fires/:type(with-industries)" component={FiresMap} industries {...props} /> */}
<Route path="/fires" component={FiresMap} industries={false} {...props} />
<Route path="/zones" component={ZonesMap} {...props} />
<Route path="/fire/:type(active|archive|alert)/:id" component={Fires} {...props} />
<Route path="/fire/:id" component={Fires} {...props} />
<Public path="/auth/:token" component={Auth} {...props} />
<Public path="/signup" component={Signup} {...props} />
<Public path="/login" component={Login} {...props} />
<Route path="/logout" component={Logout} {...props} />
<Route path="/sandbox" component={Sandbox} {...props} />
<Route path="/error" component={TestError} {...props} />
{/* <Route path="/subscriptions" render={props => <FireSubscription focusInput {...props} />} /> */}
<Route name="verify-email" path="/verify-email/:token" component={VerifyEmail} /> <Route path="/fire/:type(active|archive|alert)/:id" component={Fires} {...props} />
<Route name="recover-password" path="/recover-password" component={RecoverPassword} /> <Route path="/fire/:id" component={Fires} {...props} />
<Route name="reset-password" path="/reset-password/:token" component={ResetPassword} /> <Public path="/auth/:token" component={Auth} {...props} />
<Route name="terms" path="/terms" component={Terms} /> <Public path="/signup" component={Signup} {...props} />
<Route name="privacy" path="/privacy" component={Privacy} /> <Public path="/login" component={Login} {...props} />
<Route name="license" path="/license" component={License} /> <Route path="/logout" component={Logout} {...props} />
<Route name="credits" path="/credits" component={Credits} /> <Route path="/sandbox" component={Sandbox} {...props} />
<Route name="about" path="/about" component={About} /> <Route path="/error" component={TestError} {...props} />
<Route component={NotFound} /> <Route name="verify-email" path="/verify-email/:token" component={VerifyEmail} />
</Switch> <Route name="recover-password" path="/recover-password" component={RecoverPassword} />
</Grid> <Route name="reset-password" path="/reset-password/:token" component={ResetPassword} />
<Footer /> <Route name="terms" path="/terms" component={Terms} />
<Reconnect {...props} /> <Route name="privacy" path="/privacy" component={Privacy} />
<Feedback history={history} {...props} /> <Route name="license" path="/license" component={License} />
{props.i18nReady.get() && <Route name="credits" path="/credits" component={Credits} />
<Blaze template="cookieConsent" /> <Route name="about" path="/about" component={About} />
}
</div> : ''} <Route component={NotFound} />
</LocationListener> </Switch>
</Router> </Grid>
</ErrorBoundary> <Footer />
</I18nextProvider> <Reconnect {...props} />
<Feedback {...props} />
{props.i18nReady.get() && <Blaze template="cookieConsent" /> }
</div>}
</LocationListener>
</Router>
</ErrorBoundary>
</I18nextProvider>
</ErrorBoundary> }
</div>
); );
App.defaultProps = { App.defaultProps = {
@ -178,7 +186,9 @@ export default withTracker(() => {
const loading = !Roles.subscription.ready() || !i18nReady.get(); const loading = !Roles.subscription.ready() || !i18nReady.get();
const name = user && user.profile && user.profile.name && getUserName(user.profile.name); const name = user && user.profile && user.profile.name && getUserName(user.profile.name);
const emailAddress = user && user.emails && user.emails[0].address; const emailAddress = user && user.emails && user.emails[0].address;
// console.log(`i18n ready?: ${i18nReady.get()}`); Meteor.autorun(() => {
console.log(`i18n ready?: ${i18nReady.get()}`);
});
return { return {
loading, loading,
loggingIn, loggingIn,

View file

@ -10,6 +10,7 @@ import Gkeys from '/imports/startup/client/Gkeys';
import CenterInMyPosition from '/imports/ui/components/CenterInMyPosition/CenterInMyPosition'; import CenterInMyPosition from '/imports/ui/components/CenterInMyPosition/CenterInMyPosition';
import LocationAutocomplete from '/imports/ui/components/LocationAutocomplete/LocationAutocomplete'; import LocationAutocomplete from '/imports/ui/components/LocationAutocomplete/LocationAutocomplete';
import { isNotHomeAndMobile } from '/imports/ui/components/Utils/isMobile'; import { isNotHomeAndMobile } from '/imports/ui/components/Utils/isMobile';
import { isHome } from '/imports/ui/components/Utils/location';
class FireSubscription extends React.Component { class FireSubscription extends React.Component {
constructor(props) { constructor(props) {
@ -20,10 +21,7 @@ class FireSubscription extends React.Component {
zoom: this.props.zoom, zoom: this.props.zoom,
distance: this.props.distance distance: this.props.distance
}; };
/* if (props.location) { this.onCancel = this.onCancel.bind(this);
console.log(props.location.state);
}
console.log(this.state); */
} }
componentDidMount() { componentDidMount() {
@ -62,6 +60,10 @@ class FireSubscription extends React.Component {
this.props.onSubs(value); this.props.onSubs(value);
} }
onCancel() {
this.props.history.push('/subscriptions');
}
centerOnUserLocation(value) { centerOnUserLocation(value) {
this.setState({ center: value.center }); this.setState({ center: value.center });
} }
@ -103,6 +105,9 @@ class FireSubscription extends React.Component {
distance={this.state.distance} distance={this.state.distance}
fstBtn={this.props.subsBtn} fstBtn={this.props.subsBtn}
onFstBtn={state => this.onSubs(state)} onFstBtn={state => this.onSubs(state)}
sndBtn={isHome() ? null : 'fa-times'}
onSndBtn={this.onCancel}
sndBtnTitle={this.props.t('Cancelar')}
onSelection={state => this.onSelection(state)} onSelection={state => this.onSelection(state)}
action={action.add} action={action.add}
disableFstBtn={this.props.disableFstBtn} disableFstBtn={this.props.disableFstBtn}
@ -113,12 +118,14 @@ class FireSubscription extends React.Component {
} }
FireSubscription.propTypes = { FireSubscription.propTypes = {
t: PropTypes.func.isRequired,
center: PropTypes.arrayOf(PropTypes.number), center: PropTypes.arrayOf(PropTypes.number),
zoom: PropTypes.number, zoom: PropTypes.number,
distance: PropTypes.number, distance: PropTypes.number,
focusInput: PropTypes.bool.isRequired, focusInput: PropTypes.bool.isRequired,
subsBtn: PropTypes.string.isRequired, subsBtn: PropTypes.string.isRequired,
onSubs: PropTypes.func.isRequired, onSubs: PropTypes.func.isRequired,
history: PropTypes.object.isRequired,
disableFstBtn: PropTypes.bool.isRequired disableFstBtn: PropTypes.bool.isRequired
}; };

View file

@ -10,14 +10,15 @@ import { Row, Col, Alert, FormGroup } from 'react-bootstrap';
import { Meteor } from 'meteor/meteor'; import { Meteor } from 'meteor/meteor';
import { Bert } from 'meteor/themeteorchef:bert'; import { Bert } from 'meteor/themeteorchef:bert';
import { Helmet } from 'react-helmet'; 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 Blaze from 'meteor/gadicc:blaze-react-component';
import DefMapLayers from '/imports/ui/components/Maps/DefMapLayers'; import DefMapLayers from '/imports/ui/components/Maps/DefMapLayers';
import NotFound from '/imports/ui/pages/NotFound/NotFound'; import NotFound from '/imports/ui/pages/NotFound/NotFound';
import FiresCollection from '/imports/api/Fires/Fires'; import FiresCollection from '/imports/api/Fires/Fires';
import FireList from '/imports/ui/components/Maps/FireList'; import FireList from '/imports/ui/components/Maps/FireList';
import FromNow from '/imports/ui/components/FromNow/FromNow'; import FromNow from '/imports/ui/components/FromNow/FromNow';
import { dateLongFormat } from '/imports/api/Common/dates'; import { dateLongFormat, dateYYYYMMDD } from '/imports/api/Common/dates';
import '/imports/startup/client/comments'; import '/imports/startup/client/comments';
import FalsePositiveTypes from '/imports/api/FalsePositives/FalsePositiveTypes'; import FalsePositiveTypes from '/imports/api/FalsePositives/FalsePositiveTypes';
import FalsePositivesCollection, { falsePositivesRemap } from '/imports/api/FalsePositives/FalsePositives'; import FalsePositivesCollection, { falsePositivesRemap } from '/imports/api/FalsePositives/FalsePositives';
@ -52,7 +53,7 @@ class Fire extends React.Component {
} }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
return !(nextState.when === this.state.when && nextState.loading === this.state.loading && this.state.notfound === nextState.notfound); return !(nextState.when === this.state.when && nextState.loading === this.state.loading && this.state.notfound === nextState.notfound && nextState.nasaLink === this.state.nasaLink);
} }
onTypeSelect(key) { onTypeSelect(key) {
@ -67,7 +68,34 @@ class Fire extends React.Component {
}); });
} }
handleLeafletLoad(circle) { handleLeafletMapLoad(map) {
if (map && map.leafletElement) {
const lmap = map.leafletElement;
const bounds = lmap.getBounds();
const ne = bounds.getNorthEast();
const sw = bounds.getSouthWest();
// console.log(`${sw},${ne}`);
// const nasaLinkBase = 'https://worldview.earthdata.nasa.gov/?p=geographic&l=VIIRS_SNPP_CorrectedReflectance_TrueColor(hidden),MODIS_Aqua_CorrectedReflectance_TrueColor(hidden),MODIS_Terra_CorrectedReflectance_TrueColor,MODIS_Fires_All,MODIS_Fires_Aqua,VIIRS_SNPP_Fires_375m_Night,VIIRS_SNPP_Fires_375m_Day,MODIS_Fires_Terra,Reference_Labels(hidden),Reference_Features(hidden),Coastlines&t='
const nasaLinkBase = 'https://worldview.earthdata.nasa.gov/?p=geographic&l=' +
'VIIRS_SNPP_CorrectedReflectance_TrueColor,' +
'MODIS_Aqua_CorrectedReflectance_TrueColor,' +
'MODIS_Terra_CorrectedReflectance_TrueColor,' +
'MODIS_Terra_Data_No_Data(hidden),' +
'MODIS_Fires_All,' +
'MODIS_Fires_Aqua,' +
'VIIRS_SNPP_Fires_375m_Night,' +
'VIIRS_SNPP_Fires_375m_Day,' +
'MODIS_Fires_Terra,' +
'Reference_Labels,' +
'Reference_Features,' +
'Coastlines' +
'&t=';
const nasaZoom = 5;
this.setState({ nasaLink: `${nasaLinkBase}${this.dateYYYYMMDD}&z=3&v=${sw.lng},${sw.lat},${ne.lng},${ne.lat}&ab=off&as=${this.dateYYYYMMDD}&ae=${this.dateYYYYMMDD}&av=${nasaZoom}&al=true` });
}
}
handleLeafletCircleLoad(circle) {
if (this.fireMap && this.fireMap.leafletElement && circle && circle.leafletElement) { if (this.fireMap && this.fireMap.leafletElement && circle && circle.leafletElement) {
this.fireMap.leafletElement.fitBounds(circle.leafletElement.getBounds()); this.fireMap.leafletElement.fitBounds(circle.leafletElement.getBounds());
} }
@ -83,12 +111,14 @@ class Fire extends React.Component {
* console.log(`Not found fire: ${notfound}`); */ * console.log(`Not found fire: ${notfound}`); */
if (fire && fire.when) { if (fire && fire.when) {
this.dateLongFormat = dateLongFormat(fire.when); this.dateLongFormat = dateLongFormat(fire.when);
this.dateYYYYMMDD = dateYYYYMMDD(fire.when);
this.title = fire.address ? this.title = fire.address ?
t('Información adicional sobre fuego detectado en {{where}} el {{when}}', { where: fire.address, when: this.dateLongFormat }) : 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 }); t('Información adicional sobre fuego detectado el {{when}}', { when: this.dateLongFormat });
} }
const ready = fire && !loading;
return (fire && !loading ? ( const rect = ready ? rectangleAround({ lat: fire.lat, lon: fire.lon }, fire.track, fire.track) : null;
return (ready ? (
<div className="ViewFire"> <div className="ViewFire">
<Helmet> <Helmet>
<title>{t('AppName')}: {t('Información adicional sobre fuego')}</title> <title>{t('AppName')}: {t('Información adicional sobre fuego')}</title>
@ -98,7 +128,10 @@ class Fire extends React.Component {
<Fragment> <Fragment>
<h4 className="page-header">{this.title}</h4> <h4 className="page-header">{this.title}</h4>
<Map <Map
ref={(map) => { this.fireMap = map; }} ref={(map) => {
this.fireMap = map;
this.handleLeafletMapLoad(map);
}}
animate animate
sleep={false} sleep={false}
center={[fire.lat, fire.lon]} center={[fire.lat, fire.lon]}
@ -106,14 +139,13 @@ class Fire extends React.Component {
zoom={13} zoom={13}
> >
<Fragment> <Fragment>
<Circle <GeoJSON
center={[fire.lat, fire.lon]} ref={(circle) => { this.circle = circle; this.handleLeafletCircleLoad(circle); }}
data={rect}
color="red" color="red"
fillColor="red" stroke
fillOpacity={0.0} width="1"
interactive={false} fillOpacity="0.0"
radius={fire.scan ? fire.scan * 500 : 300}
ref={(circle) => { this.circle = circle; this.handleLeafletLoad(circle); }}
/> />
</Fragment> </Fragment>
<FireList <FireList
@ -124,6 +156,7 @@ class Fire extends React.Component {
useMarkers useMarkers
nasa={false} nasa={false}
falsePositives falsePositives
neighbour={false}
industries={false} industries={false}
/> />
<FireList <FireList
@ -134,6 +167,7 @@ class Fire extends React.Component {
useMarkers useMarkers
nasa={false} nasa={false}
falsePositives={false} falsePositives={false}
neighbour={false}
industries industries
/> />
<DefMapLayers satellite /> <DefMapLayers satellite />
@ -141,7 +175,10 @@ class Fire extends React.Component {
</Map> </Map>
<p>{t('Coordenadas:')} {fire.lat}, {fire.lon}</p> <p>{t('Coordenadas:')} {fire.lat}, {fire.lon}</p>
{(fire.type === 'modis' || fire.type === 'viirs') && {(fire.type === 'modis' || fire.type === 'viirs') &&
<p><Trans>Fuego detectado por satélites de la NASA <FromNow {...this.props} /></Trans></p> <Fragment>
<p><a target="_blank" href={`${this.state.nasaLink}`}><Trans>Fuego detectado por satélites de la NASA <FromNow {...this.props} /></Trans></a>
</p>
</Fragment>
} }
{(fire.type === 'vecinal') && {(fire.type === 'vecinal') &&
<p><Trans>Fuego notificado por uno de nuestros usuarios/as <FromNow {...this.props} /></Trans></p> <p><Trans>Fuego notificado por uno de nuestros usuarios/as <FromNow {...this.props} /></Trans></p>
@ -242,7 +279,7 @@ const FireContainer = withTracker(({ match }) => {
} else { } else {
console.log('Seems a fire from enc hash'); console.log('Seems a fire from enc hash');
fromHash = true; fromHash = true;
subscription = Meteor.subscribe('fireFromHash', id); subscription = Meteor.subscribe('fireFromHash', id, match.params);
} }
// console.log(`Type of '${fireType}' fire, active: ${active}, archive: ${archive}, fromHash: ${fromHash}`); // console.log(`Type of '${fireType}' fire, active: ${active}, archive: ${archive}, fromHash: ${fromHash}`);

View file

@ -0,0 +1,60 @@
/* eslint-disable react/jsx-indent-props */
/* eslint-disable import/no-absolute-path */
/* eslint-disable import/no-absolute-path */
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { translate, Trans } from 'react-i18next';
import FromNow from '/imports/ui/components/FromNow/FromNow';
class FireStats extends Component {
constructor(props) {
super(props);
this.state = {
lastFireDetected: props.lastFireDetected,
lastCheck: props.lastCheck,
loadingAll: props.loadingAll
};
}
componentWillReceiveProps(nextProps) {
if (this.props.lastCheck !== nextProps.lastCheck ||
this.props.lastFireDetected !== nextProps.lastFireDetected ||
this.props.loadingAll !== nextProps.loadingAll
) {
this.setState({
lastFireDetected: nextProps.lastFireDetected,
lastCheck: nextProps.lastCheck,
loadingAll: nextProps.loadingAll
});
}
}
shouldComponentUpdate(nextProps, nextState) {
return !(nextState.lastCheck === this.state.lastCheck &&
nextState.lastFireDetected === this.state.lastFireDetected &&
nextState.loadingAll === this.state.loadingAll);
}
render() {
if (this.state.loadingAll) return (<Fragment><br /><em><Trans>Actualizando...</Trans></em></Fragment>);
if (this.state.lastCheck && !this.state.lastFireDetected) {
return (<Fragment><br /><Trans>Actualizado <FromNow when={this.state.lastCheck} />.</Trans></Fragment>);
}
if (this.state.lastCheck && this.state.lastFireDetected) {
return (<Fragment><br /><Trans>Actualizado <FromNow when={this.state.lastCheck} />, último fuego detectado <FromNow when={this.state.lastFireDetected.when} />.</Trans></Fragment>);
}
return null;
}
}
FireStats.propTypes = {
lastFireDetected: PropTypes.object,
loadingAll: PropTypes.bool.isRequired,
lastCheck: PropTypes.instanceOf(Date)
};
FireStats.defaultProps = {
};
export default translate([], { wait: true })(FireStats);

View file

@ -23,30 +23,32 @@ import 'leaflet-sleep/Leaflet.Sleep.js';
import geolocation from '/imports/startup/client/geolocation'; import geolocation from '/imports/startup/client/geolocation';
import CenterInMyPosition from '/imports/ui/components/CenterInMyPosition/CenterInMyPosition'; import CenterInMyPosition from '/imports/ui/components/CenterInMyPosition/CenterInMyPosition';
import FireList from '/imports/ui/components/Maps/FireList'; import FireList from '/imports/ui/components/Maps/FireList';
import FireListUnion from '/imports/ui/components/Maps/FireListUnion';
import subsUnion from '/imports/ui/components/Maps/SubsUnion/SubsUnion'; import subsUnion from '/imports/ui/components/Maps/SubsUnion/SubsUnion';
import DefMapLayers from '/imports/ui/components/Maps/DefMapLayers'; import DefMapLayers from '/imports/ui/components/Maps/DefMapLayers';
import FromNow from '/imports/ui/components/FromNow/FromNow';
import ActiveFiresCollection from '/imports/api/ActiveFires/ActiveFires'; import ActiveFiresCollection from '/imports/api/ActiveFires/ActiveFires';
import ActiveFiresUnion from '/imports/api/ActiveFiresUnion/ActiveFiresUnion';
import FireAlertsCollection from '/imports/api/FireAlerts/FireAlerts'; import FireAlertsCollection from '/imports/api/FireAlerts/FireAlerts';
import IndustriesCollection, { industriesRemap } from '/imports/api/Industries/Industries'; import IndustriesCollection, { industriesRemap } from '/imports/api/Industries/Industries';
import FalsePositivesCollection, { falsePositivesRemap } from '/imports/api/FalsePositives/FalsePositives'; import FalsePositivesCollection, { falsePositivesRemap } from '/imports/api/FalsePositives/FalsePositives';
import SiteSettings from '/imports/api/SiteSettings/SiteSettings'; import SiteSettings from '/imports/api/SiteSettings/SiteSettings';
import { isNotHomeAndMobile, isChrome } from '/imports/ui/components/Utils/isMobile'; import { isNotHomeAndMobile, isChrome, isAnyMobile } from '/imports/ui/components/Utils/isMobile';
import { isHome } from '/imports/ui/components/Utils/location'; import { isHome } from '/imports/ui/components/Utils/location';
import ShareIt from '/imports/ui/components/ShareIt/ShareIt'; import ShareIt from '/imports/ui/components/ShareIt/ShareIt';
import FullScreenMap from '/imports/ui/components/Maps/FullScreenMap'; import FullScreenMap from '/imports/ui/components/Maps/FullScreenMap';
import LocationAutocomplete from '/imports/ui/components/LocationAutocomplete/LocationAutocomplete'; import LocationAutocomplete from '/imports/ui/components/LocationAutocomplete/LocationAutocomplete';
import Gkeys from '/imports/startup/client/Gkeys'; import Gkeys from '/imports/startup/client/Gkeys';
import FireStats from './FireStats';
import './FiresMap.scss'; import './FiresMap.scss';
const MAXZOOM = 6; const MAXZOOM = 6;
const MAXZOOMREACTIVE = 6; const MAXZOOMREACTIVE = 6;
const DEFZOOM = 8; const DEFZOOM = 8;
const zoom = new ReactiveVar(DEFZOOM); const zoom = new ReactiveVar(DEFZOOM);
const center = new ReactiveVar([0, 0]); // https://en.wikipedia.org/wiki/Geographical_midpoint_of_Europe
const center = new ReactiveVar([53.5775, 3.106111]);
const mapSize = new ReactiveVar(); const mapSize = new ReactiveVar();
const marks = new ReactiveVar(false); const marks = new ReactiveVar(true);
const showUnion = new ReactiveVar(true); const showUnion = new ReactiveVar(true);
// Remove map in subscription // Remove map in subscription
@ -82,19 +84,6 @@ class FiresMap extends React.Component {
}); });
} }
/* shouldComponentUpdate(nextProps, nextState) {
* const notMoving = !nextState.moving;
* const markersChanged = this.state.useMarkers !== nextState.useMarkers;
* const unionChanged = this.state.showSubsUnion !== nextState.showSubsUnion;
* const otherViewport = this.state.viewport !== nextState.viewport;
* // const init = nextState.viewport.center === [0, 0];
* // console.log(notMoving ? 'Not moving map' : 'Moving map');
* // console.log(otherViewport ? 'Other viewport' : 'Not other viewport');
* console.log(`${otherViewport ? 'OTHER' : 'Not other'} viewport ${nextState.viewport.center} zoom: ${nextState.viewport.zoom}`);
* return this.state.init || (notMoving && otherViewport && this.state.moved) || unionChanged || markersChanged;
* }
*/
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
const notMoving = !nextState.moving; const notMoving = !nextState.moving;
return notMoving; return notMoving;
@ -152,7 +141,7 @@ class FiresMap extends React.Component {
} }
centerOnUserLocation(viewport) { centerOnUserLocation(viewport) {
this.setState({ viewport }); this.setState({ viewport: { center: viewport.center, zoom: 10 } });
} }
useMarkers(use) { useMarkers(use) {
@ -196,11 +185,15 @@ class FiresMap extends React.Component {
render() { render() {
const { t } = this.props; const { t } = this.props;
console.log(`Rendering ${this.props.loading ? 'loading' : 'LOADED'}, zoom ${this.state.viewport.zoom}, map ${this.props.activefires.length + this.props.firealerts.length} of ${this.props.activefirestotal} total. False positives: ${this.props.falsePositives.length}. Reactive ${this.state.viewport.zoom >= MAXZOOMREACTIVE}`); const loading = this.props.loading || !this.props.subsready;
console.log(`Rendering ${loading ? 'loading' : 'LOADED'}, zoom ${this.state.viewport.zoom}, map ${this.props.activefires.length + this.props.firealerts.length} of ${this.props.activefirestotal} total. False positives: ${this.props.falsePositives.length}. Reactive ${this.state.viewport.zoom >= MAXZOOMREACTIVE}`);
const title = `${t('AppName')}: ${t('Fuegos activos')}`; const title = `${t('AppName')}: ${t('Fuegos activos')}`;
if (Meteor.isDevelopment && this.props.activefires.length === 1) console.log(`Active fire: ${JSON.stringify(this.props.activefires[0])}`);
if (Meteor.isDevelopment) console.log(`Active fires total: ${this.props.activefires.length}`);
if (Meteor.isDevelopment) console.log(`Active fires union total: ${this.props.activefiresunion.length}`);
if (Meteor.isDevelopment) console.log(`False positives total: ${this.props.falsePositives.length}`); if (Meteor.isDevelopment) console.log(`False positives total: ${this.props.falsePositives.length}`);
if (Meteor.isDevelopment) console.log(`Fire alerts total: ${this.props.firealerts.length}`);
if (Meteor.isDevelopment) console.log(`Industries total: ${this.props.industries.length}`); if (Meteor.isDevelopment) console.log(`Industries total: ${this.props.industries.length}`);
if (!this.state.init) { if (!this.state.init) {
@ -218,50 +211,51 @@ class FiresMap extends React.Component {
<title>{title}</title> <title>{title}</title>
<meta name="description" content={t('Fuegos activos en el mundo actualizados en tiempo real')} /> <meta name="description" content={t('Fuegos activos en el mundo actualizados en tiempo real')} />
</Helmet> } </Helmet> }
<h4 className="page-header"><Trans parent="span">Fuegos activos</Trans></h4> <h4 className="page-header"><Trans parent="span">Fuegos activos</Trans></h4>
<Row> <Row>
<Col xs={12} sm={6} md={6} lg={6} > <Col xs={12} sm={6} md={6} lg={6}>
<p className="firesmap-legend"> {isNotHomeAndMobile() &&
{ (this.props.activefires.length + this.props.firealerts.length) === 0 ? <Fragment>
<Fragment><Trans parent="span" i18nKey="noActiveFireInMapCount">No hay fuegos activos en esta zona del mapa. Hay un total de <strong>{{ countTotal: this.props.activefirestotal }}</strong> fuegos activos detectados en todo el mundo.</Trans> <Trans>Datos actualizados <FromNow when={this.props.lastCheck} />.</Trans></Fragment> : <LocationAutocomplete
<Fragment><Trans parent="span" i18nKey="activeFireInMapCount">En rojo, <strong>{{ count: this.props.activefires.length + this.props.firealerts.length }}</strong> fuegos activos en el mapa. Hay un total de <strong>{{ countTotal: this.props.activefirestotal }}</strong> fuegos activos detectados en todo el mundo por la NASA.</Trans> <Trans>Datos actualizados <FromNow when={this.props.lastCheck} />.</Trans></Fragment> focusInput={false}
} label=""
</p> placeHolder="Escribe un lugar para centrar el mapa"
{isNotHomeAndMobile() && this.props.firealerts.length > 0 && helpText=""
<p className="firesmap-legend"><Trans parent="span" i18nKey="activeNeigFireInMapCount">En naranja, los fuegos notificados por nuestros usuarios/as recientemente.</Trans></p> } onChange={value => this.onAutocompleteChange(value)}
{isNotHomeAndMobile() && this.props.firealerts.length === 0 && />
<Checkbox inline={false} defaultChecked={this.state.showSubsUnion} onClick={e => this.setShowSubsUnion(e.target.checked)}>
<Trans className="mark-checkbox" parent="span">Resaltar en verde el área vigilada por nuestros usuarios/as</Trans>&nbsp;(*)
</Checkbox>
{(this.state.viewport.zoom >= MAXZOOM) &&
<Checkbox inline={false} defaultChecked={this.state.useMarkers} onClick={e => this.useMarkers(e.target.checked)}>
<Trans className="mark-checkbox" parent="span">Resaltar los fuegos con un marcador</Trans>
</Checkbox>}
</Fragment>}
</Col>
<Col xs={12} sm={6} md={6} lg={6} >
<p className="firesmap-legend">
{ (this.props.activefires.length + this.props.firealerts.length) === 0 ?
<Fragment><Trans parent="span" i18nKey="noActiveFireInMapCount">No hay fuegos activos en esta zona del mapa. <strong>{{ countTotal: this.props.activefirestotal }}</strong> fuegos activos en el mundo.</Trans> <FireStats loadingAll={loading} {... this.props} /></Fragment> :
<Fragment><Trans parent="span" i18nKey="activeFireInMapCount">En rojo, <strong>{{ count: this.props.activefiresunion.length + this.props.firealerts.length }}</strong> fuegos activos. <strong>{{ countTotal: this.props.activefirestotal }}</strong> fuegos activos en el mundo.</Trans> <FireStats loadingAll={loading} {... this.props} /></Fragment>
}
</p>
{isNotHomeAndMobile() && this.props.firealerts.length > 0 &&
<p className="firesmap-legend"><Trans parent="span" i18nKey="activeNeigFireInMapCount">En naranja, los fuegos notificados por nuestros usuarios/as recientemente.</Trans></p> }
{ /* disabled */ false && isNotHomeAndMobile() && this.props.firealerts.length === 0 && !isAnyMobile &&
<p className="firesmap-legend"><Trans parent="span" i18nKey="noActiveNeigFireInMap">No hay fuegos notificados recientemente por nuestros usuarios/as en esta zona.</Trans></p> } <p className="firesmap-legend"><Trans parent="span" i18nKey="noActiveNeigFireInMap">No hay fuegos notificados recientemente por nuestros usuarios/as en esta zona.</Trans></p> }
</Col>
<Col xs={12} sm={6} md={6} lg={6}> <p className="firesmap-note">
{isNotHomeAndMobile() && <em>{ this.state.viewport.zoom >= MAXZOOMREACTIVE ?
<Fragment> <Trans>Los fuegos activos se actualizan en tiempo cuasi real.</Trans> :
<LocationAutocomplete <Trans>Haga zoom en una zona de su interés si quiere que los fuegos se actualicen en tiempo real.</Trans>
focusInput={false} }
label="" </em>
placeHolder="Escribe un lugar para centrar el mapa" </p>
helpText="" </Col>
onChange={value => this.onAutocompleteChange(value)} </Row>
/> {loading ?
<Checkbox inline={false} defaultChecked={this.state.showSubsUnion} onClick={e => this.setShowSubsUnion(e.target.checked)}> <LoadingBar progress={this.props.loading ? 0.9 : 1} />
<Trans className="mark-checkbox" parent="span">Resaltar en verde el área vigilada por nuestros usuarios/as</Trans>&nbsp;(*) : ''}
</Checkbox>
{(this.state.viewport.zoom >= MAXZOOM) &&
<Checkbox inline={false} defaultChecked={this.state.useMarkers} onClick={e => this.useMarkers(e.target.checked)}>
<Trans className="mark-checkbox" parent="span">Resaltar los fuegos con un marcador</Trans>
</Checkbox>}
</Fragment>}
<p className="firesmap-note">
<em>{ this.state.viewport.zoom >= MAXZOOMREACTIVE ?
<Trans>Los fuegos activos se actualizan en tiempo real.</Trans> :
<Trans>Haga zoom en una zona de su interés si quiere que los fuegos se actualicen en tiempo real.</Trans>
}
</em>
</p>
</Col>
</Row>
{this.props.loading || !this.props.subsready ?
<LoadingBar progress={this.props.loading ? 0.9 : 1} />
: ''}
{/* https://github.com/CliffCloud/Leaflet.Sleep */} {/* https://github.com/CliffCloud/Leaflet.Sleep */}
<Map <Map
ref={(map) => { ref={(map) => {
@ -300,8 +294,17 @@ class FiresMap extends React.Component {
useMarkers={this.state.useMarkers} useMarkers={this.state.useMarkers}
nasa={false} nasa={false}
falsePositives falsePositives
neighbour={false}
industries={false} industries={false}
/> />
{ Meteor.isDevelopment &&
<FireListUnion
t={t}
history={this.props.history}
firesUnion={this.props.activefiresunion}
nasa
/>
}
<FireList <FireList
t={t} t={t}
history={this.props.history} history={this.props.history}
@ -309,6 +312,7 @@ class FiresMap extends React.Component {
scale={this.state.viewport.zoom >= MAXZOOM} scale={this.state.viewport.zoom >= MAXZOOM}
useMarkers={this.state.useMarkers} useMarkers={this.state.useMarkers}
nasa={false} nasa={false}
neighbour={false}
falsePositives={false} falsePositives={false}
industries industries
/> />
@ -319,6 +323,7 @@ class FiresMap extends React.Component {
scale={this.state.viewport.zoom >= MAXZOOM} scale={this.state.viewport.zoom >= MAXZOOM}
useMarkers={this.state.useMarkers} useMarkers={this.state.useMarkers}
nasa nasa
neighbour={false}
falsePositives={false} falsePositives={false}
industries={false} industries={false}
/> />
@ -326,9 +331,10 @@ class FiresMap extends React.Component {
t={t} t={t}
history={this.props.history} history={this.props.history}
fires={this.props.firealerts} fires={this.props.firealerts}
scale={false} scale={this.state.viewport.zoom >= MAXZOOM}
useMarkers={this.state.useMarkers} useMarkers={this.state.useMarkers}
nasa={false} nasa={false}
neighbour
falsePositives={false} falsePositives={false}
industries={false} industries={false}
/> />
@ -344,6 +350,7 @@ class FiresMap extends React.Component {
<Row> <Row>
<Col xs={12} sm={12} md={12} lg={12}> <Col xs={12} sm={12} md={12} lg={12}>
<p className="firesmap-footnote"><span style={{ paddingRight: '5px' }}>(*)</span><Trans i18nKey="mapPrivacy" parent="span"><em>Para preservar la privacidad de nuestros usuarios/as, los datos reflejados están aleatoriamente alterados y son solo orientativos.</em></Trans></p> <p className="firesmap-footnote"><span style={{ paddingRight: '5px' }}>(*)</span><Trans i18nKey="mapPrivacy" parent="span"><em>Para preservar la privacidad de nuestros usuarios/as, los datos reflejados están aleatoriamente alterados y son solo orientativos.</em></Trans></p>
<p className="firesmap-footnote"><em><Trans>Nota: Las nubes pueden entorpecer la detección de fuegos activos.</Trans></em></p>
</Col> </Col>
</Row> </Row>
{ !isHome() && { !isHome() &&
@ -360,11 +367,14 @@ FiresMap.propTypes = {
userSubs: PropTypes.string, userSubs: PropTypes.string,
userSubsBounds: PropTypes.string, userSubsBounds: PropTypes.string,
activefires: PropTypes.arrayOf(PropTypes.object).isRequired, activefires: PropTypes.arrayOf(PropTypes.object).isRequired,
activefiresunion: PropTypes.arrayOf(PropTypes.object).isRequired,
lastFireDetected: PropTypes.object,
firealerts: PropTypes.arrayOf(PropTypes.object).isRequired, firealerts: PropTypes.arrayOf(PropTypes.object).isRequired,
falsePositives: PropTypes.arrayOf(PropTypes.object).isRequired, falsePositives: PropTypes.arrayOf(PropTypes.object).isRequired,
industries: PropTypes.arrayOf(PropTypes.object).isRequired, industries: PropTypes.arrayOf(PropTypes.object).isRequired,
lastCheck: PropTypes.instanceOf(Date), lastCheck: PropTypes.instanceOf(Date),
activefirestotal: PropTypes.number.isRequired, activefirestotal: PropTypes.number.isRequired,
activefiresuniontotal: PropTypes.number.isRequired,
center: PropTypes.arrayOf(PropTypes.number), center: PropTypes.arrayOf(PropTypes.number),
zoom: PropTypes.number, zoom: PropTypes.number,
marks: PropTypes.bool.isRequired, marks: PropTypes.bool.isRequired,
@ -381,6 +391,7 @@ export default translate([], { wait: true })(withTracker(() => {
// console.log(`With industries: ${withIndustries}`); // console.log(`With industries: ${withIndustries}`);
let subscription; let subscription;
let subscriptionUnion;
let alertSubscription; let alertSubscription;
const centerStored = store.get('firesmap_center'); const centerStored = store.get('firesmap_center');
@ -395,6 +406,8 @@ export default translate([], { wait: true })(withTracker(() => {
if (typeof showUnionStored === 'boolean') { if (typeof showUnionStored === 'boolean') {
showUnion.set(showUnionStored); showUnion.set(showUnionStored);
} }
// Disable, because this increase the number of fires by one
// Meteor.subscribe('lastFireDetected');
Meteor.autorun(() => { Meteor.autorun(() => {
if ((centerStored !== [0, 0] || geolocation.get()) && geoInit) { if ((centerStored !== [0, 0] || geolocation.get()) && geoInit) {
center.set(centerStored || geolocation.get()); center.set(centerStored || geolocation.get());
@ -410,6 +423,14 @@ export default translate([], { wait: true })(withTracker(() => {
mapSize.get()[1].lat, mapSize.get()[1].lat,
marks.get() && zoom.get() >= MAXZOOM 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( alertSubscription = Meteor.subscribe(
'fireAlerts', 'fireAlerts',
mapSize.get()[0].lng, mapSize.get()[0].lng,
@ -418,18 +439,19 @@ export default translate([], { wait: true })(withTracker(() => {
mapSize.get()[1].lat mapSize.get()[1].lat
); );
/* if (withIndustries) { /* if (withIndustries) {
Meteor.subscribe( Meteor.subscribe(
'industriesMyloc', 'industriesMyloc',
mapSize.get()[0].lng, mapSize.get()[0].lng,
mapSize.get()[0].lat, mapSize.get()[0].lat,
mapSize.get()[1].lng, mapSize.get()[1].lng,
mapSize.get()[1].lat mapSize.get()[1].lat
); );
} */ } */
} }
}); });
Meteor.subscribe('activefirestotal'); Meteor.subscribe('activefirestotal');
Meteor.subscribe('activefiresuniontotal');
const settingsSubs = Meteor.subscribe('settings'); const settingsSubs = Meteor.subscribe('settings');
const lastCheck = SiteSettings.findOne({ name: 'last-fire-check' }); const lastCheck = SiteSettings.findOne({ name: 'last-fire-check' });
@ -438,19 +460,23 @@ export default translate([], { wait: true })(withTracker(() => {
const fireAlerts = FireAlertsCollection.find().fetch(); const fireAlerts = FireAlertsCollection.find().fetch();
const falsePositives = FalsePositivesCollection.find().fetch().map(falsePositivesRemap); const falsePositives = FalsePositivesCollection.find().fetch().map(falsePositivesRemap);
const industries = IndustriesCollection.find().fetch().map(industriesRemap); const industries = IndustriesCollection.find().fetch().map(industriesRemap);
const lastFireDetected = ActiveFiresCollection.findOne({}, { sort: { when: -1 } });
return { return {
loading: !subscription ? true : !(subscription.ready() && settingsSubs.ready() && alertSubscription.ready()), loading: Meteor.status().status !== 'connected' || !subscription ? true : !(subscription.ready() && settingsSubs.ready() && alertSubscription.ready() && settingsSubs.ready() && subscriptionUnion.ready()),
userSubs: userSubs ? userSubs.value : null, userSubs: userSubs ? userSubs.value : null,
userSubsBounds: userSubs ? userSubsBounds.value : null, userSubsBounds: userSubs ? userSubsBounds.value : null,
subsready: settingsSubs.ready(), subsready: settingsSubs.ready(),
// Not reactive query depending on zoom level // Not reactive query depending on zoom level
activefires: ActiveFiresCollection.find({}, { reactive: zoom.get() >= MAXZOOMREACTIVE }).fetch(), activefires: ActiveFiresCollection.find({}, { reactive: zoom.get() >= MAXZOOMREACTIVE }).fetch(),
activefiresunion: ActiveFiresUnion.find({}, { reactive: zoom.get() >= MAXZOOMREACTIVE }).fetch(),
activefirestotal: Counter.get('countActiveFires') + fireAlerts.length, activefirestotal: Counter.get('countActiveFires') + fireAlerts.length,
activefiresuniontotal: Counter.get('countActiveFiresUnion') + fireAlerts.length,
firealerts: fireAlerts, firealerts: fireAlerts,
falsePositives, falsePositives,
industries, industries,
lastCheck: lastCheck ? lastCheck.value : null, lastCheck: lastCheck ? lastCheck.value : null,
lastFireDetected,
center: center.get(), center: center.get(),
marks: marks.get(), marks: marks.get(),
showUnion: showUnion.get(), showUnion: showUnion.get(),

View file

@ -54,6 +54,13 @@
font-size: 16px; font-size: 16px;
} }
@include breakpoint(mobile) {
p.firesmap-note,
p.firesmap-legend {
margin-bottom: 10px;
}
}
.firesmap-note, .firesmap-note,
.firesmap-footnote { .firesmap-footnote {
font-size: 14px; font-size: 14px;

View file

@ -187,28 +187,72 @@
transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0);
} }
.carousel-item-1 { .no-webp .carousel-item-1 {
background-image: url('/home-slide1.jpg') background-image: url('/images/home-slide1.jpg')
} }
.carousel-item-2 { .no-webp .carousel-item-2.lazy {
background-image: url('/home-slide2.jpg') background-image: url('/images/home-slide2.jpg')
} }
.carousel-item-3 { .no-webp .carousel-item-3.lazy {
background-image: url('/home-slide3.jpg') background-image: url('/images/home-slide3.jpg')
} }
.carousel-item-4 { .no-webp .carousel-item-4.lazy {
background-image: url('/home-slide4.jpg') background-image: url('/images/home-slide4.jpg')
} }
.carousel-snd-item-1 { .no-webp .carousel-snd-item-1 {
background-image: url('/home-slide-snd-1.jpg') background-image: url('/images/home-slide-snd-1.jpg')
} }
.carousel-snd-item-2 { .no-webp .carousel-snd-item-2.lazy {
background-image: url('/home-slide-snd-2.jpg') background-image: url('/images/home-slide-snd-2.jpg')
}
.webp .carousel-item-1 {
background-image: url('/images/home-slide1.webp')
}
.webp .carousel-item-2.lazy {
background-image: url('/images/home-slide2.webp')
}
.webp .carousel-item-3.lazy {
background-image: url('/images/home-slide3.webp')
}
.webp .carousel-item-4.lazy {
background-image: url('/images/home-slide4.webp')
}
/* .webp .carousel-item-1 {
background-image: url('/images/home-slide1-blur.webp')
} */
.webp .carousel-item-2 {
background-image: url('/images/home-slide2-blur.webp')
}
.webp .carousel-item-3 {
background-image: url('/images/home-slide3-blur.webp')
}
.webp .carousel-item-4 {
background-image: url('/images/home-slide4-blur.webp')
}
.webp .carousel-snd-item-1 {
background-image: url('/images/home-slide-snd-1.webp')
}
.webp .carousel-snd-item-2 {
background-image: url('/images/home-slide-snd-2-blur.webp')
}
.webp .carousel-snd-item-2.lazy {
background-image: url('/images/home-slide-snd-2.webp')
} }
.participe-btn { .participe-btn {
@ -225,28 +269,52 @@
-moz-background-size: cover; -moz-background-size: cover;
background-size: cover; background-size: cover;
-o-background-size: cover; -o-background-size: cover;
background-image: url('/home-fires.jpg'); }
.no-webp .bg-image-full {
background-image: url('/images/home-fires.jpg');
}
.webp .bg-image-full {
background-image: url('/images/home-fires.webp');
} }
@media only screen and (max-width: 767px) { @media only screen and (max-width: 767px) {
.bg-image-full { .no-webp .bg-image-full {
background-image: url('/home-fires-800.jpg'); background-image: url('/images/home-fires-800.jpg');
}
.webp .bg-image-full {
background-image: url('/images/home-fires-800.webp');
} }
} }
@media only screen and (max-width: 400px) { @media only screen and (max-width: 400px) {
.bg-image-full { .no-webp .bg-image-full {
background-image: url('/home-fires-400.jpg'); background-image: url('/images/home-fires-400.jpg');
}
.webp .bg-image-full {
background-image: url('/images/home-fires-400.webp');
} }
} }
@media only screen and (max-width: 1000px) { @media only screen and (max-width: 1000px) {
.bg-image-full { .no-webp .bg-image-full {
background-image: url('/home-fires-1000.jpg'); } background-image: url('/images/home-fires-1000.jpg');
}
.webp .bg-image-full {
background-image: url('/images/home-fires-1000.webp');
}
} }
.device[data-device=iPhone6][data-orientation=portrait][data-color=white] { .nowebp .device[data-device=iPhone6][data-orientation=portrait][data-color=white] {
background-image: url(/mobile.png); background-image: url(/images/mobile.png);
}
.webp .device[data-device=iPhone6][data-orientation=portrait][data-color=white] {
background-image: url(/images/mobile.webp);
} }
.full-width .page-header { // section titles .full-width .page-header { // section titles

View file

@ -2,7 +2,8 @@
/* eslint-disable react/jsx-indent-props */ /* eslint-disable react/jsx-indent-props */
/* eslint-disable key-spacing */ /* eslint-disable key-spacing */
/* eslint-env jquery */ /* eslint-env jquery */
import React, { Component, Fragment } from 'react'; import 'simple-line-icons/css/simple-line-icons.css';
import React, { Component } from 'react';
import { translate, Trans } from 'react-i18next'; import { translate, Trans } from 'react-i18next';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@ -10,6 +11,7 @@ import PropTypes from 'prop-types';
// import { Link } from 'react-router-dom'; // import { Link } from 'react-router-dom';
// https://www.npmjs.com/package/react-resize-detector // https://www.npmjs.com/package/react-resize-detector
import ReactResizeDetector from 'react-resize-detector'; import ReactResizeDetector from 'react-resize-detector';
import MobileStoreButton from 'react-mobile-store-button';
import _ from 'lodash'; import _ from 'lodash';
import 'html5-device-mockups/dist/device-mockups.min.css'; import 'html5-device-mockups/dist/device-mockups.min.css';
import 'bootstrap-carousel-swipe/carousel-swipe'; import 'bootstrap-carousel-swipe/carousel-swipe';
@ -17,9 +19,8 @@ import { isAnyMobile } from '/imports/ui/components/Utils/isMobile';
import SubscriptionEditor from '/imports/ui/components/SubscriptionEditor/SubscriptionEditor'; import SubscriptionEditor from '/imports/ui/components/SubscriptionEditor/SubscriptionEditor';
import SubscriptionsMap from '/imports/ui/pages/Subscriptions/SubscriptionsMap'; import SubscriptionsMap from '/imports/ui/pages/Subscriptions/SubscriptionsMap';
import ShareIt from '/imports/ui/components/ShareIt/ShareIt'; import ShareIt from '/imports/ui/components/ShareIt/ShareIt';
import getFallbackLang from '/imports/modules/lang-fallback';
import FiresMap from '../FiresMap/FiresMap'; import FiresMap from '../FiresMap/FiresMap';
import 'simple-line-icons/css/simple-line-icons.css';
import './Index.scss'; import './Index.scss';
import './Index-custom.scss'; import './Index-custom.scss';
@ -34,11 +35,22 @@ class Index extends Component {
self.scaleHeader(); self.scaleHeader();
}, 250); }, 250);
this.onResize = this.onResize.bind(this); this.onResize = this.onResize.bind(this);
this.getBotUrl = this.getBotUrl.bind(this);
} }
componentDidMount() { componentDidMount() {
$('#carouselMainIndicators').carousel(); const car1 = $('#carouselMainIndicators');
$('#carouselSndIndicators').carousel(); const car2 = $('#carouselSndIndicators');
car1.carousel();
car2.carousel();
/* _.defer(() => {
$('#firsthomeslide').addClass('lazy');
}); */
const loadLazy = (ev) => {
ev.relatedTarget.classList.add('lazy');
};
car1.on('slide.bs.carousel', loadLazy);
car2.on('slide.bs.carousel', loadLazy);
} }
onResize() { onResize() {
@ -54,7 +66,15 @@ class Index extends Component {
</div> </div>
</section>); </section>);
} }
return (<Fragment />); return (<div />);
}
getBotUrl() {
const fallbackLng = getFallbackLang(this.props.i18n.language);
console.log(`language: ${this.props.i18n.language}, fallback: ${fallbackLng}`);
return fallbackLng === 'es' ?
'https://t.me/TodosContraElFuego_bot' :
'https://t.me/AllAgainstTheFire_bot';
} }
scaleHeader() { scaleHeader() {
@ -69,7 +89,7 @@ class Index extends Component {
} }
handleBtnClick() { handleBtnClick() {
window.open('https://t.me/TodosContraElFuego_bot', '_blank'); window.open(this.getBotUrl(), '_blank');
} }
gotoParticipe() { gotoParticipe() {
@ -82,13 +102,14 @@ class Index extends Component {
render() { render() {
const { t } = this.props; const { t } = this.props;
const title = `${t('AppName')}: ${t('Inicio')}`; const title = `${t('AppName')}: ${t('Inicio')}`;
const androidUrl = 'https://play.google.com/store/apps/details?id=org.comunes.fires';
return ( return (
<div className="IndexDisabled full-width"> <div className="IndexDisabled full-width">
<Helmet> <Helmet>
<title>{title}</title> <title>{title}</title>
</Helmet> </Helmet>
{/* https://v4-alpha.getbootstrap.com/components/carousel/ */} {/* https://v4-alpha.getbootstrap.com/components/carousel/ */}
<Fragment> <div>
<section className="sect1"> <section className="sect1">
<header> <header>
<ReactResizeDetector handleWidth handleHeight onResize={this.onResize} /> <ReactResizeDetector handleWidth handleHeight onResize={this.onResize} />
@ -99,14 +120,14 @@ class Index extends Component {
> >
{/* for dev ^^^: data-interval=false */} {/* for dev ^^^: data-interval=false */}
<ol className="carousel-indicators"> <ol className="carousel-indicators">
<li data-target="#carouselMainIndicators" data-slide-to="0" className="active" /> <li key="sl1-1" data-target="#carouselMainIndicators" data-slide-to="0" className="active" />
<li data-target="#carouselMainIndicators" data-slide-to="1" /> <li key="sl1-2" data-target="#carouselMainIndicators" data-slide-to="1" />
<li data-target="#carouselMainIndicators" data-slide-to="2" /> <li key="sl1-3" data-target="#carouselMainIndicators" data-slide-to="2" />
<li data-target="#carouselMainIndicators" data-slide-to="3" /> <li key="sl1-4" data-target="#carouselMainIndicators" data-slide-to="3" />
</ol> </ol>
<div className="carousel-inner" role="listbox"> <div className="carousel-inner" role="listbox">
<div className="carousel-item carousel-item-1 active"> <div id="firsthomeslide" className="carousel-item carousel-item-1 active">
<div <div
tabIndex={0} tabIndex={0}
role="button" role="button"
@ -161,6 +182,9 @@ class Index extends Component {
</div> </div>
<p className="moto"><Trans>Siempre alerta a los fuegos en nuestro vecindario</Trans></p> <p className="moto"><Trans>Siempre alerta a los fuegos en nuestro vecindario</Trans></p>
{/* <Link className="participe-btn btn btn-lg btn-warning" role="button" to="/#platforms">{this.props.t('Participa')}</Link> */} {/* <Link className="participe-btn btn btn-lg btn-warning" role="button" to="/#platforms">{this.props.t('Participa')}</Link> */}
<div className="android-btn">
<MobileStoreButton store="android" url={androidUrl} />
</div>
</div> </div>
</section> </section>
</section> </section>
@ -192,8 +216,8 @@ class Index extends Component {
> >
{/* for dev ^^^: data-interval=false */} {/* for dev ^^^: data-interval=false */}
<ol className="carousel-indicators"> <ol className="carousel-indicators">
<li data-target="#carouselSndIndicators" data-slide-to="0" className="active" /> <li key="sl2-1" data-target="#carouselSndIndicators" data-slide-to="0" className="active" />
<li data-target="#carouselSndIndicators" data-slide-to="1" /> <li key="sl2-2" data-target="#carouselSndIndicators" data-slide-to="1" />
</ol> </ol>
<div className="carousel-inner" role="listbox"> <div className="carousel-inner" role="listbox">
@ -250,7 +274,7 @@ class Index extends Component {
<div className="device" data-device="iPhone6" data-orientation="portrait" data-color="white"> <div className="device" data-device="iPhone6" data-orientation="portrait" data-color="white">
<div className="screen"> <div className="screen">
{/* Demo image for screen mockup, you can put an image here, some HTML, an animation, video, or anything else! */} {/* Demo image for screen mockup, you can put an image here, some HTML, an animation, video, or anything else! */}
<img src="/telegram-screen.png" className="img-fluid" alt="" /> <img src="/images/telegram-screen.png" className="img-fluid" alt="" />
</div> </div>
<div className="button" tabIndex="-1" onKeyDown={() => {}} role="button" onClick={() => this.handleBtnClick()} /> <div className="button" tabIndex="-1" onKeyDown={() => {}} role="button" onClick={() => this.handleBtnClick()} />
</div> </div>
@ -260,7 +284,7 @@ class Index extends Component {
<div className="container-fluid"> <div className="container-fluid">
<div className="row"> <div className="row">
<div className="col-lg-6"> <div className="col-lg-6">
<a className="feature-link" rel="noopener noreferrer" target="_blank" href="https://t.me/TodosContraElFuego_bot"> <a className="feature-link" rel="noopener noreferrer" target="_blank" href={this.getBotUrl()}>
<div className="feature-item"> <div className="feature-item">
<i className="fa fa-telegram text-primary" /> <i className="fa fa-telegram text-primary" />
<h3>Telegram</h3> <h3>Telegram</h3>
@ -300,7 +324,7 @@ class Index extends Component {
</div> </div>
</div> </div>
</section> </section>
</Fragment> </div>
</div> </div>
); );
} }
@ -308,6 +332,7 @@ class Index extends Component {
Index.propTypes = { Index.propTypes = {
history: PropTypes.object.isRequired, history: PropTypes.object.isRequired,
i18n: PropTypes.object.isRequired,
t: PropTypes.func.isRequired t: PropTypes.func.isRequired
}; };

View file

@ -77,3 +77,7 @@
} }
} }
} }
.android-btn > div {
height: 50px !important;
}

View file

@ -9,15 +9,17 @@ import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base'; import { Accounts } from 'meteor/accounts-base';
import { Bert } from 'meteor/themeteorchef:bert'; import { Bert } from 'meteor/themeteorchef:bert';
import { testId } from '/imports/ui/components/Utils/TestUtils'; 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 { translate } from 'react-i18next';
import { T9n } from 'meteor-accounts-t9n'; 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'; import './Profile.scss';
// List of languages: https://github.com/i18next/i18next/issues/1068
class Profile extends React.Component { class Profile extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -155,6 +157,7 @@ class Profile extends React.Component {
renderPasswordUser(loading, user) { renderPasswordUser(loading, user) {
const { t, i18n } = this.props; const { t, i18n } = this.props;
const enabledLangs = ['en', 'es', 'gl'];
const langName = { const langName = {
en: 'English', es: 'Español', gl: 'Galego', ast: 'Asturianu', ca: 'Català' en: 'English', es: 'Español', gl: 'Galego', ast: 'Asturianu', ca: 'Català'
}; };
@ -202,7 +205,7 @@ class Profile extends React.Component {
{langName[i18n.language]} {langName[i18n.language]}
</button> </button>
<div className="dropdown-menu"> <div className="dropdown-menu">
{i18n.languages.map(lang => ( {enabledLangs.map(lang => (
<button <button
className="dropdown-item" className="dropdown-item"
onClick={() => this.onLangSelect(lang)} onClick={() => this.onLangSelect(lang)}
@ -269,7 +272,7 @@ Profile.propTypes = {
i18n: PropTypes.object.isRequired i18n: PropTypes.object.isRequired
}; };
export default translate([], { wait: true })(withTracker(() => { export default translate()(withTracker(() => {
const subscription = Meteor.subscribe('users.editProfile'); const subscription = Meteor.subscribe('users.editProfile');
return { return {

View file

@ -2,7 +2,7 @@
/* eslint-disable react/jsx-indent-props */ /* eslint-disable react/jsx-indent-props */
/* eslint-disable react/jsx-indent */ /* eslint-disable react/jsx-indent */
import React, { Fragment } from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Button, ButtonGroup, Row, Col } from 'react-bootstrap'; import { Button, ButtonGroup, Row, Col } from 'react-bootstrap';
import { Meteor } from 'meteor/meteor'; import { Meteor } from 'meteor/meteor';
@ -63,8 +63,8 @@ class SubscriptionsMap extends React.Component {
handleLeafletLoad(map) { handleLeafletLoad(map) {
// console.log('Map loading'); // console.log('Map loading');
// console.log(map); if (map && this.props.userSubs !== 'null') {
if (map) { // console.log(`Union of ${this.props.userSubs}`);
this.state.union = subsUnion(this.state.union, { this.state.union = subsUnion(this.state.union, {
map, map,
subs: this.props.userSubs, subs: this.props.userSubs,
@ -95,7 +95,7 @@ class SubscriptionsMap extends React.Component {
const title = `${t('AppName')}: ${t('Zonas vigiladas')}`; const title = `${t('AppName')}: ${t('Zonas vigiladas')}`;
console.log(`Rendering Subs users ready ${this.props.subsready} viewport: ${JSON.stringify(this.state.viewport)}`); console.log(`Rendering Subs users ready ${this.props.subsready} viewport: ${JSON.stringify(this.state.viewport)}`);
return ( return (
<Fragment> <div>
{ !isHome() && { !isHome() &&
<Helmet> <Helmet>
<title>{title}</title> <title>{title}</title>
@ -152,7 +152,7 @@ class SubscriptionsMap extends React.Component {
{ !isHome() && { !isHome() &&
<ShareIt title={title} /> <ShareIt title={title} />
} }
</Fragment> </div>
); );
} }
} }

View file

@ -3,20 +3,16 @@
/* eslint-disable import/no-absolute-path */ /* eslint-disable import/no-absolute-path */
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { withTracker } from 'meteor/react-meteor-data';
class TestError extends Component { class TestError extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.t = props.t;
this.state = { this.state = {
}; };
throw new Error('Just testing');
} }
render() { render() {
throw new Error('Just testing');
return ( return (
<div /> <div />
); );
@ -24,13 +20,9 @@ class TestError extends Component {
} }
TestError.propTypes = { TestError.propTypes = {
t: PropTypes.func.isRequired
}; };
TestError.defaultProps = { TestError.defaultProps = {
}; };
// export default translate([], { wait: true })(TestError); export default TestError;
export default translate([], { wait: true })(withTracker(props => ({
// props.something
}))(TestError));

View file

@ -1,26 +1,28 @@
/* eslint-disable react/jsx-indent-props */
/* eslint-disable import/no-absolute-path */ /* eslint-disable import/no-absolute-path */
import React, { Fragment } from 'react'; /* eslint-disable import/no-absolute-path */
import PropTypes from 'prop-types';
import { translate } from 'react-i18next'; import React, { Component } from 'react';
import SubscriptionsMap from '/imports/ui/pages/Subscriptions/SubscriptionsMap'; import SubscriptionsMap from '/imports/ui/pages/Subscriptions/SubscriptionsMap';
class ZonesMap extends React.Component { export default class ZonesMap extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.t = props.t; this.state = {
};
} }
render() { render() {
return ( return (
<Fragment> <div>
<SubscriptionsMap {...this.props} /> <SubscriptionsMap {...this.props} />
</Fragment> </div>
); );
} }
} }
ZonesMap.propTypes = { ZonesMap.propTypes = {
history: PropTypes.object.isRequired
}; };
export default translate([], { wait: true })(ZonesMap); ZonesMap.defaultProps = {
};

Some files were not shown because too many files have changed in this diff Show more