Compare commits

...

346 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
vjrj
f17fd13c2a Propagate locate state in signup 2018-03-02 10:39:14 +01:00
vjrj
ea8f6fbf5b Disable btn only in authenticated 2018-03-02 10:38:45 +01:00
vjrj
ac33bfa158 Space in center btn 2018-03-02 10:38:04 +01:00
vjrj
8deaafb96b Better error return in subscriptions 2018-03-02 10:08:52 +01:00
vjrj
9062012d18 Unsuscribe translation 2018-03-02 10:08:13 +01:00
vjrj
3ce7d65bb8 Disable btn on action in Subscriptions 2018-03-02 09:22:55 +01:00
vjrj
ceebb5d5ad Improved subs messages 2018-03-02 07:36:54 +01:00
vjrj
91c67f0b5f Don't fit subsunion when adding 2018-03-02 07:22:19 +01:00
vjrj
e703057377 Dup subscriptions not allowed 2018-03-01 17:10:31 +01:00
vjrj
621cf4e2ad Improved autocomplete error handling 2018-03-01 16:47:37 +01:00
vjrj
a83495deea Minor change in error reporting 2018-03-01 16:47:17 +01:00
vjrj
e972419b3e Added link to About 2018-03-01 11:04:35 +01:00
vjrj
ae2695ea1f i18n subs limit 2018-02-28 11:26:00 +01:00
vjrj
b546106066 Warning big zones subscriptions 2018-02-28 11:22:01 +01:00
vjrj
7d5fe8f0ee isHomeAndMobile reactive 2018-02-28 01:52:10 +01:00
vjrj
58c557ecb2 Reconnect only for anon users 2018-02-28 01:36:34 +01:00
vjrj
2bad9d4083 New geolocation error 2018-02-27 19:32:43 +01:00
vjrj
0342bdf33f Removing logs 2018-02-27 18:53:01 +01:00
vjrj
c2270a860e Fix error message in geolocation 2018-02-27 18:50:38 +01:00
vjrj
5d216d81d4 Location search in Fire Map 2018-02-27 17:46:00 +01:00
vjrj
d106fff245 Refactor autocomplete 2018-02-27 17:00:37 +01:00
vjrj
a3bdcb3982 Added more industries 2018-02-27 16:50:26 +01:00
vjrj
dc92574bc3 Full screen on maps 2018-02-27 11:37:54 +01:00
vjrj
b5620cea46 Added progress bar to fires map 2018-02-27 11:08:55 +01:00
vjrj
73588a67d2 Limit sitemap list of fires 2018-02-27 09:13:53 +01:00
vjrj
f5d43dba76 Removed duplicated code 2018-02-26 20:30:46 +01:00
vjrj
2fe2412e73 Some comments 2018-02-26 20:30:23 +01:00
vjrj
1cf2a6883a Fix some file fallback retrieval for some langs. Added tests 2018-02-26 20:11:41 +01:00
vjrj
1df41f1e5a Industry message changed 2018-02-26 16:36:52 +01:00
vjrj
ccbfe642a8 Added industries collection 2018-02-26 12:11:23 +01:00
vjrj
1ad6bf17cb Def scan for neighbors alerts 2018-02-23 17:50:38 +01:00
vjrj
a8dc8a6b58 More accurate firemap legend 2018-02-23 17:11:45 +01:00
vjrj
5e9e6896c0 Fire icon sizes 2018-02-23 17:02:54 +01:00
vjrj
28c0d091a9
Merge pull request #2 from SergioEstan/patch-1
Update about-en.md
2018-02-22 22:36:28 +01:00
SergioEstan
c90ed5f1d8
Update about-en.md 2018-02-22 22:17:23 +01:00
vjrj
1e9e870df8 Minor fixes in about 2018-02-22 11:23:18 +01:00
vjrj
ae3b9e5faf About page 2018-02-22 10:49:15 +01:00
vjrj
a296fc6051 Change feedback email 2018-02-21 20:50:44 +01:00
vjrj
ed16fe69b5 Increasing timeout in test 2018-02-21 20:25:40 +01:00
vjrj
f46aabb4d3 Added feedback 2018-02-21 15:29:25 +01:00
vjrj
d865ac8e06 Better zoom of fire pages 2018-02-20 20:50:59 +01:00
vjrj
8b1d7f7602 Better zoom of fire pages 2018-02-20 20:49:23 +01:00
vjrj
7ce9317876 Maps colors improved 2018-02-20 13:39:05 +01:00
vjrj
7df050a2b1 Use gmaps in fire page for better false positive detection 2018-02-20 09:44:29 +01:00
vjrj
0a7f6b8e40 Use fa-icons 2018-02-20 09:44:03 +01:00
vjrj
84ab9f9cdc Move simple icon where used 2018-02-20 09:29:24 +01:00
vjrj
e980bbdb24 Less clicks to show fire info 2018-02-20 09:11:07 +01:00
vjrj
4d52b6c8e0 Fire page now shows also false positives 2018-02-20 09:10:44 +01:00
vjrj
f0bf995584 Fire page now shows also false positives 2018-02-20 07:55:26 +01:00
vjrj
c962d12374 Better notifications 2018-02-19 21:20:34 +01:00
vjrj
34e0132c46 Doble check state in subscriptions 2018-02-19 21:20:20 +01:00
vjrj
8af9bec79c Trying to fix fonts issue 'Failed to decode downloaded' because of wrong path 2018-02-19 16:31:05 +01:00
vjrj
8b26accb78 Minor changes in individual Fire map style/usability 2018-02-19 16:12:09 +01:00
vjrj
d65ae4693c
Merge pull request #1 from rapto/tcef-master
errata en radio
2018-02-19 16:06:36 +01:00
marcos
4b9d742b9b errata en radio 2018-02-19 16:02:49 +01:00
vjrj
151c792fc6 Disabled feedback in production 2018-02-19 09:40:40 +01:00
vjrj
719ea435d4 Feedback not in home and email for non auth users 2018-02-18 20:21:26 +01:00
vjrj
458627f520 Fix reconnect translation issues 2018-02-18 18:43:40 +01:00
vjrj
2c53428f8d Feedback only for auth users 2018-02-18 17:37:39 +01:00
vjrj
9115063cdd Update carousel swipe 2018-02-18 17:34:13 +01:00
vjrj
169a955b0b Simple font update 2018-02-18 17:33:46 +01:00
vjrj
ba32c4fd0d Feedback button 2018-02-18 17:00:54 +01:00
vjrj
5c7380a199 Fix error message 2018-02-18 13:11:40 +01:00
vjrj
308ad08e74 Tooltip in industries 2018-02-17 09:41:21 +01:00
vjrj
79222eb978 Removed loading in FireMap. Improve data retrieving 2018-02-17 09:30:44 +01:00
vjrj
f5d105fbc0 Remove log 2018-02-17 08:52:50 +01:00
vjrj
53ab69555e Don't query again if zoom in 2018-02-17 08:48:04 +01:00
vjrj
237269f761 Fix some FireMaps data retrieving 2018-02-17 08:43:01 +01:00
vjrj
824f66df03 import L 2018-02-17 08:26:46 +01:00
vjrj
2a6d8bea78 False positives only for active fires 2018-02-16 20:40:41 +01:00
vjrj
eee1abf3ed Fix some minor bug in SubsMap 2018-02-16 20:40:17 +01:00
vjrj
ee427758c2 i18n whitelist langs 2018-02-16 17:07:18 +01:00
vjrj
79eed373d4 Make subs union no clickable (interactive) and wait for fire alerts 2018-02-16 16:46:06 +01:00
vjrj
c35349670a Fix test timming 2018-02-16 16:45:07 +01:00
vjrj
45a63548c7 Added raven/sentry to server side 2018-02-16 13:12:06 +01:00
vjrj
44428be8aa Comments config 2018-02-15 22:35:23 +01:00
vjrj
2477c985d6 Added popup to alerts 2018-02-15 18:10:38 +01:00
vjrj
aec4c2a0b0 Added meta hreflang 2018-02-15 17:13:54 +01:00
vjrj
79bdc18934 Migrated to prerender node-module 2018-02-15 16:53:12 +01:00
vjrj
dbfbba1067 Added unstranslated cookie message 2018-02-15 13:27:09 +01:00
vjrj
4f9e0cc63d Added prerender 2018-02-15 12:48:33 +01:00
vjrj
ac4331edbd Subs union to server side 2018-02-14 18:21:50 +01:00
vjrj
ed7c89476f Disable sitesettings methods 2018-02-14 11:26:14 +01:00
vjrj
9ebbe5482a SiteSettings isPublic 2018-02-14 11:22:11 +01:00
vjrj
078931b4f9 Translation link 2018-02-14 10:31:19 +01:00
vjrj
8809a3e3c8 Some work with spiderable 2018-02-14 10:26:22 +01:00
vjrj
fc4155ea49 Prevent QT error in phantom 2018-02-14 09:57:37 +01:00
vjrj
b5e9e301b9 Test docs 2018-02-14 09:57:06 +01:00
vjrj
3fc0fca23b Added untranslated message 2018-02-13 10:43:41 +01:00
vjrj
7d8034e600 Merge lang en 2018-02-13 09:58:02 +01:00
vjrj
0bb1df3584 Translated using Weblate (English)
Currently translated at 100.0% (187 of 187 strings)
2018-02-13 08:49:35 +00:00
vjrj
26d668b68c Send missing in dev 2018-02-13 09:39:23 +01:00
vjrj
9e4815d8b8 Link to tranlator 2018-02-13 09:38:58 +01:00
vjrj
add5dedcb1 Migrating users without lang 2018-02-13 09:24:56 +01:00
vjrj
dfed8d3a74 Nav reorder 2018-02-13 09:24:47 +01:00
vjrj
981e8e798e FiresMap store checkbox and improved performance 2018-02-13 01:44:15 +01:00
vjrj
bc4201ffe6 Styling in error message on mobile 2018-02-12 20:00:45 +01:00
vjrj
a7f9e51b5f Truncate subs union 2018-02-12 19:56:46 +01:00
vjrj
08b3289f4d Lang for oauth users 2018-02-12 19:52:22 +01:00
vjrj
47b55267df Truncate subs union 2018-02-12 17:23:01 +01:00
vjrj
5f329f5210 Logs removed 2018-02-12 17:22:46 +01:00
vjrj
8e924d1de4 Trying to catch subs union error 2018-02-12 16:59:03 +01:00
vjrj
8a6584acb9 Fix for participe btn 2018-02-12 14:57:13 +01:00
vjrj
bbe9b1b40e Improved tests 2018-02-12 13:07:23 +01:00
vjrj
fde04bb77f FiresMap issue with init 2018-02-12 13:07:09 +01:00
vjrj
b6963ff290 Logout typo 2018-02-12 13:06:18 +01:00
vjrj
966b2d56b9 Save missing fallback 2018-02-12 13:06:06 +01:00
vjrj
cd80d72e32 Added share it 2018-02-12 12:18:50 +01:00
vjrj
28d8f695c1 Added zones map nav link 2018-02-12 12:18:40 +01:00
vjrj
ac0fa6cebd Refactor 2018-02-12 12:17:24 +01:00
vjrj
31e8a7dfdd Minor footer style fix 2018-02-12 12:17:08 +01:00
vjrj
e46ae3a000 Added zones nav link 2018-02-12 09:49:13 +01:00
vjrj
6a9f3e4df6 English typo 2018-02-11 19:22:34 +01:00
vjrj
97eb8c91f5 Upgrade leaflet 2018-02-09 11:21:14 +01:00
vjrj
346016b5c4 Added sitemap 2018-02-09 11:02:21 +01:00
vjrj
c3bc4e604a Minor fix in firesmap for race condition 2018-02-08 20:41:24 +01:00
vjrj
35ba215834 Store center/zoom in fire map 2018-02-08 17:44:42 +01:00
vjrj
735dd55531 Fire industries marks 2018-02-08 15:54:47 +01:00
vjrj
eca58df6c2 Fire popupsand active/archive urls 2018-02-08 11:15:23 +01:00
vjrj
6870de4c7b Style and text minor changes in home 2018-02-08 00:00:53 +01:00
vjrj
5dd6a477c0 Minor css changes in home 2018-02-07 19:31:14 +01:00
vjrj
be10650150 New home slides 2018-02-07 18:26:09 +01:00
vjrj
311b4ee1f3 Removed createContainer 2018-02-07 16:37:26 +01:00
vjrj
86a94e37e4 Improvement of styles in Nav and Footer 2018-02-07 16:36:47 +01:00
vjrj
2e3430fa85 Basic title 2018-02-07 16:24:36 +01:00
vjrj
d4b7f8e9f8 Update npm dep 2018-02-07 15:39:32 +01:00
vjrj
464af6b9d1 More work with titles & descriptions 2018-02-07 15:27:12 +01:00
vjrj
a1138c8c43 More work with titles & descriptions 2018-02-07 10:00:31 +01:00
vjrj
323c77322b Upgraded npm-mongo 2018-02-02 17:05:10 +01:00
vjrj
edfb4e250c Urls in tos 2018-02-02 16:41:25 +01:00
vjrj
236d06308d Added more cucumber tests (wip) 2018-02-01 17:45:04 +01:00
vjrj
fb3511b264 Added cucumber tests (wip) 2018-02-01 17:00:30 +01:00
238 changed files with 17747 additions and 2631 deletions

