Skip to content

Commit

Permalink
geojson inspector (#709)
Browse files Browse the repository at this point in the history
  • Loading branch information
underbluewaters authored Feb 22, 2024
1 parent 303aa88 commit dbb8e7c
Show file tree
Hide file tree
Showing 11 changed files with 565 additions and 31 deletions.
11 changes: 10 additions & 1 deletion lerna.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
{
"packages": [
"packages/*"
"packages/api",
"packages/client",
"packages/email-status-handler",
"packages/lambda-db-client",
"packages/map-screenshotter",
"packages/map-tile-cache-calculator",
"packages/mapbox-gl-esri-sources",
"packages/spatial-uploads-handler",
"packages/uploads-server",
"packages/vector-data-source"
],
"version": "independent"
}
172 changes: 172 additions & 0 deletions packages/geojson-inspector/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# Logs

logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)

report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json

# Runtime data

pids
_.pid
_.seed
\*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover

lib-cov

# Coverage directory used by tools like istanbul

coverage
\*.lcov

# nyc test coverage

.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)

.grunt

# Bower dependency directory (https://bower.io/)

bower_components

# node-waf configuration

.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)

build/Release

# Dependency directories

node_modules/
jspm_packages/

# Snowpack dependency directory (https://snowpack.dev/)

web_modules/

# TypeScript cache

\*.tsbuildinfo

# Optional npm cache directory

.npm

# Optional eslint cache

.eslintcache

# Optional stylelint cache

.stylelintcache

# Microbundle cache

.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history

.node_repl_history

# Output of 'npm pack'

\*.tgz

# Yarn Integrity file

.yarn-integrity

# dotenv environment variable files

.env
.env.development.local
.env.test.local
.env.production.local
.env.local

# parcel-bundler cache (https://parceljs.org/)

.cache
.parcel-cache

# Next.js build output

.next
out

# Nuxt.js build / generate output

.nuxt
dist

# Gatsby files

.cache/

# Comment in the public line in if your project uses Gatsby and not Next.js

# https://nextjs.org/blog/next-9-1#public-directory-support

# public

# vuepress build output

.vuepress/dist

# vuepress v2.x temp and cache directory

.temp
.cache

# Docusaurus cache and generated files

.docusaurus

# Serverless directories

.serverless/

# FuseBox cache

.fusebox/

# DynamoDB Local files

.dynamodb/

# TernJS port file

.tern-port

# Stores VSCode versions used for testing VSCode extensions

.vscode-test

# yarn v2

.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.\*

# wrangler project

.dev.vars
.wrangler/
Binary file added packages/geojson-inspector/bun.lockb
Binary file not shown.
18 changes: 18 additions & 0 deletions packages/geojson-inspector/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "geojson-inspector",
"version": "0.0.0",
"private": true,
"scripts": {
"deploy": "wrangler deploy",
"dev": "wrangler dev",
"start": "wrangler dev"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20240208.0",
"typescript": "^5.0.4",
"wrangler": "^3.0.0"
},
"dependencies": {
"@turf/bbox": "^6.5.0"
}
}
153 changes: 153 additions & 0 deletions packages/geojson-inspector/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { FailedInspectorResponse, InspectorResponse } from "./types";
import calcBBox from "@turf/bbox";
import geostats, {GeostatsLayer} from "../../spatial-uploads-handler/src/geostats";

export interface Env {}

