Skip to content

Commit 4c2980c

Browse files
committed
Add tests, move file reading to only be on demand for memory mapped provider, update docs
1 parent 8964813 commit 4c2980c

8 files changed

Lines changed: 157 additions & 16 deletions

ElevationWebApi.sln

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ ProjectSection(SolutionItems) = preProject
99
Dockerfile = Dockerfile
1010
EndProjectSection
1111
EndProject
12+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElevationWebApiTests", "ElevationWebApiTests\ElevationWebApiTests.csproj", "{6210339D-AAD6-4BFC-9314-4C8A4FADFF67}"
13+
EndProject
1214
Global
1315
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1416
Debug|Any CPU = Debug|Any CPU
@@ -19,5 +21,9 @@ Global
1921
{DFEF0265-106B-4E81-A763-D2DE297E1AD3}.Debug|Any CPU.Build.0 = Debug|Any CPU
2022
{DFEF0265-106B-4E81-A763-D2DE297E1AD3}.Release|Any CPU.ActiveCfg = Release|Any CPU
2123
{DFEF0265-106B-4E81-A763-D2DE297E1AD3}.Release|Any CPU.Build.0 = Release|Any CPU
24+
{6210339D-AAD6-4BFC-9314-4C8A4FADFF67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
25+
{6210339D-AAD6-4BFC-9314-4C8A4FADFF67}.Debug|Any CPU.Build.0 = Debug|Any CPU
26+
{6210339D-AAD6-4BFC-9314-4C8A4FADFF67}.Release|Any CPU.ActiveCfg = Release|Any CPU
27+
{6210339D-AAD6-4BFC-9314-4C8A4FADFF67}.Release|Any CPU.Build.0 = Release|Any CPU
2228
EndGlobalSection
2329
EndGlobal
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
2+
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AArgumentSpecificationsFactory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F402b2077f38742cb9b381ab9e79e493229c00_003F4c_003Fc2df050d_003FArgumentSpecificationsFactory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
3+
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFixtureMethodRunner_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fd4ca1e5a92fc75932c8ddef25b934f48ed8015686ce1b79256171845d4291_003FFixtureMethodRunner_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
4+
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFuture_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F271b8d2d9d94e13a469269f46758864f64ad438c3174f74d4a591af6410e96b_003FFuture_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
5+
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AILogger_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fcc50fd1d894d4d86ab26ac07ac0af5ec10718_003F51_003F119e8ab7_003FILogger_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
6+
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ALoggerExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F81cc2099363ff84f1614136d278beb5bed411454404ec1ec16856d72b0d434e9_003FLoggerExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
7+
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMethodBaseInvoker_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fd6b757e154dd7f8c23e0e785431c97a76e4b9c6bdae38b978238421dbab55d_003FMethodBaseInvoker_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
8+
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APhysicalFileProvider_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F56b5e122ae4fd723ecae7aec5bf0c71d9181bebd75b4e96887f832e7d81c84_003FPhysicalFileProvider_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
9+
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARedundantArgumentMatcherException_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F402b2077f38742cb9b381ab9e79e493229c00_003F4f_003F17f5c53d_003FRedundantArgumentMatcherException_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
10+
<s:String x:Key="/Default/Environment/Highlighting/HighlightingSourceSnapshotLocation/@EntryValue">/Users/harelmazor/Library/Caches/JetBrains/Rider2024.3/resharper-host/temp/Rider/vAny/CoverageData/_ElevationWebApi.1119170593/Snapshot/snapshot.utdcvr</s:String>
11+
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=85cfbcaa_002D2803_002D4875_002D8b07_002D402dee8ff687/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" Name="Test1" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;
12+
&lt;Project Location="/Users/harelmazor/dev/ElevationWebAPI/ElevationWebApiTests" Presentation="&amp;lt;ElevationWebApiTests&amp;gt;" /&gt;
13+
&lt;/SessionState&gt;</s:String></wpf:ResourceDictionary>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<Project Sdk="MSTest.Sdk/3.6.1">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net9.0</TargetFramework>
5+
<LangVersion>latest</LangVersion>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
<UseVSTest>true</UseVSTest>
9+
<RootNamespace>ElevationWebAPITests</RootNamespace>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<ProjectReference Include="..\src\ElevationWebApi.csproj" />
14+
</ItemGroup>
15+
16+
<ItemGroup>
17+
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.6" />
18+
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.6" />
19+
<PackageReference Include="NSubstitute" Version="5.3.0" />
20+
</ItemGroup>
21+
22+
</Project>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
using System.Runtime.CompilerServices;
2+
using ElevationWebApi;
3+
using Microsoft.AspNetCore.Hosting;
4+
using Microsoft.Extensions.FileProviders;
5+
using Microsoft.Extensions.Logging;
6+
using NSubstitute;
7+
8+
namespace ElevationWebApiTests;
9+
10+
[TestClass]
11+
public class InMemoryElevationProviderTests
12+
{
13+
private InMemoryElevationProvider provider;
14+
15+
private string? GetSourceFileDirectory([CallerFilePath] string sourceFilePath = "")
16+
{
17+
return Path.GetDirectoryName(sourceFilePath);
18+
}
19+
20+
[TestInitialize]
21+
public void Initialize()
22+
{
23+
var hosting = Substitute.For<IWebHostEnvironment>();
24+
hosting.ContentRootFileProvider = new PhysicalFileProvider( GetSourceFileDirectory()!);
25+
var logger = Substitute.For<ILogger<InMemoryElevationProvider>>();
26+
provider = new InMemoryElevationProvider(hosting, logger);
27+
}
28+
29+
30+
[TestMethod]
31+
public void CheckElevationAt1_1_ShouldReturnFromFile()
32+
{
33+
provider.Initialize().Wait();
34+
var elevation = provider.GetElevation([[1.0, 1.0]]).Result;
35+
Assert.AreEqual(elevation[0], 1291, 0.1);
36+
}
37+
38+
[TestMethod]
39+
public void CheckElevationAt2_2_ShouldBeZero()
40+
{
41+
provider.Initialize().Wait();
42+
var elevation = provider.GetElevation([[2.0, 2.0]]).Result;
43+
Assert.AreEqual(elevation[0], 0);
44+
}
45+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using ElevationWebApi;
2+
using Microsoft.AspNetCore.Hosting;
3+
using Microsoft.Extensions.FileProviders;
4+
using NSubstitute;
5+
using Microsoft.Extensions.Logging;
6+
using System.Runtime.CompilerServices;
7+
8+
namespace ElevationWebApiTests;
9+
10+
[TestClass]
11+
public sealed class MemoryMapElevationProviderTests
12+
{
13+
private MemoryMapElevationProvider provider;
14+
15+
private string? GetSourceFileDirectory([CallerFilePath] string sourceFilePath = "")
16+
{
17+
return Path.GetDirectoryName(sourceFilePath);
18+
}
19+
20+
21+
[TestInitialize]
22+
public void Initialize()
23+
{
24+
var hosting = Substitute.For<IWebHostEnvironment>();
25+
hosting.ContentRootFileProvider = new PhysicalFileProvider( GetSourceFileDirectory()!);
26+
var logger = Substitute.For<ILogger<MemoryMapElevationProvider>>();
27+
provider = new MemoryMapElevationProvider(hosting, logger);
28+
}
29+
30+
31+
[TestMethod]
32+
public void CheckElevationAt1_1_ShouldReturnFromFile()
33+
{
34+
provider.Initialize().Wait();
35+
var elevation = provider.GetElevation([[1.0, 1.0]]).Result;
36+
Assert.AreEqual(elevation[0], 1291, 0.1);
37+
}
38+
39+
[TestMethod]
40+
public void CheckElevationAt2_2_ShouldBeZero()
41+
{
42+
provider.Initialize().Wait();
43+
var elevation = provider.GetElevation([[2.0, 2.0]]).Result;
44+
Assert.AreEqual(elevation[0], 0);
45+
}
46+
}
24.7 MB
Binary file not shown.

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ElevationWebApi
22

3-
A .net core elevation service that uses memory mapped hgt files to return elevation information for a given latitude and longitude array.
3+
A .net core elevation service that uses memory mapped hgt files or in memory files to return elevation information for a given latitude and longitude array.
44

55
To build the docker image:
66
```
@@ -10,12 +10,12 @@ docker run -p 12345:8080 -v /hgt-files-folder:/app/elevation-cache elevationweba
1010

1111
Now surf to `localhost:12345/swagger/` to get a simple UI to interact with the elevation service
1212

13-
This repository was only tested for elevation for Israel, which is a small country and the implementation be might be enough, it might not be the case for bigger countries.
13+
This service was tested on the entire world with memory mapped files and on a small country with in memory files.
1414

1515
This service supports both GET and POST methods for getting the elevation.
1616
You can place in the elevation folder zip or bz2 compressed files and the service will decompress them when booting up.
1717

18-
This docker file is also available on docker hub: `israelhikingmap/elevationwebapi`
18+
This docker image is also available on docker hub: `israelhikingmap/elevationwebapi`
1919

2020
You can choose between two types of elevation providers by specifying the `ELEVATION_PROVIDER` environment variable:
2121
`MMAP` - For memory mapped elevation provider

src/MemoryMapElevationProvider.cs

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Concurrent;
3+
using System.Collections.Generic;
34
using System.IO;
45
using System.IO.MemoryMappedFiles;
56
using System.Linq;
@@ -20,7 +21,8 @@ public class MemoryMapElevationProvider : IElevationProvider
2021
{
2122
private readonly ILogger<MemoryMapElevationProvider> _logger;
2223
private readonly IFileProvider _fileProvider;
23-
private readonly ConcurrentDictionary<Coordinate, Task<FileAndSamples>> _initializationTaskPerLatLng;
24+
private readonly ConcurrentDictionary<Coordinate, Task<FileAndSamples>> _mappedFilesCache;
25+
private readonly Dictionary<Coordinate, (string path, long length)> _initializationAvailableFiles;
2426

2527
/// <summary>
2628
/// Constructor
@@ -31,23 +33,24 @@ public MemoryMapElevationProvider(IWebHostEnvironment webHostEnvironment, ILogge
3133
{
3234
_logger = logger;
3335
_fileProvider = webHostEnvironment.ContentRootFileProvider;
34-
_initializationTaskPerLatLng = new();
36+
_mappedFilesCache = new();
37+
_initializationAvailableFiles = new();
3538
}
3639

3740
/// <summary>
3841
/// Initializes the provider by reading the elevation-cache directory,
3942
/// extracting the zip/bz2 files if needed and memory mapping them to a dictionary
4043
/// </summary>
41-
public async Task Initialize()
44+
public Task Initialize()
4245
{
4346
if (!ElevationHelper.ValidateFolder(_fileProvider, _logger))
4447
{
45-
return;
48+
return Task.CompletedTask;
4649
}
4750

4851
ElevationHelper.UnzipIfNeeded(_fileProvider, _logger);
4952
var hgtFiles = _fileProvider.GetDirectoryContents(ElevationHelper.ELEVATION_CACHE)
50-
.Where(f => f.PhysicalPath.EndsWith(".hgt")).ToArray();
53+
.Where(f => f.PhysicalPath != null && f.PhysicalPath.EndsWith(".hgt")).ToArray();
5154
_logger.LogInformation($"Found {hgtFiles.Length} hgt files.");
5255
foreach (var hgtFile in hgtFiles)
5356
{
@@ -57,11 +60,12 @@ public async Task Initialize()
5760
_logger.LogWarning($"Ignoring file: {hgtFile.Name}");
5861
continue;
5962
}
60-
_initializationTaskPerLatLng[key] = Task.Run(() => new FileAndSamples(MemoryMappedFile.CreateFromFile(hgtFile.PhysicalPath, FileMode.Open), ElevationHelper.SamplesFromLength(hgtFile.Length)));
61-
}
6263

63-
await Task.WhenAll(_initializationTaskPerLatLng.Values);
64+
_initializationAvailableFiles[key] = (hgtFile.PhysicalPath, hgtFile.Length);
65+
}
66+
6467
_logger.LogInformation("Initialization complete.");
68+
return Task.CompletedTask;
6569
}
6670

6771
/// <summary>
@@ -74,12 +78,17 @@ public Task<double[]> GetElevation(double[][] latLngs)
7478
var tasks = latLngs.Select(async latLng =>
7579
{
7680
var key = new Coordinate(Math.Floor(latLng[0]), Math.Floor(latLng[1]));
77-
if (_initializationTaskPerLatLng.ContainsKey(key) == false)
81+
if (_initializationAvailableFiles.TryGetValue(key, out (string path, long length) pathAndLength) == false)
7882
{
7983
return 0;
8084
}
8185

82-
var info = await _initializationTaskPerLatLng[key];
86+
if (!_mappedFilesCache.ContainsKey(key))
87+
{
88+
_logger.LogInformation($"Loading {pathAndLength.path} into memory mapped cache");
89+
_mappedFilesCache[key] = Task.Run(() => new FileAndSamples(MemoryMappedFile.CreateFromFile(pathAndLength.path, FileMode.Open), ElevationHelper.SamplesFromLength(pathAndLength.length)));
90+
}
91+
var info = await _mappedFilesCache[key];
8392

8493
var exactLocation = new Coordinate(Math.Abs(latLng[0] - key.X) * (info.Samples - 1),
8594
(1 - Math.Abs(latLng[1] - key.Y)) * (info.Samples - 1));
@@ -100,8 +109,8 @@ public Task<double[]> GetElevation(double[][] latLngs)
100109
/// Get the elevation of the two adjacent indices (i,j), (i, j+1)
101110
/// Then converts them to 3d points
102111
/// </summary>
103-
/// <param name="i">i Index in file</param>
104-
/// <param name="j">j index in file</param>
112+
/// <param name="i">I Index in file</param>
113+
/// <param name="j">J index in file</param>
105114
/// <param name="info">The info relevant to the file</param>
106115
/// <returns></returns>
107116
private (CoordinateZ, CoordinateZ) GetElevationForLocation(int i, int j, FileAndSamples info)
@@ -111,7 +120,7 @@ public Task<double[]> GetElevation(double[][] latLngs)
111120
var byteIndex = (i1 * info.Samples + j1) * 2;
112121
using var stream = info.File.CreateViewStream(byteIndex, 4);
113122
byte[] byteArray = new byte[4];
114-
stream.Read(byteArray);
123+
stream.ReadExactly(byteArray);
115124
return byteArray;
116125
});
117126
}

0 commit comments

Comments
 (0)