georuby 1.9.3
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.
- data/Gemfile +8 -0
- data/Gemfile.lock +29 -0
- data/History.txt +4 -0
- data/LICENSE +21 -0
- data/README.rdoc +184 -0
- data/Rakefile +48 -0
- data/VERSION +1 -0
- data/georuby.gemspec +128 -0
- data/lib/geo_ruby.rb +23 -0
- data/lib/geo_ruby/geojson.rb +129 -0
- data/lib/geo_ruby/georss.rb +133 -0
- data/lib/geo_ruby/gpx.rb +1 -0
- data/lib/geo_ruby/gpx4r/gpx.rb +118 -0
- data/lib/geo_ruby/shp.rb +1 -0
- data/lib/geo_ruby/shp4r/dbf.rb +42 -0
- data/lib/geo_ruby/shp4r/shp.rb +718 -0
- data/lib/geo_ruby/simple_features/envelope.rb +167 -0
- data/lib/geo_ruby/simple_features/ewkb_parser.rb +218 -0
- data/lib/geo_ruby/simple_features/ewkt_parser.rb +336 -0
- data/lib/geo_ruby/simple_features/geometry.rb +236 -0
- data/lib/geo_ruby/simple_features/geometry_collection.rb +144 -0
- data/lib/geo_ruby/simple_features/geometry_factory.rb +81 -0
- data/lib/geo_ruby/simple_features/helper.rb +18 -0
- data/lib/geo_ruby/simple_features/line_string.rb +228 -0
- data/lib/geo_ruby/simple_features/linear_ring.rb +34 -0
- data/lib/geo_ruby/simple_features/multi_line_string.rb +63 -0
- data/lib/geo_ruby/simple_features/multi_point.rb +58 -0
- data/lib/geo_ruby/simple_features/multi_polygon.rb +64 -0
- data/lib/geo_ruby/simple_features/point.rb +381 -0
- data/lib/geo_ruby/simple_features/polygon.rb +175 -0
- data/nofxx-georuby.gemspec +149 -0
- data/spec/data/geojson/feature_collection.json +34 -0
- data/spec/data/georss/atom.xml +21 -0
- data/spec/data/georss/gml.xml +40 -0
- data/spec/data/georss/w3c.xml +22 -0
- data/spec/data/gpx/fells_loop.gpx +1077 -0
- data/spec/data/gpx/long.gpx +1642 -0
- data/spec/data/gpx/long.kml +31590 -0
- data/spec/data/gpx/long.nmea +2220 -0
- data/spec/data/gpx/short.gpx +13634 -0
- data/spec/data/gpx/short.kml +130 -0
- data/spec/data/gpx/tracktreks.gpx +706 -0
- data/spec/data/multipoint.dbf +0 -0
- data/spec/data/multipoint.shp +0 -0
- data/spec/data/multipoint.shx +0 -0
- data/spec/data/point.dbf +0 -0
- data/spec/data/point.shp +0 -0
- data/spec/data/point.shx +0 -0
- data/spec/data/polygon.dbf +0 -0
- data/spec/data/polygon.shp +0 -0
- data/spec/data/polygon.shx +0 -0
- data/spec/data/polyline.dbf +0 -0
- data/spec/data/polyline.shp +0 -0
- data/spec/data/polyline.shx +0 -0
- data/spec/geo_ruby/geojson_spec.rb +147 -0
- data/spec/geo_ruby/georss.rb +218 -0
- data/spec/geo_ruby/georss_spec.rb +14 -0
- data/spec/geo_ruby/gpx4r/gpx_spec.rb +106 -0
- data/spec/geo_ruby/shp4r/shp_spec.rb +239 -0
- data/spec/geo_ruby/simple_features/envelope_spec.rb +47 -0
- data/spec/geo_ruby/simple_features/ewkb_parser_spec.rb +158 -0
- data/spec/geo_ruby/simple_features/ewkt_parser_spec.rb +179 -0
- data/spec/geo_ruby/simple_features/geometry_collection_spec.rb +55 -0
- data/spec/geo_ruby/simple_features/geometry_factory_spec.rb +11 -0
- data/spec/geo_ruby/simple_features/geometry_spec.rb +32 -0
- data/spec/geo_ruby/simple_features/line_string_spec.rb +259 -0
- data/spec/geo_ruby/simple_features/linear_ring_spec.rb +24 -0
- data/spec/geo_ruby/simple_features/multi_line_string_spec.rb +54 -0
- data/spec/geo_ruby/simple_features/multi_point_spec.rb +35 -0
- data/spec/geo_ruby/simple_features/multi_polygon_spec.rb +50 -0
- data/spec/geo_ruby/simple_features/point_spec.rb +356 -0
- data/spec/geo_ruby/simple_features/polygon_spec.rb +122 -0
- data/spec/geo_ruby_spec.rb +27 -0
- data/spec/spec_helper.rb +73 -0
- metadata +228 -0
@@ -0,0 +1,129 @@
|
|
1
|
+
# GeoJSON parser based on the v1.0 spec at http://geojson.org/geojson-spec.html
|
2
|
+
|
3
|
+
module GeoRuby
|
4
|
+
#Raised when an error in the GeoJSON string is detected
|
5
|
+
class GeojsonFormatError < StandardError
|
6
|
+
end
|
7
|
+
|
8
|
+
# Class added to support geojson 'Feature' objects
|
9
|
+
class GeojsonFeature
|
10
|
+
attr_accessor :geometry, :properties, :id
|
11
|
+
|
12
|
+
def initialize(geometry, properties = {}, id = nil)
|
13
|
+
@geometry = geometry
|
14
|
+
@properties = properties
|
15
|
+
@id = id
|
16
|
+
end
|
17
|
+
|
18
|
+
def ==(other)
|
19
|
+
if (self.class != other.class)
|
20
|
+
false
|
21
|
+
else
|
22
|
+
(self.id == other.id) && (self.geometry == other.geometry) && (self.properties == other.properties)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_json(options={})
|
27
|
+
output = {}
|
28
|
+
output[:type] = 'Feature'
|
29
|
+
output[:geometry] = geometry
|
30
|
+
output[:properties] = properties
|
31
|
+
output[:id] = id unless id.nil?
|
32
|
+
output.to_json(options)
|
33
|
+
end
|
34
|
+
alias :as_geojson :to_json
|
35
|
+
end
|
36
|
+
|
37
|
+
# Class added to support geojson 'Feature Collection' objects
|
38
|
+
class GeojsonFeatureCollection
|
39
|
+
attr_accessor :features
|
40
|
+
|
41
|
+
def initialize(features)
|
42
|
+
@features = features
|
43
|
+
end
|
44
|
+
|
45
|
+
def ==(other)
|
46
|
+
if (self.class != other.class) || (features.size != other.features.size)
|
47
|
+
return false
|
48
|
+
else
|
49
|
+
features.each_index do |index|
|
50
|
+
return false if self.features[index] != other.features[index]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
true
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_json(options = {})
|
57
|
+
{:type => 'FeatureCollection',
|
58
|
+
:features => features}.to_json(options)
|
59
|
+
end
|
60
|
+
alias :as_geojson :to_json
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
class GeojsonParser
|
65
|
+
attr_reader :geometry
|
66
|
+
|
67
|
+
def parse(geojson, srid=DEFAULT_SRID)
|
68
|
+
@geometry = nil
|
69
|
+
geohash = JSON.parse(geojson)
|
70
|
+
parse_geohash(geohash, srid)
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def parse_geohash(geohash, srid)
|
76
|
+
srid = srid_from_crs(geohash['crs']) || srid
|
77
|
+
case geohash['type']
|
78
|
+
when 'Point', 'MultiPoint', 'LineString', 'MultiLineString', 'Polygon', 'MultiPolygon', 'GeometryCollection'
|
79
|
+
@geometry = parse_geometry(geohash, srid)
|
80
|
+
when 'Feature'
|
81
|
+
@geometry = parse_geojson_feature(geohash, srid)
|
82
|
+
when 'FeatureCollection'
|
83
|
+
@geometry = parse_geojson_feature_collection(geohash, srid)
|
84
|
+
else
|
85
|
+
GeojsonFormatError.new('Unknown GeoJSON type')
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def parse_geometry(geohash, srid)
|
90
|
+
srid = srid_from_crs(geohash['crs']) || srid
|
91
|
+
if geohash['type'] == 'GeometryCollection'
|
92
|
+
parse_geometry_collection(geohash, srid)
|
93
|
+
else
|
94
|
+
klass = GeoRuby::SimpleFeatures.const_get(geohash['type'])
|
95
|
+
klass.from_coordinates(geohash['coordinates'], srid, false, false)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def parse_geometry_collection(geohash, srid)
|
100
|
+
srid = srid_from_crs(geohash['crs']) || srid
|
101
|
+
geometries = geohash['geometries'].map{|g| parse_geometry(g,srid)}
|
102
|
+
GeometryCollection.from_geometries(geometries,srid)
|
103
|
+
end
|
104
|
+
|
105
|
+
def parse_geojson_feature(geohash, srid)
|
106
|
+
srid = srid_from_crs(geohash['crs']) || srid
|
107
|
+
geometry = parse_geometry(geohash['geometry'],srid)
|
108
|
+
GeojsonFeature.new(geometry, geohash['properties'], geohash['id'])
|
109
|
+
end
|
110
|
+
|
111
|
+
def parse_geojson_feature_collection(geohash, srid)
|
112
|
+
srid = srid_from_crs(geohash['crs']) || srid
|
113
|
+
features = []
|
114
|
+
geohash['features'].each do |feature|
|
115
|
+
features << parse_geojson_feature(feature, srid)
|
116
|
+
end
|
117
|
+
GeojsonFeatureCollection.new(features)
|
118
|
+
end
|
119
|
+
|
120
|
+
def srid_from_crs(crs)
|
121
|
+
# We somehow need to map crs to srid, currently only support for EPSG
|
122
|
+
if crs && crs['type'] == 'OGC'
|
123
|
+
urn = crs['properties']['urn'].split(':')
|
124
|
+
return urn.last if urn[4] == 'EPSG'
|
125
|
+
end
|
126
|
+
return nil
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# require 'geo_ruby/simple_features/point'
|
2
|
+
# require 'geo_ruby/simple_features/line_string'
|
3
|
+
# require 'geo_ruby/simple_features/linear_ring'
|
4
|
+
# require 'geo_ruby/simple_features/polygon'
|
5
|
+
# require 'geo_ruby/simple_features/multi_point'
|
6
|
+
# require 'geo_ruby/simple_features/multi_line_string'
|
7
|
+
# require 'geo_ruby/simple_features/multi_polygon'
|
8
|
+
# require 'geo_ruby/simple_features/geometry_collection'
|
9
|
+
# require 'geo_ruby/simple_features/envelope'
|
10
|
+
|
11
|
+
module GeoRuby
|
12
|
+
|
13
|
+
#Raised when an error in the GeoRSS string is detected
|
14
|
+
class GeorssFormatError < StandardError
|
15
|
+
end
|
16
|
+
|
17
|
+
#Contains tags possibly found on GeoRss Simple geometries
|
18
|
+
class GeorssTags < Struct.new(:featuretypetag,:relationshiptag,:elev,:floor,:radius)
|
19
|
+
end
|
20
|
+
|
21
|
+
#Parses GeoRSS strings
|
22
|
+
#You can also use directly the static method Geometry.from_georss
|
23
|
+
class GeorssParser
|
24
|
+
attr_reader :georss_tags, :geometry
|
25
|
+
|
26
|
+
#Parses the georss geometry passed as argument and notifies the factory of events
|
27
|
+
#The parser assumes
|
28
|
+
def parse(georss,with_tags = false)
|
29
|
+
@geometry = nil
|
30
|
+
@georss_tags = GeorssTags.new
|
31
|
+
parse_geometry(georss,with_tags)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
def parse_geometry(georss,with_tags)
|
36
|
+
georss.strip!
|
37
|
+
#check for W3CGeo first
|
38
|
+
if georss =~ /<[^:>]*:lat\s*>([^<]*)</
|
39
|
+
#if valid, it is W3CGeo
|
40
|
+
lat = $1.to_f
|
41
|
+
if georss =~ /<[^:>]*:long\s*>([^<]*)</
|
42
|
+
lon = $1.to_f
|
43
|
+
@geometry = Point.from_x_y(lon,lat)
|
44
|
+
else
|
45
|
+
raise GeorssFormatError.new("Bad W3CGeo GeoRSS format")
|
46
|
+
end
|
47
|
+
elsif georss =~ /^<\s*[^:>]*:where\s*>/
|
48
|
+
#GML format found
|
49
|
+
gml = $'.strip
|
50
|
+
if gml =~ /^<\s*[^:>]*:Point\s*>/
|
51
|
+
#gml point
|
52
|
+
if gml =~ /<\s*[^:>]*:pos\s*>([^<]*)/
|
53
|
+
point = $1.split(" ")
|
54
|
+
#lat comes first
|
55
|
+
@geometry = Point.from_x_y(point[1].to_f,point[0].to_f)
|
56
|
+
else
|
57
|
+
raise GeorssFormatError.new("Bad GML GeoRSS format: Malformed Point")
|
58
|
+
end
|
59
|
+
elsif gml =~ /^<\s*[^:>]*:LineString\s*>/
|
60
|
+
if gml =~ /<\s*[^:>]*:posList\s*>([^<]*)/
|
61
|
+
xy = $1.split(" ")
|
62
|
+
@geometry = LineString.new
|
63
|
+
0.upto(xy.size/2 - 1) { |index| @geometry << Point.from_x_y(xy[index*2 + 1].to_f,xy[index*2].to_f)}
|
64
|
+
else
|
65
|
+
raise GeorssFormatError.new("Bad GML GeoRSS format: Malformed LineString")
|
66
|
+
end
|
67
|
+
elsif gml =~ /^<\s*[^:>]*:Polygon\s*>/
|
68
|
+
if gml =~ /<\s*[^:>]*:posList\s*>([^<]*)/
|
69
|
+
xy = $1.split(" ")
|
70
|
+
@geometry = Polygon.new
|
71
|
+
linear_ring = LinearRing.new
|
72
|
+
@geometry << linear_ring
|
73
|
+
xy = $1.split(" ")
|
74
|
+
0.upto(xy.size/2 - 1) { |index| linear_ring << Point.from_x_y(xy[index*2 + 1].to_f,xy[index*2].to_f)}
|
75
|
+
else
|
76
|
+
raise GeorssFormatError.new("Bad GML GeoRSS format: Malformed Polygon")
|
77
|
+
end
|
78
|
+
elsif gml =~ /^<\s*[^:>]*:Envelope\s*>/
|
79
|
+
if gml =~ /<\s*[^:>]*:lowerCorner\s*>([^<]*)</
|
80
|
+
lc = $1.split(" ").collect { |x| x.to_f}.reverse
|
81
|
+
if gml =~ /<\s*[^:>]*:upperCorner\s*>([^<]*)</
|
82
|
+
uc = $1.split(" ").collect { |x| x.to_f}.reverse
|
83
|
+
@geometry = Envelope.from_coordinates([lc,uc])
|
84
|
+
else
|
85
|
+
raise GeorssFormatError.new("Bad GML GeoRSS format: Malformed Envelope")
|
86
|
+
end
|
87
|
+
else
|
88
|
+
raise GeorssFormatError.new("Bad GML GeoRSS format: Malformed Envelope")
|
89
|
+
end
|
90
|
+
else
|
91
|
+
raise GeorssFormatError.new("Bad GML GeoRSS format: Unknown geometry type")
|
92
|
+
end
|
93
|
+
else
|
94
|
+
#must be simple format
|
95
|
+
if georss =~ /^<\s*[^>:]*:point([^>]*)>(.*)</m
|
96
|
+
tags = $1
|
97
|
+
point = $2.gsub(","," ").split(" ")
|
98
|
+
@geometry = Point.from_x_y(point[1].to_f,point[0].to_f)
|
99
|
+
elsif georss =~ /^<\s*[^>:]*:line([^>]*)>(.*)</m
|
100
|
+
tags = $1
|
101
|
+
@geometry = LineString.new
|
102
|
+
xy = $2.gsub(","," ").split(" ")
|
103
|
+
0.upto(xy.size/2 - 1) { |index| @geometry << Point.from_x_y(xy[index*2 + 1].to_f,xy[index*2].to_f)}
|
104
|
+
elsif georss =~ /^<\s*[^>:]*:polygon([^>]*)>(.*)</m
|
105
|
+
tags = $1
|
106
|
+
@geometry = Polygon.new
|
107
|
+
linear_ring = LinearRing.new
|
108
|
+
@geometry << linear_ring
|
109
|
+
xy = $2.gsub(","," ").split(" ")
|
110
|
+
0.upto(xy.size/2 - 1) { |index| linear_ring << Point.from_x_y(xy[index*2 + 1].to_f,xy[index*2].to_f)}
|
111
|
+
elsif georss =~ /^<\s*[^>:]*:box([^>]*)>(.*)</m
|
112
|
+
tags = $1
|
113
|
+
corners = []
|
114
|
+
xy = $2.gsub(","," ").split(" ")
|
115
|
+
0.upto(xy.size/2 - 1) {|index| corners << Point.from_x_y(xy[index*2 + 1].to_f,xy[index*2].to_f)}
|
116
|
+
@geometry = Envelope.from_points(corners)
|
117
|
+
else
|
118
|
+
raise GeorssFormatError.new("Bad Simple GeoRSS format: Unknown geometry type")
|
119
|
+
end
|
120
|
+
|
121
|
+
#geometry found: parse tags
|
122
|
+
return unless with_tags
|
123
|
+
|
124
|
+
@georss_tags.featuretypetag = $1 if tags =~ /featuretypetag=['"]([^"']*)['"]/
|
125
|
+
@georss_tags.relationshiptag = $1 if tags =~ /relationshiptag=['"]([^'"]*)['"]/
|
126
|
+
@georss_tags.elev = $1.to_f if tags =~ /elev=['"]([^'"]*)['"]/
|
127
|
+
@georss_tags.floor = $1.to_i if tags =~ /floor=['"]([^'"]*)['"]/
|
128
|
+
@georss_tags.radius = $1.to_f if tags =~ /radius=['"]([^'"]*)['"]/
|
129
|
+
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
data/lib/geo_ruby/gpx.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'geo_ruby/gpx4r/gpx'
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "nokogiri"
|
3
|
+
|
4
|
+
module GeoRuby
|
5
|
+
module Gpx4r
|
6
|
+
|
7
|
+
#An interface to GPX files
|
8
|
+
class GpxFile
|
9
|
+
attr_reader :record_count, :file_root #:xmin, :ymin, :xmax, :ymax, :zmin, :zmax, :mmin, :mmax, :file_length
|
10
|
+
|
11
|
+
include Enumerable
|
12
|
+
|
13
|
+
# Opens a GPX file. Both "abc.shp" and "abc" are accepted.
|
14
|
+
def initialize(file, *opts) #with_z = true, with_m = true)
|
15
|
+
@file_root = file.gsub(/\.gpx$/i,"")
|
16
|
+
raise MalformedGpxException.new("Missing GPX File") unless
|
17
|
+
File.exists? @file_root + ".gpx"
|
18
|
+
@points, @envelope = [], nil
|
19
|
+
@gpx = File.open(@file_root + ".gpx", "rb")
|
20
|
+
opt = opts.inject({}) { |o, h| h.merge(o) }
|
21
|
+
parse_file(opt[:with_z], opt[:with_m])
|
22
|
+
end
|
23
|
+
|
24
|
+
#force the reopening of the files compsing the shp. Close before calling this.
|
25
|
+
def reload!
|
26
|
+
initialize(@file_root)
|
27
|
+
end
|
28
|
+
|
29
|
+
#opens a GPX "file". If a block is given, the GpxFile object is yielded to it and is closed upon return. Else a call to <tt>open</tt> is equivalent to <tt>GpxFile.new(...)</tt>.
|
30
|
+
def self.open(file, *opts)
|
31
|
+
gpxfile = GpxFile.new(file, *opts)
|
32
|
+
if block_given?
|
33
|
+
yield gpxfile
|
34
|
+
# gpxfile.close
|
35
|
+
else
|
36
|
+
gpxfile
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
#Closes a gpxfile
|
41
|
+
def close
|
42
|
+
@gpx.close
|
43
|
+
end
|
44
|
+
|
45
|
+
#Tests if the file has no record
|
46
|
+
def empty?
|
47
|
+
record_count == 0
|
48
|
+
end
|
49
|
+
|
50
|
+
#Goes through each record
|
51
|
+
def each
|
52
|
+
(0...record_count).each do |i|
|
53
|
+
yield get_record(i)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
alias :each_record :each
|
57
|
+
|
58
|
+
#Returns record +i+
|
59
|
+
def [](i)
|
60
|
+
get_record(i)
|
61
|
+
end
|
62
|
+
|
63
|
+
#Returns all the records
|
64
|
+
def records
|
65
|
+
@points
|
66
|
+
end
|
67
|
+
|
68
|
+
# Return the GPX file as LineString
|
69
|
+
def as_line_string
|
70
|
+
GeoRuby::SimpleFeatures::LineString.from_points(@points)
|
71
|
+
end
|
72
|
+
alias :as_polyline :as_line_string
|
73
|
+
|
74
|
+
# Return the GPX file as a Polygon
|
75
|
+
# If the GPX isn't closed, a line from the first
|
76
|
+
# to the last point will be created to close it.
|
77
|
+
def as_polygon
|
78
|
+
GeoRuby::SimpleFeatures::Polygon.from_points([@points[0] == @points[-1] ? @points : @points.push(@points[0].clone)])
|
79
|
+
end
|
80
|
+
|
81
|
+
# Return GPX Envelope
|
82
|
+
def envelope
|
83
|
+
@envelope ||= as_polygon.envelope
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def get_record(i)
|
89
|
+
@points[i]
|
90
|
+
end
|
91
|
+
|
92
|
+
# wpt => waypoint => TODO?
|
93
|
+
# rte(pt) => route
|
94
|
+
# trk(pt) => track /
|
95
|
+
def parse_file(with_z, with_m)
|
96
|
+
data = @gpx.read
|
97
|
+
@file_mode = data =~ /trkpt/ ? "//trkpt" : (data =~ /wpt/ ? "//wpt" : "//rtept")
|
98
|
+
Nokogiri.HTML(data).search(@file_mode).each do |tp|
|
99
|
+
z = z.inner_text.to_f if with_z && z = tp.at("ele")
|
100
|
+
m = m.inner_text if with_m && m = tp.at("time")
|
101
|
+
@points << GeoRuby::SimpleFeatures::Point.from_coordinates([tp["lon"].to_f, tp["lat"].to_f, z, m],4326,with_z, with_m)
|
102
|
+
end
|
103
|
+
close
|
104
|
+
@record_count = @points.length
|
105
|
+
self.envelope
|
106
|
+
rescue => e
|
107
|
+
raise MalformedGpxException.new("Bad GPX. Error: #{e}")
|
108
|
+
# trackpoint.at("gpxdata:hr").nil? # heartrate
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
class MalformedGpxException < StandardError
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
data/lib/geo_ruby/shp.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'geo_ruby/shp4r/shp'
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# Uses the dbf lib, Copyright 2006 Keith Morrison (http://infused.org)
|
2
|
+
# Modified to work as external gem now
|
3
|
+
require 'rubygems'
|
4
|
+
begin
|
5
|
+
require 'dbf'
|
6
|
+
rescue LoadError
|
7
|
+
puts "You've loaded GeoRuby SHP Support."
|
8
|
+
puts "Please install the gem 'dbf' to use it. `gem install dbf`"
|
9
|
+
end
|
10
|
+
|
11
|
+
module GeoRuby
|
12
|
+
module Shp4r
|
13
|
+
Dbf = DBF
|
14
|
+
|
15
|
+
module Dbf
|
16
|
+
class Record
|
17
|
+
def [](v)
|
18
|
+
attributes[v.downcase]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class Field < Column
|
23
|
+
def initialize(name, type, length, decimal = 0)
|
24
|
+
super(name, type, length, decimal)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class Reader < Table
|
29
|
+
alias_method :fields, :columns
|
30
|
+
def header_length
|
31
|
+
@columns_count
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.open(f)
|
35
|
+
new(f)
|
36
|
+
end
|
37
|
+
|
38
|
+
def close(); nil; end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,718 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'fileutils' if !defined?(FileUtils)
|
3
|
+
require File.dirname(__FILE__) + '/dbf'
|
4
|
+
|
5
|
+
|
6
|
+
module GeoRuby
|
7
|
+
module Shp4r
|
8
|
+
|
9
|
+
#Enumerates all the types of SHP geometries. The MULTIPATCH one is the only one not currently supported by Geo_ruby.
|
10
|
+
module ShpType
|
11
|
+
NULL_SHAPE = 0
|
12
|
+
POINT = 1
|
13
|
+
POLYLINE = 3
|
14
|
+
POLYGON = 5
|
15
|
+
MULTIPOINT = 8
|
16
|
+
POINTZ = 11
|
17
|
+
POLYLINEZ = 13
|
18
|
+
POLYGONZ = 15
|
19
|
+
MULTIPOINTZ = 18
|
20
|
+
POINTM = 21
|
21
|
+
POLYLINEM = 23
|
22
|
+
POLYGONM = 25
|
23
|
+
MULTIPOINTM = 28
|
24
|
+
end
|
25
|
+
|
26
|
+
#An interface to an ESRI shapefile (actually 3 files : shp, shx and dbf). Currently supports only the reading of geometries.
|
27
|
+
class ShpFile
|
28
|
+
attr_reader :shp_type, :record_count, :xmin, :ymin, :xmax, :ymax, :zmin, :zmax, :mmin, :mmax, :file_root, :file_length
|
29
|
+
|
30
|
+
include Enumerable
|
31
|
+
|
32
|
+
#Opens a SHP file. Both "abc.shp" and "abc" are accepted. The files "abc.shp", "abc.shx" and "abc.dbf" must be present
|
33
|
+
def initialize(file)
|
34
|
+
#strip the shp out of the file if present
|
35
|
+
@file_root = file.gsub(/.shp$/i,"")
|
36
|
+
#check existence of shp, dbf and shx files
|
37
|
+
unless File.exists?(@file_root + ".shp") and File.exists?(@file_root + ".dbf") and File.exists?(@file_root + ".shx")
|
38
|
+
raise MalformedShpException.new("Missing one of shp, dbf or shx for: #{@file}")
|
39
|
+
end
|
40
|
+
|
41
|
+
@dbf = Dbf::Reader.open(@file_root + ".dbf")
|
42
|
+
@shx = File.open(@file_root + ".shx","rb")
|
43
|
+
@shp = File.open(@file_root + ".shp","rb")
|
44
|
+
read_index
|
45
|
+
end
|
46
|
+
|
47
|
+
#force the reopening of the files compsing the shp. Close before calling this.
|
48
|
+
def reload!
|
49
|
+
initialize(@file_root)
|
50
|
+
end
|
51
|
+
|
52
|
+
#opens a SHP "file". If a block is given, the ShpFile object is yielded to it and is closed upon return. Else a call to <tt>open</tt> is equivalent to <tt>ShpFile.new(...)</tt>.
|
53
|
+
def self.open(file)
|
54
|
+
shpfile = ShpFile.new(file)
|
55
|
+
if block_given?
|
56
|
+
yield shpfile
|
57
|
+
shpfile.close
|
58
|
+
else
|
59
|
+
shpfile
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
#create a new Shapefile of the specified shp type (see ShpType) and with the attribute specified in the +fields+ array (see Dbf::Field). If a block is given, the ShpFile object newly created is passed to it.
|
64
|
+
def self.create(file,shp_type,fields,&proc)
|
65
|
+
file_root = file.gsub(/.shp$/i,"")
|
66
|
+
shx_io = File.open(file_root + ".shx","wb")
|
67
|
+
shp_io = File.open(file_root + ".shp","wb")
|
68
|
+
dbf_io = File.open(file_root + ".dbf","wb")
|
69
|
+
str = [9994,0,0,0,0,0,50,1000,shp_type,0,0,0,0,0,0,0,0].pack("N7V2E8")
|
70
|
+
shp_io << str
|
71
|
+
shx_io << str
|
72
|
+
rec_length = 1 + fields.inject(0) {|s,f| s + f.length} #+1 for the prefixed space (active record marker)
|
73
|
+
dbf_io << [3,107,7,7,0,33 + 32 * fields.length,rec_length ].pack("c4Vv2x20") #32 bytes for first part of header
|
74
|
+
fields.each do |field|
|
75
|
+
dbf_io << [field.name,field.type,field.length,field.decimal].pack("a10xax4CCx14")
|
76
|
+
end
|
77
|
+
dbf_io << ['0d'].pack("H2")
|
78
|
+
|
79
|
+
shx_io.close
|
80
|
+
shp_io.close
|
81
|
+
dbf_io.close
|
82
|
+
|
83
|
+
open(file,&proc)
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
#Closes a shapefile
|
88
|
+
def close
|
89
|
+
@dbf.close
|
90
|
+
@shx.close
|
91
|
+
@shp.close
|
92
|
+
end
|
93
|
+
|
94
|
+
#starts a transaction, to buffer physical file operations on the shapefile components.
|
95
|
+
def transaction
|
96
|
+
trs = ShpTransaction.new(self,@dbf)
|
97
|
+
if block_given?
|
98
|
+
answer = yield trs
|
99
|
+
if answer == :rollback
|
100
|
+
trs.rollback
|
101
|
+
elsif !trs.rollbacked
|
102
|
+
trs.commit
|
103
|
+
end
|
104
|
+
else
|
105
|
+
trs
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
#return the description of data fields
|
110
|
+
def fields
|
111
|
+
@dbf.fields
|
112
|
+
end
|
113
|
+
|
114
|
+
#Tests if the file has no record
|
115
|
+
def empty?
|
116
|
+
record_count == 0
|
117
|
+
end
|
118
|
+
|
119
|
+
#Goes through each record
|
120
|
+
def each
|
121
|
+
(0...record_count).each do |i|
|
122
|
+
yield get_record(i)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
alias :each_record :each
|
126
|
+
|
127
|
+
#Returns record +i+
|
128
|
+
def [](i)
|
129
|
+
get_record(i)
|
130
|
+
end
|
131
|
+
|
132
|
+
#Returns all the records
|
133
|
+
def records
|
134
|
+
Array.new(record_count) do |i|
|
135
|
+
get_record(i)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
private
|
140
|
+
def read_index
|
141
|
+
@file_length, @shp_type, @xmin, @ymin, @xmax, @ymax, @zmin, @zmax, @mmin,@mmax = @shx.read(100).unpack("x24Nx4VE8")
|
142
|
+
@record_count = (@file_length - 50) / 4
|
143
|
+
if @record_count == 0
|
144
|
+
#initialize the bboxes to default values so if data added, they will be replaced
|
145
|
+
@xmin, @ymin, @xmax, @ymax, @zmin, @zmax, @mmin,@mmax = Float::MAX, Float::MAX, -Float::MAX, -Float::MAX, Float::MAX, -Float::MAX, Float::MAX, -Float::MAX
|
146
|
+
end
|
147
|
+
unless @record_count == @dbf.record_count
|
148
|
+
raise MalformedShpException.new("Not the same number of records in SHP and DBF")
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
#TODO : refactor to minimize redundant code
|
153
|
+
def get_record(i)
|
154
|
+
return nil if record_count <= i or i < 0
|
155
|
+
dbf_record = @dbf.record(i)
|
156
|
+
@shx.seek(100 + 8 * i) #100 is the header length
|
157
|
+
offset,length = @shx.read(8).unpack("N2")
|
158
|
+
@shp.seek(offset * 2 + 8)
|
159
|
+
rec_shp_type = @shp.read(4).unpack("V")[0]
|
160
|
+
|
161
|
+
case(rec_shp_type)
|
162
|
+
when ShpType::POINT
|
163
|
+
x, y = @shp.read(16).unpack("E2")
|
164
|
+
geometry = GeoRuby::SimpleFeatures::Point.from_x_y(x,y)
|
165
|
+
when ShpType::POLYLINE #actually creates a multi_polyline
|
166
|
+
@shp.seek(32,IO::SEEK_CUR) #extent
|
167
|
+
num_parts, num_points = @shp.read(8).unpack("V2")
|
168
|
+
parts = @shp.read(num_parts * 4).unpack("V" + num_parts.to_s)
|
169
|
+
parts << num_points #indexes for LS of idx i go to parts of idx i to idx i +1
|
170
|
+
points = Array.new(num_points) do
|
171
|
+
x, y = @shp.read(16).unpack("E2")
|
172
|
+
GeoRuby::SimpleFeatures::Point.from_x_y(x,y)
|
173
|
+
end
|
174
|
+
line_strings = Array.new(num_parts) do |i|
|
175
|
+
GeoRuby::SimpleFeatures::LineString.from_points(points[(parts[i])...(parts[i+1])])
|
176
|
+
end
|
177
|
+
geometry = GeoRuby::SimpleFeatures::MultiLineString.from_line_strings(line_strings)
|
178
|
+
when ShpType::POLYGON
|
179
|
+
#TODO : TO CORRECT
|
180
|
+
#does not take into account the possibility that the outer loop could be after the inner loops in the SHP + more than one outer loop
|
181
|
+
#Still sends back a multi polygon (so the correction above won't change what gets sent back)
|
182
|
+
@shp.seek(32,IO::SEEK_CUR)
|
183
|
+
num_parts, num_points = @shp.read(8).unpack("V2")
|
184
|
+
parts = @shp.read(num_parts * 4).unpack("V" + num_parts.to_s)
|
185
|
+
parts << num_points #indexes for LS of idx i go to parts of idx i to idx i +1
|
186
|
+
points = Array.new(num_points) do
|
187
|
+
x, y = @shp.read(16).unpack("E2")
|
188
|
+
GeoRuby::SimpleFeatures::Point.from_x_y(x,y)
|
189
|
+
end
|
190
|
+
linear_rings = Array.new(num_parts) do |i|
|
191
|
+
GeoRuby::SimpleFeatures::LinearRing.from_points(points[(parts[i])...(parts[i+1])])
|
192
|
+
end
|
193
|
+
# geometry = GeoRuby::SimpleFeatures::MultiPolygon.from_polygons([GeoRuby::SimpleFeatures::Polygon.from_linear_rings(linear_rings)])
|
194
|
+
outer, inner = linear_rings.partition { |lr| lr.clockwise? }
|
195
|
+
|
196
|
+
# Make polygons from the outer rings so we can concatenate
|
197
|
+
# them with inner rings.
|
198
|
+
outer.map! { |ring| GeoRuby::SimpleFeatures::Polygon.from_linear_rings([ring]) }
|
199
|
+
|
200
|
+
# We make the assumption that all vertices of holes are
|
201
|
+
# entirely contained.
|
202
|
+
inner.each do |inner_ring|
|
203
|
+
outer_poly = outer.find { |outer_poly| outer_poly[0].contains_point?(inner_ring[0]) }
|
204
|
+
if outer_poly
|
205
|
+
outer_poly << inner_ring
|
206
|
+
else
|
207
|
+
# TODO - what to do here? technically the geometry is
|
208
|
+
# not well formed (or our above assumption does not
|
209
|
+
# hold).
|
210
|
+
$stderr.puts "Failed to find polygon for inner ring!"
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
geometry = GeoRuby::SimpleFeatures::MultiPolygon.from_polygons(outer)
|
215
|
+
when ShpType::MULTIPOINT
|
216
|
+
@shp.seek(32,IO::SEEK_CUR)
|
217
|
+
num_points = @shp.read(4).unpack("V")[0]
|
218
|
+
points = Array.new(num_points) do
|
219
|
+
x, y = @shp.read(16).unpack("E2")
|
220
|
+
GeoRuby::SimpleFeatures::Point.from_x_y(x,y)
|
221
|
+
end
|
222
|
+
geometry = GeoRuby::SimpleFeatures::MultiPoint.from_points(points)
|
223
|
+
|
224
|
+
|
225
|
+
when ShpType::POINTZ
|
226
|
+
x, y, z, m = @shp.read(24).unpack("E4")
|
227
|
+
geometry = GeoRuby::SimpleFeatures::Point.from_x_y_z_m(x,y,z,m)
|
228
|
+
|
229
|
+
|
230
|
+
when ShpType::POLYLINEZ
|
231
|
+
@shp.seek(32,IO::SEEK_CUR)
|
232
|
+
num_parts, num_points = @shp.read(8).unpack("V2")
|
233
|
+
parts = @shp.read(num_parts * 4).unpack("V" + num_parts.to_s)
|
234
|
+
parts << num_points #indexes for LS of idx i go to parts of idx i to idx i +1
|
235
|
+
xys = Array.new(num_points) { @shp.read(16).unpack("E2") }
|
236
|
+
@shp.seek(16,IO::SEEK_CUR)
|
237
|
+
zs = Array.new(num_points) {@shp.read(8).unpack("E")[0]}
|
238
|
+
@shp.seek(16,IO::SEEK_CUR)
|
239
|
+
ms = Array.new(num_points) {@shp.read(8).unpack("E")[0]}
|
240
|
+
points = Array.new(num_points) do |i|
|
241
|
+
GeoRuby::SimpleFeatures::Point.from_x_y_z_m(xys[i][0],xys[i][1],zs[i],ms[i])
|
242
|
+
end
|
243
|
+
line_strings = Array.new(num_parts) do |i|
|
244
|
+
GeoRuby::SimpleFeatures::LineString.from_points(points[(parts[i])...(parts[i+1])],GeoRuby::SimpleFeatures::DEFAULT_SRID,true,true)
|
245
|
+
end
|
246
|
+
geometry = GeoRuby::SimpleFeatures::MultiLineString.from_line_strings(line_strings,GeoRuby::SimpleFeatures::DEFAULT_SRID,true,true)
|
247
|
+
|
248
|
+
|
249
|
+
when ShpType::POLYGONZ
|
250
|
+
#TODO : CORRECT
|
251
|
+
|
252
|
+
@shp.seek(32,IO::SEEK_CUR)#extent
|
253
|
+
num_parts, num_points = @shp.read(8).unpack("V2")
|
254
|
+
parts = @shp.read(num_parts * 4).unpack("V" + num_parts.to_s)
|
255
|
+
parts << num_points #indexes for LS of idx i go to parts of idx i to idx i +1
|
256
|
+
xys = Array.new(num_points) { @shp.read(16).unpack("E2") }
|
257
|
+
@shp.seek(16,IO::SEEK_CUR)#extent
|
258
|
+
zs = Array.new(num_points) {@shp.read(8).unpack("E")[0]}
|
259
|
+
@shp.seek(16,IO::SEEK_CUR)#extent
|
260
|
+
ms = Array.new(num_points) {@shp.read(8).unpack("E")[0]}
|
261
|
+
points = Array.new(num_points) do |i|
|
262
|
+
GeoRuby::SimpleFeatures::Point.from_x_y_z_m(xys[i][0],xys[i][1],zs[i],ms[i])
|
263
|
+
end
|
264
|
+
linear_rings = Array.new(num_parts) do |i|
|
265
|
+
GeoRuby::SimpleFeatures::LinearRing.from_points(points[(parts[i])...(parts[i+1])],GeoRuby::SimpleFeatures::DEFAULT_SRID,true,true)
|
266
|
+
end
|
267
|
+
geometry = GeoRuby::SimpleFeatures::MultiPolygon.from_polygons([GeoRuby::SimpleFeatures::Polygon.from_linear_rings(linear_rings)],GeoRuby::SimpleFeatures::DEFAULT_SRID,true,true)
|
268
|
+
|
269
|
+
|
270
|
+
when ShpType::MULTIPOINTZ
|
271
|
+
@shp.seek(32,IO::SEEK_CUR)
|
272
|
+
num_points = @shp.read(4).unpack("V")[0]
|
273
|
+
xys = Array.new(num_points) { @shp.read(16).unpack("E2") }
|
274
|
+
@shp.seek(16,IO::SEEK_CUR)
|
275
|
+
zs = Array.new(num_points) {@shp.read(8).unpack("E")[0]}
|
276
|
+
@shp.seek(16,IO::SEEK_CUR)
|
277
|
+
ms = Array.new(num_points) {@shp.read(8).unpack("E")[0]}
|
278
|
+
|
279
|
+
points = Array.new(num_points) do |i|
|
280
|
+
GeoRuby::SimpleFeatures::Point.from_x_y_z_m(xys[i][0],xys[i][1],zs[i],ms[i])
|
281
|
+
end
|
282
|
+
|
283
|
+
geometry = GeoRuby::SimpleFeatures::MultiPoint.from_points(points,GeoRuby::SimpleFeatures::DEFAULT_SRID,true,true)
|
284
|
+
|
285
|
+
when ShpType::POINTM
|
286
|
+
x, y, m = @shp.read(24).unpack("E3")
|
287
|
+
geometry = GeoRuby::SimpleFeatures::Point.from_x_y_m(x,y,m)
|
288
|
+
|
289
|
+
when ShpType::POLYLINEM
|
290
|
+
@shp.seek(32,IO::SEEK_CUR)
|
291
|
+
num_parts, num_points = @shp.read(8).unpack("V2")
|
292
|
+
parts = @shp.read(num_parts * 4).unpack("V" + num_parts.to_s)
|
293
|
+
parts << num_points #indexes for LS of idx i go to parts of idx i to idx i +1
|
294
|
+
xys = Array.new(num_points) { @shp.read(16).unpack("E2") }
|
295
|
+
@shp.seek(16,IO::SEEK_CUR)
|
296
|
+
ms = Array.new(num_points) {@shp.read(8).unpack("E")[0]}
|
297
|
+
points = Array.new(num_points) do |i|
|
298
|
+
GeoRuby::SimpleFeatures::Point.from_x_y_m(xys[i][0],xys[i][1],ms[i])
|
299
|
+
end
|
300
|
+
line_strings = Array.new(num_parts) do |i|
|
301
|
+
GeoRuby::SimpleFeatures::LineString.from_points(points[(parts[i])...(parts[i+1])],GeoRuby::SimpleFeatures::DEFAULT_SRID,false,true)
|
302
|
+
end
|
303
|
+
geometry = GeoRuby::SimpleFeatures::MultiLineString.from_line_strings(line_strings,GeoRuby::SimpleFeatures::DEFAULT_SRID,false,true)
|
304
|
+
|
305
|
+
|
306
|
+
when ShpType::POLYGONM
|
307
|
+
#TODO : CORRECT
|
308
|
+
|
309
|
+
@shp.seek(32,IO::SEEK_CUR)
|
310
|
+
num_parts, num_points = @shp.read(8).unpack("V2")
|
311
|
+
parts = @shp.read(num_parts * 4).unpack("V" + num_parts.to_s)
|
312
|
+
parts << num_points #indexes for LS of idx i go to parts of idx i to idx i +1
|
313
|
+
xys = Array.new(num_points) { @shp.read(16).unpack("E2") }
|
314
|
+
@shp.seek(16,IO::SEEK_CUR)
|
315
|
+
ms = Array.new(num_points) {@shp.read(8).unpack("E")[0]}
|
316
|
+
points = Array.new(num_points) do |i|
|
317
|
+
GeoRuby::SimpleFeatures::Point.from_x_y_m(xys[i][0],xys[i][1],ms[i])
|
318
|
+
end
|
319
|
+
linear_rings = Array.new(num_parts) do |i|
|
320
|
+
GeoRuby::SimpleFeatures::LinearRing.from_points(points[(parts[i])...(parts[i+1])],GeoRuby::SimpleFeatures::DEFAULT_SRID,false,true)
|
321
|
+
end
|
322
|
+
geometry = GeoRuby::SimpleFeatures::MultiPolygon.from_polygons([GeoRuby::SimpleFeatures::Polygon.from_linear_rings(linear_rings)],GeoRuby::SimpleFeatures::DEFAULT_SRID,false,true)
|
323
|
+
|
324
|
+
|
325
|
+
when ShpType::MULTIPOINTM
|
326
|
+
@shp.seek(32,IO::SEEK_CUR)
|
327
|
+
num_points = @shp.read(4).unpack("V")[0]
|
328
|
+
xys = Array.new(num_points) { @shp.read(16).unpack("E2") }
|
329
|
+
@shp.seek(16,IO::SEEK_CUR)
|
330
|
+
ms = Array.new(num_points) {@shp.read(8).unpack("E")[0]}
|
331
|
+
|
332
|
+
points = Array.new(num_points) do |i|
|
333
|
+
GeoRuby::SimpleFeatures::Point.from_x_y_m(xys[i][0],xys[i][1],ms[i])
|
334
|
+
end
|
335
|
+
|
336
|
+
geometry = GeoRuby::SimpleFeatures::MultiPoint.from_points(points,GeoRuby::SimpleFeatures::DEFAULT_SRID,false,true)
|
337
|
+
else
|
338
|
+
geometry = nil
|
339
|
+
end
|
340
|
+
|
341
|
+
ShpRecord.new(geometry,dbf_record)
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
#A SHP record : contains both the geometry and the data fields (from the DBF)
|
346
|
+
class ShpRecord
|
347
|
+
attr_reader :geometry , :data
|
348
|
+
|
349
|
+
def initialize(geometry, data)
|
350
|
+
@geometry = geometry
|
351
|
+
@data = data
|
352
|
+
end
|
353
|
+
|
354
|
+
#Tests if the geometry is a NULL SHAPE
|
355
|
+
def has_null_shape?
|
356
|
+
@geometry.nil?
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
#An object returned from ShpFile#transaction. Buffers updates to a Shapefile
|
361
|
+
class ShpTransaction
|
362
|
+
attr_reader :rollbacked
|
363
|
+
|
364
|
+
def initialize(shp, dbf)
|
365
|
+
@deleted = Hash.new
|
366
|
+
@added = Array.new
|
367
|
+
@shp = shp
|
368
|
+
@dbf = dbf
|
369
|
+
end
|
370
|
+
|
371
|
+
#delete a record. Does not take into account the records added in the current transaction
|
372
|
+
def delete(i)
|
373
|
+
raise UnexistantRecordException.new("Invalid index : #{i}") if @shp.record_count <= i
|
374
|
+
@deleted[i] = true
|
375
|
+
end
|
376
|
+
|
377
|
+
#Update a record. In effect just a delete followed by an add.
|
378
|
+
def update(i, record)
|
379
|
+
delete(i)
|
380
|
+
add(record)
|
381
|
+
end
|
382
|
+
|
383
|
+
#add a ShpRecord at the end
|
384
|
+
def add(record)
|
385
|
+
record_type = to_shp_type(record.geometry)
|
386
|
+
raise IncompatibleGeometryException.new("Incompatible type") unless record_type==@shp.shp_type
|
387
|
+
@added << record
|
388
|
+
end
|
389
|
+
|
390
|
+
#updates the physical files
|
391
|
+
def commit
|
392
|
+
@shp.close
|
393
|
+
@shp_r = open(@shp.file_root + ".shp", "rb")
|
394
|
+
@dbf_r = open(@shp.file_root + ".dbf", "rb")
|
395
|
+
@shp_io = open(@shp.file_root + ".shp.tmp.shp", "wb")
|
396
|
+
@shx_io = open(@shp.file_root + ".shx.tmp.shx", "wb")
|
397
|
+
@dbf_io = open(@shp.file_root + ".dbf.tmp.dbf", "wb")
|
398
|
+
index = commit_delete
|
399
|
+
min_x,max_x,min_y,max_y,min_z,max_z,min_m,max_m = commit_add(index)
|
400
|
+
commit_finalize(min_x,max_x,min_y,max_y,min_z,max_z,min_m,max_m)
|
401
|
+
@shp_r.close
|
402
|
+
@dbf_r.close
|
403
|
+
@dbf_io.close
|
404
|
+
@shp_io.close
|
405
|
+
@shx_io.close
|
406
|
+
FileUtils.move(@shp.file_root + ".shp.tmp.shp", @shp.file_root + ".shp")
|
407
|
+
FileUtils.move(@shp.file_root + ".shx.tmp.shx", @shp.file_root + ".shx")
|
408
|
+
FileUtils.move(@shp.file_root + ".dbf.tmp.dbf", @shp.file_root + ".dbf")
|
409
|
+
|
410
|
+
@deleted = Hash.new
|
411
|
+
@added = Array.new
|
412
|
+
|
413
|
+
@shp.reload!
|
414
|
+
end
|
415
|
+
|
416
|
+
#prevents the udpate from taking place
|
417
|
+
def rollback
|
418
|
+
@deleted = Hash.new
|
419
|
+
@added = Array.new
|
420
|
+
@rollbacked = true
|
421
|
+
end
|
422
|
+
|
423
|
+
private
|
424
|
+
|
425
|
+
def to_shp_type(geom)
|
426
|
+
root = if geom.is_a? GeoRuby::SimpleFeatures::Point
|
427
|
+
"POINT"
|
428
|
+
elsif geom.is_a? GeoRuby::SimpleFeatures::LineString
|
429
|
+
"POLYLINE"
|
430
|
+
elsif geom.is_a? GeoRuby::SimpleFeatures::Polygon
|
431
|
+
"POLYGON"
|
432
|
+
elsif geom.is_a? GeoRuby::SimpleFeatures::MultiPoint
|
433
|
+
"MULTIPOINT"
|
434
|
+
elsif geom.is_a? GeoRuby::SimpleFeatures::MultiLineString
|
435
|
+
"POLYLINE"
|
436
|
+
elsif geom.is_a? GeoRuby::SimpleFeatures::MultiPolygon
|
437
|
+
"POLYGON"
|
438
|
+
else
|
439
|
+
false
|
440
|
+
end
|
441
|
+
return false if !root
|
442
|
+
|
443
|
+
if geom.with_z
|
444
|
+
root = root + "Z"
|
445
|
+
elsif geom.with_m
|
446
|
+
root = root + "M"
|
447
|
+
end
|
448
|
+
eval "ShpType::" + root
|
449
|
+
end
|
450
|
+
|
451
|
+
def commit_add(index)
|
452
|
+
max_x, min_x, max_y, min_y,max_z,min_z,max_m,min_m = @shp.xmax,@shp.xmin,@shp.ymax,@shp.ymin,@shp.zmax,@shp.zmin,@shp.mmax,@shp.mmin
|
453
|
+
@added.each do |record|
|
454
|
+
@dbf_io << ['20'].pack('H2')
|
455
|
+
@dbf.fields.each do |field|
|
456
|
+
data = record.data[field.name]
|
457
|
+
str = if field.type == 'D'
|
458
|
+
sprintf("%04i%02i%02i",data.year,data.month,data.mday)
|
459
|
+
elsif field.type == 'L'
|
460
|
+
data ? "T" : "F"
|
461
|
+
else
|
462
|
+
data.to_s
|
463
|
+
end
|
464
|
+
@dbf_io << [str].pack("A#{field.length}")
|
465
|
+
end
|
466
|
+
|
467
|
+
shp_str,min_xp,max_xp,min_yp,max_yp,min_zp,max_zp,min_mp,max_mp = build_shp_geometry(record.geometry)
|
468
|
+
max_x = max_xp if max_xp > max_x
|
469
|
+
min_x = min_xp if min_xp < min_x
|
470
|
+
max_y = max_yp if max_yp > max_y
|
471
|
+
min_y = min_yp if min_yp < min_y
|
472
|
+
max_z = max_zp if max_zp > max_z
|
473
|
+
min_z = min_zp if min_zp < min_z
|
474
|
+
max_m = max_mp if max_mp > max_m
|
475
|
+
min_m = min_mp if min_mp < min_m
|
476
|
+
length = (shp_str.length/2 + 2).to_i #num of 16-bit words; geom type is included (+2)
|
477
|
+
@shx_io << [(@shp_io.pos/2).to_i,length].pack("N2")
|
478
|
+
@shp_io << [index,length,@shp.shp_type].pack("N2V")
|
479
|
+
@shp_io << shp_str
|
480
|
+
index += 1
|
481
|
+
end
|
482
|
+
@shp_io.flush
|
483
|
+
@shx_io.flush
|
484
|
+
@dbf_io.flush
|
485
|
+
[min_x,max_x,min_y,max_y,min_z,max_z,min_m,max_m]
|
486
|
+
end
|
487
|
+
|
488
|
+
def commit_delete
|
489
|
+
@shp_r.rewind
|
490
|
+
header = @shp_r.read(100)
|
491
|
+
@shp_io << header
|
492
|
+
@shx_io << header
|
493
|
+
index = 1
|
494
|
+
while(!@shp_r.eof?)
|
495
|
+
icur,length = @shp_r.read(8).unpack("N2")
|
496
|
+
unless(@deleted[icur-1])
|
497
|
+
@shx_io << [(@shp_io.pos/2).to_i,length].pack("N2")
|
498
|
+
@shp_io << [index,length].pack("N2")
|
499
|
+
@shp_io << @shp_r.read(length * 2)
|
500
|
+
index += 1
|
501
|
+
else
|
502
|
+
@shp_r.seek(length * 2,IO::SEEK_CUR)
|
503
|
+
end
|
504
|
+
end
|
505
|
+
@shp_io.flush
|
506
|
+
@shx_io.flush
|
507
|
+
|
508
|
+
@dbf_r.rewind
|
509
|
+
@dbf_io << @dbf_r.read(@dbf.header_length)
|
510
|
+
icur = 0
|
511
|
+
while(!@dbf_r.eof?)
|
512
|
+
unless(@deleted[icur])
|
513
|
+
@dbf_io << @dbf_r.read(@dbf.record_length)
|
514
|
+
else
|
515
|
+
@dbf_r.seek(@dbf.record_length,IO::SEEK_CUR)
|
516
|
+
end
|
517
|
+
icur += 1
|
518
|
+
end
|
519
|
+
@dbf_io.flush
|
520
|
+
index
|
521
|
+
end
|
522
|
+
|
523
|
+
def commit_finalize(min_x,max_x,min_y,max_y,min_z,max_z,min_m,max_m)
|
524
|
+
#update size in shp and dbf + extent and num records in dbf
|
525
|
+
@shp_io.seek(0,IO::SEEK_END)
|
526
|
+
shp_size = @shp_io.pos / 2
|
527
|
+
@shx_io.seek(0,IO::SEEK_END)
|
528
|
+
shx_size= @shx_io.pos / 2
|
529
|
+
@shp_io.seek(24)
|
530
|
+
@shp_io.write([shp_size].pack("N"))
|
531
|
+
@shx_io.seek(24)
|
532
|
+
@shx_io.write([shx_size].pack("N"))
|
533
|
+
@shp_io.seek(36)
|
534
|
+
@shx_io.seek(36)
|
535
|
+
str = [min_x,min_y,max_x,max_y,min_z,max_z,min_m,max_m].pack("E8")
|
536
|
+
@shp_io.write(str)
|
537
|
+
@shx_io.write(str)
|
538
|
+
|
539
|
+
@dbf_io.seek(4)
|
540
|
+
@dbf_io.write([@dbf.record_count + @added.length - @deleted.length].pack("V"))
|
541
|
+
end
|
542
|
+
|
543
|
+
def build_shp_geometry(geometry)
|
544
|
+
m_range = nil
|
545
|
+
answer =
|
546
|
+
case @shp.shp_type
|
547
|
+
when ShpType::POINT
|
548
|
+
bbox = geometry.bounding_box
|
549
|
+
[geometry.x,geometry.y].pack("E2")
|
550
|
+
when ShpType::POLYLINE
|
551
|
+
str,bbox = create_bbox(geometry)
|
552
|
+
build_polyline(geometry,str)
|
553
|
+
when ShpType::POLYGON
|
554
|
+
str,bbox = create_bbox(geometry)
|
555
|
+
build_polygon(geometry,str)
|
556
|
+
when ShpType::MULTIPOINT
|
557
|
+
str,bbox = create_bbox(geometry)
|
558
|
+
build_multi_point(geometry,str)
|
559
|
+
when ShpType::POINTZ
|
560
|
+
bbox = geometry.bounding_box
|
561
|
+
[geometry.x,geometry.y,geometry.z,geometry.m].pack("E4")
|
562
|
+
when ShpType::POLYLINEZ
|
563
|
+
str,bbox = create_bbox(geometry)
|
564
|
+
m_range = geometry.m_range
|
565
|
+
build_polyline(geometry,str)
|
566
|
+
build_polyline_zm(geometry,:@z,[bbox[0].z,bbox[1].z],str)
|
567
|
+
build_polyline_zm(geometry,:@m,m_range,str)
|
568
|
+
when ShpType::POLYGONZ
|
569
|
+
str,bbox = create_bbox(geometry)
|
570
|
+
m_range = geometry.m_range
|
571
|
+
build_polygon(geometry,str)
|
572
|
+
build_polygon_zm(geometry,:@z,[bbox[0].z,bbox[1].z],str)
|
573
|
+
build_polygon_zm(geometry,:@m,m_range,str)
|
574
|
+
when ShpType::MULTIPOINTZ
|
575
|
+
str,bbox = create_bbox(geometry)
|
576
|
+
m_range = geometry.m_range
|
577
|
+
build_multi_point(geometry,str)
|
578
|
+
build_multi_point_zm(geometry,:@z,[bbox[0].z,bbox[1].z],str)
|
579
|
+
build_multi_point_zm(geometry,:@m,m_range,str)
|
580
|
+
when ShpType::POINTM
|
581
|
+
bbox = geometry.bounding_box
|
582
|
+
[geometry.x,geometry.y,geometry.m].pack("E3")
|
583
|
+
when ShpType::POLYLINEM
|
584
|
+
str,bbox = create_bbox(geometry)
|
585
|
+
m_range = geometry.m_range
|
586
|
+
build_polyline(geometry,str)
|
587
|
+
build_polyline_zm(geometry,:@m,m_range,str)
|
588
|
+
when ShpType::POLYGONM
|
589
|
+
str,bbox = create_bbox(geometry)
|
590
|
+
m_range = geometry.m_range
|
591
|
+
build_polygon(geometry,str)
|
592
|
+
build_polygon_zm(geometry,:@m,m_range,str)
|
593
|
+
when ShpType::MULTIPOINTM
|
594
|
+
str,bbox = create_bbox(geometry)
|
595
|
+
m_range = geometry.m_range
|
596
|
+
build_multi_point(geometry,str)
|
597
|
+
build_multi_point_zm(geometry,:@m,m_range,str)
|
598
|
+
end
|
599
|
+
m_range ||= [0,0]
|
600
|
+
[answer,bbox[0].x,bbox[1].x,bbox[0].y,bbox[1].y,bbox[0].z || 0, bbox[1].z || 0, m_range[0], m_range[1]]
|
601
|
+
end
|
602
|
+
|
603
|
+
def create_bbox(geometry)
|
604
|
+
bbox = geometry.bounding_box
|
605
|
+
[[bbox[0].x,bbox[0].y,bbox[1].x,bbox[1].y].pack("E4"),bbox]
|
606
|
+
end
|
607
|
+
|
608
|
+
def build_polyline(geometry,str)
|
609
|
+
if geometry.is_a? GeoRuby::SimpleFeatures::LineString
|
610
|
+
str << [1,geometry.length,0].pack("V3")
|
611
|
+
geometry.each do |point|
|
612
|
+
str << [point.x,point.y].pack("E2")
|
613
|
+
end
|
614
|
+
else
|
615
|
+
#multilinestring
|
616
|
+
str << [geometry.length,geometry.inject(0) {|l, ls| l + ls.length}].pack("V2")
|
617
|
+
str << geometry.inject([0]) {|a,ls| a << (a.last + ls.length)}.pack("V#{geometry.length}") #last element of the previous array is dropped
|
618
|
+
geometry.each do |ls|
|
619
|
+
ls.each do |point|
|
620
|
+
str << [point.x,point.y].pack("E2")
|
621
|
+
end
|
622
|
+
end
|
623
|
+
end
|
624
|
+
str
|
625
|
+
end
|
626
|
+
|
627
|
+
def build_polyline_zm(geometry,zm,range,str)
|
628
|
+
str << range.pack("E2")
|
629
|
+
if geometry.is_a? GeoRuby::SimpleFeatures::LineString
|
630
|
+
geometry.each do |point|
|
631
|
+
str << [point.instance_variable_get(zm)].pack("E")
|
632
|
+
end
|
633
|
+
else
|
634
|
+
#multilinestring
|
635
|
+
geometry.each do |ls|
|
636
|
+
ls.each do |point|
|
637
|
+
str << [point.instance_variable_get(zm)].pack("E")
|
638
|
+
end
|
639
|
+
end
|
640
|
+
end
|
641
|
+
str
|
642
|
+
end
|
643
|
+
|
644
|
+
def build_polygon(geometry,str)
|
645
|
+
if geometry.is_a? GeoRuby::SimpleFeatures::Polygon
|
646
|
+
str << [geometry.length,geometry.inject(0) {|l, lr| l + lr.length}].pack("V2")
|
647
|
+
str << geometry.inject([0]) {|a,lr| a << (a.last + lr.length)}.pack("V#{geometry.length}") #last element of the previous array is dropped
|
648
|
+
geometry.each do |lr|
|
649
|
+
lr.each do |point|
|
650
|
+
str << [point.x,point.y].pack("E2")
|
651
|
+
end
|
652
|
+
end
|
653
|
+
else
|
654
|
+
#multipolygon
|
655
|
+
num_rings = geometry.inject(0) {|l, poly| l + poly.length}
|
656
|
+
str << [num_rings, geometry.inject(0) {|l, poly| l + poly.inject(0) {|l2,lr| l2 + lr.length} }].pack("V2")
|
657
|
+
str << geometry.inject([0]) {|a,poly| poly.inject(a) {|a2, lr| a2 << (a2.last + lr.length)}}.pack("V#{num_rings}") #last element of the previous array is dropped
|
658
|
+
geometry.each do |poly|
|
659
|
+
poly.each do |lr|
|
660
|
+
lr.each do |point|
|
661
|
+
str << [point.x,point.y].pack("E2")
|
662
|
+
end
|
663
|
+
end
|
664
|
+
end
|
665
|
+
end
|
666
|
+
str
|
667
|
+
end
|
668
|
+
|
669
|
+
def build_polygon_zm(geometry,zm,range,str)
|
670
|
+
str << range.pack("E2")
|
671
|
+
if geometry.is_a? GeoRuby::SimpleFeatures::Polygon
|
672
|
+
geometry.each do |lr|
|
673
|
+
lr.each do |point|
|
674
|
+
str << [point.instance_variable_get(zm)].pack("E")
|
675
|
+
end
|
676
|
+
end
|
677
|
+
else
|
678
|
+
geometry.each do |poly|
|
679
|
+
poly.each do |lr|
|
680
|
+
lr.each do |point|
|
681
|
+
str << [point.instance_variable_get(zm)].pack("E")
|
682
|
+
end
|
683
|
+
end
|
684
|
+
end
|
685
|
+
end
|
686
|
+
str
|
687
|
+
end
|
688
|
+
|
689
|
+
def build_multi_point(geometry,str)
|
690
|
+
str << [geometry.length].pack("V")
|
691
|
+
geometry.each do |point|
|
692
|
+
str << [point.x,point.y].pack("E2")
|
693
|
+
end
|
694
|
+
str
|
695
|
+
end
|
696
|
+
|
697
|
+
def build_multi_point_zm(geometry,zm,range,str)
|
698
|
+
str << range.pack("E2")
|
699
|
+
geometry.each do |point|
|
700
|
+
str << [point.instance_variable_get(zm)].pack("E")
|
701
|
+
end
|
702
|
+
str
|
703
|
+
end
|
704
|
+
end
|
705
|
+
|
706
|
+
class MalformedShpException < StandardError
|
707
|
+
end
|
708
|
+
|
709
|
+
class UnexistantRecordException < StandardError
|
710
|
+
end
|
711
|
+
|
712
|
+
class IncompatibleGeometryException < StandardError
|
713
|
+
end
|
714
|
+
|
715
|
+
class IncompatibleDataException < StandardError
|
716
|
+
end
|
717
|
+
end
|
718
|
+
end
|