2
\$\begingroup\$

As an exercise in using PyQt, the Python switch statement and abstract classes, I made a little app to make conversions between geodetic (WGS84) and PSD93 (Oman) and UTM 40N projections (instead of the ubiquitous calculator).

The app has a main window and 3 dialog windows; 4 types of conversions (geod_proj), (proj_geod), (proj_proj) and (geod_geod); and input/ out for geodetic coordinates can be either in decimal degrees or DMS.

The main and dialog windows designs are made with PyQt designer in XML. Conversions are done in the module convert_tools.py.

The main app is given below. I would appreciate a review and feedback. The entire source code is on GitHub.

'''
 PyQt application for conversion of coordinates for Oman PSD93 projectiom
 coordinate system (EPSG 3440)
 @ 2022 howdimain; [email protected]
'''
import sys
from dataclasses import dataclass
from abc import ABCMeta, abstractmethod
from enum import Enum
from pathlib import Path
from PyQt5 import uic, QtWidgets
from convert_tools import ConvertTools
@dataclass
class UIInterface:
 MainWindow: str = 'convert_main.ui'
 DialogFloatFloat: str = 'convert_dlg_float_float.ui'
 DialogDMSFloat: str = 'convert_dlg_DMS_float.ui'
 DialogFloatDMS: str = 'convert_dlg_float_DMS.ui'
class ConvertChoice(Enum):
 wgs84_psd93 = 1
 psd93_wgs84 = 2
 psd93_utm40 = 3
 utm40_psd93 = 4
 lon_lat = 5
class FormatChoice(Enum):
 Degrees = 1
 DMS = 2
convert = ConvertTools()
class MainWindow(QtWidgets.QMainWindow):
 def __init__(self, *args, **kwargs):
 super().__init__(*args, **kwargs)
 uic.loadUi(Path(__file__).parent / getattr(UIInterface, __class__.__name__), self)
 self.actionQuit.triggered.connect(self.action_quit)
 self.actionDegrees.triggered.connect(lambda x: self.select_format(FormatChoice.Degrees))
 self.actionDMS.triggered.connect(lambda x: self.select_format(FormatChoice.DMS))
 self.pb1_wgs84_psd93.clicked.connect(lambda x: self.select_convert(ConvertChoice.wgs84_psd93))
 self.pb2_psd93_wgs84.clicked.connect(lambda x: self.select_convert(ConvertChoice.psd93_wgs84))
 self.pb3_psd93_utm40.clicked.connect(lambda x: self.select_convert(ConvertChoice.psd93_utm40))
 self.pb4_utm40_psd93.clicked.connect(lambda x: self.select_convert(ConvertChoice.utm40_psd93))
 self.pb5_lon_lat.clicked.connect(lambda x: self.select_convert(ConvertChoice.lon_lat))
 self.dlg_class_proj_proj = DialogFloatFloat
 self.dlg_class_geog_proj = DialogFloatFloat
 self.dlg_class_proj_geog = DialogFloatFloat
 self.dlg_class_geog_geog = DialogFloatDMS
 def select_format(self, format_choice):
 match format_choice:
 case FormatChoice.Degrees:
 self.actionDegrees.setChecked(True)
 self.actionDMS.setChecked(False)
 self.menuFormat.setTitle('Degrees')
 self.dlg_class_proj_proj = DialogFloatFloat
 self.dlg_class_geog_proj = DialogFloatFloat
 self.dlg_class_proj_geog = DialogFloatFloat
 self.dlg_class_geog_geog = DialogFloatDMS
 case FormatChoice.DMS:
 self.actionDegrees.setChecked(False)
 self.actionDMS.setChecked(True)
 self.menuFormat.setTitle('DMS')
 self.dlg_class_proj_proj = DialogFloatFloat
 self.dlg_class_geog_proj = DialogDMSFloat
 self.dlg_class_proj_geog = DialogFloatDMS
 self.dlg_class_geog_geog = DialogDMSFloat
 case _:
 assert False, (
 f'check {format_choice} in {__class__.__name__} / '
 f'{__class__.select_format.__name__}'
 )
 def select_convert(self, convert_choice):
 match convert_choice:
 case ConvertChoice.wgs84_psd93:
 dlg = self.dlg_class_geog_proj(
 self, convert_choice, 'WGS84 to PSD93', 'Longitude', 'Latitude',
 'Easting', 'Northing'
 )
 case ConvertChoice.psd93_wgs84:
 dlg = self.dlg_class_proj_geog(
 self, convert_choice, 'PSD93 to WGS84', 'Easting', 'Northing',
 'Longitude', 'Latitude'
 )
 case ConvertChoice.psd93_utm40:
 dlg = self.dlg_class_proj_proj(
 self, convert_choice, 'PSD93 to UTM 40N', 'Easting', 'Northing',
 'Easting', 'Northing'
 )
 case ConvertChoice.utm40_psd93:
 dlg = self.dlg_class_proj_proj(
 self, convert_choice, 'UTM 40N to PSD93', 'Easting', 'Northing',
 'Easting', 'Northing'
 )
 case ConvertChoice.lon_lat:
 dlg = self.dlg_class_geog_geog(
 self, convert_choice, 'DMS to Degrees', 'Longitude', 'Latitude',
 'Longitude', 'Latitude'
 )
 case _:
 assert False, (
 f'Check {convert_choice} in {__class__.__name__} / '
 f'{__class__.select_convert.__name__}'
 )
 dlg.exec_()
 def action_quit(self):
 self.close()
 sys.exit()
