module NSWTopo
def add(archive, *layers, after: nil, before: nil, replace: nil, overwrite: false, strict: false, **options)
def add(archive, *layers, after: nil, before: nil, replace: nil, overwrite: false, strict: false, **options) create_options = { after: Layer.sanitise(after), before: Layer.sanitise(before), replace: Layer.sanitise(replace), overwrite: overwrite, strict: strict } map = Map.load archive Enumerator.new do |yielder| while layers.any? layer, basedir = layers.shift path = Pathname(layer).expand_path(*basedir) case layer when /^controls\.(gpx|kml)$/i yielder << [path.basename(path.extname).to_s, "type" => "Control", "path" => path] when /\.(gpx|kml)$/i yielder << [path.basename(path.extname).to_s, "type" => "Overlay", "path" => path] when /\.(tiff?|png|jpg)$/i yielder << [path.basename(path.extname).to_s, "type" => "Import", "path" => path] when "contours" yielder << [layer, "type" => "Contour"] when "spot-heights" yielder << [layer, "type" => "Spot"] when "relief" yielder << [layer, "type" => "Relief"] when "grid" yielder << [layer, "type" => "Grid"] when "declination" yielder << [layer, "type" => "Declination"] when "controls" yielder << [layer, "type" => "Control"] when /\.yml$/i basedir ||= path.parent raise "couldn't find '#{layer}'" unless path.file? case contents = YAML.load(path.read) when Array contents.reverse.map do |item| Pathname(item.to_s) end.each do |relative_path| raise "#{relative_path} is not a relative path" unless relative_path.relative? layers.prepend [Pathname(relative_path).expand_path(path.parent).relative_path_from(basedir).to_s, basedir] end when Hash name = path.sub_ext("").relative_path_from(basedir).descend.map(&:basename).join(?.) yielder << [name, contents.merge("source" => path)] else raise "couldn't parse #{path}" end else path = Pathname("#{layer}.yml") raise "#{layer} is not a relative path" unless path.relative? basedir ||= layer_dirs.find do |root| path.expand_path(root).file? end layers.prepend [path.to_s, basedir] end end rescue YAML::Exception raise "couldn't parse #{path}" end.map do |name, params| params.merge! options.transform_keys(&:to_s) params.merge! Config[name] if Config[name] Layer.new(name, map, params) end.tap do |layers| raise OptionParser::MissingArgument, "no layers specified" unless layers.any? unless layers.one? raise OptionParser::InvalidArgument, "can't specify opacity when adding multiple layers" if options[:opacity] raise OptionParser::InvalidArgument, "can't specify data path when adding multiple layers" if options[:path] end map.add *layers, **create_options end end
def config(layer = nil, **options)
def config(layer = nil, **options) path, resolution = options[:path], options[:resolution] layer = Layer.sanitise layer case when !layer raise OptionParser::InvalidArgument, "no layer name specified for path" if path raise OptionParser::InvalidArgument, "no layer name specified for resolution" if resolution when path || resolution Config.store layer, "path", path.to_s if path Config.store layer, "resolution", resolution if resolution end options.each do |key, value| case key when :chrome raise "chrome path is not an executable" unless value.executable? && !value.directory? Config.store key.to_s, value.to_s when :"layer-dir" raise "not a directory: %s" % value unless value.directory? Config.store key.to_s, value.to_s when *%i[labelling debug gpu versioning zlib-level knockout] Config.store key.to_s, value when :delete Config.delete *layer, value end end if options.empty? puts Config.to_str.each_line.drop(1) log_neutral "no configuration yet" if Config.empty? else Config.save log_success "configuration updated" end end
def contours(archive, dem_path, **options)
def contours(archive, dem_path, **options) add archive, "contours", **options, path: Pathname(dem_path) end
def controls(archive, gps_path, **options)
def controls(archive, gps_path, **options) raise OptionParser::InvalidArgument, gps_path unless gps_path =~ /\.(gpx|kml)$/i add archive, "controls", **options, path: Pathname(gps_path) end
def declination(archive, **options)
def declination(archive, **options) add archive, "declination", **options end
def delete(archive, *names, **options)
def delete(archive, *names, **options) map = Map.load archive names.map do |name| Layer.sanitise name end.uniq.map do |name| name[?*] ? %r[^#{name.gsub(?., '\.').gsub(?*, '.*')}$] : name end.tap do |names| map.delete *names end end
def grid(archive, **options)
def grid(archive, **options) add archive, "grid", **options end
def info(archive, **options)
def info(archive, **options) raise OptionParser::InvalidArgument, "one output option only" if options.slice(:json, :proj).length > 1 puts Map.load(archive).info(**options) end
def init(archive, **options)
def init(archive, **options) puts Map.init(archive, **options) end
def inspect(url_or_path, layer: nil, coords: nil, codes: nil, countwise: nil, **options)
def inspect(url_or_path, layer: nil, coords: nil, codes: nil, countwise: nil, **options) options[:geometry] = GeoJSON.multipoint(coords).bbox if coords case url_or_path when ArcGIS::Service source = ArcGIS::Service.new(url_or_path) when Shapefile::Source raise OptionParser::InvalidOption, "--id only applies to ArcGIS layers" if options[:id] raise OptionParser::InvalidOption, "--decode only applies to ArcGIS layers" if options[:decode] raise OptionParser::InvalidOption, "--codes only applies to ArcGIS layers" if codes source = Shapefile::Source.new(url_or_path) layer ||= source.only_layer else raise OptionParser::InvalidArgument, url_or_path end layer = source.layer(layer: layer, **options) case when codes TreeIndenter.new(layer.codes) do |level| level.map do |key, values| case key when Array code, value = key display_value = value.nil? || /[\t\n\r]/ === value ? value.inspect : value ["#{code} → #{display_value}", values] else ["#{key}:", values] end end end.each do |indents, info| puts indents.join << info end when fields = options[:fields] template = "%%%is │ %%%is │ %%s" TreeIndenter.new(layer.counts) do |counts| counts.group_by do |attributes, count| attributes.shift end.entries.select(&:first).map do |(name, value), counts| [[name, counts.sum(&:last), value], counts] end.sort do |((name1, count1, value1), counts1), ((name2, count2, value2), counts2)| next count2 <=> count1 if countwise value1 && value2 ? value1 <=> value2 : value1 ? 1 : value2 ? -1 : 0 end end.map do |indents, (name, count, value)| next name, count.to_s, indents.join << (value.nil? || /[\t\n\r]/ === value ? value.inspect : value.to_s) end.transpose.tap do |names, counts, lines| template %= [names.map(&:size).max, counts.map(&:size).max] if names end.transpose.each do |row| puts template % row end else TreeIndenter.new(layer.info) do |hash| hash.map do |key, value| Hash === value ? ["#{key}:", value] : "#{key}: #{value}" end end.each do |indents, info| puts indents.join << info end end rescue ArcGIS::Layer::NoLayerError, Shapefile::Layer::NoLayerError => error raise OptionParser::MissingArgument, error.message if codes || countwise || options.any? puts "layers:" TreeIndenter.new(source.layer_info, []).each do |indents, info| puts indents.join << info end rescue ArcGIS::Renderer::TooManyFieldsError raise OptionParser::InvalidOption, "use less fields with --fields" end
def layer_dirs
def layer_dirs @layer_dirs ||= Array(Config["layer-dir"]).map(&Pathname.method(:new)) << Pathname.pwd end
def layers(state: nil)
def layers(state: nil) paths = layer_dirs.grep_v(Pathname.pwd).flat_map do |directory| Array(state).inject(directory, &:/).glob("*") end.sort log_warn "no layers installed" if paths.none? TreeIndenter.new(paths) do |paths| paths.filter_map do |path| case when path.glob("**/*.yml").any? [path.basename.sub_ext(""), path.children.sort] when path.sub_ext("").directory? when path.extname == ".yml" path.basename.sub_ext("") end end end.each do |indents, name| puts [*indents, name].join end end
def move(archive, name, **options)
def move(archive, name, **options) raise OptionParser::InvalidArgument, "only one of --before and --after allowed" if options[:after] && options[:before] raise OptionParser::MissingArgument, "--before or --after required" unless options[:after] || options[:before] Map.load(archive).move(name, **options) end
def overlay(archive, gps_path, **options)
def overlay(archive, gps_path, **options) raise OptionParser::InvalidArgument, gps_path unless gps_path =~ /\.(gpx|kml)$/i add archive, gps_path, **options, path: Pathname(gps_path) end
def relief(archive, dem_path, **options)
def relief(archive, dem_path, **options) add archive, "relief", **options, path: Pathname(dem_path) end
def render(archive, basename, *formats, overwrite: false, svg_path: nil, **options)
def render(archive, basename, *formats, overwrite: false, svg_path: nil, **options) case when formats.any? when svg_path raise OptionParser::MissingArgument, "no output format specified" else formats << "svg" end formats.map do |format| Pathname(Formats === format ? "#{basename}.#{format}" : format) end.uniq.each do |path| format = path.extname.delete_prefix(?.) raise "unrecognised format: #{path}" if format.empty? raise "unrecognised format: #{format}" unless Formats === format raise "already a directory: #{path}" if path.directory? raise "file already exists: #{path}" if path.exist? && !overwrite raise "no such directory: #{path.parent}" unless path.parent.directory? end.tap do |paths| map = svg_path ? Map.from_svg(archive, svg_path) : Map.load(archive) map.render *paths, **options end end
def scrape(url, path, coords: nil, name: nil, epsg: nil, paginate: nil, concat: nil, **options)
def scrape(url, path, coords: nil, name: nil, epsg: nil, paginate: nil, concat: nil, **options) flags = %w[-skipfailures] flags += %W[-t_srs epsg:#{epsg}] if epsg flags += %W[-nln #{name}] if name format_flags = case path.to_s when Shapefile::Source then %w[-update -overwrite] when /\.sqlite3?$/ then %w[-f SQLite -dsco SPATIALITE=YES] when /\.db$/ then %w[-f SQLite -dsco SPATIALITE=YES] when /\.gpkg$/ then %w[-f GPKG] when /\.tab$/ then ["-f", "MapInfo File"] else ["-f", "ESRI Shapefile", "-lco", "ENCODING=UTF-8"] end options.merge! case path.to_s when /\.sqlite3?$/ then { mixed: concat, launder: true } when /\.db$/ then { mixed: concat, launder: true } when /\.gpkg$/ then { mixed: concat, launder: true } when /\.tab$/ then { } else { truncate: 10 } end options[:geometry] = GeoJSON.multipoint(coords).bbox if coords log_update "nswtopo: contacting server" layer = ArcGIS::Service.new(url).layer(**options) queue = Queue.new thread = Thread.new do while page = queue.pop *, status = Open3.capture3 *%W[ogr2ogr #{path} /vsistdin/], *flags, *format_flags, stdin_data: page.to_json format_flags = %w[-update -append] queue.close unless status.success? end status end total_features, percent = "%i feature%s", "%%.%if%%%%" Enumerator.new do |yielder| hold, ok, count = [], nil, 0 layer.paged(per_page: paginate).tap do total_features %= [layer.count, (?s unless layer.count == 1)] percent %= layer.count < 1000 ? 0 : layer.count < 10000 ? 1 : 2 log_update "nswtopo: retrieving #{total_features}" end.each do |page| log_update "nswtopo: retrieving #{percent} of #{total_features}" % [100.0 * (count += page.count) / layer.count] next hold << page if concat next yielder << page if ok next hold << page if page.all? do |feature| feature.properties.values.any?(&:nil?) end yielder << page ok = true end next hold.inject(yielder, &:<<) if ok && !concat next yielder << hold.inject(&:merge!) if hold.any? end.inject(queue) do |queue, page| queue << page rescue ClosedQueueError break queue end.close log_update "nswtop: saving #{total_features}" raise "error while saving features" unless thread.value&.success? log_success "saved #{total_features}" rescue ArcGIS::Layer::NoLayerError raise OptionParser::InvalidArgument, "specify an ArcGIS layer in URL or with --layer" rescue ArcGIS::Map::NoUniqueFieldError raise OptionParser::InvalidOption, "--unique required for this layer" rescue ArcGIS::Renderer::NoGeometryError raise OptionParser::InvalidOption, "--coords not available for this layer" rescue ArcGIS::Query::UniqueFieldError raise OptionParser::InvalidOption, "--unique not available for this layer" rescue ArcGIS::Service::InvalidURLError raise OptionParser::InvalidArgument, url end
def spot_heights(archive, dem_path, **options)
def spot_heights(archive, dem_path, **options) add archive, "spot-heights", **options, path: Pathname(dem_path) end