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,106 @@
|
|
|
1
|
+
module NSWTopo
|
|
2
|
+
def add(archive, *layers, after: nil, before: nil, replace: nil, overwrite: false, strict: false, **options)
|
|
3
|
+
create_options = {
|
|
4
|
+
after: Layer.sanitise(after),
|
|
5
|
+
before: Layer.sanitise(before),
|
|
6
|
+
replace: Layer.sanitise(replace),
|
|
7
|
+
overwrite: overwrite,
|
|
8
|
+
strict: strict
|
|
9
|
+
}
|
|
10
|
+
map = Map.load archive
|
|
11
|
+
|
|
12
|
+
Enumerator.new do |yielder|
|
|
13
|
+
while layers.any?
|
|
14
|
+
layer, basedir = layers.shift
|
|
15
|
+
path = Pathname(layer).expand_path(*basedir)
|
|
16
|
+
case layer
|
|
17
|
+
when /^controls\.(gpx|kml)$/i
|
|
18
|
+
yielder << [path.basename(path.extname).to_s, "type" => "Control", "path" => path]
|
|
19
|
+
when /\.(gpx|kml)$/i
|
|
20
|
+
yielder << [path.basename(path.extname).to_s, "type" => "Overlay", "path" => path]
|
|
21
|
+
when /\.(tiff?|png|jpg)$/i
|
|
22
|
+
yielder << [path.basename(path.extname).to_s, "type" => "Import", "path" => path]
|
|
23
|
+
when "contours"
|
|
24
|
+
yielder << [layer, "type" => "Contour"]
|
|
25
|
+
when "spot-heights"
|
|
26
|
+
yielder << [layer, "type" => "Spot"]
|
|
27
|
+
when "relief"
|
|
28
|
+
yielder << [layer, "type" => "Relief"]
|
|
29
|
+
when "grid"
|
|
30
|
+
yielder << [layer, "type" => "Grid"]
|
|
31
|
+
when "declination"
|
|
32
|
+
yielder << [layer, "type" => "Declination"]
|
|
33
|
+
when "controls"
|
|
34
|
+
yielder << [layer, "type" => "Control"]
|
|
35
|
+
when /\.yml$/i
|
|
36
|
+
basedir ||= path.parent
|
|
37
|
+
raise "couldn't find '#{layer}'" unless path.file?
|
|
38
|
+
case contents = YAML.load(path.read)
|
|
39
|
+
when Array
|
|
40
|
+
contents.reverse.map do |item|
|
|
41
|
+
Pathname(item.to_s)
|
|
42
|
+
end.each do |relative_path|
|
|
43
|
+
raise "#{relative_path} is not a relative path" unless relative_path.relative?
|
|
44
|
+
layers.prepend [Pathname(relative_path).expand_path(path.parent).relative_path_from(basedir).to_s, basedir]
|
|
45
|
+
end
|
|
46
|
+
when Hash
|
|
47
|
+
name = path.sub_ext("").relative_path_from(basedir).descend.map(&:basename).join(?.)
|
|
48
|
+
yielder << [name, contents.merge("source" => path)]
|
|
49
|
+
else
|
|
50
|
+
raise "couldn't parse #{path}"
|
|
51
|
+
end
|
|
52
|
+
else
|
|
53
|
+
path = Pathname("#{layer}.yml")
|
|
54
|
+
raise "#{layer} is not a relative path" unless path.relative?
|
|
55
|
+
basedir ||= layer_dirs.find do |root|
|
|
56
|
+
path.expand_path(root).file?
|
|
57
|
+
end
|
|
58
|
+
layers.prepend [path.to_s, basedir]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
rescue YAML::Exception
|
|
62
|
+
raise "couldn't parse #{path}"
|
|
63
|
+
end.map do |name, params|
|
|
64
|
+
params.merge! options.transform_keys(&:to_s)
|
|
65
|
+
params.merge! Config[name] if Config[name]
|
|
66
|
+
Layer.new(name, map, params)
|
|
67
|
+
end.tap do |layers|
|
|
68
|
+
raise OptionParser::MissingArgument, "no layers specified" unless layers.any?
|
|
69
|
+
unless layers.one?
|
|
70
|
+
raise OptionParser::InvalidArgument, "can't specify opacity when adding multiple layers" if options[:opacity]
|
|
71
|
+
raise OptionParser::InvalidArgument, "can't specify data path when adding multiple layers" if options[:path]
|
|
72
|
+
end
|
|
73
|
+
map.add *layers, **create_options
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def contours(archive, dem_path, **options)
|
|
78
|
+
add archive, "contours", **options, path: Pathname(dem_path)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def spot_heights(archive, dem_path, **options)
|
|
82
|
+
add archive, "spot-heights", **options, path: Pathname(dem_path)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def relief(archive, dem_path, **options)
|
|
86
|
+
add archive, "relief", **options, path: Pathname(dem_path)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def grid(archive, **options)
|
|
90
|
+
add archive, "grid", **options
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def declination(archive, **options)
|
|
94
|
+
add archive, "declination", **options
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def controls(archive, gps_path, **options)
|
|
98
|
+
raise OptionParser::InvalidArgument, gps_path unless gps_path =~ /\.(gpx|kml)$/i
|
|
99
|
+
add archive, "controls", **options, path: Pathname(gps_path)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def overlay(archive, gps_path, **options)
|
|
103
|
+
raise OptionParser::InvalidArgument, gps_path unless gps_path =~ /\.(gpx|kml)$/i
|
|
104
|
+
add archive, gps_path, **options, path: Pathname(gps_path)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module NSWTopo
|
|
2
|
+
def config(layer = nil, **options)
|
|
3
|
+
path, resolution = options[:path], options[:resolution]
|
|
4
|
+
layer = Layer.sanitise layer
|
|
5
|
+
|
|
6
|
+
case
|
|
7
|
+
when !layer
|
|
8
|
+
raise OptionParser::InvalidArgument, "no layer name specified for path" if path
|
|
9
|
+
raise OptionParser::InvalidArgument, "no layer name specified for resolution" if resolution
|
|
10
|
+
when path || resolution
|
|
11
|
+
Config.store layer, "path", path.to_s if path
|
|
12
|
+
Config.store layer, "resolution", resolution if resolution
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
options.each do |key, value|
|
|
16
|
+
case key
|
|
17
|
+
when :chrome
|
|
18
|
+
raise "chrome path is not an executable" unless value.executable? && !value.directory?
|
|
19
|
+
Config.store key.to_s, value.to_s
|
|
20
|
+
when :"layer-dir"
|
|
21
|
+
raise "not a directory: %s" % value unless value.directory?
|
|
22
|
+
Config.store key.to_s, value.to_s
|
|
23
|
+
when *%i[labelling debug gpu versioning zlib-level knockout]
|
|
24
|
+
Config.store key.to_s, value
|
|
25
|
+
when :delete
|
|
26
|
+
Config.delete *layer, value
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
if options.empty?
|
|
31
|
+
puts Config.to_str.each_line.drop(1)
|
|
32
|
+
log_neutral "no configuration yet" if Config.empty?
|
|
33
|
+
else
|
|
34
|
+
Config.save
|
|
35
|
+
log_success "configuration updated"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
module NSWTopo
|
|
2
|
+
def inspect(url_or_path, layer: nil, coords: nil, codes: nil, countwise: nil, **options)
|
|
3
|
+
options[:geometry] = GeoJSON.multipoint(coords).bbox if coords
|
|
4
|
+
|
|
5
|
+
case url_or_path
|
|
6
|
+
when ArcGIS::Service
|
|
7
|
+
source = ArcGIS::Service.new(url_or_path)
|
|
8
|
+
when Shapefile::Source
|
|
9
|
+
raise OptionParser::InvalidOption, "--id only applies to ArcGIS layers" if options[:id]
|
|
10
|
+
raise OptionParser::InvalidOption, "--decode only applies to ArcGIS layers" if options[:decode]
|
|
11
|
+
raise OptionParser::InvalidOption, "--codes only applies to ArcGIS layers" if codes
|
|
12
|
+
source = Shapefile::Source.new(url_or_path)
|
|
13
|
+
layer ||= source.only_layer
|
|
14
|
+
else
|
|
15
|
+
raise OptionParser::InvalidArgument, url_or_path
|
|
16
|
+
end
|
|
17
|
+
layer = source.layer(layer: layer, **options)
|
|
18
|
+
|
|
19
|
+
case
|
|
20
|
+
when codes
|
|
21
|
+
TreeIndenter.new(layer.codes) do |level|
|
|
22
|
+
level.map do |key, values|
|
|
23
|
+
case key
|
|
24
|
+
when Array
|
|
25
|
+
code, value = key
|
|
26
|
+
display_value = value.nil? || /[\t\n\r]/ === value ? value.inspect : value
|
|
27
|
+
["#{code} → #{display_value}", values]
|
|
28
|
+
else
|
|
29
|
+
["#{key}:", values]
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end.each do |indents, info|
|
|
33
|
+
puts indents.join << info
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
when fields = options[:fields]
|
|
37
|
+
template = "%%%is │ %%%is │ %%s"
|
|
38
|
+
TreeIndenter.new(layer.counts) do |counts|
|
|
39
|
+
counts.group_by do |attributes, count|
|
|
40
|
+
attributes.shift
|
|
41
|
+
end.entries.select(&:first).map do |(name, value), counts|
|
|
42
|
+
[[name, counts.sum(&:last), value], counts]
|
|
43
|
+
end.sort do |((name1, count1, value1), counts1), ((name2, count2, value2), counts2)|
|
|
44
|
+
next count2 <=> count1 if countwise
|
|
45
|
+
value1 && value2 ? value1 <=> value2 : value1 ? 1 : value2 ? -1 : 0
|
|
46
|
+
end
|
|
47
|
+
end.map do |indents, (name, count, value)|
|
|
48
|
+
next name, count.to_s, indents.join << (value.nil? || /[\t\n\r]/ === value ? value.inspect : value.to_s)
|
|
49
|
+
end.transpose.tap do |names, counts, lines|
|
|
50
|
+
template %= [names.map(&:size).max, counts.map(&:size).max] if names
|
|
51
|
+
end.transpose.each do |row|
|
|
52
|
+
puts template % row
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
else
|
|
56
|
+
TreeIndenter.new(layer.info) do |hash|
|
|
57
|
+
hash.map do |key, value|
|
|
58
|
+
Hash === value ? ["#{key}:", value] : "#{key}: #{value}"
|
|
59
|
+
end
|
|
60
|
+
end.each do |indents, info|
|
|
61
|
+
puts indents.join << info
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
rescue ArcGIS::Layer::NoLayerError, Shapefile::Layer::NoLayerError => error
|
|
66
|
+
raise OptionParser::MissingArgument, error.message if codes || countwise || options.any?
|
|
67
|
+
puts "layers:"
|
|
68
|
+
TreeIndenter.new(source.layer_info, []).each do |indents, info|
|
|
69
|
+
puts indents.join << info
|
|
70
|
+
end
|
|
71
|
+
rescue ArcGIS::Renderer::TooManyFieldsError
|
|
72
|
+
raise OptionParser::InvalidOption, "use less fields with --fields"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module NSWTopo
|
|
2
|
+
def layers(state: nil)
|
|
3
|
+
paths = layer_dirs.grep_v(Pathname.pwd).flat_map do |directory|
|
|
4
|
+
Array(state).inject(directory, &:/).glob("*")
|
|
5
|
+
end.sort
|
|
6
|
+
log_warn "no layers installed" if paths.none?
|
|
7
|
+
|
|
8
|
+
TreeIndenter.new(paths) do |paths|
|
|
9
|
+
paths.map do |path|
|
|
10
|
+
case
|
|
11
|
+
when path.glob("**/*.yml").any?
|
|
12
|
+
[path.basename.sub_ext(""), path.children.sort]
|
|
13
|
+
when path.sub_ext("").directory?
|
|
14
|
+
when path.extname == ".yml"
|
|
15
|
+
path.basename.sub_ext("")
|
|
16
|
+
end
|
|
17
|
+
end.compact
|
|
18
|
+
end.each do |indents, name|
|
|
19
|
+
puts [*indents, name].join
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
module NSWTopo
|
|
2
|
+
def scrape(url, path, coords: nil, name: nil, epsg: nil, paginate: nil, concat: nil, **options)
|
|
3
|
+
flags = %w[-skipfailures]
|
|
4
|
+
flags += %W[-t_srs epsg:#{epsg}] if epsg
|
|
5
|
+
flags += %W[-nln #{name}] if name
|
|
6
|
+
|
|
7
|
+
format_flags = case path.to_s
|
|
8
|
+
when Shapefile::Source then %w[-update -overwrite]
|
|
9
|
+
when /\.sqlite3?$/ then %w[-f SQLite -dsco SPATIALITE=YES]
|
|
10
|
+
when /\.db$/ then %w[-f SQLite -dsco SPATIALITE=YES]
|
|
11
|
+
when /\.gpkg$/ then %w[-f GPKG]
|
|
12
|
+
when /\.tab$/ then ["-f", "MapInfo File"]
|
|
13
|
+
else ["-f", "ESRI Shapefile", "-lco", "ENCODING=UTF-8"]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
options.merge! case path.to_s
|
|
17
|
+
when /\.sqlite3?$/ then { mixed: concat, launder: true }
|
|
18
|
+
when /\.db$/ then { mixed: concat, launder: true }
|
|
19
|
+
when /\.gpkg$/ then { mixed: concat, launder: true }
|
|
20
|
+
when /\.tab$/ then { }
|
|
21
|
+
else { truncate: 10 }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
options[:geometry] = GeoJSON.multipoint(coords).bbox if coords
|
|
25
|
+
|
|
26
|
+
log_update "nswtopo: contacting server"
|
|
27
|
+
layer = ArcGIS::Service.new(url).layer(**options)
|
|
28
|
+
|
|
29
|
+
queue = Queue.new
|
|
30
|
+
thread = Thread.new do
|
|
31
|
+
while page = queue.pop
|
|
32
|
+
*, status = Open3.capture3 *%W[ogr2ogr #{path} /vsistdin/], *flags, *format_flags, stdin_data: page.to_json
|
|
33
|
+
format_flags = %w[-update -append]
|
|
34
|
+
queue.close unless status.success?
|
|
35
|
+
end
|
|
36
|
+
status
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
total_features, percent = "%i feature%s", "%%.%if%%%%"
|
|
40
|
+
Enumerator.new do |yielder|
|
|
41
|
+
hold, ok, count = [], nil, 0
|
|
42
|
+
layer.paged(per_page: paginate).tap do
|
|
43
|
+
total_features %= [layer.count, (?s unless layer.count == 1)]
|
|
44
|
+
percent %= layer.count < 1000 ? 0 : layer.count < 10000 ? 1 : 2
|
|
45
|
+
log_update "nswtopo: retrieving #{total_features}"
|
|
46
|
+
end.each do |page|
|
|
47
|
+
log_update "nswtopo: retrieving #{percent} of #{total_features}" % [100.0 * (count += page.count) / layer.count]
|
|
48
|
+
next hold << page if concat
|
|
49
|
+
next yielder << page if ok
|
|
50
|
+
next hold << page if page.all? do |feature|
|
|
51
|
+
feature.properties.values.any?(&:nil?)
|
|
52
|
+
end
|
|
53
|
+
yielder << page
|
|
54
|
+
ok = true
|
|
55
|
+
end
|
|
56
|
+
next hold.inject(yielder, &:<<) if ok && !concat
|
|
57
|
+
next yielder << hold.inject(&:merge!) if hold.any?
|
|
58
|
+
end.inject(queue) do |queue, page|
|
|
59
|
+
queue << page
|
|
60
|
+
rescue ClosedQueueError
|
|
61
|
+
break queue
|
|
62
|
+
end.close
|
|
63
|
+
|
|
64
|
+
log_update "nswtop: saving #{total_features}"
|
|
65
|
+
raise "error while saving features" unless thread.value&.success?
|
|
66
|
+
log_success "saved #{total_features}"
|
|
67
|
+
|
|
68
|
+
rescue ArcGIS::Layer::NoLayerError
|
|
69
|
+
raise OptionParser::InvalidArgument, "specify an ArcGIS layer in URL or with --layer"
|
|
70
|
+
rescue ArcGIS::Map::NoUniqueFieldError
|
|
71
|
+
raise OptionParser::InvalidOption, "--unique required for this layer"
|
|
72
|
+
rescue ArcGIS::Renderer::NoGeometryError
|
|
73
|
+
raise OptionParser::InvalidOption, "--coords not available for this layer"
|
|
74
|
+
rescue ArcGIS::Query::UniqueFieldError
|
|
75
|
+
raise OptionParser::InvalidOption, "--unique not available for this layer"
|
|
76
|
+
rescue ArcGIS::Service::InvalidURLError
|
|
77
|
+
raise OptionParser::InvalidArgument, url
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
require_relative 'commands/add'
|
|
2
|
+
require_relative 'commands/layers'
|
|
3
|
+
require_relative 'commands/config'
|
|
4
|
+
require_relative 'commands/scrape'
|
|
5
|
+
require_relative 'commands/inspect'
|
|
6
|
+
|
|
7
|
+
module NSWTopo
|
|
8
|
+
def init(archive, **options)
|
|
9
|
+
puts Map.init(archive, **options)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def info(archive, **options)
|
|
13
|
+
raise OptionParser::InvalidArgument, "one output option only" if options.slice(:json, :proj).length > 1
|
|
14
|
+
puts Map.load(archive).info(**options)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def delete(archive, *names, **options)
|
|
18
|
+
map = Map.load archive
|
|
19
|
+
names.map do |name|
|
|
20
|
+
Layer.sanitise name
|
|
21
|
+
end.uniq.map do |name|
|
|
22
|
+
name[?*] ? %r[^#{name.gsub(?., '\.').gsub(?*, '.*')}$] : name
|
|
23
|
+
end.tap do |names|
|
|
24
|
+
map.delete *names
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def move(archive, name, **options)
|
|
29
|
+
raise OptionParser::InvalidArgument, "only one of --before and --after allowed" if options[:after] && options[:before]
|
|
30
|
+
raise OptionParser::MissingArgument, "--before or --after required" unless options[:after] || options[:before]
|
|
31
|
+
Map.load(archive).move(name, **options)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def render(archive, basename, *formats, overwrite: false, svg_path: nil, **options)
|
|
35
|
+
case
|
|
36
|
+
when formats.any?
|
|
37
|
+
when svg_path
|
|
38
|
+
raise OptionParser::MissingArgument, "no output format specified"
|
|
39
|
+
else
|
|
40
|
+
formats << "svg"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
formats.map do |format|
|
|
44
|
+
Pathname(Formats === format ? "#{basename}.#{format}" : format)
|
|
45
|
+
end.uniq.each do |path|
|
|
46
|
+
format = path.extname.delete_prefix(?.)
|
|
47
|
+
raise "unrecognised format: #{path}" if format.empty?
|
|
48
|
+
raise "unrecognised format: #{format}" unless Formats === format
|
|
49
|
+
raise "already a directory: #{path}" if path.directory?
|
|
50
|
+
raise "file already exists: #{path}" if path.exist? && !overwrite
|
|
51
|
+
raise "no such directory: #{path.parent}" unless path.parent.directory?
|
|
52
|
+
end.tap do |paths|
|
|
53
|
+
map = svg_path ? Map.from_svg(archive, svg_path) : Map.load(archive)
|
|
54
|
+
map.render *paths, **options
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/nswtopo/dither.rb
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
module NSWTopo
|
|
2
2
|
module Dither
|
|
3
|
+
Missing = Class.new RuntimeError
|
|
4
|
+
|
|
3
5
|
def dither(*png_paths)
|
|
4
6
|
Enumerator.new do |yielder|
|
|
5
|
-
yielder << -> { OS.pngquant
|
|
7
|
+
yielder << -> { OS.pngquant *%w[--quiet --force --ext .png --speed 1 --nofs], *png_paths }
|
|
6
8
|
gimp_script = <<~EOF
|
|
7
9
|
(map
|
|
8
10
|
(lambda (path)
|
|
@@ -19,8 +21,8 @@ module NSWTopo
|
|
|
19
21
|
)
|
|
20
22
|
EOF
|
|
21
23
|
yielder << -> { OS.gimp "-c", "-d", "-f", "-i", "-b", gimp_script, "-b", "(gimp-quit TRUE)" }
|
|
22
|
-
yielder << -> { OS.mogrify
|
|
23
|
-
raise "pngquant, GIMP or ImageMagick required for dithering"
|
|
24
|
+
yielder << -> { OS.magick *%w[mogrify -type PaletteBilevelAlpha -dither Riemersma -colors 256], *png_paths }
|
|
25
|
+
raise Missing, "pngquant, GIMP or ImageMagick required for dithering"
|
|
24
26
|
end.each do |dither|
|
|
25
27
|
dither.call
|
|
26
28
|
break
|
data/lib/nswtopo/font.rb
CHANGED
|
@@ -1,29 +1,54 @@
|
|
|
1
|
-
require_relative 'font/generic'
|
|
2
|
-
require_relative 'font/chrome'
|
|
3
|
-
|
|
4
1
|
module NSWTopo
|
|
5
2
|
module Font
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
module Chrome
|
|
4
|
+
include Log
|
|
5
|
+
ATTRIBUTES = %w[font-family font-variant font-style font-weight font-size letter-spacing word-spacing]
|
|
6
|
+
SVG = <<~XML
|
|
7
|
+
<?xml version='1.0' encoding='UTF-8'?>
|
|
8
|
+
<svg xmlns='http://www.w3.org/2000/svg' width='1mm' height='1mm' viewBox='0 0 1 1' text-rendering='geometricPrecision'>
|
|
9
|
+
<rect width='1' height='1' stroke='none' />
|
|
10
|
+
<text>placeholder</text>
|
|
11
|
+
</svg>
|
|
12
|
+
XML
|
|
8
13
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
when !chrome_path
|
|
15
|
-
log_warn "chrome browser not configured - using generic font measurements"
|
|
16
|
-
self.extend Generic
|
|
17
|
-
else
|
|
18
|
-
begin
|
|
19
|
-
stdout, stderr, status = Open3.capture3 chrome_path, "--version"
|
|
20
|
-
raise unless status.success?
|
|
21
|
-
self.extend Chrome
|
|
22
|
-
rescue Errno::ENOENT, RuntimeError
|
|
23
|
-
log_warn "couldn't run chrome - using generic font measurements"
|
|
24
|
-
self.extend Generic
|
|
14
|
+
def start_chrome
|
|
15
|
+
@families = Set[]
|
|
16
|
+
NSWTopo::Chrome.new("data:image/svg+xml;base64,#{Base64.encode64 SVG}").tap do |browser|
|
|
17
|
+
@scale = browser.query_selector("rect").width
|
|
18
|
+
@text = browser.query_selector "text"
|
|
25
19
|
end
|
|
26
20
|
end
|
|
21
|
+
|
|
22
|
+
def self.extended(instance)
|
|
23
|
+
instance.start_chrome
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def validate(attributes)
|
|
27
|
+
return unless family = attributes["font-family"]
|
|
28
|
+
return unless @families.add? family
|
|
29
|
+
@text.value = "abcdefghijklmnopqrstuvwxyz"
|
|
30
|
+
@text[:style] = "font-family:#{family}"
|
|
31
|
+
styled_width = @text.width
|
|
32
|
+
@text[:style] = nil
|
|
33
|
+
unstyled_width = @text.width
|
|
34
|
+
log_neutral "font '#{family}' doesn't appear to be available" if styled_width == unstyled_width
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def glyph_length(string, attributes)
|
|
38
|
+
validate attributes
|
|
39
|
+
style = attributes.slice(*ATTRIBUTES).map do |pair|
|
|
40
|
+
pair.join ?:
|
|
41
|
+
end.join(?;)
|
|
42
|
+
@text[:style] = style
|
|
43
|
+
@text.value = string
|
|
44
|
+
@text.width / @scale
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
extend self
|
|
49
|
+
|
|
50
|
+
def glyph_length(*args)
|
|
51
|
+
self.extend Chrome
|
|
27
52
|
glyph_length *args
|
|
28
53
|
end
|
|
29
54
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module NSWTopo
|
|
2
|
+
module Formats
|
|
3
|
+
def render_gemf(gemf_path, name:, **options, &block)
|
|
4
|
+
Dir.mktmppath do |temp_dir|
|
|
5
|
+
ranges = tiled_web_map(temp_dir, **options, extension: "gemf", &block).sort_by do |tile|
|
|
6
|
+
[tile.col, tile.row]
|
|
7
|
+
end.group_by(&:zoom)
|
|
8
|
+
|
|
9
|
+
header, source = "", "nswtopo"
|
|
10
|
+
# 3.1 overall header:
|
|
11
|
+
header << [4, 256].pack("L>L>")
|
|
12
|
+
# 3.2 sources:
|
|
13
|
+
header << [1, 0, source.bytesize, source].pack("L>L>L>a#{source.bytesize}")
|
|
14
|
+
# 3.3 number of ranges:
|
|
15
|
+
header << [ranges.length].pack("L>")
|
|
16
|
+
|
|
17
|
+
offset = header.bytesize + ranges.size * 32
|
|
18
|
+
paths = ranges.each do |zoom, tiles|
|
|
19
|
+
cols = tiles.map(&:col)
|
|
20
|
+
rows = tiles.map(&:row)
|
|
21
|
+
# 3.3 range data:
|
|
22
|
+
header << [zoom, *cols.minmax, *rows.minmax, 0, offset].pack("L>L>L>L>L>L>Q>")
|
|
23
|
+
offset += tiles.size * 12
|
|
24
|
+
end.each do |zoom, tiles|
|
|
25
|
+
# 3.4 range details:
|
|
26
|
+
tiles.each do |tile|
|
|
27
|
+
header << [offset, tile.path.size].pack("Q>L>")
|
|
28
|
+
offset += tile.path.size
|
|
29
|
+
end
|
|
30
|
+
end.values.flatten.map(&:path)
|
|
31
|
+
|
|
32
|
+
gemf_path.open("wb") do |file|
|
|
33
|
+
file.write header
|
|
34
|
+
# 4 data area:
|
|
35
|
+
paths.each do |path|
|
|
36
|
+
file.write path.binread
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
data/lib/nswtopo/formats/kmz.rb
CHANGED
|
@@ -3,7 +3,7 @@ module NSWTopo
|
|
|
3
3
|
module Kmz
|
|
4
4
|
TILE_SIZE = 512
|
|
5
5
|
EARTH_RADIUS = 6378137.0
|
|
6
|
-
TILT = 40 * Math::PI / 180.0
|
|
6
|
+
TILT = 0 # 40 * Math::PI / 180.0
|
|
7
7
|
FOV = 25 * Math::PI / 180.0
|
|
8
8
|
extend self
|
|
9
9
|
|
|
@@ -49,40 +49,39 @@ module NSWTopo
|
|
|
49
49
|
metre_resolution = 0.0254 * @scale / ppi
|
|
50
50
|
degree_resolution = 180.0 * metre_resolution / Math::PI / Kmz::EARTH_RADIUS
|
|
51
51
|
|
|
52
|
-
wgs84_bounds = bounds
|
|
53
|
-
wgs84_dimensions = wgs84_bounds.transpose.
|
|
52
|
+
wgs84_bounds = @cutline.reproject_to_wgs84.bounds
|
|
53
|
+
wgs84_dimensions = wgs84_bounds.transpose.diff / degree_resolution
|
|
54
|
+
|
|
54
55
|
max_zoom = Math::log2(wgs84_dimensions.max).ceil - Math::log2(Kmz::TILE_SIZE).to_i
|
|
55
|
-
topleft = [wgs84_bounds[0][0], wgs84_bounds[1][1]]
|
|
56
56
|
png_path = yield(ppi: ppi)
|
|
57
57
|
|
|
58
58
|
Dir.mktmppath do |temp_dir|
|
|
59
59
|
pyramid = (0..max_zoom).map do |zoom|
|
|
60
60
|
resolution = degree_resolution * 2**(max_zoom - zoom)
|
|
61
61
|
degrees_per_tile = resolution * Kmz::TILE_SIZE
|
|
62
|
-
counts = (wgs84_bounds.transpose.difference / degrees_per_tile).map(&:ceil)
|
|
63
|
-
dimensions = counts.times Kmz::TILE_SIZE
|
|
64
62
|
|
|
65
|
-
tfw_path = temp_dir / "#{name}.kmz.zoom.#{zoom}.tfw"
|
|
66
63
|
tif_path = temp_dir / "#{name}.kmz.zoom.#{zoom}.tif"
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
64
|
+
OS.gdalwarp "-t_srs", "EPSG:4326", "-tr", resolution, resolution, "-r", "bilinear", "-dstalpha", png_path, tif_path
|
|
65
|
+
|
|
66
|
+
corners = JSON.parse(OS.gdalinfo "-json", tif_path)["cornerCoordinates"]
|
|
67
|
+
top_left = corners["upperLeft"]
|
|
68
|
+
counts = corners.values.transpose.map(&:minmax).map do |min, max|
|
|
69
|
+
(max - min) / degrees_per_tile
|
|
70
|
+
end.map(&:ceil)
|
|
70
71
|
|
|
71
|
-
indices_bounds = [
|
|
72
|
+
indices_bounds = [top_left, counts, %i[+ -]].transpose.map do |coord, count, increment|
|
|
72
73
|
boundaries = (0..count).map { |index| coord.send increment, index * degrees_per_tile }
|
|
73
74
|
[boundaries[0..-2], boundaries[1..-1]].transpose.map(&:sort)
|
|
74
75
|
end.map do |tile_bounds|
|
|
75
|
-
tile_bounds.each.with_index.
|
|
76
|
-
end.inject(:product).map(&:transpose).map
|
|
77
|
-
{ indices => tile_bounds }
|
|
78
|
-
end.inject({}, &:merge)
|
|
76
|
+
tile_bounds.each.with_index.entries
|
|
77
|
+
end.inject(:product).map(&:transpose).map(&:reverse).to_h
|
|
79
78
|
|
|
80
79
|
log_update "kmz: resizing image pyramid: %i%%" % (100 * (2**(zoom + 1) - 1) / (2**(max_zoom + 1) - 1))
|
|
81
80
|
{ zoom => [indices_bounds, tif_path] }
|
|
82
81
|
end.inject({}, &:merge)
|
|
83
82
|
|
|
84
83
|
kmz_dir = temp_dir.join("#{name}.kmz").tap(&:mkpath)
|
|
85
|
-
pyramid.
|
|
84
|
+
pyramid.flat_map do |zoom, (indices_bounds, tif_path)|
|
|
86
85
|
zoom_dir = kmz_dir.join(zoom.to_s).tap(&:mkpath)
|
|
87
86
|
indices_bounds.map do |indices, tile_bounds|
|
|
88
87
|
index_dir = zoom_dir.join(indices.first.to_s).tap(&:mkpath)
|
|
@@ -114,13 +113,15 @@ module NSWTopo
|
|
|
114
113
|
end
|
|
115
114
|
tile_kml_path.write xml
|
|
116
115
|
|
|
117
|
-
|
|
118
|
-
[tif_path, "-quiet", "+repage", "-crop", crop, "+repage", "+dither", "-type", "PaletteBilevelMatte", "PNG8:#{tile_png_path}"]
|
|
116
|
+
["-srcwin", indices[0] * Kmz::TILE_SIZE, indices[1] * Kmz::TILE_SIZE, Kmz::TILE_SIZE, Kmz::TILE_SIZE, tif_path, tile_png_path]
|
|
119
117
|
end
|
|
120
|
-
end.
|
|
118
|
+
end.tap do |tiles|
|
|
121
119
|
log_update "kmz: creating %i tiles" % tiles.length
|
|
122
120
|
end.each.concurrently do |args|
|
|
123
|
-
OS.
|
|
121
|
+
OS.gdal_translate "--config", "GDAL_PAM_ENABLED", "NO", *args
|
|
122
|
+
end.map(&:last).each.concurrent_groups do |tile_png_paths|
|
|
123
|
+
dither *tile_png_paths
|
|
124
|
+
rescue Dither::Missing
|
|
124
125
|
end
|
|
125
126
|
|
|
126
127
|
xml = REXML::Document.new
|
|
@@ -128,10 +129,11 @@ module NSWTopo
|
|
|
128
129
|
xml.add_element("kml", "xmlns" => "http://earth.google.com/kml/2.1").tap do |kml|
|
|
129
130
|
kml.add_element("Document").tap do |document|
|
|
130
131
|
document.add_element("LookAt").tap do |look_at|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
names_values
|
|
132
|
+
extents = @dimensions.times(@scale / 1000.0)
|
|
133
|
+
range_x = extents.first / 2.0 / Math::tan(Kmz::FOV) / Math::cos(Kmz::TILT)
|
|
134
|
+
range_y = extents.last / Math::cos(Kmz::FOV - Kmz::TILT) / 2 / (Math::tan(Kmz::FOV - Kmz::TILT) + Math::sin(Kmz::TILT))
|
|
135
|
+
names_values = [%w[longitude latitude], @centre].transpose
|
|
136
|
+
names_values << ["tilt", Kmz::TILT * 180.0 / Math::PI] << ["range", 1.2 * [range_x, range_y].max] << ["heading", rotation]
|
|
135
137
|
names_values.each { |name, value| look_at.add_element(name).text = value }
|
|
136
138
|
end
|
|
137
139
|
document.add_element("Name").text = name
|