nswtopo 2.0.0.pre.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +7 -0
  2. data/COPYING +674 -0
  3. data/bin/nswtopo +430 -0
  4. data/docs/README.md +78 -0
  5. data/docs/add.md +49 -0
  6. data/docs/config.md +24 -0
  7. data/docs/contours.md +37 -0
  8. data/docs/controls.md +9 -0
  9. data/docs/declination.md +15 -0
  10. data/docs/delete.md +15 -0
  11. data/docs/grid.md +5 -0
  12. data/docs/info.md +5 -0
  13. data/docs/init.md +38 -0
  14. data/docs/layers.md +11 -0
  15. data/docs/overlay.md +37 -0
  16. data/docs/relief.md +22 -0
  17. data/docs/render.md +43 -0
  18. data/docs/spot-heights.md +23 -0
  19. data/lib/nswtopo/archive.rb +93 -0
  20. data/lib/nswtopo/avl_tree.rb +128 -0
  21. data/lib/nswtopo/config.rb +73 -0
  22. data/lib/nswtopo/dither.rb +31 -0
  23. data/lib/nswtopo/font/chrome.rb +59 -0
  24. data/lib/nswtopo/font/generic.rb +25 -0
  25. data/lib/nswtopo/font.rb +43 -0
  26. data/lib/nswtopo/formats/kmz.rb +149 -0
  27. data/lib/nswtopo/formats/mbtiles.rb +64 -0
  28. data/lib/nswtopo/formats/pdf.rb +31 -0
  29. data/lib/nswtopo/formats/svg.rb +69 -0
  30. data/lib/nswtopo/formats/svgz.rb +13 -0
  31. data/lib/nswtopo/formats/zip.rb +40 -0
  32. data/lib/nswtopo/formats.rb +76 -0
  33. data/lib/nswtopo/geometry/overlap.rb +78 -0
  34. data/lib/nswtopo/geometry/r_tree.rb +47 -0
  35. data/lib/nswtopo/geometry/segment.rb +27 -0
  36. data/lib/nswtopo/geometry/straight_skeleton/collapse.rb +21 -0
  37. data/lib/nswtopo/geometry/straight_skeleton/interior_node.rb +17 -0
  38. data/lib/nswtopo/geometry/straight_skeleton/node.rb +50 -0
  39. data/lib/nswtopo/geometry/straight_skeleton/nodes.rb +295 -0
  40. data/lib/nswtopo/geometry/straight_skeleton/split.rb +33 -0
  41. data/lib/nswtopo/geometry/straight_skeleton/vertex.rb +9 -0
  42. data/lib/nswtopo/geometry/straight_skeleton.rb +6 -0
  43. data/lib/nswtopo/geometry/vector.rb +91 -0
  44. data/lib/nswtopo/geometry/vector_sequence.rb +179 -0
  45. data/lib/nswtopo/geometry.rb +8 -0
  46. data/lib/nswtopo/gis/arcgis_server/connection.rb +52 -0
  47. data/lib/nswtopo/gis/arcgis_server.rb +155 -0
  48. data/lib/nswtopo/gis/dem.rb +70 -0
  49. data/lib/nswtopo/gis/esri_hdr.rb +77 -0
  50. data/lib/nswtopo/gis/gdal_glob.rb +41 -0
  51. data/lib/nswtopo/gis/geojson/collection.rb +94 -0
  52. data/lib/nswtopo/gis/geojson/line_string.rb +11 -0
  53. data/lib/nswtopo/gis/geojson/multi_line_string.rb +63 -0
  54. data/lib/nswtopo/gis/geojson/multi_point.rb +12 -0
  55. data/lib/nswtopo/gis/geojson/multi_polygon.rb +167 -0
  56. data/lib/nswtopo/gis/geojson/point.rb +9 -0
  57. data/lib/nswtopo/gis/geojson/polygon.rb +11 -0
  58. data/lib/nswtopo/gis/geojson.rb +89 -0
  59. data/lib/nswtopo/gis/gps/gpx.rb +22 -0
  60. data/lib/nswtopo/gis/gps/kml.rb +66 -0
  61. data/lib/nswtopo/gis/gps.rb +20 -0
  62. data/lib/nswtopo/gis/projection.rb +56 -0
  63. data/lib/nswtopo/gis/shapefile.rb +24 -0
  64. data/lib/nswtopo/gis/world_file.rb +19 -0
  65. data/lib/nswtopo/gis.rb +9 -0
  66. data/lib/nswtopo/help_formatter.rb +59 -0
  67. data/lib/nswtopo/helpers/array.rb +30 -0
  68. data/lib/nswtopo/helpers/colour.rb +176 -0
  69. data/lib/nswtopo/helpers/concurrently.rb +27 -0
  70. data/lib/nswtopo/helpers/dir.rb +7 -0
  71. data/lib/nswtopo/helpers/hash.rb +15 -0
  72. data/lib/nswtopo/helpers/tar_writer.rb +11 -0
  73. data/lib/nswtopo/helpers.rb +6 -0
  74. data/lib/nswtopo/layer/arcgis_raster.rb +73 -0
  75. data/lib/nswtopo/layer/contour.rb +233 -0
  76. data/lib/nswtopo/layer/control.rb +94 -0
  77. data/lib/nswtopo/layer/declination.rb +53 -0
  78. data/lib/nswtopo/layer/feature.rb +87 -0
  79. data/lib/nswtopo/layer/grid.rb +120 -0
  80. data/lib/nswtopo/layer/import.rb +25 -0
  81. data/lib/nswtopo/layer/labels/fence.rb +20 -0
  82. data/lib/nswtopo/layer/labels.rb +630 -0
  83. data/lib/nswtopo/layer/overlay.rb +53 -0
  84. data/lib/nswtopo/layer/raster.rb +63 -0
  85. data/lib/nswtopo/layer/relief.rb +143 -0
  86. data/lib/nswtopo/layer/spot.rb +171 -0
  87. data/lib/nswtopo/layer/vector.rb +263 -0
  88. data/lib/nswtopo/layer/vegetation.rb +73 -0
  89. data/lib/nswtopo/layer.rb +78 -0
  90. data/lib/nswtopo/log.rb +28 -0
  91. data/lib/nswtopo/map.rb +296 -0
  92. data/lib/nswtopo/os.rb +75 -0
  93. data/lib/nswtopo/safely.rb +13 -0
  94. data/lib/nswtopo/version.rb +4 -0
  95. data/lib/nswtopo/zip.rb +15 -0
  96. data/lib/nswtopo.rb +249 -0
  97. metadata +142 -0
