nswtopo 2.0.0.pre.beta1 → 3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/COPYING +70 -83
  3. data/bin/nswtopo +227 -116
  4. data/docs/README.md +1 -12
  5. data/docs/add.md +1 -1
  6. data/docs/config.md +1 -1
  7. data/docs/contours.md +3 -1
  8. data/docs/init.md +8 -0
  9. data/docs/inspect.md +103 -0
  10. data/docs/move.md +9 -0
  11. data/docs/render.md +16 -7
  12. data/docs/scrape.md +67 -0
  13. data/docs/spot-heights.md +6 -2
  14. data/lib/nswtopo/archive.rb +50 -41
  15. data/lib/nswtopo/chrome.rb +227 -0
  16. data/lib/nswtopo/commands/add.rb +106 -0
  17. data/lib/nswtopo/commands/config.rb +38 -0
  18. data/lib/nswtopo/commands/inspect.rb +74 -0
  19. data/lib/nswtopo/commands/layers.rb +22 -0
  20. data/lib/nswtopo/commands/scrape.rb +79 -0
  21. data/lib/nswtopo/commands.rb +57 -0
  22. data/lib/nswtopo/dither.rb +5 -3
  23. data/lib/nswtopo/font.rb +46 -21
  24. data/lib/nswtopo/formats/gemf.rb +42 -0
  25. data/lib/nswtopo/formats/kmz.rb +26 -24
  26. data/lib/nswtopo/formats/mbtiles.rb +5 -41
  27. data/lib/nswtopo/formats/pdf.rb +82 -17
  28. data/lib/nswtopo/formats/svg.rb +114 -45
  29. data/lib/nswtopo/formats/svgz.rb +2 -2
  30. data/lib/nswtopo/formats/zip.rb +33 -23
  31. data/lib/nswtopo/formats.rb +77 -32
  32. data/lib/nswtopo/geometry/overlap.rb +1 -32
  33. data/lib/nswtopo/geometry/r_tree.rb +16 -10
  34. data/lib/nswtopo/geometry/segment.rb +3 -3
  35. data/lib/nswtopo/geometry/straight_skeleton/nodes.rb +5 -6
  36. data/lib/nswtopo/geometry/vector_sequence.rb +7 -6
  37. data/lib/nswtopo/gis/arcgis/connection.rb +56 -0
  38. data/lib/nswtopo/gis/arcgis/layer/map.rb +163 -0
  39. data/lib/nswtopo/gis/arcgis/layer/query.rb +87 -0
  40. data/lib/nswtopo/gis/arcgis/layer/renderer.rb +66 -0
  41. data/lib/nswtopo/gis/arcgis/layer/statistics.rb +15 -0
  42. data/lib/nswtopo/gis/arcgis/layer.rb +201 -0
  43. data/lib/nswtopo/gis/arcgis/service.rb +57 -0
  44. data/lib/nswtopo/gis/arcgis.rb +3 -0
  45. data/lib/nswtopo/gis/dem.rb +13 -12
  46. data/lib/nswtopo/gis/esri_hdr.rb +8 -2
  47. data/lib/nswtopo/gis/geojson/collection.rb +45 -21
  48. data/lib/nswtopo/gis/geojson/multi_line_string.rb +2 -24
  49. data/lib/nswtopo/gis/geojson/multi_polygon.rb +2 -53
  50. data/lib/nswtopo/gis/geojson/polygon.rb +15 -0
  51. data/lib/nswtopo/gis/geojson.rb +12 -3
  52. data/lib/nswtopo/gis/gps/kml.rb +25 -19
  53. data/lib/nswtopo/gis/gps.rb +2 -0
  54. data/lib/nswtopo/gis/projection.rb +35 -24
  55. data/lib/nswtopo/gis/shapefile.rb +89 -16
  56. data/lib/nswtopo/gis.rb +1 -2
  57. data/lib/nswtopo/helpers/array.rb +0 -11
  58. data/lib/nswtopo/helpers/colour.rb +34 -14
  59. data/lib/nswtopo/layer/arcgis_raster.rb +44 -48
  60. data/lib/nswtopo/layer/colour_mask.rb +5 -0
  61. data/lib/nswtopo/layer/contour.rb +35 -28
  62. data/lib/nswtopo/layer/control.rb +2 -7
  63. data/lib/nswtopo/layer/declination.rb +9 -9
  64. data/lib/nswtopo/layer/feature.rb +36 -22
  65. data/lib/nswtopo/layer/grid.rb +30 -27
  66. data/lib/nswtopo/layer/import.rb +1 -21
  67. data/lib/nswtopo/layer/labels/barrier.rb +39 -0
  68. data/lib/nswtopo/layer/labels.rb +551 -383
  69. data/lib/nswtopo/layer/mask_render.rb +37 -0
  70. data/lib/nswtopo/layer/overlay.rb +2 -2
  71. data/lib/nswtopo/layer/raster.rb +31 -41
  72. data/lib/nswtopo/layer/raster_import.rb +17 -0
  73. data/lib/nswtopo/layer/raster_render.rb +15 -0
  74. data/lib/nswtopo/layer/relief.rb +27 -95
  75. data/lib/nswtopo/layer/spot.rb +63 -62
  76. data/lib/nswtopo/layer/vector/cutout.rb +15 -0
  77. data/lib/nswtopo/layer/vector/knockout.rb +16 -0
  78. data/lib/nswtopo/layer/vector.rb +121 -89
  79. data/lib/nswtopo/layer/vegetation.rb +39 -34
  80. data/lib/nswtopo/layer.rb +30 -16
  81. data/lib/nswtopo/map.rb +202 -109
  82. data/lib/nswtopo/os.rb +5 -27
  83. data/lib/nswtopo/tiled_web_map.rb +54 -0
  84. data/lib/nswtopo/tree_indenter.rb +27 -0
  85. data/lib/nswtopo/version.rb +27 -2
  86. data/lib/nswtopo.rb +6 -199
  87. metadata +41 -22
  88. data/lib/nswtopo/font/chrome.rb +0 -59
  89. data/lib/nswtopo/font/generic.rb +0 -25
  90. data/lib/nswtopo/gis/arcgis_server/connection.rb +0 -52
  91. data/lib/nswtopo/gis/arcgis_server.rb +0 -155
  92. data/lib/nswtopo/gis/geojson/multi_point.rb +0 -12
  93. data/lib/nswtopo/gis/world_file.rb +0 -19
  94. data/lib/nswtopo/layer/labels/fence.rb +0 -20
@@ -3,7 +3,9 @@ module NSWTopo
3
3
  module KML
4
4
  def styles(placemark)
5
5
  style_url = placemark.elements["styleUrl"]&.text&.delete_prefix(?#)
6
- style_element = @xml.elements["/kml/Document/[@id='%s']" % style_url]
6
+ return {} unless style_url
7
+
8
+ style_element = @xml.elements["/kml/Document/*[@id='%s']" % style_url]
7
9
  result = {}
8
10
 
9
11
  case style_element&.name
@@ -39,25 +41,29 @@ module NSWTopo
39
41
 
40
42
  def collection
41
43
  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) }
44
+ @xml.elements.each "/kml//Placemark" do |placemark|
45
+ %w[. MultiGeometry].each do |container|
46
+ placemark.elements.each "#{container}/Point/coordinates" do |coordinates|
47
+ coords = coordinates.text.split(',').take(2).map(&:to_f)
48
+ collection.add_point coords, properties(placemark).merge("styles" => {})
49
+ end
50
+ placemark.elements.each "#{container}/LineString/coordinates" do |coordinates|
51
+ coords = coordinates.text.split(' ').map { |triplet| triplet.split(',')[0..1].map(&:to_f) }
52
+ collection.add_linestring coords, properties(placemark).merge("styles" => styles(placemark))
53
+ end
54
+ placemark.elements.each "#{container}/gx:Track[gx:coord]" do |track|
55
+ coords = track.collect("gx:coord") { |coord| coord.text.split(?\s).take(2).map(&:to_f) }
56
+ collection.add_linestring coords, properties(placemark).merge("styles" => styles(placemark))
57
+ end
58
+ placemark.elements.each "#{container}/Polygon[outerBoundaryIs/LinearRing/coordinates]" do |polygon|
59
+ coords = [polygon.elements["outerBoundaryIs/LinearRing/coordinates"].text]
60
+ coords += polygon.elements.collect("innerBoundaryIs/LinearRing/coordinates", &:text)
61
+ coords.map! do |text|
62
+ text.split(' ').map { |triplet| triplet.split(?,).take(2).map(&:to_f) }
63
+ end
64
+ collection.add_polygon coords, properties(placemark).merge("styles" => styles(placemark))
65
+ end
59
66
  end
60
- collection.add_polygon coords, properties(placemark).merge("styles" => styles(placemark))
61
67
  end
62
68
  end
63
69
  end
@@ -11,6 +11,8 @@ module NSWTopo
11
11
  else
12
12
  raise "invalid GPX or KML file: #{path}"
13
13
  end
14
+ rescue SystemCallError
15
+ raise "couldn't read file: #{path}"
14
16
  end
15
17
 
16
18
  def self.load(path)
