Skip to content

Кастомизация карты

html
<yandex-map
    :height="height"
    :settings="{
        location: {
            center,
            zoom,
        },
        theme,
        showScaleInCopyrights: true,
    }"
    :width="width"
>
    <yandex-map-default-scheme-layer :settings="{ customization }"/>

    <yandex-map-controls :settings="{ position: 'top right', orientation: 'vertical' }">
        <yandex-map-control>
            <div
                class="controls"
                :style="{ '--map-height': height }"
            >
                <layers-customization-control
                    :change-handler="(type, diff) => changeColor(['water'], type, diff)"
                    :enabled-controls="['color', 'opacity', 'scale']"
                    title="Вода"
                />
                <layers-customization-control
                    :change-handler="(type, diff) => changeColor(['landscape', 'admin', 'land', 'transit'], type, diff)"
                    :enabled-controls="['color', 'opacity']"
                    title="Земля"
                />
                <layers-customization-control
                    :change-handler="(type, diff) => changeColor(['road'], type, diff)"
                    :enabled-controls="['color', 'opacity', 'scale']"
                    title="Дороги"
                />
                <layers-customization-control
                    :change-handler="(type, diff) => changeColor(['building'], type, diff)"
                    :enabled-controls="['color', 'opacity']"
                    title="Строения"
                />
            </div>
        </yandex-map-control>
    </yandex-map-controls>
</yandex-map>
<textarea
    class="editor"
    :value="JSON.stringify(customization, undefined, 2)"
    @change="(e) => changeCustomization(e as InputEvent)"
/>
ts
import { YandexMap, YandexMapControl, YandexMapControls, YandexMapDefaultSchemeLayer } from 'vue-yandex-maps';
import type { CustomizationControls } from './LayersCustomizationControl.vue';
import LayersCustomizationControl from './LayersCustomizationControl.vue';
import type { VectorCustomization, VectorCustomizationItem } from '@yandex/ymaps3-types';
import { shallowRef, triggerRef } from 'vue';

const customization = shallowRef<VectorCustomization>([
    {
        tags: {
            any: ['water'],
        },
        elements: 'geometry',
        stylers: [
            {
                color: '#000000',
            },
        ],
    },
    {
        tags: {
            any: ['landscape', 'admin', 'land', 'transit'],
        },
        elements: 'geometry',
        stylers: [
            {
                color: '#212121',
            },
        ],
    },
    {
        tags: {
            any: ['road'],
        },
        elements: 'geometry',
        stylers: [
            {
                color: '#4E4E4E',
            },
        ],
    },
    {
        tags: {
            any: ['building'],
        },
        elements: 'geometry',
        stylers: [
            {
                color: '#757474',
            },
        ],
    },
]);

const changeCustomization = (event: InputEvent) => {
    try {
        customization.value = JSON.parse((event.target as HTMLTextAreaElement).value);
    }
    catch (e) {
        console.error(e);
    }
};

// Function generates a random color in HEX format
const generateColor = () => `#${ Math.floor(Math.random() * 16777215)
    .toString(16) }`;

const changeColor = (controlTags: string[], type: CustomizationControls, diff?: number) => {
    const customizationObject = customization.value.find(
        (item: any) => typeof item.tags === 'object' && JSON.stringify(item.tags.any) === JSON.stringify(controlTags),
    );

    if (type === 'color') {
        if (customizationObject) {
            (customizationObject.stylers as any[])[0].color = generateColor();
        }
        else {
            const newTagObject: VectorCustomizationItem = {
                tags: { any: controlTags },
                elements: 'geometry',
                stylers: [{ color: generateColor() }],
            };

            customization.value.push(newTagObject);
        }
    }
    else if (type === 'opacity') {
        if (!customizationObject) {
            const newTagObject: VectorCustomizationItem = {
                tags: { any: controlTags },
                elements: 'geometry',
                stylers: [{ opacity: 0.5 }],
            };
            customization.value.push(newTagObject);
        }
        else if (Array.isArray(customizationObject.stylers)) {
            if (customizationObject.stylers[0].opacity === undefined) {
                customizationObject.stylers[0].opacity = 0.5;
            }
            else {
                customizationObject.stylers[0].opacity = +(customizationObject.stylers[0].opacity + diff!).toFixed(
                    1,
                );

                if (customizationObject.stylers[0].opacity > 1) customizationObject.stylers[0].opacity = 1;
                if (customizationObject.stylers[0].opacity < 0) customizationObject.stylers[0].opacity = 0;
            }
        }
    }
    else if (type === 'scale') {
        if (!customizationObject) {
            const newTagObject: VectorCustomizationItem = {
                tags: { any: controlTags },
                elements: 'geometry',
                stylers: [{ scale: 2 }],
            };
            customization.value.push(newTagObject);
        }
        else if (Array.isArray(customizationObject.stylers)) {
            if (customizationObject.stylers[0].scale === undefined) {
                customizationObject.stylers[0].scale = 2;
            }
            else {
                customizationObject.stylers[0].scale = Math.floor(customizationObject.stylers[0].scale + diff!);
            }
        }
    }

    triggerRef(customization);
};
css
<style scoped>
.container {
  display: flex;
  height: 100%;
}

