├── loader ├── compare.py └── loader.py ├── public ├── robots.txt ├── assets │ └── images │ │ ├── emoji │ │ ├── hot.png │ │ ├── mask.png │ │ ├── smile.png │ │ └── sneezing.png │ │ ├── social-img.png │ │ ├── weizmann-logo.jpg │ │ ├── side-bar-icons │ │ ├── pin.svg │ │ ├── email.svg │ │ ├── bag.svg │ │ ├── info.svg │ │ ├── coding.svg │ │ ├── routh.svg │ │ ├── track.svg │ │ ├── plane.svg │ │ ├── language.svg │ │ └── city.svg │ │ ├── x-icon.svg │ │ ├── map-icons │ │ ├── allTime.svg │ │ ├── yesterday.svg │ │ ├── sick │ │ │ └── high-sick.svg │ │ ├── city-m.svg │ │ ├── city-l.svg │ │ ├── city-small.svg │ │ ├── min.svg │ │ ├── plus.svg │ │ ├── gps.svg │ │ ├── gps-blue.svg │ │ ├── plane-map-icon.svg │ │ ├── delivery-icon.svg │ │ ├── filters.svg │ │ ├── help.svg │ │ ├── street-food-icon.svg │ │ └── mapReader-he.svg │ │ ├── phone.svg │ │ ├── alarm.svg │ │ ├── form-success.svg │ │ ├── crosshair.svg │ │ └── icm.svg ├── data │ ├── 178a955d-e175-4e61-9dd2-5e3821a237f3.jpeg │ └── top-cities.js ├── embed │ └── test.html ├── js │ ├── strings.js │ ├── embed.js │ ├── utils.js │ ├── search-city.js │ ├── feedback.js │ ├── flights.js │ ├── firestore.js │ ├── info.js │ ├── admin.js │ ├── tracks.js │ └── scripts.js ├── css │ ├── embed.css │ ├── flights.css │ ├── admin.css │ ├── search-city.css │ ├── feedback.css │ └── map.css ├── languages │ ├── he.js │ ├── ar.js │ ├── en.js │ ├── ru.js │ ├── th.js │ └── i18n.js ├── search-city │ └── index.html ├── sitemap.xml ├── admin │ └── index.html └── tracks │ └── index.html ├── data-gen ├── translations │ ├── manual_translations_ar.json │ ├── translate.sh │ ├── translation_cache_ar.json │ ├── README │ ├── update.py │ └── translate.py ├── translate.py ├── format_data.py ├── process.py ├── extract.py └── compare.py ├── googlebd49e7e243f53188.html ├── push_data.sh ├── package.json ├── .eslintrc.json ├── README.md ├── project_turndown └── index.html ├── firebase.json ├── check.py ├── git_export_all_file_versions.sh ├── .gitignore └── LICENSE /loader/compare.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /data-gen/translations/manual_translations_ar.json: -------------------------------------------------------------------------------- 1 | {"מלון מגדל דוד": "fdfd"} -------------------------------------------------------------------------------- /googlebd49e7e243f53188.html: -------------------------------------------------------------------------------- 1 | google-site-verification: googlebd49e7e243f53188.html -------------------------------------------------------------------------------- /public/assets/images/emoji/hot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oferb/israelcoronamap/HEAD/public/assets/images/emoji/hot.png -------------------------------------------------------------------------------- /public/assets/images/emoji/mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oferb/israelcoronamap/HEAD/public/assets/images/emoji/mask.png -------------------------------------------------------------------------------- /public/assets/images/social-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oferb/israelcoronamap/HEAD/public/assets/images/social-img.png -------------------------------------------------------------------------------- /public/assets/images/emoji/smile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oferb/israelcoronamap/HEAD/public/assets/images/emoji/smile.png -------------------------------------------------------------------------------- /public/assets/images/weizmann-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oferb/israelcoronamap/HEAD/public/assets/images/weizmann-logo.jpg -------------------------------------------------------------------------------- /public/assets/images/emoji/sneezing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oferb/israelcoronamap/HEAD/public/assets/images/emoji/sneezing.png -------------------------------------------------------------------------------- /data-gen/translations/translate.sh: -------------------------------------------------------------------------------- 1 | # Set GOOGLE_APPLICATION_CREDENTIALS=coronavirus-il-8acf4d7b9bec.json 2 | python translate.py He $1 3 | -------------------------------------------------------------------------------- /public/data/178a955d-e175-4e61-9dd2-5e3821a237f3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oferb/israelcoronamap/HEAD/public/data/178a955d-e175-4e61-9dd2-5e3821a237f3.jpeg -------------------------------------------------------------------------------- /public/embed/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /push_data.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -ex 4 | 5 | while true 6 | do 7 | python3 loader/loader.py 8 | git add -A 9 | git commit -m 'Data update' 10 | git push 11 | firebase deploy --project coronavirus-il 12 | sleep 1800 13 | done 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-package", 3 | "scripts": { 4 | "lint": "eslint .", 5 | "lint-fix": "eslint . --fix", 6 | "precommit": "npm run lint" 7 | }, 8 | "devDependencies": { 9 | "eslint": "^6.8.0", 10 | "eslint-config-airbnb-base": "^14.0.0", 11 | "eslint-plugin-import": "^2.20.1" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /data-gen/translate.py: -------------------------------------------------------------------------------- 1 | from google.cloud import translate_v2 as translate 2 | translate_client = translate.Client() 3 | 4 | result = translate_client.translate(text, target_language='he') 5 | >>> result['translatedText'] 6 | 'שלום לך' 7 | >>> text = 'שלום לך' 8 | >>> result = translate_client.translate(text, target_language='en') 9 | >>> result['translatedText'] 10 | -------------------------------------------------------------------------------- /data-gen/translations/translation_cache_ar.json: -------------------------------------------------------------------------------- 1 | { 2 | "גן לאומי קיסריה": "حديقة قيسارية الوطنية", 3 | "מלון גבריאל": "فندق جبرائيل", 4 | "כנסיית הבשורה": "كنيسة الانجيل", 5 | "כניסת המשפחה הקדושה": "دخول العائلة المقدسة", 6 | "עין גב כפר הנופש": "الجزء الخلفي من المنتجع", 7 | "כנסיית הבכורה של פטרוס": "كنيسة القديس بطرس الأولى", 8 | "מסעדת תנורין": "مطعم فرن", 9 | "כפר נחום": "كفرناحوم" 10 | } -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "sourceType": "module" 4 | }, 5 | "extends": [ 6 | "eslint:recommended" 7 | ], 8 | "env": { 9 | "browser": true, 10 | "es2017": true, 11 | "jquery": true 12 | }, 13 | "rules": { 14 | "semi": [ 15 | 2, 16 | "always" 17 | ], 18 | "no-var": 2, 19 | "indent": [ 20 | "error", 21 | 2 22 | ], 23 | "func-style": ["error", "expression"], 24 | "space-infix-ops": ["error"] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /public/assets/images/side-bar-icons/pin.svg: -------------------------------------------------------------------------------- 1 | pin -------------------------------------------------------------------------------- /public/assets/images/x-icon.svg: -------------------------------------------------------------------------------- 1 | x-icon -------------------------------------------------------------------------------- /public/assets/images/map-icons/allTime.svg: -------------------------------------------------------------------------------- 1 | All time -------------------------------------------------------------------------------- /public/assets/images/map-icons/yesterday.svg: -------------------------------------------------------------------------------- 1 | yesterday -------------------------------------------------------------------------------- /public/assets/images/side-bar-icons/email.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/assets/images/map-icons/sick/high-sick.svg: -------------------------------------------------------------------------------- 1 | yesterday 2 | -------------------------------------------------------------------------------- /public/assets/images/map-icons/city-m.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/assets/images/map-icons/city-l.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/assets/images/map-icons/city-small.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/assets/images/phone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/assets/images/map-icons/min.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Israel Coronavirus Map 2 | 3 | A website that shows you whereabouts of people with verified Coronavirus. Data is published by the Ministry of Health. 4 | 5 | Code is VanillaJS, so no framework required! 6 | 7 | ## How to run locally 8 | from the *public* dir, run: 9 | ```shell script 10 | python -m SimpleHTTPServer 11 | ``` 12 | or for python3: 13 | ```shell script 14 | python3 -m http.server 15 | ``` 16 | and navigate to http://localhost:8000. 17 | 18 | 19 | For questions, please contact Ofer Bartal: 20 | * blueofer@gmail.com 21 | * https://www.linkedin.com/in/ofer-bartal-58a50811/ 22 | * https://www.facebook.com/ofer.bartal 23 | -------------------------------------------------------------------------------- /public/js/strings.js: -------------------------------------------------------------------------------- 1 | const sickElementUpdate = (id, value) => { 2 | if (document.getElementById(id)) { 3 | document.getElementById(id).textContent = value; 4 | } 5 | }; 6 | 7 | const sickDataUpdate = (updatedTime, numberOfSickPeople, numberOfRecovered, numberOfDeaths) => { 8 | sickElementUpdate("number-of-sick-people-text", convertNumberToStringWithCommas(numberOfSickPeople)); 9 | sickElementUpdate("number-of-recovered-people-text", convertNumberToStringWithCommas(numberOfRecovered)); 10 | sickElementUpdate("number-of-deaths-text", convertNumberToStringWithCommas(numberOfDeaths)); 11 | sickElementUpdate("last-updated-time-sick", updatedTime); 12 | }; 13 | 14 | getSickPeopleData(sickDataUpdate); 15 | -------------------------------------------------------------------------------- /data-gen/translations/README: -------------------------------------------------------------------------------- 1 | To translate the hebrew data to a language: 2 | 3 | bash translate.sh 4 | 5 | Where LANGUAGE is the two-letter language code: en, ar, fr, etc. 6 | This outputs the translated data to data-.json, and maintains a cache at translation_cache_.json 7 | 8 | 9 | To update the translated data and the translation cache with a manual translation, use: 10 | 11 | python update.py 12 | 13 | Where PATH points to a JSON file with the manual translation, structured in the following way: 14 | [ 15 | {id: 1, label: 'Example label'}, 16 | {id: 2, label: 'Example label'}, 17 | ... 18 | ] 19 | 20 | The list of languages which have been updated with manual translations is maintained at updated.json. 21 | -------------------------------------------------------------------------------- /project_turndown/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 14 | 15 |
16 |
17 |

מפת הקורונה של ישראל ירדה מהאוויר, לביינתים בתקווה שלתמיד

18 |
19 |

תודה לכל האנשים והגופים הנהדרים שלקחו חלק בפרויקט הזה

20 |
21 |

לכל שאלה מוזמנים לפנות אלינו כאן: blueofer@gmail.com

