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
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
module NSWTopo
|
|
2
2
|
module Formats
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
end
|
|
6
|
-
|
|
7
|
-
def render_mbtiles(mbtiles_path, name:, zoom: Mbtiles::ZOOM, **options)
|
|
8
|
-
raise "invalid zoom outside 10-19 range: #{zoom}" unless (10..19) === zoom
|
|
9
|
-
|
|
10
|
-
web_mercator_bounds = bounds(projection: Projection.new("EPSG:3857"))
|
|
11
|
-
wgs84_bounds = bounds(projection: Projection.wgs84)
|
|
3
|
+
def render_mbtiles(mbtiles_path, name:, **options, &block)
|
|
4
|
+
wgs84_bounds = @cutline.reproject_to_wgs84.bounds
|
|
12
5
|
sql = <<~SQL
|
|
13
6
|
CREATE TABLE metadata (name TEXT, value TEXT);
|
|
14
7
|
INSERT INTO metadata VALUES ("name", "#{name}");
|
|
@@ -21,39 +14,10 @@ module NSWTopo
|
|
|
21
14
|
SQL
|
|
22
15
|
|
|
23
16
|
Dir.mktmppath do |temp_dir|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
resolution = Mbtiles::RESOLUTION / 2**zoom
|
|
27
|
-
indices, dimensions, topleft = web_mercator_bounds.map do |lower, upper|
|
|
28
|
-
((lower - Mbtiles::ORIGIN) / resolution / Mbtiles::TILE_SIZE).floor ... ((upper - Mbtiles::ORIGIN) / resolution / Mbtiles::TILE_SIZE).ceil
|
|
29
|
-
end.map.with_index do |indices, axis|
|
|
30
|
-
[indices, (indices.last - indices.first) * Mbtiles::TILE_SIZE, Mbtiles::ORIGIN + (axis.zero? ? indices.first : indices.last) * Mbtiles::TILE_SIZE * resolution]
|
|
31
|
-
end.transpose
|
|
32
|
-
tile_path = temp_dir.join("#{name}.mbtiles.#{zoom}.%09d.png").to_s
|
|
33
|
-
levels << [resolution, indices, dimensions, topleft, tile_path, zoom]
|
|
34
|
-
break levels if indices.map(&:size).all? { |size| size < 3 }
|
|
35
|
-
levels
|
|
36
|
-
end.tap do |(resolution, *, zoom), *|
|
|
37
|
-
png_path = yield(resolution: resolution)
|
|
38
|
-
end.tap do |levels|
|
|
39
|
-
log_update "mbtiles: tiling for zoom levels %s" % levels.map(&:last).minmax.uniq.join(?-)
|
|
40
|
-
end.each.concurrently do |resolution, indices, dimensions, topleft, tile_path, zoom|
|
|
41
|
-
tif_path, tfw_path = %w[tif tfw].map { |ext| temp_dir / "#{name}.mbtiles.#{zoom}.#{ext}" }
|
|
42
|
-
WorldFile.write topleft, resolution, 0, tfw_path
|
|
43
|
-
OS.convert "-size", dimensions.join(?x), "canvas:none", "-type", "TrueColorAlpha", "-depth", 8, tif_path
|
|
44
|
-
OS.gdalwarp "-s_srs", @projection, "-t_srs", "EPSG:3857", "-r", "lanczos", "-dstalpha", png_path, tif_path
|
|
45
|
-
OS.convert tif_path, "-quiet", "+repage", "-crop", "#{Mbtiles::TILE_SIZE}x#{Mbtiles::TILE_SIZE}", tile_path
|
|
46
|
-
end.map do |resolution, indices, dimensions, topleft, tile_path, zoom|
|
|
47
|
-
indices[1].to_a.reverse.product(indices[0].to_a).map.with_index do |(row, col), index|
|
|
48
|
-
[tile_path % index, zoom, col, row]
|
|
49
|
-
end
|
|
50
|
-
end.flatten(1).each do |tile_path, zoom, col, row|
|
|
51
|
-
sql << %Q[INSERT INTO tiles VALUES (#{zoom}, #{col}, #{row}, readfile("#{tile_path}"));\n]
|
|
52
|
-
end.tap do |tiles|
|
|
53
|
-
log_update "mbtiles: optimising %i tiles" % tiles.length
|
|
54
|
-
end.map(&:first).each.concurrent_groups do |png_paths|
|
|
55
|
-
dither *png_paths
|
|
17
|
+
tiled_web_map(temp_dir, **options, extension: "mbtiles", &block).each do |tile|
|
|
18
|
+
sql << %Q[INSERT INTO tiles VALUES (#{tile.zoom}, #{tile.col}, #{tile.row}, readfile("#{tile.path}"));\n]
|
|
56
19
|
end
|
|
20
|
+
|
|
57
21
|
OS.sqlite3 mbtiles_path do |stdin|
|
|
58
22
|
stdin.puts sql
|
|
59
23
|
stdin.puts ".exit"
|
data/lib/nswtopo/formats/pdf.rb
CHANGED
|
@@ -1,28 +1,93 @@
|
|
|
1
1
|
module NSWTopo
|
|
2
2
|
module Formats
|
|
3
|
-
def render_pdf(pdf_path, ppi: nil,
|
|
3
|
+
def render_pdf(pdf_path, ppi: nil, background:, **options)
|
|
4
4
|
if ppi
|
|
5
|
-
OS.gdal_translate "-
|
|
5
|
+
OS.gdal_translate "-of", "PDF", "-co", "DPI=#{ppi}", "-co", "MARGIN=0", "-co", "CREATOR=nswtopo", "-co", "GEO_ENCODING=ISO32000", yield(ppi: ppi), pdf_path
|
|
6
6
|
else
|
|
7
7
|
Dir.mktmppath do |temp_dir|
|
|
8
8
|
svg_path = temp_dir / "pdf-map.svg"
|
|
9
|
-
render_svg svg_path,
|
|
10
|
-
xml = REXML::Document.new svg_path.read
|
|
11
|
-
style = "@media print { @page { margin: 0 0 -1mm 0; size: %s %s; } }"
|
|
12
|
-
svg = xml.elements["svg"]
|
|
13
|
-
svg.add_element("style").text = style % svg.attributes.values_at("width", "height")
|
|
14
|
-
svg_path.write xml
|
|
9
|
+
render_svg svg_path, background: background
|
|
15
10
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
11
|
+
REXML::Document.new(svg_path.read).tap do |xml|
|
|
12
|
+
xml.elements["svg"].tap do |svg|
|
|
13
|
+
style = "@media print { @page { margin: 0; size: %s %s; } }"
|
|
14
|
+
svg.add_element("style").text = style % svg.attributes.values_at("width", "height")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# replace fill pattern paint with manual pattern mosaic to work around Chrome PDF bug
|
|
18
|
+
xml.elements.each("//svg//use[@id][@fill][@href]") do |use|
|
|
19
|
+
id = use.attributes["id"]
|
|
20
|
+
|
|
21
|
+
# find the pattern id, content id, pattern element and content element
|
|
22
|
+
next unless /^url\(#(?<pattern_id>.*)\)$/ =~ use.attributes["fill"]
|
|
23
|
+
next unless /^#(?<content_id>.*)$/ =~ use.attributes["href"]
|
|
24
|
+
next unless pattern = use.elements["preceding::defs/pattern[@id='#{pattern_id}'][@width][@height]"]
|
|
25
|
+
next unless content = use.elements["preceding::defs/g[@id='#{content_id}']"]
|
|
26
|
+
|
|
27
|
+
# change pattern element to a group
|
|
28
|
+
pattern.attributes.delete "patternUnits"
|
|
29
|
+
pattern.name = "g"
|
|
30
|
+
|
|
31
|
+
# create a clip path to apply to the fill pattern mosaic
|
|
32
|
+
content_clip = REXML::Element.new "clipPath"
|
|
33
|
+
content_clip.add_attribute "id", "#{content_id}.clip"
|
|
34
|
+
|
|
35
|
+
# create a clip path to apply to pattern element
|
|
36
|
+
pattern_clip = REXML::Element.new "clipPath"
|
|
37
|
+
pattern_clip.add_attribute "id", "#{pattern_id}.clip"
|
|
38
|
+
pattern.add_attribute "clip-path", "url(##{pattern_id}.clip)"
|
|
39
|
+
|
|
40
|
+
# move content and clip paths into defs
|
|
41
|
+
pattern.previous_sibling = pattern_clip
|
|
42
|
+
pattern.next_sibling = content
|
|
43
|
+
content.next_sibling = content_clip
|
|
44
|
+
|
|
45
|
+
# replace fill paint with a container for the fill pattern mosaic
|
|
46
|
+
fill = REXML::Element.new "g"
|
|
47
|
+
fill.add_attribute "clip-path", "url(##{content_id}.clip)"
|
|
48
|
+
fill.add_attribute "id", "#{id}.fill"
|
|
49
|
+
use.previous_sibling = fill
|
|
50
|
+
use.add_attribute "fill", "none"
|
|
51
|
+
|
|
52
|
+
xml.elements.each("//use[@href='##{id}']") do |use|
|
|
53
|
+
use_fill = REXML::Element.new "use"
|
|
54
|
+
use_fill.add_attribute "href", "##{id}.fill"
|
|
55
|
+
use.previous_sibling = use_fill
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# get pattern size
|
|
59
|
+
pattern_size = %w[width height].map do |name|
|
|
60
|
+
pattern.attributes[name].tap { pattern.attributes.delete name }
|
|
61
|
+
end.map(&:to_f)
|
|
62
|
+
|
|
63
|
+
# create pattern clip
|
|
64
|
+
pattern_size.each.with_object(0).inject(&:product).values_at(3,2,0,1).tap do |corners|
|
|
65
|
+
pattern_clip.add_element "path", "d" => %w[M L L L].zip(corners).push("Z").join(?\s)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# add paths to content clip, get content coverage area, and create fill pattern mosaic
|
|
69
|
+
content.elements.collect("path[@d]", &:itself).each.with_index do |path, index|
|
|
70
|
+
path.add_attribute "id", "#{content_id}.#{index}"
|
|
71
|
+
content_clip.add_element "use", "href" => "##{content_id}.#{index}"
|
|
72
|
+
end.flat_map do |path|
|
|
73
|
+
path.attributes["d"].scan /(\d+(?:\.\d+)?) (\d+(?:\.\d+)?)/
|
|
74
|
+
end.transpose.map do |coords|
|
|
75
|
+
coords.map(&:to_f).minmax
|
|
76
|
+
end.zip(pattern_size).map do |(min, max), size|
|
|
77
|
+
(min...max).step(size).entries
|
|
78
|
+
end.inject(&:product).each do |x, y|
|
|
79
|
+
fill.add_element "use", "href" => "##{pattern_id}", "x" => x, "y" => y
|
|
80
|
+
end
|
|
23
81
|
end
|
|
24
|
-
|
|
25
|
-
|
|
82
|
+
|
|
83
|
+
svg_path.write xml
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
FileUtils.rm pdf_path if pdf_path.exist?
|
|
87
|
+
log_update "chrome: rendering PDF"
|
|
88
|
+
|
|
89
|
+
Chrome.with_browser("file://#{svg_path}") do |browser|
|
|
90
|
+
browser.print_to_pdf pdf_path
|
|
26
91
|
end
|
|
27
92
|
end
|
|
28
93
|
end
|
data/lib/nswtopo/formats/svg.rb
CHANGED
|
@@ -1,68 +1,137 @@
|
|
|
1
1
|
module NSWTopo
|
|
2
|
+
class SVGFormatter < REXML::Formatters::Pretty
|
|
3
|
+
def initialize(*args)
|
|
4
|
+
super
|
|
5
|
+
self.compact, @default = true, REXML::Formatters::Default.new
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def write_element(node, output)
|
|
9
|
+
case node.name
|
|
10
|
+
when "text"
|
|
11
|
+
output << ' ' * @level
|
|
12
|
+
@default.write_element node, output
|
|
13
|
+
else
|
|
14
|
+
super
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
2
19
|
module Formats
|
|
3
|
-
def
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
desc = svg.elements["metadata/rdf:RDF/rdf:Description[@dc:creator='nswtopo']"]
|
|
11
|
-
raise "not an nswtopo SVG file: %s" % external unless desc
|
|
12
|
-
rescue REXML::ParseException
|
|
13
|
-
raise "not an SVG file: %s" % external
|
|
14
|
-
end
|
|
15
|
-
FileUtils.cp external, svg_path
|
|
20
|
+
def neatline_path_data
|
|
21
|
+
@neatline.coordinates.map do |ring|
|
|
22
|
+
ring.map do |point|
|
|
23
|
+
point.join(" ")
|
|
24
|
+
end.join(" L ").prepend("M ").concat(" Z")
|
|
25
|
+
end.join(" ")
|
|
26
|
+
end
|
|
16
27
|
|
|
17
|
-
|
|
18
|
-
|
|
28
|
+
def render_svg(svg_path, background:, **options)
|
|
29
|
+
if uptodate?("map.svg", "map.yml")
|
|
30
|
+
log_update "nswtopo: reading existing map SVG"
|
|
31
|
+
xml = REXML::Document.new read("map.svg")
|
|
32
|
+
xml.elements["svg/metadata/rdf:RDF/rdf:Description"].add_attributes("xmp:ModifyDate" => Time.now.iso8601)
|
|
19
33
|
|
|
20
34
|
else
|
|
21
|
-
width, height =
|
|
35
|
+
width, height = @dimensions
|
|
22
36
|
xml = REXML::Document.new
|
|
23
37
|
xml << REXML::XMLDecl.new(1.0, "utf-8")
|
|
24
38
|
svg = xml.add_element "svg",
|
|
25
|
-
"version" => 1.1,
|
|
26
|
-
"baseProfile" => "full",
|
|
27
39
|
"width" => "#{width}mm",
|
|
28
40
|
"height" => "#{height}mm",
|
|
29
41
|
"viewBox" => "0 0 #{width} #{height}",
|
|
30
|
-
"
|
|
31
|
-
"xmlns
|
|
32
|
-
"xmlns:
|
|
33
|
-
"xmlns:inkscape" => "http://www.inkscape.org/namespaces/inkscape"
|
|
42
|
+
"text-rendering" => "geometricPrecision",
|
|
43
|
+
"xmlns" => "http://www.w3.org/2000/svg",
|
|
44
|
+
"xmlns:nswtopo" => "http://nswtopo.com"
|
|
34
45
|
|
|
35
|
-
|
|
36
|
-
|
|
46
|
+
metadata = svg.add_element("metadata")
|
|
47
|
+
metadata.add_element("nswtopo:map",
|
|
48
|
+
"projection" => @neatline.projection.wkt2,
|
|
49
|
+
"neatline" => @neatline.coordinates.to_json,
|
|
50
|
+
"centre" => @centre.to_json,
|
|
51
|
+
"scale" => @scale,
|
|
52
|
+
"rotation" => @rotation
|
|
53
|
+
)
|
|
54
|
+
metadata.add_element("rdf:RDF",
|
|
37
55
|
"xmlns:rdf" => "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
|
|
56
|
+
"xmlns:xmp" => "http://ns.adobe.com/xap/1.0/",
|
|
38
57
|
"xmlns:dc" => "http://purl.org/dc/elements/1.1/"
|
|
39
|
-
|
|
40
|
-
"
|
|
41
|
-
"dc:format" => "image/svg+xml"
|
|
42
|
-
|
|
58
|
+
).add_element("rdf:Description",
|
|
59
|
+
"xmp:CreatorTool" => VERSION.creator_string,
|
|
60
|
+
"dc:format" => "image/svg+xml"
|
|
61
|
+
)
|
|
43
62
|
|
|
44
|
-
defs
|
|
45
|
-
svg.add_element
|
|
46
|
-
|
|
63
|
+
# add defs for map filters and masks
|
|
64
|
+
defs = svg.add_element("defs", "id" => "map.defs")
|
|
65
|
+
defs.add_element("rect", "id" => "map.rect", "width" => width, "height" => height)
|
|
66
|
+
defs.add_element("path", "id" => "map.neatline", "d" => neatline_path_data)
|
|
67
|
+
defs.add_element("clipPath", "id" => "map.clip").add_element("use", "href" => "#map.neatline")
|
|
47
68
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
labels.add layer if Vector === layer
|
|
52
|
-
end.push(labels).each do |layer|
|
|
53
|
-
log_update "compositing: #{layer.name}"
|
|
54
|
-
group = svg.add_element "g", "id" => layer.name, "inkscape:groupmode" => "layer"
|
|
55
|
-
layer.render group, defs, &labels.method(:add_fence)
|
|
69
|
+
# add a filter converting alpha channel to cutout mask
|
|
70
|
+
defs.add_element("filter", "id" => "map.filter.cutout").tap do |filter|
|
|
71
|
+
filter.add_element("feComponentTransfer", "in" => "SourceAlpha")
|
|
56
72
|
end
|
|
57
73
|
|
|
58
|
-
|
|
74
|
+
Enumerator.new do |yielder|
|
|
75
|
+
labels = Layer.new "labels", self, Config.fetch("labels", {}).merge("type" => "Labels")
|
|
76
|
+
layers.reject do |layer|
|
|
77
|
+
log_update "reading: #{layer.name}"
|
|
78
|
+
layer.empty?
|
|
79
|
+
end.each do |layer|
|
|
80
|
+
next if Config["labelling"] == false
|
|
81
|
+
labels.add layer if Vector === layer
|
|
82
|
+
end.push(labels).each.with_object [[], []] do |layer, (cutouts, knockouts)|
|
|
83
|
+
log_update "compositing: #{layer.name}"
|
|
84
|
+
new_knockouts, knockout = [], "map.mask.knockout.#{knockouts.length+1}"
|
|
85
|
+
layer.render(cutouts: cutouts, knockout: knockout) do |object|
|
|
86
|
+
case object
|
|
87
|
+
when Labels::Barrier then labels << object
|
|
88
|
+
when Vector::Cutout then cutouts << object
|
|
89
|
+
when Vector::Knockout then new_knockouts << object
|
|
90
|
+
when REXML::Element
|
|
91
|
+
object.attributes["mask"] ||= "url(#map.mask.knockout.#{knockouts.length})" unless "defs" == object.name
|
|
92
|
+
yielder << object
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
knockouts << new_knockouts if new_knockouts.any?
|
|
96
|
+
end.last.push([]).each.with_index do |knockouts, index|
|
|
97
|
+
mask = defs.add_element("mask", "id" => "map.mask.knockout.#{index}")
|
|
98
|
+
content = mask.add_element("g", "id" => "map.mask.knockout.#{index}.content")
|
|
99
|
+
content.add_element("use", "href" => "#map.mask.knockout.#{index+1}.content") if knockouts.any?
|
|
100
|
+
content.add_element("use", "href" => "#map.rect", "fill" => "white", "stroke" => "none") if knockouts.none?
|
|
101
|
+
knockouts.group_by(&:buffer).map do |buffer, knockouts|
|
|
102
|
+
group = content.add_element("g", "filter" => "url(#map.filter.knockout.#{buffer})")
|
|
103
|
+
knockouts.each do |knockout|
|
|
104
|
+
group.add_element knockout.use
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end.flatten.group_by(&:buffer).keys.each do |buffer|
|
|
108
|
+
filter = defs.add_element("filter", "id" => "map.filter.knockout.#{buffer}")
|
|
109
|
+
filter.add_element("feColorMatrix", "values" => "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 5 0")
|
|
110
|
+
filter.add_element("feMorphology", "operator" => "dilate", "radius" => buffer) unless buffer.zero?
|
|
111
|
+
filter.add_element("feComponentTransfer").add_element("feFuncA", "type" => "discrete", "tableValues" => "0 1")
|
|
112
|
+
end
|
|
113
|
+
end.reject do |element|
|
|
114
|
+
svg.add_element(element) if "defs" == element.name
|
|
115
|
+
end.tap do
|
|
116
|
+
svg.add_element("use", "id" => "map.background", "href" => "#map.neatline", "fill" => "white")
|
|
117
|
+
end.chunk do |element|
|
|
118
|
+
element.attributes["mask"]
|
|
119
|
+
end.each.with_object(svg.add_element("g", "clip-path" => "url(#map.clip)")) do |(mask, elements), clip_group|
|
|
120
|
+
elements.each.with_object(clip_group.add_element("g", "mask" => mask)) do |element, mask_group|
|
|
121
|
+
mask_group.add_element element
|
|
122
|
+
element.delete_attribute "mask"
|
|
123
|
+
end
|
|
59
124
|
end
|
|
60
125
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
126
|
+
xml.elements.each("svg//defs[not(*)]", &:remove)
|
|
127
|
+
xml.elements["svg/metadata/rdf:RDF/rdf:Description"].add_attributes %w[xmp:ModifyDate xmp:CreateDate].each.with_object(Time.now.iso8601).to_h
|
|
128
|
+
write "map.svg", xml.to_s
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
xml.elements["svg/use[@id='map.background']"].add_attributes("fill" => background) if background
|
|
132
|
+
|
|
133
|
+
svg_path.open("w") do |file|
|
|
134
|
+
SVGFormatter.new.write xml, file
|
|
66
135
|
end
|
|
67
136
|
end
|
|
68
137
|
end
|
data/lib/nswtopo/formats/svgz.rb
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
module NSWTopo
|
|
2
2
|
module Formats
|
|
3
|
-
def render_svgz(svgz_path,
|
|
3
|
+
def render_svgz(svgz_path, background:, **options)
|
|
4
4
|
Dir.mktmppath do |temp_dir|
|
|
5
5
|
svg_path = temp_dir / "svgz-map.svg"
|
|
6
|
-
render_svg svg_path,
|
|
6
|
+
render_svg svg_path, background: background
|
|
7
7
|
Zlib::GzipWriter.open svgz_path do |gz|
|
|
8
8
|
gz.write svg_path.binread
|
|
9
9
|
end
|
data/lib/nswtopo/formats/zip.rb
CHANGED
|
@@ -2,37 +2,47 @@ module NSWTopo
|
|
|
2
2
|
module Formats
|
|
3
3
|
def render_zip(zip_path, name:, ppi: PPI, **options)
|
|
4
4
|
Dir.mktmppath do |temp_dir|
|
|
5
|
-
zip_dir = temp_dir.join("
|
|
5
|
+
zip_dir = temp_dir.join("zip").tap(&:mkpath)
|
|
6
6
|
tiles_dir = zip_dir.join("tiles").tap(&:mkpath)
|
|
7
7
|
png_path = yield(ppi: ppi)
|
|
8
|
-
top_left = bounding_box.coordinates[0][3]
|
|
9
8
|
|
|
10
9
|
2.downto(0).map.with_index do |level, index|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
FileUtils.cp tile_path % n, tiles_dir / "#{level}x#{y}x#{x}.png"
|
|
10
|
+
geo_transform = geotransform(ppi: ppi / 2**index)
|
|
11
|
+
outsize = (@dimensions / geo_transform[1]).map(&:ceil)
|
|
12
|
+
case index
|
|
13
|
+
when 0
|
|
14
|
+
thumb_size = outsize.inject(&:<) ? [0, 64] : [64, 0]
|
|
15
|
+
OS.gdal_translate *%w[--config GDAL_PAM_ENABLED NO -r bilinear -outsize], *thumb_size, png_path, zip_dir / "thumb.png"
|
|
16
|
+
when 1
|
|
17
|
+
zip_dir.join("#{name}.ref").open("w") do |file|
|
|
18
|
+
file.puts @projection.wkt2
|
|
19
|
+
file.puts geo_transform.join(?,)
|
|
20
|
+
file.puts outsize.join(?,)
|
|
21
|
+
end
|
|
24
22
|
end
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
23
|
+
img_path = index.zero? ? png_path : temp_dir / "map.#{level}.png"
|
|
24
|
+
next level, outsize, img_path
|
|
25
|
+
end.each.concurrently do |level, outsize, img_path|
|
|
26
|
+
OS.gdal_translate *%w[-r bicubic -outsize], *outsize, png_path, img_path unless img_path.exist?
|
|
27
|
+
end.flat_map do |level, outsize, img_path|
|
|
28
|
+
outsize.map do |px|
|
|
29
|
+
(0...px).step(256).with_index.entries
|
|
30
|
+
end.inject(&:product).map do |(col, j), (row, i)|
|
|
31
|
+
tile_path = tiles_dir / "#{level}x#{i}x#{j}.png"
|
|
32
|
+
size = [-col, -row].zip(outsize).map(&:sum).zip([256, 256]).map(&:min)
|
|
33
|
+
%w[--config GDAL_PAM_ENABLED NO -srcwin] + [col, row, *size, img_path, tile_path]
|
|
34
|
+
end
|
|
35
|
+
end.tap do |tiles|
|
|
36
|
+
log_update "zip: creating %i tiles" % tiles.length
|
|
37
|
+
end.each.concurrently do |args|
|
|
38
|
+
OS.gdal_translate *args
|
|
39
|
+
end.map(&:last).tap do |tile_paths|
|
|
40
|
+
log_update "zip: optimising %i tiles" % tile_paths.length
|
|
41
|
+
end.each.concurrent_groups do |tile_paths|
|
|
32
42
|
dither *tile_paths
|
|
43
|
+
rescue Dither::Missing
|
|
33
44
|
end
|
|
34
45
|
|
|
35
|
-
OS.convert png_path, "-thumbnail", "64x64", "-gravity", "center", "-background", "white", "-extent", "64x64", "-alpha", "Remove", "-type", "TrueColor", zip_dir / "thumb.png"
|
|
36
46
|
zip zip_dir, zip_path
|
|
37
47
|
end
|
|
38
48
|
end
|
data/lib/nswtopo/formats.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require_relative 'formats/svg'
|
|
2
2
|
require_relative 'formats/kmz'
|
|
3
3
|
require_relative 'formats/mbtiles'
|
|
4
|
+
require_relative 'formats/gemf'
|
|
4
5
|
require_relative 'formats/zip'
|
|
5
6
|
require_relative 'formats/pdf'
|
|
6
7
|
require_relative 'formats/svgz'
|
|
@@ -9,6 +10,8 @@ module NSWTopo
|
|
|
9
10
|
module Formats
|
|
10
11
|
include Log
|
|
11
12
|
PPI = 300
|
|
13
|
+
TILE = 1500
|
|
14
|
+
ARGS = %w[--force-gpu-mem-available-mb=4096]
|
|
12
15
|
|
|
13
16
|
def self.extensions
|
|
14
17
|
instance_methods.grep(/^render_([a-z]+)/) { $1 }
|
|
@@ -19,57 +22,99 @@ module NSWTopo
|
|
|
19
22
|
end
|
|
20
23
|
|
|
21
24
|
def render_png(png_path, ppi: PPI, dither: false, **options)
|
|
25
|
+
ppm = (ppi / 0.0254).round
|
|
26
|
+
OS.exiftool yield(ppi: ppi, dither: dither), *%W[
|
|
27
|
+
-PNG:PixelsPerUnitX=#{ppm}
|
|
28
|
+
-PNG:PixelsPerUnitY=#{ppm}
|
|
29
|
+
-o #{png_path}
|
|
30
|
+
]
|
|
31
|
+
rescue OS::Missing
|
|
22
32
|
FileUtils.cp yield(ppi: ppi, dither: dither), png_path
|
|
23
33
|
end
|
|
24
34
|
|
|
25
35
|
def render_tif(tif_path, ppi: PPI, dither: false, **options)
|
|
26
|
-
OS.gdal_translate
|
|
36
|
+
OS.gdal_translate yield(ppi: ppi, dither: dither), *%W[
|
|
37
|
+
-of GTiff
|
|
38
|
+
-co COMPRESS=DEFLATE
|
|
39
|
+
-co ZLEVEL=9
|
|
40
|
+
-mo TIFFTAG_XRESOLUTION=#{ppi}
|
|
41
|
+
-mo TIFFTAG_YRESOLUTION=#{ppi}
|
|
42
|
+
-mo TIFFTAG_RESOLUTIONUNIT=2
|
|
43
|
+
], tif_path
|
|
27
44
|
end
|
|
28
45
|
|
|
29
46
|
def render_jpg(jpg_path, ppi: PPI, **options)
|
|
30
|
-
OS.gdal_translate
|
|
47
|
+
OS.gdal_translate yield(ppi: ppi), *%W[
|
|
48
|
+
-of JPEG
|
|
49
|
+
-co QUALITY=90
|
|
50
|
+
-mo EXIF_XResolution=#{ppi}
|
|
51
|
+
-mo EXIF_YResolution=#{ppi}
|
|
52
|
+
-mo EXIF_ResolutionUnit=2
|
|
53
|
+
], jpg_path
|
|
31
54
|
end
|
|
32
55
|
|
|
33
|
-
def rasterise(png_path,
|
|
56
|
+
def rasterise(png_path, background:, ppi: nil, resolution: nil)
|
|
34
57
|
Dir.mktmppath do |temp_dir|
|
|
35
|
-
dimensions, ppi, resolution = raster_dimensions_at **options
|
|
36
58
|
svg_path = temp_dir / "map.svg"
|
|
37
|
-
|
|
38
|
-
render_svg svg_path,
|
|
59
|
+
vrt_path = temp_dir / "map.vrt"
|
|
60
|
+
render_svg svg_path, background: background
|
|
39
61
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
62
|
+
case
|
|
63
|
+
when ppi
|
|
64
|
+
ppi_info = "%i ppi" % ppi
|
|
65
|
+
mm_per_px = 25.4 / ppi
|
|
66
|
+
when resolution
|
|
67
|
+
ppi_info = "%.1f m/px" % resolution
|
|
68
|
+
mm_per_px = to_mm(resolution)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
viewport_size = [TILE * mm_per_px] * 2
|
|
72
|
+
raster_size = (@dimensions / mm_per_px).map(&:ceil)
|
|
73
|
+
megapixels = raster_size.inject(&:*) / 1024.0 / 1024.0
|
|
74
|
+
|
|
75
|
+
raster_info = "%i×%i (%.1fMpx) map raster at %s" % [*raster_size, megapixels, ppi_info]
|
|
76
|
+
chrome_message = "chrome: creating #{raster_info}"
|
|
77
|
+
log_update chrome_message
|
|
44
78
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
[
|
|
79
|
+
NSWTopo::Chrome.with_browser "file://#{svg_path}", width: TILE, height: TILE, args: ARGS do |browser|
|
|
80
|
+
svg = browser.query_selector "svg"
|
|
81
|
+
svg[:width], svg[:height] = nil, nil
|
|
82
|
+
svg[:viewBox].split.map(&:to_f).last(2).map do |mm|
|
|
83
|
+
(0...(mm / mm_per_px).ceil).step(TILE).map do |px|
|
|
84
|
+
[px, px * mm_per_px]
|
|
51
85
|
end
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
86
|
+
end.inject(&:product).map(&:transpose).tap do |grid|
|
|
87
|
+
chrome_message += " (tile %i of #{grid.size})"
|
|
88
|
+
end.map.with_index do |(raster_offset, viewport_offset), index|
|
|
89
|
+
log_update chrome_message % [index + 1]
|
|
90
|
+
|
|
91
|
+
tile_path = temp_dir.join("tile.%i.%i.png" % raster_offset)
|
|
92
|
+
viewbox = [*viewport_offset, *viewport_size].join(?\s)
|
|
59
93
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
json = NSWTopo::OS.gdalinfo "-json", png_path
|
|
63
|
-
scaling = JSON.parse(json)["size"][0] / 1000.0
|
|
94
|
+
svg[:viewBox] = viewbox
|
|
95
|
+
browser.screenshot tile_path
|
|
64
96
|
|
|
65
|
-
|
|
66
|
-
|
|
97
|
+
REXML::Document.new(OS.gdal_translate "-of", "VRT", tile_path, "/vsistdout/").tap do |vrt|
|
|
98
|
+
vrt.elements.each("VRTDataset/VRTRasterBand/SimpleSource/DstRect") do |dst_rect|
|
|
99
|
+
dst_rect.add_attributes "xOff" => raster_offset[0], "yOff" => raster_offset[1]
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end.inject do |vrt, tile_vrt|
|
|
103
|
+
vrt.elements["VRTDataset/VRTRasterBand[@band='1']"].add_element tile_vrt.elements["VRTDataset/VRTRasterBand[@band='1']/SimpleSource"]
|
|
104
|
+
vrt.elements["VRTDataset/VRTRasterBand[@band='2']"].add_element tile_vrt.elements["VRTDataset/VRTRasterBand[@band='2']/SimpleSource"]
|
|
105
|
+
vrt.elements["VRTDataset/VRTRasterBand[@band='3']"].add_element tile_vrt.elements["VRTDataset/VRTRasterBand[@band='3']/SimpleSource"]
|
|
106
|
+
vrt.elements["VRTDataset/VRTRasterBand[@band='4']"].add_element tile_vrt.elements["VRTDataset/VRTRasterBand[@band='4']/SimpleSource"]
|
|
107
|
+
vrt
|
|
108
|
+
end.tap do |vrt|
|
|
109
|
+
vrt.elements.each("VRTDataset/VRTRasterBand/@blockYSize", &:remove)
|
|
110
|
+
vrt.elements.each("VRTDataset/Metadata", &:remove)
|
|
111
|
+
vrt.elements["VRTDataset"].add_attributes "rasterXSize" => raster_size[0], "rasterYSize" => raster_size[1]
|
|
112
|
+
File.write vrt_path, vrt
|
|
67
113
|
end
|
|
68
|
-
src_path.write svg
|
|
69
|
-
render.call *(dimensions / scaling).map(&:ceil)
|
|
70
114
|
end
|
|
71
115
|
|
|
72
|
-
|
|
116
|
+
log_update "nswtopo: finalising #{raster_info}"
|
|
117
|
+
OS.gdal_translate vrt_path, png_path
|
|
73
118
|
end
|
|
74
119
|
end
|
|
75
120
|
end
|
|
@@ -39,40 +39,9 @@ module Overlap
|
|
|
39
39
|
end
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
def overlap?(buffer
|
|
42
|
+
def overlap?(buffer)
|
|
43
43
|
!separated_by?(buffer)
|
|
44
44
|
end
|
|
45
|
-
|
|
46
|
-
def overlaps(buffer = 0)
|
|
47
|
-
return [] if empty?
|
|
48
|
-
axis = flatten(1).transpose.map { |values| values.max - values.min }.map.with_index.max.last
|
|
49
|
-
events, tops, bots, results = AVLTree.new, [], [], []
|
|
50
|
-
margin = [buffer, 0]
|
|
51
|
-
each.with_index do |hull, index|
|
|
52
|
-
min, max = hull.map { |point| point.rotate axis }.minmax
|
|
53
|
-
events << [min.minus(margin), index, :start]
|
|
54
|
-
events << [max.plus( margin), index, :stop ]
|
|
55
|
-
end
|
|
56
|
-
events.each do |point, index, event|
|
|
57
|
-
top, bot = at(index).transpose[1-axis].minmax
|
|
58
|
-
case event
|
|
59
|
-
when :start
|
|
60
|
-
not_above = bots.select { |bot, other| bot >= top - buffer }.map(&:last)
|
|
61
|
-
not_below = tops.select { |top, other| top <= bot + buffer }.map(&:last)
|
|
62
|
-
(not_below & not_above).reject do |other|
|
|
63
|
-
values_at(index, other).separated_by? buffer
|
|
64
|
-
end.each do |other|
|
|
65
|
-
results << [index, other]
|
|
66
|
-
end
|
|
67
|
-
tops << [top, index]
|
|
68
|
-
bots << [bot, index]
|
|
69
|
-
when :stop
|
|
70
|
-
tops.delete [top, index]
|
|
71
|
-
bots.delete [bot, index]
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
results
|
|
75
|
-
end
|
|
76
45
|
end
|
|
77
46
|
|
|
78
47
|
Array.send :include, Overlap
|