nswtopo 2.0.0.pre.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +7 -0
  2. data/COPYING +674 -0
  3. data/bin/nswtopo +430 -0
  4. data/docs/README.md +78 -0
  5. data/docs/add.md +49 -0
  6. data/docs/config.md +24 -0
  7. data/docs/contours.md +37 -0
  8. data/docs/controls.md +9 -0
  9. data/docs/declination.md +15 -0
  10. data/docs/delete.md +15 -0
  11. data/docs/grid.md +5 -0
  12. data/docs/info.md +5 -0
  13. data/docs/init.md +38 -0
  14. data/docs/layers.md +11 -0
  15. data/docs/overlay.md +37 -0
  16. data/docs/relief.md +22 -0
  17. data/docs/render.md +43 -0
  18. data/docs/spot-heights.md +23 -0
  19. data/lib/nswtopo/archive.rb +93 -0
  20. data/lib/nswtopo/avl_tree.rb +128 -0
  21. data/lib/nswtopo/config.rb +73 -0
  22. data/lib/nswtopo/dither.rb +31 -0
  23. data/lib/nswtopo/font/chrome.rb +59 -0
  24. data/lib/nswtopo/font/generic.rb +25 -0
  25. data/lib/nswtopo/font.rb +43 -0
  26. data/lib/nswtopo/formats/kmz.rb +149 -0
  27. data/lib/nswtopo/formats/mbtiles.rb +64 -0
  28. data/lib/nswtopo/formats/pdf.rb +31 -0
  29. data/lib/nswtopo/formats/svg.rb +69 -0
  30. data/lib/nswtopo/formats/svgz.rb +13 -0
  31. data/lib/nswtopo/formats/zip.rb +40 -0
  32. data/lib/nswtopo/formats.rb +76 -0
  33. data/lib/nswtopo/geometry/overlap.rb +78 -0
  34. data/lib/nswtopo/geometry/r_tree.rb +47 -0
  35. data/lib/nswtopo/geometry/segment.rb +27 -0
  36. data/lib/nswtopo/geometry/straight_skeleton/collapse.rb +21 -0
  37. data/lib/nswtopo/geometry/straight_skeleton/interior_node.rb +17 -0
  38. data/lib/nswtopo/geometry/straight_skeleton/node.rb +50 -0
  39. data/lib/nswtopo/geometry/straight_skeleton/nodes.rb +295 -0
  40. data/lib/nswtopo/geometry/straight_skeleton/split.rb +33 -0
  41. data/lib/nswtopo/geometry/straight_skeleton/vertex.rb +9 -0
  42. data/lib/nswtopo/geometry/straight_skeleton.rb +6 -0
  43. data/lib/nswtopo/geometry/vector.rb +91 -0
  44. data/lib/nswtopo/geometry/vector_sequence.rb +179 -0
  45. data/lib/nswtopo/geometry.rb +8 -0
  46. data/lib/nswtopo/gis/arcgis_server/connection.rb +52 -0
  47. data/lib/nswtopo/gis/arcgis_server.rb +155 -0
  48. data/lib/nswtopo/gis/dem.rb +70 -0
  49. data/lib/nswtopo/gis/esri_hdr.rb +77 -0
  50. data/lib/nswtopo/gis/gdal_glob.rb +41 -0
  51. data/lib/nswtopo/gis/geojson/collection.rb +94 -0
  52. data/lib/nswtopo/gis/geojson/line_string.rb +11 -0
  53. data/lib/nswtopo/gis/geojson/multi_line_string.rb +63 -0
  54. data/lib/nswtopo/gis/geojson/multi_point.rb +12 -0
  55. data/lib/nswtopo/gis/geojson/multi_polygon.rb +167 -0
  56. data/lib/nswtopo/gis/geojson/point.rb +9 -0
  57. data/lib/nswtopo/gis/geojson/polygon.rb +11 -0
  58. data/lib/nswtopo/gis/geojson.rb +89 -0
  59. data/lib/nswtopo/gis/gps/gpx.rb +22 -0
  60. data/lib/nswtopo/gis/gps/kml.rb +66 -0
  61. data/lib/nswtopo/gis/gps.rb +20 -0
  62. data/lib/nswtopo/gis/projection.rb +56 -0
  63. data/lib/nswtopo/gis/shapefile.rb +24 -0
  64. data/lib/nswtopo/gis/world_file.rb +19 -0
  65. data/lib/nswtopo/gis.rb +9 -0
  66. data/lib/nswtopo/help_formatter.rb +59 -0
  67. data/lib/nswtopo/helpers/array.rb +30 -0
  68. data/lib/nswtopo/helpers/colour.rb +176 -0
  69. data/lib/nswtopo/helpers/concurrently.rb +27 -0
  70. data/lib/nswtopo/helpers/dir.rb +7 -0
  71. data/lib/nswtopo/helpers/hash.rb +15 -0
  72. data/lib/nswtopo/helpers/tar_writer.rb +11 -0
  73. data/lib/nswtopo/helpers.rb +6 -0
  74. data/lib/nswtopo/layer/arcgis_raster.rb +73 -0
  75. data/lib/nswtopo/layer/contour.rb +233 -0
  76. data/lib/nswtopo/layer/control.rb +94 -0
  77. data/lib/nswtopo/layer/declination.rb +53 -0
  78. data/lib/nswtopo/layer/feature.rb +87 -0
  79. data/lib/nswtopo/layer/grid.rb +120 -0
  80. data/lib/nswtopo/layer/import.rb +25 -0
  81. data/lib/nswtopo/layer/labels/fence.rb +20 -0
  82. data/lib/nswtopo/layer/labels.rb +630 -0
  83. data/lib/nswtopo/layer/overlay.rb +53 -0
  84. data/lib/nswtopo/layer/raster.rb +63 -0
  85. data/lib/nswtopo/layer/relief.rb +143 -0
  86. data/lib/nswtopo/layer/spot.rb +171 -0
  87. data/lib/nswtopo/layer/vector.rb +263 -0
  88. data/lib/nswtopo/layer/vegetation.rb +73 -0
  89. data/lib/nswtopo/layer.rb +78 -0
  90. data/lib/nswtopo/log.rb +28 -0
  91. data/lib/nswtopo/map.rb +296 -0
  92. data/lib/nswtopo/os.rb +75 -0
  93. data/lib/nswtopo/safely.rb +13 -0
  94. data/lib/nswtopo/version.rb +4 -0
  95. data/lib/nswtopo/zip.rb +15 -0
  96. data/lib/nswtopo.rb +249 -0
  97. metadata +142 -0