8
.gitignore vendored
View file

@ -9,3 +9,11 @@ npm-debug.log
.deploy/
public/locales/undefined/
.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,
# 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
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
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-js@2.2.0 # JS minifier run for production mode
es5-shim@4.6.15 # ECMAScript 5 compatibility for older browsers.
ecmascript@0.9.0 # Enable ECMAScript2015+ syntax in app code
shell-server@0.3.0 # Server-side component of the `meteor shell` command
standard-minifier-css@1.4.0 # CSS minifier run for production mode
standard-minifier-js@2.3.1 # JS minifier run for production mode
es5-shim@4.7.0 # ECMAScript 5 compatibility for older browsers.
ecmascript@0.10.6 # Enable ECMAScript2015+ syntax in app code
shell-server@0.3.1 # Server-side component of the `meteor shell` command
react-meteor-data
alanning:roles
fourseven:scss
accounts-base@1.4.0
accounts-base@1.4.2
accounts-password@1.5.0
service-configuration@1.0.11
accounts-facebook@1.3.0
accounts-github@1.4.0
accounts-google@1.3.0
accounts-facebook@1.3.1
accounts-github@1.4.1
accounts-google@1.3.1
themeteorchef:bert
fortawesome:fontawesome
aldeed:collection2-core@2.0.1
audit-argument-checks@1.0.7
ddp-rate-limiter@1.0.7
dynamic-import@0.2.0
dynamic-import@0.3.0
static-html
alexwine:bootstrap-4
@ -45,7 +45,6 @@ natestrauser:publish-performant-counts
maximum:server-transform
mys:fonts # fonst in npm
ostrio:mailer # mailer
mizzao:user-status # user status (online, etc)
# babrahams:constellation # dev utility
percolate:migrations # db migrations
ostrio:meteor-root
@ -54,10 +53,17 @@ practicalmeteor:chai
aldeed:schema-deny
aldeed:template-extension
dburles:collection-helpers
less
markdown
mongo-livedata
less@2.7.11
markdown@1.0.12
mongo-livedata@1.0.12
reywood:publish-composite
barbatus:stars-rating
arkham:comments-ui
facts
facts@1.0.9
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,102 +1,110 @@
255kb:meteor-status@1.5.0
accounts-base@1.4.0
accounts-facebook@1.3.0
accounts-github@1.4.0
accounts-google@1.3.0
accounts-base@1.4.2
accounts-facebook@1.3.1
accounts-github@1.4.1
accounts-google@1.3.1
accounts-oauth@1.1.15
accounts-password@1.5.0
accounts-password@1.5.1
alanning:roles@1.2.16
aldeed:collection2-core@2.0.1
aldeed:collection2-core@2.1.2
aldeed:schema-deny@2.0.1
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
arkham:comments-ui@1.4.4
audit-argument-checks@1.0.7
autoupdate@1.3.12
babel-compiler@6.24.7
babel-runtime@1.1.1
autoupdate@1.4.0
babel-compiler@7.0.7
babel-runtime@1.2.2
barbatus:stars-rating@1.1.1
base64@1.0.10
base64@1.0.11
binary-heap@1.0.10
blaze@2.3.2
blaze-tools@1.0.10
boilerplate-generator@1.3.0
caching-compiler@1.1.9
boilerplate-generator@1.4.0
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
callback-hook@1.0.10
check@1.2.5
callback-hook@1.1.0
check@1.3.1
coffeescript@1.0.17
dburles:collection-helpers@1.1.0
dburles:mongo-collection-instances@0.3.5
ddp@1.4.0
ddp-client@2.2.0
ddp-common@1.3.0
ddp-client@2.3.2
ddp-common@1.4.0
ddp-rate-limiter@1.0.7
ddp-server@2.1.0
ddp-server@2.1.2
deps@1.0.12
diff-sequence@1.0.7
dynamic-import@0.2.0
ecmascript@0.9.0
diff-sequence@1.1.0
dynamic-import@0.3.0
ecmascript@0.10.7
ecmascript-runtime@0.5.0
ecmascript-runtime-client@0.5.0
ecmascript-runtime-client@0.6.2
ecmascript-runtime-server@0.5.0
ejson@1.1.0
email@1.2.3
es5-shim@4.6.15
es5-shim@4.7.3
facebook-oauth@1.4.0
facts@1.0.9
flowkey:raven@1.0.0
flowkey:raven@1.1.0
fortawesome:fontawesome@4.7.0
fourseven:scss@4.5.4
gadicc:blaze-react-component@1.4.0
gadicohen:robots-txt@0.0.10
gadicohen:sitemaps@0.0.26
geojson-utils@1.0.10
github-oauth@1.2.0
google-oauth@1.2.4
google-oauth@1.2.5
hot-code-push@1.0.4
html-tools@1.0.11
htmljs@1.0.11
http@1.3.0
id-map@1.0.9
jquery@1.11.10
http@1.4.0
id-map@1.1.0
jquery@1.11.11
lai:collection-extensions@0.2.1_1
launch-screen@1.1.1
less@2.7.11
less@2.7.12
livedata@1.0.18
localstorage@1.2.0
logging@1.1.19
logging@1.1.20
markdown@1.0.12
maximum:package-base@1.1.2
maximum:server-transform@0.5.0
mdg:geolocation@1.3.0
meteor@1.8.0
meteor-base@1.2.0
meteortesting:browser-tests@0.1.2
meteortesting:mocha@0.5.0
minifier-css@1.2.16
minifier-js@2.2.1
minimongo@1.4.0
mizzao:timesync@0.3.4
mizzao:user-status@0.6.7
meteor@1.8.6
meteor-base@1.3.0
meteorhacks:inject-initial@1.0.4
meteorhacks:zones@1.6.0
meteortesting:browser-tests@0.2.0
meteortesting:mocha@0.5.1
minifier-css@1.3.1
minifier-js@2.3.4
minimongo@1.4.4
mobile-experience@1.0.5
mobile-status-bar@1.0.14
modules@0.11.0
modules-runtime@0.9.0
mongo@1.3.0
modules@0.11.6
modules-runtime@0.9.2
mongo@1.4.7
mongo-dev-server@1.1.0
mongo-id@1.0.6
mongo-id@1.0.7
mongo-livedata@1.0.12
mys:fonts@0.0.2
natestrauser:publish-performant-counts@0.1.2
nimble:restivus@0.8.12
npm-bcrypt@0.9.3
npm-mongo@2.2.33
oauth@1.2.0
npm-mongo@2.2.34
nspangler:autoreconnect@0.0.1
oauth@1.2.3
oauth2@1.2.0
observe-sequence@1.0.16
ordered-dict@1.0.9
ordered-dict@1.1.0
ostrio:cookies@2.1.3
ostrio:mailer@2.1.0
ostrio:meteor-root@1.0.6
ostrio:meteor-root@1.0.7
peerlibrary:assert@0.2.5
peerlibrary:fiber-utils@0.6.0
peerlibrary:reactive-mongo@0.1.1
@ -104,37 +112,44 @@ peerlibrary:server-autorun@0.5.2
percolate:migrations@1.0.2
practicalmeteor:chai@2.1.0_1
practicalmeteor:mocha-core@1.0.1
promise@0.10.0
promise@0.10.2
raix:eventemitter@0.1.3
random@1.0.10
rate-limit@1.0.8
react-meteor-data@0.2.15
random@1.1.0
rate-limit@1.0.9
react-meteor-data@0.2.16
reactive-dict@1.2.0
reactive-var@1.0.11
reload@1.1.11
retry@1.0.9
reywood:publish-composite@1.5.2
routepolicy@1.0.12
reload@1.2.0
retry@1.1.0
reywood:publish-composite@1.6.0
routepolicy@1.0.13
saucecode:timezoned-synced-cron@1.2.11
selaias:cookie-consent@0.4.0
server-render@0.3.0
service-configuration@1.0.11
session@1.1.7
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-compiler@1.1.3
srp@1.0.10
standard-minifier-css@1.3.5
standard-minifier-js@2.2.1
standard-minifier-css@1.4.1
standard-minifier-js@2.3.3
static-html@1.2.2
templating@1.3.2
templating-compiler@1.3.3
templating-runtime@1.3.2
templating-tools@1.1.2
themeteorchef:bert@2.1.3
tmeasday:check-npm-versions@0.3.1
tmeasday:check-npm-versions@0.3.2
tracker@1.1.3
ui@1.0.13
underscore@1.0.10
url@1.1.0
url@1.2.0
vjrj:piwik@0.3.1
webapp@1.4.0
webapp@1.5.0
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)
[![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.
## 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
@ -20,9 +29,23 @@ More platforms and services in the future...
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
node_modules/.bin/chimp --watch --ddp=http://localhost:3000 --path=cucumber
# and
node_modules/.bin/chimp --ddp=http://localhost:3000 --path=cucumber
```
### FAQ & Troubleshooting
**Q** - I get something like `(...) /node_modules/fibers/future.js:280 (...) Error: Could not locate the bindings file.` What can I do?
**A** - Try `meteor npm rebuild`
## Data source acknowledgements
*We acknowledge the use of data and imagery from LANCE FIRMS operated by the NASA/GSFC/Earth Science Data and Information System (ESDIS) with funding provided by NASA/HQ*.
@ -33,4 +56,5 @@ GNU APLv3. See our [LICENSE](https://github.com/comunes/todos-contra-el-fuego-we
## Thanks & other acknowlegments
(...)
Thanks indeed to:
- Lupe Bao Reixa for Galician translation

16
bin/au-dee-npi.awk Executable file
View file

@ -0,0 +1,16 @@
# cat file.csv| ./au-dee-npi.awk | mongoimport -d "fuegos" -c "industries"
gawk -F',' 'NR>1 { \
gsub(/"/,"",$5);
gsub(/"/,"",$6);
gsub(/\t/,"",$13);
gsub(/\t/,"",$14);
gsub(/\x1a/,"",$13);
gsub(/\x1a/,"",$14);
lon = $14+0;
lat = $13+0;
name= $5", "$6
gsub(/^, /,"",name);
if (lon != 0 && lat != 0 && lon > -180 && lon < 180 && lat >= -90 && lat <= 90)
printf("{name:\"%s\",geo:{type:\"Point\",coordinates:[%s,%s]},registry:\"4\"}\n" \
,name,lon,lat)}'

19
bin/ca-godp-npri.awk Executable file
View file

@ -0,0 +1,19 @@
# cat file.csv| ./ca-godp-npri.awk | uniq | mongoimport -d "fuegos" -c "industries"
gawk -F',' 'NR>1 { \
gsub(/"/,"",$3);
gsub(/"/,"",$4);
gsub(/\t/,"",$3);
gsub(/\t/,"",$4);
gsub(/\x1a/,"",$3);
gsub(/\x1a/,"",$4);
lon = $16+0;
lat = $15+0;
if ($3 != $4)
name= $3", "$4
else
name=$3
gsub(/^, /,"",name);
# if (lon != 0 && lat != 0 && lon > -180 && lon < 180 && lat >= -90 && lat <= 90)
printf("{name:\"%s\",geo:{type:\"Point\",coordinates:[%s,%s]},registry:\"3\"}\n" \
,name,lon,lat)}'

14
bin/eu-eea-e-prtr.awk Executable file
View file

@ -0,0 +1,14 @@
# cat dbo.PUBLISH_FACILITYREPORT.csv| ./eu-eea-e-prtr.awk | mongoimport -d "fuegos" -c "industries"
gawk -F',' 'NR>1 { \
gsub(/"/,"",$6);
gsub(/"/,"",$5);
gsub(/\t/,"",$6);
gsub(/\t/,"",$5);
gsub(/\x1a/,"",$5);
gsub(/\x1a/,"",$6);
lon = $14+0;
lat = $13+0;
if (lon != 0 && lat != 0 && lon > -180 && lon < 180 && lat >= -90 && lat <= 90)
printf("{name:\"%s, %s\",geo:{type:\"Point\",coordinates:[%s,%s]},registry:\"1\"}\n" \
,$6,$5,lon,lat)}'

10
bin/us-epa-frs.awk Executable file
View file

@ -0,0 +1,10 @@
# cat NATIONAL_SINGLE.CSV| ./us-epa-frs.awk | mongoimport -d "fuegos" -c "industries"
gawk -F',' 'NR>1{ \
gsub(/"/,"",$33);
gsub(/"/,"",$32);
gsub(/"/,"",$3);
gsub(/\t/,"",$3);
lon = $33+0;
lat = $32+0;
if (lon != 0 && lat != 0 && lon > -180 && lon < 180 && lat >= -90 && lat <= 90) { printf("{name:\"%s\",geo:{type:\"Point\",coordinates:[%s,%s]},registry:\"2\"}\n",name,lon,lat)}}'

View file

@ -1,25 +1,15 @@
<head>
<meta charset="utf-8">
<meta name="fragment" content="!">
<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="viewport" content="initial-scale=1, minimal-ui, maximum-scale=1, minimum-scale=1" />
<!-- FIXME integrate this better with meteor
https://fezvrasta.github.io/bootstrap-material-design/docs/4.0/getting-started/introduction/ -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons">
<link href="https://fonts.googleapis.com/css?family=Lato" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Catamaran:100,200,300,400,500,600,700,800,900" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Muli" rel="stylesheet">
<!-- <link rel="stylesheet" href="https://unpkg.com/bootstrap-material-design@4.0.0-beta.4/dist/css/bootstrap-material-design.min.css" integrity="sha384-R80DC0KVBO4GSTw+wZ5x2zn2pu4POSErBkf8/fSFhPXHxvHJydT0CSgAP2Yo2r4I" crossorigin="anonymous"> -->
<!-- <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://unpkg.com/popper.js@1.12.6/dist/umd/popper.js" integrity="sha384-fA23ZRQ3G/J53mElWqVJEGJzU0sTs+SvzG8fXVWP+kJQ1lwFAOkcUOysnlKJC33U" crossorigin="anonymous"></script> -->
<!-- <script src="https://unpkg.com/bootstrap-material-design@4.0.0-beta.4/dist/js/bootstrap-material-design.js" integrity="sha384-3xciOSDAlaXneEmyOo0ME/2grfpqzhhTcM4cE32Ce9+8DW/04AGoTACzQpphYGYe" crossorigin="anonymous"></script>
<script>$(document).ready(function() { $('body').bootstrapMaterialDesign(); });</script> -->
<!-- End:FIXME -->
<!-- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/daemonite-material@4.0.0-beta/css/material.min.css">
<script src="https://cdn.jsdelivr.net/npm/daemonite-material@4.0.0-beta/js/material.min.js"></script> -->
<link rel="shortcut icon" type="image/png" href="/favicon.ico?v4" sizes="16x16 32x32 64x64">
<link rel="apple-touch-icon" sizes="120x120" href="/apple-touch-icon-precomposed.png?v1">
</head>

View file

@ -1,3 +1,4 @@
import '../imports/startup/client';
/* import '../node_modules/daemonite-material/js/material.min.js';
import '../node_modules/daemonite-material/css/material.css'; */
// 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

@ -0,0 +1,22 @@
Feature: Allow users to give feedback
As a user (authenticated or not)
I want to give feedback about this site
Scenario: A user can login and give feedback
Given I am on the zones page
And I have an account and I logged in
# Because is not verified
Then I can send feedback with email
When I sign out
Given I'm not logged
Then I can send feedback with email
Scenario: A anon user can give feedback
Given I am on the fires page
Given I'm not logged
Then I can send feedback with email
Scenario: A user cannot give feedback in home
Given I am on the home page
Then I cannot send feedback

View file

@ -0,0 +1,12 @@
Feature: This app should generate a correct sitemap, etc
Scenario: I verify that a sitemap is generated correctly
Given the page sitemap.xml
Then I check that exist this list of pages in the sitemap
| license | License |
| terms | Terms of Service |
| credits | Credits |
| privacy | Privacy Policy |
| fires | Active Fires |
| login | Login |
| signup | Signup |

View file

@ -0,0 +1,34 @@
Feature: Allow users to login and logout
As a user
I want to login and logout
So that I can prove my identity and see personalized data
Background:
Given I am on the home page
Scenario: A user can login
Given I have an account and I logged in
When I sign out
When I enter my email and password
Then I should be logged in
And I can edit my profile
Scenario: A user cannot login with bad information
Given I have an account and I logged in
When I sign out
When I enter incorrect authentication information
Then I should see a user not found error
Scenario: A user can register and login
Given I register with some name, password and email
Then I should be registered
And I should be logged in
When I sign out
When I enter my email and password
Then I should be logged in
And I can edit my profile
Scenario: A user cannot register without accepting service conditions
Given I register with some name, password and email but without accept conditions
Then I shouldn't be registered

View file

@ -0,0 +1,51 @@
Feature: Test all secundary pages
Background:
Given I am on the home page
Scenario: Check that all secondary pages work well
Given a list of page ids and contents
| license | License |
| tos | Terms of Service |
| credits | Credits |
| privacy | Privacy Policy |
| activeFires | Active Fires |
| about | About |
Then I check that all pages works properly
Scenario: Check that all secondary pages work well when not logged
Given I logout if logged
And a list of page urls and contents
| login | Login | true |
| signup | Sign Up | true |
| fires | Fires | true |
| zones | Monitored | true |
| recover-password | Recover Password | 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
# 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
Given I logout if logged
And a list of non visible pages ids and contents
| 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

View file

@ -0,0 +1,50 @@
/* global module expect require process client */
const { randomText, randomEmail } = require('./helper.js');
module.exports = function doSteps() {
this.Given(/^I am on the zones page$/, () => {
client.url(`${process.env.ROOT_URL}/zones`);
if (client.isVisible('#logout')) {
client.click('#logout');
}
});
this.Then(/^I can send feedback without email$/, () => {
client.waitForVisible('#feedback-tab', 10000);
client.click('#feedback-tab');
client.waitForVisible('#feedback-form', 10000);
client.setValue('#feedbackTextarea', randomText(500));
client.click('#sendFeedbackBtn');
client.waitForVisible('.bert-alert', 10000, true);
client.waitUntil(() => client.isVisible('.bert-alert') === false, 10000);
});
this.Then(/^I can send feedback with email$/, () => {
client.waitForVisible('#feedback-tab', 10000);
client.click('#feedback-tab');
client.waitForVisible('#feedback-form', 10000);
client.waitForEnabled('input[name="email"]');
client.setValue('input[name="email"]', randomEmail());
client.setValue('#feedbackTextarea', randomText(500));
client.click('#sendFeedbackBtn');
client.waitForVisible('.bert-alert', 10000, true);
client.waitUntil(() => client.isVisible('.bert-alert') === false, 10000);
});
this.Given(/^I'm not logged$/, () => {
client.waitForVisible('#react-root', 5000);
if (client.isVisible('#logout')) {
client.click('#logout');
}
});
this.Given(/^I am on the fires page$/, () => {
client.url(`${process.env.ROOT_URL}/fires`);
});
this.Then(/^I cannot send feedback$/, () => {
client.waitForVisible('#react-root', 5000);
client.waitUntil(() => client.isVisible('#feedback-tab') === false, 10000);
});
};

View file

@ -0,0 +1,20 @@
/* global module expect require process client */
module.exports = function doSteps() {
let pages;
this.Then(/^I check that exist this list of pages in the sitemap$/, (table, callback) => {
// Write code here that turns the phrase above into concrete actions
pages = table.raw();
for (let i = 0; i < pages.length; i += 1) {
// TODO download sitemap and parse
// client.waitForText('.text', pages[i][0]);
}
callback();
});
this.Given(/^the page sitemap\.xml$/, (callback) => {
client.url(`${process.env.ROOT_URL}/sitemap.xml`);
callback();
});
};

View file

@ -0,0 +1,32 @@
// https://stackoverflow.com/questions/1349404/generate-random-string-characters-in-javascript
function makeid(size) {
let text = '';
const possible = 'abcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < size; i += 1) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
export const randomText = n => makeid(n);
export const randomUsername = () => makeid(7);
export const randomPassword = () => randomUsername();
export const randomEmail = () => `${randomUsername()}@example.com`;
export const goHome = (client) => {
client.url(process.env.ROOT_URL);
client.waitForVisible('#react-root', 10000);
expect(client.isVisible('#react-root')).toBe(true);
client.waitUntil(() => client.isVisible('.pg-loading-center-middle') === false, 5000);
client.waitUntil(() => client.isVisible('.pg-loading-center-middle') === false, 5000);
// Close alert
if (client.isVisible('.bert-content')) {
client.click('.bert-content');
}
if (client.isVisible('#acceptCookies')) {
client.click('#acceptCookies');
}
};

View file

@ -0,0 +1,75 @@
/* global client */
(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(() => {
// global.expect = require('@xolvio/jasmine-expect').expect;
// https://github.com/webdriverio/webdriverio/issues/1145
if (!this.initMyCmds) {
client.addCommand('waitForClickable', function elementClickable(selector, timeout) {
this.waitForVisible(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;
}
client.windowHandleSize({ width: 1500, height: 1000 });
// server.call('fixtures/reset');
// server.call('emailStub/reset');
// server.call('emailStub/stub');
// server.call('fixtures/stubCloudFrontClient');
// go to the page first so we have access to the Meteor object
// console.log('Before all tests...');
// client.url(process.env.ROOT_URL);
/* client.executeAsync(function (done) {
* var customer = {
* id: 'randomId',
* email: 'me@example.com',
* subscriptions: {
* data: [{
* current_period_start: 1436716844,
* current_period_end: 1436716844
* }]
* }
* }; */
// this call will stub stripe both on the client and server
// Meteor.call('stripeStub/stub', customer, done);
});
};
}());

View file

@ -0,0 +1,150 @@
/* global module expect require process client */
const {
goHome, randomUsername, randomPassword, randomEmail
} = require('./helper.js');
let firstName;
let lastName;
let pass;
let email;
const setUserValues = (isEdit) => {
client.waitForVisible('input[name="firstName"]', 10000);
firstName = randomUsername();
lastName = randomUsername();
email = randomEmail();
client.setValue('input[name="firstName"]', firstName);
client.setValue('input[name="lastName"]', lastName);
client.setValue('input[name="emailAddress"]', email);
if (isEdit) {
client.setValue('input[name="currentPassword"]', pass);
pass = randomPassword();
client.setValue('input[name="newPassword"]', pass);
} else {
pass = randomPassword();
client.setValue('input[name="password"]', pass);
}
};
module.exports = function doSteps(notos) {
this.Given(/^I am on the home page$/, () => {
goHome(client);
});
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')) {
client.click('#logout');
}
client.waitForVisible('#signup', 10000);
}
function register() {
logoutIfLogged();
client.waitForVisible('#signup', 10000);
client.click('#signup');
setUserValues();
if (!notos) {
client.click('input[name="tos"]');
}
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$/, () => {
register();
});
function checkName() {
client.waitForVisible('#profile', 5000);
client.waitUntilText('#profile', firstName);
client.waitUntilText('#profile', lastName);
}
this.Then(/^I should be logged in$/, () => {
client.waitUntil(() => client.isVisible('#logout') === true, 10000);
client.waitUntil(() => client.isVisible('#profile') === true, 10000);
});
/* this.Given(/^I am signed out$/, () => {
* client.waitUntil(() => client.isVisible('#logout') === true, 10000);
* });
*/
this.When(/^I sign out$/, () => {
waitNoBert();
client.waitForClickable('#logout', 10000);
// client.waitForVisible('#logout', 10000);
client.click('#logout');
client.waitForVisible('#login', 10000);
});
function login(badpass) {
waitNoBert();
client.waitForVisible('#login', 10000);
client.click('#login');
client.waitForVisible('#loginSubmit', 5000);
client.setValue('input[name="emailAddress"]', email);
client.setValue('input[name="password"]', badpass ? randomPassword() : pass);
client.click('#loginSubmit');
waitNoBert();
}
this.When(/^I enter my email and password$/, () => {
login();
});
this.Then(/^I can edit my profile$/, () => {
waitNoBert();
client.waitForClickable('#profile', 10000);
client.click('#profile');
client.pause(2000);
setUserValues(true);
client.click('#profileSubmit');
waitNoBert();
checkName();
});
this.When(/^I enter incorrect authentication information$/, () => {
login(true);
});
this.Then(/^I should see a user not found error$/, () => {
client.waitForVisible('.alert-danger', 10000, true);
});
this.Given(/^I register with some name, password and email$/, () => {
register();
});
this.Then(/^I should be registered$/, () => {
client.waitForVisible('.bert-alert', 10000, true);
waitNoBert();
});
this.Given(/^I register with some name, password and email but without accept conditions$/, () => {
register(true);
});
this.Then(/^I shouldn't be registered$/, () => {
client.waitForVisible('.bert-alert', 10000, true);
client.waitForVisible('#login', 10000, true);
});
};

View file

@ -0,0 +1,81 @@
/* global module expect require process client */
const { goHome } = require('./helper.js');
// Does no works with arrow func
module.exports = function () {
let pages;
this.Given(/^a list of page ids and contents$/, (table, callback) => {
pages = table.raw();
callback();
});
function processPage(i) {
const id = `#${pages[i][0]}`;
const content = pages[i][1];
client.waitForVisible(id, 10000);
client.click(id);
client.waitUntilText('#react-root', content);
// https://jasmine.github.io/2.3/introduction.html#section-Expectations
expect(client.getTitle()).toContain(pages[i][1]);
}
function processPageUrl(i) {
const url = `${pages[i][0]}`;
const content = pages[i][1];
client.url(`${process.env.ROOT_URL}/${url}`);
client.waitForVisible('#react-root', 10000);
client.waitUntilText('#react-root', content);
const checkTitle = pages[i][2] === 'true';
// https://jasmine.github.io/2.3/introduction.html#section-Expectations
if (checkTitle) {
expect(client.getTitle()).toContain(pages[i][1]);
}
}
this.Given(/^a list of non visible pages ids and contents$/, (table, callback) => {
pages = table.raw();
callback();
});
this.Then(/^I check that all non visible pages works properly$/, (callback) => {
for (let i = 0; i < pages.length; i += 1) {
processPageUrl(i);
}
callback();
});
this.Then(/^I check that all pages works properly$/, (callback) => {
goHome(client);
if (client.isVisible('#logout')) {
client.click('#logout');
}
for (let i = 0; i < pages.length; i += 1) {
processPage(i);
}
callback();
});
this.Given(/^a list of page urls and contents$/, (table, callback) => {
pages = table.raw();
callback();
});
this.Then(/^I check that all page urls works properly$/, (callback) => {
goHome(client);
for (let i = 0; i < pages.length; i += 1) {
processPageUrl(i);
}
callback();
});
this.Then(/^they are spiderable$/, (callback) => {
// Write code here that turns the phrase above into concrete actions
for (let i = 0; i < pages.length; i += 1) {
client.url(`${process.env.ROOT_URL}/${pages[i][0]}?_escaped_fragment_=`);
client.waitUntilText('#react-root', pages[i][1]);
}
callback();
});
};

View file

@ -7,6 +7,7 @@
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="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:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="210mm"
@ -245,9 +246,9 @@
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.25"
inkscape:cx="-324.9908"
inkscape:cy="997.3585"
inkscape:zoom="0.23559314"
inkscape:cx="-2890.4884"
inkscape:cy="-486.23766"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
@ -338,9 +339,9 @@
<g
id="g22554"
transform="translate(103.16604,61.471991)"
inkscape:export-filename="/mnt/md1/proyectos/dev/todos-contra-el-fuego-web/public/fire-marker.png"
inkscape:export-xdpi="4.0856881"
inkscape:export-ydpi="4.0856881">
inkscape:export-filename="/mnt/md1/proyectos/dev/todos-contra-el-fuego-web/public/fire-marker-s.png"
inkscape:export-xdpi="2"
inkscape:export-ydpi="2">
<path
style="fill:#d41300;fill-opacity:1;stroke:none;stroke-width:10.58333302;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"
@ -448,11 +449,18 @@
inkscape:connector-curvature="0" />
</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
id="g17717"
inkscape:export-filename="/mnt/md1/proyectos/dev/todos-contra-el-fuego-web/public/favicon-200.png"
inkscape:export-xdpi="28.790001"
inkscape:export-ydpi="28.790001">
id="g8543"
inkscape:export-filename="/mnt/md1/proyectos/dev/todos-contra-el-fuego-web/public/fire-marker-l.png"
inkscape:export-xdpi="4"
inkscape:export-ydpi="4">
<rect
y="108.84225"
x="-419.07861"
@ -499,61 +507,66 @@
style="font-size:192px;fill:#ff0000" /></flowRegion><flowPara
id="flowPara8488"></flowPara></flowRoot> <g
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-xdpi="4.0856881"
inkscape:export-ydpi="4.0856881">
inkscape:export-ydpi="4.0856881"
style="stroke-width:5.62737894">
<g
id="g8559">
id="g8559"
style="stroke-width:5.62737894">
<g
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="rotate(-144.74008,-66.772616,119.34544)"
id="g22554-2">
id="g22554-2"
style="stroke-width:5.62737894">
<path
sodipodi:nodetypes="cszzcczasc"
inkscape:connector-curvature="0"
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"
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
sodipodi:nodetypes="cszzcczcsc"
inkscape:connector-curvature="0"
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"
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
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="rotate(96.689458,-75.070103,121.00438)"
id="g22554-5">
id="g22554-5"
style="stroke-width:5.62737894">
<path
sodipodi:nodetypes="cszzcczasc"
inkscape:connector-curvature="0"
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"
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
sodipodi:nodetypes="cszzcczcsc"
inkscape:connector-curvature="0"
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"
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
transform="rotate(-22.690691,-35.704234,120.074)"
id="g8542">
id="g8542"
style="stroke-width:5.62737894">
<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"
id="path5329-9"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cszzcczasc" />
<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"
id="path5331-35"
inkscape:connector-curvature="0"
@ -561,5 +574,86 @@
</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>
</svg>

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Before After
Before After

