lib/spatial_features/importers/shapefile.rb in spatial_features-3.4.2 vs lib/spatial_features/importers/shapefile.rb in spatial_features-3.4.8
- old
+ new
@@ -4,10 +4,13 @@
module SpatialFeatures
module Importers
class Shapefile < Base
class_attribute :default_proj4_projection
+ FEATURE_TYPE_FOR_DIMENSION = { 0 => 'point', 1 => 'line', 2 => 'polygon' }.freeze
+ PROJ4_4326 = '+proj=longlat +datum=WGS84 +no_defs'.freeze
+
def initialize(data, proj4: nil, **options)
super(data, **options)
@proj4 = proj4
end
@@ -23,19 +26,14 @@
raise ImportError, INVALID_ARCHIVE
end
private
- def build_features
- validate_shapefile!
- super
- end
-
- def each_record(&block)
- RGeo::Shapefile::Reader.open(file.path) do |records|
+ def each_record
+ open_shapefile(archive) do |records, proj4|
records.each do |record|
- yield OpenStruct.new data_from_wkt(record.geometry.as_text, proj4_projection).merge(:metadata => record.attributes) if record.geometry.present?
+ yield OpenStruct.new data_from_record(record, proj4) if record.geometry.present?
end
end
rescue Errno::ENOENT => e
case e.message
when /No such file or directory @ rb_sysopen - (.+)/
@@ -43,42 +41,73 @@
else
raise e
end
end
- def proj4_projection
- @proj4 ||= proj4_from_file || default_proj4_projection || raise(IndeterminateShapefileProjection, 'Could not determine shapefile projection. Check that `gdalsrsinfo` is installed.')
+ def data_from_record(record, proj4 = nil)
+ geometry = record.geometry
+ wkt = geometry.as_text
+ data = { :metadata => record.attributes, feature_type: FEATURE_TYPE_FOR_DIMENSION.fetch(geometry.dimension) }
+
+ if proj4 == PROJ4_4326
+ data[:geog] = wkt
+ else
+ data[:geog] = ActiveRecord::Base.connection.select_value <<-SQL
+ SELECT ST_Transform(ST_GeomFromText('#{wkt}'), '#{proj4}', 4326) AS geog
+ SQL
+ end
+
+ return data
end
- def proj4_from_file
- # Sanitize: "'+proj=utm +zone=11 +datum=NAD83 +units=m +no_defs '\n" and lately
- # "+proj=utm +zone=11 +datum=NAD83 +units=m +no_defs \n" to
- # "+proj=utm +zone=11 +datum=NAD83 +units=m +no_defs"
- `gdalsrsinfo "#{file.path}" -o proj4`.strip.remove(/^'|'$/).presence
+ def open_shapefile(file, &block)
+ # the individual SHP file for processing (automatically extracted from a ZIP archive if necessary)
+ file = possible_shp_files.first if Unzip.is_zip?(file)
+ projected_file = project_to_4326(file.path)
+ file = projected_file || file
+ validate_shapefile!(file.path)
+ proj4 = proj4_projection(file.path)
+
+ RGeo::Shapefile::Reader.open(file.path) do |records| # Fall back to unprojected geometry if projection fails
+ block.call records, proj4
+ end
+ ensure
+ if projected_file
+ projected_file.close
+ ::File.delete(projected_file)
+ end
end
- def data_from_wkt(wkt, proj4)
- ActiveRecord::Base.connection.select_one <<-SQL
- SELECT ST_Transform(ST_GeomFromText('#{wkt}'), '#{proj4}', 4326) AS geog, GeometryType(ST_GeomFromText('#{wkt}')) AS feature_type
- SQL
+ def proj4_projection(file_path)
+ proj4_from_file(file_path) || default_proj4_projection || raise(IndeterminateShapefileProjection, 'Could not determine shapefile projection. Check that `gdalsrsinfo` is installed.')
end
- # the individual SHP file for processing (automatically extracted from a ZIP archive if necessary)
- def file
- @file ||= Unzip.is_zip?(archive) ? possible_shp_files.first : archive
+ def validate_shapefile!(file_path)
+ Validation.validate_shapefile!(::File.open(file_path), default_proj4_projection: default_proj4_projection)
end
+ # Use OGR2OGR to reproject into EPSG:4326 so we can skip the reprojection step per-feature
+ def project_to_4326(file_path)
+ output_path = Tempfile.create([::File.basename(file_path, '.shp') + '_epsg_4326_', '.shp']) { |file| file.path }
+ return unless (proj4 = proj4_from_file(file_path))
+ return unless system("ogr2ogr -s_srs '#{proj4}' -t_srs EPSG:4326 '#{output_path}' '#{file_path}'")
+ return ::File.open(output_path)
+ end
+
+ def proj4_from_file(file_path)
+ # Sanitize: "'+proj=utm +zone=11 +datum=NAD83 +units=m +no_defs '\n" and lately
+ # "+proj=utm +zone=11 +datum=NAD83 +units=m +no_defs \n" to
+ # "+proj=utm +zone=11 +datum=NAD83 +units=m +no_defs"
+ `gdalsrsinfo "#{file_path}" -o proj4`.strip.remove(/^'|'$/).presence
+ end
+
# a zip archive may contain multiple SHP files
def possible_shp_files
@possible_shp_files ||= begin
Download.open_each(archive, unzip: /\.shp$/, downcase: true)
rescue Unzip::PathNotFound
raise ::SpatialFeatures::Importers::IncompleteShapefileArchive, "Shapefile archive is missing a SHP file"
end
- end
-
- def validate_shapefile!
- Validation.validate_shapefile!(file, default_proj4_projection: default_proj4_projection)
end
def archive
@archive ||= Download.open(@data)
end