class QtMixinMeta(type(QtWidgets.QDialog), ABCMeta):
 pass
class DialogMeta(QtWidgets.QDialog, metaclass=QtMixinMeta):
 def __init__(self, parent, convert_choice, title, input1, input2, output1, output2):
 super().__init__(parent)
 uic.loadUi(Path(__file__).parent / getattr(UIInterface, (self.__class__.__name__)), self)
 self.TitleText.setText(title)
 self.TextInput_1.setText(input1)
 self.TextInput_2.setText(input2)
 self.TextOutput_1.setText(output1)
 self.TextOutput_2.setText(output2)
 self.pb_exit.clicked.connect(self.action_exit)
 self.pb_convert.clicked.connect(lambda x: self.action_convert(convert_choice))
 @abstractmethod
 def action_convert(self, convert_choice):
 pass
 def action_exit(self):
 self.close()
class DialogFloatFloat(DialogMeta):
 def __init__(self, parent, convert_choice, title, input1, input2, output1, output2):
 super().__init__(parent, convert_choice, title, input1, input2, output1, output2)
 def action_convert(self, convert_choice):
 self.lineEditOutput_1.setText('')
 self.lineEditOutput_2.setText('')
 try:
 val1 = float(self.lineEditInput_1.text())
 val2 = float(self.lineEditInput_2.text())
 except ValueError:
 return
 match convert_choice:
 case ConvertChoice.wgs84_psd93:
 val1, val2 = convert.wgs84_to_psd93(val1, val2)
 f_fmt = '.2f'
 case ConvertChoice.psd93_wgs84:
 val1, val2 = convert.psd93_to_wgs84(val1, val2)
 f_fmt = '.6f'
 case ConvertChoice.psd93_utm40:
 val1, val2 = convert.psd93_to_utm40n(val1, val2)
 f_fmt = '.2f'
 case ConvertChoice.utm40_psd93:
 val1, val2 = convert.utm40n_to_psd93(val1, val2)
 f_fmt = '.2f'
 case _:
 assert False, f'Check {convert_choice} in {__class__.__name__}'
 self.lineEditOutput_1.setText(f'{val1:{f_fmt}}')
 self.lineEditOutput_2.setText(f'{val2:{f_fmt}}')
