Map customization
vanilla.html
react.html
vue.html
common.css
common.ts
variables.css
variables.ts
<!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 type {VectorCustomizationElements, VectorCustomizationItem} from '@mappable-world/mappable-types';
import {
buildingInitialCustomization,
CustomizationControl,
generateColor,
groundInitialCustomization,
roadsInitialCustomization,
waterInitialCustomization
} 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, MMapControls, MMapControl} = mappable;
let customization = [
waterInitialCustomization,
roadsInitialCustomization,
groundInitialCustomization,
buildingInitialCustomization
];
// Initialize the map
map = new MMap(
// Pass the link to the HTMLElement of the container
document.getElementById('app'),
// Pass the map initialization parameters
{location: LOCATION, showScaleInCopyrights: true}
);
// Create a function that we will call when changing the customization of the map through the control
function updateCustomization(updatedCustomization) {
console.log('updatedCustomization', updatedCustomization);
layer.update({customization: JSON.parse(updatedCustomization)});
}
// Add a map schema layer with a custom customization props
const layer = new MMapDefaultSchemeLayer({
customization
});
map.addChild(layer);
// Create a CustomizationControl to change the appearance of the road
const roadControl = new CustomizationControl({
title: 'Road',
initialValues: roadsInitialCustomization,
changeColorHandler: createChangeColorHandler(['road'], 'geometry'),
changeOpacityHandler: createChangeOpacityHandler(['road'], 'geometry'),
changeScaleHandler: createChangeScaleHandler(['road'], 'geometry'),
withDivider: true
});
// Create a CustomizationControl to change the appearance of the water
const waterControl = new CustomizationControl({
title: 'Water',
initialValues: waterInitialCustomization,
changeColorHandler: createChangeColorHandler(['water'], 'geometry'),
changeOpacityHandler: createChangeOpacityHandler(['water'], 'geometry'),
changeScaleHandler: createChangeScaleHandler(['water'], 'geometry'),
withDivider: true
});
// Create a CustomizationControl to change the appearance of the ground
const groundControl = new CustomizationControl({
title: 'Ground',
initialValues: groundInitialCustomization,
changeColorHandler: createChangeColorHandler(['landscape', 'admin', 'land', 'transit'], 'geometry'),
changeOpacityHandler: createChangeOpacityHandler(['landscape', 'admin', 'land', 'transit'], 'geometry')
});
// Create a CustomizationControl to change the appearance of the building
const buildingControl = new CustomizationControl({
title: 'Building',
initialValues: buildingInitialCustomization,
changeColorHandler: createChangeColorHandler(['building'], 'geometry'),
changeOpacityHandler: createChangeOpacityHandler(['building'], 'geometry')
});
// Create a shared container for custom CustomizationControl's and add it to the map
const controls = new MMapControls({position: 'top right', orientation: 'horizontal'});
map.addChild(controls);
// Add controls to the map
controls
.addChild(new MMapControl().addChild(waterControl).addChild(groundControl))
.addChild(new MMapControl().addChild(roadControl).addChild(buildingControl));
// A function that creates a handler for changing the color of geo objects
function createChangeColorHandler(controlTags: string[], controlElements: VectorCustomizationElements) {
return () => {
const color = generateColor();
const customizationObject = customization.find(
(item) => typeof item.tags === 'object' && JSON.stringify(item.tags.any) === JSON.stringify(controlTags)
);
if (customizationObject) {
customizationObject.stylers[0]['color'] = color;
} else {
const newTagObject: VectorCustomizationItem = {
tags: {any: controlTags},
elements: controlElements,
stylers: [{color}]
};
customization.push(newTagObject);
}
updateCustomization(JSON.stringify(customization, null, 2));
return color;
};
}
// A function that creates a handler to change the opacity of geo objects
function createChangeOpacityHandler(controlTags: string[], controlElements: VectorCustomizationElements) {
return (diff: number) => {
let opacity = 0.5;
const customizationObject = customization.find(
(item) => typeof item.tags === 'object' && JSON.stringify(item.tags.any) === JSON.stringify(controlTags)
);
if (!customizationObject) {
const newTagObject: VectorCustomizationItem = {
tags: {any: controlTags},
elements: controlElements,
stylers: [{opacity}]
};
customization.push(newTagObject);
} else if (customizationObject.stylers[0]['opacity'] === undefined) {
customizationObject.stylers[0]['opacity'] = opacity;
}
opacity = +(customizationObject.stylers[0]['opacity'] + diff).toFixed(1);
customizationObject.stylers[0]['opacity'] = opacity;
updateCustomization(JSON.stringify(customization, null, 2));
return opacity;
};
}
// A function that creates a handler to change the scale of geo objects
function createChangeScaleHandler(controlTags: string[], controlElements: VectorCustomizationElements) {
return (diff: number) => {
let scale = 1;
const customizationObject = customization.find(
(item) => typeof item.tags === 'object' && JSON.stringify(item.tags.any) === JSON.stringify(controlTags)
);
if (!customizationObject) {
const newTagObject: VectorCustomizationItem = {
tags: {any: controlTags},
elements: controlElements,
stylers: [{scale}]
};
customization.push(newTagObject);
} else if (customizationObject.stylers[0]['scale'] === undefined) {
customizationObject.stylers[0]['scale'] = scale;
}
scale = Math.floor(customizationObject.stylers[0]['scale'] + diff);
customizationObject.stylers[0]['scale'] = scale;
updateCustomization(JSON.stringify(customization, null, 2));
return scale;
};
}
}
</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" />
<link rel="stylesheet" href="../variables.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="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="react, typescript" type="text/babel">
import type {
VectorCustomization,
VectorCustomizationElements,
VectorCustomizationItem
} from '@mappable-world/mappable-types';
import {
buildingInitialCustomization,
CustomizationControl,
generateColor,
groundInitialCustomization,
roadsInitialCustomization,
waterInitialCustomization
} from './common';
import {LOCATION} from '../variables';
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} = reactify.module(mappable);
// Using mappable-rectify, we turn a custom CustomizationControl into a React component
const {CustomizationControl: CustomizationControlR} = reactify.module({CustomizationControl});
const {useCallback, useState} = React;
function App() {
const [customization, setCustomization] =
useState <
VectorCustomization >
[
waterInitialCustomization,
groundInitialCustomization,
roadsInitialCustomization,
buildingInitialCustomization
];
// A function that creates a handler for changing the color of geo objects
const createChangeColorHandler = useCallback(
(controlTags: string[], controlElements: VectorCustomizationElements) => {
return () => {
const color = generateColor();
setCustomization((customization) => {
const customizationObject = customization.find(
(item) =>
typeof item.tags === 'object' && JSON.stringify(item.tags.any) === JSON.stringify(controlTags)
);
if (customizationObject) {
customizationObject.stylers[0]['color'] = color;
} else {
const newTagObject: VectorCustomizationItem = {
tags: {any: controlTags},
elements: controlElements,
stylers: [{color}]
};
customization.push(newTagObject);
}
return [...customization];
});
return color;
};
},
[]
);
// A function that creates a handler to change the opacity of geo objects
const createChangeOpacityHandler = useCallback(
(controlTags: string[], controlElements: VectorCustomizationElements) => {
return (diff: number) => {
let opacity = 0.5;
setCustomization((customization) => {
const customizationObject = customization.find(
(item) =>
typeof item.tags === 'object' && JSON.stringify(item.tags.any) === JSON.stringify(controlTags)
);
if (!customizationObject) {
const newTagObject: VectorCustomizationItem = {
tags: {any: controlTags},
elements: controlElements,
stylers: [{opacity}]
};
customization.push(newTagObject);
} else if (customizationObject.stylers[0]['opacity'] === undefined) {
customizationObject.stylers[0]['opacity'] = opacity;
}
opacity = +(customizationObject.stylers[0]['opacity'] + diff).toFixed(1);
customizationObject.stylers[0]['opacity'] = opacity;
return [...customization];
});
return opacity;
};
},
[]
);
// A function that creates a handler to change the scale of geo objects
const createChangeScaleHandler = useCallback(
(controlTags: string[], controlElements: VectorCustomizationElements) => {
return (diff: number) => {
let scale = 1;
setCustomization((customization) => {
const customizationObject = customization.find(
(item) =>
typeof item.tags === 'object' && JSON.stringify(item.tags.any) === JSON.stringify(controlTags)
);
if (!customizationObject) {
const newTagObject: VectorCustomizationItem = {
tags: {any: controlTags},
elements: controlElements,
stylers: [{scale}]
};
customization.push(newTagObject);
} else if (customizationObject.stylers[0]['scale'] === undefined) {
customizationObject.stylers[0]['scale'] = scale;
}
scale = Math.floor(customizationObject.stylers[0]['scale'] + diff);
customizationObject.stylers[0]['scale'] = scale;
return [...customization];
});
return scale;
};
},
[]
);
return (
<MMap location={LOCATION} showScaleInCopyrights={true} ref={(x) => (map = x)}>
{/* Add a map scheme layer with a custom customization props */}
<MMapDefaultSchemeLayer customization={customization} />
{/* Add a shared container to the map for custom CustomizationControl's */}
<MMapControls position={'top right'} orientation={'horizontal'}>
<MMapControl>
{/* Add a CustomizationControl to the map to change the appearance of the water */}
<CustomizationControlR
initialValues={waterInitialCustomization}
title="Water"
changeColorHandler={createChangeColorHandler(['water'], 'geometry')}
changeOpacityHandler={createChangeOpacityHandler(['water'], 'geometry')}
changeScaleHandler={createChangeScaleHandler(['water'], 'geometry')}
withDivider
/>
{/* Add a CustomizationControl to the map to change the appearance of the ground */}
<CustomizationControlR
initialValues={groundInitialCustomization}
title="Ground"
changeColorHandler={createChangeColorHandler(
['landscape', 'admin', 'land', 'transit'],
'geometry'
)}
changeOpacityHandler={createChangeOpacityHandler(
['landscape', 'admin', 'land', 'transit'],
'geometry'
)}
/>
</MMapControl>
<MMapControl>
{/* Add a CustomizationControl to the map to change the appearance of the road */}
<CustomizationControlR
initialValues={roadsInitialCustomization}
title="Road"
changeColorHandler={createChangeColorHandler(['road'], 'geometry')}
changeOpacityHandler={createChangeOpacityHandler(['road'], 'geometry')}
changeScaleHandler={createChangeScaleHandler(['road'], 'geometry')}
withDivider
/>
{/* Add a CustomizationControl to the map to change the appearance of the building */}
<CustomizationControlR
initialValues={buildingInitialCustomization}
title="Building"
changeColorHandler={createChangeColorHandler(['building'], 'geometry')}
changeOpacityHandler={createChangeOpacityHandler(['building'], 'geometry')}
/>
</MMapControl>
</MMapControls>
</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" />
<link rel="stylesheet" href="../variables.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>
<!-- 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"></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" />
<link rel="stylesheet" href="../variables.css" />
</head>
<body>
<div id="app"></div>
</body>
</html>
.customizationControl {
padding: 8px 16px;
min-width: 200px;
display: flex;
flex-direction: column;
}
.customizationControl:last-child {
padding: 0 16px 8px 16px;
}
.customizationControl__title {
font-size: 16px;
height: 40px;
font-weight: 600;
align-content: center;
color: #050d33;
}
.customizationControl__section {
display: flex;
align-items: center;
justify-content: space-between;
height: 40px;
}
.customizationControl__sectionButtons {
width: 122px;
display: flex;
flex-direction: row;
gap: 4px;
}
.customizationControl_color__section {
display: flex;
align-items: center;
justify-content: space-between;
height: 40px;
}
.customizationControl__sectionTitle {
flex-basis: 50%;
color: #050d33;
}
.customizationControl__infoBlock {
background-color: rgba(92, 94, 102, 0.06);
width: 50px;
text-align: center;
border-radius: 8px;
align-content: center;
color: #050d33;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 32px;
}
.customizationControl__btn {
border: none;
cursor: pointer;
width: 32px;
height: 32px;
text-align: center;
padding: 8px;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 16px;
color: #050d33;
border-radius: 8px;
background-color: rgba(92, 94, 102, 0.06);
transition: background-color 0.2s;
background-repeat: no-repeat;
background-position: 50% 50%;
}
.customizationControl__btn:disabled {
opacity: 0.4;
}
.customizationControl_color__btn {
border: none;
cursor: pointer;
width: 122px;
height: 32px;
padding: 8px;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 16px;
color: #050d33;
border-radius: 8px;
text-align: left;
background-color: rgba(92, 94, 102, 0.06);
transition: background-color 0.2s;
background-image: url('./shuffle.svg');
background-repeat: no-repeat;
background-position: 95% 50%;
}
.customizationControl__btn:hover {
background-color: rgba(92, 94, 102, 0.12);
}
.customizationControl__btn:active {
background-color: rgba(92, 94, 102, 0.12);
}
.customizationControl_color__btn:hover {
background-color: rgba(92, 94, 102, 0.12);
}
.customizationControl_color__btn:active {
background-color: rgba(92, 94, 102, 0.12);
}
.customizationControl__btn:disabled {
cursor: not-allowed;
background-color: rgba(92, 94, 102, 0.04);
}
.customizationControl__btn.minus {
background-image: url('./minus.svg');
}
.customizationControl__btn.plus {
background-image: url('./plus.svg');
}
.divider {
margin: 8px 0 0 0;
width: 100%;
height: 1px;
background-color: rgba(92, 94, 102, 0.14);
border: none;
}
import {VectorCustomizationItem} from '@mappable-world/mappable-types/common/types/data-source-description';
export const roadsInitialCustomization: VectorCustomizationItem = {
tags: {
any: ['road']
},
elements: 'geometry',
stylers: [
{
color: '#4E4E4E'
}
]
};
export const waterInitialCustomization: VectorCustomizationItem = {
tags: {
any: ['water']
},
elements: 'geometry',
stylers: [
{
color: '#000000'
}
]
};
export const groundInitialCustomization: VectorCustomizationItem = {
tags: {
any: ['landscape', 'admin', 'land', 'transit']
},
elements: 'geometry',
stylers: [
{
color: '#212121'
}
]
};
export const buildingInitialCustomization: VectorCustomizationItem = {
tags: {
any: ['building']
},
elements: 'geometry',
stylers: [
{
color: '#757474'
}
]
};
// Function generates a random color in HEX format
export const generateColor = () => {
return '#' + Math.floor(Math.random() * 16777215).toString(16);
};
// Create a custom control to change the customization of the map
export let CustomizationControl = null;
interface CustomizationControlProps {
title: string;
initialValues: VectorCustomizationItem;
changeColorHandler?: () => void;
changeOpacityHandler?: (diff: number) => number;
changeScaleHandler?: (diff: number) => number;
withDivider: boolean;
}
// Wait for the api to load to access the entity system (MMapComplexEntity)
mappable.ready.then(() => {
class CustomizationControlClass extends mappable.MMapComplexEntity<CustomizationControlProps> {
private _element: HTMLDivElement;
private _detachDom: () => void;
constructor(props: CustomizationControlProps) {
super(props);
this._element = this._createElement(props);
}
// Creates a control's DOM element based on the passed properties
_createElement(props: CustomizationControlProps) {
const {title, changeColorHandler, changeOpacityHandler, changeScaleHandler, initialValues} = props;
const customizationElement = document.createElement('div');
customizationElement.classList.add('customizationControl');
const myControlTitle = document.createElement('div');
myControlTitle.classList.add('customizationControl__title');
myControlTitle.textContent = title;
myControlTitle.id = title;
customizationElement.appendChild(myControlTitle);
if (changeColorHandler) {
const colorSection = this._createColorControlSection(
'color',
changeColorHandler,
initialValues.stylers[0].color
);
customizationElement.appendChild(colorSection);
}
if (changeOpacityHandler) {
const opacitySection = this._createControlSection('opacity', changeOpacityHandler, 0.1, 0.5, [0, 1]);
customizationElement.appendChild(opacitySection);
}
if (changeScaleHandler) {
const scaleSection = this._createControlSection('scale', changeScaleHandler, 1, 1, [0, 10]);
customizationElement.appendChild(scaleSection);
}
if (props.withDivider) {
const divider = document.createElement('hr');
divider.classList.add('divider');
customizationElement.appendChild(divider);
}
return customizationElement;
}
_createControlSection(
title: string,
onClickHandler: (diff: number) => number,
diff: number = 0.1,
initialValue: number = 0.5,
minMax: number[]
) {
const [min, max] = minMax;
const section = document.createElement('div');
section.classList.add('customizationControl__section');
const sectionTitle = document.createElement('div');
sectionTitle.classList.add('customizationControl__sectionTitle');
sectionTitle.textContent = title;
section.appendChild(sectionTitle);
const sectionButtons = document.createElement('div');
sectionButtons.classList.add('customizationControl__sectionButtons');
const decrementButton = document.createElement('button');
decrementButton.classList.add('customizationControl__btn', 'minus');
decrementButton.addEventListener('click', function () {
const number = onClickHandler(-diff);
this.disabled = number === min;
(this.parentNode.lastChild as HTMLButtonElement).disabled = number === max;
this.parentNode.children.item(1).textContent = number.toString();
});
sectionButtons.appendChild(decrementButton);
const infoBlock = document.createElement('div');
infoBlock.classList.add('customizationControl__infoBlock');
infoBlock.textContent = initialValue.toString();
infoBlock.id = 'info';
sectionButtons.appendChild(infoBlock);
const incrementButton = document.createElement('button');
incrementButton.classList.add('customizationControl__btn', 'plus');
incrementButton.addEventListener('click', function () {
const number = onClickHandler(diff);
this.disabled = number === max;
(this.parentNode.firstChild as HTMLButtonElement).disabled = number === min;
this.parentNode.children.item(1).textContent = number.toString();
});
sectionButtons.appendChild(incrementButton);
section.appendChild(sectionButtons);
return section;
}
_createColorControlSection(title: string, onClickHandler: EventListener, initialValue: string) {
const section = document.createElement('div');
section.classList.add('customizationControl_color__section');
const sectionTitle = document.createElement('div');
sectionTitle.classList.add('customizationControl_color__sectionTitle');
sectionTitle.textContent = title;
section.appendChild(sectionTitle);
const sectionButton = document.createElement('button');
sectionButton.classList.add('customizationControl_color__btn');
sectionButton.textContent = initialValue;
sectionButton.addEventListener('click', function (event) {
const color = onClickHandler(event);
this.textContent = color as unknown as string;
});
section.appendChild(sectionButton);
return section;
}
// Handler for attaching the control to the map
_onAttach() {
this._detachDom = mappable.useDomContext(this, this._element, this._element);
}
// Handler for detaching control from the map
_onDetach() {
this._detachDom();
this._detachDom = null;
this._element = null;
}
}
CustomizationControl = CustomizationControlClass;
});
:root {
}
import type {MMapLocationRequest} from '@mappable-world/mappable-types';
export const LOCATION: MMapLocationRequest = {
center: [55.1666, 25.0628], // starting position [lng, lat]
zoom: 15.3 // starting zoom
};