Get POIs information on click
Get POI information from MapTiler, OpenStreetMap (OSM), and Wikidata data by clicking on the map.
Click on any POI on the map to get the information.
NPM module setup
npm install --save @maptiler/sdk blueimp-md5
import { Map, MapStyle, config } from '@maptiler/sdk';
import '@maptiler/sdk/dist/maptiler-sdk.css';
import md5 from 'blueimp-md5';
import queryString from 'https://cdn.jsdelivr.net/npm/query-string@8.1.0/+esm'
config.apiKey = 'YOUR_MAPTILER_API_KEY_HERE';
const center = [-73.986281, 40.74241];
const map = new Map({
container: 'map',
zoom: 16,
center: center,
style: MapStyle.STREETS
});
const marker = new maptilersdk.Marker();
function toggleSidebar(id) {
const elem = document.getElementById(id);
const classes = elem.className.split(' ');
const collapsed = classes.indexOf('collapsed') !== -1;
const padding = {};
if (collapsed) {
// Remove the 'collapsed' class from the class list of the element, this sets it back to the expanded state.
classes.splice(classes.indexOf('collapsed'), 1);
padding[id] = 300; // In px, matches the width of the sidebars set in .sidebar CSS class
map.easeTo({
padding: padding,
duration: 1000 // In ms, CSS transition duration property for the sidebar matches this value
});
} else {
padding[id] = 0;
// Add the 'collapsed' class to the class list of the element
classes.push('collapsed');
map.easeTo({
padding: padding,
duration: 1000
});
}
// Update the class list on the element
elem.className = classes.join(' ');
}
function showSidebar(id) {
const elem = document.getElementById(id);
const classes = elem.className.split(' ');
const collapsed = classes.indexOf('collapsed') !== -1;
const padding = {};
if (collapsed) {
// Remove the 'collapsed' class from the class list of the element, this sets it back to the expanded state.
classes.splice(classes.indexOf('collapsed'), 1);
padding[id] = 300; // In px, matches the width of the sidebars set in .sidebar CSS class
map.easeTo({
padding: padding,
duration: 1000 // In ms, CSS transition duration property for the sidebar matches this value
});
// Update the class list on the element
elem.className = classes.join(' ');
}
}
map.on('load', function () {
toggleSidebar('left');
map.on('click', async function(e) {
const features = map.queryRenderedFeatures(e.point, {
layers: ['Public', 'Sport', 'Tourism', 'Culture', 'Education', 'Shopping', 'Food',
'Transport', 'Park', 'Healthcare', 'Station']
});
if (features.length > 0) {
getInfoFromLngLat(e.lngLat, features[0]);
}
});
});
async function getInfoFromLngLat(lngLat, feature) {
marker.setLngLat(lngLat).addTo(map);
const osmInfo = await getOMSInfo(feature.id);
const wikidata = await getWikidata(osmInfo?.tags);
showPoiInfo(feature, osmInfo, wikidata);
}
async function getOMSInfo(id) {
const query = queryString.stringify({
data: `[out:json][timeout:25];
node(${id/10});
out tags;`
});
const response = await fetch(`https://overpass-api.de/api/interpreter?${query}`, {
redirect: 'follow',
headers: {
accept: 'application/json'
},
});
const info = await response.json();
return info?.elements[0] ? info.elements[0] : null;
}
function getWikidataId(tags) {
if (!tags) return null;
const regexWikidata = /(\w*:)?wikidata/;
const key = Object.keys(tags).find(key => key.match(regexWikidata));
return tags[key] ? tags[key] : null;
}
function getWikidataImageHash(name) {
const imageHash = md5(name);
return imageHash;
}
function getWikidataImagePath(image_name) {
const name = image_name.replace(/\s+/g, '_');
const hash = getWikidataImageHash(name);
return `https://upload.wikimedia.org/wikipedia/commons/${hash.substring(0,1)}/${hash.substring(0,2)}/${name}`;
}
async function getWikidata(tags) {
const id = getWikidataId(tags);
if (!id) return null;
const response = await fetch(`https://www.wikidata.org/wiki/Special:EntityData/${id}.json?flavor=simple`);
const info = await response.json();
let image;
if (info.entities[id].claims.P154) {
image = getWikidataImagePath(info.entities[id].claims.P154[0].mainsnak.datavalue.value);
}else if (info.entities[id].claims.P18) {
image = getWikidataImagePath(info.entities[id].claims.P18[0].mainsnak.datavalue.value);
}
return image ? {...info.entities[id], ...{image}} : info.entities[id];
}
function showPoiInfo(feature, osmInfo, wikidata) {
const textHtml = [];
if (wikidata?.image) {
textHtml.push(`<img src="${wikidata?.image}" />`);
}
textHtml.push(`<h1>${feature.properties.name}</h1>`);
if (feature.properties.class !== feature.properties.subclass) {
textHtml.push(`<h4>${feature.properties.class} (<small>${feature.properties.subclass}</small>)</h4>`);
} else {
textHtml.push(`<h4>${feature.properties.class}</h4>`);
}
if (osmInfo && osmInfo?.tags) {
const {opening_hours, "contact:website": contact_website, website, ...tags } = osmInfo.tags;
if (contact_website || website) {
let web = website ?? contact_website;
textHtml.push(`<div><a href="${web}">${web}</a></div>`);
}
if (opening_hours) {
textHtml.push(`<h3>Opening hours</h3>`);
osmInfo?.tags?.opening_hours.split(",").forEach(element => {
textHtml.push(`<div>${element.trim()}</div>`);
});
}
if (tags) {
textHtml.push(`<h3>Details</h3>`);
textHtml.push(`<div class="details-info">`);
Object.keys(tags).forEach(element => {
if (element.includes("email")) {
textHtml.push(`<div><label>${element}:</label><a href="mailto: ${tags[element]}">${tags[element]}</a></div>`);
} else if (element.includes("website")) {
textHtml.push(`<div><label>${element}:</label><a href="${tags[element]}">${tags[element]}</a></div>`);
} else if (element.includes("wikidata")) {
textHtml.push(`<div><label>${element}:</label><a href="https://www.wikidata.org/wiki/${tags[element]}">${tags[element]}</a></div>`);
} else if (element.includes("wikipedia")) {
const [lang, term] = tags[element].split(":");
textHtml.push(`<div><label>${element}:</label><a href="https://${lang}.wikipedia.org/wiki/${term}">${tags[element]}</a></div>`);
} else {
textHtml.push(`<div><label>${element}:</label>${tags[element]}</div>`);
}
});
textHtml.push(`</div>`);
}
}
document.querySelector(".sidebar-content-info").innerHTML = textHtml.join("");
showSidebar('left');
}
document.querySelector(".sidebar-toggle").addEventListener('click', function() {
toggleSidebar('left');
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex">
<title>Offset the vanishing point using padding</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<div id="map">
<div id="left" class="sidebar flex-center left collapsed">
<div class="sidebar-content rounded-rect flex-center">
<div class="sidebar-content-info">Left Sidebar</div>
<div
class="sidebar-toggle rounded-rect left"
>
<span class="icon"></span>
</div>
</div>
</div>
</div>
<script type="module" src="./main.js"></script>
</body>
</html>
body {margin: 0; padding: 0;}
#map {position: absolute; top: 0; bottom: 0; width: 100%;}
.rounded-rect {
background: white;
border-radius: 10px;
box-shadow: 0 0 50px -25px black;
}
.flex-center {
position: absolute;
display: flex;
justify-content: center;
align-items: center;
}
.flex-center.left {
left: 0px;
}
.sidebar-content-info {
position: absolute;
top: 0px;
font-size: 1rem;
padding: 16px;
box-sizing: border-box;
width: 100%;
word-break: break-word;
overflow-y: auto;
height: 100%;
}
.sidebar-content-info img {
width: 100%;
}
.sidebar-content-info h1 {
line-height: 1em;
}
.sidebar-content-info label {
margin-right: 8px;
color: #6B7C92;
font-weight: 600;
}
.sidebar-content-info .details-info {
font-size: 0.8em;
}
.sidebar-content {
position: absolute;
width: 95%;
height: 95%;
font-family: Arial, Helvetica, sans-serif;
font-size: 32px;
color: #AEB6C7;
}
.sidebar-toggle {
position: absolute;
width: 1.3em;
height: 1.3em;
overflow: visible;
display: flex;
justify-content: center;
align-items: center;
}
.sidebar-toggle.left {
right: -1.5em;
}
.sidebar.left .sidebar-toggle.left .icon {
background-image: url("data:image/svg+xml,%3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Ctitle%3Ekeyboard_arrow_left%3C/title%3E%3Cpath fill='currentColor' d='M15.422 16.594l-1.406 1.406-6-6 6-6 1.406 1.406-4.594 4.594z'%3E%3C/path%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: center;
}
.sidebar.left.collapsed .sidebar-toggle.left .icon {
background-image: url("data:image/svg+xml,%3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Ctitle%3Ekeyboard_arrow_right%3C/title%3E%3Cpath fill='currentColor' d='M8.578 16.594l4.594-4.594-4.594-4.594 1.406-1.406 6 6-6 6z'%3E%3C/path%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: center;
}
.icon {
display: inline-block;
width: 70%;
height: 70%;
background-size: cover;
}
.icon:hover {
filter: invert(71%) sepia(45%) saturate(7285%) hue-rotate(158deg) brightness(90%) contrast(92%);
}
.sidebar-toggle:hover {
color: #0aa1cf;
cursor: pointer;
}
.sidebar {
transition: transform 1s;
z-index: 1;
width: 300px;
height: 100%;
}
/*
The sidebar styling has them "expanded" by default, we use CSS transforms to push them offscreen
The toggleSidebar() function removes this class from the element in order to expand it.
*/
.left.collapsed {
transform: translateX(-295px);
}
.right.collapsed {
transform: translateX(295px);
}
Related examples
Geocoding search for POIs near the user's location
ExamplesFilter geocoding search results by specific place types.