From de3ddb4245a2ce159a59291f8840e801741657c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Gallego=20Jim=C3=A9nez?= <91949040+AlbertGallegoJimenez@users.noreply.github.com> Date: Wed, 10 Jan 2024 13:08:10 +0100 Subject: [PATCH] New release Among other things, a new functionality has been added related to the plotting of results. More info at release description. --- README.md | 15 +- requirements.txt | 11 + src/tools/correctTransects.py | 4 +- src/tools/generateTransects.py | 1 + src/tools/performAnalysis.py | 28 +- src/tools/plotResults.py | 122 +++++++ .../intersect_lines.cpython-39.pyc | Bin 3015 -> 3011 bytes .../__pycache__/plot_results.cpython-39.pyc | Bin 0 -> 10671 bytes .../shoreline_evolution.cpython-39.pyc | Bin 3556 -> 3552 bytes .../transect_processor.cpython-39.pyc | Bin 2664 -> 2894 bytes src/tools/utils/plot_results.py | 316 ++++++++++++++++++ src/tools/utils/transect_processor.py | 9 +- 12 files changed, 482 insertions(+), 24 deletions(-) create mode 100644 requirements.txt create mode 100644 src/tools/plotResults.py create mode 100644 src/tools/utils/__pycache__/plot_results.cpython-39.pyc create mode 100644 src/tools/utils/plot_results.py diff --git a/README.md b/README.md index 937266f..1345f98 100644 --- a/README.md +++ b/README.md @@ -36,13 +36,12 @@ This tool is developed as part of a [Python Toolbox](https://pro.arcgis.com/en/p ## Getting Started ### Prerequisites -The following is a list of the programs and libraries used in the tool, with their respective versions: -* ArcGIS Pro - ```arcpy``` (version 3.1) -* ```pandas``` (version 1.4) -* ```numpy``` (version 1.20) -* ```shapely``` (version 2.0) -* ```statsmodels``` (version 0.13) +Check that you have installed all the required libraries used in the toolbox. All packages with their tested versions are listed in [requirements.txt](https://github.com/AlbertGallegoJimenez/shoreline-evolution-tool/tree/main/requirements.txt). Note that when working with a cloned version of the ArcGIS ```anaconda``` environment there are already pre-installed libraries, those that are not and need to be installed manually are the following: + +* ```shapely``` +* ```statsmodels``` (from ArcGIS version 3.2 this package is included in the base ArcGIS ```anaconda``` environment) +* ```cartopy``` In terms of data, this tool relies on the use of the following two files: * **Baseline** (Vector - Polyline). This is the reference line used to assess the evolution of the coastal stretch. It can be digitized manually by the user with the help of a background orthophoto, it is recommended to place the baseline **inland**. The baseline must capture the general orientation of the coast. @@ -51,8 +50,8 @@ In terms of data, this tool relies on the use of the following two files: ### Installation -0. Make sure you have cloned the base ArcGIS' ```anaconda``` environment so you can install more packages. More info [here](https://pro.arcgis.com/en/pro-app/latest/arcpy/get-started/clone-an-environment.htm). -1. Install both ```shapely``` and ```statsmodels``` packages. +0. Make sure you have cloned the base ArcGIS ```anaconda``` environment so you can install more packages. More info [here](https://pro.arcgis.com/en/pro-app/latest/arcpy/get-started/clone-an-environment.htm). +1. Install ```shapely```, ```statsmodels``` and ```cartopy``` packages if you do not have it installed yet. 2. Download the content in the [src](https://github.com/AlbertGallegoJimenez/shoreline-evolution-tool/tree/main/src) folder. 3. Open the Catalog Pane in ArcGIS Pro and open the downloaded Toolbox (.pyt) to see the tools.
diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4e34f5d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,11 @@ +# Name Version +arcgispro (software) 3.1 +arcpy 3.1 +cartopy 0.21.1 +matplotlib 3.6.0 +numpy 1.20.1 +pandas 1.4.4 +regex 2022.7.9 +seaborn 0.12.1 +shapely 2.0.1 +statsmodels 0.13.5 \ No newline at end of file diff --git a/src/tools/correctTransects.py b/src/tools/correctTransects.py index de24aa9..c718fd9 100644 --- a/src/tools/correctTransects.py +++ b/src/tools/correctTransects.py @@ -74,7 +74,6 @@ def execute(self, parameters, messages): # Add Bearing attribute arcpy.management.CalculateGeometryAttributes(transectsFeature, "Bearing LINE_BEARING") - cursor = arcpy.da.SearchCursor(transectsFeature, ["transect_id", "Bearing"]) df = pd.DataFrame(data=[row for row in cursor], columns=["transect_id", "Bearing"]) @@ -87,7 +86,7 @@ def execute(self, parameters, messages): # Make the changes in the feature class RotateFeatures(df, transectsFeature) - # Recalulate the bearing with the transects inverted + # Recalculate the bearing with the transects inverted arcpy.management.CalculateGeometryAttributes(transectsFeature, "Bearing LINE_BEARING") cursor = arcpy.da.SearchCursor(transectsFeature, ["transect_id", "Bearing"]) df = pd.DataFrame(data=[row for row in cursor], columns=["transect_id", "Bearing"]) @@ -111,6 +110,7 @@ def execute(self, parameters, messages): def postExecute(self, parameters): """This method takes place after outputs are processed and added to the display.""" + # Update the labels of the transect feature aprx = arcpy.mp.ArcGISProject('CURRENT') aprxMap = aprx.activeMap transectsFeature = aprxMap.listLayers(os.path.basename(parameters[0].valueAsText))[0] diff --git a/src/tools/generateTransects.py b/src/tools/generateTransects.py index ca09340..ed2f7aa 100644 --- a/src/tools/generateTransects.py +++ b/src/tools/generateTransects.py @@ -108,6 +108,7 @@ def execute(self, parameters, messages): def postExecute(self, parameters): """This method takes place after outputs are processed and added to the display.""" + # Update the labels of the transect feature aprx = arcpy.mp.ArcGISProject('CURRENT') aprxMap = aprx.activeMap transectsFeature = aprxMap.listLayers(os.path.basename(parameters[0].valueAsText))[0] diff --git a/src/tools/performAnalysis.py b/src/tools/performAnalysis.py index fdfb923..432ad2e 100644 --- a/src/tools/performAnalysis.py +++ b/src/tools/performAnalysis.py @@ -81,11 +81,9 @@ def execute(self, parameters, messages): for field in metrics_fields: arcpy.management.AddField(transectsFeature, field, "DOUBLE") - count = 0 with arcpy.da.UpdateCursor(transectsFeature, metrics_fields) as cursor: - for row in cursor: - cursor.updateRow(shore_metrics.loc[count, metrics_fields].tolist()) - count += 1 + for i, _ in enumerate(cursor): + cursor.updateRow(shore_metrics.loc[i, metrics_fields].tolist()) return @@ -102,8 +100,6 @@ def postExecute(self, parameters): # Get the current symbology settings of the layer sym = transectsLayerObj.symbology - # Get the layer's CIM definition - cim = transectsLayerObj.getDefinition('V3') # Set the renderer to Graduated Colors sym.updateRenderer('GraduatedColorsRenderer') @@ -113,7 +109,6 @@ def postExecute(self, parameters): # Specify the field used for classification sym.renderer.classificationField = 'LRR' - cim.renderer.heading = 'LRR (m/year)' # Set the number of class breaks sym.renderer.breakCount = 6 @@ -125,7 +120,7 @@ def postExecute(self, parameters): labels = ["-50.0 - -4.0", "-4.0 - -2.0", "-2.0 - 0.0", "0.0 - 2.0", "2.0 - 4.0", "4.0 - 50.0"] # Define sizes for each class - sizes = [4, 2, 1, 1, 2, 4] + sizes = [6, 3, 1.5, 1.5, 3, 6] # Update values for each class for i, brk in enumerate(sym.renderer.classBreaks): @@ -135,6 +130,21 @@ def postExecute(self, parameters): # Apply the updated symbology settings to the layer transectsLayerObj.symbology = sym - transectsLayerObj.setDefinition(cim) + # Get the layer's CIM definition + cim = transectsLayerObj.getDefinition('V3') + + # Set the label for the symbolised field + cim.renderer.heading = 'LRR (m/year)' + + # Exclude non-significant transects and apply unique symbology + cim.renderer.useExclusionSymbol = True + cim.renderer.exclusionClause = 'Pvalue > 0.05' + cim.renderer.exclusionLabel = 'Non-significant transect' + cim.renderer.exclusionSymbol.symbol.symbolLayers[0].color.values = [130, 130, 130, 100] + cim.renderer.exclusionSymbol.symbol.symbolLayers[0].width = 1.5 + + # Update the CIM + transectsLayerObj.setDefinition(cim) + return \ No newline at end of file diff --git a/src/tools/plotResults.py b/src/tools/plotResults.py new file mode 100644 index 0000000..b695d67 --- /dev/null +++ b/src/tools/plotResults.py @@ -0,0 +1,122 @@ +import arcpy +from tools.utils.plot_results import PlottingUtils + +class PlotResults(object): + def __init__(self): + """Define the tool (tool name is the name of the class).""" + self.label = "5. Plot The Analysis Results" + self.description = "" + self.canRunInBackground = False + + def getParameterInfo(self): + """Define parameter definitions""" + + shoreline_param = arcpy.Parameter( + displayName="Input Shorelines Intersection Points Feature", + name="shore_features", + datatype="GPFeatureLayer", + parameterType="Required", + direction="Input") + shoreline_param.filter.list = ["Point"] + + transects_param = arcpy.Parameter( + displayName="Input Transects Feature", + name="transects_features", + datatype="GPFeatureLayer", + parameterType="Required", + direction="Input") + transects_param.filter.list = ["Polyline"] + + transects_ID_2plot_param = arcpy.Parameter( + displayName="Input Transects ID To Plot", + name="transects_ID_2plot", + datatype="GPValueTable", + parameterType="Required", + direction="Input") + transects_ID_2plot_param.columns = [['GPLong', 'Transects ID']] + + parameters = [shoreline_param, transects_param, transects_ID_2plot_param] + + return parameters + + def isLicensed(self): + """Set whether tool is licensed to execute.""" + return True + + def updateParameters(self, parameters): + """Modify the values and properties of parameters before internal + validation is performed. This method is called whenever a parameter + has been changed.""" + return + + def updateMessages(self, parameters): + """Modify the messages created by internal validation for each tool + parameter. This method is called after internal validation.""" + parameters[2].setWarningMessage( + "Check significance before selecting transects (Pvalue < 0.05).") + return + + def execute(self, parameters, messages): + """The source code of the tool.""" + shoreFeatures = parameters[0].valueAsText + transectsFeature = parameters[1].valueAsText + transectsID_2plot = parameters[2].value + transectsID_2plot = [id[0] for id in transectsID_2plot] # As the parameter value is passed as a list of lists, instead of a list of integers + + # Check for errors in the transect IDs selected + self._check_transects_id(transectsID_2plot, transectsFeature) + + # Initialize the class + plotter = PlottingUtils(transects=transectsFeature, + shore_intersections=shoreFeatures) + + # Plot the spatial evolution + plotter.plot_spatial_evolution() + + # Plot the time series for the transects selected + plotter.plot_time_series(transects2plot=transectsID_2plot) + + # Plot the seasonality for the transects selected. Set a minimum of 2 years of data to plot the seasonality. + if plotter.shore_intersections_df['date'].dt.year.max() - plotter.shore_intersections_df['date'].dt.year.min() >= 2: + plotter.plot_seasonality(transects2plot=transectsID_2plot) + + # Plot the LRR map + plotter.plot_map('LRR') + + # Plot the SCE map + plotter.plot_map('SCE') + + # Plot the NSM map + plotter.plot_map('NSM') + + return + + def postExecute(self, parameters): + """This method takes place after outputs are processed and + added to the display.""" + return + + def _check_transects_id(self, transectsID_2plot, transectsFeature): + """This private method checks that all transects + passed by the user are valid.""" + + # Check that all IDs are numerical. This code may be unnecessary since the parameter is defined as 'GPLong' and this means that ArcGIS will only accept numerical values. + if not all(isinstance(id, (int, float)) for id in transectsID_2plot): + arcpy.AddError('Invalid transect ID, check that all IDs are numeric.') + raise Exception('Invalid transect ID, check that all IDs are numeric.') + + # Check for duplicates + if len(transectsID_2plot) != len(set(transectsID_2plot)): + arcpy.AddError('Some transects are repeated.') + raise Exception('Some transects are repeated.') + + # Check if the user only has selected one transect + if len(transectsID_2plot) == 1: + arcpy.AddError('Please, select more than one transect to plot.') + raise Exception('Please, select more than one transect to plot.') + + # Check for ID out of range + list_transect_id = [row[0] for row in arcpy.da.SearchCursor(transectsFeature, ['transect_id'])] + if set(transectsID_2plot) - set(list_transect_id): + arcpy.AddError('Invalid transect ID, check that all IDs are within the valid range.') + raise Exception('Invalid transect ID, check that all IDs are within the valid range.') diff --git a/src/tools/utils/__pycache__/intersect_lines.cpython-39.pyc b/src/tools/utils/__pycache__/intersect_lines.cpython-39.pyc index cf462719483fd49ecdbbe66233daeb183cbbf2c7..11a22c90dd9ff85ad22125fe921e1b822a724730 100644 GIT binary patch delta 114 zcmX>ueps9@k(ZZ?0SMe?G^Oqn+{o9@ByH(p6;qO0T%u5%kzbUWlbM&QP?DdY6H{E2 zym=4PSyslV$z2?p;!&*Wsrk9ZMT$V(w>UD3<5P1BN-Co!pW@ihsI$3?^9&=S-sEs@ PGd3Ndf+D@iletX*F<~S$ delta 102 zcmX>seq5X{k(ZZ?0SH`9Nu{a_ZRG1`lDBoSiU}ynuS`uY$yW%=EG|h5&d-Sn$l1~E`eb2} z)2(2tK3$mZwRfyOQ#j7?L@-;QE6nMdm#EEmPY8QYM|u)pCxvrW6Df|R5gXxH<|>su zRn^4kH+7NS)j4(Ss`i>D#>K=JH8JtfC@ctlP0LR{!rZjgysq?%c|)aEo3ic(e%n)- zI}e&`L9^vi67sWaWjAo;O}Eu@OM!_$#UR^p)BaTC&+Hmbc4+SB{6X9~y|hgeQChfu|L8p<$9=9EhcE4)#$CD8AX1h`ex(@VVQ)h|7RgW&kCOK z^9VY6M=NTPUNgF;(D(EmGqP%luH7+fNnz|6$aOd+)zNCso*up!S>1FT*R_t;9qFJJ zN@X~1aC|h%cE?a=RG4!brBLju7L9jwfl2LBs}sV;V3Y1uUDMLJ-rq{I5k_*e95rRG z?nN6-k*gr;H~h#aS;3rgLzBy~I4q|JE!=XYi<+JcUl^oO$HHt?lI- zo*QjSZ-vAu|3a?!nd9$Vvs6P_?)yN+Hy~WOs@FvO()Ysr@=y=qMpJr4zd?PYvX80! zw0&_CB*K{SP&2ohNI1G{m0WrA_U%_!@3eW_!<_WO%^(VuCEUnUBmMCgeSuS#ti;n+ zhA&njDye>Us1m#`%BZ*0=*zPF+DF%J$tEp>GMZ?uTlyJ-x0DrnK}8;ee5k&{R zx4-;Rvl*<1vb;`_aD9``(RvG~s@PlI<<_=xir8&GDi&wyFij%R9NWz3lX_Oq;GNND z%=XNXxGwjrlN`ec)Cd=V50O^0x(OUy98b#Y z7{b~$aF`PA#KFUKOZxXA0{w#&HFM==*c`%F@o){QSCvvd`0EONNVmQc=r0pNhG8SdLyI$X4wkP5O)y>fA|m~E-g~g)Xq*S@&|nV?l?RkJ(86U=+%r>j-lXgaZmPL}HFkk;QrDlX*Ld4!HKNAwMIMJ9fho&W_!&I(CK5 zRjU2p1EhcQP*uGs_vW3OIgmY}C2&7ikw=Zptl(6Clq8L4v^~RliDwZwh zx)+wEkLfTPkFSA8K;^f+3WnP#d-5{XFfmhQNDutv1AT%(CBjzVNBLwdY-U-81qX{z zEH{N$7~3BzC#dxPsui*aGLZ}dcygT%iK&_@Vi$|l$o?+Agn`AVo*FkT-7*&RdBf3d z!!p`u4hiv-wTA$n1UG?)h4)*CKqG?YH3xv-W3aCpbK1JG1Mvlzn|C$&kq*HAt|dPb zkzQs&FS84$qu25`xNIse z%k9Qx4Hoe|hH3A?l5Mmayc#I3OMjHcqRXuIX*niWWJY$?R;hJZ|u+o68@Ww>U^l{vRu zZc0I%05;>1k+xPoaO~boMP#+EoLhnhit0E-tH6KipIYe?3>u|L9cb=I<|@;AEY;nJ z3I~2^na39{lM3m|HLqItpy7!XG8Rx2o!vnv{ps(o4xMwF{!wimw3_IkKiS*B#Ok%* zUo4u?(4r1^=xs$_Lln!jocpUm13cKk(JRgqr%k5z_nTE_9g3q_cei4Qux^W2HuI*k z?o)d5ewmg-(x=jRcxMag_+3Po`*G@zs)6n?Vl{GL@`q7krU+>-qAWn?fTS1DRd^DCmYp}o z^)%E!Tc>ws@CIa@I|La=HZ??fG=(%CMvdKb*Rs7IvVL_gdL6c`+lg4k>hYN?3vx{9_&XhHAds+7#s_|*o2ro zT&oI&K&O?O6jNea9D|xLwWrml#f&&EW@(nayo8uLGVhp}7bnEYU4#3Y+0*!aT%6+3 zEdaA;(cawPYm~k!#!Ng;y*3}6sGaOm=>>5bibqbI5odQzxKW^U<`-A<#-j{2^%3#G zJJ7{|m?4}XlI#aHDWHkDpv5Ao5XF)kdeyhTMt|SA#s}vS5iHJ4!VOv*t}=oL%5DSi zaCS`hepQZA)1>3V2ucKQ$qQr_U$RKSXDOi0WeUM21*r4gZ(j)`kx#@0KSSAEx=gXp zB52R|M-?kJG%7Yym+E;*dT(!t@BC%;``@F#f4^2phMxPtl|m(?U)_k5?MJZR%NTiV zae^G`Y6=#!-zFtKKxVZ;m{>?vkiagSf)fBps1@m;mmj>J|Nj2kJ8&LR|6EAYfZI>? z)QSTpoOd5eE)&tx`Q=LPat>i$PEhI7Kce(GN>uh{!@su)m8L42o2}Bevg)4OP??G!1jUjU zJ@7mrB9>mI2@AF&zkD}TMx&){_%Iq(Pkw|Nv!f;%2faePrEz>ajyqgA@axKofL+S0 zmfb?U$S=b|rY!P{DLe40z;uxzR_{~St%R}pB&IBa3)P5);n#$g*fz4y$!hB2vAnEoHz zxd6CK>nHg?i_+72_Fd-*&^)@UAysP(J(6HEXuge;&XMD4ZP$_%iX?zHahE)_TAaU= zjGS(&0{~{nSF)JyB)cPf8qzb}QS_Lsr5}>REGbe~HTvpiBd0c28%N6yzR9zTwnUnG z+evlo+C<0cqyT3CX<-n2GKfvprYW5~e`2Czi%AA>yL(JbF*w?_8G=7C!$4+tkBj5c zY$x$hKOO^>U7HiLVs1Bv)OeP5e-;=Ex%lRJmh*#I&hwZE*bnCbFsr;i-4h^{Qy6)s zlZMauRA(eQ)yYswX9RP=dlq(BRYw}2bU&@CV_#19z>=sa-Cd~0BTWy@guWKVLT_Ga z%wihzILEW3uSIdH_ht3=cVSk$Ykb9!AMe=R)8c%*I^7)hWK>)bPkmAAjIxI%i~KX4 z(O&*SC)-=cvl!J9uTvWJ^OV|{9%`#MQb^jOxQMYmO{2MY1@wC^I$yiceF`hKBrbsr zE{kRHz5y9LvnoFcQI#bMNQ-zxh5`(97}HRrJqA}M2F5LA-GLWOuA|Ij{j$=V?bx@) zSlnuc@R7pjn71UE$I7m|@-Ap09lx1l2+|)BxZh|Wqgv4Kq*sMYD`bTo_Li;mEv0WO zqwLFjC@7f)ei5<9!z_>|{}OuTZ#V9B1C?@^U!5wx$|0(1`i^vjHH z)B@B@@-H9d9)fTAfwGR#Qe`P{idn(q)^QB#r%dxk>n1$#BD}O8U0%yQJ=eA{${L2|1

pIV`?7A znGZ1s%Pnf;3zRz6XZWHkYMbcbOO$mKOOC~kitNA}eT9en3)G5H4VCUIJ(6^g3Zvwn zEcOd3{gaZ84+gh&1uHCN!3@g2vU>`F-5cewB&qSk((>DABV0wGnX~#NQLDKCD}<;N zv}?`cn=F$_xTa_HdHsTZMYrFzIW3FytTAb%Q98>+4EMDKLN>;Y_QI_nDWpT>n?~<+ zYFF@tUw{$?;A0;L*(w75jvAcUh#9yG!Yxx|)9Hhkf-kyuEs(AWKS#1h!}?npq$EJN zM$JJjB3roVK!Q$8HakhU=-}9^fpEKN+*avCyF|Ba*T8~+T?0EP%COY|vINaL`~}j4 zX9V?dGvx>)ZifkVk}W50x0~&eY5X-NrkN7rKF5h%#c?K{IDbOSMw1k0zx;_n6=D~&^g30(x{tngqbqeUvFg?iMpxDe{(Ib%Wt8z_Tm~{_%ZnowB8)dp0EiB^P_FXRD#nOSnD)`lRX2W+ypKeB9ebi z`5&X8MZr51{5}PLKmp;Pj1Yj$c|~xk=VMSw*dh!s1YHb?@{cG+I4J*^0>;5VL5z%) zR;=*Aeo11|i)hbkcw#+z$YL(js|Rmv7_F!F;kz2bwM2J>d2sgIk5sh9gplrFTZ5 z)sH{}&U7-c21YugBJ*|Y%g`?Gy&jF$vQ?-mfU!|HXtDqdK^fJEdkQ?Y4kQg@)$r8qyCC6U+FltndmsiZV;RSf>Lml zz+r*Q`Z;LMx?D%kv-@t5#uen8;+*@Ov(R^#G-i==nsdI)Ik|ogIYFRl)0(DfO|cqd z808$Ed5mOoPK!=-#$Xw+`v$2e#Ug`kstbKSj@6)(S9Nh-T;O>+wbN0qcBXq4tuBhE zy60%O0e?#(FD?$??`iztM7|2)P-(iSBVE^n#l7$5Cp9T>w{b^MNcY&jc=ti zRV-7$s&AqcxaGSHTX4#(Q2O6ez{pD~E@LNV#aRq?ZzC0$`X*)G;W%B#lM^>SxW9$8 z_PIA=jsNz(#(x|7+a>bN!@bux>sZU&RV-j*gx~n!{Jk zdy{lAi!ni`AOp$R+6KQY*vD=wf-H_CB{C=aJ zZ;!%@9&PkLaWREoEd`L8BvwKMUMPPL!)Q-@?wOr|P8S}9{paN;XuLNNz~f98S|%=> z4uJm!%1QW<2aWa&6=e?iHpQ3-{)6)EIx$YDY}(B$*9$3f?{HGAwtj~Su|sK_9ZCgO zY~y&$3a?QKr|-R&32Of#1)t{()`Q_rV($~{#xcQ=KK!2S0gDw5JvYja$1Fly%90q% zkq{j)4+9tRP)gRk3FQJLTNZFMU^pa(WW{fTWMJ=D;pTI2hPhge@KT9gcQh2aimZ*>~Uib<| zjUE_Nxz2*7#t)>$O(bIB{UHru>Od~Zb% literal 0 HcmV?d00001 diff --git a/src/tools/utils/__pycache__/shoreline_evolution.cpython-39.pyc b/src/tools/utils/__pycache__/shoreline_evolution.cpython-39.pyc index 7b91ffa3b433bda554c23218430734291994104d..e230549c125a0080852c64151218167570af67fd 100644 GIT binary patch delta 55 zcmaDN{Xm*Gk(ZZ?0SMe?G^GY?9ZQ7F#HFG|hH%u7`$$r1CjkEK65ap+ diff --git a/src/tools/utils/__pycache__/transect_processor.cpython-39.pyc b/src/tools/utils/__pycache__/transect_processor.cpython-39.pyc index b4739e0da667051a103c21da7824edda21aa1fd2..3dbc1eab9529c2a0d76cca7a205af56a18b617c6 100644 GIT binary patch literal 2894 zcmaJ@&2QvH7OxN6?e6qI7=?Tc5@aPLre^`opw$XNG|GG~LaXeE4*^zK%eb73I~}`I zh} zr+nzX(?i8SX1(A9me^KGa{dueHc`x*5E&!ukx*QTrwl7d<)MZ(6`=N2ONFQd)rQ56 zed!aO6edfn2f9o%V@jP$SeZT4rg8_xT!pClm`zyCVqObKls;E{jg2MsqAfOXNSKye zwskv8bYAS*P~|&2r1U)NrRDI*3bkW9X{q)7B&|wqJsQ!I)-%~)M+aC)w;=w_G&%W~ zjl1t{SDC5S&BIb>gS^PrtFjzy8=Y=b!)zZ`Ib=0GOgx;O=4~t%#&KTcRUDH^L&llt zh(H`)xp4TmXG2p?!=?{Sq2j#I#TT5^6?bwzLGbo`>O0lt9wM@aAi^_Z!bW_TjYK7D z{)FFUU;el)MsnoUQu!-P1%3o!BLVxcp!a~h!30u_bjr>}S-CDK( z%>IOPcJROFji2wM)uwj%Ci@LbHxF*AmYW$Onr&+JgPEmqs}>Wk!kt;9J6oEte)2aG z|A(*(r*X*D2|4nFAgZZl{aeNEAUpXF+P%<4+REfBV_Q`j4{}r4c2*qjXF91eD+gI& zgY0pY6$(n3S}!+w(F=4N)1zfmCAx~!he@%U#j`a{^wMeb9fBCZZFz2aY`y)&>^Vd= zVu3bvDKEaxUbtuk@ zbYFlPw8fq`-vb;0H?Buc?~&JdH~$c(YD0#?9r4`^lRv_0Wkqsh1+01s~(n0Uq*M&Z(t>gp_N5a z=rtOdc(se%cCc(S>;#@Tj;F4Eh7951eF7WB;tgiDuQTr6FCb5? zO2u~tEdPBZYYEoWBOfdp)IPB)^xLBr^ssKBeQngP+ln1>{X-p&!VBRL6p~?LyPpmf0&(Y1 zBL(Ya%(Cr6*Y#2Ps6lX$42ML73);nOSOR%AOkqoxxw7Gda&SZ!0u-xta@6ZIY(U6~ zf*p6KZICt#yg~~|T|SCJn&ODp8-h)vkR<7m9K!N5JUS>69&Vr*f-P)4 z_jnft@Z{@gE%D=(v)Fo;S_!1HhwPtn$x|o@2YxLke1ReC#3Q^f1#@r$U4kJ<3q}AH zMk0kGY(edFCGZH~;h*--k85U5$E~nYy9$ul!n!>XxEOq^7gDyU z9@>^pRE}RH%RjgL3oFyoScxR0-@<0C|A3-^|0v&uBkBzEugj_ljqfr_)OJ-2Gm$7C zqu}pn_!#9V5(m%^j?O#z53qb6#gMm|?D7}zUd4NS<(y0Bkg$cUNPMMxB(9O5>9&g` zS>@@JAIbFGaxS_qu)oI#%1`e12tj;Va7}%^wmYbPA8qR+GY^k6We*z+lR_m1=TJ5% fzv?5)I!bc0p0|VzHyR$iNlDSr(gYT~V)5FqiFu<` literal 2664 zcma)8OLH4V5T4nWRvN$a3os}a;+LdQl z#B!}J#0M@E6h9y(ANfmj<&?j`iLYm6S&nmHwz_A!XS!#mU$epVbcLaP{^7-FtIF6v zL`*gd#2O&o01`~_HuH*}dtB3oXX@Va?5~+Hgn7t>896UauLRl>HfUSZuHX+@-FXd7 ztPv8PY$k{`K>9II#7Mhmh_cqmv4_^=S;7&eL+07Sg|3P^_echD8im=WOu{Hl6ZzVJ zd`Q}k8;uF>0Mh$FIUlkkmNTE{1~6ck3%q_Sv*vN}-^dozT6V))yTQD&M6ZyM-? zS2vZFM(w67;mFbj`kTpVbpNhx+}y%_`?z*e`>vH{!Na7z^(aZ&+sS^EZe?`caIIUf}^><--=tA@00FShk#Kr9AnOy!%=KBSgre zw%v1UlA2=A*?KW=tRg;OJ1elEpi&z(y6K8&{&fciPd_r?szYrO3w1dvJ2x|K`Ys>q!TeW2^cG}9dPYPuxKB-+RIHwI?uO;%7?&Cw&NcZa7 zb$|9hb&nO-$0KROc&a*i^G4m6@H>~t&BFOQ0^|${oDbVUnzovK|JcdtdGJ$80OlTC zIqSx=0%sgb39l37Ck!TBh}2$X8Q-O3VY{ZDF&DvYg0CrKk?mTProZ!oKH+N`fw54m zbPxiRKBb1F?Yv4Hy%?pDoCQNU?N&^g#Ff*DvS*1^7s*DUZvA_=~u+sN2nNm(MMEn9@Fy3aavto@pmlX1Khf*jGI+5?I zitl$4(QDJV>ic`Wpk2(!cd%5FPvjCIeWDXYPL$*<2apnSd1cD2XkG~NRHJRz0BH?q zTpv-ZPz}w<5~z@e>?KEKv|(CD%{)jl8020QWIY+Brz#>PJVlgt^b*oAL)9A^xgntM z$mGb-feD^Dw1{_P{PE?$%B>Cd^zy*Yt)ZRsT_-mMZ^G$2{D^Y~%KCy0%)RCB;am&f zOX!JbZ?N>|>x}E~3Gi5}E%=TB&A*K_%YnVa22O4ZgcWvLACy45c?sidgK}OLtjFc6 z+#Qy3HgxlHv2vGsYmpt9bc(%Kxr2FEkN?WpumYIMog)+g!RP|mHI&dD|zrA^PtzK1jAVXBuN+*beov0JVnR0In zajzA%1)hOGRUdZ+oN=co@j|I`Pg4()7ji+XMmdXOp{AsESvNu8DK~@~nY4s*HIO3NT*5?qW*55Oh(rq3 zj{xaqAf$z1@mcQj3w+s}LvQgaz{OX?XpSGupAN8-H-aMdxKX5yOUfY#_ye!*5uZY= zAc_otX|REXxMI4v5akR?xrLQh4zK1;ZY{7mHczs)=9F@qI13aX_Lw`lu8fKi!o=MAoT^|HRGyiU68xYz0S tox diff --git a/src/tools/utils/plot_results.py b/src/tools/utils/plot_results.py new file mode 100644 index 0000000..4b60cd1 --- /dev/null +++ b/src/tools/utils/plot_results.py @@ -0,0 +1,316 @@ +import arcpy +import seaborn as sns +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +import os +import re +import cartopy.crs as ccrs +from tools.utils.intersect_lines import * +from matplotlib.colors import Normalize, TwoSlopeNorm +from matplotlib.cm import ScalarMappable +import matplotlib.lines as mlines +from matplotlib.gridspec import GridSpec + +# Define the settings for the plots +plt.style.use('classic') +plt.rcParams['axes.grid'] = True +plt.rcParams['grid.linestyle'] = '--' +plt.rcParams['grid.linewidth'] = 0.5 +plt.rcParams['axes.prop_cycle'] = plt.cycler(color=['#5B9CFD', '#FECB31', '#bfbbd9', '#fa8174', '#81b1d2', + '#fdb462', '#b3de69', '#bc82bd', '#ccebc4', '#ffed6f']) + +class PlottingUtils(): + def __init__(self, transects, shore_intersections): + """ + Constructor method for initializing PlottingUtils class. + + Parameters: + transects (arcpy.FeatureClass): Feature Class object for transects (Line geometries). + shore_intersections (arcpy.FeatureClass): Feature Class object for shoreline intersections (Point geometries). + """ + self.transects = transects + + # Set the directory where plots will be stored + aprx = arcpy.mp.ArcGISProject('CURRENT') + self.out_dir = os.path.join(aprx.homeFolder, 'Plots results') + if not os.path.exists(self.out_dir): + os.mkdir(self.out_dir) + + # Convert Feature Classes to Pandas DataFrames + self.transects_df = self._feature_class_to_dataframe(transects) + self.shore_intersections_df = self._feature_class_to_dataframe(shore_intersections) + self.shore_intersections_df['date'] = pd.to_datetime(self.shore_intersections_df['date']) + + # Create Shapely Object for transects geometries + self.transects_shapely = line_arcgis2shapely(feature=transects, id='transect_id') + + + def _feature_class_to_dataframe(self, feature_class): + """ + Private method to convert an ArcGIS Feature Class to a Pandas DataFrame. + + Parameters: + feature_class (arcpy.FeatureClass): Feature Class object. + + Returns: + pd.DataFrame: Pandas DataFrame containing feature class data. + """ + fields = [field.name for field in arcpy.ListFields(feature_class)] + + return pd.DataFrame([row for row in arcpy.da.SearchCursor(feature_class, fields)], columns=fields) + + + def _get_UTM_projection(self): + """ + Private method to get UTM projection from transects Feature Class. + + Returns: + UTM_number (int): UTM number. + southern_hemisphere (bool): Whether or not the Feature Class is in the southern hemisphere. + """ + # CRS string (e.g. 'WGS_1984_UTM_Zone_19N') + crs = arcpy.Describe(self.transects).SpatialReference.name + + # Regex codes to extract the UTM number and the hemisphere + num_code = r'\b(\d+)[A-Za-z]?\b' + hemisphere_code = r'\b\d+([A-Za-z])\b' + + UTM_number = int(re.findall(num_code, crs.split('_')[-1])[0]) + hemisphere_UTM = re.findall(hemisphere_code, crs.split('_')[-1])[0] + + if hemisphere_UTM == 'N': + southern_hemisphere = False + else: + southern_hemisphere = True + + return UTM_number, southern_hemisphere + + + def _set_map_configuration(self, metric): + """ + Private method to set the map configuration for plotting LRR, SCE and NSM. + That is, the colormap, the type of normalization scale and the type of colorbar according to the metric. + + Parameters: + metric (string): Name of the feature to plot. + + Returns: + cmap (matplotlib.colors.LinearSegmentedColormap): The colormap. + norm (matplotlib.colors.TwoSlopeNorm or matplotlib.colors.Normalize): The type of the normalization. + extend_cbar (string): The type of the colorbar according to cmap and norm. + """ + + metric_min, metric_max = self.transects_df[metric].describe()[['min', 'max']] + + if metric_min < 0 and metric_max > 0: + cmap = plt.get_cmap('RdBu') + norm = TwoSlopeNorm(vmin=metric_min, vcenter=0, vmax=metric_max) + extend_cbar = 'both' + elif metric_min < 0 and metric_max < 0: + cmap = plt.get_cmap('Reds') + norm = Normalize(vmin=metric_min, vmax=0) + extend_cbar = 'min' + elif metric_min > 0 and metric_max > 0: + cmap = plt.get_cmap('Blues') + norm = Normalize(vmin=0, vmax=metric_max) + extend_cbar = 'max' + + return cmap, norm, extend_cbar + + # ===== FROM HERE, ALL THE FUNCTIONS TO CREATE THE PLOTS ARE DEFINED ===== + + def plot_spatial_evolution(self): + # Plot the distances between all shorelines and the baseline on each transect + + fig, ax = plt.subplots(figsize=(12, 5)) + + # Plot all values as scatter + ax.scatter(self.shore_intersections_df['transect_id'], + self.shore_intersections_df['distance_from_base'], + alpha=.1, lw=0, zorder=1) + + # Plot average width value for each transect + ax.plot(self.shore_intersections_df['transect_id'].unique(), + self.shore_intersections_df.groupby('transect_id')['distance_from_base'].mean(), + label='avg', color='#fa8174', lw=2, zorder=2) + + # Plot mean +-2*std width value for each transect + ax.fill_between(self.shore_intersections_df['transect_id'].unique(), + self.shore_intersections_df.groupby('transect_id')['distance_from_base'].mean()+\ + 2 * self.shore_intersections_df.groupby('transect_id')['distance_from_base'].std(), + self.shore_intersections_df.groupby('transect_id')['distance_from_base'].mean()-\ + 2 * self.shore_intersections_df.groupby('transect_id')['distance_from_base'].std(), + color='#bfbbd9', alpha=.5, lw=0, label='avg\u00B12std', zorder=0) + + # Plot settings + ax.set_xticks((np.arange(0, max(self.shore_intersections_df['transect_id']) + 2, 2)).tolist()) + ax.set_xlabel('transect_id') + ax.set_ylabel('distance from baseline (m)') + ax.set_xlim([-1, max(self.shore_intersections_df['transect_id']) + 2]) + plt.text(-0.05, 0.9, 'seaward', transform=plt.gca().transAxes, horizontalalignment='right', fontstyle='italic') + plt.text(-0.05, .1, 'landward', transform=plt.gca().transAxes, horizontalalignment='right', fontstyle='italic') + plt.grid(linestyle='--', alpha=0.3) + ax.legend() + ax.set_title('Spatial shoreline evolution (%.f - %.f)' % (self.shore_intersections_df['date'].min().year, + self.shore_intersections_df['date'].max().year)) + fig.savefig(os.path.join(self.out_dir, 'Spatial shoreline evolution.png'), dpi=300, bbox_inches='tight') + + + def plot_time_series(self, transects2plot): + # Plot time series for selected transects + + fig = plt.figure(figsize=(12, 2 * len(transects2plot))) + gs = GridSpec(len(transects2plot), 7, figure=fig) + + for i, t in enumerate(transects2plot): + ax = fig.add_subplot(gs[i, :-1]) + + # Prepare the data to plot + data_transect = self.shore_intersections_df.loc[self.shore_intersections_df['transect_id'] == t, :] + data_transect.index = pd.to_datetime(data_transect['date']) + data_transect = data_transect.sort_index() + data_transect['Time'] = np.arange(len(data_transect.index)) + X = data_transect.index.map(pd.Timestamp.toordinal) # features + y = data_transect.loc[:, 'distance_from_base'] # target + + # Plot time series + #y.plot(ax=ax, color='#5B9CFD', linestyle='-', marker='o', markersize=2, label='shoreline positions') + ax.plot(X, y.values, linestyle='-', marker='o', markersize=2, label='shoreline positions') + sns.regplot(x=X, y=y, ci=95, scatter=False, label='linear regression fit', ax=ax) + + # Plot settings + ax.set_ylabel('distance from\nbaseline (m)') + ax.locator_params(axis='y', nbins=4) + ax.set_title('transect ' + str(t)) + ax.grid(alpha=0.3) + + # Plot LRR error plot + ax2 = fig.add_subplot(gs[i, -1:]) + lrr = self.transects_df.loc[self.transects_df['transect_id']==t, 'LRR'] + lci = self.transects_df.loc[self.transects_df['transect_id']==t, ['LCI_low', 'LCI_upp']].to_numpy()[0] + ax2.errorbar(0, lrr, yerr=([abs(lci[0] - lrr.values[0])], [lci[1] - lrr.values[0]]), + fmt='or', markersize=8, capsize=5) + # Plot settings + ax2.set_xticklabels([]) + ax2.locator_params(axis='y', nbins=4) + ax2.locator_params(axis='x', nbins=1) + ax2.grid(axis='y', alpha=0.3) + ax2.grid(axis='x', alpha=0) + + if i == len(transects2plot) - 1: + # Time series plot + ax.set_xlabel('') + xticks = ax.get_xticks() + labels = [pd.Timestamp.fromordinal(int(label)).date().strftime("%m-%Y") for label in xticks] + ax.set_xticks(xticks) + ax.set_xticklabels(labels) + ax.legend(fontsize=8) + + # LRR error plot + ax2.set_xlabel('LRR\u00B195%\n(m/year)') + + else: + # Time series plot + ax.set_xlabel('') + ax.set_xticklabels([]) + + plt.subplots_adjust(hspace=0.4, wspace=1) + + fig.savefig(os.path.join(self.out_dir, 'Time shoreline evolution.png'), bbox_inches='tight', dpi=300) + + + def plot_seasonality(self, transects2plot): + # Boxplot by months for selected transects + + shore_month = self.shore_intersections_df.copy() + shore_month['month'] = shore_month['date'].dt.month + + fig, ax = plt.subplots(len(transects2plot), 1, figsize=(10, 15), sharex=True) + + for i, t in enumerate(transects2plot): + # Grab the data + data = shore_month.loc[shore_month['transect_id']==t, ['month', 'distance_from_base']] + + # Median values Line plot + ax[i].plot(data.groupby('month')['distance_from_base'].median(), + color='#FECB31', marker='o', markeredgecolor=None, alpha=.7) + + # Box plot + medianprops = dict(linestyle='-', linewidth=2, color='#FECB31') + whiskerprops = dict(linestyle='-', color='#5B9CFD') + data.boxplot(column='distance_from_base', by='month', ax=ax[i], medianprops=medianprops, whiskerprops=whiskerprops) + + # Plot settings + ax[i].set_xlabel('') + ax[i].set_ylabel('distance from\nbaseline (m)') + ax[i].locator_params(axis='y', nbins=5) + ax[i].set_title('transect ' + str(t)) + ax[i].grid(linestyle='--', alpha=0.3) + ax[i].grid(axis='x', alpha=0) + + if i == len(transects2plot) - 1: + ax[i].set_xticklabels(['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] * len(transects2plot)) + + fig.suptitle(None) + plt.subplots_adjust(hspace=0.2) + fig.savefig(os.path.join(self.out_dir, 'Shoreline evolution seasonality.png'), dpi=300, bbox_inches='tight') + + + def plot_map(self, metric): + # Plot a map of the selected metric (LRR, SCE or NSM) + + # Set the cmap, norm and the type of cbar of the plot + cmap, norm, extend_cbar = self._set_map_configuration(metric) + + # Set projection parameter + UTM_number, southern_hemisphere = self._get_UTM_projection() + proj = ccrs.UTM(UTM_number, southern_hemisphere=southern_hemisphere) + + # Create a subplot with UTM projection + fig, ax = plt.subplots(layout='compressed', subplot_kw={'projection':proj}) + + # Iterate through transects and plot lines + for i, t in self.transects_shapely.items(): + + # Check if Pvalue is less than 0.05 for significance + if self.transects_df.loc[self.transects_df['transect_id'] == i, 'Pvalue'].values <= 0.05: + color = cmap(norm(self.transects_df.loc[self.transects_df['transect_id'] == i, metric])) + ls = '-' + else: + color = 'gray' + ls = '--' + + # Plot the transect line + ax.plot(*t.xy, + color=color, + transform=proj, + lw=3, + ls=ls) + + # Create a legend entry for non-significant transects + legend_entry = mlines.Line2D([], [], color='gray', lw=2, ls='--', label='Non-significant transect') + + # Customize gridlines, ticks, and appearance + ax.gridlines(crs=proj, linewidth=1, color='black', alpha=0, linestyle="--") + ax.set_xticks(ax.get_xticks(), crs=proj) + ax.set_yticks(ax.get_yticks(), crs=proj) + ax.grid(alpha=.3) + + # Add colorbar and set title + if self.transects_df['Pvalue'].min() <= 0.05: + fig.colorbar(ScalarMappable(norm=norm, cmap=cmap), extend=extend_cbar, ax=ax) + if metric == 'LRR': + ax.set_title('Linear Regression Rate, LRR (m/year)', y=1.05) + elif metric == 'SCE': + ax.set_title('Shoreline Change Envelope, SCE (m)', y=1.05) + elif metric == 'NSM': + ax.set_title('Net Shoreline Movement, NSM (m)', y=1.05) + + # Set limits, labels, legend, and save the figure + lons = [x for t in self.transects_shapely.values() for x in t.xy[0]] # Extract the longitudes for plotting purposes (Sometimes the plot is centered to the west and a blank space is left to the east of the study area) + ax.set_xlim([ax.get_xlim()[0], max(lons)]) + ax.set_xlabel('Eastings (m)') + ax.set_ylabel('Northings (m)') + ax.legend(handles=[legend_entry], fontsize='small') + fig.savefig(os.path.join(self.out_dir, '{0}_transects.png'.format(metric)), dpi=300, bbox_inches='tight') diff --git a/src/tools/utils/transect_processor.py b/src/tools/utils/transect_processor.py index d8de6da..ae487d1 100644 --- a/src/tools/utils/transect_processor.py +++ b/src/tools/utils/transect_processor.py @@ -17,6 +17,7 @@ def invert_angles(self): # Detect the first transects that are inverted (if there are more than one change). To do so, find a difference angle around ~180ยบ (it has been selected a range of 180 +-50). start_change_transects = self.df[(self.df['diffBear'].abs() >= 130) & (self.df['diffBear'].abs() < 230)]['transect_id'].to_list() + # Empty list of the transects that need to be inverted transects2correct = [] for i, _ in enumerate(start_change_transects): if (i + 1) % 2 != 0: # Odd number @@ -30,7 +31,7 @@ def invert_angles(self): pass self.df['Angle'] = 0 - self.df.loc[self.df['transect_id'].isin(transects2correct), 'Angle'] = 180 # Rotate 180 degrees the bearing anle + self.df.loc[self.df['transect_id'].isin(transects2correct), 'Angle'] = 180 # Rotate 180 degrees the bearing angle def classify_transects(self): # Classify transects with large differences using the corrFactor value. In addition, try not to take into account the differences in the 360-0 sector. @@ -49,11 +50,9 @@ def __init__(self, df, fclass): # Initialize the class by adding an angle field with the values calculated above arcpy.management.AddField(fclass, 'Angle', 'DOUBLE') - count = 0 with arcpy.da.UpdateCursor(fclass, 'Angle') as cursor: - for row in cursor: - cursor.updateRow([df.loc[count, 'Angle']]) - count += 1 + for i, row in enumerate(cursor): + cursor.updateRow([df.loc[i, 'Angle']]) # Rebuild each polyine with rotated vertices with arcpy.da.UpdateCursor(fclass, ['SHAPE@', 'Angle']) as cursor: