Skip to content

Commit

Permalink
Merge pull request #126 from Esri/feat/query-features
Browse files Browse the repository at this point in the history
add queryFeatures() to send query requests to feature services
  • Loading branch information
tomwayson authored Feb 28, 2018
2 parents 78512a3 + ad61b0a commit 2a30d22
Show file tree
Hide file tree
Showing 7 changed files with 428 additions and 5 deletions.
6 changes: 6 additions & 0 deletions demos/feature-service-browser/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Running this demo

1. Make sure you run `npm run bootstrap` in the root folder to setup the dependencies
1. `npm start`
1. Visit http://localhost:8080
1. Enter a search term and click "Search" to see results
122 changes: 122 additions & 0 deletions demos/feature-service-browser/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
</head>
<body>

<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<div class="jumbotron" >
<h1>query features!</h1>
<form id="queryForm" class="form-inline">
<div class="form-group">
<label for="resultRecordCount">Show up to</label>
<select id="resultRecordCount" class="form-control">
<option>5</option>
<option selected>10</option>
<option>25</option>
<option>50</option>
</select>
</div>
<div class="form-group">
<label for="queryField">records where</label>
<select id="queryField" class="form-control">
<option value="Cmn_Name">Type</option>
<option value="Condition">Condition</option>
</select>
</div>
<div class="form-group">
<label for="queryTerm">contains</label>
<input type="text" class="form-control" id="queryTerm" style="width: 200px" tabindex="0">
</div>
<button type="submit" class="btn btn-default">Go</button>
</form>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<table id="featureTable" class="table table-striped" style="display: none">
<thead>
<tr>
<th>Tree ID</th>
<th>Type</th>
<th>Condition</th>
</tr>
</thead>
<tbody id="tableBody">
</tbody>
</table>
<p id="suggestedTermsMessage">Try 'elm' or 'oak' for <strong>Type</strong>. Try 'fair' or 'good' for <strong>Condition</strong>.</p id="suggestedTermsMessage">
<p id="recordCountMessage"></p>
<p id="additionalRowsMessage" style="display: none" class="alert alert-warning">There are additional rows that meet your query criteria.</p>
</div>
</div>
</div>

<!-- load arcgis rest libraries -->
<script src="node_modules/@esri/arcgis-rest-request/dist/umd/arcgis-rest-request.umd.js"></script>
<script src="node_modules/@esri/arcgis-rest-feature-service/dist/umd/arcgis-rest-feature-service.umd.js"></script>

<script>
// respond when a user fills out the query form and
// either hits enter or clicks on the button
var queryForm = document.getElementById('queryForm');
queryForm.addEventListener('submit', function (e) {
// don't submit the form and reload the page (the default behavior)
e.preventDefault();
// get query params from form fields
var queryField = e.target.queryField.value;
var queryTerm = e.target.queryTerm.value;
var resultRecordCount = e.target.resultRecordCount.value;
// execute the query
queryTrees(queryField, queryTerm, resultRecordCount)
.then(function(response) {
// display the features in a table
refreshTable(response);
});
});

// perform query against the feature service and
// return a promise that will resolve with the response
function queryTrees(queryField, queryTerm, resultRecordCount) {
return arcgisRest.queryFeatures({
url: 'https://services.arcgis.com/V6ZHFr6zdgNZuVG0/arcgis/rest/services/Landscape_Trees/FeatureServer/0',
// see: https://developers.arcgis.com/rest/services-reference/query-feature-service-layer-.htm
// for all possible query parameters
params: {
// NOTE: returnGeometry is set to false by default
where: queryField + ' LIKE \'%' + queryTerm + '%\'',
outFields: ['FID','Tree_ID','Cmn_Name','Condition'],
// limit to number of records that will show on the page
resultRecordCount: resultRecordCount
}
});
}

function refreshTable(response) {
var features = response.features;
// clear table
var tableBody = document.getElementById('tableBody');
tableBody.innerHTML = '';
// show returned features (if any)
var recordCount = features.length;
var featureTableDisplay = recordCount > 0 ? 'table' : 'none';
document.getElementById('featureTable').style.display = featureTableDisplay;
var rows = features.map(function (feature) {
return '<tr><td>' + feature.attributes.Tree_ID + '<td>' + feature.attributes.Cmn_Name + '</td><td>' + feature.attributes.Condition + '</td></tr>';
});
tableBody.innerHTML = rows.join('');
// show number of returned features
document.getElementById('recordCountMessage').innerHTML = recordCount + ' record(s) returned.';
// show/hide additional messages
var suggestedTermsMessageDisplay = recordCount > 0 ? 'none' : 'block';
var additionalRowsMessageDisplay = response.exceededTransferLimit ? 'block' : 'none';
document.getElementById('suggestedTermsMessage').style.display = suggestedTermsMessageDisplay;
document.getElementById('additionalRowsMessage').style.display = additionalRowsMessageDisplay;
}
</script>
</body>
</html>
18 changes: 18 additions & 0 deletions demos/feature-service-browser/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "feature-service-browser",
"version": "1.0.3",
"private": true,
"description": "Vanilla JavaScript demo of @esri/arcgis-rest-feature-service",
"author": "",
"license": "Apache-2.0",
"dependencies": {
"@esri/arcgis-rest-request": "^1.0.3",
"@esri/arcgis-rest-feature-service": "^1.0.3"
},
"devDependencies": {
"http-server": "*"
},
"scripts": {
"start": "http-server ."
}
}
2 changes: 1 addition & 1 deletion packages/arcgis-rest-feature-service/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@esri/arcgis-rest-feature-service",
"version": "1.0.2",
"version": "1.0.3",
"description": "Feature service helpers for @esri/arcgis-rest-request",
"main": "dist/node/index.js",
"browser": "dist/umd/arcgis-rest-feature-service.umd.js",
Expand Down
132 changes: 130 additions & 2 deletions packages/arcgis-rest-feature-service/src/features.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
/* Copyright (c) 2017 Environmental Systems Research Institute, Inc.
* Apache-2.0 */
import { IFeature } from "@esri/arcgis-rest-common-types";
import {
esriGeometryType,
IFeature,
IField,
IGeometry,
ISpatialReference
} from "@esri/arcgis-rest-common-types";
import { request, IRequestOptions } from "@esri/arcgis-rest-request";

/**
Expand All @@ -15,7 +21,101 @@ export interface IFeatureRequestOptions extends IRequestOptions {
}

/**
* Get an feature by id
* @param statisticType - statistical operation to perform (count, sum, min, max, avg, stddev, var)
* @param onStatisticField - field on which to perform the statistical operation
* @param outStatisticFieldName - a field name for the returned statistic field. If outStatisticFieldName is empty or missing, the server will assign one. A valid field name can only contain alphanumeric characters and an underscore. If the outStatisticFieldName is a reserved keyword of the underlying DBMS, the operation can fail. Try specifying an alternative outStatisticFieldName.
*/
export interface IStatisticDefinition {
statisticType: "count" | "sum" | "min" | "max" | "avg" | "stddev" | "var";
onStatisticField: string;
outStatisticFieldName: string;
}

