Create Sample Project
This tutorial walks through creating a sample geoprocessing project for the Federated States of Micronesia and creating a report that does overlay analysis. The planning area for this example is defined as the coastline to the outer boundary of the Exclusive Economic Zone (200 nautical miles).
This tutorial assumes:
- Your system setup is complete
- Your geoprocessing virtual environment is running (Devcontainer or WSL)
- You have VSCode open in your virtual environment with a terminal pane open
Have questions along the way? Start a discussion on Github
Initialize Geoprocessing Project
Start the project init
process, which will download the framework, and collect required project metadata.
cd /workspaces
npx @seasketch/geoprocessing@7.0.0-experimental-7x-simplify.67 init 7.0.0-experimental-7x-simplify.67
? Choose a name for your project
fsm-reports-test
? Please provide a short description of this project
Micronesia reports
? Source code repository location
[LEAVE BLANK]
? Your name
[YOUR_NAME]
? Your email
[YOUR_EMAIL]
? Organization name (optional)
Example organization
? What software license would you like to use?
BSD-3-Clause
? What AWS region would you like to deploy functions in?
us-west-1
? What languages will your reports be published in, other than English? (leave blank for none)
Chuukese
Kosraean
After pressing Enter, your project will be created and all NodeJS software dependencies installed.
Now, re-open VSCode one level deeper, in your project folder::
File -> Open Folder
Type /workspaces/fsm-reports-test/
Press Ctrl-J or Ctrl-backtick to open a new terminal
Connect Github repo and push
Before you continue, let's take a snapshot of your code now, at the starting point.
Create a remote Github repository called fsm-reports-test
. Leave it empty, do not choose to initialize with a template, README, gitignore, or LICENSE.
Then connect your local repo and make your first code commit:
git init
git add .
git commit -m "first commit"
git branch -M main
git remote add origin https://github.com/PUT_YOUR_GITHUB_ORG_OR_USERNAME_HERE/fsm-reports-test.git
git push -u origin main
You should see your files successfuly pushed to Github.
It may ask you if it can use the Github extension to sign you in using Github. It will open a browser tab and communicate with the Github website. If you are already logged in there, then it should be done quickly, otherwise it may have you login to Github.
After this point, you can continue using git commands in the terminal to stage code changes and commit them if that's what you know, or you can use VSCode's built-in git support.
You can learn more about your projects folder structure
Preprocessing
Preprocessing function are invoked by the SeaSketch platform, on a user-drawn shape, right after the user finishes drawing it. It's a specialized function that validates a drawn shape and potentially modifies it, such as to remove portions of the shape outside the planning boundary. This "clipping" of the shape is useful in that it allows a user to overdraw beyond the planning boundary and it will be clipped right to the edge of that boundary.
In the src/functions
directory you will find four preprocessing functions that come with every project, and they are further configureable to meet your needs:
validatePolygon
- verifies shape is not self-crossing, is at least 500 square meters in size, and no larger than 1 million square kilometers.
clipToLand
- clips the shape to just the portion on land, as defined by OpenStreeMap land polygons. Includes validatePolygon.
clipToOcean
- clips the shape to remove the portion on land, as defined by OpenStreetMap land polygons. Includes validatePolygon.
clipToOceanEez
- clips the shape to keep the portion within the boundary from the coastline to the outer boundary of the EEZ. Includes validatePolygon.
Testing
Each preprocessing function has its own unit test and smoke test file. For example:
- Unit:
src/functions/validatePolygon.test.ts
- Smoke:
src/functions/validatePolygonSmoke.test.ts
Unit tests ensure the preprocessor produces exact output for very specific input features and configuration, and throws errors properly.
Smoke tests are about ensuring the preprocessor behaves properly for your project location, and that its results "look right" for a variety of input features. It does this by loading example shapes from the project examples/features
directory. It then runs the preprocessing function on the examples, makes sure they produce "truthy" output, and saves them to examples/output
.
To test your preprocessing functions, we need to create example features within the extent of our Micronesian planning area. To do this, run the following script:
npx tsx scripts/genRandomPolygon.ts --outDir examples/features --filename polygon1.json --bbox "[135.31244183762126,-1.1731109652985907,165.67652822599732,13.445432925389298]"
npx tsx scripts/genRandomPolygon.ts --outDir examples/features --filename polygon2.json --bbox "[135.31244183762126,-1.1731109652985907,165.67652822599732,13.445432925389298]"
This will output an example Feature and an example FeatureCollection to examples/features
.
Now run the tests:
npm test
You can now look at the geojson output visually by opening it in QGIS or pasting it into geojson.io. This is the best way to verify the preprocessor worked as expected. You should commit the output files to your git repository so that you can track changes over time.
To learn more about preprocessing, check out the guide
Simple Report
Your new project comes with a simple report that calculates the area of a sketch or sketch collection. Let's look at the pieces that go into this report.
simpleFunction
The area calculation is done within a geoprocessing function in src/functions/simpleFunction.ts
.
Open this file and you will notice this function defines its own bespoke result payload called SimpleResults
, in this case an object with an area
number value.
export interface SimpleResults {
/** area of sketch within geography in square meters */
area: number;
}
simpleFunction
starts off with the basic signature of a geoprocessing function. It accepts a sketch
parameter that is either a single Sketch
polygon or a SketchCollection
with multiple Sketch polygons. Unless your planning project only requires users to design single sketches and not collections, your geoprocessing function must be able to handle both.
async function simpleFunction(
sketch:
| Sketch<Polygon | MultiPolygon>
| SketchCollection<Polygon | MultiPolygon>,
): Promise<SimpleResults> {
The function then performs its analysis and returns the result.
// Add analysis code
const area = turfArea(sketch);
// Custom return type
return {
area,
};
Below that, a new GeoprocessingHandler
is instantiated, with the function passed into it. Behind the scenes, this is wrapping simpleFunction in an AWS Lambda handler function. This allows the function to be deployed to AWS and invoked using an API call by a report client running in a web browser.
export default new GeoprocessingHandler(simpleFunction, {
title: "simpleFunction",
description: "Function description",
timeout: 60, // seconds
memory: 1024, // megabytes
executionMode: "async",
});
GeoprocessingHandler
requires a title
and description
, to uniquely identify the geoprocessing function that will be published by your project. It also accepts some additional parameters defining what resources the Lamda should have, and its behavior:
timeout
: how many seconds the Lambda will run before it times out in error.memory
: memory allocated to the Lambda, can go up to 10,240 MB. Number of processors increase with memory size automatically.executionMode
: determines how the report client waits for your function to finish. Sync - wait with connection open, Async - wait for web socket message. Async is the best default to not tie up your browsers network connections.
You can change all these parameter values to suit your needs, but the default values are suitable for now.
simpleFunction
is already registered as a geoprocessing function in project/geoprocessing.json
, along with blankFunction
which you will learn about later.
SimpleReport
A report client is the top-level React component for creating a report. They are located in the src/clients
directory. The report client is responsible for defining the overall report layout by rendering one or more pages of cards, and setting up language translation. The report clients role compared to its underlying page and card components (located in src/components
) is not strict, they are all just React components. Two starter report clients are provided in the src/clients
directory.
SimpleReport.tsx
- one page report client containing a SketchAttributesCard and a SimpleCard.
TabReport.tsx
- more complex multi-page report layout controlled by a tab switcher component, containing a single ViabilityPage, which contains the same SimpleCard and SketchAttributesCard.
Both these report clients are already registered in project/geoprocessing.json
. Let's focus on SimpleReport
and how it invokes your simpleFunction
.
export const SimpleReport = () => {
return (
<Translator>
<SimpleCard />
<SketchAttributesCard autoHide />
</Translator>
);
};
SimpleReport renders two report cards, SimpleCard
and SketchAttributesCard
, wrapping them in a languge Translator
(more on that in the next tutorial). SketchAttributes card is a built-in report component imported from @seasketch/geoprocessing/client-ui
. SimpleCard is a custom report component found at src/components/SimpleCard.tsx
, which we can look at now.
export const SimpleCard = () => {
const { t } = useTranslation();
const [{ isCollection }] = useSketchProperties();
const titleTrans = t("SimpleCard title", "Simple Report");
return (
<>
<ResultsCard title={titleTrans} functionName="simpleFunction">
{(data: SimpleResults) => {
return (
<>
<p>
<Trans
i18nKey="SimpleCard sketch size message"
values={{
collection: isCollection ? " collection" : "",
/** Area converted to square kilometers, rounded and formatted, with very small numbers maintained */
area: roundDecimalFormat(data.area / 1_000_000, 1, {
keepSmallValues: true,
}),
}}
components={{ 1: <b /> }}
>
{`This sketch{{collection}} is <1>{{area}}</1> square kilometers.`}
</Trans>
</p>
</>
);
}}
</ResultsCard>
</>
);
};
The first thing to notice is that SimpleCard contains a lot of boilerplate for translating report strings with the useTranslation
hook, t
function, and Trans
component. If your reports need to be multi-lingual you will need to use these, otherwise you can drop it and just render English strings.
The next thing to notice is that SimpleCard renders a ResultsCard
component. Behind the scenes ResultsCard invokes simpleFunction and passes the results to its child render function. The child render function takes an input parameter data
that has the same type as the result of simpleFunction
. Now, within the render function you have access to the function result object, fully typed.
This particular render function takes the calculated area value and makes it presentable to the user. First, the area value is converted from square meters to square kilometers, then rounded to a whole number, and formatted in a way suitable to the users locale (for US this is a comma for thousand separator, and period for decimal separator). Also notice that it renders a slightly different message depending on whether it is a single sketch or a sketch collection being reported on.
This establishes the pattern that the geoprocessing function is responsible for calculating the raw values, and defining the result type interface, so that the meaning of the values is clear. The presentation details are left to be done in the report client.
Generate Examples
With a working geoprocessing function and report client already in place, you're ready to generate example sketches for testing them. We'll use the same genRandomPolygon
script as before. But let's look closer at how we figured out the bounding box extent of the Micronesian planning area. First, use ogrinfo to inspect the Micronesia EEZ polygon data layer in your data package.
ogrinfo -so -json data/src/eez_withland_mr.fgb
Deep in its output you will see a geometryFields
property, which contains the bounding box extent of the EEZ feature. Use the jq
utility to extract this extent:
ogrinfo -so -json data/src/eez_withland_mr.fgb | jq -c .layers[0].geometryFields[0].extent
[135.31244183762126,-1.1731109652985907,165.67652822599732,13.445432925389298]
This will output an array with the extent of the EEZ. Now run the genRandomPolygon script with this extent. The following commands will create a Sketch polygon, and then a SketchCollection containing 10 Sketch polygons.
npx tsx scripts/genRandomPolygon.ts --outDir examples/sketches --filename sketch1.json --bbox "[135.31244183762126,-1.1731109652985907,165.67652822599732,13.445432925389298]" --bboxShrinkFactor 5 --sketch
npx tsx scripts/genRandomPolygon.ts --outDir examples/sketches --filename sketchCollection1.json --bbox "[135.31244183762126,-1.1731109652985907,165.67652822599732,13.445432925389298]" --bboxShrinkFactor 5 --sketch --numFeatures 10
The --bboxShrinkFactor
argument used shrinks the height and width of the given bbox by a factor of 5, and then generates random features that are within that reduced bbox. A suitable shrink factor value was discovered through trial and error. Simply visualize the resulting json file in QGIS or other software and find a value that produces polygons that are completely within the planning area polygon. (see image below).
Image: cluster of 10 random sketches (in orange) within Micronesia EEZ
Learn more about the options for genRandomPolygon
by running:
npx tsx scripts/genRandomPolygon.ts --help
Run test suite
Now that you have example features and sketches, you can test simpleFunction
. Run the test suite now:
npm test
- Using
simpleFunctionSmoke.test.ts
, simpleFunction will be run against all of the polygon Sketches inexamples/sketches
. - The results of all smokes tests are output to the
examples/output
directory. - You can inspect the output files, and see the calculated area values for each sketch input.
Commit the output files to your git repository at this time.
You can make changes to simpleFunction, then rerun tests to regenerate them at any time, and delete any that are stale and no longer needed. For advanced use, check out the testing guide.
Storybook
Storybook is used to view your reports.
npm run storybook
This will:
- Generate a story for every combination of report client registered in
project/geoprocessing.json
and sketch present inexamples/sketches
. - Load all of the smoke test output for every sketch (to load in stories instead of running geoprocessing functions)
- Start the storybook server and give you the URL.
Open the storybook URL in your browser and click through the stories.
A powerful feature of Storybook is that when you save edits to your report client or component code, storybook will refresh the browser automatically with the changes. This lets you develop your reports and debug them more quickly.
If you later add more sketch examples to the examples/sketch
directory, will need to rerun the smoke tests to generate example output, and then stop and restart your storybook to re-generate all the stories.
Learn more in the storybook guide.
First Project Build
Now that you have confirmed your function is working properly, and your report client displays properly for a variety of example sketches, you are ready to do your first build. A build
of your application packages it for deployment. Specifically it:
- Checks all the Typescript code to make sure it's valid and types are used properly.
- Transpiles all Typescript to Javascript
- Bundles UI report clients into the
.build-web
directory - Bundles geoprocessing and preprocessing functions into the
.build
directory.
To build your application run the following:
npm run build
Once your build is successful, you should stage and commit all your changes to git.
Reef Report
[Work in progress past this point]
You will be creating a simple report that measures how much reef extent is captured within a Sketch or SketchCollection.
Import Data
You will now download a data package prepared for the Federated States of Micronesia (FSM).
wget -P data/src https://github.com/user-attachments/files/17697992/FSM_MSP_Data_Example_V2.zip
unzip data/src/FSM_MSP_Data_Example_V2.zip -d data/src
rm data/src/FSM_MSP_Data_Example_V2.zip
Now import your first datasource.
npm run import:data
Reef extent - single class dataset
? Type of data?
Vector
? Enter path to src file (with filename)
data/src/reefextent.fgb
? Select layer to import
reefextent
? Choose unique datasource name (a-z, A-Z, 0-9, -, _), defaults to filename
reefextent
? Should multi-part geometries be split into single-part geometries?
Yes
? Select feature properties that you want to group metrics by
[Press enter to skip]
? Select additional feature properties to keep in final datasource
[Press enter to skip]
? These formats are automatically created: fgb. Select any additional formats you want created
[Press enter to skip]
? Will you be precalculating summary metrics for this datasource after import? (Typically yes if reporting sketch % overlap with datasource)
Yes
Add Metric Group
A metric group defines a metric to be measured, for one or more classes of data. A MetricGroup
record provides the information needed for a metric to be calculated (in a geoprocessing function) and to be displayed (in a report client).
Let's create your first metric group by opening project/metrics.json
.
Choose:
- a metricId (
coralReef
) - a type of report (
areaOverlap
) - classes you want to show in the report (
reefExtent
) and the datasource they are sourced from (reefextent
)- In this case our dataset has only one class of data, and it all comes from one datasource.
Add the following record to the end of the empty array in project/metrics.json
and save the file.
{
"metricId": "coralReef",
"type": "areaOverlap",
"classes": [
{
"classId": "reefextent",
"display": "Coral Reef",
"datasourceId": "reefextent"
}
]
}
To learn more about metric groups, visit the advanced concepts page.
Create Report
We now have everything we need to generate our first report (geoprocessing function + report component).
This command asks you to choose one of your unused metric groups, and then it:
- Creates a new geoprocessing function in
src/functions
- Creates an accompanying smoke test file
- Creates a React component that displays the result metrics in
src/components
. - Creates an accompanying storybook story generator
- Adds your new geoprocessing function to the list in
project/geoprocessing.json
so that it will be published on deploy.
These assets are all created using the blank
assets that are in your project, so it's important that you leave them in place:
- src/functions/blankFunction.ts
- src/functions/blankFunctionSmoke.test.ts
- src/components/BlankCard.tsx
- src/components/BlankCard.example-stories.tsx
To get started run the command:
npm run create:report
Benthic Habitat Report
Import Data
npm run import:data
? Type of data?
Vector
? Enter path to src file (with filename)
data/src/benthic-rock.fgb
? Select layer to import
benthic-rock
? Choose unique datasource name (a-z, A-Z, 0-9, -, _), defaults to filename benthic-rock
? Should multi-part geometries be split into single-part geometries?
Yes
? Select feature properties that you want to group metrics by
class
? Select additional feature properties to keep in final datasource
[Press Enter to skip]
? These formats are automatically created: fgb. Select any additional formats you want created
[Press Enter to skip]
? Will you be precalculating summary metrics for this datasource after import? (Typically yes if reporting sketch % overlap with datasource)
Yes
The import will proceed. Once complete you will find:
- The output file
data/dist/benthic-rock.fgb
. - An updated
project/datasources.json
new datasource recordbenthic-rock
.
If the import fails, start the import over and double check everything. It is most likely one of the following:
- You specified the wrong source file path.
- You specified the wrong layer name
Add Metric Group
Metric group for vector data source with multiple classes
Next, add a metric group for measuring sketch overlap with areas where benthic species are predicted to be present. benthic
consists of a single vector datasource with multiple habitats defined by the class
attribute. While there are many types of habitats, we want to only focus on Sand, Rubble, and Rock. To do this, you'll add multiple class records, each with a unique classId
value to match on, and a classKey
that specific which feature attribute the classId values are found.
Add the following record to the end of the array in project/metrics.json
and save the file.
{
"metricId": "benthicHabitat",
"type": "areaOverlap",
"classes": [
{
"classId": "Sand",
"classKey": "class",
"display": "Sand",
"datasourceId": "benthic"
},
{
"classId": "Rock",
"classKey": "class",
"display": "Rock",
"datasourceId": "benthic"
},
{
"classId": "Rubble",
"classKey": "class",
"display": "Rubble",
"datasourceId": "benthic"
}
]
}
Create Report
npm run create:report
? Type of report to create
Vector overlap report
? Describe what this reports geoprocessing function will calculate
Calculate sketch overlap with benthic habitats
? Choose an execution mode for the geoprocessing function for this report
Async - Better for long-running processes
? Select the metric group to report on
benthicHabitat
Now:
- Add your new component to the Viability Page
npm run test
npm run storybook
- Verify report displays properly
Octocoral Report
Import Data
Now import the following additional datasources:
Octocorals - raster with 0/1 values representing predicted presence/absence of species.
? Type of data?
Raster
? Enter path to src file (with filename)
data/src/yesson_octocorals.tif
? Choose unique datasource name (a-z, A-Z, 0-9, -, _), defaults to filename
octocorals
? Select raster band to import
1
? What type of measurement is used for this raster data?
Quantitative - values represent amounts, measurement of single thing
? Will you be precalculating summary metrics for this datasource after import? (Typically yes if reporting sketch % overlap with datasource)
Yes
Add Metric Group
Create Report
Advanced Features
Precalc Data
The precalc
command calculates spatial statistics for the portion of each of your datasources that falls within each of your project's Geographies.
Geographies are simply geographic boundaries for your project, and the default Geography for this project is the entire World.
Why do this?
One of the questions our report needs to answer is "what percentage of coral reef within the planning boundary are within my Sketch polygon?
This is calculated as:
% area of coral reef in sketch = area of coral reef within sketch / area of coral reef within planning boundary
The numerator in this equation (area of reef within sketch) is relatively inexpensive to calculate and we will do it within a geoprocessing function where we have access to the sketch. But the denominator calculation can be expensive if the data is very large or complex. Thankfully we can calculate it ahead of time.
Since your datasources and geographies already have precalc: true
set, you are ready to start:
npm run precalc:data
? Do you want to precalculate only a subset?
Yes, by datasource
Yes, by geography
Yes, by both
❯ No, just precalculate everything (may take a while)
Choose to "precalculate everything". Then press enter. The precalc process may take a while.
What's happening is that the precalc script starts a local web server on port 8001 that serves up the datasources in data/dist
.
The precalc script then gets all your project datasources with precalc: true
, and all your project geographies with precalc: true
, and then calculate area
, sum
, and count
metrics for each combination of datasource and geography.
Once complete project/precalc.json
will have been updated with the new metric values.
- To learn more advanced use, see the precalc guide.
- To learn more about use of precalculated metrics, see the report client guide.
Add Preprocessor
Add example feature to clip
[Image: before clip] [Image: after, verify clip]
Add Planning Boundary
Update default Geography
Now change the projects default geography from the world, to your new planning boundary.
- Open
project/geographies.json
. You will see an array with one geography record calledworld
. This is the default geography and can be left here. You will disable its precalc and remove it from thedefault-boundary
group, then add a new geography record for yourplanning-boundary
. - Replace the contents of the geographies file with the following and save it:
[
{
"geographyId": "world",
"datasourceId": "world",
"display": "World",
"groups": [],
"precalc": false
},
{
"geographyId": "planning-boundary",
"datasourceId": "planning-boundary",
"display": "Planning Boundary",
"groups": ["default-boundary"],
"precalc": true
}
]
What's Next
You've now completed the sample tutorial. Your next step is to choose whether you would like to:
- Setup an existing project to setup, and re-deploy it.
- Create a create a new project, deploy it and integrate with SeaSketch.