nswtopo 3.0.1 → 3.1.1
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/bin/nswtopo +20 -4
- data/docs/contours.md +2 -0
- data/docs/relief.md +2 -3
- data/docs/spot-heights.md +2 -0
- data/lib/nswtopo/archive.rb +6 -3
- data/lib/nswtopo/chrome.rb +9 -6
- data/lib/nswtopo/commands/layers.rb +2 -2
- data/lib/nswtopo/config.rb +1 -0
- data/lib/nswtopo/formats/gemf.rb +1 -0
- data/lib/nswtopo/formats/kmz.rb +16 -10
- data/lib/nswtopo/formats/mbtiles.rb +1 -0
- data/lib/nswtopo/formats/pdf.rb +4 -3
- data/lib/nswtopo/formats/svg.rb +5 -13
- data/lib/nswtopo/formats/svgz.rb +1 -0
- data/lib/nswtopo/formats/zip.rb +5 -4
- data/lib/nswtopo/formats.rb +35 -36
- data/lib/nswtopo/geometry/r_tree.rb +24 -23
- data/lib/nswtopo/geometry/straight_skeleton/node.rb +4 -4
- data/lib/nswtopo/geometry/straight_skeleton/nodes.rb +51 -40
- data/lib/nswtopo/geometry/straight_skeleton/split.rb +2 -2
- data/lib/nswtopo/geometry/vector.rb +55 -49
- data/lib/nswtopo/geometry.rb +0 -5
- data/lib/nswtopo/gis/arcgis/layer/map.rb +11 -10
- data/lib/nswtopo/gis/arcgis/layer/query.rb +8 -10
- data/lib/nswtopo/gis/arcgis/layer.rb +7 -11
- data/lib/nswtopo/gis/dem.rb +3 -2
- data/lib/nswtopo/gis/gdal_glob.rb +3 -3
- data/lib/nswtopo/gis/geojson/collection.rb +60 -14
- data/lib/nswtopo/gis/geojson/line_string.rb +142 -1
- data/lib/nswtopo/gis/geojson/multi_line_string.rb +49 -7
- data/lib/nswtopo/gis/geojson/multi_point.rb +87 -0
- data/lib/nswtopo/gis/geojson/multi_polygon.rb +35 -23
- data/lib/nswtopo/gis/geojson/point.rb +16 -1
- data/lib/nswtopo/gis/geojson/polygon.rb +69 -7
- data/lib/nswtopo/gis/geojson.rb +92 -46
- data/lib/nswtopo/gis/projection.rb +5 -1
- data/lib/nswtopo/helpers/thread_pool.rb +39 -0
- data/lib/nswtopo/helpers.rb +44 -5
- data/lib/nswtopo/layer/arcgis_raster.rb +4 -6
- data/lib/nswtopo/layer/contour.rb +24 -26
- data/lib/nswtopo/layer/control.rb +5 -3
- data/lib/nswtopo/layer/declination.rb +14 -10
- data/lib/nswtopo/layer/feature.rb +5 -5
- data/lib/nswtopo/layer/grid.rb +19 -18
- data/lib/nswtopo/layer/labels/barriers.rb +23 -0
- data/lib/nswtopo/layer/labels/convex_hull.rb +12 -0
- data/lib/nswtopo/layer/labels/convex_hulls.rb +86 -0
- data/lib/nswtopo/layer/labels/label.rb +63 -0
- data/lib/nswtopo/layer/labels.rb +192 -315
- data/lib/nswtopo/layer/overlay.rb +11 -12
- data/lib/nswtopo/layer/raster.rb +1 -0
- data/lib/nswtopo/layer/relief.rb +6 -4
- data/lib/nswtopo/layer/spot.rb +11 -17
- data/lib/nswtopo/layer/{vector → vector_render}/cutout.rb +1 -1
- data/lib/nswtopo/layer/{vector → vector_render}/knockout.rb +2 -3
- data/lib/nswtopo/layer/{vector.rb → vector_render.rb} +20 -45
- data/lib/nswtopo/layer.rb +2 -1
- data/lib/nswtopo/map.rb +70 -56
- data/lib/nswtopo/svg.rb +5 -0
- data/lib/nswtopo/tiled_web_map.rb +3 -3
- data/lib/nswtopo/tree_indenter.rb +2 -2
- data/lib/nswtopo/version.rb +1 -1
- data/lib/nswtopo.rb +4 -0
- metadata +15 -17
- data/lib/nswtopo/geometry/overlap.rb +0 -47
- data/lib/nswtopo/geometry/segment.rb +0 -27
- data/lib/nswtopo/geometry/vector_sequence.rb +0 -180
- data/lib/nswtopo/helpers/array.rb +0 -19
- data/lib/nswtopo/helpers/concurrently.rb +0 -27
- data/lib/nswtopo/helpers/dir.rb +0 -7
- data/lib/nswtopo/helpers/hash.rb +0 -15
- data/lib/nswtopo/helpers/tar_writer.rb +0 -11
- data/lib/nswtopo/layer/labels/barrier.rb +0 -39
| @@ -1,32 +1,31 @@ | |
| 1 1 | 
             
            module NSWTopo
         | 
| 2 2 | 
             
              module Overlay
         | 
| 3 | 
            -
                include  | 
| 3 | 
            +
                include VectorRender
         | 
| 4 4 | 
             
                CREATE = %w[simplify tolerance]
         | 
| 5 5 | 
             
                TOLERANCE = 0.4
         | 
| 6 6 |  | 
| 7 7 | 
             
                GPX_STYLES = YAML.load <<~YAML
         | 
| 8 8 | 
             
                  stroke: black
         | 
| 9 9 | 
             
                  stroke-width: 0.4
         | 
| 10 | 
            +
                  barrier: true
         | 
| 10 11 | 
             
                YAML
         | 
| 11 12 |  | 
| 12 13 | 
             
                def get_features
         | 