.controls {
  overflow: auto;
  max-height: calc(var(--map-height) - 24px);
}

.editor {
  width: 100%;
  min-height: 300px;
  margin-top: 20px;
  padding: 10px;
}
</style>
vue
<template>
    <div class="customization-control">
        <div class="customization-control_title">
            {{ title }}
        </div>
        <div
            v-for="section in getSections"
            :key="section.title"
            class="customization-control_section"
        >
            <div class="customization-control_section_title">
                {{ section.title }}
            </div>
            <div
                v-for="(btn, btnIndex) in section.buttons"
                :key="btn"
                class="customization-control_section_btn"
                @click="section.handlers[btnIndex]()"
            >
                {{ btn }}
            </div>
        </div>
    </div>
</template>

<script setup lang="ts">
import { computed } from 'vue';

export type CustomizationControls = 'color' | 'opacity' | 'scale';

interface CustomizationControlProps {
    title: string;
    enabledControls: CustomizationControls[];
    changeHandler: (control: CustomizationControls, diff?: number) => void;
}

type CustomizationControlSection = { title: string } & (
    {
        buttons: [string];
        handlers: [() => void];
    } | {
        buttons: [string, string];
        handlers: [() => void, () => void];
    });

const props = defineProps<CustomizationControlProps>();

const getSections = computed(() => {
    const sections: CustomizationControlSection[] = [];

    if (props.enabledControls.includes('color')) {
        sections.push({
            title: 'Цвет:',
            buttons: ['Случайный'],
            handlers: [() => props.changeHandler('color')],
        });
    }

    if (props.enabledControls.includes('opacity')) {
        sections.push({
            title: 'Прозрачность:',
            buttons: ['-', '+'],
            handlers: [() => props.changeHandler('opacity', -0.1), () => props.changeHandler('opacity', 0.1)],
        });
    }

    if (props.enabledControls.includes('scale')) {
        sections.push({
            title: 'Увеличение:',
            buttons: ['-', '+'],
            handlers: [() => props.changeHandler('scale', -1), () => props.changeHandler('scale', 1)],
        });
    }

    return sections;
});
</script>

<style scoped>
.customization-control {
  padding: 10px 0;
  margin: 0 15px;

  display: flex;
  flex-direction: column;
  gap: 15px;
}

.customization-control:not(:first-child) {
  border-top: 1px solid rgba(170, 170, 170, 0.15);
}

.customization-control_title {
  text-align: center;

  font-size: 18px;
  font-weight: 600;
}

.customization-control_section {
  display: flex;
  align-items: center;
  gap: 5px;
}

.customization-control_section_title {
  flex-basis: 50%;
}

.customization-control_section_btn {
  cursor: pointer;

  padding: 8px;

  color: rgb(255, 255, 255);
  border-radius: 8px;
  background-color: rgba(0, 122, 252, 0.9);
  transition: background-color 0.2s;
  user-select: none;
}

.customization-control_section_btn:hover {
  background-color: rgb(0, 110, 252);
}

.customization-control_section_btn:active {
  background-color: rgb(0, 122, 252);
}
</style>

Сделано с ♥ под лицензией MIT.