ソースを参照

Added Placeholder capability to TextBox Editors
Created AddressEditor control
Refactored GoogleAPIKey Handling
Added .WithFilter() to AutoEntityUnionGenerator

frankvandenbos 3 ヶ月 前
コミット
77223d7c0d

+ 65 - 65
InABox.Avalonia/Geolocation.cs

@@ -1,66 +1,66 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace InABox.Avalonia
-{
-    public class GPSLocation
-    {
-        // public delegate void LocationEvent(GPSLocation sender);
-        // public event LocationEvent? OnLocationFound;
-
-        // public delegate void LocationError(GPSLocation sender, Exception error);
-        // public event LocationError? OnLocationError;
-
-        public TimeSpan ScanDelay { get; set; }
-
-        public double Latitude { get; private set; }
-        public double Longitude { get; private set; }
-        public String Address { get; private set; }
-
-        // private bool bLocating = false;
-        public DateTime TimeStamp { get; private set; }
-
-        public GPSLocation() : base()
-        {
-            TimeStamp = DateTime.MinValue;
-            ScanDelay = new TimeSpan(0, 0, 0);
-            Address = "Searching for GPS";
-        }
-
-        public bool RecentlyLocated
-        {
-            get
-            {
-                return (DateTime.Now.Subtract(TimeStamp).Ticks < ScanDelay.Ticks);
-            }
-        }
-
-        private double DistanceBetween(double sLatitude, double sLongitude, double eLatitude,
-                               double eLongitude)
-        {
-            var radiansOverDegrees = (Math.PI / 180.0);
-
-            var sLatitudeRadians = sLatitude * radiansOverDegrees;
-            var sLongitudeRadians = sLongitude * radiansOverDegrees;
-            var eLatitudeRadians = eLatitude * radiansOverDegrees;
-            var eLongitudeRadians = eLongitude * radiansOverDegrees;
-
-            var dLongitude = eLongitudeRadians - sLongitudeRadians;
-            var dLatitude = eLatitudeRadians - sLatitudeRadians;
-
-            var result1 = Math.Pow(Math.Sin(dLatitude / 2.0), 2.0) +
-                          Math.Cos(sLatitudeRadians) * Math.Cos(eLatitudeRadians) *
-                          Math.Pow(Math.Sin(dLongitude / 2.0), 2.0);
-
-            // Using 3956 as the number of miles around the earth
-            var result2 = 3956.0 * 2.0 *
-                          Math.Atan2(Math.Sqrt(result1), Math.Sqrt(1.0 - result1));
-
-            return result2;
-        }
+//using System;
+//using System.Collections.Generic;
+//using System.Linq;
+//using System.Text;
+//using System.Threading.Tasks;
+
+//namespace InABox.Avalonia
+//{
+    // public class GPSLocation
+    // {
+    //     // public delegate void LocationEvent(GPSLocation sender);
+    //     // public event LocationEvent? OnLocationFound;
+    //
+    //     // public delegate void LocationError(GPSLocation sender, Exception error);
+    //     // public event LocationError? OnLocationError;
+    //
+    //     public TimeSpan ScanDelay { get; set; }
+    //
+    //     public double Latitude { get; private set; }
+    //     public double Longitude { get; private set; }
+    //     public String Address { get; private set; }
+    //
+    //     // private bool bLocating = false;
+    //     public DateTime TimeStamp { get; private set; }
+    //
+    //     public GPSLocation() : base()
+    //     {
+    //         TimeStamp = DateTime.MinValue;
+    //         ScanDelay = new TimeSpan(0, 0, 0);
+    //         Address = "Searching for GPS";
+    //     }
+    //
+    //     public bool RecentlyLocated
+    //     {
+    //         get
+    //         {
+    //             return (DateTime.Now.Subtract(TimeStamp).Ticks < ScanDelay.Ticks);
+    //         }
+    //     }
+    //
+    //     private double DistanceBetween(double sLatitude, double sLongitude, double eLatitude,
+    //                            double eLongitude)
+    //     {
+    //         var radiansOverDegrees = (Math.PI / 180.0);
+    //
+    //         var sLatitudeRadians = sLatitude * radiansOverDegrees;
+    //         var sLongitudeRadians = sLongitude * radiansOverDegrees;
+    //         var eLatitudeRadians = eLatitude * radiansOverDegrees;
+    //         var eLongitudeRadians = eLongitude * radiansOverDegrees;
+    //
+    //         var dLongitude = eLongitudeRadians - sLongitudeRadians;
+    //         var dLatitude = eLatitudeRadians - sLatitudeRadians;
+    //
+    //         var result1 = Math.Pow(Math.Sin(dLatitude / 2.0), 2.0) +
+    //                       Math.Cos(sLatitudeRadians) * Math.Cos(eLatitudeRadians) *
+    //                       Math.Pow(Math.Sin(dLongitude / 2.0), 2.0);
+    //
+    //         // Using 3956 as the number of miles around the earth
+    //         var result2 = 3956.0 * 2.0 *
+    //                       Math.Atan2(Math.Sqrt(result1), Math.Sqrt(1.0 - result1));
+    //
+    //         return result2;
+    //     }
 
         // public void GetLocation(bool skiprecentlylocated = false)
         // {
@@ -199,5 +199,5 @@ namespace InABox.Avalonia
 
         // }
 
-    }
-}
+    //}
+//}

+ 3 - 0
InABox.Core/Classes/Address.cs

@@ -241,6 +241,9 @@ namespace InABox.Core
         [NullEditor]
         public Location Location => InitializeField(ref _location, nameof(Location));
         private Location? _location;
