1   /* Copyright 2002-2022 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.io.File;
20  import java.util.ArrayList;
21  import java.util.Comparator;
22  
23  import org.hipparchus.geometry.euclidean.twod.PolygonsSet;
24  import org.hipparchus.geometry.euclidean.twod.Vector2D;
25  import org.hipparchus.geometry.partitioning.Region.Location;
26  import org.hipparchus.ode.events.Action;
27  import org.hipparchus.util.FastMath;
28  import org.junit.After;
29  import org.junit.Assert;
30  import org.junit.Before;
31  import org.junit.Test;
32  import org.orekit.bodies.GeodeticPoint;
33  import org.orekit.bodies.OneAxisEllipsoid;
34  import org.orekit.data.DataContext;
35  import org.orekit.data.DirectoryCrawler;
36  import org.orekit.frames.Frame;
37  import org.orekit.frames.FramesFactory;
38  import org.orekit.models.earth.GeoMagneticField;
39  import org.orekit.models.earth.GeoMagneticFieldFactory;
40  import org.orekit.models.earth.GeoMagneticFieldFactory.FieldModel;
41  import org.orekit.orbits.CircularOrbit;
42  import org.orekit.orbits.Orbit;
43  import org.orekit.orbits.PositionAngle;
44  import org.orekit.propagation.Propagator;
45  import org.orekit.propagation.SpacecraftState;
46  import org.orekit.propagation.analytical.KeplerianPropagator;
47  import org.orekit.propagation.events.handlers.EventHandler;
48  import org.orekit.time.AbsoluteDate;
49  import org.orekit.time.TimeScale;
50  import org.orekit.time.TimeScalesFactory;
51  import org.orekit.utils.Constants;
52  import org.orekit.utils.IERSConventions;
53  
54  /**
55   * test class for MagneticFieldDetector
56   * @author Romaric Her
57   */
58  public class MagneticFieldDetectorTest {
59  	
60  	Propagator propagator;	
61  	Frame eme2000;
62  	Frame itrf;
63  	TimeScale utc;
64  	AbsoluteDate initialDate;
65  	OneAxisEllipsoid earth;
66  	GeoMagneticField wmm;
67  	GeoMagneticField igrf;
68  	
69  	double saaValidationThreshold = 500;
70  	double saaValidationWidth = 200;
71  	
72  	
73  	/**
74  	 * Prepare the Orekit library.
75  	 */
76  	@Before
77  	public void setup()
78  	{
79  		// Prepare the Orekit library
80  		final String orekitCfgPath = "src/test/resources";
81  
82  		// Initialize Orekit
83  		DataContext.getDefault().getDataProvidersManager().addProvider(new DirectoryCrawler(new File(orekitCfgPath)));
84  
85  		// Initialize constants
86  		eme2000 = FramesFactory.getEME2000();
87  		itrf = FramesFactory.getITRF(IERSConventions.IERS_2010, false);
88  		utc = TimeScalesFactory.getUTC();
89  		initialDate = new AbsoluteDate("2019-01-01T12:00:00.000", utc);
90  		earth = new OneAxisEllipsoid(Constants.WGS84_EARTH_EQUATORIAL_RADIUS, Constants.WGS84_EARTH_FLATTENING, itrf);
91  		
92  		//Initialize magnetic field models
93  		wmm = GeoMagneticFieldFactory.getField(FieldModel.WMM, 2019);
94  		igrf = GeoMagneticFieldFactory.getField(FieldModel.IGRF, 2019);
95  		
96  	}
97  	
98  	/**
99  	 * Remove the Orekit configuration
100 	 */
101 	@After
102 	public void tearDown()
103 	{
104 		DataContext.getDefault().getDataProvidersManager().clearProviders();
105 		DataContext.getDefault().getDataProvidersManager().clearLoadedDataNames();
106 	}
107 	
108 	/**
109 	 * initialize an analytical propagator with a polar orbit to cross the SAA
110 	 */
111 	private void initializePropagator() {
112 		double a = Constants.WGS84_EARTH_EQUATORIAL_RADIUS + 600000; // 600 km altitude
113 		double i = FastMath.toRadians(80); // 80° inclination
114 		Orbit initialOrbit = new CircularOrbit(a, 0, 0, i, 0, 0, PositionAngle.TRUE, eme2000, initialDate, Constants.WGS84_EARTH_MU);
115 		propagator = new KeplerianPropagator(initialOrbit);
116 	}
117 	
118 	/**
119 	 * Test for the magnetic field detector based on the WMM at sea level
120 	 */
121 	@Test
122 	public void magneticFieldDetectorWMMSeaLevelTest() {
123 		initializePropagator();
124 		double threshold = 45000;
125 		
126 		CustomEventHandler handler = new CustomEventHandler();
127 		MagneticFieldDetector detector = new MagneticFieldDetector(threshold, FieldModel.WMM, earth, true).withHandler(handler);
128 		propagator.addEventDetector(detector);
129 		propagator.propagate(initialDate, initialDate.shiftedBy(864000));
130 
131 		checkEvents(handler.getEvents(), threshold, wmm, true);
132 	}
133 	
134 	/**
135 	 * Test for the magnetic field detector based on the IGRF at sea level
136 	 */
137 	@Test
138 	public void magneticFieldDetectorIGRFSeaLevelTest() {
139 		initializePropagator();
140 		double threshold = 45000;
141 		
142 		CustomEventHandler handler = new CustomEventHandler();
143 		MagneticFieldDetector detector = new MagneticFieldDetector(threshold, FieldModel.IGRF, earth, true).withHandler(handler);
144 		propagator.addEventDetector(detector);
145 		propagator.propagate(initialDate, initialDate.shiftedBy(864000));
146 		
147 		checkEvents(handler.getEvents(), threshold, igrf, true);
148 	}
149 	
150 	/**
151 	 * Test for the magnetic field detector based on the WMM at sea level
152 	 */
153 	@Test
154 	public void magneticFieldDetectorWMMTrueAltitudeTest() {
155 		initializePropagator();
156 		double threshold = 45000;
157 		
158 		CustomEventHandler handler = new CustomEventHandler();
159 		MagneticFieldDetector detector = new MagneticFieldDetector(threshold, FieldModel.WMM, earth).withHandler(handler);
160 		propagator.addEventDetector(detector);
161 		propagator.propagate(initialDate, initialDate.shiftedBy(864000));
162 
163 		checkEvents(handler.getEvents(), threshold, wmm, false);
164 	}
165 	
166 	/**
167 	 * Test for the magnetic field detector based on the IGRF at sea level
168 	 */
169 	@Test
170 	public void magneticFieldDetectorIGRFTrueAltitudeTest() {
171 		initializePropagator();
172 		double threshold = 45000;
173 		
174 		CustomEventHandler handler = new CustomEventHandler();
175 		MagneticFieldDetector detector = new MagneticFieldDetector(threshold, FieldModel.IGRF, earth).withHandler(handler);
176 		propagator.addEventDetector(detector);
177 		propagator.propagate(initialDate, initialDate.shiftedBy(864000));
178 		
179 		checkEvents(handler.getEvents(), threshold, igrf, false);
180 	}
181 	
182 	/**
183 	 * Test for the SAA detector based on the WMM at sea level
184 	 */
185 	@Test
186 	public void saaDetectorWMMSeaLevelTest() {
187 		initializePropagator();
188 		
189 		double altitude = 0;		
190 		double threshold = 32000;
191 		
192 		PolygonsSet saaIn = generateGeomagneticMap(wmm, altitude, threshold - saaValidationThreshold, saaValidationWidth);
193 		PolygonsSet saaOut = generateGeomagneticMap(wmm, altitude, threshold + saaValidationThreshold, saaValidationWidth);
194 		
195 		CustomEventHandler handler = new CustomEventHandler();
196 		MagneticFieldDetector detector = new MagneticFieldDetector(threshold, FieldModel.WMM, earth, true).withHandler(handler);
197 		propagator.addEventDetector(detector);
198 		propagator.propagate(initialDate, initialDate.shiftedBy(864000));
199 
200 		checkEvents(handler.getEvents(), threshold, wmm, true);
201 		checkSaa(handler.getEvents(), saaIn, saaOut);
202 	}
203 	
204 	/**
205 	 * Test for the SAA detector based on the IGRF at sea level
206 	 */
207 	@Test
208 	public void saaDetectorIGRFSeaLevelTest() {
209 		initializePropagator();
210 		
211 		double altitude = 0;		
212 		double threshold = 32000;
213 		
214 		PolygonsSet saaIn = generateGeomagneticMap(igrf, altitude, threshold - saaValidationThreshold, saaValidationWidth);
215 		PolygonsSet saaOut = generateGeomagneticMap(igrf, altitude, threshold + saaValidationThreshold, saaValidationWidth);
216 		
217 		CustomEventHandler handler = new CustomEventHandler();
218 		MagneticFieldDetector detector = new MagneticFieldDetector(threshold, FieldModel.IGRF, earth, true).withHandler(handler);
219 		propagator.addEventDetector(detector);
220 		propagator.propagate(initialDate, initialDate.shiftedBy(864000));
221 		
222 		checkEvents(handler.getEvents(), threshold, igrf, true);
223 		checkSaa(handler.getEvents(), saaIn, saaOut);
224 	}
225 	
226 	/**
227 	 * Test for the SAA detector based on the WMM at sea level
228 	 */
229 	@Test
230 	public void saaDetectorWMMTrueAltitudeTest() {
231 		initializePropagator();
232 		
233 		double altitude = 600000;		
234 		double threshold = 23000;
235 		
236 		PolygonsSet saaIn = generateGeomagneticMap(wmm, altitude, threshold - saaValidationThreshold, saaValidationWidth);
237 		PolygonsSet saaOut = generateGeomagneticMap(wmm, altitude, threshold + saaValidationThreshold, saaValidationWidth);
238 		
239 		CustomEventHandler handler = new CustomEventHandler();
240 		MagneticFieldDetector detector = new MagneticFieldDetector(threshold, FieldModel.WMM, earth).withHandler(handler);
241 		propagator.addEventDetector(detector);
242 		propagator.propagate(initialDate, initialDate.shiftedBy(864000));
243 		
244 		checkEvents(handler.getEvents(), threshold, wmm, false);
245 		checkSaa(handler.getEvents(), saaIn, saaOut);
246 	}
247 	
248 	/**
249 	 * Test for the SAA detector based on the IGRF at sea level
250 	 */
251 	@Test
252 	public void saaDetectorIGRFTrueAltitudeTest() {
253 		initializePropagator();
254 		
255 		double altitude = 600000;
256 		double threshold = 23000;
257 		
258 		PolygonsSet saaIn = generateGeomagneticMap(igrf, altitude, threshold - saaValidationThreshold, saaValidationWidth);
259 		PolygonsSet saaOut = generateGeomagneticMap(igrf, altitude, threshold + saaValidationThreshold, saaValidationWidth);
260 		
261 		CustomEventHandler handler = new CustomEventHandler();
262 		MagneticFieldDetector detector = new MagneticFieldDetector(threshold, FieldModel.IGRF, earth).withHandler(handler);
263 		propagator.addEventDetector(detector);
264 		propagator.propagate(initialDate, initialDate.shiftedBy(864000));
265 		
266 		checkEvents(handler.getEvents(), threshold, igrf, false);
267 		checkSaa(handler.getEvents(), saaIn, saaOut);
268 	}
269 	
270 	/**
271 	 * Build 2 geographical zones, one just bigger than SAA, and another one just smaller than SAA
272 	 * @param model the geomagnetic field model
273 	 * @param threshold the value of magnetic field at sea level set in the MagneticFieldDetector
274 	 */
275 	private PolygonsSet generateGeomagneticMap(GeoMagneticField field, double altitude, double threshold, double width) {
276 		
277 		//Find a polygon corresponding to the threshold field line
278 		
279 		ArrayList<Double[]> points = new ArrayList<Double[]>();
280 		
281 		for(int latitude = -89; latitude < 90; latitude++) {
282 			for(int longitude = -179; longitude < 180; longitude++) {
283 				
284 				//Check the magnetic field value at sea level for each latitude and longitude, with a 1° step
285 				double value = field.calculateField(FastMath.toRadians(latitude), FastMath.toRadians(longitude), altitude).getTotalIntensity();
286 				
287 				//add the vertice to the outside polygon
288 				if (value - threshold > -0.5*width && value - threshold < 0.5*width) {
289 					Double[] point = {(double)latitude, (double)longitude};
290 					points.add(point);
291 				}
292 			}
293 		}
294 		
295 		//Convert lists into arrays
296 		double[][] arrayPoints = new double[points.size()][2];
297 		
298 		for(int i = 0; i < points.size(); i++) {
299 			arrayPoints[i][0] = points.get(i)[0];
300 			arrayPoints[i][1] = points.get(i)[1];
301 		}
302 		
303 		//Build the geographical zones
304 		return buildZone(arrayPoints);
305 	}
306 	
307 	/**
308 	 * Build a geographical zone with a given list of vertices
309 	 * @param points the list of vertices
310 	 * @return the polygon defining the geographical zone
311 	 */
312 	private PolygonsSet buildZone(double[][] points) {
313 		//Convert arrays to a Vector2D list
314         final Vector2D[] vertices = new Vector2D[points.length];
315         for (int i = 0; i < points.length; ++i) {
316             vertices[i] = new Vector2D(FastMath.toRadians(points[i][1]), // points[i][1] is longitude
317                                       FastMath.toRadians(points[i][0])); // points[i][0] is latitude
318         }
319         
320         //Sort the vertices to build the polygon
321         final Vector2D[] sortedVertices = sortVertices(vertices);
322 
323         //Creates the polygon
324         return new PolygonsSet(1.0e-10, sortedVertices);
325     }
326 	
327 	/**
328 	 * Sort the vertices to have a almost circular polygon, fitting the SAA shape
329 	 * the vertices are sorted by their angular position around the SAA median point.
330 	 * this angular position is defined by the angle between the vertice and the horizontal vector, regarded from the median point of the polygon
331 	 * This sort is done to creates the polygon with the right order of vertices.
332 	 * @param vertices the unsorted list of vertices
333 	 * @return the sorted list of vertices
334 	 */
335 	private Vector2D[] sortVertices(Vector2D[] vertices) {
336 		
337 		//Get the median longitude and latitude of SAA
338 
339 		double midLat = 0;
340 		double midLon = 0;
341 
342 		double minLon = vertices[0].getX();
343 		double maxLon = vertices[0].getX();
344 		double minLat = vertices[0].getY();
345 		double maxLat = vertices[0].getY();
346 		for(int i = 0; i < vertices.length; i++) {
347 			if(vertices[i].getX() < minLon) minLon = vertices[i].getX();
348 			if(vertices[i].getX() > maxLon) maxLon = vertices[i].getX();
349 			if(vertices[i].getY() < minLat) minLat = vertices[i].getY();
350 			if(vertices[i].getY() > maxLat) maxLat = vertices[i].getY();
351 		}
352 		midLat = (minLat + maxLat)/2;
353 		midLon = (minLon + maxLon)/2;
354 
355 		Vector2D mid = new Vector2D(midLon, midLat);
356 		
357 		// Convert lon/lat vector in norm/angle defined from the center of the SAA				
358 		ArrayList<Vector2D> angularVerticesList = new ArrayList<Vector2D>();
359 		Vector2D ref = new Vector2D(1, 0);
360 		Vector2D angularVertice;
361 		Vector2D centeredVertice;
362 		for(int i = 0; i < vertices.length; i++) {
363 			centeredVertice = vertices[i].subtract(mid);
364 			double norm = centeredVertice.getNorm();
365 			double angle = Vector2D.angle(ref, centeredVertice);
366 			if(centeredVertice.getY() < 0) {
367 				angle = -angle;
368 			}
369 			angularVertice = new Vector2D(norm, angle);
370 			angularVerticesList.add(angularVertice);
371 		}
372 		
373 		//Sort the norm/angle vectors by their angle
374 		Comparator<Vector2D> comparator = new Comparator<Vector2D>() {
375 
376 			@Override
377 			public int compare(Vector2D o1, Vector2D o2) {
378 				return Double.compare(o1.getY(),o2.getY());
379 			}			
380 		};
381 		
382 		angularVerticesList.sort(comparator);
383 		
384 		//Convert back the norm/angle to lon/lat vectors		
385 		Vector2D[] sortedVertices = new Vector2D[vertices.length];
386 		Vector2D originalVertice;
387 		for(int i = 0; i < vertices.length; i++) {
388 			angularVertice = angularVerticesList.get(i);
389 			double longitude = angularVertice.getX()*Math.cos(angularVertice.getY());
390 			double latitude = angularVertice.getX()*Math.sin(angularVertice.getY());
391 			centeredVertice = new Vector2D(longitude, latitude);
392 			originalVertice = centeredVertice.add(mid);
393 			sortedVertices[i] = originalVertice;
394 		}
395 				
396 		return sortedVertices;
397 	}
398 	
399 	/**
400 	 * the custom event handler used in tests to check if the event detected is located between the inside and the outside geographical zones
401 	 * @author Romaric Her
402 	 */
403 	private class CustomEventHandler implements EventHandler<EventDetector>{
404 		
405 		ArrayList<SpacecraftState> events = new ArrayList<SpacecraftState>();
406 		
407 		@Override
408 		public Action eventOccurred(SpacecraftState s, EventDetector detector, boolean increasing) {
409 			
410 			events.add(s);
411 			
412 			return Action.CONTINUE;
413 		}				
414 		
415 		public ArrayList<SpacecraftState> getEvents(){
416 			return events;
417 		}
418 	}
419 	
420 	/**
421 	 * Check that the magnetic field value at the event is equals to the expected value
422 	 * @param events the list of events
423 	 * @param threshold the expected value
424 	 * @param field the magnetic field model
425 	 * @param sea if the magnetic field is computed at sea level or at the satellite altitude
426 	 */
427 	private void checkEvents(ArrayList<SpacecraftState> events, double threshold, GeoMagneticField field, boolean sea) {
428 				
429 		for (SpacecraftState s : events) {			
430 			//Get the geodetic point corresponding to the event
431 			GeodeticPoint geo = earth.transform(s.getPVCoordinates().getPosition(), s.getFrame(), s.getDate());
432 			double altitude = geo.getAltitude();
433 			if(sea) {
434 				altitude = 0;
435 			}
436 			double meas = field.calculateField(geo.getLatitude(), geo.getLongitude(), altitude).getTotalIntensity();
437 			Assert.assertEquals(threshold, meas, 1e-3);
438 		}
439 	}
440 	
441 	/**
442 	 * Check that the events correspond to the SAA
443 	 * @param events the events to check
444 	 * @param saaIn polygon defining the inside limit of SAA
445 	 * @param saaOut polygon defining the outside limit of SAA 
446 	 */
447 	private void checkSaa(ArrayList<SpacecraftState> events, PolygonsSet saaIn, PolygonsSet saaOut) {
448 		
449 		for (SpacecraftState s : events) {
450 			//Get the geodetic point corresponding to the event
451 			GeodeticPoint geo = earth.transform(s.getPVCoordinates(itrf).getPosition(), itrf, s.getDate());
452 			Vector2D point = new Vector2D(geo.getLongitude(), geo.getLatitude());
453 
454 			//Check that the event is outside the "smaller than SAA" geographical zone
455 			Assert.assertTrue(saaIn.checkPoint(point).equals(Location.OUTSIDE));
456 			//Check that the event is inside the "bigger than SAA" geographical zone
457 			Assert.assertTrue(saaOut.checkPoint(point).equals(Location.INSIDE));
458 
459 		}
460 	}
461 }