diff --git a/Searcher/searchAddress.py b/Searcher/searchAddress.py index 8ea2acc..1d61e05 100644 --- a/Searcher/searchAddress.py +++ b/Searcher/searchAddress.py @@ -1,16 +1,18 @@ import json import os from http.client import IncompleteRead +from json import JSONDecodeError +from typing import Union, Tuple from urllib.error import HTTPError, URLError from urllib.parse import quote from urllib.request import urlopen from qgis.PyQt.QtCore import QVariant from qgis.core import QgsGeometry, QgsFeature, QgsField, QgsFields, \ - QgsProject, QgsVectorLayer, QgsMessageLog, Qgis + QgsVectorLayer, QgsMessageLog, Qgis from qgis.utils import iface -from ..utils import tr, add_map_layer_to_group, search_group_name +from ..utils import tr, add_map_layer_to_group, search_group_name, project class SearchAddress: @@ -35,7 +37,7 @@ def __init__(self): QgsField("y", QVariant.Double, "double", 10, 4), ] - def fetch_address(self, address): + def fetch_address(self, address: str) -> None: self.address = address uug = 'https://services.gugik.gov.pl/uug?request=GetAddress&address=' url = uug + quote(self.address) @@ -54,7 +56,7 @@ def fetch_address(self, address): return self.res.decode() - def get_layer(self): + def get_layer(self) -> Union[bool, Tuple[str]]: req_type = self.jres['type'] if req_type in ['city', 'address']: org = 'MultiPoint?crs=epsg:2180&index=yes' @@ -73,8 +75,9 @@ def get_layer(self): return False return org, obj_type, qml - def get_layer_data(self, org, obj_type, qml): - lyr = QgsProject.instance().mapLayersByName(obj_type) + def get_layer_data(self, org: str, obj_type: str, + qml: str) -> QgsVectorLayer: + lyr = project.mapLayersByName(obj_type) if lyr: return lyr[0] @@ -94,10 +97,11 @@ def get_layer_data(self, org, obj_type, qml): lyr.loadNamedStyle(os.path.join(direc, qml)) return lyr - def process_results(self): + def process_results(self) -> Union[ + Tuple[bool, str], Tuple[bool, QgsFeature]]: try: self.jres = json.loads(self.res) - except Exception: + except JSONDecodeError: return False, tr('Cannot parse results.') if 'found objects' in self.jres: @@ -145,14 +149,14 @@ def process_results(self): return True, feats - def zoom_to_feature(self, layer): - layer = QgsProject.instance().mapLayersByName(layer)[0] + def zoom_to_feature(self, layer: str) -> None: + layer = project.mapLayersByName(layer)[0] iface.mapCanvas().zoomScale(500) layer.selectByIds([len(layer)]) iface.mapCanvas().zoomToSelected(layer) layer.removeSelection() - def add_feats(self, feats): + def add_feats(self, feats: QgsFeature) -> Union[bool, None]: if isinstance(feats, str): pass else: diff --git a/Searcher/searchParcel.py b/Searcher/searchParcel.py index 9ec2107..385c16a 100644 --- a/Searcher/searchParcel.py +++ b/Searcher/searchParcel.py @@ -2,15 +2,16 @@ import re import socket from http.client import IncompleteRead +from typing import List from urllib.error import HTTPError, URLError from urllib.request import urlopen from qgis.core import QgsGeometry, QgsFeature, \ - QgsProject, QgsVectorLayer, Qgis + QgsVectorLayer, Qgis from qgis.utils import iface from ..utils import tr, CustomMessageBox, search_group_name, \ - add_map_layer_to_group + add_map_layer_to_group, project class FetchULDK: @@ -18,7 +19,7 @@ def __init__(self, params=None): self.params = params self.responce = [] - def fetch_list(self, area, teryt): + def fetch_list(self, area: str, teryt: str) -> bool: map(str, teryt) if area not in {'powiat', 'gmina', 'obreb'}: self.responce = [] @@ -26,11 +27,11 @@ def fetch_list(self, area, teryt): self.params = f'obiekt={area}&wynik=nazwa%2Cteryt&teryt={teryt}&' return self.fetch() - def fetch_voivodeships(self): + def fetch_voivodeships(self) -> bool: self.params = 'obiekt=wojewodztwo&wynik=nazwa%2Cteryt&teryt=&' return self.fetch() - def fetch_parcel(self, teryt): + def fetch_parcel(self, teryt: str) -> bool: self.responce = [] if not isinstance(teryt, str): return False @@ -38,19 +39,15 @@ def fetch_parcel(self, teryt): 'geom_wkt,teryt,voivodeship,county,region,commune,parcel' return self.fetch() - def fetch_in_point(self, coords): - # TODO: Dodac pobieranie działki w pkt po kliknieciu - pass - - def fetch(self): + def fetch(self) -> bool: if '- gmina' in self.params or '- miasto' in self.params: flag = self.params.find('-') self.params = self.params[0:flag] url = f'https://uldk.gugik.gov.pl/?{self.params}' self.responce = [] try: - with urlopen(url, timeout=19) as r: - content = r.read() + with urlopen(url, timeout=19) as url_handler: + content = url_handler.read() except IncompleteRead: CustomMessageBox(None, f"{tr('Error!')} {tr('Service returned incomplete responce.')}").button_ok() @@ -75,10 +72,11 @@ def fetch(self): f"{tr('Service did not find any matches, wrong plot number.')}").button_ok() return False - self.responce = self.natural_sort([x for x in res[1:] if x != '']) + self.responce = self.natural_sort( + [ter for ter in res[1:] if ter != '']) return True - def natural_sort(self, list): + def natural_sort(self, list: List[str]) -> List[str]: convert = lambda text: int(text) if text.isdigit() else text.lower() alpha_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key)] return sorted(list, key=alpha_key) @@ -97,8 +95,8 @@ def __init__(self): self.lyr_name, 'memory' ) - def get_layer(self): - lyr = QgsProject.instance().mapLayersByName(self.lyr_name) + def get_layer(self) -> None: + lyr = project.mapLayersByName(self.lyr_name) if len(lyr) > 0: self.lyr = lyr[0] return @@ -108,7 +106,7 @@ def get_layer(self): self.lyr.loadNamedStyle( os.path.join(direc, 'layer_style', 'dzialki.qml')) - def parse_responce(self, resp): + def parse_responce(self, resp: List[str]) -> None: feats = [] for row in resp: ft = self._create_feature(row) @@ -129,14 +127,14 @@ def parse_responce(self, resp): Qgis.Warning ) - def zoom_to_feature(self, layer): - layer = QgsProject.instance().mapLayersByName(layer)[0] + def zoom_to_feature(self, layer: str) -> None: + layer = project.mapLayersByName(layer)[0] iface.mapCanvas().zoomScale(500) layer.selectByIds([len(layer)]) iface.mapCanvas().zoomToSelected(layer) layer.removeSelection() - def _create_feature(self, row): + def _create_feature(self, row: str) -> QgsFeature: if row[:4].upper() == 'SRID': row = row[row.index(';') + 1:] feat = QgsFeature() diff --git a/Searcher/searchTool.py b/Searcher/searchTool.py index 22bd049..35a56a8 100644 --- a/Searcher/searchTool.py +++ b/Searcher/searchTool.py @@ -1,18 +1,22 @@ import json +from json import JSONDecodeError from urllib.parse import quote from urllib.request import urlopen import requests from PyQt5.QtCore import Qt -from PyQt5.QtGui import QFont, QFontMetrics +from qgis.PyQt.QtGui import QFont, QFontMetrics from qgis.PyQt.QtCore import QStringListModel from qgis.PyQt.QtCore import QTimer from qgis.PyQt.QtWidgets import QCompleter +from qgis.core import QgsVectorLayer from qgis.utils import iface +from typing import Union, Dict, List from .searchAddress import SearchAddress from .searchParcel import FetchULDK, ParseResponce -from ..utils import tr, CustomMessageBox +from ..utils import tr, CustomMessageBox, add_map_layer_to_group, \ + ProgressDialog, identify_layer_in_group, root, WFS_PRG class SearcherTool: @@ -53,10 +57,10 @@ def __init__(self, dock, iface): self.font = QFont('Agency FB') self.fontm = QFontMetrics(self.font) - def textChanged(self): + def textChanged(self) -> None: self.typing_timer.start(300) - def tips(self): + def tips(self) -> None: address = self.dock.lineEdit_address.displayText() url_pref = 'http://services.gugik.gov.pl/uug/?request=GetAddress&address=' quo_adr = quote(address) @@ -79,32 +83,32 @@ def tips(self): self.getStreets(obj['1']['simc'], obj['1']['city']) if obj_type == 'address': self.names.setStringList([ - f"{obj['1']['city']}, {obj['1']['street']} {obj['1']['number']}"]) - if limit == 0: + f"{obj['1']['city']}, {obj['1']['street']} {obj['1']['number']}"]) + if not limit: return self.completer.setCompletionPrefix(f"{address.split(',')[0]}, ") self.completer.complete() - except Exception: + except (JSONDecodeError, TypeError): return - def getStreets(self, simc, city): + def getStreets(self, simc: str, city: str) -> None: try: data = json.loads(urlopen( 'https://services.gugik.gov.pl/uug/?request=GetStreet&simc=' + simc).read().decode()) obj = data['results'] self.names.setStringList( [f"{city}, {obj[element]['street']}" for element in obj]) - except Exception: + except TypeError: self.names.setStringList([]) - def validateCity(self, obj): + def validateCity(self, obj: Dict[str, Dict[str, int]]) -> None: city = obj['1']['city'] self.names.setStringList( [f"{city}, {obj[element]['simc']} {obj[element]['county']}" for element in obj]) self.completer.popup().pressed.connect(lambda: self.userPick()) - def userPick(self): + def userPick(self) -> None: line = self.dock.lineEdit_address.text().split() self.completer.popup().pressed.disconnect() if len(line) == 3: @@ -116,7 +120,7 @@ def userPick(self): else: return - def search_address(self): + def search_address(self) -> None: validate_address = self.validate_lineedit() if validate_address: lineedit = self.dock.lineEdit_address.text().split(',') @@ -141,19 +145,65 @@ def change_scale(): self.timer.timeout.connect(change_scale) self.timer.start(10) - def validate_lineedit(self): + def validate_lineedit(self) -> bool: if self.dock.lineEdit_address.text(): return True else: CustomMessageBox(None, f" {tr('Invalid')} {tr('Empty address field')}").button_ok() - def widthforview(self, result): + def add_chosen_border(self, mess: str) -> None: + lay_data = {'Obręby_ewidencyjne': ["A06_Granice_obrebow_ewidencyjnych", + self.dock.comboBox_obr], + 'Gminy': ["A03_Granice_gmin", self.dock.comboBox_gmina], + 'Powiaty': ["A02_Granice_powiatow", + self.dock.comboBox_pow], + 'Województwa': ["A01_Granice_wojewodztw", + self.dock.comboBox_woj]} + for lay_key in lay_data: + if lay_data[lay_key][1].currentIndex(): + _, jpt_kod_je = lay_data[lay_key][1].currentText().split("|") + if lay_key == "Gminy": + jpt_kod_je = jpt_kod_je.replace("_", "") + adres = lay_data[lay_key][0] + lay_name = lay_key + break + if 'jpt_kod_je' not in locals() or 'adres' not in locals()\ + or 'lay_name' not in locals(): + CustomMessageBox(self.iface.mainWindow(), + mess).button_ok() + return + prg_dlg = ProgressDialog(self.iface.mainWindow()) + prg_dlg.start_steped(tr("Adding layers...")) + prg_dlg.start() + url = f"{WFS_PRG}?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAME=ms:{adres}&TYPENAMES=ms:{adres}" + vlayer = QgsVectorLayer(url, "wfs_lay", "WFS") + vlayer.setSubsetString(f"""SELECT * FROM {adres} + WHERE JPT_KOD_JE = '{jpt_kod_je}'""") + group_name = "GRANICE" + granice_group = root.findGroup(group_name) + if not granice_group: + root.addGroup(group_name) + lay = identify_layer_in_group(group_name, lay_name) + if not lay: + lay = QgsVectorLayer("Polygon", lay_name, "memory") + attr = vlayer.dataProvider().fields().toList() + lay.dataProvider().addAttributes(attr) + lay.updateFields() + add_map_layer_to_group(lay, "GRANICE") + vlayer.selectAll() + feat = vlayer.selectedFeatures()[0] + lay.dataProvider().addFeature(feat) + lay.updateExtents() + self.searchaddress_call.zoom_to_feature(lay.name()) + prg_dlg.stop() + + def widthforview(self, result: List[str]) -> int: longest = max(result, key=len) width = 2 * self.fontm.width(longest) return width - def fetch_voivodeship(self): + def fetch_voivodeship(self) -> None: """Fetching voivodeship list from GUGiK""" voi_list = [ 'Dolnośląskie|02', @@ -178,7 +228,7 @@ def fetch_voivodeship(self): self.dock.comboBox_woj.blockSignals(False) self.clear_comboBoxes('voi') - def woj_changed(self): + def woj_changed(self) -> None: voi = self._get_voi_code() if not voi: return @@ -191,7 +241,7 @@ def woj_changed(self): self.dock.comboBox_pow.view().setFixedWidth(self.widthforview(result)) self.dock.comboBox_pow.blockSignals(False) - def pow_changed(self): + def pow_changed(self) -> None: dis = self._get_dis_code() if not dis: return @@ -200,7 +250,7 @@ def pow_changed(self): fe.fetch_list('gmina', dis) self.dock.comboBox_gmina.blockSignals(True) result, communities = fe.responce, [] - multiples = [e.split('|')[0] for e in result] + multiples = [powiat.split('|')[0] for powiat in result] for district in result: end = district[-2:] if multiples.count(district.split('|')[0]) > 1: @@ -217,7 +267,7 @@ def pow_changed(self): self.widthforview(communities)) self.dock.comboBox_gmina.blockSignals(False) - def gmi_changed(self): + def gmi_changed(self) -> None: mun = self._get_mun_code() self.clear_comboBoxes('mun') if not mun: @@ -230,7 +280,7 @@ def gmi_changed(self): self.dock.comboBox_obr.view().setFixedWidth(self.widthforview(result)) self.dock.comboBox_obr.blockSignals(False) - def clear_comboBoxes(self, level=None): + def clear_comboBoxes(self, level: str = None) -> None: """Clear comboboxes to level where user change something""" self.dock.comboBox_obr.blockSignals(True) self.dock.comboBox_obr.clear() @@ -251,34 +301,37 @@ def clear_comboBoxes(self, level=None): self.dock.comboBox_pow.addItem(tr('District')) self.dock.comboBox_pow.blockSignals(False) - def _get_voi_code(self): + def _get_voi_code(self) -> Union[str, bool]: voi_txt = self.dock.comboBox_woj.currentText() if '|' not in voi_txt: self.clear_comboBoxes() return False return voi_txt.split('|')[1] - def _get_dis_code(self): + def _get_dis_code(self) -> Union[str, bool]: dis_txt = self.dock.comboBox_pow.currentText() if '|' not in dis_txt: self.clear_comboBoxes('dis') return False return dis_txt.split('|')[1] - def _get_mun_code(self): + def _get_mun_code(self) -> Union[str, bool]: mun_txt = self.dock.comboBox_gmina.currentText() if '|' not in mun_txt: self.clear_comboBoxes('mun') return False return mun_txt.split('|')[1] - def search_parcel(self): - adr = '' # ful address of parcel + def search_parcel(self) -> None: parc = self.dock.lineEdit_parcel.text() if '.' in parc and '_' in parc: # user input whole address in parcel adr = parc else: comm = self.dock.comboBox_obr.currentText() + if not parc: + self.add_chosen_border( + f"{tr('Address of parcel is not valid.')}") + return if '|' not in comm: CustomMessageBox(None, f"{tr('Address of parcel is not valid.')}").button_ok() @@ -286,9 +339,9 @@ def search_parcel(self): comm = comm.split('|')[1] adr = f'{comm}.{parc}' - f = FetchULDK() - if not f.fetch_parcel(adr): + feULDK = FetchULDK() + if not feULDK.fetch_parcel(adr): return pr = ParseResponce() pr.get_layer() - pr.parse_responce(f.responce) + pr.parse_responce(feULDK.responce) diff --git a/i18n/giap_pl.ts b/i18n/giap_pl.ts index 139688e..7cabbdd 100644 --- a/i18n/giap_pl.ts +++ b/i18n/giap_pl.ts @@ -647,6 +647,11 @@ Proszę czekać... Empty address field Puste pole adresu + + + Adding layers... + Dodawanie warstwy... + Cadastral district diff --git a/utils.py b/utils.py index 94f5906..593f277 100644 --- a/utils.py +++ b/utils.py @@ -14,6 +14,7 @@ from qgis.utils import iface project = QgsProject.instance() +root = project.layerTreeRoot() class CustomMessageBox(QMessageBox): @@ -124,6 +125,7 @@ def set_size(self, value: int) -> None: self.stylesheet = f'*{{font: {value}pt;}} {self.stylesheet}' self.setStyleSheet(self.stylesheet) + class SingletonModel: __instance = None @@ -136,6 +138,56 @@ def __new__(cls, *args): return SingletonModel.__instance +class ProgressDialog(QProgressDialog, SingletonModel): + + def __init__(self, parent=None, title='GIAP-PolaMap(lite)'): + super(ProgressDialog, self).__init__(parent) + self.setWindowTitle(title) + self.setWindowIcon(icon_manager(['window_icon'])['window_icon']) + self.setLabelText('Proszę czekać...') + self.setFixedWidth(300) + self.setFixedHeight(100) + self.setMaximum(100) + self.setCancelButton(None) + self.setWindowFlags(Qt.Dialog | Qt.WindowCloseButtonHint) + self.rejected.connect(self.stop) + self.setWindowModality(Qt.WindowModal) + + def make_percent_step(self, step=100, new_text=None): + self.setStyleSheet(self.stylesheet) + if new_text: + self.setLabelText(new_text) + if "wczytywanie" in new_text: + for pos in range(100 - self.value()): + QApplication.processEvents() + self.setValue(self.value() + 1) + return + for pos in range(step): + QApplication.processEvents() + self.setValue(self.value() + 1) + QApplication.sendPostedEvents() + QApplication.processEvents() + + def start_steped(self, title='Trwa ładowanie danych.\n Proszę czekać...'): + self.setLabelText(title) + self.setValue(1) + self.show() + QApplication.sendPostedEvents() + QApplication.processEvents() + + def start(self): + self.setFixedWidth(250) + self.setMaximum(0) + self.setCancelButton(None) + self.show() + QApplication.sendPostedEvents() + QApplication.processEvents() + + def stop(self): + self.setValue(100) + self.close() + + def identify_layer(ls, layer_to_find): for layer in list(ls.values()): if layer.name() == layer_to_find: @@ -257,6 +309,7 @@ def paint(self, painter, option, index): GIAP_NEWS_WEB_PAGE = 'https://www.giap.pl/aktualnosci/' +WFS_PRG = "https://mapy.geoportal.gov.pl/wss/service/PZGIK/PRG/WFS/AdministrativeBoundaries" # oba poniższe słowniki powinny być spójne WMS_SERVERS = { @@ -1197,6 +1250,7 @@ def paint(self, painter, option, index): max_ele_nazwy = 4 + def icon_manager(tool_list: List[str], main_qgs_widget: QObject = None) -> \ Dict[str, Union[Optional[QIcon], Any]]: dirnm = normalize_path(os.path.join(os.path.dirname(__file__), 'icons')) @@ -1244,14 +1298,13 @@ def add_map_layer_to_group( f'Warstwa nieprawidłowa {layer.name()}. Wymagana interwencja.', "GIAP - PolaMap Lite", Qgis.Info) - root = project.layerTreeRoot() if main_group_name and root.findGroup(main_group_name): group = root.findGroup(main_group_name).findGroup(group_name) else: group = root.findGroup(group_name) if not group: if force_create: - group = project.layerTreeRoot().addGroup(group_name) + group = root.addGroup(group_name) else: project.addMapLayer(layer) return @@ -1259,6 +1312,7 @@ def add_map_layer_to_group( if group_name: group.insertLayer(position, layer) + def find_widget_with_menu_in_toolbar(toolbar: QToolBar) -> List[QToolButton]: lista_widgets = toolbar.children() qmenu_list = [] @@ -1268,6 +1322,7 @@ def find_widget_with_menu_in_toolbar(toolbar: QToolBar) -> List[QToolButton]: qmenu_list.append(widget) return qmenu_list + def get_action_from_toolbar(toolbar: QToolBar) -> List[QAction]: lista_widgets = toolbar.children() act_list = [] @@ -1277,9 +1332,10 @@ def get_action_from_toolbar(toolbar: QToolBar) -> List[QAction]: act_list.append(widget.actions()[0]) return act_list + def add_action_from_toolbar(iface: iface, sec, btn: list) -> None: if iface.mainWindow().findChild(QToolBar, btn[0].split('_')[0]): - dlu = len(btn[0].split('_')) + dlu = len(btn[0].split('_')) if dlu == max_ele_nazwy: objname_toolbar, ind, typ, ind_menu = btn[0].split('_') else: @@ -1298,22 +1354,23 @@ def add_action_from_toolbar(iface: iface, sec, btn: list) -> None: if dlu == max_ele_nazwy: if widg.menu(): - #Wyciąganie i dodawanie pojdeynczej akcji z menu + # Wyciąganie i dodawanie pojdeynczej akcji z menu sel_act_from_menu = widg.menu().actions()[int(ind_menu)] objname = sel_act_from_menu.objectName() sel_act_from_menu.setObjectName(btn[0]) sec.add_action(sel_act_from_menu, btn[1], btn[2]) sel_act_from_menu.setObjectName(objname) else: - #Dodawanie wybranej akcji + # Dodawanie wybranej akcji sel_act = widg.actions()[int(ind_menu)] objname = sel_act.objectName() sel_act.setObjectName(btn[0]) sec.add_action(sel_act, btn[1], btn[2]) sel_act.setObjectName(objname) else: - #Dodawanie menu i domyślnej akcji + # Dodawanie menu i domyślnej akcji objname = widg.defaultAction().objectName() widg.defaultAction().setObjectName(btn[0]) - sec.add_action(widg.defaultAction(), btn[1], btn[2], widg.menu()) - widg.defaultAction().setObjectName(objname) \ No newline at end of file + sec.add_action(widg.defaultAction(), btn[1], btn[2], + widg.menu()) + widg.defaultAction().setObjectName(objname)