Skip to content

Instantly share code, notes, and snippets.

@moradology
Last active June 20, 2019 15:38
Show Gist options
  • Save moradology/7f889eb657a3d6b977d57f7431acd7ff to your computer and use it in GitHub Desktop.
Save moradology/7f889eb657a3d6b977d57f7431acd7ff to your computer and use it in GitHub Desktop.
Thoughts about `Feature`

feature design

Before deciding what our support for GIS features should look like in GT, we should get a sense of how the term feature is already used to avoid confusion. Unfortunately, there's no single source of truth for a question like this; we'll have to settle for sampling from the important pieces of discourse and evaluate in terms of them.

geojson spec

from https://tools.ietf.org/html/rfc7946#section-3.2

A Feature object represents a spatially bounded thing. Every Feature object is a GeoJSON object no matter where it occurs in a GeoJSON text.

o A Feature object has a "type" member with the value "Feature".

o A Feature object has a member with the name "geometry". The value of the geometry member SHALL be either a Geometry object as defined above or, in the case that the Feature is unlocated, a JSON null value.

o A Feature object has a member with the name "properties". The value of the properties member is an object (any JSON object or a JSON null value).

o If a Feature has a commonly used identifier, that identifier SHOULD be included as a member of the Feature object with the name "id", and the value of this member is either a JSON string or number.

a relevant SO post

from https://gis.stackexchange.com/questions/137946/difference-between-feature-and-geometry

A "Feature" is a single entity in GIS that has both geometry and attribute data. The attribute data can be just a single ID number or it can encompass all kinds of other data about the feature. For example, a polygonal feature representing a parcel of land could have attribute data naming the property owner, the parcel's mailing address, and so on. A feature can also consist of more than one geometry (these are called multi-part features): in the land parcel example, a parcel of land that is bisected by a railroad track right-of-way would be represented on-screen by two separate geometries (one on either side of the RR track), but would be one feature in GIS. You could also split that one feature into two features, both with identical attribute data, but different geometries. You can also have features with no geometry, which wouldn't show up on a map but would appear in the layer's attribute table.

ESRI's description

http://desktop.arcgis.com/en/arcmap/10.3/manage-data/geodatabases/feature-class-basics.htm Mostly comports with what geojson has to say but also includes an annotation type (which geojson can't really handle without being subsetted)

Notes on serialization (may be relevant/may not)

  1. We ought to cut at the meridian during serialization https://tools.ietf.org/html/rfc7946#section-3.1.9 Apparently, cutting geometries at the meridian is suggested by the geojson spec.

  2. Deserialization can make no assumptions about precision on the basis of number of decimal places https://tools.ietf.org/html/rfc7946#section-3.1.10

  3. Our bbox serialization is all wrong; invites ambiguity https://tools.ietf.org/html/rfc7946#section-5.2

The latitude of the northeast corner is always greater than the latitude of the southwest corner, but bounding boxes that cross the antimeridian have a northeast corner longitude that is less than the longitude of the southwest corner.

#!/usr/bin/env amm
// build configuration
import coursier.MavenRepository
import $plugin.$ivy.`org.spire-math::kind-projector:0.9.3`
interp.configureCompiler(_.settings.YpartialUnification.value = true)
interp.repositories() ++= Seq(coursier.maven.MavenRepository("http://download.osgeo.org/webdav/geotools/"))
@
// imports
import $ivy.`org.geotools:gt-main:21.1`
import $ivy.`org.locationtech.geotrellis::geotrellis-vector:2.3.1`
import $ivy.`org.typelevel::cats-core:1.6.0`
import geotrellis.vector._
import org.opengis.feature.{Feature => OpenGisFeature}
import org.geotools.feature.{FeatureCollection => OpenGisFeatureCollection}
import cats._
import cats.implicits._
// What if `feature` isn't a thing but rather a set of
// capacities which many different types can participate in?
trait Feature[F] {
def map[B: Feature](feature: F)(f: F => B): B
def geom(feature: F): Geometry
def id(feature: F): Option[Int]
}
object Feature {
def apply[F: Feature] = implicitly[Feature[F]]
}
// Carrying the idea forward, a feature collection is just
// some `C[F]` where `F` is understood to be feauture-y
abstract class FeatureCollection[F: Feature, C[_]] {
val collection: C[F]
def intersecting(bbox: Extent): C[F]
def within(bbox: Extent): C[F]
def outside(bbox: Extent): C[F]
def extent: Extent
}
// This is our purported feature's type. Just pretend I
// pulled the quadruple from a CSV or something.
type Quad = (Point, String, String, List[String])
// Quad instances *are* features. Proof:
implicit val quadIsFeature = new Feature[Quad] {
def map[B: Feature](feature: Quad)(f: Quad => B): B = f(feature)
def geom(feature: Quad): Geometry = feature._1
def id(feature: Quad): Option[Int] = None
}
// How do we leverage the feauture? (this could be abstracted a bit if
// we put a boundary on C such that `filter` is 'proven' via typeclass)
implicit class QuadFeatureCollection(val collection: List[Quad]) extends FeatureCollection[Quad, List] {
def intersecting(bbox: Extent): List[Quad] = {
collection.filter(Feature[Quad].geom(_).intersects(bbox))
}
def within(bbox: Extent): List[Quad] = {
collection.filter { f =>
val geom = Feature[Quad].geom(f)
bbox.contains(geom)
}
}
def outside(bbox: Extent): List[Quad] = {
collection.filter { f =>
val geom = Feature[Quad].geom(f)
!(geom.intersects(bbox))
}
}
def extent: Extent =
collection.tail
.foldLeft(Feature[Quad].geom(collection.head).envelope) {
_ combine Feature[Quad].geom(_).envelope
}
}
val battles = List(
(Point(37.276611328125, 35.36217605914681), "Qadesh", "1247BC", List("Egypt", "Hittites")),
(Point(1.3623046875, 54.29088164657006), "Hastings", "1066AD", List("Saxons", "Normans", "Norwegians")),
(Point(37.276611328125, 35.36217605914681), "Stalingrad", "1942AD", List("USSR", "Germany", "Italy", "fascist volunteers"))
) // etc etc
val oldWorldExtent = Extent(-31.99, -60.41, 191.95, 78.06)
val oldWorldBattles = battles.within(oldWorldExtent)
val newWorldBattles = battles.outside(oldWorldExtent)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment