diff --git a/places_insights/places-insights-demo/help.js b/places_insights/places-insights-demo/help.js index ad4762c..a94caea 100644 --- a/places_insights/places-insights-demo/help.js +++ b/places_insights/places-insights-demo/help.js @@ -94,13 +94,13 @@ function generateGuideHtml() {

C. Region Search

- This powerful mode allows you to search by administrative names like cities, states, or postal codes instead of drawing on the map. + This powerful mode allows you to search by administrative regions like cities, states, or postal codes using Place Autocomplete.

  1. Select Region Search from the "Demo Type" dropdown.
  2. -
  3. Choose a Region Type from the dropdown. This list is dynamically populated based on the selected ${locationTypeLower}.
  4. -
  5. Enter a name in the Region Name(s) input box (e.g., "London").
  6. -
  7. (Optional) Add Multiple Regions: To search across several regions at once, click the + button after typing each name.
  8. +
  9. Start typing a region name in the Region input box (e.g., "Manhattan").
  10. +
  11. Select the correct region from the dropdown suggestions. This guarantees accurate results by using the exact Place ID.
  12. +
  13. (Optional) Add Multiple Regions: To search across several regions at once, continue searching and adding more places to the list.

D. Route Search

@@ -134,7 +134,7 @@ function generateGuideHtml() {
  • Business Status: Filter places by their operational status (Operational, Closed Temporarily, Closed Permanently, or Any). Default is Operational.
  • Attribute Filters: Filter by Price Level, set min/max ratings, or select checkboxes for amenities (e.g., "Offers Delivery").
  • Opening Hours: Select a Day of Week and time window (Not available in H3 Function mode).
  • -
  • Brand Filters (US Only): Filter by Brand Category or Brand Name (Not available in H3 Function mode).
  • +
  • Brand Filters (US Only): Filter by Brand Name (Not available in H3 Function mode).
  • 5. Choosing Your Visualization

    diff --git a/places_insights/places-insights-demo/index.html b/places_insights/places-insights-demo/index.html index f9d426a..a7e0daf 100644 --- a/places_insights/places-insights-demo/index.html +++ b/places_insights/places-insights-demo/index.html @@ -92,19 +92,10 @@

    Places Insights

    diff --git a/places_insights/places-insights-demo/main.js b/places_insights/places-insights-demo/main.js index cb2f396..16cb5cf 100644 --- a/places_insights/places-insights-demo/main.js +++ b/places_insights/places-insights-demo/main.js @@ -72,9 +72,12 @@ function handleStartDemo() { const brandFilters = document.getElementById('brand-filters'); // Logic for Brand Filters visibility let isUS = false; + let countryCode; if (DATASET === 'SAMPLE') { - isUS = SAMPLE_LOCATIONS[selectedCountryName] === 'us'; + countryCode = SAMPLE_LOCATIONS[selectedCountryName]; + isUS = countryCode === 'us'; } else { + countryCode = COUNTRY_CODES[selectedCountryName]; isUS = selectedCountryName === 'United States'; } @@ -84,7 +87,11 @@ function handleStartDemo() { brandFilters.classList.add('hidden'); } - populateRegionTypes(selectedCountryName); + // Restrict Region Autocomplete to the active country + if (window.regionAutocomplete) { + window.regionAutocomplete.includedRegionCodes = [countryCode]; + } + startDemo(selectedCountryName); } else { alert("Please select a location to begin."); @@ -102,22 +109,6 @@ function handleChangeCountryClick() { invalidateQueryState(); } -/** - * Handles clicks on the "+" button to add a region to the list. - */ -function handleAddRegionClick() { - const regionInput = document.getElementById('region-name-input'); - const regionList = document.getElementById('selected-regions-list'); - const regionName = regionInput.value.trim(); - - if (regionName) { - addTag(toTitleCase(regionName), regionList); // Apply Title Case here - regionInput.value = ''; - regionInput.focus(); - invalidateQueryState(); - } -} - /** * Handles clicks on the "+" button to add a brand to the list. */ @@ -192,41 +183,49 @@ function handleCopyQueryClick() { /** - * Populates the Region Type dropdown based on the selected country's configuration. + * Initializes the Place Autocomplete (New) web components for Region search. */ -function populateRegionTypes(locationName) { - const select = document.getElementById('region-type-select'); - select.innerHTML = ''; - - let countryCode; - if (DATASET === 'SAMPLE') { - countryCode = SAMPLE_LOCATIONS[locationName]; - } else { - countryCode = COUNTRY_CODES[locationName]; - } +async function initializeRegionSearch() { + const { PlaceAutocompleteElement } = await google.maps.importLibrary("places"); + const container = document.getElementById('region-autocomplete-container'); - const regionFields = REGION_FIELD_CONFIG[countryCode]; + const autocomplete = new PlaceAutocompleteElement(); + container.appendChild(autocomplete); - if (regionFields && regionFields.length > 0) { - const defaultOption = document.createElement('option'); - defaultOption.value = ''; - defaultOption.textContent = '-- Select a region type --'; - defaultOption.disabled = true; - defaultOption.selected = true; - select.appendChild(defaultOption); + autocomplete.addEventListener('gmp-select', async ({ placePrediction }) => { + if (!placePrediction) return; + invalidateQueryState(); - regionFields.forEach(field => { - const option = document.createElement('option'); - option.value = `${field.field}|${field.type}`; - option.textContent = field.label; - select.appendChild(option); - }); - } else { - const disabledOption = document.createElement('option'); - disabledOption.textContent = 'Region search not available'; - disabledOption.disabled = true; - select.appendChild(disabledOption); - } + const place = placePrediction.toPlace(); + // Request the viewport field to properly frame the region on the map + await place.fetchFields({ fields: ['id', 'displayName', 'types', 'location', 'viewport'] }); + + let targetColumn = null; + if (place.types) { + for (const type of place.types) { + if (REGION_TYPE_TO_BQ_COLUMN[type]) { + targetColumn = REGION_TYPE_TO_BQ_COLUMN[type]; + break; + } + } + } + + if (!targetColumn) { + alert(`The selected place type is not supported as a search region. Please select a valid city, state, or neighborhood.`); + autocomplete.inputValue = ''; + return; + } + + if (place.location || place.viewport) { + addRegionTag(place.displayName, place.id, targetColumn, place.location, place.viewport); + } else { + alert("Could not retrieve location for this place."); + } + + autocomplete.inputValue = ''; + }); + + window.regionAutocomplete = autocomplete; } /** @@ -296,7 +295,6 @@ window.onload = () => { demoTypeSelect: document.getElementById('demo-type-select'), startButton: document.getElementById("start-demo-btn"), changeCountryBtn: document.getElementById('change-country-btn'), - addRegionBtn: document.getElementById('add-region-btn'), addBrandBtn: document.getElementById('add-brand-btn'), showHelpBtn: document.getElementById('show-help-btn'), guideModal: document.getElementById('guide-modal'), @@ -325,7 +323,6 @@ window.onload = () => { elements.copyQueryBtn.addEventListener('click', handleCopyQueryClick); elements.startButton.addEventListener("click", handleStartDemo); elements.changeCountryBtn.addEventListener('click', handleChangeCountryClick); - elements.addRegionBtn.addEventListener('click', handleAddRegionClick); elements.addBrandBtn.addEventListener('click', handleAddBrandClick); elements.showHelpBtn.addEventListener('click', showHelpModal); elements.closeHelpBtn.addEventListener('click', () => hideModal('guide-modal')); @@ -362,6 +359,7 @@ window.onload = () => { initializeIdentityServices(); initializeAutocomplete(document.getElementById('place-type-input')); + initializeRegionSearch(); initializeRouteSearch(); initializeAccordion(); diff --git a/places_insights/places-insights-demo/query.js b/places_insights/places-insights-demo/query.js index 3104d91..ee5e7cb 100644 --- a/places_insights/places-insights-demo/query.js +++ b/places_insights/places-insights-demo/query.js @@ -38,36 +38,55 @@ function getPolygonSearchParams() { } async function getRegionSearchParams() { - const regionInputValue = document.getElementById('region-type-select').value; - const regionNameInput = toTitleCase(document.getElementById('region-name-input').value.trim()); - const regionTags = [...document.querySelectorAll('#selected-regions-list span')].map(s => s.textContent); + const regionTags = [...document.querySelectorAll('#selected-regions-list .selected-region-tag')]; - if (!regionInputValue || (!regionNameInput && regionTags.length === 0)) { - return { success: false, message: "Select a Region Type and enter at least one Region Name." }; + if (regionTags.length === 0) { + return { success: false, message: "Search for and select at least one Region." }; } - const uniqueRegionNames = [...new Set([...regionTags, regionNameInput].filter(Boolean))]; - const [field, dataType] = regionInputValue.split('|'); - const filter = buildRegionFilter(field, dataType, uniqueRegionNames); + // Group tags by their target column and type to build the filter + const columnsToIds = {}; + regionTags.forEach(tag => { + const col = tag.dataset.column; + const colType = tag.dataset.colType; + if (!columnsToIds[col]) columnsToIds[col] = { type: colType, ids: [] }; + columnsToIds[col].ids.push(tag.dataset.id); + }); + + // Build exact match filters using the Place IDs based on their column data type + const filterParts = []; + for (const [col, data] of Object.entries(columnsToIds)) { + const idList = data.ids.map(id => `'${id}'`).join(', '); + if (data.type === 'STRING') { + filterParts.push(`places.${col} IN (${idList})`); + } else { + filterParts.push(`EXISTS (SELECT 1 FROM UNNEST(places.${col}) AS id WHERE id IN (${idList}))`); + } + } + const filter = `(${filterParts.join(' OR ')})`; - updateStatus('Geocoding regions...'); - const { Geocoder } = await google.maps.importLibrary("geocoding"); + // Calculate map bounds from tags, prioritizing the viewport for accurate framing const bounds = new google.maps.LatLngBounds(); - const geocodePromises = uniqueRegionNames.map(name => new Geocoder().geocode({ address: `${name}, ${selectedCountryName}` })); - - const resultsArray = await Promise.all(geocodePromises); - resultsArray.forEach(({ results }) => { - if (results.length > 0) bounds.union(results[0].geometry.viewport); + let hasBounds = false; + + regionTags.forEach(tag => { + if (tag.dataset.north) { + const sw = { lat: parseFloat(tag.dataset.south), lng: parseFloat(tag.dataset.west) }; + const ne = { lat: parseFloat(tag.dataset.north), lng: parseFloat(tag.dataset.east) }; + bounds.union(new google.maps.LatLngBounds(sw, ne)); + hasBounds = true; + } else if (tag.dataset.lat) { + bounds.extend({ lat: parseFloat(tag.dataset.lat), lng: parseFloat(tag.dataset.lng) }); + hasBounds = true; + } }); - - if (bounds.isEmpty()) { - throw new Error(`Could not geocode any of the specified regions.`); + + if (hasBounds) { + map.fitBounds(bounds); } - map.fitBounds(bounds); return { - success: true, filter, center: bounds.getCenter(), searchAreaVar: '', - isMultiRegion: uniqueRegionNames.length > 1, regionType: field, regionDataType: dataType + success: true, filter, center: bounds.getCenter(), searchAreaVar: '' }; } @@ -199,10 +218,12 @@ async function runQuery() { // Brand Filters (only applicable here, not in H3 Function) const brandNames = [...document.querySelectorAll('#selected-brands-list span')].map(s => s.textContent); + const pendingBrandInput = document.getElementById('brand-name-input').value.trim(); + if (pendingBrandInput && !brandNames.includes(pendingBrandInput)) { + brandNames.push(pendingBrandInput); + } if (brandNames.length > 0) allFilters.push(buildBrandFilter(brandNames)); - // Brand Category is removed as we no longer have brands.json data - const openingDay = document.getElementById('day-of-week-select').value; const hoursFilter = buildOpeningHoursFilter(openingDay, document.getElementById('start-time-input').value, document.getElementById('end-time-input').value); if (hoursFilter.whereClause) allFilters.push(hoursFilter.whereClause); @@ -241,7 +262,7 @@ async function runQuery() { const h3Res = parseInt(document.getElementById('h3-resolution-slider').value, 10); sqlQuery = buildH3DensityQuery(searchParams.searchAreaVar, fromClause, whereClause, h3Res); } else { - sqlQuery = buildAggregateQuery(searchParams.searchAreaVar, fromClause, whereClause, placeTypes, isBrandQuery, searchParams.isMultiRegion, searchParams.regionType, searchParams.regionDataType); + sqlQuery = buildAggregateQuery(searchParams.searchAreaVar, fromClause, whereClause, placeTypes, isBrandQuery); } lastExecutedQuery = sqlQuery; @@ -292,8 +313,6 @@ function buildBrandFilter(brandNames) { return `brands.name IN (${sanitizedNames})`; } -// Brand Category Filter removed - function buildOpeningHoursFilter(day, startTime, endTime) { if (!day || (!startTime && !endTime)) return { unnestClause: '', whereClause: '' }; const unnestClause = `, UNNEST(places.regular_opening_hours.${day}) AS opening_period`; @@ -317,21 +336,9 @@ function buildRatingFilter(min, max) { return ''; } -function buildRegionFilter(regionType, regionDataType, regionNames) { - if (!regionType || !regionNames || regionNames.length === 0) return ''; - const sanitizedNames = regionNames.map(name => `'${name.replace(/'/g, "\\'")}'`).join(', '); - if (regionDataType === 'STRING') { - return `places.${regionType} IN (${sanitizedNames})`; - } - return `EXISTS (SELECT 1 FROM UNNEST(places.${regionType}) AS name WHERE name IN (${sanitizedNames}))`; -} - // --- UNIFIED QUERY BUILDERS --- -function buildAggregateQuery(searchAreaVar, fromClause, whereClause, types, isBrandQuery, isMultiRegion, regionType, regionDataType) { - if (isMultiRegion && regionDataType === 'STRING') { - return `${searchAreaVar} SELECT WITH AGGREGATION_THRESHOLD places.${regionType} AS region_name, COUNT(*) AS count ${fromClause} ${whereClause} GROUP BY region_name ORDER BY count DESC`; - } +function buildAggregateQuery(searchAreaVar, fromClause, whereClause, types, isBrandQuery) { if (isBrandQuery) { return `${searchAreaVar} SELECT WITH AGGREGATION_THRESHOLD brands.name, COUNT(places.id) AS count ${fromClause} ${whereClause} GROUP BY brands.name ORDER BY count DESC`; } diff --git a/places_insights/places-insights-demo/state.js b/places_insights/places-insights-demo/state.js index 0b52597..1323b25 100644 --- a/places_insights/places-insights-demo/state.js +++ b/places_insights/places-insights-demo/state.js @@ -52,101 +52,27 @@ const SAMPLE_LOCATIONS = { // Holds the data from brands.json, loaded at startup. let BRANDS_DATA = []; -// Configuration for country-specific region search fields, now with explicit types. -const REGION_FIELD_CONFIG = { - 'au': [ - { label: 'State / Territory', field: 'administrative_area_level_1_name', type: 'STRING' }, - { label: 'City / Locality', field: 'locality_names', type: 'ARRAY' }, - { label: 'Postal Code', field: 'postal_code_names', type: 'ARRAY' } - ], - 'br': [ - { label: 'State', field: 'administrative_area_level_1_name', type: 'STRING' }, - { label: 'City / Municipality', field: 'administrative_area_level_2_name', type: 'STRING' }, - { label: 'Locality', field: 'locality_names', type: 'ARRAY' }, - { label: 'Postal Code', field: 'postal_code_names', type: 'ARRAY' }, - { label: 'Neighborhood', field: 'sublocality_level_1_names', type: 'ARRAY' } - ], - 'ca': [ - { label: 'Province / Territory', field: 'administrative_area_level_1_name', type: 'STRING' }, - { label: 'City / Locality', field: 'locality_names', type: 'ARRAY' }, - { label: 'Neighborhood', field: 'neighborhood_names', type: 'ARRAY' }, - { label: 'Postal Code', field: 'postal_code_names', type: 'ARRAY' } - ], - 'de': [ - { label: 'State', field: 'administrative_area_level_1_name', type: 'STRING' }, - { label: 'District', field: 'administrative_area_level_3_name', type: 'STRING' }, - { label: 'City / Locality', field: 'locality_names', type: 'ARRAY' }, - { label: 'Postal Code', field: 'postal_code_names', type: 'ARRAY' }, - { label: 'Sublocality / Borough', field: 'sublocality_level_1_names', type: 'ARRAY' } - ], - 'es': [ - { label: 'Autonomous Community', field: 'administrative_area_level_1_name', type: 'STRING' }, - { label: 'Province', field: 'administrative_area_level_2_name', type: 'STRING' }, - { label: 'City / Locality', field: 'locality_names', type: 'ARRAY' }, - { label: 'Neighborhood', field: 'neighborhood_names', type: 'ARRAY' }, - { label: 'Postal Code', field: 'postal_code_names', type: 'ARRAY' } - ], - 'fr': [ - { label: 'Region', field: 'administrative_area_level_1_name', type: 'STRING' }, - { label: 'Department', field: 'administrative_area_level_2_name', type: 'STRING' }, - { label: 'City / Locality', field: 'locality_names', type: 'ARRAY' }, - { label: 'Postal Code', field: 'postal_code_names', type: 'ARRAY' }, - { label: 'Sublocality', field: 'sublocality_level_1_names', type: 'ARRAY' } - ], - 'gb': [ - { label: 'Country', field: 'administrative_area_level_1_name', type: 'STRING' }, - { label: 'City / Locality', field: 'locality_names', type: 'ARRAY' }, - { label: 'Postal Town', field: 'postal_town_names', type: 'ARRAY' }, - { label: 'Postal Code', field: 'postal_code_names', type: 'ARRAY' } - ], - 'in': [ - { label: 'State', field: 'administrative_area_level_1_name', type: 'STRING' }, - { label: 'District', field: 'administrative_area_level_3_name', type: 'STRING' }, - { label: 'City / Locality', field: 'locality_names', type: 'ARRAY' }, - { label: 'Postal Code (PIN)', field: 'postal_code_names', type: 'ARRAY' }, - { label: 'Sublocality', field: 'sublocality_level_1_names', type: 'ARRAY' } - ], - 'id': [ - { label: 'Province', field: 'administrative_area_level_1_name', type: 'STRING' }, - { label: 'Regency / City', field: 'administrative_area_level_2_name', type: 'STRING' }, - { label: 'District', field: 'administrative_area_level_3_name', type: 'STRING' }, - { label: 'Village / Kelurahan', field: 'administrative_area_level_4_name', type: 'STRING' }, - { label: 'City / Locality', field: 'locality_names', type: 'ARRAY' }, - { label: 'Postal Code', field: 'postal_code_names', type: 'ARRAY' } - ], - 'it': [ - { label: 'Region', field: 'administrative_area_level_1_name', type: 'STRING' }, - { label: 'Province', field: 'administrative_area_level_2_name', type: 'STRING' }, - { label: 'Municipality (Comune)', field: 'administrative_area_level_3_name', type: 'STRING' }, - { label: 'Postal Code', field: 'postal_code_names', type: 'ARRAY' } - ], - 'jp': [ - { label: 'Prefecture', field: 'administrative_area_level_1_name', type: 'STRING' }, - { label: 'City / Locality', field: 'locality_names', type: 'ARRAY' }, - { label: 'Postal Code', field: 'postal_code_names', type: 'ARRAY' }, - { label: 'Sublocality', field: 'sublocality_level_1_names', type: 'ARRAY' } - ], - 'mx': [ - { label: 'State', field: 'administrative_area_level_1_name', type: 'STRING' }, - { label: 'Municipality', field: 'administrative_area_level_2_name', type: 'STRING' }, - { label: 'City / Locality', field: 'locality_names', type: 'ARRAY' }, - { label: 'Postal Code', field: 'postal_code_names', type: 'ARRAY' } - ], - 'ch': [ - { label: 'Canton', field: 'administrative_area_level_1_name', type: 'STRING' }, - { label: 'District', field: 'administrative_area_level_2_name', type: 'STRING' }, - { label: 'Municipality / Locality', field: 'locality_names', type: 'ARRAY' }, - { label: 'Postal Code', field: 'postal_code_names', type: 'ARRAY' } - ], - 'us': [ - { label: 'State', field: 'administrative_area_level_1_name', type: 'STRING' }, - { label: 'County', field: 'administrative_area_level_2_name', type: 'STRING' }, - { label: 'City / Locality', field: 'locality_names', type: 'ARRAY' }, - { label: 'Neighborhood', field: 'neighborhood_names', type: 'ARRAY' }, - { label: 'Postal Code', field: 'postal_code_names', type: 'ARRAY' } - ] -}; +// Maps Places API Types to their exact BigQuery Columns and Data Types +const REGION_TYPE_TO_BQ_COLUMN = { + 'administrative_area_level_1': { column: 'administrative_area_level_1_id', type: 'STRING' }, + 'administrative_area_level_2': { column: 'administrative_area_level_2_id', type: 'STRING' }, + 'administrative_area_level_3': { column: 'administrative_area_level_3_id', type: 'STRING' }, + 'administrative_area_level_4': { column: 'administrative_area_level_4_id', type: 'STRING' }, + 'administrative_area_level_5': { column: 'administrative_area_level_5_id', type: 'STRING' }, + 'administrative_area_level_6': { column: 'administrative_area_level_6_id', type: 'STRING' }, + 'administrative_area_level_7': { column: 'administrative_area_level_7_id', type: 'STRING' }, + 'locality': { column: 'locality_ids', type: 'ARRAY' }, + 'sublocality': { column: 'sublocality_level_1_ids', type: 'ARRAY' }, + 'sublocality_level_1': { column: 'sublocality_level_1_ids', type: 'ARRAY' }, + 'sublocality_level_2': { column: 'sublocality_level_2_ids', type: 'ARRAY' }, + 'sublocality_level_3': { column: 'sublocality_level_3_ids', type: 'ARRAY' }, + 'sublocality_level_4': { column: 'sublocality_level_4_ids', type: 'ARRAY' }, + 'sublocality_level_5': { column: 'sublocality_level_5_ids', type: 'ARRAY' }, + 'neighborhood': { column: 'neighborhood_ids', type: 'ARRAY' }, + 'postal_code': { column: 'postal_code_ids', type: 'ARRAY' }, + 'postal_town': { column: 'postal_town_ids', type: 'ARRAY' } +}; // --- MAP & OVERLAY STATE --- // References to Google Maps and deck.gl objects. diff --git a/places_insights/places-insights-demo/ui.js b/places_insights/places-insights-demo/ui.js index 6f84ed7..f69aa7d 100644 --- a/places_insights/places-insights-demo/ui.js +++ b/places_insights/places-insights-demo/ui.js @@ -69,8 +69,13 @@ function resetSidebarUI(targetMode = 'circle-search') { document.getElementById('wkt-input').value = ''; document.getElementById('wkt-input').classList.remove('invalid'); - document.getElementById('region-name-input').value = ''; + + // Reset Region Search document.getElementById('selected-regions-list').innerHTML = ''; + if (window.regionAutocomplete) { + window.regionAutocomplete.inputValue = ''; + } + document.getElementById('route-radius-input').value = '100'; // Reset Filters @@ -139,6 +144,50 @@ function addTag(text, listElement) { listElement.appendChild(tag); } +/** + * Adds a region tag that explicitly stores its Place ID, column, data type, and viewport. + */ +function addRegionTag(name, id, columnObj, location, viewport) { + const listElement = document.getElementById('selected-regions-list'); + if ([...listElement.querySelectorAll('.selected-region-tag')].some(el => el.dataset.id === id)) return; + + const tag = document.createElement('li'); + tag.className = 'selected-type-tag selected-region-tag'; + tag.dataset.id = id; + tag.dataset.column = columnObj.column; + tag.dataset.colType = columnObj.type; + + // Store Viewport for accurate bounding box + if (viewport) { + const vp = viewport.toJSON(); + tag.dataset.north = vp.north; + tag.dataset.south = vp.south; + tag.dataset.east = vp.east; + tag.dataset.west = vp.west; + } + + // Always store location as a fallback + if (location) { + tag.dataset.lat = location.lat(); + tag.dataset.lng = location.lng(); + } + + const textSpan = document.createElement('span'); + textSpan.textContent = name; + + const removeBtn = document.createElement('button'); + removeBtn.className = 'remove-tag-btn'; + removeBtn.innerHTML = '×'; + removeBtn.onclick = () => { + tag.remove(); + invalidateQueryState(); + }; + + tag.appendChild(textSpan); + tag.appendChild(removeBtn); + listElement.appendChild(tag); +} + /** * Initializes the place type autocomplete functionality. */