@@ -1,56 +1,67 @@
1
1
  module NSWTopo
2
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?
3
+ def initialize(value)
4
+ @wkt2 = Projection === value ? value.wkt2 : OS.gdalsrsinfo("-o", "wkt2", "--single-line", value).chomp.strip
5
+ raise "no georeferencing found: %s" % value if @wkt2.empty?
6
6
  end
7
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
8
+ attr_reader :wkt2
9
+ alias to_s wkt2
10
+ alias to_str wkt2
17
11
 
18
12
  def ==(other)
19
- proj4 == other.proj4
13
+ wkt2 == other.wkt2
20
14
  end
21
15
 
22
16
  extend Forwardable
23
- delegate :hash => :@proj4
17
+ delegate :hash => :@wkt2
24
18
  alias eql? ==
25
19
 
26
- def self.utm(zone, south = true)
27
- new("+proj=utm +zone=#{zone}#{' +south' if south} +ellps=WGS84 +datum=WGS84 +units=m +no_defs")
20
+ def metres?
21
+ OS.gdalsrsinfo("-o", "proj4", "--single-line", @wkt2).chomp.split.any?("+units=m")
22
+ end
23
+
24
+ def self.utm(zone, south: true)
25
+ new("EPSG:32%1d%02d" % [south ? 7 : 6, zone])
28
26
  end
29
27
 
30
28
  def self.wgs84
31
- new("+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs")
29
+ new("EPSG:4326")
30
+ end
31
+
32
+ def self.from(**params)
33
+ params.map do |key, value|
34
+ "+#{key}=#{value}"
35
+ end.then do |args|
36
+ new args.join(?\s)
37
+ end
38
+ end
39
+
40
+ def self.transverse_mercator(lon_0, lat_0, **params)
41
+ from proj: "tmerc", datum: "WGS84", lon_0: lon_0, lat_0: lat_0, **params
32
42
  end
33
43
 
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")
44
+ def self.oblique_mercator(lonc, lat_0, alpha:, **params)
45
+ from proj: "omerc", datum: "WGS84", lonc: lonc, lat_0: lat_0, gamma: 0, alpha: alpha, **params
36
46
  end
37
47
 
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")
48
+ def self.azimuthal_equidistant(lon_0, lat_0)
49
+ from proj: "aeqd", datum: "WGS84", lon_0: lon_0, lat_0: lat_0
40
50
  end
41
51
 
42
52
  def self.utm_zones(collection)
43
53
  collection.reproject_to_wgs84.bounds.first.map do |longitude|
44
54
  (longitude / 6).floor + 31
45
- end.yield_self do |min, max|
55
+ end.then do |min, max|
46
56
  min..max
47
57
  end
48
58
  end
49
59
 
50
- def self.utm_hull(zone)
60
+ def self.utm_geometry(zone)
51
61
  longitudes = [31, 30].map { |offset| (zone - offset) * 6.0 }
52
62
  latitudes = [-80.0, 84.0]
53
- longitudes.product(latitudes).values_at(0,2,3,1)
63
+ ring = longitudes.product(latitudes).values_at(0,2,3,1,0)
64
+ GeoJSON.polygon [ring], projection: Projection.wgs84
54
65
  end
55
66
  end
56
67
  end
@@ -1,24 +1,97 @@
1
1
  module NSWTopo
2
2
  module Shapefile
3
- Error = Class.new RuntimeError
3
+ class Source
4
+ def self.===(path)
5
+ OS.ogrinfo "-ro", "-so", path
6
+ true
7
+ rescue OS::Error
8
+ false
9
+ end
4
10
 
5
- def self.===(path)
6
- OS.ogrinfo "-ro", "-so", path
7
- true
8
- rescue OS::Error
9
- false
11
+ def initialize(path)
12
+ @path = path
13
+ end
14
+ attr_accessor :path
15
+
16
+ def layer(**options)
17
+ Layer.new self, **options
18
+ end
19
+
20
+ def only_layer
21
+ name, *others = OS.ogrinfo("-ro", "-so", @path).scan(/^\w*\d+: (.*?)(?: \([\w\s]+\))?$/).flatten
22
+ return nil if others.any?
23
+ return name if name
24
+ File.basename(@path, File.extname(@path)).tap do |name|
25
+ OS.ogrinfo "-ro", "-so", @path, name
26
+ end
27
+ rescue OS::Error
28
+ end
29
+
30
+ def layer_info
31
+ OS.ogrinfo("-ro", "-so", @path).scan(/^\w*\d+: (.*?)(?: \(([\w\s]+)\))?$/).sort_by(&:first).map do |name, geom_type|
32
+ geom_type ? "#{name} (#{geom_type.delete(?\s)})" : name
33
+ end
34
+ end
10
35
  end
