Skip to content

Commit a79a7a4

Browse files
authored
Merge pull request #371 from Resgrid/develop
RE1-T115 POI Bug fixes
2 parents 821a194 + 339c417 commit a79a7a4

9 files changed

Lines changed: 224 additions & 36 deletions

File tree

Lines changed: 121 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
using System;
1+
using System;
22
using System.Collections.Generic;
33
using System.IO;
44
using System.Linq;
5+
using System.Net.Http;
56
using Resgrid.Model;
67
using Resgrid.Model.Providers;
78
using SharpKml.Dom;
@@ -15,38 +16,143 @@ public List<Coordinates> ImportFile(Stream input, bool isKmz)
1516
{
1617
var coordinates = new List<Coordinates>();
1718

19+
if (input == null)
20+
return coordinates;
21+
1822
try
1923
{
2024
KmlFile file;
2125
if (isKmz)
2226
{
2327
var kmz = KmzFile.Open(input);
24-
file = kmz.GetDefaultKmlFile();
28+
file = kmz?.GetDefaultKmlFile();
2529
}
2630
else
2731
file = KmlFile.Load(input);
2832

29-
30-
Kml kml = file.Root as Kml;
31-
if (kml != null)
33+
if (file?.Root is Kml kml)
3234
{
33-
foreach (var placemark in kml.Flatten().OfType<Placemark>())
34-
{
35-
var coords = new Coordinates();
36-
coords.Name = placemark.Name;
37-
coords.Latitude = placemark.CalculateBounds().Center.Latitude;
38-
coords.Longitude = placemark.CalculateBounds().Center.Longitude;
35+
ExtractCoordinates(kml, coordinates);
3936

40-
coordinates.Add(coords);
37+
// Resolve NetworkLinks to external KML/KMZ resources
38+
var networkLinks = kml.Flatten().OfType<NetworkLink>().ToList();
39+
foreach (var networkLink in networkLinks)
40+
{
41+
try
42+
{
43+
ResolveNetworkLink(networkLink, coordinates);
44+
}
45+
catch
46+
{
47+
// Skip failed network link resolution
48+
}
4149
}
4250
}
4351
}
44-
catch
52+
catch (Exception ex)
4553
{
46-
54+
System.Diagnostics.Debug.WriteLine($"KmlProvider.ImportFile error: {ex.Message}");
4755
}
48-
56+
4957
return coordinates;
5058
}
59+
60+
private static void ExtractCoordinates(Kml kml, List<Coordinates> coordinates)
61+
{
62+
foreach (var placemark in kml.Flatten().OfType<Placemark>())
63+
{
64+
var coords = new Coordinates();
65+
coords.Name = placemark.Name;
66+
67+
try
68+
{
69+
var bounds = placemark.CalculateBounds();
70+
if (bounds != null)
71+
{
72+
coords.Latitude = bounds.Center.Latitude;
73+
coords.Longitude = bounds.Center.Longitude;
74+
}
75+
}
76+
catch
77+
{
78+
// Skip placemarks that fail bounds calculation
79+
}
80+
81+
if (coords.Latitude.HasValue && coords.Longitude.HasValue)
82+
coordinates.Add(coords);
83+
}
84+
}
85+
86+
private static void ResolveNetworkLink(NetworkLink networkLink, List<Coordinates> coordinates)
87+
{
88+
if (networkLink?.Link?.Href == null)
89+
return;
90+
91+
var href = networkLink.Link.Href.OriginalString;
92+
if (string.IsNullOrWhiteSpace(href))
93+
return;
94+
95+
// Step 1: URI-based detection — check AbsolutePath for .kmz/.kml suffix
96+
bool? isKmzFromUri = null;
97+
if (Uri.TryCreate(href, UriKind.Absolute, out Uri uri))
98+
{
99+
var path = uri.AbsolutePath;
100+
if (path.EndsWith(".kmz", StringComparison.OrdinalIgnoreCase))
101+
isKmzFromUri = true;
102+
else if (path.EndsWith(".kml", StringComparison.OrdinalIgnoreCase))
103+
isKmzFromUri = false;
104+
}
105+
106+
using (var httpClient = new HttpClient { Timeout = System.TimeSpan.FromSeconds(30) })
107+
using (var response = httpClient.GetAsync(href).GetAwaiter().GetResult())
108+
{
109+
if (!response.IsSuccessStatusCode)
110+
return;
111+
112+
// Step 2: Content-Type detection — fallback when URI is inconclusive
113+
bool? isKmzFromContentType = null;
114+
var contentType = response.Content.Headers.ContentType?.MediaType;
115+
if (string.Equals(contentType, "application/vnd.google-earth.kmz", StringComparison.OrdinalIgnoreCase))
116+
isKmzFromContentType = true;
117+
else if (string.Equals(contentType, "application/vnd.google-earth.kml+xml", StringComparison.OrdinalIgnoreCase))
118+
isKmzFromContentType = false;
119+
120+
// Download into memory so we can peek bytes and re-read for parsing
121+
var contentBytes = response.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult();
122+
123+
// Step 3: ZIP magic-byte detection — final fallback
124+
bool? isKmzFromMagic = null;
125+
if (contentBytes.Length >= 4)
126+
{
127+
// PK\x03\x04 is the ZIP magic number
128+
isKmzFromMagic = contentBytes[0] == 0x50 && contentBytes[1] == 0x4B
129+
&& contentBytes[2] == 0x03 && contentBytes[3] == 0x04;
130+
}
131+
132+
// Precedence: URI suffix > Content-Type header > magic bytes; default to false
133+
bool isKmz = isKmzFromUri ?? isKmzFromContentType ?? isKmzFromMagic ?? false;
134+
135+
using (var ms = new MemoryStream(contentBytes))
136+
{
137+
KmlFile file;
138+
if (isKmz)
139+
{
140+
var kmz = KmzFile.Open(ms);
141+
file = kmz?.GetDefaultKmlFile();
142+
if (file == null)
143+
return;
144+
}
145+
else
146+
{
147+
file = KmlFile.Load(ms);
148+
}
149+
150+
if (file?.Root is Kml kml)
151+
{
152+
ExtractCoordinates(kml, coordinates);
153+
}
154+
}
155+
}
156+
}
51157
}
52158
}

