├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .yardopts ├── ChangeLog ├── Examples.rdoc ├── Gemfile ├── MIT-LICENSE ├── README.rdoc ├── Rakefile ├── lib ├── api │ ├── api.rb │ ├── api_5_0.rb │ ├── api_5_1.rb │ ├── api_5_2.rb │ ├── api_6_0.rb │ ├── api_6_1.rb │ ├── api_6_2.rb │ ├── api_6_3.rb │ ├── api_7_0.rb │ ├── api_7_1.rb │ ├── api_7_2.rb │ ├── api_8_0.rb │ ├── api_8_1.rb │ ├── api_8_2.rb │ ├── api_9_1.rb │ ├── api_9_2.rb │ ├── api_9_4.rb │ └── api_experimental.rb ├── proj.rb ├── proj │ ├── area.rb │ ├── axis_info.rb │ ├── bounds.rb │ ├── context.rb │ ├── conversion.rb │ ├── coordinate.rb │ ├── coordinate_metadata.rb │ ├── coordinate_operation_mixin.rb │ ├── coordinate_system.rb │ ├── crs.rb │ ├── crs_info.rb │ ├── database.rb │ ├── datum.rb │ ├── datum_ensemble.rb │ ├── ellipsoid.rb │ ├── error.rb │ ├── file_api.rb │ ├── grid.rb │ ├── grid_cache.rb │ ├── grid_info.rb │ ├── network_api.rb │ ├── operation.rb │ ├── operation_factory_context.rb │ ├── parameter.rb │ ├── parameters.rb │ ├── pj_object.rb │ ├── pj_objects.rb │ ├── prime_meridian.rb │ ├── projection.rb │ ├── session.rb │ ├── strings.rb │ ├── transformation.rb │ └── unit.rb └── proj4.rb ├── proj4rb.gemspec └── test ├── abstract_test.rb ├── context_test.rb ├── conversion_test.rb ├── coordinate_system_test.rb ├── coordinate_test.rb ├── crs_test.rb ├── database_test.rb ├── datum_ensemble_test.rb ├── datum_test.rb ├── ellipsoid_test.rb ├── file_api_test.rb ├── grid_cache_test.rb ├── grid_test.rb ├── network_api_test.rb ├── operation_factory_context_test.rb ├── operation_test.rb ├── parameters_test.rb ├── pj_object_test.rb ├── prime_meridian_test.rb ├── proj_test.rb ├── projection_test.rb ├── session_test.rb ├── transformation_test.rb └── unit_test.rb /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: proj4rb 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | 5 | jobs: 6 | test: 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | os: [ubuntu-22.04, macos-13, macos-14] 11 | ruby: [3.2, 3.3] 12 | runs-on: ${{matrix.os}} 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | - name: CPU architecture 17 | run: uname -p 18 | - name: Install Proj (Ubuntu) 19 | if: startsWith(matrix.os, 'ubuntu') 20 | run: | 21 | sudo apt-get update 22 | sudo apt-get install libproj22 23 | - name: Install Proj (Mac) 24 | if: startsWith(matrix.os, 'macos') 25 | run: | 26 | brew update 27 | brew install proj 28 | - name: Set up Ruby 29 | uses: ruby/setup-ruby@v1 30 | with: 31 | ruby-version: ${{matrix.ruby}} 32 | bundler-cache: true 33 | - name: Test 34 | run: bundle exec rake test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /pkg 2 | /nbproject 3 | /tmp 4 | /ext/Makefile 5 | /ext/mkmf.log 6 | /lib/1.8/proj4_ruby.so 7 | /.idea 8 | /Gemfile.lock 9 | /doc 10 | /.yardoc 11 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --no-private --protected lib/**/*.rb - Examples.rdoc -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | 4.1.0 - March 12, 2023 2 | ====================== 3 | * Fix YARD warnings 4 | * Fix YARD types to match RBS types 5 | * Don't use type as attribute or method name to avoid conflicts with RBS 6 | 7 | 4.0.0 - March 12, 2023 8 | ====================== 9 | * Support Proj 9 10 | * Add support for missing APIs - the gem now provides almost 100% coverage 11 | * Add support for Proj experimental APIs including the creation of projection conversions 12 | * Add/updated support for a number of ISO 19111 classes, including PrimeMeridian, Ellipsoid, 13 | Datum, DatumEnsemble, CoordinateOperation and Parameters 14 | * Greatly improved documentation (https://rubydoc.info/github/cfis/proj4rb) 15 | * Remove old Proj 4.9 support 16 | * Note this release does have some API changes. These include the removal of Proj 4.9 Point and Coordinate classes, changes to the PrimeMeridian class and changes to the Ellipsoid class. These changes should not impact most users. 17 | 18 | 3.0.0 - September 26, 2021 19 | ========================= 20 | * Support Proj 8 which removes the old Proj API (Charlie Savage) 21 | * Support newer versions of FFI which remove support for returning strings from callbacks (Charlie Savage) 22 | 23 | 2.2.2 - January 10, 2020 24 | ========================= 25 | * Move proj_context_set_autoclose_database to api 6.2 - Jan Klimke) 26 | * Improve search path generation code (Charlie Savage) 27 | 28 | 2.2.1 - January 8, 2020 29 | ========================= 30 | * Move proj_as_projjson from version 6.0 to 6.2 api (Charlie Savage) 31 | * Improve search path generation code (Charlie Savage) 32 | 33 | 2.2.0 - January 7, 2020 34 | ========================= 35 | * Fix broken gem - was not including all api files (Jan Klimke) 36 | * Add paths on MacOS when using Brew (Jan Klimke) 37 | * Various code cleanups (Charlie Savage) 38 | 39 | 2.1.0 - January 5, 2020 40 | ========================= 41 | * Set Ruby 2.4.1 to be the minimum allowed version (Samuel Williams) 42 | * Fix incorrect use of context, reduce warnings when running tests (Samuel Williams) 43 | * Fix `bundle exec rake test` (Samuel Williams) 44 | * Add 2.4.1 to the travis test matrix (Samuel Williams) 45 | 46 | 2.0.0 - December 30, 2019 47 | ========================= 48 | - Full rewrite to support API changes in Proj versions 5 and 6 - Charlie Savage) 49 | - As part of rewrite switch bindings to use FFI versus a C extension (Charlie Savage) 50 | - Split Ruby code into multiple files based on classes (Charlie Savage) 51 | - Add in a bunch of new classes including Context, Crs, Coordinate, Ellipsoid, Prime Meridian and Transform (Charlie Savage) 52 | - Deprecate Projection and Point - these will stop working with Proj 7 since the use an older deprecated API (Charlie Savage) 53 | 54 | 1.0.0 - December 14, 2014 55 | ========================= 56 | - Calling this 1.0.0 since its a very stable gem (Charlie Savage) 57 | 58 | 0.4.3 - August 30, 2011 59 | ========================= 60 | - Remove reference to now private projects.h header 61 | 62 | 0.4.2 - August 15, 2011 63 | ========================= 64 | - Minor build tweak to support MSVC++ 65 | 66 | 0.4.1 - July 30, 2011 67 | ========================= 68 | - Search first for binaries when using windows gems 69 | - Add # encoding to test files 70 | - Reformat tests files to use standard ruby 2 space indenting 71 | 72 | 0.4.0 - July 30, 2011 73 | ========================= 74 | - Update to compile on Ruby 1.9.* (Fabio Renzo Panettieri) 75 | - Add in gemspec file (Charlie Savage) 76 | - Add rake-compiler as development dependency, remove older MinGW build system (Charlie Savage) 77 | - Move to GitHub (Charlie Savage) 78 | 79 | 0.3.1 - December 23, 2009 80 | ========================= 81 | - Update extconf.conf file to be more flexible to make it easier to build 82 | on OS X when using MacPorts 83 | - Updated windows binary to link against proj4.7 84 | 85 | 0.3.0 - August 14, 2008 86 | ========================= 87 | - Removed Proj4::UV class which was previously deprecated 88 | - New build infrastructure for Windows (Charlie Savage) 89 | - Fixed memory leaks in forward() and inverse() methods (Charlie Savage) -------------------------------------------------------------------------------- /Examples.rdoc: -------------------------------------------------------------------------------- 1 | = Examples 2 | 3 | == Conversion from Geodetic to Projected Coordinates 4 | This example is ported from Proj's quickstart guide. See https://proj.org/development/quickstart.html 5 | 6 | require 'bundler/setup' 7 | require 'proj' 8 | 9 | # Create a context 10 | context = Proj::Context.new 11 | 12 | # Create a projection 13 | crs = Proj::Crs.new("+proj=utm +zone=32 +datum=WGS84 +type=crs", context) 14 | 15 | # Get the geodetic CRS for that projection 16 | geodetic_crs = crs.geodetic_crs 17 | puts geodetic_crs.to_proj_string 18 | # +proj=longlat +datum=WGS84 +no_defs +type=crs 19 | 20 | # Create a transformation from the geodetic to projected coordinates 21 | transformation = Proj::Transformation.new(geodetic_crs, crs, context) 22 | puts transformation.to_proj_string 23 | # +proj=pipeline +step +proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=utm +zone=32 +ellps=WGS84 24 | 25 | # Create a coordinate for Copenhagen in degrees 26 | coordinate_geodetic = Proj::Coordinate.new(lon: 12.0, lat: 55.0) 27 | puts "lon: #{coordinate_geodetic.lon}, lat: #{coordinate_geodetic.lat}" 28 | 29 | # Transform the coordinate 30 | coordinate_projected = transformation.forward(coordinate_geodetic) 31 | # lon: 12.0, lat: 55.0 32 | 33 | puts "east: #{coordinate_projected.e}, north: #{coordinate_projected.n}" 34 | # east: 691875.632137542, north: 6098907.825129169 35 | 36 | puts "x: #{coordinate_projected.x}, y: #{coordinate_projected.y}" 37 | # x: 691875.632137542, y: 6098907.825129169 38 | 39 | # Apply the inverse transform 40 | coordinate_inverse = transformation.inverse(coordinate_projected) 41 | 42 | puts "lon: #{coordinate_inverse.lon}, lat: #{coordinate_inverse.lat}" 43 | # lon: 12.0, lat: 55.0 44 | 45 | puts coordinate_geodetic == coordinate_inverse 46 | # true 47 | 48 | == Pipeline Operator 49 | This example is ported from the Rust Proj documentation (see https://github.com/georust/proj#convert-from-nad-83-us-survey-feet-to-nad-83-meters-using-the-pipeline-operator 50 | 51 | The pipeline operator makes it easy to create complex operations by daisy-chaining operations together. For more information refer to https://proj.org/operations/pipeline.html. 52 | 53 | In this example, a coordinate from NAD 83 US Survey Feet to NAD 83 Meters. It has two steps: 54 | 55 | * Step 1 as an inverse transform, yielding geodetic coordinates 56 | * Step 2 as a forward transform to projected coordinates, yielding metres 57 | 58 | 59 | conversion = Proj::Conversion.new(<<~EOS) 60 | +proj=pipeline 61 | +step +inv +proj=lcc +lat_1=33.88333333333333 62 | +lat_2=32.78333333333333 +lat_0=32.16666666666666 63 | +lon_0=-116.25 +x_0=2000000.0001016 +y_0=500000.0001016001 +ellps=GRS80 64 | +towgs84=0,0,0,0,0,0,0 +units=us-ft +no_defs 65 | +step +proj=lcc +lat_1=33.88333333333333 +lat_2=32.78333333333333 +lat_0=32.16666666666666 66 | +lon_0=-116.25 +x_0=2000000 +y_0=500000 67 | +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs 68 | EOS 69 | 70 | # The Presidio, approximately 71 | coordinate_1 = Proj::Coordinate.new(x: 4760096.421921, y: 3744293.729449) 72 | coordinate_2 = conversion.forward(coordinate_1) 73 | 74 | assert_in_delta(1450880.2910605003, coordinate_2.x) 75 | assert_in_delta(1141263.01116045, coordinate_2.y) 76 | 77 | == Operation Factory Context 78 | Operation Factory Contexts are used to build coordinate operations between two CRSes. This examples finds the best available conversion between EPSG 4267 and 4269. 79 | 80 | source = Proj::Crs.create_from_database("EPSG", "4267", :PJ_CATEGORY_CRS) 81 | target = Proj::Crs.create_from_database("EPSG", "4269", :PJ_CATEGORY_CRS) 82 | 83 | factory_context = Proj::OperationFactoryContext.new 84 | factory_context.spatial_criterion = :PROJ_SPATIAL_CRITERION_PARTIAL_INTERSECTION 85 | factory_context.grid_availability = :PROJ_GRID_AVAILABILITY_IGNORED 86 | 87 | operations = factory_context.create_operations(source, target) 88 | 89 | coord = Proj::Coordinate.new(x: 40, y: -100) 90 | index = operations.suggested_operation(:PJ_FWD, coord) 91 | assert_equal(2, index) 92 | 93 | operation = operations[index] 94 | assert_equal("NAD27 to NAD83 (1)", operation.name) 95 | 96 | Operation Factory Contexts have many additional attributes that can be set to control how conversions should be constructed. -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Specify your gem's dependencies in tensorflow-ruby.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2000 Frank Warmerdam 2 | 3 | Copyright (c) 2006 Guilhem Vellut 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = Proj4rb 2 | This gem provides Ruby bindings for the Proj Library (https://proj.org). The Proj Library supports converting coordinates between a number of different coordinate systems and projections. Note the Proj library used to be known as Proj4. 3 | 4 | The bindings support Proj version 4 through the current version (9.3.1). The Proj library and API were completelely written during this time. To support all these versions, the gem dynamically loads code based on the installed Proj version. 5 | 6 | == Documentation 7 | Reference documentation is available at https://rubydoc.info/github/cfis/proj4rb. 8 | 9 | Examples can be found in this README file as well as in the Examples file. In addition, the test suite has examples of calling almost every API so when in doubt take a look at them! 10 | 11 | == Installation 12 | First install the gem: 13 | 14 | gem install proj4rb 15 | 16 | Next install the Proj Library. This of course varies per system, but you want to install the latest version Proj possible. Once installed, you'll need to make sure that libproj is installed on your operating system's load path. 17 | 18 | == Usage 19 | To get started first require the gem: 20 | 21 | require 'proj' 22 | 23 | If you are using the old Proj4 namespace, then you can do this: 24 | 25 | require 'proj4' 26 | 27 | === CRS 28 | To create a coordinate system, you can use CRS codes, well-known text (WKT) strings or old-style Proj strings (which are deprecated). 29 | 30 | crs1 = Proj::Crs.new('EPSG:4326') 31 | 32 | crs2 = Proj::Crs.new('urn:ogc:def:crs:EPSG::4326') 33 | 34 | crs3 = Proj::Crs.new('+proj=longlat +datum=WGS84 +no_defs +type=crs') 35 | 36 | crs4 = Proj::Crs.new(<<~EOS) 37 | GEOGCRS["WGS 84", 38 | DATUM["World Geodetic System 1984", 39 | ELLIPSOID["WGS 84",6378137,298.257223563, 40 | LENGTHUNIT["metre",1]]], 41 | PRIMEM["Greenwich",0, 42 | ANGLEUNIT["degree",0.0174532925199433]], 43 | CS[ellipsoidal,2], 44 | AXIS["geodetic latitude (Lat)",north, 45 | ORDER[1], 46 | ANGLEUNIT["degree",0.0174532925199433]], 47 | AXIS["geodetic longitude (Lon)",east, 48 | ORDER[2], 49 | ANGLEUNIT["degree",0.0174532925199433]], 50 | USAGE[ 51 | SCOPE["unknown"], 52 | AREA["World"], 53 | BBOX[-90,-180,90,180]], 54 | ID["EPSG",4326]] 55 | EOS 56 | 57 | Notice when using the old-style Proj4 string, the addition of the "+type=crs" value. 58 | 59 | If you are using Proj 5 or newer, then you should create a transformation[#tranformation] using epsg strings. If you are using Proj 4, you need to use the deprecated Projection class (see documentation). 60 | 61 | === Transformation 62 | After you have created two coordinate systems, you can then create a transformation. For example, if you want to convert coordinates from the `3-degree Gauss-Kruger zone 3` coordinate system to `WGS84` (one version of lat-long) first create a transformation: 63 | 64 | crs_gk = Proj::Crs.new('EPSG:31467') 65 | crs_wgs84 = Proj::Crs.new('EPSG:4326') 66 | transform = Proj::Transformation.new(crs_gk, crs_wgs84) 67 | 68 | Alternatively, or if you are using Proj 5 or later, you can create a transformation without first 69 | creating Crs instances. Instead, pass the EPSG information directly to the transformation: 70 | 71 | transform = Proj::Transformation.new('EPSG:31467', 'EPSG:4326') 72 | 73 | Once you've created the transformation, you can tranform coordinates using either the +forward+ or +inverse+ methods. The forward transformation looks like this: 74 | 75 | from = Proj::Coordinate.new(x: 5428192.0, y: 3458305.0, z: -5.1790915237) 76 | to = transform.forward(from) 77 | 78 | assert_in_delta(48.98963932450735, to.x, 0.01) 79 | assert_in_delta(8.429263044355544, to.y, 0.01) 80 | assert_in_delta(-5.1790915237, to.z, 0.01) 81 | assert_in_delta(0, to.t, 0.01) 82 | 83 | While the inverse transformation looks like this: 84 | 85 | from = Proj::Coordinate.new(lam: 48.9906726079, phi: 8.4302123334) 86 | to = transform.inverse(from) 87 | 88 | assert_in_delta(5428306.389495558, to.x, 0.01) 89 | assert_in_delta(3458375.3367194114, to.y, 0.01) 90 | assert_in_delta(0, to.z, 0.01) 91 | assert_in_delta(0, to.t, 0.01) 92 | 93 | === Coordinate Operations 94 | Transformations are a type of Coordinate Operation. PROJ divides coordinate operations into three groups: 95 | 96 | * Conversions 97 | * Projections 98 | * Transformations 99 | 100 | Conversions are coordinate operations that do not exert a change in reference frame. The Ruby bindings support these via the Conversion class. See https://proj.org/operations/conversions/index.html for more information. 101 | 102 | Projections are cartographic mappings of a sphere onto a plane. Technically projections are conversions (according to ISO standards), but PROJ distinguishes them from conversions. The Ruby bindings support these via the Projection module which has methods to create many common projections. A list can be found at https://proj.org/operations/projections/index.html. 103 | 104 | Transformations are coordinate operations that do cause a change in reference frames. The Ruby bindings support these via the Transformation class. 105 | 106 | For more information see https://proj.org/operations/index.html 107 | 108 | === Operation Factory 109 | The `OperationFactoryContext` class can be used to build coordinate operations between two CRSes. This is done by first creating a factory and setting appropiate filters. These include spatial filters, accuracy filters, grid availability filters, etc. Once filters are set, then the factory can be queried for a list of possible conversions. For examples, please see the operation_factory_context_test.rb file. 110 | 111 | === Coordinate 112 | Notice the examples above transform Coordinate objects. A Coordinate consists of up to four double values to represent three directions plus time. In general you will need to fill in at least the first two values: 113 | 114 | from = Proj::Coordinate.new(x: 5428192.0, y: 3458305.0, z: -5.1790915237) 115 | from = Proj::Coordinate.new(lam: 48.9906726079, phi: 8.4302123334) 116 | 117 | Lam is longitude and phi is latitude. 118 | 119 | === Axis Order 120 | By default, tranformations accept coordinates expressed in the units and axis order of the source CRS and return transformed coordinates in the units and axis order of the target CRS. 121 | 122 | For most geographic CRSes, the units will be in degrees. For geographic CRSes defined by the EPSG authority, the order of coordinates is latitude and then longitude. 123 | 124 | For projected CRSes, the units will vary (metre, us-foot, etc.). For projected CRSes defined by the EPSG authority, and with EAST / NORTH directions, the order might may be east and then north or north and then east. 125 | 126 | If you prefer to work with a uniform axis order, regardless of the axis orders mandated by the source and target CRSes, then call the Context#normalize_for_visualization method: 127 | 128 | normalized = transform.normalize_for_visualization 129 | 130 | The normalized transformation will return output coordinates in longitude, latitude order for geographic CRSes and easting, northing for most projected CRSes. 131 | 132 | For more information see https://proj.org/faq.html#why-is-the-axis-ordering-in-proj-not-consistent. 133 | 134 | === Context 135 | Contexts are used to support multi-threaded programs. The bindings expose this object via Context.current and store it using thread local storage. Use the context object to access error codes, set proj4 compatability settings, set the logging level and to install custom logging code. 136 | 137 | Both Crs and Transformation objects take a context object in their constructors. If none is passed, they default to using Context.current 138 | 139 | == Network Access 140 | Proj supports downloading grid files on demand if network access is enabled (it is disabled by default). To enable network use the method `Context#network_enabled=`. To specify the url endpoint use `Context#url=`. Advanced users can replace Proj's networking code, which uses libcurl, with their own implementation. To do this see the `NetworkApi` class. 141 | 142 | Downloaded grids are cached in a sqlite file named cache.db. To specify the location, size and other characteristics of the cache file refer to the `GridCache` class which is accessible via `Context#cache`. By default the cache size is 300MB. Caching is on by default but can be disabled via `GridCache#enabled=`. 143 | 144 | For more information see the proj networking[https://proj.org/en/latest/usage/network.html#how-to-enable-network-capabilities] documentation. 145 | 146 | == Error handling 147 | When an error occurs, a `Proj::Error` instance will be thrown with the underlying message provided from the Proj library. 148 | 149 | == Finding Proj Library (PROJ_LIB_PATH) 150 | proj4rb will search in a number of well-known locations for the libproj shared library. You can override this by specifying the full path to the library using the `PROJ_LIB_PATH` environmental variable. 151 | 152 | == Finding Proj Files (PROJ_DATA) 153 | Starting with version 6, Proj stores its information (datums, ellipsoids, prime meridians, coordinate systems, units, etc) in a sqlite file called proj.db. If Proj cannot find its database an exception will be raised. In this case, you can set the environmental variable `PROJ_DATA` to point to the folder that contains the proj.db file. Note PROJ_LIB must be set *before* Ruby is launched. Ruby itself cannot set this variable and have it work correctly (at least not on windows). 154 | 155 | For more information see https://proj.org/resource_files.html 156 | 157 | == Class Hierarchy 158 | The proj4rb class hierarchy is based on Proj's class hiearchy which, in turn, is derived from http://docs.opengeospatial.org/as/18-005r5/18-005r5.html. It is: 159 | 160 | PjObject 161 | CoordinateOperationMixin 162 | Conversion 163 | Transformation 164 | CoordinateMetadata 165 | CoordinateSystem 166 | Crs 167 | Datum 168 | DatumEnsemble 169 | Ellipsoid 170 | PrimeMerdian 171 | 172 | The PjObject class defines several methods to create new objects: 173 | 174 | * PjObject.create 175 | * PjObject.create_from_database 176 | * PjObject.create_from_name 177 | * PjObject.create_from_wkt 178 | 179 | The methods will return instances of the correct subclass. 180 | 181 | == Tests 182 | Proj4rb ships with a full test suite designed to work using Proj 6 and higher. If you are using an earlier version of Proj, then expect *many* test failures. 183 | 184 | == License 185 | Proj4rb is released under the MIT license. 186 | 187 | == Authors 188 | The proj4rb Ruby bindings were started by Guilhem Vellut with most of the code written by Jochen Topf. Charlie Savage ported the code to Windows and added 189 | the Windows build infrastructure. Later, he rewrote the code to support Proj version 5, 6, 7, 8 and 9 and ported it to use FFI. 190 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "rubygems" 4 | require "rake/testtask" 5 | require "rubygems/package_task" 6 | require "yard" 7 | require "yaml" 8 | 9 | # Read the spec file 10 | GEM_NAME = "proj4rb" 11 | spec = Gem::Specification.load("#{GEM_NAME}.gemspec") 12 | 13 | # Setup generic gem 14 | Gem::PackageTask.new(spec) do |pkg| 15 | pkg.package_dir = 'pkg' 16 | pkg.need_tar = false 17 | end 18 | 19 | # Yard Task 20 | desc "Generate documentation" 21 | YARD::Rake::YardocTask.new 22 | 23 | # Test Task 24 | Rake::TestTask.new do |t| 25 | t.libs << "test" 26 | t.test_files = FileList['test/*_test.rb'] 27 | t.verbose = true 28 | end -------------------------------------------------------------------------------- /lib/api/api.rb: -------------------------------------------------------------------------------- 1 | require 'rbconfig' 2 | require 'ffi' 3 | 4 | module Proj 5 | module Api 6 | extend FFI::Library 7 | 8 | # List of knows PROJ library versions 9 | # 10 | # @return [Array] 11 | def self.library_versions 12 | ["25", # 9.2 13 | "9_1", # 9.1 14 | "22", # 8.0 and 8.1 15 | "19", # 7.x 16 | "17", # 6.1 *and* 6.2 17 | "15", # 6.0 18 | "14", # 5.2 19 | "13", # 5.0 20 | "12", # 4.9 21 | "11"] # 4.9 22 | end 23 | 24 | # Search paths to use when looking for PROJ library 25 | # 26 | # @return [Array] 27 | def self.search_paths 28 | result = case RbConfig::CONFIG['host_os'] 29 | when /mswin|msys|mingw|cygwin|bccwin|wince|emc/ 30 | self.windows_search_paths 31 | when /darwin|mac os/ 32 | self.macos_search_paths 33 | else 34 | self.linux_search_paths 35 | end 36 | 37 | # Try libproj as catch all 38 | result << 'libproj' 39 | result 40 | end 41 | 42 | # Windows search paths for PROJ library 43 | # 44 | # @return [Array] 45 | def self.windows_search_paths 46 | self.library_versions.map do |version| 47 | ["libproj-#{version}", "libproj_#{version}"] 48 | end.flatten 49 | end 50 | 51 | # Linux search paths for PROJ library 52 | # 53 | # @return [Array] 54 | def self.linux_search_paths 55 | self.library_versions.map do |version| 56 | "libproj.so.#{version}" 57 | end 58 | end 59 | 60 | # MacOS search paths for PROJ library 61 | # 62 | # @return [Array] 63 | def self.macos_search_paths 64 | # On MacOS only support HomeBrew since the MacPort is unsupported and ancient (5.2). 65 | self.library_versions.map do |version| 66 | "libproj.#{version}.dylib" 67 | end 68 | end 69 | 70 | # Load PROJ library 71 | # 72 | # @return [FFI::DynamicLibrary] 73 | def self.load_library 74 | if ENV["PROJ_LIB_PATH"] 75 | ffi_lib ENV["PROJ_LIB_PATH"] 76 | else 77 | ffi_lib self.search_paths 78 | end 79 | 80 | ffi_libraries.first 81 | end 82 | 83 | # Load API files based on PROJ version 84 | # 85 | # @return [nil] 86 | def self.load_api 87 | # First load the base 5.0 api so we can determine the Proj Version 88 | require_relative './api_5_0' 89 | Api.const_set('PROJ_VERSION', Gem::Version.new(self.proj_info[:version])) 90 | 91 | # Now load the rest of the apis based on the proj version 92 | versions = ['5.1.0', '5.2.0', 93 | '6.0.0', '6.1.0', '6.2.0', '6.3.0', 94 | '7.0.0', '7.1.0', '7.2.0', 95 | '8.0.0', '8.1.0', '8.2.0', 96 | '9.1.0', '9.2.0', '9.4.0'] 97 | 98 | versions.each do |version| 99 | api_version = Gem::Version.new(version) 100 | 101 | if PROJ_VERSION >= api_version 102 | require_relative "./api_#{api_version.segments[0]}_#{api_version.segments[1]}" 103 | end 104 | end 105 | 106 | # Add in the experimental api 107 | require_relative "./api_experimental" 108 | end 109 | 110 | # Load the library 111 | load_library 112 | 113 | # Load the api 114 | load_api 115 | end 116 | end 117 | 118 | -------------------------------------------------------------------------------- /lib/api/api_5_1.rb: -------------------------------------------------------------------------------- 1 | module Proj 2 | module Api 3 | attach_function :proj_log_level, [:PJ_CONTEXT, PJ_LOG_LEVEL], PJ_LOG_LEVEL 4 | callback :pj_log_function, [:pointer, :int, :string], :void 5 | attach_function :proj_log_func, [:PJ_CONTEXT, :pointer, :pj_log_function], :void 6 | end 7 | end -------------------------------------------------------------------------------- /lib/api/api_5_2.rb: -------------------------------------------------------------------------------- 1 | module Proj 2 | module Api 3 | attach_function :proj_errno_string, [:int], :string 4 | end 5 | end -------------------------------------------------------------------------------- /lib/api/api_6_0.rb: -------------------------------------------------------------------------------- 1 | module Proj 2 | module Api 3 | # Comparison criteria 4 | # @return [Symbol] 5 | PJ_COMPARISON_CRITERION = enum(:PJ_COMP_STRICT, # All properties are identical 6 | :PJ_COMP_EQUIVALENT, # The objects are equivalent for the purpose of coordinate operations. They can differ by the name of their objects, identifiers, other metadata. Parameters may be expressed in different units, provided that the value is (with some tolerance) the same once expressed in a common unit. 7 | :PJ_COMP_EQUIVALENT_EXCEPT_AXIS_ORDER_GEOGCRS) # Same as EQUIVALENT, relaxed with an exception that the axis order of the base CRS of a DerivedCRS/ProjectedCRS or the axis order of a GeographicCRS is ignored. Only to be used with DerivedCRS/ProjectedCRS/GeographicCRS 8 | 9 | #Guessed WKT "dialect" 10 | # @return [Symbol] 11 | PJ_GUESSED_WKT_DIALECT = enum(:PJ_GUESSED_WKT2_2019, 12 | :PJ_GUESSED_WKT2_2018, 13 | :PJ_GUESSED_WKT2_2015, 14 | :PJ_GUESSED_WKT1_GDAL, 15 | :PJ_GUESSED_WKT1_ESRI, 16 | :PJ_GUESSED_NOT_WKT) 17 | 18 | # Base methods 19 | attach_function :proj_clone, [:PJ_CONTEXT, :PJ], :PJ 20 | attach_function :proj_get_name, [:PJ], :string 21 | attach_function :proj_get_id_auth_name, [:PJ, :int], :string 22 | attach_function :proj_get_id_code, [:PJ, :int], :string 23 | attach_function :proj_get_remarks, [:PJ], :string 24 | attach_function :proj_get_scope, [:PJ], :string 25 | attach_function :proj_get_type, [:PJ], PJ_TYPE 26 | attach_function :proj_is_crs, [:PJ], :bool 27 | attach_function :proj_is_equivalent_to, [:PJ, :PJ, PJ_COMPARISON_CRITERION], :int 28 | attach_function :proj_is_deprecated, [:PJ], :int 29 | attach_function :proj_get_source_crs, [:PJ_CONTEXT, :PJ], :PJ 30 | attach_function :proj_get_target_crs, [:PJ_CONTEXT, :PJ], :PJ 31 | 32 | # Area 33 | attach_function :proj_area_create, [], :PJ_AREA 34 | attach_function :proj_area_set_bbox, [:PJ_AREA, :double, :double, :double, :double], :void 35 | attach_function :proj_get_area_of_use, [:PJ_CONTEXT, :PJ, :pointer, :pointer, :pointer, :pointer, :pointer], :bool 36 | attach_function :proj_area_destroy, [:PJ_AREA], :void 37 | 38 | # Export to various formats 39 | attach_function :proj_as_wkt, [:PJ_CONTEXT, :PJ, PJ_WKT_TYPE, :pointer], :string 40 | attach_function :proj_as_proj_string, [:PJ_CONTEXT, :PJ, PJ_PROJ_STRING_TYPE, :pointer], :string 41 | 42 | # String List 43 | typedef :pointer, :PROJ_STRING_LIST 44 | attach_function :proj_string_list_destroy, [:PROJ_STRING_LIST], :void 45 | 46 | # ----- Object List 47 | typedef :pointer, :PJ_OBJ_LIST 48 | 49 | attach_function :proj_create_from_name, [:PJ_CONTEXT, :string, :string, :pointer, :size_t, :int, :size_t, :string], :PJ_OBJ_LIST 50 | attach_function :proj_get_non_deprecated, [:PJ_CONTEXT, :PJ], :PJ_OBJ_LIST 51 | attach_function :proj_identify, [:PJ_CONTEXT, :PJ, :string, :pointer, :pointer], :PJ_OBJ_LIST 52 | attach_function :proj_list_get_count, [:PJ_OBJ_LIST], :int 53 | attach_function :proj_list_get, [:PJ_CONTEXT, :PJ_OBJ_LIST, :int], :PJ 54 | attach_function :proj_list_destroy, [:PJ_OBJ_LIST], :void 55 | 56 | callback :proj_file_finder, [:PJ_CONTEXT, :string, :pointer], :pointer 57 | attach_function :proj_context_set_file_finder, [:PJ_CONTEXT, :proj_file_finder, :pointer], :void 58 | attach_function :proj_context_set_search_paths, [:PJ_CONTEXT, :int, :pointer], :void 59 | 60 | attach_function :proj_list_angular_units, [], :pointer #PJ_UNITS 61 | 62 | # Contains description of a CRS 63 | class PROJ_CRS_INFO < FFI::Struct 64 | fields = [:auth_name, :string, # Authority name 65 | :code, :string, # Object code 66 | :name, :string, # Object name 67 | :type, PJ_TYPE, # Object type 68 | :deprecated, :int, # Whether the object is deprecated 69 | :bbox_valid, :int, # Whether bbox values in degrees are valid 70 | :west_lon_degree, :double, # Western-most longitude of the area of use, in degrees. 71 | :south_lat_degree, :double, # Southern-most latitude of the area of use, in degrees. 72 | :east_lon_degree, :double, # Eastern-most longitude of the area of use, in degrees. 73 | :north_lat_degree, :double, # Northern-most latitude of the area of use, in degrees. 74 | :area_name, :string, # Name of the area of use 75 | :projection_method_name, :string] # Name of the projection method for a projected CRS. Might be NULL even for projected CRS in some cases. 76 | 77 | if Api::PROJ_VERSION >= Gem::Version.new('8.1.0') 78 | fields += [:celestial_body_name, :string] # Name of the celestial body of the CRS (e.g. "Earth") 79 | end 80 | layout(*fields) 81 | end 82 | 83 | attach_function :proj_crs_info_list_destroy, [:pointer], :void 84 | 85 | # Structure describing optional parameters for proj_get_crs_list 86 | class PROJ_CRS_LIST_PARAMETERS < FFI::Struct 87 | fields = [:types, :pointer, # Array of allowed object types. Should be nil if all types are allowed 88 | :types_count, :size_t, # Size of types. Should be 0 if all types are allowed 89 | :crs_area_of_use_contains_bbox, :int, # If TRUE and bbox_valid == TRUE, then only CRS whose area of use entirely contains the specified bounding box will be returned. If FALSE and bbox_valid == TRUE, then only CRS whose area of use intersects the specified bounding box will be returned 90 | :bbox_valid, :int, # To set to TRUE so that west_lon_degree, south_lat_degree, east_lon_degree and north_lat_degree fields are taken into account 91 | :west_lon_degree, :double, # Western-most longitude of the area of use, in degrees. 92 | :south_lat_degree, :double, # Southern-most latitude of the area of use, in degrees. 93 | :east_lon_degree, :double, # Eastern-most longitude of the area of use, in degrees. 94 | :north_lat_degree, :double,# Northern-most latitude of the area of use, in degrees. 95 | :allow_deprecated, :int] # Whether deprecated objects are allowed. Default to False 96 | 97 | if Api::PROJ_VERSION >= Gem::Version.new('8.1.0') 98 | fields += [:celestial_body_name, :pointer] #Name of the celestial body of the CRS (e.g. "Earth") 99 | end 100 | layout(*fields) 101 | end 102 | 103 | attach_function :proj_get_crs_list_parameters_create, [], :pointer 104 | attach_function :proj_get_crs_list_parameters_destroy, [:pointer], :void 105 | 106 | # Database functions 107 | attach_function :proj_context_set_database_path, [:PJ_CONTEXT, :string, :pointer, :pointer], :int 108 | attach_function :proj_context_get_database_path, [:PJ_CONTEXT], :string 109 | attach_function :proj_context_get_database_metadata, [:PJ_CONTEXT, :string], :string 110 | attach_function :proj_get_authorities_from_database, [:PJ_CONTEXT], :PROJ_STRING_LIST 111 | attach_function :proj_get_codes_from_database, [:PJ_CONTEXT, :string, PJ_TYPE, :int], :PROJ_STRING_LIST 112 | attach_function :proj_get_crs_info_list_from_database, [:PJ_CONTEXT, :string, PROJ_CRS_LIST_PARAMETERS, :pointer], PROJ_CRS_INFO 113 | attach_function :proj_uom_get_info_from_database, [:PJ_CONTEXT, :string, :string, :pointer, :pointer, :pointer], :int 114 | 115 | # CRS methods 116 | attach_function :proj_crs_get_geodetic_crs, [:PJ_CONTEXT, :PJ], :PJ 117 | attach_function :proj_crs_get_horizontal_datum, [:PJ_CONTEXT, :PJ], :PJ 118 | attach_function :proj_crs_get_sub_crs, [:PJ_CONTEXT, :PJ, :int], :PJ 119 | attach_function :proj_crs_get_datum, [:PJ_CONTEXT, :PJ], :PJ 120 | attach_function :proj_crs_get_coordinate_system, [:PJ_CONTEXT, :PJ], :PJ 121 | attach_function :proj_cs_get_type, [:PJ_CONTEXT, :PJ], PJ_COORDINATE_SYSTEM_TYPE 122 | attach_function :proj_cs_get_axis_count, [:PJ_CONTEXT, :PJ], :int 123 | attach_function :proj_cs_get_axis_info, [:PJ_CONTEXT, :PJ, :int, :pointer, :pointer, :pointer, :pointer, :pointer, :pointer, :pointer], :bool 124 | attach_function :proj_crs_get_coordoperation, [:PJ_CONTEXT, :PJ], :PJ 125 | attach_function :proj_coordoperation_get_accuracy, [:PJ_CONTEXT, :PJ], :double 126 | attach_function :proj_coordoperation_get_method_info, [:PJ_CONTEXT, :PJ, :pointer, :pointer, :pointer], :int 127 | attach_function :proj_coordoperation_get_towgs84_values, [:PJ_CONTEXT, :PJ, :pointer, :int, :int], :int 128 | attach_function :proj_concatoperation_get_step_count, [:PJ_CONTEXT, :PJ], :int 129 | attach_function :proj_concatoperation_get_step, [:PJ_CONTEXT, :PJ, :int], :PJ 130 | 131 | attach_function :proj_get_ellipsoid, [:PJ_CONTEXT, :PJ], :PJ 132 | attach_function :proj_ellipsoid_get_parameters, [:PJ_CONTEXT, :PJ, :pointer, :pointer, :pointer, :pointer], :int 133 | 134 | attach_function :proj_get_prime_meridian, [:PJ_CONTEXT, :PJ], :PJ 135 | attach_function :proj_prime_meridian_get_parameters, [:PJ_CONTEXT, :PJ, :pointer, :pointer, :pointer], :int 136 | 137 | # ISO-19111 138 | attach_function :proj_create_from_wkt, [:PJ_CONTEXT, :string, :pointer, :PROJ_STRING_LIST, :PROJ_STRING_LIST], :PJ 139 | attach_function :proj_create_from_database, [:PJ_CONTEXT, :string, :string, PJ_CATEGORY, :int, :pointer], :PJ 140 | attach_function :proj_context_guess_wkt_dialect, [:PJ_CONTEXT, :string], PJ_GUESSED_WKT_DIALECT 141 | 142 | # Undocumented apis 143 | attach_function :proj_context_use_proj4_init_rules, [:PJ_CONTEXT, :int], :void 144 | attach_function :proj_context_get_use_proj4_init_rules, [:PJ_CONTEXT, :int], :int 145 | end 146 | end -------------------------------------------------------------------------------- /lib/api/api_6_1.rb: -------------------------------------------------------------------------------- 1 | module Proj 2 | module Api 3 | attach_function :proj_normalize_for_visualization, [:PJ_CONTEXT, :PJ], :PJ 4 | end 5 | end -------------------------------------------------------------------------------- /lib/api/api_6_2.rb: -------------------------------------------------------------------------------- 1 | module Proj 2 | module Api 3 | attach_function :proj_cleanup, [], :void 4 | attach_function :proj_as_projjson, [:PJ_CONTEXT, :PJ, :pointer], :string 5 | attach_function :proj_create_crs_to_crs_from_pj, [:PJ_CONTEXT, :PJ, :PJ, :PJ_AREA, :pointer], :PJ 6 | attach_function :proj_grid_get_info_from_database, [:PJ_CONTEXT, :string, :pointer, :pointer, :pointer, :pointer, :pointer, :pointer], :int 7 | attach_function :proj_context_set_autoclose_database, [:PJ_CONTEXT, :int], :void 8 | attach_function :proj_operation_factory_context_set_discard_superseded, [:PJ_CONTEXT, :pointer, :int], :void 9 | end 10 | end -------------------------------------------------------------------------------- /lib/api/api_6_3.rb: -------------------------------------------------------------------------------- 1 | module Proj 2 | module Api 3 | attach_function :proj_is_equivalent_to_with_ctx, [:PJ_CONTEXT, :PJ, :PJ, PJ_COMPARISON_CRITERION], :int 4 | attach_function :proj_coordoperation_create_inverse, [:PJ_CONTEXT, :PJ], :PJ 5 | end 6 | end -------------------------------------------------------------------------------- /lib/api/api_7_0.rb: -------------------------------------------------------------------------------- 1 | module Proj 2 | module Api 3 | # ---- File API ---------- 4 | typedef :pointer, :PROJ_FILE_HANDLE 5 | typedef :pointer, :USER_DATA 6 | 7 | # @return [Symbol] 8 | PROJ_OPEN_ACCESS = enum(:PROJ_OPEN_ACCESS_READ_ONLY, # Read-only access. Equivalent to "rb" 9 | :PROJ_OPEN_ACCESS_READ_UPDATE, # Read-update access. File should be created if not existing. Equivalent to "r+b 10 | :PROJ_OPEN_ACCESS_CREATE) # Create access. File should be truncated to 0-byte if already existing. Equivalent to "w+b" 11 | 12 | # File API callbacks 13 | callback :open_file_cbk, [:PJ_CONTEXT, :string, PROJ_OPEN_ACCESS, :USER_DATA], :PROJ_FILE_HANDLE 14 | callback :read_file_cbk, [:PJ_CONTEXT, :PROJ_FILE_HANDLE, :pointer, :size_t, :USER_DATA], :size_t 15 | callback :write_file_cbk, [:PJ_CONTEXT, :PROJ_FILE_HANDLE, :pointer, :size_t, :USER_DATA], :size_t 16 | callback :seek_file_cbk, [:PJ_CONTEXT, :PROJ_FILE_HANDLE, :long_long, :int, :USER_DATA], :int 17 | callback :tell_file_cbk, [:PJ_CONTEXT, :PROJ_FILE_HANDLE, :USER_DATA], :long_long 18 | callback :close_file_cbk, [:PJ_CONTEXT, :PROJ_FILE_HANDLE, :USER_DATA], :void 19 | callback :exists_file_cbk, [:PJ_CONTEXT, :string, :USER_DATA], :int 20 | callback :mkdir_file_cbk, [:PJ_CONTEXT, :string, :USER_DATA], :int 21 | callback :unlink_file_cbk, [:PJ_CONTEXT, :string, :USER_DATA], :int 22 | callback :rename_file_cbk, [:PJ_CONTEXT, :string, :string, :USER_DATA], :int 23 | 24 | # Progress callback. The passed percentage is in the range [0, 1]. The progress callback must return 25 | # TRUE if the download should be continued. 26 | callback :progress_file_cbk, [:PJ_CONTEXT, :double, :USER_DATA], :int 27 | 28 | class PROJ_FILE_API < FFI::Struct 29 | layout :version, :int, # Version of this structure. Should be set to 1 currently. 30 | :open_cbk, :open_file_cbk, 31 | :read_cbk, :read_file_cbk, 32 | :write_cbk, :write_file_cbk, 33 | :seek_cbk, :seek_file_cbk, 34 | :tell_cbk, :tell_file_cbk, 35 | :close_cbk, :close_file_cbk, 36 | :exists_cbk, :exists_file_cbk, 37 | :mkdir_cbk, :mkdir_file_cbk, 38 | :unlink_cbk, :unlink_file_cbk, 39 | :rename_cbk, :rename_file_cbk 40 | end 41 | attach_function :proj_context_set_fileapi, [:PJ_CONTEXT, PROJ_FILE_API, :USER_DATA], :int 42 | attach_function :proj_is_download_needed, [:PJ_CONTEXT, :string, :int], :int 43 | attach_function :proj_download_file, [:PJ_CONTEXT, :string, :int, :progress_file_cbk, :USER_DATA], :int 44 | 45 | # --------- Network API ------------ 46 | typedef :pointer, :PROJ_NETWORK_HANDLE 47 | callback :open_network_cbk, [:PJ_CONTEXT, :string, :ulong_long, :size_t, :pointer, :pointer, :size_t, :string, :USER_DATA], :PROJ_NETWORK_HANDLE 48 | callback :close_network_cbk, [:PJ_CONTEXT, :PROJ_NETWORK_HANDLE, :USER_DATA], :void 49 | callback :header_value_cbk, [:PJ_CONTEXT, :PROJ_NETWORK_HANDLE, :pointer, :USER_DATA], :pointer 50 | callback :read_range_cbk, [:PJ_CONTEXT, :PROJ_NETWORK_HANDLE, :ulong_long, :size_t, :pointer, :size_t, :string, :USER_DATA], :size_t 51 | attach_function :proj_context_set_network_callbacks, [:PJ_CONTEXT, :open_network_cbk, :close_network_cbk, :header_value_cbk, :read_range_cbk, :pointer], :int 52 | 53 | attach_function :proj_context_is_network_enabled, [:PJ_CONTEXT], :int 54 | attach_function :proj_context_set_enable_network, [:PJ_CONTEXT, :int], :int 55 | attach_function :proj_context_set_url_endpoint, [:PJ_CONTEXT, :string], :void 56 | 57 | # --------- Cache ------------ 58 | attach_function :proj_grid_cache_set_enable, [:PJ_CONTEXT, :int], :void 59 | attach_function :proj_grid_cache_set_filename, [:PJ_CONTEXT, :string], :void 60 | attach_function :proj_grid_cache_set_max_size, [:PJ_CONTEXT, :int], :void 61 | attach_function :proj_grid_cache_set_ttl, [:PJ_CONTEXT, :int], :void 62 | attach_function :proj_grid_cache_clear, [:PJ_CONTEXT], :void 63 | 64 | # -------- Other ---------- 65 | attach_function :proj_assign_context, [:PJ, :PJ_CONTEXT], :void 66 | attach_function :proj_degree_input, [:PJ, PJ_DIRECTION], :int 67 | attach_function :proj_degree_output, [:PJ, PJ_DIRECTION], :int 68 | end 69 | end -------------------------------------------------------------------------------- /lib/api/api_7_1.rb: -------------------------------------------------------------------------------- 1 | module Proj 2 | module Api 3 | # ----- Int List 4 | attach_function :proj_int_list_destroy, [:pointer], :void 5 | 6 | # ---- Units ------ 7 | class PROJ_UNIT_INFO < FFI::Struct 8 | layout :auth_name, :string, 9 | :code, :string, 10 | :name, :string, 11 | :category, :string, 12 | :conv_factor, :double, 13 | :proj_short_name, :string, 14 | :deprecated, :int 15 | end 16 | 17 | attach_function :proj_get_units_from_database, [:PJ_CONTEXT, :string, :string, :int, :pointer], :pointer #Array of pointers to PROJ_UNIT_INFO 18 | attach_function :proj_unit_list_destroy, [:pointer], :void 19 | 20 | # ---- Operation Factories ------ 21 | # Specifies how source and target CRS extent should be used to restrict candidate 22 | # operations (only taken into account if no explicit area of interest is specified. 23 | enum :PROJ_CRS_EXTENT_USE, [:PJ_CRS_EXTENT_NONE, # Ignore CRS extent 24 | :PJ_CRS_EXTENT_BOTH, # Test extent against both CRS extent. 25 | :PJ_CRS_EXTENT_INTERSECTION, # Test extent against the intersection of both CRS extents 26 | :PJ_CRS_EXTENT_SMALLEST] # Test against the smallest of both CRS extent 27 | 28 | 29 | # Spatial criterion to restrict candidate operations 30 | enum :PROJ_SPATIAL_CRITERION, [:PROJ_SPATIAL_CRITERION_STRICT_CONTAINMENT, # The area of validity of transforms should strictly contain the area of interest 31 | :PROJ_SPATIAL_CRITERION_PARTIAL_INTERSECTION] # The area of validity of transforms should at least intersect the area of interest 32 | 33 | # Describe how grid availability is used 34 | enum :PROJ_GRID_AVAILABILITY_USE, [:PROJ_GRID_AVAILABILITY_USE, # Grid availability is only used for sorting results. Operations where some grids are missing will be sorted last 35 | :PROJ_GRID_AVAILABILITY_DISCARD_OPERATION_IF_MISSING_GRID, # Completely discard an operation if a required grid is missing 36 | :PROJ_GRID_AVAILABILITY_IGNORED, # Ignore grid availability at all. Results will be presented as if all grids were available 37 | :PROJ_GRID_AVAILABILITY_KNOWN_AVAILABLE] # Results will be presented as if grids known to PROJ (that is registered in the grid_alternatives table of its database) were available. Used typically when networking is enabled. 38 | 39 | # Describe if and how intermediate CRS should be used 40 | enum :PROJ_INTERMEDIATE_CRS_USE, [:PROJ_INTERMEDIATE_CRS_USE_ALWAYS, # Always search for intermediate CRS 41 | :PROJ_INTERMEDIATE_CRS_USE_IF_NO_DIRECT_TRANSFORMATION, # Only attempt looking for intermediate CRS if there is no direct transformation available 42 | :PROJ_INTERMEDIATE_CRS_USE_NEVER] # Do not attempt looking for intermediate CRS 43 | 44 | attach_function :proj_create_operation_factory_context, [:PJ_CONTEXT, :string], :PJ_OBJ_LIST 45 | attach_function :proj_operation_factory_context_destroy, [:pointer], :void 46 | attach_function :proj_create_operations, [:PJ_CONTEXT, :PJ, :PJ, :pointer], :pointer 47 | attach_function :proj_operation_factory_context_set_allow_ballpark_transformations, [:PJ_CONTEXT, :pointer, :int], :void 48 | attach_function :proj_operation_factory_context_set_desired_accuracy, [:PJ_CONTEXT, :pointer, :double], :void 49 | attach_function :proj_operation_factory_context_set_area_of_interest, [:PJ_CONTEXT, :pointer, :double, :double, :double, :double], :void 50 | attach_function :proj_operation_factory_context_set_crs_extent_use, [:PJ_CONTEXT, :pointer, :PROJ_CRS_EXTENT_USE], :void 51 | attach_function :proj_operation_factory_context_set_spatial_criterion, [:PJ_CONTEXT, :pointer, :PROJ_SPATIAL_CRITERION], :void 52 | attach_function :proj_operation_factory_context_set_grid_availability_use, [:PJ_CONTEXT, :pointer, :PROJ_GRID_AVAILABILITY_USE], :void 53 | attach_function :proj_operation_factory_context_set_use_proj_alternative_grid_names, [:PJ_CONTEXT, :pointer, :int], :void 54 | attach_function :proj_operation_factory_context_set_allow_use_intermediate_crs, [:PJ_CONTEXT, :pointer, :PROJ_INTERMEDIATE_CRS_USE], :void 55 | attach_function :proj_operation_factory_context_set_allowed_intermediate_crs, [:PJ_CONTEXT, :pointer, :pointer], :void 56 | 57 | # Operations 58 | attach_function :proj_coordoperation_has_ballpark_transformation, [:PJ_CONTEXT, :PJ], :int 59 | attach_function :proj_get_suggested_operation, [:PJ_CONTEXT, :PJ_OBJ_LIST, PJ_DIRECTION, PJ_COORD], :int 60 | attach_function :proj_coordoperation_get_param_count, [:PJ_CONTEXT, :PJ], :int 61 | attach_function :proj_coordoperation_get_param_index, [:PJ_CONTEXT, :PJ, :string], :int 62 | attach_function :proj_coordoperation_get_param, [:PJ_CONTEXT, :PJ, :int, :pointer, :pointer, :pointer, 63 | :pointer, :pointer, :pointer, :pointer,:pointer, :pointer, :pointer], :int 64 | attach_function :proj_coordoperation_is_instantiable, [:PJ_CONTEXT, :PJ], :int 65 | attach_function :proj_coordoperation_get_grid_used_count, [:PJ_CONTEXT, :PJ], :int 66 | attach_function :proj_coordoperation_get_grid_used, [:PJ_CONTEXT, :PJ, :int, 67 | :pointer, :pointer, :pointer, :pointer, :pointer, :pointer, :pointer], :int 68 | 69 | # Network 70 | attach_function :proj_context_get_url_endpoint, [:PJ_CONTEXT], :string 71 | attach_function :proj_context_get_user_writable_directory, [:PJ_CONTEXT, :int], :string 72 | end 73 | end -------------------------------------------------------------------------------- /lib/api/api_7_2.rb: -------------------------------------------------------------------------------- 1 | module Proj 2 | module Api 3 | attach_function :proj_context_clone, [:PJ_CONTEXT], :PJ_CONTEXT 4 | attach_function :proj_context_set_ca_bundle_path, [:PJ_CONTEXT, :string], :void 5 | 6 | # Datum ensembles 7 | attach_function :proj_crs_get_datum_ensemble, [:PJ_CONTEXT, :PJ], :PJ 8 | attach_function :proj_crs_get_datum_forced, [:PJ_CONTEXT, :PJ], :PJ 9 | attach_function :proj_datum_ensemble_get_member_count, [:PJ_CONTEXT, :PJ], :int 10 | attach_function :proj_datum_ensemble_get_accuracy, [:PJ_CONTEXT, :PJ], :double 11 | attach_function :proj_datum_ensemble_get_member, [:PJ_CONTEXT, :PJ, :int], :PJ 12 | attach_function :proj_dynamic_datum_get_frame_reference_epoch, [:PJ_CONTEXT, :PJ], :double 13 | end 14 | end -------------------------------------------------------------------------------- /lib/api/api_8_0.rb: -------------------------------------------------------------------------------- 1 | module Proj 2 | module Api 3 | attach_function :proj_crs_is_derived, [:PJ_CONTEXT, :PJ], :int 4 | attach_function :proj_context_errno_string, [:PJ_CONTEXT, :int], :string 5 | end 6 | end -------------------------------------------------------------------------------- /lib/api/api_8_1.rb: -------------------------------------------------------------------------------- 1 | module Proj 2 | module Api 3 | # Stores a description of a celestial body. 4 | class ProjCelestialBodyInfo < FFI::Struct 5 | layout :auth_name, :string, 6 | :name, :string 7 | end 8 | 9 | attach_function :proj_context_get_database_structure, [:PJ_CONTEXT, :pointer], :PROJ_STRING_LIST 10 | attach_function :proj_get_geoid_models_from_database, [:PJ_CONTEXT, :string, :string, :pointer], :PROJ_STRING_LIST 11 | attach_function :proj_suggests_code_for, [:PJ_CONTEXT, :PJ, :string, :int, :pointer], :pointer 12 | attach_function :proj_string_destroy, [:pointer], :void 13 | 14 | attach_function :proj_get_celestial_body_list_from_database, [:PJ_CONTEXT, :string, :pointer], :pointer 15 | attach_function :proj_get_celestial_body_name, [:PJ_CONTEXT, :PJ], :string 16 | attach_function :proj_celestial_body_list_destroy, [:pointer], :void 17 | 18 | typedef :pointer, :PJ_INSERT_SESSION 19 | attach_function :proj_insert_object_session_create, [:PJ_CONTEXT], :PJ_INSERT_SESSION 20 | attach_function :proj_insert_object_session_destroy, [:PJ_CONTEXT, :PJ_INSERT_SESSION], :void 21 | attach_function :proj_get_insert_statements, [:PJ_CONTEXT, :PJ_INSERT_SESSION, :PJ, :string, :string, :int, 22 | :pointer , :pointer], :PROJ_STRING_LIST 23 | end 24 | end -------------------------------------------------------------------------------- /lib/api/api_8_2.rb: -------------------------------------------------------------------------------- 1 | module Proj 2 | module Api 3 | attach_function :proj_trans_bounds, [:PJ_CONTEXT, :PJ, PJ_DIRECTION, :double, :double, :double, :double, 4 | :pointer, :pointer, :pointer, :pointer, :int], :int 5 | end 6 | end -------------------------------------------------------------------------------- /lib/api/api_9_1.rb: -------------------------------------------------------------------------------- 1 | module Proj 2 | module Api 3 | attach_function :proj_trans_get_last_used_operation, [:PJ], :PJ 4 | attach_function :proj_operation_factory_context_set_area_of_interest_name, [:PJ_CONTEXT, :pointer, :string], :void 5 | attach_function :proj_area_set_name, [:PJ_AREA, :string], :void 6 | end 7 | end -------------------------------------------------------------------------------- /lib/api/api_9_2.rb: -------------------------------------------------------------------------------- 1 | module Proj 2 | module Api 3 | attach_function :proj_rtodms2, [:pointer, :size_t, :double, :int, :int], :string 4 | attach_function :proj_get_area_of_use_ex, [:PJ_CONTEXT, :PJ, :int, :pointer, :pointer, :pointer, :pointer, :pointer], :bool 5 | attach_function :proj_get_domain_count, [:PJ], :int 6 | attach_function :proj_get_scope_ex, [:PJ, :int], :string 7 | attach_function :proj_coordinate_metadata_get_epoch, [:PJ_CONTEXT, :PJ], :double 8 | end 9 | end -------------------------------------------------------------------------------- /lib/api/api_9_4.rb: -------------------------------------------------------------------------------- 1 | module Proj 2 | module Api 3 | attach_function :proj_coordinate_metadata_create, [:PJ_CONTEXT, :PJ, :double], :PJ 4 | attach_function :proj_crs_has_point_motion_operation, [:PJ_CONTEXT, :PJ], :int 5 | end 6 | end -------------------------------------------------------------------------------- /lib/proj.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require_relative 'api/api' 4 | 5 | require_relative 'proj/pj_object' 6 | require_relative 'proj/pj_objects' 7 | require_relative 'proj/coordinate_operation_mixin' 8 | require_relative 'proj/projection' 9 | require_relative 'proj/conversion' 10 | require_relative 'proj/crs' 11 | require_relative 'proj/transformation' 12 | 13 | require_relative 'proj/area' 14 | require_relative 'proj/axis_info' 15 | require_relative 'proj/bounds' 16 | require_relative 'proj/coordinate_system' 17 | require_relative 'proj/crs_info' 18 | require_relative 'proj/context' 19 | require_relative 'proj/coordinate' 20 | require_relative 'proj/database' 21 | require_relative 'proj/datum' 22 | require_relative 'proj/datum_ensemble' 23 | require_relative 'proj/ellipsoid' 24 | require_relative 'proj/error' 25 | require_relative 'proj/file_api' 26 | require_relative 'proj/grid' 27 | require_relative 'proj/grid_cache' 28 | require_relative 'proj/grid_info' 29 | require_relative 'proj/network_api' 30 | require_relative 'proj/parameter' 31 | require_relative 'proj/operation' 32 | require_relative 'proj/operation_factory_context' 33 | require_relative 'proj/parameters' 34 | require_relative 'proj/prime_meridian' 35 | require_relative 'proj/session' 36 | require_relative 'proj/strings' 37 | require_relative 'proj/unit' 38 | 39 | module Proj 40 | # Returns information about the Proj library 41 | # 42 | # @see https://proj.org/development/reference/functions.html#c.proj_info proj_info 43 | # 44 | # @return [PJ_INFO] 45 | def self.info 46 | Api.proj_info 47 | end 48 | 49 | # Returns the Proj version 50 | # 51 | # @see https://proj.org/development/reference/functions.html#c.proj_info proj_info 52 | # 53 | # @return [String] 54 | def self.version 55 | self.info[:version] 56 | end 57 | 58 | # Returns default search paths 59 | # 60 | # @see https://proj.org/development/reference/functions.html#c.proj_info proj_info 61 | # 62 | # @return [Array] List of search paths 63 | def self.search_paths 64 | self.info[:searchpath].split(";") 65 | end 66 | 67 | # Return information about the specific init file 68 | # 69 | # @see https://proj.org/development/reference/functions.html#c.proj_init_info proj_init_info 70 | # 71 | # @param file_name [String] The name of the init file (not the path) 72 | # 73 | # @return [PJ_INIT_INFO] 74 | def self.init_file_info(file_name) 75 | Api.proj_init_info(file_name) 76 | end 77 | 78 | # Converts degrees to radians 79 | # 80 | # see https://proj.org/development/reference/functions.html#c.proj_torad proj_torad 81 | # 82 | # @param value [Float] Value in degrees to convert 83 | # 84 | # @return [Float] 85 | def self.degrees_to_radians(value) 86 | Api.proj_torad(value) 87 | end 88 | 89 | # Converts radians degrees 90 | # 91 | # see https://proj.org/development/reference/functions.html#c.proj_todeg proj_todeg 92 | # 93 | # @param value [Float] Value in radians to convert 94 | # 95 | # @return [Float] 96 | def self.radians_to_degrees(value) 97 | Api.proj_todeg(value) 98 | end 99 | 100 | # Convert string of degrees, minutes and seconds to radians. 101 | # 102 | # see https://proj.org/development/reference/functions.html#c.proj_dmstor proj_dmstor 103 | # 104 | # @param value [String] Value to be converted to radians 105 | # 106 | # @return [Float] 107 | def self.degrees_minutes_seconds_to_radians(value) 108 | ptr = FFI::MemoryPointer.new(:string) 109 | Api.proj_dmstor(value, ptr) 110 | end 111 | 112 | # Convert radians to a string representation of degrees, minutes and seconds 113 | # 114 | # @see https://proj.org/development/reference/functions.html#c.proj_rtodms proj_rtodms 115 | # @see https://proj.org/development/reference/functions.html#c.proj_rtodms2 proj_rtodms2 116 | # 117 | # @param value [Float] Value to be converted in radians 118 | # @param positive [String] Character denoting positive direction, typically 'N' or 'E'. Default 'N' 119 | # @param negative [String] Character denoting negative direction, typically 'S' or 'W'. Default 'S' 120 | # 121 | # @return [String] 122 | def self.radians_to_degrees_minutes_seconds(value, positive='N', negative='S') 123 | ptr = FFI::MemoryPointer.new(:char, 100) 124 | if Api::PROJ_VERSION < Gem::Version.new('9.2.0') 125 | Api.proj_rtodms(ptr, value, positive.ord, negative.ord) 126 | else 127 | Api.proj_rtodms2(ptr, ptr.size, value, positive.ord, negative.ord) 128 | end 129 | ptr.read_string_to_null 130 | end 131 | end 132 | 133 | at_exit do 134 | # Clean up any Proj allocated resources on exit. See https://proj.org/development/reference/functions.html#c.proj_cleanup 135 | Proj::Api.proj_cleanup 136 | end 137 | -------------------------------------------------------------------------------- /lib/proj/area.rb: -------------------------------------------------------------------------------- 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 = 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 75 | -------------------------------------------------------------------------------- /lib/proj/axis_info.rb: -------------------------------------------------------------------------------- 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 PJ_AXIS_DESCRIPTION structure 31 | # 32 | # @return [PJ_AXIS_DESCRIPTION] 33 | def to_description 34 | Api::PJ_AXIS_DESCRIPTION.create(name: name, abbreviation: abbreviation, direction: direction, 35 | unit_conv_factor: unit_conv_factor, unit_name: 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 -------------------------------------------------------------------------------- /lib/proj/bounds.rb: -------------------------------------------------------------------------------- 1 | module Proj 2 | class Bounds 3 | attr_reader :name, :xmin, :ymin, :xmax, :ymax 4 | 5 | def initialize(xmin, ymin, xmax, ymax, name = nil) 6 | @xmin = xmin 7 | @ymin = ymin 8 | @xmax = xmax 9 | @ymax = ymax 10 | @name = name 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/proj/context.rb: -------------------------------------------------------------------------------- 1 | module Proj 2 | # Proj 4.8 introduced the concept of a thread context object to support multi-threaded programs. The bindings 3 | # automatically create one context per thread (its stored in local thread storage). 4 | class Context 5 | attr_reader :database 6 | 7 | # Returns the default Proj context. This context *must* only be used in the main thread 8 | # In general it is better to create new contexts 9 | # 10 | # @return [Context] The default context 11 | def self.default 12 | result = Context.allocate 13 | # The default Proj Context is represented by a null pointer 14 | result.instance_variable_set(:@pointer, FFI::Pointer::NULL) 15 | result 16 | end 17 | 18 | # The context for the current thread. If a context does not exist 19 | # a new one is created 20 | # 21 | # @return [Context] The context for the current thread 22 | def self.current 23 | Thread.current[:proj_context] ||= Context.new 24 | end 25 | 26 | # @!visibility private 27 | def self.finalize(pointer) 28 | proc do 29 | Api.proj_context_destroy(pointer) 30 | end 31 | end 32 | 33 | def initialize 34 | @pointer = Api.proj_context_create 35 | ObjectSpace.define_finalizer(self, self.class.finalize(@pointer)) 36 | 37 | @database = Database.new(self) 38 | end 39 | 40 | def initialize_copy(original) 41 | ObjectSpace.undefine_finalizer(self) 42 | 43 | super 44 | 45 | @pointer = Api.proj_context_clone(original) 46 | @database = Database.new(self) 47 | 48 | ObjectSpace.define_finalizer(self, self.class.finalize(@pointer)) 49 | end 50 | 51 | def to_ptr 52 | @pointer 53 | end 54 | 55 | # Returns the current error-state of the context. An non-zero error codes indicates an error. 56 | # 57 | # See https://proj.org/development/reference/functions.html#c.proj_context_errno proj_context_errno 58 | # 59 | # return [Integer] 60 | def errno 61 | Api.proj_context_errno(self) 62 | end 63 | 64 | # Sets a custom log function 65 | # 66 | # @example 67 | # context.set_log_function(data) do |pointer, int, message| 68 | # ... do stuff... 69 | # end 70 | # 71 | # @param pointer [FFI::MemoryPointer] Optional pointer to custom data 72 | # @param proc [Proc] Custom logging procedure 73 | # 74 | # @return [nil] 75 | def set_log_function(pointer = nil, &proc) 76 | Api.proj_log_func(self, pointer, proc) 77 | end 78 | 79 | # Gets the current log level 80 | # 81 | # @return [PJ_LOG_LEVEL] 82 | def log_level 83 | Api.proj_log_level(self, :PJ_LOG_TELL) 84 | end 85 | 86 | # Sets the current log level 87 | # 88 | # @param value [PJ_LOG_LEVEL] 89 | # @return [nil] 90 | def log_level=(value) 91 | Api.proj_log_level(self, value) 92 | end 93 | 94 | # Gets if proj4 init rules are being used (i.e., support +init parameters) 95 | # 96 | # @return [Boolean] 97 | def use_proj4_init_rules 98 | result = Api.proj_context_get_use_proj4_init_rules(self, 0) 99 | result == 1 ? true : false 100 | end 101 | 102 | # Sets if proj4 init rules should be used 103 | # 104 | # @param value [Boolean] 105 | # 106 | # @return [nil] 107 | def use_proj4_init_rules=(value) 108 | Api.proj_context_use_proj4_init_rules(self, value ? 1 : 0) 109 | end 110 | 111 | # Guess the "dialect" of the specified WKT string 112 | # 113 | # @see https://proj.org/development/reference/functions.html#c.proj_context_guess_wkt_dialect 114 | # 115 | # @param wkt [String] A WKT string 116 | # 117 | # @return [PJ_GUESSED_WKT_DIALECT] 118 | def wkt_dialect(wkt) 119 | Api.proj_context_guess_wkt_dialect(self, wkt) 120 | end 121 | 122 | # Sets the CA Bundle path which will be used by PROJ when curl and PROJ_NETWORK are enabled. 123 | # 124 | # @see https://proj.org/development/reference/functions.html#c.proj_context_set_ca_bundle_path 125 | # 126 | # @param path [String] Path to CA bundle. 127 | # 128 | # @return [nil] 129 | def ca_bundle_path=(path) 130 | Api.proj_context_set_ca_bundle_path(self, path.encode(:utf8)) 131 | end 132 | 133 | # Returns the cache used to store grid files locally 134 | # 135 | # @return [GridCache] 136 | def cache 137 | GridCache.new(self) 138 | end 139 | 140 | # Returns if network access is enabled allowing {Grid} files to be downloaded 141 | # 142 | # @see https://proj.org/development/reference/functions.html#c.proj_context_is_network_enabled 143 | # 144 | # @return [Boolean] True if network access is enabled, otherwise false 145 | def network_enabled? 146 | result = Api.proj_context_is_network_enabled(self) 147 | result == 1 ? true : false 148 | end 149 | 150 | # Enable or disable network access for downloading grid files 151 | # 152 | # @see https://proj.org/development/reference/functions.html#c.proj_context_set_enable_network 153 | # 154 | # @param value [Boolean] Specifies if network access should be enabled or disabled 155 | def network_enabled=(value) 156 | Api.proj_context_set_enable_network(self, value ? 1 : 0) 157 | end 158 | 159 | # Returns the URL endpoint to query for remote grids 160 | # 161 | # @see https://proj.org/development/reference/functions.html#c.proj_context_get_url_endpoint 162 | # 163 | # @return [String] Endpoint URL 164 | def url 165 | Api.proj_context_get_url_endpoint(self) 166 | end 167 | 168 | # Sets the URL endpoint to query for remote grids. This overrides the default endpoint in the PROJ configuration file or with the PROJ_NETWORK_ENDPOINT environment variable. 169 | # 170 | # @see https://proj.org/development/reference/functions.html#c.proj_context_set_url_endpoint 171 | # 172 | # @param value [String] Endpoint URL 173 | def url=(value) 174 | Api.proj_context_set_url_endpoint(self, value) 175 | end 176 | 177 | # Returns the user directory used to save grid files. 178 | # 179 | # @see https://proj.org/development/reference/functions.html#c.proj_context_get_user_writable_directory 180 | # 181 | # @param create [Boolean] If true create the directory if it does not exist already. Defaults to false. 182 | # 183 | # @return [String] Directory 184 | def user_directory(create = false) 185 | Api.proj_context_get_user_writable_directory(self, create ? 1 : 0) 186 | end 187 | 188 | # Sets the paths that Proj will search when opening one of its resource files 189 | # such as the proj.db database, grids, etc. 190 | # 191 | # If set on the default context, they will be inherited by contexts created later. 192 | # 193 | # @see https://proj.org/development/reference/functions.html#c.proj_context_set_search_paths 194 | def search_paths=(paths) 195 | # Convert paths to C chars 196 | paths_ptr = paths.map do |path| 197 | FFI::MemoryPointer.from_string(path) 198 | end 199 | 200 | pointer = FFI::MemoryPointer.new(:pointer, paths.size) 201 | pointer.write_array_of_pointer(paths_ptr) 202 | 203 | if Api.method_defined?(:proj_context_set_search_paths) 204 | Api.proj_context_set_search_paths(self, paths.size, pointer) 205 | elsif Api.method_defined?(:pj_set_searchpath) 206 | Api.pj_set_searchpath(paths.size, pointer) 207 | end 208 | end 209 | 210 | # Installs a new {FileApiImpl FileApi} 211 | # 212 | # @see https://proj.org/development/reference/functions.html#c.proj_context_set_fileapi 213 | def set_file_api(file_api_klass) 214 | unless file_api_klass.kind_of?(Class) 215 | raise("#{file_api_klass} must be a class whose initializer has single argument which is a context") 216 | end 217 | 218 | # There is no API to "uninstall" a FileApi. Thus it needs to stay alive 219 | # until the context is GCed 220 | @file_api = file_api_klass.new(self) 221 | end 222 | 223 | # Installs a new {NetworkApiImpl NetworkApi} 224 | # 225 | # @see https://proj.org/development/reference/functions.html#c.proj_context_set_network_callbacks 226 | def set_network_api(network_api_klass) 227 | unless network_api_klass.kind_of?(Class) 228 | raise("#{network_api_klass} must be a class whose initializer has single argument which is a context") 229 | end 230 | 231 | # There is no API to "uninstall" a FileApi. Thus it needs to stay alive 232 | # until the context is GCed 233 | @network_api = network_api_klass.new(self) 234 | end 235 | 236 | # --- Deprecated ------- 237 | def database_path 238 | self.database.path 239 | end 240 | 241 | # Sets the path to the Proj database 242 | def database_path=(value) 243 | self.database.path = value 244 | end 245 | 246 | extend Gem::Deprecate 247 | deprecate :database_path, "context.database.path", 2023, 6 248 | deprecate :database_path=, "context.database.path=", 2023, 6 249 | end 250 | end -------------------------------------------------------------------------------- /lib/proj/conversion.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | require 'stringio' 3 | 4 | module Proj 5 | # Conversions are {CoordinateOperationMixin coordinate operations} that convert a source 6 | # {Coordinate coordinate} to a new value. In Proj they are defined as operations that 7 | # do not exert a change in reference frame while {Transformation transformations } do. 8 | class Conversion < PjObject 9 | include CoordinateOperationMixin 10 | 11 | # Instantiate a Conversion 12 | 13 | # Create a Transformation 14 | # 15 | # @param context [Context] Context 16 | # @param name [String] Name of the transformation. Default is nil. 17 | # @param auth_name [String] Transformation authority name. Default is nil. 18 | # @param code [String] Transformation code. Default is nil. 19 | # @param method_name [String] Method name. Default is nil. 20 | # @param method_auth_name [String] Method authority name. Default is nil. 21 | # @param method_code [String] Method code. Default is nil. 22 | # @param params [Array] Parameter descriptions 23 | # 24 | # @return [Conversion] 25 | def self.create_conversion(context, name:, auth_name:, code:, method_name:, method_auth_name:, method_code:, params:) 26 | params_ptr = FFI::MemoryPointer.new(Api::PJ_PARAM_DESCRIPTION, params.size) 27 | params.each_with_index do |param, i| 28 | param_description_target = Api::PJ_PARAM_DESCRIPTION.new(params_ptr[i]) 29 | param_description_source = param.to_description 30 | param_description_target.to_ptr.__copy_from__(param_description_source.to_ptr, Api::PJ_PARAM_DESCRIPTION.size) 31 | end 32 | 33 | pointer = Api.proj_create_conversion(context, name, auth_name, code, method_name, method_auth_name, method_code, params.size, params_ptr) 34 | Error.check_context(context) 35 | self.create_object(pointer, context) 36 | end 37 | 38 | # Instantiates an conversion from a string. The string can be: 39 | # 40 | # * proj-string, 41 | # * WKT string, 42 | # * object code (like "EPSG:4326", "urn:ogc:def:crs:EPSG::4326", "urn:ogc:def:coordinateOperation:EPSG::1671"), 43 | # * Object name. e.g "WGS 84", "WGS 84 / UTM zone 31N". In that case as uniqueness is not guaranteed, heuristics are applied to determine the appropriate best match. 44 | # * OGC URN combining references for compound coordinate reference systems (e.g "urn:ogc:def:crs,crs:EPSG::2393,crs:EPSG::5717" or custom abbreviated syntax "EPSG:2393+5717"), 45 | # * OGC URN combining references for concatenated operations (e.g. "urn:ogc:def:coordinateOperation,coordinateOperation:EPSG::3895,coordinateOperation:EPSG::1618") 46 | # * PROJJSON string. The jsonschema is at https://proj.org/schemas/v0.4/projjson.schema.json (added in 6.2) 47 | # * compound CRS made from two object names separated with " + ". e.g. "WGS 84 + EGM96 height" (added in 7.1) 48 | # 49 | # @see https://proj.org/development/reference/functions.html#c.proj_create 50 | # 51 | # @param value [String]. See above 52 | # 53 | # @return [Conversion] 54 | def initialize(value, context=nil) 55 | context ||= Context.current 56 | ptr = Api.proj_create(context, value) 57 | 58 | if ptr.null? 59 | Error.check_context(context) 60 | end 61 | 62 | if Api.method_defined?(:proj_is_crs) && Api.proj_is_crs(ptr) 63 | raise(Error, "Invalid conversion. Proj created an instance of: #{self.proj_type}.") 64 | end 65 | 66 | super(ptr, context) 67 | end 68 | 69 | # Return an equivalent projection. Currently implemented: 70 | # * EPSG_CODE_METHOD_MERCATOR_VARIANT_A (1SP) to EPSG_CODE_METHOD_MERCATOR_VARIANT_B (2SP) 71 | # * EPSG_CODE_METHOD_MERCATOR_VARIANT_B (2SP) to EPSG_CODE_METHOD_MERCATOR_VARIANT_A (1SP) 72 | # * EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_1SP to EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_2SP 73 | # * EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_2SP to EPSG_CODE_METHOD_LAMBERT_CONIC_CONFORMAL_1SP 74 | # 75 | # @param new_method_epsg_code [String] EPSG code of the target method. Or nil in which case new_method_name must be specified. 76 | # @param new_method_name [String] EPSG or PROJ target method name. Or nil in which case new_method_epsg_code must be specified 77 | # 78 | # @return [Conversion] 79 | def convert_to_other_method(new_method_epsg_code: nil, new_method_name: nil) 80 | ptr = Api.proj_convert_conversion_to_other_method(self.context, self, 81 | new_method_epsg_code ? new_method_epsg_code: 0, 82 | new_method_name) 83 | 84 | if ptr.null? 85 | Error.check_context(context) 86 | end 87 | 88 | self.class.create_object(ptr, context) 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/proj/coordinate.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Proj 4 | # A four dimensional coordinate of double values. 5 | # 6 | # For most geographic CRSes, the units will be in degrees. 7 | class Coordinate 8 | def self.from_coord(pj_coord) 9 | result = self.allocate 10 | result.instance_variable_set(:@coord, pj_coord) 11 | result 12 | end 13 | 14 | # Creates a new coordinate. 15 | # 16 | # @example 17 | # 18 | # coord = Proj::Coordinate.new(:x => 1, :y => 2, :z => 3, :t => 4) 19 | # coord = Proj::Coordinate.new(:u => 5, :v => 6, :w => 7, :t => 8) 20 | # coord = Proj::Coordinate.new(:lam => 9, :phi => 10, :z => 11, :t => 12) 21 | # coord = Proj::Coordinate.new(:lon => 9, :lat => 10, :z => 11, :t => 12) 22 | # coord = Proj::Coordinate.new(:s => 13, :a1 => 14, :a2 => 15) 23 | # coord = Proj::Coordinate.new(:o => 16, :p => 17, :k => 18) 24 | # coord = Proj::Coordinate.new(:e => 19, :n => 20, :u => 21) 25 | 26 | def initialize(x: nil, y: nil, z: nil, t: nil, 27 | u: nil, v: nil, w: nil, # t: nil 28 | lam: nil, phi: nil, # z: nil, t: nil, 29 | lat: nil, lon: nil, # z: nil, t: nil, 30 | s: nil, a1: nil, a2: nil, 31 | o: nil, p: nil, k: nil, 32 | e: nil, n: nil) #u: nil 33 | 34 | @coord = Api::PJ_COORD.new 35 | 36 | keys = if x && y && z && t 37 | [:x, :y, :z, :t] 38 | elsif x && y && z 39 | [:x, :y, :z] 40 | elsif x && y 41 | [:x, :y] 42 | elsif u && v && w && t 43 | [:u, :v, :w, :t] 44 | elsif u && v && w 45 | [:u, :v, :w] 46 | elsif u && v 47 | [:u, :v] 48 | elsif lam && phi && z && t 49 | [:lam, :phi, :z, :t] 50 | elsif lam && phi && z 51 | [:lam, :phi, :z] 52 | elsif lam && phi 53 | [:lam, :phi] 54 | elsif lon && lat && z && t 55 | [:lon, :lat, :z, :t] 56 | elsif lon && lat && z 57 | [:lon, :lat, :z] 58 | elsif lon && lat 59 | [:lon, :lat] 60 | elsif s && a1 && a2 61 | [:s, :a1, :a2] 62 | elsif e && n && u 63 | [:e, :n, :u] 64 | elsif o && p && k 65 | [:o, :p, :k] 66 | else 67 | [] 68 | end 69 | 70 | coord_struct = @coord[:v] 71 | keys.each_with_index do |key, index| 72 | coord_struct[index] = binding.local_variable_get(key) 73 | end 74 | end 75 | 76 | def to_ptr 77 | @coord.to_ptr 78 | end 79 | 80 | def pj_coord 81 | @coord 82 | end 83 | 84 | def eql?(other) 85 | @coord == other.instance_variable_get(:@coord) 86 | end 87 | 88 | def ==(other) 89 | @coord.eql?(other.instance_variable_get(:@coord)) 90 | end 91 | 92 | def enu 93 | @coord[:enu] 94 | end 95 | 96 | def geod 97 | @coord[:geod] 98 | end 99 | 100 | def lp 101 | @coord[:lp] 102 | end 103 | 104 | def lpz 105 | @coord[:lpz] 106 | end 107 | 108 | def lpzt 109 | @coord[:lpzt] 110 | end 111 | 112 | def opk 113 | @coord[:opk] 114 | end 115 | 116 | def uv 117 | @coord[:uv] 118 | end 119 | 120 | def uvw 121 | @coord[:uvw] 122 | end 123 | 124 | def uvwt 125 | @coord[:uvwt] 126 | end 127 | 128 | def xy 129 | @coord[:xy] 130 | end 131 | 132 | def xyz 133 | @coord[:xyz] 134 | end 135 | 136 | def xyzt 137 | @coord[:xyzt] 138 | end 139 | 140 | # Returns x coordinate 141 | # 142 | # @return [Float] 143 | def x 144 | @coord[:v][0] 145 | end 146 | 147 | # Returns y coordinate 148 | # 149 | # @return [Float] 150 | def y 151 | @coord[:v][1] 152 | end 153 | 154 | # Returns z coordinate 155 | # 156 | # @return [Float] 157 | def z 158 | @coord[:v][2] 159 | end 160 | 161 | # Returns t coordinate 162 | # 163 | # @return [Float] 164 | def t 165 | @coord[:v][3] 166 | end 167 | 168 | # Returns u coordinate 169 | # 170 | # @return [Float] 171 | # TODO - This could be u in uvw or enu. Going to ignore that 172 | def u 173 | @coord[:v][0] 174 | end 175 | 176 | # Returns v coordinate 177 | # 178 | # @return [Float] 179 | def v 180 | @coord[:v][1] 181 | end 182 | 183 | # Returns w coordinate 184 | # 185 | # @return [Float] 186 | def w 187 | @coord[:v][2] 188 | end 189 | 190 | # Returns longitude coordinate 191 | # 192 | # @return [Float] 193 | def lon 194 | @coord[:v][0] 195 | end 196 | 197 | # Returns latitude coordinate 198 | # 199 | # @return [Float] 200 | def lat 201 | @coord[:v][1] 202 | end 203 | 204 | # Returns lam coordinate 205 | # 206 | # @return [Float] 207 | def lam 208 | @coord[:v][0] 209 | end 210 | 211 | # Returns phi coordinate 212 | # 213 | # @return [Float] 214 | def phi 215 | @coord[:v][1] 216 | end 217 | 218 | # Returns o coordinate 219 | # 220 | # @return [Float] 221 | def o 222 | @coord[:v][0] 223 | end 224 | 225 | # Returns p coordinate 226 | # 227 | # @return [Float] 228 | def p 229 | @coord[:v][1] 230 | end 231 | 232 | # Returns k coordinate 233 | # 234 | # @return [Float] 235 | def k 236 | @coord[:v][3] 237 | end 238 | 239 | # Returns e coordinate 240 | # 241 | # @return [Float] 242 | def e 243 | @coord[:v][0] 244 | end 245 | 246 | # Returns n coordinate 247 | # 248 | # @return [Float] 249 | def n 250 | @coord[:v][1] 251 | end 252 | 253 | # Returns s coordinate 254 | # 255 | # @return [Float] 256 | def s 257 | @coord[:v][0] 258 | end 259 | 260 | # Returns a1 coordinate 261 | # 262 | # @return [Float] 263 | def a1 264 | @coord[:v][1] 265 | end 266 | 267 | # Returns a2 coordinate 268 | # 269 | # @return [Float] 270 | def a2 271 | @coord[:v][2] 272 | end 273 | 274 | # Returns nice printout of coordinate contents 275 | # 276 | # @return [String] 277 | def to_s 278 | "v0: #{self.x}, v1: #{self.y}, v2: #{self.z}, v3: #{self.t}" 279 | end 280 | end 281 | end 282 | -------------------------------------------------------------------------------- /lib/proj/coordinate_metadata.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Proj 4 | # Coordinate metadata is the information required to make coordinates unambiguous. For a 5 | # coordinate set referenced to a static CRS it is the CRS definition. For a 6 | # coordinate set referenced to a dynamic CRS it is the CRS definition together 7 | # with the coordinate epoch of the coordinates in the coordinate set. 8 | # 9 | # In a dynamic CRS, coordinates of a point on the surface of the Earth may change with time. 10 | # To be unambiguous the coordinates must always be qualified with the epoch at which they 11 | # are valid. The coordinate epoch is not necessarily the epoch at which the observation 12 | # was collected. 13 | class CoordinateMetadata < PjObject 14 | # Create a CoordinateMetadata object 15 | # 16 | # @param crs [Crs] The associated Crs 17 | # @param context [Context]. An optional Context 18 | # @param epoch [Double]. Epoch at wich the CRS is valid 19 | # 20 | # @return [CoordinateMetadata] 21 | def initialize(crs, context=nil, epoch=nil) 22 | ptr = Api.proj_coordinate_metadata_create(context || Context.current, crs, epoch) 23 | 24 | if ptr.null? 25 | Error.check_object(self) 26 | end 27 | 28 | super(ptr, context) 29 | end 30 | 31 | # Returns the coordinate epoch 32 | # 33 | # @return [Double] 34 | def epoch 35 | Api.proj_coordinate_metadata_get_epoch(self.context, self) 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/proj/coordinate_system.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Proj 4 | # Represents a coordinate system for a {Crs CRS} 5 | class CoordinateSystem < PjObject 6 | # Create a CoordinateSystem 7 | # 8 | # @param context [Context] The context associated with the CoordinateSystem 9 | # @param cs_type [PJ_COORDINATE_SYSTEM_TYPE] Coordinate system type 10 | # @param axes [Array] Array of Axes 11 | # 12 | # @return [CoordinateSystem] 13 | def self.create(cs_type, axes, context) 14 | axes_ptr = FFI::MemoryPointer.new(Api::PJ_AXIS_DESCRIPTION, axes.size) 15 | axes.each_with_index do |axis, i| 16 | axis_description_target = Api::PJ_AXIS_DESCRIPTION.new(axes_ptr[i]) 17 | axis_description_source = axis.to_description 18 | axis_description_target.to_ptr.__copy_from__(axis_description_source.to_ptr, Api::PJ_AXIS_DESCRIPTION.size) 19 | end 20 | 21 | pointer = Api.proj_create_cs(context, cs_type, axes.count, axes_ptr) 22 | Error.check_context(context) 23 | self.create_object(pointer, context) 24 | end 25 | 26 | # Create an Ellipsoidal 2D CoordinateSystem 27 | # 28 | # @param context [Context] The context associated with the CoordinateSystem 29 | # @param cs_type [PJ_COORDINATE_SYSTEM_TYPE] Coordinate system type 30 | # @param unit_name [String] Name of the angular units. Or nil for degree 31 | # @param unit_conv_factor [Float] Conversion factor from the angular unit to radian. Set to 0 if unit name is degree 32 | # 33 | # @return [CoordinateSystem] 34 | def self.create_ellipsoidal_2d(cs_type, context, unit_name: nil, unit_conv_factor: 0) 35 | pointer = Api.proj_create_ellipsoidal_2D_cs(context, cs_type, unit_name, unit_conv_factor) 36 | Error.check_context(context) 37 | self.create_object(pointer, context) 38 | end 39 | 40 | # Create an Ellipsoidal 3D CoordinateSystem 41 | # 42 | # @param context [Context] The context associated with the CoordinateSystem 43 | # @param cs_type [PJ_COORDINATE_SYSTEM_TYPE] Coordinate system type 44 | # @param horizontal_angular_unit_name [String] Name of the angular units. Or nil for degree 45 | # @param horizontal_angular_unit_conv_factor [Float] Conversion factor from the angular unit to radian. Set to 0 if horizontal_angular_unit_name name is degree 46 | # @param vertical_linear_unit_name [String] Name of the linear units. Or nil for meters. 47 | # # @param vertical_linear_unit_conv_factor [Float] Conversion factor from the linear unit to meter. Set to 0 if vertical_linear_unit_name is meter. 48 | # 49 | # @return [CoordinateSystem] 50 | def self.create_ellipsoidal_3d(cs_type, context, horizontal_angular_unit_name: nil, horizontal_angular_unit_conv_factor: 0, vertical_linear_unit_name: nil, vertical_linear_unit_conv_factor: 0) 51 | pointer = Api.proj_create_ellipsoidal_3D_cs(context, cs_type, horizontal_angular_unit_name, horizontal_angular_unit_conv_factor, vertical_linear_unit_name, vertical_linear_unit_conv_factor) 52 | Error.check_context(context) 53 | self.create_object(pointer, context) 54 | end 55 | 56 | # Create a CartesiansCS 2D coordinate system 57 | # 58 | # @param context [Context] The context associated with the CoordinateSystem 59 | # @param cs_type [PJ_COORDINATE_SYSTEM_TYPE] Coordinate system type 60 | # @param unit_name [String] Name of the unit. Default is nil. 61 | # @param unit_conv_factor [Float] Unit conversion factor to SI. Default is 0. 62 | # 63 | # @return [CoordinateSystem] 64 | def self.create_cartesian_2d(context, cs_type, unit_name: nil, unit_conv_factor: 0) 65 | pointer = Api.proj_create_cartesian_2D_cs(context, cs_type, unit_name, unit_conv_factor) 66 | Error.check_context(context) 67 | self.create_object(pointer, context) 68 | end 69 | 70 | # Returns the type of the coordinate system 71 | # 72 | # @see https://proj.org/development/reference/functions.html#c.proj_cs_get_type 73 | # 74 | # @return [PJ_COORDINATE_SYSTEM_TYPE] 75 | def cs_type 76 | result = Api.proj_cs_get_type(self.context, self) 77 | if result == :PJ_CS_TYPE_UNKNOWN 78 | Error.check_object(self) 79 | end 80 | result 81 | end 82 | 83 | # Returns the number of axes in the coordinate system 84 | # 85 | # @see https://proj.org/development/reference/functions.html#c.proj_cs_get_axis_count 86 | # 87 | # @return [Integer] 88 | def axis_count 89 | result = Api.proj_cs_get_axis_count(self.context, self) 90 | if result == -1 91 | Error.check_object(self) 92 | end 93 | result 94 | end 95 | 96 | # Returns information about a single axis 97 | # 98 | # @see https://proj.org/development/reference/functions.html#c.proj_cs_get_axis_info 99 | # 100 | # @param index [Integer] Index of the axis 101 | # 102 | # @return [AxisInfo] 103 | def axis_info(index) 104 | p_name = FFI::MemoryPointer.new(:pointer) 105 | p_abbreviation = FFI::MemoryPointer.new(:pointer) 106 | p_direction = FFI::MemoryPointer.new(:pointer) 107 | p_unit_conv_factor = FFI::MemoryPointer.new(:double) 108 | p_unit_name = FFI::MemoryPointer.new(:pointer) 109 | p_unit_auth_name = FFI::MemoryPointer.new(:pointer) 110 | p_unit_code = FFI::MemoryPointer.new(:pointer) 111 | 112 | result = Api.proj_cs_get_axis_info(self.context, self, index, 113 | p_name, p_abbreviation, p_direction, p_unit_conv_factor, p_unit_name, p_unit_auth_name, p_unit_code) 114 | 115 | unless result 116 | Error.check_object(self) 117 | end 118 | 119 | AxisInfo.new(name: p_name.read_pointer.read_string, 120 | abbreviation: p_abbreviation.read_pointer.read_string_to_null, 121 | direction: p_direction.read_pointer.read_string_to_null, 122 | unit_conv_factor: p_unit_conv_factor.read_double, 123 | unit_name: p_unit_name.read_pointer.read_string_to_null, 124 | unit_auth_name: p_unit_auth_name.read_pointer.read_string_to_null, 125 | unit_code: p_unit_code.read_pointer.read_string_to_null) 126 | end 127 | 128 | # Returns information about all axes 129 | # 130 | # @return [Array] 131 | def axes 132 | self.axis_count.times.map do |index| 133 | self.axis_info(index) 134 | end 135 | end 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /lib/proj/crs_info.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | module Proj 3 | class CrsInfo 4 | attr_reader :auth_name, :code, :name, :crs_type, :deprecated, :bbox_valid, 5 | :west_lon_degree, :south_lat_degree, :east_lon_degree, :north_lat_degree, 6 | :area_name, :projection_method_name, :celestial_body_name 7 | 8 | def self.from_proj_crs_info(proj_crs_info) 9 | data = { auth_name: proj_crs_info[:auth_name], 10 | code: proj_crs_info[:code], 11 | name: proj_crs_info[:name], 12 | crs_type: proj_crs_info[:type], 13 | deprecated: proj_crs_info[:deprecated] == 1 ? true : false, 14 | bbox_valid: proj_crs_info[:bbox_valid] == 1 ? true : false, 15 | west_lon_degree: proj_crs_info[:west_lon_degree], 16 | south_lat_degree: proj_crs_info[:south_lat_degree], 17 | east_lon_degree: proj_crs_info[:east_lon_degree], 18 | north_lat_degree: proj_crs_info[:north_lat_degree], 19 | area_name: proj_crs_info[:area_name], 20 | projection_method_name: proj_crs_info[:projection_method_name]} 21 | 22 | if Api::PROJ_VERSION >= Gem::Version.new('8.1.0') 23 | data[:celestial_body_name] = proj_crs_info[:celestial_body_name] 24 | end 25 | 26 | new(**data) 27 | end 28 | 29 | def initialize(auth_name:, code:, name:, crs_type:, deprecated:, bbox_valid:, 30 | west_lon_degree:, south_lat_degree:, east_lon_degree:, north_lat_degree:, 31 | area_name:, projection_method_name:, celestial_body_name: nil) 32 | @auth_name = auth_name 33 | @code = code 34 | @name = name 35 | @crs_type = crs_type 36 | @deprecated = deprecated 37 | @bbox_valid = bbox_valid 38 | @west_lon_degree = west_lon_degree 39 | @south_lat_degree = south_lat_degree 40 | @east_lon_degree = east_lon_degree 41 | @north_lat_degree = north_lat_degree 42 | @area_name = area_name 43 | @projection_method_name = projection_method_name 44 | @celestial_body_name = celestial_body_name 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/proj/datum.rb: -------------------------------------------------------------------------------- 1 | module Proj 2 | class Datum < PjObject 3 | # Returns the frame reference epoch of a dynamic geodetic or vertical reference frame 4 | # 5 | # @see https://proj.org/development/reference/functions.html#c.proj_dynamic_datum_get_frame_reference_epoch 6 | # 7 | # @return [Float] The frame reference epoch as decimal year, or -1 in case of error. 8 | def frame_reference_epoch 9 | Api.proj_dynamic_datum_get_frame_reference_epoch(self.context, self) 10 | end 11 | 12 | # Return the ellipsoid 13 | # 14 | # @see https://proj.org/development/reference/functions.html#c.proj_get_ellipsoid 15 | # 16 | # @return [PjObject] 17 | def ellipsoid 18 | ptr = Api.proj_get_ellipsoid(self.context, self) 19 | self.class.create_object(ptr, self.context) 20 | end 21 | 22 | # Returns the prime meridian 23 | # 24 | # @see https://proj.org/development/reference/functions.html#c.proj_get_prime_meridian 25 | # 26 | # @return [PjObject] 27 | def prime_meridian 28 | ptr = Api.proj_get_prime_meridian(self.context, self) 29 | self.class.create_object(ptr, self.context) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/proj/datum_ensemble.rb: -------------------------------------------------------------------------------- 1 | module Proj 2 | class DatumEnsemble < PjObject 3 | 4 | # Returns the number of members of a datum ensemble 5 | # 6 | # @see https://proj.org/development/reference/functions.html#c.proj_datum_ensemble_get_member_count 7 | # 8 | # @return [Integer] 9 | def count 10 | Api.proj_datum_ensemble_get_member_count(self.context, self) 11 | end 12 | 13 | # Returns a member from a datum ensemble. 14 | # 15 | # @see https://proj.org/development/reference/functions.html#c.proj_datum_ensemble_get_member 16 | # 17 | # @param index [Integer] Index of the datum member to extract. Should be between 0 and DatumEnsembel#count - 1. 18 | # 19 | # @return [Integer] 20 | def [](index) 21 | ptr = Api.proj_datum_ensemble_get_member(self.context, self, index) 22 | self.class.create_object(ptr, self.context) 23 | end 24 | 25 | # Returns the positional accuracy of the datum ensemble 26 | # 27 | # @see https://proj.org/development/reference/functions.html#c.proj_datum_ensemble_get_accuracy 28 | # 29 | # @return [Float] The data ensemble accuracy or -1 in case of error 30 | def accuracy 31 | Api.proj_datum_ensemble_get_accuracy(self.context, self) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/proj/ellipsoid.rb: -------------------------------------------------------------------------------- 1 | module Proj 2 | class Ellipsoid < PjObject 3 | # Returns a list of ellipsoids that are built into Proj. A more comprehensive 4 | # list is stored in the Proj database and can be queried via PjObject#create_from_database 5 | def self.built_in 6 | pointer_to_array = FFI::Pointer.new(Api::PJ_ELLPS, Api.proj_list_ellps) 7 | 8 | result = Array.new 9 | 0.step do |i| 10 | pj_ellps = Api::PJ_ELLPS.new(pointer_to_array[i]) 11 | break result if pj_ellps[:id].nil? 12 | result << pj_ellps 13 | end 14 | result 15 | end 16 | 17 | # Returns ellipsoid parameters 18 | # 19 | # @see https://proj.org/development/reference/functions.html#c.proj_ellipsoid_get_parameters 20 | # 21 | # @return [Hash] Hash of ellipsoid parameters. Axes are in meters 22 | def parameters 23 | @parameters ||= begin 24 | out_semi_major_metre = FFI::MemoryPointer.new(:double) 25 | out_semi_minor_metre = FFI::MemoryPointer.new(:double) 26 | out_is_semi_minor_computed = FFI::MemoryPointer.new(:int) 27 | out_inv_flattening = FFI::MemoryPointer.new(:double) 28 | 29 | result = Api.proj_ellipsoid_get_parameters(self.context, self, out_semi_major_metre, out_semi_minor_metre, out_is_semi_minor_computed, out_inv_flattening) 30 | 31 | if result != 1 32 | Error.check_object(self) 33 | end 34 | 35 | {semi_major_axis: out_semi_major_metre.read_double, 36 | semi_minor_axis: out_semi_minor_metre.read_double, 37 | semi_minor_axis_computed: out_is_semi_minor_computed.read_int == 1 ? true : false, 38 | inverse_flattening: out_inv_flattening.null? ? nil : out_inv_flattening.read_double} 39 | end 40 | end 41 | 42 | # Returns the semi-major axis in meters 43 | # 44 | # @see https://proj.org/development/reference/functions.html#c.proj_ellipsoid_get_parameters 45 | # 46 | # @return [Float] 47 | def semi_major_axis 48 | self.parameters[:semi_major_axis] 49 | end 50 | 51 | # Returns the semi-minor axis in meters 52 | # 53 | # @see https://proj.org/development/reference/functions.html#c.proj_ellipsoid_get_parameters 54 | # 55 | # @return [Float] 56 | def semi_minor_axis 57 | self.parameters[:semi_minor_axis] 58 | end 59 | 60 | # Returns whether the semi-minor axis is computed 61 | # 62 | # @see https://proj.org/development/reference/functions.html#c.proj_ellipsoid_get_parameters 63 | # 64 | # @return [Boolean] 65 | def semi_minor_axis_computed 66 | self.parameters[:semi_minor_axis_computed] 67 | end 68 | 69 | # Returns the inverse flattening value 70 | # 71 | # @see https://proj.org/development/reference/functions.html#c.proj_ellipsoid_get_parameters 72 | # 73 | # @return [Float] 74 | def inverse_flattening 75 | self.parameters[:inverse_flattening] 76 | end 77 | end 78 | end -------------------------------------------------------------------------------- /lib/proj/error.rb: -------------------------------------------------------------------------------- 1 | module Proj 2 | # Represents error thrown by Proj 3 | # 4 | # @see https://proj.org/development/errorhandling.html 5 | class Error < StandardError 6 | # Error codes typically related to coordinate operation initialization 7 | PROJ_ERR_INVALID_OP = 1024 # Other/unspecified error related to coordinate operation initialization 8 | PROJ_ERR_INVALID_OP_WRONG_SYNTAX = PROJ_ERR_INVALID_OP + 1 # Invalid pipeline structure, missing +proj argument, etc 9 | PROJ_ERR_INVALID_OP_MISSING_ARG = PROJ_ERR_INVALID_OP + 2 # Missing required operation parameter 10 | PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE = PROJ_ERR_INVALID_OP + 3 # One of the operation parameter has an illegal value 11 | PROJ_ERR_INVALID_OP_MUTUALLY_EXCLUSIVE_ARGS = PROJ_ERR_INVALID_OP + 4 # Mutually exclusive arguments 12 | PROJ_ERR_INVALID_OP_FILE_NOT_FOUND_OR_INVALID = PROJ_ERR_INVALID_OP + 5 # File not found (particular case of PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE) 13 | 14 | # Error codes related to transformation on a specific coordinate 15 | PROJ_ERR_COORD_TRANSFM = 2048 # Other error related to coordinate transformation 16 | PROJ_ERR_COORD_TRANSFM_INVALID_COORD = PROJ_ERR_COORD_TRANSFM + 1 # For e.g lat > 90deg 17 | PROJ_ERR_COORD_TRANSFM_OUTSIDE_PROJECTION_DOMAIN = PROJ_ERR_COORD_TRANSFM + 2 # Coordinate is outside of the projection domain. e.g approximate mercator with |longitude - lon_0| > 90deg, or iterative convergence method failed 18 | PROJ_ERR_COORD_TRANSFM_NO_OPERATION = PROJ_ERR_COORD_TRANSFM + 3 # No operation found, e.g if no match the required accuracy, or if ballpark transformations were asked to not be used and they would be only such candidate 19 | PROJ_ERR_COORD_TRANSFM_OUTSIDE_GRID = PROJ_ERR_COORD_TRANSFM + 4 # Point to transform falls outside grid or subgrid 20 | PROJ_ERR_COORD_TRANSFM_GRID_AT_NODATA = PROJ_ERR_COORD_TRANSFM + 5 # Point to transform falls in a grid cell that evaluates to nodata 21 | 22 | # Other type of errors 23 | PROJ_ERR_OTHER = 4096 24 | PROJ_ERR_OTHER_API_MISUSE = PROJ_ERR_OTHER + 1 # Error related to a misuse of PROJ API 25 | PROJ_ERR_OTHER_NO_INVERSE_OP = PROJ_ERR_OTHER + 2 # No inverse method available 26 | PROJ_ERR_OTHER_NETWORK_ERROR = PROJ_ERR_OTHER + 3 # Failure when accessing a network resource 27 | 28 | # Check the context to see if an error occurred. If an error has happened will 29 | # raise an exception. 30 | def self.check_context(context) 31 | unless context.errno == 0 32 | # raise(self, "#{self.category(context.errno)}: #{self.message(context)}") 33 | raise(self, self.message(context, context.errno)) 34 | end 35 | end 36 | 37 | def self.check_object(pj_object) 38 | # It would be nice if Proj exposed the proj_context_errno_set method so 39 | # we don't need a pj_object 40 | context = pj_object.context 41 | unless context.errno == 0 42 | message = self.message(context, context.errno) 43 | Api.proj_errno_reset(pj_object) 44 | raise(self, message) 45 | end 46 | end 47 | 48 | # Returns the current error-state of the context. An non-zero error codes indicates an error. 49 | # 50 | # See https://proj.org/development/reference/functions.html#c.proj_errno_string proj_errno_string 51 | # 52 | # @param context [Context] The context the error occurred in 53 | # @param errno [Integer] The error number 54 | # 55 | # return [String] 56 | def self.message(context, errno) 57 | if Api.method_defined?(:proj_context_errno_string) 58 | Api.proj_context_errno_string(context, errno) 59 | elsif Api.method_defined?(:proj_errno_string) 60 | Api.proj_errno_string(errno) 61 | end 62 | end 63 | 64 | # Converts an errno to a error category 65 | def self.category(errno) 66 | self.constants.find do |constant| 67 | self.const_get(constant) == errno 68 | end 69 | end 70 | end 71 | end -------------------------------------------------------------------------------- /lib/proj/file_api.rb: -------------------------------------------------------------------------------- 1 | module Proj 2 | module FileApiCallbacks 3 | def install_callbacks(context) 4 | proj_file_api = Api::PROJ_FILE_API.new 5 | proj_file_api[:version] = 1 6 | 7 | # Store procs to instance variables so they don't get garbage collected 8 | @open_cbk = proj_file_api[:open_cbk] = self.method(:open_callback) 9 | @read_cbk = proj_file_api[:read_cbk] = self.method(:read_callback) 10 | @write_cbk = proj_file_api[:write_cbk] = self.method(:write_callback) 11 | @seek_cbk = proj_file_api[:seek_cbk] = self.method(:seek_callback) 12 | @tell_cbk = proj_file_api[:tell_cbk] = self.method(:tell_callback) 13 | @close_cbk = proj_file_api[:close_cbk] = self.method(:close_callback) 14 | @exists_cbk = proj_file_api[:exists_cbk] = self.method(:exists_callback) 15 | @mkdir_cbk = proj_file_api[:mkdir_cbk] = self.method(:mkdir_callback) 16 | @unlink_cbk = proj_file_api[:unlink_cbk] = self.method(:unlink_callback) 17 | @rename_cbk = proj_file_api[:rename_cbk] = self.method(:rename_callback) 18 | 19 | result = Api.proj_context_set_fileapi(context, proj_file_api, nil) 20 | 21 | if result != 1 22 | Error.check_object(self) 23 | end 24 | end 25 | 26 | # Open file. Return NULL if error 27 | def open_callback(context, path, access_mode, user_data) 28 | result = self.open(path, access_mode) 29 | result ? FFI::MemoryPointer.new(:size_t) : nil 30 | end 31 | 32 | # Read sizeBytes into buffer from current position and return number of bytes read 33 | def read_callback(context, handle, buffer, size_bytes, user_data) 34 | data = self.read(size_bytes) 35 | read_bytes = [size_bytes, data.size].min 36 | buffer.write_bytes(data, 0, read_bytes) 37 | read_bytes 38 | end 39 | 40 | # Write sizeBytes into buffer from current position and return number of bytes written 41 | def write_callback(context, handle, buffer, size_bytes, user_data) 42 | data = buffer.get_bytes(0, size_bytes) 43 | self.write(data) 44 | end 45 | 46 | # Seek to offset using whence=SEEK_SET/SEEK_CUR/SEEK_END. Return TRUE in case of success 47 | def seek_callback(context, handle, offset, whence, user_data) 48 | self.seek(offset, whence) 49 | return 1 # True 50 | end 51 | 52 | # Return current file position 53 | def tell_callback(context, handle, user_data) 54 | self.tell 55 | end 56 | 57 | # Close file 58 | def close_callback(context, handle, user_data) 59 | self.close 60 | end 61 | 62 | # Return TRUE if a file exists 63 | def exists_callback(context, path, user_data) 64 | if self.exists(path) 65 | 1 66 | else 67 | 0 68 | end 69 | end 70 | 71 | # Return TRUE if directory exists or could be created 72 | def mkdir_callback(context, path, user_data) 73 | if self.mdkir(path) 74 | 1 75 | else 76 | 0 77 | end 78 | end 79 | 80 | # Return TRUE if file could be removed 81 | def unlink_callback(context, path, user_data) 82 | if self.unlink(path) 83 | 1 84 | else 85 | 0 86 | end 87 | end 88 | 89 | # Return TRUE if file could be renamed 90 | def rename_callback(context, original_path, new_path, user_data) 91 | if self.rename(original_path, new_path) 92 | 1 93 | else 94 | 0 95 | end 96 | end 97 | end 98 | 99 | # Proj allows its file api to be replaced by a custom implementation. This can be 100 | # done by calling Context#set_file_api with a user defined Class that includes the 101 | # FileApiCallbacks module and implements its required methods. 102 | # 103 | # The FileApiImpl class is a simple example file api implementation. 104 | class FileApiImpl 105 | include FileApiCallbacks 106 | 107 | def initialize(context) 108 | install_callbacks(context) 109 | end 110 | 111 | def open(path, access_mode) 112 | case access_mode 113 | when :PROJ_OPEN_ACCESS_READ_ONLY 114 | if File.exist?(path) 115 | @file = File.open(path, :mode => 'rb') 116 | else 117 | nil # False 118 | end 119 | when :PROJ_OPEN_ACCESS_READ_UPDATE 120 | if File.exist?(path) 121 | @file = File.open(path, :mode => 'r+b') 122 | else 123 | nil # False 124 | end 125 | when :PROJ_OPEN_ACCESS_CREATE 126 | @file = File.open(path, :mode => 'wb') 127 | end 128 | end 129 | 130 | def read(size_bytes) 131 | @file.read(size_bytes) 132 | end 133 | 134 | def write(data) 135 | @file.write(data) 136 | end 137 | 138 | def seek(offset, whence) 139 | @file.seek(offset, whence) 140 | end 141 | 142 | def tell 143 | @file.tell 144 | end 145 | 146 | def close 147 | @file.close 148 | end 149 | 150 | def exists(path) 151 | File.exist?(path) 152 | end 153 | 154 | def mkdir(path) 155 | Dir.mkdir(path) 156 | end 157 | 158 | def unlink(path) 159 | File.unlink(path) if File.exist?(path) 160 | end 161 | 162 | def rename(original_path, new_path) 163 | File.rename(original_path, new_path) 164 | end 165 | end 166 | end -------------------------------------------------------------------------------- /lib/proj/grid.rb: -------------------------------------------------------------------------------- 1 | module Proj 2 | # Grids define models that are used to perform dimension shifts. 3 | # 4 | # Grid files can be quite large and may not be included with Proj depending on how 5 | # it was packaged and any grid licensing requirements. Therefore, Proj has the ability 6 | # to download grids on the fly if {Context#network_enabled? networking} is enabled. 7 | # 8 | # @see https://proj.org/community/rfc/rfc-4.html#rfc4 9 | class Grid 10 | # @!attribute [r] context 11 | # @return [Context] The grid context 12 | # @!attribute [r] name 13 | # @return [String] The grid's name 14 | # @!attribute [r] full_name 15 | # @return [String] The grid's full name 16 | # @!attribute [r] package_name 17 | # @return [String] The grid's package name 18 | # @!attribute [r] url 19 | # @return [URI] A url that can be used to download the grid 20 | attr_reader :context, :name, :full_name, :package_name, :url 21 | 22 | def initialize(name, context = Context.default, full_name: nil, package_name: nil, url: nil, 23 | downloadable: false, open_license: false, available: false) 24 | @name = name 25 | @context = context 26 | @full_name = full_name 27 | @package_name = package_name 28 | @url = url 29 | @downloadable = downloadable 30 | @open_license = open_license 31 | @available = available 32 | end 33 | 34 | # Returns whether the grid can be downloaded 35 | # 36 | # @return [Boolean] 37 | def downloadable? 38 | @downloadable 39 | end 40 | 41 | # Returns whether the grid is released with an open license 42 | # 43 | # @return [Boolean] 44 | def open_license? 45 | @open_license 46 | end 47 | 48 | # Returns whether the grid is available at runtime 49 | # 50 | # @return [Boolean] 51 | def available? 52 | @available 53 | end 54 | 55 | # Returns information about this grid 56 | # 57 | # See https://proj.org/development/reference/functions.html#c.proj_grid_info proj_grid_info 58 | # 59 | # @return [GridInfo] 60 | def info 61 | ptr = Api.proj_grid_info(self.name) 62 | GridInfo.new(ptr) 63 | end 64 | 65 | # Returns if a grid is available in the PROJ user-writable directory. 66 | # This method will only return true if Context#network_enabled? is true 67 | # 68 | # @see https://proj.org/development/reference/functions.html#c.proj_is_download_needed 69 | # 70 | # @param ignore_ttl [Boolean] If set to FALSE, PROJ will only check the recentness of an already downloaded file, if the delay between the last time it has been verified and the current time exceeds the TTL setting. This can save network accesses. If set to TRUE, PROJ will unconditionally check from the server the recentness of the file. 71 | # 72 | # @return [Boolean] 73 | def downloaded?(ignore_ttl = false) 74 | if self.context.network_enabled? 75 | result = Api.proj_is_download_needed(self.context, self.url&.to_s || self.name, ignore_ttl ? 1 : 0) 76 | result == 1 ? false : true 77 | else 78 | false 79 | end 80 | end 81 | 82 | # Download a file in the PROJ user-writable directory if has not already been downlaoded. 83 | # This function can only be used if networking is enabled 84 | # 85 | # @see https://proj.org/development/reference/functions.html#c.proj_download_file 86 | # 87 | # @param ignore_ttl [Boolean] If set to FALSE, PROJ will only check the recentness of an already downloaded file, if the delay between the last time it has been verified and the current time exceeds the TTL setting. This can save network accesses. If set to TRUE, PROJ will unconditionally check from the server the recentness of the file. 88 | # @yieldparam percent [Float] The progress downloading the file in the range of 0 to 1 89 | # 90 | # @return [Boolean] True if the download was successful or unneeded. Otherwise false 91 | def download(ignore_ttl = false) 92 | callback = if block_given? 93 | Proc.new do |context, percent, user_data| 94 | result = yield percent 95 | # Return 1 to tell Proj to keep downloading the file 96 | result ? 1 : 0 97 | end 98 | end 99 | 100 | result = Api.proj_download_file(self.context, self.url&.to_s || self.name, ignore_ttl ? 1 : 0, callback, nil) 101 | result == 1 ? true : false 102 | end 103 | 104 | # Deletes the grid if it has been downloaded 105 | def delete 106 | if self.downloaded? 107 | path = File.join(self.context.user_directory, self.name) 108 | File.delete(path) if File.exist?(path) 109 | end 110 | end 111 | 112 | # Returns the path to the grid if it has been downloaded 113 | # 114 | # @return [String] 115 | def path 116 | if self.downloaded? 117 | File.join(self.context.user_directory, self.name) 118 | end 119 | end 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /lib/proj/grid_cache.rb: -------------------------------------------------------------------------------- 1 | module Proj 2 | # To avoid repeated network access, it is possible to enable a local cache of grids. 3 | # Grid data is stored in a SQLite3 database, cache.db, that is by default stored 4 | # stored in the PROJ user writable directory. 5 | # 6 | # The local cache is enabled by default with a size of 300MB. Cache settings can be overridden 7 | # by this class, env variables or the proj.ini file 8 | # 9 | # @see https://proj.org/usage/network.html#caching 10 | class GridCache 11 | attr_reader :context 12 | 13 | def initialize(context) 14 | @context = context 15 | end 16 | 17 | # Enables or disables the grid cache 18 | # 19 | # @param value [Boolean] 20 | # 21 | # @see https://proj.org/development/reference/functions.html#c.proj_grid_cache_set_enable 22 | def enabled=(value) 23 | Api.proj_grid_cache_set_enable(self.context, value ? 1 : 0) 24 | end 25 | 26 | # Set the path and file of the local cache file which is sqlite database. By default 27 | # it is stored in the user writable directory. 28 | # 29 | # @param value [String] - Full path to the cache. If set to nil then caching will be disabled. 30 | # 31 | # @see https://proj.org/development/reference/functions.html#c.proj_grid_cache_set_filename 32 | def path=(value) 33 | Api.proj_grid_cache_set_filename(self.context, value.encode('UTF-8')) 34 | value 35 | end 36 | 37 | # Sets the cache size 38 | # 39 | # @param value [Integer] Maximum size in Megabytes (1024*1024 bytes), or negative value to set unlimited size. 40 | # 41 | # @see https://proj.org/development/reference/functions.html#c.proj_grid_cache_set_max_size 42 | def max_size=(value) 43 | Api.proj_grid_cache_set_max_size(self.context, value) 44 | value 45 | end 46 | 47 | # Specifies the time-to-live delay for re-checking if the cached properties of files are still up-to-date. 48 | # 49 | # @param value [Integer] Delay in seconds. Use negative value for no expiration. 50 | # 51 | # @see https://proj.org/development/reference/functions.html#c.proj_grid_cache_set_ttl 52 | def ttl=(value) 53 | Api.proj_grid_cache_set_ttl(self.context, value) 54 | value 55 | end 56 | 57 | # Clears the cache 58 | # 59 | # @see https://proj.org/development/reference/functions.html#c.proj_grid_cache_clear 60 | def clear 61 | Api.proj_grid_cache_clear(self.context) 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/proj/grid_info.rb: -------------------------------------------------------------------------------- 1 | module Proj 2 | class GridInfo 3 | attr_reader :gridname, :filename, :format, 4 | :lower_left, :upper_right, 5 | :size_lon, :size_lat, :cell_size_lon, :cell_size_lat 6 | 7 | def initialize(pj_grid_info) 8 | @filename = pj_grid_info[:filename].to_ptr.read_string 9 | @gridname = pj_grid_info[:gridname].to_ptr.read_string 10 | @format = pj_grid_info[:format].to_ptr.read_string 11 | @lower_left = pj_grid_info[:lowerleft] 12 | @upper_right = pj_grid_info[:upperright] 13 | @size_lon = pj_grid_info[:n_lon] 14 | @size_lat = pj_grid_info[:n_lat] 15 | @cell_size_lon = pj_grid_info[:cs_lon] 16 | @cell_size_lat = pj_grid_info[:cs_lat] 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/proj/network_api.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | 3 | module Proj 4 | module NetworkApiCallbacks 5 | def install_callbacks(context) 6 | @open_cbk = self.method(:open_callback) 7 | @close_cbk = self.method(:close_callback) 8 | @header_value_cbk = self.method(:header_value_callback) 9 | @read_range_cbk = self.method(:read_range_callback) 10 | 11 | result = Api.proj_context_set_network_callbacks(context, @open_cbk, @close_cbk, @header_value_cbk, @read_range_cbk, nil) 12 | 13 | if result != 1 14 | Error.check_object(self) 15 | end 16 | end 17 | 18 | def open_callback(context, url, offset, size_to_read, buffer, out_size_read, error_string_max_size, out_error_string, user_data) 19 | uri = URI.parse(url) 20 | data = self.open(uri, offset, size_to_read) 21 | out_size = [size_to_read, data.size].min 22 | out_size_read.write(:size_t, out_size) 23 | buffer.write_bytes(data, 0, out_size) 24 | 25 | # Return fake handle 26 | FFI::MemoryPointer.new(:size_t) 27 | end 28 | 29 | def close_callback(context, handle, user_data) 30 | self.close 31 | end 32 | 33 | def header_value_callback(context, handle, header_name_ptr, user_data) 34 | header_name = header_name_ptr.read_string_to_null 35 | value = self.header_value(header_name) 36 | FFI::MemoryPointer.from_string(value) 37 | end 38 | 39 | def read_range_callback(context, handle, offset, size_to_read, buffer, error_string_max_size, out_error_string, user_data) 40 | data = self.read_range(offset, size_to_read) 41 | out_size = [size_to_read, data.size].min 42 | buffer.write_bytes(data, 0, out_size) 43 | out_size 44 | end 45 | end 46 | 47 | # Proj allows its network api to be replaced by a custom implementation. This can be 48 | # done by calling Context#set_network_api with a user defined Class that includes the 49 | # NetworkApiCallbacks module and implements its required methods. 50 | # 51 | # @see https://proj.org/usage/network.html 52 | # 53 | # The NetworkApiImpl class is a simple example of a network api implementation. 54 | class NetworkApiImpl 55 | include NetworkApiCallbacks 56 | 57 | def initialize(context) 58 | install_callbacks(context) 59 | end 60 | 61 | def open(uri, offset, size_to_read) 62 | @uri = uri 63 | @http = Net::HTTP.new(@uri.host, @uri.port) 64 | if uri.scheme == "https" 65 | @http.use_ssl = true 66 | @http.verify_mode = OpenSSL::SSL::VERIFY_PEER 67 | end 68 | @http.start 69 | 70 | read_data(offset, size_to_read) 71 | end 72 | 73 | def close 74 | @http.finish 75 | end 76 | 77 | def header_value(name) 78 | @response[name] 79 | end 80 | 81 | def read_range(offset, size_to_read) 82 | read_data(offset, size_to_read) 83 | end 84 | 85 | def read_data(offset, size_to_read) 86 | headers = {"Range": "bytes=#{offset}-#{offset + size_to_read - 1}"} 87 | request = Net::HTTP::Get.new(@uri.request_uri, headers) 88 | @response = @http.request(request) 89 | @response.body.force_encoding("ASCII-8BIT") 90 | end 91 | end 92 | end -------------------------------------------------------------------------------- /lib/proj/operation.rb: -------------------------------------------------------------------------------- 1 | module Proj 2 | class Operation 3 | attr_reader :id, :description 4 | 5 | def self.list 6 | pointer_to_array = FFI::Pointer.new(Api::PJ_LIST, Api.proj_list_operations) 7 | result = Array.new 8 | 0.step do |i| 9 | operation_info = Api::PJ_LIST.new(pointer_to_array[i]) 10 | break result if operation_info[:id].nil? 11 | id = operation_info[:id] 12 | description = operation_info[:descr].read_pointer.read_string.force_encoding('UTF-8') 13 | result << self.new(id, description) 14 | end 15 | result 16 | end 17 | 18 | def self.get(id) 19 | self.list.find {|operation| operation.id == id} 20 | end 21 | 22 | def initialize(id, description) 23 | @id = id 24 | @description = description 25 | end 26 | 27 | def <=>(other) 28 | self.id <=> other.id 29 | end 30 | 31 | def ==(other) 32 | self.id == other.id 33 | end 34 | 35 | def to_s 36 | self.id 37 | end 38 | 39 | def inspect 40 | "#<#{self.class} id=\"#{id}\", major=\"#{major}\", ell=\"#{ell}\", name=\"#{name}\">" 41 | end 42 | end 43 | end -------------------------------------------------------------------------------- /lib/proj/operation_factory_context.rb: -------------------------------------------------------------------------------- 1 | module Proj 2 | # A context for building coordinate operations between two CRS. 3 | class OperationFactoryContext 4 | attr_reader :context 5 | 6 | # @!visibility private 7 | def self.finalize(pointer) 8 | proc do 9 | Api.proj_operation_factory_context_destroy(pointer) 10 | end 11 | end 12 | 13 | # Create a new OperationFactoryContext 14 | # 15 | # @param context - The context to use or the current context if nil 16 | # @param authority - If authority is nil or the empty string, then coordinate operations 17 | # from any authority will be searched, with the restrictions set 18 | # in the authority_to_authority_preference database table. 19 | # If authority is set to "any", then coordinate operations from any 20 | # authority will be searched 21 | # If authority is a non-empty string different of "any", then coordinate 22 | # operations will be searched only in that authority namespace. 23 | def initialize(context, authority: nil) 24 | @pointer = Api.proj_create_operation_factory_context(context, authority) 25 | ObjectSpace.define_finalizer(self, self.class.finalize(@pointer)) 26 | end 27 | 28 | # Find a list of CoordinateOperation from source_crs to target_crs 29 | # 30 | # @param source [Crs] Source CRS. Must not be nil. 31 | # @param target [Crs] Target CRS. Must not be nil. 32 | # 33 | # @return [Array] - Returns a list of operations 34 | def create_operations(source, target) 35 | ptr = Api.proj_create_operations(self.context, source, target, self) 36 | PjObjects.new(ptr, self.context) 37 | end 38 | 39 | # Specifies whether ballpark transformations are allowed. 40 | # 41 | # @param value - Set to True allow ballpark transformations otherwise False 42 | def ballpark_transformations=(value) 43 | Api.proj_operation_factory_context_set_allow_ballpark_transformations(self.context, self, value ? 1 : 0) 44 | end 45 | 46 | # Set the desired accuracy of the resulting coordinate transformations. 47 | # 48 | # @param value [Float] - Accuracy in meters. Set to 0 to disable the filter. 49 | def desired_accuracy=(value) 50 | Api.proj_operation_factory_context_set_desired_accuracy(self.context, self, value) 51 | end 52 | 53 | # Set the desired area of interest for the resulting coordinate transformations. For an 54 | # area of interest crossing the anti-meridian, west_lon_degree will be greater than east_lon_degree. 55 | # 56 | # @param west West longitude (in degrees). 57 | # @param south South latitude (in degrees). 58 | # @param east East longitude (in degrees). 59 | # @param north North latitude (in degrees). 60 | def set_area_of_interest(west, south, east, north) 61 | Api.proj_operation_factory_context_set_area_of_interest(self.context, self, west, south, east, north) 62 | end 63 | 64 | # Set the name of the desired area of interest for the resulting coordinate transformations. 65 | # 66 | # @param value - Name of the area. Must be known of the database. 67 | def area_of_interest_name=(value) 68 | Api.proj_operation_factory_context_set_area_of_interest_name(self.context, self, value) 69 | end 70 | 71 | # Set how source and target CRS extent should be used when considering if a transformation 72 | # can be used (only takes effect if no area of interest is explicitly defined). 73 | # 74 | # @param value [PROJ_CRS_EXTENT_USE] How source and target CRS extent should be used. 75 | def crs_extent_use=(value) 76 | Api.proj_operation_factory_context_set_crs_extent_use(self.context, self, value) 77 | end 78 | 79 | # Set the spatial criterion to use when comparing the area of validity of coordinate operations 80 | # with the area of interest / area of validity of source and target CRS. 81 | # 82 | # @param value [PROJ_SPATIAL_CRITERION] spatial criterion to use 83 | def spatial_criterion=(value) 84 | Api.proj_operation_factory_context_set_spatial_criterion(self.context, self, value) 85 | end 86 | 87 | # Set how grid availability is used. 88 | # 89 | # @param [PROJ_GRID_AVAILABILITY_USE] - Use how grid availability is used. 90 | def grid_availability=(value) 91 | Api.proj_operation_factory_context_set_grid_availability_use(self.context, self, value) 92 | end 93 | 94 | # Set whether PROJ alternative grid names should be substituted to the official authority names. 95 | # 96 | # @param value [boolean] - Whether PROJ alternative grid names should be used 97 | def use_proj_alternative_grid_names=(value) 98 | Api.proj_operation_factory_context_set_use_proj_alternative_grid_names(self.context, self, value ? 1 : 0) 99 | end 100 | 101 | # Set whether an intermediate pivot CRS can be used for researching coordinate operations 102 | # between a source and target CRS. 103 | # 104 | # @param value [PROJ_INTERMEDIATE_CRS_USE] - Whether and how intermediate CRS may be used 105 | def allow_use_intermediate_crs=(value) 106 | Api.proj_operation_factory_context_set_allow_use_intermediate_crs(self.context, self, value) 107 | end 108 | 109 | # Restrict the potential pivot CRSs that can be used when trying to build a coordinate operation 110 | # between two CRS that have no direct operation. 111 | # 112 | # @param values [Array] - Array of string with the format ["auth_name1", "code1", "auth_name2", "code2"] 113 | def allowed_intermediate_crs=(values) 114 | # Convert strings to C chars 115 | values_ptr = values.map do |value| 116 | FFI::MemoryPointer.from_string(value) 117 | end 118 | 119 | # Add extra item at end for null pointer 120 | pointer = FFI::MemoryPointer.new(:pointer, values.size + 1) 121 | pointer.write_array_of_pointer(values_ptr) 122 | 123 | Api.proj_operation_factory_context_set_allowed_intermediate_crs(self.context, self, pointer) 124 | end 125 | 126 | # Set whether transformations that are superseded (but not deprecated) should be discarded. 127 | # 128 | # @param value [bool] - Whether to discard superseded crses 129 | def discard_superseded=(value) 130 | Api.proj_operation_factory_context_set_discard_superseded(self.context, self, value ? 1 : 0) 131 | end 132 | 133 | def to_ptr 134 | @pointer 135 | end 136 | end 137 | end -------------------------------------------------------------------------------- /lib/proj/parameter.rb: -------------------------------------------------------------------------------- 1 | module Proj 2 | class Parameter 3 | # @!attribute [r] name 4 | # @return [String] Param name 5 | # @!attribute [r] auth_name 6 | # @return [String] Authority name 7 | # @!attribute [r] code 8 | # @return [String] Authority code 9 | # @!attribute [r] value 10 | # @return [String] Param value 11 | # @!attribute [r] unit_conv_factor 12 | # @return [String] Param unit_conv_factor 13 | # @!attribute [r] unit_name 14 | # @return [String] Param unit_name 15 | # @!attribute [r] unit_type 16 | # @return [PJ_UNIT_TYPE] Unit type 17 | attr_reader :name, :auth_name, :code, :value, 18 | :unit_conv_factor, :unit_name, :unit_type 19 | 20 | def initialize(name:, auth_name: nil, code: nil, value:, unit_conv_factor:, unit_name: nil, unit_type:) 21 | @name = name 22 | @auth_name = auth_name 23 | @code = code 24 | @value = value 25 | @unit_conv_factor = unit_conv_factor 26 | @unit_name = unit_name 27 | @unit_type = unit_type 28 | end 29 | 30 | # Returns param information in PJ_PARAM_DESCRIPTION structure 31 | # 32 | # @return [PJ_PARAM_DESCRIPTION] 33 | def to_description 34 | Api::PJ_PARAM_DESCRIPTION.create(name: name, auth_name: auth_name, code: code, value: value, 35 | unit_conv_factor: unit_conv_factor, unit_name: name, unit_type: unit_type) 36 | end 37 | end 38 | end -------------------------------------------------------------------------------- /lib/proj/parameters.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | module Proj 3 | class Parameters 4 | # @!visibility private 5 | def self.finalize(pointer) 6 | proc do 7 | Api.proj_get_crs_list_parameters_destroy(pointer) 8 | end 9 | end 10 | 11 | def initialize 12 | pointer = Api.proj_get_crs_list_parameters_create 13 | @params = Api::PROJ_CRS_LIST_PARAMETERS.new(pointer) 14 | ObjectSpace.define_finalizer(self, self.class.finalize(pointer)) 15 | end 16 | 17 | def to_ptr 18 | @params.to_ptr 19 | end 20 | 21 | def types 22 | result = Array.new 23 | 24 | unless @params[:types].null? 25 | ints = @params[:types].read_array_of_int(@params[:types_count]) 26 | ints.each do |int| 27 | result << Api::PJ_TYPE[int] 28 | end 29 | end 30 | result 31 | end 32 | 33 | def types=(values) 34 | ptr = FFI::MemoryPointer.new(:int, values.size) 35 | ints = values.map {|symbol| Api::PJ_TYPE[symbol]} 36 | ptr.write_array_of_int(ints) 37 | 38 | @params[:types] = ptr 39 | @params[:types_count] = values.size 40 | end 41 | 42 | def crs_area_of_use_contains_bbox 43 | @params[:crs_area_of_use_contains_bbox] 44 | end 45 | 46 | def crs_area_of_use_contains_bbox=(value) 47 | @params[:crs_area_of_use_contains_bbox] = value 48 | end 49 | 50 | def bbox_valid 51 | @params[:bbox_valid] == 1 ? true : false 52 | end 53 | 54 | def bbox_valid=(value) 55 | @params[:bbox_valid] = value ? 1 : 0 56 | end 57 | 58 | def west_lon_degree 59 | @params[:west_lon_degree] 60 | end 61 | 62 | def west_lon_degree=(value) 63 | @params[:west_lon_degree] = value 64 | end 65 | 66 | def south_lat_degree 67 | @params[:south_lat_degree] 68 | end 69 | 70 | def south_lat_degree=(value) 71 | @params[:south_lat_degree] = value 72 | end 73 | 74 | def east_lon_degree 75 | @params[:east_lon_degree] 76 | end 77 | 78 | def east_lon_degree=(value) 79 | @params[:east_lon_degree] = value 80 | end 81 | 82 | def north_lat_degree 83 | @params[:north_lat_degree] 84 | end 85 | 86 | def north_lat_degree=(value) 87 | @params[:north_lat_degree] = value 88 | end 89 | 90 | def allow_deprecated 91 | @params[:allow_deprecated] == 1 ? true : false 92 | end 93 | 94 | def allow_deprecated=(value) 95 | @params[:allow_deprecated] = value ? 1 : 0 96 | end 97 | 98 | def celestial_body_name 99 | @params[:celestial_body_name].read_string_to_null 100 | end 101 | 102 | def celestial_body_name=(value) 103 | ptr = FFI::MemoryPointer.from_string(value) 104 | @params[:celestial_body_name] = ptr 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /lib/proj/pj_objects.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | module Proj 3 | class PjObjects 4 | # @!visibility private 5 | def self.finalize(pointer) 6 | proc do 7 | Api.proj_list_destroy(pointer) 8 | end 9 | end 10 | 11 | def initialize(pointer, context) 12 | @pointer = pointer 13 | @context = context 14 | ObjectSpace.define_finalizer(self, self.class.finalize(@pointer)) 15 | end 16 | 17 | def to_ptr 18 | @pointer 19 | end 20 | 21 | def context 22 | @context || Context.current 23 | end 24 | 25 | def count 26 | Api.proj_list_get_count(self) 27 | end 28 | alias :size :count 29 | 30 | def [](index) 31 | ptr = Api.proj_list_get(context, self, index) 32 | PjObject.create_object(ptr, self.context) 33 | end 34 | 35 | # Returns the index of the operation that would be the most appropriate to transform the specified coordinates. 36 | # 37 | # @param direction [PJ_DIRECTION] - Direction into which to transform the point. 38 | # @param coord [Coordinate] - Coordinate to transform 39 | # 40 | # @return [Integer] - Index of operation 41 | def suggested_operation(direction, coord) 42 | Api.proj_get_suggested_operation(self.context, self, direction, coord) 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/proj/prime_meridian.rb: -------------------------------------------------------------------------------- 1 | module Proj 2 | class PrimeMeridian < PjObject 3 | # Returns a list of Prime Meridians that are built into Proj. A more comprehensive 4 | # list is stored in the Proj database and can be queried via PjObject#create_from_database 5 | def self.built_in 6 | pointer_to_array = FFI::Pointer.new(Api::PJ_PRIME_MERIDIANS, Api.proj_list_prime_meridians) 7 | result = Array.new 8 | 0.step do |i| 9 | prime_meridian_info = Api::PJ_PRIME_MERIDIANS.new(pointer_to_array[i]) 10 | break result if prime_meridian_info[:id].nil? 11 | result << prime_meridian_info 12 | end 13 | result 14 | end 15 | 16 | # Returns prime meridian parameters 17 | # 18 | # @see https://proj.org/development/reference/functions.html#c.proj_prime_meridian_get_parameters 19 | # 20 | # @return [Hash] Hash of ellipsoid parameters. Axes are in meters 21 | def parameters 22 | @parameters ||= begin 23 | out_longitude = FFI::MemoryPointer.new(:double) 24 | out_unit_conv_factor = FFI::MemoryPointer.new(:double) 25 | out_unit_name = FFI::MemoryPointer.new(:string) 26 | 27 | result = Api.proj_prime_meridian_get_parameters(self.context, self, out_longitude, out_unit_conv_factor, out_unit_name) 28 | 29 | if result != 1 30 | Error.check_object(self) 31 | end 32 | 33 | {longitude: out_longitude.read_double, 34 | unit_conv_factor: out_unit_conv_factor.read_double, 35 | unit_name: out_unit_name.read_pointer.read_string_to_null} 36 | end 37 | end 38 | 39 | # Returns the longitude of the prime meridian in its native unit 40 | # 41 | # @see https://proj.org/development/reference/functions.html#c.proj_prime_meridian_get_parameters 42 | # 43 | # @return [Float] 44 | def longitude 45 | self.parameters[:longitude] 46 | end 47 | 48 | # Returns the conversion factor of the prime meridian longitude unit to radians 49 | # 50 | # @see https://proj.org/development/reference/functions.html#c.proj_prime_meridian_get_parameters 51 | # 52 | # @return [Float] 53 | def unit_conv_factor 54 | self.parameters[:unit_conv_factor] 55 | end 56 | 57 | # Returns the unit name 58 | # 59 | # @see https://proj.org/development/reference/functions.html#c.proj_prime_meridian_get_parameters 60 | # 61 | # @return [String ] 62 | def unit_name 63 | self.parameters[:unit_name] 64 | end 65 | end 66 | end -------------------------------------------------------------------------------- /lib/proj/session.rb: -------------------------------------------------------------------------------- 1 | module Proj 2 | class Session 3 | attr_reader :context 4 | 5 | # @!visibility private 6 | def self.finalize(context, pointer) 7 | proc do 8 | Api.proj_insert_object_session_destroy(context, pointer) 9 | end 10 | end 11 | 12 | def initialize(context = nil) 13 | @context = context || Context.current 14 | @pointer = Api.proj_insert_object_session_create(@context) 15 | ObjectSpace.define_finalizer(self, self.class.finalize(@context, @pointer)) 16 | end 17 | 18 | def to_ptr 19 | @pointer 20 | end 21 | 22 | # Returns SQL statements needed to insert the passed object into the database. 23 | # 24 | # @param object [PjObject] - The object to insert into the database. Currently only PrimeMeridian, Ellipsoid, Datum, GeodeticCRS, ProjectedCRS, VerticalCRS, CompoundCRS or BoundCRS are supported. 25 | # @param authority [String] - Authority name into which the object will be inserted. Must not be nil 26 | # @param code [Integer] - Code with which the object will be inserted.Must not be nil 27 | # @param numeric_codes [Boolean] - Whether intermediate objects that can be created should use numeric codes (true), or may be alphanumeric (false) 28 | # @param allowed_authorities [Array] - Authorities to which intermediate objects are allowed to refer to. "authority" will be implicitly added to it. 29 | # 30 | # @return [Strings] - List of insert statements 31 | def get_insert_statements(object, authority, code, numeric_codes = false, allowed_authorities = nil) 32 | allowed_authorities_ptr = if allowed_authorities 33 | # Add extra item at end for null pointer 34 | pointer = FFI::MemoryPointer.new(:pointer, allowed_authorities.size + 1) 35 | 36 | # Convert strings to C chars 37 | allowed_authorities.each_with_index do |authority, i| 38 | pointer.put_pointer(i, FFI::MemoryPointer.from_string(authority)) 39 | end 40 | pointer 41 | end 42 | 43 | strings_ptr = Api.proj_get_insert_statements(self.context, self, object, authority, code, numeric_codes ? 1 : 0, allowed_authorities_ptr, nil) 44 | Strings.new(strings_ptr) 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/proj/strings.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | require 'forwardable' 3 | 4 | module Proj 5 | class Strings 6 | include Enumerable 7 | extend Forwardable 8 | 9 | attr_reader :strings 10 | 11 | def initialize(pointer) 12 | @strings = Array.new 13 | read_strings(pointer) 14 | Api.proj_string_list_destroy(pointer) 15 | end 16 | 17 | private 18 | 19 | def read_strings(pointer) 20 | unless pointer.null? 21 | loop do 22 | string_ptr = pointer.read_pointer 23 | break if string_ptr.null? 24 | @strings << string_ptr.read_string_to_null 25 | pointer += FFI::Pointer::SIZE 26 | end 27 | end 28 | end 29 | 30 | def_delegators :@strings, :[], :count, :each, :empty?, :join, :size, :length, :to_s 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/proj/transformation.rb: -------------------------------------------------------------------------------- 1 | module Proj 2 | # Transformations are {CoordinateOperationMixin coordinate operations} that 3 | # convert {Coordinate coordinates} from one {Crs} to another. 4 | # In Proj they are defined as operations that exert a change in reference frame 5 | # while {Conversion conversions } do not. 6 | class Transformation < PjObject 7 | include CoordinateOperationMixin 8 | 9 | # Create a Transformation 10 | # 11 | # @param context [Context] Context 12 | # @param name [String] Name of the transformation. Default is nil. 13 | # @param auth_name [String] Transformation authority name. Default is nil. 14 | # @param code [String] Transformation code. Default is nil. 15 | # @param source_crs [CoordinateSystem] Source CRS 16 | # @param target_crs [CoordinateSystem] Target CRS 17 | # @param interpolation_crs [CoordinateSystem] Interpolation. Default is nil 18 | # @param method_name [String] Method name. Default is nil. 19 | # @param method_auth_name [String] Method authority name. Default is nil. 20 | # @param method_code [String] Method code. Default is nil. 21 | # @param params [Array] Parameter descriptions 22 | # @param accuracy [Float] Accuracy of the transformation in meters. A negative value means unknown. 23 | # 24 | # @return [Transformation] 25 | def self.create(context, name: nil, auth_name: nil, code: nil, 26 | source_crs:, target_crs:, interpolation_crs: nil, 27 | method_name: nil, method_auth_name: nil, method_code: nil, 28 | params:, accuracy:) 29 | 30 | params_ptr = FFI::MemoryPointer.new(Api::PJ_PARAM_DESCRIPTION, params.size) 31 | params.each_with_index do |param, i| 32 | param_description_target = Api::PJ_PARAM_DESCRIPTION.new(params_ptr[i]) 33 | param_description_source = param.to_description 34 | param_description_target.to_ptr.__copy_from__(param_description_source.to_ptr, Api::PJ_PARAM_DESCRIPTION.size) 35 | end 36 | 37 | ptr = Api.proj_create_transformation(context, name, auth_name, code, 38 | source_crs, target_crs, interpolation_crs, 39 | method_name, method_auth_name, method_code, 40 | params.count, params_ptr, accuracy) 41 | self.create_object(ptr, context) 42 | end 43 | 44 | # Transforms a {Coordinate} from the source {Crs} to the target {Crs}. Coordinates should be expressed in 45 | # the units and axis order of the definition of the source CRS. The returned transformed coordinate will 46 | # be in the units and axis order of the definition of the target CRS. 47 | # 48 | # For most geographic Crses, the units will be in degrees. For geographic CRS defined by the EPSG authority, 49 | # the order of coordinates is latitude first, longitude second. When using a PROJ initialization string, 50 | # on contrary, the order will be longitude first, latitude second. 51 | # 52 | # For projected CRS, the units may vary (metre, us-foot, etc..). 53 | # 54 | # For projected CRS defined by the EPSG authority, and with EAST / NORTH directions, the axis order might be 55 | # easting first, northing second, or the reverse. When using a PROJ string, the order will be 56 | # easting first, northing second, except if the +axis parameter modifies it. 57 | # 58 | # @see https://proj.org/development/reference/functions.html#c.proj_create_crs_to_crs_from_pj 59 | # @see https://proj.org/development/reference/functions.html#c.proj_create_crs_to_crs proj_create_crs_to_crs 60 | # 61 | # @param source [Crs, String] The source Crs. See the Crs documentation for the string format 62 | # @param target [Crs, String] The target Crs. See the Crs documentation for the string format 63 | # @param area [Area] If an area is specified a more accurate transformation between two given systems can be chosen 64 | # @param context [Context] 65 | # @param authority [String] Restricts the authority of coordinate operations looked up in the database 66 | # @param accuracy [Float] Sets the minimum desired accuracy (in metres) of the candidate coordinate operations 67 | # @param allow_ballpark [Boolean] Set to false to disallow the use of Ballpark transformation in the candidate coordinate operations. 68 | # @param only_best [Boolean] Set to true to cause PROJ to error out if the best transformation cannot be used. Requires Proj 9.2 and higher 69 | # 70 | # @return [Transformation] A new transformation 71 | def initialize(source, target, context=nil, 72 | area: nil, authority: nil, accuracy: nil, allow_ballpark: nil, only_best: nil, force_over: nil) 73 | 74 | context ||= Context.current 75 | 76 | options = {"AUTHORITY": authority, 77 | "ACCURACY": accuracy.nil? ? nil : accuracy.to_s, 78 | "ALLOW_BALLPARK": allow_ballpark.nil? ? nil : (allow_ballpark ? "YES" : "NO"), 79 | "ONLY_BEST": only_best.nil? ? nil : (only_best ? "YES" : "NO"), 80 | "FORCE_OVER": force_over.nil? ? nil : (force_over ? "YES" : "NO")} 81 | options_ptr = create_options_pointer(options) 82 | 83 | ptr = if source.is_a?(Crs) && target.is_a?(Crs) 84 | if Api.method_defined?(:proj_create_crs_to_crs_from_pj) 85 | Api.proj_create_crs_to_crs_from_pj(context, source, target, area, options_ptr) 86 | else 87 | Api.proj_create_crs_to_crs(context, source.definition, target.definition, area) 88 | end 89 | else 90 | Api.proj_create_crs_to_crs(context, source, target, nil) 91 | end 92 | 93 | if ptr.null? 94 | Error.check_context(context) 95 | # If that does not raise an error then no operation was found 96 | raise(Error, "No operation found matching criteria") 97 | end 98 | 99 | super(ptr, context) 100 | end 101 | end 102 | end -------------------------------------------------------------------------------- /lib/proj/unit.rb: -------------------------------------------------------------------------------- 1 | module Proj 2 | class Unit 3 | # @!attribute [r] auth_name 4 | # @return [String] Authority name 5 | # @!attribute [r] code 6 | # @return [String] Object code 7 | # @!attribute [r] name 8 | # @return [String] Object name. For example "metre", "US survey foot", etc 9 | # @!attribute [r] category 10 | # @return [String] Category of the unit: one of "linear", "linear_per_time", "angular", "angular_per_time", "scale", "scale_per_time" or "time" 11 | # @!attribute [r] conv_factor 12 | # @return [String] Conversion factor to apply to transform from that unit to the corresponding SI unit (metre for "linear", radian for "angular", etc.). It might be 0 in some cases to indicate no known conversion factor 13 | # @!attribute [r] proj_short_name 14 | # @return [String] PROJ short name, like "m", "ft", "us-ft", etc... Might be nil 15 | # @!attribute [r] deprecated 16 | # @return [Boolean] Whether the object is deprecated 17 | attr_reader :auth_name, :code, :name, :category, :conv_factor, :proj_short_name, :deprecated 18 | 19 | # Returns a list of built in Units. This is deprecated. Use Database#units instead 20 | def self.built_in(auth_name: nil, category: nil, allow_deprecated: false) 21 | # First get linear units 22 | pointer_to_array = FFI::Pointer.new(Api::PJ_UNITS, Api.proj_list_units) 23 | result = Array.new 24 | 0.step do |i| 25 | unit_info = Api::PJ_UNITS.new(pointer_to_array[i]) 26 | break if unit_info[:id].nil? 27 | result << self.new('PROJ', unit_info[:id], unit_info[:name], 28 | 'length', unit_info[:factor], unit_info[:id], false) 29 | end 30 | 31 | # Now get angular linear units 32 | if Api.method_defined?(:proj_list_angular_units) 33 | pointer_to_array = FFI::Pointer.new(Api::PJ_UNITS, Api.proj_list_angular_units) 34 | 0.step do |i| 35 | unit_info = Api::PJ_UNITS.new(pointer_to_array[i]) 36 | break result if unit_info[:id].nil? 37 | result << self.new('PROJ', unit_info[:id], unit_info[:name], 38 | 'angular', unit_info[:factor], unit_info[:id], false) 39 | end 40 | end 41 | 42 | if auth_name 43 | result = result.find_all {|unit_info| unit_info.auth_name == auth_name} 44 | end 45 | 46 | if category 47 | result = result.find_all {|unit_info| unit_info.category == category} 48 | end 49 | result 50 | end 51 | 52 | # Create a new Unit 53 | # 54 | # @param auth_name [String] Authority name 55 | # @param code [String] Object code 56 | # @param name [String] Object name. For example "metre", "US survey foot", etc 57 | # @param category [String] Category of the unit: one of "linear", "linear_per_time", "angular", "angular_per_time", "scale", "scale_per_time" or "time" 58 | # @param conv_factor [String] Conversion factor to apply to transform from that unit to the corresponding SI unit (metre for "linear", radian for "angular", etc.). It might be 0 in some cases to indicate no known conversion factor 59 | # @param proj_short_name [String] PROJ short name, like "m", "ft", "us-ft", etc... Might be nil 60 | # @param deprecated [Boolean] Whether the object is deprecated 61 | # 62 | # @return [Unit] 63 | def initialize(auth_name, code, name, category, conv_factor, proj_short_name, deprecated) 64 | @auth_name = auth_name 65 | @code = code 66 | @name = name 67 | @category = category 68 | @conv_factor = conv_factor 69 | @proj_short_name = proj_short_name 70 | @deprecated = deprecated 71 | end 72 | 73 | def <=>(other) 74 | self.name <=> other.name 75 | end 76 | 77 | def ==(other) 78 | self.auth_name == other.auth_name && 79 | self.code == other.code 80 | end 81 | 82 | def unit_type 83 | case self.category 84 | when "linear" 85 | :PJ_UT_LINEAR 86 | when "linear_per_time" 87 | :PJ_UT_LINEAR 88 | when "angular" 89 | :PJ_UT_ANGULAR 90 | when "angular_per_time" 91 | :PJ_UT_ANGULAR 92 | when "scale" 93 | :PJ_UT_SCALE 94 | when "scale_per_time" 95 | :PJ_UT_SCALE 96 | when "time" 97 | :PJ_UT_TIME 98 | end 99 | end 100 | 101 | def to_s 102 | self.name 103 | end 104 | 105 | def inspect 106 | "#<#{self.class} authority=\"#{auth_name}\", code=\"#{code}\", name=\"#{name}\">" 107 | end 108 | end 109 | end -------------------------------------------------------------------------------- /lib/proj4.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | # This file is for backwards compatibility to the Proj4 namespace 4 | require_relative './proj' 5 | 6 | Proj4 = Proj -------------------------------------------------------------------------------- /proj4rb.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |spec| 2 | spec.name = 'proj4rb' 3 | spec.version = '4.1.0' 4 | spec.summary = 'Ruby bindings for the Proj coordinate transformation library' 5 | spec.description = <<-EOF 6 | Ruby bindings for the Proj coordinate transformation library 7 | EOF 8 | spec.platform = Gem::Platform::RUBY 9 | spec.authors = ['Guilhem Vellut', 'Jochen Topf', 'Charlie Savage'] 10 | spec.homepage = 'https://github.com/cfis/proj4rb' 11 | spec.required_ruby_version = '>= 2.7' 12 | spec.license = 'MIT' 13 | 14 | spec.requirements << 'Proj Library' 15 | spec.require_path = 'lib' 16 | spec.files = Dir['ChangeLog', 17 | 'Gemfile', 18 | 'MIT-LICENSE', 19 | 'proj4rb.gemspec', 20 | 'Rakefile', 21 | 'README.rdoc', 22 | 'lib/**/*.rb', 23 | 'test/*.rb'] 24 | 25 | spec.test_files = Dir["test/test_*.rb"] 26 | 27 | spec.add_dependency "ffi" 28 | 29 | spec.add_development_dependency('bundler') 30 | spec.add_development_dependency('rake') 31 | spec.add_development_dependency('minitest') 32 | spec.add_development_dependency('yard') 33 | end -------------------------------------------------------------------------------- /test/abstract_test.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'minitest/autorun' 3 | require 'proj' 4 | 5 | class AbstractTest < Minitest::Test 6 | end -------------------------------------------------------------------------------- /test/context_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require_relative './abstract_test' 4 | 5 | class ContextTest < AbstractTest 6 | def test_create 7 | context = Proj::Context.new 8 | assert(context.to_ptr) 9 | end 10 | 11 | def test_finalize 12 | 100.times do 13 | context = Proj::Context.new 14 | assert(context.to_ptr) 15 | GC.start 16 | end 17 | assert(true) 18 | end 19 | 20 | def test_clone 21 | context = Proj::Context.new 22 | refute(context.use_proj4_init_rules) 23 | context.use_proj4_init_rules = true 24 | assert(context.use_proj4_init_rules) 25 | 26 | clone = context.clone 27 | assert(clone.use_proj4_init_rules) 28 | end 29 | 30 | def test_dup 31 | context = Proj::Context.new 32 | refute(context.use_proj4_init_rules) 33 | context.use_proj4_init_rules = true 34 | assert(context.use_proj4_init_rules) 35 | 36 | clone = context.clone 37 | assert(clone.use_proj4_init_rules) 38 | end 39 | 40 | def test_one_per_thread 41 | context_1 = Proj::Context.current 42 | context_2 = Proj::Context.current 43 | assert_same(context_1, context_2) 44 | end 45 | 46 | def test_search_paths 47 | context = Proj::Context.new 48 | path = File.join(Dir.tmpdir, "temp_proj_dic2") 49 | 50 | begin 51 | File.open(path, 'wb') do |file| 52 | file << " +proj=pipeline +step +proj=utm +zone=31 +ellps=GRS80" 53 | end 54 | 55 | # Try to use the pipeline, an error will occur since it is not on the path 56 | error = assert_raises(Proj::Error) do 57 | Proj::Conversion.new("+init=temp_proj_dic2:MY_PIPELINE") 58 | end 59 | assert_equal("Invalid value for an argument", error.to_s) 60 | 61 | # Set the path and try again 62 | context.search_paths = [File.dirname(path)] 63 | conversion = Proj::Conversion.new("+init=temp_proj_dic2:MY_PIPELINE", context) 64 | ensure 65 | File.delete(path) 66 | end 67 | end 68 | 69 | def test_database_path 70 | refute_nil(Proj::Context.current.database_path) 71 | end 72 | 73 | def test_log_level 74 | assert_equal(:PJ_LOG_ERROR, Proj::Context.current.log_level) 75 | end 76 | 77 | def test_set_log_level 78 | context = Proj::Context.new 79 | context.log_level = :PJ_LOG_ERROR 80 | assert_equal(:PJ_LOG_ERROR, context.log_level) 81 | end 82 | 83 | def test_invalid_database_path 84 | context = Proj::Context.new 85 | path = '/wrong' 86 | error = assert_raises(Proj::Error) do 87 | context.database.path = path 88 | end 89 | # TODO - if you run this test on its own you get a useful error message, if you run all tests 90 | # at once you get a useless error message. Not sure what is causing the difference 91 | #assert_match(/No such file or directory|generic error of unknown origin|File not found or invalid/, error.to_s) 92 | assert_equal("Unknown error (code 4096)", error.to_s) 93 | end 94 | 95 | def test_set_log_function 96 | context = Proj::Context.new 97 | called = false 98 | 99 | data = FFI::MemoryPointer.new(:int) 100 | data.write_int(5) 101 | 102 | context.set_log_function(data) do |pointer, int, message| 103 | called = true 104 | refute(pointer.null?) 105 | assert_equal(5, pointer.read_int) 106 | assert_equal(1, int) 107 | assert_equal('proj_context_set_database_path: Open of /wrong failed', message) 108 | end 109 | 110 | begin 111 | context.database.path = '/wrong' 112 | rescue 113 | end 114 | 115 | assert(called) 116 | end 117 | 118 | def test_use_proj4_init_rules 119 | context = Proj::Context.new 120 | refute(context.use_proj4_init_rules) 121 | 122 | context.use_proj4_init_rules = true 123 | assert(context.use_proj4_init_rules) 124 | 125 | context.use_proj4_init_rules = false 126 | refute(context.use_proj4_init_rules) 127 | end 128 | 129 | def test_network_enabled 130 | context = Proj::Context.new 131 | refute(context.network_enabled?) 132 | end 133 | 134 | def test_network_enabled_set 135 | context = Proj::Context.new 136 | refute(context.network_enabled?) 137 | 138 | context.network_enabled = true 139 | assert(context.network_enabled?) 140 | 141 | context.network_enabled = false 142 | refute(context.network_enabled?) 143 | end 144 | 145 | def test_url 146 | context = Proj::Context.new 147 | assert_equal("https://cdn.proj.org", context.url) 148 | end 149 | 150 | def test_url_set 151 | context = Proj::Context.new 152 | assert_equal("https://cdn.proj.org", context.url) 153 | 154 | context.url = "https://cdn.proj.org/changed" 155 | assert_equal("https://cdn.proj.org/changed", context.url) 156 | 157 | context.url = "https://cdn.proj.org" 158 | assert_equal("https://cdn.proj.org", context.url) 159 | end 160 | 161 | def test_user_directory 162 | context = Proj::Context.new 163 | assert_match(/proj$/, context.user_directory) 164 | end 165 | 166 | def test_wkt_dialect 167 | context = Proj::Context.new 168 | 169 | wkt = 'LOCAL_CS["foo"]' 170 | assert_equal(:PJ_GUESSED_WKT2_2015, context.wkt_dialect(wkt)) 171 | end 172 | end 173 | -------------------------------------------------------------------------------- /test/coordinate_system_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require_relative './abstract_test' 4 | 5 | class CoordinateSystemTest < AbstractTest 6 | def test_create 7 | context = Proj::Context.new 8 | crs = Proj::Crs.new('EPSG:4326', context) 9 | cs = crs.coordinate_system 10 | axes = cs.axes 11 | cs = Proj::CoordinateSystem.create(cs.cs_type, axes, context) 12 | assert_equal(2, cs.axis_count) 13 | assert_equal(:PJ_CS_TYPE_ELLIPSOIDAL, cs.cs_type) 14 | assert_equal(:PJ_TYPE_UNKNOWN, cs.proj_type) 15 | refute(cs.auth_name) 16 | refute(cs.id_code) 17 | end 18 | 19 | def test_create_ellipsoidal_2d 20 | context = Proj::Context.new 21 | cs = Proj::CoordinateSystem.create_ellipsoidal_2d(:PJ_ELLPS2D_LONGITUDE_LATITUDE, context) 22 | assert_equal(2, cs.axis_count) 23 | assert_equal(:PJ_CS_TYPE_ELLIPSOIDAL, cs.cs_type) 24 | assert_equal(:PJ_TYPE_UNKNOWN, cs.proj_type) 25 | refute(cs.auth_name) 26 | refute(cs.id_code) 27 | end 28 | 29 | def test_create_ellipsoidal_3d 30 | context = Proj::Context.new 31 | cs = Proj::CoordinateSystem.create_ellipsoidal_3d(:PJ_ELLPS3D_LATITUDE_LONGITUDE_HEIGHT, context) 32 | assert_equal(3, cs.axis_count) 33 | assert_equal(:PJ_CS_TYPE_ELLIPSOIDAL, cs.cs_type) 34 | assert_equal(:PJ_TYPE_UNKNOWN, cs.proj_type) 35 | 36 | axis = cs.axis_info(0) 37 | assert_equal("Latitude", axis.name) 38 | assert_equal("lat", axis.abbreviation) 39 | assert_equal("north", axis.direction) 40 | assert_equal("degree", axis.unit_name) 41 | assert_in_delta(0.017453292519943295, axis.unit_conv_factor) 42 | 43 | axis = cs.axis_info(1) 44 | assert_equal("Longitude", axis.name) 45 | assert_equal("lon", axis.abbreviation) 46 | assert_equal("east", axis.direction) 47 | assert_equal("degree", axis.unit_name) 48 | assert_in_delta(0.017453292519943295, axis.unit_conv_factor) 49 | end 50 | 51 | def test_create_ellipsoidal_3d_custom_units 52 | context = Proj::Context.new 53 | cs = Proj::CoordinateSystem.create_ellipsoidal_3d(:PJ_ELLPS3D_LATITUDE_LONGITUDE_HEIGHT, context, 54 | horizontal_angular_unit_name: "foo", horizontal_angular_unit_conv_factor: 0.5, 55 | vertical_linear_unit_name: "bar", vertical_linear_unit_conv_factor: 0.6) 56 | assert_equal(3, cs.axis_count) 57 | assert_equal(:PJ_CS_TYPE_ELLIPSOIDAL, cs.cs_type) 58 | assert_equal(:PJ_TYPE_UNKNOWN, cs.proj_type) 59 | 60 | axis = cs.axis_info(0) 61 | assert_equal("Latitude", axis.name) 62 | assert_equal("lat", axis.abbreviation) 63 | assert_equal("north", axis.direction) 64 | assert_equal("foo", axis.unit_name) 65 | assert_equal(0.5, axis.unit_conv_factor) 66 | 67 | axis = cs.axis_info(1) 68 | assert_equal("Longitude", axis.name) 69 | assert_equal("lon", axis.abbreviation) 70 | assert_equal("east", axis.direction) 71 | assert_equal("foo", axis.unit_name) 72 | assert_equal(0.5, axis.unit_conv_factor) 73 | 74 | axis = cs.axis_info(2) 75 | assert_equal("Ellipsoidal height", axis.name) 76 | assert_equal("h", axis.abbreviation) 77 | assert_equal("up", axis.direction) 78 | assert_equal("bar", axis.unit_name) 79 | assert_equal(0.6, axis.unit_conv_factor) 80 | end 81 | 82 | def test_create_cartesian 83 | context = Proj::Context.new 84 | coordinate_system = Proj::CoordinateSystem.create_cartesian_2d(context, :PJ_CART2D_EASTING_NORTHING) 85 | assert_equal(2, coordinate_system.axis_count) 86 | assert_equal(:PJ_CS_TYPE_CARTESIAN, coordinate_system.cs_type) 87 | assert_equal(:PJ_TYPE_UNKNOWN, coordinate_system.proj_type) 88 | end 89 | 90 | def test_type 91 | crs = Proj::Crs.new('EPSG:4326') 92 | cs = crs.coordinate_system 93 | refute(cs.name) 94 | assert_equal(:PJ_CS_TYPE_ELLIPSOIDAL, cs.cs_type) 95 | assert_equal(:PJ_TYPE_UNKNOWN, cs.proj_type) 96 | assert_equal("EPSG", cs.auth_name) 97 | assert_equal("6422", cs.id_code) 98 | end 99 | 100 | def test_axis_count 101 | crs = Proj::Crs.new('EPSG:4326') 102 | cs = crs.coordinate_system 103 | assert_equal(2, cs.axis_count) 104 | end 105 | 106 | def test_axis_info 107 | crs = Proj::Crs.new('EPSG:4326') 108 | cs = crs.coordinate_system 109 | axis_info = cs.axis_info(0) 110 | 111 | assert_equal("Geodetic latitude", axis_info.name) 112 | assert_equal("Lat", axis_info.abbreviation) 113 | assert_equal("north", axis_info.direction) 114 | assert_equal(0.017453292519943295, axis_info.unit_conv_factor) 115 | assert_equal("degree", axis_info.unit_name) 116 | assert_equal("EPSG", axis_info.unit_auth_name) 117 | assert_equal("9122", axis_info.unit_code) 118 | end 119 | 120 | def test_axes 121 | crs = Proj::Crs.new('EPSG:4326') 122 | cs = crs.coordinate_system 123 | axes = cs.axes 124 | assert_equal(2, axes.count) 125 | 126 | axis_info = axes[0] 127 | assert_equal("Geodetic latitude", axis_info.name) 128 | assert_equal("Lat", axis_info.abbreviation) 129 | assert_equal("north", axis_info.direction) 130 | assert_equal(0.017453292519943295, axis_info.unit_conv_factor) 131 | assert_equal("degree", axis_info.unit_name) 132 | assert_equal("EPSG", axis_info.unit_auth_name) 133 | assert_equal("9122", axis_info.unit_code) 134 | 135 | axis_info = axes[1] 136 | assert_equal("Geodetic longitude", axis_info.name) 137 | assert_equal("Lon", axis_info.abbreviation) 138 | assert_equal("east", axis_info.direction) 139 | assert_equal(0.017453292519943295, axis_info.unit_conv_factor) 140 | assert_equal("degree", axis_info.unit_name) 141 | assert_equal("EPSG", axis_info.unit_auth_name) 142 | assert_equal("9122", axis_info.unit_code) 143 | end 144 | end -------------------------------------------------------------------------------- /test/coordinate_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require_relative './abstract_test' 4 | 5 | class CoordinateTest < AbstractTest 6 | def test_create_xyzt 7 | coord = Proj::Coordinate.new(:x => 1, :y => 2, :z => 3, :t => 4) 8 | assert_equal('v0: 1.0, v1: 2.0, v2: 3.0, v3: 4.0', coord.to_s) 9 | end 10 | 11 | def test_create_uvwt 12 | coord = Proj::Coordinate.new(:u => 5, :v => 6, :w => 7, :t => 8) 13 | assert_equal('v0: 5.0, v1: 6.0, v2: 7.0, v3: 8.0', coord.to_s) 14 | end 15 | 16 | def test_create_lpzt 17 | coord = Proj::Coordinate.new(:lam => 9, :phi => 10, :z => 11, :t => 12) 18 | assert_equal('v0: 9.0, v1: 10.0, v2: 11.0, v3: 12.0', coord.to_s) 19 | end 20 | 21 | def test_create_geod 22 | coord = Proj::Coordinate.new(:s => 13, :a1 => 14, :a2 => 15) 23 | assert_equal('v0: 13.0, v1: 14.0, v2: 15.0, v3: 0.0', coord.to_s) 24 | end 25 | 26 | def test_create_opk 27 | coord = Proj::Coordinate.new(:o => 16, :p => 17, :k => 18) 28 | assert_equal('v0: 16.0, v1: 17.0, v2: 18.0, v3: 0.0', coord.to_s) 29 | end 30 | 31 | def test_create_enu 32 | coord = Proj::Coordinate.new(:e => 19, :n => 20, :u => 21) 33 | assert_equal('v0: 19.0, v1: 20.0, v2: 21.0, v3: 0.0', coord.to_s) 34 | end 35 | end -------------------------------------------------------------------------------- /test/datum_ensemble_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require_relative './abstract_test' 4 | 5 | class DatumEnsembleTest < AbstractTest 6 | def get_datum_ensemble 7 | wkt = <<~EOS 8 | GEOGCRS["ETRS89", 9 | ENSEMBLE["European Terrestrial Reference System 1989 ensemble", 10 | MEMBER["European Terrestrial Reference Frame 1989"], 11 | MEMBER["European Terrestrial Reference Frame 1990"], 12 | MEMBER["European Terrestrial Reference Frame 1991"], 13 | MEMBER["European Terrestrial Reference Frame 1992"], 14 | MEMBER["European Terrestrial Reference Frame 1993"], 15 | MEMBER["European Terrestrial Reference Frame 1994"], 16 | MEMBER["European Terrestrial Reference Frame 1996"], 17 | MEMBER["European Terrestrial Reference Frame 1997"], 18 | MEMBER["European Terrestrial Reference Frame 2000"], 19 | MEMBER["European Terrestrial Reference Frame 2005"], 20 | MEMBER["European Terrestrial Reference Frame 2014"], 21 | ELLIPSOID["GRS 1980",6378137,298.257222101, 22 | LENGTHUNIT["metre",1]], 23 | ENSEMBLEACCURACY[0.1]], 24 | PRIMEM["Greenwich",0, 25 | ANGLEUNIT["degree",0.0174532925199433]], 26 | CS[ellipsoidal,2], 27 | AXIS["geodetic latitude (Lat)",north, 28 | ORDER[1], 29 | ANGLEUNIT["degree",0.0174532925199433]], 30 | AXIS["geodetic longitude (Lon)",east, 31 | ORDER[2], 32 | ANGLEUNIT["degree",0.0174532925199433]]] 33 | EOS 34 | 35 | crs = Proj::Crs.create(wkt) 36 | crs.datum_ensemble 37 | end 38 | 39 | def test_count 40 | ensemble = get_datum_ensemble 41 | assert_equal(11, ensemble.count) 42 | end 43 | 44 | def test_member 45 | ensemble = get_datum_ensemble 46 | 47 | member = ensemble[0] 48 | assert_equal(:PJ_TYPE_GEODETIC_REFERENCE_FRAME, member.proj_type) 49 | assert_equal("European Terrestrial Reference Frame 1989", member.name) 50 | end 51 | 52 | def test_member_invalid 53 | ensemble = get_datum_ensemble 54 | member = ensemble[-1] 55 | refute(member) 56 | 57 | member = ensemble[100] 58 | refute(member) 59 | end 60 | 61 | def test_accuracy 62 | ensemble = get_datum_ensemble 63 | assert_equal(0.1, ensemble.accuracy) 64 | end 65 | end -------------------------------------------------------------------------------- /test/datum_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require_relative './abstract_test' 4 | 5 | class DatumTest < AbstractTest 6 | def test_datum 7 | datum = Proj::PjObject.create_from_database("EPSG", "1061", :PJ_CATEGORY_DATUM) 8 | assert_equal(:PJ_TYPE_DYNAMIC_GEODETIC_REFERENCE_FRAME, datum.proj_type) 9 | assert_equal("International Terrestrial Reference Frame 2008", datum.name) 10 | end 11 | 12 | def test_frame_reference_epoch 13 | datum = Proj::PjObject.create_from_database("EPSG", "1061", :PJ_CATEGORY_DATUM) 14 | epoch = datum.frame_reference_epoch 15 | assert_equal(2005.0, epoch) 16 | end 17 | 18 | def test_ellipsoid 19 | wkt = <<~EOS 20 | PROJCS["WGS 84 / UTM zone 31N", 21 | GEOGCS["WGS 84", 22 | DATUM["WGS_1984", 23 | SPHEROID["WGS 84",6378137,298.257223563, 24 | AUTHORITY["EPSG","7030"]], 25 | AUTHORITY["EPSG","6326"]], 26 | PRIMEM["Greenwich",0, 27 | AUTHORITY["EPSG","8901"]], 28 | UNIT["degree",0.0174532925199433, 29 | AUTHORITY["EPSG","9122"]], 30 | AUTHORITY["EPSG","4326"]], 31 | PROJECTION["Transverse_Mercator"], 32 | PARAMETER["latitude_of_origin",0], 33 | PARAMETER["central_meridian",3], 34 | PARAMETER["scale_factor",0.9996], 35 | PARAMETER["false_easting",500000], 36 | PARAMETER["false_northing",0], 37 | UNIT["metre",1, 38 | AUTHORITY["EPSG","9001"]], 39 | AXIS["Easting",EAST], 40 | AXIS["Northing",NORTH], 41 | AUTHORITY["EPSG","32631"]] 42 | EOS 43 | 44 | crs = Proj::Crs.new(wkt) 45 | ellipsoid = crs.ellipsoid 46 | assert_equal("WGS 84", ellipsoid.name) 47 | assert_equal(:PJ_TYPE_ELLIPSOID, ellipsoid.proj_type) 48 | 49 | ellipsoid_from_datum = crs.datum.ellipsoid 50 | assert_equal("WGS 84", ellipsoid_from_datum.name) 51 | assert_equal(:PJ_TYPE_ELLIPSOID, ellipsoid_from_datum.proj_type) 52 | 53 | assert(ellipsoid_from_datum.equivalent_to?(ellipsoid, :PJ_COMP_STRICT)) 54 | end 55 | end -------------------------------------------------------------------------------- /test/ellipsoid_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require_relative './abstract_test' 4 | 5 | class EllipsoidTest < AbstractTest 6 | def parameter_crs 7 | wkt = <<~EOS 8 | PROJCS["WGS 84 / UTM zone 31N", 9 | GEOGCS["WGS 84", 10 | DATUM["WGS_1984", 11 | SPHEROID["WGS 84",6378137,298.257223563, 12 | AUTHORITY["EPSG","7030"]], 13 | AUTHORITY["EPSG","6326"]], 14 | PRIMEM["Greenwich",0, 15 | AUTHORITY["EPSG","8901"]], 16 | UNIT["degree",0.0174532925199433, 17 | AUTHORITY["EPSG","9122"]], 18 | AUTHORITY["EPSG","4326"]], 19 | PROJECTION["Transverse_Mercator"], 20 | PARAMETER["latitude_of_origin",0], 21 | PARAMETER["central_meridian",3], 22 | PARAMETER["scale_factor",0.9996], 23 | PARAMETER["false_easting",500000], 24 | PARAMETER["false_northing",0], 25 | UNIT["metre",1, 26 | AUTHORITY["EPSG","9001"]], 27 | AXIS["Easting",EAST], 28 | AXIS["Northing",NORTH], 29 | AUTHORITY["EPSG","32631"]] 30 | EOS 31 | Proj::Crs.new(wkt) 32 | end 33 | 34 | def test_built_in 35 | ellipsoids = Proj::Ellipsoid.built_in.map {|ellipsoid| ellipsoid[:id] } 36 | assert(ellipsoids.include?('WGS84')) 37 | assert(ellipsoids.include?('bessel')) 38 | assert(ellipsoids.include?('lerch')) 39 | end 40 | 41 | def test_from_database 42 | ellipsoid = Proj::Ellipsoid.create_from_database("EPSG", "7030", :PJ_CATEGORY_ELLIPSOID) 43 | assert_instance_of(Proj::Ellipsoid, ellipsoid) 44 | assert_equal(:PJ_TYPE_ELLIPSOID, ellipsoid.proj_type) 45 | assert_equal("EPSG", ellipsoid.auth_name) 46 | assert_equal("WGS 84", ellipsoid.name) 47 | assert_equal("7030", ellipsoid.id_code) 48 | end 49 | 50 | def test_parameters 51 | ellipsoid = parameter_crs.ellipsoid 52 | params = ellipsoid.parameters 53 | 54 | expected = {semi_major_axis: 6378137.0, 55 | semi_minor_axis: 6356752.314245179, 56 | semi_minor_axis_computed: true, 57 | inverse_flattening: 298.257223563} 58 | assert_equal(expected, params) 59 | end 60 | 61 | def test_semi_major_axis 62 | meridian = parameter_crs.ellipsoid 63 | assert_equal(6378137.0, meridian.semi_major_axis) 64 | end 65 | 66 | def test_semi_minor_axis 67 | meridian = parameter_crs.ellipsoid 68 | assert_equal(6356752.314245179, meridian.semi_minor_axis) 69 | end 70 | 71 | def test_semi_minor_axis_computed 72 | meridian = parameter_crs.ellipsoid 73 | assert(meridian.semi_minor_axis_computed) 74 | end 75 | 76 | def test_inverse_flattening 77 | meridian = parameter_crs.ellipsoid 78 | assert_equal(298.257223563, meridian.inverse_flattening) 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /test/file_api_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require_relative './abstract_test' 4 | 5 | class FileApiTest < AbstractTest 6 | def setup 7 | super 8 | # Make sure FileAPI callbacks are not GCed 9 | #GC.stress = true 10 | end 11 | 12 | def teardown 13 | super 14 | GC.stress = false 15 | end 16 | 17 | 18 | def test_read 19 | skip "This test causes a segfault due to the way Proj cleans up on shutdown" 20 | context = Proj::Context.new 21 | # Network needs to be on for grid delete to work 22 | context.network_enabled = true 23 | 24 | # Create a grid 25 | grid = Proj::Grid.new("dk_sdfe_dvr90.tif", context) 26 | grid.delete 27 | 28 | grid.download 29 | assert(grid.downloaded?) 30 | context.network_enabled = false 31 | 32 | # Hook up a custom FileApiImpl 33 | context.set_file_api(Proj::FileApiImpl) 34 | 35 | conversion = Proj::Conversion.new(<<~EOS, context) 36 | +proj=pipeline 37 | +step +proj=unitconvert +xy_in=deg +xy_out=rad 38 | +step +proj=vgridshift +grids=dk_sdfe_dvr90.tif +multiplier=1 39 | +step +proj=unitconvert +xy_in=rad +xy_out=deg 40 | EOS 41 | 42 | coord = Proj::Coordinate.new(lon: 12, lat: 56, z: 0) 43 | new_coord = conversion.forward(coord) 44 | assert_in_delta(12, new_coord.lon) 45 | assert_in_delta(56, new_coord.lat) 46 | assert_in_delta(36.5909996032715, new_coord.z, 1e-10) 47 | 48 | context.destroy 49 | end 50 | 51 | def test_write 52 | skip "This test causes a segfault due to the way Proj cleans up on shutdown" 53 | 54 | context = Proj::Context.new 55 | context.network_enabled = true 56 | 57 | # Create a grid 58 | grid = Proj::Grid.new("dk_sdfe_dvr90.tif", context) 59 | grid.delete 60 | 61 | context.set_file_api(Proj::FileApiImpl) 62 | grid.download 63 | 64 | assert(grid.downloaded?) 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /test/grid_cache_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require_relative './abstract_test' 4 | 5 | class GridCacheTest < AbstractTest 6 | def download(grid) 7 | begin 8 | grid.download 9 | ensure 10 | grid.delete 11 | end 12 | end 13 | 14 | def test_clear 15 | context = Proj::Context.new 16 | context.network_enabled = true 17 | context.cache.enabled = true 18 | 19 | cache_path = File.join(context.user_directory, "cache.db") 20 | context.cache.clear 21 | refute(File.exist?(cache_path)) 22 | end 23 | 24 | def test_disable 25 | context = Proj::Context.new 26 | cache_path = File.join(context.user_directory, "cache.db") 27 | 28 | context.network_enabled = true 29 | context.cache.clear 30 | context.cache.enabled = false 31 | refute(File.exist?(cache_path)) 32 | 33 | database = Proj::Database.new(context) 34 | grid = database.grid("au_icsm_GDA94_GDA2020_conformal.tif") 35 | 36 | # Download the file to create the cache 37 | download(grid) 38 | 39 | refute(File.exist?(cache_path)) 40 | end 41 | 42 | def test_set_path 43 | context = Proj::Context.new 44 | context.network_enabled = true 45 | database = Proj::Database.new(context) 46 | grid = database.grid("au_icsm_GDA94_GDA2020_conformal.tif") 47 | 48 | # Custom path 49 | cache_path = context.cache.path = File.join(Dir.tmpdir, "proj_cache_test.db") 50 | refute(File.exist?(cache_path)) 51 | 52 | # Download the file to create the cache 53 | download(grid) 54 | 55 | assert(File.exist?(cache_path)) 56 | 57 | context.cache.clear 58 | refute(File.exist?(cache_path)) 59 | end 60 | 61 | def test_ttl 62 | context = Proj::Context.new 63 | context.cache.ttl = 60 64 | assert(true) 65 | end 66 | 67 | def test_max_size 68 | context = Proj::Context.new 69 | context.cache.max_size = 100 70 | assert(true) 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /test/grid_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require_relative './abstract_test' 4 | 5 | class GridTest < AbstractTest 6 | def test_grid 7 | database = Proj::Database.new(Proj::Context.current) 8 | grid = database.grid("au_icsm_GDA94_GDA2020_conformal.tif") 9 | 10 | assert_equal("au_icsm_GDA94_GDA2020_conformal.tif", grid.name) 11 | assert(grid.full_name.empty?) 12 | assert(grid.package_name.empty?) 13 | assert_equal("https://cdn.proj.org/au_icsm_GDA94_GDA2020_conformal.tif", grid.url.to_s) 14 | assert(grid.downloadable?) 15 | assert(grid.open_license?) 16 | refute(grid.available?) 17 | end 18 | 19 | def test_grid_proj6_name 20 | database = Proj::Database.new(Proj::Context.current) 21 | grid = database.grid("GDA94_GDA2020_conformal.gsb") 22 | 23 | assert_equal("GDA94_GDA2020_conformal.gsb", grid.name) 24 | assert(grid.full_name.empty?) 25 | assert(grid.package_name.empty?) 26 | assert_instance_of(URI::HTTPS, grid.url) 27 | assert_equal("https://cdn.proj.org/au_icsm_GDA94_GDA2020_conformal.tif", grid.url.to_s) 28 | assert(grid.downloadable?) 29 | assert(grid.open_license?) 30 | refute(grid.available?) 31 | end 32 | 33 | def test_downloaded_network_disabled 34 | context = Proj::Context.new 35 | context.network_enabled = false 36 | 37 | grid = Proj::Grid.new("dk_sdfe_dvr90.tif", context) 38 | refute(grid.downloaded?) 39 | end 40 | 41 | def test_downloaded_network_enabled 42 | context = Proj::Context.new 43 | context.network_enabled = true 44 | 45 | grid = Proj::Grid.new("dk_sdfe_dvr90.tif", context) 46 | refute(grid.downloaded?) 47 | end 48 | 49 | def test_download 50 | context = Proj::Context.new 51 | context.network_enabled = true 52 | 53 | grid = Proj::Grid.new("dk_sdfe_dvr90.tif", context) 54 | refute(grid.path) 55 | 56 | begin 57 | grid.download 58 | assert(grid.path) 59 | assert(grid.downloaded?) 60 | ensure 61 | grid.delete 62 | end 63 | end 64 | 65 | def test_download_with_progress 66 | context = Proj::Context.new 67 | context.network_enabled = true 68 | 69 | database = Proj::Database.new(context) 70 | grid = database.grid("au_icsm_GDA94_GDA2020_conformal.tif") 71 | 72 | progress_values = Array.new 73 | begin 74 | downloaded = grid.download do |progress| 75 | progress_values << progress 76 | end 77 | assert(downloaded) 78 | ensure 79 | grid.delete 80 | end 81 | 82 | assert(progress_values.count > 1) 83 | assert(progress_values.include?(1.0)) 84 | end 85 | 86 | def test_download_with_progress_cancel 87 | context = Proj::Context.new 88 | context.network_enabled = true 89 | 90 | database = Proj::Database.new(context) 91 | grid = database.grid("au_icsm_GDA94_GDA2020_conformal.tif") 92 | 93 | progress_values = Array.new 94 | begin 95 | downloaded = grid.download do |progress| 96 | progress_values << progress 97 | # Cancel download 98 | false 99 | end 100 | refute(downloaded) 101 | ensure 102 | grid.delete 103 | end 104 | 105 | assert_equal(1, progress_values.count) 106 | refute(progress_values.include?(1.0)) 107 | end 108 | 109 | def test_grid_info 110 | context = Proj::Context.new 111 | context.network_enabled = true 112 | grid = Proj::Grid.new("dk_sdfe_dvr90.tif", context) 113 | 114 | begin 115 | assert(grid.info.filename.empty?) 116 | #assert_equal("dk_sdfe_dvr90.tif", grid.info.gridname) 117 | # assert_equal("gtiff", grid.info.format) 118 | # assert_in_delta(0, grid.info.lower_left[:lam]) 119 | # assert_in_delta(0, grid.info.lower_left[:phi]) 120 | # assert_in_delta(0, grid.info.upper_right[:lam]) 121 | # assert_in_delta(0, grid.info.upper_right[:phi]) 122 | # assert_in_delta(0, grid.info.size_lon) 123 | # assert_in_delta(0, grid.info.size_lat) 124 | # assert_in_delta(0, grid.info.cell_size_lon) 125 | # assert_in_delta(0, grid.info.cell_size_lat) 126 | ensure 127 | grid.delete 128 | end 129 | end 130 | 131 | def test_grid_invalid 132 | skip "This test sometimes raises an error and sometimes doesn't." 133 | database = Proj::Database.new(Proj::Context.current) 134 | grid = database.grid("invalid") 135 | 136 | error = assert_raises(Proj::Error) do 137 | grid = database.grid("invalid") 138 | end 139 | assert_equal("Unknown error (code 4096)", error.to_s) 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /test/network_api_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require_relative './abstract_test' 4 | 5 | class NetworkApiTest < AbstractTest 6 | def setup 7 | super 8 | # Make sure downloader callbacks are not GCed 9 | #GC.stress = true 10 | end 11 | 12 | def teardown 13 | super 14 | GC.stress = false 15 | end 16 | 17 | def test_download 18 | skip "This test causes a segfault due to the way Proj cleans up on shutdown" 19 | 20 | context = Proj::Context.new 21 | context.network_enabled = true 22 | 23 | # Create a grid 24 | grid = Proj::Grid.new("dk_sdfe_dvr90.tif", context) 25 | grid.delete 26 | 27 | context.cache.clear 28 | 29 | # Install custom network api 30 | context.set_network_api(Proj::NetworkApiImpl) 31 | 32 | conversion = Proj::Conversion.new(<<~EOS, context) 33 | +proj=pipeline 34 | +step +proj=unitconvert +xy_in=deg +xy_out=rad 35 | +step +proj=vgridshift +grids=dk_sdfe_dvr90.tif +multiplier=1 36 | +step +proj=unitconvert +xy_in=rad +xy_out=deg 37 | EOS 38 | 39 | coord = Proj::Coordinate.new(lon: 12, lat: 56, z: 0) 40 | new_coord = conversion.forward(coord) 41 | assert_in_delta(12, new_coord.lon) 42 | assert_in_delta(56, new_coord.lat) 43 | assert_in_delta(36.5909996032715, new_coord.z, 1e-10) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /test/operation_factory_context_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require_relative './abstract_test' 4 | 5 | class OperationFactoryContextTest < AbstractTest 6 | def test_create 7 | context = Proj::Context.new 8 | factory_context = Proj::OperationFactoryContext.new(context) 9 | assert(context.to_ptr) 10 | end 11 | 12 | def test_finalize 13 | 100.times do 14 | context = Proj::Context.new 15 | factory_context = Proj::OperationFactoryContext.new(context) 16 | assert(context.to_ptr) 17 | GC.start 18 | end 19 | assert(true) 20 | end 21 | 22 | def test_create_operations 23 | context = Proj::Context.new 24 | source = Proj::Crs.create_from_database("EPSG", "4267", :PJ_CATEGORY_CRS) 25 | target = Proj::Crs.create_from_database("EPSG", "4269", :PJ_CATEGORY_CRS) 26 | 27 | factory_context = Proj::OperationFactoryContext.new(context) 28 | factory_context.spatial_criterion = :PROJ_SPATIAL_CRITERION_PARTIAL_INTERSECTION 29 | factory_context.grid_availability = :PROJ_GRID_AVAILABILITY_IGNORED 30 | 31 | operations = factory_context.create_operations(source, target) 32 | assert_equal(10, operations.count) 33 | 34 | operation = operations[0] 35 | assert_equal("NAD27 to NAD83 (4)", operation.name) 36 | refute(operation.ballpark_transformation?) 37 | end 38 | 39 | def test_suggested_operation 40 | context = Proj::Context.new 41 | source = Proj::Crs.create_from_database("EPSG", "4267", :PJ_CATEGORY_CRS) 42 | target = Proj::Crs.create_from_database("EPSG", "4269", :PJ_CATEGORY_CRS) 43 | 44 | factory_context = Proj::OperationFactoryContext.new(context) 45 | factory_context.spatial_criterion = :PROJ_SPATIAL_CRITERION_PARTIAL_INTERSECTION 46 | factory_context.grid_availability = :PROJ_GRID_AVAILABILITY_IGNORED 47 | 48 | operations = factory_context.create_operations(source, target) 49 | 50 | coord = Proj::Coordinate.new(x: 40, y: -100) 51 | index = operations.suggested_operation(:PJ_FWD, coord) 52 | 53 | expected = case 54 | when Proj::Api::PROJ_VERSION >= '9.3.0' 55 | 3 56 | when Proj::Api::PROJ_VERSION >= '9.0.0' 57 | 2 58 | else 59 | 7 60 | end 61 | assert_equal(expected, index) 62 | 63 | operation = operations[index] 64 | 65 | expected = case 66 | when Proj::Api::PROJ_VERSION >= '9.3.0' 67 | "NAD27 to NAD83 (7)" 68 | when Proj::Api::PROJ_VERSION >= '9.0.0' 69 | "NAD27 to NAD83 (1)" 70 | else 71 | "Ballpark geographic offset from NAD27 to NAD83" 72 | end 73 | 74 | assert_equal(expected, operation.name) 75 | end 76 | 77 | def test_ballpark_transformations 78 | context = Proj::Context.new 79 | source = Proj::Crs.create_from_database("EPSG", "4267", :PJ_CATEGORY_CRS) 80 | target = Proj::Crs.create_from_database("EPSG", "4258", :PJ_CATEGORY_CRS) 81 | 82 | factory_context = Proj::OperationFactoryContext.new(context) 83 | factory_context.spatial_criterion = :PROJ_SPATIAL_CRITERION_PARTIAL_INTERSECTION 84 | factory_context.grid_availability = :PROJ_GRID_AVAILABILITY_IGNORED 85 | 86 | # Allowed implicitly 87 | operations = factory_context.create_operations(source, target) 88 | assert_equal(1, operations.count) 89 | 90 | # Allow explicitly 91 | factory_context.ballpark_transformations = true 92 | operations = factory_context.create_operations(source, target) 93 | assert_equal(1, operations.count) 94 | 95 | # Disallow 96 | factory_context.ballpark_transformations = false 97 | operations = factory_context.create_operations(source, target) 98 | assert_equal(0, operations.count) 99 | end 100 | 101 | def test_desired_accuracy 102 | context = Proj::Context.new 103 | factory_context = Proj::OperationFactoryContext.new(context) 104 | factory_context.desired_accuracy = 5 105 | end 106 | 107 | def test_set_area_of_interest 108 | context = Proj::Context.new 109 | factory_context = Proj::OperationFactoryContext.new(context) 110 | factory_context.set_area_of_interest(10, 10, 10, 10) 111 | end 112 | 113 | def test_crs_extent_use 114 | context = Proj::Context.new 115 | factory_context = Proj::OperationFactoryContext.new(context) 116 | factory_context.crs_extent_use = :PJ_CRS_EXTENT_SMALLEST 117 | end 118 | 119 | def test_spatial_criterion 120 | context = Proj::Context.new 121 | factory_context = Proj::OperationFactoryContext.new(context) 122 | factory_context.spatial_criterion = :PROJ_SPATIAL_CRITERION_STRICT_CONTAINMENT 123 | end 124 | 125 | def test_grid_availability 126 | context = Proj::Context.new 127 | factory_context = Proj::OperationFactoryContext.new(context) 128 | factory_context.grid_availability = :PROJ_GRID_AVAILABILITY_USE 129 | end 130 | 131 | def test_use_proj_alternative_grid_names 132 | context = Proj::Context.new 133 | factory_context = Proj::OperationFactoryContext.new(context) 134 | factory_context.use_proj_alternative_grid_names = true 135 | end 136 | 137 | def test_allow_use_intermediate_crs 138 | context = Proj::Context.new 139 | # There is no direct transformations between both 140 | source = Proj::Crs.create_from_database("EPSG", "4230", :PJ_CATEGORY_CRS) 141 | target = Proj::Crs.create_from_database("EPSG", "4171", :PJ_CATEGORY_CRS) 142 | 143 | # Default behavior: allow any pivot 144 | factory_context = Proj::OperationFactoryContext.new(context) 145 | operations = factory_context.create_operations(source, target) 146 | assert_equal(1, operations.count) 147 | 148 | operation = operations[0] 149 | assert_equal("ED50 to ETRS89 (10) + Inverse of RGF93 v1 to ETRS89 (1)", operation.name) 150 | refute(operation.ballpark_transformation?) 151 | 152 | # Disallow pivots 153 | factory_context.allow_use_intermediate_crs = :PROJ_INTERMEDIATE_CRS_USE_NEVER 154 | operations = factory_context.create_operations(source, target) 155 | assert_equal(1, operations.count) 156 | 157 | operation = operations[0] 158 | assert_equal("Ballpark geographic offset from ED50 to RGF93 v1", operation.name) 159 | assert(operation.ballpark_transformation?) 160 | end 161 | 162 | def test_allowed_intermediate_crs 163 | context = Proj::Context.new 164 | 165 | # There is no direct transformations between both 166 | source = Proj::Crs.create_from_database("EPSG", "4230", :PJ_CATEGORY_CRS) 167 | target = Proj::Crs.create_from_database("EPSG", "4171", :PJ_CATEGORY_CRS) 168 | 169 | # Restrict pivot to ETRS89 170 | factory_context = Proj::OperationFactoryContext.new(context, authority: "EPSG") 171 | factory_context.allowed_intermediate_crs = ["EPSG", "4258"] 172 | 173 | operations = factory_context.create_operations(source, target) 174 | assert_equal(1, operations.count) 175 | 176 | operation = operations[0] 177 | assert_equal("ED50 to ETRS89 (10) + Inverse of RGF93 v1 to ETRS89 (1)", operation.name) 178 | end 179 | 180 | def test_discard_superseded 181 | context = Proj::Context.new 182 | source = Proj::Crs.create_from_database("EPSG", "4203", :PJ_CATEGORY_CRS) 183 | target = Proj::Crs.create_from_database("EPSG", "4326", :PJ_CATEGORY_CRS) 184 | 185 | factory_context = Proj::OperationFactoryContext.new(context) 186 | factory_context.spatial_criterion = :PROJ_SPATIAL_CRITERION_PARTIAL_INTERSECTION 187 | factory_context.grid_availability = :PROJ_GRID_AVAILABILITY_IGNORED 188 | 189 | factory_context.discard_superseded = true 190 | operations = factory_context.create_operations(source, target) 191 | assert_equal(4, operations.count) 192 | 193 | factory_context.discard_superseded = false 194 | operations = factory_context.create_operations(source, target) 195 | assert_equal(5, operations.count) 196 | end 197 | 198 | if Proj::Api::PROJ_VERSION >= '9.0.0' 199 | def test_set_area_of_interest_name 200 | context = Proj::Context.new 201 | factory_context = Proj::OperationFactoryContext.new(context) 202 | factory_context.area_of_interest_name = 'test' 203 | end 204 | end 205 | end 206 | -------------------------------------------------------------------------------- /test/operation_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require_relative './abstract_test' 4 | 5 | class OperationTest < AbstractTest 6 | def test_get_all 7 | operations = Proj::Operation.list.map {|operation| operation.id} 8 | assert(operations.include?('aea')) 9 | assert(operations.include?('wintri')) 10 | end 11 | 12 | def test_one 13 | operation = Proj::Operation.get('rouss') 14 | assert_kind_of(Proj::Operation, operation) 15 | assert_equal('rouss', operation.id) 16 | assert_equal("Roussilhe Stereographic\n\tAzi, Ell", operation.description) 17 | end 18 | 19 | def test_equal 20 | e1 = Proj::Operation.get('rouss') 21 | e2 = Proj::Operation.get('rouss') 22 | assert(e1 == e2) 23 | end 24 | 25 | def test_failed_get 26 | operation = Proj::Operation.get('foo') 27 | assert_nil(operation) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/parameters_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require_relative './abstract_test' 4 | 5 | class ParametersTest < AbstractTest 6 | def test_types_nil 7 | params = Proj::Parameters.new 8 | assert(params.types.empty?) 9 | end 10 | 11 | def test_types_one 12 | types = [:PJ_TYPE_GEODETIC_CRS] 13 | params = Proj::Parameters.new 14 | params.types = types 15 | assert_equal(types, params.types) 16 | end 17 | 18 | def test_types_many 19 | types = [:PJ_TYPE_GEODETIC_CRS, :PJ_TYPE_GEOCENTRIC_CRS, :PJ_TYPE_GEOGRAPHIC_CRS] 20 | params = Proj::Parameters.new 21 | params.types = types 22 | assert_equal(types, params.types) 23 | end 24 | 25 | def test_bbox_valid 26 | params = Proj::Parameters.new 27 | refute(params.bbox_valid) 28 | 29 | params.bbox_valid = true 30 | assert(params.bbox_valid) 31 | end 32 | 33 | def test_allow_deprecated 34 | params = Proj::Parameters.new 35 | refute(params.allow_deprecated) 36 | 37 | params.allow_deprecated = true 38 | assert(params.allow_deprecated) 39 | end 40 | end -------------------------------------------------------------------------------- /test/pj_object_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require_relative './abstract_test' 4 | class PjObjectTest < AbstractTest 5 | def test_clone 6 | object = Proj::PjObject.create("+proj=longlat") 7 | clone = object.clone 8 | assert(object.equivalent_to?(clone, :PJ_COMP_STRICT)) 9 | assert(object.context.equal?(clone.context)) 10 | end 11 | 12 | def test_dup 13 | object = Proj::PjObject.create("+proj=longlat") 14 | clone = object.dup 15 | assert(object.equivalent_to?(clone, :PJ_COMP_STRICT)) 16 | assert(object.context.equal?(clone.context)) 17 | end 18 | 19 | def test_equivalent 20 | from_epsg = Proj::PjObject.create_from_database("EPSG", "7844", :PJ_CATEGORY_CRS) 21 | from_wkt = Proj::PjObject.create_from_wkt(<<~EOS) 22 | GEOGCRS["GDA2020", 23 | DATUM["GDA2020", 24 | ELLIPSOID["GRS_1980",6378137,298.257222101, 25 | LENGTHUNIT["metre",1]]], 26 | PRIMEM["Greenwich",0, 27 | ANGLEUNIT["Degree",0.0174532925199433]], 28 | CS[ellipsoidal,2], 29 | AXIS["geodetic latitude (Lat)",north, 30 | ORDER[1], 31 | ANGLEUNIT["degree",0.0174532925199433]], 32 | AXIS["geodetic longitude (Lon)",east, 33 | ORDER[2], 34 | ANGLEUNIT["degree",0.0174532925199433]]]" 35 | EOS 36 | 37 | assert(from_epsg.equivalent_to?(from_wkt, :PJ_COMP_EQUIVALENT)) 38 | end 39 | 40 | def test_accuracy_crs 41 | object = Proj::PjObject.create_from_database("EPSG", "4326", :PJ_CATEGORY_CRS) 42 | assert_equal(-1, object.accuracy) 43 | end 44 | 45 | def test_accuracy_coordinate_operation 46 | object = Proj::PjObject.create_from_database("EPSG", "1170", :PJ_CATEGORY_COORDINATE_OPERATION) 47 | assert_equal(16.0, object.accuracy) 48 | end 49 | 50 | def test_accuracy_projection 51 | object = Proj::Conversion.new("+proj=helmert") 52 | assert_equal(-1.0, object.accuracy) 53 | end 54 | 55 | def test_id_code 56 | crs = Proj::Crs.new('EPSG:4326') 57 | assert_equal("4326", crs.id_code) 58 | refute(crs.id_code(1)) 59 | end 60 | 61 | def test_remarks_transformation 62 | transformation = Proj::PjObject.create_from_database("EPSG", "8048", :PJ_CATEGORY_COORDINATE_OPERATION) 63 | 64 | expected = "Scale difference in ppb where 1/billion = 1E-9. See CT codes 8444-46 for NTv2 method giving equivalent results for Christmas Island, Cocos Islands and Australia respectively. See CT code 8447 for alternative including distortion model for Australia only." 65 | assert_equal(expected, transformation.remarks) 66 | end 67 | 68 | def test_remarks_conversion 69 | operation = Proj::PjObject.create_from_database("EPSG", "3811", :PJ_CATEGORY_COORDINATE_OPERATION) 70 | 71 | expected = "Replaces Lambert 2005." 72 | assert_equal(expected, operation.remarks) 73 | end 74 | 75 | def test_scope_transformation 76 | transformation = Proj::PjObject.create_from_database("EPSG", "8048", :PJ_CATEGORY_COORDINATE_OPERATION) 77 | 78 | expected = "Transformation of GDA94 coordinates that have been derived through GNSS CORS." 79 | assert_equal(expected, transformation.scope) 80 | end 81 | 82 | def test_scope_conversion 83 | operation = Proj::PjObject.create_from_database("EPSG", "3811", :PJ_CATEGORY_COORDINATE_OPERATION) 84 | 85 | expected = "Engineering survey, topographic mapping." 86 | assert_equal(expected, operation.scope) 87 | end 88 | 89 | def test_scope_invalid 90 | operation = Proj::Conversion.new("+proj=noop") 91 | refute(operation.scope) 92 | end 93 | 94 | def test_factors 95 | conversion = Proj::Conversion.new("+proj=merc +ellps=WGS84") 96 | coord = Proj::Coordinate.new(lon: Proj.degrees_to_radians(12), lat: Proj.degrees_to_radians(55)) 97 | factors = conversion.factors(coord) 98 | 99 | assert_in_delta(1.739526610076288, factors[:meridional_scale], 1e-7) 100 | assert_in_delta(1.739526609938368, factors[:parallel_scale], 1e-7) 101 | assert_in_delta(3.0259528269235867, factors[:areal_scale], 1e-7) 102 | 103 | assert_in_delta(0.0, factors[:angular_distortion], 1e-7) 104 | assert_in_delta(1.5707963267948966, factors[:meridian_parallel_angle], 1e-7) 105 | assert_in_delta(0.0, factors[:meridian_convergence], 1e-7) 106 | 107 | assert_in_delta(1.7395266100073281, factors[:tissot_semimajor], 1e-7) 108 | assert_in_delta(1.7395266100073281, factors[:tissot_semiminor], 1e-7) 109 | 110 | assert_in_delta(0.9999999999996122, factors[:dx_dlam], 1e-7) 111 | assert_in_delta(0.0, factors[:dx_dphi], 1e-7) 112 | assert_in_delta(0.0, factors[:dy_dlam], 1e-7) 113 | assert_in_delta(1.7395897312200146, factors[:dy_dphi], 1e-7) 114 | end 115 | 116 | def test_create_from_name 117 | context = Proj::Context.new 118 | objects = Proj::PjObject.create_from_name("WGS 84", context) 119 | assert_equal(5, objects.size) 120 | end 121 | 122 | def test_create_from_name_with_auth_name 123 | context = Proj::Context.new 124 | objects = Proj::PjObject.create_from_name("WGS 84", context, auth_name: "xx") 125 | assert_equal(0, objects.size) 126 | end 127 | 128 | def test_create_from_name_with_types 129 | context = Proj::Context.new 130 | objects = Proj::PjObject.create_from_name("WGS 84", context, types: [:PJ_TYPE_GEODETIC_CRS, :PJ_TYPE_PROJECTED_CRS]) 131 | assert_equal(3, objects.size) 132 | end 133 | 134 | def test_create_from_name_with_types_and_approximate_match 135 | context = Proj::Context.new 136 | objects = Proj::PjObject.create_from_name("WGS 84", context, approximate_match: true, 137 | types: [:PJ_TYPE_GEODETIC_CRS, :PJ_TYPE_PROJECTED_CRS]) 138 | 139 | expected = case 140 | when Proj::Api::PROJ_VERSION >= '9.3.0' 141 | 443 142 | when Proj::Api::PROJ_VERSION >= '9.0.0' 143 | 442 144 | else 145 | 440 146 | end 147 | 148 | assert_equal(expected, objects.size) 149 | end 150 | 151 | def test_create_from_name_with_types_and_approximate_match_and_limit 152 | context = Proj::Context.new 153 | objects = Proj::PjObject.create_from_name("WGS 84", context, approximate_match: true, limit: 25, 154 | types: [:PJ_TYPE_GEODETIC_CRS, :PJ_TYPE_PROJECTED_CRS]) 155 | assert_equal(25, objects.size) 156 | end 157 | 158 | def test_deprecated_true 159 | wkt = <<~EOS 160 | GEOGCRS["SAD69 (deprecated)", 161 | DATUM["South_American_Datum_1969", 162 | ELLIPSOID["GRS 1967",6378160,298.247167427, 163 | LENGTHUNIT["metre",1, 164 | ID["EPSG",9001]]]], 165 | PRIMEM["Greenwich",0, 166 | ANGLEUNIT["degree",0.0174532925199433, 167 | ID["EPSG",9122]]], 168 | CS[ellipsoidal,2], 169 | AXIS["latitude",north, 170 | ORDER[1], 171 | ANGLEUNIT["degree",0.0174532925199433, 172 | ID["EPSG",9122]]], 173 | AXIS["longitude",east, 174 | ORDER[2], 175 | ANGLEUNIT["degree",0.0174532925199433, 176 | ID["EPSG",9122]]]] 177 | EOS 178 | 179 | crs = Proj::Crs.create(wkt) 180 | assert(crs.deprecated?) 181 | end 182 | 183 | def test_deprecated_false 184 | crs = Proj::Crs.create_from_database("EPSG", "4326", :PJ_CATEGORY_CRS) 185 | refute(crs.deprecated?) 186 | end 187 | end 188 | -------------------------------------------------------------------------------- /test/prime_meridian_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require_relative './abstract_test' 4 | 5 | class PrimeMeridianTest < AbstractTest 6 | def parameter_crs 7 | wkt = <<~EOS 8 | PROJCS["WGS 84 / UTM zone 31N", 9 | GEOGCS["WGS 84", 10 | DATUM["WGS_1984", 11 | SPHEROID["WGS 84",6378137,298.257223563, 12 | AUTHORITY["EPSG","7030"]], 13 | AUTHORITY["EPSG","6326"]], 14 | PRIMEM["Greenwich",0, 15 | AUTHORITY["EPSG","8901"]], 16 | UNIT["degree",0.0174532925199433, 17 | AUTHORITY["EPSG","9122"]], 18 | AUTHORITY["EPSG","4326"]], 19 | PROJECTION["Transverse_Mercator"], 20 | PARAMETER["latitude_of_origin",0], 21 | PARAMETER["central_meridian",3], 22 | PARAMETER["scale_factor",0.9996], 23 | PARAMETER["false_easting",500000], 24 | PARAMETER["false_northing",0], 25 | UNIT["metre",1, 26 | AUTHORITY["EPSG","9001"]], 27 | AXIS["Easting",EAST], 28 | AXIS["Northing",NORTH], 29 | AUTHORITY["EPSG","32631"]] 30 | EOS 31 | Proj::Crs.new(wkt) 32 | end 33 | 34 | def test_built_in 35 | prime_meridians = Proj::PrimeMeridian.built_in.map {|prime_merdian| prime_merdian[:id] } 36 | assert(prime_meridians.include?('greenwich')) 37 | assert(prime_meridians.include?('athens')) 38 | assert(prime_meridians.include?('lisbon')) 39 | assert(prime_meridians.include?('rome')) 40 | end 41 | 42 | def test_from_database 43 | meridian = Proj::PrimeMeridian.create_from_database("EPSG", "8903", :PJ_CATEGORY_PRIME_MERIDIAN) 44 | assert_instance_of(Proj::PrimeMeridian, meridian) 45 | assert_equal(:PJ_TYPE_PRIME_MERIDIAN, meridian.proj_type) 46 | assert_equal("EPSG", meridian.auth_name) 47 | assert_equal("Paris", meridian.name) 48 | assert_equal("8903", meridian.id_code) 49 | end 50 | 51 | def test_parameters 52 | meridian = parameter_crs.prime_meridian 53 | params = meridian.parameters 54 | 55 | expected = {longitude: 0.0, 56 | unit_conv_factor: 0.017453292519943295, 57 | unit_name: "degree"} 58 | 59 | assert_equal(expected, params) 60 | end 61 | 62 | def test_longitude 63 | meridian = parameter_crs.prime_meridian 64 | assert_equal(0.0, meridian.longitude) 65 | end 66 | 67 | def test_unit_conv_factor 68 | meridian = parameter_crs.prime_meridian 69 | assert_equal(0.017453292519943295, meridian.unit_conv_factor) 70 | end 71 | 72 | def test_unit_name 73 | meridian = parameter_crs.prime_meridian 74 | assert_equal("degree", meridian.unit_name) 75 | end 76 | end -------------------------------------------------------------------------------- /test/proj_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require_relative './abstract_test' 4 | 5 | class ProjTest < AbstractTest 6 | def test_info 7 | info = Proj.info 8 | assert([4,6,7,8,9].include?(info[:major])) 9 | assert([0,1,2,3].include?(info[:minor])) 10 | assert([0,1,2,3].include?(info[:patch])) 11 | refute_nil(info[:release]) 12 | refute_nil(info[:searchpath]) 13 | assert(info[:paths].null?) 14 | assert(0, info[:path_count]) 15 | end 16 | 17 | def test_search_paths 18 | search_paths = Proj.search_paths 19 | assert_instance_of(Array, search_paths) 20 | refute(search_paths.empty?) 21 | end 22 | 23 | def test_init_file_info 24 | info = Proj.init_file_info("EPSG") 25 | assert_equal("EPSG", info[:name].to_ptr.read_string) 26 | assert(info[:filename].to_ptr.read_string.empty?) 27 | # Info returns gibberish hex values, need to look at Proj source code 28 | # and see what is going on 29 | #assert_equal("epsg", info[:version].to_ptr.read_string) 30 | #assert_equal("", info[:origin].to_ptr.read_string) 31 | #assert_equal("", info[:lastupdate].to_ptr.read_string) 32 | end 33 | 34 | def test_version 35 | assert_instance_of(Gem::Version, Proj::Api::PROJ_VERSION) 36 | assert(Proj::Api::PROJ_VERSION.canonical_segments.first >= 5) 37 | end 38 | 39 | def test_degrees_to_radians 40 | radians = Proj.degrees_to_radians(180) 41 | assert_equal(Math::PI, radians) 42 | end 43 | 44 | def test_radians_to_degrees 45 | degrees = Proj.radians_to_degrees(-Math::PI) 46 | assert_equal(-180, degrees) 47 | end 48 | 49 | def test_degrees_minutes_seconds_to_radians 50 | value = "19°46'27\"E" 51 | radians = Proj.degrees_minutes_seconds_to_radians(value) 52 | assert_in_delta(0.34512432, radians, 1e-7) 53 | end 54 | 55 | def test_radians_to_degrees_minutes_seconds 56 | result = Proj.radians_to_degrees_minutes_seconds(Math::PI) 57 | assert_equal("180dN", result) 58 | end 59 | end -------------------------------------------------------------------------------- /test/session_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require_relative './abstract_test' 4 | 5 | class SessionTest < AbstractTest 6 | def teardown 7 | GC.start 8 | end 9 | 10 | def create_crs 11 | Proj::Crs.new(<<~EOS, Proj::Context.new) 12 | GEOGCRS["myGDA2020", 13 | DATUM["GDA2020", 14 | ELLIPSOID["GRS_1980",6378137,298.257222101, 15 | LENGTHUNIT["metre",1]]], 16 | PRIMEM["Greenwich",0, 17 | ANGLEUNIT["Degree",0.0174532925199433]], 18 | CS[ellipsoidal,2], 19 | AXIS["geodetic latitude (Lat)",north, 20 | ORDER[1], 21 | ANGLEUNIT["degree",0.0174532925199433]], 22 | AXIS["geodetic longitude (Lon)",east, 23 | ORDER[2], 24 | ANGLEUNIT["degree",0.0174532925199433]]] 25 | EOS 26 | end 27 | 28 | def test_create_session 29 | session = Proj::Session.new 30 | assert(session) 31 | end 32 | 33 | def test_finalize 34 | 100.times do 35 | crs = Proj::Session.new(Proj::Context.new) 36 | assert(crs.to_ptr) 37 | GC.start 38 | end 39 | assert(true) 40 | end 41 | 42 | def test_insert_statements 43 | crs = create_crs 44 | session = Proj::Session.new(crs.context) 45 | statements = session.get_insert_statements(crs, "HOBU", "XXXX") 46 | assert_equal(4, statements.count) 47 | 48 | expected = "INSERT INTO geodetic_datum VALUES('HOBU','GEODETIC_DATUM_XXXX','GDA2020','','EPSG','7019','EPSG','8901',NULL,NULL,NULL,NULL,0);" 49 | assert_equal(expected, statements[0]) 50 | end 51 | 52 | def test_insert_statements_empty_authorities 53 | crs = create_crs 54 | 55 | session = Proj::Session.new(crs.context) 56 | statements = session.get_insert_statements(crs, "HOBU", "XXXX", false, []) 57 | assert_equal(6, statements.count) 58 | end 59 | 60 | def test_insert_statements_authorities_proj 61 | crs = create_crs 62 | 63 | session = Proj::Session.new(crs.context) 64 | statements = session.get_insert_statements(crs, "HOBU", "XXXX", false, ['EPSG']) 65 | assert_equal(4, statements.count) 66 | end 67 | 68 | def test_insert_twice 69 | crs = create_crs 70 | 71 | session = Proj::Session.new(crs.context) 72 | statements = session.get_insert_statements(crs, "HOBU", "XXXX") 73 | assert_equal(4, statements.count) 74 | 75 | statements = session.get_insert_statements(crs, "HOBU", "XXXX") 76 | assert_equal(0, statements.count) 77 | end 78 | end -------------------------------------------------------------------------------- /test/transformation_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require_relative './abstract_test' 4 | 5 | class TransformationTest < AbstractTest 6 | PRECISION = 1.5 7 | 8 | def setup 9 | @crs_wgs84 = Proj::Crs.new('EPSG:4326') 10 | @crs_gk = Proj::Crs.new('EPSG:31467') 11 | end 12 | 13 | def test_create_from_strings 14 | transform = Proj::Transformation.new('EPSG:31467', 'EPSG:4326') 15 | assert(transform.info) 16 | end 17 | 18 | def test_create_from_crs 19 | transform = Proj::Transformation.new(@crs_wgs84, @crs_gk) 20 | assert(transform.info) 21 | end 22 | 23 | def test_create 24 | context = Proj::Context.new 25 | coordinate_system = Proj::CoordinateSystem.create_ellipsoidal_2d(:PJ_ELLPS2D_LONGITUDE_LATITUDE, context) 26 | source_crs = Proj::Crs.create_geographic(context, 27 | name: "Source CRS", 28 | datum_name: "World Geodetic System 1984", 29 | ellipsoid_name: "WGS 84", 30 | semi_major_meter: 6378137, 31 | inv_flattening: 298.257223563, 32 | prime_meridian_name: "Greenwich", 33 | prime_meridian_offset: 0.0, 34 | pm_angular_units: "Degree", 35 | pm_angular_units_conv: 0.0174532925199433, 36 | coordinate_system: coordinate_system) 37 | 38 | target_crs = Proj::Crs.create_geographic(context, 39 | name: "WGS 84", 40 | datum_name: "World Geodetic System 1984", 41 | ellipsoid_name: "WGS 84", 42 | semi_major_meter: 6378137, 43 | inv_flattening: 298.257223563, 44 | prime_meridian_name: "Greenwich", 45 | prime_meridian_offset: 0.0, 46 | pm_angular_units: "Degree", 47 | pm_angular_units_conv: 0.0174532925199433, 48 | coordinate_system: coordinate_system) 49 | 50 | interp_crs = Proj::Crs.create_geographic(context, 51 | name: "Interpolation CRS", 52 | datum_name: "World Geodetic System 1984", 53 | ellipsoid_name: "WGS 84", 54 | semi_major_meter: 6378137, 55 | inv_flattening: 298.257223563, 56 | prime_meridian_name: "Greenwich", 57 | prime_meridian_offset: 0.0, 58 | pm_angular_units: "Degree", 59 | pm_angular_units_conv: 0.0174532925199433, 60 | coordinate_system: coordinate_system) 61 | 62 | param = Proj::Parameter.new(name: "param name", value: 0.99, 63 | unit_conv_factor: 1.0, unit_type: :PJ_UT_SCALE) 64 | 65 | transformation = Proj::Transformation.create(context, name: "transf", auth_name: "transf auth", code: "transf code", 66 | source_crs: source_crs, target_crs: target_crs, interpolation_crs: interp_crs, 67 | method_name: "method", method_auth_name: "method_auth", method_code: "1", 68 | params: [param], accuracy: 0) 69 | 70 | assert_equal(1, transformation.param_count) 71 | 72 | assert(source_crs.equivalent_to?(transformation.source_crs, :PJ_COMP_STRICT)) 73 | assert(target_crs.equivalent_to?(transformation.target_crs, :PJ_COMP_STRICT)) 74 | end 75 | 76 | # echo "3458305 5428192" | cs2cs -f '%.10f' +init=epsg:31467 +to +init=epsg:4326 - 77 | def test_gk_to_wgs84_forward 78 | transform = Proj::Transformation.new(@crs_gk, @crs_wgs84) 79 | from = Proj::Coordinate.new(x: 5428192.0, y: 3458305.0, z: -5.1790915237) 80 | to = transform.forward(from) 81 | 82 | assert_in_delta(48.98963932450735, to.x, PRECISION) 83 | assert_in_delta(8.429263044355544, to.y, PRECISION) 84 | assert_in_delta(-5.1790915237, to.z, PRECISION) 85 | assert_in_delta(0, to.t, PRECISION) 86 | end 87 | 88 | def test_gk_to_wgs84_inverse 89 | transform = Proj::Transformation.new(@crs_gk, @crs_wgs84) 90 | from = Proj::Coordinate.new(lam: 48.9906726079, phi: 8.4302123334) 91 | to = transform.inverse(from) 92 | 93 | assert_in_delta(5428306, to.x, PRECISION) 94 | assert_in_delta(3458375, to.y, PRECISION) 95 | assert_in_delta(0, to.z, PRECISION) 96 | assert_in_delta(0, to.t, PRECISION) 97 | end 98 | 99 | # echo "8.4293092923 48.9896114523" | cs2cs -f '%.10f' +init=epsg:4326 +to +init=epsg:31467 - 100 | def test_wgs84_to_gk_forward 101 | transform = Proj::Transformation.new(@crs_wgs84, @crs_gk) 102 | from = Proj::Coordinate.new(lam: 48.9906726079, phi: 8.4302123334) 103 | to = transform.forward(from) 104 | 105 | assert_in_delta(5428306, to.x, PRECISION) 106 | assert_in_delta(3458375, to.y, PRECISION) 107 | assert_in_delta(0, to.z, PRECISION) 108 | assert_in_delta(0, to.t, PRECISION) 109 | end 110 | 111 | def test_wgs84_to_gk_forward_inverse 112 | transform = Proj::Transformation.new(@crs_wgs84, @crs_gk) 113 | from = Proj::Coordinate.new(x: 5428192.0, y: 3458305.0, z: -5.1790915237) 114 | to = transform.inverse(from) 115 | 116 | assert_in_delta(48.98963932450735, to.x, PRECISION) 117 | assert_in_delta(8.429263044355544, to.y, PRECISION) 118 | assert_in_delta(-5.1790915237, to.z, PRECISION) 119 | assert_in_delta(0, to.t, PRECISION) 120 | end 121 | 122 | def test_with_area 123 | area = Proj::Area.new(west_lon_degree: -114.1324, south_lat_degree: 49.5614, 124 | east_lon_degree: 3.76488, north_lat_degree: 62.1463) 125 | transformation = Proj::Transformation.new("EPSG:4277", "EPSG:4326", area: area) 126 | 127 | coordinate1 = Proj::Coordinate.new(x: 50, y: -2, z: 0, t: Float::INFINITY) 128 | coordinate2 = transformation.forward(coordinate1) 129 | 130 | assert_in_delta(50.00065628, coordinate2.x, 1e-4) 131 | assert_in_delta(-2.00133989, coordinate2.y, 1e-4) 132 | end 133 | 134 | def test_accuracy_filter 135 | src = Proj::Crs.new("EPSG:4326") 136 | dst = Proj::Crs.new("EPSG:4258") 137 | 138 | error = assert_raises(Proj::Error) do 139 | Proj::Transformation.new(src, dst, accuracy: 0.05) 140 | end 141 | assert_equal("No operation found matching criteria", error.to_s) 142 | end 143 | 144 | def test_ballpark_filter 145 | src = Proj::Crs.new("EPSG:4267") 146 | dst = Proj::Crs.new("EPSG:4258") 147 | 148 | error = assert_raises(Proj::Error) do 149 | Proj::Transformation.new(src, dst, allow_ballpark: false) 150 | end 151 | assert_equal("No operation found matching criteria", error.to_s) 152 | end 153 | 154 | if Proj::Api::PROJ_VERSION >= '8.0.0' 155 | def test_transform_bounds 156 | transform = Proj::Transformation.new("EPSG:4326", 157 | "+proj=laea +lat_0=45 +lon_0=-100 +x_0=0 +y_0=0 +a=6370997 +b=6370997 +units=m +no_defs") 158 | 159 | start_bounds = Proj::Bounds.new(40, -120, 64, -80) 160 | end_bounds = transform.transform_bounds(start_bounds, :PJ_FWD, 0) 161 | 162 | assert_equal(-1684649.4133828662, end_bounds.xmin) 163 | assert_equal(-350356.8137658477, end_bounds.ymin) 164 | assert_equal(1684649.4133828674, end_bounds.xmax) 165 | assert_equal(2234551.1855909275, end_bounds.ymax) 166 | end 167 | 168 | def test_transform_bounds_normalized 169 | transform = Proj::Transformation.new("EPSG:4326", 170 | "+proj=laea +lat_0=45 +lon_0=-100 +x_0=0 +y_0=0 +a=6370997 +b=6370997 +units=m +no_defs") 171 | 172 | normalized = transform.normalize_for_visualization 173 | 174 | start_bounds = Proj::Bounds.new(-120, 40, -80, 64) 175 | end_bounds = normalized.transform_bounds(start_bounds, :PJ_FWD, 100) 176 | 177 | assert_equal(-1684649.4133828662, end_bounds.xmin) 178 | assert_equal(-555777.7923351025, end_bounds.ymin) 179 | assert_equal(1684649.4133828674, end_bounds.xmax) 180 | assert_equal(2234551.1855909275, end_bounds.ymax) 181 | end 182 | 183 | def test_transform_bounds_densify 184 | transform = Proj::Transformation.new("EPSG:4326", 185 | "+proj=laea +lat_0=45 +lon_0=-100 +x_0=0 +y_0=0 +a=6370997 +b=6370997 +units=m +no_defs") 186 | 187 | start_bounds = Proj::Bounds.new(40, -120, 64, -80) 188 | end_bounds = transform.transform_bounds(start_bounds, :PJ_FWD, 100) 189 | 190 | assert_equal(-1684649.4133828662, end_bounds.xmin) 191 | assert_equal(-555777.7923351025, end_bounds.ymin) 192 | assert_equal(1684649.4133828674, end_bounds.xmax) 193 | assert_equal(2234551.1855909275, end_bounds.ymax) 194 | end 195 | 196 | def test_instantiable 197 | operation = Proj::Conversion.create_from_database("EPSG", "1671", :PJ_CATEGORY_COORDINATE_OPERATION) 198 | assert(operation.instantiable?) 199 | end 200 | 201 | def test_steps_not_concatenated 202 | operation = Proj::Conversion.create_from_database("EPSG", "8048", :PJ_CATEGORY_COORDINATE_OPERATION) 203 | assert_instance_of(Proj::Transformation, operation) 204 | assert_equal(:PJ_TYPE_TRANSFORMATION, operation.proj_type) 205 | 206 | assert_equal(0, operation.step_count) 207 | refute(operation.step(0)) 208 | end 209 | end 210 | end -------------------------------------------------------------------------------- /test/unit_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require_relative './abstract_test' 4 | 5 | class UnitsTest < AbstractTest 6 | def test_get_all 7 | database = Proj::Database.new(Proj::Context.current) 8 | units = database.units 9 | assert_equal(91, units.count) 10 | 11 | unit = units[0] 12 | assert_instance_of(Proj::Unit, unit) 13 | end 14 | 15 | def test_builtin 16 | units = Proj::Unit.built_in 17 | assert_equal(24, units.count) 18 | 19 | unit = units[0] 20 | assert_instance_of(Proj::Unit, unit) 21 | end 22 | 23 | def test_linear_unit 24 | database = Proj::Database.new(Proj::Context.current) 25 | units = database.units(category: "linear") 26 | assert_equal(52, units.count) 27 | 28 | unit = units[0] 29 | assert_instance_of(Proj::Unit, unit) 30 | assert_equal('millimetre', unit.name) 31 | assert_equal('millimetre', unit.to_s) 32 | assert_equal('mm', unit.proj_short_name) 33 | assert_equal(0.001, unit.conv_factor) 34 | assert_equal('EPSG', unit.auth_name) 35 | assert_equal('#', unit.inspect) 36 | end 37 | 38 | def test_angular_unit 39 | database = Proj::Database.new(Proj::Context.current) 40 | units = database.units(category: "angular") 41 | assert_equal(22, units.count) 42 | 43 | unit = units[0] 44 | assert_equal('milliarc-second', unit.name) 45 | assert_equal('milliarc-second', unit.to_s) 46 | refute(unit.proj_short_name) 47 | assert_in_delta(4.84813681109536e-09, unit.conv_factor, 0.0001) 48 | assert_equal('EPSG', unit.auth_name) 49 | assert_equal('#', unit.inspect) 50 | end 51 | 52 | def test_compare 53 | database = Proj::Database.new(Proj::Context.current) 54 | unit_1 = database.unit("EPSG", "9001") 55 | unit_2 = database.unit("EPSG", "9001") 56 | assert(unit_1 == unit_2) 57 | end 58 | 59 | def test_category 60 | database = Proj::Database.new(Proj::Context.current) 61 | 62 | %w[linear linear_per_time angular angular_per_time scale scale_per_time time].each do |category| 63 | units = database.units(category: category) 64 | refute_empty(units) 65 | end 66 | end 67 | 68 | def test_auth_name 69 | database = Proj::Database.new(Proj::Context.current) 70 | 71 | %w[EPSG PROJ].each do |auth_name| 72 | units = database.units(auth_name: auth_name) 73 | refute_empty(units) 74 | end 75 | end 76 | end 77 | --------------------------------------------------------------------------------