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/gemf.rb,
lib/nswtopo/formats/svgz.rb,
lib/nswtopo/formats/mbtiles.rb
Defined Under Namespace
Modules: Kmz
Constant Summary collapse
- PPI =
300
- TILE =
1500
- CHROME_ARGS =
%w[--force-gpu-mem-available-mb=4096]
- CHROME_INSTANCES =
(ThreadPool::CORES / 4).clamp(1, 6)
Constants included from Log
Log::FAILURE, Log::NEUTRAL, Log::SUCCESS, Log::UPDATE
Class Method Summary collapse
Instance Method Summary collapse
- #rasterise(png_path, background:, ppi: nil, resolution: nil) ⇒ Object
- #render_gemf(gemf_path, name:, **options, &block) ⇒ Object
- #render_jpg(jpg_path, ppi: PPI, **options) ⇒ Object
- #render_kmz(kmz_path, name:, ppi: PPI, **options) ⇒ Object
- #render_mbtiles(mbtiles_path, name:, **options, &block) ⇒ Object
- #render_pdf(pdf_path, ppi: nil, background:, **options) ⇒ Object
- #render_png(png_path, ppi: PPI, dither: false, **options) ⇒ Object
- #render_svg(svg_path, background:, **options) ⇒ Object
- #render_svgz(svgz_path, background:, **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
23 24 25 |
# File 'lib/nswtopo/formats.rb', line 23 def self.===(ext) extensions.any? ext end |
.extensions ⇒ Object
19 20 21 |
# File 'lib/nswtopo/formats.rb', line 19 def self.extensions instance_methods.grep(/^render_([a-z]+)/) { $1 } end |
Instance Method Details
#rasterise(png_path, background:, ppi: nil, resolution: nil) ⇒ Object
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 |
# File 'lib/nswtopo/formats.rb', line 59 def rasterise(png_path, background:, ppi: nil, resolution: nil) Dir.mktmppath do |temp_dir| svg_path = temp_dir / "map.svg" vrt_path = temp_dir / "map.vrt" render_svg svg_path, background: background case when ppi ppi_info = "%i ppi" % ppi mm_per_px = 25.4 / ppi when resolution ppi_info = "%.1f m/px" % resolution mm_per_px = to_mm(resolution) end = [TILE * mm_per_px] * 2 raster_size = @dimensions.map { |dimension| (dimension / mm_per_px).ceil } megapixels = raster_size.inject(&:*) / 1024.0 / 1024.0 raster_info = "%i×%i (%.1fMpx) map raster at %s" % [*raster_size, megapixels, ppi_info] log_update "chrome: creating #{raster_info}" raster_size.map do |px| (0...px).step(TILE).map do |px| [px, px * mm_per_px] end end.inject(&:product).map(&:transpose).map do |raster_offset, | next raster_offset, , temp_dir.join("tile.%i.%i.png" % raster_offset) end.inject(ThreadPool.new(CHROME_INSTANCES), &:<<).in_groups do |*grid| NSWTopo::Chrome.with_browser "file://#{svg_path}", width: TILE, height: TILE, args: CHROME_ARGS do |browser| svg = browser.query_selector "svg" svg[:width], svg[:height] = nil, nil grid.each do |raster_offset, , tile_path| svg[:viewBox] = [*, *].join(?\s) browser.screenshot tile_path end end end.map do |raster_offset, , tile_path| REXML::Document.new(OS.gdal_translate "-of", "VRT", tile_path, "/vsistdout/").tap do |vrt| vrt.elements.each("VRTDataset/VRTRasterBand/SimpleSource/DstRect") do |dst_rect| dst_rect.add_attributes "xOff" => raster_offset[0], "yOff" => raster_offset[1] end end end.inject do |vrt, tile_vrt| vrt.elements["VRTDataset/VRTRasterBand[@band='1']"].add_element tile_vrt.elements["VRTDataset/VRTRasterBand[@band='1']/SimpleSource"] vrt.elements["VRTDataset/VRTRasterBand[@band='2']"].add_element tile_vrt.elements["VRTDataset/VRTRasterBand[@band='2']/SimpleSource"] vrt.elements["VRTDataset/VRTRasterBand[@band='3']"].add_element tile_vrt.elements["VRTDataset/VRTRasterBand[@band='3']/SimpleSource"] vrt.elements["VRTDataset/VRTRasterBand[@band='4']"].add_element tile_vrt.elements["VRTDataset/VRTRasterBand[@band='4']/SimpleSource"] vrt end.tap do |vrt| vrt.elements.each("VRTDataset/VRTRasterBand/@blockYSize", &:remove) vrt.elements.each("VRTDataset/Metadata", &:remove) vrt.elements["VRTDataset"].add_attributes "rasterXSize" => raster_size[0], "rasterYSize" => raster_size[1] File.write vrt_path, vrt end log_update "nswtopo: finalising #{raster_info}" OS.gdal_translate vrt_path, png_path end end |
#render_gemf(gemf_path, name:, **options, &block) ⇒ Object
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 |
# File 'lib/nswtopo/formats/gemf.rb', line 4 def render_gemf(gemf_path, name:, **, &block) Dir.mktmppath do |temp_dir| ranges = tiled_web_map(temp_dir, **, extension: "gemf", &block).sort_by do |tile| [tile.col, tile.row] end.group_by(&:zoom) header, source = "", "nswtopo" # 3.1 overall header: header << [4, 256].pack("L>L>") # 3.2 sources: header << [1, 0, source.bytesize, source].pack("L>L>L>a#{source.bytesize}") # 3.3 number of ranges: header << [ranges.length].pack("L>") offset = header.bytesize + ranges.size * 32 paths = ranges.each do |zoom, tiles| cols = tiles.map(&:col) rows = tiles.map(&:row) # 3.3 range data: header << [zoom, *cols.minmax, *rows.minmax, 0, offset].pack("L>L>L>L>L>L>Q>") offset += tiles.size * 12 end.each do |zoom, tiles| # 3.4 range details: tiles.each do |tile| header << [offset, tile.path.size].pack("Q>L>") offset += tile.path.size end end.values.flatten.map(&:path) gemf_path.open("wb") do |file| file.write header # 4 data area: paths.each do |path| file.write path.binread end end end end |
#render_jpg(jpg_path, ppi: PPI, **options) ⇒ Object
49 50 51 52 53 54 55 56 57 |
# File 'lib/nswtopo/formats.rb', line 49 def render_jpg(jpg_path, ppi: PPI, **) OS.gdal_translate yield(ppi: ppi), *%W[ -of JPEG -co QUALITY=90 -mo EXIF_XResolution=#{ppi} -mo EXIF_YResolution=#{ppi} -mo EXIF_ResolutionUnit=2 ], jpg_path end |
#render_kmz(kmz_path, name:, ppi: PPI, **options) ⇒ Object
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 148 149 150 151 152 153 154 155 |
# File 'lib/nswtopo/formats/kmz.rb', line 50 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 = @cutline.reproject_to_wgs84.bounds wgs84_dimensions = wgs84_bounds.map do |min, max| (max - min) / degree_resolution end max_zoom = Math::log2(wgs84_dimensions.max).ceil - Math::log2(Kmz::TILE_SIZE).to_i png_path = yield(ppi: ppi) Dir.mktmppath do |temp_dir| log_update "kmz: resizing image pyramid" pyramid = (0..max_zoom).map do |zoom| resolution = degree_resolution * 2**(max_zoom - zoom) tif_path = temp_dir / "#{name}.kmz.zoom.#{zoom}.tif" next zoom, resolution, tif_path end.inject(ThreadPool.new, &:<<).each do |zoom, resolution, tif_path| OS.gdalwarp "-t_srs", "EPSG:4326", "-tr", resolution, resolution, "-r", "bilinear", "-dstalpha", png_path, tif_path end.map do |zoom, resolution, tif_path| degrees_per_tile = resolution * Kmz::TILE_SIZE corners = JSON.parse(OS.gdalinfo "-json", tif_path)["cornerCoordinates"] top_left = corners["upperLeft"] counts = corners.values.transpose.map(&:minmax).map do |min, max| (max - min) / degrees_per_tile end.map(&:ceil) indices_bounds = [top_left, 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.entries end.inject(:product).map(&:transpose).map(&:reverse).to_h next zoom, [indices_bounds, tif_path] end.to_h kmz_dir = temp_dir.join("#{name}.kmz").tap(&:mkpath) pyramid.flat_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 ["-srcwin", indices[0] * Kmz::TILE_SIZE, indices[1] * Kmz::TILE_SIZE, Kmz::TILE_SIZE, Kmz::TILE_SIZE, tif_path, tile_png_path] end end.tap do |tiles| log_update "kmz: creating %i tiles" % tiles.length end.inject(ThreadPool.new, &:<<).each do |*args| OS.gdal_translate "--config", "GDAL_PAM_ENABLED", "NO", *args end.map(&:last).inject(ThreadPool.new, &:<<).in_groups do |*tile_png_paths| dither *tile_png_paths rescue Dither::Missing 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| extents = @dimensions.map { |dimension| dimension * @scale / 1000.0 } 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], @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:, **options, &block) ⇒ Object
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
# File 'lib/nswtopo/formats/mbtiles.rb', line 4 def render_mbtiles(mbtiles_path, name:, **, &block) wgs84_bounds = @cutline.reproject_to_wgs84.bounds 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| tiled_web_map(temp_dir, **, extension: "mbtiles", &block).each do |tile| sql << %Q[INSERT INTO tiles VALUES (#{tile.zoom}, #{tile.col}, #{tile.row}, readfile("#{tile.path}"));\n] end OS.sqlite3 mbtiles_path do |stdin| stdin.puts sql stdin.puts ".exit" end end end |
#render_pdf(pdf_path, ppi: nil, background:, **options) ⇒ Object
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 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 |
# File 'lib/nswtopo/formats/pdf.rb', line 4 def render_pdf(pdf_path, ppi: nil, background:, **) if ppi OS.gdal_translate "-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, background: background REXML::Document.new(svg_path.read).tap do |xml| xml.elements["svg"].tap do |svg| style = "@media print { @page { margin: 0 0 -1mm 0; size: %s %s; } }" svg.add_element("style").text = style % svg.attributes.values_at("width", "height") end # replace fill pattern paint with manual pattern mosaic to work around Chrome PDF bug xml.elements.each("//svg//use[@id][@fill][@href]") do |use| id = use.attributes["id"] # find the pattern id, content id, pattern element and content element next unless /^url\(#(?<pattern_id>.*)\)$/ =~ use.attributes["fill"] next unless /^#(?<content_id>.*)$/ =~ use.attributes["href"] next unless pattern = use.elements["preceding::defs/pattern[@id='#{pattern_id}'][@width][@height]"] next unless content = use.elements["preceding::defs/g[@id='#{content_id}']"] # change pattern element to a group pattern.attributes.delete "patternUnits" pattern.name = "g" # create a clip path to apply to the fill pattern mosaic content_clip = REXML::Element.new "clipPath" content_clip.add_attribute "id", "#{content_id}.clip" # create a clip path to apply to pattern element pattern_clip = REXML::Element.new "clipPath" pattern_clip.add_attribute "id", "#{pattern_id}.clip" pattern.add_attribute "clip-path", "url(##{pattern_id}.clip)" # move content and clip paths into defs pattern.previous_sibling = pattern_clip pattern.next_sibling = content content.next_sibling = content_clip # replace fill paint with a container for the fill pattern mosaic fill = REXML::Element.new "g" fill.add_attribute "clip-path", "url(##{content_id}.clip)" fill.add_attribute "id", "#{id}.fill" use.previous_sibling = fill use.add_attribute "fill", "none" xml.elements.each("//use[@href='##{id}']") do |use| use_fill = REXML::Element.new "use" use_fill.add_attribute "href", "##{id}.fill" use.previous_sibling = use_fill end # get pattern size pattern_size = %w[width height].map do |name| pattern.attributes[name].tap { pattern.attributes.delete name } end.map(&:to_f) # create pattern clip pattern_size.each.with_object(0).inject(&:product).values_at(3,2,0,1).tap do |corners| pattern_clip.add_element "path", "d" => %w[M L L L].zip(corners).push("Z").join(?\s) end # add paths to content clip, get content coverage area, and create fill pattern mosaic content.elements.collect("path[@d]", &:itself).each.with_index do |path, index| path.add_attribute "id", "#{content_id}.#{index}" content_clip.add_element "use", "href" => "##{content_id}.#{index}" end.flat_map do |path| path.attributes["d"].scan /(\d+(?:\.\d+)?) (\d+(?:\.\d+)?)/ end.transpose.map do |coords| coords.map(&:to_f).minmax end.zip(pattern_size).map do |(min, max), size| (min...max).step(size).entries end.inject(&:product).each do |x, y| fill.add_element "use", "href" => "##{pattern_id}", "x" => x, "y" => y end end svg_path.write xml end log_update "chrome: rendering PDF" Chrome.with_browser("file://#{svg_path}") do |browser| browser.print_to_pdf(pdf_path) do |doc| bbox = [0, 0, dimensions[0] * 72 / 25.4, dimensions[1] * 72 / 25.4] bounds = cutline.coordinates[0][...-1].map do |coords| coords.zip(dimensions).map { |coord, dimension| coord / dimension } end.flatten lpts = [0, 0].zip( [1, 1]).inject(&:product).values_at(0,1,3,2).flatten gpts = [0, 0].zip(dimensions).inject(&:product).values_at(0,1,3,2).then do |corners| # ISO 32000-2 specifies projected coordinates instead of WGS84, but not observed in practice GeoJSON.multipoint(corners, projection: projection).reproject_to_wgs84.coordinates.map(&:to_a).map(&:reverse) end.flatten pcsm = [25.4/72, 0, 0, 0, 25.4/72, 0, 0, 0, 1, 0, 0, 0] doc.pages.first[:VP] = [doc.add({ Type: :Viewport, BBox: bbox, Measure: doc.add({ Type: :Measure, Subtype: :GEO, Bounds: bounds, GCS: doc.add({ Type: :PROJCS, WKT: projection.wkt2 }), GPTS: gpts, LPTS: lpts, PCSM: pcsm }) })] doc.trailer.info[:Creator] = "nswtopo" doc.version = "1.7" end end end end end |
#render_png(png_path, ppi: PPI, dither: false, **options) ⇒ Object
27 28 29 30 31 32 33 34 35 36 |
# File 'lib/nswtopo/formats.rb', line 27 def render_png(png_path, ppi: PPI, dither: false, **) ppm = (ppi / 0.0254).round OS.exiftool yield(ppi: ppi, dither: dither), *%W[ -PNG:PixelsPerUnitX=#{ppm} -PNG:PixelsPerUnitY=#{ppm} -o #{png_path} ] rescue OS::Missing FileUtils.cp yield(ppi: ppi, dither: dither), png_path end |
#render_svg(svg_path, background:, **options) ⇒ Object
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 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 |
# File 'lib/nswtopo/formats/svg.rb', line 20 def render_svg(svg_path, background:, **) if uptodate?("map.svg", "map.yml") log_update "nswtopo: reading existing map SVG" xml = REXML::Document.new read("map.svg") xml.elements["svg/metadata/rdf:RDF/rdf:Description"].add_attributes("xmp:ModifyDate" => Time.now.iso8601) else width, height = @dimensions xml = REXML::Document.new xml << REXML::XMLDecl.new(1.0, "utf-8") svg = xml.add_element "svg", "width" => "#{width}mm", "height" => "#{height}mm", "viewBox" => "0 0 #{width} #{height}", "text-rendering" => "geometricPrecision", "xmlns" => "http://www.w3.org/2000/svg", "xmlns:nswtopo" => "http://nswtopo.com" = svg.add_element("metadata") .add_element("nswtopo:map", "projection" => @neatline.projection.wkt2, "neatline" => @neatline.coordinates.to_json, "centre" => @centre.to_json, "scale" => @scale, "rotation" => @rotation ) .add_element("rdf:RDF", "xmlns:rdf" => "http://www.w3.org/1999/02/22-rdf-syntax-ns#", "xmlns:xmp" => "http://ns.adobe.com/xap/1.0/", "xmlns:dc" => "http://purl.org/dc/elements/1.1/" ).add_element("rdf:Description", "xmp:CreatorTool" => VERSION.creator_string, "dc:format" => "image/svg+xml" ) # add defs for map filters and masks defs = svg.add_element("defs", "id" => "map.defs") defs.add_element("rect", "id" => "map.rect", "width" => width, "height" => height) defs.add_element("path", "id" => "map.neatline", "d" => @neatline.svg_path_data) defs.add_element("clipPath", "id" => "map.clip").add_element("use", "href" => "#map.neatline") # add a filter converting alpha channel to cutout mask defs.add_element("filter", "id" => "map.filter.cutout").tap do |filter| filter.add_element("feComponentTransfer", "in" => "SourceAlpha") end Enumerator.new do |yielder| labels = Layer.new "labels", self, Config.fetch("labels", {}).merge("type" => "Labels") layers.reject do |layer| log_update "reading: #{layer.name}" layer.empty? end.each do |layer| next if Config["labelling"] == false labels.add layer if VectorRender === layer end.push(labels).each.with_object [[], []] do |layer, (cutouts, knockouts)| log_update "compositing: #{layer.name}" new_knockouts, knockout = [], "map.mask.knockout.#{knockouts.length+1}" layer.render(cutouts: cutouts, knockout: knockout) do |object| case object when Labels::ConvexHulls then labels << object when VectorRender::Cutout then cutouts << object when VectorRender::Knockout then new_knockouts << object when REXML::Element object.attributes["mask"] ||= "url(#map.mask.knockout.#{knockouts.length})" unless "defs" == object.name yielder << object end end knockouts << new_knockouts if new_knockouts.any? end.last.push([]).each.with_index do |knockouts, index| mask = defs.add_element("mask", "id" => "map.mask.knockout.#{index}") content = mask.add_element("g", "id" => "map.mask.knockout.#{index}.content") content.add_element("use", "href" => "#map.mask.knockout.#{index+1}.content") if knockouts.any? content.add_element("use", "href" => "#map.rect", "fill" => "white", "stroke" => "none") if knockouts.none? knockouts.group_by(&:buffer).map do |buffer, knockouts| group = content.add_element("g", "filter" => "url(#map.filter.knockout.#{buffer})") knockouts.each do |knockout| group.add_element knockout.use end end end.flatten.group_by(&:buffer).keys.each do |buffer| filter = defs.add_element("filter", "id" => "map.filter.knockout.#{buffer}") 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") filter.add_element("feMorphology", "operator" => "dilate", "radius" => buffer) unless buffer.zero? filter.add_element("feComponentTransfer").add_element("feFuncA", "type" => "discrete", "tableValues" => "0 1") end end.reject do |element| svg.add_element(element) if "defs" == element.name end.tap do svg.add_element("use", "id" => "map.background", "href" => "#map.neatline", "fill" => "white") end.chunk do |element| element.attributes["mask"] end.each.with_object(svg.add_element("g", "clip-path" => "url(#map.clip)")) do |(mask, elements), clip_group| elements.each.with_object(clip_group.add_element("g", "mask" => mask)) do |element, mask_group| mask_group.add_element element element.delete_attribute "mask" end end xml.elements.each("svg//defs[not(*)]", &:remove) xml.elements["svg/metadata/rdf:RDF/rdf:Description"].add_attributes %w[xmp:ModifyDate xmp:CreateDate].each.with_object(Time.now.iso8601).to_h write "map.svg", xml.to_s end xml.elements["svg/use[@id='map.background']"].add_attributes("fill" => background) if background svg_path.open("w") do |file| SVGFormatter.new.write xml, file end end |
#render_svgz(svgz_path, background:, **options) ⇒ Object
4 5 6 7 8 9 10 11 12 |
# File 'lib/nswtopo/formats/svgz.rb', line 4 def render_svgz(svgz_path, background:, **) Dir.mktmppath do |temp_dir| svg_path = temp_dir / "svgz-map.svg" render_svg svg_path, background: background 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
38 39 40 41 42 43 44 45 46 47 |
# File 'lib/nswtopo/formats.rb', line 38 def render_tif(tif_path, ppi: PPI, dither: false, **) OS.gdal_translate yield(ppi: ppi, dither: dither), *%W[ -of GTiff -co COMPRESS=DEFLATE -co ZLEVEL=9 -mo TIFFTAG_XRESOLUTION=#{ppi} -mo TIFFTAG_YRESOLUTION=#{ppi} -mo TIFFTAG_RESOLUTIONUNIT=2 ], tif_path end |
#render_zip(zip_path, name:, ppi: PPI, **options) ⇒ Object
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 |
# File 'lib/nswtopo/formats/zip.rb', line 4 def render_zip(zip_path, name:, ppi: PPI, **) Dir.mktmppath do |temp_dir| zip_dir = temp_dir.join("zip").tap(&:mkpath) tiles_dir = zip_dir.join("tiles").tap(&:mkpath) png_path = yield(ppi: ppi) 2.downto(0).map.with_index do |level, index| geo_transform = geotransform(ppi: ppi / 2**index) outsize = @dimensions.map { |dimension| (dimension / geo_transform[1]).ceil } case index when 0 thumb_size = outsize.inject(&:<) ? [0, 64] : [64, 0] OS.gdal_translate *%w[--config GDAL_PAM_ENABLED NO -r bilinear -outsize], *thumb_size, png_path, zip_dir / "thumb.png" when 1 zip_dir.join("#{name}.ref").open("w") do |file| file.puts @projection.wkt2 file.puts geo_transform.join(?,) file.puts outsize.join(?,) end end img_path = index.zero? ? png_path : temp_dir / "map.#{level}.png" next level, outsize, img_path end.inject(ThreadPool.new, &:<<).each do |level, outsize, img_path| OS.gdal_translate *%w[-r bicubic -outsize], *outsize, png_path, img_path unless img_path.exist? end.flat_map do |level, outsize, img_path| outsize.map do |px| (0...px).step(256).with_index.entries end.inject(&:product).map do |(col, j), (row, i)| tile_path = tiles_dir / "#{level}x#{i}x#{j}.png" size = [-col, -row].zip(outsize).map(&:sum).zip([256, 256]).map(&:min) %w[--config GDAL_PAM_ENABLED NO -srcwin] + [col, row, *size, img_path, tile_path] end end.tap do |tiles| log_update "zip: creating %i tiles" % tiles.length end.inject(ThreadPool.new, &:<<).each do |*args| OS.gdal_translate *args end.map(&:last).tap do |tile_paths| log_update "zip: optimising %i tiles" % tile_paths.length end.inject(ThreadPool.new, &:<<).in_groups do |*tile_paths| dither *tile_paths rescue Dither::Missing end zip zip_dir, zip_path end end |