Skip to main content

Custom Position Icon

|

This example demonstrates how to customize the position tracker icon on the map using the Maps SDK for TypeScript. It shows how to load a custom PNG image to replace the default position marker and follow the user's position.

Live Demo

Overview

The example demonstrates the following features:

  • Customizing the position tracker with a custom PNG image
  • Setting scale and appearance of the position marker
  • Requesting location permissions
  • Following the user's position on the map

Code Implementation

Initialize GemKit and Setup

index.ts
import {
GemKit,
GemMap,
PositionService,
MapSceneObject,
SceneObjectFileFormat,
GemAnimation,
AnimationType
} from '@magiclane/maps-sdk';

let map: GemMap | null = null;
let hasLiveDataSource = false;

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

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

const viewId = 2;
const wrapper = gemKit.createView(viewId, onMapCreated);
if (wrapper) container.appendChild(wrapper);

// Follow Position button
//Style Code
followPositionBtn.onclick = () => onFollowPositionButtonPressed();
document.body.appendChild(followPositionBtn);
});

Load and Set Custom Position Icon

index.ts
// Helper: load an image as ArrayBuffer
async function loadImageAsArrayBuffer(url: string): Promise<Uint8Array> {
const response = await fetch(url);
if (!response.ok) throw new Error('Failed to load image: ' + url);
return await response.bytes();
}

// Set the custom icon for the position tracker
async function setPositionTrackerImage(imageUrl: string, scale = 1.0) {
try {
const imageData = await loadImageAsArrayBuffer(imageUrl);
MapSceneObject.customizeDefPositionTracker(
Array.from(imageData),
SceneObjectFileFormat.tex
);
const positionTracker = MapSceneObject.getDefPositionTracker();
positionTracker.scale = scale;
} catch (e) {
showMessage('Failed to set custom position icon: ' + e);
}
}

async function onMapCreated(gemMap: GemMap) {
map = gemMap;
// Use a PNG icon from the public folder
await setPositionTrackerImage('./navArrow.png', 0.7);
}

Request Location Permission and Follow Position

index.ts
async function onFollowPositionButtonPressed() {
// Request location permission (handled by the SDK on web)
const permission = await PositionService.requestLocationPermission();
if (!permission) {
showMessage('Location permission denied.');
return;
}

// Set live data source only once
if (!hasLiveDataSource) {
PositionService.instance.setLiveDataSource();
hasLiveDataSource = true;
}

// Optionally, set an animation for smooth camera movement
const animation = new GemAnimation({ type: AnimationType.linear });
map?.startFollowingPosition({ animation: animation, viewAngle: 0 });
showMessage('Following position...');
}

Utility Functions

index.ts
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; bottom: 40px; left: 50%; transform: translateX(-50%);
background: rgba(33, 33, 33, 0.95); color: #fff; padding: 12px 24px; border-radius: 50px;
z-index: 2000; font-size: 0.95em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
box-shadow: 0 4px 12px rgba(0,0,0,0.15); backdrop-filter: blur(4px); transition: opacity 0.3s ease; display: none;`;
document.body.appendChild(msgDiv);
}
if (message === '') {
msgDiv.style.opacity = '0';
setTimeout(() => { if (msgDiv) msgDiv.style.display = 'none'; }, 300);
} else {
msgDiv.style.display = 'block';
requestAnimationFrame(() => {
if (msgDiv) msgDiv.style.opacity = '1';
});
msgDiv.textContent = message;
setTimeout(() => {
if (msgDiv) {
msgDiv.style.opacity = '0';
setTimeout(() => {
if(msgDiv) {
msgDiv.style.display = 'none';
msgDiv.textContent = '';
}
}, 300);
}
}, duration);
}
}

function styleButton(btn: HTMLButtonElement, color: string, hoverColor: string) {
btn.style.cssText = `
position: fixed; top: 30px; left: 50%; transform: translateX(-50%);
padding: 12px 32px; background: ${color}; color: #fff; border: none; border-radius: 50px;
font-size: 16px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-weight: 600; cursor: pointer; z-index: 2000; box-shadow: 0 4px 15px ${color}66;
transition: all 0.2s ease-in-out; letter-spacing: 0.5px; display: flex; align-items: center; gap: 8px;`;
btn.onmouseenter = () => {
btn.style.transform = 'translateX(-50%) translateY(-2px)';
btn.style.boxShadow = `0 6px 20px ${color}99`;
btn.style.background = hoverColor;
};
btn.onmouseleave = () => {
btn.style.transform = 'translateX(-50%) translateY(0)';
btn.style.boxShadow = `0 4px 15px ${color}66`;
btn.style.background = color;
};
btn.onmousedown = () => {
btn.style.transform = 'translateX(-50%) translateY(1px)';
};
}

Key Features

  • Custom PNG Image: Replace the default position tracker with a custom PNG icon.
  • Scalable Icons: Adjust the size of the position marker with the scale property.
  • Asset Loading: Load external PNG images from URLs.
  • Position Tracking: Follow user's real-time location with smooth animations.
  • Permission Handling: Automatic location permission request on web platforms.

Customization Options

Scale

Adjust the size of the position tracker:

positionTracker.scale = 0.7; // 70% of original size
positionTracker.scale = 2.0; // 200% of original size

Animation

Add smooth transitions when following position:

const animation = new GemAnimation({ 
type: AnimationType.linear,
duration: 1000 // milliseconds
});
map?.startFollowingPosition({ animation });

Position Tracker Properties

Access and modify the default position tracker:

const positionTracker = MapSceneObject.getDefPositionTracker();
positionTracker.scale = 1.5;
// Additional properties can be set here

Asset Preparation

When preparing custom position icons:

  1. Size: Keep file size small for faster loading (< 1MB recommended)
  2. Format: Use PNG for best compatibility and features
  3. Orientation: Ensure the image faces the correct direction
  4. Scale: Test different scale values to find the optimal size
  5. Location: Place assets in the public folder or serve from a CDN

Error Handling

The example includes error handling for asset loading:

index.ts
try {
const imageData = await loadImageAsArrayBuffer(imageUrl);
MapSceneObject.customizeDefPositionTracker(
Array.from(imageData),
SceneObjectFileFormat.tex
);
} catch (e) {
showMessage('Failed to set custom position icon: ' + e);
}

Explanation of Key Components

  • MapSceneObject.customizeDefPositionTracker: Sets the image for the default position tracker.
  • SceneObjectFileFormat.tex: Specifies PNG image format for the position tracker.
  • GemAnimation: Used for smooth camera movement when following position.
  • PositionService.requestLocationPermission: Requests location permission from the user.

Next Steps