hierarchy

package
v0.19.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 7, 2025 License: BSD-3-Clause Imports: 19 Imported by: 5

README

Point-in-polygon "hierarchy resolvers"

At a high-level a point-in-polygon "hierarchy resolver" consists of (4) parts:

  • Given a GeoJSON Feature use its geometry to derive the most appropriate centroid for performing a point-in-polygon query
  • Perform a point-in-polygon query for a centroid, excluding results using criteria defined by zero or more filters.
  • Convert the list of candidate results (derived from the point-in-polygon query) in to a single result using a callback function.
  • Apply updates derived from the final result to the original GeoJSON Feature using a callback function.

These functionalities are implemented by the hierarchy.PointInPolygonHierarchyResolver package. In addition to wrapping all those moving pieces the hierachy package also exports a handful of predefined callback functions to use for filtering results and applying updates.

Importantly, hierarchy resolvers are not responsible for reading Who's On First documents, writing updates to those documents or populating the spatial databases used to perform point-in-polygon operations. These tasks are left to other bits of code. The principal goal of a hierarchy resolver is to perform a point-in-polygon operation, resolve multiple overlapping candidates down to a single result and then generate/apply updates (to a source document) derive from that result.

Example

The following examples describe how to use the hierarchy.PointInPolygonHierarchyResolver package in abbreviated (incomplete) and annotated code. These example do not reflect all the functionality of the hierarchy.PointInPolygonHierarchyResolver package. For details consult the Go reference documentation.

Note: For the sake of brevity all error-handling has been removed from these examples.

Basic

This example demonstrates how to use the hierarchy.PointInPolygonHierarchyResolver package with a set of "core" Who's On First documents using a SQLite-backed spatial database.

import (
       "context"

       _ "github.com/mattn/go-sqlite3"
       _ "github.com/whosonfirst/go-whosonfirst-spatial-sqlite"
       
       "github.com/sfomuseum/go-sfomuseum-mapshaper"
       "github.com/whosonfirst/go-whosonfirst-spatial/database"
       "github.com/whosonfirst/go-whosonfirst-spatial/filter"
       "github.com/whosonfirst/go-whosonfirst-spatial/hierarchy"
       hierarchy_filter "github.com/whosonfirst/go-whosonfirst-spatial/hierarchy/filter"       
)

func main() {

	ctx := context.Background()
	
	// The Mapshaper "client" (and its associated "server") is not required by a point-in-polygon
	// hierarchy resolver but is included in this example for the sake of thoroughness. If present
	// it will be used to derive the centroid for a GeoJSON Feature using the Mapshape "inner point"
	// command. Both the "client" and "server" components are part of the [sfomuseum/go-sfomuseum-mapshaper](#)
	// package but setting up and running the "server" component is out of scope for this document.
	// Basically Mapshaper's "inner point" functonality can't be ported to Go fast enough.
	//
	// If the mapshaper client is `nil` then there are a variety of other heuristics that will be
	// used, based on the content of the input GeoJSON Feature, to derive a candidate centroid to
	// be used for point-in-polygon operations.
        mapshaper_cl, _ := mapshaper.NewClient(ctx, "http://localhost:8080")

	// Create a new spatial database instance. For the sake of this example it
	// is assumed that the database has already been populated with records.
        spatial_db, _ := database.NewSpatialDatabase(ctx, "sql://sqlite3?dsn=example.db")

	// Create configuration options for hierarchy resolver
	resolver_opts := &hierarchy.PointInPolygonHierarchyResolverOptions{
		Database:             spatial_db,
	        Mapshaper:            mapshaper_cl,
        }

	// Create the hierarchy resolver itself
        resolver, _ := hierarchy.NewPointInPolygonHierarchyResolver(ctx, resolver_opts)

	// Create zero or more filters to prune point-in-polygon results with, in this case
	// only return records whose `mz:is_current` property is "1".
        pip_inputs := &filter.SPRInputs{
                IsCurrent: []int64{1},
        }

	// Instantiate a predefined results callback function that returns the first result in a list
	// of candidates but does not trigger an error if that list is empty.
        results_cb := hierarchy_filter.FirstButForgivingSPRResultsFunc

	// Instantiate a predefined update callback that will return a dictionary populated with the 
	// following properties from the final point-in-polygon result (derived from `results_cb`):
	// wof:parent_id, wof:hierarchy, wof:country
	update_cb := hierarchy.DefaultPointInPolygonHierarchyResolverUpdateCallback()

	// Where body is assumed to be a valid Who's On First style GeoJSON Feature
	var body []byte

	// Invoke the hierarchy resolver's `PointInPolygonAndUpdate` method using `body` as the input
	// parameter.
	updates, _ := resolver.PointInPolygonAndUpdate(ctx, pip_inputs, results_cb, update_cb, body)

	// Apply updates to body here
}	
RTree-backed spatial database

This example demonstrates how to use the hierarchy.PointInPolygonHierarchyResolver package with a set of "core" Who's On First documents using an in-memory RTree-backed spatial database.

This is basically the same code as the "basic" example. The important difference is that we create and assign a new reader.Reader instance to the hierarchy resolver which is used to read properties, and specifically hierarchies, for features that have been returned by a point-in-polygon query.

This is necessary because the RTree-backed spatial database only caches (in memory) the data necessary to implement the StandardPlacesResult interface (which does not expose hierarchies). This means it is necessary to define an external reader to derive properties. This can be any valid reader.Reader implementation. For the purposes of this example a SQLite-backed implementation is assumed (using a database created by the whosonfirst/go-whosonfirst-database-sqlite package).

import (
       "context"

       _ "github.com/mattn/go-sqlite3"
       
       "github.com/sfomuseum/go-sfomuseum-mapshaper"
       "github.com/whosonfirst/go-whosonfirst-spatial/database"
       "github.com/whosonfirst/go-whosonfirst-spatial/filter"
       "github.com/whosonfirst/go-whosonfirst-spatial/hierarchy"
       "github.com/whosonfirst/go-reader/v2"
       hierarchy_filter "github.com/whosonfirst/go-whosonfirst-spatial/hierarchy/filter"       
)

func main() {

	ctx := context.Background()
	
        mapshaper_cl, _ := mapshaper.NewClient(ctx, "http://localhost:8080")
        spatial_db, _ := database.NewSpatialDatabase(ctx, "rtree://")

	resolver_opts := &hierarchy.PointInPolygonHierarchyResolverOptions{
		Database:             spatial_db,
	        Mapshaper:            mapshaper_cl,
        }

        resolver, _ := hierarchy.NewPointInPolygonHierarchyResolver(ctx, resolver_opts)

	// Create a new SQLite-backed reader.Reader instance to use to read feature properties (hierarchies).
	// Note the silent import of the github.com/whosonfirst/go-reader-database-sql package above.
	
	sql_r, _ := reader.NewReader(ctx, "sql://sqlite3/geojson/id/body?dsn=example.db")
	resolver.SetReader(sql_r)

	// Carry on as usual
	
        pip_inputs := &filter.SPRInputs{
                IsCurrent: []int64{1},
        }

        results_cb := hierarchy_filter.FirstButForgivingSPRResultsFunc

	update_cb := hierarchy.DefaultPointInPolygonHierarchyResolverUpdateCallback()

	var body []byte

	updates, _ := resolver.PointInPolygonAndUpdate(ctx, pip_inputs, results_cb, update_cb, body)

	// Apply updates to body here
}	
Custom placetypes

This example demonstrates how to the hierarchy.PointInPolygonHierarchyResolver package with a set of Who's On First style documents that contain custom placetypes (defined in a separate property from the default wof:placetype property).

import (
       "context"

       _ "github.com/mattn/go-sqlite3"
       _ "github.com/sfomuseum/go-sfomuseum-placetypes"
       _ "github.com/whosonfirst/go-whosonfirst-spatial-sqlite"
       
       "github.com/sfomuseum/go-sfomuseum-mapshaper"
       "github.com/whosonfirst/go-whosonfirst-placetypes"
       "github.com/whosonfirst/go-whosonfirst-spatial/database"
       "github.com/whosonfirst/go-whosonfirst-spatial/filter"
       "github.com/whosonfirst/go-whosonfirst-spatial/hierarchy"
       hierarchy_filter "github.com/whosonfirst/go-whosonfirst-spatial/hierarchy/filter"       
)

