RGeo: Handling Geospatial Data for Ruby and Ruby on Rails

1.7K Views

November 30, 23

スライド概要

FOSS4G Asia 2023 Seoul
Download materials: https://github.com/smellman/foss4g-asia-2023-rgeo
Schedule: https://talks.osgeo.org/foss4g-asia-2023/talk/9WU8PR/

profile-image

Georepublic / OSGeo.JP / Japan Unix Society / OpenStreetMap Foundation Japan

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

関連スライド

各ページのテキスト
1.

RGeo: Handling Geospatial Data for Ruby and Ruby on Rails Taro Matsuzawa (@smellman) Georepublic Japan FOSS4G Asia 2023 Seoul 1

2.

This presentation available at online. https://smellman.github.io/foss4g-asia-2023-rgeo/ FOSS4G Asia 2023 Seoul 2

3.

My works Georepublic Japan GIS Engineer Sub-President, Japan UNIX Society Director, OSGeo.JP Director, OpenStreetMap Foundation Japan Lead of United Nation OpenGIS/7 core FOSS4G Asia 2023 Seoul 3

4.

My hobbies Breakcore music Playing video games JRPGs, 2D shooting games, etc. Reading novels Fantasy FOSS4G Asia 2023 Seoul 4

5.

My skills Ruby / Ruby on Rails Python PostgreSQL / PostGIS JavaScript / TypeScript React Native MapLibre GL JS AWS CDK with TypeScript UNIX / Linux FOSS4G Asia 2023 Seoul 5

6.

My community works Maintainer of tile.openstreetmap.jp A worldwide tile server for OpenStreetMap Japan community. Maintainer of charites A set of tools for building Mapbox/MapLibre style. Maintainer of redmine-gtt A plugin for Redmine to add spatial features. Contributor of types-ol-ext, redux-persist and more. FOSS4G Asia 2023 Seoul 6

7.

Main topics 1. Ruby language is very powerful and simple, and friendly with Geo Data using RGeo. 2. RGeo is firendly with Ruby on Rails and PostGIS. FOSS4G Asia 2023 Seoul 7

8.

Today's talk 1. Core concepts and data models of RGeo 2. Basics of manipulating and querying geospatial data 3. Integration with Ruby on Rails and real-world application examples using RGeo FOSS4G Asia 2023 Seoul 8

9.

1. Core concepts and data models of RGeo FOSS4G Asia 2023 Seoul 9

10.

What is RGeo? RGeo is a geospatial data library for Ruby. RGeo provides a set of data classes for representing geospatial data. RGeo provides a set of spatial analysis operations and predicates. FOSS4G Asia 2023 Seoul 10

11.

RGeo's implementation RGeo is a pure Ruby library if GEOS is not available. RGeo is a Ruby wrapper of GEOS if GEOS is available. GEOS is a C++ library for manipulating and querying geospatial data. GEOS is a part of OSGeo Foundation. GEOS is used by many geospatial software, such as PostGIS, QGIS, etc. FOSS4G Asia 2023 Seoul 11

12.

RGeo's features RGeo suppports many geospatial data formats. WKT, WKB, GeoJSON, Shapefile, etc. RGeo supports Proj4. RGeo supports many spatial analysis operations and predicates. Buffer, Convex Hull, Intersection, Union, etc. FOSS4G Asia 2023 Seoul 12

13.

RGeo requirements MRI Ruby 2.6.0 or later. Partial support for JRuby 9.0 or later. The FFI implementation of GEOS is available (ffigeos gem required) but CAPI is not. Highly recommended to use MRI Ruby and GEOS CAPI. FOSS4G Asia 2023 Seoul 13

14.

CAPI benchmark CAPI is faster than FFI and pure ruby. ❯ bundle exec ruby benchmark.rb Warming up -------------------------------------with CAPI GEOS 188.859k i/100ms with FFI GEOS 84.720k i/100ms simple ruby 155.000 i/100ms Calculating ------------------------------------with CAPI GEOS 1.967M (± 1.3%) i/s with FFI GEOS 860.127k (± 1.1%) i/s simple ruby 1.579k (± 0.9%) i/s Comparison: with CAPI GEOS: with FFI GEOS: simple ruby: FOSS4G Asia 2023 Seoul 10.010M in 4.321M in 7.905k in 5.089275s 5.023924s 5.008210s 1967118.2 i/s 860127.1 i/s - 2.29x slower 1578.5 i/s - 1246.17x slower 14

15.

