Dynamic Focus
Mappedin JS version 6 is currently in a beta state while Mappedin perfects new features and APIs. Open the v6 release notes to view the latest changes.
Using Mappedin JS with your own map requires a Pro license. Try a demo map for free or refer to the Pricing page for more information.
A single venue may include multiple nearby buildings, each contained within a Mappedin.FloorStack. Dynamic Focus enables seamless exploration across these buildings by revealing indoor content as the camera pans over each structure. There are two ways to implement Dynamic Focus.
1. Dynamic Focus using the Dynamic Focus Hook
The first approach is to use the Mappedin Dynamic Focus useDynamicFocus hook, which is a prebuilt solution. It provides a simple API for implementing Dynamic Focus, allowing automatic switching between building facades and building maps as they move into and out of focus when the map is panned. It will trigger the same facades-in-view-change
and floor-change
events as the Implementing Dynamic Focus using MapViewControl method, allowing the app to listen for these events and update the map view accordingly, such as showing and hiding markers or labels when focus is transitioned to a new building.
2. Dynamic Focus using MapViewControl
The second approach is to use the MapViewControl.updateState() method to update the map view when the camera pans over a building. This approach is more flexible and allows for more control over Dynamic Focus behavior, but it requires more code to implement. This can allow for custom behaviour, such as showing the the building map of different elevations. This can be useful when floors of nearby buildings don't share the same elevation, such as when level 2 of one building is aligned with level 3 of another building. It could also allow for showing and hiding markers or labels when focus is transitioned to a new building.
Mappedin Dynamic Focus with Auto Focus Enabled
Implementing the Mappedin Dynamic Focus Hook
The Mappedin Dynamic Focus hook offers a simple API for implementing Dynamic Focus. If custom behaviour is required refer to the Implementing Dynamic Focus using MapViewControl section.
Usage
The Dynamic Focus controller is instantiated with a reference to the Mappedin.MapView instance. The following example demonstrates how to create a new Dynamic Focus controller and set initial state.
const DynamicFocusComponent = () => {
const { mapData, mapView } = useMap();
const dynamicFocus = useDynamicFocus();
const isInitialized = useRef(false);
useEffect(() => {
if (mapData && mapView && !isInitialized.current) {
console.log('Initializing Dynamic Focus...');
// Initialize dynamic focus with hook
const initializeDynamicFocus = async () => {
try {
await dynamicFocus.enable({
setFloorOnFocus: true,
autoFocus: true, // Whether to automatically focus on camera move
mode: 'lock-elevation', // The mode of the Dynamic Focus controller.
indoorZoomThreshold: 18, // The zoom level at which the indoors is revealed
outdoorZoomThreshold: 17, // The zoom level at which the indoors are hidden
autoAdjustFacadeHeights: false, // Whether to automatically adjust facade heights to align with floor boundaries in multi-floor buildings.
});
console.log('Dynamic Focus initialized successfully');
isInitialized.current = true;
} catch (error) {
console.error('Failed to initialize Dynamic Focus:', error);
}
};
initializeDynamicFocus();
return () => {
dynamicFocus.destroy().catch(console.error);
};
}
}, [mapData, mapView, dynamicFocus]);
return <></>;
};
Dynamic Focus has a number of options that can be enabled either at instantiation or at any time via DynamicFocus.updateState(), which accepts a DynamicFocusState object.
Setting FloorStack Default Floor
The default floor is the Mappedin.Floor that is visible when focus is transitioned to a new Mappedin.FloorStack (building). The default Mappedin.Floor for a Mappedin.FloorStack can be set by calling DynamicFocus.setDefaultFloorForStack with the floor stack and floor ID. The default floor can be reset by calling DynamicFocus.resetDefaultFloorForStack() with the floor stack.
const dynamicFocus = useDynamicFocus();
dynamicFocus.setDefaultFloorForStack(floorStack, floor);
dynamicFocus.resetDefaultFloorForStack(floorStack);
Implementing Dynamic Focus using MapViewControl
Dynamic Focus can be implemented using the MapViewControl.updateState method. This approach is more flexible and allows for full customization of Dynamic Focus behavior, but it requires more code to implement. For a simple implementation of Dynamic Focus, refer to the Mappedin Dynamic Focus Hook section.
Implementing Dynamic Focus using MapView.updateState requires the app to switch the visibility of a building Mappedin.Facade and Mappedin.Floor when the camera pans over a building. The Mappedin.Facade represents the look of the building from the outside, with it's roof on. The Mappedin.Floor represents the look of the building from the inside, showing the indoor map.
Listen for Changes to Facades In view
The facades-in-view-change
event is emitted when the facades in view change. This event is emitted when the camera pans over a building. The event provides the list of facades in view. The app can use this event to update the visibility of the facades and floors. The first Mappedin.Facade in the array contains the one that is most centered in view.
- Declarative
- Imperative
// Act on the facades-in-view-change event to update floor visibility.
const [facadesInView, setFacadesInView] = useState<Set<string>>(new Set());
useEvent('facades-in-view-change', (event) => {
const { facades } = event;
const newFacadesInView = new Set<string>();
if (facades.length > 0) {
for (const facade of facades) {
newFacadesInView.add(facade.id);
}
const primaryFacade = facades[0];
const primaryFloor =
floorToShowByBuilding.get(primaryFacade.floorStack.id) ??
primaryFacade.floorStack.defaultFloor;
mapView.setFloor(primaryFloor);
}
setFacadesInView(newFacadesInView);
});
// Act on the facades-in-view-change event to update floor visibility.
let facadesInView = new Set<string>();
mapView.on("facades-in-view-change", (event) => {
const { facades } = event;
facadesInView.clear();
if (facades.length > 0) {
for (const facade of facades) {
facadesInView.add(facade.id);
}
const primaryFacade = facades[0];
const primaryFloor =
floorToShowByBuilding.get(primaryFacade.floorStack.id) ??
primaryFacade.floorStack.defaultFloor;
mapView.setFloor(primaryFloor);
}
});
Listen for Changes to the Floor
The floor-change
event is emitted when the floor changes, for example when the user selects a new floor from a floor selector or when they click on a navigation marker to go up or down stairs or an elevator. The event provides the new floor. The app can use this event to update the visibility of the facades and floors.
- Declarative
- Imperative
// Act on the floor-change event to update the level selector.
useEvent("floor-change", (event) => {
const { floor: newFloor } = event;
elevation = newFloor.elevation;
console.log("Elevation: " + elevation);
updateFloorsToShow();
mapData.getByType("floor-stack").forEach((fs) => {
if (fs.facade) {
if (
facadesInView.has(fs.facade.id) ||
fs.id === newFloor.floorStack.id
) {
openFacade(fs.facade);
} else {
closeFacade(fs.facade);
}
}
});
console.log(
"Floor changed to:",
event?.floor.name,
"in building:",
event?.floor.floorStack.name
);
});
// Open the building's facade and show its indoor map.
function openFacade(facade: Facade) {
if (animationsByFacade.has(facade.id)) {
animationsByFacade.get(facade.id)?.cancel();
}
// first, show the floor we want to see
showFloors(facade.floorStack);
if (mapView.getState(facade)!.opacity === 0) {
// already animated out
return;
}
// then, animate the facade out
const animation = mapView.animateState(
facade,
{
opacity: 0,
},
{
duration: animationDuration,
}
);
animationsByFacade.set(facade.id, animation);
animation.then(() => {
animationsByFacade.delete(facade.id);
});
}
// Close the building's facade and hide its indoor map.
function closeFacade(facade: Facade) {
if (animationsByFacade.has(facade.id)) {
animationsByFacade.get(facade.id)?.cancel();
}
if (mapView.getState(facade)!.opacity === 1) {
// already animated in
return;
}
// first, animate the facade in
const animation = mapView.animateState(
facade,
{
opacity: 1,
},
{
duration: animationDuration,
}
);
animationsByFacade.set(facade.id, animation);
animation.then(() => {
animationsByFacade.delete(facade.id);
// then, hide the other floors
facade.floorStack.floors.forEach((floor) => {
mapView.updateState(floor, {
visible: false,
});
});
});
}
// Act on the floor-change event to update the level selector.
mapView.on("floor-change", (event) => {
const { floor: newFloor } = event;
elevation = newFloor.elevation;
console.log("Elevation: " + elevation);
updateFloorsToShow();
mapData.getByType("floor-stack").forEach((fs) => {
if (fs.facade) {
if (
facadesInView.has(fs.facade.id) ||
fs.id === newFloor.floorStack.id
) {
openFacade(fs.facade);
} else {
closeFacade(fs.facade);
}
}
});
console.log(
"Floor changed to:",
event?.floor.name,
"in building:",
event?.floor.floorStack.name
);
});
// Open the building's facade and show its indoor map.
function openFacade(facade: Facade) {
if (animationsByFacade.has(facade.id)) {
animationsByFacade.get(facade.id)?.cancel();
}
// first, show the floor we want to see
showFloors(facade.floorStack);
if (mapView.getState(facade)!.opacity === 0) {
// already animated out
return;
}
// then, animate the facade out
const animation = mapView.animateState(
facade,
{
opacity: 0,
},
{
duration: animationDuration,
}
);
animationsByFacade.set(facade.id, animation);
animation.then(() => {
animationsByFacade.delete(facade.id);
});
}
// Close the building's facade and hide its indoor map.
function closeFacade(facade: Facade) {
if (animationsByFacade.has(facade.id)) {
animationsByFacade.get(facade.id)?.cancel();
}
if (mapView.getState(facade)!.opacity === 1) {
// already animated in
return;
}
// first, animate the facade in
const animation = mapView.animateState(
facade,
{
opacity: 1,
},
{
duration: animationDuration,
}
);
animationsByFacade.set(facade.id, animation);
animation.then(() => {
animationsByFacade.delete(facade.id);
// then, hide the other floors
facade.floorStack.floors.forEach((floor) => {
mapView.updateState(floor, {
visible: false,
});
});
});
}