nswtopo 2.0.0.pre.beta1 → 3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
@@ -0,0 +1,37 @@
1
+ module NSWTopo
2
+ module MaskRender
3
+ def render(cutouts:, **, &block)
4
+ colour, shade, gamma = @params.values_at "colour", "shade", "gamma"
5
+ raise "can't specify both colour and shade values" if colour && shade
6
+
7
+ REXML::Element.new("defs").tap do |defs|
8
+ defs.add_attributes("id" => "#{@name}.defs")
9
+ defs.add_element(image_element).add_attributes("id" => "#{@name}.content")
10
+
11
+ filter = defs.add_element("filter", "id" => "#{@name}.filter") if shade || gamma
12
+ filter.add_element("feColorMatrix", "values" => "-1 0 0 0 1 -1 0 0 0 1 -1 0 0 0 1 0 0 0 1 0", "color-interpolation-filters" => "sRGB") if shade
13
+ filter.add_element("feComponentTransfer", "color-interpolation-filters" => "sRGB").tap do |transfer|
14
+ gamma, clip = [*gamma, 0]
15
+ amplitude, offset = 1 / (1 - clip), clip / (clip - 1)
16
+ transfer.add_element("feFuncR", "type" => "gamma", "exponent" => gamma, "amplitude" => amplitude, "offset" => offset)
17
+ transfer.add_element("feFuncG", "type" => "gamma", "exponent" => gamma, "amplitude" => amplitude, "offset" => offset)
18
+ transfer.add_element("feFuncB", "type" => "gamma", "exponent" => gamma, "amplitude" => amplitude, "offset" => offset)
19
+ end if gamma
20
+
21
+ defs.add_element("mask", "id" => "#{@name}.mask").tap do |mask|
22
+ use = mask.add_element("use", "href" => "##{@name}.content")
23
+ use.add_attributes("filter" => "url(##{@name}.filter)") if filter
24
+
25
+ cutouts.each.with_object mask.add_element("g", "filter" => "url(#map.filter.cutout)") do |cutout, group|
26
+ group.add_element cutout.use
27
+ end if cutouts.any?
28
+ end
29
+ end.tap(&block)
30
+
31
+ REXML::Element.new("use").tap do |use|
32
+ use.add_attributes "id" => @name, "mask" => "url(##{@name}.mask)", "href" => "#map.neatline", "fill" => shade || colour
33
+ use.add_attributes @params.slice("opacity")
34
+ end.tap(&block)
35
+ end
36
+ end
37
+ end
@@ -12,8 +12,8 @@ module NSWTopo
12
12
  def get_features
13
13
  GPS.new(@path).tap do |gps|
14
14
  @simplify = true if GPS::GPX === gps
15
- @tolerance ||= [5, TOLERANCE * @map.scale / 1000.0].max if @simplify
16
- end.collection.reproject_to(@map.projection).explode.each do |feature|
15
+ @tolerance ||= [@map.to_mm(5), TOLERANCE].max if @simplify
16
+ end.collection.reproject_to(@map.neatline.projection).explode.each do |feature|
17
17
  styles, folder, name = feature.values_at "styles", "folder", "name"
18
18
  styles ||= GPX_STYLES
19
19
 
@@ -1,21 +1,14 @@
1
1
  module NSWTopo
2
2
  module Raster
3
3
  def create