func main() {

        mapshaper_cl, _ := mapshaper.NewClient(ctx, "http://localhost:8080")
	
        spatial_db, _ := database.NewSpatialDatabase(ctx, "sql://sqlite3?dsn=example.db")

	// Create a new custom placetypes definition. In this case the standard Who's On First places
	// definition supplemented with custom placetypes used by SFO Museum. This is used to derive
	// the list of (custom) ancestors associated with any given (custom) placetype.
	pt_def, _ := placetypes.NewDefinition(ctx, "sfomuseum://")

	// Append the custom placetypes definition to the hierarchy resolver options AND explicitly
	// disable placetype filtering (removing candidates that are not ancestors of the placetype
	// of the Who's On First GeoJSON Feature being PIP-ed.
	//
	// If you don't disable default placetype filtering you will need to ensure that the `wof:placetype`
	// property of the features in the spatial database are manually reassigned to take the form
	// of "PLACETYPE" + "#" + "PLACETYPE_DEFINITION_URI", for example: "airport#sfomuseum://"
	//
	// More accurately though the requirement is less that you need to alter the values in the underlying
	// database so much as ensure that the value returned by the `Placetype` method of each `StandardPlacesResult`
	// (SPR) candidate result produced during a point-in-polygon operation is formatted that way. There is
	// more than one way to do this but as a practical matter it's probably easiest to store that (formatted)
	// value in the database IF the database itself is transient. There can be no guarantees thought that
	// a change like this won't have downstream effects on the rest of your code.
	//
	// If you're curious the "PLACETYPE" + "#" + "PLACETYPE_DEFINITION_URI" syntax is parsed by the
	// code used to create placetype filter flags in the `whosonfirst/go-whosonfirst-flags` package and
	// used to load custom definitions on the fly to satisfy tests.
	//
	// Basically, custom placetypes make things more complicated because they are... well, custom. At a
	// certain point it may simply be easier to disable default placetype checks in your own custom results
	// filtering callback function.
	resolver_opts := &hierarchy.PointInPolygonHierarchyResolverOptions{
		Database:             spatial_db,
	        Mapshaper:            mapshaper_cl,
		PlacetypesDefinition: pt_def,
                SkipPlacetypeFilter:  true,
        }

        resolver, _ := hierarchy.NewPointInPolygonHierarchyResolver(ctx, resolver_opts)

        pip_inputs := &filter.SPRInputs{
                IsCurrent: []int64{1},
        }

	// In the case of SFO Museum related records here is a custom results callback that implements its
	// own placetype and floor level checking.
        results_cb := sfom_hierarchy.ChoosePointInPolygonCandidateStrict

	update_cb := hierarchy.DefaultPointInPolygonHierarchyResolverUpdateCallback()
	
	var body []byte
	
	updates, _ := resolver.PointInPolygonAndUpdate(ctx, pip_inputs, results_cb, update_cb, body)

	// Apply updates to body here

}

See also

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type PointInPolygonHierarchyResolver

type PointInPolygonHierarchyResolver struct {
	// Database is the `database.SpatialDatabase` instance used to perform point-in-polygon requests.
	Database database.SpatialDatabase
	// Mapshaper is an optional `mapshaper.Client` instance used to derive centroids used in point-in-polygon requests.
	Mapshaper *mapshaper.Client
	// PlacetypesDefinition is an optional `go-whosonfirst-placetypes.Definition` instance used to resolve custom or bespoke placetypes.
	PlacetypesDefinition placetypes.Definition
	// contains filtered or unexported fields
}

PointInPolygonHierarchyResolver provides methods for constructing a hierarchy of ancestors for a given point, following rules established by the Who's On First project.

func NewPointInPolygonHierarchyResolver

func NewPointInPolygonHierarchyResolver(ctx context.Context, opts *PointInPolygonHierarchyResolverOptions) (*PointInPolygonHierarchyResolver, error)

