├── .formatter.exs ├── .github ├── actions │ └── elixir-setup │ │ └── action.yml ├── dependabot.yml └── workflows │ ├── elixir-build-and-test.yml │ ├── elixir-quality-checks.yml │ └── elixir-retired-packages-check.yml ├── .gitignore ├── .tool-versions ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── config ├── config.exs ├── dev.exs ├── prod.exs └── test.exs ├── lib ├── geo_postgis.ex └── geo_postgis │ ├── config.ex │ ├── extension.ex │ └── geometry.ex ├── mix.exs ├── mix.lock └── test ├── ecto_test.exs ├── geo_postgis_test.exs ├── geography_test.exs ├── geometry_test.exs ├── support ├── postgrex_types.ex └── repo.ex └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/actions/elixir-setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup Elixir Project 2 | description: Checks out the code, configures Elixir, fetches dependencies, and manages build caching. 3 | inputs: 4 | elixir-version: 5 | required: true 6 | type: string 7 | description: Elixir version to set up 8 | otp-version: 9 | required: true 10 | type: string 11 | description: OTP version to set up 12 | ################################################################# 13 | # Everything below this line is optional. 14 | # 15 | # It's designed to make compiling a reasonably standard Elixir 16 | # codebase "just work," though there may be speed gains to be had 17 | # by tweaking these flags. 18 | ################################################################# 19 | build-deps: 20 | required: false 21 | type: boolean 22 | default: true 23 | description: True if we should compile dependencies 24 | build-app: 25 | required: false 26 | type: boolean 27 | default: true 28 | description: True if we should compile the application itself 29 | build-flags: 30 | required: false 31 | type: string 32 | default: '--all-warnings' 33 | description: Flags to pass to mix compile 34 | install-rebar: 35 | required: false 36 | type: boolean 37 | default: true 38 | description: By default, we will install Rebar (mix local.rebar --force). 39 | install-hex: 40 | required: false 41 | type: boolean 42 | default: true 43 | description: By default, we will install Hex (mix local.hex --force). 44 | cache-key: 45 | required: false 46 | type: string 47 | default: 'v1' 48 | description: If you need to reset the cache for some reason, you can change this key. 49 | version-type: 50 | required: false 51 | type: string 52 | default: '' 53 | description: Type of version matching to use (e.g. 'strict' for exact versions) 54 | outputs: 55 | otp-version: 56 | description: "Exact OTP version selected by the BEAM setup step" 57 | value: ${{ steps.beam.outputs.otp-version }} 58 | elixir-version: 59 | description: "Exact Elixir version selected by the BEAM setup step" 60 | value: ${{ steps.beam.outputs.elixir-version }} 61 | runs: 62 | using: "composite" 63 | steps: 64 | - name: Setup elixir 65 | uses: erlef/setup-beam@v1.15.4 66 | id: beam 67 | with: 68 | elixir-version: ${{ inputs.elixir-version }} 69 | otp-version: ${{ inputs.otp-version }} 70 | version-type: ${{ inputs.version-type }} 71 | 72 | - name: Get deps cache 73 | uses: actions/cache@v4 74 | with: 75 | path: deps/ 76 | key: deps-${{ inputs.cache-key }}-${{ runner.os }}-${{ hashFiles('**/mix.lock') }} 77 | restore-keys: | 78 | deps-${{ inputs.cache-key }}-${{ runner.os }}- 79 | 80 | - name: Get build cache 81 | uses: actions/cache@v4 82 | id: build-cache 83 | with: 84 | path: _build/${{env.MIX_ENV}}/ 85 | key: build-${{ inputs.cache-key }}-${{ runner.os }}-${{ inputs.otp-version }}-${{ inputs.elixir-version }}-${{ env.MIX_ENV }}-${{ hashFiles('**/mix.lock') }} 86 | restore-keys: | 87 | build-${{ inputs.cache-key }}-${{ runner.os }}-${{ inputs.otp-version }}-${{ inputs.elixir-version }}-${{ env.MIX_ENV }}- 88 | 89 | - name: Get Hex cache 90 | uses: actions/cache@v4 91 | id: hex-cache 92 | with: 93 | path: ~/.hex 94 | key: hex-${{ inputs.cache-key }}-${{ runner.os }}-${{ inputs.otp-version }}-${{ inputs.elixir-version }}-${{ hashFiles('**/mix.lock') }} 95 | restore-keys: | 96 | hex-${{ inputs.cache-key }}-${{ runner.os }}-${{ inputs.otp-version }}-${{ inputs.elixir-version }}- 97 | 98 | - name: Get Mix cache 99 | uses: actions/cache@v4 100 | id: mix-cache 101 | with: 102 | path: ${{ env.MIX_HOME || '~/.mix' }} 103 | key: mix-${{ inputs.cache-key }}-${{ runner.os }}-${{ inputs.otp-version }}-${{ inputs.elixir-version }}-${{ hashFiles('**/mix.lock') }} 104 | restore-keys: | 105 | mix-${{ inputs.cache-key }}-${{ runner.os }}-${{ inputs.otp-version }}-${{ inputs.elixir-version }}- 106 | 107 | # In my experience, I have issues with incremental builds maybe 1 in 100 108 | # times that are fixed by doing a full recompile. 109 | # In order to not waste dev time on such trivial issues (while also reaping 110 | # the time savings of incremental builds for *most* day-to-day development), 111 | # I force a full recompile only on builds that we retry. 112 | - name: Clean to rule out incremental build as a source of flakiness 113 | if: github.run_attempt != '1' 114 | run: | 115 | mix deps.clean --all 116 | mix clean 117 | shell: sh 118 | 119 | - name: Install Rebar 120 | run: mix local.rebar --force --if-missing 121 | shell: sh 122 | if: inputs.install-rebar == 'true' 123 | 124 | - name: Install Hex 125 | run: mix local.hex --force --if-missing 126 | shell: sh 127 | if: inputs.install-hex == 'true' 128 | 129 | - name: Install Dependencies 130 | run: mix deps.get 131 | shell: sh 132 | 133 | # Normally we'd use `mix deps.compile` here, however that incurs a large 134 | # performance penalty when the dependencies are already fully compiled: 135 | # https://elixirforum.com/t/github-action-cache-elixir-always-recompiles-dependencies-elixir-1-13-3/45994/12 136 | # 137 | # Accoring to Jose Valim at the above link `mix loadpaths` will check and 138 | # compile missing dependencies 139 | - name: Compile Dependencies 140 | run: mix loadpaths 141 | shell: sh 142 | if: inputs.build-deps == 'true' 143 | 144 | - name: Compile Application 145 | run: mix compile ${{ inputs.build-flags }} 146 | shell: sh 147 | if: inputs.build-app == 'true' 148 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: mix 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "11:00" 8 | open-pull-requests-limit: 10 9 | assignees: 10 | - s3cur3 11 | -------------------------------------------------------------------------------- /.github/workflows/elixir-build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: Elixir Unit Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - "**" 10 | 11 | jobs: 12 | build: 13 | name: Elixir Unit Tests 14 | runs-on: ${{ matrix.os }} 15 | env: 16 | MIX_ENV: test 17 | PGPASSWORD: postgres 18 | strategy: 19 | matrix: 20 | include: 21 | # Elixir 1.14 requires at least OTP 23, but Ubuntu 22 only supports back to OTP 25 22 | - elixir: '1.14.4' 23 | otp: '25.3.2' 24 | os: ubuntu-22.04 25 | - elixir: '1.14.4' 26 | otp: '26.2.5' 27 | os: ubuntu-22.04 28 | # Elixir 1.15 requires at least OTP 24, but Ubuntu 22 only supports back to OTP 25 29 | - elixir: '1.15.5' 30 | otp: '25.3.2' 31 | os: ubuntu-22.04 32 | - elixir: '1.15.5' 33 | otp: '26.2.5' 34 | os: ubuntu-22.04 35 | # Elixir 1.16 requires at least OTP 24, but Ubuntu 22 only supports back to OTP 25 36 | - elixir: '1.16.2' 37 | otp: '25.3.2' 38 | os: ubuntu-22.04 39 | - elixir: '1.16.2' 40 | otp: '26.2.5' 41 | os: ubuntu-22.04 42 | 43 | services: 44 | db: 45 | image: postgis/postgis:16-3.5-alpine 46 | env: 47 | POSTGRES_USER: postgres 48 | POSTGRES_PASSWORD: postgres 49 | POSTGRES_DB: geo_postgrex_test 50 | ports: ["5432:5432"] 51 | options: >- 52 | --health-cmd pg_isready 53 | --health-interval 10s 54 | --health-timeout 5s 55 | --health-retries 5 56 | steps: 57 | - name: Checkout repository 58 | uses: actions/checkout@v2 59 | 60 | - name: Setup Elixir Project 61 | uses: ./.github/actions/elixir-setup 62 | with: 63 | elixir-version: ${{ matrix.elixir }} 64 | otp-version: ${{ matrix.otp }} 65 | version-type: 'strict' 66 | build-app: false 67 | 68 | - name: Compile with warnings as errors 69 | if: ${{ matrix.elixir != '1.11.4' }} 70 | run: mix compile --warnings-as-errors 71 | 72 | - name: Run tests with warnings as errors 73 | if: ${{ matrix.elixir != '1.11.4' }} 74 | run: mix test --warnings-as-errors 75 | 76 | - name: Run tests 77 | run: mix test 78 | -------------------------------------------------------------------------------- /.github/workflows/elixir-quality-checks.yml: -------------------------------------------------------------------------------- 1 | name: Elixir Quality Checks 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - "**" 10 | 11 | jobs: 12 | quality_checks: 13 | name: Elixir Quality Checks 14 | runs-on: ubuntu-22.04 15 | env: 16 | # In MIX_ENV=test, `$ mix xref graph` shows us a whole bunch of 17 | # test stuff that isn't really relevant. 18 | # The other checks don't really care what environment they run in. 19 | MIX_ENV: dev 20 | elixir: "1.16.2" 21 | otp: "26.1.2" 22 | 23 | steps: 24 | - name: Checkout repository 25 | uses: actions/checkout@v2 26 | 27 | - name: Setup Elixir Project 28 | uses: ./.github/actions/elixir-setup 29 | with: 30 | elixir-version: ${{ env.elixir }} 31 | otp-version: ${{ env.otp }} 32 | build-app: false 33 | 34 | - name: Check for unused deps 35 | run: mix deps.unlock --check-unused 36 | 37 | - name: Check code formatting 38 | run: mix format --check-formatted 39 | # We run all checks here even if others failed so that 40 | # we give devs as much feedback as possible & save some time. 41 | if: always() 42 | 43 | - name: Check for compile-time dependencies between modules 44 | run: mix xref graph --label compile-connected --fail-above 0 45 | if: always() 46 | -------------------------------------------------------------------------------- /.github/workflows/elixir-retired-packages-check.yml: -------------------------------------------------------------------------------- 1 | name: Elixir Retired Packages Check 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - "**" 10 | 11 | jobs: 12 | retired_packages: 13 | name: Elixir Retired Packages Check 14 | runs-on: ubuntu-22.04 15 | env: 16 | MIX_ENV: dev 17 | elixir: "1.16.2" 18 | otp: "26.1.2" 19 | 20 | steps: 21 | - name: Checkout repository 22 | uses: actions/checkout@v2 23 | 24 | - name: Setup Elixir Project 25 | uses: ./.github/actions/elixir-setup 26 | with: 27 | elixir-version: ${{ env.elixir }} 28 | otp-version: ${{ env.otp }} 29 | build-app: false 30 | 31 | - name: Check for retired/abandoned deps 32 | run: mix hex.audit 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | geo_postgis-*.tar 24 | 25 | # Temporary files for e.g. tests. 26 | /tmp/ 27 | 28 | # Emacs project file 29 | .projectile 30 | 31 | .lexical/ 32 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | erlang 27.2.1 2 | elixir 1.18.2-otp-27 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: false 3 | language: elixir 4 | elixir: 5 | - 1.11 6 | otp_release: 7 | - 22.3 8 | addons: 9 | postgresql: '9.6' 10 | apt: 11 | packages: 12 | - postgresql-9.6-postgis-2.3 13 | services: 14 | - postgresql 15 | before_script: 16 | - psql -c 'create database geo_postgrex_test;' -U postgres 17 | - psql -c 'create extension postgis;' -U postgres -d geo_postgrex_test 18 | script: 19 | - mix test 20 | before_deploy: 21 | - mix compile 22 | deploy: 23 | skip_cleanup: true 24 | provider: script 25 | script: mix hex.publish --yes 26 | on: 27 | tags: true 28 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 7 | 8 | ## [3.7.1] - 2024-09-18 9 | 10 | Adds support for the newly released [`geo`](https://github.com/felt/geo) v4.0.0, while maintaining compatibility with v3.6.x and v3.7.x. 11 | 12 | ## [3.7.0] - 2024-05-08 13 | 14 | ### New features 15 | 16 | * Do not raise on unsuccessful cast, with thanks to new contributor @marmor157 (#156) 17 | * Added support for `st_make_envelope`, with thanks to @mjquinlan2000 (#195) 18 | 19 | ### Updates 20 | 21 | * ex_doc updated to v0.32.1 22 | 23 | ## [3.6.0] - 2024-03-11 24 | 25 | ### New feature 26 | 27 | * Add support for LineStringZM, with thanks to new contributor @versilov (#160) 28 | 29 | ### Fixes and updates 30 | 31 | * Silence compile warning by updating `geo` from 3.5.1 to 3.6.0 32 | * Misc. dependency updates (ex_doc, ecto, ecto_sql, and postgrex) 33 | 34 | ## [3.5.0] - 2023-09-22 35 | 36 | ### New features 37 | 38 | - [Added ST_MakePoint function](https://github.com/felt/geo_postgis/pull/162) (thanks to new contributor @Slavenin!) 39 | - [Added ST_IsValid and ST_MakeValid](https://github.com/felt/geo_postgis/pull/146/) (thanks to new contributor @AntoineAugusti!) 40 | - [Added `Geo.PostGIS.Geometry.t()` for Dialyzer](https://github.com/felt/geo_postgis/pull/173) (thanks to new contributor @RudolfMan!) 41 | 42 | ### Bug fix 43 | 44 | - [Corrected ST_Crosses fragment](https://github.com/felt/geo_postgis/pull/154) (thanks to new contributor @varjas!) 45 | 46 | ### Compatibility fix 47 | 48 | - [Update geometry.ex to force recompilation on upgrade](https://github.com/felt/geo_postgis/pull/174)—this should make it unnecessary to do the `mix deps.clean` described in the upgrade notes to v3.4.4 below. In my testing, when moving from v3.4.3 to this release, that step was unnecessary; however, beware that it may still be needed when moving from a branch where you're using v3.5.0 to one using v3.4.3 or earlier. 49 | 50 | ## [3.4.4] - 2023-09-20 51 | 52 | As of v3.4.4, `geo_postgis` is being maintained by the Felt team. As a company building a geospatial product on Elixir, with a track record of [supporting open source software](https://felt.com/open-source), we're excited for the future of the project. 53 | 54 | ### Elixir 1.15 compatibility 55 | 56 | This release fixes a major compatibility issue with Elixir v1.15. When compiling a project that depends on `geo_postgis` prior to this release, you may have seen errors like this: 57 | 58 | ``` 59 | == Compilation error in file lib/my_app/my_module.ex == 60 | ** (ArgumentError) unknown type Geo.PostGIS.Geometry for field :bounding_box 61 | (ecto 3.10.3) lib/ecto/schema.ex:2318: Ecto.Schema.check_field_type!/4 62 | (ecto 3.10.3) lib/ecto/schema.ex:1931: Ecto.Schema.__field__/4 63 | lib/my_app/my_module.ex:23: (module) 64 | ``` 65 | 66 | ...or: 67 | 68 | ``` 69 | ** (UndefinedFunctionError) function Geo.PostGIS.Geometry.type/0 is undefined (module Geo.PostGIS.Geometry is not available) 70 | Geo.PostGIS.Geometry.type() 71 | ``` 72 | 73 | As new contributor [@aeruder](https://github.com/aeruder) [pointed out](https://github.com/felt/geo_postgis/pull/164), this was due to a change in how Elixir 1.15 prunes code more precisely when compiling dependencies, resulting in the `Geo.PostGIS.Geometry` module being compiled out if Ecto didn't _happen_ to get compiled before it. This release fixes the issue, but you'll still need to recompile both `geo_postgis` and `ecto` to get things working again. 74 | 75 | ### Upgrade notes 76 | 77 | If you're using Elixir 1.15, after installing v3.4.4, you'll need to run: 78 | 79 | ```sh 80 | mix deps.clean geo_postgis ecto && mix deps.get 81 | ``` 82 | 83 | (Alternatively, a full clean build of your project will also do the job.) 84 | 85 | Doing so will ensure `geo_postgis` compiles with the Ecto dependency and fixes the compilation errors noted above. 86 | 87 | Note that you'll _also_ need to run the above one-liner if you need to switch back to a previous version of `geo_postgis` (e.g., when moving between branches). However, if you can stick with the new version going forward, you'll only have to run it once. 88 | 89 | ### Fixed 90 | 91 | - Elixir 1.15 compatibility (see notes above) 92 | - [Called out the optional Ecto dependency in `mix.exs`](https://github.com/felt/geo_postgis/pull/164) 93 | - [Updated docs links to point to the project's new home in the Felt GitHub organization](https://github.com/felt/geo_postgis/pull/170) 94 | - Dependency updates for `ecto_sql`, `postgrex`, and `ex_doc` 95 | - Bumped the minimum Elixir version to v1.11, matching `postgrex` v0.16.0+ 96 | 97 | ## [3.4.3] - 2023-06-20 98 | 99 | ### Fixed 100 | 101 | - [Corrected bitstring specifier int32 and updated deps](https://github.com/felt/geo_postgis/pull/158) 102 | 103 | ## [3.4.2] - 2022-02-23 104 | 105 | ### Fixed 106 | 107 | - [Fixed compilation error introduced in #121](https://github.com/felt/geo_postgis/pull/128) 108 | 109 | ## [3.4.1] - 2021-12-15 110 | 111 | ### Enhancements 112 | 113 | - Add [Geo.PostGIS.st_build_area/1](https://github.com/felt/geo_postgis/pull/115) 114 | 115 | ## [3.4.0] - 2021-04-10 116 | 117 | ### Enhancements 118 | 119 | - Update to Geo 3.4.0 120 | - `Geo.PostGIS.Extension` now uses the `:binary` format instead of `:text` 121 | 122 | ### Changes 123 | 124 | - Passing latitude or longitude as string instead of floats is no longer supported and raises an `argument error` 125 | 126 | ## [3.3.1] - 2019-12-13 127 | 128 | ### Fixed 129 | 130 | - [Add new callback functions required by ecto 3](https://github.com/felt/geo_postgis/pull/55) 131 | - [Ecto 3.2+ requires callbacks for custom types](https://github.com/felt/geo_postgis/pull/59) 132 | 133 | ## [3.3.0] - 2019-08-26 134 | 135 | ### Updated 136 | 137 | - Geo dependency to 3.3 138 | 139 | ## [3.2.0] - 2019-07-23 140 | 141 | ### Add 142 | 143 | - [Z versions of the datatypes](https://github.com/felt/geo_postgis/pull/44) 144 | 145 | ## [3.1.0] - 2019-02-11 146 | 147 | ### Updated 148 | 149 | - [Add PointZ handling](https://github.com/felt/geo_postgis/pull/25) 150 | 151 | ## [3.0.0] - 2018-12-01 152 | 153 | ### Updated 154 | 155 | - Support for Ecto 3.0 156 | 157 | ## [2.1.0] - 2018-08-28 158 | 159 | ### Added 160 | 161 | - [Geo.PostGIS.st_point/2](https://github.com/felt/geo_postgis/pull/6) 162 | 163 | ### Fixed 164 | 165 | - [st_distance_in_meters/2](https://github.com/felt/geo_postgis/pull/8) 166 | 167 | ## [2.0.0] - 2018-04-14 168 | 169 | ### Changed 170 | 171 | - Use `Geo.PostGIS.Geometry` when defining structs instead of `Geo.Geometry` 172 | 173 | ```elixir 174 | #instead of 175 | schema "test" do 176 | field :name, :string 177 | field :geom, Geo.Geometry # or Geo.Point, Geo.LineString, etc 178 | end 179 | 180 | #now use 181 | schema "test" do 182 | field :name, :string 183 | field :geom, Geo.PostGIS.Geometry 184 | end 185 | ``` 186 | 187 | ## [1.1.0] - 2018-01-28 188 | 189 | ### Added 190 | 191 | - [Add ST_Collect()](https://github.com/felt/geo_postgis/pull/3) 192 | 193 | ## [1.0.0] - 2017-07-15 194 | 195 | ### Added 196 | 197 | - PostGIS extension for Postgrex 198 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Bryan Joseph 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GeoPostGIS 2 | 3 | [![Build & Test Status](https://github.com/felt/geo_postgis/actions/workflows/elixir-build-and-test.yml/badge.svg?branch=master)](https://github.com/felt/geo_postgis/actions/workflows/elixir-build-and-test.yml) 4 | [![Module Version](https://img.shields.io/hexpm/v/geo_postgis.svg)](https://hex.pm/packages/geo_postgis) 5 | [![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/geo_postgis/) 6 | [![Total Download](https://img.shields.io/hexpm/dt/geo_postgis.svg)](https://hex.pm/packages/geo_postgis) 7 | [![License](https://img.shields.io/hexpm/l/geo_postgis.svg)](https://github.com/felt/geo_postgis/blob/master/LICENSE) 8 | [![Last Updated](https://img.shields.io/github/last-commit/felt/geo_postgis.svg)](https://github.com/felt/geo_postgis/commits/master) 9 | 10 | Postgrex extension for the PostGIS data types. Uses the [geo](https://github.com/felt/geo) library 11 | 12 | ## Installation 13 | 14 | The package can be installed by adding `:geo_postgis` to your list of 15 | dependencies in `mix.exs`: 16 | 17 | ```elixir 18 | def deps do 19 | [ 20 | {:geo_postgis, "~> 3.7"} 21 | ] 22 | end 23 | ``` 24 | 25 | Make sure PostGIS extension to your database is installed. More information [here](https://trac.osgeo.org/postgis/wiki/UsersWikiPostGIS24UbuntuPGSQL10Apt#Install) 26 | 27 | ### Optional Configuration 28 | 29 | ```elixir 30 | # When a binary is passed to `Geo.PostGIS.Geometry.cast/1` implementation of 31 | # `Ecto.Type.cast/1`, it is assumed to be a GeoJSON string. When this happens, 32 | # geo_postgis will use JSON, by default, to convert the binary to a map and 33 | # then convert that map to one of the Geo structs. If in these cases you would 34 | # like to use a different JSON parser, you can set the config below. 35 | 36 | # config.exs 37 | config :geo_postgis, 38 | json_library: Jason # If you want to set your JSON module 39 | ``` 40 | 41 | ## Examples 42 | 43 | Postgrex Extension for the PostGIS data types, Geometry and Geography: 44 | 45 | ```elixir 46 | Postgrex.Types.define(MyApp.PostgresTypes, [Geo.PostGIS.Extension], []) 47 | 48 | opts = [hostname: "localhost", username: "postgres", database: "geo_postgrex_test", types: MyApp.PostgresTypes ] 49 | [hostname: "localhost", username: "postgres", database: "geo_postgrex_test", types: MyApp.PostgresTypes] 50 | 51 | {:ok, pid} = Postgrex.Connection.start_link(opts) 52 | {:ok, #PID<0.115.0>} 53 | 54 | geo = %Geo.Point{coordinates: {30, -90}, srid: 4326} 55 | %Geo.Point{coordinates: {30, -90}, srid: 4326} 56 | 57 | {:ok, _} = Postgrex.Connection.query(pid, "CREATE TABLE point_test (id int, geom geometry(Point, 4326))") 58 | {:ok, %Postgrex.Result{columns: nil, command: :create_table, num_rows: 0, rows: nil}} 59 | 60 | {:ok, _} = Postgrex.Connection.query(pid, "INSERT INTO point_test VALUES ($1, $2)", [42, geo]) 61 | {:ok, %Postgrex.Result{columns: nil, command: :insert, num_rows: 1, rows: nil}} 62 | 63 | Postgrex.Connection.query(pid, "SELECT * FROM point_test") 64 | {:ok, %Postgrex.Result{columns: ["id", "geom"], command: :select, num_rows: 1, 65 | rows: [{42, %Geo.Point{coordinates: {30.0, -90.0}, srid: 4326 }}]}} 66 | ``` 67 | 68 | Use with [Ecto](https://hexdocs.pm/ecto_sql/Ecto.Adapters.Postgres.html#module-extensions): 69 | 70 | ```elixir 71 | # If using with Ecto, you may want something like this instead 72 | Postgrex.Types.define(MyApp.PostgresTypes, 73 | [Geo.PostGIS.Extension] ++ Ecto.Adapters.Postgres.extensions(), 74 | json: Jason) 75 | 76 | # Add extensions to your repo config 77 | config :thanks, Repo, 78 | database: "geo_postgrex_test", 79 | username: "postgres", 80 | password: "postgres", 81 | hostname: "localhost", 82 | adapter: Ecto.Adapters.Postgres, 83 | types: MyApp.PostgresTypes 84 | 85 | 86 | # Create a schema 87 | defmodule Test do 88 | use Ecto.Schema 89 | 90 | schema "test" do 91 | field :name, :string 92 | field :geom, Geo.PostGIS.Geometry 93 | end 94 | end 95 | 96 | # Geometry or Geography columns can also be created in migrations 97 | defmodule Repo.Migrations.Init do 98 | use Ecto.Migration 99 | 100 | def change do 101 | create table(:test) do 102 | add :name, :string 103 | add :geom, :geometry 104 | end 105 | end 106 | end 107 | ``` 108 | 109 | Ecto migrations can also use more elaborate [PostGIS GIS Objects](http://postgis.net/docs/using_postgis_dbmanagement.html#RefObject). These types are useful for enforcing constraints on `{Lng,Lat}` (order matters), or ensuring that a particular projection/coordinate system/format is used. 110 | 111 | ```elixir 112 | defmodule Repo.Migrations.AdvancedInit do 113 | use Ecto.Migration 114 | 115 | def change do 116 | create table(:test) do 117 | add :name, :string 118 | end 119 | # Add a field `lng_lat_point` with type `geometry(Point,4326)`. 120 | # This can store a "standard GPS" (epsg4326) coordinate pair {longitude,latitude}. 121 | execute("SELECT AddGeometryColumn ('test','lng_lat_point',4326,'POINT',2);", "") 122 | 123 | # Once a GIS data table exceeds a few thousand rows, you will want to build an index to speed up spatial searches of the data 124 | # Syntax - CREATE INDEX [indexname] ON [tablename] USING GIST ( [geometryfield] ); 125 | execute("CREATE INDEX test_geom_idx ON test USING GIST (lng_lat_point);", "") 126 | end 127 | end 128 | ``` 129 | 130 | Be sure to enable the PostGIS extension if you haven't already done so: 131 | 132 | ```elixir 133 | defmodule MyApp.Repo.Migrations.EnablePostgis do 134 | use Ecto.Migration 135 | 136 | def change do 137 | execute "CREATE EXTENSION IF NOT EXISTS postgis", "DROP EXTENSION IF EXISTS postgis" 138 | end 139 | end 140 | ``` 141 | 142 | [PostGIS functions](https://postgis.net/docs/reference.html) can also be used in Ecto queries. Currently only the OpenGIS functions are implemented. Have a look at [lib/geo_postgis.ex](lib/geo_postgis.ex) for the implemented functions. You can use them like: 143 | 144 | ```elixir 145 | defmodule Example do 146 | import Ecto.Query 147 | import Geo.PostGIS 148 | 149 | def example_query(geom) do 150 | query = from location in Location, limit: 5, select: st_distance(location.geom, ^geom) 151 | query 152 | |> Repo.one 153 | end 154 | end 155 | ``` 156 | 157 | ## Development 158 | 159 | After you got the dependencies via `mix deps.get` make sure that: 160 | 161 | * `postgis` is installed 162 | * your `postgres` user has the database `"geo_postgrex_test"` 163 | * your `postgres` db user can login without a password or you set the `PGPASSWORD` environment variable appropriately 164 | 165 | Then you can run the tests as you are used to with `mix test`. 166 | 167 | 168 | ## Copyright and License 169 | 170 | Copyright (c) 2017 Bryan Joseph 171 | 172 | Released under the MIT License, which can be found in the repository in [`LICENSE`](https://github.com/felt/geo_postgis/blob/master/LICENSE). 173 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | import Config 4 | 5 | import_config "#{Mix.env()}.exs" 6 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :geo_postgis, ecto_repos: [Geo.PostGIS.Test.Repo] 4 | 5 | config :geo_postgis, Geo.PostGIS.Test.Repo, 6 | database: "geo_postgrex_test", 7 | username: "postgres", 8 | password: "postgres", 9 | hostname: "localhost", 10 | types: Geo.PostGIS.PostgrexTypes 11 | 12 | # Print only warnings and errors during test 13 | config :logger, level: :warning 14 | -------------------------------------------------------------------------------- /lib/geo_postgis.ex: -------------------------------------------------------------------------------- 1 | defmodule Geo.PostGIS do 2 | @moduledoc """ 3 | PostGIS functions that can used in ecto queries 4 | [PostGIS Function Documentation](https://postgis.net/docs/reference.html). 5 | 6 | Currently only the OpenGIS functions are implemented. 7 | 8 | ## Examples 9 | 10 | defmodule Example do 11 | import Ecto.Query 12 | import Geo.PostGIS 13 | 14 | def example_query(geom) do 15 | from location in Location, limit: 5, select: st_distance(location.geom, ^geom) 16 | end 17 | end 18 | 19 | """ 20 | 21 | defmacro st_transform(wkt, srid) do 22 | quote do: fragment("ST_Transform(?, ?)", unquote(wkt), unquote(srid)) 23 | end 24 | 25 | defmacro st_distance(geometryA, geometryB) do 26 | quote do: fragment("ST_Distance(?,?)", unquote(geometryA), unquote(geometryB)) 27 | end 28 | 29 | @doc """ 30 | Casts the 2 geometries given to geographies in order to return distance in meters. 31 | """ 32 | defmacro st_distance_in_meters(geometryA, geometryB) do 33 | quote do: 34 | fragment( 35 | "ST_Distance(?::geography, ?::geography)", 36 | unquote(geometryA), 37 | unquote(geometryB) 38 | ) 39 | end 40 | 41 | defmacro st_distancesphere(geometryA, geometryB) do 42 | quote do: fragment("ST_DistanceSphere(?,?)", unquote(geometryA), unquote(geometryB)) 43 | end 44 | 45 | @doc """ 46 | Please note that ST_Distance_Sphere has been deprecated as of Postgis 2.2. 47 | Postgis 2.1 is no longer supported on PostgreSQL >= 9.5. 48 | This macro is still in place to support users of PostgreSQL <= 9.4.x. 49 | """ 50 | defmacro st_distance_sphere(geometryA, geometryB) do 51 | quote do: fragment("ST_Distance_Sphere(?,?)", unquote(geometryA), unquote(geometryB)) 52 | end 53 | 54 | defmacro st_dwithin(geometryA, geometryB, float) do 55 | quote do: 56 | fragment("ST_DWithin(?,?,?)", unquote(geometryA), unquote(geometryB), unquote(float)) 57 | end 58 | 59 | @doc """ 60 | Casts the 2 geometries given to geographies in order to check for distance in meters. 61 | """ 62 | defmacro st_dwithin_in_meters(geometryA, geometryB, float) do 63 | quote do: 64 | fragment( 65 | "ST_DWithin(?::geography, ?::geography, ?)", 66 | unquote(geometryA), 67 | unquote(geometryB), 68 | unquote(float) 69 | ) 70 | end 71 | 72 | defmacro st_equals(geometryA, geometryB) do 73 | quote do: fragment("ST_Equals(?,?)", unquote(geometryA), unquote(geometryB)) 74 | end 75 | 76 | defmacro st_disjoint(geometryA, geometryB) do 77 | quote do: fragment("ST_Disjoint(?,?)", unquote(geometryA), unquote(geometryB)) 78 | end 79 | 80 | defmacro st_intersects(geometryA, geometryB) do 81 | quote do: fragment("ST_Intersects(?,?)", unquote(geometryA), unquote(geometryB)) 82 | end 83 | 84 | defmacro st_touches(geometryA, geometryB) do 85 | quote do: fragment("ST_Touches(?,?)", unquote(geometryA), unquote(geometryB)) 86 | end 87 | 88 | defmacro st_crosses(geometryA, geometryB) do 89 | quote do: fragment("ST_Crosses(?,?)", unquote(geometryA), unquote(geometryB)) 90 | end 91 | 92 | defmacro st_within(geometryA, geometryB) do 93 | quote do: fragment("ST_Within(?,?)", unquote(geometryA), unquote(geometryB)) 94 | end 95 | 96 | defmacro st_overlaps(geometryA, geometryB) do 97 | quote do: fragment("ST_Overlaps(?,?)", unquote(geometryA), unquote(geometryB)) 98 | end 99 | 100 | defmacro st_contains(geometryA, geometryB) do 101 | quote do: fragment("ST_Contains(?,?)", unquote(geometryA), unquote(geometryB)) 102 | end 103 | 104 | defmacro st_covers(geometryA, geometryB) do 105 | quote do: fragment("ST_Covers(?,?)", unquote(geometryA), unquote(geometryB)) 106 | end 107 | 108 | defmacro st_covered_by(geometryA, geometryB) do 109 | quote do: fragment("ST_CoveredBy(?,?)", unquote(geometryA), unquote(geometryB)) 110 | end 111 | 112 | defmacro st_relate(geometryA, geometryB, intersectionPatternMatrix) do 113 | quote do: 114 | fragment( 115 | "ST_Relate(?,?,?)", 116 | unquote(geometryA), 117 | unquote(geometryB), 118 | unquote(intersectionPatternMatrix) 119 | ) 120 | end 121 | 122 | defmacro st_relate(geometryA, geometryB) do 123 | quote do: fragment("ST_Relate(?,?)", unquote(geometryA), unquote(geometryB)) 124 | end 125 | 126 | defmacro st_centroid(geometry) do 127 | quote do: fragment("ST_Centroid(?)", unquote(geometry)) 128 | end 129 | 130 | defmacro st_area(geometry) do 131 | quote do: fragment("ST_Area(?)", unquote(geometry)) 132 | end 133 | 134 | defmacro st_length(geometry) do 135 | quote do: fragment("ST_Length(?)", unquote(geometry)) 136 | end 137 | 138 | defmacro st_point_on_surface(geometry) do 139 | quote do: fragment("ST_PointOnSurface(?)", unquote(geometry)) 140 | end 141 | 142 | defmacro st_boundary(geometry) do 143 | quote do: fragment("ST_Boundary(?)", unquote(geometry)) 144 | end 145 | 146 | defmacro st_buffer(geometry, double) do 147 | quote do: fragment("ST_Buffer(?, ?)", unquote(geometry), unquote(double)) 148 | end 149 | 150 | defmacro st_buffer(geometry, double, integer) do 151 | quote do: fragment("ST_Buffer(?, ?, ?)", unquote(geometry), unquote(double), unquote(integer)) 152 | end 153 | 154 | defmacro st_convex_hull(geometry) do 155 | quote do: fragment("ST_ConvexHull(?)", unquote(geometry)) 156 | end 157 | 158 | defmacro st_intersection(geometryA, geometryB) do 159 | quote do: fragment("ST_Intersection(?, ?)", unquote(geometryA), unquote(geometryB)) 160 | end 161 | 162 | defmacro st_shift_longitude(geometry) do 163 | quote do: fragment("ST_Shift_Longitude(?)", unquote(geometry)) 164 | end 165 | 166 | defmacro st_sym_difference(geometryA, geometryB) do 167 | quote do: fragment("ST_SymDifference(?,?)", unquote(geometryA), unquote(geometryB)) 168 | end 169 | 170 | defmacro st_difference(geometryA, geometryB) do 171 | quote do: fragment("ST_Difference(?,?)", unquote(geometryA), unquote(geometryB)) 172 | end 173 | 174 | defmacro st_collect(geometryList) do 175 | quote do: fragment("ST_Collect(?)", unquote(geometryList)) 176 | end 177 | 178 | defmacro st_collect(geometryA, geometryB) do 179 | quote do: fragment("ST_Collect(?,?)", unquote(geometryA), unquote(geometryB)) 180 | end 181 | 182 | defmacro st_union(geometryList) do 183 | quote do: fragment("ST_Union(?)", unquote(geometryList)) 184 | end 185 | 186 | defmacro st_union(geometryA, geometryB) do 187 | quote do: fragment("ST_Union(?,?)", unquote(geometryA), unquote(geometryB)) 188 | end 189 | 190 | defmacro st_mem_union(geometryList) do 191 | quote do: fragment("ST_MemUnion(?)", unquote(geometryList)) 192 | end 193 | 194 | defmacro st_as_text(geometry) do 195 | quote do: fragment("ST_AsText(?)", unquote(geometry)) 196 | end 197 | 198 | defmacro st_as_binary(geometry) do 199 | quote do: fragment("ST_AsBinary(?)", unquote(geometry)) 200 | end 201 | 202 | defmacro st_srid(geometry) do 203 | quote do: fragment("ST_SRID(?)", unquote(geometry)) 204 | end 205 | 206 | defmacro st_set_srid(geometry, srid) do 207 | quote do: fragment("ST_SetSRID(?, ?)", unquote(geometry), unquote(srid)) 208 | end 209 | 210 | defmacro st_make_box_2d(geometryA, geometryB) do 211 | quote do: fragment("ST_MakeBox2D(?, ?)", unquote(geometryA), unquote(geometryB)) 212 | end 213 | 214 | defmacro st_dimension(geometry) do 215 | quote do: fragment("ST_Dimension(?)", unquote(geometry)) 216 | end 217 | 218 | defmacro st_envelope(geometry) do 219 | quote do: fragment("ST_Envelope(?)", unquote(geometry)) 220 | end 221 | 222 | defmacro st_is_simple(geometry) do 223 | quote do: fragment("ST_IsSimple(?)", unquote(geometry)) 224 | end 225 | 226 | defmacro st_is_closed(geometry) do 227 | quote do: fragment("ST_IsClosed(?)", unquote(geometry)) 228 | end 229 | 230 | defmacro st_is_collection(geometry) do 231 | quote do: fragment("ST_IsCollection(?)", unquote(geometry)) 232 | end 233 | 234 | defmacro st_is_empty(geometry) do 235 | quote do: fragment("ST_IsEmpty(?)", unquote(geometry)) 236 | end 237 | 238 | defmacro st_is_ring(geometry) do 239 | quote do: fragment("ST_IsRing(?)", unquote(geometry)) 240 | end 241 | 242 | defmacro st_num_geometries(geometry) do 243 | quote do: fragment("ST_NumGeometries(?)", unquote(geometry)) 244 | end 245 | 246 | defmacro st_geometry_n(geometry, int) do 247 | quote do: fragment("ST_GeometryN(?, ?)", unquote(geometry), unquote(int)) 248 | end 249 | 250 | defmacro st_num_points(geometry) do 251 | quote do: fragment("ST_NumPoints(?)", unquote(geometry)) 252 | end 253 | 254 | defmacro st_point_n(geometry, int) do 255 | quote do: fragment("ST_PointN(?, ?)", unquote(geometry), unquote(int)) 256 | end 257 | 258 | defmacro st_point(x, y) do 259 | quote do: fragment("ST_Point(?, ?)", unquote(x), unquote(y)) 260 | end 261 | 262 | defmacro st_exterior_ring(geometry) do 263 | quote do: fragment("ST_ExteriorRing(?)", unquote(geometry)) 264 | end 265 | 266 | defmacro st_num_interior_rings(geometry) do 267 | quote do: fragment("ST_NumInteriorRings(?)", unquote(geometry)) 268 | end 269 | 270 | defmacro st_num_interior_ring(geometry) do 271 | quote do: fragment("ST_NumInteriorRing(?)", unquote(geometry)) 272 | end 273 | 274 | defmacro st_interior_ring_n(geometry, int) do 275 | quote do: fragment("ST_InteriorRingN(?, ?)", unquote(geometry), unquote(int)) 276 | end 277 | 278 | defmacro st_end_point(geometry) do 279 | quote do: fragment("ST_EndPoint(?)", unquote(geometry)) 280 | end 281 | 282 | defmacro st_start_point(geometry) do 283 | quote do: fragment("ST_StartPoint(?)", unquote(geometry)) 284 | end 285 | 286 | defmacro st_geometry_type(geometry) do 287 | quote do: fragment("ST_GeometryType(?)", unquote(geometry)) 288 | end 289 | 290 | defmacro st_x(geometry) do 291 | quote do: fragment("ST_X(?)", unquote(geometry)) 292 | end 293 | 294 | defmacro st_y(geometry) do 295 | quote do: fragment("ST_Y(?)", unquote(geometry)) 296 | end 297 | 298 | defmacro st_z(geometry) do 299 | quote do: fragment("ST_Z(?)", unquote(geometry)) 300 | end 301 | 302 | defmacro st_m(geometry) do 303 | quote do: fragment("ST_M(?)", unquote(geometry)) 304 | end 305 | 306 | defmacro st_points(geometry) do 307 | quote do: fragment("ST_Points(?)", unquote(geometry)) 308 | end 309 | 310 | defmacro st_geom_from_text(text, srid \\ -1) do 311 | quote do: fragment("ST_GeomFromText(?, ?)", unquote(text), unquote(srid)) 312 | end 313 | 314 | defmacro st_point_from_text(text, srid \\ -1) do 315 | quote do: fragment("ST_PointFromText(?, ?)", unquote(text), unquote(srid)) 316 | end 317 | 318 | defmacro st_line_from_text(text, srid \\ -1) do 319 | quote do: fragment("ST_LineFromText(?, ?)", unquote(text), unquote(srid)) 320 | end 321 | 322 | defmacro st_linestring_from_text(text, srid \\ -1) do 323 | quote do: fragment("ST_LinestringFromText(?, ?)", unquote(text), unquote(srid)) 324 | end 325 | 326 | defmacro st_polygon_from_text(text, srid \\ -1) do 327 | quote do: fragment("ST_PolygonFromText(?, ?)", unquote(text), unquote(srid)) 328 | end 329 | 330 | defmacro st_m_point_from_text(text, srid \\ -1) do 331 | quote do: fragment("ST_MPointFromText(?, ?)", unquote(text), unquote(srid)) 332 | end 333 | 334 | defmacro st_m_line_from_text(text, srid \\ -1) do 335 | quote do: fragment("ST_MLineFromText(?, ?)", unquote(text), unquote(srid)) 336 | end 337 | 338 | defmacro st_m_poly_from_text(text, srid \\ -1) do 339 | quote do: fragment("ST_MPolyFromText(?, ?)", unquote(text), unquote(srid)) 340 | end 341 | 342 | defmacro st_m_geom_coll_from_text(text, srid \\ -1) do 343 | quote do: fragment("ST_GeomCollFromText(?, ?)", unquote(text), unquote(srid)) 344 | end 345 | 346 | defmacro st_m_geom_from_wkb(bytea, srid \\ -1) do 347 | quote do: fragment("ST_GeomFromWKB(?, ?)", unquote(bytea), unquote(srid)) 348 | end 349 | 350 | defmacro st_m_geometry_from_wkb(bytea, srid \\ -1) do 351 | quote do: fragment("ST_GeometryFromWKB(?, ?)", unquote(bytea), unquote(srid)) 352 | end 353 | 354 | defmacro st_point_from_wkb(bytea, srid \\ -1) do 355 | quote do: fragment("ST_PointFromWKB(?, ?)", unquote(bytea), unquote(srid)) 356 | end 357 | 358 | defmacro st_line_from_wkb(bytea, srid \\ -1) do 359 | quote do: fragment("ST_LineFromWKB(?, ?)", unquote(bytea), unquote(srid)) 360 | end 361 | 362 | defmacro st_linestring_from_wkb(bytea, srid \\ -1) do 363 | quote do: fragment("ST_LinestringFromWKB(?, ?)", unquote(bytea), unquote(srid)) 364 | end 365 | 366 | defmacro st_poly_from_wkb(bytea, srid \\ -1) do 367 | quote do: fragment("ST_PolyFromWKB(?, ?)", unquote(bytea), unquote(srid)) 368 | end 369 | 370 | defmacro st_polygon_from_wkb(bytea, srid \\ -1) do 371 | quote do: fragment("ST_PolygonFromWKB(?, ?)", unquote(bytea), unquote(srid)) 372 | end 373 | 374 | defmacro st_m_point_from_wkb(bytea, srid \\ -1) do 375 | quote do: fragment("ST_MPointFromWKB(?, ?)", unquote(bytea), unquote(srid)) 376 | end 377 | 378 | defmacro st_m_line_from_wkb(bytea, srid \\ -1) do 379 | quote do: fragment("ST_MLineFromWKB(?, ?)", unquote(bytea), unquote(srid)) 380 | end 381 | 382 | defmacro st_m_poly_from_wkb(bytea, srid \\ -1) do 383 | quote do: fragment("ST_MPolyFromWKB(?, ?)", unquote(bytea), unquote(srid)) 384 | end 385 | 386 | defmacro st_geom_coll_from_wkb(bytea, srid \\ -1) do 387 | quote do: fragment("ST_GeomCollFromWKB(?, ?)", unquote(bytea), unquote(srid)) 388 | end 389 | 390 | defmacro st_bd_poly_from_text(wkt, srid) do 391 | quote do: fragment("ST_BdPolyFromText(?, ?)", unquote(wkt), unquote(srid)) 392 | end 393 | 394 | defmacro st_bd_m_poly_from_text(wkt, srid) do 395 | quote do: fragment("ST_BdMPolyFromText(?, ?)", unquote(wkt), unquote(srid)) 396 | end 397 | 398 | defmacro st_flip_coordinates(geometryA) do 399 | quote do: fragment("ST_FlipCoordinates(?)", unquote(geometryA)) 400 | end 401 | 402 | defmacro st_generate_points(geometryA, npoints) do 403 | quote do: fragment("ST_GeneratePoints(?,?)", unquote(geometryA), unquote(npoints)) 404 | end 405 | 406 | defmacro st_generate_points(geometryA, npoints, seed) do 407 | quote do: 408 | fragment( 409 | "ST_GeneratePoints(?,?,?)", 410 | unquote(geometryA), 411 | unquote(npoints), 412 | unquote(seed) 413 | ) 414 | end 415 | 416 | defmacro st_extent(geometry) do 417 | quote do: fragment("ST_EXTENT(?)::geometry", unquote(geometry)) 418 | end 419 | 420 | defmacro st_build_area(geometryA) do 421 | quote do: fragment("ST_BuildArea(?)", unquote(geometryA)) 422 | end 423 | 424 | defmacro st_is_valid(geometry) do 425 | quote do: fragment("ST_IsValid(?)", unquote(geometry)) 426 | end 427 | 428 | defmacro st_make_valid(geometry) do 429 | quote do: fragment("ST_MakeValid(?)", unquote(geometry)) 430 | end 431 | 432 | defmacro st_make_valid(geometry, params) do 433 | quote do: fragment("ST_MakeValid(?, ?)", unquote(geometry), unquote(params)) 434 | end 435 | 436 | defmacro st_make_point(x, y) do 437 | quote do: fragment("ST_MakePoint(?, ?)", unquote(x), unquote(y)) 438 | end 439 | 440 | defmacro st_make_point(x, y, z) do 441 | quote do: fragment("ST_MakePoint(?, ?, ?)", unquote(x), unquote(y), unquote(z)) 442 | end 443 | 444 | defmacro st_make_point(x, y, z, m) do 445 | quote do: fragment("ST_MakePoint(?, ?, ?, ?)", unquote(x), unquote(y), unquote(z), unquote(m)) 446 | end 447 | 448 | defmacro st_make_envelope(xMin, yMin, xMax, yMax) do 449 | quote do: 450 | fragment( 451 | "ST_MakeEnvelope(?, ?, ?, ?)", 452 | unquote(xMin), 453 | unquote(yMin), 454 | unquote(xMax), 455 | unquote(yMax) 456 | ) 457 | end 458 | 459 | defmacro st_make_envelope(xMin, yMin, xMax, yMax, srid) do 460 | quote do: 461 | fragment( 462 | "ST_MakeEnvelope(?, ?, ?, ?, ?)", 463 | unquote(xMin), 464 | unquote(yMin), 465 | unquote(xMax), 466 | unquote(yMax), 467 | unquote(srid) 468 | ) 469 | end 470 | 471 | defmacro st_node(geometry) do 472 | quote do: fragment("ST_Node(?)", unquote(geometry)) 473 | end 474 | 475 | defmacro st_line_merge(geometry) do 476 | quote do: fragment("ST_LineMerge(?)", unquote(geometry)) 477 | end 478 | 479 | @spec st_line_merge(Geo.MultiLineString.t(), boolean()) :: term() 480 | defmacro st_line_merge(geometry, directed) do 481 | quote do: fragment("ST_LineMerge(?, ?)", unquote(geometry), unquote(directed)) 482 | end 483 | 484 | defmacro st_line_interpolate_point(geometry, fraction) do 485 | quote do: fragment("ST_LineInterpolatePoint(?, ?)", unquote(geometry), unquote(fraction)) 486 | end 487 | 488 | defmacro st_line_interpolate_points(geometry, fraction, repeat) do 489 | quote do: 490 | fragment( 491 | "ST_LineInterpolatePoints(?, ?, ?)", 492 | unquote(geometry), 493 | unquote(fraction), 494 | unquote(repeat) 495 | ) 496 | end 497 | 498 | defmacro st_line_locate_point(lineGeometry, pointGeometry) do 499 | quote do: fragment("ST_LineLocatePoint(?, ?)", unquote(lineGeometry), unquote(pointGeometry)) 500 | end 501 | 502 | defmacro st_line_substring(geometry, startFraction, endFraction) do 503 | quote do: 504 | fragment( 505 | "ST_LineSubstring(?, ?, ?)", 506 | unquote(geometry), 507 | unquote(startFraction), 508 | unquote(endFraction) 509 | ) 510 | end 511 | 512 | defmacro st_dump(geometry) do 513 | quote do: fragment("ST_Dump(?)", unquote(geometry)) 514 | end 515 | end 516 | -------------------------------------------------------------------------------- /lib/geo_postgis/config.ex: -------------------------------------------------------------------------------- 1 | defmodule Geo.PostGIS.Config do 2 | if Code.ensure_loaded?(JSON) do 3 | @default_json_library JSON 4 | else 5 | @default_json_library Poison 6 | end 7 | 8 | def json_library do 9 | Application.get_env(:geo_postgis, :json_library, @default_json_library) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/geo_postgis/extension.ex: -------------------------------------------------------------------------------- 1 | defmodule Geo.PostGIS.Extension do 2 | @moduledoc """ 3 | PostGIS extension for Postgrex. Supports Geometry and Geography data types. 4 | 5 | ## Examples 6 | 7 | Create a new Postgrex Types module: 8 | 9 | Postgrex.Types.define(MyApp.PostgresTypes, [Geo.PostGIS.Extension], []) 10 | 11 | If using with Ecto, you may want something like thing instead: 12 | 13 | Postgrex.Types.define(MyApp.PostgresTypes, 14 | [Geo.PostGIS.Extension] ++ Ecto.Adapters.Postgres.extensions(), 15 | json: Poison) 16 | 17 | opts = [hostname: "localhost", username: "postgres", database: "geo_postgrex_test", 18 | types: MyApp.PostgresTypes ] 19 | 20 | [hostname: "localhost", username: "postgres", database: "geo_postgrex_test", 21 | types: MyApp.PostgresTypes] 22 | 23 | {:ok, pid} = Postgrex.Connection.start_link(opts) 24 | {:ok, #PID<0.115.0>} 25 | 26 | geo = %Geo.Point{coordinates: {30, -90}, srid: 4326} 27 | %Geo.Point{coordinates: {30, -90}, srid: 4326} 28 | 29 | {:ok, _} = Postgrex.Connection.query(pid, "CREATE TABLE point_test (id int, geom geometry(Point, 4326))") 30 | {:ok, %Postgrex.Result{columns: nil, command: :create_table, num_rows: 0, rows: nil}} 31 | 32 | {:ok, _} = Postgrex.Connection.query(pid, "INSERT INTO point_test VALUES ($1, $2)", [42, geo]) 33 | {:ok, %Postgrex.Result{columns: nil, command: :insert, num_rows: 1, rows: nil}} 34 | 35 | Postgrex.Connection.query(pid, "SELECT * FROM point_test") 36 | {:ok, %Postgrex.Result{columns: ["id", "geom"], command: :select, num_rows: 1, 37 | rows: [{42, %Geo.Point{coordinates: {30.0, -90.0}, srid: 4326}}]}} 38 | 39 | """ 40 | 41 | @behaviour Postgrex.Extension 42 | 43 | @geo_types [ 44 | Geo.GeometryCollection, 45 | Geo.LineString, 46 | Geo.LineStringZ, 47 | Geo.LineStringZM, 48 | Geo.MultiLineString, 49 | Geo.MultiLineStringZ, 50 | Geo.MultiLineStringZM, 51 | Geo.MultiPoint, 52 | Geo.MultiPointZ, 53 | Geo.MultiPolygon, 54 | Geo.MultiPolygonZ, 55 | Geo.Point, 56 | Geo.PointZ, 57 | Geo.PointM, 58 | Geo.PointZM, 59 | Geo.Polygon, 60 | Geo.PolygonZ 61 | ] 62 | 63 | def init(opts) do 64 | Keyword.get(opts, :decode_copy, :copy) 65 | end 66 | 67 | def matching(_) do 68 | [type: "geometry", type: "geography"] 69 | end 70 | 71 | def format(_) do 72 | :binary 73 | end 74 | 75 | def encode(_opts) do 76 | quote location: :keep do 77 | %x{} = geom when x in unquote(@geo_types) -> 78 | data = Geo.WKB.encode_to_iodata(geom) 79 | [<> | data] 80 | end 81 | end 82 | 83 | def decode(:reference) do 84 | quote location: :keep do 85 | <> -> 86 | Geo.WKB.decode!(wkb) 87 | end 88 | end 89 | 90 | def decode(:copy) do 91 | quote location: :keep do 92 | <> -> 93 | Geo.WKB.decode!(:binary.copy(wkb)) 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/geo_postgis/geometry.ex: -------------------------------------------------------------------------------- 1 | if Code.ensure_loaded?(Ecto.Type) do 2 | defmodule Geo.PostGIS.Geometry do 3 | @moduledoc """ 4 | Implements the Ecto.Type behaviour for all geometry types. 5 | """ 6 | 7 | alias Geo.{ 8 | Point, 9 | PointZ, 10 | PointM, 11 | PointZM, 12 | LineString, 13 | LineStringZ, 14 | LineStringZM, 15 | Polygon, 16 | PolygonZ, 17 | MultiPoint, 18 | MultiPointZ, 19 | MultiLineString, 20 | MultiLineStringZ, 21 | MultiLineStringZM, 22 | MultiPolygon, 23 | MultiPolygonZ, 24 | GeometryCollection 25 | } 26 | 27 | @types [ 28 | "Point", 29 | "PointZ", 30 | "PointM", 31 | "PointZM", 32 | "LineString", 33 | "LineStringZ", 34 | "LineStringZM", 35 | "Polygon", 36 | "PolygonZ", 37 | "MultiPoint", 38 | "MultiPointZ", 39 | "MultiLineString", 40 | "MultiLineStringZ", 41 | "MultiLineStringZM", 42 | "MultiPolygon", 43 | "MultiPolygonZ" 44 | ] 45 | 46 | @geometries [ 47 | Point, 48 | PointZ, 49 | PointM, 50 | PointZM, 51 | LineString, 52 | LineStringZ, 53 | LineStringZM, 54 | Polygon, 55 | PolygonZ, 56 | MultiPoint, 57 | MultiPointZ, 58 | MultiLineString, 59 | MultiLineStringZ, 60 | MultiLineStringZM, 61 | MultiPolygon, 62 | MultiPolygonZ, 63 | GeometryCollection 64 | ] 65 | 66 | @type t :: Geo.geometry() 67 | 68 | if macro_exported?(Ecto.Type, :__using__, 1) do 69 | use Ecto.Type 70 | else 71 | @behaviour Ecto.Type 72 | end 73 | 74 | def type, do: :geometry 75 | 76 | def blank?(_), do: false 77 | 78 | def load(%struct{} = geom) when struct in @geometries, do: {:ok, geom} 79 | def load(_), do: :error 80 | 81 | def dump(%struct{} = geom) when struct in @geometries, do: {:ok, geom} 82 | def dump(_), do: :error 83 | 84 | def cast({:ok, value}), do: cast(value) 85 | 86 | def cast(%struct{} = geom) when struct in @geometries, do: {:ok, geom} 87 | 88 | def cast(%{"type" => type, "coordinates" => _} = geom) when type in @types do 89 | do_cast(geom) 90 | end 91 | 92 | def cast(%{"type" => "GeometryCollection", "geometries" => _} = geom) do 93 | do_cast(geom) 94 | end 95 | 96 | def cast(%{type: type, coordinates: _} = geom) when type in @types do 97 | string_keys(geom) 98 | |> do_cast() 99 | end 100 | 101 | def cast(%{type: "GeometryCollection", geometries: _} = geom) do 102 | string_keys(geom) 103 | |> do_cast() 104 | end 105 | 106 | def cast(geom) when is_binary(geom) do 107 | do_cast(geom) 108 | end 109 | 110 | def cast(_), do: :error 111 | 112 | def string_keys(input_map) when is_map(input_map) do 113 | Map.new(input_map, fn {key, val} -> {to_string(key), string_keys(val)} end) 114 | end 115 | 116 | def string_keys(input_list) when is_list(input_list) do 117 | Enum.map(input_list, &string_keys(&1)) 118 | end 119 | 120 | def string_keys(other), do: other 121 | 122 | defp do_cast(geom) when is_binary(geom) do 123 | case Geo.PostGIS.Config.json_library().decode(geom) do 124 | {:ok, geom} when is_map(geom) -> do_cast(geom) 125 | {:error, reason} -> {:error, [message: "failed to decode JSON", reason: reason]} 126 | end 127 | end 128 | 129 | defp do_cast(geom) do 130 | case Geo.JSON.decode(geom) do 131 | {:ok, result} -> {:ok, result} 132 | {:error, reason} -> {:error, [message: "failed to decode GeoJSON", reason: reason]} 133 | end 134 | end 135 | 136 | def embed_as(_), do: :self 137 | 138 | def equal?(a, b), do: a == b 139 | end 140 | end 141 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule GeoPostgis.Mixfile do 2 | use Mix.Project 3 | 4 | @source_url "https://github.com/felt/geo_postgis" 5 | @version "3.7.1" 6 | 7 | def project do 8 | [ 9 | app: :geo_postgis, 10 | version: @version, 11 | elixir: "~> 1.11", 12 | start_permanent: Mix.env() == :prod, 13 | name: "GeoPostGIS", 14 | deps: deps(), 15 | package: package(), 16 | docs: docs(), 17 | elixirc_paths: elixirc_paths(Mix.env()) 18 | ] 19 | end 20 | 21 | def application do 22 | [extra_applications: [:logger]] 23 | end 24 | 25 | defp elixirc_paths(:test), do: ["lib", "test/support"] 26 | defp elixirc_paths(_), do: ["lib"] 27 | 28 | defp deps do 29 | [ 30 | {:geo, "~> 3.6 or ~> 4.0"}, 31 | {:postgrex, ">= 0.0.0"}, 32 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, 33 | {:ecto, "~> 3.0", optional: true}, 34 | {:ecto_sql, "~> 3.0", optional: true, only: :test}, 35 | {:poison, "~> 2.2 or ~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", optional: true}, 36 | {:jason, "~> 1.2", optional: true} 37 | ] 38 | end 39 | 40 | defp package do 41 | [ 42 | description: "PostGIS extension for Postgrex.", 43 | files: ["lib", "mix.exs", "README.md", "CHANGELOG.md"], 44 | maintainers: ["Tyler Young", "Bryan Joseph"], 45 | licenses: ["MIT"], 46 | links: %{"GitHub" => @source_url} 47 | ] 48 | end 49 | 50 | defp docs do 51 | [ 52 | extras: ["CHANGELOG.md", "README.md"], 53 | main: "readme", 54 | source_url: @source_url, 55 | source_ref: "v#{@version}", 56 | formatters: ["html"] 57 | ] 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, 3 | "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, 4 | "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, 5 | "ecto": {:hex, :ecto, "3.12.6", "8bf762dc5b87d85b7aca7ad5fe31ef8142a84cea473a3381eb933bd925751300", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4c0cba01795463eebbcd9e4b5ef53c1ee8e68b9c482baef2a80de5a61e7a57fe"}, 6 | "ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"}, 7 | "ex_doc": {:hex, :ex_doc, "0.38.2", "504d25eef296b4dec3b8e33e810bc8b5344d565998cd83914ffe1b8503737c02", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "732f2d972e42c116a70802f9898c51b54916e542cc50968ac6980512ec90f42b"}, 8 | "geo": {:hex, :geo, "4.0.1", "f4ae3fd912b0536bfe9ec3bce15eb554197ab0739c01297c8534c20dcedd561c", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "32eb624feff75d043bbdd43f67e3869c5fc729e221333271b07cdc98ba98563d"}, 9 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 10 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 11 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, 12 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, 13 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, 14 | "poison": {:hex, :poison, "6.0.0", "9bbe86722355e36ffb62c51a552719534257ba53f3271dacd20fbbd6621a583a", [:mix], [{:decimal, "~> 2.1", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "bb9064632b94775a3964642d6a78281c07b7be1319e0016e1643790704e739a2"}, 15 | "postgrex": {:hex, :postgrex, "0.20.0", "363ed03ab4757f6bc47942eff7720640795eb557e1935951c1626f0d303a3aed", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "d36ef8b36f323d29505314f704e21a1a038e2dc387c6409ee0cd24144e187c0f"}, 16 | "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, 17 | } 18 | -------------------------------------------------------------------------------- /test/ecto_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Geo.Ecto.Test do 2 | use ExUnit.Case, async: true 3 | use Ecto.Migration 4 | import Ecto.Query 5 | import Geo.PostGIS 6 | alias Geo.PostGIS.Test.Repo 7 | 8 | @multipoint_wkb "0106000020E6100000010000000103000000010000000F00000091A1EF7505D521C0F4AD6182E481424072B3CE92FED421C01D483CDAE281424085184FAEF7D421C0CB159111E1814240E1EBD7FBF8D421C0D421F7C8DF814240AD111315FFD421C0FE1F21C0DE81424082A0669908D521C050071118DE814240813C5E700FD521C0954EEF97DE814240DC889FA815D521C0B3382182E08142400148A81817D521C0E620D22BE2814240F1E95BDE19D521C08BD53852E3814240F81699E217D521C05B35D7DCE4814240B287C8D715D521C0336338FEE481424085882FB90FD521C0FEF65484E5814240A53E1E460AD521C09A0EA286E581424091A1EF7505D521C0F4AD6182E4814240" 9 | 10 | defmodule Location do 11 | use Ecto.Schema 12 | 13 | schema "locations" do 14 | field(:name, :string) 15 | field(:geom, Geo.PostGIS.Geometry) 16 | end 17 | end 18 | 19 | defmodule Geographies do 20 | use Ecto.Schema 21 | 22 | schema "geographies" do 23 | field(:name, :string) 24 | field(:geom, Geo.PostGIS.Geometry) 25 | end 26 | end 27 | 28 | defmodule LocationMulti do 29 | use Ecto.Schema 30 | 31 | schema "location_multi" do 32 | field(:name, :string) 33 | field(:geom, Geo.PostGIS.Geometry) 34 | end 35 | end 36 | 37 | setup _ do 38 | {:ok, pid} = Postgrex.start_link(Geo.Test.Helper.opts()) 39 | 40 | {:ok, _} = Postgrex.query(pid, "CREATE EXTENSION IF NOT EXISTS postgis", []) 41 | 42 | {:ok, _} = 43 | Postgrex.query(pid, "DROP TABLE IF EXISTS locations, geographies, location_multi", []) 44 | 45 | {:ok, _} = 46 | Postgrex.query( 47 | pid, 48 | "CREATE TABLE locations (id serial primary key, name varchar, geom geometry(MultiPolygon))", 49 | [] 50 | ) 51 | 52 | {:ok, _} = 53 | Postgrex.query( 54 | pid, 55 | "CREATE TABLE geographies (id serial primary key, name varchar, geom geography(Point))", 56 | [] 57 | ) 58 | 59 | {:ok, _} = 60 | Postgrex.query( 61 | pid, 62 | "CREATE TABLE location_multi (id serial primary key, name varchar, geom geometry)", 63 | [] 64 | ) 65 | 66 | {:ok, _} = Repo.start_link() 67 | 68 | :ok 69 | end 70 | 71 | test "query multipoint" do 72 | geom = Geo.WKB.decode!(@multipoint_wkb) 73 | 74 | Repo.insert(%Location{name: "hello", geom: geom}) 75 | query = from(location in Location, limit: 5, select: location) 76 | results = Repo.all(query) 77 | 78 | assert geom == hd(results).geom 79 | end 80 | 81 | test "query area" do 82 | geom = Geo.WKB.decode!(@multipoint_wkb) 83 | 84 | Repo.insert(%Location{name: "hello", geom: geom}) 85 | 86 | query = from(location in Location, limit: 5, select: st_area(location.geom)) 87 | results = Repo.all(query) 88 | 89 | assert is_number(hd(results)) 90 | end 91 | 92 | test "query transform" do 93 | geom = Geo.WKB.decode!(@multipoint_wkb) 94 | 95 | Repo.insert(%Location{name: "hello", geom: geom}) 96 | 97 | query = from(location in Location, limit: 1, select: st_transform(location.geom, 3452)) 98 | results = Repo.one(query) 99 | 100 | assert results.srid == 3452 101 | end 102 | 103 | test "query distance" do 104 | geom = Geo.WKB.decode!(@multipoint_wkb) 105 | 106 | Repo.insert(%Location{name: "hello", geom: geom}) 107 | 108 | query = from(location in Location, limit: 5, select: st_distance(location.geom, ^geom)) 109 | results = Repo.one(query) 110 | 111 | assert results == 0 112 | end 113 | 114 | test "query sphere distance" do 115 | geom = Geo.WKB.decode!(@multipoint_wkb) 116 | 117 | Repo.insert(%Location{name: "hello", geom: geom}) 118 | 119 | query = from(location in Location, limit: 5, select: st_distancesphere(location.geom, ^geom)) 120 | results = Repo.one(query) 121 | 122 | assert results == 0 123 | end 124 | 125 | test "st_extent" do 126 | geom = Geo.WKB.decode!(@multipoint_wkb) 127 | 128 | Repo.insert(%Location{name: "hello", geom: geom}) 129 | 130 | query = from(location in Location, select: st_extent(location.geom)) 131 | assert [%Geo.Polygon{coordinates: [coordinates]}] = Repo.all(query) 132 | assert length(coordinates) == 5 133 | end 134 | 135 | test "example" do 136 | geom = Geo.WKB.decode!(@multipoint_wkb) 137 | Repo.insert(%Location{name: "hello", geom: geom}) 138 | 139 | defmodule Example do 140 | import Ecto.Query 141 | import Geo.PostGIS 142 | 143 | def example_query(geom) do 144 | from(location in Location, select: st_distance(location.geom, ^geom)) 145 | end 146 | end 147 | 148 | query = Example.example_query(geom) 149 | results = Repo.one(query) 150 | assert results == 0 151 | end 152 | 153 | test "geography" do 154 | geom = %Geo.Point{coordinates: {30, -90}, srid: 4326} 155 | 156 | Repo.insert(%Geographies{name: "hello", geom: geom}) 157 | query = from(location in Geographies, limit: 5, select: location) 158 | results = Repo.all(query) 159 | 160 | assert geom == hd(results).geom 161 | end 162 | 163 | test "cast point" do 164 | geom = %Geo.Point{coordinates: {30, -90}, srid: 4326} 165 | 166 | Repo.insert(%Geographies{name: "hello", geom: geom}) 167 | query = from(location in Geographies, limit: 5, select: location) 168 | results = Repo.all(query) 169 | 170 | result = hd(results) 171 | 172 | json = Geo.JSON.encode(%Geo.Point{coordinates: {31, -90}, srid: 4326}) 173 | 174 | changeset = 175 | Ecto.Changeset.cast(result, %{title: "Hello", geom: json}, [:name, :geom]) 176 | |> Ecto.Changeset.validate_required([:name, :geom]) 177 | 178 | assert changeset.changes == %{geom: %Geo.Point{coordinates: {31, -90}, srid: 4326}} 179 | end 180 | 181 | test "cast point from map" do 182 | geom = %Geo.Point{coordinates: {30, -90}, srid: 4326} 183 | 184 | Repo.insert(%Geographies{name: "hello", geom: geom}) 185 | query = from(location in Geographies, limit: 5, select: location) 186 | results = Repo.all(query) 187 | 188 | result = hd(results) 189 | 190 | json = %{ 191 | "type" => "Point", 192 | "crs" => %{"type" => "name", "properties" => %{"name" => "EPSG:4326"}}, 193 | "coordinates" => [31, -90] 194 | } 195 | 196 | changeset = 197 | Ecto.Changeset.cast(result, %{title: "Hello", geom: json}, [:name, :geom]) 198 | |> Ecto.Changeset.validate_required([:name, :geom]) 199 | 200 | assert changeset.changes == %{geom: %Geo.Point{coordinates: {31, -90}, srid: 4326}} 201 | end 202 | 203 | test "order by distance" do 204 | geom1 = %Geo.Point{coordinates: {30, -90}, srid: 4326} 205 | geom2 = %Geo.Point{coordinates: {30, -91}, srid: 4326} 206 | geom3 = %Geo.Point{coordinates: {60, -91}, srid: 4326} 207 | 208 | Repo.insert(%Geographies{name: "there", geom: geom2}) 209 | Repo.insert(%Geographies{name: "here", geom: geom1}) 210 | Repo.insert(%Geographies{name: "way over there", geom: geom3}) 211 | 212 | query = 213 | from( 214 | location in Geographies, 215 | limit: 5, 216 | select: location, 217 | order_by: st_distance(location.geom, ^geom1) 218 | ) 219 | 220 | assert ["here", "there", "way over there"] == 221 | Repo.all(query) 222 | |> Enum.map(fn x -> x.name end) 223 | end 224 | 225 | test "insert multiple geometry types" do 226 | geom1 = %Geo.Point{coordinates: {30, -90}, srid: 4326} 227 | geom2 = %Geo.LineString{coordinates: [{30, -90}, {30, -91}], srid: 4326} 228 | 229 | Repo.insert(%LocationMulti{name: "hello point", geom: geom1}) 230 | Repo.insert(%LocationMulti{name: "hello line", geom: geom2}) 231 | query = from(location in LocationMulti, select: location) 232 | [m1, m2] = Repo.all(query) 233 | 234 | assert m1.geom == geom1 235 | assert m2.geom == geom2 236 | end 237 | 238 | describe "st_is_closed/1" do 239 | test "returns true for a closed linestring" do 240 | closed_line = %Geo.LineString{ 241 | coordinates: [{0, 0}, {1, 0}, {1, 1}, {0, 1}, {0, 0}], 242 | srid: 4326 243 | } 244 | 245 | Repo.insert(%LocationMulti{name: "closed_line", geom: closed_line}) 246 | 247 | query = 248 | from(l in LocationMulti, 249 | where: l.name == "closed_line", 250 | select: st_is_closed(l.geom) 251 | ) 252 | 253 | result = Repo.one(query) 254 | assert result == true 255 | end 256 | 257 | test "returns false for an open linestring" do 258 | open_line = %Geo.LineString{ 259 | coordinates: [{0, 0}, {1, 0}, {1, 1}, {0, 1}], 260 | srid: 4326 261 | } 262 | 263 | Repo.insert(%LocationMulti{name: "open_line", geom: open_line}) 264 | 265 | query = 266 | from(l in LocationMulti, 267 | where: l.name == "open_line", 268 | select: st_is_closed(l.geom) 269 | ) 270 | 271 | result = Repo.one(query) 272 | assert result == false 273 | end 274 | 275 | test "returns false for a multilinestring" do 276 | open_line = %Geo.MultiLineString{ 277 | coordinates: [ 278 | [{0, 0}, {1, 0}, {1, 1}, {0, 0}], 279 | [{0, 0}, {1, 0}] 280 | ], 281 | srid: 4326 282 | } 283 | 284 | Repo.insert(%LocationMulti{name: "multilinestring", geom: open_line}) 285 | 286 | query = 287 | from(l in LocationMulti, 288 | where: l.name == "multilinestring", 289 | select: st_is_closed(l.geom) 290 | ) 291 | 292 | result = Repo.one(query) 293 | assert result == false 294 | end 295 | 296 | test "returns true for a point" do 297 | point = %Geo.Point{coordinates: {0, 0}, srid: 4326} 298 | 299 | Repo.insert(%LocationMulti{name: "point", geom: point}) 300 | 301 | query = 302 | from(l in LocationMulti, 303 | where: l.name == "point", 304 | select: st_is_closed(l.geom) 305 | ) 306 | 307 | result = Repo.one(query) 308 | assert result == true 309 | end 310 | 311 | test "returns true for a multipoint" do 312 | point = %Geo.MultiPoint{coordinates: [{0, 0}, {1, 1}], srid: 4326} 313 | 314 | Repo.insert(%LocationMulti{name: "multipoint", geom: point}) 315 | 316 | query = 317 | from(l in LocationMulti, 318 | where: l.name == "multipoint", 319 | select: st_is_closed(l.geom) 320 | ) 321 | 322 | result = Repo.one(query) 323 | assert result == true 324 | end 325 | end 326 | 327 | describe "st_node" do 328 | test "self-intersecting linestring" do 329 | coordinates = [{0, 0, 0}, {2, 2, 2}, {0, 2, 0}, {2, 0, 2}] 330 | cross_point = {1, 1, 1} 331 | 332 | # Create a self-intersecting linestring (crossing at point {1, 1, 1}) 333 | linestring = %Geo.LineStringZ{ 334 | coordinates: coordinates, 335 | srid: 4326 336 | } 337 | 338 | Repo.insert(%LocationMulti{name: "intersecting lines", geom: linestring}) 339 | 340 | query = 341 | from( 342 | location in LocationMulti, 343 | select: st_node(location.geom) 344 | ) 345 | 346 | result = Repo.one(query) 347 | 348 | assert %Geo.MultiLineStringZ{} = result 349 | 350 | assert result.coordinates == [ 351 | [Enum.at(coordinates, 0), cross_point], 352 | [cross_point, Enum.at(coordinates, 1), Enum.at(coordinates, 2), cross_point], 353 | [cross_point, Enum.at(coordinates, 3)] 354 | ] 355 | end 356 | 357 | test "intersecting multilinestring" do 358 | coordinates1 = [{0, 0, 0}, {2, 2, 2}] 359 | coordinates2 = [{0, 2, 0}, {2, 0, 2}] 360 | cross_point = {1, 1, 1} 361 | 362 | # Create a multilinestring that intersects (crossing at point {1, 1, 1}) 363 | linestring = %Geo.MultiLineStringZ{ 364 | coordinates: [ 365 | coordinates1, 366 | coordinates2 367 | ], 368 | srid: 4326 369 | } 370 | 371 | Repo.insert(%LocationMulti{name: "intersecting lines", geom: linestring}) 372 | 373 | query = 374 | from( 375 | location in LocationMulti, 376 | select: st_node(location.geom) 377 | ) 378 | 379 | result = Repo.one(query) 380 | 381 | assert %Geo.MultiLineStringZ{} = result 382 | 383 | assert result.coordinates == [ 384 | [Enum.at(coordinates1, 0), cross_point], 385 | [Enum.at(coordinates2, 0), cross_point], 386 | [cross_point, Enum.at(coordinates1, 1)], 387 | [cross_point, Enum.at(coordinates2, 1)] 388 | ] 389 | end 390 | end 391 | 392 | describe "st_line_merge/1" do 393 | test "merge lines with different orientation" do 394 | multiline = %Geo.MultiLineString{ 395 | coordinates: [ 396 | [{10, 160}, {60, 120}], 397 | [{120, 140}, {60, 120}], 398 | [{120, 140}, {180, 120}] 399 | ], 400 | srid: 4326 401 | } 402 | 403 | Repo.insert(%LocationMulti{name: "lines with different orientation", geom: multiline}) 404 | 405 | query = 406 | from( 407 | location in LocationMulti, 408 | where: location.name == "lines with different orientation", 409 | select: st_line_merge(location.geom) 410 | ) 411 | 412 | result = Repo.one(query) 413 | 414 | assert %Geo.LineString{} = result 415 | 416 | assert result.coordinates == [ 417 | {10, 160}, 418 | {60, 120}, 419 | {120, 140}, 420 | {180, 120} 421 | ] 422 | end 423 | 424 | test "lines not merged across intersections with degree > 2" do 425 | multiline = %Geo.MultiLineString{ 426 | coordinates: [ 427 | [{10, 160}, {60, 120}], 428 | [{120, 140}, {60, 120}], 429 | [{120, 140}, {180, 120}], 430 | [{100, 180}, {120, 140}] 431 | ], 432 | srid: 4326 433 | } 434 | 435 | Repo.insert(%LocationMulti{name: "lines with intersection degree > 2", geom: multiline}) 436 | 437 | query = 438 | from( 439 | location in LocationMulti, 440 | where: location.name == "lines with intersection degree > 2", 441 | select: st_line_merge(location.geom) 442 | ) 443 | 444 | result = Repo.one(query) 445 | 446 | # Verify the result is still multiple lines after merging and consists of 3 linestrings 447 | assert %Geo.MultiLineString{} = result 448 | assert length(result.coordinates) == 3 449 | 450 | expected_linestrings = [ 451 | [{10, 160}, {60, 120}, {120, 140}], 452 | [{100, 180}, {120, 140}], 453 | [{120, 140}, {180, 120}] 454 | ] 455 | 456 | sorted_result = Enum.sort_by(result.coordinates, fn linestring -> hd(linestring) end) 457 | sorted_expected = Enum.sort_by(expected_linestrings, fn linestring -> hd(linestring) end) 458 | 459 | assert sorted_result == sorted_expected 460 | end 461 | 462 | test "return original geometry if not possible to merge" do 463 | multiline = %Geo.MultiLineString{ 464 | coordinates: [ 465 | [{-29, -27}, {-30, -29.7}, {-36, -31}, {-45, -33}], 466 | [{-45.2, -33.2}, {-46, -32}] 467 | ], 468 | srid: 4326 469 | } 470 | 471 | Repo.insert(%LocationMulti{name: "disconnected lines", geom: multiline}) 472 | 473 | query = 474 | from( 475 | location in LocationMulti, 476 | where: location.name == "disconnected lines", 477 | select: st_line_merge(location.geom) 478 | ) 479 | 480 | result = Repo.one(query) 481 | 482 | # Verify the result is not merged and consists of 2 linestrings 483 | assert %Geo.MultiLineString{} = result 484 | assert length(result.coordinates) == 2 485 | 486 | expected_linestrings = [ 487 | [{-29, -27}, {-30, -29.7}, {-36, -31}, {-45, -33}], 488 | [{-45.2, -33.2}, {-46, -32}] 489 | ] 490 | 491 | sorted_result = Enum.sort_by(result.coordinates, fn linestring -> hd(linestring) end) 492 | sorted_expected = Enum.sort_by(expected_linestrings, fn linestring -> hd(linestring) end) 493 | 494 | assert sorted_result == sorted_expected 495 | end 496 | end 497 | 498 | describe "st_line_merge/2" do 499 | test "lines with opposite directions not merged if directed is true" do 500 | multiline = %Geo.MultiLineString{ 501 | coordinates: [ 502 | [{60, 30}, {10, 70}], 503 | [{120, 50}, {60, 30}], 504 | [{120, 50}, {180, 30}] 505 | ], 506 | srid: 4326 507 | } 508 | 509 | Repo.insert(%LocationMulti{name: "lines with direction", geom: multiline}) 510 | 511 | query = 512 | from( 513 | location in LocationMulti, 514 | where: location.name == "lines with direction", 515 | select: st_line_merge(location.geom, true) 516 | ) 517 | 518 | result = Repo.one(query) 519 | 520 | # Verify the result is still a MultiLineString with only 2 lines merged 521 | assert %Geo.MultiLineString{} = result 522 | assert length(result.coordinates) == 2 523 | 524 | expected_linestrings = [ 525 | [{120, 50}, {60, 30}, {10, 70}], 526 | [{120, 50}, {180, 30}] 527 | ] 528 | 529 | sorted_result = Enum.sort_by(result.coordinates, fn linestring -> hd(linestring) end) 530 | sorted_expected = Enum.sort_by(expected_linestrings, fn linestring -> hd(linestring) end) 531 | 532 | assert sorted_result == sorted_expected 533 | end 534 | 535 | test "lines with opposite directions merged if directed is false" do 536 | multiline = %Geo.MultiLineString{ 537 | coordinates: [ 538 | [{60, 30}, {10, 70}], 539 | [{120, 50}, {60, 30}], 540 | [{120, 50}, {180, 30}] 541 | ], 542 | srid: 4326 543 | } 544 | 545 | Repo.insert(%LocationMulti{name: "lines with direction", geom: multiline}) 546 | 547 | query = 548 | from( 549 | location in LocationMulti, 550 | where: location.name == "lines with direction", 551 | select: st_line_merge(location.geom, false) 552 | ) 553 | 554 | result = Repo.one(query) 555 | 556 | assert %Geo.LineString{} = result 557 | 558 | assert result.coordinates == [ 559 | {180, 30}, 560 | {120, 50}, 561 | {60, 30}, 562 | {10, 70} 563 | ] 564 | end 565 | end 566 | 567 | describe "st_line_interpolate_point" do 568 | test "interpolates point at specified fraction along a linestring" do 569 | line = %Geo.LineString{ 570 | coordinates: [{0, 0}, {100, 200}], 571 | srid: 4326 572 | } 573 | 574 | Repo.insert(%LocationMulti{name: "test_line", geom: line}) 575 | 576 | query = 577 | from(location in LocationMulti, 578 | where: location.name == "test_line", 579 | select: st_line_interpolate_point(location.geom, 0.2) 580 | ) 581 | 582 | result = Repo.one(query) 583 | 584 | assert %Geo.Point{} = result 585 | assert result.coordinates == {20.0, 40.0} 586 | end 587 | 588 | test "interpolate mid-point of a 3D line" do 589 | line = %Geo.LineStringZ{ 590 | coordinates: [{1, 2, 3}, {4, 5, 6}, {6, 7, 8}], 591 | srid: 4326 592 | } 593 | 594 | Repo.insert(%LocationMulti{name: "test_line", geom: line}) 595 | 596 | query = 597 | from(location in LocationMulti, 598 | where: location.name == "test_line", 599 | select: st_line_interpolate_point(location.geom, 0.5) 600 | ) 601 | 602 | result = Repo.one(query) 603 | 604 | assert %Geo.PointZ{} = result 605 | assert result.coordinates == {3.5, 4.5, 5.5} 606 | end 607 | end 608 | 609 | describe "st_line_interpolate_points" do 610 | test "returns points at specified fraction intervals along a linestring" do 611 | # Construct a 9x9 square 612 | points = [{-3, -3}, {-3, 3}, {3, 3}, {3, -3}, {-3, -3}] 613 | 614 | [_first | interval_points] = points 615 | 616 | line = %Geo.LineString{ 617 | coordinates: points, 618 | srid: 4326 619 | } 620 | 621 | Repo.insert(%LocationMulti{name: "test_line", geom: line}) 622 | 623 | # Query to find points at repeating 25% intervals along the line 624 | query = 625 | from(l in LocationMulti, 626 | where: l.name == "test_line", 627 | select: st_line_interpolate_points(l.geom, 0.25, true) 628 | ) 629 | 630 | result = Repo.one(query) 631 | 632 | assert %Geo.MultiPoint{} = result 633 | assert length(result.coordinates) == 4 634 | 635 | assert result.coordinates == interval_points 636 | 637 | [first_interval_point | _rest] = interval_points 638 | 639 | # Query to find only the first 25% interval point (non-repeating) 640 | query = 641 | from(l in LocationMulti, 642 | where: l.name == "test_line", 643 | select: st_line_interpolate_points(l.geom, 0.25, false) 644 | ) 645 | 646 | result = Repo.one(query) 647 | 648 | assert %Geo.Point{} = result 649 | assert result.coordinates == first_interval_point 650 | end 651 | end 652 | 653 | describe "st_line_locate_point" do 654 | test "returns 0.0 for point at start of line" do 655 | line = %Geo.LineString{ 656 | coordinates: [{0, 0}, {1, 1}], 657 | srid: 4326 658 | } 659 | 660 | Repo.insert(%LocationMulti{name: "start_point_test", geom: line}) 661 | 662 | query = 663 | from(location in LocationMulti, 664 | where: location.name == "start_point_test", 665 | select: st_line_locate_point(location.geom, st_set_srid(st_point(0, 0), 4326)) 666 | ) 667 | 668 | result = Repo.one(query) 669 | assert result == 0.0 670 | end 671 | 672 | test "returns 1.0 for point at end of line" do 673 | line = %Geo.LineString{ 674 | coordinates: [{0, 0}, {1, 1}], 675 | srid: 4326 676 | } 677 | 678 | Repo.insert(%LocationMulti{name: "end_point_test", geom: line}) 679 | 680 | query = 681 | from(location in LocationMulti, 682 | where: location.name == "end_point_test", 683 | select: st_line_locate_point(location.geom, st_set_srid(st_point(1, 1), 4326)) 684 | ) 685 | 686 | result = Repo.one(query) 687 | assert result == 1.0 688 | end 689 | 690 | test "returns 0.5 for point at middle of line" do 691 | line = %Geo.LineString{ 692 | coordinates: [{0, 0}, {10, 10}], 693 | srid: 4326 694 | } 695 | 696 | Repo.insert(%LocationMulti{name: "mid_point_test", geom: line}) 697 | 698 | query = 699 | from(location in LocationMulti, 700 | where: location.name == "mid_point_test", 701 | select: st_line_locate_point(location.geom, st_set_srid(st_point(5, 5), 4326)) 702 | ) 703 | 704 | result = Repo.one(query) 705 | assert result == 0.5 706 | end 707 | 708 | test "returns closest point fraction for point not on line" do 709 | line = %Geo.LineString{ 710 | coordinates: [{0, 0}, {10, 0}], 711 | srid: 4326 712 | } 713 | 714 | Repo.insert(%LocationMulti{name: "off_line_test", geom: line}) 715 | 716 | # Point at (5,5) - directly above the midpoint of the line 717 | query = 718 | from(location in LocationMulti, 719 | where: location.name == "off_line_test", 720 | select: st_line_locate_point(location.geom, st_set_srid(st_point(5, 5), 4326)) 721 | ) 722 | 723 | result = Repo.one(query) 724 | # The closest point should be at the midpoint of the line 725 | assert result == 0.5 726 | end 727 | end 728 | 729 | describe "st_dump" do 730 | test "atomic geometry is returned directly" do 731 | point = %Geo.Point{ 732 | coordinates: {0.0, 0.0}, 733 | srid: 4326 734 | } 735 | 736 | Repo.insert(%LocationMulti{name: "point", geom: point}) 737 | 738 | query = 739 | from(location in LocationMulti, 740 | where: location.name == "point", 741 | select: st_dump(location.geom) 742 | ) 743 | 744 | result = Repo.one(query) 745 | assert result == {[], point} 746 | end 747 | 748 | test "breaks a multipolygon into its constituent polygons" do 749 | polygon1 = %Geo.Polygon{ 750 | coordinates: [[{0.0, 0.0}, {0.0, 1.0}, {1.0, 1.0}, {1.0, 0.0}, {0.0, 0.0}]], 751 | srid: 4326 752 | } 753 | 754 | polygon2 = %Geo.Polygon{ 755 | coordinates: [[{2.0, 2.0}, {2.0, 3.0}, {3.0, 3.0}, {3.0, 2.0}, {2.0, 2.0}]], 756 | srid: 4326 757 | } 758 | 759 | Repo.insert(%LocationMulti{name: "polygon1", geom: polygon1}) 760 | Repo.insert(%LocationMulti{name: "polygon2", geom: polygon2}) 761 | 762 | query = 763 | from( 764 | location in LocationMulti, 765 | where: location.name in ["polygon1", "polygon2"], 766 | select: st_dump(st_collect(location.geom)) 767 | ) 768 | 769 | results = Repo.all(query) 770 | 771 | assert length(results) == 2 772 | 773 | Enum.each(results, fn {_path, geom} -> 774 | assert %Geo.Polygon{} = geom 775 | end) 776 | 777 | expected_polygons = MapSet.new([polygon1, polygon2]) 778 | 779 | actual_polygons = MapSet.new(Enum.map(results, fn {_path, geom} -> geom end)) 780 | 781 | assert MapSet.equal?(expected_polygons, actual_polygons) 782 | end 783 | end 784 | 785 | describe "st_line_substring" do 786 | test "returns the first half of a line" do 787 | line = %Geo.LineString{ 788 | coordinates: [{0, 0}, {10, 10}], 789 | srid: 4326 790 | } 791 | 792 | Repo.insert(%LocationMulti{name: "substring_test", geom: line}) 793 | 794 | query = 795 | from(location in LocationMulti, 796 | where: location.name == "substring_test", 797 | select: st_line_substring(location.geom, 0.0, 0.5) 798 | ) 799 | 800 | result = Repo.one(query) 801 | 802 | assert %Geo.LineString{} = result 803 | assert result.coordinates == [{0.0, 0.0}, {5.0, 5.0}] 804 | end 805 | 806 | test "returns the second half of a line" do 807 | line = %Geo.LineString{ 808 | coordinates: [{0, 0}, {10, 10}], 809 | srid: 4326 810 | } 811 | 812 | Repo.insert(%LocationMulti{name: "substring_test", geom: line}) 813 | 814 | query = 815 | from(location in LocationMulti, 816 | where: location.name == "substring_test", 817 | select: st_line_substring(location.geom, 0.5, 1.0) 818 | ) 819 | 820 | result = Repo.one(query) 821 | 822 | assert %Geo.LineString{} = result 823 | assert result.coordinates == [{5.0, 5.0}, {10.0, 10.0}] 824 | end 825 | 826 | test "returns a middle section of a line" do 827 | line = %Geo.LineString{ 828 | coordinates: [{0, 0}, {10, 10}, {20, 0}], 829 | srid: 4326 830 | } 831 | 832 | Repo.insert(%LocationMulti{name: "multi_segment_test", geom: line}) 833 | 834 | query = 835 | from(location in LocationMulti, 836 | where: location.name == "multi_segment_test", 837 | select: st_line_substring(location.geom, 0.25, 0.75) 838 | ) 839 | 840 | result = Repo.one(query) 841 | 842 | assert %Geo.LineString{} = result 843 | # Should include the middle point (10,10) and interpolated points at 25% and 75% 844 | assert result.coordinates == [{5.0, 5.0}, {10.0, 10.0}, {15.0, 5.0}] 845 | end 846 | 847 | test "returns a point when start and end fractions are the same" do 848 | line = %Geo.LineString{ 849 | coordinates: [{0, 0}, {100, 100}], 850 | srid: 4326 851 | } 852 | 853 | Repo.insert(%LocationMulti{name: "point_test", geom: line}) 854 | 855 | query = 856 | from(location in LocationMulti, 857 | where: location.name == "point_test", 858 | select: st_line_substring(location.geom, 0.42, 0.42) 859 | ) 860 | 861 | result = Repo.one(query) 862 | 863 | assert %Geo.Point{} = result 864 | assert result.coordinates == {42.0, 42.0} 865 | end 866 | end 867 | 868 | describe "st_is_collection/1" do 869 | test "returns true for a geometry collection" do 870 | collection = %Geo.GeometryCollection{ 871 | geometries: [ 872 | %Geo.Point{coordinates: {0, 0}, srid: 4326}, 873 | %Geo.LineString{coordinates: [{0, 0}, {1, 1}], srid: 4326} 874 | ], 875 | srid: 4326 876 | } 877 | 878 | Repo.insert(%LocationMulti{name: "collection", geom: collection}) 879 | 880 | query = 881 | from(l in LocationMulti, 882 | where: l.name == "collection", 883 | select: st_is_collection(st_make_valid(l.geom)) 884 | ) 885 | 886 | result = Repo.one(query) 887 | assert result == true 888 | end 889 | 890 | test "returns true for a multi-geometry" do 891 | multi_point = %Geo.MultiPoint{ 892 | coordinates: [{0, 0}, {1, 1}, {2, 2}], 893 | srid: 4326 894 | } 895 | 896 | Repo.insert(%LocationMulti{name: "multi_point", geom: multi_point}) 897 | 898 | query = 899 | from(l in LocationMulti, 900 | where: l.name == "multi_point", 901 | select: st_is_collection(st_make_valid(l.geom)) 902 | ) 903 | 904 | result = Repo.one(query) 905 | assert result == true 906 | end 907 | 908 | test "returns false for a simple geometry" do 909 | point = %Geo.Point{coordinates: {0, 0}, srid: 4326} 910 | 911 | Repo.insert(%LocationMulti{name: "point", geom: point}) 912 | 913 | query = 914 | from(l in LocationMulti, 915 | where: l.name == "point", 916 | select: st_is_collection(l.geom) 917 | ) 918 | 919 | result = Repo.one(query) 920 | assert result == false 921 | end 922 | end 923 | 924 | describe "st_is_empty/1" do 925 | test "returns true for an empty geometry" do 926 | empty_point = %Geo.Point{coordinates: nil, srid: 4326} 927 | 928 | Repo.insert(%LocationMulti{name: "empty_point", geom: empty_point}) 929 | 930 | query = 931 | from(l in LocationMulti, 932 | where: l.name == "empty_point", 933 | select: st_is_empty(l.geom) 934 | ) 935 | 936 | result = Repo.one(query) 937 | assert result == true 938 | end 939 | 940 | test "returns false for a non-empty geometry" do 941 | point = %Geo.Point{coordinates: {0, 0}, srid: 4326} 942 | 943 | Repo.insert(%LocationMulti{name: "non_empty", geom: point}) 944 | 945 | query = 946 | from(l in LocationMulti, 947 | where: l.name == "non_empty", 948 | select: st_is_empty(l.geom) 949 | ) 950 | 951 | result = Repo.one(query) 952 | assert result == false 953 | end 954 | end 955 | 956 | describe "st_points/1" do 957 | test "returns multipoint from a linestring" do 958 | line_coords = [{0.0, 0.0}, {1.0, 1.0}, {2.0, 2.0}] 959 | 960 | line = %Geo.LineString{ 961 | coordinates: line_coords, 962 | srid: 4326 963 | } 964 | 965 | Repo.insert(%LocationMulti{name: "line_for_points", geom: line}) 966 | 967 | query = 968 | from(l in LocationMulti, 969 | where: l.name == "line_for_points", 970 | select: st_points(l.geom) 971 | ) 972 | 973 | result = Repo.one(query) 974 | assert %Geo.MultiPoint{} = result 975 | assert length(result.coordinates) == 3 976 | 977 | assert MapSet.new(line_coords) == MapSet.new(result.coordinates) 978 | end 979 | 980 | test "returns multipoint from a polygon" do 981 | polygon_coords = [{0.0, 0.0}, {0.0, 2.0}, {2.0, 2.0}, {2.0, 0.0}, {0.0, 0.0}] 982 | 983 | polygon = %Geo.Polygon{ 984 | coordinates: [polygon_coords], 985 | srid: 4326 986 | } 987 | 988 | Repo.insert(%LocationMulti{name: "polygon_for_points", geom: polygon}) 989 | 990 | query = 991 | from(l in LocationMulti, 992 | where: l.name == "polygon_for_points", 993 | select: st_points(l.geom) 994 | ) 995 | 996 | result = Repo.one(query) 997 | assert %Geo.MultiPoint{} = result 998 | 999 | # 5 coordinates expected including the equivalent overlapping start/end points 1000 | assert length(result.coordinates) == 5 1001 | 1002 | [overlapping_vertex | _rest] = polygon_coords 1003 | assert Enum.count(result.coordinates, fn coord -> coord == overlapping_vertex end) == 2 1004 | 1005 | assert MapSet.new(polygon_coords) == MapSet.new(result.coordinates) 1006 | end 1007 | end 1008 | end 1009 | -------------------------------------------------------------------------------- /test/geo_postgis_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Geo.PostGIS.Test do 2 | use ExUnit.Case, async: true 3 | 4 | setup do 5 | {:ok, pid} = Postgrex.start_link(Geo.Test.Helper.opts()) 6 | 7 | {:ok, _result} = 8 | Postgrex.query( 9 | pid, 10 | "DROP TABLE IF EXISTS text_test, point_test, linestring_test, linestringz_test, linestringzm_test, polygon_test, multipoint_test, multilinestring_test, multipolygon_test, geometrycollection_test", 11 | [] 12 | ) 13 | 14 | {:ok, [pid: pid]} 15 | end 16 | 17 | test "insert point", context do 18 | pid = context[:pid] 19 | geo = %Geo.Point{coordinates: {30, -90}, srid: 4326} 20 | 21 | {:ok, _} = 22 | Postgrex.query(pid, "CREATE TABLE point_test (id int, geom geometry(Point, 4326))", []) 23 | 24 | {:ok, _} = Postgrex.query(pid, "INSERT INTO point_test VALUES ($1, $2)", [42, geo]) 25 | {:ok, result} = Postgrex.query(pid, "SELECT * FROM point_test", []) 26 | assert(result.rows == [[42, geo]]) 27 | end 28 | 29 | test "insert with text column", context do 30 | pid = context[:pid] 31 | geo = %Geo.Point{coordinates: {30, -90}, srid: 4326} 32 | 33 | {:ok, _} = 34 | Postgrex.query( 35 | pid, 36 | "CREATE TABLE text_test (id int, t text, geom geometry(Point, 4326))", 37 | [] 38 | ) 39 | 40 | {:ok, _} = 41 | Postgrex.query(pid, "INSERT INTO text_test (id, t, geom) VALUES ($1, $2, $3)", [ 42 | 42, 43 | "test", 44 | geo 45 | ]) 46 | 47 | {:ok, result} = Postgrex.query(pid, "SELECT id, t, geom FROM text_test", []) 48 | assert(result.rows == [[42, "test", geo]]) 49 | end 50 | 51 | test "insert pointz", context do 52 | pid = context[:pid] 53 | geo = %Geo.PointZ{coordinates: {30, -90, 70}, srid: 4326} 54 | 55 | {:ok, _} = 56 | Postgrex.query(pid, "CREATE TABLE point_test (id int, geom geometry(PointZ, 4326))", []) 57 | 58 | {:ok, _} = Postgrex.query(pid, "INSERT INTO point_test VALUES ($1, $2)", [42, geo]) 59 | {:ok, result} = Postgrex.query(pid, "SELECT * FROM point_test", []) 60 | assert(result.rows == [[42, geo]]) 61 | end 62 | 63 | test "insert linestring", context do 64 | pid = context[:pid] 65 | geo = %Geo.LineString{srid: 4326, coordinates: [{30, 10}, {10, 30}, {40, 40}]} 66 | 67 | {:ok, _} = 68 | Postgrex.query( 69 | pid, 70 | "CREATE TABLE linestring_test (id int, geom geometry(Linestring, 4326))", 71 | [] 72 | ) 73 | 74 | {:ok, _} = Postgrex.query(pid, "INSERT INTO linestring_test VALUES ($1, $2)", [42, geo]) 75 | {:ok, result} = Postgrex.query(pid, "SELECT * FROM linestring_test", []) 76 | assert(result.rows == [[42, geo]]) 77 | end 78 | 79 | test "insert LineStringZ", context do 80 | pid = context[:pid] 81 | geo = %Geo.LineStringZ{srid: 4326, coordinates: [{30, 10, 20}, {10, 30, 2}, {40, 40, 50}]} 82 | 83 | {:ok, _} = 84 | Postgrex.query( 85 | pid, 86 | "CREATE TABLE linestringz_test (id int, geom geometry(LineStringZ, 4326))", 87 | [] 88 | ) 89 | 90 | {:ok, _} = Postgrex.query(pid, "INSERT INTO linestringz_test VALUES ($1, $2)", [42, geo]) 91 | {:ok, result} = Postgrex.query(pid, "SELECT * FROM linestringz_test", []) 92 | assert result.rows == [[42, geo]] 93 | end 94 | 95 | test "insert LineStringZM", context do 96 | pid = context[:pid] 97 | 98 | geo = %Geo.LineStringZM{ 99 | srid: 4326, 100 | coordinates: [{30, 10, 20, 40}, {10, 30, 2, -10}, {40, 40, 50, 100}] 101 | } 102 | 103 | {:ok, _} = 104 | Postgrex.query( 105 | pid, 106 | "CREATE TABLE linestringzm_test (id int, geom geometry(LineStringZM, 4326))", 107 | [] 108 | ) 109 | 110 | {:ok, _} = Postgrex.query(pid, "INSERT INTO linestringzm_test VALUES ($1, $2)", [42, geo]) 111 | {:ok, result} = Postgrex.query(pid, "SELECT * FROM linestringzm_test", []) 112 | assert result.rows == [[42, geo]] 113 | end 114 | 115 | test "insert polygon", context do 116 | pid = context[:pid] 117 | 118 | geo = %Geo.Polygon{ 119 | coordinates: [ 120 | [{35, 10}, {45, 45}, {15, 40}, {10, 20}, {35, 10}], 121 | [{20, 30}, {35, 35}, {30, 20}, {20, 30}] 122 | ], 123 | srid: 4326 124 | } 125 | 126 | {:ok, _} = 127 | Postgrex.query(pid, "CREATE TABLE polygon_test (id int, geom geometry(Polygon, 4326))", []) 128 | 129 | {:ok, _} = Postgrex.query(pid, "INSERT INTO polygon_test VALUES ($1, $2)", [42, geo]) 130 | {:ok, result} = Postgrex.query(pid, "SELECT * FROM polygon_test", []) 131 | assert(result.rows == [[42, geo]]) 132 | end 133 | 134 | test "insert multipoint", context do 135 | pid = context[:pid] 136 | geo = %Geo.MultiPoint{coordinates: [{0, 0}, {20, 20}, {60, 60}], srid: 4326} 137 | 138 | {:ok, _} = 139 | Postgrex.query( 140 | pid, 141 | "CREATE TABLE multipoint_test (id int, geom geometry(MultiPoint, 4326))", 142 | [] 143 | ) 144 | 145 | {:ok, _} = Postgrex.query(pid, "INSERT INTO multipoint_test VALUES ($1, $2)", [42, geo]) 146 | {:ok, result} = Postgrex.query(pid, "SELECT * FROM multipoint_test", []) 147 | assert(result.rows == [[42, geo]]) 148 | end 149 | 150 | test "insert multilinestring", context do 151 | pid = context[:pid] 152 | 153 | geo = %Geo.MultiLineString{ 154 | coordinates: [[{10, 10}, {20, 20}, {10, 40}], [{40, 40}, {30, 30}, {40, 20}, {30, 10}]], 155 | srid: 4326 156 | } 157 | 158 | {:ok, _} = 159 | Postgrex.query( 160 | pid, 161 | "CREATE TABLE multilinestring_test (id int, geom geometry(MultiLinestring, 4326))", 162 | [] 163 | ) 164 | 165 | {:ok, _} = Postgrex.query(pid, "INSERT INTO multilinestring_test VALUES ($1, $2)", [42, geo]) 166 | {:ok, result} = Postgrex.query(pid, "SELECT * FROM multilinestring_test", []) 167 | assert(result.rows == [[42, geo]]) 168 | end 169 | 170 | test "insert multipolygon", context do 171 | pid = context[:pid] 172 | 173 | geo = %Geo.MultiPolygon{ 174 | coordinates: [ 175 | [[{40, 40}, {20, 45}, {45, 30}, {40, 40}]], 176 | [ 177 | [{20, 35}, {10, 30}, {10, 10}, {30, 5}, {45, 20}, {20, 35}], 178 | [{30, 20}, {20, 15}, {20, 25}, {30, 20}] 179 | ] 180 | ], 181 | srid: 4326 182 | } 183 | 184 | {:ok, _} = 185 | Postgrex.query( 186 | pid, 187 | "CREATE TABLE multipolygon_test (id int, geom geometry(MultiPolygon, 4326))", 188 | [] 189 | ) 190 | 191 | {:ok, _} = Postgrex.query(pid, "INSERT INTO multipolygon_test VALUES ($1, $2)", [42, geo]) 192 | {:ok, result} = Postgrex.query(pid, "SELECT * FROM multipolygon_test", []) 193 | assert(result.rows == [[42, geo]]) 194 | end 195 | end 196 | -------------------------------------------------------------------------------- /test/geography_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Geo.Geography.Test do 2 | use ExUnit.Case, async: true 3 | 4 | setup do 5 | {:ok, pid} = Postgrex.start_link(Geo.Test.Helper.opts()) 6 | {:ok, _result} = Postgrex.query(pid, "DROP TABLE IF EXISTS geography_test", []) 7 | {:ok, [pid: pid]} 8 | end 9 | 10 | test "insert geography point", context do 11 | pid = context[:pid] 12 | geo = %Geo.Point{coordinates: {30, -90}, srid: 4326} 13 | 14 | {:ok, _} = 15 | Postgrex.query(pid, "CREATE TABLE geography_test (id int, geom geography(Point, 4326))", []) 16 | 17 | {:ok, _} = Postgrex.query(pid, "INSERT INTO geography_test VALUES ($1, $2)", [42, geo]) 18 | {:ok, result} = Postgrex.query(pid, "SELECT * FROM geography_test", []) 19 | assert(result.rows == [[42, geo]]) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/geometry_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Geo.PostGIS.GeometryCastTest do 2 | use ExUnit.Case, async: true 3 | alias Geo.PostGIS.Geometry 4 | 5 | describe "Geometry.cast/1" do 6 | test "cast from map with key => value syntax" do 7 | point_map = %{ 8 | "type" => "Point", 9 | "coordinates" => [30.0, -90.0], 10 | "crs" => %{"type" => "name", "properties" => %{"name" => "EPSG:4326"}} 11 | } 12 | 13 | result = Geometry.cast(point_map) 14 | 15 | assert {:ok, point} = result 16 | assert %Geo.Point{} = point 17 | assert point.coordinates == {30.0, -90.0} 18 | assert point.srid == 4326 19 | end 20 | 21 | test "cast from map with key: value syntax" do 22 | point_map = %{ 23 | type: "Point", 24 | coordinates: [30.0, -90.0], 25 | crs: %{type: "name", properties: %{name: "EPSG:4326"}} 26 | } 27 | 28 | result = Geometry.cast(point_map) 29 | 30 | assert {:ok, point} = result 31 | assert %Geo.Point{} = point 32 | assert point.coordinates == {30.0, -90.0} 33 | assert point.srid == 4326 34 | end 35 | 36 | test "cast GeometryCollection from map with key => value syntax" do 37 | collection_map = %{ 38 | "type" => "GeometryCollection", 39 | "geometries" => [ 40 | %{ 41 | "type" => "Point", 42 | "coordinates" => [30.0, -90.0] 43 | }, 44 | %{ 45 | "type" => "LineString", 46 | "coordinates" => [[30.0, -30.0], [90.0, -90.0]] 47 | } 48 | ], 49 | "crs" => %{"type" => "name", "properties" => %{"name" => "EPSG:4326"}} 50 | } 51 | 52 | result = Geometry.cast(collection_map) 53 | 54 | assert {:ok, collection} = result 55 | assert %Geo.GeometryCollection{} = collection 56 | assert length(collection.geometries) == 2 57 | assert Enum.at(collection.geometries, 0).__struct__ == Geo.Point 58 | assert Enum.at(collection.geometries, 1).__struct__ == Geo.LineString 59 | assert collection.srid == 4326 60 | end 61 | 62 | test "cast GeometryCollection from map with key: value syntax" do 63 | collection_map = %{ 64 | type: "GeometryCollection", 65 | geometries: [ 66 | %{ 67 | type: "Point", 68 | coordinates: [30.0, -90.0] 69 | }, 70 | %{ 71 | type: "LineString", 72 | coordinates: [[30.0, -30.0], [90.0, -90.0]] 73 | } 74 | ], 75 | crs: %{type: "name", properties: %{name: "EPSG:4326"}} 76 | } 77 | 78 | result = Geometry.cast(collection_map) 79 | 80 | assert {:ok, collection} = result 81 | assert %Geo.GeometryCollection{} = collection 82 | assert length(collection.geometries) == 2 83 | assert Enum.at(collection.geometries, 0).__struct__ == Geo.Point 84 | assert Enum.at(collection.geometries, 1).__struct__ == Geo.LineString 85 | assert collection.srid == 4326 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /test/support/postgrex_types.ex: -------------------------------------------------------------------------------- 1 | Postgrex.Types.define( 2 | Geo.PostGIS.PostgrexTypes, 3 | [Geo.PostGIS.Extension], 4 | decode_binary: :reference 5 | ) 6 | -------------------------------------------------------------------------------- /test/support/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule Geo.PostGIS.Test.Repo do 2 | use Ecto.Repo, otp_app: :geo_postgis, adapter: Ecto.Adapters.Postgres 3 | end 4 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | {:ok, _} = Application.ensure_all_started(:ecto_sql) 2 | 3 | defmodule Geo.Test.Helper do 4 | def opts do 5 | [ 6 | hostname: "localhost", 7 | username: "postgres", 8 | database: "geo_postgrex_test", 9 | types: Geo.PostGIS.PostgrexTypes 10 | ] 11 | end 12 | end 13 | 14 | ExUnit.start() 15 | --------------------------------------------------------------------------------