@@ -0,0 +1,53 @@
1
+ module NSWTopo
2
+ module Declination
3
+ include Vector
4
+ CREATE = %w[angle spacing arrows offset]
5
+ DEFAULTS = YAML.load <<~YAML
6
+ spacing: 40.0
7
+ offset: 0.0
8
+ arrows: 160.0
9
+ stroke: darkred
10
+ stroke-width: 0.1
11
+ symbol:
12
+ path:
13
+ d: M 0 0 L 0.4 2 L 0 1.3 L -0.4 2 Z
14
+ stroke: none
15
+ YAML
16
+
17
+ def get_features
18
+ @params["fill"] ||= @params["stroke"]
19
+ declination = @angle || @map.declination
20
+ col_spacing = 0.001 * @map.scale * @spacing
21
+ row_spacing = 0.001 * @map.scale * @arrows * 0.5
22
+ col_offset = 0.001 * @map.scale * (@offset % @spacing)
23
+
24
+ radius = 0.5 * @map.bounds.transpose.distance
25
+ j_max = (radius / col_spacing).ceil
26
+ i_max = (radius / row_spacing).ceil
27
+
28
+ collection = GeoJSON::Collection.new(@map.projection)
29
+ (-j_max..j_max).each do |j|
30
+ x = j * col_spacing + col_offset
31
+ coordinates = [[x, -radius], [x, radius]].map do |point|
32
+ point.rotate_by_degrees(-declination).plus(@map.centre)
33
+ end
34
+ collection.add_linestring coordinates
35
+ (-i_max..i_max).reject(&j.even? ? :even? : :odd?).map do |i|
36
+ [x, i * row_spacing].rotate_by_degrees(-declination).plus(@map.centre)
37
+ end.each do |coordinates|
38
+ collection.add_point coordinates, "rotation" => declination
39
+ end
40
+ end
41
+ collection
42
+ end
43
+
44
+ def to_s
45
+ lines = features.grep(GeoJSON::LineString)
46
+ return @name if lines.none?
47
+ line = lines.map(&:coordinates).max_by(&:distance)
48
+ angle = 90 - 180 * Math::atan2(*line.difference.reverse) / Math::PI
49
+ "%s: %i line%s at %.1f°%s" % [@name, lines.length, (?s unless lines.one?), angle.abs, angle > 0 ? ?E : angle < 0 ? ?W : nil]
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1,87 @@
1
+ module NSWTopo
2
+ module Feature
3
+ include Vector, ArcGISServer, Shapefile, Log
4
+ CREATE = %w[features]
5
+
6
+ def get_features
7
+ (Array === @features ? @features : [@features]).map do |args|
8
+ case args
9
+ when Hash then args.transform_keys(&:to_sym)
10
+ when String then { source: args }
11
+ else raise "#{@source.basename}: invalid or no features specified"
12
+ end
13
+ end.slice_before do |args|
14
+ !args[:fallback]
15
+ end.map do |fallbacks|
16
+ options, collection, error = fallbacks.inject [{}, nil, nil] do |(options, *), source: nil, fallback: false, **args|
17
+ source = @path if @path
18
+ log_update "%s: %s" % [@name, fallback ? "failed to retrieve features, trying fallback source" : "retrieving features"]
19
+ raise "#{@source.basename}: no feature source defined" unless source
20
+ options.merge! args
21
+ break options, arcgis_layer(source, margin: MARGIN, **options.slice(:where, :layer, :per_page)) do |index, total|
22
+ log_update "%s: retrieved %i of %i feature%s" % [@name, index, total, (?s if total > 1)]
23
+ end if ArcGISServer === source
24
+ source_path = Pathname(source).expand_path(@source.parent)
25
+ break options, shapefile_layer(source_path, margin: MARGIN, **options.slice(:where, :sql, :layer)) if Shapefile === source_path
26
+ raise "#{@source.basename}: invalid feature source: #{source}"
27
+ rescue ArcGISServer::Error => error
28
+ next options, nil, error
29
+ end
30
+
31
+ raise error if error
32
+ next collection.reproject_to(@map.projection), options
33
+ end.each do |collection, options|
34
+ rotation_attribute, arithmetic = case options[:rotation]
35
+ when /^90 - (\w+)$/ then [$1, true]
36
+ when String then options[:rotation]
37
+ end
38
+
39
+ collection.each do |feature|
40
+ categories = [*options[:category]].map do |category|
41
+ Hash === category ? [*category] : [category]
42
+ end.flatten(1).map do |attribute, substitutions|
43
+ value = feature.fetch(attribute, attribute)
44
+ substitutions ? substitutions.fetch(value, value) : value
45
+ end
46
+
47
+ options[:sizes].tap do |mm, max = 9|
48
+ unit = 0.001 * (mm == true ? 5 : mm) * @map.scale
49
+ case feature
50
+ when GeoJSON::LineString, GeoJSON::MultiLineString
51
+ size = (Math::log2(feature.length) - Math::log2(unit)).ceil rescue 0
52
+ categories << size.clamp(0, max)
53
+ when GeoJSON::Polygon, GeoJSON::MultiPolygon
54
+ size = (0.5 * Math::log2(feature.area) - Math::log2(unit)).ceil rescue 0
55
+ categories << size.clamp(0, max)
56
+ end
57
+ end if options[:sizes]
58
+
59
+ rotation = case feature
60
+ when GeoJSON::Point, GeoJSON::MultiPoint
61
+ value = begin
62
+ Float feature.fetch(rotation_attribute)
63
+ rescue KeyError, TypeError, ArgumentError
64
+ 0.0
65
+ end
66
+ categories << (value.zero? ? "unrotated" : "rotated")
67
+ arithmetic ? 90 - value : value
68
+ end if rotation_attribute
69
+
70
+ labels = Array(options[:label]).map do |attribute|
71
+ feature.fetch(attribute, attribute)
72
+ end.map(&:to_s).reject(&:empty?)
73
+
74
+ categories = categories.map(&:to_s).reject(&:empty?).map(&method(:categorise))
75
+ properties = {}
76
+ properties["category"] = categories if categories.any?
77
+ properties["label"] = labels if labels.any?
78
+ properties["draw"] = false if options[:draw] == false
79
+ properties["draw"] = false if @name =~ /-labels$/
80
+ properties["rotation"] = rotation if rotation
81
+
82
+ feature.properties.replace properties
83
+ end
84
+ end.map(&:first).inject(&:merge)
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,120 @@
1
+ module NSWTopo
2
+ module Grid
3
+ include Vector
4
+ CREATE = %w[interval]
5
+ INSET = 1.5
6
+ DEFAULTS = YAML.load <<~YAML
7
+ interval: 1000.0
8
+ stroke: black
9
+ stroke-width: 0.1
10
+ boundary:
11
+ stroke: gray
12
+ labels:
13
+ dupe: outline
14
+ outline:
15
+ stroke: white
16
+ fill: none
17
+ stroke-width: 10%
18
+ opacity: 0.65
19
+ font-family: Arial Narrow, sans-serif
20
+ font-size: 2.3
21
+ stroke: none
22
+ fill: black
23
+ orientation: uphill
24
+ YAML
25
+
26
+ def get_features
27
+ Projection.utm_zones(@map.bounding_box).map do |zone|
28
+ utm, utm_hull = Projection.utm(zone), Projection.utm_hull(zone)
29
+ map_hull = @map.bounding_box(MARGIN).reproject_to_wgs84.coordinates.first
30
+
31
+ eastings, northings = @map.bounds(projection: utm).map do |min, max|
32
+ (min / @interval).floor..(max / @interval).ceil
33
+ end.map do |counts|
34
+ counts.map { |count| count * @interval }
35
+ end
36
+
37
+ grid = eastings.map do |easting|
38
+ [easting].product northings.reverse
39
+ end
40
+
41
+ eastings, northings = [grid, grid.transpose].map.with_index do |lines, index|
42
+ lines.inject GeoJSON::Collection.new(utm) do |collection, line|
43
+ coord = line[0][index]
44
+ label = [coord / 100000, (coord / 1000) % 100]
45
+ label << coord % 1000 unless @interval % 1000 == 0
46
+ collection.add_linestring line, "label" => label, "ends" => [0, 1], "category" => index.zero? ? "easting" : "northing"
47
+ end.reproject_to_wgs84.clip!(utm_hull).clip!(map_hull).each do |linestring|
48
+ linestring["ends"].delete 0 if linestring.coordinates[0][0] % 6 < 0.00001
49
+ linestring["ends"].delete 1 if linestring.coordinates[-1][0] % 6 < 0.00001
50
+ end
51
+ end
52
+
53
+ boundary_points = GeoJSON.multilinestring(grid.transpose, projection: utm).reproject_to_wgs84.clip!(utm_hull).coordinates.map(&:first)
54
+ boundary = GeoJSON.linestring boundary_points, properties: { "category" => "boundary" }
55
+
56
+ [eastings, northings, boundary]
57
+ end.flatten.inject(&:merge)
58
+ end
59
+
60
+ def label_element(labels, label_params)
61
+ font_size = label_params["font-size"]
62
+ parts = labels.zip(%w[%d %02d %03d]).map do |part, format|
63
+ format % part
64
+ end.zip([80, 100, 80])
65
+
66
+ text_path = REXML::Element.new("textPath")
67
+ parts.each.with_index do |(text, percent), index|
68
+ tspan = text_path.add_element "tspan", "font-size" => "#{percent}%"
69
+ tspan.add_attributes "dy" => VALUE % (Labels::CENTRELINE_FRACTION * font_size) if index.zero?
70
+ tspan.add_text text
71
+ end
72
+
73
+ text_length = parts.flat_map do |text, percent|
74
+ [Font.glyph_length(?\s, label_params), Font.glyph_length(text, label_params.merge("font-size" => font_size * percent / 100.0))]
75
+ end.drop(1).sum
76
+ text_path.add_attribute "textLength", VALUE % text_length
77
+ [text_length, text_path]
78
+ end
79
+
80
+ def labeling_features
81
+ label_params = @params["labels"]
82
+ font_size = label_params["font-size"]
83
+ offset = 0.85 * font_size * @map.scale / 1000.0
84
+ inset = INSET + font_size * 0.5 * Math::sin(@map.rotation.abs * Math::PI / 180)
85
+ inset_hull = @map.bounding_box(mm: -inset).coordinates.first
86
+
87
+ gridlines = features.select do |linestring|
88
+ linestring["label"]
89
+ end
90
+ eastings = gridlines.select do |gridline|
91
+ gridline["category"] == "easting"
92
+ end
93
+
94
+ flip_eastings = eastings.partition do |easting|
95
+ Math::atan2(*easting.coordinates.values_at(0, -1).inject(&:minus)) * 180.0 / Math::PI > @map.rotation
96
+ end.map(&:length).inject(&:>)
97
+ eastings.each do |easting|
98
+ easting.coordinates.reverse!
99
+ easting["ends"].map! { |index| 1 - index }
100
+ end if flip_eastings
101
+
102
+ gridlines.map do |gridline|
103
+ gridline.offset(offset, splits: false).clip(inset_hull)
104
+ end.compact.flat_map do |gridline|
105
+ label, ends = gridline.values_at "label", "ends"
106
+ %i[itself reverse].values_at(*ends).map do |order|
107
+ text_length, text_path = label_element(label, label_params)
108
+ segment = gridline.coordinates.send(order).take(2)
109
+ fraction = text_length * @map.scale / 1000.0 / segment.distance
110
+ coordinates = [segment[0], segment.along(fraction)].send(order)
111
+ GeoJSON::LineString.new coordinates, "label" => text_path
112
+ end
113
+ end
114
+ end
115
+
116
+ def to_s
117
+ @name
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,25 @@
1
+ module NSWTopo
2
+ module Import
3
+ include Raster
4
+
5
+ def get_raster(temp_dir)
6
+ crop_path = temp_dir / "crop.tif"
7
+
8
+ json = begin
9
+ OS.gdalinfo "-json", @path
10
+ rescue OS::Error
11
+ raise "invalid raster file: #{@path}"
12
+ end
13
+ palette = JSON.parse(json)["bands"].any? do |band|
14
+ "Palette" == band["colorInterpretation"]
15
+ end
16
+
17
+ projection = Projection.new(@path)
18
+ args = ["-projwin", *@map.projwin(projection), @path, crop_path]
19
+ args = ["-expand", "rgba", *args] if palette
20
+ OS.gdal_translate *args
21
+
22
+ return Numeric === @resolution ? @resolution : @map.get_raster_resolution(crop_path), crop_path
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,20 @@
1
+ module NSWTopo
2
+ module Labels
3
+ class Fence
4
+ def initialize(segment, buffer: 0, index:)
5
+ @segment, @buffer, @index = segment, buffer, index
6
+ end
7
+ attr_reader :index
8
+
9
+ def bounds
10
+ @segment.transpose.map(&:minmax).map do |min, max|
11
+ [min - @buffer, max + @buffer]
12
+ end
13
+ end
14
+
15
+ def conflicts_with?(segment, buffer = 0)
16
+ [@segment, segment].overlap?(@buffer + buffer)
17
+ end
18
+ end
19
+ end
20
+ end