diff options
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 Binary files differnew file mode 100644 index 0000000..3603031 --- /dev/null +++ b/api/WhatApi/wwwroot/pin.png 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") + } + } + } +} |
