Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add queryFeatures() to send query requests to feature services #126

Merged
merged 7 commits into from
Feb 28, 2018
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
85 changes: 85 additions & 0 deletions demos/feature-service-browser/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<!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>search features!</h1>
<form id="searchFrom" class="form-inline">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be searchForm?

<div class="form-group">
<input type="text" class="form-control" id="search" placeholder="Search by tree type or condition" style="width: 400px" tabindex="0">
</div>
<button type="submit" class="btn btn-default">Search</button>
</form>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<table class="table table-striped">
<thead>
<tr>
<th>Tree ID</th>
<th>Type</th>
<th>Condition</th>
</tr>
</thead>
<tbody id="tableBody">
</tbody>
</table>
<p id="additionalRowsMessage" style="display: none" class="alert alert-warning">There are additional rows that meet your search criteria.</p>
</div>
</div>
</div>

<!-- TODO: I would not expect this to work w/o first adding @esri/arcgis-rest-request, but it does -->
<script src="node_modules/@esri/arcgis-rest-feature-service/dist/umd/arcgis-rest-feature-service.umd.js"></script>

<script>
// respond when s user fills out he search text and hits enter
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typos here

// or clicks on the Search button
const searchFrom = document.getElementById('searchFrom');
searchFrom.addEventListener('submit', function (e) {
var searchTerm = e.target.search.value;
searchTrees(searchTerm);
e.preventDefault();
});

function searchTrees(searchTerm) {
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: 'Cmn_Name LIKE \'%' + searchTerm + '%\' OR Condition LIKE \'%' + searchTerm + '%\'',
outFields: ['FID','Tree_ID','Cmn_Name','Condition'],
// limit to number of records that will show on the page
resultRecordCount: 15
}
}).then(function(response) {
refreshTable(response.features);
// show/hide additional rows message
var additionalRowsMessageDisplay = response.exceededTransferLimit ? 'block' : 'none';
document.getElementById('additionalRowsMessage').style.display = additionalRowsMessageDisplay;
});
}

function refreshTable(features) {
// clear table
var tableBody = document.getElementById('tableBody');
tableBody.innerHTML = '';
// show returned features (if any)
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('');
}
</script>
</body>
</html>
17 changes: 17 additions & 0 deletions demos/feature-service-browser/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"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-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