nswtopo 3.0.1 → 3.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 +19 -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 +4 -4
- 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 +59 -13
- 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 +3 -3
- 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 +56 -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
data/lib/nswtopo/gis/geojson.rb
CHANGED
@@ -7,36 +7,50 @@ module NSWTopo
|
|
7
7
|
|
8
8
|
CLASSES = TYPES.map do |type|
|
9
9
|
klass = Class.new do
|
10
|
-
def initialize(coordinates, properties =
|
11
|
-
properties
|
12
|
-
raise Error, "invalid feature properties" unless Hash === properties
|
13
|
-
|
14
|
-
@coordinates
|
15
|
-
|
16
|
-
|
17
|
-
attr_accessor :coordinates, :properties
|
18
|
-
|
19
|
-
define_method :to_h do
|
20
|
-
{
|
21
|
-
"type" => "Feature",
|
22
|
-
"geometry" => {
|
23
|
-
"type" => type,
|
24
|
-
"coordinates" => @coordinates
|
25
|
-
},
|
26
|
-
"properties" => @properties
|
27
|
-
}
|
10
|
+
def initialize(coordinates, properties = nil, &block)
|
11
|
+
@coordinates, @properties = coordinates, properties || {}
|
12
|
+
raise Error, "invalid feature properties" unless Hash === @properties
|
13
|
+
instance_eval(&block) if block_given?
|
14
|
+
@coordinates.freeze
|
15
|
+
@properties.freeze
|
16
|
+
freeze!
|
28
17
|
end
|
29
18
|
|
19
|
+
alias freeze! freeze
|
20
|
+
|
21
|
+
attr_reader :coordinates, :properties
|
22
|
+
|
30
23
|
extend Forwardable
|
31
|
-
delegate %i[[]
|
24
|
+
delegate %i[[] fetch values_at key? slice except dig] => :@properties
|
25
|
+
delegate %i[empty?] => :@coordinates
|
26
|
+
delegate %i[to_json] => :to_h
|
27
|
+
end
|
28
|
+
|
29
|
+
klass.define_method :to_h do
|
30
|
+
{
|
31
|
+
"type" => "Feature",
|
32
|
+
"geometry" => {
|
33
|
+
"type" => type,
|
34
|
+
"coordinates" => @coordinates
|
35
|
+
},
|
36
|
+
"properties" => @properties
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
klass.define_method :with_properties do |properties|
|
41
|
+
klass.new @coordinates, **properties
|
32
42
|
end
|
33
43
|
|
34
|
-
|
44
|
+
klass.define_method :add_properties do |properties|
|
45
|
+
klass.new @coordinates, **@properties, **properties
|
46
|
+
end
|
47
|
+
|
48
|
+
next type, const_set(type, klass)
|
35
49
|
end
|
36
50
|
|
37
|
-
CLASSES.
|
38
|
-
Collection.define_method "add_#{type}".downcase do |coordinates, properties =
|
39
|
-
self << klass
|
51
|
+
CLASSES.each do |type, klass|
|
52
|
+
Collection.define_method "add_#{type}".downcase do |coordinates, properties = nil|
|
53
|
+
self << klass[coordinates, properties]
|
40
54
|
end
|
41
55
|
|
42
56
|
Collection.define_method "#{type}s".downcase do
|
@@ -47,45 +61,76 @@ module NSWTopo
|
|
47
61
|
one? && klass === first
|
48
62
|
end
|
49
63
|
|
50
|
-
define_singleton_method type.downcase do |coordinates, projection: DEFAULT_PROJECTION, name: nil, properties:
|
51
|
-
Collection.new(projection: projection, name: name) << klass
|
64
|
+
define_singleton_method type.downcase do |coordinates, projection: DEFAULT_PROJECTION, name: nil, properties: nil|
|
65
|
+
Collection.new(projection: projection, name: name) << klass[coordinates, properties]
|
52
66
|
end
|
53
67
|
end
|
54
68
|
|
55
|
-
[[Point, MultiPoint ],
|
56
|
-
|
57
|
-
|
69
|
+
[ [Point, MultiPoint ],
|
70
|
+
[LineString, MultiLineString],
|
71
|
+
[Polygon, MultiPolygon ]
|
72
|
+
].each do |single_class, multi_class|
|
58
73
|
single_class.class_eval do
|
59
|
-
|
60
|
-
|
61
|
-
|
74
|
+
include Enumerable
|
75
|
+
delegate %i[each] => :@coordinates
|
76
|
+
delegate %i[clip dissolve_points +] => :multi
|
62
77
|
|
63
|
-
|
64
|
-
|
65
|
-
end
|
78
|
+
def explode = [self]
|
79
|
+
end
|
66
80
|
|
67
|
-
|
68
|
-
|
81
|
+
single_class.define_method :multi do
|
82
|
+
multi_class.new [@coordinates], @properties
|
69
83
|
end
|
70
84
|
|
71
85
|
multi_class.class_eval do
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
86
|
+
include Enumerable
|
87
|
+
|
88
|
+
alias explode entries
|
89
|
+
alias multi itself
|
90
|
+
|
91
|
+
def bounds
|
92
|
+
map(&:bounds).transpose.map(&:flatten).map(&:minmax)
|
76
93
|
end
|
77
94
|
|
78
|
-
def
|
79
|
-
|
95
|
+
def empty_points = MultiPoint.new([], @properties)
|
96
|
+
def empty_linestrings = MultiLineString.new([], @properties)
|
97
|
+
def empty_polygons = MultiPolygon.new([], @properties)
|
98
|
+
end
|
99
|
+
|
100
|
+
multi_class.define_singleton_method :[] do |coordinates, properties = nil, &block|
|
101
|
+
multi_class.new(coordinates, properties) do
|
102
|
+
@coordinates.each do |coordinates|
|
103
|
+
single_class[coordinates]
|
104
|
+
end
|
105
|
+
block&.call self
|
80
106
|
end
|
107
|
+
end
|
81
108
|
|
82
|
-
|
83
|
-
|
109
|
+
multi_class.define_method :each do |&block|
|
110
|
+
enum = Enumerator.new do |yielder|
|
111
|
+
@coordinates.each do |coordinates|
|
112
|
+
yielder << single_class.new(coordinates, @properties)
|
113
|
+
end
|
84
114
|
end
|
115
|
+
block ? enum.each(&block) : enum
|
116
|
+
end
|
85
117
|
|
86
|
-
|
118
|
+
multi_class.define_method :+ do |other|
|
119
|
+
case other
|
120
|
+
when single_class
|
121
|
+
multi_class.new @coordinates + [other.coordinates], @properties
|
122
|
+
when multi_class
|
123
|
+
multi_class.new @coordinates + other.coordinates, @properties
|
124
|
+
else
|
125
|
+
raise "heterogenous geometries not implemented"
|
126
|
+
end
|
127
|
+
end
|
87
128
|
|
88
|
-
|
129
|
+
single_type, _ = CLASSES.rassoc(single_class)
|
130
|
+
%i[select reject].each do |verb|
|
131
|
+
multi_class.define_method "#{verb}_#{single_type}s".downcase do |&block|
|
132
|
+
send(verb, &block).inject(multi_class.new([], @properties), &:+)
|
133
|
+
end
|
89
134
|
end
|
90
135
|
end
|
91
136
|
end
|
@@ -94,5 +139,6 @@ end
|
|
94
139
|
require_relative 'geojson/point'
|
95
140
|
require_relative 'geojson/line_string'
|
96
141
|
require_relative 'geojson/polygon'
|
142
|
+
require_relative 'geojson/multi_point'
|
97
143
|
require_relative 'geojson/multi_line_string'
|
98
144
|
require_relative 'geojson/multi_polygon'
|
@@ -10,7 +10,7 @@ module NSWTopo
|
|
10
10
|
alias to_str wkt2
|
11
11
|
|
12
12
|
def ==(other)
|
13
|
-
wkt2 == other.wkt2
|
13
|
+
super || wkt2 == other.wkt2
|
14
14
|
end
|
15
15
|
|
16
16
|
extend Forwardable
|
@@ -29,6 +29,10 @@ module NSWTopo
|
|
29
29
|
new("EPSG:4326")
|
30
30
|
end
|
31
31
|
|
32
|
+
def self.epsg(epsg)
|
33
|
+
new("EPSG:#{epsg}")
|
34
|
+
end
|
35
|
+
|
32
36
|
def self.from(**params)
|
33
37
|
params.map do |key, value|
|
34
38
|
"+#{key}=#{value}"
|
@@ -0,0 +1,39 @@
|
|
1
|
+
class ThreadPool
|
2
|
+
CORES = Etc.nprocessors rescue 1
|
3
|
+
|
4
|
+
def initialize(size = CORES)
|
5
|
+
@args, @size = [], size
|
6
|
+
end
|
7
|
+
|
8
|
+
def <<(args)
|
9
|
+
tap { @args << args }
|
10
|
+
end
|
11
|
+
|
12
|
+
def threads(queue, &block)
|
13
|
+
@size.times.map do
|
14
|
+
Thread.new do
|
15
|
+
while args = queue.pop
|
16
|
+
block.call(*args)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def each(&block)
|
23
|
+
queue = Queue.new
|
24
|
+
threads(queue, &block).tap do
|
25
|
+
@args.inject(queue, &:<<).close
|
26
|
+
end.each(&:join)
|
27
|
+
@args
|
28
|
+
end
|
29
|
+
|
30
|
+
def in_groups(&block)
|
31
|
+
queue = Queue.new
|
32
|
+
threads(queue, &block).tap do
|
33
|
+
@args.group_by.with_index do |args, index|
|
34
|
+
index % @size
|
35
|
+
end.values.inject(queue, &:<<).close
|
36
|
+
end.each(&:join)
|
37
|
+
@args
|
38
|
+
end
|
39
|
+
end
|
data/lib/nswtopo/helpers.rb
CHANGED
@@ -1,6 +1,45 @@
|
|
1
|
-
require_relative 'helpers/
|
2
|
-
require_relative 'helpers/array'
|
3
|
-
require_relative 'helpers/dir'
|
4
|
-
require_relative 'helpers/concurrently'
|
5
|
-
require_relative 'helpers/tar_writer'
|
1
|
+
require_relative 'helpers/thread_pool'
|
6
2
|
require_relative 'helpers/colour'
|
3
|
+
|
4
|
+
module Helpers
|
5
|
+
refine Dir.singleton_class do
|
6
|
+
def mktmppath
|
7
|
+
mktmpdir("nswtopo_") do |path|
|
8
|
+
yield Pathname.new(path)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
refine Hash do
|
14
|
+
def deep_merge(other)
|
15
|
+
merge(other) do |key, old_value, new_value|
|
16
|
+
Hash === old_value && Hash === new_value ? old_value.deep_merge(new_value) : new_value
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
refine Array do
|
22
|
+
# partially partition element range in-place, according to block, returning the partitioned ranges
|
23
|
+
def partition!(range, &block)
|
24
|
+
return range.begin...range.begin, range if range.one?
|
25
|
+
last, pivot = range.end - 1, Kernel.rand(range)
|
26
|
+
self[pivot], self[last] = self[last], self[pivot]
|
27
|
+
pivot_value = block.call(at last)
|
28
|
+
index = range.inject(range.begin) do |store, index|
|
29
|
+
next store unless index == last || block.call(at index) < pivot_value
|
30
|
+
self[index], self[store] = self[store], self[index]
|
31
|
+
store + 1
|
32
|
+
end
|
33
|
+
return range.begin...index, index...range.end
|
34
|
+
end
|
35
|
+
|
36
|
+
def median_partition!(range = 0...length, &block)
|
37
|
+
median, target = (range.begin + range.end) / 2, range
|
38
|
+
while target.begin != median
|
39
|
+
lower, upper = partition!(target, &block)
|
40
|
+
target = lower === median ? lower : upper
|
41
|
+
end
|
42
|
+
return range.begin...median, median...range.end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -27,16 +27,16 @@ module NSWTopo
|
|
27
27
|
tile_level, tile_resolution = lod.values_at "level", "resolution"
|
28
28
|
|
29
29
|
target_bbox.coordinates.first.map do |corner|
|
30
|
-
corner
|
30
|
+
corner - origin
|
31
31
|
end.transpose.map(&:minmax).zip(tile_sizes).map do |bound, tile_size|
|
32
32
|
bound / tile_resolution / tile_size
|
33
33
|
end.map do |min, max|
|
34
34
|
(min.floor..max.ceil).each_cons(2).to_a
|
35
35
|
end.inject(&:product).inject(GeoJSON::Collection.new(projection: service.projection)) do |tiles, (cols, rows)|
|
36
36
|
[cols, rows].zip(tile_sizes).map do |indices, tile_size|
|
37
|
-
indices.
|
37
|
+
indices.map { |index| index * tile_size * tile_resolution }
|
38
38
|
end.transpose.map do |corner|
|
39
|
-
corner
|
39
|
+
corner + origin
|
40
40
|
end.transpose.then do |bounds|
|
41
41
|
ring = bounds.inject(&:product).values_at(0,2,3,1,0)
|
42
42
|
ullr = bounds.inject(&:product).values_at(1,2).flatten
|
@@ -1,7 +1,9 @@
|
|
1
1
|
module NSWTopo
|
2
2
|
module Contour
|
3
|
-
|
4
|
-
|
3
|
+
using Helpers
|
4
|
+
include VectorRender, DEM, Log
|
5
|
+
|
6
|
+
CREATE = %w[interval index auxiliary smooth simplify thin density min-length no-depression knolls fill epsg]
|
5
7
|
DEFAULTS = YAML.load <<~YAML
|
6
8
|
interval: 5
|
7
9
|
smooth: 0.2
|
@@ -36,7 +38,7 @@ module NSWTopo
|
|
36
38
|
YAML
|
37
39
|
|
38
40
|
def margin
|
39
|
-
{ mm: [3 * @smooth, 1].
|
41
|
+
{ mm: [3 * @smooth, 1].max }
|
40
42
|
end
|
41
43
|
|
42
44
|
def check_geos!
|
@@ -54,7 +56,7 @@ module NSWTopo
|
|
54
56
|
@index ||= 10 * @interval
|
55
57
|
@params = {
|
56
58
|
"Index" => { "stroke-width" => 2 * @params["stroke-width"] },
|
57
|
-
"labels" => { "fill" => @fill ||
|
59
|
+
"labels" => { "fill" => @fill || "black" }
|
58
60
|
}.deep_merge(@params)
|
59
61
|
|
60
62
|
check_geos! if @thin
|
@@ -75,14 +77,16 @@ module NSWTopo
|
|
75
77
|
|
76
78
|
log_update "%s: generating contour lines" % @name
|
77
79
|
json = OS.gdal_contour "-q", "-a", "elevation", "-i", @interval, "-f", "GeoJSON", "-lco", "RFC7946=NO", blur_path, "/vsistdout/"
|
78
|
-
contours = GeoJSON::Collection.load
|
80
|
+
contours = GeoJSON::Collection.load(json, projection: @map.projection).map! do |feature|
|
81
|
+
id, elevation = feature.values_at "ID", "elevation"
|
82
|
+
properties = { "id" => id, "elevation" => elevation, "modulo" => elevation % @index, "depression" => feature.closed? && feature.anticlockwise? ? 1 : 0}
|
83
|
+
feature.with_properties(properties)
|
84
|
+
end
|
85
|
+
raise "no elevation data found in map area" unless contours.any?
|
79
86
|
|
80
87
|
if @no_depression.nil?
|
81
88
|
candidates = contours.select do |feature|
|
82
|
-
feature
|
83
|
-
feature.coordinates.anticlockwise?
|
84
|
-
end.each do |feature|
|
85
|
-
feature["depression"] = 1
|
89
|
+
feature["depression"] == 1
|
86
90
|
end
|
87
91
|
index = RTree.load(candidates, &:bounds)
|
88
92
|
|
@@ -90,29 +94,21 @@ module NSWTopo
|
|
90
94
|
next unless feature["depression"] == 1
|
91
95
|
index.search(feature.bounds).none? do |other|
|
92
96
|
next if other == feature
|
93
|
-
feature.
|
94
|
-
other.coordinates.first.within?(feature.coordinates)
|
97
|
+
feature.to_polygon.contains?(other.first) || other.to_polygon.contains?(feature.first)
|
95
98
|
end
|
96
99
|
end
|
97
100
|
end
|
98
101
|
|
99
102
|
contours.reject! do |feature|
|
100
|
-
feature.
|
103
|
+
feature.closed? &&
|
101
104
|
feature.bounds.all? { |min, max| max - min < @knolls }
|
102
|
-
end
|
103
|
-
|
104
|
-
contours.each do |feature|
|
105
|
-
id, elevation, depression = feature.values_at "ID", "elevation", "depression"
|
106
|
-
feature.properties.replace("id" => id, "elevation" => elevation, "modulo" => elevation % @index, "depression" => depression || 0)
|
107
|
-
end
|
108
|
-
|
109
|
-
contours.reject! do |feature|
|
105
|
+
end.reject! do |feature|
|
110
106
|
feature["elevation"].zero?
|
111
107
|
end
|
112
108
|
|
113
109
|
contours.each_slice(100).inject(nil) do |update, features|
|
114
110
|
OS.ogr2ogr "-a_srs", @map.projection, "-nln", "contour", *update, "-simplify", @simplify, *db_flags, db_path, "GeoJSON:/vsistdin/" do |stdin|
|
115
|
-
stdin.write
|
111
|
+
stdin.write contours.with_features(features).to_json
|
116
112
|
end
|
117
113
|
%w[-update -append]
|
118
114
|
end
|
@@ -208,7 +204,7 @@ module NSWTopo
|
|
208
204
|
end
|
209
205
|
|
210
206
|
json = OS.ogr2ogr "-f", "GeoJSON", "-lco", "RFC7946=NO", "/vsistdout/", db_path, "contour"
|
211
|
-
GeoJSON::Collection.load(json, projection: @map.projection).
|
207
|
+
GeoJSON::Collection.load(json, projection: @map.projection).map! do |feature|
|
212
208
|
elevation, modulo, depression = feature.values_at "elevation", "modulo", "depression"
|
213
209
|
category = case
|
214
210
|
when @auxiliary && elevation % (2 * @interval) != 0 then %w[Auxiliary]
|
@@ -216,10 +212,12 @@ module NSWTopo
|
|
216
212
|
else %w[Standard]
|
217
213
|
end
|
218
214
|
category << "Depression" if depression == 1
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
215
|
+
|
216
|
+
properties = Hash[]
|
217
|
+
properties["elevation"] = elevation
|
218
|
+
properties["category"] = category
|
219
|
+
properties["label"] = elevation.to_i.to_s if modulo.zero?
|
220
|
+
feature.with_properties(properties)
|
223
221
|
end
|
224
222
|
end
|
225
223
|
end
|
@@ -1,6 +1,8 @@
|
|
1
1
|
module NSWTopo
|
2
2
|
module Control
|
3
|
-
|
3
|
+
using Helpers
|
4
|
+
include VectorRender
|
5
|
+
|
4
6
|
CREATE = %w[diameter spot font-size colour]
|
5
7
|
DEFAULTS = YAML.load <<~YAML
|
6
8
|
diameter: 7.0
|
@@ -73,7 +75,7 @@ module NSWTopo
|
|
73
75
|
end
|
74
76
|
|
75
77
|
def to_s
|
76
|
-
counts = %w[control waterdrop hashhouse].
|
78
|
+
counts = %w[control waterdrop hashhouse].filter_map do |category|
|
77
79
|
waypoints = features.select do |feature|
|
78
80
|
feature["category"].any? category
|
79
81
|
end
|
@@ -82,7 +84,7 @@ module NSWTopo
|
|
82
84
|
next count unless "control" == category
|
83
85
|
total = features.sum { |feature| feature["label"].to_i.floor(-1) }
|
84
86
|
count << " (%i points)" % total
|
85
|
-
end
|
87
|
+
end
|
86
88
|
[@name, counts.join(", ")].join(": ")
|
87
89
|
end
|
88
90
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module NSWTopo
|
2
2
|
module Declination
|
3
|
-
include
|
3
|
+
include VectorRender
|
4
4
|
CREATE = %w[angle spacing arrows offset]
|
5
5
|
DEFAULTS = YAML.load <<~YAML
|
6
6
|
spacing: 40.0
|
@@ -21,31 +21,35 @@ module NSWTopo
|
|
21
21
|
row_spacing = @arrows * 0.5
|
22
22
|
col_offset = @offset % @spacing
|
23
23
|
|
24
|
-
radius = 0.5 * @map.neatline.bounds.transpose.
|
24
|
+
radius = 0.5 * @map.neatline.bounds.transpose.then do |bl, tr|
|
25
|
+
Vector[*tr] - Vector[*bl]
|
26
|
+
end.norm
|
27
|
+
|
25
28
|
j_max = (radius / col_spacing).ceil
|
26
29
|
i_max = (radius / row_spacing).ceil
|
27
30
|
|
28
|
-
|
29
|
-
(-j_max..j_max).each do |j|
|
31
|
+
(-j_max..j_max).each.with_object(GeoJSON::Collection.new(projection: @map.neatline.projection)) do |j, collection|
|
30
32
|
x = j * col_spacing + col_offset
|
31
|
-
coordinates = [
|
32
|
-
|
33
|
+
coordinates = [radius, -radius].map do |y|
|
34
|
+
Vector[x, y].rotate_by_degrees(declination - @map.rotation) + Vector[*@map.dimensions] / 2
|
33
35
|
end
|
34
36
|
collection.add_linestring coordinates
|
35
37
|
(-i_max..i_max).reject(&j.even? ? :even? : :odd?).map do |i|
|
36
|
-
[x, i * row_spacing].rotate_by_degrees(declination - @map.rotation)
|
38
|
+
Vector[x, i * row_spacing].rotate_by_degrees(declination - @map.rotation) + Vector[*@map.dimensions] / 2
|
37
39
|
end.each do |coordinates|
|
38
40
|
collection.add_point coordinates, "rotation" => declination
|
39
41
|
end
|
40
42
|
end
|
41
|
-
collection
|
42
43
|
end
|
43
44
|
|
44
45
|
def to_s
|
45
46
|
lines = features.grep(GeoJSON::LineString)
|
46
47
|
return @name if lines.none?
|
47
|
-
|
48
|
-
|
48
|
+
angle = lines.map(&:coordinates).map do |p0, p1|
|
49
|
+
p1 - p0
|
50
|
+
end.max_by(&:norm).then do |delta|
|
51
|
+
90 + 180 * Math::atan2(delta.y, delta.x) / Math::PI + @map.rotation
|
52
|
+
end
|
49
53
|
"%s: %i line%s at %.1f°%s" % [@name, lines.length, (?s unless lines.one?), angle.abs, angle > 0 ? ?E : angle < 0 ? ?W : nil]
|
50
54
|
end
|
51
55
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module NSWTopo
|
2
2
|
module Feature
|
3
|
-
include
|
3
|
+
include VectorRender, Log
|
4
4
|
CREATE = %w[features]
|
5
5
|
|
6
6
|
def get_features
|
@@ -45,7 +45,7 @@ module NSWTopo
|
|
45
45
|
when String then options[:rotation]
|
46
46
|
end
|
47
47
|
|
48
|
-
collection.
|
48
|
+
collection.map! do |feature|
|
49
49
|
categories = [*options[:category]].flat_map do |category|
|
50
50
|
Hash === category ? [*category] : [category]
|
51
51
|
end.map do |attribute, substitutions|
|
@@ -85,7 +85,7 @@ module NSWTopo
|
|
85
85
|
end
|
86
86
|
|
87
87
|
categories = categories.map(&:to_s).reject(&:empty?).map(&method(:categorise))
|
88
|
-
properties =
|
88
|
+
properties = Hash[]
|
89
89
|
properties["category"] = categories if categories.any?
|
90
90
|
properties["label"] = labels if labels.any?
|
91
91
|
properties["dual"] = dual if dual
|
@@ -93,9 +93,9 @@ module NSWTopo
|
|
93
93
|
properties["draw"] = false if @name =~ /[-_]labels$/ && !options.key?(:draw)
|
94
94
|
properties["rotation"] = rotation if rotation
|
95
95
|
|
96
|
-
feature.properties
|
96
|
+
feature.with_properties(properties)
|
97
97
|
end
|
98
|
-
end.map(&:first).inject(&:merge).
|
98
|
+
end.map(&:first).inject(&:merge).with_name(@name)
|
99
99
|
end
|
100
100
|
end
|
101
101
|
end
|
data/lib/nswtopo/layer/grid.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module NSWTopo
|
2
2
|
module Grid
|
3
|
-
include
|
3
|
+
include VectorRender
|
4
4
|
CREATE = %w[interval border]
|
5
5
|
INSET = 1.5
|
6
6
|
DEFAULTS = YAML.load <<~YAML
|
@@ -19,6 +19,7 @@ module NSWTopo
|
|
19
19
|
YAML
|
20
20
|
|
21
21
|
def get_features
|
22
|
+
@params["edge"]["stroke-width"] ||= 2 * @params["stroke-width"]
|
22
23
|
Projection.utm_zones(@map.neatline).flat_map do |zone|
|
23
24
|
utm, utm_geometry = Projection.utm(zone), Projection.utm_geometry(zone)
|
24
25
|
map_geometry = @map.neatline(**MARGIN).reproject_to_wgs84
|
@@ -40,8 +41,8 @@ module NSWTopo
|
|
40
41
|
label << coord % 1000 unless @interval % 1000 == 0
|
41
42
|
collection.add_linestring line, "label" => label, "ends" => [0, 1], "category" => index.zero? ? "easting" : "northing"
|
42
43
|
end.reproject_to_wgs84.clip(utm_geometry).clip(map_geometry).explode.each do |linestring|
|
43
|
-
linestring["ends"].delete 0 if linestring.coordinates
|
44
|
-
linestring["ends"].delete 1 if linestring.coordinates
|
44
|
+
linestring["ends"].delete 0 if linestring.coordinates.first.x % 6 < 0.00001
|
45
|
+
linestring["ends"].delete 1 if linestring.coordinates.last.x % 6 < 0.00001
|
45
46
|
end
|
46
47
|
end
|
47
48
|
|
@@ -51,9 +52,9 @@ module NSWTopo
|
|
51
52
|
[eastings, northings, boundary]
|
52
53
|
end.tap do |collections|
|
53
54
|
next unless @border
|
54
|
-
|
55
|
-
|
56
|
-
|
55
|
+
@map.neatline.reproject_to_wgs84.map! do |border|
|
56
|
+
border.with_properties("category" => "edge")
|
57
|
+
end.tap do |border|
|
57
58
|
collections << border
|
58
59
|
end
|
59
60
|
end.inject(&:merge)
|
@@ -85,33 +86,33 @@ module NSWTopo
|
|
85
86
|
font_size = label_params["font-size"]
|
86
87
|
offset = -0.85 * font_size
|
87
88
|
inset = INSET + font_size * 0.5 * Math::sin(@map.rotation.abs * Math::PI / 180)
|
88
|
-
inset_geometry = @map.neatline(mm: -inset)
|
89
|
+
inset_geometry = @map.neatline(mm: -inset).map!(&:remove_holes)
|
89
90
|
|
90
|
-
|
91
|
+
eastings, northings = features.select do |linestring|
|
91
92
|
linestring["label"]
|
92
|
-
end
|
93
|
-
eastings = gridlines.select do |gridline|
|
93
|
+
end.partition do |gridline|
|
94
94
|
gridline["category"] == "easting"
|
95
95
|
end
|
96
96
|
|
97
97
|
flip_eastings = eastings.partition do |easting|
|
98
|
-
Math::atan2(*easting.coordinates.values_at(0, -1).inject(
|
98
|
+
Math::atan2(*easting.coordinates.values_at(0, -1).inject(&:-)) * 180.0 / Math::PI > @map.rotation
|
99
99
|
end.map(&:length).inject(&:>)
|
100
|
+
|
100
101
|
eastings.each do |easting|
|
101
|
-
easting.coordinates.reverse!
|
102
102
|
easting["ends"].map! { |index| 1 - index }
|
103
|
-
end if flip_eastings
|
103
|
+
end.map!(&:reverse) if flip_eastings
|
104
104
|
|
105
|
-
|
105
|
+
eastings.concat(northings).inject(GeoJSON::Collection.new(projection: @map.neatline.projection)) do |collection, gridline|
|
106
106
|
collection << gridline.offset(offset, splits: false)
|
107
107
|
end.clip(inset_geometry).explode.flat_map do |gridline|
|
108
108
|
label, ends = gridline.values_at "label", "ends"
|
109
109
|
%i[itself reverse].values_at(*ends).map do |order|
|
110
110
|
text_length, text_path = label_element(label, label_params)
|
111
|
-
|
112
|
-
fraction = text_length /
|
113
|
-
|
114
|
-
|
111
|
+
p0, p1 = gridline.coordinates.send(order).take(2)
|
112
|
+
fraction = text_length / (p1 - p0).norm
|
113
|
+
p01 = p1 * fraction + p0 * (1 - fraction)
|
114
|
+
coordinates = [p0, p01].send(order)
|
115
|
+
GeoJSON::LineString[coordinates, "label" => text_path]
|
115
116
|
end
|
116
117
|
end
|
117
118
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module NSWTopo
|
2
|
+
module Labels
|
3
|
+
class Barriers
|
4
|
+
def initialize
|
5
|
+
@barriers, @cache = [], Hash[]
|
6
|
+
end
|
7
|
+
|
8
|
+
extend Forwardable
|
9
|
+
delegate :<< => :@barriers
|
10
|
+
|
11
|
+
def to_proc
|
12
|
+
@index ||= RTree.load(@barriers.flat_map(&:explode), &:bounds)
|
13
|
+
@proc ||= lambda do |label_hull, buffer|
|
14
|
+
@cache[[buffer, label_hull.coordinates]] ||= @index.search(label_hull.bounds, buffer).with_object Set[] do |barrier_hull, barriers|
|
15
|
+
next if barriers === barrier_hull.source
|
16
|
+
next unless ConvexHulls.overlap?(barrier_hull, label_hull, buffer)
|
17
|
+
barriers << barrier_hull.source
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|