1 /* Copyright 2002-2024 CS GROUP
2 * Licensed to CS GROUP (CS) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * CS licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 package org.orekit.propagation.events;
18
19 import java.util.ArrayList;
20 import java.util.List;
21
22 import org.hipparchus.geometry.enclosing.EnclosingBall;
23 import org.hipparchus.geometry.euclidean.threed.Vector3D;
24 import org.hipparchus.geometry.spherical.twod.Edge;
25 import org.hipparchus.geometry.spherical.twod.S2Point;
26 import org.hipparchus.geometry.spherical.twod.Sphere2D;
27 import org.hipparchus.geometry.spherical.twod.SphericalPolygonsSet;
28 import org.hipparchus.geometry.spherical.twod.Vertex;
29 import org.hipparchus.ode.events.Action;
30 import org.hipparchus.util.FastMath;
31 import org.hipparchus.util.SinCos;
32 import org.orekit.bodies.BodyShape;
33 import org.orekit.bodies.GeodeticPoint;
34 import org.orekit.bodies.OneAxisEllipsoid;
35 import org.orekit.frames.StaticTransform;
36 import org.orekit.geometry.fov.FieldOfView;
37 import org.orekit.models.earth.tessellation.DivertedSingularityAiming;
38 import org.orekit.models.earth.tessellation.EllipsoidTessellator;
39 import org.orekit.propagation.SpacecraftState;
40 import org.orekit.propagation.events.handlers.EventHandler;
41 import org.orekit.propagation.events.handlers.StopOnIncreasing;
42
43 /** Detector triggered by geographical region entering/leaving a spacecraft sensor
44 * {@link FieldOfView Field Of View}.
45 * <p>
46 * This detector is a mix between to {@link FieldOfViewDetector} and {@link
47 * GeographicZoneDetector}. Similar to the first detector above, it triggers events
48 * related to entry/exit of targets in a Field Of View, taking attitude into account.
49 * Similar to the second detector above, its target is an entire geographic region
50 * (which can even be split in several non-connected patches and can have holes).
51 * </p>
52 * <p>
53 * This detector is typically used for ground observation missions with agile
54 * satellites than can look away from nadir.
55 * </p>
56 * <p>The default implementation behavior is to {@link Action#CONTINUE continue}
57 * propagation at FOV entry and to {@link Action#STOP stop} propagation
58 * at FOV exit. This can be changed by calling
59 * {@link #withHandler(EventHandler)} after construction.</p>
60 * @see org.orekit.propagation.Propagator#addEventDetector(EventDetector)
61 * @see FieldOfViewDetector
62 * @see GeographicZoneDetector
63 * @author Luc Maisonobe
64 * @since 7.1
65 */
66 public class FootprintOverlapDetector extends AbstractDetector<FootprintOverlapDetector> {
67
68 /** Field of view. */
69 private final FieldOfView fov;
70
71 /** Body on which the geographic zone is defined. */
72 private final OneAxisEllipsoid body;
73
74 /** Geographic zone to consider. */
75 private final SphericalPolygonsSet zone;
76
77 /** Linear step used for sampling the geographic zone. */
78 private final double samplingStep;
79
80 /** Sampling of the geographic zone. */
81 private final List<SamplingPoint> sampledZone;
82
83 /** Center of the spherical cap surrounding the zone. */
84 private final Vector3D capCenter;
85
86 /** Cosine of the radius of the spherical cap surrounding the zone. */
87 private final double capCos;
88
89 /** Sine of the radius of the spherical cap surrounding the zone. */
90 private final double capSin;
91
92 /** Build a new instance.
93 * <p>The maximal interval between distance to FOV boundary checks should
94 * be smaller than the half duration of the minimal pass to handle,
95 * otherwise some short passes could be missed.</p>
96 * @param fov sensor field of view
97 * @param body body on which the geographic zone is defined
98 * @param zone geographic zone to consider
99 * @param samplingStep linear step used for sampling the geographic zone (in meters)
100 * @since 10.1
101 */
102 public FootprintOverlapDetector(final FieldOfView fov,
103 final OneAxisEllipsoid body,
104 final SphericalPolygonsSet zone,
105 final double samplingStep) {
106 this(AdaptableInterval.of(DEFAULT_MAXCHECK), DEFAULT_THRESHOLD, DEFAULT_MAX_ITER,
107 new StopOnIncreasing(),
108 fov, body, zone, samplingStep, sample(body, zone, samplingStep));
109 }
110
111 /** Protected constructor with full parameters.
112 * <p>
113 * This constructor is not public as users are expected to use the builder
114 * API with the various {@code withXxx()} methods to set up the instance
115 * in a readable manner without using a huge amount of parameters.
116 * </p>
117 * @param maxCheck maximum checking interval
118 * @param threshold convergence threshold (s)
119 * @param maxIter maximum number of iterations in the event time search
120 * @param handler event handler to call at event occurrences
121 * @param body body on which the geographic zone is defined
122 * @param zone geographic zone to consider
123 * @param fov sensor field of view
124 * @param sampledZone sampling of the geographic zone
125 * @param samplingStep linear step used for sampling the geographic zone (in meters)
126 */
127 protected FootprintOverlapDetector(final AdaptableInterval maxCheck, final double threshold,
128 final int maxIter, final EventHandler handler,
129 final FieldOfView fov,
130 final OneAxisEllipsoid body,
131 final SphericalPolygonsSet zone,
132 final double samplingStep,
133 final List<SamplingPoint> sampledZone) {
134
135 super(maxCheck, threshold, maxIter, handler);
136 this.fov = fov;
137 this.body = body;
138 this.samplingStep = samplingStep;
139 this.zone = zone;
140 this.sampledZone = sampledZone;
141
142 final EnclosingBall<Sphere2D, S2Point> cap = zone.getEnclosingCap();
143 final SinCos sc = FastMath.sinCos(cap.getRadius());
144 this.capCenter = cap.getCenter().getVector();
145 this.capCos = sc.cos();
146 this.capSin = sc.sin();
147
148 }
149
150 /** Sample the region.
151 * @param body body on which the geographic zone is defined
152 * @param zone geographic zone to consider
153 * @param samplingStep linear step used for sampling the geographic zone (in meters)
154 * @return sampling points
155 */
156 private static List<SamplingPoint> sample(final OneAxisEllipsoid body,
157 final SphericalPolygonsSet zone,
158 final double samplingStep) {
159
160 final List<SamplingPoint> sampledZone = new ArrayList<SamplingPoint>();
161
162 // sample the zone boundary
163 final List<Vertex> boundary = zone.getBoundaryLoops();
164 for (final Vertex loopStart : boundary) {
165 int count = 0;
166 for (Vertex v = loopStart; count == 0 || v != loopStart; v = v.getOutgoing().getEnd()) {
167 ++count;
168 final Edge edge = v.getOutgoing();
169 final int n = (int) FastMath.ceil(edge.getLength() * body.getEquatorialRadius() / samplingStep);
170 for (int i = 0; i < n; ++i) {
171 final S2Point intermediate = new S2Point(edge.getPointAt(i * edge.getLength() / n));
172 final GeodeticPoint gp = new GeodeticPoint(0.5 * FastMath.PI - intermediate.getPhi(),
173 intermediate.getTheta(), 0.0);
174 sampledZone.add(new SamplingPoint(body.transform(gp), gp.getZenith()));
175 }
176 }
177 }
178
179 // sample the zone interior
180 final EllipsoidTessellator tessellator =
181 new EllipsoidTessellator(body, new DivertedSingularityAiming(zone), 1);
182 final List<List<GeodeticPoint>> gpSample = tessellator.sample(zone, samplingStep, samplingStep);
183 for (final List<GeodeticPoint> list : gpSample) {
184 for (final GeodeticPoint gp : list) {
185 sampledZone.add(new SamplingPoint(body.transform(gp), gp.getZenith()));
186 }
187 }
188
189 return sampledZone;
190
191 }
192
193 /** {@inheritDoc} */
194 @Override
195 protected FootprintOverlapDetector create(final AdaptableInterval newMaxCheck, final double newThreshold,
196 final int newMaxIter,
197 final EventHandler newHandler) {
198 return new FootprintOverlapDetector(newMaxCheck, newThreshold, newMaxIter, newHandler,
199 fov, body, zone, samplingStep, sampledZone);
200 }
201
202 /** Get the geographic zone triggering the events.
203 * <p>
204 * The zone is mapped on the unit sphere
205 * </p>
206 * @return geographic zone triggering the events
207 */
208 public SphericalPolygonsSet getZone() {
209 return zone;
210 }
211
212 /** Get the Field Of View.
213 * @return Field Of View
214 * @since 10.1
215 */
216 public FieldOfView getFOV() {
217 return fov;
218 }
219
220 /** Get the body on which the geographic zone is defined.
221 * @return body on which the geographic zone is defined
222 */
223 public BodyShape getBody() {
224 return body;
225 }
226
227 /** {@inheritDoc}
228 * <p>
229 * The g function value is the minimum offset among the region points
230 * with respect to the Field Of View boundary. It is positive if all region
231 * points are outside of the Field Of View, and negative if at least some
232 * of the region points are inside of the Field Of View. The minimum is
233 * computed by sampling the region, considering only the points for which
234 * the spacecraft is above the horizon. The accuracy of the detection
235 * depends on the linear sampling step set at detector construction. If
236 * the spacecraft is below horizon for all region points, an arbitrary
237 * positive value is returned.
238 * </p>
239 * <p>
240 * As per the previous definition, when the region enters the Field Of
241 * View, a decreasing event is generated, and when the region leaves
242 * the Field Of View, an increasing event is generated.
243 * </p>
244 */
245 public double g(final SpacecraftState s) {
246
247 // initial arbitrary positive value
248 double value = FastMath.PI;
249
250 // get spacecraft position in body frame
251 final Vector3D scBody = s.getPosition(body.getBodyFrame());
252
253 // map the point to a sphere
254 final GeodeticPoint gp = body.transform(scBody, body.getBodyFrame(), s.getDate());
255 final S2Point s2p = new S2Point(gp.getLongitude(), 0.5 * FastMath.PI - gp.getLatitude());
256
257 // for faster computation, we start using only the surrounding cap, to filter out
258 // far away points (which correspond to most of the points if the zone is small)
259 final Vector3D p = s2p.getVector();
260 final double dot = Vector3D.dotProduct(p, capCenter);
261 if (dot < capCos) {
262 // the spacecraft is outside of the cap, look for the closest cap point
263 final Vector3D t = p.subtract(dot, capCenter).normalize();
264 final Vector3D close = new Vector3D(capCos, capCenter, capSin, t);
265 if (Vector3D.dotProduct(p, close) < -0.01) {
266 // the spacecraft is not visible from the cap edge,
267 // even taking some margin into account for sphere/ellipsoid different shapes
268 // this induces no points in the zone can see the spacecraft,
269 // we can return the arbitrary initial positive value without performing further computation
270 return value;
271 }
272 }
273
274 // the spacecraft may be visible from some points in the zone, check them all
275 final StaticTransform bodyToSc = StaticTransform.compose(
276 s.getDate(),
277 body.getBodyFrame().getStaticTransformTo(s.getFrame(), s.getDate()),
278 s.toStaticTransform());
279 for (final SamplingPoint point : sampledZone) {
280 final Vector3D lineOfSightBody = point.getPosition().subtract(scBody);
281 if (Vector3D.dotProduct(lineOfSightBody, point.getZenith()) <= 0) {
282 // spacecraft is above this sample point local horizon
283 // get line of sight in spacecraft frame
284 final double offset = fov.offsetFromBoundary(bodyToSc.transformVector(lineOfSightBody),
285 0.0, VisibilityTrigger.VISIBLE_ONLY_WHEN_FULLY_IN_FOV);
286 value = FastMath.min(value, offset);
287 }
288 }
289
290 return value;
291
292 }
293
294 /** Container for sampling points. */
295 private static class SamplingPoint {
296
297 /** Position of the point. */
298 private final Vector3D position;
299
300 /** Zenith vector of the point. */
301 private final Vector3D zenith;
302
303 /** Simple constructor.
304 * @param position position of the point
305 * @param zenith zenith vector of the point
306 */
307 SamplingPoint(final Vector3D position, final Vector3D zenith) {
308 this.position = position;
309 this.zenith = zenith;
310 }
311
312 /** Get the point position.
313 * @return point position
314 */
315 public Vector3D getPosition() {
316 return position;
317 }
318
319 /** Get the point zenith vector.
320 * @return point zenith vector
321 */
322 public Vector3D getZenith() {
323 return zenith;
324 }
325
326 }
327
328 }