Search code examples
pythonleafletfolium

Dynamic styling of lines in folium


I have been trying to make sense out of TimestampedGeoJson plugin from folium.

I want to draw lines that change their colour over time. At the moment, what I do is to completely redraw a line every time I need to change the color, with the massive overhead that entails.

Another issue is how to specify the time in the features. At the moment, I have this example:

import folium
from folium.plugins import TimestampedGeoJson

m = folium.Map(
    location=[42.80491692, -4.62577249],
    zoom_start=10
)

data = [
{
    'coordinates': [
        [-4.018876661, 43.11843382],
        [-4.856537491, 42.82202193],
    ],
    'dates': [
        '2017-06-02T00:00:00',
        '2017-06-02T00:10:00'
    ],
    'color': 'red'
},
{
    'coordinates': [
        [-4.018876661, 43.11843382],
        [-4.856537491, 42.82202193],
    ],
    'dates': [
        '2017-06-02T00:00:00',
        '2017-06-02T00:20:00'
    ],
    'color': 'blue'
},
]

features = [
    {
        'type': 'Feature',
        'geometry': {
            'type': 'LineString',
            'coordinates': d['coordinates'],
        },
        'properties': {
            'times': d['dates'],
            'style': {
                'color': d['color'],
                'weight': d['weight'] if 'weight' in d else 5
            }
        }
    }
    for d in data
]

TimestampedGeoJson({
    'type': 'FeatureCollection',
    'features': features,
}, period='PT1M', add_last_point=True).add_to(m)

m.save('dynamic4.html')

To me, the first date does not make any sense, but apparently it is required because otherwise the browser will not draw anything.

So:

a) How can I change the style without redrawing the lines? b) What does the time mean? How can I specify a consistent time sequence?


