├── .gitignore ├── favicon.ico ├── img ├── logo.png ├── blank.png ├── fluidicon.png ├── openhand.cur ├── closedhand.cur ├── hab-spinner.gif ├── marker-you.png ├── markers │ ├── iss.png │ ├── nyan.gif │ ├── shadow.png │ ├── antenna.png │ ├── hab_nyan.gif │ ├── nyan-afro.gif │ ├── nyan-coin.gif │ ├── nyan-cool.gif │ ├── nyan-mon.gif │ ├── balloon-pop.png │ ├── marker_hole.png │ ├── nyan-mummy.gif │ ├── nyan-pirate.gif │ ├── antenna-bronze.png │ ├── antenna-gold.png │ ├── antenna-silver.png │ ├── antenna-white.png │ ├── balloon-xmark.png │ ├── nyan-gameboy.gif │ ├── nyan-pumpkin.gif │ ├── nyan-tothemax.gif │ ├── payload-recovered.png │ ├── payload-not-recovered.png │ ├── target.svg │ ├── payload.svg │ ├── balloon.svg │ ├── car.svg │ └── parachute.svg ├── sondehub_logo.png ├── apple-touch-icon.png ├── icons │ ├── icon_x192.png │ ├── icon_x512.png │ ├── nyan_icon_x192.png │ ├── nyan_icon_x512.png │ ├── maskable_icon_x128.png │ ├── maskable_icon_x192.png │ ├── maskable_icon_x384.png │ ├── maskable_icon_x48.png │ ├── maskable_icon_x512.png │ ├── maskable_icon_x72.png │ └── maskable_icon_x96.png ├── sondehub_au_amateur.png ├── splash │ ├── splash-wide.png │ └── splash-narrow.png └── screenshots │ ├── screenshot1.png │ ├── screenshot2.png │ └── screenshot3.png ├── resources ├── car.psd ├── antenna.ai ├── logo.psd ├── antenna.psd ├── balloon.psd ├── fluid-icon.psd ├── nyan_icon.psd ├── parachute.psd ├── concept-app-tablet.png ├── concept-app-portrait.png ├── mobiletracker-screencap.png └── antenna.svg ├── css ├── fullscreen.png ├── fullscreen@2x.png ├── images │ ├── layers.png │ ├── layers-2x.png │ ├── marker-icon.png │ ├── marker-icon-2x.png │ └── marker-shadow.png ├── leaflet.fullscreen.css ├── habitat-font.css ├── layout.css ├── base.css ├── skeleton.css ├── leaflet.css └── main.css ├── font ├── HabitatFont.eot ├── HabitatFont.ttf ├── HabitatFont.woff └── Roboto-regular.woff ├── js ├── pwa.js ├── chasecar.lib.js ├── suncalc.js ├── Leaflet.fullscreen.min.js ├── flight_doc.js ├── L.Terminator.js ├── plot_config.js ├── format.js ├── L.TileLayer.NoGap.js ├── leaflet.antimeridian-src.js └── rbush.js ├── serve.py ├── opensearchspec.xml ├── DEVELOPER_README.md ├── glyphs ├── icon-balloon.svg ├── icon-clock_simple.svg ├── icon-compass_simple.svg ├── icon-code.svg ├── icon-clock.svg ├── icon-weather.svg └── icon-compass.svg ├── LICENSE ├── manifest.json ├── README.md ├── service-worker.template.js └── embed-preview.html /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.pyc 3 | *.log 4 | /index.html 5 | /service-worker.js 6 | tiles/ 7 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/favicon.ico -------------------------------------------------------------------------------- /img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/logo.png -------------------------------------------------------------------------------- /img/blank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/blank.png -------------------------------------------------------------------------------- /img/fluidicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/fluidicon.png -------------------------------------------------------------------------------- /img/openhand.cur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/openhand.cur -------------------------------------------------------------------------------- /resources/car.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/resources/car.psd -------------------------------------------------------------------------------- /css/fullscreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/css/fullscreen.png -------------------------------------------------------------------------------- /font/HabitatFont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/font/HabitatFont.eot -------------------------------------------------------------------------------- /font/HabitatFont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/font/HabitatFont.ttf -------------------------------------------------------------------------------- /img/closedhand.cur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/closedhand.cur -------------------------------------------------------------------------------- /img/hab-spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/hab-spinner.gif -------------------------------------------------------------------------------- /img/marker-you.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/marker-you.png -------------------------------------------------------------------------------- /img/markers/iss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/markers/iss.png -------------------------------------------------------------------------------- /img/markers/nyan.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/markers/nyan.gif -------------------------------------------------------------------------------- /js/pwa.js: -------------------------------------------------------------------------------- 1 | if ('serviceWorker' in navigator) { 2 | navigator.serviceWorker.register('/service-worker.js') 3 | } -------------------------------------------------------------------------------- /resources/antenna.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/resources/antenna.ai -------------------------------------------------------------------------------- /resources/logo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/resources/logo.psd -------------------------------------------------------------------------------- /css/fullscreen@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/css/fullscreen@2x.png -------------------------------------------------------------------------------- /css/images/layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/css/images/layers.png -------------------------------------------------------------------------------- /font/HabitatFont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/font/HabitatFont.woff -------------------------------------------------------------------------------- /img/markers/shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/markers/shadow.png -------------------------------------------------------------------------------- /img/sondehub_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/sondehub_logo.png -------------------------------------------------------------------------------- /resources/antenna.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/resources/antenna.psd -------------------------------------------------------------------------------- /resources/balloon.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/resources/balloon.psd -------------------------------------------------------------------------------- /css/images/layers-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/css/images/layers-2x.png -------------------------------------------------------------------------------- /font/Roboto-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/font/Roboto-regular.woff -------------------------------------------------------------------------------- /img/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/apple-touch-icon.png -------------------------------------------------------------------------------- /img/icons/icon_x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/icons/icon_x192.png -------------------------------------------------------------------------------- /img/icons/icon_x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/icons/icon_x512.png -------------------------------------------------------------------------------- /img/markers/antenna.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/markers/antenna.png -------------------------------------------------------------------------------- /img/markers/hab_nyan.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/markers/hab_nyan.gif -------------------------------------------------------------------------------- /img/markers/nyan-afro.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/markers/nyan-afro.gif -------------------------------------------------------------------------------- /img/markers/nyan-coin.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/markers/nyan-coin.gif -------------------------------------------------------------------------------- /img/markers/nyan-cool.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/markers/nyan-cool.gif -------------------------------------------------------------------------------- /img/markers/nyan-mon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/markers/nyan-mon.gif -------------------------------------------------------------------------------- /resources/fluid-icon.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/resources/fluid-icon.psd -------------------------------------------------------------------------------- /resources/nyan_icon.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/resources/nyan_icon.psd -------------------------------------------------------------------------------- /resources/parachute.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/resources/parachute.psd -------------------------------------------------------------------------------- /css/images/marker-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/css/images/marker-icon.png -------------------------------------------------------------------------------- /img/markers/balloon-pop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/markers/balloon-pop.png -------------------------------------------------------------------------------- /img/markers/marker_hole.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/markers/marker_hole.png -------------------------------------------------------------------------------- /img/markers/nyan-mummy.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/markers/nyan-mummy.gif -------------------------------------------------------------------------------- /img/markers/nyan-pirate.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/markers/nyan-pirate.gif -------------------------------------------------------------------------------- /img/sondehub_au_amateur.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/sondehub_au_amateur.png -------------------------------------------------------------------------------- /img/splash/splash-wide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/splash/splash-wide.png -------------------------------------------------------------------------------- /css/images/marker-icon-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/css/images/marker-icon-2x.png -------------------------------------------------------------------------------- /css/images/marker-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/css/images/marker-shadow.png -------------------------------------------------------------------------------- /img/icons/nyan_icon_x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/icons/nyan_icon_x192.png -------------------------------------------------------------------------------- /img/icons/nyan_icon_x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/icons/nyan_icon_x512.png -------------------------------------------------------------------------------- /img/markers/antenna-bronze.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/markers/antenna-bronze.png -------------------------------------------------------------------------------- /img/markers/antenna-gold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/markers/antenna-gold.png -------------------------------------------------------------------------------- /img/markers/antenna-silver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/markers/antenna-silver.png -------------------------------------------------------------------------------- /img/markers/antenna-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/markers/antenna-white.png -------------------------------------------------------------------------------- /img/markers/balloon-xmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/markers/balloon-xmark.png -------------------------------------------------------------------------------- /img/markers/nyan-gameboy.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/markers/nyan-gameboy.gif -------------------------------------------------------------------------------- /img/markers/nyan-pumpkin.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/markers/nyan-pumpkin.gif -------------------------------------------------------------------------------- /img/markers/nyan-tothemax.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/markers/nyan-tothemax.gif -------------------------------------------------------------------------------- /img/splash/splash-narrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/splash/splash-narrow.png -------------------------------------------------------------------------------- /img/icons/maskable_icon_x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/icons/maskable_icon_x128.png -------------------------------------------------------------------------------- /img/icons/maskable_icon_x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/icons/maskable_icon_x192.png -------------------------------------------------------------------------------- /img/icons/maskable_icon_x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/icons/maskable_icon_x384.png -------------------------------------------------------------------------------- /img/icons/maskable_icon_x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/icons/maskable_icon_x48.png -------------------------------------------------------------------------------- /img/icons/maskable_icon_x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/icons/maskable_icon_x512.png -------------------------------------------------------------------------------- /img/icons/maskable_icon_x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/icons/maskable_icon_x72.png -------------------------------------------------------------------------------- /img/icons/maskable_icon_x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/icons/maskable_icon_x96.png -------------------------------------------------------------------------------- /img/screenshots/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/screenshots/screenshot1.png -------------------------------------------------------------------------------- /img/screenshots/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/screenshots/screenshot2.png -------------------------------------------------------------------------------- /img/screenshots/screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/screenshots/screenshot3.png -------------------------------------------------------------------------------- /resources/concept-app-tablet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/resources/concept-app-tablet.png -------------------------------------------------------------------------------- /img/markers/payload-recovered.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/markers/payload-recovered.png -------------------------------------------------------------------------------- /resources/concept-app-portrait.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/resources/concept-app-portrait.png -------------------------------------------------------------------------------- /img/markers/payload-not-recovered.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/img/markers/payload-not-recovered.png -------------------------------------------------------------------------------- /resources/mobiletracker-screencap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/sondehub-amateur-tracker/HEAD/resources/mobiletracker-screencap.png -------------------------------------------------------------------------------- /serve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from http.server import HTTPServer, SimpleHTTPRequestHandler, test 3 | import sys 4 | 5 | class CORSRequestHandler (SimpleHTTPRequestHandler): 6 | def end_headers (self): 7 | self.send_header('Access-Control-Allow-Origin', '*') 8 | SimpleHTTPRequestHandler.end_headers(self) 9 | 10 | if __name__ == '__main__': 11 | test(CORSRequestHandler, HTTPServer, port=int(sys.argv[1]) if len(sys.argv) > 1 else 8000) 12 | -------------------------------------------------------------------------------- /opensearchspec.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | SondeHub Amateur 4 | SondeHub Amateur - Search 5 | UTF-8 6 | hello@rgp.io 7 | https://sondehub.org/favicon.ico 8 | Luke Prior 9 | 10 | 11 | -------------------------------------------------------------------------------- /DEVELOPER_README.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | To get a copy of the code and run a test web server: 4 | 5 | 1. [Fork the repository](https://github.com/projecthorus/sondehub-amateur-tracker/fork) by visiting [https://github.com/projecthorus/sondehub-amateur-tracker/fork](https://github.com/projecthorus/sondehub-amateur-tracker/fork). 6 | 2. Clone the repository with your git tool of choice. 7 | 3. Run `build.sh` to generate `index.html` and `service-worker.js`. 8 | 4. Run `python serve.py` to run a simple web server to (This requires python 3.x) 9 | 5. Visit [http://localhost:8000](http://localhost:8000) to view the local version of the server! 10 | -------------------------------------------------------------------------------- /glyphs/icon-balloon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /img/markers/target.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /img/markers/payload.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 23 | 24 | 32 | 33 | 35 | 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2016 Rossen Georgiev 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /css/leaflet.fullscreen.css: -------------------------------------------------------------------------------- 1 | .leaflet-control-fullscreen a { 2 | background:#fff url(fullscreen.png) no-repeat 0 0; 3 | background-size:26px 52px; 4 | } 5 | .leaflet-touch .leaflet-control-fullscreen a { 6 | background-position: 2px 2px; 7 | } 8 | .leaflet-fullscreen-on .leaflet-control-fullscreen a { 9 | background-position:0 -26px; 10 | } 11 | .leaflet-touch.leaflet-fullscreen-on .leaflet-control-fullscreen a { 12 | background-position: 2px -24px; 13 | } 14 | 15 | @media (orientation: landscape) { 16 | .leaflet-control-fullscreen { 17 | position:relative; 18 | top:-25px; 19 | } 20 | .leaflet-fullscreen-on .leaflet-control-fullscreen { 21 | position:relative; 22 | top:0px; 23 | } 24 | } 25 | 26 | 27 | /* Do not combine these two rules; IE will break. */ 28 | .leaflet-container:-webkit-full-screen { 29 | width:100%!important; 30 | height:100%!important; 31 | } 32 | .leaflet-container.leaflet-fullscreen-on { 33 | width:100%!important; 34 | height:100%!important; 35 | } 36 | 37 | .leaflet-pseudo-fullscreen { 38 | position:fixed!important; 39 | width:100%!important; 40 | height:100%!important; 41 | top:0!important; 42 | left:0!important; 43 | z-index:99999; 44 | } 45 | 46 | @media 47 | (-webkit-min-device-pixel-ratio:2), 48 | (min-resolution:192dpi) { 49 | .leaflet-control-fullscreen a { 50 | background-image:url(fullscreen@2x.png); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /js/chasecar.lib.js: -------------------------------------------------------------------------------- 1 | /* SondeHub ChaseCar lib 2 | * Uploads geolocation for chase cars to SondeHub 3 | * 4 | * Author: Rossen Gerogiev 5 | * Requires: jQuery 6 | * 7 | * Updated to SondeHub v2 by Mark Jessop 8 | */ 9 | 10 | ChaseCar = { 11 | db_uri: "https://api.v2.sondehub.org/amateur/listeners", // Sondehub API 12 | }; 13 | 14 | // Updated SondeHub position upload function. 15 | // Refer PUT listeners API here: https://generator.swagger.io/?url=https://raw.githubusercontent.com/projecthorus/sondehub-infra/main/swagger.yaml 16 | // @callsign string 17 | // @position object (geolocation position object) 18 | ChaseCar.updatePosition = function(callsign, position) { 19 | if(!position || !position.coords) return; 20 | 21 | // Set altitude to zero if not provided. 22 | _position_alt = ((!!position.coords.altitude) ? position.coords.altitude : 0); 23 | 24 | var _doc = { 25 | "software_name": "SondeHub-Amateur", 26 | "software_version": document.body.dataset.version, 27 | "uploader_callsign": callsign, 28 | "uploader_position": [position.coords.latitude, position.coords.longitude, _position_alt], 29 | "uploader_antenna": "Mobile Station", 30 | "uploader_contact_email": "none@none.com", 31 | "mobile": true 32 | }; 33 | 34 | // push the doc to sondehub 35 | $.ajax({ 36 | type: "PUT", 37 | url: ChaseCar.db_uri, 38 | contentType: "application/json; charset=utf-8", 39 | dataType: "json", 40 | data: JSON.stringify(_doc), 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SondeHub Amateur", 3 | "short_name": "SondeHub", 4 | "description": "A webapp for tracking amateur high altitude balloon flights.", 5 | "theme_color": "#00a3d3", 6 | "background_color": "#00a3d3", 7 | "display": "standalone", 8 | "categories": ["utilities"], 9 | "scope": "/", 10 | "start_url": "/", 11 | "icons": [ 12 | { 13 | "src": "/img/icons/maskable_icon_x192.png", 14 | "type": "image/png", 15 | "sizes": "192x192", 16 | "purpose": "maskable" 17 | }, 18 | { 19 | "src": "/img/icons/maskable_icon_x512.png", 20 | "type": "image/png", 21 | "sizes": "512x512", 22 | "purpose": "maskable" 23 | }, 24 | { 25 | "src": "/img/icons/icon_x192.png", 26 | "type": "image/png", 27 | "sizes": "192x192", 28 | "purpose": "any" 29 | }, 30 | { 31 | "src": "/img/icons/icon_x512.png", 32 | "type": "image/png", 33 | "sizes": "512x512", 34 | "purpose": "any" 35 | } 36 | ], 37 | "shortcuts": [ 38 | { 39 | "name": "Nyan Mode", 40 | "short_name": "Nyan", 41 | "description": "Start the tracker with Nyan Cat mode enabled", 42 | "url": "/#!nyan=1", 43 | "icons": [{ "src": "/img/icons/nyan_icon_x192.png", "sizes": "192x192" }] 44 | } 45 | ], 46 | "screenshots": [ 47 | { 48 | "src": "/img/screenshots/screenshot1.png", 49 | "type": "image/png", 50 | "sizes": "1242x2208" 51 | }, 52 | { 53 | "src": "/img/screenshots/screenshot2.png", 54 | "type": "image/png", 55 | "sizes": "1242x2208" 56 | }, 57 | { 58 | "src": "/img/screenshots/screenshot3.png", 59 | "type": "image/png", 60 | "sizes": "1242x2208" 61 | } 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SondeHub Amateur Tracker 2 | 3 | A fork of [sondehub-tracker](https://github.com/projecthorus/sondehub-tracker) for use with the [SondeHub Amateur ElasticSearch](https://github.com/projecthorus/sondehub-infra/wiki/ElasticSearch-Kibana-access) database. 4 | 5 | A webapp for tracking radiosondes. Works an desktop and mobile devices. 6 | The SondeHub Amateur tracker is a continuation of [tracker.habhub.org](https://tracker.habhub.org/). 7 | 8 | ## Features 9 | 10 | * Radiosonde Tracking using [SondeHub Amateur](https://github.com/projecthorus/sondehub-infra/wiki/ElasticSearch-Kibana-access) data. 11 | * Telemetry graph for each balloon 12 | * Near realtime weather overlays 13 | * Daylight cycle overlay, for long flights 14 | * Map tracker with Leaflet API 15 | * Run the app natively on IOS, Android, or desktop as a Progressive Wep App 16 | 17 | ### Geo position 18 | 19 | The app will ask for permission to use your location. 20 | This is required for some of the features. It is **important** to note that 21 | your location will not be made available or send to anyone. 22 | 23 | ## Browser requirements 24 | 25 | Any modern browser should be able to run the app. Some features are limited to Chromium based browsers. 26 | 27 | ## Contribute 28 | 29 | Don't hesitate to report any issues, or suggest improvements. Just visit the [issues page](https://github.com/projecthorus/sondehub-amateur-tracker/issues). 30 | Pull requests are welcome. 31 | 32 | ## Installation 33 | 34 | $ git clone https://github.com/projecthorus/sondehub-amateur-tracker.git 35 | $ ./build.sh 36 | $ python serve.py 37 | 38 | Visit [http://localhost:8000](http://localhost:8000) to view the local version of the tracker! 39 | 40 | ## Original design 41 | 42 | Author: Daniel Saul [@danielsaul](https://github.com/danielsaul) 43 | -------------------------------------------------------------------------------- /img/markers/balloon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 30 | 31 | 39 | 40 | 42 | 43 | 45 | 46 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /glyphs/icon-clock_simple.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Clock Icon 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | image/svg+xml 16 | 17 | 18 | 19 | Clock Icon 20 | 21 | 22 | 23 | 2014-06-11 24 | 25 | 26 | 27 | 28 | 29 | Rossen Georgiev 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | Rossen Georgiev 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /glyphs/icon-compass_simple.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Clock Compass 4 | 5 | 6 | 7 | image/svg+xml 8 | 9 | Clock Compass 10 | 11 | 2014-07-09 12 | 13 | 14 | Rossen Georgiev 15 | 16 | 17 | 18 | 19 | Rossen Georgiev 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /service-worker.template.js: -------------------------------------------------------------------------------- 1 | self.addEventListener('install', function(event) { 2 | event.waitUntil( 3 | caches.open("{VER}").then(function(cache) { 4 | return cache.addAll( 5 | [ 6 | '/css/base.css', 7 | '/css/skeleton.css', 8 | '/css/layout.css', 9 | '/css/habitat-font.css', 10 | '/css/main.css', 11 | '/css/leaflet.css', 12 | '/css/leaflet.fullscreen.css', 13 | '/js/leaflet.js', 14 | '/js/Leaflet.fullscreen.min.js', 15 | '/js/L.Terminator.js', 16 | '/js/L.TileLayer.NoGap.js', 17 | '/js/leaflet.antimeridian-src.js', 18 | '/js/paho-mqtt.js', 19 | '/js/jquery-1.12.4-min.js', 20 | '/js/iscroll.js', 21 | '/js/chasecar.lib.js', 22 | '/js/sondehub.js', 23 | '/js/app.js', 24 | '/js/colour-map.js', 25 | '/js/suncalc.js', 26 | '/js/flight_doc.js', 27 | '/js/format.js', 28 | '/js/rbush.js', 29 | '/js/pwa.js', 30 | '/js/_jquery.flot.js', 31 | '/js/plot_config.js', 32 | '/img/markers/balloon.svg', 33 | '/img/markers/car.svg', 34 | '/img/markers/parachute.svg', 35 | '/img/markers/payload.svg', 36 | '/img/markers/payload-not-recovered.png', 37 | '/img/markers/payload-recovered.png', 38 | '/img/markers/target.svg', 39 | '/img/markers/shadow.png', 40 | '/img/markers/balloon-pop.png', 41 | '/img/hab-spinner.gif', 42 | '/img/sondehub_logo.png', 43 | '/favicon.ico', 44 | '/font/HabitatFont.woff', 45 | '/font/Roboto-regular.woff', 46 | '/index.html' 47 | ] 48 | ); 49 | }) 50 | ); 51 | }); 52 | 53 | self.addEventListener('fetch', function (event) { 54 | event.respondWith( 55 | caches.match(event.request).then(function (response) { 56 | return response || fetch(event.request); 57 | }), 58 | ); 59 | }); 60 | -------------------------------------------------------------------------------- /js/suncalc.js: -------------------------------------------------------------------------------- 1 | // Sun Position Calculations from https://github.com/mourner/suncalc 2 | 3 | // shortcuts for easier to read formulas 4 | 5 | var PI = Math.PI, 6 | sin = Math.sin, 7 | cos = Math.cos, 8 | tan = Math.tan, 9 | asin = Math.asin, 10 | atan = Math.atan2, 11 | acos = Math.acos, 12 | rad = PI / 180; 13 | // date/time constants and conversions 14 | 15 | var dayMs = 1000 * 60 * 60 * 24, 16 | J1970 = 2440588, 17 | J2000 = 2451545; 18 | 19 | var earthradm = 6371008.8; // earth mean radius in meters 20 | 21 | function suncalc_toJulian(date) { return date.valueOf() / dayMs - 0.5 + J1970; } 22 | function suncalc_fromJulian(j) { return new Date((j + 0.5 - J1970) * dayMs); } 23 | function suncalc_toDays(date) { return suncalc_toJulian(date) - J2000; } 24 | 25 | 26 | // general calculations for position 27 | 28 | var suncalc_e = rad * 23.4397; // obliquity of the Earth 29 | 30 | function rightAscension(l, b) { return atan(sin(l) * cos(suncalc_e) - tan(b) * sin(suncalc_e), cos(l)); } 31 | function declination(l, b) { return asin(sin(b) * cos(suncalc_e) + cos(b) * sin(suncalc_e) * sin(l)); } 32 | 33 | function suncalc_azimuth(H, phi, dec) { return atan(sin(H), cos(H) * sin(phi) - tan(dec) * cos(phi)); } 34 | function suncalc_altitude(H, phi, dec) { return asin(sin(phi) * sin(dec) + cos(phi) * cos(dec) * cos(H)); } 35 | 36 | function siderealTime(d, lw) { return rad * (280.16 + 360.9856235 * d) - lw; } 37 | 38 | // general sun calculations 39 | 40 | function solarMeanAnomaly(d) { return rad * (357.5291 + 0.98560028 * d); } 41 | 42 | function eclipticLongitude(M) { 43 | 44 | var C = rad * (1.9148 * sin(M) + 0.02 * sin(2 * M) + 0.0003 * sin(3 * M)), // equation of center 45 | P = rad * 102.9372; // perihelion of the Earth 46 | 47 | return M + C + P + PI; 48 | } 49 | 50 | function sunCoords(d) { 51 | 52 | var M = solarMeanAnomaly(d), 53 | L = eclipticLongitude(M); 54 | 55 | return { 56 | dec: declination(L, 0), 57 | ra: rightAscension(L, 0) 58 | }; 59 | } 60 | 61 | var SunCalc = {}; 62 | 63 | 64 | // calculates sun position for a given date and latitude/longitude 65 | 66 | SunCalc.getPosition = function (date, lat, lng, ht) { 67 | 68 | var lw = rad * -lng, 69 | phi = rad * lat, 70 | d = suncalc_toDays(date), 71 | 72 | c = sunCoords(d), 73 | H = siderealTime(d, lw) - c.ra; 74 | 75 | return { 76 | azimuth: suncalc_azimuth(H, phi, c.dec), 77 | altitude: suncalc_altitude(H, phi, c.dec) + acos(earthradm / (earthradm + ht)) // adjust to horizon at altitude - From Steve Randall 78 | }; 79 | }; 80 | 81 | -------------------------------------------------------------------------------- /resources/antenna.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /css/habitat-font.css: -------------------------------------------------------------------------------- 1 | /* Habitat Font 2 | * icons specifically created for habitat websites and apps 3 | * 4 | * Inspired from Font Awesome @ https://github.com/FortAwesome 5 | * 6 | * Author: Rossen Georgiev @ http://rossengeorgiev.github.com/ 7 | */ 8 | 9 | @font-face { 10 | font-family: 'HabitatFont'; 11 | src: url('../font/HabitatFont.eot'); 12 | src: url('../font/HabitatFont.eot?#iefix') format('embedded-opentype'), 13 | url('../font/HabitatFont.woff') format('woff'), 14 | url('../font/HabitatFont.ttf') format('truetype'), 15 | url('../font/HabitatFont.svg#habitatfontregular') format('svg'); 16 | font-weight: normal; 17 | font-style: normal; 18 | } 19 | 20 | [class^="icon-"], 21 | [class*=" icon-"] { 22 | font-family: HabitatFont; 23 | font-weight: normal; 24 | font-style: normal; 25 | text-decoration: inherit; 26 | display: inline; 27 | width: auto; 28 | height: auto; 29 | line-height: normal; 30 | vertical-align: baseline; 31 | background-image: none !important; 32 | background-position: 0% 0%; 33 | background-repeat: repeat; 34 | } 35 | [class^="icon-"]:before, 36 | [class*=" icon-"]:before { 37 | text-decoration: inherit; 38 | display: inline-block; 39 | speak: none; 40 | } 41 | .icon-spin { 42 | display: inline-block; 43 | -moz-animation: spin 2s infinite linear; 44 | -o-animation: spin 2s infinite linear; 45 | -webkit-animation: spin 2s infinite linear; 46 | animation: spin 2s infinite linear; 47 | } 48 | @-moz-keyframes spin { 49 | 0% { -moz-transform: rotate(0deg); } 50 | 100% { -moz-transform: rotate(359deg); } 51 | } 52 | @-webkit-keyframes spin { 53 | 0% { -webkit-transform: rotate(0deg); } 54 | 100% { -webkit-transform: rotate(359deg); } 55 | } 56 | @-o-keyframes spin { 57 | 0% { -o-transform: rotate(0deg); } 58 | 100% { -o-transform: rotate(359deg); } 59 | } 60 | @-ms-keyframes spin { 61 | 0% { -ms-transform: rotate(0deg); } 62 | 100% { -ms-transform: rotate(359deg); } 63 | } 64 | @keyframes spin { 65 | 0% { transform: rotate(0deg); } 66 | 100% { transform: rotate(359deg); } 67 | } 68 | 69 | .icon-habhub:before { content: "\f000"; } 70 | .icon-compass:before { content: "\f001"; } 71 | .icon-locate-me:before { content: "\f002"; } 72 | .icon-car:before { content: "\f003"; } 73 | .icon-question:before { content: "\f004"; } 74 | .icon-location:before { content: "\f005"; } 75 | .icon-target:before { content: "\f006"; } 76 | .icon-earth:before { content: "\f007"; } 77 | .icon-daylight:before { content: "\f008"; } 78 | .icon-settings:before { content: "\f010"; } -------------------------------------------------------------------------------- /glyphs/icon-code.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | Code Icon 21 | 23 | 43 | 45 | 46 | 48 | image/svg+xml 49 | 51 | Code Icon 52 | 54 | 2014-06-06 55 | 56 | 57 | Rossen Georgiev 58 | 59 | 60 | 61 | 62 | Rossen Georgiev 63 | 64 | 65 | 66 | 67 | 68 | 72 | 73 | -------------------------------------------------------------------------------- /img/markers/car.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 40 | 41 | 51 | 52 | 57 | 58 | 59 | 60 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /js/Leaflet.fullscreen.min.js: -------------------------------------------------------------------------------- 1 | L.Control.Fullscreen=L.Control.extend({options:{position:"topleft",title:{"false":"View Fullscreen","true":"Exit Fullscreen"}},onAdd:function(map){var container=L.DomUtil.create("div","leaflet-control-fullscreen leaflet-bar leaflet-control");this.link=L.DomUtil.create("a","leaflet-control-fullscreen-button leaflet-bar-part",container);this.link.href="#";this._map=map;this._map.on("fullscreenchange",this._toggleTitle,this);this._toggleTitle();L.DomEvent.on(this.link,"click",this._click,this);return container},_click:function(e){L.DomEvent.stopPropagation(e);L.DomEvent.preventDefault(e);this._map.toggleFullscreen(this.options)},_toggleTitle:function(){this.link.title=this.options.title[this._map.isFullscreen()]}});L.Map.include({isFullscreen:function(){return this._isFullscreen||false},toggleFullscreen:function(options){var container=this.getContainer();if(this.isFullscreen()){if(options&&options.pseudoFullscreen){this._disablePseudoFullscreen(container)}else if(document.exitFullscreen){document.exitFullscreen()}else if(document.mozCancelFullScreen){document.mozCancelFullScreen()}else if(document.webkitCancelFullScreen){document.webkitCancelFullScreen()}else if(document.msExitFullscreen){document.msExitFullscreen()}else{this._disablePseudoFullscreen(container)}}else{if(options&&options.pseudoFullscreen){this._enablePseudoFullscreen(container)}else if(container.requestFullscreen){container.requestFullscreen()}else if(container.mozRequestFullScreen){container.mozRequestFullScreen()}else if(container.webkitRequestFullscreen){container.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT)}else if(container.msRequestFullscreen){container.msRequestFullscreen()}else{this._enablePseudoFullscreen(container)}}},_enablePseudoFullscreen:function(container){L.DomUtil.addClass(container,"leaflet-pseudo-fullscreen");this._setFullscreen(true);this.invalidateSize();this.fire("fullscreenchange")},_disablePseudoFullscreen:function(container){L.DomUtil.removeClass(container,"leaflet-pseudo-fullscreen");this._setFullscreen(false);this.invalidateSize();this.fire("fullscreenchange")},_setFullscreen:function(fullscreen){this._isFullscreen=fullscreen;var container=this.getContainer();if(fullscreen){L.DomUtil.addClass(container,"leaflet-fullscreen-on")}else{L.DomUtil.removeClass(container,"leaflet-fullscreen-on")}},_onFullscreenChange:function(e){var fullscreenElement=document.fullscreenElement||document.mozFullScreenElement||document.webkitFullscreenElement||document.msFullscreenElement;if(fullscreenElement===this.getContainer()&&!this._isFullscreen){this._setFullscreen(true);this.fire("fullscreenchange")}else if(fullscreenElement!==this.getContainer()&&this._isFullscreen){this._setFullscreen(false);this.fire("fullscreenchange")}}});L.Map.mergeOptions({fullscreenControl:false});L.Map.addInitHook(function(){if(this.options.fullscreenControl){this.fullscreenControl=new L.Control.Fullscreen(this.options.fullscreenControl);this.addControl(this.fullscreenControl)}var fullscreenchange;if("onfullscreenchange"in document){fullscreenchange="fullscreenchange"}else if("onmozfullscreenchange"in document){fullscreenchange="mozfullscreenchange"}else if("onwebkitfullscreenchange"in document){fullscreenchange="webkitfullscreenchange"}else if("onmsfullscreenchange"in document){fullscreenchange="MSFullscreenChange"}if(fullscreenchange){var onFullscreenChange=L.bind(this._onFullscreenChange,this);this.whenReady(function(){L.DomEvent.on(document,fullscreenchange,onFullscreenChange)});this.on("unload",function(){L.DomEvent.off(document,fullscreenchange,onFullscreenChange)})}});L.control.fullscreen=function(options){return new L.Control.Fullscreen(options)}; -------------------------------------------------------------------------------- /img/markers/parachute.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 29 | 30 | 37 | 38 | 40 | 41 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /css/layout.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Skeleton V1.2 3 | * Copyright 2011, Dave Gamache 4 | * www.getskeleton.com 5 | * Free to use under the MIT license. 6 | * http://www.opensource.org/licenses/mit-license.php 7 | * 6/20/2012 8 | 9 | * Edited by Daniel Saul 10 | * For use in the Habitat Webpage Template 11 | 12 | */ 13 | 14 | /* Table of Content 15 | ================================================== 16 | #Site Styles 17 | #Page Styles 18 | #Media Queries 19 | #Font-Face */ 20 | 21 | /* #Site Styles 22 | ================================================== */ 23 | 24 | /* Header */ 25 | header{ 26 | width: 100%; 27 | min-height: 75px; 28 | 29 | text-align: left; 30 | line-height: 75px; 31 | color: #ffffff; 32 | font-size: 14px; 33 | text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1); 34 | 35 | background: #00a3d3; 36 | border-bottom: 1px solid #009bc9; 37 | 38 | } 39 | 40 | header h1{ 41 | height: 75px; 42 | float: left; 43 | } 44 | 45 | /* Grey Area */ 46 | #grey-section{ 47 | width: 100%; 48 | margin-bottom: 50px; 49 | padding: 15px 0 15px 0; 50 | 51 | color: #666666; 52 | 53 | background: #fcfcfc; 54 | border-top: 1px solid #ffffff; 55 | border-bottom: 1px solid #eeeeee; 56 | } 57 | 58 | #grey-section h3{ 59 | color: #00a3d3; 60 | } 61 | 62 | #grey-section .badge{ 63 | margin-top: -30px; 64 | margin-bottom: -100%; 65 | float: right; 66 | visibility: hidden; 67 | } 68 | 69 | /* Forms */ 70 | .form.row label{ 71 | padding-top: 3px; 72 | font-weight: normal; 73 | } 74 | 75 | .form.row{ 76 | margin-bottom: 44px; 77 | } 78 | 79 | .form.row input, .form.row select { 80 | margin-bottom: 0 !important; 81 | } 82 | 83 | .validated input{ 84 | float: left; 85 | } 86 | 87 | .validated img{ 88 | padding: 3px 0 0 10px; 89 | float: left; 90 | } 91 | 92 | .form.row .input_extra{ 93 | background: #f8f8f8; 94 | color: #999999; 95 | padding: 5px; 96 | -moz-border-radius: 4px; 97 | -webkit-border-radius: 4px; 98 | border-radius: 4px; 99 | position: relative; 100 | min-height: 22px; 101 | width: auto; 102 | } 103 | 104 | @media only screen and (min-width: 768px){ 105 | .input_extra:before { 106 | content: "\0020"; 107 | width: 0; 108 | height: 0; 109 | border-top: 15px solid transparent; 110 | border-bottom: 15px solid transparent; 111 | border-right: 15px solid #f8f8f8; 112 | position: absolute; 113 | left: -14px; 114 | top: 1px; 115 | display: inline; 116 | } 117 | .form.row input.long { 118 | width: 380px; 119 | } 120 | } 121 | 122 | .long_protection { 123 | white-space: nowrap; 124 | overflow: hidden; 125 | display: block; 126 | } 127 | 128 | /* #Page Styles 129 | ================================================== */ 130 | 131 | /* #Media Queries 132 | ================================================== */ 133 | 134 | /* Smaller than standard 960 (devices and browsers) */ 135 | @media only screen and (max-width: 959px) {} 136 | 137 | /* Tablet Portrait size to standard 960 (devices and browsers) */ 138 | @media only screen and (min-width: 768px) and (max-width: 959px) {} 139 | 140 | /* All Mobile Sizes (devices and browser) */ 141 | @media only screen and (max-width: 767px) {} 142 | 143 | /* Mobile Landscape Size to Tablet Portrait (devices and browsers) */ 144 | @media only screen and (min-width: 480px) and (max-width: 767px) {} 145 | 146 | /* Mobile Portrait Size to Mobile Landscape Size (devices and browsers) */ 147 | @media only screen and (max-width: 479px) {} 148 | 149 | 150 | /* #Font-Face 151 | ================================================== */ 152 | /* This is the proper syntax for an @font-face file 153 | Just create a "fonts" folder at the root, 154 | copy your FontName into code below and remove 155 | comment brackets */ 156 | 157 | /* @font-face { 158 | font-family: 'FontName'; 159 | src: url('../fonts/FontName.eot'); 160 | src: url('../fonts/FontName.eot?iefix') format('eot'), 161 | url('../fonts/FontName.woff') format('woff'), 162 | url('../fonts/FontName.ttf') format('truetype'), 163 | url('../fonts/FontName.svg#webfontZam02nTh') format('svg'); 164 | font-weight: normal; 165 | font-style: normal; } 166 | */ 167 | -------------------------------------------------------------------------------- /js/flight_doc.js: -------------------------------------------------------------------------------- 1 | 2 | // populate login url 3 | document.getElementById("login_url").href= "https://auth.v2.sondehub.org/oauth2/authorize?client_id=21dpr4kth8lonk2rq803loh5oa&response_type=token&scope=email+openid+phone&redirect_uri=" + window.location.protocol + "//" + window.location.host 4 | 5 | // manage AWS cognito auth 6 | if (window.location.hash.indexOf("id_token") != -1){ 7 | console.log("Detected login") 8 | var args = window.location.hash.slice(1) 9 | var parms = new URLSearchParams(args) 10 | var id_token = parms.get("id_token") 11 | sessionStorage.setItem("id_token", id_token) 12 | } 13 | 14 | // do AWS login 15 | AWS.config.region = 'us-east-1'; 16 | 17 | AWS.config.credentials = new AWS.CognitoIdentityCredentials({ 18 | IdentityPoolId: 'us-east-1:55e43eac-9626-43e1-a7d2-bbc57f5f5aa9', 19 | Logins: { 20 | "cognito-idp.us-east-1.amazonaws.com/us-east-1_G4H7NMniM": sessionStorage.getItem("id_token") 21 | } 22 | }); 23 | 24 | AWS.config.credentials.get(function(){ 25 | // if this passes we update the login page to say logged in 26 | if (AWS.config.credentials.accessKeyId != undefined){ 27 | document.getElementById("login_url").innerText = "Logout" 28 | document.getElementById("login_url").href="javascript:logout()" 29 | document.getElementById("update-flightdocs").style.display = "block" 30 | document.getElementById("prediction_settings_message").innerText = "Use this form to configure predictions for your launch. Please only use this for your own launches. Callsigns must match your payload callsigns exactly (case sensitive)." 31 | } 32 | }); 33 | function query_flight_doc(){ 34 | var payload_callsign = document.getElementById("flight_doc_payload_callsign").value 35 | fetch("https://api.v2.sondehub.org/amateur/flightdoc/"+payload_callsign).then( 36 | function(response){ 37 | if (response.ok) { 38 | response.text().then(function(x) { 39 | var data = JSON.parse(x) 40 | if (data.float_expected) { 41 | document.getElementById("flight_doc_float_expected").checked = true 42 | } else { 43 | document.getElementById("flight_doc_float_expected").checked = false 44 | } 45 | document.getElementById("flight_doc_peak_altitude").value = data.peak_altitude 46 | document.getElementById("flight_doc_descent_rate").value = data.descent_rate 47 | document.getElementById("flight_doc_ascent_rate").value = data.ascent_rate 48 | }) 49 | } else { 50 | document.getElementById("payload-update-results").textContent = "Could not load payload data" 51 | } 52 | } 53 | ) 54 | } 55 | function logout(){ 56 | logout_url = "https://auth.v2.sondehub.org/logout?client_id=21dpr4kth8lonk2rq803loh5oa&response_type=token&logout_uri=" + window.location.protocol + "//" + window.location.host 57 | sessionStorage.removeItem("id_token") 58 | window.location = logout_url 59 | } 60 | 61 | function update_flight_doc(){ 62 | 63 | var body = JSON.stringify( 64 | { 65 | "payload_callsign": document.getElementById("flight_doc_payload_callsign").value, 66 | "float_expected": document.getElementById("flight_doc_float_expected").checked == true, 67 | "peak_altitude": parseFloat(document.getElementById("flight_doc_peak_altitude").value), 68 | "descent_rate": parseFloat(document.getElementById("flight_doc_descent_rate").value), 69 | "ascent_rate": parseFloat(document.getElementById("flight_doc_ascent_rate").value), 70 | } 71 | ) 72 | 73 | 74 | var httpRequest = new AWS.HttpRequest("https://api-raw.v2.sondehub.org/amateur/flightdoc" , "us-east-1"); 75 | var v4signer = new AWS.Signers.V4(httpRequest, "execute-api", true); 76 | httpRequest.method = "PUT"; 77 | httpRequest.headers['Host'] = 'api-raw.v2.sondehub.org'; 78 | httpRequest.headers['Content-Type'] = 'application/json'; 79 | httpRequest.headers['Content-Length'] = body.length; 80 | httpRequest.headers['X-Amz-Content-Sha256'] = v4signer.hexEncodedHash(body) 81 | httpRequest.body = body 82 | 83 | 84 | 85 | v4signer.addAuthorization(AWS.config.credentials, AWS.util.date.getDate()); 86 | document.getElementById("payload-update-results").textContent = "Updating..." 87 | fetch(httpRequest.endpoint.href , { 88 | method: httpRequest.method, 89 | headers: httpRequest.headers, 90 | body: httpRequest.body, 91 | }).then(function (response) { 92 | if (!response.ok) { 93 | response.text().then(function(x) {document.getElementById("payload-update-results").textContent =x }) 94 | return; 95 | } 96 | response.text().then(function(x) {document.getElementById("payload-update-results").textContent =x }) 97 | }); 98 | 99 | } -------------------------------------------------------------------------------- /js/L.Terminator.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('leaflet')) : 3 | typeof define === 'function' && define.amd ? define(['leaflet'], factory) : 4 | (global.L = global.L || {}, global.L.terminator = factory(global.L)); 5 | }(this, (function (L) { 'use strict'; 6 | 7 | L = L && L.hasOwnProperty('default') ? L['default'] : L; 8 | 9 | /* Terminator.js -- Overlay day/night region on a Leaflet map */ 10 | 11 | function julian(date) { 12 | /* Calculate the present UTC Julian Date. Function is valid after 13 | * the beginning of the UNIX epoch 1970-01-01 and ignores leap 14 | * seconds. */ 15 | return (date / 86400000) + 2440587.5; 16 | } 17 | 18 | function GMST(julianDay) { 19 | /* Calculate Greenwich Mean Sidereal Time according to 20 | http://aa.usno.navy.mil/faq/docs/GAST.php */ 21 | var d = julianDay - 2451545.0; 22 | // Low precision equation is good enough for our purposes. 23 | return (18.697374558 + 24.06570982441908 * d) % 24; 24 | } 25 | 26 | var Terminator = L.Polygon.extend({ 27 | options: { 28 | color: '#00', 29 | opacity: 0.5, 30 | fillColor: '#00', 31 | fillOpacity: 0.5, 32 | resolution: 2 33 | }, 34 | 35 | initialize: function (options) { 36 | this.version = '0.1.0'; 37 | this._R2D = 180 / Math.PI; 38 | this._D2R = Math.PI / 180; 39 | L.Util.setOptions(this, options); 40 | var latLng = this._compute(this.options.time); 41 | this.setLatLngs(latLng); 42 | }, 43 | 44 | setTime: function (date) { 45 | this.options.time = date; 46 | var latLng = this._compute(date); 47 | this.setLatLngs(latLng); 48 | }, 49 | 50 | _sunEclipticPosition: function (julianDay) { 51 | /* Compute the position of the Sun in ecliptic coordinates at 52 | julianDay. Following 53 | http://en.wikipedia.org/wiki/Position_of_the_Sun */ 54 | // Days since start of J2000.0 55 | var n = julianDay - 2451545.0; 56 | // mean longitude of the Sun 57 | var L$$1 = 280.460 + 0.9856474 * n; 58 | L$$1 %= 360; 59 | // mean anomaly of the Sun 60 | var g = 357.528 + 0.9856003 * n; 61 | g %= 360; 62 | // ecliptic longitude of Sun 63 | var lambda = L$$1 + 1.915 * Math.sin(g * this._D2R) + 64 | 0.02 * Math.sin(2 * g * this._D2R); 65 | // distance from Sun in AU 66 | var R = 1.00014 - 0.01671 * Math.cos(g * this._D2R) - 67 | 0.0014 * Math.cos(2 * g * this._D2R); 68 | return {lambda: lambda, R: R}; 69 | }, 70 | 71 | _eclipticObliquity: function (julianDay) { 72 | // Following the short term expression in 73 | // http://en.wikipedia.org/wiki/Axial_tilt#Obliquity_of_the_ecliptic_.28Earth.27s_axial_tilt.29 74 | var n = julianDay - 2451545.0; 75 | // Julian centuries since J2000.0 76 | var T = n / 36525; 77 | var epsilon = 23.43929111 - 78 | T * (46.836769 / 3600 79 | - T * (0.0001831 / 3600 80 | + T * (0.00200340 / 3600 81 | - T * (0.576e-6 / 3600 82 | - T * 4.34e-8 / 3600)))); 83 | return epsilon; 84 | }, 85 | 86 | _sunEquatorialPosition: function (sunEclLng, eclObliq) { 87 | /* Compute the Sun's equatorial position from its ecliptic 88 | * position. Inputs are expected in degrees. Outputs are in 89 | * degrees as well. */ 90 | var alpha = Math.atan(Math.cos(eclObliq * this._D2R) 91 | * Math.tan(sunEclLng * this._D2R)) * this._R2D; 92 | var delta = Math.asin(Math.sin(eclObliq * this._D2R) 93 | * Math.sin(sunEclLng * this._D2R)) * this._R2D; 94 | 95 | var lQuadrant = Math.floor(sunEclLng / 90) * 90; 96 | var raQuadrant = Math.floor(alpha / 90) * 90; 97 | alpha = alpha + (lQuadrant - raQuadrant); 98 | 99 | return {alpha: alpha, delta: delta}; 100 | }, 101 | 102 | _hourAngle: function (lng, sunPos, gst) { 103 | /* Compute the hour angle of the sun for a longitude on 104 | * Earth. Return the hour angle in degrees. */ 105 | var lst = gst + lng / 15; 106 | return lst * 15 - sunPos.alpha; 107 | }, 108 | 109 | _latitude: function (ha, sunPos) { 110 | /* For a given hour angle and sun position, compute the 111 | * latitude of the terminator in degrees. */ 112 | var lat = Math.atan(-Math.cos(ha * this._D2R) / 113 | Math.tan(sunPos.delta * this._D2R)) * this._R2D; 114 | return lat; 115 | }, 116 | 117 | _compute: function (time) { 118 | var today = time ? new Date(time) : new Date(); 119 | var julianDay = julian(today); 120 | var gst = GMST(julianDay); 121 | var latLng = []; 122 | 123 | var sunEclPos = this._sunEclipticPosition(julianDay); 124 | var eclObliq = this._eclipticObliquity(julianDay); 125 | var sunEqPos = this._sunEquatorialPosition(sunEclPos.lambda, eclObliq); 126 | for (var i = 0; i <= 720 * this.options.resolution; i++) { 127 | var lng = -360 + i / this.options.resolution; 128 | var ha = this._hourAngle(lng, sunEqPos, gst); 129 | latLng[i + 1] = [this._latitude(ha, sunEqPos), lng]; 130 | } 131 | if (sunEqPos.delta < 0) { 132 | latLng[0] = [90, -360]; 133 | latLng[latLng.length] = [90, 360]; 134 | } else { 135 | latLng[0] = [-90, -360]; 136 | latLng[latLng.length] = [-90, 360]; 137 | } 138 | return latLng; 139 | } 140 | }); 141 | 142 | function terminator(options) { 143 | return new Terminator(options); 144 | } 145 | 146 | return terminator; 147 | 148 | }))); 149 | -------------------------------------------------------------------------------- /glyphs/icon-clock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | Clock Icon 20 | 22 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | Clock Icon 49 | 51 | 2014-06-11 52 | 53 | 54 | Rossen Georgiev 55 | 56 | 57 | 58 | 59 | Rossen Georgiev 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 73 | 78 | 88 | 97 | 106 | 115 | 124 | 133 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /glyphs/icon-weather.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | Weather Icon 21 | 23 | 43 | 45 | 46 | 48 | image/svg+xml 49 | 51 | Weather Icon 52 | 54 | 2013 55 | 56 | 57 | KickstandApps (kickstandapps.com). 58 | 59 | 60 | 61 | 62 | KickstandApps (kickstandapps.com). 63 | 64 | 65 | https://github.com/kickstandapps/WeatherIcons 66 | 67 | 68 | 69 | 74 | 75 | -------------------------------------------------------------------------------- /js/plot_config.js: -------------------------------------------------------------------------------- 1 | // init plot 2 | plot = $.plot(plot_holder, {}, plot_options); 3 | var updateLegendTimeout = null; 4 | var polyMarker = null; 5 | 6 | // updates legend with extrapolated values under the mouse position 7 | function updateLegend(pos) { 8 | var legend = $(plot_holder + " .legendLabel"); 9 | $(plot_holder + " .legend table").css({'background-color':"rgba(255,255,255,0.9)","pointer-events":"none"}); 10 | legend.each(function() { 11 | $(this).css({'padding-left':'3px'}); 12 | }); 13 | 14 | 15 | var i, j, ij, pij, dataset = plot.getData(); 16 | var outside = false; 17 | //var axes = plot.getAxes(); 18 | 19 | if(dataset.length === 0) return; 20 | 21 | // this loop find the value for each series 22 | // and updates the legend 23 | // 24 | // here we don't snap to existing data point 25 | for (i = 0; i < dataset.length; ++i) { 26 | var series = dataset[i]; 27 | var y; 28 | y = null; 29 | 30 | // Find the nearest points, x-wise 31 | 32 | if(series.data.length < 2 || 33 | (i===1 && series.data[0][0] > pos.x)) { 34 | y = null; 35 | } 36 | else if (i !== 1 && pos.x > series.data[series.data.length-1][0]) { 37 | outside = true; 38 | } 39 | else { 40 | for (j = 0; j < series.data.length; ++j) { 41 | if (series.data[j][0] > pos.x) { 42 | break; 43 | } 44 | } 45 | 46 | if(i === 0) ij = j; 47 | if(i === 1) { 48 | pij = (j >= series.data.length) ? j-1 : j; 49 | } 50 | 51 | if(series.noInterpolate === true) { y = series.data[((j===0)?j:j-1)][1]; } 52 | else { 53 | var p1 = (j===0) ? null : series.data[j-1]; 54 | p2 = series.data[j]; 55 | 56 | if (p1 === null) { 57 | y = p2[1]; 58 | } else if (p2 === null || p2 === undefined) { 59 | y = p1[1]; 60 | } else { 61 | y = p1[1] + (p2[1] - p1[1]) * (pos.x - p1[0]) / (p2[0] - p1[0]); 62 | } 63 | 64 | y = ((p1 && p1[1] === null) || (p2 && p2[1] === null)) ? null : y.toFixed(2); 65 | } 66 | } 67 | legend.eq(i).text(series.label.replace(/=.*/, "= " + y)); 68 | } 69 | 70 | if(follow_vehicle !== null && vehicles[follow_vehicle].positions.length) { 71 | // adjust index for null data points 72 | var null_count = 0; 73 | 74 | if (!map.hasLayer(polyMarker) && polyMarker) { 75 | map.addLayer(polyMarker); 76 | } 77 | 78 | if(outside && pij !== undefined) { 79 | if(!polyMarker) { 80 | try {polyMarker = new L.Marker(vehicles[follow_vehicle].prediction_polyline[0].getLatLngs()[pij]).addTo(map);} catch (e) {}; 81 | try {polyMarker = new L.Marker(vehicles[follow_vehicle].prediction_polyline[1].getLatLngs()[pij]).addTo(map);} catch (e) {}; 82 | } else { 83 | try {polyMarker.setLatLng(vehicles[follow_vehicle].prediction_polyline[0].getLatLngs()[pij]);} catch (e) {}; 84 | try {polyMarker.setLatLng(vehicles[follow_vehicle].prediction_polyline[1].getLatLngs()[pij]);} catch (e) {}; 85 | } 86 | 87 | } 88 | else { 89 | var data_ref = vehicles[follow_vehicle].graph_data[0]; 90 | 91 | if(ij > data_ref.data.length / 2) { 92 | for(i = data_ref.data.length - 1; i > ij; i--) null_count += (data_ref.data[i][1] === null) ? 1 : 0; 93 | null_count = data_ref.nulls - null_count * 2; 94 | } else { 95 | for(i = 0; i < ij; i++) null_count += (data_ref.data[i][1] === null) ? 1 : 0; 96 | null_count *= 2; 97 | } 98 | 99 | // update position 100 | ij -= null_count + ((null_count===0||null_count===data_ref.nulls) ? 0 : 1); 101 | if(ij < 0) ij = 0; 102 | 103 | if(!polyMarker) { 104 | try {polyMarker = new L.Marker(vehicles[follow_vehicle].positions[ij]).addTo(map);} catch (e) {}; 105 | } else { 106 | try {polyMarker.setLatLng(vehicles[follow_vehicle].positions[ij]);} catch (e) {}; 107 | } 108 | } 109 | 110 | // set timebox 111 | var date = new Date(pos.x1); 112 | $('#timebox').removeClass('present').addClass('past'); 113 | updateTimebox(date); 114 | } 115 | } 116 | 117 | var plot_crosshair_locked = false; 118 | 119 | $(plot_holder).bind("click", function (event) { 120 | if(plot_crosshair_locked) { 121 | plot_crosshair_locked = false; 122 | } else if(event.ctrlKey) { 123 | plot_crosshair_locked = true; 124 | } 125 | }); 126 | // update legend values on mouse hover 127 | $(plot_holder).bind("plothover", function (event, pos, item) { 128 | if(plot_crosshair_locked) return; 129 | 130 | if (!updateLegendTimeout) { 131 | plot.lockCrosshair(); 132 | plot.setCrosshair(pos); 133 | updateLegend(pos); 134 | updateLegendTimeout = setTimeout(function() { updateLegendTimeout = null; }, 40); 135 | } 136 | }); 137 | 138 | // double click on the plot clears selection 139 | $(plot_holder).bind("dblclick", function () { 140 | if(!follow_vehicle) return; 141 | 142 | if(plot_options.xaxis) { 143 | if(plot_options.xaxis.superzoom == 2) { 144 | delete plot_options.xaxis; 145 | } 146 | else { 147 | if(plot_options.xaxis.superzoom == 1) { 148 | if(!confirm("You are about to zoom out to the entire graph. It may hang your browser. Do you wish to continue?")) return; 149 | } 150 | plot_options.xaxis = {}; 151 | } 152 | } 153 | 154 | updateGraph(follow_vehicle, false); 155 | }); 156 | 157 | // limit range after selection 158 | $(plot_holder).bind("plotselected", function (event, ranges) { 159 | if(typeof ranges.xaxis == 'undefined') return; 160 | 161 | if(plot_options.xaxis && plot_options.xaxis.superzoom) plot_options.xaxis.superzoom = 2; 162 | 163 | $.extend(true, plot_options, { 164 | xaxis: { 165 | min: ranges.xaxis.from, 166 | max: ranges.xaxis.to 167 | } 168 | }); 169 | 170 | updateGraph(follow_vehicle, false); 171 | }); 172 | -------------------------------------------------------------------------------- /glyphs/icon-compass.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | Clock Compass 20 | 22 | 43 | 47 | 51 | 55 | 59 | 62 | 63 | 65 | 66 | 68 | image/svg+xml 69 | 71 | Clock Compass 72 | 74 | 2014-07-09 75 | 76 | 77 | Rossen Georgiev 78 | 79 | 80 | 81 | 82 | Rossen Georgiev 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 96 | 101 | 117 | 133 | N 145 | W 157 | E 169 | S 181 | 191 | 192 | 193 | -------------------------------------------------------------------------------- /embed-preview.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Preview of embedded Sondehub-Amateur tracker 5 | 6 | 7 | 63 | 64 | 215 | 216 | 217 |
218 |

