-
Notifications
You must be signed in to change notification settings - Fork 9
/
geeView.py
1588 lines (1244 loc) · 78.7 KB
/
geeView.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
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
View GEE objects using Python
geeViz.geeView is the core module for managing GEE objects on the geeViz mapper object. geeViz instantiates an instance of the `mapper` class as `Map` by default. Layers can be added to the map using `Map.addLayer` or `Map.addTimeLapse` and then viewed using the `Map.view` method.
"""
"""
Copyright 2024 Ian Housman
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
# Script to allow GEE objects to be viewed in a web viewer
# Intended to work within the geeViz package
######################################################################
# Import modules
import ee, sys, os, webbrowser, json, socket, subprocess, site, datetime, requests, google
from google.auth.transport import requests as gReq
from google.oauth2 import service_account
from threading import Thread
from urllib.parse import urlparse
from IPython.display import IFrame, display, HTML
if sys.version_info[0] < 3:
import SimpleHTTPServer, SocketServer
else:
import http.server, socketserver
creds_path = ee.oauth.get_credentials_path()
creds_dir = os.path.dirname(creds_path)
if not os.path.exists(creds_dir):
os.makedirs(creds_dir)
IS_COLAB = ee.oauth.in_colab_shell() # "google.colab" in sys.modules
IS_WORKBENCH = os.getenv("DL_ANACONDA_HOME") != None
if IS_COLAB:
from google.colab.output import eval_js
######################################################################
# Functions to handle various initialization/authentication workflows to try to get a user an initialized instance of ee
# Function to have user input a project id if one is still needed
def setProject(id):
"""
Sets the project id of an instance of ee
Args:
id (str): Google Cloud Platform project id to use
"""
global project_id
project_id = id
ee.data.setCloudApiUserProject(project_id)
def getProject(overwrite=False):
"""
Tries to find the current Google Cloud Platform project id
Args:
overwrite (bool, optional): Whether or not to overwrite a cached project ID file
Returns:
str: The currently selected Google Cloud Platform project id
"""
global project_id
provided_project = "{}.proj_id".format(creds_path)
provided_project = os.path.normpath(provided_project)
current_project = ee.data._cloud_api_user_project
if (current_project == None and not os.path.exists(provided_project)) or overwrite:
project_id = input("Please enter GEE project ID: ")
print("You entered: {}".format(project_id))
o = open(provided_project, "w")
o.write(project_id)
o.close()
if current_project != None:
project_id = current_project
elif os.path.exists(provided_project):
o = open(provided_project, "r")
project_id = o.read()
print("Cached project id file path: {}".format(provided_project))
print("Cached project id: {}".format(project_id))
o.close()
ee.data.setCloudApiUserProject(project_id)
return project_id
######################################################################
def verified_initialize(project=None):
"""
Tries to initialize GEE with a given project id. Will error out if initilization fails
Args:
project (str, optional): Whether or not to overwrite a cached project ID file
"""
ee.Initialize(project=project)
z = ee.Number(1).getInfo()
print("Successfully initialized")
# Function to handle various exceptions to initializing to GEE
def robustInitializer():
"""
A method that tries to authenticate and/or initialize GEE if it isn't already successfully initialized. This method tries to handle many different scenarios, but often fails. It is best to initialize to a project prior to importing geeViz
"""
global project_id
try:
z = ee.Number(1).getInfo()
except:
print("Initializing GEE")
if not ee.oauth._valid_credentials_exist():
ee.Authenticate()
try:
verified_initialize(project=ee.data._cloud_api_user_project)
except Exception as E:
# print(E)
if str(E).find("Reauthentication is needed") > -1:
ee.Authenticate(force=True)
if str(E).find("no project found. Call with project") or str(E).find("project is not registered") > -1 or str(E).find(" quota project, which is not set by default") > -1:
project_id = getProject()
else:
project_id = None
try:
verified_initialize(project=project_id)
except Exception as E:
print(E)
try:
project_id = getProject(overwrite=True)
verified_initialize(project=project_id)
except Exception as E:
print(E)
ee.data.setCloudApiUserProject(project_id)
setProject(ee.data._cloud_api_user_project)
robustInitializer()
######################################################################
# Set up GEE and paths
geeVizFolder = "geeViz"
geeViewFolder = "geeView"
# Set up template web viewer
# Do not change
cwd = os.getcwd()
paths = sys.path
py_viz_dir = os.path.dirname(__file__)
print("geeViz package folder:", py_viz_dir)
# Specify location of files to run
template = os.path.join(py_viz_dir, geeViewFolder, "index.html")
ee_run_dir = os.path.join(py_viz_dir, geeViewFolder, "src/gee/gee-run/")
if os.path.exists(ee_run_dir) == False:
os.makedirs(ee_run_dir)
######################################################################
######################################################################
# Functions
######################################################################
# Linear color gradient functions
##############################################################
##############################################################
def color_dict_maker(gradient: list[list[int]]) -> dict:
"""Takes in a list of RGB sub-lists and returns dictionary of
colors in RGB and hex form for use in a graphing function
defined later on"""
return {
"hex": [RGB_to_hex(RGB) for RGB in gradient],
"r": [RGB[0] for RGB in gradient],
"g": [RGB[1] for RGB in gradient],
"b": [RGB[2] for RGB in gradient],
}
# color functions adapted from bsou.io/posts/color-gradients-with-python
def hex_to_rgb(value: str) -> tuple:
"""Return (red, green, blue) for the color given as #rrggbb."""
value = value.lstrip("#")
lv = len(value)
if lv == 3:
lv = 6
value = f"{value[0]}{value[0]}{value[1]}{value[1]}{value[2]}{value[2]}"
return tuple(int(value[i : i + lv // 3], 16) for i in range(0, lv, lv // 3))
def RGB_to_hex(RGB: list[int]) -> str:
"""[255,255,255] -> "#FFFFFF" """
# Components need to be integers for hex to make sense
RGB = [int(x) for x in RGB]
return "#" + "".join(["0{0:x}".format(v) if v < 16 else "{0:x}".format(v) for v in RGB])
def linear_gradient(start_hex: str, finish_hex: str = "#FFFFFF", n: int = 10) -> dict:
"""returns a gradient list of (n) colors between
two hex colors. start_hex and finish_hex
should be the full six-digit color string,
inlcuding the number sign ("#FFFFFF")"""
# Starting and ending colors in RGB form
s = hex_to_rgb(start_hex)
f = hex_to_rgb(finish_hex)
# Initilize a list of the output colors with the starting color
RGB_list = [s]
# Calcuate a color at each evenly spaced value of t from 1 to n
for t in range(1, n):
# Interpolate RGB vector for color at the current value of t
curr_vector = [int(s[j] + (float(t) / (n - 1)) * (f[j] - s[j])) for j in range(3)]
# Add it to our list of output colors
RGB_list.append(curr_vector)
# print(RGB_list)
return color_dict_maker(RGB_list)
def polylinear_gradient(colors: list[str], n: int):
"""returns a list of colors forming linear gradients between
all sequential pairs of colors. "n" specifies the total
number of desired output colors"""
# The number of colors per individual linear gradient
n_out = int(float(n) / (len(colors) - 1)) + 1
# If we don't have an even number of color values, we will remove equally spaced values at the end.
apply_offset = False
if n % n_out != 0:
apply_offset = True
n_out = n_out + 1
# returns dictionary defined by color_dict()
gradient_dict = linear_gradient(colors[0], colors[1], n_out)
if len(colors) > 1:
for col in range(1, len(colors) - 1):
next = linear_gradient(colors[col], colors[col + 1], n_out)
for k in ("hex", "r", "g", "b"):
# Exclude first point to avoid duplicates
gradient_dict[k] += next[k][1:]
# Remove equally spaced values here.
if apply_offset:
offset = len(gradient_dict["hex"]) - n
sliceval = []
for i in range(1, offset + 1):
sliceval.append(int(len(gradient_dict["hex"]) * i / float(offset + 2)))
for k in ("hex", "r", "g", "b"):
gradient_dict[k] = [i for j, i in enumerate(gradient_dict[k]) if j not in sliceval]
return gradient_dict
def get_poly_gradient_ct(palette: list[str], min: int, max: int) -> list[str]:
"""
Take a palette and a set of min and max stretch values to get a 1:1 value to color hex list
Args:
palette (list): A list of hex code colors that will be interpolated
min (int): The min value for the stretch
max (int): The max value for the stretch
Returns:
list: A list of linearly interpolated hex codes where there is 1:1 color to value from min-max (inclusive)
>>> import geeViz.geeView as gv
>>> viz = {"palette": ["#FFFF00", "00F", "0FF", "FF0000"], "min": 1, "max": 20}
>>> color_ramp = gv.get_poly_gradient_ct(viz["palette"], viz["min"], viz["max"])
>>> print("Color ramp:", color_ramp)
"""
ramp = polylinear_gradient(palette, max - min + 1)
return ramp["hex"]
##############################################################
######################################################################
# Function to check if being run inside a notebook
# Taken from: https://stackoverflow.com/questions/15411967/how-can-i-check-if-code-is-executed-in-the-ipython-notebook
def is_notebook():
"""
Check if inside Jupyter shell
Returns:
bool: Whether inside Jupyter shell or not
"""
return ee.oauth._in_jupyter_shell()
######################################################################
# Function for cleaning trailing .... in accessToken
def cleanAccessToken(accessToken):
"""
Remove trailing '....' in generated access token
Args:
accessToken (str): Raw access token
Returns:
str: Given access token without trailing '....'
"""
while accessToken[-1] == ".":
accessToken = accessToken[:-1]
return accessToken
######################################################################
# Function to get domain base without any folders
def baseDomain(url):
"""
Get root domain for a given url
Args:
url (str): URL to find the base domain of
Returns:
str: domain of given URL
"""
url_parts = urlparse(url)
return f"{url_parts.scheme}://{url_parts.netloc}"
######################################################################
# Function for using default GEE refresh token to get an access token for geeView
# Updated 12/23 to reflect updated auth methods for GEE
def refreshToken():
"""
Get a refresh token from currently authenticated ee instance
Returns:
str: temporary access token
"""
credentials = ee.data.get_persistent_credentials()
credentials.refresh(gReq.Request())
accessToken = credentials.token
# print(credentials.to_json())
accessToken = cleanAccessToken(accessToken)
return accessToken
######################################################################
# Function for using a GEE white-listed service account key to get an access token for geeView
def serviceAccountToken(service_key_file_path):
"""
Get a refresh token from service account key file credentials
Returns:
str: temporary access token
"""
try:
credentials = service_account.Credentials.from_service_account_file(service_key_file_path, scopes=ee.oauth.SCOPES)
credentials.refresh(gReq.Request())
accessToken = credentials.token
accessToken = cleanAccessToken(accessToken)
return accessToken
except Exception as e:
print(e)
print("Failed to utilize service account key file.")
return None
######################################################################
# Function for running local web server
def run_local_server(port: int = 8001):
"""
Start a local webserver using the Python http.server
Args:
port (int): Port number to run local server at
"""
if sys.version[0] == "2":
server_name = "SimpleHTTPServer"
else:
server_name = "http.server"
cwd = os.getcwd()
os.chdir(py_viz_dir)
# print('cwd',os.getcwd())
python_path = sys.executable
if python_path.find("pythonw") > -1:
python_path = python_path.replace("pythonw", "python")
c = '"{}" -m {} {}'.format(python_path, server_name, port)
print("HTTP server command:", c)
subprocess.Popen(c, shell=True)
os.chdir(cwd)
######################################################################
# Function to see if port is active
def isPortActive(port: int = 8001):
"""
See if a given port number is currently active
Args:
port (int): Port number to check status of
Returns:
bool: Whether or not the port is already active
"""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(2) # 2 Second Timeout
result = sock.connect_ex(("localhost", port))
if result == 0:
return True
else:
return False
######################################################################
######################################################################
######################################################################
# Set up mapper object
class mapper:
"""Primary geeViz map setup and manipulation object
Map object that is used to manage layers, activated user input methods, and launching the map viewer user interface
Args:
port (int, default 8001): Which port to user for web server. Sometimes a port will become "stuck," so this will need set to some other number than what it was set at in previous runs of a given session.
Attributes:
port (int, default 8001): Which port to user for web server. Sometimes a port will become "stuck," so this will need set to some other number than what it was set at in previous runs of a given session.
proxy_url (str, default None): The proxy url the web server runs through for either Google Colab or Vertex AI Workbench. This is automatically specified in Google Colab, but in Vertex AI Workbench, the `Map.proxy_url` must be specified as the current URL Workbench Notebook is running from (e.g. https://code-dot-region.notebooks.googleusercontent.com/).
refreshTokenPath (str, default ee.oauth.get_credentials_path()): Refresh token credentials file path
serviceKeyPath (str, default None): Location of a service account key json. If provided, this will be used for authentication inside geeView instead of the refresh token
project (str, default ee.data._cloud_api_user_project): Can override which project geeView will use for authentication. While geeViz will try to find a project if ee.data._cloud_api_user_project isn't already set (usually by `ee.Initialize(project="someProjectID")`) by prompting the user to enter one, in some builds, this does not work. Set this attribute manually if the URL say `project=None` when launching geeView using `Map.view()`.
turnOffLayersWhenTimeLapseIsOn (bool, default True): Whether all other layers should be turned off when a time lapse is turned on. This is set to True by default to avoid confusing layer order rendering that can occur when time lapses and non-time lapses are visible at the same time. Often this confusion is fine and visualizing time lapses and other layers is desired. Set `Map.turnOffLayersWhenTimeLapseIsOn` to False in this instance.
"""
def __init__(self, port: int = 8001):
self.port = port
self.layerNumber = 1
self.idDictList = []
self.mapCommandList = []
self.ee_run_name = "runGeeViz"
self.typeLookup = {
"Image": "geeImage",
"ImageCollection": "geeImageCollection",
"Feature": "geeVectorImage",
"FeatureCollection": "geeVectorImage",
"Geometry": "geeVectorImage",
"dict": "geoJSONVector",
}
try:
self.isNotebook = ee.oauth._in_jupyter_shell()
except:
self.isNotebook = ee.oauth.in_jupyter_shell()
try:
self.isColab = ee.oauth._in_colab_shell()
except:
self.isColab = ee.oauth.in_colab_shell()
self.proxy_url = None
self.refreshTokenPath = ee.oauth.get_credentials_path()
self.serviceKeyPath = None
self.queryWindowMode = "sidePane"
self.project = project_id
self.turnOffLayersWhenTimeLapseIsOn = True
######################################################################
# Function for adding a layer to the map
def addLayer(self, image: ee.Image | ee.ImageCollection | ee.Geometry | ee.Feature | ee.FeatureCollection, viz: dict = {}, name: str | None = None, visible: bool = True):
"""
Adds GEE object to the mapper object that will then be added to the map user interface with a `view` call.
Args:
image (ImageCollection, Image, Feature, FeatureCollection, Geometry): ee object to add to the map UI.
viz (dict): Primary set of parameters for map visualization, querying, charting, etc. In addition to the parameters supported by the addLayer function in the GEE Code Editor, there are several additional parameters available to help facilitate legend generation, querying, and area summaries. The accepted keys are:
{
"min" (int, list, or comma-separated numbers): One numeric value or one per band to map onto 00.,
"max" (int, list, or comma-separated numbers): One numeric value or one per band to map onto FF,
"gain" (int, list, or comma-separated numbers): One numeric value or one per band to map onto 00-FF.,
"bias" (int, list, or comma-separated numbers): One numeric value or one per band to map onto 00-FF.,
"gamma" (int, list, or comma-separated numbers): Gamma correction factor. One numeric value or one per band.,
"palette" (str, list, or comma-separated strings): List of CSS-style color strings (single-band previews only).,
"opacity" (float): a number between 0 and 1 for initially set opacity.,
"layerType" (str, one of geeImage, geeImageCollection, geeVector, geeVectorImage, geoJSONVector): Optional parameter. For vector data ("featureCollection", "feature", or "geometry"), you can spcify "geeVector" if you would like to force the vector to be an actual vector object on the client. This can be slow if the ee object is large and/or complex. Otherwise, any "featureCollection", "feature", or "geometry" will default to "geeVectorImage" where the vector is rasterized on-the-fly for map rendering. Any querying of the vector will query the underlying vector data though. To add a geojson vector as json, just add the json as the image parameter.,
"reducer" (Reducer, default 'ee.Reducer.lastNonNull()'): If an ImageCollection is provided, how to reduce it to create the layer that is shown on the map. Defaults to ee.Reducer.lastNonNull(),
"autoViz" (bool): Whether to take image bandName_class_values, bandName_class_names, bandName_class_palette properties to visualize, create a legend (populates `classLegendDict`), and apply class names to any query functions (populates `queryDict`),
"canQuery" (bool, default True): Whether a layer can be queried when visible.,
"addToLegend" (bool, default True): Whether geeViz should try to create a legend for this layer. Sometimes setting it to `False` is useful for continuous multi-band inputs.,
"classLegendDict" (dict): A dictionary with a key:value of the name:color(hex) to include in legend. This is auto-populated when `autoViz` : True,
"queryDict" (dict): A dictionary with a key:value of the queried number:label to include if queried numeric values have corresponding label names. This is auto-populated when `autoViz` : True,
"queryParams" (dict, optional): Dictionary of additional parameters for querying visible map layers:
{
"palette" (list, or comma-separated strings): List of hex codes for colors for charts. This is especially useful when bandName_class_values, bandName_class_names, bandName_class_palette properties are not available, but there is a desired set of colors for each band to have on the chart.,
"yLabel" (str, optional): Y axis label for query charts. This is useful when bandName_class_values, bandName_class_names, bandName_class_palette properties are not available, but there is a desired label for the Y axis.
}
"legendLabelLeftBefore" (str) : Label for continuous legend on the left before the numeric component,
"legendLabelLeftAfter" (str) : Label for continuous legend on the left after the numeric component,
"legendLabelRightBefore" (str) : Label for continuous legend on the right before the numeric component,
"legendLabelRightAfter" (str) : Label for continuous legend on the right after the numeric component,
"canAreaChart" (bool): whether to include this layer for area charting. If the layer is complex, area charting can be quite slow,
"areaChartParams" (dict, optional): Dictionary of additional parameters for area charting:
{
"reducer" (Reducer, default `ee.Reducer.mean()` if no bandName_class_values, bandName_class_names, bandName_class_palette properties are available. `ee.Reducer.frequencyHistogram` if those are available or `thematic`:True (see below)): The reducer used to compute zonal summary statistics.,
"crs" (str, default "EPSG:5070"): the coordinate reference system string to use for are chart zonal stats,
"transform" (list, default [30, 0, -2361915, 0, -30, 3177735]): the transform to snap to for zonal stats,
"scale" (int, default None): The spatial resolution to use for zonal stats. Only specify if transform : None.
"line" (bool, default True): Whether to create a line chart,
"sankey" (bool, default False): Whether to create Sankey charts - only available for thematic (discrete) inputs that have a `system:time_start` property set for each image,
"sankeyTransitionPeriods" (list of lists, default None): The years to use as transition periods for sankey charts (e.g. [[1985,1987],[2000,2002],[2020,2022]]). If not provided, users can enter years in the map user interface under `Area Tools -> Transition Charting Periods`. These will automatically be used for any layers where no sankeyTransitionPeriods were provided. If years are provided, the years in the user interface will not be used for that layer.
"sankeyMinPercentage" (float, default 0.5): The minimum percentage a given class has to be to be shown in the sankey chart.
"thematic" (bool): Whether input has discrete values or not. If True, it forces the reducer to `ee.Reducer.frequencyHistogram()` even if not specified and even if bandName_class_values, bandName_class_names, bandName_class_palette properties are not available,
"palette" (list, or comma-separated strings): List of hex codes for colors for charts. This is especially useful when bandName_class_values, bandName_class_names, bandName_class_palette properties are not available, but there is a desired set of colors for each band to have on the chart,
"showGrid" (bool, default True): Whether to show the grid lines on the line or bar graph,
"rangeSlider" (bool,default False): Whether to include the x-axis range selector on the bottom of each graph (`https://plotly.com/javascript/range-slider/>`)
"barChartMaxClasses" (int, default 20): The maximum number of classes to show for image bar charts. Will automatically only show the top `bartChartMaxClasses` in any image bar chart. Any downloaded csv table will still have all of the class counts.
"minZoomSpecifiedScale" (int, default 11): The map zoom level where any lower zoom level, not including this zoom level, will multiply the spatial resolution used for the zonal stats by 2 for each lower zoom level. E.g. if the `minZoomSpecifiedScale` is 9 and the `scale` is 30, any zoom level >= 9 will compute zonal stats at 30m spatial resolution. Then, at zoom level 8, it will be 60m. Zoom level 7 will be 120m, etc.
}
}
name (str): Descriptive name for map layer that will be shown on the map UI
visible (bool, default True): Whether layer should be visible when map UI loads
>>> import geeViz.geeView as gv
>>> Map = gv.Map
>>> ee = gv.ee
>>> nlcd = ee.ImageCollection("USGS/NLCD_RELEASES/2021_REL/NLCD").select(['landcover'])
>>> Map.addLayer(nlcd, {"autoViz": True}, "NLCD Land Cover / Land Use 2021")
>>> Map.turnOnInspector()
>>> Map.view()
"""
if name == None:
name = f"Layer {self.layerNumber}"
self.layerNumber += 1
print("Adding layer: " + name)
# Make sure not to update viz dictionary elsewhere
viz = dict(viz)
# Handle reducer if ee object is given
if "reducer" in viz.keys():
try:
viz["reducer"] = viz["reducer"].serialize()
except Exception as e:
try:
viz["reducer"] = eval(viz["reducer"]).serialize()
except Exception as e: # Most likely it's already serialized
e = e
if "areaChartParams" in viz.keys():
if "reducer" in viz["areaChartParams"].keys():
try:
viz["areaChartParams"]["reducer"] = viz["areaChartParams"]["reducer"].serialize()
except Exception as e:
try:
viz["areaChartParams"]["reducer"] = eval(viz["areaChartParams"]["reducer"]).serialize()
except Exception as e: # Most likely it's already serialized
e = e
# Get the id and populate dictionarye
idDict = {}
if "layerType" not in viz.keys():
imageType = type(image).__name__
layerType = self.typeLookup[imageType]
if imageType == "Geometry":
image = ee.FeatureCollection([ee.Feature(image)])
elif imageType == "Feature":
image = ee.FeatureCollection([image])
print(layerType)
viz["layerType"] = layerType
if not isinstance(image, dict):
image = image.serialize()
idDict["item"] = image
idDict["function"] = "addSerializedLayer"
# Handle passing in geojson vector layers
else:
idDict["item"] = json.dumps(image)
viz["layerType"] = "geoJSONVector"
idDict["function"] = "addLayer"
idDict["objectName"] = "Map"
idDict["name"] = name
idDict["visible"] = str(visible).lower()
idDict["viz"] = json.dumps(viz, sort_keys=False)
self.idDictList.append(idDict)
######################################################################
# Function for adding a layer to the map
def addTimeLapse(self, image: ee.ImageCollection, viz: dict = {}, name: str | None = None, visible: bool = True):
"""
Adds GEE ImageCollection object to the mapper object that will then be added as an interactive time lapse in the map user interface with a `view` call.
Args:
image (ImageCollection): ee ImageCollecion object to add to the map UI.
viz (dict): Primary set of parameters for map visualization, querying, charting, etc. These are largely the same as the `addLayer` function. Keys unique to `addTimeLapse` are provided here first. In addition to the parameters supported by the `addLayer` function in the GEE Code Editor, there are several additional parameters available to help facilitate legend generation, querying, and area summaries. The accepted keys are:
{
"mosaic" (bool, default False): If an ImageCollection with multiple images per time step is provided, how to reduce it to create the layer that is shown on the map. Uses ee.Reducer.lastNonNull() if True or ee.Reducer.first() if False,
"dateFormat" (str, default "YYYY"): The format of the date to show in the slider. E.g. if your data is annual, generally "YYYY" is best. If it's monthly, generally "YYYYMM" is best. Daily, generally "YYYYMMdd"...etc.,
"advanceInterval" (str, default 'year'): How much to advance each frame when creating each individual mosaic. One of 'year', 'month' 'week', 'day', 'hour', 'minute', or 'second'.
"min" (int, list, or comma-separated numbers): One numeric value or one per band to map onto 00.,
"max" (int, list, or comma-separated numbers): One numeric value or one per band to map onto FF,
"gain" (int, list, or comma-separated numbers): One numeric value or one per band to map onto 00-FF.,
"bias" (int, list, or comma-separated numbers): One numeric value or one per band to map onto 00-FF.,
"gamma" (int, list, or comma-separated numbers): Gamma correction factor. One numeric value or one per band.,
"palette" (str, list, or comma-separated strings): List of CSS-style color strings (single-band previews only).,
"opacity" (float): a number between 0 and 1 for initially set opacity.,
"autoViz" (bool): Whether to take image bandName_class_values, bandName_class_names, bandName_class_palette properties to visualize, create a legend (populates `classLegendDict`), and apply class names to any query functions (populates `queryDict`),
"canQuery" (bool, default True): Whether a layer can be queried when visible.,
"addToLegend" (bool, default True): Whether geeViz should try to create a legend for this layer. Sometimes setting it to `False` is useful for continuous multi-band inputs.,
"classLegendDict" (dict): A dictionary with a key:value of the name:color(hex) to include in legend. This is auto-populated when `autoViz` : True,
"queryDict" (dict): A dictionary with a key:value of the queried number:label to include if queried numeric values have corresponding label names. This is auto-populated when `autoViz` : True,
"queryParams" (dict, optional): Dictionary of additional parameters for querying visible map layers:
{
"palette" (list, or comma-separated strings): List of hex codes for colors for charts. This is especially useful when bandName_class_values, bandName_class_names, bandName_class_palette properties are not available, but there is a desired set of colors for each band to have on the chart.,
"yLabel" (str, optional): Y axis label for query charts. This is useful when bandName_class_values, bandName_class_names, bandName_class_palette properties are not available, but there is a desired label for the Y axis.
}
"legendLabelLeftBefore" (str) : Label for continuous legend on the left before the numeric component,
"legendLabelLeftAfter" (str) : Label for continuous legend on the left after the numeric component,
"legendLabelRightBefore" (str) : Label for continuous legend on the right before the numeric component,
"legendLabelRightAfter" (str) : Label for continuous legend on the right after the numeric component,
"canAreaChart" (bool): whether to include this layer for area charting. If the layer is complex, area charting can be quite slow,
"areaChartParams" (dict, optional): Dictionary of additional parameters for area charting:
{
"reducer" (Reducer, default `ee.Reducer.mean()` if no bandName_class_values, bandName_class_names, bandName_class_palette properties are available. `ee.Reducer.frequencyHistogram` if those are available or `thematic`:True (see below)): The reducer used to compute zonal summary statistics.,
"crs" (str, default "EPSG:5070"): the coordinate reference system string to use for are chart zonal stats,
"transform" (list, default [30, 0, -2361915, 0, -30, 3177735]): the transform to snap to for zonal stats,
"scale" (int, default None): The spatial resolution to use for zonal stats. Only specify if transform : None.
"line" (bool, default True): Whether to create a line chart,
"sankey" (bool, default False): Whether to create Sankey charts - only available for thematic (discrete) inputs that have a `system:time_start` property set for each image,
"sankeyTransitionPeriods" (list of lists, default None): The years to use as transition periods for sankey charts (e.g. [[1985,1987],[2000,2002],[2020,2022]]). If not provided, users can enter years in the map user interface under `Area Tools -> Transition Charting Periods`. These will automatically be used for any layers where no sankeyTransitionPeriods were provided. If years are provided, the years in the user interface will not be used for that layer.
"sankeyMinPercentage" (float, default 0.5): The minimum percentage a given class has to be to be shown in the sankey chart.
"thematic" (bool): Whether input has discrete values or not. If True, it forces the reducer to `ee.Reducer.frequencyHistogram()` even if not specified and even if bandName_class_values, bandName_class_names, bandName_class_palette properties are not available,
"palette" (list, or comma-separated strings): List of hex codes for colors for charts. This is especially useful when bandName_class_values, bandName_class_names, bandName_class_palette properties are not available, but there is a desired set of colors for each band to have on the chart,
"showGrid" (bool, default True): Whether to show the grid lines on the line or bar graph,
"rangeSlider" (bool,default False): Whether to include the x-axis range selector on the bottom of each graph (`https://plotly.com/javascript/range-slider/>`)
"barChartMaxClasses" (int, default 20): The maximum number of classes to show for image bar charts. Will automatically only show the top `bartChartMaxClasses` in any image bar chart. Any downloaded csv table will still have all of the class counts.
"minZoomSpecifiedScale" (int, default 11): The map zoom level where any lower zoom level, not including this zoom level, will multiply the spatial resolution used for the zonal stats by 2 for each lower zoom level. E.g. if the `minZoomSpecifiedScale` is 9 and the `scale` is 30, any zoom level >= 9 will compute zonal stats at 30m spatial resolution. Then, at zoom level 8, it will be 60m. Zoom level 7 will be 120m, etc.
}
}
name (str): Descriptive name for map layer that will be shown on the map UI
visible (bool, default True): Whether layer should be visible when map UI loads
>>> import geeViz.geeView as gv
>>> Map = gv.Map
>>> ee = gv.ee
>>> lcms = ee.ImageCollection("USFS/GTAC/LCMS/v2023-9").filter(ee.Filter.calendarRange(2010, 2023, "year"))
>>> Map.addTimeLapse(lcms.select(["Land_Cover"]), {"autoViz": True, "mosaic": True}, "LCMS Land Cover Time Lapse")
>>> Map.addTimeLapse(lcms.select(["Change"]), {"autoViz": True, "mosaic": True}, "LCMS Change Time Lapse")
>>> Map.addTimeLapse(lcms.select(["Land_Use"]), {"autoViz": True, "mosaic": True}, "LCMS Land Use Time Lapse")
>>> Map.turnOnInspector()
>>> Map.view()
"""
if name == None:
name = "Layer " + str(self.layerNumber)
self.layerNumber += 1
print("Adding layer: " + name)
# Make sure not to update viz dictionary elsewhere
viz = dict(viz)
# Handle reducer if ee object is given - delete it
if "reducer" in viz.keys():
del viz["reducer"]
# Handle area charting reducer
if "areaChartParams" in viz.keys():
if "reducer" in viz["areaChartParams"].keys():
try:
viz["areaChartParams"]["reducer"] = viz["areaChartParams"]["reducer"].serialize()
except Exception as e:
try:
viz["areaChartParams"]["reducer"] = eval(viz["areaChartParams"]["reducer"]).serialize()
except Exception as e: # Most likely it's already serialized
e = e
viz["layerType"] = "ImageCollection"
# Get the id and populate dictionary
idDict = {} # image.getMapId()
idDict["objectName"] = "Map"
idDict["item"] = image.serialize()
idDict["name"] = name
idDict["visible"] = str(visible).lower()
idDict["viz"] = json.dumps(viz, sort_keys=False)
idDict["function"] = "addSerializedTimeLapse"
self.idDictList.append(idDict)
######################################################################
# Function for adding a select layer to the map
def addSelectLayer(self, featureCollection: ee.FeatureCollection, viz: dict = {}, name: str | None = None):
"""
Adds GEE featureCollection to the mapper object that will then be added as an interactive selection layer in the map user interface with a `view` call. This layer will be availble for selecting areas to include in area summary charts.
Args:
featureCollection (FeatureCollection): ee FeatureCollecion object to add to the map UI as a selectable layer, where each feature is selectable by clicking on it.
viz (dict, optional): Primary set of parameters for map visualization and specifying which feature attribute to use as the feature name (selectLayerNameProperty), etc. In addition to the parameters supported by the `addLayer` function in the GEE Code Editor, there are several additional parameters available to help facilitate legend generation, querying, and area summaries. The accepted keys are:
{
"strokeColor" (str, default random color): The color of the selection layer on the map,
"strokeWeight" (int, default 3): The thickness of the polygon outlines,
"selectLayerNameProperty" (str, default first feature attribute with "name" in it or "system:index"): The attribute name to show when a user selects a feature.
}
name (str, default None): Descriptive name for map layer that will be shown on the map UI. Will be auto-populated with `Layer N` if not specified
>>> import geeViz.geeView as gv
>>> Map = gv.Map
>>> ee = gv.ee
>>> lcms = ee.ImageCollection("USFS/GTAC/LCMS/v2023-9").filter('study_area=="CONUS"')
>>> Map.addLayer(lcms, {"autoViz": True, "canAreaChart": True, "areaChartParams": {"line": True, "sankey": True}}, "LCMS")
>>> mtbsBoundaries = ee.FeatureCollection("USFS/GTAC/MTBS/burned_area_boundaries/v1")
>>> mtbsBoundaries = mtbsBoundaries.map(lambda f: f.set("system:time_start", f.get("Ig_Date")))
>>> Map.addSelectLayer(mtbsBoundaries, {"strokeColor": "00F", "selectLayerNameProperty": "Incid_Name"}, "MTBS Fire Boundaries")
>>> Map.turnOnSelectionAreaCharting()
>>> Map.view()
"""
if name == None:
name = "Layer " + str(self.layerNumber)
self.layerNumber += 1
# Make sure not to update viz dictionary elsewhere
viz = dict(viz)
print("Adding layer: " + name)
# Get the id and populate dictionary
idDict = {} # image.getMapId()
idDict["objectName"] = "Map"
idDict["item"] = featureCollection.serialize()
idDict["name"] = name
idDict["visible"] = str(False).lower()
idDict["viz"] = json.dumps(viz, sort_keys=False)
idDict["function"] = "addSerializedSelectLayer"
self.idDictList.append(idDict)
######################################################################
# Function for centering on a GEE object that has a geometry
def setCenter(self, lng: float, lat: float, zoom: int | None = None):
"""
Center the map on a specified point and optional zoom on loading
Args:
lng (int or float): The longitude to center the map on
lat (int or float): The latitude to center the map on
zoom (int, optional): If provided, will force the map to zoom to this level after centering it on the provided coordinates. If not provided, the current zoom level will be used.
>>> from geeViz.geeView import *
>>> Map.setCenter(-111,41,10)
>>> Map.view()
"""
command = f"Map.setCenter({lng},{lat},{json.dumps(zoom)})"
self.mapCommandList.append(command)
######################################################################
# Function for setting the map zoom
def setZoom(self, zoom: int):
"""
Set the map zoom level
Args:
zoom (int): The zoom level to set the map to on loading.
>>> from geeViz.geeView import *
>>> Map.setZoom(10)
>>> Map.view()
"""
self.mapCommandList.append(f"map.setZoom({zoom})")
######################################################################
# Function for centering on a GEE object that has a geometry
def centerObject(self, feature: ee.Geometry | ee.Feature | ee.FeatureCollection | ee.Image, zoom: int | None = None):
"""
Center the map on an object on loading
Args:
feature (Feature, FeatureCollection, or Geometry): The object to center the map on
zoom (int, optional): If provided, will force the map to zoom to this level after centering it on the object. If not provided, the highest zoom level that allows the feature to be viewed fully will be used.
>>> from geeViz.geeView import *
>>> pt = ee.Geometry.Point([-111, 41])
>>> Map.addLayer(pt.buffer(10), {}, "Plot")
>>> Map.centerObject(pt)
>>> Map.view()
"""
try:
bounds = json.dumps(feature.geometry().bounds(100, "EPSG:4326").getInfo())
except Exception as e:
bounds = json.dumps(feature.bounds(100, "EPSG:4326").getInfo())
command = "synchronousCenterObject(" + bounds + ")"
self.mapCommandList.append(command)
if zoom != None:
self.setZoom(zoom)
######################################################################
# Function for launching the web map after all adding to the map has been completed
def view(self, open_browser: bool | None = None, open_iframe: bool | None = None, iframe_height: int = 525):
"""
Compiles all map objects and commands and starts the map server
Args:
open_browser (bool): Whether or not to open the browser. If unspecified, will automatically be selected depending on whether geeViz is being used in a notebook (False) or not (True).
open_iframe (bool): Whether or not to open an iframe. If unspecified, will automatically be selected depending on whether geeViz is being used in a notebook (True) or not (False).
iframe_height (int, default 525): The height of the iframe shown if running inside a notebook
>>> from geeViz.geeView import *
>>> lcms = ee.ImageCollection("USFS/GTAC/LCMS/v2023-9").filter('study_area=="CONUS"')
>>> Map.addLayer(lcms, {"autoViz": True, "canAreaChart": True, "areaChartParams": {"line": True, "sankey": True}}, "LCMS")
>>> Map.turnOnInspector()
>>> Map.view()
"""
print("Starting webmap")
# Get access token
if self.serviceKeyPath == None:
print("Using default refresh token for geeView")
self.accessToken = refreshToken()
self.accessTokenCreationTime = int(datetime.datetime.now().timestamp() * 1000)
else:
print("Using service account key for geeView:", self.serviceKeyPath)
self.accessToken = serviceAccountToken(self.serviceKeyPath)
if self.accessToken == None:
print("Trying to authenticate to GEE using persistent refresh token.")
self.accessToken = refreshToken(self.refreshTokenPath)
self.accessTokenCreationTime = int(datetime.datetime.now().timestamp() * 1000)
else:
self.accessTokenCreationTime = None
# Set up js code to populate
lines = "var layerLoadErrorMessages=[];showMessage('Loading',staticTemplates.loadingModal[mode]);function runGeeViz(){"
# Iterate across each map layer to add js code to
for idDict in self.idDictList:
t = "{}.{}({},{},'{}',{});".format(
idDict["objectName"],
idDict["function"],
idDict["item"],
idDict["viz"],
idDict["name"],
str(idDict["visible"]).lower(),
)
# t = (
# "try{\n\t"
# + t
# + '\n}catch(err){\n\tlayerLoadErrorMessages.push("Error loading: '
# + idDict["name"]
# + '<br>GEE "+err);}\n'
# )
lines += t
lines += 'if(layerLoadErrorMessages.length>0){showMessage("Map.addLayer Error List",layerLoadErrorMessages.join("<br>"));};'
lines += "setTimeout(function(){if(layerLoadErrorMessages.length===0){$('#close-modal-button').click();}}, 2500);"
# Iterate across each map command
for mapCommand in self.mapCommandList:
lines += mapCommand + ";"
# Set location of query outputs
lines += 'queryWindowMode = "{}";'.format(self.queryWindowMode)
# Set whether all layers are turned off when a time lapse is turned on
lines += "Map.turnOffLayersWhenTimeLapseIsOn = {};".format(str(self.turnOffLayersWhenTimeLapseIsOn).lower())
lines += "};"
# Write out js file
self.ee_run = os.path.join(ee_run_dir, "{}.js".format(self.ee_run_name))
oo = open(self.ee_run, "w")
oo.writelines(lines)
oo.close()
# Find if port is already active and only start it if it is not
if not isPortActive(self.port):
print("Starting local web server at: http://localhost:{}/{}/".format(self.port, geeViewFolder))
run_local_server(self.port)
print("Done")
else:
print("Local web server at: http://localhost:{}/{}/ already serving.".format(self.port, geeViewFolder))
# Open viewer in browser or iframe in notebook
print("cwd", os.getcwd())