Skip to content Skip to sidebar Skip to footer

How To Draw Arrows On Satellite View Map With Plotly

I'm trying to visualize wind direction and strength on a satellite view map with plotly. Working in Python but, I guess, this question is not platform specific. Below is a screensh

Solution 1:

Also struggling to draw arrows in a plotly Mapbox.

I am using a similar approach to @guidout answer to build the arrows manually, but using a single geojson layer (this avoids the performance issues of using multiple traces).

I am using a geojson MultiLineString layer.

A geojson MultiLineString has the following format (from Wikipedia):

{
    "type": "MultiLineString", 
    "coordinates": [
        [[10, 10], [20, 20], [10, 40]], <-- each of this can be used to draw an arrow[[40, 40], [30, 30], [40, 20], [30, 10]]
    ]
}

Code:

functionrotate(a, theta) {
    return [a[0]*Math.cos(theta) - a[1]*Math.sin(theta), a[0]*Math.sin(theta) + a[1]*Math.cos(theta)];
}

functioncreateArrow(a, b) {
    // a: source point // b: destination pointvar angle = Math.atan2(a[1]-b[1], a[0]-b[0]);
    var v = [b[0]-a[0], b[1]-a[1]]        // get direction vectorvar m = Math.sqrt(Math.pow(v[0], 2) + Math.pow(v[1], 2))  // modulevar u = [v[0]/m, v[1]/m]              // get unitary vector in the directionvar k = 0.2// how far to place the arrow endvar newb = [b[0]-u[0]*k, b[1]-u[1]*k] // place the arrow end a bit before the destinationvar s1 = rotate([0.02, 0.02], angle)  // "sides" of the arrow. Applied to a base vector in left direction <--, and rotated to match the correct anglevar s2 = rotate([0.02, -0.02], angle)
    return [a, newb, [newb[0]+s1[0], newb[1]+s1[1]], newb, [newb[0]+s2[0], newb[1]+s2[1]]]
}

//...var arrows = sourceDestinationPairs.map(x =>createArrow(x[0], x[1]));

var mapboxLayer = { 
    'sourcetype': 'geojson',
    'source': {
        "type": "MultiLineString", 
        "coordinates": arrows
    },
    'below': '', 'type': 'line', 
    'color': 'grey', 'opacity': 1, 
    'line': {'width': 2}
}
layout.mapbox.layers.push(mapboxLayer)

// ...Plotly.plot('mapDiv', data, layout)

enter image description here

Drawbacks:

  • Having to build the arrows manually
  • Zooming also zooms the arrows
  • A slight distortion in the arrows, depending on how the projection scales the longitude vs latitude coordinates ratio (acceptable with the default mapbox Mercator projection)

Solution 2:

I've looked as far as I can tell, but I couldn't find an example of an arrow on the map that achieves this. I don't have much experience in this direction of mine. I'll answer this as an example of not being able to control it as intended.

import plotly.graph_objects as go
fig = go.Figure(go.Scattermapbox(
    mode = "lines",
    lon = [10, 20],
    lat = [10,20],
    marker = {'size': 10}))
fig.add_annotation(
    x = 10,
    y = 10,
    xref = "x",
    yref = "y",
    axref = "x",
    ayref = "y",
    ax = 3,
    ay = 3,
    text = "sample arrow",
    font=dict(
        family='Courier New, monospace',
        size=20,
        color='red'
    ),
    showarrow = True,
    align='center',
    arrowhead=2,
    arrowsize=2,
    arrowwidth=2,
    arrowcolor='blue'
)
fig.update_layout(
    margin ={'l':0,'t':0,'b':0,'r':0},
    mapbox = {
        'center': {'lon': 10, 'lat': 10},
        'style': "stamen-terrain",
        'center': {'lon': 10, 'lat': 10},
        'zoom': 3})

fig.show()

enter image description here

Solution 3:

It is not possible to plot arrows as annotation on a scattermapbox and maintain the 3D locations of the arrows.

What I did was to generate 2 traces for each arrow. The first trace is composed of 2 points, start and end, while the second trace has 4 points, [end, tipA, tipB, end) and filled "onself".

