class GeojsonDiff

Constants

META_KEY
VERSION

Attributes

after[RW]
before[RW]

Public Class Methods

new(before, after) click to toggle source
# File lib/geojson-diff.rb, line 15
def initialize(before, after)
  @before = ensure_feature_collection(RGeo::GeoJSON.decode(before, :json_parser => :json))
  @after = ensure_feature_collection(RGeo::GeoJSON.decode(after, :json_parser => :json))
end

Public Instance Methods

added() click to toggle source
# File lib/geojson-diff.rb, line 22
def added
  diff(@after,@before,"added")
end
removed() click to toggle source
# File lib/geojson-diff.rb, line 26
def removed
  diff(@before,@after,"removed")
end
unchanged() click to toggle source
# File lib/geojson-diff.rb, line 30
def unchanged
  diff(@before,@after,"unchanged")
end

Private Instance Methods

diff(before, after, type="difference") click to toggle source

Generate a feature collection representing the requested diff

before - starting decoded geojson after- end decoded geojson type - type of diff to perform, noted in each feature’s properties

For diffs, think of this as what’s in #{before} that’s not in #{after} For intersections, it’s what’s in both #{before} and {after}

returns a feature collection of the diff

# File lib/geojson-diff.rb, line 125
def diff(before, after, type="difference")
  features = []
  matched = []

  # Don't mangle the true before and after instance variables
  before_features = before.instance_variable_get("@features").clone
  after_features = after.instance_variable_get("@features").clone

  # Loop through once diffing the properties of any directly-matched geometries
  # This helps eliminates edge cases where adding/removing a single element could
  # break the entire diff, sans an LCS approach.
  before_features.each_with_index do |feature, index|
    next unless match_index = match(feature,after_features)

    # direct match for a feature, on an unchanged diff, just diff properties
    if type == "unchanged"
      diffed_feature = diff_feature(feature, after_features[match_index], type)
      features.push diffed_feature unless diffed_feature.nil?
    end

    # If they've matched, we know they can't be added or removed
    after_features.delete_at(match_index)
    matched.push feature
  end

  # You can't delete elements from an array while iterating over them
  # So wait until we've matched everything, and delete those that have been matched
  matched.each do |feature|
    before_features.delete(feature)
  end

  # Loop through what's left, and perform a straight index->index geometry diff
  before_features.each_with_index do |feature,index|
    diffed_feature = diff_feature(feature, after_features[index], type)
    features.push diffed_feature unless diffed_feature.nil?
  end
  RGeo::GeoJSON::FeatureCollection.new(features)
end
diff_feature(before_feature, after_feature, type="difference") click to toggle source

Given a feature (before and after), diffs the geometry and properties

before_feature - Feature before the change after_feature - Feature after the change type - requested diff component, either added, removed, or unchanged

Returns a feature representing the requested diff component

# File lib/geojson-diff.rb, line 66
def diff_feature(before_feature, after_feature, type="difference")
  geometry = diff_geometry(before_feature, after_feature, type)
  return nil if geometry.nil? || geometry.is_empty?
  properties = { META_KEY => {} }
  properties.merge! diff_properties(before_feature, after_feature, type)
  properties[META_KEY].merge!({type: type})
  RGeo::GeoJSON::Feature.new(geometry,nil,properties)
end
diff_geometry(before_feature, after_feature, type) click to toggle source

Diff the geometry of a given feature

before_feature - Feature before the change after_feature - Feature after the change type - requested diff component, either added, removed, or unchanged

Returns the resulting feature geometry

# File lib/geojson-diff.rb, line 82
def diff_geometry(before_feature, after_feature, type)
  if type == "unchanged"
    command = "intersection"
  else
    command = "difference"
  end

  if after_feature.nil? && type == "unchanged"
    nil # doesn't exist in other so can't intersect
  elsif after_feature.nil? #added or removed
    before_feature.geometry # pass through
  else # true diff
    before_feature.geometry.send(command, after_feature.geometry)
  end
end
diff_properties(before_feature, after_feature, type) click to toggle source

Diff the properties of a given feature

before_feature - Feature before the change after_feature - Feature after the change type - requested diff component, either added, removed, or unchanged

Returns a JSON representation of the feature’s diff’d properties

# File lib/geojson-diff.rb, line 105
def diff_properties(before_feature, after_feature, type)
  if after_feature.nil? && type == "unchanged"
    {}
  elsif type == "added" || type == "removed"
    before_feature.properties
  else
    PropertyDiff.new(before_feature.properties, after_feature.properties).properties
  end
end
ensure_feature_collection(geometry) click to toggle source

Given a geometry, ensures that it is represented as a feature collection This way diff logic can remain consistent between geometry types Rather than creating diff logic for each type

# File lib/geojson-diff.rb, line 39
def ensure_feature_collection(geometry)
  return geometry if geometry.class == RGeo::GeoJSON::FeatureCollection
  return RGeo::GeoJSON::FeatureCollection.new([]) if geometry.nil?
  geometry = RGeo::GeoJSON::Feature.new(geometry) unless geometry.class == RGeo::GeoJSON::Feature
  RGeo::GeoJSON::FeatureCollection.new([geometry])
end
match(before_feature,after_feature_collection) click to toggle source

Find index of indentical feature within a feature collection based on geometry

from_feature - the needle feature to - the haystack featurecollection

returns (int) the index of the identical feature, or nil

# File lib/geojson-diff.rb, line 52
def match(before_feature,after_feature_collection)
  after_feature_collection.find_index do |after_feature|
    after_feature.geometry.rep_equals?(before_feature.geometry) ||
    after_feature.geometry.equals?(before_feature.geometry)
  end
end