Created
August 23, 2014 09:06
-
-
Save aq1018/e3512f763d42ad8cf80b to your computer and use it in GitHub Desktop.
Streaming GeoJSON With GDAL, Rack::Chunked, and Rails Live Streaming
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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