diff --git a/README.md b/README.md index 99bd3d5..be9d675 100644 --- a/README.md +++ b/README.md @@ -61,9 +61,20 @@ At the moment Chasemapper only supports receiving chase-car positions via Horus Eventually support will be added to get car positions from either GPSD, or from the client's device. -## Offline Mapping +## Offline Mapping via FoxtrotGPS's Tile Cache (This is a work in progress) -By default Chasemapper is configured to use the online OSM and ESRI Satellite tileservers. There is also an 'offline OSM' entry in the map layer list (top right of the page), which attempt to gather maps from `http://server_ip:8080/roads/{z}/{x}/{y}.png`. I've been doing some testing with using [Tilestache](http://tilestache.org/) as a lightweight tileserver, serving tiles from mbtiles files. A guide on how to cache up OSM data for use with Tilestache is TBD... +Chasemapper can serve up map tiles from a specified directory to the web client. Of course, for this to be useful, we need map tiles to server! [FoxtrotGPS](https://www.foxtrotgps.org/) can help us with this, as it caches map tiles to `~/Maps/`, with one subdirectory per map layer (i.e. `~/Maps/OSM/`, `~/Maps/opencyclemap/`). + +This can be enabled by setting `[offline_maps] tile_server_enabled = True`, and changing `[offline_maps] tile_server_path` to point to your tile cache directory (i.e. `/home/pi/Maps/`). Chasemapper will assume each subdirectory in this folder is a valid map layer and will add them to the map layer list at the top-right of the interface. + +### Caching Maps + +To grab map tiles to use with this, we're going to use FoxtrotGPS's [Cached Maps](https://www.foxtrotgps.org/doc/foxtrotgps.html#Cached-Maps) feature. + + * Install FoxtrotGPS (Linux only unfortunately, works OK on a Pi!) either [from source](https://www.foxtrotgps.org/releases/), or via your system package manager (`sudo apt-get install foxtrotgps`). + * Load up FoxtrotGPS, and pan around the area you are intersted in caching. Pick the map layer you want, right-click on the map, and choose 'Map download'. You can then select how many zoom levels you want to cache, and start it downloading (this may take a while!) + * Once you have a set of folders within your `~/Maps` cache directory, you can startup Chasemapper and start using them! Tiles will be served up as they become available. + (If anyone has managed to get ECW support working in GDAL recently, please contact me! I would like to convert some topographic maps in ECW format to tiles for use with Chasemapper.) diff --git a/chasemapper/config.py b/chasemapper/config.py index 98c885f..2145ce7 100644 --- a/chasemapper/config.py +++ b/chasemapper/config.py @@ -6,6 +6,7 @@ # Released under GNU GPL v3 or later # import logging +import os try: # Python 2 @@ -61,6 +62,19 @@ def parse_config_file(filename): chase_config['pred_gfs_directory'] = config.get('predictor', 'gfs_directory') chase_config['pred_model_download'] = config.get('predictor', 'model_download') + # Offline Map Settings + chase_config['tile_server_enabled'] = config.getboolean('offline_maps', 'tile_server_enabled') + chase_config['tile_server_port'] = config.getint('offline_maps', 'tile_server_port') + chase_config['tile_server_path'] = config.get('offline_maps', 'tile_server_path') + + # Determine valid offline map layers. + chase_config['offline_tile_layers'] = [] + if chase_config['tile_server_enabled']: + for _dir in os.listdir(chase_config['tile_server_path']): + if os.path.isdir(os.path.join(chase_config['tile_server_path'],_dir)): + chase_config['offline_tile_layers'].append(_dir) + logging.info("Found Map Layers: %s" % str(chase_config['offline_tile_layers'])) + # Telemetry Source Profiles _profile_count = config.getint('profile_selection', 'profile_count') @@ -121,6 +135,7 @@ def read_config(filename, default_cfg="horusmapper.cfg.example"): if __name__ == "__main__": import sys + logging.basicConfig(format='%(asctime)s %(levelname)s:%(message)s', stream=sys.stdout, level=logging.DEBUG) print(read_config(sys.argv[1])) diff --git a/horusmapper.cfg.example b/horusmapper.cfg.example index 9cafee7..ef1d8be 100644 --- a/horusmapper.cfg.example +++ b/horusmapper.cfg.example @@ -91,3 +91,22 @@ gfs_directory = ./gfs/ # The gfs directory (above) will be cleared of all .dat files prior to the above command being run. model_download = none + +# +# Offline Tile Server +# +# Allows serving of map tiles from a directory. +# Each subdirectory is assumed to be a separate layer of map tiles, i.e. 'OSM', 'opencyclemap', +# and is added to the map interface as a separate layer. +# This feature can be used to serve up FoxtrotGPS's tile cache as layers, usually located in ~/Maps/ +# +[offline_maps] +# Enable serving up maps from a directory of map tiles. +tile_server_enabled = False + +# Path to map tiles. For FoxtrotGPS, this is usually ~/Maps/ +# NOTE: This must be an ABSOLUTE directory, i.e. /home/pi/Maps/ - ~/Maps/ will not work. +tile_server_path = /home/pi/Maps/ + + + diff --git a/horusmapper.py b/horusmapper.py index 98067b9..125cf5a 100644 --- a/horusmapper.py +++ b/horusmapper.py @@ -9,6 +9,7 @@ import json import logging import flask from flask_socketio import SocketIO +import os.path import sys import time import traceback @@ -48,6 +49,9 @@ data_listeners = [] # These settings are not editable by the client! pred_settings = {} +# Offline map settings, again, not editable by the client. +map_settings = {'tile_server_enabled': False} + # Payload data Stores current_payloads = {} # Archive data which will be passed to the web client current_payload_tracks = {} # Store of payload Track objects which are used to calculate instantaneous parameters. @@ -75,6 +79,19 @@ def flask_get_telemetry_archive(): def flask_get_config(): return json.dumps(chasemapper_config) +@app.route("/tiles/") +def flask_server_tiles(filename): + """ Serve up a file from the tile server location """ + global map_settings + if map_settings['tile_server_enabled']: + _filename = flask.safe_join(map_settings['tile_server_path'], filename) + if os.path.isfile(_filename): + return flask.send_file(_filename) + else: + flask.abort(404) + else: + flask.abort(404) + def flask_emit_event(event_name="none", data={}): """ Emit a socketio event to any clients. """ @@ -692,6 +709,12 @@ if __name__ == "__main__": 'pred_model_download': chasemapper_config['pred_model_download'] } + # Copy out Offline Map Settings + map_settings = { + 'tile_server_enabled': chasemapper_config['tile_server_enabled'], + 'tile_server_path': chasemapper_config['tile_server_path'] + } + # Start listeners using the default profile selection. start_listeners(chasemapper_config['profiles'][chasemapper_config['selected_profile']]) diff --git a/templates/index.html b/templates/index.html index 7b13f24..550116b 100644 --- a/templates/index.html +++ b/templates/index.html @@ -51,6 +51,7 @@ pred_update_rate: 15, pred_model: 'Disabled', show_abort: true, // Show a prediction of an 'abort' paths (i.e. if the balloon bursts *now*) + offline_tile_layers: [] }; // Object which will contain balloon markers and traces. @@ -198,13 +199,6 @@ attribution: '© OpenStreetMap contributors' }).addTo(map); - // Experimental offline maps using a local tilestache server. - var offline_osm_map = L.tileLayer(location.protocol + '//' + document.domain + ':8080/roads/{z}/{x}/{y}.png', { - attribution: '© OpenStreetMap contributors', - minNativeZoom:9, - maxNativeZoom:13 - }); - // Add ESRI Satellite Maps. var esrimapLink = 'Esri'; @@ -216,7 +210,18 @@ attribution: '© '+esrimapLink+', '+esriwholink, maxZoom: 18, }); - map.addControl(new L.Control.Layers({'OSM':osm_map, 'ESRI Satellite':esri_sat_map, 'Offline OSM': offline_osm_map})); + + var map_layers = {'OSM':osm_map, 'ESRI Satellite':esri_sat_map}; + + // Add Offline map layers, if we have any. + for (var i = 0, len = chase_config.offline_tile_layers.length; i < len; i++) { + var _layer_name = chase_config.offline_tile_layers[i]; + map_layers['Offline - ' + _layer_name] = L.tileLayer(location.protocol + '//' + document.domain + ':' + location.port + '/tiles/'+_layer_name+'/{z}/{x}/{y}.png'); + } + // Add layer selection control (top right). + map.addControl(new L.Control.Layers(map_layers)); + + // Add sidebar to map (where all of our controls are!) var sidebar = L.control.sidebar('sidebar').addTo(map); // Add custom controls, which show various sets of data.