Folium map embedded within PyQt displays blank when adding GeoJson to layer [duplicate] - pyqt5

This question already has answers here:
Add a large shapefile to map in python using folium
(1 answer)
Use QWebEngineView to Display Something Larger Than 2MB?
(1 answer)
Closed 12 months ago.
I have PyQt5 app that embeds a folium Map within a QWidget.
Here's a minimal example of the class I wrote :
import folium
import io
from folium.plugins import Draw, MousePosition, HeatMap
from PySide2 import QtWidgets, QtWebEngineWidgets
class FoliumMap(QtWidgets.QWidget):
def __init__(self, parent=None):
QtWidgets.QWidget.__init__(self, parent)
self.layout = QtWidgets.QVBoxLayout()
m = folium.Map(
title='coastlines',
zoom_start=3)
data = io.BytesIO()
m.save(data, close_file=False)
webView = QtWebEngineWidgets.QWebEngineView()
webView.setHtml(data.getvalue().decode())
self.layout.addWidget(webView)
self.setLayout(self.layout)
If i run my program here's what I have :
Now I want to add a GeoJson layer within the __init__ of my class :
folium.GeoJson('data/custom.geo.json', name='coasts').add_to(m)
This results in having a blank Qwidget :
If I save the map under html format I am able to see the layer on my web browser:
Has anyone an idea on why the layer implementation makes the QWidget blank ? And how to fix this ?

The problem seems to be the following command :
webView.setHtml(data.getvalue().decode())
It is said in the Qt documentation that :
Content larger than 2 MB cannot be displayed, because setHtml() converts the provided HTML to percent-encoding and places data: in front of it to create the URL that it navigates to. Thereby, the provided code becomes a URL that exceeds the 2 MB limit set by Chromium. If the content is too large, the loadFinished() signal is triggered with success=false.
My html file weighs 2628 KB > 2MB. So we have to use webView.load() method instead.
This is the way to go :
import folium
import io
from folium.plugins import Draw, MousePosition, HeatMap
from PySide2 import QtWidgets, QtWebEngineWidgets, QtCore
class FoliumMap(QtWidgets.QWidget):
def __init__(self, parent=None):
QtWidgets.QWidget.__init__(self, parent)
self.layout = QtWidgets.QVBoxLayout()
m = folium.Map(
title='coastlines',
zoom_start=3)
url = "C:/MYPATH/TO/map.html"
m.save(url)
webView = QtWebEngineWidgets.QWebEngineView()
html_map = QtCore.QUrl.fromLocalFile(url)
webView.load(html_map)
self.layout.addWidget(webView)
self.setLayout(self.layout)

Related

How to apply CSS color filter on Folium map tiles

I want to create a dark mode based on Google Maps tiles in Folium. However, as Google is not provided dark mode tiles, a simple workaround seems to be applying a color filter to tiles. A similar plugin for Leaflet is introduced here.
How can I reach a similar result in Folium? Is it possible by executing javascript through the runJavaScript() method (similar to what was done here)?
A minimal Foilium map embedded in PyQt5 is also provided.
import io
import folium
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QMainWindow
from PyQt5.QtWebEngineWidgets import QWebEngineView
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.m = folium.Map(
zoom_start = 18,
location = (41.8828, 12.4761),
control_scale=True,
tiles = None
)
folium.raster_layers.TileLayer(
tiles='http://mt1.google.com/vt/lyrs=m&h1=p1Z&x={x}&y={y}&z={z}',
name='Standard Roadmap',
attr = 'Google Map',
).add_to(self.m)
folium.LayerControl().add_to(self.m)
self.data = io.BytesIO()
self.m.save(self.data, close_file=False)
widget=QWidget()
vbox = QVBoxLayout()
self.webView = QWebEngineView()
self.webView.setHtml(self.data.getvalue().decode())
self.webView.setContextMenuPolicy(Qt.NoContextMenu)
vbox.addWidget(self.webView)
widget.setLayout(vbox)
self.setCentralWidget(widget)
self.setWindowTitle("App")
self.setMinimumSize(1000, 600)
self.showMaximized()
App = QApplication([])
window = Window()
App.exec()

