Source code for watex.site

# -*- coding: utf-8 -*-
#   Licence:BSD-3-Clause
#   Author: LKouadio <etanoyau@gmail.com>
"""
Manage site data.
"""
from __future__ import annotations 
import os
import shutil
import copy 
import re

import warnings 
import numpy as np 

from ._watexlog import watexlog 
from ._typing import ( 
    Optional, 
    ArrayLike,
    F
    )
from .exceptions import ( 
    SiteError, 
    ProfileError,
    NotFittedError, 
    )
from .property import ( 
    UTM_DESIGNATOR
    )
from .utils.coreutils import makeCoords 
from .utils.exmath import ( 
    get_bearing, 
    scalePosition, 
    )
from .utils.funcutils import (
    _assert_all_types, 
    to_numeric_dtypes,
    _validate_name_in, 
    interpolate_grid,
    reshape,
    is_iterable
    ) 
from .utils.gistools import (
    assert_elevation_value, 
    assert_lat_value, 
    assert_lon_value , 
    ll_to_utm, 
    utm_to_ll, 
    project_point_ll2utm, 
    project_point_utm2ll,
    assert_xy_coordinate_system,
    convert_position_str2float, 
    convert_position_float2str, 
    )
from .utils.validator import ( 
    _check_consistency_size , 
    _is_numeric_dtype , 
    assert_xy_in, 
    ) 

__all__= ['Location', 'Profile']

