|
This example demonstrates how to search for landmarks filtered by specific categories (e.g., restaurants, hotels, gas stations).
Live Demo
Overview
This example shows how to:
- Search for landmarks based on specific categories
- Display a multi-select category filter UI
- Combine text search with category filtering
- Highlight and navigate to selected search results
Code Implementation
Imports
First, import the required modules from the SDK:
import {
GemKit,
GemMap,
Coordinates,
PositionService,
Landmark,
LandmarkCategory,
SearchPreferences,
SearchService,
GemError,
HighlightRenderSettings,
HighlightOptions,
AddressField,
GemIcon,
} from '@magiclane/maps-sdk';
SDK Initialization
Initialize the SDK and create a map view (UI omitted):
let map: GemMap | null = null;
let selectedCategories: LandmarkCategory[] = [];
let searchResults: Landmark[] = [];
let categories: LandmarkCategory[] = [];
window.addEventListener('DOMContentLoaded', async () => {
const gemKit = await GemKit.initialize(GEMKIT_TOKEN);
await PositionService.instance;
const viewId = 2;
const wrapper = gemKit.createView(viewId, (gemMap: GemMap) => {
map = gemMap;
});
try {
categories = (window as any).GenericCategories?.categories || [];
} catch (error) {
categories = [];
}
});
Create Category Selection Modal
Build a modal with category checkboxes and text input:
function openSearchModal() {
if (searchModal) {
searchModal.remove();
searchModal = null;
}
searchModal = document.createElement('div');
searchModal.style.cssText = `
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.25); z-index: 3000; display: flex;
align-items: center; justify-content: center;
`;
const modalContent = document.createElement('div');
modalContent.style.cssText = `
background: #fff; border-radius: 12px; box-shadow: 0 4px 24px rgba(0,0,0,0.2);
padding: 32px 24px; min-width: 400px; max-width: 90vw; max-height: 90vh; overflow-y: auto;
`;
const title = document.createElement('h2');
title.textContent = 'Search Category';
title.style.cssText = 'margin-top: 0; color: #673ab7;';
modalContent.appendChild(title);
const textInput = document.createElement('input');
textInput.type = 'text';
textInput.placeholder = 'Enter text';
textInput.style.cssText = `
width: 100%; padding: 8px; font-size: 1em; margin-bottom: 16px;
border: 1px solid #ddd; border-radius: 6px;
`;
modalContent.appendChild(textInput);
const categoryList = document.createElement('div');
categoryList.style.cssText = 'margin-bottom: 16px;';
categories.forEach((cat) => {
const item = document.createElement('div');
item.textContent = cat.name;
const isSelected = selectedCategories.includes(cat);
item.style.cssText = `
padding: 8px 12px; margin-bottom: 4px; border-radius: 6px; cursor: pointer;
background: ${isSelected ? '#e0e0e0' : '#fafafa'};
font-weight: 500; color: #333; border: 1px solid #ddd;
transition: background-color 0.2s;
`;
item.onclick = () => {
if (selectedCategories.includes(cat)) {
selectedCategories = selectedCategories.filter(c => c !== cat);
} else {
selectedCategories.push(cat);
}
openSearchModal();
};
categoryList.appendChild(item);
});
modalContent.appendChild(categoryList);
const searchBtn = document.createElement('button');
searchBtn.textContent = 'Search';
searchBtn.style.cssText = `
padding: 10px 24px; background: #673ab7; color: #fff;
border: none; border-radius: 8px; font-size: 1em;
font-weight: 500; cursor: pointer;
`;
searchBtn.onclick = () => {
performSearch(textInput.value);
};
modalContent.appendChild(searchBtn);
if (searchResults.length > 0) {
const resultsList = document.createElement('div');
resultsList.style.cssText = 'margin-top: 24px; max-height: 300px; overflow-y: auto;';
searchResults.forEach((lmk) => {
const resultItem = document.createElement('div');
resultItem.style.cssText = `
padding: 12px; border-bottom: 1px solid #eee; cursor: pointer;
transition: background-color 0.2s;
`;
resultItem.innerHTML = `
<strong>${lmk.name}</strong><br>
<span style="color:#666;">${getFormattedDistance(lmk)} ${getAddress(lmk)}</span>
`;
resultItem.onclick = () => {
selectSearchResult(lmk);
};
resultItem.addEventListener('mouseover', () => {
resultItem.style.backgroundColor = '#f5f5f5';
});
resultItem.addEventListener('mouseout', () => {
resultItem.style.backgroundColor = 'transparent';
});
resultsList.appendChild(resultItem);
});
modalContent.appendChild(resultsList);
}
const closeBtn = document.createElement('button');
closeBtn.textContent = 'Close';
closeBtn.style.cssText = `
margin-top: 24px; padding: 8px 20px; background: #eee; color: #333;
border: none; border-radius: 8px; font-size: 1em;
font-weight: 500; cursor: pointer;
`;
closeBtn.onclick = () => {
searchModal?.remove();
searchModal = null;
searchResults = [];
};
modalContent.appendChild(closeBtn);
searchModal.appendChild(modalContent);
document.body.appendChild(searchModal);
}
Execute search using selected categories and preferences (UI omitted):
function performSearch(text: string) {
if (!map) return;
const preferences = SearchPreferences.create({
maxMatches: 40,
allowFuzzyResults: true,
searchMapPOIs: true,
searchAddresses: false,
});
selectedCategories.forEach(cat => {
preferences.landmarks?.addStoreCategoryId(cat.landmarkStoreId, cat.id);
});
SearchService.searchAroundPosition({
position: undefined as any,
preferences,
textFilter: text,
onCompleteCallback: (err: GemError, results: Landmark[]) => {
if (err !== GemError.success) {
searchResults = [];
return;
}
searchResults = results;
}
});
}
Display Search Results
Format distance and address information:
function getFormattedDistance(landmark: Landmark): string {
try {
const dist = landmark.extraInfo?.getByKey?.('gmSearchResultDistance') || 0;
const km = (dist / 1000).toFixed(1);
return `${km} km`;
} catch {
return '';
}
}
function getAddress(landmark: Landmark): string {
try {
const addressInfo = landmark.address || {};
const street = addressInfo.getField?.(AddressField.streetName) || '';
const city = addressInfo.getField?.(AddressField.city) || '';
if (!street && !city) return '';
return [street, city].filter(Boolean).join(', ');
} catch {
return '';
}
}
Select and Highlight Result
Handle result selection and map navigation:
function selectSearchResult(landmark: Landmark) {
if (!map) return;
try {
const renderSettings = new HighlightRenderSettings({
options: new Set([HighlightOptions.showLandmark])
});
try { landmark.setImageFromIcon(GemIcon.searchResultsPin); } catch(e){}
map.activateHighlight([landmark], { renderSettings });
} catch {
map.activateHighlight([landmark]);
}
if (landmark.coordinates) {
map.centerOnCoordinates(landmark.coordinates, { zoomLevel: 70 });
}
showMessage(`Selected: ${landmark.name}`);
if (window.innerWidth < 600) toggleSidebar(false);
}
Utility Functions
Display temporary messages:
function showMessage(message: string, duration = 3000) {
let msgDiv = document.getElementById('status-msg');
if (!msgDiv) {
msgDiv = document.createElement('div');
msgDiv.id = 'status-msg';
msgDiv.style.cssText = `
position: fixed; top: 20px; left: 50%; transform: translateX(-50%);
background: #333; color: #fff; padding: 12px 20px; border-radius: 8px;
z-index: 2000; font-size: 1em;
`;
document.body.appendChild(msgDiv);
}
msgDiv.textContent = message;
setTimeout(() => {
msgDiv.textContent = '';
}, duration);
}
Key Features
Category Filtering
The example uses LandmarkCategory objects to filter search results:
Category Selection:
- Display all available categories from
GenericCategories.categories
- Allow multiple category selection
- Visual feedback for selected categories
Adding Categories to Search:
selectedCategories.forEach(cat => {
preferences.landmarks.addStoreCategoryId(cat.landmarkStoreId, cat.id);
});
Search Preferences
Configure search behavior:
Parameters:
maxMatches: 40 - Limit results to 40 landmarks
allowFuzzyResults: false - Exact matching only
searchMapPOIs: true - Search points of interest
searchAddresses: false - Exclude address results
Text Filter
Combine category filtering with text search:
SearchService.searchAroundPosition({
position: coords,
preferences: preferences,
textFilter: text,
onCompleteCallback: callback
});
The textFilter parameter allows searching within the selected categories.
Map Center Coordinates
Get the center point of the current map view:
const x = container.offsetWidth / 2;
const y = container.offsetHeight / 2;
const coords = map.transformScreenToWgs({
x: Math.floor(x),
y: Math.floor(y)
});
This ensures search results are relevant to the visible area.
Multi-select UI
The category list supports multiple selections:
- Click to toggle category selection
- Visual indicator (gray background) for selected items
- State persists when modal reopens with results
Implementation Details
- Category State: Maintain
selectedCategories array across modal reopens
- Dynamic UI: Rebuild modal after each search to show results
- Error Handling: Show "No results found" message on search failure
- Result Display: Show distance and address for each landmark
- Map Integration: Highlight and center map on selected result
Use Cases
- Restaurant Search: Filter by "Restaurants" category
- Hotel Booking: Search hotels in a specific area
- Gas Stations: Find nearby fuel stations
- POI Discovery: Browse landmarks by type (museums, parks, etc.)
- Trip Planning: Combine categories (hotels + restaurants)
Next Steps