+        
+        [NullEditor]
+        public string Geofence { get; set; } = "";
 
         private class StateLookups : LookupGenerator<object>
         {

+ 2 - 0
InABox.Core/Classes/CompanyInformation.cs

@@ -27,11 +27,13 @@
 
         [EditorSequence("Postal Address", 1)]
         [Caption("")]
+        [AddressEditor(false)]
         public Address PostalAddress => InitializeField(ref _postalAddress, nameof(PostalAddress));
         private Address? _postalAddress;
 
         [EditorSequence("Delivery Address", 1)]
         [Caption("")]
+        [AddressEditor(true)]
         public Address DeliveryAddress => InitializeField(ref _deliveryAddress, nameof(DeliveryAddress));
         private Address? _deliveryAddress;
 

+ 9 - 1
InABox.Core/CoreUtils.cs

@@ -82,6 +82,12 @@ namespace InABox.Core
 
         #region Licensing
 
+        public static string GoogleAPIKey
+        {
+            get;
+            set;
+        } = "";
+        
         public static string SyncfusionLicense(SyncfusionVersion version = SyncfusionVersion.Unspecified)
         {
             var licenses = new Dictionary<SyncfusionVersion, string>
@@ -2865,6 +2871,8 @@ namespace InABox.Core
         public static bool Contains<T>(this T[] arr, T value) => Array.IndexOf(arr, value) != -1;
 
         #endregion
-
+        
+        
+        
     }
 }

+ 94 - 0
InABox.Core/GeoPoint.cs

@@ -0,0 +1,94 @@
+using System;
+using System.Collections.Generic;
+
+
+namespace InABox.Core
+{
+    public class GeoPoint
+    {
+        public GeoPoint(double latitude = 0.0, double longitude = 0.0)
+        {
+            Latitude = latitude;
+            Longitude = longitude;
+        }
+        public double Longitude { get; set; }
+        public double Latitude { get; set; }
+
+        public GeoPoint Copy() => new GeoPoint(Latitude, Longitude);
+
+        public static double DistanceBetween(GeoPoint from, GeoPoint to, UnitOfLength? unitOfLength = null)
+        {
+            var baseRad = Math.PI * from.Latitude / 180;
+            var targetRad = Math.PI * to.Latitude / 180;
+            var theta = from.Longitude - to.Longitude;
+            var thetaRad = Math.PI * theta / 180;
+
+            var dist =
+                Math.Sin(baseRad) * Math.Sin(targetRad) + Math.Cos(baseRad) *
+                Math.Cos(targetRad) * Math.Cos(thetaRad);
+            dist = Math.Acos(dist);
+
+            dist = dist * 180 / Math.PI;
+            dist = dist * 60 * 1.1515;
+
+            var unit = unitOfLength ?? UnitOfLength.Kilometers;
+            return unit.ConvertFromMiles(dist);
+        }
+        
+        public double DistanceTo(GeoPoint to, UnitOfLength? unitOfLength = null) 
+            => DistanceBetween(this, to, unitOfLength);
+        
+        private static double WGS84_RADIUS = 6370997.0;
+        private static double EarthCircumFence = 2* WGS84_RADIUS * Math.PI;
+
+        public GeoPoint Move(double mEastWest, double mNorthSouth){
+            double degreesPerMeterForLat = EarthCircumFence/360.0;
+            double shrinkFactor = Math.Cos((Latitude*Math.PI/180.0));
+            double degreesPerMeterForLon = degreesPerMeterForLat * shrinkFactor;
+            double newLat = Latitude + mNorthSouth * (1.0/degreesPerMeterForLat);
+            double newLng = Longitude + mEastWest * (1.0/degreesPerMeterForLon);
+            return new GeoPoint(newLat, newLng);
+        }
+        
+    }
+    
+    public class GeoFenceDefinition
+    {
+        
+        public List<GeoPoint> Coordinates { get;} = new List<GeoPoint>();
+        
+        public bool Contains(GeoPoint p)
+        {
+            if (Coordinates.Count < 2)
+                return false;
+            
+            double minX = Coordinates[0].Longitude;  
+            double maxX = Coordinates[0].Longitude;  
+            double minY = Coordinates[0].Latitude;  
+            double maxY = Coordinates[0].Latitude;  
+            for (int i = 1; i < Coordinates.Count; i++)  
+            {  
+                GeoPoint q = Coordinates[i];  
+                minX = Math.Min(q.Longitude, minX);  
+                maxX = Math.Max(q.Longitude, maxX);  
+                minY = Math.Min(q.Latitude, minY);  
+                maxY = Math.Max(q.Latitude, maxY);  
+            }  
+     
+            if (p.Longitude < minX || p.Longitude > maxX || p.Latitude < minY || p.Latitude > maxY)  
+            {  
+                return false;  
+            }  
+            bool inside = false;  
+            for (int i = 0, j = Coordinates.Count - 1; i < Coordinates.Count; j = i++)  
+            {  
+                if ((Coordinates[i].Latitude > p.Latitude) != (Coordinates[j].Latitude > p.Latitude) &&  
+                    p.Longitude < (Coordinates[j].Longitude - Coordinates[i].Longitude) * (p.Latitude - Coordinates[i].Latitude) / (Coordinates[j].Latitude - Coordinates[i].Latitude) + Coordinates[i].Longitude)  
+                {  
+                    inside = !inside;  
+                }  
+            }  
+            return inside;  
+        }
+    }
+}

+ 10 - 0
InABox.Core/GeoPointExtensions.cs

@@ -0,0 +1,10 @@
+using System;
+using System.Linq;
+
+namespace InABox.Core
+{
+    public static class GeoPointExtensions
+    {
+        
+    }
+}

+ 7 - 1
InABox.Core/Objects/AutoEntity/AutoEntityUnionGenerator.cs

@@ -58,7 +58,7 @@ namespace InABox.Core
     public class AutoEntityUnionTable<TInterface,TEntity> : IAutoEntityUnionTable
     {
         public Type Entity => typeof(TEntity);
-        public IFilter? Filter { get; }
+        public IFilter? Filter { get; private set; }
         
         
         private List<AutoEntityUnionConstant> _constants = new List<AutoEntityUnionConstant>();
@@ -72,6 +72,12 @@ namespace InABox.Core
             Filter = filter;
         }
         
