Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
In order to start developing your own solution, there are a number of requirements that need to be fulfilled in order to ensure a smooth development process.
The following subsections will guide you in how to get the necessary API keys for:
MapsIndoors API Key
A map engine provider, Google Maps or Mapbox
In order to include MapsIndoors in your app, you need a MapsIndoors API key. If you do not yet have access to your MapsIndoors API key, you can use the demo API key 02c329e6777d431a88480a09 to follow the guide.
If you have access to the MapsIndoors CMS, you'll find your MapsIndoors API key as described here.
Documentation on the MapsIndoors Web SDK
Unlock the power of indoor mapping with the MapsIndoors JavaScript SDK! Dive into a world where integrating indoor mapping solutions into your applications is not only possible but also incredibly efficient and customizable.
Why Choose MapsIndoors JavaScript SDK?
Comprehensive Toolkit: Seamlessly manipulate maps and interact with MapsIndoors data to meet your specific requirements.
Developer-Friendly: Utilize a range of npm-hosted web components to minimize UI creation overhead and simplify development.
Adaptability: Tailor your map to adapt to various conditions and requirements with ease.
What’s in the Box?
Multifaceted Interaction: Engage with MapsIndoors data in myriad ways.
Map Manipulation: Alter and manage the map according to your unique needs and conditions.
Web Components: Leverage pre-built components to streamline UI development.
Prefer a Ready-to-Use Solution? Consider our ! Crafted for those who'd rather leave UI and UX to the pros, this React-based application offers:
Battle-Tested Experience: Proven search and wayfinding capabilities.
Minimal Customization Needed: Especially suited for projects where quick implementation is key.
This guide will walk you through how to create your own MapsIndoors implementation from the ground up. You will gain experience with the MapsIndoors Software Development Kit (SDK) and the typical development process of using it. Furthermore, you will be able to gain an understanding of the basic concepts, tools and terminology commonly used when interacting with the SDK.
Following features are included in this Getting Started guide:
Display an interactive map with MapsIndoors
Implement search functionality to interact with the displayed map
Generate and show directions between two points on the map
Parts of this guide rely on having access to a MapsIndoors Solution. The section will guide you on how to use our Demo API key if you do not have your own.
MapsIndoors is built on top of an external map engine, either Mapbox or Google Maps.
You'll need an Access Token for your chosen map engine with the relevant APIs enabled and a MapsIndoors API key to get started building your own app. You can use either Mapbox or Google Maps as your map provider. You only need to obtain one type of token - choose the provider that best suits your needs.
Token Options
Note: While both providers support core mapping features, some specialized functionality may only be available with a specific provider. These cases will be clearly marked throughout the documentation.
To create a Mapbox Access Token, follow the steps provided in the link below:
Remember to enable relevant scopes on the Mapbox Access Token, such as:
To make sure you can use the full feature set of MapsIndoors you'll need to enable these services.
Being able to manage your own maps however and whenever you want makes you agile, independent of support and able to immediately act on your customer requests.
Customization and updating of your map is fast and easily done through the MapsIndoors CMS.\
Key features of the MapsIndoors CMS include:
Add POIs and areas, like objects, work zones, or any relevant points of interest.
Make floorplan changes
Control visibility and user roles
Update locations data\
Check out the details and explore many more CMS features in the MapsIndoors CMS section.
See the sections below for detailed setup instructions for your chosen provider:
Then you need to create your Google Maps API key by following the link below:
If you apply restrictions to your key, remember to include the Google services listed above.
Product Overview
CMS
Map Template
Design
Glossary
Changelog






Getting your map visualization right means going beyond simple floorplan representation and elevating the map to a rich and user-friendly indoor mapping experience. \
The design and customization of your map visualization are crucial for creating a user-friendly, visually engaging, and functionally relevant mapping experience. The map visualization is your foundation for effective navigation, information accessibility, and overall user satisfaction. \
In MapsIndoors, key aspects of map visualization cover:
Real-World Geospatial Visualization
MapsIndoors enables the visualization of venue details, building structures, floor plans, rooms, and related data on georeferenced Google or Mapbox maps. This real-world geospatial context enhances the accuracy and relevance of the indoor map.
Map Design
Users can customize the appearance of their maps to align with branding and specific use cases. Whether in 2D or 3D, the ability to design the map's look and feel provides flexibility and coherence with organizational aesthetics.
Dynamic Maps
MapsIndoors supports dynamic maps that can display relevant content based on users, use cases, or zoom levels. This adaptability ensures that users receive the most pertinent information based on their context and needs.
Map Interaction
The platform includes built-in features for map interaction, such as a floor selector, zoom, pan, tilt, and search functionalities. These features make it easy for users to navigate and interact with maps in ways that are relevant to their specific requirements.\
Whether you’re building your map solution on Mapbox or Google Maps, we’ve got your back. Follow the guides and you’ll soon be up and running.
Wayfinding is a critical component of your indoor map solution enabling efficient route calculations and optimal navigation within indoor spaces.\
MapsIndoors’ wayfinding functionality is a comprehensive solution that seamlessly integrates local and global maps, considers various travel modes, utilizes entry points for smooth transitions, and breaks down routes into logical legs and steps, ultimately enhancing navigation and user satisfaction.\
Key elements to dig into when enabling wayfinding in your MapsIndoors solution are:
Outdoor to indoor navigation Turn-by-turn directions visualized on the map with estimated travel time and detailed descriptions.
Venue to Venue wayfinding
When you're getting routes between two of your own MapsIndoors venues and may require using public routes from Google Maps or Mapbox to get there.
Dynamic routes Automated route updates based on obstacles like furniture and changing floor plan layout.
Personalized routes User profiles allow for personalized guidance, based on what the user is allowed to see and where they are allowed to navigate. Getting the right people to the right destinations.
Accessible wayfinding Allow users with disabilities to effortlessly navigate your building by avoiding stairs and using elevators and ramps for navigation.
\
All the guidance is here for you. Happy wayfinding!
Documentation reflects the latest version.
You can find documentation on legacy versions here:
Legacy DocsData visualization in the form of displaying external dynamic data within your indoor map is crucial for enhancing the map's utility, relevance, real-time applicability and not least for customizing it to fit your specific use cases. \
MapsIndoor’s data visualization functionality is built to ensure a unified, up-to-date, and contextually relevant mapping experience for users. \
By altering your map data visualization dynamically, you will create a use-case-specific digital twin that only shows information that is relevant for solving your users’ needs.\
Integrate everything from live data sensors, calendar booking systems, ERP systems, and more to MapsIndoors.\
Follow our guides to customise your maps with the data integrations you need.
Due to some slight differences in how the Web SDK handles Buildings and Venues compared to the Mobile SDKs, Buildings and Venues are treated as Locations, and as such, will be displayed with Labels. This is not always desirable behavior, and thus we also provide this small code snippet to remove them again.
mapsIndoorsInstance.setDisplayRule(['MI_BUILDING', 'MI_VENUE'], { visible: false });MI_BUILDING and MI_VENUE are special Location Types used specifically for this purpose, to set Display Rules for Buildings and Venues.
Enhance your application's search capabilities with MapsIndoors data. The following topics offer a brief introduction to various functionalities you can implement. For in-depth guidance, click on the specific topics.
Retrieve Specific Location: Use to obtain individual location objects.
Querying Locations: Perform broader searches with .
Filter Customization: Fine-tune your searches with specialized query parameters.
Using External IDs: Translate your unique IDs to MapsIndoors IDs using .
Advanced Search Integration: Integrate MapsIndoors data into your existing search functionalities.
Distance Matrix: Create a distance matrix when dealing with multiple locations.
Utilizing MapsIndoors web components
Components: MapsIndoors search component, list component, etc.
Click Event Handling: Interactive retrieval of location details.
If you use Mapbox as your map engine, it provides the option of creating a heatmap as an alternative way to display datapoints on your map. The MapsIndoors SDK also provides support for integrating this heatmap into the MapsIndoors layers.
This guide will focus on how to display the heatmap layer in a visually pleasing way in your MapsIndoors solution. In order to actually create your heatmap, please refer to the documentation from Mapbox.
Creating a heatmap with Mapbox:
Creating a heatmap layer with Mapbox:
Once you have created your heatmaps with Mapbox, you will need a way to get it to work with the layers that MapsIndoors already applies to your Mapbox map. By following the guides above, you should be able to simply overlay it on top of everything. But in order for it to be integrated more seamlessly in the MapsIndoors layers, you could also choose to insert it between the tile layer and the layer containing the area polygons.
In order to insert a heatmap between layers on the web SDK, refer to the . Then use the following code snippet but replace map with your Mapbox instance, and insert the relevant parameters from the API Reference:
In order to access certain MapsIndoors apps, and certain custom apps, SSO (Single Sign-On) login can be used. This includes the MapsIndoors Auth SSO, as well as organization-specific authentication providers - see Configuration.
The MapsIndoors Auth SSO page is located at https://auth.mapsindoors.com/login.
MapsIndoors has the following apps where SSO can be used to sign in:
MapsIndoors CMS
MapsIndoors Standard Web App
MapsIndoors Standard Web Kiosk
Unless an organization-specific provider is used, it is only possible to sign in for users invited orcreated via MapsIndoors CMS. Once a user exists in MapsIndoors, it is possible to sign in via username/password, or public authentication providers such as:
Azure Active Directory (pending)
This is only authentication - authorization is still based on the user configuration in MapsIndoors. This also means that the security is extremely tight. MapsIndoors does not gain any access to the authentication provider, as it only expects an id_token (as opposed to an access token) in order to authenticate. Based on this external authentication, and then internal authorization, an internal access token is granted to the app - which is completely unrelated to the authentication provider and therefore only can be used to access MapsIndoors services. If an organization-specific authentication provider is used, then there are more options in regards to authorization - see .
The language of MapsIndoors is independent of the chosen language on the device on which the app is used. This means that you need to explicitly tell MapsIndoors which language to use.
If you do not specify a language, MapsIndoors will show information in the default language defined in the MapsIndoors CMS. Likewise, if you specify a language that is not supported, MapsIndoors will also show information in the default language.
Additionally, aside from methods mentioned here, you can provide translations via the standard method for your device, such as using individual localized strings.
To provide multiple languages for items in the MapsIndoors CMS, such as "Meeting Room" or "Restroom", the translation must be provided by the user in the CMS. A translation can be provided in any language. In order to add support for additional languages that we currently do not support, please contact your MapsIndoors representative, and we will enable you to add translations in your desired language.
Once your language of choice has been created, you can add the translation by clicking on any POI, which will open a menu on the left side of the screen. Here, you will see the following menu point, where you can enter translations for the languages you wish. If a field is left empty, the fallback language is English. In the example below, English (en) and Danish (da) are the enabled languages.
The web-app will automatically adjust the language to the language set in the user's browser settings, otherwise default to English. When using Safari, the device's language setting will be used. This is limited to the following languages, and will default to English if the selected language is not supported:
English
Danish
Spanish
Portuguese
Are your building and venue labels disrupting your user experience?
Traditionally, both Buildings and Venues are treated as Locations in the Web SDK, inherently displaying Labels which might not always align with the desired user experience. A conventional method to manage their visibility is illustrated below via a
It can be a best practice to set this display rule after initializing your MapsIndoors instance.
Here, MI_BUILDING and MI_VENUE are specific Location Types dedicated to facilitate display rule settings for Buildings and Venues respectively.
Enhancing User Interaction with Adaptive Zoom-Level Popups
Let's say a user zooms out to get a broad overview of a large campus or city area. At a lower zoom level, the display becomes less cluttered, paving the way for a more clear, bird's eye view of the venues available. Here’s where dynamic, zoom-dependent
Handling Label Collisions in MapsIndoors SDK with Mapbox
Overview
When utilizing the MapsIndoors SDK, managing label collisions—particularly at high zoom levels—can be crucial to maintain a clean and informative map display. The SDK's collision detection may sometimes hide labels of Points of Interest (POIs) that are situated closely together, to avoid visual clutter and overlap. This guide introduces a method to manually control label visibility at specific zoom levels.
Why It Matters
Visibility vs Clarity: Ensuring labels are always visible might be vital for certain use cases or specific POIs. However, enabling labels to overlap can disrupt map clarity and user experience.
As a key element in the MapsIndoors platform, we offer APIs for efficiently calculating and displaying the most optimal routes from anywhere in the world to any Location in MapsIndoors. In the case of travelling inside a Venue, this calculation can be done on a local map provided by MapsIndoors. In the case of travelling between Venues or from outdoors to indoors, MapsIndoors provides a seamless journey outline from a specified Origin through automatically selected Entry Points at the edge of your Venues to the specified destination. See illustration below:
In order to provide a route between Venues, MapsIndoors integrates with external and global map engines (Mapbox and Google Maps).
The central components that utilize a Directions experience is the
Searching the map and making sure you are able to find what you need is crucial for users navigating complex indoor spaces. Search functionality on an indoor map is a critical component for improving navigation, efficiency, and overall user satisfaction.\
Depending on your solution and use case, search can come into play in different ways. It could be specific locations, points of interest, or services within the indoor space, like enabling users to search for a meeting room, a desk, a booth, a coffee, an elevator, anything.
In MapsIndoors, searching is a crucial element of user interaction, allowing users to discover locations and filter map displays for a tailored experience. The search functionality covers all MapsIndoors geodata, and customization options are available to create a search experience that aligns with your specific use case.\
Key features of the searching functionality include:
Filters for Precision
Often you may want to get directions starting from a user's actual current position instead of from another fixed Location. The following code snippet gives an example on how to implement this.
Further details on how user positioning works, and how to display it, can be found .
This results in directions queries originating from the user's current location.
MapsIndoors web components are intended to take advantage of pre-designed and already integrated HTML elements which will greatly reduce the complexity of your user-interface design and help minimize the amount of MapsIndoors specific SDK implementation code you'll need to write.
While the MapsIndoors components will not cover every full user experience, it will allow you to implement MapsIndoors more efficiently.
To learn more about each component, please see the component specific documentation at
Display Rules for the Web SDK v4
There are two ways to change the appearance of the map content in MapsIndoors:
Using Display Rules.
Using Google Maps or Mapbox styling.
Each has its own purpose which will be explained below.
To get an overview of what Display Rules are and can be used for, read the page first.
In this tutorial, you will build your own app from the ground up, gaining experience with the typical development process when working with the MapsIndoors Software Development Kit (SDK). Furthermore, you should be able to gain an understanding of the basic concepts, tools and terminology commonly used when interacting with the SDK. The goal of this guide is to enable you to develop a basic app, that is able to display a map, and incorporate a few basic features such as searching, directions and Live Data integration.
Parts of this guide rely on having access to a MapsIndoors Solution which supports Live Data Integration. If you do not have access to this through your own Solution, we recommend using our demo API key to access one: mapspeople3d.
This article assumes that you have already read the
In this article, we will discuss how to synchronize data for a subset of venues in MapsIndoors. There are multiple benefits to doing this, including performance and control over which data the user can search for.
Synchronizing data for a subset of venues reduces the time it takes to load the map, compared to synchronizing data for all venues. It also enables you to control which data the user can search for. For example, you may only want to synchronize data for venues located in a specific region.
To do this, you must not provide the MapsIndoors API key in the URL when loading the SDK. Instead, you set the MapsIndoors API key by calling the mapsindoors.MapsIndoors.setMapsIndoorsApiKey('MAPSINDOORS_API_KEY'); method before creating the MapsIndoors instance.
Here is an example of how to set which venues to synchronize data for, and then setting the MapsIndoors API key, to start the synchronization:
This will now synchronize data exclusively for these two venues, and only those venues.
In order to show a user's position ("Blue Dot") on an indoor map with MapsIndoors, a Position Provider must be implemented. You can integrate a 3rd party positioning provider (IPS) to create this experience. For you the developer, this means that the MapsIndoors SDK offers an interface for such a Position Provider, but no building blocks for acquiring positioning.
This guide will show you how to use a third party positioning provider and implement it into your MapsIndoors application. Here we will start from the finished Getting Started app. This code can be found here: .
A full implementation of three different third party positioning providers can be found here
When using the MapsIndoors SDK, the system for detecting collisions will sometimes, at high zoom levels, result in the Labels of POI's that are close together being hidden, no matter what you do. Here we present a small workaround, so you can disable collisions for specific zoom levels.
Please note that on Web it is only possible to do this when using Mapbox as a map provider.
This is accomplished by checking if there is a zoom_changed event, and if there is, enabling or disabling the text-allow-overlap depending on the zoom levels.
This documentation refers to the change of the Mapbox engine in 4.5.0
With the release of Mapbox V11, we are migrating the V4 SDK to use Mapbox v11 to make use of the new features offered by Mapbox. As this version bump can include breaking changes for your implementation. We will still be maintaining a V10 based SDK. This will retain the maven naming as of now. Where Mapbox V11 will move to:
For the migration of your Mapbox code implementation read the Mapbox guide here:
With MapsIndoors, you have a range of options for controlling how content is styled on the map, and how your users can interact with it. This guide takes you through some of these options, to help you find the best approach.
The options you have for controlling visibility and rendering on the map are the following:
Setting "Active From" and "Active To" dates
Search Result Ranking
Displaying Search Results
Clearing Filters
List Presentation
Overall, the search mechanism in MapsIndoors prioritizes proximity, text matching, and geodata type to provide users with relevant and ranked results. The flexibility to customize the search experience and the ability to present results in both map and list formats are built to contribute to a user-friendly and adaptable indoor mapping solution.
We’ve lined up all the guidance you need for getting the best out of MapsIndoors’ search functionality. Enjoy.
Setting Restrictions to "Closed for all"
Display Rules: Set Visibility and Opacity values
They generally fall into two categories:
The data is sent to the SDK, but not rendered on the map
The data is not sent to the SDK
In the first case, you can change the data to become not visible. It works like this:
In the Display Rule, you can set the "General Visibility" to false, and nothing of the Location will render; neither the icon, nor the label, polygon, 2D Model, 3D Walls, 3D Extrusions or 3D Models (all of these parts of the Location's Display Rule can of course also be controlled individually)
At runtime, either based on user input or other logic in your app, you can change the visilbity to true, rendering it visible on the map
For the second case, the SDK effectively doesn't even know it exists:
If you set an Active From and Active To date, and request the data outside of that date range, the data is not sent to the SDK
If you set the Restriction on a Location to "Closed for all", or an App User Role that you then don't request the Solution data with, it's also not available to the SDK
Besides the rendering options related to Display Rules and Restrictions, you can also control whether a Location is "Selectable" and "Searchable".
Setting a Location to be Not Selectable, it is still rendered on the map with all of the visible parts of it, but you can not select it to see more details and get directions to it. In practice, you would likely use "Not Selectable" for office decoration like plants or a statue — elements that help create a great-looking environment on your map.
All of these Locations should also have Searchable turned off, given they are for decoration purposes. In practice, you can set them to be Searchable, but you end up with a list of search results full of Locations you should not navigate to. Turn off "Searchable" on your "Not Selectable" Locations to avoid problems.
Other times, you want to keep the Locations selectable on the map, but maybe not take up space in the search result lists. For those cases, turn off Searchable.
Display an interactive map with MapsIndoors
Implementing search functionality to interact with the displayed map
Generate and show directions between two points on the map
Display one type of Live Data on the map
Italian
French

Entry Points are specified points in a MapsIndoors Venue that enable a transition between a global or regional map and the local map in MapsIndoors. The Entry Point often specifies which travel modes are suitable for entering/exiting the Venue. There are four travel modes: Walking, Bicycling, Driving and Transit (Public Transportation). As such, the Entry Point may be a bike shed for the Bicycling travel mode, a carpark for Driving and a bus stop for Transit. As a consequence, it is often at the Entry Point that the Travel Mode changes from Bicycling, Driving or Transit to Walking. The selection of an Entry Point for transitioning between route networks is based on a combination of automatic calculation, estimation and optimization.
When requesting a route with MapsIndoors Directions Service, the Route model in MapsIndoors is separated into Legs, and these Legs are again separated into Steps.
A leg represents a logical subset of the journey from Origin to Destination. A Route will break into legs when:
Travelling from one floor level to another.
Changing context, such as entering or exiting a building.
Changing travel mode, for example parking your car and continuing by foot.
If you examine the illustration above, you will see that the purple line representing the Route has been marked with purple circles where the Route would be separated into legs.
A Route Step can have different representations depending on where on a Route it is placed. A Step may represent yet another subset of the journey within a leg. Furthermore, it may represent a required action and/or manoeuvre, such as traversing floors, changing directions (left, right, etc.). A step will also contain textual instructions. Examples include “Make a right turn”, “Continue straight ahead”, “Take the elevator to Floor 4” and the like.

// Setting styling options to the route path.
miDirectionsRendererInstance.setOptions({
strokeColor: '#bada55',
strokeWeight: 10,
});
// Latitude and longitude position of the route destination.
// again, you'll likely want to retrieve this from a location
// location.properties.anchor.coordinates, and then access the array directly.
const routeDestination = {
lat: 57.058230237700194,
lng: 9.951134229974498
};
// enableHighAccuracy is a boolean value that indicates that the application would like to receive the best possible results.
// timeout represents the maximum length of time (milliseconds) the device is allowed to take in order to return a position.
const options = {
enableHighAccuracy: true,
timeout: 5000
}
// Creates origin with the users latitude and longitude. Then sets the route from this position to the route destination.
function getRoute(pos) {
const coords = pos.coords;
const origin = {
lat: coords.latitude,
lng: coords.longitude
}
directionsServiceInstance.getRoute({ origin: origin, destination: routeDestination }).then(function (res) {
if (res === undefined) {
console.log('Error: Route is undefined.')
} else {
console.log('Route', res);
miDirectionsRendererInstance.setRoute(res);
}
});
}
// Error handling function.
function errorHandler(err) {
console.log('Error: ', err)
}
// Gets users device current position and sets route with additional options. If any errors, errorHandler will trigger.
navigator.geolocation.getCurrentPosition(getRoute, errorHandler, options);mapView.on('zoom_changed', () => {
if (mi.getZoom() < mi.getMaxZoom() - 1) {
mi.getMap().setLayoutProperty('MI_POINT_LAYER', 'text-allow-overlap', true);
} else {
mi.getMap().setLayoutProperty('MI_POINT_LAYER', 'text-allow-overlap', false);
}
});While no breaking changes are introduced from the Mapsindoors SDK. Some visual changes are introduced with the use of Mapbox V11.
With Mapbox V11 they have introduced a new map style. That is being used by the Mapsindoors SDK. This includes new 3D visuals on the map, like extruded buildings, 3D landmarks, Trees etc. Giving a great aesthetic experience.
With this, if you are using the Mapsindoors default style this will be a part of your map. We have introduced a Mapsindoors transition level, that enable/disable the extruded buildings and Mapbox POI's to not clash with the Mapsindoors data. This is based on zoom level, and is by default 17. It can be configured through the MPMapConfig.
With the Mapbox V11 release, android also supports rendering 3D models on your map. Read more about this:
With 4.5.0 you can now also hide specific features, allowing you to toggle between 2d and 3d.
You can read about that feature here: Enabling and Disabling features on the map
Often you may want to get directions starting from a user's actual current position, instead of from another fixed Location. The following code snippet gives an example on how to implement this.
To query for a route, create a MPPoint from the Latitude, longitude and the z-index of the user, and use that on the DirectionsService.query function, like this:
val directionsService = MPDirectionsService(mContext)
//Create an Origin MPPoint with the users latitude, longitude and Z-index. If no Z-index is available just use 0.0
val origin = MPPoint(userLatitude, userLongitude, userZIndex)
val destination = destinationLocation.getPoint()
directionsService.setRouteResultListener { route, error ->
//Handle the route result here
}
directionsService.query(origin, destination)userLatitude, userLongitude. userZIndex and destinationLocation are all placeholder variable names where you insert your data.
Further details on how user positioning works, and how to display it, can be found here.
This results in directions queries originating from the user's current location.a
Integrating the following code snippet enables the application to listen for changes in zoom level, and depending on the zoom threshold, dynamically fetch venues and exhibit crisp, stylish popups at each venue’s anchor point:
What does this achieve?
When the map zoom level is below 19, unobtrusive popups appear, providing users with a quick overview of each venue’s name. Upon zooming in beyond the level 19, the popups discreetly vanish, preventing any visual obstruction and facilitating an unhindered exploration of the MapsIndoors map.

To remove the synchronized data for one or more venues:
// Add one or more venues to the list of venues to be synchronized.
mapsindoors.MapsIndoors.addVenuesToSync('FirstVenueId');
// Or
mapsindoors.MapsIndoors.addVenuesToSync(['FirstVenueId', 'SecondVenueId']);
// Set the MapsIndoors API key.
mapsindoors.MapsIndoors.setMapsIndoorsApiKey('MAPSINDOORS_API_KEY');// Add one or more venues to the list of venues to be synchronized.
mapsindoors.MapsIndoors.addVenuesToSync('ThirdVenueId');
// Or
mapsindoors.MapsIndoors.addVenuesToSync(['ThirdVenueId', 'FourthVenueId']);// Remove one or more venues from the list of venues to be synchronized.
mapsindoors.MapsIndoors.removeVenuesToSync('FirstVenueId');
// Or
mapsindoors.MapsIndoors.removeVenuesToSync(['FirstVenueId', 'ThridVenueId']);map.addLayer({....}, 'MI_POLYGON_LAYER');implementation("com.mapspeople.mapsindoors:mapbox-v11:4.8.5")mapsIndoorsInstance.setDisplayRule(['MI_BUILDING', 'MI_VENUE'], { visible: false });let popups = [];
mapsIndoorsInstance.addListener('zoom_changed', (zoomLevel) => {
if (zoomLevel < 19) {
mapsindoors.services.VenuesService.getVenues().then(venues => {
venues.forEach(venue => {
const anchor = venue.anchor.coordinates;
const popup = new mapboxgl.Popup({ closeOnClick: true, closeButton: false, className: 'custom-popup' })
.setLngLat([anchor[0], anchor[1]])
.setHTML(`<h2>${venue.name}</h2>`)
.addTo(mapsIndoorsInstance.getMap());
popups.push(popup);
});
});
} else {
popups.forEach(popup => popup.remove());
popups = [];
}
});let selectedPOI;
mapsIndoors.addListener("click", function (poi) {
if (selectedPOI) {
mapsIndoors.setDisplayRule(selectedPOI.id, null);
}
mapsIndoors.setDisplayRule(poi.id, {
iconSize: { width: 30, height: 30 },
});
selectedPOI = poi;
});mapsIndoors.setDisplayRule('PRINTER', {
label: "{{ "Printer: {{ name " }}}}"
});mapsIndoors.setDisplayRule('c66dccd480624c428ea5b78d', {
label: "{{ "Printer: {{ name " }}}}"
});mapsIndoors.setDisplayRule(['c66dccd480624c428ea5b780', 'c66dccd480624c428ea5b79c','c66dccd480624c428ea5b76a', ...], {
icon: "https://app.mapsindoors.com/mapsindoors/cms/assets/icons/building-icons/printer.png"
});mapsIndoors.setDisplayRule('PRINTER', null);mapsIndoors.setDisplayRule('c66dccd480624c428ea5b78d', null);mapsIndoors.setDisplayRule(['c66dccd480624c428ea5b780', 'c66dccd480624c428ea5b79c','c66dccd480624c428ea5b76a', ...], null);Zoom Level Sensitivity: At high zoom levels, the preference might lean towards always displaying labels, despite close proximity to others.
Limitations
Map Provider Dependency: This workaround for web is only available on Mapbox. For Mobile, this approach can be done on both Google Maps and Mapbox. Please see relevant mobile documentation for more details on implementation.
Global Application: The approach impacts all labels in MapsIndoors. For granular control over specific labels, consider employing display rules via the MapsIndoors CMS or the SDK as you normally handle most use-cases.
Cost-Benefit Analysis
Cost: Enabling overlap can result in labels obscuring each other, potentially hindering readability and aesthetic appeal, especially at low zoom levels like zoom 17, 18, 19.
Benefit: Guarantees label visibility at all times, especially at higher zoom levels, ensuring crucial information is always displayed, which might make sense at zoom levels 21, 22, 23, 24, 25 (we support zoom levels up to 25 with Mapbox).
Implementation Example
The following example demonstrates how to manage label collisions by enabling or disabling the text-allow-overlap property based on zoom levels.
Explanation
zoom_changed listener: Detects changes in zoom level, triggering the condition check against the current zoom level.
setLayoutProperty: Adjusts the text-allow-overlap property of the 'MI_POINT_LAYER' to either allow or prevent label overlap, depending on the zoom condition.
Optional Conditional Check: Compares the current zoom level against the maximum permissible zoom level (retrieved via mapsIndoorsInstance.getMaxZoom() ) to determine label rendering behavior.
This guide assumes you have a basic understanding of HTML, CSS, and JavaScript. If you're new to web development, we recommend using an Integrated Development Environment (IDE) like Visual Studio Code to help manage your files and code.
Let's start by creating the necessary file structure:
Create a new project folder: Choose any location on your computer. Let's call this folder mapsindoors-tutorial. Ensure the folder is empty.
Create three empty files inside your mapsindoors-tutorial folder:
index.html: This file will be your application's entry point and contain the main HTML structure.
style.css: This file will contain the CSS styles for the HTML document.
script.js: This file will contain the JavaScript code for initializing and controlling the MapsIndoors map.
Now, open the index.html file in your code editor and add the following basic HTML structure. This includes the necessary boilerplate and links to the MapsIndoors Web SDK, your custom CSS, and your custom JavaScript file:
Next, open the style.css file and add the following styles. These styles ensure the map container can fill the page correctly.
Explanation:
We've set up a standard HTML5 document (index.html).
The <meta> tags ensure proper character display and responsiveness.
The <title> tag sets the title for the browser tab.
Note: In this documentation, we indicate which file a code snippet belongs to by showing the filename on the first line in the code samples.
You have now created the basic project structure and included the MapsIndoors SDK and your custom CSS file.
External business POIs from the base map can clutter up your map with irrelevant content. This article describes how to hide unwanted features from the base map - and how to change the colours to better match your branding guidelines.
With Google Maps there are two methods to style the base map. Both style methods can be used across JavaScript, iOS and Android for a consistent look across platforms and apps.
Google Maps Cloud Styling
Cloud-based maps styling makes it easy to style, customize, and manage your maps using the Google Cloud Console, letting you create a customized map experience for your users without having to update your apps' code each time you make a style change.
Warning - paid feature: Functionality accessed by adding a map ID triggers a map load charged against the Dynamic Maps SKU for Android and iOS. See for more information. Use of Cloud Styling with JavaScript does not add any additional cost.
Follow the guidelines linked below to create a Style and associate it with a map ID.
To use a map ID with MapsIndoors, simply add the mapId parameter alongside other options in
Google Maps JSON Style (legacy method)
Use a JSON style declaration to control both colours and visual display of features like roads, parks and other points of interest. You can also hide features entirely.
The Styling Wizard can be an easy way to generate a JSON style:
To use a JSON style with MapsIndoors, simply add the styles array in . Here is an example hiding all Google-supplied POIs:
Starting with the V4 versions of our SDKs, MapsIndoors has introduced the functionality to have your map viewed in 3D, with the appropriate navigational features, rather than just a flat 2D image.
Regardless of your app platform, you need to use the Mapbox map provider in order to use this functionality. You also need to be using V4 of our MapsIndoors SDK's, the specific minimum version depending on the platform.
Android SDK v4.0.0
Web SDK v4.18.0
iOS SDK v4.2.6
If you have fulfilled the above requirements, you can contact your MapsPeople representative to have the 3D map functionality enabled for your Solution(s).
3D indoor mapping solutions, such as MapsIndoors, can provide companies with a multitude of benefits by creating a detailed and interactive representation of their indoor spaces. One of the primary advantages of using 3D maps is the ability to enhance the overall user experience for employees, visitors, and customers. These interactive maps can help users effortlessly navigate complex facilities such as corporate campuses, shopping malls, airports, or hospitals. By offering a user-friendly interface and intuitive visual aids, 3D maps can facilitate wayfinding, reduce confusion, and improve overall satisfaction for anyone interacting with the indoor environment.
In addition to improving user experience, 3D indoor mapping solutions like MapsIndoors can significantly streamline various operations within a company, leading to increased efficiency and cost savings. With the integration of real-time data, facility managers can optimize space utilization according to the needs of the company. Furthermore, 3D maps can be integrated with other select enterprise systems supported by MapsIndoors, such as asset tracking and user positioning, to create a comprehensive and connected ecosystem that enhances the overall functionality and productivity of the organization. The value of using 3D maps for indoor mapping solutions is multifaceted and can ultimately contribute to a more efficient, user-friendly, and well-managed working environment.
While exact best practices will depend on your specific solution and implementation, we can provide some general pointers of things to consider when developing your solution to work with MapsIndoors 3D maps. Most of these will be specifically pertaining to the inclusion of 3D models in your solution, not to utilising 3D walls and 3D room extrusions.
Use .glb file format
Ideally 25-100kb size. You can go higher if you want, just be aware that it impacts load time.
Keep your models as low-poly as possible.
Consider whether all your locations need a 3D model.
From Mapbox v3 (Web) and v11 (Mobile), it's now possible to control what features (layers) should be shown or hidden at runtime. We are introducing three new methods to enhance the control and visibility of features on your Mapbox map. With these methods, you can easily manipulate the visibility of specific features, enhancing the user experience and providing more control over map customization. As an example, you could easily change between a 2D and a 3D map at runtime, without reloading the map data.
hideFeatures(features: string[]): voidThis method allows you to hide specific Mapbox features by passing an array of feature identifiers. Once hidden, the features will no longer be visible on the map. This can be useful for scenarios where certain map elements need to be temporarily removed from view.
Parameters
features (Array): An array of FeatureType objects representing the features to be hidden.
Switching between 2D and 3D features example:
Example when calling hide3DFeatures() function:
Example when calling hide2DFeatures() function:
getHiddenFeatures(): string[]This method retrieves the currently hidden features on the map. It returns an array of feature identifiers representing the features that are currently not visible.
Returns
string[]: An array of strings representing the identifiers of the currently hidden features.
Example
FeatureType(): string[]This method retrieves all the available feature identifiers that can have their visibility set to "none". It returns an array of these feature identifiers, providing developers with an overview of the features that can be hidden.
Returns
string[]: An array of strings representing the identifiers of the features that can be hidden.
Example
NOTE: MapboxFeatures() method is deprecated. It still works as expected but will be removed within next major release.
You now have powerful tools to control the visibility of features on your maps. By utilizing the hideFeatures, FeatureType, and getHiddenFeatures methods, you can enhance user experience and customize map displays according to your needs.
See example of the implementation .
What a user can see and do is by default controlled in the MapsIndoors CMS. When signing in with a username and password, or via one of the public authentication providers, authorization will be determined by the user configuration.
If an organization-specific authentication server is configured and used for signing in, there are more possibilities. Similar to the login methods mentioned above, authorization will by default be determined based on the MapsIndoors user configuration. However, if a user that can sign in via the authentication server, but does not exist in MapsIndoors, it will have its authorization determined via the authentication server. This will be done via OAuth claims that can be found on the id_token (or via the userinfo endpoint upon authentication). If no claims are provided, the user will still get read access to the solutions associated with the authentication provider. If claims are provided, they will be mapped to MapsIndoors access definitions, so that authorization can occur based on what claims are associated with the user in the authentication server.
There is a default mapping that will occur if claims are provided in the following format:
"custom:maps_access": [
{
"objectId": "012345678901234567891234",
"objectType": "dataset",
"role": "editor"
},
...
]There are three types of roles: admin, editor, and viewer. Authorization can be given on two levels: organization and dataset. A valid MapsIndoors ID must be provided as ObjectId. The claim allows for more than one access definition.
If a different mapping is needed - possibly due to reuse of existing claims, or limitations in the authentication server, this will also be possible. It will, however, require some additional configuration done by MapsPeople.
Let's take a look at the events that MapsIndoors offers and how to utilize them.
Events are actions or occurrences that happen in the system you are programming, which the system tells you about so you can respond to them in some way if desired. -- MDN web docs
For example, if the user clicks on a Location on the map, then you can react to that action by presenting the user with additional info about the Location.
A code example is shown in the JSFiddle below, but will be run through bit by bit in this guide.
The ready event will be fired when MapsIndoors is done initializing and is ready to interact.
The building_changed event will be fired when the map is moved around and a new Building comes in focus.
This is also related to the Floor Selector which will update its view to show the Floors of the current Building.
The event handler is called with a object representing the building in focus.
The floor_changed event will be fired when the Floor is changed; either by clicking the Floor Selector or by calling setFloor() on the MapsIndoors instance.
The event handler is called with the Floor Index of the current Floor.
The click event will fire when the user clicks on a Location on the map.
The event handler is called with a object representing the Location clicked.
MapsIndoors languages are independent of the chosen languages on the device on which the app is used. This means that you need to explicitly tell MapsIndoors which language to use. The example below shows the 'Languages' section and drop-down from which you can select languages:
When a solution has only one language, that language will be the default one. You are able to add as many languages as you want to and choose which one should be the default one. Adding German and Danish languages:
Setting German as default language:
If you do not specify a language, MapsIndoors will show information in the default language defined in the MapsIndoors CMS. Likewise, if you specify a language that is not supported, MapsIndoors will also show information in the default language.
Additionally, aside from methods mentioned here, you can provide translations via the standard method for your device, such as using individual localised strings.
To provide multiple languages for items in the MapsIndoors CMS, such as "Meeting Room" or "Restroom", the translation must be provided by the user in the CMS. A translation can be provided in any language that is defined for this specific solution. In order to add support for additional languages that we currently do not support, please contact your MapsIndoors representative, and we will enable you to add translations in your desired language.
Once your language of choice has been added as a supported language, you can add the translation by clicking on any POI, which will open a menu on the left side of the screen. Here, you will see the following menu point, where you can enter translations for the languages you wish. If a field is left empty, the fallback language is the default one. In the example below, English (en) and Danish (da) are the enabled languages:
The MapsIndoors language can be aligned with the device language by supplying the current language code of the device.
The web-app will automatically adjust the language to the language set in the user's browser settings, otherwise default to English. When using Safari, the device's language setting will be used. This is limited to the following languages, and will default to English if the selected language is not supported:
English
Danish
Spanish
Portuguese
You can enable 2-Factor Authentication (2FA) in the MapsIndoors CMS. Click the avatar in the top-right corner, and click on "Settings". Follow the instructions to enable 2FA.
After you've enabled 2FA, the page will show instructions on how to disable 2FA again.
If you lose access to your account and are unable to use 2FA for any reason, please , and we'll do our best to find a solution.
MapsIndoors provides support for both Mapbox GL JS v2 and v3. Our commitment to staying at the forefront of mapping technologies ensure that you have the flexibility to choose the Mapbox version that best align with your requirements.
In order to access Mapbox v3 and its options, use mapView:
For Mapbox v3, we exposed three new constructor parameters for Mapbox v3:
Your 3D maps are managed and customised through the use of Display Rules. For a more extensive and detailed explanation on Display Rules, . The following information can also be found in the articles detailing Display Rules, but 3D-specific information will be covered in this article.
The MapsIndoors CMS gives you the option of displaying your map in 3D. This is achieved by ensuring that any walls present in your solution are displayed as an extrusion, giving the user the appearance of a 3D map. The appearance of the walls can be customized to match your desired visual identity.
Visibility - Controls whether the 3D walls are visible on the map.
The DirectionsRenderer includes functionality to display an animated line that traces the route's path when displayed. You have control over the visual style and timing of this animation.
To customize the animation, you first need to access the dedicated animation controller object from your DirectionsRenderer instance. Use the getAnimation() method for this:
JavaScript
When getting the result route from a , we can use the DirectionsRenderer to display this Route on a map.
Once the miDirectionsServiceInstance and miDirectionsRendererInstance are initialized, the methods used from them should be agnostic to the map provider (whether it's Mapbox, Google Maps, etc.) with the exception of getting routes via TRANSIT, which requires Google as the external provider.
This example shows how to set up a query for a route and display the result on a Google Map using the DirectionsRenderer:
for Google Maps
for Mapbox
See all available directions render options in the
Ready to add a touch of personality to your multi-stop routes? Let's dive into the world of custom stop icons! This article dives into the world of custom stop icon design for multi-stop routes within the MapsIndoors Javascript SDK. It explains how to leverage the interface to craft unique icons that perfectly match your application's style and branding.
Configuring the SSO is currently handled by MapsPeople. Therefore there needs to be an exchange of information - metadata and credentials related to the authentication server, and a unique redirect URL to MapsIndoors. In case of issues, these details must also be documented.
The list of supported providers currently includes Okta, Active Directory Federation Services, Azure Active Directory, Google and Amazon Cognito. However, any provider that can meet the OIDC requirements described below can be supported.
OIDC () is the best option for enabling login to MapsIndoors via an authentication server, available from most authentication providers. OIDC is an open standard for authentication, built upon - an open standard for authorization.
Application User Roles are a feature that lets you define various roles you can assign to your users, or they can choose between themselves.
You might have parts of your map that can only be accessed by employees, so you define "Employee" and "Visitor" roles. When using the application to search for directions, users assigned to the "Visitor" role may be shown a different route from "Employee" users based on what they have access to.
App User Roles are configured via the MapsIndoors CMS. Go to Solution Details > App Settings > App Configuration, and find App User Roles
In this guide, you will learn how to show a dot on the map, representing the user's current location.
The JSFiddle example below draws a MapsIndoors map, and adds a position control. Whenever a position is received or updated, if the user has not moved the map themselves, the map will pan to the new location. If the user has moved the map, it will not center on the new location until position control is clicked.
by leveraging the avoidHighwayTypes and excludeHighwayTypes parameters.
Navigating within buildings and complexes can be a challenge, especially when considering various obstacles and preferences. The new avoidHighwayTypes and excludeHighwayTypes parameters empower you to customize your indoor routes to suit your specific needs, ensuring a seamless and accessible journey.
Supported Highway Types:
Some locations in MapsIndoors can have additional polygon information. These polygons can be used to render a room or area in a special way or make geofences, calculating whether another point or location is contained within the polygon. If a MPLocation has polygons, these can be retrieved using:
As demonstrated above, a polygon's outer ring/path as well as holes are arranged as [longitude, latitude] pairs. As not all locations has polygons, the polygon array may be empty. On the contrary, some locations, like entire building floors, might have more than polygon.
When using the MapsIndoors SDK, the system for detecting collisions will sometimes, at high zoom levels, result in the Labels of POI's that are close together being hidden, no matter what you do. Here we present a small workaround, so you can disable collisions for specific zoom levels.
For Android, there are two ways of implementing this, and which you should use depends on your desired map behavior.
If you wish for the collision behavior to change when the maps stops moving, you should use this piece of code. This would generally be the most performance-friendly option.
However, if you wish for the collision behavior to change when the maps starts moving instead, you should use this.
MapsIndoors is built on top of Google Maps or Mapbox depending on the SDK flavor you decide to use. On this page, you'll create the necessary keys with the relevant APIs enabled, retrieve a MapsIndoors API key and install the necessary dependencies to get started building your own app.
Get Your Google Maps API key
First, you need to (Please note: You are going to need a Google Billing Account for this step, so go ahead and if you haven't already). When the project is created, the following APIs and the specific SDK you plan to use must be enabled from the .
Google Maps Distance Matrix API
This is an example of enabling and disabling location clustering on the map as well as providing custom cluster tapping behaviour and custom cluster images.
Enabling and disabling clustering is done through the SolutionConfig, in the following way:
To create custom icons for clusters you can set a MPClusterIconAdapter either on the MPMapConfig when you are creating a new instance of MapControl or you can do it on runtime by setting a MPClusterIconAdapter directly on a MapControl object. Here is an example of doing it when creating a new MapControl:
Applying a ClusterIconAdapter on runtime can be done like this:
This is an example of displaying some details of a MapsIndoors location
Requirements for this tutorial will be to have a running fragment or activity with a MapsIndoors Map loaded and ready to use.
We need a view that shows the details of the location. Here we will use a TextView to display the name and description of a location:
Once the map is ready move the camera to a Venue:
We will then create a listener for when a user clicks on a marker to show the details of the selected location. This is done by setting a onLocationSelectedListener on your MapControl object. We will also listen to when the info window closes, to remove the DetailsTextView from the view. This is done by setting the onMarkerInfoWindowCloseListener on MapControl.
Android v4
In this tutorial we will request a route and list the route parts. A MapsIndoors route is made of one or more legs, each containing one or more steps.
Start by creating a BaseAdapter implementation with a MPRoute property, and include all the required functions.
Inside the init section, setup a directions service, call the directions service and save the route result to your route property
Override the getCount function to return the number of steps
Override the getView function to create your views that should be displayed in the list
mapsIndoorsInstance.addListener('zoom_changed', (zoomLevel) => {
console.log('Zoom level changed: ', zoomLevel);
if (zoomLevel > 20) {
mapsIndoorsInstance.getMap().setLayoutProperty('MI_POINT_LAYER', 'text-allow-overlap', true);
console.log('Collisions turned OFF'); // Labels can overlap
} else {
mapsIndoorsInstance.getMap().setLayoutProperty('MI_POINT_LAYER', 'text-allow-overlap', false);
console.log('Collisions turned ON'); // Labels cannot overlap
}
});Where possible, consider reusing the same model, instead of uploading 3 different models with only minor variations.
true or false.Zoom from - Sets the minimum Zoom Level at which the 3D walls are visible.
The value should be a number between 1 and 22, with 1 being very far away, and 22 being very close (22 not available for all Solutions). In a general use case, most users will only need values between 15 and 22.
If you are developing using the JavaScript SDK for Google Maps, the value must be an integer. If you are developing for Android or iOS, or using a different map provider, the value may be fractional.
Zoom to - Sets the maximum Zoom Level at which the 3D walls are visible.
The value should be a number between 1 and 22, with 1 being very far away, and 22 being very close (22 not available for all Solutions). In a general use case, most users will only need values between 15 and 22.
If you are developing using the JavaScript SDK for Google Maps, the value must be an integer. If you are developing for Android or iOS, or using a different map provider, the value may be fractional.
Wall color - Controls the color of the 3D walls.
In the CMS, you can select a color using the color picker displayed when clicking the color input field.
If setting the color in-app, the value provided must be in 6-digit HEX code (eg. #3071D9).
Wall height - Controls the height of the 3D walls, measured in meters.
In addition to the ability to extrude all walls, you can additionally extrude specific rooms or areas, for example, if you wish to highlight a specific meeting room where an important meeting is taking place.
Visibility - Controls whether the extrusion is visible on the map.
The system will accept a Boolean here, so either true or false.
Zoom from - Sets the minimum Zoom Level at which the extrusion is visible.
The value should be a number between 1 and 22, with 1 being very far away, and 22 being very close (22 not available for all Solutions). In a general use case, most users will only need values between 15 and 22.
If you are developing using the JavaScript SDK for Google Maps, the value must be an integer. If you are developing for Android or iOS, or using a different map provider, the value may be fractional.
Zoom to - Sets the maximum Zoom Level at which the extrusion is visible.
The value should be a number between 1 and 22, with 1 being very far away, and 22 being very close (22 not available for all Solutions). In a general use case, most users will only need values between 15 and 22.
If you are developing using the JavaScript SDK for Google Maps, the value must be an integer. If you are developing for Android or iOS, or using a different map provider, the value may be fractional.
Extrusion color - Controls the color of the extrusion.
In the CMS, you can select a color using the color picker displayed when clicking the color input field.
If setting the color in-app, the value provided must be in 6-digit HEX code (eg. #3071D9).
Extrusion height - Controls the height of the room extrusion, measured in meters.
3D models are a way of including models on the map, and customising their appearance. They are uploaded using the Media Library.
Visibility - Controls whether the 3D model is visible on the map.
The system will accept a Boolean here, so either true or false.
Zoom from - Sets the minimum Zoom Level at which the 3D model is visible.
The value should be a number between 1 and 22, with 1 being very far away, and 22 being very close (22 not available for all Solutions). In a general use case, most users will only need values between 15 and 22.
If you are developing using the JavaScript SDK for Google Maps, the value must be an integer. If you are developing for Android or iOS, or using a different map provider, the value may be fractional.
Zoom to - Sets the maximum Zoom Level at which the 3D model is visible.
The value should be a number between 1 and 22, with 1 being very far away, and 22 being very close (22 not available for all Solutions). In a general use case, most users will only need values between 15 and 22.
If you are developing using the JavaScript SDK for Google Maps, the value must be an integer. If you are developing for Android or iOS, or using a different map provider, the value may be fractional.
3D Model - Open the Media Library, where you can choose which 3D model to display.
The Media Library is a tool to select the displayed model from either a pre-loaded selection of models, or for you to upload your own. Please note that 3D models are only accepted to be uploaded with .glb file-type.
In-app, you can provide a URL to a desired model.
X-axis rotation - Controls the rotation of the model on the X-axis.
This value is the rotation on a given axis measured in degrees, and can be any value between 0 and 360.
Y-axis rotation - Controls the rotation of the model on the Y-axis.
This value is the rotation on a given axis measured in degrees, and can be any value between 0 and 360.
Z-axis rotation - Controls the rotation of the model on the Z-axis.
This value is the rotation on a given axis measured in degrees, and can be any value between 0 and 360.
Scale - Control the scale of the model. Can be used to make the model larger or smaller on the map.
This value is a multiplier in relation to the original size of the uploaded model. The ability to deisgnate specific measurements will be added in a future update.
Authority URL - The base URL of the authentication server, where the OIDC/OAuth URLs are relative to.
Client ID - The ID of the MapsIndoors-specific client configured at the authentication server.
Client Secret, unless client assertion is applicable - The secret that was generated for the client, unless client assertion is to be used.
A valid e-mail must provided through the id_token, or userinfo endpoint, as one of the following claim types:
email
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress
preferred_username
name
This information is all MapsIndoors Auth needs. For the authentication server, it will also need whitelisting of the sign-in URL for the configured client:
https://auth.mapsindoors.com/signin-NAME
The NAME is usually a short handle based on the organisation name and possibly the type of authentication server - e.g. mapspeople_okta.
If client assertion is to be used, the public certificate of MapsIndoors Auth can be retreived at the MapsIndoors Auth jwks endpoint.
Using a configuration like described above, the following will be assumed - with further possibility for configuration.
MapsIndoors Auth will start an OAuth 2 authorization code flow, using the defaults:
The authentication server OIDC metadata is found at .well-known/openid-configuration, a relative URL to the Authority URL given earlier. If the metadata is found elsewhere, the absolute URL must be provided.
Two scopes are requested: openid profile. If other, or no, scopes should be provided, this must be specified.
MapsIndoors Auth will use the access_token to retrieve additional claims from the userinfo endpoint. This can be disabled if needed.
For client assertions, these are the defaults:
The signing algorithm to be used is RS256. Others are available upon request.
The audience parameter is set to the same as the Authority URL. If this differs it must be specified.
If an authentication server has been configured, there will now be an IDP (IDentity Provider) with the NAME as defined above. For apps, this can be set via the acr_values parameter of the authorize request - e.g. [...]&acr_values=idp:mapspeople_okta - in order to have MapsIndoors Auth SSO directly redirect to the authentication server SSO. However, specifically for MapsIndoors CMS, a name can also be set which allows for organization-specific login - i. e. using the authentication server. Note that it does not have to be the same name used for the sign-in redirect URL.
For example, with an organization name of mapspeople, a URL will be available at https://cms.mapsindoors.com?organizationName=mapspeople. If a login is required, it will redirect to the authentication server SSO, as opposed to MapsIndoors Auth SSO. Alternatively, the organization name can be entered at https://auth.mapsindoors.com/login/organization, if a login flow was initiated at the CMS without the organizationName parameter, or possibly initiated by a third-party app.
Google Maps Directions API
Google Places API Web Service
Maps SDK for Android/iOS - if you're developing an app for Android/iOS respectively OR Maps JavaScript API if you're developing a web application.
When the above 3 APIs and the relevant SDK are enabled, you can retrieve the API key from the Credentials page. On the Credentials page, click Create credentials > API key.
Get your Mapbox Access Token
When using Mapbox you need a Mapbox account and configure credentials to run a Mapbox map with MapsIndoors and downloading the SDK: Installation
Once this is setup for the project, you can use the MapsIndoors Mapbox flavor.
If you are not a customer yet, you can use this demo MapsIndoors API key 02c329e6777d431a88480a09 to follow this guide, or you can contact MapsPeople to get your building drawings processed and hosted by us to receive a unique API key. For the purpose of this guide, both methods will work.
If you need to work with MapsIndoors SDK behind a firewall, you might need to allowlist some IP-addresses.

Italian
French




<link rel="stylesheet" href="style.css"> tag in the <head> loads your external CSS file, allowing you to style your HTML elements.The first <script> tag in the <head> loads the MapsIndoors Web SDK library.
The <script src="script.js"></script> tag at the end of the <body> links to your custom JavaScript file. Placing it here ensures it runs after the HTML body has been parsed and the MapsIndoors SDK is available.
The style.css file currently contains basic styles for the html and body elements. We've added display: flex and flex-direction: column to prepare the page for a flexible layout where elements can fill the available space, which will be useful when adding the map and other UI components later. height: 100%, margin: 0, padding: 0, and overflow: hidden are included to ensure the page takes up the full viewport and prevents unwanted scrollbars.


val geometry: MPGeometry = location.geometry
when (geometry.iType) {
MPGeometry.TYPE_POINT -> {
val point = geometry
}
MPGeometry.TYPE_POLYGON -> {
val polygon: MPPolygonGeometry = geometry as MPPolygonGeometry
// Using GMS helper classes
// Get all the paths in the polygon
val paths: List<List<MPLatLng>> = polygon.gmsPath
val pathCount = paths.size
// Outer ring (first)
val path = paths[0]
for (coordinate in path) {
val lat = coordinate.lat
val lng = coordinate.lng
}
// Optional: Inner rings (holes)
var i = 1
while (i < pathCount) {
val hole = paths[i]
for (coordinate in hole) {
val lat = coordinate.lat
val lng = coordinate.lng
}
i++
}
}
}//Enabling clustering
MapsIndoors.getSolution()?.config?.setEnableClustering(true)
//Disabling clustering
MapsIndoors.getSolution()?.config?.setEnableClustering(false)private fun initMapControl(view: View) {
val mapConfig: MPMapConfig = MPMapConfig.Builder(requireActivity(), mMap!!, getString(R.string.google_maps_key), view, true).setClusterIconAdapter { return@setClusterIconAdapter getCircularImageWithText(it.size.toString(), 15, 30, 30) }.build()
MapControl.create(mapConfig) { mapControl: MapControl?, miError: MIError? -> }
}
private fun getCircularImageWithText(text: String, textSize: Int, width: Int, height: Int): Bitmap {
val background = Paint()
background.color = Color.WHITE
// Now add the icon on the left side of the background rect
val result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(result)
val radius = width shr 1
canvas.drawCircle(radius.toFloat(), radius.toFloat(), radius.toFloat(), background)
background.color = Color.BLACK
background.style = Paint.Style.STROKE
background.strokeWidth = 3f
canvas.drawCircle(radius.toFloat(), radius.toFloat(), (radius - 2).toFloat(), background)
val tp = TextPaint()
tp.textSize = textSize.toFloat()
tp.color = Color.BLACK
val bounds = Rect()
tp.getTextBounds(text, 0, text.length, bounds)
val textHeight: Int = bounds.height()
val textWidth: Int = bounds.width()
val textPosX = width - textWidth shr 1
val textPosY = height + textHeight shr 1
canvas.drawText(text, textPosX.toFloat(), textPosY.toFloat(), tp)
return result
}Click Add App User Role and enter the name of the newly created Role in all defined languages for your Solution.
Assigning or changing App User Roles to users is done in the app itself. The method depends on which platform you're developing for. Here are some examples:
To get the available Roles in the Web SDK, you use SolutionsService:
User Roles can be set on a global level using mapsindoors.MapsIndoors.setUserRoles().
For more information, see the reference documentation.
The App User Roles are useful for setting limits on who can find certain Locations. App User Rules influence the map in three ways; which Locations are displayed on the map, whether they show up in search results, and the directions you can get.
For any Location defined on the map, there is a menu named Restrictions, where you are presented with options for limiting functionality certain App User Roles.
Open for all - All users can view, search for, and get directions to this Location.
Open for specific App User Roles - Select which App User Roles have access to viewing, searching and getting directions to this specific Location.
Closed for All - No users will see this Location on the map.
If a Location has been restricted to certain App User Roles, it will not be displayed on the map for those who do not have permission.
Similarly to the effect on the map, if a Location has restrictions, it will not show up in the search results for users without sufficient permissions.
App User Roles can be used to determine the directions users get. For instance, you can restrict Doors between two Locations to only be passable by a certain App User Role.
Note: If you restrict a Room to only be accessible to certain App User Roles, you restrict both directions to and through that Room. It effectively sets restrictions on all Doors leading to that Room as well.
The position is determined by utilizing the Geolocation API, which all modern browsers expose.
Behind the scenes, the browser determines your position based on a number of factors, including IP address, cell towers, GPS, Wifi access points etc. The implementation varies from browser to browser, and from device to device. There is currently no way to tweak the Geolocation API to use different positioning providers.
All browsers will ask the user for permission to share the location by displaying a prompt. This prompt is a part of the browser, thus not customizable.
Also note that the Geolocation API will only work on https websites (and localhost for development).
PositionControl classThe MapsIndoors JavaScript SDK exposes a PositionControl class.
An instantiation of this class will generate a button that, when clicked:
will start tracking the user's device location
show a dot on the map representing location (if accuracy is good enough - more on that later)
show a circle representing the position accuracy
Clicking on the button will pan the map, so the current position is in the center of the map.
The button will be blue whenever the position is in center of the map.
If the user has granted permission indefinitely, the map will pan to the current position when reloading the app (this may not work in certain browsers, such as Internet Explorer 11, due to missing support of the Permissions API).
You will have to add the generated button to the map yourself.
MapsIndoors supports both Google Maps and MapBox, and the methods for each vary slightly. Both still revolve around PositionControl.
Google Maps
Mapbox
Since browsers sometimes give inaccurate positions, you can use the maxAccuracy option when instantiating the PositionControl. Then the dot is only shown on the map if the given accuracy is below the given value:
avoidHighwayTypes and excludeHighwayTypes parameters support a range of path types commonly found within venues, allowing you to fine-tune your route to your preferences: 'ramp', 'stairs', 'ladder', 'escalator', 'travelator', 'elevator', 'unclassified', 'residential', 'footway', 'wheelchairramp', and 'wheelchairlift'.Avoiding Specific Path Types:
The avoidHighwayTypes parameter allows you to specify path types that you prefer to avoid within a venue. For instance, if you're pushing a stroller or carrying heavy luggage, you might want to avoid stairs and escalators. To do so, simply add the path types you wish to avoid to the avoidHighwayTypes parameter when calling the getRoute method.
Example:
Excluding Path Types Entirely:
The excludeHighwayTypes parameter takes customization a step further, allowing you to completely exclude certain path types from your route. This means that the DirectionsService will never consider these path types as part of your journey, ensuring that your route aligns with your preferences.
Example:
Accessibility Considerations:
The avoidHighwayTypes and excludeHighwayTypes parameters are particularly useful for individuals with accessibility needs. For example, if you are unable to use stairs or escalators, you can add excludeHighwayTypes: ['stairs', 'escalator'] to your getRoute call. This will ensure that the DirectionsService provides you with a route that doesn't include these obstacles.
The avoidStairs parameter will be disregarded if either avoidHighwayTypes or excludeHighwayTypes is specified. This approach stems from the fact that avoidHighwayTypes and excludeHighwayTypes provide a more granular level of control over which path types to exclude.
Example:
To get a route that isn't restricted in any way, assign an empty array to either avoidHighwayTypes or excludeHighwayTypes
Example:
With these new parameters, you have the power to tailor your indoor routes to your specific needs and preferences, whether it's avoiding stairs for easier access, prioritizing accessibility, or simply navigating through a venue with ease.
The code for Mapbox is somewhat different - Here you must make an onMoveListener, and insert the implementation into the relevant section - onMove, onMoveBegin or onMoveEnd. Generally, onMoveEnd would be recommended, and will be shown below, as it is the most performance-friendly, but code may be moved into the others, if your specific functionality can be achieved through this.
val maxZoomForCollisions = 20 //set your desired zoom level upon which the collision behaviour changes
mGoogleMap.setOnCameraIdleListener {
if (mGoogleMap.cameraPosition.zoom >= maxZoomForCollisions) {
MapsIndoors.getSolution()?.config?.setCollisionHandling(MPCollisionHandling.ALLOW_OVERLAP)
} else {
MapsIndoors.getSolution()?.config?.setCollisionHandling(MPCollisionHandling.REMOVE_LABEL_FIRST)
}
}detailsTextView.Create the showLocationDetails(location: MPLocation) method in your project.
A TextView will now appear when a user selects a location and it will disapear again when the user clicks away from the location.
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/details_text_view"
android:background="@color/cardview_light_background"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:text="This is the text view for details of the location"/>val venue = MapsIndoors.getVenues()!!.currentVenue
activity?.runOnUiThread {
if (venue != null) {
//Animates the camera to fit the new venue
mMap!!.animateCamera(
CameraUpdateFactory.newLatLngBounds(
toLatLngBounds(venue.bounds!!),
19
)
)
}
}getItem and getItemId to let click events work corretlyNow you can add the adapter to your ListView and show the route elements
class RouteListAdapter(private val context: Context) : BaseAdapter() {
private var route: MPRoute? = null
override fun getCount(): Int {
TODO("Not yet implemented")
}
override fun getItem(position: Int): Any {
TODO("Not yet implemented")
}
override fun getItemId(position: Int): Long {
TODO("Not yet implemented")
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
TODO("Not yet implemented")
}
}init {
val origin = MPPoint(57.057917, 9.950361, 0.0)
val destination = MPPoint(57.058038, 9.950509, 1.0)
val service = MPDirectionsService()
service.setRouteResultListener { route, error ->
route?.let {
this.route = it
notifyDataSetChanged()
}
error?.let {
Log.e("RouteListAdapter", it.toString())
}
}
service.query(origin, destination)
}Please note that you are not guaranteed that the visible floor contains any search results, so that is why we change floor in the above example.
// Init the query builder and build a query, in this case we will query for coffee machines ***/
MPQuery query = new MPQuery.Builder().
setQuery("coffee machine").
build();
// Init the filter builder and build a filter, the criteria in this case we want 1 coffee machine from the 1st floor
MPFilter filter = new MPFilter.Builder().
setTake(1).
setFloorIndex(1).
build();
// Query the data
MapsIndoors.getLocationsAsync( query, filter, ( locs, err ) -> {
if( locs != null && locs.size() != 0 ) {
mMapControl.setFilter( locs, MPFilterBehavior.DEFAULT );
}
} );<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MapsIndoors</title>
<!-- Include the style.css -->
<link rel="stylesheet" href="style.css">
<!-- Include the MapsIndoors SDK -->
<script src="https://app.mapsindoors.com/mapsindoors/js/sdk/4.41.0/mapsindoors-4.41.0.js.gz"></script>
</head>
<body>
<script src="script.js"></script>
</body>
</html>/* style.css */
/* Ensure html and body take up full height and use flexbox */
html, body {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden; /* Prevent scrollbars if map is full size */
display: flex; /* Use flexbox for layout */
flex-direction: column; /* Stack children vertically */
}// Creating two arrays: one which hides 3D features,
// and the other one that hides 2D features.
const features3DToHide = [
FeatureType.WALLS3D,
FeatureType.MODEL3D,
FeatureType.EXTRUSION3D,
FeatureType.EXTRUDEDBUILDINGS
];
const features2DToHide = [
FeatureType.MODEL2D,
FeatureType.WALLS2D
];
// Then calling each function either on button click or toggle.
function hide3DFeatures() {
mapView.hideFeatures(features3DToHide);
}
function hide2DFeatures() {
mapView.hideFeatures(features2DToHide);
}
// Calling hideFeatures with an empty array as a parameter will result in
// showing all the features.
function showFeatures() {
mapView.hideFeatures([])
}mapView.hideFeatures([FeatureType.MODEL2D, FeatureType.WALLS2D])
const hiddenFeatures = mapView.getHiddenFeatures();
console.log(hiddenFeatures); // [MODEL2D, WALLS2D]const features = mapView.FeatureType();
console.log(features); // [MODEL2D, WALLS2D, MODEL3D, WALLS3D, EXTRUSION3D, EXTRUDEDBUILDINGS]mapsIndoors.addListener('ready', (e) => {
log(`MapsIndoors: Ready`);
});mapsIndoors.addListener('building_changed', (e) => {
log(`Building changed: ${e.buildingInfo.name}`);
});mapsIndoors.addListener('floor_changed', (e) => {
log(`Floor changed: ${e}`);
});mapsIndoors.addListener('click', (location) => {
log(`Clicked: ${location.properties.name}`);
});mMapControl.setClusterIconAdapter {
return@setClusterIconAdapter getCircularImageWithText(it.size.toString(), 15, 30, 30)
}mapsindoors.services.SolutionsService.getUserRoles().then(userRoles => {
console.log(userRoles);
});mapsindoors.MapsIndoors.setUserRoles(['myUserRoleId']);// MapsIndoors MapView instantiation, which you should already have
const mapViewInstance = new mapsindoors.mapView.GoogleMapsView(/*...*/);
// MapsIndoors instantiation, which you should already have
const mapsIndoorsInstance = new mapsindoors.MapsIndoors(/*...*/);
// Obtain a reference to the Google map.
const googleMapsInstance = mapViewInstance.getMap();
// Create element to hold the position control
const positionControlElement = document.createElement("div");
// Create position control and attach it to element
const positionControl = new mapsindoors.PositionControl(positionControlElement, {
mapsIndoors: mapsIndoorsInstance,
});
// Add the element now holding position control to your map
googleMapsInstance.controls[google.maps.ControlPosition.RIGHT_BOTTOM].push(positionControlElement);// MapsIndoors MapView instantiation, which you should already have
const mapViewInstance = new mapsindoors.mapView.GoogleMapsView(/*...*/);
// MapsIndoors instantiation, which you should already have
const mapsIndoorsInstance = new mapsindoors.MapsIndoors(/*...*/);
// Obtain a reference to the Mapbox map.
const mapboxInstance = mapViewInstance.getMap();
// Create element to hold the position control
const positionControlElement = document.createElement("div");
// Create position control and attach it to element
const positionControl = new mapsindoors.PositionControl(positionControlElement, {
mapsIndoors: mapsIndoorsInstance,
});
// Add the element now holding position control to your map
mapboxInstance.addControl({ onAdd: function () { return positionControlElement }, onRemove: function () { } });// Generate PositionControl and only show the dot on the map if accuracy is better than 80 meters
new mapsindoors.PositionControl(myPositionControlElm, { mapsIndoors: myMapsIndoors, maxAccuracy: 80 });miDirectionsServiceInstance.getRoute({
origin: { lat: 38.897389429704695, lng: -77.03740973527613, floor: 0 },
destination: { lat: 38.897579747054046, lng: -77.03658652944773, floor: 1 },
avoidHighwayTypes: ['stairs', 'escalator']
});miDirectionsServiceInstance.getRoute({
origin: { lat: 38.897389429704695, lng: -77.03740973527613, floor: 0 },
destination: { lat: 38.897579747054046, lng: -77.03658652944773, floor: 1 },
excludeHighwayTypes: ['stairs', 'escalator']
});miDirectionsServiceInstance.getRoute({
origin: { lat: 38.897389429704695, lng: -77.03740973527613, floor: 0 },
destination: { lat: 38.897579747054046, lng: -77.03658652944773, floor: 1 },
avoidStairs: true,
excludeHighwayTypes: ['wheelchairramp']
});miDirectionsServiceInstance.getRoute({
origin: { lat: 38.897389429704695, lng: -77.03740973527613, floor: 0 },
destination: { lat: 38.897579747054046, lng: -77.03658652944773, floor: 1 },
excludeHighwayTypes: []
});val maxZoomForCollisions = 20 //set your desired zoom level upon which the collision behaviour changes
mGoogleMap.setOnCameraMoveListener {
if (mGoogleMap.cameraPosition.zoom >= maxZoomForCollisions) {
MapsIndoors.getSolution()?.config?.setCollisionHandling(MPCollisionHandling.ALLOW_OVERLAP)
} else {
MapsIndoors.getSolution()?.config?.setCollisionHandling(MPCollisionHandling.REMOVE_LABEL_FIRST)
}
}val maxZoomForCollisions = 20
mMapBoxMap?.addOnMoveListener(object : OnMoveListener {
override fun onMove(detector: MoveGestureDetector): Boolean {
// insert implementation here if desired
return false
}
override fun onMoveBegin(detector: MoveGestureDetector) {
// insert implementation here if desired
}
override fun onMoveEnd(detector: MoveGestureDetector) {
// the implementation starts here
if (mMapBoxMap?.cameraState?.zoom!! >= maxZoomForCollisions) {
MapsIndoors.getSolution()?.config?.setCollisionHandling(MPCollisionHandling.ALLOW_OVERLAP)
} else {
MapsIndoors.getSolution()?.config?.setCollisionHandling(MPCollisionHandling.REMOVE_LABEL_FIRST)
}
// the implementation ends here
}
})mMapControl?.let { mapControl ->
mapControl.setOnLocationSelectedListener {
if (it != null) {
showLocationDetails(it)
}
return@setOnLocationSelectedListener false
}
mapControl.setOnMarkerInfoWindowCloseListener {
binding.detailsTextView.visibility = View.GONE
mMapControl?.setMapPadding(0, 0, 0, 0)
}
}private fun showLocationDetails(location: MPLocation) {
binding.detailsTextView.text = "Name: " + location.name + "\nDescription: " + location.description
binding.detailsTextView.visibility = View.VISIBLE
mMapControl?.setMapPadding(0, 0, 0, binding.detailsTextView.height)
}override fun getCount(): Int {
return route?.collapsedSteps?.size ?: 0
}override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
//inflate you view, in this example we will just use a textView
val view = TextView(context)
view.text = route?.let {
"step #$position: ${route?.collapsedSteps?.get(position)?.htmlInstructions}"
} ?: "No Route"
return view
}override fun getItem(position: Int): MPRouteStep {
return route?.collapsedSteps?.get(position) ?: throw IllegalStateException("Cannot select step that does not exist")
}
override fun getItemId(position: Int): Long {
return 0L
}val routeAdapter = RouteListAdapter()
listview.adapter = routeAdapter// Init the query builder and build a query, in this case we will query for all to toilets
MPQuery query = new MPQuery.Builder().
setQuery("Toilet").
build();
// Init the filter builder and build a filter, the criteria in this case we want maximum 50 toilets from the 1st floor
MPFilter filter = new MPFilter.Builder().
setTake( 50 ).
setFloorIndex( 1 ).
build();
// Query the data
MapsIndoors.getLocationsAsync(query, filter, (locs, err) -> {
if(locs != null && locs.size() != 0 ){
mMapControl.setFilter( locs, MPFilterBehavior.DEFAULT );
}
});mapsIndoorsTransitionLevel: number- controls transition between Mapbox and MapsIndoors data. Defaults to 17. Setting it to 17 will make a transition between Mapbox and MapsIndoors data between zoom levels 17 to 18.
showMapMarkers: boolean - boolean parameter that dictates if Mapbox map markers such as POIs should be shown or not. By not setting this parameter, Mapbox map markers will be hidden as soon as MapsIndoors data is shown.
lightPreset: string - sets global light. Can be set to: day, dawn, dusk or night. Defaults to day.
The new Mapbox v3 Standard Style design provides the new design of the map and 3D buildings.
Mapbox' extruded buildings are not visible when MapsIndoors data is shown at the specified zoom level:
You can now choose between 4 different types of light: day, dawn, night or dusk.
You can read more about the latest Mapbox v3 Standard Style here.
In order to access Mapbox v2 and its options, use mapView and instantiate it as a new MapsIndoors object:
After successful mapView load you should be able to use Mapbox v2:
The routeAnimation object has a setOptions() method that allows you to configure the animation's properties. Pass an object to this method where the keys match the options you want to modify.
These options are defined by the mapsindoors.directions.RouteAnimationOptions interface:
Interface: mapsindoors.directions.RouteAnimationOptions
speed
number
300
The speed of the animation in meters per second (m/s).
minAnimationTime
number
1.2
The minimum duration in seconds for the animation. Ensures very short routes still have a perceivable animation effect.
strokeColor
string
'hsl(215, 67%, 96%)'
Example
This example shows how to create a DirectionsRenderer and immediately configure its animation to be a thicker, red line with a slightly faster speed.
JavaScript
By calling setOptions() on the object returned by getAnimation() (here stored in routeAnimation), you can tailor the route reveal animation to better match your application's design or desired user experience. These settings will be applied the next time a route is rendered using setRoute().
As previously mentioned, the route object is separated into objects of Leg and these legs are again separated into objects of Step. Unless the Route only contains one leg, the Directions Renderer does not allow the full Route to be rendered all at once. A specific part of the route can be rendered by setting the step index and/or leg index using the DirectionsRenderer.
See all available methods in the reference documentation
The length of the legs and steps arrays determines the possible values of legIndex and stepIndex.
Implementing the route rendering along with a UI
The route renderer has the ability to update the polyline via its methods.
If a route is broken down into multiple legs, steps, and sub-steps (referred to as steps within the step objects), it will be very helpful if a user can iterate through those steps prior to and even during their journey.
If using a positioning service to get the current position of the end user, you may wish to even update this programmatically. In any case, it will be more helpful to understand the data structure of the route response.
We recommend after getting the route, to set the route, get the current leg index as and also the current step index. This will be helpful in maintaining where in the directions response you are.
If the user wishes to iterate to the next step or even next leg, the directions renderer will handle that for you if you choose to use nextStep or nextLeg, respectively. It will even handle things like floor changes, reanimating the polyline, and changing zooming in or out based on the distance length.
The RouteStopIconProvider interface acts as a blueprint for creating custom stop icons. While the MapsIndoors SDK provides a default implementation (DefaultRouteStopIconProvider), this interface empowers you to take control and design stop icons beyond the built-in options.
The core functionality of RouteStopIconProvider revolves around a single asynchronous function called getImage. It accepts an optional parameter named stopNumber. This parameter provides the actual stop number (not the index) within the route and is intended to display the stop number directly on the custom stop icon. This allows you to create numbered icons that visually communicate the stop sequence within the route.
The getImage function returns a Promise that resolves to an HTMLImageElement representing the custom stop icon that will be displayed on the map. It likely utilizes libraries like Canvas or SVG to generate the icon image.
Now that we understand the core concept, let's see how to implement a custom RouteStopIconProvider class. Here's a basic example:
This example creates a simple blue circle with a white number overlay representing the stop number. You can customize this logic to generate any desired icon based on your application's needs. The code utilizes the Canvas API to create a circular icon and conditionally adds the stop number in the center.
By following this approach and leveraging the RouteStopIconProvider interface, you can create unique and informative stop icons that enhance the visual experience of your MapsIndoors multi-stop routes.

Application User Roles are a feature that lets you define various roles you can assign to your users, or they can choose between themselves.
You might have parts of your map that can only be accessed by employees, so you define "Employee" and "Visitor" roles. When using the application to search for directions, users assigned to the "Visitor" role may be shown a different route from "Employee" users based on what they have access to.
App User Roles are configured via the MapsIndoors CMS. Go to Solution Details > App Settings > App Configuration, and find App User Roles on the page. Here, you can configure existing roles, and add new ones.
Click Add App User Role and enter the name of the newly created Role in all defined languages for your Solution.
Assigning or changing App User Roles to users is done in the app itself. The method depends on which platform you're developing for. Here are some examples:
To fetch User Roles from the SDK, you call MapsIndoors.getUserRoles() to retrieve a collection of MPUserRoles tied to a loaded solution:
To set User Roles, applyUserRoles is used:
For more information, see the .
The App User Roles are useful for setting limits on who can find certain Locations. App User Rules influence the map in three ways; which Locations are displayed on the map, whether they show up in search results, and the directions you can get.
For any Location defined on the map, there is a menu named Restrictions, where you are presented with options for limiting functionality certain App User Roles.
Open for all - All users can view, search for, and get directions to this Location.
Open for specific App User Roles - Select which App User Roles have access to viewing, searching and getting directions to this specific Location.
Closed for All - No users will see this Location on the map.
If a Location has been restricted to certain App User Roles, it will not be displayed on the map for those who do not have permission.
Similarly to the effect on the map, if a Location has restrictions, it will not show up in the search results for users without sufficient permissions.
App User Roles can be used to determine the directions users get. For instance, you can restrict Doors between two Locations to only be passable by a certain App User Role.
Note: If you restrict a Room to only be accessible to certain App User Roles, you restrict both directions to and through that Room. It effectively sets restrictions on all Doors leading to that Room as well.
Multi-stop navigation has been introduced with the release of 4.8.0. This allows users to be navigated to multiple stops within a single route.
To query a multistop route a new overload MPDirectionsService.query has been introduced MPDirectionsService.query(from: MPPoint, to: MPPoint, stops: List<MPPoint>?, optimize: Boolean).
With the introduction of the multi-stop routes. There has also been added new functionality to the MPDirectionsRenderer to facilitate the new multi-stop routes.
You can still render the route as a Route with stops, using setRoute(route: MPRoute) on the DirectionsRenderer. But the interface of the MPDirectionsRenderer has been expanded with a defaultStopIcon as well as an overload of: setRoute(route: MPRoute, icons: HashMap<Integer, MPRouteStopIconProvider>?)
The defaultStopIcon can be configured to show any image. It can also be set to null, to not show any icon for the stops on the Route. We supply the MPRouteStopIconConfig that allows you to customize the default pin to fit your application.
To use your own images, you can extend the MPRouteStopIconProvider with your own class. Here is an example using a bitmap for the image
It is also possible to render an image specific to a single stop. By using setRoute(route: MPRoute, icons: HashMap<Integer, MPRouteStopIconProvider>?)
Here we will show a blue icon for the third stop. Leaving the first two indexes as null, means that the renderer will use the defaultStopIcon. If you want to render a specific known stop on an optimized route. You can find the ordering of the stops on an optimized route through MPRoute.orderedStopIndexes.
Here's a code snippet that alternates the building outline color every second, cycling through a palette of contrasting colors, it's not the most practical application, but it shows how to achieve it.
You may desire a way to change the stroke color based on user requirements to make the map meet different accessibility requirements.
This article builds on existing knowledge and assumes familiarity with the and the .
The MapsIndoors Javascript SDK empowers you to streamline navigation within venues using the multi-stop feature. This functionality allows you to obtain directions to multiple destinations within a venue effortlessly. Provide a sequence of stops, and MapsIndoors will generate the most efficient route, considering two options:
Navigation with multiple stops: Navigate through the stops in the exact order you specify. This is ideal when the order of visits is crucial.
Optimized multi-stop navigation (traveling salesman algorithm): Let MapsIndoors intelligently reorder your stops to create the fastest possible route, saving you valuable time.
Web v4
Custom Properties are key/value data that can be associated with different geodata (Venue/Building/Location) within MapsIndoors. MapsIndoors supports two different types of Custom Properties:
Language-specific Custom Properties
Generic Custom Properties
Language-specific Custom Properties are meant for values that is displayed to the enduser in their preferred language. Using language-specific Custom Properties, it is possible to store a key/value combination in multiple different languages. The MapsIndoors SDKs allows for retrieval of the correct values based on the user's preferred language. If your Solution has multiple languages, you must provide the necessary translations for each Custom Property in each of these languages.
As opposed to static data, which does not change unless data is synchronized, Live Data can change in real time, and these changes can be instantly reflected on the map and in searches.
Common use-cases are:
Changing the appearance of meeting rooms or workspace desks on a map, or in a list, based on occupancy information. For example, change the icon in order to indicate that a room is occupied.
Changing the position of a POI representing a vehicle.
Support for Live Data requires that server-side integrations are in place. For example, visualizing live occupancy data requires that a calendar or booking system integration is in place. An integration like that is set up in
The language of MapsIndoors is independent of the chosen language on the device on which the app is used. This means that you need to explicitly tell MapsIndoors which language to use.
If you do not specify a language, MapsIndoors will show information in the default language defined in the MapsIndoors CMS. Likewise, if you specify a language that is not supported, MapsIndoors will also show information in the default language.
Additionally, aside from methods mentioned here, you can provide translations via the standard method for your device, such as using individual localized strings.
To provide multiple languages for items in the MapsIndoors CMS, such as "Meeting Room" or "Restroom", the translation must be provided by the user in the CMS. A translation can be provided in any language. In order to add support for additional languages that we currently do not support, please contact your MapsIndoors representative, and we will enable you to add translations in your desired language.
Android V4
As a key element in the MapsIndoors platform, we offer API's for effeciently calculating and displaying the most optimal routes from anywhere in the world to any Location inside a Building in MapsIndoors. In the case of travelling internally at a Venue, this calculation can be done on a local map provided by MapsIndoors. In the case of travelling between Venues or from outdoors to indoors, MapsIndoors provide a seamless journey outline from a specified Origin through automatically selected Entry Points at the edge of your Venues to the specified destination.
In order to provide a route between Venues, MapsIndoors integrate with external and global map providers. Our preferred provider is Google Maps.
The central components that utilize a Directions experience is the and the . But before we get to the fun part, let's examine some key concepts first.
const mapView = new mapsindoors.mapView.MapboxV3View({
accessToken: 'YOUR_MAPBOX_ACCESS_TOKEN',
element: document.getElementById('map'),
center: { lat: 38.8974905, lng: -77.0362723 },
zoom: 17,
maxZoom: 25,
mapsIndoorsTransitionLevel: 17,
showMapMarkers: false,
lightPreset: 'dusk'
});
// Then the MapsIndoors SDK is initialized
const mi = new mapsindoors.MapsIndoors({
mapView: mapView,
floor: "1",
labelOptions: {
pixelOffset: { width: 0, height: 18 }
}
});const mapView = new mapsindoors.mapView.MapboxView({
accessToken: YOUR_MAPBOX_ACCESS_TOKEN
element: document.getElementById('map'),
center: { lat: 38.8974905, lng: -77.0362723 },
zoom: 17,
maxZoom: 25,
});
// Then the MapsIndoors SDK is initialized
const mi = new mapsindoors.MapsIndoors({
mapView: mapView,
floor: "1",
labelOptions: {
pixelOffset: { width: 0, height: 18 }
}
});// Assuming 'directionsRenderer' is your initialized DirectionsRenderer instance
// (See the main Directions Renderer page for setup)
const routeAnimation = directionsRenderer.getAnimation();// Assuming 'mapsIndoors' and 'stopIconProvider' are defined as needed
// 1. Initialize the DirectionsRenderer (as described on the main page)
const directionsRenderer = new mapsindoors.directions.DirectionsRenderer({
mapsIndoors: mapsindoors,
fitBoundsPadding: 200,
defaultRouteStopIconProvider: stopIconProvider,
});
// 2. Get the route animation controller
const routeAnimation = directionsRenderer.getAnimation();
// 3. Set desired animation options
routeAnimation.setOptions({
strokeColor: '#E41E26', // A distinct red color
strokeWeight: 6, // Make the line noticeably thicker
speed: 450 // Increase the animation speed
});
// 4. Later, when you render a route...
// directionsRenderer.setRoute(myRoute);
// ...the animation will use the custom settings defined above.const externalDirectionsProvider = new mapsindoors.directions.GoogleMapsProvider();const externalDirectionsProvider = new mapsindoors.directions.MapboxProvider();
const miDirectionsServiceInstance = new mapsindoors.services.DirectionsService(externalDirectionsProvider);
const directionsRendererOptions = { mapsIndoors: mapsIndoorsInstance }
const miDirectionsRendererInstance = new mapsindoors.directions.DirectionsRenderer(directionsRendererOptions);
//recall that your coordinates can be found on the location objects here:
// longitude = location.properties.anchor.coordinates[0]
// latitude = location.properties.anchor.coordinates[1]
// floorIndex = location.properties.floor
const routeParameters = {
origin: { lat: 38.897389429704695, lng: -77.03740973527613, floor: 0 }, // Oval Office, The White House
destination: { lat: 38.897579747054046, lng: -77.03658652944773, floor: 1 } // Blue Room, The White House
};
miDirectionsServiceInstance.getRoute(routeParameters).then(directionsResult => {
miDirectionsRendererInstance.setRoute(directionsResult);
});miDirectionsRendererInstance.setStepIndex(stepIndex, legIndex)let currentLegIndex = null;
let currentStepIndex = null;
miDirectionsRendererInstance.setRoute(directionsResult);
currentLegIndex = miDirectionsRendererInstance.getLegIndex();
currentStepIndex = miDirectionsRendererInstance.getStepIndex();/**
* Custom route stop icon provider.
*/
class CustomRouteStopIconProvider {
/**
* Gets the icon for the specified stop number.
*
* @param {number} [stopNumber] - The stop number.
* @returns {Promise<HTMLImageElement>} - The icon image.
*/
async getImage(stopNumber) {
return new Promise((resolve) => {
// This example creates a simple circle icon with a number
const width = 64;
const height = width;
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const context = canvas.getContext('2d');
context.beginPath();
context.arc(width / 2, height / 2, width / 2 - 2, 0, 2 * Math.PI);
context.strokeStyle = '#fff'; // Set your desired stroke color here
context.lineWidth = 4;
context.fillStyle = '#00f'; // Set your desired fill color here
context.fill();
context.stroke();
if (Number.isInteger(stopNumber)) {
context.fillStyle = '#fff';
context.font = 'bold 28px sans-serif';
context.textAlign = 'center';
context.textBaseline = 'middle';
context.fillText(stopNumber, width / 2, height / 2);
}
const img = new Image(canvas.width, canvas.height);
img.onload = () => resolve(img);
img.src = canvas.toDataURL();
});
}
}const redStopIconProvider = new mapsindoors.directions.DefaultRouteStopIconProvider({
fillColor: '#f00' // Red background color
});
const greenStopIconProvider = new mapsindoors.directions.DefaultRouteStopIconProvider({
fillColor: '#0f0', // Green background color
numbered: false,
});
const customRouteStopIconProvider = new CustomRouteStopIconProvider();
const routeStopConfigs = new Map([
[0, { label: 'John\'s desk', iconProvider: redStopIconProvider}],
[1, { label: 'Meeting room 4', iconProvider: greenStopIconProvider }],
[2, { label: 'Jane\'s desk', iconProvider: customStopIconProvider }]
]);
miDirectionsRendererInstance.setRoute(routeResult, routeStopConfigs);// main.js
const mapViewOptions = {
element: document.getElementById('map'),
// your other map options here
mapId: "8e0a97af9386fef",
};
const mapViewInstance = new mapsindoors.mapView.GoogleMapsView(mapViewOptions);// main.js
const mapViewOptions = {
element: document.getElementById('map'),
// your other map options here
styles: [
{
"featureType": "poi",
"stylers": [
{
"visibility": "off"
}
]
}
],
};
const mapViewInstance = new mapsindoors.mapView.GoogleMapsView(mapViewOptions);private var directionsService: MPDirectionsService = MPDirectionsService()
//Example of querying an optimized route with multiple stops
fun getRoute() {
if (directionsService != null) {
directionsService = MPDirectionsService()
}
//Setting listener to receive the queried route
directionsService.setRouteResultListener { route, error ->
if (error == null && route != null) {
//Route is received
} else {
Log.i("Directions", "Error: $error")
}
}
//Creating variables to use for the query
val origin = MPPoint(57.05800975, 9.949916517)
val destination = MPPoint(57.058278, 9.9512196, 10.0)
val stops = listOf(MPPoint(57.0582701, 9.9508396, 0.0), MPPoint(57.0580431, 9.9505475, 0.0), MPPoint(57.0580843, 9.9506085, 10.0))
directionsService.query(origin, destination, stops, true)
}The color of the animated line. Accepts any valid CSS color value (e.g., '#ff0000', 'rgba(0, 255, 0, 0.8)', 'navy'). See MDN CSS color value.
strokeOpacity
number
1
The opacity of the animated line, from 0.0 (fully transparent) to 1.0 (fully opaque). See MDN CSS opacity.
strokeWeight
number
2
The thickness (weight) of the animated line in pixels.
Entry Points are specified points in a MapsIndoors Venue that enable a transition between a global or regional map and the local map in MapsIndoors. The Entry Point often specify which travel modes are suitable for entering/exiting the Venue. There are four travel modes: Walking, Bicycling, Driving and Transit (Public Transportation). As such, the Entry Point may be a bike shed for the Bicycling travel mode, a carpark for Driving and a bus stop for Transit. As a consequence, it is often at the Entry Point that the Travel Mode changes from Bicycling, Driving or Transit to Walking. The selection of an entry point for transitioning between route networks is based on a combination of automatic calculation, estimation and optimisation.
When requesting Routes in MapsIndoors Directions Service The Route model in MapsIndoors is seperated into Legs and these Legs are again seperated into Steps.
A Leg represents a logical subset of the journey from Origin to Destination. A Route will break into Legs when:
Travelling from one floor level to another.
Changing context, such as entering or exiting a building.
Changing travel mode, for example parking your car and continuing by foot.
If you examine the illustration above, you will see that the blue line representing the Route have been marked with blue circles where the Route would be seperated into Legs.
A Route Step can have different representations depending on where on a Route it is placed. A Step may represent yet another subset of the journey within a Leg. Furthermore, it may represent a required action and/or maneuver, such as traversing floors, changing directions (Left, Right etc.). A step will also contain textual instructions. Examples include “Make a right turn”, “Continue straight ahead”, “Take the elevator to Floor 4” and the like.







Once your language of choice has been created, you can add the translation by clicking on any POI, which will open a menu on the left side of the screen. Here, you will see the following menu point, where you can enter translations for the languages you wish. If a field is left empty, the fallback language is English. In the example below, English (en) and Danish (da) are the enabled languages.
The MapsIndoors language can be fixed to a specific language by supplying an ISO 639-1 language code, for example French:
The MapsIndoors language can be aligned with the device language by supplying the current language code of the device:
When requesting routes from MapsIndoors at the moment only external routes are translated into the current language set on MapsIndoors. The internal routes are always returned in english. Here is an example of how to achieve translated HTML instructions on internal routes.
First add strings that corresponds to the directions received from HTML instructions on the route, to the Application resources res/values/strings.xml:
Now inside the code where you handle the MPRoute route response you can create a method to receive the translated instruction.
When using the Directions Renderer, the route use labels to describe the action when clicking on them. These values are not solution specific, and does not contain translations for any language. If you want them to be translated you have to assign a value in a language specific string resource. Currently only 2 values are used:
final List<MPUserRole> cmsUserRoles = MapsIndoors.getUserRoles().getUserRoles();MapsIndoors.applyUserRoles(savedUserRoles);//Changing the default icon to be blue, with a Meeting Room label and with no number inside the pin
val defaultRouteStopIcon = MPRouteStopIconConfig.Builder(myContext)
.setColor(Color.BLUE)
.setNumbered(false)
.setLabel("Meeting Room")
.build()
directionsRenderer?.setDefaultRouteStopIconConfig(defaultRouteStopIcon)class BitmapRouteStopIcon(val image: Bitmap) : MPRouteStopIconProvider() {
override fun getImage(): Bitmap? {
return image
}
}val stopIcons = mapOf(2 to MPRouteStopIconConfig.Builder(this).setColor(Color.BLUE).build())
directionsRenderer?.setRoute(route, HashMap(stopIcons))MapsIndoors.setLanguage("fr")val lang = resources.configuration.locales[0].language
MapsIndoors.setLanguage(lang)<resources>
<string name="direction_right">Turn right</string>
<string name="direction_left">Turn left</string>
<string name="direction_straight">Continue straight ahead</string>
<string name="direction_slightly_right">Turn slight right</string>
<string name="direction_slightly_left">Turn slight left</string>
<string name="direction_sharp_right">Turn sharp right</string>
<string name="direction_sharp_left">Turn sharp left</string>
<string name="direction_make_uturn">Turn around</string>
<string name="direction_elevator">Take elevator to %1$s</string>
<string name="direction_stairs">Take stairs to %1$s</string>
</resources>//Populate a map with localized strings, remember to repopulate the map if the MapsIndoors language is changed
fun setupNames() {
directionNames["Turn left"] = res.getString(R.string.direction_left)
directionNames["Turn slight left"] = res.getString(R.string.direction_slightly_left)
directionNames["Turn sharp left"] = res.getString(R.string.direction_sharp_left)
directionNames["Turn right"] = res.getString(R.string.direction_right)
directionNames["Turn slight right"] = res.getString(R.string.direction_slightly_right)
directionNames["Turn sharp right"] = res.getString(R.string.direction_sharp_right)
directionNames["Continue straight ahead"] = res.getString(R.string.direction_straight)
directionNames["Turn around"] = res.getString(R.string.direction_make_uturn_right)
}
//Use this method to get a translated HTML instruction for a step
fun getInstructionFromStep(@NonNull routeStep: MPRouteStep): String? {
var instruction: String? = routeStep.htmlInstructions
//Check if route step is a step or elevator as they have floor level specific instruction
if (routeStep.highway == "steps") {
instruction = "Take stairs to level " + routeStep.endFloorName
} else if (routeStep.highway == "elevator") {
instruction = "Take elevator to " + routeStep.endFloorName
} else if (directionNames.containsKey(routeStep.htmlInstructions)){
instruction = directionNames[routeStep.htmlInstructions]
}
return instruction
}<resources>
<string name="misdk_level">Level</string>
<string name="misdk_next">Next</string>
</resources>To change the building outline color use the strokeColor property of the BuildingOutlineOptions interface. This property accepts any color as defined by conventional CSS color values.
See https://developer.mozilla.org/en-US/docs/Web/CSS/color_value for more information on CSS color values.
To do this in practice, on the MapsIndoors instance, call setBuildingOutlineOptions to change the appearance of the building outline.
The building outline design will be taken from the values set through the CMS.
To change the building outline you can use the different properties of the BuildingOutlineOptions interface. The properties are the following:
visible - Controls whether the Building Outline is visible on the map.
The value should be a Boolean here, so either true or false.
zoomFrom - Sets the minimum Zoom Level at which the Building Outline is visible.
The value should be a number between 1 and 25, with 1 being very far away, and 25 being very close (25 not available for all Solutions). In a general use case, most users will only need values between 15 and 25.
zoomTo- Sets the maximum Zoom Level at which the Building Outline is visible.
The value should be a number between 1 and 25, with 1 being very far away, and 25 being very close (25 not available for all Solutions). In a general use case, most users will only need values between 15 and 25.
strokeColor - Controls the stroke color of the Building Outline.
This property accepts any color as defined by conventional CSS color values. See for more information on CSS color values.
strokeWeight - Controls the stroke width (in pixels) of the Building Outline.
The value should be a number between.
strokeOpacity - Controls the stroke opacity of the Building Outline.
The value should be a number between 0 and 1, for example a value of 1 gives 100% opacity, 0.2 gives 20% opacity, etc.
To read more about the BuildingOutlineOptions interface see the reference docs.
One way to do this in practice, call setBuildingOutlineOptions on the MapsIndoors instance, to change the appearance of the building outline.
Alternatively, you can define the buildingOutlineOptions property when creating a new mapsindoors instance.

To get a route with multiple stops along the path, use the DirectionsService's getRoute in combination with the stops parameter. The stops parameter takes an array of LatLngLiterals
To optimize the route for the most direct path, you can use the optimize parameter. When set to true, this parameter ensures that the stops are ordered to optimize the travel time.
The MapsIndoors Javascript SDK offers customization options to tailor the appearance of your multi-stop route. By default, MapsIndoors provides a standard icon to visually represent each stop on your route. However, you can override the DefaultRouteStopIconProvider on the DirectionsRenderer to create a more customized experience.
To customize the icons used for route stops, you can provide a DefaultRouteStopIconProvider. For example, to set the fill color to blue (#00f):
To omit the numbering of the icons, set the numbered boolean parameter to false:
The MapsIndoors Javascript SDK allows you to enhance the visual representation of your multi-stop routes by incorporating labels beneath each stop icon. This can provide additional context or information about the stop for users.
To add labels to your stops, create a Map of RouteStopConfig objects, using the stop index as the key, and the RouteStopConfig as the value. The RouteStopConfig offers a label property that you can set to the desired text for the stop label.
The RouteStopConfig object also lets you configure a different RouteStopIconProvider for the individual stops. The DefaultRouteStopIconProvider class allows customizing the fill color of the default icon as shown previously. The RouteStopConfig has the iconProvider property, which can be used to override the DirectionsRenderer's DefaultRouteStopIconProvider for the individual stops.
Here is an example of how to use the DefaultRouteStopIconProvider to customize the background color of specific stop icons:
Whether or not a room is bookable
The calendar id of a room used for booking
Ids of a location in other systems
If a Key exists as both a Generic Custom Property and Language-specific Custom Property, the most specific element decides the value. This means that the language-specific Custom Property value will be supplied to the SDKs, as it is considered to be the most specific. This table shows the possible interactions between Generic Custom Property and a Language-specific Custom Property. Scenario 1 shows what happens if a Key is defined as a Generic Custom Property and given the value A, while a Language-specific Custom Property with the same Key is either not defined, or given an empty value:
1
A
A
2
B
B
3
A
B
If a Solution uses more than one language, it is possible to give a value for a particular Key in only a subset of the languages. If for example a Solution uses both English and German, a Language-specific Custom Property could be given a value for only the German language. In this scenario if the app requests the German language, it would be given the German-specific value, while if the app requests the English language, it would be given either an empty value, or the value of the Generic Custom Property with the same Key if such a property is defined.
On Types it is possible to define Custom Property templates, which can ease getting consistent Custom Property Keys across multiple Locations. Keys added as Custom Property templates on a Type will be shown in the CMS on all Locations of that Type. This ensure that the key naming is consistent across all locations of that type. Adding values to the keys results in key/value pairs being available trough the SDK.
Custom Properties are created for each Location, defined using a key and a value. This is found in a section in the menu for each Location. When adding a Generic Custom Property through the CMS, a value input field will be provided for each language in your Solution allowing you to input the translated values directly in the CMS.
You can add Custom Properties through the Integration API with the exact same requirements and options as when adding them via the MapsIndoors CMS.
The method for reading and using these custom properties depends on which platform you're developing for. Here are some examples:
Using the above screenshot as an example basis you fetch the entire custom property using the following code:
To retrieve individual segments of the property, you can use:
data.text retrieves the content of the key field, and in the given example, would return email.
data.value retrieves the content of the value field, and in the given example, would return [email protected].
data.type retrieves the type of the Custom Property, and will in most known cases return text.
You are a conference organizer that needs to associate some pieces of data with each exhibitor, like the contact info / email address, and if there's any kinds of refreshments at the stand.
Should this be the same value for all end users, or just for a subset of those users based on their language?
contactInfo=(123) 555-5555
refreshments@gen=True
You are a museum operator providing a digital map of your venue.
Your digital map presents points of interest for the various exhibits and you would like to associate both a text description of the item exhibited as well as a link to a video of an expert giving additional insight about the item.
To accomplish this you create a language specific custom property called itemDescription and provide a description for each language your Solution supports. You choose a Language specific Custom Property for this purpose as the values is to be displayed to the end user and you need the user to be given description in their preferred language.
In addition to this you create a language-specific custom property called videoLink to store the link to the explanation video. This can make sense as a language specific custom property as the video's audio would be in the language of the user.
If for example you wish to show the same video to each user regardless of their language, it would make sense to have a generic custom property.
The following section relies on the existence of Live Position Data. If you do not have access to a MapsIndoors Dataset that have a Live Data integration, you should use our demo API key: d876ff0e60bb430b8fabb145.
Enabling Live Data through MapControl is as simple as calling mapControl.enableLiveData() with a Domain Type.
We will create a new method on our MapsActivity called enableLiveData() to enable Live Data for the Solution.
By consequence, MapControl will manage the Live Data subscriptions needed for the currently visible map and provide a default rendering of the Live Data updates depending on the Domain Type.
In the context of your view controller showing a map, add the call after creating your MapControl object used in the Activity in the initMapControl() method created earlier.
Using the demo API key you should now be able to see a "Staff Person" moving from one end to the other at ground floor in The White House main building.
Expected result:
Learn more about controlling and rendering Live Data in MapsIndoors in the introduction to Live Data.
Congratulations! You're at the end of your journey (for now), and you've accomplished a lot! 🎉
You learned which prerequisites is needed to start building with MapsIndoors.
You loaded a interactive map with MapsIndoors locations and added a floor selector for navigating between floors.
You created a search experience to search for specific locations on the map.
You added functionality for getting directions from one Location to another.
You learned how to enable different types of Live Data Domains in your app.
This concludes the "Getting Started" tutorial, but there's always more to discover. To get more inspiration on what to build next please visit our showcase page to see how other clients use MapsIndoors! For more documentation, please visit the rest of our Docs site!.

Web v4
MapsIndoors Auth is handled in two ways:
API keys - This is how apps built on top of the SDKs are authorized by default,
MapsIndoors Auth server - This is how the MapsIndoors CMS authorizes, as well as apps to access secured solutions.
The MapsIndoors Auth server is located at https://auth.mapsindoors.com - including SSO page and OIDC metadata. The server is an IdentityServer4 implementation - with support for OAauth 2 and OIDC protocols.
It stores all users that are managed through the MapsIndoors CMS, as well as configurations for authentication providers. Based on these users and authentication providers, it can authenticate and authorize users in order to access the MapsIndoors CMS and secured MapsIndoors solutions.
This guide covers the different aspects of user authentication and authorization in the MapsIndoors JavaScript SDK.
Usually, access to the services behind the MapsIndoors SDK is restricted with API keys. However, as an additional layer of security and control, access can be restricted to users of a specific tenant. A MapsIndoors dataset can only be subject to user authentication and authorization if integration with an identity provider exists. Current examples of such identity providers are Google and Microsoft. These providers and more can be added and integrated to your MapsIndoors project by request.
We recommend using a library such as AppAuth to handle verification and response to get a token to use in the MapsIndoors SDK.
To utilize an OAuth2 login flow for your MapsIndoors project, you will need to provide some details to the OAuth2 client, like the issuer url, client id, scopes and possibly a preferred identity provider if there is more than one option. These details are available as arguments in the MapsIndoors.onAuthRequired callback.
You are required to provide a redirect_url. The authorization server will redirect the user back to the application using this URL after successful authorization.
Note that the redirect link must be known to MapsIndoors and white-listed for your identity provider integration. You must inform us about all the links that you need for your application, both for development and production use so they can be white-listed. How to apply the authentication details is varying from each OAuth2 client, but you can see below how they are applied in a login flow using the
The above login flow is executed by the SDK if authentication is needed.
The SDK will then make sure that all requests for data are performed using this access token.
For a full example, please .
Note that the access token obtained from a MapsIndoors Single Sign-on flow cannot be used as access token for the Booking Service. Single Sign-on access tokens are issued by MapsIndoors and not the underlying tenant. You need to login directly on your Booking tenant to get an access token that can be used for working with the Booking Service as an authenticated user.
Your environment is now fully configured, and you have the necessary Google Maps and MapsIndoors API keys. Next you will learn how to load a map with MapsIndoors.
We start by initializing MapsIndoors. MapsIndoors is used to get and store all references to MapsIndoors-specific data. This includes access to all MapsIndoors-specific geodata.
Place the following initialization code in the onCreate method in the MapsActivity that displays the Google map. You should also assign the mapFragment view to a local variable, as we will use this later to initialize inside the onCreate, after it has been created:
If you do not have your own key, you can use this demo MapsIndoors API key: 02c329e6777d431a88480a09.
We now want to add all the data we get by initializing MapsIndoors to our map. This is done by initializing onto the map. is used as a layer between the map provider and MapsIndoors.
Start by creating an initMapControl method which is used to initiate the and assign it to mMap:
In your onMapReady callback function, assign the mMap variable with the GoogleMap you get from the callback and call the initMapControl method with the mMapView you assigned in the onCreate to set up a Google map with MapsIndoors Venues, Buildings and Locations. For Mapbox you can simple call initMapControl inside your onCreate:\
Expected result:
See the full example of MapsActivity here:
The Mapbox examples can be found here:
To implement a custom floor selector, we expect you to already have completed the Getting Started Tutorial.
This guide will walk you through the process of implementing a custom floor selector using the MapsIndoors JavaScript SDK. The custom floor selector provides a more flexible and visually appealing way to switch between floors in your MapsIndoors-enabled application.
Before you begin, make sure you have completed the getting started tutorial for the MapsIndoors JavaScript SDK. You should have a basic MapsIndoors map set up in your project.
First, we'll create a CustomFloorSelector class that will handle the creation and management of our custom floor selector.
Add methods to handle showing, hiding, and updating the floor selector:
Add methods to create and update the floor buttons:
Now, let's initialize MapsIndoors and our custom floor selector:
Finally, set up event listeners to update the floor selector when necessary:
You can customize the appearance of the floor selector by modifying the CSS styles in the createSelectorElement and updateFloorButtons methods. Adjust colors, fonts, sizes, and positioning to match your application's design.
Some larger organisations may have not just multiple Venues, but also multiple Solutions in the MapsIndoors system. Therefore, it is naturally important to be able to switch between them.
At it's core, this is done simply by switching out the API key and reloading the system. However, there are a few more steps that can be done to ensure smooth transition between Solutions.
To initialize MapsIndoors, do the following:
You switch Solutions by changing the active API key using setAPIKey().
We recommend creating your own function to call in the future for this purpose, like the example here with switchSolution():
The MapsIndoors Template is a downloadable starting point for you to integrate basic usage of MapsIndoors, containing search and directions functionalities, into your existing app. If you just want to get started with a simple solution with no customisation, this should fulfil your needs. Going through this guide will also teach you some principles on how MapsIndoors interacts with an app, and is a natural next step after the "Getting Started" guides.
If you need more customisation you can implementing your own solution using the documentation found on this site, or modify this code as needed.
MapsIndoors Template is provided as is, and can be integrated into your existing app. If you need further features, or want to customize existing ones, you're free to modify this one to your needs. However, MapsPeople offers no support or responsibility for changes made.
Before you get started, you need to get the API keys needed. This process is the same for both platforms.
First, you need to , just like you did in the guide (Please note: You are going to need a Google Billing Account for this step, so go ahead and if you haven't already). When the project is created, the following APIs and the specific SDK you plan to use must be enabled from the .
Google Maps Distance Matrix API
Google Maps Directions API
Google Places API Web Service
Maps SDK for Android/iOS
When the above 3 APIs and the relevant SDK are enabled, you can retrieve the API key from the . On the Credentials page, click Create credentials > API key.
If you are not a customer yet, you can use this demo MapsIndoors API key {{sdk.tutorialAPIKey}} to follow this guide, or you can to get your building drawings processed and hosted by us to receive a unique API key. For the purpose of this guide, both methods will work.
This app was designed to be displayed in Portrait Mode. While it will work in Landscape Mode, some UI elements may look distorted or out-of-place.
First, download or clone the pre-made project from GitHub: .
Open the project you just downloaded, and copy the classes located in java/com/mapspeople/mapsindoorstemplate into your own App
Add the Maven repository () to your project's build.gradle file
Add the following dependencies from build.gradle:
For the next step, this project uses for image handling in your application. If you are not using Glide, either import it, or if you use a different image library, you need to change some lines of code in the app. What you need to change them to, depends on the library you use. The lines are:
Copy the layout and drawables from the res folder into your app
Copy the String values from res/values into your app
Copy the google_maps_api.xml file to your project and insert a valid Google Maps API key -
Change the places where the navigation graph is used, if you are not using navigation. Alternatively, create a navigation action for MapsFragment. If so, change the navigation controller call on line: 74 inside MapsFragment.kt under the TODO
Check the FirstFragment.kt class on how to apply User Roles to the map fragment
Congratulations! You now have a functioning map in your own app, with the ability to both search for Locations and generate directions! If you want more advanced features, check out , or modify the existing code from this tutorial to suit your needs!
Ready to add indoor navigation to your app with MapsIndoors SDK?
This guide will show you how to implement directions, render routes, and interact with them in your application.
Design
Would you like to show textual directions in a UI?
What will your user interface look like?
You begin by creating an initial application. Throughout this tutorial, you will modify and extend that starter application to create a simple application which covers the basic features of this guide.
This guide explains how to start using a MapsIndoors map in your Android application using the MapsIndoors Android SDK v4.
We recommend using Android Studio for using this tutorial. Read how to set it up here:
If you do not have a Android device, you can .
Android v4
When getting the resulting Route from a , you may want to display this Route on a map. To perform this task the MPDirectionsRenderer can be used.
This example shows how to setup a query for a route and display the result on a Google Map using the MPDirectionsRenderer:
Controlling the Visible Segments on the Directions Renderer
As previously mentioned, the route object is seperated into objects of MPRouteLeg. Each leg is again separated into objects of MPRouteStep.
Android v4
The class MPDirectionsService is used to request routes from one point to another. The minimum required input to receive a route is an origin and a destination.
This example shows how to setup and execute a query for a Route:
The route can be customized via the directionsRenderer. An example could be the color of the rendered path and the background color of the rendered line. This can be set like this:
const colors = ['#E63946', '#F1FA8C', '#A8DADC', '#457B9D'];
// Initialize an index to keep track of the current color
let colorIndex = 0;
// Use setInterval to create a loop that runs every 1000 milliseconds (1 second)
setInterval(() => {
// Set the building outline color to the current color
mapsIndoorsInstance.setBuildingOutlineOptions({strokeColor: colors[colorIndex]});
// Increment the color index, cycling back to 0 if we've reached the end of the colors array
colorIndex = (colorIndex + 1) % colors.length;
}, 1000);mapsIndoors.setBuildingOutlineOptions({strokeColor: '#3071d9'});mapsIndoorsInstance.setBuildingOutlineOptions({
visible: true,
zoomFrom: 15,
zoomTo: 20,
strokeColor: '#fcd305',
strokeWeight: 5,
strokeOpacity: 0.8
});new mapsindoors.MapsIndoors({
mapView: mapView,
buildingOutlineOptions: {
visible: true,
zoomFrom: 15,
zoomTo: 20,
strokeColor: '#fcd305',
strokeWeight: 5,
strokeOpacity: 0.8
}
});const origin = { lat: 30.362364120965957, lng: -97.74102144956545 };
const destination = { lat: 30.3603809, lng: -97.7421568, floor: 0 };
const stops = [
{ lat: 30.3603751, lng: -97.7420869, floor: 0 },
{ lat: 30.3604412, lng: -97.7421172, floor: 0 },
{ lat: 30.3604593, lng: -97.7422238, floor: 0 },
];
const routeResult = await miDirectionsServiceInstance.getRoute({
origin,
destination,
stops
});
miDirectionsRendererInstance.setRoute(routeResult);const origin = { lat: 30.362364120965957, lng: -97.74102144956545 };
const destination = { lat: 30.3603809, lng: -97.7421568, floor: 0 };
const stops = [
{ lat: 30.3603751, lng: -97.7420869, floor: 0 },
{ lat: 30.3604412, lng: -97.7421172, floor: 0 },
{ lat: 30.3604593, lng: -97.7422238, floor: 0 },
];
const routeResult = await miDirectionsServiceInstance.getRoute({
origin,
destination,
stops,
// Optimize the route for the fastest travel time
optimize: true
});
miDirectionsRendererInstance.setRoute(routeResult);const routeStopIconProvider = new mapsindoors.directions.DefaultRouteStopIconProvider({
fillColor: '#00f'
});
const directionsRendererOptions = {
mapsIndoors: mapsIndoorsInstance,
defaultRouteStopIconProvider: routeStopIconProvider
};
const miDirectionsRendererInstance = new mapsindoors.directions.DirectionsRenderer(directionsRendererOptions);const routeStopIconProvider = new mapsindoors.directions.DefaultRouteStopIconProvider({
fillColor: '#00f',
numbered: false
});
const directionsRendererOptions = {
mapsIndoors: mapsIndoorsInstance,
defaultRouteStopIconProvider: routeStopIconProvider
};
const miDirectionsRendererInstance = new mapsindoors.directions.DirectionsRenderer(directionsRendererOptions);const routeStopConfigs = new Map([
[0, { label: 'John\'s desk' }],
[1, { label: 'Meeting room 4' }],
[2, { label: 'Jane\'s desk' }]
]);
miDirectionsRendererInstance.setRoute(routeResult, routeStopConfigs);const redStopIconProvider = new mapsindoors.directions.DefaultRouteStopIconProvider({
fillColor: '#f00' // Red background color
});
const greenStopIconProvider = new mapsindoors.directions.DefaultRouteStopIconProvider({
fillColor: '#0f0', // Green background color
numbered: false,
});
const routeStopConfigs = new Map([
[0, { label: 'John\'s desk', iconProvider: redStopIconProvider}],
[1, { label: 'Meeting room 4', iconProvider: greenStopIconProvider }],
[2, { label: 'Jane\'s desk' }]
]);
miDirectionsRendererInstance.setRoute(routeResult, routeStopConfigs);let data = location.getFieldForKey('email')let text = data.text
let value = data.value
let type = data.typeprivate fun enableLiveData() {
//Enabling Live Data for the three known Live Data Domains enabled for this Solution.
mMapControl.enableLiveData(LiveDataDomainTypes.AVAILABILITY_DOMAIN)
mMapControl.enableLiveData(LiveDataDomainTypes.OCCUPANCY_DOMAIN)
mMapControl.enableLiveData(LiveDataDomainTypes.POSITION_DOMAIN)
}private fun initMapControl(view: View) {
...
//Creates a new instance of MapControl
MapControl.create(config) { mapControl, miError ->
if (miError == null) {
mMapControl = mapControl!!
//Enable live data on the map
enableLiveData()
...
}
}
...
}






B
4







override fun onCreate(savedInstanceState: Bundle?) {
...
MapsIndoors.load(applicationContext, "YOUR_MAPSINDOORS_API_KEY", null)
mapFragment.view?.let {
mapView = it
}
...
}override fun onCreate(savedInstanceState: Bundle?) {
...
MapsIndoors.load(applicationContext, "YOUR_MAPSINDOORS_API_KEY", null)
...
}protected void onCreate(Bundle savedInstanceState) {
...
mMapView = mapFragment.getView();
MapsIndoors.load(getApplicationContext(), "YOUR_MAPSINDOORS_API_KEY", null);
mapFragment.getMapAsync(this);
...
}
@Override
public void onMapReady(GoogleMap googleMap) {
mMap = googleMap;
if (mMapView != null) {
initMapControl(mMapView);
}
}
void initMapControl(View view) {
MPMapConfig mapConfig = new MPMapConfig.Builder(this, mMap, getString(R.string.google_maps_key), view, true).build();
MapControl.create(mapConfig, (mapControl, miError) -> {
mMapControl = mapControl;
if (miError == null) {
//Orient your map to where you need data to be shown. This could be done by getting the default venue through MapsIndoors and panning the camera there
}
});
}Add the API key to the manifest file under the Application tag like so:
To benefit from the guides, you will need basic knowledge about:
Android Development
Google Maps Android API
You can get started in two ways, either by reviewing and modifying the basic example or do the clean setup. The clean setup is only written for Google Maps, and we recommend following the basic example.
The tutorial will be based on you starting from our basic map implementation. This contains basic UI implementations together with layout files and drawables used to create the UI. You will then be guided through how to implement the MapsIndoors SDK into this app.
The basic example contains a single activity app with already made fragments to host the different logic to get a complete app interacting with a map and MapsIndoors data.
You can find the basic example for Google Maps here: Kotlin
The Mapbox basic example is located here: Kotlin
You can open the project through Android Studio by navigating through File -> New -> Project from Version Control -> GitHub. Log in and clone the project.
You can also follow the steps below to start your app from scratch or to enhance the Basic Examples, more features will be explained in later guides.
If you don't already have a project, we recommend using the Google Maps Activity preset from Android Studio to getting started on developing your MapsIndoors project. You find the Google Maps Activity project through File -> New -> New Project... -> Google Maps Activity.
Add the MapsIndoors SDK as a dependency to your project. The AAR for the MapsIndoors SDK contains both Java classes, SDK resources and an AndroidManifest.xml template which gets merged into your application's AndroidManifest.xml during build process.
Add or merge in the following to your app's build gradle file (usually called build.gradle).
Make sure that the minimum Android SDK version is 21 (aka. "Android Lollipop", version 5.0) or above:
MapsIndoors relies on Java 8 features, so you must add the following compile options, also in android section of your build.gradle file:
Add the following dependencies and the MapsIndoors maven repository:
Gson and okhttp is used by MapsIndoors to function properly with network calls and deserializing.
play-services-maps is used for Google Maps which MapsIndoors is build on top of on Android.
Put those lines in your proguard-rules files:
Sync your project with gradle.
This "Getting Started" guide is created using a specific version of the SDK. When moving beyond the "Getting Started" guide, please be sure to use the latest version of the SDK.
Put those lines in your proguard-rules files:
Sync your project with gradle.
This "Getting Started" guide is created using a specific version of the SDK. When moving beyond the "Getting Started" guide, please be sure to use the latest version of the SDK.
mapsindoors.MapsIndoors.onAuthRequired = async ({ authClients = [], authIssuer = '' }) => {
...
})import { AuthorizationRequest, AuthorizationNotifier, BaseTokenRequestHandler, RedirectRequestHandler, AuthorizationServiceConfiguration, FetchRequestor, TokenRequest, GRANT_TYPE_AUTHORIZATION_CODE } from "@openid/appauth";
const requestor = new FetchRequestor();
const authorizationNotifier = new AuthorizationNotifier();
const authorizationHandler = new RedirectRequestHandler();
mapsindoors.MapsIndoors.onAuthRequired = async ({ authClients = [], authIssuer = '' }) => {
//Fetch the service configuration.
const config = await AuthorizationServiceConfiguration.fetchFromIssuer(authIssuer, requestor);
//Check if the URL contains code and state in the hash. They will only be present after the authorization is done.
if (window.location.hash.includes('code') && window.location.hash.includes('state')) {
//Next we need to exchange the code to an access token.
authorizationHandler.setAuthorizationNotifier(authorizationNotifier);
authorizationNotifier.setAuthorizationListener(async (request, response, error) => {
if (response) {
const tokenHandler = new BaseTokenRequestHandler(requestor);
//Build the token request.
const tokenRequest = new TokenRequest({
client_id: request.clientId,
redirect_uri: `${window.location.origin}${window.location.pathname}`,
grant_type: GRANT_TYPE_AUTHORIZATION_CODE,
code: response.code,
state: '',
extras: { code_verifier: request?.internal?.code_verifier }
});
//Send the token request.
tokenHandler.performTokenRequest(config, tokenRequest).then(response => {
//Assign the access to ken to MapsIndoors.
mapsindoors.MapsIndoors.setAuthToken(response.accessToken);
});
}
});
await authorizationHandler.completeAuthorizationRequestIfPossible();
} else {
const authClient = authClients[0];
const preferredIDP = authClient.preferredIDPs && authClient.preferredIDPs.length > 0 ? authClient.preferredIDPs[0] : '';
//Build to authorization request.
const request = new AuthorizationRequest({
client_id: authClient.clientId,
redirect_uri: `${window.location.origin}${window.location.pathname}`,
scope: 'openid profile account client-apis',
response_type: AuthorizationRequest.RESPONSE_TYPE_CODE,
extras: { 'acr_values': `idp:${preferredIDP}`, 'response_mode': 'fragment' }
});
//Send the authorization request.
authorizationHandler.performAuthorizationRequest(config, request);
}
//Clean up the url when the authentication is done.
history.replaceState(null, '', `${window.location.origin}${window.location.pathname}${window.location.search}`);
})private fun initMapControl(view: View) {
MPMapConfig mapConfig = new MPMapConfig.Builder(this, mMap, getString(R.string.google_maps_key), view, true).build();
//Creates a new instance of MapControl
MapControl.create(config) { mapControl, miError ->
if (miError == null) {
mMapControl = mapControl!!
//Enable live data on the map
enableLiveData()
//No errors so getting the first venue (in the white house solution the only one)
val venue = MapsIndoors.getVenues()?.currentVenue
venue?.bounds?.let {
runOnUiThread {
//Animates the camera to fit the new venue
mMap.animateCamera(CameraUpdateFactory.newLatLngBounds(LatLngBoundsConverter.toLatLngBounds(it), 19))
}
}
}
}
}private fun initMapControl() {
//Creates a new instance of MapControl
val config = MPMapConfig.Builder(this, mMap, mapView, getString(R.string.mapbox_access_token),true).build()
MapControl.create(config) { mapControl, miError ->
if (miError == null) {
mMapControl = mapControl!!
//Enable live data on the map
enableLiveData()
//No errors so getting the first venue (in the white house solution the only one)
val venue = MapsIndoors.getVenues()?.currentVenue
venue?.bounds?.let {
runOnUiThread {
//Animates the camera to fit the new venue
mMap.flyTo(mMap.cameraForCoordinateBounds(CoordinateBoundsConverter.toCoordinateBounds(it)))
}
}
}
}
}override fun onMapReady(googleMap: GoogleMap) {
mMap = googleMap
mapView?.let { view ->
initMapControl(view)
}
}override fun onCreate(savedInstanceState: Bundle?) {
...
initMapControl();
...
}class CustomFloorSelector {
constructor(mapsIndoorsInstance) {
this.mapsIndoors = mapsIndoorsInstance;
this.element = this.createSelectorElement();
this.floors = {};
}
createSelectorElement() {
const container = document.createElement('div');
container.style.cssText = `
position: absolute;
top: 20px;
right: 20px;
background: rgba(30, 30, 30, 0.8);
padding: 10px;
border-radius: 15px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
transition: all 0.3s ease;
`;
return container;
}
// ... (other methods will be added here)
}class CustomFloorSelector {
// ... (previous code)
onShow() {
this.element.style.display = 'flex';
this.updateFloorButtons();
}
onHide() {
this.element.style.display = 'none';
}
updateFloors(floors) {
if (floors) {
this.floors = Object.entries(floors).reduce((acc, [index, floorInfo]) => {
acc[index] = floorInfo.name || `Floor ${index}`;
return acc;
}, {});
} else {
this.floors = {};
}
this.updateFloorButtons();
}
updateWithCurrentBuilding() {
const currentBuilding = this.mapsIndoors.getBuilding();
if (currentBuilding) {
this.updateFloors(currentBuilding.floors);
} else {
this.updateFloors(null);
}
}
}class CustomFloorSelector {
// ... (previous code)
updateFloorButtons() {
this.element.innerHTML = ''; // Clear existing buttons
const currentFloor = this.mapsIndoors.getFloor();
if (Object.keys(this.floors).length === 0) {
const noFloorsMessage = document.createElement('div');
noFloorsMessage.textContent = 'No floors available';
noFloorsMessage.style.cssText = `
color: #DDD;
font-family: 'Arial', sans-serif;
font-size: 14px;
padding: 10px;
`;
this.element.appendChild(noFloorsMessage);
return;
}
// Sort floor indices in descending order
const sortedFloors = Object.entries(this.floors)
.sort(([a], [b]) => Number(b) - Number(a));
sortedFloors.forEach(([floorIndex, floorName]) => {
const button = document.createElement('button');
button.textContent = floorName;
button.style.cssText = `
margin: 5px 0;
padding: 10px 20px;
border: none;
background-color: ${floorIndex === currentFloor ? 'rgba(200, 160, 40, 0.8)' : 'rgba(60, 60, 60, 0.6)'};
color: ${floorIndex === currentFloor ? '#FFF' : '#CCC'};
cursor: pointer;
font-family: 'Arial', sans-serif;
font-size: 16px;
font-weight: bold;
border-radius: 10px;
transition: all 0.2s ease;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
outline: none;
`;
button.onmouseover = () => {
if (floorIndex !== currentFloor) {
button.style.backgroundColor = 'rgba(80, 80, 80, 0.8)';
button.style.color = '#FFF';
}
};
button.onmouseout = () => {
if (floorIndex !== currentFloor) {
button.style.backgroundColor = 'rgba(60, 60, 60, 0.6)';
button.style.color = '#CCC';
}
};
button.onclick = () => this.changeFloor(floorIndex);
this.element.appendChild(button);
});
}
changeFloor(floorIndex) {
this.mapsIndoors.setFloor(floorIndex);
this.updateFloorButtons();
}
}//Previous code from Getting Started Guide
// Initialize CustomFloorSelector
const customFloorSelector = new CustomFloorSelector(mapsIndoorsInstance);
document.body.appendChild(customFloorSelector.element);// Set up event listeners for MapsIndoors
mapsIndoorsInstance.addListener('ready', () => {
customFloorSelector.onShow();
customFloorSelector.updateWithCurrentBuilding();
});
mapsIndoorsInstance.addListener('floor_changed', () => {
customFloorSelector.updateFloorButtons();
});
mapsIndoorsInstance.addListener('building_changed', () => {
customFloorSelector.updateWithCurrentBuilding();
});
// Log any MapsIndoors errors
mapsIndoorsInstance.addListener('error', (error) => {
console.error("MapsIndoors error:", error);
});override fun onCreate(savedInstanceState: Bundle?) {
...
MapsIndoors.load(applicationContext, "YOUR_MAPSINDOORS_API_KEY", null)
mapFragment.view?.let {
mapView = it
}
...
}
override fun onMapReady(googleMap: GoogleMap) {
mMap = googleMap
mapView?.let { view ->
initMapControl(view)
}
}
fun initMapControl(view: View) {
MPMapConfig mapConfig = new MPMapConfig.Builder(this, mMap, getString(R.string.google_maps_key), view, true).build();
//Creates a new instance of MapControl
MapControl.create(config) { mapControl, miError ->
if (miError == null) {
mMapControl = mapControl!!
//Orient your map to where you need data to be shown. This could be done by getting the default venue through MapsIndoors and panning the camera there
}
}
}protected void onCreate(Bundle savedInstanceState) {
...
MapsIndoors.load(getApplicationContext(), "YOUR_MAPSINDOORS_API_KEY", null);
...
}
void initMapControl(View view) {
MPMapConfig mapConfig = new MPMapConfig.Builder(this, mMapboxMap, mMapView, getString(R.string.mapbox_access_token),true).build();
//Creates a new instance of MapControl
MapControl.create(mapConfig, (mapControl, miError) -> {
mMapControl = mapControl;
if (miError == null) {
//Orient your map to where you need data to be shown. This could be done by getting the default venue through MapsIndoors and panning the camera there
}
});
}override fun onCreate(savedInstanceState: Bundle?) {
...
MapsIndoors.load(applicationContext, "YOUR_MAPSINDOORS_API_KEY", null)
...
}
fun initMapControl(view: View) {
val config = MPMapConfig.Builder(this, mMap, mapView, getString(R.string.mapbox_access_token),true).build()
//Creates a new instance of MapControl
MapControl.create(config) { mapControl, miError ->
if (miError == null) {
//Orient your map to where you need data to be shown. This could be done by getting the default venue through MapsIndoors and panning the camera there
}
}
}protected void switchSolution() {
mMapControl.onDestroy();
MapsIndoors.load(getApplication(), "YOUR_SECONDARY_API_KEY", null);
mMapView.getMapAsync(this);
}private fun switchSolution() {
mMapControl.onDestroy()
MapsIndoors.load(applicationContext, "YOUR_SECONDARY_API_KEY", null)
mMapView.getMapAsync(this)
}mMapControl.onDestroy();
MapsIndoors.load(getApplicationContext(), "YOUR_SECONDARY_API_KEY", null);
initMapControl(mMapBoxMap, mMapView);mMapControl.onDestroy()
MapsIndoors.load(applicationContext, "YOUR_SECONDARY_API_KEY", null)
initMapControl(mMapBoxMap, mMapView)implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.mapspeople.mapsindoors:mapsindoorssdk:3.12.1'
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
implementation "com.google.android.gms:play-services-maps:16.1.0"DirectionStepFragment.kt: 50
DirectionStepFragment.kt: 53
DirectionStepFragment.kt: 56
DirectionStepFragment.kt: 61
MPSearchItemRecyclerViewAdapter.kt: 31<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="@string/google_maps_key" />dependencies {
...
implementation 'com.google.android.gms:play-services-maps:17.0.0'
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.mapspeople.mapsindoors:googlemaps:4.12.3'
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
}
repositories{
maven {
url 'https://maven.mapsindoors.com/'
}
}-keep interface com.mapsindoors.core.** { *; }
-keep class com.mapsindoors.core.errors.** { *; }
-keepclassmembers class com.mapsindoors.core.models.** { <fields>; }
-keep class com.mapsindoors.core.MPDebugLogdependencies {
...
implementation ('com.mapbox.maps:android:11.13.1'){
exclude group: 'group_name', module: 'module_name'
}
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.mapspeople.mapsindoors:mapbox-v11:4.12.3'
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
}
repositories{
maven {
url 'https://maven.mapsindoors.com/'
}
}-keep interface com.mapsindoors.core.** { *; }
-keep class com.mapsindoors.core.errors.** { *; }
-keepclassmembers class com.mapsindoors.core.models.** { <fields>; }
-keep class com.mapsindoors.core.MPDebugLogandroid {
defaultConfig {
minSdkVersion 21
}
...
}android {
...
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}What will the user experience be like
Would you like to show directions on the map?
How will the end user let the map know it's time to update with the next part of their journey?
From an implementation standpoint, there are two functional things that need to be taken care of.
Setting up and requesting directions
The first step in getting directions is initializing the directions service instance. By passing the externalDirectionsProvider, the MapsIndoors SDK will handle merging responses from the base map, e.g. outdoor directions that will charge billable requests if you request from somewhere else other than MapsIndoors data (e.g. an end users house, to somewhere indoors.)
The class DirectionsService is used to request routes from one point to another. The minimal required input is an origin and a destination.
Mapbox (required parameter of the DirectionsService instance)
MapboxProvider reference documentation: https://app.mapsindoors.com/mapsindoors/js/sdk/latest/docs/mapsindoors.directions.MapboxProvider.html
Google (not required for legacy reasons, but recommended to pass an externalDirectionsProvider as a parameter)
GoogleMapsProvider reference documentation: https://app.mapsindoors.com/mapsindoors/js/sdk/latest/docs/mapsindoors.directions.GoogleMapsProvider.html
In the below example, the coordinates are hard coded, but you'll likely want to retrieve them from location objects. It's recommended to get those from the anchor points, e.g.
In MapsIndoors, the transportation mode is referred to as travel mode. There are four travel modes, walking, bicycling, driving and transit (public transportation). The travel modes generally apply to outdoor navigation. Indoor navigation calculations are based on walking travel mode.
Set travel mode on your request using the travelMode property on routeParameters:
Relevant for outdoor directions only
DRIVING
BYCYCLING
WALKING
TRANSIT (Only supported with Google Maps as the external provider)
For a wheelchair user or a user with physical disabilities, it could be relevant to request a Route that avoids stairs, escalators, and steps.
Set avoid stairs on your request using the avoidStairs property on routeParameters:
Application User Roles is a feature that lets you define various roles that you can assign to your users. In the context of route calculation, the feature is used to differentiate routing from one user type to the another. In the MapsIndoors CMS it is possible to restrict paths and doors in the route network for certain User Roles.
You can get available Roles for your MapsIndoors Solution with the help of the SolutionsService:
For more information, see the getUserRoles documentation which returns User Role objects.
User Roles can be set on a global level using mapsindoors.MapsIndoors.setUserRoles().
For more information, see the setUserRoles method in our documentation.
This will affect all following Directions requests, visibility of Locations as well as search queries with LocationsService. Be mindful of what restrictions are set on locations if your solution is to utilize user roles within MapsIndoors.
Set a departure date or an arrival date on the route using the transitOptions property. It will only make sense to set one of these properties at a time.
This parameter is only implemented on our side with Google Maps, not Mapbox.
For more information about available options on the
transitOptionobject, see google.com/maps/documentation.
Additional reading
For more information on the options you can provide, check the documentation on getting routes.
A specific segment of the route can be rendered by setting the legIndex on the MPDirectionsRenderer.
The length of the legs array from getLegs on the MPRoute object determines the possible values of routeLegIndex (0 ..< length).
Reacting to Label Tapping
Directions Labels refer to the labels shown at the end of the rendered route segment path, that may provide contextual information, or show instructions for a required user action at that point. The labels are created as simple Marker instances that are rendered as markers on the map. A user is able to long press these, and an event will be forwarded to the listener OnLegSelectedListener in MPDirectionsRenderer. This can be used to change the Leg to the next Leg in line on the Route.
MPDirectionsRenderer also has convenience methods to change the active leg to previous and next Leg.
Show Content of Nearby Locations
It is possible to show contextual information on the end points of the rendered path of a route segment by configuring the directions renderer to look for nearby Locations or POIs.
This is done by creating an appropriate MPContextualInfoSettings object and passing that to the Directions Renderer. If it is not set or is null, no contextual information will be shown.
The MPContextualInfoSetting can be applied on MPDirectionsRenderer by calling useContentOfNearbyLocations(MPContextualInfoSettings). Like this:
The defaults of the ContextualInfoSettings builder are maxDistance at 5 meters and the ContextualInfoScope as icon and name. No Types or Categories are set as default. Not applying any Types or Categories will make it search through all Locations to use as contextual information.
When getting the resulting Route from a Directions Service, you may want to display this Route on a map. To perform this task the MPDirectionsRenderer can be used.
This example shows how to setup a query for a route and display the result on a Google Map using the MPDirectionsRenderer:
Controlling the Visible Segments on the Directions Renderer
As previously mentioned, the route object is seperated into objects of MPRouteLeg. Each leg is again separated into objects of MPRouteStep.
Unless the Route only contains one Leg, the Directions Renderer does not allow the full Route to be rendered all at once. Therefore, if a Leg contains multiple Steps, they will all be shown on the map at the same time, but once the Leg is changed, the previous Steps are not visible anymore.
A specific segment of the route can be rendered by setting the legIndex on the MPDirectionsRenderer.
The length of the legs array from getLegs on the MPRoute object determines the possible values of routeLegIndex (0 ..< length).
Reacting to Label Tapping
Directions Labels refer to the labels shown at the end of the rendered route segment path, that may provide contextual information, or show instructions for a required user action at that point. The labels are created as simple Marker instances that are rendered as markers on the map. A user is able to long press these, and an event will be forwarded to the listener OnLegSelectedListener in MPDirectionsRenderer. This can be used to change the Leg to the next Leg in line on the Route.
MPDirectionsRenderer also has convenience methods to change the active leg to previous and next Leg.
Show Content of Nearby Locations
It is possible to show contextual information on the end points of the rendered path of a route segment by configuring the directions renderer to look for nearby Locations or POIs.
This is done by creating an appropriate MPContextualInfoSettings object and passing that to the Directions Renderer. If it is not set or is null, no contextual information will be shown.
The MPContextualInfoSetting can be applied on MPDirectionsRenderer by calling useContentOfNearbyLocations(MPContextualInfoSettings). Like this:
The defaults of the ContextualInfoSettings builder are maxDistance at 5 meters and the ContextualInfoScope as icon and name. No Types or Categories are set as default. Not applying any Types or Categories will make it search through all Locations to use as contextual information.
void getRoute() {
MPDirectionsService directionsService = new MPDirectionsService(this);
MPDirectionsRenderer directionsRenderer = new MPDirectionsRenderer(mMapControl);
MPPoint origin = new MPPoint(57.057917, 9.950361, 0.0);
MPPoint destination = new MPPoint(57.058038, 9.950509, 0.0);
directionsService.setRouteResultListener((route, error) -> {
if (route != null) {
directionsRenderer.setRoute(route);
}
});
directionsService.query(origin, destination);
}void setLegIndex(int position) {
mpDirectionsRenderer.selectLegIndex(position);
}In MapsIndoors, the transportation mode is referred to as travel mode. There are four travel modes, walking, bicycling, driving and transit (public transportation). The travel modes generally applies for outdoor navigation. Indoor navigation calculations are based on walking travel mode.
Set the travel mode on your request using the setTravelMode method on MPDirectionsService:
fun createRoute(mpLocation: MPLocation) {
// if MPDirectionsService has not been instantiated create it here and assign the results call back to the activity.
if (mpDirectionsService == null) {
mpDirectionsService = MPDirectionsService()
mpDirectionsService?.setRouteResultListener(this::onRouteResult)
}
void createRoute(MPLocation mpLocation) {
// if MPDirectionsService has not been instantiated create it here and assign the results call back to the activity.
if (mpDirectionsService == null) {
mpDirectionsService = new MPDirectionsService();
mpDirectionsService.setRouteResultListener(this::onRouteResult);
The travel modes generally only apply for outdoor navigation. Indoor navigation calculations are based on the walking travel mode.
There are several ways to influence the calculated route by applying various restrictions to the directions query:
Avoid and/or exclude certain way types under certain circumstances
Apply restrictions based on User Roles
It is possible to avoid and/or exclude certain way types when calculating a route. Avoiding a way type means that DirectionsService will do its best to find a route without the avoided way types, but if no route can be found without them, it will try to find a route where the avoided way types may be used. To eliminate certain way types entirely, add them as excluded way types. If a way type is both avoided and excluded, excluded will be obeyed.
It may for example be desirable to provide a route better suited for users with physical disabilities by avoiding e.g. stairs. This can be achieved by avoiding that way type on the route using the avoidWayTypes property:
In an emergency situation it may be required to not use elevators at all. This can be achieved by adding the way type elevator to the excludeWayTypes property:
When Route restrictions are set on the MPDirectionsService they will be applied to any subsequent queries as well. You can remove them again by calling clearAvoidWayType or clearExcludeWayType.
In the MapsIndoors CMS it is possible to restrict certain ways in the Route Network to only be accessible by users belonging to certain Roles.
You can get the available Roles with help of the MapsIndoors.getAppliedUserRoles:
User Roles can be set on a global level using MapsIndoors.applyUserRoles.
This will affect all following Directions requests as well as search queries with MapsIndoors.
For more information about App User Roles, see this documentation.
When using the Transit travel mode, you must set a departure date or an arrival date on the route using the setTime method on MPDirectionsService and declaring if it is a departure or not through setIsDeparture. The date parameter is the epoch time, in seconds, as an integer, and it is only possible to use one of these properties at a time.
val directionsService = MPDirectionsService()
val directionsRenderer = MPDirectionsRenderer(mapControl)
val origin = MPPoint(57.057917, 9.950361, 0.0)
val destination = MPPoint(57.058038, 9.950509, 0.0)
directionsService.setRouteResultListener { route, error -> }
directionsService.query(origin, destination)MPDirectionsService directionsService = new MPDirectionsService();
MPDirectionsRenderer directionsRenderer = new MPDirectionsRenderer(mapControl);
MPPoint origin = new MPPoint(57.057917, 9.950361, 0.0);
MPPoint destination = new MPPoint(57.058038, 9.950509, 0.0);
directionsService.setRouteResultListener((route, error) -> {
});
directionsService.query(origin, destination);val directionsRenderer = MPDirectionsRenderer(mapControl)
directionsRenderer.setPolylineColors(Color.GREEN, Color.BLACK)MPDirectionsRenderer directionsRenderer = MPDirectionsRenderer(mapControl);
directionsRenderer.setPolylineColors(Color.GREEN, Color.BLACK);How to change the appearance of the different states
The state DisplayRules controls how Locations are displayed on the map in different states For example, you can change the icon scale of a Location when it is hovered over or highlight a search result. The state DisplayRules gives access to the same properties as the regular DisplayRules, which can be used to control the appearance of Locations.
To change the state DisplayRules, you can access the Solution Config object using the mapsIndoors.getSolutionConfig() method. Once you have the Solution Config object, you can access the state DisplayRules using the solutionConfig.stateDisplayRules property.
Example:
The default SDK behavior is to scale the icon and lighten the fill and stroke color of the polygon and extrusion.
Default values for the hover DisplayRule:
The lightnessFactor is used to darken the fill and stroke color of both the polygon and the extrusion by 10%.
The hoverHighlight and hoverSelection is two separate state DisplayRules to configure the appearance of highlighted or selected locations when hovered.
Default values for the hoverHighlight DisplayRule:
Default values for the hoverSelection DisplayRule:
The default SDK behavior is to add a small badge to the upper left corner of the Location icon and ensure visibility by setting the zoomFrom and zoomTo values, and the fill and stroke color of the polygon and extrusion.
Default values for the highlight DisplayRule:
Highlight all Meeting Rooms:
Clear the highlight:
The selection state is for changing the appearance of a single Location, for example when the user clicks on it. To select a Location, call the mapsIndoors.selectLocation() method, passing in the Location object as the parameter.
Default values for the selection DisplayRule:
Select a Location, when the user clicks it:
Clear current selection:
Searching through your MapsIndoors data is an integral part of a great user experience with your maps. Users can look for places to go, or filter what is shown on the map.
Searches work on all MapsIndoors geodata. It is up to you to create a search experience that fits your use case. To aid you in this, there are a range of filters you can apply to the search queries to get the best results. E.g. you can filter by Categories, search only a specific part of the map or search near a Location.
All three return a list of Locations from your Solution matching the parameters they are given. The results are ranked upon the 3 following factors:
If a "near" parameter is set, how close is the origin point to the result?
How well does the search input text match the text of the result (using the "Levenshtein distance" algorithm)?
Which kind of geodata is the result (e.g. Buildings are ranked over POIs)?
This means that the first item in the search result list will be the one matching the 3 factors best and so forth.
See the full list of parameters:
Example of Creating a Search Query
Display Search Results on the Map
When displaying the search results it is helpful to filter the map to only show matching Locations. Matching Buildings and Venues will still be shown on the map, as they give context to the user, even if they aren't selectable on the map.
Example of Filtering the Map to Display Searched Locations on the Map
Clearing the Map of Your Filter
After displaying the search results on your map you can then clear the filter so that all Locations show up on the map again.
Example of Clearing Your Map Filter to Show All Locations Again
Goal: This guide will walk you through initializing the MapsIndoors SDK and displaying your first interactive indoor map using Google Maps as the map provider.
SDK Concepts Introduced:
Setting the MapsIndoors API Key using mapsindoors.MapsIndoors.setMapsIndoorsApiKey()
Initializing the Google Maps map view using mapsindoors.mapView.GoogleMapsView.
MapsIndoors is a dynamic mapping platform from MapsPeople that can provide maps of your indoor and outdoor localities and helps you create search and navigation experiences for your local users. CiscoDNA is Cisco’s newest digital and cloud-based IT infrastructure management platform. Among many other things, CiscoDNA can pinpoint the physical and geographic position of devices connected wirelessly to the local IT network.
Now you have simple app showing a map. In this step, you'll create a simple search and display the search results in a list. You'll also learn how to filter the data displayed on the map based on the
Start by creating a new activity or fragment to facilitate searches on your application. Here we will be using a fragment for search and show to search results on, while using a bottom sheet to display the results. We also create a search input field on our main map activity for the user to input the text they want to search for. This is already setup in the basic example app.
To perform a search you will need to have initiated . This was shown in the previous section of the getting started tutorial how you do this.
In this tutorial we will show how you can build a custom Location Source, representing locations of robot vacuums. The robots locations will be served from a mocked list and displayed on a map.
We will start by creating our implementation of a location source.
Create the class RobotVacuumLocationSource that implements MPLocationSource:
Implement the methods from MPLocationSource and extend the constructor of the RobotVacuumLocationSource to accept a list of locations that will represent the Robot vacuums.
Create a Fragment or Activity that contains a map with MapsIndoors loaded.
const externalDirectionsProvider = new mapsindoors.directions.MapboxProvider("YOUR_MAPBOX_TOKEN");
const miDirectionsServiceInstance = new mapsindoors.services.DirectionsService(externalDirectionsProvider);const externalDirectionsProvider = new mapsindoors.directions.GoogleMapsProvider();
const miDirectionsServiceInstance = new mapsindoors.services.DirectionsService(externalDirectionsProvider);lat: originLocation.properties.anchor.coordinates[1], lng: originLocation.properties.anchor.coordinates[0], floor: originLocation.properties.floorconst routeParameters = {
origin: { lat: 38.897389429704695, lng: -77.03740973527613, floor: 0 }, // Oval Office, The White House
destination: { lat: 38.897579747054046, lng: -77.03658652944773, floor: 1 } // Blue Room, The White House
};
miDirectionsServiceInstance.getRoute(routeParameters).then(directionsResult => {
console.log(directionsResult);
});const routeParameters = {
origin: { lat: 38.897389429704695, lng: -77.03740973527613, floor: 0 }, // Oval Office, The White House
destination: { lat: 38.897579747054046, lng: -77.03658652944773, floor: 1 }, // Blue Room, The White House
travelMode: 'WALKING'
};const routeParameters = {
origin: { lat: 38.897389429704695, lng: -77.03740973527613, floor: 0 }, // Oval Office, The White House
destination: { lat: 38.897579747054046, lng: -77.03658652944773, floor: 1 }, // Blue Room, The White House
avoidStairs: 'true'
};mapsindoors.services.SolutionsService.getUserRoles().then(userRoles => {
console.log(userRoles);
});mapsindoors.MapsIndoors.setUserRoles(['myUserRoleId']);const departureDate = new Date(new Date().getTime() + 30*60000); // 30 minutes from now
const routeParameters = {
origin: { lat: 38.897389429704695, lng: -77.03740973527613, floor: 0 }, // Oval Office, The White House
destination: { lat: 38.897579747054046, lng: -77.03658652944773, floor: 1 }, // Blue Room, The White House
travelMode: 'TRANSIT',
transitOptions: {
departureTime: departureDate
}
};fun getRoute() {
val directionsService = MPDirectionsService(this)
val directionsRenderer = MPDirectionsRenderer(mMapControl)
val origin = MPPoint(57.057917, 9.950361, 0.0)
val destination = MPPoint(57.058038, 9.950509, 0.0)
directionsService.setRouteResultListener { route, error ->
route?.let { mpRoute ->
directionsRenderer.setRoute(mpRoute)
}
}
directionsService.query(origin, destination)
}void getRoute() {
MPDirectionsService directionsService = new MPDirectionsService(this);
MPDirectionsRenderer directionsRenderer = new MPDirectionsRenderer(mMapControl);
MPPoint origin = new MPPoint(57.057917, 9.950361, 0.0);
MPPoint destination = new MPPoint(57.058038, 9.950509, 0.0);
directionsService.setRouteResultListener((route, error) -> {
if (route != null) {
directionsRenderer.setRoute(route);
}
});
directionsRenderer.setOnLegSelectedListener(i -> {
directionsRenderer.selectLegIndex(i);
});
directionsService.query(origin, destination);
}void nextLeg() {
mpDirectionsRenderer.nextLeg();
}
void previousLeg() {
mpDirectionsRenderer.previousLeg();
}//Sets the contextual info to be of locations that has the type "entries" and searches within a max distance of 30 meters from the end point of the current route segment
mpDirectionsRenderer.useContentOfNearbyLocations(new MPContextualInfoSettings.Builder()
.setTypes(Collections.singletonList("entries"))
.setMaxDistance(30.0)
.build());val directionsService: MPDirectionsService = MPDirectionsService()
directionsService.addAvoidWayType(MPHighway.STEPS)MPDirectionsService directionsService = new MPDirectionsService();
directionsService.addAvoidWayType(MPHighway.STEPS);val directionsService: MPDirectionsService = MPDirectionsService()
directionsService.addExcludeWayType(MPHighway.ELEVATOR)MPDirectionsService directionsService = new MPDirectionsService();
directionsService.addExcludeWayType(MPHighway.ELEVATOR);val directionsService: MPDirectionsService = MPDirectionsService()
directionsService.clearAvoidWayType()
directionsService.clearExcludeWayType()MPDirectionsService directionsService = new MPDirectionsService();
directionsService.clearAvoidWayType();
directionsService.clearExcludeWayType();fun getUserRoles(): List<MPUserRole>? {
return MapsIndoors.getAppliedUserRoles()
}List<MPUserRole> getUserRoles() {
return MapsIndoors.getAppliedUserRoles();
} fun setUserRoles(userRoles: List<MPUserRole>) {
MapsIndoors.applyUserRoles(userRoles)
}void setUserRoles(List<MPUserRole> userRoles) {
MapsIndoors.applyUserRoles(userRoles);
}fun setDepartureTime(date: Date?) {
mpDirectionsService.setIsDeparture(true)
mpDirectionsService.setTime(date)
}
fun setArrivalTime(date: Date?) {
mpDirectionsService.setIsDeparture(false)
mpDirectionsService.setTime(date)
}void setDepartureTime(Date date) {
mpDirectionsService.setIsDeparture(true);
mpDirectionsService.setTime(date);
}
void setArrivalTime(Date date) {
mpDirectionsService.setIsDeparture(false);
mpDirectionsService.setTime(date);
}



hover
The state when the user hovers over a Location.
highlight
The state when Locations are programmatically highlighted using the mapsIndoors.highlight() method.
selection
The state when the user has selected a Location by clicking on it.
hoverHighlight
The state when the user hovers over a highlighted Location.
hoverSelection
The state when the user hovers over a selected Location.



Types
A list of Types to limit the search to
MPFilter
Bounds
Limits the result of Locations to a bounding area
MPFilter
Floor
Limits the result of Locations to be on a specific Floor
MPFilter
Near
Sorts the list of Locations on which Location is nearest the point given
MPQuery
Depth
The Depth property makes it possible to get "x" amount of descendants to the given parent. The default for this is 1 (eg. Building > Floor)
Display Search Results on the Map
When displaying the search results it is helpful to filter the map to only show matching Locations. Matching Buildings and Venues will still be shown on the map, as they give context to the user, even if they aren't selectable on the map.
Example of Filtering the Map to Display Searched Locations on the Map
Clearing the Map of Your Filter
After displaying the search results on your map you can then clear the filter so that all Locations show up on the map again.
Example of Clearing Your Map Filter to Show All Locations Again
take
Max number of Locations to get
MPFilter
Skip
Skip the first number of entries
MPFilter
categories
A list of Categories to limit the search to
MPFilter
Parents
A list of Building or Venue IDs to limit the search to
MPFilter
In order to show a user's position on an indoor map with MapsIndoors, a Position Provider must be implemented. The MapsIndoors JavaScript SDK does not provide a default Position Provider but relies on 3rd party positioning software to create this experience. In an outdoor environment, this Position Provider can be a wrapper of the browser's native Geolocation API.
Please note that the following code sample assumes that you have already succesfully implemented MapsIndoors into your application.
The JavaScript SDK doesn't have a built-in interface like the Android and iOS SDKs. However, by following these steps, you should be able to achieve the same functionality.
The first step is to create the class CiscoPositioningService, and the constructor for it.
Next step is to create watchPosition and clearWatch, to watch for the positioning updates the system recieves.
The next step is to create some functions that manage how often the system retrieves an update, or polls, from the Cisco DNA setup.
Lastly, an error handler is implemented.
Once the class is created, it can then be used, for example, in the following way - Keep in mind that you cannot fetch the client/device IP address from the browser, an option to get around this could be a seperate service that returns the IP address:
BASE_POSITION MPLatLng that will be used to calculate a random location for the Robot Vacuums.Then we need to add some variables:
Create the baseDisplayRule after MapsIndoors has loaded:
create a method to setup the RobotVacuumLocationSource inside your fragment:
As seen in the example above we add the RobotVacuumLocationSource through MapsIndoors.addLocationSources and call RobotVacuumLocationSource.setup()
This method sets the status to of the source to available and notifies MapsIndoors that locations are updated.
In the setupLocationSource method we call generateLocations to populate the location list with new locations:
Create the startUpdatingPositions method that calls updateLocations every second:
Create a method that can stop the positions updates at any time:
Create a method called updateLocations that will update the position of the Locations:
private val BASE_POSITION = MPLatLng(57.0582502, 9.9504788)private var baseDisplayRule: WeakReference<MPDisplayRule?>? = null
private var robotDisplayRule: MPDisplayRule? = null
private var mLocations: ArrayList<MPLocation>? = null
private var mRobotVacuumLocationSource: RobotVacuumLocationSource? = nullfun setRouteLegIndex(position: Int) {
mpDirectionsRenderer?.selectLegIndex(position)
}fun getRoute() {
val directionsService = MPDirectionsService(this)
val directionsRenderer = MPDirectionsRenderer(mMapControl)
val origin = MPPoint(57.057917, 9.950361, 0.0)
val destination = MPPoint(57.058038, 9.950509, 0.0)
directionsService.setRouteResultListener { route, error ->
route?.let { mpRoute ->
directionsRenderer.setRoute(mpRoute)
}
}
directionsRenderer.setOnLegSelectedListener {
mpDirectionsRenderer?.selectLegIndex(it)
}
directionsService.query(origin, destination)
}fun nextLeg() {
mpDirectionsRenderer?.nextLeg()
}
fun previousLeg() {
mpDirectionsRenderer?.previousLeg()
}//Sets the contextual info to be of locations that has the type "entries" and searches within a max distance of 30 meters from the end point of the current route segment
mpDirectionsRenderer?.useContentOfNearbyLocations(MPContextualInfoSettings.Builder()
.setTypes(Collections.singletonList("entries"))
.setMaxDistance(30.0)
.build())// This should happen after the 'ready' event has fired.
mapsIndoors.addListener('ready', () => {
// Get the Solution Config object.
const solutionConfig = mapsIndoors.getSolutionConfig();
// Get the hover state DisplayRule.
const hoverDisplayRule = solutionConfig.stateDisplayRules.hover;
// Set the icon scale to 2. This will result in the icon being scaled to double size on hover.
hoverDisplayRule.iconScale = 2;
// Update the SolutionCofig to apply the changes.
mapsIndoors.setSolutionConfig(solutionConfig);
}); {
'iconScale': 1.25,
'polygonLightnessFactor': -0.1,
'extrusionLightnessFactor': -0.1,
'badgeScale': 1.25,
'badgePosition': 'top_left'
}{
'zoomFrom': 15.0,
'zoomTo': 999,
'iconScale': 1.25,
'polygonZoomFrom': 15.0,
'polygonZoomTo': 999,
'polygonLightnessFactor': -0.15,
'extrusionZoomFrom': 15.0,
'extrusionZoomTo': 999,
'extrusionLightnessFactor': -0.15,
'badgeVisible': true,
'badgeZoomFrom': 15,
'badgeZoomTo': 999,
'badgeRadius': 6,
'badgeStrokeWidth': 4.0,
'badgeStrokeColor': '#ffffff',
'badgeFillColor': '#ec4899',
'badgePosition': 'top_left'
'badgeScale': 1.25
};{
'zoomFrom': 0.0,
'zoomTo': 999,
'iconVisible': true,
'icon': 'https://app.mapsindoors.com/mapsindoors/gfx/select-pin.png',
'iconScale': 1.25,
'iconPlacement': 'above',
'iconSize': {
'width': 24.0,
'height': 28.0
},
'polygonZoomFrom': 15.0,
'polygonZoomTo': 999,
'polygonLightnessFactor': -0.15,
'extrusionZoomFrom': 15.0,
'extrusionZoomTo': 999,
'extrusionLightnessFactor': -0.15
}{
'zoomFrom': 15.0,
'zoomTo': 999,
'polygonZoomFrom': 15.0,
'polygonZoomTo': 999,
'polygonLightnessFactor': -0.1,
'extrusionZoomFrom': 15.0,
'extrusionZoomTo': 999,
'extrusionLightnessFactor': -0.1,
'badgeVisible': true,
'badgeZoomFrom': 15,
'badgeZoomTo': 999,
'badgeRadius': 6,
'badgeStrokeWidth': 4.0,
'badgeStrokeColor': '#ffffff',
'badgeFillColor': '#ec4899',
'badgePosition': 'top_left'
'badgeScale': 1
};// Get the LocationsService object.
const locationsService = mapsindoors.services.LocationsService;
// Get all Locations of the type "Meeting Room".
const meetingRoomLocations = await locationsService.getLocations({ types: ['MeetingRoom'] });
// Highlight the Locations.
mapsIndoors.highlight(meetingRoomLocations.map(location => location.id));mapsIndoors.highlight([]);{
'zoomFrom': 0.0,
'zoomTo': 999,
'iconVisible': true,
'icon': 'https://app.mapsindoors.com/mapsindoors/gfx/select-pin.png',
'iconScale': 1.0,
'iconPlacement': 'above',
'iconSize': {
'width': 24.0,
'height': 28.0
},
'polygonZoomFrom': 15.0,
'polygonZoomTo': 999,
'polygonLightnessFactor': -0.1,
'extrusionZoomFrom': 15.0,
'extrusionZoomTo': 999,
'extrusionLightnessFactor': -0.1
}// (location) is the Location object that is clicked by the user.
mapsIndoors.on('click', (location) => {
mapsIndoors.selectLocation(location);
});mapsIndoors.deselectLocation();void findRestroom() {
//Here we will create an empty query because we are only interrested in getting locations that match a category. If you want to be more specific here where you can add a query text like "Unisex Restroom"
MPQuery mpQuery = new MPQuery
.Builder()
.build();
List<String> categories = new ArrayList<>();
categories.add("RESTROOMS");
// Init the filter builder and build a filter, the criteria in this case we want maximum 50 restrooms
MPFilter mpFilter = new MPFilter
.Builder()
.setCategories(categories)
.setTake(50)
.build();
MapsIndoors.getLocationsAsync(mpQuery, mpFilter, (locations, error) -> {
//Check if there is an error and iterate through the list to do what you need with the search
});
}MapsIndoors.getLocationsAsync(mpQuery, mpFilter, (locations, error) -> {
if (locations != null && !locations.isEmpty()) {
//Query with the locations from the query result. Use default camera behavior
mMapControl.setFilter(locations, MPFilterBehavior.DEFAULT);
}
});mMapControl.clearFilter();fun findRestroom() {
//Here we will create an empty query because we are only interrested in getting locations that match a category. If you want to be more specific here where you can add a query text like "Unisex Restroom"
val mpQuery = MPQuery.Builder()
.build()
val categories: MutableList<String> = ArrayList()
categories.add("RESTROOMS")
// Init the filter builder and build a filter, the criteria in this case we want maximum 50 restrooms
val mpFilter = MPFilter.Builder()
.setCategories(categories)
.setTake(50)
.build()
MapsIndoors.getLocationsAsync(mpQuery, mpFilter) { locations: List<MPLocation?>?, error: MIError? ->
//Check if there is an error and iterate through the list to do what you need with the search
}
}MapsIndoors.getLocationsAsync(mpQuery, mpFilter, (locations, error) -> {
//Query with the locations from the query result. Use default camera behavior
mMapControl.setFilter(locations, MPFilterBehavior.DEFAULT)
});mMapControl.clearFilter()class CiscoPositioningService {
/**
* @param {string} args.clientIp - The local IP address of the device
* @param {string} args.tenantId - The Cisco tenant id.
* @param {number} [args.pollingInterval=1000] - The interval that the position will be polled from the backend.
* @param {string} [args.region="eu"] - The Cisco app region.
*/
constructor(args = {}) {
if (!args.clientIp)
throw new TypeError('Invalid argument: "clientIp"');
if (!args.tenantId)
throw new TypeError('Invalid argument: "tenantId"');
this._pollingInterval = 1000;
this._tenantId = args.tenantId;
this._successCallbacks = new Map();
this._errorCallbacks = new Map();
this._deviceId = '';
args.region = args.region || 'eu';
this.pollingInterval = args.pollInterval;
fetch(`https://ciscodna.mapsindoors.com/${this._tenantId}/api/ciscodna/devicelookup?clientIp=${args.clientIp}®ion=${args.region}`)
.then(this._errorHandler)
.then(res => res.json())
.then(({ deviceId }) => {
this._deviceId = deviceId;
this._startPolling();
}).catch(err => {
console.error(err.message);
});
} watchPosition(successCallback, errorCallback) {
const watchId = Symbol();
if (!(successCallback instanceof Function))
throw new TypeError('Invalid argument: "successCallback"');
if (errorCallback instanceof Function) {
this._errorCallbacks.set(watchId, errorCallback);
}
this._successCallbacks.set(watchId, successCallback);
if (!this._interval) {
this._startPolling();
}
return watchId;
}
clearWatch(watchId) {
this._successCallbacks.delete(watchId);
this._errorCallbacks.delete(watchId);
if (this._successCallbacks.size === 0) {
this._stopPolling();
}
}
getCurrentPosition(successCallback, errorCallback) {
fetch(`https://ciscodna.mapsindoors.com/${this._tenantId}/api/ciscodna/${this._deviceId}`)
.then(this._errorHandler)
.then(res => res.json())
.then(data => {
this._successCallbacks.forEach(cb => cb.call(null, data));
}).catch(err => {
this._errorCallbacks.forEach(cb => cb.call(null, err));
});
} set pollingInterval(value) {
if (!isNaN(value) && this._pollingInterval !== value) {
this._pollingInterval = value;
this._stopPolling();
this._startPolling();
}
}
get pollingInterval() {
return this._pollingInterval;
}
/**
* @private
*/
_startPolling() {
if (!this._interval && this._deviceId > '' && this._successCallbacks.size > 0) {
this._interval = window.setInterval(() => {
this.getCurrentPosition(response => {
this._successCallbacks.forEach(callback => callback(response));
},
error => {
this._errorCallbacks.forEach(callback => callback(err));
});
}, this._pollingInterval);
}
}
/**
* @private
*/
_stopPolling() {
if (this._interval) {
window.clearInterval(this._interval);
this._interval = null;
}
} /**
* @private
*/
_errorHandler(response) {
if (!response.ok) {
const contentType = response.headers.get('content-type');
if (contentType && contentType.indexOf('application/json') !== -1) {
return response.json().then(({ message }) => {
throw new Error(message);
});
} else {
let statusText;
switch (response.status) {
case 400:
statusText = 'The client IP is invalid.';
break;
case 404:
statusText = 'Device not found.';
break;
case 403:
statusText = 'The TenantId supplied is not authorized to access the device at the location.'
break;
default:
statusText = 'Unknown error.';
}
throw new Error(statusText);
}
}
return response;
}
} // end classconst map = mapView.getMap();
let watchId;
mapsindoors.services.SolutionsService.getSolution('57e4e4992e74800ef8b69718').then(solution => {
if (solution.positionProviderConfigs && solution.positionProviderConfigs.ciscodna) {
const tenantId = solution.positionProviderConfigs.ciscodna.ciscoDnaSpaceTenantId;
const region = solution.positionProviderConfigs.ciscodna.ciscoDnaSpaceTenantRegion || 'usa';
const clientIp = '10.0.0.134';
const cps = new CiscoPositioningService({ clientIp, tenantId, region });
watchId = cps.watchPosition(function (data) {
console.log(data);
map.setCenter({ lat: data.latitude, lng: data.longitude });
}, function (err) {
console.log(err);
})
}
});
const floorSelector = document.createElement('div');
new mapsindoors.FloorSelector(floorSelector, mi);
map.controls[google.maps.ControlPosition.RIGHT_TOP].push(floorSelector);class RobotVacuumLocationSource(private val robots: ArrayList<MPLocation>): MPLocationSource {
private val mObservers = ArrayList<MPLocationsObserver>()
private var mStatus = MPLocationSourceStatus.NOT_INITIALIZED
override fun getLocations(): MutableList<MPLocation> {
return robots
}
override fun addLocationsObserver(observer: MPLocationsObserver?) {
if (observer != null) {
mObservers.add(observer)
}
}
override fun removeLocationsObserver(observer: MPLocationsObserver?) {
if (observer != null) {
mObservers.remove(observer)
}
}
private fun notifyUpdateLocations() {
for (observer in mObservers) {
observer.onLocationsUpdated(robots, this)
}
}
override fun getStatus(): MPLocationSourceStatus {
return mStatus
}
override fun getSourceId(): Int {
return 10101010
}
override fun clearCache() {
robots.clear()
mObservers.clear()
}
override fun terminate() {
robots.clear()
mObservers.clear()
}
}MapsIndoors.load(requireActivity().applicationContext, "MY_API_KEY") { error ->
if (error == null) {
baseDisplayRule = WeakReference(MapsIndoors.getMainDisplayRule())
setupLocationSource()
}
}private fun setupLocationSource() {
if (mLocations == null) {
generateLocations()
}
val locationSource = RobotVacuumLocationSource(mLocations!!)
MapsIndoors.addLocationSources(Collections.singletonList(locationSource) as List<MPLocationSource>) {
}
locationSource.setup()
startUpdatingPositions()
}fun setup() {
status = MPLocationSourceStatus.AVAILABLE
notifyUpdateLocations()
}
fun setStatus(status: MPLocationSourceStatus) {
mStatus = status
for (observer in mObservers) {
observer.onStatusChanged(mStatus, this)
}
}private fun generateLocations() {
mLocations = ArrayList()
for (i in 0..15) {
val robotName = "vacuum$i"
val startPosition = getRandomPosition()
val charge = nextInt(1, 100)
val floorIndex = nextInt(4) * 10
var mpLocation = MPLocation.Builder(robotName)
.setPosition(startPosition.lat, startPosition.lng)
.setFloorIndex(floorIndex)
.setName(robotName)
.setBuilding("Stigsborgvej")
.build()
robotDisplayRule = MPDisplayRule(robotName, baseDisplayRule!!)
robotDisplayRule?.isVisible = true
if (charge >= 60) {
robotDisplayRule?.setIcon(R.drawable.ic_baseline_robo_vacuum, Color.GREEN)
}else if (charge >= 30) {
robotDisplayRule?.setIcon(R.drawable.ic_baseline_robo_vacuum, Color.YELLOW)
}else {
robotDisplayRule?.setIcon(R.drawable.ic_baseline_robo_vacuum, Color.RED)
}
MapsIndoors.addDisplayRuleForLocation(mpLocation, robotDisplayRule!!)
mLocations?.add(mpLocation)
}
}
private fun getRandomPosition(): MPLatLng {
val lat: Double = BASE_POSITION.lat + (-4 + nextInt(20)) * 0.000005
val lng: Double = BASE_POSITION.lng + (-4 + nextInt(20)) * 0.000010
return MPLatLng(lat, lng)
}private fun startUpdatingPositions() {
mUpdateTimer?.cancel()
mUpdateTimer = Timer()
mUpdateTimer?.scheduleAtFixedRate(object: TimerTask() {
override fun run() {
updateLocations();
}
}, 2000, 500)
}fun stopUpdatingPositions() {
mUpdateTimer?.cancel()
mUpdateTimer?.purge()
}fun updateLocations() {
var updatedLocations = ArrayList<MPLocation>()
mLocations?.forEach {
var newPosition = getRandomPosition()
var newLocation = MPLocation.Builder(it).setPosition(MPPoint(newPosition.lat, newPosition.lng), 20)
var charge = nextInt(1, 100)
updatedLocations.add(newLocation.build())
robotDisplayRule = MPDisplayRule("robot", baseDisplayRule!!)
robotDisplayRule?.isVisible = true
if (charge >= 60) {
robotDisplayRule?.setIcon(R.drawable.ic_baseline_robo_vacuum, Color.GREEN)
}else if (charge >= 30) {
robotDisplayRule?.setIcon(R.drawable.ic_baseline_robo_vacuum, Color.YELLOW)
}else {
robotDisplayRule?.setIcon(R.drawable.ic_baseline_robo_vacuum, Color.RED)
}
MapsIndoors.addDisplayRuleForLocation(it, robotDisplayRule!!)
}
mRobotVacuumLocationSource?.updateLocations(updatedLocations)
}Creating the main mapsindoors.MapsIndoors instance.
Adding a mapsindoors.FloorSelector control.
Using mapsIndoorsInstance.goTo() to pan and zoom the map to the selected location.
Handling map clicks to center the map on a clicked POI (location) using mapsIndoorsInstance.on('click', callback)
Completion of the Set Up Your Environment guide.
You will need a Google Maps JavaScript API Key. If you don't have one, you can create one in the Google Cloud Console.
You will need a MapsIndoors API Key. For this tutorial, you can use the demo API key: 02c329e6777d431a88480a09.
To display the map, you'll need to update your index.html, style.css, and script.js files as follows.
Open your index.html file. You need to include the Google Maps JavaScript API and ensure there's a <div> element to contain the map.
Explanation of index.html updates:
The Google Maps JavaScript API is included with your API key. Replace YOUR_GOOGLE_MAPS_API_KEY with your actual key.
The MapsIndoors SDK script (mapsindoors-4.41.0.js.gz) should already be present from the initial setup guide. You can always check the MapsIndoors SDK reference documentation for the latest SDK version.
An empty <div> with the attribute id="map" was added inside the <body>. This div is crucial as it serves as the container where both the Google base map and the MapsIndoors layers will be rendered.
Update your style.css file to ensure the #map element fills the available space. This is necessary for the map to be visible.
Explanation of style.css updates:
The styles for the #map container are added. These styles define how the map container behaves within the flexbox layout established in the initial setup (html, body styles).
flex-grow: 1; is a key flexbox property that allows the map container to expand and fill the available vertical space within its parent (body).
width: 100%; ensures the map fills the full horizontal width of its container.
margin: 0; and padding: 0; remove any default browser spacing around the map container, ensuring it fits snugly.
Open your script.js file and add the following JavaScript code. This code will initialize the Google Map, then create the MapsIndoors view and instance, and finally add a Floor Selector.
Remember to replace YOUR_MAPSINDOORS_API_KEY with your MapsIndoors API key if not using the demo key. For testing, the demo MapsIndoors API key 02c329e6777d431a88480a09 and the Austin office venue ID dfea941bb3694e728df92d3d are used.
Explanation of script.js updates:
const mapViewOptions = { ... }: This object defines essential configuration options for the MapsIndoors Google Maps view.
element: The HTML DOM element (our <div id="map">) where the map will be rendered.
center, zoom, maxZoom: Standard map parameters to set the initial view.
For more details on all available options, see the .
mapsindoors.MapsIndoors.setMapsIndoorsApiKey('YOUR_MAPSINDOORS_API_KEY');: This static method sets your MapsIndoors API key globally for the SDK. This key authenticates your requests to MapsIndoors services. See the .
const mapViewInstance = new mapsindoors.mapView.GoogleMapsView(mapViewOptions);: This line creates an instance of GoogleMapsView, which is responsible for integrating MapsIndoors data and rendering with a Google Map. For more details on GoogleMapsView, see its .
const mapsIndoorsInstance = new mapsindoors.MapsIndoors({ mapView: mapViewInstance, venue: 'YOUR_MAPSINDOORS_VENUE_ID' });: This creates the main MapsIndoors instance. This object is your primary interface for interacting with MapsIndoors functionalities like displaying locations, getting directions, etc. See the .
Floor Selector Integration:
const floorSelectorElement = document.createElement('div');: A new HTML div element is dynamically created. This element will serve as the container for the Floor Selector UI.
new mapsindoors.FloorSelector(floorSelectorElement, mapsIndoorsInstance);: This instantiates the FloorSelector control. It takes the HTML element to render into and the
After completing these steps and opening your index.html file in a web browser, you should see:
An interactive map centered on the MapsPeople Austin Office.
A Floor Selector control visible in the top-right corner of the map, allowing you to switch between different floors of the venue.
Clicking on any POI or location marker on the map will center the map on that location.
Map doesn't load / blank page:
Check your browser's developer console (usually F12) for any error messages.
Verify that YOUR_MAPSINDOORS_API_KEY is correctly set (or the demo key is used).
Double-check all CDN links in index.html are correct and accessible.
Floor selector doesn't appear:
Verify the JavaScript code for creating and adding the floor selector has no typos.
Check the console for errors related to FloorSelector or Google Maps controls.
Clicking a location does not center the map:
Ensure the handleLocationClick function is correctly defined and registered as an event listener.
Check for any JavaScript errors in the console that might indicate issues with the click handling code.
You have successfully displayed a MapsIndoors map with Google Maps and added a floor selector! This is the foundational step for building more complex indoor mapping applications.
Next, you will learn how to add search functionality to your map:
Step 2: Create a Search Experience
Create a search method that takes a search string as a parameter on your MapsActivity class. In this example we only use the setTake on the MPFilter to limit our result to 30 locations. We will expand on this method later.
To be able to search we will use a text input field where a user can write what they want to search for. This is placed at the top of the MapsActivity
To call our search method with the text in the search input field, we then add an EditorActionListener and a OnClickListener to the text input field and the search button in the onCreate of MapsActivity. Find the full onCreate example here: MapsActivity.kt
Find the full onCreate example here: MapsActivity.kt
To accompany this we use the SearchFragment that is already created for you and a BottomSheet to handle the SearchFragment.
Observe that the SearchFragmentis just a simple fragment with a RecyclerView and a SearchItemAdapter added to it
See the full example of SearchFragment here: SearchFragment.kt
Create a getter for your MapControl object on the MapsActivity so that it can be used in the SearchAdapter.
Inside the SearchItemAdapter implement logic to display the locations you get from a search result. Here we show an image of the location marker and show the name of the locations.
See the full example of SearchItemAdapter and accompanying ViewHolder here: SearchItemAdapter.kt
We have already implemented the BottomSheet in the UI. Now we add the search fragment to the BottomSheet in our search query method on our MapsActivity. You can use the addFragmentToBottomSheet too add the created fragment to the BottomSheet. When we have received the search results
See the full example of the search method here: MapsActivity.kt
When getting a search result, you might want to only show those search results on the map. You can do this through calling displaySearchResults(List<MPLocation> locations) on MapControl. This method has different parameters to make it easier for you as a developer to fit your exact need in terms of animation and more. This can be read in the JavaDoc of MapControl.
The standard implementation animates the camera to fit all Locations on the map and show the info window of a Location, if it's a list of only one Location.
When you are done showing the search results you can call clearMap() on MapControl.
Since the default displaySearchResults(List<MPLocation> locations) uses camera animation we will call it from the UI Thread and implement it in our search method inside the getLocationsAsync result with the list from the method.
Expected result:
The accompanying UI and implementation of this search experience can be found in the getting started app sample. Getting Started App sample
Goal: This guide will walk you through initializing the MapsIndoors SDK and displaying your first interactive indoor map using Mapbox GL JS as the map provider.
SDK Concepts Introduced:
Setting the MapsIndoors API Key using mapsindoors.MapsIndoors.setMapsIndoorsApiKey().
Initializing the Mapbox map view using mapsindoors.mapView.MapboxV3View.
Creating the main mapsindoors.MapsIndoors instance.
Adding a mapsindoors.FloorSelector control.
Using mapsIndoorsInstance.goTo() to pan and zoom the map to the selected location.
Handling map clicks to center the map on a clicked POI (location) using mapsIndoorsInstance.on('click', callback)
Completion of the .
You will need a Mapbox Access Token. If you don't have one, you can create one for free on the .
You will need a MapsIndoors API Key. For this tutorial, you can use the demo API key: 02c329e6777d431a88480a09.
To display the map, you'll need to update your index.html, style.css, and script.js files as follows.
Open your index.html file. You need to include the Mapbox GL JS CSS and JavaScript libraries from their CDN and ensure there's a <div> element to contain the map.
Explanation of index.html updates:
The <link href='https://api.mapbox.com/mapbox-gl-js/v3.10.0/mapbox-gl.css' rel='stylesheet' /> tag in the <head> includes the necessary CSS for Mapbox GL JS. This styles the map elements provided by Mapbox.
The <script src='https://api.mapbox.com/mapbox-gl-js/v3.10.0/mapbox-gl.js'></script> tag in the <head> includes the Mapbox GL JS library. Note: We are using version 3.10.0 in this example; you should verify this is the currently recommended version or check the for the latest.
Update your style.css file to ensure the #map element fills the available space. This is necessary for the map to be visible.
Explanation of style.css updates:
The styles for the #map container are added. These styles define how the map container behaves within the flexbox layout established in the initial setup (html, body styles).
flex-grow: 1; is a key flexbox property that allows the map container to expand and fill the available vertical space within its parent (body).
width: 100%;
Open your script.js file and add the following JavaScript code. This code will initialize the Mapbox map, then create the MapsIndoors view and instance, and finally add a Floor Selector.
Remember to replace YOUR_MAPBOX_ACCESS_TOKEN with your actual Mapbox access token if you are not using the demo one, and YOUR_MAPSINDOORS_API_KEY with your MapsIndoors API key if not using the demo key. For testing, the demo MapsIndoors API key 02c329e6777d431a88480a09 and the Austin office venue ID dfea941bb3694e728df92d3d are used.
Explanation of script.js updates:
const mapViewOptions = { ... }: This object defines essential configuration options for the MapsIndoors Mapbox view.
accessToken: Your Mapbox access token, required by Mapbox GL JS.
element: The HTML DOM element (our <div id="map">) where the map will be rendered.
After completing these steps and opening your index.html file in a web browser, you should see:
An interactive map centered on the MapsPeople Austin Office.
A Floor Selector control visible in the top-right corner of the map, allowing you to switch between different floors of the venue.
Clicking on any POI or location marker on the map will center the map on that location.
Map doesn't load / blank page:
Check your browser's developer console (usually F12) for any error messages.
Ensure you have replaced YOUR_MAPBOX_ACCESS_TOKEN with a valid token in script.js (or are using the provided demo token correctly).
You have successfully displayed a MapsIndoors map with Mapbox GL JS and added a floor selector! This is the foundational step for building more complex indoor mapping applications.
Next, you will learn how to add search functionality to your map:
Step 2:
This is an example of creating a simple search experience using MapsIndoors. We will create a map with a search button that leads to another Fragment that handles the search and selection. On selection of a location, we go back to the map and shows the selected location on the map.
First create a Fragment or Activity with a map and MapsIndoors loaded.
We will create a Fragment that will contain a textInput field and a RecyclerView that will show a list of MPLocations received from the search.
class FullscreenSearchFragment : Fragment() {As we will be using a RecyclerView we will need to create a RecyclerView Adapter to show our Location results. In this guide we will hijack the Adapter from the Template app:
Setup member variables for FullscreenSearchFragment:
A RecyclerView to contain the locations
The Adapter and LayoutManager for the RecyclerView
Some view components
Init and setup the RecyclerView:
Init and setup the view components to handle searching inside the onViewCreated
create a Runnable to execute a search
Add a listener to the Adapter for when a user selects a location, to navigate back to the map and show the selected location. Here we use navigation together with a bundle to tell the other fragment of the selected location
Now we will implement the FullscreenSearchFragment together with our Fragment or Activity containing a MapsIndoors Map. Add a Button to open the FullscreenSearchFragment inside your Activity or Fragment view and a assign a Click listener to it.
Create the openSearchFragment method to navigate to the FullScreenSearchFragment
Finally create a way to handle the selected location when a user is navigated to your fragment again. How this example is set up the Map will be reloaded when navigated to it. Therefor we will handle the selection after MapControl is created.

Now we have a simple map with a floor selector where you can search for locations. When finishing this step you'll be able to create a directions between two points and change the transportation mode.
After having created our list of search results, we have a good starting point for creating directions between two Locations. Since our search only supports a single search, we will query a random Location within the venue when requesting a route, and use that as the basis for our Origin. Then we'll create a route, navigate to a view of the navigation details, and show a route on the map from the Origin to the Destination.
Now we will create a method that can generate a route for us with a Location (picked from the search list). Start by implementing OnRouteResultListener to your MapsActivity.
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MapsIndoors</title>
<link rel="stylesheet" href="style.css">
<!-- Google Maps JavaScript API -->
<script src="https://maps.googleapis.com/maps/api/js?libraries=geometry&key=YOUR_GOOGLE_MAPS_API_KEY"></script>
<!-- MapsIndoors SDK (already included from initial setup) -->
<script src="https://app.mapsindoors.com/mapsindoors/js/sdk/4.41.0/mapsindoors-4.41.0.js.gz"
integrity="sha384-3lk3cwVPj5MpUyo5T605mB0PMHLLisIhNrSREQsQHjD9EXkHBjz9ETgopmTbfMDc"
crossorigin="anonymous"></script>
</head>
<body>
<!-- This div will hold your map -->
<div id="map"></div>
<script src="script.js"></script>
</body>
</html>/* style.css */
/* Use flexbox for the main layout (from initial setup) */
html, body {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden; /* Prevent scrollbars if map is full size */
display: flex;
flex-direction: column; /* Stack children vertically if needed later */
}
/* Style for the map container */
#map {
/* Make map fill available space within its flex parent (body) */
flex-grow: 1;
width: 100%; /* Make map fill width */
margin: 0;
padding: 0;
}// script.js
// Define options for the MapsIndoors Google Maps view
const mapViewOptions = {
element: document.getElementById('map'),
// Initial map center (MapsPeople - Austin Office example)
center: { lng: -97.74204591828197, lat: 30.36022358949809 },
zoom: 17,
maxZoom: 22,
};
// Set the MapsIndoors API key
mapsindoors.MapsIndoors.setMapsIndoorsApiKey('YOUR_MAPSINDOORS_API_KEY');
// Create a new instance of the MapsIndoors Google Maps view
const mapViewInstance = new mapsindoors.mapView.GoogleMapsView(mapViewOptions);
// Create a new MapsIndoors instance, linking it to the map view.
// This is the main object for interacting with MapsIndoors functionalities.
const mapsIndoorsInstance = new mapsindoors.MapsIndoors({
mapView: mapViewInstance,
// Set the venue ID to load the map for a specific venue
venue: 'YOUR_MAPSINDOORS_VENUE_ID', // Replace with your actual venue ID
});
/** Floor Selector **/
// Create a new HTML div element to host the floor selector
const floorSelectorElement = document.createElement('div');
// Create a new FloorSelector instance, linking it to the HTML element and the main MapsIndoors instance.
new mapsindoors.FloorSelector(floorSelectorElement, mapsIndoorsInstance);
// Get the underlying Google Maps instance
const googleMapInstance = mapViewInstance.getMap();
// Add the floor selector HTML element to the Google Maps controls.
googleMapInstance.controls[google.maps.ControlPosition.TOP_RIGHT].push(floorSelectorElement);
/** Handle Location Clicks on Map **/
// Handle Location Clicks on Map
function handleLocationClick(location) {
if (location && location.id) {
mapsIndoorsInstance.goTo(location); // Center the map on the clicked location
}
}
// Add an event listener to the MapsIndoors instance for click events on locations
mapsIndoorsInstance.on('click', handleLocationClick);private fun search(searchQuery: String) {
//Query with a string to search on
val mpQuery = MPQuery.Builder().setQuery(searchQuery).build()
//Filter for the search query, only taking 30 locations
val mpFilter = MPFilter.Builder().setTake(30).build()
//Query for the locations
MapsIndoors.getLocationsAsync(mpQuery, mpFilter) { list: List<MPLocation?>?, miError: MIError? ->
//Implement UI handling of the search result here
}
}override fun onCreate(savedInstanceState: Bundle?) {
...
mSearchTxtField = findViewById(R.id.search_edit_txt)
//Listener for when the user searches through the keyboard
mSearchTxtField.setOnEditorActionListener { textView, i, _ ->
if (i == EditorInfo.IME_ACTION_DONE || i == EditorInfo.IME_ACTION_SEARCH) {
if (textView.text.isNotEmpty()) {
search(textView.text.toString())
}
return@setOnEditorActionListener true
}
return@setOnEditorActionListener false
}
//ClickListener to start a search, when the user clicks the search button
var searchBtn = findViewById<ImageButton>(R.id.search_btn)
searchBtn.setOnClickListener {
if (mSearchTxtField.text?.length != 0) {
//There is text inside the search field. So lets do the search.
search(mSearchTxtField.text.toString())
}
}
...
}class SearchFragment : Fragment() {
private var mLocations: List<MPLocation?>? = null
private var mMapActivity: MapsActivity? = null
override fun onViewCreated(view: View, @Nullable savedInstanceState: Bundle?) {
val recyclerView = view as RecyclerView
recyclerView.layoutManager = LinearLayoutManager(context)
recyclerView.adapter = mLocations?.let { locations -> SearchItemAdapter(locations, mMapActivity) }
}
...
companion object {
fun newInstance(locations: List<MPLocation?>?, mapsActivity: MapsActivity?): SearchFragment {
val fragment = SearchFragment()
fragment.mLocations = locations
fragment.mMapActivity = mapsActivity
return fragment
}
}
}fun getMapControl(): MapControl {
return mMapControl
}internal class SearchItemAdapter(private val mLocations: List<MPLocation?>, private val mMapActivity: MapsActivity?) : RecyclerView.Adapter<ViewHolder>() {
...
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.text.text = mLocations[position]?.name
if (mMapActivity != null) {
mLocations[position]?.let { MapsIndoors.getDisplayRule(it) }?.getIconAsync {
mMapActivity.runOnUiThread {
holder.imageView.setImageBitmap(it)
}
}
}
}
...
}
internal class ViewHolder(inflater: LayoutInflater, parent: ViewGroup?) : RecyclerView.ViewHolder(inflater.inflate(R.layout.fragment_search_list_item, parent, false)) {
val text: TextView
val imageView: ImageView
init {
text = itemView.findViewById(R.id.text)
imageView = itemView.findViewById(R.id.location_image)
}
}private fun search(searchQuery: String) {
//Query with a string to search on
val mpQuery = MPQuery.Builder().setQuery(searchQuery).build()
//Filter for the search query, only taking 30 locations
val mpFilter = MPFilter.Builder().setTake(30).build()
//Query for the locations
MapsIndoors.getLocationsAsync(mpQuery, mpFilter) { list: List<MPLocation?>?, miError: MIError? ->
//Check if there is no error and the list is not empty
if (miError == null && !list.isNullOrEmpty()) {
//Create a new instance of the search fragment
mSearchFragment = SearchFragment.newInstance(list, this)
//Make a transaction to the bottom sheet
addFragmentToBottomSheet(mSearchFragment)
//Clear the search text, since we got a result
mSearchTxtField.text?.clear()
...
}
}
}private fun search(searchQuery: String) {
MapsIndoors.getLocationsAsync(mpQuery, mpFilter) { list: List<MPLocation?>?, miError: MIError? ->
//Calling displaySearchResults on the ui thread as camera movement is involved
runOnUiThread { mMapControl.setFilter(list, MPFilterBehavior.DEFAULT) }
}
}class MPSearchItemRecyclerViewAdapter : RecyclerView.Adapter<MPSearchItemRecyclerViewAdapter.ViewHolder>() {
private var mLocations: List<MPLocation> = ArrayList()
private lateinit var context: Context
private var mOnLocationSelectedListener: OnLocationSelectedListener? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
context = parent.context
return ViewHolder(FragmentSearchItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = mLocations[position]
var iconUrl = getTypeIcon(item)
iconUrl?.let {
MapsIndoors.getImageProvider().loadImageAsync(it) { bitmap, error ->
if (bitmap != null && error == null) {
holder.icon.setImageBitmap(bitmap)
}
}
}
holder.nameView.text = item.name
if (item.floorName != null && item.buildingName != null) {
val buildingName = MapsIndoors.getBuildings()?.getBuilding(item.point.latLng)?.name
if (buildingName != null) {
holder.subTextView.text = "Floor: " + item.floorName + " - " + buildingName
}else {
holder.subTextView.text = "Floor: " + item.floorName + " - " + item.buildingName
}
}else {
holder.subTextView.visibility = View.GONE
}
holder.itemView.setOnClickListener {
if (mOnLocationSelectedListener != null) {
mOnLocationSelectedListener?.onLocationSelected(item)
}
}
}
private fun getTypeIcon(mpLocation: MPLocation): String? {
MapsIndoors.getSolution()?.let {
it.types?.forEach { type ->
if (mpLocation.type?.equals(type.name, true) == true) {
return type.icon
}
}
}
return null
}
fun setOnLocationSelectedListener(onLocationSelectedListener: OnLocationSelectedListener) {
mOnLocationSelectedListener = onLocationSelectedListener
}
override fun getItemCount(): Int = mLocations.size
fun setLocations(locations: List<MPLocation>) {
mLocations = locations;
}
fun clear() {
mLocations = ArrayList()
notifyDataSetChanged()
}
inner class ViewHolder(binding: FragmentSearchItemBinding) :
RecyclerView.ViewHolder(binding.root) {
val icon: ImageView = binding.locationIconView
val nameView: TextView = binding.locationName
val subTextView: TextView = binding.locationSubtext
override fun toString(): String {
return super.toString() + " '" + subTextView.text + "'"
}
}
}

private lateinit var mRecyclerView: RecyclerView
private lateinit var mLinearLayoutManager: LinearLayoutManager
private val mAdapter: MPSearchItemRecyclerViewAdapter = MPSearchItemRecyclerViewAdapter()
private lateinit var searchInputTextView: TextInputEditText
private var searchHandler: Handler? = nullmRecyclerView = binding.searchList
mLinearLayoutManager = LinearLayoutManager(requireContext())
mRecyclerView.apply {
layoutManager = mLinearLayoutManager
adapter = mAdapter
}searchInputTextView = binding.searchInputEditText
val imm = requireActivity().getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
searchInputTextView.addTextChangedListener {
searchHandler = Handler(Looper.myLooper()!!)
searchHandler!!.postDelayed(searchRunner, 1000)
}
searchInputTextView.setOnEditorActionListener { textView, i, keyEvent ->
if (i == EditorInfo.IME_ACTION_DONE || i == EditorInfo.IME_ACTION_SEARCH) {
if (textView.text.isNotEmpty()) {
search(textView.text.toString())
}
//Making sure keyboard is closed.
imm.hideSoftInputFromWindow(textView.windowToken, 0)
return@setOnEditorActionListener true
}
return@setOnEditorActionListener false
}private val searchRunner: Runnable = Runnable {
val text = searchInputTextView.text
if (text?.length!! >= 2) {
search(text.toString())
}
}mAdapter.setOnLocationSelectedListener { location ->
if (location != null) {
val bundle = Bundle()
bundle.putString("locationId", location.locationId)
findNavController().navigate(R.id.action_nav_search_fullscreen_to_nav_search, bundle)
return@setOnLocationSelectedListener true
}
return@setOnLocationSelectedListener false
}binding.searchButton.setOnClickListener {
openSearchFragment()
}private fun openSearchFragment() {
val navController = findNavController()
navController.navigate(R.id.action_nav_search_to_nav_search_fullscreen)
}MapControl.create(mapConfig) { mapControl: MapControl?, miError: MIError? ->
mMapControl = mapControl
//Enable Live Data on the map
if (miError == null) {
var locationId = arguments?.get("locationId") as String?
if (locationId != null) {
mMapControl?.selectLocation(locationId, MPSelectionBehavior.DEFAULT)
}else {
//No errors so getting the first venue (in the white house solution the only one)
val venue = MapsIndoors.getVenues()?.defaultVenue
activity?.runOnUiThread {
if (venue != null) {
//Animates the camera to fit the new venue
mMap!!.animateCamera(
CameraUpdateFactory.newLatLngBounds(
LatLngBoundsConverter.toLatLngBounds(venue.bounds!!),
19
)
)
}
}
}
}
}mapsIndoorsInstanceFloorSelectorconst googleMapInstance = mapViewInstance.getMap();: The getMap() method on our mapViewInstance returns the underlying native Google Maps Map object. This is necessary to use Google Maps-specific functionalities.
googleMapInstance.controls[google.maps.ControlPosition.TOP_RIGHT].push(floorSelectorElement);: This uses the native Google Maps controls API to add our floorSelectorElement to the map UI in the top-right corner. For more details, refer to the Google Maps JavaScript API documentation on controls.
Click-to-Center Functionality:
function handleLocationClick(location) { ... }: This function is called whenever a location (POI) on the map is clicked. It checks that the clicked object is a valid MapsIndoors location and then calls mapsIndoorsInstance.goTo(location) to center the map on that location.
mapsIndoorsInstance.on('click', handleLocationClick);: This line registers the click handler so that clicking any POI on the map will smoothly center the map on that location. This pattern will be reused and expanded in later steps.
The MapsIndoors SDK script (mapsindoors-4.41.0.js.gz) should already be present from the initial setup guide. You can always check the MapsIndoors SDK reference documentation for the latest SDK version.
An empty <div> with the attribute id="map" was added inside the <body>. This div is crucial as it serves as the container where both the Mapbox base map and the MapsIndoors layers will be rendered.
margin: 0; and padding: 0; remove any default browser spacing around the map container, ensuring it fits snugly.
center, zoom, maxZoom: Standard map parameters to set the initial view.
mapsIndoorsTransitionLevel: The zoom level at which MapsIndoors will start showing indoor details.
For more details on all available options, see the mapsindoors.mapView.MapboxV3View class documentation.
mapsindoors.MapsIndoors.setMapsIndoorsApiKey('YOUR_MAPSINDOORS_API_KEY');: This static method sets your MapsIndoors API key globally for the SDK. This key authenticates your requests to MapsIndoors services. See the mapsindoors.MapsIndoors class reference.
const mapViewInstance = new mapsindoors.mapView.MapboxV3View(mapViewOptions);: This line creates an instance of MapboxV3View, which is responsible for integrating MapsIndoors data and rendering with a Mapbox GL JS v3 map. For more details on MapboxV3View, see its class reference.
const mapsIndoorsInstance = new mapsindoors.MapsIndoors({ mapView: mapViewInstance, venue: 'YOUR_MAPSINDOORS_VENUE_ID' });: This creates the main MapsIndoors instance. This object is your primary interface for interacting with MapsIndoors functionalities like displaying locations, getting directions, etc. See the mapsindoors.MapsIndoors class documentation.
Floor Selector Integration:
const floorSelectorElement = document.createElement('div');: A new HTML div element is dynamically created. This element will serve as the container for the Floor Selector UI.
new mapsindoors.FloorSelector(floorSelectorElement, mapsIndoorsInstance);: This instantiates the FloorSelector control. It takes the HTML element to render into and the mapsIndoorsInstance to interact with (e.g., to know available floors and change the current floor). For more details on FloorSelector, see its .
const mapboxInstance = mapViewInstance.getMap();: The getMap() method on our mapViewInstance returns the underlying native Mapbox Map object. This is necessary to use Mapbox-specific functionalities.
mapboxInstance.addControl({ ... }, 'top-right');: This uses the native Mapbox addControl method to add our floorSelectorElement to the map UI in the top-right corner. For more details, refer to the .
Click-to-Center Functionality:
function handleLocationClick(location) { ... }: This function is called whenever a location (POI) on the map is clicked. It checks that the clicked object is a valid MapsIndoors location and then calls mapsIndoorsInstance.goTo(location) to center the map on that location.
mapsIndoorsInstance.on('click', handleLocationClick);: This line registers the click handler so that clicking any POI on the map will smoothly center the map on that location. This pattern will be reused and expanded in later steps.
Verify that YOUR_MAPSINDOORS_API_KEY is correctly set (or the demo key is used).
Double-check all CDN links in index.html are correct and accessible.
Floor selector doesn't appear:
Verify the JavaScript code for creating and adding the floor selector has no typos.
Check the console for errors related to FloorSelector or addControl.
Clicking a location does not center the map:
Ensure the handleLocationClick function is correctly defined and registered as an event listener.
Check for any JavaScript errors in the console that might indicate issues with the click handling code.
class MapsActivity : FragmentActivity(), OnMapReadyCallback, OnRouteResultListenerclass MapsActivity : FragmentActivity(), OnRouteResultListenerImplement the onRouteResult method and create a method called createRoute(MPLocation mpLocation) on your MapsActivity.
Use this method to query the MPDirectionsService, which generates a route between two coordinates. We will use this to query a route with our hardcoded mUserLocation and a point from a MPLocation.
To generate a route with the MPLocation, we start by creating an onClickListener on our search ViewHolder inside the SearchItemAdapter. In the method onBindViewHolder we will call our createRoute on the MapsActivity for our route to be generated.
We start by implementing logic to our createRoute method to query a route through MPDirectionsService and assign the onRouteResultListener to the activity. When we call the createRoute through our onClickListener we will receive a result through our onRouteResult implementation.
When we receive a result on our listener, we render the route through the MPDirectionsRenderer.
We create global variables of the MPdirectionsRenderer and MPDirectionsService and create a getter to the MPdirectionsRenderer to access it from fragments later on.
See the full implementation of these methods here: MapsActivity.kt
Now we will implement logic to our NavigationFragment that we can put into our BottomSheet and show the steps for each route, as well as the time and distance it takes to travel the route.
Here we'll use a viewpager to allow the user to switch between each step, as well as display a "close" button so we are able to remove the route and the bottom sheet from the activity.
We will start by making a getter for our MPdirectionsRenderer that we store on MapsActivity:
Inside the NavigationFragment we will implement logic to navigate through Legs of our Route.
See the full implementation of NavigationFragment and the accompanying adapter here: NavigationFragment.kt
We will then create a simple textview to describe each step of the Route Leg in the RouteLegFragment for the ViewPager:
See the full implementation of the fragment here: RouteLegFragment.kt
In MapsIndoors, the transportation mode is referred to as travel mode. There are four travel modes, walking, bicycling, driving and transit (public transportation). The travel modes generally applies for outdoor navigation. Indoor navigation calculations are based on walking travel mode.
To swap Travel Modes you set the Travel Mode before making a query for the route:
Expected result:
Congratulations! You're at the end of your journey (for now), and you've accomplished a lot! 🎉
You learned which prerequisites is needed to start building with MapsIndoors.
You loaded a interactive map with MapsIndoors locations and added a floor selector for navigating between floors.
You created a search experience to search for specific locations on the map.
You added functionality for getting directions from one Location to another.
This concludes the "Getting Started" tutorial, but there's always more to discover. To get more inspiration on what to build next please visit our showcase page to see how other clients use MapsIndoors! For more documentation, please visit the rest of our Docs site!.

Cacheable Data
MapsIndoors has three levels of caching:
Basic Data: All descriptions, geometries and metadata about POIs, rooms, areas, buildings and venues.
Detailed Data: The same as Basic Data, plus images referenced by the data.
Full Dataset: The same as Detailed Data, plus Map Tiles.
Full Dataset caching requires that Map Tiles are prepared specifically for this purpose. Contact MapsPeople in order to arrange this.
Automatic Caching
Out of the box, MapsIndoors automatically caches all basic data for the active dataset on the device, whereas images and Map Tiles are cached only as they are used.
This means all MapsIndoors-specific data is cached automatically, but images are only cached after they have been needed for map display. Likewise, Map Tiles are only cached when needed for map display, so all parts of the map that has been shown are cached. Areas and Zoom Levels that have not been shown as part of user interaction are not cached.
Tweaking Caching Behaviour
Applications have a few ways to change the default caching behaviour:
The synchronization process can be started manually:
The level of caching can be changed:
Caching of Multiple Datasets
The most common use of MapsIndoors involves only one dataset, but for large deployments, data may be partitioned into multiple datasets.
Offline caching of multiple simultaneous datasets is fully supported, and is mostly limited by the available storage space on device.
NOTE: Only one dataset is active at any point in time.
Management of multiple datasets is done via MPDataSetCacheManager, which allows querying, adding, modifying and removing datasets.
Listing Managed Datasets
All datasets currently managed are accessible via the MPDataSetCacheManager:
This can be used to build a management user interface, and information about individual datasets can be accessed from the MPDataSetCache and MPDataSetCacheItem classes.
Adding Datasets for Offline Caching
Datasets are scheduled for caching using MPDataSetCacheManager:
The current MapsIndoors API key is automatically added as a managed dataset with MPDataSetCacheScope.BASIC.
Removing Datasets
Datasets are removed from caching using MPDataSetCacheManager.getInstance().removeDataSetCache(MPDataSetCache);:
NOTE: The currently active dataset is not removed.
Changing Caching Parameters
To change the extent of caching, for example in a management menu:
Determining the Caching Size of a Dataset
The estimated and cached size of a dataset is available via:
To refresh or get the size of a synced dataset:
This is an asynchronous process, and a MPDataSetCacheManagerSizeListener is needed for getting information about progress and results.
Synchronizing Data with MPDataSetCacheManager
The MPDataSetCacheManagerallows for detailed control over which datasets are synchronized, and allows for cancellation:
Cacheable Data
MapsIndoors has three levels of caching:
Basic Data: All descriptions, geometries and metadata about POIs, rooms, areas, buildings and venues.
Detailed Data: The same as Basic Data, plus images referenced by the data.
Android v4
This tutorial will show how to work with the route model returned from a directions service call. It will also show how you can utilize interactions between the route rendering on the map, and text-based instructions showed in another view.
This tutorial will be based off the MapsIndoors Samples example found here: .
An example of the view XML file for the WayfindingFragment this guide will use can be found here: .
First, create variables for MPLocation and MPRoute objects to use later in describing wayfinding. Also create a Variable for MPDirectionsRenderer so we can control rendering through changes in the ViewPager.
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MapsIndoors</title>
<link rel="stylesheet" href="style.css">
<!-- Mapbox GL JS CSS -->
<link href='https://api.mapbox.com/mapbox-gl-js/v3.10.0/mapbox-gl.css' rel='stylesheet' />
<!-- MapsIndoors SDK (already included from initial setup) -->
<script src="https://app.mapsindoors.com/mapsindoors/js/sdk/4.41.0/mapsindoors-4.41.0.js.gz"
integrity="sha384-3lk3cwVPj5MpUyo5T605mB0PMHLLisIhNrSREQsQHjD9EXkHBjz9ETgopmTbfMDc"
crossorigin="anonymous"></script>
<!-- Mapbox GL JS -->
<script src='https://api.mapbox.com/mapbox-gl-js/v3.10.0/mapbox-gl.js'></script>
</head>
<body>
<!-- This div will hold your map -->
<div id="map"></div>
<script src="script.js"></script>
</body>
</html>/* style.css */
/* Use flexbox for the main layout (from initial setup) */
html, body {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden; /* Prevent scrollbars if map is full size */
display: flex;
flex-direction: column; /* Stack children vertically if needed later */
}
/* Style for the map container */
#map {
/* Make map fill available space within its flex parent (body) */
flex-grow: 1;
width: 100%; /* Make map fill width */
margin: 0;
padding: 0;
}// script.js
// Define options for the MapsIndoors Mapbox view
const mapViewOptions = {
accessToken: 'YOUR_MAPBOX_ACCESS_TOKEN', // Replace with your Mapbox token
element: document.getElementById('map'),
center: { lng: -97.74204591828197, lat: 30.36022358949809 }, // Example: MapsPeople Austin Office
zoom: 17,
maxZoom: 22,
mapsIndoorsTransitionLevel: 16,
};
// Set the MapsIndoors API key
mapsindoors.MapsIndoors.setMapsIndoorsApiKey('YOUR_MAPSINDOORS_API_KEY'); // Replace with your MapsIndoors API key
// Create a new instance of the MapsIndoors Mapbox view
const mapViewInstance = new mapsindoors.mapView.MapboxV3View(mapViewOptions);
// Create a new MapsIndoors instance, passing the map view
const mapsIndoorsInstance = new mapsindoors.MapsIndoors({
mapView: mapViewInstance,
// Set the venue ID to load the map for a specific venue
venue: 'YOUR_MAPSINDOORS_VENUE_ID', // Replace with your actual venue ID
});
/** Floor Selector **/
// Create a new HTML div element to host the floor selector
const floorSelectorElement = document.createElement('div');
// Create a new FloorSelector instance, linking it to the HTML element and the main MapsIndoors instance.
new mapsindoors.FloorSelector(floorSelectorElement, mapsIndoorsInstance);
// Get the underlying Mapbox map instance
const mapboxInstance = mapViewInstance.getMap();
// Add the floor selector HTML element to the Mapbox map using Mapbox's addControl method
// We wrap the element in an object implementing the IControl interface expected by addControl
mapboxInstance.addControl({
onAdd: function () {
// This function is called when the control is added to the map.
// It should return the control's DOM element.
return floorSelectorElement;
},
onRemove: function () {
// This function is called when the control is removed from the map.
// Clean up any event listeners or resources here.
floorSelectorElement.parentNode.removeChild(floorSelectorElement);
},
}, 'top-right'); // Optional: Specify a position ('top-left', 'top-right', 'bottom-left', 'bottom-right')
/** Handle Location Clicks **/
// Handle Location Clicks on Map
function handleLocationClick(location) {
if (location && location.id) {
mapsIndoorsInstance.goTo(location); // Center the map on the clicked location
}
}
// Add an event listener to the MapsIndoors instance for click events on locations
mapsIndoorsInstance.on('click', handleLocationClick);override fun onBindViewHolder(holder: ViewHolder, position: Int) {
...
holder.itemView.setOnClickListener {
mLocations[position]?.let { locations -> mMapActivity?.createRoute(locations) }
//Clearing map to remove the location filter from our search result
mMapActivity?.getMapControl()?.clearFilter()
}
...
}fun createRoute(mpLocation: MPLocation) {
//If MPRoutingProvider has not been instantiated create it here and assign the results call back to the activity.
if (mpRoutingProvider == null) {
mpRoutingProvider = MPDirectionsService(this)
mpRoutingProvider?.setRouteResultListener(this)
mpRoutingProvider?.setTravelMode(MPTravelMode.WALKING)
}
//Use the locations venue to query an origin point for the route. Within the venue bounds.
if (mpLocation.venue == null) {
//Open dialog telling user to try another location, as no venue is assigned to the location.
AlertDialog.Builder(this)
.setTitle("No venue assigned")
.setMessage("Please try another location")
.show()
} else {
val venue = MapsIndoors.getVenues()?.getVenueByName(mpLocation.venue!!)
MapsIndoors.getLocationsAsync(null, MPFilter.Builder().setMapExtend(MPMapExtend(venue!!.bounds!!)).build()) { list: List<MPLocation?>?, miError: MIError? ->
if (!list.isNullOrEmpty()) {
list.first()?.let { location ->
//Queries the MPRouting provider for a route with the hardcoded user location and the point from a location.
mpRoutingProvider?.query(location.point, mpLocation.point)
}
} else {
AlertDialog.Builder(this)
.setTitle("No locations found within venue of location")
.setMessage("Please try another location")
.show()
}
}
}
}
override fun onRouteResult(@Nullable route: Route?, @Nullable miError: MIError?) {
...
//Create the MPDirectionsRenderer if it has not been instantiated.
if (mpDirectionsRenderer == null) {
mpDirectionsRenderer = MPDirectionsRenderer(mMapControl)
}
//Set the route on the Directions renderer
mpDirectionsRenderer?.setRoute(route)
//Create a new instance of the navigation fragment
mNavigationFragment = NavigationFragment.newInstance(route, this)
//Start a transaction and assign it to the BottomSheet
addFragmentToBottomSheet(mNavigationFragment)
}fun getMpDirectionsRenderer(): MPDirectionsRenderer? {
return mpDirectionsRenderer
}class NavigationFragment : Fragment() {
private var mRoute: MPRoute? = null
private var mMapsActivity: MapsActivity? = null
...
override fun onViewCreated(view: View, @Nullable savedInstanceState: Bundle?) {
val routeCollectionAdapter = RouteCollectionAdapter(this)
val mViewPager: ViewPager2 = view.findViewById(R.id.view_pager)
mViewPager.adapter = routeCollectionAdapter
mViewPager.registerOnPageChangeCallback(object : OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
//When a page is selected call the renderer with the index
mMapsActivity?.getMpDirectionsRenderer()?.selectLegIndex(position)
//Update the floor on mapcontrol if the floor might have changed for the routing
mMapsActivity?.getMpDirectionsRenderer()?.selectedLegFloorIndex?.let {floorIndex ->
mMapsActivity?.getMapControl()?.selectFloor(floorIndex)
}
}
})
...
//Button for closing the bottom sheet. Clears the route through directionsRenderer as well, and changes map padding.
closeBtn.setOnClickListener {
mMapsActivity!!.removeFragmentFromBottomSheet(this)
mMapsActivity!!.getMpDirectionsRenderer()?.clear()
}
//Next button for going through the legs of the route.
nextBtn.setOnClickListener {
mViewPager.setCurrentItem(
mViewPager.currentItem + 1,
true
)
}
//Back button for going through the legs of the route.
backBtn.setOnClickListener {
mViewPager.setCurrentItem(
mViewPager.currentItem - 1,
true
)
}
//Describing the distance in meters
distanceTxtView.text = "Distance: " + mRoute?.getDistance().toString() + " m"
//Describing the time it takes for the route in minutes
infoTxtView.text = "Time for route: " + mRoute?.duration?.toLong()?.let {duration ->
TimeUnit.MINUTES.convert(duration, TimeUnit.SECONDS).toString()
} + " minutes"
}
inner class RouteCollectionAdapter(fragment: Fragment?) :
...
}
companion object {
fun newInstance(route: Route?, mapsActivity: MapsActivity?): NavigationFragment {
val fragment = NavigationFragment()
fragment.mRoute = route
fragment.mMapsActivity = mapsActivity
return fragment
}
}
}class RouteLegFragment : Fragment() {
private var mRouteLeg: MPRouteLeg? = null
...
override fun onViewCreated(view: View, @Nullable savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//Assigning views
val fromTxtView = view.findViewById<TextView>(R.id.from_text_view)
var stepsString = ""
//A loop to write what to do for each step of the leg.
for (i in mRouteLeg!!.steps.indices) {
val routeStep = mRouteLeg!!.steps[i]
stepsString += """
Step ${i + 1}${routeStep.maneuver}
""".trimIndent()
}
fromTxtView.text = stepsString
}
companion object {
fun newInstance(routeLeg: MPRouteLeg?): RouteLegFragment {
val fragment = RouteLegFragment()
fragment.mRouteLeg = routeLeg
return fragment
}
}
}fun createRoute(mpLocation: MPLocation) {
//If MPDirectionsService has not been instantiated create it here and assign the results call back to the activity.
if (mpDirectionsService == null) {
mpDirectionsService = MPDirectionsService(this)
mpDirectionsService?.setRouteResultListener(this)
}
mpDirectionsService?.setTravelMode(MPTravelMode.WALKING)
//Queries the MPRouting provider for a route with the hardcoded user location and the point from a location.
mpDirectionsService?.query(mUserLocation, mpLocation.point)
}
Full Dataset caching requires that Map Tiles are prepared specifically for this purpose. Contact MapsPeople in order to arrange this.
Automatic Caching
Out of the box, MapsIndoors automatically caches all basic data for the active dataset on the device, whereas images and Map Tiles are cached only as they are used.
This means all MapsIndoors-specific data is cached automatically, but images are only cached after they have been needed for map display. Likewise, Map Tiles are only cached when needed for map display, so all parts of the map that has been shown are cached. Areas and Zoom Levels that have not been shown as part of user interaction are not cached.
Tweaking Caching Behaviour
Applications have a few ways to change the default caching behaviour:
The synchronization process can be started manually:
The level of caching can be changed:
Caching of Multiple Datasets
The most common use of MapsIndoors involves only one dataset, but for large deployments, data may be partitioned into multiple datasets.
Offline caching of multiple simultaneous datasets is fully supported, and is mostly limited by the available storage space on device.
NOTE: Only one dataset is active at any point in time.
Management of multiple datasets is done via MPDataSetCacheManager, which allows querying, adding, modifying and removing datasets.
Listing Managed Datasets
All datasets currently managed are accessible via the MPDataSetCacheManager:
This can be used to build a management user interface, and information about individual datasets can be accessed from the MPDataSetCache and MPDataSetCacheItem classes.
Adding Datasets for Offline Caching
Datasets are scheduled for caching using MPDataSetCacheManager:
The current MapsIndoors API key is automatically added as a managed dataset with MPDataSetCacheScope.BASIC.
Removing Datasets
Datasets are removed from caching using MPDataSetCacheManager.getInstance().removeDataSetCache(MPDataSetCache);:
NOTE: The currently active dataset is not removed.
Changing Caching Parameters
To change the extent of caching, for example in a management menu:
Determining the Caching Size of a Dataset
The estimated and cached size of a dataset is available via:
To refresh or get the size of a synced dataset:
This is an asynchronous process, and a MPDataSetCacheManagerSizeListener is needed for getting information about progress and results.
Synchronizing Data with MPDataSetCacheManager
The MPDataSetCacheManagerallows for detailed control over which datasets are synchronized, and allows for cancellation:
MapsIndoors.synchronizeContent((e) -> {
...
});MPDataSetCache dataSet = MPDataSetCacheManager.getInstance().getDataSetByID("API KEY");
dataSet.setScope(mContext, MPDataSetCacheScope.DETAILED);
MPDataSetCacheManager.getInstance().synchronizeDataSets(Collections.singletonList(dataSet));Next step we will create the Fragment that will contain a short description of each Leg of the route inside the ViewPager
An example of the view XML file for the RouteLegFragment, that this guide will use, can be found here: RouteLeg view.
We will start by adding the code inside the WayfindingFragment.
First, start by changing the code inside onCreateView.
Next we will create FragmentStateAdapter that will be used on the ViewPager to contain RouteLegFragments
Let's create a method to textually describe each Leg. This method creates a string that takes the first and last step of the next leg to create a description for the user on what to do at the end of the currently shown leg. You will also create a method to get a list of the different highway types the route can give the user. These are found as enums through the MPHighway class in the MapsIndoors SDK.
Now, lets create the RouteLegFragment to give context for the Legs in the WayfindingFragment
You must also update the onViewCreated method to use the new views added earlier in the tutorial.
Now you have the revised UI for providing the user with a more explanatory route description when navigating. Now it needs to be rendered onto the map.
We will create a method named getRoute that queries a route between two location, we know exist on the current solution. When we receive the route we will assign the route to the fragment as well as calling setRoute on the MPDirectionsRenderer with the received Route. We will also notify the ViewPager that the route is updated with notifyDataSetChanged.
To change the routing when swapping between tabs on the viewpager, use the call back that we added further up inside the onViewCreated of NavigationFragment.
You should now have a Fragment with a map that has a route rendered on it. With descriptions of each Leg of the Route inside a ViewPager that will display the Leg related to the page that is viewed.
The full working example can be found here: WayFinding.
for (MPDataSetCache dataSet : MPDataSetCacheManager.getInstance().getManagedDataSets()) {
Log.i("dataset", dataSet.getSolutionId() + ": size " + dataSet.getCacheItem().getSyncSize());
}MPDataSetCacheManager.getInstance().addDataSetWithCachingScope("API KEY", MPDataSetCacheScope.BASIC);MPDataSetCacheManager.getInstance().removeDataSetCache(MPDataSetCache);MPDataSetCache dataSet = MPDataSetCacheManager.getInstance().getDataSetByID("API KEY");
dataSet.setScope(mContext, MPDataSetCacheScope.DETAILED);
MPDataSetCacheManager.getInstance().synchronizeDataSets(Collections.singletonList(dataSet));dataSet.getCacheItem().getCacheSize(mContext);
dataSet.getCacheItem().getSyncSize();MPDataSetCacheManager.getInstance().getSyncSizesForDataSetCaches(Collections.singletonList(dataSet), this);MPDataSetCacheManager dataSetCacheManager = MPDataSetCacheManager.getInstance();
// sync all managed datasets
dataSetCacheManager.synchronizeDataSets();
// sync specific datasets
dataSetCacheManager.synchronizeDataSets(dataSets);MapsIndoors.synchronizeContent { error ->
...
}val dataset = MPDataSetCacheManager.getInstance().getDataSetByID("API KEY")
dataset?.setScope(mContext, MPDataSetCacheScope.DETAILED)
MPDataSetCacheManager.getInstance().synchronizeDataSets(Collections.singletonList(dataset))for (dataSet in MPDataSetCacheManager.getInstance().managedDataSets) {
Log.i("dataset", dataSet.solutionId + ": size " + dataSet.cacheItem.syncSize)
}MPDataSetCacheManager.getInstance()
.addDataSetWithCachingScope("API KEY", MPDataSetCacheScope.BASIC)MPDataSetCacheManager.getInstance().removeDataSetCache(MPDataSetCache)val dataset = MPDataSetCacheManager.getInstance().getDataSetByID("API KEY")
dataset?.setScope(mContext, MPDataSetCacheScope.DETAILED)
MPDataSetCacheManager.getInstance().synchronizeDataSets(Collections.singletonList(dataset))dataSet?.cacheItem?.getCacheSize(mContext)
dataSet?.cacheItem?.syncSizeMPDataSetCacheManager.getInstance().getSyncSizesForDataSetCaches(listOf(dataSet), this)MPDataSetCacheManager dataSetCacheManager = MPDataSetCacheManager.getInstance();
// sync all managed datasets
dataSetCacheManager.synchronizeDataSets()
// sync specific datasets
dataSetCacheManager.synchronizeDataSets(dataSets)private var mRoute: MPRoute? = null
private var mLocation: MPLocation? = nulloverride fun onCreateView(view: View, @Nullable savedInstanceState: Bundle?) {
_binding = FragmentWayfindingBinding.inflate(inflater, container, false)
MapsIndoors.load(requireActivity().applicationContext, "gettingstarted", null)
val routeLegAdapter = RouteCollectionAdapter(this)
val viewPager = binding.stepViewPager
viewPager.adapter = routeLegAdapter
viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
}
})
val root: View = binding.root
val supportMapFragment = childFragmentManager.findFragmentById(R.id.map) as SupportMapFragment
mMapView = supportMapFragment.view
supportMapFragment.getMapAsync(this)
return root
}inner class RouteCollectionAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
override fun getItemCount(): Int {
mRoute?.legs?.let { legs->
return legs.size
}
return 0
}
override fun createFragment(position: Int): Fragment {
if (position == mRoute?.legs?.size!! - 1) {
return RouteLegFragment.newInstance("Walk to " + mLocation?.name, mRoute?.legs!![position]?.distance?.toInt(), mRoute?.legs!![position]?.duration?.toInt())
} else {
var leg = mRoute?.legs!![position]
var firstStep = leg.steps.first()
var lastFirstStep = mRoute?.legs!![position + 1].steps.first()
var lastStep = mRoute?.legs!![position + 1].steps.last()
var firstBuilding = MapsIndoors.getBuildings()?.getBuilding(firstStep.startPoint.latLng)
var lastBuilding = MapsIndoors.getBuildings()?.getBuilding(lastStep.startPoint.latLng)
return if (firstBuilding != null && lastBuilding != null) {
RouteLegFragment.newInstance(getStepName(lastFirstStep, lastStep), leg.distance.toInt(), leg.duration.toInt())
}else if (firstBuilding != null) {
RouteLegFragment.newInstance("Exit: " + firstBuilding.name, leg.distance.toInt(), leg.duration.toInt())
}else {
RouteLegFragment.newInstance("Enter: " + lastBuilding?.name, leg.distance.toInt(), leg.duration.toInt())
}
}
}
}fun getStepName(startStep: MPRouteStep, endStep: MPRouteStep): String {
val startStepZindex: Double = startStep.startLocation!!.zIndex
val startStepFloorName: String = startStep.startLocation.floorName!!
var highway: String? = null
for (actionName in getActionNames()) {
if (startStep.highway == actionName) {
highway = if (actionName == MPHighway.STEPS) {
"stairs"
} else {
actionName
}
}
}
if (highway != null) {
return java.lang.String.format(
"Take %s to %s %s",
highway,
"level",
if (endStep.endLocation.floorName!!.isEmpty()) endStep.endLocation.zIndex else endStep.endLocation.floorName
)
}
if (startStepFloorName == endStep.endLocation.floorName) {
return "Walk to next step"
}
val endStepFloorName: String = endStep.endLocation.floorName!!
return if (endStepFloorName.isEmpty()) java.lang.String.format(
"Level %s to %s",
startStepFloorName.ifEmpty { startStepZindex },
endStep.endPoint.floorIndex
) else String.format(
"Level %s to %s",
startStepFloorName.ifEmpty { startStepZindex },
endStepFloorName
)
}
private fun getActionNames(): ArrayList<String> {
val actionNames: ArrayList<String> = ArrayList()
actionNames.add(MPHighway.ELEVATOR)
actionNames.add(MPHighway.ESCALATOR)
actionNames.add(MPHighway.STEPS)
actionNames.add(MPHighway.TRAVELATOR)
actionNames.add(MPHighway.RAMP)
actionNames.add(MPHighway.WHEELCHAIRLIFT)
actionNames.add(MPHighway.WHEELCHAIRRAMP)
actionNames.add(MPHighway.LADDER)
return actionNames
}private var mStep: String? = null
private var mDuration: Int? = null
private var mDistance: Int? = null
companion object {
@JvmStatic
fun newInstance(step: String, distance: Int?, duration: Int?) =
RouteLegFragment().apply {
mStep = step
mDistance = distance
mDuration = duration
}
}override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.stepTextView.text = mStep
if (Locale.getDefault().country == "US") {
binding.distanceTextView.text = (mDistance?.times(3.281))?.toInt().toString() + " feet"
}else {
binding.distanceTextView.text = mDistance?.toString() + " m"
}
mDuration?.let {
if (it < 60) {
binding.durationTextView.text = "$it sec"
}else {
binding.durationTextView.text = TimeUnit.MINUTES.convert(it.toLong(), TimeUnit.SECONDS).toString() + " min"
}
}
}fun getRoute() {
val directionsService = MPDirectionsService(requireContext())
if (mDirectionsRenderer == null) {
mDirectionsRenderer = MPDirectionsRenderer(mMapControl!!)
}
directionsService.setRouteResultListener { mpRoute, miError ->
if (miError == null && mpRoute != null) {
mRoute = mpRoute
mDirectionsRenderer?.setRoute(mpRoute)
requireActivity().runOnUiThread {
binding.stepViewPager.adapter?.notifyDataSetChanged()
}
}
}
val location = MapsIndoors.getLocationById("5a07435a4e074edc9396b2ff")
mLocation = MapsIndoors.getLocationById("24ede0c9a5004a148bd01d96")
if (location != null && mLocation != null) {
directionsService.query(location.point, mLocation!!.point)
}
}val routeLegAdapter = RouteCollectionAdapter(this)
val viewPager = binding.stepViewPager
viewPager.adapter = routeLegAdapter
viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
mDirectionsRenderer?.selectLegIndex(position)
mDirectionsRenderer?.selectedLegFloorIndex
}
})The External ID is a reference from your real-life data to a piece of MapsIndoors geodata.
In a large venue like a conference hall, headquarter, or university, every room will have a unique ID like 1.234AB or HALL_A in a naming scheme that makes sense to that organization.
All MapsIndoors geospatial objects contain an internal ID. While this can be used for performing lookups, it means that in general as a developer you'll need to either query for them, or create your own mapping tables against your own resources.
Our recommendation is to use the externalId as the identifier for an your system's ID or another external system of your choosing.
If you have a queue monitoring system and want to display some regularly updated statuses on a piece of geodata in MapsIndoors like a room, poi, or area, you can use the External ID as the common denominator between the systems.
There are many ways you can utilize the power of external ID as a reference point for one of your systems, and we recommend looking at the and to hear more about your options with this feature.
Alternatively, you might need to change the ID for a particular room in your physical building. It might be a large meeting room that is now split in two smaller rooms and one of them keeps that original ID. The external ID should then reflect your naming scheme, and not concern itself with the internal random identifier our database handed out to any of your rooms, as they can potentially be two new ones.
Historically, we referred to this as room ID, so if you see a property on an object when working with the SDK and see a roomId, it should be consistent with the external ID property.
A method on MapsIndoors is available to retrieve locations by their external ID. This method generally assumes that your first party system will do querying of different data, and only need to use the externalId to find the equivalent MapsIndoors location.
The function on all 3 platforms functions similarly, returning an Array or List of Locations that match the supplied External ID strings.
The below example shows how to retrieve some MapsIndoors locations based on your own IDs from your system, e.g. 'extId1', 'extId2' and updates the corresponding MapsIndoors locations with display rules.
You may wish to have some kind of recommendation system, so returning a list of your objects from your system might get a benefit to adding geospatial information.
Find all the nearby rooms that require cleaning
Find the closest nearby available meeting room
Get me all of the service orders tickets on this floor
Let's dig into how to implement this
Load the MapsIndoors SDK
Use the relevant script tag for the MapsIndoors SDK. There is no API directly accessible as a developer, so the script tag must be run and therefore you need to have a javascript environment.
As of the date of writing this Dec, 2023 the most recent version is
However, you can find the latest version of our SDK .
Use the getLocationsByExternalId method with whatever list of IDs are returned from your own search handled outside of MapsIndoors.
For a more full implementation to demonstrate this


Goal: This guide will show you how to add a search input field to your map application, allowing users to find locations within your venue. Search results will be displayed in a list and highlighted on the map.
SDK Concepts Introduced:
Using mapsindoors.services.LocationsService.getLocations() to fetch locations based on a query.
Interacting with the map based on search results:
Highlighting multiple locations: mapsIndoorsInstance.highlight().
Setting the floor: mapsIndoorsInstance.setFloor().
Selecting a specific location: mapsIndoorsInstance.selectLocation().
Clearing previous highlights and selections: mapsIndoorsInstance.highlight() (with no arguments) and mapsIndoorsInstance.deselectLocation().
Getting current venue information: mapsIndoorsInstance.getVenue().
Completion of .
Your MapsIndoors API Key and Mapbox Access Token should be correctly set up as per Step 1. We will continue using the demo API key 02c329e6777d431a88480a09 and venue ID dfea941bb3694e728df92d3d for this example.
This guide details the modifications to HTML, CSS, and JavaScript needed to add a search input field, display search results, and dynamically interact with the map based on user searches.
Open your index.html file. The primary structural change to your HTML is the introduction of a dedicated search panel. This panel will house the input field where users type their search queries and an unordered list where the corresponding search results will appear.
Explanation of index.html updates:
A new div element with the class panel is added. This div serves as the main container for all search-related UI elements.
Inside the panel, another div with the id search-ui and the class flex-column is introduced to arrange the search input and results list vertically.
Modify your style.css file to incorporate styles for the newly added search panel and its contents. These styles are crucial for ensuring the search interface is user-friendly, visually appealing, and correctly positioned over the map without obstructing it entirely.
Explanation of style.css updates:
.panel: Styles the search panel to float over the map in the top-left corner, with a white background, padding, rounded corners, and a shadow for a modern look. max-height and overflow-y: auto ensure it's scrollable if results are numerous.
.flex-column: A utility class using Flexbox to stack child elements (search input, results list) vertically with a small gap.
The script.js file sees the most significant changes as it houses the logic for the search functionality.
Explanation of script.js updates:
DOM Element References: searchInputElement and searchResultsElement get references to the HTML input field and the unordered list for displaying results, respectively.
Initial State: The searchResultsElement is hidden by default using the .hidden CSS class.
Event Listener
After implementing these changes:
A search panel will be visible in the top-left corner of your map.
Typing 3 or more characters into the search input will trigger a search.
Matching locations will appear as a clickable list below the search input.
All matching locations will be highlighted on the map.
Search not working / No results:
Check the browser's developer console (F12) for errors.
Ensure your MapsIndoors API Key (02c329e6777d431a88480a09 for demo) is correct and the MapsIndoors SDK is loaded.
You've now successfully added a powerful search feature to your indoor map! Users can easily find their way to specific points of interest.
Next, learn how to display detailed information about a selected location:
Step 3:
Goal: This guide will show you how to add a search input field to your map application, allowing users to find locations within your venue. Search results will be displayed in a list and highlighted on the map.
SDK Concepts Introduced:
Using mapsindoors.services.LocationsService.getLocations() to fetch locations based on a query.
Interacting with the map based on search results:
Highlighting multiple locations: mapsIndoorsInstance.highlight().
Setting the floor: mapsIndoorsInstance.setFloor().
Selecting a specific location: mapsIndoorsInstance.selectLocation().
Clearing previous highlights and selections: mapsIndoorsInstance.highlight() (with no arguments) and mapsIndoorsInstance.deselectLocation().
Getting current venue information: mapsIndoorsInstance.getVenue().
Completion of .
Your MapsIndoors API Key and Google Maps JavaScript API Key should be correctly set up as per Step 1. We will continue using the demo API key 02c329e6777d431a88480a09 and venue ID dfea941bb3694e728df92d3d for this example.
This guide details the modifications to HTML, CSS, and JavaScript needed to add a search input field, display search results, and dynamically interact with the map based on user searches.
Open your index.html file. The primary structural change to your HTML is the introduction of a dedicated search panel. This panel will house the input field where users type their search queries and an unordered list where the corresponding search results will appear.
Explanation of index.html updates:
A new div element with the class panel is added. This div serves as the main container for all search-related UI elements.
Inside the panel, another div with the id search-ui and the class flex-column is introduced to arrange the search input and results list vertically.
Modify your style.css file to incorporate styles for the newly added search panel and its contents. These styles are crucial for ensuring the search interface is user-friendly, visually appealing, and correctly positioned over the map without obstructing it entirely.
Explanation of style.css updates:
.panel: Styles the search panel to float over the map in the top-left corner, with a white background, padding, rounded corners, and a shadow for a modern look. max-height and overflow-y: auto ensure it's scrollable if results are numerous.
.flex-column: A utility class using Flexbox to stack child elements (search input, results list) vertically with a small gap.
The script.js file sees the most significant changes as it houses the logic for the search functionality.
Explanation of script.js updates:
DOM Element References: searchInputElement and searchResultsElement get references to the HTML input field and the unordered list for displaying results, respectively.
Initial State: The searchResultsElement is hidden by default using the .hidden CSS class.
Event Listener
After implementing these changes:
A search panel will be visible in the top-left corner of your map.
Typing 3 or more characters into the search input will trigger a search.
Matching locations will appear as a clickable list below the search input.
All matching locations will be highlighted on the map.
Search not working / No results:
Check the browser's developer console (F12) for errors.
Ensure your MapsIndoors API Key (02c329e6777d431a88480a09 for demo) is correct and the MapsIndoors SDK is loaded.
You've now successfully added a powerful search feature to your indoor map! Users can easily find their way to specific points of interest.
Next, learn how to display detailed information about a selected location:
Step 3:
Goal: This guide demonstrates how to display detailed information about a location when a user selects it from the search results or clicks a POI on the map. The details will include the location's name and description, and the map will navigate to and highlight the selected location.
SDK Classes and Methods Introduced:
Retrieving and displaying specific location properties (e.g., location.properties.name, location.properties.description) within a dedicated details panel.
Applying mapsIndoorsInstance.deselectLocation()
Goal: This guide demonstrates how to display detailed information about a location when a user selects it from the search results or clicks a POI on the map. The details will include the location's name and description, and the map will navigate to and highlight the selected location.
SDK Classes and Methods Introduced:
Retrieving and displaying specific location properties (e.g., location.properties.name, location.properties.description) within a dedicated details panel.
Applying mapsIndoorsInstance.deselectLocation()
// Define arrays of external IDs for available and unavailable resources
const availableExternalIds = ['extId1', 'extId2']; // Replace with your actual IDs
const unavailableExternalIds = ['extId3', 'extId4']; // Replace with your actual IDs
// Fetch locations based on external IDs
async function fetchLocationsByExternalIds(externalIds) {
const promises = externalIds.map(id => mapsindoors.services.LocationsService.getLocationsByExternalId(id));
return await Promise.all(promises);
}
// Fetch locations for available and unavailable resources
const availableLocations = await fetchLocationsByExternalIds(availableExternalIds);
const unavailableLocations = await fetchLocationsByExternalIds(unavailableExternalIds);
// Segment locations into Meeting Rooms and Workstations
const availableMeetingRooms = availableLocations.filter(location => location.properties.type === 'MeetingRoom');
const availableWorkstations = availableLocations.filter(location => location.properties.type === 'Workstation');
const unavailableMeetingRooms = unavailableLocations.filter(location => location.properties.type === 'MeetingRoom');
const unavailableWorkstations = unavailableLocations.filter(location => location.properties.type === 'Workstation');
// Extract MapsIndoors location Ids
const availableMeetingRoomIds = availableMeetingRooms.map(location => location.id);
const unavailableMeetingRoomIds = unavailableMeetingRooms.map(location => location.id);
const availableWorkstationIds = availableWorkstations.map(location => location.id);
const unavailableWorkstationIds = unavailableWorkstations.map(location => location.id);
// Set display rules for Meeting Rooms
mapsIndoorsInstance.setDisplayRule(availableMeetingRoomIds, {
polygonVisible: true,
polygonFillColor: "#90ee90",
polygonFillOpacity: 1,
polygonZoomFrom: 16,
polygonZoomTo: 22,
visible: true,
});
mapsIndoorsInstance.setDisplayRule(unavailableMeetingRoomIds, {
polygonVisible: true,
polygonFillColor: "#ff4d4d",
polygonFillOpacity: 1,
polygonZoomFrom: 16,
polygonZoomTo: 22,
visible: true,
});
// Set display rules for Workstations
mapsIndoorsInstance.setDisplayRule(availableWorkstationIds, {
model3DVisible: true,
model3DModel: 'your_3D_model_URL_for_available_workstations',
model3DZoomFrom: 16,
model3DZoomTo: 22,
visible: true,
});
mapsIndoorsInstance.setDisplayRule(unavailableWorkstationIds, {
model3DVisible: true,
model3DModel: 'your_3D_model_URL_for_unavailable_workstations',
model3DZoomFrom: 16,
model3DZoomTo: 22,
visible: true,
});
<script src="https://app.mapsindoors.com/mapsindoors/js/sdk/4.26.3/mapsindoors-4.26.3.js.gz"></script>// Define arrays of external IDs for available and unavailable resources
const availableExternalIds = ['extId1', 'extId2']; // Replace with your actual IDs
// Fetch locations based on external IDs
async function fetchLocationsByExternalIds(externalIds) {
const promises = externalIds.map(id => mapsindoors.services.LocationsService.getLocationsByExternalId(id));
return await Promise.all(promises);
}
// Fetch locations for available and unavailable resources
const availableLocations = await fetchLocationsByExternalIds(availableExternalIds);<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Meeting Room Finder</title>
<script src="https://app.mapsindoors.com/mapsindoors/js/sdk/4.24.8/mapsindoors-4.24.8.js.gz?apikey=3ddemo"></script>
<style>
body {
font-family: Arial, sans-serif;
}
table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
th, td {
border: 1px solid #dddddd;
text-align: left;
padding: 8px;
word-wrap: break-word;
}
th {
background-color: #f2f2f2;
cursor: pointer;
}
/* Specify the width of each column */
th:nth-child(1), td:nth-child(1) { width: 20%; }
th:nth-child(2), td:nth-child(2) { width: 15%; }
th:nth-child(3), td:nth-child(3) { width: 10%; }
th:nth-child(4), td:nth-child(4) { width: 20%; }
th:nth-child(5), td:nth-child(5) { width: 15%; }
th:nth-child(6), td:nth-child(6) { width: 20%; }
.table-container {
display: none;
}
/* Style for buttons */
#buttons-container button {
border: none;
border-radius: 12px;
padding: 10px 20px;
margin: 5px;
cursor: pointer;
background-color: #ffffff;
color: #000000;
transition: all 0.3s ease;
}
/* Hover effect */
#buttons-container button:hover {
background-color: #ffedd5;
color: #0f5655;
box-shadow: 0px 0px 10px 2px #ffedd5;
}
/* Selected state */
#buttons-container button.selected {
background-color: #7d49f3;
color: white;
}
</style>
</head>
<body>
<section>
<h2>Make better recommendations by incorporating MapsIndoors data</h2>
<p id="dynamic-text">All meeting rooms on the same floor as the employee. Most of this would be in a resource calendar system.</p>
</section>
<div id="buttons-container">
<button onclick="toggleTable('all-rooms')" class="selected">All Rooms</button>
<button onclick="toggleTable('available-rooms')">Available Meeting Rooms</button>
<button onclick="toggleTable('top-rooms')">Top 10 Nearby Meeting Rooms</button>
<button onclick="toggleTable('top-3-rooms')">Top 3 Meeting Rooms</button>
</div>
<div id="all-rooms" class="table-container"></div>
<div id="available-rooms" class="table-container"></div>
<div id="top-rooms" class="table-container"></div>
<div id="top-3-rooms" class="table-container"></div>
<script>
function toggleTable(id) {
const tables = ['all-rooms', 'available-rooms', 'top-rooms', 'top-3-rooms'];
const textElement = document.getElementById('dynamic-text');
let description = '';
switch(id) {
case 'all-rooms':
description = "All meeting rooms on the same floor as the employee. Most of this would be in a resource calendar system.";
break;
case 'available-rooms':
description = "Dynamic meeting room availability on the employee's floor based on your own data or a calendar system like Outlook or Google Calendar.";
break;
case 'top-rooms':
description = "Starting to optimize the recommended rooms based on their position in the office based on walking distance.";
break;
case 'top-3-rooms':
description = "Optimized greatly the recommendation on where they should go for a meeting based on walking distance.";
break;
}
textElement.innerText = description;
tables.forEach((tableId) => {
const el = document.getElementById(tableId);
const btn = document.querySelector(`button[onclick="toggleTable('${tableId}')"]`);
if (tableId === id) {
el.style.display = 'block';
btn.classList.add('selected');
} else {
el.style.display = 'none';
btn.classList.remove('selected');
}
});
}
function createDirectionsLink(originId, destinationId) {
return `<a href="https://map.mapsindoors.com/?apiKey=3ddemo&directionsFrom=${originId}&directionsTo=${destinationId}&pitch=10" target="_blank">Take me there</a>`;
}
// Function to create table with distance and directions
function createTableWithDistance(rows, originId) {
const headers = ['Name', 'Distance (m)', 'Floor', 'External ID', 'Type', 'Directions'];
const headerHTML = headers.map(header => `<th>${header}</th>`).join('');
const rowHTML = rows.map(row => {
const { name, distance, floor, externalId, type, mapsindoorsId } = row;
const directionsLink = createDirectionsLink(originId, mapsindoorsId);
return `<tr><td>${name || ''}</td><td>${distance || ''}</td><td>${floor/10 || ''}</td><td>${externalId || ''}</td><td>${type || ''}</td><td>${directionsLink}</td></tr>`;
}).join('');
return `
<table>
<thead><tr>${headerHTML}</tr></thead>
<tbody>${rowHTML}</tbody>
</table>
`;
}
// Function to create table without distance and directions
function createTableWithoutDistance(rows) {
const headers = ['Name', 'Floor', 'External ID', 'Type'];
const headerHTML = headers.map(header => `<th>${header}</th>`).join('');
const rowHTML = rows.map(row => {
const { name, floor, externalId, type } = row.properties; // Assuming row.properties contains the required data
return `<tr><td>${name || ''}</td><td>${floor/10 || ''}</td><td>${externalId || ''}</td><td>${type || ''}</td></tr>`;
}).join('');
return `
<table>
<thead><tr>${headerHTML}</tr></thead>
<tbody>${rowHTML}</tbody>
</table>
`;
}
(async () => {
const allRoomsElement = document.getElementById('all-rooms');
const availableRoomsElement = document.getElementById('available-rooms');
const topRoomsElement = document.getElementById('top-rooms');
const top3RoomsElement = document.getElementById('top-3-rooms');
const allRooms = await mapsindoors.services.LocationsService.getLocations({
floor: 50,
types: ['Meetingroom', 'b29e5d4d-f302-4ec2-b8f6-ec251ab879ff', 'Meetingroom small', '615d1438-f6f0-434c-99c0-a0de0c781f42'],
take: 50
});
const availableRooms = ['05-D73', '05-D42', '05-D10', '05-D47', '05-D30', '05-D14', '05-D49', '05-D7', '05-D69'];
const filteredMeetingRooms = allRooms.filter(room => availableRooms.includes(room.properties.externalId));
const originsArray = await mapsindoors.services.LocationsService.getLocationsByExternalId('05-Desk393');
const origin = originsArray[0];
const matrix = await mapsindoors.services.DistanceMatrixService.getDistanceMatrix({
graphId: 'WEWORK_Graph',
origins: [`${origin.properties.anchor.coordinates[1]},${origin.properties.anchor.coordinates[0]},${origin.properties.floor}`],
destinations: filteredMeetingRooms.map(room => `${room.properties.anchor.coordinates[1]},${room.properties.anchor.coordinates[0]},${room.properties.floor}`)
});
const sortedRooms = filteredMeetingRooms.map((room, index) => ({
name: room.properties.name,
distance: matrix.rows[0].elements[index].distance.value,
floor: room.properties.floor,
externalId: room.properties.externalId,
type: room.properties.type,
mapsindoorsId: room.id
})).sort((a, b) => a.distance - b.distance);
allRoomsElement.innerHTML = `<h3>All Meeting Rooms</h3>` + createTableWithoutDistance(allRooms);
availableRoomsElement.innerHTML = `<h3>Available Meeting Rooms</h3>` + createTableWithoutDistance(filteredMeetingRooms);
topRoomsElement.innerHTML = `<h3>Top 10 Nearby Meeting Rooms</h3>` + createTableWithDistance(sortedRooms.slice(0, 10), origin.id);
top3RoomsElement.innerHTML = `<h3>Top 3 Meeting Rooms</h3>` + createTableWithDistance(sortedRooms.slice(0, 3), origin.id);
document.getElementById('all-rooms').style.display = 'block';
})();
</script>
</body>
</html>An <input> element with id="search-input" is the text field for user search queries.
An <ul> (unordered list) element with id="search-results" will be dynamically populated with locations matching the user's query.
.hidden: A utility class to hide elements (display: none;), used initially for the search results list.#search-input: Styles the search input field with padding, border, and font size for readability.
#search-results: Styles the unordered list for search results, removing default list styling.
#search-results li: Styles individual list items with padding, a bottom border for separation, and a pointer cursor to indicate clickability.
#search-results li:last-child: Removes the bottom border from the last list item.
#search-results li:hover: Provides a hover effect for list items for better user feedback.
inputsearchInputElementonSearchonSearch() Function: This is the core of the search logic:
It retrieves the current query from the input field and gets the currentVenue using mapsIndoorsInstance.getVenue(). For more details on venue information, see the getVenue() reference.
It clears any existing highlights from the map using mapsIndoorsInstance.highlight() (called without arguments). See its for more on clearing highlights.
It deselects any currently selected location using mapsIndoorsInstance.deselectLocation() (called without arguments). Refer to its for deselection behavior.
If the query length is less than 3 characters, it hides the searchResultsElement and exits to prevent overly broad or empty searches.
It prepares searchParameters with the q (query) and scopes the search to the currentVenue.name. For a comprehensive list of search options, check out the
It calls mapsindoors.services.LocationsService.getLocations(searchParameters) to fetch locations. This asynchronous method returns a Promise.
.then(locations => { ... }): This block handles the successful response from the LocationsService. locations is an array of objects, where each object conforms to the .
It clears previous search results by setting searchResultsElement.innerHTML = null.
If no locations are found, it displays a "No results found" message in the list.
.catch(error => { ... }): Handles potential errors during the search request, logging them to the console and displaying an error message in the list.
The handleLocationClick function is now used for both map clicks and search result clicks, ensuring consistent behavior and code reuse.
When a user clicks a search result or a POI on the map, the map will center on that location, switch to the correct floor, and select the location.
This pattern will be reused and expanded in later steps.
Clicking a location in the list will:
Pan and zoom the map to that location.
Switch to the correct floor if necessary.
Select and distinctively highlight that specific location on the map.
YOUR_MAPBOX_ACCESS_TOKEN is correct.Confirm the venue ID (dfea941bb3694e728df92d3d for demo) is valid and the venue has searchable locations.
Make sure the onSearch function is being called (e.g., add a console.log at the beginning of onSearch).
Results list doesn't appear or looks wrong:
Check CSS for the .panel, #search-results, and li elements. Ensure the .hidden class is being correctly added/removed.
Map interactions (goTo, highlight, selectLocation) not working:
Verify mapsIndoorsInstance is correctly initialized.
Ensure the location objects passed to these methods are valid objects conforming to the Location interface.
Check for console errors when clicking a search result.
An <input> element with id="search-input" is the text field for user search queries.
An <ul> (unordered list) element with id="search-results" will be dynamically populated with locations matching the user's query.
.hidden: A utility class to hide elements (display: none;), used initially for the search results list.#search-input: Styles the search input field with padding, border, and font size for readability.
#search-results: Styles the unordered list for search results, removing default list styling.
#search-results li: Styles individual list items with padding, a bottom border for separation, and a pointer cursor to indicate clickability.
#search-results li:last-child: Removes the bottom border from the last list item.
#search-results li:hover: Provides a hover effect for list items for better user feedback.
inputsearchInputElementonSearchonSearch() Function: This is the core of the search logic:
It retrieves the current query from the input field and gets the currentVenue using mapsIndoorsInstance.getVenue(). For more details on venue information, see the getVenue() reference.
It clears any existing highlights from the map using mapsIndoorsInstance.highlight() (called without arguments). See its for more on clearing highlights.
It deselects any currently selected location using mapsIndoorsInstance.deselectLocation(). Refer to its for deselection behavior.
If the query length is less than 3 characters, it hides the searchResultsElement and exits to prevent overly broad or empty searches.
It prepares searchParameters with the q (query) and scopes the search to the currentVenue.name. For a comprehensive list of search options, check out the
It calls mapsindoors.services.LocationsService.getLocations(searchParameters) to fetch locations. This asynchronous method returns a Promise.
.then(locations => { ... }): This block handles the successful response from the LocationsService. locations is an array of objects, where each object conforms to the .
It clears previous search results by setting searchResultsElement.innerHTML = null.
If no locations are found, it displays a "No results found" message in the list.
.catch(error => { ... }): Handles potential errors during the search request, logging them to the console and displaying an error message in the list.
The handleLocationClick function is now used for both map clicks and search result clicks, ensuring consistent behavior and code reuse.
When a user clicks a search result or a POI on the map, the map will center on that location, switch to the correct floor, and select the location.
This pattern will be reused and expanded in later steps.
Clicking a location in the list will:
Pan and zoom the map to that location.
Switch to the correct floor if necessary.
Select and distinctively highlight that specific location on the map.
YOUR_GOOGLE_MAPS_API_KEY is correct.Confirm the venue ID (dfea941bb3694e728df92d3d for demo) is valid and the venue has searchable locations.
Make sure the onSearch function is being called (e.g., add a console.log at the beginning of onSearch).
Results list doesn't appear or looks wrong:
Check CSS for the .panel, #search-results, and li elements. Ensure the .hidden class is being correctly added/removed.
Map interactions (goTo, highlight, selectLocation) not working:
Verify mapsIndoorsInstance is correctly initialized.
Ensure the location objects passed to these methods are valid objects conforming to the Location interface.
Check for console errors when clicking a search result.
Implementation Patterns:
Expanding the unified click handler to show location details
Implementing UI state management between search and details views
Creating a responsive details panel UI
Handling transitions between different UI states
Completion of Step 2: Create a Search Experience.
Your MapsIndoors API Key and Google Maps API Key should be correctly set up. We will continue using the demo API key 02c329e6777d431a88480a09 and venue ID dfea941bb3694e728df92d3d for this example.
This step involves modifications to index.html to add elements for displaying details, style.css to style these new elements, and script.js to handle the logic of fetching and displaying location details and interacting with the map.
In Step 1, we introduced a reusable click handler (handleLocationClick) for POIs on the map. In Step 2, we reused this handler for search result clicks. In this step, we expand the handler to show a details panel with more information about the selected location, and manage the UI state.
Open your index.html file. We will add a new div within the existing .panel to hold the location details. This div will be hidden by default and shown when a location is selected.
Explanation of index.html updates:
The existing search input and results list are wrapped in a div with id="search-ui" and class flex-column. This helps in managing the visibility of the entire search section.
A new div with id="details-ui" is added within the .panel. This container will hold the location's name, description, and a close button.
It has the class hidden to be invisible by default.
It also has flex-column for layout consistency.
Inside #details-ui:
<h3 id="details-name"></h3>: For displaying the location name.
<p id="details-description"></p>: For displaying the location description.
Modify your style.css file to add styles for the new location details UI elements.
Explanation of style.css updates:
#search-ui, #details-ui: Basic styling to ensure they take full width. The flex-column class will handle their internal layout.
#details-name: Styles for the location name heading (font size, margin, border).
#details-description: Styles for the description text (font size, color, white-space: pre-wrap to respect newlines from the data, max-height and overflow-y for scrollability).
.details-button: General styling for buttons in the details view (padding, border-radius, full width).
#details-close: Specific styling for the "Close" button (background color, text color, hover effect).
The script.js file will be updated to handle showing/hiding the details panel, populating it with location data, and interacting with the map.
Explanation of script.js updates:
Unified Click Handler Implementation:
Building upon our approach in previous steps, we've implemented a comprehensive click handler system.
The handleLocationClick function manages map interactions (selecting a location, zooming, and changing floors).
This function then calls the showDetails function to update the UI presentation.
This separation of concerns makes the code more maintainable and easier to understand.
UI State Management Functions:
showSearchUI(): Shows the search interface and hides the details panel.
showDetailsUI(): Shows the details interface and hides the search panel.
Map Interaction Methods:
mapsIndoorsInstance.goTo(location): Pans and zooms the map to the selected location.
mapsIndoorsInstance.setFloor(location.properties.floor): Switches to the floor where the location exists.
Location Properties Access:
We access location.properties.name and location.properties.description to display detailed information.
The fallback text "No description available" is shown when description is missing.
Search Result Click Handler:
Search result clicks now trigger the same handler as map POI clicks, providing a consistent experience.
This unified approach ensures the same behavior whether a user interacts with the map or search results.
The separation between handleLocationClick (for map operations) and
Initial Setup:
showSearchUI() is called at the end to ensure the application starts with the search interface visible.
After implementing these changes:
When you search for locations and click on a result in the list:
The search input and results list will be hidden.
A details panel will appear showing the selected location's name and description.
The map will pan and zoom to the selected location.
The selected location will be highlighted on the map.
The map will switch to the correct floor of the selected location.
Clicking the "Close" button in the details panel will hide the details and show the search input and results list again.
When you click a POI on the map:
The details panel will appear showing the location's name and description.
The map will pan and zoom to the selected location.
The selected location will be highlighted on the map.
Details panel doesn't show or shows incorrect data:
Check browser console for errors.
Verify IDs in index.html match those used in script.js for getElementById.
Ensure location.properties.name and location.properties.description exist for the selected location or that default text is handled.
Confirm CSS for .hidden, #search-ui, and #details-ui is correctly applied and toggled.
Map doesn't navigate or select location:
Ensure mapsIndoorsInstance is correctly initialized.
Verify the location object passed to handleLocationClick is a valid object conforming to the Location interface.
"Close" button doesn't work:
Ensure the event listener is correctly attached to detailsCloseButton and that showSearchUI is called.
You've now enhanced your application to display detailed information about locations. Users can not only find locations but also learn more about them.
Next, you'll learn how to get and display directions between locations:
Step 4: Getting Directions
Implementation Patterns:
Expanding the unified click handler to show location details
Implementing UI state management between search and details views
Creating a responsive details panel UI
Handling transitions between different UI states
Completion of Step 2: Create a Search Experience.
Your MapsIndoors API Key and Mapbox Access Token should be correctly set up. We will continue using the demo API key 02c329e6777d431a88480a09 and venue ID dfea941bb3694e728df92d3d for this example.
This step involves modifications to index.html to add elements for displaying details, style.css to style these new elements, and script.js to handle the logic of fetching and displaying location details and interacting with the map.
In Step 1, we introduced a reusable click handler (handleLocationClick) for POIs on the map. In Step 2, we reused this handler for search result clicks. In this step, we expand the handler to show a details panel with more information about the selected location, and manage the UI state.
Open your index.html file. We will add a new div within the existing .panel to hold the location details. This div will be hidden by default and shown when a location is selected.
Explanation of index.html updates:
The existing search input and results list are wrapped in a div with id="search-ui" and class flex-column. This helps in managing the visibility of the entire search section.
A new div with id="details-ui" is added within the .panel. This container will hold the location's name, description, and a close button.
It has the class hidden to be invisible by default.
It also has flex-column for layout consistency.
Inside #details-ui:
<h3 id="details-name"></h3>: For displaying the location name.
<p id="details-description"></p>: For displaying the location description.
Modify your style.css file to add styles for the new location details UI elements.
Explanation of style.css updates:
#search-ui, #details-ui: Basic styling to ensure they take full width. The flex-column class will handle their internal layout.
#details-name: Styles for the location name heading (font size, margin, border).
#details-description: Styles for the description text (font size, color, white-space: pre-wrap to respect newlines from the data, max-height and overflow-y for scrollability).
.details-button: General styling for buttons in the details view (padding, border-radius, full width).
#details-close: Specific styling for the "Close" button (background color, text color, hover effect).
The script.js file will be updated to handle showing/hiding the details panel, populating it with location data, and interacting with the map.
Explanation of script.js updates:
Unified Click Handler Implementation:
Building upon our approach in previous steps, we've implemented a comprehensive click handler system.
The handleLocationClick function manages map interactions (selecting a location, zooming, and changing floors).
This function then calls the showDetails function to update the UI presentation.
This separation of concerns makes the code more maintainable and easier to understand.
UI State Management Functions:
showSearchUI(): Shows the search interface and hides the details panel.
showDetailsUI(): Shows the details interface and hides the search panel.
Map Interaction Methods:
mapsIndoorsInstance.goTo(location): Pans and zooms the map to the selected location.
mapsIndoorsInstance.setFloor(location.properties.floor): Switches to the floor where the location exists.
Location Properties Access:
We access location.properties.name and location.properties.description to display detailed information.
The fallback text "No description available" is shown when description is missing.
Search Result Click Handler:
Search result clicks now trigger the same handler as map POI clicks, providing a consistent experience.
This unified approach ensures the same behavior whether a user interacts with the map or search results.
The separation between handleLocationClick (for map operations) and
Initial Setup:
showSearchUI() is called at the end to ensure the application starts with the search interface visible.
After implementing these changes:
When you search for locations and click on a result in the list:
The search input and results list will be hidden.
A details panel will appear showing the selected location's name and description.
The map will pan and zoom to the selected location.
The selected location will be highlighted on the map.
The map will switch to the correct floor of the selected location.
Clicking the "Close" button in the details panel will hide the details and show the search input and results list again.
When you click a POI on the map:
The details panel will appear showing the location's name and description.
The map will pan and zoom to the selected location.
The selected location will be highlighted on the map.
Details panel doesn't show or shows incorrect data:
Check browser console for errors.
Verify IDs in index.html match those used in script.js for getElementById.
Ensure location.properties.name and location.properties.description exist for the selected location or that default text is handled.
Confirm CSS for .hidden, #search-ui, and #details-ui is correctly applied and toggled.
Map doesn't navigate or select location:
Ensure mapsIndoorsInstance is correctly initialized.
Verify the location object passed to handleLocationClick is a valid object conforming to the Location interface.
"Close" button doesn't work:
Ensure the event listener is correctly attached to detailsCloseButton and that showSearchUI is called.
You've now enhanced your application to display detailed information about locations using a unified click handler. Next, you'll learn how to get and display directions between locations:
Step 4: Getting Directions
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MapsIndoors</title>
<link rel="stylesheet" href="style.css">
<link href='https://api.mapbox.com/mapbox-gl-js/v3.10.0/mapbox-gl.css' rel='stylesheet' />
<script src="https://app.mapsindoors.com/mapsindoors/js/sdk/4.41.0/mapsindoors-4.41.0.js.gz"
integrity="sha384-3lk3cwVPj5MpUyo5T605mB0PMHLLisIhNrSREQsQHjD9EXkHBjz9ETgopmTbfMDc"
crossorigin="anonymous"></script>
<script src='https://api.mapbox.com/mapbox-gl-js/v3.10.0/mapbox-gl.js'></script>
</head>
<body>
<div id="map"></div>
<!-- New search panel for user interaction -->
<div class="panel">
<div id="search-ui" class="flex-column">
<!-- Input field for users to type search queries -->
<input type="text" id="search-input" placeholder="Search for a location...">
<!-- Unordered list to display search results dynamically -->
<ul id="search-results"></ul>
</div>
</div>
<script src="script.js"></script>
</body>
</html>/* style.css */
/* Use flexbox for the main layout */
html, body {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden; /* Prevent scrollbars if map is full size */
display: flex;
flex-direction: column; /* Stack children vertically if needed later */
}
/* Style for the map container */
#map {
/* Make map fill available space */
flex-grow: 1;
width: 100%; /* Make map fill width */
margin: 0;
padding: 0;
}
/* Style for the information panel container */
.panel {
position: absolute; /* Positions the panel relative to the nearest positioned ancestor (or body if none). This allows it to float over the map. */
top: 10px; /* Adds a 10px margin from the top edge of its containing element. */
left: 10px; /* Adds a 10px margin from the left edge of its containing element, placing it in the top-left corner. */
z-index: 10; /* Ensures the panel appears on top of other elements, like the map itself, which might have lower z-index values. */
background-color: white; /* Sets a solid white background for the panel, making text readable. */
padding: 10px; /* Adds 10px of space inside the panel, around its content. */
border-radius: 5px; /* Rounds the corners of the panel for a softer look. */
box-shadow: 0 2px 5px rgba(0,0,0,0.2); /* Adds a subtle shadow effect, giving the panel a sense of depth. */
max-height: 80%; /* Limits max-height to prevent overflow with details */
overflow-y: auto; /* Adds a vertical scrollbar if the content inside the panel exceeds its max-height. */
border: 1px solid #ccc; /* Adds a light gray border around the panel for better visual separation. */
width: 300px; /* Sets a fixed width for the panel. */
}
/* Class to apply flex display and column direction */
.flex-column {
display: flex; /* Enables flexbox layout for this element. */
flex-direction: column; /* Arranges child elements in a vertical column. */
gap: 10px; /* Adds a 10px gap between child elements within the flex container. */
}
/* Class to hide elements */
.hidden {
display: none; /* Makes elements with this class invisible and removes them from the layout. */
}
/* Style for the search input field */
#search-input {
padding: 8px; /* Adds 8px of internal padding to the input field for better text spacing. */
border: 1px solid #ccc; /* Adds a light gray border around the input field. */
border-radius: 4px; /* Slightly rounds the corners of the input field. */
font-size: 1rem; /* Sets the font size within the input field to a standard readable size. */
}
/* Style for the search results list */
#search-results {
list-style: none; /* Removes the default bullet points from the unordered list. */
padding: 0; /* Removes default padding from the list. */
margin: 0; /* Removes default margins from the list. */
}
/* Style for individual search result items */
#search-results li {
padding: 8px 0; /* Adds vertical padding to each list item for spacing. */
cursor: pointer; /* Changes the mouse cursor to a pointer on hover, indicating the item is clickable. */
border-bottom: 1px solid #eee; /* Adds a light gray line below each list item, acting as a separator. */
}
/* Style for the last search result item (no bottom border) */
#search-results li:last-child {
border-bottom: none; /* Removes the bottom border from the last item in the list for a cleaner look. */
}
/* Hover effect for search result items */
#search-results li:hover {
background-color: #f0f0f0; /* Changes the background color of a list item when hovered, providing visual feedback. */
}// script.js
// Define options for the MapsIndoors Mapbox view
const mapViewOptions = {
accessToken: 'YOUR_MAPBOX_ACCESS_TOKEN', // Replace with your Mapbox token
element: document.getElementById('map'),
center: { lng: -97.74204591828197, lat: 30.36022358949809 }, // Example: MapsPeople Austin Office
zoom: 17,
maxZoom: 22,
mapsIndoorsTransitionLevel: 16,
};
// Set the MapsIndoors API key
mapsindoors.MapsIndoors.setMapsIndoorsApiKey('YOUR_MAPSINDOORS_API_KEY'); // Replace with your MapsIndoors API key
// Create a new instance of the MapsIndoors Mapbox view
const mapViewInstance = new mapsindoors.mapView.MapboxV3View(mapViewOptions);
// Create a new MapsIndoors instance, passing the map view
const mapsIndoorsInstance = new mapsindoors.MapsIndoors({
mapView: mapViewInstance,
// Set the venue ID to load the map for a specific venue
venue: 'YOUR_MAPSINDOORS_VENUE_ID', // Replace with your actual venue ID
});
/** Floor Selector **/
// Create a new HTML div element to host the floor selector
const floorSelectorElement = document.createElement('div');
// Create a new FloorSelector instance, linking it to the HTML element and the main MapsIndoors instance.
new mapsindoors.FloorSelector(floorSelectorElement, mapsIndoorsInstance);
// Get the underlying Mapbox map instance
const mapboxInstance = mapViewInstance.getMap();
// Add the floor selector HTML element to the Mapbox map using Mapbox's addControl method
// We wrap the element in an object implementing the IControl interface expected by addControl
mapboxInstance.addControl({
onAdd: function () {
// This function is called when the control is added to the map.
// It should return the control's DOM element.
return floorSelectorElement;
},
onRemove: function () {
// This function is called when the control is removed from the map.
// Clean up any event listeners or resources here.
floorSelectorElement.parentNode.removeChild(floorSelectorElement);
},
}, 'top-right'); // Optional: Specify a position ('top-left', 'top-right', 'bottom-left', 'bottom-right')
/** Handle Location Clicks **/
// Function to handle clicks on MapsIndoors locations
function handleLocationClick(location) {
if (location && location.id) {
// Move the map to the selected location
mapsIndoorsInstance.goTo(location);
// Ensure that the map shows the correct floor
mapsIndoorsInstance.setFloor(location.properties.floor);
// Select the location on the map
mapsIndoorsInstance.selectLocation(location);
}
}
// Add an event listener to the MapsIndoors instance for click events on locations
mapsIndoorsInstance.on('click', handleLocationClick);
/** Search Functionality **/
// Get references to the search input and results list elements
const searchInputElement = document.getElementById('search-input');
const searchResultsElement = document.getElementById('search-results');
// Initially hide the search results list
searchResultsElement.classList.add('hidden');
// Add an event listener to the search input for 'input' events
// This calls the onSearch function every time the user types in the input field
searchInputElement.addEventListener('input', onSearch);
// Function to perform the search and update the results list and map highlighting
function onSearch() {
// Get the current value from the search input
const query = searchInputElement.value;
// Get the current venue from the MapsIndoors instance
const currentVenue = mapsIndoorsInstance.getVenue();
// Clear map highlighting
mapsIndoorsInstance.highlight();
// Deselect any selected location
mapsIndoorsInstance.deselectLocation();
// Check if the query is too short (less than 3 characters) or empty
if (query.length < 3) {
// Hide the results list if the query is too short or empty
searchResultsElement.classList.add('hidden');
return; // Stop here
}
// Define search parameters with the current input value
// Include the current venue name in the search parameters
const searchParameters = { q: query, venue: currentVenue ? currentVenue.name : undefined };
// Call the MapsIndoors LocationsService to get locations based on the search query
mapsindoors.services.LocationsService.getLocations(searchParameters).then(locations => {
// Clear previous search results
searchResultsElement.innerHTML = null;
// If no locations are found, display a "No results found" message
if (locations.length === 0) {
const noResultsItem = document.createElement('li');
noResultsItem.textContent = 'No results found';
searchResultsElement.appendChild(noResultsItem);
// Ensure the results list is visible to show the "No results found" message
searchResultsElement.classList.remove('hidden');
return; // Stop here if no results
}
// Append new search results to the list
locations.forEach(location => {
const listElement = document.createElement('li');
// Display the location name
listElement.innerHTML = location.properties.name;
// Store the location ID on the list item for easy access
listElement.dataset.locationId = location.id;
// Add a click event listener to each list item
listElement.addEventListener('click', function () {
// Call the handleLocationClick function when a location in the search results is clicked.
handleLocationClick(location);
});
searchResultsElement.appendChild(listElement);
});
// Show the results list now that it has content
searchResultsElement.classList.remove('hidden');
// Filter map to only display search results by highlighting them
mapsIndoorsInstance.highlight(locations.map(location => location.id));
})
.catch(error => {
console.error("Error fetching locations:", error);
const errorItem = document.createElement('li');
errorItem.textContent = 'Error performing search.';
searchResultsElement.appendChild(errorItem);
searchResultsElement.classList.remove('hidden');
});
}<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MapsIndoors</title>
<link rel="stylesheet" href="style.css">
<script src="https://maps.googleapis.com/maps/api/js?libraries=geometry&key=YOUR_GOOGLE_MAPS_API_KEY"></script>
<script src="https://app.mapsindoors.com/mapsindoors/js/sdk/4.41.0/mapsindoors-4.41.0.js.gz"
integrity="sha384-3lk3cwVPj5MpUyo5T605mB0PMHLLisIhNrSREQsQHjD9EXkHBjz9ETgopmTbfMDc"
crossorigin="anonymous"></script>
</head>
<body>
<div id="map"></div>
<!-- New search panel for user interaction -->
<div class="panel">
<div id="search-ui" class="flex-column">
<!-- Input field for users to type search queries -->
<input type="text" id="search-input" placeholder="Search for a location...">
<!-- Unordered list to display search results dynamically -->
<ul id="search-results"></ul>
</div>
</div>
<script src="script.js"></script>
</body>
</html>/* style.css */
/* Use flexbox for the main layout */
html, body {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden; /* Prevent scrollbars if map is full size */
display: flex;
flex-direction: column; /* Stack children vertically if needed later */
}
/* Style for the map container */
#map {
/* Make map fill available space */
flex-grow: 1;
width: 100%; /* Make map fill width */
margin: 0;
padding: 0;
}
/* Style for the information panel container */
.panel {
position: absolute; /* Positions the panel relative to the nearest positioned ancestor (or body if none). This allows it to float over the map. */
top: 10px; /* Adds a 10px margin from the top edge of its containing element. */
left: 10px; /* Adds a 10px margin from the left edge of its containing element, placing it in the top-left corner. */
z-index: 10; /* Ensures the panel appears on top of other elements, like the map itself, which might have lower z-index values. */
background-color: white; /* Sets a solid white background for the panel, making text readable. */
padding: 10px; /* Adds 10px of space inside the panel, around its content. */
border-radius: 5px; /* Rounds the corners of the panel for a softer look. */
box-shadow: 0 2px 5px rgba(0,0,0,0.2); /* Adds a subtle shadow effect, giving the panel a sense of depth. */
max-height: 80%; /* Limits max-height to prevent overflow with details */
overflow-y: auto; /* Adds a vertical scrollbar if the content inside the panel exceeds its max-height. */
border: 1px solid #ccc; /* Adds a light gray border around the panel for better visual separation. */
width: 300px; /* Sets a fixed width for the panel. */
}
/* Class to apply flex display and column direction */
.flex-column {
display: flex; /* Enables flexbox layout for this element. */
flex-direction: column; /* Arranges child elements in a vertical column. */
gap: 10px; /* Adds a 10px gap between child elements within the flex container. */
}
/* Class to hide elements */
.hidden {
display: none; /* Makes elements with this class invisible and removes them from the layout. */
}
/* Style for the search input field */
#search-input {
padding: 8px; /* Adds 8px of internal padding to the input field for better text spacing. */
border: 1px solid #ccc; /* Adds a light gray border around the input field. */
border-radius: 4px; /* Slightly rounds the corners of the input field. */
font-size: 1rem; /* Sets the font size within the input field to a standard readable size. */
}
/* Style for the search results list */
#search-results {
list-style: none; /* Removes the default bullet points from the unordered list. */
padding: 0; /* Removes default padding from the list. */
margin: 0; /* Removes default margins from the list. */
}
/* Style for individual search result items */
#search-results li {
padding: 8px 0; /* Adds vertical padding to each list item for spacing. */
cursor: pointer; /* Changes the mouse cursor to a pointer on hover, indicating the item is clickable. */
border-bottom: 1px solid #eee; /* Adds a light gray line below each list item, acting as a separator. */
}
/* Style for the last search result item (no bottom border) */
#search-results li:last-child {
border-bottom: none; /* Removes the bottom border from the last item in the list for a cleaner look. */
}
/* Hover effect for search result items */
#search-results li:hover {
background-color: #f0f0f0; /* Changes the background color of a list item when hovered, providing visual feedback. */
}// script.js
// Define options for the MapsIndoors Google Maps view
const mapViewOptions = {
element: document.getElementById('map'),
center: { lng: -97.74204591828197, lat: 30.36022358949809 },
zoom: 17,
maxZoom: 22,
};
// Set the MapsIndoors API key
mapsindoors.MapsIndoors.setMapsIndoorsApiKey('YOUR_MAPSINDOORS_API_KEY'); // Replace with your MapsIndoors API key
// Create a new instance of the MapsIndoors Google Maps view
const mapViewInstance = new mapsindoors.mapView.GoogleMapsView(mapViewOptions);
// Create a new MapsIndoors instance, passing the map view
const mapsIndoorsInstance = new mapsindoors.MapsIndoors({
mapView: mapViewInstance,
// Set the venue ID to load the map for a specific venue
venue: 'YOUR_MAPSINDOORS_VENUE_ID', // Replace with your actual venue ID
});
/** Floor Selector **/
// Create a new HTML div element to host the floor selector
const floorSelectorElement = document.createElement('div');
// Create a new FloorSelector instance, linking it to the HTML element and the main MapsIndoors instance.
new mapsindoors.FloorSelector(floorSelectorElement, mapsIndoorsInstance);
// Get the underlying Google Maps instance
const googleMapInstance = mapViewInstance.getMap();
// Add the floor selector HTML element to the Google Maps controls.
googleMapInstance.controls[google.maps.ControlPosition.TOP_RIGHT].push(floorSelectorElement);
/** Handle Location Clicks **/
// Function to handle clicks on MapsIndoors locations
function handleLocationClick(location) {
if (location && location.id) {
// Move the map to the selected location
mapsIndoorsInstance.goTo(location);
// Ensure that the map shows the correct floor
mapsIndoorsInstance.setFloor(location.properties.floor);
// Select the location on the map
mapsIndoorsInstance.selectLocation(location);
}
}
// Add an event listener to the MapsIndoors instance for click events on locations
mapsIndoorsInstance.on('click', handleLocationClick);
/** Search Functionality **/
// Get references to the search input and results list elements
const searchInputElement = document.getElementById('search-input');
const searchResultsElement = document.getElementById('search-results');
// Initially hide the search results list
searchResultsElement.classList.add('hidden');
// Add an event listener to the search input for 'input' events
// This calls the onSearch function every time the user types in the input field
searchInputElement.addEventListener('input', onSearch);
// Function to perform the search and update the results list and map highlighting
function onSearch() {
// Get the current value from the search input
const query = searchInputElement.value;
// Get the current venue from the MapsIndoors instance
const currentVenue = mapsIndoorsInstance.getVenue();
// Clear map highlighting
mapsIndoorsInstance.highlight();
// Deselect any selected location
mapsIndoorsInstance.deselectLocation();
// Check if the query is too short (less than 3 characters) or empty
if (query.length < 3) {
// Hide the results list if the query is too short or empty
searchResultsElement.classList.add('hidden');
return; // Stop here
}
// Define search parameters with the current input value
// Include the current venue name in the search parameters
const searchParameters = { q: query, venue: currentVenue ? currentVenue.name : undefined };
// Call the MapsIndoors LocationsService to get locations based on the search query
mapsindoors.services.LocationsService.getLocations(searchParameters).then(locations => {
// Clear previous search results
searchResultsElement.innerHTML = null;
// If no locations are found, display a "No results found" message
if (locations.length === 0) {
const noResultsItem = document.createElement('li');
noResultsItem.textContent = 'No results found';
searchResultsElement.appendChild(noResultsItem);
// Ensure the results list is visible to show the "No results found" message
searchResultsElement.classList.remove('hidden');
return; // Stop here if no results
}
// Append new search results to the list
locations.forEach(location => {
const listElement = document.createElement('li');
// Display the location name
listElement.innerHTML = location.properties.name;
// Store the location ID on the list item for easy access
listElement.dataset.locationId = location.id;
// Add a click event listener to each list item
listElement.addEventListener('click', function () {
// Call the handleLocationClick function when a location in the search results is clicked.
handleLocationClick(location);
});
searchResultsElement.appendChild(listElement);
});
// Show the results list now that it has content
searchResultsElement.classList.remove('hidden');
// Filter map to only display search results by highlighting them
mapsIndoorsInstance.highlight(locations.map(location => location.id));
})
.catch(error => {
console.error("Error fetching locations:", error);
const errorItem = document.createElement('li');
errorItem.textContent = 'Error performing search.';
searchResultsElement.appendChild(errorItem);
searchResultsElement.classList.remove('hidden');
});
}<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MapsIndoors</title>
<link rel="stylesheet" href="style.css">
<script src="https://maps.googleapis.com/maps/api/js?libraries=geometry&key=YOUR_GOOGLE_MAPS_API_KEY"></script>
<script src="https://app.mapsindoors.com/mapsindoors/js/sdk/4.41.0/mapsindoors-4.41.0.js.gz"
integrity="sha384-3lk3cwVPj5MpUyo5T605mB0PMHLLisIhNrSREQsQHjD9EXkHBjz9ETgopmTbfMDc"
crossorigin="anonymous"></script>
</head>
<body>
<div id="map"></div>
<div class="panel">
<div id="search-ui" class="flex-column">
<input type="text" id="search-input" placeholder="Search for a location...">
<ul id="search-results"></ul>
</div>
<!-- New Details UI - initially hidden -->
<div id="details-ui" class="hidden flex-column">
<h3 id="details-name"></h3>
<p id="details-description"></p>
<button id="details-close" class="details-button">Close</button>
</div>
</div>
<script src="script.js"></script>
</body>
</html>/* style.css */
/* Use flexbox for the main layout */
html, body {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden; /* Prevent scrollbars if map is full size */
display: flex;
flex-direction: column; /* Stack children vertically if needed later */
}
/* Style for the map container */
#map {
/* Make map fill available space */
flex-grow: 1;
width: 100%; /* Make map fill width */
margin: 0;
padding: 0;
}
/* Style for the information panel container */
.panel {
position: absolute; /* Position over the map */
top: 10px; /* Distance from the top */
left: 10px; /* Distance from the left */
z-index: 10; /* Ensure it's above the map and other elements */
background-color: white; /* White background for readability */
padding: 10px;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.2); /* Add a subtle shadow */
max-height: 80%; /* Limits max-height to prevent overflow with details */
overflow-y: auto; /* Add scroll if content exceeds max-height */
border: 1px solid #ccc; /* Add border for clarity */
width: 300px;
}
/* Class to apply flex display and column direction */
.flex-column {
display: flex;
flex-direction: column;
gap: 10px; /* Space between elements */
}
/* Class to hide elements */
.hidden {
display: none;
}
/* Style for the search input field */
#search-input {
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 1rem;
}
/* Style for the search results list */
#search-results {
list-style: none; /* Remove default list bullets */
padding: 0;
margin: 0;
}
/* Style for individual search result items */
#search-results li {
padding: 8px 0;
cursor: pointer; /* Indicate clickable items */
border-bottom: 1px solid #eee; /* Separator line */
}
/* Style for the last search result item (no bottom border) */
#search-results li:last-child {
border-bottom: none;
}
/* Hover effect for search result items */
#search-results li:hover {
background-color: #f0f0f0; /* Highlight on hover */
}
/* --- New Styles for Location Details UI elements --- */
/* Styles for the new UI wrappers within #search-container */
#search-ui,
#details-ui {
width: 100%; /* Ensure they fill the container's width */
/* display: flex and flex-direction are controlled by .flex-column-container class via JS */
}
/* Style for the location name in details */
#details-name {
margin-top: 0;
margin-bottom: 10px;
font-size: 1.2rem;
border-bottom: 1px solid #eee;
padding-bottom: 5px;
}
/* Style for the location description in details */
#details-description {
margin-bottom: 15px;
font-size: 0.9rem;
color: #555;
}
/* Style for general buttons within details */
.details-button {
padding: 8px;
border: none;
border-radius: 4px;
font-size: 0.9rem;
cursor: pointer;
margin-bottom: 8px; /* Space between buttons */
transition: background-color 0.3s ease;
}
.details-button:last-child {
margin-bottom: 0;
}
/* Specific style for the Close button */
#details-close {
background-color: #ccc; /* Grey */
color: #333;
}
#details-close:hover {
background-color: #bbb;
}// script.js
// Define options for the MapsIndoors Google Maps view
const mapViewOptions = {
element: document.getElementById('map'),
// Initial map center (MapsPeople - Austin Office example)
center: { lng: -97.74204591828197, lat: 30.36022358949809 },
// Initial zoom level
zoom: 17,
// Maximum zoom level
maxZoom: 22
};
// Set the MapsIndoors API key
mapsindoors.MapsIndoors.setMapsIndoorsApiKey('YOUR_MAPSINDOORS_API_KEY'); // Replace with your MapsIndoors API key
// Create a new instance of the MapsIndoors Google Maps view
const mapViewInstance = new mapsindoors.mapView.GoogleMapsView(mapViewOptions);
// Create a new MapsIndoors instance, passing the map view
const mapsIndoorsInstance = new mapsindoors.MapsIndoors({
mapView: mapViewInstance,
// Set the venue ID to load the map for a specific venue
venue: 'YOUR_MAPSINDOORS_VENUE_ID', // Replace with your actual venue ID
});
/** Floor Selector **/
// Create a new HTML div element to host the floor selector
const floorSelectorElement = document.createElement('div');
// Create a new FloorSelector instance, linking it to the HTML element and the main MapsIndoors instance.
new mapsindoors.FloorSelector(floorSelectorElement, mapsIndoorsInstance);
// Get the underlying Google Maps instance
const googleMapInstance = mapViewInstance.getMap();
// Add the floor selector HTML element to the Google Maps controls.
googleMapInstance.controls[google.maps.ControlPosition.TOP_RIGHT].push(floorSelectorElement);
/** Handle Location Clicks **/
// Function to handle clicks on MapsIndoors locations
function handleLocationClick(location) {
if (location && location.id) {
// Move the map to the selected location
mapsIndoorsInstance.goTo(location);
// Ensure that the map shows the correct floor
mapsIndoorsInstance.setFloor(location.properties.floor);
// Select the location on the map
mapsIndoorsInstance.selectLocation(location);
// Show the details UI for the clicked location
showDetails(location);
}
}
// Add an event listener to the MapsIndoors instance for click events on locations
mapsIndoorsInstance.on('click', handleLocationClick);
/** Search Functionality **/
// Get references to the search input and results list elements
const searchInputElement = document.getElementById('search-input');
const searchResultsElement = document.getElementById('search-results');
// Initially hide the search results list
searchResultsElement.classList.add('hidden');
// Add an event listener to the search input for 'input' events
// This calls the onSearch function every time the user types in the input field
searchInputElement.addEventListener('input', onSearch);
// Function to perform the search and update the results list and map highlighting
function onSearch() {
// Get the current value from the search input
const query = searchInputElement.value;
// Get the current venue from the MapsIndoors instance
const currentVenue = mapsIndoorsInstance.getVenue();
// Clear map highlighting
mapsIndoorsInstance.highlight();
// Deselect any selected location
mapsIndoorsInstance.deselectLocation();
// Check if the query is too short (less than 3 characters) or empty
if (query.length < 3) {
// Hide the results list if the query is too short or empty
searchResultsElement.classList.add('hidden');
return; // Stop here
}
// Define search parameters with the current input value
// Include the current venue name in the search parameters
const searchParameters = { q: query, venue: currentVenue ? currentVenue.name : undefined };
// Call the MapsIndoors LocationsService to get locations based on the search query
mapsindoors.services.LocationsService.getLocations(searchParameters).then(locations => {
// Clear previous search results
searchResultsElement.innerHTML = null;
// If no locations are found, display a "No results found" message
if (locations.length === 0) {
const noResultsItem = document.createElement('li');
noResultsItem.textContent = 'No results found';
searchResultsElement.appendChild(noResultsItem);
// Ensure the results list is visible to show the "No results found" message
searchResultsElement.classList.remove('hidden');
return; // Stop here if no results
}
// Append new search results to the list
locations.forEach(location => {
const listElement = document.createElement('li');
// Display the location name
listElement.innerHTML = location.properties.name;
// Store the location ID on the list item for easy access
listElement.dataset.locationId = location.id;
// Add a click event listener to each list item
listElement.addEventListener('click', function () {
// Call the handleLocationClick function when a location in the search results is clicked.
handleLocationClick(location);
});
searchResultsElement.appendChild(listElement);
});
// Show the results list now that it has content
searchResultsElement.classList.remove('hidden');
// Filter map to only display search results by highlighting them
mapsIndoorsInstance.highlight(locations.map(location => location.id));
})
.catch(error => {
console.error("Error fetching locations:", error);
const errorItem = document.createElement('li');
errorItem.textContent = 'Error performing search.';
searchResultsElement.appendChild(errorItem);
searchResultsElement.classList.remove('hidden');
});
}
/** UI state management **/
const searchUIElement = document.getElementById('search-ui');
const detailsUIElement = document.getElementById('details-ui');
function showSearchUI() {
hideDetailsUI();
searchUIElement.classList.remove('hidden');
searchInputElement.focus();
}
function showDetailsUI() {
hideSearchUI();
detailsUIElement.classList.remove('hidden');
}
function hideSearchUI() {
searchUIElement.classList.add('hidden');
}
function hideDetailsUI() {
detailsUIElement.classList.add('hidden');
}
/** Location Details **/
// Get references to the static details view elements
const detailsNameElement = document.getElementById('details-name');
const detailsDescriptionElement = document.getElementById('details-description');
const detailsCloseButton = document.getElementById('details-close');
detailsCloseButton.addEventListener('click', () => {
mapsIndoorsInstance.deselectLocation(); // Deselect any selected location
showSearchUI();
});
// Show the details of a location
function showDetails(location) {
detailsNameElement.textContent = location.properties.name;
detailsDescriptionElement.textContent = location.properties.description || 'No description available.';
showDetailsUI();
}
// Initial call to set up the search UI when the page loads
showSearchUI();<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MapsIndoors</title>
<link rel="stylesheet" href="style.css">
<link href='https://api.mapbox.com/mapbox-gl-js/v3.10.0/mapbox-gl.css' rel='stylesheet' />
<script src="https://app.mapsindoors.com/mapsindoors/js/sdk/4.41.0/mapsindoors-4.41.0.js.gz"
integrity="sha384-tFHttWqE6qOoX8etJurRBBXpH6puWNTgC8Ilq477ltu4EcpHk9ZwFPJDIli9wAS7"
crossorigin="anonymous"></script>
<script src='https://api.mapbox.com/mapbox-gl-js/v3.10.0/mapbox-gl.js'></script>
</head>
<body>
<div id="map"></div>
<div class="panel">
<!-- Search UI elements from Step 2 -->
<div id="search-ui" class="flex-column"> <!-- Wrap search elements -->
<input type="text" id="search-input" placeholder="Search for a location...">
<ul id="search-results"></ul>
</div>
<!-- New Details UI - initially hidden -->
<div id="details-ui" class="hidden flex-column">
<h3 id="details-name"></h3>
<p id="details-description"></p>
<button id="details-close" class="details-button">Close</button>
</div>
</div>
<script src="script.js"></script>
</body>
</html>/* style.css */
/* Use flexbox for the main layout */
html, body {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden; /* Prevent scrollbars if map is full size */
display: flex;
flex-direction: column; /* Stack children vertically if needed later */
}
/* Style for the map container */
#map {
/* Make map fill available space */
flex-grow: 1;
width: 100%; /* Make map fill width */
margin: 0;
padding: 0;
}
/* Style for the information panel container */
.panel {
position: absolute; /* Position over the map */
top: 10px; /* Distance from the top */
left: 10px; /* Distance from the left */
z-index: 10; /* Ensure it's above the map and other elements */
background-color: white; /* White background for readability */
padding: 10px;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.2); /* Add a subtle shadow */
max-height: 80%; /* Limits max-height to prevent overflow with details */
overflow-y: auto; /* Add scroll if content exceeds max-height */
border: 1px solid #ccc; /* Add border for clarity */
width: 300px;
}
/* Class to apply flex display and column direction */
.flex-column {
display: flex;
flex-direction: column;
gap: 10px; /* Space between elements */
}
/* Class to hide elements */
.hidden {
display: none;
}
/* Style for the search input field */
#search-input {
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 1rem;
}
/* Style for the search results list */
#search-results {
list-style: none; /* Remove default list bullets */
padding: 0;
margin: 0;
}
/* Style for individual search result items */
#search-results li {
padding: 8px 0;
cursor: pointer; /* Indicate clickable items */
border-bottom: 1px solid #eee; /* Separator line */
}
/* Style for the last search result item (no bottom border) */
#search-results li:last-child {
border-bottom: none;
}
/* Hover effect for search result items */
#search-results li:hover {
background-color: #f0f0f0; /* Highlight on hover */
}
/* --- New Styles for Location Details UI elements --- */
/* Styles for the new UI wrappers within #search-container */
#search-ui,
#details-ui {
width: 100%; /* Ensure they fill the container's width */
/* display: flex and flex-direction are controlled by .flex-column-container class via JS */
}
/* Style for the location name in details */
#details-name {
margin-top: 0;
margin-bottom: 10px;
font-size: 1.2rem;
border-bottom: 1px solid #eee;
padding-bottom: 5px;
}
/* Style for the location description in details */
#details-description {
margin-bottom: 15px;
font-size: 0.9rem;
color: #555;
}
/* Style for general buttons within details */
.details-button {
padding: 8px;
border: none;
border-radius: 4px;
font-size: 0.9rem;
cursor: pointer;
margin-bottom: 8px; /* Space between buttons */
transition: background-color 0.3s ease;
}
.details-button:last-child {
margin-bottom: 0;
}
/* Specific style for the Close button */
#details-close {
background-color: #ccc; /* Grey */
color: #333;
}
#details-close:hover {
background-color: #bbb;
}// script.js
// Define options for the MapsIndoors Mapbox view
const mapViewOptions = {
accessToken: 'YOUR_MAPBOX_ACCESS_TOKEN', // Replace with your Mapbox token
element: document.getElementById('map'),
// Initial map center (MapsPeople - Austin Office example)
center: { lng: -97.74204591828197, lat: 30.36022358949809 },
// Initial zoom level
zoom: 17,
// Maximum zoom level
maxZoom: 22,
// The zoom level at which MapsIndoors transitions
mapsIndoorsTransitionLevel: 16
};
// Set the MapsIndoors API key
mapsindoors.MapsIndoors.setMapsIndoorsApiKey('YOUR_MAPSINDOORS_API_KEY'); // Replace with your MapsIndoors API key
// Create a new instance of the MapsIndoors Mapbox view
const mapViewInstance = new mapsindoors.mapView.MapboxV3View(mapViewOptions);
// Create a new MapsIndoors instance, passing the map view
const mapsIndoorsInstance = new mapsindoors.MapsIndoors({
mapView: mapViewInstance,
// Set the venue ID to load the map for a specific venue
venue: 'YOUR_MAPSINDOORS_VENUE_ID', // Replace with your actual venue ID
});
/** Floor Selector **/
// Create a new HTML div element to host the floor selector
const floorSelectorElement = document.createElement('div');
// Create a new FloorSelector instance, linking it to the HTML element and the main MapsIndoors instance.
new mapsindoors.FloorSelector(floorSelectorElement, mapsIndoorsInstance);
// Get the underlying Mapbox map instance
const mapboxInstance = mapViewInstance.getMap();
// Add the floor selector HTML element to the Mapbox map using Mapbox's addControl method
// We wrap the element in an object implementing the IControl interface expected by addControl
mapboxInstance.addControl({
onAdd: function () {
// This function is called when the control is added to the map.
// It should return the control's DOM element.
return floorSelectorElement;
},
onRemove: function () {
// This function is called when the control is removed from the map.
// Clean up any event listeners or resources here.
floorSelectorElement.parentNode.removeChild(floorSelectorElement);
},
}, 'top-right'); // Optional: Specify a position ('top-left', 'top-right', 'bottom-left', 'bottom-right')
/** Handle Location Clicks **/
// Function to handle clicks on MapsIndoors locations
function handleLocationClick(location) {
if (location && location.id) {
// Move the map to the selected location
mapsIndoorsInstance.goTo(location);
// Ensure that the map shows the correct floor
mapsIndoorsInstance.setFloor(location.properties.floor);
// Select the location on the map
mapsIndoorsInstance.selectLocation(location);
// Show the details UI for the clicked location
showDetails(location);
}
}
// Add an event listener to the MapsIndoors instance for click events on locations
mapsIndoorsInstance.on('click', handleLocationClick);
/** Search Functionality **/
// Get references to the search input and results list elements
const searchInputElement = document.getElementById('search-input');
const searchResultsElement = document.getElementById('search-results');
// Initially hide the search results list
searchResultsElement.classList.add('hidden');
// Add an event listener to the search input for 'input' events
// This calls the onSearch function every time the user types in the input field
searchInputElement.addEventListener('input', onSearch);
// Function to perform the search and update the results list and map highlighting
function onSearch() {
// Get the current value from the search input
const query = searchInputElement.value;
// Get the current venue from the MapsIndoors instance
const currentVenue = mapsIndoorsInstance.getVenue();
// Clear map highlighting
mapsIndoorsInstance.highlight();
// Deselect any selected location
mapsIndoorsInstance.deselectLocation();
// Check if the query is too short (less than 3 characters) or empty
if (query.length < 3) {
// Hide the results list if the query is too short or empty
searchResultsElement.classList.add('hidden');
return; // Stop here
}
// Define search parameters with the current input value
// Include the current venue name in the search parameters
const searchParameters = { q: query, venue: currentVenue ? currentVenue.name : undefined };
// Call the MapsIndoors LocationsService to get locations based on the search query
mapsindoors.services.LocationsService.getLocations(searchParameters).then(locations => {
// Clear previous search results
searchResultsElement.innerHTML = null;
// If no locations are found, display a "No results found" message
if (locations.length === 0) {
const noResultsItem = document.createElement('li');
noResultsItem.textContent = 'No results found';
searchResultsElement.appendChild(noResultsItem);
// Ensure the results list is visible to show the "No results found" message
searchResultsElement.classList.remove('hidden');
return; // Stop here if no results
}
// Append new search results to the list
locations.forEach(location => {
const listElement = document.createElement('li');
// Display the location name
listElement.innerHTML = location.properties.name;
// Store the location ID on the list item for easy access
listElement.dataset.locationId = location.id;
// Add a click event listener to each list item
listElement.addEventListener('click', function () {
// Call the handleLocationClick function when a location in the search results is clicked.
handleLocationClick(location);
});
searchResultsElement.appendChild(listElement);
});
// Show the results list now that it has content
searchResultsElement.classList.remove('hidden');
// Filter map to only display search results by highlighting them
mapsIndoorsInstance.highlight(locations.map(location => location.id));
})
.catch(error => {
console.error("Error fetching locations:", error);
const errorItem = document.createElement('li');
errorItem.textContent = 'Error performing search.';
searchResultsElement.appendChild(errorItem);
searchResultsElement.classList.remove('hidden');
});
}
/** UI state management **/
const searchUIElement = document.getElementById('search-ui');
const detailsUIElement = document.getElementById('details-ui');
function showSearchUI() {
hideDetailsUI();
searchUIElement.classList.remove('hidden');
searchInputElement.focus();
}
function showDetailsUI() {
hideSearchUI();
detailsUIElement.classList.remove('hidden');
}
function hideSearchUI() {
searchUIElement.classList.add('hidden');
}
function hideDetailsUI() {
detailsUIElement.classList.add('hidden');
}
/** Location Details **/
// Get references to the static details view elements
const detailsNameElement = document.getElementById('details-name');
const detailsDescriptionElement = document.getElementById('details-description');
const detailsCloseButton = document.getElementById('details-close');
detailsCloseButton.addEventListener('click', () => {
mapsIndoorsInstance.deselectLocation(); // Deselect any selected location
showSearchUI();
});
// Show the details of a location
function showDetails(location) {
detailsNameElement.textContent = location.properties.name;
detailsDescriptionElement.textContent = location.properties.description || 'No description available.';
showDetailsUI();
}
// Initial call to set up the search UI when the page loads
showSearchUI();Otherwise, it iterates through each location object:
Creates an <li> element.
Sets its innerHTML to location.properties.name. The properties object on an object conforming to the Location interface contains various details about the location. For more information, see the .
Stores location.id in listElement.dataset.locationId for potential future use.
Adds a click event listener to the list item. When clicked:
mapsIndoorsInstance.goTo(location): Pans and zooms the map to the clicked location. For more details on goTo(), see its .
mapsIndoorsInstance.setFloor(location.properties.floor): Changes the map to the location's floor. To understand floor management, check the
Appends the new list item to searchResultsElement.
Collects all location.ids into locationIdsToHighlight.
Makes the searchResultsElement visible by removing the .hidden class.
Calls mapsIndoorsInstance.highlight(locationIdsToHighlight) to highlight all found locations on the map simultaneously. The highlight() method accepts an array of location IDs. See its API documentation for details on batch highlighting.
Otherwise, it iterates through each location object:
Creates an <li> element.
Sets its innerHTML to location.properties.name. The properties object on an object conforming to the Location interface contains various details about the location. For more information, see the .
Stores location.id in listElement.dataset.locationId for potential future use.
Adds a click event listener to the list item. When clicked:
mapsIndoorsInstance.goTo(location): Pans and zooms the map to the clicked location. For more details on goTo(), see its .
mapsIndoorsInstance.setFloor(location.properties.floor): Changes the map to the location's floor. To understand floor management, check the
Appends the new list item to searchResultsElement.
Collects all location.ids into locationIdsToHighlight.
Makes the searchResultsElement visible by removing the .hidden class.
Calls mapsIndoorsInstance.highlight(locationIdsToHighlight) to highlight all found locations on the map simultaneously. The highlight() method accepts an array of location IDs. See its API documentation for details on batch highlighting.
<button id="details-close" class="details-button">Close</button>: A button to close the details view and return to the search results.hideSearchUI() and hideDetailsUI(): Helper functions to manage UI visibility.showDetails(location): Updates the details UI with the location's name and description.
The close button in the details panel has an event listener that returns to the search UI.
mapsIndoorsInstance.selectLocation(location): Visually highlights the selected location on the map.showDetailsThe map will switch to the correct floor of the selected location.
Check for console errors when selectLocation, goTo, or setFloor are called.
<button id="details-close" class="details-button">Close</button>: A button to close the details view and return to the search results.hideSearchUI() and hideDetailsUI(): Helper functions to manage UI visibility.showDetails(location): Updates the details UI with the location's name and description.
The close button in the details panel has an event listener that returns to the search UI.
mapsIndoorsInstance.selectLocation(location): Visually highlights the selected location on the map.showDetailsThe map will switch to the correct floor of the selected location.
Check for console errors when selectLocation, goTo, or setFloor are called.
mapsIndoorsInstance.selectLocation(location): Selects and highlights this specific location on the map. For further information, refer to the selectLocation() API documentation.
mapsIndoorsInstance.selectLocation(location): Selects and highlights this specific location on the map. For further information, refer to the selectLocation() API documentation.
Searching through your MapsIndoors data is an key part of a great user experience with your maps. Users can look for places to go, or filter what is shown on the map.
Searches work on all MapsIndoors geodata. It is up to you to create a search experience that fits your use case.
getLocation(id)This example assumes that you've stored the MapsIndoors Location ID directly on the card, accessible via a custom attribute or some other mechanism. When the card is clicked, getLocation(id) retrieves the corresponding location, centers the map on it, and sets the zoom level.
In this example, an external (custom) ID is stored on the card. The getLocationsByExternalId method is used to fetch locations, taking into account that multiple locations could be returned.
Both examples work off a click event attached to a card element. The key difference lies in the method used to query MapsIndoors for the location information. Example 1 uses the native MapsIndoors ID, while Example 2 uses an external ID. Both methods then focus the map on the retrieved location.
Take advantage of using MapsIndoors native filtering and highlighting functionality via the SDK without needing to implement your own custom display logic.
This code assumes that if multiple locations are returned from getLocationByExternalId(), the first one is the relevant location for display. The remainder of the code remains largely similar to the previous example, but we're fetching the locations based on an external ID instead of a MapsIndoors Location ID.
For this use-case, we're focusing on how to display an information popup or info window when a MapsIndoors location is clicked. Both Mapbox and Google Maps implementations are provided. These popups will show a custom image and a link, allowing developers the flexibility to populate it with any content. Let's assume you're getting back your location object from one of the approaches in Use-Case 1.
Mapbox Implementation
In the Mapbox example, we add an event listener to the MapsIndoors instance that listens for 'click' events. When a location is clicked, we display a Mapbox popup at the location's coordinates. Any previously displayed popup will be removed to avoid clutter.
Google Maps Implementation
In the Google Maps example, we add an event listener to the MapsIndoors instance to listen for location clicks. When a location is clicked, an info window appears at the coordinates of that location. Similar to the Mapbox example, any previously displayed info window will be closed.
Both implementations ensure that only one popup or info window is open at a given time, closing any previous ones when a new location is clicked. This keeps the map clean and focuses the user's attention on the most recently clicked location.
To help you in this, there is a range of filters you can apply to the search queries to get the best results. E.g. you can filter by Categories, search only a specific part of the map or search near a Location.
All three return a list of Locations from your solution matching the parameters they are given. The results are ranked upon the three following factors:
If a "near" parameter is set, how close is the origin point to the result?
How well does the search input text match the text of the result?
(Our base algorithm for searching is using the "Levenshtein distance" algorithm)
Which kind of geodata is the result (e.g. Buildings are ranked over POIs)?
This means that the first item in the search result list will be the one best matching the three factors. You always have the ability to reorder your array of locations based on your preference before rendering them in your user interface, if you choose to handle that via some client-side code.
Feel free to refer to this table for a comprehensive understanding of each parameter's type, optional/required status, and functionality.
q
string
Optional
Use a text query to search for one or more locations.
fields
string
Optional
See the full list of parameters in the reference guide:
When displaying the search results, it is helpful to filter the map to only show matching Locations. Matching Buildings and Venues will still be shown on the map, as they give context to the user, even if they aren't selectable on the map.
After displaying the search results on your map you can then clear the filter so that all Locations show up on the map again.
You can also search for Locations, and have them presented to you as a list, instead of just displaying them on the map.
The full code example is shown in the JSFiddle below which will be examined below.
The mapsindoors.services.LocationsService class exposes the getLocations function that enables you to search for Locations.
It will return a promise that gets resolved when the query has executed.
See mapsindoors.services.LocationsService for more information.
The debounce method is there to ensure that the service is not being called in rapid succession. This method delays the execution of the function by 500ms, unless debounce is called again within 500ms, in which case the timer is reset.
See this article "What is debouncing" by Jamis Charles for a more detailed description of the debounce concept.
When the function executes, we check whether the input is empty or not. A request object is created if the input is not empty.
The getLocations function expects either no input, in which case it returns all Locations, or an Object (please refer to the official documentation for an exhaustive list of properties). In this case, the constant value is passed to the q property and the includeOutsidePOI property is set to true. When the Promise resolves, the response is passed to the displayResults helper function.
If the input is empty, we clear the result list and reset the map filter by calling the helper functions clearResults and clearFilter.
We need to clear the previous results, and check if any Locations were returned. If so, we loop through them and add them to the result list.
If no Locations are returned, a message is shown to the user stating "No results matched the query.". Otherwise, we pass the Locations on to the next helper function called filterMap.
The purpose of the filterMap function is to create a list of location ids used to filter the Locations on the map.
The second parameter tells MapsIndoors not to change the viewport of the map.
For more information, see MapsIndoors.filter in the reference documentation.
Goal: This guide will show you how to add directions functionality to your application. Users will be able to select an origin and destination, get a route between them, and step through the directions on the map. This step builds on the search and details UI from Step 3, introducing a new directions panel and integrating the MapsIndoors DirectionsService and DirectionsRenderer.
SDK Concepts Introduced:
Using the to calculate routes between locations.
Using the to display and step through routes on the map.
card.addEventListener('click', () => {
const locationId = card.getAttribute('data-location-id'); // Assume the MapsIndoors ID is stored in a data attribute
mapsindoors.services.LocationsService.getLocation(locationId).then(location => {
mapsIndoorsInstance.setFloor(location.properties.floor);
mapInstance.setCenter({
lat: location.properties.anchor.coordinates[1],
lng: location.properties.anchor.coordinates[0]
});
mapInstance.setZoom(18);
});
});card.addEventListener('click', () => {
const externalId = card.getAttribute('data-external-id'); // Assume the external ID is stored in a data attribute
// Use getLocationsByExternalId to fetch locations by their external IDs
mapsindoors.services.LocationsService.getLocationsByExternalId(externalId).then(locations => {
if (locations.length > 0) {
const location = locations[0]; // Take the first location if multiple are returned
// Set the floor and center the map
mapsIndoorsInstance.setFloor(location.properties.floor);
mapInstance.setCenter({
lat: location.properties.anchor.coordinates[1],
lng: location.properties.anchor.coordinates[0]
});
mapInstance.setZoom(18);
} else {
// Handle the case where no locations are returned for the given external ID
console.warn(`No locations found for external ID ${externalId}`);
}
});
});// Define the rule outside the listener for better readability and reusability
const rule = {
visible: true,
polygonVisible: true,
polygonFillColor: "#FF0000",
polygonFillOpacity: 1,
iconSize: { width: 30, height: 30 },
labelVisible: true,
};
let previousLocationId = null; // Keep track of previously filtered location
card.addEventListener('click', () => {
const locationId = card.getAttribute('data-location-id');
mapsindoors.services.LocationsService.getLocation(locationId).then(location => {
// Optionally set the floor and center the map
mapsIndoorsInstance.setFloor(location.properties.floor);
mapInstance.setCenter({
lat: location.properties.anchor.coordinates[1],
lng: location.properties.anchor.coordinates[0]
});
// Reset display rule for previously filtered location, if any
if (previousLocationId) {
mapsIndoorsInstance.setDisplayRule(previousLocationId, null);
}
// Apply the new display rule to the clicked location
mapsIndoorsInstance.setDisplayRule(location.id, rule);
// Update the previous location ID
previousLocationId = location.id;
});
});// Define the display rule
const rule = {
visible: true,
polygonVisible: true,
polygonFillColor: "#FF0000",
polygonFillOpacity: 1,
iconSize: { width: 30, height: 30 },
labelVisible: true,
};
let previousLocationId = null; // Keep track of the previously filtered location
card.addEventListener('click', () => {
const externalId = card.getAttribute('data-external-id');
// Use getLocationsByExternalId() to fetch locations by their external IDs
mapsindoors.services.LocationsService.getLocationsByExternalId(externalId).then(locations => {
if (locations.length > 0) {
const location = locations[0]; // Assume the first location is the one to display
// Optionally set the floor and center the map
mapsIndoorsInstance.setFloor(location.properties.floor);
mapInstance.setCenter({
lat: location.properties.anchor.coordinates[1],
lng: location.properties.anchor.coordinates[0]
});
// Reset display rule for the previously filtered location, if any
if (previousLocationId) {
mapsIndoorsInstance.setDisplayRule(previousLocationId, null);
}
// Apply the new display rule to the location
mapsIndoorsInstance.setDisplayRule(location.id, rule);
// Update the ID of the previously filtered location
previousLocationId = location.id;
} else {
console.warn(`No locations found for external ID ${externalId}`);
}
});
});let currentPopup = null;
export const handleLocationClickForMapbox = (location, mapsIndoorsInstance, mapInstance) => {
if (currentPopup) {
currentPopup.remove();
}
const coords = location.properties.anchor.coordinates;
const popupContent = `
<img src="${'Your_custom_image_url_here'}" alt="${location.properties.description}" width="100" height="100" />
<h2><a href="${'Your_custom_link_here'}" target="_blank">${location.properties.name}</a></h2>
`;
currentPopup = new mapboxgl.Popup({ closeOnClick: true, closeButton: true })
.setLngLat(coords)
.setHTML(popupContent)
.addTo(mapInstance);
};
mapsIndoorsInstance.addListener('click', location => {
handleLocationClickForMapbox(location, mapsIndoorsInstance, mapInstance);
});let previousInfoWindow = null;
mapsIndoorsInstance.addListener('click', location => {
if (previousInfoWindow) {
previousInfoWindow.close();
}
const coords = location.properties.anchor.coordinates;
const infoWindowContent = `
<img src="${'Your_custom_image_url_here'}" alt="${location.properties.description}" width="100" height="100" />
<h2><a href="${'Your_custom_link_here'}" target="_blank">${location.properties.name}</a></h2>
`;
const infoWindowOptions = {
content: infoWindowContent,
position: {
lat: coords[1],
lng: coords[0]
}
};
const infoWindow = new google.maps.InfoWindow(infoWindowOptions);
infoWindow.open(googleMapsInstance);
previousInfoWindow = infoWindow;
});const searchParameters = {
q: 'Office',
near: { lat: 38.897579747054046, lng: -77.03658652944773 }, // // Blue Room, The White House
take: 1
}
mapsindoors.services.LocationsService.getLocations(searchParameters).then(locations => {
console.log(locations);
});const searchParameters = {
q: 'Office',
near: { lat: 38.897579747054046, lng: -77.03658652944773 }, // // Blue Room, The White House
take: 1
}
mapsindoors.services.LocationsService.getLocations(searchParameters).then(locations => {
mapsIndoorsInstance.filter(locations.map(location => location.id), false);
});mapsIndoorsInstance.filter(null);searchElement.addEventListener('input', debounce((e) => {
const value = e.target.value;
if (value > '') {
mapsindoors.services.LocationsService.getLocations({ q: value, includeOutsidePOI: true })
.then(displayResults)
.then(filterMap);
} else {
clearResults();
clearFilter();
}
}, 500));function displayResults(locations) {
clearResults();
if (locations.length > 0) {
for (const location of locations) {
searchResults.innerHTML += `<li>${location.properties.name}</li>`;
}
} else {
searchResults.innerHTML = '<li class="no-results">No results matched the query.</li>';
}
return locations;
}function filterMap(locations) {
mapsIndoors.filter(locations.map(location => location.id), false);
return locations;
}Fields to search in when using the search string parameter 'q'. Options: "name,description,aliases,categories"
types
Array.
Optional
Filter by types in a comma-separated list. A location only has one type.
mapsindoors.services.LocationsService.getLocations({ lr: 'en', types: ['meetingroom'] }).then(locations => { ... });
categories
Array.
Optional
Filter by categories in a comma-separated list. A location can be in multiple categories.
mapsindoors.services.LocationsService.getLocations({ categories: categoryKey, lr: 'en' }).then(locations => { ... });
bbox
Object
Optional
Limits the result to inside the bounding box. Must include bbox.east, bbox.north, bbox.south, bbox.west
bbox.east
number
Required if bbox
Max longitude of the bounds in degrees.
bbox.north
number
Required if bbox
Max latitude of the bounds in degrees.
bbox.south
number
Required if bbox
Min latitude of the bounds in degrees.
bbox.west
number
Required if bbox
Min longitude of the bounds in degrees.
take
number
Optional
Max number of locations to get.
await mapsindoors.services.LocationsService.getLocations({ near: 'location:9897fd93fcb14bd39ec8110d', take: 5, ... });
skip
number
Optional
Skip the first number of entries.
near
LatLngLiteral | string
Optional
Can either be a coordinate {lat: number, lng: number} or a string in the format "type:id".
await mapsindoors.services.LocationsService.getLocations({ near: 'location:9897fd93fcb14bd39ec8110d', radius: 50, ... });
radius
number
Optional
A radius in meters. Must be supplied when using near with a point.
await mapsindoors.services.LocationsService.getLocations({ near: 'location:9897fd93fcb14bd39ec8110d', radius: 50, ... });
floor
integer
Optional
Filter locations to a specific floor.
await mapsindoors.services.LocationsService.getLocations({ near: 'location:9897fd93fcb14bd39ec8110d', floor: 50, ... });
orderBy
string
Optional
Which property the result should be sorted by.
sortOrder
string
Optional
Specifies in which order the results are sorted, either "ASC" or "DESC"
"ASC"
building
string
Optional
Limit the search for locations to a building.
venue
string
Optional
Limit the search for locations to a venue (id or name).
directionsRenderer.setRoute() to display a calculated route on the map.Using directionsRenderer.setStepIndex() to navigate to a specific step in the route.
Using directionsRenderer.nextStep() and directionsRenderer.previousStep() for step navigation.
Using directionsRenderer.getLegIndex() and directionsRenderer.getStepIndex() to track current position.
Completion of Step 3: Show Location Details. Your app should already support searching for locations and viewing details.
Your MapsIndoors API Key and Mapbox Access Token should be correctly set up. We will continue using the demo API key 02c329e6777d431a88480a09 and venue ID dfea941bb3694e728df92d3d for this example.
Open your index.html file. Add a new directions panel inside the existing .panel container:
The #details-ui panel now includes a "Get Directions" button, allowing users to open the directions panel for the selected location.
The #directions-ui panel is added to the .panel container. This new panel contains:
Input fields for origin and destination.
A button to get directions.
Step navigation controls (step indicator, previous/next buttons).
A close button for the directions panel.
All panels (#search-ui, #details-ui, #directions-ui) use consistent class and ID naming, and only one is visible at a time, managed by the show/hide functions in the JavaScript.
Add styles for the new directions UI elements:
Styles for the new #directions-ui panel and its child elements are added:
.directions-inputs styles the input fields and results list for selecting origin and destination.
.directions-results-list styles the list of search results for the origin input.
.directions-step-nav and #step-indicator style the step navigation controls.
Add the following logic for directions and UI state management:
Explanation of script.js updates:
UI State Management:
New constants like directionsUIElement are added to reference the new directions panel in the DOM.
The existing UI state management functions (showSearchUI(), showDetailsUI()) are updated to explicitly hide the directionsUIElement.
New functions showDirectionsUI() and hideDirectionsUI() are introduced to manage the visibility of the directions panel, ensuring it's mutually exclusive with the search and details panels.
The "Get Directions" button (detailsDirectionsButton) in the details panel now has an event listener that calls showDirectionsPanel(), passing the currentDetailsLocation to pre-fill the destination.
The directionsCloseButton listener is added to hide the directions UI, show the details UI, and make the directionsRenderer invisible when directions are closed, providing a clear exit path.
Location Click Handling:
The script maintains the unified click handler pattern established in previous steps. The handleLocationClick function continues to serve as the central entry point for user interactions with locations, responding to both map POI clicks and search result clicks.
This consistent pattern ensures that when a user clicks on a location (either on the map or in search results), the following actions always occur:
Directions Panel and Origin/Destination Selection:
New DOM element references are established for directions-related UI elements: originInputElement, originResultsElement, destinationInputElement, getDirectionsButton, prevStepButton, nextStepButton, stepIndicator, and directionsCloseButton
Route Calculation:
When the user clicks the "Show Route" button (getDirectionsButton), the script checks if both selectedOrigin and selectedDestination are set.
It extracts coordinates and floor information from the location objects' properties.anchor and properties.floor
Route Display and Step Navigation:
Any existing directionsRenderer is hidden before creating a new one.
A new mapsindoors.directions.DirectionsRenderer is instantiated with the mapsIndoorsInstance, fitBounds: true for automatic map adjustment, and styling options.
Workflow Integration:
The script carefully integrates the directions functionality with the existing search and details features.
The details panel now includes a "Get Directions" button that transitions to the directions workflow.
The directions panel allows users to return to the details view via the close button.
These updates allow users to search for a location, view its details, and get step-by-step directions between two locations within your venue, all within a clear and interactive UI.
Users can search for a location, view its details, and click "Get Directions" to open the directions panel.
When the directions panel opens from the details view, the destination input is pre-filled with the selected location's name and is disabled.
The user can type in the origin input to search for and select an origin location.
After both origin and destination are selected, clicking "Show Route" calculates and displays the route on the map.
The map updates to show the full route, and the step navigation controls become active.
Users can use the "Previous" and "Next" buttons to step through the route, with the map highlighting the current step and the step-indicator updating accordingly.
Only one panel (search, details, or directions) is visible at a time.
Clicking the "Close Directions" button returns to the details panel and hides the route from the map.
If the route is not shown after clicking "Show Route", ensure both origin and destination are selected and that the locations have valid anchor coordinates.
If the map does not update, check for errors in the browser console and verify your API keys and venue ID.
If the UI panels do not switch as expected, ensure the show/hide functions are called correctly and that the correct classes are applied.
Goal: This guide will show you how to add directions functionality to your application. Users will be able to select an origin and destination, get a route between them, and step through the directions on the map. This step builds on the search and details UI from Step 3, introducing a new directions panel and integrating the MapsIndoors DirectionsService and DirectionsRenderer.
SDK Concepts Introduced:
Using the DirectionsService to calculate routes between locations.
Using the DirectionsRenderer to display and step through routes on the map.
Using directionsRenderer.setRoute() to display a calculated route on the map.
Using directionsRenderer.setStepIndex() to navigate to a specific step in the route.
Using directionsRenderer.nextStep() and directionsRenderer.previousStep() for step navigation.
Using directionsRenderer.getLegIndex() and directionsRenderer.getStepIndex() to track current position.
Completion of . Your app should already support searching for locations and viewing details.
Your MapsIndoors API Key and Google Maps API Key should be correctly set up. We will continue using the demo API key 02c329e6777d431a88480a09 and venue ID dfea941bb36964e728df92d3d for this example.
Open your index.html file. Add a new directions panel inside the existing .panel container:
The #details-ui panel now includes a "Get Directions" button, allowing users to open the directions panel for the selected location.
The #directions-ui panel is added to the .panel container. This new panel contains:
Input fields for origin and destination.
Add styles for the new directions UI elements:
Styles for the new #directions-ui panel and its child elements are added:
.directions-inputs styles the input fields and results list for selecting origin and destination.
.directions-results-list styles the list of search results for the origin input.
Add the following logic for directions and UI state management.
Explanation of script.js updates:
UI State Management:
New constants like directionsUIElement are added to reference the new directions panel in the DOM.
The existing UI state management functions (showSearchUI(), showDetailsUI()) are updated to explicitly hide the directionsUIElement
These updates allow users to search for a location, view its details, and get step-by-step directions between two locations within your venue, all within a clear and interactive UI.
Users can search for a location, view its details, and click "Get Directions" to open the directions panel.
When the directions panel opens from the details view, the destination input is pre-filled with the selected location's name and is disabled.
The user can type in the origin input to search for and select an origin location.
After both origin and destination are selected, clicking "Show Route" calculates and displays the route on the map.
If the route is not shown after clicking "Show Route", ensure both origin and destination are selected and that the locations have valid anchor coordinates.
If the map does not update, check for errors in the browser console and verify your API keys and venue ID.
If the UI panels do not switch as expected, ensure the show/hide functions are called correctly and that the correct classes are applied.
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MapsIndoors</title>
<link rel="stylesheet" href="style.css">
<link href='https://api.mapbox.com/mapbox-gl-js/v3.10.0/mapbox-gl.css' rel='stylesheet' />
<script src="https://app.mapsindoors.com/mapsindoors/js/sdk/4.41.0/mapsindoors-4.41.0.js.gz"
integrity="sha384-3lk3cwVPj5MpUyo5T605mB0PMHLLisIhNrSREQsQHjD9EXkHBjz9ETgopmTbfMDc"
crossorigin="anonymous"></script>
<script src='https://api.mapbox.com/mapbox-gl-js/v3.10.0/mapbox-gl.js'></script>
</head>
<body>
<div id="map"></div>
<div class="panel">
<div id="search-ui" class="flex-column">
<input type="text" id="search-input" placeholder="Search for a location...">
<ul id="search-results"></ul>
</div>
<div id="details-ui" class="hidden">
<h3 id="details-name"></h3>
<p id="details-description"></p>
<button id="details-directions" class="details-button details-action-button">Get Directions</button>
<button id="details-close" class="details-button">Close</button>
</div>
<div id="directions-ui" class="hidden flex-column">
<h3>Directions</h3>
<div class="directions-inputs">
<input type="text" id="origin-input" placeholder="Choose origin...">
<ul id="origin-results" class="directions-results-list"></ul>
<input type="text" id="destination-input" placeholder="Choose destination..." disabled>
</div>
<button id="get-directions" class="details-button details-action-button">Show Route</button>
<div class="directions-step-nav">
<span id="step-indicator"></span>
<button id="prev-step" class="details-button">Previous</button>
<button id="next-step" class="details-button">Next</button>
</div>
<button id="directions-close" class="details-button">Close Directions</button>
</div>
</div>
<script src="script.js"></script>
</body>
</html>/* style.css */
/* Use flexbox for the main layout */
html, body {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden; /* Prevent scrollbars if map is full size */
display: flex;
flex-direction: column; /* Stack children vertically if needed later */
}
/* Style for the map container */
#map {
/* Make map fill available space */
flex-grow: 1;
width: 100%; /* Make map fill width */
margin: 0;
padding: 0;
}
/* Style for the information panel container */
.panel {
position: absolute; /* Position over the map */
top: 10px; /* Distance from the top */
left: 10px; /* Distance from the left */
z-index: 10; /* Ensure it's above the map and other elements */
background-color: white; /* White background for readability */
padding: 10px;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.2); /* Add a subtle shadow */
max-height: 80%; /* Limits max-height to prevent overflow with details */
overflow-y: auto; /* Add scroll if content exceeds max-height */
border: 1px solid #ccc; /* Add border for clarity */
width: 300px;
}
/* Class to apply flex display and column direction */
.flex-column {
display: flex;
flex-direction: column;
gap: 10px; /* Space between elements */
}
/* Class to hide elements */
.hidden {
display: none;
}
/* Style for the search input field */
#search-input {
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 1rem;
}
/* Style for the search results list */
#search-results {
list-style: none; /* Remove default list bullets */
padding: 0;
margin: 0;
}
/* Style for individual search result items */
#search-results li {
padding: 8px 0;
cursor: pointer; /* Indicate clickable items */
border-bottom: 1px solid #eee; /* Separator line */
}
/* Style for the last search result item (no bottom border) */
#search-results li:last-child {
border-bottom: none;
}
/* Hover effect for search result items */
#search-results li:hover {
background-color: #f0f0f0; /* Highlight on hover */
}
/* --- New Styles for Location Details UI elements --- */
/* Styles for the new UI wrappers within #search-container */
#search-ui,
#details-ui {
width: 100%; /* Ensure they fill the container's width */
/* display: flex and flex-direction are controlled by .flex-column-container class via JS */
}
/* Style for the location name in details */
#details-name {
margin-top: 0;
margin-bottom: 10px;
font-size: 1.2rem;
border-bottom: 1px solid #eee;
padding-bottom: 5px;
}
/* Style for the location description in details */
#details-description {
margin-bottom: 15px;
font-size: 0.9rem;
color: #555;
}
/* Style for general buttons within details */
.details-button {
padding: 8px;
border: none;
border-radius: 4px;
font-size: 0.9rem;
cursor: pointer;
transition: background-color 0.3s ease;
}
/* Specific style for the Close button */
#details-close {
background-color: #ccc; /* Grey */
color: #333;
}
#details-close:hover {
background-color: #bbb;
}
/* Directions panel specific */
#directions-ui {
width: 100%;
}
.directions-inputs {
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 8px;
}
.directions-results-list {
list-style: none;
padding: 0;
margin: 0;
max-height: 120px;
overflow-y: auto;
}
.directions-results-list li {
padding: 8px 0;
cursor: pointer;
border-bottom: 1px solid #eee;
}
.directions-results-list li:last-child {
border-bottom: none;
}
.directions-results-list li:hover {
background-color: #f0f0f0;
}
.directions-step-nav {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
#step-indicator {
grid-column: span 2;
}// script.js
// Define options for the MapsIndoors Mapbox view
const mapViewOptions = {
accessToken: 'YOUR_MAPBOX_ACCESS_TOKEN', // Replace with your Mapbox token
element: document.getElementById('map'),
// Initial map center (MapsPeople - Austin Office example)
center: { lng: -97.74204591828197, lat: 30.36022358949809 },
// Initial zoom level
zoom: 17,
// Maximum zoom level
maxZoom: 22,
// The zoom level at which MapsIndoors transitions
mapsIndoorsTransitionLevel: 16
};
// Set the MapsIndoors API key
mapsindoors.MapsIndoors.setMapsIndoorsApiKey('YOUR_MAPSINDOORS_API_KEY'); // Replace with your MapsIndoors API key
// Create a new instance of the MapsIndoors Mapbox view
const mapViewInstance = new mapsindoors.mapView.MapboxV3View(mapViewOptions);
// Create a new MapsIndoors instance, passing the map view
const mapsIndoorsInstance = new mapsindoors.MapsIndoors({
mapView: mapViewInstance,
// Set the venue ID to load the map for a specific venue
venue: 'YOUR_MAPSINDOORS_VENUE_ID', // Replace with your actual venue ID
});
/** Floor Selector **/
// Create a new HTML div element to host the floor selector
const floorSelectorElement = document.createElement('div');
// Create a new FloorSelector instance, linking it to the HTML element and the main MapsIndoors instance.
new mapsindoors.FloorSelector(floorSelectorElement, mapsIndoorsInstance);
// Get the underlying Mapbox map instance
const mapboxInstance = mapViewInstance.getMap();
// Add the floor selector HTML element to the Mapbox map using Mapbox's addControl method
// We wrap the element in an object implementing the IControl interface expected by addControl
mapboxInstance.addControl({
onAdd: function () {
// This function is called when the control is added to the map.
// It should return the control's DOM element.
return floorSelectorElement;
},
onRemove: function () {
// This function is called when the control is removed from the map.
// Clean up any event listeners or resources here.
floorSelectorElement.parentNode.removeChild(floorSelectorElement);
},
}, 'top-right'); // Optional: Specify a position ('top-left', 'top-right', 'bottom-left', 'bottom-right')
/** Handle Location Clicks **/
// Function to handle clicks on MapsIndoors locations
function handleLocationClick(location) {
if (location && location.id) {
// Move the map to the selected location
mapsIndoorsInstance.goTo(location);
// Ensure that the map shows the correct floor
mapsIndoorsInstance.setFloor(location.properties.floor);
// Select the location on the map
mapsIndoorsInstance.selectLocation(location);
// Show the details UI for the clicked location
showDetails(location);
}
}
// Add an event listener to the MapsIndoors instance for click events on locations
mapsIndoorsInstance.on('click', handleLocationClick);
/** Search Functionality **/
// Get references to the search input and results list elements
const searchInputElement = document.getElementById('search-input');
const searchResultsElement = document.getElementById('search-results');
// Initially hide the search results list
searchResultsElement.classList.add('hidden');
// Add an event listener to the search input for 'input' events
// This calls the onSearch function every time the user types in the input field
searchInputElement.addEventListener('input', onSearch);
// Function to perform the search and update the results list and map highlighting
function onSearch() {
// Get the current value from the search input
const query = searchInputElement.value;
// Get the current venue from the MapsIndoors instance
const currentVenue = mapsIndoorsInstance.getVenue();
// Clear map highlighting
mapsIndoorsInstance.highlight();
// Deselect any selected location
mapsIndoorsInstance.deselectLocation();
// Check if the query is too short (less than 3 characters) or empty
if (query.length < 3) {
// Hide the results list if the query is too short or empty
searchResultsElement.classList.add('hidden');
return; // Stop here
}
// Define search parameters with the current input value
// Include the current venue name in the search parameters
const searchParameters = { q: query, venue: currentVenue ? currentVenue.name : undefined };
// Call the MapsIndoors LocationsService to get locations based on the search query
mapsindoors.services.LocationsService.getLocations(searchParameters).then(locations => {
// Clear previous search results
searchResultsElement.innerHTML = null;
// If no locations are found, display a "No results found" message
if (locations.length === 0) {
const noResultsItem = document.createElement('li');
noResultsItem.textContent = 'No results found';
searchResultsElement.appendChild(noResultsItem);
// Ensure the results list is visible to show the "No results found" message
searchResultsElement.classList.remove('hidden');
return; // Stop here if no results
}
// Append new search results to the list
locations.forEach(location => {
const listElement = document.createElement('li');
// Display the location name
listElement.innerHTML = location.properties.name;
// Store the location ID on the list item for easy access
listElement.dataset.locationId = location.id;
// Add a click event listener to each list item
listElement.addEventListener('click', function () {
// Call the handleLocationClick function when a location in the search results is clicked.
handleLocationClick(location);
});
searchResultsElement.appendChild(listElement);
});
// Show the results list now that it has content
searchResultsElement.classList.remove('hidden');
// Filter map to only display search results by highlighting them
mapsIndoorsInstance.highlight(locations.map(location => location.id));
})
.catch(error => {
console.error("Error fetching locations:", error);
const errorItem = document.createElement('li');
errorItem.textContent = 'Error performing search.';
searchResultsElement.appendChild(errorItem);
searchResultsElement.classList.remove('hidden');
});
}
/** UI state management **/
const searchUIElement = document.getElementById('search-ui');
const detailsUIElement = document.getElementById('details-ui');
const directionsUIElement = document.getElementById('directions-ui');
function showSearchUI() {
hideDetailsUI();
hideDirectionsUI(); // Ensure directions UI is hidden
searchUIElement.classList.remove('hidden');
searchInputElement.focus();
}
function showDetailsUI() {
hideSearchUI();
hideDirectionsUI(); // Ensure directions UI is hidden
detailsUIElement.classList.remove('hidden');
}
function hideSearchUI() {
searchUIElement.classList.add('hidden');
}
function hideDetailsUI() {
detailsUIElement.classList.add('hidden');
}
function showDirectionsUI() {
hideSearchUI();
hideDetailsUI();
directionsUIElement.classList.remove('hidden');
}
function hideDirectionsUI() {
directionsUIElement.classList.add('hidden');
}
/** Location Details **/
// Get references to the static details view elements
const detailsNameElement = document.getElementById('details-name');
const detailsDescriptionElement = document.getElementById('details-description');
const detailsCloseButton = document.getElementById('details-close');
detailsCloseButton.addEventListener('click', () => {
mapsIndoorsInstance.deselectLocation(); // Deselect any selected location
showSearchUI();
});
// Variable to store the location currently shown in details
let currentDetailsLocation = null;
// Show the details of a location
function showDetails(location) {
// Keep track of the currently selected location
currentDetailsLocation = location;
detailsNameElement.textContent = location.properties.name;
detailsDescriptionElement.textContent = location.properties.description || 'No description available.';
showDetailsUI();
}
// Initial call to set up the search UI when the page loads
showSearchUI();
/** Directions Functionality **/
// Handles origin/destination selection, route calculation, and step navigation
const originInputElement = document.getElementById('origin-input');
const originResultsElement = document.getElementById('origin-results');
const destinationInputElement = document.getElementById('destination-input');
const getDirectionsButton = document.getElementById('get-directions');
const prevStepButton = document.getElementById('prev-step');
const nextStepButton = document.getElementById('next-step');
const stepIndicator = document.getElementById('step-indicator');
const directionsCloseButton = document.getElementById('directions-close');
let selectedOrigin = null;
let selectedDestination = null;
let currentRoute = null;
let directionsRenderer = null;
// Reference the details-directions button defined in the HTML
const detailsDirectionsButton = document.getElementById('details-directions');
detailsDirectionsButton.addEventListener('click', () => {
showDirectionsPanel(currentDetailsLocation);
});
detailsCloseButton.addEventListener('click', showSearchUI);
directionsCloseButton.addEventListener('click', () => {
hideDirectionsUI();
showDetailsUI();
if (directionsRenderer) directionsRenderer.setVisible(false);
});
// Show the directions panel and reset state for a new route
function showDirectionsPanel(destinationLocation) {
selectedOrigin = null;
selectedDestination = destinationLocation;
currentRoute = null;
destinationInputElement.value = destinationLocation.properties.name;
originInputElement.value = '';
originResultsElement.innerHTML = '';
hideSearchUI();
hideDetailsUI();
showDirectionsUI();
stepIndicator.textContent = '';
}
// Search for origin locations as the user types
originInputElement.addEventListener('input', onOriginSearch);
function onOriginSearch() {
const query = originInputElement.value;
const currentVenue = mapsIndoorsInstance.getVenue();
originResultsElement.innerHTML = '';
if (query.length < 3) return;
const searchParameters = { q: query, venue: currentVenue ? currentVenue.name : undefined };
mapsindoors.services.LocationsService.getLocations(searchParameters).then(locations => {
if (locations.length === 0) {
const noResultsItem = document.createElement('li');
noResultsItem.textContent = 'No results found';
originResultsElement.appendChild(noResultsItem);
return;
}
locations.forEach(location => {
const listElement = document.createElement('li');
listElement.textContent = location.properties.name;
listElement.addEventListener('click', () => {
selectedOrigin = location;
originInputElement.value = location.properties.name;
originResultsElement.innerHTML = '';
});
originResultsElement.appendChild(listElement);
});
});
}
// Calculate and display the route when both origin and destination are selected
getDirectionsButton.addEventListener('click', async () => {
if (!selectedOrigin || !selectedDestination) {
// Optionally, show an alert or update stepIndicator instead
stepIndicator.textContent = 'Please select both origin and destination.';
return;
}
// Use anchor property for LatLngLiteral (anchor is always a point)
const origin = {
lat: selectedOrigin.properties.anchor.coordinates[1],
lng: selectedOrigin.properties.anchor.coordinates[0],
floor: selectedOrigin.properties.floor
};
const destination = {
lat: selectedDestination.properties.anchor.coordinates[1],
lng: selectedDestination.properties.anchor.coordinates[0],
floor: selectedDestination.properties.floor
};
const directionsService = new mapsindoors.services.DirectionsService();
const route = await directionsService.getRoute({ origin, destination });
currentRoute = route;
if (directionsRenderer) {
directionsRenderer.setVisible(false);
}
directionsRenderer = new mapsindoors.directions.DirectionsRenderer({
mapsIndoors: mapsIndoorsInstance,
fitBounds: true,
strokeColor: '#4285f4',
strokeWeight: 5
});
await directionsRenderer.setRoute(route);
directionsRenderer.setStepIndex(0, 0);
showCurrentStep();
});
// Update the step indicator and enable/disable navigation buttons
function showCurrentStep() {
if (currentRoute?.legs?.length < 1) return;
const currentLegIndex = directionsRenderer.getLegIndex();
const currentStepIndex = directionsRenderer.getStepIndex();
const legs = currentRoute.legs;
const steps = legs[currentLegIndex].steps;
if (steps.length === 0) {
stepIndicator.textContent = '';
return;
}
stepIndicator.textContent = `Leg ${currentLegIndex + 1} of ${legs.length}, Step ${currentStepIndex + 1} of ${steps.length}`;
prevStepButton.disabled = currentLegIndex === 0 && currentStepIndex === 0;
nextStepButton.disabled = currentLegIndex === legs.length - 1 && currentStepIndex === steps.length - 1;
}
// Step navigation event listeners
prevStepButton.addEventListener('click', () => {
if (!directionsRenderer) {
return;
}
directionsRenderer.previousStep();
showCurrentStep();
});
nextStepButton.addEventListener('click', () => {
if (!directionsRenderer) {
return;
}
directionsRenderer.nextStep();
showCurrentStep();
});The map pans to the selected location using mapsIndoorsInstance.goTo(location)
The floor is set to the location's floor with mapsIndoorsInstance.setFloor(location.properties.floor)
The location is visually selected on the map using mapsIndoorsInstance.selectLocation(location)
The location details panel is shown via showDetails(location)
This unified approach ensures a consistent user experience regardless of how locations are selected.
State variables selectedOrigin, selectedDestination, currentRoute, and directionsRenderer are declared to manage the directions workflow state.
The showDirectionsPanel(destinationLocation) function resets the directions state, pre-fills the destination input with the selected location's name, and shows the directions UI.
An origin search is implemented with an input event listener on originInputElement, which calls the onOriginSearch() function. This function uses mapsindoors.services.LocationsService.getLocations() to search for origin locations, similar to the main search functionality.
A new instance of mapsindoors.services.DirectionsService() is created.
The getRoute() method is called with origin and destination parameters to calculate the route.
The route is stored in the currentRoute variable for later reference.
The renderer is configured with the calculated route via directionsRenderer.setRoute(route).
The renderer is set to display the first step of the first leg with directionsRenderer.setStepIndex(0, 0).
The showCurrentStep() function updates the step indicator text and enables/disables the navigation buttons based on the current position in the route.
Event listeners for the navigation buttons call directionsRenderer.previousStep() and directionsRenderer.nextStep() respectively, followed by showCurrentStep() to update the UI.
All three UI states (search, details, directions) are mutually exclusive, providing a clear and focused user interface.
A button to get directions.
Step navigation controls (step indicator, previous/next buttons).
A close button for the directions panel.
All panels (#search-ui, #details-ui, #directions-ui) use consistent class and ID naming, and only one is visible at a time, managed by the show/hide functions in the JavaScript.
.directions-step-nav and #step-indicator style the step navigation controls.
New functions showDirectionsUI() and hideDirectionsUI() are introduced to manage the visibility of the directions panel, ensuring it's mutually exclusive with the search and details panels.
The "Get Directions" button (detailsDirectionsButton) in the details panel now has an event listener that calls showDirectionsPanel(), passing the currentDetailsLocation to pre-fill the destination.
The directionsCloseButton listener is added to hide the directions UI, show the details UI, and make the directionsRenderer invisible when directions are closed, providing a clear exit path.
Location Click Handling:
The script maintains the unified click handler pattern established in previous steps. The handleLocationClick function continues to serve as the central entry point for user interactions with locations, responding to both map POI clicks and search result clicks.
This consistent pattern ensures that when a user clicks on a location (either on the map or in search results), the following actions always occur:
The map pans to the selected location using mapsIndoorsInstance.goTo(location)
The floor is set to the location's floor with mapsIndoorsInstance.setFloor(location.properties.floor)
The location is visually selected on the map using mapsIndoorsInstance.selectLocation(location)
The location details panel is shown via showDetails(location)
This unified approach ensures a consistent user experience regardless of how locations are selected.
Directions Panel and Origin/Destination Selection:
New DOM element references are established for directions-related UI elements: originInputElement, originResultsElement, destinationInputElement, getDirectionsButton, prevStepButton, nextStepButton, stepIndicator, and directionsCloseButton.
State variables selectedOrigin, selectedDestination, currentRoute, and directionsRenderer are declared to manage the directions workflow state.
The showDirectionsPanel(destinationLocation) function resets the directions state, pre-fills the destination input with the selected location's name, and shows the directions UI.
An origin search is implemented with an input event listener on originInputElement, which calls the onOriginSearch() function. This function uses mapsindoors.services.LocationsService.getLocations() to search for origin locations, similar to the main search functionality.
Route Calculation:
When the user clicks the "Show Route" button (getDirectionsButton), the script checks if both selectedOrigin and selectedDestination are set.
It extracts coordinates and floor information from the location objects' properties.anchor and properties.floor.
A new instance of mapsindoors.services.DirectionsService() is created.
The getRoute() method is called with origin and destination parameters to calculate the route.
The route is stored in the currentRoute variable for later reference.
Route Display and Step Navigation:
Any existing directionsRenderer is hidden before creating a new one.
A new mapsindoors.directions.DirectionsRenderer is instantiated with the mapsIndoorsInstance, fitBounds: true for automatic map adjustment, and styling options.
The renderer is configured with the calculated route via directionsRenderer.setRoute(route).
The renderer is set to display the first step of the first leg with directionsRenderer.setStepIndex(0, 0).
The showCurrentStep() function updates the step indicator text and enables/disables the navigation buttons based on the current position in the route.
Event listeners for the navigation buttons call directionsRenderer.previousStep() and directionsRenderer.nextStep() respectively, followed by showCurrentStep() to update the UI.
Workflow Integration:
The script carefully integrates the directions functionality with the existing search and details features.
The details panel now includes a "Get Directions" button that transitions to the directions workflow.
The directions panel allows users to return to the details view via the close button.
All three UI states (search, details, directions) are mutually exclusive, providing a clear and focused user interface.
The map updates to show the full route, and the step navigation controls become active.
Users can use the "Previous" and "Next" buttons to step through the route, with the map highlighting the current step and the step-indicator updating accordingly.
Only one panel (search, details, or directions) is visible at a time.
Clicking the "Close Directions" button returns to the details panel and hides the route from the map.
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MapsIndoors</title>
<link rel="stylesheet" href="style.css">
<!-- Google Maps JavaScript API -->
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_GOOGLE_MAPS_API_KEY"></script>
<script src="https://app.mapsindoors.com/mapsindoors/js/sdk/4.41.0/mapsindoors-4.41.0.js.gz"
xintegrity="sha384-3lk3cwVPj5MpUyo5T605mB0PMHLLisIhNrSREQsQHjD9EXkHBjz9ETgopmTbfMDc"
crossorigin="anonymous"></script>
</head>
<body>
<div id="map"></div>
<div class="panel">
<div id="search-ui" class="flex-column">
<input type="text" id="search-input" placeholder="Search for a location...">
<ul id="search-results"></ul>
</div>
<div id="details-ui" class="hidden">
<h3 id="details-name"></h3>
<p id="details-description"></p>
<button id="details-directions" class="details-button details-action-button">Get Directions</button>
<button id="details-close" class="details-button">Close</button>
</div>
<div id="directions-ui" class="hidden flex-column">
<h3>Directions</h3>
<div class="directions-inputs">
<input type="text" id="origin-input" placeholder="Choose origin...">
<ul id="origin-results" class="directions-results-list"></ul>
<input type="text" id="destination-input" placeholder="Choose destination..." disabled>
</div>
<button id="get-directions" class="details-button details-action-button">Show Route</button>
<div class="directions-step-nav">
<span id="step-indicator"></span>
<button id="prev-step" class="details-button">Previous</button>
<button id="next-step" class="details-button">Next</button>
</div>
<button id="directions-close" class="details-button">Close Directions</button>
</div>
</div>
<script src="script.js"></script>
</body>
</html>/* style.css */
/* Use flexbox for the main layout */
html, body {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden; /* Prevent scrollbars if map is full size */
display: flex;
flex-direction: column; /* Stack children vertically if needed later */
}
/* Style for the map container */
#map {
/* Make map fill available space */
flex-grow: 1;
width: 100%; /* Make map fill width */
margin: 0;
padding: 0;
}
/* Style for the information panel container */
.panel {
position: absolute; /* Position over the map */
top: 10px; /* Distance from the top */
left: 10px; /* Distance from the left */
z-index: 10; /* Ensure it's above the map and other elements */
background-color: white; /* White background for readability */
padding: 10px;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.2); /* Add a subtle shadow */
max-height: 80%; /* Limits max-height to prevent overflow with details */
overflow-y: auto; /* Add scroll if content exceeds max-height */
border: 1px solid #ccc; /* Add border for clarity */
width: 300px;
}
/* Class to apply flex display and column direction */
.flex-column {
display: flex;
flex-direction: column;
gap: 10px; /* Space between elements */
}
/* Class to hide elements */
.hidden {
display: none;
}
/* Style for the search input field */
#search-input {
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 1rem;
}
/* Style for the search results list */
#search-results {
list-style: none; /* Remove default list bullets */
padding: 0;
margin: 0;
}
/* Style for individual search result items */
#search-results li {
padding: 8px 0;
cursor: pointer; /* Indicate clickable items */
border-bottom: 1px solid #eee; /* Separator line */
}
/* Style for the last search result item (no bottom border) */
#search-results li:last-child {
border-bottom: none;
}
/* Hover effect for search result items */
#search-results li:hover {
background-color: #f0f0f0; /* Highlight on hover */
}
/* --- New Styles for Location Details UI elements --- */
/* Styles for the new UI wrappers within #search-container */
#search-ui,
#details-ui {
width: 100%; /* Ensure they fill the container's width */
/* display: flex and flex-direction are controlled by .flex-column-container class via JS */
}
/* Style for the location name in details */
#details-name {
margin-top: 0;
margin-bottom: 10px;
font-size: 1.2rem;
border-bottom: 1px solid #eee;
padding-bottom: 5px;
}
/* Style for the location description in details */
#details-description {
margin-bottom: 15px;
font-size: 0.9rem;
color: #555;
}
/* Style for general buttons within details */
.details-button {
padding: 8px;
border: none;
border-radius: 4px;
font-size: 0.9rem;
cursor: pointer;
transition: background-color 0.3s ease;
}
/* Specific style for the Close button */
#details-close {
background-color: #ccc; /* Grey */
color: #333;
}
#details-close:hover {
background-color: #bbb;
}
/* Directions panel specific */
#directions-ui {
width: 100%;
}
.directions-inputs {
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 8px;
}
.directions-results-list {
list-style: none;
padding: 0;
margin: 0;
max-height: 120px;
overflow-y: auto;
}
.directions-results-list li {
padding: 8px 0;
cursor: pointer;
border-bottom: 1px solid #eee;
}
.directions-results-list li:last-child {
border-bottom: none;
}
.directions-results-list li:hover {
background-color: #f0f0f0;
}
.directions-step-nav {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
#step-indicator {
grid-column: span 2;
}// script.js
// Define options for the MapsIndoors Google Maps view
const mapViewOptions = {
element: document.getElementById('map'),
// Initial map center (MapsPeople - Austin Office example)
center: { lng: -97.74204591828197, lat: 30.36022358949809 },
// Initial zoom level
zoom: 17,
// Maximum zoom level
maxZoom: 22
};
// Set the MapsIndoors API key
mapsindoors.MapsIndoors.setMapsIndoorsApiKey('YOUR_MAPSINDOORS_API_KEY'); // Replace with your MapsIndoors API key
// Create a new instance of the MapsIndoors Google Maps view
const mapViewInstance = new mapsindoors.mapView.GoogleMapsView(mapViewOptions);
// Create a new MapsIndoors instance, passing the map view
const mapsIndoorsInstance = new mapsindoors.MapsIndoors({
mapView: mapViewInstance,
// Set the venue ID to load the map for a specific venue
venue: 'YOUR_MAPSINDOORS_VENUE_ID', // Replace with your actual venue ID
});
/** Floor Selector **/
// Create a new HTML div element to host the floor selector
const floorSelectorElement = document.createElement('div');
// Create a new FloorSelector instance, linking it to the HTML element and the main MapsIndoors instance.
new mapsindoors.FloorSelector(floorSelectorElement, mapsIndoorsInstance);
// Get the underlying Google Maps instance
const googleMapInstance = mapViewInstance.getMap();
// Add the floor selector HTML element to the Google Maps controls.
googleMapInstance.controls[google.maps.ControlPosition.TOP_RIGHT].push(floorSelectorElement);
/** Handle Location Clicks **/
// Function to handle clicks on MapsIndoors locations
function handleLocationClick(location) {
if (location && location.id) {
// Move the map to the selected location
mapsIndoorsInstance.goTo(location);
// Ensure that the map shows the correct floor
mapsIndoorsInstance.setFloor(location.properties.floor);
// Select the location on the map
mapsIndoorsInstance.selectLocation(location);
// Show the details UI for the clicked location
showDetails(location);
}
}
// Add an event listener to the MapsIndoors instance for click events on locations
mapsIndoorsInstance.on('click', handleLocationClick);
/** Search Functionality **/
// Get references to the search input and results list elements
const searchInputElement = document.getElementById('search-input');
const searchResultsElement = document.getElementById('search-results');
// Initially hide the search results list
searchResultsElement.classList.add('hidden');
// Add an event listener to the search input for 'input' events
// This calls the onSearch function every time the user types in the input field
searchInputElement.addEventListener('input', onSearch);
// Function to perform the search and update the results list and map highlighting
function onSearch() {
// Get the current value from the search input
const query = searchInputElement.value;
// Get the current venue from the MapsIndoors instance
const currentVenue = mapsIndoorsInstance.getVenue();
// Clear map highlighting
mapsIndoorsInstance.highlight();
// Deselect any selected location
mapsIndoorsInstance.deselectLocation();
// Check if the query is too short (less than 3 characters) or empty
if (query.length < 3) {
// Hide the results list if the query is too short or empty
searchResultsElement.classList.add('hidden');
return; // Stop here
}
// Define search parameters with the current input value
// Include the current venue name in the search parameters
const searchParameters = { q: query, venue: currentVenue ? currentVenue.name : undefined };
// Call the MapsIndoors LocationsService to get locations based on the search query
mapsindoors.services.LocationsService.getLocations(searchParameters).then(locations => {
// Clear previous search results
searchResultsElement.innerHTML = null;
// If no locations are found, display a "No results found" message
if (locations.length === 0) {
const noResultsItem = document.createElement('li');
noResultsItem.textContent = 'No results found';
searchResultsElement.appendChild(noResultsItem);
// Ensure the results list is visible to show the "No results found" message
searchResultsElement.classList.remove('hidden');
return; // Stop here if no results
}
// Append new search results to the list
locations.forEach(location => {
const listElement = document.createElement('li');
// Display the location name
listElement.innerHTML = location.properties.name;
// Store the location ID on the list item for easy access
listElement.dataset.locationId = location.id;
// Add a click event listener to each list item
listElement.addEventListener('click', function () {
// Call the handleLocationClick function when a location in the search results is clicked.
handleLocationClick(location);
});
searchResultsElement.appendChild(listElement);
});
// Show the results list now that it has content
searchResultsElement.classList.remove('hidden');
// Filter map to only display search results by highlighting them
mapsIndoorsInstance.highlight(locations.map(location => location.id));
})
.catch(error => {
console.error("Error fetching locations:", error);
const errorItem = document.createElement('li');
errorItem.textContent = 'Error performing search.';
searchResultsElement.appendChild(errorItem);
searchResultsElement.classList.remove('hidden');
});
}
/** UI state management **/
const searchUIElement = document.getElementById('search-ui');
const detailsUIElement = document.getElementById('details-ui');
const directionsUIElement = document.getElementById('directions-ui');
function showSearchUI() {
hideDetailsUI();
hideDirectionsUI(); // Ensure directions UI is hidden
searchUIElement.classList.remove('hidden');
searchInputElement.focus();
}
function showDetailsUI() {
hideSearchUI();
hideDirectionsUI(); // Ensure directions UI is hidden
detailsUIElement.classList.remove('hidden');
}
function hideSearchUI() {
searchUIElement.classList.add('hidden');
}
function hideDetailsUI() {
detailsUIElement.classList.add('hidden');
}
function showDirectionsUI() {
hideSearchUI();
hideDetailsUI();
directionsUIElement.classList.remove('hidden');
}
function hideDirectionsUI() {
directionsUIElement.classList.add('hidden');
}
/** Location Details **/
// Get references to the static details view elements
const detailsNameElement = document.getElementById('details-name');
const detailsDescriptionElement = document.getElementById('details-description');
const detailsCloseButton = document.getElementById('details-close');
detailsCloseButton.addEventListener('click', () => {
mapsIndoorsInstance.deselectLocation(); // Deselect any selected location
showSearchUI();
});
// Variable to store the location currently shown in details
let currentDetailsLocation = null;
// Show the details of a location
function showDetails(location) {
// Keep track of the currently selected location
currentDetailsLocation = location;
detailsNameElement.textContent = location.properties.name;
detailsDescriptionElement.textContent = location.properties.description || 'No description available.';
showDetailsUI();
}
// Initial call to set up the search UI when the page loads
showSearchUI();
/** Directions Functionality **/
// Handles origin/destination selection, route calculation, and step navigation
const originInputElement = document.getElementById('origin-input');
const originResultsElement = document.getElementById('origin-results');
const destinationInputElement = document.getElementById('destination-input');
const getDirectionsButton = document.getElementById('get-directions');
const prevStepButton = document.getElementById('prev-step');
const nextStepButton = document.getElementById('next-step');
const stepIndicator = document.getElementById('step-indicator');
const directionsCloseButton = document.getElementById('directions-close');
let selectedOrigin = null;
let selectedDestination = null;
let currentRoute = null;
let directionsRenderer = null;
// Reference the details-directions button defined in the HTML
const detailsDirectionsButton = document.getElementById('details-directions');
detailsDirectionsButton.addEventListener('click', () => {
showDirectionsPanel(currentDetailsLocation);
});
detailsCloseButton.addEventListener('click', showSearchUI);
directionsCloseButton.addEventListener('click', () => {
hideDirectionsUI();
showDetailsUI();
if (directionsRenderer) directionsRenderer.setVisible(false);
});
// Show the directions panel and reset state for a new route
function showDirectionsPanel(destinationLocation) {
selectedOrigin = null;
selectedDestination = destinationLocation;
currentRoute = null;
destinationInputElement.value = destinationLocation.properties.name;
originInputElement.value = '';
originResultsElement.innerHTML = '';
hideSearchUI();
hideDetailsUI();
showDirectionsUI();
stepIndicator.textContent = '';
}
// Search for origin locations as the user types
originInputElement.addEventListener('input', onOriginSearch);
function onOriginSearch() {
const query = originInputElement.value;
const currentVenue = mapsIndoorsInstance.getVenue();
originResultsElement.innerHTML = '';
if (query.length < 3) return;
const searchParameters = { q: query, venue: currentVenue ? currentVenue.name : undefined };
mapsindoors.services.LocationsService.getLocations(searchParameters).then(locations => {
if (locations.length === 0) {
const noResultsItem = document.createElement('li');
noResultsItem.textContent = 'No results found';
originResultsElement.appendChild(noResultsItem);
return;
}
locations.forEach(location => {
const listElement = document.createElement('li');
listElement.textContent = location.properties.name;
listElement.addEventListener('click', () => {
selectedOrigin = location;
originInputElement.value = location.properties.name;
originResultsElement.innerHTML = '';
});
originResultsElement.appendChild(listElement);
});
});
}
// Calculate and display the route when both origin and destination are selected
getDirectionsButton.addEventListener('click', async () => {
if (!selectedOrigin || !selectedDestination) {
// Optionally, show an alert or update stepIndicator instead
stepIndicator.textContent = 'Please select both origin and destination.';
return;
}
// Use anchor property for LatLngLiteral (anchor is always a point)
const origin = {
lat: selectedOrigin.properties.anchor.coordinates[1],
lng: selectedOrigin.properties.anchor.coordinates[0],
floor: selectedOrigin.properties.floor
};
const destination = {
lat: selectedDestination.properties.anchor.coordinates[1],
lng: selectedDestination.properties.anchor.coordinates[0],
floor: selectedDestination.properties.floor
};
const directionsService = new mapsindoors.services.DirectionsService();
const route = await directionsService.getRoute({ origin, destination });
currentRoute = route;
if (directionsRenderer) {
directionsRenderer.setVisible(false);
}
directionsRenderer = new mapsindoors.directions.DirectionsRenderer({
mapsIndoors: mapsIndoorsInstance,
fitBounds: true,
strokeColor: '#4285f4',
strokeWeight: 5
});
await directionsRenderer.setRoute(route);
directionsRenderer.setStepIndex(0, 0);
showCurrentStep();
});
// Update the step indicator and enable/disable navigation buttons
function showCurrentStep() {
if (currentRoute?.legs?.length < 1) return;
const currentLegIndex = directionsRenderer.getLegIndex();
const currentStepIndex = directionsRenderer.getStepIndex();
const legs = currentRoute.legs;
const steps = legs[currentLegIndex].steps;
if (steps.length === 0) {
stepIndicator.textContent = '';
return;
}
stepIndicator.textContent = `Leg ${currentLegIndex + 1} of ${legs.length}, Step ${currentStepIndex + 1} of ${steps.length}`;
prevStepButton.disabled = currentLegIndex === 0 && currentStepIndex === 0;
nextStepButton.disabled = currentLegIndex === legs.length - 1 && currentStepIndex === steps.length - 1;
}
// Step navigation event listeners
prevStepButton.addEventListener('click', () => {
if (!directionsRenderer) {
return;
}
directionsRenderer.previousStep();
showCurrentStep();
});
nextStepButton.addEventListener('click', () => {
if (!directionsRenderer) {
return;
}
directionsRenderer.nextStep();
showCurrentStep();
});The Android SDK for MapsIndoors has been upgraded from V3 to V4, which comes with improved interfaces and flexibility for developing your own map experience. The MapsIndoors SDK now supports Mapbox as a map provider, alongside some reworked and refactored features that simplify development and SDK behavior. This guide will cover specific changes to the SDK and how to use it to provide you with a guide on how to upgrade from V3 to V4.
With the release of V4 the MapsIndoors SDK is released as two separate libraries depending on the map provider - Google Maps or Mapbox. You can get them through Maven by changing your dependency to get:
MapsIndoors is a singleton class, which can be described as the data layer of the SDK. Below you will find an example that demonstrates how initialization has been simplified between V3 and V4.
V3
In V3, SDK initialization is started with:
And subsequently setting the Google API key using:
If you want to change the MapsIndoors API key of an already initialized SDK you invoke:
And to close down the SDK, call:
V4
In V4, initialization is started by the new function MapsIndoors.load():
Map engine specific API keys are handled by MPMapConfig, covered in the "MapControl Initialization" section of this guide.
Switching to another MapsIndoors API key, such as for switching active solutions, is now done by invoking MapsIndoors.load() again with a new key. The SDK will close down, and reload with the new API key.
To close down the SDK without reloading a new API key, invoke:
MapControl instantiation and initialization are separate concepts. You create a new instance of MapControl and configure it with a map and view - optionally you could set clustering, overlapping and other behavior on the object.
V3
In V3, MapControl.init() is a separate asynchronous call:
V4
In V4, MapControl now requires a MPMapConfig object, which is acquired using a builder on the class MPMapConfig. Here you must provide an activity, a map provider (Google Maps or Mapbox), a mapview and a map engine API key.
With a MPMapConfig instance, you may create a new MapControl instance. This now happens through a factory pattern. This both instantiates and initializes your MapControl object asynchronously. If everything succeeds, you will receive a ready-to-use MapControl instance - if not, you will get an error and receive no MapControl instance.
Please note that this factory method will wait to return until a valid MapsIndoors solution is loaded, therefore it is safe to invoke MapControl.create() prior to, or in parallel with MapsIndoors.load().
V3
In V3, AppConfig contained information about clustering (POI_GROUPING) and collisions (POI_HIDE_ON_OVERLAP), which could be fetched and updated like this:
V4
In V4, these settings have been moved to MPSolutionConfig, which is located on the MPSolution. Now these settings have types (a boolean and an Enum type). This helps ensure that the settings are easier to configure and have no parsing errors. They can be fetched and updates like this:
NB: As a consequence the SDK will no longer respect these settings in the appConfig, they will have to be set in the solutionConfig.
V3
In V3, the getName() method return the venue's Administrative ID, shadowing its Display Name.
V4
In V4, the getName() method now returns the venue's Display Name. A new method has been added: getAdministrativeId() which returns the venue's Administrative ID.
The manner in which the SDK handles Display Rules has recieved a major overhaul in V4. This is intended to simplify usage, such as editing Display Rules for certain Locations.
V3
In V3 you would create new DisplayRule objects and add them onto Locations through MapControl.
Editing a single location
Editing multiple locations
V4
In V4, DisplayRules have been changed to a reference-based approach. You now receive MPDisplayRules through MapsIndoors and are able to change the values, and see it reflected on the map instantly.
Editing a single DisplayRule
Editing multiple DisplayRules
Resetting Display Rules
Building outlines and selections are now also DisplayRules, so that you can customize the looks just like you can when doing it on locations.
Please note that MapsIndoors has to have finished loading for these DisplayRules to not be
null.
Editing Selection and Building Outline
The following methods are examples of how you can use DisplayRules to set the outline color of a building, or if selecting a building highlights it.
There are two basic functions here - Retrieving, or querying a route, and rendering it onto the map.
V3
In V3, the process to query a route is to instantiate a MPRoutingProvider and set the desired travel mode, departure/arrival time, etc. You should also instantiate an OnRouteResultListener to receive the result (or error in case of failure).
V4
In V4, MPRoutingProvider has been renamed to MPDirectionsService, to align with other platforms. It has also changed the method of setting a departure or arrival, as shown below.
Instantiate a new MPDirectionsService, and apply the settings needed for a route. Use the query() method to search for a route between two points.
V3
To render a given route in V3, instantiate a MPDirectionsRenderer with parameters. Then your IDE should be able to show you the various configurable attributes (various animation settings and styling) as well as setting the route. Alternatively, refer to further documentation. To start the renderer/animation, invoke initMap().
V4
In V4, this has been simplified. Given a route, you can instantiate a new MPDirectionsRenderer, and set the route using setRoute(). Use the MPDirectionsRenderer object to navigate through the route (next/previous leg) as well as configure the animation and styling of the route on the map. By default the route is animated and repeating, but this is customizable on the MPDirectionsRenderer instance.
In V3, there were many overloaded methods for selection and filtering, where various boolean and integer/double values were set. In V4, the preferred method is configuration objects for heavily configurable use cases. Thus, filtering and selection methods are now dependent on MP...Behavior objects.
We have introduced MPFilterBehavior and MPSelectionBehavior. These object contains behavioral configuration to describe how and if the camera should behave. The following can be configured:
setZoomToFit(boolean)
setMoveCamera(boolean)
setShowInfoWindow(boolean)
There are statically defined defaults available on the classes.
In V4 MapControl.goTo(MPEntity) is introduced. This is an easy way to quickly move the camera to almost any MapsIndoors geographical object (referred to as MPEntity). The method implements pre-determined defaults for camera behavior, which cannot be configured.
The following classes are of type MPEntity:
MPLocation
MPFloor
MPBuilding
V3
In V3, filtering map content is performed with MapControl.displaySearchResult(). This results in a lot of undesirable overloads.
Clearing the map filter is done by invoking MapControl.clearMap().
V4
To avoid the aforementioned undesirable overloads, in V4, filtering map content is now performed with MapControl.setFilter(List<MPLocation>, MPFilterBehavior) or alternatively MapControl.setFilter(MPFilter, MPFilterBehavior, MPSuccessListener). To clear the filter, invoke MapControl.clearFilter().
One way to perform map filtering, is given a list of MPLocation, display only these locations on the map.
Another way is to configure a MPFilter object. This is an easy way to only show locations of a given type or category on the map.
V3
In V3, the snippet below is the PositionProvider interface. While perfectly functional, it leaves a lot be desired in terms of readability and clarity, and avoiding bloat in the code.
V4
To fix this in V4, PositionProvider has been optimized and renamed to MPPositionProvider, to fall in line with other naming conventions. It has been renamed with the MP-prefix and has been heavily trimmed, to only describe the necessary interface for the MapsIndoors SDK to utilize a position provider sufficiently.
implementation 'com.mapspeople.mapsindoors:googlemaps:4.2.5'
implementation 'com.mapspeople.mapsindoors:mapbox:4.2.5'setAnimationDuration(int)
setAllowFloorChange(boolean)
MPVenueLintTestClass
ListenerCallbacks
LocationsUpdatedListener
MapView (interface)
MathUtil
MPAuthClient
MPBadgeType
MPBookingListener
MPBookingListListener
MPDataSetCacheManagerSyncListener
MPDistanceMatrixReceiver
MPFloatRange
MPLocationCluster
MPLocationClusteringEngine
MPLocationListListener
MPOrdering
MultiLineStringGeometry
MultiPointGeometry
NodeLabel
PolyUtil
PositionIndicator
Renderer
RouteVertex
TileCacheStrategy
TileSize
UriLoaderListener
Utils
DSCUnzipFileTask
DSCUrlDownloadingTask
MPCategoryCollection
DataField
MPDataField
DataSet
MPDataSet
DataSetManagerStatus
MPDataSetManagerStatus
dbglog
MPDebugLog
DistanceMatrixResponse
MPDistanceMatrixResponse
FastSphericalUtils
MPFastSphericalUtils
Floor
MPFloor
FloorSelectorInterface
MPFloorSelectorInterface
GeocodedWaypoints
MPGeocodedWaypoints
GeoData
MPGeoData
Geometry
MPGeometry
Highway
MPHighway
IFloorSelector
MPFloorSelectorInterface
ImageProvider
MPImageProvider
LocationDisplayRule
MPDisplayRule
LocationPropertyNames
MPLocationPropertyNames
Maneuver
MPManeuver
MapExtend
MPMapExtend
MapStyle
MPMapStyle
MenuInfo
MPMenuInfo
MPApiKeyValidatorService
MPApiKeyValidator
MPBaseType
MPLocationBaseType
MPLocationClusterImageAdapter
MPClusterIconAdapter
MPRoutingProvider
MPDirectionsService
MultiPolygonGeometry
MPMultiPolygonGeometry
NodeData
MPNodeData
Object
MPObject
PermissionsAndPSListener
MPPermissionsAndPSListener
Point
MPPoint
POIType
MPPOIType
PolygonDisplayRule
MPPolygonDisplayRule
PolygonGeometry
MPPolygonGeometry
PositionProvider
MPPositionProvider
PositionResult
MPPositionResult & MPPositionResultInterface
PropertyData
MPPropertyData
ReadyListener
MPReadyListener
Route
MPRoute
RouteCoordinate
MPRouteCoordinate
RouteLeg
MPRouteLeg
RoutePolyline
MPRoutePolyline
RouteProperty
MPRouteProperty
RouteResult
MPRouteResult
RouteSegmentPath
MPRouteSegmentPath
RouteStep
MPRouteStep
RoutingProvider
MPDirectionsServiceInterface & MPDirectionsServiceExternalInterface
Solution
MPSolution
SolutionInfo
MPSolutionInfo
TransitDetails
MPTransitDetails
TravelMode
MPTravelMOde
URITemplate
MPURITemplate
UrlResourceGroupType
MPUrlResourceGroupType
UserRole
MPUserRole
Venue
MPVenue
VenueCollection
MPVenueCollection
VenueInfo
MPVenueInfo
ImageSize
SphericalUtil
Convert
DirectionsRenderer (interface)
DisplayRule
Feature
FloorTileOfflineManager
GeometryCollectionGeometry
GoogleMapsDirectionStatusCodes
JavaClusteringEngine
JSONUtil
AppConfig
MPAppConfig
BadgePosition
MPBadgePosition
Building
MPBuilding
BuildingCollection
MPBuildingCollection
BuildingInfo
MPBuildingInfo
Category
MPCategory
LineStringGeometry
CategoryCollection
MapsIndoors.initialize(getApplicationContext(), "mapsindoors-key", listener);MapsIndoors.setGoogleAPIKey(getString(R.string.google_maps_key));MapsIndoors.setApiKey("new key")MapsIndoors.onApplicationTerminate()MapsIndoors.load(getApplicationContext(), "mapsindoors-key", listener);MapsIndoors.destroy()mMapControl = new MapControl(this);
mMapControl.setGoogleMap(mMap, view);
mMapControl.init(miError -> {
// MapControl init complete
});MPMapConfig mapConfig = new MPMapConfig.Builder(activity, googleMap, "google-api-key", view, true)
.setShowFloorSelector(true)
.build();MapControl.create(mapConfig, (mapControl, miError) -> {
// MapControl init complete
});// get whether collisions are enabled... as a string
MapsIndoors.getAppConfig().getAppSettings().get(AppConfig.APP_SETTING_POI_HIDE_ON_OVERLAP);
// set whether clustering is enabled... with a string
MapsIndoors.getAppConfig().getAppSettings().put(AppConfig.APP_SETTING_POI_GROUPING, "true");// get the config from the solution
MPSolutionConfig config = MapsIndoors.getSolution().getConfig();
// get the collisionHandling enum from the config
MPCollisionHandling collisionHandling = config.getCollisionHandling();
// update the config
config.setEnableClustering(true);
config.setCollisionHandling(MPCollisionHandling.ALLOW_OVERLAP);LocationDisplayRule singleLocationDisplayRule = new LocationDisplayRule.Builder("singleRule").setVectorDrawableIcon(R.drawable.ic_baseline_air_24).setLabel("single display rule").build();
MPLocation mpLocation = MapsIndoors.getLocationById("MyLocationId");
mMapControl.setDisplayRule(singleLocationDisplayRule, mpLocation);multipleLocationDisplayRule = new LocationDisplayRule.Builder("multipleRule").setVectorDrawableIcon(R.drawable.ic_baseline_air_24).setLabel("multiple display rule").build();
MapsIndoors.getLocationsAsync(null, new MPFilter.Builder().setTypes(Collections.singletonList("Meetingroom")).build(), (locations, miError) -> {
if (locations != null) {
mMapControl.setDisplayRule(multipleLocationDisplayRule, locations);
}
});MPLocation mpLocation = MapsIndoors.getLocationById("MyLocationId");
MPDisplayRule mpDisplayRule = MapsIndoors.getDisplayRule(mpLocation);
if (mpDisplayRule != null) {
mpDisplayRule.setIcon(R.drawable.ic_baseline_air_24, Color.GRAY);
}MapsIndoors.getLocationsAsync(null, new MPFilter.Builder().setTypes(Collections.singletonList("Meetingroom")).build(), (locations, error) -> {
if (locations != null) {
MPDisplayRuleOptions displayRuleOptions = new MPDisplayRuleOptions().setIcon(R.drawable.ic_baseline_chair_24)
.setPolygonStrokeColor(Color.BLUE)
.setPolygonVisible(true)
.setLabel("Meeting Room");
for (MPLocation location : locations) {
MPDisplayRule displayRule = MapsIndoors.getDisplayRule(location);
if (displayRule != null) {
displayRule.applyOptions(displayRuleOptions);
}
}
}
});MapsIndoors.getLocationsAsync(null, new MPFilter.Builder().setTypes(Collections.singletonList("Meetingroom")).build(), (locations, error) -> {
if (locations != null) {
for (MPLocation location : locations) {
MPDisplayRule displayRule = MapsIndoors.getDisplayRule(location);
if (displayRule != null) {
displayRule.reset();
}
}
}
});MapsIndoors.getDisplayRule(MPSolutionDisplayRule.BUILDING_OUTLINE).setPolygonStrokeColor(Color.BLUE);
MapsIndoors.getDisplayRule(MPSolutionDisplayRule.SELECTION_HIGHLIGHT).setPolygonVisible(false);int timeNowSeconds = (int) (System.currentTimeMillis() / 1000);
MPRoutingProvider routingProvider = new MPRoutingProvider();
routingProvider.setTravelMode(TravelMode.WALKING);
routingProvider.setDateTime(timeNowSeconds, true);
routingProvider.setOnRouteResultListener((route, error) -> {
// You get your route (or error) here!
});
Point from = new Point(57.039395177203936, 9.939182484455051);
Point to = new Point(57.03238690202058, 9.93220061362637);
routingProvider.query(from, to);Date date = new Date();
MPDirectionsService directionsService = new MPDirectionsService();
directionsService.setIsDeparture(true);
directionsService.setTime(date);
directionsService.setTravelMode(TravelMode.WALKING);
directionsService.setOnRouteResultListener((route, error) -> {
// You get your route (or error) here!
})
MPPoint from = new MPPoint(57.039395177203936, 9.939182484455051);
MPPoint to = new MPPoint(57.03238690202058, 9.93220061362637);
directionsService.query(from, to);MPDirectionsRenderer directionsRenderer = new MPDirectionsRenderer(this, mMap, mMapControl, null);
directionsRenderer.setPolylineAnimated(true);
directionsRenderer.setAnimated(true);
directionsRenderer.setRoute(route);
runOnUiThread( ()-> {
directionsRenderer.initMap(true);
directionsRenderer.setRouteLegIndex(0);
});MPDirectionsRenderer renderer = new MPDirectionsRenderer(mMapControl);
renderer.setRoute(route);boolean displaySearchResults(@NonNull List<MPLocation> locations)
boolean displaySearchResults(@NonNull List<MPLocation> locations, boolean animateCamera)
boolean displaySearchResults(@NonNull List<MPLocation> locations, @Nullable ReadyListener readyListener)
boolean displaySearchResults(@NonNull List<MPLocation> locations, boolean animateCamera, int cameraPadding)
boolean displaySearchResults(@NonNull List<MPLocation> locations, boolean animateCamera, int cameraPadding, boolean showInfoWindow)
boolean displaySearchResults(@NonNull List<MPLocation> locations, boolean animateCamera, int cameraPadding, @Nullable ReadyListener readyListener)
boolean displaySearchResults(@NonNull List<MPLocation> locations, boolean animateCamera, int cameraPadding, boolean showInfoWindow, @Nullable CameraUpdate googleMapCameraUpdate, int durationMs, GoogleMap.CancelableCallback googleMapCancelableCallback)
boolean displaySearchResults(@NonNull List<MPLocation> locations, boolean animateCamera, int cameraPadding, boolean showInfoWindow, @Nullable CameraUpdate googleMapCameraUpdate, int durationMs, GoogleMap.CancelableCallback googleMapCancelableCallback, @Nullable ReadyListener readyListener)MapsIndoors.getLocationsAsync(new MPQuery.Builder().setQuery("stairs").build(), null, (locations, error) -> {
if(error == null && !locations.isEmpty()) {
mMapControl.setFilter(locations, MPFilterBehavior.DEFAULT);
}
});MPFilter filter = new MPFilter.Builder().setTypes(Collections.singletonList("Stairs")).build();
mMapControl.setFilter(filter, MPFilterBehavior.DEFAULT, null);public interface PositionProvider {
@NonNull String[] getRequiredPermissions();
boolean isPSEnabled();
void startPositioning( @Nullable String arg );
void stopPositioning( @Nullable String arg );
boolean isRunning();
void addOnPositionUpdateListener( @Nullable OnPositionUpdateListener listener );
void removeOnPositionUpdateListener( @Nullable OnPositionUpdateListener listener );
void setProviderId( @Nullable String id );
void addOnStateChangedListener( @Nullable OnStateChangedListener onStateChangedListener );
void removeOnStateChangedListener( @Nullable OnStateChangedListener onStateChangedListener );
void checkPermissionsAndPSEnabled( @Nullable PermissionsAndPSListener permissionAPSlist );
@Nullable String getProviderId();
@Nullable PositionResult getLatestPosition();
void startPositioningAfter( @IntRange(from = 0, to = Integer.MAX_VALUE) int delayInMs, @Nullable String arg );
void terminate();
}public interface MPPositionProvider {
void addOnPositionUpdateListener(@NonNull OnPositionUpdateListener listener);
void removeOnPositionUpdateListener(@NonNull OnPositionUpdateListener listener);
@Nullable MPPositionResultInterface getLatestPosition();
}