/
kepler.py
526 lines (440 loc) · 17.6 KB
/
kepler.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
"""Module for creating interactive maps with kepler.gl."""
# *******************************************************************************#
# This module contains extra features of the geemap package. #
# The geemap community will maintain the extra features. #
# *******************************************************************************#
import json
import os
import sys
import requests
import ipywidgets as widgets
import pandas as pd
from IPython.display import display, HTML
from .common import *
from .osm import *
from . import examples
try:
import keplergl
if "google.colab" in sys.modules:
from google.colab import output
output.enable_custom_widget_manager()
except ImportError:
raise ImportError(
"Kepler needs to be installed to use this module. Use 'pip install keplergl' to install the package. See https://docs.kepler.gl/docs/keplergl-jupyter for more details."
)
class Map(keplergl.KeplerGl):
"""The Map class inherits keplergl.KeperGl.
Returns:
object: keplergl.KeperGl map object.
"""
def __init__(self, **kwargs):
if "center" not in kwargs:
kwargs["center"] = [20, 0]
if "zoom" not in kwargs:
kwargs["zoom"] = 1.3
if "height" not in kwargs:
kwargs["height"] = 600
elif "px" in str(kwargs["height"]):
kwargs["height"] = kwargs["height"].replace("px", "")
if "width" not in kwargs:
kwargs["width"] = 600
elif "px" in str(kwargs["width"]):
kwargs["width"] = kwargs["width"].replace("px", "")
if "widescreen" not in kwargs:
kwargs["widescreen"] = False
if "pitch" not in kwargs:
kwargs["pitch"] = 0
if "bearing" not in kwargs:
kwargs["bearing"] = 0
if "dragRotate" not in kwargs:
kwargs["dragRotate"] = False
if "isSplit" not in kwargs:
kwargs["isSplit"] = False
if kwargs["widescreen"]:
display(HTML("<style>.container { width:100% !important; }</style>"))
config = {
"version": "v1",
"config": {
"mapState": {
"latitude": kwargs["center"][0],
"longitude": kwargs["center"][1],
"zoom": kwargs["zoom"],
"bearing": kwargs["bearing"],
"pitch": kwargs["pitch"],
"isSplit": kwargs["isSplit"],
"dragRotate": kwargs["dragRotate"],
"height": kwargs["height"],
"width": kwargs["width"],
}
},
}
kwargs.pop("widescreen")
kwargs.pop("center")
kwargs.pop("zoom")
if "show_docs" not in kwargs:
kwargs["show_docs"] = False
super().__init__(**kwargs)
self.config = config
# def _repr_mimebundle_(self, include=None, exclude=None):
# """Display the map in a notebook.
# Args:
# include (list, optional): A list of MIME types to include.
# exclude (list, optional): A list of MIME types to exclude.
# Returns:
# dict: A dictionary of MIME type keyed dict of MIME type data.
# """
# print("hello")
# # import base64
# # bundle = super()._repr_mimebundle_(include=include, exclude=exclude)
# # if bundle["text/html"]:
# # bundle["text/html"] = self.display_html()
# # return bundle
def add_geojson(self, in_geojson, layer_name="Untitled", config=None, **kwargs):
"""Adds a GeoJSON file to the map.
Args:
in_geojson (str | dict): The file path or http URL to the input GeoJSON or a dictionary containing the geojson.
layer_name (str, optional): The layer name to be used.. Defaults to "Untitled".
config (str, optional): Local path or HTTP URL to the config file. Defaults to None.
Raises:
FileNotFoundError: The provided GeoJSON file could not be found.
TypeError: The input geojson must be a type of str or dict.
"""
if "encoding" in kwargs:
encoding = kwargs["encoding"]
else:
encoding = "utf-8"
try:
if isinstance(in_geojson, str):
if in_geojson.startswith("http"):
data = requests.get(in_geojson).json()
else:
in_geojson = os.path.abspath(in_geojson)
if not os.path.exists(in_geojson):
raise FileNotFoundError(
"The provided GeoJSON file could not be found."
)
with open(in_geojson, encoding=encoding) as f:
data = json.load(f)
elif isinstance(in_geojson, dict):
data = in_geojson
else:
raise TypeError("The input geojson must be a type of str or dict.")
except Exception as e:
raise Exception(e)
self.add_data(data, name=layer_name)
self.load_config(config)
def add_shp(self, in_shp, layer_name="Untitled", config=None, **kwargs):
"""Adds a shapefile to the map.
Args:
in_shp (str): The input file path to the shapefile.
layer_name (str, optional): The layer name to be used.. Defaults to "Untitled".
config (str, optional): Local path or HTTP URL to the config file. Defaults to None.
Raises:
FileNotFoundError: The provided shapefile could not be found.
"""
import glob
if in_shp.startswith("http") and in_shp.endswith(".zip"):
out_dir = os.path.abspath("./cache/shp")
if not os.path.exists(out_dir):
os.makedirs(out_dir)
download_from_url(in_shp, out_dir=out_dir, verbose=False)
files = list(glob.glob(os.path.join(out_dir, "*.shp")))
if len(files) > 0:
in_shp = files[0]
else:
raise FileNotFoundError(
"The downloaded zip file does not contain any shapefile in the root directory."
)
else:
in_shp = os.path.abspath(in_shp)
if not os.path.exists(in_shp):
raise FileNotFoundError("The provided shapefile could not be found.")
geojson = shp_to_geojson(in_shp)
self.add_geojson(
geojson,
layer_name,
**kwargs,
)
self.load_config(config)
def add_gdf(
self,
gdf,
layer_name="Untitled",
config=None,
**kwargs,
):
"""Adds a GeoDataFrame to the map.
Args:
gdf (GeoDataFrame): A GeoPandas GeoDataFrame.
layer_name (str, optional): The layer name to be used.. Defaults to "Untitled".
config (str, optional): Local path or HTTP URL to the config file. Defaults to None.
"""
data = gdf_to_geojson(gdf, epsg="4326")
self.add_geojson(data, layer_name, **kwargs)
self.load_config(config)
def add_df(
self,
df,
layer_name="Untitled",
config=None,
**kwargs,
):
"""Adds a DataFrame to the map.
Args:
df (DataFrame): A Pandas DataFrame.
layer_name (str, optional): The layer name to be used.. Defaults to "Untitled".
config (str, optional): Local path or HTTP URL to the config file. Defaults to None.
"""
try:
self.add_data(data=df, name=layer_name)
self.load_config(config)
except Exception as e:
print(e)
def add_csv(
self,
in_csv,
layer_name="Untitled",
config=None,
**kwargs,
):
"""Adds a CSV to the map.
Args:
in_csv (str): File path to the CSV.
layer_name (str, optional): The layer name to be used.. Defaults to "Untitled".
config (str, optional): Local path or HTTP URL to the config file. Defaults to None.
"""
df = pd.read_csv(in_csv)
self.add_df(df, layer_name, config, **kwargs)
def add_vector(
self,
filename,
layer_name="Untitled",
config=None,
**kwargs,
):
"""Adds any geopandas-supported vector dataset to the map.
Args:
filename (str): Either the absolute or relative path to the file or URL to be opened, or any object with a read() method (such as an open file or StringIO).
layer_name (str, optional): The layer name to use. Defaults to "Untitled".
config (str, optional): Local path or HTTP URL to the config file. Defaults to None.
"""
if not filename.startswith("http"):
filename = os.path.abspath(filename)
ext = os.path.splitext(filename)[1].lower()
if ext == ".shp":
self.add_shp(
filename,
layer_name,
**kwargs,
)
self.load_config(config)
elif ext in [".json", ".geojson"]:
self.add_geojson(
filename,
layer_name,
**kwargs,
)
self.load_config(config)
else:
geojson = vector_to_geojson(
filename,
epsg="4326",
**kwargs,
)
self.add_geojson(
geojson,
layer_name,
**kwargs,
)
self.load_config(config)
def add_kml(
self,
in_kml,
layer_name="Untitled",
config=None,
**kwargs,
):
"""Adds a KML file to the map.
Args:
in_kml (str): The input file path to the KML.
layer_name (str, optional): The layer name to be used.. Defaults to "Untitled".
config (str, optional): Local path or HTTP URL to the config file. Defaults to None.
Raises:
FileNotFoundError: The provided KML file could not be found.
"""
if in_kml.startswith("http") and in_kml.endswith(".kml"):
out_dir = os.path.abspath("./cache")
if not os.path.exists(out_dir):
os.makedirs(out_dir)
download_from_url(in_kml, out_dir=out_dir, unzip=False, verbose=False)
in_kml = os.path.join(out_dir, os.path.basename(in_kml))
if not os.path.exists(in_kml):
raise FileNotFoundError("The downloaded kml file could not be found.")
else:
in_kml = os.path.abspath(in_kml)
if not os.path.exists(in_kml):
raise FileNotFoundError("The provided KML could not be found.")
self.add_vector(
in_kml,
layer_name,
**kwargs,
)
self.load_config(config)
def add_gdf_from_postgis(
self,
sql,
con,
layer_name="Untitled",
config=None,
**kwargs,
):
"""Reads a PostGIS database and returns data as a GeoDataFrame to be added to the map.
Args:
sql (str): SQL query to execute in selecting entries from database, or name of the table to read from the database.
con (sqlalchemy.engine.Engine): Active connection to the database to query.
layer_name (str, optional): The layer name to be used.. Defaults to "Untitled".
config (str, optional): Local path or HTTP URL to the config file. Defaults to None.
"""
gdf = read_postgis(sql, con, **kwargs)
gdf = gdf.to_crs("epsg:4326")
self.add_gdf(
gdf,
layer_name,
**kwargs,
)
self.load_config(config)
def static_map(
self, width=950, height=600, read_only=False, out_file=None, **kwargs
):
"""Display a kepler.gl static map in a Jupyter Notebook.
Args
width (int, optional): Width of the map. Defaults to 950.
height (int, optional): Height of the map. Defaults to 600.
read_only (bool, optional): Whether to hide the side panel to disable map customization. Defaults to False.
out_file (str, optional): Output html file path. Defaults to None.
"""
if isinstance(self, keplergl.KeplerGl):
if out_file is None:
if os.environ.get("USE_MKDOCS") is not None:
out_file = "../maps/" + "kepler_" + random_string(3) + ".html"
else:
out_file = "./cache/" + "kepler_" + random_string(3) + ".html"
out_dir = os.path.abspath(os.path.dirname(out_file))
if not os.path.exists(out_dir):
os.makedirs(out_dir)
output = widgets.Output()
with output:
self.save_to_html(file_name=out_file, read_only=read_only)
display_html(src=out_file, width=width, height=height)
else:
raise TypeError("The provided map is not a kepler.gl map.")
def to_html(
self,
filename=None,
read_only=False,
**kwargs,
):
"""Saves the map as a HTML file.
Args:
filename (str, optional): The output file path to the HTML file.
read_only (bool, optional): Whether to hide the side panel to disable map customization. Defaults to False.
"""
try:
save = True
if filename is not None:
if not filename.endswith(".html"):
raise ValueError("The output file extension must be html.")
filename = os.path.abspath(filename)
out_dir = os.path.dirname(filename)
if not os.path.exists(out_dir):
os.makedirs(out_dir)
else:
filename = os.path.abspath(random_string() + ".html")
save = False
output = widgets.Output()
with output:
self.save_to_html(file_name=filename, read_only=read_only)
if not save:
out_html = ""
with open(filename) as f:
lines = f.readlines()
out_html = "".join(lines)
os.remove(filename)
return out_html
except Exception as e:
raise Exception(e)
def to_streamlit(
self, width=800, height=600, responsive=True, scrolling=False, **kwargs
):
"""Renders `keplergl.KeplerGl` map figure in a Streamlit app.
Args:
width (int, optional): Width of the map. Defaults to 800.
height (int, optional): Height of the map. Defaults to 600.
responsive (bool, optional): Whether to make the map responsive. Defaults to True.
scrolling (bool, optional): If True, show a scrollbar when the content is larger than the iframe. Otherwise, do not show a scrollbar. Defaults to False.
Raises:
ImportError: If streamlit is not installed.
Returns:
streamlit.components: components.html object.
"""
try:
import streamlit as st
import streamlit.components.v1 as components
html = self._repr_html_()
if responsive:
make_map_responsive = """
<style>
[title~="st.iframe"] { width: 100%}
</style>
"""
st.markdown(make_map_responsive, unsafe_allow_html=True)
return components.html(
html, width=width, height=height, scrolling=scrolling
)
except ImportError:
raise ImportError(
"streamlit is not installed. You need to install streamlitusing 'pip install streamlit'. Seehttps://docs.streamlit.io/library/get-started/installation"
)
def load_config(self, config=None):
"""Loads a kepler.gl config file.
Args:
config (str, optional): Local path or HTTP URL to the config file. Defaults to None.
Raises:
FileNotFoundError: The provided config file could not be found.
TypeError: The provided config file is not a kepler.gl config file.
"""
if config is None:
pass
elif isinstance(config, dict):
self.config = config
elif isinstance(config, str):
if config.startswith("http"):
r = requests.get(config)
self.config = r.json()
elif os.path.isfile(config):
with open(config) as f:
self.config = json.load(f)
else:
raise FileNotFoundError("The provided config file could not be found.")
else:
raise TypeError("The provided config is not a dictionary or filepath.")
def save_config(self, out_json):
"""Saves a kepler.gl config file.
Args:
out_json (str): Output file path to the config file.
Raises:
ValueError: The output file extension must be json.
TypeError: The provided filepath is invalid.
"""
if isinstance(out_json, str):
if not out_json.endswith(".json"):
raise ValueError("The output file extension must be json.")
out_json = os.path.abspath(out_json)
out_dir = os.path.dirname(out_json)
if not os.path.exists(out_dir):
os.makedirs(out_dir)
json_str = json.dumps(self.config, indent=2)
with open(out_json, "w") as f:
f.write(json_str)
else:
raise TypeError("The provided filepath is invalid.")