Add existing to tracked
This commit is contained in:
+80
@@ -0,0 +1,80 @@
|
||||
.cesium-viewer-geocoderContainer .cesium-geocoder-input {
|
||||
border: solid 1px #444;
|
||||
background-color: rgba(40, 40, 40, 0.7);
|
||||
color: white;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: 0;
|
||||
height: 32px;
|
||||
margin: 0;
|
||||
padding: 0 32px 0 0;
|
||||
border-radius: 0;
|
||||
box-sizing: border-box;
|
||||
transition:
|
||||
width ease-in-out 0.25s,
|
||||
background-color 0.2s ease-in-out;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.cesium-viewer-geocoderContainer:hover .cesium-geocoder-input {
|
||||
border-color: #aef;
|
||||
box-shadow: 0 0 8px #fff;
|
||||
}
|
||||
|
||||
.cesium-viewer-geocoderContainer .cesium-geocoder-input:focus {
|
||||
border-color: #ea4;
|
||||
background-color: rgba(15, 15, 15, 0.9);
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.cesium-viewer-geocoderContainer:hover .cesium-geocoder-input,
|
||||
.cesium-viewer-geocoderContainer .cesium-geocoder-input:focus,
|
||||
.cesium-viewer-geocoderContainer .cesium-geocoder-input-wide {
|
||||
padding-left: 4px;
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.cesium-viewer-geocoderContainer .search-results {
|
||||
position: absolute;
|
||||
background-color: #000;
|
||||
color: #eee;
|
||||
overflow-y: auto;
|
||||
opacity: 0.8;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cesium-viewer-geocoderContainer .search-results ul {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.cesium-viewer-geocoderContainer .search-results ul li {
|
||||
font-size: 14px;
|
||||
padding: 3px 10px;
|
||||
}
|
||||
.cesium-viewer-geocoderContainer .search-results ul li:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cesium-viewer-geocoderContainer .search-results ul li.active {
|
||||
background: #48b;
|
||||
}
|
||||
|
||||
.cesium-geocoder-searchButton {
|
||||
background-color: #303336;
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
width: 32px;
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
height: 30px;
|
||||
vertical-align: middle;
|
||||
fill: #edffff;
|
||||
}
|
||||
|
||||
.cesium-geocoder-searchButton:hover {
|
||||
background-color: #48b;
|
||||
}
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
import {
|
||||
defined,
|
||||
destroyObject,
|
||||
DeveloperError,
|
||||
FeatureDetection,
|
||||
getElement,
|
||||
} from "@cesium/engine";
|
||||
import knockout from "../ThirdParty/knockout.js";
|
||||
import GeocoderViewModel from "./GeocoderViewModel.js";
|
||||
|
||||
const startSearchPath =
|
||||
"M29.772,26.433l-7.126-7.126c0.96-1.583,1.523-3.435,1.524-5.421C24.169,8.093,19.478,3.401,13.688,3.399C7.897,3.401,3.204,8.093,3.204,13.885c0,5.789,4.693,10.481,10.484,10.481c1.987,0,3.839-0.563,5.422-1.523l7.128,7.127L29.772,26.433zM7.203,13.885c0.006-3.582,2.903-6.478,6.484-6.486c3.579,0.008,6.478,2.904,6.484,6.486c-0.007,3.58-2.905,6.476-6.484,6.484C10.106,20.361,7.209,17.465,7.203,13.885z";
|
||||
const stopSearchPath =
|
||||
"M24.778,21.419 19.276,15.917 24.777,10.415 21.949,7.585 16.447,13.087 10.945,7.585 8.117,10.415 13.618,15.917 8.116,21.419 10.946,24.248 16.447,18.746 21.948,24.248z";
|
||||
|
||||
/**
|
||||
* A widget for finding addresses and landmarks, and flying the camera to them. Geocoding is
|
||||
* performed using {@link https://cesium.com/cesium-ion/|Cesium ion}.
|
||||
*
|
||||
* @alias Geocoder
|
||||
* @constructor
|
||||
*
|
||||
* @param {object} options Object with the following properties:
|
||||
* @param {Element|string} options.container The DOM element or ID that will contain the widget.
|
||||
* @param {Scene} options.scene The Scene instance to use.
|
||||
* @param {GeocoderService[]} [options.geocoderServices] The geocoder services to be used
|
||||
* @param {boolean} [options.autoComplete = true] True if the geocoder should query as the user types to autocomplete
|
||||
* @param {number} [options.flightDuration=1.5] The duration of the camera flight to an entered location, in seconds.
|
||||
* @param {Geocoder.DestinationFoundFunction} [options.destinationFound=GeocoderViewModel.flyToDestination] A callback function that is called after a successful geocode. If not supplied, the default behavior is to fly the camera to the result destination.
|
||||
*/
|
||||
function Geocoder(options) {
|
||||
//>>includeStart('debug', pragmas.debug);
|
||||
if (!defined(options) || !defined(options.container)) {
|
||||
throw new DeveloperError("options.container is required.");
|
||||
}
|
||||
if (!defined(options.scene)) {
|
||||
throw new DeveloperError("options.scene is required.");
|
||||
}
|
||||
//>>includeEnd('debug');
|
||||
|
||||
const container = getElement(options.container);
|
||||
const viewModel = new GeocoderViewModel(options);
|
||||
|
||||
viewModel._startSearchPath = startSearchPath;
|
||||
viewModel._stopSearchPath = stopSearchPath;
|
||||
|
||||
const form = document.createElement("form");
|
||||
form.setAttribute("data-bind", "submit: search");
|
||||
|
||||
const textBox = document.createElement("input");
|
||||
textBox.type = "search";
|
||||
textBox.className = "cesium-geocoder-input";
|
||||
textBox.setAttribute("placeholder", "Enter an address or landmark...");
|
||||
textBox.setAttribute(
|
||||
"data-bind",
|
||||
'\
|
||||
textInput: searchText,\
|
||||
disable: isSearchInProgress,\
|
||||
event: { keyup: handleKeyUp, keydown: handleKeyDown, mouseover: deselectSuggestion },\
|
||||
css: { "cesium-geocoder-input-wide" : keepExpanded || searchText.length > 0 },\
|
||||
hasFocus: _focusTextbox',
|
||||
);
|
||||
|
||||
this._onTextBoxFocus = function () {
|
||||
// as of 2016-10-19, setTimeout is required to ensure that the
|
||||
// text is focused on Safari 10
|
||||
setTimeout(function () {
|
||||
textBox.select();
|
||||
}, 0);
|
||||
};
|
||||
|
||||
textBox.addEventListener("focus", this._onTextBoxFocus, false);
|
||||
form.appendChild(textBox);
|
||||
this._textBox = textBox;
|
||||
|
||||
const searchButton = document.createElement("span");
|
||||
searchButton.className = "cesium-geocoder-searchButton";
|
||||
searchButton.setAttribute(
|
||||
"data-bind",
|
||||
"\
|
||||
click: search,\
|
||||
cesiumSvgPath: { path: isSearchInProgress ? _stopSearchPath : _startSearchPath, width: 32, height: 32 }",
|
||||
);
|
||||
form.appendChild(searchButton);
|
||||
|
||||
container.appendChild(form);
|
||||
|
||||
const searchSuggestionsContainer = document.createElement("div");
|
||||
searchSuggestionsContainer.className = "search-results";
|
||||
searchSuggestionsContainer.setAttribute(
|
||||
"data-bind",
|
||||
"visible: _suggestionsVisible",
|
||||
);
|
||||
|
||||
const suggestionsList = document.createElement("ul");
|
||||
suggestionsList.setAttribute("data-bind", "foreach: _suggestions");
|
||||
const suggestions = document.createElement("li");
|
||||
suggestionsList.appendChild(suggestions);
|
||||
suggestions.setAttribute(
|
||||
"data-bind",
|
||||
"text: $data.displayName, \
|
||||
click: $parent.activateSuggestion, \
|
||||
event: { mouseover: $parent.handleMouseover}, \
|
||||
css: { active: $data === $parent._selectedSuggestion }",
|
||||
);
|
||||
|
||||
searchSuggestionsContainer.appendChild(suggestionsList);
|
||||
container.appendChild(searchSuggestionsContainer);
|
||||
|
||||
knockout.applyBindings(viewModel, form);
|
||||
knockout.applyBindings(viewModel, searchSuggestionsContainer);
|
||||
|
||||
this._container = container;
|
||||
this._searchSuggestionsContainer = searchSuggestionsContainer;
|
||||
this._viewModel = viewModel;
|
||||
this._form = form;
|
||||
|
||||
this._onInputBegin = function (e) {
|
||||
// e.target will not be correct if we are inside of the Shadow DOM
|
||||
// and contains will always fail. To retrieve the correct target,
|
||||
// we need to access the first element of the composedPath.
|
||||
// This allows us to use shadow DOM if it exists and fall
|
||||
// back to legacy behavior if its not being used.
|
||||
|
||||
let target = e.target;
|
||||
if (typeof e.composedPath === "function") {
|
||||
target = e.composedPath()[0];
|
||||
}
|
||||
|
||||
if (!container.contains(target)) {
|
||||
viewModel._focusTextbox = false;
|
||||
viewModel.hideSuggestions();
|
||||
}
|
||||
};
|
||||
|
||||
this._onInputEnd = function (e) {
|
||||
viewModel._focusTextbox = true;
|
||||
viewModel.showSuggestions();
|
||||
};
|
||||
|
||||
//We subscribe to both begin and end events in order to give the text box
|
||||
//focus no matter where on the widget is clicked.
|
||||
|
||||
if (FeatureDetection.supportsPointerEvents()) {
|
||||
document.addEventListener("pointerdown", this._onInputBegin, true);
|
||||
container.addEventListener("pointerup", this._onInputEnd, true);
|
||||
container.addEventListener("pointercancel", this._onInputEnd, true);
|
||||
} else {
|
||||
document.addEventListener("mousedown", this._onInputBegin, true);
|
||||
container.addEventListener("mouseup", this._onInputEnd, true);
|
||||
document.addEventListener("touchstart", this._onInputBegin, true);
|
||||
container.addEventListener("touchend", this._onInputEnd, true);
|
||||
container.addEventListener("touchcancel", this._onInputEnd, true);
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperties(Geocoder.prototype, {
|
||||
/**
|
||||
* Gets the parent container.
|
||||
* @memberof Geocoder.prototype
|
||||
*
|
||||
* @type {Element}
|
||||
*/
|
||||
container: {
|
||||
get: function () {
|
||||
return this._container;
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the parent container.
|
||||
* @memberof Geocoder.prototype
|
||||
*
|
||||
* @type {Element}
|
||||
*/
|
||||
searchSuggestionsContainer: {
|
||||
get: function () {
|
||||
return this._searchSuggestionsContainer;
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the view model.
|
||||
* @memberof Geocoder.prototype
|
||||
*
|
||||
* @type {GeocoderViewModel}
|
||||
*/
|
||||
viewModel: {
|
||||
get: function () {
|
||||
return this._viewModel;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* @returns {boolean} true if the object has been destroyed, false otherwise.
|
||||
*/
|
||||
Geocoder.prototype.isDestroyed = function () {
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Destroys the widget. Should be called if permanently
|
||||
* removing the widget from layout.
|
||||
*/
|
||||
Geocoder.prototype.destroy = function () {
|
||||
const container = this._container;
|
||||
if (FeatureDetection.supportsPointerEvents()) {
|
||||
document.removeEventListener("pointerdown", this._onInputBegin, true);
|
||||
container.removeEventListener("pointerup", this._onInputEnd, true);
|
||||
} else {
|
||||
document.removeEventListener("mousedown", this._onInputBegin, true);
|
||||
container.removeEventListener("mouseup", this._onInputEnd, true);
|
||||
document.removeEventListener("touchstart", this._onInputBegin, true);
|
||||
container.removeEventListener("touchend", this._onInputEnd, true);
|
||||
}
|
||||
this._viewModel.destroy();
|
||||
knockout.cleanNode(this._form);
|
||||
knockout.cleanNode(this._searchSuggestionsContainer);
|
||||
container.removeChild(this._form);
|
||||
container.removeChild(this._searchSuggestionsContainer);
|
||||
this._textBox.removeEventListener("focus", this._onTextBoxFocus, false);
|
||||
|
||||
return destroyObject(this);
|
||||
};
|
||||
|
||||
/**
|
||||
* A function that handles the result of a successful geocode.
|
||||
* @callback Geocoder.DestinationFoundFunction
|
||||
* @param {GeocoderViewModel} viewModel The view model.
|
||||
* @param {Cartesian3|Rectangle} destination The destination result of the geocode.
|
||||
*/
|
||||
export default Geocoder;
|
||||
+607
@@ -0,0 +1,607 @@
|
||||
import {
|
||||
computeFlyToLocationForRectangle,
|
||||
defined,
|
||||
DeveloperError,
|
||||
destroyObject,
|
||||
Event,
|
||||
GeocoderService,
|
||||
GeocodeType,
|
||||
getElement,
|
||||
IonGeocoderService,
|
||||
Math as CesiumMath,
|
||||
Matrix4,
|
||||
Rectangle,
|
||||
sampleTerrainMostDetailed,
|
||||
} from "@cesium/engine";
|
||||
import knockout from "../ThirdParty/knockout.js";
|
||||
import createCommand from "../createCommand.js";
|
||||
|
||||
// The height we use if geocoding to a specific point instead of an rectangle.
|
||||
const DEFAULT_HEIGHT = 1000;
|
||||
|
||||
/**
|
||||
* The view model for the {@link Geocoder} widget.
|
||||
* @alias GeocoderViewModel
|
||||
* @constructor
|
||||
*
|
||||
* @param {object} options Object with the following properties:
|
||||
* @param {Scene} options.scene The Scene instance to use.
|
||||
* @param {GeocoderService[]} [options.geocoderServices] Geocoder services to use for geocoding queries.
|
||||
* If more than one are supplied, suggestions will be gathered for the geocoders that support it,
|
||||
* and if no suggestion is selected the result from the first geocoder service wil be used.
|
||||
* @param {number} [options.flightDuration] The duration of the camera flight to an entered location, in seconds.
|
||||
* @param {Geocoder.DestinationFoundFunction} [options.destinationFound=GeocoderViewModel.flyToDestination] A callback function that is called after a successful geocode. If not supplied, the default behavior is to fly the camera to the result destination.
|
||||
*/
|
||||
function GeocoderViewModel(options) {
|
||||
//>>includeStart('debug', pragmas.debug);
|
||||
if (!defined(options) || !defined(options.scene)) {
|
||||
throw new DeveloperError("options.scene is required.");
|
||||
}
|
||||
//>>includeEnd('debug');
|
||||
|
||||
if (defined(options.geocoderServices)) {
|
||||
this._geocoderServices = options.geocoderServices;
|
||||
} else {
|
||||
this._geocoderServices = [new IonGeocoderService({ scene: options.scene })];
|
||||
}
|
||||
|
||||
this._viewContainer = options.container;
|
||||
this._scene = options.scene;
|
||||
this._flightDuration = options.flightDuration;
|
||||
this._searchText = "";
|
||||
this._isSearchInProgress = false;
|
||||
this._wasGeocodeCancelled = false;
|
||||
this._previousCredits = [];
|
||||
this._complete = new Event();
|
||||
this._suggestions = [];
|
||||
this._selectedSuggestion = undefined;
|
||||
this._showSuggestions = true;
|
||||
|
||||
this._handleArrowDown = handleArrowDown;
|
||||
this._handleArrowUp = handleArrowUp;
|
||||
|
||||
const that = this;
|
||||
|
||||
this._suggestionsVisible = knockout.pureComputed(function () {
|
||||
const suggestions = knockout.getObservable(that, "_suggestions");
|
||||
const suggestionsNotEmpty = suggestions().length > 0;
|
||||
const showSuggestions = knockout.getObservable(that, "_showSuggestions")();
|
||||
return suggestionsNotEmpty && showSuggestions;
|
||||
});
|
||||
|
||||
this._searchCommand = createCommand(function (geocodeType) {
|
||||
geocodeType = geocodeType ?? GeocodeType.SEARCH;
|
||||
that._focusTextbox = false;
|
||||
if (defined(that._selectedSuggestion)) {
|
||||
that.activateSuggestion(that._selectedSuggestion);
|
||||
return false;
|
||||
}
|
||||
that.hideSuggestions();
|
||||
if (that.isSearchInProgress) {
|
||||
cancelGeocode(that);
|
||||
} else {
|
||||
return geocode(that, that._geocoderServices, geocodeType);
|
||||
}
|
||||
});
|
||||
|
||||
this.deselectSuggestion = function () {
|
||||
that._selectedSuggestion = undefined;
|
||||
};
|
||||
|
||||
this.handleKeyDown = function (data, event) {
|
||||
const downKey =
|
||||
event.key === "ArrowDown" || event.key === "Down" || event.keyCode === 40;
|
||||
const upKey =
|
||||
event.key === "ArrowUp" || event.key === "Up" || event.keyCode === 38;
|
||||
if (downKey || upKey) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
this.handleKeyUp = function (data, event) {
|
||||
const downKey =
|
||||
event.key === "ArrowDown" || event.key === "Down" || event.keyCode === 40;
|
||||
const upKey =
|
||||
event.key === "ArrowUp" || event.key === "Up" || event.keyCode === 38;
|
||||
const enterKey = event.key === "Enter" || event.keyCode === 13;
|
||||
if (upKey) {
|
||||
handleArrowUp(that);
|
||||
} else if (downKey) {
|
||||
handleArrowDown(that);
|
||||
} else if (enterKey) {
|
||||
that._searchCommand();
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
this.activateSuggestion = function (data) {
|
||||
that.hideSuggestions();
|
||||
that._searchText = data.displayName;
|
||||
const destination = data.destination;
|
||||
clearSuggestions(that);
|
||||
that.destinationFound(that, destination);
|
||||
};
|
||||
|
||||
this.hideSuggestions = function () {
|
||||
that._showSuggestions = false;
|
||||
that._selectedSuggestion = undefined;
|
||||
};
|
||||
|
||||
this.showSuggestions = function () {
|
||||
that._showSuggestions = true;
|
||||
};
|
||||
|
||||
this.handleMouseover = function (data, event) {
|
||||
if (data !== that._selectedSuggestion) {
|
||||
that._selectedSuggestion = data;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets or sets a value indicating if this instance should always show its text input field.
|
||||
*
|
||||
* @type {boolean}
|
||||
* @default false
|
||||
*/
|
||||
this.keepExpanded = false;
|
||||
|
||||
/**
|
||||
* True if the geocoder should query as the user types to autocomplete
|
||||
* @type {boolean}
|
||||
* @default true
|
||||
*/
|
||||
this.autoComplete = options.autocomplete ?? true;
|
||||
|
||||
/**
|
||||
* Gets and sets the command called when a geocode destination is found
|
||||
* @type {Geocoder.DestinationFoundFunction}
|
||||
*/
|
||||
this.destinationFound =
|
||||
options.destinationFound ?? GeocoderViewModel.flyToDestination;
|
||||
|
||||
this._focusTextbox = false;
|
||||
|
||||
knockout.track(this, [
|
||||
"_searchText",
|
||||
"_isSearchInProgress",
|
||||
"keepExpanded",
|
||||
"_suggestions",
|
||||
"_selectedSuggestion",
|
||||
"_showSuggestions",
|
||||
"_focusTextbox",
|
||||
]);
|
||||
|
||||
const searchTextObservable = knockout.getObservable(this, "_searchText");
|
||||
searchTextObservable.extend({ rateLimit: { timeout: 500 } });
|
||||
this._suggestionSubscription = searchTextObservable.subscribe(function () {
|
||||
GeocoderViewModel._updateSearchSuggestions(that);
|
||||
});
|
||||
/**
|
||||
* Gets a value indicating whether a search is currently in progress. This property is observable.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.isSearchInProgress = undefined;
|
||||
knockout.defineProperty(this, "isSearchInProgress", {
|
||||
get: function () {
|
||||
return this._isSearchInProgress;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Gets or sets the text to search for. The text can be an address, or longitude, latitude,
|
||||
* and optional height, where longitude and latitude are in degrees and height is in meters.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
this.searchText = undefined;
|
||||
knockout.defineProperty(this, "searchText", {
|
||||
get: function () {
|
||||
if (this.isSearchInProgress) {
|
||||
return "Searching...";
|
||||
}
|
||||
|
||||
return this._searchText;
|
||||
},
|
||||
set: function (value) {
|
||||
//>>includeStart('debug', pragmas.debug);
|
||||
if (typeof value !== "string") {
|
||||
throw new DeveloperError("value must be a valid string.");
|
||||
}
|
||||
//>>includeEnd('debug');
|
||||
this._searchText = value;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Gets or sets the the duration of the camera flight in seconds.
|
||||
* A value of zero causes the camera to instantly switch to the geocoding location.
|
||||
* The duration will be computed based on the distance when undefined.
|
||||
*
|
||||
* @type {number|undefined}
|
||||
* @default undefined
|
||||
*/
|
||||
this.flightDuration = undefined;
|
||||
knockout.defineProperty(this, "flightDuration", {
|
||||
get: function () {
|
||||
return this._flightDuration;
|
||||
},
|
||||
set: function (value) {
|
||||
//>>includeStart('debug', pragmas.debug);
|
||||
if (defined(value) && value < 0) {
|
||||
throw new DeveloperError("value must be positive.");
|
||||
}
|
||||
//>>includeEnd('debug');
|
||||
|
||||
this._flightDuration = value;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Object.defineProperties(GeocoderViewModel.prototype, {
|
||||
/**
|
||||
* Gets the event triggered on flight completion.
|
||||
* @memberof GeocoderViewModel.prototype
|
||||
*
|
||||
* @type {Event}
|
||||
*/
|
||||
complete: {
|
||||
get: function () {
|
||||
return this._complete;
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the scene to control.
|
||||
* @memberof GeocoderViewModel.prototype
|
||||
*
|
||||
* @type {Scene}
|
||||
*/
|
||||
scene: {
|
||||
get: function () {
|
||||
return this._scene;
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the Command that is executed when the button is clicked.
|
||||
* @memberof GeocoderViewModel.prototype
|
||||
*
|
||||
* @type {Command}
|
||||
*/
|
||||
search: {
|
||||
get: function () {
|
||||
return this._searchCommand;
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the currently selected geocoder search suggestion
|
||||
* @memberof GeocoderViewModel.prototype
|
||||
*
|
||||
* @type {object}
|
||||
*/
|
||||
selectedSuggestion: {
|
||||
get: function () {
|
||||
return this._selectedSuggestion;
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the list of geocoder search suggestions
|
||||
* @memberof GeocoderViewModel.prototype
|
||||
*
|
||||
* @type {object[]}
|
||||
*/
|
||||
suggestions: {
|
||||
get: function () {
|
||||
return this._suggestions;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
function handleArrowUp(viewModel) {
|
||||
if (viewModel._suggestions.length === 0) {
|
||||
return;
|
||||
}
|
||||
const currentIndex = viewModel._suggestions.indexOf(
|
||||
viewModel._selectedSuggestion,
|
||||
);
|
||||
if (currentIndex === -1 || currentIndex === 0) {
|
||||
viewModel._selectedSuggestion = undefined;
|
||||
return;
|
||||
}
|
||||
const next = currentIndex - 1;
|
||||
viewModel._selectedSuggestion = viewModel._suggestions[next];
|
||||
GeocoderViewModel._adjustSuggestionsScroll(viewModel, next);
|
||||
}
|
||||
|
||||
function handleArrowDown(viewModel) {
|
||||
if (viewModel._suggestions.length === 0) {
|
||||
return;
|
||||
}
|
||||
const numberOfSuggestions = viewModel._suggestions.length;
|
||||
const currentIndex = viewModel._suggestions.indexOf(
|
||||
viewModel._selectedSuggestion,
|
||||
);
|
||||
const next = (currentIndex + 1) % numberOfSuggestions;
|
||||
viewModel._selectedSuggestion = viewModel._suggestions[next];
|
||||
|
||||
GeocoderViewModel._adjustSuggestionsScroll(viewModel, next);
|
||||
}
|
||||
|
||||
function computeFlyToLocationForCartographic(cartographic, terrainProvider) {
|
||||
const availability = defined(terrainProvider)
|
||||
? terrainProvider.availability
|
||||
: undefined;
|
||||
|
||||
if (!defined(availability)) {
|
||||
cartographic.height += DEFAULT_HEIGHT;
|
||||
return Promise.resolve(cartographic);
|
||||
}
|
||||
|
||||
return sampleTerrainMostDetailed(terrainProvider, [cartographic]).then(
|
||||
function (positionOnTerrain) {
|
||||
cartographic = positionOnTerrain[0];
|
||||
cartographic.height += DEFAULT_HEIGHT;
|
||||
return cartographic;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function flyToDestination(viewModel, destination) {
|
||||
const scene = viewModel._scene;
|
||||
const ellipsoid = scene.ellipsoid;
|
||||
|
||||
const camera = scene.camera;
|
||||
const terrainProvider = scene.terrainProvider;
|
||||
let finalDestination = destination;
|
||||
|
||||
let promise;
|
||||
if (destination instanceof Rectangle) {
|
||||
// Some geocoders return a Rectangle of zero width/height, treat it like a point instead.
|
||||
if (
|
||||
CesiumMath.equalsEpsilon(
|
||||
destination.south,
|
||||
destination.north,
|
||||
CesiumMath.EPSILON7,
|
||||
) &&
|
||||
CesiumMath.equalsEpsilon(
|
||||
destination.east,
|
||||
destination.west,
|
||||
CesiumMath.EPSILON7,
|
||||
)
|
||||
) {
|
||||
// destination is now a Cartographic
|
||||
destination = Rectangle.center(destination);
|
||||
} else {
|
||||
promise = computeFlyToLocationForRectangle(destination, scene);
|
||||
}
|
||||
} else {
|
||||
// destination is a Cartesian3
|
||||
destination = ellipsoid.cartesianToCartographic(destination);
|
||||
}
|
||||
|
||||
if (!defined(promise)) {
|
||||
promise = computeFlyToLocationForCartographic(destination, terrainProvider);
|
||||
}
|
||||
|
||||
return promise
|
||||
.then(function (result) {
|
||||
finalDestination = ellipsoid.cartographicToCartesian(result);
|
||||
})
|
||||
.finally(function () {
|
||||
// Whether terrain querying succeeded or not, fly to the destination.
|
||||
camera.flyTo({
|
||||
destination: finalDestination,
|
||||
complete: function () {
|
||||
viewModel._complete.raiseEvent();
|
||||
},
|
||||
duration: viewModel._flightDuration,
|
||||
endTransform: Matrix4.IDENTITY,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function attemptGeocode(geocoderService, query, geocodeType) {
|
||||
try {
|
||||
const result = await geocoderService.geocode(query, geocodeType);
|
||||
return {
|
||||
state: "fulfilled",
|
||||
value: result,
|
||||
credits: geocoderService.credit,
|
||||
};
|
||||
} catch (error) {
|
||||
return { state: "rejected", reason: error };
|
||||
}
|
||||
}
|
||||
|
||||
async function geocode(viewModel, geocoderServices, geocodeType) {
|
||||
const query = viewModel._searchText;
|
||||
|
||||
if (hasOnlyWhitespace(query)) {
|
||||
viewModel.showSuggestions();
|
||||
return;
|
||||
}
|
||||
|
||||
viewModel._isSearchInProgress = true;
|
||||
viewModel._wasGeocodeCancelled = false;
|
||||
|
||||
let i;
|
||||
let result;
|
||||
for (i = 0; i < geocoderServices.length; i++) {
|
||||
if (viewModel._wasGeocodeCancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
result = await attemptGeocode(geocoderServices[i], query, geocodeType);
|
||||
if (
|
||||
defined(result) &&
|
||||
result.state === "fulfilled" &&
|
||||
result.value.length > 0
|
||||
) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (viewModel._wasGeocodeCancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
viewModel._isSearchInProgress = false;
|
||||
clearCredits(viewModel);
|
||||
|
||||
const geocoderResults = result.value;
|
||||
if (
|
||||
result.state === "fulfilled" &&
|
||||
defined(geocoderResults) &&
|
||||
geocoderResults.length > 0
|
||||
) {
|
||||
viewModel._searchText = geocoderResults[0].displayName;
|
||||
viewModel.destinationFound(viewModel, geocoderResults[0].destination);
|
||||
const credits = updateCredits(
|
||||
viewModel,
|
||||
GeocoderService.getCreditsFromResult(geocoderResults[0]),
|
||||
);
|
||||
// If the result does not contain any credits, default to the service credit.
|
||||
if (!defined(credits)) {
|
||||
updateCredit(viewModel, geocoderServices[i].credit);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
viewModel._searchText = `${query} (not found)`;
|
||||
}
|
||||
|
||||
function updateCredit(viewModel, credit) {
|
||||
if (
|
||||
defined(credit) &&
|
||||
!viewModel._scene.isDestroyed() &&
|
||||
!viewModel._scene.frameState.creditDisplay.isDestroyed()
|
||||
) {
|
||||
viewModel._scene.frameState.creditDisplay.addStaticCredit(credit);
|
||||
viewModel._previousCredits.push(credit);
|
||||
}
|
||||
}
|
||||
|
||||
function updateCredits(viewModel, credits) {
|
||||
if (defined(credits)) {
|
||||
credits.forEach((credit) => updateCredit(viewModel, credit));
|
||||
}
|
||||
|
||||
return credits;
|
||||
}
|
||||
|
||||
function clearCredits(viewModel) {
|
||||
if (
|
||||
!viewModel._scene.isDestroyed() &&
|
||||
!viewModel._scene.frameState.creditDisplay.isDestroyed()
|
||||
) {
|
||||
viewModel._previousCredits.forEach((credit) => {
|
||||
viewModel._scene.frameState.creditDisplay.removeStaticCredit(credit);
|
||||
});
|
||||
}
|
||||
|
||||
viewModel._previousCredits.length = 0;
|
||||
}
|
||||
|
||||
function adjustSuggestionsScroll(viewModel, focusedItemIndex) {
|
||||
const container = getElement(viewModel._viewContainer);
|
||||
const searchResults = container.getElementsByClassName("search-results")[0];
|
||||
const listItems = container.getElementsByTagName("li");
|
||||
const element = listItems[focusedItemIndex];
|
||||
|
||||
if (focusedItemIndex === 0) {
|
||||
searchResults.scrollTop = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const offsetTop = element.offsetTop;
|
||||
if (offsetTop + element.clientHeight > searchResults.clientHeight) {
|
||||
searchResults.scrollTop = offsetTop + element.clientHeight;
|
||||
} else if (offsetTop < searchResults.scrollTop) {
|
||||
searchResults.scrollTop = offsetTop;
|
||||
}
|
||||
}
|
||||
|
||||
function cancelGeocode(viewModel) {
|
||||
if (viewModel._isSearchInProgress) {
|
||||
viewModel._isSearchInProgress = false;
|
||||
viewModel._wasGeocodeCancelled = true;
|
||||
}
|
||||
}
|
||||
|
||||
function hasOnlyWhitespace(string) {
|
||||
return /^\s*$/.test(string);
|
||||
}
|
||||
|
||||
function clearSuggestions(viewModel) {
|
||||
knockout.getObservable(viewModel, "_suggestions").removeAll();
|
||||
}
|
||||
|
||||
async function updateSearchSuggestions(viewModel) {
|
||||
if (!viewModel.autoComplete) {
|
||||
return;
|
||||
}
|
||||
|
||||
const query = viewModel._searchText;
|
||||
|
||||
clearSuggestions(viewModel);
|
||||
clearCredits(viewModel);
|
||||
|
||||
if (hasOnlyWhitespace(query)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const service of viewModel._geocoderServices) {
|
||||
const newResults = await service.geocode(query, GeocodeType.AUTOCOMPLETE);
|
||||
viewModel._suggestions = viewModel._suggestions.concat(newResults);
|
||||
if (newResults.length > 0) {
|
||||
let useDefaultCredit = true;
|
||||
newResults.forEach((result) => {
|
||||
const credits = GeocoderService.getCreditsFromResult(result);
|
||||
useDefaultCredit = useDefaultCredit && !defined(credits);
|
||||
updateCredits(viewModel, credits);
|
||||
});
|
||||
|
||||
// Use the service credit if there were no attributions in the results
|
||||
if (useDefaultCredit) {
|
||||
updateCredit(viewModel, service.credit);
|
||||
}
|
||||
}
|
||||
|
||||
if (viewModel._suggestions.length >= 5) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A function to fly to the destination found by a successful geocode.
|
||||
* @type {Geocoder.DestinationFoundFunction}
|
||||
*/
|
||||
GeocoderViewModel.flyToDestination = flyToDestination;
|
||||
|
||||
//exposed for testing
|
||||
GeocoderViewModel._updateSearchSuggestions = updateSearchSuggestions;
|
||||
GeocoderViewModel._adjustSuggestionsScroll = adjustSuggestionsScroll;
|
||||
|
||||
/**
|
||||
* @returns {boolean} true if the object has been destroyed, false otherwise.
|
||||
*/
|
||||
GeocoderViewModel.prototype.isDestroyed = function () {
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Destroys the widget. Should be called if permanently
|
||||
* removing the widget from layout.
|
||||
*/
|
||||
GeocoderViewModel.prototype.destroy = function () {
|
||||
this._suggestionSubscription.dispose();
|
||||
clearCredits(this);
|
||||
return destroyObject(this);
|
||||
};
|
||||
export default GeocoderViewModel;
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
.cesium-lighter .cesium-geocoder-input {
|
||||
border: solid 1px #759dc0;
|
||||
background-color: rgba(240, 240, 240, 0.9);
|
||||
color: black;
|
||||
}
|
||||
|
||||
.cesium-lighter .cesium-viewer-geocoderContainer:hover .cesium-geocoder-input {
|
||||
border-color: #aef;
|
||||
box-shadow: 0 0 8px #fff;
|
||||
}
|
||||
|
||||
.cesium-lighter .cesium-geocoder-searchButton {
|
||||
background-color: #e2f0ff;
|
||||
fill: #111;
|
||||
}
|
||||
|
||||
.cesium-lighter .cesium-geocoder-searchButton:hover {
|
||||
background-color: #a6d2ff;
|
||||
}
|
||||
Reference in New Issue
Block a user