How to install (Debian/Ubuntu) $ apt install libgeos-dev libproj-dev proj-data Then $ gem install rgeo or insert the following line into your Gemfile. gem 'rgeo' FOSS4G Asia 2023 Seoul 15

16.

RGeo extensions rgeo-geojson GeoJSON format support rgeo-shapefile Shapefile format support rgeo-proj4 Proj4 support FOSS4G Asia 2023 Seoul 16

17.

Ruby on Rails support RGeo provides ActiveRecord extensions for Ruby on Rails with PostGIS. https://github.com/rgeo/activerecord-postgis-adapter Mysql / Spatialite ActiveRecord adapter is archived or not maintained. FOSS4G Asia 2023 Seoul 17

18.

RGeo's data models RGeo supports OGC Simple Features Specification. RGeo provides a set of data classes for representing geospatial data. Coordinates, Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon, GeometryCollection, etc. FOSS4G Asia 2023 Seoul 18

19.

Coordinates basis Coordinates is a set of X, Y(, Z and M) values. require factory point = point.x point.y 'rgeo' = RGeo::Cartesian.factory factory.point(1, 2) # => 1.0 # => 2.0 FOSS4G Asia 2023 Seoul 19

20.

factories RGeo implements a lot of factories. Cartesian, Geographic, Geographic Projected, Spherical, etc. Cartesian factory is the default factory. FOSS4G Asia 2023 Seoul 20

21.

Cartesian factory require factory point = point.x point.y 'rgeo' = RGeo::Cartesian.factory factory.point(1, 2) # => 1.0 # => 2.0 implementation will be GEOS::CAPIFactory if GEOS is available. FOSS4G Asia 2023 Seoul 21

22.

Ruby Cartesian factory require factory point = point.x point.y 'rgeo' = RGeo::Cartesian.simple_factory factory.point(1, 2) # => 1.0 # => 2.0 Create a 2D Cartesian factory using a Ruby implementation. FOSS4G Asia 2023 Seoul 22

23.

Spherical Factory require factory point = point.x point.y 'rgeo' = RGeo::Geographic.spherical_factory factory.point(1, 2) # => 1.0 # => 2.0 Create a factory that uses a spherical model of Earth when creating and analyzing geometries. FOSS4G Asia 2023 Seoul 23

24.

3D factory require factory point = point.x point.y point.z 'rgeo' = RGeo::Geos.factory(has_z_coordinate: true) factory.point(1, 2, 3) # => 1.0 # => 2.0 # => 3.0 Create a 3D factory using GEOS. FOSS4G Asia 2023 Seoul 24

25.

3D Factory (With M-Coordinate) require 'rgeo' factory = RGeo::Geos.factory(has_z_coordinate: true, has_m_coordinate: true) Create a 3D factory with M-Coordinate using GEOS. FOSS4G Asia 2023 Seoul 25

26.

Specify an SRID require 'rgeo' factory = RGeo::Geos.factory(srid: 4326) point = factory.point(139.766865, 35.680760) # Tokyo station Create a factory with SRID 4326 using GEOS. FOSS4G Asia 2023 Seoul 26

27.

Point require factory point = point.x point.y 'rgeo' = RGeo::Geos.factory(srid: 4326) factory.point(139.766865, 35.680760) # Tokyo station # => 139.766865 # => 35.68076 Create a point from coordinates. FOSS4G Asia 2023 Seoul 27

28.

working with WKT require 'rgeo' factory = RGeo::Geos.factory(srid: 4326) wkt = 'POINT(139.766865 35.680760)' point = factory.parse_wkt(wkt) Create a point from WKT. FOSS4G Asia 2023 Seoul 28

29.

LineString require 'rgeo' factory = RGeo::Geos.factory line_string = factory.line_string([ factory.point(1, 2), factory.point(3, 4), factory.point(5, 6), ]) Create a LineString from points. FOSS4G Asia 2023 Seoul 29

30.

Others LinerRing Polygon GeometryCollection MultiPoint MultiLineString MultiPolygon All features supports WKT. see: https://github.com/rgeo/rgeo/blob/main/doc/Examples.md FOSS4G Asia 2023 Seoul 30

31.

Basics of manipulating and querying geospatial data FOSS4G Asia 2023 Seoul 31

32.

Spatial analysis operations unary predicates. ccw? empty? simple? binary predicates. contains? crosses? disjoint? crosses? intersects? overlaps? FOSS4G Asia 2023 Seoul 32

33.

