Merge pull request #8631 from tchak/fix-rgeo-super-slow

fix(geometry): implement our own bbox to replace rgeo
This commit is contained in:
Paul Chavard 2023-02-15 12:09:39 +01:00 committed by GitHub
commit f1ecee4240
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 123 additions and 39 deletions

View file

@ -12,7 +12,7 @@ module Types
global_id_field :id
field :source, GeoAreaSource, null: false
field :geometry, Types::GeoJSON, null: false, method: :safe_geometry
field :geometry, Types::GeoJSON, null: false
field :description, String, null: true
definition_methods do

View file

@ -0,0 +1,11 @@
class Migrations::NormalizeGeoAreaJob < ApplicationJob
def perform(ids)
GeoArea.where(id: ids).find_each do |geo_area|
geojson = RGeo::GeoJSON.decode(geo_area.geometry.to_json, geo_factory: RGeo::Geographic.simple_mercator_factory)
geometry = RGeo::GeoJSON.encode(geojson)
geo_area.update_column(:geometry, geometry)
rescue RGeo::Error::InvalidGeometry
geo_area.destroy
end
end
end

View file

@ -64,21 +64,14 @@ class Champs::CarteChamp < Champ
end
def bounding_box
factory = RGeo::Geographic.simple_mercator_factory
bounding_box = RGeo::Cartesian::BoundingBox.new(factory)
if geo_areas.present?
geo_areas.filter_map(&:rgeo_geometry).each do |geometry|
bounding_box.add(geometry)
end
GeojsonService.bbox(type: 'FeatureCollection', features: geo_areas.map(&:to_feature))
elsif dossier.present?
point = dossier.geo_position
bounding_box.add(factory.point(point[:lon], point[:lat]))
GeojsonService.bbox(type: 'Feature', geometry: { type: 'Point', coordinates: [point[:lon], point[:lat]] })
else
bounding_box.add(factory.point(DEFAULT_LON, DEFAULT_LAT))
GeojsonService.bbox(type: 'Feature', geometry: { type: 'Point', coordinates: [DEFAULT_LON, DEFAULT_LAT] })
end
[bounding_box.max_point, bounding_box.min_point].compact.flat_map(&:coordinates)
end
def to_feature_collection

View file

@ -1312,14 +1312,7 @@ class Dossier < ApplicationRecord
end
def bounding_box
factory = RGeo::Geographic.simple_mercator_factory
bounding_box = RGeo::Cartesian::BoundingBox.new(factory)
geo_areas.filter_map(&:rgeo_geometry).each do |geometry|
bounding_box.add(geometry)
end
[bounding_box.max_point, bounding_box.min_point].compact.flat_map(&:coordinates)
GeojsonService.bbox(type: 'FeatureCollection', features: geo_areas.map(&:to_feature))
end
def log_dossier_operation(author, operation, subject = nil)

View file