| 13 14 | 
             
                  GPS.new(@path).tap do |gps|
         | 
| 14 15 | 
             
                    @simplify = true if GPS::GPX === gps
         | 
| 15 16 | 
             
                    @tolerance ||= [@map.to_mm(5), TOLERANCE].max if @simplify
         | 
| 16 | 
            -
                  end.collection.reproject_to(@map.neatline.projection).explode. | 
| 17 | 
            +
                  end.collection.reproject_to(@map.neatline.projection).explode.map! do |feature|
         | 
| 18 | 
            +
                    if @tolerance && GeoJSON::LineString === feature
         | 
| 19 | 
            +
                      feature.simplify(@tolerance).segmentise(2*@tolerance).smooth_window(3)
         | 
| 20 | 
            +
                    else
         | 
| 21 | 
            +
                      feature
         | 
| 22 | 
            +
                    end
         | 
| 23 | 
            +
                  end.map! do |feature|
         | 
| 17 24 | 
             
                    styles, folder, name = feature.values_at "styles", "folder", "name"
         | 
| 18 25 | 
             
                    styles ||= GPX_STYLES
         | 
| 19 | 
            -
             | 
| 20 26 | 
             
                    case feature
         | 
| 21 27 | 
             
                    when GeoJSON::LineString
         | 
| 22 28 | 
             
                      styles["stroke-linejoin"] = "round"
         | 
| 23 | 
            -
                      if @tolerance
         | 
| 24 | 
            -
                        simplified = feature.coordinates.douglas_peucker(@tolerance)
         | 
| 25 | 
            -
                        smoothed = simplified.sample_at(2*@tolerance).each_cons(2).map do |segment|
         | 
| 26 | 
            -
                          segment.along(0.5)
         | 
| 27 | 
            -
                        end.push(simplified.last).prepend(simplified.first)
         | 
| 28 | 
            -
                        feature.coordinates = smoothed
         | 
| 29 | 
            -
                      end
         | 
| 30 29 | 
             
                    when GeoJSON::Polygon
         | 
| 31 30 | 
             
                      styles["stroke-linejoin"] = "miter"
         | 
| 32 31 | 
             
                    end
         | 
| @@ -34,10 +33,10 @@ module NSWTopo | |
| 34 33 | 
             
                    categories = [folder, name].compact.reject(&:empty?).map(&method(:categorise))
         | 
| 35 34 | 
             
                    keys = styles.keys - params_for(categories.to_set).keys
         | 
| 36 35 | 
             
                    styles = styles.slice *keys
         | 
| 36 | 
            +
                    categories << feature.object_id
         | 
| 37 37 |  | 
| 38 | 
            -
                    feature.clear
         | 
| 39 | 
            -
                    feature["category"] = categories << feature.object_id
         | 
| 40 38 | 
             
                    @params[categories.join(?\s)] = styles if styles.any?
         | 
| 39 | 
            +
                    feature.with_properties("category" => categories)
         | 
| 41 40 | 
             
                  end
         | 
| 42 41 | 
             
                end
         | 
| 43 42 |  | 
    
        data/lib/nswtopo/layer/raster.rb
    CHANGED
    
    
    
        data/lib/nswtopo/layer/relief.rb
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            module NSWTopo
         | 
| 2 2 | 
             
              module Relief
         | 
| 3 3 | 
             
                include Raster, MaskRender, DEM, Log
         | 
| 4 | 
            -
                CREATE = %w[method azimuth factor smooth contours]
         | 
| 4 | 
            +
                CREATE = %w[method azimuth factor smooth contours epsg]
         | 
| 5 5 | 
             
                DEFAULTS = YAML.load <<~YAML
         | 
| 6 6 | 
             
                  shade: rgb(0,0,48)
         | 
| 7 7 | 
             
                  method: combined
         | 
| @@ -26,7 +26,9 @@ module NSWTopo | |
| 26 26 | 
             
                  when @contours
         | 
| 27 27 | 
             
                    bounds = cutline.bounds
         | 
| 28 28 | 
             
                    raise "no resolution specified for #{@name}" unless Numeric === @mm_per_px
         | 
| 29 | 
            -
                    outsize =  | 
| 29 | 
            +
                    outsize = bounds.map do |min, max|
         | 
| 30 | 
            +
                      (max - min) / @mm_per_px
         | 
| 31 | 
            +
                    end.map(&:ceil)
         | 
| 30 32 |  | 
| 31 33 | 
             
                    collection = @contours.map do |url_or_path, attribute_or_hash|
         | 
| 32 34 | 
             
                      raise "no elevation attribute specified for #{url_or_path}" unless attribute_or_hash
         | 
| @@ -42,8 +44,8 @@ module NSWTopo | |
| 42 44 | 
             
                        Shapefile::Source.new(url_or_path).layer(**options, geometry: cutline).features
         | 
| 43 45 | 
             
                      else
         | 
| 44 46 | 
             
                        raise "unrecognised elevation data source: #{url_or_path}"
         | 
| 45 | 
            -
                      end. | 
| 46 | 
            -
                        feature. | 
| 47 | 
            +
                      end.map! do |feature|
         | 
| 48 | 
            +
                        feature.with_properties("elevation" => feature.fetch(attribute, attribute).to_f)
         | 
| 47 49 | 
             
                      end.reproject_to(@map.projection)
         | 
| 48 50 | 
             
                    end.inject(&:merge)
         | 
| 49 51 |  | 
    
        data/lib/nswtopo/layer/spot.rb
    CHANGED
    
    | @@ -1,7 +1,9 @@ | |
| 1 1 | 
             
            module NSWTopo
         | 
| 2 2 | 
             
              module Spot
         | 
| 3 | 
            -
                 | 
| 4 | 
            -
                 | 
| 3 | 
            +
                using Helpers
         | 