11
36
 
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
37
+ class Layer
38
+ NoLayerError = Class.new RuntimeError
39
+
40
+ def initialize(source, layer: nil, where: nil, fields: nil, sql: nil, geometry: nil, projection: nil)
41
+ @source, @layer, @where, @fields, @sql, @geometry, @projection = source, layer, where, fields, sql, geometry, projection
42
+ end
43
+
44
+ def features
45
+ raise "can't specify both SQL and where clause" if @sql && @where
46
+ raise "can't specify both SQL and layer name" if @sql && @layer
47
+ raise "no layer name or SQL specified" unless @layer || @sql
48
+ sql = ["-sql", sql] if @sql
49
+ where = ["-where", "(" << Array(@where).join(") AND (") << ")"] if @where
50
+ srs = ["-t_srs", @projection] if @projection
51
+ spat = ["-spat", *@geometry.bounds.transpose.flatten, "-spat_srs", @geometry.projection] if @geometry
52
+ misc = %w[-mapFieldType Date=Integer,DateTime=Integer -dim XY]
53
+ json = OS.ogr2ogr *(sql || where), *srs, *spat, *misc, *%w[-f GeoJSON -lco RFC7946=NO /vsistdout/], @source.path, *@layer
54
+ GeoJSON::Collection.load json, **{projection: @projection}.compact
55
+ rescue OS::Error => error
56
+ raise unless /Couldn't fetch requested layer (.*)!/ === error.message
57
+ raise "no such layer: #{$1}"
58
+ end
59
+
60
+ def counts
61
+ raise NoLayerError, "no layer name provided" unless @layer
62
+ count = ?_ * @fields.map(&:size).max + "count"
63
+ where = %Q[WHERE (%s)] % [*@where].join(") AND (") if @where
64
+ field_list = %Q["%s"] % @fields.join('", "')
65
+ sql = <<~SQL % [field_list, count, @layer, where, field_list]
66
+ SELECT %s, count(*) AS "%s"
67
+ FROM "%s"
68
+ %s
69
+ GROUP BY %s
70
+ SQL
71
+ json = OS.ogr2ogr *%w[-f GeoJSON -lco RFC7946=NO -dialect sqlite -sql], sql, "/vsistdout/", @source.path
72
+ JSON.parse(json)["features"].map do |feature|
73
+ feature["properties"]
74
+ end.map do |properties|
75
+ [properties.slice(*@fields), properties[count]]
76
+ end
77
+ rescue OS::Error => error
78
+ raise unless /no such column: (.*)$/ === error.message
79
+ raise "invalid field: #{$1}"
80
+ end
81
+
82
+ def info
83
+ raise NoLayerError, "no layer name provided" unless @layer
84
+ info = OS.ogrinfo *%w[-ro -so -noextent], @source.path, @layer
85
+ geom_type = info.match(/^Geometry: (.*)$/)&.[](1)&.delete(?\s)
86
+ count = info.match(/^Feature Count: (\d+)$/)&.[](1)
87
+ fields = info.scan(/^(.*): (.*?) \(\d+\.\d+\)$/).to_h
88
+ wkt = info.each_line.slice_after(/^Layer SRS WKT:/).drop(1).first&.slice_before(/^\S/)&.first&.join
89
+ epsg = OS.gdalsrsinfo("-o", "epsg", wkt)[/\d+/] if wkt and !wkt["unknown"]
90
+ { name: @layer, geometry: geom_type, EPSG: epsg, features: count, fields: (fields unless fields.empty?) }.compact
91
+ rescue OS::Error => error
92
+ raise unless /Couldn't fetch requested layer (.*)!/ === error.message
93
+ raise "no such layer: #{$1}"
94
+ end
22
95
  end
23
96
  end
24
97
  end
data/lib/nswtopo/gis.rb CHANGED
@@ -1,9 +1,8 @@
1
1
  require_relative 'gis/projection'
2
2
  require_relative 'gis/geojson'
3
3
  require_relative 'gis/gps'
4
- require_relative 'gis/world_file'
5
4
  require_relative 'gis/esri_hdr'
6
- require_relative 'gis/arcgis_server'
5
+ require_relative 'gis/arcgis'
7
6
  require_relative 'gis/shapefile'
8
7
  require_relative 'gis/gdal_glob'
9
8
  require_relative 'gis/dem'
@@ -14,17 +14,6 @@ module ArrayHelpers
14
14
  def in_two
15
15
  each_slice(1 + [length - 1, 0].max / 2)
16
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
17
  end
29
18
 
