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
@@ -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