diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c081dcb --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +hauk diff --git a/frontend/assets/controls/locate-active.svg b/frontend/assets/controls/locate-active.svg new file mode 100644 index 0000000..0593e59 --- /dev/null +++ b/frontend/assets/controls/locate-active.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/frontend/assets/controls/locate-inactive.svg b/frontend/assets/controls/locate-inactive.svg new file mode 100644 index 0000000..b8c82ef --- /dev/null +++ b/frontend/assets/controls/locate-inactive.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/frontend/assets/controls/locate-pending.svg b/frontend/assets/controls/locate-pending.svg new file mode 100644 index 0000000..e4d6f82 --- /dev/null +++ b/frontend/assets/controls/locate-pending.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/assets/controls/radar.svg b/frontend/assets/controls/radar.svg new file mode 100644 index 0000000..18bcc49 --- /dev/null +++ b/frontend/assets/controls/radar.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/assets/favicon.png b/frontend/assets/favicon.png new file mode 100644 index 0000000..624af61 Binary files /dev/null and b/frontend/assets/favicon.png differ diff --git a/frontend/assets/favicon.svg b/frontend/assets/favicon.svg new file mode 100644 index 0000000..e7208ff --- /dev/null +++ b/frontend/assets/favicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/assets/lang/ca.json b/frontend/assets/lang/ca.json new file mode 100644 index 0000000..bf005f4 --- /dev/null +++ b/frontend/assets/lang/ca.json @@ -0,0 +1,28 @@ +{ + "google_play_badge_url": "https://play.google.com/intl/ca_ES/badges/static/images/badges/ca_badge_web_generic.png", + "f_droid_badge_url": "https://fdroid.gitlab.io/artwork/badge/get-it-on-ca.png", + "btn_decrypt": "Desxifrar", + "btn_cancel": "Cancel·la", + "btn_dismiss": "Rebutjar", + "last_update_days": "Fa {{time}} dies", + "last_update_hours": "Fa {{time}} hores", + "last_update_minutes": "Fa {{time}} minuts", + "last_update_seconds": "Fa {{time}} segons", + "status_offline": "Fora de línia", + "status_expired": "Caducat", + "dialog_connection_body": "S'ha perdut la connexió amb el servidor Hauk. Hauk intentarà tornar a connectar en segon pla. Comproveu que teniu connexió a la xarxa.", + "dialog_connection_head": "Error de connexió", + "dialog_expired_body": "Aquesta compartició d’ubicació ha caducat.", + "dialog_expired_head": "La compartició ha caducat", + "point_app_to": "Apunteu l'aplicació Hauk a aquest servidor per compartir la vostra ubicació:", + "gnss_signal_body": "El remitent està esperant el senyal GPS", + "gnss_signal_head": "Si us plau, esperi", + "e2e_unsupported": "Aquest compartiment està protegit per xifrat d'extrem a extrem. Sembla que el vostre navegador no admet les funcions criptogràfiques necessàries per desxifrar aquestes accions. Torneu-ho a provar amb un altre navegador web.", + "e2e_unavailable_secure": "Aquest compartiment està protegit per xifrat d'extrem a extrem. Actualment, el desxiframent no està disponible perquè no utilitzeu HTTPS. Assegureu-vos que utilitzeu HTTPS i, després, torneu-ho a provar.", + "e2e_incorrect": "La contrasenya de xifrat que heu introduït no és correcta. Si us plau torna-ho a provar.", + "e2e_password_prompt": "Aquest compartiment està protegit per xifrat d'extrem a extrem. Introduïu la contrasenya de xifratge per accedir al recurs compartit.", + "e2e_placeholder": "Contrasenya de xifrat", + "e2e_title": "Xifrat d'extrem a extrem", + "expired_body": "La ubicació compartida a la qual heu intentat accedir no s'ha trobat al servidor. Si aquest enllaç funcionava abans, la participació podria haver caducat.", + "expired_head": "L'ubicació ha expirat" +} diff --git a/frontend/assets/lang/de.json b/frontend/assets/lang/de.json new file mode 100644 index 0000000..b1fcd55 --- /dev/null +++ b/frontend/assets/lang/de.json @@ -0,0 +1,36 @@ +{ + "f_droid_badge_url": "https://fdroid.gitlab.io/artwork/badge/get-it-on.png", + "google_play_badge_url": "https://play.google.com/intl/en_us/badges/static/images/badges/de_badge_web_generic.png", + "btn_dismiss": "Verwerfen", + "status_offline": "Offline", + "status_expired": "Abgelaufen", + "dialog_connection_body": "Die Verbindung zum Hauk-Server wurde unterbrochen. Hauk wird versuchen, sich im Hintergrund wieder zu verbinden. Bitte prüfen Sie, ob Sie über eine Netzwerkverbindung verfügen.", + "dialog_connection_head": "Verbindungsfehler", + "dialog_expired_body": "Diese Standortfreigabe ist abgelaufen.", + "dialog_expired_head": "Freigabe abgelaufen", + "point_app_to": "In der Hauk App diese URL eingeben, um Ihren Standort zu teilen:", + "gnss_signal_body": "Der Sender wartet auf ein GPS-Signal", + "gnss_signal_head": "Bitte warten", + "expired_body": "Die Standortfreigabe, auf die Sie zugreifen wollen, wurde nicht auf dem Server gefunden. Wenn dieser Link zuvor funktioniert hat, ist die Freigabe möglicherweise abgelaufen.", + "expired_head": "Standortfreigabe abgelaufen", + "btn_decrypt": "Entsperren", + "btn_cancel": "Abbrechen", + "last_update_days": "{{time}}T her", + "last_update_hours": "{{time}}S her", + "last_update_minutes": "{{time}}M her", + "last_update_seconds": "{{time}}S her", + "e2e_unsupported": "Diese Freigabe ist passwortgeschützt. Dem Browser fehlt die Funktionalität solche Freigaben zu öffnen. Bitte mit einem anderen Browser noch einmal probieren.", + "e2e_unavailable_secure": "Diese Freigabe ist passwortgeschützt. Die Freigabe kann nicht entsperrt werden, da kein HTTPS benutzt wird. Bitte sicherstellen, dass HTTPS benutzt wird, dann noch einmal probieren.", + "e2e_incorrect": "Das eingegebene Passwort ist falsch. Bitte noch einmal probieren.", + "e2e_password_prompt": "Diese Freigabe ist passwortgeschützt. Bitte das Passwort eingeben, um der Freigabe beizutreten.", + "e2e_placeholder": "Passwort eingeben", + "e2e_title": "Passwortgeschützt", + "google_play_badge_text": "Hole es von Google Play", + "f_droid_badge_text": "Hole es von F-Droid", + "btn_show_all": "Zeige alle", + "btn_close": "Schließen", + "control_show_self": "Zeige meine Position", + "dialog_user_navigate": "Navigiere zu", + "dialog_user_follow": "Auf der Karte zeigen", + "dialog_active_head": "Aktive Personen" +} diff --git a/frontend/assets/lang/en.json b/frontend/assets/lang/en.json new file mode 100644 index 0000000..e7c75d6 --- /dev/null +++ b/frontend/assets/lang/en.json @@ -0,0 +1,36 @@ +{ + "expired_head": "Location expired", + "expired_body": "The shared location you tried to access was not found on the server. If this link worked before, the share might have expired.", + "e2e_title": "Password protected", + "e2e_placeholder": "Enter password", + "e2e_password_prompt": "This share is password protected. Please enter the password to access the share.", + "e2e_incorrect": "The password you entered was wrong. Please try again.", + "e2e_unavailable_secure": "This share is password protected. You cannot unlock this share because you are not using HTTPS. Please ensure you are using HTTPS, then try again.", + "e2e_unsupported": "This share is password protected. Your browser lacks the functionality required to open such shares. Please try again with another web browser.", + "gnss_signal_head": "Please wait", + "gnss_signal_body": "Sender is waiting for GPS signal", + "point_app_to": "Point the Hauk app to this server to share your location:", + "dialog_expired_head": "Share expired", + "dialog_expired_body": "This location share has expired.", + "dialog_connection_head": "Connection error", + "dialog_connection_body": "Connection to the Hauk server was lost. Hauk will try to reconnect in the background. Please check that you have network connectivity.", + "dialog_active_head": "Active users", + "dialog_user_follow": "Show on map", + "dialog_user_navigate": "Navigate to", + "status_expired": "Expired", + "status_offline": "Offline", + "control_show_self": "Show my location", + "last_update_seconds": "{{time}}s ago", + "last_update_minutes": "{{time}}m ago", + "last_update_hours": "{{time}}h ago", + "last_update_days": "{{time}}d ago", + "btn_dismiss": "Dismiss", + "btn_cancel": "Cancel", + "btn_decrypt": "Unlock", + "btn_close": "Close", + "btn_show_all": "Show all", + "f_droid_badge_url": "https://fdroid.gitlab.io/artwork/badge/get-it-on.png", + "f_droid_badge_text": "Get it on F-Droid", + "google_play_badge_url": "https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png", + "google_play_badge_text": "Get it on Google Play" +} diff --git a/frontend/assets/lang/eu.json b/frontend/assets/lang/eu.json new file mode 100644 index 0000000..050f216 --- /dev/null +++ b/frontend/assets/lang/eu.json @@ -0,0 +1,16 @@ +{ + "f_droid_badge_url": "https://fdroid.gitlab.io/artwork/badge/get-it-on-eu.png", + "google_play_badge_url": "https://play.google.com/intl/en_us/badges/static/images/badges/eu_badge_web_generic.png", + "btn_dismiss": "Baztertu", + "status_offline": "Deskonektatuta", + "status_expired": "Iraungita", + "dialog_connection_body": "Hauk zerbitzariarekin konexioa galdu da. Hauk bigarren planoan konektatzen saiatuko da. Egiaztatu baduzula sarera konexioa.", + "dialog_connection_head": "Konexio errorea", + "dialog_expired_body": "Kokaleku partekatze honek iraungi du.", + "dialog_expired_head": "Partekatzea iraungita", + "point_app_to": "Zuzendu Hauk aplikazioa zerbitzari honetara kokalekua partekatzeko:", + "gnss_signal_body": "Igorlea GPS seinalearen zain dago", + "gnss_signal_head": "Itxaron mesedez", + "expired_body": "Atzitzen saiatu zaren partekatutako kokalekua ez da zerbitzarian aurkitu. Esteka honek lehen funtzionatzen bazuen, agian partekatzeak iraungi du.", + "expired_head": "Kokalekua iraungita" +} diff --git a/frontend/assets/lang/fr.json b/frontend/assets/lang/fr.json new file mode 100644 index 0000000..06e7281 --- /dev/null +++ b/frontend/assets/lang/fr.json @@ -0,0 +1,36 @@ +{ + "btn_dismiss": "Annuler", + "status_offline": "Hors ligne", + "status_expired": "Expiré", + "dialog_connection_body": "La connection avec le serveur Hauk a été perdue. Hauk essayera de se reconnecter en tâche de fond. Veuillez vérifier votre connexion réseau.", + "dialog_connection_head": "Erreur de connection", + "dialog_expired_body": "Ce partage de localisation a expiré.", + "dialog_expired_head": "Partage expiré", + "point_app_to": "Renseignez ce serveur dans votre application Hauk pour partager votre localisation:", + "gnss_signal_body": "La source manque de signal GPS", + "gnss_signal_head": "Veuillez patienter", + "expired_body": "La localisation que vous recherchez n'a pas été trouvée. Si ce lien fonctionnait, le partage de localisation a peut-être expiré.", + "expired_head": "Localisation expirée", + "f_droid_badge_url": "https://fdroid.gitlab.io/artwork/badge/get-it-on-fr.png", + "google_play_badge_url": "https://play.google.com/intl/en_us/badges/static/images/badges/fr_badge_web_generic.png", + "e2e_password_prompt": "Ce partage est protégé par un mot de passe. Entrez le mot de passe pour y accéder.", + "google_play_badge_text": "Installer par Google Play", + "f_droid_badge_text": "Installer par F-Droid", + "btn_show_all": "Monter tout", + "btn_close": "Fermer", + "btn_decrypt": "Déverrouiller", + "btn_cancel": "Annuler", + "last_update_days": "Il y a {{time}}j", + "last_update_hours": "Il y a {{time}}h", + "last_update_minutes": "Il y a {{time}}m", + "last_update_seconds": "Il y a {{time}}s", + "control_show_self": "Montrer ma position", + "dialog_user_navigate": "Aller à", + "dialog_user_follow": "Monter sur la carte", + "dialog_active_head": "Utilisateurs actifs", + "e2e_unsupported": "Ce partage est protégé par un mot de passe. Votre navigateur ne dispose pas de la fonctionnalité permettant d'ouvrir ce type de partage. Merci de réessayer avec un autre navigateur.", + "e2e_unavailable_secure": "Ce partage est protégé par un mot de passe. Vous ne pouvez pas le débloquer car vous n'utilisez pas HTTPS. Assurez-vous d'utiliser HTTPS, puis réessayez.", + "e2e_incorrect": "Le mot de passe que vous avez saisi est incorrect. Veuillez réessayer.", + "e2e_placeholder": "Entrez votre mot de passe", + "e2e_title": "Protégé par mot de passe" +} diff --git a/frontend/assets/lang/it.json b/frontend/assets/lang/it.json new file mode 100644 index 0000000..ab73839 --- /dev/null +++ b/frontend/assets/lang/it.json @@ -0,0 +1,36 @@ +{ + "google_play_badge_text": "Disponibile su Google Play", + "google_play_badge_url": "https://play.google.com/intl/it_it/badges/static/images/badges/it_badge_web_generic.png", + "f_droid_badge_text": "Disponibile su F-Droid", + "f_droid_badge_url": "https://fdroid.gitlab.io/artwork/badge/get-it-on-it.png", + "btn_show_all": "Mostra tutto", + "btn_close": "Chiudi", + "btn_decrypt": "Sblocca", + "btn_cancel": "Annulla", + "btn_dismiss": "Ignora", + "last_update_days": "{{time}}g fa", + "last_update_hours": "{{time}} ore fa", + "last_update_minutes": "{{time}}min fa", + "last_update_seconds": "{{time}}sec fa", + "control_show_self": "Mostra la mia posizione", + "status_offline": "Non in linea", + "status_expired": "Scaduto", + "dialog_user_navigate": "Naviga verso", + "dialog_user_follow": "Mostra sulla mappa", + "dialog_active_head": "Utenti attivi", + "dialog_connection_body": "La connessione al server Hauk è stata perduta. Hauk tenterà di riconnersi. Controlla di avere una connessione di rete attiva.", + "dialog_connection_head": "Errore di connessione", + "dialog_expired_body": "Questa condivisione della posizione è scaduta.", + "dialog_expired_head": "Condivisione scaduta", + "point_app_to": "Inserisci il seguente link nell'app Hauk per condividere la tua posizione:", + "gnss_signal_body": "Il mittente è in attesa del segnale GPS", + "gnss_signal_head": "Attendere prego", + "e2e_unsupported": "Questa condivisione è protetta da password. Il tuo browser non ha le funzionalità necessarie per accedervi. Riprova con un altro browser.", + "e2e_unavailable_secure": "La condivisione è protetta da password. Non puoi accedervi perché non stai usando HTTPS. Assicurati di star usando HTTPS e riprova.", + "e2e_incorrect": "La password inserita non è corretta. Riprova.", + "e2e_password_prompt": "Questa condivisione è protetta da password. Inserire la password per accedervi.", + "e2e_placeholder": "Inserire password", + "e2e_title": "Protetto da password", + "expired_body": "La posizione condivisa a cui hai tentato di accedere non è stata trovata sul server. Se questo indirizzo funzionava in passato, la condivisione potrebbe essere scaduta.", + "expired_head": "Posizione scaduta" +} diff --git a/frontend/assets/lang/ko.json b/frontend/assets/lang/ko.json new file mode 100644 index 0000000..411d4e5 --- /dev/null +++ b/frontend/assets/lang/ko.json @@ -0,0 +1,36 @@ +{ + "btn_dismiss": "닫기", + "google_play_badge_text": "Google Play 에서 다운로드", + "google_play_badge_url": "https://play.google.com/intl/en_us/badges/static/images/badges/ko_badge_web_generic.png", + "f_droid_badge_text": "F-Droid 에서 다운로드", + "f_droid_badge_url": "https://fdroid.gitlab.io/artwork/badge/get-it-on-ko.png", + "btn_show_all": "모두 보기", + "btn_close": "닫기", + "btn_decrypt": "잠금 해제", + "btn_cancel": "취소", + "last_update_days": "{{time}}일 전", + "last_update_hours": "{{time}}시간 전", + "last_update_minutes": "{{time}}분 전", + "last_update_seconds": "{{time}}초 전", + "control_show_self": "내 위치 표시하기", + "status_offline": "오프라인", + "status_expired": "만료됨", + "dialog_user_navigate": "이 위치로 안내", + "dialog_user_follow": "지도에서 보기", + "dialog_active_head": "활성 사용자들", + "dialog_connection_body": "Hauk 서버로의 접속이 끊겼습니다. Hauk은 백그라운드에서 재접속을 시도하겠습니다. 네트워크 접속 여부를 확인해주세요.", + "dialog_connection_head": "접속 오류", + "dialog_expired_body": "이 위치 공유가 만료되었습니다.", + "dialog_expired_head": "링크가 만료됨", + "point_app_to": "위치를 공유하기 위해 Hauk 앱을 이 서버로 설정하여 주십시오:", + "gnss_signal_body": "발신자가 GPS 신호를 기다리고 있습니다", + "gnss_signal_head": "잠시만 기다려주세요", + "e2e_unsupported": "이 링크는 비밀번호로 보호되어 있습니다. 사용하고 계시는 브라우저는 잠금을 해제하기 위한 기능을 지원하지 않습니다. 다른 브라우저로 다시 시도해주세요.", + "e2e_unavailable_secure": "이 링크는 비밀번호로 보호되어 있습니다. HTTPS를 사용하고 있지 않기 때문에 보호를 해제할 수 없습니다. HTTPS를 사용중인지 확인하시고 다시 시도해주세요.", + "e2e_incorrect": "입력한 비밀번호가 올바르지 않습니다. 다시 시도해주세요.", + "e2e_password_prompt": "이 링크는 비밀번호로 보호되어 있습니다. 링크에 접근하기 위해 비밀번호를 입력해주세요.", + "e2e_placeholder": "비밀번호를 입력하세요", + "e2e_title": "비밀번호로 보호됨", + "expired_body": "요청하신 위치정보를 서버에서 찾을 수 없습니다. 기존에 작동하던 링크라면, 위치 공유가 만료되었을 수 있습니다.", + "expired_head": "위치 정보가 만료됨" +} diff --git a/frontend/assets/lang/nb_NO.json b/frontend/assets/lang/nb_NO.json new file mode 100644 index 0000000..1ab2928 --- /dev/null +++ b/frontend/assets/lang/nb_NO.json @@ -0,0 +1,36 @@ +{ + "f_droid_badge_url": "https://fdroid.gitlab.io/artwork/badge/get-it-on-no.png", + "google_play_badge_url": "https://play.google.com/intl/en_us/badges/static/images/badges/no_badge_web_generic.png", + "btn_dismiss": "Lukk", + "status_offline": "Frakoblet", + "status_expired": "Utløpt", + "dialog_connection_body": "Tilkoblingen til Hauk-serveren forsvant. Hauk vil prøve å koble til igjen i bakgrunnen. Vennligst sjekk at du er tilkoblet nettet.", + "dialog_connection_head": "Tilkoblingsfeil", + "dialog_expired_body": "Den delte posisjonen du fulgte har utløpt.", + "dialog_expired_head": "Deling utløpt", + "point_app_to": "Skriv inn følgende link i Hauk-appen for å dele posisjonen din:", + "gnss_signal_body": "Sender venter på GPS-signal", + "gnss_signal_head": "Vennligst vent", + "expired_body": "Den delte posisjonen du prøvde å åpne ble ikke funnet på serveren. Hvis denne linken virket tidligere, kan det hende at delingen har utløpt.", + "expired_head": "Posisjon utløpt", + "btn_decrypt": "Åpne", + "btn_cancel": "Avbryt", + "e2e_unsupported": "Denne delte posisjonen er passordbeskyttet. Nettleseren din mangler funksjonaliteten som kreves for å åpne slike delinger. Vennligst prøv igjen med en annen nettleser.", + "e2e_unavailable_secure": "Denne delte posisjonen er passordbeskyttet. Du kan ikke åpne denne delingen fordi du ikke bruker HTTPS for øyeblikket. Vennligst last inn siden med HTTPS og prøv igjen.", + "e2e_incorrect": "Passordet du skrev inn er feil. Vennligst prøv igjen.", + "e2e_password_prompt": "Denne delte posisjonen er passordbeskyttet Vennligst skriv inn passordet for å få tilgang til delingen.", + "e2e_placeholder": "Skriv inn passord", + "e2e_title": "Passordbeskyttet", + "last_update_days": "{{time}}d siden", + "last_update_hours": "{{time}}t siden", + "last_update_minutes": "{{time}}m siden", + "last_update_seconds": "{{time}}s siden", + "btn_show_all": "Vis alle", + "btn_close": "Lukk", + "control_show_self": "Vis posisjonen min", + "dialog_user_navigate": "Naviger til", + "dialog_user_follow": "Vis på kartet", + "dialog_active_head": "Aktive brukere", + "google_play_badge_text": "Tilgjengelig på Google Play", + "f_droid_badge_text": "Tilgjengelig på F-Droid" +} diff --git a/frontend/assets/lang/nl.json b/frontend/assets/lang/nl.json new file mode 100644 index 0000000..fd67b17 --- /dev/null +++ b/frontend/assets/lang/nl.json @@ -0,0 +1,36 @@ +{ + "dialog_expired_body": "De locatiedeling is verlopen.", + "f_droid_badge_url": "https://fdroid.gitlab.io/artwork/badge/get-it-on-nl.png", + "google_play_badge_url": "https://play.google.com/intl/en_us/badges/static/images/badges/nl_badge_web_generic.png", + "btn_dismiss": "Sluiten", + "status_offline": "Offline", + "status_expired": "Verlopen", + "dialog_connection_body": "De verbinding met de Hauk server is verbroken. Hauk zal proberen om opnieuw te verbinden in de achtergrond. Controleer alstublieft dat u een werkende internetverbinding heeft.", + "dialog_connection_head": "Verbindingsfout", + "dialog_expired_head": "Locatiedeling verlopen", + "point_app_to": "Stuur de Hauk app naar deze server om uw locatie te delen:", + "gnss_signal_body": "Verzender is aan het wachten op GPS signaal", + "gnss_signal_head": "Wacht alstublieft", + "expired_body": "The locatiedeling die u probeerde te bekijken kon niet gevonden worden op de server. Als deze link eerder wel werkte, dan zou de sessie verlopen kunnen zijn.", + "expired_head": "Locatie verlopen", + "btn_decrypt": "Ontgrendelen", + "btn_cancel": "Annuleren", + "last_update_days": "{{time}}d geleden", + "last_update_hours": "{{time}}u geleden", + "last_update_minutes": "{{time}}m geleden", + "last_update_seconds": "{{time}}s geleden", + "e2e_unsupported": "Deze locatiedeling is met wachtwoord beveiligd. Uw browser mist functionaliteit vereist om deze locatiedelingen te openen. Probeer het alstublieft met een andere browser.", + "e2e_unavailable_secure": "Deze locatiedeling is met wachtwoord beveiligd. U kunt de locatiedeling niet ontgrendelen omdat u geen HTTPS gebruikt. Zorg ervoor dat u met HTTPS verbindt, en probeer het dan opnieuw.", + "e2e_incorrect": "Het ingevoerde wachtwoord is incorrect. Probeer het alstublieft nog eens.", + "e2e_password_prompt": "Deze deling is met wachtwoord beveiligd. Vul het wachtwoord in om toegang te krijgen tot de deling.", + "e2e_placeholder": "Voer wachtwoord in", + "e2e_title": "Beveiligd met wachtwoord", + "google_play_badge_text": "Download op Google Play", + "f_droid_badge_text": "Download op F-Droid", + "btn_show_all": "Toon alle", + "btn_close": "Sluiten", + "control_show_self": "Toon mijn locatie", + "dialog_user_navigate": "Navigeer naar", + "dialog_user_follow": "Toon op kaart", + "dialog_active_head": "Actieve gebruikers" +} diff --git a/frontend/assets/lang/nn.json b/frontend/assets/lang/nn.json new file mode 100644 index 0000000..8dac2ca --- /dev/null +++ b/frontend/assets/lang/nn.json @@ -0,0 +1,36 @@ +{ + "f_droid_badge_url": "https://fdroid.gitlab.io/artwork/badge/get-it-on-no.png", + "google_play_badge_url": "https://play.google.com/intl/en_us/badges/static/images/badges/no_badge_web_generic.png", + "btn_dismiss": "Lukk", + "status_offline": "Fråkopla", + "status_expired": "Gått ut", + "dialog_connection_body": "Tilkoplinga til Hauk-serveren forsvann. Hauk vil prøva å kopla til igjen i bakgrunnen. Vennlegast sjekk at du er tilkopla nett.", + "dialog_connection_head": "Tilkoplingsfeil", + "dialog_expired_body": "Den delte posisjonen du følgde har gått ut.", + "dialog_expired_head": "Deling gått ut", + "point_app_to": "Skriv inn følgjande link i Hauk-appen for å dela posisjonen din:", + "gnss_signal_body": "Sendar ventar på GPS-signal", + "gnss_signal_head": "Ver vennleg og vent", + "expired_body": "Den delte posisjonen du prøvde å opna vart ikkje funne på serveren. Viss denne linken verkte tidlegare, kan det hendet at delingen har gått ut.", + "expired_head": "Posisjon gått ut", + "btn_decrypt": "Opne", + "btn_cancel": "Avbryt", + "e2e_unsupported": "Denne delte posisjonen krev passord for å opnast. Nettlesaren din mangler dei funksjonane som krevjast for å opna slike delingar. Ver vennleg og prøv igjen med ein annan nettlesar.", + "e2e_unavailable_secure": "Denne delte posisjonen krev passord for å opnast. Du kan ikkje opna denne delingen fordi du ikkje bruker HTTPS. Ver venleg og last inn sidan med HTTPS og prøv igjen.", + "e2e_incorrect": "Passordet du skreiv inn er feil. Ver vennleg og prøv igjen.", + "e2e_password_prompt": "Denne delte posisjonen krev passord for å opnast. Ver vennleg og skriv inn passordet for å få tilgang til delinga.", + "e2e_placeholder": "Skriv inn passord", + "e2e_title": "Passord krevjast", + "last_update_days": "{{time}}d sidan", + "last_update_hours": "{{time}}t sidan", + "last_update_minutes": "{{time}}m sidan", + "last_update_seconds": "{{time}}s sidan", + "btn_show_all": "Vis alle", + "btn_close": "Lukk", + "control_show_self": "Vis posisjonen min", + "dialog_user_navigate": "Naviger til", + "dialog_user_follow": "Vis på kartet", + "dialog_active_head": "Aktive brukarar", + "google_play_badge_text": "Tilgjengeleg på Google Play", + "f_droid_badge_text": "Tilgjengeleg på F-Droid" +} diff --git a/frontend/assets/lang/pl.json b/frontend/assets/lang/pl.json new file mode 100644 index 0000000..80f9410 --- /dev/null +++ b/frontend/assets/lang/pl.json @@ -0,0 +1,36 @@ +{ + "google_play_badge_text": "Pobierz z Google Play", + "google_play_badge_url": "https://play.google.com/intl/en_us/badges/static/images/badges/pl_badge_web_generic.png", + "f_droid_badge_text": "Pobierz z F-Droid", + "f_droid_badge_url": "https://fdroid.gitlab.io/artwork/badge/get-it-on-pl.png", + "btn_show_all": "Pokaż wszystko", + "btn_close": "Zamknij", + "btn_decrypt": "Odblokuj", + "btn_cancel": "Anuluj", + "btn_dismiss": "Odrzuć", + "last_update_days": "{{time}} dni temu", + "last_update_hours": "{{time}}h temu", + "last_update_minutes": "{{time}}m temu", + "last_update_seconds": "{{time}s temu", + "control_show_self": "Pokaż moją lokalizację", + "status_offline": "Offline", + "status_expired": "Wygasło", + "dialog_user_navigate": "Nawiguj do", + "dialog_user_follow": "Pokaż na mapie", + "dialog_active_head": "Aktywni użytkownicy", + "dialog_connection_body": "Połączenie z serwerem Hauk zostało utracone. Hauk spróbuje odzyskać połączenie. Proszę sprawdzić swoje połączenie sieciowe.", + "dialog_connection_head": "Błąd połączenia", + "dialog_expired_body": "Ta sesja udostępniania wygasła.", + "dialog_expired_head": "Sesja wygasła", + "point_app_to": "Wprowadź w aplikacji Hauk ten adres serwera:", + "gnss_signal_body": "Nadawca oczekuje na sygnał GPS", + "gnss_signal_head": "Proszę czekać", + "e2e_unsupported": "Ta sesja jest chroniona hasłem. Twoja przeglądarka nie obsługuje funkcji potrzebnych do otwierania takich sesji. Spróbuj użyć innej przeglądarki.", + "e2e_unavailable_secure": "Ta sesja jest chroniona hasłem. Nie można jej odblokować, gdyż nie używasz protokołu HTTPS. Upewnij się, że używasz protokołu HTTPS i spróbuj ponownie.", + "e2e_incorrect": "Niepoprawne hasło. Spróbuj ponownie.", + "e2e_password_prompt": "Ta sesja jest chroniona hasłem. Wprowadź hasło.", + "e2e_placeholder": "Wprowadź hasło", + "e2e_title": "Chronione hasłem", + "expired_body": "Lokalizacja którą próbujesz zobaczyć nie została odnaleziona na serwerze. Jeśli ten link wcześniej działał, oznacza to, że sesja udostępniania mogła wygasnąć.", + "expired_head": "Lokalizacja wygasła" +} diff --git a/frontend/assets/lang/pt_BR.json b/frontend/assets/lang/pt_BR.json new file mode 100644 index 0000000..ef3f167 --- /dev/null +++ b/frontend/assets/lang/pt_BR.json @@ -0,0 +1,36 @@ +{ + "expired_head": "Localização Expirada", + "expired_body": "O local compartilhado que você tentou acessar não foi encontrado no servidor. Se esse link funcionou antes, o compartilhamento pode ter expirado.", + "e2e_title": "Senha protegia", + "e2e_placeholder": "Digite a senha", + "e2e_password_prompt": "Esse compartilhamento é protegido por senha. Digite a senha para acessar o compartilhamento.", + "e2e_incorrect": "A senha que você digitou estava errada. Por favor, tente novamente.", + "e2e_unavailable_secure": "Esse compartilhamento é protegido por senha. Você não pode desbloquear esse compartilhamento porque não está usando HTTPS. Verifique se você está usando HTTPS e tente novamente.", + "e2e_unsupported": "Esse compartilhamento é protegido por senha. Seu navegador não possui a funcionalidade necessária para abrir esses compartilhamentos. Tente novamente com outro navegador da web.", + "gnss_signal_head": "Por favor, aguarde", + "gnss_signal_body": "Remetente está aguardando sinal de GPS", + "point_app_to": "Aponte o aplicativo Hauk para este servidor para compartilhar sua localização:", + "dialog_expired_head": "Compartilhamento expirado", + "dialog_expired_body": "Este compartilhamento de local expirou.", + "dialog_connection_head": "Erro na conecção", + "dialog_connection_body": "A conexão com o servidor Hauk foi perdida. Hauk tentará se reconectar em segundo plano. Verifique se você possui conectividade de rede.", + "dialog_active_head": "Usuarios Ativos", + "dialog_user_follow": "Mostrar no mapa", + "dialog_user_navigate": "Navegar para", + "status_expired": "Expirado", + "status_offline": "Offline", + "control_show_self": "Mostrar minha localização", + "last_update_seconds": "{{time}} segundos atras", + "last_update_minutes": "{{time}} minutos atras", + "last_update_hours": "{{time}} horas atras", + "last_update_days": "{{time}} dias atras", + "btn_dismiss": "Dispensar", + "btn_cancel": "Cancelar", + "btn_decrypt": "Desbloquear", + "btn_close": "Fechar", + "btn_show_all": "Mostrar Todos", + "f_droid_badge_url": "https://fdroid.gitlab.io/artwork/badge/get-it-on-pt-br.png", + "f_droid_badge_text": "Obtê-lo no F-Droid", + "google_play_badge_url": "https://play.google.com/intl/pt_br/badges/static/images/badges/pt_badge_web_generic.png", + "google_play_badge_text": "Obtê-lo no Google Play" +} diff --git a/frontend/assets/lang/ro.json b/frontend/assets/lang/ro.json new file mode 100644 index 0000000..035bc62 --- /dev/null +++ b/frontend/assets/lang/ro.json @@ -0,0 +1,36 @@ +{ + "dialog_connection_body": "S-a pierdut conexiunea la serverul. Hauk va încerca să se reconecteze în fundal. Vă rugăm să verificați dacă aveți conectivitate la rețea.", + "e2e_unsupported": "Această partajare este protejată cu parolă. Aplicația dumneavoastră de navigare pe internet nu are funcționalitatea necesară pentru a deschide o asemenea partajare. Vă rugăm să reîncercați cu alta.", + "e2e_unavailable_secure": "Această partajare este protejată cu parolă. Nu puteți debloca această partajare pentru ca nu utilizați HTTPS. Vă rugăm să vă asigurați că folosiți HTTPS, apoi reîncercați.", + "e2e_incorrect": "Parola introdusă este eronată. Vă rugăm să reîncercați.", + "e2e_password_prompt": "Această partajare este protejată cu parolă. Vă rugăm să introduceți parola pentru a o accesa.", + "expired_body": "Locația partajată pe care ați încercat să o accesați nu a fost găsită pe server. Dacă această adresă a funcționat înainte, se poate să fi expirat deja.", + "btn_cancel": "Anulare", + "btn_dismiss": "Revocare", + "dialog_connection_head": "Eroare de conexiune", + "google_play_badge_url": "https://play.google.com/intl/ro_ro/badges/static/images/badges/ro_badge_web_generic.png", + "f_droid_badge_url": "https://fdroid.gitlab.io/artwork/badge/get-it-on-ro.png", + "btn_decrypt": "Deblocare", + "last_update_days": "Acum {{time}}z", + "last_update_hours": "Acum {{time}}o", + "last_update_minutes": "Acum {{time}}m", + "last_update_seconds": "Acum {{time}}s", + "status_offline": "Deconectat", + "status_expired": "Expirată", + "dialog_expired_body": "Această partajare de locației a expirat.", + "dialog_expired_head": "Partajare expirată", + "point_app_to": "Folosiți aplicația Hauk cu acest server pentru a vă partaja locația:", + "gnss_signal_body": "Expeditorul așteaptă semnalul GPS", + "gnss_signal_head": "Vă rugăm să așteptați", + "e2e_placeholder": "Introduceți parola", + "e2e_title": "Protejată cu parolă", + "expired_head": "Sesiune expirată", + "btn_show_all": "Arată toate", + "btn_close": "Închide", + "control_show_self": "Arată locația mea", + "dialog_user_navigate": "Navighează", + "dialog_user_follow": "Arată pe hartă", + "dialog_active_head": "Persoane active", + "google_play_badge_text": "Acum pe Google Play", + "f_droid_badge_text": "Acum pe F-Droid" +} diff --git a/frontend/assets/lang/ru.json b/frontend/assets/lang/ru.json new file mode 100644 index 0000000..098df68 --- /dev/null +++ b/frontend/assets/lang/ru.json @@ -0,0 +1,32 @@ +{ + "f_droid_badge_url": "https://fdroid.gitlab.io/artwork/badge/get-it-on-ru.png", + "google_play_badge_url": "https://play.google.com/intl/en_us/badges/static/images/badges/ru_badge_web_generic.png", + "btn_dismiss": "Отменить", + "status_offline": "Не подключено", + "status_expired": "Истекло", + "dialog_connection_body": "Соединение с сервером Hauk потеряно. Hauk будет пытаться переподключится в фоне. Пожалуйста проверьте соединение с сетью.", + "dialog_connection_head": "Ошибка подлючения", + "dialog_expired_body": "Эта шара устарела.", + "dialog_expired_head": "Шара устарела", + "point_app_to": "Укажите этот сервер в приложении Hauk что бы делится своим местоположением:", + "gnss_signal_body": "Ждем сигнала GPS", + "gnss_signal_head": "Пожалуйста подождите", + "expired_body": "Шара к которой вы пытаетесь получить доступ не найдена на сервере. Если эта ссылка работала в прошлом, возможно истекло время жизни шары.", + "expired_head": "Местоположение устарело", + "btn_decrypt": "Разблокировать", + "btn_cancel": "Отмена", + "e2e_placeholder": "Введите пароль", + "google_play_badge_text": "Установить из Google Play", + "btn_show_all": "Показать все", + "btn_close": "Закрыть", + "last_update_days": "был {{time}} дней назад", + "last_update_hours": "был {{time}} час(ов) назад", + "last_update_minutes": "был {{time}} минут назад", + "last_update_seconds": "был {{time}} назад", + "control_show_self": "Показать мое местоположение", + "dialog_user_follow": "Показать на карте", + "dialog_active_head": "Активные пользователи", + "e2e_incorrect": "Неверный пароль. Попробуйте еще раз.", + "e2e_password_prompt": "Это местоположение защищено паролем. Пожалуйста, введите пароль.", + "e2e_title": "Защищено паролем" +} diff --git a/frontend/assets/lang/tr.json b/frontend/assets/lang/tr.json new file mode 100644 index 0000000..d6ca6ec --- /dev/null +++ b/frontend/assets/lang/tr.json @@ -0,0 +1,36 @@ +{ + "google_play_badge_text": "Google Play'den alın", + "google_play_badge_url": "https://play.google.com/intl/en_us/badges/static/images/badges/tr_badge_web_generic.png", + "f_droid_badge_text": "F-Droid'den alın", + "f_droid_badge_url": "https://fdroid.gitlab.io/artwork/badge/get-it-on-tr.png", + "btn_show_all": "Hepsini göster", + "btn_close": "Kapat", + "btn_decrypt": "Kilidi aç", + "btn_cancel": "Vazgeç", + "btn_dismiss": "Yoksay", + "last_update_days": "{{time}} gün önce", + "last_update_hours": "{{time}} saat önce", + "last_update_minutes": "{{time}} dakika önce", + "last_update_seconds": "{{time}} saniye önce", + "control_show_self": "Konumumu göster", + "status_offline": "Çevrimdışı", + "status_expired": "Süresi doldu", + "dialog_user_navigate": "Şu dizine git", + "dialog_user_follow": "Haritada göster", + "dialog_active_head": "Aktif kullanıcılar", + "dialog_connection_body": "Hauk sunucusuna bağlantı koptu. Hauk arkaplanda bağlanmayı tekrar deneyecek. İnternet bağlantınızın çalışır olduğundan emin olunuz.", + "dialog_connection_head": "Bağlantı hatası", + "dialog_expired_body": "Bu konumun paylaşım süresi doldu.", + "dialog_expired_head": "Paylaşım süresi doldu", + "point_app_to": "Konumunuzu paylaşmak için Hauk uygulamasını şu sunucuya yönlendirin:", + "gnss_signal_body": "GPS sinyali bekleniyor", + "gnss_signal_head": "Lütfen bekleyin", + "e2e_unsupported": "Bu paylaşım parola korumalı. Tarayıcınız paylaşımın açılabilmesi için gerekli özellikleri karşılamıyor. Lütfen başka bir tarayıcıyla tekrar deneyiniz.", + "e2e_unavailable_secure": "Bu paylaşım parola korumalı. Bu paylaşımın kilidini açamazsınız çünkü HTTPS kullanmıyorsunuz. Lütfen HTTPS kullandığınıza emin olun ve tekrar deneyiniz.", + "e2e_incorrect": "Girdiğiniz parola yanlış. Lütfen tekrar deneyiniz.", + "e2e_password_prompt": "Bu paylaşım parola korumalı. Lütfen erişebilmek için parolayı giriniz.", + "e2e_placeholder": "Parolayı giriniz", + "e2e_title": "Parola korumalı", + "expired_body": "Ulaşmaya çalıştığınız paylaşılmış lokasyon sunucuda bulunamadı. Eğer bu link daha önce çalıştıysa, paylaşımın süresi dolmuş olabilir.", + "expired_head": "Konum süresi doldu" +} diff --git a/frontend/assets/lang/uk.json b/frontend/assets/lang/uk.json new file mode 100644 index 0000000..542aae4 --- /dev/null +++ b/frontend/assets/lang/uk.json @@ -0,0 +1,16 @@ +{ + "f_droid_badge_url": "https://fdroid.gitlab.io/artwork/badge/get-it-on-ua.png", + "google_play_badge_url": "https://play.google.com/intl/en_us/badges/static/images/badges/ua_badge_web_generic.png", + "btn_dismiss": "Відмінити", + "status_offline": "Офлайн", + "status_expired": "Термін дії закінчився", + "dialog_connection_body": "З’єднання з сервером Hauk було втрачено. Hauk спробує знову підключитися на задньому плані. Перевірте наявність у вас підключення до мережі.", + "dialog_connection_head": "Помилка з'єднання", + "dialog_expired_body": "Термін дії цієї шари закінчився.", + "dialog_expired_head": "Шара недійсна", + "point_app_to": "Налаштуйте Hauk на цей сервер щоб поділитися своїм місцеположенням:", + "gnss_signal_body": "Чекаємо на сигнал GPS", + "gnss_signal_head": "Будь ласка зачекайте", + "expired_body": "Розшарене місцезнаходження, до якого ви намагалися отримати доступ, не знайдено на сервері. Якщо це посилання працювало раніше, шара, можливо, померла.", + "expired_head": "Термін дії закінчився" +} diff --git a/frontend/assets/location-pending.svg b/frontend/assets/location-pending.svg new file mode 100644 index 0000000..e4d7de9 --- /dev/null +++ b/frontend/assets/location-pending.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/frontend/assets/logo.svg b/frontend/assets/logo.svg new file mode 100644 index 0000000..4c5f6ec --- /dev/null +++ b/frontend/assets/logo.svg @@ -0,0 +1,30 @@ + + + + + + + + + + diff --git a/frontend/assets/markers/moving-dead.svg b/frontend/assets/markers/moving-dead.svg new file mode 100644 index 0000000..b1faaa2 --- /dev/null +++ b/frontend/assets/markers/moving-dead.svg @@ -0,0 +1,26 @@ + + + + + + + + + diff --git a/frontend/assets/markers/moving-live.svg b/frontend/assets/markers/moving-live.svg new file mode 100644 index 0000000..ddf8ea6 --- /dev/null +++ b/frontend/assets/markers/moving-live.svg @@ -0,0 +1,26 @@ + + + + + + + + + diff --git a/frontend/assets/markers/moving-rough.svg b/frontend/assets/markers/moving-rough.svg new file mode 100644 index 0000000..d20fe5b --- /dev/null +++ b/frontend/assets/markers/moving-rough.svg @@ -0,0 +1,26 @@ + + + + + + + + + diff --git a/frontend/assets/markers/still-dead.svg b/frontend/assets/markers/still-dead.svg new file mode 100644 index 0000000..56b7f1e --- /dev/null +++ b/frontend/assets/markers/still-dead.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/assets/markers/still-live.svg b/frontend/assets/markers/still-live.svg new file mode 100644 index 0000000..8aebd2c --- /dev/null +++ b/frontend/assets/markers/still-live.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/assets/markers/still-rough.svg b/frontend/assets/markers/still-rough.svg new file mode 100644 index 0000000..7c35bee --- /dev/null +++ b/frontend/assets/markers/still-rough.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/assets/markers/still-self.svg b/frontend/assets/markers/still-self.svg new file mode 100644 index 0000000..819c90f --- /dev/null +++ b/frontend/assets/markers/still-self.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/embed.go b/frontend/embed.go new file mode 100644 index 0000000..94db92c --- /dev/null +++ b/frontend/embed.go @@ -0,0 +1,6 @@ +package frontend + +import "embed" + +//go:embed index.html main.js style.css lib assets +var Files embed.FS diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..56fc2f7 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + Hauk + + + + + +
+ +
+ + +
+
+
+ +
+
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/lib/leaflet/1.6.0/leaflet.css b/frontend/lib/leaflet/1.6.0/leaflet.css new file mode 100644 index 0000000..609a662 --- /dev/null +++ b/frontend/lib/leaflet/1.6.0/leaflet.css @@ -0,0 +1,640 @@ +/* required styles */ + +.leaflet-pane, +.leaflet-tile, +.leaflet-marker-icon, +.leaflet-marker-shadow, +.leaflet-tile-container, +.leaflet-pane > svg, +.leaflet-pane > canvas, +.leaflet-zoom-box, +.leaflet-image-layer, +.leaflet-layer { + position: absolute; + left: 0; + top: 0; + } +.leaflet-container { + overflow: hidden; + } +.leaflet-tile, +.leaflet-marker-icon, +.leaflet-marker-shadow { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + -webkit-user-drag: none; + } +/* Prevents IE11 from highlighting tiles in blue */ +.leaflet-tile::selection { + background: transparent; +} +/* Safari renders non-retina tile on retina better with this, but Chrome is worse */ +.leaflet-safari .leaflet-tile { + image-rendering: -webkit-optimize-contrast; + } +/* hack that prevents hw layers "stretching" when loading new tiles */ +.leaflet-safari .leaflet-tile-container { + width: 1600px; + height: 1600px; + -webkit-transform-origin: 0 0; + } +.leaflet-marker-icon, +.leaflet-marker-shadow { + display: block; + } +/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */ +/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */ +.leaflet-container .leaflet-overlay-pane svg, +.leaflet-container .leaflet-marker-pane img, +.leaflet-container .leaflet-shadow-pane img, +.leaflet-container .leaflet-tile-pane img, +.leaflet-container img.leaflet-image-layer, +.leaflet-container .leaflet-tile { + max-width: none !important; + max-height: none !important; + } + +.leaflet-container.leaflet-touch-zoom { + -ms-touch-action: pan-x pan-y; + touch-action: pan-x pan-y; + } +.leaflet-container.leaflet-touch-drag { + -ms-touch-action: pinch-zoom; + /* Fallback for FF which doesn't support pinch-zoom */ + touch-action: none; + touch-action: pinch-zoom; +} +.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom { + -ms-touch-action: none; + touch-action: none; +} +.leaflet-container { + -webkit-tap-highlight-color: transparent; +} +.leaflet-container a { + -webkit-tap-highlight-color: rgba(51, 181, 229, 0.4); +} +.leaflet-tile { + filter: inherit; + visibility: hidden; + } +.leaflet-tile-loaded { + visibility: inherit; + } +.leaflet-zoom-box { + width: 0; + height: 0; + -moz-box-sizing: border-box; + box-sizing: border-box; + z-index: 800; + } +/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */ +.leaflet-overlay-pane svg { + -moz-user-select: none; + } + +.leaflet-pane { z-index: 400; } + +.leaflet-tile-pane { z-index: 200; } +.leaflet-overlay-pane { z-index: 400; } +.leaflet-shadow-pane { z-index: 500; } +.leaflet-marker-pane { z-index: 600; } +.leaflet-tooltip-pane { z-index: 650; } +.leaflet-popup-pane { z-index: 700; } + +.leaflet-map-pane canvas { z-index: 100; } +.leaflet-map-pane svg { z-index: 200; } + +.leaflet-vml-shape { + width: 1px; + height: 1px; + } +.lvml { + behavior: url(#default#VML); + display: inline-block; + position: absolute; + } + + +/* control positioning */ + +.leaflet-control { + position: relative; + z-index: 800; + pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ + pointer-events: auto; + } +.leaflet-top, +.leaflet-bottom { + position: absolute; + z-index: 1000; + pointer-events: none; + } +.leaflet-top { + top: 0; + } +.leaflet-right { + right: 0; + } +.leaflet-bottom { + bottom: 0; + } +.leaflet-left { + left: 0; + } +.leaflet-control { + float: left; + clear: both; + } +.leaflet-right .leaflet-control { + float: right; + } +.leaflet-top .leaflet-control { + margin-top: 10px; + } +.leaflet-bottom .leaflet-control { + margin-bottom: 10px; + } +.leaflet-left .leaflet-control { + margin-left: 10px; + } +.leaflet-right .leaflet-control { + margin-right: 10px; + } + + +/* zoom and fade animations */ + +.leaflet-fade-anim .leaflet-tile { + will-change: opacity; + } +.leaflet-fade-anim .leaflet-popup { + opacity: 0; + -webkit-transition: opacity 0.2s linear; + -moz-transition: opacity 0.2s linear; + transition: opacity 0.2s linear; + } +.leaflet-fade-anim .leaflet-map-pane .leaflet-popup { + opacity: 1; + } +.leaflet-zoom-animated { + -webkit-transform-origin: 0 0; + -ms-transform-origin: 0 0; + transform-origin: 0 0; + } +.leaflet-zoom-anim .leaflet-zoom-animated { + will-change: transform; + } +.leaflet-zoom-anim .leaflet-zoom-animated { + -webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1); + -moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1); + transition: transform 0.25s cubic-bezier(0,0,0.25,1); + } +.leaflet-zoom-anim .leaflet-tile, +.leaflet-pan-anim .leaflet-tile { + -webkit-transition: none; + -moz-transition: none; + transition: none; + } + +.leaflet-zoom-anim .leaflet-zoom-hide { + visibility: hidden; + } + + +/* cursors */ + +.leaflet-interactive { + cursor: pointer; + } +.leaflet-grab { + cursor: -webkit-grab; + cursor: -moz-grab; + cursor: grab; + } +.leaflet-crosshair, +.leaflet-crosshair .leaflet-interactive { + cursor: crosshair; + } +.leaflet-popup-pane, +.leaflet-control { + cursor: auto; + } +.leaflet-dragging .leaflet-grab, +.leaflet-dragging .leaflet-grab .leaflet-interactive, +.leaflet-dragging .leaflet-marker-draggable { + cursor: move; + cursor: -webkit-grabbing; + cursor: -moz-grabbing; + cursor: grabbing; + } + +/* marker & overlays interactivity */ +.leaflet-marker-icon, +.leaflet-marker-shadow, +.leaflet-image-layer, +.leaflet-pane > svg path, +.leaflet-tile-container { + pointer-events: none; + } + +.leaflet-marker-icon.leaflet-interactive, +.leaflet-image-layer.leaflet-interactive, +.leaflet-pane > svg path.leaflet-interactive, +svg.leaflet-image-layer.leaflet-interactive path { + pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ + pointer-events: auto; + } + +/* visual tweaks */ + +.leaflet-container { + background: #ddd; + outline: 0; + } +.leaflet-container a { + color: #0078A8; + } +.leaflet-container a.leaflet-active { + outline: 2px solid orange; + } +.leaflet-zoom-box { + border: 2px dotted #38f; + background: rgba(255,255,255,0.5); + } + + +/* general typography */ +.leaflet-container { + font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif; + } + + +/* general toolbar styles */ + +.leaflet-bar { + box-shadow: 0 1px 5px rgba(0,0,0,0.65); + border-radius: 4px; + } +.leaflet-bar a, +.leaflet-bar a:hover { + background-color: #fff; + border-bottom: 1px solid #ccc; + width: 26px; + height: 26px; + line-height: 26px; + display: block; + text-align: center; + text-decoration: none; + color: black; + } +.leaflet-bar a, +.leaflet-control-layers-toggle { + background-position: 50% 50%; + background-repeat: no-repeat; + display: block; + } +.leaflet-bar a:hover { + background-color: #f4f4f4; + } +.leaflet-bar a:first-child { + border-top-left-radius: 4px; + border-top-right-radius: 4px; + } +.leaflet-bar a:last-child { + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + border-bottom: none; + } +.leaflet-bar a.leaflet-disabled { + cursor: default; + background-color: #f4f4f4; + color: #bbb; + } + +.leaflet-touch .leaflet-bar a { + width: 30px; + height: 30px; + line-height: 30px; + } +.leaflet-touch .leaflet-bar a:first-child { + border-top-left-radius: 2px; + border-top-right-radius: 2px; + } +.leaflet-touch .leaflet-bar a:last-child { + border-bottom-left-radius: 2px; + border-bottom-right-radius: 2px; + } + +/* zoom control */ + +.leaflet-control-zoom-in, +.leaflet-control-zoom-out { + font: bold 18px 'Lucida Console', Monaco, monospace; + text-indent: 1px; + } + +.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out { + font-size: 22px; + } + + +/* layers control */ + +.leaflet-control-layers { + box-shadow: 0 1px 5px rgba(0,0,0,0.4); + background: #fff; + border-radius: 5px; + } +.leaflet-control-layers-toggle { + background-image: url(images/layers.png); + width: 36px; + height: 36px; + } +.leaflet-retina .leaflet-control-layers-toggle { + background-image: url(images/layers-2x.png); + background-size: 26px 26px; + } +.leaflet-touch .leaflet-control-layers-toggle { + width: 44px; + height: 44px; + } +.leaflet-control-layers .leaflet-control-layers-list, +.leaflet-control-layers-expanded .leaflet-control-layers-toggle { + display: none; + } +.leaflet-control-layers-expanded .leaflet-control-layers-list { + display: block; + position: relative; + } +.leaflet-control-layers-expanded { + padding: 6px 10px 6px 6px; + color: #333; + background: #fff; + } +.leaflet-control-layers-scrollbar { + overflow-y: scroll; + overflow-x: hidden; + padding-right: 5px; + } +.leaflet-control-layers-selector { + margin-top: 2px; + position: relative; + top: 1px; + } +.leaflet-control-layers label { + display: block; + } +.leaflet-control-layers-separator { + height: 0; + border-top: 1px solid #ddd; + margin: 5px -10px 5px -6px; + } + +/* Default icon URLs */ +.leaflet-default-icon-path { + background-image: url(images/marker-icon.png); + } + + +/* attribution and scale controls */ + +.leaflet-container .leaflet-control-attribution { + background: #fff; + background: rgba(255, 255, 255, 0.7); + margin: 0; + } +.leaflet-control-attribution, +.leaflet-control-scale-line { + padding: 0 5px; + color: #333; + } +.leaflet-control-attribution a { + text-decoration: none; + } +.leaflet-control-attribution a:hover { + text-decoration: underline; + } +.leaflet-container .leaflet-control-attribution, +.leaflet-container .leaflet-control-scale { + font-size: 11px; + } +.leaflet-left .leaflet-control-scale { + margin-left: 5px; + } +.leaflet-bottom .leaflet-control-scale { + margin-bottom: 5px; + } +.leaflet-control-scale-line { + border: 2px solid #777; + border-top: none; + line-height: 1.1; + padding: 2px 5px 1px; + font-size: 11px; + white-space: nowrap; + overflow: hidden; + -moz-box-sizing: border-box; + box-sizing: border-box; + + background: #fff; + background: rgba(255, 255, 255, 0.5); + } +.leaflet-control-scale-line:not(:first-child) { + border-top: 2px solid #777; + border-bottom: none; + margin-top: -2px; + } +.leaflet-control-scale-line:not(:first-child):not(:last-child) { + border-bottom: 2px solid #777; + } + +.leaflet-touch .leaflet-control-attribution, +.leaflet-touch .leaflet-control-layers, +.leaflet-touch .leaflet-bar { + box-shadow: none; + } +.leaflet-touch .leaflet-control-layers, +.leaflet-touch .leaflet-bar { + border: 2px solid rgba(0,0,0,0.2); + background-clip: padding-box; + } + + +/* popup */ + +.leaflet-popup { + position: absolute; + text-align: center; + margin-bottom: 20px; + } +.leaflet-popup-content-wrapper { + padding: 1px; + text-align: left; + border-radius: 12px; + } +.leaflet-popup-content { + margin: 13px 19px; + line-height: 1.4; + } +.leaflet-popup-content p { + margin: 18px 0; + } +.leaflet-popup-tip-container { + width: 40px; + height: 20px; + position: absolute; + left: 50%; + margin-left: -20px; + overflow: hidden; + pointer-events: none; + } +.leaflet-popup-tip { + width: 17px; + height: 17px; + padding: 1px; + + margin: -10px auto 0; + + -webkit-transform: rotate(45deg); + -moz-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); + } +.leaflet-popup-content-wrapper, +.leaflet-popup-tip { + background: white; + color: #333; + box-shadow: 0 3px 14px rgba(0,0,0,0.4); + } +.leaflet-container a.leaflet-popup-close-button { + position: absolute; + top: 0; + right: 0; + padding: 4px 4px 0 0; + border: none; + text-align: center; + width: 18px; + height: 14px; + font: 16px/14px Tahoma, Verdana, sans-serif; + color: #c3c3c3; + text-decoration: none; + font-weight: bold; + background: transparent; + } +.leaflet-container a.leaflet-popup-close-button:hover { + color: #999; + } +.leaflet-popup-scrolled { + overflow: auto; + border-bottom: 1px solid #ddd; + border-top: 1px solid #ddd; + } + +.leaflet-oldie .leaflet-popup-content-wrapper { + zoom: 1; + } +.leaflet-oldie .leaflet-popup-tip { + width: 24px; + margin: 0 auto; + + -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)"; + filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678); + } +.leaflet-oldie .leaflet-popup-tip-container { + margin-top: -1px; + } + +.leaflet-oldie .leaflet-control-zoom, +.leaflet-oldie .leaflet-control-layers, +.leaflet-oldie .leaflet-popup-content-wrapper, +.leaflet-oldie .leaflet-popup-tip { + border: 1px solid #999; + } + + +/* div icon */ + +.leaflet-div-icon { + background: #fff; + border: 1px solid #666; + } + + +/* Tooltip */ +/* Base styles for the element that has a tooltip */ +.leaflet-tooltip { + position: absolute; + padding: 6px; + background-color: #fff; + border: 1px solid #fff; + border-radius: 3px; + color: #222; + white-space: nowrap; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + pointer-events: none; + box-shadow: 0 1px 3px rgba(0,0,0,0.4); + } +.leaflet-tooltip.leaflet-clickable { + cursor: pointer; + pointer-events: auto; + } +.leaflet-tooltip-top:before, +.leaflet-tooltip-bottom:before, +.leaflet-tooltip-left:before, +.leaflet-tooltip-right:before { + position: absolute; + pointer-events: none; + border: 6px solid transparent; + background: transparent; + content: ""; + } + +/* Directions */ + +.leaflet-tooltip-bottom { + margin-top: 6px; +} +.leaflet-tooltip-top { + margin-top: -6px; +} +.leaflet-tooltip-bottom:before, +.leaflet-tooltip-top:before { + left: 50%; + margin-left: -6px; + } +.leaflet-tooltip-top:before { + bottom: 0; + margin-bottom: -12px; + border-top-color: #fff; + } +.leaflet-tooltip-bottom:before { + top: 0; + margin-top: -12px; + margin-left: -6px; + border-bottom-color: #fff; + } +.leaflet-tooltip-left { + margin-left: -6px; +} +.leaflet-tooltip-right { + margin-left: 6px; +} +.leaflet-tooltip-left:before, +.leaflet-tooltip-right:before { + top: 50%; + margin-top: -6px; + } +.leaflet-tooltip-left:before { + right: 0; + margin-right: -12px; + border-left-color: #fff; + } +.leaflet-tooltip-right:before { + left: 0; + margin-left: -12px; + border-right-color: #fff; + } diff --git a/frontend/lib/leaflet/1.6.0/leaflet.js b/frontend/lib/leaflet/1.6.0/leaflet.js new file mode 100644 index 0000000..bc9ef0f --- /dev/null +++ b/frontend/lib/leaflet/1.6.0/leaflet.js @@ -0,0 +1,5 @@ +/* @preserve + * Leaflet 1.6.0+Detached: 0c81bdf904d864fd12a286e3d1979f47aba17991.0c81bdf, a JS library for interactive maps. http://leafletjs.com + * (c) 2010-2019 Vladimir Agafonkin, (c) 2010-2011 CloudMade + */ +!function(t,i){"object"==typeof exports&&"undefined"!=typeof module?i(exports):"function"==typeof define&&define.amd?define(["exports"],i):i(t.L={})}(this,function(t){"use strict";var i=Object.freeze;function h(t){var i,e,n,o;for(e=1,n=arguments.length;e=this.min.x&&e.x<=this.max.x&&i.y>=this.min.y&&e.y<=this.max.y},intersects:function(t){t=R(t);var i=this.min,e=this.max,n=t.min,o=t.max,s=o.x>=i.x&&n.x<=e.x,r=o.y>=i.y&&n.y<=e.y;return s&&r},overlaps:function(t){t=R(t);var i=this.min,e=this.max,n=t.min,o=t.max,s=o.x>i.x&&n.xi.y&&n.y=n.lat&&e.lat<=o.lat&&i.lng>=n.lng&&e.lng<=o.lng},intersects:function(t){t=D(t);var i=this._southWest,e=this._northEast,n=t.getSouthWest(),o=t.getNorthEast(),s=o.lat>=i.lat&&n.lat<=e.lat,r=o.lng>=i.lng&&n.lng<=e.lng;return s&&r},overlaps:function(t){t=D(t);var i=this._southWest,e=this._northEast,n=t.getSouthWest(),o=t.getNorthEast(),s=o.lat>i.lat&&n.lati.lng&&n.lng';var i=t.firstChild;return i.style.behavior="url(#default#VML)",i&&"object"==typeof i.adj}catch(t){return!1}}();function Bt(t){return 0<=navigator.userAgent.toLowerCase().indexOf(t)}var At=(Object.freeze||Object)({ie:it,ielt9:et,edge:nt,webkit:ot,android:st,android23:rt,androidStock:ht,opera:ut,chrome:lt,gecko:ct,safari:_t,phantom:dt,opera12:pt,win:mt,ie3d:ft,webkit3d:gt,gecko3d:vt,any3d:yt,mobile:xt,mobileWebkit:wt,mobileWebkit3d:Pt,msPointer:Lt,pointer:bt,touch:Tt,mobileOpera:zt,mobileGecko:Mt,retina:Ct,passiveEvents:Et,canvas:St,svg:Zt,vml:kt}),It=Lt?"MSPointerDown":"pointerdown",Ot=Lt?"MSPointerMove":"pointermove",Rt=Lt?"MSPointerUp":"pointerup",Nt=Lt?"MSPointerCancel":"pointercancel",Dt=["INPUT","SELECT","OPTION"],jt={},Wt=!1,Ht=0;function Ft(t,i,e,n){return"touchstart"===i?function(t,i,e){var n=a(function(t){if("mouse"!==t.pointerType&&t.MSPOINTER_TYPE_MOUSE&&t.pointerType!==t.MSPOINTER_TYPE_MOUSE){if(!(Dt.indexOf(t.target.tagName)<0))return;ji(t)}Gt(t,i)});t["_leaflet_touchstart"+e]=n,t.addEventListener(It,n,!1),Wt||(document.documentElement.addEventListener(It,Ut,!0),document.documentElement.addEventListener(Ot,Vt,!0),document.documentElement.addEventListener(Rt,qt,!0),document.documentElement.addEventListener(Nt,qt,!0),Wt=!0)}(t,e,n):"touchmove"===i?function(t,i,e){function n(t){(t.pointerType!==t.MSPOINTER_TYPE_MOUSE&&"mouse"!==t.pointerType||0!==t.buttons)&&Gt(t,i)}t["_leaflet_touchmove"+e]=n,t.addEventListener(Ot,n,!1)}(t,e,n):"touchend"===i&&function(t,i,e){function n(t){Gt(t,i)}t["_leaflet_touchend"+e]=n,t.addEventListener(Rt,n,!1),t.addEventListener(Nt,n,!1)}(t,e,n),this}function Ut(t){jt[t.pointerId]=t,Ht++}function Vt(t){jt[t.pointerId]&&(jt[t.pointerId]=t)}function qt(t){delete jt[t.pointerId],Ht--}function Gt(t,i){for(var e in t.touches=[],jt)t.touches.push(jt[e]);t.changedTouches=[t],i(t)}var Kt=Lt?"MSPointerDown":bt?"pointerdown":"touchstart",Yt=Lt?"MSPointerUp":bt?"pointerup":"touchend",Xt="_leaflet_";function Jt(t,o,i){var s,r,a=!1;function e(t){var i;if(bt){if(!nt||"mouse"===t.pointerType)return;i=Ht}else i=t.touches.length;if(!(1this.options.maxZoom)?this.setZoom(t):this},panInsideBounds:function(t,i){this._enforcingBounds=!0;var e=this.getCenter(),n=this._limitCenter(e,this._zoom,D(t));return e.equals(n)||this.panTo(n,i),this._enforcingBounds=!1,this},panInside:function(t,i){var e=I((i=i||{}).paddingTopLeft||i.padding||[0,0]),n=I(i.paddingBottomRight||i.padding||[0,0]),o=this.getCenter(),s=this.project(o),r=this.project(t),a=this.getPixelBounds(),h=a.getSize().divideBy(2),u=R([a.min.add(e),a.max.subtract(n)]);if(!u.contains(r)){this._enforcingBounds=!0;var l=s.subtract(r),c=I(r.x+l.x,r.y+l.y);(r.xu.max.x)&&(c.x=s.x-l.x,0u.max.y)&&(c.y=s.y-l.y,0=this.options.transform3DLimit&&this._resetView(this.getCenter(),this.getZoom())},_findEventTargets:function(t,i){for(var e,n=[],o="mouseout"===i||"mouseover"===i,s=t.target||t.srcElement,r=!1;s;){if((e=this._targets[u(s)])&&("click"===i||"preclick"===i)&&!t._simulated&&this._draggableMoved(e)){r=!0;break}if(e&&e.listens(i,!0)){if(o&&!Yi(s,t))break;if(n.push(e),o)break}if(s===this._container)break;s=s.parentNode}return n.length||r||o||!Yi(s,t)||(n=[this]),n},_handleDOMEvent:function(t){if(this._loaded&&!Ki(t)){var i=t.type;"mousedown"!==i&&"keypress"!==i&&"keyup"!==i&&"keydown"!==i||Mi(t.target||t.srcElement),this._fireDOMEvent(t,i)}},_mouseEvents:["click","dblclick","mouseover","mouseout","contextmenu"],_fireDOMEvent:function(t,i,e){if("click"===t.type){var n=h({},t);n.type="preclick",this._fireDOMEvent(n,n.type,e)}if(!t._stopped&&(e=(e||[]).concat(this._findEventTargets(t,i))).length){var o=e[0];"contextmenu"===i&&o.listens(i,!0)&&ji(t);var s={originalEvent:t};if("keypress"!==t.type&&"keydown"!==t.type&&"keyup"!==t.type){var r=o.getLatLng&&(!o._radius||o._radius<=10);s.containerPoint=r?this.latLngToContainerPoint(o.getLatLng()):this.mouseEventToContainerPoint(t),s.layerPoint=this.containerPointToLayerPoint(s.containerPoint),s.latlng=r?o.getLatLng():this.layerPointToLatLng(s.layerPoint)}for(var a=0;athis.options.zoomAnimationThreshold)return!1;var n=this.getZoomScale(i),o=this._getCenterOffset(t)._divideBy(1-1/n);return!(!0!==e.animate&&!this.getSize().contains(o))&&(M(function(){this._moveStart(!0,!1)._animateZoom(t,i,!0)},this),!0)},_animateZoom:function(t,i,e,n){this._mapPane&&(e&&(this._animatingZoom=!0,this._animateToCenter=t,this._animateToZoom=i,mi(this._mapPane,"leaflet-zoom-anim")),this.fire("zoomanim",{center:t,zoom:i,noUpdate:n}),setTimeout(a(this._onZoomTransitionEnd,this),250))},_onZoomTransitionEnd:function(){this._animatingZoom&&(this._mapPane&&fi(this._mapPane,"leaflet-zoom-anim"),this._animatingZoom=!1,this._move(this._animateToCenter,this._animateToZoom),M(function(){this._moveEnd(!0)},this))}});function Qi(t){return new te(t)}var te=S.extend({options:{position:"topright"},initialize:function(t){p(this,t)},getPosition:function(){return this.options.position},setPosition:function(t){var i=this._map;return i&&i.removeControl(this),this.options.position=t,i&&i.addControl(this),this},getContainer:function(){return this._container},addTo:function(t){this.remove(),this._map=t;var i=this._container=this.onAdd(t),e=this.getPosition(),n=t._controlCorners[e];return mi(i,"leaflet-control"),-1!==e.indexOf("bottom")?n.insertBefore(i,n.firstChild):n.appendChild(i),this._map.on("unload",this.remove,this),this},remove:function(){return this._map&&(li(this._container),this.onRemove&&this.onRemove(this._map),this._map.off("unload",this.remove,this),this._map=null),this},_refocusOnMap:function(t){this._map&&t&&0",n=document.createElement("div");return n.innerHTML=e,n.firstChild},_addItem:function(t){var i,e=document.createElement("label"),n=this._map.hasLayer(t.layer);t.overlay?((i=document.createElement("input")).type="checkbox",i.className="leaflet-control-layers-selector",i.defaultChecked=n):i=this._createRadioElement("leaflet-base-layers_"+u(this),n),this._layerControlInputs.push(i),i.layerId=u(t.layer),ki(i,"click",this._onInputClick,this);var o=document.createElement("span");o.innerHTML=" "+t.name;var s=document.createElement("div");return e.appendChild(s),s.appendChild(i),s.appendChild(o),(t.overlay?this._overlaysList:this._baseLayersList).appendChild(e),this._checkDisabledLayers(),e},_onInputClick:function(){var t,i,e=this._layerControlInputs,n=[],o=[];this._handlingClick=!0;for(var s=e.length-1;0<=s;s--)t=e[s],i=this._getLayer(t.layerId).layer,t.checked?n.push(i):t.checked||o.push(i);for(s=0;si.options.maxZoom},_expandIfNotCollapsed:function(){return this._map&&!this.options.collapsed&&this.expand(),this},_expand:function(){return this.expand()},_collapse:function(){return this.collapse()}}),ee=te.extend({options:{position:"topleft",zoomInText:"+",zoomInTitle:"Zoom in",zoomOutText:"−",zoomOutTitle:"Zoom out"},onAdd:function(t){var i="leaflet-control-zoom",e=ui("div",i+" leaflet-bar"),n=this.options;return this._zoomInButton=this._createButton(n.zoomInText,n.zoomInTitle,i+"-in",e,this._zoomIn),this._zoomOutButton=this._createButton(n.zoomOutText,n.zoomOutTitle,i+"-out",e,this._zoomOut),this._updateDisabled(),t.on("zoomend zoomlevelschange",this._updateDisabled,this),e},onRemove:function(t){t.off("zoomend zoomlevelschange",this._updateDisabled,this)},disable:function(){return this._disabled=!0,this._updateDisabled(),this},enable:function(){return this._disabled=!1,this._updateDisabled(),this},_zoomIn:function(t){!this._disabled&&this._map._zoomthis._map.getMinZoom()&&this._map.zoomOut(this._map.options.zoomDelta*(t.shiftKey?3:1))},_createButton:function(t,i,e,n,o){var s=ui("a",e,n);return s.innerHTML=t,s.href="#",s.title=i,s.setAttribute("role","button"),s.setAttribute("aria-label",i),Di(s),ki(s,"click",Wi),ki(s,"click",o,this),ki(s,"click",this._refocusOnMap,this),s},_updateDisabled:function(){var t=this._map,i="leaflet-disabled";fi(this._zoomInButton,i),fi(this._zoomOutButton,i),!this._disabled&&t._zoom!==t.getMinZoom()||mi(this._zoomOutButton,i),!this._disabled&&t._zoom!==t.getMaxZoom()||mi(this._zoomInButton,i)}});$i.mergeOptions({zoomControl:!0}),$i.addInitHook(function(){this.options.zoomControl&&(this.zoomControl=new ee,this.addControl(this.zoomControl))});var ne=te.extend({options:{position:"bottomleft",maxWidth:100,metric:!0,imperial:!0},onAdd:function(t){var i="leaflet-control-scale",e=ui("div",i),n=this.options;return this._addScales(n,i+"-line",e),t.on(n.updateWhenIdle?"moveend":"move",this._update,this),t.whenReady(this._update,this),e},onRemove:function(t){t.off(this.options.updateWhenIdle?"moveend":"move",this._update,this)},_addScales:function(t,i,e){t.metric&&(this._mScale=ui("div",i,e)),t.imperial&&(this._iScale=ui("div",i,e))},_update:function(){var t=this._map,i=t.getSize().y/2,e=t.distance(t.containerPointToLatLng([0,i]),t.containerPointToLatLng([this.options.maxWidth,i]));this._updateScales(e)},_updateScales:function(t){this.options.metric&&t&&this._updateMetric(t),this.options.imperial&&t&&this._updateImperial(t)},_updateMetric:function(t){var i=this._getRoundNum(t),e=i<1e3?i+" m":i/1e3+" km";this._updateScale(this._mScale,e,i/t)},_updateImperial:function(t){var i,e,n,o=3.2808399*t;5280Leaflet'},initialize:function(t){p(this,t),this._attributions={}},onAdd:function(t){for(var i in(t.attributionControl=this)._container=ui("div","leaflet-control-attribution"),Di(this._container),t._layers)t._layers[i].getAttribution&&this.addAttribution(t._layers[i].getAttribution());return this._update(),this._container},setPrefix:function(t){return this.options.prefix=t,this._update(),this},addAttribution:function(t){return t&&(this._attributions[t]||(this._attributions[t]=0),this._attributions[t]++,this._update()),this},removeAttribution:function(t){return t&&this._attributions[t]&&(this._attributions[t]--,this._update()),this},_update:function(){if(this._map){var t=[];for(var i in this._attributions)this._attributions[i]&&t.push(i);var e=[];this.options.prefix&&e.push(this.options.prefix),t.length&&e.push(t.join(", ")),this._container.innerHTML=e.join(" | ")}}});$i.mergeOptions({attributionControl:!0}),$i.addInitHook(function(){this.options.attributionControl&&(new oe).addTo(this)});te.Layers=ie,te.Zoom=ee,te.Scale=ne,te.Attribution=oe,Qi.layers=function(t,i,e){return new ie(t,i,e)},Qi.zoom=function(t){return new ee(t)},Qi.scale=function(t){return new ne(t)},Qi.attribution=function(t){return new oe(t)};var se=S.extend({initialize:function(t){this._map=t},enable:function(){return this._enabled||(this._enabled=!0,this.addHooks()),this},disable:function(){return this._enabled&&(this._enabled=!1,this.removeHooks()),this},enabled:function(){return!!this._enabled}});se.addTo=function(t,i){return t.addHandler(i,this),this};var re,ae={Events:Z},he=Tt?"touchstart mousedown":"mousedown",ue={mousedown:"mouseup",touchstart:"touchend",pointerdown:"touchend",MSPointerDown:"touchend"},le={mousedown:"mousemove",touchstart:"touchmove",pointerdown:"touchmove",MSPointerDown:"touchmove"},ce=k.extend({options:{clickTolerance:3},initialize:function(t,i,e,n){p(this,n),this._element=t,this._dragStartTarget=i||t,this._preventOutline=e},enable:function(){this._enabled||(ki(this._dragStartTarget,he,this._onDown,this),this._enabled=!0)},disable:function(){this._enabled&&(ce._dragging===this&&this.finishDrag(),Ai(this._dragStartTarget,he,this._onDown,this),this._enabled=!1,this._moved=!1)},_onDown:function(t){if(!t._simulated&&this._enabled&&(this._moved=!1,!pi(this._element,"leaflet-zoom-anim")&&!(ce._dragging||t.shiftKey||1!==t.which&&1!==t.button&&!t.touches||((ce._dragging=this)._preventOutline&&Mi(this._element),Ti(),Qt(),this._moving)))){this.fire("down");var i=t.touches?t.touches[0]:t,e=Ei(this._element);this._startPoint=new B(i.clientX,i.clientY),this._parentScale=Si(e),ki(document,le[t.type],this._onMove,this),ki(document,ue[t.type],this._onUp,this)}},_onMove:function(t){if(!t._simulated&&this._enabled)if(t.touches&&1i.max.x&&(e|=2),t.yi.max.y&&(e|=8),e}function ge(t,i,e,n){var o,s=i.x,r=i.y,a=e.x-s,h=e.y-r,u=a*a+h*h;return 0this._layersMaxZoom&&this.setZoom(this._layersMaxZoom),void 0===this.options.minZoom&&this._layersMinZoom&&this.getZoom()t.y!=n.y>t.y&&t.x<(n.x-e.x)*(t.y-e.y)/(n.y-e.y)+e.x&&(u=!u);return u||je.prototype._containsPoint.call(this,t,!0)}});var He=ke.extend({initialize:function(t,i){p(this,i),this._layers={},t&&this.addData(t)},addData:function(t){var i,e,n,o=v(t)?t:t.features;if(o){for(i=0,e=o.length;iu.x&&(l=s.x+n-u.x+h.x),s.x-l-a.x<0&&(l=s.x-a.x),s.y+e+h.y>u.y&&(c=s.y+e-u.y+h.y),s.y-c-a.y<0&&(c=s.y-a.y),(l||c)&&t.fire("autopanstart").panBy([l,c])}},_onCloseButtonClick:function(t){this._close(),Wi(t)},_getAnchor:function(){return I(this._source&&this._source._getPopupAnchor?this._source._getPopupAnchor():[0,0])}});$i.mergeOptions({closePopupOnClick:!0}),$i.include({openPopup:function(t,i,e){return t instanceof sn||(t=new sn(e).setContent(t)),i&&t.setLatLng(i),this.hasLayer(t)?this:(this._popup&&this._popup.options.autoClose&&this.closePopup(),this._popup=t,this.addLayer(t))},closePopup:function(t){return t&&t!==this._popup||(t=this._popup,this._popup=null),t&&this.removeLayer(t),this}}),Se.include({bindPopup:function(t,i){return t instanceof sn?(p(t,i),(this._popup=t)._source=this):(this._popup&&!i||(this._popup=new sn(i,this)),this._popup.setContent(t)),this._popupHandlersAdded||(this.on({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!0),this},unbindPopup:function(){return this._popup&&(this.off({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!1,this._popup=null),this},openPopup:function(t,i){return this._popup&&this._map&&(i=this._popup._prepareOpen(this,t,i),this._map.openPopup(this._popup,i)),this},closePopup:function(){return this._popup&&this._popup._close(),this},togglePopup:function(t){return this._popup&&(this._popup._map?this.closePopup():this.openPopup(t)),this},isPopupOpen:function(){return!!this._popup&&this._popup.isOpen()},setPopupContent:function(t){return this._popup&&this._popup.setContent(t),this},getPopup:function(){return this._popup},_openPopup:function(t){var i=t.layer||t.target;this._popup&&this._map&&(Wi(t),i instanceof Re?this.openPopup(t.layer||t.target,t.latlng):this._map.hasLayer(this._popup)&&this._popup._source===i?this.closePopup():this.openPopup(i,t.latlng))},_movePopup:function(t){this._popup.setLatLng(t.latlng)},_onKeyPress:function(t){13===t.originalEvent.keyCode&&this._openPopup(t)}});var rn=on.extend({options:{pane:"tooltipPane",offset:[0,0],direction:"auto",permanent:!1,sticky:!1,interactive:!1,opacity:.9},onAdd:function(t){on.prototype.onAdd.call(this,t),this.setOpacity(this.options.opacity),t.fire("tooltipopen",{tooltip:this}),this._source&&this._source.fire("tooltipopen",{tooltip:this},!0)},onRemove:function(t){on.prototype.onRemove.call(this,t),t.fire("tooltipclose",{tooltip:this}),this._source&&this._source.fire("tooltipclose",{tooltip:this},!0)},getEvents:function(){var t=on.prototype.getEvents.call(this);return Tt&&!this.options.permanent&&(t.preclick=this._close),t},_close:function(){this._map&&this._map.closeTooltip(this)},_initLayout:function(){var t="leaflet-tooltip "+(this.options.className||"")+" leaflet-zoom-"+(this._zoomAnimated?"animated":"hide");this._contentNode=this._container=ui("div",t)},_updateLayout:function(){},_adjustPan:function(){},_setPosition:function(t){var i=this._map,e=this._container,n=i.latLngToContainerPoint(i.getCenter()),o=i.layerPointToContainerPoint(t),s=this.options.direction,r=e.offsetWidth,a=e.offsetHeight,h=I(this.options.offset),u=this._getAnchor();t="top"===s?t.add(I(-r/2+h.x,-a+h.y+u.y,!0)):"bottom"===s?t.subtract(I(r/2-h.x,-h.y,!0)):"center"===s?t.subtract(I(r/2+h.x,a/2-u.y+h.y,!0)):"right"===s||"auto"===s&&o.xthis.options.maxZoom||ethis.options.maxZoom||void 0!==this.options.minZoom&&oe.max.x)||!i.wrapLat&&(t.ye.max.y))return!1}if(!this.options.bounds)return!0;var n=this._tileCoordsToBounds(t);return D(this.options.bounds).overlaps(n)},_keyToBounds:function(t){return this._tileCoordsToBounds(this._keyToTileCoords(t))},_tileCoordsToNwSe:function(t){var i=this._map,e=this.getTileSize(),n=t.scaleBy(e),o=n.add(e);return[i.unproject(n,t.z),i.unproject(o,t.z)]},_tileCoordsToBounds:function(t){var i=this._tileCoordsToNwSe(t),e=new N(i[0],i[1]);return this.options.noWrap||(e=this._map.wrapLatLngBounds(e)),e},_tileCoordsToKey:function(t){return t.x+":"+t.y+":"+t.z},_keyToTileCoords:function(t){var i=t.split(":"),e=new B(+i[0],+i[1]);return e.z=+i[2],e},_removeTile:function(t){var i=this._tiles[t];i&&(li(i.el),delete this._tiles[t],this.fire("tileunload",{tile:i.el,coords:this._keyToTileCoords(t)}))},_initTile:function(t){mi(t,"leaflet-tile");var i=this.getTileSize();t.style.width=i.x+"px",t.style.height=i.y+"px",t.onselectstart=l,t.onmousemove=l,et&&this.options.opacity<1&&yi(t,this.options.opacity),st&&!rt&&(t.style.WebkitBackfaceVisibility="hidden")},_addTile:function(t,i){var e=this._getTilePos(t),n=this._tileCoordsToKey(t),o=this.createTile(this._wrapCoords(t),a(this._tileReady,this,t));this._initTile(o),this.createTile.length<2&&M(a(this._tileReady,this,t,null,o)),Pi(o,e),this._tiles[n]={el:o,coords:t,current:!0},i.appendChild(o),this.fire("tileloadstart",{tile:o,coords:t})},_tileReady:function(t,i,e){i&&this.fire("tileerror",{error:i,tile:e,coords:t});var n=this._tileCoordsToKey(t);(e=this._tiles[n])&&(e.loaded=+new Date,this._map._fadeAnimated?(yi(e.el,0),C(this._fadeFrame),this._fadeFrame=M(this._updateOpacity,this)):(e.active=!0,this._pruneTiles()),i||(mi(e.el,"leaflet-tile-loaded"),this.fire("tileload",{tile:e.el,coords:t})),this._noTilesToLoad()&&(this._loading=!1,this.fire("load"),et||!this._map._fadeAnimated?M(this._pruneTiles,this):setTimeout(a(this._pruneTiles,this),250)))},_getTilePos:function(t){return t.scaleBy(this.getTileSize()).subtract(this._level.origin)},_wrapCoords:function(t){var i=new B(this._wrapX?r(t.x,this._wrapX):t.x,this._wrapY?r(t.y,this._wrapY):t.y);return i.z=t.z,i},_pxBoundsToTileRange:function(t){var i=this.getTileSize();return new O(t.min.unscaleBy(i).floor(),t.max.unscaleBy(i).ceil().subtract([1,1]))},_noTilesToLoad:function(){for(var t in this._tiles)if(!this._tiles[t].loaded)return!1;return!0}});var un=hn.extend({options:{minZoom:0,maxZoom:18,subdomains:"abc",errorTileUrl:"",zoomOffset:0,tms:!1,zoomReverse:!1,detectRetina:!1,crossOrigin:!1},initialize:function(t,i){this._url=t,(i=p(this,i)).detectRetina&&Ct&&0')}}catch(t){return function(t){return document.createElement("<"+t+' xmlns="urn:schemas-microsoft.com:vml" class="lvml">')}}}(),fn={_initContainer:function(){this._container=ui("div","leaflet-vml-container")},_update:function(){this._map._animatingZoom||(_n.prototype._update.call(this),this.fire("update"))},_initPath:function(t){var i=t._container=mn("shape");mi(i,"leaflet-vml-shape "+(this.options.className||"")),i.coordsize="1 1",t._path=mn("path"),i.appendChild(t._path),this._updateStyle(t),this._layers[u(t)]=t},_addPath:function(t){var i=t._container;this._container.appendChild(i),t.options.interactive&&t.addInteractiveTarget(i)},_removePath:function(t){var i=t._container;li(i),t.removeInteractiveTarget(i),delete this._layers[u(t)]},_updateStyle:function(t){var i=t._stroke,e=t._fill,n=t.options,o=t._container;o.stroked=!!n.stroke,o.filled=!!n.fill,n.stroke?(i||(i=t._stroke=mn("stroke")),o.appendChild(i),i.weight=n.weight+"px",i.color=n.color,i.opacity=n.opacity,n.dashArray?i.dashStyle=v(n.dashArray)?n.dashArray.join(" "):n.dashArray.replace(/( *, *)/g," "):i.dashStyle="",i.endcap=n.lineCap.replace("butt","flat"),i.joinstyle=n.lineJoin):i&&(o.removeChild(i),t._stroke=null),n.fill?(e||(e=t._fill=mn("fill")),o.appendChild(e),e.color=n.fillColor||n.color,e.opacity=n.fillOpacity):e&&(o.removeChild(e),t._fill=null)},_updateCircle:function(t){var i=t._point.round(),e=Math.round(t._radius),n=Math.round(t._radiusY||e);this._setPath(t,t._empty()?"M0 0":"AL "+i.x+","+i.y+" "+e+","+n+" 0,23592600")},_setPath:function(t,i){t._path.v=i},_bringToFront:function(t){_i(t._container)},_bringToBack:function(t){di(t._container)}},gn=kt?mn:$,vn=_n.extend({getEvents:function(){var t=_n.prototype.getEvents.call(this);return t.zoomstart=this._onZoomStart,t},_initContainer:function(){this._container=gn("svg"),this._container.setAttribute("pointer-events","none"),this._rootGroup=gn("g"),this._container.appendChild(this._rootGroup)},_destroyContainer:function(){li(this._container),Ai(this._container),delete this._container,delete this._rootGroup,delete this._svgSize},_onZoomStart:function(){this._update()},_update:function(){if(!this._map._animatingZoom||!this._bounds){_n.prototype._update.call(this);var t=this._bounds,i=t.getSize(),e=this._container;this._svgSize&&this._svgSize.equals(i)||(this._svgSize=i,e.setAttribute("width",i.x),e.setAttribute("height",i.y)),Pi(e,t.min),e.setAttribute("viewBox",[t.min.x,t.min.y,i.x,i.y].join(" ")),this.fire("update")}},_initPath:function(t){var i=t._path=gn("path");t.options.className&&mi(i,t.options.className),t.options.interactive&&mi(i,"leaflet-interactive"),this._updateStyle(t),this._layers[u(t)]=t},_addPath:function(t){this._rootGroup||this._initContainer(),this._rootGroup.appendChild(t._path),t.addInteractiveTarget(t._path)},_removePath:function(t){li(t._path),t.removeInteractiveTarget(t._path),delete this._layers[u(t)]},_updatePath:function(t){t._project(),t._update()},_updateStyle:function(t){var i=t._path,e=t.options;i&&(e.stroke?(i.setAttribute("stroke",e.color),i.setAttribute("stroke-opacity",e.opacity),i.setAttribute("stroke-width",e.weight),i.setAttribute("stroke-linecap",e.lineCap),i.setAttribute("stroke-linejoin",e.lineJoin),e.dashArray?i.setAttribute("stroke-dasharray",e.dashArray):i.removeAttribute("stroke-dasharray"),e.dashOffset?i.setAttribute("stroke-dashoffset",e.dashOffset):i.removeAttribute("stroke-dashoffset")):i.setAttribute("stroke","none"),e.fill?(i.setAttribute("fill",e.fillColor||e.color),i.setAttribute("fill-opacity",e.fillOpacity),i.setAttribute("fill-rule",e.fillRule||"evenodd")):i.setAttribute("fill","none"))},_updatePoly:function(t,i){this._setPath(t,Q(t._parts,i))},_updateCircle:function(t){var i=t._point,e=Math.max(Math.round(t._radius),1),n="a"+e+","+(Math.max(Math.round(t._radiusY),1)||e)+" 0 1,0 ",o=t._empty()?"M0 0":"M"+(i.x-e)+","+i.y+n+2*e+",0 "+n+2*-e+",0 ";this._setPath(t,o)},_setPath:function(t,i){t._path.setAttribute("d",i)},_bringToFront:function(t){_i(t._path)},_bringToBack:function(t){di(t._path)}});function yn(t){return Zt||kt?new vn(t):null}kt&&vn.include(fn),$i.include({getRenderer:function(t){var i=t.options.renderer||this._getPaneRenderer(t.options.pane)||this.options.renderer||this._renderer;return i||(i=this._renderer=this._createRenderer()),this.hasLayer(i)||this.addLayer(i),i},_getPaneRenderer:function(t){if("overlayPane"===t||void 0===t)return!1;var i=this._paneRenderers[t];return void 0===i&&(i=this._createRenderer({pane:t}),this._paneRenderers[t]=i),i},_createRenderer:function(t){return this.options.preferCanvas&&pn(t)||yn(t)}});var xn=We.extend({initialize:function(t,i){We.prototype.initialize.call(this,this._boundsToLatLngs(t),i)},setBounds:function(t){return this.setLatLngs(this._boundsToLatLngs(t))},_boundsToLatLngs:function(t){return[(t=D(t)).getSouthWest(),t.getNorthWest(),t.getNorthEast(),t.getSouthEast()]}});vn.create=gn,vn.pointsToPath=Q,He.geometryToLayer=Fe,He.coordsToLatLng=Ve,He.coordsToLatLngs=qe,He.latLngToCoords=Ge,He.latLngsToCoords=Ke,He.getFeature=Ye,He.asFeature=Xe,$i.mergeOptions({boxZoom:!0});var wn=se.extend({initialize:function(t){this._map=t,this._container=t._container,this._pane=t._panes.overlayPane,this._resetStateTimeout=0,t.on("unload",this._destroy,this)},addHooks:function(){ki(this._container,"mousedown",this._onMouseDown,this)},removeHooks:function(){Ai(this._container,"mousedown",this._onMouseDown,this)},moved:function(){return this._moved},_destroy:function(){li(this._pane),delete this._pane},_resetState:function(){this._resetStateTimeout=0,this._moved=!1},_clearDeferredResetState:function(){0!==this._resetStateTimeout&&(clearTimeout(this._resetStateTimeout),this._resetStateTimeout=0)},_onMouseDown:function(t){if(!t.shiftKey||1!==t.which&&1!==t.button)return!1;this._clearDeferredResetState(),this._resetState(),Qt(),Ti(),this._startPoint=this._map.mouseEventToContainerPoint(t),ki(document,{contextmenu:Wi,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseMove:function(t){this._moved||(this._moved=!0,this._box=ui("div","leaflet-zoom-box",this._container),mi(this._container,"leaflet-crosshair"),this._map.fire("boxzoomstart")),this._point=this._map.mouseEventToContainerPoint(t);var i=new O(this._point,this._startPoint),e=i.getSize();Pi(this._box,i.min),this._box.style.width=e.x+"px",this._box.style.height=e.y+"px"},_finish:function(){this._moved&&(li(this._box),fi(this._container,"leaflet-crosshair")),ti(),zi(),Ai(document,{contextmenu:Wi,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseUp:function(t){if((1===t.which||1===t.button)&&(this._finish(),this._moved)){this._clearDeferredResetState(),this._resetStateTimeout=setTimeout(a(this._resetState,this),0);var i=new N(this._map.containerPointToLatLng(this._startPoint),this._map.containerPointToLatLng(this._point));this._map.fitBounds(i).fire("boxzoomend",{boxZoomBounds:i})}},_onKeyDown:function(t){27===t.keyCode&&this._finish()}});$i.addInitHook("addHandler","boxZoom",wn),$i.mergeOptions({doubleClickZoom:!0});var Pn=se.extend({addHooks:function(){this._map.on("dblclick",this._onDoubleClick,this)},removeHooks:function(){this._map.off("dblclick",this._onDoubleClick,this)},_onDoubleClick:function(t){var i=this._map,e=i.getZoom(),n=i.options.zoomDelta,o=t.originalEvent.shiftKey?e-n:e+n;"center"===i.options.doubleClickZoom?i.setZoom(o):i.setZoomAround(t.containerPoint,o)}});$i.addInitHook("addHandler","doubleClickZoom",Pn),$i.mergeOptions({dragging:!0,inertia:!rt,inertiaDeceleration:3400,inertiaMaxSpeed:1/0,easeLinearity:.2,worldCopyJump:!1,maxBoundsViscosity:0});var Ln=se.extend({addHooks:function(){if(!this._draggable){var t=this._map;this._draggable=new ce(t._mapPane,t._container),this._draggable.on({dragstart:this._onDragStart,drag:this._onDrag,dragend:this._onDragEnd},this),this._draggable.on("predrag",this._onPreDragLimit,this),t.options.worldCopyJump&&(this._draggable.on("predrag",this._onPreDragWrap,this),t.on("zoomend",this._onZoomEnd,this),t.whenReady(this._onZoomEnd,this))}mi(this._map._container,"leaflet-grab leaflet-touch-drag"),this._draggable.enable(),this._positions=[],this._times=[]},removeHooks:function(){fi(this._map._container,"leaflet-grab"),fi(this._map._container,"leaflet-touch-drag"),this._draggable.disable()},moved:function(){return this._draggable&&this._draggable._moved},moving:function(){return this._draggable&&this._draggable._moving},_onDragStart:function(){var t=this._map;if(t._stop(),this._map.options.maxBounds&&this._map.options.maxBoundsViscosity){var i=D(this._map.options.maxBounds);this._offsetLimit=R(this._map.latLngToContainerPoint(i.getNorthWest()).multiplyBy(-1),this._map.latLngToContainerPoint(i.getSouthEast()).multiplyBy(-1).add(this._map.getSize())),this._viscosity=Math.min(1,Math.max(0,this._map.options.maxBoundsViscosity))}else this._offsetLimit=null;t.fire("movestart").fire("dragstart"),t.options.inertia&&(this._positions=[],this._times=[])},_onDrag:function(t){if(this._map.options.inertia){var i=this._lastTime=+new Date,e=this._lastPos=this._draggable._absPos||this._draggable._newPos;this._positions.push(e),this._times.push(i),this._prunePositions(i)}this._map.fire("move",t).fire("drag",t)},_prunePositions:function(t){for(;1i.max.x&&(t.x=this._viscousLimit(t.x,i.max.x)),t.y>i.max.y&&(t.y=this._viscousLimit(t.y,i.max.y)),this._draggable._newPos=this._draggable._startPos.add(t)}},_onPreDragWrap:function(){var t=this._worldWidth,i=Math.round(t/2),e=this._initialWorldOffset,n=this._draggable._newPos.x,o=(n-i+e)%t+i-e,s=(n+i+e)%t-i-e,r=Math.abs(o+e)i.getMaxZoom()&&1= 0; i--) { + for (var j = 0; j < locales.length; j++) { + if (navigator.languages[i] == locales[j]) { + prefLang = locales[j].split('-').join('_'); + } + } + } +} + +// Load localization English as fallback. +var LANG = {}; +var xhr = new XMLHttpRequest(); +xhr.onreadystatechange = function() { + if (this.readyState == 4 && this.status == 200) { + LANG = JSON.parse(this.responseText); + // Overwrite the L10N data with localizations for the preferred language. + if (prefLang != 'en') { + var xhr2 = new XMLHttpRequest(); + xhr2.onreadystatechange = function() { + if (this.readyState == 4 && this.status == 200) { + var data = JSON.parse(this.responseText); + for (var key in data) { + if (!data.hasOwnProperty(key)) continue; + LANG[key] = data[key]; + } + localizeHTML(); + init(); + } + }; + xhr2.open('GET', './assets/lang/' + prefLang + '.json', true); + xhr2.send(); + } else { + localizeHTML(); + init(); + } + } +}; +xhr.open('GET', './assets/lang/en.json', true); +xhr.send(); + +// Put localized strings in HTML. +function localizeHTML() { + var tags = document.querySelectorAll('[data-i18n]'); + for (var key in tags) { + if (!tags.hasOwnProperty(key)) continue; + var i18nKey = tags[key].getAttribute('data-i18n'); + if (tags[key].hasAttribute('data-i18n-attr')) { + tags[key].setAttribute(tags[key].getAttribute('data-i18n-attr'), LANG[i18nKey]); + } else { + tags[key].textContent = LANG[i18nKey]; + } + } +} + +// Starts fetching location data from the server. This is called after I18N has +// loaded to ensure strings are present for prompts. +function init() { + if (location.href.indexOf("?") === -1 || id == "") { + // If there is no share ID, show the root page. + var url = location.href.indexOf("?") === -1 ? location.href : location.href.substring(0, location.href.indexOf("?")); + + var urlE = document.getElementById("url"); + var indexE = document.getElementById("index"); + var storeIconFdroidE = document.getElementById("store-icon-fdroid"); + var storeIconGplayE = document.getElementById("store-icon-gplay"); + if (urlE !== null) urlE.textContent = url; + if (indexE !== null) indexE.style.display = "block"; + if (storeIconFdroidE !== null) storeIconFdroidE.src = LANG["f_droid_badge_url"]; + if (storeIconGplayE !== null) storeIconGplayE.src = LANG["google_play_badge_url"]; + } else { + // Attempt to fetch location data from the server once. + getJSON("./api/fetch.php?id=" + id, function(data) { + // Initialize the Leaflet map. + initMap(); + noGPS.style.display = "block"; + processUpdate(data, true); + }, function() { + var notFoundE = document.getElementById("notfound"); + if (notFoundE !== null) notFoundE.style.display = "block"; + }); + } +} + +// For locating the viewer of the map. Watcher is a watchPosition() reference. +var watcher = null; + +// Marker and accuracy circle of the person viewing the map. +var selfCircle = null; +var selfMarker = null; + +// Create a geolocation control. +L.control.Locate = L.Control.extend({ + onAdd: function(map) { + var btn = L.DomUtil.create('div', 'leaflet-bar leaflet-control'); + var anchor = L.DomUtil.create('a', 'leaflet-control-locate-inactive'); + + btn.style.backgroundColor = '#fff'; + btn.appendChild(anchor); + + anchor.href = '#'; + anchor.title = LANG["control_show_self"]; + anchor.onclick = function(e) { + // Prevent # from appearing in URL. + e.preventDefault(); + + if (watcher === null) { + // If there is no location watcher active, create one. + // Update the icon to indicate that the location is pending. + anchor.className = 'leaflet-control-locate-pending'; + watcher = navigator.geolocation.watchPosition(function(pos) { + // If the circle and marker is missing, add it and set the + // location status to active. + if (selfCircle == null && selfMarker == null) { + anchor.className = 'leaflet-control-locate-active'; + selfCircle = L.circle([pos.coords.latitude, pos.coords.longitude], { + radius: pos.coords.accuracy, + fillColor: '#1e90ff', + fillOpacity: 0.25, + color: '#1e90ff', + opacity: 0.5, + interactive: false + }).addTo(circleLayer); + var selfIcon = L.divIcon({ + html: '
', + iconAnchor: [33, 18] + }); + selfMarker = L.marker([pos.coords.latitude, pos.coords.longitude], { + icon: selfIcon, + interactive: false + }).addTo(markerLayer); + + // Unfollow any curretly followed user, then pan to the + // device location. + following = null; + map.panTo([pos.coords.latitude, pos.coords.longitude]); + } else { + selfCircle.setLatLng([pos.coords.latitude, pos.coords.longitude]); + selfMarker.setLatLng([pos.coords.latitude, pos.coords.longitude]); + selfCircle.setRadius(pos.coords.accuracy); + } + }); + } else { + // If there is already a watcher, clicking the control should + // unregister it and hide the user's location. + anchor.className = 'leaflet-control-locate-inactive'; + navigator.geolocation.clearWatch(watcher); + watcher = null; + circleLayer.removeLayer(selfCircle); + markerLayer.removeLayer(selfMarker); + selfCircle = null; + selfMarker = null; + } + return false; + }; + + return btn; + } +}); + +// Create a "show list of users" control. +L.control.Radar = L.Control.extend({ + onAdd: function(map) { + var btn = L.DomUtil.create('div', 'leaflet-bar leaflet-control'); + var anchor = L.DomUtil.create('a', 'leaflet-control-radar'); + + btn.style.backgroundColor = '#fff'; + btn.appendChild(anchor); + + anchor.href = '#'; + anchor.title = LANG["control_show_self"]; + anchor.onclick = function(e) { + // Prevent # from appearing in URL. + e.preventDefault(); + var userListPopupE = document.getElementById("user-list-popup"); + if (userListPopupE !== null) { + userListPopupE.style.display = 'block'; + } + var userListE = document.getElementById("user-list"); + if (userListE !== null) { + var users = userListE.getElementsByTagName("p"); + if (users.length == 1) users[0].click(); + } + return false; + }; + + return btn; + } +}); + +// Function for spawning the controls. +L.control.locate = function(opts) { + return new L.control.Locate(opts); +} +L.control.radar = function(opts) { + return new L.control.Radar(opts); +} + +var map, circleLayer, markerLayer; + +function initMap() { + // Create a Leaflet map. + map = L.map('map').setView([0, 0], DEFAULT_ZOOM); + L.tileLayer(TILE_URI, { + attribution: ATTRIBUTION, + maxZoom: MAX_ZOOM + }).addTo(map); + + // Add the geolocation control to the map if supported by the browser. + if ("geolocation" in navigator && window.isSecureContext) { + L.control.locate({ position: 'topleft' }).addTo(map); + } + L.control.radar({ position: 'topleft' }).addTo(map); + + circleLayer = L.layerGroup().addTo(map); + markerLayer = L.layerGroup().addTo(map); + + // Unfollow the user when the map is panned. + map.on('mousedown', function() { + following = null; + }); +} + +// The leaflet markers and associated data. +var shares = {}; + +// Retrieve the sharing link ID from the URL. E.g. +// https://example.com/?ABCD-1234 --> "ABCD-1234" +var id = location.href.substr(location.href.indexOf("?") + 1); +if (id.indexOf("&") !== -1) id = id.substr(0, id.indexOf("&")); + +// Whether the "offline" popup has appeared when the browser is offline. +var knownOffline = false; + +function getJSON(url, callback, invalid) { + var xhr = new XMLHttpRequest(); + xhr.timeout = REQUEST_TIMEOUT * 1000; + xhr.open('GET', url, true); + xhr.onreadystatechange = function() { + if (this.readyState == 4) { + var offlineE = document.getElementById("offline"); + var notchE = document.getElementById("notch"); + if (this.status === 200) { + // Request successful. Reset offline state and parse the JSON. + knownOffline = false; + if (offlineE !== null) { + document.getElementById("offline").style.display = "none"; + } + if (notchE !== null) { + document.getElementById("notch").className = ""; + } + try { + var json = JSON.parse(this.responseText); + callback(json); + } catch (ex) { + console.log(ex); + invalid(); + } + } else if (this.status === 404) { + invalid(); + } else { + // Requested failed; offline. + if (!knownOffline) { + knownOffline = true; + if (offlineE !== null) { + document.getElementById("offline").style.display = "block"; + } + if (notchE !== null) { + document.getElementById("notch").className = "offline"; + } + } + } + } + } + xhr.send(); +} + +// General message popup box. Reused for several popups. +var dismissMessageE = document.getElementById("dismiss-message"); +if (dismissMessageE !== null) { + dismissMessageE.addEventListener("click", function() { + var messageE = document.getElementById("message-popup"); + if (messageE !== null) messageE.style.display = "none"; + }); +} + +// Shows a dialog box with a title and message. +function showMessage(title, message) { + var messageE = document.getElementById("message-popup"); + var titleE = document.getElementById("message-title"); + var bodyE = document.getElementById("message-body"); + if (titleE !== null) titleE.textContent = title; + if (bodyE !== null) bodyE.textContent = message; + if (messageE !== null) messageE.style.display = "block"; +} + +var dismissOfflineE = document.getElementById("dismiss-offline"); +if (dismissOfflineE !== null) { + dismissOfflineE.addEventListener("click", function() { + var offlineE = document.getElementById("offline"); + if (offlineE !== null) offlineE.style.display = "none"; + }); +} + +// End-to-end encryption password prompt handlers. +var passwordInputE = document.getElementById("e2e-password"); +var passwordDecryptE = document.getElementById("decrypt-e2e-password"); +if (passwordInputE !== null) { + passwordInputE.addEventListener("keyup", function(e) { + if (e.keyCode == 13) { + if (passwordDecryptE !== null) { + passwordDecryptE.click(); + } + } + }); +} + +var passwordCancelE = document.getElementById("cancel-e2e-password"); +if (passwordCancelE !== null) { + passwordCancelE.addEventListener("click", function() { + var promptE = document.getElementById("e2e-prompt"); + if (promptE !== null) promptE.style.display = "none"; + if (passwordDecryptE !== null && acceptKeyFunc !== null) passwordDecryptE.removeEventListener("click", acceptKeyFunc); + }); +} + +var userDetailsE = document.getElementById("user-details-popup"); + +var userDetailsFollowE = document.getElementById("user-details-follow"); +if (userDetailsFollowE !== null) { + userDetailsFollowE.addEventListener("click", function() { + var user = userDetailsFollowE.dataset.user; + follow(user); + if (userDetailsE !== null) userDetailsE.style.display = "none"; + }); +} + +var userDetailsNavigateE = document.getElementById("user-details-navigate"); +if (userDetailsNavigateE !== null) { + userDetailsNavigateE.addEventListener("click", function() { + var user = userDetailsNavigateE.dataset.user; + var points = shares[user].points; + if (points.length > 0) { + var last = points[points.length - 1]; + window.open("geo:" + last.lat + "," + last.lon); + } + if (userDetailsE !== null) userDetailsE.style.display = "none"; + }); +} + +var closeUserListE = document.getElementById("close-user-list"); +if (closeUserListE !== null) { + closeUserListE.addEventListener("click", function() { + var userListPopupE = document.getElementById("user-list-popup"); + if (userListPopupE !== null) { + userListPopupE.style.display = 'none'; + } + }); +} + +var showAllUsersE = document.getElementById("btn-show-all"); +if (showAllUsersE !== null) { + showAllUsersE.addEventListener("click", function() { + autoCenter(); + var userListPopupE = document.getElementById("user-list-popup"); + if (userListPopupE !== null) { + userListPopupE.style.display = 'none'; + } + }); +} + +var closeUserDetailsE = document.getElementById("close-user-details"); +if (closeUserDetailsE !== null) { + closeUserDetailsE.addEventListener("click", function() { + if (userDetailsE !== null) { + userDetailsE.style.display = 'none'; + } + }); +} + +var fetchIntv; +var countIntv; + +function setNewInterval(expire, interval, serverTime) { + var countdownE = document.getElementById("countdown"); + var timeOffset = Date.now() / 1000 - serverTime; + + // The data contains an expiration time. Create a countdown at the top of + // the map screen that ends when the share is over. + countIntv = setInterval(function() { + var seconds = expire - Math.round((Date.now() - timeOffset) / 1000); + if (seconds < 0) { + clearInterval(countIntv); + return; + } + + var h = Math.floor(seconds / 3600); + var m = Math.floor((seconds % 3600) / 60); + var s = seconds % 60; + + var time = ""; + if (h > 0) time += h + ":"; + if (h > 0 && m < 10) time += "0"; + time += m + ":"; + if (s < 10) time += "0"; + time += s; + + if (countdownE !== null) countdownE.textContent = time; + }, 1000); + + // The location data contains an interval. Schedule a task that fetches data + // once per interval time. + fetchIntv = setInterval(function() { + // Stop the task if the share has expired. + if ((Date.now() - timeOffset) / 1000 >= expire) { + clearInterval(fetchIntv); + clearInterval(countIntv); + if (countdownE !== null) countdownE.textContent = LANG["status_expired"]; + showMessage(LANG["dialog_expired_head"], LANG["dialog_expired_body"]); + } + + // Start incremental fetch + getJSON("./api/fetch.php?id=" + id + "&since=" + getOldestPointTime(), function(data) { + // Recreate the interval timers if the interval or expiration + // change. + if (data.expire != expire || data.interval != interval) { + clearInterval(fetchIntv); + clearInterval(countIntv); + setNewInterval(data.expire, data.interval, data.serverTime); + } + processUpdate(data, false); + }, function() { + // On failure to get new location data: + clearInterval(fetchIntv); + clearInterval(countIntv); + if (countdownE !== null) countdownE.textContent = LANG["status_expired"]; + showMessage(LANG["dialog_expired_head"], LANG["dialog_expired_body"]); + }); + }, interval * 1000); +} + +// Scans across all most recent points and returns the time of the oldest one +function getOldestPointTime() { + var oldestTime = Number.MAX_VALUE; + var foundTime = false; + for (var share in shares) { + var points = shares[share].points + if (points && points.length > 0 ) { + var mostRecentTime = points[ points.length-1 ].time + oldestTime = mostRecentTime < oldestTime ? mostRecentTime : oldestTime; + foundTime = true; + } + } + return foundTime ? oldestTime : 0; +} + +var noGPS = document.getElementById("searching"); + +// Whether or not an initial location has been received. +var hasReceivedFirst = false; + +// Whether the map has been initially centered. +var hasInitiated = false; + +// The user being followed on the map. +var following = null; + +// The decryption key for end-to-end encrypted shares. +var aesKey = null; + +// Button handler for the "Decrypt" button on the E2E password prompt. +var acceptKeyFunc = null; + +// Converts a base64-encoded string to a Uint8Array ArrayBuffer for use with +// WebCrypto. +function byteArray(base64) { + var raw = atob(base64); + var len = raw.length; + var arr = new Uint8Array(new ArrayBuffer(len)); + for (var i = 0; i < len; i++) { + arr[i] = raw.charCodeAt(i); + } + return arr; +} + +// Follow a user on the map. +function follow(user) { + following = shares[user].id; + var last = shares[user].points[shares[user].points.length - 1]; + map.panTo([last.lat, last.lon]); +} + +// Zoom and pan map to show all users. +function autoCenter() { + var markers = []; + for (var share in shares) { + if (!shares.hasOwnProperty(share)) continue; + if (shares[share].marker === null) continue; + markers.push(shares[share].marker); + } + var fg = new L.featureGroup(markers); + map.fitBounds(fg.getBounds().pad(0.5)); + + // Do not exceed the default zoom level. + if (map.getZoom() > DEFAULT_ZOOM) map.setZoom(DEFAULT_ZOOM); +} + +// Parses the data returned from ./api/fetch.php and updates the map marker. +function processUpdate(data, init) { + var users = {}; + var multiUser = false; + if (data.type == SHARE_TYPE_ALONE) { + users[""] = data.points; + multiUser = false; + } else if (data.type == SHARE_TYPE_GROUP) { + users = data.points; + multiUser = true; + } + + // Check for crypto support if necessary. + if (data.encrypted && !("crypto" in window)) { + showMessage(LANG["e2e_title"], LANG["e2e_unsupported"]); + return; + } else if (data.encrypted && !("subtle" in window.crypto)) { + if (!window.isSecureContext) { + showMessage(LANG["e2e_title"], LANG["e2e_unavailable_secure"]); + } else { + showMessage(LANG["e2e_title"], LANG["e2e_unsupported"]); + } + return; + } + + if (data.encrypted && aesKey == null) { + // If using end-to-end encryption, we need to decrypt the data. We have + // not obtained an AES key yet, so prompt the user for it. + var promptE = document.getElementById("e2e-prompt"); + var labelE = document.getElementById("e2e-password-label"); + if (promptE !== null && passwordInputE !== null && passwordDecryptE !== null && labelE !== null) { + acceptKeyFunc = function() { + // Remove the event listener, hide the dialog and fetch the + // password. + passwordDecryptE.removeEventListener("click", acceptKeyFunc); + promptE.style.display = "none"; + labelE.textContent = LANG["e2e_incorrect"]; + var password = passwordInputE.value; + + // Get the salt in binary format. + var salt = byteArray(data.salt); + + // Derive the encryption key using PBKDF2 with SHA-1. SHA-1 was chosen + // because of availability in Android. + crypto.subtle + .importKey("raw", new TextEncoder("utf-8").encode(password), "PBKDF2", false, ["deriveKey"]) + .then(key => crypto.subtle.deriveKey( + {name: "PBKDF2", salt: salt, iterations: 65536, hash: "SHA-1"}, + key, + {name: "AES-CBC", length: 256}, + false, + ["decrypt"] + )) + .then(key => { + // Store the crypto key and re-process the update. + aesKey = key; + processUpdate(data, init); + }); + }; + + // Attach the listener to the dialog box and show it. + passwordDecryptE.addEventListener("click", acceptKeyFunc); + passwordInputE.value = ""; + promptE.style.display = "block"; + passwordInputE.focus(); + } + + return; + + } else if (data.encrypted) { + // The data is encrypted, but now we have a key we can use to decrypt + // it. Decrypt each point using the key. + var algo = {name: "AES-CBC"}; + + var pointPromises = []; + for (var i = 0; i < data.points.length; i++) { + algo.iv = byteArray(data.points[i][0]); + var promises = []; + for (var j = 1; j < data.points[i].length; j++) { + // Check that the array entry is not null to prevent an + // exception. If the entry is null, push a promise that returns + // null to the array to maintain indexing in the decrypted + // result. + if (data.points[i][j] !== null) { + promises.push(crypto.subtle.decrypt(algo, aesKey, byteArray(data.points[i][j]))); + } else { + promises.push(new Promise(function(resolve, reject) { + resolve(null); + })); + } + } + pointPromises.push(Promise.all(promises)); + } + + // Wait for all points to be decrypted. + Promise + .all(pointPromises) + .then(function(values) { + // Parse all points and conver them to floating point values + // (all values in the array are currently numbers). + var decoder = new TextDecoder("utf-8"); + for (var i = 0; i < values.length; i++) { + for (var j = 0; j < values[i].length; j++) { + // Check that the value isn't null to avoid exceptions. + if (values[i][j] !== null) { + data.points[i][j] = parseFloat(decoder.decode(values[i][j])); + } else { + data.points[i][j] = null; + } + } + // The IV was the first item in the array, so all items have + // been shifted up once. Pop the last array element off. + data.points[i].pop(); + } + + // Flag the data as unencrypted and re-process the update. + data.encrypted = false; + processUpdate(data, init); + }) + .catch(function(error) { + // Decryption error. Most likely incorrect password. Reset the + // key and prompt the user for the password again. + console.log(error); + aesKey = null; + if (!init) { + clearInterval(fetchIntv); + clearInterval(countIntv); + } + processUpdate(data, true); + }); + + return; + } + + // If flagged to initialize, set up polling. + if (init) setNewInterval(data.expire, data.interval, data.serverTime); + var userListE = document.getElementById("user-list"); + + for (var user in users) { + if (!users.hasOwnProperty(user)) continue; + var locData = users[user]; + + if (!shares.hasOwnProperty(user)) { + // Add an entry to the user list. + var listE = document.createElement("p"); + listE.textContent = user; + listE.dataset.user = user; + listE.style.display = "none"; + listE.addEventListener("click", function() { + var userListPopupE = document.getElementById("user-list-popup"); + if (userListPopupE !== null && userDetailsE !== null) { + userListPopupE.style.display = "none"; + userDetailsE.style.display = "block"; + var user = this.dataset.user; + var userDetailsHeaderE = document.getElementById("user-details-header"); + if (userDetailsHeaderE !== null) userDetailsHeaderE.textContent = user; + if (userDetailsFollowE !== null) userDetailsFollowE.dataset.user = user; + if (userDetailsNavigateE !== null) userDetailsNavigateE.dataset.user = user; + } + }); + if (userListE !== null) userListE.appendChild(listE); + + shares[user] = { + "marker": null, + "circle": null, + "icon": null, + "points": [], + "id": Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 5), + "listEntry": listE, + "state": "live" + }; + } + + // Get the last location received. + var lastPoint = shares[user].points.length > 0 ? shares[user].points[shares[user].points.length - 1] : null; + + for (var i = 0; i < users[user].length; i++) { + var lat = users[user][i][0]; + var lon = users[user][i][1]; + var time = users[user][i][2]; + var prov = users[user][i][3]; + var acc = users[user][i][4]; + var spd = users[user][i][5]; + + // Default to "Fine" provider for older clients. + if (prov === null) prov = LOC_PROVIDER_FINE; + // Determine the icon color to use depending on provider. + shares[user].state = prov == LOC_PROVIDER_FINE ? "live" : "rough"; + var iconColor = STATE_LIVE_COLOR; + if (shares[user].state == "rough") iconColor = STATE_ROUGH_COLOR; + + // Check if the location should be added. Only add new location points + // if the point was not recorded before the last recorded point. + if (lastPoint === null || time > lastPoint.time) { + var line = null; + if (shares[user].marker == null) { + // Add a marker to the map if it's not already there. + shares[user].icon = L.divIcon({ + html: + '
' + + '
' + + '