contains? require require require require "open-uri" "json" "rgeo" "rgeo-geojson" my_lat, my_lng = [45, 5] my_position = RGeo::Cartesian. factory. point(my_lng, my_lat) geojson = URI. open("https://git.io/rhone-alpes.geojson"). read rhone_alpes = RGeo::GeoJSON.decode(geojson).geometry if rhone_alpes.contains?(my_position) " puts "Let's ski end FOSS4G Asia 2023 Seoul 33

34.

intersects? require require require require "open-uri" "json" "rgeo" "rgeo-geojson" # FeatureCollection line1 = 'https://raw.githubusercontent.com/smellman/foss4g-asia-2023-rgeo/main/data/line1.geojson' line2 = 'https://raw.githubusercontent.com/smellman/foss4g-asia-2023-rgeo/main/data/line2.geojson' geojson1 = URI.open(line1).read geojson2 = URI.open(line2).read geometry1 = RGeo::GeoJSON.decode(geojson1)[0].geometry geometry2 = RGeo::GeoJSON.decode(geojson2)[0].geometry p geometry1.intersects?(geometry2) # => true FOSS4G Asia 2023 Seoul 34

35.

Analysis operations distance buffer envelope convex_hull intersection union unary_union difference sym_difference FOSS4G Asia 2023 Seoul 35

36.

distance require require require require "open-uri" "json" "rgeo" "rgeo-geojson" lng, lat = [139.764786, 35.677724] tokyo_station = RGeo::Cartesian. factory. point(lng, lat) url = "https://raw.githubusercontent.com/smellman/foss4g-asia-2023-rgeo/main/data/hotels.geojson" geojson = URI.open(url).read hotels = RGeo::GeoJSON.decode(geojson) nearest_hotel = hotels.min_by do |hotel| hotel.geometry.distance(tokyo_station) end puts nearest_hotel.properties["name:en"] # => "Tokyo Station Hotel" farthest_hotel = hotels.max_by do |hotel| hotel.geometry.distance(tokyo_station) end puts farthest_hotel.properties["name"] # => "Appt Ikebukuro" FOSS4G Asia 2023 Seoul 36

37.

Integration with Ruby on Rails and real-world application examples using RGeo FOSS4G Asia 2023 Seoul 37

38.

Ruby on Rails support RGeo provides ActiveRecord extensions for Ruby on Rails with PostGIS. activerecord-postgis-adapter ActiveRecord is powerful and simple to use. And RGeo is friendly with ActiveRecord. FOSS4G Asia 2023 Seoul 38

39.

Overview FOSS4G Asia 2023 Seoul 39

40.

1. Create a new Rails application rails new myapp --api -d postgresql FOSS4G Asia 2023 Seoul 40

41.

2. Add activerecord-postgis-adapter to Gemfile gem 'rgeo' gem 'rgeo-geojson' gem 'activerecord-postgis-adapter' FOSS4G Asia 2023 Seoul 41

42.

3. Setup database --- a/myapp/config/database.yml +++ b/myapp/config/database.yml @@ -15,7 +15,7 @@ # gem "pg" # default: &default - adapter: postgresql + adapter: postgis encoding: unicode # For details on connection pooling, see Rails configuration guide # https://guides.rubyonrails.org/configuring.html#database-pooling FOSS4G Asia 2023 Seoul 42

43.

4. Create database rails db:create FOSS4G Asia 2023 Seoul 43

44.

5. Create a migration file to enable PostGIS extension rails g migration AddPostgisExtensionToDatabase class AddPostgisExtensionToDatabase < ActiveRecord::Migration[7.0] def change enable_extension 'postgis' end end FOSS4G Asia 2023 Seoul 44

45.

6. Create a model/migration/controller via scaffold rails g scaffold toilet FOSS4G Asia 2023 Seoul 45

46.

7. Edit migration file --- a/myapp/db/migrate/20231120235529_create_toilets.rb +++ b/myapp/db/migrate/20231120235529_create_toilets.rb @@ -1,6 +1,8 @@ class CreateToilets < ActiveRecord::Migration[7.0] def change create_table :toilets do |t| + t.string :name + t.st_point :location, geographic: true t.timestamps end FOSS4G Asia 2023 Seoul 46

47.

8. Run migration rails db:migrate FOSS4G Asia 2023 Seoul 47

48.

9. Prepare seeds Download data from Overpass Turbo. node [amenity=toilets] ({{bbox}}); out; Export as GeoJSON. FOSS4G Asia 2023 Seoul 48

49.

