nswtopo 2.0.0 → 3.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/COPYING +70 -83
- data/bin/nswtopo +227 -116
- data/docs/README.md +1 -12
- data/docs/add.md +1 -1
- data/docs/config.md +1 -1
- data/docs/contours.md +3 -1
- data/docs/init.md +8 -0
- data/docs/inspect.md +103 -0
- data/docs/move.md +9 -0
- data/docs/render.md +16 -7
- data/docs/scrape.md +67 -0
- data/docs/spot-heights.md +6 -2
- data/lib/nswtopo/archive.rb +50 -41
- data/lib/nswtopo/chrome.rb +232 -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 +112 -18
- 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 +204 -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 +7 -196
- metadata +39 -20
- data/lib/nswtopo/font/chrome.rb +0 -59
- data/lib/nswtopo/font/generic.rb +0 -25
- data/lib/nswtopo/gis/arcgis_server/connection.rb +0 -52
- data/lib/nswtopo/gis/arcgis_server.rb +0 -155
- data/lib/nswtopo/gis/geojson/multi_point.rb +0 -12
- data/lib/nswtopo/gis/world_file.rb +0 -19
- data/lib/nswtopo/layer/labels/fence.rb +0 -20
@@ -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,122 @@
|
|
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
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
9
|
+
render_svg svg_path, background: background
|
10
|
+
|
11
|
+
REXML::Document.new(svg_path.read).tap do |xml|
|
12
|
+
xml.elements["svg"].tap do |svg|
|
13
|
+
style = "@media print { @page { margin: 0 0 -1mm 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
|
81
|
+
end
|
82
|
+
|
83
|
+
svg_path.write xml
|
84
|
+
end
|
85
|
+
|
86
|
+
log_update "chrome: rendering PDF"
|
87
|
+
Chrome.with_browser("file://#{svg_path}") do |browser|
|
88
|
+
browser.print_to_pdf(pdf_path) do |doc|
|
89
|
+
bbox = [0, 0, *dimensions.times(72/25.4)]
|
90
|
+
bounds = cutline.coordinates[0][...-1].map do |coords|
|
91
|
+
coords.zip(dimensions).map { |coord, dimension| coord / dimension }
|
92
|
+
end.flatten
|
93
|
+
lpts = [0, 0].zip( [1, 1]).inject(&:product).values_at(0,1,3,2).flatten
|
94
|
+
gpts = [0, 0].zip(dimensions).inject(&:product).values_at(0,1,3,2).then do |corners|
|
95
|
+
# corners.map(&:reverse) # ISO 32000-2 specifies this, but not observed in practice
|
96
|
+
GeoJSON.multipoint(corners, projection: projection).reproject_to_wgs84.coordinates.map(&:reverse)
|
97
|
+
end.flatten
|
98
|
+
pcsm = [25.4/72, 0, 0, 0, 25.4/72, 0, 0, 0, 1, 0, 0, 0]
|
99
|
+
|
100
|
+
doc.pages.first[:VP] = [doc.add({
|
101
|
+
Type: :Viewport,
|
102
|
+
BBox: bbox,
|
103
|
+
Measure: doc.add({
|
104
|
+
Type: :Measure,
|
105
|
+
Subtype: :GEO,
|
106
|
+
Bounds: bounds,
|
107
|
+
GCS: doc.add({
|
108
|
+
Type: :PROJCS,
|
109
|
+
WKT: projection.wkt2
|
110
|
+
}),
|
111
|
+
GPTS: gpts,
|
112
|
+
LPTS: lpts,
|
113
|
+
PCSM: pcsm
|
114
|
+
})
|
115
|
+
})]
|
116
|
+
|
117
|
+
doc.trailer.info[:Creator] = "nswtopo"
|
118
|
+
doc.version = "1.7"
|
23
119
|
end
|
24
|
-
stdout, stderr, status = Open3.capture3 browser_path.to_s, *args, "file://#{svg_path}"
|
25
|
-
raise "couldn't create PDF using %s" % browser_name unless status.success? && pdf_path.file?
|
26
120
|
end
|
27
121
|
end
|
28
122
|
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
|