@ -52,11 +52,12 @@ class GeoArea < ApplicationRecord
scope :cadastres, -> { where(source: sources.fetch(:cadastre)) }
validates :geometry, geo_json: true, allow_blank: false
before_validation :normalize_geometry
def to_feature
{
type: 'Feature',
geometry: safe_geometry,
geometry: geometry.deep_symbolize_keys,
properties: cadastre_properties.merge(
source: source,
area: area,
@ -96,16 +97,6 @@ class GeoArea < ApplicationRecord
end
end
def safe_geometry
RGeo::GeoJSON.encode(rgeo_geometry)
end
def rgeo_geometry
RGeo::GeoJSON.decode(geometry.to_json, geo_factory: RGeo::Geographic.simple_mercator_factory)
rescue RGeo::Error::InvalidGeometry
nil
end
def area
if polygon?
GeojsonService.area(geometry.deep_symbolize_keys).round(1)
@ -120,7 +111,7 @@ class GeoArea < ApplicationRecord
def location
if point?
Geo::Coord.new(*rgeo_geometry.coordinates.reverse).to_s
Geo::Coord.new(*geometry['coordinates'].reverse).to_s
end
end
@ -238,4 +229,21 @@ class GeoArea < ApplicationRecord
properties['id']
end
end
private
def normalize_geometry
if geometry.present?
normalized_geometry = rgeo_geometry
if normalized_geometry.present?
self.geometry = RGeo::GeoJSON.encode(normalized_geometry)
end
end
end
def rgeo_geometry
RGeo::GeoJSON.decode(geometry.to_json, geo_factory: RGeo::Geographic.simple_mercator_factory)
rescue RGeo::Error::InvalidGeometry
nil
end
end

View file

@ -13,7 +13,7 @@ class ChampSerializer < ActiveModel::Serializer
def value
case object
when GeoArea
object.safe_geometry
object.geometry
else
object.for_api
end

View file

@ -12,7 +12,7 @@ class GeoAreaSerializer < ActiveModel::Serializer
attribute :code_arr, if: :include_cadastre?
def geometry
object.safe_geometry
object.geometry
end
def include_cadastre?

View file

@ -45,6 +45,66 @@ class GeojsonService
radians * EQUATORIAL_RADIUS
end
def self.bbox(geojson)
result = [-Float::INFINITY, -Float::INFINITY, Float::INFINITY, Float::INFINITY]
self.coord_each(geojson) do |coord|
if result[3] > coord[1]
result[3] = coord[1]
end
if result[2] > coord[0]
result[2] = coord[0]
end
if result[1] < coord[1]
result[1] = coord[1]
end
if result[0] < coord[0]
result[0] = coord[0]
end
end
result
end
def self.coord_each(geojson)
geometries = if geojson.fetch(:type) == "FeatureCollection"
geojson.fetch(:features).map { _1.fetch(:geometry) }
else
[geojson.fetch(:geometry)]
end.compact
geometries.each do |geometry|
geometries = if geometry.fetch(:type) == "GeometryCollection"
geometry.fetch(:geometries)
else
[geometry]
end.compact
geometries.each do |geometry|
case geometry.fetch(:type)
when "Point"
yield geometry.fetch(:coordinates).map(&:to_f)
when "LineString", "MultiPoint"
geometry.fetch(:coordinates).each { yield _1.map(&:to_f) }
when "Polygon", "MultiLineString"
geometry.fetch(:coordinates).each do |shapes|
shapes.each { yield _1.map(&:to_f) }
end
when "MultiPolygon"
geometry.fetch(:coordinates).each do |polygons|
polygons.each do |shapes|
shapes.each { yield _1.map(&:to_f) }
end
end
when "GeometryCollection"
geometry.fetch(:geometries).each do |geometry|
coord_each(geometry) { yield _1 }
end
end
end
end
end
def self.calculate_area(geom)
total = 0
case geom[:type]

View file

@ -0,0 +1,19 @@
namespace :after_party do
desc 'Deployment task: normalize_geometries'
task normalize_geometries: :environment do
puts "Running deploy task 'normalize_geometries'"
progress = ProgressReport.new(GeoArea.count)
GeoArea.in_batches(of: 100) do |geo_areas|
ids = geo_areas.ids
Migrations::NormalizeGeoAreaJob.perform_later(ids)
progress.inc(ids.size)
end
progress.finish
# Update task as completed. If you remove the line below, the task will
# run with every deploy (or every time you call after_party:run).
AfterParty::TaskRecord
.create version: AfterParty::TaskRecorder.new(__FILE__).timestamp
end
end

View file

@ -1,7 +1,7 @@
describe Champs::CarteChamp do
let(:champ) { Champs::CarteChamp.new(geo_areas: geo_areas, type_de_champ: create(:type_de_champ_carte)) }
let(:value) { '' }
let(:coordinates) { [[2.3859214782714844, 48.87442541960633], [2.3850631713867183, 48.87273183590832], [2.3809432983398438, 48.87081237174292], [2.3859214782714844, 48.87442541960633]] }
let(:coordinates) { [[[2.3859214782714844, 48.87442541960633], [2.3850631713867183, 48.87273183590832], [2.3809432983398438, 48.87081237174292], [2.3859214782714844, 48.87442541960633]]] }
let(:geo_json) do
{
"type" => 'Polygon',

View file

@ -1516,8 +1516,8 @@ describe Dossier do
{
type: 'Feature',
geometry: {
'coordinates' => [[[2.428439855575562, 46.538476837725796], [2.4284291267395024, 46.53842148758162], [2.4282521009445195, 46.53841410755813], [2.42824137210846, 46.53847314771794], [2.428284287452698, 46.53847314771794], [2.428364753723145, 46.538487907747864], [2.4284291267395024, 46.538491597754714], [2.428439855575562, 46.538476837725796]]],
'type' => 'Polygon'
coordinates: [[[2.428439855575562, 46.538476837725796], [2.4284291267395024, 46.53842148758162], [2.4282521009445195, 46.53841410755813], [2.42824137210846, 46.53847314771794], [2.428284287452698, 46.53847314771794], [2.428364753723145, 46.538487907747864], [2.4284291267395024, 46.538491597754714], [2.428439855575562, 46.538476837725796]]],
type: 'Polygon'
},
properties: {
area: 103.6,

View file

@ -23,7 +23,7 @@ RSpec.describe GeoArea, type: :model do
it { expect(geo_area.location).to eq("46°32'19\"N 2°25'42\"E") }
end
describe '#rgeo_geometry' do
describe '#geometry' do
let(:geo_area) { build(:geo_area, :polygon, champ: nil) }
let(:polygon) do
{
@ -47,9 +47,9 @@ RSpec.describe GeoArea, type: :model do
context 'polygon_with_extra_coordinate' do
let(:geo_area) { build(:geo_area, :polygon_with_extra_coordinate, champ: nil) }
before { geo_area.valid? }
it { expect(geo_area.geometry).not_to eq(polygon) }
it { expect(geo_area.safe_geometry).to eq(polygon) }
it { expect(geo_area.geometry).to eq(polygon) }
end
end