Merge pull request #8631 from tchak/fix-rgeo-super-slow
fix(geometry): implement our own bbox to replace rgeo
This commit is contained in:
commit
f1ecee4240
12 changed files with 123 additions and 39 deletions
|
@ -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
|
||||
|
|
11
app/jobs/migrations/normalize_geo_area_job.rb
Normal file
11
app/jobs/migrations/normalize_geo_area_job.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -13,7 +13,7 @@ class ChampSerializer < ActiveModel::Serializer
|
|||
def value
|
||||
case object
|
||||
when GeoArea
|
||||
object.safe_geometry
|
||||
object.geometry
|
||||
else
|
||||
object.for_api
|
||||
end
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in a new issue