View file

@ -1,10 +1,9 @@
{
"extensions": [ "ttf", "woff", "woff2" ],
"map": {
"node_modules/simple-line-icons/fonts/Simple-Line-Icons.woff": "fonts/Simple-Line-Icons.woff",
"node_modules/simple-line-icons/fonts/Simple-Line-Icons.woff": "fonts/Simple-Line-Icons.woff?v=2.4.0",
"node_modules/simple-line-icons/fonts/Simple-Line-Icons.woff2": "fonts/Simple-Line-Icons.woff2?v=2.4.0",
"node_modules/simple-line-icons/fonts/Simple-Line-Icons.ttf": "fonts/Simple-Line-Icons.ttf?v=2.4.0"
"node_modules/simple-line-icons/fonts/Simple-Line-Icons.woff": "/fonts/Simple-Line-Icons.woff?v=2.4.1",
"node_modules/simple-line-icons/fonts/Simple-Line-Icons.woff2": "/fonts/Simple-Line-Icons.woff2?v=2.4.1",
"node_modules/simple-line-icons/fonts/Simple-Line-Icons.ttf": "/fonts/Simple-Line-Icons.ttf?v=2.4.1"
},
"verbose": false
}

View file

@ -1,5 +1,6 @@
/* eslint-disable consistent-return */
import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import SimpleSchema from 'simpl-schema';
import firesCommonSchema from '../Common/FiresSchema';
@ -18,8 +19,11 @@ ActiveFires.deny({
remove: () => true
});
const activeFiresSchema = firesCommonSchema;
ActiveFires.schema = new SimpleSchema(firesCommonSchema);
activeFiresSchema.fireUnion = { type: Meteor.Collection.ObjectID, optional: true, blackbox: true };
ActiveFires.schema = new SimpleSchema(activeFiresSchema);
ActiveFires.attachSchema(ActiveFires.schema);

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

