diff --git a/examples/autoComplete/example.js b/examples/autoComplete/example.js index 48f65be..eeb7f59 100644 --- a/examples/autoComplete/example.js +++ b/examples/autoComplete/example.js @@ -11,6 +11,7 @@ let map; let placesService; let predictionItems = []; +let markers = []; function initMap() { const austinCoords = { lat: 30.268193, lng: -97.7457518 }; // Austin, TX :) @@ -30,7 +31,7 @@ function initMap() { for (let i = 0; i < predictionItems.length; i++) { let prediction = predictionItems[i]; if (prediction.description === ui.item.label) { - getPlaceDetails(prediction.place_id); + getPredictionInfo(prediction); // Clear our cached prediction items after making selection, // otherwise when the input widget loses focus it will trigger @@ -77,11 +78,28 @@ function initMap() { searchInput.val(prediction.description); searchInput.autocomplete("close"); - getPlaceDetails(prediction.place_id); + getPredictionInfo(prediction); } }); } +function getPredictionInfo(prediction) { + // Clear out any previous markers before we place new ones for the new prediction + markers.map((marker) => { + marker.setMap(null); + }); + markers = []; + + // If the prediction has a place_id, then we can do a getDetails + // Otherwise, the prediction is a query string (e.g. whataburgers in austin) + // so instead we need to do a textSearch to gather the place results + if (prediction.place_id) { + getPlaceDetails(prediction.place_id); + } else { + getTextSearch(prediction.description); + } +} + function getPlaceDetails(placeId) { var request = { placeId: placeId, @@ -96,6 +114,29 @@ function getPlaceDetails(placeId) { }); } +function getTextSearch(query) { + var request = { + query: query, + location: map.getCenter(), + }; + + const resultsBounds = new google.maps.LatLngBounds(); + + placesService.textSearch(request, function (results, status) { + if (status === google.maps.places.PlacesServiceStatus.OK) { + results.map((result) => { + createMarker(result); + + resultsBounds.extend(result.geometry.location); + }); + + // Adjust the map to fit all the new markers we added + const paddingInPixels = 50; + map.fitBounds(resultsBounds, paddingInPixels); + } + }); +} + function createMarker(place) { if (!place.geometry || !place.geometry.location) return; @@ -104,6 +145,8 @@ function createMarker(place) { position: place.geometry.location, }); + markers.push(marker); + // TODO: Add support for re-routing these event listeners to our MapLibre markers google.maps.event.addListener(marker, "click", () => { console.log("MARKER CLICKED", place.name); diff --git a/src/places.ts b/src/places.ts index 5f366a4..a8af645 100644 --- a/src/places.ts +++ b/src/places.ts @@ -153,6 +153,76 @@ class MigrationPlacesService { callback(null, PlacesServiceStatus.UNKNOWN_ERROR); }); } + + textSearch(request, callback) { + const query = request.query; // optional + const locationBias = request.location; // optional + const bounds = request.bounds; // optional + const language = request.language; // optional + const region = request.region; // optional + + const input: SearchPlaceIndexForTextRequest = { + IndexName: this._placeIndexName, + Text: query, // required + }; + + // If bounds is specified, then location bias is ignored + if (bounds) { + // TODO: Change this to use GoogleLatLngBounds once MigrationLatLngBounds has + // been updated to handle all the constructor variants, which will handle converting + // either bounds from both LatLngBounds|LatLngBoundsLiteral for us + let southWest; + let northEast; + if (bounds.getSouthWest !== undefined) { + southWest = bounds.getSouthWest(); + northEast = bounds.getNorthEast(); + } else { + southWest = GoogleLatLng(bounds.south, bounds.west); + northEast = GoogleLatLng(bounds.north, bounds.east); + } + + input.FilterBBox = [southWest.lng(), southWest.lat(), northEast.lng(), northEast.lat()]; + } else if (locationBias) { + const lngLat = LatLngToLngLat(locationBias); + if (lngLat) { + input.BiasPosition = lngLat; + } + } + + if (language) { + input.Language = language; + } + + if (region) { + input.FilterCountries = [region]; + } + + const command = new SearchPlaceIndexForTextCommand(input); + + this._client + .send(command) + .then((response) => { + const googleResults = []; + + const results = response.Results; + if (results.length !== 0) { + results.forEach(function (place) { + // Include all supported fields as in findPlaceFromQuery, + // but not the additional fields for getDetails + const placeResponse = convertAmazonPlaceToGoogle(place, ["ALL"], false); + + googleResults.push(placeResponse); + }); + } + + callback(googleResults, PlacesServiceStatus.OK); + }) + .catch((error) => { + console.error(error); + + callback([], PlacesServiceStatus.UNKNOWN_ERROR); + }); + } } class MigrationAutocompleteService { diff --git a/test/places.test.ts b/test/places.test.ts index 9109581..cbd133d 100644 --- a/test/places.test.ts +++ b/test/places.test.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { MigrationPlacesService } from "../src/places"; -import { PlacesServiceStatus } from "../src/googleCommon"; +import { GoogleLatLng, GoogleLatLngBounds, PlacesServiceStatus } from "../src/googleCommon"; // Spy on console.error so we can verify it gets called in error cases jest.spyOn(console, "error").mockImplementation(() => {}); @@ -249,3 +249,155 @@ test("getDetails should handle client error", (done) => { done(); }); }); + +test("textSearch should only use bounds if location was also specified", (done) => { + const east = 0; + const north = 1; + const south = 2; + const west = 3; + const request = { + query: "cool places in austin", + bounds: GoogleLatLngBounds(GoogleLatLng(south, west), GoogleLatLng(east, north)), + location: GoogleLatLng(4, 5), + }; + + placesService.textSearch(request, (results, status) => { + expect(results.length).toStrictEqual(1); + const firstResult = results[0]; + + expect(mockedClientSend).toHaveBeenCalledTimes(1); + expect(mockedClientSend).toHaveBeenCalledWith(expect.any(SearchPlaceIndexForTextCommand)); + const clientInput = mockedClientSend.mock.calls[0][0].input; + + expect(clientInput.FilterBBox).toStrictEqual([west, south, north, east]); + expect(clientInput.BiasPosition).toBeUndefined(); + + expect(firstResult.name).toStrictEqual("Austin"); + expect(status).toStrictEqual(PlacesServiceStatus.OK); + + // Signal the unit test is complete + done(); + }); +}); + +test("textSearch should accept bounds as a literal", (done) => { + const east = 0; + const north = 1; + const south = 2; + const west = 3; + const request = { + query: "cool places in austin", + bounds: { east: east, north: north, south: south, west: west }, + }; + + placesService.textSearch(request, (results, status) => { + expect(results.length).toStrictEqual(1); + const firstResult = results[0]; + + expect(mockedClientSend).toHaveBeenCalledTimes(1); + expect(mockedClientSend).toHaveBeenCalledWith(expect.any(SearchPlaceIndexForTextCommand)); + const clientInput = mockedClientSend.mock.calls[0][0].input; + + expect(clientInput.FilterBBox).toStrictEqual([west, south, east, north]); + expect(clientInput.BiasPosition).toBeUndefined(); + + expect(firstResult.name).toStrictEqual("Austin"); + expect(status).toStrictEqual(PlacesServiceStatus.OK); + + // Signal the unit test is complete + done(); + }); +}); + +test("textSearch should accept location bias if there is no bounds specified", (done) => { + const request = { + query: "cool places in austin", + location: GoogleLatLng(testLat, testLng), + }; + + placesService.textSearch(request, (results, status) => { + expect(results.length).toStrictEqual(1); + const firstResult = results[0]; + + expect(mockedClientSend).toHaveBeenCalledTimes(1); + expect(mockedClientSend).toHaveBeenCalledWith(expect.any(SearchPlaceIndexForTextCommand)); + const clientInput = mockedClientSend.mock.calls[0][0].input; + + expect(clientInput.BiasPosition).toStrictEqual([testLng, testLat]); + expect(clientInput.FilterBBox).toBeUndefined(); + + expect(firstResult.name).toStrictEqual("Austin"); + expect(status).toStrictEqual(PlacesServiceStatus.OK); + + // Signal the unit test is complete + done(); + }); +}); + +test("textSearch should accept language", (done) => { + const request = { + query: "cool places in austin", + location: GoogleLatLng(testLat, testLng), + language: "en", + }; + + placesService.textSearch(request, (results, status) => { + expect(results.length).toStrictEqual(1); + const firstResult = results[0]; + + expect(mockedClientSend).toHaveBeenCalledTimes(1); + expect(mockedClientSend).toHaveBeenCalledWith(expect.any(SearchPlaceIndexForTextCommand)); + const clientInput = mockedClientSend.mock.calls[0][0].input; + + expect(clientInput.BiasPosition).toStrictEqual([testLng, testLat]); + expect(clientInput.Language).toStrictEqual("en"); + + expect(firstResult.name).toStrictEqual("Austin"); + expect(status).toStrictEqual(PlacesServiceStatus.OK); + + // Signal the unit test is complete + done(); + }); +}); + +test("textSearch should convert region to countries filter", (done) => { + const request = { + query: "cool places in austin", + location: GoogleLatLng(testLat, testLng), + region: "us", + }; + + placesService.textSearch(request, (results, status) => { + expect(results.length).toStrictEqual(1); + const firstResult = results[0]; + + expect(mockedClientSend).toHaveBeenCalledTimes(1); + expect(mockedClientSend).toHaveBeenCalledWith(expect.any(SearchPlaceIndexForTextCommand)); + const clientInput = mockedClientSend.mock.calls[0][0].input; + + expect(clientInput.BiasPosition).toStrictEqual([testLng, testLat]); + expect(clientInput.FilterCountries).toStrictEqual(["us"]); + + expect(firstResult.name).toStrictEqual("Austin"); + expect(status).toStrictEqual(PlacesServiceStatus.OK); + + // Signal the unit test is complete + done(); + }); +}); + +test("textSearch should handle client error", (done) => { + const request = { + query: clientErrorQuery, + }; + + placesService.textSearch(request, (results, status) => { + expect(results).toHaveLength(0); + expect(status).toStrictEqual(PlacesServiceStatus.UNKNOWN_ERROR); + + expect(console.error).toHaveBeenCalledTimes(1); + + // Signal the unit test is complete + done(); + }); +});