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.
- 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 +41 -22
- 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
|