travel_log
Organize my travel log. Edit with org-mode, store in Git and render via GitHub geoJSON.
Introduction
Usage
Check out the repository, open this file and run C-c C-v C-b
to execute
all the blocks. The editable travel log appears after the first block has
been executed.
Requirements
- Emacs with
org-mode
- Ruby
>= 1.9
- Ruby gem
geocoder
Inspiration
Literate Devops with Emacs by Howard Abrams
Organizing your geoJSON travel log
Load the travel log from the existing geoJSON file, so we can edit it.
require 'json'
require 'date'
if travellog == 'nil'
geojson = JSON.parse open(geojson_file).read
destinations = geojson['features'].select do |entry|
entry['geometry']['type'] == 'Point'
end
destinations.map do |dest|
properties = dest['properties']
date = Date.parse properties['Arrival date']
location = properties['Location']
remarks = properties['Remarks']
[date.strftime('<%Y-%m-%d %a>'), location, remarks || '']
end
else
travellog
end
Backend
geoJSON file
Search the current directory for a geoJSON file. Please make sure that there is only one, otherwise unexpected behavior may occur.
ls . | grep .geojson | head -n 1
Interacting with geocoding API
Extract the locations from the travel log and geocode them. We don’t want to exceed the Google Maps API limits, so we save the coordinates in a table. Furthermore, we also warm the cache by extracting coordinates from the geoJSON travel log when the cache is empty.
Please check the table for error messages when a location is not found, or something else goes wrong.
require 'json'
require 'geocoder'
if cache_table != 'nil'
cache = Hash[cache_table.map do |item|
location, lng, lat = item
[location, [lng, lat]]
end]
else
raw = JSON.parse open(geojson_file).read
destinations = raw['features'].select do |entry|
entry['geometry']['type'] == 'Point'
end
cache = Hash[destinations.map do |dest|
location = dest['properties']['Location']
coordinates = dest['geometry']['coordinates']
[location, coordinates]
end]
end
# Remove invalid (e.g. strings with errors) values from cache
cache.reject! { |_, coords| coords.map(&:to_f).include?(0.0) }
locations = travellog.map do |entry|
_, location = entry
location
end
distinct_locations = locations.uniq
distinct_locations.sort!
distinct_locations.map do |location|
lnglat = cache[location] || begin
geo = Geocoder.search location
result = geo.first
if result
loc = result.geometry['location']
[loc['lng'], loc['lat']]
else
['Location not found...', '']
end
rescue => e
[e.inspect, '']
end
[location, *lnglat]
end
Decorate travel log with geocoded data
Combine the travel log and geocode cache to construct a geoJSON file. After that, we save the file.
require 'date'
require 'json'
geo = Hash[geo_cache.map do |entry|
location, lng, lat = entry
[location, [lng, lat]]
end]
features = []
travellog << nil
travellog.each_cons(2) do |entry, cons_entry|
org_date, location, remarks = entry
# Prevent invalid coords from being loaded
geo[location].each do |coord|
Float(coord)
end
date = Date.parse org_date
properties = { Location: location }
properties[:Remarks] = remarks unless remarks.empty?
properties['Arrival date'] = date
features << {
type: 'Feature',
geometry: {
type: 'Point',
coordinates: geo[location]
},
properties: properties
}
next unless cons_entry
cons_org_date, cons_location = cons_entry
cons_date = Date.parse cons_org_date
features.last[:properties]['Departure date'] = cons_date
features << {
type: 'Feature',
geometry: {
type: 'LineString',
coordinates: [geo[location], geo[cons_location]]
},
properties: {
'Origin' => location,
'Destination' => cons_location,
'Departure date' => date,
'Arrival date' => cons_date
}
}
end
output = JSON.pretty_generate(
type: 'FeatureCollection',
features: features
)
open(geojson_file, 'w') { |file| file.write output }
Version control
Commit only the geoJSON file and push everything to the remote (e.g. GitHub).
git commit --message 'Update travel log' $GEOJSON_FILE
git push