Blog

OGC Made Easy: Tiling Spec

Looking for help getting started with the Open Geospatial Consortium (OGC) API Tiles specification? This primer can get you going.

OGC Made Easy: Tiles

The Open Geospatial Consortium (or OGC) publishes standards related to geospatial information services and formats. The organization recently published “OGC API - Tiles - Part 1: Core” – a specification aimed at web map tile services. The standard weighs in at 137 pages and depends on another 273 page standard, so if you are new to OGC specifications, getting started can be intimidating. The goal of this post is to provide a quickstart for tiled map providers who might want to implement the standard but are struggling to know where to begin.

What’s in a name?

The “OGC API - Tiles - Part 1: Core” specification name is quite a mouthful. The rest of this post will use "OGC Tiles" as a shorthand, but it is worth picking apart the name first.

The previous generation of OGC standards had titles like "Web Map Service" (WMS), "Web Feature Service" (WFS), and "Web Coverage Service" (WCS). These were affectionately referred to as the WxS standards. The "OGC API" prefix in this new specification’s name refers to the new generation of standards designed to work in harmony with existing web standards and best practices – so things like content negotiation and HTTP status codes aren’t redefined by the standards themselves.

The "Tiles" part of the name is what this standard is really about. Before the Google Maps era, maps on web pages were often provided by services that would generate map images of arbitrary places on demand. It is generally faster and more efficient to render map images ahead of time and to cut these images up into regular grids of tiles. The OGC Tiles specification describes how mapping clients (like web pages) can get information about tilesets and how to request the tiled data itself.

The "Part 1: Core" suffix in the standard’s name implies that additional parts will be published in the future describing functionality beyond the core. The core concerns itself with how to provide metadata about a tileset, how to describe the tiling scheme, and how to relate a tileset to a geospatial data collection that might have other representations. The core also discusses how a tileset with a temporal dimension can be handled.

Prior art

For the last 18 years since the launch of Google Maps, people who develop mapping applications on the web have been consolidating on a common scheme for serving tiled map data. The tiles in this scheme fit into a conceptual pyramid where each tile can be identified by three parameters: zoom level (Z), column (X), and row (Y). This has become known as the XYZ tiling scheme.

A conceptual tileset
A conceptual tileset. Zoom levels are numbered from 0 to n. Column and row numbers (not shown above) are used to locate a tile within a given zoom level.

In an XYZ tileset, the 0/0/0 tile at the top usually covers the whole world, and each level below contains four tiles for every one tile above. Most XYZ tilesets use the same Web Mercator projection as Google Maps. XYZ tiles are usually available with a URL that looks like this: https://example.com/tiles/{z}/{x}/{y}.png. In this URL template, {z} is a placeholder for the zoom level, {x} for the column number, and {y} for the row number. The available zoom, column, and row values generally start with zero. The zoom level may extend to something in the 20s for "street level" data.

Of course all of these things (extent, projection, URL template, maximum level) may vary from tileset to tileset. The Mapbox TileJSON specification formalizes how some of this variety can be described in a simple JSON document. One notable constraint of TileJSON is that tilesets must use the Web Mercator projection popularized by Google Maps.

Conformance

The OGC Tiles specification builds on what people have been doing for years with XYZ tilesets and goes beyond what is possible to describe with TileJSON. The standard is organized into conformance classes that build on one another to handle increasingly complex use cases.

The good news is that if you have an existing XYZ tileset, you are (most likely) already OGC Tiles compliant! The Core Conformance Class boils down to three unconditional requirements:

  • Tiles must be available using a URL built from a template like https://example.com/tiles/{z}/{x}/{y}.png (see Requirement 1 for more detail).
  • Successful responses must have a 200 status code (see Requirement 5 for more detail).
  • Responses for tiles that do not exist must have a 400 or 404 status code (see Requirement 6 for more detail).

The Core Conformance Class in OGC Tiles has additional requirements that only kick in under certain conditions. For example, if you also conform to the Core Conformance Class from the separate “OGC API - Common - Part 1: Core” standard, then requirements 2, 3, and 4 in the Tiles standard say that you have to use {tileMatrix} instead of {z}, {tileRow} instead of {y}, and {tileCol} instead of {x} in your tile URL template (but the core doesn’t have any requirements about how this tile URL template is advertised).

If you have an XYZ tileset and you pass the list of requirements above, then you can say that you implement the Core Conformance Class from the "OGC API - Tiles - Part 1: Core" specification. The OGC Common standard describes a formal way to advertise which conformance classes are implemented. For example, if you conform with the Core Tile requirements and the Core Common requirements, you would advertise this with a document like this:

{
  "conformsTo": [
    "http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/core",
    "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/core"
  ]
}

The Core Conformance Class from the "OGC API - Common - Part 1: Core" standard requires that a conformance document like the one above is available at a /conformance path relative to the root of your service. So a service at https://example.com/ would make the conformance document available at https://example.com/conformance.

Beyond the core

The Core Conformance Class in OGC Tiles adds a bit of formality to what people have already been doing with XYZ tilesets. If you want to provide additional metadata about your tilesets, you can make use of the conformance classes beyond Core. The next set of requirements to consider are part of the Tileset Conformance Class. You might choose to implement these requirements if your tileset doesn’t cover the whole world, if you want to let clients know about a limited set of available zoom levels, or if you use an alternative projection, for example.

The Tileset Conformance Class depends on a separate standard, “OGC Two Dimensional Tile Matrix Set and Tile Set Metadata” (referred to as TMS below), to define the structure of the tileset metadata. Examples are included below, but if all you want is the JSON Schema for tileset metadata, you can find that here: https://schemas.opengis.net/tms/2.0/json/tileSet.json.

Here is an example tileset metadata document that describes the XYZ OpenStreetMap tiles:

{
  "title": "OpenStreetMap",
  "dataType": "map",
  "crs": "http://www.opengis.net/def/crs/EPSG/0/3857",
  "tileMatrixSetURI": "http://www.opengis.net/def/tilematrixset/OGC/1.0/WebMercatorQuad",
  "links": [
    {
      "title": "Tile URL Template",
      "href": "https://tile.openstreetmap.org/{tileMatrix}/{tileCol}/{tileRow}.png",
      "rel": "item",
      "type": "image/png",
      "templated": true
    },
    {
      "title": "Tiling Scheme",
      "href": "https://ogc-tiles.xyz/tileMatrixSets/WebMercatorQuad",
      "rel": "http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme",
      "type": "application/json"
    }
  ]
}

The optional title provides users with an easy way to identify the tileset. You can also provide an optional description with more information. The dataType is a required property. Values can be map (for rendered raster data), vector (for vector tile data), or coverage (for raw raster data). The crs is an identifier (specifically a URI) that indicates which coordinate reference system is used. The tileMatrixSetURI is an identifier for the tiling scheme used. The coordinate reference system and tiling scheme shown corresponds to the setup popularized by Google Maps (zoom level zero has one 256 x 256 pixel tile covering the world in Web Mercator; every level below has four tiles for each tile above). The links property includes a link to the tiling scheme definition (also referred to as tile matrix set). As an alternative to linking to a separate document with the tiling scheme definition, the definition can be included in the tileset metadata using a tileMatrixSet property (this is Requirement 17 of the TMS standard). Finally, and most importantly, the links list includes the tile URL template for requesting individual tiles.

One of the most useful reasons to implement the requirements of the Tileset Conformance Class is to let clients know that a tileset is not available for the whole world. The boundingBox property of the tileset metadata serves this purpose, and including it enables a client to automatically zoom to exactly where there are tiles available. Here is an example bounding box member that indicates that tiles are only available north of the equator and west of the prime meridian (see the tileset-bbox.json for a complete example that includes the whole world as a bounding box):

"boundingBox": {
  "crs": "http://www.opengis.net/def/crs/OGC/1.3/CRS84",
  "lowerLeft": [-180, 0],
  "upperRight": [0, 90]
}

The tileset metadata can also include a tileMatrixSetLimits property. It is recommended that tilesets with limited extent (compared to the extent of the tiling scheme) use this property in addition to the boundingBox to describe those limits.

The tileMatrixSetLimits property is also useful to describe a limited set of zoom levels. For example, if only zoom levels 2 and 3 were available for a particular tileset, the limits could be described with this metadata (see the tileset-limits.json for a complete example):

"tileMatrixSetLimits": [
  {
    "tileMatrix": "2",
    "minTileRow": 0,
    "maxTileRow": 1,
    "minTileCol": 0,
    "maxTileCol": 1
  },
  {
    "tileMatrix": "3",
    "minTileRow": 0,
    "maxTileRow": 3,
    "minTileCol": 0,
    "maxTileCol": 3
  }
]

The above tileMatrixSetLimits only limit the available zoom levels or tile matrix levels. To limit the available extent as well, adjust the minimum and maximum column and row values. The JSON schema for this property is available here: https://schemas.opengis.net/tms/2.0/json/tileMatrixLimits.json.

Next steps

The OGC Tiles standard describes additional sets of requirements beyond the Core and Tileset Conformance Classes. For example, the Tileset List Conformance Class includes requirements for describing a list of tilesets with a subset of the metadata required for an individual tileset (see Requirement 9 and 10). In addition, the OGC Common standard includes requirements that can be useful if your tilesets are related to other geospatial data collections. The OGC API family of standards leverages OpenAPI and JSON Schema for describing services. If your tileset is provided by a service that already makes use of OpenAPI, it can be useful to implement the OpenAPI Conformance Class of the OGC Common standard.

Because these requirements are spread across multiple specifications with slightly different conventions and vocabulary, it can be a challenge to get started if you want to implement a compliant tile service. We put together a go-ogc library and a xyz2ogc utility to help bootstrap the process. You can use the xyz2ogc generate command to produce metadata documents describing an existing XYZ tileset that satisfies the requirements of the conformance classes discussed above. See https://ogc-tiles.xyz/ for a complete set of metadata documents representing some common XYZ tilesets.

Be sure to share your experiences using OGC Tiles with us at https://community.planet.com/developers-55. Follow us on Twitter @PlanetDevs. Sign up for Wavelengths, the Planet Developer Relations newsletter, to learn more about our workflows and get announcements about what is new.

Lightweight GIS pipelines with fio-planet

Planet’s new CLI has no builtin GIS capabilities other than what Planet’s APIs provide. To help make more sophisticated workflows possible, Planet is releasing a new package of command line programs for manipulating streams of GeoJSON features.

Making Command Line GIS Pipelines with fio-planet

Planet’s new command line interface (CLI) has no builtin GIS capabilities other than what Planet’s APIs provide. This is by design; it is meant to be complemented by other tools. To help make more sophisticated workflows possible, Planet is releasing a new package of command line programs, fio-planet, which let you build Unix pipelines for manipulating streams of GeoJSON features. Feature simplification before searching the Data API is one such application.

Planet’s new command line interface (CLI) has three main jobs. One is to help you prepare API requests. Planet’s APIs and data products have many options. The CLI provides tools to make specifying what you want easy. Another purpose is to make the status of those requests visible. Some requests take a few minutes to be fulfilled. The CLI permits notification when they are done, like the following command line output.

planet orders wait 65df4eb0-e416-4243-a4d2-38afcf382c30 && cowsay "your order is ready for download"
__________________________________
< your order is ready for download >
 ----------------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Or you may prefer to leave cows alone and just download the order when it is ready.

planet orders wait 65df4eb0-e416-4243-a4d2-38afcf382c30 && planet orders download 65df4eb0-e416-4243-a4d2-38afcf382c30

The third purpose of the CLI is to print API responses in a form that lets them be used as inputs to other CLI programs such as jq, the streaming JSON filter. CLI commands, generally speaking, print newline-delimited streams of JSON objects or GeoJSON features.

By design, the CLI has limited options for manipulating JSON and relies heavily on jq. The CLI docs have many examples of usage in combination with jq. Similarly, the CLI outsources GIS capabilities to other programs. One option is the inscrutable and venerable ogr2ogr, but it is not as handy with streaming JSON or GeoJSON as jq is.

To help make sophisticated command line geo-processing workflows more accessible, Planet is releasing a package of command line programs that let you build Unix pipelines for manipulating streams of GeoJSON features. Feature simplification before searching the Data API is one of the many applications.

Processing GeoJSON with fio and fio-planet

The Python package Fiona includes a CLI designed around streams of GeoJSON features. The fio-cat command streams GeoJSON features out of one or more vector datasets. The fio-load command performs the reverse operation, streaming features into GIS file formats. Two Shapefiles with the same schema can be combined into a GeoPackage file using the Unix pipeline below.

fio cat example1.shp example2.shp | fio load -f GPKG examples.gpkg

Note: pipeline examples in this blog post assume a POSIX shell such as bash or zsh. The vertical bar | is a “pipe”. It creates two processes and connects the standard output stream of one to the standard input stream of the other. GeoJSON features pass through this pipe from one program to the other.

