charta 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +44 -0
- data/Rakefile +10 -0
- data/charta.gemspec +30 -0
- data/lib/charta.rb +176 -0
- data/lib/charta/bounding_box.rb +28 -0
- data/lib/charta/geo_json.rb +168 -0
- data/lib/charta/geojson_import.rb +30 -0
- data/lib/charta/geometry.rb +278 -0
- data/lib/charta/geometry_collection.rb +9 -0
- data/lib/charta/gml.rb +140 -0
- data/lib/charta/gml_import.rb +92 -0
- data/lib/charta/kml.rb +92 -0
- data/lib/charta/line_string.rb +22 -0
- data/lib/charta/multi_polygon.rb +28 -0
- data/lib/charta/point.rb +14 -0
- data/lib/charta/polygon.rb +12 -0
- data/lib/charta/version.rb +3 -0
- data/lib/rgeo/svg.rb +69 -0
- metadata +179 -0
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
class GeojsonImport
|
4
|
+
# TODO: handle a File object instead of calling IO read/write directly
|
5
|
+
def initialize(data)
|
6
|
+
@shapes = nil
|
7
|
+
@xml = data
|
8
|
+
end
|
9
|
+
|
10
|
+
def valid?
|
11
|
+
shapes = JSON.parse(@xml)
|
12
|
+
::Charta::GeoJSON.valid?(shapes)
|
13
|
+
end
|
14
|
+
|
15
|
+
def shapes(options = {})
|
16
|
+
options[:to] ||= :json
|
17
|
+
|
18
|
+
@shapes = JSON.parse(@xml)
|
19
|
+
|
20
|
+
if options[:to].equal? :json
|
21
|
+
@shapes = @shapes.to_json
|
22
|
+
elsif options[:to].equal? :string
|
23
|
+
@shapes = @shapes.to_s
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def as_geojson
|
28
|
+
@shapes.to_json
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,278 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'rgeo/geo_json'
|
3
|
+
require 'rgeo/svg' # integrated lib for now
|
4
|
+
|
5
|
+
module Charta
|
6
|
+
# Represents a Geometry with SRID
|
7
|
+
class Geometry
|
8
|
+
def initialize(ewkt)
|
9
|
+
@ewkt = ewkt
|
10
|
+
raise ArgumentError, 'Need EWKT to instantiate Geometry' if @ewkt.to_s =~ /\A[[:space:]]*\z/
|
11
|
+
end
|
12
|
+
|
13
|
+
def inspect
|
14
|
+
"<#{self.class.name}(#{to_ewkt})>"
|
15
|
+
end
|
16
|
+
|
17
|
+
# Returns the type of the geometry as a string. Example: point,
|
18
|
+
# multi_polygon, geometry_collection...
|
19
|
+
def type
|
20
|
+
Charta.underscore(feature.geometry_type.type_name).to_sym
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns the type of the geometry as a string. EG: 'ST_Linestring', 'ST_Polygon',
|
24
|
+
# 'ST_MultiPolygon' etc. This function differs from GeometryType(geometry)
|
25
|
+
# in the case of the string and ST in front that is returned, as well as the fact
|
26
|
+
# that it will not indicate whether the geometry is measured.
|
27
|
+
def collection?
|
28
|
+
feature.geometry_type == RGeo::Feature::GeometryCollection
|
29
|
+
end
|
30
|
+
|
31
|
+
# Return the spatial reference identifier for the ST_Geometry
|
32
|
+
def srid
|
33
|
+
feature.srid.to_i
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns the underlaying object managed by Charta: the RGeo feature
|
37
|
+
def to_rgeo
|
38
|
+
feature
|
39
|
+
end
|
40
|
+
|
41
|
+
# Returns the Well-Known Text (WKT) representation of the geometry/geography without SRID metadata
|
42
|
+
def to_text
|
43
|
+
feature.as_text
|
44
|
+
end
|
45
|
+
alias as_text to_text
|
46
|
+
alias to_wkt to_text
|
47
|
+
|
48
|
+
# Returns EWKT: WKT with its SRID
|
49
|
+
def to_ewkt
|
50
|
+
@ewkt.to_s
|
51
|
+
end
|
52
|
+
alias to_s to_ewkt
|
53
|
+
|
54
|
+
def ewkt
|
55
|
+
puts 'DEPRECATION WARNING: Charta::Geometry.ewkt is deprecated. Please use Charta::Geometry.to_ewkt instead'
|
56
|
+
to_ewkt
|
57
|
+
end
|
58
|
+
|
59
|
+
# Return the Well-Known Binary (WKB) representation of the geometry with SRID meta data.
|
60
|
+
def to_binary
|
61
|
+
generator = RGeo::WKRep::WKBGenerator.new(tag_format: :ewkbt, emit_ewkbt_srid: true)
|
62
|
+
generator.generate(feature)
|
63
|
+
end
|
64
|
+
alias to_ewkb to_binary
|
65
|
+
|
66
|
+
# Pas bien compris le fonctionnement
|
67
|
+
def to_svg(options = {})
|
68
|
+
svg = '<svg xmlns="http://www.w3.org/2000/svg" version="1.1"'
|
69
|
+
{ preserve_aspect_ratio: 'xMidYMid meet',
|
70
|
+
width: 180, height: 180,
|
71
|
+
view_box: bounding_box.svg_view_box.join(' ') }.merge(options).each do |attr, value|
|
72
|
+
svg << " #{Charta.camelcase(attr.to_s, :lower)}=\"#{value}\""
|
73
|
+
end
|
74
|
+
svg << "><path d=\"#{to_svg_path}\"/></svg>"
|
75
|
+
svg
|
76
|
+
end
|
77
|
+
|
78
|
+
# Return the geometry as Scalar Vector Graphics (SVG) path data.
|
79
|
+
def to_svg_path
|
80
|
+
RGeo::SVG.encode(feature)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Return the geometry as a Geometry Javascript Object Notation (GeoJSON) element.
|
84
|
+
def to_geojson
|
85
|
+
to_json_object.to_json
|
86
|
+
end
|
87
|
+
alias to_json to_geojson
|
88
|
+
|
89
|
+
# Returns object in JSON (Hash)
|
90
|
+
def to_json_object
|
91
|
+
RGeo::GeoJSON.encode(feature)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Test if the other measure is equal to self
|
95
|
+
def ==(other)
|
96
|
+
other_geometry = Charta.new_geometry(other).transform(srid)
|
97
|
+
return true if empty? && other_geometry.empty?
|
98
|
+
return inspect == other_geometry.inspect if collection? && other_geometry.collection?
|
99
|
+
feature.equals?(other_geometry.feature)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Test if the other measure is equal to self
|
103
|
+
def !=(other)
|
104
|
+
other_geometry = Charta.new_geometry(other).transform(srid)
|
105
|
+
return true if empty? && other_geometry.empty?
|
106
|
+
return inspect == other_geometry.inspect if collection? && other_geometry.collection?
|
107
|
+
!feature.equals?(other_geometry.feature)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Returns true if Geometry is a Surface
|
111
|
+
def surface?
|
112
|
+
[RGeo::Feature::Polygon, RGeo::Feature::MultiPolygon].include? feature.geometry_type
|
113
|
+
end
|
114
|
+
|
115
|
+
# Returns area in unit corresponding to the SRS
|
116
|
+
def area
|
117
|
+
surface? ? feature.area : 0
|
118
|
+
end
|
119
|
+
|
120
|
+
# Returns true if this Geometry is an empty geometrycollection, polygon,
|
121
|
+
# point etc.
|
122
|
+
def empty?
|
123
|
+
feature.is_empty?
|
124
|
+
end
|
125
|
+
alias blank? empty?
|
126
|
+
|
127
|
+
# Computes the geometric center of a geometry, or equivalently, the center
|
128
|
+
# of mass of the geometry as a POINT.
|
129
|
+
def centroid
|
130
|
+
surface? ? feature.centroid : nil
|
131
|
+
end
|
132
|
+
|
133
|
+
# Returns a POINT guaranteed to lie on the surface.
|
134
|
+
def point_on_surface
|
135
|
+
surface? ? feature.point_on_surface : nil
|
136
|
+
end
|
137
|
+
|
138
|
+
def convert_to(new_type)
|
139
|
+
if new_type == type
|
140
|
+
self
|
141
|
+
elsif new_type == :multi_point
|
142
|
+
flatten_multi(:point)
|
143
|
+
elsif new_type == :multi_line_string
|
144
|
+
flatten_multi(:line_string)
|
145
|
+
elsif new_type == :multi_polygon
|
146
|
+
flatten_multi(:polygon)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def flatten_multi(as_type)
|
151
|
+
items = []
|
152
|
+
as_multi_type = "multi_#{as_type}".to_sym
|
153
|
+
if type == as_type
|
154
|
+
items << feature
|
155
|
+
elsif is_a? :geometry_collection
|
156
|
+
feature.each do |geom|
|
157
|
+
type_name = Charta.underscore(geom.geometry_type.type_name).to_sym
|
158
|
+
if type_name == as_type
|
159
|
+
items << geom
|
160
|
+
elsif type_name == as_multi_type
|
161
|
+
geom.each do |item|
|
162
|
+
items << item
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
Charta.new_geometry(feature.factory.send(as_multi_type, items))
|
168
|
+
end
|
169
|
+
|
170
|
+
# Returns a new geometry with the coordinates converted into the new SRS
|
171
|
+
def transform(new_srid)
|
172
|
+
return self if new_srid == srid
|
173
|
+
raise 'Proj is not supported. Cannot tranform' unless RGeo::CoordSys::Proj4.supported?
|
174
|
+
new_srid = Charta::SRS[new_srid] || new_srid
|
175
|
+
database = self.class.srs_database
|
176
|
+
new_proj_entry = database.get(new_srid)
|
177
|
+
raise "Cannot find proj for SRID: #{new_srid}" if new_proj_entry.nil?
|
178
|
+
new_feature = RGeo::CoordSys::Proj4.transform(
|
179
|
+
database.get(srid).proj4,
|
180
|
+
feature,
|
181
|
+
new_proj_entry.proj4,
|
182
|
+
self.class.factory(new_srid)
|
183
|
+
)
|
184
|
+
generator = RGeo::WKRep::WKTGenerator.new(tag_format: :ewkt, emit_ewkt_srid: true)
|
185
|
+
self.class.new(generator.generate(new_feature))
|
186
|
+
end
|
187
|
+
|
188
|
+
# Produces buffer
|
189
|
+
def buffer(radius)
|
190
|
+
feature.buffer(radius)
|
191
|
+
end
|
192
|
+
|
193
|
+
def merge(other)
|
194
|
+
other_geometry = Charta.new_geometry(other).transform(srid)
|
195
|
+
feature.union(other_geometry.feature)
|
196
|
+
end
|
197
|
+
alias + merge
|
198
|
+
|
199
|
+
def intersection(other)
|
200
|
+
other_geometry = Charta.new_geometry(other).transform(srid)
|
201
|
+
feature.intersection(other_geometry.feature)
|
202
|
+
end
|
203
|
+
|
204
|
+
def difference(other)
|
205
|
+
other_geometry = Charta.new_geometry(other).transform(srid)
|
206
|
+
feature.difference(other_geometry.feature)
|
207
|
+
end
|
208
|
+
alias - difference
|
209
|
+
|
210
|
+
def bounding_box
|
211
|
+
unless defined? @bounding_box
|
212
|
+
bbox = RGeo::Cartesian::BoundingBox.create_from_geometry(feature)
|
213
|
+
instance_variable_set('@x_min', bbox.min_x || 0)
|
214
|
+
instance_variable_set('@y_min', bbox.min_y || 0)
|
215
|
+
instance_variable_set('@x_max', bbox.max_x || 0)
|
216
|
+
instance_variable_set('@y_max', bbox.max_y || 0)
|
217
|
+
@bounding_box = BoundingBox.new(@y_min, @x_min, @y_max, @x_max)
|
218
|
+
end
|
219
|
+
@bounding_box
|
220
|
+
end
|
221
|
+
|
222
|
+
%i[x_min y_min x_max y_max].each do |name|
|
223
|
+
define_method name do
|
224
|
+
bounding_box.send(name)
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
def find_srid(name_or_srid)
|
229
|
+
Charta.find_srid(name_or_srid)
|
230
|
+
end
|
231
|
+
|
232
|
+
def feature
|
233
|
+
self.class.feature(@ewkt)
|
234
|
+
end
|
235
|
+
|
236
|
+
class << self
|
237
|
+
def srs_database
|
238
|
+
@srs_database ||= RGeo::CoordSys::SRSDatabase::Proj4Data.new('epsg', authority: 'EPSG', cache: true)
|
239
|
+
end
|
240
|
+
|
241
|
+
def factory(srid = 4326)
|
242
|
+
RGeo::Geos.factory(
|
243
|
+
# srs_database: srs_database,
|
244
|
+
srid: srid,
|
245
|
+
# has_z_coordinate: true,
|
246
|
+
wkt_generator: {
|
247
|
+
type_format: :ewkt,
|
248
|
+
emit_ewkt_srid: true,
|
249
|
+
convert_case: :upper
|
250
|
+
},
|
251
|
+
wkt_parser: {
|
252
|
+
support_ewkt: true
|
253
|
+
},
|
254
|
+
wkb_generator: {
|
255
|
+
type_format: :ewkb,
|
256
|
+
emit_ewkb_srid: true,
|
257
|
+
hex_format: true
|
258
|
+
},
|
259
|
+
wkb_parser: {
|
260
|
+
support_ewkb: true
|
261
|
+
}
|
262
|
+
)
|
263
|
+
end
|
264
|
+
|
265
|
+
def feature(ewkt)
|
266
|
+
# Cleans empty geometries
|
267
|
+
ewkt.gsub!(/(GEOMETRYCOLLECTION|GEOMETRY|((MULTI)?(POINT|LINESTRING|POLYGON)))\(\)/, '\1 EMPTY')
|
268
|
+
srs = ewkt.split(/[\=\;]+/)[0..1]
|
269
|
+
srid = nil
|
270
|
+
srid = srs[1] if srs[0] =~ /srid/i
|
271
|
+
srid ||= 4326
|
272
|
+
factory(srid).parse_wkt(ewkt)
|
273
|
+
rescue RGeo::Error::ParseError => e
|
274
|
+
raise "Invalid EWKT (#{e.class.name}: #{e.message}): #{ewkt}"
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
data/lib/charta/gml.rb
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
module Charta
|
4
|
+
# Represents a Geometry with SRID
|
5
|
+
class GML
|
6
|
+
attr_reader :srid
|
7
|
+
|
8
|
+
TAGS = %w[Point LineString Polygon MultiGeometry].freeze
|
9
|
+
OGR_PREFIX = 'ogr'.freeze
|
10
|
+
GML_PREFIX = 'gml'.freeze
|
11
|
+
NS = {
|
12
|
+
gml: 'http://www.opengis.net/gml',
|
13
|
+
ogr: 'http://ogr.maptools.org/'
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
def initialize(data, srid = :WGS84)
|
17
|
+
srid ||= :WGS84
|
18
|
+
@gml = if data.is_a? String
|
19
|
+
|
20
|
+
Nokogiri::XML(data.to_s.split.join(' ')) do |config|
|
21
|
+
config.options = Nokogiri::XML::ParseOptions::NOBLANKS
|
22
|
+
end
|
23
|
+
|
24
|
+
else
|
25
|
+
# Nokogiri::XML::Document expected
|
26
|
+
data
|
27
|
+
end
|
28
|
+
up = false
|
29
|
+
# ensure namespaces are defined
|
30
|
+
begin
|
31
|
+
@gml.root.add_namespace_definition('xmlns', '')
|
32
|
+
NS.each do |k, v|
|
33
|
+
if @gml.xpath("//@*[xmlns:#{k}]").empty?
|
34
|
+
@gml.root.namespace_definitions << @gml.root.add_namespace_definition(k.to_s, v)
|
35
|
+
up = true
|
36
|
+
end
|
37
|
+
end
|
38
|
+
rescue
|
39
|
+
false
|
40
|
+
end
|
41
|
+
|
42
|
+
@gml = Nokogiri::XML(@gml.to_xml) if up
|
43
|
+
|
44
|
+
boundaries = @gml.css("#{GML_PREFIX}|boundedBy")
|
45
|
+
unless boundaries.nil?
|
46
|
+
boundaries.each do |node|
|
47
|
+
srid = Charta.find_srid(node['srsName']) unless node['srsName'].nil?
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
@srid = Charta.find_srid(srid)
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_ewkt
|
55
|
+
"SRID=#{@srid};" + self.class.document_to_ewkt(@gml, @srid)
|
56
|
+
end
|
57
|
+
|
58
|
+
def valid?
|
59
|
+
to_ewkt
|
60
|
+
true
|
61
|
+
end
|
62
|
+
|
63
|
+
class << self
|
64
|
+
# Test is given data is a valid GML
|
65
|
+
def valid?(data, srid = :WGS84)
|
66
|
+
new(data, srid).valid?
|
67
|
+
end
|
68
|
+
|
69
|
+
def object_to_ewkt(fragment, srid)
|
70
|
+
send("#{Charta.underscore(fragment.name)}_to_ewkt", fragment, srid)
|
71
|
+
end
|
72
|
+
|
73
|
+
def document_to_ewkt(gml, srid)
|
74
|
+
# whole document
|
75
|
+
if gml.css("#{OGR_PREFIX}|FeatureCollection").nil? || gml.css("#{GML_PREFIX}|featureMember").nil?
|
76
|
+
# fragment
|
77
|
+
if gml.root.name && TAGS.include?(gml.root.name)
|
78
|
+
object_to_ewkt(gml.root, srid)
|
79
|
+
else
|
80
|
+
'GEOMETRYCOLLECTION EMPTY'
|
81
|
+
end
|
82
|
+
else
|
83
|
+
'GEOMETRYCOLLECTION(' + gml.css("#{GML_PREFIX}|featureMember").collect do |feature|
|
84
|
+
TAGS.collect do |tag|
|
85
|
+
next if feature.css("#{GML_PREFIX}|#{tag}").empty?
|
86
|
+
feature.css("#{GML_PREFIX}|#{tag}").collect do |fragment|
|
87
|
+
object_to_ewkt(fragment, srid)
|
88
|
+
end.compact.join(', ')
|
89
|
+
end.compact.join(', ')
|
90
|
+
end.compact.join(', ') + ')'
|
91
|
+
end
|
92
|
+
end
|
93
|
+
alias geometry_collection_to_ewkt document_to_ewkt
|
94
|
+
|
95
|
+
def transform(data, from_srid, to_srid)
|
96
|
+
Charta.new_geometry(data, from_srid).transform(to_srid).to_text
|
97
|
+
end
|
98
|
+
|
99
|
+
def polygon_to_ewkt(gml, srid)
|
100
|
+
return 'POLYGON EMPTY' if gml.css("#{GML_PREFIX}|coordinates").nil?
|
101
|
+
|
102
|
+
wkt = 'POLYGON(' + %w[outerBoundaryIs innerBoundaryIs].collect do |boundary|
|
103
|
+
next if gml.css("#{GML_PREFIX}|#{boundary}").empty?
|
104
|
+
gml.css("#{GML_PREFIX}|#{boundary}").collect do |hole|
|
105
|
+
'(' + hole.css("#{GML_PREFIX}|coordinates").collect { |coords| coords.content.split(/\r\n|\n| /) }.flatten.reject(&:empty?).collect { |c| c.split ',' }.collect { |dimension| %(#{dimension.first} #{dimension[1]}) }.join(', ') + ')'
|
106
|
+
end.join(', ')
|
107
|
+
end.compact.join(', ') + ')'
|
108
|
+
|
109
|
+
unless gml['srsName'].nil? || Charta.find_srid(gml['srsName']).to_s == srid.to_s
|
110
|
+
wkt = transform(wkt, Charta.find_srid(gml['srsName']), srid)
|
111
|
+
end
|
112
|
+
|
113
|
+
wkt
|
114
|
+
end
|
115
|
+
|
116
|
+
def point_to_ewkt(gml, srid)
|
117
|
+
return 'POINT EMPTY' if gml.css("#{GML_PREFIX}|coordinates").nil?
|
118
|
+
wkt = 'POINT(' + gml.css("#{GML_PREFIX}|coordinates").collect { |coords| coords.content.split ',' }.flatten.join(' ') + ')'
|
119
|
+
|
120
|
+
unless gml['srsName'].nil? || Charta.find_srid(gml['srsName']).to_s == srid.to_s
|
121
|
+
wkt = transform(wkt, Charta.find_srid(gml['srsName']), srid)
|
122
|
+
end
|
123
|
+
|
124
|
+
wkt
|
125
|
+
end
|
126
|
+
|
127
|
+
def line_string_to_ewkt(gml, srid)
|
128
|
+
return 'LINESTRING EMPTY' if gml.css("#{GML_PREFIX}|coordinates").nil?
|
129
|
+
|
130
|
+
wkt = 'LINESTRING(' + gml.css("#{GML_PREFIX}|coordinates").collect { |coords| coords.content.split(/\r\n|\n| /) }.flatten.reject(&:empty?).collect { |c| c.split ',' }.collect { |dimension| %(#{dimension.first} #{dimension[1]}) }.join(', ') + ')'
|
131
|
+
|
132
|
+
unless gml['srsName'].nil? || Charta.find_srid(gml['srsName']).to_s == srid.to_s
|
133
|
+
wkt = transform(wkt, Charta.find_srid(gml['srsName']), srid)
|
134
|
+
end
|
135
|
+
|
136
|
+
wkt
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|