+        public AutoEntityUnionTable<TInterface,TEntity> WithFilter(Filter<TEntity>? filter)
+        {
+            Filter = filter;
+            return this;
+        }
+        
         public AutoEntityUnionTable<TInterface, TEntity> AddConstant<TType>(Expression<Func<TInterface, object?>> mapping, TType constant)
         {
             _constants.Add(new AutoEntityUnionConstant(constant, new Column<TInterface>(mapping)));

+ 15 - 0
InABox.Core/Objects/Editors/AddressEditor.cs

@@ -0,0 +1,15 @@
+namespace InABox.Core
+{
+    public class AddressEditor : BaseEditor
+    {
+        public bool ShowMapButton { get; set; }
+
+        public AddressEditor(bool showMapButton)
+        {
+            ShowMapButton = showMapButton;
+        }
+
+        protected override BaseEditor DoClone() => new AddressEditor(ShowMapButton);
+        
+    }
+}

+ 270 - 0
inabox.wpf/DynamicGrid/Editors/AddressEditor/AddressEditor.cs

@@ -0,0 +1,270 @@
+using System;
+using InABox.Core;
+using Syncfusion.Windows.Shared;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+using Geocoding;
+using Geocoding.Google;
+using InABox.Wpf;
+using InABox.WPF;
+using InABox.Wpf.DynamicGrid;
+using Address = InABox.Core.Address;
+using Location = InABox.Core.Location;
+
+namespace InABox.DynamicGrid;
+
+public class AddressEditorControl : DynamicEnclosedEditorControl<Address, AddressEditor>
+{
+
+    private Grid Grid = null!; // Late-initialized in CreateEditor
+    
+    private TextBox StreetBox = null!; // Late-initialized in CreateEditor
+    private TextBox CityBox = null!; // Late-initialized in CreateEditor
+    private TextBox StateBox = null!; // Late-initialized in CreateEditor
+    private TextBox PostcodeBox = null!; // Late-initialized in CreateEditor
+    private Button MapButton = null!; // Late-initialized in CreateEditor
+    
+    public override int DesiredHeight()
+    {
+        return int.MaxValue;
+    }
+
+    public override int DesiredWidth()
+    {
+        return int.MaxValue;
+    }
+
+    public override void SetColor(Color color)
+    {
+        StreetBox.Background = new SolidColorBrush(color);
+        CityBox.Background = new SolidColorBrush(color);
+        StateBox.Background = new SolidColorBrush(color);
+        PostcodeBox.Background = new SolidColorBrush(color);
+    }
+
+    public override void SetFocus()
+    {
+        StreetBox.Focus();
+    }
+
+    public override void Configure()
+    {
+    }
+
+    protected override FrameworkElement CreateEditor()
+    {
+        Grid = new Grid
+        {
+            VerticalAlignment = VerticalAlignment.Stretch,
+            HorizontalAlignment = HorizontalAlignment.Stretch
+        };
+        Grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
+        Grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Auto) });
+        Grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Auto) });
+        Grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Auto) });
+
+        Grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
+        Grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Auto) });
+
+        StreetBox = CreateBox(0, 0, 4, 0, 0.0, 0.0, nameof(Address.Street), "Street");
+        CityBox = CreateBox(1, 0, 1, 0, 0.0, 5.0, nameof(Address.City),"City");
+        StateBox = CreateBox(1, 1, 1, 150, 5.0, 5.0, nameof(Address.State),"State");
+        PostcodeBox = CreateBox(1,2,1,80, 5.0, 5.0, nameof(Address.PostCode),"Postcode");
+
+        MapButton = new Button();
+        MapButton.SetValue(Grid.RowProperty,1);
+        MapButton.SetValue(Grid.ColumnProperty,3);
+        MapButton.Width = 25.0;
+        MapButton.Margin = new Thickness(5.0, 5.0, 0.0, 0.0);
+        MapButton.BorderThickness = new Thickness(0.75);
+        MapButton.Background = Brushes.Transparent;
+        MapButton.Content = new Image() { Source = InABox.Wpf.Resources.map.AsBitmapImage() };
+        MapButton.Click += (sender, args) => ShowMap();
+        MapButton.Visibility = !string.IsNullOrWhiteSpace(CoreUtils.GoogleAPIKey) && EditorDefinition.ShowMapButton 
+            ? Visibility.Visible 
+            : Visibility.Collapsed;
+        Grid.Children.Add(MapButton);
+        
+        return Grid;
+    }
+
+    private void ShowMap()
+    {
+        var values = new Dictionary<string,object?>(GetChildValues());
+
+        var address = new Address();
+        if (values.TryGetValue(nameof(Address.Street), out var street)) 
+            address.Street = street?.ToString() ?? "";
+        if (values.TryGetValue(nameof(Address.City), out var city)) 
+            address.City = city?.ToString() ?? "";
+        if (values.TryGetValue(nameof(Address.State), out var state)) 
+            address.State = state?.ToString() ?? "";
+        if (values.TryGetValue(nameof(Address.PostCode), out var postcode)) 
+            address.PostCode = postcode?.ToString() ?? "";
+        if (values.TryGetValue(nameof(Address.Geofence), out var geofence)) 
+            address.Geofence = geofence?.ToString() ?? "";
+        if (values.TryGetValue($"{nameof(Address.Location)}.{nameof(Address.Location.Latitude)}", out var latitude)) 
+            address.Location.Latitude = latitude != null ? (double)latitude : 0.0;
+        if (values.TryGetValue($"{nameof(Address.Location)}.{nameof(Address.Location.Longitude)}", out var longitude)) 
+            address.Location.Longitude = longitude != null ? (double)longitude : 0.0;
+        
+        var map = new GeofenceEditor(address, true);
+        map.ShowDialog();
+        _latitude = address.Location.Latitude;
+        _longitude = address.Location.Longitude;
+        _geofence = address.Geofence;
+        
+        CheckChanged($"{nameof(Address.Location)}.{nameof(Address.Location.Latitude)}");
+        CheckChanged($"{nameof(Address.Location)}.{nameof(Address.Location.Longitude)}");
+        CheckChanged(nameof(Address.Geofence));
+
+    }
+
+    private TextBox CreateBox(int row, int col, int colspan, double minWidth, double leftpad, double toppad, string property, string watermark)
+    {
+        var box = new TextBox();
+        box.SetValue(Grid.RowProperty,row);
+        box.SetValue(Grid.ColumnProperty,col);
+        box.SetValue(Grid.ColumnSpanProperty,Math.Max(1,colspan));
+        box.MinWidth = minWidth;
+        box.Margin = new Thickness(leftpad, toppad, 0, 0);
+        box.TextChanged += AddressChanged;
+        box.LostFocus += CheckChanged;
+        box.Tag = property;
+        TextBoxUtils.SetPlaceholder(box, watermark);
+        Grid.Children.Add(box);
+        return box;
+    }
+
+    private bool _dirty = false;
+    
+    private void CheckChanged(object sender, RoutedEventArgs e)
+    {
+        if (sender is not TextBox box)
+            return;
+        
+        CheckChanged((box.Tag as string)!);
+
+        if (_dirty)
+        {
+            _latitude = 0.0;
+            _longitude = 0.0;
+            _geofence = "";
+            CheckChanged($"{nameof(Address.Location)}.{nameof(Address.Location.Latitude)}");
+            CheckChanged($"{nameof(Address.Location)}.{nameof(Address.Location.Longitude)}");
+            CheckChanged($"{nameof(Address.Geofence)}");
+            _dirty = false;
+        }
+        MapButton.Background = string.IsNullOrWhiteSpace(_geofence) ? Brushes.Red : Brushes.LightGreen;
+
+        // CoreUtils.GoogleAPIKey = "AIzaSyAVReknl_sP2VLz5SUD8R-sZh1KDVCcWis";
+        // IGeocoder geocoder = new GoogleGeocoder(CoreUtils.GoogleAPIKey);
+        // var address = geocoder.GeocodeAsync($"{StreetBox.Text}, {CityBox.Text} {StateBox.Text} {PostcodeBox.Text} Australia").Result.FirstOrDefault();
+        // if (address != null)
+        // {
+        //     var latOffset = new GeoPoint(address.Coordinates.Latitude, address.Coordinates.Longitude).DistanceTo(new GeoPoint(_latitude, address.Coordinates.Longitude));
+        //     if (address.Coordinates.Latitude < _latitude)
+        //         latOffset *= -1.0;
+        //     
+        //     var longOffset = new GeoPoint(address.Coordinates.Latitude, address.Coordinates.Longitude).DistanceTo(new GeoPoint(address.Coordinates.Latitude, _longitude));
+        //     if (address.Coordinates.Longitude < _longitude)
+        //         longOffset *= -1.0;
+        //     
+        //     var fence = Serialization.Deserialize<GeoFence>(_geofence) ?? new GeoFence();
+        //     List<GeoPoint> newPoints = new List<GeoPoint>();
+        //     foreach (var coord in fence.Coordinates)
+        //         newPoints.Add(coord.Move(longOffset * 1000.0, latOffset * 1000.0));
+        //     fence.Coordinates.Clear();
+        //     fence.Coordinates.AddRange(newPoints);
+                
+        //         _latitude = address.Coordinates.Latitude;
+        //         _longitude = address.Coordinates.Longitude;
+        //         _geofence = Serialization.Serialize(fence);
+        //     }
+        //     else
+        //     {
+        //         _latitude = 0.0;
+        //         _longitude = 0.0;
+        //         _geofence = "";
+        //     }
+        //
+        // }
+        
+    }
+
+    private void AddressChanged(object sender, TextChangedEventArgs e)
+    {
+        _dirty = true;
+        // Set Latitude and Longitude to Zero
+    }
+
+    public override void SetEnabled(bool enabled)
+    {
+        StreetBox.IsEnabled = enabled;
+        CityBox.IsEnabled = enabled;
+        StateBox.IsEnabled = enabled;
+        PostcodeBox.IsEnabled = enabled;
+        base.SetEnabled(enabled);
+    }
+    
+    protected override object? GetChildValue(string property)
+    {
+        if (string.Equals(property,nameof(Address.Street))) 
+            return StreetBox.Text;
+        if (string.Equals(property,nameof(Address.City))) 
+            return CityBox.Text;
+        if (string.Equals(property,nameof(Address.State))) 
+            return StateBox.Text;
+        if (string.Equals(property,nameof(Address.PostCode))) 
+            return PostcodeBox.Text;
+        if (string.Equals(property, $"{nameof(Address.Location)}.{nameof(Address.Location.Latitude)}"))
+            return _latitude;
+        if (string.Equals(property, $"{nameof(Address.Location)}.{nameof(Address.Location.Longitude)}"))
+            return _longitude;
+        if (string.Equals(property,nameof(Address.Geofence))) 
+            return _geofence;
+        return null;
+    }
+
+    protected override IEnumerable<KeyValuePair<string, object?>> GetChildValues()
+    {
+        yield return new(nameof(Address.Street), StreetBox.Text);
+        yield return new(nameof(Address.City), CityBox.Text);
+        yield return new(nameof(Address.State), StateBox.Text);
+        yield return new(nameof(Address.PostCode), PostcodeBox.Text);
+        yield return new($"{nameof(Address.Location)}.{nameof(Address.Location.Latitude)}", _latitude);
+        yield return new($"{nameof(Address.Location)}.{nameof(Address.Location.Longitude)}", _longitude);
+        yield return new(nameof(Address.Geofence), _geofence);
+    }
+
+    private double _latitude = 0.0;
+    private double _longitude = 0.0;
+    private String _geofence = "";
+
+    protected override void SetChildValue(string property, object? value)
+    {
+        if (string.Equals(property,nameof(Address.Street))) 
+            StreetBox.Text = value?.ToString() ?? "";
+        if (string.Equals(property,nameof(Address.City))) 
+            CityBox.Text = value?.ToString() ?? "";
+        if (string.Equals(property,nameof(Address.State))) 
+            StateBox.Text = value?.ToString() ?? "";
+        if (string.Equals(property,nameof(Address.PostCode))) 
+            PostcodeBox.Text = value?.ToString() ?? "";
+        if (string.Equals(property, $"{nameof(Address.Location)}.{nameof(Address.Location.Latitude)}"))
+            _latitude = (double?)value ?? 0.0;
+        if (string.Equals(property, $"{nameof(Address.Location)}.{nameof(Address.Location.Longitude)}"))
+            _longitude = (double?)value ?? 0.0;
+        if (string.Equals(property, nameof(Address.Geofence)))
+        {
+            _geofence = value?.ToString() ?? "";
+            MapButton.Background = string.IsNullOrWhiteSpace(_geofence) ? Brushes.Red : Brushes.LightGreen;
+        }
+
+    }
+
+}

+ 184 - 0
inabox.wpf/DynamicGrid/Editors/AddressEditor/GeofenceEditor.xaml

@@ -0,0 +1,184 @@
+<Window x:Class="InABox.Wpf.DynamicGrid.GeofenceEditor"
+        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+        xmlns:local="clr-namespace:InABox.Wpf.DynamicGrid"
+        xmlns:syncfusion="http://schemas.syncfusion.com/wpf"
+        xmlns:wpf="clr-namespace:InABox.Wpf"
+        mc:Ignorable="d"
+        Title="AddressEditorMap" Height="800" Width="1200">
+    <Grid>
+        
+        <Grid.ColumnDefinitions>
+            <ColumnDefinition Width="*"/>
+        </Grid.ColumnDefinitions>
+        <Grid.RowDefinitions>
+            <RowDefinition Height="*"/>
+        </Grid.RowDefinitions>
+        <syncfusion:SfMap 
+            x:Name="Map" 
+            BorderBrush="Gray"
+            BorderThickness="0.75" 
+            ZoomLevel="19" 
+            MinZoom="19"
+            MaxZoom="19"
+            EnableZoom="False"
+            EnablePan="False"
+            Grid.Row="0"
+            Grid.Column="0"
+            MouseUp="Map_OnMouseUp">
+            
+            <syncfusion:SfMap.Layers>
+                <local:GoogleImageryLayer
+                    x:Name="ImageryLayer"
+                    Center="-31.95105, 115.85939"
+                    Radius="10"
+                    >
+                    <syncfusion:ImageryLayer.SubShapeFileLayers>
+                        <syncfusion:SubShapeFileLayer x:Name="subLayer">
+                            <syncfusion:SubShapeFileLayer.MapElements>
+
+                                <syncfusion:MapPolygon 
+                                    x:Name="Polygon" 
+                                    Stroke="Firebrick" 
+                                    StrokeThickness="0.75">
+                                    <syncfusion:MapPolygon.Fill>
+                                        <SolidColorBrush Color="Salmon" Opacity="0.5"/>     
+                                    </syncfusion:MapPolygon.Fill>
+                                </syncfusion:MapPolygon>
+                                
+                            </syncfusion:SubShapeFileLayer.MapElements>
+                        </syncfusion:SubShapeFileLayer>
+                    </syncfusion:ImageryLayer.SubShapeFileLayers>
+                    
+                </local:GoogleImageryLayer>
+            </syncfusion:SfMap.Layers>
+        </syncfusion:SfMap>
+        
+        <Border
+            x:Name="SearchBar"
+            Grid.Row="0"
+            Grid.Column="0"
+            HorizontalAlignment="Stretch"
+            VerticalAlignment="Top"
+            Margin="10,10,10,0"
+            Padding="5"
+            Background="WhiteSmoke"
+            CornerRadius="5" 
+            Height="40">
+            <DockPanel>
+                
+                <Button
+                    x:Name="SearchAddress"
+                    DockPanel.Dock="Right"
+                    Margin="5,0,0,0"
+                    Click="SearchAddress_Click"
+                    Padding="2">
+                    <Image Source="../../../Resources/go.png" />
+                </Button>
+                
+                <TextBox
+                    x:Name="PostCode"
+                    DockPanel.Dock="Right"
+                    Background="LightYellow"
+                    Margin="5,0,0,0"
+                    MinWidth="80"
+                    VerticalContentAlignment="Center"
+                    wpf:TextBoxUtils.Placeholder="Postcode" />
+                
+                <TextBox
+                    x:Name="State"
+                    DockPanel.Dock="Right"
+                    Background="LightYellow"
+                    Margin="5,0,0,0"
+                    MinWidth="120"
+                    VerticalContentAlignment="Center"
+                    wpf:TextBoxUtils.Placeholder="State" />
+                
+                <TextBox
+                    x:Name="City"
+                    DockPanel.Dock="Right"
+                    Background="LightYellow"
+                    Margin="5,0,0,0"
+                    MinWidth="200"
+                    VerticalContentAlignment="Center"
+                    wpf:TextBoxUtils.Placeholder="City" />
+                
+                <TextBox
+                    x:Name="Street"
+                    DockPanel.Dock="Left"
+                    Background="LightYellow"
+                    VerticalContentAlignment="Center"
+                    wpf:TextBoxUtils.Placeholder="Street" />
+            </DockPanel>
+        </Border>
+        
+        <DockPanel 
+            VerticalAlignment="Bottom" 
+            HorizontalAlignment="Stretch"
+            Margin="10,0,10,10" 
+            Grid.Row="0"
+            Grid.Column="0">
+            
+            <Button 
+                x:Name="ZoomOut" 
+                Height="50" 
+                Width="50"
+                Padding="5"
+                Margin="5,0,0,0" 
+                DockPanel.Dock="Right"
+                Click="ZoomOut_OnClick">
+                <Image Source="../../../Resources/zoomout.png" />
+            </Button>
+            
+            <Button 
+                x:Name="ZoomIn" 
+                Height="50" 
+                Width="50"
+                Padding="5"
+                Margin="5,0,0,0" 
+                DockPanel.Dock="Right"
+                Click="ZoomIn_OnClick">
+                <Image Source="../../../Resources/zoomin.png" />
+            </Button>
+            
+            
+            <Button 
+                x:Name="SetGeometry" 
+                Height="50" 
+                Width="50"
+                Padding="5"
+                Margin="0,0,5,0"
+                DockPanel.Dock="Left"
+                Click="SetGeometry_OnClick">
+                <Image Source="../../../Resources/line.png" />
+            </Button>
+            
+            <Button 
+                x:Name="SetRadius" 
+                Height="50" 
+                Width="50"
+                Padding="5"
+                Margin="0,0,5,0"
+                DockPanel.Dock="Left"
+                Click="SetRadius_OnClick">
+                <Image Source="../../../Resources/circle.png" />
+            </Button>
+            
+            <syncfusion:SfRangeSlider 
+                x:Name="RadiusSlider"
+                VerticalAlignment="Stretch"
+                Margin="10"
+                Minimum="0"
+                Maximum="1000"
+                TickFrequency="50"
+                MinorTickFrequency="5"
+                ThumbToolTipPrecision="0"
+                Visibility="Collapsed"
+                DockPanel.Dock="Left"/>
+            
+        </DockPanel>
+        
+    </Grid>
+</Window>

