34 Commits

Author SHA1 Message Date
f92c8fced7 Update package version to 1.2.2 in package.json and package-lock.json
Some checks failed
CI / check (push) Successful in 52s
OSV-Scanner Scheduled Scan / scan-scheduled (push) Successful in 17s
Publish NPM Package / publish (push) Failing after 49s
CI / build (push) Successful in 48s
Build and Publish Docker Image / build (push) Successful in 14m35s
2025-12-25 14:26:32 -06:00
f80919c45f Update npm-publish workflow to use updated Gitea token variable for authentication 2025-12-25 14:26:20 -06:00
ad4fa4f93a Update npm-publish workflow to configure registry for publishing and ensure dependencies are installed from npmjs.org
Some checks failed
CI / check (push) Successful in 1m7s
OSV-Scanner Scheduled Scan / scan-scheduled (push) Successful in 35s
CI / build (push) Successful in 1m11s
Publish NPM Package / publish (push) Failing after 1m38s
Build and Publish Docker Image / build (push) Has been cancelled
2025-12-25 14:15:37 -06:00
66a1933cfb Improve app version retrieval in vite.config.ts to declare process.env for improved type safety and clarity.
Some checks failed
OSV-Scanner Scheduled Scan / scan-scheduled (push) Successful in 30s
Build and Publish Docker Image / build (push) Has been cancelled
CI / check (push) Successful in 52s
CI / build (push) Successful in 1m13s
2025-12-25 14:14:28 -06:00
620e63b7ba Fix
Some checks failed
CI / check (push) Failing after 27s
OSV-Scanner Scheduled Scan / scan-scheduled (push) Successful in 23s
CI / build (push) Has been skipped
Build and Publish Docker Image / build (push) Has been cancelled
Publish NPM Package / publish (push) Failing after 26s
2025-12-25 14:12:27 -06:00
9918638cb8 Refactor Camera interface in map.ts to export as a type for improved type management and clarity. 2025-12-25 14:11:32 -06:00
165f06e06b Update app version retrieval in vite.config.ts to use import.meta.env for improved compatibility with Vite's environment variables 2025-12-25 14:11:26 -06:00
405a254824 Add eslint directive to suppress unused variable warning for setStatusMessage function in +page.svelte 2025-12-25 14:11:19 -06:00
0bb79ff612 Add app version definition in vite.config.ts for improved version management 2025-12-25 14:09:40 -06:00
55a90bd146 Refactor APP_VERSION definition in version.ts to support multiple sources 2025-12-25 14:09:28 -06:00
7dcfb1ff7c format workflow 2025-12-25 14:01:35 -06:00
6b35ab80a2 Update module exports in index.ts to include API, constants, map, settings, and version files for improved accessibility and organization. 2025-12-25 14:00:30 -06:00
d9d97db0f9 Refactor package configuration to use scoped name and enhance export settings
- Updated package name to '@quad4/surveilled' for better namespace management.
- Added publish configuration for custom NPM registry.
- Defined exports for module entry points and included necessary files for packaging.
- Updated dependencies in package-lock.json to include new packages and versions.
2025-12-25 14:00:12 -06:00
6796d85a6a Update Makefile to include new package and publish targets, and modify clean command to remove 'dist' and 'package' directories. 2025-12-25 13:59:56 -06:00
6f1428b8e8 Configure NPM registry and authentication token in .npmrc for package management 2025-12-25 13:59:51 -06:00
1b38193ca3 Update .dockerignore and .gitignore to include 'dist/' directory for package management 2025-12-25 13:59:44 -06:00
f258254adf Add NPM publish workflow for automated package deployment 2025-12-25 13:59:39 -06:00
61c47c2d60 Update version to 1.2.0 in package.json and package-lock.json 2025-12-25 13:53:10 -06:00
5b31ef951d Update README
All checks were successful
OSV-Scanner Scheduled Scan / scan-scheduled (push) Successful in 24s
CI / check (push) Successful in 28s
CI / build (push) Successful in 45s
Build and Publish Docker Image / build (push) Successful in 10m17s
2025-12-25 13:43:16 -06:00
3c574b58f6 Enhance camera map interface with new settings and improved functionality
- Added settings management for Overpass API, custom tile URLs, and Nominatim search endpoints, allowing users to customize their experience.
- Implemented responsive design adjustments for mobile views, including a modal for location search.
- Improved camera selection and measurement features with enhanced visual feedback and distance labeling.
- Updated footer behavior to persist user preferences and improve usability.
2025-12-25 13:36:09 -06:00
07d5c540b6 Add settings management for Overpass API, custom tiles, Nominatim, basemap, and footer preferences
- Implemented Svelte stores to manage user preferences for Overpass API endpoints, custom tile URLs, Nominatim search endpoints, basemap selection, and footer visibility.
- Integrated localStorage for persistent settings across sessions, enhancing user experience and customization options.
2025-12-25 13:36:01 -06:00
b826ee8f4f Refactor API endpoint handling to prioritize user-defined settings
- Introduced dynamic endpoint selection for Overpass API based on user preferences.
- Updated Nominatim API URL construction to utilize a configurable base URL from settings.
- Enhanced the fetchOverpassWithFallback function to improve reliability by using preferred endpoints.
2025-12-25 13:35:55 -06:00
2df526c606 Update Overpass API endpoints in constants.ts to include additional servers for improved reliability 2025-12-25 13:35:48 -06:00
26f91f1aee Add responsive styling for zoom control in mobile view 2025-12-25 13:35:42 -06:00
85febfacc1 Update ESLint configuration to add readonly definitions for HTMLButtonElement and localStorage 2025-12-25 13:35:34 -06:00
0eab72db49 Add Docker support to Makefile 2025-12-25 13:34:20 -06:00
dffd3ff838 Format yaml 2025-12-25 13:34:08 -06:00
f5407e56e1 Add Docker workflow for building and publishing images
Some checks failed
OSV-Scanner Scheduled Scan / scan-scheduled (push) Successful in 23s
CI / check (push) Successful in 39s
Build and Publish Docker Image / build (push) Failing after 58s
CI / build (push) Successful in 46s
2025-12-25 13:16:25 -06:00
ivan
8adc0aa85d Update README.md
All checks were successful
OSV-Scanner Scheduled Scan / scan-scheduled (push) Successful in 22s
CI / check (push) Successful in 24s
CI / build (push) Successful in 39s
2025-12-25 04:50:40 +00:00
ivan
2e7780e711 Upload files to "showcase"
All checks were successful
OSV-Scanner Scheduled Scan / scan-scheduled (push) Successful in 21s
CI / check (push) Successful in 24s
CI / build (push) Successful in 39s
2025-12-25 04:47:24 +00:00
c0dd901def Add validation functions for API responses and map parameters
All checks were successful
OSV-Scanner Scheduled Scan / scan-scheduled (push) Successful in 19s
CI / check (push) Successful in 22s
CI / build (push) Successful in 39s
- Introduced validation functions for Overpass and Nominatim API responses to ensure correct data structure.
- Added latitude, longitude, and bounds validation functions in the Svelte component to enhance input handling.
- Updated the map state restoration logic to utilize these validation checks, improving robustness and error handling.
2025-12-24 21:20:07 -06:00
2d7efb03fb Update version to 1.1.0 in package.json and package-lock.json
All checks were successful
OSV-Scanner Scheduled Scan / scan-scheduled (push) Successful in 18s
CI / check (push) Successful in 22s
CI / build (push) Successful in 39s
2025-12-24 21:13:20 -06:00
191ed6b0e1 Improve box selection functionality in map component
- Improved box selection mode logic by ensuring proper state checks and event handling.
- Added conditions to prevent actions when the selection mode is not active or when necessary elements are missing.
- Updated the UI to include a tooltip for better user guidance during box selection.
- Refactored code for clarity and maintainability, ensuring a smoother user experience.
2025-12-24 21:13:08 -06:00
57187e7ed4 Add tooltip for box selection feature in map component
All checks were successful
OSV-Scanner Scheduled Scan / scan-scheduled (push) Successful in 19s
CI / check (push) Successful in 22s
CI / build (push) Successful in 40s
- Introduced a tooltip that guides users to draw a box for camera searches, displayed conditionally based on local storage state.
- Implemented tooltip positioning logic and dismiss functionality.
- Updated the Svelte component to enhance user interaction and experience.
2025-12-24 21:09:29 -06:00
20 changed files with 1178 additions and 196 deletions