Web/Resgrid.Web.Services/Controllers/v4/MappingController.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -469,7 +469,8 @@ public async Task<ActionResult<GetMapDataResult>> GetMapDataAndMarkers()
469469
PoiTypeId = poiType.PoiTypeId,
470470
Name = poiType.Name,
471471
Color = poiType.Color,
472-
ImagePath = poiType.Image,
472+
ImagePath = null,
473+
PoiImage = poiType.Image,
473474
Marker = poiType.Marker,
474475
IsDestination = poiType.IsDestination
475476
});
@@ -630,7 +631,8 @@ public static PoiTypeResultData ConvertPoiTypeData(PoiType poiType)
630631
PoiTypeId = poiType.PoiTypeId,
631632
Name = poiType.Name,
632633
Color = poiType.Color,
633-
ImagePath = poiType.Image,
634+
ImagePath = null,
635+
PoiImage = poiType.Image,
634636
Marker = poiType.Marker,
635637
IsDestination = poiType.IsDestination
636638
};
@@ -649,7 +651,8 @@ public static PoiResultData ConvertPoiData(Poi poi, PoiType poiType)
649651
Latitude = poi.Latitude,
650652
Longitude = poi.Longitude,
651653
Color = poiType.Color,
652-
ImagePath = poiType.Image,
654+
ImagePath = null,
655+
PoiImage = poiType.Image,
653656
Marker = poiType.Marker,
654657
IsDestination = poiType.IsDestination
655658
};
@@ -664,7 +667,8 @@ private static MapMakerInfoData ConvertPoiMapMarker(Poi poi, PoiType poiType)
664667
Latitude = poi.Latitude,
665668
Title = GetPoiTitle(poi, poiType),
666669
InfoWindowContent = GetPoiInfoWindowContent(poi, poiType),
667-
ImagePath = poiType.Image,
670+
ImagePath = null,
671+
PoiImage = poiType.Image,
668672
Marker = poiType.Marker,
669673
Color = poiType.Color,
670674
Type = 4,

Web/Resgrid.Web.Services/Models/v4/Mapping/GetMapDataResult.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,13 @@ public class MapMakerInfoData
5151
public string Note { get; set; }
5252
public string LayerId { get; set; }
5353
public string LayerName { get; set; }
54+
55+
/// <summary>
56+
/// The POI-specific custom icon image name (only set for POI markers, Type=4).
57+
/// New app versions should use this field instead of ImagePath for POI icons.
58+
/// ImagePath is set to null for POI markers so old apps fall back to their default icon.
59+
/// </summary>
60+
public string PoiImage { get; set; }
5461
}
5562

5663
public class PoiLayerData
@@ -61,5 +68,12 @@ public class PoiLayerData
6168
public string ImagePath { get; set; }
6269
public string Marker { get; set; }
6370
public bool IsDestination { get; set; }
71+
72+
/// <summary>
73+
/// The POI-specific custom icon image name.
74+
/// New app versions should use this field for POI type icons.
75+
/// ImagePath is set to null so old apps fall back to their default icon.
76+
/// </summary>
77+
public string PoiImage { get; set; }
6478
}
6579
}

Web/Resgrid.Web.Services/Models/v4/Mapping/PoiResultModels.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ public class PoiTypeResultData
1010
public string ImagePath { get; set; }
1111
public string Marker { get; set; }
1212
public bool IsDestination { get; set; }
13+
14+
/// <summary>
15+
/// The POI-specific custom icon image name.
16+
/// New app versions should use this field for POI type icons.
17+
/// ImagePath is set to null so old apps fall back to their default icon.
18+
/// </summary>
19+
public string PoiImage { get; set; }
1320
}
1421

