proj4rb 4.1.1 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +98 -0
- data/Gemfile +4 -4
- data/README.md +53 -0
- data/lib/api/proj.rb +750 -0
- data/lib/api/proj_experimental.rb +7 -0
- data/lib/api/proj_ffi.rb +47 -0
- data/lib/api/proj_version.rb +26 -0
- data/lib/examples/axis_order_normalization.rb +13 -0
- data/lib/examples/batch_transformation.rb +25 -0
- data/lib/examples/context_logging.rb +26 -0
- data/lib/examples/crs_identification.rb +18 -0
- data/lib/examples/database_query.rb +27 -0
- data/lib/examples/geodetic_distance.rb +38 -0
- data/lib/examples/geodetic_to_projected.rb +18 -0
- data/lib/examples/operation_factory_context.rb +19 -0
- data/lib/examples/pipeline_operator.rb +21 -0
- data/lib/examples/promote_demote_3d.rb +23 -0
- data/lib/examples/serialization_formats.rb +17 -0
- data/lib/examples/transform_bounds.rb +18 -0
- data/lib/examples/transformation_with_area.rb +18 -0
- data/lib/proj/area.rb +74 -74
- data/lib/proj/axis_info.rb +44 -44
- data/lib/proj/bounds.rb +22 -0
- data/lib/proj/bounds3d.rb +45 -0
- data/lib/proj/context.rb +57 -23
- data/lib/proj/conversion.rb +94 -91
- data/lib/proj/coordinate.rb +304 -281
- data/lib/proj/coordinate_metadata.rb +38 -38
- data/lib/proj/coordinate_operation_mixin.rb +464 -381
- data/lib/proj/coordinate_system.rb +143 -137
- data/lib/proj/crs.rb +688 -680
- data/lib/proj/crs_info.rb +47 -47
- data/lib/proj/database.rb +310 -305
- data/lib/proj/datum.rb +32 -32
- data/lib/proj/datum_ensemble.rb +34 -34
- data/lib/proj/domain.rb +82 -0
- data/lib/proj/ellipsoid.rb +77 -77
- data/lib/proj/error.rb +7 -8
- data/lib/proj/file_api_callbacks.rb +165 -0
- data/lib/proj/grid.rb +121 -121
- data/lib/proj/grid_cache.rb +65 -64
- data/lib/proj/grid_info.rb +19 -19
- data/lib/proj/life_span.rb +21 -0
- data/lib/proj/network_api_callbacks.rb +86 -0
- data/lib/proj/operation.rb +66 -42
- data/lib/proj/operation_factory_context.rb +4 -2
- data/lib/proj/options.rb +41 -0
- data/lib/proj/parameter.rb +37 -37
- data/lib/proj/parameters.rb +106 -107
- data/lib/proj/pj_axis_description.rb +26 -0
- data/lib/proj/pj_object.rb +602 -672
- data/lib/proj/pj_objects.rb +45 -45
- data/lib/proj/pj_param_description.rb +28 -0
- data/lib/proj/prime_meridian.rb +65 -65
- data/lib/proj/projection.rb +54 -25
- data/lib/proj/session.rb +2 -0
- data/lib/proj/transformation.rb +102 -102
- data/lib/proj/unit.rb +81 -108
- data/lib/proj.rb +10 -3
- data/lib/proj4.rb +5 -5
- data/proj4rb.gemspec +10 -5
- data/test/abstract_test.rb +7 -5
- data/test/context_test.rb +210 -172
- data/test/context_validation_test.rb +11 -0
- data/test/conversion_test.rb +376 -368
- data/test/coordinate_metadata_test.rb +34 -0
- data/test/coordinate_system_test.rb +162 -144
- data/test/coordinate_test.rb +289 -34
- data/test/crs_test.rb +1112 -1082
- data/test/database_test.rb +407 -391
- data/test/datum_ensemble_test.rb +64 -64
- data/test/datum_test.rb +61 -54
- data/test/domain_test.rb +72 -0
- data/test/ellipsoid_test.rb +80 -80
- data/test/examples_test.rb +149 -0
- data/test/file_api_example.rb +58 -0
- data/test/file_api_test.rb +74 -66
- data/test/grid_cache_test.rb +72 -72
- data/test/grid_test.rb +126 -141
- data/test/network_api_example.rb +48 -0
- data/test/network_api_test.rb +33 -45
- data/test/operation_factory_context_test.rb +225 -205
- data/test/operation_test.rb +40 -29
- data/test/options_test.rb +17 -0
- data/test/parameters_test.rb +86 -40
- data/test/pj_object_test.rb +221 -187
- data/test/prime_meridian_test.rb +75 -75
- data/test/proj_test.rb +58 -58
- data/test/projection_test.rb +680 -650
- data/test/session_test.rb +78 -77
- data/test/transformation_test.rb +238 -210
- data/test/unit_test.rb +114 -76
- metadata +44 -32
- data/ChangeLog +0 -94
- data/README.rdoc +0 -189
- data/lib/api/api.rb +0 -117
- data/lib/api/api_5_0.rb +0 -338
- data/lib/api/api_5_1.rb +0 -7
- data/lib/api/api_5_2.rb +0 -5
- data/lib/api/api_6_0.rb +0 -146
- data/lib/api/api_6_1.rb +0 -5
- data/lib/api/api_6_2.rb +0 -10
- data/lib/api/api_6_3.rb +0 -6
- data/lib/api/api_7_0.rb +0 -69
- data/lib/api/api_7_1.rb +0 -73
- data/lib/api/api_7_2.rb +0 -14
- data/lib/api/api_8_0.rb +0 -6
- data/lib/api/api_8_1.rb +0 -24
- data/lib/api/api_8_2.rb +0 -6
- data/lib/api/api_9_1.rb +0 -7
- data/lib/api/api_9_2.rb +0 -9
- data/lib/api/api_9_4.rb +0 -6
- data/lib/api/api_experimental.rb +0 -201
- data/lib/proj/file_api.rb +0 -166
- data/lib/proj/network_api.rb +0 -92
data/lib/api/proj_ffi.rb
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Generated by ruby-bindgen (1.0.0)
|
|
2
|
+
|
|
3
|
+
require 'ffi'
|
|
4
|
+
|
|
5
|
+
module Proj
|
|
6
|
+
module Api
|
|
7
|
+
extend FFI::Library
|
|
8
|
+
|
|
9
|
+
def self.library_names
|
|
10
|
+
["proj"]
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.library_versions
|
|
14
|
+
["9_1", "9", "25", "22", "19", "17", "15", "14", "13", "12", "11"]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.search_names
|
|
18
|
+
result = Array.new
|
|
19
|
+
self.library_names.each do |name|
|
|
20
|
+
result << "lib#{name}"
|
|
21
|
+
self.library_versions.each do |version|
|
|
22
|
+
case RbConfig::CONFIG['host_os']
|
|
23
|
+
when /darwin|mac os/
|
|
24
|
+
result << "lib#{name}.#{version}"
|
|
25
|
+
when /mingw/
|
|
26
|
+
result << "lib#{name}-#{version}"
|
|
27
|
+
when /mswin/
|
|
28
|
+
result << "#{name}_#{version}"
|
|
29
|
+
else
|
|
30
|
+
result << "lib#{name}.so.#{version}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
result
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
if ENV['PROJ_LIB_PATH']
|
|
38
|
+
ffi_lib self.search_names.map { |name| File.join(ENV['PROJ_LIB_PATH'], name) } + self.search_names
|
|
39
|
+
else
|
|
40
|
+
ffi_lib self.search_names
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
require_relative 'proj_version'
|
|
46
|
+
require_relative './proj'
|
|
47
|
+
require_relative './proj_experimental'
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Generated by ruby-bindgen (1.0.0)
|
|
2
|
+
|
|
3
|
+
module Proj
|
|
4
|
+
module Api
|
|
5
|
+
class PjInfo < FFI::Struct
|
|
6
|
+
layout :major, :int,
|
|
7
|
+
:minor, :int,
|
|
8
|
+
:patch, :int,
|
|
9
|
+
:release, :string,
|
|
10
|
+
:version, :string,
|
|
11
|
+
:searchpath, :string,
|
|
12
|
+
:paths, :pointer,
|
|
13
|
+
:path_count, :size_t
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
attach_function :proj_info, :proj_info, [], PjInfo.by_value
|
|
17
|
+
|
|
18
|
+
def self.proj_version
|
|
19
|
+
info = proj_info
|
|
20
|
+
info[:major] * 10000 + info[:minor] * 100 + info[:patch]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
info = proj_info
|
|
24
|
+
Proj::Api::PROJ_VERSION = Gem::Version.new("#{info[:major]}.#{info[:minor]}.#{info[:patch]}")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'proj'
|
|
4
|
+
|
|
5
|
+
transform = Proj::Transformation.new('EPSG:4326', 'EPSG:3857')
|
|
6
|
+
normalized = transform.normalize_for_visualization
|
|
7
|
+
|
|
8
|
+
coord = Proj::Coordinate.new(x: -122.4194, y: 37.7749)
|
|
9
|
+
result = normalized.forward(coord)
|
|
10
|
+
|
|
11
|
+
raise 'normalization transform failed' unless result.x.finite? && result.y.finite?
|
|
12
|
+
|
|
13
|
+
puts "ok: web mercator x=#{result.x}, y=#{result.y}"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'proj'
|
|
4
|
+
|
|
5
|
+
# Create a UTM zone 32 conversion.
|
|
6
|
+
conversion = Proj::Conversion.new('+proj=utm +zone=32 +ellps=GRS80')
|
|
7
|
+
|
|
8
|
+
# Build an array of coordinates (lon/lat in radians).
|
|
9
|
+
coordinates = [
|
|
10
|
+
Proj::Coordinate.new(lon: Proj.degrees_to_radians(12), lat: Proj.degrees_to_radians(55), z: 45),
|
|
11
|
+
Proj::Coordinate.new(lon: Proj.degrees_to_radians(12), lat: Proj.degrees_to_radians(56), z: 50),
|
|
12
|
+
Proj::Coordinate.new(lon: Proj.degrees_to_radians(13), lat: Proj.degrees_to_radians(55), z: 30)
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
# Transform all at once (more efficient than looping).
|
|
16
|
+
results = conversion.transform_array(coordinates, :PJ_FWD)
|
|
17
|
+
|
|
18
|
+
results.each_with_index do |coord, i|
|
|
19
|
+
puts "Point #{i}: x=#{coord.x.round(2)}, y=#{coord.y.round(2)}, z=#{coord.z}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
raise 'wrong count' unless results.size == 3
|
|
23
|
+
raise 'bad transform' unless results.all? { |c| c.x.finite? && c.y.finite? }
|
|
24
|
+
|
|
25
|
+
puts 'ok'
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'proj'
|
|
4
|
+
|
|
5
|
+
# Create a dedicated context so we don't affect the default.
|
|
6
|
+
context = Proj::Context.new
|
|
7
|
+
|
|
8
|
+
# Collect log messages into an array.
|
|
9
|
+
messages = []
|
|
10
|
+
|
|
11
|
+
context.set_log_function do |_pointer, level, message|
|
|
12
|
+
messages << "[level=#{level}] #{message}"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Trigger a log message by setting an invalid database path.
|
|
16
|
+
begin
|
|
17
|
+
context.database.path = '/nonexistent'
|
|
18
|
+
rescue Proj::Error
|
|
19
|
+
# Expected - we just want to see the log output.
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
messages.each { |msg| puts msg }
|
|
23
|
+
|
|
24
|
+
raise 'no log messages captured' if messages.empty?
|
|
25
|
+
|
|
26
|
+
puts 'ok'
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'proj'
|
|
4
|
+
|
|
5
|
+
# Start with a CRS created from a well-known string.
|
|
6
|
+
crs = Proj::Crs.new('OGC:CRS84')
|
|
7
|
+
|
|
8
|
+
# Identify it against the OGC authority.
|
|
9
|
+
objects, confidences = crs.identify('OGC')
|
|
10
|
+
|
|
11
|
+
objects.count.times do |i|
|
|
12
|
+
puts "Match: #{objects[i]} (confidence: #{confidences[i]}%)"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
raise 'no matches' if objects.count.zero?
|
|
16
|
+
raise 'low confidence' if confidences[0] < 100
|
|
17
|
+
|
|
18
|
+
puts 'ok'
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'proj'
|
|
4
|
+
|
|
5
|
+
database = Proj::Database.new(Proj::Context.current)
|
|
6
|
+
|
|
7
|
+
# List all authorities in the database.
|
|
8
|
+
authorities = database.authorities
|
|
9
|
+
puts "Authorities: #{authorities.to_a.first(5).join(', ')} (#{authorities.count} total)"
|
|
10
|
+
|
|
11
|
+
# Count EPSG coordinate reference systems.
|
|
12
|
+
codes = database.codes('EPSG', :PJ_TYPE_GEOGRAPHIC_2D_CRS)
|
|
13
|
+
puts "EPSG geographic 2D CRS codes: #{codes.count}"
|
|
14
|
+
|
|
15
|
+
# Query CRS entries for a specific authority.
|
|
16
|
+
crs_infos = database.crs_info('EPSG')
|
|
17
|
+
puts "EPSG CRS entries: #{crs_infos.count}"
|
|
18
|
+
|
|
19
|
+
# Show the first entry.
|
|
20
|
+
info = crs_infos.first
|
|
21
|
+
puts "First: #{info.auth_name}:#{info.code} - #{info.name} (#{info.crs_type})"
|
|
22
|
+
puts " Area: #{info.area_name}"
|
|
23
|
+
puts " Bounds: W=#{info.west_lon_degree}, S=#{info.south_lat_degree}, E=#{info.east_lon_degree}, N=#{info.north_lat_degree}"
|
|
24
|
+
|
|
25
|
+
raise 'no CRS info found' if crs_infos.empty?
|
|
26
|
+
|
|
27
|
+
puts 'ok'
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'proj'
|
|
4
|
+
|
|
5
|
+
# Use WGS 84 as the ellipsoid for distance calculations.
|
|
6
|
+
crs = Proj::Crs.new('EPSG:4326')
|
|
7
|
+
|
|
8
|
+
# Paris (lon, lat) and Berlin (lon, lat) in radians.
|
|
9
|
+
paris = Proj::Coordinate.new(x: Proj.degrees_to_radians(2.3522),
|
|
10
|
+
y: Proj.degrees_to_radians(48.8566))
|
|
11
|
+
berlin = Proj::Coordinate.new(x: Proj.degrees_to_radians(13.4050),
|
|
12
|
+
y: Proj.degrees_to_radians(52.5200))
|
|
13
|
+
|
|
14
|
+
# Simple geodesic distance on the ellipsoid.
|
|
15
|
+
distance = crs.lp_distance(paris, berlin)
|
|
16
|
+
raise "unexpected distance #{distance}" unless (distance - 878_000).abs < 5_000
|
|
17
|
+
|
|
18
|
+
puts "Distance Paris -> Berlin: #{(distance / 1000).round(1)} km"
|
|
19
|
+
|
|
20
|
+
# Full geodesic: distance + forward/reverse azimuth.
|
|
21
|
+
# Returns distance (meters) in x, forward azimuth (degrees) in y, reverse azimuth (degrees) in z.
|
|
22
|
+
result = crs.geod_distance(paris, berlin)
|
|
23
|
+
puts "Geodesic distance: #{(result.x / 1000).round(1)} km"
|
|
24
|
+
puts "Forward azimuth: #{result.y.round(2)} degrees"
|
|
25
|
+
puts "Reverse azimuth: #{result.z.round(2)} degrees"
|
|
26
|
+
|
|
27
|
+
# Geodesic direct problem (PROJ 9.7+): given a start point, azimuth, and distance,
|
|
28
|
+
# compute the endpoint.
|
|
29
|
+
if Proj::Api::PROJ_VERSION >= Gem::Version.new('9.7.0')
|
|
30
|
+
# From Paris, azimuth 0 (due north), 100 km.
|
|
31
|
+
endpoint = crs.geod_direct(paris, 0.0, 100_000)
|
|
32
|
+
puts "100 km north of Paris: lon=#{Proj.radians_to_degrees(endpoint.x).round(4)}, lat=#{Proj.radians_to_degrees(endpoint.y).round(4)}"
|
|
33
|
+
raise 'geod_direct failed' unless endpoint.x.finite? && endpoint.y.finite?
|
|
34
|
+
else
|
|
35
|
+
puts "geod_direct requires PROJ 9.7+ (have #{Proj::Api::PROJ_VERSION})"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
puts 'ok'
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'proj'
|
|
4
|
+
|
|
5
|
+
context = Proj::Context.new
|
|
6
|
+
crs_projected = Proj::Crs.new('+proj=utm +zone=32 +datum=WGS84 +type=crs', context)
|
|
7
|
+
crs_geodetic = crs_projected.geodetic_crs
|
|
8
|
+
transform = Proj::Transformation.new(crs_geodetic, crs_projected, context)
|
|
9
|
+
|
|
10
|
+
from = Proj::Coordinate.new(lon: 12.0, lat: 55.0)
|
|
11
|
+
to = transform.forward(from)
|
|
12
|
+
back = transform.inverse(to)
|
|
13
|
+
|
|
14
|
+
raise 'forward failed' unless to.x.finite? && to.y.finite?
|
|
15
|
+
raise 'inverse mismatch lon' unless (back.lon - 12.0).abs < 1e-8
|
|
16
|
+
raise 'inverse mismatch lat' unless (back.lat - 55.0).abs < 1e-8
|
|
17
|
+
|
|
18
|
+
puts "ok: projected x=#{to.x}, y=#{to.y}"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'proj'
|
|
4
|
+
|
|
5
|
+
context = Proj::Context.new
|
|
6
|
+
source = Proj::Crs.create_from_database('EPSG', '4267', :PJ_CATEGORY_CRS)
|
|
7
|
+
target = Proj::Crs.create_from_database('EPSG', '4269', :PJ_CATEGORY_CRS)
|
|
8
|
+
|
|
9
|
+
factory = Proj::OperationFactoryContext.new(context)
|
|
10
|
+
factory.spatial_criterion = :PROJ_SPATIAL_CRITERION_PARTIAL_INTERSECTION
|
|
11
|
+
factory.grid_availability = :PROJ_GRID_AVAILABILITY_IGNORED
|
|
12
|
+
|
|
13
|
+
operations = factory.create_operations(source, target)
|
|
14
|
+
raise 'no operations found' if operations.count.zero?
|
|
15
|
+
|
|
16
|
+
index = operations.suggested_operation(:PJ_FWD, Proj::Coordinate.new(x: 40, y: -100))
|
|
17
|
+
raise 'invalid suggested operation index' if index.negative? || index >= operations.count
|
|
18
|
+
|
|
19
|
+
puts "ok: best operation #{index} => #{operations[index].name}"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'proj'
|
|
4
|
+
|
|
5
|
+
conversion = Proj::Conversion.new(<<~EOS)
|
|
6
|
+
+proj=pipeline
|
|
7
|
+
+step +inv +proj=lcc +lat_1=33.88333333333333
|
|
8
|
+
+lat_2=32.78333333333333 +lat_0=32.16666666666666
|
|
9
|
+
+lon_0=-116.25 +x_0=2000000.0001016 +y_0=500000.0001016001 +ellps=GRS80
|
|
10
|
+
+towgs84=0,0,0,0,0,0,0 +units=us-ft +no_defs
|
|
11
|
+
+step +proj=lcc +lat_1=33.88333333333333 +lat_2=32.78333333333333 +lat_0=32.16666666666666
|
|
12
|
+
+lon_0=-116.25 +x_0=2000000 +y_0=500000
|
|
13
|
+
+ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs
|
|
14
|
+
EOS
|
|
15
|
+
|
|
16
|
+
from = Proj::Coordinate.new(x: 4_760_096.421921, y: 3_744_293.729449)
|
|
17
|
+
to = conversion.forward(from)
|
|
18
|
+
|
|
19
|
+
raise 'pipeline conversion failed' unless to.x.finite? && to.y.finite?
|
|
20
|
+
|
|
21
|
+
puts "ok: converted x=#{to.x}, y=#{to.y}"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'proj'
|
|
4
|
+
|
|
5
|
+
# Start with a 2D geographic CRS.
|
|
6
|
+
crs_2d = Proj::Crs.new('EPSG:4326')
|
|
7
|
+
puts "Original: #{crs_2d.name}"
|
|
8
|
+
puts " Type: #{crs_2d.proj_type}"
|
|
9
|
+
|
|
10
|
+
# Promote to 3D (adds an ellipsoidal height axis).
|
|
11
|
+
crs_3d = crs_2d.promote_to_3d
|
|
12
|
+
puts "Promoted: #{crs_3d.name}"
|
|
13
|
+
puts " Type: #{crs_3d.proj_type}"
|
|
14
|
+
|
|
15
|
+
# Demote back to 2D.
|
|
16
|
+
crs_back = crs_3d.demote_to_2d
|
|
17
|
+
puts "Demoted: #{crs_back.name}"
|
|
18
|
+
puts " Type: #{crs_back.proj_type}"
|
|
19
|
+
|
|
20
|
+
raise 'promote failed' unless crs_3d.proj_type == :PJ_TYPE_GEOGRAPHIC_3D_CRS
|
|
21
|
+
raise 'demote failed' unless crs_back.proj_type == :PJ_TYPE_GEOGRAPHIC_2D_CRS
|
|
22
|
+
|
|
23
|
+
puts 'ok'
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'proj'
|
|
5
|
+
|
|
6
|
+
crs = Proj::Crs.new('EPSG:4326')
|
|
7
|
+
wkt = crs.to_wkt(:PJ_WKT2_2019, multiline: false)
|
|
8
|
+
projjson = crs.to_json(multiline: false)
|
|
9
|
+
proj_str = crs.to_proj_string
|
|
10
|
+
|
|
11
|
+
raise 'wkt missing' if wkt.nil? || wkt.empty?
|
|
12
|
+
raise 'proj string missing' if proj_str.nil? || proj_str.empty?
|
|
13
|
+
|
|
14
|
+
parsed = JSON.parse(projjson)
|
|
15
|
+
raise 'projjson parse failed' unless parsed['type']
|
|
16
|
+
|
|
17
|
+
puts "ok: serialized #{crs.auth}"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'proj'
|
|
4
|
+
|
|
5
|
+
# Transform a bounding box from geographic to projected coordinates.
|
|
6
|
+
transform = Proj::Transformation.new('EPSG:4326',
|
|
7
|
+
'+proj=laea +lat_0=45 +lon_0=-100 +x_0=0 +y_0=0 +a=6370997 +b=6370997 +units=m +no_defs')
|
|
8
|
+
|
|
9
|
+
# Bounding box covering parts of North America (lat_min, lon_min, lat_max, lon_max).
|
|
10
|
+
bounds = Proj::Bounds.new(40, -120, 64, -80)
|
|
11
|
+
result = transform.transform_bounds(bounds, :PJ_FWD, 21)
|
|
12
|
+
|
|
13
|
+
puts "Input: xmin=#{bounds.xmin}, ymin=#{bounds.ymin}, xmax=#{bounds.xmax}, ymax=#{bounds.ymax}"
|
|
14
|
+
puts "Output: xmin=#{result.xmin.round(2)}, ymin=#{result.ymin.round(2)}, xmax=#{result.xmax.round(2)}, ymax=#{result.ymax.round(2)}"
|
|
15
|
+
|
|
16
|
+
raise 'transform_bounds failed' unless result.xmin.finite? && result.ymax.finite?
|
|
17
|
+
|
|
18
|
+
puts 'ok'
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'proj'
|
|
4
|
+
|
|
5
|
+
area = Proj::Area.new(
|
|
6
|
+
west_lon_degree: -114.1324,
|
|
7
|
+
south_lat_degree: 49.5614,
|
|
8
|
+
east_lon_degree: 3.76488,
|
|
9
|
+
north_lat_degree: 62.1463
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
transform = Proj::Transformation.new('EPSG:4277', 'EPSG:4326', area: area)
|
|
13
|
+
from = Proj::Coordinate.new(x: 50, y: -2)
|
|
14
|
+
to = transform.forward(from)
|
|
15
|
+
|
|
16
|
+
raise 'area transformation failed' unless to.x.finite? && to.y.finite?
|
|
17
|
+
|
|
18
|
+
puts "ok: transformed x=#{to.x}, y=#{to.y}"
|
data/lib/proj/area.rb
CHANGED
|
@@ -1,74 +1,74 @@
|
|
|
1
|
-
# encoding: UTF-8
|
|
2
|
-
|
|
3
|
-
module Proj
|
|
4
|
-
# Areas are used to specify the area of use for the choice of relevant coordinate operations.
|
|
5
|
-
# See Transformation#new
|
|
6
|
-
class Area
|
|
7
|
-
attr_reader :name, :west_lon_degree, :south_lat_degree, :east_lon_degree, :north_lat_degree
|
|
8
|
-
|
|
9
|
-
# @!visibility private
|
|
10
|
-
def self.finalize(pointer)
|
|
11
|
-
proc do
|
|
12
|
-
Api.proj_area_destroy(pointer)
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def initialize(west_lon_degree:, south_lat_degree:, east_lon_degree:, north_lat_degree:, name: nil)
|
|
17
|
-
@west_lon_degree = west_lon_degree
|
|
18
|
-
@south_lat_degree = south_lat_degree
|
|
19
|
-
@east_lon_degree = east_lon_degree
|
|
20
|
-
@north_lat_degree = north_lat_degree
|
|
21
|
-
@name = name
|
|
22
|
-
create_area
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def to_ptr
|
|
26
|
-
@area
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# Sets the bounds for an area
|
|
30
|
-
#
|
|
31
|
-
# @see https://proj.org/development/reference/functions.html#c.proj_area_set_bbox
|
|
32
|
-
#
|
|
33
|
-
# @param west_lon_degree [Float] West longitude, in degrees. In [-180,180] range.
|
|
34
|
-
# @param south_lat_degree [Float] South latitude, in degrees. In [-90,90] range.
|
|
35
|
-
# @param east_lon_degree [Float] East longitude, in degrees. In [-180,180] range.
|
|
36
|
-
# @param north_lat_degree [Float] North latitude, in degrees. In [-90,90] range.
|
|
37
|
-
def set_bounds(west_lon_degree:, south_lat_degree:, east_lon_degree:, north_lat_degree:)
|
|
38
|
-
Api.proj_area_set_bbox(self, west_lon_degree, south_lat_degree, east_lon_degree, north_lat_degree)
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
# Sets the name for an area
|
|
42
|
-
#
|
|
43
|
-
# @param value [String] The name of the area
|
|
44
|
-
def name=(value)
|
|
45
|
-
@name =
|
|
46
|
-
# This Api wasn't added until proj 9.1
|
|
47
|
-
if defined?(Api.proj_area_set_name)
|
|
48
|
-
Api.proj_area_set_name(self, value)
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# Returns nice printout of an Area
|
|
53
|
-
#
|
|
54
|
-
# @return [String]
|
|
55
|
-
def to_s
|
|
56
|
-
"Area west_lon_degree: #{self.west_lon_degree}, south_lat_degree: #{self.south_lat_degree}, east_lon_degree: #{self.east_lon_degree}, north_lat_degree: #{self.north_lat_degree}"
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
private
|
|
60
|
-
|
|
61
|
-
# Creates an area
|
|
62
|
-
#
|
|
63
|
-
# @see https://proj.org/development/reference/functions.html#c.proj_area_create
|
|
64
|
-
def create_area
|
|
65
|
-
@area = Api.proj_area_create
|
|
66
|
-
self.set_bounds(west_lon_degree: west_lon_degree, south_lat_degree: south_lat_degree,
|
|
67
|
-
east_lon_degree: east_lon_degree, north_lat_degree: north_lat_degree)
|
|
68
|
-
if name
|
|
69
|
-
self.name = name
|
|
70
|
-
end
|
|
71
|
-
ObjectSpace.define_finalizer(self, self.class.finalize(@area))
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
end
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
|
|
3
|
+
module Proj
|
|
4
|
+
# Areas are used to specify the area of use for the choice of relevant coordinate operations.
|
|
5
|
+
# See Transformation#new
|
|
6
|
+
class Area
|
|
7
|
+
attr_reader :name, :west_lon_degree, :south_lat_degree, :east_lon_degree, :north_lat_degree
|
|
8
|
+
|
|
9
|
+
# @!visibility private
|
|
10
|
+
def self.finalize(pointer)
|
|
11
|
+
proc do
|
|
12
|
+
Api.proj_area_destroy(pointer)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(west_lon_degree:, south_lat_degree:, east_lon_degree:, north_lat_degree:, name: nil)
|
|
17
|
+
@west_lon_degree = west_lon_degree
|
|
18
|
+
@south_lat_degree = south_lat_degree
|
|
19
|
+
@east_lon_degree = east_lon_degree
|
|
20
|
+
@north_lat_degree = north_lat_degree
|
|
21
|
+
@name = name
|
|
22
|
+
create_area
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def to_ptr
|
|
26
|
+
@area
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Sets the bounds for an area
|
|
30
|
+
#
|
|
31
|
+
# @see https://proj.org/development/reference/functions.html#c.proj_area_set_bbox
|
|
32
|
+
#
|
|
33
|
+
# @param west_lon_degree [Float] West longitude, in degrees. In [-180,180] range.
|
|
34
|
+
# @param south_lat_degree [Float] South latitude, in degrees. In [-90,90] range.
|
|
35
|
+
# @param east_lon_degree [Float] East longitude, in degrees. In [-180,180] range.
|
|
36
|
+
# @param north_lat_degree [Float] North latitude, in degrees. In [-90,90] range.
|
|
37
|
+
def set_bounds(west_lon_degree:, south_lat_degree:, east_lon_degree:, north_lat_degree:)
|
|
38
|
+
Api.proj_area_set_bbox(self, west_lon_degree, south_lat_degree, east_lon_degree, north_lat_degree)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Sets the name for an area
|
|
42
|
+
#
|
|
43
|
+
# @param value [String] The name of the area
|
|
44
|
+
def name=(value)
|
|
45
|
+
@name = value
|
|
46
|
+
# This Api wasn't added until proj 9.1
|
|
47
|
+
if defined?(Api.proj_area_set_name)
|
|
48
|
+
Api.proj_area_set_name(self, value)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Returns nice printout of an Area
|
|
53
|
+
#
|
|
54
|
+
# @return [String]
|
|
55
|
+
def to_s
|
|
56
|
+
"Area west_lon_degree: #{self.west_lon_degree}, south_lat_degree: #{self.south_lat_degree}, east_lon_degree: #{self.east_lon_degree}, north_lat_degree: #{self.north_lat_degree}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
# Creates an area
|
|
62
|
+
#
|
|
63
|
+
# @see https://proj.org/development/reference/functions.html#c.proj_area_create
|
|
64
|
+
def create_area
|
|
65
|
+
@area = Api.proj_area_create
|
|
66
|
+
self.set_bounds(west_lon_degree: west_lon_degree, south_lat_degree: south_lat_degree,
|
|
67
|
+
east_lon_degree: east_lon_degree, north_lat_degree: north_lat_degree)
|
|
68
|
+
if name
|
|
69
|
+
self.name = name
|
|
70
|
+
end
|
|
71
|
+
ObjectSpace.define_finalizer(self, self.class.finalize(@area))
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
data/lib/proj/axis_info.rb
CHANGED
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
module Proj
|
|
2
|
-
class AxisInfo
|
|
3
|
-
# @!attribute [r] name
|
|
4
|
-
# @return [String] Axis name
|
|
5
|
-
# @!attribute [r] abbreviation
|
|
6
|
-
# @return [String] Axis abbreviation
|
|
7
|
-
# @!attribute [r] direction
|
|
8
|
-
# @return [String] Axis direction
|
|
9
|
-
# @!attribute [r] unit_conv_factor
|
|
10
|
-
# @return [String] Axis unit_conv_factor
|
|
11
|
-
# @!attribute [r] unit_name
|
|
12
|
-
# @return [String] Axis unit_name
|
|
13
|
-
# @!attribute [r] unit_auth_name
|
|
14
|
-
# @return [String] Axis unit_auth_name
|
|
15
|
-
# @!attribute [r] unit_code
|
|
16
|
-
# @return [String] Axis unit_code
|
|
17
|
-
attr_reader :name, :abbreviation, :direction,
|
|
18
|
-
:unit_name, :unit_auth_name, :unit_code, :unit_conv_factor
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
@name = name
|
|
22
|
-
@abbreviation = abbreviation
|
|
23
|
-
@direction = direction
|
|
24
|
-
@unit_conv_factor = unit_conv_factor
|
|
25
|
-
@unit_name = unit_name
|
|
26
|
-
@unit_auth_name = unit_auth_name
|
|
27
|
-
@unit_code = unit_code
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# Returns axis information in
|
|
31
|
-
#
|
|
32
|
-
# @return [
|
|
33
|
-
def to_description
|
|
34
|
-
Api::
|
|
35
|
-
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def unit_type
|
|
39
|
-
database = Database.new(Context.default)
|
|
40
|
-
unit = database.unit(self.unit_auth_name, self.unit_code)
|
|
41
|
-
unit.unit_type
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
1
|
+
module Proj
|
|
2
|
+
class AxisInfo
|
|
3
|
+
# @!attribute [r] name
|
|
4
|
+
# @return [String] Axis name
|
|
5
|
+
# @!attribute [r] abbreviation
|
|
6
|
+
# @return [String] Axis abbreviation
|
|
7
|
+
# @!attribute [r] direction
|
|
8
|
+
# @return [String] Axis direction
|
|
9
|
+
# @!attribute [r] unit_conv_factor
|
|
10
|
+
# @return [String] Axis unit_conv_factor
|
|
11
|
+
# @!attribute [r] unit_name
|
|
12
|
+
# @return [String] Axis unit_name
|
|
13
|
+
# @!attribute [r] unit_auth_name
|
|
14
|
+
# @return [String] Axis unit_auth_name
|
|
15
|
+
# @!attribute [r] unit_code
|
|
16
|
+
# @return [String] Axis unit_code
|
|
17
|
+
attr_reader :name, :abbreviation, :direction,
|
|
18
|
+
:unit_name, :unit_auth_name, :unit_code, :unit_conv_factor
|
|
19
|
+
|
|
20
|
+
def initialize(name:, abbreviation:, direction:, unit_conv_factor:, unit_name:, unit_auth_name:, unit_code:)
|
|
21
|
+
@name = name
|
|
22
|
+
@abbreviation = abbreviation
|
|
23
|
+
@direction = direction
|
|
24
|
+
@unit_conv_factor = unit_conv_factor
|
|
25
|
+
@unit_name = unit_name
|
|
26
|
+
@unit_auth_name = unit_auth_name
|
|
27
|
+
@unit_code = unit_code
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Returns axis information in PjAxisDescription structure
|
|
31
|
+
#
|
|
32
|
+
# @return [PjAxisDescription]
|
|
33
|
+
def to_description
|
|
34
|
+
Api::PjAxisDescription.create(name: name, abbreviation: abbreviation, direction: direction,
|
|
35
|
+
unit_conv_factor: unit_conv_factor, unit_name: unit_name, unit_type: self.unit_type)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def unit_type
|
|
39
|
+
database = Database.new(Context.default)
|
|
40
|
+
unit = database.unit(self.unit_auth_name, self.unit_code)
|
|
41
|
+
unit.unit_type
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
data/lib/proj/bounds.rb
CHANGED
|
@@ -1,7 +1,29 @@
|
|
|
1
1
|
module Proj
|
|
2
|
+
# Represents a 2D bounding box defined by minimum and maximum x/y values.
|
|
3
|
+
# Used with {CoordinateOperationMixin#transform_bounds} to transform rectangular
|
|
4
|
+
# regions between coordinate reference systems.
|
|
2
5
|
class Bounds
|
|
6
|
+
# @!attribute [r] xmin
|
|
7
|
+
# @return [Float] Minimum x value (e.g., west longitude)
|
|
8
|
+
# @!attribute [r] ymin
|
|
9
|
+
# @return [Float] Minimum y value (e.g., south latitude)
|
|
10
|
+
# @!attribute [r] xmax
|
|
11
|
+
# @return [Float] Maximum x value (e.g., east longitude)
|
|
12
|
+
# @!attribute [r] ymax
|
|
13
|
+
# @return [Float] Maximum y value (e.g., north latitude)
|
|
14
|
+
# @!attribute [r] name
|
|
15
|
+
# @return [String, nil] Optional name for this bounds
|
|
3
16
|
attr_reader :name, :xmin, :ymin, :xmax, :ymax
|
|
4
17
|
|
|
18
|
+
# Creates a new 2D bounding box.
|
|
19
|
+
#
|
|
20
|
+
# @param xmin [Float] Minimum x value
|
|
21
|
+
# @param ymin [Float] Minimum y value
|
|
22
|
+
# @param xmax [Float] Maximum x value
|
|
23
|
+
# @param ymax [Float] Maximum y value
|
|
24
|
+
# @param name [String, nil] Optional name
|
|
25
|
+
#
|
|
26
|
+
# @return [Bounds]
|
|
5
27
|
def initialize(xmin, ymin, xmax, ymax, name = nil)
|
|
6
28
|
@xmin = xmin
|
|
7
29
|
@ymin = ymin
|