class DialogDMSFloat(DialogMeta):
 def __init__(self, parent, convert_choice, title, input1, input2, output1, output2):
 super().__init__(parent, convert_choice, title, input1, input2, output1, output2)
 def action_convert(self, convert_choice):
 self.lineEditOutput_1.setText('')
 self.lineEditOutput_2.setText('')
 val1 = self.lineEditInput_1.text()
 val2 = self.lineEditInput_2.text() if self.lineEditInput_2.text() else '0'
 val3 = self.lineEditInput_3.text() if self.lineEditInput_3.text() else '0'
 val4 = self.lineEditInput_4.text()
 val5 = self.lineEditInput_5.text() if self.lineEditInput_5.text() else '0'
 val6 = self.lineEditInput_6.text() if self.lineEditInput_6.text() else '0'
 val1 = ''.join([val1, '\u00B0', val2,'\'', val3, '\"', 'E'])
 val2 = ''.join([val4, '\u00B0', val5,'\'', val6, '\"', 'N'])
 val1, val2 = convert.convert_dms_to_dec_degree(val1, val2)
 if val1 == -1 and val2 == -1:
 return
 match convert_choice:
 case ConvertChoice.wgs84_psd93:
 val1, val2 = convert.wgs84_to_psd93(val1, val2)
 f_fmt = '.2f'
 case ConvertChoice.lon_lat:
 f_fmt = '.6f'
 case _:
 assert False, f'Check {convert_choice} in {__class__.__name__}'
 self.lineEditOutput_1.setText(f'{val1:{f_fmt}}')
 self.lineEditOutput_2.setText(f'{val2:{f_fmt}}')
class DialogFloatDMS(DialogMeta):
 def __init__(self, parent, convert_choice, title, input1, input2, output1, output2):
 super().__init__(parent, convert_choice, title, input1, input2, output1, output2)
 def action_convert(self, convert_choice):
 self.lineEditOutput_1.setText('')
 self.lineEditOutput_2.setText('')
 self.lineEditOutput_3.setText('')
 self.lineEditOutput_4.setText('')
 self.lineEditOutput_5.setText('')
 self.lineEditOutput_6.setText('')
 self.lineEditOutput_7.setText('')
 self.lineEditOutput_8.setText('')
 try:
 val1 = float(self.lineEditInput_1.text())
 val2 = float(self.lineEditInput_2.text())
 except ValueError:
 return
 match convert_choice:
 case ConvertChoice.psd93_wgs84:
 val1, val2 = convert.psd93_to_wgs84(val1, val2)
 case ConvertChoice.lon_lat:
 pass
 case _:
 assert False, f'Check {convert_choice} in {__class__.__name__}'
 val1, val2 = convert.convert_dec_degree_to_dms(val1, val2)
 lon, lat = convert.strip_lon_lat(val1, val2)
 if lon and lat:
 self.lineEditOutput_1.setText(f'{float(lon.group(1)):.0f}')
 self.lineEditOutput_2.setText(f'{float(lon.group(2)):.0f}')
 self.lineEditOutput_3.setText(f'{float(lon.group(3)):.2f}')
 self.lineEditOutput_4.setText(f'{lon.group(4)}')
 self.lineEditOutput_5.setText(f'{float(lat.group(1)):.0f}')
 self.lineEditOutput_6.setText(f'{float(lat.group(2)):.0f}')
 self.lineEditOutput_7.setText(f'{float(lat.group(3)):.2f}')
 self.lineEditOutput_8.setText(f'{lat.group(4)}')
def start_app():
 app = QtWidgets.QApplication([])
 main_window = MainWindow()
 main_window.show()
 app.exec_()
if __name__ == '__main__':
 start_app()
mdfst13
22.4k6 gold badges34 silver badges70 bronze badges
asked Sep 24, 2022 at 9:25
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

The members of UIInterface should all be lower_snake_case, as in main_window.

ConvertChoice members can use the auto feature of enum instead of writing out integers. Same for FormatChoice.

Isn't getattr(UIInterface, __class__.__name__) just.. the string 'UIInterface'? I think the getattr magic has limited utility. Perhaps the calculation of this path could be moved to a @classmethod on UIInterface.

On pb1_wgs84_psd93 etc., rather than lambdas, try setData on the elements for each ConvertChoice and having only one callback.

select_format() would be better re-expressed as a dictionary lookup, with all seven of those resulting properties sitting in the dictionary value as a tuple or named tuple. Get rid of your assert False and rely on the indexing operation [] throwing a KeyError if format_choice doesn't make sense. Similar for select_convert.

Add PEP484 typehints, especially to things like the signature of DialogFloatFloat.__init__.

These join calls:

 val1 = ''.join([val1, '\u00B0', val2,'\'', val3, '\"', 'E'])
 val2 = ''.join([val4, '\u00B0', val5,'\'', val6, '\"', 'N'])

are better-expressed with string interpolation:

val1 = f'{val1}\u00B0{val2}\'{val3}"E'
answered Sep 25, 2022 at 17:04
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.