Planet’s new fio-planet package fits into this conceptual framework and adds three commands to the fio toolbox: fio-filter, fio-map, and fio-reduce. These commands provide some of the features of spatial SQL, but act on features in a GeoJSON feature sequence instead of rows in a spatial table. Each command accepts a JSON sequence as input, evaluates an expression in the context of the sequence, and produces a new JSON sequence as output. These may be GeoJSON sequences.

  • fio-filter evaluates an expression for each feature in a stream of GeoJSON features, passing those for which the expression is true.
  • fio-map maps an expression over a stream of GeoJSON features, producing a stream of new features or other values
  • fio-reduce applies an expression to a sequence of GeoJSON features, reducing them to a single feature or other value.

In combination, many transformations are possible with these three commands.

Expressions take the form of parenthesized, comma-less lists which may contain other expressions. The first item in a list is the name of a function or method, or an expression that evaluates to a function. It’s a kind of prefix, or Polish notation. The second item is the function's first argument or the object to which the method is bound. The remaining list items are the positional and keyword arguments for the named function or method. The list of functions and callables available in an expression includes:

  • Python builtins such as dict, list, and map
  • From functools: reduce
  • All public functions from itertools, for example, islice, and repeat
  • All functions importable from Shapely 2.0, for example, Point, and unary_union
  • All methods of Shapely geometry classes

Let’s look at some examples. Below is an expression that evaluates to a Shapely Point instance. Point is a callable instance constructor and the pair of 0 values are positional arguments. Note that the outermost parentheses of an expression are optional.

(Point 0 0)

Fio-planet translates that to the following Python expression.

from shapely import Point
Point(0, 0)

Next is an expression which evaluates to a Polygon, using Shapely’s buffer function. The distance parameter is computed from its own expression, demonstrating prefix notation once again.

(buffer (Point 0 0) :distance (/ 5 2))

The equivalent in Python is

from shapely import buffer
buffer(Point(0, 0), distance=5 / 2)

Fio-filter and fio-map evaluate expressions in the context of a GeoJSON feature and its geometry attribute. These are named f and g. For example, here is an expression that tests whether the distance from the input feature to the point at 0 degrees North and 0 degrees E is less than or equal to one kilometer (1,000 meters).

(<= (distance g (Point 0 0)) 1000)

fio-reduce evaluates expressions in the context of the sequence of all input geometries, which is named c. For example, the expression below dissolves all input geometries using Shapely's unary_union function.

(unary_union c)

Fio-filter evaluates expressions to true or false. Fio-map and fio-reduce will attempt to wrap expression results as GeoJSON feature objects unless raw results are specified. For more information about expressions and usage, please see the fio-planet project page.

Why does fio-planet use Lisp-like expressions for code that is ultimately executed by a Python interpreter? Fio-planet expressions are intended to allow a small subset of what you can do in GeoPandas, PostGIS, QGIS, or a bespoke Python program. By design, fio-planet’s expressions should help discriminate between workflows that can execute effectively on the command line using GeoJSON and workflows that are better executed in a more powerful environment. A domain-specific language (DSL), instead of a general purpose language, is intended to clarify the situation. Fio-planet’s DSL doesn’t afford variable assignment or flow control, for example. That’s supposed to be a signal of its limits. As soon as you feel fio-planet expressions are holding you back, you are right. Use something else!

The program ogr2ogr uses a dialect of SQL as its DSL. The QGIS expression language is also akin to a subset of SQL. Those programs tend to see features as rows of a table. Fio-planet works with streams, not tables, and so a distinctly different kind of DSL seemed appropriate. It adopted the Lisp-like expression language of Rasterio. Parenthesized lists are very easy to parse and Lisp has a long history of use in DSLs. Hylang is worth a look if you’re curious about a more fully featured Lisp for the Python runtime.