10. Put the GeoJSON file into db/seed_data directory. - Put the GeoJSON file into `db/seed_data` directory. ```sh mkdir db/seed_data mv ~/Downloads/export.geojson db/seed_data/toilets.geojson FOSS4G Asia 2023 Seoul 49

50.

11. Edit db/seeds.rb def seed_toilets Rails.logger.info 'Seed toilets' toilets_geojson = File.read('db/seed_data/toilets.geojson') toilets = RGeo::GeoJSON.decode(toilets_geojson) toilets.each do |toilet| name = toilet.properties['name'] ? toilet.properties['name'] : 'no name' Toilet.create( name: name, location: toilet.geometry ) end end seed_toilets FOSS4G Asia 2023 Seoul 50

51.

12. Run db:seed rails db:seed Check the database. ❯ rails r "p Toilet.count" 662 FOSS4G Asia 2023 Seoul 51

52.
[beta]
13. Edit app/models/toilet.rb
class Toilet < ApplicationRecord
def as_geojson
{
type: "Feature",
geometry: RGeo::GeoJSON.encode(self.location),
properties: self.attributes.except("location")
}
end
def as_json(options = {})
as_geojson
end
end

FOSS4G Asia 2023 Seoul

52

53.

14. Edit app/controllers/toilets_controller.rb def index @toilets = Toilet.all geojson = { type: "FeatureCollection", features: @toilets.map(&:as_json) } render json: geojson end FOSS4G Asia 2023 Seoul 53

54.
[beta]
Check output
curl "http://127.0.0.1:3000/toilets.json" | jq .|head -n 20
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
139.7978412,
35.6785662
]
},
"properties": {
"id": 1,
"name": "no name",
"created_at": "2023-11-21T00:18:37.776Z",
"updated_at": "2023-11-21T00:18:37.776Z"
}
},
{

FOSS4G Asia 2023 Seoul

54

55.
[beta]
as_json

called in json: render by default.

❯ curl "http://127.0.0.1:3000/toilets/1.json" | jq .
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
139.7978412,
35.6785662
]
},
"properties": {
"id": 1,
"name": "no name",
"created_at": "2023-11-21T00:18:37.776Z",
"updated_at": "2023-11-21T00:18:37.776Z"
}
}

FOSS4G Asia 2023 Seoul

55

56.

15. Supports GeoJSON output Create config/initalizers/mime_types.rb Mime::Type.register 'application/vnd.geo+json', :geojson FOSS4G Asia 2023 Seoul 56

57.

16. Fix config/routes.rb Default format to geojson . Rails.application.routes.draw do resources :toilets, defaults: { format: 'geojson' } end FOSS4G Asia 2023 Seoul 57

58.
[beta]
Check output
curl "http://127.0.0.1:3000/toilets/1.geojson" | jq .
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
139.7978412,
35.6785662
]
},
"properties": {
"id": 1,
"name": "no name",
"created_at": "2023-11-21T00:18:37.776Z",
"updated_at": "2023-11-21T00:18:37.776Z"
}
}

FOSS4G Asia 2023 Seoul

58

59.

FOSS4G Asia 2023 Seoul 59

60.
[beta]
17. Define scope for spatial query in
app/models/toilet.rb

scope :distance_sphere, lambda { |longitude, latitude, meter|
where("ST_DWithin(toilets.location, ST_GeomFromText('POINT(:longitude :latitude)', 4326), :meter)",
{ longitude: longitude, latitude: latitude, meter: meter })
}

FOSS4G Asia 2023 Seoul

60

61.

18. Use scope in app/controllers/toilets_controller.rb def index if params[:longitude] && params[:latitude] && params[:radius] @toilets = Toilet.distance_sphere( params[:longitude].to_f, params[:latitude].to_f, params[:radius].to_i ) else @toilets = Toilet.all end geojson = { type: "FeatureCollection", features: @toilets.map(&:as_json) } render json: geojson end FOSS4G Asia 2023 Seoul 61

62.

Check output # without params ❯ curl "http://localhost:3000/toilets.json" | jq '.features | length' 662 # with params ❯ curl "http://localhost:3000/toilets.json?latitude=35.677724&longitude=139.76478f6&radius=1000" | jq '.features | length' 31 FOSS4G Asia 2023 Seoul 62

63.

TODO for this application: Add cors support. Add a map to the frontend. Add routing function using pgRouting. FOSS4G Asia 2023 Seoul 63

64.

Thank you! FOSS4G Asia 2023 Seoul 64