/**
* The geojson-inspector is designed to validate GeoJSON data for use in
* SeaSketch using a direct connection as a remote data source. To support that,
* it confirms:
*
* * The url is valid and returns a 200 status code
* * The response is a valid GeoJSON object
* * The GeoJSON object is a Feature or FeatureCollection with a simple
* geometry type
* * And includes some useful context for consideration such as the bounding
* box, content length, and latency
*
*/
export default {
async fetch(
request: Request,
env: Env,
ctx: ExecutionContext
): Promise<Response> {
// validate that request is a GET request and includes a search param named
// location
if (request.method !== "GET") {
return new Response("Method not allowed", { status: 405 });
}
// create a URL, access searchparams, and get the location
const url = new URL(request.url);
const location = url.searchParams.get("location");
// if location is not provided return an error
if (!location) {
return new Response("Please provide a location", { status: 400 });
}
// fetch the geojson from the provided location, and calculate the latency
const startT = performance.now();
const response = await fetch(location, {
headers: {
accept: "application/json",
},
});
const latency = performance.now() - startT;

// if the fetch fails return an error
if (!response.ok) {
return handleFailedResponse(response, location);
} else {
let contentLength = parseInt(
response.headers.get("content-length") || "0"
);
// if size is not provided as a header, calculate it from the blob
if (contentLength === 0) {
const blob = await response.clone().blob();
contentLength = blob.size;
}
try {
const geojson: any = await response.json();
if (!("type" in geojson)) {
return new Response(
JSON.stringify({
location,
error: "Response is not a valid GeoJSON",
errorsStatus: 200,
}),
FAILED_RESPONSE_OPTS
);
} else if (
geojson.type !== "FeatureCollection" &&
geojson.type !== "Feature"
) {
return new Response(
JSON.stringify({
location,
error: "GeoJSON object must be a Feature or FeatureCollection",
errorsStatus: 200,
}),
FAILED_RESPONSE_OPTS
);
} else {
const rootType = geojson.type;
let geometryType = rootType === "Feature" ? geojson.geometry.type : geojson.features && geojson.features.length > 0 ? geojson.features[0]?.geometry?.type : "Unknown";
return new Response(
JSON.stringify({
location,
contentLength,
contentType: response.headers.get("content-type"),
cacheControl: response.headers.get("cache-control"),
latency,
rootType,
featureCount:
rootType === "FeatureCollection" ? geojson.features.length : 1,
geometryType,
bbox: calcBBox(geojson),
geostats: geostats(geojson, 'geojson'),
} as InspectorResponse),
{
status: 200,
headers: {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=120, s-maxage=120",
},
}
);
}
} catch (e) {
return new Response(
JSON.stringify({
location,
error: "Failed to parse response as JSON",
errorsStatus: 200,
}),
FAILED_RESPONSE_OPTS
);
}
}
},
};

const FAILED_RESPONSE_OPTS = {
status: 200,
headers: {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=10, s-maxage=10",
},
};

async function handleFailedResponse(response: Response, location: string) {
let failedResponse: FailedInspectorResponse;
if (response.status === 404) {
failedResponse = {
location,
error: "Server returned 404. Location not found",
errorsStatus: 404,
};
} else if (response.status >= 400 && response.status < 500) {
failedResponse = {
location,
error: "Client error. Please check the location",
errorsStatus: response.status,
};
} else {
const text = await response.text();
failedResponse = {
location,
error: "Server error. Could not retrieve data from location.\n" + text,
errorsStatus: response.status,
};
}
return new Response(JSON.stringify(failedResponse), FAILED_RESPONSE_OPTS);
}
24 changes: 24 additions & 0 deletions packages/geojson-inspector/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {GeostatsLayer} from "../../spatial-uploads-handler/src/geostats";
interface BaseInspectorResponse {
location: string;
}

export interface FailedInspectorResponse extends BaseInspectorResponse {
error: string;
errorsStatus: number;
}

export interface SuccessfulInspectorResponse extends BaseInspectorResponse {
contentLength: number;
contentType?: string;
cacheControl?: string;
latency: number;
bbox: number[];
geostats: GeostatsLayer;
}

export type InspectorResponse = FailedInspectorResponse | SuccessfulInspectorResponse;

export function isSuccessfulInspectorResponse(response: InspectorResponse): response is SuccessfulInspectorResponse {
return (response as FailedInspectorResponse).error === undefined;
}
Loading

0 comments on commit dbb8e7c

Please sign in to comment.