Skip to content

Instantly share code, notes, and snippets.

@aq1018
Created August 23, 2014 09:06
Show Gist options
  • Save aq1018/e3512f763d42ad8cf80b to your computer and use it in GitHub Desktop.
Save aq1018/e3512f763d42ad8cf80b to your computer and use it in GitHub Desktop.
Streaming GeoJSON With GDAL, Rack::Chunked, and Rails Live Streaming
# config/application.rb
module GeoApp
class Application
#
# other rails application configurations
#
# ...
# Add Rack::Chunked before Rack::Sendfile
config.middleware.insert_before(Rack::Sendfile, Rack::Chunked)
# On the fly compression for HTTP chunked encoding
config.middleware.insert_after(Rack::Chunked, Rack::Deflater)
end
end
# lib/chunked_stream.rb
#
# ChunkedStream
#
# It takes any IO object and reads the output in chunks.
# It implements #each and #close and is designed to be used
# to interface with Rails Live Stream or Rack::Chunked.
#
class ChunkedStream
CHUNK_SIZE = 1024 * 4 # read in 4 kB size
attr_reader :io, :chunk_size
# Instantiates the ChunkedStream object
#
# @param [IO] io
# The IO object to split.
#
# @param [Int] chunk_size
# Number of bytes to read each chunk.
#
# @return [undefined]
def initialize(io, chunk_size = CHUNK_SIZE)
@io = io
@chunk_size = chunk_size
end
# Yields string in chunks.
#
# @yield [String]
# yields IO buffer in specified byte size
#
# @return [undefined]
def each
while chunk = io.readpartial(chunk_size)
yield chunk
end
rescue EOFError => e
nil
ensure
close
end
# Closes the underlaying IO object.
#
# @return [undefined]
def close
io.close
end
end
# lib/geojson_command.rb
#
# GeojsonCommand
#
# Takes an ActiveRecord Scope and run ogr2ogr
# in a sub-process to generate GeoJSON.
#
class GeojsonCommand
# Instantiates the command object
#
# @param [ActiveRecord::Relation] scope
# The ActiveRecord Scope to generate GeoJSON
#
# @return [undefined]
def initialize(scope)
@scope = scope
end
# Runs the command in a sub-process and returns
# IO object containing STDOUT
#
#
# @return [IO]
def run
IO.popen(command)
end
private
# Generates the ogr2ogr command based on supplied
# ActiveRecord Scope and Rails database configuration
#
# @return [String]
def command
[
# the command name
'ogr2ogr',
# output geojson to stdout
'-f', 'GeoJSON', '/vsistdout/',
# postgres db config
conn_str,
# SQL statement to run
'-sql', "\"#{sql}\""
].join(' ')
end
# creates a db connection string suitable for ogr2ogr
#
# @return [String]
def conn_str
db_config = Rails.configuration.database_configuration[Rails.env]
host = db_config['host']
port = db_config['port']
database = db_config['database']
username = db_config['username']
password = db_config['password']
args = []
args.push "host=#{host}" if host
args.push "port=#{port}" if port
args.push "dbname=#{database}" if database
args.push "user=#{username}" if username
args.push "password=#{password}" if password
"PG:\"#{args.join(' ')}\""
end
# Generates SQL command for ogr2ogr
#
# @return [String]
def sql
@scope.connection.unprepared_statement { @scope.to_sql }
end
end
# app/api/regions_controller.rb
#
# Api::RegionsController
#
# Handles Regions related endpoints
class Api::RegionsController < ActionController::Metal
# GET /api/regions
# Accepts JSON
# Returns GeoJSON
def index
self.status = :ok
self.content_type = 'application/json'
self.response_body = regions_stream
end
private
# Generates a chunked stream containing GeoJSON string
#
# @return [ChunkedStream]
def regions_stream
@geojson_stream ||= ChunkedStream.new(
GeojsonCommand.new(regions).run
)
end
# Generates a Regions scope, ready to be transformed into GeoJSON
#
# @return [ActiveRecord::Relation]
def regions
# customize to fit your needs
@regions ||= Region.all
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment