Multi-Features Editor
An example demonstrates the optimization of multiple geometry, with the possibility of selective editing.
Thanks to the option dynamic
We can prompt the API that we are going to often update data in YMapFeatureDataSource.
The example generates 1000 random triangles. They are added to YMapFeatureDataSource #1 with the dynamic: false option.
The API translates such geometry into an asynchronous display regimen, it is optimized for drawing a large number of objects.
If you change the geometry of one of the triangles, the API asynchronously carries out [https://en.wikipedia.org/wiki/Tessellation_(computer_graphics)) and renews the performance in the video card.
In fact, this will mean a tangible delay for the eye.
Therefore, in the example when clicking on the triangle, it is removed from YMapFeatureDataSource #1 and is added to YMapFeature #2 with the dynamic: true option.
API translates the geometry mode of synchronous update. The drawing of such geometry with a change does not occur with a delay.
When clicking on another triangle for it, the operation is repeated, and the first returns to YMapFeatureDataSource #1.
More information about the dynamic option can be read in documentation.
<!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;
}