30
19
  Array.send :include, ArrayHelpers
@@ -151,26 +151,46 @@ class Colour
151
151
  yellowgreen: [154, 205, 50]
152
152
  YAML
153
153
 
154
- def initialize(string_or_array)
155
- @triplet = case string_or_array
156
- when Array then string_or_array.take(3).map(&:round)
154
+ def initialize(value)
155
+ @string = value
156
+ @triplet = case value
157
+ when Colour
158
+ @string = value.string.dup
159
+ value.triplet.dup
160
+ when Array
161
+ value.take(3).map(&:round)
157
162
  when *COLOURS.keys
158
- @name = string_or_array
159
- COLOURS[string_or_array]
163
+ COLOURS[value]
160
164
  when /^#?([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})$/i
161
165
  [$1, $2, $3].map { |hex| Integer("0x#{hex}") }
162
- when /^rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)$/
166
+ when /^rgb\((\d{1,3}), *(\d{1,3}), *(\d{1,3})\)$/
163
167
  [$1, $2, $3].map(&:to_i)
168
+ when /^hsl\((\d{1,3}), *(\d{1,3})%, *(\d{1,3})%\)$/
169
+ h, s, l = [$1, $2, $3].map(&:to_i)
170
+ h %= 360;
171
+ c = (100 - (2 * l - 100).abs) * s / 10000.0
172
+ x = (60 - (h % 120 - 60).abs) * c / 60.0
173
+ m = (l - 50 * c) / 100.0
174
+ r, g, b = case
175
+ when s == 0 then [0, 0, 0]
176
+ when h < 60 then [c, x, 0]
177
+ when h < 120 then [x, c, 0]
178
+ when h < 180 then [0, c, x]
179
+ when h < 240 then [0, x, c]
180
+ when h < 300 then [x, 0, c]
181
+ when h < 360 then [c, 0, x]
182
+ end.map do |v|
183
+ 255 * (v + m)
184
+ end.map(&:to_i)
164
185
  end
165
- raise Error, "invalid colour: #{string_or_array}" unless @triplet&.all?(0..255)
186
+ raise Error, "invalid colour: #{value}" unless @triplet&.all?(0..255)
187
+ @string = "rgb(%i,%i,%i)" % @triplet unless String === @string
188
+ @string.tr! ?\s, ""
166
189
  end
167
- attr_reader :triplet
168
190
 
169
- def mix(other, fraction)
170
- Colour.new [triplet, other.triplet].along(fraction.to_f).map(&:to_i)
171
- end
191
+ attr_reader :triplet, :string
192
+ alias to_s string
172
193
 
173
- def to_s
174
- @name || "#%.2X%.2X%.2X" % triplet
175
- end
194
+ extend Forwardable
195
+ delegate :[] => :@triplet
176
196
  end
@@ -1,73 +1,69 @@
1
1
  module NSWTopo
2
2
  module ArcGISRaster
3
- include Raster, Log
3
+ include Raster, RasterRender, Log
4
4
  CREATE = %w[url]
5
5
 
6
6
  def get_raster(temp_dir)
7
- raise "no resolution specified for #{@name}" unless Numeric === @resolution
7
+ raise "no resolution specified for #{@name}" unless Numeric === @mm_per_px
8
8
  txt_path = temp_dir / "mosaic.txt"
9
9
  vrt_path = temp_dir / "mosaic.vrt"
10
10
 
11
- ArcGISServer.start @url do |connection, service, projection|
12
- local_bbox = @map.bounding_box
13
- target_bbox = local_bbox.reproject_to projection
14
- target_resolution = @resolution * Math::sqrt(target_bbox.first.area / local_bbox.first.area)
11
+ service = ArcGIS::Service.new @url
12
+ local_bbox = @map.cutline.bbox
13
+ target_bbox = local_bbox.reproject_to service.projection
14
+ target_resolution = @mm_per_px * Math::sqrt(target_bbox.first.area / local_bbox.first.area)
15
15
 
16
- raise "not a tiled map or image server: #{@url}" unless tile_info = service["tileInfo"]
17
- lods = tile_info["lods"]
18
- origin = tile_info["origin"].values_at "x", "y"
19
- tile_sizes = tile_info.values_at "cols", "rows"
16
+ raise "not a tiled map or image server: #{@url}" unless tile_info = service["tileInfo"]
17
+ lods = tile_info["lods"]
18
+ origin = tile_info["origin"].values_at "x", "y"
19
+ tile_sizes = tile_info.values_at "cols", "rows"
20
20
 
