Only this pageAll pages
Powered by GitBook
Couldn't generate the PDF for 283 pages, generation stopped at 100.
Extend with 50 more pages.
1 of 100

Platform Overview

Loading...

SDKS & Frameworks

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...

Prerequisites

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

MapsIndoors

Get your MapsIndoors API Key​

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.

Web

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.

Getting Started

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.

Welcome

Your go-to resource for tapping into the full potential of the MapsIndoors® platform. Dive into comprehensive guides, tutorials, and reference materials designed to make your implementation process se

Not ready for a full integration? Quickly get started .

SDKs & Frameworks

Map Template
MapsIndoors

Using Mapbox

Using Google Maps

Other guides

Products

Other

without code
Cover

Web SDK

Cover

Android SDK

Cover

iOS SDK

Cover

Integration API

Cover

React Native

Cover

Flutter

Map Engine Provider

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.

Option 1: Get your Mapbox Access Token

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:

Option 2: Get your Google Maps API Keys​

To make sure you can use the full feature set of MapsIndoors you'll need to enable these services.

Map Management

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:

    Mapbox Token
    Google Maps API Key
  • Directions API

  • Mapbox Access Token documentation
    Vector Tiles API
    Styles API
    Google Places API Web Service

    Then you need to create your Google Maps API key by following the link below:

    • Create Google Maps API key

    If you apply restrictions to your key, remember to include the Google services listed above.

    Maps JavaScript API
    Google Maps Distance Matrix API
    Google Maps Directions API

    Product Overview

    CMS

    Map Template

    Design

    Glossary

    Changelog

    Cover
    Cover
    Cover
    Cover
    Cover
    Cover

    Map Visualization

    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

    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!

    Displaying Objects

    Android

    Documentation reflects the latest version.

    You can find documentation on legacy versions here:

    Legacy Docs

    Data Visualization

    Data 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.

    Offline Data

    Offline Data

    Currently, the Web SDK does not support offline mode.

    Remove Labels from Buildings and Venues for Web

    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.

    Search Operations

    Search Operations with MapsIndoors

    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.

    Display Heatmap Overlay

    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.

    Creating a heatmap​

    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:

    Inserting the heatmap layer between MapsIndoors layers

    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:

    Single Sign-On

    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:

    • Google

    • 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 .

    Password Reset

    Password Validation​

    A password must be 16 characters or longer in order to be valid.

    Password Reset​

    You can reset your password via . Submit your e-mail address, and we'll send you a link to reset your password.

    Enter your password twice on the Password Reset page to change it.

    Display Language

    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.

    Configuring POI translations in CMS​

    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.

    Use Device Language

    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

    Remove Labels from Buildings and Venues

    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

    Managing Collisions Based on Zoom Level

    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.

    Directions

    Introduction to Directions

    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

    Search

    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

    User's Location as Point of Origin

    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.

    Utilizing MapsIndoors Web Components and Other Searches

    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 in Practice

    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.

    Getting Started

    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.

    Take-Away Skills

    Synchronizing data for a subset of venues

    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.

    User Positioning

    How User Positioning Works in MapsIndoors

    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

    Turn Off Collisions Based on Zoom Level

    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.

    Migrating to Mapbox V11

    This documentation refers to the change of the Mapbox engine in 4.5.0

    Migrating to Mapbox V11

    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:

    Managing map visibility

    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.

    Visibility and rendering

    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:

    1. The data is sent to the SDK, but not rendered on the map

    2. 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

    Selectable and searchable

    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.

    Venue and Building Info: Access information about venues and buildings stored in MapsIndoors.
    Basic Searching
    getLocation(id)
    getLocations(args)
    Extending Your Search
    getLocationsByExternalId
    Ref docs here
    Utilizing MapsIndoors Web Components and Other Searches
    Authorization
    https://auth.mapsindoors.com/login/forgotpassword

    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

  • ​
    We recommend reading Show the Blue Dot with MapsIndoors guides to get a basic understanding of the MapsIndoors positioning provider interface.
    ​
    MapsIndoors Getting Started app
    PositionProviders

    Italian

  • French

  • ​
    and the
    . Let's examine some key concepts first.

    Entry Points​

    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.

    The Route Model​

    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.

    The Route Leg Model​

    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.

    The Route Step Model​

    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.

    Directions Service
    Directions Renderer
    https://docs.mapbox.com/help/tutorials/make-a-heatmap-with-mapbox-gl-js/
    https://docs.mapbox.com/mapbox-gl-js/example/heatmap-layer/
    ​
    Mapbox GL JS API Reference
    // 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);
    here
    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);
        }
    });
    Mapsindoors changes

    While no breaking changes are introduced from the Mapsindoors SDK. Some visual changes are introduced with the use of Mapbox V11.

    Mapbox Standard Style

    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.

    3D Models

    With the Mapbox V11 release, android also supports rendering 3D models on your map. Read more about this:

    Toggling Mapsindoors features

    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

    Mapbox - Migrate to V11

    User's Location as Point of Origin

    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

    come into play, serving as intuitive markers and information windows. This example is specific to Mapbox, for Google Maps you will need to use
    .

    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.

    setDisplayRule
    Dynamically adding a Mapbox Popup based on zoom level
    popups
    info windows
    Changing the Appearance when the User clicks on a Location

    Change the Label for All Locations for the Type PRINTER

    Change the Label for a single, specific Location

    Apply the Same Display Rule to Multiple Locations

    Reset the Display Rule Back to Default

    Display Rules
    You can also add one or more venues after the initial data synchronization:

    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');
    Getting Started guide.
    // 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.

    https://components.mapsindoors.com
    https://components.mapsindoors.com/
    https://components.mapsindoors.com/search/
    https://components.mapsindoors.com/map-mapbox/

    Set Up Your Environment

    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:

    1. Create a new project folder: Choose any location on your computer. Let's call this folder mapsindoors-tutorial. Ensure the folder is empty.

    2. 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.

    Base Map Styling - Google Maps

    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.

    Use styling to control or hide visual features like the business POIs on the right hand side

    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:

    3D Maps

    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.

    Requirements​

    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).

    Why 3D Maps?

    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.

    Best Practices

    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.

    Managing feature visibility for Mapbox

    Overview

    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.

    Methods

    hideFeatures(features: string[]): void

    This 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.

    Conclusion

    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 .

    SSO Authorisation

    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.

    Working with Events

    Overview​

    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.

    Ready Event

    The ready event will be fired when MapsIndoors is done initializing and is ready to interact.

    Building Changed Event

    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.

    Floor Changed Event

    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.

    Click Event

    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.

    Language

    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.

    Configuring POI translations in CMS

    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:

    Use Device Language

    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

    2-Factor Authentication

    Enabling 2-Factor Authentication​

    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.

    Disabling 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.

    Map Engine Setup

    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.

    Mapbox V3

    In order to access Mapbox v3 and its options, use mapView:

    For Mapbox v3, we exposed three new constructor parameters for Mapbox v3:

    Managing your 3D Maps

    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.

    1. Visibility - Controls whether the 3D walls are visible on the map.

    Customizing the Route Animation

    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.

    Accessing the Route Animation Controller

    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

    Directions Renderer

    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

    Custom Icons

    Creating Custom Stop Icons

    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.

    SSO Configuration

    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

    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

    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.

    How to Configure App User Roles

    App User Roles are configured via the MapsIndoors CMS. Go to Solution Details > App Settings > App Configuration, and find App User Roles

    Show User's Location aka. Blue Dot

    Overview

    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.

    Tailoring the directions to your specific needs

    by leveraging the avoidHighwayTypes and excludeHighwayTypes parameters.

    This is a continuation of the article

    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:

    Getting a Polygon from a Location

    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.

    Turn Off Collisions Based on Zoom Level

    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.

    Google Maps for Android

    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.

    Prerequisites

    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

    Location Clustering

    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:

    Location Details

    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.

    See Route Element Details

    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

    Searching on a Map

    Use the MapsIndoors.getLocationsAsync() method to search for content in your MapsIndoors Solution.

    Setup a query for the nearest single best matching location and display the result on the map

    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.

    ​
    ​
    The system will accept a Boolean here, so either 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.

    1. Visibility - Controls whether the extrusion is visible on the map.

      • The system will accept a Boolean here, so either true or false.

    2. 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.

    3. 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.

    4. 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).

    5. 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.

    1. Visibility - Controls whether the 3D model is visible on the map.

      • The system will accept a Boolean here, so either true or false.

    2. 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.

    3. 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.

    4. 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.

    5. 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.

    6. 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.

    7. 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.

    8. 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.

    please read this article covering the topic
    The bare minimum needed by MapsIndoors Auth - given that the authentication server follows the standard as closely as possible - is the following:
    • 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.

    Additional configuration​

    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.

    Organization-specific CMS URL​

    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.

    ​
    Open ID Connect
    OAuth 2

    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.

    Get Your MapsIndoors API key​

    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.

    Work with MapsIndoors SDK behind a Firewall​

    If you need to work with MapsIndoors SDK behind a firewall, you might need to allowlist some IP-addresses.

    ​
    setup at a new project in the Google Cloud Console
    create one
    Maps API Library Page

    Italian

  • French

  • ​
    ​
    The <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.

  • here
    Only 2D features visible
    Only 3D features visible
    ​
    ​
    building
    ​
    ​
    location
    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++
            }
        }
    }
    See the sample in LocationClusteringFragment.kt
    //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
    }
    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.

    How to Assign/Change a Role to a User​

    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.

    Features Affected by App User Roles​

    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.

    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.

    Search​

    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.

    Directions​

    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.

    ​
    How the position is determined​

    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).

    The MapsIndoors PositionControl class​

    The 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.

    Basic Example​

    MapsIndoors supports both Google Maps and MapBox, and the methods for each vary slightly. Both still revolve around PositionControl.

    Google Maps​

    Mapbox​

    maxAccuracy​

    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:

    ​
    The 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 avoidHighwayTypes and excludeHighwayTypes parameters take priority over avoidStairs

    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:

    Unrestricted routes

    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.

    Directions Service

    Mapbox for Android​

    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)
        }
    }
    ​
    When a marker is clicked, get the related MapsIndoors location object and propagate that to a method that fills the text in the 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.

    See the sample in LocationDetailsFragment.kt

    <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
                )
            )
        }
    }
    Override getItem and getItemId to let click events work corretly

    Now 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)
    }
    Setup a query for a group of locations and display the result on the map​

    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 v3 Map layout

    Mapbox' extruded buildings are not visible when MapsIndoors data is shown at the specified zoom level:

    MapsIndoors solution

    You can now choose between 4 different types of light: day, dawn, night or dusk.

    Mapbox v3 'dusk' light

    You can read more about the latest Mapbox v3 Standard Style here.

    Mapbox V2

    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:

    Solution using Mapbox v2

    Setting Animation Options

    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

    Property
    Type
    Default Value
    Description

    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.

    Directions Service
    reference documentation
    The RouteStopIconProvider Interface

    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.

    Implementing a Custom RouteStopIconProvider

    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.

    Using the CustomRouteStopIconProvider

    Multi-stop route with a custom icon.
    RouteStopIconProvider
    Google Maps Billing
    GoogleMapsView
    GoogleMapsView
    ​
    get in touch

    Application User Roles

    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.

    How to Configure App User Roles​

    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.

    How to Assign/Change a Role to a User

    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 .

    Features Affected by App User Roles

    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.

    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.

    Search

    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.

    Directions

    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.

    Using multi-stop navigation

    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.

    Querying a multi-stop 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).

    Showing and configuring a multi-stop route on the map

    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

    Adding a custom image to a specific waypoint

    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.

    Change Building Outline

    Crafting a Dynamic, Color-Transitioning Example

    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.

    Multi-stop navigation

    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.

    Custom Properties

    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.

    Enable Live Data

    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

    Display Language

    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.

    Configuring POI translations in CMS

    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.

    Directions

    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​

    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.

    The Route Model​

    When requesting Routes in MapsIndoors Directions Service The Route model in MapsIndoors is seperated into Legs and these Legs are again seperated into Steps.

    The Route Leg Model​

    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.

    The Route Step Model​

    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.

    Directions Service
    Directions Renderer
    ​
    reference documentation
    ​
    ​
    ​
    ​
    By default route stops will be shown as a red pin with numbers
    Customized MPRouteStopIcon

    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.

    Use Fixed Language​

    The MapsIndoors language can be fixed to a specific language by supplying an ISO 639-1 language code, for example French:

    Use Device Language​

    The MapsIndoors language can be aligned with the device language by supplying the current language code of the device:

    Translate HTML instructions on directions.

    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.

    Translate Route Labels

    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>
    Additional Details

    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.

    CMS configuration

    The building outline design will be taken from the values set through the CMS.

    Overriding the CMS configuration via the SDK

    To change the building outline you can use the different properties of the BuildingOutlineOptions interface. The properties are the following:

    1. visible - Controls whether the Building Outline is visible on the map.

      • The value should be a Boolean here, so either true or false.

    2. 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.

    3. 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.

    4. 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.

    5. strokeWeight - Controls the stroke width (in pixels) of the Building Outline.

      • The value should be a number between.

    6. 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.

    Changing border color programmatically
    Navigation with multiple stops

    Leverage Multi-Stop Navigation in Your App

    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

    Route with multiple stops, in the given input order.

    Optimized multi-stop navigation

    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.

    Route with multiple stops, optimized for shortest travel time.

    Customizing all Route Stop Icons

    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):

    Stop icons with a different background color.

    To omit the numbering of the icons, set the numbered boolean parameter to false:

    Stop icons without numbers.

    Adding Labels to Route Stops

    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.

    Numbered stop icons with labels displayed underneath.

    Customizing Individual Stop Icons

    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:

    Directions Service
    Directions Renderer
    Generic Custom Properties are meant for tagging data with key/value data that is relevant for the apps using the map data. These values will be available to the app regardless of the language of the end user, and are often not directly displayed. Examples of values that might be added as Generic Custom Properties could be:
    • 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:

    Scenario
    Generic
    Language Specific
    SDK 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.

    Custom Property Templates​

    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.

    Creating Custom Properties​

    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.

    Reading Custom Properties​

    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.

    Example 1​

    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

    It could make sense to make this a generic custom property if your app does not render anything language specific in the application based on this value's string.

    All custom property values are strings. You'll need to potentially convert the data type on the front end of your application.

    refreshments@gen=True

    It could make sense to make this a generic custom property if your app does not render anything language specific in the application based on this value's string.

    All custom property values are strings. You'll need to convert it to a Boolean value on the front end of your application.

    Example 2​

    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.

    generic property for video without audio
    description and videoLink with language specific properties
    .

    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.

    MapsActivity.kt

    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.

    MapsActivity.kt

    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.

    Summary​

    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!.

    collaboration with MapsPeople

    Authentication

    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.

    Show a Map

    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.

    Show a Map with MapsIndoors​

    ASYNC

    Please note that data in MapsIndoors is loaded asynchronously. This results in behavior where data might not have finished loading if you call methods accessing it immediately after initializing MapsIndoors. Best practice is to set up listeners or delegates to inform of when data is ready. Please be aware of this when developing using the MapsIndoors SDK.

    Initialize 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.

    Initialize MapsControl

    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.

    uses Google Maps listeneres to control some map logic. So be aware that using Google Maps listeners directly might break intended behavior of the MapsIndoors experience. We recommend to check our reference docs, and see if you can add a specific Listener through the and always use those when possible.

    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:

    Custom Floor Selector

    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.

    Prerequisites

    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.

    Implementation

    Step 1: Create the CustomFloorSelector Class

    First, we'll create a CustomFloorSelector class that will handle the creation and management of our custom floor selector.

    Step 2: Implement Floor Management Methods

    Add methods to handle showing, hiding, and updating the floor selector:

    Step 3: Implement Floor Button Creation and Updating

    Add methods to create and update the floor buttons:

    Step 4: Initialize MapsIndoors and CustomFloorSelector

    Now, let's initialize MapsIndoors and our custom floor selector:

    Step 5: Set Up Event Listeners

    Finally, set up event listeners to update the floor selector when necessary:

    Customization

    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.

    Switching Solutions

    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.

    Starting a Solution​

    To initialize MapsIndoors, do the following:

    Google Maps

    Mapbox

    Switching Solutions

    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():

    Google maps

    Mapbox

    Integrating MapsIndoors into your own App

    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.

    Prerequisites

    Before you get started, you need to get the API keys needed. This process is the same for both platforms.

    Get Your Google Maps API key

    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.

    Get Your MapsIndoors 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.

    Integrating the App

    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:

    Material 1.5 is used for this app. If another version is used some UI elements might differ from the initial application. If you do not want to use the Material library you will need to find alternatives for some view elements inside the fragments.

    • 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

    The Final Result

    Summary

    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!

    Directions Service

    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?

    Create a New Project

    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.

    Set Up Your Environment

    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 .

    Directions Renderer

    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.

    Directions Service

    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.type
    private 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()
                ...
            }
        }
        ...
    }
    https://developer.mozilla.org/en-US/docs/Web/CSS/color_value

    B

    4

    https://www.npmjs.com/package/@mapsindoors/componentswww.npmjs.com
    OAuth2 client library from Open Id
    see here
    ​
    MapControl
    MapsActivity.kt
    MapsActivity.kt
    ​
    MapControl
    MapControl
    MapControl
    MapControl
    MapControl
    MapsActivity.kt
    MapsActivity.kt
    MapsActivity.kt
    MapsActivity.kt
    MapsActivity.kt
    MapsActivity.kt
    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:

  • ​
    ​
    setup at a new project in the Google Cloud Console
    "Getting Started"
    create one
    Maps API Library Page
    Credentials page
    ​
    contact MapsPeople
    ​
    https://github.com/MapsPeople/MapsIndoorsTemplate-Android
    http://maven.mapsindoors.com/
    Glide
    ​
    ​
    further documentation
    See more info on how to do that here.
    If you already have an Android device, make sure to enable developer mode and USB debugging

    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.

    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.

    Setup MapsIndoors​

    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.

    On newer versions of android studio this preset has been moved. You can instead choose an empty activity and inside you package you can right click and choose New -> Google -> Google Maps Activity. It is explained in google maps documentation here: Create a Google Maps project in Android Studio

    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:

    Please note that mapsindoors uses java 8 language features, that requires desugaring if minSdkVersion is 24 or below. Read how to enable this in gradle here: Use Java 8 language features and APIs

    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.

    ​
    Installing Android Studio
    set up an emulator through Android Studio
    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.MPDebugLog
    dependencies {
        ...
        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.MPDebugLog
    android {
        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?

  • Creating and combining the interfaces and the map view.
    Determining the scope and simplicity of your end user experience should be a big focus when implementing the MapsIndoors SDK

    From an implementation standpoint, there are two functional things that need to be taken care of.

    1. Setting up and requesting directions

    1. Handling and rendering directions responses

    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.)

    Implementation

    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.

    Change Transportation Mode​

    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)

    Route Restrictions​

    Avoiding Stairs and Steps​

    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:

    App User Role Restrictions​

    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.

    Transit Departure and Arrival Time​

    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 transitOption object, see google.com/maps/documentation.

    Additional reading

    For more information on the options you can provide, check the documentation on getting routes.

    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.

    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.

    Directions Service
    ​
    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);
    }
    Change Transportation Mode​

    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.

    Route Restrictions​

    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

    Avoid and exclude way types

    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.

    App User Role Restrictions​

    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.

    Transit Departure and Arrival Time​

    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);

    Highlight, Hover and Select

    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.

    Available states

    State
    Description

    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:

    Hover

    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%.

    Highlight and Selection

    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:

    Highlight

    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:

    Selection

    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

    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:

    Parameter
    Description
    Class

    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

    Styling Wizard: Google Maps APIsmapstyle.withgoogle.com
    Google Maps Styling Wizard (legacy)
    Cloud-based maps styling overview  |  Maps JavaScript API  |  Google for DevelopersGoogle for Developers
    Feature documentation from Google Maps

    Display a map

    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.

    Using Cisco DNA Spaces

    MapsIndoors & CiscoDNA for Web

    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.

    Create a Search Experience

    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

    Create a Simple Query Search

    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.

    Location Data Sources

    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.

    Routeapp.mapsindoors.com
    Route object
    Legapp.mapsindoors.com
    Leg object
    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.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 => {
      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);
    }
    mpDirectionsService?.setTravelMode(MPTravelMode.WALKING)
    // queries the MPDirectionsService for a route with the hardcoded user location and the point from a location.
    mpDirectionsService?.query(mUserLocation, mpLocation.point)
    }
    }
    mpDirectionsService.setTravelMode(MPTravelMode.WALKING);
    // queries the MPDirectionsService for a route with the hardcoded user location and the point from a location.
    mpDirectionsService.query(mUserLocation, mpLocation.getPoint());
    }
    ​
    ​

    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.

    An example of the hover state DisplayRule in action
    An example of the highlight state DisplayRule in action
    An example of the selection state DisplayRule in action

    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)

    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

    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

    User Positioning in MapsIndoors for Web​

    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.

    Code Sample​

    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:

    ​
    Add a 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:

    See the samples in the locationsources folder

    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? = null
    fun 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}&region=${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 class
    const 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)

  • Prerequisites

    • 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.

    The Code

    To display the map, you'll need to update your index.html, style.css, and script.js files as follows.

    Update index.html

    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 style.css

    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.

    Add JavaScript to script.js

    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

    Expected Outcome

    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.

    Troubleshooting

    • 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.

    Next Steps

    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

    For advanced usage of the search functionality read the Search guide and tutorials connected to it: Search Guide

    Show a List of Search Results​

    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.

    MapsActivity.kt

    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

    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

    SearchFragment.kt

    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.

    MapsActivity.kt

    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.

    SearchItemAdapter.kt

    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

    MapsActivity.kt

    See the full example of the search method here: MapsActivity.kt

    Filter Locations on Map Based on Search Results​

    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.

    MapsActivity.kt

    Expected result:

    The accompanying UI and implementation of this search experience can be found in the getting started app sample. Getting Started App sample

    ​
    MapsIndoors

    Display a map

    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)

    Prerequisites

    • 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.

    The Code

    To display the map, you'll need to update your index.html, style.css, and script.js files as follows.

    Update index.html

    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 style.css

    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%;

    Add JavaScript to script.js

    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.

    Expected Outcome

    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.

    Troubleshooting

    • 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).

    Here's a CodeSandbox demonstrating the result you should have by now:

    Next Steps

    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:

    Creating a Search Experience

    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.

    Stepapp.mapsindoors.com
    Step object
    DirectionsResultapp.mapsindoors.com
    Directions response object
    Logo
    Logo

    Getting Directions

    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.

    Get Directions Between Two Locations

    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 + "'"
            }
        }
    }
    Logo
    Logo
    private lateinit var mRecyclerView: RecyclerView
    private lateinit var mLinearLayoutManager: LinearLayoutManager
    private val mAdapter: MPSearchItemRecyclerViewAdapter = MPSearchItemRecyclerViewAdapter()
    private lateinit var searchInputTextView: TextInputEditText
    private var searchHandler: Handler? = null
    See the sample in FullscreenSearchFragment.kt
    See the sample in SearchFragment.kt
    mRecyclerView = 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
                            )
                        )
                    }
                }
            }
        }
    }
    mapsIndoorsInstance
    to interact with (e.g., to know available floors and change the current floor). For more details on
    FloorSelector
    , see its
    .
  • const 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.

  • mapsindoors.mapView.GoogleMapsView class documentation
    mapsindoors.MapsIndoors class reference
    class reference
    mapsindoors.MapsIndoors class documentation
    class reference

    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.

  • 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.

  • 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.

  • Set Up Your Environment guide
    Mapbox website
    Mapbox documentation
    Create a Search Experience

    MapsActivity.kt

    class MapsActivity : FragmentActivity(), OnMapReadyCallback, OnRouteResultListener

    MapsActivity.kt

    class MapsActivity : FragmentActivity(), OnRouteResultListener

    Implement 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.

    SearchItemAdapter.kt

    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.

    MapsActivity.kt

    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:

    MapsActivity.kt

    Inside the NavigationFragment we will implement logic to navigate through Legs of our Route.

    NavigationFragment.kt

    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:

    RouteLegFragment.kt

    See the full implementation of the fragment here: RouteLegFragment.kt

    Change Transportation Mode​

    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!.

    ​
    Logo

    Caching & Offline Data

    Cacheable Data​

    MapsIndoors has three levels of caching:

    1. Basic Data: All descriptions, geometries and metadata about POIs, rooms, areas, buildings and venues.

    2. Detailed Data: The same as Basic Data, plus images referenced by the data.

    3. 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:

    1. Basic Data: All descriptions, geometries and metadata about POIs, rooms, areas, buildings and venues.

    2. Detailed Data: The same as Basic Data, plus images referenced by the data.

    Wayfinding Instructions

    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)
    }
    Logo
    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:

    ​
    ​
    ​
    ​
    ​
    ​
    ​
    ​
    ​
    ​
    MapsIndoors.synchronizeContent((e) -> {
        ...
    });
    MPDataSetCache dataSet = MPDataSetCacheManager.getInstance().getDataSetByID("API KEY");
    dataSet.setScope(mContext, MPDataSetCacheScope.DETAILED);
    MPDataSetCacheManager.getInstance().synchronizeDataSets(Collections.singletonList(dataSet));
    WayfindingFragment.kt

    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.

    WayfindingFragment.kt

    Next we will create FragmentStateAdapter that will be used on the ViewPager to contain RouteLegFragments

    WayfindingFragment.kt

    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.

    WayfindingFragment.kt

    Now, lets create the RouteLegFragment to give context for the Legs in the WayfindingFragment

    RouteLegFragment.kt

    You must also update the onViewCreated method to use the new views added earlier in the tutorial.

    RouteLegFragment.kt

    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.

    WayfindingFragment.kt

    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.

    WayfindingFragment.kt

    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.

    WayFinding
    Wayfinding view
    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?.syncSize
    MPDataSetCacheManager.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? = null
    override 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
        }
    })
    class reference
    Mapbox GL JS documentation on controls

    Using External ID, Geospatial Joins

    Background on External ID

    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.

    Getting Locations by External ID

    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.

    Implementation example

    Adding Geospatial Criteria to Your Own Search

    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

    First and foremost, the Map is not required

    1. 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 .

    1. 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

    Make sure to use the latest version of the SDK when implementing.

    Depending on your ambition, you could do things like automatically book the closest nearby for the user if they wish.

    This particular feature shows value with MapsIndoors even for employees who are quite familiar with their surroundings.

    Integration API documentation
    getting in touch
    here
    Update the map with display rules based on your own data
    Narrow recommendations based on walking distance proximity

    Create a Search Experience

    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().

    Prerequisites

    • 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.

    The Code

    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.

    Update index.html

    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.

    Update style.css

    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.

    Update script.js

    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

    Expected Outcome

    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.

    Troubleshooting

    • 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.

    Next Steps

    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:

    Create a Search Experience

    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().

    Prerequisites

    • 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.

    The Code

    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.

    Update index.html

    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.

    Update style.css

    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.

    Update script.js

    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

    Expected Outcome

    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.

    Troubleshooting

    • 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.

    Next Steps

    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:

    Show the Details

    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()

    Show the Details

    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.

  • : An
    input
    event listener is attached to
    searchInputElement
    . This calls the
    onSearch
    function each time the user types into the search field.
  • onSearch() 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.

    Verify 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.

  • Step 1: Displaying a Map with Mapbox GL JS
    Show the Details

    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.

  • : An
    input
    event listener is attached to
    searchInputElement
    . This calls the
    onSearch
    function each time the user types into the search field.
  • onSearch() 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.

    Verify 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.

  • Step 1: Display a Map
    Show the Details
    to manage map state when closing the details view.

    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

    Prerequisites

    • 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.

    The Code

    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.

    Unified Click Handler Pattern

    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.

    Update index.html

    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.

    Update style.css

    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).

    Update script.js

    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.

    Expected Outcome

    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.

    Troubleshooting

    • 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.

    Next Steps

    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

    to manage map state when closing the details view.

    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

    Prerequisites

    • 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.

    The Code

    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.

    Unified Click Handler Pattern

    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.

    Update index.html

    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.

    Update style.css

    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).

    Update script.js

    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.

    Expected Outcome

    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.

    Troubleshooting

    • 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.

    Next Steps

    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.

  • API documentation
    API documentation
    LocationsService.getLocations() documentation
    Location interface
  • 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.

  • API documentation
    API documentation
    LocationsService.getLocations() documentation
    Location interface
    <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.
    showDetails
    (for UI updates) creates a cleaner architecture that's easier to maintain and extend.

    The 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.
    showDetails
    (for UI updates) creates a cleaner architecture that's easier to maintain and extend.

    The 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.

  • Location documentation
    reference
    setFloor() documentation
    .
  • mapsIndoorsInstance.selectLocation(location): Selects and highlights this specific location on the map. For further information, refer to the selectLocation() API documentation.

  • Location documentation
    reference
    setFloor() documentation

    Searching

    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.

    Retrieve Specific Location: getLocation(id)

    Meeting Room Finderstorage.googleapis.com
    Logo
    Use-Case 1: Center the Map to the Location

    Example 1: Using MapsIndoors Location ID on the Card

    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.

    Example 2: Using an External Custom ID on the Card

    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.

    Use-Case 2: Utilizing MapsIndoors SDK Highlighting and Filtering

    Take advantage of using MapsIndoors native filtering and highlighting functionality via the SDK without needing to implement your own custom display logic.

    Use-Case 3: Modify Display Rule

    Example 1: Customizing Display Rule on Click Event with MapsIndoors Location ID

    Example 2: Using an External Custom ID

    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.

    Use-Case 4: Add a Popup / Info Window

    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.

    Retrieve Queried Locations: getLocations(args opt)

    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.

    Parameter
    Type
    Optional/Required
    Description
    Default / Example
    Code Example

    q

    string

    Optional

    Use a text query to search for one or more locations.

    fields

    string

    Optional

    Example of Creating a Search Query​

    See the full list of parameters in the reference guide:

    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

    Display Locations as List​

    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.

    Search example​

    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.

    Checking for Results​

    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.

    Getting Directions

    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).

    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.

  • Prerequisites

    • 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.

    Update index.html

    Open your index.html file. Add a new directions panel inside the existing .panel container:

    Explanation of index.html updates

    • 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.

    Update style.css

    Add styles for the new directions UI elements:

    Explanation of style.css updates

    • 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.

    Update script.js

    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.

    Expected Outcome

    • 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.

    Troubleshooting

    • 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.

    DirectionsService
    DirectionsRenderer

    Getting Directions

    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.

    Prerequisites

    • 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.

    Update index.html

    Open your index.html file. Add a new directions panel inside the existing .panel container:

    Explanation of index.html updates

    • 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.

    Update style.css

    Add styles for the new directions UI elements:

    Explanation of style.css updates

    • 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.

    Update script.js

    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.

    Expected Outcome

    • 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.

    Troubleshooting

    • 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.

  • Step 3: Show the Details
    <!-- 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();
    });

    Migrating from V3 to V4

    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.

    MapsIndoors SDK Map Engine Flavors​

    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 Initialization

    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 Initialization

    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().

    SolutionConfig & AppConfig

    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.

    Venue Name

    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.

    Display Rules

    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.

    DirectionsService & DirectionsRenderer

    There are two basic functions here - Retrieving, or querying a route, and rendering it onto the map.

    Query Route

    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.

    Render Route

    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.

    Map & Camera Behavior Configs

    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.

    The "Go-To" Function

    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

    Map Filtering

    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.

    Positioning Providers

    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.

    SDK Interface Changes

    Removed Classes & Interfaces

    Removed

    Renamed Classes & Interfaces

    V3
    V4

    implementation 'com.mapspeople.mapsindoors:googlemaps:4.2.5'
    
    implementation 'com.mapspeople.mapsindoors:mapbox:4.2.5'

    setAnimationDuration(int)

  • setAllowFloorChange(boolean)

  • MPVenue

    LintTestClass

    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();
    }