Let me start by apologizing for the complexity of this post. hopefully I am missing something simple but in order to find out I have to make a lengthy explanation.
I'm building a staff tracking app that allows users to draw polygons on a map. The polygon is used as a zone. If the device location is inside the zone it will set them to one status such as "In" and then set them to another status like "Out" when they leave. I'm using several nugets; SQLite, TK.CustomMap, PopupPlugin, to accomplish as much of this as possible in the Shared Project. The SQLite data model is based on the structure of the remote database for the solution which also has a desktop application, web app, and many other interfaces so that structure must be maintained.
The tables involved in this operation are Zones, ZonePoints and StatusClass. When the Zone is saved the Points of the TK.Polygon are saved into the points table. The Statuses assigned to the in and out status of the Zone are assigned to the zone as is the zone name.
The process works this way - first the user clicks on a button to add the zone. This creates a new zone, saves it to the database and gets its id from the table. For now, the ZoneSys (Primary Key in the remote db) is also set to the id although when the API is ready this will be Auto Incremented in the remote db. Here is that command and the objects it references. I did not include the database method definitions given that they work on the first time through, but if anyone thinks they will help solve this riddle please let me know -
public static List<Position> ActiveZonePositions = new List<Position>();
public static int ActiveZoneID;
public static Zone ActiveZone;
public static int PointCount;
public static bool NewZoneOpen = false;
public Command<EventArgs> OpenNewZone
{
get
{
return new Command<EventArgs>(async e =>
{
NewZoneOpen = true;
PointCount = 0;
ActiveZonePositions.Clear();
Zone ZoneToAdd = new Zone
{
Contactsys = MobileUser.ContactSys,
ZoneTypeSys = 1,
OrganizationSys = MobileUser.OrganizationSys,
ZoneName = null
};
ActiveZoneID = await AddZoneToDBAsync(ZoneToAdd);
ZoneToAdd.ID = ActiveZoneID;
ZoneToAdd.ZoneSys = ActiveZoneID;
await AddZoneToDBAsync(ZoneToAdd);
ActiveZone = await App.Database.GetZoneAsync(ActiveZoneID);
});
}
}
As the user click points in the map the polygon is drawn using those positions and those positions are also used to create points which are added to a static list. Here is the MapClicked_Command -
public Command<Position> MapClickedCommand
{
get
{
return new Command<Position>(async position =>
{
if (NewZoneOpen)
{
bool isPointInPolygon = IsPointInAnyPolygon(position);
if (isPointInPolygon)
{
var action = await Application.Current.MainPage.DisplayActionSheet(
"Region Collides with Another Region",
"Cancel",
null,
"Try Again",
"Close Zone Editor");
if (action == "Close Zone Editor")
{
await RemoveZoneAsync(ActiveZoneID);
}
if (action == "Try Again")
{
return;
}
}
else if (!isPointInPolygon)
{
ActiveZonePositions.Add(position);
}
if (ActiveZonePositions.Count == 2)
{
ZonePolyLine.LineCoordinates = ActiveZonePositions;
_lines.Remove(ZonePolyLine);
_lines.Add(ZonePolyLine);
}
else if (ActiveZonePositions.Count == 3)
{
ActiveZonePositions.Add(position);
_lines.Remove(ZonePolyLine);
TKPolygon poly = new TKPolygon
{
StrokeColor = System.Drawing.Color.CornflowerBlue,
StrokeWidth = 2f,
Color = System.Drawing.Color.CornflowerBlue
};
foreach (Position pos in ActiveZonePositions)
{
poly.Coordinates.Add(pos);
}
_polygons.Add(poly);
_currentPolygon.Clear();
_currentPolygon.Add(poly);
currentPolygonRendering = true;
}
else if (ActiveZonePositions.Count > 3)
{
ActiveZonePositions.Add(position);
TKPolygon poly = new TKPolygon
{
StrokeColor = System.Drawing.Color.CornflowerBlue,
StrokeWidth = 2f,
Color = System.Drawing.Color.CornflowerBlue
};
foreach (Position pos in ActiveZonePositions)
{
poly.Coordinates.Add(pos);
}
_polygons.Remove(_polygons.Last());
_polygons.Add(poly);
_currentPolygon.Clear();
_currentPolygon.Add(poly);
}
var pin = new TKCustomMapPin
{
Position = new TK.CustomMap.Position(position.Latitude, position.Longitude),
Title = string.Format("Pin {0}, {1}", position.Latitude, position.Longitude),
IsVisible = true,
IsDraggable = true,
ShowCallout = true
};
_pins.Add(pin);
await CreatePointAsync(position);
PointCount++;
}
else if (EditZoneOpen)
{
ActiveZonePositions.Add(position);
var poly = _polygons[0];
poly.Coordinates.Clear();
foreach (Position pos in ActiveZonePositions)
{
poly.Coordinates.Add(pos);
}
_polygons.Remove(_polygons.Last());
_polygons.Add(poly);
_currentPolygon.Clear();
_currentPolygon.Add(poly);
var pin = new TKCustomMapPin
{
Position = new TK.CustomMap.Position(position.Latitude, position.Longitude),
Title = string.Format("Pin {0}, {1}", position.Latitude, position.Longitude),
IsVisible = true,
IsDraggable = true,
ShowCallout = true
};
_pins.Add(pin);
await CreatePointAsync(position);
PointCount++;
}
});
}
}
Here is the CreatePointAsyncMethod -
public async Task CreatePointAsync(TK.CustomMap.Position position)
{
var zone = await RetrieveZoneAsync(ActiveZoneID);
Model.Point PointToAdd = new Model.Point
{
ZoneSys = zone.ZoneSys,
PointName = "",
Latitude = position.Latitude,
Longitude = position.Longitude,
PointOrder = PointCount + 1
};
ActiveZonePoints.Add(PointToAdd);
}
Here is the IsPointInAnyPolygon method that checks against the list of polygons to ensure the point clicked is not inside any of them as well as its supporting methods.
private bool IsPointInAnyPolygon(Position position)
{
bool inBounds = false;
for (var i = 0; i < ZonePolygons.Count(); i++)
foreach (ZonePolygon zpoly in ZonePolygons)
{
TKPolygon tkpoly = zpoly.Zpolygon;
inBounds = IsPointInPolygon(position, tkpoly.Coordinates);
if (inBounds)
{
ActiveZoneID = zpoly.ID;
return inBounds;
}
}
return inBounds;
}
private bool IsPointInPolygon(TK.CustomMap.Position position, List<Position> coords)
{
int intersectCount = 0;
for (int j = 0; j < coords.Count() - 1; j++)
{
if (j+1 >= coords.Count())
{
if (rayCastIntersect(position, coords[j], coords[0]))
{
intersectCount++;
}
} else if (rayCastIntersect(position, coords[j], coords[j + 1]))
{
intersectCount++;
}
}
return ((intersectCount % 2) == 1); // odd = inside, even = outside;
}
private bool rayCastIntersect(TK.CustomMap.Position position, TK.CustomMap.Position vertA, TK.CustomMap.Position vertB)
{
double aY = vertA.Latitude;
double bY = vertB.Latitude;
double aX = vertA.Longitude;
double bX = vertB.Longitude;
double pY = position.Latitude;
double pX = position.Longitude;
if ((aY > pY && bY > pY) | (aY < pY && bY < pY)
| (aX < pX && bX < pX))
{
return false; // a and b can't both be above or below pt.y, and a or
// b must be east of pt.x
}
double m = (aY - bY) / (aX - bX); // Rise over run
double bee = (-aX) * m + aY; // y = mx + b
double x = (pY - bee) / m; // algebra is neat!
return x > pX;
}
Upon clicking the save button a popup opens that allows the user to give the Zone a name, define the statuses it will be assigned and the points are added to the database. There is a ZoneSys column in the Points table which allows the points to be matched to their respective zones when retrieved. This is done withe the UpdateZone command
public Command<EventArgs> UpdateZone
{
get
{
return new Command<EventArgs>(async e =>
{
Zone zone = await App.Database.GetZoneAsync(ActiveZoneID);
zone.ZoneName = ZoneParameters.ZoneName;
zone.StatusSys = ZoneParameters.InStatus.StatusSys;
zone.OutOfZoneStatusSys = ZoneParameters.OutStatus.StatusSys;
await AddZoneToDBAsync(zone);
if (MapPage.SaveZoneInfoPopupPageOpen)
{
SavePointsOnExit();
MapPage.SaveZoneInfoPopupPageOpen = false;
}
});
}
}
The UpdateZone command calls the SavePointsOnExit method
private async void SavePointsOnExit()
{
ActiveZonePoints.OrderBy(o => o.PointOrder);
for (var i = 0; i < ActiveZonePoints.Count(); i++)
{
Model.Point PointToAdd = new Model.Point();
PointToAdd = ActiveZonePoints[i];
ActivePointID = await AddPointToDBAsync(PointToAdd);
PointToAdd.ID = ActivePointID;
PointToAdd.PointSys = ActivePointID;
await AddPointToDBAsync(PointToAdd);
}
try
{
Zone zone = await RetrieveZoneAsync(ActiveZoneID);
}
catch
{
await Application.Current.MainPage.DisplayActionSheet("no zone returned", "database error", "cancel");
}
try
{
Zone zone = await RetrieveZoneAsync(ActiveZoneID);
await CreateZonedPolygonAsync(zone);
}
catch
{
await Application.Current.MainPage.DisplayActionSheet("Could not create ZonePolygon", "object error", "cancel");
}
ActiveZonePoints.Clear();
ActiveZonePositions.Clear();
NewZoneOpen = false;
ClearPins();
PointCount = 0;
PopulatePoints();
}
In addition to saving the points to the db the SaveZonePointsOnExit method also creates the ZonePolygon and adds it to an observable collection using the CreateZonedPolygonAsync method -
private async Task<ZonePolygon> CreateZonedPolygonAsync(Zone zone)
{
int StatusSys = zone.StatusSys;
var status = await App.Database.GetStatusBySysAsync(StatusSys);
int OutStatusSys = zone.OutOfZoneStatusSys;
var outStatus = await App.Database.GetStatusBySysAsync(OutStatusSys);
var points = await App.Database.GetZonePointsAsync(zone.ZoneSys);
ZonePolygon zonePolygon = new ZonePolygon
{
ID = zone.ID
};
TKPolygon poly = new TKPolygon();
foreach (Model.Point point in points)
{
poly.Coordinates.Add(new Position(point.Latitude, point.Longitude));
}
poly.Color = Color.FromHex(status.ColorCode);
poly.StrokeColor = Color.Firebrick;
poly.StrokeWidth = 5f;
_polygons.Add(poly);
ZonePolygons.Add(zonePolygon);
return zonePolygon;
}
So far all of this works to a point. I have been successful in creating the first Polygon. I don't run into a problem until I attempt to create a second Zone. When I click on the AddZone button a second time that works fine but when I click on the map to begin creating the second zone a nullreference exception occurs.
Given that the first zone is created without issue I think the problem must be arising from something that occurs when the IsPointInAnyPolygon method no longer immediately returns false because the ZonePolygons list is no longer empty. So something about the retrieving of zones from the database to check against is the problem or possibly adding coordinates when the TKPolygon is created. I don't know what has a null reference. I would think that since I am creating the Zones directly from the database that all the objects would be saved properly and their previous references wouldn't matter. I'm very stuck on this.
TL;DR there is an issue with either the CreateZonedPolygonAsync Method or the IsPointInAnyPolygon method