21
- lods.sort_by! do |lod|
22
- -lod["resolution"]
23
- end
24
- lod = lods.find do |lod|
25
- lod["resolution"] < target_resolution
26
- end || lods.last
27
- tile_level, tile_resolution = lod.values_at "level", "resolution"
28
-
29
- tiles = target_bbox.coordinates.first.map do |corner|
30
- corner.minus(origin)
31
- end.transpose.map(&:minmax).zip(tile_sizes).map do |bound, tile_size|
32
- bound / tile_resolution / tile_size
33
- end.map do |min, max|
34
- (min.floor..max.ceil).each_cons(2).to_a
35
- end.inject(&:product).map do |cols, rows|
36
- bounds = [cols, rows].zip(tile_sizes).map do |indices, tile_size|
37
- indices.times(tile_size * tile_resolution)
38
- end.transpose.map do |corner|
39
- corner.plus(origin)
40
- end.transpose
41
-
42
- bbox = bounds.inject(&:product).values_at(0,2,3,1)
43
- next unless target_bbox.first.clip(bbox)
21
+ lods.sort_by! do |lod|
22
+ -lod["resolution"]
23
+ end
24
+ lod = lods.find do |lod|
25
+ lod["resolution"] < target_resolution
26
+ end || lods.last
27
+ tile_level, tile_resolution = lod.values_at "level", "resolution"
44
28
 
29
+ target_bbox.coordinates.first.map do |corner|
30
+ corner.minus(origin)
31
+ end.transpose.map(&:minmax).zip(tile_sizes).map do |bound, tile_size|
32
+ bound / tile_resolution / tile_size
33
+ end.map do |min, max|
34
+ (min.floor..max.ceil).each_cons(2).to_a
35
+ end.inject(&:product).inject(GeoJSON::Collection.new(projection: service.projection)) do |tiles, (cols, rows)|
36
+ [cols, rows].zip(tile_sizes).map do |indices, tile_size|
37
+ indices.times(tile_size * tile_resolution)
38
+ end.transpose.map do |corner|
39
+ corner.plus(origin)
40
+ end.transpose.then do |bounds|
41
+ ring = bounds.inject(&:product).values_at(0,2,3,1,0)
42
+ ullr = bounds.inject(&:product).values_at(1,2).flatten
45
43
  row, col = rows[1].abs, cols[0]
44
+ tiles.add_polygon [ring], ullr: ullr, row: row, col: col
45
+ end
46
+ end.clip(target_bbox.first).then do |tiles|
47
+ tiles.map.with_index do |feature, index|
48
+ row, col, ullr = feature.values_at("row", "col", "ullr")
46
49
  rel_path = "tile/#{tile_level}/#{row}/#{col}"
47
50
  jpg_path = temp_dir / "#{row}.#{col}" # could be png
48
51
  tif_path = temp_dir / "#{row}.#{col}.tif"
49
-
50
- ullr = bounds.inject(&:product).values_at(1,2).flatten
51
- gdal_args = ["-a_srs", projection, "-a_ullr", *ullr, "-of", "GTiff", jpg_path, tif_path]
52
-
53
- [rel_path, jpg_path, gdal_args, tif_path]
54
- end.compact
55
- tiles.each.with_index do |(rel_path, jpg_path, gdal_args, tif_path), index|
52
+ gdal_args = ["-a_srs", service.projection, "-a_ullr", *ullr, "-of", "GTiff", jpg_path, tif_path]
56
53
  log_update "%s: retrieving tile %i of %i" % [@name, index + 1, tiles.length]
57
- connection.get(rel_path, blankTile: true) do |response|
54
+ service.get(rel_path, blankTile: true) do |response|
58
55
  jpg_path.binwrite response.body
59
56
  end
57
+ OS.gdal_translate *gdal_args
58
+ tif_path
60
59
  end
61
- end.each do |rel_path, jpg_path, gdal_args, tif_path|
62
- OS.gdal_translate *gdal_args
63
- end.map(&:last).tap do |tif_paths|
60
+ end.tap do |tif_paths|
61
+ log_update "%s: mosaicing %s tiles" % [@name, tif_paths.length] if tif_paths.length > 1
64
62
  txt_path.write tif_paths.join(?\n)
65
- OS.gdalbuildvrt "-input_file_list", txt_path, vrt_path
66
63
  end
67
64
 
68
- OS.gdal_translate vrt_path, Pathname.pwd / "foo.tif"
69
-
70
- return @resolution, vrt_path
65
+ OS.gdalbuildvrt "-input_file_list", txt_path, vrt_path
66
+ return vrt_path
71
67
  end
72
68
  end
73
69
  end