+ 201 - 0
inabox.wpf/DynamicGrid/Editors/AddressEditor/GeofenceEditor.xaml.cs

@@ -0,0 +1,201 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Input;
+using Geocoding;
+using Geocoding.Google;
+using InABox.Core;
+using Syncfusion.UI.Xaml.Maps;
+using Address = InABox.Core.Address;
+using Point = System.Windows.Point;
+
+namespace InABox.Wpf.DynamicGrid;
+
+public class GoogleImageryLayer : ImageryLayer
+{
+    protected override string GetUri(int X, int Y, int Scale)
+    {
+        var link = $"https://mt1.google.com/vt/lyrs=m@221097413,3&x={X}&y={Y}&z={Scale}";
+        return link;
+    }
+}
+
+public partial class GeofenceEditor : Window
+{
+    public Address Address { get; private set; }
+    
+    GeoFenceDefinition _definition = null;
+    
+    public GeofenceEditor(Address address, bool canEdit)
+    {
+        Address = address;
+        _definition = Serialization.Deserialize<GeoFenceDefinition>(address.Geofence) ?? new GeoFenceDefinition();
+        
+        InitializeComponent();
+        
+        SetGeometry.Visibility = canEdit ? Visibility.Visible : Visibility.Collapsed;
+        SetRadius.Visibility = canEdit ? Visibility.Visible : Visibility.Collapsed;
+        SearchBar.Visibility = canEdit ? Visibility.Visible : Visibility.Collapsed;
+        
+        Street.Text = address.Street;
+        City.Text = address.City;
+        State.Text = address.State;
+        PostCode.Text = Address.PostCode;
+
+        if (canEdit && string.IsNullOrWhiteSpace(Address.Geofence))
+            Task.Run(CheckAddress);
+        else
+            SetupMap();
+        
+    }
+
+    private async Task CheckAddress()
+    {
+        IGeocoder geocoder = new GoogleGeocoder(CoreUtils.GoogleAPIKey);
+        var matches = await geocoder.GeocodeAsync($"{Address.Street}, {Address.City} {Address.State} {Address.PostCode} Australia");
+        var match = matches.FirstOrDefault();
+
+        Address.Location.Longitude = match?.Coordinates.Longitude ?? 0.0;
+        Address.Location.Latitude = match?.Coordinates.Latitude ?? 0.0;
+        SquareFence(20.0);
+        Address.Geofence = Serialization.Serialize(_definition);
+
+        Dispatcher.BeginInvoke(SetupMap);
+
+    }
+
+    private void SquareFence(double side)
+    {
+        
+        _definition.Coordinates.Clear();
+        _definition.Coordinates.Add(new GeoPoint(Address.Location.Latitude, Address.Location.Longitude).Move(0-(side/2.0),0-(side/2.0)));
+        _definition.Coordinates.Add(new GeoPoint(Address.Location.Latitude, Address.Location.Longitude).Move(0-(side/2.0),side/2.0));
+        _definition.Coordinates.Add(new GeoPoint(Address.Location.Latitude, Address.Location.Longitude).Move(side/2.0,side/2.0));
+        _definition.Coordinates.Add(new GeoPoint(Address.Location.Latitude, Address.Location.Longitude).Move(side/2.0,0-(side/2.0)));
+        _definition.Coordinates.Add(new GeoPoint(Address.Location.Latitude, Address.Location.Longitude).Move(0-(side/2.0),0-(side/2.0)));
+    }
+
+    private void SetupMap()
+    {
+        ImageryLayer.Center = new Point(Address.Location.Latitude, Address.Location.Longitude);
+        ImageryLayer.Markers = new CoreObservableCollection<MapMarker>([new MapMarker()
+        {
+            Latitude = $"{Address.Location.Latitude:F15}",
+            Longitude = $"{Address.Location.Longitude:F15}"
+        }]);
+        
+        RadiusSlider.ValueChanged -= RadiusSliderChanged;
+        RadiusSlider.Value = 20.0;
+        if (_definition.Coordinates.Count < 4)
+            SquareFence(20.0);
+        RadiusSlider.ValueChanged += RadiusSliderChanged;
+        UpdateMap();
+    }
+    
+    private void RadiusSliderChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
+    {
+        RecalculateSquareFence();
+        UpdateMap();
+    }
+
+    private void RecalculateSquareFence()
+    {
+        SquareFence(RadiusSlider.Value);
+        Address.Geofence = Serialization.Serialize(_definition);
+    }
+
+    private void UpdateMap()
+    {
+       
+        Polygon.Points = new ObservableCollection<Point>(_definition.Coordinates.Select(p => new Point(p.Latitude, p.Longitude)));
+        
+    }
+    
+    private void ZoomIn_OnClick(object sender, RoutedEventArgs e)
+    {
+        Map.MaxZoom = Math.Min(20, Map.ZoomLevel + 1);
+        Map.ZoomLevel = Math.Min(20, Map.ZoomLevel + 1);
+        UpdateMap();
+    }
+
+    private void ZoomOut_OnClick(object sender, RoutedEventArgs e)
+    {
+        Map.MinZoom= Math.Max(5, Map.ZoomLevel - 1);
+        Map.ZoomLevel = Math.Max(5, Map.ZoomLevel - 1);
+        UpdateMap();
+    }
+    
+    private bool _polygon = false;
+    private bool _square = false;
+    
+    private void SetGeometry_OnClick(object sender, RoutedEventArgs e)
+    {
+        if (!_polygon)
+        {
+            _square = false;
+            SetRadius.Background = System.Windows.Media.Brushes.Silver;
+            RadiusSlider.Visibility = Visibility.Collapsed;
+            
+            _polygon = true;
+            _definition.Coordinates.Clear();
+            UpdateMap();
+            SetGeometry.Background = System.Windows.Media.Brushes.Yellow;
+        }
+        else
+        {
+            _polygon = false;
+            SetGeometry.Background = System.Windows.Media.Brushes.Silver;
+        }
+    }
+
+    private void Map_OnMouseUp(object sender, MouseButtonEventArgs e)
+    {
+        var point = Mouse.GetPosition(Map);
+        var latlon = ImageryLayer.GetLatLonFromPoint(point);
+        var geopoint = new GeoPoint(latlon.Y, latlon.X);
+        
+        if (!_polygon)
+            return;
+        
+        if (!_definition.Coordinates.Any())
+            _definition.Coordinates.Add(geopoint.Copy());
+        _definition.Coordinates.Insert(_definition.Coordinates.Count - 1, geopoint.Copy());
+        Address.Geofence = Serialization.Serialize(_definition);
+        UpdateMap();
+        e.Handled = true;
+    }
+
+
+    private void SetRadius_OnClick(object sender, RoutedEventArgs e)
+    {
+        if (!_square)
+        {
+            _polygon = false;
+            SetGeometry.Background = System.Windows.Media.Brushes.Silver;
+            
+            _square = true;
+
+            RadiusSlider.ValueChanged -= RadiusSliderChanged;
+            RadiusSlider.Value = 20.0;
+            SquareFence(20.0);
+            RadiusSlider.ValueChanged += RadiusSliderChanged;
+            RadiusSlider.Visibility = Visibility.Visible;
+            RecalculateSquareFence();
+            UpdateMap();
+            SetRadius.Background = System.Windows.Media.Brushes.Yellow;
+        }
+        else
+        {
+            _square = false;
+            RadiusSlider.Visibility = Visibility.Collapsed;
+            SetRadius.Background = System.Windows.Media.Brushes.Silver;
+        }
+    }
+
+    private void SearchAddress_Click(object sender, RoutedEventArgs e)
+    {
+        Task.Run(CheckAddress);
+    }
+}

