charta 0.1.0
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.
- 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
|