QIcon Using ndarray [duplicate]

This question already has answers here:
PyQt5 QImage from Numpy Array
(7 answers)
Converting numpy image to QPixmap
(1 answer)
Closed 10 months ago.
In some interface using PyQt5, let button be a QPushButton. If we want to set an icon for the button using a given saved image image.jpg, we usually write:
button.setIcon(QtGui.QIcon("image.jpg"))
However, if I am given an n-dimensional numpy array array which also represents an image, I cannot simply write button.setIcon(QtGui.QIcon(array)) as it will give an error message. What should I do to instead? (saving the array as an image is not in consideration as I want large amount of arrays to be made into PushButton)
Edit:
For a typical situation, consider:
import numpy
array = numpy.random.rand(100,100,3) * 255
Then array is a square array representing an image. To see this, we can write (we do not have to use PIL or Image to solve our problem and this is just a demonstration):
from PIL import Image
im = Image.fromarray(imarray.astype('uint8')).convert('RGBA')
Reference:100x100 image with random pixel colour
I want to make this fixed array an icon.
You have to build a QImage -> QPixmap -> QIcon:
import numpy as np
from PyQt5.QtGui import QIcon, QImage, QPixmap
from PyQt5.QtWidgets import QApplication, QPushButton
app = QApplication([])
array = (np.random.rand(1000, 1000, 3) * 255).astype("uint8")
height, width, _ = array.shape
image = QImage(bytearray(array), width, height, QImage.Format.Format_RGB888)
# image.convertTo(QImage.Format.Format_RGB32)
button = QPushButton()
button.setIconSize(image.size())
button.setIcon(QIcon(QPixmap(image)))
button.show()
app.exec_()

Accessing methods within a class from bokeh FileInput widget

