Compare commits
102 commits
2.7.22
...
tcef-maste
| Author | SHA1 | Date | |
|---|---|---|---|
| 39415808bf | |||
| 9d92ec4f88 | |||
| 2a31435135 | |||
| 8281fa3427 | |||
|
|
ee7af168d6 | ||
|
|
4cbbe65fa3 | ||
|
|
b9457fe141 | ||
|
|
c4cb52abaf | ||
|
|
423dbc5ed9 | ||
|
|
1fdf86dd47 | ||
|
|
5f9a84dfd7 | ||
|
|
564b9e8ead | ||
|
|
df51368d50 | ||
|
|
0f6dd21541 | ||
|
|
8903c765e3 | ||
|
|
266707052c | ||
|
|
3f8bc3666b | ||
|
|
6abe668740 | ||
|
|
7b5f17fbe8 | ||
|
|
8b598fab7c | ||
|
|
cb7aeda4b8 | ||
|
|
181f488263 | ||
|
|
e96e6ee8f7 | ||
|
|
df47bef794 | ||
|
|
3ea653552e | ||
|
|
6063437df0 | ||
|
|
cdc3bbdd5b | ||
|
|
33c68001c2 | ||
|
|
53a7c3be21 | ||
|
|
3d059287ae | ||
|
|
a208eb7ea1 | ||
|
|
c4b2f8cd73 | ||
|
|
f9febb4415 | ||
|
|
f5665a56a8 | ||
|
|
4d7938220b | ||
|
|
829fae4f07 | ||
|
|
e223ddb643 | ||
|
|
dc5e38f8d5 | ||
|
|
ed08563cad | ||
|
|
9a31ab2457 | ||
|
|
f608fc4d28 | ||
|
|
6f683ebaa2 | ||
|
|
8e729a6939 | ||
|
|
ffef42d19a | ||
|
|
3defbb01a2 | ||
|
|
bbdc53bda1 | ||
|
|
8a741dc729 | ||
|
|
15ed7e917a | ||
|
|
759797e412 | ||
|
|
5e6271bb88 | ||
|
|
5429dfbc04 | ||
|
|
9556bf4e60 | ||
|
|
d4be8c7c46 | ||
|
|
cfe6c2f570 | ||
|
|
20359a79e3 | ||
|
|
8780edc832 | ||
|
|
dc0df7cf32 | ||
|
|
bf926514c9 | ||
|
|
47e9c18607 | ||
|
|
5642b106d4 | ||
|
|
5b3d86e6a5 | ||
|
|
b8ab47e867 | ||
|
|
d980a31c05 | ||
|
|
714cc8610a | ||
|
|
a48da85ebe | ||
|
|
1de7e4a7b3 | ||
|
|
eebbdda064 | ||
|
|
d551dea425 | ||
|
|
9dbd97009b | ||
|
|
ec0edf4475 | ||
|
|
5e5ecb69b8 | ||
|
|
e28886e031 | ||
|
|
eefa586007 | ||
|
|
3e5700f648 | ||
|
|
37e42b70f1 | ||
|
|
12bf1ef09d | ||
|
|
783967c0e1 | ||
|
|
b54707f2dc | ||
|
|
b53e5d0e36 | ||
|
|
245619f4e4 | ||
|
|
391b8935be | ||
|
|
d30385b49e | ||
|
|
bea2703f2f | ||
|
|
e36dda74db | ||
|
|
f144c986b7 | ||
|
|
9502ea3604 | ||
|
|
22534ddb7e | ||
|
|
0f0983bcf5 | ||
|
|
b7ebb9a28c | ||
|
|
f65c1ee614 | ||
|
|
37a24d288a | ||
|
|
b7930eeb0e | ||
|
|
2d19778553 | ||
|
|
0a8d59bc53 | ||
|
|
9d68b1e6c1 | ||
|
|
53c81e4f15 | ||
|
|
b4afa820ab | ||
|
|
6e82e2813b | ||
|
|
03acf8ba65 | ||
|
|
88a3eb9639 | ||
|
|
032f6939f6 | ||
|
|
984ce8531d |
8
.gitignore
vendored
|
|
@ -9,3 +9,11 @@ npm-debug.log
|
||||||
.deploy/
|
.deploy/
|
||||||
public/locales/undefined/
|
public/locales/undefined/
|
||||||
.deploy-staging
|
.deploy-staging
|
||||||
|
.screenshots
|
||||||
|
.vscode
|
||||||
|
output.json
|
||||||
|
|
||||||
|
# fase-0 higiene: variantes de settings con secretos (no versionar)
|
||||||
|
settings-production-*.json
|
||||||
|
settings-tests.json
|
||||||
|
*.json~
|
||||||
|
|
|
||||||
3
.meteorignore
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Don't reload meteor server when we modify or do test
|
||||||
|
cucumber
|
||||||
|
output.json
|
||||||
21
README.md
|
|
@ -8,7 +8,12 @@ This is web service that notifies about fires detected in an area of your intere
|
||||||
|
|
||||||
Prerrequisites: a running meteor (we share the `fuegos` database with the telegram bot).
|
Prerrequisites: a running meteor (we share the `fuegos` database with the telegram bot).
|
||||||
|
|
||||||
Install `meteor` and run `npm start`
|
Install `meteor` and run `meteor npm install` and `npm start`
|
||||||
|
|
||||||
|
Some other development deps (in debian & ubuntu):
|
||||||
|
```
|
||||||
|
apt-get install libcairo2-dev libjpeg-dev libgif-dev pkg-config
|
||||||
|
```
|
||||||
|
|
||||||
### GeoIP
|
### GeoIP
|
||||||
|
|
||||||
|
|
@ -24,18 +29,23 @@ More platforms and services in the future...
|
||||||
|
|
||||||
We do tests via:
|
We do tests via:
|
||||||
```
|
```
|
||||||
TEST_WATCH=1 MONGO_URL=mongodb://localhost:27017/fuegostest meteor --settings settings-development.json test --driver-package meteortesting:mocha --port 3010
|
TEST_PORT=3000 TEST_WATCH=1 TEST_CLIENT=0 MONGO_URL=mongodb://localhost:27017/fuegostest meteor --settings settings-development.json test --driver-package meteortesting:mocha --port 3010
|
||||||
|
|
||||||
# and
|
# and
|
||||||
|
|
||||||
chimp --watch --ddp=http://localhost:3000 --path=cucumber
|
node_modules/.bin/chimp --watch --ddp=http://localhost:3000 --path=cucumber
|
||||||
|
|
||||||
# and
|
# and
|
||||||
|
|
||||||
chimp --ddp=http://localhost:3000 --path=cucumber
|
node_modules/.bin/chimp --ddp=http://localhost:3000 --path=cucumber
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### FAQ & Troubleshooting
|
||||||
|
|
||||||
|
**Q** - I get something like `(...) /node_modules/fibers/future.js:280 (...) Error: Could not locate the bindings file.` What can I do?
|
||||||
|
**A** - Try `meteor npm rebuild`
|
||||||
|
|
||||||
## Data source acknowledgements
|
## Data source acknowledgements
|
||||||
|
|
||||||
*We acknowledge the use of data and imagery from LANCE FIRMS operated by the NASA/GSFC/Earth Science Data and Information System (ESDIS) with funding provided by NASA/HQ*.
|
*We acknowledge the use of data and imagery from LANCE FIRMS operated by the NASA/GSFC/Earth Science Data and Information System (ESDIS) with funding provided by NASA/HQ*.
|
||||||
|
|
@ -46,4 +56,5 @@ GNU APLv3. See our [LICENSE](https://github.com/comunes/todos-contra-el-fuego-we
|
||||||
|
|
||||||
## Thanks & other acknowlegments
|
## Thanks & other acknowlegments
|
||||||
|
|
||||||
(...)
|
Thanks indeed to:
|
||||||
|
- Lupe Bao Reixa for Galician translation
|
||||||
|
|
|
||||||
|
|
@ -1 +1,4 @@
|
||||||
import '../imports/startup/client';
|
import '../imports/startup/client';
|
||||||
|
|
||||||
|
// https://github.com/GoogleChrome/rendertron#rendering-budget-timeout
|
||||||
|
window.renderComplete = false;
|
||||||
|
|
|
||||||
29
cucumber/chimp-config.js
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
@watch
|
|
||||||
Feature: Allow users to give feedback
|
Feature: Allow users to give feedback
|
||||||
|
|
||||||
As a user (authenticated or not)
|
As a user (authenticated or not)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
Feature: Test all secundary pages
|
Feature: Test all secundary pages
|
||||||
|
|
||||||
|
Background:
|
||||||
|
Given I am on the home page
|
||||||
|
|
||||||
Scenario: Check that all secondary pages work well
|
Scenario: Check that all secondary pages work well
|
||||||
Given a list of page ids and contents
|
Given a list of page ids and contents
|
||||||
| license | License |
|
| license | License |
|
||||||
|
|
@ -10,8 +13,9 @@ Feature: Test all secundary pages
|
||||||
| about | About |
|
| about | About |
|
||||||
Then I check that all pages works properly
|
Then I check that all pages works properly
|
||||||
|
|
||||||
Scenario: Check that all secondary pages work well
|
Scenario: Check that all secondary pages work well when not logged
|
||||||
Given a list of page urls and contents
|
Given I logout if logged
|
||||||
|
And a list of page urls and contents
|
||||||
| login | Login | true |
|
| login | Login | true |
|
||||||
| signup | Sign Up | true |
|
| signup | Sign Up | true |
|
||||||
| fires | Fires | true |
|
| fires | Fires | true |
|
||||||
|
|
@ -21,12 +25,25 @@ Feature: Test all secundary pages
|
||||||
| fire/inexistent | This page doesn't exist | false |
|
| fire/inexistent | This page doesn't exist | false |
|
||||||
| fire/Fe26.2**1a0361ed0384f741403682e26b4bbc3850bee24e775da13c9782b22365ea895f*r_3gmVad5vzkeyqPpo6UcA*1s5fFz3iDGKTYP2RCvXMshof00QCHf4ErDl9K2dxoX0u-J-t6scyOWG8pGp3ehg_FfEtyR_kYcEKU3rE0jaSlZbD09TIvhiIJeS3C6Uc8YD-rit0XBrgsVKfYSxKzTRoOYiYFJ8JYd298hMtfiASePjS05Z58hhicyCcJYYRlarqDScG3LiVY3lL5y2nfcdIMNuSjCiKOJWuMkxwd9nR1UHMudLl0hEoy56mPdnHpDYtP9IYUlIOk1LlWBxcmHKifbXeqHu94p8j13Kk20dh2R49Hw3KsSoE9UbWmGQA9wAZXT82301i3rGF5GPAKjlTlRYcWisQurnPwHSVmx3DhUdiYwKGxt4KeaM5QVI4BE9octvE41OOprB_-Il105diQEh2Y9vdvX51ZVWIRfCboICPM6rJb0Oin7U7F1iM-oD_5s3DGnelfM5LGBcKwiB5paMo5M5vdBMaO-zR216cW9yGVXw9IZqHx8xDQWnoHAZjt8NLHeiGF2QOmIGtEUH7qnwhGpkcvszajmAZzR8saZgoH1qfBfvpVA41YfV14gU**4d030f05e23ad75409cebc609107467fc60be5077d52cf041087cd024fc4dc45*GY97aGFc1MyAsoO3Qxqtgwk9j-MbAPdEGBmEHq6r8VU | Additional information | false |
|
| fire/Fe26.2**1a0361ed0384f741403682e26b4bbc3850bee24e775da13c9782b22365ea895f*r_3gmVad5vzkeyqPpo6UcA*1s5fFz3iDGKTYP2RCvXMshof00QCHf4ErDl9K2dxoX0u-J-t6scyOWG8pGp3ehg_FfEtyR_kYcEKU3rE0jaSlZbD09TIvhiIJeS3C6Uc8YD-rit0XBrgsVKfYSxKzTRoOYiYFJ8JYd298hMtfiASePjS05Z58hhicyCcJYYRlarqDScG3LiVY3lL5y2nfcdIMNuSjCiKOJWuMkxwd9nR1UHMudLl0hEoy56mPdnHpDYtP9IYUlIOk1LlWBxcmHKifbXeqHu94p8j13Kk20dh2R49Hw3KsSoE9UbWmGQA9wAZXT82301i3rGF5GPAKjlTlRYcWisQurnPwHSVmx3DhUdiYwKGxt4KeaM5QVI4BE9octvE41OOprB_-Il105diQEh2Y9vdvX51ZVWIRfCboICPM6rJb0Oin7U7F1iM-oD_5s3DGnelfM5LGBcKwiB5paMo5M5vdBMaO-zR216cW9yGVXw9IZqHx8xDQWnoHAZjt8NLHeiGF2QOmIGtEUH7qnwhGpkcvszajmAZzR8saZgoH1qfBfvpVA41YfV14gU**4d030f05e23ad75409cebc609107467fc60be5077d52cf041087cd024fc4dc45*GY97aGFc1MyAsoO3Qxqtgwk9j-MbAPdEGBmEHq6r8VU | Additional information | false |
|
||||||
Then I check that all page urls works properly
|
Then I check that all page urls works properly
|
||||||
|
|
||||||
# And they are spiderable
|
# And they are spiderable
|
||||||
|
|
||||||
|
@watch
|
||||||
|
Scenario: Check that all secondary pages work well when logged
|
||||||
|
Given I have an account and I logged in
|
||||||
|
And a list of page urls and contents
|
||||||
|
| status | Status | false |
|
||||||
|
| fires | Fires | true |
|
||||||
|
| zones | Monitored | true |
|
||||||
|
| verify-email/something | Verify | true |
|
||||||
|
| fire/inexistent | This page doesn't exist | false |
|
||||||
|
| fire/Fe26.2**1a0361ed0384f741403682e26b4bbc3850bee24e775da13c9782b22365ea895f*r_3gmVad5vzkeyqPpo6UcA*1s5fFz3iDGKTYP2RCvXMshof00QCHf4ErDl9K2dxoX0u-J-t6scyOWG8pGp3ehg_FfEtyR_kYcEKU3rE0jaSlZbD09TIvhiIJeS3C6Uc8YD-rit0XBrgsVKfYSxKzTRoOYiYFJ8JYd298hMtfiASePjS05Z58hhicyCcJYYRlarqDScG3LiVY3lL5y2nfcdIMNuSjCiKOJWuMkxwd9nR1UHMudLl0hEoy56mPdnHpDYtP9IYUlIOk1LlWBxcmHKifbXeqHu94p8j13Kk20dh2R49Hw3KsSoE9UbWmGQA9wAZXT82301i3rGF5GPAKjlTlRYcWisQurnPwHSVmx3DhUdiYwKGxt4KeaM5QVI4BE9octvE41OOprB_-Il105diQEh2Y9vdvX51ZVWIRfCboICPM6rJb0Oin7U7F1iM-oD_5s3DGnelfM5LGBcKwiB5paMo5M5vdBMaO-zR216cW9yGVXw9IZqHx8xDQWnoHAZjt8NLHeiGF2QOmIGtEUH7qnwhGpkcvszajmAZzR8saZgoH1qfBfvpVA41YfV14gU**4d030f05e23ad75409cebc609107467fc60be5077d52cf041087cd024fc4dc45*GY97aGFc1MyAsoO3Qxqtgwk9j-MbAPdEGBmEHq6r8VU | Additional information | false |
|
||||||
|
Then I check that all page urls works properly
|
||||||
|
|
||||||
Scenario: Check that other non visible pages work well
|
Scenario: Check that other non visible pages work well
|
||||||
Given a list of non visible pages ids and contents
|
Given I logout if logged
|
||||||
| status | Status |
|
And a list of non visible pages ids and contents
|
||||||
| error | Upps |
|
| error | Upppps | false |
|
||||||
# FIXME: find a way to monitor json pages
|
# FIXME: find a way to monitor json pages
|
||||||
# | api/v1/status/last-fire-detected | updateAt |
|
# | api/v1/status/last-fire-detected | updateAt |
|
||||||
# | api/v1/status/last-fire-check | description |
|
# | api/v1/status/last-fire-check | description |
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ module.exports = function doSteps() {
|
||||||
client.waitForVisible('#feedback-tab', 10000);
|
client.waitForVisible('#feedback-tab', 10000);
|
||||||
client.click('#feedback-tab');
|
client.click('#feedback-tab');
|
||||||
client.waitForVisible('#feedback-form', 10000);
|
client.waitForVisible('#feedback-form', 10000);
|
||||||
|
client.waitForEnabled('input[name="email"]');
|
||||||
client.setValue('input[name="email"]', randomEmail());
|
client.setValue('input[name="email"]', randomEmail());
|
||||||
client.setValue('#feedbackTextarea', randomText(500));
|
client.setValue('#feedbackTextarea', randomText(500));
|
||||||
client.click('#sendFeedbackBtn');
|
client.click('#sendFeedbackBtn');
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,50 @@
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
module.exports = function () {
|
module.exports = function () {
|
||||||
|
const printLog = (type) => {
|
||||||
|
client.log(type).value.forEach((logItem) => {
|
||||||
|
const { source, message } = logItem;
|
||||||
|
if (type === 'browser' && source === 'console-api') {
|
||||||
|
// Clean messages of type:
|
||||||
|
// http://localhost:3000/app/app.js?hash=34da4258bd2caa4e14c7c0c20ba6396f309e7236 13325:18 "GMaps script just loaded"
|
||||||
|
// to get the last part
|
||||||
|
const cleanMessage = message.replace(/[^ ]{1,} [^ ]{1,} "(.*)"$/g, '$1');
|
||||||
|
console.log(`${logItem.level}: ${cleanMessage}`);
|
||||||
|
} else {
|
||||||
|
// Right now we don't need logs like:
|
||||||
|
// %cDownload the React DevTools for a better development experience: https://fb.me/react-devtools
|
||||||
|
// console.log(`${logItem.level}: ${message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
this.StepResult((stepResult) => {
|
||||||
|
if (stepResult.getStatus() === 'failed') {
|
||||||
|
// http://webdriver.io/api/protocol/log.html
|
||||||
|
console.log(`Step result: ${stepResult.getStatus()}`);
|
||||||
|
printLog('browser');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.Before(() => {
|
this.Before(() => {
|
||||||
// global.expect = require('@xolvio/jasmine-expect').expect;
|
// global.expect = require('@xolvio/jasmine-expect').expect;
|
||||||
|
|
||||||
/* Not used
|
// https://github.com/webdriverio/webdriverio/issues/1145
|
||||||
if (!this.initMyCmds) {
|
if (!this.initMyCmds) {
|
||||||
client.addCommand('waitForClickable', function elementClickable(selector, timeout) {
|
client.addCommand('waitForClickable', function elementClickable(selector, timeout) {
|
||||||
this.waitForVisible(selector, timeout);
|
this.waitForVisible(selector, timeout);
|
||||||
this.waitForEnabled(selector, timeout);
|
this.waitForEnabled(selector, timeout);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
client.addCommand('waitUntilText', function waitUntilText(selector, text, ms) {
|
||||||
|
ms = ms || 5000;
|
||||||
|
this.waitForVisible(selector);
|
||||||
|
const self = this;
|
||||||
|
this.waitUntil(() => self.getText(selector).includes(text), ms, `expected text in ${selector} be different after ${ms / 1000}s. Obtained: '${self.getText(selector)}' expected: '${text}'`);
|
||||||
|
});
|
||||||
|
|
||||||
this.initMyCmds = true;
|
this.initMyCmds = true;
|
||||||
} */
|
}
|
||||||
|
|
||||||
client.windowHandleSize({ width: 1500, height: 1000 });
|
client.windowHandleSize({ width: 1500, height: 1000 });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ let pass;
|
||||||
let email;
|
let email;
|
||||||
|
|
||||||
const setUserValues = (isEdit) => {
|
const setUserValues = (isEdit) => {
|
||||||
client.waitForVisible('input[name="firstName"]', 5000);
|
client.waitForVisible('input[name="firstName"]', 10000);
|
||||||
firstName = randomUsername();
|
firstName = randomUsername();
|
||||||
lastName = randomUsername();
|
lastName = randomUsername();
|
||||||
email = randomEmail();
|
email = randomEmail();
|
||||||
|
|
@ -32,31 +32,54 @@ module.exports = function doSteps(notos) {
|
||||||
goHome(client);
|
goHome(client);
|
||||||
});
|
});
|
||||||
|
|
||||||
function register() {
|
function waitNoBert() {
|
||||||
|
client.pause(2000);
|
||||||
|
if (client.isVisible('.bert-container')) {
|
||||||
|
// console.log('Removing bert alert');
|
||||||
|
client.click('.bert-container');
|
||||||
|
client.waitForVisible('.bert-container', 10000, true);
|
||||||
|
} else {
|
||||||
|
// console.log('No bert alert');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function logoutIfLogged() {
|
||||||
if (client.isVisible('#logout')) {
|
if (client.isVisible('#logout')) {
|
||||||
client.click('#logout');
|
client.click('#logout');
|
||||||
}
|
}
|
||||||
client.waitForVisible('#signup', 5000);
|
client.waitForVisible('#signup', 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function register() {
|
||||||
|
logoutIfLogged();
|
||||||
|
client.waitForVisible('#signup', 10000);
|
||||||
client.click('#signup');
|
client.click('#signup');
|
||||||
setUserValues();
|
setUserValues();
|
||||||
if (!notos) {
|
if (!notos) {
|
||||||
client.click('input[name="tos"]');
|
client.click('input[name="tos"]');
|
||||||
}
|
}
|
||||||
client.click('#signUpSubmit');
|
client.click('#signUpSubmit');
|
||||||
|
client.waitForVisible('#logout', 10000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.Given(/^I logout if logged$/, () => {
|
||||||
|
waitNoBert();
|
||||||
|
logoutIfLogged();
|
||||||
|
});
|
||||||
|
|
||||||
this.Given(/^I have an account and I logged in$/, () => {
|
this.Given(/^I have an account and I logged in$/, () => {
|
||||||
register();
|
register();
|
||||||
});
|
});
|
||||||
|
|
||||||
function checkName() {
|
function checkName() {
|
||||||
client.waitForVisible('#profile', 5000);
|
client.waitForVisible('#profile', 5000);
|
||||||
client.waitForText('#profile', firstName);
|
client.waitUntilText('#profile', firstName);
|
||||||
client.waitForText('#profile', lastName);
|
client.waitUntilText('#profile', lastName);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.Then(/^I should be logged in$/, () => {
|
this.Then(/^I should be logged in$/, () => {
|
||||||
client.waitUntil(() => client.isVisible('#logout') === true, 10000);
|
client.waitUntil(() => client.isVisible('#logout') === true, 10000);
|
||||||
|
client.waitUntil(() => client.isVisible('#profile') === true, 10000);
|
||||||
});
|
});
|
||||||
|
|
||||||
/* this.Given(/^I am signed out$/, () => {
|
/* this.Given(/^I am signed out$/, () => {
|
||||||
|
|
@ -64,29 +87,24 @@ module.exports = function doSteps(notos) {
|
||||||
* });
|
* });
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function closeAlert() {
|
|
||||||
// client.waitForVisible('.bert-alert', 5000);
|
|
||||||
// client.click('.bert-content');
|
|
||||||
client.waitUntil(() => client.isVisible('.bert-alert') === false, 10000);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.When(/^I sign out$/, () => {
|
this.When(/^I sign out$/, () => {
|
||||||
if (client.isVisible('.bert-alert')) {
|
waitNoBert();
|
||||||
client.waitUntil(() => client.isVisible('.bert-alert') === false, 10000);
|
client.waitForClickable('#logout', 10000);
|
||||||
}
|
// client.waitForVisible('#logout', 10000);
|
||||||
client.waitForVisible('#logout', 5000);
|
|
||||||
client.click('#logout');
|
client.click('#logout');
|
||||||
|
client.waitForVisible('#login', 10000);
|
||||||
});
|
});
|
||||||
|
|
||||||
function login(badpass) {
|
function login(badpass) {
|
||||||
|
waitNoBert();
|
||||||
client.waitForVisible('#login', 10000);
|
client.waitForVisible('#login', 10000);
|
||||||
client.waitUntil(() => client.isVisible('.bert-alert') === false, 10000);
|
|
||||||
client.click('#login');
|
client.click('#login');
|
||||||
client.waitForVisible('#loginSubmit', 5000);
|
client.waitForVisible('#loginSubmit', 5000);
|
||||||
client.setValue('input[name="emailAddress"]', email);
|
client.setValue('input[name="emailAddress"]', email);
|
||||||
client.setValue('input[name="password"]', badpass ? randomPassword() : pass);
|
client.setValue('input[name="password"]', badpass ? randomPassword() : pass);
|
||||||
client.click('#loginSubmit');
|
client.click('#loginSubmit');
|
||||||
closeAlert();
|
waitNoBert();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.When(/^I enter my email and password$/, () => {
|
this.When(/^I enter my email and password$/, () => {
|
||||||
|
|
@ -94,11 +112,13 @@ module.exports = function doSteps(notos) {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.Then(/^I can edit my profile$/, () => {
|
this.Then(/^I can edit my profile$/, () => {
|
||||||
client.waitForVisible('#profile', 5000);
|
waitNoBert();
|
||||||
|
client.waitForClickable('#profile', 10000);
|
||||||
client.click('#profile');
|
client.click('#profile');
|
||||||
|
client.pause(2000);
|
||||||
setUserValues(true);
|
setUserValues(true);
|
||||||
client.click('#profileSubmit');
|
client.click('#profileSubmit');
|
||||||
closeAlert();
|
waitNoBert();
|
||||||
checkName();
|
checkName();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -116,7 +136,7 @@ module.exports = function doSteps(notos) {
|
||||||
|
|
||||||
this.Then(/^I should be registered$/, () => {
|
this.Then(/^I should be registered$/, () => {
|
||||||
client.waitForVisible('.bert-alert', 10000, true);
|
client.waitForVisible('.bert-alert', 10000, true);
|
||||||
closeAlert();
|
waitNoBert();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.Given(/^I register with some name, password and email but without accept conditions$/, () => {
|
this.Given(/^I register with some name, password and email but without accept conditions$/, () => {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ module.exports = function () {
|
||||||
const content = pages[i][1];
|
const content = pages[i][1];
|
||||||
client.waitForVisible(id, 10000);
|
client.waitForVisible(id, 10000);
|
||||||
client.click(id);
|
client.click(id);
|
||||||
client.waitForText('#react-root', content);
|
client.waitUntilText('#react-root', content);
|
||||||
// https://jasmine.github.io/2.3/introduction.html#section-Expectations
|
// https://jasmine.github.io/2.3/introduction.html#section-Expectations
|
||||||
expect(client.getTitle()).toContain(pages[i][1]);
|
expect(client.getTitle()).toContain(pages[i][1]);
|
||||||
}
|
}
|
||||||
|
|
@ -26,7 +26,7 @@ module.exports = function () {
|
||||||
const content = pages[i][1];
|
const content = pages[i][1];
|
||||||
client.url(`${process.env.ROOT_URL}/${url}`);
|
client.url(`${process.env.ROOT_URL}/${url}`);
|
||||||
client.waitForVisible('#react-root', 10000);
|
client.waitForVisible('#react-root', 10000);
|
||||||
client.waitForText('#react-root', content);
|
client.waitUntilText('#react-root', content);
|
||||||
const checkTitle = pages[i][2] === 'true';
|
const checkTitle = pages[i][2] === 'true';
|
||||||
// https://jasmine.github.io/2.3/introduction.html#section-Expectations
|
// https://jasmine.github.io/2.3/introduction.html#section-Expectations
|
||||||
if (checkTitle) {
|
if (checkTitle) {
|
||||||
|
|
@ -41,10 +41,7 @@ module.exports = function () {
|
||||||
|
|
||||||
this.Then(/^I check that all non visible pages works properly$/, (callback) => {
|
this.Then(/^I check that all non visible pages works properly$/, (callback) => {
|
||||||
for (let i = 0; i < pages.length; i += 1) {
|
for (let i = 0; i < pages.length; i += 1) {
|
||||||
client.url(`${process.env.ROOT_URL}/${pages[i][0]}`);
|
processPageUrl(i);
|
||||||
const content = pages[i][1];
|
|
||||||
client.waitForVisible('#react-root', 10000);
|
|
||||||
client.waitForText('#react-root', content);
|
|
||||||
}
|
}
|
||||||
callback();
|
callback();
|
||||||
});
|
});
|
||||||
|
|
@ -77,7 +74,7 @@ module.exports = function () {
|
||||||
// Write code here that turns the phrase above into concrete actions
|
// Write code here that turns the phrase above into concrete actions
|
||||||
for (let i = 0; i < pages.length; i += 1) {
|
for (let i = 0; i < pages.length; i += 1) {
|
||||||
client.url(`${process.env.ROOT_URL}/${pages[i][0]}?_escaped_fragment_=`);
|
client.url(`${process.env.ROOT_URL}/${pages[i][0]}?_escaped_fragment_=`);
|
||||||
client.waitForText('#react-root', pages[i][1]);
|
client.waitUntilText('#react-root', pages[i][1]);
|
||||||
}
|
}
|
||||||
callback();
|
callback();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -247,7 +247,7 @@
|
||||||
inkscape:pageopacity="0.0"
|
inkscape:pageopacity="0.0"
|
||||||
inkscape:pageshadow="2"
|
inkscape:pageshadow="2"
|
||||||
inkscape:zoom="0.23559314"
|
inkscape:zoom="0.23559314"
|
||||||
inkscape:cx="-1825.0924"
|
inkscape:cx="-2890.4884"
|
||||||
inkscape:cy="-486.23766"
|
inkscape:cy="-486.23766"
|
||||||
inkscape:document-units="mm"
|
inkscape:document-units="mm"
|
||||||
inkscape:current-layer="layer1"
|
inkscape:current-layer="layer1"
|
||||||
|
|
@ -621,5 +621,39 @@
|
||||||
y="0"
|
y="0"
|
||||||
x="0" />
|
x="0" />
|
||||||
</g>
|
</g>
|
||||||
|
<g
|
||||||
|
transform="translate(176.48029,228.33241)"
|
||||||
|
id="g8543-0"
|
||||||
|
inkscape:export-filename="/home/vjrj/proyectos/dev/fires_flutter/images/logo-1024.png"
|
||||||
|
inkscape:export-xdpi="147.38"
|
||||||
|
inkscape:export-ydpi="147.38">
|
||||||
|
<rect
|
||||||
|
y="108.84225"
|
||||||
|
x="-419.07861"
|
||||||
|
height="176.48029"
|
||||||
|
width="176.48029"
|
||||||
|
id="rect17701-2"
|
||||||
|
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:none;stroke-width:0.26458332;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||||
|
<g
|
||||||
|
style="stroke-width:0.88457036"
|
||||||
|
inkscape:export-ydpi="4.0856881"
|
||||||
|
inkscape:export-xdpi="4.0856881"
|
||||||
|
inkscape:export-filename="/mnt/md1/proyectos/dev/todos-contra-el-fuego-web/public/fire-marker.png"
|
||||||
|
transform="matrix(1.1304923,0,0,1.1304923,-313.01185,70.713452)"
|
||||||
|
id="g22554-4-3">
|
||||||
|
<path
|
||||||
|
sodipodi:nodetypes="cszzcczasc"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path5329-5-7"
|
||||||
|
d="m -25.638382,38.048346 c 11.566313,13.887908 17.1215844,22.244854 13.751242,33.868326 -2.465387,8.50265 -8.895424,18.626534 -18.100172,25.532106 -9.204776,6.905572 -20.549951,13.517692 -24.263404,17.728802 -3.71348,4.21108 -9.835779,11.6046 -10.976769,21.82688 -2.725208,24.7073 15.038044,45.63926 39.702238,48.35952 11.289242,1.2311 26.6324564,-5.2645 34.2331684,-10.31973 C 16.308659,169.98901 28.520768,152.76263 31.570884,138.81099 35.854436,119.21732 35.065926,96.385127 23.626002,79.16853 12.946123,63.095807 3.8673424,50.775037 -25.638382,38.048346 Z"
|
||||||
|
style="fill:#d41300;fill-opacity:1;stroke:none;stroke-width:9.36170292;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||||
|
<path
|
||||||
|
sodipodi:nodetypes="cszzcczcsc"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path5331-3-5"
|
||||||
|
d="m -7.5198746,101.09834 c 4.018254,8.70077 3.821166,14.69136 0.49964,20.18233 -2.429696,4.01667 -7.1125824,8.31964 -12.7749834,10.59336 -5.662401,2.27373 -12.38086,4.1034 -14.860826,5.75067 -2.479966,1.64727 -6.630512,4.59399 -8.617877,9.67322 -4.786736,12.28103 1.27127,26.11038 13.530846,30.8887 5.613373,2.17993 11.515354,2.13146 17.442629,-0.0849 5.9272234,-2.21636 14.0405914,-9.44377 18.0234704,-15.42084 6.3490206,-9.59829 8.9531296,-23.76395 5.4565016,-34.15229 -3.2643496,-9.69812 -4.6224286,-12.3603 -18.6994006,-27.43025 z"
|
||||||
|
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:9.36170292;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 51 KiB |
|
|
@ -1,5 +1,6 @@
|
||||||
/* eslint-disable consistent-return */
|
/* eslint-disable consistent-return */
|
||||||
|
|
||||||
|
import { Meteor } from 'meteor/meteor';
|
||||||
import { Mongo } from 'meteor/mongo';
|
import { Mongo } from 'meteor/mongo';
|
||||||
import SimpleSchema from 'simpl-schema';
|
import SimpleSchema from 'simpl-schema';
|
||||||
import firesCommonSchema from '../Common/FiresSchema';
|
import firesCommonSchema from '../Common/FiresSchema';
|
||||||
|
|
@ -18,8 +19,11 @@ ActiveFires.deny({
|
||||||
remove: () => true
|
remove: () => true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const activeFiresSchema = firesCommonSchema;
|
||||||
|
|
||||||
ActiveFires.schema = new SimpleSchema(firesCommonSchema);
|
activeFiresSchema.fireUnion = { type: Meteor.Collection.ObjectID, optional: true, blackbox: true };
|
||||||
|
|
||||||
|
ActiveFires.schema = new SimpleSchema(activeFiresSchema);
|
||||||
|
|
||||||
ActiveFires.attachSchema(ActiveFires.schema);
|
ActiveFires.attachSchema(ActiveFires.schema);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,27 @@ const findFiresInRegion = (zone) => {
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const countFires = (regions, stringsToRemove) => {
|
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;
|
let total = 0;
|
||||||
const fireStats = {};
|
const fireStats = {};
|
||||||
|
|
||||||
|
|
@ -41,25 +61,9 @@ const countFires = (regions, stringsToRemove) => {
|
||||||
const fires = findFiresInRegion(region);
|
const fires = findFiresInRegion(region);
|
||||||
// TODO Also check neighbour fires (when better implementation of that part)
|
// TODO Also check neighbour fires (when better implementation of that part)
|
||||||
const initialCount = fires.count();
|
const initialCount = fires.count();
|
||||||
let count = initialCount;
|
if (initialCount > 0) {
|
||||||
if (count > 0) {
|
const unionCount = countRealFires(fires);
|
||||||
const realFires = [];
|
if (debug) console.log(`${regionName} initial: ${initialCount}, union calc: ${unionCount}`);
|
||||||
fires.forEach((fire) => {
|
|
||||||
const union = firesUnion([fire]);
|
|
||||||
const falsePos = whichAreFalsePositives(FalsePositives, union);
|
|
||||||
const industries = whichAreFalsePositives(Industries, union);
|
|
||||||
if (falsePos.count() === 0 && industries.count() === 0) {
|
|
||||||
realFires.push(fire);
|
|
||||||
} else {
|
|
||||||
count -= 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// group fires
|
|
||||||
const realUnion = firesUnion(realFires);
|
|
||||||
const unionCount = realUnion[0] &&
|
|
||||||
realUnion[0].geometry &&
|
|
||||||
realUnion[0].geometry.coordinates ? realUnion[0].geometry.coordinates.length : 0;
|
|
||||||
if (debug) console.log(`${regionName} initial: ${initialCount}, first calc: ${count} union calc: ${unionCount}`);
|
|
||||||
if (unionCount > 0) {
|
if (unionCount > 0) {
|
||||||
total += unionCount;
|
total += unionCount;
|
||||||
fireStats[regionName] = { count: unionCount, code: regionCode };
|
fireStats[regionName] = { count: unionCount, code: regionCode };
|
||||||
|
|
@ -72,4 +76,4 @@ const countFires = (regions, stringsToRemove) => {
|
||||||
return { total, fires: fireStats };
|
return { total, fires: fireStats };
|
||||||
};
|
};
|
||||||
|
|
||||||
export default countFires;
|
export default countFiresInRegions;
|
||||||
|
|
|
||||||
|
|
@ -31,12 +31,12 @@ const activefires = (northEastLng, northEastLat, southWestLng, southWestLat, wit
|
||||||
lat: 1,
|
lat: 1,
|
||||||
lon: 1,
|
lon: 1,
|
||||||
when: 1,
|
when: 1,
|
||||||
scan: 1
|
scan: 1,
|
||||||
|
track: 1
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// console.log(JSON.stringify(result));
|
if (Meteor.isDevelopment) console.log(`Active fires total: ${fires.count()}`);
|
||||||
// console.log(`Fires total: ${fires.count()}`);
|
|
||||||
|
|
||||||
if (withMarks && fires.fetch().length > 0) {
|
if (withMarks && fires.fetch().length > 0) {
|
||||||
const union = firesUnion(fires);
|
const union = firesUnion(fires);
|
||||||
|
|
@ -59,6 +59,7 @@ Meteor.publish('activefiresmyloc', function activeInMyLoc(northEastLng, northEas
|
||||||
return activefires(northEastLng, northEastLat, southWestLng, southWestLat, withMarks);
|
return activefires(northEastLng, northEastLat, southWestLng, southWestLat, withMarks);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Warning: this increase always by one the fire stats
|
||||||
Meteor.publish('lastFireDetected', function lastFireDetected() {
|
Meteor.publish('lastFireDetected', function lastFireDetected() {
|
||||||
return ActiveFires.find({}, { limit: 1, sort: { when: -1 } });
|
return ActiveFires.find({}, { limit: 1, sort: { when: -1 } });
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@ import { Meteor } from 'meteor/meteor';
|
||||||
import tbuffer from '@turf/buffer';
|
import tbuffer from '@turf/buffer';
|
||||||
import emojiFlags from 'emoji-flags';
|
import emojiFlags from 'emoji-flags';
|
||||||
import tweet from '/imports/modules/server/twitter';
|
import tweet from '/imports/modules/server/twitter';
|
||||||
import countFires from './countFires';
|
import countFiresInRegions from './countFires';
|
||||||
|
|
||||||
|
const debug = false;
|
||||||
|
|
||||||
const autonomies = tbuffer(JSON.parse(getPrivateFile('data/es-atlas-autonomies.json')), 0);
|
const autonomies = tbuffer(JSON.parse(getPrivateFile('data/es-atlas-autonomies.json')), 0);
|
||||||
const portugal = tbuffer(JSON.parse(getPrivateFile('data/pt-atlas.json')), 0);
|
const portugal = tbuffer(JSON.parse(getPrivateFile('data/pt-atlas.json')), 0);
|
||||||
|
|
@ -37,7 +39,7 @@ const emoji = (key, name, fires) => {
|
||||||
if (Meteor.isDevelopment) console.log(`Cannot find flag for country ${name}: ${JSON.stringify(key)}`);
|
if (Meteor.isDevelopment) console.log(`Cannot find flag for country ${name}: ${JSON.stringify(key)}`);
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
return `${emo.emoji} ${fires},`;
|
return `${emo.emoji} ${fires}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const composeEuropeTweet = (num, stats, abrev) => {
|
const composeEuropeTweet = (num, stats, abrev) => {
|
||||||
|
|
@ -46,14 +48,20 @@ const composeEuropeTweet = (num, stats, abrev) => {
|
||||||
}
|
}
|
||||||
const keys = Object.keys(stats);
|
const keys = Object.keys(stats);
|
||||||
const numZones = keys.length;
|
const numZones = keys.length;
|
||||||
let text = abrev ? 'Active #wildfires:\n' : '';
|
let text = '';
|
||||||
keys.map((key, index) => { // , index
|
keys.map((key, index) => { // , index
|
||||||
const zoneFires = stats[key].count;
|
const zoneFires = stats[key].count;
|
||||||
const zoneFiresText = zoneFires > 1 ? index === 0 ? zoneFires : `${zoneFires}` : index === 0 ? 'One' : '1';
|
const zoneFiresText = zoneFires > 1 ? index === 0 ? zoneFires : `${zoneFires}` : index === 0 ? 'One' : '1';
|
||||||
const what = index > 0 ? ' ' : zoneFires > 1 ? ' active #wildfires ' : ' active #wildfire ';
|
const what = index > 0 ? ' ' : zoneFires > 1 ? ' active #wildfires ' : ' active #wildfire ';
|
||||||
const subText = abrev ? emoji(stats[key], key, zoneFires) :
|
const subText = abrev ? emoji(stats[key], key, zoneFires) :
|
||||||
`${zoneFiresText}${what}in #{key}`;
|
`${zoneFiresText}${what}in #{key}`;
|
||||||
const delimiter = abrev ? '' : index === numZones - 1 ? ' and' : ',';
|
let delimiter;
|
||||||
|
if (!abrev) {
|
||||||
|
delimiter = index === numZones - 1 ? ' and' : ',';
|
||||||
|
} else {
|
||||||
|
delimiter = ',';
|
||||||
|
}
|
||||||
|
|
||||||
const subTexts = index === 0 ? subText : `${delimiter} ${subText}`;
|
const subTexts = index === 0 ? subText : `${delimiter} ${subText}`;
|
||||||
text += subTexts;
|
text += subTexts;
|
||||||
return '';
|
return '';
|
||||||
|
|
@ -95,12 +103,15 @@ const correctTweetSize = (tweetText) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const tweetEuropeFires = () => {
|
const tweetEuropeFires = () => {
|
||||||
const resultEu = countFires(europe, [[' ', '']]);
|
const resultEu = countFiresInRegions(europe, [[' ', '']]);
|
||||||
|
|
||||||
if (resultEu.total > 0) {
|
if (resultEu.total > 0) {
|
||||||
let tweetText = `${tweetHeaders[0].trim()} ${composeEuropeTweet(resultEu.total, resultEu.fires, false)}. More info: https://fires.comunes.org/fires`;
|
let tweetText = `${tweetHeaders[0].trim()} ${composeEuropeTweet(resultEu.total, resultEu.fires, false)}. More info: https://fires.comunes.org/fires`;
|
||||||
if (!correctTweetSize(tweetText)) {
|
if (!correctTweetSize(tweetText)) {
|
||||||
tweetText = `${tweetHeaders[0].trim()} ${composeEuropeTweet(resultEu.total, resultEu.fires, true)}. More info: https://fires.comunes.org/fires`;
|
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)) {
|
if (!correctTweetSize(tweetText)) {
|
||||||
tweetText = `${tweetHeaders[0].trim()} There are ${resultEu.total} active #wildfires in #Europe. More info: https://fires.comunes.org/fires`;
|
tweetText = `${tweetHeaders[0].trim()} There are ${resultEu.total} active #wildfires in #Europe. More info: https://fires.comunes.org/fires`;
|
||||||
|
|
@ -115,8 +126,8 @@ const tweetEuropeFires = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const tweetIberiaFires = () => {
|
const tweetIberiaFires = () => {
|
||||||
const resultEs = countFires(autonomies, stringsToRemoveIberia);
|
const resultEs = countFiresInRegions(autonomies, stringsToRemoveIberia);
|
||||||
const resultPt = countFires(portugal, stringsToRemoveIberia);
|
const resultPt = countFiresInRegions(portugal, stringsToRemoveIberia);
|
||||||
|
|
||||||
const fires = Object.assign(resultEs.fires, resultPt.fires);
|
const fires = Object.assign(resultEs.fires, resultPt.fires);
|
||||||
const total = resultEs.total + resultPt.total;
|
const total = resultEs.total + resultPt.total;
|
||||||
|
|
@ -136,7 +147,7 @@ const tweetIberiaFires = () => {
|
||||||
return '';
|
return '';
|
||||||
};
|
};
|
||||||
|
|
||||||
if (Meteor.isDevelopment) {
|
if (Meteor.isDevelopment && debug) {
|
||||||
tweetIberiaFires();
|
tweetIberiaFires();
|
||||||
tweetEuropeFires();
|
tweetEuropeFires();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
71
imports/api/ActiveFiresUnion/ActiveFiresUnion.js
Normal 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;
|
||||||
80
imports/api/ActiveFiresUnion/server/publications.js
Normal 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 } });
|
||||||
|
});
|
||||||
|
|
@ -6,6 +6,29 @@ import FalsePositiveTypes from './FalsePositiveTypes';
|
||||||
import Fires from '../Fires/Fires';
|
import Fires from '../Fires/Fires';
|
||||||
import rateLimit from '../../modules/rate-limit';
|
import rateLimit from '../../modules/rate-limit';
|
||||||
|
|
||||||
|
export function upsertFalsePositive(keyType, owner, fire) {
|
||||||
|
const fireType = fire.type;
|
||||||
|
if (fireType === 'vecinal') {
|
||||||
|
throw new Meteor.Error('500', 'No se puede marcar este tipo de fuego');
|
||||||
|
}
|
||||||
|
const date = new Date();
|
||||||
|
const formated = moment(date).format('YYYYMMDD');
|
||||||
|
try {
|
||||||
|
const set = {
|
||||||
|
owner,
|
||||||
|
type: keyType,
|
||||||
|
when: date,
|
||||||
|
whendateformat: formated,
|
||||||
|
fireId: fire._id,
|
||||||
|
geo: fire.ourid
|
||||||
|
};
|
||||||
|
return FalsePositives.upsert({ geo: fire.ourid, owner }, { $set: set }, { multi: false, upsert: true });
|
||||||
|
} catch (exception) {
|
||||||
|
console.log(exception);
|
||||||
|
throw new Meteor.Error('500', exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Meteor.methods({
|
Meteor.methods({
|
||||||
'falsePositives.insert': function falsePositivesInsert(fireId, keyType) {
|
'falsePositives.insert': function falsePositivesInsert(fireId, keyType) {
|
||||||
check(fireId, Meteor.Collection.ObjectID);
|
check(fireId, Meteor.Collection.ObjectID);
|
||||||
|
|
@ -17,27 +40,9 @@ Meteor.methods({
|
||||||
}
|
}
|
||||||
check(this.userId, String);
|
check(this.userId, String);
|
||||||
const fire = Fires.findOne(fireId);
|
const fire = Fires.findOne(fireId);
|
||||||
|
const owner = this.userId;
|
||||||
if (fire) {
|
if (fire) {
|
||||||
const fireType = fire.type;
|
upsertFalsePositive(keyType, owner, fire);
|
||||||
if (fireType === 'vecinal') {
|
|
||||||
throw new Meteor.Error('500', 'No se puede marcar este tipo de fuego');
|
|
||||||
}
|
|
||||||
const date = new Date();
|
|
||||||
const formated = moment(date).format('YYYYMMDD');
|
|
||||||
try {
|
|
||||||
const set = {
|
|
||||||
owner: this.userId,
|
|
||||||
type: keyType,
|
|
||||||
when: date,
|
|
||||||
whendateformat: formated,
|
|
||||||
fireId,
|
|
||||||
geo: fire.ourid
|
|
||||||
};
|
|
||||||
return FalsePositives.upsert({ geo: fire.ourid, owner: this.userId }, { $set: set }, { multi: false, upsert: true });
|
|
||||||
} catch (exception) {
|
|
||||||
console.log(exception);
|
|
||||||
throw new Meteor.Error('500', exception);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
throw new Meteor.Error('500', 'Fuego no encontrado');
|
throw new Meteor.Error('500', 'Fuego no encontrado');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@
|
||||||
import { Meteor } from 'meteor/meteor';
|
import { Meteor } from 'meteor/meteor';
|
||||||
import { check } from 'meteor/check';
|
import { check } from 'meteor/check';
|
||||||
import { NumberBetween } from '/imports/modules/server/other-checks';
|
import { NumberBetween } from '/imports/modules/server/other-checks';
|
||||||
import L from 'leaflet-headless';
|
import '/imports/startup/server/leaflet-workaround.js';
|
||||||
|
import L from 'leaflet';
|
||||||
import calcUnion from '/imports/ui/components/Maps/SubsUnion/Unify';
|
import calcUnion from '/imports/ui/components/Maps/SubsUnion/Unify';
|
||||||
import Industries from '/imports/api/Industries/Industries';
|
import Industries from '/imports/api/Industries/Industries';
|
||||||
import FalsePositives from '../FalsePositives';
|
import FalsePositives from '../FalsePositives';
|
||||||
|
|
@ -17,13 +18,26 @@ Meteor.publish('falsePositivesTotal', function total() {
|
||||||
});
|
});
|
||||||
|
|
||||||
export const firesUnion = (fires) => {
|
export const firesUnion = (fires) => {
|
||||||
const group = new L.FeatureGroup();
|
|
||||||
const firesArray = Array.isArray(fires) ? fires : fires.fetch(); // if not is a cursor
|
const firesArray = Array.isArray(fires) ? fires : fires.fetch(); // if not is a cursor
|
||||||
const remap = firesArray.map(function remap(doc) {
|
const remap = firesArray.map(function remap(doc) {
|
||||||
// default scan: 1 for neightbor alerts
|
const isNASA = doc.type === 'modis' || doc.type === 'viirs';
|
||||||
return { location: { lat: doc.lat, lon: doc.lon }, distance: doc.scan || 1 };
|
const pixelSize = doc.type === 'viirs' ? 0.375 : 1; // viirs has 375m pixel size, modis 1000m
|
||||||
|
// default 1 km for neightbor alerts
|
||||||
|
const map = {
|
||||||
|
location: { lat: doc.lat, lon: doc.lon },
|
||||||
|
distance: isNASA ? doc.scan * pixelSize : 1,
|
||||||
|
distanceY: isNASA ? doc.track * pixelSize : 1
|
||||||
|
};
|
||||||
|
// console.log(map);
|
||||||
|
return map;
|
||||||
});
|
});
|
||||||
const union = calcUnion(remap, group, sub => sub);
|
const union = calcUnion(L, remap, sub => sub, false);
|
||||||
|
return union;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const zoneToUnion = (lat, lon, distance) => {
|
||||||
|
const remap = [{ location: { lat, lon }, distance }];
|
||||||
|
const union = calcUnion(L, remap, sub => sub, true);
|
||||||
return union;
|
return union;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,8 @@ Meteor.publish('fireAlerts', function fireAlerts(northEastLng, northEastLat, sou
|
||||||
fields: {
|
fields: {
|
||||||
lat: 1,
|
lat: 1,
|
||||||
lon: 1,
|
lon: 1,
|
||||||
scan: 1
|
scan: 1,
|
||||||
|
track: 1
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (Meteor.isDevelopment) console.log(`Neighbour alerts total: ${fires.count()}`);
|
if (Meteor.isDevelopment) console.log(`Neighbour alerts total: ${fires.count()}`);
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import FireAlertsCollection from '/imports/api/FireAlerts/FireAlerts';
|
||||||
import { whichAreFalsePositives, firesUnion } from '/imports/api/FalsePositives/server/publications';
|
import { whichAreFalsePositives, firesUnion } from '/imports/api/FalsePositives/server/publications';
|
||||||
import FalsePositives from '/imports/api/FalsePositives/FalsePositives';
|
import FalsePositives from '/imports/api/FalsePositives/FalsePositives';
|
||||||
import Industries from '/imports/api/Industries/Industries';
|
import Industries from '/imports/api/Industries/Industries';
|
||||||
|
import ravenLogger from '/imports/startup/server/ravenLogger';
|
||||||
|
|
||||||
import FiresCollection from '../Fires';
|
import FiresCollection from '../Fires';
|
||||||
|
|
||||||
|
|
@ -132,32 +133,46 @@ Meteor.publish('fireFromId', function fireFromId(_id) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Meteor.publish('fireFromHash', function fireFromHash(fireEnc) {
|
function logUrl(fireEnc, params) {
|
||||||
|
const message = `Wrong fire: ${fireEnc}. Params received in url: ${JSON.stringify(params)}`;
|
||||||
|
console.log(message);
|
||||||
|
ravenLogger.log(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fireFromHash(fireEnc, params) {
|
||||||
check(fireEnc, String);
|
check(fireEnc, String);
|
||||||
|
check(params, Object);
|
||||||
|
|
||||||
|
// console.log(fireEnc);
|
||||||
|
// const unsealed = Promise.await(urlEnc.decrypt(fireEnc));
|
||||||
|
const unsealed = Promise.await(unsealW(fireEnc));
|
||||||
|
if (unsealed === undefined) {
|
||||||
|
throw Error(`Fail to unseal '${fireEnc}'`);
|
||||||
|
}
|
||||||
|
const w = unsealed.when;
|
||||||
|
unsealed.when = new Date(w);
|
||||||
|
const c = unsealed.createdAt;
|
||||||
|
unsealed.createdAt = !c ? new Date() : new Date(c);
|
||||||
|
const u = unsealed.updatedAt;
|
||||||
|
unsealed.updatedAt = !u ? new Date() : new Date(u);
|
||||||
|
// console.log(unsealed);
|
||||||
|
// FIXME:
|
||||||
|
const unsealedFix = fixConfidence(unsealed);
|
||||||
|
FiresCollection.schema.validate(unsealedFix);
|
||||||
|
return findOrCreateFire(unsealedFix);
|
||||||
|
/* console.log(`fires: ${fire.count()}`);
|
||||||
|
* return fire; */
|
||||||
|
}
|
||||||
|
|
||||||
|
Meteor.publish('fireFromHash', function fireFromHashFunc(fireEnc, params) {
|
||||||
|
check(fireEnc, String);
|
||||||
|
check(params, Object);
|
||||||
try {
|
try {
|
||||||
// console.log(fireEnc);
|
return fireFromHash(fireEnc, params);
|
||||||
// const unsealed = Promise.await(urlEnc.decrypt(fireEnc));
|
|
||||||
const unsealed = Promise.await(unsealW(fireEnc));
|
|
||||||
if (unsealed === undefined) {
|
|
||||||
console.info(`Wrong fire: ${fireEnc}`);
|
|
||||||
// https://guide.meteor.com/data-loading.html
|
|
||||||
return this.ready();
|
|
||||||
}
|
|
||||||
const w = unsealed.when;
|
|
||||||
unsealed.when = new Date(w);
|
|
||||||
const c = unsealed.createdAt;
|
|
||||||
unsealed.createdAt = !c ? new Date() : new Date(c);
|
|
||||||
const u = unsealed.updatedAt;
|
|
||||||
unsealed.updatedAt = !u ? new Date() : new Date(u);
|
|
||||||
// console.log(unsealed);
|
|
||||||
// FIXME:
|
|
||||||
const unsealedFix = fixConfidence(unsealed);
|
|
||||||
FiresCollection.schema.validate(unsealedFix);
|
|
||||||
return findOrCreateFire(unsealedFix);
|
|
||||||
/* console.log(`fires: ${fire.count()}`);
|
|
||||||
* return fire; */
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(e);
|
console.warn(e);
|
||||||
|
logUrl(fireEnc, params);
|
||||||
|
// https://guide.meteor.com/data-loading.html
|
||||||
return this.ready();
|
return this.ready();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
/* eslint-disable import/no-absolute-path */
|
/* eslint-disable import/no-absolute-path */
|
||||||
|
|
||||||
import { Mongo } from 'meteor/mongo';
|
import { Mongo } from 'meteor/mongo';
|
||||||
|
import { Meteor } from 'meteor/meteor';
|
||||||
import SimpleSchema from 'simpl-schema';
|
import SimpleSchema from 'simpl-schema';
|
||||||
import { defaultCreatedAt, defaultUpdateAt } from '/imports/api/Utility/Utils.js';
|
import { defaultCreatedAt, defaultUpdateAt } from '/imports/api/Utility/Utils.js';
|
||||||
import LocationSchema from '/imports/api/Utility/LocationSchema.js';
|
import LocationSchema from '/imports/api/Utility/LocationSchema.js';
|
||||||
|
|
@ -22,11 +23,12 @@ Notifications.deny({
|
||||||
|
|
||||||
Notifications.schema = new SimpleSchema({
|
Notifications.schema = new SimpleSchema({
|
||||||
userId: String,
|
userId: String,
|
||||||
|
subsId: { type: Meteor.Collection.ObjectID, optional: true, blackbox: true },
|
||||||
content: String,
|
content: String,
|
||||||
geo: LocationSchema,
|
geo: LocationSchema,
|
||||||
type: String,
|
type: String,
|
||||||
webNotified: { type: Boolean, optional: true },
|
notified: { type: Boolean, optional: true },
|
||||||
webNotifiedAt: { type: Date, optional: true },
|
notifiedAt: { type: Date, optional: true },
|
||||||
emailNotified: { type: Boolean, optional: true },
|
emailNotified: { type: Boolean, optional: true },
|
||||||
emailNotifiedAt: { type: Date, optional: true },
|
emailNotifiedAt: { type: Date, optional: true },
|
||||||
when: Date,
|
when: Date,
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ Meteor.methods({
|
||||||
check(notifId, Meteor.Collection.ObjectID);
|
check(notifId, Meteor.Collection.ObjectID);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Notifications.update(notifId, { $set: { webNotified: true, webNotifiedAt: new Date() } });
|
Notifications.update(notifId, { $set: { notified: true, notifiedAt: new Date() } });
|
||||||
return notifId;
|
return notifId;
|
||||||
} catch (exception) {
|
} catch (exception) {
|
||||||
throw new Meteor.Error('500', exception);
|
throw new Meteor.Error('500', exception);
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { Meteor } from 'meteor/meteor';
|
||||||
import Notifications from '../Notifications';
|
import Notifications from '../Notifications';
|
||||||
|
|
||||||
Meteor.publish('mynotifications', function notifications() {
|
Meteor.publish('mynotifications', function notifications() {
|
||||||
const notif = Notifications.find({ userId: this.userId, type: 'web', webNotified: null });
|
const notif = Notifications.find({ userId: this.userId, type: 'web', notified: null });
|
||||||
// console.log(`Notifications for user ${this.userId}: ${notif.count()}`);
|
// console.log(`Notifications for user ${this.userId}: ${notif.count()}`);
|
||||||
return notif;
|
return notif;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
457
imports/api/Rest/Rest.js
Normal file
|
|
@ -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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
/* eslint-disable consistent-return */
|
/* eslint-disable consistent-return */
|
||||||
/* eslint-disable import/no-absolute-path */
|
/* eslint-disable import/no-absolute-path */
|
||||||
|
|
||||||
|
import { Meteor } from 'meteor/meteor';
|
||||||
import { Mongo } from 'meteor/mongo';
|
import { Mongo } from 'meteor/mongo';
|
||||||
import SimpleSchema from 'simpl-schema';
|
import SimpleSchema from 'simpl-schema';
|
||||||
import { defaultCreatedAt, defaultUpdateAt } from '/imports/api/Utility/Utils.js';
|
import { defaultCreatedAt, defaultUpdateAt } from '/imports/api/Utility/Utils.js';
|
||||||
|
|
@ -42,6 +43,7 @@ Subscriptions.deny({
|
||||||
|
|
||||||
|
|
||||||
Subscriptions.schema = new SimpleSchema({
|
Subscriptions.schema = new SimpleSchema({
|
||||||
|
_id: { type: Meteor.Collection.ObjectID, optional: true, blackbox: true },
|
||||||
location: Object,
|
location: Object,
|
||||||
'location.lat': Number,
|
'location.lat': Number,
|
||||||
'location.lon': Number,
|
'location.lon': Number,
|
||||||
|
|
|
||||||
|
|
@ -10,30 +10,49 @@ function geo(doc) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function subscriptionsInsert(doc, userId, type) {
|
||||||
|
check(doc, {
|
||||||
|
_id: Match.Maybe(Meteor.Collection.ObjectID),
|
||||||
|
location: Match.ObjectIncluding({ lat: Number, lon: Number }),
|
||||||
|
distance: Number
|
||||||
|
});
|
||||||
|
const newDoc = {
|
||||||
|
owner: userId,
|
||||||
|
type,
|
||||||
|
geo: geo(doc),
|
||||||
|
...doc
|
||||||
|
};
|
||||||
|
// console.log(newDoc);
|
||||||
|
const already = Subscriptions.findOne(newDoc);
|
||||||
|
if (already) {
|
||||||
|
throw new Meteor.Error('on-already-subscribed', 'The user is already subscribed to this area');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Subscriptions.insert(newDoc);
|
||||||
|
} catch (exception) {
|
||||||
|
// console.error(exception);
|
||||||
|
throw new Meteor.Error('500', exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function subscriptionsRemove(subscriptionId) {
|
||||||
|
check(subscriptionId, Meteor.Collection.ObjectID);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return Subscriptions.remove(subscriptionId);
|
||||||
|
} catch (exception) {
|
||||||
|
console.error(exception);
|
||||||
|
throw new Meteor.Error('500', exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Meteor.methods({
|
Meteor.methods({
|
||||||
'subscriptions.insert': function subscriptionsInsert(doc) {
|
'subscriptions.insert': function subscriptionInsertMethod(doc) {
|
||||||
check(doc, {
|
check(doc, {
|
||||||
location: Match.ObjectIncluding({ lat: Number, lon: Number }),
|
location: Match.ObjectIncluding({ lat: Number, lon: Number }),
|
||||||
distance: Number
|
distance: Number
|
||||||
});
|
});
|
||||||
const type = 'web';
|
return subscriptionsInsert(doc, this.userId, 'web');
|
||||||
const newDoc = {
|
|
||||||
owner: this.userId,
|
|
||||||
type,
|
|
||||||
geo: geo(doc),
|
|
||||||
...doc
|
|
||||||
};
|
|
||||||
// console.log(newDoc);
|
|
||||||
const already = Subscriptions.findOne(newDoc);
|
|
||||||
if (already) {
|
|
||||||
throw new Meteor.Error('on-already-subscribed', 'The user is already subscribed to this area');
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return Subscriptions.insert(newDoc);
|
|
||||||
} catch (exception) {
|
|
||||||
// console.error(exception);
|
|
||||||
throw new Meteor.Error('500', exception);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
'subscriptions.update': function subscriptionsUpdate(doc) {
|
'subscriptions.update': function subscriptionsUpdate(doc) {
|
||||||
check(doc, {
|
check(doc, {
|
||||||
|
|
@ -52,15 +71,9 @@ Meteor.methods({
|
||||||
throw new Meteor.Error('500', exception);
|
throw new Meteor.Error('500', exception);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'subscriptions.remove': function subscriptionsRemove(subscriptionId) {
|
'subscriptions.remove': function subscriptionsRemoveMethod(subscriptionId) {
|
||||||
check(subscriptionId, Meteor.Collection.ObjectID);
|
check(subscriptionId, Meteor.Collection.ObjectID);
|
||||||
|
return subscriptionsRemove(subscriptionId);
|
||||||
try {
|
|
||||||
return Subscriptions.remove(subscriptionId);
|
|
||||||
} catch (exception) {
|
|
||||||
console.error(exception);
|
|
||||||
throw new Meteor.Error('500', exception);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,7 @@ const schemaUser = new SimpleSchema({
|
||||||
telegramUsername: { type: String, optional: true },
|
telegramUsername: { type: String, optional: true },
|
||||||
telegramFirstName: { type: String, optional: true },
|
telegramFirstName: { type: String, optional: true },
|
||||||
telegramLanguageCode: { type: String, optional: true },
|
telegramLanguageCode: { type: String, optional: true },
|
||||||
|
fireBaseToken: { type: String, optional: true },
|
||||||
createdAt: defaultCreatedAt,
|
createdAt: defaultCreatedAt,
|
||||||
updatedAt: defaultUpdateAt
|
updatedAt: defaultUpdateAt
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
|
/* eslint-disable import/no-absolute-path */
|
||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import getFallbackLang from '/imports/startup/server/accounts/lang-fallback';
|
import getFallbackLang from '/imports/modules/lang-fallback';
|
||||||
|
|
||||||
const as = f => `assets/app/${f}`;
|
const as = f => `assets/app/${f}`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
const getFallbackLang = (lang) => {
|
const getFallbackLang = (lang) => {
|
||||||
if (lang && (lang === 'ast' || lang === 'gl' || lang === 'eu' || lang === 'ca' || lang.match(/^es-/))) {
|
if (lang && (lang === 'ast' || lang === 'gl' || lang === 'eu' ||
|
||||||
|
lang === 'es' || lang === 'ca' || lang.match(/^es-/))) {
|
||||||
return 'es';
|
return 'es';
|
||||||
}
|
}
|
||||||
return 'en';
|
return 'en';
|
||||||
153
imports/modules/server/notificationsProcess.js
Normal 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;
|
||||||
|
|
@ -17,7 +17,7 @@ import i18nOpts from '../common/i18n';
|
||||||
|
|
||||||
const detectorOptions = {
|
const detectorOptions = {
|
||||||
// order and from where user language should be detected
|
// order and from where user language should be detected
|
||||||
order: ['querystring', 'cookie', 'localStorage', 'navigator', 'htmlTag'],
|
order: ['querystring', 'navigator', 'cookie', 'localStorage', 'htmlTag'],
|
||||||
|
|
||||||
// keys or params to lookup language from
|
// keys or params to lookup language from
|
||||||
lookupQuerystring: 'lng',
|
lookupQuerystring: 'lng',
|
||||||
|
|
@ -61,6 +61,14 @@ if (sendMissing && Meteor.isDevelopment) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setT9(lang) {
|
||||||
|
if (lang === 'gl') {
|
||||||
|
T9n.setLanguage('es');
|
||||||
|
} else {
|
||||||
|
T9n.setLanguage(lang);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
i18n.use(backend)
|
i18n.use(backend)
|
||||||
.use(LngDetector)
|
.use(LngDetector)
|
||||||
.use(Cache)
|
.use(Cache)
|
||||||
|
|
@ -75,7 +83,7 @@ i18n.use(backend)
|
||||||
// document.title = t('AppName');
|
// document.title = t('AppName');
|
||||||
// Accounts translation
|
// Accounts translation
|
||||||
// https://github.com/softwarerero/meteor-accounts-t9n
|
// https://github.com/softwarerero/meteor-accounts-t9n
|
||||||
T9n.setLanguage(i18n.language);
|
setT9(i18n.language);
|
||||||
// console.log(T9n.get('error.accounts.User not found'));
|
// console.log(T9n.get('error.accounts.User not found'));
|
||||||
moment.locale(i18n.language);
|
moment.locale(i18n.language);
|
||||||
|
|
||||||
|
|
@ -104,7 +112,8 @@ Meteor.subscribe('userData'); // lang is there
|
||||||
|
|
||||||
i18n.on('languageChanged', (lng) => {
|
i18n.on('languageChanged', (lng) => {
|
||||||
moment.locale(lng);
|
moment.locale(lng);
|
||||||
T9n.setLanguage(lng);
|
// TODO fix this when gl is translated
|
||||||
|
setT9(lng);
|
||||||
Tracker.autorun((computation) => {
|
Tracker.autorun((computation) => {
|
||||||
if (Meteor.userId()) {
|
if (Meteor.userId()) {
|
||||||
// logged
|
// logged
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
|
/* global */
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render } from 'react-dom';
|
import { render } from 'react-dom';
|
||||||
import { Meteor } from 'meteor/meteor';
|
import { Meteor } from 'meteor/meteor';
|
||||||
|
import '/imports/startup/client/modernizr';
|
||||||
import App from '../../ui/layouts/App/App';
|
import App from '../../ui/layouts/App/App';
|
||||||
|
|
||||||
import '../../ui/stylesheets/app.scss';
|
import '../../ui/stylesheets/app.scss';
|
||||||
|
|
@ -9,4 +11,9 @@ import '../../ui/stylesheets/app.scss';
|
||||||
const isIE9OrBelow = () => /MSIE\s/.test(navigator.userAgent) && parseFloat(navigator.appVersion.split('MSIE')[1]) < 10;
|
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.');
|
if (isIE9OrBelow()) window.alert('You are using an outdated browser. Please use Chrome or Firefox to display this site.');
|
||||||
else Meteor.startup(() => render(<App />, document.getElementById('react-root')));
|
else {
|
||||||
|
Modernizr.on('webp', (result) => {
|
||||||
|
if (Meteor.isDevelopment) console.log(`webp ${result ? '' : 'not '}supported`);
|
||||||
|
});
|
||||||
|
Meteor.startup(() => render(<App />, document.getElementById('react-root')));
|
||||||
|
}
|
||||||
|
|
|
||||||
3
imports/startup/client/modernizr.js
Normal file
|
|
@ -2,13 +2,15 @@ import RavenLogger from 'meteor/flowkey:raven';
|
||||||
import { Meteor } from 'meteor/meteor';
|
import { Meteor } from 'meteor/meteor';
|
||||||
|
|
||||||
const ravenOptions = {};
|
const ravenOptions = {};
|
||||||
|
const publicDSN = Meteor.settings.public.sentryPublicDSN;
|
||||||
const enabled = true && !Meteor.isDevelopment;
|
const enabled = !!publicDSN;
|
||||||
|
|
||||||
const ravenLogger = enabled ? new RavenLogger({
|
const ravenLogger = enabled ? new RavenLogger({
|
||||||
publicDSN: Meteor.settings.public.sentryPublicDSN, // will be used on the client
|
publicDSN, // will be used on the client
|
||||||
shouldCatchConsoleError: true, // default
|
shouldCatchConsoleError: true, // default
|
||||||
trackUser: true // default
|
trackUser: true // default
|
||||||
}, ravenOptions) : { log: (error) => { console.log(error); } };
|
}, ravenOptions) : { log: (error) => { console.log(error); } };
|
||||||
|
|
||||||
|
console.log(`ravenLogger ${enabled ? 'enabled' : 'disabled'}`);
|
||||||
|
|
||||||
export default ravenLogger;
|
export default ravenLogger;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import moment from 'moment';
|
||||||
// Load the js langs
|
// Load the js langs
|
||||||
import es from 'meteor-accounts-t9n/build/es';
|
import es from 'meteor-accounts-t9n/build/es';
|
||||||
import en from 'meteor-accounts-t9n/build/en';
|
import en from 'meteor-accounts-t9n/build/en';
|
||||||
|
// TODO ask for translation of this
|
||||||
|
// import gl from 'meteor-accounts-t9n/build/gl';
|
||||||
|
|
||||||
const backOpts = {
|
const backOpts = {
|
||||||
// path where resources get loaded from
|
// path where resources get loaded from
|
||||||
|
|
@ -21,7 +23,10 @@ const shouldDebug = (forceDebug && !Meteor.isProduction);
|
||||||
const i18nOpts = {
|
const i18nOpts = {
|
||||||
backend: backOpts,
|
backend: backOpts,
|
||||||
// lng: 'es',
|
// lng: 'es',
|
||||||
fallbackLng: ['es', 'en'],
|
fallbackLng: {
|
||||||
|
gl: ['es'],
|
||||||
|
default: ['en']
|
||||||
|
},
|
||||||
sendMissingTo: 'fallback',
|
sendMissingTo: 'fallback',
|
||||||
interpolation: {
|
interpolation: {
|
||||||
escapeValue: false, // not needed for react!!
|
escapeValue: false, // not needed for react!!
|
||||||
|
|
@ -35,7 +40,7 @@ const i18nOpts = {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
whitelist: ['es', 'en'], // allowed languages
|
whitelist: ['es', 'en', 'gl'], // allowed languages
|
||||||
load: 'languageOnly', // 'es' o 'en', previously: 'all', // es-ES -> es, en-US -> en
|
load: 'languageOnly', // 'es' o 'en', previously: 'all', // es-ES -> es, en-US -> en
|
||||||
debug: shouldDebug,
|
debug: shouldDebug,
|
||||||
ns: 'common',
|
ns: 'common',
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
|
/* eslint-disable import/no-absolute-path */
|
||||||
|
|
||||||
import { Accounts } from 'meteor/accounts-base';
|
import { Accounts } from 'meteor/accounts-base';
|
||||||
import sendWelcomeEmail from '../../../api/Users/server/send-welcome-email';
|
import getFallbackLang from '/imports/modules/lang-fallback';
|
||||||
import getOAuthProfile from '../../../modules/get-oauth-profile';
|
import sendWelcomeEmail from '/imports/api/Users/server/send-welcome-email';
|
||||||
import getFallbackLang from './lang-fallback';
|
import getOAuthProfile from '/imports/modules/get-oauth-profile';
|
||||||
|
|
||||||
Accounts.onCreateUser((options, user) => {
|
Accounts.onCreateUser((options, user) => {
|
||||||
const userToCreate = user;
|
const userToCreate = user;
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import '../../api/Users/server/publications';
|
||||||
import '../../api/Utility/server/methods';
|
import '../../api/Utility/server/methods';
|
||||||
|
|
||||||
import '../../api/ActiveFires/server/publications';
|
import '../../api/ActiveFires/server/publications';
|
||||||
|
import '../../api/ActiveFiresUnion/server/publications';
|
||||||
import '../../api/FireAlerts/server/publications';
|
import '../../api/FireAlerts/server/publications';
|
||||||
|
|
||||||
import '../../api/Subscriptions/methods';
|
import '../../api/Subscriptions/methods';
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@
|
||||||
import { Meteor } from 'meteor/meteor';
|
import { Meteor } from 'meteor/meteor';
|
||||||
import { tweetIberiaFires, tweetEuropeFires } from '/imports/api/ActiveFires/server/tweetFiresInZone';
|
import { tweetIberiaFires, tweetEuropeFires } from '/imports/api/ActiveFires/server/tweetFiresInZone';
|
||||||
import { isMailServerMaster, sendEmailsFromQueue } from '/imports/startup/server/email';
|
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/
|
// https://github.com/thesaucecode/meteor-synced-cron/
|
||||||
|
|
||||||
|
|
@ -44,7 +46,7 @@ Meteor.startup(() => {
|
||||||
timezone: 'Europe/Madrid',
|
timezone: 'Europe/Madrid',
|
||||||
schedule: (parser) => {
|
schedule: (parser) => {
|
||||||
// http://bunkat.github.io/later/
|
// http://bunkat.github.io/later/
|
||||||
const sched = parser.text('every 1 min');
|
const sched = parser.text('every 10 min');
|
||||||
if (sched.error !== -1) {
|
if (sched.error !== -1) {
|
||||||
console.error(`Mail cron 'when' field parsed with errors: ${sched.error}`);
|
console.error(`Mail cron 'when' field parsed with errors: ${sched.error}`);
|
||||||
}
|
}
|
||||||
|
|
@ -52,6 +54,30 @@ Meteor.startup(() => {
|
||||||
},
|
},
|
||||||
job: () => sendEmailsFromQueue()
|
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 esEn = Meteor.settings.private.twitter.es.enabled;
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,4 @@ import './prerender';
|
||||||
import './feedback';
|
import './feedback';
|
||||||
import './cron';
|
import './cron';
|
||||||
import './isMaster';
|
import './isMaster';
|
||||||
import './segfaults';
|
|
||||||
import './rest';
|
import './rest';
|
||||||
|
|
|
||||||
19
imports/startup/server/leaflet-workaround.js
Normal 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'
|
||||||
|
};
|
||||||
|
|
@ -9,6 +9,8 @@ import SiteSettings from '/imports/api/SiteSettings/SiteSettings';
|
||||||
import FalsePositives from '/imports/api/FalsePositives/FalsePositives';
|
import FalsePositives from '/imports/api/FalsePositives/FalsePositives';
|
||||||
import Industries from '/imports/api/Industries/Industries';
|
import Industries from '/imports/api/Industries/Industries';
|
||||||
import IndustryRegistries from '/imports/api/Industries/IndustryRegistries';
|
import IndustryRegistries from '/imports/api/Industries/IndustryRegistries';
|
||||||
|
import Notifications from '/imports/api/Notifications/Notifications';
|
||||||
|
import ActiveFiresUnion from '/imports/api/ActiveFiresUnion/ActiveFiresUnion';
|
||||||
import { Mongo } from 'meteor/mongo';
|
import { Mongo } from 'meteor/mongo';
|
||||||
|
|
||||||
Meteor.startup(() => {
|
Meteor.startup(() => {
|
||||||
|
|
@ -227,6 +229,30 @@ Meteor.startup(() => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Migrations.add({
|
||||||
|
version: 17,
|
||||||
|
up: function renameWebNotifiedField() {
|
||||||
|
Notifications.update({ webNotified: { $exists: true } }, {
|
||||||
|
$rename: { webNotifiedAt: 'notifiedAt' }
|
||||||
|
}, { upsert: false, multi: true });
|
||||||
|
Notifications.update({ webNotified: { $exists: true } }, {
|
||||||
|
$rename: { webNotified: 'notified' }
|
||||||
|
}, { upsert: false, multi: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Migrations.add({
|
||||||
|
version: 18,
|
||||||
|
up: () => {
|
||||||
|
const raw = ActiveFiresUnion.rawCollection();
|
||||||
|
raw.createIndex({ centerid: '2dsphere' });
|
||||||
|
raw.createIndex({ shape: '2dsphere' });
|
||||||
|
raw.createIndex({ when: 1 });
|
||||||
|
raw.createIndex({ createdAt: 1 });
|
||||||
|
raw.createIndex({ updatedAt: 1 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Set createdAt in users & subs
|
// Set createdAt in users & subs
|
||||||
Migrations.migrateTo('latest');
|
Migrations.migrateTo('latest');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,92 +1,19 @@
|
||||||
/* eslint-disable import/no-absolute-path */
|
/* eslint-disable import/no-absolute-path */
|
||||||
|
|
||||||
import i18n from 'i18next';
|
|
||||||
import { Meteor } from 'meteor/meteor';
|
import { Meteor } from 'meteor/meteor';
|
||||||
import moment from 'moment';
|
|
||||||
import { dateLongFormat } from '/imports/api/Common/dates';
|
|
||||||
import Notifications from '/imports/api/Notifications/Notifications';
|
import Notifications from '/imports/api/Notifications/Notifications';
|
||||||
// import sendMail from '/imports/startup/server/email';
|
import processNotif from '/imports/modules/server/notificationsProcess.js';
|
||||||
import sendEmail, { 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 { trim } from '/imports/ui/components/NotificationsObserver/util.js';
|
|
||||||
|
|
||||||
Meteor.startup(() => {
|
Meteor.startup(() => {
|
||||||
// https://www.npmjs.com/package/google-maps-image-api-url
|
if (isMailServerMaster) {
|
||||||
// https://stackoverflow.com/questions/24355007/is-there-no-way-to-embed-a-google-map-into-an-html-email
|
Notifications.find().observe({
|
||||||
// https://developers.google.com/maps/documentation/static-maps/intro
|
added: function notifAdded(notif) {
|
||||||
|
processNotif(notif);
|
||||||
function imgUrl(lat, lng) {
|
},
|
||||||
return image({
|
changed: function notifChanged(updatedNotif) { // , oldNotif) {
|
||||||
key: Meteor.settings.gmaps.key,
|
processNotif(updatedNotif);
|
||||||
type: 'staticmap',
|
}
|
||||||
center: `${lat},${lng}`,
|
|
||||||
size: '640x480',
|
|
||||||
zoom: 16,
|
|
||||||
maptype: 'hybrid',
|
|
||||||
language: 'es',
|
|
||||||
markers: `icon: ${Meteor.settings.private.fireIconUrl}|${lat},${lng}`
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
function imgEl(lat, lng) {
|
|
||||||
return `<img src="${imgUrl(lat, lng)}" width="640" height="480"/>`;
|
|
||||||
} */
|
|
||||||
|
|
||||||
function process(notif) {
|
|
||||||
if (notif.type === 'web' && !notif.emailNotified) {
|
|
||||||
const user = Meteor.users.findOne({ _id: notif.userId });
|
|
||||||
const { 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() } });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Notifications.find().observe({
|
|
||||||
added: function notifAdded(notif) {
|
|
||||||
process(notif);
|
|
||||||
},
|
|
||||||
changed: function notifChanged(updatedNotif, oldNotif) {
|
|
||||||
process(updatedNotif);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,17 @@ import RavenLogger from 'meteor/flowkey:raven';
|
||||||
import { Meteor } from 'meteor/meteor';
|
import { Meteor } from 'meteor/meteor';
|
||||||
|
|
||||||
const ravenOptions = {};
|
const ravenOptions = {};
|
||||||
|
const publicDSN = Meteor.settings.public.sentryPublicDSN;
|
||||||
|
const privateDSN = Meteor.settings.sentryPrivateDSN;
|
||||||
|
const enabled = publicDSN && privateDSN;
|
||||||
|
|
||||||
const ravenLogger = new RavenLogger({
|
const ravenLogger = enabled ? new RavenLogger({
|
||||||
publicDSN: Meteor.settings.public.sentryPublicDSN,
|
publicDSN,
|
||||||
privateDSN: Meteor.settings.sentryPrivateDSN,
|
privateDSN,
|
||||||
shouldCatchConsoleError: true, // default true
|
shouldCatchConsoleError: true, // default true
|
||||||
trackUser: true // default false
|
trackUser: true // default false
|
||||||
}, ravenOptions);
|
}, ravenOptions) : { log: (error) => { console.log(error); } };
|
||||||
|
|
||||||
|
console.log(`ravenLogger ${enabled ? 'enabled' : 'disabled'}`);
|
||||||
|
|
||||||
export default ravenLogger;
|
export default ravenLogger;
|
||||||
|
|
|
||||||
|
|
@ -1,69 +1 @@
|
||||||
/* global Restivus */
|
import '../../api/Rest/Rest.js';
|
||||||
/* eslint-disable import/no-absolute-path */
|
|
||||||
|
|
||||||
import { Meteor } from 'meteor/meteor';
|
|
||||||
import Fires from '/imports/api/Fires/Fires';
|
|
||||||
import SiteSettings from '/imports/api/SiteSettings/SiteSettings';
|
|
||||||
import ActiveFiresCollection from '/imports/api/ActiveFires/ActiveFires';
|
|
||||||
|
|
||||||
Meteor.startup(() => {
|
|
||||||
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'],
|
|
||||||
* 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/last-fire-detected
|
|
||||||
apiV1.addRoute('status/active-fires-count', { authRequired: false }, {
|
|
||||||
get: function get() {
|
|
||||||
return { total: ActiveFiresCollection.find({}).count() };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
import SegfaultHandler from 'segfault-handler';
|
|
||||||
import moment from 'moment';
|
|
||||||
// import ravenLogger from './ravenLogger';
|
|
||||||
|
|
||||||
// https://github.com/ddopson/node-segfault-handler
|
|
||||||
|
|
||||||
const dateFileFormat = moment().format('YYYYMMDD_HH:mm:ss');
|
|
||||||
|
|
||||||
SegfaultHandler.registerHandler(`/var/tmp/tcef-stacktrace-${dateFileFormat}.log`);
|
|
||||||
|
|
||||||
|
|
||||||
// This callaback does not work:
|
|
||||||
// https://github.com/ddopson/node-segfault-handler/issues/49
|
|
||||||
|
|
||||||
/* SegfaultHandler.registerHandler('/var/tmp/tcef-crash.log', (signal, address, stack) => {
|
|
||||||
* // Do what you want with the signal, address, or stack (array)
|
|
||||||
* // This callback will execute before the signal is forwarded on.
|
|
||||||
* ravenLogger.log(stack, signal);
|
|
||||||
* }); */
|
|
||||||
|
|
||||||
// Only uncomment for segv tests
|
|
||||||
// SegfaultHandler.causeSegfault();
|
|
||||||
|
|
||||||
// Or this for normal error (should go to raven in configured)
|
|
||||||
// throw new Error('This is just a test of error in server');
|
|
||||||
|
|
@ -4,6 +4,8 @@
|
||||||
import Fires from '/imports/api/Fires/Fires';
|
import Fires from '/imports/api/Fires/Fires';
|
||||||
// import Comments from '/imports/api/Comments/Comments';
|
// import Comments from '/imports/api/Comments/Comments';
|
||||||
|
|
||||||
|
const firesMapEnabled = false;
|
||||||
|
|
||||||
sitemaps.add('/sitemap.xml', () => {
|
sitemaps.add('/sitemap.xml', () => {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
|
|
||||||
|
|
@ -20,21 +22,23 @@ sitemaps.add('/sitemap.xml', () => {
|
||||||
|
|
||||||
// When user has public page
|
// When user has public page
|
||||||
/* const users = Meteor.users.find().fetch();
|
/* const users = Meteor.users.find().fetch();
|
||||||
_.each(users, function(user) {
|
_.each(users, function(user) {
|
||||||
out.push({
|
out.push({
|
||||||
page: '/persona/' + user.username,
|
page: '/persona/' + user.username,
|
||||||
lastmod: user.updatedAt
|
lastmod: user.updatedAt
|
||||||
});
|
});
|
||||||
}); */
|
}); */
|
||||||
|
|
||||||
Fires.find({}, { limit: 100, sort: { createdAt: -1 } }).fetch().forEach((fire) => {
|
if (firesMapEnabled) {
|
||||||
// Search the last comment of tha fire
|
Fires.find({}, { limit: 100, sort: { createdAt: -1 } }).fetch().forEach((fire) => {
|
||||||
const lastComment = Comments.getCollection().findOne({ referenceId: `fire-${fire._id}` }, { sort: { createdAt: -1 } });
|
// Search the last comment of tha fire
|
||||||
out.push({
|
const lastComment = Comments.getCollection().findOne({ referenceId: `fire-${fire._id}` }, { sort: { createdAt: -1 } });
|
||||||
page: `/fire/archive/${fire._id._str}`,
|
out.push({
|
||||||
lastmod: lastComment ? lastComment.createdAt : fire.updatedAt
|
page: `/fire/archive/${fire._id._str}`,
|
||||||
|
lastmod: lastComment ? lastComment.createdAt : fire.updatedAt
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,18 @@ import { Meteor } from 'meteor/meteor';
|
||||||
import Subscriptions from '/imports/api/Subscriptions/Subscriptions';
|
import Subscriptions from '/imports/api/Subscriptions/Subscriptions';
|
||||||
import SiteSettings from '/imports/api/SiteSettings/SiteSettings';
|
import SiteSettings from '/imports/api/SiteSettings/SiteSettings';
|
||||||
import Perlin from 'loms.perlin';
|
import Perlin from 'loms.perlin';
|
||||||
import L from 'leaflet-headless';
|
import './leaflet-workaround';
|
||||||
|
import L from 'leaflet';
|
||||||
import calcUnion from '/imports/ui/components/Maps/SubsUnion/Unify';
|
import calcUnion from '/imports/ui/components/Maps/SubsUnion/Unify';
|
||||||
|
import { isMailServerMaster } from '/imports/startup/server/email';
|
||||||
|
|
||||||
// sudo apt-get install libcairo2-dev libjpeg-dev libgif-dev
|
// sudo apt-get install libcairo2-dev libjpeg-dev libgif-dev
|
||||||
|
|
||||||
Meteor.startup(() => {
|
Meteor.startup(() => {
|
||||||
|
if (!isMailServerMaster) {
|
||||||
|
console.log('We only process subsUnion in master');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const debug = true; // Meteor.isDevelopment;
|
const debug = true; // Meteor.isDevelopment;
|
||||||
Perlin.seed(Math.random());
|
Perlin.seed(Math.random());
|
||||||
|
|
||||||
|
|
@ -30,9 +36,8 @@ Meteor.startup(() => {
|
||||||
const noNoisy = sub => sub;
|
const noNoisy = sub => sub;
|
||||||
|
|
||||||
const process = (isPublic) => {
|
const process = (isPublic) => {
|
||||||
const group = new L.FeatureGroup();
|
|
||||||
const subscribers = Subscriptions.find().fetch();
|
const subscribers = Subscriptions.find().fetch();
|
||||||
const result = calcUnion(subscribers, group, isPublic ? addNoisy : noNoisy);
|
const result = calcUnion(L, subscribers, isPublic ? addNoisy : noNoisy, true);
|
||||||
const union = result[0];
|
const union = result[0];
|
||||||
const bounds = result[1];
|
const bounds = result[1];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Line } from 'react-progressbar.js';
|
import { Meteor } from 'meteor/meteor';
|
||||||
|
import { Line } from 'react-progress-bar.js';
|
||||||
|
|
||||||
import './LoadingBar.scss';
|
import './LoadingBar.scss';
|
||||||
|
|
||||||
|
|
@ -10,7 +11,7 @@ import './LoadingBar.scss';
|
||||||
const LoadingBar = ({ progress }) => (
|
const LoadingBar = ({ progress }) => (
|
||||||
<div className="loading-bar">
|
<div className="loading-bar">
|
||||||
<Line
|
<Line
|
||||||
progress={progress}
|
progress={Meteor.status().status !== 'connected' ? Meteor.status().retryCount / 10 : progress}
|
||||||
options={{ strokeWidth: 2, color: '#5A7636' }}
|
options={{ strokeWidth: 2, color: '#5A7636' }}
|
||||||
initialAnimate
|
initialAnimate
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,8 @@ class DefMapLayers extends Component {
|
||||||
<TileLayer
|
<TileLayer
|
||||||
opacity={defOpacity}
|
opacity={defOpacity}
|
||||||
attribution="© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors"
|
attribution="© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors"
|
||||||
url="https://tiles.wmflabs.org/bw-mapnik/{z}/{x}/{y}.png"
|
// url="https://tiles.wmflabs.org/bw-mapnik/{z}/{x}/{y}.png"
|
||||||
|
url="https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png"
|
||||||
/>
|
/>
|
||||||
</BaseLayer>);
|
</BaseLayer>);
|
||||||
const osmlayer = (
|
const osmlayer = (
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Circle } from 'react-leaflet';
|
import { GeoJSON } from 'react-leaflet';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { rectangleAround } from 'map-common-utils';
|
||||||
import { onMarkClick } from './MarkListeners';
|
import { onMarkClick } from './MarkListeners';
|
||||||
import FirePopup from './FirePopup';
|
import FirePopup from './FirePopup';
|
||||||
|
|
||||||
|
|
||||||
class FireCircleMark extends Component {
|
class FireCircleMark extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
@ -22,22 +22,26 @@ class FireCircleMark extends Component {
|
||||||
lat,
|
lat,
|
||||||
lon,
|
lon,
|
||||||
scan,
|
scan,
|
||||||
|
track,
|
||||||
nasa,
|
nasa,
|
||||||
id,
|
id,
|
||||||
history,
|
history,
|
||||||
when,
|
when,
|
||||||
t
|
t
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
const rect = rectangleAround({ lat, lon }, track, track);
|
||||||
return (
|
return (
|
||||||
<Circle center={[lat, lon]} color="red" stroke={false} fillOpacity="1" fill radius={scan * 500} onClick={this.onClick}>
|
<GeoJSON data={rect} color="red" stroke width="1" opacity=".4" fillOpacity="1">
|
||||||
<FirePopup t={t} history={history} id={id} nasa={nasa} lat={lat} lon={lon} when={when} />
|
<FirePopup t={t} history={history} id={id} nasa={nasa} lat={lat} lon={lon} when={when} />
|
||||||
</Circle>
|
</GeoJSON>
|
||||||
);
|
);
|
||||||
|
/* <Circle center={[lat, lon]} color="red" stroke={false} fillOpacity="1" fill radius={scan * 500} onClick={this.onClick}> */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FireCircleMark.propTypes = {
|
FireCircleMark.propTypes = {
|
||||||
scan: PropTypes.number.isRequired,
|
scan: PropTypes.number.isRequired,
|
||||||
|
track: PropTypes.number.isRequired,
|
||||||
lat: PropTypes.number.isRequired,
|
lat: PropTypes.number.isRequired,
|
||||||
lon: PropTypes.number.isRequired,
|
lon: PropTypes.number.isRequired,
|
||||||
nasa: PropTypes.bool.isRequired,
|
nasa: PropTypes.bool.isRequired,
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ import React, { Component, Fragment } from 'react';
|
||||||
import { CircleMarker, Marker, Tooltip } from 'react-leaflet';
|
import { CircleMarker, Marker, Tooltip } from 'react-leaflet';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { fireIconS, fireIconM, fireIconL, nFireIcon, industryIcon, regIndustryIcon } from '/imports/ui/components/Maps/Icons';
|
import { fireIconS, fireIconM, fireIconL, nFireIcon, industryIcon, regIndustryIcon } from '/imports/ui/components/Maps/Icons';
|
||||||
import { onMarkClick } from './MarkListeners';
|
|
||||||
import { translate } from 'react-i18next';
|
import { translate } from 'react-i18next';
|
||||||
|
import { onMarkClick } from './MarkListeners';
|
||||||
import FirePopup from './FirePopup';
|
import FirePopup from './FirePopup';
|
||||||
|
|
||||||
class FireIconMark extends Component {
|
class FireIconMark extends Component {
|
||||||
|
|
@ -30,17 +30,7 @@ class FireIconMark extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
lat,
|
lat, lon, scan, track, nasa, id, history, falsePositives, industries, neighbour, when, t
|
||||||
lon,
|
|
||||||
scan,
|
|
||||||
nasa,
|
|
||||||
id,
|
|
||||||
history,
|
|
||||||
falsePositives,
|
|
||||||
industries,
|
|
||||||
neighbour,
|
|
||||||
when,
|
|
||||||
t
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -79,6 +69,7 @@ FireIconMark.propTypes = {
|
||||||
lat: PropTypes.number.isRequired,
|
lat: PropTypes.number.isRequired,
|
||||||
lon: PropTypes.number.isRequired,
|
lon: PropTypes.number.isRequired,
|
||||||
scan: PropTypes.number,
|
scan: PropTypes.number,
|
||||||
|
track: PropTypes.number,
|
||||||
nasa: PropTypes.bool,
|
nasa: PropTypes.bool,
|
||||||
falsePositives: PropTypes.bool.isRequired,
|
falsePositives: PropTypes.bool.isRequired,
|
||||||
industries: PropTypes.bool.isRequired,
|
industries: PropTypes.bool.isRequired,
|
||||||
|
|
|
||||||
22
imports/ui/components/Maps/FireListUnion.js
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
/* eslint-disable import/no-absolute-path */
|
||||||
|
/* eslint-disable react/jsx-indent-props */
|
||||||
|
/* eslint-disable react/jsx-indent */
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import FirePolygonMark from '/imports/ui/components/Maps/FirePolygonMark';
|
||||||
|
|
||||||
|
export default function FireListUnion(props) {
|
||||||
|
const {
|
||||||
|
firesUnion, nasa, t, history
|
||||||
|
} = props;
|
||||||
|
/* console.log(`Using marks: ${useMarks}, using pixels: ${usePixel}`); */
|
||||||
|
const items = firesUnion.map(({ _id, ...otherProps }) => (<FirePolygonMark t={t} history={history} id={_id} key={_id} nasa={nasa} {...otherProps} />));
|
||||||
|
return (<div style={{ display: 'none' }}>{items}</div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
FireListUnion.propTypes = {
|
||||||
|
firesUnion: PropTypes.array.isRequired,
|
||||||
|
nasa: PropTypes.bool.isRequired,
|
||||||
|
history: PropTypes.object.isRequired,
|
||||||
|
t: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
@ -3,31 +3,32 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { CircleMarker } from 'react-leaflet';
|
import { CircleMarker } from 'react-leaflet';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import FirePopup from './FirePopup';
|
|
||||||
import { translate } from 'react-i18next';
|
import { translate } from 'react-i18next';
|
||||||
|
import FirePopup from './FirePopup';
|
||||||
|
|
||||||
/* Less acurate (1 pixel per fire) but faster */
|
/* Less acurate (1 pixel per fire) but faster */
|
||||||
const FirePixel = ({
|
const FirePixel = ({
|
||||||
lat, lon, nasa, id, when, t, history
|
lat, lon, nasa, id, when, t, history, track
|
||||||
}) => (
|
}) => (
|
||||||
<CircleMarker
|
<CircleMarker
|
||||||
center={[lat, lon]}
|
center={[lat, lon]}
|
||||||
color={nasa ? 'red' : '#D35400'}
|
color={nasa ? 'red' : '#D35400'}
|
||||||
stroke={false}
|
stroke={false}
|
||||||
fillOpacity="1"
|
fillOpacity="1"
|
||||||
fill
|
fill
|
||||||
radius={nasa ? 1 : 2}
|
radius={nasa ? 1 : 2}
|
||||||
>
|
>
|
||||||
<FirePopup t={t} history={history} id={id} nasa={nasa} lat={lat} lon={lon} when={when} />
|
<FirePopup t={t} history={history} id={id} nasa={nasa} lat={lat} lon={lon} when={when} />
|
||||||
</CircleMarker>
|
</CircleMarker>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
FirePixel.propTypes = {
|
FirePixel.propTypes = {
|
||||||
lat: PropTypes.number.isRequired,
|
lat: PropTypes.number.isRequired,
|
||||||
lon: PropTypes.number.isRequired,
|
lon: PropTypes.number.isRequired,
|
||||||
id: PropTypes.object.isRequired,
|
id: PropTypes.object.isRequired,
|
||||||
history: PropTypes.object.isRequired,
|
history: PropTypes.object.isRequired,
|
||||||
|
scan: PropTypes.number,
|
||||||
|
track: PropTypes.number,
|
||||||
when: PropTypes.instanceOf(Date),
|
when: PropTypes.instanceOf(Date),
|
||||||
nasa: PropTypes.bool.isRequired,
|
nasa: PropTypes.bool.isRequired,
|
||||||
t: PropTypes.func.isRequired
|
t: PropTypes.func.isRequired
|
||||||
|
|
|
||||||
48
imports/ui/components/Maps/FirePolygonMark.js
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { GeoJSON } from 'react-leaflet';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { onMarkClick } from './MarkListeners';
|
||||||
|
import FirePopup from './FirePopup';
|
||||||
|
|
||||||
|
class FirePolygonMark extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.t = props.t;
|
||||||
|
this.onClick = this.onClick.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
onClick() {
|
||||||
|
const { history, nasa, id } = this.props;
|
||||||
|
onMarkClick(history, nasa, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
nasa,
|
||||||
|
id,
|
||||||
|
shape,
|
||||||
|
centerid,
|
||||||
|
history,
|
||||||
|
when,
|
||||||
|
t
|
||||||
|
} = this.props;
|
||||||
|
const lon = centerid.coordinates[0];
|
||||||
|
const lat = centerid.coordinates[1];
|
||||||
|
return (
|
||||||
|
<GeoJSON data={shape} color="orange" stroke width="1" opacity=".2" fillOpacity=".0" />
|
||||||
|
);
|
||||||
|
/* <FirePopup t={t} history={history} id={id} nasa={nasa} lat={lat} lon={lon} when={when} /> */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FirePolygonMark.propTypes = {
|
||||||
|
id: PropTypes.object.isRequired,
|
||||||
|
nasa: PropTypes.bool.isRequired,
|
||||||
|
shape: PropTypes.object.isRequired,
|
||||||
|
centerid: PropTypes.object.isRequired,
|
||||||
|
history: PropTypes.object.isRequired,
|
||||||
|
when: PropTypes.instanceOf(Date).isRequired,
|
||||||
|
t: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FirePolygonMark;
|
||||||
|
|
@ -33,8 +33,7 @@ const subsUnion = (union, options) => {
|
||||||
options.map.leafletElement.fitBounds(L.latLngBounds(bounds._northEast, bounds._southWest));
|
options.map.leafletElement.fitBounds(L.latLngBounds(bounds._northEast, bounds._southWest));
|
||||||
}
|
}
|
||||||
} else if (options.subs.length > 0) {
|
} else if (options.subs.length > 0) {
|
||||||
const unionGroup = new L.FeatureGroup();
|
const result = calcUnion(L, options.subs, sub => sub, true);
|
||||||
const result = calcUnion(options.subs, unionGroup, sub => sub);
|
|
||||||
const unionJson = result[0];
|
const unionJson = result[0];
|
||||||
const bounds = result[1];
|
const bounds = result[1];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,48 +1,9 @@
|
||||||
import { check } from 'meteor/check';
|
import { calcUnion as calcUnionWoBounds } from 'map-common-utils';
|
||||||
import LGeo from 'leaflet-geodesy';
|
|
||||||
import tunion from '@turf/union';
|
|
||||||
import ttrunc from '@turf/truncate';
|
|
||||||
|
|
||||||
// https://stackoverflow.com/questions/35394577/leaflet-js-union-merge-circles
|
const calcUnion = (L, subs, decorated, typeCircle) => {
|
||||||
const truncOptions = { precision: 6, coordinates: 2 };
|
const unionJson = calcUnionWoBounds(subs, decorated, typeCircle);
|
||||||
|
// return [unionJson, L.geoJSON(unionJson).getBounds()];
|
||||||
const unify = (polyList) => {
|
return [unionJson, unionJson === null ? null : L.geoJSON(unionJson).getBounds()];
|
||||||
let unionTemp;
|
|
||||||
for (let i = 0; i < polyList.length; i += 1) {
|
|
||||||
const pol = polyList[i].toGeoJSON();
|
|
||||||
const cleanPol = ttrunc(pol, truncOptions);
|
|
||||||
if (i === 0) {
|
|
||||||
unionTemp = cleanPol;
|
|
||||||
} else {
|
|
||||||
unionTemp = ttrunc(tunion(unionTemp, cleanPol), truncOptions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return unionTemp;
|
|
||||||
};
|
|
||||||
|
|
||||||
const calcUnion = (subs, group, decorated) => {
|
|
||||||
const unionGroup = group;
|
|
||||||
const copts = {
|
|
||||||
parts: 144
|
|
||||||
};
|
|
||||||
subs.forEach((osub) => {
|
|
||||||
try {
|
|
||||||
if (osub.location && osub.location.lat && osub.location.lon && osub.distance) {
|
|
||||||
check(osub.location.lon, Number);
|
|
||||||
check(osub.location.lat, Number);
|
|
||||||
check(osub.distance, Number);
|
|
||||||
const dsub = decorated(osub);
|
|
||||||
const circle = LGeo.circle([dsub.location.lat, dsub.location.lon], dsub.distance * 1000, copts);
|
|
||||||
circle.addTo(unionGroup);
|
|
||||||
} else {
|
|
||||||
console.info(`Wrong subscription ${JSON.stringify(osub)}`);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e, `Wrong subscription trying to make union ${JSON.stringify(osub)}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const unionJson = unify(unionGroup.getLayers());
|
|
||||||
return [unionJson, unionGroup.getBounds()];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default calcUnion;
|
export default calcUnion;
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import { trim } from './util.js';
|
||||||
function process(notif) {
|
function process(notif) {
|
||||||
// No already notified
|
// No already notified
|
||||||
if (Push.Permission.has()) {
|
if (Push.Permission.has()) {
|
||||||
if (!notif.webNotified) {
|
if (!notif.notified && notif.type === 'web') {
|
||||||
Push.create(i18n.t('AppName'), {
|
Push.create(i18n.t('AppName'), {
|
||||||
body: `${trim(notif.content)} (${i18n.t('fireDetected', { when: dateFromNow(notif.when) })})`,
|
body: `${trim(notif.content)} (${i18n.t('fireDetected', { when: dateFromNow(notif.when) })})`,
|
||||||
icon: '/n-fire-marker.png',
|
icon: '/n-fire-marker.png',
|
||||||
|
|
@ -47,7 +47,7 @@ Meteor.startup(() => {
|
||||||
if (Meteor.userId()) {
|
if (Meteor.userId()) {
|
||||||
Meteor.subscribe('mynotifications');
|
Meteor.subscribe('mynotifications');
|
||||||
// Check for notifications not processed at startup
|
// Check for notifications not processed at startup
|
||||||
Notifications.find({ webNotified: null }).forEach((notif) => {
|
Notifications.find({ notified: null, type: 'web' }).forEach((notif) => {
|
||||||
process(notif);
|
process(notif);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import Blaze from 'meteor/gadicc:blaze-react-component';
|
||||||
|
|
||||||
const Reconnect = ({ t, authenticated }) => (
|
const Reconnect = ({ t, authenticated }) => (
|
||||||
<div>
|
<div>
|
||||||
{ !authenticated &&
|
{ true && /* !authenticated && */
|
||||||
<Blaze
|
<Blaze
|
||||||
template="meteorStatus"
|
template="meteorStatus"
|
||||||
textDisconnect={t('Desconectado del servidor, reconectando en %delay% segundos.')}
|
textDisconnect={t('Desconectado del servidor, reconectando en %delay% segundos.')}
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,8 @@ class LocationListener extends Component {
|
||||||
// https://stackoverflow.com/questions/43512450/react-router-v4-route-onchange-event
|
// https://stackoverflow.com/questions/43512450/react-router-v4-route-onchange-event
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
// https://github.com/GoogleChrome/rendertron#rendering-budget-timeout
|
||||||
|
window.renderComplete = true;
|
||||||
this.handleLocationChange(this.context.router.history.location);
|
this.handleLocationChange(this.context.router.history.location);
|
||||||
this.unlisten =
|
this.unlisten =
|
||||||
this.context.router.history.listen(this.handleLocationChange);
|
this.context.router.history.listen(this.handleLocationChange);
|
||||||
|
|
@ -96,66 +98,66 @@ const App = props => (
|
||||||
/* https://react.i18next.com/components/i18nextprovider.html */
|
/* https://react.i18next.com/components/i18nextprovider.html */
|
||||||
<div>
|
<div>
|
||||||
{props.i18nReady.get() &&
|
{props.i18nReady.get() &&
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<I18nextProvider i18n={i18n}>
|
<I18nextProvider i18n={i18n}>
|
||||||
<ErrorBoundary appName={i18n.t('AppNameFull')} title={i18n.t('general-error-title')} subTitle={i18n.t('general-error-description')}>
|
<ErrorBoundary appName={i18n.t('AppNameFull')} title={i18n.t('general-error-title')} subTitle={i18n.t('general-error-description')}>
|
||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
<LocationListener>
|
<LocationListener>
|
||||||
{ !props.loading &&
|
{ !props.loading &&
|
||||||
<div className="App">
|
<div className="App">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<meta charSet="utf-8" />
|
<meta charSet="utf-8" />
|
||||||
<title>{i18n.t('AppName')}</title>
|
<title>{i18n.t('AppName')}</title>
|
||||||
<meta name="description" content={`${i18n.t('AppDescrip')}: ${i18n.t('AppDescripLong')}`} />
|
<meta name="description" content={`${i18n.t('AppDescrip')}: ${i18n.t('AppDescripLong')}`} />
|
||||||
<link rel="alternate" href={`https://fires.comunes.org${history.location.pathname}`} hrefLang="en" />
|
<link rel="alternate" href={`https://fires.comunes.org${history.location.pathname}`} hrefLang="en" />
|
||||||
<link rel="alternate" href={`https://fuegos.comunes.org${history.location.pathname}`} hrefLang="es" />
|
<link rel="alternate" href={`https://fuegos.comunes.org${history.location.pathname}`} hrefLang="es" />
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<Navigation {...props} />
|
<Navigation {...props} />
|
||||||
<ReSendEmail {...props} />
|
<ReSendEmail {...props} />
|
||||||
|
|
||||||
<Grid>
|
<Grid>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact name="index" path="/" component={Index} />
|
<Route exact name="index" path="/" component={Index} />
|
||||||
<Authenticated exact path="/subscriptions" component={Subscriptions} {...props} />
|
<Authenticated exact path="/subscriptions" component={Subscriptions} {...props} />
|
||||||
<Authenticated exact path="/subscriptions/new" component={NewSubscription} {...props} />
|
<Authenticated exact path="/subscriptions/new" component={NewSubscription} {...props} />
|
||||||
<Authenticated exact path="/subscriptions/:_id" component={ViewSubscription} {...props} />
|
<Authenticated exact path="/subscriptions/:_id" component={ViewSubscription} {...props} />
|
||||||
<Authenticated exact path="/subscriptions/:_id/edit" component={EditSubscription} {...props} />
|
<Authenticated exact path="/subscriptions/:_id/edit" component={EditSubscription} {...props} />
|
||||||
<Authenticated exact path="/profile" component={Profile} {...props} />
|
<Authenticated exact path="/profile" component={Profile} {...props} />
|
||||||
<Authenticated exact path="/status" component={Status} {...props} />
|
<Authenticated exact path="/status" component={Status} {...props} />
|
||||||
<Route path="/fires" component={FiresMap} industries={false} {...props} />
|
<Route path="/fires" component={FiresMap} industries={false} {...props} />
|
||||||
<Route path="/zones" component={ZonesMap} {...props} />
|
<Route path="/zones" component={ZonesMap} {...props} />
|
||||||
|
|
||||||
<Route path="/fire/:type(active|archive|alert)/:id" component={Fires} {...props} />
|
<Route path="/fire/:type(active|archive|alert)/:id" component={Fires} {...props} />
|
||||||
<Route path="/fire/:id" component={Fires} {...props} />
|
<Route path="/fire/:id" component={Fires} {...props} />
|
||||||
<Public path="/auth/:token" component={Auth} {...props} />
|
<Public path="/auth/:token" component={Auth} {...props} />
|
||||||
<Public path="/signup" component={Signup} {...props} />
|
<Public path="/signup" component={Signup} {...props} />
|
||||||
<Public path="/login" component={Login} {...props} />
|
<Public path="/login" component={Login} {...props} />
|
||||||
<Route path="/logout" component={Logout} {...props} />
|
<Route path="/logout" component={Logout} {...props} />
|
||||||
<Route path="/sandbox" component={Sandbox} {...props} />
|
<Route path="/sandbox" component={Sandbox} {...props} />
|
||||||
<Route path="/error" component={TestError} {...props} />
|
<Route path="/error" component={TestError} {...props} />
|
||||||
|
|
||||||
<Route name="verify-email" path="/verify-email/:token" component={VerifyEmail} />
|
<Route name="verify-email" path="/verify-email/:token" component={VerifyEmail} />
|
||||||
<Route name="recover-password" path="/recover-password" component={RecoverPassword} />
|
<Route name="recover-password" path="/recover-password" component={RecoverPassword} />
|
||||||
<Route name="reset-password" path="/reset-password/:token" component={ResetPassword} />
|
<Route name="reset-password" path="/reset-password/:token" component={ResetPassword} />
|
||||||
<Route name="terms" path="/terms" component={Terms} />
|
<Route name="terms" path="/terms" component={Terms} />
|
||||||
<Route name="privacy" path="/privacy" component={Privacy} />
|
<Route name="privacy" path="/privacy" component={Privacy} />
|
||||||
<Route name="license" path="/license" component={License} />
|
<Route name="license" path="/license" component={License} />
|
||||||
<Route name="credits" path="/credits" component={Credits} />
|
<Route name="credits" path="/credits" component={Credits} />
|
||||||
<Route name="about" path="/about" component={About} />
|
<Route name="about" path="/about" component={About} />
|
||||||
|
|
||||||
<Route component={NotFound} />
|
<Route component={NotFound} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Footer />
|
<Footer />
|
||||||
<Reconnect {...props} />
|
<Reconnect {...props} />
|
||||||
<Feedback {...props} />
|
<Feedback {...props} />
|
||||||
{props.i18nReady.get() && <Blaze template="cookieConsent" /> }
|
{props.i18nReady.get() && <Blaze template="cookieConsent" /> }
|
||||||
</div>}
|
</div>}
|
||||||
</LocationListener>
|
</LocationListener>
|
||||||
</Router>
|
</Router>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</I18nextProvider>
|
</I18nextProvider>
|
||||||
</ErrorBoundary> }
|
</ErrorBoundary> }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -184,7 +186,9 @@ export default withTracker(() => {
|
||||||
const loading = !Roles.subscription.ready() || !i18nReady.get();
|
const loading = !Roles.subscription.ready() || !i18nReady.get();
|
||||||
const name = user && user.profile && user.profile.name && getUserName(user.profile.name);
|
const name = user && user.profile && user.profile.name && getUserName(user.profile.name);
|
||||||
const emailAddress = user && user.emails && user.emails[0].address;
|
const emailAddress = user && user.emails && user.emails[0].address;
|
||||||
// console.log(`i18n ready?: ${i18nReady.get()}`);
|
Meteor.autorun(() => {
|
||||||
|
console.log(`i18n ready?: ${i18nReady.get()}`);
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
loading,
|
loading,
|
||||||
loggingIn,
|
loggingIn,
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,8 @@ import { Row, Col, Alert, FormGroup } from 'react-bootstrap';
|
||||||
import { Meteor } from 'meteor/meteor';
|
import { Meteor } from 'meteor/meteor';
|
||||||
import { Bert } from 'meteor/themeteorchef:bert';
|
import { Bert } from 'meteor/themeteorchef:bert';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { Map, Circle } from 'react-leaflet';
|
import { Map, GeoJSON } from 'react-leaflet';
|
||||||
|
import { rectangleAround } from 'map-common-utils';
|
||||||
import Blaze from 'meteor/gadicc:blaze-react-component';
|
import Blaze from 'meteor/gadicc:blaze-react-component';
|
||||||
import DefMapLayers from '/imports/ui/components/Maps/DefMapLayers';
|
import DefMapLayers from '/imports/ui/components/Maps/DefMapLayers';
|
||||||
import NotFound from '/imports/ui/pages/NotFound/NotFound';
|
import NotFound from '/imports/ui/pages/NotFound/NotFound';
|
||||||
|
|
@ -115,8 +116,9 @@ class Fire extends React.Component {
|
||||||
t('Información adicional sobre fuego detectado en {{where}} el {{when}}', { where: fire.address, when: this.dateLongFormat }) :
|
t('Información adicional sobre fuego detectado en {{where}} el {{when}}', { where: fire.address, when: this.dateLongFormat }) :
|
||||||
t('Información adicional sobre fuego detectado el {{when}}', { when: this.dateLongFormat });
|
t('Información adicional sobre fuego detectado el {{when}}', { when: this.dateLongFormat });
|
||||||
}
|
}
|
||||||
|
const ready = fire && !loading;
|
||||||
return (fire && !loading ? (
|
const rect = ready ? rectangleAround({ lat: fire.lat, lon: fire.lon }, fire.track, fire.track) : null;
|
||||||
|
return (ready ? (
|
||||||
<div className="ViewFire">
|
<div className="ViewFire">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{t('AppName')}: {t('Información adicional sobre fuego')}</title>
|
<title>{t('AppName')}: {t('Información adicional sobre fuego')}</title>
|
||||||
|
|
@ -137,14 +139,13 @@ class Fire extends React.Component {
|
||||||
zoom={13}
|
zoom={13}
|
||||||
>
|
>
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Circle
|
<GeoJSON
|
||||||
center={[fire.lat, fire.lon]}
|
|
||||||
color="red"
|
|
||||||
fillColor="red"
|
|
||||||
fillOpacity={0.0}
|
|
||||||
interactive={false}
|
|
||||||
radius={fire.scan ? fire.scan * 500 : 300}
|
|
||||||
ref={(circle) => { this.circle = circle; this.handleLeafletCircleLoad(circle); }}
|
ref={(circle) => { this.circle = circle; this.handleLeafletCircleLoad(circle); }}
|
||||||
|
data={rect}
|
||||||
|
color="red"
|
||||||
|
stroke
|
||||||
|
width="1"
|
||||||
|
fillOpacity="0.0"
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
<FireList
|
<FireList
|
||||||
|
|
@ -278,7 +279,7 @@ const FireContainer = withTracker(({ match }) => {
|
||||||
} else {
|
} else {
|
||||||
console.log('Seems a fire from enc hash');
|
console.log('Seems a fire from enc hash');
|
||||||
fromHash = true;
|
fromHash = true;
|
||||||
subscription = Meteor.subscribe('fireFromHash', id);
|
subscription = Meteor.subscribe('fireFromHash', id, match.params);
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log(`Type of '${fireType}' fire, active: ${active}, archive: ${archive}, fromHash: ${fromHash}`);
|
// console.log(`Type of '${fireType}' fire, active: ${active}, archive: ${archive}, fromHash: ${fromHash}`);
|
||||||
|
|
|
||||||
|
|
@ -8,19 +8,49 @@ import { translate, Trans } from 'react-i18next';
|
||||||
import FromNow from '/imports/ui/components/FromNow/FromNow';
|
import FromNow from '/imports/ui/components/FromNow/FromNow';
|
||||||
|
|
||||||
class FireStats extends Component {
|
class FireStats extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
lastFireDetected: props.lastFireDetected,
|
||||||
|
lastCheck: props.lastCheck,
|
||||||
|
loadingAll: props.loadingAll
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
if (this.props.lastCheck !== nextProps.lastCheck ||
|
||||||
|
this.props.lastFireDetected !== nextProps.lastFireDetected ||
|
||||||
|
this.props.loadingAll !== nextProps.loadingAll
|
||||||
|
) {
|
||||||
|
this.setState({
|
||||||
|
lastFireDetected: nextProps.lastFireDetected,
|
||||||
|
lastCheck: nextProps.lastCheck,
|
||||||
|
loadingAll: nextProps.loadingAll
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
|
return !(nextState.lastCheck === this.state.lastCheck &&
|
||||||
|
nextState.lastFireDetected === this.state.lastFireDetected &&
|
||||||
|
nextState.loadingAll === this.state.loadingAll);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
if (this.state.loadingAll) return (<Fragment><br /><em><Trans>Actualizando...</Trans></em></Fragment>);
|
||||||
<Fragment>
|
if (this.state.lastCheck && !this.state.lastFireDetected) {
|
||||||
{ this.props.lastCheck && this.props.lastFireDetected ?
|
return (<Fragment><br /><Trans>Actualizado <FromNow when={this.state.lastCheck} />.</Trans></Fragment>);
|
||||||
<Trans>Actualizado <FromNow when={this.props.lastCheck} />, último fuego detectado <FromNow when={this.props.lastFireDetected.when} />.</Trans>
|
}
|
||||||
: '' }
|
if (this.state.lastCheck && this.state.lastFireDetected) {
|
||||||
</Fragment>
|
return (<Fragment><br /><Trans>Actualizado <FromNow when={this.state.lastCheck} />, último fuego detectado <FromNow when={this.state.lastFireDetected.when} />.</Trans></Fragment>);
|
||||||
);
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FireStats.propTypes = {
|
FireStats.propTypes = {
|
||||||
lastFireDetected: PropTypes.object,
|
lastFireDetected: PropTypes.object,
|
||||||
|
loadingAll: PropTypes.bool.isRequired,
|
||||||
lastCheck: PropTypes.instanceOf(Date)
|
lastCheck: PropTypes.instanceOf(Date)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,11 @@ import 'leaflet-sleep/Leaflet.Sleep.js';
|
||||||
import geolocation from '/imports/startup/client/geolocation';
|
import geolocation from '/imports/startup/client/geolocation';
|
||||||
import CenterInMyPosition from '/imports/ui/components/CenterInMyPosition/CenterInMyPosition';
|
import CenterInMyPosition from '/imports/ui/components/CenterInMyPosition/CenterInMyPosition';
|
||||||
import FireList from '/imports/ui/components/Maps/FireList';
|
import FireList from '/imports/ui/components/Maps/FireList';
|
||||||
|
import FireListUnion from '/imports/ui/components/Maps/FireListUnion';
|
||||||
import subsUnion from '/imports/ui/components/Maps/SubsUnion/SubsUnion';
|
import subsUnion from '/imports/ui/components/Maps/SubsUnion/SubsUnion';
|
||||||
import DefMapLayers from '/imports/ui/components/Maps/DefMapLayers';
|
import DefMapLayers from '/imports/ui/components/Maps/DefMapLayers';
|
||||||
import FromNow from '/imports/ui/components/FromNow/FromNow';
|
|
||||||
import ActiveFiresCollection from '/imports/api/ActiveFires/ActiveFires';
|
import ActiveFiresCollection from '/imports/api/ActiveFires/ActiveFires';
|
||||||
|
import ActiveFiresUnion from '/imports/api/ActiveFiresUnion/ActiveFiresUnion';
|
||||||
import FireAlertsCollection from '/imports/api/FireAlerts/FireAlerts';
|
import FireAlertsCollection from '/imports/api/FireAlerts/FireAlerts';
|
||||||
import IndustriesCollection, { industriesRemap } from '/imports/api/Industries/Industries';
|
import IndustriesCollection, { industriesRemap } from '/imports/api/Industries/Industries';
|
||||||
import FalsePositivesCollection, { falsePositivesRemap } from '/imports/api/FalsePositives/FalsePositives';
|
import FalsePositivesCollection, { falsePositivesRemap } from '/imports/api/FalsePositives/FalsePositives';
|
||||||
|
|
@ -83,19 +84,6 @@ class FiresMap extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* shouldComponentUpdate(nextProps, nextState) {
|
|
||||||
* const notMoving = !nextState.moving;
|
|
||||||
* const markersChanged = this.state.useMarkers !== nextState.useMarkers;
|
|
||||||
* const unionChanged = this.state.showSubsUnion !== nextState.showSubsUnion;
|
|
||||||
* const otherViewport = this.state.viewport !== nextState.viewport;
|
|
||||||
* // const init = nextState.viewport.center === [0, 0];
|
|
||||||
* // console.log(notMoving ? 'Not moving map' : 'Moving map');
|
|
||||||
* // console.log(otherViewport ? 'Other viewport' : 'Not other viewport');
|
|
||||||
* console.log(`${otherViewport ? 'OTHER' : 'Not other'} viewport ${nextState.viewport.center} zoom: ${nextState.viewport.zoom}`);
|
|
||||||
* return this.state.init || (notMoving && otherViewport && this.state.moved) || unionChanged || markersChanged;
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
const notMoving = !nextState.moving;
|
const notMoving = !nextState.moving;
|
||||||
return notMoving;
|
return notMoving;
|
||||||
|
|
@ -197,11 +185,15 @@ class FiresMap extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { t } = this.props;
|
const { t } = this.props;
|
||||||
console.log(`Rendering ${this.props.loading ? 'loading' : 'LOADED'}, zoom ${this.state.viewport.zoom}, map ${this.props.activefires.length + this.props.firealerts.length} of ${this.props.activefirestotal} total. False positives: ${this.props.falsePositives.length}. Reactive ${this.state.viewport.zoom >= MAXZOOMREACTIVE}`);
|
const loading = this.props.loading || !this.props.subsready;
|
||||||
|
console.log(`Rendering ${loading ? 'loading' : 'LOADED'}, zoom ${this.state.viewport.zoom}, map ${this.props.activefires.length + this.props.firealerts.length} of ${this.props.activefirestotal} total. False positives: ${this.props.falsePositives.length}. Reactive ${this.state.viewport.zoom >= MAXZOOMREACTIVE}`);
|
||||||
const title = `${t('AppName')}: ${t('Fuegos activos')}`;
|
const title = `${t('AppName')}: ${t('Fuegos activos')}`;
|
||||||
|
|
||||||
|
if (Meteor.isDevelopment && this.props.activefires.length === 1) console.log(`Active fire: ${JSON.stringify(this.props.activefires[0])}`);
|
||||||
|
if (Meteor.isDevelopment) console.log(`Active fires total: ${this.props.activefires.length}`);
|
||||||
|
if (Meteor.isDevelopment) console.log(`Active fires union total: ${this.props.activefiresunion.length}`);
|
||||||
if (Meteor.isDevelopment) console.log(`False positives total: ${this.props.falsePositives.length}`);
|
if (Meteor.isDevelopment) console.log(`False positives total: ${this.props.falsePositives.length}`);
|
||||||
|
if (Meteor.isDevelopment) console.log(`Fire alerts total: ${this.props.firealerts.length}`);
|
||||||
if (Meteor.isDevelopment) console.log(`Industries total: ${this.props.industries.length}`);
|
if (Meteor.isDevelopment) console.log(`Industries total: ${this.props.industries.length}`);
|
||||||
|
|
||||||
if (!this.state.init) {
|
if (!this.state.init) {
|
||||||
|
|
@ -243,8 +235,8 @@ class FiresMap extends React.Component {
|
||||||
<Col xs={12} sm={6} md={6} lg={6} >
|
<Col xs={12} sm={6} md={6} lg={6} >
|
||||||
<p className="firesmap-legend">
|
<p className="firesmap-legend">
|
||||||
{ (this.props.activefires.length + this.props.firealerts.length) === 0 ?
|
{ (this.props.activefires.length + this.props.firealerts.length) === 0 ?
|
||||||
<Fragment><Trans parent="span" i18nKey="noActiveFireInMapCount">No hay fuegos activos en esta zona del mapa. <strong>{{ countTotal: this.props.activefirestotal }}</strong> fuegos activos en el mundo.</Trans> <FireStats {... this.props} /></Fragment> :
|
<Fragment><Trans parent="span" i18nKey="noActiveFireInMapCount">No hay fuegos activos en esta zona del mapa. <strong>{{ countTotal: this.props.activefirestotal }}</strong> fuegos activos en el mundo.</Trans> <FireStats loadingAll={loading} {... this.props} /></Fragment> :
|
||||||
<Fragment><Trans parent="span" i18nKey="activeFireInMapCount">En rojo, <strong>{{ count: this.props.activefires.length + this.props.firealerts.length }}</strong> fuegos activos. <strong>{{ countTotal: this.props.activefirestotal }}</strong> fuegos activos en el mundo.</Trans> <FireStats {... this.props} /></Fragment>
|
<Fragment><Trans parent="span" i18nKey="activeFireInMapCount">En rojo, <strong>{{ count: this.props.activefiresunion.length + this.props.firealerts.length }}</strong> fuegos activos. <strong>{{ countTotal: this.props.activefirestotal }}</strong> fuegos activos en el mundo.</Trans> <FireStats loadingAll={loading} {... this.props} /></Fragment>
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
{isNotHomeAndMobile() && this.props.firealerts.length > 0 &&
|
{isNotHomeAndMobile() && this.props.firealerts.length > 0 &&
|
||||||
|
|
@ -261,7 +253,7 @@ class FiresMap extends React.Component {
|
||||||
</p>
|
</p>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
{this.props.loading || !this.props.subsready ?
|
{loading ?
|
||||||
<LoadingBar progress={this.props.loading ? 0.9 : 1} />
|
<LoadingBar progress={this.props.loading ? 0.9 : 1} />
|
||||||
: ''}
|
: ''}
|
||||||
{/* https://github.com/CliffCloud/Leaflet.Sleep */}
|
{/* https://github.com/CliffCloud/Leaflet.Sleep */}
|
||||||
|
|
@ -305,6 +297,14 @@ class FiresMap extends React.Component {
|
||||||
neighbour={false}
|
neighbour={false}
|
||||||
industries={false}
|
industries={false}
|
||||||
/>
|
/>
|
||||||
|
{ Meteor.isDevelopment &&
|
||||||
|
<FireListUnion
|
||||||
|
t={t}
|
||||||
|
history={this.props.history}
|
||||||
|
firesUnion={this.props.activefiresunion}
|
||||||
|
nasa
|
||||||
|
/>
|
||||||
|
}
|
||||||
<FireList
|
<FireList
|
||||||
t={t}
|
t={t}
|
||||||
history={this.props.history}
|
history={this.props.history}
|
||||||
|
|
@ -367,12 +367,14 @@ FiresMap.propTypes = {
|
||||||
userSubs: PropTypes.string,
|
userSubs: PropTypes.string,
|
||||||
userSubsBounds: PropTypes.string,
|
userSubsBounds: PropTypes.string,
|
||||||
activefires: PropTypes.arrayOf(PropTypes.object).isRequired,
|
activefires: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
activefiresunion: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
lastFireDetected: PropTypes.object,
|
lastFireDetected: PropTypes.object,
|
||||||
firealerts: PropTypes.arrayOf(PropTypes.object).isRequired,
|
firealerts: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
falsePositives: PropTypes.arrayOf(PropTypes.object).isRequired,
|
falsePositives: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
industries: PropTypes.arrayOf(PropTypes.object).isRequired,
|
industries: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
lastCheck: PropTypes.instanceOf(Date),
|
lastCheck: PropTypes.instanceOf(Date),
|
||||||
activefirestotal: PropTypes.number.isRequired,
|
activefirestotal: PropTypes.number.isRequired,
|
||||||
|
activefiresuniontotal: PropTypes.number.isRequired,
|
||||||
center: PropTypes.arrayOf(PropTypes.number),
|
center: PropTypes.arrayOf(PropTypes.number),
|
||||||
zoom: PropTypes.number,
|
zoom: PropTypes.number,
|
||||||
marks: PropTypes.bool.isRequired,
|
marks: PropTypes.bool.isRequired,
|
||||||
|
|
@ -389,6 +391,7 @@ export default translate([], { wait: true })(withTracker(() => {
|
||||||
// console.log(`With industries: ${withIndustries}`);
|
// console.log(`With industries: ${withIndustries}`);
|
||||||
|
|
||||||
let subscription;
|
let subscription;
|
||||||
|
let subscriptionUnion;
|
||||||
let alertSubscription;
|
let alertSubscription;
|
||||||
|
|
||||||
const centerStored = store.get('firesmap_center');
|
const centerStored = store.get('firesmap_center');
|
||||||
|
|
@ -403,7 +406,8 @@ export default translate([], { wait: true })(withTracker(() => {
|
||||||
if (typeof showUnionStored === 'boolean') {
|
if (typeof showUnionStored === 'boolean') {
|
||||||
showUnion.set(showUnionStored);
|
showUnion.set(showUnionStored);
|
||||||
}
|
}
|
||||||
Meteor.subscribe('lastFireDetected');
|
// Disable, because this increase the number of fires by one
|
||||||
|
// Meteor.subscribe('lastFireDetected');
|
||||||
Meteor.autorun(() => {
|
Meteor.autorun(() => {
|
||||||
if ((centerStored !== [0, 0] || geolocation.get()) && geoInit) {
|
if ((centerStored !== [0, 0] || geolocation.get()) && geoInit) {
|
||||||
center.set(centerStored || geolocation.get());
|
center.set(centerStored || geolocation.get());
|
||||||
|
|
@ -419,6 +423,14 @@ export default translate([], { wait: true })(withTracker(() => {
|
||||||
mapSize.get()[1].lat,
|
mapSize.get()[1].lat,
|
||||||
marks.get() && zoom.get() >= MAXZOOM
|
marks.get() && zoom.get() >= MAXZOOM
|
||||||
);
|
);
|
||||||
|
subscriptionUnion = Meteor.subscribe(
|
||||||
|
'activefiresunionmyloc',
|
||||||
|
mapSize.get()[0].lng,
|
||||||
|
mapSize.get()[0].lat,
|
||||||
|
mapSize.get()[1].lng,
|
||||||
|
mapSize.get()[1].lat,
|
||||||
|
false
|
||||||
|
);
|
||||||
alertSubscription = Meteor.subscribe(
|
alertSubscription = Meteor.subscribe(
|
||||||
'fireAlerts',
|
'fireAlerts',
|
||||||
mapSize.get()[0].lng,
|
mapSize.get()[0].lng,
|
||||||
|
|
@ -439,6 +451,7 @@ export default translate([], { wait: true })(withTracker(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
Meteor.subscribe('activefirestotal');
|
Meteor.subscribe('activefirestotal');
|
||||||
|
Meteor.subscribe('activefiresuniontotal');
|
||||||
|
|
||||||
const settingsSubs = Meteor.subscribe('settings');
|
const settingsSubs = Meteor.subscribe('settings');
|
||||||
const lastCheck = SiteSettings.findOne({ name: 'last-fire-check' });
|
const lastCheck = SiteSettings.findOne({ name: 'last-fire-check' });
|
||||||
|
|
@ -447,20 +460,23 @@ export default translate([], { wait: true })(withTracker(() => {
|
||||||
const fireAlerts = FireAlertsCollection.find().fetch();
|
const fireAlerts = FireAlertsCollection.find().fetch();
|
||||||
const falsePositives = FalsePositivesCollection.find().fetch().map(falsePositivesRemap);
|
const falsePositives = FalsePositivesCollection.find().fetch().map(falsePositivesRemap);
|
||||||
const industries = IndustriesCollection.find().fetch().map(industriesRemap);
|
const industries = IndustriesCollection.find().fetch().map(industriesRemap);
|
||||||
|
const lastFireDetected = ActiveFiresCollection.findOne({}, { sort: { when: -1 } });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
loading: !subscription ? true : !(subscription.ready() && settingsSubs.ready() && alertSubscription.ready()),
|
loading: Meteor.status().status !== 'connected' || !subscription ? true : !(subscription.ready() && settingsSubs.ready() && alertSubscription.ready() && settingsSubs.ready() && subscriptionUnion.ready()),
|
||||||
userSubs: userSubs ? userSubs.value : null,
|
userSubs: userSubs ? userSubs.value : null,
|
||||||
userSubsBounds: userSubs ? userSubsBounds.value : null,
|
userSubsBounds: userSubs ? userSubsBounds.value : null,
|
||||||
subsready: settingsSubs.ready(),
|
subsready: settingsSubs.ready(),
|
||||||
// Not reactive query depending on zoom level
|
// Not reactive query depending on zoom level
|
||||||
activefires: ActiveFiresCollection.find({}, { reactive: zoom.get() >= MAXZOOMREACTIVE }).fetch(),
|
activefires: ActiveFiresCollection.find({}, { reactive: zoom.get() >= MAXZOOMREACTIVE }).fetch(),
|
||||||
|
activefiresunion: ActiveFiresUnion.find({}, { reactive: zoom.get() >= MAXZOOMREACTIVE }).fetch(),
|
||||||
activefirestotal: Counter.get('countActiveFires') + fireAlerts.length,
|
activefirestotal: Counter.get('countActiveFires') + fireAlerts.length,
|
||||||
|
activefiresuniontotal: Counter.get('countActiveFiresUnion') + fireAlerts.length,
|
||||||
firealerts: fireAlerts,
|
firealerts: fireAlerts,
|
||||||
falsePositives,
|
falsePositives,
|
||||||
industries,
|
industries,
|
||||||
lastCheck: lastCheck ? lastCheck.value : null,
|
lastCheck: lastCheck ? lastCheck.value : null,
|
||||||
lastFireDetected: ActiveFiresCollection.findOne({}, { sort: { when: -1 } }),
|
lastFireDetected,
|
||||||
center: center.get(),
|
center: center.get(),
|
||||||
marks: marks.get(),
|
marks: marks.get(),
|
||||||
showUnion: showUnion.get(),
|
showUnion: showUnion.get(),
|
||||||
|
|
|
||||||
|
|
@ -187,28 +187,72 @@
|
||||||
transform: translate3d(0, 0, 0);
|
transform: translate3d(0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.carousel-item-1 {
|
.no-webp .carousel-item-1 {
|
||||||
background-image: url('/home-slide1.jpg')
|
background-image: url('/images/home-slide1.jpg')
|
||||||
}
|
}
|
||||||
|
|
||||||
.carousel-item-2 {
|
.no-webp .carousel-item-2.lazy {
|
||||||
background-image: url('/home-slide2.jpg')
|
background-image: url('/images/home-slide2.jpg')
|
||||||
}
|
}
|
||||||
|
|
||||||
.carousel-item-3 {
|
.no-webp .carousel-item-3.lazy {
|
||||||
background-image: url('/home-slide3.jpg')
|
background-image: url('/images/home-slide3.jpg')
|
||||||
}
|
}
|
||||||
|
|
||||||
.carousel-item-4 {
|
.no-webp .carousel-item-4.lazy {
|
||||||
background-image: url('/home-slide4.jpg')
|
background-image: url('/images/home-slide4.jpg')
|
||||||
}
|
}
|
||||||
|
|
||||||
.carousel-snd-item-1 {
|
.no-webp .carousel-snd-item-1 {
|
||||||
background-image: url('/home-slide-snd-1.jpg')
|
background-image: url('/images/home-slide-snd-1.jpg')
|
||||||
}
|
}
|
||||||
|
|
||||||
.carousel-snd-item-2 {
|
.no-webp .carousel-snd-item-2.lazy {
|
||||||
background-image: url('/home-slide-snd-2.jpg')
|
background-image: url('/images/home-slide-snd-2.jpg')
|
||||||
|
}
|
||||||
|
|
||||||
|
.webp .carousel-item-1 {
|
||||||
|
background-image: url('/images/home-slide1.webp')
|
||||||
|
}
|
||||||
|
|
||||||
|
.webp .carousel-item-2.lazy {
|
||||||
|
background-image: url('/images/home-slide2.webp')
|
||||||
|
}
|
||||||
|
|
||||||
|
.webp .carousel-item-3.lazy {
|
||||||
|
background-image: url('/images/home-slide3.webp')
|
||||||
|
}
|
||||||
|
|
||||||
|
.webp .carousel-item-4.lazy {
|
||||||
|
background-image: url('/images/home-slide4.webp')
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .webp .carousel-item-1 {
|
||||||
|
background-image: url('/images/home-slide1-blur.webp')
|
||||||
|
} */
|
||||||
|
|
||||||
|
.webp .carousel-item-2 {
|
||||||
|
background-image: url('/images/home-slide2-blur.webp')
|
||||||
|
}
|
||||||
|
|
||||||
|
.webp .carousel-item-3 {
|
||||||
|
background-image: url('/images/home-slide3-blur.webp')
|
||||||
|
}
|
||||||
|
|
||||||
|
.webp .carousel-item-4 {
|
||||||
|
background-image: url('/images/home-slide4-blur.webp')
|
||||||
|
}
|
||||||
|
|
||||||
|
.webp .carousel-snd-item-1 {
|
||||||
|
background-image: url('/images/home-slide-snd-1.webp')
|
||||||
|
}
|
||||||
|
|
||||||
|
.webp .carousel-snd-item-2 {
|
||||||
|
background-image: url('/images/home-slide-snd-2-blur.webp')
|
||||||
|
}
|
||||||
|
|
||||||
|
.webp .carousel-snd-item-2.lazy {
|
||||||
|
background-image: url('/images/home-slide-snd-2.webp')
|
||||||
}
|
}
|
||||||
|
|
||||||
.participe-btn {
|
.participe-btn {
|
||||||
|
|
@ -225,28 +269,52 @@
|
||||||
-moz-background-size: cover;
|
-moz-background-size: cover;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
-o-background-size: cover;
|
-o-background-size: cover;
|
||||||
background-image: url('/home-fires.jpg');
|
}
|
||||||
|
|
||||||
|
.no-webp .bg-image-full {
|
||||||
|
background-image: url('/images/home-fires.jpg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.webp .bg-image-full {
|
||||||
|
background-image: url('/images/home-fires.webp');
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 767px) {
|
@media only screen and (max-width: 767px) {
|
||||||
.bg-image-full {
|
.no-webp .bg-image-full {
|
||||||
background-image: url('/home-fires-800.jpg');
|
background-image: url('/images/home-fires-800.jpg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.webp .bg-image-full {
|
||||||
|
background-image: url('/images/home-fires-800.webp');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 400px) {
|
@media only screen and (max-width: 400px) {
|
||||||
.bg-image-full {
|
.no-webp .bg-image-full {
|
||||||
background-image: url('/home-fires-400.jpg');
|
background-image: url('/images/home-fires-400.jpg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.webp .bg-image-full {
|
||||||
|
background-image: url('/images/home-fires-400.webp');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 1000px) {
|
@media only screen and (max-width: 1000px) {
|
||||||
.bg-image-full {
|
.no-webp .bg-image-full {
|
||||||
background-image: url('/home-fires-1000.jpg'); }
|
background-image: url('/images/home-fires-1000.jpg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.webp .bg-image-full {
|
||||||
|
background-image: url('/images/home-fires-1000.webp');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.device[data-device=iPhone6][data-orientation=portrait][data-color=white] {
|
.nowebp .device[data-device=iPhone6][data-orientation=portrait][data-color=white] {
|
||||||
background-image: url(/mobile.png);
|
background-image: url(/images/mobile.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
.webp .device[data-device=iPhone6][data-orientation=portrait][data-color=white] {
|
||||||
|
background-image: url(/images/mobile.webp);
|
||||||
}
|
}
|
||||||
|
|
||||||
.full-width .page-header { // section titles
|
.full-width .page-header { // section titles
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
/* eslint-disable react/jsx-indent-props */
|
/* eslint-disable react/jsx-indent-props */
|
||||||
/* eslint-disable key-spacing */
|
/* eslint-disable key-spacing */
|
||||||
/* eslint-env jquery */
|
/* eslint-env jquery */
|
||||||
|
import 'simple-line-icons/css/simple-line-icons.css';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { translate, Trans } from 'react-i18next';
|
import { translate, Trans } from 'react-i18next';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
|
|
@ -10,6 +11,7 @@ import PropTypes from 'prop-types';
|
||||||
// import { Link } from 'react-router-dom';
|
// import { Link } from 'react-router-dom';
|
||||||
// https://www.npmjs.com/package/react-resize-detector
|
// https://www.npmjs.com/package/react-resize-detector
|
||||||
import ReactResizeDetector from 'react-resize-detector';
|
import ReactResizeDetector from 'react-resize-detector';
|
||||||
|
import MobileStoreButton from 'react-mobile-store-button';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import 'html5-device-mockups/dist/device-mockups.min.css';
|
import 'html5-device-mockups/dist/device-mockups.min.css';
|
||||||
import 'bootstrap-carousel-swipe/carousel-swipe';
|
import 'bootstrap-carousel-swipe/carousel-swipe';
|
||||||
|
|
@ -17,9 +19,8 @@ import { isAnyMobile } from '/imports/ui/components/Utils/isMobile';
|
||||||
import SubscriptionEditor from '/imports/ui/components/SubscriptionEditor/SubscriptionEditor';
|
import SubscriptionEditor from '/imports/ui/components/SubscriptionEditor/SubscriptionEditor';
|
||||||
import SubscriptionsMap from '/imports/ui/pages/Subscriptions/SubscriptionsMap';
|
import SubscriptionsMap from '/imports/ui/pages/Subscriptions/SubscriptionsMap';
|
||||||
import ShareIt from '/imports/ui/components/ShareIt/ShareIt';
|
import ShareIt from '/imports/ui/components/ShareIt/ShareIt';
|
||||||
|
import getFallbackLang from '/imports/modules/lang-fallback';
|
||||||
import FiresMap from '../FiresMap/FiresMap';
|
import FiresMap from '../FiresMap/FiresMap';
|
||||||
|
|
||||||
import 'simple-line-icons/css/simple-line-icons.css';
|
|
||||||
import './Index.scss';
|
import './Index.scss';
|
||||||
import './Index-custom.scss';
|
import './Index-custom.scss';
|
||||||
|
|
||||||
|
|
@ -34,11 +35,22 @@ class Index extends Component {
|
||||||
self.scaleHeader();
|
self.scaleHeader();
|
||||||
}, 250);
|
}, 250);
|
||||||
this.onResize = this.onResize.bind(this);
|
this.onResize = this.onResize.bind(this);
|
||||||
|
this.getBotUrl = this.getBotUrl.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
$('#carouselMainIndicators').carousel();
|
const car1 = $('#carouselMainIndicators');
|
||||||
$('#carouselSndIndicators').carousel();
|
const car2 = $('#carouselSndIndicators');
|
||||||
|
car1.carousel();
|
||||||
|
car2.carousel();
|
||||||
|
/* _.defer(() => {
|
||||||
|
$('#firsthomeslide').addClass('lazy');
|
||||||
|
}); */
|
||||||
|
const loadLazy = (ev) => {
|
||||||
|
ev.relatedTarget.classList.add('lazy');
|
||||||
|
};
|
||||||
|
car1.on('slide.bs.carousel', loadLazy);
|
||||||
|
car2.on('slide.bs.carousel', loadLazy);
|
||||||
}
|
}
|
||||||
|
|
||||||
onResize() {
|
onResize() {
|
||||||
|
|
@ -57,6 +69,14 @@ class Index extends Component {
|
||||||
return (<div />);
|
return (<div />);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getBotUrl() {
|
||||||
|
const fallbackLng = getFallbackLang(this.props.i18n.language);
|
||||||
|
console.log(`language: ${this.props.i18n.language}, fallback: ${fallbackLng}`);
|
||||||
|
return fallbackLng === 'es' ?
|
||||||
|
'https://t.me/TodosContraElFuego_bot' :
|
||||||
|
'https://t.me/AllAgainstTheFire_bot';
|
||||||
|
}
|
||||||
|
|
||||||
scaleHeader() {
|
scaleHeader() {
|
||||||
const scalable = document.getElementById('tcefh1');
|
const scalable = document.getElementById('tcefh1');
|
||||||
const margin = 10;
|
const margin = 10;
|
||||||
|
|
@ -69,7 +89,7 @@ class Index extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleBtnClick() {
|
handleBtnClick() {
|
||||||
window.open('https://t.me/TodosContraElFuego_bot', '_blank');
|
window.open(this.getBotUrl(), '_blank');
|
||||||
}
|
}
|
||||||
|
|
||||||
gotoParticipe() {
|
gotoParticipe() {
|
||||||
|
|
@ -82,6 +102,7 @@ class Index extends Component {
|
||||||
render() {
|
render() {
|
||||||
const { t } = this.props;
|
const { t } = this.props;
|
||||||
const title = `${t('AppName')}: ${t('Inicio')}`;
|
const title = `${t('AppName')}: ${t('Inicio')}`;
|
||||||
|
const androidUrl = 'https://play.google.com/store/apps/details?id=org.comunes.fires';
|
||||||
return (
|
return (
|
||||||
<div className="IndexDisabled full-width">
|
<div className="IndexDisabled full-width">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
|
|
@ -106,7 +127,7 @@ class Index extends Component {
|
||||||
</ol>
|
</ol>
|
||||||
<div className="carousel-inner" role="listbox">
|
<div className="carousel-inner" role="listbox">
|
||||||
|
|
||||||
<div className="carousel-item carousel-item-1 active">
|
<div id="firsthomeslide" className="carousel-item carousel-item-1 active">
|
||||||
<div
|
<div
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
role="button"
|
role="button"
|
||||||
|
|
@ -161,6 +182,9 @@ class Index extends Component {
|
||||||
</div>
|
</div>
|
||||||
<p className="moto"><Trans>Siempre alerta a los fuegos en nuestro vecindario</Trans></p>
|
<p className="moto"><Trans>Siempre alerta a los fuegos en nuestro vecindario</Trans></p>
|
||||||
{/* <Link className="participe-btn btn btn-lg btn-warning" role="button" to="/#platforms">{this.props.t('Participa')}</Link> */}
|
{/* <Link className="participe-btn btn btn-lg btn-warning" role="button" to="/#platforms">{this.props.t('Participa')}</Link> */}
|
||||||
|
<div className="android-btn">
|
||||||
|
<MobileStoreButton store="android" url={androidUrl} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
|
@ -250,7 +274,7 @@ class Index extends Component {
|
||||||
<div className="device" data-device="iPhone6" data-orientation="portrait" data-color="white">
|
<div className="device" data-device="iPhone6" data-orientation="portrait" data-color="white">
|
||||||
<div className="screen">
|
<div className="screen">
|
||||||
{/* Demo image for screen mockup, you can put an image here, some HTML, an animation, video, or anything else! */}
|
{/* Demo image for screen mockup, you can put an image here, some HTML, an animation, video, or anything else! */}
|
||||||
<img src="/telegram-screen.png" className="img-fluid" alt="" />
|
<img src="/images/telegram-screen.png" className="img-fluid" alt="" />
|
||||||
</div>
|
</div>
|
||||||
<div className="button" tabIndex="-1" onKeyDown={() => {}} role="button" onClick={() => this.handleBtnClick()} />
|
<div className="button" tabIndex="-1" onKeyDown={() => {}} role="button" onClick={() => this.handleBtnClick()} />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -260,7 +284,7 @@ class Index extends Component {
|
||||||
<div className="container-fluid">
|
<div className="container-fluid">
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-lg-6">
|
<div className="col-lg-6">
|
||||||
<a className="feature-link" rel="noopener noreferrer" target="_blank" href="https://t.me/TodosContraElFuego_bot">
|
<a className="feature-link" rel="noopener noreferrer" target="_blank" href={this.getBotUrl()}>
|
||||||
<div className="feature-item">
|
<div className="feature-item">
|
||||||
<i className="fa fa-telegram text-primary" />
|
<i className="fa fa-telegram text-primary" />
|
||||||
<h3>Telegram</h3>
|
<h3>Telegram</h3>
|
||||||
|
|
@ -308,6 +332,7 @@ class Index extends Component {
|
||||||
|
|
||||||
Index.propTypes = {
|
Index.propTypes = {
|
||||||
history: PropTypes.object.isRequired,
|
history: PropTypes.object.isRequired,
|
||||||
|
i18n: PropTypes.object.isRequired,
|
||||||
t: PropTypes.func.isRequired
|
t: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,3 +77,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.android-btn > div {
|
||||||
|
height: 50px !important;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,15 +9,17 @@ import { Meteor } from 'meteor/meteor';
|
||||||
import { Accounts } from 'meteor/accounts-base';
|
import { Accounts } from 'meteor/accounts-base';
|
||||||
import { Bert } from 'meteor/themeteorchef:bert';
|
import { Bert } from 'meteor/themeteorchef:bert';
|
||||||
import { testId } from '/imports/ui/components/Utils/TestUtils';
|
import { testId } from '/imports/ui/components/Utils/TestUtils';
|
||||||
import Col from '../../components/Col/Col';
|
|
||||||
import { withTracker } from 'meteor/react-meteor-data';
|
|
||||||
import InputHint from '../../components/InputHint/InputHint';
|
|
||||||
import validate from '../../../modules/validate';
|
|
||||||
import { translate } from 'react-i18next';
|
import { translate } from 'react-i18next';
|
||||||
import { T9n } from 'meteor-accounts-t9n';
|
import { T9n } from 'meteor-accounts-t9n';
|
||||||
|
import { withTracker } from 'meteor/react-meteor-data';
|
||||||
|
import Col from '../../components/Col/Col';
|
||||||
|
import InputHint from '../../components/InputHint/InputHint';
|
||||||
|
import validate from '../../../modules/validate';
|
||||||
|
|
||||||
import './Profile.scss';
|
import './Profile.scss';
|
||||||
|
|
||||||
|
// List of languages: https://github.com/i18next/i18next/issues/1068
|
||||||
|
|
||||||
class Profile extends React.Component {
|
class Profile extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
@ -155,6 +157,7 @@ class Profile extends React.Component {
|
||||||
|
|
||||||
renderPasswordUser(loading, user) {
|
renderPasswordUser(loading, user) {
|
||||||
const { t, i18n } = this.props;
|
const { t, i18n } = this.props;
|
||||||
|
const enabledLangs = ['en', 'es', 'gl'];
|
||||||
const langName = {
|
const langName = {
|
||||||
en: 'English', es: 'Español', gl: 'Galego', ast: 'Asturianu', ca: 'Català'
|
en: 'English', es: 'Español', gl: 'Galego', ast: 'Asturianu', ca: 'Català'
|
||||||
};
|
};
|
||||||
|
|
@ -202,7 +205,7 @@ class Profile extends React.Component {
|
||||||
{langName[i18n.language]}
|
{langName[i18n.language]}
|
||||||
</button>
|
</button>
|
||||||
<div className="dropdown-menu">
|
<div className="dropdown-menu">
|
||||||
{i18n.languages.map(lang => (
|
{enabledLangs.map(lang => (
|
||||||
<button
|
<button
|
||||||
className="dropdown-item"
|
className="dropdown-item"
|
||||||
onClick={() => this.onLangSelect(lang)}
|
onClick={() => this.onLangSelect(lang)}
|
||||||
|
|
|
||||||
|
|
@ -63,8 +63,8 @@ class SubscriptionsMap extends React.Component {
|
||||||
|
|
||||||
handleLeafletLoad(map) {
|
handleLeafletLoad(map) {
|
||||||
// console.log('Map loading');
|
// console.log('Map loading');
|
||||||
// console.log(map);
|
if (map && this.props.userSubs !== 'null') {
|
||||||
if (map) {
|
// console.log(`Union of ${this.props.userSubs}`);
|
||||||
this.state.union = subsUnion(this.state.union, {
|
this.state.union = subsUnion(this.state.union, {
|
||||||
map,
|
map,
|
||||||
subs: this.props.userSubs,
|
subs: this.props.userSubs,
|
||||||
|
|
|
||||||
2687
package-lock.json
generated
18
package.json
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "tcef",
|
"name": "tcef",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "QT_QPA_PLATFORM='' MONGO_URL=mongodb://localhost:27017/fuegos meteor --settings settings-development.json",
|
"start": "QT_QPA_PLATFORM='' MONGO_URL=mongodb://fuegos:fuegos@localhost:27017/fuegos meteor --settings settings-development.json",
|
||||||
"test": "jest"
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -10,8 +10,6 @@
|
||||||
"@cleverbeagle/dates": "^0.5.1",
|
"@cleverbeagle/dates": "^0.5.1",
|
||||||
"@cleverbeagle/seeder": "^1.3.1",
|
"@cleverbeagle/seeder": "^1.3.1",
|
||||||
"@turf/buffer": "^5.1.5",
|
"@turf/buffer": "^5.1.5",
|
||||||
"@turf/truncate": "^5.1.5",
|
|
||||||
"@turf/union": "^5.1.5",
|
|
||||||
"babel-runtime": "^6.26.0",
|
"babel-runtime": "^6.26.0",
|
||||||
"bcrypt": "^1.0.3",
|
"bcrypt": "^1.0.3",
|
||||||
"bootstrap-carousel-swipe": "0.0.6",
|
"bootstrap-carousel-swipe": "0.0.6",
|
||||||
|
|
@ -37,20 +35,23 @@
|
||||||
"ismobilejs": "^0.4.1",
|
"ismobilejs": "^0.4.1",
|
||||||
"jquery": "^2.2.4",
|
"jquery": "^2.2.4",
|
||||||
"jquery-validation": "^1.17.0",
|
"jquery-validation": "^1.17.0",
|
||||||
|
"jsend": "^1.0.2",
|
||||||
"juice": "^4.2.2",
|
"juice": "^4.2.2",
|
||||||
"leaflet": "^1.3.1",
|
"leaflet": "^1.3.1",
|
||||||
"leaflet-geodesy": "^0.2.1",
|
|
||||||
"leaflet-graphicscale": "0.0.2",
|
"leaflet-graphicscale": "0.0.2",
|
||||||
"leaflet-headless": "^0.2.6",
|
|
||||||
"leaflet-sleep": "^0.5.1",
|
"leaflet-sleep": "^0.5.1",
|
||||||
"lodash": "^4.17.4",
|
"lodash": "^4.17.4",
|
||||||
"loms.perlin": "^1.0.1",
|
"loms.perlin": "^1.0.1",
|
||||||
|
"map-common-utils": "^0.5.0",
|
||||||
"maxmind": "^2.3.0",
|
"maxmind": "^2.3.0",
|
||||||
"meteor-accounts-t9n": "^2.0.3",
|
"meteor-accounts-t9n": "^2.0.3",
|
||||||
"meteor-node-stubs": "^0.3.2",
|
"meteor-node-stubs": "^0.3.2",
|
||||||
|
"modernizr": "^3.6.0",
|
||||||
"moment": "^2.19.1",
|
"moment": "^2.19.1",
|
||||||
"moment-timezone": "^0.5.14",
|
"moment-timezone": "^0.5.14",
|
||||||
|
"node-gcm": "^1.0.2",
|
||||||
"node-geocoder": "^3.21.1",
|
"node-geocoder": "^3.21.1",
|
||||||
|
"node-gyp": "^3.7.0",
|
||||||
"nodemailer": "^4.4.2",
|
"nodemailer": "^4.4.2",
|
||||||
"npm": "^5.8.0",
|
"npm": "^5.8.0",
|
||||||
"pm2-master": "^1.1.3",
|
"pm2-master": "^1.1.3",
|
||||||
|
|
@ -72,15 +73,15 @@
|
||||||
"react-leaflet-fullscreen": "0.0.6",
|
"react-leaflet-fullscreen": "0.0.6",
|
||||||
"react-leaflet-google": "^3.2.1",
|
"react-leaflet-google": "^3.2.1",
|
||||||
"react-leaflet-sidebarv2": "^0.5.1",
|
"react-leaflet-sidebarv2": "^0.5.1",
|
||||||
|
"react-mobile-store-button": "0.0.3",
|
||||||
"react-places-autocomplete": "^5.4.3",
|
"react-places-autocomplete": "^5.4.3",
|
||||||
"react-progressbar.js": "git://github.com/squarecat/react-progressbar.js.git",
|
"react-progress-bar.js": "^0.2.3",
|
||||||
"react-resize-detector": "^1.1.0",
|
"react-resize-detector": "^1.1.0",
|
||||||
"react-router-bootstrap": "^0.24.4",
|
"react-router-bootstrap": "^0.24.4",
|
||||||
"react-router-dom": "^4.2.2",
|
"react-router-dom": "^4.2.2",
|
||||||
"react-router-hash-link": "^1.1.1",
|
"react-router-hash-link": "^1.1.1",
|
||||||
"react-share": "^2.0.0",
|
"react-share": "^2.0.0",
|
||||||
"reactstrap": "^5.0.0-alpha.3",
|
"reactstrap": "^5.0.0-alpha.3",
|
||||||
"segfault-handler": "^1.0.0",
|
|
||||||
"simpl-schema": "^1.5.0",
|
"simpl-schema": "^1.5.0",
|
||||||
"simple-line-icons": "^2.4.1",
|
"simple-line-icons": "^2.4.1",
|
||||||
"store": "^2.0.12",
|
"store": "^2.0.12",
|
||||||
|
|
@ -157,7 +158,8 @@
|
||||||
"_ensureIndex",
|
"_ensureIndex",
|
||||||
"_verifyEmailToken",
|
"_verifyEmailToken",
|
||||||
"_resetPasswordToken",
|
"_resetPasswordToken",
|
||||||
"_name"
|
"_name",
|
||||||
|
"_str"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 3346b6db70c153b1dd66e6a16a0ad28f10ebf740
|
Subproject commit 4761da921f467838a2c01b34e84a7703ee49b12c
|
||||||
|
|
@ -1,97 +1,156 @@
|
||||||
Your privacy is important to us.
|
# PRIVACY POLICY #
|
||||||
|
|
||||||
Comunes Association built the 'All Against the Fire' app and this SERVICE is provided by Comunes Association at no cost and is intended for use as is.
|
*Last updated July 12, 2018*
|
||||||
|
|
||||||
This page is used to inform website visitors regarding our policies with the collection, use, and disclosure of Personal Information if anyone decided to use our Service.
|
**In Short**: *Your privacy is important to us. We do not do business with your data, we do not share it with third parties and we only worry about the fires and how to strengthen neighborhoods against them.*
|
||||||
|
|
||||||
If you choose to use our Service, then you agree to the collection and use of information in relation to this policy. The Personal Information that we collect is used for providing and improving the Service. We will not use or share your information with anyone except as described in this Privacy Policy.
|
Thank you for choosing to be part of our community 'All Against the Fires' at Comunes Association (“we”, “us”, or “our”). We are committed to protecting your personal information and your right to privacy. If you have any questions or concerns about our policy, or our practices with regards to your personal information, please contact us at info@comunes.org.
|
||||||
|
|
||||||
The terms used in this Privacy Policy have the same meanings as in our [Terms and Conditions](/terms), which is accessible at 'All Against the Fire' unless otherwise defined in this Privacy Policy.
|
When you visit our website [fires.comunes.org](https://fires.comunes.org), mobile application, and use our services, you trust us with your personal information. We take your privacy very seriously. In this privacy notice, we describe our privacy policy. We seek to explain to you in the clearest way possible what information we collect, how we use it and what rights you have in relation to it. We hope you take some time to read through it carefully, as it is important. If there are any terms in this privacy policy that you do not agree with, please discontinue use of our Sites or Apps and our services.
|
||||||
|
|
||||||
**Information Collection and Use**
|
This privacy policy applies to all information collected through our website (such as [fires.comunes.org](https://fires.comunes.org)), mobile application (the "**Apps**"), and/or any related services, or events (we refer to them collectively in this privacy policy as the "**Sites**").
|
||||||
|
|
||||||
For a better experience, while using our Service, we may require you to provide us with certain personally identifiable information, including but not limited to name, email, language, and the telegram username (of our Telegram users). The information that we request is will be retained by us and used as described in this privacy policy.
|
**Please read this privacy policy carefully as it will help you make informed decisions about sharing your personal information with us.**
|
||||||
|
|
||||||
In addition, the app may collect certain information automatically, including, but not limited to, the type of device you use, the IP address of your device, your mobile operating system, the type of Internet browsers you use, and information about the way you use the app.
|
|
||||||
|
|
||||||
We may use the information we collect in the following ways:
|
## 1. WHAT INFORMATION DO WE COLLECT? ##
|
||||||
* To personalize your experience and to allow us to deliver the type of content in which you are most interested.
|
|
||||||
* To send periodic emails regarding our services.
|
|
||||||
|
|
||||||
The app does use third party services that may collect information used to identify you.
|
### Information automatically collected ###
|
||||||
|
|
||||||
Links to privacy policy of third party service providers used by the app:
|
**In Short:** *Some information – such as IP address and/or browser and device characteristics – is collected
|
||||||
|
automatically when you visit our Sites or Apps.*
|
||||||
|
|
||||||
* [OpenStreetMap](https://wiki.openstreetmap.org/wiki/Privacy_Policy/)
|
We automatically collect certain information when you visit, use or navigate the Sites or Apps. This information does not reveal your specific identity (like your name or contact information) but may include device and usage information, such as your IP address, browser and device characteristics, operating system, language preferences, referring URLs, device name, country, location, information about how and when you use our Sites or Apps and other technical information. This information is primarily needed to maintain the security and operation of our Sites or Apps, and for our internal analytics and reporting purposes. Like many businesses, we also collect information through cookies and similar technologies.
|
||||||
* [Google Maps](https://developers.google.com/maps/terms?hl=es#3-privacy-and-personal-information)
|
|
||||||
|
|
||||||
We have not enabled Google AdSense.
|
### Information collected through our Apps ###
|
||||||
|
|
||||||
**Does the app collect precise real time location information of the device?**
|
**In Short:** *We may collect information regarding your geo-location, push notifications, when you use our apps.*
|
||||||
|
|
||||||
When you visit the app, we may use GPS technology (or other similar technology) to determine your current location in order to determine the city you are located within and/or display a location map of local fires and/or provide alerts of future fires in your zone. We **will not** share your current location with other users or partners.
|
If you use our Apps, we may also collect the following information:
|
||||||
|
|
||||||
If you do not want us to use your location for the purposes set forth above, you should turn off the location services for the mobile app located in your account settings or in your mobile phone settings and/or within the app and/or when asked by your browser.
|
- *Geo-Location Information.* We may request at some point access or permission to location-based information from your mobile device to provide location-based services. If you wish to change our access or permissions, you may do so in your device's settings.
|
||||||
|
- *Push Notifications.* We may request to send you push notifications regarding your account or the mobile application. If you wish to opt-out from receiving these types of communications, you may turn them off in your device's settings.
|
||||||
|
|
||||||
**How do we protect your information?**
|
|
||||||
|
|
||||||
We do not use vulnerability scanning and/or scanning to PCI standards. We only provide articles and information. We never ask for credit card numbers. We do not use Malware Scanning.
|
## 2. HOW DO WE USE YOUR INFORMATION? ##
|
||||||
|
|
||||||
Your personal information is contained behind secured networks and is only accessible by a limited number of persons who have special access rights to such systems, and are required to keep the information confidential. In addition, all sensitive information you supply is encrypted via Secure Socket Layer (SSL) technology.
|
**In Short:** *We process your information for purposes based on legitimate interests, the fulfillment of our contract with you, compliance with our legal obligations, and/or your consent.*
|
||||||
|
|
||||||
We implement a variety of security measures when a user enters, submits, or accesses their information to maintain the safety of your personal information.
|
We use personal information collected via our Sites or Apps for a variety of purposes described below. We process your personal information for these purposes in order to enter into or perform a contract with you ("Contractual"), with your consent ("Consent"), and/or for compliance with our legal obligations ("Legal Reasons"). We indicate the specific processing grounds we rely on next to each purpose listed below.
|
||||||
|
|
||||||
**Log Data**
|
We use the information we collect or receive:
|
||||||
|
|
||||||
We want to inform you that whenever you use our Service, in a case of an error in the app we collect data and information (through third party products) on your phone called Log Data. This Log Data may include information such as your device Internet Protocol (“IP”) address, device name, operating system version, the configuration of the app when utilizing our Service, the time and date of your use of the Service, and other statistics.
|
- **To facilitate account creation and logon process** with your Consent. If you choose to link your account with us to a third party account \*(such as your Google or Telegram account), we use the information you allowed us to collect from those third parties to facilitate account creation and logon process.
|
||||||
|
- **Request Feedback**. We may use your information to request feedback and to contact you about your use of our Sites or Apps.
|
||||||
|
- **To enable user-to-user communications** with your Consent. We may use your information in order to enable user-to-user communications with each user's consent.
|
||||||
|
- **To enforce our terms, conditions and policies**.
|
||||||
|
- **To respond to legal requests and prevent harm** for Legal Reasons. If we receive a subpoena or other legal request, we may need to inspect the data we hold to determine how to respond.
|
||||||
|
|
||||||
**Do we use 'cookies'?**
|
|
||||||
|
|
||||||
Yes. Cookies are small files that a site or its service provider transfers to your computer's hard drive through your Web browser (if you allow) that enables the site's or service provider's systems to recognize your browser and capture and remember certain information. For instance, we use cookies to help us remember and process your prefered language. They are also used to help us understand your preferences based on previous or current site activity, which enables us to provide you with improved services. We also use cookies to help us compile aggregate data about site traffic and site interaction so that we can offer better site experiences and tools in the future.
|
## 3. WILL YOUR INFORMATION BE SHARED WITH ANYONE? ##
|
||||||
|
|
||||||
We use cookies to understand and save user's preferences for future visits.
|
**In Short:** *We only share information with your consent, to comply with laws or to protect your rights.*
|
||||||
|
|
||||||
You can choose to have your computer warn you each time a cookie is being sent, or you can choose to turn off all cookies. You do this through your browser settings. Since browser is a little different, look at your browser's Help Menu to learn the correct way to modify your cookies.
|
We only share and disclose your information in the following situations:
|
||||||
|
|
||||||
If you turn cookies off, some of the features that make your site experience more efficient may not function properly.
|
- **Compliance with Laws**. We may disclose your information where we are legally required to do so in order to comply with applicable law, governmental requests, a judicial proceeding, court order, or legal process, such as in response to a court order or a subpoena (including in response to public authorities to meet national security or law enforcement requirements).
|
||||||
|
- **Vital Interests and Legal Rights**. We may disclose your information where we believe it is necessary to investigate, prevent, or take action regarding potential violations of our policies, suspected fraud, situations involving potential threats to the safety of any person and illegal activities, or as evidence in litigation in which we are involved.
|
||||||
|
- **With your Consent**. We may disclose your personal information for any other purpose with your consent.
|
||||||
|
|
||||||
**Service Providers**
|
|
||||||
|
|
||||||
We may employ third-party companies and individuals to facilitate our Service.
|
## 4. DO WE USE COOKIES AND OTHER TRACKING TECHNOLOGIES? ##
|
||||||
|
|
||||||
**Third-party disclosure**
|
**In Short:** *We may use cookies and other tracking technologies to collect and store your information.*
|
||||||
|
|
||||||
We do not sell, trade, or otherwise transfer to outside parties your Personally Identifiable Information.
|
We may use cookies to access or store information about account login and your user preferences (like your prefered language).
|
||||||
|
|
||||||
**Security**
|
|
||||||
|
|
||||||
We value your trust in providing us your Personal Information, thus we are striving to use commercially acceptable means of protecting it. But remember that no method of transmission over the internet, or method of electronic storage is 100% secure and reliable, and we cannot guarantee its absolute security.
|
## 5. DO WE USE GOOGLE MAPS? ##
|
||||||
|
|
||||||
**Links to Other Sites**
|
**In Short:** *Yes, we use Google Maps for the purpose of providing better service.*
|
||||||
|
|
||||||
We do not include or offer third-party products or services on our website but this Service may contain links to other sites in user comments. If you click on a third-party link, you will be directed to that site. Note that these external sites are not operated by us. Therefore, we strongly advise you to review the Privacy Policy of these websites. We have no control over and assume no responsibility for the content, privacy policies, or practices of any third-party sites or services.
|
This website, mobile application, or Telegram application uses Google Maps APIs. You may find the Google Maps APIs Terms of Service [here](https://developers.google.com/maps/terms). To better understand Google's Privacy Policy, please refer to this [link](https://policies.google.com/privacy).
|
||||||
|
|
||||||
**Children’s Privacy**
|
By using our Maps API Implementation, you agree to be bound by Google's Terms of Service. You agree to allow us to obtain or cache your location. You may revoke your consent at anytime. We use information about location in conjunction with data from other data providers.
|
||||||
|
|
||||||
These Services do not address anyone under the age of 13. We do not knowingly collect personally identifiable information from children under 13. In the case we discover that a child under 13 has provided us with personal information, we immediately delete this from our servers. If you are a parent or guardian and you are aware that your child has provided us with personal information, please contact us so that we will be able to do necessary actions.
|
|
||||||
|
|
||||||
**Fair Information Practices**
|
## 6. HOW LONG DO WE KEEP YOUR INFORMATION? ##
|
||||||
|
|
||||||
The Fair Information Practices Principles form the backbone of privacy law in the United States and the concepts they include have played a significant role in the development of data protection laws around the globe. Understanding the Fair Information Practice Principles and how they should be implemented is critical to comply with the various privacy laws that protect personal information.
|
**In Short:** *We keep your information for as long as necessary to fulfill the purposes outlined in this privacy policy unless otherwise required by law.*
|
||||||
|
|
||||||
In order to be in line with Fair Information Practices we will take the following responsive action, should a data breach occur:
|
We will only keep your personal information for as long as it is necessary for the purposes set out in this privacy policy, unless a longer retention period is required or permitted by law (such as tax, accounting or other legal requirements). No purpose in this policy will require us keeping your personal information for longer than the period of time in which users have an account with us.
|
||||||
* We will notify you via email, within 7 business days
|
|
||||||
|
|
||||||
We also agree to the Individual Redress Principle which requires that individuals have the right to legally pursue enforceable rights against data collectors and processors who fail to adhere to the law. This principle requires not only that individuals have enforceable rights against data users, but also that individuals have recourse to courts or government agencies to investigate and/or prosecute non-compliance by data processors.
|
When we have no ongoing legitimate need to process your personal information, we will either delete or anonymize it, or, if this is not possible (for example, because your personal information has been stored in backup archives), then we will securely store your personal information and isolate it from any further processing until deletion is possible.
|
||||||
|
|
||||||
**Changes to This Privacy Policy**
|
|
||||||
|
|
||||||
We may update our Privacy Policy from time to time. Thus, you are advised to review this page periodically for any changes. We will notify you of any changes by posting the new Privacy Policy on this page. These changes are effective immediately after they are posted on this page.
|
## 7.HOW DO WE KEEP YOUR INFORMATION SAFE? ##
|
||||||
|
|
||||||
**Your Consent**
|
**In Short:** *We aim to protect your personal information through a system of organizational and technical security measures.*
|
||||||
|
|
||||||
By using the app, you are consenting to our processing of your information as set forth in this Privacy Policy now and as amended by us. "Processing," means using cookies on a computer/hand held device or using or touching information in any way, including, but not limited to, collecting, storing, deleting, using, combining and disclosing information, all of which activities will take place in Spain. If you reside outside Spain your information will be transferred, processed and stored there under Spain privacy standards.
|
We have implemented appropriate technical and organizational security measures designed to protect the security of any personal information we process. However, please also remember that we cannot guarantee that the internet itself is 100% secure. Although we will do our best to protect your personal information, transmission of personal information to and from our Sites or Apps is at your own risk. You should only access the services within a secure environment.
|
||||||
|
|
||||||
**Contact Us**
|
|
||||||
|
|
||||||
If you have any questions or suggestions about our Privacy Policy, do not hesitate to contact us: info AT comunes DOT org.
|
## 8. DO WE COLLECT INFORMATION FROM MINORS? ##
|
||||||
|
|
||||||
|
**In Short:** *We do not knowingly collect data from or market to children under 18 years of age.*
|
||||||
|
|
||||||
|
We do not knowingly solicit data from or market to children under 18 years of age. By using the Sites or Apps, you represent that you are at least 18 or that you are the parent or guardian of such a minor and consent to such minor dependent's use of the Sites or Apps. If we learn that personal information from users less than 18 years of age has been collected, we will deactivate the account and take reasonable measures to promptly delete such data from our records. If you become aware of any data we have collected from children under age 18, please contact us at info@comunes.org.
|
||||||
|
|
||||||
|
|
||||||
|
## 9. WHAT ARE YOUR PRIVACY RIGHTS? ##
|
||||||
|
|
||||||
|
**In Short:** *In some regions, such as the European Economic Area, you have rights that allow you greater access to and control over your personal information. You may review, change, or terminate your account at any time.*
|
||||||
|
|
||||||
|
In some regions (like the European Economic Area), you have certain rights under applicable data protection laws. These may include the right (i) to request access and obtain a copy of your personal information, (ii) to request rectification or erasure; (iii) to restrict the processing of your personal information; and (iv) if applicable, to data portability. In certain circumstances, you may also have the right to object to the processing of your personal information. To make such a request, please use the contact details provided below. We will consider and act upon any request in accordance with applicable data protection laws.
|
||||||
|
|
||||||
|
If we are relying on your consent to process your personal information, you have the right to withdraw your consent at any time. Please note however that this will not affect the lawfulness of the processing before its withdrawal.
|
||||||
|
|
||||||
|
If you are resident in the European Economic Area and you believe we are unlawfully processing your personal information, you also have the right to complain to your local data protection supervisory authority. You can find their contact details here: [http://ec.europa.eu/justice/data-protection/bodies/authorities/index_en.htm](http://ec.europa.eu/justice/data-protection/bodies/authorities/index_en.htm)
|
||||||
|
|
||||||
|
### Account Information ###
|
||||||
|
|
||||||
|
If you would at any time like to review or change the information in your account or terminate your account, you can:
|
||||||
|
|
||||||
|
- Log into your account settings and update your user account.
|
||||||
|
|
||||||
|
Upon your request to terminate your account, we will deactivate or delete your account and information from our active databases. However, some information may be retained in our files to prevent fraud, troubleshoot problems, assist with any investigations, enforce our Terms of Use and/or comply with legal requirements.
|
||||||
|
|
||||||
|
**Cookies and similar technologies:** Most Web browsers are set to accept cookies by default. If you prefer, you can usually choose to set your browser to remove cookies and to reject cookies. If you choose to remove cookies or reject cookies, this could affect certain features or services of our Sites or Apps.
|
||||||
|
|
||||||
|
**Opting out of email notifications:** You can unsubscribe from our notification emails at any time by clicking on the unsubscribe link in the emails that we send or by contacting us using the details provided below. You will then be removed from these emails – however, we will still need to send you service-related emails that are necessary for the administration and use of your account. To otherwise opt-out, you may:
|
||||||
|
|
||||||
|
- Note your preferences when you register an account with the site.
|
||||||
|
- Access your account settings and update preferences.
|
||||||
|
|
||||||
|
|
||||||
|
## 10. DO CALIFORNIA RESIDENTS HAVE SPECIFIC PRIVACY RIGHTS? ##
|
||||||
|
|
||||||
|
**In Short:** *Yes, if you are a resident of California, you are granted specific rights regarding access to your personal information.*
|
||||||
|
|
||||||
|
California Civil Code Section 1798.83, also known as the "Shine The Light" law, permits our users who are California residents to request and obtain from us, once a year and free of charge, information about categories of personal information (if any) we disclosed to third parties for direct marketing purposes and the names and addresses of all third parties with which we shared personal information in the immediately preceding calendar year. If you are a California resident and would like to make such a request, please submit your request in writing to us using the contact information provided below.
|
||||||
|
|
||||||
|
If you are under 18 years of age, reside in California, and have a registered account with the Sites or Apps, you have the right to request removal of unwanted data that you publicly post on the Sites or Apps. To request removal of such data, please contact us using the contact information provided below, and include the email address associated with your account and a statement that you reside in California. We will make sure the data is not publicly displayed on the Sites or Apps, but please be aware that the data may not be completely or comprehensively removed from our systems.
|
||||||
|
|
||||||
|
|
||||||
|
## 11. DO WE MAKE UPDATES TO THIS POLICY? ##
|
||||||
|
|
||||||
|
**In Short:** *Yes, we will update this policy as necessary to stay compliant with relevant laws.*
|
||||||
|
|
||||||
|
We may update this privacy policy from time to time. The updated version will be indicated by an updated "Revised" date and the updated version will be effective as soon as it is accessible. If we make material changes to this privacy policy, we may notify you either by prominently posting a notice of such changes or by directly sending you a notification. We encourage you to review this privacy policy frequently to be informed of how we are protecting your information.
|
||||||
|
|
||||||
|
|
||||||
|
## 12. HOW CAN YOU CONTACT US ABOUT THIS POLICY? ##
|
||||||
|
|
||||||
|
If you have questions or comments about this policy, you may email us at info@comunes.org or by post to:
|
||||||
|
|
||||||
|
ASOCIACIÓN COMUNES
|
||||||
|
C/ COLÓN 21, 5º DCHA, ESC DCHA
|
||||||
|
03001 ALICANTE
|
||||||
|
SPAIN
|
||||||
|
|
||||||
|
If you have any further questions or comments about us or our policies, email us at info@comunes.org or by post to:
|
||||||
|
|
||||||
|
ASOCIACIÓN COMUNES
|
||||||
|
C/ COLÓN 21, 5º DCHA, ESC DCHA
|
||||||
|
03001 ALICANTE
|
||||||
|
SPAIN
|
||||||
|
|
|
||||||
|
|
@ -1,97 +1,155 @@
|
||||||
Su privacidad es importante para nosotros.
|
# POLÍTICA DE PRIVACIDAD #
|
||||||
|
|
||||||
La Asociación Comunes ha creado esta aplicación 'Tod@s contra el fuego' y este SERVICIO que se provee por la Asociación de Comunes sin costo y está destinado a ser usado tal cual.
|
*Última actualización 12 de julio de 2018*
|
||||||
|
|
||||||
Esta página se utiliza para informar a los visitantes del sitio web sobre nuestras políticas con la recopilación, el uso y la divulgación de información personal si alguien decide utilizar nuestro Servicio.
|
**En resumen**: *Su privacidad es importante para nosotros/as. No hacemos negocios con sus datos, no los compartimos con terceros y solo nos preocupan los incendios y cómo fortalecer vecindarios contra ellos.*
|
||||||
|
|
||||||
Si elige usar nuestro Servicio, acepta la recopilación y el uso de la información en relación con esta política. La información personal que recopilamos se usa para proporcionar y mejorar el servicio. No usaremos ni compartiremos su información con nadie, excepto según se describe en esta Política de privacidad.
|
Gracias por elegir ser parte de nuestra comunidad 'Tod@s contra los Fuegos' en la Asociación de Comunes ("nosotras/os" o "nos"). Nos comprometemos a proteger su información personal y su derecho a la privacidad. Si tiene alguna pregunta o inquietud sobre nuestra política o nuestras prácticas con respecto a su información personal, contáctenos a info@comunes.org.
|
||||||
|
|
||||||
Los términos utilizados en esta Política de privacidad tienen los mismos significados que en nuestros [Términos y condiciones](/terms), a los que se puede acceder en "Tod@s contra el fuego" a menos que se defina lo contrario en esta Política de privacidad.
|
Cuando visita nuestro sitio web [fuegos.comunes.org](https://fuegos.comunes.org), la aplicación móvil y utiliza nuestros servicios, confía en nosotras/os con su información personal. Nos tomamos su privacidad muy en serio. En este aviso de privacidad, describimos nuestra política de privacidad. Buscamos explicarte de la manera más clara posible qué información recopilamos, cómo la utilizamos y qué derechos tienes en relación con ella. Esperamos que se tome un tiempo para leer detenidamente, ya que es importante. Si hay términos en esta política de privacidad con los que no está de acuerdo, interrumpa el uso de nuestros Sitios o Aplicaciones y nuestros servicios.
|
||||||
|
|
||||||
**Recopilación y uso de información**
|
Esta política de privacidad se aplica a toda la información recopilada a través de nuestro sitio web (como [fuegos.comunes.org](https://fuegos.comunes.org)), la aplicación móvil (las "**Aplicaciones**") y/o cualquier servicio relacionado o eventos (nos referimos a ellos colectivamente en esta política de privacidad como los "**Sitios**").
|
||||||
|
|
||||||
Para una mejor experiencia, al utilizar nuestro Servicio, podemos solicitarle que nos brinde cierta información de identificación personal, que incluye, entre otros, nombre, correo electrónico, idioma y el nombre de usuario del telegrama (de nuestros usuarios de Telegram). La información que solicitamos será retenida por nosotros y utilizada como se describe en esta política de privacidad.
|
**Lea atentamente esta política de privacidad, ya que le ayudará a tomar decisiones informadas sobre cómo compartir su información personal con nosotras/os.**
|
||||||
|
|
||||||
Además, la aplicación puede recopilar cierta información automáticamente, que incluye, entre otros, el tipo de dispositivo que usa, la dirección IP de su dispositivo, su sistema operativo móvil, el tipo de navegador de Internet que utiliza e información sobre la forma que usas la aplicación.
|
|
||||||
|
|
||||||
Podemos usar la información que recopilamos de las siguientes maneras:
|
## 1. ¿QUÉ INFORMACIÓN RECOPILAMOS? ##
|
||||||
* Para personalizar su experiencia y permitirnos ofrecer el tipo de contenido que más le interesa.
|
|
||||||
* Para enviar correos electrónicos periódicos con respecto a nuestros servicios.
|
|
||||||
|
|
||||||
La aplicación utiliza servicios de terceros que pueden recopilar información utilizada para identificarlo.
|
### Información recopilada automáticamente ###
|
||||||
|
|
||||||
Enlaces a la política de privacidad de proveedores de servicios de terceros utilizados por la aplicación:
|
**En resumen:** *Recopilamos información automáticamente, como la dirección IP y/o las características del navegador y del dispositivo, cuando visita nuestros Sitios o Aplicaciones.*
|
||||||
|
|
||||||
* [OpenStreetMap](https://wiki.openstreetmap.org/wiki/Privacy_Policy/)
|
Recopilamos cierta información automáticamente cuando visita, utiliza o navega por nuestros Sitios o Aplicaciones. Esta información no revela su identidad específica (como su nombre o información de contacto) pero puede incluir información de uso y dispositivo, como su dirección IP, características del navegador y del dispositivo, sistema operativo, preferencias de idioma, URL de referencia, nombre del dispositivo, país, ubicación , información sobre cómo y cuándo utiliza nuestros sitios o aplicaciones y otra información técnica. Esta información es principalmente necesaria para mantener la seguridad y el funcionamiento de nuestros sitios o aplicaciones, y para nuestros fines de análisis internos e informes. Al igual que muchas empresas, también recopilamos información a través de cookies y tecnologías similares.
|
||||||
* [Google Maps](https://developers.google.com/maps/terms?hl=es#3-privacy-and-personal-information)
|
|
||||||
|
|
||||||
No hemos habilitado Google AdSense.
|
### Información recopilada a través de nuestras aplicaciones ###
|
||||||
|
|
||||||
**¿La aplicación recopila información precisa sobre la ubicación en tiempo real del dispositivo?**
|
**En resumen:** *Podemos recopilar información sobre su ubicación geográfica, notificaciones push, cuando usa nuestras aplicaciones.*
|
||||||
|
|
||||||
Cuando visita la aplicación, podemos usar tecnología GPS (u otra tecnología similar) para determinar su ubicación actual a fin de determinar la ciudad en la que se encuentra y/o mostrar un mapa de ubicación de incendios locales y/o proporcionar alertas de incendios futuros en tu zona. **No** compartiremos su ubicación actual con otros usuarios o socios.
|
Si usa nuestras aplicaciones, también podemos recopilar la siguiente información:
|
||||||
|
|
||||||
Si no desea que usemos su ubicación para los fines establecidos anteriormente, debe desactivar los servicios de ubicación para la aplicación móvil ubicados en la configuración de su cuenta o en la configuración de su teléfono móvil y/o dentro de la aplicación y/o cuando se le solicite por tu navegador.
|
- *Información de geolocalización.* Podemos solicitar en algún momento acceso o permiso a la información basada en la ubicación desde su dispositivo móvil para proporcionar servicios basados en la ubicación. Si desea cambiar nuestro acceso o permisos, puede hacerlo en la configuración de su dispositivo.
|
||||||
|
- *Notificaciones Push.* Podemos solicitar el envío de notificaciones push con respecto a su cuenta o la aplicación móvil. Si desea optar por no recibir este tipo de comunicaciones, puede desactivarlas en la configuración de su dispositivo.
|
||||||
|
|
||||||
**¿Cómo protegemos tu información?**
|
|
||||||
|
|
||||||
No utilizamos el escaneo y/o escaneo de vulnerabilidades a estándares PCI. Solo proporcionamos artículos e información. Nunca pedimos números de tarjetas de crédito. No utilizamos el Malware Scanning.
|
## 2. ¿CÓMO USAMOS SU INFORMACIÓN? ##
|
||||||
|
|
||||||
Su información personal está almacenada detrás de redes seguras y solo es accesible para un número limitado de personas que tienen derechos especiales de acceso a dichos sistemas, y se les exige mantener la confidencialidad de la información. Además, toda la información sensible que usted suministra se cifra a través de la tecnología Secure Socket Layer (SSL).
|
**En resumen:** *Procesamos su información para fines basados en intereses legítimos, el cumplimiento de nuestro contrato con usted, el cumplimiento de nuestras obligaciones legales y/o su consentimiento.*
|
||||||
|
|
||||||
Implementamos una variedad de medidas de seguridad cuando un usuario ingresa, envía o accede a su información para mantener la seguridad de su información personal.
|
Usamos información personal recopilada a través de nuestros Sitios o Aplicaciones para una variedad de propósitos que se describen a continuación. Procesamos su información personal para estos fines a fin de celebrar o realizar un contrato con usted ("Contractual"), con su consentimiento ("Consentimiento"), y/o para cumplir con nuestras obligaciones legales ("Razones legales"). Indicamos los motivos de procesamiento específicos en los que confiamos junto a cada uno de los propósitos enumerados a continuación.
|
||||||
|
|
||||||
**Dato de registro**
|
Usamos la información que recopilamos o recibimos:
|
||||||
|
|
||||||
Queremos informarle que cada vez que utiliza nuestro Servicio, en caso de error en la aplicación, recopilamos datos e información (a través de productos de terceros) en su teléfono, llamados Datos de registro. Este registro de datos puede incluir información como la dirección del protocolo de Internet ("IP"), el nombre del dispositivo, la versión del sistema operativo, la configuración de la aplicación al utilizar nuestro servicio, la hora y fecha de uso del servicio y otras estadísticas.
|
- **Para facilitar la creación de la cuenta y el proceso de inicio de sesión** con su consentimiento. Si elige vincular su cuenta con nosotras/os a una cuenta de terceros \*(como su cuenta de Google o Telegram), usamos la información que nos permitió recopilar de esos terceros para facilitar la creación de la cuenta y el proceso de inicio de sesión.
|
||||||
|
- **Solicitud de comentarios**. Podemos utilizar su información para solicitar comentarios y para comunicarnos con usted sobre su uso de nuestros Sitios o Aplicaciones.
|
||||||
|
- **Para habilitar las comunicaciones de usuario a usuario** con su consentimiento. Podemos usar su información para permitir las comunicaciones de usuario a usuario con el consentimiento de cada usuario.
|
||||||
|
- **Para hacer cumplir nuestros términos, condiciones y políticas**.
|
||||||
|
- **Para responder a solicitudes legales y evitar daños** por razones legales. Si recibimos una citación u otra solicitud legal, es posible que tengamos que inspeccionar los datos que tenemos para determinar cómo responder.
|
||||||
|
|
||||||
**¿Usamos 'cookies'?**
|
|
||||||
|
|
||||||
Sí. Las cookies son pequeños archivos que un sitio o su proveedor de servicios transfiere al disco duro de su computadora a través de su navegador web (si lo permite) que permite que los sistemas del sitio o del proveedor de servicios reconozcan su navegador y capturen y recuerden cierta información. Por ejemplo, usamos cookies para ayudarnos a recordar y procesar su idioma preferido. También se utilizan para ayudarnos a comprender sus preferencias en función de la actividad del sitio anterior o actual, lo que nos permite brindarle mejores servicios. También utilizamos cookies para ayudarnos a recopilar datos agregados sobre el tráfico del sitio y la interacción del sitio para que podamos ofrecer mejores experiencias y herramientas del sitio en el futuro.
|
## 3. ¿SU INFORMACIÓN SERÁ COMPARTIDA CON ALGUIEN? ##
|
||||||
|
|
||||||
Usamos cookies para comprender y guardar las preferencias del usuario para futuras visitas.
|
**En resumen:** *Solo compartimos información con su consentimiento, para cumplir con las leyes o proteger sus derechos*
|
||||||
|
|
||||||
Puede optar por que su computadora lo advierta cada vez que se envía una cookie, o puede optar por desactivar todas las cookies. Lo haces a través de la configuración de tu navegador. Dado que el navegador es un poco diferente, consulte el Menú de Ayuda de su navegador para conocer la forma correcta de modificar sus cookies.
|
Solo compartimos y divulgamos su información en las siguientes situaciones:
|
||||||
|
|
||||||
Si desactiva las cookies, es posible que algunas de las funciones que hacen que su sitio sea más eficiente no funcionen correctamente.
|
- **De acuerdo con las leyes**. Podemos divulgar su información cuando se nos requiera legalmente para cumplir con la ley aplicable, las solicitudes gubernamentales, un procedimiento judicial, una orden judicial o un proceso legal, como por ejemplo en respuesta a una orden judicial o una citación judicial (incluso en respuesta a las autoridades públicas para cumplir con los requisitos de seguridad nacional o cumplimiento de la ley).
|
||||||
|
- **Intereses vitales y derechos legales**. Podemos divulgar su información cuando creemos que es necesario investigar, prevenir o tomar medidas con respecto a posibles violaciones de nuestras políticas, sospecha de fraude, situaciones que involucren amenazas potenciales a la seguridad de cualquier persona y actividades ilegales, o como evidencia en un litigio en el cual estamos involucrados.
|
||||||
|
- **Con su consentimiento**. Podemos divulgar su información personal para cualquier otro propósito con su consentimiento.
|
||||||
|
|
||||||
**Proveedores de servicio**
|
|
||||||
|
|
||||||
Podemos emplear compañías e individuos de terceros para facilitar nuestro Servicio.
|
## 4. ¿USAMOS COOKIES Y OTRAS TECNOLOGÍAS DE SEGUIMIENTO? ##
|
||||||
|
|
||||||
**Divulgación de terceros**
|
**En resumen:** *Podemos utilizar cookies y otras tecnologías de seguimiento para recopilar y almacenar su información.*
|
||||||
|
|
||||||
No vendemos, comercializamos ni transferimos a terceros su información de identificación personal.
|
Podemos usar cookies para acceder o almacenar información sobre el inicio de sesión de la cuenta y sus preferencias de usuario (como su idioma preferido).
|
||||||
|
|
||||||
**Seguridad**
|
|
||||||
|
|
||||||
Valoramos su confianza al proporcionarnos su información personal, por lo tanto, nos esforzamos por utilizar medios comercialmente aceptables para protegerla. Pero recuerde que ningún método de transmisión a través de Internet o método de almacenamiento electrónico es 100% seguro y confiable, y no podemos garantizar su seguridad absoluta.
|
## 5. ¿USAMOS GOOGLE MAPS? ##
|
||||||
|
|
||||||
**Enlaces a otros sitios**
|
**En resumen:** *Sí, utilizamos Google Maps con el fin de proporcionar un mejor servicio.*
|
||||||
|
|
||||||
No incluimos ni ofrecemos productos o servicios de terceros en nuestro sitio web, pero este Servicio puede contener enlaces a otros sitios en los comentarios de los usuarios. Si hace clic en un enlace de un tercero, se lo dirigirá a ese sitio. Tenga en cuenta que estos sitios externos no son operados por nosotros. Por lo tanto, le recomendamos encarecidamente que revise la Política de privacidad de estos sitios web. No tenemos control ni asumimos ninguna responsabilidad por el contenido, las políticas de privacidad o las prácticas de sitios o servicios de terceros.
|
Este sitio web, aplicación móvil o aplicación Telegram utiliza las API de Google Maps. Puede encontrar las Condiciones del servicio de las API de Google Maps [aquí](https://developers.google.com/maps/terms). Para comprender mejor la Política de privacidad de Google, consulte este [enlace](https://policies.google.com/privacy).
|
||||||
|
|
||||||
**Privacidad de los niños**
|
Al utilizar nuestra implementación de Maps API, usted acepta estar sujeto a los Términos de Servicio de Google. Usted acepta permitirnos obtener o almacenar en caché su ubicación. Puede revocar su consentimiento en cualquier momento. Usamos información sobre la ubicación junto con datos de otros proveedores de datos.
|
||||||
|
|
||||||
Estos servicios no se dirigen a personas menores de 13 años. No recopilamos deliberadamente información personal identificable de niños menores de 13 años. En el caso de que descubramos que un niño menor de 13 años nos ha proporcionado información personal, la borramos inmediatamente de nuestros servidores. Si usted es un padre o tutor y sabe que su hijo nos ha proporcionado información personal, comuníquese con nosotros para que podamos hacer las acciones necesarias.
|
|
||||||
|
|
||||||
**Prácticas justas de información**
|
## 6. ¿CUÁNTO TIEMPO CONSERVAMOS SU INFORMACIÓN? ##
|
||||||
|
|
||||||
Los principios de prácticas justas de información (del inglés 'Fair Information Practices') forman la columna vertebral de la ley de privacidad en los Estados Unidos y los conceptos que incluyen han jugado un papel importante en el desarrollo de las leyes de protección de datos en todo el mundo. Comprender los principios prácticos de información justa y cómo deben implementarse es fundamental para cumplir con las diversas leyes de privacidad que protegen la información personal.
|
**En resumen:** *Conservamos su información el tiempo que sea necesario para cumplir con los fines descritos en esta política de privacidad, a menos que la ley así lo requiera.*
|
||||||
|
|
||||||
Para estar en línea con las prácticas de información justas, tomaremos la siguiente medida de respuesta, en caso de que se produzca una violación de datos:
|
Solo conservaremos su información personal durante el tiempo que sea necesario para los fines establecidos en esta política de privacidad, a menos que la ley exija o permita un período de retención más largo (como impuestos, contabilidad u otros requisitos legales). Ningún propósito de esta política nos obligará a conservar su información personal por un período mayor al que el usuario tenga una cuenta con nosotras/os.
|
||||||
* Le notificaremos por correo electrónico dentro de los 7 días hábiles
|
|
||||||
|
|
||||||
También aceptamos el principio de reparación individual que exige que las personas tengan derecho a buscar legalmente derechos exigibles contra los recolectores de datos y procesadores que no cumplan con la ley. Este principio requiere no solo que los individuos tengan derechos exigibles contra los usuarios de los datos, sino también que los individuos recurran a los tribunales o agencias gubernamentales para investigar y/o enjuiciar el incumplimiento por parte de los procesadores de datos.
|
Cuando no tenemos una necesidad legítima continua de procesar su información personal, la borraremos o la anonimizamos, o, si esto no es posible (por ejemplo, porque su información personal ha sido almacenada en archivos de respaldo), almacenaremos de manera segura su información personal y aislarlo de cualquier procesamiento posterior hasta que la eliminación sea posible.
|
||||||
|
|
||||||
**Cambios a esta política de privacidad**
|
|
||||||
|
|
||||||
Es posible que actualicemos nuestra política de privacidad de vez en cuando. Por lo tanto, se recomienda revisar esta página periódicamente para cualquier cambio. Le notificaremos de cualquier cambio publicando la nueva política de privacidad en esta página. Estos cambios entran en vigencia inmediatamente después de que se publiquen en esta página.
|
## 7. ¿CÓMO MANTENEMOS SU INFORMACIÓN SEGURA? ##
|
||||||
|
|
||||||
**Tu consentimiento**
|
**En resumen:** *Nuestro objetivo es proteger su información personal a través de un sistema de medidas de seguridad técnicas y organizativas.*
|
||||||
|
|
||||||
Al utilizar la aplicación, usted está dando su consentimiento para que procesemos su información según lo establecido en esta política de privacidad ahora y en la forma en que nosotros lo modifiquemos. "Procesamiento" significa usar cookies en una computadora/dispositivo de mano o usar o tocar información de cualquier manera, incluyendo, pero no limitado a, recolectar, almacenar, eliminar, usar, combinar y divulgar información, todas las cuales se llevarán a cabo en España. Si reside fuera de España, su información será transferida, procesada y almacenada allí según los estándares de privacidad de España.
|
Hemos implementado medidas de seguridad organizacionales y técnicas adecuadas diseñadas para proteger la seguridad de cualquier información personal que procesamos. Sin embargo, recuerde que no podemos garantizar que Internet sea 100% seguro. Aunque haremos nuestro mejor esfuerzo para proteger su información personal, la transmisión de información personal hacia y desde nuestros Sitios o Aplicaciones es bajo su propio riesgo. Solo debe acceder a los servicios dentro de un entorno seguro.
|
||||||
|
|
||||||
**Contáctenos**
|
|
||||||
|
|
||||||
Si tiene alguna pregunta o sugerencia sobre nuestra Política de privacidad, no dude en ponerse en contacto con nosotros: info ARROBA comunes PUNTO org.
|
## 8. ¿RECOPILAMOS INFORMACIÓN DE MENORES DE EDAD? ##
|
||||||
|
|
||||||
|
**En resumen:** *No recopilamos deliberadamente ni comercializamos datos de niños menores de 18 años.*
|
||||||
|
|
||||||
|
No solicitamos intencionalmente ni comercializamos datos de niños menores de 18 años. Al utilizar los Sitios o Aplicaciones, usted declara que tiene al menos 18 años o que es el padre o tutor de dicho menor y acepta el uso de los Sitios o Aplicaciones por parte de dicho menor dependiente. Si descubrimos que se ha recopilado información personal de usuarios menores de 18 años, desactivaremos la cuenta y tomaremos medidas razonables para eliminar dichos datos de inmediato de nuestros registros. Si toma conocimiento de cualquier información que hayamos recopilado de niños menores de 18 años, contáctenos a info@comunes.org.
|
||||||
|
|
||||||
|
|
||||||
|
## 9. ¿CUÁLES SON SUS DERECHOS DE PRIVACIDAD? ##
|
||||||
|
|
||||||
|
**En resumen:** *En algunas regiones, como el Unión Europea, tiene derechos que le permiten un mayor acceso y control sobre su información personal. Puede revisar, cambiar o cancelar su cuenta en cualquier momento.*
|
||||||
|
|
||||||
|
En algunas regiones (como la Unión Europea), usted tiene ciertos derechos bajo las leyes de protección de datos aplicables. Estos pueden incluir el derecho (i) de solicitar acceso y obtener una copia de su información personal, (ii) solicitar una rectificación o cancelación; (iii) restringir el procesamiento de su información personal; y (iv) si corresponde, a la portabilidad de datos. En determinadas circunstancias, también puede tener derecho a oponerse al procesamiento de su información personal. Para realizar dicha solicitud, utilice los datos de contacto que se proporcionan a continuación. Consideraremos y actuaremos en cualquier solicitud de acuerdo con las leyes de protección de datos aplicables.
|
||||||
|
|
||||||
|
Si confiamos en su consentimiento para procesar su información personal, tiene derecho a retirar su consentimiento en cualquier momento. Sin embargo, tenga en cuenta que esto no afectará la legalidad del procesamiento antes de su retirada.
|
||||||
|
|
||||||
|
Si usted reside en la Unión Europea y considera que estamos procesando ilegalmente su información personal, también tiene derecho a presentar una queja ante la autoridad de supervisión de protección de datos local. Puede encontrar sus datos de contacto aquí: [http://ec.europa.eu/justice/data-protection/bodies/authorities/index_en.htm](http://ec.europa.eu/justice/data-protection/bodies/authorities/index_en.htm)
|
||||||
|
|
||||||
|
### Información de la cuenta ###
|
||||||
|
|
||||||
|
Si en algún momento desea revisar o cambiar la información en su cuenta o cancelar su cuenta, puede:
|
||||||
|
|
||||||
|
- Inicie sesión en la configuración de su cuenta y actualice su cuenta de usuario.
|
||||||
|
|
||||||
|
Tras su solicitud para cancelar su cuenta, desactivaremos o eliminaremos su cuenta y la información de nuestras bases de datos activas. Sin embargo, parte de la información se puede conservar en nuestros archivos para evitar fraudes, solucionar problemas, ayudar con cualquier investigación, hacer cumplir nuestros Términos de uso y/o cumplir con los requisitos legales.
|
||||||
|
|
||||||
|
**Cookies y tecnologías similares:** La mayoría de los navegadores web están configurados para aceptar cookies por defecto. Si lo prefiere, generalmente puede elegir configurar su navegador para que elimine las cookies y rechace las cookies. Si elige eliminar las cookies o rechazar las cookies, esto podría afectar ciertas funciones o servicios de nuestros sitios o aplicaciones.
|
||||||
|
|
||||||
|
**Optar por no recibir notificaciones por correo electrónico:** Puede darse de baja de nuestros correos electrónicos de notificación en cualquier momento haciendo clic en el enlace para darse de baja en los correos electrónicos que le enviamos o poniéndose en contacto con nosotras/os utilizando los detalles que se proporcionan a continuación. A continuación, se le eliminará de estos correos electrónicos; sin embargo, aún así le enviaremos correos electrónicos relacionados con el servicio que son necesarios para la administración y el uso de su cuenta. Para optar por no participar, usted puede:
|
||||||
|
|
||||||
|
- Tener en cuenta sus preferencias cuando registra una cuenta en el sitio.
|
||||||
|
- Acceder a la configuración de tu cuenta y actualizar tus preferencias.
|
||||||
|
|
||||||
|
|
||||||
|
## 10. ¿LOS RESIDENTES DE CALIFORNIA TIENEN DERECHOS DE PRIVACIDAD ESPECÍFICOS? ##
|
||||||
|
|
||||||
|
**En resumen:** *Sí, si usted es residente de California, se le otorgan derechos específicos con respecto al acceso a su información personal.*
|
||||||
|
|
||||||
|
La Sección 1798.83 del Código Civil de California, también conocida como la ley "Shine The Light", permite a nuestros usuarios que son residentes de California solicitar y obtener de nosotras/os, una vez al año y sin cargo, información sobre categorías de información personal (si corresponde) que divulgado a terceros para fines de marketing directo y los nombres y direcciones de todos los terceros con quienes compartimos información personal en el año calendario inmediatamente anterior. Si usted es residente de California y desea realizar dicha solicitud, envíe su solicitud por escrito a nosotras/os utilizando la información de contacto que se proporciona a continuación.
|
||||||
|
|
||||||
|
Si eres menor de 18 años, resides en California y tienes una cuenta registrada en los Sitios o Aplicaciones, tienes el derecho de solicitar la eliminación de datos no deseados que publiques en los Sitios o Aplicaciones. Para solicitar la eliminación de dichos datos, contáctenos utilizando la información de contacto que se proporciona a continuación, e incluya la dirección de correo electrónico asociada a su cuenta y una declaración de que reside en California. Nos aseguraremos de que los datos no se muestren públicamente en los Sitios o Aplicaciones, pero tenga en cuenta que es posible que los datos no se eliminen completa o completamente de nuestros sistemas.
|
||||||
|
|
||||||
|
|
||||||
|
## 11. ¿HACEMOS ACTUALIZACIONES DE ESTA POLÍTICA? ##
|
||||||
|
|
||||||
|
**En resumen:** *Sí, actualizaremos esta política según sea necesario para cumplir con las leyes pertinentes.*
|
||||||
|
|
||||||
|
Podemos actualizar esta política de privacidad de vez en cuando. La versión actualizada se indicará con una fecha actualizada "Revisada" y la versión actualizada entrará en vigencia tan pronto como sea accesible. Si realizamos cambios sustanciales a esta política de privacidad, podemos notificarlo publicando de manera prominente un aviso de dichos cambios o enviándole directamente una notificación. Le recomendamos que revise esta política de privacidad con frecuencia para recibir información sobre cómo protegemos su información.
|
||||||
|
|
||||||
|
|
||||||
|
## 12. ¿CÓMO PUEDE CONTACTARNOS SOBRE ESTA POLÍTICA? ##
|
||||||
|
|
||||||
|
Si tiene preguntas o comentarios sobre esta política, puede enviarnos un correo electrónico a info@comunes.org o por correo postal a:
|
||||||
|
|
||||||
|
ASOCIACIÓN COMUNES
|
||||||
|
C/ COLÓN 21, 5º DCHA, ESC DCHA
|
||||||
|
03001 ALICANTE
|
||||||
|
ESPAÑA
|
||||||
|
|
||||||
|
Si tiene más preguntas o comentarios sobre nosotras/os o nuestras políticas, envíenos un correo electrónico a info@comunes.org o por correo postal a:
|
||||||
|
|
||||||
|
ASOCIACIÓN COMUNES
|
||||||
|
C/ COLÓN 21, 5º DCHA, ESC DCHA
|
||||||
|
03001 ALICANTE
|
||||||
|
ESPAÑA
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 629 KiB After Width: | Height: | Size: 629 KiB |
BIN
public/images/home-fires-1000.webp
Normal file
|
After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 128 KiB |
BIN
public/images/home-fires-400.webp
Normal file
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 429 KiB After Width: | Height: | Size: 429 KiB |
BIN
public/images/home-fires-800.webp
Normal file
|
After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 703 KiB After Width: | Height: | Size: 703 KiB |
BIN
public/images/home-fires.webp
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
public/images/home-slide-snd-1-blur.webp
Normal file
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 2.4 MiB After Width: | Height: | Size: 2.4 MiB |
BIN
public/images/home-slide-snd-1.webp
Normal file
|
After Width: | Height: | Size: 154 KiB |
BIN
public/images/home-slide-snd-2-blur.webp
Normal file
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
BIN
public/images/home-slide-snd-2.webp
Normal file
|
After Width: | Height: | Size: 280 KiB |
BIN
public/images/home-slide1-blur.webp
Normal file
|
After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 2.3 MiB After Width: | Height: | Size: 2.3 MiB |
BIN
public/images/home-slide1.webp
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
public/images/home-slide2-blur.webp
Normal file
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 363 KiB After Width: | Height: | Size: 363 KiB |
BIN
public/images/home-slide2.webp
Normal file
|
After Width: | Height: | Size: 216 KiB |
BIN
public/images/home-slide3-blur.webp
Normal file
|
After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 2.1 MiB |
BIN
public/images/home-slide3.webp
Normal file
|
After Width: | Height: | Size: 235 KiB |
BIN
public/images/home-slide4-blur.webp
Normal file
|
After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 953 KiB After Width: | Height: | Size: 953 KiB |
BIN
public/images/home-slide4.webp
Normal file
|
After Width: | Height: | Size: 385 KiB |