summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorivar <i@oiee.no>2025-10-20 00:26:34 +0200
committerivar <i@oiee.no>2025-10-20 00:26:34 +0200
commita1f0518d0cd123a791adde64f4f11bd8e44276c7 (patch)
tree675a7dff8262eea877ec800ff1efe9b92f5d7e7d
downloadwhat-a1f0518d0cd123a791adde64f4f11bd8e44276c7.tar.xz
what-a1f0518d0cd123a791adde64f4f11bd8e44276c7.zip
Initial commit
-rw-r--r--.gitignore120
-rw-r--r--api/.dockerignore25
-rw-r--r--api/.idea/.idea.WhatApi/.idea/.gitignore13
-rw-r--r--api/.idea/.idea.WhatApi/.idea/dictionaries/project.xml7
-rw-r--r--api/.idea/.idea.WhatApi/.idea/encodings.xml4
-rw-r--r--api/.idea/.idea.WhatApi/.idea/indexLayout.xml8
-rw-r--r--api/.idea/.idea.WhatApi/.idea/jsLibraryMappings.xml6
-rw-r--r--api/WhatApi.sln16
-rw-r--r--api/WhatApi/Constants.cs6
-rw-r--r--api/WhatApi/Database.cs19
-rw-r--r--api/WhatApi/Dockerfile23
-rw-r--r--api/WhatApi/Endpoints/BaseEndpoint.cs12
-rw-r--r--api/WhatApi/Endpoints/DownloadContentEndpoint.cs19
-rw-r--r--api/WhatApi/Endpoints/GetPlacesEndpoint.cs57
-rw-r--r--api/WhatApi/Endpoints/UploadContentEndpoint.cs49
-rw-r--r--api/WhatApi/Middleware/AuthenticationMiddleware.cs8
-rw-r--r--api/WhatApi/Middleware/AuthorizationMiddleware.cs8
-rw-r--r--api/WhatApi/Middleware/UserLastSeenMiddleware.cs8
-rw-r--r--api/WhatApi/Migrations/20251013213511_Initial.Designer.cs93
-rw-r--r--api/WhatApi/Migrations/20251013213511_Initial.cs75
-rw-r--r--api/WhatApi/Migrations/DatabaseModelSnapshot.cs90
-rw-r--r--api/WhatApi/Pages/Map.cshtml92
-rw-r--r--api/WhatApi/Pages/Map.cshtml.cs10
-rw-r--r--api/WhatApi/Pages/Upload.cshtml82
-rw-r--r--api/WhatApi/Pages/Upload.cshtml.cs10
-rw-r--r--api/WhatApi/Program.cs45
-rw-r--r--api/WhatApi/Properties/launchSettings.json14
-rw-r--r--api/WhatApi/Seed.cs24
-rw-r--r--api/WhatApi/Tables/Content.cs12
-rw-r--r--api/WhatApi/Tables/Place.cs11
-rw-r--r--api/WhatApi/Tables/Session.cs10
-rw-r--r--api/WhatApi/Tables/User.cs12
-rw-r--r--api/WhatApi/WhatApi.csproj34
-rw-r--r--api/WhatApi/appsettings.Development.json8
-rw-r--r--api/WhatApi/appsettings.json9
-rw-r--r--api/WhatApi/wwwroot/pin.pngbin0 -> 986 bytes
-rw-r--r--ios/H--appen-Info.plist5
-rw-r--r--ios/Hæ-appen.xcodeproj/project.pbxproj381
-rw-r--r--ios/Hæ-appen.xcodeproj/project.xcworkspace/contents.xcworkspacedata7
-rw-r--r--ios/Hæ-appen.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved15
-rw-r--r--ios/Hæ-appen/Assets.xcassets/AccentColor.colorset/Contents.json11
-rw-r--r--ios/Hæ-appen/Assets.xcassets/AppIcon.appiconset/Contents.json35
-rw-r--r--ios/Hæ-appen/Assets.xcassets/Contents.json6
-rw-r--r--ios/Hæ-appen/ClusterHit.swift18
-rw-r--r--ios/Hæ-appen/HaeappenApp.swift17
-rw-r--r--ios/Hæ-appen/LocationAuthorizer.swift20
-rw-r--r--ios/Hæ-appen/MapContentView.swift45
-rw-r--r--ios/Hæ-appen/PostAnnotationView.swift9
-rw-r--r--ios/Hæ-appen/TabBarView.swift22
49 files changed, 1630 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f2e5fe9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,120 @@
+# Xcode
+#
+# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
+
+## User settings
+xcuserdata/
+
+## Obj-C/Swift specific
+*.hmap
+
+## App packaging
+*.ipa
+*.dSYM.zip
+*.dSYM
+
+## Playgrounds
+timeline.xctimeline
+playground.xcworkspace
+
+# Swift Package Manager
+#
+# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
+# Packages/
+# Package.pins
+# Package.resolved
+# *.xcodeproj
+#
+# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
+# hence it is not needed unless you have added a package configuration file to your project
+# .swiftpm
+
+.build/
+
+# CocoaPods
+#
+# We recommend against adding the Pods directory to your .gitignore. However
+# you should judge for yourself, the pros and cons are mentioned at:
+# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
+#
+# Pods/
+#
+# Add this line if you want to avoid checking in source code from the Xcode workspace
+# *.xcworkspace
+
+# Carthage
+#
+# Add this line if you want to avoid checking in source code from Carthage dependencies.
+# Carthage/Checkouts
+
+Carthage/Build/
+
+# fastlane
+#
+# It is recommended to not store the screenshots in the git repo.
+# Instead, use fastlane to re-generate the screenshots whenever they are needed.
+# For more information about the recommended setup visit:
+# https://docs.fastlane.tools/best-practices/source-control/#source-control
+
+fastlane/report.xml
+fastlane/Preview.html
+fastlane/screenshots/**/*.png
+fastlane/test_output
+i## A streamlined .gitignore for modern .NET projects
+## including temporary files, build results, and
+## files generated by popular .NET tools. If you are
+## developing with Visual Studio, the VS .gitignore
+## https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
+## has more thorough IDE-specific entries.
+##
+## Get latest from https://github.com/github/gitignore/blob/main/Dotnet.gitignore
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+
+# Others
+~$*
+*~
+CodeCoverage/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+Bogus.Premium.LicenseKey
+**/files/*
+**/.DS_Store
+**/*.user
diff --git a/api/.dockerignore b/api/.dockerignore
new file mode 100644
index 0000000..cd967fc
--- /dev/null
+++ b/api/.dockerignore
@@ -0,0 +1,25 @@
+**/.dockerignore
+**/.env
+**/.git
+**/.gitignore
+**/.project
+**/.settings
+**/.toolstarget
+**/.vs
+**/.vscode
+**/.idea
+**/*.*proj.user
+**/*.dbmdl
+**/*.jfm
+**/azds.yaml
+**/bin
+**/charts
+**/docker-compose*
+**/Dockerfile*
+**/node_modules
+**/npm-debug.log
+**/obj
+**/secrets.dev.yaml
+**/values.dev.yaml
+LICENSE
+README.md \ No newline at end of file
diff --git a/api/.idea/.idea.WhatApi/.idea/.gitignore b/api/.idea/.idea.WhatApi/.idea/.gitignore
new file mode 100644
index 0000000..a16c839
--- /dev/null
+++ b/api/.idea/.idea.WhatApi/.idea/.gitignore
@@ -0,0 +1,13 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Rider ignored files
+/modules.xml
+/projectSettingsUpdater.xml
+/contentModel.xml
+/.idea.WhatApi.iml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/api/.idea/.idea.WhatApi/.idea/dictionaries/project.xml b/api/.idea/.idea.WhatApi/.idea/dictionaries/project.xml
new file mode 100644
index 0000000..bc9adff
--- /dev/null
+++ b/api/.idea/.idea.WhatApi/.idea/dictionaries/project.xml
@@ -0,0 +1,7 @@
+<component name="ProjectDictionaryState">
+ <dictionary name="project">
+ <words>
+ <w>postgis</w>
+ </words>
+ </dictionary>
+</component> \ No newline at end of file
diff --git a/api/.idea/.idea.WhatApi/.idea/encodings.xml b/api/.idea/.idea.WhatApi/.idea/encodings.xml
new file mode 100644
index 0000000..df87cf9
--- /dev/null
+++ b/api/.idea/.idea.WhatApi/.idea/encodings.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
+</project> \ No newline at end of file
diff --git a/api/.idea/.idea.WhatApi/.idea/indexLayout.xml b/api/.idea/.idea.WhatApi/.idea/indexLayout.xml
new file mode 100644
index 0000000..7b08163
--- /dev/null
+++ b/api/.idea/.idea.WhatApi/.idea/indexLayout.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="UserContentModel">
+ <attachedFolders />
+ <explicitIncludes />
+ <explicitExcludes />
+ </component>
+</project> \ No newline at end of file
diff --git a/api/.idea/.idea.WhatApi/.idea/jsLibraryMappings.xml b/api/.idea/.idea.WhatApi/.idea/jsLibraryMappings.xml
new file mode 100644
index 0000000..5bb18c4
--- /dev/null
+++ b/api/.idea/.idea.WhatApi/.idea/jsLibraryMappings.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="JavaScriptLibraryMappings">
+ <file url="PROJECT" libraries="{maplibre-gl}" />
+ </component>
+</project> \ No newline at end of file
diff --git a/api/WhatApi.sln b/api/WhatApi.sln
new file mode 100644
index 0000000..b8ebda5
--- /dev/null
+++ b/api/WhatApi.sln
@@ -0,0 +1,16 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WhatApi", "WhatApi\WhatApi.csproj", "{09327C36-26ED-4F0F-8AB3-0A2D8E1EABE3}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {09327C36-26ED-4F0F-8AB3-0A2D8E1EABE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {09327C36-26ED-4F0F-8AB3-0A2D8E1EABE3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {09327C36-26ED-4F0F-8AB3-0A2D8E1EABE3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {09327C36-26ED-4F0F-8AB3-0A2D8E1EABE3}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+EndGlobal
diff --git a/api/WhatApi/Constants.cs b/api/WhatApi/Constants.cs
new file mode 100644
index 0000000..01385c1
--- /dev/null
+++ b/api/WhatApi/Constants.cs
@@ -0,0 +1,6 @@
+namespace WhatApi;
+
+public static class Constants
+{
+ public const int Wgs84SpatialReferenceId = 4326;
+} \ No newline at end of file
diff --git a/api/WhatApi/Database.cs b/api/WhatApi/Database.cs
new file mode 100644
index 0000000..39de79a
--- /dev/null
+++ b/api/WhatApi/Database.cs
@@ -0,0 +1,19 @@
+using Microsoft.EntityFrameworkCore;
+
+namespace WhatApi;
+
+public class Database(DbContextOptions<Database> options) : DbContext(options)
+{
+ public DbSet<Tables.Content> Content { get; set; }
+ public DbSet<Tables.Place> Places { get; set; }
+ protected override void OnModelCreating(ModelBuilder b) {
+ b.HasPostgresExtension("postgis");
+ b.Entity<Tables.Place>(e => {
+ e.Property(x => x.Location).HasColumnType($"geometry(point,{Constants.Wgs84SpatialReferenceId})");
+ e.HasIndex(x => x.Location).HasMethod("gist");
+ e.ToTable("Place");
+ });
+ b.Entity<Tables.Content>();
+ base.OnModelCreating(b);
+ }
+} \ No newline at end of file
diff --git a/api/WhatApi/Dockerfile b/api/WhatApi/Dockerfile
new file mode 100644
index 0000000..d559b98
--- /dev/null
+++ b/api/WhatApi/Dockerfile
@@ -0,0 +1,23 @@
+FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
+USER $APP_UID
+WORKDIR /app
+EXPOSE 8080
+EXPOSE 8081
+
+FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
+ARG BUILD_CONFIGURATION=Release
+WORKDIR /src
+COPY ["WhatApi/WhatApi.csproj", "WhatApi/"]
+RUN dotnet restore "WhatApi/WhatApi.csproj"
+COPY . .
+WORKDIR "/src/WhatApi"
+RUN dotnet build "./WhatApi.csproj" -c $BUILD_CONFIGURATION -o /app/build
+
+FROM build AS publish
+ARG BUILD_CONFIGURATION=Release
+RUN dotnet publish "./WhatApi.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
+
+FROM base AS final
+WORKDIR /app
+COPY --from=publish /app/publish .
+ENTRYPOINT ["dotnet", "WhatApi.dll"]
diff --git a/api/WhatApi/Endpoints/BaseEndpoint.cs b/api/WhatApi/Endpoints/BaseEndpoint.cs
new file mode 100644
index 0000000..4d8f9ad
--- /dev/null
+++ b/api/WhatApi/Endpoints/BaseEndpoint.cs
@@ -0,0 +1,12 @@
+using System.Net;
+using Microsoft.AspNetCore.Mvc;
+
+namespace WhatApi.Endpoints;
+
+[ApiController]
+public class BaseEndpoint : ControllerBase
+{
+ protected IPAddress GetIp() => Request.Headers.TryGetValue("X-Forwarded-For", out var ip)
+ ? IPAddress.Parse(ip.ToString())
+ : HttpContext.Connection.RemoteIpAddress!;
+} \ No newline at end of file
diff --git a/api/WhatApi/Endpoints/DownloadContentEndpoint.cs b/api/WhatApi/Endpoints/DownloadContentEndpoint.cs
new file mode 100644
index 0000000..dbe6bff
--- /dev/null
+++ b/api/WhatApi/Endpoints/DownloadContentEndpoint.cs
@@ -0,0 +1,19 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace WhatApi.Endpoints;
+
+public class DownloadContentEndpoint : BaseEndpoint
+{
+ [HttpGet("~/{id:guid}")]
+ public async Task<ActionResult> HandleAsync(Guid id, CancellationToken ct = default) {
+ try {
+ var path = Path.Combine(Directory.GetCurrentDirectory(), "files", id.ToString());
+ await using var file = new FileStream(path, FileMode.Open);
+ if (!file.CanRead) return NotFound();
+ return File(file, "application/octet-stream", id.ToString());
+ } catch (Exception e) {
+ if (e is not FileNotFoundException) Console.WriteLine(e);
+ return NotFound();
+ }
+ }
+} \ No newline at end of file
diff --git a/api/WhatApi/Endpoints/GetPlacesEndpoint.cs b/api/WhatApi/Endpoints/GetPlacesEndpoint.cs
new file mode 100644
index 0000000..5630229
--- /dev/null
+++ b/api/WhatApi/Endpoints/GetPlacesEndpoint.cs
@@ -0,0 +1,57 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using NetTopologySuite;
+using NetTopologySuite.Features;
+using NetTopologySuite.Geometries;
+using WhatApi.Tables;
+
+namespace WhatApi.Endpoints;
+
+public class GetPlacesEndpoint(Database db) : BaseEndpoint
+{
+ [HttpGet("~/places")]
+ public async Task<ActionResult> HandleAsync(string w, string s, string e, string n, CancellationToken ct = default) {
+ var north = double.Parse(n);
+ var east = double.Parse(e);
+ var south = double.Parse(s);
+ var west = double.Parse(w);
+
+ IQueryable<Place> resultingQuery;
+
+ if (west > east) {
+ resultingQuery = db.Places
+ .FromSqlInterpolated($"""
+ SELECT * FROM "Place"
+ WHERE ST_Intersects(
+ "Location",
+ ST_MakeEnvelope({west}, {south}, 180, {north}, {Constants.Wgs84SpatialReferenceId}) || ST_MakeEnvelope(-180, {south}, {east}, {north}, {Constants.Wgs84SpatialReferenceId})
+ )
+ """);
+ } else {
+ resultingQuery = db.Places
+ .FromSqlInterpolated($"""
+ SELECT * FROM "Place"
+ WHERE ST_Intersects(
+ "Location",
+ ST_MakeEnvelope({west}, {south}, {east}, {north}, {Constants.Wgs84SpatialReferenceId})
+ )
+ """);
+ }
+
+ var gf = NtsGeometryServices.Instance.CreateGeometryFactory(srid: Constants.Wgs84SpatialReferenceId);
+ var fc = new FeatureCollection();
+
+ await foreach (var p in resultingQuery.AsAsyncEnumerable().WithCancellation(ct)) {
+ var point = gf.CreatePoint(new Coordinate(p.Location.X, p.Location.Y));
+ fc.Add(new Feature(point, new AttributesTable {
+ {
+ "id", p.Id
+ }, {
+ "cid", p.ContentId
+ }
+ }));
+ }
+
+ return Ok(fc);
+ }
+} \ No newline at end of file
diff --git a/api/WhatApi/Endpoints/UploadContentEndpoint.cs b/api/WhatApi/Endpoints/UploadContentEndpoint.cs
new file mode 100644
index 0000000..82fb71b
--- /dev/null
+++ b/api/WhatApi/Endpoints/UploadContentEndpoint.cs
@@ -0,0 +1,49 @@
+using Microsoft.AspNetCore.Http.Extensions;
+using Microsoft.AspNetCore.Mvc;
+using NetTopologySuite;
+using NetTopologySuite.Geometries;
+
+namespace WhatApi.Endpoints;
+
+public class UploadContentEndpoint(Database db) : BaseEndpoint
+{
+ public record UploadContent(IFormFile File, string LatLong);
+
+ [HttpPost("~/upload")]
+ public async Task<ActionResult> HandleAsync([FromForm] UploadContent request, CancellationToken ct = default) {
+ if (string.IsNullOrWhiteSpace(Request.GetMultipartBoundary())) {
+ return StatusCode(415, "Unsupported Media Type");
+ }
+
+ var blobId = Guid.NewGuid();
+ var contentId = Guid.NewGuid();
+
+ var latitude = request.LatLong.Split(',')[0];
+ var longitude = request.LatLong.Split(',')[1];
+
+ var gf = NtsGeometryServices.Instance.CreateGeometryFactory(srid: Constants.Wgs84SpatialReferenceId);
+ var point = gf.CreatePoint(new Coordinate(double.Parse(longitude), double.Parse(latitude)));
+
+ var place = new Tables.Place() {
+ ContentId = contentId,
+ Location = point
+ };
+
+ var content = new Tables.Content() {
+ Id = contentId,
+ Mime = request.File.ContentType,
+ Created = DateTime.UtcNow,
+ BlobId = blobId,
+ Ip = GetIp()
+ };
+
+ var path = Path.Combine(Directory.GetCurrentDirectory(), "files", blobId.ToString());
+
+ await using var writer = new FileStream(path, FileMode.CreateNew);
+ await request.File.CopyToAsync(writer, ct);
+ await db.Content.AddAsync(content, ct);
+ await db.Places.AddAsync(place, ct);
+ await db.SaveChangesAsync(ct);
+ return Ok(contentId);
+ }
+} \ No newline at end of file
diff --git a/api/WhatApi/Middleware/AuthenticationMiddleware.cs b/api/WhatApi/Middleware/AuthenticationMiddleware.cs
new file mode 100644
index 0000000..8cdce53
--- /dev/null
+++ b/api/WhatApi/Middleware/AuthenticationMiddleware.cs
@@ -0,0 +1,8 @@
+namespace WhatApi.Middleware;
+
+public class AuthenticationMiddleware(RequestDelegate next,Database db)
+{
+ public async Task InvokeAsync(HttpContext context) {
+ await next(context);
+ }
+} \ No newline at end of file
diff --git a/api/WhatApi/Middleware/AuthorizationMiddleware.cs b/api/WhatApi/Middleware/AuthorizationMiddleware.cs
new file mode 100644
index 0000000..26b3f4a
--- /dev/null
+++ b/api/WhatApi/Middleware/AuthorizationMiddleware.cs
@@ -0,0 +1,8 @@
+namespace WhatApi.Middleware;
+
+public class AuthorizationMiddleware(RequestDelegate next, Database db)
+{
+ public async Task InvokeAsync(HttpContext context) {
+ await next(context);
+ }
+} \ No newline at end of file
diff --git a/api/WhatApi/Middleware/UserLastSeenMiddleware.cs b/api/WhatApi/Middleware/UserLastSeenMiddleware.cs
new file mode 100644
index 0000000..ef1b685
--- /dev/null
+++ b/api/WhatApi/Middleware/UserLastSeenMiddleware.cs
@@ -0,0 +1,8 @@
+namespace WhatApi.Middleware;
+
+public class UserLastSeenMiddleware(RequestDelegate next, Database db)
+{
+ public async Task InvokeAsync(HttpContext context) {
+ await next(context);
+ }
+} \ No newline at end of file
diff --git a/api/WhatApi/Migrations/20251013213511_Initial.Designer.cs b/api/WhatApi/Migrations/20251013213511_Initial.Designer.cs
new file mode 100644
index 0000000..5ddcc9f
--- /dev/null
+++ b/api/WhatApi/Migrations/20251013213511_Initial.Designer.cs
@@ -0,0 +1,93 @@
+// <auto-generated />
+using System;
+using System.Net;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using NetTopologySuite.Geometries;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using WhatApi;
+
+#nullable disable
+
+namespace WhatApi.Migrations
+{
+ [DbContext(typeof(Database))]
+ [Migration("20251013213511_Initial")]
+ partial class Initial
+ {
+ /// <inheritdoc />
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.9")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis");
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("WhatApi.Tables.Content", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property<Guid>("BlobId")
+ .HasColumnType("uuid");
+
+ b.Property<DateTime>("Created")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property<IPAddress>("Ip")
+ .IsRequired()
+ .HasColumnType("inet");
+
+ b.Property<string>("Mime")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("Content");
+ });
+
+ modelBuilder.Entity("WhatApi.Tables.Place", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property<Guid>("ContentId")
+ .HasColumnType("uuid");
+
+ b.Property<Point>("Location")
+ .IsRequired()
+ .HasColumnType("geometry(point,4326)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ContentId");
+
+ b.HasIndex("Location");
+
+ NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Location"), "gist");
+
+ b.ToTable("Place", (string)null);
+ });
+
+ modelBuilder.Entity("WhatApi.Tables.Place", b =>
+ {
+ b.HasOne("WhatApi.Tables.Content", "Content")
+ .WithMany()
+ .HasForeignKey("ContentId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Content");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/api/WhatApi/Migrations/20251013213511_Initial.cs b/api/WhatApi/Migrations/20251013213511_Initial.cs
new file mode 100644
index 0000000..1fa8bbf
--- /dev/null
+++ b/api/WhatApi/Migrations/20251013213511_Initial.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Net;
+using Microsoft.EntityFrameworkCore.Migrations;
+using NetTopologySuite.Geometries;
+
+#nullable disable
+
+namespace WhatApi.Migrations
+{
+ /// <inheritdoc />
+ public partial class Initial : Migration
+ {
+ /// <inheritdoc />
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AlterDatabase()
+ .Annotation("Npgsql:PostgresExtension:postgis", ",,");
+
+ migrationBuilder.CreateTable(
+ name: "Content",
+ columns: table => new
+ {
+ Id = table.Column<Guid>(type: "uuid", nullable: false),
+ Mime = table.Column<string>(type: "text", nullable: false),
+ Created = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
+ BlobId = table.Column<Guid>(type: "uuid", nullable: false),
+ Ip = table.Column<IPAddress>(type: "inet", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Content", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Place",
+ columns: table => new
+ {
+ Id = table.Column<Guid>(type: "uuid", nullable: false),
+ ContentId = table.Column<Guid>(type: "uuid", nullable: false),
+ Location = table.Column<Point>(type: "geometry(point,4326)", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Place", x => x.Id);
+ table.ForeignKey(
+ name: "FK_Place_Content_ContentId",
+ column: x => x.ContentId,
+ principalTable: "Content",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Place_ContentId",
+ table: "Place",
+ column: "ContentId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Place_Location",
+ table: "Place",
+ column: "Location")
+ .Annotation("Npgsql:IndexMethod", "gist");
+ }
+
+ /// <inheritdoc />
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "Place");
+
+ migrationBuilder.DropTable(
+ name: "Content");
+ }
+ }
+}
diff --git a/api/WhatApi/Migrations/DatabaseModelSnapshot.cs b/api/WhatApi/Migrations/DatabaseModelSnapshot.cs
new file mode 100644
index 0000000..f1e5fcb
--- /dev/null
+++ b/api/WhatApi/Migrations/DatabaseModelSnapshot.cs
@@ -0,0 +1,90 @@
+// <auto-generated />
+using System;
+using System.Net;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using NetTopologySuite.Geometries;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using WhatApi;
+
+#nullable disable
+
+namespace WhatApi.Migrations
+{
+ [DbContext(typeof(Database))]
+ partial class DatabaseModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.9")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis");
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("WhatApi.Tables.Content", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property<Guid>("BlobId")
+ .HasColumnType("uuid");
+
+ b.Property<DateTime>("Created")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property<IPAddress>("Ip")
+ .IsRequired()
+ .HasColumnType("inet");
+
+ b.Property<string>("Mime")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("Content");
+ });
+
+ modelBuilder.Entity("WhatApi.Tables.Place", b =>
+ {
+ b.Property<Guid>("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property<Guid>("ContentId")
+ .HasColumnType("uuid");
+
+ b.Property<Point>("Location")
+ .IsRequired()
+ .HasColumnType("geometry(point,4326)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ContentId");
+
+ b.HasIndex("Location");
+
+ NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Location"), "gist");
+
+ b.ToTable("Place", (string)null);
+ });
+
+ modelBuilder.Entity("WhatApi.Tables.Place", b =>
+ {
+ b.HasOne("WhatApi.Tables.Content", "Content")
+ .WithMany()
+ .HasForeignKey("ContentId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Content");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/api/WhatApi/Pages/Map.cshtml b/api/WhatApi/Pages/Map.cshtml
new file mode 100644
index 0000000..31adf86
--- /dev/null
+++ b/api/WhatApi/Pages/Map.cshtml
@@ -0,0 +1,92 @@
+@page
+@model WhatApi.Pages.Map
+
+@{
+ Layout = null;
+}
+
+<!DOCTYPE html>
+
+<html>
+<head>
+ <link href="https://unpkg.com/maplibre-gl@@^5.7.2/dist/maplibre-gl.css"/>
+ <style>
+ body {
+ margin: 0;
+ padding: 0;
+ }
+
+ html, body, #map {
+ height: 90%;
+ }
+ </style>
+ <title></title>
+</head>
+<body>
+<div id="map"></div>
+<script src="https://unpkg.com/maplibre-gl@@^5.7.2/dist/maplibre-gl.js"></script>
+<script>
+ const map = new maplibregl.Map({
+ container: "map",
+ style: "https://tiles.openfreemap.org/styles/bright",
+ center: [10.253494570441944, 59.937419399772125],
+ zoom: 7
+ });
+
+ const markers = new Map();
+
+ let t = null;
+
+ map.on("moveend", () => {
+ clearTimeout(t);
+ t = setTimeout(updateData, 150);
+ });
+
+ map.on("load", () => {
+ map.loadImage("/pin.png").then(image => map.addImage("custom-marker", image.data));
+ map.addSource("places", {
+ type: "geojson",
+ data: {type: "FeatureCollection", features: []},
+ // cluster: true,
+ // clusterRadius: 40,
+ // clusterMaxZoom: 14
+ });
+
+ map.addLayer({
+ id: "places-layer",
+ type: "symbol",
+ source: "places",
+ layout: {
+ "icon-image": "custom-marker"
+ }
+ });
+
+ updateData();
+ });
+
+ let aborter = new AbortController();
+
+ async function updateData() {
+ const b = map.getBounds();
+ const south = b.getSouth(), west = b.getWest(), north = b.getNorth(), east = b.getEast();
+
+ if (aborter) {
+ aborter.abort();
+ }
+
+ aborter = new AbortController();
+
+ const res = await fetch(`/places?w=${west}&s=${south}&e=${east}&n=${north}`, {
+ signal: aborter.signal
+ });
+
+ if (!res.ok) {
+ return;
+ }
+
+ const data = await res.json().finally(() => aborter = null);
+ map.getSource("places").setData(data);
+ }
+</script>
+</body>
+</html> \ No newline at end of file
diff --git a/api/WhatApi/Pages/Map.cshtml.cs b/api/WhatApi/Pages/Map.cshtml.cs
new file mode 100644
index 0000000..786180b
--- /dev/null
+++ b/api/WhatApi/Pages/Map.cshtml.cs
@@ -0,0 +1,10 @@
+using Microsoft.AspNetCore.Mvc.RazorPages;
+
+namespace WhatApi.Pages;
+
+public class Map : PageModel
+{
+ public void OnGet() {
+
+ }
+} \ No newline at end of file
diff --git a/api/WhatApi/Pages/Upload.cshtml b/api/WhatApi/Pages/Upload.cshtml
new file mode 100644
index 0000000..4c87b11
--- /dev/null
+++ b/api/WhatApi/Pages/Upload.cshtml
@@ -0,0 +1,82 @@
+@page
+@model WhatApi.Pages.Upload
+
+@{
+ Layout = null;
+}
+
+<!DOCTYPE html>
+
+<html>
+<head>
+ <link href="https://unpkg.com/maplibre-gl@5.9.0/dist/maplibre-gl.css"
+ rel="stylesheet"/>
+ <style>
+ body {
+ margin: 0;
+ padding: 0;
+ }
+
+ html, body, #map {
+ height: 100%;
+ }
+
+ .coordinates {
+ background: rgba(0, 0, 0, 0.5);
+ color: #fff;
+ position: absolute;
+ bottom: 40px;
+ left: 10px;
+ padding: 5px 10px;
+ margin: 0;
+ font-size: 11px;
+ line-height: 18px;
+ border-radius: 3px;
+ display: none;
+ }
+ </style>
+ <title></title>
+</head>
+<body>
+<div style="display: flex; flex-direction: row; align-items: center; z-index: 10; position: absolute; background: white">
+ <form action="/upload"
+ enctype="multipart/form-data"
+ method="post">
+ <input type="hidden"
+ name="LatLong">
+ <input type="file"
+ required="required"
+ accept="image/png, image/jpeg"
+ name="File">
+ <input type="submit">
+ </form>
+</div>
+<div id="map"></div>
+<pre id="coordinates"
+ class="coordinates"></pre>
+<script src='https://unpkg.com/maplibre-gl@5.9.0/dist/maplibre-gl.js'></script>
+<script>
+ const latlongInput = document.querySelector("[name=LatLong]");
+ const coordinates = document.getElementById("coordinates");
+ const map = new maplibregl.Map({
+ container: "map",
+ style: "https://tiles.openfreemap.org/styles/bright",
+ center: [10.253494, 59.937419],
+ zoom: 7
+ });
+
+ const center = map.getCenter();
+ const marker = new maplibregl.Marker({draggable: true}).setLngLat([center.lng, center.lat]).addTo(map);
+
+ function onDragEnd() {
+ const {lat: lat, lng: lng} = marker.getLngLat();
+ coordinates.style.display = "block";
+ latlongInput.value = lat + "," + lng;
+ coordinates.innerHTML =
+ `Longitude: ${lng}<br />Latitude: ${lat}`;
+ }
+
+ marker.on("dragend", onDragEnd);
+</script>
+</body>
+</html> \ No newline at end of file
diff --git a/api/WhatApi/Pages/Upload.cshtml.cs b/api/WhatApi/Pages/Upload.cshtml.cs
new file mode 100644
index 0000000..2fe362e
--- /dev/null
+++ b/api/WhatApi/Pages/Upload.cshtml.cs
@@ -0,0 +1,10 @@
+using Microsoft.AspNetCore.Mvc.RazorPages;
+
+namespace WhatApi.Pages;
+
+public class Upload : PageModel
+{
+ public void OnGet() {
+
+ }
+} \ No newline at end of file
diff --git a/api/WhatApi/Program.cs b/api/WhatApi/Program.cs
new file mode 100644
index 0000000..80d06e8
--- /dev/null
+++ b/api/WhatApi/Program.cs
@@ -0,0 +1,45 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.EntityFrameworkCore;
+using NetTopologySuite.IO.Converters;
+
+namespace WhatApi;
+
+public partial class Program
+{
+ public static int Main(string[] args) {
+ var builder = WebApplication.CreateBuilder(args);
+
+ builder.Services.AddDbContextPool<Database>(b => {
+ b.EnableSensitiveDataLogging();
+ b.UseNpgsql(builder.Configuration.GetConnectionString("Master"), o => o.UseNetTopologySuite());
+ });
+
+ builder.Services.AddRazorPages();
+ builder.Services.AddCors(o => o.AddDefaultPolicy(p => p.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()));
+ builder.Services.AddControllers()
+ .AddJsonOptions(o => {
+ o.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
+ o.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals;
+ o.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
+ o.JsonSerializerOptions.Converters.Add(new GeoJsonConverterFactory());
+ });
+
+ var app = builder.Build();
+
+#if DEBUG
+ using var scope = app.Services.CreateScope();
+ var db = scope.ServiceProvider.GetRequiredService<Database>();
+ Seed(db);
+#endif
+
+ app.UseRouting();
+ app.UseForwardedHeaders();
+ app.UseCors();
+ app.MapStaticAssets();
+ app.MapRazorPages();
+ app.MapControllers();
+ app.Run();
+ return 0;
+ }
+} \ No newline at end of file
diff --git a/api/WhatApi/Properties/launchSettings.json b/api/WhatApi/Properties/launchSettings.json
new file mode 100644
index 0000000..581ee7b
--- /dev/null
+++ b/api/WhatApi/Properties/launchSettings.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "http://localhost:5281",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/api/WhatApi/Seed.cs b/api/WhatApi/Seed.cs
new file mode 100644
index 0000000..c3127cc
--- /dev/null
+++ b/api/WhatApi/Seed.cs
@@ -0,0 +1,24 @@
+using Bogus;
+using Bogus.Locations;
+using NetTopologySuite.Geometries;
+using WhatApi.Tables;
+
+namespace WhatApi;
+
+public partial class Program
+{
+ private static void Seed(Database db) {
+ if (db.Places.Any() || true) return;
+ var places = new List<Place>();
+ var location = new Faker().Location();
+ for (var i = 0; i < 1000; i++) {
+ var point = location.AreaCircle(59.91838, 10.73861, 30000);
+ places.Add(new Place() {
+ Location = new Point(new Coordinate(point.Longitude, point.Latitude)),
+ ContentId = new Guid("1337710a-8cdb-4d50-815f-772c0e9f1482")
+ });
+ }
+ db.Places.AddRange(places);
+ db.SaveChanges();
+ }
+} \ No newline at end of file
diff --git a/api/WhatApi/Tables/Content.cs b/api/WhatApi/Tables/Content.cs
new file mode 100644
index 0000000..79f2579
--- /dev/null
+++ b/api/WhatApi/Tables/Content.cs
@@ -0,0 +1,12 @@
+using System.Net;
+
+namespace WhatApi.Tables;
+
+public class Content
+{
+ public Guid Id { get; set; }
+ public string Mime { get; set; }
+ public DateTime Created { get; set; }
+ public Guid BlobId { get; set; }
+ public IPAddress Ip { get; set; }
+} \ No newline at end of file
diff --git a/api/WhatApi/Tables/Place.cs b/api/WhatApi/Tables/Place.cs
new file mode 100644
index 0000000..ff95c96
--- /dev/null
+++ b/api/WhatApi/Tables/Place.cs
@@ -0,0 +1,11 @@
+using NetTopologySuite.Geometries;
+
+namespace WhatApi.Tables;
+
+public class Place
+{
+ public Guid Id { get; set; }
+ public Guid ContentId { get; set; }
+ public Content Content { get; set; }
+ public required Point Location { get; set; }
+} \ No newline at end of file
diff --git a/api/WhatApi/Tables/Session.cs b/api/WhatApi/Tables/Session.cs
new file mode 100644
index 0000000..a0affa8
--- /dev/null
+++ b/api/WhatApi/Tables/Session.cs
@@ -0,0 +1,10 @@
+namespace WhatApi.Tables;
+
+public class Session
+{
+ public Guid Id { get; set; }
+ public string Token { get; set; }
+ public DateTime Created { get; set; }
+ public DateTime? Expires { get; set; }
+ public User User { get; set; }
+} \ No newline at end of file
diff --git a/api/WhatApi/Tables/User.cs b/api/WhatApi/Tables/User.cs
new file mode 100644
index 0000000..0ebe9b6
--- /dev/null
+++ b/api/WhatApi/Tables/User.cs
@@ -0,0 +1,12 @@
+namespace WhatApi.Tables;
+
+public class User
+{
+ public Guid Id { get; set; }
+ public string Name { get; set; }
+ public string Email { get; set; }
+ public string Password { get; set; }
+ public DateTime Created { get; set; }
+ public DateTime? LastSeen { get; set; }
+ public IEnumerable<Place> Places { get; set; }
+} \ No newline at end of file
diff --git a/api/WhatApi/WhatApi.csproj b/api/WhatApi/WhatApi.csproj
new file mode 100644
index 0000000..84dac71
--- /dev/null
+++ b/api/WhatApi/WhatApi.csproj
@@ -0,0 +1,34 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+ <PropertyGroup>
+ <TargetFramework>net9.0</TargetFramework>
+ <Nullable>enable</Nullable>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
+ <UserSecretsId>5506c159-f534-4090-b80b-2703e1eb7f6c</UserSecretsId>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Content Include="..\.dockerignore">
+ <Link>.dockerignore</Link>
+ </Content>
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Bogus" Version="35.6.3" />
+ <PackageReference Include="Bogus.Locations" Version="35.6.3" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.9" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.9">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="NetTopologySuite.IO.GeoJSON4STJ" Version="4.0.0" />
+ <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
+ <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite" Version="9.0.4" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <Folder Include="files\" />
+ </ItemGroup>
+
+</Project>
diff --git a/api/WhatApi/appsettings.Development.json b/api/WhatApi/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/api/WhatApi/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/api/WhatApi/appsettings.json b/api/WhatApi/appsettings.json
new file mode 100644
index 0000000..10f68b8
--- /dev/null
+++ b/api/WhatApi/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/api/WhatApi/wwwroot/pin.png b/api/WhatApi/wwwroot/pin.png
new file mode 100644
index 0000000..3603031
--- /dev/null
+++ b/api/WhatApi/wwwroot/pin.png
Binary files differ
diff --git a/ios/H--appen-Info.plist b/ios/H--appen-Info.plist
new file mode 100644
index 0000000..0c67376
--- /dev/null
+++ b/ios/H--appen-Info.plist
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict/>
+</plist>
diff --git a/ios/Hæ-appen.xcodeproj/project.pbxproj b/ios/Hæ-appen.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..d237a7b
--- /dev/null
+++ b/ios/Hæ-appen.xcodeproj/project.pbxproj
@@ -0,0 +1,381 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 77;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 1B9B791B2E7A06C300D5AF05 /* GISTools in Frameworks */ = {isa = PBXBuildFile; productRef = 1B9B791A2E7A06C300D5AF05 /* GISTools */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXFileReference section */
+ 1B9B79032E79DFB800D5AF05 /* Hæ-appen.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Hæ-appen.app"; sourceTree = BUILT_PRODUCTS_DIR; };
+/* End PBXFileReference section */
+
+/* Begin PBXFileSystemSynchronizedRootGroup section */
+ 1B9B79052E79DFB800D5AF05 /* Hæ-appen */ = {
+ isa = PBXFileSystemSynchronizedRootGroup;
+ path = "Hæ-appen";
+ sourceTree = "<group>";
+ };
+/* End PBXFileSystemSynchronizedRootGroup section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 1B9B79002E79DFB800D5AF05 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 1B9B791B2E7A06C300D5AF05 /* GISTools in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 1B9B78FA2E79DFB800D5AF05 = {
+ isa = PBXGroup;
+ children = (
+ 1B9B79052E79DFB800D5AF05 /* Hæ-appen */,
+ 1B9B79042E79DFB800D5AF05 /* Products */,
+ );
+ sourceTree = "<group>";
+ };
+ 1B9B79042E79DFB800D5AF05 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 1B9B79032E79DFB800D5AF05 /* Hæ-appen.app */,
+ );
+ name = Products;
+ sourceTree = "<group>";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 1B9B79022E79DFB800D5AF05 /* Hæ-appen */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 1B9B790E2E79DFBA00D5AF05 /* Build configuration list for PBXNativeTarget "Hæ-appen" */;
+ buildPhases = (
+ 1B9B78FF2E79DFB800D5AF05 /* Sources */,
+ 1B9B79002E79DFB800D5AF05 /* Frameworks */,
+ 1B9B79012E79DFB800D5AF05 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ fileSystemSynchronizedGroups = (
+ 1B9B79052E79DFB800D5AF05 /* Hæ-appen */,
+ );
+ name = "Hæ-appen";
+ packageProductDependencies = (
+ 1B9B791A2E7A06C300D5AF05 /* GISTools */,
+ );
+ productName = "Hæ-appen";
+ productReference = 1B9B79032E79DFB800D5AF05 /* Hæ-appen.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 1B9B78FB2E79DFB800D5AF05 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = 1;
+ LastSwiftUpdateCheck = 2600;
+ LastUpgradeCheck = 2600;
+ TargetAttributes = {
+ 1B9B79022E79DFB800D5AF05 = {
+ CreatedOnToolsVersion = 26.0;
+ };
+ };
+ };
+ buildConfigurationList = 1B9B78FE2E79DFB800D5AF05 /* Build configuration list for PBXProject "Hæ-appen" */;
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 1B9B78FA2E79DFB800D5AF05;
+ minimizedProjectReferenceProxies = 1;
+ packageReferences = (
+ 1B9B79192E7A06C300D5AF05 /* XCRemoteSwiftPackageReference "gis-tools" */,
+ );
+ preferredProjectObjectVersion = 77;
+ productRefGroup = 1B9B79042E79DFB800D5AF05 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 1B9B79022E79DFB800D5AF05 /* Hæ-appen */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 1B9B79012E79DFB800D5AF05 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 1B9B78FF2E79DFB800D5AF05 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin XCBuildConfiguration section */
+ 1B9B790C2E79DFBA00D5AF05 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 26.0;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ 1B9B790D2E79DFBA00D5AF05 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 26.0;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 1B9B790F2E79DFBA00D5AF05 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = ZH95BL9NUK;
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = "H--appen-Info.plist";
+ INFOPLIST_KEY_CFBundleDisplayName = "Hæ";
+ INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
+ INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "";
+ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
+ IPHONEOS_DEPLOYMENT_TARGET = 17.6;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = ivarivarivar.Haeappen;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ STRING_CATALOG_GENERATE_SYMBOLS = YES;
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+ SUPPORTS_MACCATALYST = NO;
+ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
+ SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
+ SWIFT_APPROACHABLE_CONCURRENCY = YES;
+ SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = 1;
+ };
+ name = Debug;
+ };
+ 1B9B79102E79DFBA00D5AF05 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = ZH95BL9NUK;
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = "H--appen-Info.plist";
+ INFOPLIST_KEY_CFBundleDisplayName = "Hæ";
+ INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
+ INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "";
+ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
+ IPHONEOS_DEPLOYMENT_TARGET = 17.6;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = ivarivarivar.Haeappen;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ STRING_CATALOG_GENERATE_SYMBOLS = YES;
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+ SUPPORTS_MACCATALYST = NO;
+ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
+ SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
+ SWIFT_APPROACHABLE_CONCURRENCY = YES;
+ SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = 1;
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 1B9B78FE2E79DFB800D5AF05 /* Build configuration list for PBXProject "Hæ-appen" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 1B9B790C2E79DFBA00D5AF05 /* Debug */,
+ 1B9B790D2E79DFBA00D5AF05 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 1B9B790E2E79DFBA00D5AF05 /* Build configuration list for PBXNativeTarget "Hæ-appen" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 1B9B790F2E79DFBA00D5AF05 /* Debug */,
+ 1B9B79102E79DFBA00D5AF05 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+
+/* Begin XCRemoteSwiftPackageReference section */
+ 1B9B79192E7A06C300D5AF05 /* XCRemoteSwiftPackageReference "gis-tools" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "https://github.com/Outdooractive/gis-tools.git";
+ requirement = {
+ kind = upToNextMajorVersion;
+ minimumVersion = 1.3.0;
+ };
+ };
+/* End XCRemoteSwiftPackageReference section */
+
+/* Begin XCSwiftPackageProductDependency section */
+ 1B9B791A2E7A06C300D5AF05 /* GISTools */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 1B9B79192E7A06C300D5AF05 /* XCRemoteSwiftPackageReference "gis-tools" */;
+ productName = GISTools;
+ };
+/* End XCSwiftPackageProductDependency section */
+ };
+ rootObject = 1B9B78FB2E79DFB800D5AF05 /* Project object */;
+}
diff --git a/ios/Hæ-appen.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Hæ-appen.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ b/ios/Hæ-appen.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+ version = "1.0">
+ <FileRef
+ location = "self:">
+ </FileRef>
+</Workspace>
diff --git a/ios/Hæ-appen.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Hæ-appen.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
new file mode 100644
index 0000000..36b946c
--- /dev/null
+++ b/ios/Hæ-appen.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -0,0 +1,15 @@
+{
+ "originHash" : "a1cca039aa1704ebabd161056262205e26a4daf8a42ebb77bc98b0b620ee8fb4",
+ "pins" : [
+ {
+ "identity" : "gis-tools",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/Outdooractive/gis-tools.git",
+ "state" : {
+ "revision" : "f7a4d2aa2acf771b79a91150f359bcd84de9ceac",
+ "version" : "1.13.2"
+ }
+ }
+ ],
+ "version" : 3
+}
diff --git a/ios/Hæ-appen/Assets.xcassets/AccentColor.colorset/Contents.json b/ios/Hæ-appen/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 0000000..eb87897
--- /dev/null
+++ b/ios/Hæ-appen/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors" : [
+ {
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ios/Hæ-appen/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Hæ-appen/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..2305880
--- /dev/null
+++ b/ios/Hæ-appen/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,35 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "tinted"
+ }
+ ],
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ios/Hæ-appen/Assets.xcassets/Contents.json b/ios/Hæ-appen/Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/ios/Hæ-appen/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ios/Hæ-appen/ClusterHit.swift b/ios/Hæ-appen/ClusterHit.swift
new file mode 100644
index 0000000..ebab096
--- /dev/null
+++ b/ios/Hæ-appen/ClusterHit.swift
@@ -0,0 +1,18 @@
+//
+// ClusterHit.swift
+// Hæ-appen
+//
+// Created by Ivar Løvlie on 16/09/2025.
+//
+import GISTools
+import WebKit
+
+struct ClusterHit : Decodable { }
+
+final class ClusterProvider {
+ func load(west:Int, south: Int, north: Int, east: Int) async throws -> [ClusterHit] {
+ let url = URL(string: "http://http://localhost:5281/places?w=\(west)&e=\(east)&s=\(south)&n=\(north)")!
+ let (data, _) = try await URLSession.shared.data(from: url)
+ return try JSONDecoder().decode([ClusterHit].self, from: data)
+ }
+}
diff --git a/ios/Hæ-appen/HaeappenApp.swift b/ios/Hæ-appen/HaeappenApp.swift
new file mode 100644
index 0000000..e8aac4d
--- /dev/null
+++ b/ios/Hæ-appen/HaeappenApp.swift
@@ -0,0 +1,17 @@
+//
+// H__appenApp.swift
+// Hæ-appen
+//
+// Created by Ivar Løvlie on 16/09/2025.
+//
+
+import SwiftUI
+
+@main
+struct HaeappenApp: App {
+ var body: some Scene {
+ WindowGroup {
+ TabBarView()
+ }
+ }
+}
diff --git a/ios/Hæ-appen/LocationAuthorizer.swift b/ios/Hæ-appen/LocationAuthorizer.swift
new file mode 100644
index 0000000..fb6f3c8
--- /dev/null
+++ b/ios/Hæ-appen/LocationAuthorizer.swift
@@ -0,0 +1,20 @@
+//
+// LocationAuthorizer.swift
+// Hæ-appen
+//
+// Created by Ivar Løvlie on 16/10/2025.
+//
+
+import SwiftUI
+import MapKit
+import CoreLocation
+
+final class LocationAuthorizer: NSObject, CLLocationManagerDelegate {
+ static let shared = LocationAuthorizer()
+ private let manager = CLLocationManager()
+
+ func requestWhenInUse() {
+ manager.delegate = self
+ manager.requestWhenInUseAuthorization()
+ }
+}
diff --git a/ios/Hæ-appen/MapContentView.swift b/ios/Hæ-appen/MapContentView.swift
new file mode 100644
index 0000000..4d35dfe
--- /dev/null
+++ b/ios/Hæ-appen/MapContentView.swift
@@ -0,0 +1,45 @@
+//
+// ContentView.swift
+// Hæ-appen
+//
+// Created by Ivar Løvlie on 16/09/2025.
+//
+
+import SwiftUI
+import MapKit
+
+
+struct MapContentView: View {
+ @Namespace var mapScope
+ @State private var position: MapCameraPosition = .automatic
+ var annotationStringKey: LocalizedStringKey = "Annotation"
+ var annotationCoordinate: CLLocationCoordinate2D = CLLocationCoordinate2D.init()
+ let symbolSet: [String] = ["cloud.bolt.rain.fill", "sun.rain.fill", "moon.stars.fill", "moon.fill"]
+
+ var body: some View {
+ if #available(iOS 26, *) {
+ VStack {
+ Map(initialPosition: .userLocation(fallback: position), scope: mapScope)
+ }.safeAreaInset(edge: .trailing) {
+ GlassEffectContainer(spacing: 10.0) {
+ HStack(spacing: 20.0) {
+ ForEach(symbolSet.indices, id: \.self) { item in
+ Image(systemName: symbolSet[item])
+ .frame(width: 80.0, height: 80.0)
+ .font(.system(size: 36))
+ .glassEffect()
+ .glassEffectUnion(id: item < 2 ? "1" : "2", namespace: mapScope)
+ }
+ }
+ }
+ }.mapScope(mapScope)
+ .task {
+ LocationAuthorizer.shared.requestWhenInUse()
+ }
+ }
+ }
+}
+
+#Preview {
+ MapContentView()
+}
diff --git a/ios/Hæ-appen/PostAnnotationView.swift b/ios/Hæ-appen/PostAnnotationView.swift
new file mode 100644
index 0000000..024015b
--- /dev/null
+++ b/ios/Hæ-appen/PostAnnotationView.swift
@@ -0,0 +1,9 @@
+//
+// PostAnnotationView.swift
+// Hæ-appen
+//
+// Created by Ivar Løvlie on 18/10/2025.
+//
+
+import Foundation
+
diff --git a/ios/Hæ-appen/TabBarView.swift b/ios/Hæ-appen/TabBarView.swift
new file mode 100644
index 0000000..ed0a89c
--- /dev/null
+++ b/ios/Hæ-appen/TabBarView.swift
@@ -0,0 +1,22 @@
+//
+// TabBarView.swift
+// Hæ-appen
+//
+// Created by Ivar Løvlie on 19/10/2025.
+//
+import SwiftUI
+
+struct TabBarView : View {
+ var body: some View {
+ if #available(iOS 26, *) {
+ TabView {
+ Tab("Hææ", systemImage: "house") {
+ MapContentView()
+ }
+ }
+ .tabViewBottomAccessory {
+ Image(systemName: "person.fill")
+ }
+ }
+ }
+}