taxi

Winning entry to the Kaggle taxi competition
git clone https://esimon.eu/repos/taxi.git
Log | Files | Refs | README

commit 1eca8867751df644a62752fbbfbc6a6de849de74
parent bfda3532ea58a48533ceaa417b1bd5c3f5137be3
Author: Étienne Simon <esimon@esimon.eu>
Date:   Mon, 11 May 2015 20:00:04 -0400

Add visualizer.

Lasciate ogni speranza voi ch'entrate:
    I am the bone of my javascript
    DOM is my body and JQuery is my blood
    I have created over a thousand lines
    Unknown to death
    Nor known to life
    Have withstood pain to create many functions
    Yet those hands shall never type anything
    So, as I pray, Unlimited Openlayers Works

Diffstat:
Avisualizer/HTTPServer.py | 128+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Avisualizer/__init__.py | 130+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Avisualizer/extract_all.sh | 1+
Avisualizer/extractor/destinations.py | 19+++++++++++++++++++
Avisualizer/extractor/stands.py | 14++++++++++++++
Avisualizer/extractor/test_positions.py | 12++++++++++++
Avisualizer/extractor/train_poi.py | 21+++++++++++++++++++++
Avisualizer/index.html | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Avisualizer/script.js | 1037+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Avisualizer/style.css | 124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
10 files changed, 1557 insertions(+), 0 deletions(-)

