<!DOCTYPE html>
<html>
    <head>
        <meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
        <link href='https://api.tiles.mapbox.com/mapbox-gl-js/v0.40.1/mapbox-gl.css' rel='stylesheet' />
        <link rel="stylesheet" href="style.css">
        <script src="https://api.tiles.mapbox.com/mapbox-gl-js/v0.40.1/mapbox-gl.js"></script>
        <script src="https://npmcdn.com/@turf/turf/turf.js"></script>
    </head>
    <body>
        <div id="map"></div>
        <script src="script.js"></script>
    </body>
</html>
var map = new mapboxgl.Map({
    container: 'map',
    style: {
        "version": 8,
        "sources": {
            "osm2vectortiles": {
                "type": "vector",
                "url": "https://osm2vectortiles.tileserver.com/v2.json"
            },
        },
        "layers": [
            {
                "id": "background",
                "type": "background",
                "paint": {
                    "background-color": "#41afa5"
                }
            },
            {
                "id": "water",
                "type": "fill",
                "source": "osm2vectortiles",
                "source-layer": "water",
                "filter": ["==", "$type", "Polygon"],
                "paint": {
                    "fill-color": "#3887be"
                }
            }
        ],
    },
    center: [-96, 37.8],
    zoom: 2,
    interactive: true
});

map.on('style.load', function (e) {
    map.addSource('markers', {
        "type": "geojson",
        "data": {
            "type": "FeatureCollection",
            "features": [{
                "type": "Feature",
                "geometry": {
                    "type": "Point",
                    "coordinates": [-77, 39]
                },
                "properties": {
                    "title": "A",
                    "marker-symbol": "default_marker"
                }
            }, {
                "type": "Feature",
                "geometry": {
                    "type": "Point",
                    "coordinates": [-122, 38]
                },
                "properties": {
                    "title": "B",
                    "marker-color": "#ff00ff",
                    "marker-symbol": "secondary_marker"
                }
            }]
        }
    });

    map.addLayer({
        "id": "markers",
        "source": "markers",
        "type": "circle",
        "paint": {
            "circle-radius": 10,
            "circle-color": "#007cbf"
        }
    });
    let popup = new mapboxgl.Popup({
        closeButton: false,
        closeOnClick: false
    });
    map.on("mouseenter", "markers", e => {
      map.getCanvas().style.cursor = "pointer";
      popup
          .setLngLat(map.unproject(e.point))
          .setHTML("<h3>" + e.features[0].properties.title + "</h3>")
          .addTo(map);
    });
    map.on("mouseleave", "markers", () => {
        map.getCanvas().style.cursor = "";
        popup.remove();
    });
    let line = {
        "type": "LineString",
        "coordinates": [
            [-77, 39],
            [-90, 50],
            [-122, 38],
        ],
    };
    map.addLayer({
        "id": "line",
        "source": {
            "type": "geojson",
            "data": {
                "type": "FeatureCollection",
                "features": [{
                    "type": "Feature",
                    "geometry": line,
                }],
            },
        },
        "type": "line",
        "paint": {
            "line-color": "yellow",
        }
    });
    var getDirectDistance = function (point1, point2) {
        let long1 = point1[0];
        let long2 = point2[0];
        let lat1 = point1[1];
        let lat2 = point2[1];
        let longDiff = long2 - long1;
        let latDiff = lat2 - lat1;
        return Math.sqrt(Math.pow(longDiff, 2) + Math.pow(latDiff, 2));
    };
    var getPointAndAngleAlongLineString = function (lineString, percentage) {
        let coords;
        if (line.type === 'Feature') {
            coords = line.geometry.coordinates;
        } else if (line.type === 'LineString') {
            coords = line.coordinates;
        } else {
            throw new Error('input must be a LineString Feature or Geometry');
        }
        if (coords.length < 2) {
            throw new Error('input must be a at least two points');
        }
        if (percentage < 0 || percentage > 1) {
            throw new Error("percentage should be between 0 and 1");
        }
        let distances = [];
        let totalDistance = 0;
        for (let i = 0; i < coords.length - 1; i++) {
            let dist = getDirectDistance(coords[i], coords[i + 1]);
            totalDistance += dist;
            distances.push([dist, totalDistance, [coords[i], coords[i + 1]]]);
        }
        let distForPoint = totalDistance * percentage;
        let segmentInfo = distForPoint === 0 ? distances[0] : distances.find(el => distForPoint > el[1] - el[0] && distForPoint <= el[1]);
        if (segmentInfo) {
            let dist = segmentInfo[0];
            let distPtFromSegStart = distForPoint - segmentInfo[1] + segmentInfo[0];
            let segmentStart = segmentInfo[2][0];
            let startLong = segmentStart[0];
            let startLat = segmentStart[1];
            let segmentEnd = segmentInfo[2][1];
            let endLong = segmentEnd[0];
            let endLat = segmentEnd[1];
            let longSign = endLong >= startLong ? 1 : -1;
            let latSign = endLat >= startLat ? 1 : -1;
            let pointLong = distPtFromSegStart * (endLong - startLong) / dist + startLong;
            let pointLat = distPtFromSegStart * (endLat - startLat) / dist + startLat;
            let angle = Math.acos((endLat - startLat) / dist);
            return {
                point: turf.point([pointLong, pointLat]),
                latAngle: angle,
                longSign: longSign,
                latSign: latSign,
            };
        } else {
            return null;
        }
    };
    var getPointFromAnotherPointWithDistanceAngleAndDirections = function (refPoint, distance, latAngle, longSign, latSign) {
        let refCoords = turf.getCoord(refPoint);
        let diffLong = distance * Math.sin(latAngle);
        let diffLat = distance * Math.cos(latAngle);
        return turf.point([
            refCoords[0] + longSign * diffLong,
            refCoords[1] + latSign * diffLat,
        ]);
    }
    let pointInfo = getPointAndAngleAlongLineString(line, 0.5);
    let centerPoint = pointInfo.point;
    let firstPoint = getPointFromAnotherPointWithDistanceAngleAndDirections(
        centerPoint,
        2,
        pointInfo.latAngle + Math.PI / 8,
        pointInfo.longSign,
        pointInfo.latSign
    );
    let lastPoint = getPointFromAnotherPointWithDistanceAngleAndDirections(
        centerPoint,
        2,
        - pointInfo.latAngle + Math.PI / 8,
        pointInfo.longSign,
        pointInfo.latSign
    );
    let directionLine = {
        "type": "LineString",
        "coordinates": [
            turf.getCoord(firstPoint),
            turf.getCoord(centerPoint),
            turf.getCoord(lastPoint),
        ],
    };
    map.addLayer({
        "id": "direction",
        "source": {
            "type": "geojson",
            "data": {
                "type": "FeatureCollection",
                "features": [{
                    "type": "Feature",
                    "geometry": directionLine,
                }],
            },
        },
        "type": "line",
        "paint": {
            "line-color": "red",
        },
    });
    map.addLayer({
        "id": "centerPoint",
        "source": {
            "type": "geojson",
            "data": {
                "type": "FeatureCollection",
                "features": [centerPoint],
            },
        },
        "type": "circle",
        "paint": {
            "circle-radius": 5,
            "circle-color": "magenta",
        },
    })
});
body {
    margin:0;
    padding:0;
}

#map {
    position:absolute;
    top:0;
    bottom:0;
    width:100%;
}