lib/nswtopo/layer/spot.rb in nswtopo-2.0.0 vs lib/nswtopo/layer/spot.rb in nswtopo-3.0

- old
+ new

@@ -1,12 +1,13 @@ module NSWTopo module Spot include Vector, DEM, Log - CREATE = %w[spacing smooth prefer] + CREATE = %w[spacing smooth prefer extent] DEFAULTS = YAML.load <<~YAML spacing: 15 smooth: 0.2 + extent: 4 symbol: circle: r: 0.2 stroke: none fill: black @@ -14,11 +15,10 @@ font-family: Arial, Helvetica, sans-serif font-size: 1.4 margin: 0.7 position: [right, above, below, left, aboveright, belowright, aboveleft, belowleft] YAML - NOISE_MM = 2.0 # TODO: noise sensitivity should depend on contour interval def margin { mm: 3 * @smooth } end @@ -37,16 +37,18 @@ line.chomp.split(?\s).map(&:to_f) end end module Candidate + attr_accessor :elevation, :knoll + module PreferKnolls - def ordinal; [conflicts.size, -self["elevation"]] end + def ordinal; [conflicts.size, -elevation] end end module PreferSaddles - def ordinal; [conflicts.size, self["elevation"]] end + def ordinal; [conflicts.size, elevation] end end module PreferNeither def ordinal; conflicts.size end end @@ -57,11 +59,11 @@ def <=>(other) self.ordinal <=> other.ordinal end - def bounds(buffer = 0) + def bounds(buffer: 0) coordinates.map { |coordinate| [coordinate - buffer, coordinate + buffer] } end end def ordering @@ -70,80 +72,83 @@ when "saddles" then Candidate::PreferSaddles else Candidate::PreferNeither end end + def pixels_knolls(dem_path, &block) + Enumerator.new do |yielder| + log_update "%s: calculating aspect map" % @name + aspect_path = dem_path.sub_ext ".bil" + OS.gdaldem "aspect", dem_path, aspect_path, "-trigonometric" + aspect = ESRIHdr.new aspect_path, -9999 + + offsets = [-1..1, -1..1].map(&:entries).inject(&:product).map do |row, col| + row * aspect.ncols + col - 1 + end.values_at(0,3,6,7,8,5,2,1,0) + + aspect.nrows.times do |row| + log_update "%s: finding flat areas: %.1f%%" % [@name, 100.0 * (row + 1) / aspect.nrows] + aspect.ncols.times do |col| + offsets.map!(&:next) + next if row < 1 || col < 1 || row >= aspect.nrows - 1 || col >= aspect.ncols - 1 + next if block&.call col, row + ccw, cw = offsets.each_cons(2).inject([true, true]) do |(ccw, cw), (o1, o2)| + break unless ccw || cw + a1, a2 = aspect.values.values_at o1, o2 + break unless a1 && a2 + (a2 - a1) % 360 < 180 ? [ccw, false] : [false, cw] + end + yielder << [[col, row], true] if ccw + yielder << [[col, row], false] if cw + end + end + end + end + def candidates @candidates ||= Dir.mktmppath do |temp_dir| raw_path = temp_dir / "raw.tif" - dem_path = temp_dir / "dem.tif" - aspect_path = temp_dir / "aspect.bil" + dem_hr_path = temp_dir / "dem.hr.tif" + dem_lr_path = temp_dir / "dem.lr.tif" if @smooth.zero? - get_dem temp_dir, dem_path + get_dem temp_dir, dem_hr_path else get_dem temp_dir, raw_path - blur_dem raw_path, dem_path + blur_dem raw_path, dem_hr_path end - log_update "%s: calculating aspect map" % @name - OS.gdaldem "aspect", dem_path, aspect_path, "-trigonometric" + low_resolution = 0.5 * @extent + OS.gdalwarp "-r", "med", "-tr", low_resolution, low_resolution, dem_hr_path, dem_lr_path - Enumerator.new do |yielder| - aspect = ESRIHdr.new aspect_path, -9999 - indices = [-1, 0, 1].map do |row| - [-1, 0, 1].map do |col| - row * aspect.ncols + col - 1 - end - end.flatten.values_at(0,3,6,7,8,5,2,1,0) + mask = pixels_knolls(dem_lr_path).map(&:first).to_set + pixels, knolls = pixels_knolls(dem_hr_path) do |col, row| + !mask.include? [(col * @mm_per_px / low_resolution).floor, (row * @mm_per_px / low_resolution).floor] + end.entries.transpose - aspect.nrows.times do |i| - log_update "%s: finding flat areas: %.1f%%" % [@name, 100.0 * i / aspect.nrows] - aspect.ncols.times do |j| - indices.map!(&:next) - next if i < 1 || j < 1 || i > aspect.nrows - 2 || j > aspect.ncols - 2 - ring = aspect.values.values_at *indices - next if ring.any?(&:nil?) - anticlockwise = ring.each_cons(2).map do |a1, a2| - (a2 - a1) % 360 < 180 - end - yielder << [[j + 1, i + 1], true] if anticlockwise.all? - yielder << [[j + 1, i + 1], false] if anticlockwise.none? - end - end - end.group_by(&:last).flat_map do |knoll, group| - pixels = group.map(&:first) - locations = raster_locations dem_path, pixels - elevations = raster_values dem_path, pixels + locations = raster_locations dem_hr_path, pixels + elevations = raster_values dem_hr_path, pixels - locations.zip(elevations).map do |coordinates, elevation| - GeoJSON::Point.new coordinates, "knoll" => knoll, "elevation" => elevation - end.each do |feature| + locations.zip(elevations, knolls).map do |coordinates, elevation, knoll| + GeoJSON::Point.new(coordinates).tap do |feature| feature.extend Candidate, ordering + feature.knoll, feature.elevation = knoll, elevation + feature["label"] = elevation.round end end end end def get_features - selected, rejected, remaining = [], Set[], AVLTree.new - index = RTree.load(candidates, &:bounds) + selected, remaining = [], AVLTree.new + spatial_index = RTree.load(candidates, &:bounds) - log_update "%s: choosing candidates" % @name - candidates.to_set.each do |candidate| - buffer = NOISE_MM * @map.scale / 1000.0 - index.search(candidate.bounds(buffer)).each do |other| - next unless candidate["knoll"] ^ other["knoll"] - next if [candidate, other].map(&:coordinates).distance > buffer - rejected << candidate << other - end - end.difference(rejected).each do |candidate| - buffer = @spacing * @map.scale / 1000.0 - index.search(candidate.bounds(buffer)).each do |other| + candidates.each.with_index do |candidate, index| + log_update "%s: examining candidates: %.1f%%" % [@name, 100.0 * index / candidates.length] + spatial_index.search(candidate.bounds(buffer: @spacing)).each do |other| next if other == candidate - next if rejected === other - next if [candidate, other].map(&:coordinates).distance > buffer + next if [candidate, other].map(&:coordinates).distance > @spacing candidate.conflicts << other end end.each do |candidate| remaining << candidate end @@ -159,13 +164,9 @@ other.conflicts.subtract removals remaining.insert other end end - selected.each do |feature| - feature.properties.replace "label" => feature["elevation"].round - end.yield_self do |features| - GeoJSON::Collection.new @map.projection, features - end + GeoJSON::Collection.new projection: @map.projection, features: selected end end end