# -*- coding: utf-8 -*-
'''
geophpy.geoposset
-----------------
Module for the management of Geographic Positioning Sets.
This module provides a number of tools, including the
:class:`~geophpy.geoposset.GeoPosSet` class,
to dealing with Geographic Positioning Sets (or Ground Control Points).
:copyright: Copyright 2014-2019 Lionel Darras, Philippe Marty, Quentin Vitale and contributors, see AUTHORS.
:license: GNU GPL v3.
Conversion
----------
- `utm_to_wgs84` -- Convert UTM to lat, long coordinates.
- `wgs84_to_utm` -- Convert lat, long to UTM coordinates.
Saving
------
- `save` -- Save GCPs to an ascii file.
- `to_kml` -- Save GCPs to a kml file.
'''
from __future__ import absolute_import
import os
# import glob # to manage severals files thanks to "*." extension
import csv
import shapefile # pyshp package for reading and writing shapefiles
import utm # utm and wgs84 conversion
import matplotlib.pyplot as plt
import numpy as np
import geophpy.geopositioning.kml as kml
import geophpy.filesmanaging.files as iofiles
import geophpy.misc.utils as utils
#-----------------------------------------------------------------------------#
# Module's parameters definition #
#-----------------------------------------------------------------------------#
###
##
# ... TODO ...
# use pyproj to manage other coordinate system and re-projection ?
##
###
# Display parameters
POINT_PARAMS = 'bo'
# available reference systems
REFSYSTEM_LIST = ['UTM', 'WGS84']
# UTM system limits
UTM_MINLETTER = 'E'
UTM_MINNUMBER = 1
UTM_MAXLETTER = 'X'
UTM_MAXNUMBER = 60
# file types read
FILETYPE_LIST = ['ascii', 'shapefile'] # list of file types available
format_chooser = {
".dat" : "ascii",
".txt" : "ascii",
".csv" : "ascii",
".shp" : "shapefile"
}
#-----------------------------------------------------------------------------#
# Geographic Positioning Set Object #
#-----------------------------------------------------------------------------#
[docs]class GeoPosSet:
''' Class to manage geographic positioning set.
Attributes
----------
refsystem : :obj:`str` or :obj:`None`, opt
Geographic reference system ('UTM', 'WGS84', ...).
utm_zoneletter : str, opt
Utm zone letter for 'UTM' `refsystem` (E -> X).
utm_zonenumber : int, opt
Utm zone number for 'UTM' `refsystem` (1 -> 60).
points_list : :obj:`list` of :obj:`scalar`, opt
List of Ground Control Points:
>>> [[lat1, lon1, x1, y1], [lat2, lon2, x2, y2], ...]
'''
###
##
### Change arguments order to more logic
# points_list, refsystem, utm_letter and utm_number
##
###
def __init__(self, refsystem=None, utm_letter=None, utm_number=None, points_list=None):
self.refsystem = refsystem
self.utm_letter = utm_letter
self.utm_number = utm_number
self.points_list = points_list
def __str__(self):
return '%s:\n Geographic system: %s\n GCPs: %s points' % (self.__class__.__name__,
self.refsystem,
len(self.points_list))
## ###
## ##
## # To Be Implemented
## @classmethod
## def from_ascii_file(cls, filenameslist):
## pass
##
## @classmethod
## def from_shape_file(cls, filenameslist):
## pass
## #
## ##
## ###
[docs] @classmethod
def from_ascii_file(cls, filenames, delimiter=None):
''' Build a :class:`geophpy.geoposdet.GeoPosSet` object from one or several ascii files.
Parameters
----------
filenames : :obj:`str` or :obj:`list` of :obj:`str`
Names of files to be read.
delimiter : :obj:`str` or ``None``
The ASCII file delimiter. If ``None`` (by default),
the delimiter will be sniffed from the file itself.
Returns
-------
:class:`~geophpy.geoposset.GeoPosSet` object (possibly empty).
succes : ``bool``
``True`` if build was successful, ``False`` otherwise.
'''
# Attributes initialization
refsystem = None
utm_letter = None
utm_number = None
points_list = []
point_num = 1
success = True
# Type checking & extending file names list
## takes of cases with a star operator : ['GPS_ex2.dat', '*.csv']
full_filelist = iofiles.extent_file_list(filenames)
# Reading files
for filename in full_filelist:
# If file exists
if os.path.isfile(filename) is True:
# Sniffing delimiter
## passing 1st line (=refsystem)
if delimiter is None:
delimiter = iofiles.sniff_delimiter(filename, skiplines=1)
# Reading csv file
with open(filename, 'r', newline='') as csvfile:
reader = csv.reader(csvfile, delimiter=delimiter)
# Dealing with the 1st line (refsystem) ####################
# refsytem + info (could be ['UTM', '32', 'N'])
fullrefsystem = reader.__next__()
# only refsytem
ref_system = fullrefsystem[0]
# UTM
if ref_system.lower() == 'UTM'.lower():
refsystem = ref_system
try:
# Valid UTM zone letter
utm_zoneletter = fullrefsystem[1]
if isvalid_utm_letter(utm_zoneletter):
utm_letter = utm_zoneletter.upper()
# Valid UTM zone number
utm_zonenumber = fullrefsystem[2]
if isvalid_utm_number(utm_zonenumber):
utm_number = utm_zonenumber
except:
pass
# WGS84
elif ref_system.lower() == 'WGS84'.lower():
refsystem = ref_system
# Unkwnown system or just points
else:
# Unkwnown refsystem
if not len(fullrefsystem) > 1:
refsystem = ref_system
# Just points
else:
row = fullrefsystem
try:
x = float(row[3])
except:
x = None
try:
y = float(row[4])
except:
y = None
try:
###
##
# Why ask for point num in file format if not used here ????
##
###
points_list.append([point_num, float(row[1]), float(row[2]), x, y])
point_num += 1
except IOError:
print("File Read Error (", filename, ": ", row, ")")
success = False
# Reading data points ######################################
# num, east, north, y, x
for row in reader:
try:
x = float(row[3])
except:
x = None
try:
y = float(row[4])
except:
y = None
try:
points_list.append([point_num, float(row[1]), float(row[2]), x, y])
point_num += 1
except IOError:
print("File Read Error (", filename, ": ", row, ")")
success = False
# No points read
if not points_list: # empty sequence are False
points_list = None
success = False
# Conversion to np.array
points_list.append([-1, 0, 0, None, None]) # trick to convert np.array in good format
points_list = np.array(points_list)
points_list = np.delete(points_list, -1, 0) # to delete last row added
return success, cls(refsystem, utm_letter, utm_number, points_list)
@classmethod
def from_shape_file(cls, filenames):
''' Build a :class:`geophpy.geoposdet.GeoPosSet` object from one or several shape files.
Parameters
----------
filenames : :obj:`str` or :obj:`list` of :obj:`str`
Names of files to be read.
delimiter : :obj:`str` or ``None``
The ASCII file delimiter. If ``None`` (by default),
the delimiter will be sniffed from the file itself.
Returns
-------
:class:`~geophpy.geoposset.GeoPosSet` object (possibly empty).
succes : bool
``True`` if build was successful, ``False`` otherwise.
'''
# Attributes initialization
refsystem = None
utm_letter = None
utm_number = None
points_list = []
point_num = 0
success = True
# Type checking & extending file names list
## takes of cases with a star operator : ['GPS_ex2.dat', '*.csv']
full_filelist = iofiles.extent_file_list(filenames)
# Reading shapefile
for filename in full_filelist:
try:
sf = shapefile.Reader(filename)
shapes = sf.shapes()
# for each shape in the file
for shape in shapes:
valid_point = (shape.shapeType == shapefile.POINT
or shape.shapeType == shapefile.POINTZ
or shape.shapeType == shapefile.POINTM)
if valid_point:
points_list.append([point_num, shape.points[0][0],
shape.points[0][1],
None,
None
])
point_num = point_num + 1
except IOError:
print("File Read Error (", filename, ")")
success = False
break
# Conversion to np.array
points_list.append([-1, 0, 0, None, None]) # trick to convert np.array in good format
points_list = np.array(points_list)
points_list = np.delete(points_list, -1, 0) # to delete last row added
return success, cls(refsystem, utm_letter, utm_number, points_list)
[docs] @classmethod
def from_file(cls, filenames, filetype=None):
'''Build a :class:`~geophpy.geoposdet.GeoPosSet` object from one or several files.
Parameters
----------
filenames : ``str`` or ``list`` of ``str``
Names of files to be read.
filetype : {'ascii', 'shapefile', ``None``}
Type of the files to read. Id ``None`` (default), the file type
will be determined from the file extension.
Returns
-------
:class:`~geophpy.geoposset.GeoPosSet` object (possibly empty).
succes : bool
``True`` if build was successful, ``False`` otherwise.
'''
read_func_chooser = {'ascii' :cls.from_ascii_file,
'shape' : cls.from_shape_file}
# type checking
if isinstance(filenames, str):
filenames = [filenames]
# Type from file extension
if filetype is None:
file_ext = os.path.splitext(filenames[0])[1]
filetype = format_chooser.get(file_ext, 'ascii')
return read_func_chooser[filetype](filenames)
[docs] def to_ascii(self, filename, delimiter=';'):
''' Save :class:`~geophpy.geoposset.GeoPosSet` points list to an ascii file.
Parameters
----------
filename: ``str``
Name of the ascii file to save in.
delimiter: ``str``, opt
Delimiter to use.
Returns
-------
success: ``bool``
``True`` if a file was saved.
'''
# Writing reference system
if self.refsystem is not None:
# UTM
if self.refsystem.upper() == 'UTM':
# UTM;N;31
try:
# Valid UTM zone letter
if isvalid_utm_letter(self.utm_zoneletter):
utm_letter = self.utm_zoneletter.upper()
# Valid UTM zone number
if isvalid_utm_number(self.utm_zonenumber):
utm_number = self.utm_zonenumber
except:
utm_letter = ''
utm_number = ''
refsystem = delimiter.join('UTM' + utm_letter + utm_number)
# Other
else:
refsystem = self.refsystem.upper()
# No refsystem
else:
refsystem = ''
# Writting GCPs
arr = self.points_list
np.savetxt(filename, arr, fmt='%g', delimiter=delimiter, header=refsystem, comments='')
success = True
## try:
## with open(filename, 'w', newline='') as csvfile:
## writer = csv.writer(csvfile, delimiter=delimiter)
##
## # Reference system (1st row)
## if self.refsystem is not None:
## # UTM
## if 'UTM'.lower() == self.refsystem.lower():
## # UTM;N;31
## refsystem = [self.refsystem.upper(),
## self.utm_letter.upper(),
## self.utm_number]
## # Other
## else:
## refsystem = [self.refsystem.upper()]
## print(refsystem)
##
## writer.writerow(refsystem)
##
## # No refsystem
## else:
## pass
## # Points list
## writer.writerows(self.points_list)
##
## except:
## success = False
## ResultFile = csv.writer(open(filename,"w", newline=''), delimiter=';')
## # writes the first line of the file with reference system ('UTM', 'WGS84', ...)
## fullrefsystem = [self.refsystem]
## # if 'UTM' ref system, writes the second and third lines with UTM zone letter and number
## if (self.refsystem == 'UTM'):
## fullrefsystem = [self.refsystem, self.utm_letter, self.utm_number]
## else :
## fullrefsystem = [self.refsystem]
##
## ResultFile.writerow(fullrefsystem)
## # writes points list in the file.
## ResultFile.writerows(self.points_list)
return success
[docs] def plot(self, filename=None, dpi=None, transparent=False,
i_xmin=None, i_xmax=None, i_ymin=None, i_ymax=None, long_label=False):
''' Display GCPs.
Plots the GCPs using the point number as label.
To save the plot, use the ``picturefilename`` option.
Parameters
----------
filename : ``str`` or ``None``
Name of the file to save the picture.
If ``None``, no picture is saved.
dpi : ``int``
'dot per inch' definition for the picture file if filename is not None.
transparent : ``bool``
if True, picture display points not plotted as transparents
i_xmin : x minimal value to display, None by default
i_xmax : x maximal value to display, None by default
i_ymin : y minimal value to display, None by default
i_ymax : y maximal value to display, None by default
long_label : bool
Flag to display both point number and local coordinates.
Returns
-------
success: True if no error
fig: ``Figure`` object
'''
fig = plt.figure() # creation of the empty figure
for point in self.points_list:
plt.plot(point[1], point[2], POINT_PARAMS)
###
# Complete label plot
if long_label:
dx, dy = 3, 0
label_box_opts = dict(fc='white', ec='none', alpha=0.5)
if point.size == 5:
strg = '%s (%s, %s)' % (point[0], point[3], point[4])
else:
strg = '%s (%s, %s)' % (point[0], 'x', 'y')
plt.text(point[1] + dx, point[2] + dy, strg, bbox=label_box_opts)
else:
plt.text(point[1] + dx, point[2] + dy, point[0], bbox=label_box_opts)
xmin = min(self.points_list.T[0])
xmax = max(self.points_list.T[0])
ymin = min(self.points_list.T[1])
ymax = max(self.points_list.T[1])
success = True
if success:
# for each x or y limit not configured in input, initialisation of the value with the limits of points displayed
nolimits = True
if i_xmin is None:
i_xmin = xmin
else:
nolimits = False
if i_xmax is None:
i_xmax = xmax
else:
nolimits = False
if i_ymin is None:
i_ymin = ymin
else:
nolimits = False
if i_ymax is None:
i_ymax = ymax
else:
nolimits = False
if not nolimits: # sets the axis limits
xmin, xmax, ymin, ymax = plt.axis([i_xmin, i_xmax, i_ymin, i_ymax])
else: # gets the axis limits
xmin, xmax, ymin, ymax = plt.axis()
# to have the same scale in X and Y axis
dy = ymax - ymin
dx = xmax - xmin
if dy > dx:
xmax = xmin + dy
elif dx > dy:
ymax = ymin + dx
plt.axis([xmin, xmax, ymin, ymax])
ax = plt.gca()
ax.ticklabel_format(useOffset=False)
# Forcing equal x and Y
ax.set_aspect('equal')
if filename is not None:
plt.savefig(filename, dpi=dpi, transparent=transparent)
return success, fig
[docs] def to_kml(self, filename):
""" Save :class:`~geophpy.geoposset.GeoPosSet` points list to a kml file.
Parameters
----------
filename: ``str``
Name for the kml file to save in.
Returns
-------
success: ``bool``
``True`` if a file was saved.
"""
success = True # by default
kml.geoposset_to_kmlfile(self.points_list, filename)
return success
def GCPs(self):
''' Returns the list of ground control Points. '''
return self.points_list
#-----------------------------------------------------------------------------#
# General Geographic Positioning Set Environments functions #
#-----------------------------------------------------------------------------#
[docs]def refsys_getlist():
""" List of available geographic reference system. """
return REFSYSTEM_LIST
[docs]def filetype_getlist():
""" List read file types, 'ascii', 'shapefile', ... """
return FILETYPE_LIST
###
##
# ... MOVE TO ... Should we move those general routines to geopositionning.general
##
###
[docs]def utm_to_wgs84(easting, northing, zonenumber, zoneletter):
""" Conversion from UTM to WGS84 coordinates (lat, lon).
Parameters
----------
easting : ``scalar``
Easting UTM coordinate.
northing : ``scalar``
Northing UTM coordinate.
zonenumber : ``int``
UTM zone number.
zoneletter: ``str``
UTM zone letter.
Returns
-------
latitude : ``scalar``
WGS84 latitude coordinate.
longitude : ``scalar``
WGS84 longitude coordinate.
"""
### Making it work on list too
easting = utils.make_sequence(easting)
northing = utils.make_sequence(northing)
if len(easting) != len(northing):
print('Easting and Norting should be same size but'
'but (%s and %s) encountered' %(len(easting), len(northing))
)
return None
rank = len(easting)
zonenumber = utils.make_normalize_sequence(zonenumber, rank)
zoneletter = utils.make_normalize_sequence(zoneletter, rank)
latitude, longitude = [], []
for i in range(rank):
lat, long = utm.to_latlon(easting[i],
northing[i],
zonenumber[i],
zoneletter[i])
latitude.append(lat)
longitude.append(long)
# Single value
if len(latitude) == 1:
return latitude[0], longitude[0]
return latitude, longitude
###
# return utm.to_latlon(easting, northing, zonenumber, zoneletter)
[docs]def wgs84_to_utm(latitude, longitude):
""" Conversion from WGS84 to UTM coordinates.
works on list
Parameters
----------
latitude : ``scalar``
WGS84 latitude coordinate.
longitude : ``scalar``
WGS84 longitude coordinate.
Returns
-------
easting : ``scalar``
Easting UTM coordinate.
northing : ``scalar``
Northing UTM coordinate.
zonenumber : ``int``
UTM zone number.
zoneletter: ``str``
UTM zone letter.
"""
### Making it work on list too
latitude = utils.make_sequence(latitude)
longitude = utils.make_sequence(longitude)
if len(latitude) != len(longitude):
print('Latitude and Longitude should be same size but'
'but (%s and %s) encountered' %(len(latitude), len(longitude))
)
return None
easting, northing, number, letter = [], [], [], []
for i, lat in enumerate(latitude):
#lat = latitude[i]
long = longitude[i]
east, north, num, let = utm.from_latlon(lat, long)
easting.append(east)
northing.append(north)
number.append(num)
letter.append(let)
# Single value
if len(easting) == 1:
return easting[0], northing[0], number[0], letter[0]
return easting, northing, number, letter
###
#return utm.from_latlon(latitude, longitude)
[docs]def utm_getzonelimits():
""" UTM coordinates system min and max numbers and letters.
Returns
-------
min_number : ``int``
Minimal number of the UTM zone (1).
min_letter : ``str``
Minimal letter of the UTM zone (E).
max_number : ``int``
Maximal number of the UTM zone (60).
max letter : ``str``
Maximal letter of the UTM zone (X).
"""
return UTM_MINNUMBER, UTM_MINLETTER, UTM_MAXNUMBER, UTM_MAXLETTER
[docs]def isvalid_utm_letter(strg):
''' Check validity of an UTM zone letter. '''
if not isinstance(strg, str):
strg = str(strg)
valid = (strg.isalpha()
and strg >= UTM_MINLETTER
and strg <= UTM_MAXLETTER
)
return valid
[docs]def isvalid_utm_number(strg):
''' Check validity of an UTM zone number. '''
if not isinstance(strg, str):
strg = str(strg)
valid = (strg.isdigit()
and float(strg) >= UTM_MINNUMBER
and float(strg) <= UTM_MAXNUMBER
)
return valid