Organize my geoJSON travel log with org-mode
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
media
README.org
my_summer_2015.geojson

README.org

travel_log

Organize my travel log. Edit with org-mode, store in Git and render via GitHub geoJSON.

./media/demo.gif

Introduction

Turns this: ./media/emacs.png

Into this: ./media/geojson_github.png

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