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,176 @@
1
+ class Colour
2
+ Error = Class.new RuntimeError
3
+
4
+ COLOURS = YAML.load <<~YAML
5
+ aliceblue: [240, 248, 255]
6
+ antiquewhite: [250, 235, 215]
7
+ aqua: [0, 255, 255]
8
+ aquamarine: [127, 255, 212]
9
+ azure: [240, 255, 255]
10
+ beige: [245, 245, 220]
11
+ bisque: [255, 228, 196]
12
+ black: [0, 0, 0]
13
+ blanchedalmond: [255, 235, 205]
14
+ blue: [0, 0, 255]
15
+ blueviolet: [138, 43, 226]
16
+ brown: [165, 42, 42]
17
+ burlywood: [222, 184, 135]
18
+ cadetblue: [95, 158, 160]
19
+ chartreuse: [127, 255, 0]
20
+ chocolate: [210, 105, 30]
21
+ coral: [255, 127, 80]
22
+ cornflowerblue: [100, 149, 237]
23
+ cornsilk: [255, 248, 220]
24
+ crimson: [220, 20, 60]
25
+ cyan: [0, 255, 255]
26
+ darkblue: [0, 0, 139]
27
+ darkcyan: [0, 139, 139]
28
+ darkgoldenrod: [184, 134, 11]
29
+ darkgray: [169, 169, 169]
30
+ darkgreen: [0, 100, 0]
31
+ darkgrey: [169, 169, 169]
32
+ darkkhaki: [189, 183, 107]
33
+ darkmagenta: [139, 0, 139]
34
+ darkolivegreen: [85, 107, 47]
35
+ darkorange: [255, 140, 0]
36
+ darkorchid: [153, 50, 204]
37
+ darkred: [139, 0, 0]
38
+ darksalmon: [233, 150, 122]
39
+ darkseagreen: [143, 188, 143]
40
+ darkslateblue: [72, 61, 139]
41
+ darkslategray: [47, 79, 79]
42
+ darkslategrey: [47, 79, 79]
43
+ darkturquoise: [0, 206, 209]
44
+ darkviolet: [148, 0, 211]
45
+ deeppink: [255, 20, 147]
46
+ deepskyblue: [0, 191, 255]
47
+ dimgray: [105, 105, 105]
48
+ dimgrey: [105, 105, 105]
49
+ dodgerblue: [30, 144, 255]
50
+ firebrick: [178, 34, 34]
51
+ floralwhite: [255, 250, 240]
52
+ forestgreen: [34, 139, 34]
53
+ fuchsia: [255, 0, 255]
54
+ gainsboro: [220, 220, 220]
55
+ ghostwhite: [248, 248, 255]
56
+ gold: [255, 215, 0]
57
+ goldenrod: [218, 165, 32]
58
+ gray: [128, 128, 128]
59
+ grey: [128, 128, 128]
60
+ green: [0, 128, 0]
61
+ greenyellow: [173, 255, 47]
62
+ honeydew: [240, 255, 240]
63
+ hotpink: [255, 105, 180]
64
+ indianred: [205, 92, 92]
65
+ indigo: [75, 0, 130]
66
+ ivory: [255, 255, 240]
67
+ khaki: [240, 230, 140]
68
+ lavender: [230, 230, 250]
69
+ lavenderblush: [255, 240, 245]
70
+ lawngreen: [124, 252, 0]
71
+ lemonchiffon: [255, 250, 205]
72
+ lightblue: [173, 216, 230]
73
+ lightcoral: [240, 128, 128]
74
+ lightcyan: [224, 255, 255]
75
+ lightgoldenrodyellow: [250, 250, 210]
76
+ lightgray: [211, 211, 211]
77
+ lightgreen: [144, 238, 144]
78
+ lightgrey: [211, 211, 211]
79
+ lightpink: [255, 182, 193]
80
+ lightsalmon: [255, 160, 122]
81
+ lightseagreen: [32, 178, 170]
82
+ lightskyblue: [135, 206, 250]
83
+ lightslategray: [119, 136, 153]
84
+ lightslategrey: [119, 136, 153]
85
+ lightsteelblue: [176, 196, 222]
86
+ lightyellow: [255, 255, 224]
87
+ lime: [0, 255, 0]
88
+ limegreen: [50, 205, 50]
89
+ linen: [250, 240, 230]
90
+ magenta: [255, 0, 255]
91
+ maroon: [128, 0, 0]
92
+ mediumaquamarine: [102, 205, 170]
93
+ mediumblue: [0, 0, 205]
94
+ mediumorchid: [186, 85, 211]
95
+ mediumpurple: [147, 112, 219]
96
+ mediumseagreen: [60, 179, 113]
97
+ mediumslateblue: [123, 104, 238]
98
+ mediumspringgreen: [0, 250, 154]
99
+ mediumturquoise: [72, 209, 204]
100
+ mediumvioletred: [199, 21, 133]
101
+ midnightblue: [25, 25, 112]
102
+ mintcream: [245, 255, 250]
103
+ mistyrose: [255, 228, 225]
104
+ moccasin: [255, 228, 181]
105
+ navajowhite: [255, 222, 173]
106
+ navy: [0, 0, 128]
107
+ oldlace: [253, 245, 230]
108
+ olive: [128, 128, 0]
109
+ olivedrab: [107, 142, 35]
110
+ orange: [255, 165, 0]
111
+ orangered: [255, 69, 0]
112
+ orchid: [218, 112, 214]
113
+ palegoldenrod: [238, 232, 170]
114
+ palegreen: [152, 251, 152]
115
+ paleturquoise: [175, 238, 238]
116
+ palevioletred: [219, 112, 147]
117
+ papayawhip: [255, 239, 213]
118
+ peachpuff: [255, 218, 185]
119
+ peru: [205, 133, 63]
120
+ pink: [255, 192, 203]
121
+ plum: [221, 160, 221]
122
+ powderblue: [176, 224, 230]
123
+ purple: [128, 0, 128]
124
+ red: [255, 0, 0]
125
+ rosybrown: [188, 143, 143]
126
+ royalblue: [65, 105, 225]
127
+ saddlebrown: [139, 69, 19]
128
+ salmon: [250, 128, 114]
129
+ sandybrown: [244, 164, 96]
130
+ seagreen: [46, 139, 87]
131
+ seashell: [255, 245, 238]
132
+ sienna: [160, 82, 45]
133
+ silver: [192, 192, 192]
134
+ skyblue: [135, 206, 235]
135
+ slateblue: [106, 90, 205]
136
+ slategray: [112, 128, 144]
137
+ slategrey: [112, 128, 144]
138
+ snow: [255, 250, 250]
139
+ springgreen: [0, 255, 127]
140
+ steelblue: [70, 130, 180]
141
+ tan: [210, 180, 140]
142
+ teal: [0, 128, 128]
143
+ thistle: [216, 191, 216]
144
+ tomato: [255, 99, 71]
145
+ turquoise: [64, 224, 208]
146
+ violet: [238, 130, 238]
147
+ wheat: [245, 222, 179]
148
+ white: [255, 255, 255]
149
+ whitesmoke: [245, 245, 245]
150
+ yellow: [255, 255, 0]
151
+ yellowgreen: [154, 205, 50]
152
+ YAML
153
+
154
+ def initialize(string_or_array)
155
+ @triplet = case string_or_array
156
+ when Array then string_or_array.take(3).map(&:round)
157
+ when *COLOURS.keys
158
+ @name = string_or_array
159
+ COLOURS[string_or_array]
160
+ when /^#?([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})$/i
161
+ [$1, $2, $3].map { |hex| Integer("0x#{hex}") }
162
+ when /^rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)$/
163
+ [$1, $2, $3].map(&:to_i)
164
+ end
165
+ raise Error, "invalid colour: #{string_or_array}" unless @triplet&.all?(0..255)
166
+ end
167
+ attr_reader :triplet
168
+
169
+ def mix(other, fraction)
170
+ Colour.new [triplet, other.triplet].along(fraction.to_f).map(&:to_i)
171
+ end
172
+
173
+ def to_s
174
+ @name || "#%.2X%.2X%.2X" % triplet
175
+ end
176
+ end
@@ -0,0 +1,27 @@
1
+ module Concurrently
2
+ CORES = Etc.nprocessors rescue 1
3
+
4
+ def concurrently(threads = CORES, &block)
5
+ elements = Queue.new
6
+ threads.times.map do
7
+ Thread.new do
8
+ while element = elements.pop
9
+ block.call element
10
+ end
11
+ end
12
+ end.tap do
13
+ inject(elements, &:<<).close
14
+ end.each(&:join)
15
+ self
16
+ end
17
+
18
+ def concurrent_groups(threads = CORES, &block)
19
+ group_by.with_index do |item, index|
20
+ index % threads
21
+ end.values.map do |items|
22
+ Thread.new(items, &block)
23
+ end.each(&:join)
24
+ end
25
+ end
26
+
27
+ Enumerator.send :include, Concurrently
@@ -0,0 +1,7 @@
1
+ class Dir
2
+ def self.mktmppath
3
+ mktmpdir do |path|
4
+ yield Pathname.new(path)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,15 @@
1
+ module HashHelpers
2
+ def deep_merge(other)
3
+ merge(other) do |key, old_value, new_value|
4
+ Hash === old_value ? Hash === new_value ? old_value.deep_merge(new_value) : new_value : new_value
5
+ end
6
+ end
7
+
8
+ def deep_merge!(other)
9
+ merge!(other) do |key, old_value, new_value|
10
+ Hash === old_value ? Hash === new_value ? old_value.deep_merge!(new_value) : new_value : new_value
11
+ end
12
+ end
13
+ end
14
+
15
+ Hash.send :include, HashHelpers
@@ -0,0 +1,11 @@
1
+ module TarWriterHelpers
2
+ def add_entry(entry)
3
+ check_closed
4
+ @io.write entry.header
5
+ @io.write entry.read
6
+ @io.write ?\0 while @io.pos % 512 > 0
7
+ self
8
+ end
9
+ end
10
+
11
+ Gem::Package::TarWriter.send :include, TarWriterHelpers
@@ -0,0 +1,6 @@
1
+ require_relative 'helpers/hash'
2
+ require_relative 'helpers/array'
3
+ require_relative 'helpers/dir'
4
+ require_relative 'helpers/concurrently'
5
+ require_relative 'helpers/tar_writer'
6
+ require_relative 'helpers/colour'
@@ -0,0 +1,73 @@
1
+ module NSWTopo
2
+ module ArcGISRaster
3
+ include Raster, Log
4
+ CREATE = %w[url]
5
+
6
+ def get_raster(temp_dir)
7
+ raise "no resolution specified for #{@name}" unless Numeric === @resolution
8
+ txt_path = temp_dir / "mosaic.txt"
9
+ vrt_path = temp_dir / "mosaic.vrt"
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)
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"
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)
44
+
45
+ row, col = rows[1].abs, cols[0]
46
+ rel_path = "tile/#{tile_level}/#{row}/#{col}"
47
+ jpg_path = temp_dir / "#{row}.#{col}" # could be png
48
+ 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|
56
+ log_update "%s: retrieving tile %i of %i" % [@name, index + 1, tiles.length]
57
+ connection.get(rel_path, blankTile: true) do |response|
58
+ jpg_path.binwrite response.body
59
+ end
60
+ 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|
64
+ txt_path.write tif_paths.join(?\n)
65
+ OS.gdalbuildvrt "-input_file_list", txt_path, vrt_path
66
+ end
67
+
68
+ OS.gdal_translate vrt_path, Pathname.pwd / "foo.tif"
69
+
70
+ return @resolution, vrt_path
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,233 @@
1
+ module NSWTopo
2
+ module Contour
3
+ include Vector, DEM, Log
4
+ CREATE = %w[interval index smooth simplify thin density min-length no-depression knolls fill]
5
+ DEFAULTS = YAML.load <<~YAML
6
+ interval: 5
7
+ smooth: 0.2
8
+ density: 4.0
9
+ min-length: 2.0
10
+ knolls: 0.2
11
+ section: 100
12
+ stroke: "#805100"
13
+ stroke-width: 0.08
14
+ Depression:
15
+ symbolise:
16
+ interval: 2.0
17
+ line:
18
+ stroke-width: 0.12
19
+ y2: -0.3
20
+ labels:
21
+ font-size: 1.4
22
+ letter-spacing: 0.05
23
+ orientation: downhill
24
+ collate: true
25
+ min-radius: 5
26
+ max-turn: 20
27
+ sample: 10
28
+ minimum-area: 70
29
+ separation: 40
30
+ separation-all: 15
31
+ separation-along: 100
32
+ YAML
33
+
34
+ def margin
35
+ { mm: [3 * @smooth, 1].min }
36
+ end
37
+
38
+ def check_geos!
39
+ json = OS.ogr2ogr "-dialect", "SQLite", "-sql", "SELECT geos_version() AS version", "-f", "GeoJSON", "/vsistdout/", "/vsistdin/" do |stdin|
40
+ stdin.write GeoJSON::Collection.new.to_json
41
+ end
42
+ raise unless version = JSON.parse(json).dig("features", 0, "properties", "version")
43
+ raise unless (version.split(?-).first.split(?.).map(&:to_i) <=> [3, 3]) >= 0
44
+ rescue OS::Error, JSON::ParserError, RuntimeError
45
+ raise "contour thinning requires GDAL with SpatiaLite and GEOS support"
46
+ end
47
+
48
+ def get_features
49
+ @simplify ||= [0.5 * @interval / Math::tan(Math::PI * 85 / 180), 0.001 * 0.05 * @map.scale].min
50
+ @index ||= 10 * @interval
51
+ @params = {
52
+ "Index" => { "stroke-width" => 2 * @params["stroke-width"] },
53
+ "labels" => { "fill" => @fill || @params["stroke"] }
54
+ }.deep_merge(@params)
55
+
56
+ check_geos! if @thin
57
+ raise "%im index interval not a multiple of %im contour interval" % [@index, @interval] unless @index % @interval == 0
58
+
59
+ Dir.mktmppath do |temp_dir|
60
+ dem_path, blur_path = temp_dir / "dem.tif", temp_dir / "dem.blurred.tif"
61
+
62
+ if @smooth.zero?
63
+ get_dem temp_dir, blur_path
64
+ else
65
+ get_dem temp_dir, dem_path
66
+ blur_dem dem_path, blur_path
67
+ end
68
+
69
+ db_flags = @thin ? %w[-f SQLite -dsco SPATIALITE=YES] : ["-f", "ESRI Shapefile"]
70
+ db_path = temp_dir / "contour"
71
+
72
+ log_update "%s: generating contour lines" % @name
73
+ 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
75
+
76
+ if @no_depression.nil?
77
+ candidates = contours.select do |feature|
78
+ feature.coordinates.last == feature.coordinates.first &&
79
+ feature.coordinates.anticlockwise?
80
+ end.each do |feature|
81
+ feature["depression"] = 1
82
+ end
83
+ index = RTree.load(candidates, &:bounds)
84
+
85
+ contours.reject! do |feature|
86
+ next unless feature["depression"] == 1
87
+ index.search(feature.bounds).none? do |other|
88
+ next if other == feature
89
+ feature.coordinates.first.within?(other.coordinates) ||
90
+ other.coordinates.first.within?(feature.coordinates)
91
+ end
92
+ end
93
+ end
94
+
95
+ contours.reject! do |feature|
96
+ feature.coordinates.last == feature.coordinates.first &&
97
+ feature.bounds.all? { |min, max| max - min < @knolls * @map.scale / 1000.0 }
98
+ end
99
+
100
+ contours.each do |feature|
101
+ id, elevation, depression = feature.values_at "ID", "elevation", "depression"
102
+ feature.properties.replace("id" => id, "elevation" => elevation, "modulo" => elevation % @index, "depression" => depression || 0)
103
+ end
104
+
105
+ contours.reject! do |feature|
106
+ feature["elevation"].zero?
107
+ end
108
+
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
111
+ end
112
+
113
+ if @thin
114
+ slope_tif_path = temp_dir / "slope.tif"
115
+ slope_vrt_path = temp_dir / "slope.vrt"
116
+ min_length = @min_length * @map.scale / 1000.0
117
+
118
+ log_update "%s: generating slope masks" % @name
119
+ OS.gdaldem "slope", blur_path, slope_tif_path, "-compute_edges"
120
+ json = OS.gdalinfo "-json", slope_tif_path
121
+ width, height = JSON.parse(json)["size"]
122
+ srcwin = [ -2, -2, width + 4, height + 4 ]
123
+ OS.gdal_translate "-srcwin", *srcwin, "-a_nodata", "none", "-of", "VRT", slope_tif_path, slope_vrt_path
124
+
125
+ 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
137
+ mask_path = temp_dir / "mask.#{count}.sqlite"
138
+
139
+ OS.gdal_contour "-nln", "ring", "-a", "angle", "-fl", angle, *db_flags, slope_vrt_path, mask_path
140
+
141
+ OS.ogr2ogr "-update", "-nln", "mask", "-nlt", "MULTIPOLYGON", mask_path, mask_path, "-dialect", "SQLite", "-sql", <<~SQL
142
+ SELECT
143
+ ST_Buffer(ST_Buffer(ST_Polygonize(geometry), #{0.5 * min_length}, 6), #{-0.5 * min_length}, 6) AS geometry
144
+ FROM ring
145
+ SQL
146
+
147
+ drop.each do |index|
148
+ OS.ogr2ogr "-nln", "mask", "-update", "-append", "-explodecollections", "-q", db_path, mask_path, "-dialect", "SQLite", "-sql", <<~SQL
149
+ SELECT geometry, #{index * @interval} AS modulo
150
+ FROM mask
151
+ SQL
152
+ end
153
+
154
+ count - drop.count
155
+ end
156
+
157
+ log_update "%s: thinning contour lines" % @name
158
+ OS.ogr2ogr "-nln", "divided", "-update", "-explodecollections", db_path, db_path, "-dialect", "SQLite", "-sql", <<~SQL
159
+ WITH intersecting(contour, mask) AS (
160
+ SELECT contour.rowid, mask.rowid
161
+ FROM contour
162
+ INNER JOIN mask
163
+ ON
164
+ mask.modulo = contour.modulo AND
165
+ contour.rowid IN (
166
+ SELECT rowid FROM SpatialIndex
167
+ WHERE
168
+ f_table_name = 'contour' AND
169
+ search_frame = mask.geometry
170
+ ) AND
171
+ ST_Relate(contour.geometry, mask.geometry, 'T********')
172
+ )
173
+
174
+ SELECT contour.geometry, contour.id, contour.elevation, contour.modulo, contour.depression, 1 AS unmasked, 1 AS unaltered
175
+ FROM contour
176
+ LEFT JOIN intersecting ON intersecting.contour = contour.rowid
177
+ WHERE intersecting.contour IS NULL
178
+
179
+ UNION SELECT ExtractMultiLinestring(ST_Difference(contour.geometry, ST_Collect(mask.geometry))) AS geometry, contour.id, contour.elevation, contour.modulo, contour.depression, 1 AS unmasked, 0 AS unaltered
180
+ FROM contour
181
+ INNER JOIN intersecting ON intersecting.contour = contour.rowid
182
+ INNER JOIN mask ON intersecting.mask = mask.rowid
183
+ GROUP BY contour.rowid
184
+ HAVING min(ST_Relate(contour.geometry, mask.geometry, '**T******'))
185
+
186
+ UNION SELECT ExtractMultiLinestring(ST_Intersection(contour.geometry, ST_Collect(mask.geometry))) AS geometry, contour.id, contour.elevation, contour.modulo, contour.depression, 0 AS unmasked, 0 AS unaltered
187
+ FROM contour
188
+ INNER JOIN intersecting ON intersecting.contour = contour.rowid
189
+ INNER JOIN mask ON intersecting.mask = mask.rowid
190
+ GROUP BY contour.rowid
191
+ SQL
192
+
193
+ OS.ogr2ogr "-nln", "thinned", "-update", "-explodecollections", db_path, db_path, "-dialect", "SQLite", "-sql", <<~SQL
194
+ SELECT ST_LineMerge(ST_Collect(geometry)) AS geometry, id, elevation, modulo, depression, unaltered
195
+ FROM divided
196
+ WHERE unmasked OR ST_Length(geometry) < #{min_length}
197
+ GROUP BY id, elevation, modulo, unaltered
198
+ SQL
199
+
200
+ OS.ogr2ogr "-nln", "contour", "-update", "-overwrite", db_path, db_path, "-dialect", "SQLite", "-sql", <<~SQL
201
+ SELECT geometry, id, elevation, modulo, depression
202
+ FROM thinned
203
+ WHERE unaltered OR ST_Length(geometry) > #{min_length}
204
+ SQL
205
+ end
206
+
207
+ json = OS.ogr2ogr "-f", "GeoJSON", "-lco", "RFC7946=NO", "/vsistdout/", db_path, "contour"
208
+ GeoJSON::Collection.load(json, @map.projection).each do |feature|
209
+ elevation, modulo, depression = feature.values_at "elevation", "modulo", "depression"
210
+ category = modulo.zero? ? %w[Index] : %w[Standard]
211
+ category << "Depression" if depression == 1
212
+ feature.clear
213
+ feature["elevation"] = elevation
214
+ feature["category"] = category
215
+ feature["label"] = elevation.to_i.to_s if modulo.zero?
216
+ end
217
+ end
218
+ end
219
+
220
+ def to_s
221
+ elevations = features.map do |feature|
222
+ [feature["elevation"], feature["category"].include?("Index")]
223
+ end.uniq.sort_by(&:first)
224
+ range = elevations.map(&:first).minmax
225
+ interval, index = %i[itself last].map do |selector|
226
+ elevations.select(&selector).map(&:first).each_cons(2).map { |e0, e1| e1 - e0 }.min
227
+ end
228
+ [["%im intervals", interval], ["%im indices", index], ["%im-%im elevation", (range if range.all?)]].select(&:last).map do |label, value|
229
+ label % value
230
+ end.join(", ").prepend("%s: " % @name)
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,94 @@
1
+ module NSWTopo
2
+ module Control
3
+ include Vector
4
+ CREATE = %w[diameter spot font-size colour]
5
+ DEFAULTS = YAML.load <<~YAML
6
+ diameter: 7.0
7
+ colour: darkmagenta
8
+ stroke-width: 0.25
9
+ waterdrop:
10
+ stroke: blue
11
+ labels:
12
+ dupe: outline
13
+ outline:
14
+ stroke: white
15
+ fill: none
16
+ stroke-width: 0.25
17
+ stroke-opacity: 0.75
18
+ position: [ aboveright, belowright, aboveleft, belowleft, right, left, above, below ]
19
+ font-family: sans-serif
20
+ font-style: normal
21
+ stroke: none
22
+ YAML
23
+ SCALING_PARAMS = <<~YAML
24
+ fence: 2.0
25
+ control:
26
+ symbol:
27
+ - circle:
28
+ r: 1.0
29
+ fill: none
30
+ hashhouse:
31
+ symbol:
32
+ path:
33
+ d: M 0.0 -1.0 L -0.866 0.5 L 0.866 0.5 Z
34
+ fill: none
35
+ anc:
36
+ symbol:
37
+ path:
38
+ d: M 0.7071 0.7071 L -0.7071 0.7071 L -0.7071 -0.7071 L 0.7071 -0.7071 Z
39
+ fill: none
40
+ waterdrop:
41
+ symbol:
42
+ path:
43
+ d:
44
+ M 0 0 m -0.63954,0.063887 -0.0040064,0.32652 0.33453,0.034055 0,-0.38261 -0.33052,0.022034 z
45
+ M 0 0 m -0.0095612,-0.43108 0,0.11413
46
+ M 0 0 m 0.11225,-0.43108 0,0.11413
47
+ M 0 0 m -0.30901,0.04046 c 0.020447,0.0013422 0.029653,0.024004 0.049078,0.031048 0.018777,0.0053462 0.038045,0.0035119 0.05709,0.0020132 0.0063728,-0.0033554 0.042405,-0.030227 0.044414,-0.034299 0.01612,-0.013399 0.037913,-0.04687 0.037717,-0.065861 l 0.0040064,-0.10517 c 0.00020519,-0.0053688 -0.021845,-0.022347 -0.025354,-0.026427 -0.0042688,-0.0049656 0.0055968,-0.05284 -0.0026192,-0.051919 -0.0092762,-0.0035792 -0.013901,0.00044738 -0.02763,-0.0061064 -0.0033824,-0.010245 -0.0050752,-0.053478 0.0081307,-0.051037 0.022266,0.0013424 0.036219,0.0040488 0.061533,-0.0045856 0.015342,0 0.068088,-0.048747 0.083316,-0.048747 l 0.13477,0.00067107 c 0.0086143,0 0.025117,0.022723 0.029266,0.026872 0.019873,0.024221 0.046404,0.029805 0.082646,0.041671 0.0092704,0.0011184 0.041429,-0.0045408 0.044242,0.0020136 0.0063744,0.021228 0.0042104,0.0427 0.0024896,0.050818 -0.0023072,0.010894 -0.023914,-0.00015658 -0.025061,0.013936 -0.0009304,0.011453 -0.00032659,0.031954 -0.0034784,0.046116 -0.019142,0.019707 -0.042333,0.032122 -0.034371,0.040092 l -0.00052567,0.090013 c 0.0024592,0.02049 0.0088222,0.043385 0.016268,0.056554 0.020901,0.046083 0.064072,0.047163 0.06903,0.04844 0.0022744,0.00067108 0.1974,0.025825 0.30749,0.13121 0.0093678,0.00897 0.029965,0.026342 0.053084,0.097154 0.02234,0.068425 0.037538,0.2914 0.030048,0.29447 -0.06374,0.026123 -0.15033,0.053353 -0.2534,0.020043 -0.01286,-0.0041608 0.0013624,-0.088139 -0.0070112,-0.1282 -0.0085866,-0.041085 0.0002729,-0.12091 -0.08864,-0.13672 -0.037049,-0.0065768 -0.15685,0.014316 -0.24088,0.03205 -0.040012,0.0084555 -0.094156,0.013712 -0.12269,0.0080082 -0.017636,-0.0035344 -0.12587,-0.027022 -0.1458,-0.032006 -0.013224,-0.0033104 -0.042701,-0.0050104 -0.087246,0.0088806 -0.033064,0.010312 -0.050813,0.035506 -0.049812,0.026722 0.00073147,-0.00642 0.0012504,-0.371 -8.5944e-05,-0.37768 z
48
+ M 0 0 m 0.050734,-0.6446 c -0.020293,-0.00022369 -0.052848,0.029116 -0.09202,0.057281 -0.051683,0.032021 -0.11317,0.0094174 -0.16568,-0.00774 -0.049831,-0.019774 -0.12933,-0.0033552 -0.13608,0.058974 0.0013488,0.036319 0.014608,0.092861 0.058748,0.095185 0.074174,-0.010044 0.15057,-0.043112 0.22528,-0.018924 0.029827,0.013175 0.035194,0.033585 0.037678,0.033102 l 0.14636,0 c 0.002484,0.00044738 0.0078512,-0.019931 0.037678,-0.033102 0.074708,-0.024188 0.1511,0.0088806 0.22528,0.018924 0.044141,-0.0023264 0.057457,-0.058867 0.058805,-0.095185 -0.006752,-0.062332 -0.086307,-0.078753 -0.13614,-0.058974 -0.052507,0.017157 -0.114,0.039761 -0.16568,0.00774 -0.039878,-0.028673 -0.072888,-0.058556 -0.093094,-0.057225 -0.00036462,-2.4608e-05 -0.00072.451,-5.1456e-05 -0.0011296,-5.5928e-05 z
49
+ M 0 0 m -0.16679,-0.26528 c 0.23655,0.036829 0.43804,0.013466 0.43804,0.013466
50
+ M 0 0 m -0.17245,-0.2153 c 0.23655,0.036826 0.44371,0.017694 0.44371,0.017694
51
+ fill: none
52
+ labels:
53
+ margin: 1.4142
54
+ font-size: 1.5
55
+ YAML
56
+
57
+ def get_features
58
+ scaled_params = SCALING_PARAMS.gsub(/\-?\d\.\d+/) { |number| "%.5g" % (number.to_f * 0.5 * @diameter) }
59
+ scaled_params = YAML.load scaled_params
60
+ scaled_params["control"]["symbol"] << { "circle" => { "r" => 0.07, "stroke-width" => 0.14, "fill" => "none" } } if @spot
61
+ @params = scaled_params.deep_merge @params
62
+ @params["labels"]["font-size"] = @font_size if @font_size
63
+ @params["labels"]["fill"] = @params["stroke"] = @colour.to_s if @colour
64
+ points, controls = GPS.load(@path).points, GeoJSON::Collection.new
65
+ [["control", /^(1?\d\d)W?$/ ],
66
+ ["hashhouse", /^(HH)$/ ],
67
+ ["anc", /^(ANC)$/ ],
68
+ ["waterdrop", /^1?\d\dW$|^W$/],
69
+ ].each do |type, selector|
70
+ points.each do |point|
71
+ name = point["name"]
72
+ next unless name =~ selector
73
+ properties = [["category", [type, *$1]], ["label", $1]].select(&:last).to_h
74
+ controls.add_point point.coordinates, properties
75
+ end
76
+ end
77
+ controls
78
+ end
79
+
80
+ def to_s
81
+ counts = %w[control waterdrop hashhouse].map do |category|
82
+ waypoints = features.select do |feature|
83
+ feature["category"].any? category
84
+ end
85
+ next if waypoints.empty?
86
+ count = "%i %s%s" % [waypoints.length, category, waypoints.one? ? nil : ?s]
87
+ next count unless "control" == category
88
+ total = features.sum { |feature| feature["label"].to_i.floor(-1) }
89
+ count << " (%i points)" % total
90
+ end.compact
91
+ [@name, counts.join(", ")].join(": ")
92
+ end
93
+ end
94
+ end