4
- tif = Dir.mktmppath do |temp_dir|
5
- tif_path = temp_dir / "final.tif"
6
- tfw_path = temp_dir / "final.tfw"
7
- out_path = temp_dir / "output.tif"
8
-
9
- resolution, raster_path = get_raster(temp_dir)
10
- dimensions, ppi, resolution = @map.raster_dimensions_at resolution: resolution
11
- density = 0.01 * @map.scale / resolution
12
- tiff_tags = %W[-mo TIFFTAG_XRESOLUTION=#{density} -mo TIFFTAG_YRESOLUTION=#{density} -mo TIFFTAG_RESOLUTIONUNIT=3]
13
-
14
- @map.write_world_file tfw_path, resolution: resolution
15
- OS.convert "-size", dimensions.join(?x), "canvas:none", "-type", "TrueColorMatte", "-depth", 8, tif_path
16
- OS.gdalwarp "-t_srs", @map.projection, "-r", "bilinear", raster_path, tif_path
17
- OS.gdal_translate "-a_srs", @map.projection, *tiff_tags, tif_path, out_path
18
- @map.write filename, out_path.binread
4
+ Dir.mktmppath do |temp_dir|
5
+ args = ["-t_srs", @map.projection, "-r", "bilinear", "-cutline", "GeoJSON:/vsistdin/", "-te", *@map.te, "-of", "GTiff", "-co", "TILED=YES"]
6
+ args += ["-tr", @mm_per_px, @mm_per_px] if Numeric === @mm_per_px
7
+ OS.gdalwarp *args, get_raster(temp_dir), "/vsistdout/" do |stdin|
8
+ stdin.puts @map.cutline.to_json
9
+ end.then do |tif|
10
+ @map.write filename, tif
11
+ end
19
12
  end
20
13
  end
21
14
 
@@ -27,37 +20,34 @@ module NSWTopo
27
20
  false
28
21
  end
29
22
 
30
- def size_resolution
31
- json = OS.gdalinfo "-json", "/vsistdin/" do |stdin|
32
- stdin.binmode
33
- stdin.write @map.read(filename)
23
+ def image_element
24
+ REXML::Element.new("image").tap do |image|
25
+ tif = @map.read filename
26
+ OS.gdalinfo "-json", "/vsistdin/" do |stdin|
27
+ stdin.binmode.write tif
28
+ end.then do |json|
29
+ JSON.parse(json).values_at "size", "geoTransform"
30
+ end.then do |(width, height), (_, mm_per_px, *)|
31
+ image.add_attributes "width" => width, "height" => height, "transform" => "scale(#{mm_per_px})"
32
+ end
33
+ OS.gdal_translate "-of", "PNG", "-co", "ZLEVEL=9", "/vsistdin/", "/vsistdout/" do |stdin|
34
+ stdin.binmode.write tif
35
+ end.then do |png|
36
+ image.add_attributes "href" => "data:image/png;base64,#{Base64.encode64 png}", "image-rendering" => "optimizeQuality"
37
+ end
34
38
  end
35
- size, geotransform = JSON.parse(json).values_at "size", "geoTransform"
36
- resolution = geotransform.values_at(1, 2).norm
37
- return size, resolution
38
39
  end
39
40
 
40
41
  def to_s
41
- size, resolution = size_resolution
42
- megapixels = size.inject(&:*) / 1024.0 / 1024.0
43
- ppi = 0.0254 * @map.scale / resolution
44
- "%s: %i×%i (%.1fMpx) @ %.1fm/px (%.0f ppi)" % [@name, *size, megapixels, resolution, ppi]
45
- end
46
-
47
- def render(group, defs)
48
- (width, height), resolution = size_resolution
49
- group.add_attributes "style" => "opacity:%s" % params.fetch("opacity", 1)
50
- transform = "scale(#{1000.0 * resolution / @map.scale})"
51
- png = Dir.mktmppath do |temp_dir|
52
- tif_path = temp_dir / "raster.tif"
53
- png_path = temp_dir / "raster.png"
54
- tif_path.binwrite @map.read(filename)
55
- OS.gdal_translate "-of", "PNG", "-co", "ZLEVEL=9", tif_path, png_path
56
- png_path.binread
42
+ OS.gdalinfo "-json", "/vsistdin/" do |stdin|
43
+ stdin.binmode.write @map.read(filename)
44
+ end.then do |json|
45
+ JSON.parse(json).values_at "size", "geoTransform"
46
+ end.then do |(width, height), (_, mm_per_px, *)|
47
+ resolution, ppi = @map.to_metres(mm_per_px), 25.4 / mm_per_px
48
+ megapixels = width * height / 1024.0 / 1024.0
49
+ "%s: %i×%i (%.1fMpx) @ %.3gm/px (%.3g ppi)" % [@name, width, height, megapixels, resolution, ppi]
57
50
  end
58
- href = "data:image/png;base64,#{Base64.encode64 png}"
59
- group.add_element "image", "transform" => transform, "width" => width, "height" => height, "image-rendering" => "optimizeQuality", "xlink:href" => href
60
- group.add_attribute "mask", "url(#raster-mask)" if defs.elements["mask[@id='raster-mask']"]
61
51
  end
62
52
  end
63
53
  end
@@ -0,0 +1,17 @@
1
+ module NSWTopo
2
+ module RasterImport
3
+ def get_raster(temp_dir)
4
+ @path = Pathname(@path).expand_path(@source ? @source.parent : Pathname.pwd)
5
+ temp_dir.join("import.vrt").tap do |vrt_path|
6
+ JSON.parse(OS.gdalinfo "-json", @path).fetch("bands").any? do |band|
7
+ "Palette" == band["colorInterpretation"]
8
+ end.then do |palette|
9
+ args = ["-expand", "rgba"] if palette
10
+ OS.gdal_translate *args, @path, vrt_path
11
+ end
12
+ end
13
+ rescue OS::Error
14
+ raise "invalid raster file: #{@path}"
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ module NSWTopo
2
+ module RasterRender
3
+ def render(**, &block)
4
+ REXML::Element.new("defs").tap do |defs|
5
+ defs.add_attributes("id" => "#{@name}.defs")
6
+ defs.add_element(image_element).add_attributes("id" => "#{@name}.content")
7
+ end.tap(&block)
8
+
9
+ REXML::Element.new("use").tap do |use|
10
+ use.add_attributes "id" => @name, "mask" => "none", "href" => "##{@name}.content"
11
+ use.add_attributes params.slice("opacity")
12
+ end.tap(&block)
13
+ end
14
+ end
15
+ end
@@ -1,16 +1,14 @@
1
1
  module NSWTopo
2
2
  module Relief
3
- include Raster, ArcGISServer, Shapefile, DEM, Log
4
- CREATE = %w[altitude azimuth factor sources yellow smooth median bilateral contours]
3
+ include Raster, MaskRender, DEM, Log
4
+ CREATE = %w[method azimuth factor smooth contours]
5
5
  DEFAULTS = YAML.load <<~YAML
6
- altitude: 45
6
+ shade: rgb(0,0,48)
7
+ method: combined
7
8
  azimuth: 315
8
9
  factor: 2.0
9
- sources: 3
10
- yellow: 0.2
11
10
  smooth: 4
12
- resolution: 5.0
13
- opacity: 0.3
11
+ opacity: 0.25
14
12
  YAML
15
13
 
16
14
  def margin
@@ -18,29 +16,30 @@ module NSWTopo
18
16
  end
19
17
 
20
18
  def get_raster(temp_dir)
19
+ cutline = @map.cutline(**margin)
21
20
  dem_path = temp_dir / "dem.tif"
22
- flat_relief = (Math::sin(@altitude * Math::PI / 180) * 255).to_i
23
21
 
24
22
  case
25
23
  when @path
26
24
  get_dem temp_dir, dem_path
27
25
 
28
26
  when @contours
29
- bounds = @map.bounds(margin: margin)
30
- txe, tye, spat = bounds[0], bounds[1].reverse, bounds.transpose.flatten
31
- outsize = (bounds.transpose.difference / @resolution).map(&:ceil)
27
+ bounds = cutline.bounds
28
+ raise "no resolution specified for #{@name}" unless Numeric === @mm_per_px
29
+ outsize = (bounds.transpose.diff / @mm_per_px).map(&:ceil)
32
30
 
33
31
  collection = @contours.map do |url_or_path, attribute_or_hash|
34
32
  raise "no elevation attribute specified for #{url_or_path}" unless attribute_or_hash
35
- options = Hash == attribute_or_hash ? attribute_or_hash.transform_keys(&:to_sym).slice(:where, :layer) : {}
36
- attribute = Hash == attribute_or_hash ? attribute_or_hash["attribute"] : attribute_or_hash
33
+ options = Hash === attribute_or_hash ? attribute_or_hash.transform_keys(&:to_sym).slice(:where, :layer) : {}
34
+ attribute = Hash === attribute_or_hash ? attribute_or_hash["attribute"] : attribute_or_hash
37
35
  case url_or_path
38
- when ArcGISServer
39
- arcgis_layer url_or_path, margin: margin, **options do |index, total|
40
- log_update "%s: retrieved %i of %i contours" % [@name, index, total]
36
+ when ArcGIS::Service
37
+ layer = ArcGIS::Service.new(url_or_path).layer(**options, geometry: cutline)
38
+ layer.features do |count, total|
39
+ log_update "%s: retrieved %i of %i contours" % [@name, count, total]
41
40
  end
42
- when Shapefile
43
- shapefile_layer source_path, margin: margin, **options
41
+ when Shapefile::Source
42
+ Shapefile::Source.new(url_or_path).layer(**options, geometry: cutline).features
44
43
  else
45
44
  raise "unrecognised elevation data source: #{url_or_path}"
46
45
  end.each do |feature|
@@ -49,7 +48,7 @@ module NSWTopo
49
48
  end.inject(&:merge)
50
49
 
51
50
  log_update "%s: calculating DEM" % @name
52
- OS.gdal_grid "-a", "linear:radius=0:nodata=-9999", "-zfield", "elevation", "-ot", "Float32", "-txe", *txe, "-tye", *tye, "-spat", *spat, "-outsize", *outsize, "/vsistdin/", dem_path do |stdin|
51
+ OS.gdal_grid "-a", "linear:radius=0:nodata=-9999", "-zfield", "elevation", "-ot", "Float32", "-txe", *bounds[0], "-tye", *bounds[1], "-outsize", *outsize, "/vsistdin/", dem_path do |stdin|
53
52
  stdin.puts collection.to_json
54
53
  end
55
54
 
@@ -57,87 +56,20 @@ module NSWTopo
57
56
  raise "no elevation data specified for relief layer #{@name}"
58
57
  end
59
58
 
60
- log_update "%s: generating shaded relief" % @name
61
- reliefs = -90.step(90, 90.0 / @sources).select.with_index do |offset, index|
62
- index.odd?
63
- end.map do |offset|
64
- (@azimuth + offset) % 360
65
- end.map do |azimuth|
66
- relief_path = temp_dir / "relief.#{azimuth}.bil"
67
- OS.gdaldem "hillshade", "-of", "EHdr", "-compute_edges", "-s", 1, "-alt", @altitude, "-z", @factor, "-az", azimuth, dem_path, relief_path
68
- [azimuth, ESRIHdr.new(relief_path, 0)]
69
- rescue OS::Error
70
- raise "invalid elevation data"
71
- end.to_h
72
-
73
- bil_path = temp_dir / "relief.bil"
74
- if reliefs.one?
75
- reliefs.values.first.write bil_path
76
- else
77
- blur_path = temp_dir / "dem.blurred.tif"
78
- blur_dem dem_path, blur_path
79
-
80
- aspect_path = temp_dir / "aspect.bil"
81
- OS.gdaldem "aspect", "-zero_for_flat", "-of", "EHdr", blur_path, aspect_path
82
- aspect = ESRIHdr.new aspect_path, 0.0
83
-
84
- log_update "%s: combining shaded relief" % @name
85
- reliefs.map do |azimuth, relief|
86
- [relief.values, aspect.values].transpose.map do |relief, aspect|
87
- relief ? aspect ? 2 * relief * Math::sin((aspect - azimuth) * Math::PI / 180)**2 : relief : flat_relief
88
- end
89
- end.transpose.map do |values|
90
- values.inject(&:+) / @sources
91
- end.map do |value|
92
- [255, value.ceil].min
93
- end.tap do |values|
94
- ESRIHdr.new(reliefs.values.first, values).write bil_path
95
- end
96
- end
97
-
59
+ raw_path = temp_dir / "relief.raw.tif"
98
60
  tif_path = temp_dir / "relief.tif"
99
- OS.gdalwarp "-co", "TFW=YES", "-s_srs", @map.projection, "-dstnodata", "None", bil_path, tif_path
100
61
 
101
- filters = []
102
- if @median
103
- pixels = (2 * @median + 1).to_i
104
- filters += %W[-channel RGBA -statistic median #{pixels}x#{pixels}]
105
- end
106
- if @bilateral
107
- threshold, sigma = *@bilateral, (60.0 / @resolution).round
108
- filters += %W[-channel RGB -selective-blur 0x#{sigma}+#{threshold}%]
109
- end
110
- if filters.any?
111
- log_update "%s: applying filters" % @name
112
- OS.mogrify "-virtual-pixel", "edge", *filters, tif_path
113
- end
114
-
115
- log_update "%s: rendering shaded relief" % @name
116
- vrt_path = temp_dir / "coloured.vrt"
117
- OS.gdalbuildvrt vrt_path, tif_path
118
-
119
- xml = REXML::Document.new vrt_path.read
120
- vrt_raster_band = xml.elements["VRTDataset/VRTRasterBand[ColorInterp[text()='Gray']]"]
121
- vrt_raster_band.elements["ColorInterp[text()='Gray']"].text = "Palette"
122
- color_table = vrt_raster_band.add_element "ColorTable"
123
-
124
- shade, sun = 90 * flat_relief / 100, (10 + 90 * flat_relief) / 100
125
- 256.times do |index|
126
- case
127
- when index < shade
128
- color_table.add_element "Entry", "c1" => 0, "c2" => 0, "c3" => 0, "c4" => (shade - index) * 255 / shade
129
- when index > sun
130
- color_table.add_element "Entry", "c1" => 255, "c2" => 255, "c3" => 0, "c4" => ((index - sun) * 255 * @yellow / (255 - sun)).to_i
131
- else
132
- color_table.add_element "Entry", "c1" => 0, "c2" => 0, "c3" => 0, "c4" => 0
62
+ begin
63
+ log_update "%s: generating shaded relief" % @name
64
+ OS.gdaldem *%W[hillshade -q -compute_edges -s #{@map.scale / 1000.0} -z #{@factor} -az #{@azimuth} -#{@method}], dem_path, raw_path
65
+ OS.gdalwarp "-t_srs", @map.projection, "-cutline", "GeoJSON:/vsistdin/", "-crop_to_cutline", raw_path, tif_path do |stdin|
66
+ stdin.puts cutline.to_json
133
67
  end
68
+ rescue OS::Error
69
+ raise "invalid elevation data"
134
70
  end
135
71
 
136
- vrt_path.write xml
137
- coloured_path = temp_dir / "coloured.tif"
138
- OS.gdal_translate "-expand", "rgba", vrt_path, coloured_path
139
- FileUtils.mv coloured_path, tif_path
140
- return @resolution, tif_path
72
+ return tif_path
141
73
  end
142
74
  end
143
75
  end
@@ -1,10 +1,11 @@
1
1
  module NSWTopo
2
2
  module Spot
3
3
  include Vector, DEM, Log
4
- CREATE = %w[spacing smooth prefer]
4
+ CREATE = %w[spacing smooth prefer extent]
5
5
  DEFAULTS = YAML.load <<~YAML
6
6
  spacing: 15
7
7
  smooth: 0.2
8
+ extent: 4
8
9
  symbol:
9
10
  circle:
10
11
  r: 0.2
@@ -16,7 +17,6 @@ module NSWTopo
16
17
  margin: 0.7
17
18
  position: [right, above, below, left, aboveright, belowright, aboveleft, belowleft]
18
19
  YAML
19
- NOISE_MM = 2.0 # TODO: noise sensitivity should depend on contour interval
20
20
 
21
21
  def margin
22
22
  { mm: 3 * @smooth }
@@ -39,12 +39,14 @@ module NSWTopo
39
39
  end
40
40
 
41
41
  module Candidate
42
+ attr_accessor :elevation, :knoll
43
+
42
44
  module PreferKnolls
43
- def ordinal; [conflicts.size, -self["elevation"]] end
45
+ def ordinal; [conflicts.size, -elevation] end
44
46
  end
45
47
 
46
48
  module PreferSaddles
47
- def ordinal; [conflicts.size, self["elevation"]] end
49
+ def ordinal; [conflicts.size, elevation] end
48
50
  end
49
51
 
50
52
  module PreferNeither
@@ -59,7 +61,7 @@ module NSWTopo
59
61
  self.ordinal <=> other.ordinal
60
62
  end
61
63
 
62
- def bounds(buffer = 0)
64
+ def bounds(buffer: 0)
63
65
  coordinates.map { |coordinate| [coordinate - buffer, coordinate + buffer] }
64
66
  end
65
67
  end
@@ -72,76 +74,79 @@ module NSWTopo
72
74
  end
73
75
  end
74
76
 
77
+ def pixels_knolls(dem_path, &block)
78
+ Enumerator.new do |yielder|
79
+ log_update "%s: calculating aspect map" % @name
80
+ aspect_path = dem_path.sub_ext ".bil"
81
+ OS.gdaldem "aspect", dem_path, aspect_path, "-trigonometric"
82
+ aspect = ESRIHdr.new aspect_path, -9999
83
+
84
+ offsets = [-1..1, -1..1].map(&:entries).inject(&:product).map do |row, col|
85
+ row * aspect.ncols + col - 1
86
+ end.values_at(0,3,6,7,8,5,2,1,0)
87
+
88
+ aspect.nrows.times do |row|
89
+ log_update "%s: finding flat areas: %.1f%%" % [@name, 100.0 * (row + 1) / aspect.nrows]
90
+ aspect.ncols.times do |col|
91
+ offsets.map!(&:next)
92
+ next if row < 1 || col < 1 || row >= aspect.nrows - 1 || col >= aspect.ncols - 1
93
+ next if block&.call col, row
94
+ ccw, cw = offsets.each_cons(2).inject([true, true]) do |(ccw, cw), (o1, o2)|
95
+ break unless ccw || cw
96
+ a1, a2 = aspect.values.values_at o1, o2
97
+ break unless a1 && a2
98
+ (a2 - a1) % 360 < 180 ? [ccw, false] : [false, cw]
99
+ end
100
+ yielder << [[col, row], true] if ccw
101
+ yielder << [[col, row], false] if cw
102
+ end
103
+ end
104
+ end
105
+ end
106
+
75
107
  def candidates
76
108
  @candidates ||= Dir.mktmppath do |temp_dir|
77
109
  raw_path = temp_dir / "raw.tif"
78
- dem_path = temp_dir / "dem.tif"
79
- aspect_path = temp_dir / "aspect.bil"
110
+ dem_hr_path = temp_dir / "dem.hr.tif"
111
+ dem_lr_path = temp_dir / "dem.lr.tif"
80
112
 
81
113
  if @smooth.zero?
82
- get_dem temp_dir, dem_path
114
+ get_dem temp_dir, dem_hr_path
83
115
  else
84
116
  get_dem temp_dir, raw_path
85
- blur_dem raw_path, dem_path
117
+ blur_dem raw_path, dem_hr_path
86
118
  end
87
119
 
88
- log_update "%s: calculating aspect map" % @name
89
- OS.gdaldem "aspect", dem_path, aspect_path, "-trigonometric"
120
+ low_resolution = 0.5 * @extent
121
+ OS.gdalwarp "-r", "med", "-tr", low_resolution, low_resolution, dem_hr_path, dem_lr_path
90
122
 
91
- Enumerator.new do |yielder|
92
- aspect = ESRIHdr.new aspect_path, -9999
93
- indices = [-1, 0, 1].map do |row|
94
- [-1, 0, 1].map do |col|
95
- row * aspect.ncols + col - 1
96
- end
97
- end.flatten.values_at(0,3,6,7,8,5,2,1,0)
98
-
99
- aspect.nrows.times do |i|
100
- log_update "%s: finding flat areas: %.1f%%" % [@name, 100.0 * i / aspect.nrows]
101
- aspect.ncols.times do |j|
102
- indices.map!(&:next)
103
- next if i < 1 || j < 1 || i > aspect.nrows - 2 || j > aspect.ncols - 2
104
- ring = aspect.values.values_at *indices
105
- next if ring.any?(&:nil?)
106
- anticlockwise = ring.each_cons(2).map do |a1, a2|
107
- (a2 - a1) % 360 < 180
108
- end
109
- yielder << [[j + 1, i + 1], true] if anticlockwise.all?
110
- yielder << [[j + 1, i + 1], false] if anticlockwise.none?
111
- end
112
- end
113
- end.group_by(&:last).flat_map do |knoll, group|
114
- pixels = group.map(&:first)
115
- locations = raster_locations dem_path, pixels
116
- elevations = raster_values dem_path, pixels
117
-
118
- locations.zip(elevations).map do |coordinates, elevation|
119
- GeoJSON::Point.new coordinates, "knoll" => knoll, "elevation" => elevation
120
- end.each do |feature|
123
+ mask = pixels_knolls(dem_lr_path).map(&:first).to_set
124
+ pixels, knolls = pixels_knolls(dem_hr_path) do |col, row|
125
+ !mask.include? [(col * @mm_per_px / low_resolution).floor, (row * @mm_per_px / low_resolution).floor]
126
+ end.entries.transpose
127
+
128
+ locations = raster_locations dem_hr_path, pixels
129
+ elevations = raster_values dem_hr_path, pixels
130
+
131
+ locations.zip(elevations, knolls).map do |coordinates, elevation, knoll|
132
+ GeoJSON::Point.new(coordinates).tap do |feature|
121
133
  feature.extend Candidate, ordering
134
+ feature.knoll, feature.elevation = knoll, elevation
135
+ feature["label"] = elevation.round
122
136
  end
123
137
  end
124
138
  end
125
139
  end
126
140
 
127
141
  def get_features
128
- selected, rejected, remaining = [], Set[], AVLTree.new
129
- index = RTree.load(candidates, &:bounds)
130
-
131
- log_update "%s: choosing candidates" % @name
132
- candidates.to_set.each do |candidate|
133
- buffer = NOISE_MM * @map.scale / 1000.0
134
- index.search(candidate.bounds(buffer)).each do |other|
135
- next unless candidate["knoll"] ^ other["knoll"]
136
- next if [candidate, other].map(&:coordinates).distance > buffer
137
- rejected << candidate << other
138
- end
139
- end.difference(rejected).each do |candidate|
140
- buffer = @spacing * @map.scale / 1000.0
141
- index.search(candidate.bounds(buffer)).each do |other|
142
+ selected, remaining = [], AVLTree.new
143
+ spatial_index = RTree.load(candidates, &:bounds)
144
+
145
+ candidates.each.with_index do |candidate, index|
146
+ log_update "%s: examining candidates: %.1f%%" % [@name, 100.0 * index / candidates.length]
147
+ spatial_index.search(candidate.bounds(buffer: @spacing)).each do |other|
142
148
  next if other == candidate
143
- next if rejected === other
144
- next if [candidate, other].map(&:coordinates).distance > buffer
149
+ next if [candidate, other].map(&:coordinates).distance > @spacing
145
150
  candidate.conflicts << other
146
151
  end
147
152
  end.each do |candidate|
@@ -161,11 +166,7 @@ module NSWTopo
161
166
  end
162
167
  end
163
168
 
164
- selected.each do |feature|
165
- feature.properties.replace "label" => feature["elevation"].round
166
- end.yield_self do |features|
167
- GeoJSON::Collection.new @map.projection, features
168
- end
169
+ GeoJSON::Collection.new projection: @map.projection, features: selected
169
170
  end
170
171
  end
171
172
  end
@@ -0,0 +1,15 @@
1
+ module NSWTopo
2
+ module Vector
3
+ class Cutout
4
+ def initialize(element)
5
+ @href = "#" + element.attributes["id"]
6
+ end
7
+
8
+ def use
9
+ REXML::Element.new("use").tap do |use|
10
+ use.add_attributes "href" => @href
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ module NSWTopo
2
+ module Vector
3
+ class Knockout
4
+ def initialize(element, buffer)
5
+ buffer = Config["knockout"] || 0.3 if buffer == true
6
+ @buffer = Float(buffer)
7
+ @href = "#" + element.attributes["id"]
8
+ end
9
+ attr_reader :buffer
10
+
11
+ def use
12
+ REXML::Element.new("use").tap { |use| use.add_attributes "href" => @href }
13
+ end
14
+ end
15
+ end
16
+ end