def test_siteDownstreamOnSegment(self): streamGraph = StreamGraph() node1 = streamGraph.addNode((0, 0)) node2 = streamGraph.addNode((0, -1)) segment = streamGraph.addSegment(node1, node2, "1", 1, 1, 1) streamGraph.addSite("site1", "1", 0.1) streamGraph.addSite("site2", "1", 0.9) streamGraph.addSite("site3", "1", 1) navigator = StreamGraphNavigator(streamGraph) downstreamSite = navigator.getNextDownstreamSite(segment, 0.5) foundSiteID = downstreamSite[0] foundSiteDist = downstreamSite[1] self.assertEqual(foundSiteID, "site2") self.assertEqual(foundSiteDist, 0.4)
def test_findUpstreamSiteWithBacktrack(self): streamGraph = StreamGraph() node1 = streamGraph.addNode((0, 0)) node2 = streamGraph.addNode((0, -1)) node3 = streamGraph.addNode((1, 0)) segment1 = streamGraph.addSegment(node1, node2, "1", 1, 2, 1) #trib of segment2 path segment2 = streamGraph.addSegment(node3, node2, "2", 1, 1, 1) streamGraph.addSite("site1", "2", 0.2) dataBoundary = DataBoundary(point=(0, 0), radius=10) streamGraph.safeDataBoundary.append(dataBoundary) navigator = StreamGraphNavigator(streamGraph) downstreamSite = navigator.getNextUpstreamSite(segment1, 0.5) foundSiteID = downstreamSite[0] foundSiteDist = downstreamSite[1] self.assertEqual(foundSiteID, "site1") self.assertEqual(foundSiteDist, 1.3)
def getSiteSnapAssignment (graph): #a copy of the current graph used to try different possible snap operations testingGraph = graph#copy.deepcopy(graph)#.clone() testingGraphNavigator = StreamGraphNavigator(testingGraph) allSnaps = [] for snaps in graph.siteSnaps.values(): allSnaps.extend(snaps) #assign all possible snaps of each site to the graph testingGraph.refreshSiteSnaps(allSnaps) assignments = [] allRankedChoices = {} #for each site, holds a list of other sites that siteConflicts = set() def addAssignment (siteAssignment): alreadyContainedAssignment = False for i, assignment in enumerate(assignments): #if we find a match if assignment.siteID == siteAssignment.siteID: #and the newly added assignment is better than the original if siteAssignment.snapDist < assignment.snapDist: #then replace assignments[i] = siteAssignment elif __debug__: print("tried to add a second assignment") #at this point, we've either replaced, or not since our current assignment is worse return #if we reach this line then we don't have an assignment for this ID yet. Add one assignments.append(siteAssignment) def getSiteIndexRange (siteID, sites): firstIndex = -1 lastIndex = -1 for i, site in enumerate(sites): if site.siteID == siteID: if firstIndex == -1: firstIndex = i lastIndex = i return (firstIndex, lastIndex) def getBestRankedChoice (rankedChoices): minOrderError = sys.maxsize bestScoreChoice = None #find the choice that minimize ordering error for choice in rankedChoices: orderError = choice[1] distanceScore = choice[2] nameMatch = choice[4] #if we find a better order error, always choose this option if orderError < minOrderError: bestScoreChoice = choice minOrderError = orderError elif orderError == minOrderError: #if we find an equal order error but smaller dist score choice, choose it bestDistScore = bestScoreChoice[2] bestScoreNameMatch = bestScoreChoice[4] # if this dist is better than previous # AND either this choice is a name match or this isn't and the previous best isn't if distanceScore < bestDistScore and (nameMatch or (not nameMatch and not bestScoreNameMatch)): bestScoreChoice = choice return bestScoreChoice sinks = graph.getSinks() for sink in sinks: upstreamPaths = sink.getUpstreamNeighbors() for path in upstreamPaths: upstreamSitesInfo = testingGraphNavigator.collectSortedUpstreamSites(path, path.length, siteLimit = sys.maxsize, autoExpand = False)[0] #trim the extra distance info off of the results. Not needed upstreamSites = [siteInfo[0] for siteInfo in upstreamSitesInfo] siteIndexRanges = {} for site in upstreamSites: siteID = site.siteID if siteID not in siteIndexRanges: firstOccuranceIdx, lastOccuranceIdx = getSiteIndexRange(siteID, upstreamSites) siteIndexRanges[siteID] = (firstOccuranceIdx, lastOccuranceIdx) #count all unique sites found on this branch. List them in order of appearance uniqueOrderedIDs = [] for i, site in enumerate(upstreamSites): siteID = site.siteID if siteID not in uniqueOrderedIDs: uniqueOrderedIDs.append(siteID) uniqueOrderedIDs = sorted(uniqueOrderedIDs, key=lambda site: int(Helpers.getFullID(site)), reverse=True) #list of sites that have already been chosen on this branch resolvedSites = dict() for orderedIdx, siteID in enumerate(uniqueOrderedIDs): firstOccuranceIdx, lastOccuranceIdx = siteIndexRanges[siteID] #get a list of possible assignments for this ID #Here, an choice is a tuple (assignment, index) siteChoices = [] #Here, a ranked choice is a tuple (assignment, orderError, distScore) rankedChoices = [] for i in range(firstOccuranceIdx, lastOccuranceIdx+1): if upstreamSites[i].siteID == siteID: siteChoices.append((upstreamSites[i], i)) for choice in siteChoices: assignment = choice[0] upstreamSitesIdx = choice[1] #the index of this site in the list 'upstreamSites' orderError = 0 distanceScore = 0 nameMatch = assignment.nameMatch #siteIDs that this assignment will force an out of sequence snap for orderConflicts = [] #calculate the order error for this choice for i in range(0, len(uniqueOrderedIDs)): cmpSiteID = uniqueOrderedIDs[i] #the case when we are comparing to a site thats been resolved #is different than the case when a site we compare to is unresolved #if the cmp site is unresolved, we are looking to see if this site's choice #will force an order error for site that has yet to be chosen #if the cmp site is resolved, we are looking to see if this site's choice #conflicts with the choice ALREADY made for the cmp site if cmpSiteID in resolvedSites: #the third elem in the tuple is the ranked choice's upstream sites index resolvedCmpUpstreamSitesIdx = resolvedSites[cmpSiteID][3] #if this cmp site is resolved it must be a larger ID than us because #sites are resolved in decending order of their IDs if upstreamSitesIdx < resolvedCmpUpstreamSitesIdx: orderError += 1 orderConflicts.append(cmpSiteID) else: cmpFirstOccuranceIdx, cmpLastOccuranceIdx = siteIndexRanges[cmpSiteID] compare = Helpers.siteIDCompare(assignment.siteID, cmpSiteID) #moving forward, if I choose this choice, will I cut off all the assignments for any remaining sites? if cmpLastOccuranceIdx < upstreamSitesIdx and compare > 0: # by choosing this choice, I'm stranding the the last snap choice # of a site with a lower ID than us downstream from us. orderError += 1 orderConflicts.append(cmpSiteID) if cmpFirstOccuranceIdx > upstreamSitesIdx and compare < 0: # by choosing this choice, I'm stranding all of the snap options # for cmpSite upstream from our current choice even though # cmpSiteID is higher than us orderError += 1 orderConflicts.append(cmpSiteID) #get list of sites involved in the outcome of this sites snap choice #this is all site IDs that have a snap choice that appears between the first instance of the #current site id and the last instance in the traversal involvedSites = set() for i in range(firstOccuranceIdx+1, lastOccuranceIdx): if upstreamSites[i].siteID != siteID and upstreamSites[i].siteID not in resolvedSites: involvedSites.add(upstreamSites[i].siteID) #for all sites that are 'involved' (appear between the first and last occurance index of the current site), #find the best nearest possible distance allowed if we choose this assignment minDistOfInvolved = {} # by starting this loop at the index of the choice, # we won't get snap options of this involved site that occur before the index of the current # choice. This is because if we choose this choice, anything before it on the traversal can't be chosen anymore # if there are no instances of an involved site that occur after this choice, it won't be counted # But, then that should trigger an increase in order error. # since order error is taken as higher priority than distance, the fact we don't # count up the distance for the missing site shouldn't be an issue for i in range(upstreamSitesIdx, len(upstreamSites)): involvedID = upstreamSites[i].siteID #check if this site is truely an involved site if involvedID in involvedSites: #if this site is not the same as the one we are trying to assign: if involvedID in minDistOfInvolved: minDistOfInvolved[involvedID] = min(minDistOfInvolved[involvedID], upstreamSites[i].snapDist) else: minDistOfInvolved[involvedID] = upstreamSites[i].snapDist # the total snap distance must be inceased by the snapDist of this choice distanceScore += assignment.snapDist # and it is increased at MINIMUM by the best choices remaining for other involved sites for minDist in minDistOfInvolved.values(): distanceScore += minDist rankedChoices.append((assignment, orderError, distanceScore, upstreamSitesIdx, nameMatch, orderConflicts)) bestScoreChoice = getBestRankedChoice(rankedChoices) resolvedSites[siteID] = bestScoreChoice if siteID in allRankedChoices: #catch case when a site gets snapped onto two networks #later on we choose which network has the best fit allRankedChoices[siteID].append(bestScoreChoice) else: allRankedChoices[siteID] = [bestScoreChoice] #choose an assignment from the best picked ranked choices #in almost all cases, there will only be one ranked choice to choose from #there will only be two if the site had possible snaps on networks with different sinks for choices in allRankedChoices.values(): bestRankedChoice = getBestRankedChoice(choices) assignment = bestRankedChoice[0] addAssignment(assignment) # for each conflict forced by this choice, add a conflict to the total list going # in both directions (a conflicts with b AND b conflicts with a) for conflictingSite in bestRankedChoice[5]: conflictingCmp = Helpers.siteIDCompare(conflictingSite, assignment.siteID) #make sure we put the larger ID first so that if this pair appears again we don't add it again (bc we use a set) if conflictingCmp > 0: siteConflicts.add((conflictingSite, assignment.siteID)) else: siteConflicts.add((assignment.siteID, conflictingSite)) if bestRankedChoice[1] > 0 and __debug__: print("adding " + assignment.siteID + " with " + str(bestRankedChoice[1]) + " order error:") for conflictingSite in bestRankedChoice[5]: print("\t conflicts with " + conflictingSite) #verify that all site IDs are accounted for #this code should never really have to run accountedForSiteIDs = set() for assignment in assignments: accountedForSiteIDs.add(assignment.siteID) for siteID in graph.siteSnaps: if siteID not in accountedForSiteIDs: if __debug__: print("missing site! adding in post: " + str(siteID)) #add the most likely snap for this site assignments.append(graph.siteSnaps[siteID][0]) #keep track of which sites we think are causing the conflicts atFaultSites = [] atFaultPairs = [] #store all sites that may be involved in a conflict allImplicatedSites = set() while len(siteConflicts) > 0: #count which sites appear in the most number of conflicts siteConflictCounts = dict((siteID, 0) for siteID in graph.siteSnaps) mostConflicts = 0 mostConflictingSite = None for conflict in siteConflicts: #a conflict is between two sites conflictA = conflict[0] conflictB = conflict[1] siteConflictCounts[conflictA] += 1 siteConflictCounts[conflictB] += 1 if siteConflictCounts[conflictA] > mostConflicts: mostConflicts = siteConflictCounts[conflictA] mostConflictingSite = conflictA if siteConflictCounts[conflictB] > mostConflicts: mostConflicts = siteConflictCounts[conflictB] mostConflictingSite = conflictB #catch cases when sites conflict with eachother equally and fixing either would remove issues if mostConflicts == 1: #find the conflict pair that caused this conflict for conflict in siteConflicts: conflictA = conflict[0] conflictB = conflict[1] if conflictA == mostConflictingSite or conflictB == mostConflictingSite: atFaultPairs.append((conflictA, conflictB)) allImplicatedSites.add(conflictA) allImplicatedSites.add(conflictB) break else: #remove this conflict and keep track of it as a problem site atFaultSites.append((mostConflictingSite, mostConflicts)) allImplicatedSites.add(mostConflictingSite) siteConflictsCpy = siteConflicts.copy() for conflict in siteConflictsCpy: #a conflict is between two sites conflictA = conflict[0] conflictB = conflict[1] if conflictA == mostConflictingSite or conflictB == mostConflictingSite: siteConflicts.remove(conflict) #reset warnings in this catagory so they don't build up warnings = [] assignmentWarnings = [] for faultySite in atFaultSites: faultySiteID = faultySite[0] faultySiteConflictCount = faultySite[1] message = str(faultySiteID) + " conflicts with " + str(faultySiteConflictCount) + " other sites. Consider changing this site's ID" warnings.append(WarningLog.Warning(priority=WarningLog.MED_PRIORITY, message=message)) for faultyPair in atFaultPairs: pairA = str(faultyPair[0]) pairB = str(faultyPair[1]) message = pairA + " conflicts with " + pairB + ". Consider changing the site ID of one of these two sites" warnings.append(WarningLog.Warning(priority=WarningLog.MED_PRIORITY, message=message)) #finally, assign any warning to the site itself for assignment in assignments: assignmentID = assignment.siteID if assignmentID in allImplicatedSites: message = str(assignmentID) + " is involved in a site conflict. See story/medium priority warnings for conflict details." warning = WarningLog.Warning(WarningLog.HIGH_PRIORITY, message) assignment.assignmentWarnings.clear() assignment.assignmentWarnings.append(warning) return (assignments, warnings)
def getSiteNameContext(self): """ Build up a dict containing contextual information to generate site names. The finished context object is stored in the SiteInfoCreator instance. """ lat = self.lat lng = self.lng streamGraph = self.streamGraph baseData = self.baseData if Failures.isFailureCode(baseData): return baseData if self.context is not None: return self.context context = {} point = (lng, lat) snapablePoint = SnapablePoint(point=point, name="", id="") snapInfo = snapPoint(snapablePoint, baseData, snapCutoff=1) #get the most likely snap if Failures.isFailureCode(snapInfo): return snapInfo feature = snapInfo[0].feature segmentID = str(feature["properties"]["OBJECTID"]) distAlongSegment = snapInfo[0].distAlongFeature #get the segment ID of the snapped segment graphSegment = streamGraph.getCleanedSegment(segmentID) navigator = StreamGraphNavigator(streamGraph) downstreamSegment = navigator.findNextLowerStreamLevelPath( graphSegment, downStreamPositionOnSegment=distAlongSegment, expand=False) streamName = graphSegment.streamName if streamName == "": if not Failures.isFailureCode( downstreamSegment ) and downstreamSegment[0].streamName != "": context["streamName"] = downstreamSegment[ 0].streamName + " tributary" else: context["streamName"] = "(INSERT STREAM NAME)" else: context["streamName"] = streamName placeInfo = GDALData.getNearestPlace(lat, lng) if Failures.isFailureCode(placeInfo): context["distanceToPlace"] = -1 context["state"] = "unknown" context["placeName"] = "unknown" else: context["distanceToPlace"] = placeInfo["distanceToPlace"] context["state"] = placeInfo["state"] context["placeName"] = placeInfo["placeName"] context["lat"] = lat context["long"] = lng contextualPlaces = [] bridges = GDALData.getNearestBridges(lat, lng) namedTribMouths = navigator.getNamedTribMouths() if not Failures.isFailureCode(bridges): contextualPlaces.extend([{ "name": contextBridge.name, "point": contextBridge.point, "distance": contextBridge.distance } for contextBridge in bridges]) if not Failures.isFailureCode(namedTribMouths): contextualPlaces.extend([{ "name": mouth[0], "point": mouth[1], "distance": Helpers.degDistance(mouth[1][0], mouth[1][1], lng, lat) } for mouth in namedTribMouths]) context["contextualPlaces"] = contextualPlaces upstreamDistance = graphSegment.arbolateSum - (graphSegment.length - distAlongSegment) #check if we are at a stream mouth upstreamDistMiles = Helpers.metersToMiles(upstreamDistance * 1000) if upstreamDistMiles < 1: context["source"] = "at source" elif upstreamDistMiles < 3: context["source"] = "near source" else: context["source"] = "" if Failures.isFailureCode(downstreamSegment): context["mouth"] = "" else: downstreamDistMiles = Helpers.metersToMiles(downstreamSegment[1] * 1000) #make sure that the mouth distance is less than upstream dist #before assigning descriptor. Otherwise, we could have near mouth and near source as option #on the same site if downstreamDistMiles > upstreamDistMiles: context["mouth"] = "" else: #likewise, if downstream is closer, don't use "at source" type descriptors context["source"] = "" if downstreamDistMiles < 1: context["mouth"] = "at mouth" elif downstreamDistMiles < 3: context["mouth"] = "near mouth" else: context["mouth"] = "" self.context = context return context
def getSiteID(self, useBadSites=True, logWarnings=False): """ Get the siteID. Lat and Lng are provided to the constructor. :param useBadSites: When false, any site that has warnings associated with it will be ignored in calculating the new ID. :param logWarnings: Should this request log warnings into the SiteInfoCreator's WarningLog instance? :return: A dict {"id": the_id, "story": the_story}""" lat = self.lat lng = self.lng warningLog = self.warningLog streamGraph = self.streamGraph siteIDManager = self.siteIDManager streamGraph.setAssignBadSitesStatus(useBadSites) #typically lat/long are switched to fit the x/y order paradigm point = (lng, lat) story = "" newID = "" huc = "" #create the json that gets resturned def getResults(siteID="unknown", story="See warning log", failed=False): results = dict() results["id"] = Helpers.formatID(siteID) snapLatFormatted = Helpers.getFloatTruncated(lat, 7) snapLngFormatted = Helpers.getFloatTruncated(lng, 7) storyHeader = "Requested site info at " + str( snapLatFormatted) + ", " + str(snapLngFormatted) + ". " useBadSitesStory = ( "" if useBadSites else "ADONNIS ignored sites with incorrect ID's when calculating the new ID. " ) results["story"] = storyHeader + useBadSitesStory + story return results if Failures.isFailureCode(self.baseData): return getResults(failed=True) #snap query point to a segment snapablePoint = SnapablePoint(point=point, name="", id="") snapInfo = snapPoint(snapablePoint, self.baseData, snapCutoff=1) #get the most likely snap if Failures.isFailureCode(snapInfo): if __debug__: print("could not snap") if logWarnings: warningLog.addWarning(WarningLog.HIGH_PRIORITY, snapInfo) return getResults(failed=True) feature = snapInfo[0].feature segmentID = str(feature["properties"]["OBJECTID"]) distAlongSegment = snapInfo[0].distAlongFeature #get the segment ID of the snapped segment graphSegment = streamGraph.getCleanedSegment(segmentID) snappedPoint = streamGraph.segments[segmentID].getPointOnSegment( distAlongSegment) if __debug__: SnapSites.visualize(self.baseData, []) streamGraph.visualize(customPoints=[snappedPoint], showSegInfo=True) streamGraph.visualize(customPoints=[snappedPoint], showSegInfo=False) #build a navigator object #we want to terminate the search each time a query happens #this allows us to stagger upstream and downstream searches #although this means repeating parts of the search multiple times, searching a already constructed #graph takes practically no time at all navigator = StreamGraphNavigator(streamGraph, terminateSearchOnQuery=True) upstreamSite = None downstreamSite = None endOfUpstreamNetwork = False endOfDownstreamNetwork = False secondaryQueries = 0 primaryQueries = 0 #each iteration extends the graph by one query worth of data # in this step we try to find an upstream and downstream site while ( upstreamSite is None or downstreamSite is None ) and secondaryQueries < MAX_SECONDARY_SITE_QUERIES and primaryQueries < MAX_PRIMARY_QUERIES and ( endOfUpstreamNetwork is False or endOfDownstreamNetwork is False): if upstreamSite is None and endOfUpstreamNetwork is False: #we haven't found upstream yet upstreamReturn = navigator.getNextUpstreamSite( graphSegment, distAlongSegment) if upstreamReturn == Failures.END_OF_BASIN_CODE: endOfUpstreamNetwork = True if Failures.isFailureCode( upstreamReturn ) is not True and upstreamReturn is not None: upstreamSite = upstreamReturn if downstreamSite is None and endOfDownstreamNetwork is False: #we haven't found downstream yet downstreamReturn = navigator.getNextDownstreamSite( graphSegment, distAlongSegment) if downstreamReturn == Failures.END_OF_BASIN_CODE: endOfDownstreamNetwork = True if Failures.isFailureCode( downstreamReturn ) is not True and downstreamReturn is not None: downstreamSite = downstreamReturn if upstreamSite is not None or downstreamSite is not None: #we've found at least one site secondaryQueries += 1 else: primaryQueries += 1 #add warnings from found sites, collect HUC if upstreamSite is not None: siteAssignment = upstreamSite[0] if logWarnings: for warning in siteAssignment.generalWarnings: warningLog.addWarningTuple(warning) for warning in siteAssignment.assignmentWarnings: warningLog.addWarningTuple(warning) huc = siteAssignment.huc if downstreamSite is not None: siteAssignment = downstreamSite[0] if logWarnings: for warning in siteAssignment.generalWarnings: warningLog.addWarningTuple(warning) for warning in siteAssignment.assignmentWarnings: warningLog.addWarningTuple(warning) huc = siteAssignment.huc if logWarnings: for warning in streamGraph.currentAssignmentWarnings: #apply warnings from streamGraph warningLog.addWarningTuple(warning) #handle all combinations of having an upstream site and/or a downstream site (also having neither) #~~~~~~~~~~~~~~~~~~~UPSTREAM AND DOWNSTREAM SITES FOUND CASE~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ if upstreamSite is not None and downstreamSite is not None: #we have an upstream and downstream upstreamSiteID = upstreamSite[0].siteID downstreamSiteID = downstreamSite[0].siteID partCode = upstreamSiteID[0:2] if Helpers.siteIDCompare(downstreamSiteID, upstreamSiteID) < 0: message = "The found upstream site is larger than found downstream site. ADONNIS output almost certainly incorrect." if logWarnings: warningLog.addWarning(WarningLog.HIGH_PRIORITY, message) fullUpstreamSiteID = Helpers.getFullID(upstreamSiteID) fullDownstreamSiteID = Helpers.getFullID(downstreamSiteID) upstreamSiteIdDsnStr = fullUpstreamSiteID[2:] downstreamSiteIdDsnStr = fullDownstreamSiteID[2:] #get the downstream number portion of the ID upstreamSiteIdDsn = int(upstreamSiteIdDsnStr) downstreamSiteIdDsn = int(downstreamSiteIdDsnStr) totalAddressSpaceDistance = upstreamSite[1] + downstreamSite[1] newSitePercentage = downstreamSite[1] / totalAddressSpaceDistance newDon = int(downstreamSiteIdDsn * (1 - newSitePercentage) + upstreamSiteIdDsn * newSitePercentage) newID = Helpers.buildFullID(partCode, newDon) newID = self.beautifyID(newID, downstreamSiteID, upstreamSiteID, logWarnings=logWarnings) story = "Found a upstream site " + Helpers.formatID( upstreamSiteID) + " and a downstream site " + Helpers.formatID( downstreamSiteID ) + ". New site is the weighted average of these two sites." if __debug__: print("found upstream is " + upstreamSiteID) print("found downstream is " + downstreamSiteID) streamGraph.visualize(customPoints=[snappedPoint]) SnapSites.visualize(self.baseData, []) #~~~~~~~~~~~~~~~~~~~UPSTREAM SITE FOUND ONLY CASE~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ elif upstreamSite is not None: upstreamSiteID = upstreamSite[0].siteID partCode = upstreamSiteID[:2] fullUpstreamID = Helpers.getFullID(upstreamSiteID) foundSiteNeighbors = siteIDManager.getNeighborIDs( upstreamSiteID, huc) if Failures.isFailureCode(foundSiteNeighbors): nextSequentialDownstreamSite = None else: nextSequentialDownstreamSite = foundSiteNeighbors[1] upstreamSiteDSN = int(fullUpstreamID[2:]) upstreamSiteDistance = upstreamSite[1] #calculate offset. If we have a sequential downstream use that as a bound siteIDOffset = math.ceil(upstreamSiteDistance / MIN_SITE_DISTANCE) if nextSequentialDownstreamSite is not None: #if we have the sequential downstream bound, don't let the new site get added any closer than halfway between siteIDOffset = min( siteIDOffset, Helpers.getSiteIDOffset(upstreamSiteID, nextSequentialDownstreamSite) / 2) newSiteIDDSN = upstreamSiteDSN + siteIDOffset #allowed wiggle room in the new site. Depending on how much distance is between the found site #we allow for a larger range in the final ID. Has to be at least 10% within the rule of min_site_distance #at most 5 digits up or down. At least, 0 allowedError = math.floor(max(1, min(siteIDOffset / 10, 5))) upperBound = Helpers.buildFullID( partCode, upstreamSiteDSN + siteIDOffset + allowedError) lowerBound = Helpers.buildFullID( partCode, upstreamSiteDSN + siteIDOffset - allowedError) newID = Helpers.buildFullID(partCode, newSiteIDDSN) newID = self.beautifyID(newID, lowerBound, upperBound, logWarnings=logWarnings) offsetAfterBeautify = Helpers.getSiteIDOffset( newID, fullUpstreamID) if nextSequentialDownstreamSite is None: story = "Only found a upstream site (" + upstreamSiteID + "). New site ID is based on upstream site while allowing space for " + str( offsetAfterBeautify ) + " sites between upstream site and new site" if logWarnings: warningLog.addWarning( WarningLog.HIGH_PRIORITY, "No downstream bound on result. Needs verification!") else: story = "Found an upstream site " + Helpers.formatID( upstreamSiteID ) + ". Based on list of all sites, assume that " + Helpers.formatID( nextSequentialDownstreamSite ) + " is the nearest sequential downstream site. New ID is based on the upstream site and bounded by the sequential downstream site" if logWarnings: warningLog.addWarning( WarningLog.LOW_PRIORITY, "Found upstream and downstream bound. But, downstream bound is based on list of sequential sites and may not be the true downstream bound. This could result in site ID clustering." ) if __debug__: print("found upstream, but not downstream") print("upstream siteID is " + str(upstreamSiteID)) streamGraph.visualize(customPoints=[snappedPoint]) SnapSites.visualize(self.baseData, []) #~~~~~~~~~~~~~~~~~~~DOWNSTREAM SITE ONLY CASE~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ elif downstreamSite is not None: downstreamSiteID = downstreamSite[0].siteID partCode = downstreamSiteID[:2] fullDownstreamID = Helpers.getFullID(downstreamSiteID) foundSiteNeighbors = siteIDManager.getNeighborIDs( downstreamSiteID, huc) if Failures.isFailureCode(foundSiteNeighbors): nextSequentialUpstreamSite = None else: nextSequentialUpstreamSite = foundSiteNeighbors[0] downstreamSiteDSN = int(fullDownstreamID[2:]) downstreamSiteDistance = downstreamSite[1] siteIDOffset = math.ceil(downstreamSiteDistance / MIN_SITE_DISTANCE) if nextSequentialUpstreamSite is not None: #if we have the sequential upstream bound, don't let the new site get added any closer than halfway between siteIDOffset = min( siteIDOffset, Helpers.getSiteIDOffset(downstreamSiteID, nextSequentialUpstreamSite) / 2) newSiteIDDSN = downstreamSiteDSN - siteIDOffset allowedError = math.floor(max(1, min(siteIDOffset / 10, 5))) upperBound = Helpers.buildFullID( partCode, downstreamSiteDSN - siteIDOffset + allowedError) lowerBound = Helpers.buildFullID( partCode, downstreamSiteDSN - siteIDOffset - allowedError) newID = Helpers.buildFullID(partCode, newSiteIDDSN) newID = self.beautifyID(newID, lowerBound, upperBound, logWarnings=logWarnings) offsetAfterBeautify = Helpers.getSiteIDOffset( newID, fullDownstreamID) if nextSequentialUpstreamSite is None: story = "Only found a downstream site " + Helpers.formatID( downstreamSiteID ) + ". New site ID is based on downstream site while allowing space for " + str( offsetAfterBeautify ) + " sites between downstream site and new site" if logWarnings: warningLog.addWarning( WarningLog.HIGH_PRIORITY, "No upstream bound on result. Needs verification!") else: story = "Found a downstream site " + Helpers.formatID( downstreamSiteID ) + ". Based on list of all sites, assume that " + Helpers.formatID( nextSequentialUpstreamSite ) + " is the nearest sequential upstream site. New ID is based on the downstream site and bounded by the sequential upstream site" if logWarnings: warningLog.addWarning( WarningLog.LOW_PRIORITY, "Found upstream and downstream bound. But, upstream bound is based on list of sequential sites and may not be the true upstream bound. This could result in site ID clustering." ) if __debug__: print("found downstream, but not upstream") print("downstream siteID is " + str(downstreamSiteID)) streamGraph.visualize(customPoints=[snappedPoint]) SnapSites.visualize(self.baseData, []) #~~~~~~~~~~~~~~~~~~~NO SITES FOUND CASE~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ else: # get huge radius of sites: sitesInfo = GDALData.loadSitesFromQuery(lat, lng, 30) if Failures.isFailureCode(sitesInfo): if logWarnings: warningLog.addWarning(WarningLog.HIGH_PRIORITY, sitesInfo) return getResults(failed=True) sites = [] for site in sitesInfo: siteNumber = site["properties"]["site_no"] siteHUC = site["properties"]["huc_cd"] sitePoint = site["geometry"]["coordinates"] fastDistance = Helpers.fastMagDist(sitePoint[0], sitePoint[1], point[0], point[1]) sites.append((siteNumber, sitePoint, fastDistance, siteHUC)) sortedSites = sorted(sites, key=lambda site: site[2]) huc = sortedSites[0][3] oppositePairA = None oppositePairB = None foundOppositePair = False i = 1 while foundOppositePair is False: curSite = sortedSites[i] curPartCode = curSite[0][:2] curSitePoint = curSite[1] curDirection = Helpers.normalize(curSitePoint[0] - point[0], curSitePoint[1] - point[1]) for cmpSite in sortedSites[:i]: cmpSitePoint = cmpSite[1] cmpDirection = Helpers.normalize( cmpSitePoint[0] - point[0], cmpSitePoint[1] - point[1]) cmpPartCode = cmpSite[0][:2] dot = Helpers.dot(curDirection[0], curDirection[1], cmpDirection[0], cmpDirection[1]) #check if these two directions are mostly opposite # dot < 0 means they are at least perpendicular if dot < 0.4 and curPartCode == cmpPartCode: foundOppositePair = True oppositePairA = cmpSite oppositePairB = curSite i += 1 partCode = oppositePairA[0][:2] fullIDA = Helpers.getFullID(oppositePairA[0]) fullIDB = Helpers.getFullID(oppositePairB[0]) dsnA = int(fullIDA[2:]) dsnB = int(fullIDB[2:]) distA = oppositePairA[2] distB = oppositePairB[2] totalAddressSpaceDistance = distA + distB newSitePercentage = distA / totalAddressSpaceDistance newDsn = int(dsnA * (1 - newSitePercentage) + dsnB * newSitePercentage) newID = Helpers.buildFullID(partCode, newDsn) newID = self.beautifyID(newID, fullIDA, fullIDB, logWarnings=logWarnings) story = "Could not find any sites on the network. Estimating based on " + Helpers.formatID( oppositePairA[0]) + " and " + Helpers.formatID( oppositePairB[0]) + "." if __debug__: print( "no sites found nearby. Estimating new ID based on nearby sites" ) print("new estimate based on " + oppositePairA[0] + " and " + oppositePairB[0]) print("estimation is " + newID) streamGraph.visualize() SnapSites.visualize(self.baseData, []) return getResults(siteID=newID, story=story)