The arrow on the map

Open in CodeSandbox

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.

<!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&amp;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);
    }
);