Organize my travel log. Edit with org-mode, store in Git and render via GitHub geoJSON.
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.
- Emacs with
org-mode
- Ruby
>= 1.9
- Ruby gem
geocoder
Literate Devops with Emacs by Howard Abrams
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
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
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
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 }
Commit only the geoJSON file and push everything to the remote (e.g. GitHub).
git commit --message 'Update travel log' $GEOJSON_FILE
git push