Adds spatial methods to a model.
- Install libraries
-
PostGIS
-
libgeos and gdal (optional libraries required for Shapefile import)
# Ubuntu Installation instructions. Source: https://github.com/rgeo/rgeo/issues/26#issuecomment-106059741 sudo apt-get -y install libgeos-3.4.2 libgeos-dev libproj0 libproj-dev gdal-bin sudo ln -s /usr/lib/libgeos-3.4.2.so /usr/lib/libgeos.so sudo ln -s /usr/lib/libgeos-3.4.2.so /usr/lib/libgeos.so.1
- Create spatial tables
execute("
CREATE EXTENSION hstore;
CREATE EXTENSION postgis;
CREATE TABLE features (
id integer NOT NULL,
type character varying(255),
spatial_model_type character varying(255),
spatial_model_id integer,
name character varying(255),
feature_type character varying(255),
geog geography,
geom geometry(Geometry,4326),
geom_lowres geometry(Geometry,4326),
tilegeom geometry(Geometry,3857),
metadata hstore,
area double precision,
north numeric(9,6),
east numeric(9,6),
south numeric(9,6),
west numeric(9,6),
centroid geography,
);
CREATE SEQUENCE features_id_seq START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1;
ALTER SEQUENCE features_id_seq OWNED BY features.id;
ALTER TABLE ONLY features ALTER COLUMN id SET DEFAULT nextval('features_id_seq'::regclass);
ALTER TABLE ONLY features ADD CONSTRAINT features_pkey PRIMARY KEY (id);
CREATE INDEX index_features_on_feature_type ON features USING btree (feature_type);
CREATE INDEX index_features_on_spatial_model_id_and_spatial_model_type ON features USING btree (spatial_model_id, spatial_model_type);
CREATE INDEX index_features_on_geom ON features USING gist (geom);
CREATE INDEX index_features_on_geom_lowres ON features USING gist (geom_lowres);
CREATE INDEX index_features_on_tilegeom ON features USING gist (tilegeom);
CREATE TABLE spatial_caches (
id integer NOT NULL,
intersection_model_type character varying(255),
spatial_model_type character varying(255),
spatial_model_id integer,
created_at timestamp without time zone,
updated_at timestamp without time zone,
intersection_cache_distance double precision,
features_hash character varying(255)
);
CREATE SEQUENCE spatial_caches_id_seq START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1;
ALTER SEQUENCE spatial_caches_id_seq OWNED BY spatial_caches.id;
ALTER TABLE ONLY spatial_caches ALTER COLUMN id SET DEFAULT nextval('spatial_caches_id_seq'::regclass);
ALTER TABLE ONLY spatial_caches ADD CONSTRAINT spatial_caches_pkey PRIMARY KEY (id);
CREATE INDEX index_spatial_caches_on_spatial_model ON spatial_caches USING btree (spatial_model_id, spatial_model_type);
CREATE TABLE spatial_proximities (
id integer NOT NULL,
model_a_type character varying(255),
model_a_id integer,
model_b_type character varying(255),
model_b_id integer,
distance_in_meters double precision,
intersection_area_in_square_meters double precision
);
CREATE SEQUENCE spatial_proximities_id_seq START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1;
ALTER SEQUENCE spatial_proximities_id_seq OWNED BY spatial_proximities.id;
ALTER TABLE ONLY spatial_proximities ALTER COLUMN id SET DEFAULT nextval('spatial_proximities_id_seq'::regclass);
ALTER TABLE ONLY spatial_proximities ADD CONSTRAINT spatial_proximities_pkey PRIMARY KEY (id);
CREATE INDEX index_spatial_proximities_on_model_a ON spatial_proximities USING btree (model_a_id, model_a_type);
CREATE INDEX index_spatial_proximities_on_model_b ON spatial_proximities USING btree (model_b_id, model_b_type);
")
In your model
class Location < ActiveRecord::Base
has_spatial_features
end
Person.new(:features => [Feature.new(:geog => 'some binary PostGIS Geography string')])
You can specify multiple import sources for geometry. Each key is a method that returns the data for the Importer, and
each value is the Importer to use to parse the data. Options can be passed in place of the Importer name, but must still
include a name
key with the name of the importer. See each Importer for more details.
def ImageImporter
def self.call(feature, image_paths)
image_paths.each do |pathname|
# ...
end
end
end
class Location < ActiveRecord::Base
has_spatial_features :image_handlers => ['ImageImporter'],
:import => {
:remote_kml_url => { :name => 'KMLFile', :feature_name => lambda {|source_feature| source_feature.metadata['shapeID'] },
:file => 'File', :geojson => 'ESRIGeoJSON' }
}
def remote_kml_url
"www.test.com/kml/#{id}.kml"
end
def file
File.open('local/files/my_kml')
end
def geojson
{ "type" => "FeatureCollection", "features" => [] }
end
end
Default options can be specified on the Shapefile importer class itself.
-
A proj4 formatted projection string to use when no other projection has been specified either in the shapefile or the importer instance.
Example:
SpatialFeatures::Importers::Shapefile.default_proj4_projection = "+proj=aea +lat_1=50 +lat_2=58.5 +lat_0=45 +lon_0=-126 +x_0=1000000 +y_0=0 +ellps=GRS80 +datum=NAD83 +units=m +no_defs"
Default:
nil
Features now generate an AggregateFeature
comprised of a union of all their spatial model's features. This improves query performance because
unioning is precalculated in these shapes instead of at query time.
# In your migration
add_column :features, :type, :string
Feature.reset_column_information
AbstractFeature.update_all(:type => 'Feature')
Feature.refresh_aggregates
Cached KML layers are no longer generated as Mapbox Vector Tile is the primary expected output. The columns can be left in place or you can remove the KML cache columns.
remove_column :features, :kml
remove_column :features, :kml_lowres
remove_column :features, :kml_centroid
A new tilegeom
column has been added to support MVT tile output, which is now the preferred map output format instead
of GeoJSON or KML. It keeps memory usage low and is fast to generate.
add_column :features, :tilegeom, :geometry
add_index :features, :tilegeom, :using => :gist
Feature.update_all('tilegeom = ST_Transform(geom, 3857)')
SpatialProximity now expects the model_a
and model_b
records to be decided based on the name of the record type so
queries can be optimized. Migrate existing SpatialProximity rows to this new scheme by running the code below.
SpatialProximity.normalize
Features now record the source they were imported from in the new source_identifier
column. This column is indexed and
can be used filter features by source.
add_column :features, :source_identifier, :string
add_index :features, :source_identifier
MyModel.update_features!(:force => true) # Force an `update_features!` will populate the source_identifier column.
The gem now relies on virtual columns to set a number of derived column values.
change_table :features do |t|
t.remove :tilegeom, :feature_type, :centroid, :area, :north, :east, :south, :west
t.virtual :tilegeom, :type => 'geometry(Geometry,3857)', as: "ST_Transform(geom, 3857)", stored: true, :index => { :using => :gist }
t.virtual :feature_type, :type => :string, as: "CASE GeometryType(geog) WHEN 'POLYGON' THEN 'polygon' WHEN 'MULTIPOLYGON' THEN 'polygon' WHEN 'GEOMETRYCOLLECTION' THEN 'polygon' WHEN 'LINESTRING' THEN 'line' WHEN 'MULTILINESTRING' THEN 'line' WHEN 'POINT' THEN 'point' WHEN 'MULTIPOINT' THEN 'point' END", stored: true, :index => true
t.virtual :centroid, :type => :geography, as: "ST_PointOnSurface(geog::geometry)", stored: true
t.virtual :area, :type => :decimal, as: "ST_Area(geog)", stored: true
t.virtual :north, :type => :decimal, as: "ST_YMax(geog::geometry)", stored: true
t.virtual :east, :type => :decimal, as: "ST_XMax(geog::geometry)", stored: true
t.virtual :south, :type => :decimal, as: "ST_YMin(geog::geometry)", stored: true
t.virtual :west, :type => :decimal, as: "ST_XMin(geog::geometry)", stored: true
end
Create a postgres database:
createdb spatial_features_test
There are multiple gemfiles available for testing against different Rails versions. Set BUNDLE_GEMFILE
to target them, e.g.
bundle install
BUNDLE_GEMFILE=gemfiles/rails_7_0.gemfile bundle install
BUNDLE_GEMFILE=gemfiles/rails_7_0.gemfile bundle exec rspec