Group of objects on the map
vanilla.html
react.html
vue.html
common.css
common.ts
variables.ts
sandbox.json
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<script crossorigin src="https://cdn.jsdelivr.net/npm/@babel/standalone@7/babel.min.js"></script>
<!-- To make the map appear, you must add your apikey -->
<script src="https://js.api.mappable.world/v3/?apikey=<YOUR_APIKEY>&lang=en_US" type="text/javascript"></script>
<script
data-plugins="transform-modules-umd"
data-presets="typescript"
type="text/babel"
src="../variables.ts"
></script>
<script
data-plugins="transform-modules-umd"
data-presets="typescript"
type="text/babel"
src="./common.ts"
></script>
<script data-plugins="transform-modules-umd" data-presets="typescript" type="text/babel">
import {GroupPropTypes, groupsProps} from './common';
import {LOCATION} from '../variables';
window.map = null;
main();
async function main() {
// Waiting for all api elements to be loaded
await mappable.ready;
const {MMap, MMapDefaultSchemeLayer, MMapDefaultFeaturesLayer, MMapControls, MMapControl, MMapGroupEntity} = mappable;
const {MMapDefaultMarker} = await mappable.import('@mappable-world/mappable-default-ui-theme');
interface CustomMenuControlProps {
groupsProps: Array<GroupPropTypes>;
}
const markersState = {};
class CustomMenuControl extends MMapGroupEntity<CustomMenuControlProps> {
private _element?: HTMLHeadingElement;
private _container?: HTMLDivElement;
private _detachDom?: () => void;
protected _onAttach() {
this._createMenu();
}
_createMenu() {
this._element = document.createElement('div');
this._element.classList.add('menu');
this._container = document.createElement('div');
this._container.classList.add('container');
const titleElement = document.createElement('div');
titleElement.textContent = 'What to do in the city';
titleElement.classList.add('menu__title');
this._element.appendChild(titleElement);
this._props.groupsProps.forEach((group) => {
const divElement = document.createElement('div');
const innerDivElement = document.createElement('div');
innerDivElement.classList.add('menu__checkbox');
const inputElement = document.createElement('input');
inputElement.style.backgroundColor = group.color as string;
inputElement.classList.add('menu__checkbox_input');
const labelElement = document.createElement('label');
labelElement.classList.add('menu__checkbox_title');
inputElement.classList.add(`parent-${group.id}`);
inputElement.type = 'checkbox';
inputElement.id = group.id;
inputElement.checked = true;
inputElement.addEventListener('change', (event) => {
const checked = (<HTMLInputElement>event.target).checked;
const childCheckboxes = divElement.querySelectorAll('.child-mark');
for (let i = 0; i < childCheckboxes.length; i++) {
(<HTMLInputElement>childCheckboxes[i]).checked = checked;
}
if (checked) {
Object.entries(markersState[group.id]).forEach(([id]) => {
const markerInfo = group.marks.find((item) => item.id === id);
const newMarker = new MMapDefaultMarker({
coordinates: markerInfo.coordinates,
iconName: markerInfo.iconName,
title: markerInfo.title,
color: {day: group.color, night: group.color},
draggable: false
});
markersState[group.id][id] = newMarker;
map.addChild(newMarker);
});
} else {
Object.entries(markersState[group.id]).forEach(([id, marker]: any) => {
map.removeChild(marker);
});
}
});
labelElement.textContent = group.groupName;
innerDivElement.appendChild(inputElement);
innerDivElement.appendChild(labelElement);
divElement.appendChild(innerDivElement);
group.marks.forEach((mark) => {
const markDivElement = document.createElement('div');
markDivElement.style.marginLeft = '20px';
markDivElement.classList.add('menu__checkbox');
const markInputElement = document.createElement('input');
markInputElement.classList.add('menu__checkbox_input');
const markLabelElement = document.createElement('label');
markLabelElement.classList.add('menu__checkbox_title');
markInputElement.classList.add('child-mark');
markInputElement.style.backgroundColor = group.color as string;
markInputElement.type = 'checkbox';
markInputElement.checked = true;
markInputElement.addEventListener('change', (event) => {
const checked = (<HTMLInputElement>event.target).checked;
if (checked) {
const parent = divElement.querySelector(`.parent-${group.id}`);
(parent as any).checked = true;
const newMarker = new MMapDefaultMarker({
coordinates: mark.coordinates,
iconName: mark.iconName,
title: mark.title,
color: {day: group.color, night: group.color},
draggable: false
});
markersState[group.id][mark.id] = newMarker;
map.addChild(newMarker);
} else {
map.removeChild(markersState[group.id][mark.id]);
markersState[group.id][mark.id] = null;
}
if (markersState[group.id]) {
const markers = Object.values(markersState[group.id]);
const checked = markers.some((marker) => marker !== null);
const parent = divElement.querySelector(`.parent-${group.id}`);
(<HTMLInputElement>parent).checked = checked;
}
});
markLabelElement.textContent = mark.title;
markDivElement.appendChild(markInputElement);
markDivElement.appendChild(markLabelElement);
divElement.appendChild(markDivElement);
});
this._container.appendChild(divElement);
});
// Inserting the container inside the element
this._element.appendChild(this._container);
// Creating an entity binding to the DOM
this._detachDom = mappable.useDomContext(this, this._element, this._container);
}
protected _onDetach(): void {
// Detaching the DOM from the entity and removing references to the elements
this._detachDom?.();
this._detachDom = undefined;
this._element = undefined;
this._container = undefined;
}
}
const controls = new MMapControls({
position: 'top right',
orientation: 'vertical'
});
const control = new MMapControl({});
control.addChild(new CustomMenuControl({groupsProps}));
controls.addChild(control);
map = new MMap(document.getElementById('app'), {location: LOCATION, showScaleInCopyrights: true}, [
new MMapDefaultSchemeLayer({}),
new MMapDefaultFeaturesLayer({}),
controls
]);
groupsProps.forEach((group) => {
markersState[group.id] = {};
group.marks.forEach((mark) => {
const marker = new MMapDefaultMarker({
coordinates: mark.coordinates,
iconName: mark.iconName,
title: mark.title,
color: {day: group.color, night: group.color},
draggable: false
});
markersState[group.id][mark.id] = marker;
map.addChild(marker);
});
});
}
</script>
<!-- prettier-ignore -->
<style> html, body, #app { width: 100%; height: 100%; margin: 0; padding: 0; font-family: Arial, Helvetica, sans-serif; } .toolbar { position: absolute; z-index: 1000; top: 0; left: 0; display: flex; align-items: center; padding: 16px; } .toolbar a { padding: 16px; } </style>
<link rel="stylesheet" href="./common.css" />
</head>
<body>
<div id="app"></div>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<script crossorigin src="https://cdn.jsdelivr.net/npm/react@17/umd/react.production.min.js"></script>
<script crossorigin src="https://cdn.jsdelivr.net/npm/react-dom@17/umd/react-dom.production.min.js"></script>
<script crossorigin src="https://cdn.jsdelivr.net/npm/@babel/standalone@7/babel.min.js"></script>
<!-- To make the map appear, you must add your apikey -->
<script src="https://js.api.mappable.world/v3/?apikey=<YOUR_APIKEY>&lang=en_US" type="text/javascript"></script>
<script
data-plugins="transform-modules-umd"
data-presets="react, typescript"
type="text/babel"
src="../variables.ts"
></script>
<script
data-plugins="transform-modules-umd"
data-presets="react, typescript"
type="text/babel"
src="./common.ts"
></script>
<script data-plugins="transform-modules-umd" data-presets="react, typescript" type="text/babel">
import {LOCATION} from '../variables';
import {groupsProps} from './common';
window.map = null;
main();
async function main() {
// For each object in the JS API, there is a React counterpart
// To use the React version of the API, include the module @mappable-world/mappable-reactify
const [mappableReact] = await Promise.all([mappable.import('@mappable-world/mappable-reactify'), mappable.ready]);
const reactify = mappableReact.reactify.bindTo(React, ReactDOM);
const {MMap, MMapDefaultSchemeLayer, MMapControls, MMapControl, MMapDefaultFeaturesLayer} = reactify.module(mappable);
const {MMapDefaultMarker} = await reactify.module(await mappable.import('@mappable-world/mappable-default-ui-theme'));
const {useState, useCallback, useMemo} = React;
function App() {
const [checkedMarks, setCheckedMarks] = useState(
groupsProps.map((groupProp) => groupProp.marks.map((mark) => mark.id)).flat()
);
const onCheckGroup = useCallback(
(event, groupId: string) => {
const isChecked = event.target.checked;
if (isChecked) {
setCheckedMarks([
...checkedMarks,
...groupsProps.find((groupProp) => groupProp.id === groupId).marks.map((mark) => mark.id)
]);
return;
}
setCheckedMarks(checkedMarks.filter((markId: string) => !markId.startsWith(groupId)));
},
[checkedMarks]
);
const onCheckMark = useCallback(
(markId: string) => {
if (checkedMarks.includes(markId)) {
setCheckedMarks(checkedMarks.filter((id: string) => id !== markId));
return;
}
setCheckedMarks([...checkedMarks, markId]);
},
[checkedMarks]
);
const isGroupChecked = useCallback(
(groupId: string) =>
groupsProps
.find((groupProp) => groupProp.id === groupId)
.marks.some((mark) => checkedMarks.includes(mark.id)),
[checkedMarks, groupsProps]
);
return (
<MMap location={LOCATION} showScaleInCopyrights={true} ref={(x) => (map = x)}>
<MMapDefaultSchemeLayer />
<MMapDefaultFeaturesLayer />
<MMapControls position="top right" orientation="vertical">
<MMapControl>
<div className="menu">
<div className="menu__title">What to do in the city</div>
{groupsProps.map((groupProp) => {
const checked = isGroupChecked(groupProp.id);
return (
<div key={groupProp.id}>
<div className="menu__checkbox">
<input
type="checkbox"
id={groupProp.groupName}
className="menu__checkbox_input"
style={{backgroundColor: groupProp.color as string}}
checked={checked}
onChange={(event) => onCheckGroup(event, groupProp.id)}
/>
<label className="menu__checkbox_title">{groupProp.groupName}</label>
</div>
<div style={{marginLeft: 20}}>
{groupProp.marks.map((mark) => (
<div className="menu__checkbox" key={mark.id}>
<input
className="menu__checkbox_input"
type="checkbox"
checked={checkedMarks.includes(mark.id)}
onChange={() => onCheckMark(mark.id)}
style={{backgroundColor: groupProp.color as string}}
/>
<label className="menu__checkbox_title">{mark.title}</label>
</div>
))}
</div>
</div>
);
})}
</div>
</MMapControl>
</MMapControls>
{groupsProps.map((group) =>
group.marks
.filter((mark) => checkedMarks.includes(mark.id))
.map((mark) => (
<MMapDefaultMarker
iconName={mark.iconName}
coordinates={mark.coordinates}
title={mark.title}
color={{
day: group.color,
night: group.color
}}
/>
))
)}
</MMap>
);
}
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('app')
);
}
</script>
<!-- prettier-ignore -->
<style> html, body, #app { width: 100%; height: 100%; margin: 0; padding: 0; font-family: Arial, Helvetica, sans-serif; } .toolbar { position: absolute; z-index: 1000; top: 0; left: 0; display: flex; align-items: center; padding: 16px; } .toolbar a { padding: 16px; } </style>
<link rel="stylesheet" href="./common.css" />
</head>
<body>
<div id="app"></div>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<script crossorigin src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
<script crossorigin src="https://cdn.jsdelivr.net/npm/@babel/standalone@7/babel.min.js"></script>
<script src="https://js.api.mappable.world/v3/?apikey=<YOUR_APIKEY>&lang=en_US" type="text/javascript"></script>
<script
data-plugins="transform-modules-umd"
data-presets="typescript"
type="text/babel"
src="../variables.ts"
></script>
<script
data-plugins="transform-modules-umd"
data-presets="typescript"
type="text/babel"
src="./common.ts"
></script>
<script data-plugins="transform-modules-umd" data-presets="typescript" type="text/babel"></script>
<!-- prettier-ignore -->
<style> html, body, #app { width: 100%; height: 100%; margin: 0; padding: 0; font-family: Arial, Helvetica, sans-serif; } .toolbar { position: absolute; z-index: 1000; top: 0; left: 0; display: flex; align-items: center; padding: 16px; } .toolbar a { padding: 16px; } </style>
<link rel="stylesheet" href="./common.css" />
</head>
<body>
<div id="app"></div>
</body>
</html>
.menu {
overflow-y: auto;
box-sizing: border-box;
height: 350px;
padding: 12px;
}
.menu__title {
padding: 8px 12px;
font-size: 20px;
font-weight: 500;
}
.menu__checkbox {
display: flex;
flex-direction: row;
align-items: center;
height: 40px;
}
.menu__checkbox_input {
width: 20px;
height: 20px;
border-radius: 6px;
-webkit-appearance: none;
appearance: none;
}
.menu__checkbox_input::after {
display: none;
}
.menu__checkbox_input:checked::after {
position: relative;
top: 10px;
left: 10px;
display: block;
width: 11px;
height: 8px;
content: '';
background-image: url('data:image/svg+xml;utf8,<svg width="11" height="8" viewBox="0 0 11 8" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1.70711 3.29289C1.31658 2.90237 0.683418 2.90237 0.292893 3.29289C-0.0976311 3.68342 -0.0976311 4.31658 0.292893 4.70711L3.29289 7.70711C3.68342 8.09763 4.31658 8.09763 4.70711 7.70711L10.7071 1.70711C11.0976 1.31658 11.0976 0.683418 10.7071 0.292893C10.3166 -0.0976311 9.68342 -0.0976311 9.29289 0.292893L4 5.58579L1.70711 3.29289Z" fill="%23F5F6F7"/></svg>');
transform: translate(-50%, -50%);
}
.menu__checkbox_title {
margin-left: 12px;
font-size: 16px;
font-weight: bold;
}
import type {MMapDefaultMarkerProps} from '@mappable-world/mappable-default-ui-theme';
import type {LngLat, LngLatBounds, MMapLocationRequest} from '@mappable-world/mappable-types';
import {BOUNDS} from './variables';
mappable.ready.then(() =>
mappable.import.registerCdn(
'https://cdn.jsdelivr.net/npm/{package}',
'@mappable-world/mappable-default-ui-theme@latest'
)
);
export type GroupPropTypes = {
groupName: string;
id: string;
color: string;
marks: MMapDefaultMarkerProps[];
};
// Function for generating a pseudorandom number
const seed = (s: number) => () => {
s = Math.sin(s) * 10000;
return s - Math.floor(s);
};
const rnd = seed(10000); // () => Math.random()
// Generating random coordinates of a point [lng, lat] in a given boundary
const getRandomPointCoordinates = (bounds: LngLatBounds): LngLat => [
bounds[0][0] + (bounds[1][0] - bounds[0][0]) * rnd(),
bounds[1][1] + (bounds[0][1] - bounds[1][1]) * rnd()
];
export const groupsProps: Array<GroupPropTypes> = [
{
groupName: 'Cinema',
color: '#E096D0',
id: '1',
marks: [
{coordinates: getRandomPointCoordinates(BOUNDS), title: 'fox 20th Century', id: '1-1', iconName: 'cinemas'}
]
},
{
groupName: 'Food & drinks',
color: '#F09A75',
id: '2',
marks: [
{coordinates: getRandomPointCoordinates(BOUNDS), title: 'Burger King', id: '2-1', iconName: 'restaurants'},
{coordinates: getRandomPointCoordinates(BOUNDS), title: 'Subway', id: '2-2', iconName: 'restaurants'},
{coordinates: getRandomPointCoordinates(BOUNDS), title: 'Big burger', id: '2-3', iconName: 'restaurants'},
{coordinates: getRandomPointCoordinates(BOUNDS), title: 'Quesadilia', id: '2-4', iconName: 'restaurants'}
]
},
{
groupName: 'Outdoor',
color: '#5EBD8C',
id: '3',
marks: [
{coordinates: getRandomPointCoordinates(BOUNDS), title: 'Park Kinks', id: '3-1', iconName: 'park'},
{coordinates: getRandomPointCoordinates(BOUNDS), title: 'Zoo Pikiki', id: '3-2', iconName: 'park'},
{coordinates: getRandomPointCoordinates(BOUNDS), title: 'Park Levi', id: '3-3', iconName: 'park'}
]
},
{
groupName: 'Water',
color: '#88AECF',
id: '4',
marks: [
{coordinates: getRandomPointCoordinates(BOUNDS), title: 'Port Velington', id: '4-1', iconName: 'waterpark'},
{coordinates: getRandomPointCoordinates(BOUNDS), title: 'Port Que', id: '4-2', iconName: 'waterpark'}
]
}
];
import type {LngLatBounds, MMapLocationRequest} from '@mappable-world/mappable-types';
export const BOUNDS: LngLatBounds = [
[55.35919, 25.27852],
[55.52639, 25.21371]
];
export const LOCATION: MMapLocationRequest = {
bounds: BOUNDS // starting bounds
};
{"node": 20, "template": "static"}