Map customization

Open in CodeSandbox

<!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 {CustomizationOptions, MapMode, CustomizationElements, CustomizationItem} from '@mappable-world/mappable-types';
            import {
                buildingInitialCustomization,
                CustomizationControl,
                generateColor,
                groundInitialCustomization,
                roadsInitialCustomization,
                waterInitialCustomization,
                ACTIVE_BUTTON_COLOR,
                INACTIVE_BUTTON_COLOR,
                MODE,
                setLocationMode
            } 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, MMapControlButton} = mappable;
            
                let customization: CustomizationOptions = {
                    style: [
                        waterInitialCustomization,
                        groundInitialCustomization,
                        roadsInitialCustomization,
                        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, mode: MODE}
                );
            
                // 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);
            
                const modeControls = new MMapControls({position: 'top'});
                map.addChild(modeControls);
            
                // Create buttons for switching modes
                const vectorButton = new MMapControlButton({
                    text: 'Vector',
                    background: MODE !== 'vector' ? INACTIVE_BUTTON_COLOR : ACTIVE_BUTTON_COLOR,
                    onClick: () => {
                        changeMode('vector');
                    }
                });
            
                const rasterButton = new MMapControlButton({
                    text: 'Raster',
                    background: MODE !== 'raster' ? INACTIVE_BUTTON_COLOR : ACTIVE_BUTTON_COLOR,
                    onClick: () => {
                        changeMode('raster');
                    }
                });
            
                const threeDButton = new MMapControlButton({
                    text: '3d',
                    background: customization['render-3d'] === 'off' ? INACTIVE_BUTTON_COLOR : ACTIVE_BUTTON_COLOR,
                    onClick: () => {
                        customization['render-3d'] = customization['render-3d'] === 'off' ? undefined : 'off';
                        updateCustomization(JSON.stringify(customization, null, 2));
                        threeDButton.update({
                            background: customization['render-3d'] === 'off' ? INACTIVE_BUTTON_COLOR : ACTIVE_BUTTON_COLOR
                        });
                    }
                });
            
                function changeMode(mode: MapMode) {
                    map.update({
                        mode
                    });
                    setLocationMode(mode);
                    vectorButton.update({
                        background: mode !== 'vector' ? INACTIVE_BUTTON_COLOR : ACTIVE_BUTTON_COLOR
                    });
                    rasterButton.update({
                        background: mode !== 'raster' ? INACTIVE_BUTTON_COLOR : ACTIVE_BUTTON_COLOR
                    });
                }
            
                // Add buttons to controls
                modeControls.addChild(vectorButton).addChild(rasterButton).addChild(threeDButton);
            
                // 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: CustomizationElements) {
                    return () => {
                        const color = generateColor();
                        const customizationObject = customization.style.find(
                            (item) => typeof item.tags === 'object' && JSON.stringify(item.tags.any) === JSON.stringify(controlTags)
                        );
            
                        if (customizationObject) {
                            customizationObject.stylers[0]['color'] = color;
                        } else {
                            const newTagObject: CustomizationItem = {
                                tags: {any: controlTags},
                                elements: controlElements,
                                stylers: [{color}]
                            };
            
                            customization.style.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: CustomizationElements) {
                    return (diff: number) => {
                        let opacity = 0.5;
                        const customizationObject = customization.style.find(
                            (item) => typeof item.tags === 'object' && JSON.stringify(item.tags.any) === JSON.stringify(controlTags)
                        );
            
                        if (!customizationObject) {
                            const newTagObject: CustomizationItem = {
                                tags: {any: controlTags},
                                elements: controlElements,
                                stylers: [{opacity}]
                            };
                            customization.style.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: CustomizationElements) {
                    return (diff: number) => {
                        let scale = 1;
                        const customizationObject = customization.style.find(
                            (item) => typeof item.tags === 'object' && JSON.stringify(item.tags.any) === JSON.stringify(controlTags)
                        );
            
                        if (!customizationObject) {
                            const newTagObject: CustomizationItem = {
                                tags: {any: controlTags},
                                elements: controlElements,
                                stylers: [{scale}]
                            };
                            customization.style.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>

        <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 {MapMode, CustomizationOptions, CustomizationElements, CustomizationItem} from '@mappable-world/mappable-types';
            import {
                buildingInitialCustomization,
                CustomizationControl,
                generateColor,
                groundInitialCustomization,
                roadsInitialCustomization,
                waterInitialCustomization,
                ACTIVE_BUTTON_COLOR,
                INACTIVE_BUTTON_COLOR,
                MODE,
                setLocationMode
            } 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, MMapControlButton} = 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 [mode, setMode] = useState(MODE);
            
                    const [customization, setCustomization] = useState<CustomizationOptions>({
                        style: [
                            waterInitialCustomization,
                            groundInitialCustomization,
                            roadsInitialCustomization,
                            buildingInitialCustomization
                        ]
                    });
            
                    // A function that creates a handler for changing the color of geo objects
                    const createChangeColorHandler = useCallback(
                        (controlTags: string[], controlElements: CustomizationElements) => {
                            return () => {
                                const color = generateColor();
                                setCustomization((customization: CustomizationOptions) => {
                                    const customizationObject = customization.style.find(
                                        (item) =>
                                            typeof item.tags === 'object' &&
                                            JSON.stringify(item.tags.any) === JSON.stringify(controlTags)
                                    );
            
                                    if (customizationObject) {
                                        customizationObject.stylers[0]['color'] = color;
                                    } else {
                                        const newTagObject: CustomizationItem = {
                                            tags: {any: controlTags},
                                            elements: controlElements,
                                            stylers: [{color}]
                                        };
            
                                        customization.style.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: CustomizationElements) => {
                            return (diff: number) => {
                                let opacity = 0.5;
                                setCustomization((customization: CustomizationOptions) => {
                                    const customizationObject = customization.style.find(
                                        (item) =>
                                            typeof item.tags === 'object' &&
                                            JSON.stringify(item.tags.any) === JSON.stringify(controlTags)
                                    );
                                    if (!customizationObject) {
                                        const newTagObject: CustomizationItem = {
                                            tags: {any: controlTags},
                                            elements: controlElements,
                                            stylers: [{opacity}]
                                        };
                                        customization.style.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: CustomizationElements) => {
                            return (diff: number) => {
                                let scale = 1;
                                setCustomization((customization: CustomizationOptions) => {
                                    const customizationObject = customization.style.find(
                                        (item) =>
                                            typeof item.tags === 'object' &&
                                            JSON.stringify(item.tags.any) === JSON.stringify(controlTags)
                                    );
            
                                    if (!customizationObject) {
                                        const newTagObject: CustomizationItem = {
                                            tags: {any: controlTags},
                                            elements: controlElements,
                                            stylers: [{scale}]
                                        };
                                        customization.style.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;
                            };
                        },
                        []
                    );
            
                    const updateMode = useCallback((newMode: MapMode) => {
                        setMode(newMode);
                        setLocationMode(newMode);
                    }, []);
            
                    return (
                        <MMap location={LOCATION} showScaleInCopyrights={true} ref={(x) => (map = x)} mode={mode}>
                            {/* Add a map scheme layer with a custom customization props */}
                            <MMapDefaultSchemeLayer customization={customization} />
            
                            <MMapControls position={'top'}>
                                <MMapControlButton
                                    text={'Vector'}
                                    onClick={() => updateMode('vector')}
                                    background={mode !== 'vector' ? INACTIVE_BUTTON_COLOR : ACTIVE_BUTTON_COLOR}
                                />
                                <MMapControlButton
                                    text={'Raster'}
                                    onClick={() => updateMode('raster')}
                                    background={mode !== 'raster' ? INACTIVE_BUTTON_COLOR : ACTIVE_BUTTON_COLOR}
                                />
                                <MMapControlButton
                                    text={'3d'}
                                    onClick={() => setCustomization((customization: CustomizationOptions) => {
                                        customization['render-3d'] = customization['render-3d'] === 'off' ? undefined : 'off';
                                        return {...customization};
                                    })}
                                    background={customization['render-3d'] === 'off' ? INACTIVE_BUTTON_COLOR : ACTIVE_BUTTON_COLOR}
                                />
                            </MMapControls>
            
                            {/* 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>

        <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">
            import {
                buildingInitialCustomization,
                CustomizationControl,
                generateColor,
                groundInitialCustomization,
                roadsInitialCustomization,
                waterInitialCustomization,
                ACTIVE_BUTTON_COLOR,
                INACTIVE_BUTTON_COLOR,
                MODE,
                setLocationMode
            } from './common';
            import type {MapMode, CustomizationOptions} from '@mappable-world/mappable-types';
            import {LOCATION} from '../variables';
            
            window.map = null;
            
            async function main() {
                // For each object in the JS API, there is a Vue counterpart
                // To use the Vue version of the API, include the module @mappable-world/mappable-vuefy
                const [mappableVue] = await Promise.all([mappable.import('@mappable-world/mappable-vuefy'), mappable.ready]);
                const vuefy = mappableVue.vuefy.bindTo(Vue);
                const {MMap, MMapDefaultSchemeLayer, MMapControls, MMapControl, MMapControlButton} = vuefy.module(mappable);
            
                // Using mappable-vuefy, we turn a custom CustomizationControl into a Vue component
                const {CustomizationControl: CustomizationControlV} = vuefy.module({CustomizationControl});
            
                const customization = Vue.shallowRef<CustomizationOptions>({style: [
                    waterInitialCustomization,
                    groundInitialCustomization,
                    roadsInitialCustomization,
                    buildingInitialCustomization
                ]});
            
                // A function that creates a handler for changing the color of geo objects
                const createChangeColorHandler = (controlTags, controlElements) => {
                    return () => {
                        let color = generateColor();
                        // @ts-ignore
                        if (!customization.value.style.some((item) => JSON.stringify(item.tags.any) === JSON.stringify(controlTags))) {
                            const newTagObject = {
                                tags: {any: controlTags},
                                elements: controlElements,
                                stylers: [{color}]
                            };
                            customization.value.style.push(newTagObject);
                        } else {
                            const customizationObject = customization.value.style.find(
                                // @ts-ignore
                                (item) => JSON.stringify(item.tags.any) === JSON.stringify(controlTags)
                            );
                            customizationObject.stylers[0]['color'] = color;
                        }
                        customization.value = {...customization.value};
                        return color;
                    };
                };
            
                // A function that creates a handler to change the opacity of geo objects
                const createChangeOpacityHandler = (controlTags, controlElements) => {
                    return (diff) => {
                        let opacity = 0.5;
                        // @ts-ignore
                        if (!customization.value.style.some((item) => JSON.stringify(item.tags.any) === JSON.stringify(controlTags))) {
                            const newTagObject = {
                                tags: {any: controlTags},
                                elements: controlElements,
                                stylers: [{opacity}]
                            };
                            customization.value.style.push(newTagObject);
                        } else {
                            const customizationObject = customization.value.style.find(
                                // @ts-ignore
                                (item) => JSON.stringify(item.tags.any) === JSON.stringify(controlTags)
                            );
                            if (customizationObject.stylers[0]['opacity'] === undefined) {
                                customizationObject.stylers[0]['opacity'] = opacity;
                            }
                            opacity = +(customizationObject.stylers[0]['opacity'] + diff).toFixed(1);
                            customizationObject.stylers[0]['opacity'] = opacity;
                        }
                        customization.value = {...customization.value};
                        return opacity;
                    };
                };
            
                // A function that creates a handler to change the scale of geo objects
                const createChangeScaleHandler = (controlTags, controlElements) => {
                    return (diff) => {
                        let scale = 1;
                        // @ts-ignore
                        if (!customization.value.style.some((item) => JSON.stringify(item.tags.any) === JSON.stringify(controlTags))) {
                            const newTagObject = {
                                tags: {any: controlTags},
                                elements: controlElements,
                                stylers: [{scale}]
                            };
                            customization.value.style.push(newTagObject);
                        } else {
                            const customizationObject = customization.value.style.find(
                                // @ts-ignore
                                (item) => JSON.stringify(item.tags.any) === JSON.stringify(controlTags)
                            );
                            if (customizationObject.stylers[0]['scale'] === undefined) {
                                customizationObject.stylers[0]['scale'] = scale;
                            }
                            scale = Math.floor(customizationObject.stylers[0]['scale'] + diff);
                            customizationObject.stylers[0]['scale'] = scale;
                        }
                        customization.value = {...customization.value};
                        return scale;
                    };
                };
            
                const app = Vue.createApp({
                    setup() {
                        const refMap = (ref) => {
                            window.map = ref?.entity;
                        };
            
                        const onChange = (newCustomization) => {
                            customization.value = newCustomization;
                        };
            
                        const mode = Vue.ref(MODE);
            
                        const onChangeMode = (newMode: MapMode) => {
                            mode.value = newMode;
                            setLocationMode(newMode);
                        };
            
                        const onChange3Mode = () => {
                            customization.value['render-3d'] = customization.value['render-3d'] === 'off' ? undefined : 'off';
                            customization.value = {...customization.value};
                        };
            
                        return {
                            LOCATION,
                            mode,
                            refMap,
                            customization,
                            createChangeColorHandler,
                            createChangeOpacityHandler,
                            createChangeScaleHandler,
                            onChange,
                            onChangeMode,
                            onChange3Mode,
                            ACTIVE_BUTTON_COLOR,
                            INACTIVE_BUTTON_COLOR,
                            groundInitialCustomization,
                            waterInitialCustomization,
                            roadsInitialCustomization,
                            buildingInitialCustomization
                        };
                    },
                    components: {
                        MMap,
                        MMapDefaultSchemeLayer,
                        MMapControls,
                        MMapControl,
                        MMapControlButton,
                        CustomizationControlV
                    },
                    template: `
                  <!-- Initialize the map and pass initialization parameters -->
                  <MMap :location="LOCATION" :showScaleInCopyrights="true" :ref="refMap" :mode="mode">
                    <!-- Add a map scheme layer with a custom customization props -->
                    <MMapDefaultSchemeLayer :customization="customization"/>
                    <MMapControls position="top">
                      <MMapControlButton text="Vector" :background="mode !== 'vector' ? INACTIVE_BUTTON_COLOR : ACTIVE_BUTTON_COLOR" @click="() => onChangeMode('vector')" />
                      <MMapControlButton text="Raster" :background="mode !== 'raster' ? INACTIVE_BUTTON_COLOR : ACTIVE_BUTTON_COLOR" @click="() => onChangeMode('raster')" />
                      <MMapControlButton text="3d" :background="customization['render-3d'] === 'off' ? INACTIVE_BUTTON_COLOR : ACTIVE_BUTTON_COLOR" @click="onChange3Mode" />
                    </MMapControls>
                    <!-- 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 -->
                        <CustomizationControlV
                          title="Water"
                          :initialValues="waterInitialCustomization"
                          :changeColorHandler="createChangeColorHandler(['water'], 'geometry')"
                          :changeOpacityHandler="createChangeOpacityHandler(['water'], 'geometry')"
                          :changeScaleHandler="createChangeScaleHandler(['water'], 'geometry')"
                          :withDivider="true"
                        />
                        <!-- Add a CustomizationControl to the map to change the appearance of the ground -->
                        <CustomizationControlV
                          title="Ground"
                          :initialValues="groundInitialCustomization"
                          :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 -->
                        <CustomizationControlV
                          title="Road"
                          :initialValues="roadsInitialCustomization"
                          :changeColorHandler="createChangeColorHandler(['road'], 'geometry')"
                          :changeOpacityHandler="createChangeOpacityHandler(['road'], 'geometry')"
                          :changeScaleHandler="createChangeScaleHandler(['road'], 'geometry')"
                          :withDivider="true"
                        />
                        <!-- Add a CustomizationControl to the map to change the appearance of the building -->
                        <CustomizationControlV
                          title="Building"
                          :initialValues="buildingInitialCustomization"
                          :changeColorHandler="createChangeColorHandler(['building'], 'geometry')"
                          :changeOpacityHandler="createChangeOpacityHandler(['building'], 'geometry')"
                        />
                      </MMapControl>
                    </MMapControls>
                  </MMap>
                `
                });
            
                app.mount('#app');
            }
            
            main();
        </script>

        <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>
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
};
:root {
}
import type {CustomizationItem, MapMode} from '@mappable-world/mappable-types';

const search = new URLSearchParams(window.location.search);

export const MODE: MapMode =
    search.has('mode') && ['vector', 'raster'].includes(search.get('mode') as string)
        ? (search.get('mode') as MapMode)
        : 'auto';

export const roadsInitialCustomization: CustomizationItem = {
    tags: {
        any: ['road']
    },
    elements: 'geometry',
    stylers: [
        {
            color: '#4E4E4E'
        }
    ]
};

export const waterInitialCustomization: CustomizationItem = {
    tags: {
        any: ['water']
    },
    elements: 'geometry',
    stylers: [
        {
            color: '#000000'
        }
    ]
};

export const groundInitialCustomization: CustomizationItem = {
    tags: {
        any: ['landscape', 'admin', 'land', 'transit']
    },
    elements: 'geometry',
    stylers: [
        {
            color: '#212121'
        }
    ]
};

export const buildingInitialCustomization: CustomizationItem = {
    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)
            .padStart(6, '0')
    );
};

// Create a custom control to change the customization of the map
export let CustomizationControl = null;

interface CustomizationControlProps {
    title: string;
    initialValues: CustomizationItem;
    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;
});

export const ACTIVE_BUTTON_COLOR = '#ffffff';
export const INACTIVE_BUTTON_COLOR = '#a4a4a4';

export function setLocationMode(newMode: MapMode) {
    const newRelativePathQuery = window.location.pathname + '?mode=' + newMode;
    history.replaceState(null, '', newRelativePathQuery);
}
.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;
}