View File

@@ -21,3 +21,7 @@ Thumbs.db
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
# Package
dist/

View File

@@ -0,0 +1,66 @@
name: Build and Publish Docker Image
on:
workflow_dispatch:
push:
branches: [master]
tags: ['v*']
pull_request:
branches: [master]
env:
REGISTRY: git.quad4.io
IMAGE_NAME: quad4-software/surveilled
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
outputs:
image_digest: ${{ steps.build.outputs.digest }}
image_tags: ${{ steps.meta.outputs.tags }}
steps:
- name: Checkout repository
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744
- name: Set up QEMU
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392
with:
platforms: amd64,arm64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435
- name: Log in to the Container registry
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=ref,event=branch,prefix=,suffix=,enable={{is_default_branch}}
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,format=short
- name: Build and push Docker image
id: build
uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -0,0 +1,36 @@
name: Publish NPM Package
on:
workflow_dispatch:
push:
tags:
- 'v*'
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Install dependencies
run: npm ci --registry=https://registry.npmjs.org/
- name: Package
run: make package
- name: Configure npm for publishing
uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: 'https://git.quad4.io/api/packages/quad4-software/npm/'
- name: Publish
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.GT_TOKEN }}

3
.gitignore vendored
View File

@@ -21,3 +21,6 @@ Thumbs.db
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
# Package
dist/

