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.
Files changed (75) hide show
  1. data/Gemfile +8 -0
  2. data/Gemfile.lock +29 -0
  3. data/History.txt +4 -0
  4. data/LICENSE +21 -0
  5. data/README.rdoc +184 -0
  6. data/Rakefile +48 -0
  7. data/VERSION +1 -0
  8. data/georuby.gemspec +128 -0
  9. data/lib/geo_ruby.rb +23 -0
  10. data/lib/geo_ruby/geojson.rb +129 -0
  11. data/lib/geo_ruby/georss.rb +133 -0
  12. data/lib/geo_ruby/gpx.rb +1 -0
  13. data/lib/geo_ruby/gpx4r/gpx.rb +118 -0
  14. data/lib/geo_ruby/shp.rb +1 -0
  15. data/lib/geo_ruby/shp4r/dbf.rb +42 -0
  16. data/lib/geo_ruby/shp4r/shp.rb +718 -0
  17. data/lib/geo_ruby/simple_features/envelope.rb +167 -0
  18. data/lib/geo_ruby/simple_features/ewkb_parser.rb +218 -0
  19. data/lib/geo_ruby/simple_features/ewkt_parser.rb +336 -0
  20. data/lib/geo_ruby/simple_features/geometry.rb +236 -0
  21. data/lib/geo_ruby/simple_features/geometry_collection.rb +144 -0
  22. data/lib/geo_ruby/simple_features/geometry_factory.rb +81 -0
  23. data/lib/geo_ruby/simple_features/helper.rb +18 -0
  24. data/lib/geo_ruby/simple_features/line_string.rb +228 -0
  25. data/lib/geo_ruby/simple_features/linear_ring.rb +34 -0
  26. data/lib/geo_ruby/simple_features/multi_line_string.rb +63 -0
  27. data/lib/geo_ruby/simple_features/multi_point.rb +58 -0
  28. data/lib/geo_ruby/simple_features/multi_polygon.rb +64 -0
  29. data/lib/geo_ruby/simple_features/point.rb +381 -0
  30. data/lib/geo_ruby/simple_features/polygon.rb +175 -0
  31. data/nofxx-georuby.gemspec +149 -0
  32. data/spec/data/geojson/feature_collection.json +34 -0
  33. data/spec/data/georss/atom.xml +21 -0
  34. data/spec/data/georss/gml.xml +40 -0
  35. data/spec/data/georss/w3c.xml +22 -0
  36. data/spec/data/gpx/fells_loop.gpx +1077 -0
  37. data/spec/data/gpx/long.gpx +1642 -0
  38. data/spec/data/gpx/long.kml +31590 -0
  39. data/spec/data/gpx/long.nmea +2220 -0
  40. data/spec/data/gpx/short.gpx +13634 -0
  41. data/spec/data/gpx/short.kml +130 -0
  42. data/spec/data/gpx/tracktreks.gpx +706 -0
  43. data/spec/data/multipoint.dbf +0 -0
  44. data/spec/data/multipoint.shp +0 -0
  45. data/spec/data/multipoint.shx +0 -0
  46. data/spec/data/point.dbf +0 -0
  47. data/spec/data/point.shp +0 -0
  48. data/spec/data/point.shx +0 -0
  49. data/spec/data/polygon.dbf +0 -0
  50. data/spec/data/polygon.shp +0 -0
  51. data/spec/data/polygon.shx +0 -0
  52. data/spec/data/polyline.dbf +0 -0
  53. data/spec/data/polyline.shp +0 -0
  54. data/spec/data/polyline.shx +0 -0
  55. data/spec/geo_ruby/geojson_spec.rb +147 -0
  56. data/spec/geo_ruby/georss.rb +218 -0
  57. data/spec/geo_ruby/georss_spec.rb +14 -0
  58. data/spec/geo_ruby/gpx4r/gpx_spec.rb +106 -0
  59. data/spec/geo_ruby/shp4r/shp_spec.rb +239 -0
  60. data/spec/geo_ruby/simple_features/envelope_spec.rb +47 -0
  61. data/spec/geo_ruby/simple_features/ewkb_parser_spec.rb +158 -0
  62. data/spec/geo_ruby/simple_features/ewkt_parser_spec.rb +179 -0
  63. data/spec/geo_ruby/simple_features/geometry_collection_spec.rb +55 -0
  64. data/spec/geo_ruby/simple_features/geometry_factory_spec.rb +11 -0
  65. data/spec/geo_ruby/simple_features/geometry_spec.rb +32 -0
  66. data/spec/geo_ruby/simple_features/line_string_spec.rb +259 -0
  67. data/spec/geo_ruby/simple_features/linear_ring_spec.rb +24 -0
  68. data/spec/geo_ruby/simple_features/multi_line_string_spec.rb +54 -0
  69. data/spec/geo_ruby/simple_features/multi_point_spec.rb +35 -0
  70. data/spec/geo_ruby/simple_features/multi_polygon_spec.rb +50 -0
  71. data/spec/geo_ruby/simple_features/point_spec.rb +356 -0
  72. data/spec/geo_ruby/simple_features/polygon_spec.rb +122 -0
  73. data/spec/geo_ruby_spec.rb +27 -0
  74. data/spec/spec_helper.rb +73 -0
  75. 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
@@ -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
@@ -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