@ -5,6 +5,9 @@
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 ActiveFires from '../ActiveFires';
const counter = new Counter('countActiveFires', ActiveFires.find({}));
@ -13,7 +16,7 @@ Meteor.publish('activefirestotal', function total() {
return counter;
});
const activefires = (northEastLng, northEastLat, southWestLng, southWestLat) => {
const activefires = (northEastLng, northEastLat, southWestLng, southWestLat, withMarks) => {
const fires = ActiveFires.find({
ourid: {
$geoWithin: {
@ -27,19 +30,36 @@ const activefires = (northEastLng, northEastLat, southWestLng, southWestLat) =>
fields: {
lat: 1,
lon: 1,
scan: 1
when: 1,
scan: 1,
track: 1
}
});
// console.log(`Fires total: ${fires.count()}`);
if (Meteor.isDevelopment) console.log(`Active fires 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('activefiresmyloc', function activeInMyLoc(northEastLng, northEastLat, southWestLng, southWestLat) {
Meteor.publish('activefiresmyloc', 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);
return activefires(northEastLng, northEastLat, southWestLng, southWestLat);
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 dateLongFormat = date => momentTz(date).format('LLLL (z)');
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();

View file

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

View file

@ -34,4 +34,15 @@ FalsePositives.schema = new SimpleSchema({
FalsePositives.attachSchema(FalsePositives.schema);
export const falsePositivesRemap = (odoc) => {
const doc = odoc;
const geo = doc.geo;
doc.lat = geo.coordinates[1];
doc.lon = geo.coordinates[0];
doc._id = doc.fireId;
doc.id = doc.fireId;
delete doc.geo;
return doc;
};
export default FalsePositives;

View file

@ -6,6 +6,29 @@ import FalsePositiveTypes from './FalsePositiveTypes';
import Fires from '../Fires/Fires';
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({
'falsePositives.insert': function falsePositivesInsert(fireId, keyType) {
check(fireId, Meteor.Collection.ObjectID);
@ -17,27 +40,9 @@ Meteor.methods({
}
check(this.userId, String);
const fire = Fires.findOne(fireId);
const owner = this.userId;
if (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: 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);
}
upsertFalsePositive(keyType, owner, fire);
} else {
throw new Meteor.Error('500', 'Fuego no encontrado');
}

View file

@ -0,0 +1,105 @@
/* 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 '/imports/startup/server/leaflet-workaround.js';
import L from 'leaflet';
import calcUnion from '/imports/ui/components/Maps/SubsUnion/Unify';
import Industries from '/imports/api/Industries/Industries';
import FalsePositives from '../FalsePositives';
const counter = new Counter('countFalsePositives', FalsePositives.find({}));
Meteor.publish('falsePositivesTotal', function total() {
return counter;
});
export const firesUnion = (fires) => {
const firesArray = Array.isArray(fires) ? fires : fires.fetch(); // if not is a cursor
const remap = firesArray.map(function remap(doc) {
const isNASA = doc.type === 'modis' || doc.type === 'viirs';
const pixelSize = doc.type === 'viirs' ? 0.375 : 1; // viirs has 375m pixel size, modis 1000m
// default 1 km for neightbor alerts
const map = {
location: { lat: doc.lat, lon: doc.lon },
distance: isNASA ? doc.scan * pixelSize : 1,
distanceY: isNASA ? doc.track * pixelSize : 1
};
// console.log(map);
return map;
});
const union = calcUnion(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;
};
export const whichAreFalsePositives = (collection, union) => {
const result = collection.find({
geo: {
$geoWithin: {
$geometry: union[0].geometry
}
}
}, {
fields: {
geo: 1,
// type: 1,
// when: 1,
fireId: 1
}
});
/* console.log(`False positive total: ${falsePos.count()}`);
console.log(`False positives: ${JSON.stringify(falsePos.fetch())}`); */
return result;
};
const find = (collection, northEastLng, northEastLat, southWestLng, southWestLat) => {
const fires = collection.find({
geo: {
$geoWithin: {
$box: [
[southWestLng, southWestLat],
[northEastLng, northEastLat]
]
}
}
}, {
fields: {
geo: 1,
// type: 1,
// when: 1,
fireId: 1
}
});
// console.log(`Fires total: ${fires.count()}`);
return fires;
};
Meteor.publishTransformed('falsePositivesMyloc', function falsePositivesInMyLoc(northEastLng, northEastLat, southWestLng, southWestLat) {
// 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));
return find(FalsePositives, northEastLng, northEastLat, southWestLng, southWestLat);
});
Meteor.publishTransformed('industriesMyloc', function industriesInMyLoc(northEastLng, northEastLat, southWestLng, southWestLat) {
// 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));
return find(Industries, northEastLng, northEastLat, southWestLng, southWestLat);
});

View file

@ -33,9 +33,10 @@ Meteor.publish('fireAlerts', function fireAlerts(northEastLng, northEastLat, sou
fields: {
lat: 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;
});

View file

@ -7,6 +7,13 @@ import urlEnc from '/imports/modules/url-encode';
import { Promise } from 'meteor/promise';
import NodeGeocoder from 'node-geocoder';
import { gmapServerKey } from '/imports/startup/server/IPGeocoder';
import ActiveFiresCollection from '/imports/api/ActiveFires/ActiveFires';
import FireAlertsCollection from '/imports/api/FireAlerts/FireAlerts';
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 FiresCollection from '../Fires';
function findFire(unsealed) {
@ -24,50 +31,148 @@ const options = {
const geocoder = NodeGeocoder(options);
Meteor.publish('fireFromHash', function fireFromHash(fireEnc) {
check(fireEnc, String);
const unseal = async (obj) => {
try {
// console.log(fireEnc);
const unsealed = Promise.await(urlEnc.decrypt(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);
const unsealed = await urlEnc.decrypt(obj);
return unsealed;
} catch (error) {
// console.warn(error);
return undefined;
}
};
// FIXME:
if (typeof unsealed.confidence === 'string') {
unsealed.confidence = Number.parseInt(unsealed.confidence, 10);
}
if (typeof unsealed.confidence === 'undefined' || isNaN(unsealed.confidence)) {
delete unsealed.confidence;
}
FiresCollection.schema.validate(unsealed);
const fire = findFire(unsealed);
// console.log(`Found: ${fire.count()}`);
if (!unsealed.address) {
try {
const rev = Promise.await(geocoder.reverse({ lat: unsealed.lat, lon: unsealed.lon }));
if (rev[0]) {
unsealed.address = rev[0].formattedAddress;
}
// console.log(unsealed.address);
} catch (reve) {
console.error(reve);
const unsealW = obj => unseal(obj);
const fixConfidence = (obj) => {
if (typeof obj.confidence === 'string') {
obj.confidence = Number.parseInt(obj.confidence, 10);
}
if (typeof obj.confidence === 'undefined' || isNaN(obj.confidence)) {
delete obj.confidence;
}
return obj;
};
const findOrCreateFire = (obj) => {
const fire = findFire(obj);
// console.log(`Found: ${fire.count()}`);
if (!obj.address) {
try {
const rev = Promise.await(geocoder.reverse({ lat: obj.lat, lon: obj.lon }));
if (rev[0]) {
obj.address = rev[0].formattedAddress;
}
// console.log(obj.address);
} catch (reve) {
console.warn(reve);
}
if (fire.count() === 0) {
// const result =
FiresCollection.upsert({ ourid: unsealed.ourid, when: unsealed.when, type: unsealed.type }, { $set: unsealed }, { multi: false, upsert: true });
// console.log(JSON.stringify(result));
}
if (fire.count() === 0) {
// const result =
// console.log('Creating new fire');
FiresCollection.upsert({ ourid: obj.ourid, when: obj.when, type: obj.type }, { $set: obj }, { multi: false, upsert: true });
// console.log(JSON.stringify(result));
}
return findFire(obj);
};
Meteor.publish('fireFromAlertId', function fireFromAlertId(_id) {
try {
check(_id, String);
// console.log(`Looking for alert fire ${_id}`);
const fire = FireAlertsCollection.findOne(new Meteor.Collection.ObjectID(_id));
if (fire) {
// console.info(`Active fire found: ${_id}`);
return findOrCreateFire(fixConfidence(fire));
}
return findFire(unsealed);
/* console.log(`fires: ${fire.count()}`);
* return fire; */
console.info(`Alert fire not found: ${_id}`);
// Not found in active fires!
return this.ready();
} catch (e) {
console.error(e);
throw new Meteor.Error('500', e);
console.info(`Alert fire not found (with error): ${_id}`);
return this.ready();
}
});
Meteor.publish('fireFromActiveId', function fireFromActiveId(_id) {
try {
check(_id, String);
// console.log(`Looking for active fire ${_id}`);
const fire = ActiveFiresCollection.findOne(new Meteor.Collection.ObjectID(_id));
if (fire) {
// console.info(`Active fire found: ${_id}`);
return findOrCreateFire(fixConfidence(fire));
}
console.info(`Active fire not found: ${_id}`);
// Not found in active fires!
return this.ready();
} catch (e) {
console.info(`Active fire not found (with error): ${_id}`);
return this.ready();
}
});
Meteor.publish('fireFromId', function fireFromId(_id) {
try {
check(_id, String);
// console.log(`Looking for archive fire ${_id}`);
const fire = FiresCollection.find(new Meteor.Collection.ObjectID(_id));
if (fire.count() !== 0) {
// console.info(`Archive fire found: ${_id}`);
const union = firesUnion(fire);
const falsePos = whichAreFalsePositives(FalsePositives, union);
const industries = whichAreFalsePositives(Industries, union);
return [fire, falsePos, industries];
}
console.info(`Fire not found: ${_id}`);
// Not found in active fires!
return this.ready();
} catch (e) {
console.info(`Archive fire not found (with error): ${_id}`);
return this.ready();
}
});
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(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 {
return fireFromHash(fireEnc, params);
} catch (e) {
console.warn(e);
logUrl(fireEnc, params);
// https://guide.meteor.com/data-loading.html
return this.ready();
}
});

View file

@ -0,0 +1,58 @@
/* eslint-disable consistent-return */
import { Mongo } from 'meteor/mongo';
import SimpleSchema from 'simpl-schema';
import LocationSchema from '/imports/api/Utility/LocationSchema.js';
const Industries = new Mongo.Collection('industries');
// https://prtr.unece.org/prtr-global-map
// For now using:
// AUSTRALIA
// https://data.gov.au/dataset/npi#
//
// EUROPE
// https://www.eea.europa.eu/data-and-maps/data/member-states-reporting-art-7-under-the-european-pollutant-release-and-transfer-register-e-prtr-regulation-16
//
// CANADA
// https://www.canada.ca/en/environment-climate-change/services/national-pollutant-release-inventory/tools-resources-data/access.html
//
// USA
// https://www.epa.gov/enviro/epa-frs-facilities-state-single-file-csv-download
// TODO:
// http://www.nite.go.jp/
// http://www.retc.cl/datos-retc/
Industries.allow({
insert: () => false,
update: () => false,
remove: () => false
});
Industries.deny({
insert: () => true,
update: () => true,
remove: () => true
});
Industries.schema = new SimpleSchema({
geo: LocationSchema,
name: { type: String, optional: true },
registry: String
});
Industries.attachSchema(Industries.schema);
export const industriesRemap = (odoc) => {
const doc = odoc;
const geo = doc.geo;
doc.lat = geo.coordinates[1];
doc.lon = geo.coordinates[0];
doc.id = doc._id;
delete doc.geo;
return doc;
};
export default Industries;

View file

@ -0,0 +1,29 @@
/* eslint-disable consistent-return */
import { Mongo } from 'meteor/mongo';
import SimpleSchema from 'simpl-schema';
const IndustryRegistries = new Mongo.Collection('industryregistries', { idGeneration: 'MONGO' });
IndustryRegistries.allow({
insert: () => false,
update: () => false,
remove: () => false
});
IndustryRegistries.deny({
insert: () => true,
update: () => true,
remove: () => true
});
IndustryRegistries.schema = new SimpleSchema({
name: String,
agency: String,
region: String
});
IndustryRegistries.attachSchema(IndustryRegistries.schema);
export default IndustryRegistries;

View file

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

View file

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

View file

@ -4,7 +4,7 @@ import { Meteor } from 'meteor/meteor';
import Notifications from '../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()}`);
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

@ -40,6 +40,7 @@ SiteSettings.getSchema = type => new SimpleSchema({
type: String,
description: String,
value: SiteSettingsTypes[type].value,
isPublic: Boolean,
createdAt: defaultCreatedAt,
updatedAt: defaultUpdateAt
});

View file

@ -1,11 +1,11 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
/* import { check } from 'meteor/check';
import SiteSettings from './SiteSettings';
import SiteSettingsTypes from './SiteSettingsTypes';
import SiteSettingsTypes from './SiteSettingsTypes'; */
import rateLimit from '../../modules/rate-limit';
Meteor.methods({
'siteSettings.insert': function siteSettingsInsert(setting) {
/* 'siteSettings.insert': function siteSettingsInsert(setting) {
check(setting, {
name: String,
type: String,
@ -46,14 +46,14 @@ Meteor.methods({
} catch (exception) {
throw new Meteor.Error('500', exception);
}
}
} */
});
rateLimit({
methods: [
'siteSettings.insert',
/* 'siteSettings.insert',
'siteSettings.update',
'siteSettings.remove'
'siteSettings.remove' */
],
limit: 5,
timeRange: 1000

View file

@ -3,4 +3,4 @@
import { Meteor } from 'meteor/meteor';
import SiteSettings from '../SiteSettings';
Meteor.publish('settings', () => SiteSettings.find());
Meteor.publish('settings', () => SiteSettings.find({ isPublic: true }));

View file

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

View file

@ -10,26 +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({
'subscriptions.insert': function subscriptionsInsert(doc) {
'subscriptions.insert': function subscriptionInsertMethod(doc) {
check(doc, {
location: Match.ObjectIncluding({ lat: Number, lon: Number }),
distance: Number
});
const type = 'web';
try {
const newDoc = {
owner: this.userId,
type,
geo: geo(doc),
...doc
};
// console.log(newDoc);
return Subscriptions.insert(newDoc);
} catch (exception) {
console.error(exception);
throw new Meteor.Error('500', exception);
}
return subscriptionsInsert(doc, this.userId, 'web');
},
'subscriptions.update': function subscriptionsUpdate(doc) {
check(doc, {
@ -48,15 +71,9 @@ Meteor.methods({
throw new Meteor.Error('500', exception);
}
},
'subscriptions.remove': function subscriptionsRemove(subscriptionId) {
'subscriptions.remove': function subscriptionsRemoveMethod(subscriptionId) {
check(subscriptionId, Meteor.Collection.ObjectID);
try {
return Subscriptions.remove(subscriptionId);
} catch (exception) {
console.error(exception);
throw new Meteor.Error('500', exception);
}
return subscriptionsRemove(subscriptionId);
}
});

View file

@ -3,43 +3,8 @@
import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import { check } from 'meteor/check';
import Perlin from 'loms.perlin';
import Subscriptions from '../Subscriptions';
Perlin.seed(Math.random());
Meteor.publishTransformed('userSubsToFires', function transform() {
// https://en.wikipedia.org/wiki/Location_obfuscation
// https://en.wikipedia.org/wiki/Decimal_degrees#Precision
// https://gis.stackexchange.com/questions/27792/what-simple-effective-techniques-for-obfuscating-points-are-available
return Subscriptions.find().serverTransform(function transformDoc(odoc) {
const doc = odoc;
// Destructuring gives me an error: "Cannot destructure property `location` of 'undefined'"
const location = doc.location;
/* doc.lat = location.lat;
* doc.lon = location.lon; */
let lat;
let lon;
if (location) {
lat = Math.round(location.lat * 10) / 10;
lon = Math.round(location.lon * 10) / 10;
// console.log(`[${lat}, ${lon}]`);
const noiseBase = Perlin.perlin2(lat, lon);
const noise = Math.abs(noiseBase / 3);
// console.log(`Noise ${noise}, abs: ${Math.abs(noise)}`);
lat += noise;
lon += noise;
doc.location.lat = lat;
doc.location.lon = lon;
doc.distance += noiseBase;
}
// console.log(`with noise: [${doc.lat}, ${doc.lon}]`);
delete doc.chatId;
delete doc.geo;
return doc;
});
});
Meteor.publish('mysubscriptions', function subscriptions() {
return Subscriptions.find({ owner: this.userId }); // type: 'web'
});

View file

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

View file

@ -10,11 +10,12 @@ export default (options, user, lang) => {
const applicationName = i18n.t('AppName');
const firstName = OAuthProfile ? OAuthProfile.name.first : options.profile.name.first;
const emailAddress = OAuthProfile ? OAuthProfile.email : options.email;
const welcome = i18n.t('welcome');
if (emailAddress) {
sendEmail({
to: emailAddress,
from: `${applicationName} <noreply@comunes.org>`,
subject: `[${applicationName}] Welcome, ${firstName}!`,
subject: `[${applicationName}] ${welcome} ${firstName}!`,
lang,
template: 'welcome',
templateVars: {

View file

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

View file

@ -1,15 +1,10 @@
/* eslint-disable import/no-absolute-path */
import fs from 'fs';
import getFallbackLang from '/imports/modules/lang-fallback';
const as = f => `assets/app/${f}`;
export const getFallbackLang = (lang) => {
if (lang === 'ast' || lang === 'gl' || lang === 'eu' || lang === 'ca') {
return 'es';
}
return 'en';
};
export const getFileNameOfLang = (dir, fileName, ext, lang) => {
const base = `${dir}/${fileName}`;
const fallback = getFallbackLang(lang);

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,12 +1,11 @@
const parseGoogleData = service => {
return {
email: service.email,
name: {
first: service.given_name,
last: service.family_name,
},
};
};
const parseGoogleData = service => ({
email: service.email,
name: {
first: service.given_name,
last: service.family_name
},
lang: service.locale
});
const parseGithubData = (profile, service) => {
const name = profile.name.split(' ');
@ -14,20 +13,18 @@ const parseGithubData = (profile, service) => {
email: service.email,
name: {
first: name[0],
last: name[1],
},
last: name[1]
}
};
};
const parseFacebookData = service => {
return {
email: service.email,
name: {
first: service.first_name,
last: service.last_name,
},
};
};
const parseFacebookData = service => ({
email: service.email,
name: {
first: service.first_name,
last: service.last_name
}
});
const getDataForService = (profile, services) => {
if (services.facebook) return parseFacebookData(services.facebook);

View file

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

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 { 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 templateToText from './handlebars-email-to-text';
import templateToHTML from './handlebars-email-to-html';
import { getFileNameOfLang } from '/imports/api/Utility/server/files.js';
import sendMail from '/imports/startup/server/email';
import i18n from 'i18next';
export function subjectTruncate(n = 70, useWordBoundary = true) {
// 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 }) => {
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

@ -37,6 +37,6 @@ i18n.init((err, t) => {
template: 'bootstrap', // default 'semantic-ui'
// default 'http://s3.amazonaws.com/37assets/svn/765-default-avatar.png'
defaultAvatar: '/default-avatar.png',
markdown: false
markdown: true
});
});

View file

@ -17,7 +17,7 @@ import i18nOpts from '../common/i18n';
const detectorOptions = {
// order and from where user language should be detected
order: ['querystring', 'cookie', 'localStorage', 'navigator', 'htmlTag'],
order: ['querystring', 'navigator', 'cookie', 'localStorage', 'htmlTag'],
// keys or params to lookup language from
lookupQuerystring: 'lng',
@ -53,15 +53,22 @@ i18nOpts.react = {
nsMode: 'default' */
};
const sendMissing = true;
if (sendMissing && !Meteor.isProduction) {
const sendMissing = true; // Meteor.isDevelopment;
if (sendMissing && Meteor.isDevelopment) {
i18nOpts.sendMissing = true;
i18nOpts.sendMissingTo = 'fallback';
i18nOpts.missingKeyHandler = function miss(lng, ns, key, defaultValue, lngs) {
i18nOpts.missingKeyHandler = function miss(lng, ns, key, defaultValue) {
Meteor.call('utility.saveMissingI18n', key, defaultValue);
};
}
function setT9(lang) {
if (lang === 'gl') {
T9n.setLanguage('es');
} else {
T9n.setLanguage(lang);
}
}
i18n.use(backend)
.use(LngDetector)
.use(Cache)
@ -71,16 +78,17 @@ i18n.use(backend)
console.error(err);
return;
}
i18nReady.set(true);
console.log(`Language initialized: ${i18n.language}`);
// document.title = t('AppName');
// Accounts translation
// https://github.com/softwarerero/meteor-accounts-t9n
// console.log("Language: " + i18n.language);
T9n.setLanguage(i18n.language);
setT9(i18n.language);
// console.log(T9n.get('error.accounts.User not found'));
moment.locale(i18n.language);
i18nReady.set(true);
// cookies eu consent
const cookiesOpt = {
cookieTitle: t('Uso de Cookies'),
@ -91,7 +99,7 @@ i18n.use(backend)
position: 'bottom',
linkText: 'Lee más',
linkRouteName: '/privacy',
acceptButtonText: 'Aceptar',
acceptButtonText: t('Aceptar'),
html: false,
expirationInDays: 70,
forceShow: false
@ -104,7 +112,8 @@ Meteor.subscribe('userData'); // lang is there
i18n.on('languageChanged', (lng) => {
moment.locale(lng);
T9n.setLanguage(lng);
// TODO fix this when gl is translated
setT9(lng);
Tracker.autorun((computation) => {
if (Meteor.userId()) {
// logged

View file

@ -1,8 +1,19 @@
/* global */
import React from 'react';
import { render } from 'react-dom';
import { Meteor } from 'meteor/meteor';
import '/imports/startup/client/modernizr';
import App from '../../ui/layouts/App/App';
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')));
}

View file

@ -0,0 +1,4 @@
/* eslint-env jquery */
// remove static description so it's not duplicated
$('meta[name=description]').remove();

File diff suppressed because one or more lines are too long

View file

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

View file

@ -2,12 +2,15 @@ import RavenLogger from 'meteor/flowkey:raven';
import { Meteor } from 'meteor/meteor';
const ravenOptions = {};
const publicDSN = Meteor.settings.public.sentryPublicDSN;
const enabled = !!publicDSN;
const ravenLogger = new RavenLogger({
publicDSN: Meteor.settings.public.sentryPublicDSN, // will be used on the client
privateDSN: Meteor.settings.sentryPrivateDSN, // will be used on the server
const ravenLogger = enabled ? new RavenLogger({
publicDSN, // will be used on the client
shouldCatchConsoleError: true, // default
trackUser: false // default
}, ravenOptions);
trackUser: true // default
}, ravenOptions) : { log: (error) => { console.log(error); } };
console.log(`ravenLogger ${enabled ? 'enabled' : 'disabled'}`);
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,11 +3,16 @@
// Client and Server
Comments.config({
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',
publishUserFields: {
profile: 1
},
mediaAnalyzers: [
Comments.analyzers.image,
Comments.analyzers.youtube
],
generateUsername: function genUser(user) {
// console.log(JSON.stringify(user));
// FIXME

View file

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

View file

@ -1,18 +1,27 @@
/* eslint-disable import/no-absolute-path */
import { Accounts } from 'meteor/accounts-base';
import sendWelcomeEmail from '../../../api/Users/server/send-welcome-email';
import getFallbackLang from '/imports/modules/lang-fallback';
import sendWelcomeEmail from '/imports/api/Users/server/send-welcome-email';
import getOAuthProfile from '/imports/modules/get-oauth-profile';
Accounts.onCreateUser((options, user) => {
const userToCreate = user;
// console.log(JSON.stringify(user));
// console.log(JSON.stringify(options));
let lang = 'en'; // fallback
if (options.profile) {
userToCreate.profile = options.profile;
userToCreate.lang = options.profile.lang;
lang = options.profile.lang;
// console.log(JSON.stringify(user));
lang = userToCreate.lang;
delete options.profile.lang;
} else {
// TODO others (google, etc) ?
}
const OAuthProfile = getOAuthProfile(options, user);
if (OAuthProfile) {
userToCreate.lang = getFallbackLang(OAuthProfile && OAuthProfile.lang ? OAuthProfile.lang : lang);
lang = userToCreate.lang;
}
sendWelcomeEmail(options, userToCreate, lang);
return userToCreate;

View file

@ -9,6 +9,7 @@ import '../../api/Users/server/publications';
import '../../api/Utility/server/methods';
import '../../api/ActiveFires/server/publications';
import '../../api/ActiveFiresUnion/server/publications';
import '../../api/FireAlerts/server/publications';
import '../../api/Subscriptions/methods';
@ -26,3 +27,4 @@ import '../../api/SiteSettings/methods';
import '../../api/SiteSettings/server/publications';
import '../../api/FalsePositives/methods';
import '../../api/FalsePositives/server/publications';

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 { Mongo } from 'meteor/mongo';
import nodemailer from 'nodemailer';
import { MailTime } from 'meteor/ostrio:mailer';
import i18n from 'i18next';
import isMaster from './isMaster';
const transports = [];
@ -20,38 +20,46 @@ export const hr = `<table cellspacing="0" cellpadding="0" border="0" width="100%
</tr>
</table>`;
const MailQueue = new MailTime({
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
});
let MailQueue;
// A Client (not used yet)
// const MailQueueClient = new MailTime({
// db,
// type: 'client',
// debug: true,
// strategy: 'balancer', // Transports will be used in round robin chain
// concatEmails: true // Concatenate emails to the same address
// });
export const isMailServerMaster = Meteor.settings.private.isMailServer && isMaster();
if (isMailServerMaster) {
console.log('I\'m the mail server');
MailQueue = new MailTime({
db,
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) {
if (debug) {
@ -78,27 +86,6 @@ if (Meteor.settings.private.testMailer) {
sendMail(emailOpts, true);
}
// Set interval to greater than 256
// 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());
export const sendEmailsFromQueue = () => {
MailQueue.___send(() => { });
};
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,25 @@
/* eslint-disable prefer-arrow-callback */
/* eslint-disable import/no-absolute-path */
import { Meteor } from 'meteor/meteor';
import i18n from 'i18next';
import { check } from 'meteor/check';
import sendEmail from '/imports/startup/server/email';
Meteor.methods({
'send-feedback': function sendFeedback(email, feedback) {
check(email, String);
check(feedback, String);
const appName = i18n.t('AppName');
sendEmail({
to: 'fires@comunes.org',
from: `${appName} <noreply@comunes.org>`,
subject: `Feedback de ${email}!`,
sendAt: new Date(),
text: `Feedback de ${email}\n\n${feedback}`,
html: `<p>${feedback}</p>`,
template: '<body>{{appName}}<h2>{{{subject}}}</h2>{{{html}}}</body>',
appName
}, true);
}
});

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

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

View file

@ -1,3 +1,6 @@
import './ravenLogger';
import './catchExceptions';
import './fibers';
import './i18n';
import './accounts';
import './api';
@ -7,4 +10,11 @@ import './IPGeocoder';
import './migrations';
import './notificationsObserver';
import './facts';
import '../common/comments';
import './comments';
import './sitemaps';
import './subsUnion';
import './prerender';
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 */
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
@ -7,13 +7,18 @@ import UserSubsToFiresCollection from '/imports/api/Subscriptions/Subscriptions'
import FireAlertsCollection from '/imports/api/FireAlerts/FireAlerts';
import SiteSettings from '/imports/api/SiteSettings/SiteSettings';
import FalsePositives from '/imports/api/FalsePositives/FalsePositives';
import Industries from '/imports/api/Industries/Industries';
import IndustryRegistries from '/imports/api/Industries/IndustryRegistries';
import Notifications from '/imports/api/Notifications/Notifications';
import ActiveFiresUnion from '/imports/api/ActiveFiresUnion/ActiveFiresUnion';
import { Mongo } from 'meteor/mongo';
Meteor.startup(() => {
// https://github.com/percolatestudio/meteor-migrations
Migrations.config({
// Log job run details to console
log: true
log: Meteor.isProduction
});
Migrations.add({
@ -104,8 +109,152 @@ Meteor.startup(() => {
}
});
Migrations.add({
version: 7,
up: function defLangIfNull() {
Meteor.users.find({ lang: null }).forEach((user) => {
Meteor.users.update({ _id: user._id }, {
$set: {
lang: 'es'
}
});
});
}
});
Migrations.add({
version: 8,
up: function siteSettingsAddIndex() {
SiteSettings._ensureIndex({ isPublic: 1 });
SiteSettings.find({ isPublic: null }).forEach((setting) => {
SiteSettings.update({ _id: setting._id }, { $set: { isPublic: true } });
});
}
});
Migrations.add({
version: 9,
up: function siteSettingsAddIndex() {
Industries._ensureIndex({ registry: 1 });
Industries._ensureIndex({ geo: '2dsphere' });
// https://www.eea.europa.eu/data-and-maps/data/member-states-reporting-art-7-under-the-european-pollutant-release-and-transfer-register-e-prtr-regulation-16
IndustryRegistries.insert({
_id: '1', name: 'E-PRTR', agency: 'EEA', region: 'EU'
});
// https://www.epa.gov/enviro/epa-frs-facilities-state-single-file-csv-download
IndustryRegistries.insert({
_id: '2', name: 'FRS', agency: 'EPA', region: 'US'
});
}
});
Migrations.add({
version: 10,
up: function siteSettingsAddIndex() {
IndustryRegistries.insert({
_id: '3', name: 'NPRI', agency: 'GCODP', region: 'Canada'
});
IndustryRegistries.insert({
_id: '4', name: 'NPI', agency: 'DEE', region: 'Australia'
});
}
});
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
Migrations.migrateTo('latest');
// Migrations.migrateTo('5,rerun');
// Migrations.migrateTo('14,rerun');
});

View file

@ -1,111 +1,19 @@
/* eslint-disable import/no-absolute-path */
import i18n from 'i18next';
import { Meteor } from 'meteor/meteor';
import moment from 'moment';
import { dateLongFormat } from '/imports/api/Common/dates';
import Notifications from '/imports/api/Notifications/Notifications';
// import sendMail from '/imports/startup/server/email';
import sendEmail from '/imports/modules/server/send-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';
import processNotif from '/imports/modules/server/notificationsProcess.js';
import { isMailServerMaster } from '/imports/startup/server/email';
Meteor.startup(() => {
// https://stackoverflow.com/questions/1199352/smart-way-to-shorten-long-strings-with-javascript
function truncate(n, useWordBoundary) {
if (this.length <= n) {
return this;
}
const subString = this.substr(0, n - 1);
return `${useWordBoundary ?
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}`
if (isMailServerMaster) {
Notifications.find().observe({
added: function notifAdded(notif) {
processNotif(notif);
},
changed: function notifChanged(updatedNotif) { // , oldNotif) {
processNotif(updatedNotif);
}
});
}
/*
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

@ -0,0 +1,46 @@
/* global WebApp */
import { Meteor } from 'meteor/meteor';
import prerenderIO from 'prerender-node';
// NOTE: Fetch as Google needs the /?_escaped_fragment_=
// https://webmasters.stackexchange.com/questions/72819/fetch-as-google-doesnt-find-the-html-snapshot-for-my-ajax-content
// https://forums.meteor.com/t/any-guide-for-prerender-node-and-your-own-server-on-meteor/22054/9
const settings = Meteor.settings.PrerenderIO;
// https://github.com/dferber90/meteor-prerender/blob/master/server/prerender.js
const token = process.env.PRERENDERIO_TOKEN || (settings && settings.token);
const protocol = process.env.PRERENDERIO_PROTOCOL || (settings && settings.protocol);
// service url (support `prerenderServiceUrl` (for historical reasons) and `serviceUrl`)
let serviceUrl = settings && (settings.prerenderServiceUrl || settings.serviceUrl);
serviceUrl = process.env.PRERENDERIO_SERVICE_URL || serviceUrl;
Meteor.startup(() => {
/* TODO if (!__meteor_runtime_config__.ROOT_URL.match(/www|stg|app/)) return; */
prerenderIO.set('prerenderToken', token);
if (serviceUrl) prerenderIO.set('prerenderServiceUrl', serviceUrl);
prerenderIO.set('prerenderToken', token);
if (protocol) prerenderIO.set('protocol', protocol);
prerenderIO.set('beforeRender', (req, done) => {
if (Meteor.isDevelopment) console.log('\nprerender before', req.headers, '\n\n');
/* This method is intended to be used for caching, but could be used to save analytics or anything else you need to do for each crawler request. If you return a string from beforeRender, the middleware will serve that to the crawler (with status 200) instead of making a request to the prerender service. If you return an object the middleware will look for a status and body property (defaulting to 200 and "" respectively) and serve those instead. */
done();
});
prerenderIO.set('afterRender', (err, req, prerender_res) => {
/* This method is intended to be used for caching, but could be used to save analytics or anything else you need to do for each crawler request. This method is a noop and is called after the prerender service returns HTML. */
if (err) {
console.log('prerenderio error', err);
return;
}
if (Meteor.isDevelopment) console.log('prerender after', req.url, '\nheaders:', req.headers, '\nres complete:', prerender_res.complete, prerender_res.statusCode, prerender_res.statusMessage, '\nres headers:', prerender_res.headers, '\nres body', prerender_res);
});
WebApp.rawConnectHandlers.use(prerenderIO);
// console.log('\nprerender service:', settings);
});

View file

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

View file

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

View file

@ -0,0 +1,44 @@
/* global sitemaps Comments */
/* eslint-disable import/no-absolute-path */
import Fires from '/imports/api/Fires/Fires';
// import Comments from '/imports/api/Comments/Comments';
const firesMapEnabled = false;
sitemaps.add('/sitemap.xml', () => {
const today = new Date();
const out = [];
out.push({ page: '/fires', lastmod: today });
out.push({ page: '/login', lastmod: today });
out.push({ page: '/signup', lastmod: today });
out.push({ page: '/recover-password', lastmod: today });
out.push({ page: '/credits', lastmod: today });
out.push({ page: '/terms', lastmod: today });
out.push({ page: '/license', lastmod: today });
out.push({ page: '/privacy', lastmod: today });
out.push({ page: '/about', lastmod: today });
// When user has public page
/* const users = Meteor.users.find().fetch();
_.each(users, function(user) {
out.push({
page: '/persona/' + user.username,
lastmod: user.updatedAt
});
}); */
if (firesMapEnabled) {
Fires.find({}, { limit: 100, sort: { createdAt: -1 } }).fetch().forEach((fire) => {
// Search the last comment of tha fire
const lastComment = Comments.getCollection().findOne({ referenceId: `fire-${fire._id}` }, { sort: { createdAt: -1 } });
out.push({
page: `/fire/archive/${fire._id._str}`,
lastmod: lastComment ? lastComment.createdAt : fire.updatedAt
});
});
}
return out;
});

View file

@ -0,0 +1,123 @@
/* eslint-disable import/no-absolute-path */
import { Meteor } from 'meteor/meteor';
import Subscriptions from '/imports/api/Subscriptions/Subscriptions';
import SiteSettings from '/imports/api/SiteSettings/SiteSettings';
import Perlin from 'loms.perlin';
import './leaflet-workaround';
import L from 'leaflet';
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
Meteor.startup(() => {
if (!isMailServerMaster) {
console.log('We only process subsUnion in master');
return;
}
const debug = true; // Meteor.isDevelopment;
Perlin.seed(Math.random());
const addNoisy = (osub) => {
const sub = osub;
let lat = Math.round(sub.location.lat * 10) / 10;
let lon = Math.round(sub.location.lon * 10) / 10;
const noiseBase = Perlin.perlin2(lat, lon);
const noise = Math.abs(noiseBase / 3);
lat += noise;
lon += noise;
sub.location.lat = lat;
sub.location.lon = lon;
sub.distance += noiseBase;
return sub;
};
const noNoisy = sub => sub;
const process = (isPublic) => {
const subscribers = Subscriptions.find().fetch();
const result = calcUnion(L, subscribers, isPublic ? addNoisy : noNoisy, true);
const union = result[0];
const bounds = result[1];
const publicl = isPublic ? 'public' : 'private';
const Publicl = publicl.replace(/\b\w/g, l => l.toUpperCase());
if (typeof union === 'object') {
const unionSet = {
$set: {
name: `subs-${publicl}-union`,
value: JSON.stringify(union),
isPublic,
description: `${Publicl} subscriptions union`,
type: 'string'
}
};
const boundsSet = {
$set: {
name: `subs-${publicl}-union-bounds`,
value: JSON.stringify(bounds),
isPublic,
description: `${Publicl} subscriptions union bounds`,
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:
// https://stackoverflow.com/questions/10827812/what-is-the-length-maximum-for-a-string-data-type-in-mongodb-used-with-ruby
SiteSettings.upsert({ name: `subs-${publicl}-union` }, unionSet, { multi: false });
SiteSettings.upsert({ name: `subs-${publicl}-union-bounds` }, boundsSet, { multi: false });
SiteSettings.upsert({ name: 'subs-union-count' }, sizeSet, { multi: false });
if (debug) console.log(`${Publicl} subscription union calculated`);
} else {
console.log('Subscription union failed!');
}
};
// At startup, we check if it's necessary to calc subscriptions union again
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({
added: function newSubAdded() { // doc) {
if (debug) console.log('Subs added so recreate union');
process(true);
process(false);
}
});
Subscriptions.find().observe({
changed: function subsChanged() { // updatedDoc, oldDoc) {
if (debug) console.log('Subs changed so recreate union');
process(true);
process(false);
},
removed: function subsRemoved() { // oldDoc) {
if (debug) console.log('Subs removed so recreate union');
process(true);
process(false);
}
});
});

View file

@ -1,9 +1,12 @@
/* eslint-disable import/no-absolute-path */
import React from 'react';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';
import { LinkContainer } from 'react-router-bootstrap';
/* import { Nav, NavDropdown } from 'react-bootstrap'; */
import { translate, Trans } from 'react-i18next';
import { testId } from '/imports/ui/components/Utils/TestUtils';
/*
FIXME:
navitem needs a nav-link class but does not works
@ -22,10 +25,10 @@ const AuthenticatedNavigation = ({ name, history, props }) => (
{/* <LinkContainer className="nav-item" anchorClassName="nav-link" to="/subscriptions">
<NavItem eventKey={5} href="/subscriptions"><Trans>Mis alertas</Trans></NavItem>
</LinkContainer> */}
<LinkContainer className="nav-item" anchorClassName="nav-link" to="/profile">
<LinkContainer id={testId('profile')} className="nav-item" anchorClassName="nav-link" to="/profile">
<NavItem eventKey={5.1} href="/profile">{name}</NavItem>
</LinkContainer>
<LinkContainer className="nav-item" anchorClassName="nav-link" to="/logout">
<LinkContainer id={testId('logout')} className="nav-item" anchorClassName="nav-link" to="/logout">
<NavItem eventKey={5.2} onClick={() => history.push('/logout')} href="/logout"><Trans i18nKey="Cerrar sesión">Cerrar sesión</Trans></NavItem>
</LinkContainer>
{/* </Nav> */}

View file

@ -5,6 +5,8 @@ import { Tracker } from 'meteor/tracker';
import { ReactiveVar } from 'meteor/reactive-var';
import { Button } from 'react-bootstrap';
import { translate } from 'react-i18next';
import { Bert } from 'meteor/themeteorchef:bert';
import './CenterInMyPosition.scss';
class CenterInMyPosition extends React.Component {
@ -14,7 +16,6 @@ class CenterInMyPosition extends React.Component {
// only with SSL:
// https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/getCurrentPosition
console.log('Center');
// https://stackoverflow.com/questions/31608579/somethings-wrong-with-my-meteor-geolocation-functions
const userGeoLocation = new ReactiveVar(null);
const self = this;
@ -27,8 +28,17 @@ class CenterInMyPosition extends React.Component {
zoom: 11
};
self.props.onClick(viewport);
// console.log(viewport);
// self.onViewportChanged(viewport);
computation.stop();
}
// https://developer.mozilla.org/en-US/docs/Web/API/PositionError)
const error = Geolocation.error();
if (error) {
const cod = error.code;
// window.alert(cod);
if (cod === 1) Bert.alert(self.props.t('geo-not-perms-error'), 'danger');
else if (cod === 2) Bert.alert(self.props.t('geo-not-avail-error'), 'danger');
else if (cod === 3) Bert.alert(self.props.t('geo-not-timeout-error'), 'danger');
else Bert.alert(error.message, 'danger');
computation.stop();
}
});
@ -38,8 +48,8 @@ class CenterInMyPosition extends React.Component {
const { onlyIcon, t } = this.props;
const msg = t('Centrar en tu ubicación');
return (
<Button bsStyle="default" onClick={() => this.onClick()}>
<i className="icons icon-target" title={!onlyIcon ? msg : ''} />{!onlyIcon ? msg : ''}
<Button bsStyle="default" title={msg} onClick={() => this.onClick()}>
<i className="fa fa-crosshairs" />{!onlyIcon ? ` ${msg}` : ''}
</Button>);
}
}

View file

@ -54,7 +54,7 @@ class DistanceSlider extends React.Component {
render() {
return (
<div className="dist-slider">
<p><Trans parent="span">¿A que distancia a la redonda quieres recibir notificaciones?</Trans></p>
<p><Trans parent="span">¿A que distancia a la redonda quieres recibir notificaciones de fuegos?</Trans></p>
<Slider
min={1}
max={105}

View file

@ -4,9 +4,7 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { Row, Col } from 'react-bootstrap';
import { withTracker } from 'meteor/react-meteor-data';
import ravenLogger from '/imports/startup/client/ravenLogger';
import './ErrorBoundary.scss';
@ -14,7 +12,6 @@ import './ErrorBoundary.scss';
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.t = props.t;
this.state = { hasError: false };
}
@ -22,38 +19,48 @@ class ErrorBoundary extends Component {
// Display fallback UI
this.setState({ hasError: true });
// You can also log the error to an error reporting service
ravenLogger.log(error);
ravenLogger.log(info);
// console.log(`typeof error ${typeof error}`);
// console.log(`typeof info ${typeof info}`);
ravenLogger.log(error, info);
// ravenLogger.log(info);
}
render() {
const {
appName, title, subTitle, children
} = this.props;
if (this.state.hasError) {
// You can render any custom fallback UI
return (
<div className="error-boundary">
<Row className="align-items-center justify-content-center">
<Col xs={12} sm={6} md={5} lg={4}>
<h1 className="page-header">{this.t('AppNameFull')}</h1>
<h4 className="page-header">{this.t('general-error-title')}</h4>
<Row>
{this.t('general-error-description')}
</Row>
<h1 className="page-header">{appName}</h1>
<h4 className="page-header">{title}</h4>
<p>{subTitle}</p>
</Col>
</Row>
</div>
);
}
return this.props.children;
return children;
}
}
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 = {
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 => ({
// props.something
}))(ErrorBoundary));
export default ErrorBoundary;

View file

@ -2,9 +2,9 @@
background-image: url('/error-background.png');
height: 100%;
min-height: 55vh;
margin-top: 40px;
margin: 40px 10px 10px 10px;
}
.error-boundary > div > div > h4 {
line-height: 1.5em;
line-height: 1.3em;
}

View file

@ -0,0 +1,141 @@
/* eslint-disable react/jsx-indent-props */
/* eslint-disable import/no-absolute-path */
/* eslint-disable import/no-absolute-path */
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Meteor } from 'meteor/meteor';
import { translate } from 'react-i18next';
import { FormGroup, Button, FormControl } from 'react-bootstrap';
import { Bert } from 'meteor/themeteorchef:bert';
import { withTracker } from 'meteor/react-meteor-data';
import { isHome } from '/imports/ui/components/Utils/location';
import { testId } from '/imports/ui/components/Utils/TestUtils';
import validate from '/imports/modules/validate';
import './Feedback.scss';
class Feedback extends Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.onTabClick = this.onTabClick.bind(this);
}
componentDidMount() {
const component = this;
const { t } = this.props;
validate(component.form, {
rules: {
email: {
required: true,
email: true
},
feedbackText: {
required: true
}
},
messages: {
feedbackText: {
required: t('Por favor, escribe aquí tu feedback...')
},
email: {
required: t('Tu correo'),
email: t('¿Es correcto este correo?')
}
},
submitHandler() { component.handleSubmit(); }
});
}
onTabClick() {
$('#feedback-form').toggle('slide');
}
handleSubmit() {
const email = this.email.value.trim();
const feedbackText = this.feedbackText.value.trim();
Meteor.call('send-feedback', email, feedbackText, (error) => {
if (error) {
Bert.alert(error.reason, 'danger');
} else {
this.form.reset();
Bert.alert('Feedback recibido, gracias...', 'success');
this.onTabClick();
}
});
}
render() {
// console.log(`Render Feedback because isHome ${this.props.isHome}, email: '${this.props.emailAddress}'`);
const disabled = this.props.emailVerified && this.props.emailAddress;
const { t } = this.props;
return (
<div>
{ !this.props.isHome &&
<div id="feedback">
<div id="feedback-form" ref={formdiv => (this.formdiv = formdiv)} style={{ display: 'none' }} className="card">
<form
ref={form => (this.form = form)}
className="form card-body"
onSubmit={event => event.preventDefault()}
>
<FormGroup controlId="formEmail">
<input
id={testId('emailInput')}
onChange={this.handleChange}
name="email"
className="form-control"
ref={email => (this.email = email)}
placeholder={t('Tu correo')}
key={disabled ? 'disabledEmail' : 'enabledEmail'}
disabled={disabled}
defaultValue={disabled ? this.props.emailAddress : ''}
type="email"
/>
</FormGroup>
<FormGroup
controlId="formFeedback"
>
<textarea
id={testId('feedbackTextarea')}
className="form-control"
name="feedbackText"
ref={feedbackText => (this.feedbackText = feedbackText)}
placeholder={t('Por favor, escribe aquí tu feedback...')}
rows="6"
/>
<FormControl.Feedback />
</FormGroup>
<Button type="submit" bsStyle="success" className="float-right" id={testId('sendFeedbackBtn')} >
{t('Enviar')}
</Button>
</form>
</div>
<div
id="feedback-tab"
onClick={(event) => { this.onTabClick(event); }}
>
{t('Feedback')}
</div>
</div>
}
</div>
);
}
}
Feedback.propTypes = {
t: PropTypes.func.isRequired,
emailAddress: PropTypes.string,
emailVerified: PropTypes.bool.isRequired,
isHome: PropTypes.bool.isRequired
};
export default translate()(withTracker(props => ({
emailAddress: props.emailAddress,
emailVerified: props.emailVerified,
isHome: isHome()
}))(Feedback));

View file

@ -0,0 +1,71 @@
/* From: https://github.com/spektom/bootstrap-feedback-form */
@import '../../stylesheets/mixins';
#feedback {
position: fixed;
z-index: 1000;
left: 0;
bottom: 0;
height: 350px;
margin-left: -3px;
margin-bottom: 10px;
font-size: 12px;
}
@include breakpoint(tablet) {
#feedback {
height: 400px;
}
}
#feedback-form {
float: left;
width: 400px;
height: 100%;
z-index: 1000;
padding-left: 5px;
padding-right: 10px;
background-clip: padding-box;
border: 1px solid rgba(0,0,0,.2);
border-radius: 0px;
box-shadow: 0 5px 10px rgba(0,0,0,.2);
}
@include breakpoint(mobile) {
#feedback-form {
width: 270px;
}
}
#feedback-tab {
float: right;
color: #fff;
font-size: 12px;
cursor: pointer;
text-align: center;
width: 80px;
height: 24px;
background-color: rgba(0,0,0,0.5);
margin-top: 60px;
margin-left: -28px;
padding-top: 3px;
-webkit-border-top-left-radius: 3px;
-webkit-border-top-right-radius: 3px;
-moz-border-radius-topleft: 3px;
-moz-border-radius-topright: 3px;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
-webkit-transform: rotate(90deg);
-moz-transform: rotate(90deg);
-ms-transform: rotate(90deg);
-o-transform: rotate(90deg);
transform: rotate(90deg);
}
#feedback-tab:hover {
background-color: rgba(0,0,0,0.4);
}
#feedback-form textarea {
resize: none;
}

View file

@ -1,8 +1,11 @@
/* eslint-disable import/no-absolute-path */
import React from 'react';
import { year } from '@cleverbeagle/dates';
import { Link } from 'react-router-dom';
import { Grid } from 'react-bootstrap';
import { translate } from 'react-i18next';
import { testId } from '/imports/ui/components/Utils/TestUtils';
import './Footer.scss';
@ -21,19 +24,25 @@ const Footer = (props) => {
<ul className="pull-right">
<li>
<Link to="/terms">
<span className="d-none d-md-inline">{t('Términos de Servicio')}</span>
<span className="d-md-none">{t('Términos')}</span>
<Link id={testId('about')} to="/about">
<span className="d-none d-lg-inline">{t('Sobre nosotr@s')}</span>
<span className="d-lg-none">{t('Nosotr@s')}</span>
</Link>
</li>
<li>
<Link to="/privacy">
<span className="d-none d-md-inline">{t('Política de Privacidad')}</span>
<span className="d-md-none">{t('Privacidad')}</span>
<Link id={testId('tos')} to="/terms">
<span className="d-none d-lg-inline">{t('Términos de Servicio')}</span>
<span className="d-lg-none" />
</Link>
</li>
<li><span className="d-none d-md-inline"><Link to="/license">{t('Licencia')}</Link></span></li>
<li><span className="d-none d-md-inline"><Link to="/credits">{t('Créditos')}</Link></span></li>
<li>
<Link id={testId('privacy')} to="/privacy">
<span className="d-none d-lg-inline">{t('Política de Privacidad')}</span>
<span className="d-lg-none">{t('Privacidad')}</span>
</Link>
</li>
<li><span className="d-none d-md-inline"><Link id={testId('license')} to="/license">{t('Licencia')}</Link></span></li>
<li><span className="d-none d-md-inline"><Link id={testId('credits')} to="/credits">{t('Créditos')}</Link></span></li>
</ul>
</Grid>
</div>

View file

@ -63,7 +63,10 @@ body {
}
@include breakpoint(tablet) {
.Footer ul li:first-child, .Footer ul li:nth-child(2) {
.Footer ul li:first-child,
.Footer ul li:nth-child(2),
.Footer ul li:nth-child(3),
.Footer ul li:nth-child(4){
margin-right: 15px;
}
}
@ -79,4 +82,14 @@ body {
.Footer, .Footer p {
font-size: 13px;
}
}
.Footer {
line-height: 1.8em;
}
.Footer ul li:first-child,
.Footer ul li:nth-child(2),
.Footer ul li:nth-child(3) {
margin-right: 7px;
}
}

View file

@ -5,6 +5,10 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withTracker } from 'meteor/react-meteor-data';
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 {
constructor(props) {
@ -27,26 +31,39 @@ class FromNow extends Component {
return !(nextState.when === this.state.when);
}
thatIs() {
return this.props.t('esDecirTalDia', { date: dateLongFormat(this.props.whenDate) });
}
render() {
console.log(`Render from now ${this.state.when}`);
return (
<span>{this.state.when}</span>
<span data-toggle="tooltip" className="from-now" title={this.thatIs()}>{this.state.when}</span>
);
}
}
FromNow.propTypes = {
when: PropTypes.string
t: PropTypes.func.isRequired,
when: PropTypes.string,
whenDate: PropTypes.instanceOf(Date)
};
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;
return {
when: whenDate ? Chronos.moment(whenDate).fromNow() : null
when: whenDate ? Chronos.moment(whenDate).fromNow() : null,
whenDate
};
})(FromNow);
export default FromNowContainer;
})(FromNow));

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