nswtopo 2.0.0.pre.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/COPYING +674 -0
- data/bin/nswtopo +430 -0
- data/docs/README.md +78 -0
- data/docs/add.md +49 -0
- data/docs/config.md +24 -0
- data/docs/contours.md +37 -0
- data/docs/controls.md +9 -0
- data/docs/declination.md +15 -0
- data/docs/delete.md +15 -0
- data/docs/grid.md +5 -0
- data/docs/info.md +5 -0
- data/docs/init.md +38 -0
- data/docs/layers.md +11 -0
- data/docs/overlay.md +37 -0
- data/docs/relief.md +22 -0
- data/docs/render.md +43 -0
- data/docs/spot-heights.md +23 -0
- data/lib/nswtopo/archive.rb +93 -0
- data/lib/nswtopo/avl_tree.rb +128 -0
- data/lib/nswtopo/config.rb +73 -0
- data/lib/nswtopo/dither.rb +31 -0
- data/lib/nswtopo/font/chrome.rb +59 -0
- data/lib/nswtopo/font/generic.rb +25 -0
- data/lib/nswtopo/font.rb +43 -0
- data/lib/nswtopo/formats/kmz.rb +149 -0
- data/lib/nswtopo/formats/mbtiles.rb +64 -0
- data/lib/nswtopo/formats/pdf.rb +31 -0
- data/lib/nswtopo/formats/svg.rb +69 -0
- data/lib/nswtopo/formats/svgz.rb +13 -0
- data/lib/nswtopo/formats/zip.rb +40 -0
- data/lib/nswtopo/formats.rb +76 -0
- data/lib/nswtopo/geometry/overlap.rb +78 -0
- data/lib/nswtopo/geometry/r_tree.rb +47 -0
- data/lib/nswtopo/geometry/segment.rb +27 -0
- data/lib/nswtopo/geometry/straight_skeleton/collapse.rb +21 -0
- data/lib/nswtopo/geometry/straight_skeleton/interior_node.rb +17 -0
- data/lib/nswtopo/geometry/straight_skeleton/node.rb +50 -0
- data/lib/nswtopo/geometry/straight_skeleton/nodes.rb +295 -0
- data/lib/nswtopo/geometry/straight_skeleton/split.rb +33 -0
- data/lib/nswtopo/geometry/straight_skeleton/vertex.rb +9 -0
- data/lib/nswtopo/geometry/straight_skeleton.rb +6 -0
- data/lib/nswtopo/geometry/vector.rb +91 -0
- data/lib/nswtopo/geometry/vector_sequence.rb +179 -0
- data/lib/nswtopo/geometry.rb +8 -0
- data/lib/nswtopo/gis/arcgis_server/connection.rb +52 -0
- data/lib/nswtopo/gis/arcgis_server.rb +155 -0
- data/lib/nswtopo/gis/dem.rb +70 -0
- data/lib/nswtopo/gis/esri_hdr.rb +77 -0
- data/lib/nswtopo/gis/gdal_glob.rb +41 -0
- data/lib/nswtopo/gis/geojson/collection.rb +94 -0
- data/lib/nswtopo/gis/geojson/line_string.rb +11 -0
- data/lib/nswtopo/gis/geojson/multi_line_string.rb +63 -0
- data/lib/nswtopo/gis/geojson/multi_point.rb +12 -0
- data/lib/nswtopo/gis/geojson/multi_polygon.rb +167 -0
- data/lib/nswtopo/gis/geojson/point.rb +9 -0
- data/lib/nswtopo/gis/geojson/polygon.rb +11 -0
- data/lib/nswtopo/gis/geojson.rb +89 -0
- data/lib/nswtopo/gis/gps/gpx.rb +22 -0
- data/lib/nswtopo/gis/gps/kml.rb +66 -0
- data/lib/nswtopo/gis/gps.rb +20 -0
- data/lib/nswtopo/gis/projection.rb +56 -0
- data/lib/nswtopo/gis/shapefile.rb +24 -0
- data/lib/nswtopo/gis/world_file.rb +19 -0
- data/lib/nswtopo/gis.rb +9 -0
- data/lib/nswtopo/help_formatter.rb +59 -0
- data/lib/nswtopo/helpers/array.rb +30 -0
- data/lib/nswtopo/helpers/colour.rb +176 -0
- data/lib/nswtopo/helpers/concurrently.rb +27 -0
- data/lib/nswtopo/helpers/dir.rb +7 -0
- data/lib/nswtopo/helpers/hash.rb +15 -0
- data/lib/nswtopo/helpers/tar_writer.rb +11 -0
- data/lib/nswtopo/helpers.rb +6 -0
- data/lib/nswtopo/layer/arcgis_raster.rb +73 -0
- data/lib/nswtopo/layer/contour.rb +233 -0
- data/lib/nswtopo/layer/control.rb +94 -0
- data/lib/nswtopo/layer/declination.rb +53 -0
- data/lib/nswtopo/layer/feature.rb +87 -0
- data/lib/nswtopo/layer/grid.rb +120 -0
- data/lib/nswtopo/layer/import.rb +25 -0
- data/lib/nswtopo/layer/labels/fence.rb +20 -0
- data/lib/nswtopo/layer/labels.rb +630 -0
- data/lib/nswtopo/layer/overlay.rb +53 -0
- data/lib/nswtopo/layer/raster.rb +63 -0
- data/lib/nswtopo/layer/relief.rb +143 -0
- data/lib/nswtopo/layer/spot.rb +171 -0
- data/lib/nswtopo/layer/vector.rb +263 -0
- data/lib/nswtopo/layer/vegetation.rb +73 -0
- data/lib/nswtopo/layer.rb +78 -0
- data/lib/nswtopo/log.rb +28 -0
- data/lib/nswtopo/map.rb +296 -0
- data/lib/nswtopo/os.rb +75 -0
- data/lib/nswtopo/safely.rb +13 -0
- data/lib/nswtopo/version.rb +4 -0
- data/lib/nswtopo/zip.rb +15 -0
- data/lib/nswtopo.rb +249 -0
- 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
|