A-B
A-B

Reputation: 41

Adding a textbox to a python Folium map with a GeoJSON timeline

I made an animated timeline map using python Folium and GeoJSON.

The map uses lines to show some people travelling from point A to point B. These lines appear over time with an animated timeline made by adding the lines ad points as features in GeoJSON.

While these travel lines and points appear on the map over time, I also need to display some text (maybe a text box or some titles or labels) with text that changes over time, for example *"Events of 2017: blablabla", "Events of 2018: blablabla"*, etc. These text boxes, or labels, or changing titles (??) would appear over time as the rest of the elements appear along the timeline. I'd be okay even with a popup that just stays on and visible without the need to click on the maker.

I reviewed other stackoverflow questions but they only address GeoJSON popups, which I know how to create but they aren't what I need, unless there's a way to have the popup show permanently without the need to click on it.

Attempts:

  1. I tried adding a text box to the map as a point marker using DivIcon to have HTML text appear instead of the icon, and this works great except that it's just one text that doesn't change over time, so it's static and doesn't update the text.
from folium.features import DivIcon
folium.Marker([47.717414, -20.897951], icon=DivIcon(html=('<textarea  style= "font-family:Helvetica, Avenir, Helvetica neue, Sans-serif;font-size:12pt;color:Black; outline:2px black; background-color:transparent;"> Sometext.</textarea>'))).add_to(m)
  1. I tried adding multiple point markers with their text in the DivIcon icon, but that just creates an illegible text overlap. I could have the markers appear in non-overlapping locations of the map, but that would anyway show all the texts all at once, while I need them to appear gradually over time.

  2. I tried having the text displayed as the popup of one of the GeoJSOn point features, but all I achieved is to put the text in the popup or tooltip of a point in the map, so I can't actually see it unless I stop the timeline and go manually click or hover on that one icon each time I "suspect" new text might be there. I didn't find a way to make the popup stay on without the need to click on it.

  3. I tried asking Chat GPT and Google Bard and they both come up with solutions that involve nonexisting code or nonexisting libraries.

Is there a way to have some sort of permanently visible text that just shows and changes over time? Maybe not using GeoJSON at all, but some JavaScript thing that works with the HTML of the map? I'm not expert in this.

This is a sample code from the code I used to make my map:

import folium
from folium import plugins
from folium.plugins import TimestampedGeoJson

#Create map
m = folium.Map(location=[latitude, longitude], zoom_start=4)

# Create points
points=[
{
        "times": ["2017-06-02T00:10:00", "2017-06-02T00:40:00"],
        "popup": "<h1>Some text 1</h1>",
        "coordinates": [10.000654, 53.550341], # Longitude first
    },
    # more points
]

lines = [
    {
        "coordinates": [
            [28.9662187, 41.0091982], # Longitude first
            [10.000654, 53.550341],
        ],
        "times": ["2017-06-02T00:00:00", "2017-06-02T00:10:00"],
    },
    # more lines
]

# Create "features" list to be filled for GeoJSON
features=[]

# Fill features with points
for point in points:
    features.append(
    {
        "type": "Feature",
        "geometry": {
            "type": "Point",
            "coordinates": point["coordinates"],
        },
        "properties": {
            "times": point["times"],
            "popup": point["popup"],
        },
    }
    )

# Fill features with lines
for line in lines:
    features.append(
    {
        "type": "Feature",
        "geometry": {
            "type": "LineString",
            "coordinates": line["coordinates"],
        },
        "properties": {
            "times": line["dates"],
            "popup": "Some Line Text",
        },
    }
)

# Create GeoJSON layer and add to map
timestamped_geojson = plugins.TimestampedGeoJson(
    {
        "type": "FeatureCollection",
        "features": features,
    },
    period="PT1M",
    add_last_point=True,
)
timestamped_geojson.add_to(m)

Upvotes: 2

Views: 644

Answers (1)

Nikita
Nikita

Reputation: 11

I'm not very good at English so sorry.

I ran into exactly the same problem when I was writing software on Pyqt 6. The only more real crutch that I came up with is to slightly change _template in TimestampedGeoJson, so that the current date is printed to the javascript console and then intercepts messages from the console.

First, I created a Customtimestamp class inherited from TimestampedGeoJson:

class CustomTimestamped(TimestampedGeoJson):
    _template = Template(
        """
        {% macro script(this, kwargs) %}
            L.Control.TimeDimensionCustom = L.Control.TimeDimension.extend({
                _getDisplayDateFormat: function(date){
                    var newdate = new moment(date);
                    console.log(newdate.format("{{this.date_options}}"))
                    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) {
                        return feature.properties.style;
                    },
                    onEachFeature: function(feature, layer) {
                        if (feature.properties.popup) {
                        layer.bindPopup(feature.properties.popup);
                        }
                        if (feature.properties.tooltip) {
                        layer.bindTooltip(feature.properties.tooltip);
                        }
                    }
                })

            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 %}
        """
    )

    def __init__(
        self,
        data,
        transition_time=200,
        loop=True,
        auto_play=True,
        add_last_point=True,
        period="P1D",
        min_speed=0.1,
        max_speed=10,
        loop_button=False,
        date_options="YYYY-MM-DD HH:mm:ss",
        time_slider_drag_update=False,
        duration=None,
        speed_slider=True,
    ):
        super().__init__(data,
                         transition_time=transition_time,
                         loop=loop,
                         auto_play=auto_play,
                         add_last_point=add_last_point,
                         period=period,
                         min_speed=min_speed,
                         max_speed=max_speed,
                         loop_button=loop_button,
                         date_options=date_options,
                         time_slider_drag_update=time_slider_drag_update,
                         duration=duration,
                         speed_slider=speed_slider,
                         )

In this class, in _template, I changed the

console.log(newdate.format(this.date_options)

on

console.log(new date.format("{{this.date_options}}"))

Now the date format that TimestampedGeoJson displays during animation will be printed to the javascript console

Next in the base class inherited from QWebEngineView i redefine function javaScriptConsoleMessage of class QWebEnginePage like this:

page = QWebEnginePage(self)
page.javaScriptConsoleMessage = lambda _1, message, _2, _3: self.change_params(message)
self.setPage(page)

Now, every time an animation moment occurs, the self.change_params function will be called. It accepts a message from the javascript console as an argument

In this function, you can do what you need.

Upvotes: 1

Related Questions