@@ -0,0 +1,167 @@
1
+ module NSWTopo
2
+ module GeoJSON
3
+ class MultiPolygon
4
+ include StraightSkeleton
5
+
6
+ def clip(hull)
7
+ polys = @coordinates.inject([]) do |result, rings|
8
+ lefthanded = rings.first.clockwise?
9
+ interior, exterior = hull.zip(hull.perps).inject(rings) do |rings, (vertex, perp)|
10
+ insides, neighbours, clipped = Hash[].compare_by_identity, Hash[].compare_by_identity, []
11
+ rings.each do |points|
12
+ points.map do |point|
13
+ point.minus(vertex).dot(perp) >= 0
14
+ end.segments.zip(points.segments).each do |inside, segment|
15
+ insides[segment] = inside
16
+ neighbours[segment] = [nil, nil]
17
+ end.map(&:last).ring.each do |segment0, segment1|
18
+ neighbours[segment1][0], neighbours[segment0][1] = segment0, segment1
19
+ end
20
+ end
21
+ neighbours.select! do |segment, _|
22
+ insides[segment].any?
23
+ end
24
+ insides.select do |segment, inside|
25
+ inside.inject(&:^)
26
+ end.each do |segment, inside|
27
+ segment[inside[0] ? 1 : 0] = segment.along(vertex.minus(segment[0]).dot(perp) / segment.difference.dot(perp))
28
+ end.sort_by do |segment, inside|
29
+ segment[inside[0] ? 1 : 0].minus(vertex).cross(perp) * (lefthanded ? -1 : 1)
30
+ end.map(&:first).each_slice(2) do |segment0, segment1|
31
+ segment = [segment0[1], segment1[0]]
32
+ neighbours[segment0][1] = neighbours[segment1][0] = segment
33
+ neighbours[segment] = [segment0, segment1]
34
+ end
35
+ while neighbours.any?
36
+ segment, * = neighbours.first
37
+ clipped << []
38
+ while neighbours.include? segment
39
+ clipped.last << segment[0]
40
+ *, segment = neighbours.delete(segment)
41
+ end
42
+ clipped.last << clipped.last.first
43
+ end
44
+ clipped
45
+ end.partition(&:clockwise?).rotate(lefthanded ? 1 : 0)
46
+ next result << exterior + interior if exterior.one?
47
+ exterior.inject(result) do |result, exterior_ring|
48
+ within, interior = interior.partition do |interior_ring|
49
+ interior_ring.first.within? exterior_ring
50
+ end
51
+ result << [exterior_ring, *within]
52
+ end
53
+ end
54
+ polys.none? ? nil : polys.one? ? Polygon.new(*polys, @properties) : MultiPolygon.new(polys, @properties)
55
+ end
56
+
57
+ def area
58
+ @coordinates.flatten(1).sum(&:signed_area)
59
+ end
60
+
61
+ def skeleton
62
+ segments = []
63
+ Nodes.new(@coordinates.flatten(1)).progress do |event, node0, node1|
64
+ segments << [node0.point, node1.point].to_f
65
+ end
66
+ MultiLineString.new segments, @properties
67
+ end
68
+
69
+ def centres(fraction: 0.5, min_width: nil, interval:, lines: true)
70
+ neighbours = Hash.new { |neighbours, node| neighbours[node] = [] }
71
+ samples, tails, node1 = {}, {}, nil
72
+
73
+ Nodes.new(@coordinates.flatten(1)).progress(interval: interval) do |event, *args|
74
+ case event
75
+ when :nodes
76
+ node0, node1 = *args
77
+ neighbours[node0] << node1
78
+ neighbours[node1] << node0
79
+ when :interval
80
+ travel, rings = *args
81
+ samples[travel] = rings.flat_map do |ring|
82
+ ring.sample_at interval
83
+ end
84
+ end
85
+ end
86
+
87
+ samples[node1.travel] = [node1.point.to_f]
88
+ max_travel = neighbours.keys.map(&:travel).max
89
+ min_travel = [fraction * max_travel, min_width && 0.5 * min_width].compact.max
90
+
91
+ features = samples.select do |travel, points|
92
+ travel > min_travel
93
+ end.map do |travel, points|
94
+ MultiPoint.new points, @properties
95
+ end.reverse
96
+ return features unless lines
97
+
98
+ loop do
99
+ break unless neighbours.reject do |node, (neighbour, *others)|
100
+ others.any? || neighbours[neighbour].one?
101
+ end.each do |node, (neighbour, *)|
102
+ next if neighbours[neighbour].one?
103
+ neighbours.delete node
104
+ neighbours[neighbour].delete node
105
+ nodes, length = tails.delete(node) || [[node], 0]
106
+ candidate = [nodes << neighbour, length + [node.point, neighbour.point].distance]
107
+ tails[neighbour] = [tails[neighbour], candidate].compact.max_by(&:last)
108
+ end.any?
109
+ end
110
+
111
+ lengths, lines, candidates = Hash.new(0), Hash.new, tails.values
112
+ while candidates.any?
113
+ (*nodes, node), length = candidates.pop
114
+ next if (neighbours[node] - nodes).each do |neighbour|
115
+ candidates << [[*nodes, node, neighbour], length + [node.point, neighbour.point].distance]
116
+ end.any?
117
+ index = nodes.find(&:index).index
118
+ tail_nodes, tail_length = tails[node] || [[node], 0]
119
+ lengths[index], lines[index] = length + tail_length, nodes + tail_nodes.reverse if length + tail_length > lengths[index]
120
+ end
121
+
122
+ linestrings = lines.values.map do |nodes|
123
+ nodes.chunk do |node|
124
+ node.travel >= min_travel
125
+ end.select(&:first).map(&:last).reject(&:one?).map do |nodes|
126
+ nodes.map(&:point).to_f
127
+ end
128
+ end.flatten(1)
129
+ features.prepend MultiLineString.new(linestrings, @properties)
130
+ end
131
+
132
+ def centrepoints(interval:, **options)
133
+ centres(**options, interval: interval, lines: false)
134
+ end
135
+
136
+ def centrelines(**options)
137
+ centres(**options, interval: nil, lines: true)
138
+ end
139
+
140
+ def buffer(*margins, **options)
141
+ nodes = Nodes.new @coordinates.flatten(1)
142
+ margins.each do |margin|
143
+ nodes.progress limit: -margin, **options.slice(:rounding_angle, :cutoff_angle)
144
+ end
145
+ interior_rings, exterior_rings = nodes.readout.partition(&:hole?)
146
+ polygons, foo = exterior_rings.sort_by(&:signed_area).inject [[], interior_rings] do |(polygons, interior_rings), exterior_ring|
147
+ claimed, unclaimed = interior_rings.partition do |interior_ring|
148
+ interior_ring.first.within? exterior_ring
149
+ end
150
+ [polygons << [exterior_ring, *claimed], unclaimed]
151
+ end
152
+ MultiPolygon.new polygons.entries, @properties
153
+ end
154
+
155
+ def centroids
156
+ MultiPoint.new @coordinates.map(&:first).map(&:centroid), @properties
157
+ end
158
+
159
+ def samples(interval)
160
+ points = @coordinates.flatten(1).flat_map do |ring|
161
+ ring.sample_at interval
162
+ end
163
+ MultiPoint.new points, @properties
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,9 @@
1
+ module NSWTopo
2
+ module GeoJSON
3
+ class Point
4
+ def bounds
5
+ @coordinates.zip.map(&:minmax)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ module NSWTopo
2
+ module GeoJSON
3
+ class Polygon
4
+ delegate %i[area skeleton centres centrepoints centrelines buffer centroids samples] => :multi
5
+
6
+ def bounds
7
+ @coordinates.first.transpose.map(&:minmax)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,89 @@
1
+ require_relative 'geojson/collection'
2
+
3
+ module NSWTopo
4
+ module GeoJSON
5
+ Error = Class.new StandardError
6
+ TYPES = Set.new %W[Point MultiPoint LineString MultiLineString Polygon MultiPolygon]
7
+
8
+ CLASSES = TYPES.map do |type|
9
+ klass = Class.new do
10
+ def initialize(coordinates, properties = {})
11
+ properties ||= {}
12
+ raise Error, "invalid feature properties" unless Hash === properties
13
+ raise Error, "invalid feature geometry" unless Array === coordinates
14
+ @coordinates, @properties = coordinates, properties
15
+ end
16
+ attr_accessor :coordinates, :properties
17
+
18
+ define_method :to_h do
19
+ {
20
+ "type" => "Feature",
21
+ "geometry" => {
22
+ "type" => type,
23
+ "coordinates" => @coordinates
24
+ },
25
+ "properties" => @properties
26
+ }
27
+ end
28
+
29
+ extend Forwardable
30
+ delegate %i[[] []= fetch values_at key? store clear] => :@properties
31
+ end
32
+
33
+ const_set type, klass
34
+ end
35
+
36
+ CLASSES.zip(TYPES).each do |klass, type|
37
+ Collection.define_method "add_#{type}".downcase do |coordinates, properties = {}|
38
+ self << klass.new(coordinates, properties)
39
+ end
40
+
41
+ Collection.define_method "#{type}s".downcase do
42
+ grep klass
43
+ end
44
+
45
+ define_singleton_method type.downcase do |coordinates, projection: nil, properties: {}|
46
+ Collection.new(*projection) << klass.new(coordinates, properties)
47
+ end
48
+ end
49
+
50
+ [[Point, MultiPoint ],
51
+ [LineString, MultiLineString],
52
+ [Polygon, MultiPolygon ]].each do |single_class, multi_class|
53
+ single_class.class_eval do
54
+ def explode
55
+ [self]
56
+ end
57
+
58
+ define_method :multi do
59
+ multi_class.new [@coordinates], @properties
60
+ end
61
+
62
+ delegate :clip => :multi
63
+ end
64
+
65
+ multi_class.class_eval do
66
+ define_method :explode do
67
+ @coordinates.map do |coordinates|
68
+ single_class.new coordinates, @properties
69
+ end
70
+ end
71
+
72
+ def bounds
73
+ explode.map(&:bounds).transpose.map(&:flatten).map(&:minmax)
74
+ end
75
+
76
+ delegate :empty? => :@coordinates
77
+
78
+ alias multi dup
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ require_relative 'geojson/point'
85
+ require_relative 'geojson/line_string'
86
+ require_relative 'geojson/polygon'
87
+ require_relative 'geojson/multi_point'
88
+ require_relative 'geojson/multi_line_string'
89
+ require_relative 'geojson/multi_polygon'
@@ -0,0 +1,22 @@
1
+ module NSWTopo
2
+ class GPS
3
+ module GPX
4
+ def collection
5
+ GeoJSON::Collection.new.tap do |collection|
6
+ @xml.elements.each "/gpx/wpt" do |wpt|
7
+ coords = %w[lon lat].map { |name| wpt.attributes[name].to_f }
8
+ name = wpt.elements["name"]&.text
9
+ collection.add_point coords, "name" => name
10
+ end
11
+ @xml.elements.each "/gpx/trk" do |trk|
12
+ coords = trk.elements.collect("trkseg") do |trkseg|
13
+ trkseg.elements.collect("trkpt") { |trkpt| %w[lon lat].map { |name| trkpt.attributes[name].to_f } }
14
+ end
15
+ name = trk.elements["name"]&.text
16
+ collection.add_multilinestring coords, "name" => name
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,66 @@
1
+ module NSWTopo
2
+ class GPS
3
+ module KML
4
+ def styles(placemark)
5
+ style_url = placemark.elements["styleUrl"]&.text&.delete_prefix(?#)
6
+ style_element = @xml.elements["/kml/Document/[@id='%s']" % style_url]
7
+ result = {}
8
+
9
+ case style_element&.name
10
+ when "StyleMap"
11
+ style_element = style_element.elements["Pair[key[text()='normal']]"]
12
+ result = styles(style_element) if style_element
13
+
14
+ when "Style"
15
+ [%w[LineStyle stroke], %w[PolyStyle fill]].each do |element, attribute|
16
+ /^[0-9a-fA-F]{8}$/.match(style_element.elements["#{element}/color"]&.text) do |match|
17
+ a, b, g, r = match[0].each_char.each_slice(2).map(&:join)
18
+ result["#{attribute}-opacity"] = (Float("0x%s" % a) / 255).round(5)
19
+ result[attribute] = Colour.new("#%s%s%s" % [r, g, b]).to_s
20
+ end
21
+ end
22
+ result["stroke"] ||= "#FFFFFF"
23
+ result["stroke-width"] = ((style_element.elements["LineStyle/width"]&.text || 1).to_f * 25.4 / 96.0).round(5)
24
+
25
+ [%w[fill fill], %w[outline stroke]].each do |element, attribute|
26
+ if style_element.elements["PolyStyle/#{element}"]&.text == ?0
27
+ result[attribute] = "none"
28
+ result.delete "#{attribute}-opacity"
29
+ result.delete "#{attribute}-width"
30
+ end
31
+ end
32
+ end
33
+ result
34
+ end
35
+
36
+ def properties(placemark)
37
+ { "name" => placemark.elements["name"]&.text, "folder" => placemark.elements["ancestor::Folder/name"]&.text }
38
+ end
39
+
40
+ def collection
41
+ GeoJSON::Collection.new.tap do |collection|
42
+ @xml.elements.each "/kml//Placemark[Point/coordinates]" do |placemark|
43
+ coords = placemark.elements["Point/coordinates"].text.split(',').take(2).map(&:to_f)
44
+ collection.add_point coords, properties(placemark).merge("styles" => {})
45
+ end
46
+ @xml.elements.each "/kml//Placemark[LineString/coordinates]" do |placemark|
47
+ coords = placemark.elements["LineString/coordinates"].text.split(' ').map { |triplet| triplet.split(',')[0..1].map(&:to_f) }
48
+ collection.add_linestring coords, properties(placemark).merge("styles" => styles(placemark))
49
+ end
50
+ @xml.elements.each "/kml//Placemark[gx:Track]" do |placemark|
51
+ coords = placemark.elements.collect("gx:Track/gx:coord") { |coord| coord.text.split(?\s).take(2).map(&:to_f) }
52
+ collection.add_linestring coords, properties(placemark).merge("styles" => styles(placemark))
53
+ end
54
+ @xml.elements.each "/kml//Placemark[Polygon/outerBoundaryIs/LinearRing/coordinates]" do |placemark|
55
+ coords = [placemark.elements["Polygon/outerBoundaryIs/LinearRing/coordinates"].text]
56
+ coords += placemark.elements.collect("Polygon/innerBoundaryIs/LinearRing/coordinates", &:text)
57
+ coords.map! do |text|
58
+ text.split(' ').map { |triplet| triplet.split(?,).take(2).map(&:to_f) }
59
+ end
60
+ collection.add_polygon coords, properties(placemark).merge("styles" => styles(placemark))
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,20 @@
1
+ require_relative 'gps/gpx'
2
+ require_relative 'gps/kml'
3
+
4
+ module NSWTopo
5
+ class GPS
6
+ def initialize(path)
7
+ @xml = REXML::Document.new(path.read)
8
+ case
9
+ when @xml.elements["/gpx"] then extend GPX
10
+ when @xml.elements["/kml"] then extend KML
11
+ else
12
+ raise "invalid GPX or KML file: #{path}"
13
+ end
14
+ end
15
+
16
+ def self.load(path)
17
+ new(path).collection
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,56 @@
1
+ module NSWTopo
2
+ class Projection
3
+ def initialize(string_or_path)
4
+ @proj4 = OS.gdalsrsinfo("-o", "proj4", string_or_path).chomp.strip
5
+ raise "no georeferencing found: %s" % string_or_path if @proj4.empty?
6
+ end
7
+
8
+ %w[wkt wkt_simple wkt_noct wkt_esri mapinfo xml].each do |format|
9
+ define_method format do
10
+ OS.gdalsrsinfo("-o", format, @proj4).split(/['\r\n]+/).map(&:strip).join("")
11
+ end
12
+ end
13
+
14
+ attr_reader :proj4
15
+ alias to_s proj4
16
+ alias to_str proj4
17
+
18
+ def ==(other)
19
+ proj4 == other.proj4
20
+ end
21
+
22
+ extend Forwardable
23
+ delegate :hash => :@proj4
24
+ alias eql? ==
25
+
26
+ def self.utm(zone, south = true)
27
+ new("+proj=utm +zone=#{zone}#{' +south' if south} +ellps=WGS84 +datum=WGS84 +units=m +no_defs")
28
+ end
29
+
30
+ def self.wgs84
31
+ new("+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs")
32
+ end
33
+
34
+ def self.transverse_mercator(lon, lat)
35
+ new("+proj=tmerc +lon_0=#{lon} +lat_0=#{lat} +k=1 +x_0=0 +y_0=0 +ellps=WGS84 +datum=WGS84 +units=m +no_defs")
36
+ end
37
+
38
+ def self.azimuthal_equidistant(lon, lat)
39
+ new("+proj=aeqd +lon_0=#{lon} +lat_0=#{lat} +k_0=1 +x_0=0 +y_0=0 +ellps=WGS84 +datum=WGS84 +units=m +no_defs")
40
+ end
41
+
42
+ def self.utm_zones(collection)
43
+ collection.reproject_to_wgs84.bounds.first.map do |longitude|
44
+ (longitude / 6).floor + 31
45
+ end.yield_self do |min, max|
46
+ min..max
47
+ end
48
+ end
49
+
50
+ def self.utm_hull(zone)
51
+ longitudes = [31, 30].map { |offset| (zone - offset) * 6.0 }
52
+ latitudes = [-80.0, 84.0]
53
+ longitudes.product(latitudes).values_at(0,2,3,1)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,24 @@
1
+ module NSWTopo
2
+ module Shapefile
3
+ Error = Class.new RuntimeError
4
+
5
+ def self.===(path)
6
+ OS.ogrinfo "-ro", "-so", path
7
+ true
8
+ rescue OS::Error
9
+ false
10
+ end
11
+
12
+ def shapefile_layer(shapefile_path, where: nil, sql: nil, layer: nil, margin: {})
13
+ raise "#{@source}: can't specify both SQL and where clause" if sql && where
14
+ raise "#{@source}: can't specify both SQL and layer name" if sql && layer
15
+ sql = ["-sql", sql] if sql
16
+ where = ["-where", "(" << Array(where).join(") AND (") << ")"] if where
17
+ srs = ["-t_srs", @map.projection]
18
+ spat = ["-spat", *@map.bounds(margin: margin).transpose.flatten, "-spat_srs", @map.projection]
19
+ misc = %w[-mapFieldType Date=Integer,DateTime=Integer -dim XY]
20
+ json = OS.ogr2ogr *(sql || where), *srs, *spat, *misc, "-f", "GeoJSON", "-lco", "RFC7946=NO", "/vsistdout/", shapefile_path, *layer
21
+ GeoJSON::Collection.load json, @map.projection
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,19 @@
1
+ module NSWTopo
2
+ module WorldFile
3
+ extend self
4
+
5
+ def geotransform(top_left, resolution, angle)
6
+ sin, cos = Math::sin(angle * Math::PI / 180.0), Math::cos(angle * Math::PI / 180.0)
7
+ [[top_left[0], resolution * cos, resolution * sin],
8
+ [top_left[1], resolution * sin, -resolution * cos]]
9
+ end
10
+
11
+ def write(top_left, resolution, angle, path)
12
+ (x, r00, r01), (y, r10, r11) = geotransform(top_left, resolution, angle)
13
+ path.open("w") do |file|
14
+ file.puts r00, r01, r10, r11
15
+ file.puts x + 0.5 * resolution, y - 0.5 * resolution
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,9 @@
1
+ require_relative 'gis/projection'
2
+ require_relative 'gis/geojson'
3
+ require_relative 'gis/gps'
4
+ require_relative 'gis/world_file'
5
+ require_relative 'gis/esri_hdr'
6
+ require_relative 'gis/arcgis_server'
7
+ require_relative 'gis/shapefile'
8
+ require_relative 'gis/gdal_glob'
9
+ require_relative 'gis/dem'
@@ -0,0 +1,59 @@
1
+ class HelpFormatter < RDoc::Markup::ToAnsi
2
+ def initialize(ansi)
3
+ @ansi = ansi
4
+ super()
5
+ @headings.clear
6
+ return unless @ansi
7
+ @headings[1] = ["\e[1m", "\e[m"] # bold
8
+ @headings[2] = ["\e[4m", "\e[m"] # underline
9
+ @headings[3] = ["\e[3m", "\e[m"] # italic
10
+ end
11
+
12
+ def init_tags
13
+ return unless @ansi
14
+ add_tag :BOLD, "\e[1m", "\e[m" # bold
15
+ add_tag :EM, "\e[3m", "\e[m" # italic
16
+ add_tag :TT, "\e[3m", "\e[m" # italic
17
+ end
18
+
19
+ def accept_list_item_start(list_item)
20
+ @indent += 2
21
+ super
22
+ @res.pop if @res.last == ?\n
23
+ @indent -= 2
24
+ end
25
+
26
+ def accept_verbatim(verbatim)
27
+ indent = ?\s * (@indent + (@ansi ? 2 : 4))
28
+ verbatim.parts.map(&:each_line).flat_map(&:entries).each.with_index do |line, index|
29
+ case
30
+ when !@ansi
31
+ @res << indent << line
32
+ when line.start_with?("$ ")
33
+ @res << indent << "\e[90m$\e[;3m " << line[2..-1] << "\e[m"
34
+ else
35
+ @res << indent << "\e[90m> " << line << "\e[m"
36
+ end
37
+ end
38
+ @res << ?\n
39
+ end
40
+
41
+ def accept_heading(heading)
42
+ @indent += 2 unless heading.level == 1
43
+ super
44
+ @indent -= 2 unless heading.level == 1
45
+ end
46
+
47
+ def accept_paragraph(paragraph)
48
+ @indent += 2
49
+ text = paragraph.text.tr_s(?\r, ?\s).tr_s(?\n, ?\s)
50
+ wrap attributes text
51
+ @indent -= 2
52
+ @res << ?\n
53
+ end
54
+
55
+ def start_accepting
56
+ super
57
+ @res = @ansi ? ["\e[0m"] : []
58
+ end
59
+ end
@@ -0,0 +1,30 @@
1
+ module ArrayHelpers
2
+ def median
3
+ sort[length / 2]
4
+ end
5
+
6
+ def mean
7
+ empty? ? nil : inject(&:+) / length
8
+ end
9
+
10
+ def many?
11
+ length > 1
12
+ end
13
+
14
+ def in_two
15
+ each_slice(1 + [length - 1, 0].max / 2)
16
+ end
17
+
18
+ def nearby_pairs(closed = false, &block)
19
+ Enumerator.new do |yielder|
20
+ each.with_index do |element1, index|
21
+ (closed ? rotate(index) : drop(index)).drop(1).each do |element2|
22
+ break unless block.call [element1, element2]
23
+ yielder << [element1, element2]
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ Array.send :include, ArrayHelpers