1522
public class PoiResultData
@@ -26,6 +33,13 @@ public class PoiResultData
2633
public string ImagePath { get; set; }
2734
public string Marker { get; set; }
2835
public bool IsDestination { get; set; }
36+
37+
/// <summary>
38+
/// The POI-specific custom icon image name.
39+
/// New app versions should use this field for POI icons.
40+
/// ImagePath is set to null so old apps fall back to their default icon.
41+
/// </summary>
42+
public string PoiImage { get; set; }
2943
}
3044

3145
public class PoiTypesResult : StandardApiResponseV4Base

Web/Resgrid.Web.Services/Resgrid.Web.Services.xml

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Web/Resgrid.Web/Areas/User/Controllers/MappingController.cs

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,8 @@ public async Task<IActionResult> POIs()
289289
{
290290
var modal = new POIsView();
291291
modal.Types = await _mappingService.GetPOITypesForDepartmentAsync(DepartmentId);
292+
modal.Message = TempData["ImportPOIsMessage"] as string;
293+
modal.ErrorMessage = TempData["ImportPOIsError"] as string;
292294

293295
return View(modal);
294296
}
@@ -305,37 +307,41 @@ public async Task<IActionResult> AddPOIType()
305307
}
306308

307309
[HttpGet]
308-
public async Task<IActionResult> ImportPOIs()
310+
public async Task<IActionResult> ImportPOIs(int poiTypeId)
309311
{
310312
var model = new ImportPOIsView();
313+
model.TypeId = poiTypeId;
311314

312315
return View(model);
313316
}
314317

315318
[HttpPost]
319+
[ValidateAntiForgeryToken]
316320
public async Task<IActionResult> ImportPOIs(ImportPOIsView modal, IFormFile fileToUpload, CancellationToken cancellationToken)
317321
{
318-
if (fileToUpload != null && fileToUpload.Length > 0)
322+
if (fileToUpload == null || fileToUpload.Length == 0)
319323
{
320-
//Path.GetExtension(file.FileName).ToLower() == "kmz"
321-
322-
//var extenion = file.FileName.Substring(file.FileName.IndexOf(char.Parse(".")) + 1, file.FileName.Length - file.FileName.IndexOf(char.Parse(".")) - 1);
323-
var extenion = Path.GetExtension(fileToUpload.FileName).ToLower();
324-
325-
if (!String.IsNullOrWhiteSpace(extenion))
326-
extenion = extenion.ToLower();
324+
ModelState.AddModelError("fileToUpload", "Please select a file to upload.");
325+
}
326+
else
327+
{
328+
var extension = Path.GetExtension(fileToUpload.FileName).ToLower();
327329

328-
if (extenion != ".kml" && extenion != ".kmz")
329-
ModelState.AddModelError("fileToUpload", string.Format("File type ({0}) is not a KMZ or KML extension to import POIs.", extenion));
330+
if (extension != ".kml" && extension != ".kmz")
331+
ModelState.AddModelError("fileToUpload", string.Format("File type ({0}) is not a KMZ or KML extension to import POIs.", extension));
330332

331333
if (fileToUpload.Length > 10000000)
332334
ModelState.AddModelError("fileToUpload", "Document is too large, must be smaller then 10MB.");
333335
}
334336

337+
if (modal.TypeId <= 0)
338+
ModelState.AddModelError("TypeId", "Please select a POI type before importing.");
339+
335340
if (ModelState.IsValid)
336341
{
337342
var coordinates = _kmlProvider.ImportFile(fileToUpload.OpenReadStream(), Path.GetExtension(fileToUpload.FileName).ToLower() == ".kmz");
338343

344+
int importedCount = 0;
339345
foreach (var coordinate in coordinates)
340346
{
341347
var poi = new Poi();
@@ -348,9 +354,15 @@ public async Task<IActionResult> ImportPOIs(ImportPOIsView modal, IFormFile file
348354
poi.Longitude = coordinate.Longitude.Value;
349355

350356
await _mappingService.SavePOIAsync(poi, cancellationToken);
357+
importedCount++;
351358
}
352359
}
353360

361+
if (importedCount > 0)
362+
TempData["ImportPOIsMessage"] = string.Format("Successfully imported {0} POI(s).", importedCount);
363+
else
364+
TempData["ImportPOIsError"] = "No valid placemarks with coordinates could be found in the uploaded file.";
365+
354366
return RedirectToAction("POIs");
355367
}
356368

Web/Resgrid.Web/Areas/User/Models/Mapping/POIsView.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ namespace Resgrid.Web.Areas.User.Models.Mapping
66
public class POIsView
77
{
88
public List<PoiType> Types { get; set; }
9+
public string Message { get; set; }
10+
public string ErrorMessage { get; set; }
911

1012
public POIsView()
1113
{

0 commit comments

Comments
 (0)