+ 42 - 0
inabox.wpf/DynamicGrid/Editors/AddressEditor/MapMarker.cs

@@ -0,0 +1,42 @@
+using System;
+using System.Windows;
+using System.Windows.Media;
+
+namespace InABox.Wpf;
+
+public class MapMarker : DependencyObject
+{
+    public Guid ID { get; set; }
+    
+    private static readonly DependencyProperty LatitudeProperty =
+        DependencyProperty.Register(nameof(Latitude), typeof(string), typeof(MapMarker));
+    public string? Latitude
+    {
+        get => GetValue(LatitudeProperty) as string;
+        set => SetValue(LatitudeProperty, value);
+    }
+    
+    private static readonly DependencyProperty LongitudeProperty =
+        DependencyProperty.Register(nameof(Longitude), typeof(string), typeof(MapMarker));
+    
+    public string? Longitude
+    {
+        get => GetValue(LongitudeProperty) as string;
+        set => SetValue(LongitudeProperty, value);
+    }
+
+    private static readonly DependencyProperty LabelProperty =
+        DependencyProperty.Register(nameof(Label), typeof(string), typeof(MapMarker));
+    
+    public string? Label
+    {
+        get => GetValue(LabelProperty) as string;
+        set => SetValue(LabelProperty, value);
+    }
+    
+    public string Description { get; set; }
+    public DateTime Updated { get; set; }
+    public string UpdatedBy { get; set; }
+    public string DeviceID { get; set; }
+    public Brush Background { get; set; }
+}

