Редактор множества объектов
Пример демонстрирует оптимизацию отрисовки множественной геометрии, с возможностью выборочного редактирования.
Благодаря опции dynamic мы можем подсказывать API что собираемся часто обновлять данные в YMapFeatureDataSource.
В примере генерируется 1000 случайных треугольников. Они добавляются в YMapFeatureDataSource #1 с опцией dynamic: false.
API переводит такую геометрию в асинхронный режим отображения, он оптимизирован для отрисовки большого количества объектов.
Если изменить геометрию одного из треугольников, API асинхронно проводит тесселяцию и обновляет представление в видеокарте.
На деле это будет означать ощутимую для глаза задержку.
Поэтому в примере при клике на треугольник, он удаляется из YMapFeatureDataSource #1 и добавляется в YMapFeatureDataSource #2 с опцией dynamic: true.
API переводит геометрию режим синхронного обновления. Отрисовка такой геометрии при изменении, не происходит с задержкой.
При клике на другой треугольник для него операция повторяется, а первый возвращается в YMapFeatureDataSource #1.
Подробнее про опцию dynamic можно почитать в документации.
<!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://api-maps.yandex.ru/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 {BBOX, LOCATION} from '../variables';
import {FEATURE_COUNT, generateRandomTriangles} from './common';
window.map = null;
main();
async function main() {
// Waiting for all api elements to be loaded
await ymaps3.ready;
const {
YMap,
YMapDefaultSchemeLayer,
YMapFeatureDataSource,
YMapLayer,
YMapFeature,
YMapListener,
YMapDefaultFeaturesLayer,
YMapMarker,
YMapCollection
} = ymaps3;
// Initialize the map
map = new YMap(
// Pass the link to the HTMLElement of the container
document.getElementById('app'),
// Pass the map initialization parameters
{location: LOCATION, showScaleInCopyrights: true},
// Add a map scheme layer
[
new YMapDefaultSchemeLayer({}),
new YMapFeatureDataSource({id: 'features-vector', dynamic: false}),
new YMapFeatureDataSource({id: 'features-raster', dynamic: true}),
new YMapLayer({type: 'features', source: 'features-vector', zIndex: 1400}),
new YMapLayer({type: 'features', source: 'features-raster', zIndex: 1401}),
new YMapDefaultFeaturesLayer({})
]
);
const featuresCollection = new YMapCollection({});
const fakeFeaturesCollection = new YMapCollection({});
const pointsCollection = new YMapCollection({});
map.addChild(featuresCollection);
map.addChild(fakeFeaturesCollection);
map.addChild(pointsCollection);
const triangles = generateRandomTriangles(BBOX, FEATURE_COUNT);
for (const triangle of triangles) {
const feature = new YMapFeature({
...triangle
});
featuresCollection.addChild(feature);
}
let previousSelectedIndex = -1;
let selectedIndex = -1;
const listener = new YMapListener({
onFastClick: (object) => {
if (object.type === 'feature') {
const newSelectedIndex = triangles.findIndex(
(triangle: {id: string}) => triangle.id === object.entity.id
);
if (newSelectedIndex === -1 || selectedIndex === newSelectedIndex) {
return;
}
selectedIndex = newSelectedIndex;
updateSelected();
}
}
});
map.addChild(listener);
function updateSelected() {
if (!featuresCollection.children[selectedIndex]) {
return;
}
const featureCopy = new YMapFeature({
...triangles[selectedIndex],
id: `selected-${triangles[selectedIndex].id}`,
source: 'features-raster'
});
fakeFeaturesCollection.addChild(featureCopy);
featuresCollection.children[selectedIndex].update({
source: 'features-raster'
});
requestAnimationFrame(
((previousSelectedIndex) => {
fakeFeaturesCollection.removeChild(featureCopy);
featuresCollection.children[previousSelectedIndex]?.update({
source: 'features-vector'
});
}).bind(null, previousSelectedIndex)
);
previousSelectedIndex = selectedIndex;
updateSimpleEditor();
}
function updateSimpleEditor() {
if (!triangles[selectedIndex]) {
return;
}
if (!pointsCollection.children.length) {
for (let i = 0; i < 3; i++) {
const div = document.createElement('div');
div.classList.add('point');
const marker = new YMapMarker(
{
coordinates: triangles[selectedIndex].geometry.coordinates[0][i],
onDragMove: (newCoordinates) => {
triangles[selectedIndex].geometry.coordinates[0][i] = newCoordinates;
featuresCollection.children[selectedIndex]?.update({
geometry: {...triangles[selectedIndex].geometry}
});
updatePointsPositions();
return false;
},
draggable: true
},
div
);
pointsCollection.addChild(marker);
}
}
updatePointsPositions();
}
function updatePointsPositions() {
const selectedTriangle = triangles[selectedIndex];
const coordinates = selectedTriangle.geometry.coordinates[0];
pointsCollection.children.forEach((point, index) => {
point.update({
coordinates: coordinates[index]
});
});
}
}
</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" />
</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://api-maps.yandex.ru/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 {BBOX, LOCATION} from '../variables';
import {FEATURE_COUNT, type FeatureProps, generateRandomTriangles} from './common';
import type {DomEvent, DomEventHandlerObject, LngLat} from '@yandex/ymaps3-types';
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 @yandex/ymaps3-reactify
const [ymaps3React] = await Promise.all([ymaps3.import('@yandex/ymaps3-reactify'), ymaps3.ready]);
const reactify = ymaps3React.reactify.bindTo(React, ReactDOM);
const {
YMap,
YMapDefaultSchemeLayer,
YMapFeatureDataSource,
YMapLayer,
YMapFeature,
YMapListener,
YMapDefaultFeaturesLayer,
YMapMarker,
YMapCollection
} = reactify.module(ymaps3);
const {useMemo, useState, useCallback} = React;
function App() {
const [location, setLocation] = useState(LOCATION);
const [triangles, setTriangles] = useState<FeatureProps[]>(() => {
return generateRandomTriangles(BBOX, FEATURE_COUNT);
}, []);
const [selectedIndex, setSelectedIndex] = useState<number>(-1);
const onSelectFeature = useCallback(
(object: DomEventHandlerObject) => {
if (object.type === 'feature') {
const newSelectedIndex = triangles.findIndex(
(triangle: {id: string}) => triangle.id === object.entity.id
);
if (newSelectedIndex === -1 || selectedIndex === newSelectedIndex) {
return;
}
const newTriangles = triangles.map((triangle) => ({
...triangle,
source: object.entity.id === triangle.id ? 'features-raster' : 'features-vector'
}));
setTriangles(newTriangles);
setSelectedIndex(newSelectedIndex);
}
},
[selectedIndex, triangles]
);
return (
// Initialize the map and pass initialization parameters
<YMap location={reactify.useDefault(location)} showScaleInCopyrights={true} ref={(x) => (map = x)}>
<YMapDefaultSchemeLayer />
<YMapFeatureDataSource id={'features-vector'} dynamic={false} />
<YMapFeatureDataSource id={'features-raster'} dynamic={true} />
<YMapLayer type={'features'} source={'features-vector'} zIndex={1400} />
<YMapLayer type={'features'} source={'features-raster'} zIndex={1401} />
<YMapDefaultFeaturesLayer />
{triangles.map((feature: FeatureProps) => (
<YMapFeature key={feature.id} {...feature} />
))}
{triangles[selectedIndex] && (
<SimpleGeometryEditor
selectedIndex={selectedIndex}
triangles={triangles}
setTriangles={setTriangles}
/>
)}
<YMapListener onFastClick={onSelectFeature} />
</YMap>
);
}
function SimpleGeometryEditor({
selectedIndex,
triangles,
setTriangles
}: {
selectedIndex: number;
triangles: FeatureProps[];
setTriangles: (newTriangles: FeatureProps[]) => void;
}) {
return (
<YMapCollection>
{triangles[selectedIndex].geometry.coordinates[0].map((_, index) => (
<SimplePoint
key={`point_${index}`}
pointIndex={index}
featureIndex={selectedIndex}
triangles={triangles}
setTriangles={setTriangles}
/>
))}
</YMapCollection>
);
}
function SimplePoint({
featureIndex,
pointIndex,
triangles,
setTriangles
}: {
featureIndex: number;
pointIndex: number;
triangles: FeatureProps[];
setTriangles: (newTriangles: FeatureProps[]) => void;
}) {
const onDragMove = useCallback((newCoordinates: LngLat) => {
setTriangles(
triangles.map((triangle, triangleIndex) => {
if (triangleIndex === featureIndex) {
return {
...triangle,
geometry: {
...triangle.geometry,
coordinates: [
triangle.geometry.coordinates[0].map((p, i) => {
if (i === pointIndex) {
return newCoordinates;
}
return p;
})
]
}
} as FeatureProps;
}
return triangle;
})
);
return false;
}, [featureIndex, pointIndex, triangles]);
if (!triangles[featureIndex]) {
return null;
}
return (
<YMapMarker
id={`point_${pointIndex}`}
coordinates={triangles[featureIndex].geometry.coordinates[0][pointIndex]}
onDragMove={onDragMove}
draggable={true}
>
<div className={'point'} />
</YMapMarker>
);
}
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" />
</head>
<body>
<div id="app"></div>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<script crossorigin src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
<script crossorigin src="https://cdn.jsdelivr.net/npm/@babel/standalone@7/babel.min.js"></script>
<script src="https://api-maps.yandex.ru/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 {BBOX, LOCATION} from '../variables';
import {FEATURE_COUNT, generateRandomTriangles} from './common';
import type {DomEventHandlerObject, LngLat, YMapFeature as YMapfeatureI} from '@yandex/ymaps3-types';
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 @yandex/ymaps3-vuefy
const [ymaps3Vue] = await Promise.all([ymaps3.import('@yandex/ymaps3-vuefy'), ymaps3.ready]);
const vuefy = ymaps3Vue.vuefy.bindTo(Vue);
const {
YMap,
YMapDefaultSchemeLayer,
YMapFeatureDataSource,
YMapLayer,
YMapFeature,
YMapListener,
YMapDefaultFeaturesLayer,
YMapMarker,
YMapCollection
} = vuefy.module(ymaps3);
class SimplePoint extends ymaps3.YMapMarker {
private __index: number = 0;
constructor({
index,
coordinates,
onDragMoveIndex
}: {
index: number;
coordinates: LngLat;
onDragMoveIndex: (pointIndex: number, coordinates: LngLat) => void;
}) {
const div = document.createElement('div');
super(
{
coordinates,
onDragMove: (newCoorinates: LngLat) => {
onDragMoveIndex(index, newCoorinates);
return false;
},
draggable: true
},
div
);
div.classList.add('point');
}
}
const app = Vue.createApp({
components: {
YMap,
YMapDefaultSchemeLayer,
YMapFeatureDataSource,
YMapLayer,
YMapFeature,
YMapListener,
YMapDefaultFeaturesLayer,
YMapMarker,
YMapCollection,
SimplePoint: vuefy.entity(SimplePoint, {
index: Number,
coordinates: Object as Vue.PropType<LngLat>,
onDragMoveIndex: Function
})
},
setup() {
const refMap = (ref) => {
window.map = ref?.entity;
};
const triangles = Vue.shallowRef(generateRandomTriangles(BBOX, FEATURE_COUNT));
const selected = Vue.shallowRef({
index: -1,
coordinates: [] as LngLat[]
});
const onFastClick = (object: DomEventHandlerObject) => {
if (object.type === 'feature' && object.entity.geometry.type === 'Polygon') {
const newSelectedIndex = triangles.value.findIndex(
(triangle: {id: string}) => triangle.id === object.entity.id
);
if (newSelectedIndex === -1 || selected.value.index === newSelectedIndex) {
return;
}
selected.value = {
index: newSelectedIndex,
coordinates: [...(object.entity.geometry.coordinates[0] as LngLat[])]
};
}
};
const onDragMove = (index: number, coordinates: LngLat) => {
const coordinatesArray = [
...(triangles.value[selected.value.index].geometry.coordinates[0] as LngLat[])
];
coordinatesArray[index] = coordinates;
selected.value = {
...selected.value,
coordinates: coordinatesArray
};
triangles.value = [
...triangles.value.slice(0, selected.value.index),
{
...triangles.value[selected.value.index],
geometry: {
...triangles.value[selected.value.index].geometry,
coordinates: [coordinatesArray]
}
},
...triangles.value.slice(selected.value.index + 1)
];
};
return {LOCATION, refMap, triangles, onFastClick, selected, onDragMove};
},
template: `
<YMap :location="LOCATION" :showScaleInCopyrights="true" :ref="refMap">
<YMapDefaultSchemeLayer />
<YMapFeatureDataSource id='features-vector' :dynamic=false />
<YMapFeatureDataSource id='features-raster' :dynamic=true />
<YMapLayer type='features' source='features-vector' :zIndex=1400 />
<YMapLayer type='features' source='features-raster' :zIndex=1401 />
<YMapDefaultFeaturesLayer />
<YMapCollection>
<YMapFeature
v-for="(triangle, index) in triangles"
:key="triangle.id"
:source="index === selected.index ? 'features-raster' : 'features-vector'"
:geometry="triangle.geometry"
:style="triangle.style"
:id="triangle.id"
/>
</YMapCollection>
<YMapListener :onFastClick="onFastClick" />
<YMapCollection v-if="selected.index !== -1">
<SimplePoint
v-for="(coordinates, index) in selected.coordinates"
:key="index"
:index="index"
:coordinates="coordinates"
:onDragMoveIndex="onDragMove"
/>
</YMapCollection>
</YMap>`
});
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" />
</head>
<body>
<div id="app"></div>
</body>
</html>
import type {LngLatBounds, YMapLocationRequest} from '@yandex/ymaps3-types';
export const LOCATION: YMapLocationRequest = {
center: [37.623082, 55.75254], // starting position [lng, lat]
zoom: 9 // starting zoom
};
export const BBOX: LngLatBounds = [
[35.25003512499998, 56.368732574338814],
[39.99612887499998, 55.12641465498419]
];
import type {DrawingStyle, LngLat, LngLatBounds, PolygonGeometry} from '@yandex/ymaps3-types';
export const FEATURE_COUNT = /autotest/.test(location.href) ? 50 : 1000;
const seed = (s: number) => () => {
s = Math.sin(s) * 10000;
return s - Math.floor(s);
};
const rnd = seed(10000); // () => Math.random()
function rndColor() {
const rgb = [Math.floor(rnd() * 256), Math.floor(rnd() * 256), Math.floor(rnd() * 256)];
return `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
}
export type FeatureProps = {
id: string;
geometry: PolygonGeometry;
style: DrawingStyle;
source: 'features-vector' | 'features-raster';
};
export function generateRandomTriangles(bbox: LngLatBounds, count: number): FeatureProps[] {
const triangles: FeatureProps[] = [];
for (let i = 0; i < count; i++) {
const baseLng = rnd() * (bbox[1][0] - bbox[0][0]) + bbox[0][0];
const baseLat = rnd() * (bbox[1][1] - bbox[0][1]) + bbox[0][1];
const coordinates = [
[
[baseLng, baseLat] as LngLat,
[baseLng + rnd() * 0.8 - 0.4, baseLat + rnd() * 0.8 - 0.4] as LngLat,
[baseLng + rnd() * 0.8 - 0.4, baseLat + rnd() * 0.8 - 0.4] as LngLat
]
];
triangles.push({
id: `triangle-${i}`,
geometry: {type: 'Polygon', coordinates} as PolygonGeometry,
style: {
zIndex: i,
fill: rndColor(),
fillOpacity: 0.5,
stroke: [{width: 3, opacity: 1, color: rndColor()}]
},
source: 'features-vector'
});
}
return triangles;
}
.point {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #0066ff;
border: 1px solid #000;
position: absolute;
transform: translate(-50%, -50%);
cursor: grab;
}