├── .gitignore ├── website ├── sample_csv_file.png ├── trajVis_demo_01.gif ├── trajVis_demo_02.gif └── simulated_data.csv ├── postcss.config.js ├── src ├── index.js ├── css │ ├── icons │ │ ├── pause.svg │ │ ├── record.svg │ │ ├── play.svg │ │ ├── replay.svg │ │ ├── graph.svg │ │ ├── upload.svg │ │ ├── map.svg │ │ ├── track.svg │ │ ├── window.svg │ │ ├── help.svg │ │ ├── marker.svg │ │ ├── ioe_logo_small.svg │ │ └── ioe_logo.svg │ └── app.css ├── layers.js ├── graph.js ├── slider.js └── app.js ├── .babelrc ├── CONTRIBUTING.md ├── public ├── index.html └── collared_peccary.svg ├── webpack.config.js ├── LICENSE ├── package.json ├── README.md └── CODE_OF_CONDUCT.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .env 4 | -------------------------------------------------------------------------------- /website/sample_csv_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/internetofelephants/trajvis/HEAD/website/sample_csv_file.png -------------------------------------------------------------------------------- /website/trajVis_demo_01.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/internetofelephants/trajvis/HEAD/website/trajVis_demo_01.gif -------------------------------------------------------------------------------- /website/trajVis_demo_02.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/internetofelephants/trajvis/HEAD/website/trajVis_demo_02.gif -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | 'postcss-preset-env', 4 | 'postcss-inline-svg' 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './app.js'; 4 | ReactDOM.render(, document.getElementById('content')); 5 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/env", 4 | "@babel/preset-react" 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-proposal-class-properties", 8 | "@babel/plugin-transform-runtime" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src/css/icons/pause.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/css/icons/record.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/css/icons/play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to trajVis 2 | 3 | Thank you for your interest - please check back later, as we will populate this with details soon. 4 | 5 | In the meantime, please report bugs or suggest features by opening an issue, or email raff@internetofelephants.com with your query. 6 | -------------------------------------------------------------------------------- /src/css/icons/replay.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/css/icons/graph.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/css/icons/upload.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/css/icons/map.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/css/icons/track.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | trajVis 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/css/icons/window.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/css/icons/help.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/css/icons/marker.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const TerserPlugin = require('terser-webpack-plugin'); 4 | const dotenv = require('dotenv').config( { 5 | path: path.join(__dirname, '.env') 6 | } ); 7 | 8 | module.exports = { 9 | entry: './src/index.js', 10 | mode: 'none', 11 | optimization: { 12 | minimize: true, 13 | minimizer: [new TerserPlugin()], 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.(js|jsx)$/, 19 | exclude: /(node_modules|bower_components)/, 20 | loader: 'babel-loader', 21 | options: { presets: ['@babel/env'] } 22 | }, 23 | { 24 | test: /\.css$/i, 25 | use: ['style-loader', 'css-loader', 'postcss-loader'] 26 | }, 27 | { 28 | test: /\.js$/, 29 | enforce: 'pre', 30 | use: 'source-map-loader' 31 | } 32 | ] 33 | }, 34 | resolve: { 35 | extensions: ['*', '.js', '.jsx'], 36 | fallback: { 'util': false } 37 | }, 38 | output: { 39 | path: path.resolve(__dirname, 'dist/'), 40 | publicPath: '/dist/', 41 | filename: 'bundle.js' 42 | }, 43 | devServer: { 44 | static: { 45 | directory: path.join(__dirname, 'public/') 46 | }, 47 | port: 3000, 48 | hot: 'only', 49 | devMiddleware: { 50 | publicPath: 'http://localhost:3000/dist/' 51 | } 52 | }, 53 | plugins: [ 54 | new webpack.DefinePlugin( { 'process.env': JSON.stringify(process.env) } ) 55 | ] 56 | }; 57 | -------------------------------------------------------------------------------- /src/layers.js: -------------------------------------------------------------------------------- 1 | import { TripsLayer, ScatterplotLayer } from 'deck.gl'; 2 | import { DataFilterExtension } from '@deck.gl/extensions'; 3 | 4 | export default function renderLayers(props) { 5 | const {trackData, trackTrail, tsRange, trackOpacity, trackVisible, markerData, changeProps, markerOpacity, markerVisible} = props; 6 | return [ 7 | new TripsLayer({ 8 | id: 'track-layer', 9 | data: trackData, 10 | trailLength: trackTrail, 11 | currentTime: tsRange[1], 12 | getPath: d => d.coordinates, 13 | getTimestamps: d => d.timestamps, 14 | getColor: d => d.colour, 15 | opacity: trackOpacity, 16 | widthUnits: 'pixels', 17 | getWidth: changeProps ? d => d.width : d => d.width, 18 | updateTriggers: { 19 | getWidth: [changeProps] 20 | }, 21 | capRounded: true, 22 | jointRounded: true, 23 | shadowEnabled: false, 24 | visible: trackVisible 25 | }), 26 | new ScatterplotLayer({ 27 | id: 'marker-layer', 28 | data: markerData, 29 | getPosition: d => d.coordinates, 30 | radiusUnits: 'pixels', 31 | getRadius: changeProps ? d => d.radius : d => d.radius, 32 | updateTriggers: { 33 | getRadius: [changeProps] 34 | }, 35 | filled: true, 36 | stroked: false, 37 | opacity: markerOpacity, 38 | getFillColor: d => d.colour, 39 | getFilterValue: d => [d.timestamps], 40 | filterRange: tsRange, 41 | extensions: [new DataFilterExtension({filterSize: 1})], 42 | pickable: true, 43 | visible: markerVisible 44 | }) 45 | ]; 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, Internet of Elephants 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trajvis", 3 | "version": "1.1.0", 4 | "description": "Visualise animal movement data", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack serve --mode development", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/internetofelephants/trajvis.git" 13 | }, 14 | "author": { 15 | "name": "Raff Mares" 16 | }, 17 | "license": "BSD-3-Clause", 18 | "devDependencies": { 19 | "@babel/cli": "^7.15.7", 20 | "@babel/core": "^7.15.5", 21 | "@babel/plugin-proposal-class-properties": "^7.13.0", 22 | "@babel/plugin-transform-runtime": "^7.15.0", 23 | "@babel/preset-env": "^7.15.6", 24 | "@babel/preset-react": "^7.13.13", 25 | "@types/react": "^17.0.24", 26 | "babel-loader": "^8.2.2", 27 | "css-loader": "^6.3.0", 28 | "dotenv": "^10.0.0", 29 | "postcss": "^8.3.8", 30 | "postcss-inline-svg": "^5.0.0", 31 | "postcss-loader": "^6.1.1", 32 | "postcss-preset-env": "^6.7.0", 33 | "source-map-loader": "^3.0.0", 34 | "style-loader": "^3.3.0", 35 | "terser-webpack-plugin": "^5.2.4", 36 | "webpack": "^5.54.0", 37 | "webpack-cli": "^4.8.0", 38 | "webpack-dev-server": "^4.3.0" 39 | }, 40 | "dependencies": { 41 | "@babel/runtime": "^7.15.4", 42 | "dayjs": "^1.10.7", 43 | "deck.gl": "^8.5.10", 44 | "fix-webm-duration": "^1.0.3", 45 | "papaparse": "^5.3.0", 46 | "react": "^17.0.2", 47 | "react-dom": "^17.0.2", 48 | "react-hot-loader": "^4.13.0", 49 | "react-map-gl": "^6.1.17", 50 | "react-range": "^1.8.11", 51 | "react-vis": "^1.11.7", 52 | "simplify-js": "^1.2.4" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/graph.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../node_modules/react-vis/dist/style.css'; 3 | import { FlexibleWidthXYPlot, XAxis, YAxis, HorizontalGridLines, LineSeries } from 'react-vis'; 4 | 5 | export default function Graph( {graphTitle, graphData, graphSeriesOpacity, maxTimeVal, graphMaxY, tsRange} ) { 6 | return ( 7 |
8 |
9 |
{graphTitle}
10 |
11 | 17 | 21 | 25 | Math.round(d / 1000)} 29 | tickTotal={4} 30 | tickSizeInner={0} 31 | tickSizeOuter={0} 32 | /> 33 | {graphData.map((props, i) => ( 34 | 39 | ))} 40 | 46 | 52 | 53 |
54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # trajVis 2 | Visualise, animate and create videos of animal movement data from GPS tags. 3 | 4 | https://internetofelephants.github.io/trajvis 5 | 6 | ## Features 7 | - Visualise data as lines and points (pickable) 8 | - Switch individuals on and off 9 | - Playback controls and range slider 10 | - Record videos (via the Screen Capture API) 11 | - Graph showing cumulative distance 12 | - Base maps from Mapbox (light, dark and satellite) 13 | 14 | ![GIF of trajVis demo 01](https://github.com/internetofelephants/trajvis/blob/main/website/trajVis_demo_01.gif) 15 | Four spider monkeys (_Ateles geoffroyi_) on Barro Colorado Island, Panama. Data courtesy of Prof Meg Crofoot (University of Konstanz) and colleagues. 16 | 17 | ## Usage 18 | To begin, select a csv file containing movement data for a single or multiple individuals. The file must contain the following headers: 19 | - species 20 | - animal_id 21 | - timestamp (date and time as YYYY-MM-DD HH:MM:SS) 22 | - lon (longitude in decimal degrees) 23 | - lat (latitude in decimal degrees) 24 | - alt (altitude in meters, optional) 25 | 26 | ![example of csv data](https://github.com/internetofelephants/trajvis/blob/main/website/sample_csv_file.png) 27 | 28 | You can download a sample file with simulated data [here](https://raw.githubusercontent.com/internetofelephants/trajvis/main/website/simulated_data.csv). 29 | 30 | ![GIF of trajVis demo 02](https://github.com/internetofelephants/trajvis/blob/main/website/trajVis_demo_02.gif) 31 | 32 | ## Roadmap 33 | - add a graph for altitude 34 | - zoomable graph 35 | - option to add polygons as shapefile or geojson 36 | - add option to create popups 37 | - hide/minimize UI during recording 38 | - allow sharing with data via link 39 | - add 3D support 40 | 41 | ## Support 42 | For help with usage, email raff@internetofelephants.com 43 | 44 | For reporting bugs, please open an issue. 45 | 46 | ## Contributing 47 | Contributions are welcome! Please open an issue first or get in touch to discuss what you would like to change or how you would like to contribute. 48 | -------------------------------------------------------------------------------- /public/collared_peccary.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 42 | 43 | -------------------------------------------------------------------------------- /src/slider.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Range, getTrackBackground } from 'react-range'; 3 | 4 | export default function Slider(props) { 5 | const {sliderSteps, sliderValue, onChange, sliderTicks} = props; 6 | return ( 7 |
8 | ( 17 |
27 | {sliderTicks.map((p, index) => ( 28 |
30 | ))} 31 |
46 | {children} 47 |
48 |
49 | )} 50 | renderThumb={({ props, isDragged }) => ( 51 |
65 |
73 |
74 | )} 75 | /> 76 |
77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Attribution 60 | 61 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 62 | version 2.0, available at 63 | [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. 64 | 65 | Community Impact Guidelines were inspired by 66 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 67 | 68 | For answers to common questions about this code of conduct, see the FAQ at 69 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available 70 | at [https://www.contributor-covenant.org/translations][translations]. 71 | 72 | [homepage]: https://www.contributor-covenant.org 73 | [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html 74 | [Mozilla CoC]: https://github.com/mozilla/diversity 75 | [FAQ]: https://www.contributor-covenant.org/faq 76 | [translations]: https://www.contributor-covenant.org/translations 77 | -------------------------------------------------------------------------------- /src/css/icons/ioe_logo_small.svg: -------------------------------------------------------------------------------- 1 | 4 | 81 | 82 | -------------------------------------------------------------------------------- /src/css/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; padding: 0; 3 | font-family: monospace, monospace; 4 | font-size: 1em; 5 | overflow: hidden; 6 | /* cursor: url('../data/green_dot.png'), auto; */ 7 | } 8 | 9 | #content { 10 | position: absolute; 11 | width: 100vw; 12 | height: 100vh; 13 | } 14 | 15 | #map { 16 | width: 100%; 17 | height: 100%; 18 | } 19 | 20 | .scaleBar { 21 | position: absolute; 22 | top: 20px; 23 | left: 20px; 24 | } 25 | 26 | .zoomButton { 27 | position: absolute; 28 | top: 60px; 29 | left: 20px; 30 | } 31 | 32 | .mapboxgl-ctrl.mapboxgl-ctrl-attrib { 33 | background-color: Transparent; 34 | color: rgb(80, 80, 80); 35 | text-shadow: 0 0 20px rgb(255, 255, 255); 36 | } 37 | .mapboxgl-ctrl-attrib-inner a { 38 | color: inherit; 39 | text-shadow: inherit; 40 | font-family: 'Courier New', monospace; 41 | } 42 | 43 | .blockScreen { 44 | z-index: 1; 45 | position: absolute; 46 | top: 0; 47 | bottom: 0; 48 | width: 100%; 49 | height: 100%; 50 | background-color: Transparent; 51 | } 52 | 53 | .counter { 54 | position: absolute; 55 | top: 0; 56 | left: 0; 57 | width: 100%; 58 | height: auto; 59 | text-align: center; 60 | font-family: 'Courier New', monospace; 61 | font-size: 148%; 62 | font-weight: bold; 63 | } 64 | .counter p { 65 | margin-bottom: 0; 66 | } 67 | 68 | .dataLoadingProgress { 69 | position: absolute; 70 | top: 0; 71 | right: 0; 72 | width: auto; 73 | height: auto; 74 | padding: 10px; 75 | font-family: 'Courier New', monospace; 76 | } 77 | .dataLoadingProgress p { 78 | margin: 0; 79 | } 80 | @keyframes blink { 81 | 0% { opacity: 0; } 82 | 20% { opacity: 1; } 83 | 100% { opacity: 0; } 84 | } 85 | .readingData span { 86 | animation-name: blink; 87 | animation-duration: 1.4s; 88 | animation-fill-mode: both; 89 | } 90 | .readingData span:nth-child(2) { 91 | animation-delay: .2s; 92 | } 93 | .readingData span:nth-child(3) { 94 | animation-delay: .4s; 95 | } 96 | 97 | .centerDiv { 98 | flex-wrap: wrap; 99 | position: absolute; 100 | top: 20%; 101 | width: 100%; 102 | height: auto; 103 | justify-content: center; 104 | font-family: 'Courier New', monospace; 105 | } 106 | .welcomeBox { 107 | width: 56%; 108 | height: 100%; 109 | padding: 20px 24px 60px 24px; 110 | background: rgba(255, 255, 255, 0.9); 111 | border-radius: 4px; 112 | box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.2); 113 | } 114 | .fileInput { 115 | display: flex; 116 | position: relative; 117 | top: 20px; 118 | width: 100%; 119 | justify-content: center; 120 | } 121 | input[type='file'] { 122 | display: none; 123 | } 124 | .fileInputTxt { 125 | color: rgb(0, 0, 238); 126 | text-decoration: underline; 127 | cursor: pointer; 128 | } 129 | .fileInputBtnBig { 130 | width: 50px; 131 | height: 50px; 132 | border-radius: 4px; 133 | background: rgb(220, 220, 220); 134 | background-position: center center; 135 | background-repeat: no-repeat; 136 | background-size: 32px; 137 | background-image: svg-load('icons/upload.svg', fill: rgb(0, 0, 0)); 138 | box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.4); 139 | cursor: pointer; 140 | } 141 | 142 | .ioeLogoBig { 143 | position: absolute; 144 | bottom: 0; 145 | right: 22%; 146 | width: 120px; 147 | height: 60px; 148 | } 149 | .ioeLogoBig a { 150 | position: absolute; 151 | width: 100%; 152 | height: 100%; 153 | background-position: center center; 154 | background-repeat: no-repeat; 155 | background-size: 100%; 156 | background-image: svg-load('icons/ioe_logo.svg', fill: rgb(0, 0, 0)); 157 | } 158 | 159 | .button { 160 | width: 52px; 161 | height: 30px; 162 | padding: 4px; 163 | margin: 4px 8px; 164 | border: none; 165 | border-radius: 4px; 166 | background: rgb(220, 220, 220); 167 | background-position: center center; 168 | background-repeat: no-repeat; 169 | box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.2); 170 | font-family: 'Monaco', monospace; 171 | font-size: 90%; 172 | cursor: pointer; 173 | } 174 | .button:focus {outline: none;} 175 | 176 | .playbackOptions { 177 | flex-wrap: wrap; 178 | position: absolute; 179 | bottom: 0; 180 | left: 0; 181 | width: 100%; 182 | height: 52px; 183 | justify-content: center; 184 | background: rgba(0, 0, 0, 0.8); 185 | } 186 | #play { 187 | background-size: 32px; 188 | background-image: svg-load('icons/play.svg', fill: rgb(40, 40, 40)); 189 | } 190 | #pause { 191 | background-size: 32px; 192 | background-image: svg-load('icons/pause.svg', fill: rgb(40, 40, 40)); 193 | } 194 | #replay { 195 | background-size: 24px; 196 | background-image: svg-load('icons/replay.svg', fill: rgb(40, 40, 40)); 197 | } 198 | #winOff { 199 | background-size: 24px; 200 | background-image: svg-load('icons/window.svg', fill: rgb(120, 120, 120)); 201 | } 202 | #winOn { 203 | background-size: 24px; 204 | background-image: svg-load('icons/window.svg', fill: rgb(0, 0, 0)); 205 | } 206 | #recOff { 207 | background-size: 24px; 208 | background-image: svg-load('icons/record.svg', fill: rgb(120, 120, 120)); 209 | } 210 | #recOn { 211 | background-size: 24px; 212 | background-image: svg-load('icons/record.svg', fill: rgb(255, 0, 0)); 213 | } 214 | #markersOff { 215 | background-size: 18px; 216 | background-image: svg-load('icons/marker.svg', fill: rgb(120, 120, 120)); 217 | } 218 | #markersOn { 219 | background-size: 18px; 220 | background-image: svg-load('icons/marker.svg', fill: rgb(0, 0, 0)); 221 | } 222 | #tracksOff { 223 | background-size: 24px; 224 | background-image: svg-load('icons/track.svg', fill: rgb(120, 120, 120)); 225 | } 226 | #tracksOn { 227 | background-size: 24px; 228 | background-image: svg-load('icons/track.svg', fill: rgb(0, 0, 0)); 229 | } 230 | #graphOff { 231 | background-size: 18px; 232 | background-image: svg-load('icons/graph.svg', fill: rgb(120, 120, 120)); 233 | } 234 | #graphOn { 235 | background-size: 18px; 236 | background-image: svg-load('icons/graph.svg', fill: rgb(0, 0, 0)); 237 | } 238 | #mapIcon { 239 | background-size: 18px; 240 | background-image: svg-load('icons/map.svg', fill: rgb(40, 40, 40)); 241 | } 242 | .fileInputBtn { 243 | width: 52px; 244 | height: 30px; 245 | margin: 4px 8px; 246 | border-radius: 4px; 247 | background: rgb(220, 220, 220); 248 | background-position: center center; 249 | background-repeat: no-repeat; 250 | background-size: 20px; 251 | background-image: svg-load('icons/upload.svg', fill: rgb(40, 40, 40)); 252 | box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.2); 253 | cursor: pointer; 254 | } 255 | #helpOn { 256 | background-size: 24px; 257 | background-image: svg-load('icons/help.svg', fill: rgb(0, 0, 0)); 258 | } 259 | #helpOff { 260 | background-size: 24px; 261 | background-image: svg-load('icons/help.svg', fill: rgb(120, 120, 120)); 262 | } 263 | 264 | .helpDiv { 265 | flex-wrap: wrap; 266 | position: absolute; 267 | top: 8%; 268 | width: 100%; 269 | height: 58%; 270 | justify-content: center; 271 | font-family: 'Courier New', monospace; 272 | } 273 | .helpText { 274 | width: 60%; 275 | height: 98%; 276 | overflow: auto; 277 | padding: 30px; 278 | background: rgba(255, 255, 255, 0.9); 279 | border-radius: 4px; 280 | box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.2); 281 | } 282 | 283 | .ioeLogoSmall { 284 | z-index: 1; 285 | position: absolute; 286 | right: 10px; 287 | bottom: 10px; 288 | width: 32px; 289 | height: 32px; 290 | } 291 | .ioeLogoSmall a { 292 | position: absolute; 293 | width: 100%; 294 | height: 100%; 295 | background-position: center center; 296 | background-repeat: no-repeat; 297 | background-size: 100%; 298 | background-image: svg-load('icons/ioe_logo_small.svg', fill: rgba(255, 255, 255, 0.4)); 299 | } 300 | 301 | .timeControls { 302 | flex-wrap: wrap; 303 | position: absolute; 304 | bottom: 52px; 305 | left: 0; 306 | width: 100%; 307 | height: 66px; 308 | padding-bottom: 6px; 309 | justify-content: center; 310 | background: rgba(0, 0, 0, 0.8); 311 | } 312 | .rsDates { 313 | display: flex; 314 | flex-wrap: wrap; 315 | position: relative; 316 | width: 100%; 317 | height: 50%; 318 | justify-content: center; 319 | font-family: 'Courier New', monospace; 320 | } 321 | .rsDate { 322 | position: relative; 323 | width: 9%; 324 | padding: 6px 0; 325 | text-align: center; 326 | color: rgb(200, 200, 200); 327 | text-shadow: 0px 0px 4px rgba(0, 0, 0, 0.2); 328 | } 329 | .rangeSlider { 330 | display: flex; 331 | position: relative; 332 | width: 100%; 333 | height: 50%; 334 | margin: 0 64px 0 62px; 335 | justify-content: center; 336 | item-align: center; 337 | } 338 | .sliderTick { 339 | position: absolute; 340 | top: 0; 341 | width: 2px; 342 | height: 80%; 343 | background: rgba(80, 80, 80); 344 | /* box-shadow: 0 0px 20px 0px rgba(0, 0, 0, 0.4); */ 345 | } 346 | 347 | .graph { 348 | display: flex; 349 | flex-wrap: wrap; 350 | position: absolute; 351 | bottom: 124px; 352 | left: 0; 353 | width: 100%; 354 | height: auto; 355 | justify-content: center; 356 | background: rgba(0, 0, 0, 0.8); 357 | font-family: 'Courier New', monospace; 358 | } 359 | .gTitleContainer { 360 | display: flex; 361 | flex-wrap: wrap; 362 | position: absolute; 363 | bottom: 0; 364 | left: 0; 365 | width: 2.4%; 366 | height: 100%; 367 | justify-content: center; 368 | align-items: center; 369 | } 370 | .gTitle { 371 | position: relative; 372 | white-space: nowrap; 373 | color: rgb(200, 200, 200); 374 | transform: rotate(270deg); 375 | } 376 | 377 | .animalList { 378 | position: absolute; 379 | top: 60px; 380 | right: 20px; 381 | margin: auto; 382 | padding: 0; 383 | list-style: none; 384 | font-family: 'Courier New', monospace; 385 | } 386 | .animalListItem { 387 | padding: 5px 10px 5px 10px; 388 | margin: 4px 0; 389 | border-radius: 4px; 390 | box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.2); 391 | cursor: pointer; 392 | } 393 | 394 | /* modifiers for devices smaller than than 861px */ 395 | @media only screen and (max-width: 860px) { 396 | .scaleBar { 397 | top: 10px; 398 | left: 10px; 399 | } 400 | .zoomButton { 401 | top: 30px; 402 | left: 10px; 403 | } 404 | 405 | .centerDiv { 406 | top: 10%; 407 | } 408 | .welcomeBox { 409 | padding-top: 10px; 410 | } 411 | .fileInputBtnBig { 412 | width: 40px; 413 | height: 40px; 414 | background-size: 28px; 415 | } 416 | .ioeLogoBig { 417 | width: 92px; 418 | height: 46px; 419 | } 420 | .counter { 421 | font-size: 106%; 422 | } 423 | .counter p { 424 | margin-left: auto; 425 | margin-right: auto; 426 | max-width: 240px; 427 | } 428 | 429 | .button { 430 | width: 34px; 431 | height: 28px; 432 | padding: 4px; 433 | margin: 4px 3px; 434 | } 435 | .fileInputBtn { 436 | width: 34px; 437 | height: 28px; 438 | margin: 4px 3px; 439 | } 440 | 441 | .ioeLogoSmall { 442 | width: 26px; 443 | height: 26px; 444 | } 445 | 446 | .rsDates { 447 | margin: 0 46px 0 46px; 448 | font-size: 70%; 449 | } 450 | 451 | .animalList { 452 | right: 10px; 453 | font-size: 90%; 454 | } 455 | } 456 | -------------------------------------------------------------------------------- /src/css/icons/ioe_logo.svg: -------------------------------------------------------------------------------- 1 | 4 | 356 | 357 | -------------------------------------------------------------------------------- /website/simulated_data.csv: -------------------------------------------------------------------------------- 1 | species,animal_id,timestamp,lon,lat,alt 2 | zebra,1000,2018-01-01 00:00:00,36.8479966241145,0.051996186567822,0 3 | zebra,1000,2018-01-01 01:00:00,36.847888530557,0.055151561819454,0 4 | zebra,1000,2018-01-01 02:00:00,36.8501403399046,0.061387897423829,0 5 | zebra,1000,2018-01-01 03:00:00,36.8489549425216,0.065124154871808,0 6 | zebra,1000,2018-01-01 04:00:00,36.8490149161694,0.065226474319365,0 7 | zebra,1000,2018-01-01 05:00:00,36.8486224662566,0.066386038680489,0 8 | zebra,1000,2018-01-01 06:00:00,36.8475814153899,0.078071263332897,0 9 | zebra,1000,2018-01-01 07:00:00,36.843689567097,0.075439339898611,0 10 | zebra,1000,2018-01-01 08:00:00,36.8402106289596,0.076604784053682,0 11 | zebra,1000,2018-01-01 09:00:00,36.8375335959026,0.075990638875371,0 12 | zebra,1000,2018-01-01 10:00:00,36.8372746144821,0.075782930103605,0 13 | zebra,1000,2018-01-01 11:00:00,36.8390933901332,0.072137095579933,0 14 | zebra,1000,2018-01-01 12:00:00,36.8416779919135,0.069486915224055,0 15 | zebra,1000,2018-01-01 13:00:00,36.8421675798318,0.066971169142682,0 16 | zebra,1000,2018-01-01 14:00:00,36.8501910464142,0.057909804862203,0 17 | zebra,1000,2018-01-01 15:00:00,36.8503835941547,0.057502396534055,0 18 | zebra,1000,2018-01-01 16:00:00,36.851499630074,0.055543348103807,0 19 | zebra,1000,2018-01-01 17:00:00,36.8535331429846,0.052899794061314,0 20 | zebra,1000,2018-01-01 18:00:00,36.8599645015989,0.038300716677781,0 21 | zebra,1000,2018-01-01 19:00:00,36.8587865234051,0.034887234403865,0 22 | zebra,1000,2018-01-01 20:00:00,36.8585682739681,0.035868629511306,0 23 | zebra,1000,2018-01-01 21:00:00,36.8585870878499,0.03596577913083,0 24 | zebra,1000,2018-01-01 22:00:00,36.8596064294785,0.036106991452074,0 25 | zebra,1000,2018-01-01 23:00:00,36.8596272934791,0.036116006069693,0 26 | zebra,1000,2018-01-02 00:00:00,36.860388047866,0.035497710684717,0 27 | zebra,1000,2018-01-02 01:00:00,36.859033455738,0.035150073183555,0 28 | zebra,1000,2018-01-02 02:00:00,36.8597686361583,0.034563158357866,0 29 | zebra,1000,2018-01-02 03:00:00,36.8598856670936,0.034805817649333,0 30 | zebra,1000,2018-01-02 04:00:00,36.8549379797007,0.05743143049862,0 31 | zebra,1000,2018-01-02 05:00:00,36.8529005213181,0.066362932236138,0 32 | zebra,1000,2018-01-02 06:00:00,36.8752118978887,0.078474012923615,0 33 | zebra,1000,2018-01-02 07:00:00,36.8789231144774,0.083130844449618,0 34 | zebra,1000,2018-01-02 08:00:00,36.8783643654481,0.0915751707153,0 35 | zebra,1000,2018-01-02 09:00:00,36.8775467225003,0.101569214076154,0 36 | zebra,1000,2018-01-02 10:00:00,36.8775557912208,0.102023461632172,0 37 | zebra,1000,2018-01-02 11:00:00,36.87890529317,0.097454106506665,0 38 | zebra,1000,2018-01-02 12:00:00,36.8774748280325,0.097021986637648,0 39 | zebra,1000,2018-01-02 13:00:00,36.877173099778,0.091938625082772,0 40 | zebra,1000,2018-01-02 14:00:00,36.8741393270983,0.093767376399331,0 41 | zebra,1000,2018-01-02 15:00:00,36.8696585992872,0.095009391810407,0 42 | zebra,1000,2018-01-02 16:00:00,36.835507998702,0.09953488924383,0 43 | zebra,1000,2018-01-02 17:00:00,36.8328621263477,0.099090301265002,0 44 | zebra,1000,2018-01-02 18:00:00,36.8314711185244,0.098450258918901,0 45 | zebra,1000,2018-01-02 19:00:00,36.8322579974915,0.098631343338006,0 46 | zebra,1000,2018-01-02 20:00:00,36.8353237872012,0.09353153851927,0 47 | zebra,1000,2018-01-02 21:00:00,36.8414178317146,0.069102677792236,0 48 | zebra,1000,2018-01-02 22:00:00,36.8425299604036,0.069326517950248,0 49 | zebra,1000,2018-01-02 23:00:00,36.845011114797,0.066868944596475,0 50 | zebra,1000,2018-01-03 00:00:00,36.8436879533599,0.065442889287685,0 51 | zebra,1000,2018-01-03 01:00:00,36.8340701882755,0.06017446453705,0 52 | zebra,1000,2018-01-03 02:00:00,36.8377745743593,0.048611652572035,0 53 | zebra,1000,2018-01-03 03:00:00,36.8370593171786,0.041212850298887,0 54 | zebra,1000,2018-01-03 04:00:00,36.8312573266253,0.036913378090546,0 55 | zebra,1000,2018-01-03 05:00:00,36.8311131686681,0.034559348472163,0 56 | zebra,1000,2018-01-03 06:00:00,36.8177951892632,0.026279653072964,0 57 | zebra,1000,2018-01-03 07:00:00,36.8159274632567,0.025513415825754,0 58 | zebra,1000,2018-01-03 08:00:00,36.8153258958242,0.027538388313757,0 59 | zebra,1000,2018-01-03 09:00:00,36.8133376678739,0.030938435409233,0 60 | zebra,1000,2018-01-03 10:00:00,36.8223283560041,0.02680280012714,0 61 | zebra,1000,2018-01-03 11:00:00,36.8246866594858,0.02766277444395,0 62 | zebra,1000,2018-01-03 12:00:00,36.8282847311342,0.028766529793337,0 63 | zebra,1000,2018-01-03 13:00:00,36.8354377217358,0.025292723514529,0 64 | zebra,1000,2018-01-03 14:00:00,36.8485344059254,0.017332538254235,0 65 | zebra,1000,2018-01-03 15:00:00,36.8526525161411,0.009690315846324,0 66 | zebra,1000,2018-01-03 16:00:00,36.8526460498667,0.009747567410507,0 67 | zebra,1000,2018-01-03 17:00:00,36.8509390349531,0.010114344646606,0 68 | zebra,1000,2018-01-03 18:00:00,36.8501265131917,0.009957660746618,0 69 | zebra,1000,2018-01-03 19:00:00,36.8458720081207,0.006398210007882,0 70 | zebra,1000,2018-01-03 20:00:00,36.846167705718,0.005846906656809,0 71 | zebra,1000,2018-01-03 21:00:00,36.846204127889,0.005454163177928,0 72 | zebra,1000,2018-01-03 22:00:00,36.8435215206309,-0.002590671043198,0 73 | zebra,1000,2018-01-03 23:00:00,36.8388095440141,-0.011295476968571,0 74 | zebra,1000,2018-01-04 00:00:00,36.8373678775949,-0.023742697497735,0 75 | zebra,1000,2018-01-04 01:00:00,36.8377784244312,-0.025787853418108,0 76 | zebra,1000,2018-01-04 02:00:00,36.8387096878621,-0.026957645377915,0 77 | zebra,1000,2018-01-04 03:00:00,36.839794332602,-0.028282811879782,0 78 | zebra,1000,2018-01-04 04:00:00,36.8485524381997,-0.033857744030536,0 79 | zebra,1000,2018-01-04 05:00:00,36.8483270028874,-0.03542353725536,0 80 | zebra,1000,2018-01-04 06:00:00,36.8457239280137,-0.036162245685927,0 81 | zebra,1000,2018-01-04 07:00:00,36.8455334054476,-0.036127488067847,0 82 | zebra,1000,2018-01-04 08:00:00,36.8459824892384,-0.034965862637555,0 83 | zebra,1000,2018-01-04 09:00:00,36.8438611031017,-0.029254230902766,0 84 | zebra,1000,2018-01-04 10:00:00,36.8433177230966,-0.028215358882973,0 85 | zebra,1000,2018-01-04 11:00:00,36.841161560414,-0.022146223813717,0 86 | zebra,1000,2018-01-04 12:00:00,36.8282861047804,-0.012771695254722,0 87 | zebra,1000,2018-01-04 13:00:00,36.8217652478617,-0.009676830345776,0 88 | zebra,1000,2018-01-04 14:00:00,36.8148858707089,-0.004777249511603,0 89 | zebra,1000,2018-01-04 15:00:00,36.8199600225634,-0.010421520050777,0 90 | zebra,1000,2018-01-04 16:00:00,36.8151835337362,-0.008985120492937,0 91 | zebra,1000,2018-01-04 17:00:00,36.8228799325841,-0.002011416481498,0 92 | zebra,1000,2018-01-04 18:00:00,36.8228218543112,-0.001939887737191,0 93 | zebra,1000,2018-01-04 19:00:00,36.809827666523,0.01415001168448,0 94 | zebra,1000,2018-01-04 20:00:00,36.8073154883008,0.017028043670036,0 95 | zebra,1000,2018-01-04 21:00:00,36.8161356952671,0.025628212937579,0 96 | zebra,1000,2018-01-04 22:00:00,36.8176040060083,0.026107558084806,0 97 | zebra,1000,2018-01-04 23:00:00,36.8177380882479,0.026113882372182,0 98 | zebra,1000,2018-01-05 00:00:00,36.8169880126811,0.027328671601738,0 99 | zebra,1000,2018-01-05 01:00:00,36.8169381580302,0.027307082628252,0 100 | zebra,1000,2018-01-05 02:00:00,36.8170425847416,0.024155672862705,0 101 | zebra,1000,2018-01-05 03:00:00,36.817869725999,0.023964072769326,0 102 | zebra,1000,2018-01-05 04:00:00,36.8186215838061,0.023585007413535,0 103 | zebra,1000,2018-01-05 05:00:00,36.8189240563088,0.02710806280571,0 104 | zebra,1000,2018-01-05 06:00:00,36.819777991048,0.028829141281843,0 105 | zebra,1000,2018-01-05 07:00:00,36.8097223826805,0.037527562224876,0 106 | zebra,1000,2018-01-05 08:00:00,36.7895618180451,0.052036916444954,0 107 | zebra,1000,2018-01-05 09:00:00,36.7793185761799,0.05589133443662,0 108 | zebra,1000,2018-01-05 10:00:00,36.7658864369801,0.057076092080794,0 109 | zebra,1000,2018-01-05 11:00:00,36.7565075695611,0.05966309992114,0 110 | zebra,1000,2018-01-05 12:00:00,36.7558761393182,0.062724353488777,0 111 | zebra,1000,2018-01-05 13:00:00,36.7559534211833,0.062805468798278,0 112 | zebra,1000,2018-01-05 14:00:00,36.7578973176761,0.063752806300572,0 113 | zebra,1000,2018-01-05 15:00:00,36.7587836911943,0.064434366301564,0 114 | zebra,1000,2018-01-05 16:00:00,36.7543472279052,0.080907609930633,0 115 | zebra,1000,2018-01-05 17:00:00,36.7448431649867,0.094154352841686,0 116 | zebra,1000,2018-01-05 18:00:00,36.7500072452191,0.098420575812655,0 117 | zebra,1000,2018-01-05 19:00:00,36.7623067016521,0.094669394546203,0 118 | zebra,1000,2018-01-05 20:00:00,36.7599085440318,0.096016308204573,0 119 | zebra,1000,2018-01-05 21:00:00,36.7578415603505,0.096197669529911,0 120 | zebra,1000,2018-01-05 22:00:00,36.7555134356222,0.095973868416395,0 121 | zebra,1000,2018-01-05 23:00:00,36.7540906134863,0.093886698032508,0 122 | zebra,1000,2018-01-06 00:00:00,36.7561802313949,0.085834528761717,0 123 | zebra,1000,2018-01-06 01:00:00,36.7563963013524,0.084263014523339,0 124 | zebra,1000,2018-01-06 02:00:00,36.7579389482416,0.082347446507028,0 125 | zebra,1000,2018-01-06 03:00:00,36.7602247252343,0.081982482665559,0 126 | zebra,1000,2018-01-06 04:00:00,36.7601395778095,0.08293559521197,0 127 | zebra,1000,2018-01-06 05:00:00,36.7630919765025,0.082888508373309,0 128 | zebra,1000,2018-01-06 06:00:00,36.7637845150402,0.084811703728277,0 129 | zebra,1000,2018-01-06 07:00:00,36.7642202819449,0.084368532278208,0 130 | zebra,1000,2018-01-06 08:00:00,36.7631636978948,0.087149850594213,0 131 | zebra,1000,2018-01-06 09:00:00,36.7599055543984,0.091665718691104,0 132 | zebra,1000,2018-01-06 10:00:00,36.75443814354,0.102414576463985,0 133 | zebra,1000,2018-01-06 11:00:00,36.7585189220332,0.117978117042953,0 134 | zebra,1000,2018-01-06 12:00:00,36.7743913144032,0.129631152766669,0 135 | zebra,1000,2018-01-06 13:00:00,36.7636804607427,0.107177206950965,0 136 | zebra,1000,2018-01-06 14:00:00,36.758073586594,0.090443772686704,0 137 | zebra,1000,2018-01-06 15:00:00,36.7635737915769,0.061163229036701,0 138 | zebra,1000,2018-01-06 16:00:00,36.7623664805285,0.057397761505656,0 139 | zebra,1000,2018-01-06 17:00:00,36.7624983665969,0.055820670388179,0 140 | zebra,1000,2018-01-06 18:00:00,36.7680727312895,0.063340249427045,0 141 | zebra,1000,2018-01-06 19:00:00,36.770515718129,0.067152248419288,0 142 | zebra,1000,2018-01-06 20:00:00,36.7719869093313,0.069154672411135,0 143 | zebra,1000,2018-01-06 21:00:00,36.7726811014125,0.070789464498801,0 144 | zebra,1000,2018-01-06 22:00:00,36.771251408753,0.072012297531434,0 145 | zebra,1000,2018-01-06 23:00:00,36.7708909686788,0.06962273705957,0 146 | zebra,1000,2018-01-07 00:00:00,36.7694252471947,0.067426692339269,0 147 | zebra,1000,2018-01-07 01:00:00,36.7676612791834,0.064692521648548,0 148 | zebra,1000,2018-01-07 02:00:00,36.7629257901661,0.071670887745946,0 149 | zebra,1000,2018-01-07 03:00:00,36.7563339633102,0.074529142125126,0 150 | zebra,1000,2018-01-07 04:00:00,36.7473072078145,0.083472783486679,0 151 | zebra,1000,2018-01-07 05:00:00,36.7488413896441,0.083849346047189,0 152 | zebra,1000,2018-01-07 06:00:00,36.7472510390529,0.08248545099884,0 153 | zebra,1000,2018-01-07 07:00:00,36.7502506128249,0.067524782732133,0 154 | zebra,1000,2018-01-07 08:00:00,36.7444361149529,0.070868459782118,0 155 | zebra,1000,2018-01-07 09:00:00,36.7357309978234,0.068255370199004,0 156 | zebra,1000,2018-01-07 10:00:00,36.7324249521858,0.068258426996527,0 157 | zebra,1000,2018-01-07 11:00:00,36.7316782031354,0.068189967060479,0 158 | zebra,1000,2018-01-07 12:00:00,36.7318331530916,0.068158504772441,0 159 | zebra,1000,2018-01-07 13:00:00,36.7431996216813,0.061610914575574,0 160 | zebra,1000,2018-01-07 14:00:00,36.7450347437498,0.061623595822005,0 161 | zebra,1000,2018-01-07 15:00:00,36.750001094487,0.065733244766641,0 162 | zebra,1000,2018-01-07 16:00:00,36.7520529069139,0.070114815280239,0 163 | zebra,1000,2018-01-07 17:00:00,36.7522002792083,0.070528932168817,0 164 | zebra,1000,2018-01-07 18:00:00,36.7494905997707,0.071563891959676,0 165 | zebra,1000,2018-01-07 19:00:00,36.7502350979964,0.073044989499552,0 166 | zebra,1000,2018-01-07 20:00:00,36.7522123563333,0.07269903729588,0 167 | zebra,1000,2018-01-07 21:00:00,36.7515021729584,0.072086427836345,0 168 | zebra,1000,2018-01-07 22:00:00,36.7514623221437,0.072345301735277,0 169 | zebra,1000,2018-01-07 23:00:00,36.7491474866991,0.069342581404413,0 170 | zebra,1000,2018-01-08 00:00:00,36.7492567568283,0.069385170218007,0 171 | zebra,1000,2018-01-08 01:00:00,36.7500093625761,0.070273225564315,0 172 | zebra,1000,2018-01-08 02:00:00,36.7569343661708,0.076082502847143,0 173 | zebra,1000,2018-01-08 03:00:00,36.7664916510267,0.086691108055973,0 174 | zebra,1000,2018-01-08 04:00:00,36.7659313476637,0.086689055358495,0 175 | zebra,1000,2018-01-08 05:00:00,36.7653828155862,0.09033936803294,0 176 | zebra,1000,2018-01-08 06:00:00,36.767482828467,0.088213280908742,0 177 | zebra,1000,2018-01-08 07:00:00,36.766546734731,0.089247437124864,0 178 | zebra,1000,2018-01-08 08:00:00,36.7658970634004,0.088616433658996,0 179 | zebra,1000,2018-01-08 09:00:00,36.7659208534507,0.088605127136593,0 180 | zebra,1000,2018-01-08 10:00:00,36.7689813825644,0.089266541618867,0 181 | zebra,1000,2018-01-08 11:00:00,36.7690122535907,0.0890524235361,0 182 | zebra,1000,2018-01-08 12:00:00,36.7643024425311,0.08212027478192,0 183 | zebra,1000,2018-01-08 13:00:00,36.7632705057156,0.083937233663453,0 184 | zebra,1000,2018-01-08 14:00:00,36.7597988821978,0.084641014063468,0 185 | zebra,1000,2018-01-08 15:00:00,36.7568298882206,0.081073131534324,0 186 | zebra,1000,2018-01-08 16:00:00,36.7517564241208,0.079665760096445,0 187 | zebra,1000,2018-01-08 17:00:00,36.7441512007283,0.079253071799965,0 188 | zebra,1000,2018-01-08 18:00:00,36.7452146233437,0.079639893301938,0 189 | zebra,1000,2018-01-08 19:00:00,36.7453220167495,0.078018913544654,0 190 | zebra,1000,2018-01-08 20:00:00,36.744587773071,0.072235063894667,0 191 | zebra,1000,2018-01-08 21:00:00,36.7562157993581,0.06829169806121,0 192 | zebra,1000,2018-01-08 22:00:00,36.7633162821883,0.062490985767877,0 193 | zebra,1000,2018-01-08 23:00:00,36.7666033393748,0.064203659878447,0 194 | zebra,1000,2018-01-09 00:00:00,36.7677728899914,0.064193566501884,0 195 | zebra,1000,2018-01-09 01:00:00,36.7674489726364,0.065013090693815,0 196 | zebra,1000,2018-01-09 02:00:00,36.7679607022512,0.066399454566816,0 197 | zebra,1000,2018-01-09 03:00:00,36.7670711460541,0.067680049212806,0 198 | zebra,1000,2018-01-09 04:00:00,36.7673065255349,0.068091259819469,0 199 | zebra,1000,2018-01-09 05:00:00,36.767856028185,0.069062022208104,0 200 | zebra,1000,2018-01-09 06:00:00,36.7622957880694,0.070009573619679,0 201 | zebra,1000,2018-01-09 07:00:00,36.772638067141,0.055638854322715,0 202 | zebra,1000,2018-01-09 08:00:00,36.7762663756415,0.050844798074453,0 203 | zebra,1000,2018-01-09 09:00:00,36.7757164236833,0.051473541717215,0 204 | zebra,1000,2018-01-09 10:00:00,36.7789224792472,0.054286657199235,0 205 | zebra,1000,2018-01-09 11:00:00,36.7809084818791,0.056210492354621,0 206 | zebra,1000,2018-01-09 12:00:00,36.7799565907611,0.055913569672617,0 207 | zebra,1000,2018-01-09 13:00:00,36.7788112688042,0.052411143538589,0 208 | zebra,1000,2018-01-09 14:00:00,36.7765414969739,0.058034037781361,0 209 | zebra,1000,2018-01-09 15:00:00,36.7678937558171,0.061672310334893,0 210 | zebra,1000,2018-01-09 16:00:00,36.7685980277678,0.062870550323995,0 211 | zebra,1000,2018-01-09 17:00:00,36.7687348264375,0.062807268880229,0 212 | zebra,1000,2018-01-09 18:00:00,36.7708538367815,0.060317285207709,0 213 | zebra,1000,2018-01-09 19:00:00,36.7911892175967,0.044464115261323,0 214 | zebra,1000,2018-01-09 20:00:00,36.7978161053348,0.051937703412278,0 215 | zebra,1000,2018-01-09 21:00:00,36.8109181631193,0.060369680607167,0 216 | zebra,1000,2018-01-09 22:00:00,36.8152627853872,0.062917383084641,0 217 | zebra,1000,2018-01-09 23:00:00,36.8244878693805,0.067925835761267,0 218 | zebra,1000,2018-01-10 00:00:00,36.8479695907452,0.086299092150033,0 219 | zebra,1000,2018-01-10 01:00:00,36.8517943986972,0.091065886594922,0 220 | zebra,1000,2018-01-10 02:00:00,36.854236014864,0.093782223015532,0 221 | zebra,1000,2018-01-10 03:00:00,36.851518015289,0.094490100688952,0 222 | zebra,1000,2018-01-10 04:00:00,36.8498726831899,0.09615478228819,0 223 | zebra,1000,2018-01-10 05:00:00,36.8518982709471,0.098626425549842,0 224 | zebra,1000,2018-01-10 06:00:00,36.8519070706514,0.098597965742822,0 225 | zebra,1000,2018-01-10 07:00:00,36.8517744519554,0.098103039605629,0 226 | zebra,1000,2018-01-10 08:00:00,36.851672744477,0.09848674548466,0 227 | zebra,1000,2018-01-10 09:00:00,36.8525887565649,0.099357534895889,0 228 | zebra,1000,2018-01-10 10:00:00,36.8525782514446,0.099776085237896,0 229 | zebra,1000,2018-01-10 11:00:00,36.8517915965708,0.100269292896272,0 230 | zebra,1000,2018-01-10 12:00:00,36.8519633265269,0.101276174534139,0 231 | zebra,1000,2018-01-10 13:00:00,36.8530592079519,0.106218801411112,0 232 | zebra,1000,2018-01-10 14:00:00,36.8586402259929,0.112290888747815,0 233 | zebra,1000,2018-01-10 15:00:00,36.860337408216,0.111816689430131,0 234 | zebra,1000,2018-01-10 16:00:00,36.8635514363936,0.110629672676182,0 235 | zebra,1000,2018-01-10 17:00:00,36.8661027077969,0.113286628882784,0 236 | zebra,1000,2018-01-10 18:00:00,36.870114469434,0.109910136777062,0 237 | zebra,1000,2018-01-10 19:00:00,36.8709812609887,0.108322269900496,0 238 | zebra,1000,2018-01-10 20:00:00,36.8719811288179,0.107209194269777,0 239 | zebra,1000,2018-01-10 21:00:00,36.8720458823543,0.108961090463488,0 240 | zebra,1000,2018-01-10 22:00:00,36.870513698,0.110983511359756,0 241 | zebra,1000,2018-01-10 23:00:00,36.8709721613908,0.111388811516057,0 242 | zebra,2000,2018-01-01 00:00:00,36.8499966241145,0.0499961865678216,0 243 | zebra,2000,2018-01-01 01:00:00,36.8499538644148,0.0499649492867899,0 244 | zebra,2000,2018-01-01 02:00:00,36.849601457501,0.0456140924149072,0 245 | zebra,2000,2018-01-01 03:00:00,36.8503796827511,0.045423247206121,0 246 | zebra,2000,2018-01-01 04:00:00,36.8518048541687,0.0465318201529522,0 247 | zebra,2000,2018-01-01 05:00:00,36.8621240770694,0.0485377441440913,0 248 | zebra,2000,2018-01-01 06:00:00,36.8731973854235,0.0500118592560545,0 249 | zebra,2000,2018-01-01 07:00:00,36.8919386003883,0.0528928837211637,0 250 | zebra,2000,2018-01-01 08:00:00,36.8928035967234,0.0503992159226574,0 251 | zebra,2000,2018-01-01 09:00:00,36.8927702043814,0.0485415222666204,0 252 | zebra,2000,2018-01-01 10:00:00,36.8907623320385,0.0474238130027966,0 253 | zebra,2000,2018-01-01 11:00:00,36.8905281299735,0.0488815991247764,0 254 | zebra,2000,2018-01-01 12:00:00,36.8909408730179,0.0504605305353582,0 255 | zebra,2000,2018-01-01 13:00:00,36.8928857564821,0.0506251830515072,0 256 | zebra,2000,2018-01-01 14:00:00,36.8939145873349,0.0506867565269156,0 257 | zebra,2000,2018-01-01 15:00:00,36.8963453665199,0.0498517576438692,0 258 | zebra,2000,2018-01-01 16:00:00,36.8963388182817,0.049668644818204,0 259 | zebra,2000,2018-01-01 17:00:00,36.8962890131374,0.0495246843259168,0 260 | zebra,2000,2018-01-01 18:00:00,36.898988613178,0.0483766432447747,0 261 | zebra,2000,2018-01-01 19:00:00,36.9006460586891,0.0571937919543837,0 262 | zebra,2000,2018-01-01 20:00:00,36.9020362653026,0.0576514905682019,0 263 | zebra,2000,2018-01-01 21:00:00,36.9010685124701,0.0600544334312429,0 264 | zebra,2000,2018-01-01 22:00:00,36.8975702009132,0.0601164211952763,0 265 | zebra,2000,2018-01-01 23:00:00,36.8978550523331,0.0599741252139508,0 266 | zebra,2000,2018-01-02 00:00:00,36.8992191430928,0.0605893126464287,0 267 | zebra,2000,2018-01-02 01:00:00,36.9004202386438,0.0605598501419683,0 268 | zebra,2000,2018-01-02 02:00:00,36.898521422412,0.0586694175236057,0 269 | zebra,2000,2018-01-02 03:00:00,36.899209769183,0.0483805221856087,0 270 | zebra,2000,2018-01-02 04:00:00,36.90038180283,0.0407318007841879,0 271 | zebra,2000,2018-01-02 05:00:00,36.903120734162,0.00945034431864551,0 272 | zebra,2000,2018-01-02 06:00:00,36.9068989914954,-0.014957362116051,0 273 | zebra,2000,2018-01-02 07:00:00,36.9075395977535,-0.0271581074142843,0 274 | zebra,2000,2018-01-02 08:00:00,36.9078110346619,-0.0271662662653985,0 275 | zebra,2000,2018-01-02 09:00:00,36.9083891933362,-0.0270757748481808,0 276 | zebra,2000,2018-01-02 10:00:00,36.9083040954965,-0.0268495552562641,0 277 | zebra,2000,2018-01-02 11:00:00,36.9109265360681,-0.0265389748649646,0 278 | zebra,2000,2018-01-02 12:00:00,36.9092029977325,-0.0259825307039797,0 279 | zebra,2000,2018-01-02 13:00:00,36.9073850283115,-0.0254803598930968,0 280 | zebra,2000,2018-01-02 14:00:00,36.9061133306256,-0.0252241978802528,0 281 | zebra,2000,2018-01-02 15:00:00,36.9101809034835,-0.021142085343412,0 282 | zebra,2000,2018-01-02 16:00:00,36.9096080485086,-0.0195380918734023,0 283 | zebra,2000,2018-01-02 17:00:00,36.906202623616,-0.0217986784433516,0 284 | zebra,2000,2018-01-02 18:00:00,36.9030567411783,-0.00658455592428532,0 285 | zebra,2000,2018-01-02 19:00:00,36.9008817778199,-0.00803339939564018,0 286 | zebra,2000,2018-01-02 20:00:00,36.8894885509122,-0.0181349850778773,0 287 | zebra,2000,2018-01-02 21:00:00,36.8829585185892,-0.0170084582974312,0 288 | zebra,2000,2018-01-02 22:00:00,36.8617799492566,-0.016027529694052,0 289 | zebra,2000,2018-01-02 23:00:00,36.8590170438387,-0.0132745551971504,0 290 | zebra,2000,2018-01-03 00:00:00,36.8590595862456,-0.0134343822742453,0 291 | zebra,2000,2018-01-03 01:00:00,36.8590011174943,-0.0134653136321926,0 292 | zebra,2000,2018-01-03 02:00:00,36.8601258665275,-0.0134080399378574,0 293 | zebra,2000,2018-01-03 03:00:00,36.859756091887,-0.017978642469604,0 294 | zebra,2000,2018-01-03 04:00:00,36.8623884073619,-0.0338645384583473,0 295 | zebra,2000,2018-01-03 05:00:00,36.8592518780676,-0.0316383910853257,0 296 | zebra,2000,2018-01-03 06:00:00,36.8587317549124,-0.0307176233317044,0 297 | zebra,2000,2018-01-03 07:00:00,36.8614013168386,-0.0295321079028493,0 298 | zebra,2000,2018-01-03 08:00:00,36.8648178446249,-0.0276720725518013,0 299 | zebra,2000,2018-01-03 09:00:00,36.8651681272838,-0.028449889091605,0 300 | zebra,2000,2018-01-03 10:00:00,36.8617300676572,-0.0314343333772517,0 301 | zebra,2000,2018-01-03 11:00:00,36.8605223180588,-0.0323954945920089,0 302 | zebra,2000,2018-01-03 12:00:00,36.8605629292025,-0.0323958519676616,0 303 | zebra,2000,2018-01-03 13:00:00,36.8605813684569,-0.0323836607055137,0 304 | zebra,2000,2018-01-03 14:00:00,36.8606181635449,-0.0323977991174443,0 305 | zebra,2000,2018-01-03 15:00:00,36.8701783598237,-0.0366098832808517,0 306 | zebra,2000,2018-01-03 16:00:00,36.8703381650132,-0.0368419852496301,0 307 | zebra,2000,2018-01-03 17:00:00,36.8700918096955,-0.0366886877025371,0 308 | zebra,2000,2018-01-03 18:00:00,36.8658226287292,-0.0365515963496486,0 309 | zebra,2000,2018-01-03 19:00:00,36.8487934270197,-0.0369350035157514,0 310 | zebra,2000,2018-01-03 20:00:00,36.8490128434861,-0.0356409518783634,0 311 | zebra,2000,2018-01-03 21:00:00,36.8486746071556,-0.030366755206377,0 312 | zebra,2000,2018-01-03 22:00:00,36.8510355541436,-0.0256028937655382,0 313 | zebra,2000,2018-01-03 23:00:00,36.8507737532852,-0.0259110530782366,0 314 | zebra,2000,2018-01-04 00:00:00,36.8484371841855,-0.024229924917802,0 315 | zebra,2000,2018-01-04 01:00:00,36.8501573901797,-0.0231907339073738,0 316 | zebra,2000,2018-01-04 02:00:00,36.8513242930057,-0.0225018711218836,0 317 | zebra,2000,2018-01-04 03:00:00,36.8511041827355,-0.0224696929703486,0 318 | zebra,2000,2018-01-04 04:00:00,36.8502810302938,-0.0229133660481977,0 319 | zebra,2000,2018-01-04 05:00:00,36.8500858506938,-0.0222882367243317,0 320 | zebra,2000,2018-01-04 06:00:00,36.8506503188548,-0.0236722208440398,0 321 | zebra,2000,2018-01-04 07:00:00,36.8505156601733,-0.0297184022416979,0 322 | zebra,2000,2018-01-04 08:00:00,36.852082690091,-0.0248828179729096,0 323 | zebra,2000,2018-01-04 09:00:00,36.8494146517223,-0.0195267213781532,0 324 | zebra,2000,2018-01-04 10:00:00,36.8482707843814,-0.0174133830428108,0 325 | zebra,2000,2018-01-04 11:00:00,36.843322561348,-0.0187057938913617,0 326 | zebra,2000,2018-01-04 12:00:00,36.8429975756649,-0.0189846885638874,0 327 | zebra,2000,2018-01-04 13:00:00,36.839648770474,-0.0176899102921114,0 328 | zebra,2000,2018-01-04 14:00:00,36.839804282961,-0.0175894584602374,0 329 | zebra,2000,2018-01-04 15:00:00,36.8380975026051,-0.0181938722607264,0 330 | zebra,2000,2018-01-04 16:00:00,36.8079847527484,-0.0134038799322229,0 331 | zebra,2000,2018-01-04 17:00:00,36.7796955081027,-0.000925150935238849,0 332 | zebra,2000,2018-01-04 18:00:00,36.7720046949655,-0.00208805667162174,0 333 | zebra,2000,2018-01-04 19:00:00,36.7429767154966,0.00906759298509872,0 334 | zebra,2000,2018-01-04 20:00:00,36.7383755398417,0.00895906806879309,0 335 | zebra,2000,2018-01-04 21:00:00,36.7414887598237,0.00571958399304839,0 336 | zebra,2000,2018-01-04 22:00:00,36.7421933209809,-0.00100050889270756,0 337 | zebra,2000,2018-01-04 23:00:00,36.7423684003863,-0.00237848151932642,0 338 | zebra,2000,2018-01-05 00:00:00,36.7440492323073,-0.0106158394661958,0 339 | zebra,2000,2018-01-05 01:00:00,36.7446835534838,-0.0126837212883104,0 340 | zebra,2000,2018-01-05 02:00:00,36.7445496850459,-0.0140500447254723,0 341 | zebra,2000,2018-01-05 03:00:00,36.7442924944684,-0.0146151948111401,0 342 | zebra,2000,2018-01-05 04:00:00,36.7440347724135,-0.0140437521948519,0 343 | zebra,2000,2018-01-05 05:00:00,36.746834088584,-0.0127022268176512,0 344 | zebra,2000,2018-01-05 06:00:00,36.7496925926704,-0.0136885124934199,0 345 | zebra,2000,2018-01-05 07:00:00,36.748258041528,-0.0163766293344368,0 346 | zebra,2000,2018-01-05 08:00:00,36.7479120835677,-0.0163195651606239,0 347 | zebra,2000,2018-01-05 09:00:00,36.7469335168786,-0.0153327216935684,0 348 | zebra,2000,2018-01-05 10:00:00,36.7413451457611,-0.00850524272917535,0 349 | zebra,2000,2018-01-05 11:00:00,36.7405313520973,-0.00986915052193682,0 350 | zebra,2000,2018-01-05 12:00:00,36.7424512636644,-0.00948709553657918,0 351 | zebra,2000,2018-01-05 13:00:00,36.7436444038605,-0.00908474626193407,0 352 | zebra,2000,2018-01-05 14:00:00,36.7445149709626,-0.00812058396260012,0 353 | zebra,2000,2018-01-05 15:00:00,36.7443240787001,-0.00826753322077643,0 354 | zebra,2000,2018-01-05 16:00:00,36.7382377350354,-0.0106469122258583,0 355 | zebra,2000,2018-01-05 17:00:00,36.7328237542955,-0.00964961789846661,0 356 | zebra,2000,2018-01-05 18:00:00,36.7323207357449,-0.00862577143263554,0 357 | zebra,2000,2018-01-05 19:00:00,36.7386982052112,-0.00622778853615289,0 358 | zebra,2000,2018-01-05 20:00:00,36.7389347832549,-0.00576150303417469,0 359 | zebra,2000,2018-01-05 21:00:00,36.7401508938124,-0.00372304699859897,0 360 | zebra,2000,2018-01-05 22:00:00,36.7398223858927,-0.00451598044674002,0 361 | zebra,2000,2018-01-05 23:00:00,36.7391200111367,-0.00623432626003899,0 362 | zebra,2000,2018-01-06 00:00:00,36.7391697602885,-0.00629008184388915,0 363 | zebra,2000,2018-01-06 01:00:00,36.7400467588604,-0.00781736183690546,0 364 | zebra,2000,2018-01-06 02:00:00,36.7384416129793,-0.00529180068820812,0 365 | zebra,2000,2018-01-06 03:00:00,36.7385472543081,-0.00938394889390753,0 366 | zebra,2000,2018-01-06 04:00:00,36.7403683580682,-0.0095581219260921,0 367 | zebra,2000,2018-01-06 05:00:00,36.7446214954821,-0.00602648027791241,0 368 | zebra,2000,2018-01-06 06:00:00,36.745635801158,-0.00703392238084006,0 369 | zebra,2000,2018-01-06 07:00:00,36.7623787602261,-0.0160157194258233,0 370 | zebra,2000,2018-01-06 08:00:00,36.7765070189943,-0.027570220161839,0 371 | zebra,2000,2018-01-06 09:00:00,36.7756759040283,-0.0267757652987683,0 372 | zebra,2000,2018-01-06 10:00:00,36.7707883703877,-0.0242083775081742,0 373 | zebra,2000,2018-01-06 11:00:00,36.7708581047792,-0.0243392243916117,0 374 | zebra,2000,2018-01-06 12:00:00,36.7719110615412,-0.0236135215462381,0 375 | zebra,2000,2018-01-06 13:00:00,36.7711162268459,-0.0231424069486684,0 376 | zebra,2000,2018-01-06 14:00:00,36.7635843325383,-0.0152527767097002,0 377 | zebra,2000,2018-01-06 15:00:00,36.7683638036381,-0.00401577661623067,0 378 | zebra,2000,2018-01-06 16:00:00,36.7722549278939,-0.00720379302718244,0 379 | zebra,2000,2018-01-06 17:00:00,36.7749745970376,-0.00911967134471642,0 380 | zebra,2000,2018-01-06 18:00:00,36.7895462766267,-0.0106858585497245,0 381 | zebra,2000,2018-01-06 19:00:00,36.7923842086611,-0.0127772305282146,0 382 | zebra,2000,2018-01-06 20:00:00,36.7932325241838,-0.0131794150779491,0 383 | zebra,2000,2018-01-06 21:00:00,36.79392101037,-0.0133227758765808,0 384 | zebra,2000,2018-01-06 22:00:00,36.7925610806353,-0.0137368896394106,0 385 | zebra,2000,2018-01-06 23:00:00,36.7927187134253,-0.0133084232119435,0 386 | zebra,2000,2018-01-07 00:00:00,36.7930539087397,-0.0130806437537059,0 387 | zebra,2000,2018-01-07 01:00:00,36.793096470021,-0.0136246404579519,0 388 | zebra,2000,2018-01-07 02:00:00,36.7945003769789,-0.0191745220939659,0 389 | zebra,2000,2018-01-07 03:00:00,36.8012386607931,-0.0448461567997003,0 390 | zebra,2000,2018-01-07 04:00:00,36.8140229501526,-0.0516836086685849,0 391 | zebra,2000,2018-01-07 05:00:00,36.8100384811497,-0.0375314229586066,0 392 | zebra,2000,2018-01-07 06:00:00,36.8048474980299,-0.0215095299165282,0 393 | zebra,2000,2018-01-07 07:00:00,36.8080276822484,-0.0161034520217684,0 394 | zebra,2000,2018-01-07 08:00:00,36.8188910448486,0.0221971280651351,0 395 | zebra,2000,2018-01-07 09:00:00,36.8164119091742,0.0226939705713218,0 396 | zebra,2000,2018-01-07 10:00:00,36.816415082434,0.0226901508467127,0 397 | zebra,2000,2018-01-07 11:00:00,36.8151613636142,0.0259431772805127,0 398 | zebra,2000,2018-01-07 12:00:00,36.8123251342072,0.0317497583612355,0 399 | zebra,2000,2018-01-07 13:00:00,36.8127623770109,0.034160037105191,0 400 | zebra,2000,2018-01-07 14:00:00,36.8103392063445,0.0348887583000441,0 401 | zebra,2000,2018-01-07 15:00:00,36.8119568867818,0.0357045224044146,0 402 | zebra,2000,2018-01-07 16:00:00,36.8122998182909,0.0360876426691199,0 403 | zebra,2000,2018-01-07 17:00:00,36.8117662984292,0.0366567004266213,0 404 | zebra,2000,2018-01-07 18:00:00,36.8117503416935,0.0366537393062777,0 405 | zebra,2000,2018-01-07 19:00:00,36.8116104273043,0.0370319827653257,0 406 | zebra,2000,2018-01-07 20:00:00,36.8117815338886,0.0368386051927257,0 407 | zebra,2000,2018-01-07 21:00:00,36.811681338741,0.036475333142678,0 408 | zebra,2000,2018-01-07 22:00:00,36.8123846714518,0.0368602854819396,0 409 | zebra,2000,2018-01-07 23:00:00,36.8117212235056,0.0367037234668872,0 410 | zebra,2000,2018-01-08 00:00:00,36.8113230283926,0.0358427324683121,0 411 | zebra,2000,2018-01-08 01:00:00,36.8057057775695,0.0383733293812578,0 412 | zebra,2000,2018-01-08 02:00:00,36.8030343479003,0.0458743171112417,0 413 | zebra,2000,2018-01-08 03:00:00,36.7975846812625,0.0581683741474683,0 414 | zebra,2000,2018-01-08 04:00:00,36.7994010895751,0.0634254084196754,0 415 | zebra,2000,2018-01-08 05:00:00,36.7999249113672,0.0637914374973824,0 416 | zebra,2000,2018-01-08 06:00:00,36.8000427602908,0.0638102442568264,0 417 | zebra,2000,2018-01-08 07:00:00,36.8010402116811,0.0633324628507449,0 418 | zebra,2000,2018-01-08 08:00:00,36.8000909277028,0.0645072744162221,0 419 | zebra,2000,2018-01-08 09:00:00,36.800145285052,0.0652296787651258,0 420 | zebra,2000,2018-01-08 10:00:00,36.7989935539869,0.065516236742048,0 421 | zebra,2000,2018-01-08 11:00:00,36.7686397919286,0.0667060270264067,0 422 | zebra,2000,2018-01-08 12:00:00,36.7649919549954,0.0694726814952057,0 423 | zebra,2000,2018-01-08 13:00:00,36.7622331198681,0.0706967324463625,0 424 | zebra,2000,2018-01-08 14:00:00,36.7531111369625,0.0744837610852679,0 425 | zebra,2000,2018-01-08 15:00:00,36.7527189039984,0.0770856483233054,0 426 | zebra,2000,2018-01-08 16:00:00,36.7519602492108,0.0772364962961772,0 427 | zebra,2000,2018-01-08 17:00:00,36.7494225420789,0.0750086036084102,0 428 | zebra,2000,2018-01-08 18:00:00,36.7485182967781,0.0752711425932714,0 429 | zebra,2000,2018-01-08 19:00:00,36.7491750177978,0.0743813292308651,0 430 | zebra,2000,2018-01-08 20:00:00,36.7502801688177,0.0729429235574219,0 431 | zebra,2000,2018-01-08 21:00:00,36.7500941666102,0.0732459699129125,0 432 | zebra,2000,2018-01-08 22:00:00,36.7489417072166,0.0645568457811012,0 433 | zebra,2000,2018-01-08 23:00:00,36.7510552847292,0.0631287107524091,0 434 | zebra,2000,2018-01-09 00:00:00,36.752065682245,0.0618251641984309,0 435 | zebra,2000,2018-01-09 01:00:00,36.7524890755928,0.0612851392681176,0 436 | zebra,2000,2018-01-09 02:00:00,36.7537913489452,0.0621085752412436,0 437 | zebra,2000,2018-01-09 03:00:00,36.7537132331644,0.0621970947729865,0 438 | zebra,2000,2018-01-09 04:00:00,36.7538207021442,0.0612145600887049,0 439 | zebra,2000,2018-01-09 05:00:00,36.7533603003337,0.0595177439549161,0 440 | zebra,2000,2018-01-09 06:00:00,36.7534891763497,0.0601188387247282,0 441 | zebra,2000,2018-01-09 07:00:00,36.751674838844,0.0652594086147129,0 442 | zebra,2000,2018-01-09 08:00:00,36.7514668147262,0.0646114944205762,0 443 | zebra,2000,2018-01-09 09:00:00,36.7516752210387,0.064677371449951,0 444 | zebra,2000,2018-01-09 10:00:00,36.75200259455,0.0655622754500865,0 445 | zebra,2000,2018-01-09 11:00:00,36.746181595471,0.0634994087817615,0 446 | zebra,2000,2018-01-09 12:00:00,36.7491601783638,0.0651143859951802,0 447 | zebra,2000,2018-01-09 13:00:00,36.7467438361386,0.0629993452076903,0 448 | zebra,2000,2018-01-09 14:00:00,36.7464126521498,0.060153578236955,0 449 | zebra,2000,2018-01-09 15:00:00,36.7539346010416,0.0601424391608285,0 450 | zebra,2000,2018-01-09 16:00:00,36.7734952382322,0.0518291305089545,0 451 | zebra,2000,2018-01-09 17:00:00,36.7770787196993,0.052008125159512,0 452 | zebra,2000,2018-01-09 18:00:00,36.8018770429363,0.0571239605726322,0 453 | zebra,2000,2018-01-09 19:00:00,36.8030893744437,0.0551407789671398,0 454 | zebra,2000,2018-01-09 20:00:00,36.8031299279388,0.0551419824805992,0 455 | zebra,2000,2018-01-09 21:00:00,36.8048745534826,0.0540917754161529,0 456 | zebra,2000,2018-01-09 22:00:00,36.8094163070469,0.0548061763482291,0 457 | zebra,2000,2018-01-09 23:00:00,36.8157377275759,0.0535627198385705,0 458 | zebra,2000,2018-01-10 00:00:00,36.8171337276273,0.053731269513941,0 459 | zebra,2000,2018-01-10 01:00:00,36.8190601592387,0.0528728468503107,0 460 | zebra,2000,2018-01-10 02:00:00,36.8180516567209,0.0500277431008928,0 461 | zebra,2000,2018-01-10 03:00:00,36.8182757315448,0.0496830458448825,0 462 | zebra,2000,2018-01-10 04:00:00,36.8226535287438,0.0475234125021483,0 463 | zebra,2000,2018-01-10 05:00:00,36.8205166579076,0.0497999275363219,0 464 | zebra,2000,2018-01-10 06:00:00,36.8201318438344,0.0474470185659921,0 465 | zebra,2000,2018-01-10 07:00:00,36.8196245418472,0.0422663420530635,0 466 | zebra,2000,2018-01-10 08:00:00,36.8190156967172,0.040683961397941,0 467 | zebra,2000,2018-01-10 09:00:00,36.8174948233262,0.0387780759150123,0 468 | zebra,2000,2018-01-10 10:00:00,36.8179390790062,0.0382576984791627,0 469 | zebra,2000,2018-01-10 11:00:00,36.8169747957604,0.0341743298451911,0 470 | zebra,2000,2018-01-10 12:00:00,36.816714940317,0.0338175193068232,0 471 | zebra,2000,2018-01-10 13:00:00,36.794462097894,0.0336098077736365,0 472 | zebra,2000,2018-01-10 14:00:00,36.7814499866167,0.035966449413532,0 473 | zebra,2000,2018-01-10 15:00:00,36.7691544829462,0.0416878320526481,0 474 | zebra,2000,2018-01-10 16:00:00,36.7706986357659,0.0412643941443984,0 475 | zebra,2000,2018-01-10 17:00:00,36.7729842451766,0.0448947520329075,0 476 | zebra,2000,2018-01-10 18:00:00,36.7735867520244,0.0455170073377776,0 477 | zebra,2000,2018-01-10 19:00:00,36.7709134816765,0.0421375818601944,0 478 | zebra,2000,2018-01-10 20:00:00,36.7558602861017,0.0182482566638816,0 479 | zebra,2000,2018-01-10 21:00:00,36.7711958679541,-0.00440844897415199,0 480 | zebra,2000,2018-01-10 22:00:00,36.7711959632353,-0.00425913269876408,0 481 | zebra,2000,2018-01-10 23:00:00,36.7669579870288,0.00536683163235014,0 482 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021, Internet of Elephants 2 | // All rights reserved. 3 | // 4 | // Redistribution and use in source and binary forms, with or without 5 | // modification, are permitted provided that the following conditions are met: 6 | // 7 | // 1. Redistributions of source code must retain the above copyright notice, this 8 | // list of conditions and the following disclaimer. 9 | // 10 | // 2. Redistributions in binary form must reproduce the above copyright notice, 11 | // this list of conditions and the following disclaimer in the documentation 12 | // and/or other materials provided with the distribution. 13 | // 14 | // 3. Neither the name of the copyright holder nor the names of its 15 | // contributors may be used to endorse or promote products derived from 16 | // this software without specific prior written permission. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | import React, { Component } from 'react'; 30 | import 'mapbox-gl/dist/mapbox-gl.css'; 31 | import './css/app.css'; 32 | import 'react-vis/dist/style.css'; 33 | import { hot } from 'react-hot-loader'; 34 | import Papa from 'papaparse'; 35 | import dayjs from 'dayjs'; 36 | import customParseFormat from 'dayjs/plugin/customParseFormat'; 37 | dayjs.extend(customParseFormat); 38 | import simplify from 'simplify-js'; 39 | import ysFixWebmDuration from 'fix-webm-duration'; 40 | import DeckGL, { WebMercatorViewport, FlyToInterpolator } from 'deck.gl'; 41 | import { StaticMap, MapContext, NavigationControl, ScaleControl, TRANSITION_EVENTS } from 'react-map-gl'; 42 | import renderLayers from './layers.js'; 43 | import Graph from './graph.js'; 44 | import Slider from './slider.js'; 45 | 46 | const MAPBOX_TOKEN = process.env.MAPBOX_TOKEN; 47 | const mapboxLogo = document.getElementsByClassName('mapboxgl-ctrl-bottom-left'); 48 | const mapboxAttrib = document.getElementsByClassName('mapboxgl-ctrl-bottom-right'); 49 | const mapboxAttribIn = document.getElementsByClassName('mapboxgl-ctrl-attrib-inner'); 50 | 51 | let mapView = { 52 | longitude: 0, 53 | latitude: 0, 54 | zoom: 2, 55 | bearing: 0, 56 | pitch: 15, 57 | maxPitch: 85 58 | }; 59 | 60 | //colours for tracks and markers (length should be >= number of species/individuals) 61 | const PALETTE = [[255, 0, 41], [102, 166, 30], [152, 78, 163], [0, 210, 213], [255, 127, 0], [175, 141, 0], [55, 126, 184], [127, 128, 205], [179, 233, 0], [196, 46, 96], [166, 86, 40], [247, 129, 191], [255, 0, 41], [102, 166, 30], [152, 78, 163], [0, 210, 213], [255, 127, 0], [175, 141, 0], [55, 126, 184], [127, 128, 205], [179, 233, 0], [196, 46, 96], [166, 86, 40], [247, 129, 191]]; 62 | 63 | let selectedFile = null; 64 | let dataLoaded = null; 65 | 66 | let species = []; //species for each individual to be plotted 67 | let animalID = []; //unique identification/name of each individual 68 | let datetimes = [[]]; //time stamps of each individual 69 | let realTS = null; //unmodiefied time stamps for displaying in marker tooltip 70 | let coords = [[]]; //coordinates for all tracks and markers 71 | let ids = 0; //total number of individuals 72 | let colour = []; //track colour for each species or individual 73 | let colourHex = []; //for animal list background requires hex 74 | let useColourHex = []; //copy of above for storing changes 75 | let fontColour = []; //for animal list font colour 76 | let useFontColour = []; //copy of above for storing changes 77 | const r = 4; //marker radius 78 | const w = 2; //track width 79 | let minLon = 0; 80 | let maxLon = 0; 81 | let minLat = 0; 82 | let maxLat = 0; 83 | let distance = [[]]; 84 | let maxDist = 0; //for Y axis of graph 85 | let plotData = []; 86 | let seriesOpacity = []; 87 | let minTimestamp = 0; 88 | let uniqueTimes = []; //unique time stamps rounded to nearest interval 89 | let timestamps = []; //unique time stamps as strings for display 90 | let sampleInterval = 0; 91 | let maxTime = 0; //for X axis of graph 92 | let utsLength = 0; //for range slider steps (not using continous time) 93 | let useDates = []; //values of slider ticks 94 | const rsTicks = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]; //for position of ticks on slider 95 | let tailLength = 0; 96 | let speed = 1; 97 | let t0 = 0; //starting time index for timeRange and sliderTime 98 | let t = 0; //time index for drawTracks function (allows skipping time stamps with no data) 99 | let timeRange = [0, 0]; //current and previous time stamp for tracks, markers and graph 100 | let displayTime = [0, 0]; //current time for display 101 | let sliderTime = [0, 0, 0]; 102 | let tracks = []; 103 | let tracksOff = true; 104 | let markers = []; 105 | let markersLength = 0; 106 | let hiddenIDs = []; 107 | 108 | let playback = null; 109 | let playbackState = 0; //for playback button text 110 | let playbackType = 0; //for moving window playback 111 | let maxfps = 60; //animation speed as frames per second, for drawTracks function 112 | let interval = 1000/maxfps; //for drawTracks function 113 | let now = Date.now(); //for drawTracks function 114 | let then = Date.now(); //for drawTracks function 115 | let delta = now - then; //for drawTracks function 116 | 117 | //for video input and output variables and parameters 118 | let recorder, stream, videoURL, videoStartTime; 119 | let recording = false; 120 | const displayMediaOptions = { 121 | video: { 122 | width: { ideal: 1920 }, 123 | height: { ideal: 1080 }, 124 | frameRate: { ideal: 60 } 125 | }, 126 | audio: false 127 | }; 128 | const mediaRecorderOptions = { mimeType : 'video/webm; codecs=vp9' }; 129 | 130 | //sleep function for checkIfDataLoaded function 131 | const sleep = (ms) => { 132 | return new Promise(resolve => setTimeout(resolve, ms)); 133 | } 134 | 135 | //convert rgb to hex (hex required for graph) 136 | const rgbToHex = (rgb) => '#' + rgb.map(x => { 137 | const hex = x.toString(16) 138 | return hex.length === 1 ? '0' + hex : hex 139 | }).join('') 140 | 141 | //calculate distance between consecutive locations 142 | //from https://stackoverflow.com/questions/27928/calculate-distance-between-two-latitude-longitude-points-haversine-formula 143 | const calcDistance = (lat1, lon1, lat2, lon2) => { 144 | const p = 0.017453292519943295; // Math.PI / 180 145 | const c = Math.cos; 146 | const a = 0.5 - c((lat2 - lat1) * p)/2 + c(lat1 * p) * c(lat2 * p) * (1 - c((lon2 - lon1) * p))/2; 147 | return Math.round(12742 * Math.asin(Math.sqrt(a)) * 1000); // 2 * R; R = 6371 km; * 1000 for result in meters 148 | } 149 | 150 | const createMarker = (newTime, i, j) => { 151 | newTime = Math.round((datetimes[i][j] - minTimestamp) / sampleInterval); 152 | let marker = {id:i,species:species[i],animal:animalID[i],colour:colour[i],radius:r,coordinates:coords[i][j],timestamps:newTime, realTS:realTS[i][j]}; 153 | markers.push(marker); 154 | return newTime; 155 | } 156 | 157 | const createTrack = (i, newDistance) => { 158 | let track = {id:i,species:species[i],animal:animalID[i],colour:colour[i],width:w,coordinates:coords[i],timestamps:datetimes[i]}; 159 | tracks.push(track); 160 | //simplify graph data 161 | let simplified = simplify(distance[i], 50, true); 162 | let series = {key:i,data:simplified,color:rgbToHex(colour[i])}; 163 | plotData.push(series); 164 | seriesOpacity.push(0.8); 165 | //animal list background and font colours 166 | colourHex.push(rgbToHex(colour[i])); 167 | fontColour.push('#FFFFFF'); 168 | //find max distance for graph 169 | return maxDist = (newDistance > maxDist) ? newDistance : maxDist; 170 | } 171 | 172 | 173 | class App extends Component { 174 | 175 | state = { 176 | viewport: mapView, 177 | baseMap: 'light-v10', 178 | animals: [], 179 | animalListBGCol: [], 180 | animalListCol: [], 181 | trackData: [], 182 | trackTrail: 0, 183 | trackOpacity: 0.26, 184 | trackVisible: false, 185 | markerData: [], 186 | tsRange: timeRange, 187 | markerOpacity: 0.16, 188 | markerVisible: true, 189 | changeProps: false, 190 | counterTime: '', 191 | counterColour: 'rgb(0, 0, 0)', 192 | counterShadow: '0 0 20px rgb(255, 255, 255)', 193 | progDisplay: 'none', 194 | progFileName: '', 195 | progAnimation: 0, 196 | progDots: [0, 1, 2], 197 | sliderDates: [], 198 | sliderValue: [0, 0, 0], 199 | sliderSteps: 10, 200 | sliderTicks: [], 201 | maxTimeVal: 0, 202 | graphVisible: 'hidden', 203 | graphTitle: 'dist (km)', 204 | graphData: [], 205 | graphSeriesOpacity: [], 206 | graphMaxY: 0, 207 | playButton: 'play', 208 | playTypeButton: 'winOff', 209 | playbackSpeed: speed, 210 | recordButton: 'recOff', 211 | markerButton: 'markersOff', 212 | trackButton:'tracksOn', 213 | graphButton: 'graphOff', 214 | fileInputDisplay: 'flex', 215 | fileInputDisabled: false, 216 | controlsDisplay: 'none', 217 | helpDisplay: 'none', 218 | helpButton: 'helpOff', 219 | blockScreenDisplay: 'none' 220 | }; 221 | 222 | //load data from selected csv file for tracks and graph 223 | loadData = () => { 224 | let that = this; 225 | Papa.parse(selectedFile, { 226 | download: false, 227 | delimeter: ',', 228 | header: true, 229 | skipEmptyLines: true, 230 | dynamicTyping: true, 231 | complete: function(results) { 232 | try { 233 | //check headers 234 | const data = results.data; 235 | const header = results.meta.fields; 236 | if (!header.includes('species') || !header.includes('animal_id') || !header.includes('timestamp') || !header.includes('lon') || !header.includes('lat')) { 237 | dataLoaded = false; 238 | alert('ERROR: The selected file is missing one or more of the required fields, or has incorrect column names. Check the file and try again.'); 239 | return; 240 | } 241 | //check format of first time stamp 242 | let _datetime = dayjs(data[0].timestamp.substring(0, 19), 'YYYY-MM-DD HH:mm:ss', true); 243 | if (!_datetime.isValid()) { 244 | dataLoaded = false; 245 | alert('ERROR: A time stamp is missing or is not in the required format in the first row of data. Check the file and try again.'); 246 | return; 247 | } 248 | //extract all time stamps and coordinates, and add to corresponding variables 249 | let _time = 0; 250 | const _timestamps = []; //all time stamps as numbers 251 | let lon = null; 252 | let lat = null; 253 | const dataLength = data.length; 254 | let x = 0; //index of animal - corresponds to value in animal_id column in csv file 255 | let y = 0; //index within each animal feature 256 | let z = 0; //index for each line within csv file 257 | const altitude = typeof(data[0].alt); 258 | const getAltitude = (z) => { 259 | if (altitude === 'undefined') { 260 | return 0; 261 | } else { 262 | let alt = Math.round(data[z].alt); 263 | if (Number.isFinite(alt)) { 264 | return alt; 265 | } else { 266 | return -1; 267 | } 268 | } 269 | } 270 | //for bounding box and zooming into data 271 | minLon = data[0].lon; 272 | maxLon = data[0].lon; 273 | minLat = data[0].lat; 274 | maxLat = data[0].lat; 275 | //loop through data 276 | while (z < dataLength) { 277 | lon = data[z].lon; 278 | lat = data[z].lat; 279 | if (Number.isFinite(lon) && Number.isFinite(lat)) { 280 | _datetime = dayjs(data[z].timestamp.substring(0, 19)); 281 | _time = dayjs(_datetime).unix(); //unix time (seconds) 282 | datetimes[x].push(_time); //time stamps per individual 283 | _timestamps.push(_time); //all time stamps 284 | coords[x].push([Math.round(lon * 1000000) / 1000000, Math.round(lat * 1000000) / 1000000, getAltitude(z)]); 285 | minLon = (lon < minLon) ? lon : minLon; 286 | maxLon = (lon > maxLon) ? lon : maxLon; 287 | minLat = (lat < minLat) ? lat : minLat; 288 | maxLat = (lat > maxLat) ? lat : maxLat; 289 | } 290 | y++; 291 | //end of data or next individual 292 | if (z + 1 === dataLength) { 293 | species.push(data[z].species); 294 | animalID.push(data[z].animal_id); 295 | break; 296 | } else if (data[z].animal_id !== data[z + 1].animal_id) { 297 | species.push(data[z].species); 298 | animalID.push(data[z].animal_id); 299 | x = x + 1; 300 | coords.push([]); 301 | datetimes.push([]); 302 | distance.push([]); 303 | y = 0; 304 | } 305 | z++; 306 | } 307 | realTS = [...datetimes]; 308 | //number of individuals: for number of tracks, and drawTracks and timeSlider functions 309 | ids = animalID.length; 310 | //define the track colour for each species or individual if only 1 species 311 | const uniqueSpecies = [...new Set(species)]; 312 | const uniqueSpeciesCount = uniqueSpecies.length 313 | if (colour[0] === undefined) { 314 | x = 0; 315 | y = 0; 316 | while (x < ids) { 317 | colour.push(PALETTE[y]); 318 | x++; 319 | if (uniqueSpeciesCount === 1 || species[x] !== species[x - 1]) { 320 | y = y + 1; 321 | } 322 | } 323 | } 324 | //normalise time stamps... 325 | //1. remove duplicated time stamps and sort min to max 326 | const uniqueTimestamps = [...new Set(_timestamps)]; 327 | uniqueTimestamps.sort(function(a, b) { 328 | return a - b; 329 | }); 330 | //2. find sampling interval 331 | //based on median of difference (in seconds) between consecutive locations 332 | const timeDiff = []; 333 | let j = 0; 334 | const maxIndex = uniqueTimestamps.length - 1; 335 | for (let i = 0; i < maxIndex; i++) { 336 | let _timeDiff = uniqueTimestamps[i + 1] - uniqueTimestamps[j]; 337 | //if difference > 59 secs and < 3 hrs, add to timediff variable (for locations recorded in bursts and when there are on/off times) 338 | if (_timeDiff > 59) { 339 | j = i + 1; 340 | // if (_timeDiff <= (3 * 3600)) { 341 | timeDiff.push(_timeDiff); 342 | // } 343 | } 344 | } 345 | timeDiff.sort(function(a, b) { return a - b; }); 346 | const timeDiffLength = timeDiff.length; 347 | if (timeDiffLength % 2 === 0) { //is even, use average of two middle numbers 348 | sampleInterval = (timeDiff[timeDiffLength / 2 - 1] + timeDiff[timeDiffLength / 2]) / 2; 349 | } else { //is odd, use middle number only 350 | sampleInterval = timeDiff[(timeDiffLength - 1) / 2]; 351 | } 352 | //3. round unique time stamps to nearest multiple of sample interval and remove duplicates 353 | minTimestamp = uniqueTimestamps[0]; 354 | utsLength = uniqueTimestamps.length - 1; 355 | for (let i = 0; i <= utsLength; i++) { 356 | uniqueTimestamps[i] = Math.round((uniqueTimestamps[i] - minTimestamp) / sampleInterval); 357 | if (uniqueTimestamps[i] !== uniqueTimestamps[i - 1]) { 358 | uniqueTimes.push(uniqueTimestamps[i]); 359 | } 360 | } 361 | utsLength = uniqueTimes.length - 1; 362 | maxTime = uniqueTimes[utsLength]; 363 | 364 | //4. find expected sequence and check if interpolation is necessary 365 | //... method should work for continuous and non-continuous data 366 | const islands = []; //chunks of continuous time stamps, given the sample interval 367 | const gaps = []; //length of each gap between islands 368 | let startTimes = []; //start time of each island 369 | let _td = 1; 370 | for (let i = 1; i <= utsLength; i++) { 371 | if(uniqueTimes[i] - uniqueTimes[i - 1] === 1){ 372 | _td++; 373 | } else { 374 | islands.push(_td); 375 | _td = 1; 376 | gaps.push((uniqueTimes[i] - uniqueTimes[i - 1]) - 1); 377 | startTimes.push(uniqueTimes[i]); 378 | } 379 | } 380 | //check if any missing values detected 381 | const gapsLength = gaps.length; 382 | if (gapsLength !== 0) { 383 | //find median length of islands, gaps and start times (for non-continuous data) 384 | const sTLength = startTimes.length; 385 | for (let i = 0; i < sTLength; i++) { //remove dates, keep time in seconds 386 | startTimes[i] = ((startTimes[i] * sampleInterval + minTimestamp) - Math.floor((startTimes[i] * sampleInterval + minTimestamp)/86400) * 86400) / sampleInterval; 387 | } 388 | islands.sort(function(a, b) { return a - b; }); 389 | gaps.sort(function(a, b) { return a - b; }); 390 | startTimes.sort(function(a, b) { return a - b; }); 391 | let island = 0; 392 | let gap = 0; 393 | let startTime = 0; 394 | if (sTLength % 2 === 0) { //is even, use average of two middle numbers 395 | island = (islands[sTLength / 2 - 1] + islands[sTLength / 2]) / 2; 396 | gap = (gaps[sTLength / 2 - 1] + gaps[sTLength / 2]) / 2; 397 | startTime = (startTimes[sTLength / 2 - 1] + startTimes[sTLength / 2]) / 2; 398 | } else { //is odd, use middle number only 399 | island = islands[(sTLength - 1) / 2]; 400 | gap = gaps[(sTLength - 1) / 2]; 401 | startTime = startTimes[(sTLength - 1) / 2]; 402 | } 403 | //get expected times... 404 | let expTimes = []; 405 | if ((island + gap) * sampleInterval === 86400) {//based on island, gap and maxTime 406 | let count = 0; 407 | let mgap = 0; 408 | let _val = 0; 409 | while (_val < maxTime) { 410 | for (let i = 0; i < island; i++) { 411 | _val = count + (gap * mgap); 412 | expTimes.push(_val); 413 | count++; 414 | } 415 | mgap++; 416 | } 417 | //check if first time stamp is equal to expected start time (median start time) 418 | const firstStartTime = (minTimestamp - Math.floor(minTimestamp/86400) * 86400) / sampleInterval; 419 | if (firstStartTime != startTime) { 420 | let newST = firstStartTime - startTime; 421 | expTimes.slice(newST); 422 | const newSTLength = newST.length; 423 | for (let i = 0; i < newSTLength; i++) { 424 | expTimes[i] = expTimes[i] - newST; 425 | } 426 | } 427 | } else {//based only on maxTime 428 | for (let i = 0; i < maxTime; i++) { 429 | expTimes.push(i); 430 | } 431 | } 432 | //update unique times 433 | //check if length of expTimes exceeds uniquTimes by more than 50% and throw warning 434 | let expTimesLength = expTimes.length - 1; 435 | if (expTimesLength > utsLength * 2) { 436 | alert('WARNING: Data contains less than 50% of expected time stamps, based on a ' + sampleInterval + ' second sampling interval and a time range of ' + dayjs.unix(minTimestamp).format('YYYY-MM-DD HH:mm:ss') + ' to ' + dayjs.unix(maxTime * sampleInterval + minTimestamp).format('YYYY-MM-DD HH:mm') + '. This could be due to missing data, or to invalid time stamp or location data formats in the file. Consider checking whether all data are in the required formats.'); 437 | } 438 | uniqueTimes = [...expTimes]; 439 | utsLength = uniqueTimes.length - 1; 440 | } 441 | 442 | //create array of time stamps for displaying in counter 443 | for (let i = 0; i <= utsLength; i++) { 444 | timestamps.push(dayjs.unix(uniqueTimes[i] * sampleInterval + minTimestamp).format('YYYY MMM DD HH:mm')); 445 | } 446 | //get dates to display above range slider 447 | let nTicks = rsTicks.length - 1; 448 | let maxTimeSec = maxTime * sampleInterval; 449 | for (let i = 0; i <= nTicks; i++) { 450 | let j = Math.round(rsTicks[i] * utsLength); 451 | if (maxTimeSec <= 86400) { 452 | useDates.push(timestamps[j].slice(12)); 453 | } else if (maxTimeSec <= 31536000) { 454 | useDates.push(timestamps[j].slice(5, 12)); 455 | } else { 456 | useDates.push("'" + timestamps[j].slice(2, 9)); 457 | } 458 | } 459 | 460 | //5. round time stamps per individual to nearest multiple of sample interval 461 | //... interpolate missing time stamps if necessary (based expected time stamps) 462 | //... calculate distance between subsequent non-interpolated locations (for graph) 463 | //... push markers, tracks and graph data to objects 464 | if (gapsLength === 0) { 465 | for (let i = 0; i < ids; i++) { 466 | let newTime = 0; 467 | let newDistance = 0; 468 | let prevDistance = 0; 469 | let dtLength = datetimes[i].length; 470 | for (let j = 0; j < dtLength; j++) { 471 | //push markers and return newTime 472 | newTime = createMarker(newTime, i, j); 473 | datetimes[i][j] = newTime; 474 | //distance 475 | if (j !== 0) { 476 | newDistance = calcDistance(coords[i][j - 1][1], coords[i][j - 1][0], coords[i][j][1], coords[i][j][0]) + prevDistance; 477 | } 478 | distance[i].push({x: newTime, y: newDistance}); 479 | prevDistance = Number(newDistance); 480 | } 481 | //push tracks and graph data, and return max distance for graph 482 | maxDist = createTrack(i, newDistance); 483 | } 484 | } else { 485 | for (let i = 0; i < ids; i++) { 486 | let newTime = 0; 487 | let prevTime = 0; 488 | let tDiff = 0; 489 | let newCoords = []; 490 | let newDistance = 0; 491 | let prevDistance = 0; 492 | let j = 0; 493 | let newJ = 0; 494 | while (j < datetimes[i].length) { 495 | //push markers and return newTime 496 | newTime = createMarker(newTime, i, j); 497 | if (j !== 0) { 498 | //check if any missing datetimes and interpolate (plus coordinates) 499 | tDiff = newTime - prevTime; 500 | newJ = Number(j); 501 | if (tDiff > 1) { 502 | for (let k = 1; k < tDiff; k++) { 503 | let iTime = prevTime + k; 504 | if (uniqueTimes.indexOf(iTime, newJ - 1) > 0) { //for non-continuous sampling 505 | datetimes[i].splice(newJ, 0, iTime); 506 | // newCoords = interPoint(coords[i][newJ - 1][1], coords[i][newJ - 1][0], coords[i][newJ][1], coords[i][newJ][0], 1/(tDiff - (k - 1))); //interpolate 507 | newCoords = [coords[i][newJ - 1][0], coords[i][newJ - 1][1]]; //repeat previous 508 | newCoords.push(coords[i][newJ - 1][2]); //add altitude data 509 | coords[i].splice(newJ, 0, newCoords); 510 | newJ = newJ + 1; 511 | } 512 | } 513 | datetimes[i][newJ] = newTime; 514 | } else { 515 | datetimes[i][j] = newTime; 516 | } 517 | newDistance = calcDistance(coords[i][j - 1][1], coords[i][j - 1][0], coords[i][j][1], coords[i][j][0]) + prevDistance; 518 | j = newJ + 1; 519 | } else { 520 | datetimes[i][j] = newTime; 521 | j = j + 1; 522 | } 523 | distance[i].push({x: newTime, y: newDistance}); 524 | prevDistance = Number(newDistance); 525 | prevTime = Number(newTime); 526 | } 527 | //push tracks and graph data, and return max distance for graph 528 | maxDist = createTrack(i, newDistance); 529 | } 530 | } 531 | //order markers by timestamp instead of id for nicer look when plotted 532 | markers.sort((a, b) => a.timestamps - b.timestamps); 533 | markersLength = markers.length; 534 | displayTime = [timestamps[0], timestamps[0]]; 535 | useColourHex.push(...colourHex); 536 | useFontColour.push(...fontColour); 537 | dataLoaded = true; 538 | } 539 | catch (err){ 540 | dataLoaded = false; 541 | alert('ERROR: Something went wrong when reading the selected file. Check that it has all the required fields and that all data are in the required formats.'); 542 | that.clearData(); 543 | return; 544 | } 545 | } 546 | }); 547 | } 548 | 549 | //open "file browswer window" 550 | inputData = (e) => { 551 | if (e.target.files.length === 0) { 552 | return; 553 | } 554 | if (dataLoaded === true) { 555 | dataLoaded = null; 556 | this.clearData(); 557 | this.clearConsole(); 558 | } 559 | selectedFile = e.target.files[0]; 560 | this.setState({ 561 | fileInputDisabled: true, 562 | fileInputDisplay: 'none', 563 | progFileName: selectedFile.name, 564 | progAnimation: 'infinite', 565 | progDisplay: 'block', 566 | blockScreenDisplay: 'block' 567 | }); 568 | let extension = selectedFile.name.split('.').pop(); 569 | if (extension != 'csv') { 570 | alert('ERROR: Invalid file type. Please select a csv file.'); 571 | this.setState({ 572 | fileInputDisplay: 'flex', 573 | fileInputDisabled: false, 574 | progFileName: '', 575 | progDisplay: 'none', 576 | blockScreenDisplay: 'none' 577 | }); 578 | return; 579 | } else { 580 | this.loadData(); 581 | this.checkIfDataLoaded(); 582 | } 583 | } 584 | 585 | clearData = () => { 586 | species = []; 587 | animalID = []; 588 | datetimes = [[]]; 589 | coords = [[]]; 590 | colour = []; 591 | colourHex = []; 592 | useColourHex = []; 593 | fontColour = []; 594 | useFontColour = []; 595 | distance = [[]]; 596 | maxDist = 0; 597 | plotData = []; 598 | seriesOpacity = []; 599 | uniqueTimes = []; 600 | timestamps = []; 601 | useDates = []; 602 | tailLength = 0; 603 | speed = 1; 604 | t0 = 0; 605 | t = 0; 606 | timeRange = [0, 0]; 607 | displayTime = [0, 0]; 608 | sliderTime = [0, 0, 0]; 609 | tracks = []; 610 | tracksOff = true; 611 | markers = []; 612 | hiddenIDs = []; 613 | playbackType = 0; 614 | } 615 | 616 | clearConsole = () => { 617 | this.setState({ 618 | animals: [], 619 | animalListBGCol: [], 620 | animalListCol: [], 621 | trackData: [], 622 | trackTrail: 0, 623 | markerData: [], 624 | trackVisible: false, 625 | markerVisible: true, 626 | changeProps: false, 627 | counterTime: '', 628 | sliderValue: sliderTime, 629 | playTypeButton: 'winOff', 630 | playbackSpeed: speed, 631 | markerButton: 'markersOff', 632 | trackButton:'tracksOn', 633 | recordButton: 'recOff', 634 | graphButton: 'graphOff', 635 | graphVisible: 'hidden', 636 | controlsDisplay: 'none', 637 | }); 638 | mapboxLogo[0].style.marginBottom = '0px'; 639 | mapboxAttrib[0].style.marginBottom = '0px'; 640 | } 641 | 642 | checkIfDataLoaded = async () => { 643 | while (dataLoaded == null) { 644 | await sleep(100); 645 | } 646 | if (dataLoaded === true) { 647 | this.flyToData(); 648 | } else { 649 | this.setState({ 650 | fileInputDisplay: 'flex', 651 | fileInputDisabled: false, 652 | progFileName: '', 653 | progDisplay: 'none', 654 | blockScreenDisplay: 'none' 655 | }); 656 | dataLoaded = null; 657 | } 658 | } 659 | 660 | flyToData = () => { 661 | const _viewport = new WebMercatorViewport(this.state.viewport); 662 | const { longitude, latitude, zoom } = _viewport.fitBounds([[minLon, minLat], [maxLon, maxLat]], { padding: {top: -390, bottom: -210, left: -700, right: -700} }); 663 | mapView = { 664 | ...this.state.viewport, 665 | longitude, 666 | latitude, 667 | zoom, 668 | transitionDuration: 3500, 669 | transitionInterruption: TRANSITION_EVENTS.IGNORE, 670 | transitionInterpolator: new FlyToInterpolator() 671 | }; 672 | this.setState({ 673 | viewport: mapView, 674 | animals: animalID, 675 | animalListBGCol: colourHex, 676 | animalListCol: fontColour, 677 | trackData: tracks, 678 | markerData: markers, 679 | tsRange: timeRange, 680 | counterTime: displayTime[0] + ' to ' + displayTime[1], 681 | progDisplay: 'none', 682 | progAnimation: 0, 683 | sliderDates: useDates, 684 | sliderSteps: utsLength, 685 | sliderTicks: rsTicks, 686 | maxTimeVal: maxTime, 687 | graphData: plotData, 688 | graphMaxY: maxDist, 689 | graphSeriesOpacity: seriesOpacity, 690 | controlsDisplay: 'flex', 691 | fileInputDisabled: false, 692 | blockScreenDisplay: 'none' 693 | }); 694 | mapboxLogo[0].style.marginBottom = '124px'; 695 | mapboxAttrib[0].style.marginBottom = '124px'; 696 | } 697 | 698 | stopPlaybackAndRec = () => { 699 | if (playbackState === 1) { 700 | playbackState = 0; 701 | cancelAnimationFrame(playback); 702 | this.setState({ playButton: 'play' }); 703 | } 704 | if (recording === true) { 705 | recording = false; 706 | recorder.stop(); 707 | this.setState({ recordButton: 'recOff' }); 708 | stream.getVideoTracks()[0].stop(); 709 | URL.revokeObjectURL(videoURL); 710 | } 711 | } 712 | 713 | startPlotting = () => { 714 | if (playbackState === 1) { 715 | playbackState = 0; 716 | cancelAnimationFrame(playback); 717 | this.setState({ playButton: 'play' }); 718 | } else { 719 | if (playbackState === -1) { 720 | if (playbackType === 0) { 721 | t = 0 - speed; 722 | t0 = 0; 723 | } else { 724 | t = t - (t0 + speed); 725 | t0 = 0 - speed; 726 | } 727 | } 728 | this.setState({ playButton: 'pause' }); 729 | playbackState = 1; 730 | this.drawTracks(); 731 | } 732 | } 733 | 734 | playSpeed = () => { 735 | if (speed === 32) { 736 | speed = 1; 737 | } else { 738 | speed = speed * 2; 739 | } 740 | this.setState({ playbackSpeed: speed }); 741 | } 742 | 743 | playType = () => { 744 | if (playbackType === 0) { 745 | playbackType = 1; 746 | this.setState({ playTypeButton: 'winOn' }); 747 | } else { 748 | playbackType = 0; 749 | this.setState({ playTypeButton: 'winOff' }); 750 | } 751 | } 752 | 753 | startRecording = async () => { 754 | stream = await navigator.mediaDevices.getDisplayMedia(displayMediaOptions); 755 | recorder = new MediaRecorder(stream, mediaRecorderOptions); 756 | this.setState({ recordButton: 'recOn' }); 757 | const chunks = []; 758 | recorder.ondataavailable = (e) => chunks.push(e.data); 759 | recorder.onstop = () => { 760 | let duration = Date.now() - videoStartTime; 761 | const completeBlob = new Blob(chunks, { type: chunks[0].type }); 762 | ysFixWebmDuration(completeBlob, duration, function(fixedBlob) { 763 | videoURL = URL.createObjectURL(fixedBlob); 764 | const videoLink = document.createElement('a'); 765 | videoLink.href = videoURL; 766 | videoLink.download = 'trajVisRec.webm'; 767 | videoLink.click(); //opens the "Save As" dialogue window 768 | }); 769 | }; 770 | recorder.start(); 771 | videoStartTime = Date.now(); 772 | } 773 | 774 | recordScreen = () => { 775 | if (recording !== true) { 776 | recording = true; 777 | this.startRecording(); 778 | } else { 779 | recording = false; 780 | recorder.stop(); 781 | this.setState({ recordButton: 'recOff' }); 782 | stream.getVideoTracks()[0].stop(); 783 | URL.revokeObjectURL(videoURL); 784 | if (playbackState === 1) { 785 | playbackState = 0; 786 | cancelAnimationFrame(playback); 787 | this.setState({ playButton: 'play' }); 788 | } 789 | } 790 | } 791 | 792 | toggleMapStyle = () => { 793 | if (this.state.baseMap === 'light-v10') { 794 | this.setState({ 795 | baseMap: 'dark-v10', 796 | counterColour: 'rgb(255, 255, 255)', 797 | counterShadow: '0 0 20px rgb(0, 0, 0)' 798 | }); 799 | mapboxAttribIn[0].style.color = 'rgb(180, 180, 180)'; 800 | mapboxAttribIn[0].style.textShadow = '0 0 20px rgb(0, 0, 0)'; 801 | } else if (this.state.baseMap === 'dark-v10') { 802 | this.setState({ 803 | baseMap: 'satellite-v9', 804 | trackOpacity: 0.9, 805 | markerOpacity: 0.3 806 | }); 807 | } else { 808 | this.setState({ 809 | baseMap: 'light-v10', 810 | counterColour: 'rgb(0, 0, 0)', 811 | counterShadow: '0 0 20px rgb(255, 255, 255)', 812 | trackOpacity: 0.26, 813 | markerOpacity: 0.16 814 | }); 815 | mapboxAttribIn[0].style.color = 'rgb(80, 80, 80)'; 816 | mapboxAttribIn[0].style.textShadow = '0 0 20px rgb(255, 255, 255)'; 817 | } 818 | } 819 | 820 | showHelp = () => { 821 | if (this.state.helpDisplay === 'none') { 822 | this.setState({ 823 | helpDisplay: 'flex', 824 | helpButton: 'helpOn' 825 | }); 826 | if (playbackState === 1) { 827 | playbackState = 0; 828 | cancelAnimationFrame(playback); 829 | this.setState({ playButton: 'play' }); 830 | } 831 | } else { 832 | this.setState({ 833 | helpDisplay: 'none', 834 | helpButton: 'helpOff' 835 | }); 836 | } 837 | } 838 | 839 | graphVisibility = () => { 840 | if (this.state.graphVisible === 'visible') { 841 | this.setState({ 842 | graphVisible: 'hidden', 843 | graphButton: 'graphOff' 844 | }); 845 | mapboxLogo[0].style.marginBottom = '124px'; 846 | mapboxAttrib[0].style.marginBottom = '124px'; 847 | } else { 848 | this.setState({ 849 | graphVisible: 'visible', 850 | graphButton: 'graphOn' 851 | }); 852 | mapboxLogo[0].style.marginBottom = '244px'; 853 | mapboxAttrib[0].style.marginBottom = '244px'; 854 | } 855 | } 856 | 857 | markerVisibility = () => { 858 | if (this.state.markerVisible === true) { 859 | this.setState({ 860 | markerVisible: false, 861 | markerButton: 'markersOff' 862 | }); 863 | } else { 864 | this.setState({ 865 | markerVisible: true, 866 | markerButton: 'markersOn' 867 | }); 868 | } 869 | } 870 | 871 | trackVisibility = () => { 872 | if (this.state.trackVisible === true) { 873 | this.setState({ 874 | trackVisible: false, 875 | trackButton: 'tracksOff' 876 | }); 877 | } else { 878 | this.setState({ 879 | trackVisible: true, 880 | trackButton: 'tracksOn' 881 | }); 882 | } 883 | } 884 | 885 | toggleAnimals = (e) => { 886 | let selectedID = Number(e.target.getAttribute('id')); 887 | let selectedIndex = hiddenIDs.indexOf(selectedID); 888 | if (selectedIndex >= 0) { 889 | hiddenIDs.splice(selectedIndex, 1); 890 | tracks[selectedID].width = w; //change width back to default 891 | for (let i = 0; i < markersLength; i++) { 892 | if (markers[i].id === selectedID) { 893 | markers[i].radius = r; //change radius back to default 894 | }; 895 | } 896 | seriesOpacity[selectedID] = 0.8; 897 | useColourHex[selectedID] = colourHex[selectedID]; 898 | useFontColour[selectedID] = '#FFFFFF'; 899 | } else { 900 | hiddenIDs.push(selectedID); 901 | tracks[selectedID].width = 0; //change width to 0 902 | for (let i = 0; i < markersLength; i++) { 903 | if (markers[i].id === selectedID) { 904 | markers[i].radius = 0; //change radius to 0 905 | }; 906 | } 907 | seriesOpacity[selectedID] = 0; 908 | useColourHex[selectedID] = '#FFFFFF'; 909 | useFontColour[selectedID] = '#808080'; 910 | } 911 | this.setState({ 912 | changeProps: !this.state.changeProps, 913 | graphSeriesOpacity: seriesOpacity, 914 | animalListBGCol: useColourHex, 915 | animalListCol: useFontColour 916 | }); 917 | } 918 | 919 | // componentDidMount() { 920 | // this.inputData(); 921 | // } 922 | // componentWillUnmount() { 923 | // } 924 | 925 | drawTracks = () => { 926 | playback = requestAnimationFrame(this.drawTracks); 927 | now = Date.now(); 928 | delta = now - then; 929 | if (delta > interval) { 930 | if (playbackType === 1) { 931 | t0 = t0 + speed; 932 | } 933 | t = t + speed; 934 | if (t > utsLength) { 935 | if (playbackType === 1) { 936 | t0 = t0 - (t - utsLength); 937 | } 938 | t = utsLength; 939 | playbackState = -1; 940 | cancelAnimationFrame(playback); 941 | this.setState({ playButton: 'replay' }); 942 | } 943 | timeRange = [uniqueTimes[t0], uniqueTimes[t]]; 944 | tailLength = timeRange[1] - timeRange[0]; 945 | sliderTime = [t0, (t0 + (t - t0)/2), t]; 946 | displayTime = [timestamps[t0], timestamps[t]]; 947 | this.setState({ 948 | trackTrail: tailLength, 949 | tsRange: timeRange, 950 | counterTime: displayTime[0] + ' to ' + displayTime[1], 951 | sliderValue: sliderTime 952 | }); 953 | if (tracksOff === true) { 954 | if (tailLength !== 0) { 955 | tracksOff = false; 956 | this.setState({ 957 | trackVisible: true, 958 | markerVisible: false 959 | }); 960 | } 961 | } 962 | } 963 | then = now - (delta % interval); 964 | } 965 | 966 | rangeSlider = (sliderValue) => { 967 | if (playbackState === 1) { 968 | cancelAnimationFrame(playback); 969 | } 970 | if (sliderValue[2] === utsLength) { 971 | playbackState = -1; 972 | this.setState({ playButton: 'replay' }); 973 | } else { 974 | playbackState = 0; 975 | this.setState({ playButton: 'play' }); 976 | } 977 | this.setState({ sliderValue }); 978 | if (sliderValue[1] != sliderTime[1]) { 979 | let tDiff = Math.round((t - sliderTime[1]) - (t - sliderValue[1])); 980 | let _t0 = t0 + tDiff; 981 | let _t = t + tDiff; 982 | if (_t0 >= 0 && _t < utsLength) { 983 | t0 = _t0; 984 | t = _t; 985 | sliderTime = [t0, sliderValue[1], t]; 986 | } else if (_t0 < 0) { 987 | t = t - t0; 988 | t0 = 0; 989 | sliderTime = [t0, (t0 + (t - t0)/2), t]; 990 | } else if (_t > utsLength) { 991 | t0 = t0 + (utsLength - t); 992 | t = utsLength; 993 | sliderTime = [t0, (t0 + (t - t0)/2), t]; 994 | } 995 | } else if (sliderValue[2] < sliderValue[0] && sliderValue[2] < t) { 996 | t0 = sliderValue[2]; 997 | t = sliderValue[2]; 998 | sliderTime = [t, t, t]; 999 | } else if (sliderValue[0] > sliderValue[2] && sliderValue[0] > t0) { 1000 | t0 = sliderValue[0]; 1001 | t = sliderValue[0]; 1002 | sliderTime = [t0, t0, t0]; 1003 | } else { 1004 | t0 = sliderValue[0]; 1005 | t = sliderValue[2]; 1006 | sliderTime = [t0, (t0 + (t - t0)/2), t]; 1007 | } 1008 | this.setState({ sliderValue: sliderTime }); 1009 | timeRange = [uniqueTimes[t0], uniqueTimes[t]]; 1010 | tailLength = timeRange[1] - timeRange[0]; 1011 | displayTime = [timestamps[t0], timestamps[t]]; 1012 | this.setState({ 1013 | trackTrail: tailLength, 1014 | tsRange: timeRange, 1015 | counterTime: displayTime[0] + ' to ' + displayTime[1] 1016 | }); 1017 | if (tailLength === 0) { 1018 | if (this.state.trackVisible === true && this.state.markerVisible === false) { 1019 | tracksOff = true; 1020 | this.setState({ 1021 | trackVisible: false, 1022 | markerVisible: true 1023 | }); 1024 | } 1025 | } 1026 | if (tracksOff === true) { 1027 | if (tailLength !== 0) { 1028 | tracksOff = false; 1029 | this.setState({ 1030 | trackVisible: true, 1031 | markerVisible: false 1032 | }); 1033 | } 1034 | } 1035 | } 1036 | 1037 | render() { 1038 | const {viewport, baseMap, animals, animalListBGCol, animalListCol, counterTime, counterColour, counterShadow, progDisplay, progFileName, progAnimation, progDots, sliderDates, playButton, playbackSpeed, playTypeButton, recordButton, markerButton, trackButton, graphVisible, graphButton, fileInputDisplay, fileInputDisabled, controlsDisplay, helpDisplay, helpButton, blockScreenDisplay} = this.state; 1039 | 1040 | return ( 1041 |
1042 | object && `species: ${object.species}\n tag: ${object.animal}\n ts: ${dayjs.unix(object.realTS).format('YYYY-MM-DD HH:mm:ss')}\n altitude: ${object.coordinates[2]}`} 1047 | ContextProvider={MapContext.Provider} 1048 | parameters={{ 1049 | depthTest: false 1050 | }} 1051 | > 1052 | 1056 | 1057 |
1058 | 1059 |
1060 |
1061 | 1062 |
1063 |
1064 |
1065 |
1066 |

trajVis: Visualise, animate and create videos of animal movement data from GPS tags.

1067 |

To begin,  1068 | 1071 |  with movement data for a single or multiple individuals.

1072 |

File must contain the following headers: species, animal_id, timestamp [as YYYY-MM-DD HH:MM:SS], lon, lat, and (optional) alt

1073 |

You can also download a sample file to get you started.

1074 |

For feature requests, contributing code or reporting bugs, visit our GitHub page.

1075 |
1076 | 1079 |
1080 |
1081 | 1082 |
1083 |
1084 |
1085 |
1086 |

{counterTime}

1087 |
1088 |
1089 |

File: {progFileName}

1090 |

Status: reading data 1091 | {progDots.map((index) => ( 1092 | . 1093 | ))} 1094 |

1095 |
1096 |
1097 | 1098 | 1099 | 1100 | 1101 | 1102 | 1103 | 1104 | 1105 | 1108 | 1109 |
1110 |
1111 | 1112 |
1113 |
1114 |
1115 | {sliderDates.map((d, index) => ( 1116 |
{d}
1117 | ))} 1118 |
1119 | 1120 |
1121 |
1122 | 1123 |
1124 |
    1125 | {animals.map((name, index) => ( 1126 |
  • {name}
  • 1127 | ))} 1128 |
1129 |
1130 |
1131 |

trajVis: Visualise, animate and create videos of animal movement data from GPS tags

1132 |
1133 |

Control panel:

1134 |
    1135 |
  • Range slider allows moving forward and backward in time, and changing the time window (see below)
  • 1136 |
  • Play / Pause / Replay
  • 1137 |
  • Change playback speed
  • 1138 |
  • Show data incrementaly or create a specific time window
  • 1139 |
  • Record a video (via the Screen Capture API): for best results, put your browser into full screen, and when prompted, select the current tab and click Share. When you stop recording, you will be prompted to save the video as a webm file which can be played directly in your browser
  • 1140 |
  • Show data as points (hover over a point to see its timestamp)
  • 1141 |
  • Show data as lines (on by default)
  • 1142 |
  • Show graph with cumulative distance
  • 1143 |
  • Switch between base maps: light, dark and satellite (Mapbox)
  • 1144 |
  • Load a new dataset (will remove current data)
  • 1145 |
1146 |

Switch lines and points on or off for each individual by clicking the buttons to the right

1147 |
1148 |

For feature requests, contributing code or reporting bugs, visit our GitHub page

1149 |

Copyright (c) 2021, Internet of Elephants

1150 |
1151 |
1152 |
1153 |
1154 | ); 1155 | } 1156 | } 1157 | 1158 | export default hot(module)(App); 1159 | // export default App; 1160 | --------------------------------------------------------------------------------