def _preparePolygonLayer(self, theQgisLayer): """Create a new layer with no intersecting features to self.layer. A helper function to align the polygons to the postprocLayer polygons. If one input polygon is in two or more postprocLayer polygons then it is divided so that each part is within only one of the postprocLayer polygons. this allows to aggregate in postrocessing using centroid in polygon. The function assumes EPSG:4326 but no checks are enforced Args: theQgisLayer of the file to be processed Returns: QgisLayer of the processed file Raises: Any exceptions raised by the InaSAFE library will be propagated. """ # import time # startTime = time.clock() myMessage = m.Message( m.Heading(self.tr('Preclipping input data...')), m.Paragraph(self.tr( 'Modifying %1 to avoid intersections with the aggregation ' 'layer' ).arg(theQgisLayer.name()))) self._sendMessage(myMessage) theLayerFilename = str(theQgisLayer.source()) myPostprocPolygons = self.safeLayer.get_geometry() myPolygonsLayer = safe_read_layer(theLayerFilename) myRemainingPolygons = numpy.array(myPolygonsLayer.get_geometry()) # myRemainingAttributes = numpy.array(myPolygonsLayer.get_data()) myRemainingIndexes = numpy.array(range(len(myRemainingPolygons))) #used for unit tests only self.preprocessedFeatureCount = 0 # FIXME (MB) the intersecting array is used only for debugging and # could be safely removed myIntersectingPolygons = [] myInsidePolygons = [] # FIXME (MB) maybe do raw geos without qgis #select all postproc polygons with no attributes aggregationProvider = self.layer.dataProvider() aggregationProvider.select([]) # copy polygons to a memory layer myQgisMemoryLayer = create_memory_layer(theQgisLayer) polygonsProvider = myQgisMemoryLayer.dataProvider() allPolygonAttrs = polygonsProvider.attributeIndexes() polygonsProvider.select(allPolygonAttrs) myQgisPostprocPoly = QgsFeature() myQgisFeat = QgsFeature() myInsideFeat = QgsFeature() fields = polygonsProvider.fields() myTempdir = temp_dir(sub_dir='preprocess') myOutFilename = unique_filename(suffix='.shp', dir=myTempdir) self.keywordIO.copy_keywords(theQgisLayer, myOutFilename) mySHPWriter = QgsVectorFileWriter(myOutFilename, 'UTF-8', fields, polygonsProvider.geometryType(), polygonsProvider.crs()) if mySHPWriter.hasError(): raise InvalidParameterError(mySHPWriter.errorMessage()) # end FIXME for (myPostprocPolygonIndex, myPostprocPolygon) in enumerate(myPostprocPolygons): LOGGER.debug('PostprocPolygon %s' % myPostprocPolygonIndex) myPolygonsCount = len(myRemainingPolygons) aggregationProvider.featureAtId( myPostprocPolygonIndex, myQgisPostprocPoly, True, []) myQgisPostprocGeom = QgsGeometry(myQgisPostprocPoly.geometry()) # myPostprocPolygon bounding box values A = numpy.array(myPostprocPolygon) minx = miny = sys.maxint maxx = maxy = -minx myPostprocPolygonMinx = min(minx, min(A[:, 0])) myPostprocPolygonMaxx = max(maxx, max(A[:, 0])) myPostprocPolygonMiny = min(miny, min(A[:, 1])) myPostprocPolygonMaxy = max(maxy, max(A[:, 1])) # create an array full of False to store if a BB vertex is inside # or outside the myPostprocPolygon myAreVerticesInside = numpy.zeros(myPolygonsCount * 4, dtype=numpy.bool) # Create Nx2 vector of vertices of bounding boxes myBBVertices = [] # Compute bounding box for each geometry type for myPoly in myRemainingPolygons: minx = miny = sys.maxint maxx = maxy = -minx # Do outer ring only as the BB is outside anyway A = numpy.array(myPoly) minx = min(minx, numpy.min(A[:, 0])) maxx = max(maxx, numpy.max(A[:, 0])) miny = min(miny, numpy.min(A[:, 1])) maxy = max(maxy, numpy.max(A[:, 1])) myBBVertices.extend([(minx, miny), (minx, maxy), (maxx, maxy), (maxx, miny)]) # see if BB vertices are in myPostprocPolygon myBBVertices = numpy.array(myBBVertices) inside, _ = points_in_and_outside_polygon(myBBVertices, myPostprocPolygon) # make True if the vertice was in myPostprocPolygon myAreVerticesInside[inside] = True # myNextIterPolygons has the 0:count indexes # myOutsidePolygons has the mapped to original indexes # and is overwritten at every iteration because we care only of # the outside polygons remaining after the last iteration myNextIterPolygons = [] myOutsidePolygons = [] for i in range(myPolygonsCount): k = i * 4 myMappedIndex = myRemainingIndexes[i] # memory layers counting starts at 1 instead of 0 as in our # indexes myFeatId = myMappedIndex + 1 doIntersection = False # summ the isInside bool for each of the boundingbox vertices # of each poygon. for example True + True + False + True is 3 myPolygonLocation = numpy.sum(myAreVerticesInside[k:k + 4]) if myPolygonLocation == 4: # all vertices are inside -> polygon is inside #ignore this polygon from further analysis myInsidePolygons.append(myMappedIndex) polygonsProvider.featureAtId(myFeatId, myQgisFeat, True, allPolygonAttrs) mySHPWriter.addFeature(myQgisFeat) self.preprocessedFeatureCount += 1 # LOGGER.debug('Polygon %s is fully inside' %myMappedIndex) # tmpWriter.addFeature(myQgisFeat) elif myPolygonLocation == 0: # all vertices are outside # check if the polygon BB is completely outside of the # myPostprocPolygon BB. myPolyMinx = numpy.min(myBBVertices[k:k + 4, 0]) myPolyMaxx = numpy.max(myBBVertices[k:k + 4, 0]) myPolyMiny = numpy.min(myBBVertices[k:k + 4, 1]) myPolyMaxy = numpy.max(myBBVertices[k:k + 4, 1]) # check if myPoly is all E,W,N,S of myPostprocPolygon if ((myPolyMinx > myPostprocPolygonMaxx) or (myPolyMaxx < myPostprocPolygonMinx) or (myPolyMiny > myPostprocPolygonMaxy) or (myPolyMaxy < myPostprocPolygonMiny)): #polygon is surely outside myOutsidePolygons.append(myMappedIndex) # we need this polygon in the next iteration myNextIterPolygons.append(i) else: # polygon might be outside or intersecting. consider # it intersecting so it goes into further analysis doIntersection = True else: # some vertices are outside some inside -> polygon is # intersecting doIntersection = True #intersect using qgis if doIntersection: # LOGGER.debug('Intersecting polygon %s' % myMappedIndex) myIntersectingPolygons.append(myMappedIndex) ok = polygonsProvider.featureAtId(myFeatId, myQgisFeat, True, allPolygonAttrs) if not ok: LOGGER.debug('Couldn\'t fetch feature: %s' % myFeatId) LOGGER.debug([str(error) for error in polygonsProvider.errors()]) myQgisPolyGeom = QgsGeometry(myQgisFeat.geometry()) myAtMap = myQgisFeat.attributeMap() # for (k, attr) in myAtMap.iteritems(): # LOGGER.debug( "%d: %s" % (k, attr.toString())) # make intersection of the myQgisFeat and the postprocPoly # write the inside part to a shp file and the outside part # back to the original QGIS layer try: myIntersec = myQgisPostprocGeom.intersection( myQgisPolyGeom) # if myIntersec is not None: myIntersecGeom = QgsGeometry(myIntersec) #from ftools myUnknownGeomType = 0 if myIntersecGeom.wkbType() == myUnknownGeomType: int_com = myQgisPostprocGeom.combine( myQgisPolyGeom) int_sym = myQgisPostprocGeom.symDifference( myQgisPolyGeom) myIntersecGeom = QgsGeometry( int_com.difference(int_sym)) # LOGGER.debug('wkbType type of intersection: %s' % # myIntersecGeom.wkbType()) polygonTypesList = [QGis.WKBPolygon, QGis.WKBMultiPolygon] if myIntersecGeom.wkbType() in polygonTypesList: myInsideFeat.setGeometry(myIntersecGeom) myInsideFeat.setAttributeMap(myAtMap) mySHPWriter.addFeature(myInsideFeat) self.preprocessedFeatureCount += 1 else: pass # LOGGER.debug('Intersection not a polygon so ' # 'the two polygons either touch ' # 'only or do not intersect. Not ' # 'adding this to the inside list') #Part of the polygon that is outside the postprocpoly myOutside = myQgisPolyGeom.difference(myIntersecGeom) # if myOutside is not None: myOutsideGeom = QgsGeometry(myOutside) if myOutsideGeom.wkbType() in polygonTypesList: # modifiy the original geometry to the part # outside of the postproc polygon polygonsProvider.changeGeometryValues( {myFeatId: myOutsideGeom}) # we need this polygon in the next iteration myOutsidePolygons.append(myMappedIndex) myNextIterPolygons.append(i) except TypeError: LOGGER.debug('ERROR with FID %s', myMappedIndex) # LOGGER.debug('Inside %s' % myInsidePolygons) # LOGGER.debug('Outside %s' % myOutsidePolygons) # LOGGER.debug('Intersec %s' % myIntersectingPolygons) if len(myNextIterPolygons) > 0: #some polygons are still completely outside of the postprocPoly #so go on and reiterate using only these nextIterPolygonsIndex = numpy.array(myNextIterPolygons) myRemainingPolygons = myRemainingPolygons[ nextIterPolygonsIndex] # myRemainingAttributes = myRemainingAttributes[ # nextIterPolygonsIndex] myRemainingIndexes = myRemainingIndexes[nextIterPolygonsIndex] LOGGER.debug('Remaining: %s' % len(myRemainingPolygons)) else: print 'no more polygons to be checked' break # del tmpWriter # here the full polygon set is represented by: # myInsidePolygons + myIntersectingPolygons + myNextIterPolygons # the a polygon intersecting multiple postproc polygons appears # multiple times in the array # noinspection PyUnboundLocalVariable LOGGER.debug('Results:\nInside: %s\nIntersect: %s\nOutside: %s' % ( myInsidePolygons, myIntersectingPolygons, myOutsidePolygons)) #add in- and outside polygons for i in myOutsidePolygons: myFeatId = i + 1 polygonsProvider.featureAtId(myFeatId, myQgisFeat, True, allPolygonAttrs) mySHPWriter.addFeature(myQgisFeat) self.preprocessedFeatureCount += 1 del mySHPWriter # LOGGER.debug('Created: %s' % self.preprocessedFeatureCount) myName = '%s %s' % (theQgisLayer.name(), self.tr('preprocessed')) myOutLayer = QgsVectorLayer(myOutFilename, myName, 'ogr') if not myOutLayer.isValid(): #TODO (MB) use a better exception raise Exception('Invalid qgis Layer') if self.showIntermediateLayers: self.keywordIO.update_keywords(myOutLayer, {'title': myName}) QgsMapLayerRegistry.instance().addMapLayer(myOutLayer) return myOutLayer
def _aggregateVectorImpact(self, theQGISImpactLayer, theSafeImpactLayer): """Performs Aggregation postprocessing step on vector impact layers. Args: myQGISImpactLayer a valid QgsRasterLayer Returns: None """ #TODO (MB) implement line aggregation myAggrFieldMap = {} myAggrFieldIndex = None try: self.targetField = self.keywordIO.read_keywords(theQGISImpactLayer, 'target_field') except KeywordNotFoundError: myMessage = m.Paragraph( self.tr( 'No "target_field" keyword found in the impact layer %1 ' 'keywords. The impact function should define this.').arg( theQGISImpactLayer.name())) LOGGER.debug('Skipping postprocessing due to: %s' % myMessage) self.errorMessage = myMessage return myImpactProvider = theQGISImpactLayer.dataProvider() myTargetFieldIndex = theQGISImpactLayer.fieldNameIndex( self.targetField) #if a feature has no field called if myTargetFieldIndex == -1: myMessage = m.Paragraph( self.tr('No attribute "%1" was found in the attribute table ' 'for layer "%2". The impact function must define this' ' attribute for postprocessing to work.').arg( self.targetField, theQGISImpactLayer.name())) LOGGER.debug('Skipping postprocessing due to: %s' % myMessage) self.errorMessage = myMessage return # start data retreival: fetch no geometry and # 1 attr for each feature myImpactProvider.select([myTargetFieldIndex], QgsRectangle(), False) myTotal = 0 myAggregationProvider = self.layer.dataProvider() self.layer.startEditing() if self.statisticsType == 'class_count': #add the class count fields to the layer myFields = [QgsField('%s_%s' % (f, self.targetField), QtCore.QVariant.String) for f in self.statisticsClasses] myAggregationProvider.addAttributes(myFields) self.layer.commitChanges() myTmpAggrFieldMap = myAggregationProvider.fieldNameMap() for k, v in myTmpAggrFieldMap.iteritems(): myAggrFieldMap[str(k)] = v elif self.statisticsType == 'sum': #add the total field to the layer myAggrField = self._sumFieldName() myAggregationProvider.addAttributes([QgsField( myAggrField, QtCore.QVariant.Int)]) self.layer.commitChanges() myAggrFieldIndex = self.layer.fieldNameIndex( myAggrField) self.layer.startEditing() myImpactGeoms = theSafeImpactLayer.get_geometry() myImpactValues = theSafeImpactLayer.get_data() if not self.aoiMode: myAggregtionUnits = self.safeLayer.get_geometry() if (theSafeImpactLayer.is_point_data or theSafeImpactLayer.is_polygon_data): LOGGER.debug('Doing point in polygon aggregation') myRemainingValues = myImpactValues if theSafeImpactLayer.is_polygon_data: # Using centroids to do polygon in polygon aggregation # this is always ok because # deintersect() took care of splitting # polygons that spawn across multiple postprocessing # polygons. After deintersect() # each impact polygon will never be contained by more than # one aggregation polygon # Calculate points for each polygon myCentroids = [] for myPolygon in myImpactGeoms: if hasattr(myPolygon, 'outer_ring'): outer_ring = myPolygon.outer_ring else: # Assume it is an array outer_ring = myPolygon c = calculate_polygon_centroid(outer_ring) myCentroids.append(c) myRemainingPoints = myCentroids else: #this are already points data myRemainingPoints = myImpactGeoms #iterate over the aggregation units for myPolygonIndex, myPolygon in enumerate(myAggregtionUnits): if hasattr(myPolygon, 'outer_ring'): outer_ring = myPolygon.outer_ring inner_rings = myPolygon.inner_rings else: # Assume it is an array outer_ring = myPolygon inner_rings = None inside, outside = points_in_and_outside_polygon( myRemainingPoints, outer_ring, holes=inner_rings, closed=True, check_input=True) #self.impactLayerAttributes is a list of list of dict #[ # [{...},{...},{...}], # [{...},{...},{...}] #] self.impactLayerAttributes.append([]) if self.statisticsType == 'class_count': myResults = OrderedDict() for myClass in self.statisticsClasses: myResults[myClass] = 0 for i in inside: myKey = myRemainingValues[i][self.targetField] try: myResults[myKey] += 1 except KeyError: myError = ('StatisticsClasses %s does not ' 'include the %s class which was ' 'found in the data. This is a ' 'problem in the %s ' 'statistics_classes definition' % (self.statisticsClasses, myKey, self.getFunctionID())) raise KeyError(myError) self.impactLayerAttributes[myPolygonIndex].append( myRemainingValues[i]) myAttrs = {} for k, v in myResults.iteritems(): myKey = '%s_%s' % (k, self.targetField) #FIXME (MB) remove next line when we get rid of #shape files as internal format myKey = myKey[:10] myAggrFieldIndex = myAggrFieldMap[myKey] myAttrs[myAggrFieldIndex] = QtCore.QVariant(v) elif self.statisticsType == 'sum': #by default summ attributes myTotal = 0 for i in inside: try: myTotal += myRemainingValues[i][ self.targetField] except TypeError: pass #add all attributes to the impactLayerAttributes self.impactLayerAttributes[myPolygonIndex].append( myRemainingValues[i]) myAttrs = {myAggrFieldIndex: QtCore.QVariant(myTotal)} # Add features inside this polygon myFID = myPolygonIndex myAggregationProvider.changeAttributeValues( {myFID: myAttrs}) # make outside points the input to the next iteration # this could maybe be done quicklier using directly numpy # arrays like this: # myRemainingPoints = myRemainingPoints[outside] # myRemainingValues = # [myRemainingValues[i] for i in outside] myTmpPoints = [] myTmpValues = [] for i in outside: myTmpPoints.append(myRemainingPoints[i]) myTmpValues.append(myRemainingValues[i]) myRemainingPoints = myTmpPoints myRemainingValues = myTmpValues # LOGGER.debug('Before: ' + str(len(myRemainingValues))) # LOGGER.debug('After: ' + str(len(myRemainingValues))) # LOGGER.debug('Inside: ' + str(len(inside))) # LOGGER.debug('Outside: ' + str(len(outside))) elif theSafeImpactLayer.is_line_data: LOGGER.debug('Doing line in polygon aggregation') else: myMessage = m.Paragraph( self.tr( 'Aggregation on vector impact layers other than points' ' or polygons not implemented yet not implemented yet.' ' Called on %1').arg(theQGISImpactLayer.name())) LOGGER.debug('Skipping postprocessing due to: %s' % myMessage) self.errorMessage = myMessage self.layer.commitChanges() return else: if self.statisticsType == 'class_count': #loop over all features in impact layer myResults = OrderedDict() for myClass in self.statisticsClasses: myResults[myClass] = 0 self.impactLayerAttributes.append([]) for myImpactValueList in myImpactValues: myKey = myImpactValueList[self.targetField] try: myResults[myKey] += 1 except KeyError: myError = ('StatisticsClasses %s does not ' 'include the %s class which was ' 'found in the data. This is a ' 'problem in the %s ' 'statistics_classes definition' % (self.statisticsClasses, myKey, self.getFunctionID())) raise KeyError(myError) self.impactLayerAttributes[0].append(myImpactValueList) myAttrs = {} for k, v in myResults.iteritems(): myKey = '%s_%s' % (k, self.targetField) #FIXME (MB) remove next line when we get rid of #shape files as internal format myKey = myKey[:10] myAggrFieldIndex = myAggrFieldMap[myKey] myAttrs[myAggrFieldIndex] = QtCore.QVariant(v) elif self.statisticsType == 'sum': #loop over all features in impact layer self.impactLayerAttributes.append([]) for myImpactValueList in myImpactValues: if myImpactValueList[self.targetField] == 'None': myImpactValueList[self.targetField] = None try: myTotal += myImpactValueList[self.targetField] except TypeError: pass self.impactLayerAttributes[0].append(myImpactValueList) myAttrs = {myAggrFieldIndex: QtCore.QVariant(myTotal)} #apply to all area feature myFID = 0 myAggregationProvider.changeAttributeValues({myFID: myAttrs}) self.layer.commitChanges() return