Module: NSWTopo::Formats
- Includes:
- Log
- Included in:
- Map
- Defined in:
- lib/nswtopo/formats.rb,
lib/nswtopo/formats/kmz.rb,
lib/nswtopo/formats/pdf.rb,
lib/nswtopo/formats/svg.rb,
lib/nswtopo/formats/zip.rb,
lib/nswtopo/formats/svgz.rb,
lib/nswtopo/formats/mbtiles.rb
Defined Under Namespace
Constant Summary collapse
- PPI =
300
Constants included from Log
Log::FAILURE, Log::NEUTRAL, Log::SUCCESS, Log::UPDATE
Class Method Summary collapse
Instance Method Summary collapse
- #rasterise(png_path, external:, **options) ⇒ Object
- #render_jpg(jpg_path, ppi: PPI, **options) ⇒ Object
- #render_kmz(kmz_path, name:, ppi: PPI, **options) ⇒ Object
- #render_mbtiles(mbtiles_path, name:, zoom: Mbtiles::ZOOM, **options) ⇒ Object
- #render_pdf(pdf_path, ppi: nil, external: nil, **options) ⇒ Object
- #render_png(png_path, ppi: PPI, dither: false, **options) ⇒ Object
- #render_svg(svg_path, external: nil, **options) ⇒ Object
- #render_svgz(svgz_path, external: nil, **options) ⇒ Object
- #render_tif(tif_path, ppi: PPI, dither: false, **options) ⇒ Object
- #render_zip(zip_path, name:, ppi: PPI, **options) ⇒ Object
Methods included from Log
#log_abort, #log_neutral, #log_success, #log_update, #log_warn
Class Method Details
.===(ext) ⇒ Object
17 18 19 |
# File 'lib/nswtopo/formats.rb', line 17 def self.===(ext) extensions.any? ext end |
.extensions ⇒ Object
13 14 15 |
# File 'lib/nswtopo/formats.rb', line 13 def self.extensions instance_methods.grep(/^render_([a-z]+)/) { $1 } end |
Instance Method Details
#rasterise(png_path, external:, **options) ⇒ Object
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/nswtopo/formats.rb', line 33 def rasterise(png_path, external:, **) Dir.mktmppath do |temp_dir| dimensions, ppi, resolution = raster_dimensions_at ** svg_path = temp_dir / "map.svg" src_path = temp_dir / "browser.svg" render_svg svg_path, external: external NSWTopo.with_browser do |browser_name, browser_path| megapixels = dimensions.inject(&:*) / 1024.0 / 1024.0 log_update "%s: creating %i×%i (%.1fMpx) map raster at %i ppi" % [browser_name, *dimensions, megapixels, [:ppi] ] if [:ppi] log_update "%s: creating %i×%i (%.1fMpx) map raster at %.1f m/px" % [browser_name, *dimensions, megapixels, [:resolution]] if [:resolution] render = lambda do |width, height| args = case browser_name when "firefox" ["--window-size=#{width},#{height}", "-headless", "-screenshot", png_path.to_s] when "chrome" ["--window-size=#{width},#{height}", "--headless", "--screenshot=#{png_path}", "--disable-lcd-text", "--disable-extensions", "--hide-scrollbars", "--disable-gpu"] end FileUtils.rm png_path if png_path.exist? stdout, stderr, status = Open3.capture3 browser_path.to_s, *args, "file://#{src_path}" case browser_name when "firefox" then raise "couldn't rasterise map using firefox (ensure browser is closed)" when "chrome" then raise "couldn't rasterise map using chrome" end unless status.success? && png_path.file? end src_path.write %Q[<?xml version='1.0' encoding='UTF-8'?><svg version='1.1' baseProfile='full' xmlns='http://www.w3.org/2000/svg'></svg>] render.call 1000, 1000 json = NSWTopo::OS.gdalinfo "-json", png_path scaling = JSON.parse(json)["size"][0] / 1000.0 svg = %w[width height].inject(svg_path.read) do |svg, attribute| svg.sub(/#{attribute}='(.*?)mm'/) { %Q[#{attribute}='#{$1.to_f * ppi / 96.0 / scaling}mm'] } end src_path.write svg render.call *(dimensions / scaling).map(&:ceil) end OS.mogrify "+repage", "-crop", "#{dimensions.join ?x}+0+0", "-background", "white", "-flatten", "-alpha", "Off", "-units", "PixelsPerInch", "-density", ppi, "-define", "PNG:exclude-chunk=bkgd,itxt,ztxt,text,chrm", png_path end end |
#render_jpg(jpg_path, ppi: PPI, **options) ⇒ Object
29 30 31 |
# File 'lib/nswtopo/formats.rb', line 29 def render_jpg(jpg_path, ppi: PPI, **) OS.gdal_translate "-of", "JPEG", "-co", "QUALITY=90", "-mo", "EXIF_XResolution=#{ppi}", "-mo", "EXIF_YResolution=#{ppi}", "-mo", "EXIF_ResolutionUnit=2", yield(ppi: ppi), jpg_path end |
#render_kmz(kmz_path, name:, ppi: PPI, **options) ⇒ Object
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
# File 'lib/nswtopo/formats/kmz.rb', line 48 def render_kmz(kmz_path, name:, ppi: PPI, **) metre_resolution = 0.0254 * @scale / ppi degree_resolution = 180.0 * metre_resolution / Math::PI / Kmz::EARTH_RADIUS wgs84_bounds = bounds(projection: Projection.wgs84) wgs84_dimensions = wgs84_bounds.transpose.difference / degree_resolution max_zoom = Math::log2(wgs84_dimensions.max).ceil - Math::log2(Kmz::TILE_SIZE).to_i topleft = [wgs84_bounds[0][0], wgs84_bounds[1][1]] png_path = yield(ppi: ppi) Dir.mktmppath do |temp_dir| pyramid = (0..max_zoom).map do |zoom| resolution = degree_resolution * 2**(max_zoom - zoom) degrees_per_tile = resolution * Kmz::TILE_SIZE counts = (wgs84_bounds.transpose.difference / degrees_per_tile).map(&:ceil) dimensions = counts.times Kmz::TILE_SIZE tfw_path = temp_dir / "#{name}.kmz.zoom.#{zoom}.tfw" tif_path = temp_dir / "#{name}.kmz.zoom.#{zoom}.tif" WorldFile.write topleft, resolution, 0, tfw_path OS.convert "-size", dimensions.join(?x), "canvas:none", "-type", "TrueColorMatte", "-depth", 8, tif_path OS.gdalwarp "-s_srs", @projection, "-t_srs", Projection.wgs84, "-r", "bilinear", "-dstalpha", png_path, tif_path indices_bounds = [topleft, counts, i[+ -]].transpose.map do |coord, count, increment| boundaries = (0..count).map { |index| coord.send increment, index * degrees_per_tile } [boundaries[0..-2], boundaries[1..-1]].transpose.map(&:sort) end.map do |tile_bounds| tile_bounds.each.with_index.to_a end.inject(:product).map(&:transpose).map do |tile_bounds, indices| { indices => tile_bounds } end.inject({}, &:merge) log_update "kmz: resizing image pyramid: %i%%" % (100 * (2**(zoom + 1) - 1) / (2**(max_zoom + 1) - 1)) { zoom => [indices_bounds, tif_path] } end.inject({}, &:merge) kmz_dir = temp_dir.join("#{name}.kmz").tap(&:mkpath) pyramid.map do |zoom, (indices_bounds, tif_path)| zoom_dir = kmz_dir.join(zoom.to_s).tap(&:mkpath) indices_bounds.map do |indices, tile_bounds| index_dir = zoom_dir.join(indices.first.to_s).tap(&:mkpath) tile_kml_path = index_dir / "#{indices.last}.kml" tile_png_path = index_dir / "#{indices.last}.png" xml = REXML::Document.new xml << REXML::XMLDecl.new(1.0, "UTF-8") xml.add_element("kml", "xmlns" => "http://earth.google.com/kml/2.1").tap do |kml| kml.add_element("Document").tap do |document| document.add_element("Style").tap(&Kmz.style) document.add_element("Region").tap(&Kmz.region(tile_bounds, true)) document.add_element("GroundOverlay").tap do || .add_element("drawOrder").text = zoom .add_element("Icon").add_element("href").text = tile_png_path.basename .add_element("LatLonBox").tap(&Kmz.lat_lon_box(tile_bounds)) end if zoom < max_zoom indices.map do |index| [2 * index, 2 * index + 1] end.inject(:product).select do |subindices| pyramid[zoom + 1][0][subindices] end.each do |subindices| path = "../../%i/%i/%i.kml" % [zoom + 1, *subindices] document.add_element("NetworkLink").tap(&Kmz.network_link(pyramid[zoom + 1][0][subindices], path)) end end end end tile_kml_path.write xml crop = "%ix%i+%i+%s" % [Kmz::TILE_SIZE, Kmz::TILE_SIZE, indices[0] * Kmz::TILE_SIZE, indices[1] * Kmz::TILE_SIZE] [tif_path, "-quiet", "+repage", "-crop", crop, "+repage", "+dither", "-type", "PaletteBilevelMatte", "PNG8:#{tile_png_path}"] end end.flatten(1).tap do |tiles| log_update "kmz: creating %i tiles" % tiles.length end.each.concurrently do |args| OS.convert *args end xml = REXML::Document.new xml << REXML::XMLDecl.new(1.0, "UTF-8") xml.add_element("kml", "xmlns" => "http://earth.google.com/kml/2.1").tap do |kml| kml.add_element("Document").tap do |document| document.add_element("LookAt").tap do |look_at| range_x = @extents.first / 2.0 / Math::tan(Kmz::FOV) / Math::cos(Kmz::TILT) range_y = @extents.last / Math::cos(Kmz::FOV - Kmz::TILT) / 2 / (Math::tan(Kmz::FOV - Kmz::TILT) + Math::sin(Kmz::TILT)) names_values = [%w[longitude latitude], wgs84_centre].transpose names_values << ["tilt", Kmz::TILT * 180.0 / Math::PI] << ["range", 1.2 * [range_x, range_y].max] << ["heading", @rotation] names_values.each { |name, value| look_at.add_element(name).text = value } end document.add_element("Name").text = name document.add_element("Style").tap(&Kmz.style) document.add_element("NetworkLink").tap(&Kmz.network_link(pyramid[0][0][[0,0]], "0/0/0.kml")) end end kml_path = kmz_dir / "doc.kml" kml_path.write xml zip kmz_dir, kmz_path end end |
#render_mbtiles(mbtiles_path, name:, zoom: Mbtiles::ZOOM, **options) ⇒ Object
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
# File 'lib/nswtopo/formats/mbtiles.rb', line 7 def render_mbtiles(mbtiles_path, name:, zoom: Mbtiles::ZOOM, **) raise "invalid zoom outside 10-19 range: #{zoom}" unless (10..19) === zoom web_mercator_bounds = bounds(projection: Projection.new("EPSG:3857")) wgs84_bounds = bounds(projection: Projection.wgs84) sql = " CREATE TABLE metadata (name TEXT, value TEXT);\n INSERT INTO metadata VALUES (\"name\", \"\#{name}\");\n INSERT INTO metadata VALUES (\"type\", \"baselayer\");\n INSERT INTO metadata VALUES (\"version\", \"1.1\");\n INSERT INTO metadata VALUES (\"description\", \"\#{name}\");\n INSERT INTO metadata VALUES (\"format\", \"png\");\n INSERT INTO metadata VALUES (\"bounds\", \"\#{wgs84_bounds.transpose.flatten.join ?,}\");\n CREATE TABLE tiles (zoom_level INTEGER, tile_column INTEGER, tile_row INTEGER, tile_data BLOB);\n SQL\n\n Dir.mktmppath do |temp_dir|\n png_path = nil\n zoom.downto(0).inject([]) do |levels, zoom|\n resolution = Mbtiles::RESOLUTION / 2**zoom\n indices, dimensions, topleft = web_mercator_bounds.map do |lower, upper|\n ((lower - Mbtiles::ORIGIN) / resolution / Mbtiles::TILE_SIZE).floor ... ((upper - Mbtiles::ORIGIN) / resolution / Mbtiles::TILE_SIZE).ceil\n end.map.with_index do |indices, axis|\n [indices, (indices.last - indices.first) * Mbtiles::TILE_SIZE, Mbtiles::ORIGIN + (axis.zero? ? indices.first : indices.last) * Mbtiles::TILE_SIZE * resolution]\n end.transpose\n tile_path = temp_dir.join(\"\#{name}.mbtiles.\#{zoom}.%09d.png\").to_s\n levels << [resolution, indices, dimensions, topleft, tile_path, zoom]\n break levels if indices.map(&:size).all? { |size| size < 3 }\n levels\n end.tap do |(resolution, *, zoom), *|\n png_path = yield(resolution: resolution)\n end.tap do |levels|\n log_update \"mbtiles: tiling for zoom levels %s\" % levels.map(&:last).minmax.uniq.join(?-)\n end.each.concurrently do |resolution, indices, dimensions, topleft, tile_path, zoom|\n tif_path, tfw_path = %w[tif tfw].map { |ext| temp_dir / \"\#{name}.mbtiles.\#{zoom}.\#{ext}\" }\n WorldFile.write topleft, resolution, 0, tfw_path\n OS.convert \"-size\", dimensions.join(?x), \"canvas:none\", \"-type\", \"TrueColorAlpha\", \"-depth\", 8, tif_path\n OS.gdalwarp \"-s_srs\", @projection, \"-t_srs\", \"EPSG:3857\", \"-r\", \"lanczos\", \"-dstalpha\", png_path, tif_path\n OS.convert tif_path, \"-quiet\", \"+repage\", \"-crop\", \"\#{Mbtiles::TILE_SIZE}x\#{Mbtiles::TILE_SIZE}\", tile_path\n end.map do |resolution, indices, dimensions, topleft, tile_path, zoom|\n indices[1].to_a.reverse.product(indices[0].to_a).map.with_index do |(row, col), index|\n [tile_path % index, zoom, col, row]\n end\n end.flatten(1).each do |tile_path, zoom, col, row|\n sql << %Q[INSERT INTO tiles VALUES (\#{zoom}, \#{col}, \#{row}, readfile(\"\#{tile_path}\"));\\n]\n end.tap do |tiles|\n log_update \"mbtiles: optimising %i tiles\" % tiles.length\n end.map(&:first).each.concurrent_groups do |png_paths|\n dither *png_paths\n end\n OS.sqlite3 mbtiles_path do |stdin|\n stdin.puts sql\n stdin.puts \".exit\"\n end\n end\nend\n" |
#render_pdf(pdf_path, ppi: nil, external: nil, **options) ⇒ Object
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
# File 'lib/nswtopo/formats/pdf.rb', line 3 def render_pdf(pdf_path, ppi: nil, external: nil, **) if ppi OS.gdal_translate "-a_srs", @projection, "-of", "PDF", "-co", "DPI=#{ppi}", "-co", "MARGIN=0", "-co", "CREATOR=nswtopo", "-co", "GEO_ENCODING=ISO32000", yield(ppi: ppi), pdf_path else Dir.mktmppath do |temp_dir| svg_path = temp_dir / "pdf-map.svg" render_svg svg_path, external: external xml = REXML::Document.new svg_path.read style = "@media print { @page { margin: 0 0 -1mm 0; size: %s %s; } }" svg = xml.elements["svg"] svg.add_element("style").text = style % svg.attributes.values_at("width", "height") svg_path.write xml FileUtils.rm pdf_path if pdf_path.exist? NSWTopo.with_browser do |browser_name, browser_path| args = case browser_name when "chrome" ["--headless", "--disable-gpu", "--print-to-pdf=#{pdf_path}"] when "firefox" raise "can't create vector PDF with firefox; use chrome or specify ppi for a raster PDF" end stdout, stderr, status = Open3.capture3 browser_path.to_s, *args, "file://#{svg_path}" raise "couldn't create PDF using %s" % browser_name unless status.success? && pdf_path.file? end end end end |
#render_png(png_path, ppi: PPI, dither: false, **options) ⇒ Object
21 22 23 |
# File 'lib/nswtopo/formats.rb', line 21 def render_png(png_path, ppi: PPI, dither: false, **) FileUtils.cp yield(ppi: ppi, dither: dither), png_path end |
#render_svg(svg_path, external: nil, **options) ⇒ Object
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
# File 'lib/nswtopo/formats/svg.rb', line 3 def render_svg(svg_path, external: nil, **) case when external raise "not a file: %s" % external unless external.file? begin svg = REXML::Document.new(external.read).elements["svg"] raise "not an SVG file: %s" % external unless svg desc = svg.elements["metadata/rdf:RDF/rdf:Description[@dc:creator='nswtopo']"] raise "not an nswtopo SVG file: %s" % external unless desc rescue REXML::ParseException raise "not an SVG file: %s" % external end FileUtils.cp external, svg_path when @archive.uptodate?("map.svg", "map.yml") svg_path.write @archive.read("map.svg") else width, height = extents.times(1000.0 / scale) xml = REXML::Document.new xml << REXML::XMLDecl.new(1.0, "utf-8") svg = xml.add_element "svg", "version" => 1.1, "baseProfile" => "full", "width" => "#{width}mm", "height" => "#{height}mm", "viewBox" => "0 0 #{width} #{height}", "xmlns" => "http://www.w3.org/2000/svg", "xmlns:xlink" => "http://www.w3.org/1999/xlink", "xmlns:sodipodi" => "http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd", "xmlns:inkscape" => "http://www.inkscape.org/namespaces/inkscape" = svg.add_element "metadata" rdf = .add_element "rdf:RDF", "xmlns:rdf" => "http://www.w3.org/1999/02/22-rdf-syntax-ns#", "xmlns:dc" => "http://purl.org/dc/elements/1.1/" rdf.add_element "rdf:Description", "dc:date" => Date.today.iso8601, "dc:format" => "image/svg+xml", "dc:creator" => "nswtopo" defs = svg.add_element "defs" svg.add_element "sodipodi:namedview", "borderlayer" => true svg.add_element "rect", "x" => 0, "y" => 0, "width" => width, "height" => height, "fill" => "white" labels = Layer.new "labels", self, Config.fetch("labels", {}).merge("type" => "Labels") layers.reject(&:empty?).each do |layer| next if Config["labelling"] == false labels.add layer if Vector === layer end.push(labels).each do |layer| log_update "compositing: #{layer.name}" group = svg.add_element "g", "id" => layer.name, "inkscape:groupmode" => "layer" layer.render group, defs, &labels.method(:add_fence) end until xml.elements.each("svg//g[not(*)]", &:remove).empty? do end string, formatter = String.new, REXML::Formatters::Pretty.new formatter.compact = true formatter.write xml, string write "map.svg", string svg_path.write string end end |
#render_svgz(svgz_path, external: nil, **options) ⇒ Object
3 4 5 6 7 8 9 10 11 |
# File 'lib/nswtopo/formats/svgz.rb', line 3 def render_svgz(svgz_path, external: nil, **) Dir.mktmppath do |temp_dir| svg_path = temp_dir / "svgz-map.svg" render_svg svg_path, external: external Zlib::GzipWriter.open svgz_path do |gz| gz.write svg_path.binread end end end |
#render_tif(tif_path, ppi: PPI, dither: false, **options) ⇒ Object
25 26 27 |
# File 'lib/nswtopo/formats.rb', line 25 def render_tif(tif_path, ppi: PPI, dither: false, **) OS.gdal_translate "-of", "GTiff", "-co", "COMPRESS=DEFLATE", "-co", "ZLEVEL=9", "-a_srs", @projection, yield(ppi: ppi, dither: dither), tif_path end |
#render_zip(zip_path, name:, ppi: PPI, **options) ⇒ Object
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
# File 'lib/nswtopo/formats/zip.rb', line 3 def render_zip(zip_path, name:, ppi: PPI, **) Dir.mktmppath do |temp_dir| zip_dir = temp_dir.join("#{name}.avenza").tap(&:mkpath) tiles_dir = zip_dir.join("tiles").tap(&:mkpath) png_path = yield(ppi: ppi) top_left = bounding_box.coordinates[0][3] 2.downto(0).map.with_index do |level, index| [level, index, ppi.to_f / 2**index] end.each.concurrently do |level, index, ppi| dimensions, ppi, resolution = raster_dimensions_at ppi: ppi img_path = index.zero? ? png_path : temp_dir / "#{name}.avenza.#{level}.png" tile_path = temp_dir.join("#{name}.avenza.tile.#{level}.%09d.png").to_s OS.convert png_path, "-filter", "Lanczos", "-resize", "%ix%i!" % dimensions, img_path unless img_path.exist? OS.convert img_path, "+repage", "-crop", "256x256", tile_path dimensions.reverse.map do |dimension| 0.upto((dimension - 1) / 256).to_a end.inject(&:product).each.with_index do |(y, x), n| FileUtils.cp tile_path % n, tiles_dir / "#{level}x#{y}x#{x}.png" end zip_dir.join("#{name}.ref").open("w") do |file| file.puts @projection.wkt_simple file.puts WorldFile.geotransform(top_left, resolution, -@rotation).flatten.join(?,) file << dimensions.join(?,) end if index == 1 end Pathname.glob(tiles_dir / "*.png").each.concurrent_groups do |tile_paths| dither *tile_paths end OS.convert png_path, "-thumbnail", "64x64", "-gravity", "center", "-background", "white", "-extent", "64x64", "-alpha", "Remove", "-type", "TrueColor", zip_dir / "thumb.png" zip zip_dir, zip_path end end |