| 4 | 
            +
                include VectorRender, DEM, Log
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                CREATE = %w[spacing smooth prefer extent epsg]
         | 
| 5 7 | 
             
                DEFAULTS = YAML.load <<~YAML
         | 
| 6 8 | 
             
                  spacing: 15
         | 
| 7 9 | 
             
                  smooth: 0.2
         | 
| @@ -39,7 +41,7 @@ module NSWTopo | |
| 39 41 | 
             
                end
         | 
| 40 42 |  | 
| 41 43 | 
             
                module Candidate
         | 
| 42 | 
            -
                  attr_accessor :elevation, :knoll
         | 
| 44 | 
            +
                  attr_accessor :elevation, :knoll, :conflicts
         | 
| 43 45 |  | 
| 44 46 | 
             
                  module PreferKnolls
         | 
| 45 47 | 
             
                    def ordinal; [conflicts.size, -elevation] end
         | 
| @@ -53,17 +55,9 @@ module NSWTopo | |
| 53 55 | 
             
                    def ordinal; conflicts.size end
         | 
| 54 56 | 
             
                  end
         | 
| 55 57 |  | 
| 56 | 
            -
                  def conflicts
         | 
| 57 | 
            -
                    @conflicts ||= Set[]
         | 
| 58 | 
            -
                  end
         | 
| 59 | 
            -
             | 
| 60 58 | 
             
                  def <=>(other)
         | 
| 61 59 | 
             
                    self.ordinal <=> other.ordinal
         | 
| 62 60 | 
             
                  end
         | 
| 63 | 
            -
             | 
| 64 | 
            -
                  def bounds(buffer: 0)
         | 
| 65 | 
            -
                    coordinates.map { |coordinate| [coordinate - buffer, coordinate + buffer] }
         | 
| 66 | 
            -
                  end
         | 
| 67 61 | 
             
                end
         | 
| 68 62 |  | 
| 69 63 | 
             
                def ordering
         | 
| @@ -90,7 +84,7 @@ module NSWTopo | |
| 90 84 | 
             
                      aspect.ncols.times do |col|
         | 
| 91 85 | 
             
                        offsets.map!(&:next)
         | 
| 92 86 | 
             
                        next if row < 1 || col < 1 || row >= aspect.nrows - 1 || col >= aspect.ncols - 1
         | 
| 93 | 
            -
                        next if block | 
| 87 | 
            +
                        next if block_given? && block.call(col, row)
         | 
| 94 88 | 
             
                        ccw, cw = offsets.each_cons(2).inject([true, true]) do |(ccw, cw), (o1, o2)|
         | 
| 95 89 | 
             
                          break unless ccw || cw
         | 
| 96 90 | 
             
                          a1, a2 = aspect.values.values_at o1, o2
         | 
| @@ -124,15 +118,15 @@ module NSWTopo | |
| 124 118 | 
             
                    pixels, knolls = pixels_knolls(dem_hr_path) do |col, row|
         | 
| 125 119 | 
             
                      !mask.include? [(col * @mm_per_px / low_resolution).floor, (row * @mm_per_px / low_resolution).floor]
         | 
| 126 120 | 
             
                    end.entries.transpose
         | 
| 121 | 
            +
                    raise "no elevation data found in map area" unless pixels
         | 
| 127 122 |  | 
| 128 123 | 
             
                    locations = raster_locations dem_hr_path, pixels
         | 
| 129 124 | 
             
                    elevations = raster_values dem_hr_path, pixels
         | 
| 130 125 |  | 
| 131 126 | 
             
                    locations.zip(elevations, knolls).map do |coordinates, elevation, knoll|
         | 
| 132 | 
            -
                      GeoJSON::Point | 
| 127 | 
            +
                      GeoJSON::Point[coordinates, "label" => elevation.round] do |feature|
         | 
| 133 128 | 
             
                        feature.extend Candidate, ordering
         | 
| 134 | 
            -
                        feature.knoll, feature.elevation = knoll, elevation
         | 
| 135 | 
            -
                        feature["label"] = elevation.round
         | 
| 129 | 
            +
                        feature.knoll, feature.elevation, feature.conflicts = knoll, elevation, Set[]
         | 
| 136 130 | 
             
                      end
         | 
| 137 131 | 
             
                    end
         | 
| 138 132 | 
             
                  end
         | 
| @@ -144,9 +138,9 @@ module NSWTopo | |
| 144 138 |  | 
| 145 139 | 
             
                  candidates.each.with_index do |candidate, index|
         | 
| 146 140 | 
             
                    log_update "%s: examining candidates: %.1f%%" % [@name, 100.0 * index  / candidates.length]
         | 
