module NSWTopo::Formats
Constants
- PPI
Public Class Methods
===(ext)
click to toggle source
# File lib/nswtopo/formats.rb, line 17 def self.===(ext) extensions.any? ext end
extensions()
click to toggle source
# File lib/nswtopo/formats.rb, line 13 def self.extensions instance_methods.grep(/^render_([a-z]+)/) { $1 } end
Public Instance Methods
rasterise(png_path, external:, **options)
click to toggle source
# File lib/nswtopo/formats.rb, line 33 def rasterise(png_path, external:, **options) Dir.mktmppath do |temp_dir| dimensions, ppi, resolution = raster_dimensions_at **options 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, options[:ppi] ] if options[:ppi] log_update "%s: creating %i×%i (%.1fMpx) map raster at %.1f m/px" % [browser_name, *dimensions, megapixels, options[:resolution]] if options[: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) { |ppi: ppi| ... }
click to toggle source
# File lib/nswtopo/formats.rb, line 29 def render_jpg(jpg_path, ppi: PPI, **options) 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) { |ppi: ppi| ... }
click to toggle source
# File lib/nswtopo/formats/kmz.rb, line 48 def render_kmz(kmz_path, name:, ppi: PPI, **options) 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 |overlay| overlay.add_element("drawOrder").text = zoom overlay.add_element("Icon").add_element("href").text = tile_png_path.basename overlay.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) { |resolution: resolution| ... }
click to toggle source
# File lib/nswtopo/formats/mbtiles.rb, line 7 def render_mbtiles(mbtiles_path, name:, zoom: Mbtiles::ZOOM, **options) 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 = <<~SQL CREATE TABLE metadata (name TEXT, value TEXT); INSERT INTO metadata VALUES ("name", "#{name}"); INSERT INTO metadata VALUES ("type", "baselayer"); INSERT INTO metadata VALUES ("version", "1.1"); INSERT INTO metadata VALUES ("description", "#{name}"); INSERT INTO metadata VALUES ("format", "png"); INSERT INTO metadata VALUES ("bounds", "#{wgs84_bounds.transpose.flatten.join ?,}"); CREATE TABLE tiles (zoom_level INTEGER, tile_column INTEGER, tile_row INTEGER, tile_data BLOB); SQL Dir.mktmppath do |temp_dir| png_path = nil zoom.downto(0).inject([]) do |levels, zoom| resolution = Mbtiles::RESOLUTION / 2**zoom indices, dimensions, topleft = web_mercator_bounds.map do |lower, upper| ((lower - Mbtiles::ORIGIN) / resolution / Mbtiles::TILE_SIZE).floor ... ((upper - Mbtiles::ORIGIN) / resolution / Mbtiles::TILE_SIZE).ceil end.map.with_index do |indices, axis| [indices, (indices.last - indices.first) * Mbtiles::TILE_SIZE, Mbtiles::ORIGIN + (axis.zero? ? indices.first : indices.last) * Mbtiles::TILE_SIZE * resolution] end.transpose tile_path = temp_dir.join("#{name}.mbtiles.#{zoom}.%09d.png").to_s levels << [resolution, indices, dimensions, topleft, tile_path, zoom] break levels if indices.map(&:size).all? { |size| size < 3 } levels end.tap do |(resolution, *, zoom), *| png_path = yield(resolution: resolution) end.tap do |levels| log_update "mbtiles: tiling for zoom levels %s" % levels.map(&:last).minmax.uniq.join(?-) end.each.concurrently do |resolution, indices, dimensions, topleft, tile_path, zoom| tif_path, tfw_path = %w[tif tfw].map { |ext| temp_dir / "#{name}.mbtiles.#{zoom}.#{ext}" } WorldFile.write topleft, resolution, 0, tfw_path OS.convert "-size", dimensions.join(?x), "canvas:none", "-type", "TrueColorAlpha", "-depth", 8, tif_path OS.gdalwarp "-s_srs", @projection, "-t_srs", "EPSG:3857", "-r", "lanczos", "-dstalpha", png_path, tif_path OS.convert tif_path, "-quiet", "+repage", "-crop", "#{Mbtiles::TILE_SIZE}x#{Mbtiles::TILE_SIZE}", tile_path end.map do |resolution, indices, dimensions, topleft, tile_path, zoom| indices[1].to_a.reverse.product(indices[0].to_a).map.with_index do |(row, col), index| [tile_path % index, zoom, col, row] end end.flatten(1).each do |tile_path, zoom, col, row| sql << %Q[INSERT INTO tiles VALUES (#{zoom}, #{col}, #{row}, readfile("#{tile_path}"));\n] end.tap do |tiles| log_update "mbtiles: optimising %i tiles" % tiles.length end.map(&:first).each.concurrent_groups do |png_paths| dither *png_paths end OS.sqlite3 mbtiles_path do |stdin| stdin.puts sql stdin.puts ".exit" end end end
render_pdf(pdf_path, ppi: nil, external: nil, **options) { |ppi: ppi| ... }
click to toggle source
# File lib/nswtopo/formats/pdf.rb, line 3 def render_pdf(pdf_path, ppi: nil, external: nil, **options) 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) { |ppi: ppi, dither: dither| ... }
click to toggle source
# File lib/nswtopo/formats.rb, line 21 def render_png(png_path, ppi: PPI, dither: false, **options) FileUtils.cp yield(ppi: ppi, dither: dither), png_path end
render_svg(svg_path, external: nil, **options)
click to toggle source
# File lib/nswtopo/formats/svg.rb, line 3 def render_svg(svg_path, external: nil, **options) 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" meta = svg.add_element "metadata" rdf = meta.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)
click to toggle source
# File lib/nswtopo/formats/svgz.rb, line 3 def render_svgz(svgz_path, external: nil, **options) 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) { |ppi: ppi, dither: dither| ... }
click to toggle source
# File lib/nswtopo/formats.rb, line 25 def render_tif(tif_path, ppi: PPI, dither: false, **options) 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) { |ppi: ppi| ... }
click to toggle source
# File lib/nswtopo/formats/zip.rb, line 3 def render_zip(zip_path, name:, ppi: PPI, **options) 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