3
.npmrc
View File

@@ -1 +1,2 @@
engine-strict=true
@quad4:registry=https://git.quad4.io/api/packages/quad4-software/npm/
//git.quad4.io/api/packages/quad4-software/npm/:_authToken=${NPM_TOKEN}

View File

@@ -1,4 +1,4 @@
.PHONY: help install dev build preview check lint format clean
.PHONY: help install dev build preview check lint format clean docker-build docker-run docker package publish
help:
@echo 'Usage: make [target]'
@@ -6,15 +6,19 @@ help:
@echo 'Available targets:'
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " %-15s %s\n", $$1, $$2}' $(MAKEFILE_LIST)
install:
npm install
dev:
npm install
npm run dev
build:
npm run build
package:
npm run package
publish:
npm publish
preview:
npm run preview
@@ -28,5 +32,13 @@ format:
npm run format
clean:
rm -rf .svelte-kit build node_modules/.vite
rm -rf .svelte-kit build node_modules/.vite dist package
docker-build:
docker build -t surveilled .
docker-run:
docker run --rm -p 3000:3000 surveilled
docker: docker-build docker-run

View File

@@ -1,40 +1,84 @@
# Surveilled
[Website](https://surveilled.quad4.io)
<img src="showcase/surveilled.png" alt="showcase image" width="900">
A map of cameras in the world using OpenStreetMap data.
Check out the live website at [surveilled.quad4.io](https://surveilled.quad4.io)
A map of cameras in the world using OpenStreetMap overpass data.
## Disclaimer
Data is fetched from OSM Overpass and may not be accurate or up to date as this data is community-sourced.
## Features
- Draw a box to get cameras for that area
- Measure distance between two points/cameras
- Export or copy GeoJSON of cameras in an area
- Customize Nominatim, Overpass, and Tile endpoints
- No reliance on external CDNs or Google fonts.
- PWA installable
- Mobile-friendly
## Self-hosting
### NPM
coming soon
### Docker
```sh
docker run -p 3000:3000 git.quad4.io/quad4-software/surveilled:latest
```
### Podman
```sh
podman run -p 3000:3000 git.quad4.io/quad4-software/surveilled:latest
```
## Development
```sh
git clone https://git.quad4.io/Quad4-Software/Surveilled
cd Surveilled
```
### NPM
```sh
npm install
npm run dev
```
### Makefile
```sh
make dev
```
## Building
#### Check/Lint/Format
```sh
make build
make check
make lint
make format
```
## Preview
```sh
make preview
```
## Docker
## Building Docker Image
Uses Chainguard Images which are rootless and very minimal images.
```sh
docker build -t surveilled .
docker run -p 3000:3000 surveilled
docker run --rm -p 3000:3000 surveilled
```
## Contributing
Send email to [team@quad4.io](mailto:team@quad4.io) with your feedback or any issues you may have.
## LICENSE
[MIT](LICENSE)
[MIT](LICENSE)

View File

@@ -22,9 +22,11 @@ export default [
HTMLElement: 'readonly',
HTMLImageElement: 'readonly',
HTMLInputElement: 'readonly',
HTMLButtonElement: 'readonly',
navigator: 'readonly',
window: 'readonly',
document: 'readonly',
localStorage: 'readonly',
Blob: 'readonly',
Event: 'readonly',
MouseEvent: 'readonly',

91
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "surveilled",
"version": "1.0.0",
"name": "@quad4/surveilled",
"version": "1.2.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "surveilled",
"version": "1.0.0",
"name": "@quad4/surveilled",
"version": "1.2.2",
"dependencies": {
"autoprefixer": "^10.4.23",
"lucide-svelte": "^0.562.0",
@@ -18,6 +18,7 @@
"@eslint/js": "^9.39.2",
"@sveltejs/adapter-auto": "^7.0.0",
"@sveltejs/kit": "^2.49.1",
"@sveltejs/package": "^2.3.9",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"@typescript-eslint/eslint-plugin": "^8.50.1",
"@typescript-eslint/parser": "^8.50.1",
@@ -2914,6 +2915,59 @@
}
}
},
"node_modules/@sveltejs/package": {
"version": "2.5.7",
"resolved": "https://registry.npmjs.org/@sveltejs/package/-/package-2.5.7.tgz",
"integrity": "sha512-qqD9xa9H7TDiGFrF6rz7AirOR8k15qDK/9i4MIE8te4vWsv5GEogPks61rrZcLy+yWph+aI6pIj2MdoK3YI8AQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"chokidar": "^5.0.0",
"kleur": "^4.1.5",
"sade": "^1.8.1",
"semver": "^7.5.4",
"svelte2tsx": "~0.7.33"
},
"bin": {
"svelte-package": "svelte-package.js"
},
"engines": {
"node": "^16.14 || >=18"
},
"peerDependencies": {
"svelte": "^3.44.0 || ^4.0.0 || ^5.0.0-next.1"
}
},
"node_modules/@sveltejs/package/node_modules/chokidar": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
"dev": true,
"license": "MIT",
"dependencies": {
"readdirp": "^5.0.0"
},
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@sveltejs/package/node_modules/readdirp": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@sveltejs/vite-plugin-svelte": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.1.tgz",
@@ -3933,6 +3987,13 @@
}
}
},
"node_modules/dedent-js": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dedent-js/-/dedent-js-1.0.1.tgz",
"integrity": "sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ==",
"dev": true,
"license": "MIT"
},
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -7080,6 +7141,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/scule": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz",
"integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==",
"dev": true,
"license": "MIT"
},
"node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
@@ -7728,6 +7796,21 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/svelte2tsx": {
"version": "0.7.46",
"resolved": "https://registry.npmjs.org/svelte2tsx/-/svelte2tsx-0.7.46.tgz",
"integrity": "sha512-S++Vw3w47a8rBuhbz4JK0fcGea8tOoX1boT53Aib8+oUO2EKeOG+geXprJVTDfBlvR+IJdf3jIpR2RGwT6paQA==",
"dev": true,
"license": "MIT",
"dependencies": {
"dedent-js": "^1.0.1",
"scule": "^1.3.0"
},
"peerDependencies": {
"svelte": "^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0",
"typescript": "^4.9.4 || ^5.0.0"
}
},
"node_modules/tailwindcss": {
"version": "3.4.19",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",

View File

@@ -1,8 +1,23 @@
{
"name": "surveilled",
"private": true,
"version": "1.0.0",
"name": "@quad4/surveilled",
"version": "1.2.2",
"type": "module",
"publishConfig": {
"registry": "https://git.quad4.io/api/packages/quad4-software/npm/"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"svelte": "./dist/index.js",
"default": "./dist/index.js"
}
},
"files": [
"dist",
"package",
"README.md",
"LICENSE"
],
"scripts": {
"dev": "VITE_APP_VERSION=$(node -p \"require('./package.json').version\") vite dev",
"build": "VITE_APP_VERSION=$(node -p \"require('./package.json').version\") vite build",
@@ -11,12 +26,14 @@
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"format": "prettier --write .",
"lint": "eslint ."
"lint": "eslint .",
"package": "svelte-kit sync && svelte-package"
},
"devDependencies": {
"@eslint/js": "^9.39.2",
"@sveltejs/adapter-auto": "^7.0.0",
"@sveltejs/kit": "^2.49.1",
"@sveltejs/package": "^2.3.9",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"@typescript-eslint/eslint-plugin": "^8.50.1",
"@typescript-eslint/parser": "^8.50.1",

BIN
showcase/surveilled.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 837 KiB

View File

@@ -80,6 +80,12 @@
background-color: #262626 !important;
}
@media (max-width: 640px) {
.ol-zoom {
display: none !important;
}
}
.ol-attribution.ol-unselectable.ol-control {
background-color: rgba(23, 23, 23, 0.9) !important;
color: #fafafa !important;

View File

@@ -1,9 +1,11 @@
import { get } from 'svelte/store';
import {
OVERPASS_ENDPOINTS,
OVERPASS_TIMEOUT,
OVERPASS_MAX_BOUNDS_SPAN,
ERROR_MESSAGES,
} from './constants';
import { overpassEndpoint, nominatimEndpoint } from './settings';
export interface Camera {
lon: number;
@@ -27,9 +29,19 @@ export interface OverpassResponse {
elements: OverpassElement[];
}
function isValidOverpassResponse(data: unknown): data is OverpassResponse {
if (!data || typeof data !== 'object') return false;
if (!('elements' in data)) return false;
if (!Array.isArray(data.elements)) return false;
return true;
}
export async function fetchOverpassWithFallback(query: string): Promise<OverpassResponse> {
let lastErr: Error | null = null;
for (const endpoint of OVERPASS_ENDPOINTS) {
const preferred = get(overpassEndpoint);
const endpoints = [preferred, ...OVERPASS_ENDPOINTS.filter((e) => e !== preferred)];
for (const endpoint of endpoints) {
try {
const url = `${endpoint}?data=${encodeURIComponent(query)}`;
const response = await fetch(url);
@@ -39,7 +51,11 @@ export async function fetchOverpassWithFallback(query: string): Promise<Overpass
if (response.status === 429) continue;
else continue;
}
return JSON.parse(text);
const parsed = JSON.parse(text);
if (!isValidOverpassResponse(parsed)) {
throw new Error('Invalid Overpass response structure');
}
return parsed;
} catch (err) {
lastErr = err as Error;
continue;
@@ -132,13 +148,30 @@ export interface NominatimResult {
export type NominatimResponse = NominatimResult[];
function isValidNominatimResponse(data: unknown): data is NominatimResponse {
if (!Array.isArray(data)) return false;
return data.every((item) => {
return (
item &&
typeof item === 'object' &&
typeof item.place_id === 'number' &&
typeof item.display_name === 'string' &&
typeof item.lat === 'string' &&
typeof item.lon === 'string'
);
});
}
export async function searchNominatim(query: string): Promise<NominatimResult[]> {
if (!query.trim()) {
return [];
}
const url = new URL('https://nominatim.openstreetmap.org/search');
url.searchParams.set('q', query);
const sanitizedQuery = query.trim().slice(0, 200);
const baseUrl = get(nominatimEndpoint);
const url = new URL(baseUrl);
url.searchParams.set('q', sanitizedQuery);
url.searchParams.set('format', 'json');
url.searchParams.set('limit', '10');
url.searchParams.set('addressdetails', '0');
@@ -156,7 +189,10 @@ export async function searchNominatim(query: string): Promise<NominatimResult[]>
throw new Error(`Nominatim error ${response.status}`);
}
const data: NominatimResponse = await response.json();
const data: unknown = await response.json();
if (!isValidNominatimResponse(data)) {
throw new Error('Invalid Nominatim response structure');
}
return data;
} catch (err) {
console.error('Nominatim search failed:', err);

View File

@@ -1,7 +1,9 @@
// Overpass API Configuration
export const OVERPASS_ENDPOINTS = [
'https://overpass-api.de/api/interpreter',
'https://lz4.overpass-api.de/api/interpreter',
'https://overpass.kumi.systems/api/interpreter',
'https://overpass.openstreetmap.fr/api/interpreter',
] as const;
export const OVERPASS_TIMEOUT = 25; // seconds

View File

@@ -1 +1,5 @@
// place files you want to import through the `$lib` alias in this folder.
export * from './api';
export * from './constants';
export * from './map';
export * from './settings';
export * from './version';

View File

@@ -28,18 +28,27 @@ import {
FOV_MIN_ZOOM,
LAYER_RENDER_BUFFER,
} from './constants';
import type { Camera } from './api';
export interface Camera {
lon: number;
lat: number;
type: string;
direction: string | null;
operator?: string;
description?: string;
}
export type { Camera };
const canUseCacheApi = typeof caches !== 'undefined';
function isValidTileUrl(url: string): boolean {
if (!url || typeof url !== 'string') return false;
if (url.startsWith('javascript:')) return false;
if (url.startsWith('data:')) {
if (!url.startsWith('data:image/')) return false;
return true;
}
try {
const parsed = new URL(url);
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
} catch {
return false;
}
}
async function cachedTileLoad(imageTile: Tile, src: string) {
if (!(imageTile instanceof ImageTile)) {
return;
@@ -48,6 +57,10 @@ async function cachedTileLoad(imageTile: Tile, src: string) {
if (!(image instanceof HTMLImageElement)) {
return;
}
if (!isValidTileUrl(src)) {
console.error('Invalid tile URL:', src);
return;
}
if (!canUseCacheApi) {
image.src = src;
return;

129
src/lib/settings.ts Normal file
View File

@@ -0,0 +1,129 @@
import { writable } from 'svelte/store';
import { OVERPASS_ENDPOINTS } from './constants';
const STORAGE_KEY = 'surveilled-overpass-endpoint';
const TILE_STORAGE_KEY = 'surveilled-custom-tile-url';
const NOMINATIM_STORAGE_KEY = 'surveilled-nominatim-endpoint';
const BASEMAP_STORAGE_KEY = 'surveilled-basemap';
const FOOTER_COLLAPSED_KEY = 'surveilled-footer-collapsed';
function createOverpassSettings() {
// Default to the first endpoint in the constants
const defaultEndpoint: string = OVERPASS_ENDPOINTS[0];
// Initial value from localStorage if available
let initialEndpoint: string = defaultEndpoint;
if (typeof window !== 'undefined') {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
initialEndpoint = stored;
}
}
const { subscribe, set } = writable<string>(initialEndpoint);
return {
subscribe,
set: (value: string) => {
if (typeof window !== 'undefined') {
localStorage.setItem(STORAGE_KEY, value);
}
set(value);
},
reset: () => {
if (typeof window !== 'undefined') {
localStorage.removeItem(STORAGE_KEY);
}
set(defaultEndpoint);
},
};
}
export const overpassEndpoint = createOverpassSettings();
function createTileSettings() {
let initialUrl = '';
if (typeof window !== 'undefined') {
initialUrl = localStorage.getItem(TILE_STORAGE_KEY) || '';
}
const { subscribe, set } = writable<string>(initialUrl);
return {
subscribe,
set: (value: string) => {
if (typeof window !== 'undefined') {
if (value) localStorage.setItem(TILE_STORAGE_KEY, value);
else localStorage.removeItem(TILE_STORAGE_KEY);
}
set(value);
},
};
}
export const customTileUrl = createTileSettings();
function createNominatimSettings() {
let initialEndpoint = 'https://nominatim.openstreetmap.org/search';
if (typeof window !== 'undefined') {
const stored = localStorage.getItem(NOMINATIM_STORAGE_KEY);
if (stored) initialEndpoint = stored;
}
const { subscribe, set } = writable<string>(initialEndpoint);
return {
subscribe,
set: (value: string) => {
if (typeof window !== 'undefined') {
localStorage.setItem(NOMINATIM_STORAGE_KEY, value);
}
set(value);
},
};
}
export const nominatimEndpoint = createNominatimSettings();
function createBasemapSettings() {
let initialBasemap = 'dark';
if (typeof window !== 'undefined') {
const stored = localStorage.getItem(BASEMAP_STORAGE_KEY);
if (stored) initialBasemap = stored;
}
const { subscribe, set } = writable<string>(initialBasemap);
return {
subscribe,
set: (value: string) => {
if (typeof window !== 'undefined') {
localStorage.setItem(BASEMAP_STORAGE_KEY, value);
}
set(value);
},
};
}
export const basemapPreference = createBasemapSettings();
function createFooterSettings() {
let initialCollapsed = false;
if (typeof window !== 'undefined') {
initialCollapsed = localStorage.getItem(FOOTER_COLLAPSED_KEY) === 'true';
}
const { subscribe, set } = writable<boolean>(initialCollapsed);
return {
subscribe,
set: (value: boolean) => {
if (typeof window !== 'undefined') {
localStorage.setItem(FOOTER_COLLAPSED_KEY, String(value));
}
set(value);
},
};
}
export const footerCollapsedPref = createFooterSettings();

View File

@@ -1 +1,20 @@
export const APP_VERSION = import.meta.env.VITE_APP_VERSION || 'dev';
declare const __APP_VERSION__: string | undefined;
type ProcessLike = {
env?: Record<string, string | undefined>;
};
const definedVersion =
typeof __APP_VERSION__ !== 'undefined' && __APP_VERSION__ ? __APP_VERSION__ : undefined;
const processEnv =
typeof globalThis === 'object' && globalThis !== null
? ((globalThis as unknown as { process?: ProcessLike }).process?.env ?? undefined)
: undefined;
const envVersion =
typeof processEnv?.npm_package_version === 'string' && processEnv.npm_package_version
? processEnv.npm_package_version
: undefined;
export const APP_VERSION = definedVersion ?? envVersion ?? 'dev';

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,18 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import { VitePWA } from 'vite-plugin-pwa';
import pkg from './package.json' with { type: 'json' };
declare const process: {
env: Record<string, string | undefined>;
};
const appVersion = process.env.VITE_APP_VERSION ?? pkg.version ?? 'dev';
export default defineConfig({
define: {
__APP_VERSION__: JSON.stringify(appVersion),
},
plugins: [
sveltekit(),
VitePWA({