I am working on a Bokeh serve UI and am running into trouble interfacing a class (and its methods) with the FileInput widget. I am using a class (in this example, called "EIS_data") which, when instantiated, loads a file using pd.read_csv. The EIS_data class also has a method to plot the data in a particular way, and I'd like to be able to load the pandas dataframe and call and manipulate the data using the methods already in place in the class.
So far, I have been able to load the data successfully using the FileInput widget, but I can't figure out how to access the dataframe again once it's loaded in. In a standalone Jupyter notebook, I could run d = EIS_data("filename") and then ```d.plot''' to load the data into a pandas dataframe and then plot it according to the method defined in the EIS_data class, but I can't figure out how to replicate this in the UI code once the data are loaded using the FileInput widget.
Is there a way I can interface this with Bokeh widgets, such that I could simply add d.plot() to curdoc()? I have found a workaround using ColumnDataSource, but it seems a shame to redefine plotting methods and data handling when they are already defined in the class. Below are minimal working examples of the UI code and the class definition.
UI Code:
import numpy as np
import pandas as pd
from eis_analysis_trimmed import EIS_data
import bokeh
from bokeh.io import curdoc
from bokeh import layouts
from bokeh.layouts import column,row,gridplot
from bokeh.plotting import figure
from bokeh.models import *
import base64
import io
## Instantiate the EIS_data class for loading data
def load_data(f):
return EIS_data(f)
## updater function called to load data with FileInput widget
## Must be decoded using base64
def load_file(attr, old, new):
decoded = base64.b64decode(new)
d = io.BytesIO(decoded)
dat = load_data(d)
print(dat.df)
print(dat)
print("EIS Data Uploaded Successfully")
return dat
f_load = Paragraph(text="""Load Data""",height=15)
f = FileInput()
f.on_change('value',load_file)
curdoc().add_root(column(f))
and here is the EIS_data class:
import numpy as np
from scipy.optimize import curve_fit
from sklearn.metrics import r2_score
from bokeh.plotting import figure, show
from bokeh.models import LinearAxis, Range1d
from bokeh.resources import INLINE
import bokeh.io
#locally include javascript dependencies in html
bokeh.io.output_notebook(INLINE)
class EIS_data:
def __init__(self, file_name, delimiter='\t',
header=0, f_low=None, f_high=None):
#load eis data into a pandas dataframe
eis_data = pd.read_csv(file_name, delimiter=delimiter, header=header)
#iterate through all of the columns and check to see
#if all of the values in that column are null
#if they are, then remove that column
for c in eis_data.columns:
if eis_data[c].isnull().all():
eis_data = eis_data.drop([c], axis=1)
#make sure that the data are imported as floats and not strings
eis_data = eis_data[['freq/Hz', 'Re(Z)/Ohm', '-Im(Z)/Ohm']]
eis_data['freq/Hz'] = pd.to_numeric(eis_data['freq/Hz'])
eis_data['Re(Z)/Ohm'] = pd.to_numeric(eis_data['Re(Z)/Ohm'])
eis_data['-Im(Z)/Ohm'] = pd.to_numeric(eis_data['-Im(Z)/Ohm'])
self.df = eis_data.sort_values(by='freq/Hz')
def plot(self, fit_vals = None):
plot = figure(title="Nyquist Plot",
x_axis_label='Re(Z) Ohm',
y_axis_label='-Im(Z) Ohm',
plot_width=600,
plot_height=600)
plot.circle(self.df['Re(Z)/Ohm'], self.df['-Im(Z)/Ohm'],
size=7, color='navy', name='Data')
return plot
EDIT: Adding the workaround using ColumnDataSource
from bokeh.layouts import column
from bokeh.plotting import figure
from bokeh.models import *
from bokeh.models.widgets import FileInput
import base64
import io
from eis_analysis2 import EIS_data
# Instantiate the EIS_data class for loading data
def load_data(data):
return EIS_data(data)
# updater function called to load data with FileInput widget
# Must be decoded using base64
def load_file(attr, old, new):
decoded = base64.b64decode(new)
d = io.BytesIO(decoded)
dat = load_data(d)
dat_df = dat.df
# Replace plot data with data from newly-loaded file
source.data = dict(freq=dat_df[dat_df.columns[0]], reZ=dat_df[dat_df.columns[1]], imZ=dat_df[dat_df.columns[2]])
#phase,mag = bode_calc(reZ,imZ)
print(dat_df)
print("EIS Data Uploaded Successfully")
# Create Column Data Source that will be used by the plot
source = ColumnDataSource(data=dict(freq=[], reZ=[], imZ=[]))
##Make the nyquist plot
nyq_plot = figure(title="Nyquist Plot",
x_axis_label='Re(Z) Ohm',
y_axis_label='-Im(Z) Ohm',
plot_width=600,
plot_height=600)
nyq_plot.circle(x="reZ", y="imZ",source=source,size=7, color='navy', name='Data')
f = FileInput()
f.on_change('value', load_file)
layout = column(f, nyq_plot)
curdoc().add_root(layout)

matplotlib animation embedded in tkinter : update function never called

I've been trying to embed a matplotlib animation into tkinter.
The goal of this app is to simulate some differentials equations with rk4 method and show a real time graph as the simulation goes.
In fact the plot is rightly embedded into the tkinter frame.
However, the animation never run, I've noticed that the update function is never called.
I've been searching everywhere but I didn't find anything.
Thanks for the help.
Here is a code sample of the GUI class showing where I execute the animation
# called when I click on a button "start simulation"
def plot_neutrons_flow(self):
# getting parameters from the graphical interface
if not self._started:
I0 = float(self._field_I0.get())
X0 = float(self._field_X0.get())
flow0 = float(self._field_flow0.get())
time_interval = float(self._field_time_interval.get())
stop = int(self._field_stop.get())
FLOW_CI = [I0, X0, flow0] # [I(T_0), X(T_0), PHI[T_0]]
self._simulation = NeutronsFlow(
edo=neutrons_flow_edo,
t0=0,
ci=FLOW_CI,
time_interval=time_interval,
stop=hour_to_seconds(stop)
)
# launch the animation
self._neutrons_flow_plot.animate(self._simulation)
self._started = True
Here is the code for the matplotlib animation :
import matplotlib
import tkinter as tk
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
from matplotlib import style
matplotlib.use("TkAgg")
style.use('seaborn-whitegrid')
class PlotAnimation(FigureCanvasTkAgg):
def __init__(self, tk_root):
self._figure = Figure(dpi=100)
# bind plot to tkinter frame
super().__init__(self._figure, tk_root)
x_label = "Temps (h)"
y_label = "Flux / Abondance"
self._axes = self._figure.add_subplot(111, xlabel=x_label, ylabel=y_label, yscale="log")
self.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
def update(self, interval):
# this is never called
# get data from rk4 simulation
time_set = self._simulation.get_time_set()
y_set = self._simulation.get_y_set()
self._axes.clear()
self._axes.plot(time_set, y_set, visible=True, linewidth=1)
self._axes.legend(fancybox=True)
# redraw canvas
self.draw_idle()
def animate(self, simulation):
# this is called
self._simulation = simulation
# simulate differential equations with rk4 method
self._simulation.resolve()
# https://github.com/matplotlib/matplotlib/issues/1656
anim = animation.FuncAnimation(
self._figure,
self.update,
interval=1000
)
EDIT :
The solution was to instantiate the FuncAnimation function directly in the init method
As indicated in the documentation of the animation module (emphasis mine)
(...) it is critical to keep a reference to the instance object. The
animation is advanced by a timer (typically from the host GUI
framework) which the Animation object holds the only reference to. If
you do not hold a reference to the Animation object, it (and hence the
timers), will be garbage collected which will stop the animation.
You need to return the anim object from your animate() function, and store it somewhere in your code so that it is not garbage-collected

How to save an animated GIF to a variable using Pillow

I found out from here that I can create and save animated GIFs using Pillow. However, it doesn't look like the save method returns any value.
I can save the GIF to a file and then open that file using Image.open, but that seems unnecessary, given that I don't really want the GIF to be saved.
How can I save the GIF to a variable, rather than a file?
That is, I would like to be able to do some_variable.show() and display a GIF, without ever having to save the GIF onto my computer.
To avoid writing any files, you can just save your image to BytesIO object. For example:
#!/usr/bin/env python
from __future__ import division
from PIL import Image
from PIL import ImageDraw
from io import BytesIO
N = 25 # number of frames
# Create individual frames
frames = []
for n in range(N):
frame = Image.new("RGB", (200, 150), (25, 25, 255*(N-n)//N))
draw = ImageDraw.Draw(frame)
x, y = frame.size[0]*n/N, frame.size[1]*n/N
draw.ellipse((x, y, x+40, y+40), 'yellow')
# Saving/opening is needed for better compression and quality
fobj = BytesIO()
frame.save(fobj, 'GIF')
frame = Image.open(fobj)
frames.append(frame)
# Save the frames as animated GIF to BytesIO
animated_gif = BytesIO()
frames[0].save(animated_gif,
format='GIF',
save_all=True,
append_images=frames[1:], # Pillow >= 3.4.0
delay=0.1,
loop=0)
animated_gif.seek(0,2)
print ('GIF image size = ', animated_gif.tell())
# Optional: display image
#animated_gif.seek(0)
#ani = Image.open(animated_gif)
#ani.show()
# Optional: write contents to file
animated_gif.seek(0)
open('animated.gif', 'wb').write(animated_gif.read())
In the end, variable animated_gif contains contents of the following image:
However, displaying an animated GIF in Python is not very reliable. ani.show() from the code above displays only first frame on my machine.