Basic Example
A common user interface element of a Mappedin Web SDK powered app is a list of a venue's locations sorted by their associated categories.
For venues with simple category structures, it may be sufficient to display each category with their associated locations as a flat list.
We start by adding categories
to our things
object in the venue configuration object so that the category data is fetched when the map loads. For now, we only require the category names. We also define a utility function for sorting objects alphabetically by their name
property. Following that we can sort the list of categories by name, sort each categories location by name, and add those categories and locations to a list in our app's sidebar.
Sample Code
<html> <head> <script src="https://d1p5cqqchvbqmy.cloudfront.net/websdk/v1.71.12/mappedin.js"></script> </head> <body> <div class="container"> <div id="sidebar"> <h2>Locations:</h2> </div> <div class="map-container"> <div id="mapView" /> </div> </div> </body></html>
body { margin: 0; padding: 0;}
.container { display: grid; grid-template-columns: minmax(200px, 25%) 1fr;}
.map-container { position: relative;}
#sidebar { max-height: 100vh; margin: 0; padding: 0 12px; overflow-y: scroll; border-right: 1px dashed grey;}
const div = document.getElementById("mapView");
let mapview, venue;
const options = { mapview: { antialias: "AUTO", mode: Mappedin.modes.TEST, onFirstMapLoaded: () => { populateLocationsList(); }, }, venue: { clientId: "<MAPPEDIN_CLIENT_ID>", clientSecret: "<MAPPEDIN_CLIENT_SECRET>", perspective: "Website", //pick the perspective you would like to load things: { //fetch some data venue: ["slug", "name"], categories: ["name"], maps: ["name", "elevation", "shortName"], }, venue: "mappedin-demo-mall", },};
function sortByName(arr) { return arr.sort((a, b) => a.name.toUpperCase() > b.name.toUpperCase() ? 1 : -1 );}
function populateLocationsList() { const sortedCategories = sortByName(venue.categories);
sortedCategories.forEach((category) => { const heading = document.createElement("h3");
heading.appendChild(document.createTextNode(category.name)); const sidebarElement = document.getElementById("sidebar"); sidebarElement.appendChild(heading);
const ulElement = document.createElement("ul");
sortByName(category.locations).forEach((location) => { const listItem = document.createElement("li"); listItem.appendChild(document.createTextNode(location.name)); ulElement.appendChild(listItem); }); sidebarElement.appendChild(ulElement); });}
Mappedin.initialize(options, div).then((data) => { mapview = data.mapview; venue = data.venue;});
The Result
Advanced Category Structures
Mappedin's CMS allows clients to create complex data structures involving categories. Locations may belong to multiple categories, and categories have a parents
field that support defining one or more parent categories that the respective category is also a member of. This allows for a very flexible way to define your data, but also introduces some additional considerations when determining how to display a list of locations.
Since a venue may have dozens of categories and subcategories containing the same locations in a nested structure, one way to simplify this data is to show top level category headings only, while listing both their direct child locations and the locations associated with any subcategories. Note in the following example that the Footwear and Accessories categories are not shown, and that all locations that were present in either category are now visible beneath the Clothing category.
Sample Code
const div = document.getElementById("mapView");
let mapview, venue;
const options = { mapview: { antialias: "AUTO", mode: Mappedin.modes.TEST, onFirstMapLoaded: () => { populateLocationsList(); }, }, venue: { clientId: "<MAPPEDIN_CLIENT_ID>", clientSecret: "<MAPPEDIN_CLIENT_SECRET>", perspective: "Website", //pick the perspective you would like to load things: { //fetch some data venue: ["slug", "name"], categories: ["name"], maps: ["name", "elevation", "shortName"], }, venue: "mappedin-demo-mall", },};
function sortByName(arr) { return arr.sort((a, b) => a.name.toUpperCase() > b.name.toUpperCase() ? 1 : -1 );}
function getCategoricalLocations(coalesceWithParents = true) { // coalesceWithParents => // if true, return the top-level categories sorted by name with their locations array // containing the sorted locations of all subcategories. // if false, return all categories sorted by name with their associated sorted locations
const { locations: allLocations, categories: allCategories } = venue;
if (!coalesceWithParents) { //Filter locations to ones with polygons and sort categories and locations alphabetically
const result = sortByName( allCategories .filter( (item) => Array.isArray(item.locations) && item.locations.length > 0 ) .map((category) => { return { ...category, locations: category.locations.filter( (location) => location.polygons?.length > 0 ), }; }) ); return result; } else if (Array.isArray(allLocations) && Array.isArray(allCategories)) { //Create an object map of categoryId: category const categoryMap = allCategories.reduce((acc, category) => { acc[category.id] = { ...category, locations: [ ...category.locations.filter( (location) => location?.polygons?.length > 0 ), ], }; return acc; }, {});
//Find the IDs of all child categories const childCategoryList = allCategories .filter((category) => category?.parents?.length > 0) .map((category) => category.id);
const getParentCategories = (categoryID) => { //Given a category ID, return a list of the top level parents the category relates to
const flatten = (list, accumulator = []) => { Array.from(list).forEach((item) => { if (Array.isArray(item)) { flatten(item, accumulator); } else if (item.id) { accumulator.push(item); } }); return accumulator; };
if (categoryMap[categoryID]?.parents?.length == 0) { return [categoryMap[categoryID]]; } else { return flatten( categoryMap[categoryID].parents.map((id) => getParentCategories(id)) ); } };
//Add the child categories' locations to the parent category locations, avoid adding duplicate locations childCategoryList.forEach((categoryID) => { console.log("start", categoryID);
const childCategory = categoryMap[categoryID]; const parentCategories = getParentCategories(categoryID);
parentCategories.forEach((parentCategory) => { parentCategory.locations = sortByName([ ...parentCategory.locations, ...childCategory.locations.filter( (location) => !parentCategory.locations.some( (existingLocation) => existingLocation && existingLocation?.id === location.id && existingLocation?.polygons?.length > 0 ) ), ]); }); });
//Filter for exclusively parent categories from our object map const parents = Object.keys(categoryMap) .filter((categoryID) => { return !categoryMap[categoryID]?.parents?.length > 0; }) .map((categoryID) => categoryMap[categoryID]);
//Return sorted list of categories return sortByName(parents); } else { console.error("Something went wrong generating categorical locations"); return {}; }}
function populateLocationsList() { const sortedCategories = getCategoricalLocations();
sortedCategories.forEach((category) => { const heading = document.createElement("h3");
heading.appendChild(document.createTextNode(category.name)); const sidebarElement = document.getElementById("sidebar"); sidebarElement.appendChild(heading);
const ulElement = document.createElement("ul");
sortByName(category.locations).forEach((location) => { const listItem = document.createElement("li"); listItem.appendChild(document.createTextNode(location.name)); ulElement.appendChild(listItem); }); sidebarElement.appendChild(ulElement); });}
Mappedin.initialize(options, div).then((data) => { mapview = data.mapview; venue = data.venue;});