From 6f4f47d1cc4925dea6705eececb2e20b0a8931eb Mon Sep 17 00:00:00 2001 From: Samuel Swanner Date: Sun, 30 Aug 2015 09:38:30 -0500 Subject: [PATCH 1/4] Adding option to display Waypoint Coordinates. --- source/WaypointManager/Config.cs | 4 +- source/WaypointManager/Util.cs | 630 +++++----- .../WaypointManager/WaypointFlightRenderer.cs | 1082 +++++++++-------- 3 files changed, 873 insertions(+), 843 deletions(-) diff --git a/source/WaypointManager/Config.cs b/source/WaypointManager/Config.cs index b94b945..0dd2e16 100644 --- a/source/WaypointManager/Config.cs +++ b/source/WaypointManager/Config.cs @@ -50,6 +50,7 @@ public enum WaypointDisplay public static bool hudTime = true; public static bool hudHeading = false; public static bool hudAngle = false; + public static bool hudCoordinates = false; public static bool useStockToolbar = true; @@ -80,7 +81,7 @@ public static void Save() configNode.AddValue("hudAngle", hudAngle); configNode.AddValue("useStockToolbar", useStockToolbar); configNode.AddValue("opacity", opacity); - + configNode.AddValue("hudCoordinates", hudCoordinates); configNode.Save(ConfigFileName, "Waypoint Manager Configuration File\r\n" + "//\r\n" + @@ -111,6 +112,7 @@ public static void Load() hudDistance = Convert.ToBoolean(configNode.GetValue("hudDistance")); hudTime = Convert.ToBoolean(configNode.GetValue("hudTime")); hudHeading = Convert.ToBoolean(configNode.GetValue("hudHeading")); + hudCoordinates = Convert.ToBoolean(configNode.GetValue("hudCoordinates")); hudAngle = configNode.HasValue("hudAngle") ? Convert.ToBoolean(configNode.GetValue("hudAngle")) : false; opacity = configNode.HasValue("opacity") ? (float)Convert.ToDouble(configNode.GetValue("opacity")) : 1.0f; if (configNode.HasValue("useStockToolbar")) diff --git a/source/WaypointManager/Util.cs b/source/WaypointManager/Util.cs index 7e5896b..9424a4a 100644 --- a/source/WaypointManager/Util.cs +++ b/source/WaypointManager/Util.cs @@ -1,305 +1,325 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using UnityEngine; -using FinePrint; -using FinePrint.Utilities; -using DDSHeaders; - -namespace WaypointManager -{ - /// - /// Utility methods for WaypointManager. - /// - public static class Util - { - private static string[] UNITS = { "m", "km", "Mm", "Gm", "Tm" }; - private static Dictionary> contractIcons = new Dictionary>(); - private static float lastAlpha; - - /// - /// Gets the lateral distance in meters from the active vessel to the given waypoint. - /// - /// Activated waypoint - /// Distance in meters - public static double GetLateralDistance(WaypointData wpd) - { - Vessel v = FlightGlobals.ActiveVessel; - CelestialBody celestialBody = v.mainBody; - - // Use the haversine formula to calculate great circle distance. - double sin1 = Math.Sin(Math.PI / 180.0 * (v.latitude - wpd.waypoint.latitude) / 2); - double sin2 = Math.Sin(Math.PI / 180.0 * (v.longitude - wpd.waypoint.longitude) / 2); - double cos1 = Math.Cos(Math.PI / 180.0 * wpd.waypoint.latitude); - double cos2 = Math.Cos(Math.PI / 180.0 * v.latitude); - - return 2 * (celestialBody.Radius + wpd.waypoint.height + wpd.waypoint.altitude) * - Math.Asin(Math.Sqrt(sin1 * sin1 + cos1 * cos2 * sin2 * sin2)); - } - - /// - /// Gets the distance in meters from the active vessel to the given waypoint. - /// - /// Activated waypoint - /// Distance in meters - public static double GetDistanceToWaypoint(WaypointData wpd) - { - Vessel v = FlightGlobals.ActiveVessel; - CelestialBody celestialBody = v.mainBody; - - // Simple distance - if (Config.distanceCalcMethod == Config.DistanceCalcMethod.STRAIGHT_LINE || celestialBody != wpd.celestialBody) - { - return GetStraightDistance(wpd); - } - - // Use the haversine formula to calculate great circle distance. - double sin1 = Math.Sin(Math.PI / 180.0 * (v.latitude - wpd.waypoint.latitude) / 2); - double sin2 = Math.Sin(Math.PI / 180.0 * (v.longitude - wpd.waypoint.longitude) / 2); - double cos1 = Math.Cos(Math.PI / 180.0 * wpd.waypoint.latitude); - double cos2 = Math.Cos(Math.PI / 180.0 * v.latitude); - - double lateralDist = 2 * (celestialBody.Radius + wpd.waypoint.height + wpd.waypoint.altitude) * - Math.Asin(Math.Sqrt(sin1 * sin1 + cos1 * cos2 * sin2 * sin2)); - double heightDist = Math.Abs(wpd.waypoint.altitude + wpd.waypoint.height - v.altitude); - - if (Config.distanceCalcMethod == Config.DistanceCalcMethod.LATERAL || heightDist <= lateralDist / 2.0) - { - return lateralDist; - } - else - { - // Get the ratio to use in our formula - double x = (heightDist - lateralDist / 2.0) / lateralDist; - - // x / (x + 1) starts at 0 when x = 0, and increases to 1 - return (x / (x + 1)) * heightDist + lateralDist; - } - } - - public static double GetStraightDistance(WaypointData wpd) - { - Vessel v = FlightGlobals.ActiveVessel; - - Vector3 wpPosition = wpd.celestialBody.GetWorldSurfacePosition(wpd.waypoint.latitude, wpd.waypoint.longitude, wpd.waypoint.height + wpd.waypoint.altitude); - return Vector3.Distance(wpPosition, v.transform.position); - } - - /// - /// Gets the printable distance to the waypoint. - /// - /// WaypointData object - /// The distance and unit for screen output - public static string PrintDistance(WaypointData wpd) - { - int unit = 0; - double distance = wpd.distanceToActive; - while (unit < 4 && distance >= 10000.0) - { - distance /= 1000.0; - unit++; - } - - return distance.ToString("N1") + " " + UNITS[unit]; - } - - /// - /// Gets the celestial body for the given name. - /// - /// Name of the celestial body - /// The CelestialBody object - public static CelestialBody GetBody(string name) - { - CelestialBody body = FlightGlobals.Bodies.Where(b => b.name == name).FirstOrDefault(); - if (body == null) - { - Debug.LogWarning("Couldn't find celestial body with name '" + name + "'."); - } - return body; - } - - /// - /// Checks if the given waypoint is the nav waypoint. - /// - /// - /// - public static bool IsNavPoint(Waypoint waypoint) - { - NavWaypoint navPoint = FinePrint.WaypointManager.navWaypoint; - if (navPoint == null || !FinePrint.WaypointManager.navIsActive()) - { - return false; - } - - return navPoint.latitude == waypoint.latitude && navPoint.longitude == waypoint.longitude; - } - - /// - /// Gets the contract icon for the given id and seed (color). - /// - /// URL of the icon - /// Seed to use for generating the color - /// The texture - public static Texture2D GetContractIcon(string url, int seed) - { - // Check cache for texture - Texture2D texture; - Color color = SystemUtilities.RandomColor(seed, 1.0f, 1.0f, 1.0f); - if (!contractIcons.ContainsKey(url)) - { - contractIcons[url] = new Dictionary(); - } - if (!contractIcons[url].ContainsKey(color)) - { - Texture2D baseTexture = ContractDefs.textures[url]; - - try - { - Texture2D loadedTexture = null; - string path = (url.Contains('/') ? "GameData/" : "GameData/Squad/Contracts/Icons/") + url; - // PNG loading - if (File.Exists(path + ".png")) - { - path += ".png"; - loadedTexture = new Texture2D(baseTexture.width, baseTexture.height, TextureFormat.RGBA32, false); - loadedTexture.LoadImage(File.ReadAllBytes(path.Replace('/', Path.DirectorySeparatorChar))); - } - // DDS loading - else if (File.Exists(path + ".dds")) - { - path += ".dds"; - BinaryReader br = new BinaryReader(new MemoryStream(File.ReadAllBytes(path))); - - if (br.ReadUInt32() != DDSValues.uintMagic) - { - throw new Exception("Format issue with DDS texture '" + path + "'!"); - } - DDSHeader ddsHeader = new DDSHeader(br); - if (ddsHeader.ddspf.dwFourCC == DDSValues.uintDX10) - { - DDSHeaderDX10 ddsHeaderDx10 = new DDSHeaderDX10(br); - } - - TextureFormat texFormat; - if (ddsHeader.ddspf.dwFourCC == DDSValues.uintDXT1) - { - texFormat = UnityEngine.TextureFormat.DXT1; - } - else if (ddsHeader.ddspf.dwFourCC == DDSValues.uintDXT3) - { - texFormat = UnityEngine.TextureFormat.DXT1 | UnityEngine.TextureFormat.Alpha8; - } - else if (ddsHeader.ddspf.dwFourCC == DDSValues.uintDXT5) - { - texFormat = UnityEngine.TextureFormat.DXT5; - } - else - { - throw new Exception("Unhandled DDS format!"); - } - - loadedTexture = new Texture2D((int)ddsHeader.dwWidth, (int)ddsHeader.dwHeight, texFormat, false); - loadedTexture.LoadRawTextureData(br.ReadBytes((int)(br.BaseStream.Length - br.BaseStream.Position))); - } - else - { - throw new Exception("Couldn't find file for icon '" + url + "'"); - } - - Color[] pixels = loadedTexture.GetPixels(); - for (int i = 0; i < pixels.Length; i++) - { - pixels[i] *= color; - } - texture = new Texture2D(baseTexture.width, baseTexture.height, TextureFormat.RGBA32, false); - texture.SetPixels(pixels); - texture.Apply(false, false); - contractIcons[url][color] = texture; - UnityEngine.Object.Destroy(loadedTexture); - } - catch (Exception e) - { - Debug.LogError("WaypointManager: Couldn't create texture for '" + url + "'!"); - Debug.LogException(e); - texture = contractIcons[url][color] = baseTexture; - } - } - else - { - texture = contractIcons[url][color]; - } - - return texture; - } - - public static double WaypointHeight(Waypoint w, CelestialBody body) - { - return TerrainHeight(w.latitude, w.longitude, body); - } - - public static double TerrainHeight(double latitude, double longitude, CelestialBody body) - { - // Not sure when this happens - for Sun and Jool? - if (body.pqsController == null) - { - return 0; - } - - // Figure out the terrain height - double latRads = Math.PI / 180.0 * latitude; - double lonRads = Math.PI / 180.0 * longitude; - Vector3d radialVector = new Vector3d(Math.Cos(latRads) * Math.Cos(lonRads), Math.Sin(latRads), Math.Cos(latRads) * Math.Sin(lonRads)); - return Math.Max(body.pqsController.GetSurfaceHeight(radialVector) - body.pqsController.radius, 0.0); - } - - public static void DrawWaypoint(CelestialBody targetBody, double latitude, double longitude, double altitude, string id, int seed, float alpha = -1.0f) - { - // Translate to screen position - Vector3d localSpacePoint = targetBody.GetWorldSurfacePosition(latitude, longitude, altitude); - Vector3d scaledSpacePoint = ScaledSpace.LocalToScaledSpace(localSpacePoint); - Vector3 screenPos = MapView.MapCamera.camera.WorldToScreenPoint(new Vector3((float)scaledSpacePoint.x, (float)scaledSpacePoint.y, (float)scaledSpacePoint.z)); - - // Don't draw if it's behind the camera - Camera camera = MapView.MapIsEnabled ? PlanetariumCamera.Camera : FlightCamera.fetch.mainCamera; - Vector3 cameraPos = ScaledSpace.ScaledToLocalSpace(camera.transform.position); - if (Vector3d.Dot(camera.transform.forward, scaledSpacePoint.normalized) < 0.0) - { - return; - } - - // Draw the marker at half-resolution (30 x 45) - that seems to match the one in the map view - Rect markerRect = new Rect(screenPos.x - 15f, (float)Screen.height - screenPos.y - 45.0f, 30f, 45f); - - // Half-res for the icon too (16 x 16) - Rect iconRect = new Rect(screenPos.x - 8f, (float)Screen.height - screenPos.y - 39.0f, 16f, 16f); - - if (alpha < 0.0f) - { - bool occluded = WaypointData.IsOccluded(targetBody, cameraPos, localSpacePoint, altitude); - float desiredAlpha = occluded ? 0.3f : 1.0f * Config.opacity; - if (lastAlpha < 0.0f) - { - lastAlpha = desiredAlpha; - } - else if (lastAlpha < desiredAlpha) - { - lastAlpha = Mathf.Clamp(lastAlpha + Time.deltaTime * 4f, lastAlpha, desiredAlpha); - } - else - { - lastAlpha = Mathf.Clamp(lastAlpha - Time.deltaTime * 4f, desiredAlpha, lastAlpha); - } - alpha = lastAlpha; - } - - // Draw the marker - Graphics.DrawTexture(markerRect, GameDatabase.Instance.GetTexture("Squad/Contracts/Icons/marker", false), new Rect(0.0f, 0.0f, 1f, 1f), 0, 0, 0, 0, new Color(0.5f, 0.5f, 0.5f, 0.5f * (alpha - 0.3f) / 0.7f)); - - // Draw the icon - Graphics.DrawTexture(iconRect, ContractDefs.textures[id], new Rect(0.0f, 0.0f, 1f, 1f), 0, 0, 0, 0, SystemUtilities.RandomColor(seed, alpha)); - } - - } -} +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using UnityEngine; +using FinePrint; +using FinePrint.Utilities; +using DDSHeaders; + +namespace WaypointManager +{ + /// + /// Utility methods for WaypointManager. + /// + public static class Util + { + private static string[] UNITS = { "m", "km", "Mm", "Gm", "Tm" }; + private static Dictionary> contractIcons = new Dictionary>(); + private static float lastAlpha; + + /// + /// Gets the lateral distance in meters from the active vessel to the given waypoint. + /// + /// Activated waypoint + /// Distance in meters + public static double GetLateralDistance(WaypointData wpd) + { + Vessel v = FlightGlobals.ActiveVessel; + CelestialBody celestialBody = v.mainBody; + + // Use the haversine formula to calculate great circle distance. + double sin1 = Math.Sin(Math.PI / 180.0 * (v.latitude - wpd.waypoint.latitude) / 2); + double sin2 = Math.Sin(Math.PI / 180.0 * (v.longitude - wpd.waypoint.longitude) / 2); + double cos1 = Math.Cos(Math.PI / 180.0 * wpd.waypoint.latitude); + double cos2 = Math.Cos(Math.PI / 180.0 * v.latitude); + + return 2 * (celestialBody.Radius + wpd.waypoint.height + wpd.waypoint.altitude) * + Math.Asin(Math.Sqrt(sin1 * sin1 + cos1 * cos2 * sin2 * sin2)); + } + + /// + /// Gets the distance in meters from the active vessel to the given waypoint. + /// + /// Activated waypoint + /// Distance in meters + public static double GetDistanceToWaypoint(WaypointData wpd) + { + Vessel v = FlightGlobals.ActiveVessel; + CelestialBody celestialBody = v.mainBody; + + // Simple distance + if (Config.distanceCalcMethod == Config.DistanceCalcMethod.STRAIGHT_LINE || celestialBody != wpd.celestialBody) + { + return GetStraightDistance(wpd); + } + + // Use the haversine formula to calculate great circle distance. + double sin1 = Math.Sin(Math.PI / 180.0 * (v.latitude - wpd.waypoint.latitude) / 2); + double sin2 = Math.Sin(Math.PI / 180.0 * (v.longitude - wpd.waypoint.longitude) / 2); + double cos1 = Math.Cos(Math.PI / 180.0 * wpd.waypoint.latitude); + double cos2 = Math.Cos(Math.PI / 180.0 * v.latitude); + + double lateralDist = 2 * (celestialBody.Radius + wpd.waypoint.height + wpd.waypoint.altitude) * + Math.Asin(Math.Sqrt(sin1 * sin1 + cos1 * cos2 * sin2 * sin2)); + double heightDist = Math.Abs(wpd.waypoint.altitude + wpd.waypoint.height - v.altitude); + + if (Config.distanceCalcMethod == Config.DistanceCalcMethod.LATERAL || heightDist <= lateralDist / 2.0) + { + return lateralDist; + } + else + { + // Get the ratio to use in our formula + double x = (heightDist - lateralDist / 2.0) / lateralDist; + + // x / (x + 1) starts at 0 when x = 0, and increases to 1 + return (x / (x + 1)) * heightDist + lateralDist; + } + } + + public static double GetStraightDistance(WaypointData wpd) + { + Vessel v = FlightGlobals.ActiveVessel; + + Vector3 wpPosition = wpd.celestialBody.GetWorldSurfacePosition(wpd.waypoint.latitude, wpd.waypoint.longitude, wpd.waypoint.height + wpd.waypoint.altitude); + return Vector3.Distance(wpPosition, v.transform.position); + } + + /// + /// Gets the printable distance to the waypoint. + /// + /// WaypointData object + /// The distance and unit for screen output + public static string PrintDistance(WaypointData wpd) + { + int unit = 0; + double distance = wpd.distanceToActive; + while (unit < 4 && distance >= 10000.0) + { + distance /= 1000.0; + unit++; + } + + return distance.ToString("N1") + " " + UNITS[unit]; + } + + /// + /// Gets the celestial body for the given name. + /// + /// Name of the celestial body + /// The CelestialBody object + public static CelestialBody GetBody(string name) + { + CelestialBody body = FlightGlobals.Bodies.Where(b => b.name == name).FirstOrDefault(); + if (body == null) + { + Debug.LogWarning("Couldn't find celestial body with name '" + name + "'."); + } + return body; + } + + /// + /// Checks if the given waypoint is the nav waypoint. + /// + /// + /// + public static bool IsNavPoint(Waypoint waypoint) + { + NavWaypoint navPoint = FinePrint.WaypointManager.navWaypoint; + if (navPoint == null || !FinePrint.WaypointManager.navIsActive()) + { + return false; + } + + return navPoint.latitude == waypoint.latitude && navPoint.longitude == waypoint.longitude; + } + + /// + /// Gets the contract icon for the given id and seed (color). + /// + /// URL of the icon + /// Seed to use for generating the color + /// The texture + public static Texture2D GetContractIcon(string url, int seed) + { + // Check cache for texture + Texture2D texture; + Color color = SystemUtilities.RandomColor(seed, 1.0f, 1.0f, 1.0f); + if (!contractIcons.ContainsKey(url)) + { + contractIcons[url] = new Dictionary(); + } + if (!contractIcons[url].ContainsKey(color)) + { + Texture2D baseTexture = ContractDefs.textures[url]; + + try + { + Texture2D loadedTexture = null; + string path = (url.Contains('/') ? "GameData/" : "GameData/Squad/Contracts/Icons/") + url; + // PNG loading + if (File.Exists(path + ".png")) + { + path += ".png"; + loadedTexture = new Texture2D(baseTexture.width, baseTexture.height, TextureFormat.RGBA32, false); + loadedTexture.LoadImage(File.ReadAllBytes(path.Replace('/', Path.DirectorySeparatorChar))); + } + // DDS loading + else if (File.Exists(path + ".dds")) + { + path += ".dds"; + BinaryReader br = new BinaryReader(new MemoryStream(File.ReadAllBytes(path))); + + if (br.ReadUInt32() != DDSValues.uintMagic) + { + throw new Exception("Format issue with DDS texture '" + path + "'!"); + } + DDSHeader ddsHeader = new DDSHeader(br); + if (ddsHeader.ddspf.dwFourCC == DDSValues.uintDX10) + { + DDSHeaderDX10 ddsHeaderDx10 = new DDSHeaderDX10(br); + } + + TextureFormat texFormat; + if (ddsHeader.ddspf.dwFourCC == DDSValues.uintDXT1) + { + texFormat = UnityEngine.TextureFormat.DXT1; + } + else if (ddsHeader.ddspf.dwFourCC == DDSValues.uintDXT3) + { + texFormat = UnityEngine.TextureFormat.DXT1 | UnityEngine.TextureFormat.Alpha8; + } + else if (ddsHeader.ddspf.dwFourCC == DDSValues.uintDXT5) + { + texFormat = UnityEngine.TextureFormat.DXT5; + } + else + { + throw new Exception("Unhandled DDS format!"); + } + + loadedTexture = new Texture2D((int)ddsHeader.dwWidth, (int)ddsHeader.dwHeight, texFormat, false); + loadedTexture.LoadRawTextureData(br.ReadBytes((int)(br.BaseStream.Length - br.BaseStream.Position))); + } + else + { + throw new Exception("Couldn't find file for icon '" + url + "'"); + } + + Color[] pixels = loadedTexture.GetPixels(); + for (int i = 0; i < pixels.Length; i++) + { + pixels[i] *= color; + } + texture = new Texture2D(baseTexture.width, baseTexture.height, TextureFormat.RGBA32, false); + texture.SetPixels(pixels); + texture.Apply(false, false); + contractIcons[url][color] = texture; + UnityEngine.Object.Destroy(loadedTexture); + } + catch (Exception e) + { + Debug.LogError("WaypointManager: Couldn't create texture for '" + url + "'!"); + Debug.LogException(e); + texture = contractIcons[url][color] = baseTexture; + } + } + else + { + texture = contractIcons[url][color]; + } + + return texture; + } + + public static double WaypointHeight(Waypoint w, CelestialBody body) + { + return TerrainHeight(w.latitude, w.longitude, body); + } + + public static double TerrainHeight(double latitude, double longitude, CelestialBody body) + { + // Not sure when this happens - for Sun and Jool? + if (body.pqsController == null) + { + return 0; + } + + // Figure out the terrain height + double latRads = Math.PI / 180.0 * latitude; + double lonRads = Math.PI / 180.0 * longitude; + Vector3d radialVector = new Vector3d(Math.Cos(latRads) * Math.Cos(lonRads), Math.Sin(latRads), Math.Cos(latRads) * Math.Sin(lonRads)); + return Math.Max(body.pqsController.GetSurfaceHeight(radialVector) - body.pqsController.radius, 0.0); + } + + public static void DrawWaypoint(CelestialBody targetBody, double latitude, double longitude, double altitude, string id, int seed, float alpha = -1.0f) + { + // Translate to screen position + Vector3d localSpacePoint = targetBody.GetWorldSurfacePosition(latitude, longitude, altitude); + Vector3d scaledSpacePoint = ScaledSpace.LocalToScaledSpace(localSpacePoint); + Vector3 screenPos = MapView.MapCamera.camera.WorldToScreenPoint(new Vector3((float)scaledSpacePoint.x, (float)scaledSpacePoint.y, (float)scaledSpacePoint.z)); + + // Don't draw if it's behind the camera + Camera camera = MapView.MapIsEnabled ? PlanetariumCamera.Camera : FlightCamera.fetch.mainCamera; + Vector3 cameraPos = ScaledSpace.ScaledToLocalSpace(camera.transform.position); + if (Vector3d.Dot(camera.transform.forward, scaledSpacePoint.normalized) < 0.0) + { + return; + } + + // Draw the marker at half-resolution (30 x 45) - that seems to match the one in the map view + Rect markerRect = new Rect(screenPos.x - 15f, (float)Screen.height - screenPos.y - 45.0f, 30f, 45f); + + // Half-res for the icon too (16 x 16) + Rect iconRect = new Rect(screenPos.x - 8f, (float)Screen.height - screenPos.y - 39.0f, 16f, 16f); + + if (alpha < 0.0f) + { + bool occluded = WaypointData.IsOccluded(targetBody, cameraPos, localSpacePoint, altitude); + float desiredAlpha = occluded ? 0.3f : 1.0f * Config.opacity; + if (lastAlpha < 0.0f) + { + lastAlpha = desiredAlpha; + } + else if (lastAlpha < desiredAlpha) + { + lastAlpha = Mathf.Clamp(lastAlpha + Time.deltaTime * 4f, lastAlpha, desiredAlpha); + } + else + { + lastAlpha = Mathf.Clamp(lastAlpha - Time.deltaTime * 4f, desiredAlpha, lastAlpha); + } + alpha = lastAlpha; + } + + // Draw the marker + Graphics.DrawTexture(markerRect, GameDatabase.Instance.GetTexture("Squad/Contracts/Icons/marker", false), new Rect(0.0f, 0.0f, 1f, 1f), 0, 0, 0, 0, new Color(0.5f, 0.5f, 0.5f, 0.5f * (alpha - 0.3f) / 0.7f)); + + // Draw the icon + Graphics.DrawTexture(iconRect, ContractDefs.textures[id], new Rect(0.0f, 0.0f, 1f, 1f), 0, 0, 0, 0, SystemUtilities.RandomColor(seed, alpha)); + } + + /// + /// Converts decimal degrees to a string of DMS formatted degrees with N/S, E/W prefix + /// + /// + /// boolean to determin latitude or longitude for compass prefix + /// + public static string DecimalDegreesToDMS(double decimalDegrees, bool latitude) + { + string dms = string.Empty; + string direction = string.Empty; + int d = Math.Abs((int)(decimalDegrees)); + double decimalpart = Math.Abs(decimalDegrees - d); + int m = (int)(decimalpart * 60); + double s = (decimalpart - m / 60f) * 3600; + if (latitude) direction = decimalDegrees >= 0 ? "N" : "S"; + else direction = decimalDegrees >= 0 ? "E" : "W"; + dms = string.Format("{3} {0}\x00B0{1}\'{2:F1}\"", d, m, s, direction); + return dms; + } + + } +} diff --git a/source/WaypointManager/WaypointFlightRenderer.cs b/source/WaypointManager/WaypointFlightRenderer.cs index 88851f1..16b1311 100644 --- a/source/WaypointManager/WaypointFlightRenderer.cs +++ b/source/WaypointManager/WaypointFlightRenderer.cs @@ -1,537 +1,545 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using UnityEngine; -using KSP; -using Contracts; -using FinePrint; -using FinePrint.Utilities; - -namespace WaypointManager -{ - [KSPAddon(KSPAddon.Startup.SpaceCentre, true)] - class WaypointFlightRenderer : MonoBehaviour - { - private GUIStyle nameStyle = null; - private GUIStyle valueStyle = null; - private GUIStyle hintTextStyle = null; - - private bool visible = true; - private Waypoint selectedWaypoint = null; - private string waypointName = ""; - private Rect windowPos; - private bool newClick = false; - private AltimeterSliderButtons asb = null; - - private float referencePos = 0.0f; - private float referenceUISize = 0.0f; - private float lastPos = 0.0f; - private bool referenceSet = false; - - private const double MIN_TIME = 300; - private const double MIN_DISTANCE = 25000; - private const double MIN_SPEED = MIN_DISTANCE / MIN_TIME; - private const double FADE_TIME = 20; - - void Start() - { - if (MapView.MapCamera.gameObject.GetComponent() == null) - { - MapView.MapCamera.gameObject.AddComponent(); - - // Destroy this object - otherwise we'll have two - Destroy(this); - } - - GameEvents.onGameSceneLoadRequested.Add(new EventData.OnEvent(OnGameSceneLoadRequested)); - GameEvents.onHideUI.Add(new EventVoid.OnEvent(OnHideUI)); - GameEvents.onShowUI.Add(new EventVoid.OnEvent(OnShowUI)); - } - - protected void OnDestroy() - { - GameEvents.onGameSceneLoadRequested.Remove(new EventData.OnEvent(OnGameSceneLoadRequested)); - GameEvents.onHideUI.Remove(OnHideUI); - GameEvents.onShowUI.Remove(OnShowUI); - } - - public void OnGameSceneLoadRequested(GameScenes gameScene) - { - asb = null; - } - - public void OnHideUI() - { - visible = false; - } - - public void OnShowUI() - { - visible = true; - } - - public void OnGUI() - { - if (visible) - { - if (Event.current.type == EventType.MouseUp && Event.current.button == 0) - { - newClick = true; - } - - if (HighLogic.LoadedSceneIsFlight || HighLogic.LoadedScene == GameScenes.TRACKSTATION) - { - // Draw the marker for custom waypoints that are currently being created - CustomWaypointGUI.DrawMarker(); - - // Draw waypoints if not in career mode - if (ContractSystem.Instance == null && MapView.MapIsEnabled) - { - foreach (WaypointData wpd in WaypointData.Waypoints) - { - if (wpd.celestialBody != null && wpd.waypoint.celestialName == wpd.celestialBody.name) - { - if (Event.current.type == EventType.Repaint) - { - wpd.SetAlpha(); - Util.DrawWaypoint(wpd.celestialBody, wpd.waypoint.latitude, wpd.waypoint.longitude, - wpd.waypoint.altitude, wpd.waypoint.id, wpd.waypoint.seed, wpd.currentAlpha); - } - - // Handling clicking on the waypoint - if (Event.current.type == EventType.MouseUp && Event.current.button == 0) - { - HandleClick(wpd); - } - - // Draw hint text - if (Event.current.type == EventType.Repaint) - { - HintText(wpd); - } - } - } - } - } - - if (HighLogic.LoadedSceneIsFlight && !MapView.MapIsEnabled) - { - SetupStyles(); - - WaypointData.CacheWaypointData(); - - foreach (WaypointData wpd in WaypointData.Waypoints) - { - DrawWaypoint(wpd); - } - } - - if (HighLogic.LoadedSceneIsFlight && (!MapView.MapIsEnabled || ContractSystem.Instance == null)) - { - ShowNavigationWindow(); - } - } - } - - // Styles taken directly from Kerbal Engineer Redux - because they look great and this will - // make our display consistent with that - protected void SetupStyles() - { - if (nameStyle != null) - { - return; - } - - nameStyle = new GUIStyle(HighLogic.Skin.label) - { - normal = - { - textColor = Color.white - }, - margin = new RectOffset(), - padding = new RectOffset(5, 0, 0, 0), - alignment = TextAnchor.MiddleRight, - fontSize = 11, - fontStyle = FontStyle.Bold, - fixedHeight = 20.0f - }; - - valueStyle = new GUIStyle(HighLogic.Skin.label) - { - margin = new RectOffset(), - padding = new RectOffset(0, 5, 0, 0), - alignment = TextAnchor.MiddleLeft, - fontSize = 11, - fontStyle = FontStyle.Normal, - fixedHeight = 20.0f - }; - - hintTextStyle = new GUIStyle(HighLogic.Skin.box) - { - padding = new RectOffset(4, 4, 7, 4), - font = MapView.OrbitIconsTextSkin.label.font, - fontSize = MapView.OrbitIconsTextSkin.label.fontSize, - fontStyle = MapView.OrbitIconsTextSkin.label.fontStyle, - fixedWidth = 0, - fixedHeight = 0, - stretchHeight = true, - stretchWidth = true - }; - } - - protected void DrawWaypoint(WaypointData wpd) - { - // Not our planet - CelestialBody celestialBody = FlightGlobals.currentMainBody; - if (celestialBody == null || wpd.waypoint.celestialName != celestialBody.name) - { - return; - } - - // Check if the waypoint should be visible - if (!wpd.waypoint.visible) - { - return; - } - - // Figure out waypoint label - string label = wpd.waypoint.name + (wpd.waypoint.isClustered ? (" " + StringUtilities.IntegerToGreek(wpd.waypoint.index)) : ""); - - // Set the alpha and do a nice fade - wpd.SetAlpha(); - - // Decide whether to actually draw the waypoint - if (FlightGlobals.ActiveVessel != null) - { - // Figure out the distance to the waypoint - Vessel v = FlightGlobals.ActiveVessel; - - // Only change alpha if the waypoint isn't the nav point - if (!Util.IsNavPoint(wpd.waypoint)) - { - // Get the distance to the waypoint at the current speed - double speed = v.srfSpeed < MIN_SPEED ? MIN_SPEED : v.srfSpeed; - double directTime = Util.GetStraightDistance(wpd) / speed; - - // More than two minutes away - if (directTime > MIN_TIME || Config.waypointDisplay != Config.WaypointDisplay.ALL) - { - return; - } - else if (directTime >= MIN_TIME - FADE_TIME) - { - wpd.currentAlpha = (float)((MIN_TIME - directTime) / FADE_TIME) * Config.opacity; - } - } - // Draw the distance information to the nav point - else - { - // Draw the distance to waypoint text - if (Event.current.type == EventType.Repaint) - { - if (asb == null) - { - asb = UnityEngine.Object.FindObjectOfType(); - } - - if (referenceUISize != ScreenSafeUI.VerticalRatio || !referenceSet) - { - referencePos = ScreenSafeUI.referenceCam.ViewportToScreenPoint(asb.transform.position).y; - referenceUISize = ScreenSafeUI.VerticalRatio; - - // Need two consistent numbers in a row to set the reference - if (lastPos == referencePos) - { - referenceSet = true; - } - else - { - lastPos = referencePos; - } - } - - float ybase = (referencePos - ScreenSafeUI.referenceCam.ViewportToScreenPoint(asb.transform.position).y + Screen.height / 11.67f) / ScreenSafeUI.VerticalRatio; - - string timeToWP = GetTimeToWaypoint(wpd); - if (Config.hudDistance) - { - GUI.Label(new Rect((float)Screen.width / 2.0f - 188f, ybase, 240f, 20f), "Distance to " + label + ":", nameStyle); - GUI.Label(new Rect((float)Screen.width / 2.0f + 60f, ybase, 120f, 20f), - v.state != Vessel.State.DEAD ? Util.PrintDistance(wpd) : "N/A", valueStyle); - ybase += 18f; - } - - if (timeToWP != null && Config.hudTime) - { - GUI.Label(new Rect((float)Screen.width / 2.0f - 188f, ybase, 240f, 20f), "ETA to " + label + ":", nameStyle); - GUI.Label(new Rect((float)Screen.width / 2.0f + 60f, ybase, 120f, 20f), - v.state != Vessel.State.DEAD ? timeToWP : "N/A", valueStyle); - ybase += 18f; - } - - if (Config.hudHeading) - { - GUI.Label(new Rect((float)Screen.width / 2.0f - 188f, ybase, 240f, 20f), "Heading to " + label + ":", nameStyle); - GUI.Label(new Rect((float)Screen.width / 2.0f + 60f, ybase, 120f, 20f), - v.state != Vessel.State.DEAD ? wpd.heading.ToString("N1") : "N/A", valueStyle); - ybase += 18f; - } - - if (Config.hudAngle && v.mainBody == wpd.celestialBody) - { - double distance = Util.GetLateralDistance(wpd); - double heightDist = wpd.waypoint.altitude + wpd.waypoint.height - v.altitude; - double angle = Math.Atan2(heightDist, distance) * 180.0 / Math.PI; - - GUI.Label(new Rect((float)Screen.width / 2.0f - 188f, ybase, 240f, 20f), "Angle to " + label + ":", nameStyle); - GUI.Label(new Rect((float)Screen.width / 2.0f + 60f, ybase, 120f, 20f), - v.state != Vessel.State.DEAD ? angle.ToString("N2") : "N/A", valueStyle); - ybase += 18f; - - if (v.srfSpeed >= 0.1) - { - double velAngle = 90 - Math.Acos(Vector3d.Dot(v.srf_velocity.normalized, v.upAxis)) * 180.0 / Math.PI; - - GUI.Label(new Rect((float)Screen.width / 2.0f - 188f, ybase, 240f, 20f), "Velocity pitch angle:", nameStyle); - GUI.Label(new Rect((float)Screen.width / 2.0f + 60f, ybase, 120f, 20f), - v.state != Vessel.State.DEAD ? velAngle.ToString("N2") : "N/A", valueStyle); - ybase += 18f; - } - } - } - } - } - - // Don't draw the waypoint - if (Config.waypointDisplay == Config.WaypointDisplay.NONE) - { - return; - } - - // Translate to scaled space - Vector3d localSpacePoint = celestialBody.GetWorldSurfacePosition(wpd.waypoint.latitude, wpd.waypoint.longitude, wpd.waypoint.height + wpd.waypoint.altitude); - Vector3d scaledSpacePoint = ScaledSpace.LocalToScaledSpace(localSpacePoint); - - // Don't draw if it's behind the camera - if (Vector3d.Dot(MapView.MapCamera.camera.transform.forward, scaledSpacePoint.normalized) < 0.0) - { - return; - } - - // Translate to screen position - Vector3 screenPos = MapView.MapCamera.camera.WorldToScreenPoint(new Vector3((float)scaledSpacePoint.x, (float)scaledSpacePoint.y, (float)scaledSpacePoint.z)); - - // Draw the marker at half-resolution (30 x 45) - that seems to match the one in the map view - Rect markerRect = new Rect(screenPos.x - 15f, (float)Screen.height - screenPos.y - 45.0f, 30f, 45f); - - // Set the window position relative to the selected waypoint - if (selectedWaypoint == wpd.waypoint) - { - windowPos = new Rect(markerRect.xMin - 97, markerRect.yMax + 12, 224, 60); - } - - // Handling clicking on the waypoint - if (Event.current.type == EventType.MouseUp && Event.current.button == 0) - { - if (markerRect.Contains(Event.current.mousePosition)) - { - selectedWaypoint = wpd.waypoint; - windowPos = new Rect(markerRect.xMin - 97, markerRect.yMax + 12, 224, 60); - waypointName = label; - newClick = false; - } - else if (newClick) - { - selectedWaypoint = null; - } - } - - // Only handle on repaint events - if (Event.current.type == EventType.Repaint) - { - // Half-res for the icon too (16 x 16) - Rect iconRect = new Rect(screenPos.x - 8f, (float)Screen.height - screenPos.y - 39.0f, 16f, 16f); - - // Draw the marker - Graphics.DrawTexture(markerRect, GameDatabase.Instance.GetTexture("Squad/Contracts/Icons/marker", false), new Rect(0.0f, 0.0f, 1f, 1f), 0, 0, 0, 0, new Color(0.5f, 0.5f, 0.5f, 0.5f * (wpd.currentAlpha - 0.3f) / 0.7f)); - - // Draw the icon, but support blinking - if (!Util.IsNavPoint(wpd.waypoint) || !FinePrint.WaypointManager.navWaypoint.blinking || (int)((Time.fixedTime - (int)Time.fixedTime) * 4) % 2 == 0) - { - Graphics.DrawTexture(iconRect, ContractDefs.textures[wpd.waypoint.id], new Rect(0.0f, 0.0f, 1f, 1f), 0, 0, 0, 0, SystemUtilities.RandomColor(wpd.waypoint.seed, wpd.currentAlpha)); - } - - // Hint text! - if (iconRect.Contains(Event.current.mousePosition)) - { - // Add agency to label - if (wpd.waypoint.contractReference != null) - { - label += "\n" + wpd.waypoint.contractReference.Agent.Name; - } - float width = 240f; - float height = hintTextStyle.CalcHeight(new GUIContent(label), width); - float yoffset = height + 48.0f; - GUI.Box(new Rect(screenPos.x - width/2.0f, (float)Screen.height - screenPos.y - yoffset, width, height), label, hintTextStyle); - } - } - } - - private void ShowNavigationWindow() - { - if (selectedWaypoint != null) - { - GUI.skin = HighLogic.Skin; - windowPos = GUILayout.Window(10, windowPos, NavigationWindow, waypointName, GUILayout.MinWidth(224)); - } - } - - private void NavigationWindow(int windowID) - { - if (selectedWaypoint == null) - { - return; - } - - GUILayout.BeginVertical(); - if (!Util.IsNavPoint(selectedWaypoint)) - { - if (GUILayout.Button("Activate Navigation", HighLogic.Skin.button, GUILayout.ExpandWidth(true))) - { - FinePrint.WaypointManager.setupNavPoint(selectedWaypoint); - FinePrint.WaypointManager.activateNavPoint(); - selectedWaypoint = null; - } - } - else - { - if (GUILayout.Button("Deactivate Navigation", HighLogic.Skin.button, GUILayout.ExpandWidth(true))) - { - FinePrint.WaypointManager.clearNavPoint(); - selectedWaypoint = null; - } - - } - if (CustomWaypoints.Instance.IsCustom(selectedWaypoint)) - { - if (GUILayout.Button("Edit Custom Waypoint", HighLogic.Skin.button, GUILayout.ExpandWidth(true))) - { - CustomWaypointGUI.EditWaypoint(selectedWaypoint); - selectedWaypoint = null; - } - if (GUILayout.Button("Delete Custom Waypoint", HighLogic.Skin.button, GUILayout.ExpandWidth(true))) - { - CustomWaypointGUI.DeleteWaypoint(selectedWaypoint); - selectedWaypoint = null; - } - } - GUILayout.EndVertical(); - } - - /// - /// Calculates the time to the distance based on the vessels srfSpeed and transform it to a readable string. - /// - /// The waypoint - /// Distance in meters - /// - protected string GetTimeToWaypoint(WaypointData wpd) - { - Vessel v = FlightGlobals.ActiveVessel; - if (v.srfSpeed < 0.1) - { - return null; - } - - double time = (wpd.distanceToActive / v.horizontalSrfSpeed); - - // Earthtime - uint SecondsPerYear = 31536000; // = 365d - uint SecondsPerDay = 86400; // = 24h - uint SecondsPerHour = 3600; // = 60m - uint SecondsPerMinute = 60; // = 60s - - if (GameSettings.KERBIN_TIME == true) - { - SecondsPerYear = 9201600; // = 426d - SecondsPerDay = 21600; // = 6h - SecondsPerHour = 3600; // = 60m - SecondsPerMinute = 60; // = 60s - } - - int years = (int)(time / SecondsPerYear); - time -= years * SecondsPerYear; - - int days = (int)(time / SecondsPerDay); - time -= days * SecondsPerDay; - - int hours = (int)(time / SecondsPerHour); - time -= hours * SecondsPerHour; - - int minutes = (int)(time / SecondsPerMinute); - time -= minutes * SecondsPerMinute; - - int seconds = (int)(time); - - string output = ""; - if (years != 0) - { - output += years + "y"; - } - if (days != 0) - { - if (output.Length != 0) output += ", "; - output += days + "d"; - } - if (hours != 0 || minutes != 0 || seconds != 0 || output.Length == 0) - { - if (output.Length != 0) output += ", "; - output += hours.ToString("D2") + ":" + minutes.ToString("D2") + ":" + seconds.ToString("D2"); - } - - return output; - } - - public void HandleClick(WaypointData wpd) - { - // Translate to screen position - Vector3d localSpacePoint = wpd.celestialBody.GetWorldSurfacePosition(wpd.waypoint.latitude, wpd.waypoint.longitude, wpd.waypoint.altitude); - Vector3d scaledSpacePoint = ScaledSpace.LocalToScaledSpace(localSpacePoint); - Vector3 screenPos = MapView.MapCamera.camera.WorldToScreenPoint(new Vector3((float)scaledSpacePoint.x, (float)scaledSpacePoint.y, (float)scaledSpacePoint.z)); - - Rect markerRect = new Rect(screenPos.x - 15f, (float)Screen.height - screenPos.y - 45.0f, 30f, 45f); - - if (markerRect.Contains(Event.current.mousePosition)) - { - selectedWaypoint = wpd.waypoint; - windowPos = new Rect(markerRect.xMin - 97, markerRect.yMax + 12, 224, 60); - waypointName = wpd.waypoint.name + (wpd.waypoint.isClustered ? (" " + StringUtilities.IntegerToGreek(wpd.waypoint.index)) : ""); - newClick = false; - } - else if (newClick) - { - selectedWaypoint = null; - } - } - - public void HintText(WaypointData wpd) - { - // Translate to screen position - Vector3d localSpacePoint = wpd.celestialBody.GetWorldSurfacePosition(wpd.waypoint.latitude, wpd.waypoint.longitude, wpd.waypoint.altitude); - Vector3d scaledSpacePoint = ScaledSpace.LocalToScaledSpace(localSpacePoint); - Vector3 screenPos = MapView.MapCamera.camera.WorldToScreenPoint(new Vector3((float)scaledSpacePoint.x, (float)scaledSpacePoint.y, (float)scaledSpacePoint.z)); - - Rect iconRect = new Rect(screenPos.x - 8f, (float)Screen.height - screenPos.y - 39.0f, 16f, 16f); - - // Hint text! - if (iconRect.Contains(Event.current.mousePosition)) - { - string label = wpd.waypoint.name + (wpd.waypoint.isClustered ? (" " + StringUtilities.IntegerToGreek(wpd.waypoint.index)) : ""); - float width = 240f; - float height = hintTextStyle.CalcHeight(new GUIContent(label), width); - float yoffset = height + 48.0f; - GUI.Box(new Rect(screenPos.x - width / 2.0f, (float)Screen.height - screenPos.y - yoffset, width, height), label, hintTextStyle); - } - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; +using KSP; +using Contracts; +using FinePrint; +using FinePrint.Utilities; + +namespace WaypointManager +{ + [KSPAddon(KSPAddon.Startup.SpaceCentre, true)] + class WaypointFlightRenderer : MonoBehaviour + { + private GUIStyle nameStyle = null; + private GUIStyle valueStyle = null; + private GUIStyle hintTextStyle = null; + + private bool visible = true; + private Waypoint selectedWaypoint = null; + private string waypointName = ""; + private Rect windowPos; + private bool newClick = false; + private AltimeterSliderButtons asb = null; + + private float referencePos = 0.0f; + private float referenceUISize = 0.0f; + private float lastPos = 0.0f; + private bool referenceSet = false; + + private const double MIN_TIME = 300; + private const double MIN_DISTANCE = 25000; + private const double MIN_SPEED = MIN_DISTANCE / MIN_TIME; + private const double FADE_TIME = 20; + + void Start() + { + if (MapView.MapCamera.gameObject.GetComponent() == null) + { + MapView.MapCamera.gameObject.AddComponent(); + + // Destroy this object - otherwise we'll have two + Destroy(this); + } + + GameEvents.onGameSceneLoadRequested.Add(new EventData.OnEvent(OnGameSceneLoadRequested)); + GameEvents.onHideUI.Add(new EventVoid.OnEvent(OnHideUI)); + GameEvents.onShowUI.Add(new EventVoid.OnEvent(OnShowUI)); + } + + protected void OnDestroy() + { + GameEvents.onGameSceneLoadRequested.Remove(new EventData.OnEvent(OnGameSceneLoadRequested)); + GameEvents.onHideUI.Remove(OnHideUI); + GameEvents.onShowUI.Remove(OnShowUI); + } + + public void OnGameSceneLoadRequested(GameScenes gameScene) + { + asb = null; + } + + public void OnHideUI() + { + visible = false; + } + + public void OnShowUI() + { + visible = true; + } + + public void OnGUI() + { + if (visible) + { + if (Event.current.type == EventType.MouseUp && Event.current.button == 0) + { + newClick = true; + } + + if (HighLogic.LoadedSceneIsFlight || HighLogic.LoadedScene == GameScenes.TRACKSTATION) + { + // Draw the marker for custom waypoints that are currently being created + CustomWaypointGUI.DrawMarker(); + + // Draw waypoints if not in career mode + if (ContractSystem.Instance == null && MapView.MapIsEnabled) + { + foreach (WaypointData wpd in WaypointData.Waypoints) + { + if (wpd.celestialBody != null && wpd.waypoint.celestialName == wpd.celestialBody.name) + { + if (Event.current.type == EventType.Repaint) + { + wpd.SetAlpha(); + Util.DrawWaypoint(wpd.celestialBody, wpd.waypoint.latitude, wpd.waypoint.longitude, + wpd.waypoint.altitude, wpd.waypoint.id, wpd.waypoint.seed, wpd.currentAlpha); + } + + // Handling clicking on the waypoint + if (Event.current.type == EventType.MouseUp && Event.current.button == 0) + { + HandleClick(wpd); + } + + // Draw hint text + if (Event.current.type == EventType.Repaint) + { + HintText(wpd); + } + } + } + } + } + + if (HighLogic.LoadedSceneIsFlight && !MapView.MapIsEnabled) + { + SetupStyles(); + + WaypointData.CacheWaypointData(); + + foreach (WaypointData wpd in WaypointData.Waypoints) + { + DrawWaypoint(wpd); + } + } + + if (HighLogic.LoadedSceneIsFlight && (!MapView.MapIsEnabled || ContractSystem.Instance == null)) + { + ShowNavigationWindow(); + } + } + } + + // Styles taken directly from Kerbal Engineer Redux - because they look great and this will + // make our display consistent with that + protected void SetupStyles() + { + if (nameStyle != null) + { + return; + } + + nameStyle = new GUIStyle(HighLogic.Skin.label) + { + normal = + { + textColor = Color.white + }, + margin = new RectOffset(), + padding = new RectOffset(5, 0, 0, 0), + alignment = TextAnchor.MiddleRight, + fontSize = 11, + fontStyle = FontStyle.Bold, + fixedHeight = 20.0f + }; + + valueStyle = new GUIStyle(HighLogic.Skin.label) + { + margin = new RectOffset(), + padding = new RectOffset(0, 5, 0, 0), + alignment = TextAnchor.MiddleLeft, + fontSize = 11, + fontStyle = FontStyle.Normal, + fixedHeight = 20.0f + }; + + hintTextStyle = new GUIStyle(HighLogic.Skin.box) + { + padding = new RectOffset(4, 4, 7, 4), + font = MapView.OrbitIconsTextSkin.label.font, + fontSize = MapView.OrbitIconsTextSkin.label.fontSize, + fontStyle = MapView.OrbitIconsTextSkin.label.fontStyle, + fixedWidth = 0, + fixedHeight = 0, + stretchHeight = true, + stretchWidth = true + }; + } + + protected void DrawWaypoint(WaypointData wpd) + { + // Not our planet + CelestialBody celestialBody = FlightGlobals.currentMainBody; + if (celestialBody == null || wpd.waypoint.celestialName != celestialBody.name) + { + return; + } + + // Check if the waypoint should be visible + if (!wpd.waypoint.visible) + { + return; + } + + // Figure out waypoint label + string label = wpd.waypoint.name + (wpd.waypoint.isClustered ? (" " + StringUtilities.IntegerToGreek(wpd.waypoint.index)) : ""); + + // Set the alpha and do a nice fade + wpd.SetAlpha(); + + // Decide whether to actually draw the waypoint + if (FlightGlobals.ActiveVessel != null) + { + // Figure out the distance to the waypoint + Vessel v = FlightGlobals.ActiveVessel; + + // Only change alpha if the waypoint isn't the nav point + if (!Util.IsNavPoint(wpd.waypoint)) + { + // Get the distance to the waypoint at the current speed + double speed = v.srfSpeed < MIN_SPEED ? MIN_SPEED : v.srfSpeed; + double directTime = Util.GetStraightDistance(wpd) / speed; + + // More than two minutes away + if (directTime > MIN_TIME || Config.waypointDisplay != Config.WaypointDisplay.ALL) + { + return; + } + else if (directTime >= MIN_TIME - FADE_TIME) + { + wpd.currentAlpha = (float)((MIN_TIME - directTime) / FADE_TIME) * Config.opacity; + } + } + // Draw the distance information to the nav point + else + { + // Draw the distance to waypoint text + if (Event.current.type == EventType.Repaint) + { + if (asb == null) + { + asb = UnityEngine.Object.FindObjectOfType(); + } + + if (referenceUISize != ScreenSafeUI.VerticalRatio || !referenceSet) + { + referencePos = ScreenSafeUI.referenceCam.ViewportToScreenPoint(asb.transform.position).y; + referenceUISize = ScreenSafeUI.VerticalRatio; + + // Need two consistent numbers in a row to set the reference + if (lastPos == referencePos) + { + referenceSet = true; + } + else + { + lastPos = referencePos; + } + } + + float ybase = (referencePos - ScreenSafeUI.referenceCam.ViewportToScreenPoint(asb.transform.position).y + Screen.height / 11.67f) / ScreenSafeUI.VerticalRatio; + + string timeToWP = GetTimeToWaypoint(wpd); + if (Config.hudDistance) + { + GUI.Label(new Rect((float)Screen.width / 2.0f - 188f, ybase, 240f, 20f), "Distance to " + label + ":", nameStyle); + GUI.Label(new Rect((float)Screen.width / 2.0f + 60f, ybase, 120f, 20f), + v.state != Vessel.State.DEAD ? Util.PrintDistance(wpd) : "N/A", valueStyle); + ybase += 18f; + } + + if (timeToWP != null && Config.hudTime) + { + GUI.Label(new Rect((float)Screen.width / 2.0f - 188f, ybase, 240f, 20f), "ETA to " + label + ":", nameStyle); + GUI.Label(new Rect((float)Screen.width / 2.0f + 60f, ybase, 120f, 20f), + v.state != Vessel.State.DEAD ? timeToWP : "N/A", valueStyle); + ybase += 18f; + } + + if (Config.hudHeading) + { + GUI.Label(new Rect((float)Screen.width / 2.0f - 188f, ybase, 240f, 20f), "Heading to " + label + ":", nameStyle); + GUI.Label(new Rect((float)Screen.width / 2.0f + 60f, ybase, 120f, 20f), + v.state != Vessel.State.DEAD ? wpd.heading.ToString("N1") : "N/A", valueStyle); + ybase += 18f; + } + + if (Config.hudAngle && v.mainBody == wpd.celestialBody) + { + double distance = Util.GetLateralDistance(wpd); + double heightDist = wpd.waypoint.altitude + wpd.waypoint.height - v.altitude; + double angle = Math.Atan2(heightDist, distance) * 180.0 / Math.PI; + + GUI.Label(new Rect((float)Screen.width / 2.0f - 188f, ybase, 240f, 20f), "Angle to " + label + ":", nameStyle); + GUI.Label(new Rect((float)Screen.width / 2.0f + 60f, ybase, 120f, 20f), + v.state != Vessel.State.DEAD ? angle.ToString("N2") : "N/A", valueStyle); + ybase += 18f; + + if (v.srfSpeed >= 0.1) + { + double velAngle = 90 - Math.Acos(Vector3d.Dot(v.srf_velocity.normalized, v.upAxis)) * 180.0 / Math.PI; + + GUI.Label(new Rect((float)Screen.width / 2.0f - 188f, ybase, 240f, 20f), "Velocity pitch angle:", nameStyle); + GUI.Label(new Rect((float)Screen.width / 2.0f + 60f, ybase, 120f, 20f), + v.state != Vessel.State.DEAD ? velAngle.ToString("N2") : "N/A", valueStyle); + ybase += 18f; + } + } + if(Config.hudCoordinates&&v.mainBody==wpd.celestialBody) + { + ybase += 18; + GUI.Label(new Rect((float)Screen.width / 2.0f - 188f, ybase, 240f, 38f), "Coordinates of " + label + ":", nameStyle); + GUI.Label(new Rect((float)Screen.width / 2.0f + 60f, ybase, 120f, 38f), + v.state != Vessel.State.DEAD ? string.Format("{0}\r\n{1}", Util.DecimalDegreesToDMS(wpd.waypoint.latitude,true), Util.DecimalDegreesToDMS(wpd.waypoint.longitude,false)) : "N/A", valueStyle); + ybase += 18f; + } + } + } + } + + // Don't draw the waypoint + if (Config.waypointDisplay == Config.WaypointDisplay.NONE) + { + return; + } + + // Translate to scaled space + Vector3d localSpacePoint = celestialBody.GetWorldSurfacePosition(wpd.waypoint.latitude, wpd.waypoint.longitude, wpd.waypoint.height + wpd.waypoint.altitude); + Vector3d scaledSpacePoint = ScaledSpace.LocalToScaledSpace(localSpacePoint); + + // Don't draw if it's behind the camera + if (Vector3d.Dot(MapView.MapCamera.camera.transform.forward, scaledSpacePoint.normalized) < 0.0) + { + return; + } + + // Translate to screen position + Vector3 screenPos = MapView.MapCamera.camera.WorldToScreenPoint(new Vector3((float)scaledSpacePoint.x, (float)scaledSpacePoint.y, (float)scaledSpacePoint.z)); + + // Draw the marker at half-resolution (30 x 45) - that seems to match the one in the map view + Rect markerRect = new Rect(screenPos.x - 15f, (float)Screen.height - screenPos.y - 45.0f, 30f, 45f); + + // Set the window position relative to the selected waypoint + if (selectedWaypoint == wpd.waypoint) + { + windowPos = new Rect(markerRect.xMin - 97, markerRect.yMax + 12, 224, 60); + } + + // Handling clicking on the waypoint + if (Event.current.type == EventType.MouseUp && Event.current.button == 0) + { + if (markerRect.Contains(Event.current.mousePosition)) + { + selectedWaypoint = wpd.waypoint; + windowPos = new Rect(markerRect.xMin - 97, markerRect.yMax + 12, 224, 60); + waypointName = label; + newClick = false; + } + else if (newClick) + { + selectedWaypoint = null; + } + } + + // Only handle on repaint events + if (Event.current.type == EventType.Repaint) + { + // Half-res for the icon too (16 x 16) + Rect iconRect = new Rect(screenPos.x - 8f, (float)Screen.height - screenPos.y - 39.0f, 16f, 16f); + + // Draw the marker + Graphics.DrawTexture(markerRect, GameDatabase.Instance.GetTexture("Squad/Contracts/Icons/marker", false), new Rect(0.0f, 0.0f, 1f, 1f), 0, 0, 0, 0, new Color(0.5f, 0.5f, 0.5f, 0.5f * (wpd.currentAlpha - 0.3f) / 0.7f)); + + // Draw the icon, but support blinking + if (!Util.IsNavPoint(wpd.waypoint) || !FinePrint.WaypointManager.navWaypoint.blinking || (int)((Time.fixedTime - (int)Time.fixedTime) * 4) % 2 == 0) + { + Graphics.DrawTexture(iconRect, ContractDefs.textures[wpd.waypoint.id], new Rect(0.0f, 0.0f, 1f, 1f), 0, 0, 0, 0, SystemUtilities.RandomColor(wpd.waypoint.seed, wpd.currentAlpha)); + } + + // Hint text! + if (iconRect.Contains(Event.current.mousePosition)) + { + // Add agency to label + if (wpd.waypoint.contractReference != null) + { + label += "\n" + wpd.waypoint.contractReference.Agent.Name; + } + float width = 240f; + float height = hintTextStyle.CalcHeight(new GUIContent(label), width); + float yoffset = height + 48.0f; + GUI.Box(new Rect(screenPos.x - width/2.0f, (float)Screen.height - screenPos.y - yoffset, width, height), label, hintTextStyle); + } + } + } + + private void ShowNavigationWindow() + { + if (selectedWaypoint != null) + { + GUI.skin = HighLogic.Skin; + windowPos = GUILayout.Window(10, windowPos, NavigationWindow, waypointName, GUILayout.MinWidth(224)); + } + } + + private void NavigationWindow(int windowID) + { + if (selectedWaypoint == null) + { + return; + } + + GUILayout.BeginVertical(); + if (!Util.IsNavPoint(selectedWaypoint)) + { + if (GUILayout.Button("Activate Navigation", HighLogic.Skin.button, GUILayout.ExpandWidth(true))) + { + FinePrint.WaypointManager.setupNavPoint(selectedWaypoint); + FinePrint.WaypointManager.activateNavPoint(); + selectedWaypoint = null; + } + } + else + { + if (GUILayout.Button("Deactivate Navigation", HighLogic.Skin.button, GUILayout.ExpandWidth(true))) + { + FinePrint.WaypointManager.clearNavPoint(); + selectedWaypoint = null; + } + + } + if (CustomWaypoints.Instance.IsCustom(selectedWaypoint)) + { + if (GUILayout.Button("Edit Custom Waypoint", HighLogic.Skin.button, GUILayout.ExpandWidth(true))) + { + CustomWaypointGUI.EditWaypoint(selectedWaypoint); + selectedWaypoint = null; + } + if (GUILayout.Button("Delete Custom Waypoint", HighLogic.Skin.button, GUILayout.ExpandWidth(true))) + { + CustomWaypointGUI.DeleteWaypoint(selectedWaypoint); + selectedWaypoint = null; + } + } + GUILayout.EndVertical(); + } + + /// + /// Calculates the time to the distance based on the vessels srfSpeed and transform it to a readable string. + /// + /// The waypoint + /// Distance in meters + /// + protected string GetTimeToWaypoint(WaypointData wpd) + { + Vessel v = FlightGlobals.ActiveVessel; + if (v.srfSpeed < 0.1) + { + return null; + } + + double time = (wpd.distanceToActive / v.horizontalSrfSpeed); + + // Earthtime + uint SecondsPerYear = 31536000; // = 365d + uint SecondsPerDay = 86400; // = 24h + uint SecondsPerHour = 3600; // = 60m + uint SecondsPerMinute = 60; // = 60s + + if (GameSettings.KERBIN_TIME == true) + { + SecondsPerYear = 9201600; // = 426d + SecondsPerDay = 21600; // = 6h + SecondsPerHour = 3600; // = 60m + SecondsPerMinute = 60; // = 60s + } + + int years = (int)(time / SecondsPerYear); + time -= years * SecondsPerYear; + + int days = (int)(time / SecondsPerDay); + time -= days * SecondsPerDay; + + int hours = (int)(time / SecondsPerHour); + time -= hours * SecondsPerHour; + + int minutes = (int)(time / SecondsPerMinute); + time -= minutes * SecondsPerMinute; + + int seconds = (int)(time); + + string output = ""; + if (years != 0) + { + output += years + "y"; + } + if (days != 0) + { + if (output.Length != 0) output += ", "; + output += days + "d"; + } + if (hours != 0 || minutes != 0 || seconds != 0 || output.Length == 0) + { + if (output.Length != 0) output += ", "; + output += hours.ToString("D2") + ":" + minutes.ToString("D2") + ":" + seconds.ToString("D2"); + } + + return output; + } + + public void HandleClick(WaypointData wpd) + { + // Translate to screen position + Vector3d localSpacePoint = wpd.celestialBody.GetWorldSurfacePosition(wpd.waypoint.latitude, wpd.waypoint.longitude, wpd.waypoint.altitude); + Vector3d scaledSpacePoint = ScaledSpace.LocalToScaledSpace(localSpacePoint); + Vector3 screenPos = MapView.MapCamera.camera.WorldToScreenPoint(new Vector3((float)scaledSpacePoint.x, (float)scaledSpacePoint.y, (float)scaledSpacePoint.z)); + + Rect markerRect = new Rect(screenPos.x - 15f, (float)Screen.height - screenPos.y - 45.0f, 30f, 45f); + + if (markerRect.Contains(Event.current.mousePosition)) + { + selectedWaypoint = wpd.waypoint; + windowPos = new Rect(markerRect.xMin - 97, markerRect.yMax + 12, 224, 60); + waypointName = wpd.waypoint.name + (wpd.waypoint.isClustered ? (" " + StringUtilities.IntegerToGreek(wpd.waypoint.index)) : ""); + newClick = false; + } + else if (newClick) + { + selectedWaypoint = null; + } + } + + public void HintText(WaypointData wpd) + { + // Translate to screen position + Vector3d localSpacePoint = wpd.celestialBody.GetWorldSurfacePosition(wpd.waypoint.latitude, wpd.waypoint.longitude, wpd.waypoint.altitude); + Vector3d scaledSpacePoint = ScaledSpace.LocalToScaledSpace(localSpacePoint); + Vector3 screenPos = MapView.MapCamera.camera.WorldToScreenPoint(new Vector3((float)scaledSpacePoint.x, (float)scaledSpacePoint.y, (float)scaledSpacePoint.z)); + + Rect iconRect = new Rect(screenPos.x - 8f, (float)Screen.height - screenPos.y - 39.0f, 16f, 16f); + + // Hint text! + if (iconRect.Contains(Event.current.mousePosition)) + { + string label = wpd.waypoint.name + (wpd.waypoint.isClustered ? (" " + StringUtilities.IntegerToGreek(wpd.waypoint.index)) : ""); + float width = 240f; + float height = hintTextStyle.CalcHeight(new GUIContent(label), width); + float yoffset = height + 48.0f; + GUI.Box(new Rect(screenPos.x - width / 2.0f, (float)Screen.height - screenPos.y - yoffset, width, height), label, hintTextStyle); + } + } + } +} From ec327bbfb292bd083d52acee4508dc668cccdd20 Mon Sep 17 00:00:00 2001 From: Samuel Swanner Date: Mon, 31 Aug 2015 17:38:27 -0500 Subject: [PATCH 2/4] Correcting unexpected file format change --- source/WaypointManager/Util.cs | 649 +++++----- .../WaypointManager/WaypointFlightRenderer.cs | 1090 ++++++++--------- 2 files changed, 869 insertions(+), 870 deletions(-) diff --git a/source/WaypointManager/Util.cs b/source/WaypointManager/Util.cs index 9424a4a..ad3bce9 100644 --- a/source/WaypointManager/Util.cs +++ b/source/WaypointManager/Util.cs @@ -1,325 +1,324 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using UnityEngine; -using FinePrint; -using FinePrint.Utilities; -using DDSHeaders; - -namespace WaypointManager -{ - /// - /// Utility methods for WaypointManager. - /// - public static class Util - { - private static string[] UNITS = { "m", "km", "Mm", "Gm", "Tm" }; - private static Dictionary> contractIcons = new Dictionary>(); - private static float lastAlpha; - - /// - /// Gets the lateral distance in meters from the active vessel to the given waypoint. - /// - /// Activated waypoint - /// Distance in meters - public static double GetLateralDistance(WaypointData wpd) - { - Vessel v = FlightGlobals.ActiveVessel; - CelestialBody celestialBody = v.mainBody; - - // Use the haversine formula to calculate great circle distance. - double sin1 = Math.Sin(Math.PI / 180.0 * (v.latitude - wpd.waypoint.latitude) / 2); - double sin2 = Math.Sin(Math.PI / 180.0 * (v.longitude - wpd.waypoint.longitude) / 2); - double cos1 = Math.Cos(Math.PI / 180.0 * wpd.waypoint.latitude); - double cos2 = Math.Cos(Math.PI / 180.0 * v.latitude); - - return 2 * (celestialBody.Radius + wpd.waypoint.height + wpd.waypoint.altitude) * - Math.Asin(Math.Sqrt(sin1 * sin1 + cos1 * cos2 * sin2 * sin2)); - } - - /// - /// Gets the distance in meters from the active vessel to the given waypoint. - /// - /// Activated waypoint - /// Distance in meters - public static double GetDistanceToWaypoint(WaypointData wpd) - { - Vessel v = FlightGlobals.ActiveVessel; - CelestialBody celestialBody = v.mainBody; - - // Simple distance - if (Config.distanceCalcMethod == Config.DistanceCalcMethod.STRAIGHT_LINE || celestialBody != wpd.celestialBody) - { - return GetStraightDistance(wpd); - } - - // Use the haversine formula to calculate great circle distance. - double sin1 = Math.Sin(Math.PI / 180.0 * (v.latitude - wpd.waypoint.latitude) / 2); - double sin2 = Math.Sin(Math.PI / 180.0 * (v.longitude - wpd.waypoint.longitude) / 2); - double cos1 = Math.Cos(Math.PI / 180.0 * wpd.waypoint.latitude); - double cos2 = Math.Cos(Math.PI / 180.0 * v.latitude); - - double lateralDist = 2 * (celestialBody.Radius + wpd.waypoint.height + wpd.waypoint.altitude) * - Math.Asin(Math.Sqrt(sin1 * sin1 + cos1 * cos2 * sin2 * sin2)); - double heightDist = Math.Abs(wpd.waypoint.altitude + wpd.waypoint.height - v.altitude); - - if (Config.distanceCalcMethod == Config.DistanceCalcMethod.LATERAL || heightDist <= lateralDist / 2.0) - { - return lateralDist; - } - else - { - // Get the ratio to use in our formula - double x = (heightDist - lateralDist / 2.0) / lateralDist; - - // x / (x + 1) starts at 0 when x = 0, and increases to 1 - return (x / (x + 1)) * heightDist + lateralDist; - } - } - - public static double GetStraightDistance(WaypointData wpd) - { - Vessel v = FlightGlobals.ActiveVessel; - - Vector3 wpPosition = wpd.celestialBody.GetWorldSurfacePosition(wpd.waypoint.latitude, wpd.waypoint.longitude, wpd.waypoint.height + wpd.waypoint.altitude); - return Vector3.Distance(wpPosition, v.transform.position); - } - - /// - /// Gets the printable distance to the waypoint. - /// - /// WaypointData object - /// The distance and unit for screen output - public static string PrintDistance(WaypointData wpd) - { - int unit = 0; - double distance = wpd.distanceToActive; - while (unit < 4 && distance >= 10000.0) - { - distance /= 1000.0; - unit++; - } - - return distance.ToString("N1") + " " + UNITS[unit]; - } - - /// - /// Gets the celestial body for the given name. - /// - /// Name of the celestial body - /// The CelestialBody object - public static CelestialBody GetBody(string name) - { - CelestialBody body = FlightGlobals.Bodies.Where(b => b.name == name).FirstOrDefault(); - if (body == null) - { - Debug.LogWarning("Couldn't find celestial body with name '" + name + "'."); - } - return body; - } - - /// - /// Checks if the given waypoint is the nav waypoint. - /// - /// - /// - public static bool IsNavPoint(Waypoint waypoint) - { - NavWaypoint navPoint = FinePrint.WaypointManager.navWaypoint; - if (navPoint == null || !FinePrint.WaypointManager.navIsActive()) - { - return false; - } - - return navPoint.latitude == waypoint.latitude && navPoint.longitude == waypoint.longitude; - } - - /// - /// Gets the contract icon for the given id and seed (color). - /// - /// URL of the icon - /// Seed to use for generating the color - /// The texture - public static Texture2D GetContractIcon(string url, int seed) - { - // Check cache for texture - Texture2D texture; - Color color = SystemUtilities.RandomColor(seed, 1.0f, 1.0f, 1.0f); - if (!contractIcons.ContainsKey(url)) - { - contractIcons[url] = new Dictionary(); - } - if (!contractIcons[url].ContainsKey(color)) - { - Texture2D baseTexture = ContractDefs.textures[url]; - - try - { - Texture2D loadedTexture = null; - string path = (url.Contains('/') ? "GameData/" : "GameData/Squad/Contracts/Icons/") + url; - // PNG loading - if (File.Exists(path + ".png")) - { - path += ".png"; - loadedTexture = new Texture2D(baseTexture.width, baseTexture.height, TextureFormat.RGBA32, false); - loadedTexture.LoadImage(File.ReadAllBytes(path.Replace('/', Path.DirectorySeparatorChar))); - } - // DDS loading - else if (File.Exists(path + ".dds")) - { - path += ".dds"; - BinaryReader br = new BinaryReader(new MemoryStream(File.ReadAllBytes(path))); - - if (br.ReadUInt32() != DDSValues.uintMagic) - { - throw new Exception("Format issue with DDS texture '" + path + "'!"); - } - DDSHeader ddsHeader = new DDSHeader(br); - if (ddsHeader.ddspf.dwFourCC == DDSValues.uintDX10) - { - DDSHeaderDX10 ddsHeaderDx10 = new DDSHeaderDX10(br); - } - - TextureFormat texFormat; - if (ddsHeader.ddspf.dwFourCC == DDSValues.uintDXT1) - { - texFormat = UnityEngine.TextureFormat.DXT1; - } - else if (ddsHeader.ddspf.dwFourCC == DDSValues.uintDXT3) - { - texFormat = UnityEngine.TextureFormat.DXT1 | UnityEngine.TextureFormat.Alpha8; - } - else if (ddsHeader.ddspf.dwFourCC == DDSValues.uintDXT5) - { - texFormat = UnityEngine.TextureFormat.DXT5; - } - else - { - throw new Exception("Unhandled DDS format!"); - } - - loadedTexture = new Texture2D((int)ddsHeader.dwWidth, (int)ddsHeader.dwHeight, texFormat, false); - loadedTexture.LoadRawTextureData(br.ReadBytes((int)(br.BaseStream.Length - br.BaseStream.Position))); - } - else - { - throw new Exception("Couldn't find file for icon '" + url + "'"); - } - - Color[] pixels = loadedTexture.GetPixels(); - for (int i = 0; i < pixels.Length; i++) - { - pixels[i] *= color; - } - texture = new Texture2D(baseTexture.width, baseTexture.height, TextureFormat.RGBA32, false); - texture.SetPixels(pixels); - texture.Apply(false, false); - contractIcons[url][color] = texture; - UnityEngine.Object.Destroy(loadedTexture); - } - catch (Exception e) - { - Debug.LogError("WaypointManager: Couldn't create texture for '" + url + "'!"); - Debug.LogException(e); - texture = contractIcons[url][color] = baseTexture; - } - } - else - { - texture = contractIcons[url][color]; - } - - return texture; - } - - public static double WaypointHeight(Waypoint w, CelestialBody body) - { - return TerrainHeight(w.latitude, w.longitude, body); - } - - public static double TerrainHeight(double latitude, double longitude, CelestialBody body) - { - // Not sure when this happens - for Sun and Jool? - if (body.pqsController == null) - { - return 0; - } - - // Figure out the terrain height - double latRads = Math.PI / 180.0 * latitude; - double lonRads = Math.PI / 180.0 * longitude; - Vector3d radialVector = new Vector3d(Math.Cos(latRads) * Math.Cos(lonRads), Math.Sin(latRads), Math.Cos(latRads) * Math.Sin(lonRads)); - return Math.Max(body.pqsController.GetSurfaceHeight(radialVector) - body.pqsController.radius, 0.0); - } - - public static void DrawWaypoint(CelestialBody targetBody, double latitude, double longitude, double altitude, string id, int seed, float alpha = -1.0f) - { - // Translate to screen position - Vector3d localSpacePoint = targetBody.GetWorldSurfacePosition(latitude, longitude, altitude); - Vector3d scaledSpacePoint = ScaledSpace.LocalToScaledSpace(localSpacePoint); - Vector3 screenPos = MapView.MapCamera.camera.WorldToScreenPoint(new Vector3((float)scaledSpacePoint.x, (float)scaledSpacePoint.y, (float)scaledSpacePoint.z)); - - // Don't draw if it's behind the camera - Camera camera = MapView.MapIsEnabled ? PlanetariumCamera.Camera : FlightCamera.fetch.mainCamera; - Vector3 cameraPos = ScaledSpace.ScaledToLocalSpace(camera.transform.position); - if (Vector3d.Dot(camera.transform.forward, scaledSpacePoint.normalized) < 0.0) - { - return; - } - - // Draw the marker at half-resolution (30 x 45) - that seems to match the one in the map view - Rect markerRect = new Rect(screenPos.x - 15f, (float)Screen.height - screenPos.y - 45.0f, 30f, 45f); - - // Half-res for the icon too (16 x 16) - Rect iconRect = new Rect(screenPos.x - 8f, (float)Screen.height - screenPos.y - 39.0f, 16f, 16f); - - if (alpha < 0.0f) - { - bool occluded = WaypointData.IsOccluded(targetBody, cameraPos, localSpacePoint, altitude); - float desiredAlpha = occluded ? 0.3f : 1.0f * Config.opacity; - if (lastAlpha < 0.0f) - { - lastAlpha = desiredAlpha; - } - else if (lastAlpha < desiredAlpha) - { - lastAlpha = Mathf.Clamp(lastAlpha + Time.deltaTime * 4f, lastAlpha, desiredAlpha); - } - else - { - lastAlpha = Mathf.Clamp(lastAlpha - Time.deltaTime * 4f, desiredAlpha, lastAlpha); - } - alpha = lastAlpha; - } - - // Draw the marker - Graphics.DrawTexture(markerRect, GameDatabase.Instance.GetTexture("Squad/Contracts/Icons/marker", false), new Rect(0.0f, 0.0f, 1f, 1f), 0, 0, 0, 0, new Color(0.5f, 0.5f, 0.5f, 0.5f * (alpha - 0.3f) / 0.7f)); - - // Draw the icon - Graphics.DrawTexture(iconRect, ContractDefs.textures[id], new Rect(0.0f, 0.0f, 1f, 1f), 0, 0, 0, 0, SystemUtilities.RandomColor(seed, alpha)); - } - - /// - /// Converts decimal degrees to a string of DMS formatted degrees with N/S, E/W prefix - /// - /// - /// boolean to determin latitude or longitude for compass prefix - /// - public static string DecimalDegreesToDMS(double decimalDegrees, bool latitude) - { - string dms = string.Empty; - string direction = string.Empty; - int d = Math.Abs((int)(decimalDegrees)); - double decimalpart = Math.Abs(decimalDegrees - d); - int m = (int)(decimalpart * 60); - double s = (decimalpart - m / 60f) * 3600; - if (latitude) direction = decimalDegrees >= 0 ? "N" : "S"; - else direction = decimalDegrees >= 0 ? "E" : "W"; - dms = string.Format("{3} {0}\x00B0{1}\'{2:F1}\"", d, m, s, direction); - return dms; - } - - } -} +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using UnityEngine; +using FinePrint; +using FinePrint.Utilities; +using DDSHeaders; + +namespace WaypointManager +{ + /// + /// Utility methods for WaypointManager. + /// + public static class Util + { + private static string[] UNITS = { "m", "km", "Mm", "Gm", "Tm" }; + private static Dictionary> contractIcons = new Dictionary>(); + private static float lastAlpha; + + /// + /// Gets the lateral distance in meters from the active vessel to the given waypoint. + /// + /// Activated waypoint + /// Distance in meters + public static double GetLateralDistance(WaypointData wpd) + { + Vessel v = FlightGlobals.ActiveVessel; + CelestialBody celestialBody = v.mainBody; + + // Use the haversine formula to calculate great circle distance. + double sin1 = Math.Sin(Math.PI / 180.0 * (v.latitude - wpd.waypoint.latitude) / 2); + double sin2 = Math.Sin(Math.PI / 180.0 * (v.longitude - wpd.waypoint.longitude) / 2); + double cos1 = Math.Cos(Math.PI / 180.0 * wpd.waypoint.latitude); + double cos2 = Math.Cos(Math.PI / 180.0 * v.latitude); + + return 2 * (celestialBody.Radius + wpd.waypoint.height + wpd.waypoint.altitude) * + Math.Asin(Math.Sqrt(sin1 * sin1 + cos1 * cos2 * sin2 * sin2)); + } + + /// + /// Gets the distance in meters from the active vessel to the given waypoint. + /// + /// Activated waypoint + /// Distance in meters + public static double GetDistanceToWaypoint(WaypointData wpd) + { + Vessel v = FlightGlobals.ActiveVessel; + CelestialBody celestialBody = v.mainBody; + + // Simple distance + if (Config.distanceCalcMethod == Config.DistanceCalcMethod.STRAIGHT_LINE || celestialBody != wpd.celestialBody) + { + return GetStraightDistance(wpd); + } + + // Use the haversine formula to calculate great circle distance. + double sin1 = Math.Sin(Math.PI / 180.0 * (v.latitude - wpd.waypoint.latitude) / 2); + double sin2 = Math.Sin(Math.PI / 180.0 * (v.longitude - wpd.waypoint.longitude) / 2); + double cos1 = Math.Cos(Math.PI / 180.0 * wpd.waypoint.latitude); + double cos2 = Math.Cos(Math.PI / 180.0 * v.latitude); + + double lateralDist = 2 * (celestialBody.Radius + wpd.waypoint.height + wpd.waypoint.altitude) * + Math.Asin(Math.Sqrt(sin1 * sin1 + cos1 * cos2 * sin2 * sin2)); + double heightDist = Math.Abs(wpd.waypoint.altitude + wpd.waypoint.height - v.altitude); + + if (Config.distanceCalcMethod == Config.DistanceCalcMethod.LATERAL || heightDist <= lateralDist / 2.0) + { + return lateralDist; + } + else + { + // Get the ratio to use in our formula + double x = (heightDist - lateralDist / 2.0) / lateralDist; + + // x / (x + 1) starts at 0 when x = 0, and increases to 1 + return (x / (x + 1)) * heightDist + lateralDist; + } + } + + public static double GetStraightDistance(WaypointData wpd) + { + Vessel v = FlightGlobals.ActiveVessel; + + Vector3 wpPosition = wpd.celestialBody.GetWorldSurfacePosition(wpd.waypoint.latitude, wpd.waypoint.longitude, wpd.waypoint.height + wpd.waypoint.altitude); + return Vector3.Distance(wpPosition, v.transform.position); + } + + /// + /// Gets the printable distance to the waypoint. + /// + /// WaypointData object + /// The distance and unit for screen output + public static string PrintDistance(WaypointData wpd) + { + int unit = 0; + double distance = wpd.distanceToActive; + while (unit < 4 && distance >= 10000.0) + { + distance /= 1000.0; + unit++; + } + + return distance.ToString("N1") + " " + UNITS[unit]; + } + + /// + /// Gets the celestial body for the given name. + /// + /// Name of the celestial body + /// The CelestialBody object + public static CelestialBody GetBody(string name) + { + CelestialBody body = FlightGlobals.Bodies.Where(b => b.name == name).FirstOrDefault(); + if (body == null) + { + Debug.LogWarning("Couldn't find celestial body with name '" + name + "'."); + } + return body; + } + + /// + /// Checks if the given waypoint is the nav waypoint. + /// + /// + /// + public static bool IsNavPoint(Waypoint waypoint) + { + NavWaypoint navPoint = FinePrint.WaypointManager.navWaypoint; + if (navPoint == null || !FinePrint.WaypointManager.navIsActive()) + { + return false; + } + + return navPoint.latitude == waypoint.latitude && navPoint.longitude == waypoint.longitude; + } + + /// + /// Gets the contract icon for the given id and seed (color). + /// + /// URL of the icon + /// Seed to use for generating the color + /// The texture + public static Texture2D GetContractIcon(string url, int seed) + { + // Check cache for texture + Texture2D texture; + Color color = SystemUtilities.RandomColor(seed, 1.0f, 1.0f, 1.0f); + if (!contractIcons.ContainsKey(url)) + { + contractIcons[url] = new Dictionary(); + } + if (!contractIcons[url].ContainsKey(color)) + { + Texture2D baseTexture = ContractDefs.textures[url]; + + try + { + Texture2D loadedTexture = null; + string path = (url.Contains('/') ? "GameData/" : "GameData/Squad/Contracts/Icons/") + url; + // PNG loading + if (File.Exists(path + ".png")) + { + path += ".png"; + loadedTexture = new Texture2D(baseTexture.width, baseTexture.height, TextureFormat.RGBA32, false); + loadedTexture.LoadImage(File.ReadAllBytes(path.Replace('/', Path.DirectorySeparatorChar))); + } + // DDS loading + else if (File.Exists(path + ".dds")) + { + path += ".dds"; + BinaryReader br = new BinaryReader(new MemoryStream(File.ReadAllBytes(path))); + + if (br.ReadUInt32() != DDSValues.uintMagic) + { + throw new Exception("Format issue with DDS texture '" + path + "'!"); + } + DDSHeader ddsHeader = new DDSHeader(br); + if (ddsHeader.ddspf.dwFourCC == DDSValues.uintDX10) + { + DDSHeaderDX10 ddsHeaderDx10 = new DDSHeaderDX10(br); + } + + TextureFormat texFormat; + if (ddsHeader.ddspf.dwFourCC == DDSValues.uintDXT1) + { + texFormat = UnityEngine.TextureFormat.DXT1; + } + else if (ddsHeader.ddspf.dwFourCC == DDSValues.uintDXT3) + { + texFormat = UnityEngine.TextureFormat.DXT1 | UnityEngine.TextureFormat.Alpha8; + } + else if (ddsHeader.ddspf.dwFourCC == DDSValues.uintDXT5) + { + texFormat = UnityEngine.TextureFormat.DXT5; + } + else + { + throw new Exception("Unhandled DDS format!"); + } + + loadedTexture = new Texture2D((int)ddsHeader.dwWidth, (int)ddsHeader.dwHeight, texFormat, false); + loadedTexture.LoadRawTextureData(br.ReadBytes((int)(br.BaseStream.Length - br.BaseStream.Position))); + } + else + { + throw new Exception("Couldn't find file for icon '" + url + "'"); + } + + Color[] pixels = loadedTexture.GetPixels(); + for (int i = 0; i < pixels.Length; i++) + { + pixels[i] *= color; + } + texture = new Texture2D(baseTexture.width, baseTexture.height, TextureFormat.RGBA32, false); + texture.SetPixels(pixels); + texture.Apply(false, false); + contractIcons[url][color] = texture; + UnityEngine.Object.Destroy(loadedTexture); + } + catch (Exception e) + { + Debug.LogError("WaypointManager: Couldn't create texture for '" + url + "'!"); + Debug.LogException(e); + texture = contractIcons[url][color] = baseTexture; + } + } + else + { + texture = contractIcons[url][color]; + } + + return texture; + } + + public static double WaypointHeight(Waypoint w, CelestialBody body) + { + return TerrainHeight(w.latitude, w.longitude, body); + } + + public static double TerrainHeight(double latitude, double longitude, CelestialBody body) + { + // Not sure when this happens - for Sun and Jool? + if (body.pqsController == null) + { + return 0; + } + + // Figure out the terrain height + double latRads = Math.PI / 180.0 * latitude; + double lonRads = Math.PI / 180.0 * longitude; + Vector3d radialVector = new Vector3d(Math.Cos(latRads) * Math.Cos(lonRads), Math.Sin(latRads), Math.Cos(latRads) * Math.Sin(lonRads)); + return Math.Max(body.pqsController.GetSurfaceHeight(radialVector) - body.pqsController.radius, 0.0); + } + + public static void DrawWaypoint(CelestialBody targetBody, double latitude, double longitude, double altitude, string id, int seed, float alpha = -1.0f) + { + // Translate to screen position + Vector3d localSpacePoint = targetBody.GetWorldSurfacePosition(latitude, longitude, altitude); + Vector3d scaledSpacePoint = ScaledSpace.LocalToScaledSpace(localSpacePoint); + Vector3 screenPos = MapView.MapCamera.camera.WorldToScreenPoint(new Vector3((float)scaledSpacePoint.x, (float)scaledSpacePoint.y, (float)scaledSpacePoint.z)); + + // Don't draw if it's behind the camera + Camera camera = MapView.MapIsEnabled ? PlanetariumCamera.Camera : FlightCamera.fetch.mainCamera; + Vector3 cameraPos = ScaledSpace.ScaledToLocalSpace(camera.transform.position); + if (Vector3d.Dot(camera.transform.forward, scaledSpacePoint.normalized) < 0.0) + { + return; + } + + // Draw the marker at half-resolution (30 x 45) - that seems to match the one in the map view + Rect markerRect = new Rect(screenPos.x - 15f, (float)Screen.height - screenPos.y - 45.0f, 30f, 45f); + + // Half-res for the icon too (16 x 16) + Rect iconRect = new Rect(screenPos.x - 8f, (float)Screen.height - screenPos.y - 39.0f, 16f, 16f); + + if (alpha < 0.0f) + { + bool occluded = WaypointData.IsOccluded(targetBody, cameraPos, localSpacePoint, altitude); + float desiredAlpha = occluded ? 0.3f : 1.0f * Config.opacity; + if (lastAlpha < 0.0f) + { + lastAlpha = desiredAlpha; + } + else if (lastAlpha < desiredAlpha) + { + lastAlpha = Mathf.Clamp(lastAlpha + Time.deltaTime * 4f, lastAlpha, desiredAlpha); + } + else + { + lastAlpha = Mathf.Clamp(lastAlpha - Time.deltaTime * 4f, desiredAlpha, lastAlpha); + } + alpha = lastAlpha; + } + + // Draw the marker + Graphics.DrawTexture(markerRect, GameDatabase.Instance.GetTexture("Squad/Contracts/Icons/marker", false), new Rect(0.0f, 0.0f, 1f, 1f), 0, 0, 0, 0, new Color(0.5f, 0.5f, 0.5f, 0.5f * (alpha - 0.3f) / 0.7f)); + + // Draw the icon + Graphics.DrawTexture(iconRect, ContractDefs.textures[id], new Rect(0.0f, 0.0f, 1f, 1f), 0, 0, 0, 0, SystemUtilities.RandomColor(seed, alpha)); + } + + /// + /// Converts decimal degrees to a string of DMS formatted degrees with N/S, E/W prefix + /// + /// + /// boolean to determin latitude or longitude for compass prefix + /// + public static string DecimalDegreesToDMS(double decimalDegrees, bool latitude) + { + string dms = string.Empty; + string direction = string.Empty; + int d = Math.Abs((int)(decimalDegrees)); + double decimalpart = Math.Abs(decimalDegrees - d); + int m = (int)(decimalpart * 60); + double s = (decimalpart - m / 60f) * 3600; + if (latitude) direction = decimalDegrees >= 0 ? "N" : "S"; + else direction = decimalDegrees >= 0 ? "E" : "W"; + dms = string.Format("{3} {0}\x00B0{1}\'{2:F1}\"", d, m, s, direction); + return dms; + } + } +} diff --git a/source/WaypointManager/WaypointFlightRenderer.cs b/source/WaypointManager/WaypointFlightRenderer.cs index 16b1311..72a21f0 100644 --- a/source/WaypointManager/WaypointFlightRenderer.cs +++ b/source/WaypointManager/WaypointFlightRenderer.cs @@ -1,545 +1,545 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using UnityEngine; -using KSP; -using Contracts; -using FinePrint; -using FinePrint.Utilities; - -namespace WaypointManager -{ - [KSPAddon(KSPAddon.Startup.SpaceCentre, true)] - class WaypointFlightRenderer : MonoBehaviour - { - private GUIStyle nameStyle = null; - private GUIStyle valueStyle = null; - private GUIStyle hintTextStyle = null; - - private bool visible = true; - private Waypoint selectedWaypoint = null; - private string waypointName = ""; - private Rect windowPos; - private bool newClick = false; - private AltimeterSliderButtons asb = null; - - private float referencePos = 0.0f; - private float referenceUISize = 0.0f; - private float lastPos = 0.0f; - private bool referenceSet = false; - - private const double MIN_TIME = 300; - private const double MIN_DISTANCE = 25000; - private const double MIN_SPEED = MIN_DISTANCE / MIN_TIME; - private const double FADE_TIME = 20; - - void Start() - { - if (MapView.MapCamera.gameObject.GetComponent() == null) - { - MapView.MapCamera.gameObject.AddComponent(); - - // Destroy this object - otherwise we'll have two - Destroy(this); - } - - GameEvents.onGameSceneLoadRequested.Add(new EventData.OnEvent(OnGameSceneLoadRequested)); - GameEvents.onHideUI.Add(new EventVoid.OnEvent(OnHideUI)); - GameEvents.onShowUI.Add(new EventVoid.OnEvent(OnShowUI)); - } - - protected void OnDestroy() - { - GameEvents.onGameSceneLoadRequested.Remove(new EventData.OnEvent(OnGameSceneLoadRequested)); - GameEvents.onHideUI.Remove(OnHideUI); - GameEvents.onShowUI.Remove(OnShowUI); - } - - public void OnGameSceneLoadRequested(GameScenes gameScene) - { - asb = null; - } - - public void OnHideUI() - { - visible = false; - } - - public void OnShowUI() - { - visible = true; - } - - public void OnGUI() - { - if (visible) - { - if (Event.current.type == EventType.MouseUp && Event.current.button == 0) - { - newClick = true; - } - - if (HighLogic.LoadedSceneIsFlight || HighLogic.LoadedScene == GameScenes.TRACKSTATION) - { - // Draw the marker for custom waypoints that are currently being created - CustomWaypointGUI.DrawMarker(); - - // Draw waypoints if not in career mode - if (ContractSystem.Instance == null && MapView.MapIsEnabled) - { - foreach (WaypointData wpd in WaypointData.Waypoints) - { - if (wpd.celestialBody != null && wpd.waypoint.celestialName == wpd.celestialBody.name) - { - if (Event.current.type == EventType.Repaint) - { - wpd.SetAlpha(); - Util.DrawWaypoint(wpd.celestialBody, wpd.waypoint.latitude, wpd.waypoint.longitude, - wpd.waypoint.altitude, wpd.waypoint.id, wpd.waypoint.seed, wpd.currentAlpha); - } - - // Handling clicking on the waypoint - if (Event.current.type == EventType.MouseUp && Event.current.button == 0) - { - HandleClick(wpd); - } - - // Draw hint text - if (Event.current.type == EventType.Repaint) - { - HintText(wpd); - } - } - } - } - } - - if (HighLogic.LoadedSceneIsFlight && !MapView.MapIsEnabled) - { - SetupStyles(); - - WaypointData.CacheWaypointData(); - - foreach (WaypointData wpd in WaypointData.Waypoints) - { - DrawWaypoint(wpd); - } - } - - if (HighLogic.LoadedSceneIsFlight && (!MapView.MapIsEnabled || ContractSystem.Instance == null)) - { - ShowNavigationWindow(); - } - } - } - - // Styles taken directly from Kerbal Engineer Redux - because they look great and this will - // make our display consistent with that - protected void SetupStyles() - { - if (nameStyle != null) - { - return; - } - - nameStyle = new GUIStyle(HighLogic.Skin.label) - { - normal = - { - textColor = Color.white - }, - margin = new RectOffset(), - padding = new RectOffset(5, 0, 0, 0), - alignment = TextAnchor.MiddleRight, - fontSize = 11, - fontStyle = FontStyle.Bold, - fixedHeight = 20.0f - }; - - valueStyle = new GUIStyle(HighLogic.Skin.label) - { - margin = new RectOffset(), - padding = new RectOffset(0, 5, 0, 0), - alignment = TextAnchor.MiddleLeft, - fontSize = 11, - fontStyle = FontStyle.Normal, - fixedHeight = 20.0f - }; - - hintTextStyle = new GUIStyle(HighLogic.Skin.box) - { - padding = new RectOffset(4, 4, 7, 4), - font = MapView.OrbitIconsTextSkin.label.font, - fontSize = MapView.OrbitIconsTextSkin.label.fontSize, - fontStyle = MapView.OrbitIconsTextSkin.label.fontStyle, - fixedWidth = 0, - fixedHeight = 0, - stretchHeight = true, - stretchWidth = true - }; - } - - protected void DrawWaypoint(WaypointData wpd) - { - // Not our planet - CelestialBody celestialBody = FlightGlobals.currentMainBody; - if (celestialBody == null || wpd.waypoint.celestialName != celestialBody.name) - { - return; - } - - // Check if the waypoint should be visible - if (!wpd.waypoint.visible) - { - return; - } - - // Figure out waypoint label - string label = wpd.waypoint.name + (wpd.waypoint.isClustered ? (" " + StringUtilities.IntegerToGreek(wpd.waypoint.index)) : ""); - - // Set the alpha and do a nice fade - wpd.SetAlpha(); - - // Decide whether to actually draw the waypoint - if (FlightGlobals.ActiveVessel != null) - { - // Figure out the distance to the waypoint - Vessel v = FlightGlobals.ActiveVessel; - - // Only change alpha if the waypoint isn't the nav point - if (!Util.IsNavPoint(wpd.waypoint)) - { - // Get the distance to the waypoint at the current speed - double speed = v.srfSpeed < MIN_SPEED ? MIN_SPEED : v.srfSpeed; - double directTime = Util.GetStraightDistance(wpd) / speed; - - // More than two minutes away - if (directTime > MIN_TIME || Config.waypointDisplay != Config.WaypointDisplay.ALL) - { - return; - } - else if (directTime >= MIN_TIME - FADE_TIME) - { - wpd.currentAlpha = (float)((MIN_TIME - directTime) / FADE_TIME) * Config.opacity; - } - } - // Draw the distance information to the nav point - else - { - // Draw the distance to waypoint text - if (Event.current.type == EventType.Repaint) - { - if (asb == null) - { - asb = UnityEngine.Object.FindObjectOfType(); - } - - if (referenceUISize != ScreenSafeUI.VerticalRatio || !referenceSet) - { - referencePos = ScreenSafeUI.referenceCam.ViewportToScreenPoint(asb.transform.position).y; - referenceUISize = ScreenSafeUI.VerticalRatio; - - // Need two consistent numbers in a row to set the reference - if (lastPos == referencePos) - { - referenceSet = true; - } - else - { - lastPos = referencePos; - } - } - - float ybase = (referencePos - ScreenSafeUI.referenceCam.ViewportToScreenPoint(asb.transform.position).y + Screen.height / 11.67f) / ScreenSafeUI.VerticalRatio; - - string timeToWP = GetTimeToWaypoint(wpd); - if (Config.hudDistance) - { - GUI.Label(new Rect((float)Screen.width / 2.0f - 188f, ybase, 240f, 20f), "Distance to " + label + ":", nameStyle); - GUI.Label(new Rect((float)Screen.width / 2.0f + 60f, ybase, 120f, 20f), - v.state != Vessel.State.DEAD ? Util.PrintDistance(wpd) : "N/A", valueStyle); - ybase += 18f; - } - - if (timeToWP != null && Config.hudTime) - { - GUI.Label(new Rect((float)Screen.width / 2.0f - 188f, ybase, 240f, 20f), "ETA to " + label + ":", nameStyle); - GUI.Label(new Rect((float)Screen.width / 2.0f + 60f, ybase, 120f, 20f), - v.state != Vessel.State.DEAD ? timeToWP : "N/A", valueStyle); - ybase += 18f; - } - - if (Config.hudHeading) - { - GUI.Label(new Rect((float)Screen.width / 2.0f - 188f, ybase, 240f, 20f), "Heading to " + label + ":", nameStyle); - GUI.Label(new Rect((float)Screen.width / 2.0f + 60f, ybase, 120f, 20f), - v.state != Vessel.State.DEAD ? wpd.heading.ToString("N1") : "N/A", valueStyle); - ybase += 18f; - } - - if (Config.hudAngle && v.mainBody == wpd.celestialBody) - { - double distance = Util.GetLateralDistance(wpd); - double heightDist = wpd.waypoint.altitude + wpd.waypoint.height - v.altitude; - double angle = Math.Atan2(heightDist, distance) * 180.0 / Math.PI; - - GUI.Label(new Rect((float)Screen.width / 2.0f - 188f, ybase, 240f, 20f), "Angle to " + label + ":", nameStyle); - GUI.Label(new Rect((float)Screen.width / 2.0f + 60f, ybase, 120f, 20f), - v.state != Vessel.State.DEAD ? angle.ToString("N2") : "N/A", valueStyle); - ybase += 18f; - - if (v.srfSpeed >= 0.1) - { - double velAngle = 90 - Math.Acos(Vector3d.Dot(v.srf_velocity.normalized, v.upAxis)) * 180.0 / Math.PI; - - GUI.Label(new Rect((float)Screen.width / 2.0f - 188f, ybase, 240f, 20f), "Velocity pitch angle:", nameStyle); - GUI.Label(new Rect((float)Screen.width / 2.0f + 60f, ybase, 120f, 20f), - v.state != Vessel.State.DEAD ? velAngle.ToString("N2") : "N/A", valueStyle); - ybase += 18f; - } - } - if(Config.hudCoordinates&&v.mainBody==wpd.celestialBody) - { - ybase += 18; - GUI.Label(new Rect((float)Screen.width / 2.0f - 188f, ybase, 240f, 38f), "Coordinates of " + label + ":", nameStyle); - GUI.Label(new Rect((float)Screen.width / 2.0f + 60f, ybase, 120f, 38f), - v.state != Vessel.State.DEAD ? string.Format("{0}\r\n{1}", Util.DecimalDegreesToDMS(wpd.waypoint.latitude,true), Util.DecimalDegreesToDMS(wpd.waypoint.longitude,false)) : "N/A", valueStyle); - ybase += 18f; - } - } - } - } - - // Don't draw the waypoint - if (Config.waypointDisplay == Config.WaypointDisplay.NONE) - { - return; - } - - // Translate to scaled space - Vector3d localSpacePoint = celestialBody.GetWorldSurfacePosition(wpd.waypoint.latitude, wpd.waypoint.longitude, wpd.waypoint.height + wpd.waypoint.altitude); - Vector3d scaledSpacePoint = ScaledSpace.LocalToScaledSpace(localSpacePoint); - - // Don't draw if it's behind the camera - if (Vector3d.Dot(MapView.MapCamera.camera.transform.forward, scaledSpacePoint.normalized) < 0.0) - { - return; - } - - // Translate to screen position - Vector3 screenPos = MapView.MapCamera.camera.WorldToScreenPoint(new Vector3((float)scaledSpacePoint.x, (float)scaledSpacePoint.y, (float)scaledSpacePoint.z)); - - // Draw the marker at half-resolution (30 x 45) - that seems to match the one in the map view - Rect markerRect = new Rect(screenPos.x - 15f, (float)Screen.height - screenPos.y - 45.0f, 30f, 45f); - - // Set the window position relative to the selected waypoint - if (selectedWaypoint == wpd.waypoint) - { - windowPos = new Rect(markerRect.xMin - 97, markerRect.yMax + 12, 224, 60); - } - - // Handling clicking on the waypoint - if (Event.current.type == EventType.MouseUp && Event.current.button == 0) - { - if (markerRect.Contains(Event.current.mousePosition)) - { - selectedWaypoint = wpd.waypoint; - windowPos = new Rect(markerRect.xMin - 97, markerRect.yMax + 12, 224, 60); - waypointName = label; - newClick = false; - } - else if (newClick) - { - selectedWaypoint = null; - } - } - - // Only handle on repaint events - if (Event.current.type == EventType.Repaint) - { - // Half-res for the icon too (16 x 16) - Rect iconRect = new Rect(screenPos.x - 8f, (float)Screen.height - screenPos.y - 39.0f, 16f, 16f); - - // Draw the marker - Graphics.DrawTexture(markerRect, GameDatabase.Instance.GetTexture("Squad/Contracts/Icons/marker", false), new Rect(0.0f, 0.0f, 1f, 1f), 0, 0, 0, 0, new Color(0.5f, 0.5f, 0.5f, 0.5f * (wpd.currentAlpha - 0.3f) / 0.7f)); - - // Draw the icon, but support blinking - if (!Util.IsNavPoint(wpd.waypoint) || !FinePrint.WaypointManager.navWaypoint.blinking || (int)((Time.fixedTime - (int)Time.fixedTime) * 4) % 2 == 0) - { - Graphics.DrawTexture(iconRect, ContractDefs.textures[wpd.waypoint.id], new Rect(0.0f, 0.0f, 1f, 1f), 0, 0, 0, 0, SystemUtilities.RandomColor(wpd.waypoint.seed, wpd.currentAlpha)); - } - - // Hint text! - if (iconRect.Contains(Event.current.mousePosition)) - { - // Add agency to label - if (wpd.waypoint.contractReference != null) - { - label += "\n" + wpd.waypoint.contractReference.Agent.Name; - } - float width = 240f; - float height = hintTextStyle.CalcHeight(new GUIContent(label), width); - float yoffset = height + 48.0f; - GUI.Box(new Rect(screenPos.x - width/2.0f, (float)Screen.height - screenPos.y - yoffset, width, height), label, hintTextStyle); - } - } - } - - private void ShowNavigationWindow() - { - if (selectedWaypoint != null) - { - GUI.skin = HighLogic.Skin; - windowPos = GUILayout.Window(10, windowPos, NavigationWindow, waypointName, GUILayout.MinWidth(224)); - } - } - - private void NavigationWindow(int windowID) - { - if (selectedWaypoint == null) - { - return; - } - - GUILayout.BeginVertical(); - if (!Util.IsNavPoint(selectedWaypoint)) - { - if (GUILayout.Button("Activate Navigation", HighLogic.Skin.button, GUILayout.ExpandWidth(true))) - { - FinePrint.WaypointManager.setupNavPoint(selectedWaypoint); - FinePrint.WaypointManager.activateNavPoint(); - selectedWaypoint = null; - } - } - else - { - if (GUILayout.Button("Deactivate Navigation", HighLogic.Skin.button, GUILayout.ExpandWidth(true))) - { - FinePrint.WaypointManager.clearNavPoint(); - selectedWaypoint = null; - } - - } - if (CustomWaypoints.Instance.IsCustom(selectedWaypoint)) - { - if (GUILayout.Button("Edit Custom Waypoint", HighLogic.Skin.button, GUILayout.ExpandWidth(true))) - { - CustomWaypointGUI.EditWaypoint(selectedWaypoint); - selectedWaypoint = null; - } - if (GUILayout.Button("Delete Custom Waypoint", HighLogic.Skin.button, GUILayout.ExpandWidth(true))) - { - CustomWaypointGUI.DeleteWaypoint(selectedWaypoint); - selectedWaypoint = null; - } - } - GUILayout.EndVertical(); - } - - /// - /// Calculates the time to the distance based on the vessels srfSpeed and transform it to a readable string. - /// - /// The waypoint - /// Distance in meters - /// - protected string GetTimeToWaypoint(WaypointData wpd) - { - Vessel v = FlightGlobals.ActiveVessel; - if (v.srfSpeed < 0.1) - { - return null; - } - - double time = (wpd.distanceToActive / v.horizontalSrfSpeed); - - // Earthtime - uint SecondsPerYear = 31536000; // = 365d - uint SecondsPerDay = 86400; // = 24h - uint SecondsPerHour = 3600; // = 60m - uint SecondsPerMinute = 60; // = 60s - - if (GameSettings.KERBIN_TIME == true) - { - SecondsPerYear = 9201600; // = 426d - SecondsPerDay = 21600; // = 6h - SecondsPerHour = 3600; // = 60m - SecondsPerMinute = 60; // = 60s - } - - int years = (int)(time / SecondsPerYear); - time -= years * SecondsPerYear; - - int days = (int)(time / SecondsPerDay); - time -= days * SecondsPerDay; - - int hours = (int)(time / SecondsPerHour); - time -= hours * SecondsPerHour; - - int minutes = (int)(time / SecondsPerMinute); - time -= minutes * SecondsPerMinute; - - int seconds = (int)(time); - - string output = ""; - if (years != 0) - { - output += years + "y"; - } - if (days != 0) - { - if (output.Length != 0) output += ", "; - output += days + "d"; - } - if (hours != 0 || minutes != 0 || seconds != 0 || output.Length == 0) - { - if (output.Length != 0) output += ", "; - output += hours.ToString("D2") + ":" + minutes.ToString("D2") + ":" + seconds.ToString("D2"); - } - - return output; - } - - public void HandleClick(WaypointData wpd) - { - // Translate to screen position - Vector3d localSpacePoint = wpd.celestialBody.GetWorldSurfacePosition(wpd.waypoint.latitude, wpd.waypoint.longitude, wpd.waypoint.altitude); - Vector3d scaledSpacePoint = ScaledSpace.LocalToScaledSpace(localSpacePoint); - Vector3 screenPos = MapView.MapCamera.camera.WorldToScreenPoint(new Vector3((float)scaledSpacePoint.x, (float)scaledSpacePoint.y, (float)scaledSpacePoint.z)); - - Rect markerRect = new Rect(screenPos.x - 15f, (float)Screen.height - screenPos.y - 45.0f, 30f, 45f); - - if (markerRect.Contains(Event.current.mousePosition)) - { - selectedWaypoint = wpd.waypoint; - windowPos = new Rect(markerRect.xMin - 97, markerRect.yMax + 12, 224, 60); - waypointName = wpd.waypoint.name + (wpd.waypoint.isClustered ? (" " + StringUtilities.IntegerToGreek(wpd.waypoint.index)) : ""); - newClick = false; - } - else if (newClick) - { - selectedWaypoint = null; - } - } - - public void HintText(WaypointData wpd) - { - // Translate to screen position - Vector3d localSpacePoint = wpd.celestialBody.GetWorldSurfacePosition(wpd.waypoint.latitude, wpd.waypoint.longitude, wpd.waypoint.altitude); - Vector3d scaledSpacePoint = ScaledSpace.LocalToScaledSpace(localSpacePoint); - Vector3 screenPos = MapView.MapCamera.camera.WorldToScreenPoint(new Vector3((float)scaledSpacePoint.x, (float)scaledSpacePoint.y, (float)scaledSpacePoint.z)); - - Rect iconRect = new Rect(screenPos.x - 8f, (float)Screen.height - screenPos.y - 39.0f, 16f, 16f); - - // Hint text! - if (iconRect.Contains(Event.current.mousePosition)) - { - string label = wpd.waypoint.name + (wpd.waypoint.isClustered ? (" " + StringUtilities.IntegerToGreek(wpd.waypoint.index)) : ""); - float width = 240f; - float height = hintTextStyle.CalcHeight(new GUIContent(label), width); - float yoffset = height + 48.0f; - GUI.Box(new Rect(screenPos.x - width / 2.0f, (float)Screen.height - screenPos.y - yoffset, width, height), label, hintTextStyle); - } - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; +using KSP; +using Contracts; +using FinePrint; +using FinePrint.Utilities; + +namespace WaypointManager +{ + [KSPAddon(KSPAddon.Startup.SpaceCentre, true)] + class WaypointFlightRenderer : MonoBehaviour + { + private GUIStyle nameStyle = null; + private GUIStyle valueStyle = null; + private GUIStyle hintTextStyle = null; + + private bool visible = true; + private Waypoint selectedWaypoint = null; + private string waypointName = ""; + private Rect windowPos; + private bool newClick = false; + private AltimeterSliderButtons asb = null; + + private float referencePos = 0.0f; + private float referenceUISize = 0.0f; + private float lastPos = 0.0f; + private bool referenceSet = false; + + private const double MIN_TIME = 300; + private const double MIN_DISTANCE = 25000; + private const double MIN_SPEED = MIN_DISTANCE / MIN_TIME; + private const double FADE_TIME = 20; + + void Start() + { + if (MapView.MapCamera.gameObject.GetComponent() == null) + { + MapView.MapCamera.gameObject.AddComponent(); + + // Destroy this object - otherwise we'll have two + Destroy(this); + } + + GameEvents.onGameSceneLoadRequested.Add(new EventData.OnEvent(OnGameSceneLoadRequested)); + GameEvents.onHideUI.Add(new EventVoid.OnEvent(OnHideUI)); + GameEvents.onShowUI.Add(new EventVoid.OnEvent(OnShowUI)); + } + + protected void OnDestroy() + { + GameEvents.onGameSceneLoadRequested.Remove(new EventData.OnEvent(OnGameSceneLoadRequested)); + GameEvents.onHideUI.Remove(OnHideUI); + GameEvents.onShowUI.Remove(OnShowUI); + } + + public void OnGameSceneLoadRequested(GameScenes gameScene) + { + asb = null; + } + + public void OnHideUI() + { + visible = false; + } + + public void OnShowUI() + { + visible = true; + } + + public void OnGUI() + { + if (visible) + { + if (Event.current.type == EventType.MouseUp && Event.current.button == 0) + { + newClick = true; + } + + if (HighLogic.LoadedSceneIsFlight || HighLogic.LoadedScene == GameScenes.TRACKSTATION) + { + // Draw the marker for custom waypoints that are currently being created + CustomWaypointGUI.DrawMarker(); + + // Draw waypoints if not in career mode + if (ContractSystem.Instance == null && MapView.MapIsEnabled) + { + foreach (WaypointData wpd in WaypointData.Waypoints) + { + if (wpd.celestialBody != null && wpd.waypoint.celestialName == wpd.celestialBody.name) + { + if (Event.current.type == EventType.Repaint) + { + wpd.SetAlpha(); + Util.DrawWaypoint(wpd.celestialBody, wpd.waypoint.latitude, wpd.waypoint.longitude, + wpd.waypoint.altitude, wpd.waypoint.id, wpd.waypoint.seed, wpd.currentAlpha); + } + + // Handling clicking on the waypoint + if (Event.current.type == EventType.MouseUp && Event.current.button == 0) + { + HandleClick(wpd); + } + + // Draw hint text + if (Event.current.type == EventType.Repaint) + { + HintText(wpd); + } + } + } + } + } + + if (HighLogic.LoadedSceneIsFlight && !MapView.MapIsEnabled) + { + SetupStyles(); + + WaypointData.CacheWaypointData(); + + foreach (WaypointData wpd in WaypointData.Waypoints) + { + DrawWaypoint(wpd); + } + } + + if (HighLogic.LoadedSceneIsFlight && (!MapView.MapIsEnabled || ContractSystem.Instance == null)) + { + ShowNavigationWindow(); + } + } + } + + // Styles taken directly from Kerbal Engineer Redux - because they look great and this will + // make our display consistent with that + protected void SetupStyles() + { + if (nameStyle != null) + { + return; + } + + nameStyle = new GUIStyle(HighLogic.Skin.label) + { + normal = + { + textColor = Color.white + }, + margin = new RectOffset(), + padding = new RectOffset(5, 0, 0, 0), + alignment = TextAnchor.MiddleRight, + fontSize = 11, + fontStyle = FontStyle.Bold, + fixedHeight = 20.0f + }; + + valueStyle = new GUIStyle(HighLogic.Skin.label) + { + margin = new RectOffset(), + padding = new RectOffset(0, 5, 0, 0), + alignment = TextAnchor.MiddleLeft, + fontSize = 11, + fontStyle = FontStyle.Normal, + fixedHeight = 20.0f + }; + + hintTextStyle = new GUIStyle(HighLogic.Skin.box) + { + padding = new RectOffset(4, 4, 7, 4), + font = MapView.OrbitIconsTextSkin.label.font, + fontSize = MapView.OrbitIconsTextSkin.label.fontSize, + fontStyle = MapView.OrbitIconsTextSkin.label.fontStyle, + fixedWidth = 0, + fixedHeight = 0, + stretchHeight = true, + stretchWidth = true + }; + } + + protected void DrawWaypoint(WaypointData wpd) + { + // Not our planet + CelestialBody celestialBody = FlightGlobals.currentMainBody; + if (celestialBody == null || wpd.waypoint.celestialName != celestialBody.name) + { + return; + } + + // Check if the waypoint should be visible + if (!wpd.waypoint.visible) + { + return; + } + + // Figure out waypoint label + string label = wpd.waypoint.name + (wpd.waypoint.isClustered ? (" " + StringUtilities.IntegerToGreek(wpd.waypoint.index)) : ""); + + // Set the alpha and do a nice fade + wpd.SetAlpha(); + + // Decide whether to actually draw the waypoint + if (FlightGlobals.ActiveVessel != null) + { + // Figure out the distance to the waypoint + Vessel v = FlightGlobals.ActiveVessel; + + // Only change alpha if the waypoint isn't the nav point + if (!Util.IsNavPoint(wpd.waypoint)) + { + // Get the distance to the waypoint at the current speed + double speed = v.srfSpeed < MIN_SPEED ? MIN_SPEED : v.srfSpeed; + double directTime = Util.GetStraightDistance(wpd) / speed; + + // More than two minutes away + if (directTime > MIN_TIME || Config.waypointDisplay != Config.WaypointDisplay.ALL) + { + return; + } + else if (directTime >= MIN_TIME - FADE_TIME) + { + wpd.currentAlpha = (float)((MIN_TIME - directTime) / FADE_TIME) * Config.opacity; + } + } + // Draw the distance information to the nav point + else + { + // Draw the distance to waypoint text + if (Event.current.type == EventType.Repaint) + { + if (asb == null) + { + asb = UnityEngine.Object.FindObjectOfType(); + } + + if (referenceUISize != ScreenSafeUI.VerticalRatio || !referenceSet) + { + referencePos = ScreenSafeUI.referenceCam.ViewportToScreenPoint(asb.transform.position).y; + referenceUISize = ScreenSafeUI.VerticalRatio; + + // Need two consistent numbers in a row to set the reference + if (lastPos == referencePos) + { + referenceSet = true; + } + else + { + lastPos = referencePos; + } + } + + float ybase = (referencePos - ScreenSafeUI.referenceCam.ViewportToScreenPoint(asb.transform.position).y + Screen.height / 11.67f) / ScreenSafeUI.VerticalRatio; + + string timeToWP = GetTimeToWaypoint(wpd); + if (Config.hudDistance) + { + GUI.Label(new Rect((float)Screen.width / 2.0f - 188f, ybase, 240f, 20f), "Distance to " + label + ":", nameStyle); + GUI.Label(new Rect((float)Screen.width / 2.0f + 60f, ybase, 120f, 20f), + v.state != Vessel.State.DEAD ? Util.PrintDistance(wpd) : "N/A", valueStyle); + ybase += 18f; + } + + if (timeToWP != null && Config.hudTime) + { + GUI.Label(new Rect((float)Screen.width / 2.0f - 188f, ybase, 240f, 20f), "ETA to " + label + ":", nameStyle); + GUI.Label(new Rect((float)Screen.width / 2.0f + 60f, ybase, 120f, 20f), + v.state != Vessel.State.DEAD ? timeToWP : "N/A", valueStyle); + ybase += 18f; + } + + if (Config.hudHeading) + { + GUI.Label(new Rect((float)Screen.width / 2.0f - 188f, ybase, 240f, 20f), "Heading to " + label + ":", nameStyle); + GUI.Label(new Rect((float)Screen.width / 2.0f + 60f, ybase, 120f, 20f), + v.state != Vessel.State.DEAD ? wpd.heading.ToString("N1") : "N/A", valueStyle); + ybase += 18f; + } + + if (Config.hudAngle && v.mainBody == wpd.celestialBody) + { + double distance = Util.GetLateralDistance(wpd); + double heightDist = wpd.waypoint.altitude + wpd.waypoint.height - v.altitude; + double angle = Math.Atan2(heightDist, distance) * 180.0 / Math.PI; + + GUI.Label(new Rect((float)Screen.width / 2.0f - 188f, ybase, 240f, 20f), "Angle to " + label + ":", nameStyle); + GUI.Label(new Rect((float)Screen.width / 2.0f + 60f, ybase, 120f, 20f), + v.state != Vessel.State.DEAD ? angle.ToString("N2") : "N/A", valueStyle); + ybase += 18f; + + if (v.srfSpeed >= 0.1) + { + double velAngle = 90 - Math.Acos(Vector3d.Dot(v.srf_velocity.normalized, v.upAxis)) * 180.0 / Math.PI; + + GUI.Label(new Rect((float)Screen.width / 2.0f - 188f, ybase, 240f, 20f), "Velocity pitch angle:", nameStyle); + GUI.Label(new Rect((float)Screen.width / 2.0f + 60f, ybase, 120f, 20f), + v.state != Vessel.State.DEAD ? velAngle.ToString("N2") : "N/A", valueStyle); + ybase += 18f; + } + } + if(Config.hudCoordinates&&v.mainBody==wpd.celestialBody) + { + ybase += 18; + GUI.Label(new Rect((float)Screen.width / 2.0f - 188f, ybase, 240f, 38f), "Coordinates of " + label + ":", nameStyle); + GUI.Label(new Rect((float)Screen.width / 2.0f + 60f, ybase, 120f, 38f), + v.state != Vessel.State.DEAD ? string.Format("{0}\r\n{1}", Util.DecimalDegreesToDMS(wpd.waypoint.latitude,true), Util.DecimalDegreesToDMS(wpd.waypoint.longitude,false)) : "N/A", valueStyle); + ybase += 18f; + } + } + } + } + + // Don't draw the waypoint + if (Config.waypointDisplay == Config.WaypointDisplay.NONE) + { + return; + } + + // Translate to scaled space + Vector3d localSpacePoint = celestialBody.GetWorldSurfacePosition(wpd.waypoint.latitude, wpd.waypoint.longitude, wpd.waypoint.height + wpd.waypoint.altitude); + Vector3d scaledSpacePoint = ScaledSpace.LocalToScaledSpace(localSpacePoint); + + // Don't draw if it's behind the camera + if (Vector3d.Dot(MapView.MapCamera.camera.transform.forward, scaledSpacePoint.normalized) < 0.0) + { + return; + } + + // Translate to screen position + Vector3 screenPos = MapView.MapCamera.camera.WorldToScreenPoint(new Vector3((float)scaledSpacePoint.x, (float)scaledSpacePoint.y, (float)scaledSpacePoint.z)); + + // Draw the marker at half-resolution (30 x 45) - that seems to match the one in the map view + Rect markerRect = new Rect(screenPos.x - 15f, (float)Screen.height - screenPos.y - 45.0f, 30f, 45f); + + // Set the window position relative to the selected waypoint + if (selectedWaypoint == wpd.waypoint) + { + windowPos = new Rect(markerRect.xMin - 97, markerRect.yMax + 12, 224, 60); + } + + // Handling clicking on the waypoint + if (Event.current.type == EventType.MouseUp && Event.current.button == 0) + { + if (markerRect.Contains(Event.current.mousePosition)) + { + selectedWaypoint = wpd.waypoint; + windowPos = new Rect(markerRect.xMin - 97, markerRect.yMax + 12, 224, 60); + waypointName = label; + newClick = false; + } + else if (newClick) + { + selectedWaypoint = null; + } + } + + // Only handle on repaint events + if (Event.current.type == EventType.Repaint) + { + // Half-res for the icon too (16 x 16) + Rect iconRect = new Rect(screenPos.x - 8f, (float)Screen.height - screenPos.y - 39.0f, 16f, 16f); + + // Draw the marker + Graphics.DrawTexture(markerRect, GameDatabase.Instance.GetTexture("Squad/Contracts/Icons/marker", false), new Rect(0.0f, 0.0f, 1f, 1f), 0, 0, 0, 0, new Color(0.5f, 0.5f, 0.5f, 0.5f * (wpd.currentAlpha - 0.3f) / 0.7f)); + + // Draw the icon, but support blinking + if (!Util.IsNavPoint(wpd.waypoint) || !FinePrint.WaypointManager.navWaypoint.blinking || (int)((Time.fixedTime - (int)Time.fixedTime) * 4) % 2 == 0) + { + Graphics.DrawTexture(iconRect, ContractDefs.textures[wpd.waypoint.id], new Rect(0.0f, 0.0f, 1f, 1f), 0, 0, 0, 0, SystemUtilities.RandomColor(wpd.waypoint.seed, wpd.currentAlpha)); + } + + // Hint text! + if (iconRect.Contains(Event.current.mousePosition)) + { + // Add agency to label + if (wpd.waypoint.contractReference != null) + { + label += "\n" + wpd.waypoint.contractReference.Agent.Name; + } + float width = 240f; + float height = hintTextStyle.CalcHeight(new GUIContent(label), width); + float yoffset = height + 48.0f; + GUI.Box(new Rect(screenPos.x - width/2.0f, (float)Screen.height - screenPos.y - yoffset, width, height), label, hintTextStyle); + } + } + } + + private void ShowNavigationWindow() + { + if (selectedWaypoint != null) + { + GUI.skin = HighLogic.Skin; + windowPos = GUILayout.Window(10, windowPos, NavigationWindow, waypointName, GUILayout.MinWidth(224)); + } + } + + private void NavigationWindow(int windowID) + { + if (selectedWaypoint == null) + { + return; + } + + GUILayout.BeginVertical(); + if (!Util.IsNavPoint(selectedWaypoint)) + { + if (GUILayout.Button("Activate Navigation", HighLogic.Skin.button, GUILayout.ExpandWidth(true))) + { + FinePrint.WaypointManager.setupNavPoint(selectedWaypoint); + FinePrint.WaypointManager.activateNavPoint(); + selectedWaypoint = null; + } + } + else + { + if (GUILayout.Button("Deactivate Navigation", HighLogic.Skin.button, GUILayout.ExpandWidth(true))) + { + FinePrint.WaypointManager.clearNavPoint(); + selectedWaypoint = null; + } + + } + if (CustomWaypoints.Instance.IsCustom(selectedWaypoint)) + { + if (GUILayout.Button("Edit Custom Waypoint", HighLogic.Skin.button, GUILayout.ExpandWidth(true))) + { + CustomWaypointGUI.EditWaypoint(selectedWaypoint); + selectedWaypoint = null; + } + if (GUILayout.Button("Delete Custom Waypoint", HighLogic.Skin.button, GUILayout.ExpandWidth(true))) + { + CustomWaypointGUI.DeleteWaypoint(selectedWaypoint); + selectedWaypoint = null; + } + } + GUILayout.EndVertical(); + } + + /// + /// Calculates the time to the distance based on the vessels srfSpeed and transform it to a readable string. + /// + /// The waypoint + /// Distance in meters + /// + protected string GetTimeToWaypoint(WaypointData wpd) + { + Vessel v = FlightGlobals.ActiveVessel; + if (v.srfSpeed < 0.1) + { + return null; + } + + double time = (wpd.distanceToActive / v.horizontalSrfSpeed); + + // Earthtime + uint SecondsPerYear = 31536000; // = 365d + uint SecondsPerDay = 86400; // = 24h + uint SecondsPerHour = 3600; // = 60m + uint SecondsPerMinute = 60; // = 60s + + if (GameSettings.KERBIN_TIME == true) + { + SecondsPerYear = 9201600; // = 426d + SecondsPerDay = 21600; // = 6h + SecondsPerHour = 3600; // = 60m + SecondsPerMinute = 60; // = 60s + } + + int years = (int)(time / SecondsPerYear); + time -= years * SecondsPerYear; + + int days = (int)(time / SecondsPerDay); + time -= days * SecondsPerDay; + + int hours = (int)(time / SecondsPerHour); + time -= hours * SecondsPerHour; + + int minutes = (int)(time / SecondsPerMinute); + time -= minutes * SecondsPerMinute; + + int seconds = (int)(time); + + string output = ""; + if (years != 0) + { + output += years + "y"; + } + if (days != 0) + { + if (output.Length != 0) output += ", "; + output += days + "d"; + } + if (hours != 0 || minutes != 0 || seconds != 0 || output.Length == 0) + { + if (output.Length != 0) output += ", "; + output += hours.ToString("D2") + ":" + minutes.ToString("D2") + ":" + seconds.ToString("D2"); + } + + return output; + } + + public void HandleClick(WaypointData wpd) + { + // Translate to screen position + Vector3d localSpacePoint = wpd.celestialBody.GetWorldSurfacePosition(wpd.waypoint.latitude, wpd.waypoint.longitude, wpd.waypoint.altitude); + Vector3d scaledSpacePoint = ScaledSpace.LocalToScaledSpace(localSpacePoint); + Vector3 screenPos = MapView.MapCamera.camera.WorldToScreenPoint(new Vector3((float)scaledSpacePoint.x, (float)scaledSpacePoint.y, (float)scaledSpacePoint.z)); + + Rect markerRect = new Rect(screenPos.x - 15f, (float)Screen.height - screenPos.y - 45.0f, 30f, 45f); + + if (markerRect.Contains(Event.current.mousePosition)) + { + selectedWaypoint = wpd.waypoint; + windowPos = new Rect(markerRect.xMin - 97, markerRect.yMax + 12, 224, 60); + waypointName = wpd.waypoint.name + (wpd.waypoint.isClustered ? (" " + StringUtilities.IntegerToGreek(wpd.waypoint.index)) : ""); + newClick = false; + } + else if (newClick) + { + selectedWaypoint = null; + } + } + + public void HintText(WaypointData wpd) + { + // Translate to screen position + Vector3d localSpacePoint = wpd.celestialBody.GetWorldSurfacePosition(wpd.waypoint.latitude, wpd.waypoint.longitude, wpd.waypoint.altitude); + Vector3d scaledSpacePoint = ScaledSpace.LocalToScaledSpace(localSpacePoint); + Vector3 screenPos = MapView.MapCamera.camera.WorldToScreenPoint(new Vector3((float)scaledSpacePoint.x, (float)scaledSpacePoint.y, (float)scaledSpacePoint.z)); + + Rect iconRect = new Rect(screenPos.x - 8f, (float)Screen.height - screenPos.y - 39.0f, 16f, 16f); + + // Hint text! + if (iconRect.Contains(Event.current.mousePosition)) + { + string label = wpd.waypoint.name + (wpd.waypoint.isClustered ? (" " + StringUtilities.IntegerToGreek(wpd.waypoint.index)) : ""); + float width = 240f; + float height = hintTextStyle.CalcHeight(new GUIContent(label), width); + float yoffset = height + 48.0f; + GUI.Box(new Rect(screenPos.x - width / 2.0f, (float)Screen.height - screenPos.y - yoffset, width, height), label, hintTextStyle); + } + } + } +} From dd22964a0ae5c796bc0891d54ec6a7d6d2fcd692 Mon Sep 17 00:00:00 2001 From: Samuel Swanner Date: Tue, 1 Sep 2015 14:19:13 -0500 Subject: [PATCH 3/4] Fixing text formating issues and a conversion problem with negative degrees --- GameData/WaypointManager/WaypointManager.dll | Bin 70656 -> 69632 bytes source/WaypointManager/Util.cs | 11 +++++------ .../WaypointManager/WaypointFlightRenderer.cs | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/GameData/WaypointManager/WaypointManager.dll b/GameData/WaypointManager/WaypointManager.dll index e648c3e436b5effc85df1c81584edc05b1a211c5..0cb5b9526bc5d1dd161f8bf78056a8382b008d3f 100644 GIT binary patch literal 69632 zcmd4434D~*^*4T=WuDn5d1jKCtYm}aA(OCbC5jRj7u*$80;1pwf(%Yjt08evL{UJf z6vY(~t5mI0amNBKsMT7v*lO#pb*a{BYk!t%@%^52pIH*n+P=T{|M`z{o_o$c_uO;O zJ$Jv)lOrZCCxeJg{QKq`q6hKhU%SBX1{p+0Y95ZzCjYPM9@Gy1Ro%oHGdpYNrc-C8 zr=MMW>hw8tQuAt0IlVT0?ws10b85#OHKF$G)M=*=3Wo#zUC~D$NOZVn(59V#JT9Bt zTU1{g)P@lGG$Kn;pC5p5EyC@15*15WS9&vn^_O1;@u2h1p!Q3dmH*E_U6e`qT?n~H zaR74Hup#>XrfC!c?Y;gQRrQVgF1!}%7sUI3A5wr1ns@pS=K)`7L0`m4SzW!eY#>h? zlmFgss#MICTs`2f~uG zC(q0z@^n;*>F6%K42a5y*?XAny3`0H?{3R!zzKSoab@?uAU3mo{)~b{|Ga%*w?dHVeQ5X>7$pFc8(vyt?!{ekPM&aXrPCIzFe4cTt4 zcrJLp^m-SUSry#%jmdb*ue?Av*EPJ5vj^7@a)E1Hl;cyOyi;%loRDoiF`G{!Q_5nM z^ZE9igOCez$njm2i&AdKXmjNdAN>vvj_go}T=ylrG;}LJ<2*YLK#_ywjKZU<9iZn% zFbrr-H1^zpz%J{TuwCb9L=4Dqi~|ozrp5vfC6Dr@#vu?Uf7gL{rmNx2>B9h02LX+l zJ@82M9S$03GUl;8Ee0hky*-B@(l%4$<h$=*A8_agOFPm+mB>^+i&}p%^Y?1H{X0?y9MDQIL9*M1b|Ldd>~T} zS`UMGysWJAIF39XfE2AX9#^yznASIK1*c^Y$FDFg(<&;estVEyUm`V0WOF^-Y zk(WCeL8VhA%Vm(70-AFoKx!&8QqRSpcEQYM%(m^ei(q=yb+&}12gUe=PBf{-V^KTW z_G_-D7f?9eIf-SQ%wQTo*`yxs6-#zmXs=f1bdWCMo^e+kx$3D?fTvMgbDdL}dFgCa z$bh)iq;nc5>joqe&gmR*&H$7iR`t4P0tMgl(Qbb{L*djuiBpdjAkCxa6l9mTr!y0b zv6vle@ljlwJN00CWX4MDlD5}T3c9F>6PyJ$72=%Wjiq*J%P@Dqfb?s|x?_b>XO_zT znz5YmDAg`m&K`|YKBaVaU65jDsU z)LSfJCt4D!sQL0bFh8oEM%)LGt>svmUDmdq4f8Ga%N-nwf`tFBehbUTsZ>{4P?Jn_ z{}?MhLOwSq;_ z@tLtIyQ*b4IjBmDoU>u@)Et1s@FInzfVd5-+OC$=oD8LDCJnWx)8RDbHko3|oNOgx zpqWjsap!Usrtd(3dLl?CV{00IJ=ZyhB`Xh0Gm{(clmzeQEiexRnP7pcRkB7QKwI z5*<-*yQzs2JXzVzwa^by7@3|FTFebAa}U>OZX(}#T|UOS0BP1aV(0XM-xaG2#=S!##I$a3Js2d?m=8%a@l-;EV-R<{Ci6ORyQwq0A`P~YM zZQ78jm`bKn4yxF4wVu!Fcx`Xn3T}Sc-X_Dnj?76ExCv;%2T7Uf%E(`kB|JWv;Tfz(qE z5*%06oxEBkh z&+APHr!VeJh?zeBG@0OFdjk&3?k$bZxfJQU_XAKgDYMXc1)^xe3}-c+E8)T2i*gZc z3=yC_mLod4_vdJy`e4b@#bcr>104XdfCzNgtpM`{aa z-VC#GAO9Tc6e`+*VGl9WkHmhjpxUP<=Onm zZ(>r=Yb~2O?8O3o-EM$8z*!&@0gN2u77oS@%#?n#u(avry&DNOkM==5Nhc0-x;;5} zwEu2rfp%|i*k$6j%fj~LBNv1IomCqf;-ynij3C#mYERjz(;SorT^)Q2)0c51y8?T3 ztUm3t_izULWVSd<#fjlayl#IHeuNGH`LRoOAudqjECZC0*P;>kg2BOzGvF)-NMY~` z1fmor=SNJtn!!p2KW4BBAR8wFQ`a#0T7Y0agRa%ULk(PG{KLA{9I-$5QC^6>juWt0 zU(|B0$5Y1KkC-tp(YBl$ILen7Dk0mKx{=dktPMCf17xXdn6NfWb=Gm{76$79Qnv!c z{8(~#I6vVa#=pE^(Y64+4ICY=@*V9{u%za(X&6yU64p=c;S$YhmZ)$%di?ItIY zFKqimw#j1@7Tg%glY5aZ>lhwTu;H*b`Q6;=+b8=bx#9lpQ+lOBnwnwI1!K*=n|6Kk z4XRh_S7W>4SG{z)tUuGM&;YRXOy3~Svh+H6mZyI%&x-Ve@~lj6muFS_C4Ocg#jF{! z^a7Ug0&3ZGZbxA<210PE561u)ygppWplob@&#+hQ5{_LnM#W+R)H`jjSj>V<)d}YZ zjS?9|R0E64tVH#&C9=6vk|~iiJU3PYXk2x6(g`Sgc<2Dw0&N8Tz5xY*Cp$mIv%K1qAI{G} z;Z1{>bla8KJI4q`77bvIZ7NgGPs!;;DCJT7e$gpDAyzW+v1ly=3@A5^-V zNzH1V6rp}Cgk_eh;{)*og1z@d_s@|Ik9+yyVEC~Dmes`y1<{3yX5Q{ih?F?dLQmZX z9Ev_Bii%8w&cPEM8DuJ*3iSAEjFE+rq89_@F&Tt+UmB>i1gO;gBfCb~B5wF9sy}s+hBwoe`1^)CVX4nh{ znR+ng7deUpGE;t^qi_$)l=rd}=TT0|(-bEEPj50@K*#})n}_1w-z8V&$iHAAY&#Z$ zWrNbl5i-wKY*W=WKe@7{xZG#Ay^Na6dYFSrSyJ3AW{tQBWb~=*YS?hJy|Nc=oAPkB zg4%L2h)QvJsNS$)>7Vag^X2w?4%n+5(W(IQ=o?*!XjKq-xY5_G=HEl3cFDwumojCp z`(s0qJ*m;ox;<;he!uVW*lvXDTfJOAo61!FifvhOSX~zvLOIx4iAl~jJd`u}5|ctc zFSai$REMbAPE_0YxF&6<5^MNx*eK%X2Fv3XThKN~Y?o-ad5Y_FqKO7VhA;I5o{c{7 z7O!kYq@LvTKJgZ1+$@E?#oNW6lBKY>C|fT%nWt9Bhm*3mC>!@8v$rVQ_9C;lc%|4& zxMR)=VQ=xu^0k1>qq5j`kg{!`ZpGbyiPNip$@VKAIuwOR?}aw#JO%v%>0yu@rTI?- zN&f(0=NW`R_+?+hv%Lt;b3olT%4c3!lTiuFm2DE|SBS--61P|B6|B_rpio>WR_fP4 zQZKNCs->tY?4hPdB5(B4UMb(Y0cGiBr!gL=CGPNh8&I&z*>4MTfeIA61L#Y1LO6@>e z?EFXUY5N1-#Pw{sp{Qc)zuP{gfS>xh~PK?v1DP zs~6R>yYkogU;9_Gb@f>&c(~9g{PheM#yyNaBvH6V+m6SO8{z z{D@gevT@vuau_rkI`B{Ld>#DV{SmM8MvK)wfP>B(pd!}jh7=Qh1XH=!GSeS%UKcYb zrc!)-1$h>Y!80!XKo^tsE>2hGI-CJ#GuNfwgv@}YWOYu3grMHV8C%}eTb!LYlq6sC zFfrP$Qq1ov`Ha^Tn`22~>(1Mvm63XfCFy2&BWGo$KZi;+C8>8oHB#>Z-pcK{y9u$) zW3s7G7KaJV7|aev=Gn2*plawYYX-dw#ewbv0hJ+dy#j*NDZMHux_45#C<~?iUbjX9 zx|PV4*ZCu~=w>-kZ!EK1#1>=m&BvpQ94h%F0gcrAV5Qxt9|%?I>axChPAJH;L}kUI zkkw@zBbC#l2tUpdS{32u9AS_m+?yi|R)k;W2tyR%btWue{aJ0%RHXCqJ?zBQ+L-kN zkf==gfn+E=IlC+w4s$9;xRrz1`wtd@naWi<(W`g5tnY}2D%aum#FiALduhT{(YzNG z^11JvgD3wAJbW0J((HylaCEb46rNp&0i+Ed%{BPY%Ho+!KJsvmjO8QIC*&iK;K7V)#Px$Odc<7e%Cp?0N$E#dIHHV8)8dgo|g=P!?WQ~oI z)W={~eX2q5b*fbM+Y`6)=6TbzHec0C>X^LLB{g8lix$0Ec2!^5`7~}z;e=A3y)_=2 zmz@>Mw%DV-dEUPI_Le#BKaiQ{?JKjl4Gzr9Tyn*pZ4m5hgM1p-2AnYO({o`r^euMh zb8w-6Wpv++Jg6jSb#VawH?HS6*^1a7q-;B}c3HndlFm<1!Rw@b2eWLWz}9sTfbs&b zYg1jx?A`}4GA4(RcR!^s(?qGP-FGX;!8v(-%hB6LhvfK-?`@-en(vl9{=bl&Pt#X6 z{MMq2V1pXAKjuSX|24=%?9TyZ|9!HCJpiQacm?}o)?{`5ne9Iy0TlBmtMk)usg6T) zsuxrTXVEK*Kq!VeCGMlM}_qHO!ax40H#p^ z%RBtdX7F=8?kU~p-y6^w>zhvW`_6Q|^kdO^{TS0b;gJ?!3~}`{>2laxewah$J7$ys4Oj*h>CBz^oTL}a z;ld(A%eS!{AuK$=sLsHZQjpJ)xwz~wJ%|jCnfd^p!u6}u(NL~%KBK2cW9*VCCZ7nH zditCk&RvwAzAcAylS#jtMKOSNUV>tDa~CDP1N{j8W_P{_GA4fAYY{^|e?{g!w;(9z zeCrU{WxWiJ&c7jIK!#%+cu5k?4r>gn^Y0)#sFq~m9J&ayF(<|Tih-Hmmte{EyM`>l zvTPC7e8vtF6)h&awkv9BU0vZ!@ByxnY ziZD1wkkQE~m?6rLVx&jqI1W-tj>{3Gvo_LQIfC@CM*7Me0Zz!--kc*y2W6x;GGPI0 zD*k7rx8(@pZASVJIfD3>kdI&fE1> zUi=~iC%Ho4%7ICfeZI-IX?rI7@!urpYtWp30ZevRN?g`O8gXCSr>s2_QMSaOh*$4A z10kmtsrU?&w`U@lTOun9H_;A7;yx-C6k@lc{{P=B1=e$eVpOZE6$WrO|Gi?ImQvem zTe*suvdh3SnN7rJipNcBcX@-JsNb3e8y^x!_bR^$m~|ZuuN#kLE>q}8B{daCsI^?FmLZ}C0 zgK1qSySy#Z)lV1Qy{SPKjX7 zGYC>D1c2QM#S{kOO+^%I*x&XG9s0^;QC;qe;&7)V=Fw{g^EOZtcLYvANwHIP4Ia8& z669;4={vamr*ip!10YTEVE9{}4lXlQglzCA#v^+X6niGNRWn!)Hc~bsoG3sl2GB?& zPC;9Sl>2&1$0d@sp@``f#cD{}PTVXHbe00J+o$3P^dtb(EfKy|QAX)4kO;xwuwbsX z&N6UZXa_pWIUO29R|NuHl>j|e0JtK+CtE?>ZSAaPmLlrK(@*j6-3TAm;J}QMNA_Z< zQ4F}c$qYeeXzj&Ns~Gqq0vJNf5Tdjvh`lhy$Jxdm!R}e$kWp)z`|mCQn<#B{q=Y(mPGUL+OexJh1d8Nfn9PA2ffZf za4g^;*)hH?obhiP^!CkmO(Af(xwKssb>j!|dyj}aK#|VCOwgIO01qi82LZP1hfOL%n zNP6TXJ?t;&vag1c`4jY*>BiwaTpfdBJM{n#j{WPI-l)gd)sEB8- zBb>c7aAf=BneZxGuE@+CGiA1WccJ#q_Q^3%x)NzC+-+ZzqSaJIkG1{F@$g@0sf;=nIQ~8Qm1qNQ6P|Y|YeR5Z7wN zlf^E+8TIZ+9&BeT&1O_gUMWcuvl^iZGeX|4w0ls5|#Fpp630=bIR{!<-% zd~dex3#HiLmN!UrLn(bY^tqL(YUQuirYD1+f6yR*t7b0Wo2hm0&9GJDV#_kIU|j5$ z5JM$0Y?*k5O%cyImrLfR@bbqxSvw0Z+{? zJ{H8xKInzdXnZ$>Mo8MTk=BW4lT~8ha=C}7P2$k%Oy?-boh{cs;7#WvPEj%(8OJ@r zCm=V4=};gv;&Po|qUwh*-pOk5`Md5#kS|o?Vn*9ph*YZjQ*c?Hkn>En)vd0ht5jKLjdvrwXSAg(D-7#y^*X9tsN-Vr__+@435k*XQdaM@yn3!OvS!JK zM?yw6%xl$bxX&^Wx4|J%3Mtc#`~(AUaH%qO?Ur1I?(Q?Q>Z>#pxO4&88R7FMcg~SH zqkT$4S?jooU)A|BZUj|4?|rC|s!x0hA zDpD`=?R)mWa2KWUeLm#|Jr99PHUJ+)V3+kC>az1VWHunfG5l&t(kTOoMR_UEWJm4T zEx;>dCGaNkq-cqA0urPq0W@MM;4TBAe9P5pt?iiuIbA;oNOge2*@hgPa!7RFPD}98 zp{(!nZA)+A4e)+jU9k}%q}(s)y|z|T3>*>x@Dy`nm2BPw;alE<>MN#=lL z*SZFQST1?%8t7BgyV%X6=VOY38XSs$*CFsM{sj@{AJe-G*^`#<5n!K2eoTK6^at>~ z9L=JT{xjgCLH|o1dL{b!(?Gun&n!RBGZuihFOLg`>7<|}&-muzSx@aZ9JtGr=?1#` zm(ByYdGQsZ6!c7*60*EbC+M?Pi>A*;VQ;iUwnQ7$ z4YUoTChgL*D?ByUu7p15pm=@Co#37pFvwvhqUynkMHhCLaFA(rGl`N@RGhWTROg@y zEnL3146=N26E*TF38ydM>%g57A@Kr6@nMYBc^xf3y+nq!{M26tjRKTMATXW5kGOQ` zIyBB~5n1j{f#!Ey(;$LJ-p+Z*EW;yO=eZnW-|jq*L+o26(hxJ;w0N#y&;`&0zJfWT zEPu}jW2zF3M-8bsgDWqLL9dc*)ZLAUo(sXO@3L;@F0zXg&Ih2LZbe?hWV~q|NL?i4 zj3Te?O>Rd1tOrgH)NgmG3nck&QZL@KR3SH`I!e0F=TagR?@qg+gbE?2O5Dlc8;t{QSzw^XR8v))1|gYAfZ5a zeu0W7M3W&bbscNtrpyXM3(3toV1-J1 zxP{a6s|c)7-lW(Qw}Z-_A!rfeHv+%`8t2mbF0#OzXF%O7R1OKrp(R{Ub2uE}?08oc zHSgT2)MdXNjl~r^%q93|qe@reUl?KjG5xo|>+t-tfX?Ut7>o#3X1`0pjnr8!=Fpgc zB7@6N()=s<_*lpk=<8h0)TwL@Z|7nTpCIAN-tX3XD^ph>lDsv+)RhuQq?X9D9vAf8 z7wWy;oa^ox70Qi6aKStK-txY3<9m`jH!pW7a+UY>gWigY>WV^H&N9SF2~$^bXt^j@ zKwZI9rG5eRM@-EX20N2iOF9$`;SRAzB84hI>D@00c)QPmLfp@VX-7czFtH~3F<5eE z3}uFz>Tt`lRGV6bcs>x~B?v0zCM0y3u0afs;qE$PVLk?q5OA5U1ryEy4#S>ZvO>;Q z+=xx!aaJSNIh8{Uf`)3DGV4~W3|UH?4M$anm@&Uf?JS1dao+IywrjY^w!iB-2x~Zw ze=kpLABGksl%zjLiv z?C1|5BbO30cNM0++NaD?dxC0YqY?LGT2Rm@a4W}KD{VJ1S?}V`*j=ab27VZxUX9U2 zI%z~>95S67P@v(M0(1e9R-Ta8_D%-xjfhf0@^gpOO(4pCX!j41LYMU2EJ^RWP#}|` z2EXcu&L+{#f+>lUP`!(zx|xfWW8~jt;qJfIAN%0Gqo+)gtfs_@acA>3)J)8lT_Ksd zuV5pq@1(GAwod^ZIkv+t##0vjYevh)Wcv3-xX+#b0#FVbRA=Daj5762K6lI{N&XqX zkK-msZLa}q8E!`Z10s@^;amny^e%3MN{XaeK6+BDm~f`zl`Sgn%5W6Xh93O;X^9y*YOEdGwsCQ#nH6(0`!3zf)HYvw&VN@~Zt}jZz9qd3 z@?6i_4<40ZSF~ILPwV1BaMr`(d#+3_f(!@v=31=AuHms@gOL})QVZl2c(EH~-<~8# zX(68+rG@iHX{xi{pX>Z;utkkMA9~f=wVdrRWSdXxSI2OEbqwe4>T?ViGov279n=ol z+_Pp6yg(Wa(w;)J5Nog-T8_Z2Fqv<3vl{rz#h={8uv%h#=Na^hl2&S+WmC!f8mVHP>bwuIcHO*keN+O zlU)7ix($|Usg(U!0W)WDJ>tZ0X4zDCLeOM4wVX(Hqny#X9@vHoldBkD{`ONqmzmcw zu3`|ajs*)&?qI3DRp;1F+bWjqN_Y?wcwel=ZfR*CZ+cC>3362Gc9bHufomS~&p?p( z{O6914b;1MI_mBGDPrt_sXOq9HQUXIZ67l!mUQdK9>|4gpW@CpvQ=a!JM4jWv&_0*C?wk(g^e*M zjdgwoZuRXd1X& z(*O9k(&R;XzJV-SVMcD{^hwv-|E@%`e8{Et7N&N;>B8$04{e40+S#t|okcaN&kd~V z1l^AdM&S`yn7{+E0cToW_o0weg0-39Z)>6247MD?wZ+(Sh?zAbW1gP-5i3*oa;n{W z<`N3kPMe6OX#opfjy-Sol$kLL7v0YC&6zyABk1)D#f#O=S$;*aO(FTbw(l(4bZH); z`MtLPEZd`KGh+c+@ZeSu7AXc#`|+hN8Yg=flzJM|`DdU9cUUn}Nbt;KerHIz#)H~4s2kt&qQUrg&Gvuf#dIo!5` zIN6T+(kHRxhoA$$ZOBt`oQOyLh2#pCTp0}%ke!F!(gb5+T$>4_jd%#B;Jj~&eNdSc zy1m7o$4?$K|I6zqQ6qQFBRA@O=d}={Z->UGZdah^=rbG{g~OonZ-zm4B$GE}-ID$% zOW!?@WjO097|WWWVb-o={y(*AZiLL+b@u@hlbN%*7V~2~+ZDT^xq)IYME@7IiFq&t zV8p;;j{AAMGATobrG^Z&Pek*Ofw$~=_G8O6p4^Dxs%-mkZPoV5fMI)jjTp+VI$>*A ze&}XpN2sJ*HgHO0+XSm+-V}AaU|rgGVWlC0m-M_LO{CV}Dk$r36_iDITvpw>gBwlm z_tl1=os5;S#M#Wmo=YJJ!#g%$ix5@#(Y{9!|3WdEtemhrzW{nJqrvoPXu00h77)^l zxDoSE>!3HqsNvit-L11pJ?`O0&o)G1ad6!~r@7wloXTiZCL>qT}qq3`b_ zB%_2p(1-lEBHzp8ClF3O2_PR>v8Yi0oBHUVxfu3HWY{W{bHA= zWaqnQw*oET#Mw>xyyl8~H_a7!H_e}7Av(Wlj=k0u4BCr--|x8{k{bHrVF;wd1)HUXLdUB-!DbzK5lZsUCl5bS&4 zFk>I8X~d1Hr%@XC!hKvZ&lH4ziEs&i-X2yy^(@eCmW0CcxlIe?yK^ihzZO0RsTHIo zO0KErkcwvtDk1k@nQS@-32@b-kOg;xo(t8+{vZ+6)c5|r}CX33k@Ha*{;Lk2M#Lo-|C22>DR!ED?7y{y;?b#YC(}J zH@qY3;3PO%Ggw_==CcQEdU?~hfaFdmFa?vWao6_O$T#%@0B-M3jQX7yImqASm9GW6 zkD9@$mq1TUO8thTgFRQHbePfvx^@6hy$nEU93Icbq-Abz)d(_wi}==Ca9Hc#6(30X zGwL_Wak(!&j}zb&h`-aTuI%!~TzvBbUkbv9LGWpd&u|K&OqD2gO@;5s$hUw;{2Fz_ zUjx1_R@C`N*qHbCa4RBjkM4(|Yx+PWw2QkqthSt`cT`sBYKQA0@)1N1Xsu{O`r=*I z`w-SS8ZiT6(Qb~iqxsJ&$L!?3w$1P0;ajwnKAok<#9{@~?U;OQxiF#|)sAx7>fGf~ zoV(zDH&FN7#g3rRd?y5-F&@>3bA2hb=-2b8XooL8Z$@C3^-)f}49AFSP7SIZ%ZL8X z?_kbYX|&XN1%Zm;u{_~b5by;%-ulE&aNBB_&vgcs%`bXq^QU|^98+L{c=BKyy&GFV zu3vD;`hHxx3`d?YT$y}anzzOHYp=KppYVy}bhG!M`r^tQ_rM(Z*ey`k9BA^SVcwwA zRW$vx>@j--y{T*E2O&zQ&Dz0+9_AwG^b37r)Q~Xu#f_k;{z>OcK|G|YrkTaiUh&Nf z>AtyzqRCzkHV!3Ji2HPl>d{t_7uu@KZ|0zsX!0tt^aAKE7AyHduiEm{onyh3Kep<= z67hN$L%x7paQx$P@=8$7hq|v2PKNFwlitNhBK3Q=)NX>82s=afXv^iu9XNF_6%K}p z)E`6!fBp=J91iJ%^gUFA0k}jIcd%Q6T*YE7K3%>h4adQ`u8!^u>b%3{{I+RH*p0`C zroJWJ1GDgg8;#KYX!2!fH-4G7NV9Q2Vs{1-$h}H+tMyEv_3Hb{EZ{%ywE+b5|V&LN?EY%;FG zEC?zuk#9_A+B*~#JL*hb#+zvFH>SHeU!1@wOXe(#NADiG;_I!(6tDo@i`=Sm!V_&?|HMl z4tK>a3xA&w(;pL^p^p+ODD3F>(%yK^ip-AQGRNb*!S3F35|X9|fK9%7pKr^qo^OH) zn=9x$P%`)uSEl>v-=685bi#Kgl|2`HP(ZmL_e$)FIZQTMa{ZF~g+(wg15@_%>h5OI zfCRGE$EokEJ8wa)Zsy7whJCl~ebf@_2^)4+ssg5qc}M&l1dRI$#(ek&zuffVD+oBK zJsqkiv6+#28`AiA)$M-rpTE3)tn&`y);YXW#oH1sG5(IfuM4wb>0|lHQe9kLdcY_O z3NkiR?{czmvLqUI-s2#SAMJ>P;T4ty?T?`0Izmx2yepn8m?^NvacI2n7{2TlhQIni zafgW!pQ}<{!=-$mr525Nj7wRR`hc-wiWWN`a>(vk2_-Pzegv!~lEwZ6j0eOhVw#Z4 zzbQ3urIeA4n}zrgtSiHgrap!gWGZAnWJRfw?2mGKBeLV9Vbt|ET>2dS z<+0f@)1uRK`8ztj+A5849jp_E!5#@;jkmI%=(bSt#Ih{or>qxTliXH;&5<*>>e;dm zCNKB`5Bd+hRFQ4peG|A1O*#j6#{iQ6yfdP%C-BY+nr%P6Ei$^- zC&I;|J?Qk!^ws~q&xl|r?Nspcm$mVK&ooYV_jjk`Eu>sJucRyduJ5<$=VpXS0B*vU zxP{=}(yLY!JSsL}0?y+GTu?Td`As@EUBRX``kwPp9d17c6X27#7cxQCUpSJZ@n|w6 z>j@+E8L~j(vDIg!AbA$VW zPd;#tPrE4h;ZuohS65s>lRnSQjQIY)NmJ0nAI5UC9*Y7j={0txe~k*zyLblXno0T8 zPLR|71jZ0nG$60kpu5O^W_!SktHV=Esp` zA-{}K>g@?iPblA`nkzs~uLf|{JdVrJTa1iXJfd{{ZvuHkC^y>5-MahHoJ%r(|cu>kG%LBFLK}B)2@$r}Rm40$B1( zW29jMiY@PzklejUXZoa>luN^x^^k@MoQ9tyFEORp_es-{OT*XmkcJ7IhM$!FCD`{+ zOx~bL|EZ5CCMuPA$Q?}beiV!12tj0^#l0T>BrCdyB>L)V=WYq3)~fcH)rm+fFaB}w zILdWk_;NtrpGl8Fs&pDa>e`LQZixEUJSHjCB0e3i2H*YwyYcZm(BP{QzFtFB!X2ux z#xiIH+OLn>S(3lB^do510E+xlf8{<}?*fY{+pejGS{)Tg$lBtSerT&RXw=i$<0<$Kz5$98i}6!S)2&H05SJL28{0v$8<JC*}9; zB{!AS8H{SqE^zEVEXk-^`4q5rpyG1*7O-<>P;SlWNe)))Y-Con})!V=OuHdhlX8$JN5nKm7KL(3>dyD&iv|T!q=>@k`F^24~_}uUQ7To8q zMcXL&ECX)g?*$!G=QF57TqFMRTF4OH)Ltk1K~VBALCxI;FV5z1KaAC=l6h3Jdb~>Z zF$$*Ak4Ad_xf`L`#i-gObhjHo0N8Vq0$cO+`&1oXsUWgR zg*Y$ps!8_x9Pbq<{YGHAy#K-9cP{+8GMI8kCyq?qzr!e)Je@S!_8z%FaAg?QRKfn^P zsN*Mp|JZ~Nu*QkN8|P$`u(NQyo9-m|XY0eN1ZdtYYgZ}y)yn*vLGlI_j*l@>sDV0j zz@mhgWrdeywT%bf@afL)xlpgcN7)u!#6SK~{mIX>Ts}9Sg7>HJHE{kGui0GDSp?H# zD#vYb2+|&tvs=xs;njK5aQ@Swfie7Ic7im$?ZRT`YpMDMFP|AjYLXW|+2o9Cgrh6Nw zP<4JVbXPvZOz@enI}T)rh~OVreMP}P#+jZh6L0zynZM?G7F9>?N#m7*7{ zq!H+y$WdKkEx0h#?1mVjr0{L>fYRs*mR0k$$d(4c1f|iF2(d=|R2r4J8oeTz@MP!EdrYKOt9-%ICKsI`3uT^OBADJP!u}i+%oY2sMwHaOfC~pcH-~ z{5x;hpdo|y9OKncM$$DFVqtr=3|@NE?$Qx(Gw+1ho8{k;=`*g z+9q&}&EczxN~&WtTPBh(6@47HXnirmO9c+JIsCSS|0(bz;rT^TE7C3yI7a9v14d|v z!?k~6T|;SvjzZ=UIs%j^^{cxk9HI8cV;iHiz5g}gD19Ad%Fj?h3}vMar562F`_yC6 zdw>yIi$a}GYsxt74hgS#ae4TUTud+s@P9pR;(njdd zXf75FKp8E%1u#XwgI7lAIX_GG>eqxVdRF7ye_X~TU8=JV2S*x8qqIh1ts0Kq35rE` z)ieAm#_)dOFSQ#=1N3s7DR&A*FJeu;D93v?^k=cgZ&4zY8q%O!BkS`)`6Fh8UN5h& zjnW@$nU1f6*IMK)<9sjmFeOws8I&IA^A!CHo|4p9bBjI?hz^w()L8UJ>giiDXZC`k+MJI(>!lQsu)YLWMqX^94c?UL$P@SYY ztcm5!C}sOB4ssb^3!GhU(MwS--IM)}ZH&;{W$#r-X;Fmp^7Z5Jg(lb0JxCR$pFwhz zMi#MzTOi?jZCHY3zL8-1JB6dO2^RehJQfXfSpIt+*5p+l3N#zd5Y`@hEkK z%Hauj>s!z@+^zo%HGa1~r;_3CP>Q?tCt-)X^$!4}bU$>n=pgITs70S98cMnEXy>j6 zeyY&|OjeFIkE|Fk7=NST`r0EaZpF8cZBUA7R0YS?;=i>>kpxCK3mhPrU8b-`!Gb}B zwM)9tfFmnj1m_siV1u6^-OKoX;#gV$nP2tiI0tV+{Hr-2;+t zAa2CXU~E>Dv14hu$h_Ob*r|j$0%Q)W1g53vM!}ARX5+OC-60+K zcvyElt)i#NfYz(f5|`37jMWv>Z{aoL=>~}_=G`!0H`8Pxk=h{NBQtG@kCI zR||4@klv9vxEJCcrauF*c<53l({id%J;VJ13_EN70{X~WhSf2qEb=q_qV!9^ODh>37y1IR`w9#;03WF-1Dw;ZzNis?)Cf4R>`VA{7s47X9>|=# z1~vl@4sv*mq*^5KD1j#nTp{qq{)54@cNufCOpVr6>;-tQz$F3`9=@p-_UsQhv}82k z^vVMP|7eddYM}SRM*!{}VR*ZQyULD5_!SAyX>LY%n81j@dV#EA19eIGT?s!X@I;}! zB;mk-!QdGv@FVEiK&-P(ubY2>-KNwWk8qo3o-a-xq6XvipnpbDoCf&L2He$@X6V}t zA2>eDFdb$1e30Q4{TNd2T_fr)uW;G>S(V?vU_Vb(`s!%%jE@!0QBd3A|0<$ME@M zGG3EQce4L>NSIM}Cty4L#HJs6S?BXimcJ9$ut@390h?%ax@dK%z*d3h3tX7k2nqMY z|24W=pi_Q7C})Nrh0K{XPXKPO{&i7_`15e>8~xQH%lgXvl8(>Z68<1^eC9%n;raCp zk1P5h@&g)ayn&i}nBD;#M4uE{^pM>qaDTwPVGZW_AH<$dU%8a5%y-QhMaO480nAeF zHqQoSyTR~Pz%2a%GY!gn2E!eI7M0e`!YDeul*_Kyg^M*hwCGEe{jUC8M~xPTH)^Gz zpRV6j#5&X$!_%m#7VJs1B0W}U}>^+qZmUti-p%EiEhgI|zup)X;uthYqbS7Sp z{7kUz)M+g?ZK~amIk(exd$Ea`;z-68&^>mlI7TZ4TSZqjF4Rk?|Nb1eimnxGqKmB( z%yR(8sS=e?v0$rwHwO;(me52O8|6P2*Y#czYyrw}AuztzMfASbV_j5SO6Litq$D^3 zzwI85%VA-&shI!vhWxgNziQkdI}?m<58qW;tYc~+zwP0hD;E`G*mSX@aQvOeM#VPp zUx{$zJmtj&*v_&I@>f0G zL%pR1*Z}Wvs?Bl!y1ujk+g3lE#BmY7JzO7LRh-94gE#OC!}8l6?pLz50BZ);kc%5% zd0Vl#F(})^lYwC>E5GgGU)gsRW3uUDZvewoLt!5NTq%}IF2?;=9&?)REp}+3)X^oV zzs2CR*@Yu5%IFK%_?wrf`ho+%!b!xjf#E*?rx=A^t~VCqi+u^~Kw79UaBA2cNQ_1@csbkiAR6Lg zY|n#ehG2_mN21g|nAW=3rx`?wglj-zOji*f5Xnyz#)ZXHL{oi4_$;~09= z#kh4GL%wlJCR^!P8tG!(I*z3WU5s1D1bV^6xOGe*<3N>;TgL>#A5_NgeLuI3iFC4y zaqE~!m%A9Zj^k*vi*f5Xjt)6UrQ_CdJWX>kZXL(dwJyf3;{1h=Y|BZYz^%u3!u3P;gG6HEtZYoD=C47vq*Qm5LAHbPMRAx(ka>qISXF*SN+` zrX6nFCl#&s$@G?BTz?v!LLx~r)nGQvKEu641an@8EF(k2(XXW%INbb7(X#tj&6 zpGlv%*nHmvdnWk~WhpFksy&NJcv*rTI^aLeo-IdpjQwT6Y4+K)@(2}Ijkr1VwTlhK z`&ucQaioe{MQKwrp8uP5nZ99<6aPyMBp%9*y9w zF~p5*TxnlG>v;(W>`8NtouR46Gp2g>i|Gc1`JTpzcQJh^7*<{OLj7XukBZ@R=UR;M z5iyUc3+sgSGq#8tJR1J~#b$*`9bHb5Q#ej(eK{Q>*aBM6$eb?-wopdj%cS8@6RvNUAi`^*LI2Rit*yhtY-9jqs$DBjXRM^PIPwmB2oMvnRbv9{q1x}U~ zc7Brw*wdYg)6@8ceI>0tm$5|@E&0@5LZ6(cu!^Qn?WHvChk{9omeFRxR2i1hKQ7=n zHG^12mtUx`dC+AUm2@fW-g3s~x!5h`AK1(2P8YiXaaYlZ`HJ&W!DhJF5!MIxRkYs4 z92i6?Bk`vCUS{(Jsa|TR|tf7~5#UCMEb@Rd6cd~ez^ z#uiW&N^}jqcNJr+e0S7_qu0>a3X^%(H8hPE`$)m_tZQkNi!B4^YFZw=FLucjKoRNQrx6ilVNj`kAl5_*CX(d+0S7aKv<(d&D0-au2`xFfKs zeM6tPv)s56Y{%R{KXkF>+P%fSId7yzZrm+eL-a;kEtt~mrd~2{qFY_g3^hl4$K6an zcjHEBBceCclY()H4v4Oy4|A9Y`$4`Ru_fNt?#I};jt+6LzcvmBHW8J^R@w=z*Wuef z!s&md;lSuRI$vSh;=01Pi~ElQCm#v3OmEd;;N0wD_o2gBM^6Z*O0=$5iPq5z5_bvx zm5xKY!g{!cK62xlv_$k4`no{M`d*yt$+wbozl6>O=lWdz9jYgyw^D_~u_Zh>VHhcx zD*J78r;BmfZ==0du@qJI+vq44yB*s`x6xUGz3tzx|0&VidRZbL$CkK_<|$5XcGa2S zyvN1Z61PFRhBYL^VF5lDi2xRc4LVdP;~SZ-;H3tKcZW(&A*Tv|?Pq=kpQ+^VeiaP2 z)^Ltbdi#czoc|Z_Su7zmN(|t9yUI9xY8{7f_Hp=_Xcp?^FX0m&j@9VldWMR>5Ptu( z-=A|ZmJ-<-Q5H}-H#9MQZ|JOJ?}@`lNGTr0Zn+`!-l_gO>#=2e%lS5n%6${;md$D4>*2IFR%bai`W?J9t2s;7<*UX99S+Y|5;VG~KwIViyX(AIcoggq|0(3G z*-e6qEo?Q)4;!V%y;9zOwXCh8JSCd%5U64gE#lZ&SP*A<6sLlR7U8duVE1?=cG;?M zqELqY&j9|lX^q&IX{4ce?yWT=rWv$m>>@OS)(o1Tc$R7dda&B&a2;R(C!C*D72|u) z3`YwbFYp+F9RjBdoC#Qpzbww27YJM+a4}#7)oCnqG+-Td0JhLGLYb~VgK=P{&V68r z*7hHT@a^c~XX~n_8I+{feUFhym1-eU;J0o ztNQua%U&(JkgMswk~Q=UUEr+8*fTV|fi}{Yp}Xj9eSOIT^pU4eSX$tqwK-$)L)2h0Oiu)8>AWMH@-s;(U5Y^ zM)~*GKB6-tV{mbuSJawuT%xDb??kkAEqUdckhV*rO5GnzR3v3DKd1Nhd;OyeKEJM;y{i;;T}o?G^S zez-8d;g}dFgnA#gx@fB>N_G~VT;u6Kg*-+Vt0(dJMmI zRq17ht<9=mYHTz<_N|ie&7fabdOOlSmAD&}*W&jXtBqw%Tgb146PF=enb-#U=)?=) z(E_g_d~w;k#zRJN%}1a#3gxFhruYZ`4YAYfRrnjE{kZNmW1Hcv4w|ctROuFc?>FHT zn{ON2%WKS!46d)wjgw>brr-EeX`?w&$~a!y=0x~myLqDc;fcg8bR*r5lY@=)FP!Ua zq(^ZoGf{kRA{9lanLCX;qGy;JX@-9m$a1c(4&duhosBi(KgFJM^UOfpGt_*hwhlZ`OI|;; zIj^VfM$bfI&eLdxJse*+{-o+&?KC>w9)<9d`cbqE`XA_-E%sRUD9-O?& zh?Y+E+(7=)8@=m=US~WStI_L>ed^i%kNIx*4mEG-_cO1iO$coAt}s8Tdf02DKi>*i zq(9|dYyRHztam6V*zpB?-dnDp9(~!n!JJ_{i`XK1);mz>Yt2(MrrfVH{2gGqerJr~ zp!#Pa?aw8@_pUZyjJ*YTMB*(_R`+`k`Cj9B7;tdSpS(NCKkzezO9$?RwygOxx-;+_ z?@r^_m1n?{%`(fkUdm#lMk;-)jo*n4?{2F1Z8Uy~miv)@j%S?j0&|JyP@k>+3a!=F zmNYY52B<;Lqw%`vgaIe`Hky0IrU9PeIo-Fw{Hc$7fVEz3>D`TI`854kO*p&Kmo(4w zJ!I}1KHs;^99iDw`-skuT;qx0m9e5Xd*RN`C_1z#g*US@2>pU~H zAKTCS%C#MlKlnD$!~MSWX~ypZhW{b+5J=l3b+JjxxJgR1z&y76GV}@8>Gf#M%72=i z7yVDrR&392lD7Puw1pia?KSiRzQB9v2WkV)(N?UH-;>_p6QO@0^c|AR4#{PQB8da4F#5>n{zwYR>mA50pros?sjB?gZsB^MSxl z_}&EVbMr!LGbld+r3&rq&cNqpQFL=)g>eK@tLUDx8 zj}gLO*Og)(G81Q|e&ZPQfg9zlaU-38IZ2&oOv&-VXXp-`r#_=U6`m3t==pWo`M!ak z*GkeDP0oN{4aK-~CU}l2nhQ8$;JJVmHRlGm8RfM>^oL*7UKl(FqtAli2K2_41^xOu zob`^@U$0*lZ1ZdiZID?(F*JX?Ze?(&@p#jjfNz&xADn62g3+@LxF`i&RS>Yu^La;@~quLW12Eodmk z4C@>33hgZ?7&^vd7Tci(bV+;z?KFI`Gt5`b!OjDyi+7{T0UxreLwiZBjg(q@Mh_N! z;~gn=v_Z$LDKy=~H8@k?2K|Qk*w6<3XYuKwkEjb{jb^OJ`2G=+$GUApq@o%q^RS@P19R!=v?gVda^|CfMycHEv3dCyM0L#EWJNWuC^ZY`*yX zeDU-7qQgAxbh|b%(`c1B%*v9dA$euVOW}#aIZ-$l!W#E#O9U>|c(fX73@NWSmmoF} zSqM4BktG8C#=3!Zo`pi+Mvc({k=5v<=J{5mkDBM(Ku^H~H_(`RhL>aBwSj&O$R5=i zS#0u|;$rh;{}BlDPS+CDg+>bnP82v_px<~c_PpWOZzP#=??;$ z`lAsa{$AT-fT!tC0nX5$1DvhD2zaf~ZxH%=3EwW^JB4SHP#zS@W>5y6?mHO7RYIju(W9c*9hDykcyaID{xvg zi11rwLaBV0QwdxnaI3(#1X2~p)(V_f&Ebqd>L+x8?E%m~DO5q7JRxY;ltcc31_ z9jt%SH*}JIxgIr+G>$bY%)QKG%}M55^9l1E^J{as=Qz*lo)w;JJU4ot_B`i#(etL~ zJ@D-|>pjdH#>?A|?|R>zzMuPk<9pXv;dlH; z_)iL45O_TB_dr!}UGUN1JHfvOJ)s|jW`sTsdBcx}Uk|?<{y3b7>=PLc3!N4@D>65d z!QB^LIh$BjW1)_lK>Y!ySMCM)M|%XIe6wUXx{g!^b{cwOF;+VU^whIY-sa#je$xp>qLG_Fn*aOQ1)>7X$Wdx(sk*<&}UFVjQk5XE-~! z9PpZfD*>lVc#M}Rzo}<9GPoL0@hJFzBj-8M?Y`QZAoJ+j+W~KM?ghNN_G!SPhL-?0 zHoO8jr{Og>?8EK?-+0>>P}xMo(?B1wA1uRr8MrYIyLdp^A9gCnU!la!6j;d%3&sF_ zI3e_bemtNKi}~?PW8b3~EfjBF!G1wd&c*&hF`WmfQxm?>kNp!sAN>%!AiQIN7xHl{ zu?TbrP>1*Ny~<`l9e$o&ha?pdG6HP|IkK z>8<9WW{c-=&nupfJ%?M1tQPMA?~lB%`2Os>&;O?XBmZaqul*%~0fE7RQGtU4M+dG8 z+!A;{@M)kiG$^z+^i=4L&;{WI;g#W^gg1qEh0Vy00UAnZW_Yix@W;1N^#a18$TQq< z;dAnkm_?B(*xl@#at!v}`u?uLE+4-fqNB~~sB7H1$G;l*N-h4?!B6VZo*VG5KmHAX zuQcLc6aG2yn1T4$jDJb@R!fuiCo@H#!Y+TKOQ}=4imul#1^zGm`$qo#jV{veHQ(0y zVNKYAfBRxp*lqn;yVbgs_Vxz#y}dYv^EPUmeT~{<`1iE$M!nR3qrSiYQhL&Vm;R(b zMfaK)X>Ebe^(zB^!@qru>A@R~^+?;PeXbu8`5XSBNaMy$IOO!{r=6Z2H)-e)I(O!r zc@h|Tz^NynJZ@&^+}YDF7(07#`BxgOG8PAKTN$O%KY|t3Zwj01kH^3V078|rFnxX*` z6hRxbULYR~)Ij^8Ko@O*2I#gZTBFE^0!@K-`}?2!ybOnmvx>c=L8asR<i^6Qe~ylsI8Y;YIx?=Xoy!{6SQ0p>F~v2I;@Q-KYd=UXG-O*;8v;EY-q8iPDw-h zDm9`-JLs&KoO~ZmZO(HtOmG{z}lf)Ts2rk~-e3wyW#aTD21* zJxf7X|dI7DZLLeDNG&a~F1+9x#&6xGx30o26PexK^sRzIw*ZR?A{- zsnw%U=#=Vb+|8g2*_tOI_e`j!E3bHNyb`ve=*K@BH8^L?TZ@E!sfP$Vtx~zuzF2{a zue!6Zzkc@eYp)f~pDDa{{$_R^Wc`I$4Xo_oz*DAa1v zFxL6(>sq_Ju-K?{(GqvzTB~}m)CuOdo3&sY;h2`p2Axv1*1qT}JC_^X*367s3GR2g zt>Db8Tby57D_)zMn{_*8OMJHJdY2Jeb$H;bXXlG6g{7G}ceyY-x3)6BIOo1JcjFQr z4b}(-6jyE(=C52`SzDN2nsZCbyv-~xUc0fpIA5G|g_)K4n{y5uS-UkqyK>dd&nz#k zU7Md_tPoS2TUnW3x>5{_(MlJp^?+_}HmC(1gUr<2e7RA-RxQ5~v{YLRIvss(OYls$ z-Dzw`{e_vk-)yuxf?}89+FRy2+;aVDwGvR(h=5|b6$JHSX+zlMdXa`!9Rdr5mAM;* z1y@*DaK&D`6KtQFUxvJLb-PrX4L0fAUTMrO7TvY$MYp;%zfyz~OR#5#v&y-Jx#G%v zVPWmk^6YJQPnx&3=Bo8Js#XrxTqU&mHFqyI;x#R=P-||LT)Eq7Ay{9)Cx%eTDlyWv znUajoU8|Mq$h}qVZikrdQnem}YPVQwhNa83>gHBwpsMVLkS*OP?S#rAC*}6JEw?k^ z!`ybe+-TLR>#o%1DWr>KGl9j@X0`0DGRHzgP{=Z@y@p+(X6;i~f_l)R${X4%73T@8 zmo|gS%2o@1FkfN3+S*zb+pm^tG16A-S>kcX>k8x-%M7Unz%VI;fwLIiY|!qs8olMZ zVG5OsEAg6dFPF=;ZY8L=Y65OGTGg-7r&KGpHj2%F+I9@6gaRgPUMaPpW@f8YhiWI$ z%xndyr_HdoTrbpn%T29!rY5be@Rf$!jeEhvYRX{?oL|tYHLSH(I#-uBg^B?fInKAE zitR?X9n7g0t*Y&r8jCl0h$}aOH@h%`MvcAfN-9E5<72%z>WvoaZAJ6@HyhOoY8lG# zN{iBk`A~~5zOlA;sZ@RggS=b~Y87`S=y*WNJ8r^yYC+y8(rc^60 z2A!=&#Z~sBkX%^VZB}qvVF9#fbW$z`%@QHA(F#$}6GD&?LNdGv^hy$zwmktmP0xsp zMr)_kGDWEuyRD5Q!3UoEw_ zW*U{iZI_y&N*YQ$2|`b4ppOj(GON-tgDV3tmb9c^K0h&#=}1)!T*Mi5?*%O@Y!4Rd z>ZD~i>YLRL7U*g*q=jvlHZghO)d=c*n+RwHu)18Rh4@kg$hzn08<;D>F#}y|v^(Cl zF9z+lFf)y9^dgwMUpA|CbM?x6{a)jZz+DP9tMw2T8DZjAxJM+F4Z4oc1QWz^-5cRx z-s1{bW|RcsnP8rEo);Gz92j;F9b#4*o0~L6p9^&XMa-Ye-eIme(?zsJB?+704rr zXMPZCa>Z+zmS$yZZcs#wDYR{M;xRj-3(>dGHlLr(G=K>arS(!f2ooBov5_ZM_rF93r;I}Rk9uRqmrFISQRj*=GpW>2aZH*_iquF!4=+v~O7icJ0? z=oZ?yU246NR3QwXwkN1)PSW#AP*3o4_d7wojUFbbXg=B2&7k%0%K18>d)oqBsrD;d zc>@kN*!yQdK;>4|^0=hZv(`qw5!6ce4QeO#5qZ~IIK6Ubf5l9r*=s4pCsim=Y1{U@ z2`(B(^Ofqoq#|_BhVO(s|D^U(FH{7xRY$1yzzs`4Jq-U9s(b-!TO%)sYdla4Iv%(h z-1ope?Org5m6lzKrF((WX;RunC87&~5OJD2fUAVL%SxkQamm$7_vYIn?8%~En57UQ zNIThk)XD~kI&;gy#y(nwr6@;r3mDaWy%OAC-f)Yh&en#jRNBprFg=lP)PWOm#D(!B zGH1VwP=!WrvKqnbwQ612ED7G`trDAKK=}(&`DQa}OxErTB!KoMBCgn7XSbnx((Kjw zLbFL?Z_2Sy!k6NqR_of`R5yCz;7)r?zgwLpGBhA#7-44NHiAxh%a&#)9y+gUEY%1f zEQ?hfR8WzC;vzQK>V*YSH;>0R5gLFuvXtEi8+QW< zHMZHVdS6MPDb?HZYBnq_!;(yy)b0`yKPc7RmU&Q8FQr%x=UeTzk*IaL9|1i9F1{qa7F& zrqu@1%Z_xcaE15~^1{j(X2fzUlLZm(_MnKMojr(|DB)5fWxbFd-Vh3m^ACv+a+YMO z1LnhCPuun(r1B}~OO9rpl!bvC%{9D$$xy{GXnO)Ye~VNvw1+kU_^y|Ghgv%+zO5(NxX#|{RD&eUK_ zKn)_8H*qo?N7EWbiZ(VjSmbbS$lF_}Ak8GTHB;DWv?{k+6hwjYVzP3ph~OfbX^6JW zG=#NIHB~pk2*`0?WV?w;`F!i-OmVB!@-4cIsm)WH(SXen>*_ON6zffx9^cX++pi6j zh~C8x2Tvk{8y>8HBM%r9m2J$ZFoTn6xmiB7|X$BG2%nsWkEJ{DPp9x*X9#n-x+49yNRH?HM)qJyU-cDzvEhR1H zKy>907fiH9e{_2zAk%4qE+x5_(FNV%5o1Ct>DpENSBnERiS>S`KqAs;p?}^X5JMVRrQvarx-7K7Hk zYB|_f?RO?YE3WsODzZ(T;4zEg4 z@8mrkdI(?8=|J1NJ`@+X+-8#<0_+QB8hB?sRR_F$l{QZv!XNnpF3t8oe)%C7Y2IwG z!Q>o;u(Zpy65k}K+e`RM%VqpJqJnb!xW;@!p+yHfJ}ca>wq3Vr7jNcQ_@+Tl9wf7C zobmwCH-tjh&~_w)>z3`wy|v>bUtd}e^PaX-Jdc%Sz~bWLIXkH!F#DhlO+*`xy3i!^ z&z`$&=i_G^7uCkugc~+80z3K8wYBYY6~Vd#m6Udx2^`!XDD2psNOdTbd28~Dg9wuF zhm%)>8qrq^#9e9r?Ix1+5{XQ9;8ttwL))hco7_M13S}(c+ea%+Zn6&DO7u4ZnLS^W16;Nv2rbAY7Z!KIyJ!i;vE)#~a>vxhAB&l3$^-_A z@tK)r5lLw~I!kj6JC{pDgywMC7vH7u_pTzRGoTN#=f!KCx)HSLCVG}PHUw__UH(SL zHs@CZonROVwyg}yeqR|D(A_W>9cUfyG(!xj$4bC`{%So$M4S$bm7NgSBob{UbB3TL z6#is$zU>XiSZ2p7O2>6`yjrey+#DHmcu2k6!fZ>N#>u$MpcBmHYOCEzit`0+Sz(1* zqjI{45{4zEE!8~(l9K97<%}`hLEEhB&b-2)l$LW<3e1`m!VGOt3gHdXL1}#KI7s;A zrVlHQjzoWTm_^tZ5{Dh}|Kh1f3=zdX>B%FS{hwHfIQlWCc-UedGtA(E2Af{EcxG*_ z9P&~X5zIbEURW&VXmaRguX!5qNowgaR$e3@iyBy#=?h9lJ;c2wJ!%v6pVJLqkGVo} zV^vV%fxwEk>(h-$?f9?hAtQIQ8th0#_hL&recr6drU)~`t>%uKZD0~Qq>`g1FYY*RyHxmu!WG{HsRKo|2va3Jz{CNIUA)Kf zxlYe8<}#v0d%25!M|)s5UvE;D#C5~XC{s3Vlo`O$sE*z%R=A4b4Ctth)~aJGq)Lt~ zdHQ3kfik||@KVgB#ttOvo}3S~>c1I+I%b5Wy6;Gk50Qv|?S6DTRJw1(-CqQ-3IQ6n zC!h{JcD~A!E1a^rOATMS&Q+~@)^xVi3lm0Dnw7>45RtTWvxGazarlhNnq!m4ykM0* z>%qaU9fPwLfBjWwsmj)RZqFf)+jH+MdU$4$*rFpKqftqT6;MmNq6?4^*GJ@D>KQ6h zv9P!v4Z~ApmSZqlTd+=+!cDnN6Bh=xrRGT}MGc8gc1x&9g|@A5&rFN0P7igO^^YYG z#7e}ko*qV7pP54AYi`hHCd0zL8nil0ePia@J4__d5dX_r%_w9J%ig`8CHtlwX=FRJ=@6i`| z20N9xO`kSjg1$ssS9sW*j~gqZ!bY81V6KkQ+l4dm>DU4zbFeyy`}`_%YCWQsvljY@ zzj>a{4uH5BLyguaPUNYr?HzNi!20~^{A^`wQ$tpH8(Fc+TEU6^3U#7g1a=2~|xtd7l3l_q3K8V`!zKMC;d@UH`90tb@eVhMMFNXUP~K zrSj}(8}TAhOVDt6dToSdu}vZo+X5!(FtwKL6)z%8ONj_$K`%yh43nsl#rQ?~H)%J+ zm2-G{YeB!Nm*aWH$E|DKs46o~mGRy%0dF#7q-m>i6NB`) z(Pe!p@~9}(JM`HvJgzB{8Vswry0V3>BMvFS{IpXb~MRGIrjiv<+5k^OAdvVSeUC+MVPjlaxAl z8GQ?A*o>cZsi|9hgyJ?|owtm;S>mJdZd_EGgZiYGqVzxtDI^+%RjSK_+rI`kg7cY> zsmmDUtlV=me7L5_T9(nuHJs+&6&8^w+kM8Y^K$(^9u|jJ=0tdTymKPF{a#q+b{PiA zdrt7_otO9*UGtE!MV;Cte$KN_iX$edPRioFa}on0qkTMmkM^)qFL!Ki!1IUf9FIJi zJGlb$8mxdPZeipps2(g2Df864Q7!ttsk@A#7~*?!k$&P85595j@XwUw{NC|S@hKgz zWW`tzZG55lR5eqR(RXkF?nr_af88s?BVA*LCUZ3rwnW->v#g+G%D!$gaTBZ5^7O1J zk{gfe-LZmEy-aQFai8`$xu?jnY7Mx7BeURS3Q{yEdtsT`3cSdu21b}+PVcZxp^qdE z%*s#8ls$*$=<6M})f`J|7MN*+I``7>YUdvSq5@db)+jt8?nd;YnS0!c^cLFqM${oqIU<=R5_C_ioal5SvEdp?EDS$nLZ@+L zIHK$Oow{6~Rc8C2=p)h+0C-XI2?kX^7S>*~pbg zy?ln|XL!!J&pnD`iT{l9x+n1gxB3+Z@JfZeU_H{=O~*VdjGJ0CDCk*e*WX| zS*wbZh>=_9#2hce&&U!V6#hD&czg?LzRqVc-{KR)f=&Y|cz>M$_*r6#%D=|Cwh4-F z5Zv78qP<_S_Z7Xr_~`Yw-E)dWQ_~JP}-^oWqxUTR|iNF;~?pdus`g$wW z+6Ho$zg0vbY(KId5BVg|w$oOrnD#BQBTEoo@c1NtC`qef&+_0(?9|RZyJNkQz0>TT z(cF_;=o(M>8#Wos9r1Yi5KCi@`|?%BR+Q3u(DEsdmH*x}t#r?NuUv!zW#MlOd0KZw zaYjjU=rvFH5u{D>YJOy|L)gyl&xQr_u(U$KQEI(uW~0tN@qbi8Awk|iL|#l%XtB(v z82(+Rrj){OaZWP20;6(SPo4}gGNx0&c``r+C1sbsuLQ5`R~T=)8bgUnm7aNo>Dx=e zlcdM@7iNReYJB)uE=^Pn_mdluVt6N>jJM8sjc{hZs=x&3S0^!Pl*kb&uPrf;Xq3QI zmcV`9pGO=D3w}qGBx4DuqV@aKOC0g7qdC@XWUXaC_i({a535~7g&X-Szq_?W3RobZ z$)E}`h3?6{N*MPc5sB0=#t}xyfJsDd|_j?;R{I@U@TA zTWPWa3n~7D_Zi}E-MNp&lf5!2;tv#9_?q#3S~Bd7-Aj)Tke5jGXYspp3XTNSy@zpo zc_Y8PzzPzrLlck4yF?mwe!SQ?w6^)b}bGZ@5?En7b3DaIs`De24 zQAVe;Gaoo4Mm~@d)_d3Moor-CGF5N5$>BhrJwTMd`b9o0wBe{uKAzQl_7-L@Sr(Gy zj$XCo+^aAy&o@E9)aUlsN4aR&;@5{cGAw*X*(c8ZZL}xcqu|G{Sh&*OQ-Q8W&c;am z>VgzE*~hyRkK&a(Im0M&t)lv|E^ule6;JwO9?$WCS&t8gD5LI^iEAF_ihFjiGu~JK z>ERK5z*pv`a6b?%j`&ktpK_q!{eKgscu_;2O-PVZAjL5$q@uC148>`clnUzb`fDLe zWzAmvdwYrD+64M+X4U+N&q4NP9xocJz28@!5h;{cZe>x5StVXtjy5b&3uP+}tJOBg z$3Z`1>XS+t{SkC@@T{%1oi2UpPrGil_cLowDTKDSqQj4bWgnMf$te9vmpG^MtB?G0 z#WpX4i0cnyluzFx_G<4O9r7lL&<@Mnd(UF{@J?+-qCcFBBp>a>r4^AgSFje_Bu?Fl z@I>b={qrgyK|Y*$`{{=X`$Cde7bSpRnWEi>1YhSEbi-Ka1(V7YC`li`F>9nGjeVTh zf4->UrCo_m>PCc?Dx7py*^3t8Pe}J};-yil_r%CkOP-z>7Bi9LUP!V$OjD%Q$=pf) zr7`ylN>2QdGmgtym}Y)u7D&enUaNlkMYHUw{q=|E+k4u)aK&VzJV<*8 zQKWr|HwT|vlgkVpcq!*3`(jRQI3bHgq+Z%2uevd{M2 zOA6KUJpP8=qqAw~?0;vlgSQRuN*{Md95HbJ+0ZbNdlMbqyVPb*i`?Fqj1Jt|cxmuJ z|8a3nOL=dw!^c}nayQ`r+HyWtURpGrl2oPXFPUx0!ub2QZ~Xj&|MG`(Uwrqk|M<`o z8@GEpIZPeMr&8Hb-lmQVoYY%-EI*Q(%w`Yf)2XTccTP<8-y2hl{`csS&K%FZnacIw zIgsZYoIp%xkLUXTQI+%{OFub1mh zrbEPuY&JXfR<6H$V(P7I)(DN?SdIFKif+aidYa?ibgq{kWy#Y|J$*FAKrHqd_z_*+ zO+Vw(BBB>EqUZV2qtE2zBtIpn*{$_6Y?qM)y!dVS4jGeTWp4M|lM-)0)k_+y5g+Wm=moX$81 zqRp5aVI01bGM%zM0BHk|TI%V{laq&I{t7@4`kSEHq%}LhILuGY(ZhUc9V+u}4IA?P zZu&5Sj+s3UE3+q_;hV-Jj!AyVV}u@eLs};%b0vZ3TmL;%KdWJeiOGYR(G$75+1%Z9 znlIPPW>rvIW74;cZT;OtZUp)JK@PeD(lxai;Tw5BkvTfm|Hf4R?TLwOnu}f&6RZQR zMVcQPe@^f|wVTT3F{p0<7`sxioyNj@p#QdjT_H7Z*slNfA*Vro#Ux(b*RQ9fU6#OnObfeJGPY zKGpvLW}LgtO+AD8mcbAwOFW#-WDdaZ!}2QWV@{%**6N>oif_k0k?a2`%PV&~8OPKX zTqcjM^nWB5G&xDP?Br3zDsziPEBm>PQbH2%sPbfHB&J70%UpiMf0O+5SO)(5coHq- zGWAw^Y&7EzND=WRX*HKhGo$4DV0_e}ocyZ8NbbSZpVc}7{UH=13(EngOpUypaZgT3u3oLPZ z8ttA2dtzE`fqp^Tss4vBkr58z?E3G6F`U&hd zmb780ta&a-^yK2=dNej3(4JI4?WFw^WfJJa&DIZCbb^LNcdX8 zPvEA}Fc!5dhm98o-W|{FPRKC>9><|&kK(a`pZ2Z9w4bIY?cKu7!34yBkD)*Wf)NFX zkrA}>#Ha#5`ot*j6yz6j4;Jy$@fVom5<18qWEG~`l!n?CQhBe!r#cx}Rwi_>UkE zb3Lqi4`M3cwD)h>`|n^KryY#TM=815S<5RmYpcAGLca9uP?I~|&eoYEbW`t)<=kCBB&jlE$ zzZ1SS^JilS7r1*8b$E{4iT}Pt3-$J1&G2C}UvUJg-W3K?z@!gFbWk9NND=pp?q~R# z`KXci-FM^!V1Z2s3=2Y7ejGx?kVBpbYjc1EkFUtKhK#Q9pccq%@*UC`EQ72sB6Y%# zA2Mh%LqaD_m@rOQ5Qcc#&=^IY>x}l%?G<-wsnWW5zZY;8B6Lu_23)+^0rZ;R10(ZZ3 znY%t2tG%f^Str39wGY!1*#@2^{8JNDn)H;^d6XYxCjCH{b{Dhc3sb9P9b_C2zN56` zZA*uyr!`goL*|udHX_Kg(}#0+m1@!TJZ8b1m)`F9ly9(*tN;JbL-tQQ8Ey0P6~Vc;T>6U`@g;dA}JWrFINo|97Ze`*%96*vuH?v*BksK5Tr7~ zVX_Z6P%wsi!GD0PQrV@+oJR~6=?}#%H36rCLrC- zWsWGR9sC=W`A*ooq6+&7bT5@X<|Z=Q^5pJ9Z#tXK;LZCV=I*jAxoDOX`_$Vc7wKHZ zLSn^?ZElP}qi`+D`;d^5#nc(Zy!Y>pkry(u?Go`Ew+;Juv;XL8cWyp?_TQ0%y1y|) zz~%3swZ7ni-~88K$gb+|U#)}~clHZ3{NW65-_jdDNtV)ET5p6F@=e$Orz2x0CV6E? z#->(z)!0fBiAd)GO1ut-JGa|hEwKalM0!klYX5!tA(%`O26lnhQ$qh-Zy)LH7oaZi zn&$O9ud}>fSH%Ya*QR{$fv7H%|EAu)rMKTX=o`q99u7fsx6)^mtq(97@EzWEwHK}k zxUTLWj?rv8HR&%C;b2npkZg2A?fyQ!nufBCX$0L>&eHE2kyIr4TF4NQlmsQU{k@~2 zYcS*p`v>?3zYl=lxkd9yyr;<;@IcBPggzTU%n7cqT@>7BMN5JRp{j9yeZkr}0vC>rpWZ3+nJX!G{3)!@Gpm$mxV#W*)#hSj$Hpg(EzIa@CyeuFzNP3lhc5V`w_)yoHMeX#O%szr#4w8sc^~#UY9Q z_-WjfTS@-~eS-yG3m1QD2j85;H`FiQ>>65R&`fBCe>5Vg|&j;t7 zJD75#XHK0x#ghd5@W=2xHi>8;6}F2}^OphT*)qZ#dYn-JqBcML@_qRns`1el)=cCfz>pvK1$1gVUb$7G1nm*>_ zzmhiy?E8p(pFeqlQI1Y9*acoS+&t>)&a>#h$#o3etKfc*>p$mO1@|55{)FoWxHTB| z3h+9wui$_FJJ(Oq?yphz-*|n6@?WL>FSvf0@;9k_0eBVMKj!+Mxn2j~=k*_XErR zKVI#w^7`@@Qf?OcMt`#-*u1;R(*kR}&+&L%k;gRj{J7(!RI3DIB!p7t1u{6Hwg;An)p5f;E zSzb6&Wb_1W#U+0;PtPz9e0bzBbLshGyGzSBImTIFEgjE%>P5-K zX~~ua)u*&9T$or=J*TxgxpZN5+rsMc$4#kTkeJ&#AQ<%Zc0?b4IMFehK@V^K#k6d0 z?~-2Q*Zf2QjmT2ecj^$XMz|4AqGAc_N^d5x{_>MSJm~ymP~*AG%K!6E2W1j|E`Z!A z9Dv+GY>2Kuvo#8Uc6@)0x_6EHcX&0Cry$+~{HOwaz>?OpmjM632YnGIWp(tkGj9d7F>UDJYX(k@Gwt4aa-TdIkx!3p&U32}Go< zWfGY$YPUh%pJj{4?$2q_bj%`6h0&DFB3*@p2wMr1!`Bf((1x)1{26T-w1zAlmN9MJ zp`!t)Y=ymp*{)60BYAs6P6JNR!Hie64*-#EY|`f_IVz@;jnE$cpV+8OtfOZ_S!MM! z+b3@%JB|S@a5j{l9t>PpHGe5;zR6$W*pY1~HrLZ!_`C)5G?!Zy z-ay%BpzQKfUZ9xMHrx=hpSB_7g0`_D$EQO1R>2XdKh9@rHlJ7q{oHhN@a#VaAs6P5 z<6Du7QtggW^iQ22l-*xgPY*%)lIWz15+QqZXFvEOeG^i8{E^M|{zUf7p4ThJ9iGRYCIhBFc=;1N(5! z&+J|msEDSH;IOM|_9aE|>Jcj%amULjY=zyIF(GWO<(L(w(T)Ry*I+H zu$4HH6%4y#iK94Rg zdATPbsC2^kV0JJ_OaaYC^Y$gCF(Y+c18Vw4fNb-+0TrP9Z@o|4Fh;q!79(kUZ*xt( zfbm1Or?Zd~88iWuHFcn|qEapNbt}~jk|bUWX?kJ?&?HQ;$ezi(mmqH~5(o#PQls`P z5I2Vdv6y|53fVu#Q{1$2-6sPD>*Zq{r#@TZ)Y!xV#t4uu*3kwf@>aE5z?f~q9*T4uX%!NgT@ppf+_nfoA`V;DUJWWYD#E>O5XJ);=9VD0vsk*1^NK4BW}Lxeua`_#CD) zn8^vxlLY7w*%mpU`DC(*Su4x1;nu8dFG_y0Kh9hyvlp;9d!fJtL+H(VUcO&FzhK2M zI9UI!iXpPAm3Ct518XPI`_YcaWw=qXp)NBqnUcti#9o8~ID<3Ka~t66&}KW^TFn~K z8!B{(qWShg5EfthJ{CioHq1g>J+9|en zVHO>JDAhTOz_?r%tf9}sTxAQ;MZ|%|>iM9pzkjhTWQ)LG#R_12ZW#2?`@|>P#fdFbR_&e(+i&Zlmtv zHfk>}b90$*6y%D29?$TcTc+gQ)F{9f;g-#_bdni)mk3Tk+14&Czp%PdEt|%wYUzMt zX0jRencN7FTE-<)1Hd^5O0-i2-AFLhNQM(RHKAH=q4eZ8`?QYByELGVAOwiChJYF! zd2SGQ$((==Sg&a?DU}+61r0=;k}`>vVacSF>R1l`9(jwkQPQuYpkn|E3)&90Y&#pT z7KqEMK+CTkWZAkL{n?>$J5sco9E~$}Y(gsMA#PcAHUtIj={V3RNrOI8dEW+i@>!@8 zcP0Od2L(LI3gf`q73EyE<9y>eKisJX=0k=Op4piYF_WvhP+Z;ycbEr*xTZ85 z(U0d=%877)wL2iGeIS6Mi4BA{C`aRNlV4D^ugFEPaYTS>*&NZ_KA5AuY_@zI^;J3A zXuy^VX4uoGGT_aycp7#o)KdOm32Q)MNy+^mN} z7}LaUDl6uUpWnEMcuG9^rN5d;>-(D;Hz%H81?pf$9y{JeHN(k-oo?rMv&Y}yxMR0{ zIu}rUkGQo8r&+B8S=>)yyDM<_vKo-ty)3T8M_rp(g;Yzi3e*y-MJ%sZRFqmhh~rTj zT%OZl4YTg+W(qP>3%Ecu7>|3r*J8~iYa8W8l(ZdJcx{(Q(`6{Y?9fz)Ga05W%Mu3Rh z6LvS-H*paA6M4a+Z~?jr9L7`wZV(?r;+e9wfDM(wr;Ib96?FYs&vegWe8AUqAUU*P!(o|8e(;P+4Xodntg_`L_Rjs|`X_|e$X3di3bcRxEkFJj;@s08QW?$(r7%k~A)&IXh{{||jUT15+f|xjuN#$(M74%f^T?>U zHMOVP2-{%HN1wd~LU~_w3!W8Xoq#85-wJHACzgDTOYJF3ek{-Oq|apBz75f`I^nYo zXvAkM;f}t@R;WGo42SD3)l;{Fv%MCG9&PW<5Bn#ez)9Iy7Qrrcy!WWlXvhe%KBT9a zJ8=iN{P8dpyOZ<6w*9Y#AB4L=2zVMkc&DgD+Y^QLDzjUDbp4E;z^+}BAp(lO{9NO;W+}U;pEr2{-@_zJf1H4R_*~c=mNm6R~9LC-Eyz@0K=ViB+={ zZX}j$b7U#pN-Qz+T6h92@u*75&BPJ|cOr8;vBb2U$lOpYDg0V?&sibdQY<;vDIjwx z91GVg3+tUNTn7p0EILK$%`dXP#6?H50oXok_SP02`zsNNb(Sb?Z*)U;fbz< zCp!`Br-1t7%;Le|gC=5tD8l(1a5bVo- zPM5J*{G8Hh7h=$#;G!^D$EXD7jm4>5Eg=*`$^qxD%8@)1SxDnh-qcDwhxFKO6Cd)t z3SR@BgbJ&zxGhXTclGtnJJrMszzYg`vGpGm^d@0Ga6x-2lgb&{!O$HYAxta!vE5(f zY~2kp;xk`Mx!ct5K+JHo!}Jnx$Hk)j24Mq;H%Q@QnJHLe*e`>lA%FnOgZ;SY*9SXn zL@SxHxuF&l$vdE~ok4auL3frI)v(#COkTxWlqOz*7{5D(hG1n_2xhbV2*k;QN-&_q zSm?k%dg}M!ZI2^XPptrddp!s3S3yOr(cYg?J4%4_V!~ zOnREhdYaRfNy7_zqsC@$%-rlvyauU0OG!(e3>khs%~@OS#Os`)TQodKY`dqM2kr*K zq|F?HXB6^v`wh{|NW96ibd%Torfwy_fLc|hiMK#C5^n?E+KNWcD|6_0L^c@;1aA$> z0Qjy6Pyk{(g}HuufGBNR4u??|_;6Dh_0}|m=Ihc$qre*zy_Ch0UU#;3bSoz1$J*1_ z?6%*5PQ1y$%8p~Em25+dU(jYc%r=ap(gy(=i9djMU+TL;WqTNO0rFM$aPpl}kZ-BV zjAbFS^EpN;Y=|OUog)lYgqv~%G;dCFe~tjVGvT=$;ZQ|*n+dDA1gy8xRm4jPN~NaZ zBA_ewrJ-w?Y8#TFu;gvC7T!$T?3N_Aa=6gGs4Xg2X+^h!v~Xy~W{=aRr0uFE#SW7V zDynx*nLYx~^b~-k;h{x_J(~;vqr%9k92v<+ zjx3Cv#*x^r&89pIxHDH|`&397ZEnF1KnIs*|ijSy(lt z7n(8}4{L0cCO!bW>X!z=!%(HN-~PB&t6p+=*6Qm!Nj*9*buFBpmKQB;kY!^^$9+C8 zJD$gal`Xjd;bcxMjQ@jG^^98kaY_4>cQ!ri0 zZ---3?Ni<3>#6FQsg91SuJ$SWaXE=y%ih_p$LIKrA8glrntv~Q@_!*apQfwqY@HpV zr|*MczEqOkH`USjiK?UyInLlkrm93~A1*ontd@E#4WwQ`WYBB0Gb?J+EAp~q_{40* zpffrnCr`(@F1>rK7r8D!%+;kU<`FWc8dmC0AoIYv85>nR%5<*c6Ch5xhFle^jN3=E z8sei>9oMKjW^%5M2caCjq8xi+>B&^+=|7O3UDJx5 z-XPAohK>S-DJWIWmr z3lD{Qng>ADE*+MFe5U8(vNMSRWVp=4yJ$UJ-#S6pPV*l=!2%7T%5zv zR)F80!?}YcU(cc#U{mLDvLisIx@>QU{H%}e!}XQ;1bHX^0^sxwUiGOqYRH8nJ-rnI zI-kiKvLD$O=~FKu3rv06nTa|+g`AEf5tO@e=OQlkKOkk8gkxcT*g>=bYmfCZM}Ce- z8_hB9EC;16q1t)8!aPS||JAJ&al*OvA4QlwBjLo?uE1-#Y39bWhcUvpR9 zV2-yy;tc!m;EkBMIF+YEcIC2jr@zXJUx8qgBV;g#XLvj_cn5EW7yp{j5U<76(iI2CzA$zf(M~Rep(Ea5M?pXrH46%| zPjUbMZ<+!NIzch4m9BvioMRog7^uF4?{?-YW9A-1PS)8#ZrB&sUI*X~Xdy>)*qX^_ zLSc`S>hsY}GsEr~VXFz;xYdd}aS@D9_4ztcbx>ebC&9A#lWybyx@t(J=sHW z%miL4(UO1X9`!GXP5u+1juRopx>okOC7YbbWxt;Hsg+2e^M>_4K9zME0G(2=pn3V- zdOI_by$1>0d#vAsn1ZYF@kO?{T@1jl8k`S`?DRJDpyXcY;TGA+$v!C8#_aISKBN_$ z;6rtS7p8$+_6`jP7+E0=ZW|k*vTu$925hYRvaa+Nnx2rFWUF+u)m`TMbQ&3q>lu{!3JD$6G{r_XugCS09!eV#{rfuX7VmBJ!AGR0N6)z9fwW*d{o5r zB=EKku2}m4otw=;FAf0*%E3?quN(R7AV4An(3ghIM}LQepYKojh+&BA6w8W8@>CI$ zr-}i1;+qH~(1BNRU_;TWjFOK+A_VVc!CYsl2%^uy-Dn)^;e0l1rMn@JjskSxtqte4 zEF02~yVR*N=HhNwz*gRg%|lfXs1r|RSDpa#1Ssk9V~)iw1R1N0eAPUCYawC;K!A`r+!}B2Li5r@} zd8wg*iGu+G>Y7=g;}9U}LjmG0xlMsHqyqO2qZM4Eh?q_sdZoGxfwR-$93BCX9kwWc z?Eu#_euEBN;wr&904%+5Lv~EFdgBQsTmg``poWg+8|j~q0T?>g#snC5HLQFR+E0@1 zlEk?-uabl;q^ErDBYw)L9yTo*w_JR)!cv#2z?d1`aSFWWjJT^8j?qTp)qsv>;GIRV zBk*LHPVre1ANJ%+hkIunWgn(WmT8>PkSQ)- zN}2^-1&3T>O(D~0c~eEnQ0vl0WQo2Uu9VY#zWd-0;0OhGAByBzmh2|a@?@nvE0Q(x z%zK+-y0C!u6=?Ixn-QhXz{H1`gyuj8vz;Vf zoeetYQJ96H*J6Y~IyRgf&dcffJioG60VUw}pam5+o($XBO0x0n5;*G&TTq#e&*S15 zww;w^gYg<5o?)w5Nj8;|JB}L%8_i0xr3#2_Gb_o4Dj>4StR&khF&-%!jA*>r!5>78 z+ANMTY(gu^7E)Yp(K;S>Xu|mr*ymA(Eodd##KNj3fiW0ENI97qwm+}wWc^%&=OJv* z7chaUw5LOqZYV8r?cH}PQ)O@gAMcl-3Ca)D$lvc-#5Z`Vof|xC#;6#tOiUIPL&d~0 zu?(9cnwc$-%N7%Lq)<`-rI#k}=p8D3s~ud4BcKakl8I+^;d$$5Xfg~u_OXaf90#DM ze-7>;MSA)HJgfFNmylz)Y24t)r5mQ|zPLM@!g!2r3N>HNzLPv0vSiKg>}*ydk9U#B z2&Rifzx!|#1TTKk@pe=z>zqf^~3EgUpuPG#FZP? zBR*Kymosjkz%i&FlXsZ-*?vEOUM6fY-#fFN6y)$v0Y3_~L%08cQcy<*-03v{iK&P| zy(6*;c{5#!D$k1QmQyY2d&i^OZ?OQp9w_%8xG1l@qFzhS+T=%4cGgEMUy+)IEY3lr zfNii=ChXNXw((i9=EJiV;b+{$KRxc}d_>W=(CXAZ0%%v$IiHc8^EpTDz2F#Asw`i_ z?OefiXE?=B2H6vhK_PYTDcgRa7tfu-ReL&BHQKy7j~InG>R16`b~M|pxJ`9OIw&8; z3&+z@()3x(aUw$V)tw}MS-o{r%>t!NC|k{?Mo5k zac<~b3|jlyOprZ_+$u1uj2E&zvPt7s96YbE)D>^Z!IPMYEGXuqcqp6cEKqP0NEM=8 z4ABERs<+weD;z;gl1-uP$}?AhE8vOANp&$lO zjD-zWKK74Mn4I_-xdP0L_&s626LDsvbFVdXX5&fi=(gO&(d@=T5VwsH%l?HMRi)t~ zKAhq9!uTnc^%>{9(`x7;9xLz3_+F63Z@tBx_cU1J zICl%`;#VUj@v@Q`_!65PrWEf@+#V|_bQfdJtVn9J!R z!!{zrl8D_3jBmzCUF-gMZp9PZ2lB)8_krJs=auL+h4hDk<3NCPxcEZ)K;W}MzpQ}HdyA_JrZk8?XT6NFzj?qnfbhQv?eXax37}iHmFN0MZg1z7byDTap z9#j8G5rD3d|3&cGOs11|Z|OJ$JMbFyRjY#bT-AKqJ(q$gL~W2cGMsa54kzEui;?++Sn?fJelVG8E7hV2U6P^?O^9`!^CsL zF^+c23nRNDvajxJFLh+VukW#LVL!W))2;%bj@631g~@A}e6EnsQ{-DalTm&{_H)Gm z`-(hY((WVjg8hpWavkcdw0#}Q$)RXFHdZ7QQz2x;-^&=S6W_JtHpL9^Ab$6V1`xmh z4Qn(XFJDK$oK4wrAyjGT`VJvvVGa#hnAYK)?Tf&pjEeWi;OesTCa_^Uj`=o#FL5#0 z0>dz9k|(hiVeGXz(Zr5qogK`JG}9hAZgp_T&b5W$b$O-X>4m5RKaz_U$Zy-|JzW@|KK;vu1<}KatpM z77c&gpM0sy`bw#x`r?9G6YECF$r6=s-=TD_qcKC3Peywq?D^BR_$}tr;D=k&o4~8^ z{I-A|!F(KpD2-F???Tih%Br>lu(pDD+!!kONQlo;AL6)_sncL1^sLJ`e4Kyj*t_f>V=_h590iyIje2XXU!1 zN-i()vvS?-7v<$%iCpE2A^}fDMURRCSswc;#3d%O@T)nro)s*lZeXgYUr4=%sZto; z4IxTwlyoXQnXTc8B~S(OJYMj2tTphV9^TKb!U#W}ZQgMmSaRn^c>O{;fb=WXrSMji z>Z3R_jf5k-KKi2<8w!f?2E=B`&*#ZD@^d(D1P2axP`*X+3OQYKBJKr`eG_8kaEAo? z4b?Ga9-sU67H2TxVvNk{Bs-u4@N03FL}9y$*GQWnt9BayyfU@%Agno<5clRt6g1?t z*E^GsbJWPPBiT5}C!1(a!_F2)Lt(N^nZ-CSl!!iL~{Ik#+mcfzOEVqY1rE) zE8E5Mh^~)wjGktnoEuw2bP#PF3$nPlYj9i%4tq07FvO#$uLdHAvRngmzJqVP$0tc{yn*Hy3OrXT~z2)PAkoZjX)kSe87#5y4P^C_V@?^>h@?^>h za@`ixxY{D(kA%rlVkD41OH^Jhf8rN7;EA6zCQU}>`Q#-5=O{AZ97P7wLtxs1qsU0G zBvcaI_k1w;@6HGHG`xeUN|5)XgGnz5$te#U(c@#Aft-SZ0%;u61UQ?w*cYiS_V&vl zpkQP;(3gvN#9PUBhA*PvI`fk{) zzNhSj3TT_h$G$i=Z7Vwof(gd1mKM z{S-0b;>3M;@D?3n8^<<9N}PHM7lQytnLP@3sw!O4%sYQF{Nkbtby-@6Ogai>1l3ah2OTAJjNbk&4#C`nf~g`klk9KIx|_LV0Cc_6=+tCjJP^}fmoDUE<%-4^^VilQlNVFRJ;&hz(sD) zne%71MJ!w>Jj1ha#{6c#+ba||z94gkS5d;IkUZ|N=ZvuF&|E_Ey2IWx!Y)N?i}++^ zfiZ*)E?LKLr6+93>!Zmh*=~0lu4N5L-s!^^W^KlztU4;VY#IItSuGq5^I<+W|6$p6vu^MHdQ@A2jbMaO61IF{}+;mG0Q5Oy_g@>~48pdBwet@`}8V z^1E1wZvULM=kF%T3vjGYMw9_ilh58Ljk@KQnTrxA;_N&T-Nqqq$rDe_5kJloPs#v&`>nj6ukt8G(}Qr#mMTaIm59WPNXR`)rR@vj zaaK}~JWs%cjwV7*DcS@Ur98l5m$?IMy@AQ6bdqga+9% za+YdN%9AI?5@SSCspzEM#X{zaU+s+>JqB0?zH`Xi*f2|!4@Yn!zzd=q@KDW%aA;!K zup~^9eF31;{eSrcxSmEgHVZ#O&OF_m{G9KUS4Uug-@)94cc8lPVDqo*x6BRs+YrZa zU8-h!D$99^p_rMab0z^XbsaTF;`iPqt*c_GOJCCY?iPu4o zH6`BQXjjJ_FbaRDC5>HwU*atQO1_7@!>(lC(oQ<@68CLR#8ar7a1I>W6YWpgOF?hp z@^*Oy{-};q*(?VickRzlxtNWQUpMgwvBUoCb@36O;><*u>p7$mbMcLOw=&q{s2OFj z)VrwKh!>a93(V5awVC`U5{A7}v+C6YaohzTjSu5}*WE}ipNr>@fjOUuFN%-AH4HRW zzM`M3>;^F&u~UJVut&aIUl`Fo1SxX2JJ=B0vsXJ9DZJXTp8_}F?tm0neb4@Glt8Y1 z{3felhDo^g!4FZ^#yL!!Z`s%Pv)^Ic7nc;XO-9@T>p3k$ zWp+#F`+3y*N%ok(H@&fk%GaWO>vV@U@k)yM(}Vbrb1X>S=1NJVS@BonSo*G@TpI#!h0e)JQVzx zp1ui5F{5}!)UYuq6dZH4-;78-&5&+_*ME zNA*IbH@U0_oH;r17%9~U@;hcg21bKlaRX}xHF+4im6}>fuW~jAevBx&2&`^hpA58@ z`V2e^uxBg$2Y)0Tl~tS@7VKX-mByokjDsUe;COMX^H!EU2eO?YXDho(NRNY>PP(ie z$J>FK#wN9wJ1CHkjYso$#lf>yzLuY9Y$io^l)sdB5}nWGw{y-owo%s1Su2KDCpiWN zdpu{b78tC+N|LldUkJt5@*To|VJDsfDg&WevxQLha>q6W7Rr7upFdrOnSlKyKk7y~ z9m*Mc>)`pZe#O@d`Mu=qw_f=L3V!e4f$Yb(ozJ~?`nKuSD5dj7W@eA7{U2PzI|Fr+ zy?KD1E}Ils3NPaTx7Ca$o3x z6EKr+f;ZKOyz?F1(fz|t*|Ecfci?JvK1bfc6UwAF`a{z9;bT0fiONJqVe;1ta*oUS zCM!6%K(g~ECD>rjh8h0c2eQ}4JAYilZT|@xwli@ZLt}6!E1`cJpHTWUB5hbo#0G>oav{yd7cG zo@6-moAmcqX2)I^C3T`>^L-x61GGaLtJdKJM^sCh!%fi@2$FnS_Su`LYCy1q} z`V$Sl>|rS%?PPsqvBM?DJ>o4TJN32;>uJ&y1K$(Q_Y?Kq@U^+GkV}(Io{O5$)0?5S zV=mRiehPB(QLd$B$kZnE(g?CCAk;e``TCTyd?jMxPnG1L@_Gq)|Y~Ibp+zG=&5Bq)zqki_E%_)#5?~tNnW=1>aI~VOS!!gN!AXU=t z?o^Qz-CodKc}a`Bl23pq`L}<^*i#rg4PLSA;KggetBJ=f^xZhFMyG#*+-pIRpTsxp zsP**itkOi@u=$n=4qM`%>>grGd+^buh9}kj`>9Z|$B;_GUP?%jgg}boCOVpC8fVG)y_kKlk zZqRPim?j%vl4dKjW;pp~y=^a(@WbB>!Pw2UFV!la8(uG8lFzn?)XayR@ihBPWx=R@2D*UYQAgXF*d0am<6Fc7H|?F?!V-50;S$nuGD7_MZz z!kD7ksqF_nmlxVzaW*eZ$9;WO+CZnYqmdSq?@sSu{sJZ8_b(sDbKmzbnX2Bu{0P?B z|NEEkag6*;SboQFELznFnDck%wwQl6RL2gOK5>8ViRSdn zzbDEW=>_kJazm8&L?uG%sPLoK1yO9mPEl+Nl#|a{`2Ep>rn){V2Ue*N>Mz5yxl|E{ z?&J{Mxi5p7Q176OaqQ}xjC4L4aCZ7Yyb1hzsu)7rc{tEhVGgpx$Q~p@&e$nOqZ^>I z@DsrAnFkFRG~nPthaPk&Q(SVsSv?3B13R$kgJH*rc zDKw~E-KVcTa@qte_oxy0+7iT#ou8P)%=pn!6(`>GVBE____ogw{#X&`{t$lq;THxY zR2yxKpw zAgd4^iH~_$^q7k!2dr;h7S&i!nISsL!}QZVOkXN+yXRY%kA9ENQ9_>zWsR5l-}Exi zav$rk8y`yX(ZbN&kVSWdo-!?Z8!&{uWv2fDt5YBSwwTKjk8rAoA}rx~fu9KUl`^GP z;7EZd3QP*Tv-ILpi+&|=mr(vFQ18aNslOO;WVfN+enT_6aV{5iJENOLcLJ8s(|{q^ zutcw-j}YsluLahkCVezb;1a+PT^QxE-x_5N?*=TPm!iMM%gUc4)~)>uu!LeUjy*ia z@@FF)6{}iwL5ywtSnNfV_r2Kju@LQzv9yXZcbP>4%ed|)mNEUbGOm#e1g;f$yTE?{ z;-IzsNo};&TF&~U%bD|Hfo}`^4A7#VRrKwAQYHF{jv1MNiUJ3%>=J^>6-kDe^& zgq~dc=Sz4U!at&Gdve-e3fv{|4M2|Zs>JIFN?PJ z;&S~0;i$y==qZGK^cR6|VLBS3U~iULE$~pliF7hLD=g64U3Q|*rMBoVeTTwIX1_Vr z77gylGM|AHNa()<4AC3?Sf7y17AOZ?p^uXA5rF4A;WO>Ip%7hTb6&p^_;)}b&F%j@ zUO2y@KYNq+0kPdu7q0WsPcRsu=hksv7si=>1K=<8djvip@DYJe3jD3W*8m^V-vhL$ zvVrSpR6_}TgYq>DHClr?H9Bipzi-B?O=SsSz z90%+Q#JOk{ZIax#Ag-Lwl@b&~U&hwcPZ3A-B*uOh-Am6?1Zg5@c^kE#0yCh?|5R~Y z7VA;X*nRYZ#9b!XR(eUWH)}ZVXY`6-=c6sVVB=Q>>r>CzHhNdE4>9p|(RO-|CTHr= z7NayuFF~zM(xZSU=@o$M^!|W1>kWYS2;44kr#={z4|H4sq)+wXfL{qDgTr2f=@ka2 zYA{9ujubda;AzHaP!&G9rn zbB;L}aD~9t0xtm6=uhTlfM0+oO#d`F_XVzd0C%~b1$^7}BJ;Rj0gPF%0S>U<0zAa} zBlSgFnW6QiX`b;2Z}iMSL`Iz1fC-BOo5krw}JBo;bfT_ zec*i@l)nq~`#3z*_j|xb-y48co_7HIdER5X=R>WQR{H)1c(uU01#a|wt%d2G>U+Gk z^sP`V|Lx$32<#(pxIoskmL^DemcX+Fo-c5tz&rih!1Iv6X9Tk5VS3Zgwf~8~N)OXF zer^x<1{we#3ACX_JRMj7_-0@+;3t8RpnolJuRvdrDG`CSK~5VFE(PbI0*@5fBygU< zvjMa9cxSLppPsoNFiUwnxB!%A1-=HDrN19s4Emo0{tuuN0v;rk5%~68n2tm^OecoU1w1Qs8Q?{sjer|Nw*$8H8y2)^Mv-n>pm=Yeu#IBA6G&hRq82g0jxY{2~`O!Moxf87Nb(FXUJ z;I`-@SfHKfXBmk5v})}ruPK@=?=COFQ{uY2%Zda^r3+_a_J{I>^NY5 z&SBHAL=0dgXU@MGvoPcK@MIgXH|yfVwR0VB#H!B$d0 zuw5!0tTYR8-FX25&7v;6ghAH`b{_R{U57dB%YyBs;jWu8y8mM&bMB-@V5L+xim{b6 z$#pwM+(m+|r|GVVXp!b~bQjN!NsHBMJfvjkHzF?p4roxw@g*ZkKl9BjJvoqVT=(VfAQ zEm=4_aSJR ztdA0j+}qrPH0*N8&(7dEz%WI1uuI*CXqb{Z*iFDN;Z&IGe)k6A{RjTp=^EoH#E$n& z(duZv)XHkq*pD^oC}a?~$X1Q_&v>=FQ)}_e)3ES%u+^T$nC)b-%RFgq2;G~LvJ%Ie zI|{IaX;%Suh?7p`Al=5v;e1URykM(6muS+c1>+nJ>y%EKv6F6t*676T)K+_L&?K{5 zy73an^~W`RIK3v(A9@vLj^$|9t1!=(o}Xz)&|C-edw-@KNy~Gb8*rz+cpRtu zP-A-@MYe;nJ&&Tff}KaVc^}ax&`l0@pLYXh!asK~u9c%{mtbu9o!ZfKl&HFrYJAUW z$Iwc_KJ;?$IF7bB8266j=~V~g-f_IV76e_q+&d=Iki$hYFZYhgG}Xblcbq`yI~ezl z6X+)n#=T<-z3E`wJEoAyTN;qbR+>t)9E^L%RQj`naqpNWFHtZj_l{{a`It##tKugs*U9gO?REHaPgbSvouaGpd%1^dv;J?F=?$icYhoJ@B+oF{{GHoc|d zG_J80S~O8fY4d%owa{|Gxc)ZKT|me!!umY=KxD3?=c{k_E}+jPj&ms27t(i=IEVFQ1P15{`tz|0ivn9j%h3=x zt{&K8`oO^&14H#BosGj#jypCmLQm1V4mLf|s4tgP9 z+rct{Y5FocYPyQMG;or>oOU%arhNHoDr*+Zb6p^VF9OXKY(1?CP6W1ghDvw7U~kP7 zPFfpWrmv>JEWu>lJeQUTw%+ql=q~+Sdf>;B&hu300bnmU*k3}w1Xg;olDRiD5o@l- zR)v)py}+D;okzWkHqeE1->Dp@M%WAKFCt|f9oerwcp;r7ajFa#5h}*v3>g=ZE!aw` zFWx{G(|v-ik&*dgGENsxI;i+C<6;Ur*hFxyrI>?F6|A>|Rhb;O7FfO4|+|A&0mlYptTuT2~#@KmubMZLiGMbfE*ge25 zrya`$lNPy-hP5-M%CL@>bSR8x7wgDep|Hooj9u-<0gR#x7q9qQ- zHoJ;$yMWWJp-GZ%n}ZEECm2`J-D_3cjFNH2)l`3(!p|hs` zOfasYp;t2Jd2}_fjr5veD`^vQxR#o(=D78qyGw2~uBAl^lR4P6^p=D19PB#!@*3f! z9pJp4hHhj`wX^GKV1}`k6se!5T~B9=CDzf@`nY)mUGHF50=tnyl1{b3o9IyStxC#G z)Tl7hs*AOo=o9gn*|@(cj0RP2z~5*#kK+8!qjA+!!1=oP5XHHPu*o8Eo2#$$Zla0O z`&Hb{G*w~rKQ%Y{Zl-Ib|LxYc_D%&h(+3U~?b8nII|u7qR;+ENf1JcJcYBZTvl?+l z(yMlB_xIUAo2ksf-VtoJ!o1~uF97FRf^qH>jLo!Ou$9!*cLUu*$B12&X1CB(h0)0R z&x&rLvh|Xm_D#d3;Vm@R!C12`wA{g1vn}-J4N9}I1Fiw*UWJKfTgW3dwp(M(w$KF% z6V0~JO@b-Sw$QHxTS>PK*g&_^>kjs)V7%M*98MK9?2qty!dHNWVM7iGG^}8muHZ|; zzc1_-W1cG`43*5nR4V+Iq*ZXYg#QGn)5j|8;O;rkw^{FlxpE(Bz*=lD)?8;$KhXNoLOdIQ57x>xp8wSVeog%a4i-3E z;241u1Wp#%EU*R8ueC{dvA|^lR|>ok@aM)lz(!{QMak- z??nA(GkqYrd|Wadd9_8x1G4aU*`JIo9~@&@wicbOARubzxG znWdn#YMQ<(Hs9<{>tk1#y=hDARq2N)g7DAEDgb{a@EP#8&>Lkn;QUx|miKinqi}h>Yn@(EPTK1LUaFFzXaj`GQ_33OjM+jcwPGQCb~0p)R_ z{6^r*9Z@Zw!YC9S6*j*p`TxQv!xl=RNiTMjk_!Fv%}~biWhfX!j35Z|MFvmfw9Z!bfzs+~vm1?g950 zY8A?2p|p38y1mAQPPmtQw|*sJYm7U(4?y^y?yL0S#%Vo{bx$xX*GcYXR^o?*zF~~C&S{Npz}-a3yt*%Uuawd$n9h( z_|FfH^OOU!oI83>@N6{h?>Ssr(Q%%ejZb?{^K3WFs?))?#Pd3xQFV{!8Dm1v zhdjHDbE_WnJVRGiy#Tlc@O65q>O+Jd1LuqM`>M}8Tj`Hgx_2rhv}%tDoF(u@no-^4 z{lZvL-Qs-_wr%oiW~k7MusNXDW7tz>zc<`uzqg!5^m@d-2Amu4_0%JJt@kXauVSD0 z)(CyFetC4bYqEYVAp5tkeA|uVdVS~HO|yFG{xPQI^7tnJh5*m+)y>~*-rlR+-vY|l z)CSny-)6qttJc5Fd;|1m^L@aTLT@&|6^c>I^Z;O+*-c;%zzJsGTH8O_Jh*lMU?boJ zb5iX={@rw9tq1Uo+F|}>)ZTp*!b@t8^sf=kw`2cfl7E?TPVM7p&#P;vg3e{}qSvhU z@76D`Wg8}YUgQrO8)~;(yR|!Oul3iM+iExYy~Y!W^%_qJ{57D4(d0J&7y2``_xLX~ zKdOBY@Uz-q`1_lS(Yw9Ix3%2sy}ft%HSLL7oT+F}0j@JkdOz#mX!h;>yx*(W_kIO> z^4N5<+1UFn|8lYOO5>Q`r=!hJ?|qmVrq3(-1g@e)?-2n_Kd1K*f$L>V*=nwj@w$OW zmS+96J{vHeZ;4$HcnsFtK+l-|ewzZ%m_7Opcik;%A3&<-0*`?IeL(K-PfEXdQTTVE zN4y_cXI{|nS^vu-|1FVxmDKW8V!f-xR{hQO{qBV4xuM@h{tuk?Gp_8M;4ZqQ->v3V z;wzq{yZUXQzetW>N{)AnoV!KN-6H2n@hxA9Cf|W`BZgrv%iSWu)L6rS#&QB0x4?cCdYweKq7zuQU0HhCQ-?Hj5w|JZL> z=p*yXexm?2dsOHPvyVL?REpYc)w*jd%sHSOZ!ZXiT`~f?R+vdpnnCHVMSIQ(d0o+- z$(2hq`p38)>3=oGrsw+K<6nkR>{al5)c;+; z&-(un@H@a7!>AkJ-i}f1UUv;+Fid#Mv$+Hy!war-xZBVJ#KU#BlXx{|5tr^aep0e{uGDxNW)Tn zsmgSDJ_4N zl@#YB}18wJi5xJlp+MeokBvjuJvxI-ZI zV0yJcjdozOs+yiKJ~Tczs?5IT*{*TcDr<%0TKqr6kRDeo%pI`1=h!E2Q77~f>ybl)kyOMDxAkN9@s zdt@v8m-|2UTY-u|ePCwbw!i~{&jNoBd>hykC=Wh@&pg(IE)6{r(u?9n6N_4k78Tu7 z^jgt}MT3i7goe=qPI_u-&?=$-)hg@jEHQvw2`o?{VKJPQ7A_PwF+hZdEHT)X#G(Z^z z%Xsj2eb`ED+hXiRSfGps-MpGMsO^*mre<2q+us!2*Qh@ zg@(V2dN9H%Iuzj~ppFxZ;eZ>&>es-!I=u5}z^7;o;2ZE$I=zkii#q)g--E;b6Zq{u zbg#A!`#$$+R|5V_I~SA(w5t(*P}=~wO*gcs@Lzk*Y~pTJzuZD;K1C#yuibO zCjzer{v0?eI62r9oEERiwa8OkTwGgxY4Q5v+l#js z?<^h@J~CWE|lB2EWN6mdCxH ze~vSiJZ{NlXdC7Dt-vq$>1woq8nmNc_^n0j?Ty6ds$&Cl@rh5Ul9ed>NmTj+gBYsc?e?;kY7_r7NPQuGi`R3GxC>94*z%^$#@ z`oeFwz-9V${Q80^N(3L%6OjL@JEdJ0{FgpHv=_hQj2A<97=K0DHRiwc_V8Z(LY0w| z5_6Z%ZyiM^wk%teXj`~sQp>`Y(^``Q=FXo_R{fJgQ~k-1)6Z znv__YYMqogvsJ|jdicU6t;quXnAVnk<2wlujx1%u^tM#noVNLGOP0}*Eel$woYK0m zHB~@7KCx)&qDid_m*%u{)OAWRb=jiU6e*etPg~LkgC!Q8+IAX^Z%Zwj-?B{1g7A`- zg{QQRYngw_q}C;;C+6mi=&)lqLLZXJlC2gm)q?U|to!@#|%aT@_wh%@+eBo)x8kS#}7~6V!%b9J7rAeAF z?*Fv+ZLx8tXE4^>WFmDHaOJx$x)WlcxjW@LAqLwB+`nN;j@Qj8tDT~0DBdQ?-c z`fb;Aol=L`9vE<0M6l8f0*RG&q~&5kE0_zUMFJr}3%iK9Kw^*(8ZoQYhy^qkBeCL& z3vPhtdEf8*|2nvnv;-1@by8K|@%`TKJ%8{2|5clh3R}6AZ)CR%Pjjm~56xbqwOc8d z{d%s{sJBhq_salTY+6)mL22Lb6vFoQKy3vpw>y<$^?sv#Y&yjj21{|9+c|8ih;236 zrd12e{!w4YVJL-bz2x8g)duz_r6RU3g*LD#b^Nj;wy9uC zTM$UUg{3Q<3PiPi&^Y8ywb?)_cl=fhOnT^(Gp6O2z~qbr34I|GDbEDfHl;?j(E^zO zCVSLuv^uNUbz|RvPV0>lkZaUMWE?JB3q)1KRyf%JX1;R_Wf53U4}8DcS6?sgx_Y_N zrc#x6{G*P^l*4^x%au;BNpij82i5#AW}9^?y^dl@(Pl|G!BR*O84GRRD7HX0vsEpY z0@#_(O1IU<4xSgQUB6H;j~Wm#AZCb=UBXaK28~X8ty1+LK}Dr6S65{MRXT;Bq6)AG z3XxQxVterc^tn|jnRRI2x0lSJHEwNF2R(Aopd(Wf+81s ztzzduw}(ltF8$cjPNf-aon^L*rG^#|qALgOYSx3Uu$tZ5qkKH~tH=G5vUSlE0p`jC zrd%vSeF-b9u`h57>F(DwO7%l9MGz zI&y}ja=VmIrNbFCaC;7S7;MOGW)p@9=1#mWL|-b3&Esa7ru7L3+h|=goT(1mx%c|> z#^=z8O#8TA+V z*7JU|*eZfRZnWANTN7_A`)G_{r6Eqnfuz9^J-f7F@N4cH6v1Z}gDk26AEa|V?h=tmn zOtpDXG!OjFI#jq-th!K@O|HDt2;3lA@f32Ap^ob{5^4oCjF8AJ*M3xdzQxl7L9O&H z97JU{WEu>h$k|}IeOOjZMLZq2!y00(Fa#|3McF71N7>Hj13L!e=Ac^+5Jh>Xf|vrI z5B*{pewf8feIGsrbj^1frO&Nknntr&;yIzC?p1;^=(xjE>V98PB@N|)z)G6nHadKS zTPme$qwPyc;s=Q(Kq?0B8ba*YU?bQ&g72N}%nI7G>)HHHF0)>^zq$IUS%8S zj>tGok~9!f*cya7l#Q^jzZODln{W8XLd>mVrG@BaaG0#JLYy5SK~^2vb(0HsocHZ6 z-N1Gr#r>A~>6fmjV6olJ` zO)v@-oH5~ZRf)|=15|)|*=Kar>F91AS{m>aWKW>^?9NW^(Sy9LW|~d7Az5O*2rqKr z_6*q`s9I&VuCjtNFOFi^d09?pJjn2rh7E|ynl{qVIIzkM1mN!2Wwk@wI~Co% zX_s0!n}p{`3pQvVHW)lmXZ*%?e2S^s#p##^qiqEWLRLWvf;_M`9z-U=chpY7@)ujk z&D338M%uo|^(yDt+7L^5aI3>zw<_J*jvU8V4vO`C(1sw?BzFK1D2(bA|I;M*|jJg1f9 zWCp5L%J^m`dyr=UWZkAFO`s!8%wV{SN?RJL?mJm;n=OQJwG7Vt$C(bkjAOTpu$Qrd z@g-%YAVd+%hAJ$lB;+s3rrWCG)B^e)8gkJT#qWZb@NIIKEMa&Hk6MVwESX?Y2oBD% zX22H93NF91Q|VOw5FOWyBSNWFk?3}`bAW+ob=R83dfy%$P(&i-ZmDzDjOKBRQNd`} zPODhW9Z zdI_>`67V7?>vx~~Es?%jf29o#@XL9`Jih8aj&?{UR?Tz$pb|0+6CcNlhf>-9q6i zE-kIWSs-Fq$-lRJ_jaKmVFJR#M*Wz_8$Cg|^VJeXQ+O#!Cxp^{GHFp2*V%qr!M@6N zb-TDh4i86D7vA8Ei0}q((1ttAOOeV*o-x`&W(u)vi3umk4u>Jts=wDZLGp8v*=YDQ z$empW#1?CX{VGdx%o#f*Ed6loaT&CJjBucT?vvU`)+3m1NM=!z+z4X94>5{Xz{7== zYXixNHPJBI_pQp257{8)fKZzrU2>a9iOGK3t{}b@+b(rB_x9R|Lhvdy5q~pgXBpN7 z`2#5;dFOXZ2Tw(Q*tdsO5a3I4mQ?Iu51Mepdg7ybPSi9~ow!wH@qO5M0m$*X0jqsf z9*s!IBi7bhjhb!Y3TsHmi4LIqVX=L%LLa2=kuoJd6TCx+!tJgXj~m^NW(&eP@`!>l zvW5_RTd^*dYaDG9j|9)-4NI*fS2$)DcP?Yp#c_%a)-~}j7J#iC9>7_EFXN>c$;80) z@rppztTU?7wug;Y<-3%)DzvLSSPRC(RqtCYm-Q1nq(Z%>9$m*ttB`f}ijt|UQg(_( zGI?;FCZKf;Zw%R6tF+q4C($Vz$?9(I*^tCM1P#|4`^*6&YJo`;SE|#jUCB#o9j@X* z3()}yQK=R8eIX!IQ~NF0kFp8(t5LMs`Xq0f&)U>M#&w$Nb`jHx`f~e0pthK+H@j+# zBt;Sgd)vTWt)HBjcN%QqJP@6dZBHa+k_Ra|#J9+o=R-4X%R3ETL#Qg9C<1mmAq9aC zK;*l-a(#)>8@zJ`=FEyyxq=HABB6}g@=Y2h@xpD}jM>OP>9vpt$QlG!QMd*}P4h_? z>eQ+O_NtN7bmpi6<(3SCUT5J@INC$RrreNRe`;2B!xBVrbiUi#gO#(QzrpNqW*66g z6kX6{u_PILV0ZnRSw<4#Io2g{{2HvJxXx8Xu$5~2?w`xt%3i;AW9j3C%<9dn3pZ9j z&Me$oy0x-!}uxb%F6W{-!pPh6wN3UXcQ_Xk*XX{25jZ2=+4sZYv#T8G|;Z>^j0*2fsB2N%t zRk+h=9S8Y8MCC0bZvZEkA4%N4BhYrE(UG}-27#sXi}WZs&etuCQMRWrMlS{z(r{@+ zuYYJ%1NGT|S_DsFZ%g(}i;D(&b|kYHLkN#GaRIGBV9|jZ2L4*v1OY{o244S&SxkvM z@aZPu`UzOsGWU;Zk8!^Q;Mnk3rg3X^S&1ce?5Z^z@E_1x5KKY{TOF+Fc?>Go863oj z5SM9y7uY5Vd&P%qzY)2Bg1d$a30DiEgXDfwCN@fT6FEZFe5JR-8q1Gi_;d9 zQbmFd2O^1PTYmPabifnl=!)0P^1jlQi!B8gw#BBmtZ5G-^Zv0{ap%oz#c$UFph8Sg zMJ}C_VS3#T7g1pW^r(&4PWq2UD0zr>Ema0v>QN9~!=#I=3MB>Lf?O^^%b^j2Km*90 zST>V?2F1y=(TEXGadp3JwKQ-RBp5E7A`-;Ws@~?2MX-iRx3wV0Il?_K6kr*+E0_1d zd7DKLKN{i+i{MVxILZ!`b8VX}r1!%)pp7J(I82Erx4~l^$*IIjubep44X73j7s9h* z%|I(~HIXCXIBgqikJU`9uI8DNr6EO8#W>rPQ>^u2qy9j92s z*$IlM8!``C#pZz=e3_rIjXMoz+5*!l>fJKWWA1Q7 z2Q_+9ao746`-Uw$@wGP}ExI~cfs27dx7geTjQ$hX8H@A*9~!)T_gWzcu+NxjZ$${u zgT?>S=#eSz2zbU44mp5c#5Go&lJwmU?K`zC>H&_HcBxQ+!-2P5zFRF6YzkD(bgq0B zo%>hmM`*cJx_gtxdHL?gg~B0318(JRtx&*m6zR0%>t_M`-$9kvq=*})2^qhIG$n3X z3UepAXOT@iw68@4s2tGcQ3KIwlec&Hc-^#Ndt$dhPw#Ra2EmkPG7mUnk2?{at2ZP zl+EYef&cKJ0-2;u$sm6Z`^W$VIjsFULwl+1VY*zVSYtu1lyHzVON|@eZRjC+6St^! zBymodvJ9sw#aVDugBAfz5FtPr!(>93Td>In>mf>-pV@Gku}XWT+Qns&7J?jj4a5Rr zVuS$@?y%fxJb!>me*J8Wvb2kP78OJZPwS2SLNqJ zSX}|t@rtsz$OuzAZ(?4RAqHF89biIf(C)yB|+Bl`1tFJYx3_jWC zvPMXYK#0h-jtysE{QSTgKWZF`4&aVh8}+RsOGAjbG}&x&9u)vL!Gx8=Myvc3-cWkV zh*}TKTGc<|ol2Uek$fAJ962OOh*KWriVhFSR)^|HTm=JgUj_1uo2_iEiIhGSP;3=f zJ`9qc2bw)(qiW@`S;p>N1k`3sb^CQ(GVojuGsnv0p8s_b%I5!@{aX0V`Mje*|()dn{BA&kR@z_fq z&s=m-E8~fb6^vki3-uB{+i1(7#8#H^u)(D?o?97&X5MyV(s-H-7utEv5wjbBV-_~? zbeoUTAzl@<`dHn^b1lTCg?buKjZ|dL*YVZ+tLWo=9ajQoxHA(YYZ!Mpv~C*D9z-VI z$8#W&*r%A&#={JcFxF(wTJu$~92NpKwT`uLi!)wbPzH3mv<}F~jA{DuDO7FnAxZbUnwMnyd>9r}8KfYjZvWOq#MQ zljz6COl-gxbQI6fSd~LbQwZ+27;}_!?nV z;#GWh@G|cXt0w7D$7^3VHRkapx!4Xxapa_$w1 z`L*{kZUHZDF-Z^sbXImZKHcI zV=z7msydaC#>&jC;93LD$F7C(y-1U^G2;O z)ol#~qV-JR3BnKY&wZwW#sT^`CbV-(ItmVnz&;9#TIbLQIHi3xelNGM6fbir+h_SL zIj5W@GYdN)UIRPW1tu+wAFK}vb1ElT6Mt286QdUc``+9D6w=B!_wm`mKM|7KgYXvd zq@W5}K43%}wvcqHh^kTPJ1_v|LfA_Bn(M=mwy;7IYc;`avb2j|R-iwiulnkmE1Os?gV!+xErhUwHPv8= zKwk(AnAG2*Dl3Jun5za`dM<@M>sVdxnaQ9b9rH8a5gRQ1VUJMknOM_>eOPps<6w1I%EYFQ19+l`Lqp zhk3dynngOWaIDD51ya}H3&|WXCzM;@mKxdD1@biHpm~e+P^LqU4fl{^*8Lz7T7n^i zA^K$0n9~MmyfA89(~#E&B$CefPettVUv!3aMAS|?6S|=gzg6g49xaq)a$I@pu!W2H+GD_F7{Yy%H#FsL zmPTm0!kjU8UPiIde!95MIiWNlA|f5b>s!Ota?p1D3Pz5uQ4%>+`!Ko_w;G_|{xWpt zu7V(9tOm#SIUJOk*pT-J^1j3O_g{{45FA_|c4Y;~W(H0<1ACgqx9ko(B zODo)e{#hM>#xMJ2l&`HJEOSBIX$K?}xvcevfK5-s{XkMv+DaR|FOp^6Rm++O28^UX zAF`I_0(}JoU2FMzKOZLjSjKa4s!c8&J@cdNs)95-F1L|C|mN6<@RKW;?;Y0;9(;&Q#rx=EziOy%s z9-vk6l?60B#A0|JmAwsusi_U&;dXC8)|K&qxUh&USGEv}GZ&0Li&%#^k|nH@r5c^5 zMU-d~XgtD@Nns6!Olm&#n5EG9zAKQqmr?4$P`))RSPly_hA+pDJwl+Cf!m?VWC9G$ zn}cE&%t5%|1TAQTu}7@>wI{%U&dmuHPi3Tn#@)nS1r)1};k!oP4-bLP=rFQ=jye1I zbn<1s#*iaCm%ViCd{z{LRC#&0Y8D99QT>&p$6y5TC5LclV^HXcRql6gSZfB2EXKY` zPT|SVKv@@IZV-m1!ge|WwEXFp!}<_3DFKqEK`29Em}*wO%v|Dvr{#fDzdsK1l)Cd-DYH~&?9-jHl7Z1OgVqol9c*NUq3vH1n1FB zqnpQKi8Lk|j6~iSQmf}++~EXDBMQbH^m;|SxE&rd+z3W54e90KL_Mjl75er zT}QytjLtBUgFz0jIP!4EP1Z)j*}e-BANHr=UuP?Svwy;u+e(2drb@5j-)nH8(4G( zI7YDk1V6qalir(3k9nVX?|F~Wik*ET?^?d9eOF!I4Xxim9l$;X+{kR|TQZ@D1|KC* z0HunqvlR#IisvUe{2pFuygtC|7GBHfjCj6RVP_4m9Z=k>C1WP)eQG~_`W=peQxaYk?@4+0=8`CbNs|EbEExx^gg5Wa#mpGk@6DaZ&!l^EC??~Onbf!O zD&uv4)t|*LM)1>~-fc)xVoFxQ%&IMQy=Fq&=0qN%aJXeibsXh58bBF{<3Cuz+xMYM-VsOsDt5gA0flTQ^&_FenFJy1Mpf))0m55I zg4fb&2*_|81 zcP>vrNltz_8N;w&Mxixvi28IaJsz3HAIgby7qJ6U_#HET4$F_7i^OI4e~*sGqEToy zSlydC2Yh=8mZI2mloA4sT_E`0MEuYHmlyy3pZ%TVze+~--ilzcO(SRn5pnG{K4lf5e>+OuQ~YUcIk zMRYmd%mRiPPu+=Qhf~;TJf7;^j;bEr;Ri+w-WjamEs>qx5=j!rpY~_T*J;4=dO6TO zbtguxN%b~Bu{f+9Py$`N-edg2EkB|c%tY!+JaxtEeUh9-!#Oh+Wo;htNh0X4N%e|X zVhN=iwwH`kw`PGScBVU?jmJ3|i$(YegNlWcuwD@Xib7NhqJ9fPY_MbjOhyOCC8c{D z^MM%JE>7dSUBR?@f+JyN_<6AW9nq0@%oHdapM3}4s!4N|#BU>yVZwslLo(?tu?Ru_ zmdKiP0DdBwj7=q{lhJ6D{m}$8kjO8{283{G!=IgpSAb9pm~&>FV#gVY*o3aaIdCXm z4`v30I>tMcI1X#tqc)RnsN$l1??qVXy zE@{A=1e&;1?*;ai>ivLq8S(>E@ZSwb`33izxEwoAi~ubGc@nURXo6~Y0tgU-N~-sp zP|Dfl6bPB>{Rl?;jfnXRuqjczVtA=xiB67zH{euqGB%bRn@v)aFRDJxQw>tRe+)H< z$)`6z9UJrJZ8-{!rnQM)GUV|L6_kQK3xf)YO~R)};>inOFQv?}m}(7yk3zcNh{e3y zu(Z73IJ$?vjzWWlCg30-*yYeNcbP8?%>yHVrHDu2&_S`AJYY3RpIncP)^Ry zKyDKZrs zhx_y}ZX6m|13kUoFOsPqtXgCSn}#aF=gfGuS^PjtR6a0s1`ZLZZOqPKb(tN<#4P7c z0kaAh%*KIO7UfC|#*~) zDB8rTck);4z%*2lz`Y6#*4R7vQ5Wcy+SL!RDenXRi;^F=SiQx6Qe9?sng8eyu%+_` zEQHsqd8d&JL=B32YMV&&(+RxKdbN1WHbR3=6Y$vm17|K;qbXx34q|!53`b(;2oVr; zPAz&#T=SrfCaSdvnb99spigsRCC_RU-MAMre`T@ zr?c=>l%gPJ!5j@ zpvsPO7&$uu-7_@Uuo!pvk17KNget-V%g5=`S7R5vXSD2aP2TAh@ARg3nt=o2JnwYL zdxn?^LYJU7muNYP_o>)Kf)*$t{<_!Yy1io#fw`nD6haZDEZ%y@ARdR~KU_LY{TR(E zbPH=nFPeBnTn`)){!(|o5<9130R?jj>q_cMbQ=C0Dhw8D-#m!Fx^F+yYg* z3ChkOvPhkNE9xQoV<6o`SUm&F?49}v3dY~C^P>~Mwzr^&U@X)TXmA!$A_OrVAuL1> z5w)t-iP%^aP6hy)ApOjF3N+vnz2vz_e^}_LKJ_<26{Awe?yV}iY7{0EK`1(Ov-5Vd z^QwrhGkVT0f`Kp$5x83=nB4=Krxb6LNW`Y%4cgHcc+F{{{+xsk)m-edEU7X9;o+ZK z%4^zSVt&>&0Y^*TU=0hFK28J+@5vZE8MQGHWg|Sxbd>fub^0ti6_Ky)#t}~Rp*wLk zVy#+-VeIMm5DsJKr#<`0`Z?&7G{IFtY{gA`r@tzU{ee=eYN=JFi>q3^t5m<#>0j56 zUy}uYs4DqGR8ptEMpL=Of6;Nco%qB>BEw+v1VOj-u07kvoR9`ho@Pf@0!ZYvm?|Y|HFt18VZ&pdEpWj1}FVXn#iDfwb zhqLK84i?_&?|7%b=bir8JN;MQ=})}Vf15h}DZ)HDz|)^ftcy`}b{|9$hk$;J+!?se z%$!eyF&d;Nu@s{C-jodm7;{JBV|c@{<0&k62(29BWBDF%q){1&cfc3Xb_g4f( zxPj%^wAb{yID_@~qC-McMX0IxRYxGglp;RmNN^bEI0;lCV@aJo0b7Ao2i~7~3-nYn zG&(`TCFGs11Sby2RV04NV9$sM7O-a;VeB;ypXM#3Hlofj&QrsW2=_>N`c_~X=OL}$ zJOn{#IZtyqFGlb;bQAb_9GYJ;Z=Tj{UMv|pVaetZY+i6r7}#evp|H%mm^bm;8!HX_ z6;tVd@uPJ5+SR2S20uJ$-utLj{&??JvAl5m`pugQH~1|Nw~9BG7H)m?QTgW5-nFIM z{%vEhpj%tQ+#PWj>G5VKN}*)XKg&rT=GYn8@u26OrC5U?pbV> zcZt^jm4Cner$Vy-8~L4;1^Iv+d1wbUf;YIF+Zcg+rOVhp^@eZt|J%l&O6)kxOmSi%LTci zY7J%n{m%G*{U3ZZW?Zx-8hqk;7Wo^;dmmV7Tg7_>PPG{Al@Vy;`6*KT<; zI^l%Z^|{W=^Y;d~Z6V4I;5-2wJlqN>4$o`2ta<}4m?^CJ2F7xidYOjzl=j`s|BAJE zKd4-P0~`-<*0C0EQ^F5aV6`SLco2KNJZ8>lOPe1-k= 0 ? "N" : "S") : (decimalDegrees >= 0 ? "E" : "W"); + decimalDegrees = Math.Abs(decimalDegrees); + int d =(int)(decimalDegrees); + double decimalpart = decimalDegrees - d; int m = (int)(decimalpart * 60); double s = (decimalpart - m / 60f) * 3600; - if (latitude) direction = decimalDegrees >= 0 ? "N" : "S"; - else direction = decimalDegrees >= 0 ? "E" : "W"; - dms = string.Format("{3} {0}\x00B0{1}\'{2:F1}\"", d, m, s, direction); + dms = string.Format("{3} {0}\x00B0 {1}\' {2:F1}\"", d, m, s, direction); return dms; } } diff --git a/source/WaypointManager/WaypointFlightRenderer.cs b/source/WaypointManager/WaypointFlightRenderer.cs index 72a21f0..c743e67 100644 --- a/source/WaypointManager/WaypointFlightRenderer.cs +++ b/source/WaypointManager/WaypointFlightRenderer.cs @@ -301,7 +301,7 @@ protected void DrawWaypoint(WaypointData wpd) } if(Config.hudCoordinates&&v.mainBody==wpd.celestialBody) { - ybase += 18; + ybase += 9; GUI.Label(new Rect((float)Screen.width / 2.0f - 188f, ybase, 240f, 38f), "Coordinates of " + label + ":", nameStyle); GUI.Label(new Rect((float)Screen.width / 2.0f + 60f, ybase, 120f, 38f), v.state != Vessel.State.DEAD ? string.Format("{0}\r\n{1}", Util.DecimalDegreesToDMS(wpd.waypoint.latitude,true), Util.DecimalDegreesToDMS(wpd.waypoint.longitude,false)) : "N/A", valueStyle); From 18b8a75962403a80b6649af39b2ba79d5eb38a4a Mon Sep 17 00:00:00 2001 From: Samuel Swanner Date: Mon, 9 Nov 2015 21:22:36 -0600 Subject: [PATCH 4/4] Adding configuration change that didn't get moved to github. --- GameData/WaypointManager/WaypointManager.dll | Bin 69632 -> 69632 bytes source/WaypointManager/WaypointManager.cs | 4 ++++ 2 files changed, 4 insertions(+) diff --git a/GameData/WaypointManager/WaypointManager.dll b/GameData/WaypointManager/WaypointManager.dll index 0cb5b9526bc5d1dd161f8bf78056a8382b008d3f..f391c6ab92004ee113e06cf13ba53bb05fbb84ba 100644 GIT binary patch delta 4271 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 U-bntd;)XudCGq4ksUNEQAMyO|P5=M^ delta 4249 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 B