diff --git a/visualizer/HTTPServer.py b/visualizer/HTTPServer.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python + +import os +import sys +import urllib +import SimpleHTTPServer +import SocketServer +from cStringIO import StringIO + +import h5py + +import data +from data.hdf5 import TaxiDataset +from visualizer import Vlist, Path + + +visualizer_path = os.path.join(data.path, 'visualizer') +source_path = os.path.split(os.path.realpath(__file__))[0] + +test_data = None +train_data = None + +class VisualizerHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): + def send_head(self): + spath = self.path.split('?')[0] + path = spath.split('/')[1:] + if len(path) == 1: + if path[0] == '': + path[0] = 'index.html' + file_path = os.path.join(source_path, path[0]) + return self.send_file(file_path) + elif path[0] == 'ls': + return self.send_datalist() + elif path[0] == 'get': + return self.send_file(os.path.join(visualizer_path, spath[5:])) + elif path[0] == 'extract': + return self.send_extract(spath[9:]) + + def send_file(self, file_path): + file_path = urllib.unquote(file_path) + ctype = self.guess_type(file_path) + + try: + f = open(file_path, 'rb') + except IOError: + self.send_error(404, 'File not found') + return None + try: + self.send_response(200) + self.send_header('Content-type', ctype) + fs = os.fstat(f.fileno()) + self.send_header('Content-Length', str(fs[6])) + self.send_header('Last-Modified', self.date_time_string(fs.st_mtime)) + self.end_headers() + return f + except: + f.close() + raise + + def send_datalist(self): + l = [] + for path, subs, files in os.walk(visualizer_path): + for file in files: + mtime = os.stat('%s/%s' % (path, file))[8] + l.append('{"path":["%s"],"name":"%s","mtime":%d}' % ('","'.join(path[len(visualizer_path):].split('/')), file, mtime)) + l.sort() + f = StringIO() + f.write("[") + f.write(','.join(l)) + f.write("]") + length = f.tell() + f.seek(0) + self.send_response(200) + encoding = sys.getfilesystemencoding() + self.send_header("Content-type", "text/html; charset=%s" % encoding) + self.send_header("Content-Length", str(length)) + self.end_headers() + return f + + def send_extract(self, query): + f = StringIO() + query = urllib.unquote(query) + content = Vlist() + for (i,sub) in enumerate(query.split(',')): + r = sub.split('-') + if len(r)==1: + if sub.strip()[0].lower()=='t': + sub=sub.strip()[1:] + content.append(Path(test_data.extract(int(sub)), 'T%s<br>'%sub)) + else: + content.append(Path(train_data.extract(int(sub)), '%s<br>'%sub)) + elif len(r)==2: + test = False + if r[0].strip()[0].lower()=='t': + test = True + r[0]=r[0].strip()[1:] + if r[1].strip()[0].lower()=='t': + r[1]=r[1].strip()[1:] + for i in xrange(int(r[0]), int(r[1])+1): + if test: + content.append(Path(test_data.extract(i), 'T%d<br>'%i)) + else: + content.append(Path(train_data.extract(i), '%d<br>'%i)) + elif len(r)>2: + self.send_error(404, 'File not found') + return None + content.write(f) + length = f.tell() + f.seek(0) + self.send_response(200) + encoding = sys.getfilesystemencoding() + self.send_header("Content-type", "text/html; charset=%s" % encoding) + self.send_header("Content-Length", str(length)) + self.end_headers() + return f + +if __name__ == '__main__': + if len(sys.argv) != 2: + print >>sys.stderr, 'Usage: %s port' % sys.argv[0] + + print >>sys.stderr, 'Loading dataset...', + path = os.path.join(data.path, 'data.hdf5') + train_data = TaxiDataset('train') + test_data = TaxiDataset('test') + print >>sys.stderr, 'done' + + httpd = SocketServer.TCPServer(('', int(sys.argv[1])), VisualizerHTTPRequestHandler) + httpd.serve_forever() diff --git a/visualizer/__init__.py b/visualizer/__init__.py @@ -0,0 +1,130 @@ +import os +import json +import getpass +from datetime import datetime +import itertools + +import numpy + +import data + + +class NumpyEncoder(json.JSONEncoder): + def default(self, o): + if type(o).__module__ == numpy.__name__: + return o.item() + super(NumpyEncoder, self).default(o) + + +class EGJ(object): + def save(self, path=getpass.getuser(), append=False): + path = os.path.join(data.path, 'visualizer', path) + if append: + if not os.path.isdir(path): + raise ValueError("Can't append to the given directory") + name = str(1+max(map(int, filter(str.isdigit, os.listdir(path)))+[-1])) + path = os.path.join(path, name) + else: + while os.path.isdir(path): + path = os.path.join(path, '0') + + with open(path, 'w') as f: + self.write(f) + + def write(self, file): + file.write(json.dumps(self.object(), cls=NumpyEncoder)) + + def type(self): + return 'raw' + + def options(self): + return [] + + def object(self): + return { + 'type': self.type(), + 'data': { + 'type': 'FeatureCollection', + 'crs': { + 'type': 'name', + 'properties': { + 'name': 'urn:ogc:def:crs:OGC:1.3:CRS84' + } + }, + 'features': self.features() + } + } + + +class Point(EGJ): + def __init__(self, latitude, longitude, info=None): + self.latitude = latitude + self.longitude = longitude + self.info = info + + def features(self): + d = { + 'type': 'Feature', + 'geometry': { + 'type': 'Point', + 'coordinates': [self.longitude, self.latitude] + } + } + if self.info is not None: + d['properties'] = { 'info': self.info } + return [d] + + +class Path(EGJ): + def __init__(self, path, info=''): + self.path = path + self.info = info + + def features(self): + info = self.info + '''trip_id: %(trip_id)s<br> + call_type: %(call_type_f)s<br> + origin_call: %(origin_call)d<br> + origin_stand: %(origin_stand)d<br> + taxi_id: %(taxi_id)d<br> + timestamp: %(timestamp_f)s<br> + day_type: %(day_type_f)s<br> + missing_data: %(missing_data)d<br>''' \ + % dict(self.path, + call_type_f = ['central', 'stand', 'street'][self.path['call_type']], + timestamp_f = datetime.fromtimestamp(self.path['timestamp']).strftime('%c'), + day_type_f = ['normal', 'holiday', 'holiday eve'][self.path['day_type']]) + + return [{ + 'type': 'Feature', + 'properties': { + 'info': info, + 'display': 'path', + 'timestamp': self.path['timestamp'] + }, + 'geometry': { + 'type': 'LineString', + 'coordinates': [[lon, lat] for (lat, lon) in zip(self.path['latitude'], self.path['longitude'])] + } + }] + + +class Vlist(EGJ, list): + def __init__(self, cluster=False, heatmap=False, *args): + list.__init__(self, *args) + self.cluster = cluster + self.heatmap = heatmap + + def type(self): + if self.cluster or self.heatmap: + if all(isinstance(c, Point) for c in self): + if self.cluster: + return 'cluster' + elif self.heatmap: + return 'heatmap' + else: + raise ValueError('Building a %s with something that is not a Point' % ('cluster' if self.cluster else 'heatmap')) + else: + return 'raw' + + def features(self): + return list(itertools.chain.from_iterable(p.features() for p in self)) diff --git a/visualizer/extract_all.sh b/visualizer/extract_all.sh @@ -0,0 +1 @@ +find extractor -type f -print -exec {} \; diff --git a/visualizer/extractor/destinations.py b/visualizer/extractor/destinations.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python + +from data.hdf5 import taxi_it +from visualizer import Vlist, Point + + +_sample_size = 5000 + +if __name__ == '__main__': + points = Vlist(cluster=True) + for line in taxi_it('train'): + if len(line['latitude'])>0: + points.append(Point(line['latitude'][-1], line['longitude'][-1])) + if len(points) >= _sample_size: + break + points.save('destinations (cluster)') + points.cluster = False + points.heatmap = True + points.save('destinations (heatmap)') diff --git a/visualizer/extractor/stands.py b/visualizer/extractor/stands.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python + +from data.hdf5 import taxi_it +from visualizer import Vlist, Point + + +if __name__ == '__main__': + it = taxi_it('stands') + next(it) # Ignore the "no stand" entry + + points = Vlist() + for (i, line) in enumerate(it): + points.append(Point(line['stands_latitude'], line['stands_longitude'], 'Stand (%d): %s' % (i+1, line['stands_name']))) + points.save('stands') diff --git a/visualizer/extractor/test_positions.py b/visualizer/extractor/test_positions.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python + +from data.hdf5 import taxi_it +from visualizer import Vlist, Point + + +if __name__ == '__main__': + points = Vlist(heatmap=True) + for line in taxi_it('test'): + for (lat, lon) in zip(line['latitude'], line['longitude']): + points.append(Point(lat, lon)) + points.save('test positions') diff --git a/visualizer/extractor/train_poi.py b/visualizer/extractor/train_poi.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python + +import os + +import data +from data.hdf5 import TaxiDataset +from visualizer import Path + + +poi = { + 'longest': 1492417 +} + +if __name__ == '__main__': + prefix = os.path.join(data.path, 'visualizer', 'Train POI') + if not os.path.isdir(prefix): + os.mkdir(prefix) + + d = TaxiDataset('train') + for (k, v) in poi.items(): + Path(d.extract(v)).save(os.path.join('Train POI', k)) diff --git a/visualizer/index.html b/visualizer/index.html @@ -0,0 +1,71 @@ +<html lang="en"> +<head> + <title>Taxi Visualizer</title> + <meta charset="UTF-8" /> + <link rel="stylesheet" href="http://code.jquery.com/ui/1.11.4/themes/smoothness/jquery-ui.css"> + <link rel="stylesheet" href="http://openlayers.org/en/v3.4.0/css/ol.css"> + <link rel="stylesheet" href="style.css"> + + <script src="http://code.jquery.com/jquery-1.10.2.js"></script> + <script src="http://code.jquery.com/ui/1.11.4/jquery-ui.js"></script> + <script src="http://openlayers.org/en/v3.4.0/build/ol.js"></script> + <script src="script.js"></script> +</head> +<body> + <div id="map" class="map"> + <div id="featureinfo"> + <div id="featureinfo-static"></div> + <div id="featureinfo-dynamic"></div> + </div> + <div id="coordinfo"></div> + </div> + <div id="config"> + <ul> + <li>Measure + <ul id="config-measure"> + <li>Enable</li> + <li style="display: none;">Disable</li> + <li><input type="checkbox" id="measure-euclidean"><label for="measure-euclidean">Euclidean</label></li> + <li><input type="checkbox" id="measure-equirectangular"><label for="measure-equirectangular">Equirectangular</label></li> + <li><input type="checkbox" id="measure-haversine" checked><label for="measure-haversine">Haversine</label></li> + <li>Clear</li> + </ul> + </li> + <li>Base layer + <ul id="config-layer"> + <li>OSM</li> + <li>Bing</li> + <li>Bing (no labels)</li> + </ul> + </li> + <li>Path draw options + <ul id="config-pathdraw"> + <li>No points</li> + <li>Endpoints</li> + <li>All points</li> + <li>Set resolution</li> + </ul> + </li> + <li>Heatmap options + <ul id="config-heatmap"> + <li>Set radius</li> + <li>Set blur</li> + </ul> + </li> + <li style="display: none;">Enable coord</li> + <li>Disable coord</li> + <li>Set player speed</li> + </ul> + </div> + <div id="datalist"> + <div id="datalist-tree"> + <ul></ul> + </div> + <div id="dataextract"> + <input type="text"> + <button type="button">Clear</button> + <button type="button">Refresh</button> + </div> + </div> +</body> +</html> diff --git a/visualizer/script.js b/visualizer/script.js @@ -0,0 +1,1037 @@ +/***************/ +/*** General ***/ +/***************/ + +window.app = {}; +var app = window.app; + +app.mainLayer = new ol.layer.Tile({ source: new ol.source.OSM() }); + + +/****************/ +/*** Geometry ***/ +/****************/ + +app.geometry = {} +app.geometry.REarth = 6371000; +app.geometry.toRadians = function(x){ return x * Math.PI / 180; }; + +app.geometry.haversine = function(lat1, lon1, lat2, lon2){ + var lat1 = app.geometry.toRadians(lat1); + var lon1 = app.geometry.toRadians(lon1); + var lat2 = app.geometry.toRadians(lat2); + var lon2 = app.geometry.toRadians(lon2); + + var dlat = Math.abs(lat1-lat2); + var dlon = Math.abs(lon1-lon2); + + var alpha = Math.pow(Math.sin(dlat/2), 2) + Math.cos(lat1) * Math.cos(lat2) * Math.pow(Math.sin(dlon/2), 2); + var d = Math.atan2(Math.sqrt(alpha), Math.sqrt(1-alpha)); + + return 2 * app.geometry.REarth * d; +}; + +app.geometry.equirectangular = function(lat1, lon1, lat2, lon2){ + var lat1 = app.geometry.toRadians(lat1); + var lon1 = app.geometry.toRadians(lon1); + var lat2 = app.geometry.toRadians(lat2); + var lon2 = app.geometry.toRadians(lon2); + var x = (lon2-lon1) * Math.cos((lat1+lat2)/2); + var y = (lat2-lat1); + return Math.sqrt(x*x + y*y) * app.geometry.REarth; +}; + + +/***************/ +/*** Measure ***/ +/***************/ + +app.measure = {}; +app.measure.tooltip_list = []; + +app.measure.source = new ol.source.Vector(); + +app.measure.layer = new ol.layer.Vector({ + source: app.measure.source, + style: new ol.style.Style({ + fill: new ol.style.Fill({ + color: 'rgba(255, 255, 255, 0.2)' + }), + stroke: new ol.style.Stroke({ + color: '#FC3', + width: 2 + }), + image: new ol.style.Circle({ + radius: 7, + fill: new ol.style.Fill({ + color: '#FC3' + }) + }) + }) +}); + +app.measure.pointerMoveHandler = function(evt){ + if(evt.dragging){ return; } + var tooltipCoord = evt.coordinate; + + if(app.measure.sketch){ + var output; + var geom = (app.measure.sketch.getGeometry()); + if(geom instanceof ol.geom.LineString){ + output = app.measure.formatLength((geom)); + tooltipCoord = geom.getLastCoordinate(); + } + app.measure.tooltipElement.innerHTML = output; + app.measure.tooltip.setPosition(tooltipCoord); + } +}; + +app.measure.addInteraction = function(){ + app.measure.draw = new ol.interaction.Draw({ + source: app.measure.source, + type: ('LineString'), + style: new ol.style.Style({ + fill: new ol.style.Fill({ + color: 'rgba(255, 255, 255, 0.2)' + }), + stroke: new ol.style.Stroke({ + color: 'rgba(0, 0, 0, 0.5)', + lineDash: [10, 10], + width: 2 + }), + image: new ol.style.Circle({ + radius: 5, + stroke: new ol.style.Stroke({ + color: 'rgba(0, 0, 0, 0.7)' + }), + fill: new ol.style.Fill({ + color: 'rgba(255, 255, 255, 0.2)' + }) + }) + }) + }); + app.map.addInteraction(app.measure.draw); + + app.measure.createTooltip(); + + app.measure.draw.on('drawstart', + function(evt){ + app.measure.sketch = evt.feature; + }, this); + + app.measure.draw.on('drawend', + function(evt){ + app.measure.tooltipElement.className = 'measure-tooltip measure-tooltip-static'; + app.measure.tooltip.setOffset([0, -7]); + app.measure.sketch = null; + app.measure.tooltipElement = null; + app.measure.createTooltip(); + }, this); +}; + +app.measure.createTooltip = function(){ + if(app.measure.tooltipElement){ + app.measure.tooltipElement.parentNode.removeChild(app.measure.tooltipElement); + } + app.measure.tooltipElement = document.createElement('div'); + app.measure.tooltipElement.className = 'measure-tooltip measure-tooltip-value'; + app.measure.tooltip = new ol.Overlay({ + element: app.measure.tooltipElement, + offset: [0, -15], + positioning: 'bottom-center' + }); + app.measure.tooltip_list.push(app.measure.tooltip); + app.map.addOverlay(app.measure.tooltip); +}; + +app.measure.formatLength = function(line){ + var length_euclidean = line.getLength(); + var length_equirectangular = 0; + var length_haversine = 0; + var coordinates = line.getCoordinates(); + var sourceProj = app.map.getView().getProjection(); + for(var i = 0, ii = coordinates.length - 1; i < ii; ++i){ + var c1 = ol.proj.transform(coordinates[i], sourceProj, 'EPSG:4326'); + var c2 = ol.proj.transform(coordinates[i + 1], sourceProj, 'EPSG:4326'); + length_equirectangular += app.geometry.equirectangular(c1[1], c1[0], c2[1], c2[0]); + length_haversine += app.geometry.haversine(c1[1], c1[0], c2[1], c2[0]); + } + + var disp = function(x){ + if(x > 100){ + return Math.round(x / 1000 * 1000) / 1000 + 'km'; + } else { + return Math.round(x * 1000) / 1000 + 'm'; + } + } + + var length_euclidean = disp(length_euclidean); + var length_equirectangular = disp(length_equirectangular); + var length_haversine = disp(length_haversine); + + var display_euclidean = $('input#measure-euclidean').prop('checked'); + var display_equirectangular = $('input#measure-equirectangular').prop('checked'); + var display_haversine = $('input#measure-haversine').prop('checked'); + + var header = true; + if(display_euclidean + display_equirectangular + display_haversine == 1){ + header = false; + } + + var str = ''; + if(display_euclidean){ + if(header){ str += 'euclidean: '; } + str += length_euclidean; + } + if(display_equirectangular){ + if(header){ if(display_euclidean){ str += '<br>'; } str += 'equirectangular: '; } + str += length_equirectangular; + } + if(display_haversine){ + if(header){ str += '<br> haversine: '; } + str += length_haversine; + } + return str; +}; + + +/*******************/ +/*** DataDisplay ***/ +/*******************/ + +app.dataDisplay = {}; +app.dataDisplay.layers = {}; +app.dataDisplay.heatmapRadius = 5; +app.dataDisplay.heatmapBlur = 5; +app.dataDisplay.pathPointMode = 1; // endpoints +app.dataDisplay.pathPointResolution = 50; + +app.dataDisplay.loadLayer = function(path){ + $.ajax({url: path, cache: false, dataType: 'json', + success: function(result){ + app.dataDisplay.layers[path] = app.dataDisplay.preprocess(result); + app.map.addLayer(app.dataDisplay.layers[path]); + } + }); +}; + +app.dataDisplay.unloadLayer = function(path){ + app.map.removeLayer(app.dataDisplay.layers[path]); + delete app.dataDisplay.layers[path]; +}; + +app.dataDisplay.rawStyle = function(feature, resolution){ + var style = [ new ol.style.Style({ + stroke: new ol.style.Stroke({ + color: '#00F', + width: 5 + }), + image: new ol.style.Circle({ + radius: 5, + fill: new ol.style.Fill({ + color: '#00F' + }) + }) + }), + new ol.style.Style({ + stroke: new ol.style.Stroke({ + color: '#000', + width: 2 + }), + image: new ol.style.Circle({ + radius: 2, + fill: new ol.style.Fill({ + color: '#FFF' + }) + }) + }) + ]; + + if(feature.get('display') == 'path' && resolution < app.dataDisplay.pathPointResolution){ + if(app.dataDisplay.pathPointMode == 2){ + var polyline = feature.getGeometry(); + var points = polyline.getCoordinates(); + for(var i=1; i<points.length-1; ++i){ + var point = points[i]; + var pos = i/points.length; + var red = 0; + var green = 0; + if(pos < 0.5){ + green = 255; + red = Math.round(pos*2*255); + } else { + red = 255; + green = Math.round((1-pos)*2*255); + } + style.push(new ol.style.Style({ + geometry: new ol.geom.Point(point), + image: new ol.style.Circle({ + radius: 3, + fill: new ol.style.Fill({ + color: 'rgb('+red+','+green+',0)' + }) + }) + })); + } + } + if(app.dataDisplay.pathPointMode >= 1){ + var polyline = feature.getGeometry(); + var first = polyline.getFirstCoordinate(); + var last = polyline.getLastCoordinate(); + style.push(new ol.style.Style({ + geometry: new ol.geom.Point(first), + image: new ol.style.Circle({ + radius: 5, + fill: new ol.style.Fill({ + color: '#0F0' + }) + }) + })); + style.push(new ol.style.Style({ + geometry: new ol.geom.Point(last), + image: new ol.style.Circle({ + radius: 5, + fill: new ol.style.Fill({ + color: '#F00' + }) + }) + })); + } + } + + return style; +}; + +app.dataDisplay.clusterStyleCache = {}; +app.dataDisplay.clusterStyle = function(feature, resolution){ + var size = feature.get('features').length; + var style = app.dataDisplay.clusterStyleCache[size]; + if(!style){ + style = [new ol.style.Style({ + image: new ol.style.Circle({ + radius: 10, + stroke: new ol.style.Stroke({ + color: '#FFF' + }), + fill: new ol.style.Fill({ + color: '#39C' + }) + }), + text: new ol.style.Text({ + text: size.toString(), + fill: new ol.style.Fill({ + color: '#FFF' + }) + }) + })]; + app.dataDisplay.clusterStyleCache[size] = style; + } + return style; +}; + +app.dataDisplay.preprocess = function(egj){ + var source = new ol.source.GeoJSON({ + projection: 'EPSG:3857', + object: egj.data + }); + + if(egj.type == 'raw'){ + return new ol.layer.Vector({ + source: source, + style: app.dataDisplay.rawStyle + }); + + } else if(egj.type == 'cluster'){ + return new ol.layer.Vector({ + source: new ol.source.Cluster({ + distance: 40, + source: source + }), + style: app.dataDisplay.clusterStyle + }); + + } else if(egj.type == 'heatmap'){ + return new ol.layer.Heatmap({ + source: source, + blur: app.dataDisplay.heatmapBlur, + radius: app.dataDisplay.heatmapRadius + }); + } +}; + +app.dataDisplay.reloadPathes = function(){ + for(var layer in app.dataDisplay.layers){ + if(app.dataDisplay.layers[layer].getSource().getFeatures()[0].get('display') == 'path'){ + app.dataDisplay.layers[layer].changed(); + } + } +}; + +app.dataDisplay.reloadHeatmaps = function(){ + for(var key in app.dataDisplay.layers){ + var layer = app.dataDisplay.layers[key]; + if(layer instanceof ol.layer.Heatmap){ + layer.setBlur(app.dataDisplay.heatmapBlur); + layer.setRadius(app.dataDisplay.heatmapRadius); + } + } +}; + + +/****************/ +/*** DataList ***/ +/****************/ + +app.dataList = {}; +app.dataList.current = {}; +app.dataList.idgen = 0; + +app.dataList.init = function(){ + app.dataList.elementTree = {}; + app.dataList.elementTree.parent = null; + app.dataList.elementTree.children = {}; + app.dataList.elementTree.checkbox = null; + app.dataList.elementTree.ul = $('#datalist-tree ul'); + + app.dataList.updateList(); + setInterval(app.dataList.updateList, 1000); +}; + +app.dataList.updateList = function(){ + $.ajax({url: '/ls/', cache: false, dataType: 'json', + success: function(result){ + result.forEach(function(file){ + file.uri = file.path.join('/') + '/' + file.name + if(file.uri in app.dataList.current){ + if(file.mtime > app.dataList.current[file.uri].mtime){ + var act = app.dataList.current[file.uri]; + if(act.checkbox.prop('checked')){ + app.dataList.unloadLayer(file.uri); + app.dataList.loadLayer(file.uri); + } + act.mtime = file.mtime; + } + } else { + app.dataList.insert(file); + } + }); + } + }); +}; + +app.dataList.insert = function(file){ + var cur = app.dataList.elementTree; + var prev = null; + for(var i = 1; i<file.path.length; i++){ + if(!(file.path[i] in cur.children)){ + var n = {}; + n.uri = file.path.slice(0, i+1).join('/'); + n.children = {}; + n.parent = cur; + n.ul = $('<ul>') + .prop('id', 'folder-'+app.dataList.idgen) + .hide(); + + var hidelink = $('<a>') + .prop('href', '') + .append('hide') + .hide(); + var showlink = $('<a>') + .prop('href', '') + .append('show'); + + var playlink = $('<a>') + .prop('href', '') + .append('play'); + var stoplink = $('<a>') + .prop('href', '') + .append('stop') + .hide(); + + n.checkbox = $('<input>') + .prop('type', 'checkbox') + .prop('id', 'data-'+app.dataList.idgen) + .prop('name', n.uri); + n.checkbox.change(app.dataList.selectData); + var item = $('<li>') + .append(n.checkbox) + .append($('<label>') + .prop('for', 'data-'+app.dataList.idgen) + .append(file.path[i])) + .append(' ') + .append(hidelink) + .append(showlink) + .append(' ') + .append(playlink) + .append(stoplink) + .append(n.ul) + app.dataList.idgen++; + cur.ul.append(item); + cur.children[file.path[i]] = n; + app.dataList.current[n.uri] = n; + + var foldertoggler = function(){ + hidelink.toggle(); + showlink.toggle(); + n.ul.toggle(); + return false; + }; + hidelink.click(foldertoggler); + showlink.click(foldertoggler); + + playlink.click(function(){ + playlink.toggle(); + stoplink.toggle(); + app.dataPlayer.play(n); + return false; + }); + stoplink.click(function(){ + playlink.toggle(); + stoplink.toggle(); + app.dataPlayer.stop(n); + return false; + }); + } + prev = cur; + cur = cur.children[file.path[i]]; + } + + file.parent = cur; + file.checkbox = $('<input>') + .prop('type', 'checkbox') + .prop('id', 'data-'+app.dataList.idgen) + .prop('name', file.uri); + file.checkbox.change(app.dataList.selectData); + var item = $('<li>') + .append(file.checkbox) + .append($('<label>') + .prop('for', 'data-'+app.dataList.idgen) + .append(file.name)) + app.dataList.idgen++; + cur.ul.append(item); + cur.children[file.name] = file; + app.dataList.current[file.uri] = file; + + if(cur.checkbox && cur.checkbox.prop('checked')){ + file.checkbox.prop('checked', true); + app.dataList.updateData(file); + } +}; + +app.dataList.updateData = function(cur){ + if(cur.checkbox.prop('checked')){ + app.dataList.loadLayer(cur.uri); + } else { + app.dataList.unloadLayer(cur.uri); + } +}; + +app.dataList.updateCheckboxes = function(cur){ + if(cur.checkbox.prop('checked')){ + app.dataList.check(cur); + } else { + app.dataList.uncheck(cur); + } +}; + +app.dataList.selectData = function(e){ + var cur = app.dataList.current[e.target.name]; + if(!('children' in cur)){ + app.dataList.updateData(cur); + } + app.dataList.updateCheckboxes(cur); +}; + +app.dataList.changeChildren = function rec(cur, val){ + cur.checkbox.prop('checked', val); + if('children' in cur){ + for(var child in cur.children){ + rec(cur.children[child], val); + } + } else { + app.dataList.updateData(cur); + } +}; + +app.dataList.check = function(cur){ + // Check all parents + var p = cur.parent; + while(p.checkbox != null){ + p.checkbox.prop('checked', true); + p = p.parent; + } + + // Check all children + for(var child in cur.children){ + app.dataList.changeChildren(cur.children[child], true); + } +}; + +app.dataList.uncheck = function(cur){ + // Uncheck empty parents + var p = cur.parent; + while(p.checkbox != null && p.checkbox.prop('checked')){ + var cc = false; + for(var child in p.children){ + cc = cc || p.children[child].checkbox.prop('checked'); + } + if(cc){ + break; + } + p.checkbox.prop('checked', false); + p = p.parent; + } + + // Uncheck all children + for(var child in cur.children){ + app.dataList.changeChildren(cur.children[child], false); + } +}; + + +app.dataList.loadLayer = function(uri){ + app.dataDisplay.loadLayer('/get'+uri); +}; + +app.dataList.unloadLayer = function(uri){ + app.dataDisplay.unloadLayer('/get'+uri); +}; + + +/*******************/ +/*** DataExtract ***/ +/*******************/ + +app.dataExtract = {}; +app.dataExtract.current = null; + +app.dataExtract.init = function(){ + $('#dataextract button:contains("Refresh")').click(app.dataExtract.display); + $('#dataextract input').keypress(function(e){ + if(e.keyCode == 13){ + app.dataExtract.display(); + } + }); + $('#dataextract button:contains("Clear")').click(app.dataExtract.clear); +}; + +app.dataExtract.display = function(){ + if(app.dataExtract.current){ + app.dataDisplay.unloadLayer('/extract/' + app.dataExtract.current); + } + app.dataExtract.current = $('#dataextract input').val(); + app.dataDisplay.loadLayer('/extract/' + app.dataExtract.current); +}; + +app.dataExtract.clear = function(){ + if(app.dataExtract.current){ + app.dataDisplay.unloadLayer('/extract/' + app.dataExtract.current); + app.dataExtract.current = null; + } +}; + + + +/******************/ +/*** DataPlayer ***/ +/******************/ + +app.dataPlayer = {}; +app.dataPlayer.current = {}; +app.dataPlayer.updateFrequency = 200; +app.dataPlayer.time = 0; + +app.dataPlayer.init = function(){ + app.dataPlayer.intervalId = setInterval(app.dataPlayer.update, app.dataPlayer.updateFrequency); +}; + +app.dataPlayer.updateInterval = function(){ + clearInterval(app.dataPlayer.intervalId); + app.dataPlayer.intervalId = setInterval(app.dataPlayer.update, app.dataPlayer.updateFrequency); +}; + +app.dataPlayer.play = function(cur){ + app.dataPlayer.updateKeys(cur); + if(cur.keys.length == 0){ + alert("ERROR: No number in directory."); + return; + } + app.dataList.uncheck(cur); + cur.checkbox.prop('checked', true); + cur.playIndex = 0; + app.dataPlayer.current[cur.uri] = cur; + for(var key in cur.context){ + var child = cur.children[cur.context[key]]; + child.checkbox.prop('checked', true); + if(!('children' in child)){ + app.dataList.updateData(child); + } + app.dataList.updateCheckboxes(child); + } +}; + +app.dataPlayer.updateKeys = function(cur){ + var keys = Object.keys(cur.children); + cur.keys = keys.map(Number).filter(function(x){ return !isNaN(x); }).sort(function(l,r){return l-r;}); + cur.context = keys.filter(function(x){ return isNaN(Number(x)); }); +}; + +app.dataPlayer.stop = function(cur){ + delete app.dataPlayer.current[cur.uri]; + delete cur.keys; + delete cur.context; + delete cur.playIndex; +}; + +app.dataPlayer.update = function(){ + for(var key in app.dataPlayer.current){ + var cur = app.dataPlayer.current[key]; + var prev = cur.children[cur.keys[cur.playIndex]]; + cur.playIndex++; + if(cur.playIndex >= cur.keys.length){ + app.dataPlayer.updateKeys(cur); + if(cur.playIndex >= cur.keys.length){ + cur.playIndex = 0; + } + } + var next = cur.children[cur.keys[cur.playIndex]]; + + prev.checkbox.prop('checked', false); + app.dataList.updateData(prev); + next.checkbox.prop('checked', true); + app.dataList.updateData(next); + } +}; + + +/*****************/ +/*** CoordInfo ***/ +/*****************/ + +app.coordInfo = {}; +app.coordInfo.init = function(){ + app.coordInfo.element = $('#coordInfo'); + app.coordInfo.enable(); +}; + +app.coordInfo.enable = function(){ + app.map.on('pointermove', app.coordInfo.update); + app.coordInfo.element.show(); +}; + +app.coordInfo.disable = function(){ + app.coordInfo.element.hide(); + app.map.un('pointermove', app.coordInfo.update); +}; + +app.coordInfo.update = function(evt){ + var coord = ol.proj.transform(app.map.getEventCoordinate(evt.originalEvent), app.map.getView().getProjection(), 'EPSG:4326'); + app.coordInfo.element.text(coord[1] + ', ' + coord[0]); +}; + + +/*******************/ +/*** FeatureInfo ***/ +/*******************/ + +app.featureInfo = {}; + +app.featureInfo.init = function(){ + app.featureInfo.element = $('#featureinfo'); + app.featureInfo.static = $('#featureinfo-static'); + app.featureInfo.dynamic = $('#featureinfo-dynamic'); + app.featureInfo.element.hide(); + app.featureInfo.enable(); +}; + +app.featureInfo.enable = function(){ + app.map.on('pointermove', app.featureInfo.updateMove); + app.map.on('click', app.featureInfo.updateClick); +}; + +app.featureInfo.disable = function(){ + app.map.un('pointermove', app.featureInfo.updateMove); + app.map.un('click', app.featureInfo.updateClick); +}; + +app.featureInfo.updateMove = function(evt){ + if(evt.dragging){ + app.featureInfo.element.hide(); + return; + } + app.featureInfo.display(evt); +}; + +app.featureInfo.updateClick = function(evt){ + app.featureInfo.display(evt); +}; + +app.featureInfo.display = function(evt){ + app.featureInfo.element.css({ + left: (evt.pixel[0] + 10) + 'px', + top: evt.pixel[1] + 'px' + }); + var feature = app.map.forEachFeatureAtPixel(evt.pixel, function(feature, layer) { + return feature; + }); + if(feature && feature.get('info')){ + if(feature.get('display') == 'path'){ + var dtime = app.featureInfo.interpolateTime(feature.getGeometry(), evt.coordinate); + var date = new Date(1000*(feature.get('timestamp') + dtime*15)); + var desc = 'index: '+dtime+'<br>'; + desc += 'date: '+date.toISOString()+'<br>'; + app.featureInfo.dynamic.html(desc); + } else { + app.featureInfo.dynamic.html(''); + } + app.featureInfo.static.html(feature.get('info')); + app.featureInfo.element.show(); + } else { + app.featureInfo.element.hide(); + } +}; + +app.featureInfo.interpolateTime = function(polyline, coord){ + var closest = polyline.getClosestPoint(coord); + var best = 1000; + var bestStart = -1; + var bestEnd = -1; + var bestI = -1; + var i = 0; + var points = polyline.getCoordinates(); + for(var i=0; i<points.length-1; i++){ + var start = points[i]; + var end = points[i+1]; + var dist = Math.abs((end[0]-start[0])*closest[1] - (end[1]-start[1])*closest[0] + end[1]*start[0] - end[0]*start[1]) / Math.sqrt(Math.pow(end[0]-start[0], 2) + Math.pow(end[1]-start[1], 2)); + if(dist<best){ + best = dist; + bestStart = start; + bestEnd = end; + bestI = i; + } + } + + if(bestI == -1){ + return 0; + } + + var distClosest = app.geometry.equirectangular(bestStart[1], bestStart[0], closest[1], closest[0]); + var distEnd = app.geometry.equirectangular(bestStart[1], bestStart[0], bestEnd[1], bestEnd[0]); + var ratio = distClosest / distEnd; + return bestI + ratio; +}; + + +/***************/ +/*** Control ***/ +/***************/ + +app.control = {}; + +app.control.OpenConfigControl = function(opt_options){ + var options = opt_options || {}; + + var button = document.createElement('button'); + button.innerHTML = '⚙'; + + button.addEventListener('click', function(e){ $('#config').toggle() }, false); + button.addEventListener('touchstart', function(e){ $('#config').toggle() }, false); + + var element = document.createElement('div'); + element.className = 'open-config ol-unselectable ol-control'; + element.appendChild(button); + + ol.control.Control.call(this, { + element: element, + target: options.target + }); +}; +ol.inherits(app.control.OpenConfigControl, ol.control.Control); + +app.control.OpenDatalist = function(opt_options){ + var options = opt_options || {}; + + var button = document.createElement('button'); + button.innerHTML = '«'; + + var toggler = function(e){ + $('#datalist').toggle(); + if(button.innerHTML == '«'){ + button.innerHTML = '»'; + $('.open-datalist').css('right', '20.5em'); + } + else{ + button.innerHTML = '«'; + $('.open-datalist').css('right', '.5em'); + } + }; + button.addEventListener('click', toggler, false); + button.addEventListener('touchstart', toggler, false); + + var element = document.createElement('div'); + element.className = 'open-datalist ol-unselectable ol-control'; + element.appendChild(button); + + ol.control.Control.call(this, { + element: element, + target: options.target + }); +}; +ol.inherits(app.control.OpenDatalist, ol.control.Control); + + +/************/ +/*** Menu ***/ +/************/ + +app.menu = {}; + +app.menu.init = function(){ + $('#config ul').menu(); + + $('#config ul li').click(function(e){ + switch($(this).text()){ + case 'Enable coord': + app.coordInfo.enable(); + $('#config ul li:contains("Enable coord")').toggle(); + $('#config ul li:contains("Disable coord")').toggle(); + break; + case 'Disable coord': + app.coordInfo.disable(); + $('#config ul li:contains("Enable coord")').toggle(); + $('#config ul li:contains("Disable coord")').toggle(); + break; + case 'Set player speed': + var tmp = prompt("Player update frequency (milliseconds)", app.dataPlayer.updateFrequency); + if(tmp){ + app.dataPlayer.updateFrequency = parseInt(tmp); + app.dataPlayer.updateInterval(); + } + break; + } + }); + + $('ul#config-measure li').click(function(e){ + switch($(this).text()){ + case 'Enable': + app.measure.addInteraction(); + app.map.on('pointermove', app.measure.pointerMoveHandler); + app.featureInfo.disable(); + $('ul#config-measure li:contains("Enable")').toggle(); + $('ul#config-measure li:contains("Disable")').toggle(); + break; + case 'Disable': + app.featureInfo.enable(); + app.map.un('pointermove', app.measure.pointerMoveHandler); + app.map.removeInteraction(app.measure.draw); + app.measure.draw = null; + $('ul#config-measure li:contains("Enable")').toggle(); + $('ul#config-measure li:contains("Disable")').toggle(); + break; + case 'Clear': + app.measure.source.clear(); + app.measure.tooltip_list.forEach(function(e){ + app.map.removeOverlay(e); + }); + app.measure.tooltip_list.length = 0; + if(app.measure.draw){ + app.map.removeInteraction(app.measure.draw); + app.measure.addInteraction(); + } + break; + } + }); + + $('ul#config-layer li').click(function(e){ + switch($(this).text()){ + case 'OSM': + app.mainLayer = new ol.layer.Tile({ source: new ol.source.OSM() }); + break; + case 'Bing': + app.mainLayer = new ol.layer.Tile({ source: new ol.source.BingMaps({ + key: 'Ak-dzM4wZjSqTlzveKz5u0d4IQ4bRzVI309GxmkgSVr1ewS6iPSrOvOKhA-CJlm3', + imagerySet: 'AerialWithLabels', + maxZoom: 19 + }) }); + break; + case 'Bing (no labels)': + app.mainLayer = new ol.layer.Tile({ source: new ol.source.BingMaps({ + key: 'Ak-dzM4wZjSqTlzveKz5u0d4IQ4bRzVI309GxmkgSVr1ewS6iPSrOvOKhA-CJlm3', + imagerySet: 'Aerial', + maxZoom: 19 + }) }); + break; + } + app.map.getLayers().setAt(0, app.mainLayer); + }); + + $('ul#config-pathdraw li').click(function(e){ + switch($(this).text()){ + case 'Set resolution': + var tmp = prompt("Path points resolution", app.dataDisplay.pathPointResolution); + if(tmp){ + app.dataDisplay.pathPointResolution = parseInt(tmp); + app.dataDisplay.reloadPathes(); + } + break; + case 'No points': + app.dataDisplay.pathPointMode = 0; + app.dataDisplay.reloadPathes(); + break; + case 'Endpoints': + app.dataDisplay.pathPointMode = 1; + app.dataDisplay.reloadPathes(); + break; + case 'All points': + app.dataDisplay.pathPointMode = 2; + app.dataDisplay.reloadPathes(); + break; + } + }); + + $('ul#config-heatmap li').click(function(e){ + switch($(this).text()){ + case 'Set blur': + var tmp = prompt("Heatmap blur", app.dataDisplay.heatmapBlur); + if(tmp){ + app.dataDisplay.heatmapBlur = parseInt(tmp); + app.dataDisplay.reloadHeatmaps(); + } + break; + case 'Set radius': + var tmp = prompt("Heatmap radius", app.dataDisplay.heatmapRadius); + if(tmp){ + app.dataDisplay.heatmapRadius = parseInt(tmp); + app.dataDisplay.reloadHeatmaps(); + } + break; + } + }); +}; + + +/**********************/ +/*** Initialization ***/ +/**********************/ + +$(function(){ + app.map = new ol.Map({ + controls: ol.control.defaults().extend([ + new app.control.OpenConfigControl(), + new app.control.OpenDatalist() + ]), + target: 'map', + layers: [ app.mainLayer, app.measure.layer ], + view: new ol.View({ + center: ol.proj.transform([-8.621953, 41.162142], 'EPSG:4326', 'EPSG:3857'), + zoom: 13 + }) + }); + + app.menu.init(); + app.coordInfo.init(); + app.featureInfo.init(); + app.dataList.init(); + app.dataExtract.init(); + app.dataPlayer.init(); +}); diff --git a/visualizer/style.css b/visualizer/style.css @@ -0,0 +1,124 @@ +.map { + position: fixed; + left: 0; right:0; top:0; bottom:0; + z-index: 0; +} + +.open-datalist { + top: .5em; + right: .5em; + z-index: 10; +} + +#datalist { + position: fixed; + right:0; top:0; bottom:0; + width: 20em; + display: none; + z-index: 1; + background-color: #fff; + border-left: solid 1px black; +} + +#datalist-tree { + position: absolute; + left:0; top:0; bottom:4em; + width: 100%; + overflow-y: auto; +} + +#datalist-tree > ul { + padding-left: 0.5em; +} + +#datalist-tree li > ul { + padding-left: 1.5em; +} + +#datalist-tree li { + list-style-type: none; +} + +#dataextract { + position: fixed; + bottom: 0; + height: 3.5em; +} + +#dataextract input { width: 100%; } + +.open-config { + top: 65px; + left: .5em; + z-index: 10; +} + +#config { + position: absolute; + left: 2em; top: 65px; + display: none; + z-index: 2; +} + +#config ul ul { + top: 0px !important; + width: 12em; +} + +.measure-tooltip { + position: relative; + background: rgba(0, 0, 0, 0.5); + border-radius: 4px; + color: white; + padding: 4px 8px; + opacity: 0.7; + white-space: nowrap; +} + +.measure-tooltip-value { + opacity: 1; + font-weight: bold; +} + +.measure-tooltip-static { + background-color: #ffcc33; + color: black; + border: 1px solid #fff; +} + +.measure-tooltip-value:before, +.measure-tooltip-static:before { + border-top: 6px solid rgba(0, 0, 0, 0.5); + border-right: 6px solid transparent; + border-left: 6px solid transparent; + content: ""; + position: absolute; + bottom: -6px; + margin-left: -7px; + left: 50%; +} + +.measure-tooltip-static:before { + border-top-color: #ffcc33; +} + +#featureinfo { + position: absolute; + z-index: 100; + color: #fff; + background-color: #000; + border: solid 1px #fff; + border-radius: 10px; + padding: 4px; + opacity: 0.75; +} + +#coordinfo { + position: absolute; + left: 0; bottom: 0; + z-index: 100; + color: #fff; + background-color: #000; + padding: 3px; + opacity: 0.66; +}