Кастомизация карты
html
<yandex-map
:settings="{
location: {
center,
zoom,
},
theme,
showScaleInCopyrights: true,
}"
:width="width"
:height="height"
>
<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
title="Вода"
:enabled-controls="['color', 'opacity', 'scale']"
:change-handler="(type, diff) => changeColor(['water'], type, diff)"
/>
<layers-customization-control
title="Земля"
:enabled-controls="['color', 'opacity']"
:change-handler="(type, diff) => changeColor(['landscape', 'admin', 'land', 'transit'], type, diff)"
/>
<layers-customization-control
title="Дороги"
:enabled-controls="['color', 'opacity', 'scale']"
:change-handler="(type, diff) => changeColor(['road'], type, diff)"
/>
<layers-customization-control
title="Строения"
:enabled-controls="['color', 'opacity']"
:change-handler="(type, diff) => changeColor(['building'], type, diff)"
/>
</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>