@@ -0,0 +1,5 @@
1
+ module NSWTopo
2
+ module ColourMask
3
+ include RasterImport, Raster, MaskRender
4
+ end
5
+ end
@@ -1,7 +1,7 @@
1
1
  module NSWTopo
2
2
  module Contour
3
3
  include Vector, DEM, Log
4
- CREATE = %w[interval index smooth simplify thin density min-length no-depression knolls fill]
4
+ CREATE = %w[interval index auxiliary smooth simplify thin density min-length no-depression knolls fill]
5
5
  DEFAULTS = YAML.load <<~YAML
6
6
  interval: 5
7
7
  smooth: 0.2
@@ -9,8 +9,11 @@ module NSWTopo
9
9
  min-length: 2.0
10
10
  knolls: 0.2
11
11
  section: 100
12
- stroke: "#805100"
12
+ stroke: hsl(40,100%,25%)
13
13
  stroke-width: 0.08
14
+ Auxiliary:
15
+ stroke-dasharray: 0.5 0.5
16
+ stroke-dashoffset: 0.5
14
17
  Depression:
15
18
  symbolise:
16
19
  interval: 2.0
@@ -26,9 +29,10 @@ module NSWTopo
26
29
  max-turn: 20
27
30
  sample: 10
28
31
  minimum-area: 70
29
- separation: 40
30
- separation-all: 15
31
- separation-along: 100
32
+ separation:
33
+ self: 40
34
+ other: 15
35
+ along: 100
32
36
  YAML
33
37
 
34
38
  def margin
@@ -36,7 +40,7 @@ module NSWTopo
36
40
  end
37
41
 
38
42
  def check_geos!
39
- json = OS.ogr2ogr "-dialect", "SQLite", "-sql", "SELECT geos_version() AS version", "-f", "GeoJSON", "/vsistdout/", "/vsistdin/" do |stdin|
43
+ json = OS.ogr2ogr "-dialect", "SQLite", "-sql", "SELECT geos_version() AS version", "-f", "GeoJSON", "-lco", "RFC7946=NO", "/vsistdout/", "/vsistdin/" do |stdin|
40
44
  stdin.write GeoJSON::Collection.new.to_json
41
45
  end
42
46
  raise unless version = JSON.parse(json).dig("features", 0, "properties", "version")
@@ -46,7 +50,7 @@ module NSWTopo
46
50
  end
47
51
 
48
52
  def get_features
49
- @simplify ||= [0.5 * @interval / Math::tan(Math::PI * 85 / 180), 0.001 * 0.05 * @map.scale].min
53
+ @simplify ||= [@map.to_mm(0.5 * @interval) / Math::tan(Math::PI * 85 / 180), 0.05].min
50
54
  @index ||= 10 * @interval
