geojson-diff 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []