-
Notifications
You must be signed in to change notification settings - Fork 395
Expand file tree
/
Copy pathWebFeatureServiceSearchProviderMixin.ts
More file actions
232 lines (203 loc) · 7.68 KB
/
WebFeatureServiceSearchProviderMixin.ts
File metadata and controls
232 lines (203 loc) · 7.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
import { makeObservable, runInAction } from "mobx";
import Resource from "terriajs-cesium/Source/Core/Resource";
import URI from "urijs";
import AbstractConstructor from "../../Core/AbstractConstructor";
import zoomRectangleFromPoint from "../../Map/Vector/zoomRectangleFromPoint";
import Model from "../../Models/Definition/Model";
import SearchProviderResult from "../../Models/SearchProviders/SearchProviderResults";
import SearchResult from "../../Models/SearchProviders/SearchResult";
import xml2json from "../../ThirdParty/xml2json";
import WebFeatureServiceSearchProviderTraits from "../../Traits/SearchProviders/WebFeatureServiceSearchProviderTraits";
import LocationSearchProviderMixin from "./LocationSearchProviderMixin";
function WebFeatureServiceSearchProviderMixin<
T extends AbstractConstructor<Model<WebFeatureServiceSearchProviderTraits>>
>(Base: T) {
abstract class WebFeatureServiceSearchProviderMixin extends LocationSearchProviderMixin(
Base
) {
constructor(...args: any[]) {
super(...args);
makeObservable(this);
}
protected abstract featureToSearchResultFunction: (
feature: any
) => SearchResult;
protected abstract transformSearchText:
| ((searchText: string) => string)
| undefined;
protected abstract searchResultFilterFunction:
| ((feature: any) => boolean)
| undefined;
protected abstract searchResultScoreFunction:
| ((feature: any, searchText: string) => number)
| undefined;
cancelRequest?: () => void;
private _waitingForResults: boolean = false;
getXml(url: string): Promise<XMLDocument> {
const resource = new Resource({ url });
this._waitingForResults = true;
const xmlPromise = resource.fetchXML()!;
this.cancelRequest = resource.request.cancelFunction;
return xmlPromise.finally(() => {
this._waitingForResults = false;
});
}
protected doSearch(
searchText: string,
results: SearchProviderResult
): Promise<void> {
results.results.length = 0;
results.message = undefined;
if (this._waitingForResults) {
// There's been a new search! Cancel the previous one.
if (this.cancelRequest !== undefined) {
this.cancelRequest();
this.cancelRequest = undefined;
}
this._waitingForResults = false;
}
const originalSearchText = searchText;
searchText = searchText.trim();
if (this.transformSearchText !== undefined) {
searchText = this.transformSearchText(searchText);
}
if (searchText.length < 2) {
return Promise.resolve();
}
// Support for matchCase="false" is patchy, but we try anyway
const filter = `<ogc:Filter><ogc:PropertyIsLike wildCard="*" matchCase="false">
<ogc:ValueReference>${this.searchPropertyName}</ogc:ValueReference>
<ogc:Literal>*${searchText}*</ogc:Literal></ogc:PropertyIsLike></ogc:Filter>`;
const _wfsServiceUrl = new URI(this.url);
_wfsServiceUrl.setSearch({
service: "WFS",
request: "GetFeature",
typeName: this.searchPropertyTypeName,
version: "1.1.0",
srsName: "urn:ogc:def:crs:EPSG::4326", // srsName must be formatted like this for correct lat/long order >:(
filter: filter
});
return this.getXml(_wfsServiceUrl.toString())
.then((xml: any) => {
const json: any = xml2json(xml);
let features: any[];
if (json === undefined) {
results.message = {
content: "translate#viewModels.searchErrorOccurred"
};
return;
}
if (json.member !== undefined) {
features = json.member;
} else if (json.featureMember !== undefined) {
features = json.featureMember;
} else {
results.message = {
content: "translate#viewModels.searchNoPlaceNames"
};
return;
}
// if there's only one feature, make it an array
if (!Array.isArray(features)) {
features = [features];
}
const resultSet = new Set();
runInAction(() => {
if (this.searchResultFilterFunction !== undefined) {
features = features.filter(this.searchResultFilterFunction);
}
if (features.length === 0) {
results.message = {
content: "translate#viewModels.searchNoPlaceNames"
};
return;
}
if (this.searchResultScoreFunction !== undefined) {
features = features.sort(
(featureA, featureB) =>
this.searchResultScoreFunction!(
featureB,
originalSearchText
) -
this.searchResultScoreFunction!(featureA, originalSearchText)
);
}
let searchResults = features
.map(this.featureToSearchResultFunction)
.map((result) => {
result.clickAction = createZoomToFunction(
this,
result.location
);
return result;
});
// If we don't have a scoring function, sort the search results now
// We can't do this earlier because we don't know what the schema of the unprocessed feature looks like
if (this.searchResultScoreFunction === undefined) {
// Put shorter results first
// They have a larger percentage of letters that the user actually typed in them
searchResults = searchResults.sort(
(featureA, featureB) =>
featureA.name.length - featureB.name.length
);
}
// Remove results that have the same name and are close to each other
searchResults = searchResults.filter((result) => {
const hash = `${result.name},${result.location?.latitude.toFixed(
1
)},${result.location?.longitude.toFixed(1)}`;
if (resultSet.has(hash)) {
return false;
}
resultSet.add(hash);
return true;
});
// append new results to all results
results.results.push(...searchResults);
});
})
.catch((_e) => {
if (results.isCanceled) {
// A new search has superseded this one, so ignore the result.
return;
}
results.message = {
content: "translate#viewModels.searchErrorOccurred"
};
});
}
get isWebFeatureServiceSearchProviderMixin() {
return true;
}
}
return WebFeatureServiceSearchProviderMixin;
}
namespace WebFeatureServiceSearchProviderMixin {
export interface Instance
extends InstanceType<
ReturnType<typeof WebFeatureServiceSearchProviderMixin>
> {}
export function isMixedInto(model: any): model is Instance {
return model && model.isWebFeatureServiceSearchProviderMixin;
}
}
export default WebFeatureServiceSearchProviderMixin;
function createZoomToFunction(
model: WebFeatureServiceSearchProviderMixin.Instance,
location: any
) {
// Server does not return information of a bounding box, just a location.
// bboxSize is used to expand a point
const bboxSize = 0.2;
const rectangle = zoomRectangleFromPoint(
location.latitude,
location.longitude,
bboxSize
);
const flightDurationSeconds: number =
model.flightDurationSeconds ||
model.terria.searchBarModel.flightDurationSeconds;
return function () {
model.terria.currentViewer.zoomTo(rectangle, flightDurationSeconds);
};
}