windField.windvectorList.forEach((item)=>{this.TrackMapPlot_data.push({type:"scattermapbox",mode:"lines",lat: [item.startlat, item.stoplat],lon: [item.startlon, item.stoplon],line: { color:"rgb(48,151,255)", width:3}
        });this.TrackMapPlot_data.push({type:"scattermapbox",mode:"lines",fill:"toself",fillcolor:"rgb(48,151,255)",lat: [item.stoplat, item.tipalat, item.tipblat, item.stoplat ],lon: [item.stoplon, item.tipalon, item.tipblon, item.stoplon],line: { color:"rgb(48,151,255)", width:3}
        })});

To come up with tipA and tipB and have the arrow tip also scale with the arrow length I define:

  • arrow opening: alpha
  • arrow length as % of the total arrow length: d
  • use the geopy library in the python backend to generate the 3 additional points. Arrow end and the 2 arrow tips. enter image description here
def AddArrowTipsToWindVectors(wind_table: pd.DataFrame(), scale: float) -> pd.DataFrame():
    #wind_table = wind_table.loc[:100, :]
    # Make wind vectors
    arrow_angle = 20
    arrow_length = 0.2
    wind_vectors = list()
    wind_table['WindArrowTipALat'] = 0
    wind_table['WindArrowTipALon'] = 0
    wind_table['WindArrowTipBLat'] = 0
    wind_table['WindArrowTipBLon'] = 0

    for index, wind_row in wind_table.iterrows():
        arrow_delta = math.asin(arrow_length*math.sin(math.radians(arrow_angle/2))/(1-arrow_length))
        arrow_dist = wind_row['WindSpeed']*scale*(1-arrow_length)/math.cos(arrow_delta)
        arrow_tip_A = GetTerminalLocFromDistAndHeading(wind_row['StationLat'], wind_row['StationLon'], arrow_dist, wind_row['WindDirection'] + math.degrees(arrow_delta))
        wind_table.loc[index, 'WindArrowTipALat'] = arrow_tip_A[0]
        wind_table.loc[index, 'WindArrowTipALon'] = arrow_tip_A[1]
        arrow_tip_B = GetTerminalLocFromDistAndHeading(wind_row['StationLat'], wind_row['StationLon'], arrow_dist, wind_row['WindDirection'] - math.degrees(arrow_delta))
        wind_table.loc[index, 'WindArrowTipBLat'] = arrow_tip_B[0]
        wind_table.loc[index, 'WindArrowTipBLon'] = arrow_tip_B[1]

This is not a great solution but it works. HOWEVER, there is a big issue with plotly and mapbox. Unfortunately, when adding several traces everything becomes extremely slow as pointed out in the following github links: https://github.com/plotly/plotly.js/issues/3227https://github.com/plotly/plotly.js/issues/1535

The problem is not the number of points in a single trace but the number of traces. This makes plotly unusable for my application.

AGM solution

Angular Google Maps seems to work very well. https://angular-maps.com/guides/getting-started/ Here is a snippet that shows how I use it:

<agm-map
        [clickableIcons]="false"
        [disableDefaultUI]="true"
        [latitude]="trackDetails.mapboxlat"
        [longitude]="trackDetails.mapboxlon"
        [mapTypeId]="'satellite'"
        [zoom]="trackDetails.mapboxzoomsmall"
        (mapClick)="onChoseLocation($event)"
        >
    <agm-polyline *ngFor="let windvector of windField.windvectorList"
            [visible]="true" [strokeWeight]="2" [strokeColor]="'rgb(43,42,255)'">
        <agm-polyline-point
            [latitude]="windvector.startlat"
            [longitude]="windvector.startlon">
        </agm-polyline-point>
        <agm-polyline-point
            [latitude]="windvector.stoplat"
            [longitude]="windvector.stoplon">
        </agm-polyline-point>
        <agm-icon-sequence
                [fixedRotation]="false"
                [scale]="1.5"
                [path]="'FORWARD_OPEN_ARROW'"
                [fillColor]="'rgb(43,42,255)'">
        </agm-icon-sequence>
    </agm-polyline>
</agm-map>

Post a Comment for "How To Draw Arrows On Satellite View Map With Plotly"