using System; using ARLocation.UI; using UnityEngine; using UnityEngine.Events; namespace ARLocation { using Utils; [Serializable] public class OverrideAltitudeData { [Tooltip("If true, override the LocationData's altitude options.")] public bool OverrideAltitude; [Tooltip("The override altitude value.")] public double Altitude; [Tooltip("The override altitude mode.")] public AltitudeMode AltitudeMode = AltitudeMode.GroundRelative; } [Serializable] public class LocationPropertyData { [Serializable] public enum LocationPropertyType { Location, LocationData } [Tooltip("The type of location coordinate input used. Either 'Location' to directly input location " + "coordinates, or 'LocationData' to use a ScriptableObject.")] public LocationPropertyType LocationInputType = LocationPropertyType.Location; [Tooltip("A LocationData ScriptableObject storing the desired GPS coordinates to place the object.")] public LocationData LocationData; [Tooltip("Input the desired GPS coordinates here.")] public Location Location = new Location(); [Tooltip("Use this to override the LocationData's altitude options.")] public OverrideAltitudeData OverrideAltitudeData = new OverrideAltitudeData(); } /// /// Apply to a GameObject to place it at a specified geographic location. /// [AddComponentMenu("AR+GPS/Place At Location")] [HelpURL("https://http://docs.unity-ar-gps-location.com/guide/#placeatlocation")] [DisallowMultipleComponent] public class PlaceAtLocation : MonoBehaviour { [Serializable] public class ObjectUpdatedEvent : UnityEvent { } [Serializable] public class PlaceAtOptions { [Tooltip( "The smoothing factor for movement due to GPS location adjustments; if set to zero it is disabled."), Range(0, 1)] public float MovementSmoothing = 0.05f; [Tooltip( "The maximum number of times this object will be affected by GPS location updates. Zero means no limits are imposed.")] public int MaxNumberOfLocationUpdates = 4; [Tooltip("If true, use a moving average filter.")] public bool UseMovingAverage; [Tooltip( "If true, the object will be hidden until the object is placed at the geolocation. If will enable/disable the MeshRenderer or SkinnedMeshRenderer " + "when available, and enable/disable all child game objects.")] public bool HideObjectUntilItIsPlaced = true; [ConditionalPropertyAttribute("HideObjectUntilItIsPlaced")] [Tooltip("The number of location updates to wait until the object is shown after being initially " + "hidden from view. Only works when 'Hide Object Until It Is Placed' is set to true. If this "+ "is set to 0, 'Hide Object Until It Is Placed' will be disabled.")] public uint ShowObjectAfterThisManyUpdates = 1; } [Serializable] public class LocationSettingsData { public LocationPropertyData LocationInput = new LocationPropertyData(); public Location GetLocation() { Location location; if (LocationInput.LocationInputType == LocationPropertyData.LocationPropertyType.LocationData) { if (LocationInput.LocationData == null) { Debug.LogWarning("[AR+GPS][LocationSettingsData#GetLocation]: " + "Null LocationData; falling back to Location. When using `Location Input Type = Location Data` " + "make sure you associate a LocationData ScriptableObject to it."); location = LocationInput.Location.Clone(); } else { location = LocationInput.LocationData.Location.Clone(); if (LocationInput.OverrideAltitudeData.OverrideAltitude) { location.Altitude = LocationInput.OverrideAltitudeData.Altitude; location.AltitudeMode = LocationInput.OverrideAltitudeData.AltitudeMode; } } } else { location = LocationInput.Location.Clone(); } return location; } } [Serializable] public class StateData { public Location Location; public uint LocationUpdatedCount; public uint PositionUpdatedCount; public bool Paused; } public LocationSettingsData LocationOptions = new LocationSettingsData(); [Space(4.0f)] public PlaceAtOptions PlacementOptions = new PlaceAtOptions(); [Space(4.0f)] [Header("Debug")] [Tooltip("When debug mode is enabled, this component will print relevant messages to the console. Filter by 'PlateAtLocation' in the log output to see the messages. It will also " + "display the direction from the user to the object on the screen, as well as a line renderer from the camera to the object location. To customize how this line looks, add " + "a Line Renderer component to this game object.")] public bool DebugMode; [Space(4.0f)] [Header("Events")] [Space(4.0f)] [Tooltip( "Event called when the object's location is updated. The arguments are the current GameObject, the location, and the number of location updates received " + "by the object so far.")] public ObjectUpdatedEvent ObjectLocationUpdated = new ObjectUpdatedEvent(); [Tooltip( "Event called when the object's position is updated after a location update. " + "If the Movement Smoothing is larger than 0, this will fire at a later time than the Location Updated event. The arguments are the current GameObject, the location, and the number of position updates received " + "by the object so far.")] public ObjectUpdatedEvent ObjectPositionUpdated = new ObjectUpdatedEvent(); public Location Location { get => state.Location; set { if (!hasInitialized) { LocationOptions.LocationInput.LocationInputType = LocationPropertyData.LocationPropertyType.Location; LocationOptions.LocationInput.LocationData = null; LocationOptions.LocationInput.Location = value.Clone(); return; } if (groundHeight != null) { groundHeight.Settings.Altitude = (float) value.Altitude; } state.Location = value.Clone(); UpdatePosition(locationProvider.CurrentLocation.ToLocation(), true); } } public float SceneDistance { get { var cameraPos = mainCameraTransform.position; return MathUtils.HorizontalDistance(cameraPos, transform.position); } } public double RawGpsDistance => Location.HorizontalDistance(locationProvider.Provider.CurrentLocationRaw.ToLocation(), state.Location); public bool Paused { get => state.Paused; set => state.Paused = value; } public bool UseGroundHeight => state.Location.AltitudeMode == AltitudeMode.GroundRelative; private StateData state = new StateData(); private ARLocationProvider locationProvider; private Transform arLocationRoot; private SmoothMove smoothMove; private MovingAveragePosition movingAverageFilter; private GameObject debugPanel; private ARLocationManager arLocationManager; private Transform mainCameraTransform; private bool hasInitialized; private GroundHeight groundHeight; // Use this for initialization void Start() { locationProvider = ARLocationProvider.Instance; arLocationManager = ARLocationManager.Instance; arLocationRoot = arLocationManager.gameObject.transform; mainCameraTransform = arLocationManager.MainCamera.transform; if (locationProvider == null) { Debug.LogError("[AR+GPS][PlaceAtLocation]: LocationProvider GameObject or Component not found."); return; } Initialize(); hasInitialized = true; } public void Restart() { Logger.LogFromMethod("PlaceAtLocation", "Restart", $"({gameObject.name}) - Restarting!", DebugMode); RemoveLocationProviderListeners(); state = new StateData(); Initialize(); if (locationProvider.IsEnabled) { locationUpdatedHandler(locationProvider.CurrentLocation, locationProvider.LastLocation); } } void Initialize() { state.Location = LocationOptions.GetLocation(); Transform transform1; (transform1 = transform).SetParent(arLocationRoot.transform, false); transform1.localPosition = Vector3.zero; if (!hasInitialized) { if (PlacementOptions.HideObjectUntilItIsPlaced) { Misc.HideGameObject(gameObject); } if (PlacementOptions.MovementSmoothing > 0) { smoothMove = SmoothMove.AddSmoothMove(gameObject, PlacementOptions.MovementSmoothing); } if (UseGroundHeight) { groundHeight = gameObject.AddComponent(); groundHeight.Settings.Altitude = (float) state.Location.Altitude; } if (PlacementOptions.UseMovingAverage) { movingAverageFilter = new MovingAveragePosition { aMax = locationProvider.Provider.Options.AccuracyRadius > 0 ? locationProvider.Provider.Options.AccuracyRadius : 20 }; } if (DebugMode) { gameObject.AddComponent(); } if (PlacementOptions.ShowObjectAfterThisManyUpdates == 0) { PlacementOptions.HideObjectUntilItIsPlaced = false; } RegisterLocationProviderListeners(); } Logger.LogFromMethod("PlaceAtLocation", "Initialize", $"({gameObject.name}) initialized object with geo-location {state.Location}", DebugMode); } private void RegisterLocationProviderListeners() { // NOTE(daniel): `useRawIfEnabled` = true in this call so that when adding objects at runtime it uses the // best guess for the user location. locationProvider.OnLocationUpdatedEvent(locationUpdatedHandler, true); locationProvider.OnProviderRestartEvent(ProviderRestarted); } private void RemoveLocationProviderListeners() { locationProvider.OnLocationUpdatedDelegate -= locationUpdatedHandler; locationProvider.OnRestartDelegate -= ProviderRestarted; } private void ProviderRestarted() { Logger.LogFromMethod("PlaceAtLocation", "ProviderRestarted", $"({gameObject.name})", DebugMode); state.LocationUpdatedCount = 0; state.PositionUpdatedCount = 0; } private void locationUpdatedHandler(LocationReading currentLocation, LocationReading lastLocation) { UpdatePosition(currentLocation.ToLocation()); } public void UpdatePosition(Location deviceLocation, bool forceUpdate = false) { Logger.LogFromMethod("PlaceAtLocation", "UpdatePosition", $"({gameObject.name}): Received location update, location = {deviceLocation}", DebugMode); if (state.Paused) { Logger.LogFromMethod("PlaceAtLocation", "UpdatePosition", $"({gameObject.name}): Updates are paused; returning", DebugMode); return; } Vector3 targetPosition; var location = state.Location; var useSmoothMove = smoothMove != null; var isHeightRelative = location.AltitudeMode == AltitudeMode.DeviceRelative; // If we have reached the max number of location updates, do nothing if ((PlacementOptions.MaxNumberOfLocationUpdates > 0) && (state.LocationUpdatedCount >= PlacementOptions.MaxNumberOfLocationUpdates) && !forceUpdate) { return; } // Calculate the target position where the object will be placed next if (movingAverageFilter != null) { var position = Location.GetGameObjectPositionForLocation( arLocationRoot, mainCameraTransform, deviceLocation, location, isHeightRelative ); var accuracy = locationProvider.CurrentLocation.accuracy; movingAverageFilter.AddEntry(new DVector3(position), accuracy); targetPosition = movingAverageFilter.CalculateAveragePosition().toVector3(); } else { targetPosition = Location.GetGameObjectPositionForLocation( arLocationRoot, mainCameraTransform, deviceLocation, location, isHeightRelative ); } // If GroundHeight is enabled, don't change the objects position if (UseGroundHeight) { targetPosition.y = transform.position.y; if (useSmoothMove) { smoothMove.SmoothMoveMode = SmoothMove.Mode.Horizontal; } } Logger.LogFromMethod("PlaceAtLocation", "UpdatePosition", $"({gameObject.name}): Moving object to target position {targetPosition} ( {location}, {deviceLocation} )", DebugMode); if (useSmoothMove && state.PositionUpdatedCount > 0) { Logger.LogFromMethod("PlaceAtLocation", "UpdatePosition", $"({gameObject.name}): Using smooth move...", DebugMode); smoothMove.Move(targetPosition, PositionUpdated); } else { transform.position = targetPosition; PositionUpdated(); } state.LocationUpdatedCount++; ObjectLocationUpdated?.Invoke(gameObject, location, (int) state.LocationUpdatedCount); } private void PositionUpdated() { if (PlacementOptions.HideObjectUntilItIsPlaced)// && state.PositionUpdatedCount <= 0) { if (state.PositionUpdatedCount == (PlacementOptions.ShowObjectAfterThisManyUpdates - 1)) { Misc.ShowGameObject(gameObject); } } state.PositionUpdatedCount++; Logger.LogFromMethod("PlaceAtLocation", "PositionUpdated", $"({gameObject.name}): Object position updated! PositionUpdatedCount = {state.PositionUpdatedCount}, transform.position = {transform.position}", DebugMode); ObjectPositionUpdated?.Invoke(gameObject, state.Location, (int) state.PositionUpdatedCount); } public static GameObject CreatePlacedInstance(GameObject go, Location location, PlaceAtOptions options, bool useDebugMode = false) { var instance = Instantiate(go, ARLocationManager.Instance.gameObject.transform); AddPlaceAtComponent(instance, location, options, useDebugMode); return instance; } public static PlaceAtLocation AddPlaceAtComponent(GameObject go, Location location, PlaceAtOptions options, bool useDebugMode = false) { go.SetActive(false); var placeAt = go.AddComponent(); placeAt.PlacementOptions = options; placeAt.LocationOptions.LocationInput.LocationInputType = LocationPropertyData.LocationPropertyType.Location; placeAt.LocationOptions.LocationInput.Location = location.Clone(); placeAt.DebugMode = useDebugMode; go.SetActive(true); return placeAt; } public static GameObject CreatePlacedInstanceAtWorldPosition(GameObject go, Vector3 worldPosition, PlaceAtOptions options, out Location location, bool useDebugMode = false) { location = ARLocationManager.Instance.GetLocationForWorldPosition(worldPosition); return CreatePlacedInstance(go, location, options, useDebugMode); } private void OnDestroy() { RemoveLocationProviderListeners(); } } }