22 |
23 |
24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "project_turndown", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ], 9 | "rewrites": [ 10 | { 11 | "source": "**", 12 | "destination": "/index.html" 13 | } 14 | ], 15 | "headers": [ 16 | { 17 | "source": "**/*.*", 18 | "headers": [ 19 | { 20 | "key": "Cache-Control", 21 | "value": "no-cache, no-store, must-revalidate" 22 | }, 23 | { 24 | "key": "Pragma", 25 | "value": "no-cache" 26 | }, 27 | { 28 | "key": "Expires", 29 | "value": "0" 30 | } 31 | ] 32 | } 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /public/assets/images/alarm.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /public/js/embed.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /* eslint-disable no-undef */ 3 | const copyToClipboard = () => { 4 | let clipboard = new ClipboardJS('#copyToClipboardButton', { 5 | container: document.getElementsByClassName('modal') 6 | }); 7 | clipboard.on('success', () => { 8 | changeCopyToClipboardButtonText(); 9 | }); 10 | }; 11 | 12 | const changeCopyToClipboardButtonText = () => { 13 | let button = document.getElementById("copyToClipboardButton"); 14 | button.innerText = "הועתק"; 15 | button.classList.remove("btn-copy-clipboard"); 16 | button.classList.add("btn-copy-clipboard-success"); 17 | setTimeout(() => { 18 | button.innerText = "העתקה"; 19 | button.classList.add("btn-copy-clipboard"); 20 | button.classList.remove("btn-copy-clipboard-success"); 21 | }, 3000); 22 | }; 23 | -------------------------------------------------------------------------------- /public/assets/images/form-success.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /check.py: -------------------------------------------------------------------------------- 1 | from bs4 import BeautifulSoup 2 | import os 3 | 4 | def getHtmlFiles(): 5 | result = [] 6 | for root, dirs, files in os.walk("public"): 7 | for f in files: 8 | if f.endswith(".html"): 9 | result.append(os.path.join(root, f)) 10 | return result 11 | 12 | pages = [ 13 | 'public/index.html', 14 | 'public/info/index.html', 15 | 'public/flights/index.html', 16 | 'public/embed/index.html' 17 | ] 18 | 19 | canonicalHead = None 20 | for filepath in pages: 21 | with open(filepath, "r") as f: 22 | htmlContents = f.read() 23 | soup = BeautifulSoup(htmlContents, 'html.parser') 24 | if not canonicalHead: 25 | canonicalHead = str(soup.head) 26 | canonicalHead = canonicalHead[:canonicalHead.index("")] 27 | head = str(soup.head) 28 | head = head[:head.index("")] 29 | assert head == canonicalHead, 'mean head differs with ' + filepath 30 | 31 | #print(soup.head) 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /public/assets/images/side-bar-icons/bag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /public/assets/images/side-bar-icons/info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /data-gen/format_data.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | import urllib.request 4 | 5 | OUTPUT_FILE = 'data.json' 6 | CORONA_DATA_URL = "https://gis.health.gov.il/arcgis/rest/services/CoronaExposureSites/MapServer/0/query?f=json&where=(ID%20%3C%3E%20%27XXXX%27)&returnGeometry=true&spatialRel=esriSpatialRelIntersects&geometryType=esriGeometryEnvelope&outFields=*" 7 | 8 | with urllib.request.urlopen(CORONA_DATA_URL) as response: 9 | corona_exposures_sites_query_data = json.loads(response.read().decode('utf-8')) 10 | 11 | output_set = [] 12 | for node in corona_exposures_sites_query_data["features"]: 13 | case = node['attributes'] 14 | pos = node['geometry'] 15 | 16 | output_set.append({"lat": pos["y"], 17 | "lon": pos["x"], 18 | "label": case["Place"], 19 | "text": case["Name"], 20 | "pat_num": case["ID"], 21 | "t_start": str(datetime.datetime.fromtimestamp(case["fromTime"]/1000)), 22 | "t_end": str(datetime.datetime.fromtimestamp(case["toTime"]/1000)), 23 | "pub_date": "", 24 | "pub_ts": case["Date"], 25 | "link": ""}) 26 | 27 | with open(OUTPUT_FILE, 'w', encoding='utf-8') as outfile: 28 | json.dump(output_set, outfile, ensure_ascii=False, indent=4) 29 | -------------------------------------------------------------------------------- /public/css/embed.css: -------------------------------------------------------------------------------- 1 | #iframeCodeBlock { 2 | color: #475b6f; 3 | background: rgba(0,68,102,.06); 4 | font-size: 13px; 5 | padding: 10px 15px; 6 | border-radius: 10px; 7 | margin-bottom: 10px; 8 | } 9 | 10 | h5:after { 11 | content:' '; 12 | display:block; 13 | width: 40%; 14 | margin-top: 5px; 15 | border:2px solid #FFCF4A; 16 | border-radius:4px; 17 | -webkit-border-radius:4px; 18 | -moz-border-radius:4px; 19 | box-shadow:inset 0 3px 30px #FFCF4A99; 20 | -webkit-box-shadow:inset 0 3px 30px #FFCF4A99; 21 | -moz-box-shadow:inset 0 3px 30px #FFCF4A99; 22 | } 23 | 24 | .btn-copy-clipboard, .btn-copy-clipboard-success, .btn-copy-clipboard-error{ 25 | font-weight: 700; 26 | letter-spacing: 0.05em; 27 | } 28 | 29 | .btn-copy-clipboard { 30 | color: #00a4eb; 31 | } 32 | 33 | .btn-copy-clipboard:hover { 34 | color: #00a4eb; 35 | } 36 | 37 | .btn-copy-clipboard-success { 38 | color: #00cf8a; 39 | pointer-events: none; 40 | } 41 | 42 | .btn-copy-clipboard-error { 43 | color: #DE3B3B; 44 | pointer-events: none; 45 | } 46 | 47 | .btn-copy-clipboard:focus, .btn-copy-clipboard:active, 48 | .btn-copy-clipboard-success:focus, .btn-copy-clipboard-success:active { 49 | outline: none !important; 50 | box-shadow: none; 51 | } 52 | 53 | .powered-by-icm-logo { 54 | margin-left: 5px; 55 | } 56 | -------------------------------------------------------------------------------- /public/assets/images/map-icons/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /public/languages/he.js: -------------------------------------------------------------------------------- 1 | const he = { 2 | direction: 'rtl', 3 | title: 'מפת הקורונה של ישראל: כל המקומות שבהם שהו החולים בישראל', 4 | patientNumber: 'מספר חולה', 5 | patientNumberShort: 'חולה', 6 | visitingTimes: 'זמני ביקור', 7 | visitingTime: 'זמן ביקור', 8 | publishedDate: 'תאריך פרסום', 9 | timeLeftForStayingInSolitary: 'זמן נותר לשוהים בבידוד', 10 | expire14DaysAfterExposure: 'תמו 14 ימים ממועד החשיפה', 11 | linkToTheMinistryOfHealthPublication:'לינק לפרסום של משרד הבריאות', 12 | days: 'ימים', 13 | hours:'שעות', 14 | minutes:'דקות', 15 | secondes: 'שניות', 16 | betweenTheHours: '', 17 | departure: 'המראה', 18 | arrival: 'נחיתה', 19 | flightSearchPlaceholder: 'חפש טיסה לפי מספר ,תאריך, יעד, מקור או מס\' חולה', 20 | 'last-updated-text': 'עדכון אחרון:', 21 | 'magen-david-adom': 'מד"א: 101', 22 | 'health-ministry': 'משרד הבריאות: 5400*', 23 | 'search-for-flights': 'חפשו טיסות', 24 | 'embed-code': 'הטמיעו קוד באתר', 25 | 'about': 'אודות המיזם', 26 | 'contact-use': 'צרו קשר', 27 | 'select-language': 'בחרו שפה', 28 | 'state-of-patients-israel': 'מצב החולים בישראל', 29 | 'number-of-sick-people': 'מספר החולים:', 30 | 'number-of-recovered-people': 'מספר המחלימים:', 31 | 'number-of-deaths': 'מספר המתים:', 32 | 'number-of-people-in-quarantine': 'מספר מבודדי בית:', 33 | 'sick-update-title': 'מצב החולים בישראל', 34 | }; 35 | langs['he'] = he; 36 | 37 | -------------------------------------------------------------------------------- /public/languages/ar.js: -------------------------------------------------------------------------------- 1 | const ar = { 2 | direction: 'rtl', 3 | title: 'Israel Corona Map', 4 | patientNumber: 'رقم المريض', 5 | patientNumberShort: 'المريض', 6 | visitingTimes: 'أوقات الزيارة', 7 | visitingTime: 'وقت الزيارة', 8 | publishedDate: 'تاريخ النشر', 9 | timeLeftForStayingInSolitary: 'الوقت المتبقي للمكوث في الحجر', 10 | expire14DaysAfterExposure: 'تنتهي بعد 14 يوم من موعد التعرض', 11 | linkToTheMinistryOfHealthPublication:'الرابط لمنشور وزارة الصحة', 12 | days: 'أيام', 13 | hours:'ساعات', 14 | minutes:'دقائق', 15 | secondes: 'ثواني', 16 | betweenTheHours: '', 17 | departure: 'Departure', 18 | arrival: 'Arrival', 19 | flightSearchPlaceholder: 'Search by flight, location, date, patient number', 20 | 'last-updated-text': ':آخر تحديث في', 21 | 'magen-david-adom': 'نجمة داود الحمراء: 101', 22 | 'health-ministry': 'وزارة الصحة: 5400*', 23 | 'search-for-flights': 'البحث عن رحلات جوية', 24 | 'embed-code': 'التعليمات البرمجية للتضمين', 25 | 'about': 'حول', 26 | 'contact-use': 'تواصل معنا', 27 | 'select-language': 'اختر اللغة', 28 | 'state-of-patients-israel': 'دولة المرضى في إسرائيل', 29 | 'number-of-sick-people': 'Sick Patients', 30 | 'number-of-recovered-people': 'Recovered', 31 | 'number-of-deaths': 'Deaths', 32 | 'number-of-people-in-quarantine': 'People In Quarantine', 33 | 'sick-update-title': 'Patients State In Israel', 34 | }; 35 | langs['ar'] = ar; 36 | 37 | -------------------------------------------------------------------------------- /public/languages/en.js: -------------------------------------------------------------------------------- 1 | const en = { 2 | direction: 'ltr', 3 | title: 'Israel Corona Map', 4 | patientNumber: 'Patient number', 5 | patientNumberShort: 'Patient', 6 | visitingTimes: 'Visiting times', 7 | visitingTime: 'Visit time', 8 | publishedDate: 'Publication Date', 9 | timeLeftForStayingInSolitary: 'Time left for quarantine', 10 | expire14DaysAfterExposure: '14 days have passed - no need for quarantine', 11 | linkToTheMinistryOfHealthPublication:'Ministry Of Health official publication', 12 | days: 'days', 13 | hours:'hours', 14 | minutes:'minutes', 15 | secondes: 'seconds', 16 | betweenTheHours: '', 17 | departure: 'Departure', 18 | arrival: 'Arrival', 19 | flightSearchPlaceholder: 'Search by flight, location, date, patient number', 20 | 'last-updated-text': 'Last Updated In:', 21 | 'magen-david-adom': 'Magen David Adom: 101', 22 | 'health-ministry': 'Ministry of Health: *5400', 23 | 'search-for-flights': 'Search for flights', 24 | 'embed-code': 'Embeded code generator', 25 | 'about': 'About', 26 | 'contact-use': 'Contact us', 27 | 'select-language': 'Change language', 28 | 'state-of-patients-israel': 'Patient statistics', 29 | 'number-of-sick-people': 'Sick Patients', 30 | 'number-of-recovered-people': 'Recovered', 31 | 'number-of-deaths': 'Deaths', 32 | 'number-of-people-in-quarantine': 'People In Quarantine', 33 | 'sick-update-title': 'Patient statistics', 34 | }; 35 | langs['en'] = en; 36 | 37 | -------------------------------------------------------------------------------- /public/assets/images/map-icons/gps.svg: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | gps_new 9 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /public/languages/ru.js: -------------------------------------------------------------------------------- 1 | const ru = { 2 | direction: 'ltr', 3 | title: 'Israel Corona Map', 4 | patientNumber: 'Номера пациентов', 5 | patientNumberShort: 'терпеливый', 6 | visitingTimes: 'Времена посещения', 7 | visitingTime: 'Время посещения', 8 | publishedDate: 'Дата публикации.', 9 | timeLeftForStayingInSolitary: 'Осталось времени в карантине', 10 | expire14DaysAfterExposure: 'Истекает через 14 дней после взаимодействия', 11 | linkToTheMinistryOfHealthPublication:'Ссылка на публикацию министерства здравоохранения', 12 | days: 'дней', 13 | hours: 'часов', 14 | minutes:'минут', 15 | secondes: 'секунд', 16 | betweenTheHours: 'Между часами', 17 | departure: 'Departure', 18 | arrival: 'Arrival', 19 | flightSearchPlaceholder: 'Search by flight, location, date, patient number', 20 | 'last-updated-text': 'Последнее обновление:', 21 | 'magen-david-adom': 'Маген Давид Адом', 22 | 'health-ministry': 'Министерство здравоохранения', 23 | 'search-for-flights': 'Искать рейсы', 24 | 'embed-code': 'Встроить карту', 25 | 'about': 'О нас', 26 | 'contact-use': 'Связаться с нами', 27 | 'select-language': 'Выбрать язык', 28 | 'state-of-patients-israel': 'Patient statistics', 29 | 'number-of-sick-people': 'Sick Patients', 30 | 'number-of-recovered-people': 'Recovered', 31 | 'number-of-deaths': 'Deaths', 32 | 'number-of-people-in-quarantine': 'People In Quarantine', 33 | 'sick-update-title': 'Patient statistics', 34 | }; 35 | langs['ru'] = ru; 36 | -------------------------------------------------------------------------------- /public/assets/images/map-icons/gps-blue.svg: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | gps_new 9 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /public/assets/images/side-bar-icons/coding.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /public/css/flights.css: -------------------------------------------------------------------------------- 1 | .search-box { 2 | margin-right: inherit; 3 | padding-left: 10px; 4 | width: 86%; 5 | order: 1; 6 | margin: inherit; 7 | } 8 | 9 | .search-box-dekstop { 10 | text-align: center; 11 | margin: 75px auto 5px; 12 | } 13 | 14 | .flight-list { 15 | margin-top: 50px; 16 | } 17 | 18 | .card { 19 | width: 100% !important; 20 | } 21 | 22 | .card-contant-box{ 23 | display: flex; 24 | justify-content: space-between; 25 | align-items: center; 26 | margin-bottom: 5px; 27 | } 28 | 29 | .card-text{ 30 | display: flex; 31 | } 32 | 33 | .flight-detailes{ 34 | margin-right: 10px; 35 | } 36 | 37 | .patient-number{ 38 | width: 100px; 39 | } 40 | 41 | .time-text { 42 | width: 100px; 43 | } 44 | 45 | .card-title-box { 46 | display: flex; 47 | justify-content: space-between; 48 | } 49 | 50 | .card-title{ 51 | font-weight: bold; 52 | } 53 | 54 | .flight-description{ 55 | font-weight: 600; 56 | } 57 | 58 | .search-input{ 59 | width:400px !important; 60 | margin: 0px !important; 61 | } 62 | 63 | .flip-icon { 64 | -moz-transform: scaleX(-1); 65 | -o-transform: scaleX(-1); 66 | -webkit-transform: scaleX(-1); 67 | transform: scaleX(-1); 68 | filter: FlipH; 69 | -ms-filter: "FlipH"; 70 | } 71 | 72 | @media(min-width:768px) { 73 | .search-box-dekstop { 74 | width: 400px; 75 | margin: 75px auto 5px; 76 | } 77 | 78 | .flight-list { 79 | margin: auto; 80 | width: fit-content; 81 | height: 81vh; 82 | } 83 | 84 | .card { 85 | width: 25rem !important; 86 | } 87 | } -------------------------------------------------------------------------------- /public/assets/images/map-icons/plane-map-icon.svg: -------------------------------------------------------------------------------- 1 | plane-map-icon -------------------------------------------------------------------------------- /public/assets/images/crosshair.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /public/assets/images/side-bar-icons/routh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /data-gen/translations/update.py: -------------------------------------------------------------------------------- 1 | import json 2 | from sys import argv 3 | 4 | def main(): 5 | lang = argv[1] 6 | src_path = argv[2] 7 | try: 8 | with open('data%s.json' % lang, 'r', encoding='utf8') as infile: 9 | source = json.load(infile) 10 | except FileNotFoundError: 11 | print('Existing translation not found at data%s.json, the file will be created.' % lang) 12 | source = [] 13 | try: 14 | with open('translation_cache_%s.json' % lang, 'r', encoding='utf8') as incache: 15 | cache = json.load(incache) 16 | except FileNotFoundError: 17 | print('Existing cache not found at translation_cache_%s.json, a new cache will be created.' % lang) 18 | cache = dict() 19 | 20 | new = [] 21 | with open(src_path, 'r', encoding='utf8') as new_translations: 22 | new = json.load(new_translations) 23 | 24 | for entry in new: 25 | _id, label = entry['id'], entry['label'] 26 | cache[_id] = label 27 | find_entry_by_id(source, _id)['label'] = label 28 | 29 | with open('data%s.json' % lang, 'w', encoding='utf8') as data_out: 30 | json.dump(source, data_out, ensure_ascii=False) 31 | with open('translation_cache_%s.json' % lang, 'w', encoding='utf8') as cache_out: 32 | json.dump(cache, cache_out, ensure_ascii=False) 33 | 34 | try: 35 | with open('updated.json', 'r', encoding='utf8') as langlist: 36 | languages = json.load(langlist) 37 | if lang not in languages: 38 | languages.append(lang) 39 | except FileNotFoundError: 40 | languages = [lang] 41 | with open('updated.json', 'a+', encoding='utf8') as langlist: 42 | json.dump(languages, langlist, ensure_ascii=False) 43 | 44 | 45 | def find_entry_by_id(data, _id): 46 | for entry in data: 47 | if entry['id'] == _id: 48 | return entry 49 | 50 | if __name__ == '__main__': 51 | main() 52 | -------------------------------------------------------------------------------- /git_export_all_file_versions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # we'll write all git versions of the file to this folder: 4 | EXPORT_TO=/tmp/all_versions_exported 5 | 6 | # take relative path to the file to inspect 7 | GIT_PATH_TO_FILE=$1 8 | 9 | # ---------------- don't edit below this line -------------- 10 | 11 | USAGE="Please cd to the root of your git proj and specify path to file you with to inspect (example: $0 some/path/to/file)" 12 | 13 | # check if got argument 14 | if [ "${GIT_PATH_TO_FILE}" == "" ]; then 15 | echo "error: no arguments given. ${USAGE}" >&2 16 | exit 1 17 | fi 18 | 19 | # check if file exist 20 | if [ ! -f ${GIT_PATH_TO_FILE} ]; then 21 | echo "error: File '${GIT_PATH_TO_FILE}' does not exist. ${USAGE}" >&2 22 | exit 1 23 | fi 24 | 25 | # extract just a filename from given relative path (will be used in result file names) 26 | GIT_SHORT_FILENAME=$(basename $GIT_PATH_TO_FILE) 27 | 28 | # create folder to store all revisions of the file 29 | if [ ! -d ${EXPORT_TO} ]; then 30 | echo "creating folder: ${EXPORT_TO}" 31 | mkdir ${EXPORT_TO} 32 | fi 33 | 34 | ## uncomment next line to clear export folder each time you run script 35 | #rm ${EXPORT_TO}/* 36 | 37 | # reset coutner 38 | COUNT=0 39 | 40 | # iterate all revisions 41 | git rev-list --all --objects -- ${GIT_PATH_TO_FILE} | \ 42 | cut -d ' ' -f1 | \ 43 | while read h; do \ 44 | COUNT=$((COUNT + 1)); \ 45 | COUNT_PRETTY=$(printf "%04d" $COUNT); \ 46 | COMMIT_DATE=`git show $h | head -3 | grep 'Date:' | awk '{print $4"-"$3"-"$6}'`; \ 47 | if [ "${COMMIT_DATE}" != "" ]; then \ 48 | git cat-file -p ${h}:${GIT_PATH_TO_FILE} > ${EXPORT_TO}/${COUNT_PRETTY}.${COMMIT_DATE}.${h}.${GIT_SHORT_FILENAME};\ 49 | fi;\ 50 | done 51 | 52 | # return success code 53 | echo "result stored to ${EXPORT_TO}" 54 | exit 0 -------------------------------------------------------------------------------- /public/js/utils.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const getQueryParam = (name) => { 3 | const queryString = window.location.search; 4 | const urlParams = new URLSearchParams(queryString); 5 | return urlParams.get(name); 6 | }; 7 | 8 | const setQueryParam = (name, value) => { 9 | let urlParams = new URLSearchParams(window.location.search); 10 | urlParams.set(name, value); 11 | urlParams.sort(); 12 | window.history.pushState('Corona map', 'Corona map', window.location.pathname + '?' + urlParams.toString()); 13 | }; 14 | 15 | const deleteQueryParam = (name) => { 16 | const queryString = window.location.search; 17 | const urlParams = new URLSearchParams(queryString); 18 | urlParams.delete(name); 19 | window.history.pushState('Corona map', 'Corona map', window.location.pathname + '?' + urlParams.toString()); 20 | }; 21 | 22 | /** 23 | * @typedef {{ 24 | * zoom: number, 25 | * center: { 26 | * lat: number, 27 | * lng: number 28 | * } 29 | * }} MapBounds 30 | */ 31 | 32 | /** 33 | * @param {MapBounds} mapLocation 34 | */ 35 | const setBoundsToLocalStorage = (mapLocation) => { 36 | localStorage.setItem('mapLocation', JSON.stringify(mapLocation)); 37 | } 38 | 39 | /** 40 | * @returns {MapBounds} 41 | */ 42 | const getBoundsFromLocalStorage = () => { 43 | return JSON.parse(localStorage.getItem('mapLocation') || 'null'); 44 | } 45 | 46 | const convertNumberToStringWithCommas = (x) => { 47 | return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); 48 | } 49 | 50 | const convertTimestampToDateAndTime = (timestamp) => { 51 | const date = new Date(timestamp); 52 | const dd = String(date.getDate()).padStart(2, '0'); 53 | const mm = String(date.getMonth() + 1).padStart(2, '0'); 54 | const yyyy = date.getFullYear(); 55 | 56 | let minutes = date.getMinutes(); 57 | minutes = (minutes < 10 ? '0' : '') + minutes; 58 | const hour = date.getHours(); 59 | 60 | return `${dd}.${mm}.${yyyy} ${hour}:${minutes}`; 61 | } 62 | -------------------------------------------------------------------------------- /public/languages/th.js: -------------------------------------------------------------------------------- 1 | // Thai translation th-TH 2 | const th = { 3 | direction: 'ltr', 4 | title: 'คอรอนาแผนที่ในประเทศอิสราเอล', // Israel Corona Map 5 | patientNumber: 'คนไข้มายเลข', // Patient number 6 | patientNumberShort: 'คนไข้', // Patient 7 | visitingTimes: 'ชั่วโมงที่เขาอยู่ที่นั่น', // Visiting times 8 | visitingTime: '', // Visit time 9 | publishedDate: 'ปรากฏ', // Publication Date 10 | timeLeftForStayingInSolitary: 'เหลือเวลาสำหรับกักกันเสร็จ', // Time left for quarantine 11 | expire14DaysAfterExposure: '14 วันที่ผ่านมากักบริเวณไม่ได้', // 14 days have passed - no need for quarantine 12 | linkToTheMinistryOfHealthPublication:'ปรากฏอิสราเอลกระทรวงสาธารณสุขโดย', // Ministry Of Health official publication 13 | days: 'วัน', // days 14 | hours:'ชั่วโมง', // hours 15 | minutes:'นาที', // minutes 16 | secondes: 'วินาที', // seconds 17 | betweenTheHours: '', 18 | departure: 'จะออก', // Departure 19 | arrival: 'จะลงจอด', // Arrival 20 | flightSearchPlaceholder: 'ใช้หาเที่ยวบินหรือที่ตั้งรือวันที่รือคนไข้เบอร์', // Search by flight, location, date, patient number 21 | 'last-updated-text': 'เช็คด้วย', // Last Updated In: 22 | 'magen-david-adom': 'อิสราเอลฉุกเฉินเบอร์โทรศัพท์ 101', // Magen David Adom: 101 23 | 'health-ministry': 'อิสราเอลกระทรวงสาธารณสุขเบอร์โทรศัพท์ *5400', // Ministry of Health: *5400 24 | 'search-for-flights': 'ใช้หาเที่ยวบิน', // Search for flights 25 | 'embed-code': 'Embeded code generator', 26 | 'about': 'เกี่ยวกับเว็บไซต์', // About 27 | 'contact-use': 'ติดต่อ', // Contact us 28 | 'select-language': 'ใช้เปลี่ยนภาษา', // Change language 29 | 'state-of-patients-israel': 'Patient statistics', 30 | 'number-of-sick-people': 'คนไข้เป็นไม่ค่อยสบาย', // Sick Patients 31 | 'number-of-recovered-people': 'หาย', // Recovered 32 | 'number-of-deaths': 'มรณะ', // Deaths 33 | 'number-of-people-in-quarantine': 'คนอิสราเอลอยู่ที่ในเป็นกักบริเวณ', // People In Quarantine 34 | 'sick-update-title': 'Statistics เป็นคนไข้', // Patient Statistics 35 | }; 36 | langs['th'] = th; -------------------------------------------------------------------------------- /public/css/admin.css: -------------------------------------------------------------------------------- 1 | body { 2 | 3 | width: 100%; 4 | margin:0; 5 | padding:0; 6 | background: #f4f4f4; 7 | color: #26262C; 8 | font-family: 'Assistant', sans-serif; 9 | } 10 | 11 | #sick-pop-up-admin-container { 12 | padding: 50px; 13 | direction: rtl; 14 | } 15 | 16 | .input-labels { 17 | float: right; 18 | } 19 | 20 | .admin-container{ 21 | display: flex; 22 | flex-direction: column; 23 | align-items: center; 24 | } 25 | 26 | .title-sick-input-status { 27 | font-weight: 700; 28 | font-size: 27px; 29 | margin-top: 20px; 30 | } 31 | 32 | .btn-google { 33 | color: white; 34 | background-color: #ea4335; 35 | width: 230px; 36 | font-size: 15px; 37 | font-weight: 700; 38 | border-radius: 25px; 39 | } 40 | 41 | #submit-success, #submit-failed { 42 | display: none; 43 | } 44 | 45 | #submit-cities-success, #submit-cities-failed { 46 | display: none; 47 | } 48 | 49 | .current-status { 50 | padding-top: 10px; 51 | font-size: 17px; 52 | font-weight: 600; 53 | } 54 | 55 | .bootstrap-table { 56 | width: 100%; 57 | padding: 10px; 58 | } 59 | 60 | .cities-container { 61 | display: flex; 62 | flex-direction: column; 63 | align-items: center; 64 | width: 100%; 65 | margin-top: 40px; 66 | } 67 | 68 | #update-cities-data { 69 | margin-bottom: 8px; 70 | } 71 | 72 | #user-email { 73 | text-transform: lowercase; 74 | } 75 | 76 | .login-container { 77 | display: none; 78 | height: 100vh; 79 | justify-content: center; 80 | align-items: center; 81 | } 82 | 83 | #sign-in-with-google-text { 84 | margin-left: 10px; 85 | } 86 | 87 | .input-group { 88 | max-width: 500px; 89 | padding: 20px; 90 | } 91 | 92 | .custom-file-input ~ .custom-file-label::after { 93 | display: none; 94 | } 95 | 96 | .input-group>.custom-file { 97 | text-align: right; 98 | } 99 | 100 | .excel-file-name { 101 | display: none; 102 | } 103 | 104 | #submit-excel-failed, #submit-excel-success { 105 | display: none; 106 | } 107 | -------------------------------------------------------------------------------- /public/languages/i18n.js: -------------------------------------------------------------------------------- 1 | let langs = {}; 2 | let lang; 3 | let langDirection; 4 | 5 | const getLanguage = () => { 6 | let lang = getQueryParam('lang'); 7 | if (lang) { 8 | return lang.toLowerCase(); 9 | } 10 | lang = localStorage.getItem('language'); 11 | if (lang) { 12 | return lang.toLowerCase(); 13 | } 14 | return 'he'; 15 | }; 16 | 17 | // Save language in local storage if not already there, update HTML 18 | const setLanguage = (selectedLang) => { 19 | lang = selectedLang; 20 | localStorage.setItem('language', lang); 21 | setQueryParam('lang', lang); 22 | langDirection = langs[lang].direction; 23 | setTranslationInHTML(); 24 | document.title = i18n('title'); 25 | }; 26 | 27 | const setTranslationInHTML = () => { 28 | // For keys that match element IDs: 29 | for (var key in langs[lang]) { 30 | setTranslationByID(key, key); 31 | } 32 | // For keys that don't, since 2 elements have the same text 33 | setTranslationByID('magen-david-adom-mobile', 'magen-david-adom'); 34 | setTranslationByID('health-ministry-mobile', 'health-ministry'); 35 | setTranslationByID('last-updated-title-sick', 'last-updated-text'); 36 | setTranslationByID('select-language-header', 'select-language'); 37 | 38 | setPlaceholderTranslationByID('search-track-input-dekstop', 'trackPlaceholder'); 39 | setTranslationByID('track-start-time', 'trackStart'); 40 | setTranslationByID('track-end-time', 'trackEnd'); 41 | 42 | }; 43 | 44 | const setTranslationByID = (id, langKey) => { 45 | const el = document.getElementById(id); 46 | if (el) { 47 | el.innerText = i18n(langKey); 48 | } 49 | }; 50 | 51 | const i18n = langKey => { 52 | if (langs[lang].hasOwnProperty(langKey)) { 53 | return langs[lang][langKey]; 54 | } else { 55 | return langs['he'][langKey]; 56 | } 57 | }; 58 | 59 | const setPlaceholderTranslationByID = (id, text) => { 60 | const el = $("#" + id); 61 | if (el) { 62 | el.attr("placeholder", i18n(text)); 63 | } 64 | }; 65 | 66 | const initLanguage = () => { 67 | // Get language from url or local storage 68 | lang = getLanguage(); 69 | // Set it, and update everything needed 70 | setLanguage(lang); 71 | }; 72 | -------------------------------------------------------------------------------- /data-gen/translations/translate.py: -------------------------------------------------------------------------------- 1 | from sys import argv 2 | import urllib, json 3 | from google.cloud import translate_v2 as translate 4 | 5 | # Set GOOGLE_APPLICATION_CREDENTIALS= 6 | LANGS = ['ar', 'en'] 7 | CLIENT = translate.Client() 8 | 9 | def main(targetLang): 10 | data = [] 11 | with open("../../public/data/data-he.json", newline='') as f: 12 | data = json.load(f) 13 | 14 | print('%d entries' % len(data)) 15 | translateLabels(data[0:12], targetLang) 16 | with open("../../public/data/data-{}.json".format(targetLang), 'w') as f: 17 | json.dump(data, f, indent =2, ensure_ascii=False) 18 | print('Translated %d entries to %s' % (len(data), targetLang)) 19 | 20 | def getCache(lang): 21 | try: 22 | with open('../../translation_cache_%s.json' % lang, 'r', encoding='utf8') as raw_cache: 23 | return json.load(raw_cache) 24 | except FileNotFoundError: 25 | return {} 26 | 27 | def saveCache(targetLang, cache): 28 | with open('translation_cache_%s.json' % targetLang, 'w', encoding='utf8') as cache_out: 29 | json.dump(cache, cache_out, indent=2, ensure_ascii=False) 30 | 31 | def getManualTranslations(lang): 32 | try: 33 | with open('manual_translations_%s.json' % lang, 'r', encoding='utf8') as raw_cache: 34 | return json.load(raw_cache) 35 | except FileNotFoundError: 36 | return {} 37 | 38 | def translateLabels(data, targetLang): 39 | cache = getCache(targetLang) 40 | manual = getManualTranslations(targetLang) 41 | 42 | for index, entry in enumerate(data): 43 | label = entry['label'] 44 | entry['label'] = translateLabel(label, targetLang, cache, manual) 45 | if index % 10 == 0: 46 | saveCache(targetLang, cache) 47 | 48 | def translateLabel(label, dst, cache, manual): 49 | manualResult = manual.get(label) 50 | print(label) 51 | if manualResult: 52 | print("Using manual " + manualResult) 53 | return manualResult 54 | cachedResult = cache.get(label) 55 | if cachedResult: 56 | return cachedResult 57 | result = CLIENT.translate(label, target_language=dst)['translatedText'] 58 | cache.update({label: result}) 59 | print('%s -> %s (%s)' % (label, result, dst)) 60 | return result 61 | 62 | 63 | if __name__ == "__main__": 64 | main(LANGS[0]) 65 | #save_cache() 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .firebase 2 | .DS_Store 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | 12 | # Diagnostic reports (https://nodejs.org/api/report.html) 13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | *.lcov 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Microbundle cache 60 | .rpt2_cache/ 61 | .rts2_cache_cjs/ 62 | .rts2_cache_es/ 63 | .rts2_cache_umd/ 64 | 65 | # Optional REPL history 66 | .node_repl_history 67 | 68 | # Output of 'npm pack' 69 | *.tgz 70 | 71 | # Yarn Integrity file 72 | .yarn-integrity 73 | 74 | # dotenv environment variables file 75 | .env 76 | .env.test 77 | 78 | # parcel-bundler cache (https://parceljs.org/) 79 | .cache 80 | 81 | # Next.js build output 82 | .next 83 | 84 | # Nuxt.js build / generate output 85 | .nuxt 86 | dist 87 | 88 | # Gatsby files 89 | .cache/ 90 | # Comment in the public line in if your project uses Gatsby and not Next.js 91 | # https://nextjs.org/blog/next-9-1#public-directory-support 92 | # public 93 | 94 | # vuepress build output 95 | .vuepress/dist 96 | 97 | # Serverless directories 98 | .serverless/ 99 | 100 | # FuseBox cache 101 | .fusebox/ 102 | 103 | # DynamoDB Local files 104 | .dynamodb/ 105 | 106 | # TernJS port file 107 | .tern-port 108 | 109 | # Stores VSCode versions used for testing VSCode extensions 110 | .vscode-test 111 | 112 | # IntelliJ IDEA 113 | .idea 114 | 115 | data-gen/data.txt 116 | data-gen/locations.txt 117 | 118 | __pycache__ -------------------------------------------------------------------------------- /public/js/search-city.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | let substringMatcher = function(strs) { 5 | return function findMatches(q, cb) { 6 | let matches, substringRegex; 7 | 8 | // an array that will be populated with substring matches 9 | matches = []; 10 | 11 | // regex used to determine if a string contains the substring `q` 12 | substrRegex = new RegExp(q, 'i'); 13 | 14 | // iterate through the pool of strings and for any string that 15 | // contains the substring `q`, add it to the `matches` array 16 | $.each(strs, function(i, str) { 17 | if (substrRegex.test(str)) { 18 | matches.push(str); 19 | } 20 | }); 21 | 22 | cb(matches); 23 | }; 24 | }; 25 | 26 | let citiesByName = []; 27 | let allCities = []; 28 | let lastUpdateOfCities = ''; 29 | 30 | const getCitiesByName = async () => { 31 | const {cities, lastUpdate} = await getAllCitiesData(); 32 | citiesByName = cities.reduce((acc, city) => {acc.push(city.cityName); return acc;}, []); 33 | lastUpdateOfCities = lastUpdate; 34 | allCities = cities; 35 | }; 36 | 37 | $(document).ready(async () => { 38 | await getCitiesByName(); 39 | const lastUpdateDateFormat = convertTimestampToDateAndTime(lastUpdateOfCities); 40 | 41 | $('#last-updated-time-city-search').text(lastUpdateDateFormat); 42 | 43 | $('#city-search-input .typeahead').typeahead({ 44 | highlight: true, 45 | minLength: 1 46 | }, 47 | { 48 | name: 'citiesByName', 49 | source: substringMatcher(citiesByName) 50 | }); 51 | 52 | $('.typeahead').bind('typeahead:select', (ev, suggestion) => { 53 | const chosenCity = allCities.find((city) => city.cityName === suggestion); 54 | if (chosenCity === undefined) return; 55 | 56 | $('.chosen-city-container').css('display', 'block'); 57 | 58 | const { 59 | cityName, 60 | population, 61 | howManyPeopleTested, 62 | infectedPeople, 63 | recovered, 64 | increaseInPercentageInTheLastThreeDays, 65 | increaseInPercentageInTheLastWeek, 66 | sicknessPercentageForOneHundredThousand 67 | } = chosenCity; 68 | 69 | $('#city-name').text(cityName); 70 | $('#city-population').text(population); 71 | $('#city-tested').text(howManyPeopleTested); 72 | $('#city-infected').text(infectedPeople); 73 | $('#city-recovered').text(recovered); 74 | $('#city-increase-sick-three-days').text(increaseInPercentageInTheLastThreeDays); 75 | $('#city-increase-sick-week').text(increaseInPercentageInTheLastWeek); 76 | $('#city-percentage').text(sicknessPercentageForOneHundredThousand); 77 | }); 78 | 79 | }); 80 | 81 | -------------------------------------------------------------------------------- /data-gen/process.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import json 3 | from datetime import date, datetime, timedelta, timezone 4 | 5 | def getPoints(language): 6 | points = [] 7 | labelName = 'label ' + language 8 | descriptionName = 'description ' + language 9 | with open('public/data/data.csv', newline='') as f: 10 | reader = csv.DictReader(f) 11 | for row in reader: 12 | place_name = row[labelName].strip() 13 | if not place_name: 14 | place_name = row['label hebrew'].strip() 15 | location = row['position'].split(',') 16 | lat = float(location[0].strip()) 17 | lon = float(location[1].strip()) 18 | date = row['date'].strip() 19 | t_start = row['start time'].strip() 20 | t_end = row['end time'].strip() 21 | start_time = datetime.strptime( 22 | date + " " + t_start, '%d/%m/%Y %H:%M') 23 | end_time = datetime.strptime(date + " " + t_end, '%d/%m/%Y %H:%M') 24 | description = row[descriptionName].strip() 25 | if not description: 26 | description = row['description hebrew'].strip() 27 | link = row['link'].strip() 28 | publication_time = '' 29 | if 'www.health.gov.il' in link: 30 | publication_time = datetime.strptime(link[63:69], '%d%m%y') 31 | else: 32 | publication_time = datetime.strptime(row['pub_date'].strip(), '%d%m%y') 33 | points.append({"id": row['ID'], 34 | "lat": lat, 35 | "lon": lon, 36 | "label": place_name, 37 | "text": description, 38 | "pat_num": row['patient_number'], 39 | "t_start": start_time.isoformat() + '+02:00', 40 | "t_end": end_time.isoformat() + '+02:00', 41 | "pub_date": publication_time.strftime('%d/%m'), 42 | "pub_ts": int(publication_time.timestamp()), 43 | "link": link}) 44 | return points 45 | 46 | with open('public/data/data-he.json', 'w', encoding='utf-8') as f: 47 | json.dump(getPoints('hebrew'), f, ensure_ascii=False, indent=4) 48 | 49 | with open('public/data/data-ar.json', 'w', encoding='utf-8') as f: 50 | json.dump(getPoints('arabic'), f, ensure_ascii=False, indent=4) 51 | 52 | with open('public/data/data-en.json', 'w', encoding='utf-8') as f: 53 | json.dump(getPoints('english'), f, ensure_ascii=False, indent=4) 54 | -------------------------------------------------------------------------------- /public/js/feedback.js: -------------------------------------------------------------------------------- 1 | 2 | $(document).ready(() => { 3 | $('.report-pop-up-click').click(showFeedbackPopup); 4 | 5 | $('#report-close-x').click(() => { 6 | $('#report-pop-up').fadeOut('fast'); 7 | deleteQueryParam('feedback'); 8 | }); 9 | 10 | $('#health_habits_survey').click(() => { 11 | ga('send', { 12 | hitType: 'event', 13 | eventCategory: 'ClickOnSurvey', 14 | eventAction: 'Click', 15 | eventLabel: 'health_habits_survey' 16 | }); 17 | }); 18 | 19 | $('#behavior_survey').click(() => { 20 | ga('send', { 21 | hitType: 'event', 22 | eventCategory: 'ClickOnSurvey', 23 | eventAction: 'Click', 24 | eventLabel: 'behavior survey' 25 | }); 26 | }); 27 | 28 | 29 | $('#parenthood_survey').click(() => { 30 | ga('send', { 31 | hitType: 'event', 32 | eventCategory: 'ClickOnSurvey', 33 | eventAction: 'Click', 34 | eventLabel: 'Parenthood survey' 35 | }); 36 | }); 37 | 38 | $('#crisis_survey').click(() => { 39 | ga('send', { 40 | hitType: 'event', 41 | eventCategory: 'ClickOnSurvey', 42 | eventAction: 'Click', 43 | eventLabel: 'Crisis survey' 44 | }); 45 | }); 46 | 47 | $('#hope_survey').click(() => { 48 | ga('send', { 49 | hitType: 'event', 50 | eventCategory: 'ClickOnSurvey', 51 | eventAction: 'Click', 52 | eventLabel: 'Hope survey' 53 | }); 54 | }); 55 | 56 | $('#close-feedback-popup, #go-back-to-main-screen').click(() => { 57 | $('#report-pop-up').fadeOut('fast'); 58 | deleteQueryParam('feedback'); 59 | }); 60 | 61 | }); 62 | 63 | const showFeedbackPopup = () => { 64 | ga('send', { 65 | hitType: 'event', 66 | eventCategory: 'ClickOnFeedbackPopUp', 67 | eventAction: 'Click', 68 | eventLabel: 'User open feedback page' 69 | }); 70 | $('#report-pop-up').fadeIn('fast'); 71 | setQueryParam('feedback', 'true'); 72 | }; 73 | 74 | $('#submit-feedback-button').click(async () => { 75 | 76 | ga('send', { 77 | hitType: 'event', 78 | eventCategory: 'SendFeedback', 79 | eventAction: 'Click', 80 | eventLabel: 'User send feedback' 81 | }); 82 | 83 | cleanErrors(); 84 | showLoaderAndPreventDoubleClick(); 85 | const selectedCity = $('#select-city-input :selected').text(); 86 | const selectedFeeling = $('.feeling-box.selected'); 87 | const feedbackError = []; 88 | 89 | if (selectedCity === '') { 90 | feedbackError.push('עיר'); 91 | } 92 | 93 | if (!selectedFeeling.length) { 94 | feedbackError.push('הרגשה'); 95 | } 96 | }); 97 | 98 | if (getQueryParam('feedback') === 'true') { 99 | showFeedbackPopup(); 100 | }; -------------------------------------------------------------------------------- /loader/loader.py: -------------------------------------------------------------------------------- 1 | import json 2 | from datetime import datetime, timedelta 3 | import time 4 | 5 | import requests 6 | 7 | import json 8 | 9 | URL = 'https://services5.arcgis.com/dlrDjz89gx9qyfev/ArcGIS/rest/services/Corona_Exposure_View/FeatureServer/0/query?where=1%3D1&objectIds=&time=&geometry=&geometryType=esriGeometryEnvelope&inSR=&spatialRel=esriSpatialRelIntersects&resultType=none&distance=0.0&units=esriSRUnit_Meter&returnGeodetic=false&outFields=*&returnGeometry=true&featureEncoding=esriDefault&multipatchOption=xyFootprint&maxAllowableOffset=&geometryPrecision=&outSR=&datumTransformation=&applyVCSProjection=false&returnIdsOnly=false&returnUniqueIdsOnly=false&returnCountOnly=false&returnExtentOnly=false&returnQueryGeometry=false&returnDistinctValues=false&cacheHint=false&orderByFields=&groupByFieldsForStatistics=&outStatistics=&having=&resultOffset=&resultRecordCount=&returnZ=false&returnM=false&returnExceededLimitFeatures=true&quantizationParameters=&sqlFormat=none&f=pgeojson&token=' 10 | def load(): 11 | outputData = [] 12 | inputData = [] 13 | resp = requests.get(URL) 14 | inputData = json.loads(resp.content) 15 | 16 | for i, p in enumerate(inputData['features']): 17 | x, y = p['geometry']['coordinates'][0], p['geometry']['coordinates'][1] 18 | point = {"id": p['id'], 19 | "lat": y, 20 | "lon": x, 21 | "label": p['properties']['Place'], 22 | "text": '', 23 | "stayTimes": p['properties']['stayTimes'], 24 | "t_start": (datetime.fromtimestamp(p['properties']['fromTime'] / 1000) - timedelta(hours=2)).isoformat(), 25 | "t_end": (datetime.fromtimestamp(p['properties']['toTime'] / 1000) - timedelta(hours=2)).isoformat(), 26 | "link": ''} 27 | if 'קו אוטובוס' not in (p['properties']['Comments'] or '') and 'אוטובוס' not in (p['properties']['Place'] or ''): 28 | outputData.append(point) 29 | else: 30 | pass 31 | 32 | 33 | print('Total', len(outputData)) 34 | if (len(outputData) > 100): 35 | currentTime = round(time.time()) 36 | outputData = {'points': outputData, 'update_time': currentTime} 37 | 38 | with open('public/data/data-he.json', 'w') as f: 39 | json.dump(outputData, f, indent = 2, ensure_ascii=False) 40 | 41 | with open('public/data/data-ar.json', 'w', encoding='utf-8') as f: 42 | json.dump(outputData, f, ensure_ascii=False, indent=2) 43 | 44 | with open('public/data/data-en.json', 'w', encoding='utf-8') as f: 45 | json.dump(outputData, f, ensure_ascii=False, indent=2) 46 | else: 47 | print("Error: Less than 100 points in data") 48 | 49 | 50 | load() -------------------------------------------------------------------------------- /public/css/search-city.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | width: 100%; 7 | height: 100%; 8 | margin:0; 9 | padding:0; 10 | color: #26262C; 11 | font-family: 'Assistant', sans-serif; 12 | } 13 | 14 | .search-city-container{ 15 | height: 100%; 16 | display: flex; 17 | align-items: center; 18 | text-align: center; 19 | flex-direction: column; 20 | } 21 | 22 | .search-city-divider { 23 | box-shadow: 0 3px 30px #00000029; 24 | border: 2px solid #FFCF4A; 25 | opacity: 1; 26 | border-radius: 30px; 27 | } 28 | 29 | .search-city-title { 30 | font-weight: 700; 31 | font-size: 22px; 32 | padding-top: 50px; 33 | } 34 | 35 | .tt-menu { 36 | width: 100%; 37 | margin: 12px 0; 38 | padding: 8px 0; 39 | background-color: #fff; 40 | border-radius: 5px; 41 | border: 1px solid #9E9EAC; 42 | box-shadow: 0px 3px 30px #00000029; 43 | } 44 | 45 | .typeahead { 46 | width: 300px; 47 | padding: 12px 40px; 48 | background-color: transparent; 49 | font-size: 16px; 50 | color: #575756; 51 | font-weight: 400; 52 | border-radius: 5px; 53 | border: 1px solid #9E9EAC; 54 | box-shadow: 0px 3px 30px #00000029; 55 | direction: rtl; 56 | } 57 | 58 | .typeahead::placeholder { 59 | color: rgba(87, 87, 86, 0.8); 60 | text-transform: uppercase; 61 | letter-spacing: 1.5px; 62 | } 63 | 64 | .typeahead:hover, 65 | .typeahead:focus { 66 | outline: 0; 67 | border-radius: 5px; 68 | border: 1px solid #9E9EAC; 69 | box-shadow: 0px 3px 30px #00000029; 70 | direction: rtl; 71 | } 72 | 73 | .chosen-city-container { 74 | display: none; 75 | background-color: white; 76 | border-radius: 5px; 77 | border: 1px solid #9E9EAC; 78 | box-shadow: 0px 3px 30px #00000029; 79 | margin-bottom: 10%; 80 | padding: 40px; 81 | width: 90%; 82 | margin-left:auto; 83 | margin-right:auto; 84 | 85 | } 86 | 87 | .search-city-subtitle { 88 | font-size: 16px; 89 | margin-top: 10px; 90 | } 91 | 92 | .new{ 93 | background-color:#37C5AB; 94 | padding: 4px 8px; 95 | color: #F9F9F9; 96 | border-radius: 2px; 97 | font-size: 14px; 98 | } 99 | 100 | .chosen-city-title{ 101 | font-weight:700; 102 | font-size: 20px; 103 | } 104 | 105 | .chosen-city-label{ 106 | font-size: 18px; 107 | 108 | } 109 | 110 | .big{ 111 | font-size: 24px; 112 | font-weight: 900; 113 | } 114 | @media(min-width:768px) { 115 | 116 | .chosen-city-title{ 117 | font-weight:700; 118 | font-size: 20px; 119 | } 120 | 121 | .search-city-title { 122 | font-size: 30px; 123 | margin-top: 50px; 124 | } 125 | 126 | .search-city-subtitle { 127 | font-size: 22px; 128 | margin-top: 10px; 129 | } 130 | 131 | 132 | 133 | 134 | } -------------------------------------------------------------------------------- /public/assets/images/side-bar-icons/track.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/images/side-bar-icons/plane.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.15, written by Peter Selinger 2001-2017 9 | 10 | 12 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/assets/images/side-bar-icons/language.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/images/map-icons/delivery-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /public/data/top-cities.js: -------------------------------------------------------------------------------- 1 | const TOP_CITIES = [ 2 | { 3 | city: 'ירושלים', 4 | population: 914559, 5 | sick: 1132, 6 | position: { 7 | lat: 31.768866, 8 | lng: 35.209003 9 | } 10 | }, 11 | { 12 | city: 'בני ברק', 13 | population: 195298, 14 | sick: 1061, 15 | position: { 16 | lat: 32.085007, 17 | lng: 34.835727 18 | } 19 | }, 20 | { 21 | city: 'תל אביב-יפו', 22 | population: 450192, 23 | sick: 337, 24 | position: { 25 | lat: 32.074109, 26 | lng: 34.779736 27 | } 28 | }, 29 | { 30 | city: 'אשקלון', 31 | population: 139032, 32 | sick: 170, 33 | position: { 34 | lat: 31.666405, 35 | lng: 34.566199 36 | } 37 | }, 38 | { 39 | city: 'פתח תקווה', 40 | population: 242478, 41 | sick: 138, 42 | position: { 43 | lat: 32.094276, 44 | lng: 34.884095 45 | } 46 | }, 47 | { 48 | city: 'ראשון לציון', 49 | population: 241010, 50 | sick: 132, 51 | position: { 52 | lat: 31.970607, 53 | lng: 34.784395 54 | } 55 | }, 56 | { 57 | city: 'נתניה', 58 | population: 213971, 59 | sick: 126, 60 | position: { 61 | lat: 32.306610, 62 | lng: 34.855066 63 | } 64 | }, 65 | { 66 | city: 'אלעד', 67 | population: 46760, 68 | sick: 120, 69 | position: { 70 | lat: 32.049945, 71 | lng: 34.955449 72 | } 73 | }, 74 | { 75 | city: 'בית שמש', 76 | population: 120812, 77 | sick: 118, 78 | position: { 79 | lat: 31.721742, 80 | lng: 34.982266 81 | } 82 | }, 83 | { 84 | city: 'באר שבע', 85 | population: 196755, 86 | sick: 112, 87 | position: { 88 | lat: 31.246615, 89 | lng: 34.786938 90 | } 91 | }, 92 | { 93 | city: 'אשדוד', 94 | population: 225073, 95 | sick: 111, 96 | position: { 97 | lat: 31.799184, 98 | lng: 34.645279 99 | } 100 | }, 101 | { 102 | city: 'רמת גן', 103 | population: 143370, 104 | sick: 106, 105 | position: { 106 | lat: 32.069431, 107 | lng: 34.826147 108 | } 109 | }, 110 | { 111 | city: 'חולון', 112 | population: 185741, 113 | sick: 104, 114 | position: { 115 | lat: 32.014908, 116 | lng: 34.787732 117 | } 118 | }, 119 | { 120 | city: 'בת ים', 121 | population: 130523, 122 | sick: 102, 123 | position: { 124 | lat: 32.014997, 125 | lng: 34.747402 126 | } 127 | }, 128 | { 129 | city: 'חיפה', 130 | population: 272584, 131 | sick: 96, 132 | position: { 133 | lat: 32.799762, 134 | lng: 34.995490 135 | } 136 | }, 137 | { 138 | city: 'מודיעין-עילית', 139 | population: 73808, 140 | sick: 91, 141 | position: { 142 | lat: 31.932772, 143 | lng: 35.042841 144 | } 145 | }, 146 | { 147 | city: 'מודיעין-מכבים-רעות', 148 | population: 87108, 149 | sick: 91, 150 | position: { 151 | lat: 31.894687, 152 | lng: 35.009012 153 | } 154 | }, 155 | { 156 | city: 'טבריה', 157 | population: 44353, 158 | sick: 83, 159 | position: { 160 | lat: 32.785078, 161 | lng: 35.528878 162 | } 163 | }, 164 | { 165 | city: 'מגדל העמק', 166 | population: 26058, 167 | sick: 82, 168 | position: { 169 | lat: 32.679595, 170 | lng: 35.244905 171 | } 172 | } 173 | ]; 174 | -------------------------------------------------------------------------------- /data-gen/extract.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import json 3 | import time 4 | import sys 5 | from datetime import date, datetime, timedelta, timezone 6 | import googlemaps 7 | import os 8 | 9 | 10 | def extractDate(): 11 | dates = {} 12 | keys = ['0%d/03', '%d/03', '0%d/3', '%d/3'] 13 | keys += [x + '/20' for x in keys] + [x + '/2020' for x in keys] 14 | keys += [x.replace('/', '.') for x in keys] 15 | 16 | for day in range(1, 32): 17 | for key in keys: 18 | formattedDate = '%d/03/2020' % day 19 | dates[key % day] = formattedDate 20 | 21 | # print('%d/03' % day) 22 | contents = [] 23 | with open('data-gen/data.txt', newline='') as f: 24 | contents = f.read() 25 | for line in contents.splitlines(): 26 | matches = set() 27 | for key in dates.keys(): 28 | if (key in line): 29 | matches.add(dates[key]) 30 | filteredMatches = [] 31 | for match in matches: 32 | if len([m for m in matches if match in m]) == 1: 33 | filteredMatches.append(match) 34 | if len(filteredMatches) == 1: 35 | formattedDate = '0' + filteredMatches[0] 36 | formattedDate = formattedDate[-10:] 37 | print(formattedDate) 38 | else: 39 | print() 40 | 41 | def extractTime(): 42 | times = {} 43 | keys = ['%d:%d', '0%d:%d'] 44 | 45 | for key in keys: 46 | for hour in range(0, 24): 47 | for minute in range(0, 60): 48 | 49 | if (minute == 0): 50 | formattedTime = '%d:00' % (hour) 51 | times[key % (hour, 0) + '0'] = formattedTime 52 | else: 53 | formattedTime = '%d:%d' % (hour, minute) 54 | times[key % (hour, minute)] = formattedTime 55 | 56 | # print('%d/03' % day) 57 | contents = [] 58 | with open('data-gen/data.txt', newline='') as f: 59 | contents = f.read() 60 | for line in contents.splitlines(): 61 | matches = set() 62 | for key in sorted(times.keys()): 63 | if (key in line): 64 | matches.add(times[key]) 65 | filteredMatches = [] 66 | for match in sorted(matches): 67 | if len([m for m in matches if match in m]) == 1: 68 | formattedTime = '0' + match 69 | formattedTime = formattedTime[-5:] 70 | filteredMatches.append(formattedTime) 71 | 72 | 73 | if (len(filteredMatches) == 1): 74 | print(filteredMatches[0]) 75 | elif (len(filteredMatches) == 2): 76 | t0 = filteredMatches[0] 77 | t1 = filteredMatches[1] 78 | h0 = time.strptime(t0, "%H:%M").tm_hour 79 | h1 = time.strptime(t1, "%H:%M").tm_hour 80 | if (h1 > h0 | h1 < 4): 81 | print('%s\t%s' % (t1, t0)) 82 | else: 83 | print('%s\t%s' % (t0, t1)) 84 | else: 85 | print('00:00\t23:59') 86 | 87 | 88 | def extractLocation(): 89 | gmaps = googlemaps.Client(key=os.environ['TOKEN']) 90 | 91 | with open('data-gen/locations.txt', newline='') as f: 92 | contents = f.read() 93 | for line in contents.splitlines(): 94 | geocode_result = gmaps.geocode(line) 95 | if geocode_result: 96 | location = geocode_result[0]['geometry']['location'] 97 | print('%s, %s' % (location['lat'], location['lng'])) 98 | else: 99 | print() 100 | 101 | 102 | 103 | def fixLoc(): 104 | contents = [] 105 | with open('data-gen/data.txt', newline='') as f: 106 | contents = f.read() 107 | for line in contents.splitlines(): 108 | split = line.split(", ") 109 | if len(split) == 2: 110 | print(split[1].strip() + ", " + split[0].strip()) 111 | else: 112 | print(line) 113 | 114 | 115 | extractTime() 116 | -------------------------------------------------------------------------------- /public/assets/images/side-bar-icons/city.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /public/js/flights.js: -------------------------------------------------------------------------------- 1 | let flights = []; 2 | 3 | const init = () => { 4 | initLanguage(); 5 | $("#flight-list").attr("dir", langDirection); 6 | $("#search-input-dekstop").attr("dir", langDirection); 7 | $("#search-input-dekstop").attr("placeholder", i18n('flightSearchPlaceholder')); 8 | 9 | fetch('/data/flightsData.json') 10 | .then((response) => { 11 | return response.json(); 12 | }) 13 | .then((data) => { 14 | flights = data; 15 | }).then(() => { 16 | renderList(flights); 17 | }); 18 | }; 19 | 20 | const createList = (flightsList) => { 21 | let strHtml = '
'; 22 | flightsList.reverse().forEach(flight => { 23 | let card = `
24 |
25 |
26 |
${flight.flight_number} ${flight.airline}
27 |
${i18n('patientNumberShort')}: ${flight.patient_number}
28 |
29 |
30 |
31 | flight_takeoff 32 |
${i18n('departure')}: ${flight["departure from"]}
33 |
34 |
${flight["departure day"]}
35 |
36 |
37 |
38 | flight_land 39 |
${i18n('arrival')}: ${flight.destination}
40 |
41 |
${flight["arrival day"]}
42 |
43 | `; 44 | if (flight.health_gov_link) { 45 | card += `${i18n('linkToTheMinistryOfHealthPublication')}`; 46 | } 47 | 48 | card += `
49 |
`; 50 | strHtml += card; 51 | }); 52 | strHtml += '
'; 53 | return strHtml; 54 | }; 55 | 56 | // const handleSearchMobile = () => { 57 | // const input = document.getElementById('search-input'); 58 | // const value = input.value.toUpperCase(); 59 | // let filterdFlights = filterFlights(flights, value); 60 | // renderList(filterdFlights); 61 | // } 62 | 63 | // eslint-disable-next-line no-unused-vars 64 | const handleSearchDesktop = () => { 65 | // Declare variables 66 | const input = document.getElementById('search-input-dekstop'); 67 | const value = input.value.toUpperCase(); 68 | let filterdFlights = filterFlights(flights, value); 69 | renderList(filterdFlights); 70 | }; 71 | 72 | const filterFlights = (flights, value) => { 73 | value = value.trim(); 74 | let filterdFlights = flights.filter((flight) => { 75 | return flight.flight_number.toString().includes(value) || 76 | (flight.departure_from && flight.departure_from.toString().includes(value)) || 77 | (flight.destination && flight.destination.toString().includes(value)) || 78 | (flight["departure day"] && flight["departure day"].toString().includes(value)) || 79 | (flight["arrival day"] && flight["arrival day"].toString().includes(value)) || 80 | (flight["patient_number"] && flight["patient_number"].toString().includes(value)); 81 | }); 82 | return filterdFlights; 83 | }; 84 | 85 | const renderList = (flights) => { 86 | let flightListEl = document.querySelector('.flight-list'); 87 | let list = createList(flights); 88 | flightListEl.innerHTML = list; 89 | }; 90 | 91 | init(); 92 | -------------------------------------------------------------------------------- /public/js/firestore.js: -------------------------------------------------------------------------------- 1 | 2 | firebase.initializeApp({ 3 | apiKey: 'AIzaSyB1clMUVSN58mtwRl4UB7cK2ayscW9sHIM', 4 | authDomain: 'coronavirus-il.firebaseapp.com', 5 | projectId: 'coronavirus-il' 6 | }); 7 | 8 | const db = firebase.firestore(); 9 | 10 | const getSickPeopleData = (callback = undefined) => { 11 | db.collection("sick-pop-up").doc('data').get().then((doc) => { 12 | const {death, recover, sick, timestamp} = doc.data(); 13 | if (callback) { 14 | const updateTime = convertTimestampToDateAndTime(timestamp); 15 | 16 | callback(updateTime, sick, recover, death); 17 | } 18 | }); 19 | }; 20 | 21 | const setSickPeopleDate = (sickNumber, recoverNumber, deathNumber) => { 22 | db.collection("sick-pop-up").doc("data").set({ 23 | death: deathNumber, 24 | recover: recoverNumber, 25 | sick: sickNumber, 26 | timestamp: (new Date()).getTime() 27 | }) 28 | .then(() => { 29 | $('#submit-success').css('display', 'block'); 30 | $('#submit-failed').css('display', 'none'); 31 | }) 32 | .catch((error) => { 33 | $('#submit-success').css('display', 'none'); 34 | $('#submit-failed').css('display', 'block'); 35 | console.error(error); 36 | }); 37 | }; 38 | 39 | const setCitiesData = (data) => { 40 | db.collection("cities").doc("data").set({ 41 | cities: data, 42 | lastUpdate: (new Date()).getTime() 43 | }) 44 | .then(() => { 45 | $('#submit-cities-success').css('display', 'block'); 46 | $('#submit-cities-failed').css('display', 'none'); 47 | }) 48 | .catch((error) => { 49 | $('#submit-cities-success').css('display', 'none'); 50 | $('#submit-cities-failed').css('display', 'block'); 51 | console.error(error); 52 | }); 53 | }; 54 | 55 | const getCitiesData = async (callback = undefined) => { 56 | let data = {}; 57 | await db.collection("cities").doc("data").get() 58 | .then(async (doc) => { 59 | if (callback) { 60 | const isolations = await getIsolationsByCity(); 61 | const cities = doc.data().cities; 62 | const citiesWithIsolations = (isolations && isolations.length) ? migrateNumberOfIsolationsPerCityFromMoh(cities, isolations) : cities; 63 | callback(citiesWithIsolations); 64 | } 65 | data = doc.data(); 66 | }) 67 | .catch((error) => { 68 | console.error(error); 69 | }); 70 | return data; 71 | }; 72 | 73 | const migrateNumberOfIsolationsPerCityFromMoh = (cities, isolations) => { 74 | const idsOnMoh = cities.map((city) => city.id_on_moh); 75 | const isolationsFromMoh = isolations.reduce((acc, el) => { 76 | const {attributes : {Municipality, Isolations}} = el; 77 | if (idsOnMoh.includes(Municipality)) { 78 | acc[Municipality] = Isolations; 79 | } 80 | return acc; 81 | }, {}); 82 | 83 | return cities.map((city) => { 84 | city.iso = isolationsFromMoh[city.id_on_moh]; 85 | return city; 86 | }); 87 | }; 88 | 89 | 90 | const setAllCitiesFromExcelData = (data) => { 91 | 92 | if (data.length === 0) { 93 | return; 94 | } 95 | 96 | db.collection("cities").doc("all-cities").set({ 97 | cities: data, 98 | lastUpdate: (new Date()).getTime() 99 | }) 100 | .then(() => { 101 | $('#submit-excel-success').css('display', 'block'); 102 | $('#submit-excel-failed').css('display', 'none'); 103 | }) 104 | .catch((error) => { 105 | $('#submit-excel-failed').css('display', 'block'); 106 | $('#submit-excel-success').css('display', 'none'); 107 | }); 108 | }; 109 | 110 | const getAllCitiesData = async () => { 111 | let data = {}; 112 | await db.collection("cities").doc("all-cities").get() 113 | .then(async (doc) => { 114 | data = doc.data(); 115 | }) 116 | .catch((error) => { 117 | console.error(error); 118 | }); 119 | return data; 120 | }; 121 | 122 | -------------------------------------------------------------------------------- /public/assets/images/map-icons/filters.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /public/assets/images/map-icons/help.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /public/search-city/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | חיפוש חולים לפי עיר 8 | 12 | 13 | 14 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
44 |
45 |

חיפוש מידע על חולי קורונה לפי עיר

46 |
47 |

חפשו את העיר שלכם וגלו את נתוני קורונה העדכניים ביותר לגביהם

48 |
49 |
50 | עודכן לאחרונה: 51 | 52 |
53 |
54 |
55 | 58 |
59 |
60 |
61 |
62 |
63 |
64 |

65 |
66 |
67 |
68 |
69 |
70 | 71 | 72 |
73 |
74 | 75 |

אוכולוסיה נכון ל-2018

76 |

77 |
78 |
79 |
80 |
81 |

חולים מאומתים

82 |

83 |
84 |
85 |
86 |
87 |

מספר מחלימים

88 |

89 |
90 |
91 |
92 |
93 | 94 |

תחלואה ל-100,000

95 |

96 | 97 |
98 |
99 | 100 |
101 |
102 | 103 | 104 |
105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /public/css/feedback.css: -------------------------------------------------------------------------------- 1 | #report-pop-up { 2 | display: none; 3 | } 4 | 5 | #close-feedback-popup { 6 | font-size: 2.5rem; 7 | line-height: 2; 8 | z-index: 3000; 9 | } 10 | 11 | .q-button{ 12 | border-radius: 30px; 13 | padding-top: 10px; 14 | padding-bottom: 10px; 15 | padding-right: 30px; 16 | padding-left: 30px; 17 | font-size: 16px!important; 18 | color: #ffffff!important; 19 | background-color: #18AF93; 20 | box-shadow: 0px 3px 30px #00000029; 21 | font-weight: 600; 22 | 23 | font-size: 14px; 24 | cursor: pointer; 25 | transition: all .35s; 26 | 27 | } 28 | 29 | .q-button:hover{ 30 | transform: scale(0.98); 31 | text-decoration: none!important; 32 | } 33 | 34 | .city-container { 35 | padding-top: 15px; 36 | padding-bottom: 15px; 37 | box-shadow: 0 3px 30px #00000029; 38 | background-color: white; 39 | border: none; 40 | width: 100%; 41 | color:#475172; 42 | font-size: 20px; 43 | border-radius: 10px; 44 | transition: all .2s ease-in-out; 45 | } 46 | 47 | .select2-container{ width: 100% !important; } 48 | 49 | [class^='select2'] { 50 | border-radius: 0 !important; 51 | } 52 | 53 | .select-city { 54 | width: 100%; 55 | } 56 | 57 | .select2-container--default .select2-selection--single { 58 | border: none !important; 59 | } 60 | 61 | select.select2-hidden-accessible { visibility: hidden; } 62 | 63 | .select2-selection__arrow { 64 | display: none; 65 | } 66 | 67 | .select2-results__options { 68 | -webkit-overflow-scrolling: touch; 69 | } 70 | 71 | .error-filling-feedback { 72 | display: none; 73 | } 74 | 75 | .network-error-filling-feedback { 76 | display: none; 77 | } 78 | 79 | .form-success-message { 80 | display: none; 81 | } 82 | 83 | #success-filling-feedback { 84 | font-weight: 700; 85 | font-size: larger; 86 | } 87 | 88 | #success-filling-feedback-refill { 89 | font-weight: 600; 90 | } 91 | 92 | .disable-double-click { 93 | pointer-events: none; 94 | cursor: default; 95 | } 96 | 97 | #feedback-button-spinner { 98 | display: none; 99 | } 100 | 101 | 102 | .report-btn{ 103 | border-radius: 30px; 104 | padding: 10px 15px; 105 | box-shadow: 0px 3px 30px #00000029; 106 | font-size: 12px; 107 | cursor: pointer; 108 | background-color: #18AF93; 109 | transition: all .35s; 110 | left: 0; 111 | top: 0; 112 | position: absolute; 113 | margin-top: 66px; 114 | margin-left: 10px; 115 | } 116 | 117 | .report-btn:hover{ 118 | transform: scale(0.98); 119 | text-decoration: none!important; 120 | } 121 | 122 | .report-text{ 123 | color: #26262C; 124 | } 125 | 126 | .report-row{ 127 | width: 90%; 128 | margin: auto; 129 | } 130 | 131 | ::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */ 132 | color: #26262C; 133 | opacity: 1; /* Firefox */ 134 | } 135 | 136 | .hr-titels-tau{ 137 | border: 2px solid #18AF93; 138 | opacity: 1; 139 | border-radius: 30px; 140 | } 141 | 142 | .report-row{ 143 | width: 100%; 144 | margin: auto; 145 | } 146 | 147 | .jobs-row{ 148 | width: 100%; 149 | margin: auto; 150 | } 151 | 152 | 153 | 154 | .feeling-box{ 155 | display: flex; 156 | flex-direction:column ; 157 | justify-content: center; 158 | align-items: center; 159 | width:100%; 160 | height: auto; 161 | padding: 25px 0px 25px 0px; 162 | margin: auto; 163 | border-radius: 10px; 164 | box-shadow: 0px 3px 30px #00000029; 165 | background-color: white; 166 | transition-duration: 0.1s; 167 | } 168 | 169 | @media(min-width:768px) { 170 | 171 | .report-btn{ 172 | border-radius: 30px; 173 | padding: 10px 20px; 174 | box-shadow: 0px 3px 30px #00000029; 175 | font-size: 14px; 176 | cursor: pointer; 177 | background-color: #18AF93; 178 | transition: all .35s; 179 | left: 0; 180 | top: 0; 181 | position: absolute; 182 | margin-top: 66px; 183 | margin-left: 10px; 184 | } 185 | 186 | .report-btn:hover{ 187 | transform: scale(0.98); 188 | text-decoration: none!important; 189 | } 190 | 191 | .report-row{ 192 | max-width: 1100px; 193 | } 194 | 195 | .jobs-row{ 196 | max-width: 1100px; 197 | } 198 | 199 | .feeling-box{ 200 | display: flex; 201 | flex-direction:column ; 202 | justify-content: center; 203 | align-items: center; 204 | max-width:340px; 205 | min-height: 300px; 206 | padding: 25px 0px 25px 0px; 207 | margin: auto; 208 | border-radius: 10px; 209 | box-shadow: 0px 3px 30px #00000029; 210 | background-color: white; 211 | transition-duration: 0.1s; 212 | } 213 | 214 | .input { 215 | font-size: 20px; 216 | } 217 | 218 | 219 | 220 | .report-text{ 221 | font-size:20px; 222 | width: 80%; 223 | margin: auto; 224 | } 225 | 226 | } 227 | 228 | 229 | 230 | 231 | 232 | -------------------------------------------------------------------------------- /public/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | https://israelcoronamap.co.il/ 5 | 2020-03-22 6 | daily 7 | 1.0 8 | 9 | https://israelcoronamap.co.il/assets/images/icm.svg 10 | icon 11 | 12 | 13 | https://israelcoronamap.co.il/assets/images/alarm.svg 14 | icon 15 | 16 | 17 | https://israelcoronamap.co.il/assets/images/map-icons/plus.svg 18 | icon 19 | 20 | 21 | https://israelcoronamap.co.il/assets/images/map-icons/min.svg 22 | icon 23 | 24 | 25 | https://israelcoronamap.co.il/assets/images/map-icons/filters.svg 26 | icon 27 | 28 | 29 | https://israelcoronamap.co.il/assets/images/map-icons/gps.svg 30 | zoom-to-location 31 | 32 | 33 | https://israelcoronamap.co.il/assets/images/map-icons/mapReader-he.svg 34 | map-reader 35 | 36 | 37 | https://israelcoronamap.co.il/assets/images/side-bar-icons/plane.svg 38 | icon 39 | 40 | 41 | https://israelcoronamap.co.il/assets/images/side-bar-icons/coding.svg 42 | icon 43 | 44 | 45 | https://israelcoronamap.co.il/assets/images/side-bar-icons/language.svg 46 | icon 47 | 48 | 49 | https://israelcoronamap.co.il/assets/images/side-bar-icons/info.svg 50 | icon 51 | 52 | 53 | https://israelcoronamap.co.il/assets/images/side-bar-icons/email.svg 54 | icon 55 | 56 | 57 | https://israelcoronamap.co.il/assets/images/phone.svg 58 | icon 59 | 60 | 61 | https://israelcoronamap.co.il/assets/images/x-icon.svg 62 | x-icon 63 | 64 | 65 | 66 | https://israelcoronamap.co.il/info/ 67 | 2020-03-22 68 | daily 69 | 0.8 70 | 71 | https://israelcoronamap.co.il/assets/images/icm.svg 72 | icon 73 | 74 | 75 | 76 | https://israelcoronamap.co.il/flights/ 77 | 2020-03-22 78 | daily 79 | 0.8 80 | 81 | https://israelcoronamap.co.il/assets/images/icm.svg 82 | icon 83 | 84 | 85 | https://israelcoronamap.co.il/assets/images/side-bar-icons/plane.svg 86 | icon 87 | 88 | 89 | https://israelcoronamap.co.il/assets/images/side-bar-icons/coding.svg 90 | icon 91 | 92 | 93 | https://israelcoronamap.co.il/assets/images/side-bar-icons/info.svg 94 | icon 95 | 96 | 97 | https://israelcoronamap.co.il/assets/images/side-bar-icons/email.svg 98 | icon 99 | 100 | 101 | https://israelcoronamap.co.il/assets/images/phone.svg 102 | icon 103 | 104 | 105 | https://israelcoronamap.co.il/assets/images/x-icon.svg 106 | x-icon 107 | 108 | 109 | -------------------------------------------------------------------------------- /public/js/info.js: -------------------------------------------------------------------------------- 1 | 2 | $( document ).ready(() => { 3 | 4 | // windows navbar scroll 5 | $(window).scroll(() => { 6 | let scroll = $(window).scrollTop(); 7 | if (scroll > 50) { 8 | $(".wonder-nav").addClass("wonder-nav-scroll"); 9 | $('.top-bar-icon-desk').attr('src', 'assets/images/logo/w-white.svg'); 10 | 11 | $(".nav-link").addClass("nav-link-scroll"); 12 | 13 | $(".you-switcher").addClass("you-switcher-scroll"); 14 | $(".you-switcher-line").addClass("you-switcher-line-scroll"); 15 | 16 | } 17 | else{ 18 | $(".wonder-nav").removeClass("wonder-nav-scroll"); 19 | 20 | $(".nav-link").removeClass("nav-link-scroll"); 21 | $('.top-bar-icon-desk').attr('src', 'assets/images/logo/w-grey.svg'); 22 | 23 | $(".you-switcher").removeClass("you-switcher-scroll"); 24 | $(".you-switcher-line").removeClass("you-switcher-line-scroll"); 25 | 26 | } 27 | }); 28 | 29 | // page smooth scroll on link click 30 | $('a[href^="#"]').on('click', (event) => { 31 | 32 | let target = $(this.getAttribute('href')); 33 | 34 | if( target.length ) { 35 | event.preventDefault(); 36 | $('html, body').stop().animate({ 37 | scrollTop: target.offset().top 38 | }, 500); 39 | } 40 | 41 | }); 42 | 43 | // side-bar menu open 44 | $('.hamburger').click( () => { 45 | $( this ).toggleClass( "is-active" ); 46 | $('.side-bar').fadeToggle(50); 47 | }); 48 | 49 | // side-bar menu close 50 | $('.side-bar-link').click ( () => { 51 | 52 | $('.hamburger').removeClass( "is-active" ); 53 | $('.side-bar').fadeToggle(50); 54 | 55 | }); 56 | 57 | // open service pop-ups on click // 58 | 59 | $('.terms-of-use-pop-up-click').click ( () => { 60 | 61 | $('#terms-of-use-pop-up').fadeIn("fast"); 62 | 63 | }); 64 | 65 | 66 | // close popups on X click // 67 | 68 | $('.close-pop-up').click ( () => { 69 | $('.pop-up-area').fadeOut('fast'); 70 | $('.overlay').removeClass('blur'); 71 | }); 72 | 73 | }); 74 | 75 | /*! track-focus v 1.0.0 | Author: Jeremy Fields [jeremy.fields@vget.com], 2015 | License: MIT */ 76 | // inspired by: http://irama.org/pkg/keyboard-focus-0.3/jquery.keyboard-focus.js 77 | 78 | ((body) => { 79 | 80 | let usingMouse; 81 | 82 | let preFocus = (event) => { 83 | usingMouse = (event.type === 'mousedown'); 84 | }; 85 | 86 | let addFocus = (event) => { 87 | if (usingMouse) 88 | event.target.classList.add('focus--mouse'); 89 | }; 90 | 91 | let removeFocus = (event) => { 92 | event.target.classList.remove('focus--mouse'); 93 | }; 94 | 95 | let bindEvents = () => { 96 | body.addEventListener('keydown', preFocus); 97 | body.addEventListener('mousedown', preFocus); 98 | body.addEventListener('focusin', addFocus); 99 | body.addEventListener('focusout', removeFocus); 100 | }; 101 | 102 | bindEvents(); 103 | 104 | })(document.body); 105 | 106 | 107 | /** 108 | * Minified by jsDelivr using UglifyJS v3.4.0. 109 | * Original file: /npm/js-cookie@2.2.0/src/js.cookie.js 110 | * 111 | * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files 112 | */ 113 | // eslint-disable-next-line no-useless-escape, no-undef, no-empty, func-style, no-var, space-infix-ops 114 | !function(e){let n=!1;if("function"==typeof define&&define.amd&&(define(e),n=!0),"object"==typeof exports&&(module.exports=e(),n=!0),!n){let o=window.Cookies,t=window.Cookies=e();t.noConflict=function(){return window.Cookies=o,t;};}}(function(){function g(){for(var e=0,n={};e { 4 | $('#sick-pop-up-admin-container').on('submit', (e) => { 5 | e.preventDefault(); 6 | const sickNumber = $('#sickPeopleInput').val(); 7 | const recoveredNumber = $('#recoveredPeopleInput').val(); 8 | const deathNumber = $('#deathPeopleInput').val(); 9 | let user = firebase.auth().currentUser; 10 | if (!user) { 11 | const provider = new firebase.auth.GoogleAuthProvider(); 12 | firebase.auth().signInWithRedirect(provider).then(() => { 13 | setSickPeopleDate(sickNumber, recoveredNumber, deathNumber); 14 | }); 15 | } else { 16 | console.log(`writing to db as ${user.email}`); 17 | setSickPeopleDate(sickNumber, recoveredNumber, deathNumber); 18 | } 19 | }); 20 | }); 21 | 22 | const addLabelsToInputsForCurrentStatus = (updateTime, sick, recover, death) => { 23 | $('#current-death-number').text(death); 24 | $('#current-recovered-number').text(recover); 25 | $('#current-sick-number').text(sick); 26 | $('#latest-time-update').text(updateTime); 27 | } 28 | 29 | getSickPeopleData(addLabelsToInputsForCurrentStatus); 30 | 31 | const initAdminCitiesTable = (cities, table) => { 32 | table.bootstrapTable({ 33 | data: cities, 34 | cellInputEnabled: true, 35 | onClickRow: (_row, element) => { 36 | element.css('background-color', '#cbffc0'); 37 | }, 38 | columns: [ 39 | { 40 | field: 'city', 41 | title: 'שם העיר', 42 | cellInputEnabled: false, 43 | cellInputType: 'text', 44 | }, 45 | { 46 | field: 'sick', 47 | title: 'מספר החולים', 48 | cellInputEnabled: true, 49 | cellInputType: 'text', 50 | }, 51 | { 52 | field: 'population', 53 | title: 'אוכולוסיה לפי 2018', 54 | cellInputType: 'text', 55 | } 56 | ] 57 | }); 58 | }; 59 | 60 | $(document).ready(async () => { 61 | const table = $('#cities-table'); 62 | const {cities, lastUpdate} = await getCitiesData(); 63 | $('#latest-time-cities-update').text(convertTimestampToDateAndTime(lastUpdate)); 64 | const citiesWithFormattedPopulation = cities.map((city) => { 65 | return Object.assign(city, {population: convertNumberToStringWithCommas(city.population)}); 66 | }); 67 | initAdminCitiesTable(citiesWithFormattedPopulation, table); 68 | 69 | $('#update-cities-data').click(() => { 70 | const dataFromTable = table.bootstrapTable('getData', {unfiltered: true}); 71 | const updatedCitiesValues = cities.map((city, index) => { 72 | return Object.assign(city, {sick: parseInt(dataFromTable[index].sick)}); 73 | }) 74 | 75 | let user = firebase.auth().currentUser; 76 | if (!user) { 77 | const provider = new firebase.auth.GoogleAuthProvider(); 78 | firebase.auth().signInWithRedirect(provider).then(() => { 79 | setCitiesData(updatedCitiesValues); 80 | }); 81 | } else { 82 | console.log(`writing to db as ${user.email}`); 83 | setCitiesData(updatedCitiesValues); 84 | } 85 | }); 86 | 87 | $('#sign-in-with-google-btn').click(signInWithGoogle); 88 | 89 | $('#sign-out-from-google-btn').click(() => firebase.auth().signOut()); 90 | 91 | $('#upload-excel-button').click(() => { 92 | setAllCitiesFromExcelData(citiesParsed); 93 | }); 94 | 95 | $('#inputGroupFile01').change((evt) => { 96 | const selectedFile = evt.target.files[0]; 97 | $('#excel-file-name').text(selectedFile.name); 98 | $('.excel-file-name').css('display', 'block'); 99 | const reader = new FileReader(); 100 | reader.onload = function(event) { 101 | const data = event.target.result; 102 | const workbook = XLSX.read(data, { 103 | type: 'binary' 104 | }); 105 | workbook.SheetNames.forEach(function(sheetName) { 106 | 107 | const XL_row_object = XLSX.utils.sheet_to_row_object_array(workbook.Sheets[sheetName]); 108 | const cities_array = JSON.stringify(XL_row_object); 109 | parseCities(XL_row_object); 110 | }); 111 | }; 112 | 113 | reader.onerror = function(event) { 114 | console.error("File could not be read! Code " + event.target.error.code); 115 | }; 116 | 117 | reader.readAsBinaryString(selectedFile); 118 | }); 119 | 120 | }); 121 | 122 | const signInWithGoogle = () => { 123 | const provider = new firebase.auth.GoogleAuthProvider(); 124 | firebase.auth().signInWithRedirect(provider); 125 | }; 126 | 127 | firebase.auth().onAuthStateChanged(function(user) { 128 | if (user) { 129 | $('#user-email').text(user.email); 130 | } else { 131 | $('.admin-container').css('display', 'none'); 132 | $('.login-container').css('display', 'flex'); 133 | } 134 | }); 135 | 136 | const parseCities = (cities) => { 137 | citiesParsed = cities.map((city) => { 138 | const cityName = city['עיר']; 139 | const population = city['אוכלוסיה נכון ל-2018']; 140 | const howManyPeopleTested = city['מספר נבדקים עד כה']; 141 | const infectedPeople = city['חולים מאומתים שהתגלו עד כה']; 142 | const recovered = city['מספר מחלימים']; 143 | const increaseInPercentageInTheLastThreeDays = city['שיעור הגידול של חולים מאומתים ב-3 ימים אחרונים']; 144 | const increaseInPercentageInTheLastWeek = city['שיעור הגידול בשבוע האחרון של חולים מאומתים']; 145 | const sicknessPercentageForOneHundredThousand = city['שיעור תחלואה בפועל** ל-100,000']; 146 | return { 147 | cityName, 148 | population, 149 | howManyPeopleTested, 150 | infectedPeople, 151 | recovered, 152 | increaseInPercentageInTheLastThreeDays, 153 | increaseInPercentageInTheLastWeek, 154 | sicknessPercentageForOneHundredThousand 155 | }; 156 | }); 157 | }; 158 | -------------------------------------------------------------------------------- /public/css/map.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | margin: 0; 4 | padding: 0; 5 | overflow: hidden 6 | } 7 | 8 | .city-card { 9 | margin-top: 20px; 10 | margin-right: 5px; 11 | margin-left: 5px; 12 | } 13 | 14 | .gm-style > div:nth-child(3) { 15 | left: initial !important; 16 | right: 0 !important; 17 | } 18 | 19 | .gm-style-cc { display:none; } 20 | 21 | #map { 22 | height: 100%; 23 | width: 100vw; 24 | } 25 | 26 | #map-wrapper { 27 | height: calc(100% - 41px); 28 | } 29 | 30 | .footer-container { 31 | display:none; 32 | } 33 | 34 | .infowindow { 35 | text-align: right; 36 | padding: 10px; 37 | font-family: 'Assistant', sans-serif; 38 | } 39 | 40 | .app-header{ 41 | padding: 1vh; 42 | text-align: center; 43 | color: white; 44 | background: black; 45 | font-size: 30px; 46 | } 47 | 48 | .save-button-box{ 49 | display: flex; 50 | justify-content: center; 51 | } 52 | 53 | .save-button { 54 | width: 220px; 55 | font-size: 20px; 56 | font-weight: 700; 57 | outline: none; 58 | border: none; 59 | border-radius: 25px; 60 | padding: 10px; 61 | color: #6c6a6a; 62 | background: white; 63 | box-shadow: 3px 3px 10px -2px; 64 | } 65 | .container-1{ 66 | height: 100vh; 67 | } 68 | 69 | .container-2{ 70 | height: 100vh; 71 | width: 100%; 72 | } 73 | 74 | .app-description{ 75 | color: white; 76 | max-width: 500px; 77 | direction: rtl; 78 | margin: 0px auto 30px; 79 | text-align: center; 80 | font-size: 28px; 81 | font-weight: initial; 82 | } 83 | 84 | .user-location-box { 85 | font-size: 16px; 86 | text-align: center; 87 | min-width: 100px; 88 | margin: 6px; 89 | } 90 | 91 | .app-footer{ 92 | width: 25vw; 93 | display: flex; 94 | justify-content: space-between; 95 | flex-direction: column; 96 | position: absolute; 97 | z-index: 10; 98 | right: 0; 99 | top: 16vh; 100 | } 101 | 102 | .map-filters { 103 | display: flex; 104 | justify-content: flex-end; 105 | } 106 | 107 | .filter-button { 108 | background-color: white; 109 | width: 200px; 110 | font-weight: 600; 111 | margin-left: 10px; 112 | margin-right: 5px; 113 | } 114 | 115 | .filter-button.selected { 116 | background-color: #37C5AB; 117 | color: white; 118 | } 119 | 120 | .info-label { 121 | text-decoration: underline; 122 | font-size: 17px; 123 | font-weight: 700; 124 | padding-bottom: 5px; 125 | } 126 | 127 | .info-description { 128 | font-size: 16px; 129 | font-weight: 500; 130 | } 131 | 132 | .pub_date { 133 | font-size: 14px; 134 | } 135 | 136 | .quarantine-time { 137 | font-size: 14px; 138 | } 139 | 140 | .green-text { 141 | color: green; 142 | } 143 | 144 | .red-text { 145 | color: red; 146 | } 147 | 148 | .map-reader { 149 | z-index: 1; 150 | position: absolute; 151 | left: 0; 152 | bottom: 0; 153 | box-shadow: 0 3px 30px #00000029; 154 | margin-left: 7px; 155 | margin-bottom: 50px; 156 | } 157 | 158 | .left-section-buttons { 159 | position: absolute; 160 | left: 0; 161 | bottom: 0; 162 | width: 100%; 163 | margin-bottom: 52px; 164 | } 165 | 166 | .floating-buttons { 167 | display: flex; 168 | flex-direction: column; 169 | justify-content: space-between; 170 | align-items: flex-end; 171 | } 172 | 173 | .floating-button { 174 | cursor: pointer; 175 | margin: 5px 0 5px 10px; 176 | } 177 | 178 | .floating-button > img { 179 | width: 45px; 180 | filter: drop-shadow(0px 3px 3px rgba(0, 0, 0, 0.4)); 181 | } 182 | 183 | .zoom-control-buttons { 184 | display: flex; 185 | flex-direction: column; 186 | align-items: flex-end; 187 | } 188 | 189 | .zoom-control-buttons > img { 190 | width: 38px; 191 | filter: drop-shadow(0px 3px 3px rgba(0, 0, 0, 0.4)); 192 | } 193 | 194 | #zoom-to-location-icon { 195 | display: none; 196 | } 197 | 198 | #sick-people-city-container { 199 | display: flex; 200 | justify-content: space-between; 201 | flex-direction: column; 202 | height: 110px; 203 | } 204 | 205 | .sick-city-title { 206 | font-size: 20px; 207 | font-weight: 700; 208 | text-decoration: underline; 209 | text-align: center; 210 | } 211 | 212 | .sick-city-label { 213 | font-size: 16px; 214 | font-weight: 700 215 | } 216 | 217 | .sick-city-value{ 218 | font-size: 16px; 219 | } 220 | 221 | .jobs-title { 222 | font-size: 20px; 223 | font-weight: 700; 224 | text-decoration: underline; 225 | text-align: center; 226 | } 227 | 228 | .jobs-subtitle { 229 | font-size: 16px; 230 | font-weight: 500; 231 | text-align: right; 232 | padding-top: 8px; 233 | padding-bottom: 8px; 234 | } 235 | 236 | .jobs-label { 237 | font-size: 15px; 238 | font-weight: 700 239 | } 240 | 241 | .jobs-value { 242 | font-size: 15px; 243 | } 244 | 245 | .jobs-group { 246 | padding-top: 5px; 247 | padding-bottom: 5px; 248 | } 249 | 250 | 251 | @media(max-width:768px) { 252 | 253 | .map-reader-img{ 254 | width: 300px !important; 255 | box-shadow: 0 3px 30px #00000029; 256 | } 257 | .map-reader { 258 | left: 0; 259 | bottom: 0; 260 | width: 300px; 261 | display: flex; 262 | justify-content: center; 263 | } 264 | 265 | #zoom-to-location-icon { 266 | display: block; 267 | margin-bottom: 5px; 268 | } 269 | 270 | .zoom-control-buttons { 271 | display: none; 272 | } 273 | 274 | .map-filters { 275 | justify-content: space-evenly; 276 | } 277 | 278 | .filter-button { 279 | width: 48%; 280 | margin: 0; 281 | } 282 | 283 | .left-section-buttons { 284 | margin-bottom: 70px; 285 | } 286 | 287 | } 288 | 289 | @media(max-width: 400px) { 290 | 291 | .filter-button { 292 | width: 46%; 293 | margin: 0; 294 | font-size: 14px; 295 | } 296 | 297 | #zoom-to-location-icon { 298 | width: 37px; 299 | } 300 | 301 | } 302 | -------------------------------------------------------------------------------- /public/assets/images/map-icons/street-food-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /public/admin/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | admin 6 | 10 | 11 | 12 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 51 |
52 | 55 |

עדכון מצב החולים בישראל

56 |
57 | עדכון אחרון: 58 | 59 |
60 |
61 |
62 | 63 | 64 |
65 | מספר חולים עכשווי: 66 | 67 |
68 |
69 |
70 | 71 | 72 |
73 | מספר מחלימים עכשווי: 74 | 75 |
76 |
77 |
78 | 79 | 80 |
81 | מספר מתים עכשווי: 82 | 83 |
84 |
85 | 86 |
87 |

נשלח בהצלחה

88 |

אנא נסה שנית

89 |
90 |
91 | העלאה 92 |
93 |
94 | 95 | 96 |
97 |
98 |

99 | שם הקובץ: 100 | 101 |

102 |

קובץ אקסל עלה בהצלחה

103 |

אנא נסה שנית

104 |
105 | 106 |

נתוני ערים עודכנו בהצלחה

107 |

אנא נסה שנית

108 |
109 | עדכון אחרון: 110 | 111 |
112 |
113 |
114 |
115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /data-gen/compare.py: -------------------------------------------------------------------------------- 1 | import wakkaide.network_file 2 | wakkaide.network_file.setup() 3 | import json 4 | import difflib 5 | 6 | EXPECTED_DATA_KEYS = { "id", "lat", "lon", "label", "text", "pat_num", "t_start", "t_end", "pub_date", "pub_ts", "link"} 7 | EXPECTED_POINTS_KEYS = {"position", "label", "description", "t_start", "t_end", "radius", "link"} 8 | 9 | 10 | def validate(point_list, expected_keys, id_key): 11 | errors = [] 12 | 13 | for point in point_list: 14 | keys = set(point.keys()) 15 | if keys != expected_keys: 16 | for extra in keys - expected_keys: 17 | errors.append('Unexpected key: "{}"'.format(extra)) 18 | 19 | for missing in expected_keys - keys: 20 | errors.append('Missing key "{}" in point with ID {}'.format(missing, point.get(id_key, ''))) 21 | 22 | return errors 23 | 24 | 25 | def chop_plus(s): 26 | pos = s.find('+') 27 | if pos >= 0: 28 | s = s[:pos] 29 | return s 30 | 31 | 32 | def add_position_string(pt): 33 | pt['position'] = '(%.5f, %.5f)' % (pt['lon'], pt['lat']) 34 | 35 | 36 | def convert_data_json(in_json): 37 | def convert_data_item(in_): 38 | res = { 39 | 'lat': float(in_['lat']), 40 | 'lon': float(in_['lon']), 41 | 'label': in_['label'], 42 | 'description': in_['text'], 43 | 't_start': chop_plus(in_['t_start']), 44 | 't_end': chop_plus(in_['t_end']), 45 | 'link': in_['link'], 46 | 'pat_num': in_['pat_num'], 47 | 'pub_ts': in_['pub_ts']} 48 | add_position_string(res) 49 | return res 50 | 51 | return [convert_data_item(item) for item in in_json] 52 | 53 | 54 | def convert_points_json(in_json): 55 | def convert_points_item(in_): 56 | res = {key: in_[key] for key in ['label', 'description', 't_start', 't_end', 'radius', 'link']} 57 | res['lat'] = in_['position'][1] 58 | res['lon'] = in_['position'][0] 59 | add_position_string(res) 60 | return res 61 | 62 | return [convert_points_item(item) for item in in_json] 63 | 64 | 65 | def compare_points(pt1, pt2): 66 | EPS = 1e-5 # about 1m 67 | MIN_STRING_SIMILARITY = 0.7 68 | 69 | differences = [] 70 | 71 | if abs(pt1['lat'] - pt2['lat']) > EPS or abs(pt1['lon'] - pt2['lon']) > EPS: 72 | differences.append('position') 73 | 74 | for field in ['label', 't_start', 't_end', 'link']: 75 | if pt1[field] != pt2[field]: 76 | differences.append(field) 77 | 78 | for field in ['label', 'description']: 79 | if pt1[field] and pt2[field] and string_similarity(pt1[field], pt2[field]) < MIN_STRING_SIMILARITY: 80 | differences.append(field) 81 | 82 | return differences 83 | 84 | 85 | def string_similarity(str1, str2): 86 | base_len = max(len(str1), len(str2)) 87 | if min(len(str1), len(str2)) < 5: 88 | return 0 89 | 90 | matcher = difflib.SequenceMatcher(None, str2, str2) 91 | common_size = sum(block[2] for block in matcher.get_matching_blocks()) 92 | return common_size / base_len 93 | 94 | 95 | def remove_multiplicities(points): 96 | keyed = {} 97 | for point in points: 98 | key = (point['position'], point['t_start'], point['t_end']) 99 | keyed.setdefault(key, []).append(point) 100 | 101 | print('{} multiplicities'.format(len(points) - len(keyed))) 102 | return [l[0] for l in keyed.values()] 103 | 104 | 105 | def match_points(pts1, pts2): 106 | by_position1 = {} 107 | for pt in pts1: 108 | by_position1.setdefault(pt['position'], []).append(pt) 109 | by_position2 = {} 110 | for pt in pts2: 111 | by_position2.setdefault(pt['position'], []).append(pt) 112 | 113 | positions1 = set(by_position1.keys()) 114 | positions2 = set(by_position2.keys()) 115 | matched_positions = positions1 & positions2 116 | 117 | unmatched1 = [] 118 | for pos, points in by_position1.items(): 119 | if pos not in matched_positions: 120 | unmatched1 += points 121 | 122 | unmatched2 = [] 123 | for pos, points in by_position2.items(): 124 | if pos not in matched_positions: 125 | unmatched2 += points 126 | 127 | matches = [] 128 | for pos in matched_positions: 129 | pos_pts1 = by_position1[pos][:] 130 | pos_pts2 = by_position2[pos][:] 131 | 132 | for pt1 in pos_pts1: 133 | candidates = [(i, pt2) for (i, pt2) in enumerate(pos_pts2) 134 | if pt2['t_start'] == pt1['t_start'] and pt2['t_end'] == pt1['t_end']] 135 | if len(candidates) > 1: 136 | print('Detected multiplicity between') 137 | [print(' ' + cand[1]['label']) for cand in candidates] 138 | for cand in candidates[::-1]: 139 | del pos_pts2[cand[0]] 140 | 141 | if candidates: 142 | matches.append((pt1, candidates[0][1])) 143 | else: 144 | unmatched1.append(pt1) 145 | 146 | unmatched2 += pos_pts2 147 | 148 | return {'matches': matches, 'unmatched1': unmatched1, 'unmatched2': unmatched2} 149 | 150 | 151 | def print_differences(matches): 152 | for pt1, pt2 in matches: 153 | differences = compare_points(pt1, pt2) 154 | differences = set(differences) - {'link'} 155 | if differences: 156 | print('differences in point "{}"'.format(pt1['label'])) 157 | for dif in differences: 158 | print(' {}: {} vs {}'.format(dif, pt1[dif], pt2[dif])) 159 | 160 | 161 | def compare_sets(pts1, pts2): 162 | matching = match_points(pts1, pts2) 163 | 164 | print('{} points in set 1 only, {} points in set 2 only, {} matched'.format( 165 | len(matching['unmatched1']), len(matching['unmatched2']), len(matching['matches']))) 166 | 167 | stats = {} 168 | for pt1, pt2 in matching['matches']: 169 | 170 | differences = compare_points(pt1, pt2) 171 | for dif in differences: 172 | stats[dif] = stats.get(dif, 0) + 1 173 | 174 | print_differences(matching['matches']) 175 | 176 | print('difference stats:', stats) 177 | 178 | with open('corona/moh_compare/data.json', 'r') as f: 179 | data_points = f.read() 180 | data_points = json.loads(data_points) 181 | errors = validate(data_points, EXPECTED_DATA_KEYS, 'id') 182 | if errors: 183 | print('============================ Errors in data.json ===========') 184 | print(*errors, sep='\n') 185 | data_points = convert_data_json(data_points) 186 | data_points = remove_multiplicities(data_points) 187 | 188 | with open('corona/moh_compare/points.json', 'r') as f: 189 | points_points = json.loads(f.read()) 190 | errors = validate(points_points, EXPECTED_POINTS_KEYS, 'label') 191 | if errors: 192 | print('============================ Errors in points.json ===========') 193 | print(*errors, sep='\n') 194 | points_points = convert_points_json(points_points) 195 | points_points = remove_multiplicities(points_points) 196 | 197 | compare_sets(data_points, points_points) -------------------------------------------------------------------------------- /public/js/tracks.js: -------------------------------------------------------------------------------- 1 | var fullTrackList; 2 | var selectedStart; 3 | var selectedEnd; 4 | 5 | const createList = (tracksList, initial) => { 6 | let strHtml = '
'; 7 | let startDate; 8 | let endDate; 9 | let startTime; 10 | let endTime; 11 | let startDateString; 12 | let endDateString; 13 | let month1; 14 | let month2; 15 | 16 | tracksList.reverse().forEach(track => { 17 | startDate = new Date(track.t_start); 18 | endDate = new Date(track.t_end); 19 | 20 | startTime = ("0" + startDate.getHours()).slice(-2) + ":" + ("0" + startDate.getMinutes()).slice(-2); 21 | endTime = ("0" + endDate.getHours()).slice(-2) + ":" + ("0" + endDate.getMinutes()).slice(-2); 22 | 23 | month1 = startDate.getMonth() + 1; 24 | month2 = endDate.getMonth() + 1; 25 | 26 | startDateString = `${startDate.getDate()}.${month1}.${startDate.getFullYear()}`; 27 | endDateString = `${endDate.getDate()}.${month2}.${endDate.getFullYear()}`; 28 | 29 | if (initial) { 30 | track.startTime = startTime; 31 | track.endTime = endTime; 32 | track.startDate = startDate; 33 | track.endDate = endDate; 34 | track.startDateString = startDateString; 35 | track.endDateString = endDateString; 36 | } 37 | 38 | 39 | let card = `
40 |
41 |
42 |
${track.label}
43 | 44 |
45 |
46 |
47 |
48 | זמן התחלה: ${startDateString} 49 |
50 |
51 |
52 |
53 |
54 |
55 | שעת התחלה: ${startTime} 56 |
57 |
58 |
59 |
60 |
61 |
62 | זמן סיום: ${endDateString} 63 |
64 |
65 |
66 |
67 |
68 |
69 | שעת סיום: ${endTime} 70 |
71 |
72 |
73 | 74 | 75 | `; 76 | if (track.link) { 77 | card += ` מידע נוסף מאתר משרד הבריאות`; 78 | } 79 | 80 | card += `
81 |
`; 82 | strHtml += card; 83 | }); 84 | strHtml += '
'; 85 | 86 | if (initial) { 87 | fullTrackList = tracksList; 88 | } 89 | return strHtml; 90 | }; 91 | 92 | // const handleSearchMobile = () => { 93 | // const input = document.getElementById('search-input'); 94 | // const value = input.value.toUpperCase(); 95 | // let filterdFlights = filterFlights(flights, value); 96 | // renderList(filterdFlights); 97 | // } 98 | 99 | // eslint-disable-next-line no-unused-vars 100 | const handleTracksSearchDesktop = () => { 101 | // Declare variables 102 | const input = document.getElementById('search-track-input-dekstop'); 103 | const value = input.value.toUpperCase(); 104 | let filteredTracks = filterTracks(fullTrackList, value); 105 | fillTable(filteredTracks, false); 106 | }; 107 | 108 | const filterTracks = (trackList, value) => { 109 | value = value.trim(); 110 | let filterdTracks = trackList.filter((track) => { 111 | let rangeMatch = false; 112 | if (selectedStart !== undefined && selectedEnd !== undefined) { 113 | rangeMatch = doDateRangesIntersect(selectedStart, selectedEnd, track.startDate, track.endDate); 114 | } else { 115 | rangeMatch = true; 116 | } 117 | 118 | if (value === "" || value === undefined) { 119 | return rangeMatch; 120 | } 121 | 122 | return ((track.label && track.label.toString().includes(value)) || 123 | (track.startTime && track.startTime.toString().includes(value)) || 124 | (track.endTime && track.endTime.toString().includes(value)) || 125 | (track.pat_num && track.pat_num.toString().includes(value)) || 126 | (track.startDateString && track.startDateString.toString().includes(value)) || 127 | (track.endDateString && track.endDateString.toString().includes(value))) && rangeMatch; 128 | }); 129 | return filterdTracks; 130 | }; 131 | 132 | function doDateRangesIntersect(startDate1, endDate1, startDate2, endDate2) { 133 | let minDateStart, minDateEnd, maxDateStart, maxDateEnd; 134 | if (startDate1.getTime() > startDate2.getTime()) { 135 | minDateStart = startDate2; 136 | minDateEnd = endDate2; 137 | maxDateStart = startDate1; 138 | maxDateEnd = endDate1; 139 | } else { 140 | minDateStart = startDate1; 141 | minDateEnd = endDate1; 142 | maxDateStart = startDate2; 143 | maxDateEnd = endDate2; 144 | } 145 | 146 | return (minDateEnd.getTime() > maxDateStart.getTime()); 147 | } 148 | 149 | function isDateInRange(date, start, end) { 150 | return doDateRangesIntersect(date, date, start, end); 151 | } 152 | 153 | function inputParseDate(dateString, timeString) { 154 | let dateFields = dateString.split("-"); 155 | let timeFields = timeString.split(":"); 156 | 157 | let year = parseInt(dateFields[0]); 158 | let month = parseInt(dateFields[1]); 159 | let day = parseInt(dateFields[2]); 160 | 161 | let hour = parseInt(timeFields[0]); 162 | let minute = parseInt(timeFields[1]); 163 | 164 | return new Date(year, month - 1, day, hour, minute, 0, 0); 165 | } 166 | 167 | function parseDateInputs() { 168 | let startDateString = ""; 169 | let endDateString = ""; 170 | let startTimeString = ""; 171 | let endTimeString = ""; 172 | 173 | if (startDateString === "") { 174 | return; 175 | } 176 | 177 | if (startTimeString === "") { 178 | startTimeString = "00:00"; 179 | endTimeString = "23:59"; 180 | } 181 | 182 | if (endDateString === "") { 183 | endDateString = startDateString; 184 | } 185 | 186 | if (endTimeString === "") { 187 | endTimeString = "23:59"; 188 | } 189 | 190 | selectedStart = inputParseDate(startDateString, startTimeString); 191 | selectedEnd = inputParseDate(endDateString, endTimeString); 192 | handleTracksSearchDesktop(); 193 | return; 194 | } 195 | 196 | function fillTable(data, initial) { 197 | $("#tracks-table").html(createList(data, initial)); 198 | } 199 | -------------------------------------------------------------------------------- /public/assets/images/map-icons/mapReader-he.svg: -------------------------------------------------------------------------------- 1 | map reader -------------------------------------------------------------------------------- /public/js/scripts.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | $(document).ready(() => { 3 | 4 | // don't hide pop up when click on this items 5 | $(".hamburger").click(e => { 6 | e.stopPropagation(); 7 | }); 8 | 9 | // on body click anywhere - hide side bar pop-up 10 | $("body").click(() => { 11 | $('.side-bar').fadeOut('fast'); 12 | $('#overlay').removeClass('overlay'); 13 | $( '.hamburger' ).removeClass( "is-active" ); 14 | 15 | }); 16 | 17 | // side-bar menu open 18 | $('.hamburger').click(() => { 19 | $('.side-bar').fadeToggle(50); 20 | $( this ).toggleClass( "is-active" ); 21 | $('#overlay').toggleClass('overlay'); 22 | }); 23 | 24 | // jobs report pop-up 25 | $('.jobs-pop-up-click').click(function () { 26 | 27 | ga('send', { 28 | hitType: 'event', 29 | eventCategory: 'jobsClick', 30 | eventAction: 'Click', 31 | eventLabel: 'Open jobs Pop-Up' 32 | }); 33 | 34 | $('#jobs-pop-up').fadeIn("fast"); 35 | }); 36 | 37 | // jobs-close-x icon 38 | $('#jobs-close-x').click(function () { 39 | $('#jobs-pop-up').fadeOut('fast'); 40 | }); 41 | 42 | $('#keepondoing_careers').click(() => { 43 | ga('send', { 44 | hitType: 'event', 45 | eventCategory: 'ClickOnKeepondoingCareers', 46 | eventAction: 'Click', 47 | eventLabel: 'Open keepondoing careers link' 48 | }); 49 | }); 50 | 51 | $('#techjobscorona_careers').click(() => { 52 | ga('send', { 53 | hitType: 'event', 54 | eventCategory: 'ClickOnTechjobscoronaCareers', 55 | eventAction: 'Click', 56 | eventLabel: 'Open Techjobscorona careers link' 57 | }); 58 | }); 59 | 60 | $('#facebook_group_careers').click(() => { 61 | ga('send', { 62 | hitType: 'event', 63 | eventCategory: 'ClickOnFacebookGroupCareers', 64 | eventAction: 'Click', 65 | eventLabel: 'Open facebook group careers link' 66 | }); 67 | }); 68 | 69 | $('#rouths').click(() => { 70 | ga('send', { 71 | hitType: 'event', 72 | eventCategory: 'ClickOnRouths', 73 | eventAction: 'Click', 74 | eventLabel: 'Open rouths page' 75 | }); 76 | }); 77 | 78 | 79 | 80 | 81 | // terms-close-x icon 82 | $('#terms-close-x').click(() => { 83 | $('#terms-of-use-pop-up').fadeOut('fast'); 84 | }); 85 | 86 | 87 | // open terms of use pop-up 88 | $('.terms-of-use-pop-up-click').click(() => { 89 | $('#terms-of-use-pop-up').fadeIn("fast"); 90 | }); 91 | 92 | 93 | // open embed pop-up 94 | $('.code-embed-pop-up-click').click(() => { 95 | 96 | $('#embedCoronaMap').modal('show'); 97 | $('#hamburgerOnMainScreen').modal('hide'); 98 | 99 | }); 100 | 101 | $('#contact-us').click(() => { 102 | $('#contactUsModal').modal('show'); 103 | $('#hamburgerOnMainScreen').modal('hide'); 104 | ga('send', { 105 | hitType: 'event', 106 | eventCategory: 'Open Contact Us Form', 107 | eventAction: 'Click', 108 | eventLabel: 'Open Contact Us Form' 109 | }); 110 | }); 111 | 112 | $('.update-pop-up-click').click(function () { 113 | 114 | ga('send', { 115 | hitType: 'event', 116 | eventCategory: 'SickPeoplePopUp', 117 | eventAction: 'Click', 118 | eventLabel: 'Open Sick People Pop-Up' 119 | }); 120 | $('#update').attr("dir", langDirection); 121 | $('#update').modal('show'); 122 | 123 | }); 124 | 125 | 126 | 127 | // open map reader pop-up 128 | $('.map-reader-pop-up-click').click(() => { 129 | 130 | $('#map-reader-pop-up').fadeIn("fast"); 131 | 132 | }); 133 | 134 | // map-reader-close-x icon 135 | $('#map-reader-close-x').click(() => { 136 | $('#map-reader-pop-up').fadeOut('fast'); 137 | }); 138 | 139 | if (typeof zoomToLocation !== 'undefined') { 140 | $('#zoom-to-location-button').click(zoomToLocation); 141 | } 142 | 143 | // open language pop-up 144 | $('.language-click').click(function () { 145 | 146 | ga('send', { 147 | hitType: 'event', 148 | eventCategory: 'SwitchLanguage', 149 | eventAction: 'Click', 150 | eventLabel: 'Open Language Pop-Up' 151 | }); 152 | 153 | $('#language-popup').modal('toggle'); 154 | }); 155 | 156 | $('#show-sick-people-track').click(() => { 157 | const showTracksButton = $('#show-sick-people-track'); 158 | showTracksButton.toggleClass('selected'); 159 | const isSelected = showTracksButton.hasClass('selected'); 160 | $('#show-filter-text-sick-people').text(isSelected ? 'הסתר: ' : 'הצג: '); 161 | toggleSickTracksOnMap({shouldShowTracks: isSelected}); 162 | 163 | const eventCategory = isSelected ? 'Show People Tracks' : 'Hide People Tracks'; 164 | 165 | ga('send', { 166 | hitType: 'event', 167 | eventCategory: eventCategory, 168 | eventAction: 'Click', 169 | eventLabel: 'Sick people track filter' 170 | }); 171 | 172 | }); 173 | 174 | $('#show-top-corona-cities').click(() => { 175 | const citiesButton = $('#show-top-corona-cities'); 176 | citiesButton.toggleClass('selected'); 177 | const isSelected = citiesButton.hasClass('selected'); 178 | $('#show-filter-text-top-cities').text(isSelected ? 'הסתר: ' : 'הצג: '); 179 | toggleCitiesOnMap({shouldShowCities: citiesButton.hasClass('selected')}); 180 | 181 | const eventCategory = isSelected ? 'Show Top Cities' : 'Hide Top Cities'; 182 | 183 | ga('send', { 184 | hitType: 'event', 185 | eventCategory: eventCategory, 186 | eventAction: 'Click', 187 | eventLabel: 'Sick people track filter' 188 | }); 189 | }); 190 | }); 191 | 192 | 193 | 194 | /*! track-focus v 1.0.0 | Author: Jeremy Fields [jeremy.fields@vget.com], 2015 | License: MIT */ 195 | // inspired by: http://irama.org/pkg/keyboard-focus-0.3/jquery.keyboard-focus.js 196 | 197 | ((body) => { 198 | 199 | let usingMouse; 200 | 201 | let preFocus = (event) => { 202 | usingMouse = (event.type === 'mousedown'); 203 | }; 204 | 205 | let addFocus = (event) => { 206 | if (usingMouse) 207 | event.target.classList.add('focus--mouse'); 208 | }; 209 | 210 | let removeFocus = (event) => { 211 | event.target.classList.remove('focus--mouse'); 212 | }; 213 | 214 | let bindEvents = () => { 215 | body.addEventListener('keydown', preFocus); 216 | body.addEventListener('mousedown', preFocus); 217 | body.addEventListener('focusin', addFocus); 218 | body.addEventListener('focusout', removeFocus); 219 | }; 220 | 221 | bindEvents(); 222 | 223 | })(document.body); 224 | 225 | $('#contact-us-container').on('submit', (e) => { 226 | e.preventDefault(); 227 | const button = $('#contactUsButton'); 228 | const name = $('#contact-us-name').val(); 229 | const mail = $('#contact-us-mail').val(); 230 | const message = $('#contact-us-message').val(); 231 | db.collection("contact-us").add({ 232 | name, 233 | mail, 234 | message, 235 | timestamp: (new Date()).getTime() 236 | }) 237 | .then(() => { 238 | onContactUsSuccess(button); 239 | resetSubmitText(button, 5000); 240 | setTimeout(() => {$('#contact-us-container').get(0).reset();}, 3000); 241 | ga('send', { 242 | hitType: 'event', 243 | eventCategory: 'Send Contact Us Form Success', 244 | eventAction: 'Click', 245 | eventLabel: 'Send form success' 246 | }); 247 | }) 248 | .catch((error) => { 249 | onContactUsError(button); 250 | resetSubmitText(button, 2000); 251 | console.error(error); 252 | ga('send', { 253 | hitType: 'event', 254 | eventCategory: 'Send Contact Us Form Failed', 255 | eventAction: 'Click', 256 | eventLabel: 'Send form failed' 257 | }); 258 | }); 259 | }); 260 | 261 | const onContactUsSuccess = (button) => { 262 | button.text("נשלח"); 263 | button.removeClass("btn-copy-clipboard"); 264 | button.addClass("btn-copy-clipboard-success"); 265 | }; 266 | 267 | const onContactUsError = (button) => { 268 | button.text("אנא נסה שנית"); 269 | button.removeClass("btn-copy-clipboard"); 270 | button.addClass("btn-copy-clipboard-error"); 271 | }; 272 | 273 | const resetSubmitText = (button, time = 5000) => { 274 | setTimeout(() => { 275 | button.text("שלח"); 276 | button.addClass("btn-copy-clipboard"); 277 | button.removeClass("btn-copy-clipboard-success"); 278 | button.removeClass("btn-copy-clipboard-error"); 279 | }, time); 280 | }; 281 | 282 | -------------------------------------------------------------------------------- /public/assets/images/icm.svg: -------------------------------------------------------------------------------- 1 | icm -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /public/tracks/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 | מפת הקורונה של ישראל 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |
50 | 51 | 52 | 83 | 84 | 85 |
86 |
87 | 94 |
95 |
96 | 97 | 98 | 116 | 117 | 118 |
119 | 120 |
121 |
122 | 123 | 124 | 227 | 228 | 229 | 230 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | --------------------------------------------------------------------------------