+ 9 - 0
inabox.wpf/InABox.Wpf.csproj

@@ -133,6 +133,8 @@
         <PackageReference Include="ControlzEx" Version="6.0.0" />
         <PackageReference Include="Extended.Wpf.Toolkit" Version="4.6.0" />
         <PackageReference Include="FluentResults" Version="3.15.2" />
+        <PackageReference Include="Geocoding.Core" Version="4.0.1" />
+        <PackageReference Include="Geocoding.Google" Version="4.0.1" />
         <PackageReference Include="GhostScript.NetCore" Version="1.0.1" />
         <PackageReference Include="HtmlRenderer.Core" Version="1.5.0.6" />
         <PackageReference Include="HtmlRenderer.WPF" Version="1.5.0.6" />
@@ -457,6 +459,10 @@
       <Resource Include="Resources\upload.svg">
         <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
       </Resource>
+      <None Remove="Resources\map.png" />
+      <Resource Include="Resources\map.png">
+        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+      </Resource>
     </ItemGroup>
 
     <ItemGroup>
@@ -602,6 +608,9 @@
     </ItemGroup>
 
     <ItemGroup>
+      <Reference Include="Syncfusion.SfMaps.WPF">
+        <HintPath>..\..\..\Users\frank\.nuget\packages\syncfusion.sfmaps.wpf\25.2.6\lib\net6.0-windows7.0\Syncfusion.SfMaps.WPF.dll</HintPath>
+      </Reference>
       <Reference Include="Syncfusion.SfSpreadsheet.WPF">
         <HintPath>C:\Users\Fiona\.nuget\packages\syncfusion.sfspreadsheet.wpf\20.2.0.46\lib\net5.0-windows7.0\Syncfusion.SfSpreadsheet.WPF.dll</HintPath>
       </Reference>

+ 10 - 0
inabox.wpf/Resources.Designer.cs

@@ -648,6 +648,16 @@ namespace InABox.Wpf {
             }
         }
         
+        /// <summary>
+        ///   Looks up a localized resource of type System.Drawing.Bitmap.
+        /// </summary>
+        public static System.Drawing.Bitmap map {
+            get {
+                object obj = ResourceManager.GetObject("map", resourceCulture);
+                return ((System.Drawing.Bitmap)(obj));
+            }
+        }
+        
         /// <summary>
         ///   Looks up a localized resource of type System.Drawing.Bitmap.
         /// </summary>

+ 4 - 5
inabox.wpf/Resources.resx

@@ -424,15 +424,14 @@
   <data name="zoomout" type="System.Resources.ResXFileRef, System.Windows.Forms">
     <value>Resources\zoomout.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
   </data>
-
   <data name="download" type="System.Resources.ResXFileRef, System.Windows.Forms">
     <value>Resources\download.svg;System.IO.MemoryStream, mscorlib, Version=4.0.0.0, Culture=neutral,
       PublicKeyToken=b77a5c561934e089</value>
   </data>
