nswtopo 2.0.0 → 3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/COPYING +70 -83
- data/bin/nswtopo +227 -116
- data/docs/README.md +1 -12
- data/docs/add.md +1 -1
- data/docs/config.md +1 -1
- data/docs/contours.md +3 -1
- data/docs/init.md +8 -0
- data/docs/inspect.md +103 -0
- data/docs/move.md +9 -0
- data/docs/render.md +16 -7
- data/docs/scrape.md +67 -0
- data/docs/spot-heights.md +6 -2
- data/lib/nswtopo/archive.rb +50 -41
- data/lib/nswtopo/chrome.rb +227 -0
- data/lib/nswtopo/commands/add.rb +106 -0
- data/lib/nswtopo/commands/config.rb +38 -0
- data/lib/nswtopo/commands/inspect.rb +74 -0
- data/lib/nswtopo/commands/layers.rb +22 -0
- data/lib/nswtopo/commands/scrape.rb +79 -0
- data/lib/nswtopo/commands.rb +57 -0
- data/lib/nswtopo/dither.rb +5 -3
- data/lib/nswtopo/font.rb +46 -21
- data/lib/nswtopo/formats/gemf.rb +42 -0
- data/lib/nswtopo/formats/kmz.rb +26 -24
- data/lib/nswtopo/formats/mbtiles.rb +5 -41
- data/lib/nswtopo/formats/pdf.rb +82 -17
- data/lib/nswtopo/formats/svg.rb +114 -45
- data/lib/nswtopo/formats/svgz.rb +2 -2
- data/lib/nswtopo/formats/zip.rb +33 -23
- data/lib/nswtopo/formats.rb +77 -32
- data/lib/nswtopo/geometry/overlap.rb +1 -32
- data/lib/nswtopo/geometry/r_tree.rb +16 -10
- data/lib/nswtopo/geometry/segment.rb +3 -3
- data/lib/nswtopo/geometry/straight_skeleton/nodes.rb +5 -6
- data/lib/nswtopo/geometry/vector_sequence.rb +7 -6
- data/lib/nswtopo/gis/arcgis/connection.rb +56 -0
- data/lib/nswtopo/gis/arcgis/layer/map.rb +163 -0
- data/lib/nswtopo/gis/arcgis/layer/query.rb +87 -0
- data/lib/nswtopo/gis/arcgis/layer/renderer.rb +66 -0
- data/lib/nswtopo/gis/arcgis/layer/statistics.rb +15 -0
- data/lib/nswtopo/gis/arcgis/layer.rb +201 -0
- data/lib/nswtopo/gis/arcgis/service.rb +57 -0
- data/lib/nswtopo/gis/arcgis.rb +3 -0
- data/lib/nswtopo/gis/dem.rb +13 -12
- data/lib/nswtopo/gis/esri_hdr.rb +8 -2
- data/lib/nswtopo/gis/geojson/collection.rb +45 -21
- data/lib/nswtopo/gis/geojson/multi_line_string.rb +2 -24
- data/lib/nswtopo/gis/geojson/multi_polygon.rb +2 -53
- data/lib/nswtopo/gis/geojson/polygon.rb +15 -0
- data/lib/nswtopo/gis/geojson.rb +12 -3
- data/lib/nswtopo/gis/gps/kml.rb +25 -19
- data/lib/nswtopo/gis/gps.rb +2 -0
- data/lib/nswtopo/gis/projection.rb +35 -24
- data/lib/nswtopo/gis/shapefile.rb +89 -16
- data/lib/nswtopo/gis.rb +1 -2
- data/lib/nswtopo/helpers/array.rb +0 -11
- data/lib/nswtopo/helpers/colour.rb +34 -14
- data/lib/nswtopo/layer/arcgis_raster.rb +44 -48
- data/lib/nswtopo/layer/colour_mask.rb +5 -0
- data/lib/nswtopo/layer/contour.rb +35 -28
- data/lib/nswtopo/layer/control.rb +2 -7
- data/lib/nswtopo/layer/declination.rb +9 -9
- data/lib/nswtopo/layer/feature.rb +36 -22
- data/lib/nswtopo/layer/grid.rb +30 -27
- data/lib/nswtopo/layer/import.rb +1 -21
- data/lib/nswtopo/layer/labels/barrier.rb +39 -0
- data/lib/nswtopo/layer/labels.rb +551 -383
- data/lib/nswtopo/layer/mask_render.rb +37 -0
- data/lib/nswtopo/layer/overlay.rb +2 -2
- data/lib/nswtopo/layer/raster.rb +31 -41
- data/lib/nswtopo/layer/raster_import.rb +17 -0
- data/lib/nswtopo/layer/raster_render.rb +15 -0
- data/lib/nswtopo/layer/relief.rb +27 -95
- data/lib/nswtopo/layer/spot.rb +63 -62
- data/lib/nswtopo/layer/vector/cutout.rb +15 -0
- data/lib/nswtopo/layer/vector/knockout.rb +16 -0
- data/lib/nswtopo/layer/vector.rb +121 -89
- data/lib/nswtopo/layer/vegetation.rb +39 -34
- data/lib/nswtopo/layer.rb +30 -16
- data/lib/nswtopo/map.rb +202 -109
- data/lib/nswtopo/os.rb +5 -27
- data/lib/nswtopo/tiled_web_map.rb +54 -0
- data/lib/nswtopo/tree_indenter.rb +27 -0
- data/lib/nswtopo/version.rb +27 -2
- data/lib/nswtopo.rb +6 -199
- metadata +39 -20
- data/lib/nswtopo/font/chrome.rb +0 -59
- data/lib/nswtopo/font/generic.rb +0 -25
- data/lib/nswtopo/gis/arcgis_server/connection.rb +0 -52
- data/lib/nswtopo/gis/arcgis_server.rb +0 -155
- data/lib/nswtopo/gis/geojson/multi_point.rb +0 -12
- data/lib/nswtopo/gis/world_file.rb +0 -19
- 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
|
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
|
|
data/lib/nswtopo/layer/raster.rb
CHANGED
@@ -1,21 +1,14 @@
|
|
1
1
|
module NSWTopo
|
2
2
|
module Raster
|
3
3
|
def create
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
31
|
-
|
32
|
-
|
33
|
-
|
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
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
data/lib/nswtopo/layer/relief.rb
CHANGED
@@ -1,16 +1,14 @@
|
|
1
1
|
module NSWTopo
|
2
2
|
module Relief
|
3
|
-
include Raster,
|
4
|
-
CREATE = %w[
|
3
|
+
include Raster, MaskRender, DEM, Log
|
4
|
+
CREATE = %w[method azimuth factor smooth contours]
|
5
5
|
DEFAULTS = YAML.load <<~YAML
|
6
|
-
|
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
|
-
|
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 =
|
30
|
-
|
31
|
-
outsize = (bounds.transpose.
|
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
|
36
|
-
attribute = 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
|
39
|
-
|
40
|
-
|
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
|
-
|
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", *
|
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
|
-
|
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
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
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
|
-
|
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
|
data/lib/nswtopo/layer/spot.rb
CHANGED
@@ -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, -
|
45
|
+
def ordinal; [conflicts.size, -elevation] end
|
44
46
|
end
|
45
47
|
|
46
48
|
module PreferSaddles
|
47
|
-
def ordinal; [conflicts.size,
|
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
|
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
|
-
|
79
|
-
|
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,
|
114
|
+
get_dem temp_dir, dem_hr_path
|
83
115
|
else
|
84
116
|
get_dem temp_dir, raw_path
|
85
|
-
blur_dem raw_path,
|
117
|
+
blur_dem raw_path, dem_hr_path
|
86
118
|
end
|
87
119
|
|
88
|
-
|
89
|
-
OS.
|
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
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
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,
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
buffer
|
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
|
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
|
-
|
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,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
|