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,73 @@
|
|
1
|
+
module NSWTopo
|
2
|
+
module Vegetation
|
3
|
+
include Raster, GDALGlob
|
4
|
+
CREATE = %w[mapping contrast colour]
|
5
|
+
|
6
|
+
def get_raster(temp_dir)
|
7
|
+
txt_path = temp_dir / "source.txt"
|
8
|
+
vrt_path = temp_dir / "source.vrt"
|
9
|
+
|
10
|
+
min, max = minmax = @mapping&.values_at("min", "max")
|
11
|
+
low, high, factor = { "low" => 0, "high" => 100, "factor" => 0.0 }.merge(@contrast || {}).values_at "low", "high", "factor"
|
12
|
+
woody, nonwoody = { "woody" => "#A6F1A6", "non-woody" => "#FFFFFF" }.merge(@colour || {}).values_at("woody", "non-woody").map { |string| Colour.new string }
|
13
|
+
|
14
|
+
colour_table = (0..255).map do |index|
|
15
|
+
case
|
16
|
+
when minmax&.all?(Integer) && minmax.all?(0..255)
|
17
|
+
(100.0 * (index - min) / (max - min)).clamp(0.0, 100.0)
|
18
|
+
when @mapping&.keys&.all?(Integer)
|
19
|
+
@mapping.fetch(index, 0)
|
20
|
+
else raise "no vegetation colour mapping specified for #{name}"
|
21
|
+
end
|
22
|
+
end.map do |percent|
|
23
|
+
(Float(percent - low) / (high - low)).clamp(0.0, 1.0)
|
24
|
+
end.map do |x|
|
25
|
+
next x if factor.zero?
|
26
|
+
[x, 1.0].map do |x|
|
27
|
+
[x, 0.0].map do |x|
|
28
|
+
1 / (1 + Math::exp(factor * (0.5 - x)))
|
29
|
+
end.inject(&:-)
|
30
|
+
end.inject(&:/) # sigmoid between 0..1
|
31
|
+
end.map do |x|
|
32
|
+
nonwoody.mix(woody, x)
|
33
|
+
end
|
34
|
+
|
35
|
+
Dir.chdir(@source ? @source.parent : Pathname.pwd) do
|
36
|
+
gdal_rasters @path
|
37
|
+
end.tap do |rasters|
|
38
|
+
raise "no vegetation data file specified" if rasters.none?
|
39
|
+
end.group_by do |path, info|
|
40
|
+
Projection.new info.dig("coordinateSystem", "wkt")
|
41
|
+
end.map.with_index do |(projection, rasters), index|
|
42
|
+
indexed_tif_path = temp_dir / "indexed.#{index}.tif"
|
43
|
+
indexed_vrt_path = temp_dir / "indexed.#{index}.vrt"
|
44
|
+
coloured_tif_path = temp_dir / "coloured.#{index}.tif"
|
45
|
+
tif_path = temp_dir / "output.#{index}.tif"
|
46
|
+
|
47
|
+
txt_path.write rasters.map(&:first).join(?\n)
|
48
|
+
OS.gdalbuildvrt "-overwrite", "-input_file_list", txt_path, vrt_path
|
49
|
+
OS.gdal_translate "-projwin", *@map.projwin(projection), "-r", "near", "-co", "TFW=YES", vrt_path, indexed_tif_path
|
50
|
+
OS.gdal_translate "-of", "VRT", indexed_tif_path, indexed_vrt_path
|
51
|
+
|
52
|
+
xml = REXML::Document.new indexed_vrt_path.read
|
53
|
+
raise "can't process vegetation data for #{@name}" unless xml.elements.each("/VRTDataset/VRTRasterBand/ColorTable", &:itself).one?
|
54
|
+
raise "can't process vegetation data for #{@name}" unless xml.elements.each("/VRTDataset/VRTRasterBand/ColorTable/Entry", &:itself).count == 256
|
55
|
+
xml.elements.collect("/VRTDataset/VRTRasterBand/ColorTable/Entry", &:itself).zip(colour_table) do |entry, colour|
|
56
|
+
entry.attributes["c1"], entry.attributes["c2"], entry.attributes["c3"], entry.attributes["c4"] = *colour.triplet, 255
|
57
|
+
end
|
58
|
+
xml.elements.each("/VRTDataset/VRTRasterBand/NoDataValue", &:remove)
|
59
|
+
indexed_vrt_path.write xml
|
60
|
+
OS.gdal_translate "-expand", "rgb", indexed_vrt_path, coloured_tif_path
|
61
|
+
|
62
|
+
OS.gdalwarp "-s_srs", projection, "-t_srs", @map.projection, "-r", "bilinear", coloured_tif_path, tif_path
|
63
|
+
next tif_path, Numeric === @resolution ? @resolution : @map.get_raster_resolution(tif_path)
|
64
|
+
end.transpose.tap do |tif_paths, resolutions|
|
65
|
+
@resolution = resolutions.min
|
66
|
+
txt_path.write tif_paths.join(?\n)
|
67
|
+
OS.gdalbuildvrt "-overwrite", "-input_file_list", txt_path, vrt_path
|
68
|
+
end
|
69
|
+
|
70
|
+
return @resolution, vrt_path
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require_relative 'layer/raster'
|
2
|
+
require_relative 'layer/vector'
|
3
|
+
require_relative 'layer/vegetation'
|
4
|
+
require_relative 'layer/import'
|
5
|
+
require_relative 'layer/arcgis_raster'
|
6
|
+
require_relative 'layer/feature'
|
7
|
+
require_relative 'layer/contour'
|
8
|
+
require_relative 'layer/spot'
|
9
|
+
require_relative 'layer/overlay'
|
10
|
+
require_relative 'layer/relief'
|
11
|
+
require_relative 'layer/grid'
|
12
|
+
require_relative 'layer/declination'
|
13
|
+
require_relative 'layer/control'
|
14
|
+
require_relative 'layer/labels'
|
15
|
+
|
16
|
+
module NSWTopo
|
17
|
+
class Layer
|
18
|
+
TYPES = Set[Vegetation, Import, ArcGISRaster, Feature, Contour, Spot, Overlay, Relief, Grid, Declination, Control, Labels]
|
19
|
+
|
20
|
+
def initialize(name, map, params)
|
21
|
+
@type = begin
|
22
|
+
NSWTopo.const_get params["type"]
|
23
|
+
rescue NameError, TypeError
|
24
|
+
end
|
25
|
+
|
26
|
+
raise "unrecognised layer type: %s" % params["type"].inspect unless TYPES === @type
|
27
|
+
extend @type
|
28
|
+
|
29
|
+
@params = @type.const_defined?(:DEFAULTS) ? @type.const_get(:DEFAULTS).transform_keys(&:to_s).merge(params) : params
|
30
|
+
@name, @map, @source, @path, @resolution = Layer.sanitise(name), map, @params.delete("source"), @params.delete("path"), @params.delete("resolution")
|
31
|
+
|
32
|
+
@type.const_get(:CREATE).map(&:to_s).each do |attr|
|
33
|
+
instance_variable_set ?@ + attr.tr_s(?-, ?_), @params.delete(attr)
|
34
|
+
end if @type.const_defined?(:CREATE)
|
35
|
+
end
|
36
|
+
|
37
|
+
attr_reader :name, :params
|
38
|
+
alias to_s name
|
39
|
+
|
40
|
+
def level
|
41
|
+
case
|
42
|
+
when Vegetation == @type then 0
|
43
|
+
when Import == @type then 1
|
44
|
+
when ArcGISRaster == @type then 1
|
45
|
+
when Feature == @type then 2
|
46
|
+
when Contour == @type then 2
|
47
|
+
when Spot == @type then 2
|
48
|
+
when Overlay == @type then 3
|
49
|
+
when Relief == @type then 4
|
50
|
+
when Grid == @type then 5
|
51
|
+
when Declination == @type then 6
|
52
|
+
when Control == @type then 7
|
53
|
+
when Labels == @type then 99
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def <=>(other)
|
58
|
+
[self, other].map(&:level).inject(&:<=>)
|
59
|
+
end
|
60
|
+
|
61
|
+
def ==(other)
|
62
|
+
Layer === other && self.name == other.name
|
63
|
+
end
|
64
|
+
|
65
|
+
def uptodate?
|
66
|
+
mtimes = [@source&.mtime, @map.mtime(filename)]
|
67
|
+
mtimes.all? && mtimes.inject(&:<)
|
68
|
+
end
|
69
|
+
|
70
|
+
def pair
|
71
|
+
return name, params
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.sanitise(name)
|
75
|
+
name&.tr_s '^_a-zA-Z0-9*\-', ?.
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/lib/nswtopo/log.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
module NSWTopo
|
2
|
+
module Log
|
3
|
+
SUCCESS = $stdout.tty? ? "\r\e[2K\e[32mnswtopo:\e[0m %s" : "nswtopo: %s"
|
4
|
+
FAILURE = $stderr.tty? ? "\r\e[2K\e[31mnswtopo:\e[0m %s" : "nswtopo: %s"
|
5
|
+
NEUTRAL = $stdout.tty? ? "\r\e[2Knswtopo: %s" : "nswtopo: %s"
|
6
|
+
UPDATE = "\r\e[2K%s"
|
7
|
+
|
8
|
+
def log_success(message)
|
9
|
+
puts SUCCESS % message
|
10
|
+
end
|
11
|
+
|
12
|
+
def log_neutral(message)
|
13
|
+
puts NEUTRAL % message
|
14
|
+
end
|
15
|
+
|
16
|
+
def log_update(message)
|
17
|
+
print UPDATE % message if $stdout.tty?
|
18
|
+
end
|
19
|
+
|
20
|
+
def log_warn(message)
|
21
|
+
warn FAILURE % message
|
22
|
+
end
|
23
|
+
|
24
|
+
def log_abort(message)
|
25
|
+
abort FAILURE % message
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/nswtopo/map.rb
ADDED
@@ -0,0 +1,296 @@
|
|
1
|
+
module NSWTopo
|
2
|
+
class Map
|
3
|
+
include Formats, Dither, Zip, Log, Safely
|
4
|
+
|
5
|
+
def initialize(archive, proj4:, scale:, centre:, extents:, rotation:, layers: {})
|
6
|
+
@archive, @scale, @centre, @extents, @rotation, @layers = archive, scale, centre, extents, rotation, layers
|
7
|
+
@projection = Projection.new proj4
|
8
|
+
ox, oy = bounding_box.coordinates[0][3]
|
9
|
+
@affine = [[1, 0], [0, -1], [-ox, oy]].map do |vector|
|
10
|
+
vector.rotate_by_degrees(-@rotation).times(1000.0 / @scale)
|
11
|
+
end.transpose
|
12
|
+
end
|
13
|
+
attr_reader :projection, :scale, :centre, :extents, :rotation
|
14
|
+
|
15
|
+
extend Forwardable
|
16
|
+
delegate %i[write mtime read] => :@archive
|
17
|
+
|
18
|
+
def self.init(archive, scale: 25000, rotation: 0.0, bounds: nil, coords: nil, dimensions: nil, margins: nil)
|
19
|
+
wgs84_points = case
|
20
|
+
when coords && bounds
|
21
|
+
raise "can't specify both bounds file and map coordinates"
|
22
|
+
when coords
|
23
|
+
coords
|
24
|
+
when bounds
|
25
|
+
gps = GPS.load bounds
|
26
|
+
margins ||= [15, 15] unless dimensions || gps.polygons.any?
|
27
|
+
case
|
28
|
+
when gps.polygons.any?
|
29
|
+
gps.polygons.map(&:coordinates).flatten(1).inject(&:+)
|
30
|
+
when gps.linestrings.any?
|
31
|
+
gps.linestrings.map(&:coordinates).inject(&:+)
|
32
|
+
when gps.points.any?
|
33
|
+
gps.points.map(&:coordinates)
|
34
|
+
else
|
35
|
+
raise "no features found in %s" % bounds
|
36
|
+
end
|
37
|
+
else
|
38
|
+
raise "no bounds file or map coordinates specified"
|
39
|
+
end
|
40
|
+
|
41
|
+
wgs84_centre = wgs84_points.transpose.map(&:minmax).map(&:sum).times(0.5)
|
42
|
+
projection = Projection.azimuthal_equidistant *wgs84_centre
|
43
|
+
|
44
|
+
case rotation
|
45
|
+
when "auto"
|
46
|
+
raise "can't specify both map dimensions and auto-rotation" if dimensions
|
47
|
+
points = GeoJSON.multipoint(wgs84_points).reproject_to(projection).coordinates
|
48
|
+
centre, extents, rotation = points.minimum_bounding_box(*margins)
|
49
|
+
rotation *= -180.0 / Math::PI
|
50
|
+
when "magnetic"
|
51
|
+
rotation = declination(*wgs84_centre)
|
52
|
+
else
|
53
|
+
raise "map rotation must be between ±45°" unless rotation.abs <= 45
|
54
|
+
end
|
55
|
+
|
56
|
+
case
|
57
|
+
when centre
|
58
|
+
when dimensions
|
59
|
+
raise "can't specify both margins and map dimensions" if margins
|
60
|
+
extents = dimensions.map do |dimension|
|
61
|
+
dimension * 0.001 * scale
|
62
|
+
end
|
63
|
+
centre = GeoJSON.point(wgs84_centre).reproject_to(projection).coordinates
|
64
|
+
else
|
65
|
+
points = GeoJSON.multipoint(wgs84_points).reproject_to(projection).coordinates
|
66
|
+
centre, extents = points.map do |point|
|
67
|
+
point.rotate_by_degrees rotation
|
68
|
+
end.transpose.map(&:minmax).map do |min, max|
|
69
|
+
[0.5 * (max + min), max - min]
|
70
|
+
end.transpose
|
71
|
+
centre.rotate_by_degrees! -rotation
|
72
|
+
end
|
73
|
+
|
74
|
+
wgs84_centre = GeoJSON.point(centre, projection: projection).reproject_to_wgs84.coordinates
|
75
|
+
projection = Projection.transverse_mercator *wgs84_centre
|
76
|
+
|
77
|
+
extents = extents.zip(margins).map do |extent, margin|
|
78
|
+
extent + 2 * margin * 0.001 * scale
|
79
|
+
end if margins
|
80
|
+
|
81
|
+
case
|
82
|
+
when extents.all?(&:positive?)
|
83
|
+
when coords
|
84
|
+
raise "not enough information to calculate map size – add more coordinates, or specify map dimensions or margins"
|
85
|
+
when bounds
|
86
|
+
raise "not enough information to calculate map size – check bounds file, or specify map dimensions or margins"
|
87
|
+
end
|
88
|
+
|
89
|
+
new(archive, proj4: projection.proj4, scale: scale, centre: [0, 0], extents: extents, rotation: rotation).save
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.load(archive)
|
93
|
+
new archive, **YAML.load(archive.read "map.yml")
|
94
|
+
end
|
95
|
+
|
96
|
+
def save
|
97
|
+
tap { @archive.write "map.yml", YAML.dump(proj4: @projection.proj4, scale: @scale, centre: @centre, extents: @extents, rotation: @rotation, layers: @layers) }
|
98
|
+
end
|
99
|
+
|
100
|
+
def layers
|
101
|
+
@layers.map do |name, params|
|
102
|
+
Layer.new(name, self, params)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def raster_dimensions_at(ppi: nil, resolution: nil)
|
107
|
+
resolution ||= 0.0254 * @scale / ppi
|
108
|
+
ppi ||= 0.0254 * @scale / resolution
|
109
|
+
return (@extents / resolution).map(&:ceil), ppi, resolution
|
110
|
+
end
|
111
|
+
|
112
|
+
def wgs84_centre
|
113
|
+
GeoJSON.point(@centre, projection: @projection).reproject_to_wgs84.coordinates
|
114
|
+
end
|
115
|
+
|
116
|
+
def bounding_box(mm: nil, metres: nil)
|
117
|
+
margin = mm ? mm * 0.001 * @scale : metres ? metres : 0
|
118
|
+
ring = @extents.map do |extent|
|
119
|
+
[-0.5 * extent - margin, 0.5 * extent + margin]
|
120
|
+
end.inject(&:product).map do |offset|
|
121
|
+
@centre.plus offset.rotate_by_degrees(-@rotation)
|
122
|
+
end.values_at(0,2,3,1,0)
|
123
|
+
GeoJSON.polygon [ring], projection: projection
|
124
|
+
end
|
125
|
+
|
126
|
+
def bounds(margin: {}, projection: nil)
|
127
|
+
bounding_box(margin).yield_self do |bbox|
|
128
|
+
projection ? bbox.reproject_to(projection) : bbox
|
129
|
+
end.coordinates.first.transpose.map(&:minmax)
|
130
|
+
end
|
131
|
+
|
132
|
+
def projwin(projection)
|
133
|
+
bounds(projection: projection).flatten.values_at(0,3,1,2)
|
134
|
+
end
|
135
|
+
|
136
|
+
def write_world_file(path, resolution: nil, ppi: nil)
|
137
|
+
resolution ||= 0.0254 * @scale / ppi
|
138
|
+
top_left = bounding_box.coordinates[0][3]
|
139
|
+
WorldFile.write top_left, resolution, -@rotation, path
|
140
|
+
end
|
141
|
+
|
142
|
+
def coords_to_mm(point)
|
143
|
+
@affine.map do |row|
|
144
|
+
row.dot [*point, 1.0]
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def get_raster_resolution(raster_path)
|
149
|
+
metre_diagonal = bounding_box.coordinates.first.values_at(0, 2)
|
150
|
+
pixel_diagonal = OS.gdaltransform "-i", "-t_srs", @projection, raster_path do |stdin|
|
151
|
+
metre_diagonal.each do |point|
|
152
|
+
stdin.puts point.join(?\s)
|
153
|
+
end
|
154
|
+
end.each_line.map do |line|
|
155
|
+
line.split(?\s).take(2).map(&:to_f)
|
156
|
+
end
|
157
|
+
metre_diagonal.distance / pixel_diagonal.distance
|
158
|
+
rescue OS::Error
|
159
|
+
raise "invalid raster"
|
160
|
+
end
|
161
|
+
|
162
|
+
def self.declination(longitude, latitude)
|
163
|
+
today = Date.today
|
164
|
+
query = { lat1: latitude.abs, lat1Hemisphere: latitude < 0 ? ?S : ?N, lon1: longitude.abs, lon1Hemisphere: longitude < 0 ? ?W : ?E, model: "WMM", startYear: today.year, startMonth: today.month, startDay: today.day, resultFormat: "xml" }
|
165
|
+
uri = URI::HTTPS.build host: "www.ngdc.noaa.gov", path: "/geomag-web/calculators/calculateDeclination", query: URI.encode_www_form(query)
|
166
|
+
xml = Net::HTTP.get uri
|
167
|
+
text = REXML::Document.new(xml).elements["//declination"]&.text
|
168
|
+
text ? text.to_f : raise
|
169
|
+
rescue RuntimeError, SystemCallError, EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError, SocketError
|
170
|
+
raise "couldn't get magnetic declination value"
|
171
|
+
end
|
172
|
+
|
173
|
+
def declination
|
174
|
+
Map.declination *wgs84_centre
|
175
|
+
end
|
176
|
+
|
177
|
+
def add(*layers, after: nil, before: nil, replace: nil, overwrite: false)
|
178
|
+
[%w[before after replace], [before, after, replace]].transpose.select(&:last).each do |option, name|
|
179
|
+
next if self.layers.any? { |other| other.name == name }
|
180
|
+
raise "no such layer: %s" % name
|
181
|
+
end.map(&:first).combination(2).each do |options|
|
182
|
+
raise OptionParser::AmbiguousOption, "can't specify --%s and --%s simultaneously" % options
|
183
|
+
end
|
184
|
+
|
185
|
+
layers.inject [self.layers, false, replace || after, []] do |(layers, changed, follow, errors), layer|
|
186
|
+
index = layers.index layer unless replace || after || before
|
187
|
+
if overwrite || !layer.uptodate?
|
188
|
+
layer.create
|
189
|
+
log_success "%s layer: %s" % [layer.empty? ? "empty" : "added", layer.name]
|
190
|
+
else
|
191
|
+
log_neutral "kept existing layer: %s" % layer.name
|
192
|
+
next layers, changed, layer.name, errors if index
|
193
|
+
end
|
194
|
+
layers.delete layer
|
195
|
+
case
|
196
|
+
when index
|
197
|
+
when follow
|
198
|
+
index = layers.index { |other| other.name == follow }
|
199
|
+
index += 1
|
200
|
+
when before
|
201
|
+
index = layers.index { |other| other.name == before }
|
202
|
+
else
|
203
|
+
index = layers.index { |other| (other <=> layer) > 0 } || -1
|
204
|
+
end
|
205
|
+
next layers.insert(index, layer), true, layer.name, errors
|
206
|
+
rescue ArcGISServer::Error, RuntimeError => error
|
207
|
+
log_warn ArcGISServer::Error === error ? "couldn't download layer: #{layer.name}" : error.message
|
208
|
+
next layers, changed, follow, errors << error
|
209
|
+
end.tap do |ordered_layers, changed, follow, errors|
|
210
|
+
if changed
|
211
|
+
@layers.replace Hash[ordered_layers.map(&:pair)]
|
212
|
+
replace ? delete(replace) : save
|
213
|
+
end
|
214
|
+
raise PartialFailureError, "failed to create %s" % [layers.one? ? "layer" : errors.one? ? "1 layer" : "#{errors.length} layers"] if errors.any?
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def delete(*names)
|
219
|
+
raise OptionParser::MissingArgument, "no layers specified" unless names.any?
|
220
|
+
names.inject Set[] do |matched, name|
|
221
|
+
matches = @layers.keys.grep(name)
|
222
|
+
raise "no such layer: #{name}" if String === name && matches.none?
|
223
|
+
matched.merge matches
|
224
|
+
end.tap do |names|
|
225
|
+
raise "no matching layers found" unless names.any?
|
226
|
+
end.each do |name|
|
227
|
+
params = @layers.delete name
|
228
|
+
@archive.delete Layer.new(name, self, params).filename
|
229
|
+
log_success "deleted layer: %s" % name
|
230
|
+
end
|
231
|
+
save
|
232
|
+
end
|
233
|
+
|
234
|
+
def info(empty: nil)
|
235
|
+
StringIO.new.tap do |io|
|
236
|
+
io.puts "%-11s 1:%i" % ["scale:", @scale]
|
237
|
+
io.puts "%-11s %imm × %imm" % ["dimensions:", *@extents.times(1000.0 / @scale)]
|
238
|
+
io.puts "%-11s %.1fkm × %.1fkm" % ["extent:", *@extents.times(0.001)]
|
239
|
+
io.puts "%-11s %.1fkm²" % ["area:", @extents.inject(&:*) * 0.000001]
|
240
|
+
io.puts "%-11s %.1f°" % ["rotation:", @rotation]
|
241
|
+
layers.reject(&empty ? :nil? : :empty?).inject("layers:") do |heading, layer|
|
242
|
+
io.puts "%-11s %s" % [heading, layer]
|
243
|
+
nil
|
244
|
+
end
|
245
|
+
end.string
|
246
|
+
end
|
247
|
+
alias to_s info
|
248
|
+
|
249
|
+
def render(*paths, worldfile: false, force: false, external: nil, **options)
|
250
|
+
@archive.delete "map.svg" if force
|
251
|
+
Dir.mktmppath do |temp_dir|
|
252
|
+
rasters = Hash.new do |rasters, opts|
|
253
|
+
png_path = temp_dir / "raster.#{rasters.size}.png"
|
254
|
+
pgw_path = temp_dir / "raster.#{rasters.size}.pgw"
|
255
|
+
rasterise png_path, external: external, **opts
|
256
|
+
write_world_file pgw_path, opts
|
257
|
+
rasters[opts] = png_path
|
258
|
+
end
|
259
|
+
dithers = Hash.new do |dithers, opts|
|
260
|
+
png_path = temp_dir / "dither.#{dithers.size}.png"
|
261
|
+
pgw_path = temp_dir / "dither.#{dithers.size}.pgw"
|
262
|
+
FileUtils.cp rasters[opts], png_path
|
263
|
+
dither png_path
|
264
|
+
write_world_file pgw_path, opts
|
265
|
+
dithers[opts] = png_path
|
266
|
+
end
|
267
|
+
|
268
|
+
outputs = paths.map.with_index do |path, index|
|
269
|
+
ext = path.extname.delete_prefix ?.
|
270
|
+
name = path.basename(path.extname)
|
271
|
+
out_path = temp_dir / "output.#{index}.#{ext}"
|
272
|
+
send "render_#{ext}", out_path, name: name, external: external, **options do |dither: false, **opts|
|
273
|
+
(dither ? dithers : rasters)[opts]
|
274
|
+
end
|
275
|
+
next out_path, path
|
276
|
+
end
|
277
|
+
|
278
|
+
safely "saving, please wait..." do
|
279
|
+
outputs.each do |out_path, path|
|
280
|
+
FileUtils.cp out_path, path
|
281
|
+
log_success "created %s" % path
|
282
|
+
end
|
283
|
+
|
284
|
+
paths.select do |path|
|
285
|
+
%w[.png .tif .jpg].include? path.extname
|
286
|
+
end.group_by do |path|
|
287
|
+
path.parent / path.basename(path.extname)
|
288
|
+
end.keys.each do |base|
|
289
|
+
write_world_file Pathname("#{base}.wld"), ppi: options.fetch(:ppi, Formats::PPI)
|
290
|
+
Pathname("#{base}.prj").write "#{@projection}\n"
|
291
|
+
end if worldfile
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
data/lib/nswtopo/os.rb
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
module NSWTopo
|
2
|
+
module OS
|
3
|
+
Error = Class.new RuntimeError
|
4
|
+
Missing = Class.new RuntimeError
|
5
|
+
|
6
|
+
GDAL = %w[
|
7
|
+
gdal_contour
|
8
|
+
gdal_grid
|
9
|
+
gdal_rasterize
|
10
|
+
gdal_translate
|
11
|
+
gdaladdo
|
12
|
+
gdalbuildvrt
|
13
|
+
gdaldem
|
14
|
+
gdalenhance
|
15
|
+
gdalinfo
|
16
|
+
gdallocationinfo
|
17
|
+
gdalmanage
|
18
|
+
gdalserver
|
19
|
+
gdalsrsinfo
|
20
|
+
gdaltindex
|
21
|
+
gdaltransform
|
22
|
+
gdalwarp
|
23
|
+
gnmanalyse
|
24
|
+
gnmmanage
|
25
|
+
nearblack
|
26
|
+
ogr2ogr
|
27
|
+
ogrinfo
|
28
|
+
ogrlineref
|
29
|
+
ogrtindex
|
30
|
+
testepsg
|
31
|
+
]
|
32
|
+
ImageMagick = %w[
|
33
|
+
animate
|
34
|
+
compare
|
35
|
+
composite
|
36
|
+
conjure
|
37
|
+
convert
|
38
|
+
display
|
39
|
+
identify
|
40
|
+
import
|
41
|
+
mogrify
|
42
|
+
montage
|
43
|
+
stream
|
44
|
+
]
|
45
|
+
SQLite3 = %w[sqlite3]
|
46
|
+
PNGQuant = %w[pngquant]
|
47
|
+
GIMP = %w[gimp]
|
48
|
+
Zip = %w[zip]
|
49
|
+
SevenZ = %w[7z]
|
50
|
+
|
51
|
+
extend self
|
52
|
+
|
53
|
+
%w[GDAL ImageMagick SQLite3 PNGQuant GIMP Zip SevenZ].each do |package|
|
54
|
+
OS.const_get(package).each do |command|
|
55
|
+
define_method command do |*args, &block|
|
56
|
+
Open3.popen3 command, *args.map(&:to_s) do |stdin, stdout, stderr, thread|
|
57
|
+
thr_in = Thread.new do
|
58
|
+
block.call(stdin) if block
|
59
|
+
rescue Errno::EPIPE
|
60
|
+
ensure
|
61
|
+
stdin.close
|
62
|
+
end
|
63
|
+
thr_out = Thread.new { stdout.read }
|
64
|
+
thr_err = Thread.new { stderr.read }
|
65
|
+
[thr_in, thr_out, thr_err].each(&:join)
|
66
|
+
raise Error, "#{command}: #{thr_err.value.empty? ? thr_out.value : thr_err.value}" unless thread.value.success?
|
67
|
+
thr_out.value
|
68
|
+
end
|
69
|
+
rescue Errno::ENOENT
|
70
|
+
raise Missing, "#{package} not installed"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
data/lib/nswtopo/zip.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
module NSWTopo
|
2
|
+
module Zip
|
3
|
+
def zip(directory, archive)
|
4
|
+
Enumerator.new do |yielder|
|
5
|
+
yielder << ->(dir) { OS.zip "-r", archive.expand_path, *Pathname.glob('*') }
|
6
|
+
yielder << ->(dir) { OS.send "7z", "a", "-tzip", "-r", archive.expand_path, *Pathname.glob('*') }
|
7
|
+
raise "no zip utility installed"
|
8
|
+
end.each do |zip|
|
9
|
+
Dir.chdir(directory, &zip)
|
10
|
+
break
|
11
|
+
rescue OS::Missing
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|