[docs] class Profile: def __init__( self , *, utm_zone =None, coordinate_system=None, datum = 'WGS84', epsg = None, reference_ellipsoid =23, ): self.epsg=epsg self.datum =datum self.reference_ellipsoid= reference_ellipsoid self.utm_zone=utm_zone self.coordinate_system= coordinate_system
[docs] def fit( self, x:ArrayLike |str , y:ArrayLike | str , elev: Optional [ArrayLike] =None, **fit_params ): """ Populate profile attributes from x and y. By default if the coordinate system is given as latitude/longitude x, y are latitude and longitude respectively. Parameters ------------ x, y: ArrayLike 1d /str One dimensional arrays. `x` can be consider as the abscissa of the landmark and `y` as ordinates array. If `x` or `y` is passed as string argument, `data` must be passed as `fit_params` keyword arguments and the name of `x` and `y` must be a column name of the `data`. By default `x` and `y` are considered as `longitude` and `latitude` when ``dms`` or ``ll`` coordinate system is passed. elev: ArrayLike 1d/str Arraylike 1d of elevation at each positions. If not supplied should be set to null at each points. If given, it must be consistent with `x` and `y`. data: pd.DataFrame Data containing `x` and `y` values as series. Then if `x` and `y` are given as string argument, their names must be included in the data columns. Otherwise an error will raise. Return --------- self :`watex.site.Profile` Object of Profile. Note ------ When `data` is supplied and `x` and `y` are given by their names existing in the dataframe columns, by default, the non-numerical data are removed. However, if `y` and `x` are given in DD:MM:SS in the dataframe, the coordinate system must explicitly set to ``dms` to keep the non-numerical values in the data. """ #15921 epsg io emsg =("'DD:MM:SS.ms' coordinate system is detected.") data = fit_params.pop('data', None) self.coordinate_system = str( self.coordinate_system).lower().strip() if ( self.coordinate_system.find ('dms')>=0 or 'dd:mm' in self.coordinate_system ) : self.coordinate_system='dms' if data is not None: # when coordinate_system is explicity passed # and data is given, don't suppress the non-numerical # data. Keep it untouch and try to convert x, y to # float type later. suppress_cf = True if self.coordinate_system=='dms': suppress_cf= False # suppress non numerical values data = to_numeric_dtypes(data, pop_cat_features= suppress_cf) # get elevation if string is given if isinstance (elev , str): elev = elev if elev in data.columns else None if elev is None: warnings.warn("Elevation not found in the dataframe") if elev is not None: elev = np.array (data[elev]) self.x, self.y = assert_xy_in( x , y, data = data ) # set x and y names useful for # plotting labels. self.xname_=None ; self.yname_=None if ( isinstance ( x, str ) or hasattr (x, "name" ) ) : self.xname_= x if isinstance ( x, str ) else x.name if ( isinstance ( y, str ) or hasattr (y, "name" ) ) : self.yname_= y if isinstance ( y, str ) else y.name if self.coordinate_system in ('none', 'auto'): self.coordinate_system = assert_xy_coordinate_system ( self.x, self.y ) if self.coordinate_system =='dms' : try : self.x, self.y = Profile.dms2ll(self.x, self.y) except ValueError as e: raise ValueError( emsg + str(e)) else: # initialize the system to ll if # conversion succeeded. self.coordinate_system =='ll' # set 0. to elevation self.elev = elev if elev is not None else np.zeros_like( self.x, dtype = np.float32 ) self.elev = np.array (self.elev , dtype = np.float32 ) if not _check_consistency_size (self.x , np.array( self.elev), error ='ignore' ): raise ProfileError ("Elevation and x or y must be consistent." f" Got {len(x)} and {len(elev)}") return self
[docs] def bearing (self, *, to_degree:bool = True ): """ Compute the bearing between two coordinates. A bearing is a direction of one point relative to another point, usually given as an angle measured clockwise from north. In navigation, bearings are often used to determine the direction to a destination or to plot a course on a map. There are two main types of bearings: absolute bearing and relative bearing. - Relative bearing refers to the angle between the forward direction of a craft (heading) and the location of another object. - Absolute bearing refers to the angle between the magnetic north (magnetic bearing) or true north (true bearing) and an object The bearing (:math:`\beta`) between two coordinates points 1(lon1, lat1) and 2 (lon2, lat2) can becalculated as: .. math:: \beta = atan2(sin(lon2-lon1)*cos(lat2), cos(lat1)*sin(lat2) – \ sin(lat1)*cos(lat2)*cos(lon2-lon1)) By default, the first and last coordinates for points 1 and 2 are used as `latlon1` and `latlon2` respectively. Parameters ------------ to_degree: bool, default=True Convert the bearing from radians to degree. Returns -------- :math:`\beta` : float, The value of bearing in degree ( default). """ msg =( "x, y are not in longitude/latitude format" " while 'utm_zone' is not set. The bearing" " should be less accurate. Provide the UTM" " zone to improve the accuracy.") self.inspect if self.coordinate_system =='ll': xs = np.array(copy.deepcopy(self.x)) ys = np.array(copy.deepcopy(self.y)) if ( self.coordinate_system !='ll' and self.utm_zone is None) : warnings.warn(msg ) self.utm_zone = self.utm_zone or '49R' if self.coordinate_system !='ll': # recompute value to lat/lon # from easting/northing ys , xs = Location.to_latlon_in( self.x, self.y, utm_zone= self.utm_zone) # compute bearing. return get_bearing((ys[0], xs[0]) , ( ys[-1], xs[-1] ), to_deg= to_degree )
[docs] def distance (self, *, average_distance: bool = True): """Compute the distance between profile coordinates points. Preferably, it uses the UTM coordinates positions. By default the coordinate system is automatically detected. Parameters ----------- average_distance: bool, default =True, Returns the average value of the distance between different points. Returns --------- d: Arraylike of shape (N-1) Is the distance between points or the average of all distances. See Also --------- watex.utils.exmath.get_distance: Get the distance from longitude/latitude or easting/northing coordinates. Examples --------- >>> import numpy as np >>> from watex.site import Profile >>> posx = np.random.rand (7) *10 >>> posx = np.abs ( np.random.randn (7) * 12 ) >>> po= Profile().fit(posx, posy ) >>> # convert data to UTM and compute distance becuase >>> # our toy example has value less than 90 and 180. >>> po.distance () 251053.3287093233 >>> po.coordinate_system = 'UTM' >>> 6.451210308544236 """ self.inspect x, y = self.x , self.y if self.coordinate_system =='ll': x , y = Location.to_utm_in( lats= self.y, lons =self.y, epsg = self.epsg , datum = self.datum , reference_ellipsoid= self.reference_ellipsoid ) d = np.sqrt( np.diff (x) **2 + np.diff (y)**2 ) return d.mean() if average_distance else d
[docs] def scale_positions (self, **sp_kws): """ Scale the position coordinates along x and y. Parameters ----------- sp_kws: dict Keyword arguments passed to :func:`~watex.utils.scalePosition`. Returns -------- x, y : Arraylikes Scaled positions See also --------- watex.utils.scalePositions: Scale positions using the `scipy` curve fit. watex.utils.exmath.scale_positions: Scale and shift positions using bearing. """ self.inspect x, *_ = scalePosition(self.x , **sp_kws) y, *_ = scalePosition(self.y , **sp_kws) return x, y
[docs] def shift_positions ( self, *, step:float= None, use_average_dist:bool=False, angle:Optional[float]=None, ): """ Shift the x and y position coordinates from the step and angle. By default, it consider `x` and `y` as easting/latitude and northing/longitude coordinates respectively. It latitude and longitude are given, specify the parameter `is_latlon` to ``True``. Parameters ------------ step: float, Optional The positions separation. If not given, the average distance between all positions should be used instead. use_average_dist: bool, default=False, Use the distance computed between positions for the correction. angle: float, Optional Bearing angle in degree to shift the profile line . If ``None``, the ``bearing`` of `x` and `y` is used instead. Returns -------- xx, yy: Arraylike 1d, The arrays of position correction from `x` and `y` using the bearing. See Also --------- watex.utils.exmath.get_bearing: Compute the direction of one point relative to another point. Examples --------- >>> from watex.utils.exmath import scale_positions >>> east = [336698.731, 336714.574, 336730.305] >>> north = [3143970.128, 3143957.934, 3143945.76] >>> p= Profile().fit (east, north) >>> east_c , north_c= p.scale_positions (east, north, step =20) >>> east_c , north_c (array([336686.69198337, 336702.53498337, 336718.26598337]), array([3143986.09866306, 3143973.90466306, 3143961.73066306])) """ cs ={'ll': 'longitude/latitude', 'dms': 'dd:mm:ss'} self.inspect if self.coordinate_system !='utm': cse = cs.get('ll').title () if self.coordinate_system =='ll' else ( cs.get('dms').upper() if self.coordinate_system =='dms' else self.coordinate_system .upper() ) warnings.warn(( "{0!r} coordinates system is detected. Shifting positions" " expects UTM coordinates. It is recommended to convert" " positions data to UTM (ref:`watex.site.to_utm_in`) before" " processing. The following with {0} coordinates" " might lead to invalid results. Use at your own risk." ).format(cse) ) if ( not use_average_dist and step is None ): warnings.warn("Step is not given. The mean-distance of" " positions should be used instead.") use_average_dist =True if use_average_dist: step = self.distance ( average_distance= use_average_dist) step = float (_assert_all_types(step, float, int, objname ='Step')) if angle is not None: angle = float (_assert_all_types( step, float, int, objname ='Bearing angle (in degree)')) angle = np.deg2rad (angle %360) angle = angle or self.bearing(to_degree =False ) # return bearing in rad. xx = self.x + ( step * np.cos (angle)) yy = self.y + (step * np.sin(angle)) return xx, yy
[docs] @staticmethod def f_ (ar , /, func: str | F = 'dms->ll'): """ Converter position function from dms to longitude/latitude degree decimal or vice versa. Convert position from str (DD:MM:SS) to float (latitude/longitude) and vice versa. Parameters ----------- ar: ArrayLike 1d, Array containing the position coordinates for conversion. func: Callable or str, default ='dms->ll' Converter functions. They can be: - :func:`~watex.utils.gistools.convert_position_str2float` for ``:dd:mm:ss`` to foat(long, lat) coordinates. If string is passed it should be ['dms2ll'|'dmstoll'|'dms-<ll']. - :func:`~watex.utils.gistools.convert_position_float2str` from float (long, lat) in decimal degree coordinates to ``dd:mm:ss``. When string is passed, it should be ['ll2dms'|'lltodms'|'ll->dms'] Returns -------- generator obj. Map object composed of value converted. """ if isinstance (func, str): f = _validate_name_in(func, defaults = ( 'll2dms', 'lltodms', 'll->dms'), expect_name= convert_position_float2str ) if not callable (f): f = convert_position_str2float # faster than # x = np.array ( [ convert_position_str2float(i) for i in x ], # dtype = np.float64) # while apply_along not possible due to the string dtype during # the loop # x = np.apply_along_axis(convert_position_str2float, 0, # np.array (x, dtype =str )) return map ( lambda i: f(i) , ar)
[docs] @staticmethod def dms2ll (x:ArrayLike|str , y:ArrayLike|str , *, data=None): """ Convert array x and y from DD:MM:SS to degree decimal -longitude (x) and latitude (y). Parameters ----------- x, y: ArrayLike containing the degree-minutes-seconds (DMS) coordinates positions. Returns -------- x, y: Arraylike or str ArrayLike in degree decimal coordinates format. By default `x` and `y` are longitude and latitude respectively. If `x` and `y` has a string argument, data must be provided, otherwise an error raises. data: pd.DataFrame DataFrame containg the position x and y. .. versionadded:: 0.2.5 Example --------- >>> from watex.site import Profile >>> x=['20:15:35'] ; y =['7:45:8.5'] >>> Profile.dms2ll (x, y) Out[83]: (array([20.25972222]), array([7.75236111])) """ if ( (isinstance (x, str) or isinstance (y, str)) and data is None ): raise TypeError( "Data can't be None when x or y has a string argument.") if data is not None: x, y = assert_xy_in(x, y, data=data ) if not _is_numeric_dtype(x , to_array =True ): # reconvert object to str for consistency x = np.array ( list ( Profile.f_(x)), dtype = np.float64 ) if not _is_numeric_dtype(y , to_array =True): y = np.array ( list (Profile.f_ (y)), dtype = np.float64 ) return x, y
[docs] @staticmethod def ll2dms (x:ArrayLike|str , y:ArrayLike|str , *, data=None ): """ Convert array x and y from degree decimal to degree-minutes-seconds (DMS) Parameters ----------- x, y: ArrayLike or str Arrays containing the degree decimal position coordinates. If `x` and `y` has a string argument, data must be provided, otherwise an error raises Returns -------- x, y: Arraylike ArrayLike in DD:MM:SS coordinates format. data: pd.DataFrame DataFrame containg the position x and y. .. versionadded:: 0.2.5 Example --------- >>> from watex.site import Profile >>> x =[15.18 ] ; y =[19.60] >>> Profile.ll2dms (x, y) Out[84]: (array(['15:10:48.00'], dtype='<U11'), array(['19:36:00.00'], dtype='<U11')) """ if ( (isinstance (x, str) or isinstance (y, str)) and data is None): raise TypeError( "Data can't be None when x or y has a string argument.") if data is not None: x, y = assert_xy_in(x, y, data=data ) if _is_numeric_dtype(x , to_array =True ): x = np.array ( list (Profile.f_ (x, 'll->dms')), dtype = str ) if _is_numeric_dtype(y , to_array =True): y = np.array ( list (Profile.f_ (y, 'll->dms')), dtype = str) return x, y
[docs] def make_xy_coordinates ( self, *, step =None, r = None, todms:bool =False, **kws ): """ Generate synthetic coordinates from references latitude and longitude from x and y. Parameters ----------- step: float or str Offset or the distance of seperation between different sites in meters. If the value is given as string type, except the ``km``, it should be considered as a ``m`` value. Only meters and kilometers are accepables. r: float or int The rotate angle in degrees. Rotate the angle features toward the direction of the projection profile. Default value use the :meth:`~.bearing` value in degrees. todms: bool, Default=False Reconvert the longitude/latitude degree decimal values into the DD:MM:SS. kws: dict, Additional keywords of :func:`~watex.utils.exmath.makeCoords`. Returns ---------- lon, lat : ArrayLike ArrayLike of synthetic coordinates latitude and longitude. See Also -------- watex.utils.coreutils.makeCoords: Create mutiple coordinates with different strategies using references latitude and longitude. Examples ---------- >>> import watex as wx >>> # get two coordinates from random stations >>> data = wx.make_erp (n_stations =2 ).frame >>> p = Profile ().fit(data.longitude, data.latitude) >>> longs, lats = p.make_xy_coordinates(step =100 , todms =True ) >>> longs Out[40]: array(['-99:13:48.11', '-99:13:48.11'], dtype='<U12') >>> lats array(['-85:31:37.57', '-85:31:37.57'], dtype='<U12') >>> p = Profile ().fit(data.easting, data.northing) >>> longs, lats = p.make_xy_coordinates(step =100 , todms =True ) >>> longs, lats Out[43]: (array(['110:58:55.00', '110:58:55.00'], dtype='<U12'), array(['4:32:10.96', '4:32:10.96'], dtype='<U10')) """ def _auto ( v): if str (v).lower().strip() in ('none', 'auto'): return None self.inspect step = _auto (step ); r = _auto (r ) # use distance by default and bearing as r angle step = step or self.distance (average_distance= True ) r = r or self.bearing() reflat = ( self.y[0], self.y[-1]) reflong = (self.x[0], self.x[-1]) isutm = False if self.coordinate_system =='ll' else True utm_zone = kws.pop ('utm_zone', None ) or self.utm_zone return makeCoords( reflong, reflat, nsites = len(self.x ), r= r , step =step , todms=todms, utm_zone= utm_zone, is_utm= isutm, datum=self.datum, espg=self.epsg, **kws )
[docs] def interpolate( self, method ='linear', inplace =True, **kws ): """ Interpolate x, y and elev ( if applicable). Parameters ----------- inplace: bool, default=True Erase existing value of x , y and elev with the interpolated one. If ``False`` , it returns interpolated x, y and elev and keep the previous attributes x, y and elev untouchable. method: bool, default='nearest', Method of interpolation. One of ['nearest'|'linear'|'cubic'] kws: dict, Additional keywords arguments passed to :func:`~watex.utils.funcutils.interpolate_grid`. Returns -------- self|x, y, elev: :class:`~watex.site.Profile` or Arraylikes `:class:`.Profile` objects if `inplace` is ``True`` or interpolated x, y and elev. See Also -------- watex.utils.funcutils.interpolate_grid: Interpolate two dimensional array. Examples -------- >>> import numpy as np >>> from watex.site import Profile >>> x = [ 28, np.nan, 50, 60 ] >>> y =[ np.nan, 1000, 2000, 3000] >>> elev=[ 0, 1 , np.nan, np.nan] >>> po = Profile().fit(x, y, elev ) >>> po.interpolate () >>> po.x array([28., 39., 50., 60.]) >>> po.y array([1000., 1000., 2000., 3000.]) >>> po.elev array([0., 1., 1., 1.]) """ emsg = ("Interpolation expects x, y and elev to have a consistent" " size. sizes x, y and elev are {}, {} and {}.") self.inspect try : _check_consistency_size(self.x, self.y) _check_consistency_size(self.y, self.elev) except: raise ProfileError(emsg.format( len(self.x), len(self.y), len(self.elev )) ) # make a grid data along axis =0 ar = np.vstack ((self.x, self.y, self.elev )) # interpolate is done along axis =0 so # for x, y and elev we may transpose # the data first and do the reverse # processing back for new x, y and z ar = interpolate_grid(ar, method = method , **kws) x , y, elev = np.vsplit (ar , 3 ) x, y, elev = reshape (x) , reshape (y), reshape (elev) if inplace : self.x, self.y, self.elev = x, y, elev return self return x, y, elev
[docs] @staticmethod def out ( x:ArrayLike , y:ArrayLike, /, elev:ArrayLike =None, filename:str=None, basename:str=None , counter:bool= False , sep:str =None , savepath:str =None, extension:str ='.txt', how:str='py', verbose:int=0, ): """Export coordinates data. File to export should be ['name' | 'x' |'y' |'elev' ]. Parameters ----------- x: Arraylike X-coordinate values to export. y: ArrayLike, Y -coordinate values to export. elev: ArrayLike, Elevation values to export. If not given should be null. filename: str, Name of output file. basename: str, default='Site' Station/site name of each coordinate (x, y) position. If not given should be ``site``. counter: bool, default=False If ``True``, count the number site and prefix it. For instance of `42` sites with the ``basename=AMT``, the first counter site should be ``00_AMT for Python indexing (``how='py'``) sep: str, default =' ' the separator between site, x, y, elev. savepath: str, Output file destination. Default is current directory. extension: str, default='.txt' The format of output coordinates files. If the `extension` is suffixed with the `filename`, it should be used instead. how: str, default='py' Way for counting the site/station. Any other value should starting counting the site from 1. verbose:int, default=False show message to where the file is saved. .. versionadded:: 0.2.1 Examples --------- >>> import numpy as np >>> import watex as wx >>> data = wx.make_erp (n_stations=24).frame >>> wx.site.Profile.out (data.easting, data.northing, verbose =True ) >>> elev = np.random.permutation (len(data.easting)) *100 >>> wx.site.Profile.out (data.easting, data.northing, elev, filename ='coordinates.txt', basename ='AMT-E1', counter =True ) """ x = np.array( is_iterable( x, exclude_string =True, transform =True )) y = np.array( is_iterable(y , exclude_string =True, transform =True )) elev = np.array( is_iterable(elev , exclude_string =True, transform =True )) _check_consistency_size(x, y ) elev = elev if elev is not None else np.repeat([0.], len(x)) # for consitency _check_consistency_size(y, elev ) basename = basename or 'Site' basename = str (basename ) filename= filename or 'site' filename = str(filename ) sep = sep or " " sep = str(sep ) # make site sites = [(( f"{ii:02}_" if how=='py' else f"{ii+1:02}_") if counter else '') + basename for ii in range (len(x )) ] writelines =[] #x = x.astype (str) ; y = y.astype (str) ; elev= elev.astype (str) for site, east, north , elev in zip ( sites, x, y, elev ): writelines.extend( [site + sep + f"{east:.3f}" +sep + f"{north:.3f}"+ sep + f"{elev:.3f}" + '\n' ] ) filename, ex = os.path.splitext(filename ) extension = extension if ex =='' else ex extension ='.'+ extension.replace ('.', '') # for consistency with open (filename + extension, 'w',encoding='utf8') as df: df.writelines(writelines) if savepath : shutil.move (filename + extension, savepath ) if verbose: print(f"--> File {filename+extension} is successfull written" + (f"to {savepath}." if savepath else '.'))
[docs] def plot ( self, sites: ArrayLike[str]=None , basename:str ='S', coerce:bool=True, **kws): """Base plot of the profile. Parameters ----------- sites: str, Arraylike The name of the sites that fits each position x, y basename: str, default='S' the text to prefix the `site` position when the text is not given. coerce:bool, default=True Force the plot despite the given sites do not match the number of positions `x` and `y`. If ``False``, number of positions must be consistent with x and y, otherwise error raises. kws: dict, Additional keyword arguments passed to :func:`~watex.utils.plotutils.plot_text` .. versionadded:: 0.2.1 """ from .utils.plotutils import plot_text self.inspect return plot_text ( self.x, self.y , text = sites, coerce =coerce, basename=basename, xlabel= self.xname_ or '', ylabel = self.yname_ or '', **kws )
@property def inspect (self): """ Inspect object whether is fitted or not""" msg = ( "{obj.__class__.__name__} instance is not fitted yet." " Call 'fit' with appropriate arguments before using" " this method" ) if not hasattr (self, 'x'): raise NotFittedError(msg.format( obj=self) ) return 1 def __repr__(self): """ Represent the output class format """ t_=("utm_zone" ,"coordinate_system","datum" , "epsg" ,"reference_ellipsoid" ) outm = ( '<{!r}:' + ', '.join( [f"{k}={getattr(self, k)!r}" for k in t_]) + '>' ) return outm.format(self.__class__.__name__)
Profile.__doc__="""\ Profile class deals with the positions collected in the survey area. In principle, there is no exact solution between longitude/latitude to x/y coordinates. Indeed, there is no isometric map from the sphere to the plane. when you convert lon/lat coordinates from the sphere to x/y coordinates in the plane, we cannot hope that all lengths will be preserved by this operation. Therefore, we have to accept some kind of deformation. For smallish parts of earth's surface, transverse Mercator is quite common. By default, we use ``x`` for `longitude` and ``y`` for `latitude`. This is useful when data is passed as longitude-latitude (``ll``) or degree-minutes- seconds (``dms``) in the `fit` method. Parameters ---------- utm_zone: Optional, string zone number and 'S' or 'N' e.g. '55S'. Default to the centre point of coordinates points in the survey area. It should be a string (##N or ##S) in the form of number and North or South hemisphere, 10S or 03N coordinate_system: str, ['utm'|'dms'|'ll'] The coordinate system in which the data points for the profile is collected. If not given, the auto-detection will be triggered and find the suitable coordinate system. However, it is recommended to provide it for consistency. Note that if `x` and `y` are composed of value less than 180 degrees for longitude and 90 degrees for latitude, it should be considered as longitude-latitude (``ll``) coordinates system. If `x` and `y` are degree-minutes-second (``dms`` or ``dd:mm:ss``) data, they must be specify as coordinate system in order to accept the non-numerical data before transforming to ``ll``. If ``data`` is passed to the :meth:`.fit` method and ``dms`` is not specify, `x` and `y` values should be discarded. datum: string, default = 'WGS84' well known datum ex. WGS84, NAD27, NAD83, etc. epsg: Optional, int epsg number defining projection ( see http://spatialreference.org/ref/ for moreinfo) Overrides utm_zone if both are provided. or https://epsg.io/?q=China%20kind%3APROJCRS reference_ellipsoid: int, default=23 reference ellipsoids is derived from Peter H. Dana's website- http://www.utexas.edu/depts/grg/gcraft/notes/datum/elist.html Department of Geography, University of Texas at Austin Internet: pdana@mail.utexas.edu . Default is ``23`` constrained to WGS84. Examples --------- >>> from watex.datasets import load_edis >>> from watex.site import Profile >>> xy = load_edis (samples =17 , as_frame =True , key ='latitude longitude') >>> xy.head (2) longitude latitude 0 110.485833 26.051390 1 110.486153 26.051794 >>> pro= Profile ().fit( xy.longitude, xy.latitude) >>> pro.distance () 62.890276656978244 >>> pro.bearing () 35.4252016495945 >>> pro.make_xy_coordinates( ) (array([110.48583316, 110.48615319, 110.48647322, 110.48679325, 110.48711328, 110.48743331, 110.48775334]), array([26.05138954, 26.05179371, 26.05219788, 26.05260205, 26.05300622, 26.05341038, 26.05381455])) """
[docs] class Location (object): def __init__( self, lat= None, lon=None, elev=None, datum='WGS84', epsg=None, reference_ellipsoid=23, utm_zone=None, **kwds ) : self._logging = watexlog.get_watex_logger( self.__class__.__name__) self._lat = lat self._lon = lon self._utm_zone=utm_zone self._elev=elev self.datum=datum self.epsg=epsg self.reference_ellipsoid=reference_ellipsoid self._east= None self._north =None for key in list(kwds.keys()) : setattr (self, key, kwds[key]) @property def utm_zone (self): return self._utm_zone @property def lat(self): return self._lat @property def lon(self) : return self._lon @property def east(self ): return self._east @property def north(self): return self._north @property def elev(self): return self._elev @utm_zone.setter def utm_zone (self, utm_zone): utm_xone = copy.deepcopy(utm_zone) utm_zone = str(utm_zone).upper().strip() str_ = f"{'|'.join([ i for i in UTM_DESIGNATOR.keys()]).lower()}" regex= re.compile(rf'{str_}', flags=re.IGNORECASE) if regex.search(utm_zone) is None: raise SiteError (f"Wrong UTM zone value!: {utm_xone!r} ") self._utm_zone =utm_zone.upper() @lat.setter def lat (self, lat): self._lat = assert_lat_value(lat) @lon.setter def lon(self, lon) : self._lon = assert_lon_value(lon) @elev.setter def elev(self, elev) : self._elev = assert_elevation_value(elev ) @east.setter def east (self, east): self._east = np.array(east, dtype = float ) @north.setter def north (self, north): self._north = np.array(north, dtype =float)
[docs] def to_utm ( self, lat:float=None, lon:float=None, datum:str= None , utm_zone:str=None, epsg:int=None, reference_ellipsoid:int= None ): """ Project coordinates to utm if coordinates are in degrees at given reference ellipsoid constrained to WGS84 by default. Parameters ----------- lat: float or string (DD:MM:SS.ms) latitude of point lon: float or string (DD:MM:SS.ms) longitude of point datum: string, default='WGS84' well known datum ex. WGS84, NAD27, NAD83, etc. utm_zone: Optional, string zone number and 'S' or 'N' e.g. '55S'. Defaults to the centre point of the provided points epsg: Optional, int epsg number defining projection (see http://spatialreference.org/ref/ for moreinfo) Overrides utm_zone if both are provided reference_ellipsoid: Optional, int reference ellipsoids is derived from Peter H. Dana's website- http://www.utexas.edu/depts/grg/gcraft/notes/datum/elist.html Department of Geography, University of Texas at Austin Internet: pdana@mail.utexas.edu . Default is ``23`` constrained to WGS84. Returns -------- proj_point: tuple(easting, northing, zone) projected point in UTM in Datum """ if lat is not None : self.lat = lat if lon is not None : self.lon =lon if (self.lat or self.lon) is None : raise SiteError (" Latitude and longitude must not be None") self.epsg = epsg or self.epsg self.datum = datum or self.datum self.reference_ellipsoid = reference_ellipsoid or \ self.reference_ellipsoid try : self.east, self.north, self.utm_zone= project_point_ll2utm( lat = self.lat , lon= self.lon, datum = self.datum , utm_zone = self.utm_zone , epsg= self.epsg ) except : self.utm_zone, self.east, self.north= ll_to_utm( reference_ellipsoid=self.reference_ellipsoid, lat = self.lat, lon= self.lon) return float(self.east), float(self.north)
[docs] def to_latlon( self, east:float=None, north:float= None, utm_zone:str=None, reference_ellipsoid:int=None , datum:str = None, epsg: int= None, ): """ Project coodinate on longitude latitude once data are utm at given reference ellispoid constrained to WGS-84 by default. Parameters ----------- east: float easting coordinate in meters north: float northing coordinate in meters utm_zone: Optional, string (##N or ##S) utm zone in the form of number and North or South hemisphere, 10S or 03N datum: string, default ='WGS84' well known datum ex. WGS84, NAD27, etc. epsg: Optional, int epsg number defining projection (see http://spatialreference.org/ref/ for moreinfo) Overrides utm_zone if both are provided reference_ellipsoid: Optional, int reference ellipsoids is derived from Peter H. Dana's website- http://www.utexas.edu/depts/grg/gcraft/notes/datum/elist.html Department of Geography, University of Texas at Austin Internet: pdana@mail.utexas.edu . Default is ``23`` constrained to WGS84. Returns ------- proj_point: tuple(lat, lon) projected point in lat and lon in Datum, as decimal degrees. """ if east is not None: self.east = east if north is not None: self.north = north if (self.east or self.north) is None : raise SiteError(" Easting and northing must not be None") if utm_zone is not None : self._utm_zone =utm_zone self.epsg = epsg or self.epsg self.datum = datum or self.datum self.reference_ellipsoid = reference_ellipsoid or \ self.reference_ellipsoid try : self.lat , self.lon = project_point_utm2ll( easting = self.east, northing= self.north, utm_zone = self.utm_zone, datum= self.datum, epsg= self.epsg ) except : self.lat, self.lon = utm_to_ll( reference_ellipsoid=self.reference_ellipsoid, northing = self.north , easting= self.east, zone= self.utm_zone ) return self.lat, self.lon
[docs] @staticmethod def to_utm_in( lats:ArrayLike, lons:ArrayLike, *, data=None, utm_zone:str =None, datum: str=None, **kws ): """ Convert array of longitude and latitude to array of X, y UTM coordinates. Parameters ------------ lats: arraylike 1d, Array composed of latitude values lons: ArrayLike 1d, Array composed of longitude values. data: pd.dataFrame Accepts dataframe containing the latitude and longitude coordinates by specifying the columns through 'lats' and 'lons' columns. .. versionadded:: 0.2.5 utm_zone: Optional, string zone number and 'S' or 'N' e.g. '55S'. Defaults to the centre point of the provided points, datum: string well known datum ex. WGS84, NAD27, NAD83, etc. kws: dict, Keywords argument passed to :meth:`~watex.site.Location.to_utm`. Returns -------- (easts, norths): Iterable object composed of easting and northing coordinates. .. versionadded:: 0.1.8 See Also ---------- watex.site.Location.to_utm: Convert longitude and latitude value to easting and northing coordinates. """ if ( (isinstance (lats, str ) or isinstance ( lons, str) ) and data is None): raise TypeError ("Data can't be None when latitude or longitude" " has a string argumemt.") if data is not None: lats, lons = assert_xy_in(lats, lons , data = data ) emsg = ("longitude is " if lons is None else 'latitude is') if ( lats is None or lons is None) else "Both longitude and latitude are" if lats is None or lons is None: raise TypeError (emsg + "missing.") # For consistency, check iterable values lats = is_iterable(lats, exclude_string= True, transform =True ) lons = is_iterable(lons, exclude_string= True, transform= True ) _check_consistency_size(lats, lons) lats = np.array(lats ) ; lons = np.array (lons ) easts = np.zeros_like (lats , dtype = np.float64) norths = np.zeros_like (lons , dtype = np.float64) for ii, (lat, lon) in enumerate (zip (lats, lons )) : Loc = Location() x, y = Loc.to_utm (lat, lon , utm_zone= utm_zone , **kws ) easts[ii] = x ; norths [ii] =y return easts, norths
[docs] @staticmethod def to_latlon_in( easts:ArrayLike, norths:ArrayLike, *, data=None, utm_zone:str=None, datum: str=None, **kws ): """ Convert array of longitude and latitude to array of X, y UTM coordinates. Parameters ------------ lats: arraylike 1d, Array composed of latitude values lons: ArrayLike 1d, Array composed of longitude values. utm_zone: Optional, string zone number and 'S' or 'N' e.g. '55S'. Defaults to the centre point of the provided points, data: pd.dataFrame Accepts dataframe containing the easting and northing coordinates by specifying the columns through 'easts' and 'lats' columns. .. versionadded:: 0.2.5 datum: string well known datum ex. WGS84, NAD27, NAD83, etc. kws: dict, Keywords argument passed to :meth:`~watex.site.Location.to_latlon`. Returns ------- (lats, lons): Iterable object composed of latitude and longitude coordinates. .. versionadded:: 0.1.8 See Also ------------ watex.site.Location.to_latlon: Convert easting and northing value to latitude and longitude coordinates. """ if ( ( isinstance (easts, str ) or isinstance ( norths, str) ) and data is None): raise TypeError ("Data can't be None when easting or northing" " has a string argumemt.") if data is not None: easts , norths = assert_xy_in(easts, norths, data = data ) emsg = ("easting is" if easts is None else 'northing is') if ( easts is None or norths is None) else ( "Both easting and northing are") if easts is None or norths is None: raise TypeError (emsg + " missing.") # For consistency, check iterable values easts = is_iterable(easts, exclude_string= True, transform =True ) norths = is_iterable(norths, transform= True , exclude_string =True ) _check_consistency_size(easts, norths) easts = np.array(easts ) ; norths = np.array (norths ) lats_lons =[] for east, north in zip (easts, norths ) : Loc = Location() lats_lons.append ( Loc.to_latlon(east, north , utm_zone= utm_zone , **kws ) ) return tuple (zip ( *lats_lons))
Location.__doc__="""\ Location class Assert, convert coordinates lat/lon , east/north to appropriate formats. Location does not follow the `scikit-learn` API in order to encompass the the syntax of :term:`pycsamt` and :term:`mtpy`. The latter follows the Generci Mapping Tool (`GMT <ttps://www.generic-mapping-tools.org>`__) API format. Parameters -------------- lat: float or string (DD:MM:SS.ms) latitude of point lon: float or string (DD:MM:SS.ms) longitude of point east: float easting coordinate in meters north: float northing coordinate in meters datum: string well known datum ex. WGS84, NAD27, NAD83, etc. utm_zone: Optional, string zone number and 'S' or 'N' e.g. '55S'. Defaults to the centre point of the provided points epsg: Optional, int epsg number defining projection (see http://spatialreference.org/ref/ for moreinfo) Overrides utm_zone if both are provided reference_ellipsoid: Optional, int reference ellipsoids is derived from Peter H. Dana's website- http://www.utexas.edu/depts/grg/gcraft/notes/datum/elist.html Department of Geography, University of Texas at Austin Internet: pdana@mail.utexas.edu . Default is ``23`` constrained to WGS84. """