Skip to main content

What Is Nearby

|

This example demonstrates how to discover all nearby landmarks around the user's current position or a default location.

Live Demo

Overview

This example shows how to:

  • Request location permissions and access current position
  • Search for nearby landmarks across all categories
  • Display results in a scrollable modal with distance information
  • Navigate and highlight landmarks on the map
  • Handle cases where location is unavailable (fallback to default position)

Code Implementation

Imports

First, import the required modules from the SDK:

import {
GemKit,
GemMap,
Coordinates,
PositionService,
SearchService,
SearchPreferences,
Landmark,
GemError,
GemAnimation,
AnimationType,
GenericCategories,
HighlightRenderSettings,
HighlightOptions,
ImageFileFormat,
GemIcon
} from '@magiclane/maps-sdk';

Setup State and Default Position

Initialize global variables and default location:

index.ts
let map: GemMap | null = null;
let hasLiveDataSource = false;
let currentPosition: Coordinates | null = null;

// Default position: Brasov, Romania
const defaultPosition = new Coordinates({
latitude: 45.6427,
longitude: 25.5887,
altitude: 0.0
});

// UI Elements
let sidebarPanel: HTMLDivElement;
let resultsContainer: HTMLDivElement;
let whatIsNearbyBtn: HTMLButtonElement;
let followBtn: HTMLButtonElement | null = null;

Request Location Permission

Get user's current location with permission handling:

index.ts
async function getCurrentLocation(): Promise<void> {
try {
const permissionGranted = await PositionService.requestLocationPermission();

if (permissionGranted) {
if (!hasLiveDataSource) {
PositionService.instance.setLiveDataSource();
hasLiveDataSource = true;
}

currentPosition = PositionService.instance.position?.coordinates || null;

if (currentPosition && map) {
const animation = new GemAnimation({ type: AnimationType.linear });
map.startFollowingPosition({ animation });
showMessage('Location access granted.');
} else {
showMessage('Waiting for position...');
}
} else {
showMessage('Location permission denied. Using default.');
}
} catch (error) {
console.error('Error getting location:', error);
showMessage('Error accessing location');
}
}

Initialize Map and UI

Setup the map view and UI elements:

index.ts
window.addEventListener('DOMContentLoaded', async () => {
const gemKit = await GemKit.initialize(GEMKIT_TOKEN);

const container = document.getElementById('map-container');
if (!container) throw new Error('Map container not found');

const viewId = 1;
const wrapper = gemKit.createView(viewId, async (gemMap: GemMap) => {
map = gemMap;
map.centerOnCoordinates(defaultPosition, { zoomLevel: 50 });
await getCurrentLocation();
});
if (wrapper) container.appendChild(wrapper);

createSidebar();

// "What's Nearby" button
whatIsNearbyBtn = document.createElement('button');
whatIsNearbyBtn.innerHTML = `${ICONS.nearby} What's Nearby`;
styleMainButton(whatIsNearbyBtn);
whatIsNearbyBtn.onclick = () => performSearch();
document.body.appendChild(whatIsNearbyBtn);

showFollowButton();
});

Search for Nearby Locations

Query all nearby landmarks across all categories:

index.ts
async function getNearbyLocations(position: Coordinates): Promise<Landmark[]> {
return new Promise((resolve) => {
try {
// Create search preferences with all categories
const preferences = SearchPreferences.create({
searchAddresses: false,
maxMatches: 50,
});

// Add all generic categories
const genericCategories = GenericCategories.categories;
if (genericCategories && preferences.landmarks) {
genericCategories.forEach((category: any) => {
if (category.landmarkStoreId && category.id) {
preferences.landmarks.addStoreCategoryId(
category.landmarkStoreId,
category.id
);
}
});
}

// Perform search around position
SearchService.searchAroundPosition({
position,
preferences,
onCompleteCallback: (err: GemError, results: Landmark[]) => {
if (err === GemError.success && results) {
resolve(results);
} else {
resolve([]);
}
}
});
} catch (error) {
console.error('Error in nearby search:', error);
resolve([]);
}
});
}

Perform Search and Display Results

Execute search and show results in a sidebar:

index.ts
async function performSearch() {
toggleSidebar(true);
resultsContainer.innerHTML = `
<div style="padding: 40px; text-align: center; color: #666;">
<div style="width: 30px; height: 30px; border: 3px solid #f3f3f3; border-top: 3px solid #673ab7; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto 16px;"></div>
Scanning area...
</div>
<style>@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }</style>
`;

const searchPosition = currentPosition || defaultPosition;
const normalizedPosition = new Coordinates({
latitude: searchPosition.latitude,
longitude: searchPosition.longitude,
altitude: 0.0
});

try {
const nearbyLandmarks = await getNearbyLocations(normalizedPosition);
renderResults(nearbyLandmarks, normalizedPosition);
} catch (e) {
resultsContainer.innerHTML = `<div style="padding: 20px; text-align: center; color: #e74c3c;">Error loading locations</div>`;
}

Render Search Results

Display landmarks in sidebar with highlight functionality:

index.ts
function renderResults(landmarks: Landmark[], currentPos: Coordinates) {
resultsContainer.innerHTML = '';

if (landmarks.length === 0) {
resultsContainer.innerHTML = `<div style="padding: 20px; text-align: center; color: #999;">No locations found nearby.</div>`;
return;
}

landmarks.forEach((landmark) => {
const item = document.createElement('div');
item.style.cssText = `
padding: 12px; margin-bottom: 8px; border: 1px solid #eee; border-radius: 8px;
cursor: pointer; transition: background 0.2s; display: flex; gap: 12px; align-items: center;
`;

// Icon / Image
const iconDiv = document.createElement('div');
iconDiv.style.cssText = `
width: 48px; height: 48px; flex-shrink: 0; display: flex;
align-items: center; justify-content: center;
background: #ede7f6; border-radius: 8px; overflow: hidden;
`;

let hasImage = false;
try {
const imageData = landmark.getImage({ width: 48, height: 48 }, ImageFileFormat.png);
if (imageData && imageData.byteLength > 0) {
const img = document.createElement('img');
const blob = new Blob([new Uint8Array(imageData.buffer as ArrayBuffer)], { type: 'image/png' });
img.src = URL.createObjectURL(blob);
img.style.cssText = 'width: 100%; height: 100%; object-fit: cover;';
iconDiv.appendChild(img);
hasImage = true;
}
} catch(e) {}

if (!hasImage) {
iconDiv.innerHTML = ICONS.pin;
}

// Text
const contentDiv = document.createElement('div');
contentDiv.style.cssText = 'flex: 1; min-width: 0;';

const name = landmark.name || (landmark.categories && landmark.categories.length > 0 ? landmark.categories[0].name : 'Unknown');
const dist = landmark.coordinates ? convertDistance(landmark.coordinates.distance(currentPos)) : '';

contentDiv.innerHTML = `
<div style="font-weight:600; color:#333; font-size:14px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">${name}</div>
<div style="color:#673ab7; font-size:12px; font-weight:500; margin-top:2px;">${dist} away</div>
`;

item.appendChild(iconDiv);
item.appendChild(contentDiv);

item.onmouseenter = () => item.style.background = '#f5f5f5';
item.onmouseleave = () => item.style.background = '#fff';

item.onclick = () => {
if (map && landmark.coordinates) {
map.centerOnCoordinates(landmark.coordinates, { zoomLevel: 70 });
try {
const renderSettings = new HighlightRenderSettings({
options: new Set([HighlightOptions.showLandmark])
});
// Ensure icon exists for highlight
try { landmark.setImageFromIcon(GemIcon.searchResultsPin); } catch(e){}
map.activateHighlight([landmark], { renderSettings });
} catch(e) {
map.activateHighlight([landmark]);
}

// Close sidebar on mobile
if (window.innerWidth < 600) toggleSidebar(false);
}
};

resultsContainer.appendChild(item);
});
}

Key Features

  • Location Permission: Request and handle location permissions using PositionService.requestLocationPermission()
  • Live Position Tracking: Set live data source and follow user position with animations
  • Comprehensive Search: Search across all landmark categories using GenericCategories.categories
  • Search Around Position: Use SearchService.searchAroundPosition() to find nearby landmarks
  • Interactive Highlights: Highlight selected landmarks on map with HighlightRenderSettings
  • Distance Calculation: Calculate and display distances using Coordinates.distance()
  • Fallback Position: Gracefully handle denied permissions with default location

Explanation of Key Components

  • PositionService: Manages location permissions and provides current user position
  • SearchPreferences: Configure search parameters including categories and maximum results
  • GenericCategories: Access all available landmark category types for comprehensive search
  • SearchService.searchAroundPosition(): Search for landmarks within radius of a position
  • HighlightRenderSettings: Control how landmarks are highlighted on the map
  • GemIcon.searchResultsPin: Set custom pin icon for highlighted landmarks

Next Steps