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
data/lib/nswtopo/gis/gps/kml.rb
CHANGED
|
@@ -3,7 +3,9 @@ module NSWTopo
|
|
|
3
3
|
module KML
|
|
4
4
|
def styles(placemark)
|
|
5
5
|
style_url = placemark.elements["styleUrl"]&.text&.delete_prefix(?#)
|
|
6
|
-
|
|
6
|
+
return {} unless style_url
|
|
7
|
+
|
|
8
|
+
style_element = @xml.elements["/kml/Document/*[@id='%s']" % style_url]
|
|
7
9
|
result = {}
|
|
8
10
|
|
|
9
11
|
case style_element&.name
|
|
@@ -39,25 +41,29 @@ module NSWTopo
|
|
|
39
41
|
|
|
40
42
|
def collection
|
|
41
43
|
GeoJSON::Collection.new.tap do |collection|
|
|
42
|
-
@xml.elements.each "/kml//Placemark
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
44
|
+
@xml.elements.each "/kml//Placemark" do |placemark|
|
|
45
|
+
%w[. MultiGeometry].each do |container|
|
|
46
|
+
placemark.elements.each "#{container}/Point/coordinates" do |coordinates|
|
|
47
|
+
coords = coordinates.text.split(',').take(2).map(&:to_f)
|
|
48
|
+
collection.add_point coords, properties(placemark).merge("styles" => {})
|
|
49
|
+
end
|
|
50
|
+
placemark.elements.each "#{container}/LineString/coordinates" do |coordinates|
|
|
51
|
+
coords = coordinates.text.split(' ').map { |triplet| triplet.split(',')[0..1].map(&:to_f) }
|
|
52
|
+
collection.add_linestring coords, properties(placemark).merge("styles" => styles(placemark))
|
|
53
|
+
end
|
|
54
|
+
placemark.elements.each "#{container}/gx:Track[gx:coord]" do |track|
|
|
55
|
+
coords = track.collect("gx:coord") { |coord| coord.text.split(?\s).take(2).map(&:to_f) }
|
|
56
|
+
collection.add_linestring coords, properties(placemark).merge("styles" => styles(placemark))
|
|
57
|
+
end
|
|
58
|
+
placemark.elements.each "#{container}/Polygon[outerBoundaryIs/LinearRing/coordinates]" do |polygon|
|
|
59
|
+
coords = [polygon.elements["outerBoundaryIs/LinearRing/coordinates"].text]
|
|
60
|
+
coords += polygon.elements.collect("innerBoundaryIs/LinearRing/coordinates", &:text)
|
|
61
|
+
coords.map! do |text|
|
|
62
|
+
text.split(' ').map { |triplet| triplet.split(?,).take(2).map(&:to_f) }
|
|
63
|
+
end
|
|
64
|
+
collection.add_polygon coords, properties(placemark).merge("styles" => styles(placemark))
|
|
65
|
+
end
|
|
59
66
|
end
|
|
60
|
-
collection.add_polygon coords, properties(placemark).merge("styles" => styles(placemark))
|
|
61
67
|
end
|
|
62
68
|
end
|
|
63
69
|
end
|
data/lib/nswtopo/gis/gps.rb
CHANGED
|
@@ -1,56 +1,67 @@
|
|
|
1
1
|
module NSWTopo
|
|
2
2
|
class Projection
|
|
3
|
-
def initialize(
|
|
4
|
-
@
|
|
5
|
-
raise "no georeferencing found: %s" %
|
|
3
|
+
def initialize(value)
|
|
4
|
+
@wkt2 = Projection === value ? value.wkt2 : OS.gdalsrsinfo("-o", "wkt2", "--single-line", value).chomp.strip
|
|
5
|
+
raise "no georeferencing found: %s" % value if @wkt2.empty?
|
|
6
6
|
end
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
attr_reader :proj4
|
|
15
|
-
alias to_s proj4
|
|
16
|
-
alias to_str proj4
|
|
8
|
+
attr_reader :wkt2
|
|
9
|
+
alias to_s wkt2
|
|
10
|
+
alias to_str wkt2
|
|
17
11
|
|
|
18
12
|
def ==(other)
|
|
19
|
-
|
|
13
|
+
wkt2 == other.wkt2
|
|
20
14
|
end
|
|
21
15
|
|
|
22
16
|
extend Forwardable
|
|
23
|
-
delegate :hash => :@
|
|
17
|
+
delegate :hash => :@wkt2
|
|
24
18
|
alias eql? ==
|
|
25
19
|
|
|
26
|
-
def
|
|
27
|
-
|
|
20
|
+
def metres?
|
|
21
|
+
OS.gdalsrsinfo("-o", "proj4", "--single-line", @wkt2).chomp.split.any?("+units=m")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.utm(zone, south: true)
|
|
25
|
+
new("EPSG:32%1d%02d" % [south ? 7 : 6, zone])
|
|
28
26
|
end
|
|
29
27
|
|
|
30
28
|
def self.wgs84
|
|
31
|
-
new("
|
|
29
|
+
new("EPSG:4326")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.from(**params)
|
|
33
|
+
params.map do |key, value|
|
|
34
|
+
"+#{key}=#{value}"
|
|
35
|
+
end.then do |args|
|
|
36
|
+
new args.join(?\s)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.transverse_mercator(lon_0, lat_0, **params)
|
|
41
|
+
from proj: "tmerc", datum: "WGS84", lon_0: lon_0, lat_0: lat_0, **params
|
|
32
42
|
end
|
|
33
43
|
|
|
34
|
-
def self.
|
|
35
|
-
|
|
44
|
+
def self.oblique_mercator(lonc, lat_0, alpha:, **params)
|
|
45
|
+
from proj: "omerc", datum: "WGS84", lonc: lonc, lat_0: lat_0, gamma: 0, alpha: alpha, **params
|
|
36
46
|
end
|
|
37
47
|
|
|
38
|
-
def self.azimuthal_equidistant(
|
|
39
|
-
|
|
48
|
+
def self.azimuthal_equidistant(lon_0, lat_0)
|
|
49
|
+
from proj: "aeqd", datum: "WGS84", lon_0: lon_0, lat_0: lat_0
|
|
40
50
|
end
|
|
41
51
|
|
|
42
52
|
def self.utm_zones(collection)
|
|
43
53
|
collection.reproject_to_wgs84.bounds.first.map do |longitude|
|
|
44
54
|
(longitude / 6).floor + 31
|
|
45
|
-
end.
|
|
55
|
+
end.then do |min, max|
|
|
46
56
|
min..max
|
|
47
57
|
end
|
|
48
58
|
end
|
|
49
59
|
|
|
50
|
-
def self.
|
|
60
|
+
def self.utm_geometry(zone)
|
|
51
61
|
longitudes = [31, 30].map { |offset| (zone - offset) * 6.0 }
|
|
52
62
|
latitudes = [-80.0, 84.0]
|
|
53
|
-
longitudes.product(latitudes).values_at(0,2,3,1)
|
|
63
|
+
ring = longitudes.product(latitudes).values_at(0,2,3,1,0)
|
|
64
|
+
GeoJSON.polygon [ring], projection: Projection.wgs84
|
|
54
65
|
end
|
|
55
66
|
end
|
|
56
67
|
end
|
|
@@ -1,24 +1,97 @@
|
|
|
1
1
|
module NSWTopo
|
|
2
2
|
module Shapefile
|
|
3
|
-
|
|
3
|
+
class Source
|
|
4
|
+
def self.===(path)
|
|
5
|
+
OS.ogrinfo "-ro", "-so", path
|
|
6
|
+
true
|
|
7
|
+
rescue OS::Error
|
|
8
|
+
false
|
|
9
|
+
end
|
|
4
10
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
def initialize(path)
|
|
12
|
+
@path = path
|
|
13
|
+
end
|
|
14
|
+
attr_accessor :path
|
|
15
|
+
|
|
16
|
+
def layer(**options)
|
|
17
|
+
Layer.new self, **options
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def only_layer
|
|
21
|
+
name, *others = OS.ogrinfo("-ro", "-so", @path).scan(/^\w*\d+: (.*?)(?: \([\w\s]+\))?$/).flatten
|
|
22
|
+
return nil if others.any?
|
|
23
|
+
return name if name
|
|
24
|
+
File.basename(@path, File.extname(@path)).tap do |name|
|
|
25
|
+
OS.ogrinfo "-ro", "-so", @path, name
|
|
26
|
+
end
|
|
27
|
+
rescue OS::Error
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def layer_info
|
|
31
|
+
OS.ogrinfo("-ro", "-so", @path).scan(/^\w*\d+: (.*?)(?: \(([\w\s]+)\))?$/).sort_by(&:first).map do |name, geom_type|
|
|
32
|
+
geom_type ? "#{name} (#{geom_type.delete(?\s)})" : name
|
|
33
|
+
end
|
|
34
|
+
end
|
|
10
35
|
end
|
|
11
36
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
37
|
+
class Layer
|
|
38
|
+
NoLayerError = Class.new RuntimeError
|
|
39
|
+
|
|
40
|
+
def initialize(source, layer: nil, where: nil, fields: nil, sql: nil, geometry: nil, projection: nil)
|
|
41
|
+
@source, @layer, @where, @fields, @sql, @geometry, @projection = source, layer, where, fields, sql, geometry, projection
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def features
|
|
45
|
+
raise "can't specify both SQL and where clause" if @sql && @where
|
|
46
|
+
raise "can't specify both SQL and layer name" if @sql && @layer
|
|
47
|
+
raise "no layer name or SQL specified" unless @layer || @sql
|
|
48
|
+
sql = ["-sql", sql] if @sql
|
|
49
|
+
where = ["-where", "(" << Array(@where).join(") AND (") << ")"] if @where
|
|
50
|
+
srs = ["-t_srs", @projection] if @projection
|
|
51
|
+
spat = ["-spat", *@geometry.bounds.transpose.flatten, "-spat_srs", @geometry.projection] if @geometry
|
|
52
|
+
misc = %w[-mapFieldType Date=Integer,DateTime=Integer -dim XY]
|
|
53
|
+
json = OS.ogr2ogr *(sql || where), *srs, *spat, *misc, *%w[-f GeoJSON -lco RFC7946=NO /vsistdout/], @source.path, *@layer
|
|
54
|
+
GeoJSON::Collection.load json, **{projection: @projection}.compact
|
|
55
|
+
rescue OS::Error => error
|
|
56
|
+
raise unless /Couldn't fetch requested layer (.*)!/ === error.message
|
|
57
|
+
raise "no such layer: #{$1}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def counts
|
|
61
|
+
raise NoLayerError, "no layer name provided" unless @layer
|
|
62
|
+
count = ?_ * @fields.map(&:size).max + "count"
|
|
63
|
+
where = %Q[WHERE (%s)] % [*@where].join(") AND (") if @where
|
|
64
|
+
field_list = %Q["%s"] % @fields.join('", "')
|
|
65
|
+
sql = <<~SQL % [field_list, count, @layer, where, field_list]
|
|
66
|
+
SELECT %s, count(*) AS "%s"
|
|
67
|
+
FROM "%s"
|
|
68
|
+
%s
|
|
69
|
+
GROUP BY %s
|
|
70
|
+
SQL
|
|
71
|
+
json = OS.ogr2ogr *%w[-f GeoJSON -lco RFC7946=NO -dialect sqlite -sql], sql, "/vsistdout/", @source.path
|
|
72
|
+
JSON.parse(json)["features"].map do |feature|
|
|
73
|
+
feature["properties"]
|
|
74
|
+
end.map do |properties|
|
|
75
|
+
[properties.slice(*@fields), properties[count]]
|
|
76
|
+
end
|
|
77
|
+
rescue OS::Error => error
|
|
78
|
+
raise unless /no such column: (.*)$/ === error.message
|
|
79
|
+
raise "invalid field: #{$1}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def info
|
|
83
|
+
raise NoLayerError, "no layer name provided" unless @layer
|
|
84
|
+
info = OS.ogrinfo *%w[-ro -so -noextent], @source.path, @layer
|
|
85
|
+
geom_type = info.match(/^Geometry: (.*)$/)&.[](1)&.delete(?\s)
|
|
86
|
+
count = info.match(/^Feature Count: (\d+)$/)&.[](1)
|
|
87
|
+
fields = info.scan(/^(.*): (.*?) \(\d+\.\d+\)$/).to_h
|
|
88
|
+
wkt = info.each_line.slice_after(/^Layer SRS WKT:/).drop(1).first&.slice_before(/^\S/)&.first&.join
|
|
89
|
+
epsg = OS.gdalsrsinfo("-o", "epsg", wkt)[/\d+/] if wkt and !wkt["unknown"]
|
|
90
|
+
{ name: @layer, geometry: geom_type, EPSG: epsg, features: count, fields: (fields unless fields.empty?) }.compact
|
|
91
|
+
rescue OS::Error => error
|
|
92
|
+
raise unless /Couldn't fetch requested layer (.*)!/ === error.message
|
|
93
|
+
raise "no such layer: #{$1}"
|
|
94
|
+
end
|
|
22
95
|
end
|
|
23
96
|
end
|
|
24
97
|
end
|
data/lib/nswtopo/gis.rb
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
require_relative 'gis/projection'
|
|
2
2
|
require_relative 'gis/geojson'
|
|
3
3
|
require_relative 'gis/gps'
|
|
4
|
-
require_relative 'gis/world_file'
|
|
5
4
|
require_relative 'gis/esri_hdr'
|
|
6
|
-
require_relative 'gis/
|
|
5
|
+
require_relative 'gis/arcgis'
|
|
7
6
|
require_relative 'gis/shapefile'
|
|
8
7
|
require_relative 'gis/gdal_glob'
|
|
9
8
|
require_relative 'gis/dem'
|
|
@@ -14,17 +14,6 @@ module ArrayHelpers
|
|
|
14
14
|
def in_two
|
|
15
15
|
each_slice(1 + [length - 1, 0].max / 2)
|
|
16
16
|
end
|
|
17
|
-
|
|
18
|
-
def nearby_pairs(closed = false, &block)
|
|
19
|
-
Enumerator.new do |yielder|
|
|
20
|
-
each.with_index do |element1, index|
|
|
21
|
-
(closed ? rotate(index) : drop(index)).drop(1).each do |element2|
|
|
22
|
-
break unless block.call [element1, element2]
|
|
23
|
-
yielder << [element1, element2]
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
17
|
end
|
|
29
18
|
|
|
30
19
|
Array.send :include, ArrayHelpers
|
|
@@ -151,26 +151,46 @@ class Colour
|
|
|
151
151
|
yellowgreen: [154, 205, 50]
|
|
152
152
|
YAML
|
|
153
153
|
|
|
154
|
-
def initialize(
|
|
155
|
-
@
|
|
156
|
-
|
|
154
|
+
def initialize(value)
|
|
155
|
+
@string = value
|
|
156
|
+
@triplet = case value
|
|
157
|
+
when Colour
|
|
158
|
+
@string = value.string.dup
|
|
159
|
+
value.triplet.dup
|
|
160
|
+
when Array
|
|
161
|
+
value.take(3).map(&:round)
|
|
157
162
|
when *COLOURS.keys
|
|
158
|
-
|
|
159
|
-
COLOURS[string_or_array]
|
|
163
|
+
COLOURS[value]
|
|
160
164
|
when /^#?([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})$/i
|
|
161
165
|
[$1, $2, $3].map { |hex| Integer("0x#{hex}") }
|
|
162
|
-
when /^rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)$/
|
|
166
|
+
when /^rgb\((\d{1,3}), *(\d{1,3}), *(\d{1,3})\)$/
|
|
163
167
|
[$1, $2, $3].map(&:to_i)
|
|
168
|
+
when /^hsl\((\d{1,3}), *(\d{1,3})%, *(\d{1,3})%\)$/
|
|
169
|
+
h, s, l = [$1, $2, $3].map(&:to_i)
|
|
170
|
+
h %= 360;
|
|
171
|
+
c = (100 - (2 * l - 100).abs) * s / 10000.0
|
|
172
|
+
x = (60 - (h % 120 - 60).abs) * c / 60.0
|
|
173
|
+
m = (l - 50 * c) / 100.0
|
|
174
|
+
r, g, b = case
|
|
175
|
+
when s == 0 then [0, 0, 0]
|
|
176
|
+
when h < 60 then [c, x, 0]
|
|
177
|
+
when h < 120 then [x, c, 0]
|
|
178
|
+
when h < 180 then [0, c, x]
|
|
179
|
+
when h < 240 then [0, x, c]
|
|
180
|
+
when h < 300 then [x, 0, c]
|
|
181
|
+
when h < 360 then [c, 0, x]
|
|
182
|
+
end.map do |v|
|
|
183
|
+
255 * (v + m)
|
|
184
|
+
end.map(&:to_i)
|
|
164
185
|
end
|
|
165
|
-
raise Error, "invalid colour: #{
|
|
186
|
+
raise Error, "invalid colour: #{value}" unless @triplet&.all?(0..255)
|
|
187
|
+
@string = "rgb(%i,%i,%i)" % @triplet unless String === @string
|
|
188
|
+
@string.tr! ?\s, ""
|
|
166
189
|
end
|
|
167
|
-
attr_reader :triplet
|
|
168
190
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
end
|
|
191
|
+
attr_reader :triplet, :string
|
|
192
|
+
alias to_s string
|
|
172
193
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
end
|
|
194
|
+
extend Forwardable
|
|
195
|
+
delegate :[] => :@triplet
|
|
176
196
|
end
|
|
@@ -1,73 +1,69 @@
|
|
|
1
1
|
module NSWTopo
|
|
2
2
|
module ArcGISRaster
|
|
3
|
-
include Raster, Log
|
|
3
|
+
include Raster, RasterRender, Log
|
|
4
4
|
CREATE = %w[url]
|
|
5
5
|
|
|
6
6
|
def get_raster(temp_dir)
|
|
7
|
-
raise "no resolution specified for #{@name}" unless Numeric === @
|
|
7
|
+
raise "no resolution specified for #{@name}" unless Numeric === @mm_per_px
|
|
8
8
|
txt_path = temp_dir / "mosaic.txt"
|
|
9
9
|
vrt_path = temp_dir / "mosaic.vrt"
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
service = ArcGIS::Service.new @url
|
|
12
|
+
local_bbox = @map.cutline.bbox
|
|
13
|
+
target_bbox = local_bbox.reproject_to service.projection
|
|
14
|
+
target_resolution = @mm_per_px * Math::sqrt(target_bbox.first.area / local_bbox.first.area)
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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)
|
|
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"
|
|
44
28
|
|
|
29
|
+
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).inject(GeoJSON::Collection.new(projection: service.projection)) do |tiles, (cols, rows)|
|
|
36
|
+
[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.then do |bounds|
|
|
41
|
+
ring = bounds.inject(&:product).values_at(0,2,3,1,0)
|
|
42
|
+
ullr = bounds.inject(&:product).values_at(1,2).flatten
|
|
45
43
|
row, col = rows[1].abs, cols[0]
|
|
44
|
+
tiles.add_polygon [ring], ullr: ullr, row: row, col: col
|
|
45
|
+
end
|
|
46
|
+
end.clip(target_bbox.first).then do |tiles|
|
|
47
|
+
tiles.map.with_index do |feature, index|
|
|
48
|
+
row, col, ullr = feature.values_at("row", "col", "ullr")
|
|
46
49
|
rel_path = "tile/#{tile_level}/#{row}/#{col}"
|
|
47
50
|
jpg_path = temp_dir / "#{row}.#{col}" # could be png
|
|
48
51
|
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|
|
|
52
|
+
gdal_args = ["-a_srs", service.projection, "-a_ullr", *ullr, "-of", "GTiff", jpg_path, tif_path]
|
|
56
53
|
log_update "%s: retrieving tile %i of %i" % [@name, index + 1, tiles.length]
|
|
57
|
-
|
|
54
|
+
service.get(rel_path, blankTile: true) do |response|
|
|
58
55
|
jpg_path.binwrite response.body
|
|
59
56
|
end
|
|
57
|
+
OS.gdal_translate *gdal_args
|
|
58
|
+
tif_path
|
|
60
59
|
end
|
|
61
|
-
end.
|
|
62
|
-
|
|
63
|
-
end.map(&:last).tap do |tif_paths|
|
|
60
|
+
end.tap do |tif_paths|
|
|
61
|
+
log_update "%s: mosaicing %s tiles" % [@name, tif_paths.length] if tif_paths.length > 1
|
|
64
62
|
txt_path.write tif_paths.join(?\n)
|
|
65
|
-
OS.gdalbuildvrt "-input_file_list", txt_path, vrt_path
|
|
66
63
|
end
|
|
67
64
|
|
|
68
|
-
OS.
|
|
69
|
-
|
|
70
|
-
return @resolution, vrt_path
|
|
65
|
+
OS.gdalbuildvrt "-input_file_list", txt_path, vrt_path
|
|
66
|
+
return vrt_path
|
|
71
67
|
end
|
|
72
68
|
end
|
|
73
69
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module NSWTopo
|
|
2
2
|
module Contour
|
|
3
3
|
include Vector, DEM, Log
|
|
4
|
-
CREATE = %w[interval index smooth simplify thin density min-length no-depression knolls fill]
|
|
4
|
+
CREATE = %w[interval index auxiliary smooth simplify thin density min-length no-depression knolls fill]
|
|
5
5
|
DEFAULTS = YAML.load <<~YAML
|
|
6
6
|
interval: 5
|
|
7
7
|
smooth: 0.2
|
|
@@ -9,8 +9,11 @@ module NSWTopo
|
|
|
9
9
|
min-length: 2.0
|
|
10
10
|
knolls: 0.2
|
|
11
11
|
section: 100
|
|
12
|
-
stroke:
|
|
12
|
+
stroke: hsl(40,100%,25%)
|
|
13
13
|
stroke-width: 0.08
|
|
14
|
+
Auxiliary:
|
|
15
|
+
stroke-dasharray: 0.5 0.5
|
|
16
|
+
stroke-dashoffset: 0.5
|
|
14
17
|
Depression:
|
|
15
18
|
symbolise:
|
|
16
19
|
interval: 2.0
|
|
@@ -26,9 +29,10 @@ module NSWTopo
|
|
|
26
29
|
max-turn: 20
|
|
27
30
|
sample: 10
|
|
28
31
|
minimum-area: 70
|
|
29
|
-
separation:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
separation:
|
|
33
|
+
self: 40
|
|
34
|
+
other: 15
|
|
35
|
+
along: 100
|
|
32
36
|
YAML
|
|
33
37
|
|
|
34
38
|
def margin
|
|
@@ -36,7 +40,7 @@ module NSWTopo
|
|
|
36
40
|
end
|
|
37
41
|
|
|
38
42
|
def check_geos!
|
|
39
|
-
json = OS.ogr2ogr "-dialect", "SQLite", "-sql", "SELECT geos_version() AS version", "-f", "GeoJSON", "/vsistdout/", "/vsistdin/" do |stdin|
|
|
43
|
+
json = OS.ogr2ogr "-dialect", "SQLite", "-sql", "SELECT geos_version() AS version", "-f", "GeoJSON", "-lco", "RFC7946=NO", "/vsistdout/", "/vsistdin/" do |stdin|
|
|
40
44
|
stdin.write GeoJSON::Collection.new.to_json
|
|
41
45
|
end
|
|
42
46
|
raise unless version = JSON.parse(json).dig("features", 0, "properties", "version")
|
|
@@ -46,7 +50,7 @@ module NSWTopo
|
|
|
46
50
|
end
|
|
47
51
|
|
|
48
52
|
def get_features
|
|
49
|
-
@simplify ||= [0.5 * @interval / Math::tan(Math::PI * 85 / 180), 0.
|
|
53
|
+
@simplify ||= [@map.to_mm(0.5 * @interval) / Math::tan(Math::PI * 85 / 180), 0.05].min
|
|
50
54
|
@index ||= 10 * @interval
|
|
51
55
|
@params = {
|
|
52
56
|
"Index" => { "stroke-width" => 2 * @params["stroke-width"] },
|
|
@@ -71,7 +75,7 @@ module NSWTopo
|
|
|
71
75
|
|
|
72
76
|
log_update "%s: generating contour lines" % @name
|
|
73
77
|
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
|
|
78
|
+
contours = GeoJSON::Collection.load json, projection: @map.projection
|
|
75
79
|
|
|
76
80
|
if @no_depression.nil?
|
|
77
81
|
candidates = contours.select do |feature|
|
|
@@ -94,7 +98,7 @@ module NSWTopo
|
|
|
94
98
|
|
|
95
99
|
contours.reject! do |feature|
|
|
96
100
|
feature.coordinates.last == feature.coordinates.first &&
|
|
97
|
-
feature.bounds.all? { |min, max| max - min < @knolls
|
|
101
|
+
feature.bounds.all? { |min, max| max - min < @knolls }
|
|
98
102
|
end
|
|
99
103
|
|
|
100
104
|
contours.each do |feature|
|
|
@@ -106,14 +110,16 @@ module NSWTopo
|
|
|
106
110
|
feature["elevation"].zero?
|
|
107
111
|
end
|
|
108
112
|
|
|
109
|
-
|
|
110
|
-
|
|
113
|
+
contours.each_slice(100).inject(nil) do |update, features|
|
|
114
|
+
OS.ogr2ogr "-a_srs", @map.projection, "-nln", "contour", *update, "-simplify", @simplify, *db_flags, db_path, "GeoJSON:/vsistdin/" do |stdin|
|
|
115
|
+
stdin.write GeoJSON::Collection.new(projection: @map.projection, features: features).to_json
|
|
116
|
+
end
|
|
117
|
+
%w[-update -append]
|
|
111
118
|
end
|
|
112
119
|
|
|
113
120
|
if @thin
|
|
114
121
|
slope_tif_path = temp_dir / "slope.tif"
|
|
115
122
|
slope_vrt_path = temp_dir / "slope.vrt"
|
|
116
|
-
min_length = @min_length * @map.scale / 1000.0
|
|
117
123
|
|
|
118
124
|
log_update "%s: generating slope masks" % @name
|
|
119
125
|
OS.gdaldem "slope", blur_path, slope_tif_path, "-compute_edges"
|
|
@@ -123,24 +129,21 @@ module NSWTopo
|
|
|
123
129
|
OS.gdal_translate "-srcwin", *srcwin, "-a_nodata", "none", "-of", "VRT", slope_tif_path, slope_vrt_path
|
|
124
130
|
|
|
125
131
|
multiplier = @index / @interval
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
132
|
+
Enumerator.new do |yielder|
|
|
133
|
+
keep = 0...multiplier
|
|
134
|
+
until keep.one?
|
|
135
|
+
keep, drop = keep.count.even? ? keep.each_slice(2).entries.transpose : [[0], keep.drop(1)]
|
|
136
|
+
yielder << drop
|
|
137
|
+
end
|
|
138
|
+
end.inject(multiplier) do |count, drop|
|
|
139
|
+
angle = Math::atan(@index * @density / count) * 180.0 / Math::PI
|
|
137
140
|
mask_path = temp_dir / "mask.#{count}.sqlite"
|
|
138
141
|
|
|
139
142
|
OS.gdal_contour "-nln", "ring", "-a", "angle", "-fl", angle, *db_flags, slope_vrt_path, mask_path
|
|
140
143
|
|
|
141
144
|
OS.ogr2ogr "-update", "-nln", "mask", "-nlt", "MULTIPOLYGON", mask_path, mask_path, "-dialect", "SQLite", "-sql", <<~SQL
|
|
142
145
|
SELECT
|
|
143
|
-
ST_Buffer(ST_Buffer(ST_Polygonize(geometry), #{0.5 * min_length}, 6), #{-0.5 * min_length}, 6) AS geometry
|
|
146
|
+
ST_Buffer(ST_Buffer(ST_Polygonize(geometry), #{0.5 * @min_length}, 6), #{-0.5 * @min_length}, 6) AS geometry
|
|
144
147
|
FROM ring
|
|
145
148
|
SQL
|
|
146
149
|
|
|
@@ -193,21 +196,25 @@ module NSWTopo
|
|
|
193
196
|
OS.ogr2ogr "-nln", "thinned", "-update", "-explodecollections", db_path, db_path, "-dialect", "SQLite", "-sql", <<~SQL
|
|
194
197
|
SELECT ST_LineMerge(ST_Collect(geometry)) AS geometry, id, elevation, modulo, depression, unaltered
|
|
195
198
|
FROM divided
|
|
196
|
-
WHERE unmasked OR ST_Length(geometry) < #{min_length}
|
|
199
|
+
WHERE unmasked OR ST_Length(geometry) < #{@min_length}
|
|
197
200
|
GROUP BY id, elevation, modulo, unaltered
|
|
198
201
|
SQL
|
|
199
202
|
|
|
200
203
|
OS.ogr2ogr "-nln", "contour", "-update", "-overwrite", db_path, db_path, "-dialect", "SQLite", "-sql", <<~SQL
|
|
201
204
|
SELECT geometry, id, elevation, modulo, depression
|
|
202
205
|
FROM thinned
|
|
203
|
-
WHERE unaltered OR ST_Length(geometry) > #{min_length}
|
|
206
|
+
WHERE unaltered OR ST_Length(geometry) > #{@min_length}
|
|
204
207
|
SQL
|
|
205
208
|
end
|
|
206
209
|
|
|
207
210
|
json = OS.ogr2ogr "-f", "GeoJSON", "-lco", "RFC7946=NO", "/vsistdout/", db_path, "contour"
|
|
208
|
-
GeoJSON::Collection.load(json, @map.projection).each do |feature|
|
|
211
|
+
GeoJSON::Collection.load(json, projection: @map.projection).each do |feature|
|
|
209
212
|
elevation, modulo, depression = feature.values_at "elevation", "modulo", "depression"
|
|
210
|
-
category =
|
|
213
|
+
category = case
|
|
214
|
+
when @auxiliary && elevation % (2 * @interval) != 0 then %w[Auxiliary]
|
|
215
|
+
when modulo.zero? then %w[Index]
|
|
216
|
+
else %w[Standard]
|
|
217
|
+
end
|
|
211
218
|
category << "Depression" if depression == 1
|
|
212
219
|
feature.clear
|
|
213
220
|
feature["elevation"] = elevation
|