class UiDataManager(QObject): """ Handle all interaction between the UI and Data Manager. Also stores data as formatted for the UI @param QObject: inherits from QObject Class @type QObject: QObject """ rDataChangedSignal = pyqtSignal() fDataChangedSignal = pyqtSignal() #logging global uilog uilog = Logger.setup(lf='uiLog') def __init__(self, iface, controller): """ Initialise the UI Data Manager @param iface: QgisInterface Abstract base class defining interfaces exposed by QgisApp @type iface: Qgisinterface Object @param controller: instance of the plugins controller @type controller: AimsUI.AimsClient.Gui.Controller """ QObject.__init__(self) self._controller = controller self._iface = iface self.dm = None self._observers = [] self.data = { FEEDS['AF']: {}, FEEDS['AC']: {}, FEEDS['AR']: {}, FEEDS['GC']: {}, FEEDS['GR']: {} } self.groups = ('Replace', 'AddLineage', 'ParcelReferenceData' ) # more to come... self.rDataChangedSignal.connect(self._controller.rDataChanged) self.fDataChangedSignal.connect(self._controller.fDataChanged) def startDM(self): """ Start running the DM observer thread and Listener (of the DM observer) thread when the plugin is enabled """ self.dm = DataManager() # common data obj self.DMData = DMData() dmObserver = DMObserver(self.DMData, self.dm) listener = Listener(self.DMData) self.connect(listener, SIGNAL('dataChanged'), self.dataUpdated) #### listener.start() dmObserver.start() uilog.info('dm started') def killDm(self): """ Close DataManager at plugin unload """ if self.dm: self.dm.close() ### Observer Methods ### def register(self, observer): """ Listeners of the UIDataManager to regiester themselves to this method @param observer: listerning class @type observer: AIMS class objects wishing to listen """ self._observers.append(observer) @pyqtSlot() def dataUpdated(self, data=None, feedType=FEEDS['AR']): """ Slot communicated to when Review data changed. Updates review layer and table data @param data: list of AIMS objects related for feed (as communicated in param feedtype) @type data: list @param feedType: Type of AIMS API feed @type feedType: AIMSDataManager.FeatureFactory.FeedRef """ self.setData(data, feedType) for observer in self._observers: observer.notify(feedType) def exlopdeGroup(self): """ key groups agianst the group id and groups of single features against each group resultant format == {(groupId, groupObj): {addId: addObj, addId: addObj}, (gro...}} """ gDict = {} for gId, gFeats in self.data[FEEDS['GR']].items(): fDict = {} for gFeat in gFeats.meta._entities: fDict[gFeat._changeId] = gFeat gDict[(gId, gFeats)] = fDict self.data[FEEDS['GR']] = gDict def idProperty(self, feedtype): """ Returns the property that the each object should derive its reference id from @return: Reference to which id should be used to reference each feedtype @rtype: string """ if feedtype == FEEDS['AF']: return '_components_addressId' if feedtype == FEEDS['AR']: return '_changeId' if feedtype == FEEDS['GR']: return '_changeGroupId' def keyData(self, listofFeatures, feedtype): """ Key Data from Data Manager @param dataRefresh: list of AIMS objects related for feed (as communicated in param feedtype) @type dataRefresh: list @param feedType: Type of AIMS API feed @type feedType: AIMSDataManager.FeatureFactory.FeedRef """ ### was if listofFeatures: but stop the queue being emptied to None if listofFeatures or listofFeatures == []: li = [] keyId = self.idProperty(feedtype) li = dict((getattr(feat, keyId), feat) for feat in listofFeatures) self.data[feedtype] = li # [GroupKey:{AdKey:}] if feedtype == FEEDS['GR']: # key group objects self.exlopdeGroup() def setData(self, dataRefresh, FeedType): # redundant? now straight to keyData? """ Method receives new data from the data manager via the UIDM observer pattern and then starts the data update process @param dataRefresh: list of AIMS objects related for feed (as communicated in param feedtype) @type dataRefresh: list @param feedType: Type of AIMS API feed @type feedType: AIMSDataManager.FeatureFactory.FeedRef """ self.keyData(dataRefresh, FeedType) def updateRdata(self, respFeature, feedType): """ Between dm threaded interval data deliveries, temp AIMS review objects are created or render irrelevant by user actions. This method updates the main data (self._data) to reflect these changes. @param respFeature: Aims Address object @type respFeature: AIMSDataManager.Address @param feedType: Type of AIMS API feed @type feedType: AIMSDataManager.FeatureFactory.FeedRef """ # remove from data if respFeature._queueStatus in ('Declined', 'Accepted'): del self.data[FEEDS['AR']][respFeature._changeId] else: # add to data self.data[FEEDS['AR']][respFeature._changeId] = respFeature #uilog.info('new AR record with changeid: {}'.format(respFeature._changeId)) self.rDataChangedSignal.emit() def updateFdata(self, respFeature): """ Between dm threaded interval data deliveries, temp AIMS feature objects are created or render irrelevant by user actions. This method updates the main data (self._data) to reflect these changes. @param respFeature: Aims Address object @type respFeature: AIMSDataManager.Address """ # responses do not have a 'full number' # as it is required for labeling it is set here respFeature.setFullAddressNumber(respFeature.getFullNumber()) self.data[FEEDS['AF']][respFeature._components_addressId] = respFeature self.fDataChangedSignal.emit() def updateGdata(self, respFeature): groupKey = self.matchGroupKey(respFeature._changeGroupId) self.data[FEEDS['GR']][groupKey][respFeature._changeId] = respFeature self.rDataChangedSignal.emit() def matchGroupKey(self, groupId): for groupKey in self.data.get(FEEDS['GR']).keys(): if groupId in groupKey: return groupKey def setBbox(self, sw, ne): """ Intermediate method, passes bboxes from layer manager to the DM @param sw: (x ,y) @type sw: tuple @param ne: (x ,y) @type ne: tuple """ #logging uilog.info('*** BBOX *** New bbox passed to dm.setbb') self.dm.setbb(sw, ne) uilog.info('*** BBOX *** Bbox set') def reviewData(self): """ Returns current group review data @return: Dictionary of group formatted review data @rtype: dictionary """ return self.data.get(FEEDS['AR']) def groupReviewData(self): ''' return (single and group) review data ''' return self.data.get(FEEDS['GR']) def combinedReviewData(self): """ De-nests group review data and combines with standard review data, Returning a complete "Review Data" set @return: Flat dictionary of review items @rtype: dictionary """ groupData = self.groupReviewData() addData = self.reviewData() combinedData = {} combinedData.update(addData) if not groupData: return addData for k, v in groupData.items(): combinedData.update(v) return combinedData def featureData(self): """ Returns feature data return: Dict of aims features {featureID: address obj,...} rtype: dictionary """ return self.data.get(FEEDS['AF']) # --- DM convenience methods--- def addAddress(self, feature, respId=None): """ Passes an new AIMS Feature to DataManager to add the feature to the AIM system @param feature: Aims Address object @type feature: AIMSDataManager.Address @param respId: id used to match response @type respId: integer """ uilog.info( 'obj with respId: {0} passed to convenience method "{1}" '.format( respId, 'addAddress')) self.dm.addAddress(feature, respId) def retireAddress(self, feature, respId=None): """ Passes an AIMS Feature to DataManager for retirement @param feature: Aims Address object @type feature: AIMSDataManager.Address @param respId: id used to match response @type respId: integer """ uilog.info( 'obj with respId: {0} passed to convenience method "{1}" '.format( respId, 'retireAddress')) self.dm.retireAddress(feature, respId) def updateAddress(self, feature, respId=None): """ Passes an AIMS Feature to the DataManager to update a published feature @param feature: Aims Address object @type feature: AIMSDataManager.Address @param respId: id used to match response @type respId: integer """ uilog.info( 'obj with respId: {0} passed to convenience method "{1}" '.format( respId, 'updateAddress')) self.dm.updateAddress(feature, respId) def decline(self, feature, feedType, respId=None): """ Passes an AIMS Feature to the DataManager to decline a review item @param feature: Aims Address object @type feature: AIMSDataManager.Address @param feedType: Type of AIMS API feed @type feedType: AIMSDataManager.FeatureFactory.FeedRef @param respId: id used to match response @type respId: integer """ uilog.info( 'obj with respId: {0} passed to convenience method "{1}" '.format( respId, 'declineAddress')) if feedType == FEEDS['AR']: self.dm.declineAddress(feature, respId) else: self.dm.declineGroup(feature, respId) def accept(self, feature, feedType, respId=None): """ Passes an AIMS Feature to the DataManager to accept a review item @param feature: Aims Address object @type feature: AIMSDataManager.Address @param feedType: Type of AIMS API feed @type feedType: AIMSDataManager.FeatureFactory.FeedRef @param respId: id used to match response @type respId: integer """ if respId: uilog.info( 'obj with respId: {0} passed to convenience method "{1}" '. format(respId, 'acceptAddress')) if feedType == FEEDS['AR']: self.dm.acceptAddress(feature, respId) else: self.dm.acceptGroup(feature, respId) def repairAddress(self, feature, respId=None): """ Passes an updated AIMS Review Feature to the DataManager to be updated on the review feed @param feature: Aims Address object @type feature: AIMSDataManager.Address @param feedType: Type of AIMS API feed @type feedType: AIMSDataManager.FeatureFactory.FeedRef @param respId: id used to match response @type respId: integer """ uilog.info( 'obj with respId: {0} passed to convenience method "{1}" '.format( respId, 'repairAddress')) self.dm.repairAddress(feature, respId) def supplementAddress(self, feature, reqid=None): """ Retrieve properties missing on the feature feed from the last relevant resolution feed feature @param feature: Feature feed item that require supplement data @type feature: AIMSDataManager.Address @param respId: id used to match response @type respId: integer """ self.dm.supplementAddress(feature, reqid) #--- Groups DM Methods --- def repairGroup(self, feature, respId=None): """ Passes an updated AIMS Review Feature to the DataManager to be updated on the review feed @param feature: Aims Address object @type feature: AIMSDataManager.Address @param respId: id used to match response @type respId: integer """ uilog.info( 'obj with respId: {0} passed to convenience method "{1}" '.format( respId, 'repairAddress')) self.dm.repairGroup(feature, respId) # Lineage Related - ON HOLD # def openGroup(self): # self.dm.replaceGroup() # # def updateGroup(self): # self.dm.updateGroup() # # def submitGroup(self): # self.dm.submitGroup() # # def closeGroup(self): # self.dm.closeGroup() # # def addGroup(self): # self.dm.addGroup() # # def removeGroup(self): # self.dm.removeGroup() # def response(self, feedtype=None): """ Returns DataMAnager response for a specific feedtype @param feedType: Type of AIMS feed @type feedType: AIMSDataManager.FeatureFactory.FeedRef @return: tuple of AIMSDataManager.Address @rtype: tuple """ return self.dm.response(feedtype) def isNested(self, feat, prop): """ Test if the class object has said nested entity @param feat: Group object @type feat: AIMSDataManager.Address @param prop: address property @type prop: string """ try: return hasattr( getattr(getattr(feat, 'meta'), '_entities')[0], prop) except: return False def nestedEntities(self, feat, prop): """ Returns property from nested object @param feat: Group object @type feat: AIMSDataManager.Address @param prop: address property @type prop: string @return: AIMSDataManager.Address property @rtype: AIMSDataManager.Address property """ return getattr(getattr(getattr(feat, 'meta'), '_entities')[0], prop) def flatEntities(self, feat, prop): """ Returns property from flat object @param feat: Group object @type feat: AIMSDataManager.Address @param prop: address property @type prop: string @return: AIMSDataManager.Address property @rtype: AIMSDataManager.Address property """ att = getattr(feat, prop) return att if att else '' def fullRoad(self, feat, feedtype): """ Compiles a full road name 'label' for the UI @param feat: Group object @type feat: AIMSDataManager.Address @param feedType: Type of AIMS API feed @type feedType: AIMSDataManager.FeatureFactory.FeedRef @return: Individual road components concatenated @rtype: string """ fullRoad = '' for prop in [ '_components_roadPrefix', '_components_roadName', '_components_roadType', '_components_roadSuffix', '_components_waterRoute' ]: addProp = None # Groups have nested entities if feat._changeType in self.groups: if self.isNested(feat, prop): addProp = self.nestedEntities(feat, prop) # retired have nested entities except when the retired # feature is derived from an response object elif feat._changeType == 'Retire': #if self.isNested(feat, prop): if not feat.meta.requestId: if hasattr( getattr(getattr(feat, 'meta'), '_entities')[0], prop): addProp = self.nestedEntities(feat, prop) elif hasattr(feat, prop): addProp = self.flatEntities(feat, prop) # else we have an Add or update of whoms # properties are flat elif hasattr(feat, prop): addProp = self.flatEntities(feat, prop) else: continue if addProp != None: fullRoad += addProp + ' ' return fullRoad.lstrip() def formatGroupTableData(self, obj, groupProperties): """ Returns data formatted for the group table model @param obj: Group object @type obj: AIMSDataManager.Group.GroupResolution @param groupProperties: list of properties required to format table data @type groupProperties: list @return: return list reperesting a group entity. Formatted: ['changeGroupId', 'groupType', 'workflow_sourceOrganisation', 'submitterUserName', 'submittedDate'] @rtype: list """ groupValues = [] for prop in groupProperties: if hasattr(obj, prop): if getattr(obj, prop) != None: groupValues.append(getattr(obj, prop)) #continue else: groupValues.append('') return groupValues def iterFeatProps(self, feat, featProperties, feedtype): """ Iterate over AIMS class objects, Return those relevant to the parent model (that related to the feedtype) @param feat: Address object @type feat: AIMSDataManager.Address @param featProperties: List of properties required to format table data @type featProperties: list @param feedType: Type of AIMS API feed @type feedType: AIMSDataManager.FeatureFactory.FeedRef @return: List of properties as formatted for relevant data model @rtype: list """ fValues = [] fValues.extend([ getattr(feat, '_changeId'), feat.getFullNumber(), self.fullRoad(feat, feedtype) ]) # address and road labels for prop in featProperties: # Groups have nested entities if feat._changeType in self.groups and self.isNested(feat, prop): fValues.append(self.nestedEntities(feat, prop)) # retired have nested entities except when the retired # feature is derived from an response object elif feat._changeType == 'Retire': if self.isNested(feat, prop): fValues.append(self.nestedEntities(feat, prop)) elif hasattr(feat, prop): fValues.append(self.flatEntities(feat, prop)) # else we have an Add or update of whoms # properties are flat elif hasattr(feat, prop): fValues.append(self.flatEntities(feat, prop)) #!= None: else: fValues.append('') return fValues def formatFeatureTableData(self, feat, featProperties, feedtype): """ Returns data formatted for the feature table model @param feature: Aims Address object @type feature: dictionary @param featProperties: List of Address Properties required format Feature Table Data @type featProperties: list @param feedType: Type of AIMS API feed @type feedType: AIMSDataManager.FeatureFactory.FeedRef @return: List representing on row of feature table data @rtype: list """ if feedtype == FEEDS['AR']: return self.iterFeatProps(feat, featProperties, feedtype) else: fValuesList = [] # AR for f in feat.values(): fValues = self.iterFeatProps(f, featProperties, feedtype) fValuesList.append(fValues) return fValuesList def addClassProps(self, feedtype): """ Properties required to format Group and Feature data for the respective table models @param feedType: Type of AIMS API feed @type feedType: AIMSDataManager.FeatureFactory.FeedRef @return: Tuple of properties required to format the data related to the feedtype @rtype: tuple """ prop = { 'AR': { 'kProperties': [ '_changeId', '_changeType', '_workflow_sourceOrganisation', '_workflow_submitterUserName', '_workflow_submittedDate' ], 'vProperties': [ '_components_lifecycle', '_components_townCity', '_components_suburbLocality' ] }, 'GR': { 'kProperties': [ '_changeGroupId', '_groupType', '_workflow_sourceOrganisation', '_submitterUserName', '_submittedDate' ], 'vProperties': [ '_components_lifecycle', '_components_townCity', '_components_suburbLocality' ] } } if feedtype == FEEDS['AR']: return (prop['AR']['kProperties'], prop['AR']['vProperties']) return (prop['GR']['kProperties'], prop['AR']['vProperties']) def formatTableData(self, feedtypes): """ Returns review data as formatted for the review data model @param feedType: Type of AIMS API feed @type feedType: AIMSDataManager.FeatureFactory.FeedRef @return: data formated for feature table model @rtype: dictionary """ fData = {} for feedtype in feedtypes: if self.data[feedtype]: props = self.addClassProps(feedtype) kProperties = props[0] vProperties = props[1] for k, v in self.data.get(feedtype).items(): if feedtype == FEEDS['AR']: if v._queueStatus in ('Declined', 'Accepted'): continue featureValues = [] if feedtype == FEEDS['AR']: groupValues = self.formatGroupTableData(v, kProperties) featureValues = [ self.formatFeatureTableData( v, vProperties, feedtype) ] else: #GR featureValues = [] groupValues = self.formatGroupTableData( k[1], kProperties) featureValues = self.formatFeatureTableData( v, vProperties, feedtype) fData[tuple(groupValues)] = featureValues if fData: return fData else: return {('', '', '', '', ''): [['', '', '', '', '']]} def singleFeatureObj(self, objkey): """ Returns a AIMS Address object that matches the object key @param feedType: Type of AIMS API feed @type feedType: AIMSDataManager.FeatureFactory.FeedRef @param objkey: Feautre id @type objkey: integer """ return self.data.get(FEEDS['AF'])[(objkey)] def singleReviewObj(self, feedtype, objkey): ''' return the value of which is an aims review obj (group and single) for the keyed data ''' if feedtype == FEEDS['AR']: return self.data.get(feedtype).get(objkey) elif feedtype == FEEDS['GR']: for k in self.data.get(feedtype): if objkey == k[0]: return k[1] def currentReviewFeature(self, currentGroup, currentFeatureKey): """ Returns aims feautre object as per supplied data key(s) @param currentGroup: Current group key (groupId, changeType) @type currentGroup: tuple @param currentFeatureKey: Current Feautre id @type currentFeatureKey: integer @return: Address object @rtype: AIMSDataManager.Address """ if currentGroup[1] not in ('Add', 'Update', 'Retire'): for group in self.data.get(FEEDS['GR']).values(): if group.has_key(currentFeatureKey): return group[currentFeatureKey] else: return self.data.get(FEEDS['AR']).get(currentFeatureKey) def reviewItemCoords(self, currentGroup, currentFeatureKey): """ Returns the coords of a review obj @param currentGroup: Current group key (groupId, changeType) @type currentGroup: tuple @param currentFeatureKey: Current Feautre id @type currentFeatureKey: integer @return: list [x,y] @rtype: list """ obj = self.currentReviewFeature(currentGroup, currentFeatureKey) if not obj: return None if obj._changeType not in ('Update', 'Add') and not obj.meta.requestId: pos = obj.meta.entities[0].getAddressPositions( )[0]._position_coordinates else: pos = obj.getAddressPositions()[0]._position_coordinates return pos
class UiUtility(object): """ Where modular UI methods live and are leveraged """ # logging global uilog uilog = Logger.setup(lf='uiLog') # (uiElement, [Address.property, Address/QueueEditorWidget.setter, Address.getter, QueueEditorWidget.getter,]) uiObjMappings = OrderedDict([ ('uAddressType', ['_components_addressType', 'setAddressType', '', 'set']), ('uWarning', ['_warning', 'setWarnings', '_getEntities', '']), ('uNotes', ['_workflow_sourceReason', 'setSourceReason', '', '']), ('ulifeCycle', ['_components_lifecycle', 'setLifecycle', '', '']), ('uLevelType', ['_components_levelType', 'setLevelType', '', '']), ('uLevelValue', ['_components_levelValue', 'setLevelValue', '', '']), ('uUnitType', ['_components_unitType', 'setUnitType', '', '']), ('uUnit', ['_components_unitValue', 'setUnitValue', '', '']), ('uPrefix', ['_components_addressNumberPrefix', 'setAddressNumberPrefix', '', '']), ('uBase', ['_components_addressNumber', 'setAddressNumber', '', '']), ('uAlpha', ['_components_addressNumberSuffix', 'setAddressNumberSuffix', '', '']), ('uHigh', ['_components_addressNumberHigh', 'setAddressNumberHigh', '', '']), ('uExternalAddressIdScheme', [ '_components_externalAddressIdScheme', 'setExternalAddressIdScheme', '', '' ]), ('uExternalAddId', ['_components_externalAddressId', 'setExternalAddressId', '', '']), ('uRclId', ['_components_roadCentrelineId', 'setRoadCentrelineId', '', '']), ('uRoadPrefix', ['_components_roadPrefix', 'setRoadPrefix', '', '']), ('uRoadName', ['_components_roadName', 'setRoadName', '', '']), ('uRoadTypeName', ['_components_roadType', 'setRoadType', '', '']), ('uRoadSuffix', ['_components_roadSuffix', 'setRoadSuffix', '', '']), ('uWaterName', ['_components_waterName', 'setWaterName', '', '']), ('uWaterRouteName', ['_components_waterRoute', 'setWaterRoute', '', '']), ('uObjectType', ['_addressedObject_objectType', 'setAddObjectType', '', '']), ('uObjectName', ['_addressedObject_objectName', 'setAddObjectName', '', '']), #'uPositionType':['_addressedObject_addressPositions[0]','_addressedObject_addressPositions[0].setPositionType',''], ('uExtObjectIdScheme', [ '_addressedObject_externalObjectIdScheme', 'setExternalObjectIdScheme', '', '' ]), ('uExternalObjectId', ['_addressedObject_externalObjectId', 'setExternalObjectId', '', '']), ('uValuationReference', [ '_addressedObject_valuationReference', 'setValuationReference', '', '' ]), ('uCertificateOfTitle', [ '_addressedObject_certificateOfTitle', 'setCertificateOfTitle', '', '' ]), ('uAppellation', ['_addressedObject_appellation', 'setAppellation', '', '']), ('uMblkOverride', ['_codes_meshblock', 'setMeshblock', '', '']) ]) @staticmethod def transform(iface, coords, tgt=4167): """ Ensure point coordinates are in terms of AIMS system spatial reference system (4167) @param iface: QgisInterface Abstract base class defining interfaces exposed by QgisApp @type iface: Qgisinterface Object @param coords: Point @type coords: QgsPoint @param tgt: Srs to transform to @type tgt: integer """ src_crs = iface.mapCanvas().mapSettings().destinationCrs() tgt_crs = QgsCoordinateReferenceSystem() tgt_crs.createFromOgcWmsCrs('EPSG:{}'.format(tgt)) transform = QgsCoordinateTransform(src_crs, tgt_crs) return transform.transform(coords.x(), coords.y()) @staticmethod def setFormCombos(self): """ Set combo boxes to defualt values """ # set from the parent self.uAddressType.addItems(['Road', 'Water']) self.ulifeCycle.addItems(['Current', 'Proposed', 'Retired']) self.uUnitType.addItems([ None, 'Apartment', 'Kiosk', 'Room', 'Shop', 'Suite', 'Villa', 'Flat', 'Unit' ]) self.uLevelType.addItems([None, 'Floor', "Level"]) self.uObjectType.addItems(['Parcel', 'Building']) self.uPositionType.addItems([ 'Unknown', 'Property Centroid', 'Unit Centroid', 'Frontage Centre Set Back', 'Building Centroid', 'Property Access Point Set Back', 'Building Access Point', 'Front Door Access' ]) @staticmethod def formMask(self): """ Mask form input values """ self.uBase.setValidator(QRegExpValidator(QRegExp(r'[0-9]{0,6}'), self)) self.uHigh.setValidator(QRegExpValidator(QRegExp(r'[0-9]{0,6}'), self)) self.uMblkOverride.setValidator( QRegExpValidator(QRegExp(r'[0-9]{0,7}'), self)) self.uAlpha.setValidator( QRegExpValidator(QRegExp(r'^[A-Za-z]{0,3}'), self)) self.uUnit.setValidator( QRegExpValidator(QRegExp(r'[0-9A-Za-z]{0,5}'), self)) self.uPrefix.setValidator( QRegExpValidator(QRegExp(r'[0-9A-Za-z]{0,5}'), self)) self.uLevelValue.setValidator( QRegExpValidator(QRegExp(r'[0-9A-Za-z]{0,7}'), self)) @staticmethod def toUpper(uInput, UiElement): """ Converts lower case to upper case user input information for UI elements that are required to be upper case when submitted to API @param uInput: User input to a UI component @type uInput: string @param UiElement: UI Component @type UiElement: QtGui.QLineEdit | QtGui.QCombo @return: User input cast tp upper case where required @rtype: string """ if UiElement.objectName() in ('uAlpha', 'uUnit', 'uLevelValue', 'uPrefix'): return uInput.upper() else: return uInput @staticmethod def nullEqualsNone(uInput): """ Cast whitespace or 'NULL' to None @rtype: string """ if uInput == '' or uInput == 'NULL': return None else: return uInput @staticmethod def extractFlatProperty(feature, property, getter): """ Return the require property for an object @param feature: Aims Address object @type feature: AIMSDataManager.Address @param property: Required features property @type property: string @param getter: Properties Getter @type getter: string """ if getter: # use getter if getattr(feature, getter)() != 'None': prop = (getattr(feature, getter)()) else: prop = '' else: # go straight for the objects property if unicode(getattr(feature, property)) != 'None': prop = unicode(getattr(feature, property)) else: prop = '' return prop @staticmethod def featureToUi(self, parent=None): """ Populates update form and review editor queue from aims obj """ UiUtility.setEditability(self, parent) UiUtility.clearForm(self) for ui, objProp in UiUtility.uiObjMappings.items(): # Test the UI has the relative UI component if hasattr(self, ui): uiElement = getattr(self, ui) else: continue # Test the object has the required property or a getter # Groups and retired feature properties may be either nested or flat if self.feature._changeType in ( 'Retire', 'Replace', 'AddLineage', 'ParcelReferenceData', 'MeshblockReferenceData' ) and self.feature._changeType not in ('Accepted', 'Declined'): if hasattr(self.feature, objProp[0]) or hasattr( self.feature, objProp[2]): prop = UiUtility.extractFlatProperty( self.feature, objProp[0], objProp[2]) elif hasattr( getattr(getattr(self.feature, 'meta'), '_entities')[0], objProp[0]): prop = getattr( getattr(getattr(self.feature, 'meta'), '_entities')[0], objProp[0]) else: continue # Add and update properties are only flat least the are temp objs else: if hasattr(self.feature, objProp[0]) or hasattr( self.feature, objProp[2]): prop = UiUtility.extractFlatProperty( self.feature, objProp[0], objProp[2]) else: continue # populate relvant UI components if isinstance(uiElement, QLineEdit) or isinstance( uiElement, QLabel): if ui == 'uWarning': warnings = '' for i in prop: if hasattr(i, '_severity'): warnings += i._severity.upper( ) + ': ' + i._description + ('\n' * 2) uiElement.setText(warnings) for i in self.feature.meta.errors: if hasattr(i, '_severity'): warnings += i._severity.upper( ) + ': ' + i._description + ('\n' * 2) uiElement.setText(warnings) # added to handle the resp warnings which differ if self.feature.meta.errors: for k, v in self.feature.meta.errors.iteritems(): if k: for i in v: warnings += k.upper() + ': ' + i + ('\n' * 2) uiElement.setText(warnings) # New MBLK overwrite flag implemented in API elif ui == 'uMblkOverride' and parent == 'update' and self.feature._codes_isMeshblockOverride != True: continue else: uiElement.setText(unicode(prop)) continue elif isinstance(uiElement, QComboBox): uiElement.setCurrentIndex(0) uiElement.setCurrentIndex(QComboBox.findText(uiElement, prop)) if self.feature._changeType not in ( 'Retire') or self.feature.meta.requestId: #Therefore flat posType = self.feature._addressedObject_addressPositions[ 0]._positionType else: posType = self.feature.meta.entities[ 0]._addressedObject_addressPositions[0]._positionType self.uPositionType.setCurrentIndex( QComboBox.findText(self.uPositionType, posType)) @staticmethod def formToObj(self): """ Maps user input from the new and update form as well as queue editor widget to an AIMS object """ if hasattr(self, 'uQueueEditor'): form = self.uQueueEditor else: form = self for uiElement, objProp in UiUtility.uiObjMappings.items(): #user can not mod warnings ... continue if uiElement == 'uWarning': continue # test if the ui widget/ form ... has the ui component if hasattr(form, uiElement): uiElement = getattr(form, uiElement) setter = getattr(self.feature, objProp[1]) if isinstance(uiElement, QLineEdit): setter(UiUtility.toUpper(uiElement.text(), uiElement)) elif isinstance(uiElement, QComboBox): setter(uiElement.currentText()) elif isinstance(uiElement, QPlainTextEdit): setter(uiElement.toPlainText()) self.feature._addressedObject_addressPositions[0].setPositionType( getattr(form, 'uPositionType').currentText()) @staticmethod def clearForm(self): """ Resets and clears data from UI forms """ widgetChildern = self.findChildren(QWidget, QRegExp(r'^u.*')) for child in widgetChildern: child.setEnabled(True) if isinstance(child, QLineEdit) or isinstance(child, QLabel): child.clear() elif isinstance( child, QComboBox) and child.objectName() != 'uAddressType': child.setCurrentIndex(0) elif isinstance(child, QPlainTextEdit): child.clear() @staticmethod def setReadability(self, regMatch, bool=False): """ set the readability / writeabilty of UI Fields @param self: the class that called featureToUi() @type self: ui object @param bool: boolean, True = Writeable @type bool: boolean """ uiElements = self.findChildren(QWidget, QRegExp(regMatch)) for uiElement in uiElements: if uiElement.objectName() == 'uRclId': uiElement.setReadOnly(True) elif isinstance(uiElement, QLineEdit): uiElement.setReadOnly(bool) elif isinstance(uiElement, QComboBox): uiElement.setDisabled(bool) @staticmethod def setEditability(self, parent=None): """ Toggle editable fields depending on the objects Address Type (road or water) @param self: the class that called featureToUi() @type self: ui object """ # Set wrtieability of EditFeature Form if not parent: UiUtility.setReadability(self, r'^u.*', False) elif parent == 'update': UiUtility.setReadability(self, r'uRoad.*|uWaterR.*', True) elif parent == 'rRetire': UiUtility.setReadability(self, r'^u.*', True) return # no elif parent == 'rUpdate' or parent == 'rAdd': UiUtility.setReadability(self, r'^u.*', False) UiUtility.setReadability(self, r'uRoad.*|uWaterR.*', True) for child in self.findChildren(QWidget): child.setEnabled(True) # Toggle between Water and Road Fields if self.uAddressType.currentText() == 'Road': waterChildern = self.findChildren(QWidget, QRegExp(r'uWater.*')) for child in waterChildern: child.clear() child.setDisabled(True) elif self.uAddressType.currentText() == 'Water': roadChildern = self.findChildren(QWidget, QRegExp(r'uRoad.*')) for child in roadChildern: child.clear() child.setDisabled(True) @staticmethod def raiseErrorMesg(iface, mesg): QMessageBox.warning(iface.mainWindow(), "AIMS Warnings", '{0}'.format(mesg)) @staticmethod def formCompleteness(action, form, iface): """ Test the minimum required properties have been set. This is different for New Features and Updated Features @rtype: boolean """ # All Features must have a Base Number if not form.uBase.text(): UiUtility.raiseErrorMesg( iface, 'Please supply a Complete Address Number') return False # Updates must have both an RCL and Road Name if action == 'update': if (form.uAddressType.currentText() == 'Road' and (not UiUtility.nullEqualsNone(form.uRclId.text()) or not UiUtility.nullEqualsNone(form.uRoadName.text()))): UiUtility.raiseErrorMesg( iface, 'An Update must Supply a Road Name and RCL Id') return False elif (form.uAddressType.currentText() == 'Water' and (not UiUtility.nullEqualsNone(form.uRclId.text()) or not UiUtility.nullEqualsNone(form.uWaterRouteName.text()))): UiUtility.raiseErrorMesg( iface, 'An Update must Supply a Water Route Name and RCL Id') return False # An address can not got from <x> to proposed elif form.ulifeCycle.currentText() == 'Proposed': UiUtility.raiseErrorMesg( iface, 'A Feature may not be updated to "Proposed"') return False # For New Features a road Name is required if action == 'add': if form.uAddressType.currentText( ) == 'Road' and not UiUtility.nullEqualsNone( form.uRoadName.text()): UiUtility.raiseErrorMesg(iface, 'Please supply a Road Name') return False elif (form.uAddressType.currentText() == 'Water' and (not UiUtility.nullEqualsNone(form.uWaterName.text()) or not UiUtility.nullEqualsNone(form.uWaterRouteName.text()))): UiUtility.raiseErrorMesg( iface, 'Please supply a Water Route Name and Water Name') return False return True @staticmethod def fullNumChanged(obj, newnumber): """ Splits a full (user inputted) address string into address components @param obj: The UI class that the user is editing @type obj: e.g. AimsUI.AimsClient.Gui.EditFeatureWidget @param newnumber: User input to uFullNum field @type newnumber: string """ # Set address components to None [ i.setText(None) for i in ( [obj.uPrefix, obj.uUnit, obj.uBase, obj.uAlpha, obj.uHigh]) ] # Split full address into components if '-' not in newnumber: p = re.compile( r'^(?P<flat_prefix>[A-Z]+)?(?:\s)?(?P<flat>[0-9]+/\s*|[0-9]+[A-Z]{,2}/\s*)?(?P<base>[0-9]+)(?P<alpha>[A-Z]+)?$' ) m = p.match(newnumber.upper()) try: if m.group('flat_prefix') is not None: obj.uPrefix.setText(m.group('flat_prefix')) if m.group('flat') is not None: obj.uUnit.setText(m.group('flat').strip('/')) if m.group('base') is not None: obj.uBase.setText(m.group('base')) if m.group('alpha') is not None: obj.uAlpha.setText(m.group('alpha')) except: pass #silently. This is an edge case, user must split them self else: p = re.compile( r'^(?P<flat_prefix>[A-Z]+)?(?:\s)?(?P<flat>[0-9]+/\s*|[0-9]+[A-Z]{,2}/\s*)?(?P<base>[0-9]+)(?:-)(?P<high>[0-9]+)(?P<alpha>[A-Z]+)?$' ) m = p.match(newnumber.upper()) try: if m.group('flat_prefix') is not None: obj.uPrefix.setText(m.group('flat_prefix')) if m.group('flat') is not None: obj.uUnit.setText(m.group('flat').strip('/')) if m.group('base') is not None: obj.uBase.setText(m.group('base')) if m.group('high') is not None: obj.uHigh.setText(m.group('high')) if m.group('alpha') is not None: obj.uAlpha.setText(m.group('alpha')) except: pass #silently
class Controller(QObject): ''' Managers all UI Components and Plugins Tools as well as initialisation and dessimnation of Singleton Instances ''' # log global uilog uilog = Logger.setup(lf='uiLog') _instance = None def __init__(self, iface): """ Initialise UI Data Manager and Response Handler @param iface: QgisInterface Abstract base class defining interfaces exposed by QgisApp @type iface: Qgisinterface Object """ QObject.__init__(self) self.iface = iface self._queues = None self._dockWindow = None self._currentMapTool = None self.rclParent = None self.currentRevItem = None self.actions = [] if Controller._instance == None: Controller._instance = self self.uidm = UiDataManager(self.iface, self) self.RespHandler = ResponseHandler(self.iface, self.uidm) self.refLayer = None self.adrlayer = None self.revLayer = None def initGui(self): """ Set up UI within QGIS """ # set srs self._displayCrs = QgsCoordinateReferenceSystem() self._displayCrs.createFromOgcWmsCrs('EPSG:4167') self.iface.mapCanvas().mapSettings().setDestinationCrs( self._displayCrs) # init layerManager self._layerManager = LayerManager(self.iface, self) self._layerManager.registerFunctions() # init Highlighter self.highlighter = FeatureHighlighter(self.iface, self._layerManager, self) # Build an action list from QGIS navigation toolbar actionList = self.iface.mapNavToolToolBar().actions() self.actions = self.iface.mapNavToolToolBar().actions() # Main address editing window self._loadaction = QAction( QIcon(':/plugins/QGIS-AIMS-Plugin/resources/loadaddress.png'), 'QGIS-AIMS-Plugin', self.iface.mainWindow()) self._loadaction.setWhatsThis('Open the QGIS-AIMS-Plugin') self._loadaction.setStatusTip('Open the QGIS-AIMS-Plugin') self._loadaction.triggered.connect(self.loadQueues) self._loadaction.triggered.connect(self.loadLayers) self._loadaction.triggered.connect(self.enableAddressLayer) self._loadaction.triggered.connect(self.startDM) # Create new address tool self._createnewaddressaction = QAction( QIcon(':/plugins/QGIS-AIMS-Plugin/resources/newaddresspoint.png'), 'Create AIMS Feature', self.iface.mainWindow()) self._createnewaddressaction.setWhatsThis('Create AIMS Feature') self._createnewaddressaction.setStatusTip('Create AIMS Feature') self._createnewaddressaction.setEnabled(False) self._createnewaddressaction.setCheckable(True) self._createnewaddressaction.triggered.connect( self.startNewAddressTool) self._createnewaddresstool = CreateNewAddressTool( self.iface, self._layerManager, self) self._createnewaddresstool.setAction(self._createnewaddressaction) self.actions.append(self._createnewaddressaction) # Delete address point self._deladdressaction = QAction( QIcon(':/plugins/QGIS-AIMS-Plugin/resources/deleteaddress.png'), 'Delete AIMS Feature', self.iface.mainWindow()) self._deladdressaction.setWhatsThis('Delete AIMS Feature') self._deladdressaction.setStatusTip('Delete AIMS Feature') self._deladdressaction.setEnabled(False) self._deladdressaction.setCheckable(True) self._deladdressaction.triggered.connect(self.startDelAddressTool) self._deladdtool = DelAddressTool(self.iface, self._layerManager, self) self._deladdtool.setAction(self._deladdressaction) self.actions.append(self._deladdressaction) # Move address self._moveaddressaction = QAction( QIcon(':/plugins/QGIS-AIMS-Plugin/resources/moveaddress.png'), 'Move AIMS Feature(s)', self.iface.mainWindow()) self._moveaddressaction.setWhatsThis('Move AIMS Feature(s)') self._moveaddressaction.setStatusTip('Move AIMS Feature(s)') self._moveaddressaction.setEnabled(False) self._moveaddressaction.setCheckable(True) self._moveaddressaction.triggered.connect(self.startMoveAddressTool) self._moveaddtool = MoveAddressTool(self.iface, self._layerManager, self) self._moveaddtool.setAction(self._moveaddressaction) self.actions.append(self._moveaddressaction) # Update address self._updateaddressaction = QAction( QIcon(':/plugins/QGIS-AIMS-Plugin/resources/updateaddress.png'), 'Update AIMS Feature', self.iface.mainWindow()) self._updateaddressaction.setWhatsThis('Update AIMS Feature') self._updateaddressaction.setStatusTip('Update AIMS Feature') self._updateaddressaction.setEnabled(False) self._updateaddressaction.setCheckable(True) self._updateaddressaction.triggered.connect( self.startUpdateAddressTool) self._updateaddtool = UpdateAddressTool(self.iface, self._layerManager, self) self._updateaddtool.setAction(self._updateaddressaction) self.actions.append(self._updateaddressaction) # RCL tool -- Not a QAction as triggered from many palaces but not the toolbar self._rcltool = GetRcl(self.iface, self._layerManager, self) # UpdateReview Position tool -- Not a QAction as triggered initiated from review queue form self._updateReviewPos = UpdateReviewPosition(self.iface, self._layerManager, self) # Address lineage """ self._lineageaction = QAction(QIcon(':/plugins/QGIS-AIMS-Plugin/resources/lineage.png'), 'Build Lineage Relationships Between Features', self.iface.mainWindow()) self._lineageaction.setWhatsThis('Build Lineage Relationships Between Features') self._lineageaction.setStatusTip('Build Lineage Relationships Between Features') self._lineageaction.setEnabled(False) self._lineageaction.setCheckable(True) self._lineagetool = LineageTool( self.iface, self._layerManager, self) self._lineageaction.triggered.connect(self._lineagetool.setEnabled) self.actions.append(self._lineageaction) """ # Address highlighter self._highlightaction = QAction( QIcon(":/plugins/QGIS-AIMS-Plugin/resources/addresshighlight.png"), "Electoral address highlighter", self.iface.mainWindow()) self._highlightaction.setWhatsThis( "Turn the electoral address highlighter on or off") self._highlightaction.setStatusTip( "Turn the electoral address highlighter on or off") self._highlightaction.setText('Highlightaction') self._highlightaction.setEnabled(False) self._highlightaction.setCheckable(True) self._highlightaction.toggled.connect(self.highlighter.setEnabled) # Add to own toolbar self._toolbar = self.iface.addToolBar('QGIS-AIMS-Plugin') self._toolbar.addAction(self._createnewaddressaction) self._toolbar.addAction(self._deladdressaction) self._toolbar.addAction(self._updateaddressaction) self._toolbar.addAction(self._moveaddressaction) #self._toolbar.addAction(self._lineageaction) self._toolbar.addAction(self._highlightaction) # Add actions to menu self.iface.addToolBarIcon(self._loadaction) self.iface.addPluginToMenu('&QGIS-AIMS-Plugin', self._loadaction) self.iface.addPluginToMenu('&QGIS-AIMS-Plugin', self._createnewaddressaction) self.iface.addPluginToMenu('&QGIS-AIMS-Plugin', self._deladdressaction) self.iface.addPluginToMenu('&QGIS-AIMS-Plugin', self._updateaddressaction) self.iface.addPluginToMenu('&QGIS-AIMS-Plugin', self._moveaddressaction) self.iface.addPluginToMenu('&QGIS-AIMS-Plugin', self._highlightaction) # capture maptool selection changes QObject.connect(self.iface.mapCanvas(), SIGNAL("mapToolSet(QgsMapTool *)"), self.mapToolChanged) # Add actions from QGIS attributes toolbar (handling QWidgetActions) tmpActionList = self.iface.attributesToolBar().actions() for action in tmpActionList: if isinstance(action, QWidgetAction): actionList.extend(action.defaultWidget().actions()) else: actionList.append(action) # ... could add other toolbars' action lists... # Build a group with actions from actionList group = QActionGroup(self.iface.mainWindow()) group.setExclusive(True) for qgisAction in actionList: group.addAction(qgisAction) # Add our own actions for action in self.actions: if action.text() == 'Highlightaction': continue group.addAction(action) # Plugin Management def loadQueues(self): """ Initialise Loading of the queue widgets into QGIS """ queues = self.Queues() if not queues.isVisible(): queues.parent().show() def Queues(self): """ Load of the queue widgets into QGIS @rtype: QtGui.QTabWidget @return: Docked QTabWidget with UI compinets for displaying and editing AIMS features """ if not self._queues: queues = AimsQueueWidget(self.iface.mainWindow()) self._dockWindow = DockWindow(self.iface.mainWindow(), queues, "AimsQueues", "Aims Queues") self._queues = queues self._dockWindow.unloadPlugin.connect(self.unload) return self._queues def startDM(self): """ Start the Data Manager only once the user enables the Plugin """ self.uidm.startDM() def enableAddressLayer(self): """ enable tools that are dependent on the Address Layer only when the address layer exists """ self._deladdressaction.setEnabled(True) self._createnewaddressaction.setEnabled(True) self._moveaddressaction.setEnabled(True) self._updateaddressaction.setEnabled(True) self._highlightaction.setEnabled(True) def loadLayers(self): """ Install map layers """ if not self.refLayer: self.refLayer = self._layerManager.installRefLayers() if not self.adrlayer: self._layerManager.installAimsLayer('adr', 'AIMS Features') if not self.revLayer: self._layerManager.installAimsLayer('rev', 'AIMS Review') self._layerManager.initialiseExtentEvent() def mapToolChanged(self): """ Track the current maptool (excluding rcl tool) to allow for rollback to previous tool when the Rcltool is deactivated """ if (isinstance(self.iface.mapCanvas().mapTool(), GetRcl) == False and isinstance(self.iface.mapCanvas().mapTool(), UpdateReviewPosition) == False): self._currentMapTool = self.iface.mapCanvas().mapTool() #self.highlighter.hideAll() # logging uilog.info('*** TOOL CHANGE *** {0} started'.format( self.iface.mapCanvas().mapTool())) def setPreviousMapTool(self): """ Roll back to the previous maptool """ if self.iface.mapCanvas().mapTool() != self._currentMapTool: self.iface.mapCanvas().setMapTool(self._currentMapTool) def startNewAddressTool(self): """ Enable the 'create new address' map tool """ self.iface.mapCanvas().setMapTool(self._createnewaddresstool) self._createnewaddresstool.setEnabled(True) def startRclTool(self, parent=None): """ Enable the 'get rcl tool' map tool @param parent: Map that enabled the RCL tool. Based on the RCL tools parent, different highlighting of features is performed @type parent: string """ self.rclParent = parent self.iface.mapCanvas().setMapTool(self._rcltool) self._rcltool.setEnabled(True) def startUpdateReviewPosTool(self, revItem=None): """ Enable the 'get update Review position tool' map tool @param revItem: The current Review Item that is assigned to self.currentRevItem @type revItem: AIMSDataManager.Address.AddressResolution() Object """ self.currentRevItem = revItem self.iface.mapCanvas().setMapTool(self._updateReviewPos) self._rcltool.setEnabled(True) def startMoveAddressTool(self): """ Enable the 'move address' map tool """ self.iface.mapCanvas().setMapTool(self._moveaddtool) self._moveaddtool.setEnabled(True) def startUpdateAddressTool(self): """ Enable the "update address" map tool """ self.iface.mapCanvas().setMapTool(self._updateaddtool) self._updateaddtool.setEnabled(True) def startDelAddressTool(self): """ Enable the "delete address" map tool """ self.iface.mapCanvas().setMapTool(self._deladdtool) self._deladdtool.setEnabled(True) ''' def startLineageTool(self): """ Enable the "lineage" map tool """ self.iface.mapCanvas().setMapTool(self._lineagetool) self._deladdtool.setEnabled(True) ''' def unload(self): """ Remove Plugins UI Elements From QGIS """ self._layerManager.disconnectExtentEvent() if self._queues: self._queues.close() self._queues = None self.uidm.killDm() self.iface.mainWindow().removeToolBar(self._toolbar) self.iface.removeToolBarIcon(self._loadaction) self.iface.removePluginMenu('&QGIS-AIMS-Plugin', self._loadaction) self.iface.removePluginMenu('&QGIS-AIMS-Plugin', self._createnewaddressaction) self.iface.removePluginMenu('&QGIS-AIMS-Plugin', self._deladdressaction) self.iface.removePluginMenu('&QGIS-AIMS-Plugin', self._updateaddressaction) self.iface.removePluginMenu('&QGIS-AIMS-Plugin', self._moveaddressaction) #self.iface.removePluginMenu('&QGIS-AIMS-Plugin', self._lineageaction) self.iface.removePluginMenu("&QGIS-AIMS-Plugin'", self._highlightaction) self._layerManager @pyqtSlot() def rDataChanged(self): """ Review data changed, update review layer and table """ self._queues.uResolutionTab.refreshData() self._layerManager.updateReviewLayer() @pyqtSlot() def fDataChanged(self): """ Feature data changed, update review layer and table """ self._layerManager.getAimsFeatures()
class ReviewQueueWidget(Ui_ReviewQueueWidget, QWidget): ''' connects View <--> Proxy <--> Data Model and manage review data''' #logging global uilog uilog = Logger.setup(lf='uiLog') def __init__(self, parent=None, controller=None): QWidget.__init__(self, parent) self.setupUi(self) self.setController(controller) self._iface = self._controller.iface self.highlight = self._controller.highlighter self.uidm = self._controller.uidm self.uidm.register(self) self.reviewData = None self.currentFeatureKey = 0 self.currentAdrCoord = [0, 0] self.feature = None self.currentGroup = (0, 0) #(id, type) self.altSelectionId = () self.comboSelection = [] # Connections self.uDisplayButton.clicked.connect(self.display) self.uUpdateButton.clicked.connect(self.updateFeature) self.uRejectButton.clicked.connect(self.decline) self.uAcceptButton.clicked.connect(self.accept) # Features View self._featureProxyModel = QSortFilterProxyModel() featuresHeader = [ 'Id', 'Full Num', 'Full Road', 'Life Cycle', 'Town', 'Suburb Locality' ] self.featuresTableView = self.uFeaturesTableView self.featureModel = FeatureTableModel(self.reviewData, featuresHeader) self._featureProxyModel.setSourceModel(self.featureModel) self.featuresTableView.setModel(self._featureProxyModel) self.featuresTableView.rowSelected.connect(self.featureSelected) self.featuresTableView.resizeColumnsToContents() self.featuresTableView.setColumnHidden(5, True) self.featuresTableView.selectRow(0) # Group View self._groupProxyModel = QSortFilterProxyModel() self._groupProxyModel.setFilterCaseSensitivity(Qt.CaseInsensitive) self._groupProxyModel.layoutChanged.connect(self.groupSelected) groupHeader = ['Id', 'Change', 'Source Org.', 'Submitter Name', 'Date'] self.groupTableView = self.uGroupTableView self.groupModel = GroupTableModel(self.reviewData, self.featureModel, groupHeader) self._groupProxyModel.setSourceModel(self.groupModel) self.groupTableView.setModel(self._groupProxyModel) self.groupTableView.resizeColumnsToContents() self.groupTableView.rowSelectionChanged.connect(self.groupSelected) # connect combobox_users to view and model self.comboModelUser = QStandardItemModel() self.comboBoxUser.setView(QListView()) self.comboBoxUser.setModel(self.comboModelUser) self.comboBoxUser.view().clicked.connect( self.applyFilter) # combo box checked self.comboBoxUser.view().pressed.connect( self.userFilterChanged) # or more probable, list item clicked self.popUserCombo() def setController(self, controller): """ Access and assign the single instance of the Controller @param controller: instance of the plugins controller @type controller: AimsUI.AimsClient.Gui.Controller """ import Controller if not controller: controller = Controller.instance() self._controller = controller def notify(self, feedType): """ Observer pattern, registered with uidm @param feedType: feed type indicator """ if feedType == FEEDS['AF']: return uilog.info('*** NOTIFY *** Notify A[{}]'.format(feedType)) self.refreshData() def setMarker(self, coords): """ Add a review marker to canvas if the highlight action action is checked @param coords: list [x , y] @type coords: list """ self.highlight.setReview(coords) def refreshData(self): """ Update Review Queue data """ # request new data self.reviewData = self.uidm.formatTableData((FEEDS['GR'], FEEDS['AR'])) self.groupModel.beginResetModel() self.groupModel.refreshData(self.reviewData) self.groupModel.endResetModel() self.featureModel.beginResetModel() self.featureModel.refreshData(self.reviewData) self.featureModel.endResetModel() self.popUserCombo() uilog.info('*** NOTIFY *** Table Data Refreshed') if self.reviewData: self.reinstateSelection() def reinstateSelection(self): """ Select group item based on the last selected or alternative (next) feature in queue """ if self.currentFeatureKey: matchedIndex = self.groupModel.findfield('{}'.format( self.currentGroup[0])) if matchedIndex.isValid() == False: matchedIndex = self.groupModel.findfield('{}'.format( self.altSelectionId)) or 0 row = matchedIndex.row() self.groupModel.setKey(row) if row != -1: self.groupTableView.selectRow( self._groupProxyModel.mapFromSource(matchedIndex).row()) #self.featuresTableView.selectRow(0) self.reinstateFeatSelection() coords = self.uidm.reviewItemCoords(self.currentGroup, self.currentFeatureKey) if coords: self.setMarker(coords) else: self.uQueueEditor.clearForm() def singleReviewObj(self, feedType, objKey): """ Return either single or group review object as per supplied key @param feedType: feed type indicator @type feedType: AIMSDataManager.AimsUtility.FeedRef @param objKey: object reference id @type objKey: integer @return: AIMS Address Feature @rtype: AIMSDataManager.Address """ if objKey: return self.uidm.singleReviewObj(feedType, objKey) def currentReviewFeature(self): """ Returns the current review feature as registered by last review item selection @return: AIMS Address Feature @rtype: AIMSDataManager.Address """ return self.uidm.currentReviewFeature(self.currentGroup, self.currentFeatureKey) def featureSelected(self, row=None): """ Sets the current feature reference when the user selects a feature @param row: The row the user has selected @type row: integer """ if self.currentGroup[0]: fProxyIndex = self.featuresTableView.selectionModel().currentIndex( ) fSourceIndex = self._featureProxyModel.mapToSource(fProxyIndex) self.currentFeatureKey = self.featureModel.tableSelectionMade( fSourceIndex.row()) self.uQueueEditor.currentFeatureToUi(self.currentReviewFeature()) coords = self.uidm.reviewItemCoords(self.currentGroup, self.currentFeatureKey) if coords: self.setMarker(coords) def reinstateFeatSelection(self): """ When data is refreshed, attempt to reinstate the last feature selection """ if self.currentFeatureKey: matchedIndex = self.featureModel.findfield('{}'.format( self.currentFeatureKey)) if matchedIndex.isValid(): self.featuresTableView.selectRow( self._featureProxyModel.mapFromSource(matchedIndex).row()) return self.featuresTableView.selectRow(0) def groupSelected(self, row=None): """ Set reference to current and alternative group records @param row: The row the user has selected @type row: integer """ proxyIndex = self.groupTableView.selectionModel().currentIndex() sourceIndex = self._groupProxyModel.mapToSource(proxyIndex) self.currentGroup = self.groupModel.tableSelectionMade( sourceIndex.row()) altProxyIndex = self.groupTableView.model().index( proxyIndex.row() + 1, 0) if self._groupProxyModel.rowCount() == 0: self.currentGroup = None self.altSelectionId = 0 elif self._groupProxyModel.rowCount() == 1: self.altSelectionId = 0 #self.featuresTableView.selectRow(0) self.reinstateFeatSelection() return elif altProxyIndex.row() == -1: altProxyIndex = self.groupTableView.model().index( proxyIndex.row() - 1, 0) self.altSelectionId = self.groupTableView.model().data(altProxyIndex) self.reinstateFeatSelection() #self.featuresTableView.selectRow(0) def userFilterChanged(self, index): """ Capture the user selection from filtering comboBox @param index: Combobox Index @type index: QModelIndex """ item = self.comboBoxUser.model().itemFromIndex(index) if item.checkState() == Qt.Checked: item.setCheckState(Qt.Unchecked) else: item.setCheckState(Qt.Checked) self.applyFilter(self.comboBoxUser) self.groupSelected() def groupsFilter(self, row, data): """ Apply filter to group data @param data: String of active group filter item separated by vBars @type data: string """ self._groupProxyModel.setFilterKeyColumn(-1) self._groupProxyModel.setFilterRegExp(data) def applyFilter(self, parent): """ Filter Group Table when the comboBoxUser parameters are modified """ self.comboSelection = [] uFilter = '' model = parent.model() for row in range(model.rowCount()): item = model.item(row) if item.checkState() == Qt.Checked: uFilter += '|' + item.text() self.comboSelection.append(item.text()) self.groupsFilter(row, str(uFilter)[1:]) def popUserCombo(self): """ Obtain all unique and active AIMS publisher values """ data = list(set(self.groupModel.getUsers())) data.sort() self.popCombo(data, self.comboModelUser) def popCombo(self, cElements, model): """ Populate the comboBoxUser with unique system users @param cElements: List of Elements to populate combo box @type cElements: list @param model: The ComboBox ItemModel @type model: QtGui.QStandardItemModel """ for i in range(len(cElements)): item = QStandardItem(cElements[i]) item.setCheckable(True) if item.text() in self.comboSelection: item.setCheckState(Qt.Checked) model.setItem(i, item) def updateFeature(self): """ Update the properties of a review queue item """ self.feature = self.currentReviewFeature() if not self.feature: return if self.feature._changeType == 'Retire': UiUtility.raiseErrorMesg(self._iface, 'Retire Items cannot be updated') return if UiUtility.formCompleteness('update', self.uQueueEditor, self._iface): UiUtility.formToObj(self) respId = int(time.time()) self.uidm.repairAddress(self.feature, respId) self._controller.RespHandler.handleResp(respId, FEEDS['AR']) self.feature = None self.uQueueEditor.featureId = 0 def isDuplicateOnRoad(self, reviewObj): """ Hack. Solution to adding duplicates to a road has seen the API down grade duplicates from warning to infos. the requirement is to now raise a warning to the user when creating a duplicate on a road. Unfortunately this can only be caught at this late stage by match the info strings. @param resObj: resolution object that is being accpeted @type feedType: AIMSDataManager.Address. """ info = [] dupOnRoad = 'Address is not Unique on the road object' # Standard API feed if hasattr(reviewObj.meta, '_entities'): info = [ reviewObj.meta._entities[x]._description for x in range(len(reviewObj.meta._entities)) if hasattr(reviewObj.meta._entities[x], '_description') ] # Temp obj created from API response if hasattr(reviewObj.meta, '_errors'): # is dict if populated if type(reviewObj.meta._errors) is dict: info = [ reviewObj.meta._errors['info'][x] for x in range(len(reviewObj.meta._errors['info'])) if reviewObj.meta._errors.has_key('info') ] if dupOnRoad in info: proceed = QMessageBox.question( self._iface.mainWindow(), 'Duplicate Warning', '{} \n \n Do You Want To Proceed and Create / Modify a Duplicate Address' .format(dupOnRoad), QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if proceed == QMessageBox.No: return False return True def reviewResolution(self, action): """ Decline or Accept review item as per the action parameter @param feedType: feed type indicator @type feedType: AIMSDataManager.AimsUtility.FeedRef """ for row in self.groupTableView.selectionModel().selectedRows(): sourceIndex = self._groupProxyModel.mapToSource(row) objRef = () objRef = self.groupModel.getObjRef(sourceIndex) feedType = FEEDS['GR'] if objRef[1] not in ( 'Add', 'Update', 'Retire') else FEEDS['AR'] reviewObj = self.singleReviewObj(feedType, objRef[0]) if reviewObj: respId = int(time.time()) if action == 'accept': if not self.isDuplicateOnRoad(reviewObj): return self.uidm.accept(reviewObj, feedType, respId) elif action == 'decline': self.uidm.decline(reviewObj, feedType, respId) if self._controller.RespHandler.handleResp( respId, feedType, action): self.highlight.hideReview() self.reinstateSelection() def decline(self): """ Decline review item """ self.reviewResolution('decline') def accept(self): """ Accept review item """ self.reviewResolution('accept') def display(self): """ Zoom to Review Items Coordinates """ if self.currentFeatureKey: coords = self.uidm.reviewItemCoords(self.currentGroup, self.currentFeatureKey) if self.currentAdrCoord == coords or not coords: return self.currentAdrCoord = coords buffer = .00100 extents = QgsRectangle(coords[0] - buffer, coords[1] - buffer, coords[0] + buffer, coords[1] + buffer) self._iface.mapCanvas().setExtent(extents) self._iface.mapCanvas().refresh() self.setMarker(coords) @pyqtSlot() def rDataChanged(self): """ Slot communicated to when the UIDataManager Modifies the Review Data """ self._queues.uResolutionTab.refreshData()
class ResponseHandler(object): # logging global uilog uilog = Logger.setup(lf='uiLog') def __init__(self, iface, uidm): self._iface = iface self.uidm = uidm self.updateSuccessful = None self.afar = { ft: AddressFactory.getInstance(FEEDS['AR']) for ft in FeedType.reverse } self.afaf = { ft: AddressFactory.getInstance(FEEDS['AF']) for ft in FeedType.reverse } def updateData(self, respObj, feedType, action): """ Update the UiDataManager's data to reflect the received response from the API @param respObj: AIMS Feature @type respObj: AIMSDataManager.Address.AddressResolution @param feedType: feed type indicator @type feedType: AIMSDataManager.AimsUtility.FeedRef @param action: Either Accept or Decline indicating review actions @type action: QtGui.QWidget() """ if hasattr(respObj, '_changeGroupId') and feedType == FEEDS['AR']: respObj = self.afar[FeedType.RESOLUTIONFEED].cast(respObj) # Hack to allow the supplementing of missing AF data with AR data if action == 'supplement': self.updateSuccessful = respObj return self.uidm.updateGdata(respObj) elif feedType == FEEDS['AC']: respObj = self.afar[FeedType.RESOLUTIONFEED].cast(respObj) self.uidm.updateRdata(respObj, feedType) elif feedType == FEEDS['AR']: # Hack to allow the supplementing of missing AF data with AR data if action == 'supplement': self.updateSuccessful = respObj return self.uidm.updateRdata(respObj, feedType) if respObj._queueStatus == 'Accepted': self.ismeshblockoverride(respObj) respObj = self.afaf[FeedType.FEATURES].cast(respObj) self.uidm.updateFdata(respObj) else: self.updateSuccessful = False return self.updateSuccessful = True return def ismeshblockoverride(self, respObj): """ The derived mblk override flag is not in the resp obj, here we check and create if need be @param respObj: AIMS Feature @type respObj: AIMSDataManager.Address.AddressResolution """ if hasattr(respObj, '_codes_meshblock'): if respObj._codes_meshblock: respObj.setIsMeshblockOverride(True) def displayWarnings(self, warnings): """ Raise warnings to the user @param warnings: Tuple of the warnings that are require to shown to the user @type warnings: tuple """ message = '' for warning in warnings: message += u'\u2022 {}\n'.format(warning) QMessageBox.warning(self._iface.mainWindow(), "Action Rejected", message) def matchResp(self, response, respId, feedType, i, action): """ Compile a list of warnings that are at the "Reject" level. If there are no warnings for the matching respID, proceed to update data @param response: AIMS Feature @type response: AIMSDataManager.Address.AddressResolution @param respId: Int that relates the request and response @type respId: integer @param feedType: feed type indicator @type feedType: AIMSDataManager.AimsUtility.FeedRef @param i: Number iterations taken the responseHandler to receive a response. Logging only @type i: integer @param action: Either Accept or Decline indicating review actions @type action: QtGui.QWidget() """ warnings = [] for resp in response: if resp.meta._requestId == respId: #logging uilog.info( ' *** DATA *** response received from DM for respId: {0} of type: {1} after {2} seconds' .format(respId, feedType, i)) if resp.meta._errors['reject']: self.displayWarnings(resp.meta.errors['reject']) return True # Hack -- failed acceptance but has an accepted status elif resp.meta._errors[ 'warning'] and resp._queueStatus == 'Accepted': self.displayWarnings(resp.meta.errors['warning']) resp.setQueueStatus('Under Review') # Hack -- failed acceptance but has an accepted status elif resp.meta._errors[ 'error'] and resp._queueStatus == 'Accepted': self.displayWarnings(resp.meta.errors['error']) resp.setQueueStatus('Under Review') # else captured resp and no critical warnings # precede to update self._data self.updateData(resp, feedType, action) return True def handleResp(self, respId, feedType, action=None): """ Test for a response in the response queue with the relevant repId''' @param respId: Int that relates the request and response @type respId: integer @param feedType: feed type indicator @type feedType: AIMSDataManager.AimsUtility.FeedRef @param action: Either Accept or Decline indicating review actions @type action: QtGui.QWidget() """ self.updateSuccessful = False for i in range(0, 20): resp = self.uidm.response(feedType) if resp and resp != (None, ): # found and processed response if self.matchResp(resp, respId, feedType, i, action): return self.updateSuccessful else: time.sleep(1) self._iface.messageBar().pushMessage( "Incomplete Response", "Data may not be complete - Please expect a data refresh shortly", level=QgsMessageBar.WARNING) #logging uilog.info( ' *** DATA *** Time Out ({0} seconds): No response received from DM for respId: {1} of feedtype: {2}' .format(i, respId, feedType))
class MoveAddressTool(QgsMapToolIdentify): """ Tool for relocating AIMS Features """ # logging global uilog uilog = Logger.setup(lf='uiLog') def __init__(self, iface, layerManager, controller): """ Intialise Move Address Tool @param iface: QgisInterface Abstract base class defining interfaces exposed by QgisApp @type iface: Qgisinterface Object @param layerManager: Plugins layer manager @type layerManager: AimsUI.LayerManager() @param controller: Instance of the plugins controller @type controller: AimsUI.AimsClient.Gui.Controller() """ QgsMapToolIdentify.__init__(self, iface.mapCanvas()) self._iface = iface self._canvas = iface.mapCanvas() self._layers = layerManager self._controller = controller self.RespHandler = ResponseHandler(self._iface, self._controller.uidm) self.highlighter = self._controller.highlighter self.afc = { ft: AddressFactory.getInstance(FEEDS['AC']) for ft in FeedType.reverse } self.aff = FeatureFactory.getInstance( FeedRef(FeatureType.ADDRESS, FeedType.FEATURES)) self._features = [] self._sb = self._iface.mainWindow().statusBar() self.activate() def activate(self): """ Activate Move Address Tool """ QgsMapTool.activate(self) self._sb.showMessage("Select feature to move") self.cursor = QCursor(Qt.CrossCursor) self.parent().setCursor(self.cursor) def deactivate(self): """ Deactivate Move Address Tool """ self._sb.clearMessage() def setEnabled(self, enabled): """ When Tool related QAction is checked/unchecked Activate / Disable respectively @param enabled: Tool enabled. Boolean value @type enabled: boolean """ self._enabled = enabled if enabled: self.activate() else: self.deactivate() def setMarker(self, coords): """ Place marker on canvas to indicate the feature to be moved @param coords: QgsPoint @type coords: QgsPoint """ self.highlighter.setAddress(coords) def hideMarker(self): """ Remove the marker from the canvas """ self.highlighter.hideAddress() def canvasReleaseEvent(self, mouseEvent): """ Identify the AIMS Feature(s) the user clicked @param mouseEvent: QtGui.QMouseEvent @type mouseEvent: QtGui.QMouseEvent """ self._iface.setActiveLayer(self._layers.addressLayer()) if mouseEvent.button() == Qt.LeftButton: results = self.identify(mouseEvent.x(), mouseEvent.y(), self.ActiveLayer, self.VectorLayer) # Ensure feature list and highlighting is reset self._features = [] self.hideMarker() if len(results) == 0: return elif len(results) == 1: # Highlight feature coords = results[0].mFeature.geometry().asPoint() self.setMarker(coords) # create address object for feature. It is this obj properties that will be passed to API self._features.append( self._controller.uidm.singleFeatureObj( results[0].mFeature.attribute('addressId'))) self._sb.showMessage("Right click for features new location") else: # Stacked points identifiedFeatures = [] coords = results[0].mFeature.geometry().asPoint() self.setMarker(coords) for i in range(0, len(results)): identifiedFeatures.append( dict(fullAddress=results[i].mFeature.attribute( 'fullAddress'), addressId=results[i].mFeature.attribute( 'addressId'))) dlg = MoveAddressDialog(self._iface.mainWindow()) moveFeatures = dlg.selectFeatures(identifiedFeatures) if moveFeatures: # Form a list of Ids as selected by the user moveFeaturesIds = [d['addressId'] for d in moveFeatures] for featureId in moveFeaturesIds: self._features.append( self._controller.uidm.singleFeatureObj(featureId)) self._sb.showMessage( "Right click for features new location") else: self._features = None # Right click for new position if mouseEvent.button() == Qt.RightButton: results = self.identify(mouseEvent.x(), mouseEvent.y(), self.ActiveLayer, self.VectorLayer) if self._features: if len(results) == 0: coords = self.toMapCoordinates( QPoint(mouseEvent.x(), mouseEvent.y())) else: # Snapping. i.e Move to stack coords = results[0].mFeature.geometry().asPoint() # set new coords for all selected features coords = list(UiUtility.transform(self._iface, coords)) for feature in self._features: # Hack to retrieve the properties missing on the # feature feed from the resolution feed < respId = int(time.time()) self._controller.uidm.supplementAddress(feature, respId) feature = self._controller.RespHandler.handleResp( respId, FEEDS['AR'], 'supplement') # /> #feature.type = FEEDS['AF'] # below clone is part of a fix still to be tested clone = feature.clone(feature, self.aff.get()) clone._addressedObject_addressPositions[0].setCoordinates( coords, ) if clone._codes_isMeshblockOverride != True: clone.setMeshblock(None) respId = int(time.time()) clone = self.afc[FeedType.CHANGEFEED].cast(clone) self._controller.uidm.updateAddress(clone, respId) self.RespHandler.handleResp(respId, FEEDS['AC']) self._features = [] self.hideMarker() self._sb.clearMessage()