' + + '' + + '' + + '0.0 ' + + VELOCITY_UNIT.unit + + '' + + '' + + '

' + + '
', + iconAnchor: [33, 18] + }); + shares[user].marker = L.marker([lat, lon], {icon: shares[user].icon}).on("click", function() { + follow(this.haukUser); + }); + shares[user].marker.haukUser = user; + shares[user].marker.addTo(markerLayer); + } else { + // If there is a marker, draw a line from its last location + // instead and move the marker. + line = L.polyline([shares[user].marker.getLatLng(), [lat, lon]], {color: TRAIL_COLOR}).addTo(markerLayer); + shares[user].marker.setLatLng([lat, lon]); + } + // Draw an accuracy circle if GPS accuracy was provided by the + // client. + if (acc !== null && shares[user].circle == null) { + shares[user].circle = L.circle([lat, lon], {radius: acc, fillColor: iconColor, fillOpacity: 0.25, color: iconColor, opacity: 0.5, interactive: false}).addTo(circleLayer); + } else if (shares[user].circle !== null) { + shares[user].circle.setLatLng([lat, lon]); + if (acc !== null) shares[user].circle.setRadius(acc); + } + shares[user].points.push({lat: lat, lon: lon, line: line, time: time, spd: spd, acc: acc}); + lastPoint = shares[user].points[shares[user].points.length - 1]; + } + } + + if (lastPoint !== null) shares[user].listEntry.style.display = "block"; + + var eVelocity = document.getElementById("velocity-" + shares[user].id) + var vel = 0; + if (lastPoint !== null && lastPoint.spd !== null && eVelocity !== null) { + // Prefer client-provided speed if possible. + vel = lastPoint.spd * VELOCITY_UNIT.mpsMultiplier; + eVelocity.textContent = vel.toFixed(1); + } else if (eVelocity !== null) { + // If the client didn't provide its speed, calculate it locally from its + // list of locations. + var dist = 0; + var time = 0; + var idx = shares[user].points.length; + + // Iterate over all locations backwards until we either reach our + // required VELOCITY_DELTA_TIME, or we run out of points. + while (idx > 2) { + idx--; + var pt1 = shares[user].points[idx - 1]; + var pt2 = shares[user].points[idx]; + var dTime = pt2.time - pt1.time; + + // If the new time does not exceed the VELOCITY_DELTA_TIME, add the + // time and distance deltas to the appropriate sum for averaging; + // otherwise, break the loop and proceed to calculate. + if (time + dTime <= VELOCITY_DELTA_TIME) { + time += dTime; + dist += distance(pt1, pt2); + } else { + break; + } + } + + // Update the UI with the velocity. + vel = velocity(dist, time); + eVelocity.textContent = vel.toFixed(1);; + } + + // Flag that the first location has been received, for map centering. + if (lastPoint !== null && !hasReceivedFirst) { + hasReceivedFirst = true; + } + + // Move the marker if needed. + if (lastPoint !== null && !multiUser && following !== null) { + map.panTo([lastPoint.lat, lastPoint.lon]); + } else if (lastPoint !== null && multiUser && following == shares[user].id) { + map.panTo([lastPoint.lat, lastPoint.lon]); + } + + // Rotate the marker to the direction of movement. + var eArrow = document.getElementById("arrow-" + shares[user].id); + if (eArrow !== null && shares[user].points.length >= 2) { + var last = shares[user].points.length - 1; + eArrow.style.transform = "rotate(" + angle(shares[user].points[last - 1], shares[user].points[last]) + "deg)"; + if (vel.toFixed(1) == "0.0") { + eArrow.className = "arrow still-" + shares[user].state; + } else { + eArrow.className = "arrow moving-" + shares[user].state; + } + } + + // Prune the array of locations so it does not exceed our MAX_POINTS defined + // in the config. + if (shares[user].points.length > MAX_POINTS) { + var remove = shares[user].points.splice(0, shares[user].points.length - MAX_POINTS); + for (var j = 0; j < remove.length; j++) if (remove[j].line !== null) map.removeLayer(remove[j].line); + } + + // Add the user's nickname if this is a group share. + var nameE = document.getElementById("nickname-" + shares[user].id); + if (nameE !== null && multiUser) { + nameE.textContent = user; + nameE.innerHTML += "
"; + nameE.style.fontWeight = "bold"; + } + } + + for (var user in shares) { + if (!shares.hasOwnProperty(user)) continue; + + // Gray out the user's location if no data has been received for the + // OFFLINE_TIMEOUT. + var eArrow = document.getElementById("arrow-" + shares[user].id); + if (eArrow !== null) { + var last = shares[user].points.length - 1; + var point = shares[user].points[last]; + var eLabel = document.getElementById("label-" + shares[user].id); + var eLastSeen = document.getElementById("last-seen-" + shares[user].id); + + if (point.time < data.serverTime - OFFLINE_TIMEOUT) { + eArrow.className = eArrow.className.split("live").join("dead").split("rough").join("dead"); + if (eLabel !== null) eLabel.className = 'dead'; + if (eLastSeen !== null) { + // Calculate time since last update and choose an + // appropriate unit. + var time = Math.round(data.serverTime - point.time); + var unit = LANG["last_update_seconds"]; + if (time >= 60) { + time = Math.floor(time / 60); + unit = LANG["last_update_minutes"]; + if (time >= 60) { + time = Math.floor(time / 60); + unit = LANG["last_update_hours"]; + if (time >= 24) { + time = Math.floor(time / 24); + unit = LANG["last_update_days"]; + } + } + } + eLastSeen.textContent = unit.split("{{time}}").join(time); + } + setAccuracyCircleColor(shares[user].circle, STATE_DEAD_COLOR); + } else { + eArrow.className = eArrow.className.split("dead").join(shares[user].state); + if (eLabel !== null) eLabel.className = shares[user].state; + var iconColor = STATE_LIVE_COLOR; + if (shares[user].state == "rough") iconColor = STATE_ROUGH_COLOR; + setAccuracyCircleColor(shares[user].circle, iconColor); + } + } + } + + // On first location update, center the map so it shows all participants. + if (hasReceivedFirst && !hasInitiated) { + hasInitiated = true; + noGPS.style.display = "none"; + var mapOuterE = document.getElementById("mapouter"); + if (mapOuterE !== null) { + mapOuterE.style.visibility = "visible"; + } + autoCenter(); + + // Auto-follow single-user shares. + if (!multiUser) { + following = true; + } + } +} + +function setAccuracyCircleColor(circle, color) { + if (circle) { + circle.setStyle({ + fillColor: color, + color: color + }); + } +} + +// Calculates the distance between two points on a sphere using the Haversine +// algorithm. +function distance(from, to) { + var d2r = Math.PI / 180; + var fla = from.lat * d2r, tla = to.lat * d2r; + var flo = from.lon * d2r, tlo = to.lon * d2r; + var havLat = Math.sin((tla - fla) / 2); havLat *= havLat; + var havLon = Math.sin((tlo - flo) / 2); havLon *= havLon; + var hav = havLat + Math.cos(fla) * Math.cos(tla) * havLon; + var d = Math.asin(Math.sqrt(hav)); + return d; +} + +// Calculates a velocity using the velocity unit from the config. +function velocity(distance, intv) { + if (intv == 0) return 0.0; + return distance * VELOCITY_UNIT.mpsMultiplier * HAV_MOD / intv; +} + +// Calculates the bearing between two points on a sphere in degrees. +function angle(from, to) { + var d2r = Math.PI / 180; + var fromLat = from.lat * d2r, toLat = to.lat * d2r; + var fromLon = from.lon * d2r, toLon = to.lon * d2r; + /* + Calculation code by krishnar from + https://stackoverflow.com/a/52079217 + */ + var x = Math.cos(fromLat) * Math.sin(toLat) + - Math.sin(fromLat) * Math.cos(toLat) * Math.cos(toLon - fromLon); + var y = Math.sin(toLon - fromLon) * Math.cos(toLat); + var heading = Math.atan2(y, x) / d2r; + + return (heading + 360) % 360; +} diff --git a/frontend/style.css b/frontend/style.css new file mode 100644 index 0000000..4967b12 --- /dev/null +++ b/frontend/style.css @@ -0,0 +1,345 @@ +@charset "UTF-8"; +/* The main stylesheet for the Hauk web view interface. */ + +* { + font-family: sans-serif; +} + +/* Override browser-specific defaults. */ +body { + background-color: #fff; + color: #000; +} + +.hidden { + display: none; +} + +/* The map should cover the entire viewport. */ +#mapouter { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + /* Hide by default while we check whether or not the share exists. */ + visibility: hidden; +} + +#map { + width: 100%; + height: 100%; +} + +/* Popup covers that should display on top of the map. */ +.cover { + position: absolute; + width: 80vmin; + height: 80vmin; + top: 0; + left: 50%; + z-index: 2000; + background-color: #fff; + text-align: center; + padding: 10vmin; + transform: translateX(-50%); +} + +/* Hauk logo. */ +.cover > img.logo { + width: 100%; +} + +/* Hauk logo. */ +.cover > img.pending { + width: 60%; +} + +/* Popup header. */ +.cover > p.header { + font-size: 8vmin; + font-weight: bold; +} + +/* Searching for GNSS signal header (SVG image above, no need for margin). */ +#searching > p.header { + margin-top: 0; +} + +/* Default page header. (Logo above, need custom margin.) */ +.point-app-to { + margin-top: 7vmin; +} + +/* Popup information. */ +.cover > p.body { + font-size: 4vmin; +} + +/* Dialog window that contains a title, message and button. This is the outer + box, with a semitransparent black background to provide shading against the + map, which it renders on top of. */ +.dialog { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + z-index: 2000; + background-color: rgba(0, 0, 0, 0.5); +} + +/* The actual message dialog itself. */ +.dialog > div { + width: 300px; + max-width: 80vw; + background-color: white; + padding: 5px 20px; + position: relative; + top: 50%; + left: 50%; + transform: translateX(-50%) translateY(-50%); +} + +.dialog p.header { + font-size: 1.2em; + font-weight: bold; +} + +/* Ensure the button is big enough to be clicked on all devices. */ +.dialog input[type=button] { + font-size: 1em; +} + +/* Ensure the password prompt fills the dialog box. */ +.dialog input[type=password] { + width: calc(100% - 20px); +} + +/* Visible on the root page of Hauk. */ +#url { + color: #d80037; + font-family: monospace; + font-size: 5vmin; +} +.store-icon { + margin-top: 1vmin; + width: 40vmin; +} +a:first-child > .store-icon { + margin-left: -5vmin; +} +a:last-child > .store-icon { + margin-right: -5vmin; +} + +/* Display the Hauk logo in the bottom left corner of the map if the viewport is + wide enough to accomodate it. If it's so narrow that the Leaflet attribution + could be covered by it, display it in the bottom left corner instead, above + the attribution. */ +@media (max-width: 700px) { + #logo { + position: fixed; + bottom: 20px; + right: 5px; + z-index: 1000; + } +} +@media (min-width: 700.001px) { + #logo { + position: fixed; + bottom: 5px; + left: 10px; + z-index: 1000; + } +} + +#logo div { + width: 73.33479px; + height: 28.853556px; + background: url(./assets/logo.svg) no-repeat; + background-size: cover; + margin: auto; +} + +.leaflet-control-locate-inactive { + background: url(./assets/controls/locate-inactive.svg) no-repeat; + background-size: cover; +} + +.leaflet-control-locate-pending { + background: url(./assets/controls/locate-pending.svg) no-repeat; + background-size: cover; +} + +.leaflet-control-locate-active { + background: url(./assets/controls/locate-active.svg) no-repeat; + background-size: cover; +} + +.leaflet-control-radar { + background: url(./assets/controls/radar.svg) no-repeat; + background-size: cover; +} + +/** User list. **/ +.popup-list { + max-height: 60vh; + overflow-y: scroll; + margin: 0; +} +.popup-list > a { + text-decoration: none; + color: #000; +} +.popup-list p { + background-color: #eee; + margin: 0; + padding: 9px 20px; + cursor: pointer; +} +.popup-list p:hover { + background-color: #ddd; +} + +/* The notch at the top of the screen that shows the time remaining of the + share. Outer container. */ +#notch { + top: 0; + left: 50%; + transform: translateX(-50%); + z-index: 1000; + position: fixed; +} + +/* The left and right triangles. */ +#notch div.tri { + width: 0; + height: 0; + position: absolute; + border-style: solid; +} + +/* The left triangle. */ +#notch div.t-left { + left: 0.15px; + top: 0; + border-width: 0 23px 23px 0; + border-color: transparent rgba(0,0,0,0.5) transparent transparent; +} + +/* The right triangle. */ +#notch div.t-right { + right: 0.15px; + top: 0; + border-width: 23px 23px 0 0; + border-color: rgba(0,0,0,0.5) transparent transparent transparent; +} + +/* The middle part of the notch, with the countdown itself. */ +#notch div.inner { + width: 80px; + height: 20px; + text-align: center; + padding: 1.5px; + background-color: rgba(0,0,0,0.5); + color: #fff; + margin: 0 23px; +} + +/* Change the color if client is offline. */ +#notch.offline div.t-left { + border-color: transparent rgba(165,0,42,0.5) transparent transparent; +} +#notch.offline div.t-right { + border-color: rgba(165,0,42,0.5) transparent transparent transparent; +} +#notch.offline div.inner { + display: none; +} +#notch.offline div.inner.offline { + background-color: rgba(165,0,42,0.5); + display: block; +} + +/* The outer marker div. */ +.marker { + width: 66px; + height: 62px; +} + +/* The arrow within the marker div. */ +.arrow.moving-live { + background-image: url(./assets/markers/moving-live.svg); +} + +.arrow.moving-rough { + background-image: url(./assets/markers/moving-rough.svg); +} + +.arrow.moving-dead { + background-image: url(./assets/markers/moving-dead.svg); +} + +.arrow.still-live { + background-image: url(./assets/markers/still-live.svg); +} + +.arrow.still-rough { + background-image: url(./assets/markers/still-rough.svg); +} + +.arrow.still-dead { + background-image: url(./assets/markers/still-dead.svg); +} + +.arrow.still-self { + background-image: url(./assets/markers/still-self.svg); +} + +.arrow { + background-size: cover; + background-repeat: no-repeat; + width: 36px; + height: 36px; + margin: auto; +} + +/* The velocity indicator on the marker div. */ +.marker p { + font-size: 0.9em; + color: white; + width: 100%; + border-radius: 15px; + text-align: center; + padding: 2px 0; + line-height: 100%; + font-family: sans-serif; + overflow: hidden; + white-space: nowrap; + text-overflow: clip; +} + +/* Location data has been received recently. */ +.marker p.live, .marker p.rough { + background-color: rgba(0,0,0,0.5); +} + +.marker p.live > span.offline, .marker p.rough > span.offline { + display: none; +} + +/* No location data has been received for a while. */ +.marker p.dead { + background-color: rgba(165,0,42,0.5); +} + +.marker p.dead > span.velocity { + display: none; +} + +/* Hide the default white box in the top left corner of the marker div. */ +.leaflet-div-icon { + background: none; + border: none; +}