Solution

  • I will first try to address your questions individually and I'm putting a full solution of what I would do in the end. essentially:

    1. change the TimestampedGeoJson _template variable to change the style_function and it will enable it to make style dinamic
    2. make sure you have one timestep per coordinates in the TimestampedGeoJson data
    3. For avoiding confusion try to not overlap features or have features missing data of in a certain timestep
    4. I believe in your scenario you only have one feature but change colors in different timesteps

    Addressing your questions:

    a) How can I change the style without redrawing the lines?

    I don't think it's possible from folium itself, it would be necessary to pass a style_function to TimestampedGeoJson which is not even a parameter on the class init at the moment. It seems to be hard to do that because you would need to translate a python style_function, to a javascript style_function.

    There would be a simple work around. Inside the class definition of TimestampedGeoJson it uses a _template variable as a string template of the javascript code so you could potentially adapt this template however you want but using javascript.

    class TimestampedGeoJson(MacroElement):
             .... hidding lines to save space
        _template = Template("""
            {% macro script(this, kwargs) %}
                     .... hidding lines to save space
                        style: function (feature) {
                            return feature.properties.style;
                        },
                        onEachFeature: function(feature, layer) {
                            if (feature.properties.popup) {
                            layer.bindPopup(feature.properties.popup);
                            }
                        }
                    })
            {% endmacro %}
            """)  # noqa
         ..... hidding lines to save space
    

    So for changing the line color at every time step you could change this part of the template:

    style: function (feature) {
                            return feature.properties.style;
                        },
    

    by this : loop through an array of colors

                        style: function(feature) {
                            lastIdx=feature.properties.colors.length-1
                            currIdx=feature.properties.colors.indexOf(feature.properties.color);
                            if(currIdx==lastIdx){
                                feature.properties.color = feature.properties.colors[0]  
                            }
                            else{
                                feature.properties.color =feature.properties.colors[currIdx+1] 
                            }
                            return {color: feature.properties.color}
                        },
    
    

    change it so that you update the color inside properties.style every timestep.

    b) What does the time mean? How can I specify a consistent time sequence?

    TimestampedGeoJson is using the library Leaflet.TimeDimension, so TimestampedGeoJson corresponds to L.TimeDimension.Layer.GeoJSON.

    from that documentation you get

    "coordTimes, times or linestringTimestamps: array of times that can be associated with a geometry (datestrings or ms). In the case of a LineString, it must have as many items as coordinates in the LineString."

    so essentially to be consistent just make sure 1. for each feature the times size is the same as coordinates and 2. use a valid datestrings or ms format 3. if your dates are increasing by a constant set your period to that value

    putting all together I mainly changed in your previous example: 1) added a _template variable with the new style_function and change the TimestampedGeoJson default template

    2) changed the two features coordinates a bit to show that the two features you set were overlapping and some timesteps and at some timesteps just the first feature was defined and later just the second feature was defined so it get's confusing what is happening in each time step.

    3) added a list of colors to loop through for each feature

    from jinja2 import Template
    _template = Template("""
        {% macro script(this, kwargs) %}
            L.Control.TimeDimensionCustom = L.Control.TimeDimension.extend({
                _getDisplayDateFormat: function(date){
                    var newdate = new moment(date);
                    console.log(newdate)
                    return newdate.format("{{this.date_options}}");
                }
            });
            {{this._parent.get_name()}}.timeDimension = L.timeDimension(
                {
                    period: {{ this.period|tojson }},
                }
            );
            var timeDimensionControl = new L.Control.TimeDimensionCustom(
                {{ this.options|tojson }}
            );
            {{this._parent.get_name()}}.addControl(this.timeDimensionControl);
            var geoJsonLayer = L.geoJson({{this.data}}, {
                    pointToLayer: function (feature, latLng) {
                        if (feature.properties.icon == 'marker') {
                            if(feature.properties.iconstyle){
                                return new L.Marker(latLng, {
                                    icon: L.icon(feature.properties.iconstyle)});
                            }
                            //else
                            return new L.Marker(latLng);
                        }
                        if (feature.properties.icon == 'circle') {
                            if (feature.properties.iconstyle) {
                                return new L.circleMarker(latLng, feature.properties.iconstyle)
                                };
                            //else
                            return new L.circleMarker(latLng);
                        }
                        //else
                        return new L.Marker(latLng);
                    },
                    style: function(feature) {
                        lastIdx=feature.properties.colors.length-1
                        currIdx=feature.properties.colors.indexOf(feature.properties.color);
                        if(currIdx==lastIdx){
                            feature.properties.color = feature.properties.colors[currIdx+1] 
                        }
                        else{
                            feature.properties.color =feature.properties.colors[currIdx+1] 
                        }
                        return {color: feature.properties.color}
                    },
                    onEachFeature: function(feature, layer) {
                        if (feature.properties.popup) {
                        layer.bindPopup(feature.properties.popup);
                        }
                    }
                })
            var {{this.get_name()}} = L.timeDimension.layer.geoJson(
                geoJsonLayer,
                {
                    updateTimeDimension: true,
                    addlastPoint: {{ this.add_last_point|tojson }},
                    duration: {{ this.duration }},
                }
            ).addTo({{this._parent.get_name()}});
        {% endmacro %}
        """)
    import folium
    from folium.plugins import TimestampedGeoJson
    
    m = folium.Map(
        location=[42.80491692, -4.62577249],
        zoom_start=9
    )
    
    data = [
    {
        'coordinates': [
            [-4.018876661, 43.11843382],
            [-4.856537491, 42.82202193],
        ],
        'dates': [
            '2017-06-02T00:00:00',
            '2017-06-02T00:10:00'
        ],
        'color': 'brown',
        'colors':["black","orange","pink"],
    },
    {
        'coordinates': [
            [-4.058876661, 43.11843382],
            [-4.936537491, 42.82202193],
        ],
        'dates': [
            '2017-06-02T00:00:00',
            '2017-06-02T00:10:00'
        ],
        'color': 'blue',
        'colors':["red","yellow","green"],
    },
    ]
    
    features = [
        {
            'type': 'Feature',
            'geometry': {
                'type': 'LineString',
                'coordinates': d['coordinates'],
            },
            'properties': {
                'times': d['dates'],
                'color': d["color"],
                'colors':d["colors"]
            }
        }
        for d in data
    ]
    
    t=TimestampedGeoJson({
        'type': 'FeatureCollection',
        'features': features,
    }, period='PT10H', add_last_point=True)
    t._template=_template
    t.add_to(m)
    
    m.save('original.html')