nswtopo 3.0.1 → 3.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|