def extend_ground_position( cls, initial: Point, max_distance: int, heading: Heading, theater: ConflictTheater, ) -> Point: """Finds the first intersection with an exclusion zone in one heading from an initial point up to max_distance""" extended = initial.point_from_heading(heading.degrees, max_distance) if theater.landmap is None: # TODO: Why is this possible? return extended p0 = ShapelyPoint(initial.x, initial.y) p1 = ShapelyPoint(extended.x, extended.y) line = LineString([p0, p1]) intersection = line.intersection( theater.landmap.inclusion_zone_only.boundary) if intersection.is_empty: # Max extent does not intersect with the boundary of the inclusion # zone, so the full front line is usable. This does assume that the # front line was centered on a valid location. return extended # Otherwise extend the front line only up to the intersection. return initial.point_from_heading(heading.degrees, p0.distance(intersection))
def map_bounds(theater: ConflictTheater) -> Polygon: points = [] for cp in theater.controlpoints: points.append(ShapelyPoint(cp.position.x, cp.position.y)) for tgo in cp.ground_objects: points.append(ShapelyPoint(tgo.position.x, tgo.position.y)) # Needs to be a large enough boundary beyond the known points so that # threatened airbases at the map edges have room to retreat from the # threat without running off the navmesh. return box(*LineString(points).bounds).buffer( nautical_miles(100).meters, resolution=1)
def for_faction(cls, game: Game, player: bool) -> ThreatZones: """Generates the threat zones projected by the given coalition. Args: game: The game to generate the threat zone for. player: True if the coalition projecting the threat zone belongs to the player. Returns: The threat zones projected by the given coalition. If the threat zone belongs to the player, it is the zone that will be avoided by the enemy and vice versa. """ air_threats = [] air_defenses = [] for control_point in game.theater.controlpoints: if control_point.captured != player: continue if control_point.runway_is_operational(): point = ShapelyPoint(control_point.position.x, control_point.position.y) cap_threat_range = cls.barcap_threat_range(game, control_point) air_threats.append(point.buffer(cap_threat_range.meters)) for tgo in control_point.ground_objects: for group in tgo.groups: threat_range = tgo.threat_range(group) # Any system with a shorter range than this is not worth # even avoiding. if threat_range > nautical_miles(3): point = ShapelyPoint(tgo.position.x, tgo.position.y) threat_zone = point.buffer(threat_range.meters) air_defenses.append(threat_zone) for front_line in game.theater.conflicts(player): vector = Conflict.frontline_vector(front_line.control_point_a, front_line.control_point_b, game.theater) start = vector[0] end = vector[0].point_from_heading(vector[1], vector[2]) line = LineString([ ShapelyPoint(start.x, start.y), ShapelyPoint(end.x, end.y), ]) doctrine = game.faction_for(player).doctrine air_threats.append( line.buffer(doctrine.cap_engagement_range.meters)) return cls(airbases=unary_union(air_threats), air_defenses=unary_union(air_defenses))
def for_threats( cls, theater: ConflictTheater, doctrine: Doctrine, barcap_locations: Iterable[ControlPoint], air_defenses: Iterable[TheaterGroundObject], ) -> ThreatZones: """Generates the threat zones projected by the given locations. Args: theater: The theater the threat zones are in. doctrine: The doctrine of the owning coalition. barcap_locations: The locations that will be considered for BARCAP planning. air_defenses: TGOs that may have air defenses. Returns: The threat zones projected by the given locations. If the threat zone belongs to the player, it is the zone that will be avoided by the enemy and vice versa. """ air_threats = [] air_defense_threats = [] radar_sam_threats = [] for barcap in barcap_locations: point = ShapelyPoint(barcap.position.x, barcap.position.y) cap_threat_range = cls.barcap_threat_range(doctrine, barcap) air_threats.append(point.buffer(cap_threat_range.meters)) for tgo in air_defenses: for group in tgo.groups: threat_range = tgo.threat_range(group) # Any system with a shorter range than this is not worth # even avoiding. if threat_range > nautical_miles(3): point = ShapelyPoint(tgo.position.x, tgo.position.y) threat_zone = point.buffer(threat_range.meters) air_defense_threats.append(threat_zone) radar_threat_range = tgo.threat_range(group, radar_only=True) if radar_threat_range > nautical_miles(3): point = ShapelyPoint(tgo.position.x, tgo.position.y) threat_zone = point.buffer(threat_range.meters) radar_sam_threats.append(threat_zone) return ThreatZones( theater, airbases=unary_union(air_threats), air_defenses=unary_union(air_defense_threats), radar_sam_threats=unary_union(radar_sam_threats), )
def random_point(aoi: Polygon) -> Point: minx, miny, maxx, maxy = aoi.bounds while True: point = ShapelyPoint(random.uniform(minx, maxx), random.uniform(miny, maxy)) if aoi.contains(point): return point.x, point.y
def cut(line, distance, normalized=False): """ Cuts a LineString in two at a distance from its starting point. This is an example in the Shapely documentation. """ if normalized: distance *= line.length if distance <= 0.0: return [None, line] elif distance >= line.length: return [line, None] coords = list(ShapelyPoint(p) for p in line.coords) traveled = 0 last_point = coords[0] for i, p in enumerate(coords[1:], 1): traveled += p.distance(last_point) last_point = p if traveled == distance: return [LineString(coords[:i + 1]), LineString(coords[i:])] if traveled > distance: cp = line.interpolate(distance) return [ LineString(coords[:i] + [(cp.x, cp.y)]), LineString([(cp.x, cp.y)] + coords[i:]) ]
def csv_to_neatline(in_file, out_file): """ Format a CSV file for Neatline. """ reader = csv.DictReader(in_file) # Add wkt field to the CSV. cols = reader.fieldnames + ['wkt'] writer = csv.DictWriter(out_file, cols) writer.writeheader() for row in reader: lat = float(row.pop('latitude')) lon = float(row.pop('longitude')) # Convert degrees -> meters. meters = degrees_to_meters(lon, lat) # Convert to WKT. point = ShapelyPoint(meters) row['wkt'] = wkt.dumps(point) writer.writerow(row)
def _make_threat_poly(self) -> ThreatPoly | None: threat_range = self.max_threat_range() if not threat_range: return None point = ShapelyPoint(self.position.x, self.position.y) return point.buffer(threat_range.meters)
def parse_lsr(prod, text): """Emit a LSR object based on this text! 0914 PM HAIL SHAW 33.60N 90.77W 04/29/2005 1.00 INCH BOLIVAR MS EMERGENCY MNGR """ lines = text.split("\n") if len(lines) < 2: prod.warnings.append(("LSR text is too short |%s|\n%s") % (text.replace("\n", "<NL>"), text)) return None lsr = LSR() lsr.product = prod lsr.text = text tokens = lines[0].split() h12 = tokens[0][:-2] mm = tokens[0][-2:] ampm = tokens[1] dstr = "%s:%s %s %s" % (h12, mm, ampm, lines[1][:10]) lsr.valid = datetime.datetime.strptime(dstr, "%I:%M %p %m/%d/%Y") lsr.assign_timezone(prod.tz, prod.z) # Check that we are within bounds if lsr.utcvalid > (prod.valid + FUTURE_THRESHOLD) or lsr.utcvalid > ( utc() + FUTURE_THRESHOLD): prod.warnings.append(("LSR is from the future!\n" "prod.valid: %s lsr.valid: %s\n" "%s\n") % (prod.valid, lsr.valid, text)) return None lsr.wfo = prod.source[1:] lsr.typetext = lines[0][12:29].strip() if lsr.typetext.upper() not in reference.lsr_events: prod.warnings.append( ("Unknown lsr.typetext |%s|\n%s") % (lsr.typetext, text)) return None lsr.city = lines[0][29:53].strip() tokens = lines[0][53:].strip().split() lat = float(tokens[0][:-1]) lon = 0 - float(tokens[1][:-1]) if lon <= -180 or lon >= 180 or lat >= 90 or lat <= -90: prod.warnings.append( ("Invalid Geometry Lat: %s Lon: %s\n%s") % (lat, lon, text)) return None lsr.geometry = ShapelyPoint((lon, lat)) lsr.consume_magnitude(lines[1][12:29].strip()) if lsr.magnitude_f is not None and math.isnan(lsr.magnitude_f): prod.warnings.append("LSR has NAN magnitude\n%s" % (text, )) return None lsr.county = lines[1][29:48].strip() lsr.state = lines[1][48:50] lsr.source = lines[1][53:].strip() if len(lines) > 2: meat = " ".join(lines[2:]).strip() if meat.strip() != "": lsr.remark = " ".join(meat.split()) return lsr
def for_faction(cls, game: Game, player: bool) -> ThreatZones: """Generates the threat zones projected by the given coalition. Args: game: The game to generate the threat zone for. player: True if the coalition projecting the threat zone belongs to the player. Returns: The threat zones projected by the given coalition. If the threat zone belongs to the player, it is the zone that will be avoided by the enemy and vice versa. """ air_threats = [] air_defenses = [] radar_sam_threats = [] for control_point in game.theater.controlpoints: if control_point.captured != player: continue if control_point.runway_is_operational(): point = ShapelyPoint(control_point.position.x, control_point.position.y) cap_threat_range = cls.barcap_threat_range(game, control_point) air_threats.append(point.buffer(cap_threat_range.meters)) for tgo in control_point.ground_objects: for group in tgo.groups: threat_range = tgo.threat_range(group) # Any system with a shorter range than this is not worth # even avoiding. if threat_range > nautical_miles(3): point = ShapelyPoint(tgo.position.x, tgo.position.y) threat_zone = point.buffer(threat_range.meters) air_defenses.append(threat_zone) radar_threat_range = tgo.threat_range(group, radar_only=True) if radar_threat_range > nautical_miles(3): point = ShapelyPoint(tgo.position.x, tgo.position.y) threat_zone = point.buffer(threat_range.meters) radar_sam_threats.append(threat_zone) return cls( airbases=unary_union(air_threats), air_defenses=unary_union(air_defenses), radar_sam_threats=unary_union(radar_sam_threats), )
def is_point_of_line(self, point): intersection = LineString([ (self.point_1.x, self.point_1.y), (self.point_2.x, self.point_2.y) ]).intersection(ShapelyPoint(point.x, point.y)) if type(intersection) == ShapelyPoint: return Point(intersection.x, intersection.y) return None
def draw_barcap_commit_range(self, scene: QGraphicsScene, flight: Flight) -> None: if flight.flight_type is not FlightType.BARCAP: return if not isinstance(flight.flight_plan, BarCapFlightPlan): return start = flight.flight_plan.patrol_start end = flight.flight_plan.patrol_end line = LineString([ ShapelyPoint(start.x, start.y), ShapelyPoint(end.x, end.y), ]) doctrine = self.game.faction_for(flight.departure.captured).doctrine bubble = line.buffer(doctrine.cap_engagement_range.meters) self.flight_path_items.append( self.draw_shapely_poly(scene, bubble, CONST.COLORS["yellow"], CONST.COLORS["transparent"]))
def commit_boundary( flight_id: UUID, game: Game = Depends(GameContext.require)) -> LeafletPoly: flight = game.db.flights.get(flight_id) if not isinstance(flight.flight_plan, PatrollingFlightPlan): return [] start = flight.flight_plan.layout.patrol_start end = flight.flight_plan.layout.patrol_end if isinstance(flight.flight_plan, CasFlightPlan): center = flight.flight_plan.layout.target.position commit_center = ShapelyPoint(center.x, center.y) else: commit_center = LineString([ ShapelyPoint(start.x, start.y), ShapelyPoint(end.x, end.y), ]) bubble = commit_center.buffer( flight.flight_plan.engagement_distance.meters) return ShapelyUtil.poly_to_leaflet(bubble, game.theater)
def commitBoundary(self) -> LeafletPoly: if not isinstance(self.flight.flight_plan, PatrollingFlightPlan): return [] start = self.flight.flight_plan.patrol_start end = self.flight.flight_plan.patrol_end if isinstance(self.flight.flight_plan, CasFlightPlan): center = self.flight.flight_plan.target.position commit_center = ShapelyPoint(center.x, center.y) else: commit_center = LineString( [ ShapelyPoint(start.x, start.y), ShapelyPoint(end.x, end.y), ] ) bubble = commit_center.buffer( self.flight.flight_plan.engagement_distance.meters ) return shapely_poly_to_leaflet_points(bubble, self.theater)
def __init__(self, pt, viapair, r, net=None): self.pt = pt self.r = r self.viapair = viapair self.net = net self.bbox = Rect.fromCenterSize(pt, r * 2, r * 2) self._project = None self.__poly_repr = ShapelyPoint(pt).buffer(self.r)
def getPointsGroupedFromCloud(self, cloud, outer_region, inner_region): outer_pts = [] inner_pts = [] outer_polygon = ShapelyPolygon(outer_region) inner_polygon = ShapelyPolygon(inner_region) for pt in cloud: point = ShapelyPoint(pt[0], pt[1]) if (outer_polygon.contains(point)): outer_pts.append(Point32(pt[0], pt[1], 0)) if (inner_polygon.contains(point)): inner_pts.append(Point32(pt[0], pt[1], 0)) return outer_pts, inner_pts
def getPointsInRegionFromClusters(self, clusters, cloud, detection_region): detected_pts = [] detection_polygon = ShapelyPolygon(detection_region) if (len(clusters) != 0): for cluster in clusters: for pt_id, pt in enumerate(cluster): point = ShapelyPoint(cloud[pt][0], cloud[pt][1]) if (detection_polygon.contains(point)): detected_pts.append(point) if (len(detected_pts) > 10): return detected_pts return detected_pts
def localize(self, point: Point) -> Optional[NavMeshPoly]: # This is a naive implementation but it's O(n). Runs at about 10k # lookups a second on a 5950X. Flights usually have 5-10 waypoints, so # that's 1k-2k flights before we lose a full second to localization as a # part of flight plan creation. # # Can improve the algorithm later if needed, but that seems unnecessary # currently. p = ShapelyPoint(point.x, point.y) for navpoly in self.polys: if navpoly.poly.contains(p): return navpoly return None
def new_point(polygon): # Loop until the point matches the poligon. while True: # Generate using a uniform distribution inside the bounding box. minx, miny, maxx, maxy = polygon.bounds p = ShapelyPoint(random.uniform(minx, maxx), random.uniform(miny, maxy)) # If is contained, return. if polygon.contains(p): return Delivery( id=new_id(), point=Point(p.x, p.y), size=random.randint(1, max_size), )
def __init__(self, pt: Vec2, viapair: 'ViaPair', r: float, net: Optional['pcbre.model.net.Net'] = None) -> None: super(Via, self).__init__() self.pt = pt self.r = r self.viapair = viapair self._net = net self._bbox = Rect.from_center_size(pt, r * 2, r * 2) self._project = None self.__poly_repr = ShapelyPoint(pt).buffer(self.r)
def scanCB(self, msg): overall_pts = [] inner_pts = [] remaining_pts = [] overall_clusters = [] remaining_clusters = [] self.new_timestamp = msg.header.stamp for i in xrange(len(msg.ranges)): pt = Point() pt.x = msg.ranges[i] * np.cos(msg.angle_min + i * msg.angle_increment) pt.y = msg.ranges[i] * np.sin(msg.angle_min + i * msg.angle_increment) shapely_point = ShapelyPoint(pt.x, pt.y) if (self.outer_zone.contains(shapely_point)): overall_pts.append(pt) if (self.inner_zone.contains(shapely_point)): inner_pts.append(pt) if (self.mode == 0): print "Mode 0 ---> Obstacle Stop Function Closed" self.debug_pub.publish("Mode 0 ---> Obstacle Stop Function Closed") elif (self.mode == 1): #print "Mode 1 ---> Without Table on Top" if (overall_pts != []): overall_clusters = self.cluster_check(overall_pts) if (overall_clusters != []): self.stopping() print "Mode 1 ---> Obstacle Detected and Stopping" self.debug_pub.publish( "Mode 1 ---> Obstacle Detected and Stopping") else: self.debug_pub.publish("Mode 1 ---> No Obstacles") elif (self.mode == 2): #print "Mode 2 ---> Carry Table on Top" remaining_pts = list(set(overall_pts) - set(inner_pts)) if (remaining_pts != []): remaining_clusters = self.cluster_check(remaining_pts) if (remaining_clusters != []): self.stopping() print "Mode 2 ---> Obstacle Detected and Stopping" self.debug_pub.publish( "Mode 2 ---> Obstacle Detected and Stopping") else: self.debug_pub.publish("Mode 2 ---> No Obstacles") self.load_params_from_yaml(self.region_file)
def drawWithBrush(self, other, pathSmoothness=50, brushSmoothness=20, alpha=0.15): """Assuming that `other` is a closed Bezier path representing a pen or brush of a certain shape and that `self` is an open path, this method traces the brush along the path, returning an array of Bezier paths. `other` may also be a function which, given a time `t` (0-1), returns a closed path representing the shape of the brush at the given time. This requires the `shapely` and `scipy` libraries to be installed, and is very, very slow. """ samples = self.sample(int(self.length * (pathSmoothness / 100.0))) self.closed = False def constantBrush(t): return other brush = other if not callable(brush): brush = constantBrush c = brush(0).centroid points = [] from shapely.geometry import Point as ShapelyPoint t = 0 for n in samples: brushHere = brush(t).clone() brushHere.translate(n - brushHere.centroid) brushsamples = brushHere.sample( int(brushHere.length * (brushSmoothness / 100.0))) points.extend([ShapelyPoint(s.x, s.y) for s in brushsamples]) t = t + 1.0 / len(samples) from beziers.utils.alphashape import alpha_shape concave_hull, edge_points = alpha_shape(points, alpha=alpha) points = [Point(p[0], p[1]) for p in concave_hull.exterior.coords] path = BezierPath.fromPoints(points, error=5, maxSegments=100) return path
def parse_lsr(prod, text): ''' Emit a LSR object based on this text! 0914 PM HAIL SHAW 33.60N 90.77W 04/29/2005 1.00 INCH BOLIVAR MS EMERGENCY MNGR ''' lines = text.split("\n") if len(lines) < 2: prod.warnings.append(("LSR text is too short |%s|\n%s") % (text.replace("\n", "<NL>"), text)) return None lsr = LSR() lsr.text = text tokens = lines[0].split() h12 = tokens[0][:-2] mm = tokens[0][-2:] ampm = tokens[1] dstr = "%s:%s %s %s" % (h12, mm, ampm, lines[1][:10]) lsr.valid = datetime.datetime.strptime(dstr, "%I:%M %p %m/%d/%Y") lsr.typetext = lines[0][12:29].strip() if lsr.typetext.upper() not in reference.lsr_events: prod.warnings.append( ("Unknown lsr.typetext |%s|\n%s") % (lsr.typetext, text)) return None lsr.city = lines[0][29:53].strip() tokens = lines[0][53:].strip().split() lat = float(tokens[0][:-1]) lon = 0 - float(tokens[1][:-1]) if lon <= -180 or lon >= 180 or lat >= 90 or lat <= -90: prod.warnings.append( ("Invalid Geometry Lat: %s Lon: %s\n%s") % (lat, lon, text)) return None lsr.geometry = ShapelyPoint((lon, lat)) lsr.consume_magnitude(lines[1][12:29].strip()) lsr.county = lines[1][29:48].strip() lsr.state = lines[1][48:50] lsr.source = lines[1][53:].strip() if len(lines) > 2: meat = " ".join(lines[2:]).strip() lsr.remark = " ".join(meat.split()) return lsr
def cut(line, distance): """ Cuts a LineString in two at a distance from its starting point. This is an example in the Shapely documentation. """ if distance <= 0.0 or distance >= line.length: return [LineString(line), None] coords = list(line.coords) for i, p in enumerate(coords): # TODO: I think this doesn't work if the path doubles back on itself pd = line.project(ShapelyPoint(p)) if pd == distance: return [LineString(coords[:i + 1]), LineString(coords[i:])] if pd > distance: cp = line.interpolate(distance) return [ LineString(coords[:i] + [(cp.x, cp.y)]), LineString([(cp.x, cp.y)] + coords[i:]) ]
def aggregate(self): working_cad = np.array(self.cad) res = collections.OrderedDict() for g in self.shapely_grid: # res[g] = np.array([(t, x, y) for (t, x, y) in self.cad if ShapelyPoint(x, y).within(g)]) in_grid = [] idx = [] if working_cad.ndim == 1: import ipdb ipdb.set_trace() for i in range(working_cad.shape[0]): pt = ShapelyPoint(working_cad[i, 1], working_cad[i, 2]) if pt.intersects(g): in_grid.append(working_cad[i, :]) idx.append(i) res[g] = np.array(in_grid) working_cad = np.delete(working_cad, idx, axis=0) return res
def dcs_to_shapely_point(point: Point) -> ShapelyPoint: return ShapelyPoint(point.x, point.y)
def to_shapely(self) -> ShapelyPoint: return ShapelyPoint(self.to_list())
def scanCB(self, msg): self.start_time = rospy.Time.now().to_sec() self.serv_stopping = 0 #to stop obstacle service for image more than once overall_pts = [] overall_clusters = [] front_points = [] inner_region_top_speed_points = [] self.new_timestamp = msg.header.stamp for i in xrange(len(msg.ranges)): pt = Point32() pt.x = msg.ranges[i] * np.cos(msg.angle_min + i * msg.angle_increment) pt.y = msg.ranges[i] * np.sin(msg.angle_min + i * msg.angle_increment) shapely_point = ShapelyPoint(pt.x, pt.y) if (self.front_region_zone.contains(shapely_point)): front_points.append(pt) if (-2.0 < pt.x < 2.0 and -1.0 < pt.y < 1.0): if self.lifter_status == "bottom": if (self.mode != "table_dropping" and self.mode != "table_picking" and self.outer_zone.contains(shapely_point)): overall_pts.append(pt) if (self.mode == "table_dropping" and self.mode == "table_picking" and self.inner_zone.contains(shapely_point)): #inner_pts.appnd(pt) overall_pts.append(pt) if self.lifter_status == "top": if (self.speed_type == 1 and self.inner_region_top_speed_zone.contains( shapely_point)): overall_pts.append(pt) if (self.speed_type == 0 and self.inner_region_slow_speed_zone.contains( shapely_point)): #inner_region_slow_speed_points.append(pt) overall_pts.append(pt) print('----------------------------------') print(self.mode) if self.data != "ERROR": self.err = 1 if (front_points != []) and (self.mode == "delivery" or self.mode == "error"): overall_clusters = self.cluster_check(front_points) print("over all,", overall_clusters) if (overall_clusters != []): print("obstacle ahead detected") if self.data == "ERROR" and self.err == 1: self.err = 0 self.img_send() #call service self.stopping() try: print(len(overall_pts)) overall_clusters = self.cluster_check(overall_pts) if (overall_clusters != []): self.stopping() if self.serv_stopping == 0: self.serv_stopping = 1 print self.lifter_status, " - Obstacle Detected........................" else: self.serv_stopping = 0 except: print("LIFTER STATUS NOT RECOGNIZED") self.load_params_from_yaml(self.region_file) print "start_time", self.start_time print "end_time", rospy.Time.now().to_sec()
def _parse_init_param(self, param): if isinstance(param, (float, int)): return param elif isinstance(param, (tuple, InkstitchPoint, ShapelyPoint)): return self.satin.center.project(ShapelyPoint(param), normalized=True)
def __init__( self, target: Point, home: Point, coalition: Coalition, ) -> None: self._target = target self.threat_zone = coalition.opponent.threat_zone.all self.home = ShapelyPoint(home.x, home.y) max_ip_distance = coalition.doctrine.max_ingress_distance min_ip_distance = coalition.doctrine.min_ingress_distance # The minimum distance between the home location and the IP. min_distance_from_home = nautical_miles(5) # The distance that is expected to be needed between the beginning of the attack # and weapon release. This buffers the threat zone to give a 5nm window between # the edge of the "safe" zone and the actual threat so that "safe" IPs are less # likely to end up with the attacker entering a threatened area. attack_distance_buffer = nautical_miles(5) home_threatened = coalition.opponent.threat_zone.threatened(home) shapely_target = ShapelyPoint(target.x, target.y) home_to_target_distance = meters(home.distance_to_point(target)) self.home_bubble = self.home.buffer(home_to_target_distance.meters).difference( self.home.buffer(min_distance_from_home.meters) ) # If the home zone is not threatened and home is within LAR, constrain the max # range to the home-to-target distance to prevent excessive backtracking. # # If the home zone *is* threatened, we need to back out of the zone to # rendezvous anyway. if not home_threatened and ( min_ip_distance < home_to_target_distance < max_ip_distance ): max_ip_distance = home_to_target_distance max_ip_bubble = shapely_target.buffer(max_ip_distance.meters) min_ip_bubble = shapely_target.buffer(min_ip_distance.meters) self.ip_bubble = max_ip_bubble.difference(min_ip_bubble) # The intersection of the home bubble and IP bubble will be all the points that # are within the valid IP range that are not farther from home than the target # is. However, if the origin airfield is threatened but there are safe # placements for the IP, we should not constrain to the home zone. In this case # we'll either end up with a safe zone outside the home zone and pick the # closest point in to to home (minimizing backtracking), or we'll have no safe # IP anywhere within range of the target, and we'll later pick the IP nearest # the edge of the threat zone. if home_threatened: self.permissible_zone = self.ip_bubble else: self.permissible_zone = self.ip_bubble.intersection(self.home_bubble) if self.permissible_zone.is_empty: # If home is closer to the target than the min range, there will not be an # IP solution that's close enough to home, in which case we need to ignore # the home bubble. self.permissible_zone = self.ip_bubble safe_zones = self.permissible_zone.difference( self.threat_zone.buffer(attack_distance_buffer.meters) ) if not isinstance(safe_zones, MultiPolygon): safe_zones = MultiPolygon([safe_zones]) self.safe_zones = safe_zones