Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
10 changes: 5 additions & 5 deletions places_insights/places-insights-demo/help.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,13 @@ function generateGuideHtml() {

<h3>C. Region Search</h3>
<p>
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.
</p>
<ol>
<li>Select <strong>Region Search</strong> from the "Demo Type" dropdown.</li>
<li>Choose a <strong>Region Type</strong> from the dropdown. This list is dynamically populated based on the selected ${locationTypeLower}.</li>
<li>Enter a name in the <strong>Region Name(s)</strong> input box (e.g., "London").</li>
<li><strong>(Optional) Add Multiple Regions:</strong> To search across several regions at once, click the <code>+</code> button after typing each name.</li>
<li>Start typing a region name in the <strong>Region</strong> input box (e.g., "Manhattan").</li>
<li>Select the correct region from the dropdown suggestions. This guarantees accurate results by using the exact Place ID.</li>
<li><strong>(Optional) Add Multiple Regions:</strong> To search across several regions at once, continue searching and adding more places to the list.</li>
</ol>

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

<h2>5. Choosing Your Visualization</h2>
Expand Down
15 changes: 3 additions & 12 deletions places_insights/places-insights-demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -92,19 +92,10 @@ <h1>Places Insights</h1>
</div>

<div id="region-search-controls" class="hidden">
<p class="info-text">Select a region type and enter name(s).</p>
<p class="info-text">Search for a region (e.g., city, state, zip code).</p>
<div class="control-group">
<label for="region-type-select">Region Type</label>
<select id="region-type-select">
<option value="" disabled selected>-- Select location first --</option>
</select>
</div>
<div class="control-group">
<label for="region-name-input">Region Name(s)</label>
<div class="input-with-button">
<input type="text" id="region-name-input" placeholder="e.g., London, California...">
<button id="add-region-btn" class="secondary-button">+</button>
</div>
<label for="region-autocomplete-container">Region</label>
<div id="region-autocomplete-container"></div>
</div>
<ul id="selected-regions-list"></ul>
</div>
Expand Down
100 changes: 49 additions & 51 deletions places_insights/places-insights-demo/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}

Expand All @@ -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.");
Expand All @@ -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.
*/
Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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'),
Expand Down Expand Up @@ -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'));
Expand Down Expand Up @@ -362,6 +359,7 @@ window.onload = () => {

initializeIdentityServices();
initializeAutocomplete(document.getElementById('place-type-input'));
initializeRegionSearch();
initializeRouteSearch();
initializeAccordion();

Expand Down
85 changes: 46 additions & 39 deletions places_insights/places-insights-demo/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: ''
};
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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`;
Expand All @@ -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`;
}
Expand Down
Loading