Skip to content

Commit

Permalink
Add delegation to geometry in Feature (#53)
Browse files Browse the repository at this point in the history
Feature is now provided a `method_missing` that will default
to inner geometry. FeatureCollection has access to some OGC
methods, and a method_missing message to indicate the lack
of implementation if one uses a not yet implemented method.
  • Loading branch information
BuonOmo committed Jun 28, 2021
1 parent 7316c96 commit bee0133
Show file tree
Hide file tree
Showing 10 changed files with 212 additions and 44 deletions.
23 changes: 23 additions & 0 deletions .rdoc_options
@@ -0,0 +1,23 @@
--- !ruby/object:RDoc::Options
encoding: UTF-8
static_path: []
rdoc_include:
- "."
- "/Users/ulysse/Dev/rgeo/rgeo-geojson"
charset: UTF-8
exclude: !ruby/regexp /~\z|\.orig\z|\.rej\z|\.bak\z|\.gemspec\z/
hyperlink_all: false
line_numbers: false
locale:
locale_dir: locale
locale_name:
main_page:
markup: markdown
output_decoration: true
page_dir:
show_hash: false
tab_width: 8
template_stylesheets: []
title:
visibility: :protected
webcvs:
5 changes: 5 additions & 0 deletions History.md
@@ -1,3 +1,8 @@
### Ongoing

* Delegation to inner geometry for a feature (#53)
* MultiJson rather than JSON (#46)

### 2.1.1 / 2018-11-27

* Freeze strings
Expand Down
2 changes: 1 addition & 1 deletion lib/rgeo-geojson.rb
@@ -1,3 +1,3 @@
# frozen_string_literal: true

require "rgeo/geo_json"
require_relative "rgeo/geo_json"
8 changes: 4 additions & 4 deletions lib/rgeo/geo_json.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true

require "rgeo"
require "rgeo/geo_json/version"
require "rgeo/geo_json/entities"
require "rgeo/geo_json/coder"
require "rgeo/geo_json/interface"
require_relative "geo_json/version"
require_relative "geo_json/entities"
require_relative "geo_json/coder"
require_relative "geo_json/interface"
require "multi_json"
3 changes: 0 additions & 3 deletions lib/rgeo/geo_json/coder.rb
Expand Up @@ -6,7 +6,6 @@ module GeoJSON
# the RGeo::Feature::Factory and the RGeo::GeoJSON::EntityFactory to
# be used) so that you can encode and decode without specifying those
# settings every time.

class Coder
# Create a new coder settings object. The geo factory is passed as
# a required argument.
Expand Down Expand Up @@ -41,7 +40,6 @@ def initialize(opts = {})
# appropriate JSON library installed.
#
# Returns nil if nil is passed in as the object.

def encode(object)
if @entity_factory.is_feature_collection?(object)
{
Expand All @@ -60,7 +58,6 @@ def encode(object)
# Decode an object from GeoJSON. The input may be a JSON hash, a
# String, or an IO object from which to read the JSON string.
# If an error occurs, nil is returned.

def decode(input)
if input.is_a?(IO)
input = input.read rescue nil
Expand Down
35 changes: 35 additions & 0 deletions lib/rgeo/geo_json/collection_methods.rb
@@ -0,0 +1,35 @@
# frozen_string_literal: true

module RGeo
# This module is here to fill the gap between what is a GeometryCollection (GIS)
# and a FeatureCollection (GeoJSON).
#
# Note for contributors, you can rely on `@features` to be defined and
# you can consider working with an Enumerable wrapping `@features`. See
# GeoJSON::FeatureCollection.
module GeoJSON::CollectionMethods
# There is tight coupling between {FeatureCollection} and this, hence the
# guard.
private_class_method def self.included(base)
return if base.to_s == "RGeo::GeoJSON::FeatureCollection"

raise Error::RGeoError, "#{self.class} must only be used by FeatureCollection"
end

private def method_missing(symbol, *args)
return super unless any? { |feature| feature.respond_to?(symbol) }

raise Error::UnsupportedOperation, "Method FeatureCollection##{symbol} " \
"is not defined. You may consider filing an issue or opening a pull " \
"request at https://github.com/rgeo/rgeo-geojson"
end

def contains?(geometry)
any? { |feature| feature.contains?(geometry) }
end

def intersects?(geometry)
any? { |feature| feature.intersects?(geometry) }
end
end
end
61 changes: 36 additions & 25 deletions lib/rgeo/geo_json/entities.rb
@@ -1,7 +1,39 @@
# frozen_string_literal: true

require_relative "collection_methods"

module RGeo
module CastOverlay
def self.included(base)
# The original {RGeo::Feature.cast} would copy a GeoJSON::Feature, which
# fails most operations. When casting, we MUST get a geometry.
original_cast = base.method(:cast)
base.define_singleton_method(:cast) do |obj, *params|
if obj.class == GeoJSON::Feature
original_cast.call(obj.geometry, *params)
else
original_cast.call(obj, *params)
end
end
end
end
Feature.include(CastOverlay)

module GeoJSON
# Simplify usage of inner geometries for Feature and FeatureCollection
# objets. Including class must contain a `#geometry` method.
module DelegateToGeometry
private def method_missing(symbol, *args)
return geometry.public_send(symbol, *args) if geometry

super
end

private def respond_to_missing?(symbol, *)
geometry&.respond_to?(symbol) || super
end
end

# This is a GeoJSON wrapper entity that corresponds to the GeoJSON
# "Feature" type. It is an immutable type.
#
Expand All @@ -11,11 +43,11 @@ module GeoJSON
# implementation need not subclass or even duck-type this class.
# the entity factory mediates all interaction between the GeoJSON
# engine and features.

class Feature
include DelegateToGeometry

# Create a feature wrapping the given geometry, with the given ID
# and properties.

def initialize(geometry, id = nil, properties = {})
@geometry = geometry
@id = id
Expand All @@ -41,7 +73,6 @@ def hash
# are all equal.
# This method uses the eql? method to test geometry equality, which
# may behave differently than the == operator.

def eql?(other)
other.is_a?(Feature) && @geometry.eql?(other.geometry) && @id.eql?(other.feature_id) && @properties.eql?(other.instance_variable_get(:@properties))
end
Expand All @@ -50,37 +81,31 @@ def eql?(other)
# are all equal.
# This method uses the == operator to test geometry equality, which
# may behave differently than the eql? method.

def ==(other)
other.is_a?(Feature) && @geometry == other.geometry && @id == other.feature_id && @properties == other.instance_variable_get(:@properties)
end

# Returns the geometry contained in this feature, which may be nil.

attr_reader :geometry

# Returns the ID for this feature, which may be nil.

def feature_id
@id
end

# Returns a copy of the properties for this feature.

def properties
@properties.dup
end

# Gets the value of the given named property.
# Returns nil if the given property is not found.

def property(key)
@properties[key.to_s]
end
alias [] property

# Gets an array of the known property keys in this feature.

def keys
@properties.keys
end
Expand All @@ -95,16 +120,16 @@ def keys
# FeatureCollection implementation need not subclass or even
# duck-type this class. The entity factory mediates all interaction
# between the GeoJSON engine and feature collections.

class FeatureCollection
include Enumerable
include CollectionMethods

# Create a new FeatureCollection with the given features, which must
# be provided as an Enumerable.

def initialize(features = [])
@features = []
features.each { |f| @features << f if f.is_a?(Feature) }
@features.freeze
end

def inspect
Expand All @@ -123,7 +148,6 @@ def hash
# features in the same order.
# This methods uses the eql? method to test geometry equality, which
# may behave differently than the == operator.

def eql?(other)
other.is_a?(FeatureCollection) && @features.eql?(other.instance_variable_get(:@features))
end
Expand All @@ -132,25 +156,21 @@ def eql?(other)
# features in the same order.
# This methods uses the == operator to test geometry equality, which
# may behave differently than the eql? method.

def ==(other)
other.is_a?(FeatureCollection) && @features == other.instance_variable_get(:@features)
end

# Iterates or returns an iterator for the features.

def each(&block)
@features.each(&block)
end

# Returns the number of features contained in this collection.

def size
@features.size
end

# Access a feature by index.

def [](index)
@features[index]
end
Expand All @@ -164,60 +184,51 @@ class EntityFactory
# Create and return a new feature, given geometry, ID, and
# properties hash. Note that, per the GeoJSON spec, geometry and/or
# properties may be nil.

def feature(geometry, id = nil, properties = nil)
Feature.new(geometry, id, properties || {})
end

# Create and return a new feature collection, given an enumerable
# of feature objects.

def feature_collection(features = [])
FeatureCollection.new(features)
end

# Returns true if the given object is a feature created by this
# entity factory.

def is_feature?(object)
object.is_a?(Feature)
end

# Returns true if the given object is a feature collection created
# by this entity factory.

def is_feature_collection?(object)
object.is_a?(FeatureCollection)
end

# Run Enumerable#map on the features contained in the given feature
# collection.

def map_feature_collection(object, &block)
object.map(&block)
end

# Returns the geometry associated with the given feature.

def get_feature_geometry(object)
object.geometry
end

# Returns the ID of the given feature, or nil for no ID.

def get_feature_id(object)
object.feature_id
end

# Returns the properties of the given feature as a hash. Editing
# this hash does not change the state of the feature.

def get_feature_properties(object)
object.properties
end

# Return the singleton instance of EntityFactory.

def self.instance
@instance ||= new
end
Expand Down
7 changes: 3 additions & 4 deletions lib/rgeo/geo_json/interface.rb
Expand Up @@ -13,9 +13,8 @@ class << self
# RGeo::GeoJSON::EntityFactory for more information. By default,
# encode supports objects of type RGeo::GeoJSON::Feature and
# RGeo::GeoJSON::FeatureCollection.

def encode(object, opts = {})
Coder.new(opts).encode(object)
coder(opts).encode(object)
end

# High-level convenience routine for decoding an object from GeoJSON.
Expand All @@ -33,8 +32,8 @@ def encode(object, opts = {})
# RGeo::GeoJSON::EntityFactory, which generates objects of type
# RGeo::GeoJSON::Feature or RGeo::GeoJSON::FeatureCollection.
# See RGeo::GeoJSON::EntityFactory for more information.
def decode(input_, opts = {})
Coder.new(opts).decode(input_)
def decode(input, opts = {})
coder(opts).decode(input)
end

# Creates and returns a coder object of type RGeo::GeoJSON::Coder
Expand Down

0 comments on commit bee0133

Please sign in to comment.