geojson-diff 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: fc1a1694621504685cf1d34a82bca93fc9e7716f
4
+ data.tar.gz: d62ceaf8a1ce1eb84c6737c139d594b144fe6b15
5
+ SHA512:
6
+ metadata.gz: 676f81f290083fbb5b2870e23c42156c4774a03c8a59a90cfaa4b13ed180258f76f390ed3df0e1dc071d8a77e46b80e7c6d4cd5ddeadd21f885afc40d13fb326
7
+ data.tar.gz: e201fdb7c6897c8f3985757a98d40aa4830967eba25759b03fab89b05b8f16994fe31e966f6469297616dd3a6ff7838a329968fed2b1e2f09eaff42d8ee645d1
@@ -0,0 +1,163 @@
1
+ require 'rgeo'
2
+ require 'rgeo/geo_json'
3
+ require 'diffy'
4
+ require 'json'
5
+ require_relative 'rgeo/geojson'
6
+ require_relative 'geojson-diff/property-diff'
7
+ require_relative 'geojson-diff/version'
8
+
9
+ ENV["GEOS_LIBRARY_PATH"] ||= File.expand_path("lib", `geos-config --prefix`.strip)
10
+
11
+ class GeojsonDiff
12
+
13
+ META_KEY = '_geojson_diff'
14
+
15
+ def initialize(before, after)
16
+ @before = ensure_feature_collection(RGeo::GeoJSON.decode(before, :json_parser => :json))
17
+ @after = ensure_feature_collection(RGeo::GeoJSON.decode(after, :json_parser => :json))
18
+ end
19
+
20
+ attr_accessor :before, :after
21
+
22
+ def added
23
+ diff(@after,@before,"added")
24
+ end
25
+
26
+ def removed
27
+ diff(@before,@after,"removed")
28
+ end
29
+
30
+ def unchanged
31
+ diff(@before,@after,"unchanged")
32
+ end
33
+
34
+ private
35
+
36
+ # Given a geometry, ensures that it is represented as a feature collection
37
+ # This way diff logic can remain consistent between geometry types
38
+ # Rather than creating diff logic for each type
39
+ def ensure_feature_collection(geometry)
40
+ return geometry if geometry.class == RGeo::GeoJSON::FeatureCollection
41
+ return RGeo::GeoJSON::FeatureCollection.new([]) if geometry.nil?
42
+ geometry = RGeo::GeoJSON::Feature.new(geometry) unless geometry.class == RGeo::GeoJSON::Feature
43
+ RGeo::GeoJSON::FeatureCollection.new([geometry])
44
+ end
45
+
46
+ # Find index of indentical feature within a feature collection based on geometry
47
+ #
48
+ # from_feature - the needle feature
49
+ # to - the haystack featurecollection
50
+ #
51
+ # returns (int) the index of the identical feature, or nil
52
+ def match(before_feature,after_feature_collection)
53
+ after_feature_collection.find_index do |after_feature|
54
+ after_feature.geometry.rep_equals?(before_feature.geometry) ||
55
+ after_feature.geometry.equals?(before_feature.geometry)
56
+ end
57
+ end
58
+
59
+ # Given a feature (before and after), diffs the geometry and properties
60
+ #
61
+ # before_feature - Feature before the change
62
+ # after_feature - Feature after the change
63
+ # type - requested diff component, either added, removed, or unchanged
64
+ #
65
+ # Returns a feature representing the requested diff component
66
+ def diff_feature(before_feature, after_feature, type="difference")
67
+ geometry = diff_geometry(before_feature, after_feature, type)
68
+ return nil if geometry.nil? || geometry.is_empty?
69
+ properties = { META_KEY => {} }
70
+ properties.merge! diff_properties(before_feature, after_feature, type)
71
+ properties[META_KEY].merge!({type: type})
72
+ RGeo::GeoJSON::Feature.new(geometry,nil,properties)
73
+ end
74
+
75
+ # Diff the geometry of a given feature
76
+ #
77
+ # before_feature - Feature before the change
78
+ # after_feature - Feature after the change
79
+ # type - requested diff component, either added, removed, or unchanged
80
+ #
81
+ # Returns the resulting feature geometry
82
+ def diff_geometry(before_feature, after_feature, type)
83
+ if type == "unchanged"
84
+ command = "intersection"
85
+ else
86
+ command = "difference"
87
+ end
88
+
89
+ if after_feature.nil? && type == "unchanged"
90
+ nil # doesn't exist in other so can't intersect
91
+ elsif after_feature.nil? #added or removed
92
+ before_feature.geometry # pass through
93
+ else # true diff
94
+ before_feature.geometry.send(command, after_feature.geometry)
95
+ end
96
+ end
97
+
98
+ # Diff the properties of a given feature
99
+ #
100
+ # before_feature - Feature before the change
101
+ # after_feature - Feature after the change
102
+ # type - requested diff component, either added, removed, or unchanged
103
+ #
104
+ # Returns a JSON representation of the feature's diff'd properties
105
+ def diff_properties(before_feature, after_feature, type)
106
+ if after_feature.nil? && type == "unchanged"
107
+ {}
108
+ elsif type == "added" || type == "removed"
109
+ before_feature.properties
110
+ else
111
+ PropertyDiff.new(before_feature.properties, after_feature.properties).properties
112
+ end
113
+ end
114
+
115
+ # Generate a feature collection representing the requested diff
116
+ #
117
+ # before - starting decoded geojson
118
+ # after- end decoded geojson
119
+ # type - type of diff to perform, noted in each feature's properties
120
+ #
121
+ # For diffs, think of this as what's in #{before} that's not in #{after}
122
+ # For intersections, it's what's in both #{before} and {after}
123
+ #
124
+ # returns a feature collection of the diff
125
+ def diff(before, after, type="difference")
126
+ features = []
127
+ matched = []
128
+
129
+ # Don't mangle the true before and after instance variables
130
+ before_features = before.instance_variable_get("@features").clone
131
+ after_features = after.instance_variable_get("@features").clone
132
+
133
+ # Loop through once diffing the properties of any directly-matched geometries
134
+ # This helps eliminates edge cases where adding/removing a single element could
135
+ # break the entire diff, sans an LCS approach.
136
+ before_features.each_with_index do |feature, index|
137
+ next unless match_index = match(feature,after_features)
138
+
139
+ # direct match for a feature, on an unchanged diff, just diff properties
140
+ if type == "unchanged"
141
+ diffed_feature = diff_feature(feature, after_features[match_index], type)
142
+ features.push diffed_feature unless diffed_feature.nil?
143
+ end
144
+
145
+ # If they've matched, we know they can't be added or removed
146
+ after_features.delete_at(match_index)
147
+ matched.push feature
148
+ end
149
+
150
+ # You can't delete elements from an array while iterating over them
151
+ # So wait until we've matched everything, and delete those that have been matched
152
+ matched.each do |feature|
153
+ before_features.delete(feature)
154
+ end
155
+
156
+ # Loop through what's left, and perform a straight index->index geometry diff
157
+ before_features.each_with_index do |feature,index|
158
+ diffed_feature = diff_feature(feature, after_features[index], type)
159
+ features.push diffed_feature unless diffed_feature.nil?
160
+ end
161
+ RGeo::GeoJSON::FeatureCollection.new(features)
162
+ end
163
+ end
@@ -0,0 +1,114 @@
1
+ class GeojsonDiff
2
+ class PropertyDiff
3
+
4
+ # Creates a new PropertyDiff instance
5
+ #
6
+ # before - the property element of the starting geometry (as parsed JSON)
7
+ # after - the property element of the resulting geometry (as parsed JSON)
8
+ #
9
+ # returns the PropertyDiff instance
10
+ def initialize(before,after)
11
+ @before = before
12
+ @after = after
13
+ @meta = { :added => [], :removed => [], :changed => [] }
14
+ diff
15
+ end
16
+
17
+ # Checks if the given key has been added to the resulting geometry
18
+ #
19
+ # key - string the JSON property key to check
20
+ #
21
+ # Returns bool true if added, otherwise false
22
+ def added?(key)
23
+ !@before.key?(key) && @after.key?(key)
24
+ end
25
+
26
+ # Returns an array of all keys added to the resulting geometry
27
+ def added
28
+ @meta[:added]
29
+ end
30
+
31
+ # Checks if the given key has been removed from the resulting geometry
32
+ #
33
+ # key - string the JSON property key to check
34
+ #
35
+ # Returns bool true if removed, otherwise false
36
+ def removed?(key)
37
+ @before.key?(key) && !@after.key?(key)
38
+ end
39
+
40
+ # Returns an array of all keys removed from the resulting geometry
41
+ def removed
42
+ @meta[:removed]
43
+ end
44
+
45
+ # Checks if the given key has been modified in the resulting geometry
46
+ #
47
+ # key - string the JSON property key to check
48
+ #
49
+ # Returns bool true if modified, otherwise false
50
+ def changed?(key)
51
+ !added?(key) && !removed?(key) && @before[key] != @after[key]
52
+ end
53
+
54
+ # Returns an array of all keys modified in the resulting geometry
55
+ def changed
56
+ @meta[:changed]
57
+ end
58
+
59
+ # Returns the JSON representation of the diffed properties, including metadata
60
+ def to_json
61
+ diff.to_json
62
+ end
63
+
64
+ def to_s
65
+ diff.to_s
66
+ end
67
+
68
+ def inspect
69
+ "#<GeojsonDiff::PropertyDiff added=#{added.to_s} removed=#{removed.to_s} changed=#{changed.to_s}>"
70
+ end
71
+
72
+ # Returns a hash of the diffed properties, including metadata
73
+ def diff
74
+ @diff ||= begin
75
+ properties = {}
76
+ @before.merge(@after).each { |key,value| properties.merge! diffed_property(key) }
77
+ properties.merge({ GeojsonDiff::META_KEY => @meta })
78
+ end
79
+ end
80
+ alias_method :properties, :diff
81
+
82
+ private
83
+
84
+ # Diffs an individual key/value pair
85
+ # Also propegates @meta arrays
86
+ #
87
+ # key - the property key to diff
88
+ #
89
+ # Returns the resulting key/value pair, either before (removed), after (added), or diffed (changed)
90
+ def diffed_property(key)
91
+ if added? key
92
+ @meta[:added].push key
93
+ { key => @after[key] }
94
+ elsif removed? key
95
+ @meta[:removed].push key
96
+ { key => @before[key] }
97
+ elsif changed? key
98
+ @meta[:changed].push key
99
+ { key => diffed_value(key) }
100
+ else # unchanged
101
+ { key => @after[key] }
102
+ end
103
+ end
104
+
105
+ # Diffs an indivual changed value
106
+ #
107
+ # key - the property key to diff
108
+ #
109
+ # Returns (string) the Diffy representation of the changed property
110
+ def diffed_value(key)
111
+ Diffy::Diff.new(@before[key], @after[key]).to_s(:html)
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,3 @@
1
+ class GeojsonDiff
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,10 @@
1
+ # monkey patch in a native to_json method for FeatureCollections
2
+ module RGeo
3
+ module GeoJSON
4
+ class FeatureCollection
5
+ def to_json
6
+ RGeo::GeoJSON.encode(self).to_json
7
+ end
8
+ end
9
+ end
10
+ end
metadata ADDED
@@ -0,0 +1,161 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: geojson-diff
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Ben Balter
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-04-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rgeo
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rgeo-geojson
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: ffi-geos
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.5'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.5'
55
+ - !ruby/object:Gem::Dependency
56
+ name: diffy
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '10.3'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '10.3'
83
+ - !ruby/object:Gem::Dependency
84
+ name: mocha
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: bundler
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '1.5'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '1.5'
111
+ - !ruby/object:Gem::Dependency
112
+ name: pry
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '0.9'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '0.9'
125
+ description: GeoJSON Diff takes two GeoJSON files representing the same geometry (or
126
+ geometries) at two points in time, and generates three GeoJSON files represented
127
+ the added, removed, and unchanged geometries.
128
+ email: ben.balter@github.com
129
+ executables: []
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - lib/geojson-diff.rb
134
+ - lib/geojson-diff/property-diff.rb
135
+ - lib/geojson-diff/version.rb
136
+ - lib/rgeo/geojson.rb
137
+ homepage: https://github.com/benbalter/geojson-diff
138
+ licenses:
139
+ - MIT
140
+ metadata: {}
141
+ post_install_message:
142
+ rdoc_options: []
143
+ require_paths:
144
+ - lib
145
+ required_ruby_version: !ruby/object:Gem::Requirement
146
+ requirements:
147
+ - - ">="
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ required_rubygems_version: !ruby/object:Gem::Requirement
151
+ requirements:
152
+ - - ">="
153
+ - !ruby/object:Gem::Version
154
+ version: '0'
155
+ requirements: []
156
+ rubyforge_project:
157
+ rubygems_version: 2.2.2
158
+ signing_key:
159
+ specification_version: 4
160
+ summary: A Ruby library for diffing GeoJSON files
161
+ test_files: []