Lisp Cycles
(https://xkcd.com/297/)

Simplifying shapes with fio-map and fio-reduce

Previously, the Planet Developers Blog published a deep dive into simplifying areas of interest for use with the Planet platform. The takeaway from that post is that it’s a good idea to simplify areas of interest as much as you can. Fio-planet brings methods for simplifying shapes and measuring the complexity of shapes to the command line alongside Planet’s CLI. Examples of using them in the context of the previous post are shown below. All the examples use a 25-feature shapefile. You can get it from rmnp.zip or access it in a streaming fashion as shown in the examples below.

Note: all examples assume a POSIX shell such as bash or zsh. Some use the program named jq. The vertical bar | is a “pipe”. It creates two processes and connects the standard output stream of one to the standard input stream of the other. The backward slash \ permits line continuation and allows pipelines to be typed in a more readable form.

A figure accompanies each example. The figures are rendered from a GeoJSON file by QGIS. The GeoJSON files are made by collecting, with fio-collect, the non-raw output of fio-cat, fio-map, or fio-reduce. For example, the pipeline below converts the zipped Shapefile on the web to a GeoJSON file on your computer.

fio cat zip+https://github.com/planetlabs/fio-planet/files/10045442/rmnp.zip \
| fio collect > rmnp.geojson

Counting vertices in a feature collection

The vertex_count function, in conjunction with fio-map's --raw option, prints out the number of vertices in each feature. The default for fio-map is to wrap the result of every evaluated expression in a GeoJSON feature; --raw disables this. The program jq provides a nice way of summing the resulting sequence of numbers. jq -s “slurps” a stream of JSON objects into an array.

The following pipeline prints the number 28,915.

Input:

fio cat zip+https://github.com/planetlabs/fio-planet/files/10045442/rmnp.zip \
| fio map 'vertex_count g' --raw \
| jq -s 'add'

Output:

28915
Zones
The 25 Wilderness Patrol Zones of Rocky Mountain National Park have 28,915 vertices.

Counting vertices after making a simplified buffer

One traditional way of simplifying an area of interest is to buffer it by some distance and then simplify it by a comparable distance. The effectiveness of this method depends on the nature of the data, especially the distance between vertices around the boundary of the area. There's no need to use jq in the following pipeline because fio-reduce prints out a sequence of exactly one value.

Input:

fio cat zip+https://github.com/planetlabs/fio-planet/files/10045442/rmnp.zip \
| fio reduce 'unary_union c' \
| fio map 'simplify (buffer g 40) 40' \
| fio map 'vertex_count g' --raw

Output:

469

Variable assignment and flow control are not provided by fio-planet’s expression language. It is the pipes between commands which afford some assignment and logic. For example, the pipeline above is practically equivalent to the following Python program.

import fiona
from fiona.transform import transform_geom
from shapely import buffer, shape, simplify, mapping, unary_union


with fiona.open("zip+https://github.com/planetlabs/fio-planet/files/10045442/rmnp.zip") as dataset:
    c = [shape(feat.geometry) for feat in dataset]


g = unary_union(c)


g = shape(transform_geom("OGC:CRS84", "EPSG:6933", mapping(g)))
g = simplify(buffer(g, 40), 40)
g = shape(transform_geom("EPSG:6933", "OGC:CRS84", mapping(g)))


print(vertex_count(g))  
Simplified
Zones merged, buffered, and simplified into one shape with 469 vertices.

Counting vertices after merging convex hulls of features

Convex hulls are an easy means of simplification. There are no distance parameters to tweak as there were in the example above. The --dump-parts option of fio-map turns the parts of multi-part features into separate single-part features. This is one of the ways in which fio-map can multiply its inputs, printing out more features than it receives.

The following pipeline prints the number 157.

Input:

fio cat zip+https://github.com/planetlabs/fio-planet/files/10045442/rmnp.zip \
| fio map 'convex_hull g' --dump-parts \
| fio reduce 'unary_union c' \
| fio map 'vertex_count g' --raw

Output:

157
Convex
The convex hulls of the zones, when merged, have 157 vertices.

Counting vertices after merging the concave hulls of features

Convex hulls simplify, but also dilate concave areas of interest. They fill the "bays", so to speak, and this can be undesirable. Concave hulls do a better job at preserving the concave nature of a shape and result in a smaller increase of area.

The following pipeline prints the number 301.

Input:

fio cat zip+https://github.com/planetlabs/fio-planet/files/10045442/rmnp.zip \
| fio map 'concave_hull g 0.4' --dump-parts \
| fio reduce 'unary_union c' \
| fio map 'vertex_count g' --raw

Output:

301
Concave
The concave hulls of the zones, when merged, have 301 vertices.

Using fio-planet with the Planet CLI

Now for more specific examples of using fio-planet with Planet’s CLI on the command line. If you want to spatially constrain a search for assets in Planet’s catalog or clip an order to an area of interest, you will need a GeoJSON object. The pipeline below creates one based on the data used above and saves it to a file. Note that the fourth piece of the pipeline forces the output GeoJSON to be two-dimensional. Planet’s Data API doesn’t accept GeoJSON with Z coordinates.

fio cat zip+https://github.com/planetlabs/fio-planet/files/10045442/rmnp.zip \
| fio map 'concave_hull g 0.4' --dump-parts \
| fio reduce 'unary_union c' \
| fio map 'force_2d g' \
| fio collect \
> rmnp.geojson

Incorporating that GeoJSON object into a Data API search filter is the next step. The filter produced by the command shown below will find items acquired after Feb 14, 2023 that intersect with the shape of Rocky Mountain National Park.

planet data filter \
--date-range acquired gt 2023-02-14 \
--date-range acquired lt 2023-02-25 \
--geom=rmnp.geojson \
> filter.json

With a filter document, stored here in filter.json – the name of the filter doesn’t matter – you can make a filtered search and stream the GeoJSON results into a file. It’s a common practice to use .geojsons, plural, as the file extension for newline-delimited sequences of GeoJSON features.

planet data search PSScene --filter=filter.json > results.geojsons

With a filter document, stored here in filter.json – the name of the filter doesn’t matter – you can make a filtered search and stream the GeoJSON results into a file. It’s a common practice to use .geojsons, plural, as the file extension for newline-delimited sequences of GeoJSON features.

planet data search PSScene --filter=filter.json > results.geojsons

The fio-planet commands are useful for processing search results, too. The results.geojsons file contains a stream of GeoJSON features which represent some PSScene items in Planet’s catalog. Here’s an example of finding the eight scenes that cover 40.255 degrees North and 105.615 degrees West (the summit of Longs Peak in Rocky Mountain National Park).

Input:

cat results.geojsons \
| fio filter 'contains g (Point -105.615 40.255)' \
| jq '.id'

Output:

"20230222_172633_18_247f"
"20230221_165059_39_241b"
"20230221_165057_22_241b"
"20230220_171805_27_2251"
"20230219_172658_28_2461"
"20230217_172940_13_2474"
"20230216_171328_72_2262"
"20230214_173045_31_2489"

To further filter by the degree to which the ground is visible in each scene – which could also have been specified in the search filter, by the way – we can add a jq step to the pipeline.

cat results.geojsons \
| fio filter 'contains g (Point -105.615 40.255)' \
| jq -c 'select(.properties.visible_percent > 90)' \
| fio collect \
> psscenes.geojson
Search
Three PlanetScope Scene footprints mapped with RMNP and Longs Peak.

Fio-filter and fio-map can also be used as checks on the number of vertices and the area of GeoJSON features. Let’s say that you want to keep your areas of interest to 500 vertices or less and no more than 2,000 square kilometers. You can ask fio-map to print true or false or ask fio-filter to screen out features that don’t meet the criteria.

Input:

fio cat rmnp.geojson \
| fio map -r '& (< (vertex_count g) 500) (< (area g) 2000e6)'

Output:

true

Note that the value returned by the builtin area function has units of meters squared. The area of the feature in the rmnp.geojson file is 1,117.4 square kilometers and has 301 vertices.

Creating search geometries on the command line

What if you want to do a quick search that isn’t related to any particular GIS dataset? The new fio-map command can help.

All of Shapely’s geometry type constructors are available in fio-planet expressions and can be used to create new shapes directly on the command line. Remember that fio-map’s --raw/-r option specifies that the outputs of the command will not be wrapped in GeoJSON feature objects, but returned in their natural, raw form. Another fio-map option, --no-input/-n, specifies that the given expression will be mapped over the sequence [None], as with jq --null-input/-n, producing a single output. Together, these options let fio-map produce new GeoJSON data that is not based on any existing data. On the command line you can combine the options as -nr or -rn. If it helps, think of -rn as “right now”.

Input:

fio map -rn '(Point -105.615 40.255)'

Output:

{"type": "Point", "coordinates": [-105.615, 40.255]}

This value can be saved to a file and used with the planet-data-filter program.

fio map -rn '(Point -105.615 40.255)' > search.geojson
planet data filter --geom search.geojson > filter.json
planet data search PSScene --filter filter.json

Or it can be used inline to make a search without saving any intermediate files at all using POSIX command substitution with the $(...) syntax, although nested substitution takes a heavy toll on the readability of commands. Please speak with your tech lead before putting pipelines like the one below into production.

planet data search PSScene --filter="$(planet data filter --geom="$(fio map -rn '(Point -105.615 40.255)')")"

Creating and simplifying GeoJSON features for use with the Planet CLI are two of the applications for fio-planet’s map, filter, and reduce commands. You can surely think of more! Combine them in different ways to create new pipelines suited to your workflows and share your experience with others in Planet’s Developers Community or in the fio-planet discussion forum.

Next Steps

Keep up to date with the fio-planet GitHub repository and the latest project documentation. Chat with us about your experiences and issues using Fiona and fio-planet with Planet data at https://community.planet.com/developers-55. Follow us on Twitter @PlanetDevs. Sign up for Wavelengths, the Planet Developer Relations newsletter, to get more information on the tech behind the workflows.

Planet's Role in Sustaining the Python Geospatial Stack

Can Planet data or the Planet platform be used with Python? It can, and not by accident. Planet is working to make it so. This post attempts to explain what the Python Geospatial Stack is and Planet’s role in keeping the stack in good shape so that developers continue to get value from it.

Planet’s customers don’t only use Planet products via commercial or open source desktop GIS or partner platforms. Many of you are integrating Planet’s products and services into custom-built GIS systems, which use the open source Python Geospatial Stack. This post attempts to explain what the Python Geospatial Stack is and Planet’s role in keeping the stack in good shape so that developers like you continue to get value from it. Can Planet data or the Planet platform be used with Python? It can, and not by accident. Planet is working to make it so. You can help, too.

What is the Python Geospatial Stack?

The Python Geospatial Stack is a set of Python packages that use a smaller set of non-standard system libraries written in C/C++: GEOS, PROJ, and GDAL. GEOS is a library of 2-D computational geometry routines. PROJ is a library for cartographic projections and geodetic transformations. GDAL is a model for computing with raster and vector data and a collection of format drivers to allow data access and translation on your computer or over your networks. Planet engineers, including myself, have contributed significantly to these system libraries. Principal Engineer Frank Warmerdam is the author of GDAL, a longtime maintainer of PROJ version 4, and a major contributor to GEOS.

Shapely, Pyproj, Fiona, Rasterio, GeoPandas, PDAL, and Xarray are the core of the stack. Shapely draws upon GEOS. Shapely is useful to developers for filtering asset catalog search results, comparing catalog item footprints to areas of interest, and much more. Pyproj wraps PROJ. Pyproj is helpful for calculating the projected area of regions that are described in longitude and latitude coordinates, and more. Fiona and Rasterio are based on GDAL and allow developers to read and write vector and raster data. PDAL translates and manipulates point cloud data, GeoPandas and Xarray use Fiona and Rasterio and provide higher levels of abstraction for analysis of column-oriented tabular and gridded scalar data, respectively.

How did the stack come about?

Why are so many organizations using Python for GIS work? Isn’t it slow? Only an instructional language? Timing explains a lot. When Bruce Dodson started looking at alternatives to Avenue, the scripting language of Esri's ArcView GIS version 3, Python was ready. Python had a good extension story in 2000, meaning that although Python was relatively slow, it was fairly easy to extend with fast code written in C.

When you compute and dissolve the convex hulls of multiple shapes using Shapely, all of the intensive calculation is done by native code, not Python bytecode. The open source native code (GDAL, GEOS, PROJ, etc) just happened to rise up at the same point in time. Timing really is everything.

Piecewise convex hulls, dissolved

To close the loop, the Geospatial Stack has helped make the Python language sticky. Developers chose Python from many options for the availability of solid, feature-rich, decently-documented libraries in a particular domain, and end up staying for all the other nice things about the Python language and community. The reason why Python is the second best language for many domains is that the same discovery happened in Numerical Analysis, Machine Learning, Image Processing, and elsewhere.

How is Planet helping?

Software and software communities need care and feeding. Planet is involved through financial sponsorship, project governance, code maintenance, and builds of binary distributions.

In 2021, Planet became an inaugural platinum sponsor of GDAL. Planet is helping to pay for infrastructure costs and for the labor of full-time maintainers for GDAL and affiliated projects like GEOS and PROJ. In addition, two Planet engineers (Frank Warmerdam and I) serve on GDAL’s project steering committee. The impact of this sponsorship is huge. GDAL has a better and faster build system, which makes it easier to contribute to and which reduces the cost of every bug fix and new feature. Most importantly, the sponsorship makes it possible for GDAL’s longtime maintainers to avoid burnout, stay involved, and mentor their eventual successors.

xkcd - dependency

Planet’s Developer Relations team is mainly involved at the Python level. I’m the release manager for Fiona and Rasterio. I make sure that binary distributions (aka “wheels”) for Linux, MacOS, and Windows are uploaded to Python’s Package Index so that when developers run “pip install rasterio” almost everybody gets pre-compiled Python packages with GDAL, GEOS, and PROJ batteries included. Packages that “just work” for many cases on laptops and in hosted notebooks.

A major new version of Shapely was released at the end of 2022 through collaboration with core GeoPandas developers. Shapely 2.0 adds vectorized operations that radically speed up GeoPandas and keep this piece of the stack relevant as projects like GeoParquet start to change the nature of vector data.

Fiona 1.9.0 was released on Jan 20, 2023 and also provides a boost to GeoPandas. Planet’s Developer Relations team is currently building new tools that integrate with this new version of the Geospatial Stack’s vector data package. They will be announced soon.

Multiple teams at Planet helped make Rasterio 1.3.0, Shapely 2.0.0, and Fiona 1.9.0 successful releases by testing beta releases of these packages. Planet is financially sustaining the open source projects that form the foundation of the stack. The DevRel team is engaged with the open source communities that write the stack’s code. At Planet we consider the health of the Python Geospatial Stack to be a big factor in the overall experience of our platform.

Next Steps

Are you using Fiona, Rasterio, or Shapely? Upgrade to the latest versions and try them out. Chat with us about any ideas or issues with using these packages with Planet data at https://community.planet.com/developers-55. Follow us on twitter @planetdevs.

GEE Integration: a Planet Developers Deep Dive

Directly Integrating Planet Data Delivery with Google Earth Engine

Do you need some time and space?

With Planet’s high resolution, multi-band, daily imagery, with large color depth, users have a lot of data! Do you find your computer always reminding you that you need to “make more space available?” Are you always running late because your data processing is taking too long? Please let me present:

Planet’s Google Earth Engine Delivery Integration
Empty trash Laptop on fire

Google Earth Engine, or GEE for short, is a cloud-based platform for geospatial analysis and remote sensing applications. It is part of Google’s larger Earth platform, which includes the popular Google Earth mapping software. GEE gives users the opportunity to store their Planet data on Google’s cloud storage and harness Google’s computing infrastructure. GEE is used to analyze and monitor the Earth’s changing environment. It can be used to measure land cover and vegetation health, track changes in surface water, monitor wildfires, and measure other impacts of climate change. It can also be used to generate maps, 3D models, and other visualizations. GEE has become a popular platform for geospatial analysis and remote sensing applications, and has been used for a range of projects, from monitoring crop yields in Canada to tracking deforestation in the Amazon.

Planet 𝗑 Google

Google Earth Engine is used to run large-scale geospatial data analysis or to perform a few simple commands on geospatial data. GEE leverages Google’s tools and services, and performs computations at scale with a user-friendly interface. This sort of geospatial working environment makes analyzing your Planet data fast and easy.

Planet users can easily have their data delivered directly to their GEE project thanks to Planet’s Google Earth Engine Delivery Integration - a simpler way for GEE users to incorporate Planet data into their existing workflows and ongoing projects. This integration simplifies the GEE delivery experience by creating a direct connection from Planet's Orders API to GEE, so that you don't have to download then re-upload images or spin up temporary cloud storage when moving imagery to your GEE account.

In this blog post we are going to cover:

  • How to prepare your Google Earth Engine account for data delivery via Planet’s Orders API
  • How to use Planet’s GEE Delivery Integration service on your GEE project
  • A basic and advanced example JSON requests and responses
  • A Python example using Planet’s Python SDK, hosted in a Jupyter Notebook
  • A Planet CLI example

How to deliver your data to GEE

To deliver data to your GEE project, you must first sign up for an Earth Engine account, create a Cloud Project, enable the Earth Engine API, and grant access to a Google service account.

Set up GEE

Before we get into the coding aspect of it all, we need to set up our GEE project.

1. Sign up for an EE account

First thing’s first, let’s sign up for an Earth Engine account. Go to: https://signup.earthengine.google.com/

GEE sign up form

2. Register a Google Cloud Project (GCP) project

Now that you have an Earth Engine account, let’s create a new Cloud Project for your account. This will simultaneously create an empty ImageCollection. This ImageCollection is where all of your data will be delivered to.

Go to: https://code.earthengine.google.com/register/

GEE getting started page

3. Enable the EE API for the project

In order to allow Planet’s Orders API to interact with GEE, we must first enable the Earth Engine API.

Go to: https://console.cloud.google.com/apis/library/earthengine.googleapis.com

GEE getting started page

4. Grant Planet access to deliver to your GEE project

Lastly, you need to create a service account, which in this case is essentially a virtual account, which will be used to automate our GEE integration. To create this service account, return to the console and select: Navigation menu > IAM & Admin > Service Accounts

Then we can create a service account by clicking “+CREATE SERVICE ACCOUNT”. Here we will add Planet’s Google service account, named planet-gee-uploader@planet-earthengine-staging.iam.gserviceaccount.com

Finally, your service account must be granted the role of “Earth Engine Resource Writer”, which will allow it to deliver your data to your ImageCollection.

Examples

Now that you have a GEE project that Planet has access to, here’s how you tell Planet how to access that GEE project.

To communicate with Planet’s servers, we use RESTful endpoints. Regardless of the language you use to make a network request, here’s the body of the request. In the language that you use, you’ll need to provide the following to successfully make a request:

  • API endpoint (https://api.planet.com/compute/ops/orders/v2)
GEE getting started page
  • Basic auth using your Planet API key
  • Header to specify that the Content-Type is application/json
  • Body, the specifics of what you’re requesting

Basic JSON request

REST method:

POST https://api.planet.com/compute/ops/orders/v2/

An example JSON request body for delivery for:

  • Order name: iowa_order
  • Item IDs: 20200925_161029_69_2223, 20200925_161027_48_2223
  • Item type: PSScene
  • Product bundle: analytic_sr_udm2
  • GEE project name: planet-devrel-dev
  • ImageCollection name: gee-integration-testing

Request:

{
  "name": "iowa_order",
  "products": [
    {
      "item_ids": [
        "20200925_161029_69_2223",
        "20200925_161027_48_2223"
      ],
      "item_type": "PSScene",
      "product_bundle": "analytic_sr_udm2"
    }
  ],
  "delivery": {
    "google_earth_engine": {
      "project": "planet-devrel-dev",
      "collection": "gee-integration-testing"
    }
  }
}

Response:

​​{
  "_links": {
    "_self": "https://api.planet.com/compute/ops/orders/v2/1e4ade86-20dd-45bc-a3cf-4e6f378b5774"
  },
  "created_on": "2022-11-30T19:08:34.193Z",
  "error_hints": [],
  "id": "1e4ade86-20dd-45bc-a3cf-4e6f378b5774",
  "last_message": "Preparing order",
  "last_modified": "2022-11-30T19:08:34.193Z",
  "metadata": {
    "stac": {}
  },
  "name": "iowa_order",
  "products": [
    {
      "item_ids": [
        "20200925_161029_69_2223",
        "20200925_161027_48_2223"
      ],
      "item_type": "PSScene",
      "product_bundle": "analytic_sr_udm2"
    }
  ],
  "state": "queued"
}

Advanced JSON request

GEE integration also supports two tools, clipping, and sensor harmonization. To include them, we add them to the “tools” schema. Adding to the previous example, if we want to clip to an AOI defined as a polygon and harmonize our data to Sentinel-2’s sensor, the JSON request would look like:

Request:

{
  "name": "iowa_order",
  "products": [
    {
      "item_ids": [
        "20200925_161029_69_2223",
        "20200925_161027_48_2223"
      ],
      "item_type": "PSScene",
      "product_bundle": "analytic_sr_udm2"
    }
  ],
  "delivery": {
    "google_earth_engine": {
      "project": "planet-devrel-dev",
      "collection": "gee-integration-testing"
    }
  },
  "tools": [
    {
      "clip": {
        "aoi": {
          "type": "Polygon",
          "coordinates": [
            [
              [-91.198465, 42.893071],
              [-91.121931, 42.893071],
              [-91.121931, 42.946205],
              [-91.198465, 42.946205],
              [-91.198465, 42.893071]
            ]
          ]
        }
      }
    },
    {
      "harmonize": {
        "target_sensor": "Sentinel-2"
      }
    }
  ]
}

Response:

​​{
  "_links": {
    "_self": "https://api.planet.com/compute/ops/orders/v2/1e4ade86-20dd-45bc-a3cf-4e6f378b5774"
  },
  "created_on": "2022-11-30T19:08:34.193Z",
  "error_hints": [],
  "id": "1e4ade86-20dd-45bc-a3cf-4e6f378b5774",
  "last_message": "Preparing order",
  "last_modified": "2022-11-30T19:08:34.193Z",
  "metadata": {
    "stac": {}
  },
  "name": "iowa_order",
  "products": [
    {
      "item_ids": [
        "20200925_161029_69_2223",
        "20200925_161027_48_2223"
      ],
      "item_type": "PSScene",
      "product_bundle": "analytic_sr_udm2"
    }
  ],
  "state": "queued",
  "tools": [
    {
      "clip": {
        "aoi": {
          "coordinates": [
            [
              [-91.198465, 42.893071],
              [-91.121931, 42.893071],
              [-91.121931, 42.946205],
              [-91.198465, 42.946205],
              [-91.198465, 42.893071]
            ]
          ],
          "type": "Polygon"
        }
      }
    },
    {
      "harmonize": {
        "target_sensor": "Sentinel-2"
      }
    }
  ]
}

Integration with Python

Users can integrate their Planet data delivery with GEE with Python via Planet’s Python SDK 2.0. Using the SDK, users can generate their request, order data using the Orders API, then have it delivered directly to GEE. For an in-depth example, please see this Jupyter Notebook.

import planet
import asyncio

# The area of interest (AOI) defined as a polygon
iowa_aoi = {
    "type":
    "Polygon",
    "coordinates": [[[-91.198465, 42.893071], [-91.121931, 42.893071],
                     [-91.121931, 42.946205], [-91.198465, 42.946205],
                     [-91.198465, 42.893071]]]
}

# The item IDs we wish to order
iowa_images = ['20200925_161029_69_2223', '20200925_161027_48_2223']
# Google Earth Engine configuration
cloud_config = planet.order_request.google_earth_engine(
    project='planet-devrel-dev', collection='gee-integration-testing')
# Order delivery configuration
delivery_config = planet.order_request.delivery(cloud_config=cloud_config)
# Product description for the order request
data_products = [
    planet.order_request.product(item_ids=iowa_images,
                                 product_bundle='analytic_sr_udm2',
                                 item_type='PSScene')
]

# Build the order request
iowa_order = planet.order_request.build_request(name='iowa_order',
                                                products=data_products,
                                                delivery=delivery_config)

# Create a function to create and deliver an order
async def create_and_deliver_order(order_request, client):
    '''Create and deliver an order.

    Parameters:
        order_request: An order request
        client: An Order client object
    '''
    with planet.reporting.StateBar(state='creating') as reporter:
        # Place an order to the Orders API
        order = await client.create_order(order_request)
        reporter.update(state='created', order_id=order['id'])
        # Wait while the order is being completed
        await client.wait(order['id'],
                          callback=reporter.update_state,
                          max_attempts=0)

    # Grab the details of the orders
    order_details = await client.get_order(order_id=order['id'])

    return order_details

# Create a function to run the create_and_deliver_order function
async def main():
    async with planet.Session() as ps:
        # The Orders API client
        client = ps.client('orders')
        # Create the order and deliver it to GEE
        order_details = await create_and_deliver_order(iowa_order, client)
        return order_details

# Deliver data to GEE and return the order’s details
order_details = asyncio.run(main())

Using the Planet CLI

Lastly, we can use the Planet command line interface (CLI) to deliver data to GEE. If we want to clip or harmonize our data, first we need to create a file called tools.json containing the following information:

[
  {
    "clip": {
      "aoi": {
        "type": "Polygon",
        "coordinates": [
          [
            [-91.198465, 42.893071],
            [-91.121931, 42.893071],
            [-91.121931, 42.946205],
            [-91.198465, 42.946205],
            [-91.198465, 42.893071]
          ]
        ]
      }
    }
  },
  {
    "harmonize": {
      "target_sensor": "Sentinel-2"
    }
  }
]

Then, to order and deliver data with the Planet CLI, we query the Orders API with 3 commands:

1. Generate an order request

The request function requires that you give it an item type, product bundle, an order name, and item IDs.

$ planet orders request \
20200925_161029_69_2223,20200925_161027_48_2223 \
--item-type psscene \
--bundle analytic_sr_udm2 \
--name "iowa_order" \
--tools tools.json \
> my_order.json

Here, we are saving the order request into a file called my_order.json, which we will use to create the order.

2. Create an order

$ planet orders create my_order.json

{"_links": {"_self": "https://api.planet.com/compute/ops/orders/v2/1e4ade86-20dd-45bc-a3cf-4e6f378b5774"}, "created_on": "2022-11-30T19:08:34.193Z", "error_hints": [], "id": "1e4ade86-20dd-45bc-a3cf-4e6f378b5774", "last_message": "Preparing order", "last_modified": "2022-11-30T19:08:34.193Z", "metadata": {"stac": {}}, "name": "iowa_order", "products": [{"item_ids": ["20200925_161029_69_2223", "20200925_161027_48_2223"], "item_type": "PSScene", "product_bundle": "analytic_sr_udm2"}], "state": "queued", "tools": [{"clip": {"aoi": {"coordinates": [[[-91.198465, 42.893071], [-91.121931, 42.893071], [-91.121931, 42.946205], [-91.198465, 42.946205], [-91.198465, 42.893071]]], "type": "Polygon"}}}, {"harmonize": {"target_sensor": "Sentinel-2"}}]}

Alternatively, we can combine steps 1 and 2 into a single command by harnessing the power of STDIN with the format <request command> | <create command> -.

$ planet orders request 20200925_161029_69_2223,20200925_161027_48_2223 --item-type psscene --bundle analytic_sr_udm2 --name "iowa_order" | planet orders create -

We can find the order ID in the response from the Orders API under the field called “id”. In this case, the order ID is “1e4ade86-20dd-45bc-a3cf-4e6f378b5774”

3. Report the state of the order

$ planet orders wait 1e4ade86-20dd-45bc-a3cf-4e6f378b5774
08:25 - order 1e4ade86-20dd-45bc-a3cf-4e6f378b5774 - state: running

If successful, the states will go from queued > running > success!

Results in GEE

Now that you’ve delivered your data to GEE, here’s where you’ll find it. Head on over to your GEE console, https://code.earthengine.google.com/, click the “Assets” tab on the left hand side of the screen, and below you will find the Cloud Project you created under the tab called “CLOUD ASSETS”. Under the your Cloud Project name you will find your ImageCollection, and within you will find the images you requested.

Data in GEE

Next steps

In this example, we relied on Planet's Google Service Account for data delivery. However, this Service Account is used by many users and may be subject to delays if it reaches its maximum delivery quota. To avoid this, you can create your own Service Account, which would provide you with a dedicated delivery queue and sooner access to your data. In a future blog post, we will discuss the benefits of using a custom Service Account and provide instructions on how to integrate it with your GEE project. Chat with us about any ideas or issues at https://community.planet.com/developers-55. Follow us on Twitter @planetdevs.

The Next Release of the Planet SDK for Python is in Beta

The second version of the Planet SDK for Python is in Beta! Install it as a package. Add it to your Conda environment. Install it from the source. And let us know what you think.

Introduction

We are excited to announce the Beta release of version two of our Planet SDK for Python! The Planet SDK (Software Development Kit) for Python is a Python package developed and maintained by the Developer Relations team at Planet. It is version two of what was previously known as the Planet Python Client and works with Python 3.7+. Version two is currently in development and has long been in the works. The beta version has been released and we’re pleased to introduce you to it. This blog post provides an introduction to the Planet SDK for Python and is the first in a blog series which will cover specific aspects of the SDK in more detail.

The Planet SDK for Python is a Python package developed and maintained by the Developer Relations team at Planet. It is version two of what was previously known as the Planet Python Client and it works with Python 3.7+. Currently, it is in development and the beta version has just been released. This blog post provides an introduction to the Planet SDK for Python and is the first in a blog series which will cover specific aspects of the SDK in more detail.

The Planet SDK for Python interfaces with the Planet APIs and has two parts: a Python client library and a command-line interface (CLI). It is free and open source (maintained on github) and available for use by anyone with a Planet API key. It allows developers to automate their tasks and workflows and enables integration with existing processes using Python and more accessible API interactions via the CLI.

In addition to providing Python and CLI interfaces for API endpoints, the SDK also simplifies tasks such as managing paged results, polling for when a resource is ready, and downloading resources. Additionally, it is optimized for fast and reliable http communication. The Python library speeds up complex and bulk operations through asynchronous communication with the servers. The CLI, on the other hand, is designed to fit into complex workflows with piping. These three functionalities will be covered in depth in future posts in this series.

Improvements over version one include a first-class, full-functionality Python library and optimized, robust communication with the servers.

CLI Usage

The CLI is designed to be simple enough to allow for quick interactions with the Planet APIs while also being suitable for inclusion in complex workflows. It supports JSON/GeoJSON as inputs and outputs, which can be provided and handled as files or standard input/output. Here, we demonstrate using the CLI to create and download an order with the Orders API.

Creating an order from an order request that has been saved in a file is pretty simple:

planet orders create order_request.json

The output of this command is the description of the created order. The order id from the description can be used to wait and download the order.

Waiting for the order to be ready and downloading the order is achieved with:

planet orders wait 65df4eb0-e416-4243-a4d2-38afcf382c30 \
&& planet orders download 65df4eb0-e416-4243-a4d2-38afcf382c30

Additionally, the CLI provides support for creating order requests. These requests can be saved as a file or piped directly into the command for creating the order:

planet orders request --item-type PSScene \
  --bundle analytic_sr_udm2 \
  --name 'Two Item Order' \
  20220605_124027_64_242b,20220605_124025_34_242b \
  | planet orders create -

See the CLI documentation for more examples and a code example for how to search the Data API for a PSScene item id for use in defining the order.

Python Library Usage

Planet’s Python library provides optimized and robust communication with the Planet servers. To provide lightning-fast communication in bulk operations, this library is asynchronous. Asynchronous support was added to the Python standard library in version 3.7. Our Python library documentation provides coding examples specifically geared toward the use of the SDK asynchronously. The Python asyncio module documentation is also a great resource.

Here, we demonstrate using the Python library to create and download an order with a defined order request:

import asyncio

from planet import reporting, Session, OrdersClient

request = {
  "name": "Two Item Order",
  "products": [
    {
      "item_ids": [
        "20220605_124027_64_242b",
        "20220605_124025_34_242b"
      ],
      "item_type": "PSScene",
      "product_bundle": "analytic_sr_udm2"
    }
  ],
  "metadata": {
    "stac": {}
  }
}
# use "async def" to create an async coroutine
async def create_poll_and_download():
    async with Session() as sess:
        cl = OrdersClient(sess)

        with reporting.StateBar(state='creating') as bar:
            # create order via Orders client
            # use "await" to run a coroutine
            order = await cl.create_order(request)
            bar.update(state='created', order_id=order['id'])

            # poll...poll...poll...
            await cl.wait(order['id'], callback=bar.update_state)

        # The order completed. Yay! Now download the files
        await cl.download_order(order['id'])

# run the entire coroutine in the event loop
asyncio.run(create_poll_and_download())

The Python library also provides support for creating an order request with the order_request module:

from planet import order_request

item_ids = ["20220605_124027_64_242b", "20220605_124025_34_242b"]

products = [
    order_request.product(item_ids, "analytic_sr_udm2", "PSScene")
]

tools = [
    order_request.reproject_tool(projection="EPSG:4326", kernel="cubic")
]

request = order_request.build_request(
    "Two Item Order, reprojected", products=products, tools=tools)

See the Python library documentation for more examples and a code example for how to search the Data API for the PSScene item ids used in defining the order.

Next Steps

Check it out for yourself! Get instructions on how to install the Planet SDK for Python, learn more about the package, and get plenty of code examples at https://planet-sdk-for-python-v2.readthedocs.io. Browse the source code and track progress at https://github.com/planetlabs/planet-client-python. Check out our Resources Page of Jupyter Notebooks for help getting started. Our Jupyter Notebooks include: Get started guides for Planet API Python Client, Order API & Planet SDK, the Analysis Ready Data Tutorial Part 1: Introduction and Best Practices, and the Analysis Ready Data Tutorial Part 2. Chat with us about any ideas or issues at https://community.planet.com/developers-55. Follow us on twitter @planetdevs.