Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
f92c8fced7
|
|||
|
f80919c45f
|
|||
|
ad4fa4f93a
|
|||
|
66a1933cfb
|
|||
|
620e63b7ba
|
|||
|
9918638cb8
|
|||
|
165f06e06b
|
|||
|
405a254824
|
|||
|
0bb79ff612
|
|||
|
55a90bd146
|
|||
|
7dcfb1ff7c
|
|||
|
6b35ab80a2
|
|||
|
d9d97db0f9
|
|||
|
6796d85a6a
|
|||
|
6f1428b8e8
|
|||
|
1b38193ca3
|
|||
|
f258254adf
|
|||
|
61c47c2d60
|
|||
|
5b31ef951d
|
|||
|
3c574b58f6
|
|||
|
07d5c540b6
|
|||
|
b826ee8f4f
|
|||
|
2df526c606
|
|||
|
26f91f1aee
|
|||
|
85febfacc1
|
|||
|
0eab72db49
|
|||
|
dffd3ff838
|
|||
|
f5407e56e1
|
|||
|
|
8adc0aa85d | ||
|
|
2e7780e711 | ||
|
c0dd901def
|
|||
|
2d7efb03fb
|
|||
|
191ed6b0e1
|
|||
|
57187e7ed4
|
@@ -21,3 +21,7 @@ Thumbs.db
|
||||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
# Package
|
||||
dist/
|
||||
|
||||
|
||||
66
.gitea/workflows/docker.yml
Normal file
66
.gitea/workflows/docker.yml
Normal 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 }}
|
||||
36
.gitea/workflows/npm-publish.yml
Normal file
36
.gitea/workflows/npm-publish.yml
Normal 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
3
.gitignore
vendored
@@ -21,3 +21,6 @@ Thumbs.db
|
||||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
# Package
|
||||
dist/
|
||||
|
||||
3
.npmrc
3
.npmrc
@@ -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}
|
||||
|
||||
22
Makefile
22
Makefile
@@ -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
|
||||
|
||||
|
||||
70
README.md
70
README.md
@@ -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)
|
||||
|
||||
@@ -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
91
package-lock.json
generated
@@ -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",
|
||||
|
||||
25
package.json
25
package.json
@@ -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
BIN
showcase/surveilled.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 837 KiB |
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
129
src/lib/settings.ts
Normal 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();
|
||||
@@ -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';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user