scooterw/ffi-ogr

View on GitHub
lib/ffi-ogr/data_source.rb

Summary

Maintainability
B
5 hrs
Test Coverage
module OGR
  class DataSource
    include FFIOGR

    attr_accessor :ptr

    def initialize(ptr)
      @ptr = FFI::AutoPointer.new(ptr, self.class.method(:release))
      @ptr.autorelease = false
    end

    def self.release(ptr);end

    def free
      FFIOGR.OGR_DS_Destroy(@ptr)
    end

    def copy(driver_name, output_path, driver_options=nil)
      driver = OGRGetDriverByName driver_name
      new_ds = FFIOGR.OGR_Dr_CopyDataSource(driver, @ptr, File.expand_path(output_path), driver_options)
      FFIOGR.OGR_DS_Destroy(new_ds)
    end

    def copy_with_transform(driver_name, output_path, spatial_ref=nil, driver_options=nil)
      writer = OGR::Writer.new driver_name
      writer.set_output(output_path)
      out = writer.ptr

      layers.each do |layer|
        name = layer.name
        geometry_type = layer.geometry_type
        old_sr = layer.spatial_ref

        ct = OGR::CoordinateTransformation.find_transformation(old_sr, spatial_ref) unless spatial_ref.nil? || (spatial_ref == old_sr)

        sr = spatial_ref.nil? ? nil : spatial_ref.ptr

        new_layer = out.add_layer name, geometry_type, sr, driver_options

        ptr = layer.ptr

        layer_definition = FFIOGR.OGR_L_GetLayerDefn(ptr)
        field_count = FFIOGR.OGR_FD_GetFieldCount(layer_definition)

        for i in (0...field_count)
          fd = FFIOGR.OGR_FD_GetFieldDefn(layer_definition, i)
          name = FFIOGR.OGR_Fld_GetNameRef(fd)
          type = FFIOGR.OGR_Fld_GetType(fd)

          opts = {}.tap do |o|
            case type
            when :real
              o[:precision] = FFIOGR.OGR_Fld_GetPrecision fd
            when :string
              o[:width] = FFIOGR.OGR_Fld_GetWidth fd
            end
          end

          new_layer.add_field name, type, opts
        end

        layer.features.each do |feature|
          geometry = OGR::Tools.cast_geometry(feature.geometry)
          geometry.transform ct if ct

          new_feature = new_layer.create_feature
          new_feature.add_geometry geometry

          ptr = feature.ptr
          field_count = FFIOGR.OGR_F_GetFieldCount(ptr)

          for i in (0...field_count)
            fd = FFIOGR.OGR_F_GetFieldDefnRef(ptr, i)
            field_name = FFIOGR.OGR_Fld_GetNameRef(fd)
            field_type = FFIOGR.OGR_Fld_GetType(fd)

            case field_type
            when :integer
              field_value = FFIOGR.OGR_F_GetFieldAsInteger(ptr, i)
            when :real
              field_value = FFIOGR.OGR_F_GetFieldAsDouble(ptr, i)
            else
              field_value = FFIOGR.OGR_F_GetFieldAsString(ptr, i)
            end

            new_feature.set_field_value field_name, field_value, field_type
          end

          new_layer.add_feature new_feature
        end

        new_layer.sync
      end
      out.free
    end

    def add_layer(name, geometry_type, spatial_ref=nil, options=nil)
      layer = FFIOGR.OGR_DS_CreateLayer(@ptr, name, spatial_ref, geometry_type.to_sym, options)
      OGR::Tools.cast_layer(layer)
    end

    def num_layers
      FFIOGR.OGR_DS_GetLayerCount(@ptr)
    end

    def get_layers
      layers = []

      for i in (0...num_layers) do
        layers << OGR::Tools.cast_layer(OGR_DS_GetLayer(@ptr, i))
      end

      layers
    end
    alias_method :layers, :get_layers

    def get_features
      layers.map {|l| l.features}
    end
    alias_method :features, :get_features

    def get_geometries(as_geojson=false)
      unless as_geojson
        features.map {|feature| feature.map {|f| OGR::Tools.cast_geometry(f.geometry)}}
      else
        features.map {|feature| feature.map {|f| OGR::Tools.cast_geometry(f.geometry).to_geojson}}
      end
    end
    alias_method :geometries, :get_geometries

    def get_fields
      features.map {|feature| feature.map {|f| f.fields}}
    end
    alias_method :fields, :get_fields

    def to_format(format, output_path, options={})
      raise RuntimeError.new("Output path not specified.") if output_path.nil?

      spatial_ref = options.delete :spatial_ref

      driver_options = parse_driver_options options

      driver_name = OGR::DRIVER_TYPES[format]

      unless spatial_ref
        copy driver_name, output_path, driver_options
      else
        if spatial_ref.instance_of? OGR::SpatialReference
          copy_with_transform driver_name, output_path, spatial_ref, driver_options
        else
          raise RuntimeError.new("Invalid spatial reference specified.")
        end
      end
    end

    def to_shp(output_path, options={})
      to_format('shapefile', output_path, options)
    end

    def to_csv(output_path, options={})
      to_format('csv', output_path, options)
    end

    def to_kml(output_path, options={})
      format = OGR.drivers.include?('LIBKML') ? 'kml' : 'kml_lite'

      warn "GDAL is compiled without LIBKML support. Without LIBKML support KML output will always be in EPSG:4326" if format == 'kml_lite'

      to_format(format, output_path, options)
    end

    def to_geojson(output_path, options={})
      to_format('geojson', output_path, options)
    end

    def parse_driver_options(options)
      tf_values = {
        true => "YES",
        false => "NO"
      }

      pointers = [].tap do |ptrs|
        options.each do |k,v|
          tf_value = tf_values[v] || v
          ptrs << FFI::MemoryPointer.from_string("#{k.to_s.upcase}=#{tf_value.upcase}")
        end
      end

      pointers << nil

      driver_options = FFI::MemoryPointer.new :pointer, pointers.size

      pointers.each_with_index do |ptr, i|
        driver_options[i].put_pointer 0, ptr
      end

      driver_options
    end

    def to_json(pretty=false)
      h = {
        type: 'FeatureCollection',
        features: []
      }

      layers.each do |layer|
        h[:features].tap do |features|
          layer.features.each do |feature|
            features << {
              type: 'Feature',
              geometry: OGR::Tools.cast_geometry(feature.geometry).to_geojson,
              properties: feature.fields
            }
          end
        end
      end

      unless pretty
        MultiJson.dump(h)
      else
        MultiJson.dump(h, pretty: true)
      end
    end
  end
end