NewPointInPolygonHierarchyResolver returns a `PointInPolygonHierarchyResolver` instance for 'spatial_db' and 'ms_client'. The former is used to perform point in polygon operations and the latter is used to determine a "reverse geocoding" centroid to use for point-in-polygon operations.

func (*PointInPolygonHierarchyResolver) PointInPolygon

func (t *PointInPolygonHierarchyResolver) PointInPolygon(ctx context.Context, inputs *filter.SPRInputs, body []byte) ([]spr.StandardPlacesResult, error)

PointInPolygon will perform a point-in-polygon (reverse geocoding) operation for 'body' using zero or more 'inputs' as query filters. This is known to not work as expected if the `wof:placetype` property is "common". There needs to be a way to a) retrieve placetypes using a custom WOFPlacetypeSpecification (go-whosonfirst-placetypes v0.6.0+) and b) specify an alternate property to retrieve placetypes from if `wof:placetype=custom`.

func (*PointInPolygonHierarchyResolver) PointInPolygonAndUpdate

PointInPolygonAndUpdate will ...

func (*PointInPolygonHierarchyResolver) PointInPolygonCentroid

func (t *PointInPolygonHierarchyResolver) PointInPolygonCentroid(ctx context.Context, body []byte) (*orb.Point, error)

PointInPolygonCentroid derives an *orb.Point (or "centroid") to use for point-in-polygon operations.

func (*PointInPolygonHierarchyResolver) SetReader

func (t *PointInPolygonHierarchyResolver) SetReader(r reader.Reader)

SetReader assigns 'r' as the internal `reader.Reader` instance used to retrieve ancestor records when resolving a hierarchy.

type PointInPolygonHierarchyResolverOptions

type PointInPolygonHierarchyResolverOptions struct {
	// Database is the `database.SpatialDatabase` instance used to perform point-in-polygon requests.
	Database database.SpatialDatabase
	// Mapshaper is an optional `mapshaper.Client` instance used to derive centroids used in point-in-polygon requests.
	Mapshaper *mapshaper.Client
	// PlacetypesDefinition is an optional `go-whosonfirst-placetypes.Definition` instance used to resolve custom or bespoke placetypes.
	PlacetypesDefinition placetypes.Definition
	// SkipPlacetypeFilter is an optional boolean flag to signal whether or not point-in-polygon operations should be performed using
	// the list of known ancestors for a given placetype. If you are using a custom placetypes defintion (see whosonfirst/go-whosonfirst-placetypes)
	// and do not enable this flag you will need to manually re-assign the `wof:placetype` property of each record being ingested in to your spatial
	// database to take the form of "{CUSTOM_PLACETYPE}#{CUSTOM_PLACETYPE_DEFINITION_URI}". This is necessary because by the time placetype filtering
	// occurs the code is working with `whosonfirst/go-whosonfirst-spr.StandardPlacesResult` instances which only have access to a generic `Placetype`
	// method. There is no guarantee that changing the default value of the `wof:placetype` property will not have unintended consequences so it might
	// be easiest just to enable this flag and deal with placetype filtering in a custom `FilterSPRResultsFunc` callback. Default is false.
	SkipPlacetypeFilter bool
	// Roles is an optional list of Who's On First placetype roles used to derive ancestors during point-in-polygon operations.
	// If missing (or zero length) then all possible roles will be assumed.
	Roles []string
}

type PointInPolygonHierarchyResolverUpdateCallback

type PointInPolygonHierarchyResolverUpdateCallback func(context.Context, reader.Reader, spr.StandardPlacesResult) (map[string]interface{}, error)

PointInPolygonHierarchyResolverUpdateCallback is a function definition for a custom callback to convert 'spr' in to a dictionary of properties containining hierarchy information. Records in 'spr' are expected to be able to be read from 'r'.

func DefaultPointInPolygonHierarchyResolverUpdateCallback

func DefaultPointInPolygonHierarchyResolverUpdateCallback() PointInPolygonHierarchyResolverUpdateCallback

DefaultPointInPolygonHierarchyResolverUpdateCallback returns a `PointInPolygonHierarchyResolverUpdateCallback` function that will return a dictionary containing the following properties: wof:parent_id, wof:country, wof:hierarchy

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL