Кастомные активные области карты
В этом примере показана карта железных дорог на основе OpenRailwayMap.
Карта загружает данные с сервера в виде стандартных тайлов, а также использует активные области.
Эти области позволяют получать дополнительную информацию: если кликнуть на любой объект карты (например, станцию или вокзал), во всплывающем окне отобразятся детали — данные об объекте, которые карта автоматически запрашивает с сервера.
Данные получены с помощью сервиса Overpass Turbo.
Использовался следующий запрос лля получения данных железнодорожных станций:
[out:json][timeout:25];
{{geocodeArea:{{City}}}}->.searchArea;
(
node["railway"="station"](area.searchArea);
node["railway"="halt"](area.searchArea);
node["railway"="tram_stop"](area.searchArea);
);
out geom;
Где {{City}} нужно заменить на название города, где нужно выполнить поиск.
vanilla.ts
react.ts
vue.ts
layer.ts
tile-data-server.ts
import {setValue} from '../common';
import {dataSourceProps, layerProps} from '../layer';
import {LOCATION, ZOOM_RANGE} from '../variables';
window.map = null;
main();
async function main() {
// Waiting for all api elements to be loaded
await ymaps3.ready;
const {WebMercator} = await ymaps3.import('@yandex/ymaps3-web-mercator-projection');
const {YMapPopupMarker} = await ymaps3.import('@yandex/ymaps3-default-ui-theme');
const {YMap, YMapDefaultSchemeLayer, YMapTileDataSource, YMapLayer, YMapListener, YMapDefaultFeaturesLayer} =
ymaps3;
const projection = new WebMercator();
// Initialize the map
map = new YMap(
// Pass the link to the HTMLElement of the container
document.getElementById('app'),
// Pass the map initialization location and the Mercator projection used to represent the Earth's surface on a plane
{location: LOCATION, showScaleInCopyrights: true, zoomRange: ZOOM_RANGE, projection},
[
// Adding our own data source
new YMapTileDataSource(dataSourceProps),
// Adding a layer that will display data from `dataSource`
new YMapLayer(layerProps),
// Add a map scheme layer
new YMapDefaultSchemeLayer({}),
new YMapDefaultFeaturesLayer({})
]
);
let currentProperties = {};
const infoPopup = new YMapPopupMarker({
coordinates: [0, 0],
blockBehaviors: true,
content: () => {
const container = document.createElement('div');
container.classList.add('info');
const jsonElement = document.createElement('pre');
const {name, '@id': id} = currentProperties as {name: string; '@id': string};
const preparedProperties = {id, name};
jsonElement.textContent = JSON.stringify(preparedProperties, null, 2);
container.appendChild(jsonElement);
return container;
},
position: 'top'
});
const listener = new YMapListener({
layer: layerProps.id,
onClick: (object, {coordinates}) => {
map.removeChild(infoPopup);
if (object && object.type === 'hotspot') {
currentProperties = object.entity.properties;
console.log(currentProperties);
infoPopup.update({coordinates});
map.addChild(infoPopup);
setValue(currentProperties);
}
},
onActionStart: () => {
map.removeChild(infoPopup);
}
});
map.addChild(listener);
}
import type {DomEventHandler, LngLat} from '@yandex/ymaps3-types';
import {setValue} from '../common';
import {dataSourceProps, layerProps} from '../layer';
import {LOCATION, ZOOM_RANGE} 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 @yandex/ymaps3-reactify
const [ymaps3React] = await Promise.all([ymaps3.import('@yandex/ymaps3-reactify'), ymaps3.ready]);
const reactify = ymaps3React.reactify.bindTo(React, ReactDOM);
const {WebMercator} = await ymaps3.import('@yandex/ymaps3-web-mercator-projection');
const {YMapPopupMarker} = reactify.module(await ymaps3.import('@yandex/ymaps3-default-ui-theme'));
const {YMap, YMapDefaultSchemeLayer, YMapTileDataSource, YMapLayer, YMapListener, YMapDefaultFeaturesLayer} =
reactify.module(ymaps3);
const projection = new WebMercator();
function App() {
const [showPopup, setShowPopup] = React.useState(false);
const [currentProperties, setCurrentProperties] = React.useState({});
const [popupCoordinates, setPopupCoordinates] = React.useState<LngLat>([0, 0]);
const onCLickMap: DomEventHandler = React.useCallback(
(object, {coordinates}) =>
React.startTransition(() => {
setShowPopup(false);
if (object && object.type === 'hotspot') {
setCurrentProperties(object.entity.properties);
console.log(object.entity.properties);
setPopupCoordinates(coordinates);
setShowPopup(true);
setValue(object.entity.properties);
}
}),
[currentProperties]
);
const onActionStartMap = React.useCallback(() => {
setShowPopup(false);
}, []);
const createPopupContent = React.useCallback(() => {
const {name, '@id': id} = currentProperties as {name: string; '@id': string};
const preparedProperties = {id, name};
return (
<div className="info">
<pre>{JSON.stringify(preparedProperties, null, 2)}</pre>
</div>
);
}, [currentProperties]);
return (
// Initialize the map and pass initialization location and the Mercator projection used to represent the Earth's surface on a plane
<YMap
location={reactify.useDefault(LOCATION)}
showScaleInCopyrights={true}
zoomRange={ZOOM_RANGE}
projection={projection}
ref={(x) => (map = x)}
>
{/* Adding our own data source */}
<YMapTileDataSource {...dataSourceProps} />
{/* Adding a layer that will display data from `dataSource`s */}
<YMapLayer {...layerProps} />
<YMapDefaultSchemeLayer />
<YMapDefaultFeaturesLayer />
<YMapListener layer={layerProps.id} onClick={onCLickMap} onActionStart={onActionStartMap} />
{showPopup && (
<YMapPopupMarker
coordinates={popupCoordinates}
content={createPopupContent}
position="top"
blockBehaviors={true}
/>
)}
</YMap>
);
}
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('app')
);
}
import type {DomEventHandler, LngLat} from '@yandex/ymaps3-types';
import {setValue} from '../common';
import {dataSourceProps, layerProps} from '../layer';
import {LOCATION, ZOOM_RANGE} 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 @yandex/ymaps3-vuefy
const [ymaps3Vue] = await Promise.all([ymaps3.import('@yandex/ymaps3-vuefy'), ymaps3.ready]);
const vuefy = ymaps3Vue.vuefy.bindTo(Vue);
const {WebMercator} = await ymaps3.import('@yandex/ymaps3-web-mercator-projection');
const {YMapPopupMarker} = vuefy.module(await ymaps3.import('@yandex/ymaps3-default-ui-theme'));
const {YMap, YMapDefaultSchemeLayer, YMapTileDataSource, YMapLayer, YMapListener, YMapDefaultFeaturesLayer} =
vuefy.module(ymaps3);
const app = Vue.createApp({
components: {
YMap,
YMapDefaultSchemeLayer,
YMapTileDataSource,
YMapLayer,
YMapListener,
YMapDefaultFeaturesLayer,
YMapPopupMarker
},
setup() {
const refMap = (ref) => {
window.map = ref?.entity;
};
const showPopup = Vue.ref(false);
const currentProperties = Vue.ref({});
const jsonCurrentProperties = Vue.computed(() => {
const {name, '@id': id} = currentProperties.value as {name: string; '@id': string};
const preparedProperties = {id, name};
return JSON.stringify(preparedProperties, null, 2);
});
const popupCoordinates = Vue.ref<LngLat>([0, 0]);
const projection = new WebMercator();
const onCLickMap: DomEventHandler = (object, {coordinates}) => {
showPopup.value = false;
if (object && object.type === 'hotspot') {
currentProperties.value = object.entity.properties;
console.log(object.entity.properties);
popupCoordinates.value = coordinates;
showPopup.value = true;
setValue(object.entity.properties);
}
};
const onActionStartMap = () => {
showPopup.value = false;
};
return {
LOCATION,
ZOOM_RANGE,
refMap,
showPopup,
jsonCurrentProperties,
popupCoordinates,
projection,
dataSourceProps,
layerProps,
onCLickMap,
onActionStartMap
};
},
template: `
<!--Initialize the map and pass initialization location and the Mercator projection used to represent the Earth's surface on a plane-->
<YMap :location="LOCATION" :projection="projection" :showScaleInCopyrights="true" :zoomRange="ZOOM_RANGE" :ref="refMap">
<!--Adding our own data source-->
<YMapTileDataSource v-bind="dataSourceProps" />
<!--Adding a layer that will display data from \`dataSource\`s-->
<YMapLayer v-bind="layerProps" />
<YMapDefaultSchemeLayer />
<YMapDefaultFeaturesLayer />
<YMapListener :layer="layerProps.id", :onClick="onCLickMap" :onActionStart="onActionStartMap" />
<YMapPopupMarker
v-if="showPopup"
:coordinates="popupCoordinates"
position="top"
:blockBehaviors="true">
<template #content>
<div class="info">
<pre>{{ jsonCurrentProperties }}</pre>
</div>
</template>
</YMapPopupMarker>
</YMap>`
});
app.mount('#app');
}
main();
import type {YMapLayerProps, YMapTileDataSourceProps} from '@yandex/ymaps3-types';
import {fetchHotspotData, DEFAULT_TILE_SIZE} from './tile-data-server';
export const dataSourceProps: YMapTileDataSourceProps = {
id: 'custom',
copyrights: ['© OpenRailwayMap contributors'],
raster: {
type: 'ground',
size: DEFAULT_TILE_SIZE,
/*
fetchTile is called to get data for displaying a custom tile
This method can be of several variants:
1) x y z placeholders for tile coordinates
2) method that returns final url
3) method that fetches tile manually
In this example, we use option 1
*/
fetchTile: 'https://tiles.openrailwaymap.org/standard/z/x/y.png',
fetchHotspots: async (x, y, zoom) => {
const hotspots = await fetchHotspotData(x, y, zoom);
return hotspots;
}
},
zoomRange: {min: 0, max: 19},
clampMapZoom: true
};
/*
A text identifier is used to link the data source and the layer.
Be careful, the identifier for the data source is set in the id field,
and the source field is used when transferring to the layer
*/
export const layerProps: YMapLayerProps = {
id: 'customLayer',
source: 'custom',
type: 'ground',
options: {
raster: {
awaitAllTilesOnFirstDisplay: true
}
}
};
import {BBox, FeatureCollection, Point} from '@turf/helpers';
import type {Hotspot, LngLatBounds, PixelCoordinates, WorldBounds, WorldCoordinates} from '@yandex/ymaps3-types';
import type {WebMercator} from '@yandex/ymaps3-web-mercator-projection';
import {GEOJSON_URL} from './variables';
let featureCollection: FeatureCollection<Point>;
let projection: WebMercator;
export const DEFAULT_TILE_SIZE = 256;
const DEFAULT_HOTSPOT_RADIUS = 26;
const serverReady: Promise<void> = Promise.all([
fetch(GEOJSON_URL).then((response) => response.json()),
ymaps3.import('@yandex/ymaps3-web-mercator-projection'),
ymaps3.ready
]).then(([geojson, {WebMercator}]) => {
featureCollection = geojson;
projection = new WebMercator();
});
/**
* Simulating the processing of a request to a real backend
* @param tx tile x number
* @param ty tile y number
* @param zoom map zoom
*/
export async function fetchHotspotData(tx: number, ty: number, zoom: number): Promise<Hotspot[]> {
await serverReady;
const tileWorldBounds = tileToWorld(tx, ty, zoom);
const tileLngLatBounds = tileWorldBounds.map((worldCoordinate) =>
projection.fromWorldCoordinates(worldCoordinate)
) as LngLatBounds;
const tileFeatureCollection: FeatureCollection<Point> = getPointsInBounds(featureCollection, tileLngLatBounds);
return getRendererHotspots(tileFeatureCollection, tileLngLatBounds); // function create renderer hotspots (with pixel coordinates)
}
/**
* Convert tile coordinates to special world coordinates.
* It's not a real some standard, but it's used in our JS API.
*/
function tileToWorld(tx: number, ty: number, tz: number): WorldBounds {
const ntiles = 2 ** tz;
const ts = (1 / ntiles) * 2;
const x = (tx / ntiles) * 2 - 1;
const y = -((ty / ntiles) * 2 - 1);
return [
{x, y},
{x: x + ts, y: y - ts}
];
}
/**
* Get points in bounds.
* @param featureCollection - feature collection with points
* @param bounds - lnglat bounds
* @returns feature collection with points in bounds
*/
function getPointsInBounds(
featureCollection: FeatureCollection<Point>,
bounds: LngLatBounds
): FeatureCollection<Point> {
const tilePolygon = turf.bboxPolygon(bounds.flat() as BBox);
return turf.pointsWithinPolygon(featureCollection, tilePolygon) as FeatureCollection<Point>;
}
function getRendererHotspots(
tileFeatureCollection: FeatureCollection<Point>,
tileLngLatBounds: LngLatBounds
): Hotspot[] {
const [topLeftLngLat, bottomRightLngLat] = tileLngLatBounds;
return tileFeatureCollection.features.map<Hotspot>((feature) => {
const {geometry, properties} = feature;
const center: PixelCoordinates = {
x:
((geometry.coordinates[0] - topLeftLngLat[0]) / (bottomRightLngLat[0] - topLeftLngLat[0])) *
DEFAULT_TILE_SIZE,
y:
((geometry.coordinates[1] - topLeftLngLat[1]) / (bottomRightLngLat[1] - topLeftLngLat[1])) * DEFAULT_TILE_SIZE
};
return {
type: 'rendered',
feature: {id: feature.id.toString(), type: feature.type, properties, geometry},
geometry: {
type: 'Polygon',
coordinates: [
[
{x: center.x - DEFAULT_HOTSPOT_RADIUS, y: center.y - DEFAULT_HOTSPOT_RADIUS},
{x: center.x + DEFAULT_HOTSPOT_RADIUS, y: center.y - DEFAULT_HOTSPOT_RADIUS},
{x: center.x + DEFAULT_HOTSPOT_RADIUS, y: center.y + DEFAULT_HOTSPOT_RADIUS},
{x: center.x - DEFAULT_HOTSPOT_RADIUS, y: center.y + DEFAULT_HOTSPOT_RADIUS}
]
]
}
};
});
}