diff --git a/docs/source/images/menu_gis.png b/docs/source/images/menu_gis.png index 95a809ea..d2572c15 100644 Binary files a/docs/source/images/menu_gis.png and b/docs/source/images/menu_gis.png differ diff --git a/docs/source/menus_in_detail/gistools.rst b/docs/source/menus_in_detail/gistools.rst index cde4c52e..e7ef2635 100644 --- a/docs/source/menus_in_detail/gistools.rst +++ b/docs/source/menus_in_detail/gistools.rst @@ -120,16 +120,6 @@ And this is what it looks like :align: center :alt: scenario_comparison_map -Lowest Common denominator -------------------------- - -When using AequilibraE, one of its premisses is that the zones do not overlay each -other. Thus in case of overlaying geometries, we have to fix the geometries before -using them in AequilibraE. - -If we click **GIS > Lowest common denominator** a new window opens and -we can set the layers we want to overlay the geometries to fix them. - Simple TAG ---------- diff --git a/qaequilibrae/modules/gis/__init__.py b/qaequilibrae/modules/gis/__init__.py index 630eafc4..8ff0033c 100644 --- a/qaequilibrae/modules/gis/__init__.py +++ b/qaequilibrae/modules/gis/__init__.py @@ -1,4 +1,3 @@ from .create_bandwidths_dialog import CreateBandwidthsDialog -from .least_common_denominator_dialog import LeastCommonDenominatorDialog from .simple_tag_dialog import SimpleTagDialog from .compare_scenarios_dialog import CompareScenariosDialog diff --git a/qaequilibrae/modules/gis/forms/ui_least_common_denominator.ui b/qaequilibrae/modules/gis/forms/ui_least_common_denominator.ui deleted file mode 100644 index 583f2fb0..00000000 --- a/qaequilibrae/modules/gis/forms/ui_least_common_denominator.ui +++ /dev/null @@ -1,181 +0,0 @@ - - - least_common_denominator - - - - 0 - 0 - 397 - 284 - - - - - 9 - - - - AequilibraE - Least common denominator - - - - - 10 - 10 - 381 - 101 - - - - Layer 1 - - - - - 110 - 60 - 261 - 24 - - - - - - - 110 - 30 - 261 - 24 - - - - - - - 10 - 64 - 121 - 16 - - - - Data field - - - - - - 10 - 34 - 121 - 16 - - - - Layer - - - - - - - 10 - 120 - 381 - 101 - - - - Layer 2 - - - - - 110 - 60 - 261 - 24 - - - - - - - 110 - 30 - 261 - 24 - - - - - - - 10 - 65 - 121 - 16 - - - - Data field - - - - - - 10 - 34 - 121 - 16 - - - - Layer - - - - - - - 20 - 230 - 251 - 23 - - - - 0 - - - - - - 310 - 230 - 75 - 48 - - - - OK - - - - - - 20 - 260 - 211 - 16 - - - - - - - - - - diff --git a/qaequilibrae/modules/gis/least_common_denominator_dialog.py b/qaequilibrae/modules/gis/least_common_denominator_dialog.py deleted file mode 100644 index 21c70a6c..00000000 --- a/qaequilibrae/modules/gis/least_common_denominator_dialog.py +++ /dev/null @@ -1,104 +0,0 @@ -import os -from functools import partial - -import qgis -from qaequilibrae.modules.common_tools.global_parameters import poly_types, multi_poly, point_types -from qgis.PyQt import QtWidgets, uic -from qgis.core import QgsProject -from .least_common_denominator_procedure import LeastCommonDenominatorProcedure -from qaequilibrae.modules.common_tools import get_vector_layer_by_name - -FORM_CLASS, _ = uic.loadUiType(os.path.join(os.path.dirname(__file__), "forms/ui_least_common_denominator.ui")) - - -class LeastCommonDenominatorDialog(QtWidgets.QDialog, FORM_CLASS): - def __init__(self, qgis_project): - QtWidgets.QDialog.__init__(self) - self.iface = qgis_project.iface - self.setupUi(self) - - # The whole software is prepared to deal with all geometry types, but it has only been tested to work with - # polygons, so I am turning the other layer types off - # self.valid_layer_types = point_types + line_types + poly_types + multi_poly + multi_line + multi_point - self.valid_layer_types = poly_types + multi_poly - - self.fromlayer.currentIndexChanged.connect(partial(self.reload_fields, "from")) - self.tolayer.currentIndexChanged.connect(partial(self.reload_fields, "to")) - self.OK.clicked.connect(self.run) - - # We load the node and area layers existing in our canvas - for layer in qgis.utils.iface.mapCanvas().layers(): # We iterate through all layers - if "wkbType" in dir(layer): - if layer.wkbType() in self.valid_layer_types: - self.fromlayer.addItem(layer.name()) - self.tolayer.addItem(layer.name()) - - def reload_fields(self, box): - if box == "from": - self.fromfield.clear() - if self.fromlayer.currentIndex() >= 0: - layer = get_vector_layer_by_name(self.fromlayer.currentText()) # If we have the right layer in hands - for field in layer.fields().toList(): - self.fromfield.addItem(field.name()) - else: - self.tofield.clear() - if self.tolayer.currentIndex() >= 0: - layer = get_vector_layer_by_name(self.tolayer.currentText()) # If we have the right layer in hands - for field in layer.fields().toList(): - self.tofield.addItem(field.name()) - - def run_thread(self): - self.worker_thread.ProgressValue.connect(self.progress_value_from_thread) - self.worker_thread.ProgressMaxValue.connect(self.progress_range_from_thread) - self.worker_thread.ProgressText.connect(self.progress_text_from_thread) - - self.worker_thread.finished_threaded_procedure.connect(self.finished_threaded_procedure) - self.OK.setEnabled(False) - self.worker_thread.start() - self.exec_() - - def progress_range_from_thread(self, value): - self.progressbar.setRange(0, value) - - def progress_text_from_thread(self, value): - self.progress_label.setText(value) - - def progress_value_from_thread(self, value): - self.progressbar.setValue(value) - - def finished_threaded_procedure(self, procedure): - if self.worker_thread.error is None: - QgsProject.instance().addMapLayer(self.worker_thread.result) - else: - qgis.utils.iface.messageBar().pushMessage( - self.tr("Input data not provided correctly"), self.worker_thread.error, level=3 - ) - self.close() - - def run(self): - error = None - if ( - self.fromlayer.currentIndex() < 0 - or self.fromfield.currentIndex() < 0 - or self.tolayer.currentIndex() < 0 - or self.tofield.currentIndex() < 0 - ): - error = self.tr("ComboBox with ilegal value") - - flayer = self.fromlayer.currentText() - ffield = self.fromfield.currentText() - tlayer = self.tolayer.currentText() - tfield = self.tofield.currentText() - - layer1 = get_vector_layer_by_name(self.fromlayer.currentText()).wkbType() - layer2 = get_vector_layer_by_name(self.tolayer.currentText()).wkbType() - if layer1 in point_types and layer2 in point_types: - error = self.tr("It is not sensible to have two point layers for this analysis") - - if error is None: - self.worker_thread = LeastCommonDenominatorProcedure( - qgis.utils.iface.mainWindow(), flayer, tlayer, ffield, tfield - ) - self.run_thread() - else: - qgis.utils.iface.messageBar().pushMessage(self.tr("Input data not provided correctly. "), error, level=3) diff --git a/qaequilibrae/modules/gis/least_common_denominator_procedure.py b/qaequilibrae/modules/gis/least_common_denominator_procedure.py deleted file mode 100644 index d0bbae18..00000000 --- a/qaequilibrae/modules/gis/least_common_denominator_procedure.py +++ /dev/null @@ -1,196 +0,0 @@ -from aequilibrae.utils.worker_thread import WorkerThread - -from qaequilibrae.modules.common_tools import get_vector_layer_by_name -from qaequilibrae.modules.common_tools.global_parameters import ( - multi_line, - multi_poly, - line_types, - point_types, - poly_types, -) -from qaequilibrae.modules.common_tools.global_parameters import multi_point -from qgis.PyQt.QtCore import QVariant -from qgis.PyQt.QtCore import pyqtSignal -from qgis.core import QgsCoordinateReferenceSystem -from qgis.core import QgsCoordinateTransform, QgsSpatialIndex, QgsFeature, QgsGeometry, QgsField, QgsVectorLayer -from qgis.core import QgsProject - - -class LeastCommonDenominatorProcedure(WorkerThread): - ProgressValue = pyqtSignal(object) - ProgressMaxValue = pyqtSignal(object) - ProgressText = pyqtSignal(object) - finished_threaded_procedure = pyqtSignal(object) - - def __init__(self, parentThread, flayer, tlayer, ffield, tfield): - WorkerThread.__init__(self, parentThread) - self.flayer = flayer - self.tlayer = tlayer - self.ffield = ffield - self.tfield = tfield - self.error = None - self.result = None - self.output_type = None - self.transform = None - self.poly_types = poly_types + multi_poly - self.line_types = line_types + multi_line - self.point_types = point_types + multi_point - - def doWork(self): - flayer = self.flayer - tlayer = self.tlayer - ffield = self.ffield - tfield = self.tfield - - self.from_layer = get_vector_layer_by_name(flayer) - self.to_layer = get_vector_layer_by_name(tlayer) - - EPSG1 = QgsCoordinateReferenceSystem(int(self.from_layer.crs().authid().split(":")[1])) - EPSG2 = QgsCoordinateReferenceSystem(int(self.to_layer.crs().authid().split(":")[1])) - if EPSG1 != EPSG2: - self.transform = QgsCoordinateTransform(EPSG1, EPSG2, QgsProject.instance()) - - # FIELDS INDICES - idx = self.from_layer.dataProvider().fieldNameIndex(ffield) - fid = self.to_layer.dataProvider().fieldNameIndex(tfield) - - # We create an spatial self.index to hold all the features of the layer that will receive the data - # And a dictionary that will hold all the features IDs found to intersect with each feature in the spatial index - self.ProgressMaxValue.emit(self.to_layer.dataProvider().featureCount()) - self.ProgressText.emit(self.tr("Building Spatial Index")) - self.ProgressValue.emit(0) - allfeatures = {} - merged = {} - self.index = QgsSpatialIndex() - for i, feature in enumerate(self.to_layer.getFeatures()): - allfeatures[feature.id()] = feature - merged[feature.id()] = feature - self.index.insertFeature(feature) - self.ProgressValue.emit(i) - - self.ProgressText.emit(self.tr("Duplicating Layers")) - self.all_attr = {} - # We create the memory layer that will have the analysis result, which is the lowest common - # denominator of both layers - epsg_code = int(self.to_layer.crs().authid().split(":")[1]) - if self.from_layer.wkbType() in self.poly_types and self.to_layer.wkbType() in self.poly_types: - lcd_layer = QgsVectorLayer(f"MultiPolygon?crs=epsg:{epsg_code}", "output", "memory") - self.output_type = "Poly" - - elif ( - self.from_layer.wkbType() in self.poly_types + self.line_types - and self.to_layer.wkbType() in self.poly_types + self.line_types - ): - lcd_layer = QgsVectorLayer(f"MultiLineString?crs=epsg:{epsg_code}", "output", "memory") - self.output_type = "Line" - else: - lcd_layer = QgsVectorLayer(f"MultiPoint?crs=epsg:{epsg_code}", "output", "memory") - self.output_type = "Point" - - lcdpr = lcd_layer.dataProvider() - lcdpr.addAttributes( - [ - QgsField("Part_ID", QVariant.Int), - QgsField(ffield, self.from_layer.fields().field(idx).type()), - QgsField(tfield, self.to_layer.fields().field(fid).type()), - QgsField("P-" + str(ffield), QVariant.Double), # percentage of the from field - QgsField("P-" + str(tfield), QVariant.Double), - ] - ) # percentage of the to field - lcd_layer.updateFields() - - # PROGRESS BAR - self.ProgressMaxValue.emit(self.from_layer.dataProvider().featureCount()) - self.ProgressText.emit(self.tr("Running Analysis")) - self.ProgressValue.emit(0) - part_id = 1 - features = [] - areas = {} - for fc, feat in enumerate(self.from_layer.getFeatures()): - geom = feat.geometry() - if geom is not None: - if self.transform is not None: - geom = geom.transform(self.transform) - geometry, statf = self.find_geometry(geom) - uncovered, statf = self.find_geometry(geom) - - intersecting = self.index.intersects(geometry.boundingBox()) - # Find all intersecting parts - for f in intersecting: - g = geometry.intersection(allfeatures[f].geometry()) - if g.area() > 0: - feature = QgsFeature() - geo, stati = self.find_geometry(g) - feature.setGeometry(geo) - geo, statt = self.find_geometry(allfeatures[f].geometry()) - perct = stati / statt - percf = stati / statf - areas[f] = statt - feature.setAttributes( - [part_id, feat.attributes()[idx], allfeatures[f].attributes()[fid], percf, perct] - ) - features.append(feature) - - # prepare the data for the non overlapping - if uncovered is not None: - uncovered = uncovered.difference(g) - aux = merged[f].geometry().difference(g) - if aux is not None: - merged[f].setGeometry(aux) - part_id += 1 - - # Find the part that does not intersect anything - if uncovered is not None: - if uncovered.area() > 0: - feature = QgsFeature() - geo, stati = self.find_geometry(uncovered) - feature.setGeometry(geo) - perct = 0 - percf = stati / statf - feature.setAttributes([part_id, feat.attributes()[idx], "", percf, perct]) - features.append(feature) - part_id += 1 - - self.ProgressValue.emit(fc) - self.ProgressText.emit(self.tr("Running Analysis ({}/{})").format(fc, self.from_layer.featureCount())) - - # Find the features on TO that have no correspondence in FROM - for f, feature in merged.items(): - geom = feature.geometry() - if geom.area() > 0: - feature = QgsFeature() - geo, stati = self.find_geometry(geom) - feature.setGeometry(geo) - perct = stati / areas[f] - percf = 0 - feature.setAttributes([part_id, "", allfeatures[f].attributes()[fid], percf, perct]) - features.append(feature) - part_id += 1 - - if features: - _ = lcdpr.addFeatures(features) - self.result = lcd_layer - - self.ProgressValue.emit(self.from_layer.dataProvider().featureCount()) - self.finished_threaded_procedure.emit("procedure") - - def find_geometry(self, g): - if self.output_type == "Poly": - stat = g.area() - if g.isMultipart(): - geometry = QgsGeometry.fromMultiPolygonXY(g.asMultiPolygon()) - else: - geometry = QgsGeometry.fromPolygonXY(g.asPolygon()) - elif self.output_type == "Line": - stat = g.length() - if g.isMultipart(): - geometry = QgsGeometry.fromMultiPolylineXY(g.asMultiPolyLine()) - else: - geometry = QgsGeometry.fromPolyline(g.asPoly()) - else: - stat = 1 - if g.isMultipart(): - geometry = QgsGeometry.fromMultiPointXY(g.asMultiPoint()) - else: - geometry = QgsGeometry.fromPointXY(g.asPoint()) - return geometry, stat diff --git a/qaequilibrae/modules/menu_actions/__init__.py b/qaequilibrae/modules/menu_actions/__init__.py index bc37bd20..88ab9fdf 100644 --- a/qaequilibrae/modules/menu_actions/__init__.py +++ b/qaequilibrae/modules/menu_actions/__init__.py @@ -3,7 +3,6 @@ from .action_distribution import run_distribution_models from .action_edit_parameters import run_change_parameters from .action_gis_desire_lines import run_desire_lines -from .action_gis_lcd import run_lcd from .action_gis_scenario_comparison import run_scenario_comparison from .action_gis_simple_tag import run_tag from .action_gis_stacked_bandwidths import run_stacked_bandwidths diff --git a/qaequilibrae/modules/menu_actions/action_gis_lcd.py b/qaequilibrae/modules/menu_actions/action_gis_lcd.py deleted file mode 100644 index 621b93c3..00000000 --- a/qaequilibrae/modules/menu_actions/action_gis_lcd.py +++ /dev/null @@ -1,6 +0,0 @@ -def run_lcd(qgis_project): - from qaequilibrae.modules.gis import LeastCommonDenominatorDialog - - dlg2 = LeastCommonDenominatorDialog(qgis_project) - dlg2.show() - dlg2.exec_() diff --git a/qaequilibrae/qaequilibrae.py b/qaequilibrae/qaequilibrae.py index 375431d6..a2259b0b 100644 --- a/qaequilibrae/qaequilibrae.py +++ b/qaequilibrae/qaequilibrae.py @@ -21,7 +21,7 @@ from qaequilibrae.message import messages from qaequilibrae.modules.menu_actions import load_matrices, run_add_connectors, run_stacked_bandwidths, run_tag from qaequilibrae.modules.menu_actions import run_add_zones, run_show_project_data -from qaequilibrae.modules.menu_actions import run_desire_lines, run_scenario_comparison, run_lcd, run_import_gtfs +from qaequilibrae.modules.menu_actions import run_desire_lines, run_scenario_comparison, run_import_gtfs from qaequilibrae.modules.menu_actions import ( run_distribution_models, run_tsp, @@ -175,7 +175,6 @@ def __init__(self, iface): self.add_menu_action("GIS", self.tr("Desire Lines"), partial(run_desire_lines, self)) self.add_menu_action("GIS", self.tr("Stacked Bandwidth"), partial(run_stacked_bandwidths, self)) self.add_menu_action("GIS", self.tr("Scenario Comparison"), partial(run_scenario_comparison, self)) - self.add_menu_action("GIS", self.tr("Lowest common denominator"), partial(run_lcd, self)) self.add_menu_action("GIS", self.tr("Simple tag"), partial(run_tag, self)) # # ######################################################################## diff --git a/test/test_qaequilibrae_menu_with_project.py b/test/test_qaequilibrae_menu_with_project.py index e397cfc0..888155d6 100644 --- a/test/test_qaequilibrae_menu_with_project.py +++ b/test/test_qaequilibrae_menu_with_project.py @@ -247,27 +247,13 @@ def handle_trigger(): assert len(messagebar.messages[3]) == 0, "Messagebar should be empty" + str(messagebar.messages) -def test_gis_lowest_common_denominator_menu(ae_with_project, qtbot): - from qaequilibrae.modules.gis import LeastCommonDenominatorDialog - - def handle_trigger(): - check_if_new_active_window_matches_class(qtbot, LeastCommonDenominatorDialog) - - action = ae_with_project.menuActions["GIS"][3] - assert action.text() == "Lowest common denominator", "Wrong text content" - QTimer.singleShot(10, handle_trigger) - action.trigger() - messagebar = ae_with_project.iface.messageBar() - assert len(messagebar.messages[3]) == 0, "Messagebar should be empty" + str(messagebar.messages) - - def test_gis_simple_tag_menu(ae_with_project, qtbot): from qaequilibrae.modules.gis import SimpleTagDialog def handle_trigger(): check_if_new_active_window_matches_class(qtbot, SimpleTagDialog) - action = ae_with_project.menuActions["GIS"][4] + action = ae_with_project.menuActions["GIS"][3] assert action.text() == "Simple tag", "Wrong text content" QTimer.singleShot(10, handle_trigger) action.trigger() diff --git a/test/test_qaequilibrae_menu_without_project.py b/test/test_qaequilibrae_menu_without_project.py index a6c97da3..0f8510bd 100644 --- a/test/test_qaequilibrae_menu_without_project.py +++ b/test/test_qaequilibrae_menu_without_project.py @@ -206,25 +206,13 @@ def test_gis_scenario_comparison_menu(ae, qtbot): assert messagebar.messages[3][-1] == "Error:You need to load a project first", "Level 3 error message is missing" -def test_gis_lowest_common_denominator_menu(ae, qtbot): - from qaequilibrae.modules.gis import LeastCommonDenominatorDialog - - def handle_trigger(): - check_if_new_active_window_matches_class(qtbot, LeastCommonDenominatorDialog) - - action = ae.menuActions["GIS"][3] - assert action.text() == "Lowest common denominator", "Wrong text content" - QTimer.singleShot(10, handle_trigger) - action.trigger() - - def test_gis_simple_tag_menu(ae, qtbot): from qaequilibrae.modules.gis import SimpleTagDialog def handle_trigger(): check_if_new_active_window_matches_class(qtbot, SimpleTagDialog) - action = ae.menuActions["GIS"][4] + action = ae.menuActions["GIS"][3] assert action.text() == "Simple tag", "Wrong text content" QTimer.singleShot(10, handle_trigger) action.trigger()