51
55
  @params = {
52
56
  "Index" => { "stroke-width" => 2 * @params["stroke-width"] },
@@ -71,7 +75,7 @@ module NSWTopo
71
75
 
72
76
  log_update "%s: generating contour lines" % @name
73
77
  json = OS.gdal_contour "-q", "-a", "elevation", "-i", @interval, "-f", "GeoJSON", "-lco", "RFC7946=NO", blur_path, "/vsistdout/"
74
- contours = GeoJSON::Collection.load json, @map.projection
78
+ contours = GeoJSON::Collection.load json, projection: @map.projection
75
79
 
76
80
  if @no_depression.nil?
77
81
  candidates = contours.select do |feature|
@@ -94,7 +98,7 @@ module NSWTopo
94
98
 
95
99
  contours.reject! do |feature|
96
100
  feature.coordinates.last == feature.coordinates.first &&
97
- feature.bounds.all? { |min, max| max - min < @knolls * @map.scale / 1000.0 }
101
+ feature.bounds.all? { |min, max| max - min < @knolls }
98
102
  end
99
103
 
100
104
  contours.each do |feature|
@@ -106,14 +110,16 @@ module NSWTopo
106
110
  feature["elevation"].zero?
107
111
  end
108
112
 
109
- OS.ogr2ogr "-a_srs", @map.projection, "-nln", "contour", "-simplify", @simplify, *db_flags, db_path, "GeoJSON:/vsistdin/" do |stdin|
110
- stdin.write contours.to_json
113
+ contours.each_slice(100).inject(nil) do |update, features|
114
+ OS.ogr2ogr "-a_srs", @map.projection, "-nln", "contour", *update, "-simplify", @simplify, *db_flags, db_path, "GeoJSON:/vsistdin/" do |stdin|
115
+ stdin.write GeoJSON::Collection.new(projection: @map.projection, features: features).to_json
116
+ end
117
+ %w[-update -append]
111
118
  end
112
119
 
113
120
  if @thin
114
121
  slope_tif_path = temp_dir / "slope.tif"
115
122
  slope_vrt_path = temp_dir / "slope.vrt"
116
- min_length = @min_length * @map.scale / 1000.0
117
123
 
118
124
  log_update "%s: generating slope masks" % @name
119
125
  OS.gdaldem "slope", blur_path, slope_tif_path, "-compute_edges"
@@ -123,24 +129,21 @@ module NSWTopo
123
129
  OS.gdal_translate "-srcwin", *srcwin, "-a_nodata", "none", "-of", "VRT", slope_tif_path, slope_vrt_path
124
130
 
125
131
  multiplier = @index / @interval
126
- case multiplier
127
- when 4 then [ [1,3], 2 ]
128
- when 5 then [ [1,4], [2,3] ]
129
- when 6 then [ [1,4], [2,5], 3 ]
130
- when 7 then [ [2,5], [1,3,6], 4 ]
131
- when 8 then [ [1,3,5,7], [2,6], 4 ]
132
- when 9 then [ [1,4,7], [2,5,8], [3,6] ]
133
- when 10 then [ [2,5,8], [1,4,6,9], [3,7] ]
134
- else raise "contour thinning not available for specified index interval"
135
- end.inject(multiplier) do |count, (*drop)|
136
- angle = Math::atan(1000.0 * @index * @density / @map.scale / count) * 180.0 / Math::PI
132
+ Enumerator.new do |yielder|
133
+ keep = 0...multiplier
134
+ until keep.one?
135
+ keep, drop = keep.count.even? ? keep.each_slice(2).entries.transpose : [[0], keep.drop(1)]
136
+ yielder << drop
137
+ end
138
+ end.inject(multiplier) do |count, drop|
139
+ angle = Math::atan(@index * @density / count) * 180.0 / Math::PI
137
140
  mask_path = temp_dir / "mask.#{count}.sqlite"
138
141
 
139
142
  OS.gdal_contour "-nln", "ring", "-a", "angle", "-fl", angle, *db_flags, slope_vrt_path, mask_path
140
143
 
141
144
  OS.ogr2ogr "-update", "-nln", "mask", "-nlt", "MULTIPOLYGON", mask_path, mask_path, "-dialect", "SQLite", "-sql", <<~SQL
142
145
  SELECT
143
- ST_Buffer(ST_Buffer(ST_Polygonize(geometry), #{0.5 * min_length}, 6), #{-0.5 * min_length}, 6) AS geometry
146
+ ST_Buffer(ST_Buffer(ST_Polygonize(geometry), #{0.5 * @min_length}, 6), #{-0.5 * @min_length}, 6) AS geometry
144
147
  FROM ring
145
148
  SQL
146
149
 
@@ -193,21 +196,25 @@ module NSWTopo
193
196
  OS.ogr2ogr "-nln", "thinned", "-update", "-explodecollections", db_path, db_path, "-dialect", "SQLite", "-sql", <<~SQL
194
197
  SELECT ST_LineMerge(ST_Collect(geometry)) AS geometry, id, elevation, modulo, depression, unaltered
195
198
  FROM divided
196
- WHERE unmasked OR ST_Length(geometry) < #{min_length}
199
+ WHERE unmasked OR ST_Length(geometry) < #{@min_length}
197
200
  GROUP BY id, elevation, modulo, unaltered
198
201
  SQL
199
202
 
200
203
  OS.ogr2ogr "-nln", "contour", "-update", "-overwrite", db_path, db_path, "-dialect", "SQLite", "-sql", <<~SQL
201
204
  SELECT geometry, id, elevation, modulo, depression
202
205
  FROM thinned
203
- WHERE unaltered OR ST_Length(geometry) > #{min_length}
206
+ WHERE unaltered OR ST_Length(geometry) > #{@min_length}
204
207
  SQL
205
208
  end
206
209
 
207
210
  json = OS.ogr2ogr "-f", "GeoJSON", "-lco", "RFC7946=NO", "/vsistdout/", db_path, "contour"
208
- GeoJSON::Collection.load(json, @map.projection).each do |feature|
211
+ GeoJSON::Collection.load(json, projection: @map.projection).each do |feature|
209
212
  elevation, modulo, depression = feature.values_at "elevation", "modulo", "depression"
210
- category = modulo.zero? ? %w[Index] : %w[Standard]
213
+ category = case
214
+ when @auxiliary && elevation % (2 * @interval) != 0 then %w[Auxiliary]
215
+ when modulo.zero? then %w[Index]
216
+ else %w[Standard]
217
+ end
211
218
  category << "Depression" if depression == 1
212
219
  feature.clear
213
220
  feature["elevation"] = elevation