Embed SondeHub-Amateur tracker on your page

219 |

1. Options

220 |
221 | 222 | 223 |
224 | 225 | 226 |
227 | 228 | 229 |
230 | 231 | 232 |
233 |

2. Style

234 |
235 | 236 | 237 | 238 | 239 |
240 | 241 | 242 | 243 | 244 |
245 |

3. HTML code

246 | 247 | 248 |

4. Live preview

249 |
250 | 251 |
252 |
253 | 254 | 255 | -------------------------------------------------------------------------------- /js/format.js: -------------------------------------------------------------------------------- 1 | /* SondeHub Amateur Tracker Format Incoming Data 2 | * 3 | * Author: Luke Prior 4 | */ 5 | 6 | var excludedFields = [ 7 | "payload_callsign", 8 | "uploader_callsign", 9 | "software_version", 10 | "position", 11 | "user-agent", 12 | "uploaders", 13 | "snr", 14 | "rssi", 15 | "software_name", 16 | "alt", 17 | "lat", 18 | "lon", 19 | "heading", 20 | "datetime", 21 | "payload_callsign", 22 | "path", 23 | "time_received", 24 | "frame", 25 | "uploader_alt", 26 | "uploader_position", 27 | "uploader_radio", 28 | "uploader_antenna", 29 | "raw", 30 | "aprs_tocall", 31 | "telemetry_hidden", 32 | "upload_time", 33 | "raw_payload" 34 | ]; 35 | 36 | var uniqueKeys = { 37 | "batt": {"precision": 2}, 38 | "frequency": {"precision": 4}, 39 | "tx_frequency": {"precision": 4} 40 | } 41 | 42 | function formatData(data) { 43 | var showAprs = offline.get('opt_show_aprs'); 44 | var showTesting = offline.get("opt_show_testing"); 45 | var response = {}; 46 | response.positions = {}; 47 | var dataTemp = []; 48 | for (let key in data) { 49 | if (data.hasOwnProperty(key)) { 50 | if (typeof data[key] === 'object') { 51 | for (let i in data[key]) { 52 | var dataTempEntry = {}; 53 | var aprsflag = false; 54 | dataTempEntry.callsign = {}; 55 | maximumAltitude = 0; 56 | if (vehicles.hasOwnProperty(data[key][i].payload_callsign)) { 57 | maximumAltitude = vehicles[data[key][i].payload_callsign].max_alt; 58 | if (data[key][i].datetime == vehicles[data[key][i].payload_callsign].curr_position.gps_time) { 59 | dataTempEntry = vehicles[data[key][i].payload_callsign].curr_position; 60 | } 61 | } 62 | if (!data[key][i].hasOwnProperty("uploaders")) { 63 | data[key][i].uploaders = []; 64 | data[key][i].uploaders[0] = {} 65 | data[key][i].uploaders[0].uploader_callsign = data[key][i].uploader_callsign; 66 | if (data[key][i].snr && typeof data[key][i].snr === "number") { 67 | data[key][i].uploaders[0].snr = + data[key][i].snr.toFixed(1); 68 | } 69 | if (data[key][i].rssi && typeof data[key][i].rssi === "number") { 70 | data[key][i].uploaders[0].rssi = + data[key][i].rssi.toFixed(1); 71 | } 72 | if (data[key][i].frequency && typeof data[key][i].frequency === "number") { 73 | data[key][i].uploaders[0].frequency = + data[key][i].frequency.toFixed(4); 74 | } 75 | } 76 | for (let entry in data[key][i].uploaders) { 77 | // This check should probably be done using a modulation field, but this still works I guess.. 78 | if ("software_name" in data[key][i] && data[key][i].software_name.includes("APRS")) { 79 | aprsflag = true; 80 | var stations = data[key][i].uploaders[entry].uploader_callsign.split(","); 81 | for (let uploader in stations) { 82 | dataTempEntry.callsign[stations[uploader]] = {}; 83 | } 84 | } else { 85 | uploader_callsign = data[key][i].uploaders[entry].uploader_callsign 86 | dataTempEntry.callsign[uploader_callsign] = {}; 87 | 88 | if (data[key][i].uploaders[entry].snr && typeof data[key][i].uploaders[entry].snr === "number") { 89 | dataTempEntry.callsign[uploader_callsign].snr = + data[key][i].uploaders[entry].snr.toFixed(1); 90 | } 91 | if (data[key][i].uploaders[entry].rssi && typeof data[key][i].uploaders[entry].rssi === "number") { 92 | dataTempEntry.callsign[uploader_callsign].rssi = + data[key][i].uploaders[entry].rssi.toFixed(1); 93 | } 94 | if (data[key][i].uploaders[entry].frequency && typeof data[key][i].uploaders[entry].frequency === "number") { 95 | dataTempEntry.callsign[uploader_callsign].frequency = + data[key][i].uploaders[entry].frequency.toFixed(4); 96 | } 97 | 98 | } 99 | } 100 | dataTempEntry.gps_alt = parseFloat((data[key][i].alt).toPrecision(8)); 101 | if (dataTempEntry.gps_alt > maximumAltitude) { 102 | maximumAltitude = dataTempEntry.gps_alt; 103 | } 104 | // APRS Altitude filter. 105 | if (maximumAltitude < 1500 && aprsflag && !showAprs) { 106 | continue; 107 | } 108 | // Testing payload filter. 109 | if (data[key][i].telemetry_hidden && !showTesting){ 110 | continue; 111 | } 112 | // No GPS lock filter. Filter out positions with sats = 0, if sats is provided. 113 | // The historical data API will do this already, but we need this to filter out data 114 | // coming in via websockets. 115 | if (data[key][i].hasOwnProperty("sats")){ 116 | if (data[key][i].sats == 0){ 117 | continue; 118 | } 119 | } 120 | // 121 | dataTempEntry.gps_lat = parseFloat((data[key][i].lat).toPrecision(8)); 122 | dataTempEntry.gps_lon = parseFloat((data[key][i].lon).toPrecision(8)); 123 | if (dataTempEntry.gps_lat == 0 && dataTempEntry.gps_lon == 0) { 124 | continue; 125 | } 126 | if (data[key][i].heading) { 127 | dataTempEntry.gps_heading = data[key][i].heading; 128 | } 129 | dataTempEntry.gps_time = data[key][i].datetime; 130 | dataTempEntry.server_time = data[key][i].datetime; 131 | dataTempEntry.vehicle = data[key][i].payload_callsign; 132 | dataTempEntry.position_id = data[key][i].payload_callsign + "-" + data[key][i].datetime; 133 | if (!dataTempEntry.hasOwnProperty("data")) { 134 | dataTempEntry.data = {}; 135 | } 136 | 137 | // Automatically add all remaining fields as data excluding excluded fields 138 | 139 | for (let field in data[key][i]) { 140 | if (excludedFields.includes(field)) { 141 | continue; 142 | } 143 | if (uniqueKeys.hasOwnProperty(field)) { 144 | dataTempEntry.data[field] = parseFloat(data[key][i][field]).toFixed(uniqueKeys[field].precision); 145 | } else { 146 | dataTempEntry.data[field] = data[key][i][field]; 147 | } 148 | } 149 | 150 | // Payload data post-processing, where we can modify / add data elements if needed. 151 | 152 | // Determine if this payload is a WSPR payload 153 | // We determine this through either the modulation field, or comment field. 154 | var wspr_payload = false; 155 | if (data[key][i].hasOwnProperty("modulation")){ 156 | if(data[key][i].modulation.includes("WSPR")){ 157 | wspr_payload = true; 158 | } 159 | } 160 | if (data[key][i].hasOwnProperty("comment")){ 161 | if(data[key][i].comment.includes("WSPR")){ 162 | wspr_payload = true; 163 | } 164 | } 165 | 166 | // For WSPR payloads, calculate solar elevation. 167 | if(wspr_payload){ 168 | dataTempEntry.data['solar_elevation'] = (SunCalc.getPosition(stringToDateUTC(dataTempEntry.gps_time), dataTempEntry.gps_lat, dataTempEntry.gps_lon, dataTempEntry.gps_alt).altitude/rad).toFixed(1); 169 | } 170 | 171 | 172 | dataTemp.push(dataTempEntry); 173 | } 174 | } 175 | } 176 | } 177 | response.positions.position = dataTemp; 178 | response.fetch_timestamp = Date.now(); 179 | return response; 180 | } -------------------------------------------------------------------------------- /js/L.TileLayer.NoGap.js: -------------------------------------------------------------------------------- 1 | // @class TileLayer 2 | 3 | L.TileLayer.mergeOptions({ 4 | // @option keepBuffer 5 | // The amount of tiles outside the visible map area to be kept in the stitched 6 | // `TileLayer`. 7 | 8 | // @option dumpToCanvas: Boolean = true 9 | // Whether to dump loaded tiles to a `` to prevent some rendering 10 | // artifacts. (Disabled by default in IE) 11 | dumpToCanvas: L.Browser.canvas && !L.Browser.ie, 12 | }); 13 | 14 | L.TileLayer.include({ 15 | _onUpdateLevel: function(z, zoom) { 16 | if (this.options.dumpToCanvas) { 17 | this._levels[z].canvas.style.zIndex = 18 | this.options.maxZoom - Math.abs(zoom - z); 19 | } 20 | }, 21 | 22 | _onRemoveLevel: function(z) { 23 | if (this.options.dumpToCanvas) { 24 | L.DomUtil.remove(this._levels[z].canvas); 25 | } 26 | }, 27 | 28 | _onCreateLevel: function(level) { 29 | if (this.options.dumpToCanvas) { 30 | level.canvas = L.DomUtil.create( 31 | "canvas", 32 | "leaflet-tile-container leaflet-zoom-animated", 33 | this._container 34 | ); 35 | level.ctx = level.canvas.getContext("2d"); 36 | this._resetCanvasSize(level); 37 | } 38 | }, 39 | 40 | _removeTile: function(key) { 41 | if (this.options.dumpToCanvas) { 42 | var tile = this._tiles[key]; 43 | var level = this._levels[tile.coords.z]; 44 | var tileSize = this.getTileSize(); 45 | 46 | if (level) { 47 | // Where in the canvas should this tile go? 48 | var offset = L.point(tile.coords.x, tile.coords.y) 49 | .subtract(level.canvasRange.min) 50 | .scaleBy(this.getTileSize()); 51 | 52 | level.ctx.clearRect(offset.x, offset.y, tileSize.x, tileSize.y); 53 | } 54 | } 55 | 56 | L.GridLayer.prototype._removeTile.call(this, key); 57 | }, 58 | 59 | _resetCanvasSize: function(level) { 60 | var buff = this.options.keepBuffer, 61 | pixelBounds = this._getTiledPixelBounds(this._map.getCenter()), 62 | tileRange = this._pxBoundsToTileRange(pixelBounds), 63 | tileSize = this.getTileSize(); 64 | 65 | tileRange.min = tileRange.min.subtract([buff, buff]); // This adds the no-prune buffer 66 | tileRange.max = tileRange.max.add([buff + 1, buff + 1]); 67 | 68 | var pixelRange = L.bounds( 69 | tileRange.min.scaleBy(tileSize), 70 | tileRange.max.add([1, 1]).scaleBy(tileSize) // This prevents an off-by-one when checking if tiles are inside 71 | ), 72 | mustRepositionCanvas = false, 73 | neededSize = pixelRange.max.subtract(pixelRange.min); 74 | 75 | // Resize the canvas, if needed, and only to make it bigger. 76 | if ( 77 | neededSize.x > level.canvas.width || 78 | neededSize.y > level.canvas.height 79 | ) { 80 | // Resizing canvases erases the currently drawn content, I'm afraid. 81 | // To keep it, dump the pixels to another canvas, then display it on 82 | // top. This could be done with getImageData/putImageData, but that 83 | // would break for tainted canvases (in non-CORS tilesets) 84 | var oldSize = { x: level.canvas.width, y: level.canvas.height }; 85 | // console.info('Resizing canvas from ', oldSize, 'to ', neededSize); 86 | 87 | var tmpCanvas = L.DomUtil.create("canvas"); 88 | tmpCanvas.style.width = (tmpCanvas.width = oldSize.x) + "px"; 89 | tmpCanvas.style.height = (tmpCanvas.height = oldSize.y) + "px"; 90 | tmpCanvas.getContext("2d").drawImage(level.canvas, 0, 0); 91 | // var data = level.ctx.getImageData(0, 0, oldSize.x, oldSize.y); 92 | 93 | level.canvas.style.width = (level.canvas.width = neededSize.x) + "px"; 94 | level.canvas.style.height = (level.canvas.height = neededSize.y) + "px"; 95 | level.ctx.drawImage(tmpCanvas, 0, 0); 96 | // level.ctx.putImageData(data, 0, 0, 0, 0, oldSize.x, oldSize.y); 97 | } 98 | 99 | // Translate the canvas contents if it's moved around 100 | if (level.canvasRange) { 101 | var offset = level.canvasRange.min 102 | .subtract(tileRange.min) 103 | .scaleBy(this.getTileSize()); 104 | 105 | // console.info('Offsetting by ', offset); 106 | 107 | if (!L.Browser.safari) { 108 | // By default, canvases copy things "on top of" existing pixels, but we want 109 | // this to *replace* the existing pixels when doing a drawImage() call. 110 | // This will also clear the sides, so no clearRect() calls are needed to make room 111 | // for the new tiles. 112 | level.ctx.globalCompositeOperation = "copy"; 113 | level.ctx.drawImage(level.canvas, offset.x, offset.y); 114 | level.ctx.globalCompositeOperation = "source-over"; 115 | } else { 116 | // Safari clears the canvas when copying from itself :-( 117 | if (!this._tmpCanvas) { 118 | var t = (this._tmpCanvas = L.DomUtil.create("canvas")); 119 | t.width = level.canvas.width; 120 | t.height = level.canvas.height; 121 | this._tmpContext = t.getContext("2d"); 122 | } 123 | this._tmpContext.clearRect( 124 | 0, 125 | 0, 126 | level.canvas.width, 127 | level.canvas.height 128 | ); 129 | this._tmpContext.drawImage(level.canvas, 0, 0); 130 | level.ctx.clearRect(0, 0, level.canvas.width, level.canvas.height); 131 | level.ctx.drawImage(this._tmpCanvas, offset.x, offset.y); 132 | } 133 | 134 | mustRepositionCanvas = true; // Wait until new props are set 135 | } 136 | 137 | level.canvasRange = tileRange; 138 | level.canvasPxRange = pixelRange; 139 | level.canvasOrigin = pixelRange.min; 140 | 141 | // console.log('Canvas tile range: ', level, tileRange.min, tileRange.max ); 142 | // console.log('Canvas pixel range: ', pixelRange.min, pixelRange.max ); 143 | // console.log('Level origin: ', level.origin ); 144 | 145 | if (mustRepositionCanvas) { 146 | this._setCanvasZoomTransform( 147 | level, 148 | this._map.getCenter(), 149 | this._map.getZoom() 150 | ); 151 | } 152 | }, 153 | 154 | /// set transform/position of canvas, in addition to the transform/position of the individual tile container 155 | _setZoomTransform: function(level, center, zoom) { 156 | L.GridLayer.prototype._setZoomTransform.call(this, level, center, zoom); 157 | if (this.options.dumpToCanvas) { 158 | this._setCanvasZoomTransform(level, center, zoom); 159 | } 160 | }, 161 | 162 | // This will get called twice: 163 | // * From _setZoomTransform 164 | // * When the canvas has shifted due to a new tile being loaded 165 | _setCanvasZoomTransform: function(level, center, zoom) { 166 | // console.log('_setCanvasZoomTransform', level, center, zoom); 167 | if (!level.canvasOrigin) { 168 | return; 169 | } 170 | var scale = this._map.getZoomScale(zoom, level.zoom), 171 | translate = level.canvasOrigin 172 | .multiplyBy(scale) 173 | .subtract(this._map._getNewPixelOrigin(center, zoom)) 174 | .round(); 175 | 176 | if (L.Browser.any3d) { 177 | L.DomUtil.setTransform(level.canvas, translate, scale); 178 | } else { 179 | L.DomUtil.setPosition(level.canvas, translate); 180 | } 181 | }, 182 | 183 | _onOpaqueTile: function(tile) { 184 | if (!this.options.dumpToCanvas) { 185 | return; 186 | } 187 | 188 | // Guard against an NS_ERROR_NOT_AVAILABLE (or similar) exception 189 | // when a non-image-tile has been loaded (e.g. a WMS error). 190 | // Checking for tile.el.complete is not enough, as it has been 191 | // already marked as loaded and ready somehow. 192 | try { 193 | this.dumpPixels(tile.coords, tile.el); 194 | } catch (ex) { 195 | return this.fire("tileerror", { 196 | error: "Could not copy tile pixels: " + ex, 197 | tile: tile, 198 | coods: tile.coords, 199 | }); 200 | } 201 | 202 | // If dumping the pixels was successful, then hide the tile. 203 | // Do not remove the tile itself, as it is needed to check if the whole 204 | // level (and its canvas) should be removed (via level.el.children.length) 205 | tile.el.style.display = "none"; 206 | }, 207 | 208 | // @section Extension methods 209 | // @uninheritable 210 | 211 | // @method dumpPixels(coords: Object, imageSource: CanvasImageSource): this 212 | // Dumps pixels from the given `CanvasImageSource` into the layer, into 213 | // the space for the tile represented by the `coords` tile coordinates (an object 214 | // like `{x: Number, y: Number, z: Number}`; the image source must have the 215 | // same size as the `tileSize` option for the layer. Has no effect if `dumpToCanvas` 216 | // is `false`. 217 | dumpPixels: function(coords, imageSource) { 218 | var level = this._levels[coords.z], 219 | tileSize = this.getTileSize(); 220 | 221 | if (!level.canvasRange || !this.options.dumpToCanvas) { 222 | return; 223 | } 224 | 225 | // Check if the tile is inside the currently visible map bounds 226 | // There is a possible race condition when tiles are loaded after they 227 | // have been panned outside of the map. 228 | if (!level.canvasRange.contains(coords)) { 229 | this._resetCanvasSize(level); 230 | } 231 | 232 | // Where in the canvas should this tile go? 233 | var offset = L.point(coords.x, coords.y) 234 | .subtract(level.canvasRange.min) 235 | .scaleBy(this.getTileSize()); 236 | 237 | level.ctx.drawImage(imageSource, offset.x, offset.y, tileSize.x, tileSize.y); 238 | 239 | // TODO: Clear the pixels of other levels' canvases where they overlap 240 | // this newly dumped tile. 241 | return this; 242 | }, 243 | }); 244 | -------------------------------------------------------------------------------- /css/base.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Skeleton V1.2 3 | * Copyright 2011, Dave Gamache 4 | * www.getskeleton.com 5 | * Free to use under the MIT license. 6 | * http://www.opensource.org/licenses/mit-license.php 7 | * 6/20/2012 8 | 9 | * Edited by Daniel Saul 10 | * For use in the Habitat Webpage Template 11 | 12 | */ 13 | 14 | 15 | 16 | /* Table of Content 17 | ================================================== 18 | #Reset & Basics 19 | #Basic Styles 20 | #Site Styles 21 | #Typography 22 | #Links 23 | #Lists 24 | #Images 25 | #Buttons 26 | #Forms 27 | #Misc */ 28 | 29 | 30 | /* #Reset & Basics (Inspired by E. Meyers) 31 | ================================================== */ 32 | html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { 33 | margin: 0; 34 | padding: 0; 35 | border: 0; 36 | font-size: 100%; 37 | font: inherit; 38 | vertical-align: baseline; } 39 | sup { 40 | font-size: smaller; 41 | vertical-align: +0.4em; } 42 | sub { 43 | font-size: smaller; 44 | vertical-align: -0.25em; } 45 | article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section { 46 | display: block; } 47 | body { 48 | line-height: 1; } 49 | ol, ul { 50 | list-style: none; } 51 | blockquote, q { 52 | quotes: none; } 53 | blockquote:before, blockquote:after, 54 | q:before, q:after { 55 | content: ''; 56 | content: none; } 57 | table { 58 | border-collapse: collapse; 59 | border-spacing: 0; } 60 | 61 | 62 | /* #Basic Styles 63 | ================================================== */ 64 | body { 65 | background: #fff; 66 | font: 14px/21px "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; 67 | color: #666; 68 | -webkit-font-smoothing: antialiased; /* Fix for webkit rendering */ 69 | -webkit-text-size-adjust: 100%; 70 | } 71 | 72 | 73 | /* #Typography 74 | ================================================== */ 75 | h1, h2, h3, h4, h5, h6 { 76 | font-weight: normal; } 77 | h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { font-weight: inherit; } 78 | h1 { font-size: 46px; line-height: 50px; margin-bottom: 14px;} 79 | h2 { font-size: 35px; line-height: 40px; margin-bottom: 10px; } 80 | h3 { font-size: 28px; line-height: 34px; margin-bottom: 8px; } 81 | h4 { font-size: 21px; line-height: 30px; margin-bottom: 4px; } 82 | h5 { font-size: 17px; line-height: 24px; } 83 | h6 { font-size: 14px; line-height: 21px; } 84 | .subheader { color: #777; } 85 | 86 | p { margin: 0 0 20px 0; } 87 | p img { margin: 0; } 88 | p.lead { font-size: 21px; line-height: 27px; color: #777; } 89 | 90 | em { font-style: italic; } 91 | strong { font-weight: bold; color: #333; } 92 | b { font-weight: bold; } 93 | small { font-size: 80%; } 94 | 95 | /* Blockquotes */ 96 | blockquote, blockquote p { font-size: 17px; line-height: 24px; color: #777; font-style: italic; } 97 | blockquote { margin: 0 0 20px; padding: 9px 20px 0 19px; border-left: 1px solid #ddd; } 98 | blockquote cite { display: block; font-size: 12px; color: #555; } 99 | blockquote cite:before { content: "\2014 \0020"; } 100 | blockquote cite a, blockquote cite a:visited, blockquote cite a:visited { color: #555; } 101 | 102 | hr { border: solid #ddd; border-width: 1px 0 0; clear: both; height: 0; } 103 | 104 | 105 | /* #Links 106 | ================================================== */ 107 | a, a:visited { color: #333; text-decoration: underline; outline: 0; } 108 | a:hover, a:focus { color: #000; } 109 | p a, p a:visited { line-height: inherit; } 110 | 111 | 112 | /* #Lists 113 | ================================================== */ 114 | ul, ol { margin-bottom: 20px; } 115 | ul { list-style: none outside; } 116 | ol { list-style: decimal; } 117 | ol, ul.square, ul.circle, ul.disc { margin-left: 30px; } 118 | ul.square { list-style: square outside; } 119 | ul.circle { list-style: circle outside; } 120 | ul.disc { list-style: disc outside; } 121 | ul ul, ul ol, 122 | ol ol, ol ul { margin: 4px 0 5px 30px; font-size: 90%; } 123 | ul ul li, ul ol li, 124 | ol ol li, ol ul li { margin-bottom: 6px; } 125 | li { line-height: 18px; margin-bottom: 12px; } 126 | ul.large li { line-height: 21px; } 127 | li p { line-height: 21px; } 128 | 129 | /* #Images 130 | ================================================== */ 131 | 132 | img.scale-with-grid { 133 | max-width: 100%; 134 | height: auto; } 135 | 136 | 137 | /* #Buttons 138 | ================================================== */ 139 | 140 | .button, 141 | button, 142 | input[type="submit"], 143 | input[type="reset"], 144 | input[type="button"] { 145 | background: #eee; /* Old browsers */ 146 | background: #eee -moz-linear-gradient(top, rgba(255,255,255,.2) 0%, rgba(0,0,0,.2) 100%); /* FF3.6+ */ 147 | background: #eee -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255,255,255,.2)), color-stop(100%,rgba(0,0,0,.2))); /* Chrome,Safari4+ */ 148 | background: #eee -webkit-linear-gradient(top, rgba(255,255,255,.2) 0%,rgba(0,0,0,.2) 100%); /* Chrome10+,Safari5.1+ */ 149 | background: #eee -o-linear-gradient(top, rgba(255,255,255,.2) 0%,rgba(0,0,0,.2) 100%); /* Opera11.10+ */ 150 | background: #eee -ms-linear-gradient(top, rgba(255,255,255,.2) 0%,rgba(0,0,0,.2) 100%); /* IE10+ */ 151 | background: #eee linear-gradient(top, rgba(255,255,255,.2) 0%,rgba(0,0,0,.2) 100%); /* W3C */ 152 | border: 1px solid #aaa; 153 | border-top: 1px solid #ccc; 154 | border-left: 1px solid #ccc; 155 | margin-right: 1px; 156 | -moz-border-radius: 3px; 157 | -webkit-border-radius: 3px; 158 | border-radius: 3px; 159 | color: #444; 160 | display: inline-block; 161 | font-size: 11px; 162 | font-weight: bold; 163 | text-decoration: none; 164 | text-shadow: 0 1px rgba(255, 255, 255, .75); 165 | cursor: pointer; 166 | line-height: normal; 167 | padding: 8px 10px; 168 | font-family: "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; } 169 | 170 | .button:hover, 171 | button:hover, 172 | input[type="submit"]:hover, 173 | input[type="reset"]:hover, 174 | input[type="button"]:hover { 175 | color: #222; 176 | background: #ddd; /* Old browsers */ 177 | background: #ddd -moz-linear-gradient(top, rgba(255,255,255,.3) 0%, rgba(0,0,0,.3) 100%); /* FF3.6+ */ 178 | background: #ddd -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255,255,255,.3)), color-stop(100%,rgba(0,0,0,.3))); /* Chrome,Safari4+ */ 179 | background: #ddd -webkit-linear-gradient(top, rgba(255,255,255,.3) 0%,rgba(0,0,0,.3) 100%); /* Chrome10+,Safari5.1+ */ 180 | background: #ddd -o-linear-gradient(top, rgba(255,255,255,.3) 0%,rgba(0,0,0,.3) 100%); /* Opera11.10+ */ 181 | background: #ddd -ms-linear-gradient(top, rgba(255,255,255,.3) 0%,rgba(0,0,0,.3) 100%); /* IE10+ */ 182 | background: #ddd linear-gradient(top, rgba(255,255,255,.3) 0%,rgba(0,0,0,.3) 100%); /* W3C */ 183 | border: 1px solid #888; 184 | border-top: 1px solid #aaa; 185 | border-left: 1px solid #aaa; } 186 | 187 | .button:active, 188 | button:active, 189 | input[type="submit"]:active, 190 | input[type="reset"]:active, 191 | input[type="button"]:active { 192 | border: 1px solid #666; 193 | background: #ccc; /* Old browsers */ 194 | background: #ccc -moz-linear-gradient(top, rgba(255,255,255,.35) 0%, rgba(10,10,10,.4) 100%); /* FF3.6+ */ 195 | background: #ccc -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255,255,255,.35)), color-stop(100%,rgba(10,10,10,.4))); /* Chrome,Safari4+ */ 196 | background: #ccc -webkit-linear-gradient(top, rgba(255,255,255,.35) 0%,rgba(10,10,10,.4) 100%); /* Chrome10+,Safari5.1+ */ 197 | background: #ccc -o-linear-gradient(top, rgba(255,255,255,.35) 0%,rgba(10,10,10,.4) 100%); /* Opera11.10+ */ 198 | background: #ccc -ms-linear-gradient(top, rgba(255,255,255,.35) 0%,rgba(10,10,10,.4) 100%); /* IE10+ */ 199 | background: #ccc linear-gradient(top, rgba(255,255,255,.35) 0%,rgba(10,10,10,.4) 100%); /* W3C */ } 200 | 201 | .button.full-width, 202 | button.full-width, 203 | input[type="submit"].full-width, 204 | input[type="reset"].full-width, 205 | input[type="button"].full-width { 206 | width: 100%; 207 | padding-left: 0 !important; 208 | padding-right: 0 !important; 209 | text-align: center; } 210 | 211 | /* Fix for odd Mozilla border & padding issues */ 212 | button::-moz-focus-inner, 213 | input::-moz-focus-inner { 214 | border: 0; 215 | padding: 0; 216 | } 217 | 218 | .button.disabled, 219 | button.disabled, 220 | input[type="submit"].disabled, 221 | input[type="reset"].disabled, 222 | input[type="button"].disabled { 223 | border: 1px solid #aaa; 224 | color: #aaa; 225 | background: #fff; 226 | } 227 | 228 | 229 | /* #Forms 230 | ================================================== */ 231 | 232 | fieldset { 233 | margin-bottom: 20px; } 234 | input[type="text"], 235 | input[type="password"], 236 | input[type="email"], 237 | textarea, 238 | select { 239 | border: 1px solid #ccc; 240 | padding: 6px 4px; 241 | outline: none; 242 | -moz-border-radius: 4px; 243 | -webkit-border-radius: 4px; 244 | border-radius: 4px; 245 | font: 13px "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; 246 | color: #888; 247 | margin: 0; 248 | max-width: 100%; 249 | background: #fff; } 250 | select { 251 | padding: 0; } 252 | input[type="text"]:focus, 253 | input[type="password"]:focus, 254 | input[type="email"]:focus, 255 | textarea:focus { 256 | border: 1px solid #aaa; 257 | color: #444; 258 | -moz-box-shadow: 0 0 3px rgba(0,0,0,.2); 259 | -webkit-box-shadow: 0 0 3px rgba(0,0,0,.2); 260 | box-shadow: 0 0 3px rgba(0,0,0,.2); } 261 | textarea { 262 | min-height: 60px; } 263 | label, 264 | legend { 265 | display: block; 266 | font-weight: bold; 267 | font-size: 13px; } 268 | select { 269 | width: 220px; } 270 | input[type="checkbox"] { 271 | display: inline; } 272 | label span, 273 | legend span { 274 | font-weight: normal; 275 | font-size: 13px; 276 | color: #444; } 277 | 278 | /* #Misc 279 | ================================================== */ 280 | .remove-bottom { margin-bottom: 0 !important; } 281 | .half-bottom { margin-bottom: 10px !important; } 282 | .add-bottom { margin-bottom: 20px !important; } 283 | .no-margin { margin: 0 !important; padding: 0; } 284 | 285 | 286 | -------------------------------------------------------------------------------- /css/skeleton.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Skeleton V1.2 3 | * Copyright 2011, Dave Gamache 4 | * www.getskeleton.com 5 | * Free to use under the MIT license. 6 | * http://www.opensource.org/licenses/mit-license.php 7 | * 6/20/2012 8 | */ 9 | 10 | 11 | /* Table of Contents 12 | ================================================== 13 | #Base 960 Grid 14 | #Tablet (Portrait) 15 | #Mobile (Portrait) 16 | #Mobile (Landscape) 17 | #Clearing */ 18 | 19 | 20 | 21 | /* #Base 960 Grid 22 | ================================================== */ 23 | 24 | .container { position: relative; width: 960px; margin: 0 auto; padding: 0; } 25 | .container .column, 26 | .container .columns { float: left; display: inline; margin-left: 10px; margin-right: 10px; } 27 | .row { margin-bottom: 20px; } 28 | 29 | /* Nested Column Classes */ 30 | .column.alpha, .columns.alpha { margin-left: 0; } 31 | .column.omega, .columns.omega { margin-right: 0; } 32 | 33 | /* Base Grid */ 34 | .container .one.column, 35 | .container .one.columns { width: 40px; } 36 | .container .two.columns { width: 100px; } 37 | .container .three.columns { width: 160px; } 38 | .container .four.columns { width: 220px; } 39 | .container .five.columns { width: 280px; } 40 | .container .six.columns { width: 340px; } 41 | .container .seven.columns { width: 400px; } 42 | .container .eight.columns { width: 460px; } 43 | .container .nine.columns { width: 520px; } 44 | .container .ten.columns { width: 580px; } 45 | .container .eleven.columns { width: 640px; } 46 | .container .twelve.columns { width: 700px; } 47 | .container .thirteen.columns { width: 760px; } 48 | .container .fourteen.columns { width: 820px; } 49 | .container .fifteen.columns { width: 880px; } 50 | .container .sixteen.columns { width: 940px; } 51 | 52 | .container .one-third.column { width: 300px; } 53 | .container .two-thirds.column { width: 620px; } 54 | 55 | /* Offsets */ 56 | .container .offset-by-one { padding-left: 60px; } 57 | .container .offset-by-two { padding-left: 120px; } 58 | .container .offset-by-three { padding-left: 180px; } 59 | .container .offset-by-four { padding-left: 240px; } 60 | .container .offset-by-five { padding-left: 300px; } 61 | .container .offset-by-six { padding-left: 360px; } 62 | .container .offset-by-seven { padding-left: 420px; } 63 | .container .offset-by-eight { padding-left: 480px; } 64 | .container .offset-by-nine { padding-left: 540px; } 65 | .container .offset-by-ten { padding-left: 600px; } 66 | .container .offset-by-eleven { padding-left: 660px; } 67 | .container .offset-by-twelve { padding-left: 720px; } 68 | .container .offset-by-thirteen { padding-left: 780px; } 69 | .container .offset-by-fourteen { padding-left: 840px; } 70 | .container .offset-by-fifteen { padding-left: 900px; } 71 | 72 | 73 | 74 | /* #Tablet (Portrait) 75 | ================================================== */ 76 | 77 | /* Note: Design for a width of 768px */ 78 | 79 | @media only screen and (min-width: 768px) and (max-width: 959px) { 80 | .container { width: 768px; } 81 | .container .column, 82 | .container .columns { margin-left: 10px; margin-right: 10px; } 83 | .column.alpha, .columns.alpha { margin-left: 0; margin-right: 10px; } 84 | .column.omega, .columns.omega { margin-right: 0; margin-left: 10px; } 85 | .alpha.omega { margin-left: 0; margin-right: 0; } 86 | 87 | .container .one.column, 88 | .container .one.columns { width: 28px; } 89 | .container .two.columns { width: 76px; } 90 | .container .three.columns { width: 124px; } 91 | .container .four.columns { width: 172px; } 92 | .container .five.columns { width: 220px; } 93 | .container .six.columns { width: 268px; } 94 | .container .seven.columns { width: 316px; } 95 | .container .eight.columns { width: 364px; } 96 | .container .nine.columns { width: 412px; } 97 | .container .ten.columns { width: 460px; } 98 | .container .eleven.columns { width: 508px; } 99 | .container .twelve.columns { width: 556px; } 100 | .container .thirteen.columns { width: 604px; } 101 | .container .fourteen.columns { width: 652px; } 102 | .container .fifteen.columns { width: 700px; } 103 | .container .sixteen.columns { width: 748px; } 104 | 105 | .container .one-third.column { width: 236px; } 106 | .container .two-thirds.column { width: 492px; } 107 | 108 | /* Offsets */ 109 | .container .offset-by-one { padding-left: 48px; } 110 | .container .offset-by-two { padding-left: 96px; } 111 | .container .offset-by-three { padding-left: 144px; } 112 | .container .offset-by-four { padding-left: 192px; } 113 | .container .offset-by-five { padding-left: 240px; } 114 | .container .offset-by-six { padding-left: 288px; } 115 | .container .offset-by-seven { padding-left: 336px; } 116 | .container .offset-by-eight { padding-left: 384px; } 117 | .container .offset-by-nine { padding-left: 432px; } 118 | .container .offset-by-ten { padding-left: 480px; } 119 | .container .offset-by-eleven { padding-left: 528px; } 120 | .container .offset-by-twelve { padding-left: 576px; } 121 | .container .offset-by-thirteen { padding-left: 624px; } 122 | .container .offset-by-fourteen { padding-left: 672px; } 123 | .container .offset-by-fifteen { padding-left: 720px; } 124 | } 125 | 126 | 127 | /* #Mobile (Portrait) 128 | ================================================== */ 129 | 130 | /* Note: Design for a width of 320px */ 131 | 132 | @media only screen and (max-width: 767px) { 133 | .container { width: 300px; } 134 | .container .columns, 135 | .container .column { margin: 0; } 136 | 137 | .container .one.column, 138 | .container .one.columns, 139 | .container .two.columns, 140 | .container .three.columns, 141 | .container .four.columns, 142 | .container .five.columns, 143 | .container .six.columns, 144 | .container .seven.columns, 145 | .container .eight.columns, 146 | .container .nine.columns, 147 | .container .ten.columns, 148 | .container .eleven.columns, 149 | .container .twelve.columns, 150 | .container .thirteen.columns, 151 | .container .fourteen.columns, 152 | .container .fifteen.columns, 153 | .container .sixteen.columns, 154 | .container .one-third.column, 155 | .container .two-thirds.column { width: 300px; } 156 | 157 | /* Offsets */ 158 | .container .offset-by-one, 159 | .container .offset-by-two, 160 | .container .offset-by-three, 161 | .container .offset-by-four, 162 | .container .offset-by-five, 163 | .container .offset-by-six, 164 | .container .offset-by-seven, 165 | .container .offset-by-eight, 166 | .container .offset-by-nine, 167 | .container .offset-by-ten, 168 | .container .offset-by-eleven, 169 | .container .offset-by-twelve, 170 | .container .offset-by-thirteen, 171 | .container .offset-by-fourteen, 172 | .container .offset-by-fifteen { padding-left: 0; } 173 | 174 | } 175 | 176 | 177 | /* #Mobile (Landscape) 178 | ================================================== */ 179 | 180 | /* Note: Design for a width of 480px */ 181 | 182 | @media only screen and (min-width: 480px) and (max-width: 767px) { 183 | .container { width: 420px; } 184 | .container .columns, 185 | .container .column { margin: 0; } 186 | 187 | .container .one.column, 188 | .container .one.columns, 189 | .container .two.columns, 190 | .container .three.columns, 191 | .container .four.columns, 192 | .container .five.columns, 193 | .container .six.columns, 194 | .container .seven.columns, 195 | .container .eight.columns, 196 | .container .nine.columns, 197 | .container .ten.columns, 198 | .container .eleven.columns, 199 | .container .twelve.columns, 200 | .container .thirteen.columns, 201 | .container .fourteen.columns, 202 | .container .fifteen.columns, 203 | .container .sixteen.columns, 204 | .container .one-third.column, 205 | .container .two-thirds.column { width: 420px; } 206 | } 207 | 208 | 209 | /* #Clearing 210 | ================================================== */ 211 | 212 | /* Self Clearing Goodness */ 213 | .container:after { content: "\0020"; display: block; height: 0; clear: both; visibility: hidden; } 214 | 215 | /* Use clearfix class on parent to clear nested columns, 216 | or wrap each row of columns in a
*/ 217 | .clearfix:before, 218 | .clearfix:after, 219 | .row:before, 220 | .row:after { 221 | content: '\0020'; 222 | display: block; 223 | overflow: hidden; 224 | visibility: hidden; 225 | width: 0; 226 | height: 0; } 227 | .row:after, 228 | .clearfix:after { 229 | clear: both; } 230 | .row, 231 | .clearfix { 232 | zoom: 1; } 233 | 234 | /* You can also use a
to clear columns */ 235 | .clear { 236 | clear: both; 237 | display: block; 238 | overflow: hidden; 239 | visibility: hidden; 240 | width: 0; 241 | height: 0; 242 | } 243 | -------------------------------------------------------------------------------- /js/leaflet.antimeridian-src.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : 3 | typeof define === 'function' && define.amd ? define(['exports'], factory) : 4 | (factory((global.L = global.L || {}, global.L.Wrapped = {}))); 5 | }(this, (function (exports) { 'use strict'; 6 | 7 | var version = "1.0.0+master.7986dc5"; 8 | 9 | /* 10 | * @namespace L.Wrapped 11 | * Utility functions to perform calculations not always supported by the 12 | * standard Javascript Math namespace. 13 | */ 14 | 15 | // @function sign(Number) 16 | // Returns NaN for non-numbers, 0 for 0, -1 for negative numbers, 17 | // 1 for positive numbers 18 | function sign(x) { 19 | return typeof x === 'number' ? x ? x < 0 ? -1 : 1 : 0 : NaN; 20 | } 21 | 22 | /* 23 | * @namespace L.Wrapped 24 | * Utility functions to calculate various shared aspects of mapping a line 25 | * accross the antimeridian. 26 | */ 27 | 28 | // @function calculateAntimeridianLat (latLngA: L.LatLng, latLngB: L.LatLng) 29 | // Returns the calculated latitude where a line drawn between 30 | // two Latitude/Longitude points will cross the antimeridian. 31 | function calculateAntimeridianLat(latLngA, latLngB) { 32 | if (latLngA instanceof L.LatLng && latLngB instanceof L.LatLng) { 33 | // Ensure that the latitude A is less than latidue B. This will allow the 34 | // crossing point to be calculated based on the proportional similarity of 35 | // right triangles. 36 | 37 | // Locate which latitude is lower on the map. This will be the most 38 | // accute angle of the right triangle. If the lowest latitude is not latLngA 39 | // then swap the latlngs so it is. 40 | if (latLngA.lat > latLngB.lat) { 41 | var temp = latLngA; 42 | latLngA = latLngB; 43 | latLngB = temp; 44 | } 45 | 46 | // This gets the width of the distance between the two points 47 | // (The bottom of a large right triangle drawn between them) 48 | var A = 360 - Math.abs(latLngA.lng - latLngB.lng); 49 | // This gets the height of the of distance between the two points 50 | // (The vertical line of a large right triange drawn between them) 51 | var B = latLngB.lat - latLngA.lat; 52 | // This gets the bottom distance of a proportional triangle inside the large 53 | // trangle where the vertical line instead sits at the 180 mark. 54 | var a = Math.abs(180 - Math.abs(latLngA.lng)); 55 | 56 | // Because triangle with identical angles must be proportional along the sides, 57 | // find the length of the vertical side of that inner triangle and then 58 | // add it to the lower point to predict the crossing point of the Antimeridian. 59 | return latLngA.lat + ((B * a) / A); 60 | } else { 61 | throw new Error('In order to calculate the Antimeridian latitude, two valid LatLngs are required.'); 62 | } 63 | } 64 | 65 | // @function isCrossAntimeridian(latLngA: L.LatLng, latLngB: L.LatLng) 66 | // Returns true if the line between the two points will cross either 67 | // the prime meridian (Greenwich) or its antimeridian (International Date Line) 68 | function isCrossMeridian(latLngA, latLngB) { 69 | if (latLngA instanceof L.LatLng && latLngB instanceof L.LatLng) { 70 | // Returns true if the signs are not the same. 71 | return sign(latLngA.lng) * sign(latLngB.lng) < 0; 72 | } else { 73 | throw new Error('In order to calculate whether two LatLngs cross a meridian, two valid LatLngs are required.'); 74 | } 75 | } 76 | 77 | 78 | // @function pushLatLng(ring: L.Point[], projectedBounds: L.Bounds, latlng: L.LatLng, map: L.Map) 79 | // Adds the latlng to the current ring as a layer point and expands the projected bounds. 80 | function pushLatLng(ring, projectedBounds, latlng, map) { 81 | if (ring instanceof Array && projectedBounds instanceof L.Bounds && latlng instanceof L.LatLng && map instanceof L.Map) { 82 | ring.push(map.latLngToLayerPoint(latlng)); 83 | projectedBounds.extend(ring[ring.length - 1]); 84 | } else { 85 | throw new Error('In order to push a LatLng into a ring, the ring point array, the LatLng, the projectedBounds, and the map must all be valid.'); 86 | } 87 | } 88 | 89 | // @function isBreakRing(latLngA: L.LatLng, latLngB: L.LatLng) 90 | // Determines when the ring should be broken and a new one started. 91 | // This will return true if the distance is smaller when mapped across the Antimeridian. 92 | function isBreakRing(latLngA, latLngB) { 93 | if (latLngA instanceof L.LatLng && latLngB instanceof L.LatLng) { 94 | return isCrossMeridian(latLngA, latLngB) && 95 | (360 - Math.abs(latLngA.lng) - Math.abs(latLngB.lng) < 180); 96 | 97 | } else { 98 | throw new Error('In order to calculate whether the ring created by two LatLngs should be broken, two valid LatLngs are required.'); 99 | } 100 | } 101 | 102 | // @function breakRing(currentLat: L.LatLng, nextLat: L.LatLng, rings: L.Point[][], 103 | // projectedBounds: L.Bounds, map: L.Map) 104 | // Breaks the existing ring along the anti-meridian. 105 | // returns the starting latLng for the next ring. 106 | function breakRing(currentLat, nextLat, rings, projectedBounds, map) { 107 | if (currentLat instanceof L.LatLng && nextLat instanceof L.LatLng && rings instanceof Array && projectedBounds instanceof L.Bounds && map instanceof L.Map) { 108 | var ring = rings[rings.length - 1]; 109 | 110 | // Calculate two points for the anti-meridian crossing. 111 | var breakLat = calculateAntimeridianLat(currentLat, nextLat); 112 | var breakLatLngs = [new L.LatLng(breakLat, 180), new L.LatLng(breakLat, -180)]; 113 | 114 | // Add in first anti-meridian latlng to this ring to finish it. 115 | // Positive if positive, negative if negative. 116 | if (sign(currentLat.lng) > 0) { 117 | pushLatLng(ring, projectedBounds, breakLatLngs.shift(), map); 118 | } else { 119 | pushLatLng(ring, projectedBounds, breakLatLngs.pop(), map); 120 | } 121 | 122 | // Return the second anti-meridian latlng 123 | return breakLatLngs.pop(); 124 | } else { 125 | throw new Error('In order to break a ring, all the inputs must exist and be valid.'); 126 | } 127 | } 128 | 129 | /* 130 | * @namespace L.Wrapped 131 | * A polyline that will automatically split and wrap around the Antimeridian (Internation Date Line). 132 | */ 133 | var Polyline = L.Polyline.extend({ 134 | 135 | // recursively turns latlngs into a set of rings with projected coordinates 136 | // This is the entrypoint that is called from the overriden class to change 137 | // the rendering. 138 | _projectLatlngs: function (latlngs, result, projectedBounds) { 139 | var isMultiRing = latlngs[0] instanceof L.LatLng; 140 | 141 | if (isMultiRing) { 142 | this._createRings(latlngs, result, projectedBounds); 143 | } else { 144 | for (var i = 0; i < latlngs.length; i++) { 145 | this._projectLatlngs(latlngs[i], result, projectedBounds); 146 | } 147 | } 148 | }, 149 | 150 | // Creates the rings used to render the latlngs. 151 | _createRings: function (latlngs, rings, projectedBounds) { 152 | var len = latlngs.length; 153 | rings.push([]); 154 | 155 | for (var i = 0; i < len; i++) { 156 | var compareLatLng = this._getCompareLatLng(i, len, latlngs); 157 | var currentLatLng = latlngs[i]; 158 | 159 | pushLatLng(rings[rings.length - 1], projectedBounds, latlngs[i], this._map); 160 | 161 | // If the next point to check exists, then check to see if the 162 | // ring should be broken. 163 | if (compareLatLng && isBreakRing(compareLatLng, currentLatLng)) { 164 | var secondMeridianLatLng = breakRing(currentLatLng, compareLatLng, 165 | rings, projectedBounds, this._map); 166 | 167 | this._startNextRing(rings, projectedBounds, secondMeridianLatLng); 168 | } 169 | } 170 | }, 171 | 172 | // returns the latlng to compare the current latlng to. 173 | _getCompareLatLng: function (i, len, latlngs) { 174 | return (i + 1 < len) ? latlngs[i + 1] : null; 175 | }, 176 | 177 | // Starts a new ring and adds the second meridian point. 178 | _startNextRing: function (rings, projectedBounds, secondMeridianLatLng) { 179 | var ring = []; 180 | rings.push(ring); 181 | pushLatLng(ring, projectedBounds, secondMeridianLatLng, this._map); 182 | } 183 | }); 184 | 185 | // @factory L.wrappedPolyline(latlngs: LatLng[], options?: Polyline options) 186 | // Instantiates a polyline that will automatically split around the 187 | // antimeridian (Internation Date Line) if that is a shorter path. 188 | function wrappedPolyline(latlngs, options) { 189 | return new L.Wrapped.Polyline(latlngs, options); 190 | } 191 | 192 | /* 193 | * @namespace L.Wrapped 194 | * A polygon that will automatically split and wrap around the Antimeridian (Internation Date Line). 195 | */ 196 | var Polygon = L.Polygon.extend({ 197 | 198 | // recursively turns latlngs into a set of rings with projected coordinates 199 | // This is the entrypoint that is called from the overriden class to change 200 | // the rendering. 201 | _projectLatlngs: function (latlngs, result, projectedBounds) { 202 | var isMultiRing = latlngs[0] instanceof L.LatLng; 203 | 204 | if (isMultiRing) { 205 | this._createRings(latlngs, result, projectedBounds); 206 | } else { 207 | for (var i = 0; i < latlngs.length; i++) { 208 | this._projectLatlngs(latlngs[i], result, projectedBounds); 209 | } 210 | } 211 | }, 212 | 213 | // Creates the rings used to render the latlngs. 214 | _createRings: function (latlngs, rings, projectedBounds) { 215 | var len = latlngs.length; 216 | rings.push([]); 217 | 218 | for (var i = 0; i < len; i++) { 219 | // Because this is a polygon, there will always be a comparison latlng 220 | var compareLatLng = this._getCompareLatLng(i, len, latlngs); 221 | var currentLatLng = latlngs[i]; 222 | 223 | pushLatLng(rings[rings.length - 1], projectedBounds, currentLatLng, this._map); 224 | 225 | // Check to see if the ring should be broken. 226 | if (isBreakRing(compareLatLng, currentLatLng)) { 227 | var secondMeridianLatLng = breakRing(currentLatLng, compareLatLng, 228 | rings, projectedBounds, this._map); 229 | 230 | this._startNextRing(rings, projectedBounds, secondMeridianLatLng, i === len - 1); 231 | } 232 | } 233 | 234 | // Join the last two rings if needed. 235 | this._checkConcaveRings(rings); 236 | this._joinLastRing(rings, latlngs); 237 | }, 238 | 239 | // Starts a new ring if needed and adds the second meridian point to the 240 | // correct ring. 241 | _startNextRing: function (rings, projectedBounds, secondMeridianLatLng, isLastLatLng) { 242 | var ring; 243 | if (!isLastLatLng) { 244 | ring = []; 245 | rings.push(ring); 246 | pushLatLng(ring, projectedBounds, secondMeridianLatLng, this._map); 247 | } else { 248 | // If this is the last latlng, don't bother starting a new ring. 249 | // instead, join the last meridian point to the first point, to connect 250 | // the shape correctly. 251 | ring = rings[0]; 252 | ring.unshift(this._map.latLngToLayerPoint(secondMeridianLatLng)); 253 | projectedBounds.extend(ring[0]); 254 | } 255 | }, 256 | 257 | // returns the latlng to compare the current latlng to. 258 | _getCompareLatLng: function (i, len, latlngs) { 259 | return (i + 1 < len) ? latlngs[i + 1] : latlngs[0]; 260 | }, 261 | 262 | // Joins the last ring to the first if they were accidentally disconnected by 263 | // crossing the anti-meridian 264 | _joinLastRing: function (rings, latlngs) { 265 | var firstRing = rings[0]; 266 | var lastRing = rings[rings.length - 1]; 267 | 268 | // If either the first or last latlng cross the meridian immediately, then 269 | // they will be drawn as a single line, not a polygon, since they will not be 270 | // connected to the last ring. Reconnect them. 271 | if (rings.length > 1 && (firstRing.length === 2 || lastRing.length === 2) && 272 | !isCrossMeridian(latlngs[0], latlngs[latlngs.length - 1])) { 273 | var len = lastRing.length; 274 | for (var i = 0; i < len; i++) { 275 | firstRing.unshift(lastRing.pop()); 276 | } 277 | // Remove the empty ring. 278 | rings.pop(); 279 | } 280 | }, 281 | 282 | // Check for concave sections of the rings and join the rings if they are 283 | // concave 284 | _checkConcaveRings: function (rings) { 285 | var firstLatLng = this._map.layerPointToLatLng(rings[0][0]); 286 | 287 | for (var i = 0; i <= rings.length - 3; i++) { 288 | var middleLatLng = this._map.layerPointToLatLng(rings[i + 1][0]); 289 | var lastLatLng = this._map.layerPointToLatLng(rings[i + 2][0]); 290 | 291 | // If the meridian is crossed and then is crossed again 292 | // over the first polygon, the polygon is concave. Join the rings. 293 | if (isCrossMeridian(firstLatLng, middleLatLng) && 294 | isCrossMeridian(middleLatLng, lastLatLng)) { 295 | var firstRing = rings[0]; 296 | var lastRing = rings[i + 2]; 297 | 298 | var newRing = firstRing.concat(lastRing); 299 | 300 | // Remove the joined polygon and then update the first polygon. 301 | rings.splice(i + 2, 1); 302 | rings.splice(0, 1, newRing); 303 | } 304 | } 305 | } 306 | }); 307 | 308 | // @factory L.wrappedPolygon(latlngs: LatLng[], options?: Polygon options) 309 | // Instantiates a polygon that will automatically split around the 310 | // antimeridian (Internation Date Line) if that is a shorter path. 311 | function wrappedPolygon(latlngs, options) { 312 | return new L.Wrapped.Polygon(latlngs, options); 313 | } 314 | 315 | exports.version = version; 316 | exports.Polyline = Polyline; 317 | exports.wrappedPolyline = wrappedPolyline; 318 | exports.Polygon = Polygon; 319 | exports.wrappedPolygon = wrappedPolygon; 320 | exports.calculateAntimeridianLat = calculateAntimeridianLat; 321 | exports.isCrossMeridian = isCrossMeridian; 322 | exports.isBreakRing = isBreakRing; 323 | exports.sign = sign; 324 | 325 | }))); 326 | //# sourceMappingURL=leaflet.antimeridian-src.js.map 327 | -------------------------------------------------------------------------------- /css/leaflet.css: -------------------------------------------------------------------------------- 1 | /* required styles */ 2 | 3 | .leaflet-pane, 4 | .leaflet-tile, 5 | .leaflet-marker-icon, 6 | .leaflet-marker-shadow, 7 | .leaflet-tile-container, 8 | .leaflet-pane > svg, 9 | .leaflet-pane > canvas, 10 | .leaflet-zoom-box, 11 | .leaflet-image-layer, 12 | .leaflet-layer { 13 | position: absolute; 14 | left: 0; 15 | top: 0; 16 | } 17 | .leaflet-container { 18 | overflow: hidden; 19 | } 20 | .leaflet-tile, 21 | .leaflet-marker-icon, 22 | .leaflet-marker-shadow { 23 | -webkit-user-select: none; 24 | -moz-user-select: none; 25 | user-select: none; 26 | -webkit-user-drag: none; 27 | } 28 | /* Prevents IE11 from highlighting tiles in blue */ 29 | .leaflet-tile::selection { 30 | background: transparent; 31 | } 32 | /* Safari renders non-retina tile on retina better with this, but Chrome is worse */ 33 | .leaflet-safari .leaflet-tile { 34 | image-rendering: -webkit-optimize-contrast; 35 | } 36 | /* hack that prevents hw layers "stretching" when loading new tiles */ 37 | .leaflet-safari .leaflet-tile-container { 38 | width: 1600px; 39 | height: 1600px; 40 | -webkit-transform-origin: 0 0; 41 | } 42 | .leaflet-marker-icon, 43 | .leaflet-marker-shadow { 44 | display: block; 45 | } 46 | /* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */ 47 | /* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */ 48 | .leaflet-container .leaflet-overlay-pane svg, 49 | .leaflet-container .leaflet-marker-pane img, 50 | .leaflet-container .leaflet-shadow-pane img, 51 | .leaflet-container .leaflet-tile-pane img, 52 | .leaflet-container img.leaflet-image-layer, 53 | .leaflet-container .leaflet-tile { 54 | max-width: none !important; 55 | max-height: none !important; 56 | } 57 | 58 | .leaflet-container.leaflet-touch-zoom { 59 | -ms-touch-action: pan-x pan-y; 60 | touch-action: pan-x pan-y; 61 | } 62 | .leaflet-container.leaflet-touch-drag { 63 | -ms-touch-action: pinch-zoom; 64 | /* Fallback for FF which doesn't support pinch-zoom */ 65 | touch-action: none; 66 | touch-action: pinch-zoom; 67 | } 68 | .leaflet-container.leaflet-touch-drag.leaflet-touch-zoom { 69 | -ms-touch-action: none; 70 | touch-action: none; 71 | } 72 | .leaflet-container { 73 | -webkit-tap-highlight-color: transparent; 74 | } 75 | .leaflet-container a { 76 | -webkit-tap-highlight-color: rgba(51, 181, 229, 0.4); 77 | } 78 | .leaflet-tile { 79 | filter: inherit; 80 | visibility: hidden; 81 | } 82 | .leaflet-tile-loaded { 83 | visibility: inherit; 84 | } 85 | .leaflet-zoom-box { 86 | width: 0; 87 | height: 0; 88 | -moz-box-sizing: border-box; 89 | box-sizing: border-box; 90 | z-index: 800; 91 | } 92 | /* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */ 93 | .leaflet-overlay-pane svg { 94 | -moz-user-select: none; 95 | } 96 | 97 | .leaflet-pane { z-index: 400; } 98 | 99 | .leaflet-tile-pane { z-index: 200; } 100 | .leaflet-overlay-pane { z-index: 400; } 101 | .leaflet-shadow-pane { z-index: 500; } 102 | .leaflet-marker-pane { z-index: 600; } 103 | .leaflet-tooltip-pane { z-index: 650; } 104 | .leaflet-popup-pane { z-index: 700; } 105 | 106 | .leaflet-map-pane canvas { z-index: 100; } 107 | .leaflet-map-pane svg { z-index: 200; } 108 | 109 | .leaflet-vml-shape { 110 | width: 1px; 111 | height: 1px; 112 | } 113 | .lvml { 114 | behavior: url(#default#VML); 115 | display: inline-block; 116 | position: absolute; 117 | } 118 | 119 | 120 | /* control positioning */ 121 | 122 | .leaflet-control { 123 | position: relative; 124 | z-index: 800; 125 | pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ 126 | pointer-events: auto; 127 | } 128 | .leaflet-top, 129 | .leaflet-bottom { 130 | position: absolute; 131 | z-index: 1000; 132 | pointer-events: none; 133 | } 134 | .leaflet-top { 135 | top: 0; 136 | } 137 | .leaflet-right { 138 | right: 0; 139 | } 140 | .leaflet-bottom { 141 | bottom: 0; 142 | } 143 | .leaflet-left { 144 | left: 0; 145 | } 146 | .leaflet-control { 147 | float: left; 148 | clear: both; 149 | } 150 | .leaflet-right .leaflet-control { 151 | float: right; 152 | } 153 | .leaflet-top .leaflet-control { 154 | margin-top: 10px; 155 | } 156 | .leaflet-bottom .leaflet-control { 157 | margin-bottom: 10px; 158 | } 159 | .leaflet-left .leaflet-control { 160 | margin-left: 10px; 161 | } 162 | .leaflet-right .leaflet-control { 163 | margin-right: 10px; 164 | } 165 | 166 | 167 | /* zoom and fade animations */ 168 | 169 | .leaflet-fade-anim .leaflet-tile { 170 | will-change: opacity; 171 | } 172 | .leaflet-fade-anim .leaflet-popup { 173 | opacity: 0; 174 | -webkit-transition: opacity 0.2s linear; 175 | -moz-transition: opacity 0.2s linear; 176 | transition: opacity 0.2s linear; 177 | } 178 | .leaflet-fade-anim .leaflet-map-pane .leaflet-popup { 179 | opacity: 1; 180 | } 181 | .leaflet-zoom-animated { 182 | -webkit-transform-origin: 0 0; 183 | -ms-transform-origin: 0 0; 184 | transform-origin: 0 0; 185 | } 186 | .leaflet-zoom-anim .leaflet-zoom-animated { 187 | will-change: transform; 188 | } 189 | .leaflet-zoom-anim .leaflet-zoom-animated { 190 | -webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1); 191 | -moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1); 192 | transition: transform 0.25s cubic-bezier(0,0,0.25,1); 193 | } 194 | .leaflet-zoom-anim .leaflet-tile, 195 | .leaflet-pan-anim .leaflet-tile { 196 | -webkit-transition: none; 197 | -moz-transition: none; 198 | transition: none; 199 | } 200 | 201 | .leaflet-zoom-anim .leaflet-zoom-hide { 202 | visibility: hidden; 203 | } 204 | 205 | 206 | /* cursors */ 207 | 208 | .leaflet-interactive { 209 | cursor: pointer; 210 | } 211 | .leaflet-grab { 212 | cursor: -webkit-grab; 213 | cursor: -moz-grab; 214 | cursor: grab; 215 | } 216 | .leaflet-crosshair, 217 | .leaflet-crosshair .leaflet-interactive { 218 | cursor: crosshair; 219 | } 220 | .leaflet-popup-pane, 221 | .leaflet-control { 222 | cursor: auto; 223 | } 224 | .leaflet-dragging .leaflet-grab, 225 | .leaflet-dragging .leaflet-grab .leaflet-interactive, 226 | .leaflet-dragging .leaflet-marker-draggable { 227 | cursor: move; 228 | cursor: -webkit-grabbing; 229 | cursor: -moz-grabbing; 230 | cursor: grabbing; 231 | } 232 | 233 | /* marker & overlays interactivity */ 234 | .leaflet-marker-icon, 235 | .leaflet-marker-shadow, 236 | .leaflet-image-layer, 237 | .leaflet-pane > svg path, 238 | .leaflet-tile-container { 239 | pointer-events: none; 240 | } 241 | 242 | .leaflet-marker-icon.leaflet-interactive, 243 | .leaflet-image-layer.leaflet-interactive, 244 | .leaflet-pane > svg path.leaflet-interactive, 245 | svg.leaflet-image-layer.leaflet-interactive path { 246 | pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ 247 | pointer-events: auto; 248 | } 249 | 250 | /* visual tweaks */ 251 | 252 | .leaflet-container { 253 | background: #ddd; 254 | outline: 0; 255 | } 256 | .leaflet-container a { 257 | color: #0078A8; 258 | } 259 | .leaflet-container a.leaflet-active { 260 | outline: 2px solid orange; 261 | } 262 | .leaflet-zoom-box { 263 | border: 2px dotted #38f; 264 | background: rgba(255,255,255,0.5); 265 | } 266 | 267 | 268 | /* general typography */ 269 | .leaflet-container { 270 | font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif; 271 | } 272 | 273 | 274 | /* general toolbar styles */ 275 | 276 | .leaflet-bar { 277 | box-shadow: 0 1px 5px rgba(0,0,0,0.65); 278 | border-radius: 4px; 279 | } 280 | .leaflet-bar a, 281 | .leaflet-bar a:hover { 282 | background-color: #fff; 283 | border-bottom: 1px solid #ccc; 284 | width: 26px; 285 | height: 26px; 286 | line-height: 26px; 287 | display: block; 288 | text-align: center; 289 | text-decoration: none; 290 | color: black; 291 | } 292 | .leaflet-bar a, 293 | .leaflet-control-layers-toggle { 294 | background-position: 50% 50%; 295 | background-repeat: no-repeat; 296 | display: block; 297 | } 298 | .leaflet-bar a:hover { 299 | background-color: #f4f4f4; 300 | } 301 | .leaflet-bar a:first-child { 302 | border-top-left-radius: 4px; 303 | border-top-right-radius: 4px; 304 | } 305 | .leaflet-bar a:last-child { 306 | border-bottom-left-radius: 4px; 307 | border-bottom-right-radius: 4px; 308 | border-bottom: none; 309 | } 310 | .leaflet-bar a.leaflet-disabled { 311 | cursor: default; 312 | background-color: #f4f4f4; 313 | color: #bbb; 314 | } 315 | 316 | .leaflet-touch .leaflet-bar a { 317 | width: 30px; 318 | height: 30px; 319 | line-height: 30px; 320 | } 321 | .leaflet-touch .leaflet-bar a:first-child { 322 | border-top-left-radius: 2px; 323 | border-top-right-radius: 2px; 324 | } 325 | .leaflet-touch .leaflet-bar a:last-child { 326 | border-bottom-left-radius: 2px; 327 | border-bottom-right-radius: 2px; 328 | } 329 | 330 | /* zoom control */ 331 | 332 | .leaflet-control-zoom-in, 333 | .leaflet-control-zoom-out { 334 | font: bold 18px 'Lucida Console', Monaco, monospace; 335 | text-indent: 1px; 336 | } 337 | 338 | .leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out { 339 | font-size: 22px; 340 | } 341 | 342 | 343 | /* layers control */ 344 | 345 | .leaflet-control-layers { 346 | box-shadow: 0 1px 5px rgba(0,0,0,0.4); 347 | background: #fff; 348 | border-radius: 5px; 349 | } 350 | .leaflet-control-layers-toggle { 351 | background-image: url(images/layers.png); 352 | width: 36px; 353 | height: 36px; 354 | } 355 | .leaflet-retina .leaflet-control-layers-toggle { 356 | background-image: url(images/layers-2x.png); 357 | background-size: 26px 26px; 358 | } 359 | .leaflet-touch .leaflet-control-layers-toggle { 360 | width: 44px; 361 | height: 44px; 362 | } 363 | .leaflet-control-layers .leaflet-control-layers-list, 364 | .leaflet-control-layers-expanded .leaflet-control-layers-toggle { 365 | display: none; 366 | } 367 | .leaflet-control-layers-expanded .leaflet-control-layers-list { 368 | display: block; 369 | position: relative; 370 | } 371 | .leaflet-control-layers-expanded { 372 | padding: 6px 10px 6px 6px; 373 | color: #333; 374 | background: #fff; 375 | } 376 | .leaflet-control-layers-scrollbar { 377 | overflow-y: scroll; 378 | overflow-x: hidden; 379 | padding-right: 5px; 380 | } 381 | .leaflet-control-layers-selector { 382 | margin-top: 2px; 383 | position: relative; 384 | top: 1px; 385 | } 386 | .leaflet-control-layers label { 387 | display: block; 388 | } 389 | .leaflet-control-layers-separator { 390 | height: 0; 391 | border-top: 1px solid #ddd; 392 | margin: 5px -10px 5px -6px; 393 | } 394 | 395 | /* Default icon URLs */ 396 | .leaflet-default-icon-path { 397 | background-image: url(images/marker-icon.png); 398 | } 399 | 400 | 401 | /* attribution and scale controls */ 402 | 403 | .leaflet-container .leaflet-control-attribution { 404 | background: #fff; 405 | background: rgba(255, 255, 255, 0.7); 406 | margin: 0; 407 | } 408 | .leaflet-control-attribution, 409 | .leaflet-control-scale-line { 410 | padding: 0 5px; 411 | color: #333; 412 | } 413 | .leaflet-control-attribution a { 414 | text-decoration: none; 415 | } 416 | .leaflet-control-attribution a:hover { 417 | text-decoration: underline; 418 | } 419 | .leaflet-container .leaflet-control-attribution, 420 | .leaflet-container .leaflet-control-scale { 421 | font-size: 11px; 422 | } 423 | .leaflet-left .leaflet-control-scale { 424 | margin-left: 5px; 425 | } 426 | .leaflet-bottom .leaflet-control-scale { 427 | margin-bottom: 5px; 428 | } 429 | .leaflet-control-scale-line { 430 | border: 2px solid #777; 431 | border-top: none; 432 | line-height: 1.1; 433 | padding: 2px 5px 1px; 434 | font-size: 11px; 435 | white-space: nowrap; 436 | overflow: hidden; 437 | -moz-box-sizing: border-box; 438 | box-sizing: border-box; 439 | 440 | background: #fff; 441 | background: rgba(255, 255, 255, 0.5); 442 | } 443 | .leaflet-control-scale-line:not(:first-child) { 444 | border-top: 2px solid #777; 445 | border-bottom: none; 446 | margin-top: -2px; 447 | } 448 | .leaflet-control-scale-line:not(:first-child):not(:last-child) { 449 | border-bottom: 2px solid #777; 450 | } 451 | 452 | .leaflet-touch .leaflet-control-attribution, 453 | .leaflet-touch .leaflet-control-layers, 454 | .leaflet-touch .leaflet-bar { 455 | box-shadow: none; 456 | } 457 | .leaflet-touch .leaflet-control-layers, 458 | .leaflet-touch .leaflet-bar { 459 | border: 2px solid rgba(0,0,0,0.2); 460 | background-clip: padding-box; 461 | } 462 | 463 | 464 | /* popup */ 465 | 466 | .leaflet-popup { 467 | position: absolute; 468 | text-align: center; 469 | margin-bottom: 20px; 470 | } 471 | .leaflet-popup-content-wrapper { 472 | padding: 1px; 473 | text-align: left; 474 | border-radius: 12px; 475 | } 476 | .leaflet-popup-content { 477 | margin: 13px 19px; 478 | line-height: 1.4; 479 | } 480 | .leaflet-popup-content p { 481 | margin: 18px 0; 482 | } 483 | .leaflet-popup-tip-container { 484 | width: 40px; 485 | height: 20px; 486 | position: absolute; 487 | left: 50%; 488 | margin-left: -20px; 489 | overflow: hidden; 490 | pointer-events: none; 491 | } 492 | .leaflet-popup-tip { 493 | width: 17px; 494 | height: 17px; 495 | padding: 1px; 496 | 497 | margin: -10px auto 0; 498 | 499 | -webkit-transform: rotate(45deg); 500 | -moz-transform: rotate(45deg); 501 | -ms-transform: rotate(45deg); 502 | transform: rotate(45deg); 503 | } 504 | .leaflet-popup-content-wrapper, 505 | .leaflet-popup-tip { 506 | background: white; 507 | color: #333; 508 | box-shadow: 0 3px 14px rgba(0,0,0,0.4); 509 | } 510 | .leaflet-container a.leaflet-popup-close-button { 511 | position: absolute; 512 | top: 0; 513 | right: 0; 514 | padding: 4px 4px 0 0; 515 | border: none; 516 | text-align: center; 517 | width: 18px; 518 | height: 14px; 519 | font: 16px/14px Tahoma, Verdana, sans-serif; 520 | color: #c3c3c3; 521 | text-decoration: none; 522 | font-weight: bold; 523 | background: transparent; 524 | } 525 | .leaflet-container a.leaflet-popup-close-button:hover { 526 | color: #999; 527 | } 528 | .leaflet-popup-scrolled { 529 | overflow: auto; 530 | border-bottom: 1px solid #ddd; 531 | border-top: 1px solid #ddd; 532 | } 533 | 534 | .leaflet-oldie .leaflet-popup-content-wrapper { 535 | -ms-zoom: 1; 536 | } 537 | .leaflet-oldie .leaflet-popup-tip { 538 | width: 24px; 539 | margin: 0 auto; 540 | 541 | -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)"; 542 | filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678); 543 | } 544 | .leaflet-oldie .leaflet-popup-tip-container { 545 | margin-top: -1px; 546 | } 547 | 548 | .leaflet-oldie .leaflet-control-zoom, 549 | .leaflet-oldie .leaflet-control-layers, 550 | .leaflet-oldie .leaflet-popup-content-wrapper, 551 | .leaflet-oldie .leaflet-popup-tip { 552 | border: 1px solid #999; 553 | } 554 | 555 | 556 | /* div icon */ 557 | 558 | .leaflet-div-icon { 559 | background: #fff; 560 | border: 1px solid #666; 561 | } 562 | 563 | 564 | /* Tooltip */ 565 | /* Base styles for the element that has a tooltip */ 566 | .leaflet-tooltip { 567 | position: absolute; 568 | padding: 6px; 569 | background-color: #fff; 570 | border: 1px solid #fff; 571 | border-radius: 3px; 572 | color: #222; 573 | white-space: nowrap; 574 | -webkit-user-select: none; 575 | -moz-user-select: none; 576 | -ms-user-select: none; 577 | user-select: none; 578 | pointer-events: none; 579 | box-shadow: 0 1px 3px rgba(0,0,0,0.4); 580 | } 581 | .leaflet-tooltip.leaflet-clickable { 582 | cursor: pointer; 583 | pointer-events: auto; 584 | } 585 | .leaflet-tooltip-top:before, 586 | .leaflet-tooltip-bottom:before, 587 | .leaflet-tooltip-left:before, 588 | .leaflet-tooltip-right:before { 589 | position: absolute; 590 | pointer-events: none; 591 | border: 6px solid transparent; 592 | background: transparent; 593 | content: ""; 594 | } 595 | 596 | /* Directions */ 597 | 598 | .leaflet-tooltip-bottom { 599 | margin-top: 6px; 600 | } 601 | .leaflet-tooltip-top { 602 | margin-top: -6px; 603 | } 604 | .leaflet-tooltip-bottom:before, 605 | .leaflet-tooltip-top:before { 606 | left: 50%; 607 | margin-left: -6px; 608 | } 609 | .leaflet-tooltip-top:before { 610 | bottom: 0; 611 | margin-bottom: -12px; 612 | border-top-color: #fff; 613 | } 614 | .leaflet-tooltip-bottom:before { 615 | top: 0; 616 | margin-top: -12px; 617 | margin-left: -6px; 618 | border-bottom-color: #fff; 619 | } 620 | .leaflet-tooltip-left { 621 | margin-left: -6px; 622 | } 623 | .leaflet-tooltip-right { 624 | margin-left: 6px; 625 | } 626 | .leaflet-tooltip-left:before, 627 | .leaflet-tooltip-right:before { 628 | top: 50%; 629 | margin-top: -6px; 630 | } 631 | .leaflet-tooltip-left:before { 632 | right: 0; 633 | margin-right: -12px; 634 | border-left-color: #fff; 635 | } 636 | .leaflet-tooltip-right:before { 637 | left: 0; 638 | margin-left: -12px; 639 | border-right-color: #fff; 640 | } 641 | -------------------------------------------------------------------------------- /js/rbush.js: -------------------------------------------------------------------------------- 1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.rbush = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o= 0) { 308 | if (insertPath[level].children.length > this._maxEntries) { 309 | this._split(insertPath, level); 310 | level--; 311 | } else break; 312 | } 313 | 314 | // adjust bboxes along the insertion path 315 | this._adjustParentBBoxes(bbox, insertPath, level); 316 | }, 317 | 318 | // split overflowed node into two 319 | _split: function (insertPath, level) { 320 | 321 | var node = insertPath[level], 322 | M = node.children.length, 323 | m = this._minEntries; 324 | 325 | this._chooseSplitAxis(node, m, M); 326 | 327 | var splitIndex = this._chooseSplitIndex(node, m, M); 328 | 329 | var newNode = createNode(node.children.splice(splitIndex, node.children.length - splitIndex)); 330 | newNode.height = node.height; 331 | newNode.leaf = node.leaf; 332 | 333 | calcBBox(node, this.toBBox); 334 | calcBBox(newNode, this.toBBox); 335 | 336 | if (level) insertPath[level - 1].children.push(newNode); 337 | else this._splitRoot(node, newNode); 338 | }, 339 | 340 | _splitRoot: function (node, newNode) { 341 | // split root node 342 | this.data = createNode([node, newNode]); 343 | this.data.height = node.height + 1; 344 | this.data.leaf = false; 345 | calcBBox(this.data, this.toBBox); 346 | }, 347 | 348 | _chooseSplitIndex: function (node, m, M) { 349 | 350 | var i, bbox1, bbox2, overlap, area, minOverlap, minArea, index; 351 | 352 | minOverlap = minArea = Infinity; 353 | 354 | for (i = m; i <= M - m; i++) { 355 | bbox1 = distBBox(node, 0, i, this.toBBox); 356 | bbox2 = distBBox(node, i, M, this.toBBox); 357 | 358 | overlap = intersectionArea(bbox1, bbox2); 359 | area = bboxArea(bbox1) + bboxArea(bbox2); 360 | 361 | // choose distribution with minimum overlap 362 | if (overlap < minOverlap) { 363 | minOverlap = overlap; 364 | index = i; 365 | 366 | minArea = area < minArea ? area : minArea; 367 | 368 | } else if (overlap === minOverlap) { 369 | // otherwise choose distribution with minimum area 370 | if (area < minArea) { 371 | minArea = area; 372 | index = i; 373 | } 374 | } 375 | } 376 | 377 | return index; 378 | }, 379 | 380 | // sorts node children by the best axis for split 381 | _chooseSplitAxis: function (node, m, M) { 382 | 383 | var compareMinX = node.leaf ? this.compareMinX : compareNodeMinX, 384 | compareMinY = node.leaf ? this.compareMinY : compareNodeMinY, 385 | xMargin = this._allDistMargin(node, m, M, compareMinX), 386 | yMargin = this._allDistMargin(node, m, M, compareMinY); 387 | 388 | // if total distributions margin value is minimal for x, sort by minX, 389 | // otherwise it's already sorted by minY 390 | if (xMargin < yMargin) node.children.sort(compareMinX); 391 | }, 392 | 393 | // total margin of all possible split distributions where each node is at least m full 394 | _allDistMargin: function (node, m, M, compare) { 395 | 396 | node.children.sort(compare); 397 | 398 | var toBBox = this.toBBox, 399 | leftBBox = distBBox(node, 0, m, toBBox), 400 | rightBBox = distBBox(node, M - m, M, toBBox), 401 | margin = bboxMargin(leftBBox) + bboxMargin(rightBBox), 402 | i, child; 403 | 404 | for (i = m; i < M - m; i++) { 405 | child = node.children[i]; 406 | extend(leftBBox, node.leaf ? toBBox(child) : child); 407 | margin += bboxMargin(leftBBox); 408 | } 409 | 410 | for (i = M - m - 1; i >= m; i--) { 411 | child = node.children[i]; 412 | extend(rightBBox, node.leaf ? toBBox(child) : child); 413 | margin += bboxMargin(rightBBox); 414 | } 415 | 416 | return margin; 417 | }, 418 | 419 | _adjustParentBBoxes: function (bbox, path, level) { 420 | // adjust bboxes along the given tree path 421 | for (var i = level; i >= 0; i--) { 422 | extend(path[i], bbox); 423 | } 424 | }, 425 | 426 | _condense: function (path) { 427 | // go through the path, removing empty nodes and updating bboxes 428 | for (var i = path.length - 1, siblings; i >= 0; i--) { 429 | if (path[i].children.length === 0) { 430 | if (i > 0) { 431 | siblings = path[i - 1].children; 432 | siblings.splice(siblings.indexOf(path[i]), 1); 433 | 434 | } else this.clear(); 435 | 436 | } else calcBBox(path[i], this.toBBox); 437 | } 438 | }, 439 | 440 | _initFormat: function (format) { 441 | // data format (minX, minY, maxX, maxY accessors) 442 | 443 | // uses eval-type function compilation instead of just accepting a toBBox function 444 | // because the algorithms are very sensitive to sorting functions performance, 445 | // so they should be dead simple and without inner calls 446 | 447 | var compareArr = ['return a', ' - b', ';']; 448 | 449 | this.compareMinX = new Function('a', 'b', compareArr.join(format[0])); 450 | this.compareMinY = new Function('a', 'b', compareArr.join(format[1])); 451 | 452 | this.toBBox = new Function('a', 453 | 'return {minX: a' + format[0] + 454 | ', minY: a' + format[1] + 455 | ', maxX: a' + format[2] + 456 | ', maxY: a' + format[3] + '};'); 457 | } 458 | }; 459 | 460 | function findItem(item, items, equalsFn) { 461 | if (!equalsFn) return items.indexOf(item); 462 | 463 | for (var i = 0; i < items.length; i++) { 464 | if (equalsFn(item, items[i])) return i; 465 | } 466 | return -1; 467 | } 468 | 469 | // calculate node's bbox from bboxes of its children 470 | function calcBBox(node, toBBox) { 471 | distBBox(node, 0, node.children.length, toBBox, node); 472 | } 473 | 474 | // min bounding rectangle of node children from k to p-1 475 | function distBBox(node, k, p, toBBox, destNode) { 476 | if (!destNode) destNode = createNode(null); 477 | destNode.minX = Infinity; 478 | destNode.minY = Infinity; 479 | destNode.maxX = -Infinity; 480 | destNode.maxY = -Infinity; 481 | 482 | for (var i = k, child; i < p; i++) { 483 | child = node.children[i]; 484 | extend(destNode, node.leaf ? toBBox(child) : child); 485 | } 486 | 487 | return destNode; 488 | } 489 | 490 | function extend(a, b) { 491 | a.minX = Math.min(a.minX, b.minX); 492 | a.minY = Math.min(a.minY, b.minY); 493 | a.maxX = Math.max(a.maxX, b.maxX); 494 | a.maxY = Math.max(a.maxY, b.maxY); 495 | return a; 496 | } 497 | 498 | function compareNodeMinX(a, b) { return a.minX - b.minX; } 499 | function compareNodeMinY(a, b) { return a.minY - b.minY; } 500 | 501 | function bboxArea(a) { return (a.maxX - a.minX) * (a.maxY - a.minY); } 502 | function bboxMargin(a) { return (a.maxX - a.minX) + (a.maxY - a.minY); } 503 | 504 | function enlargedArea(a, b) { 505 | return (Math.max(b.maxX, a.maxX) - Math.min(b.minX, a.minX)) * 506 | (Math.max(b.maxY, a.maxY) - Math.min(b.minY, a.minY)); 507 | } 508 | 509 | function intersectionArea(a, b) { 510 | var minX = Math.max(a.minX, b.minX), 511 | minY = Math.max(a.minY, b.minY), 512 | maxX = Math.min(a.maxX, b.maxX), 513 | maxY = Math.min(a.maxY, b.maxY); 514 | 515 | return Math.max(0, maxX - minX) * 516 | Math.max(0, maxY - minY); 517 | } 518 | 519 | function contains(a, b) { 520 | return a.minX <= b.minX && 521 | a.minY <= b.minY && 522 | b.maxX <= a.maxX && 523 | b.maxY <= a.maxY; 524 | } 525 | 526 | function intersects(a, b) { 527 | return b.minX <= a.maxX && 528 | b.minY <= a.maxY && 529 | b.maxX >= a.minX && 530 | b.maxY >= a.minY; 531 | } 532 | 533 | function createNode(children) { 534 | return { 535 | children: children, 536 | height: 1, 537 | leaf: true, 538 | minX: Infinity, 539 | minY: Infinity, 540 | maxX: -Infinity, 541 | maxY: -Infinity 542 | }; 543 | } 544 | 545 | // sort an array so that items come in groups of n unsorted items, with groups sorted between each other; 546 | // combines selection algorithm with binary divide & conquer approach 547 | 548 | function multiSelect(arr, left, right, n, compare) { 549 | var stack = [left, right], 550 | mid; 551 | 552 | while (stack.length) { 553 | right = stack.pop(); 554 | left = stack.pop(); 555 | 556 | if (right - left <= n) continue; 557 | 558 | mid = left + Math.ceil((right - left) / n / 2) * n; 559 | quickselect(arr, mid, left, right, compare); 560 | 561 | stack.push(left, mid, mid, right); 562 | } 563 | } 564 | 565 | },{"quickselect":2}],2:[function(require,module,exports){ 566 | 'use strict'; 567 | 568 | module.exports = quickselect; 569 | module.exports.default = quickselect; 570 | 571 | function quickselect(arr, k, left, right, compare) { 572 | quickselectStep(arr, k, left || 0, right || (arr.length - 1), compare || defaultCompare); 573 | }; 574 | 575 | function quickselectStep(arr, k, left, right, compare) { 576 | 577 | while (right > left) { 578 | if (right - left > 600) { 579 | var n = right - left + 1; 580 | var m = k - left + 1; 581 | var z = Math.log(n); 582 | var s = 0.5 * Math.exp(2 * z / 3); 583 | var sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1); 584 | var newLeft = Math.max(left, Math.floor(k - m * s / n + sd)); 585 | var newRight = Math.min(right, Math.floor(k + (n - m) * s / n + sd)); 586 | quickselectStep(arr, k, newLeft, newRight, compare); 587 | } 588 | 589 | var t = arr[k]; 590 | var i = left; 591 | var j = right; 592 | 593 | swap(arr, left, k); 594 | if (compare(arr[right], t) > 0) swap(arr, left, right); 595 | 596 | while (i < j) { 597 | swap(arr, i, j); 598 | i++; 599 | j--; 600 | while (compare(arr[i], t) < 0) i++; 601 | while (compare(arr[j], t) > 0) j--; 602 | } 603 | 604 | if (compare(arr[left], t) === 0) swap(arr, left, j); 605 | else { 606 | j++; 607 | swap(arr, j, right); 608 | } 609 | 610 | if (j <= k) left = j + 1; 611 | if (k <= j) right = j - 1; 612 | } 613 | } 614 | 615 | function swap(arr, i, j) { 616 | var tmp = arr[i]; 617 | arr[i] = arr[j]; 618 | arr[j] = tmp; 619 | } 620 | 621 | function defaultCompare(a, b) { 622 | return a < b ? -1 : a > b ? 1 : 0; 623 | } 624 | 625 | },{}]},{},[1])(1) 626 | }); -------------------------------------------------------------------------------- /css/main.css: -------------------------------------------------------------------------------- 1 | /* SondeHub Amateur 2 | * Main style sheet 3 | * 4 | */ 5 | @font-face { 6 | font-family: 'Roboto'; 7 | font-style: normal; 8 | font-weight: 400; 9 | src: local('Roboto Regular'), local('Roboto-Regular'), url(../font/Roboto-regular.woff) format('woff'); 10 | } 11 | 12 | html, body { 13 | margin: 0; 14 | padding: 0; 15 | width: 100%; 16 | height: 100%; 17 | overflow: hidden; 18 | -webkit-touch-action: none; 19 | -khtml-touch-action: none; 20 | -moz-touch-action: none; 21 | -ms-touch-action: none; 22 | touch-action: none; 23 | } 24 | body { 25 | font-family: "Roboto", Ariel, sans-serif; 26 | } 27 | 28 | .noselect { 29 | -webkit-touch-callout: none; 30 | -webkit-user-select: none; 31 | -khtml-user-select: none; 32 | -moz-user-select: none; 33 | -ms-user-select: none; 34 | user-select: none; 35 | } 36 | 37 | .rfloat { 38 | float: right; 39 | } 40 | .lfloat { 41 | float: left; 42 | } 43 | 44 | .iScrollVerticalScrollbar { 45 | z-index: 49; 46 | width: 5px; 47 | bottom: 6px; 48 | top: 6px; 49 | left: 1px; 50 | position: absolute; 51 | } 52 | 53 | .iScrollVerticalScrollbar > div { 54 | position: absolute; 55 | background: none repeat scroll 0 0 padding-box rgba(0, 0, 0, 0.5); 56 | border: 1px solid rgba(255, 255, 255, 0.9); 57 | border-radius: 3px 3px 3px 3px; 58 | position: absolute; 59 | width: 100%; 60 | z-index: 49; 61 | } 62 | 63 | .slickbox { 64 | z-index: 49; 65 | background-color: #fff; 66 | position: absolute; 67 | height: 28px; 68 | width: 195px; 69 | border-radius: 20px; 70 | box-shadow: 0 0 5px #888; 71 | font-size: 11px; 72 | } 73 | 74 | .slickbox svg { 75 | width: 28px; 76 | height: 28px; 77 | } 78 | 79 | .slickbox span { 80 | line-height: 14px; 81 | } 82 | 83 | .slickbox div { 84 | font-size: 14px; 85 | margin-top: 3px; 86 | width: 150px; 87 | text-align: center; 88 | } 89 | 90 | .slickbox svg path { 91 | -webkit-transition: all 0.4s ease-in-out; 92 | -moz-transition: all 0.4s ease-in-out; 93 | -ms-transition: all 0.4s ease-in-out; 94 | -o-transition: all 0.4s ease-in-out; 95 | transition: all 0.4s ease-in-out; 96 | fill: #00a3d3; 97 | } 98 | 99 | #timebox { 100 | top: 7px; 101 | right: 5px; 102 | } 103 | 104 | #lookanglesbox { 105 | top: 40px; 106 | right: 5px; 107 | } 108 | 109 | #timebox.past svg path { 110 | fill: #c00; 111 | } 112 | #timebox .current { 113 | margin-left: 11px; 114 | } 115 | #timebox .local { 116 | margin-left: 5px; 117 | } 118 | 119 | .slickbox .azimuth { 120 | margin-left: 9px; 121 | width: 91px; 122 | } 123 | .slickbox .bearing { 124 | margin-right: 5px; 125 | width: 60px; 126 | } 127 | .slickbox .elevation { 128 | margin-left: 5px; 129 | width: 95px; 130 | } 131 | .slickbox .range { 132 | margin-right: 5px; 133 | width: 60px; 134 | } 135 | 136 | #mapscreen { 137 | float: right; 138 | position: relative; 139 | } 140 | 141 | #map img { 142 | max-width: none; 143 | } 144 | 145 | #loading { 146 | position: absolute; 147 | z-index: 99; 148 | width: 100%; 149 | height: 100%; 150 | background: #00A3D3; 151 | } 152 | #loading img { 153 | position: absolute; 154 | display: block; 155 | width: 60vw; 156 | max-width: 300px; 157 | left: 50%; 158 | top: 50%; 159 | -ms-transform: translate(-50%, -50%); 160 | transform: translate(-50%, -50%); 161 | } 162 | #loading .bar { 163 | position: relative; 164 | width: 200px; 165 | height: 5px; 166 | border-radius: 3px; 167 | background: #005C76; 168 | } 169 | #loading .complete { 170 | width: 0px; 171 | height: 5px; 172 | border-radius: 3px; 173 | background: #fff; 174 | } 175 | 176 | header { 177 | position: fixed; 178 | top: 0; 179 | left: 0; 180 | padding: 0; 181 | margin: 0; 182 | height: 50px; 183 | min-height: 50px; 184 | max-height: 50px; 185 | line-height: normal; 186 | border-bottom: 5px solid #33b5e5; 187 | box-shadow: 0px 1px 3px #555; 188 | z-index: 5; 189 | } 190 | #app_name { 191 | line-height: normal; 192 | margin-top: 6px; 193 | position: absolute; 194 | left: 225px; 195 | height: 40px; 196 | text-align: left; 197 | cursor: pointer; 198 | } 199 | header > div { 200 | position: relative; 201 | height: 50px; 202 | } 203 | 204 | #mapscreen { 205 | margin-top: 55px; 206 | } 207 | 208 | #map, 209 | #main { 210 | position: relative; 211 | z-index: 1; 212 | } 213 | #map { 214 | margin: 0; 215 | padding: 0; 216 | height: 100%; 217 | width: 100%; 218 | } 219 | #main { 220 | float: left; 221 | overflow: hidden; 222 | -webkit-touch-callout: none; 223 | -webkit-user-select: none; 224 | -khtml-user-select: none; 225 | -moz-user-select: none; 226 | -ms-user-select: none; 227 | user-select: none; 228 | } 229 | 230 | header .search { 231 | float: left; 232 | width: 190px; 233 | margin-top: 10px; 234 | } 235 | 236 | header .search form { 237 | position: relative; 238 | } 239 | header .search form input { 240 | position: absolute; 241 | border-radius: 100px; 242 | } 243 | header .search form input[type='text'] { 244 | left: 0; 245 | padding-left: 10px; 246 | padding-right: 35px; 247 | width: 143px; 248 | } 249 | header .search form input[type='submit'] { 250 | right: 0; 251 | } 252 | 253 | .nav { 254 | list-style: none outside none; 255 | margin: 0; 256 | padding: 0; 257 | height: 40px; 258 | display: block; 259 | min-width: 40px; 260 | width: auto; 261 | float: right; 262 | margin: 5px 0px; 263 | } 264 | 265 | #locate-me { 266 | position: absolute; 267 | left: 195px; 268 | top: 12px; 269 | font-size: 25px; 270 | height: 25px; 271 | width: 25px; 272 | line-height: 25px; 273 | cursor: pointer; 274 | } 275 | 276 | .nav > li { 277 | margin: 0; 278 | padding: 0 5px;; 279 | float: left; 280 | height: 40px; 281 | width: 35px; 282 | border-right: 1px solid #33b5e5; 283 | cursor: pointer; 284 | color: #fff; 285 | line-height: 45px; 286 | font-size: 35px; 287 | } 288 | .nav > li.last { border: 0; } 289 | .nav > li:active { background-color: #33b5e5; } 290 | .nav > li:hover { border-bottom: 5px solid #fff; } 291 | 292 | 293 | #main .data { 294 | cursor: url('../img/openhand.cur'), row-resize; 295 | } 296 | 297 | #main.drag, 298 | #main.drag .data, 299 | #main.drag .header { 300 | cursor: url('../img/closedhand.cur'), row-resize; 301 | } 302 | 303 | #main .header { 304 | height: 20px; 305 | padding: 10px; 306 | padding-right: 3px; 307 | padding-left: 5px; 308 | border-bottom: 1px solid #33b5e5; 309 | position: relative; 310 | z-index: 51; 311 | cursor: pointer; 312 | background-color: #fff; 313 | border-left: 5px solid #fff; 314 | } 315 | #main .row.selected { 316 | border-left: 5px solid #00A3D3; 317 | } 318 | #main .row:hover .header { 319 | border-left: 5px solid #00A3D3; 320 | } 321 | 322 | #main .row:hover .data { 323 | border-left: 5px solid #ccc; 324 | } 325 | 326 | #main .header.empty { 327 | text-align: center; 328 | width: 100%; 329 | height: 120px; 330 | line-height: 100px; 331 | border: 0; 332 | } 333 | #main .header.empty:hover { 334 | border:0; 335 | } 336 | #main .header span { 337 | overflow: hidden; 338 | display: block; 339 | width: 90%; 340 | float: left; 341 | white-space: nowrap; 342 | } 343 | 344 | .header .arrow { 345 | font-weight: normal; 346 | float: right; 347 | color: #aaa; 348 | display: block; 349 | height: 14px; 350 | width: 16px; 351 | line-height: 11px; 352 | font-size: 16px; 353 | margin-top: 4px; 354 | -webkit-transition: 0.2s linear; 355 | -moz-transition: 0.2s linear; 356 | -ms-transition: 0.2s linear; 357 | -o-transition: 0.2s linear; 358 | transition: 0.2s linear; 359 | -webkit-transform-origin: center; 360 | -moz-transform-origin: center; 361 | -ms-transform-origin: center; 362 | -o-transform-origin: center; 363 | transform-origin: center; 364 | } 365 | 366 | 367 | .row .header .arrow:after { 368 | content: "▲"; 369 | } 370 | .row:hover .arrow { 371 | -webkit-transform: rotate(-90deg); 372 | -moz-transform: rotate(-90deg); 373 | -ms-transform: rotate(-90deg); 374 | -o-transform: rotate(-90deg); 375 | transform: rotate(-90deg); 376 | color: #00a3d3; 377 | } 378 | .row.active .header .arrow { 379 | -webkit-transform: rotate(-180deg); 380 | -moz-transform: rotate(-180deg); 381 | -ms-transform: rotate(-180deg); 382 | -o-transform: rotate(-180deg); 383 | transform: rotate(-180deg); 384 | } 385 | 386 | #main .row { 387 | background-color: #f4f4f4; 388 | margin: 0; 389 | padding: 0; 390 | position: relative; 391 | } 392 | #main .row .header { 393 | } 394 | #main .row .data { 395 | display: none; 396 | width: 100%; 397 | border-left: 5px solid #F4F4F4; 398 | } 399 | 400 | #main .row .icon-target:before { 401 | display: none; 402 | } 403 | #main .row.follow .icon-target:before { 404 | display: inline-block; 405 | } 406 | #main .row.active .data { 407 | display: inline-block; 408 | min-height: 190px; 409 | } 410 | #main .row .data .left, 411 | #main .row .data .right { 412 | position: relative; 413 | z-index: 4; 414 | } 415 | #main .row .data .vbutton { 416 | position: absolute; 417 | background-color: #fff; 418 | width: 30px !important; 419 | right: 5px; 420 | top: 150px; 421 | padding-left: 3px; 422 | padding-right: 3px; 423 | font-size: 10px; 424 | border-radius: 5px; 425 | border: 1px solid #ccc; 426 | cursor: pointer; 427 | z-index: 5; 428 | text-align: center; 429 | } 430 | 431 | #main .row .data .sbutton { 432 | position: absolute; 433 | background-color: #fff; 434 | width: 30px !important; 435 | right: 5px; 436 | top: 170px; 437 | padding-left: 3px; 438 | padding-right: 3px; 439 | font-size: 10px; 440 | border-radius: 5px; 441 | border: 1px solid #ccc; 442 | cursor: pointer; 443 | z-index: 5; 444 | text-align: center; 445 | } 446 | 447 | #main .row .data .sbutton.active { 448 | background-color: #33b5e5; 449 | border: 1px solid #33b5e5; 450 | color: #fff; 451 | } 452 | 453 | #main .row .data .vbutton.active { 454 | background-color: #33b5e5; 455 | border: 1px solid #33b5e5; 456 | color: #fff; 457 | } 458 | 459 | #main .row .data .vbutton:hover { 460 | border: 1px solid #5E5E5E; 461 | } 462 | 463 | #main .row .data img { 464 | position: absolute; 465 | z-index: 2; 466 | right: 35%; 467 | opacity: 0.6; 468 | width: 46px; 469 | height: 84px; 470 | -webkit-transition: 0.2s ease; 471 | -moz-transition: 0.2s ease; 472 | -ms-transition: 0.2s ease; 473 | -o-transition: 0.2s ease; 474 | transition: 0.2s ease; 475 | } 476 | #main .row .data img.car { 477 | width: 55px; 478 | height: 25px; 479 | } 480 | #main .row:hover .data img { 481 | opacity: 0.8; 482 | } 483 | #main .row.follow .data img { 484 | opacity: 1.0; 485 | } 486 | #main .row .header .graph { 487 | position: absolute; 488 | bottom: -1px; 489 | right: 22px; 490 | width: 60px; 491 | height: 40px; 492 | z-index: 1; 493 | } 494 | #main .row .data dt > i { 495 | font-size: 12px; 496 | } 497 | #main .row .data a { 498 | text-decoration: none; 499 | color: #00A3D3; 500 | } 501 | #main .data dl > dt.receivers { 502 | font-size: 12px; 503 | font-weight: normal; 504 | } 505 | 506 | .flatpage { 507 | margin-top: 55px; 508 | overflow: auto; 509 | position: absolute; 510 | width: 100%; 511 | z-index: 100; 512 | background: #fff; 513 | } 514 | 515 | .topanel { 516 | float: right; 517 | position: relative; 518 | width: auto; 519 | padding: 0; 520 | padding-left: 10px; 521 | padding-right: 20px; 522 | box-shadow: 2px 0px 8px 0px #555; 523 | overflow-x: hidden; 524 | z-index: 3; 525 | } 526 | 527 | .flatpage p { 528 | display: block; 529 | text-align: justify; 530 | margin-bottom: 15px; 531 | } 532 | #cc_callsign { 533 | text-align: right; 534 | padding: 4px 10px; 535 | margin: 0; 536 | } 537 | .slimContainer { 538 | position: relative; 539 | margin: 20px auto; 540 | width: 290px; 541 | } 542 | .slimContainer hr { 543 | margin-bottom: 10px; 544 | } 545 | .slimContainer .row { 546 | width: 280px; 547 | display: block; 548 | margin: 5px; 549 | vertical-align: middle; 550 | position: relative; 551 | } 552 | .slimContainer .row.info { 553 | margin-top: 10px; 554 | } 555 | .slimContainer .row > span { 556 | float: left; 557 | } 558 | .slimContainer .row.option > span { 559 | width: 200px; 560 | } 561 | .slimContainer .row.option > span { 562 | line-height: 30px; 563 | } 564 | .slimContainer .row > span.r { 565 | float: right; 566 | } 567 | 568 | /* iOS styled switch buttons 569 | */ 570 | .switch { 571 | position: absolute; 572 | right: 0px; 573 | height: 28px; 574 | width: 77px; 575 | border: 1px solid #979797; 576 | border-radius: 20px; 577 | box-shadow: inset 0 1px 3px #BABABA, inset 0 12px 3px 2px rgba(232, 232, 232, 0.5); 578 | cursor: pointer; 579 | overflow: hidden; 580 | } 581 | .switch input[type=checkbox] { 582 | display: none; 583 | } 584 | .switch:before { 585 | content: ""; 586 | display: block; 587 | height: 28px; 588 | width: 0px; 589 | position: absolute; 590 | border-radius: 20px; 591 | -webkit-box-shadow: inset 0 1px 2px #33B5E5, inset 0 12px 3px 2px #00A3D3; 592 | box-shadow: inset 0 1px 2px #33B5E5, inset 0 12px 3px 2px #00A3D3; 593 | background-color: #33B5E5; 594 | } 595 | .switch.on:before { 596 | width: 77px; 597 | } 598 | .switch > .thumb { 599 | display: block; 600 | width: 26px; 601 | height: 26px; 602 | position: relative; 603 | top: 0; 604 | z-index: 51; 605 | border: solid 1px #919191; 606 | border-radius: 28px; 607 | box-shadow: inset 0 2px 1px white, inset 0 -2px 1px white; 608 | background-color: #CECECE; 609 | background-image: -webkit-linear-gradient(#CECECE, #FBFBFB); 610 | background-image: -moz-linear-gradient(#CECECE, #FBFBFB); 611 | background-image: -o-linear-gradient(#CECECE, #FBFBFB); 612 | -webkit-transition: all 0.125s ease-in-out; 613 | -moz-transition: all 0.125s ease-in-out; 614 | -ms-transition: all 0.125s ease-in-out; 615 | -o-transition: all 0.125s ease-in-out; 616 | transition: all 0.125s ease-in-out; 617 | -webkit-transform: translate3d(0,0,0); 618 | -moz-transform: translateX(0px); 619 | -ms-transform: translateX(0px); 620 | -o-transform: translateX(0px); 621 | transform: translateX(0px); 622 | } 623 | .switch.on > .thumb { 624 | -webkit-transform: translate3d(49px,0,0); 625 | -moz-transform: translateX(49px); 626 | -ms-transform: translateX(49px); 627 | -o-transform: translateX(49px); 628 | transform: translateX(49px); 629 | } 630 | .switch:hover > .thumb { 631 | box-shadow: inset 0 2px 1px #fff, inset 0 -2px 1px #fff; 632 | background-image: none; 633 | } 634 | .switch > .thumb:before { 635 | font-weight: bold; 636 | font-size: 14px; 637 | color: #fff; 638 | content: "On"; 639 | display: block; 640 | height: 14px; 641 | width: 14px; 642 | border: none; 643 | position: absolute; 644 | top: 3px; 645 | left: -30px; 646 | } 647 | .switch > .thumb:after { 648 | font-weight: bold; 649 | font-size: 14px; 650 | content: "Off"; 651 | display: block; 652 | height: 14px; 653 | width: 14px; 654 | position: absolute; 655 | right: -28px; 656 | top: 3px; 657 | } 658 | 659 | .switchyn { 660 | position: absolute; 661 | right: 0px; 662 | height: 28px; 663 | width: 77px; 664 | border: 1px solid #979797; 665 | border-radius: 20px; 666 | box-shadow: inset 0 1px 3px #BABABA, inset 0 12px 3px 2px rgba(232, 232, 232, 0.5); 667 | cursor: pointer; 668 | overflow: hidden; 669 | } 670 | .switchyn input[type=checkbox] { 671 | display: none; 672 | } 673 | .switchyn:before { 674 | content: ""; 675 | display: block; 676 | height: 28px; 677 | width: 0px; 678 | position: absolute; 679 | border-radius: 20px; 680 | -webkit-box-shadow: inset 0 1px 2px #33B5E5, inset 0 12px 3px 2px #00A3D3; 681 | box-shadow: inset 0 1px 2px #33B5E5, inset 0 12px 3px 2px #00A3D3; 682 | background-color: #33B5E5; 683 | } 684 | .switchyn.on:before { 685 | width: 77px; 686 | } 687 | .switchyn > .thumb { 688 | display: block; 689 | width: 26px; 690 | height: 26px; 691 | position: relative; 692 | top: 0; 693 | z-index: 51; 694 | border: solid 1px #919191; 695 | border-radius: 28px; 696 | box-shadow: inset 0 2px 1px white, inset 0 -2px 1px white; 697 | background-color: #CECECE; 698 | background-image: -webkit-linear-gradient(#CECECE, #FBFBFB); 699 | background-image: -moz-linear-gradient(#CECECE, #FBFBFB); 700 | background-image: -o-linear-gradient(#CECECE, #FBFBFB); 701 | -webkit-transition: all 0.125s ease-in-out; 702 | -moz-transition: all 0.125s ease-in-out; 703 | -ms-transition: all 0.125s ease-in-out; 704 | -o-transition: all 0.125s ease-in-out; 705 | transition: all 0.125s ease-in-out; 706 | -webkit-transform: translate3d(0,0,0); 707 | -moz-transform: translateX(0px); 708 | -ms-transform: translateX(0px); 709 | -o-transform: translateX(0px); 710 | transform: translateX(0px); 711 | } 712 | .switchyn.on > .thumb { 713 | -webkit-transform: translate3d(49px,0,0); 714 | -moz-transform: translateX(49px); 715 | -ms-transform: translateX(49px); 716 | -o-transform: translateX(49px); 717 | transform: translateX(49px); 718 | } 719 | .switchyn:hover > .thumb { 720 | box-shadow: inset 0 2px 1px #fff, inset 0 -2px 1px #fff; 721 | background-image: none; 722 | } 723 | .switchyn > .thumb:before { 724 | font-weight: bold; 725 | font-size: 14px; 726 | color: #fff; 727 | content: "Yes"; 728 | display: block; 729 | height: 14px; 730 | width: 14px; 731 | border: none; 732 | position: absolute; 733 | top: 3px; 734 | left: -30px; 735 | } 736 | .switchyn > .thumb:after { 737 | font-weight: bold; 738 | font-size: 14px; 739 | content: "No"; 740 | display: block; 741 | height: 14px; 742 | width: 14px; 743 | position: absolute; 744 | right: -28px; 745 | top: 3px; 746 | } 747 | 748 | #telemetry_graph { 749 | display: none; 750 | } 751 | 752 | .nav .home { 753 | display: none; 754 | } 755 | 756 | .leaflet-tooltip.serialtooltip { 757 | background-color: transparent; 758 | border: 0px; 759 | box-shadow: none; 760 | font-size: 12px; 761 | font-weight: bold; 762 | font-family: 'Roboto'; 763 | text-shadow: -1px 0 white, 0 1px white, 1px 0 white, 0 -1px white; 764 | } 765 | 766 | .gold { 767 | filter: drop-shadow(1px 0px 0 gold) drop-shadow(0px 1px 0 gold) drop-shadow(-1px -0px 0 gold) drop-shadow(-0px -1px 0 gold); 768 | } 769 | .silver { 770 | filter: drop-shadow(1px 0px 0 grey) drop-shadow(0px 1px 0 grey) drop-shadow(-1px -0px 0 grey) drop-shadow(-0px -1px 0 grey); 771 | } 772 | .bronze { 773 | filter: drop-shadow(1px 0px 0 chocolate) drop-shadow(0px 1px 0 chocolate) drop-shadow(-1px -0px 0 chocolate) drop-shadow(-0px -1px 0 chocolate); 774 | } 775 | .white { 776 | filter: drop-shadow(1px 0px 0 white) drop-shadow(0px 1px 0 white) drop-shadow(-1px -0px 0 white) drop-shadow(-0px -1px 0 white); 777 | } 778 | 779 | .leaflet-marker-icon .number{ 780 | position: relative; 781 | top: -41px; 782 | font-size: 12px; 783 | width: 25px; 784 | text-align: center; 785 | } 786 | 787 | .leaflet-div-icon { 788 | background: transparent !important; 789 | border: none !important; 790 | } 791 | 792 | /* Tooltip container */ 793 | .tooltip { 794 | position: relative; 795 | display: inline-block; 796 | } 797 | 798 | /* Tooltip text */ 799 | .tooltip .tooltiptext { 800 | visibility: hidden; 801 | width: 120px; 802 | background-color: black; 803 | color: #fff; 804 | text-align: center; 805 | padding: 5px 0; 806 | border-radius: 6px; 807 | text-transform: none !important; 808 | 809 | /* Position the tooltip text - see examples below! */ 810 | position: absolute; 811 | z-index: 1; 812 | } 813 | 814 | /* Show the tooltip text when you mouse over the tooltip container */ 815 | .tooltip:hover .tooltiptext { 816 | visibility: visible; 817 | } 818 | 819 | /* workaround for subpixel lines https://github.com/Leaflet/Leaflet/issues/3575#issuecomment-688644225 */ 820 | .leaflet-tile-container img { 821 | width: 256.5px !important; 822 | height: 256.5px !important; 823 | } 824 | 825 | @media only screen and (min-width: 900px) { 826 | } 827 | 828 | @media only screen and (max-width: 600px) { 829 | #app_name { 830 | left: 150px; 831 | } 832 | #locate-me { 833 | left: 120px; 834 | } 835 | header .search { 836 | width: 110px; 837 | } 838 | header .search form input[type='text'] { 839 | width: 63px; 840 | } 841 | } 842 | 843 | @media only screen and (max-width: 500px) { 844 | #app_name { 845 | left: 30px; 846 | } 847 | #locate-me { 848 | left: 0px; 849 | } 850 | header .search { 851 | display: none; 852 | } 853 | } 854 | 855 | @media only screen and (min-width: 481px) { 856 | .portrait { display: none; } 857 | .landscape { display: block; } 858 | #telemetry_graph { 859 | display: block; 860 | float: right; 861 | height: 200px; 862 | width: 280px; 863 | background: #fff; 864 | position: relative; 865 | z-index: 2; 866 | } 867 | #telemetry_graph .holder { 868 | border-left: 1px solid #ddd; 869 | } 870 | #telemetry_graph .graph_label { 871 | position: absolute; 872 | top: -26px; 873 | left: 0px; 874 | height: 20px; 875 | padding: 3px 5px; 876 | background: #00a3d3; 877 | z-index: 20; 878 | font-weight: bold; 879 | font-size: 11px; 880 | color: #fff; 881 | border-radius: 0px 5px 0 0; 882 | box-shadow: 1px -1px 5px rgba(0, 0, 0, 0.2); 883 | cursor: pointer; 884 | } 885 | #map { 886 | height: 245px; 887 | width: 280px; 888 | } 889 | #main { 890 | float: left; 891 | height: 245px; 892 | width: 199px; 893 | margin-top: 55px; 894 | box-shadow: -2px 0px 6px 0px #555; 895 | } 896 | #main .data { 897 | height: 100%; 898 | padding-bottom: 5px; 899 | } 900 | #main .data .left { 901 | float: left; 902 | width: 80%; 903 | padding-left: 5px; 904 | } 905 | #main .data dl > dt { 906 | color: #000; 907 | line-height: 11px; 908 | margin-top: 5px; 909 | font-weight: bold; 910 | font-size: 14px; 911 | } 912 | #main .data dl > dd { 913 | padding: 0; 914 | margin: 0; 915 | text-transform:uppercase; 916 | line-height: 11px; 917 | font-size: 11px; 918 | } 919 | #main .row .data img { 920 | right: 5%; 921 | top: 50px; 922 | } 923 | } 924 | @media only screen and (max-width: 480px) { 925 | .portrait { display: block; } 926 | .landscape { display: none; } 927 | #map{ 928 | height: 225px; 929 | } 930 | #main { 931 | height: 150px; 932 | } 933 | #main .data { 934 | min-height: 108px; 935 | } 936 | #main .data .left { 937 | float: left; 938 | width: 55%; 939 | padding-left: 5px; 940 | } 941 | #main .data .right { 942 | float: right; 943 | padding-right: 10px; 944 | width: 30%; 945 | } 946 | #main .data dl > dt { 947 | color: #000; 948 | line-height: 11px; 949 | margin-top: 7px; 950 | font-weight: bold; 951 | font-size: 14px; 952 | } 953 | #main .data dl > dd { 954 | padding: 0; 955 | margin: 0; 956 | text-transform:uppercase; 957 | line-height: 11px; 958 | font-size: 11px; 959 | } 960 | #main .row .header .graph { 961 | width: 180px; 962 | height: 40px; 963 | } 964 | #locate-me { 965 | display: none; 966 | } 967 | #app_name { 968 | left: 0px; 969 | } 970 | } 971 | 972 | /* login button */ 973 | #login_url { 974 | display: block; 975 | background-color: #33b5e5; 976 | color: white; 977 | text-decoration: none; 978 | font-weight: bold; 979 | text-align: center; 980 | padding: 10px; 981 | } 982 | /* predictor settings pane */ 983 | .prediction_settings { 984 | background-image: url("../glyphs/icon-balloon.svg"); 985 | background-repeat: no-repeat; 986 | background-size: 50%; 987 | background-position: center; 988 | } --------------------------------------------------------------------------------