The arrow on the map
The example shows how to create a module for drawing arrows on the map based on the Polyline lines class.
The example creates two custom modules - one is responsible for the arrow object class, and the other includes the visual display of the arrow - a class implementing the IOverlay interface. In the example, the modules are located in the same file. For project development, we recommend putting the modules in separate files.
index.html
arrow.js
<!DOCTYPE html>
<html>
<head>
<title>Examples. The arrow on the map</title>
<meta
http-equiv="Content-Type"
content="text/html; charset=UTF-8"
/>
<!--
Set your own API-key. Testing key is not valid for other web-sites and services.
Get your API-key on the Developer Dashboard: https://developer.tech.yandex.ru/keys/
-->
<script
src="https://api-maps.yandex.ru/2.1/?lang=en_RU&apikey=<your API-key>"
type="text/javascript"
></script>
<script src="arrow.js" type="text/javascript"></script>
<style>
html,
body,
#map {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}
</style>
</head>
<body>
<div id="map"></div>
</body>
</html>
ymaps.ready(function () {
var myMap = new ymaps.Map(
"map",
{
center: [55.733835, 37.588227],
zoom: 5,
},
{
searchControlProvider: "yandex#search",
}
);
/**
* Custom modules are not appended to the ymaps namespace.
* So we can access them asynchronously through the ymaps.modules.require method.
*/
ymaps.modules.require(["geoObject.Arrow"], function (Arrow) {
var arrow = new Arrow(
[
[57.733835, 38.788227],
[55.833835, 35.688227],
],
null,
{
geodesic: true,
strokeWidth: 5,
opacity: 0.5,
strokeStyle: "shortdash",
}
);
myMap.geoObjects.add(arrow);
});
});
/**
* Class for creating an arrow on the map.
* It is a helper for creating a polyline with a special overlay.
* When using the modules in a real project, we recommend placing them in separate files.
*/
ymaps.modules.define(
"geoObject.Arrow",
["Polyline", "overlay.Arrow", "util.extend"],
function (provide, Polyline, ArrowOverlay, extend) {
/**
* @param {Number[][] | Object | ILineStringGeometry} geometry Geometry of the polyline.
* @param {Object} properties Polyline data.
* @param {Object} options Polyline options.
* Supports the same set of options as the ymaps.Polyline class.
* @param {Number} [options.arrowAngle=20] Angle in degrees between the main line and the lines of the arrow.
* @param {Number} [options.arrowMinLength=3] Minimum length of the arrow. If the length of the arrow is less than the minimum value, the arrow is not drawn.
* @param {Number} [options.arrowMaxLength=20] Maximum length of the arrow.
*/
var Arrow = function (geometry, properties, options) {
return new Polyline(
geometry,
properties,
extend({}, options, {
lineStringOverlay: ArrowOverlay,
})
);
};
provide(Arrow);
}
);
/**
* Class that implements the IOverlay interface.
* Gets the pixel geometry of the line as input and adds an arrow at the end of the line.
*/
ymaps.modules.define(
"overlay.Arrow",
[
"overlay.Polygon",
"util.extend",
"event.Manager",
"option.Manager",
"Event",
"geometry.pixel.Polygon",
],
function (
provide,
PolygonOverlay,
extend,
EventManager,
OptionManager,
Event,
PolygonGeometry
) {
var domEvents = [
"click",
"contextmenu",
"dblclick",
"mousedown",
"mouseenter",
"mouseleave",
"mousemove",
"mouseup",
"multitouchend",
"multitouchmove",
"multitouchstart",
"wheel",
],
/**
* @param {geometry.pixel.Polyline} pixelGeometry The line's pixel geometry.
* @param {Object} data The overlay data.
* @param {Object} options Overlay options.
*/
ArrowOverlay = function (pixelGeometry, data, options) {
// The .events and .options fields are mandatory for IOverlay.
this.events = new EventManager();
this.options = new OptionManager(options);
this._map = null;
this._data = data;
this._geometry = pixelGeometry;
this._overlay = null;
};
ArrowOverlay.prototype = extend(ArrowOverlay.prototype, {
// Implementing all the methods and events that the IOverlay interface requires.
getData: function () {
return this._data;
},
setData: function (data) {
if (this._data != data) {
var oldData = this._data;
this._data = data;
this.events.fire("datachange", {
oldData: oldData,
newData: data,
});
}
},
getMap: function () {
return this._map;
},
setMap: function (map) {
if (this._map != map) {
var oldMap = this._map;
if (!map) {
this._onRemoveFromMap();
}
this._map = map;
if (map) {
this._onAddToMap();
}
this.events.fire("mapchange", {
oldMap: oldMap,
newMap: map,
});
}
},
setGeometry: function (geometry) {
if (this._geometry != geometry) {
var oldGeometry = geometry;
this._geometry = geometry;
if (this.getMap() && geometry) {
this._rebuild();
}
this.events.fire("geometrychange", {
oldGeometry: oldGeometry,
newGeometry: geometry,
});
}
},
getGeometry: function () {
return this._geometry;
},
getShape: function () {
return null;
},
isEmpty: function () {
return false;
},
_rebuild: function () {
this._onRemoveFromMap();
this._onAddToMap();
},
_onAddToMap: function () {
/**
* As a trick to get self-intersections drawn correctly in a transparent polyline,
* we draw a polygon instead of a polyline.
* Each contour of the polygon will be responsible for a section of the line.
*/
this._overlay = new PolygonOverlay(
new PolygonGeometry(this._createArrowContours())
);
this._startOverlayListening();
/**
* This string will connect the two options managers.
* Options specified in the parent manager
* will be propagated to the child.
*/
this._overlay.options.setParent(this.options);
this._overlay.setMap(this.getMap());
},
_onRemoveFromMap: function () {
this._overlay.setMap(null);
this._overlay.options.setParent(null);
this._stopOverlayListening();
},
_startOverlayListening: function () {
this._overlay.events.add(domEvents, this._onDomEvent, this);
},
_stopOverlayListening: function () {
this._overlay.events.remove(
domEvents,
this._onDomEvent,
this
);
},
_onDomEvent: function (e) {
/**
* We listen to events from the child service overlay and
* throw them on an external class.
* This is to ensure that the "target" field was correctly defined
* in the event.
*/
this.events.fire(
e.get("type"),
new Event(
{
target: this,
/**
* Linking the original event with the current one,
* so that all of the data fields of child events are accessible in a derived event.
*/
},
e
)
);
},
_createArrowContours: function () {
var contours = [],
mainLineCoordinates =
this.getGeometry().getCoordinates(),
arrowLength = calculateArrowLength(
mainLineCoordinates,
this.options.get("arrowMinLength", 3),
this.options.get("arrowMaxLength", 20)
);
contours.push(
getContourFromLineCoordinates(mainLineCoordinates)
);
// We will draw the arrow only if the line length is not less than the length of the arrow.
if (arrowLength > 0) {
// Creating 2 more contours for arrows.
var lastTwoCoordinates = [
mainLineCoordinates[
mainLineCoordinates.length - 2
],
mainLineCoordinates[
mainLineCoordinates.length - 1
],
],
/**
* For convenience of calculation, we will rotate the arrow so that it is pointing along the y axis,
* and then turn the results back.
*/
rotationAngle = getRotationAngle(
lastTwoCoordinates[0],
lastTwoCoordinates[1]
),
rotatedCoordinates = rotate(
lastTwoCoordinates,
rotationAngle
),
arrowAngle =
(this.options.get("arrowAngle", 20) / 180) *
Math.PI,
arrowBeginningCoordinates =
getArrowsBeginningCoordinates(
rotatedCoordinates,
arrowLength,
arrowAngle
),
firstArrowCoordinates = rotate(
[
arrowBeginningCoordinates[0],
rotatedCoordinates[1],
],
-rotationAngle
),
secondArrowCoordinates = rotate(
[
arrowBeginningCoordinates[1],
rotatedCoordinates[1],
],
-rotationAngle
);
contours.push(
getContourFromLineCoordinates(firstArrowCoordinates)
);
contours.push(
getContourFromLineCoordinates(
secondArrowCoordinates
)
);
}
return contours;
},
});
function getArrowsBeginningCoordinates(
coordinates,
arrowLength,
arrowAngle
) {
var p1 = coordinates[0],
p2 = coordinates[1],
dx = arrowLength * Math.sin(arrowAngle),
y = p2[1] - arrowLength * Math.cos(arrowAngle);
return [
[p1[0] - dx, y],
[p1[0] + dx, y],
];
}
function rotate(coordinates, angle) {
var rotatedCoordinates = [];
for (var i = 0, l = coordinates.length, x, y; i < l; i++) {
x = coordinates[i][0];
y = coordinates[i][1];
rotatedCoordinates.push([
x * Math.cos(angle) - y * Math.sin(angle),
x * Math.sin(angle) + y * Math.cos(angle),
]);
}
return rotatedCoordinates;
}
function getRotationAngle(p1, p2) {
return Math.PI / 2 - Math.atan2(p2[1] - p1[1], p2[0] - p1[0]);
}
function getContourFromLineCoordinates(coords) {
var contour = coords.slice();
for (var i = coords.length - 2; i > -1; i--) {
contour.push(coords[i]);
}
return contour;
}
function calculateArrowLength(coords, minLength, maxLength) {
var linePixelLength = 0;
for (var i = 1, l = coords.length; i < l; i++) {
linePixelLength += getVectorLength(
coords[i][0] - coords[i - 1][0],
coords[i][1] - coords[i - 1][1]
);
if (linePixelLength / 3 > maxLength) {
return maxLength;
}
}
var finalArrowLength = linePixelLength / 3;
return finalArrowLength < minLength ? 0 : finalArrowLength;
}
function getVectorLength(x, y) {
return Math.sqrt(x * x + y * y);
}
provide(ArrowOverlay);
}
);