| 147 | 
            -
                    spatial_index.search(candidate.bounds | 
| 141 | 
            +
                    spatial_index.search(candidate.bounds, @spacing).each do |other|
         | 
| 148 142 | 
             
                      next if other == candidate
         | 
| 149 | 
            -
                      next if  | 
| 143 | 
            +
                      next if (candidate.coordinates - other.coordinates).norm > @spacing
         | 
| 150 144 | 
             
                      candidate.conflicts << other
         | 
| 151 145 | 
             
                    end
         | 
| 152 146 | 
             
                  end.each do |candidate|
         | 
| @@ -1,9 +1,8 @@ | |
| 1 1 | 
             
            module NSWTopo
         | 
| 2 | 
            -
              module  | 
| 2 | 
            +
              module VectorRender
         | 
| 3 3 | 
             
                class Knockout
         | 
| 4 4 | 
             
                  def initialize(element, buffer)
         | 
| 5 | 
            -
                    buffer =  | 
| 6 | 
            -
                    @buffer = Float(buffer)
         | 
| 5 | 
            +
                    @buffer = Labels::Label.knockout(buffer)
         | 
| 7 6 | 
             
                    @href = "#" + element.attributes["id"]
         | 
| 8 7 | 
             
                  end
         | 
| 9 8 | 
             
                  attr_reader :buffer
         | 
| @@ -1,8 +1,11 @@ | |
| 1 | 
            -
            require_relative ' | 
| 2 | 
            -
            require_relative ' | 
| 1 | 
            +
            require_relative 'vector_render/cutout'
         | 
| 2 | 
            +
            require_relative 'vector_render/knockout'
         | 
| 3 3 |  | 
| 4 4 | 
             
            module NSWTopo
         | 
| 5 | 
            -
              module  | 
| 5 | 
            +
              module VectorRender
         | 
| 6 | 
            +
                using Helpers
         | 
| 7 | 
            +
                include SVG
         | 
| 8 | 
            +
             | 
| 6 9 | 
             
                SVG_ATTRIBUTES = %w[
         | 
| 7 10 | 
             
                  fill-opacity
         | 
| 8 11 | 
             
                  fill
         | 
| @@ -39,7 +42,6 @@ module NSWTopo | |
| 39 42 |  | 
| 40 43 | 
             
                SHIELD_X, SHIELD_Y = 1.0, 0.5
         | 
| 41 44 | 
             
                MARGIN = { mm: 1.0 }
         | 
| 42 | 
            -
                VALUE, POINT, ANGLE = "%.5f", "%.5f %.5f", "%.2f"
         | 
| 43 45 |  | 
| 44 46 | 
             
                def create
         | 
| 45 47 | 
             
                  @features = get_features.reproject_to(@map.neatline.projection).clip(@map.neatline(**MARGIN))
         | 
| @@ -78,28 +80,6 @@ module NSWTopo | |
| 78 80 | 
             
                  string.tr_s('^_a-zA-Z0-9', ?-).delete_prefix(?-).delete_suffix(?-)
         | 
| 79 81 | 
             
                end
         | 
| 80 82 |  | 
| 81 | 
            -
                def svg_path_data(points, bezier: false)
         | 
| 82 | 
            -
                  if bezier
         | 
| 83 | 
            -
                    fraction = Numeric === bezier ? bezier.clamp(0.0, 1.0) : 1.0
         | 
| 84 | 
            -
                    extras = points.first == points.last ? [points[-2], *points, points[2]] : [points.first, *points, points.last]
         | 
| 85 | 
            -
                    midpoints = extras.segments.map(&:midpoint)
         | 
| 86 | 
            -
                    distances = extras.segments.map(&:distance)
         | 
| 87 | 
            -
                    offsets = midpoints.zip(distances).segments.map(&:transpose).map do |segment, distance|
         | 
| 88 | 
            -
                      segment.along(distance.first / distance.inject(&:+))
         | 
| 89 | 
            -
                    end.zip(points).map(&:diff)
         | 
| 90 | 
            -
                    controls = midpoints.segments.zip(offsets).flat_map do |segment, offset|
         | 
| 91 | 
            -
                      segment.map { |point| [point, point.plus(offset)].along(fraction) }
         | 
| 92 | 
            -
                    end.drop(1).each_slice(2).entries.prepend(nil)
         | 
| 93 | 
            -
                    points.zip(controls).map do |point, controls|
         | 
| 94 | 
            -
                      controls ? "C %s %s %s" % [POINT, POINT, POINT] % [*controls.flatten, *point] : "M %s" % POINT % point
         | 
| 95 | 
            -
                    end.join(" ")
         | 
| 96 | 
            -
                  else
         | 
| 97 | 
            -
                    points.map do |point|
         | 
| 98 | 
            -
                      POINT % point
         | 
| 99 | 
            -
                    end.join(" L ").prepend("M ")
         | 
| 100 | 
            -
                  end
         | 
| 101 | 
            -
                end
         | 
| 102 | 
            -
             | 
| 103 83 | 
             
                def params_for(categories)
         | 
| 104 84 | 
             
                  params.select do |key, value|
         | 
| 105 85 | 
             
                    Array(key).any? do |selector|
         | 
| @@ -140,7 +120,8 @@ module NSWTopo | |
| 140 120 | 
             
                    use.tap(&block)
         | 
| 141 121 |  | 
| 142 122 | 
             
                    category_params = params_for(categories)
         | 
| 143 | 
            -
                    font_size, stroke_width, bezier | 
| 123 | 
            +
                    font_size, stroke_width, bezier = category_params.values_at "font-size", "stroke-width", "bezier"
         | 
| 124 | 
            +
                    subdivide = category_params.slice("subdivide", "section").values.first
         | 
| 144 125 |  | 
| 145 126 | 
             
                    category_params.slice(*SVG_ATTRIBUTES).tap do |svg_attributes|
         | 
| 146 127 | 
             
                      svg_attributes.slice(*FONT_SCALED_ATTRIBUTES).each do |key, value|
         | 
| @@ -157,25 +138,18 @@ module NSWTopo | |
| 157 138 | 
             
                        content.add_element "use", "transform" => transform, "href" => "#%s" % symbol_id
         | 
| 158 139 |  | 
| 159 140 | 
             
                      when GeoJSON::LineString
         | 
| 160 | 
            -
                         | 
| 161 | 
            -
             | 
| 162 | 
            -
                          content.add_element "path", "fill" => "none", "d" => svg_path_data(linestring, bezier: bezier)
         | 
| 141 | 
            +
                        (subdivide ? feature.subdivide(subdivide) : feature).explode.each do |feature|
         | 
| 142 | 
            +
                          content.add_element "path", "fill" => "none", "d" => feature.svg_path_data(bezier: bezier)
         | 
| 163 143 | 
             
                        end
         | 
| 164 144 |  | 
| 165 145 | 
             
                      when GeoJSON::Polygon
         | 
| 166 | 
            -
                         | 
| 167 | 
            -
                          svg_path_data ring, bezier: bezier
         | 
| 168 | 
            -
                        end.each.with_object("Z").entries.join(?\s)
         | 
| 169 | 
            -
                        content.add_element "path", "fill-rule" => "nonzero", "d" => path_data
         | 
| 146 | 
            +
                        content.add_element "path", "fill-rule" => "nonzero", "d" => feature.svg_path_data
         | 
| 170 147 |  | 
| 171 148 | 
             
                      when REXML::Element
         | 
| 172 149 | 
             
                        case feature.name
         | 
| 173 150 | 
             
                        when "text", "textPath" then content << feature
         | 
| 174 151 | 
             
                        when "path" then defs << feature
         | 
| 175 152 | 
             
                        end
         | 
| 176 | 
            -
             | 
| 177 | 
            -
                      when Array
         | 
| 178 | 
            -
                        content.add_element "path", "fill" => "none", "d" => svg_path_data(feature + feature.take(1))
         | 
| 179 153 | 
             
                      end
         | 
| 180 154 | 
             
                    end if content
         | 
| 181 155 |  | 
| @@ -223,11 +197,12 @@ module NSWTopo | |
| 223 197 | 
             
                            defs.add_element("g", "id" => symbol_id).add_element(element, attributes)
         | 
| 224 198 | 
             
                          end
         | 
| 225 199 | 
             
                        end
         | 
| 226 | 
            -
                         | 
| 227 | 
            -
                         | 
| 228 | 
            -
                         | 
| 229 | 
            -
                           | 
| 230 | 
            -
                             | 
| 200 | 
            +
                        rings = features.grep(GeoJSON::Polygon).map(&:rings).flat_map(&:explode)
         | 
| 201 | 
            +
                        lines = features.grep(GeoJSON::LineString)
         | 
| 202 | 
            +
                        (rings + lines).each do |feature|
         | 
| 203 | 
            +
                          feature.sample_at(interval, offset: offset) do |point, along, angle|
         | 
| 204 | 
            +
                            "translate(%s) rotate(%s)" % [POINT, ANGLE] % [*point, 180.0 * angle / Math::PI]
         | 
| 205 | 
            +
                          end.each do |transform|
         | 
| 231 206 | 
             
                            content.add_element "use", "transform" => transform, "href" => "#%s" % symbol_ids.sample
         | 
| 232 207 | 
             
                          end
         | 
| 233 208 | 
             
                        end
         | 
| @@ -244,8 +219,8 @@ module NSWTopo | |
| 244 219 | 
             
                          when "inpoint"  then [line.first(2)]
         | 
| 245 220 | 
             
                          when "outpoint" then [line.last(2).rotate]
         | 
| 246 221 | 
             
                          when "endpoint" then [line.first(2), line.last(2).rotate]
         | 
| 247 | 
            -
                          end.each do | | 
| 248 | 
            -
                            transform = "translate(%s) rotate(%s)" % [POINT, ANGLE] % [* | 
| 222 | 
            +
                          end.each do |p0, p1|
         | 
| 223 | 
            +
                            transform = "translate(%s) rotate(%s)" % [POINT, ANGLE] % [*p0, 180.0 * (p1 - p0).angle / Math::PI]
         | 
| 249 224 | 
             
                            use.add_element "use", "transform" => transform, "href" => "#%s" % symbol_id
         | 
| 250 225 | 
             
                          end
         | 
| 251 226 | 
             
                        end
         | 
| @@ -264,7 +239,7 @@ module NSWTopo | |
| 264 239 | 
             
                        next unless content && args
         | 
| 265 240 | 
             
                        buffer = 0.5 * (Numeric === args ? args : Numeric === stroke_width ? stroke_width : 0)
         | 
| 266 241 | 
             
                        features.grep_v(REXML::Element).each do |feature|
         | 
| 267 | 
            -
                          Labels:: | 
| 242 | 
            +
                          Labels::ConvexHulls.new(feature, buffer).tap(&block)
         | 
| 268 243 | 
             
                        end
         | 
| 269 244 |  | 
| 270 245 | 
             
                      when "shield"
         | 
    
        data/lib/nswtopo/layer.rb
    CHANGED
    
    | @@ -2,7 +2,7 @@ require_relative 'layer/raster_import' | |
| 2 2 | 
             
            require_relative 'layer/raster'
         | 
| 3 3 | 
             
            require_relative 'layer/raster_render'
         | 
| 4 4 | 
             
            require_relative 'layer/mask_render'
         | 
| 5 | 
            -
            require_relative 'layer/ | 
| 5 | 
            +
            require_relative 'layer/vector_render'
         | 
| 6 6 | 
             
            require_relative 'layer/vegetation'
         | 
| 7 7 | 
             
            require_relative 'layer/import'
         | 
| 8 8 | 
             
            require_relative 'layer/arcgis_raster'
         | 
| @@ -19,6 +19,7 @@ require_relative 'layer/labels' | |
| 19 19 |  | 
| 20 20 | 
             
            module NSWTopo
         | 
| 21 21 | 
             
              class Layer
         | 
| 22 | 
            +
                using Helpers
         | 
| 22 23 | 
             
                TYPES = Set[Vegetation, Import, ColourMask, ArcGISRaster, Feature, Contour, Spot, Overlay, Relief, Grid, Declination, Control, Labels]
         | 
| 23 24 |  | 
| 24 25 | 
             
                def initialize(name, map, params)
         | 
    
        data/lib/nswtopo/map.rb
    CHANGED
    
    | @@ -1,5 +1,6 @@ | |
| 1 1 | 
             
            module NSWTopo
         | 
| 2 2 | 
             
              class Map
         | 
| 3 | 
            +
                using Helpers
         | 
| 3 4 | 
             
                include Formats, Dither, Zip, Log, Safely, TiledWebMap
         | 
| 4 5 |  | 
| 5 6 | 
             
                def initialize(archive, neatline:, centre:, dimensions:, scale:, rotation:, layers: {})
         | 
| @@ -15,60 +16,58 @@ module NSWTopo | |
| 15 16 | 
             
                extend Forwardable
         | 
| 16 17 | 
             
                delegate %i[write mtime read uptodate?] => :@archive
         | 
| 17 18 |  | 
| 18 | 
            -
                def self.init(archive, scale: 25000, rotation: 0.0, bounds: nil, coords: nil,  | 
| 19 | 
            +
                def self.init(archive, scale: 25000, rotation: 0.0, bounds: nil, coords: nil, neatline: nil, dimensions: nil, margins: nil, **neatline_options)
         | 
| 19 20 | 
             
                  points = case
         | 
| 20 21 | 
             
                  when dimensions && margins
         | 
| 21 22 | 
             
                    raise "can't specify both margins and map dimensions"
         | 
| 23 | 
            +
                  when dimensions && neatline
         | 
| 24 | 
            +
                    raise "can't specify both neatline and map dimensions"
         | 
| 25 | 
            +
                  when bounds && neatline
         | 
| 26 | 
            +
                    raise "can't specify both bounds and neatline"
         | 
| 27 | 
            +
                  when coords && neatline
         | 
| 28 | 
            +
                    raise "can't specify both neatline and map coordinates"
         | 
| 22 29 | 
             
                  when coords && bounds
         | 
| 23 | 
            -
                    raise "can't specify both bounds  | 
| 30 | 
            +
                    raise "can't specify both bounds and map coordinates"
         | 
| 24 31 | 
             
                  when coords
         | 
| 25 | 
            -
                    coords
         | 
| 32 | 
            +
                    GeoJSON.multipoint(coords)
         | 
| 26 33 | 
             
                  when bounds
         | 
| 27 | 
            -
                     | 
| 28 | 
            -
             | 
| 29 | 
            -
             | 
| 30 | 
            -
                    when gps.polygons.any?
         | 
| 31 | 
            -
                      gps.polygons.flat_map(&:coordinates).inject(&:+)
         | 
| 32 | 
            -
                    when gps.linestrings.any?
         | 
| 33 | 
            -
                      gps.linestrings.map(&:coordinates).inject(&:+)
         | 
| 34 | 
            -
                    when gps.points.any?
         | 
| 35 | 
            -
                      gps.points.map(&:coordinates)
         | 
| 36 | 
            -
                    else
         | 
| 37 | 
            -
                      raise "no features found in %s" % bounds
         | 
| 34 | 
            +
                    GPS.load(bounds).explode.tap do |gps|
         | 
| 35 | 
            +
                      margins ||= [15, 15] unless dimensions || gps.polygons.any?
         | 
| 36 | 
            +
                      raise "no features found in %s" % bounds if gps.none?
         | 
| 38 37 | 
             
                    end
         | 
| 38 | 
            +
                  when neatline
         | 
| 39 | 
            +
                    neatline = GPS.load(neatline)
         | 
| 40 | 
            +
                    raise "neatline must be a single polygon" unless neatline.polygon?
         | 
| 41 | 
            +
                    neatline
         | 
| 39 42 | 
             
                  else
         | 
| 40 43 | 
             
                    raise "no bounds file or map coordinates specified"
         | 
| 41 | 
            -
                  end
         | 
| 42 | 
            -
                  margins ||= [0, 0]
         | 
| 44 | 
            +
                  end.dissolve_points
         | 
| 43 45 |  | 
| 44 | 
            -
                  centre = points. | 
| 46 | 
            +
                  centre = *points.bbox_centre.coordinates
         | 
| 45 47 | 
             
                  equidistant = Projection.azimuthal_equidistant *centre
         | 
| 48 | 
            +
                  margins ||= [0, 0]
         | 
| 46 49 |  | 
| 47 50 | 
             
                  case rotation
         | 
| 48 51 | 
             
                  when "auto"
         | 
| 49 52 | 
             
                    raise "can't specify both map dimensions and auto-rotation" if dimensions
         | 
| 50 | 
            -
                    local_points =  | 
| 51 | 
            -
                     | 
| 52 | 
            -
                    rotation *= -180.0 / Math::PI
         | 
| 53 | 
            +
                    local_points = points.reproject_to equidistant
         | 
| 54 | 
            +
                    rotation = -180 * local_points.minimum_bbox_angle(*margins) / Math::PI
         | 
| 53 55 | 
             
                  when "magnetic"
         | 
| 54 | 
            -
                    rotation = declination | 
| 56 | 
            +
                    rotation = declination *centre
         | 
| 55 57 | 
             
                  else
         | 
| 56 58 | 
             
                    raise "map rotation must be between ±45°" unless rotation.abs <= 45
         | 
| 57 59 | 
             
                  end
         | 
| 58 60 |  | 
| 59 | 
            -
                  unless dimensions | 
| 60 | 
            -
                    local_points  | 
| 61 | 
            -
                     | 
| 62 | 
            -
             | 
| 63 | 
            -
                    end.transpose.map(&:minmax).map do |min, max|
         | 
| 64 | 
            -
                      [0.5 * (max + min), max - min]
         | 
| 65 | 
            -
                    end.transpose
         | 
| 61 | 
            +
                  unless dimensions
         | 
| 62 | 
            +
                    local_points ||= points.reproject_to equidistant
         | 
| 63 | 
            +
                    local_points.rotate_by_degrees! rotation
         | 
| 64 | 
            +
                    local_extents, local_centre = local_points.bbox_extents, local_points.bbox_centre
         | 
| 66 65 | 
             
                    local_centre.rotate_by_degrees! -rotation
         | 
| 67 | 
            -
                  end
         | 
| 68 66 |  | 
| 69 | 
            -
             | 
| 70 | 
            -
             | 
| 71 | 
            -
                     | 
| 67 | 
            +
                    dimensions = local_extents.zip(margins).map do |extent, margin|
         | 
| 68 | 
            +
                      extent * 1000.0 / scale + 2 * margin
         | 
| 69 | 
            +
                    end
         | 
| 70 | 
            +
                    centre = *local_centre.reproject_to_wgs84.coordinates
         | 
| 72 71 | 
             
                  end
         | 
| 73 72 |  | 
| 74 73 | 
             
                  params = { units: "mm", axis: "esu", k_0: 1.0 / scale, x_0: 0.0005 * dimensions[0], y_0: -0.0005 * dimensions[1] }
         | 
| @@ -84,31 +83,47 @@ module NSWTopo | |
| 84 83 | 
             
                    raise "not enough information to calculate map size – check bounds file, or specify map dimensions or margins"
         | 
| 85 84 | 
             
                  end
         | 
| 86 85 |  | 
| 87 | 
            -
                   | 
| 88 | 
            -
                     | 
| 89 | 
            -
                   | 
| 90 | 
            -
                     | 
| 91 | 
            -
             | 
| 92 | 
            -
                    end
         | 
| 93 | 
            -
                    collection.add_polygon [bounds.inject(&:product).values_at(0,2,3,1,0)]
         | 
| 86 | 
            +
                  if neatline
         | 
| 87 | 
            +
                    neatline = neatline.reproject_to projection
         | 
| 88 | 
            +
                  else
         | 
| 89 | 
            +
                    ring = [0, 0].zip(dimensions).inject(&:product).values_at(0,2,3,1,0)
         | 
| 90 | 
            +
                    neatline = GeoJSON.polygon [ring], projection: projection, name: "neatline"
         | 
| 94 91 | 
             
                  end
         | 
| 95 92 |  | 
| 96 | 
            -
                   | 
| 97 | 
            -
                     | 
| 98 | 
            -
             | 
| 99 | 
            -
                       | 
| 100 | 
            -
             | 
| 101 | 
            -
             | 
| 102 | 
            -
             | 
| 103 | 
            -
                       | 
| 93 | 
            +
                  neatline_options.each do |key, value|
         | 
| 94 | 
            +
                    case key
         | 
| 95 | 
            +
                    when :radius
         | 
| 96 | 
            +
                      radius, segments = [*value, 9]
         | 
| 97 | 
            +
                      neatline = neatline.with_sql(<<~SQL, name: "neatline").explode
         | 
| 98 | 
            +
                        SELECT ST_Buffer(ST_Buffer(ST_Buffer(geometry, #{-radius}, #{segments}), #{2*radius}, #{segments}), #{-radius}, #{segments})
         | 
| 99 | 
            +
                        FROM neatline
         | 
| 100 | 
            +
                      SQL
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                      raise OptionParser::InvalidArgument, "radius too big" unless neatline.one?
         | 
| 103 | 
            +
                    when :inset
         | 
| 104 | 
            +
                      value.map do |corners|
         | 
| 105 | 
            +
                        corners.each_slice(2).entries.transpose.map(&:sort)
         | 
| 106 | 
            +
                      end.flat_map do |bounds|
         | 
| 107 | 
            +
                        dimensions.zip(bounds)
         | 
| 108 | 
            +
                      end.each do |dimension, (min, max)|
         | 
| 109 | 
            +
                        raise OptionParser::InvalidArgument, "inset falls outside map area" unless max > 0 && min < dimension
         | 
| 110 | 
            +
                      end
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                      values = value.map do |corners|
         | 
| 113 | 
            +
                        %Q[(BuildMBR(#{corners.join ?,}))]
         | 
| 114 | 
            +
                      end
         | 
| 115 | 
            +
             | 
| 116 | 
            +
                      neatline = neatline.with_sql(<<~SQL, name: "neatline").explode
         | 
| 117 | 
            +
                        WITH insets(geometry) AS (VALUES #{values.join ?,})
         | 
| 118 | 
            +
                        SELECT ST_Difference(neatline.geometry, ST_Union(insets.geometry)) AS geometry
         | 
| 119 | 
            +
                        FROM neatline JOIN insets
         | 
| 120 | 
            +
                      SQL
         | 
| 121 | 
            +
             | 
| 122 | 
            +
                      raise OptionParser::InvalidArgument, "inset too big" if neatline.none?
         | 
| 123 | 
            +
                      raise OptionParser::InvalidArgument, "inset creates non-contiguous map" unless neatline.one?
         | 
| 104 124 | 
             
                    end
         | 
| 105 | 
            -
                  else
         | 
| 106 | 
            -
                    ring = [[0, 0], dimensions].transpose.inject(&:product).values_at(0,2,3,1,0)
         | 
| 107 | 
            -
                    GeoJSON.polygon [ring], projection: projection, name: "neatline"
         | 
| 108 125 | 
             
                  end
         | 
| 109 126 |  | 
| 110 | 
            -
                  raise OptionParser::InvalidArgument, "inset covers map" if neatline.none?
         | 
| 111 | 
            -
                  raise OptionParser::InvalidArgument, "inset creates non-contiguous map" unless neatline.one?
         | 
| 112 127 | 
             
                  new(archive, neatline: neatline, centre: centre, dimensions: dimensions, scale: scale, rotation: rotation).save
         | 
| 113 128 | 
             
                end
         | 
| 114 129 |  | 
| @@ -314,16 +329,15 @@ module NSWTopo | |
| 314 329 | 
             
                def info(empty: nil, json: false, proj: false, wkt: false)
         | 
| 315 330 | 
             
                  case
         | 
| 316 331 | 
             
                  when json
         | 
| 317 | 
            -
                     | 
| 318 | 
            -
                     | 
| 319 | 
            -
                    JSON.pretty_generate bbox.to_h
         | 
| 332 | 
            +
                    properties = { dimensions: @dimensions, scale: @scale, rotation: @rotation, layers: layers.map(&:name) }
         | 
| 333 | 
            +
                    JSON.pretty_generate @neatline.reproject_to_wgs84.first.with_properties(properties)
         | 
| 320 334 | 
             
                  when proj
         | 
| 321 335 | 
             
                    OS.gdalsrsinfo("-o", "proj4", "--single-line", @projection)
         | 
| 322 336 | 
             
                  when wkt
         | 
| 323 337 | 
             
                    OS.gdalsrsinfo("-o", "wkt2", @projection).gsub(/\n\n+|\A\n+/, "")
         | 
| 324 338 | 
             
                  else
         | 
| 325 339 | 
             
                    area_km2 = @neatline.area * (0.000001 * @scale)**2
         | 
| 326 | 
            -
                    extents_km = @dimensions. | 
| 340 | 
            +
                    extents_km = @dimensions.map { |dimension| dimension * 0.000001 * @scale }
         | 
| 327 341 | 
             
                    StringIO.new.tap do |io|
         | 
| 328 342 | 
             
                      io.puts "%-11s 1:%i" %            ["scale:",      @scale]
         | 
| 329 343 | 
             
                      io.puts "%-11s %imm × %imm" %     ["dimensions:", *@dimensions.map(&:round)]
         | 
    
        data/lib/nswtopo/svg.rb
    ADDED
    
    
| @@ -28,7 +28,7 @@ module NSWTopo | |
| 28 28 | 
             
                    png_path = yield(resolution: max_level.resolution)
         | 
| 29 29 | 
             
                  end.tap do |levels|
         | 
| 30 30 | 
             
                    log_update "#{extension}: creating zoom levels %s" % levels.map(&:zoom).minmax.uniq.join(?-)
         | 
| 31 | 
            -
                  end. | 
| 31 | 
            +
                  end.inject(ThreadPool.new, &:<<).each do |level|
         | 
| 32 32 | 
             
                    OS.gdalwarp "-t_srs", "EPSG:3857", "-ts", *level.ts, "-te", *level.te, "-r", "cubic", "-dstalpha", png_path, level.tif_path
         | 
| 33 33 | 
             
                  end.flat_map do |level|
         | 
| 34 34 | 
             
                    cols, rows = level.indices
         | 
| @@ -40,11 +40,11 @@ module NSWTopo | |
| 40 40 | 
             
                    end
         | 
| 41 41 | 
             
                  end.tap do |tiles|
         | 
| 42 42 | 
             
                    log_update "#{extension}: creating %i tiles" % tiles.length
         | 
| 43 | 
            -
                  end. | 
| 43 | 
            +
                  end.inject(ThreadPool.new, &:<<).each do |tile|
         | 
| 44 44 | 
             
                    OS.gdal_translate *tile.args
         | 
| 45 45 | 
             
                  end.entries.tap do |tiles|
         | 
| 46 46 | 
             
                    log_update "#{extension}: optimising %i tiles" % tiles.length
         | 
| 47 | 
            -
                    tiles.map(&:path). | 
| 47 | 
            +
                    tiles.map(&:path).inject(ThreadPool.new, &:<<).in_groups do |*paths|
         | 
| 48 48 | 
             
                      dither *paths
         | 
| 49 49 | 
             
                    rescue Dither::Missing
         | 
| 50 50 | 
             
                    end
         | 
| @@ -3,7 +3,7 @@ module NSWTopo | |
| 3 3 | 
             
                def initialize(items, parts = nil, &block)
         | 
| 4 4 | 
             
                  @enum = Enumerator.new do |yielder|
         | 
| 5 5 | 
             
                    next unless items
         | 
| 6 | 
            -
                    grouped =  | 
| 6 | 
            +
                    grouped = block_given? ? block.call(items) : items
         | 
| 7 7 | 
             
                    grouped.each.with_index do |(item, group), index|
         | 
| 8 8 | 
             
                      *new_parts, last_part = parts
         | 
| 9 9 | 
             
                      case last_part
         | 
| @@ -15,7 +15,7 @@ module NSWTopo | |
| 15 15 | 
             
                      else                       "├─ "
         | 
| 16 16 | 
             
                      end if parts
         | 
| 17 17 | 
             
                      yielder << [new_parts, item]
         | 
| 18 | 
            -
                      TreeIndenter.new(group, new_parts, &block). | 
| 18 | 
            +
                      TreeIndenter.new(group, new_parts, &block).each(&yielder)
         | 
| 19 19 | 
             
                    end
         | 
| 20 20 | 
             
                  end
         | 
| 21 21 | 
             
                end
         | 
    
        data/lib/nswtopo/version.rb
    CHANGED
    
    
    
        data/lib/nswtopo.rb
    CHANGED
    
    | @@ -19,6 +19,7 @@ require 'forwardable' | |
| 19 19 | 
             
            require 'rubygems/package'
         | 
| 20 20 | 
             
            require 'zlib'
         | 
| 21 21 | 
             
            require 'io/nonblock'
         | 
| 22 | 
            +
            require 'bigdecimal/util'
         | 
| 22 23 | 
             
            begin
         | 
| 23 24 | 
             
              require 'hexapdf'
         | 
| 24 25 | 
             
            rescue LoadError
         | 
| @@ -26,6 +27,7 @@ end | |
| 26 27 |  | 
| 27 28 | 
             
            require_relative 'nswtopo/helpers'
         | 
| 28 29 | 
             
            require_relative 'nswtopo/avl_tree'
         | 
| 30 | 
            +
            require_relative 'nswtopo/svg'
         | 
| 29 31 | 
             
            require_relative 'nswtopo/geometry'
         | 
| 30 32 | 
             
            require_relative 'nswtopo/log'
         | 
| 31 33 | 
             
            require_relative 'nswtopo/safely'
         | 
| @@ -58,3 +60,5 @@ begin | |
| 58 60 | 
             
              require 'nswtopo/layers'
         | 
| 59 61 | 
             
            rescue LoadError
         | 
| 60 62 | 
             
            end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
            BigDecimal.limit 1000
         |