/**
* feature query parameters
*
* See https://developers.arcgis.com/rest/services-reference/query-feature-service-layer-.htm
*/
export interface IQueryFeaturesParams {
// TODO: are _any_ of these required?
where?: string;
objectIds?: [number];
geometry?: IGeometry;
geometryType?: esriGeometryType;
// NOTE: either WKID or ISpatialReference
inSR?: string | ISpatialReference;
spatialRel?:
| "esriSpatialRelIntersects"
| "esriSpatialRelContains"
| "esriSpatialRelCrosses"
| "esriSpatialRelEnvelopeIntersects"
| "esriSpatialRelIndexIntersects"
| "esriSpatialRelOverlaps"
| "esriSpatialRelTouches"
| "esriSpatialRelWithin";
relationParam?: string;
// NOTE: either time=1199145600000 or time=1199145600000, 1230768000000
time?: Date | [Date];
distance?: number;
units?:
| "esriSRUnit_Meter"
| "esriSRUnit_StatuteMile"
| "esriSRUnit_Foot"
| "esriSRUnit_Kilometer"
| "esriSRUnit_NauticalMile"
| "esriSRUnit_USNauticalMile";
outFields?: "*" | [string];
returnGeometry?: boolean;
maxAllowableOffset?: number;
geometryPrecision?: number;
// NOTE: either WKID or ISpatialReference
outSR?: string | ISpatialReference;
gdbVersion?: string;
returnDistinctValues?: boolean;
returnIdsOnly?: boolean;
returnCountOnly?: boolean;
returnExtentOnly?: boolean;
orderByFields?: string;
groupByFieldsForStatistics?: string;
outStatistics?: [IStatisticDefinition];
returnZ?: boolean;
returnM?: boolean;
multipatchOption?: "xyFootprint";
resultOffset?: number;
resultRecordCount?: number;
// TODO: IQuantizationParameters?
quantizationParameters?: any;
returnCentroid?: boolean;
resultType?: "none" | "standard" | "tile";
// TODO: is Date the right type for epoch time in milliseconds?
historicMoment?: Date;
returnTrueCurves?: false;
sqlFormat?: "none" | "standard" | "native";
returnExceededLimitFeatures?: boolean;
}

/**
* feature query request options
*
* @param url - layer service url
* @param params - query parameters to be sent to the feature service
*/
export interface IQueryFeaturesRequestOptions extends IRequestOptions {
url: string;
params?: IQueryFeaturesParams;
}

export interface IQueryFeaturesResponse {
objectIdFieldName: string;
globalIdFieldName: string;
geometryType: esriGeometryType;
spatialReference: ISpatialReference;
fields: [IField];
features: [IFeature];
}

/**
* Get a feature by id
*
* @param requestOptions - Options for the request
* @returns A Promise that will resolve with the feature.
Expand All @@ -32,3 +132,31 @@ export function getFeature(
};
return request(url, options).then((response: any) => response.feature);
}

/**
* Query features
*
* @param requestOptions - Options for the request
* @returns A Promise that will resolve with the query response.
*/
export function queryFeatures(
requestOptions: IQueryFeaturesRequestOptions
): Promise<IQueryFeaturesResponse> {
// set default query parameters
// and default to a GET request
const options: IQueryFeaturesRequestOptions = {
...{
params: {},
httpMethod: "GET"
},
...requestOptions
};
if (!options.params.where) {
options.params.where = "1=1";
}
if (!options.params.outFields) {
options.params.outFields = "*";
}
// TODO: do we need to serialize any of the array/object params?
return request(`${requestOptions.url}/query`, options);
}
20 changes: 18 additions & 2 deletions packages/arcgis-rest-feature-service/test/features.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { getFeature } from "../src/index";
import { getFeature, queryFeatures } from "../src/index";

import * as fetchMock from "fetch-mock";

import { featureResponse } from "./mocks/feature";
import { featureResponse, queryResponse } from "./mocks/feature";

describe("feature", () => {
afterEach(fetchMock.restore);
Expand All @@ -23,4 +23,20 @@ describe("feature", () => {
done();
});
});

it("should supply default query parameters", done => {
const params = {
url:
"https://services.arcgis.com/V6ZHFr6zdgNZuVG0/arcgis/rest/services/Landscape_Trees/FeatureServer/0"
};
fetchMock.once("*", queryResponse);
queryFeatures(params).then(response => {
expect(fetchMock.called()).toBeTruthy();
const [url, options]: [string, RequestInit] = fetchMock.lastCall("*");
expect(url).toEqual(`${params.url}/query?f=json&where=1%3D1&outFields=*`);
expect(options.method).toBe("GET");
// expect(response.attributes.FID).toEqual(42);
done();
});
});
});
Loading

0 comments on commit 2a30d22

Please sign in to comment.