-
   <data name="upload" type="System.Resources.ResXFileRef, System.Windows.Forms">
-    <value>Resources\upload.svg;System.IO.MemoryStream, mscorlib, Version=4.0.0.0, Culture=neutral,
-      PublicKeyToken=b77a5c561934e089</value>
+    <value>Resources\upload.svg;System.IO.MemoryStream, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </data>
+  <data name="map" type="System.Resources.ResXFileRef, System.Windows.Forms">
+    <value>Resources\map.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
   </data>
-
 </root>

BIN
inabox.wpf/Resources/map.png


+ 143 - 0
inabox.wpf/Utils/TextBoxUtils.cs

@@ -0,0 +1,143 @@
+using System.Linq;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Documents;
+using System.Windows.Media;
+
+namespace InABox.Wpf;
+
+public static class TextBoxUtils
+{
+    public static string GetPlaceholder(DependencyObject obj) =>
+        (string)obj.GetValue(PlaceholderProperty);
+
+    public static void SetPlaceholder(DependencyObject obj, string value) =>
+        obj.SetValue(PlaceholderProperty, value);
+
+    public static readonly DependencyProperty PlaceholderProperty =
+        DependencyProperty.RegisterAttached(
+            "Placeholder",
+            typeof(string),
+            typeof(TextBoxUtils),
+            new FrameworkPropertyMetadata(
+                defaultValue: null,
+                propertyChangedCallback: OnPlaceholderChanged)
+            );
+
+    private static void OnPlaceholderChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+    {
+        if (d is TextBox textBoxControl)
+        {
+            if (!textBoxControl.IsLoaded)
+            {
+                // Ensure that the events are not added multiple times
+                textBoxControl.Loaded -= TextBoxControl_Loaded;
+                textBoxControl.Loaded += TextBoxControl_Loaded;
+            }
+
+            textBoxControl.TextChanged -= TextBoxControl_TextChanged;
+            textBoxControl.TextChanged += TextBoxControl_TextChanged;
+
+            // If the adorner exists, invalidate it to draw the current text
+            if (GetOrCreateAdorner(textBoxControl, out PlaceholderAdorner adorner))
+                adorner.InvalidateVisual();
+        }
+    }
+
+    private static void TextBoxControl_Loaded(object sender, RoutedEventArgs e)
+    {
+        if (sender is TextBox textBoxControl)
+        {
+            textBoxControl.Loaded -= TextBoxControl_Loaded;
+            GetOrCreateAdorner(textBoxControl, out _);
+        }
+    }
+
+    private static void TextBoxControl_TextChanged(object sender, TextChangedEventArgs e)
+    {
+        if (sender is TextBox textBoxControl
+            && GetOrCreateAdorner(textBoxControl, out PlaceholderAdorner adorner))
+        {
+            // Control has text. Hide the adorner.
+            if (textBoxControl.Text.Length > 0)
+                adorner.Visibility = Visibility.Hidden;
+
+            // Control has no text. Show the adorner.
+            else
+                adorner.Visibility = Visibility.Visible;
+        }
+    }
+
+    private static bool GetOrCreateAdorner(TextBox textBoxControl, out PlaceholderAdorner adorner)
+    {
+        // Get the adorner layer
+        AdornerLayer layer = AdornerLayer.GetAdornerLayer(textBoxControl);
+
+        // If null, it doesn't exist or the control's template isn't loaded
+        if (layer == null)
+        {
+            adorner = null;
+            return false;
+        }
+
+        // Layer exists, try to find the adorner
+        adorner = layer.GetAdorners(textBoxControl)?.OfType<PlaceholderAdorner>().FirstOrDefault();
+
+        // Adorner never added to control, so add it
+        if (adorner == null)
+        {
+            adorner = new PlaceholderAdorner(textBoxControl);
+            layer.Add(adorner);
+        }
+
+        return true;
+    }
+
+    public class PlaceholderAdorner : Adorner
+    {
+        public PlaceholderAdorner(TextBox textBox) : base(textBox) { }
+
+        protected override void OnRender(DrawingContext drawingContext)
+        {
+            TextBox textBoxControl = (TextBox)AdornedElement;
+
+            string placeholderValue = TextBoxUtils.GetPlaceholder(textBoxControl);
+
+            if (string.IsNullOrEmpty(placeholderValue))
+                return;
+
+            // Create the formatted text object
+            FormattedText text = new FormattedText(
+                                        placeholderValue,
+                                        System.Globalization.CultureInfo.CurrentCulture,
+                                        textBoxControl.FlowDirection,
+                                        new Typeface(textBoxControl.FontFamily,
+                                                     textBoxControl.FontStyle,
+                                                     textBoxControl.FontWeight,
+                                                     textBoxControl.FontStretch),
+                                        textBoxControl.FontSize,
+                                        SystemColors.InactiveCaptionBrush,
+                                        VisualTreeHelper.GetDpi(textBoxControl).PixelsPerDip);
+
+            text.MaxTextWidth = System.Math.Max(textBoxControl.ActualWidth - textBoxControl.Padding.Left - textBoxControl.Padding.Right, 10);
+            text.MaxTextHeight = System.Math.Max(textBoxControl.ActualHeight, 10);
+
+            // Render based on padding of the control, to try and match where the textbox places text
+            Point renderingOffset = new Point(textBoxControl.Padding.Left, textBoxControl.Padding.Top);
+
+            // Template contains the content part; adjust sizes to try and align the text
+            if (textBoxControl.Template.FindName("PART_ContentHost", textBoxControl) is FrameworkElement part)
+            {
+                Point partPosition = part.TransformToAncestor(textBoxControl).Transform(new Point(0, 0));
+                renderingOffset.X += partPosition.X;
+                renderingOffset.Y += partPosition.Y;
+
+                text.MaxTextWidth = System.Math.Max(part.ActualWidth - renderingOffset.X, 10);
+                text.MaxTextHeight = System.Math.Max(part.ActualHeight, 10);
+            }
+
+            // Draw the text
+            drawingContext.DrawText(text, renderingOffset);
+        }
+    }
+}