Add theme system for customizable backgrounds
- Move backgrounds to themed folders (backgrounds/desert/, backgrounds/custom/) - Add theme config option in config.json (default: "desert") - Add /api/config endpoint to serve theme and location - Update pixel-view.js to load backgrounds from theme folder - Add config.json.example for reference - Update CONFIG.md documentation Users can now set "theme": "custom" and place their own backgrounds in backgrounds/custom/ to customize the view for their location. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
40
CONFIG.md
@@ -15,7 +15,8 @@ Edit `config.json` to customize your installation:
|
|||||||
"lat": 0.0,
|
"lat": 0.0,
|
||||||
"lon": 0.0
|
"lon": 0.0
|
||||||
},
|
},
|
||||||
"web_port": 2001
|
"web_port": 2001,
|
||||||
|
"theme": "desert"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ Edit `config.json` to customize your installation:
|
|||||||
| `location.lat` | number | Latitude of your receiver location |
|
| `location.lat` | number | Latitude of your receiver location |
|
||||||
| `location.lon` | number | Longitude of your receiver location |
|
| `location.lon` | number | Longitude of your receiver location |
|
||||||
| `web_port` | number | Port for the web interface (default: 2001) |
|
| `web_port` | number | Port for the web interface (default: 2001) |
|
||||||
|
| `theme` | string | Background theme: `"desert"` (default) or `"custom"` |
|
||||||
|
|
||||||
### Receiver Configuration Examples
|
### Receiver Configuration Examples
|
||||||
|
|
||||||
@@ -51,12 +53,44 @@ Edit `config.json` to customize your installation:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Background Images
|
## Background Themes
|
||||||
|
|
||||||
Pixel View uses directional background images to show the horizon view from your receiver location. You should customize these to match your actual surroundings.
|
Pixel View uses directional background images to show the horizon view from your receiver location. Backgrounds are organized into themes stored in the `backgrounds/` folder.
|
||||||
|
|
||||||
|
### Available Themes
|
||||||
|
|
||||||
|
| Theme | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `desert` | Las Vegas desert landscape (default) |
|
||||||
|
| `custom` | Your own custom backgrounds |
|
||||||
|
|
||||||
|
### Theme Folder Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
backgrounds/
|
||||||
|
├── desert/ # Default desert theme
|
||||||
|
│ ├── north.png
|
||||||
|
│ ├── east.png
|
||||||
|
│ ├── south.png
|
||||||
|
│ └── west.png
|
||||||
|
└── custom/ # Your custom backgrounds
|
||||||
|
├── README.md # Instructions for creating custom backgrounds
|
||||||
|
├── north.png
|
||||||
|
├── east.png
|
||||||
|
├── south.png
|
||||||
|
└── west.png
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Custom Backgrounds
|
||||||
|
|
||||||
|
1. Add your background images to `backgrounds/custom/`
|
||||||
|
2. Set `"theme": "custom"` in your `config.json`
|
||||||
|
3. Restart the server
|
||||||
|
|
||||||
### Image Files
|
### Image Files
|
||||||
|
|
||||||
|
Each theme folder needs these 4 directional images:
|
||||||
|
|
||||||
| File | Direction | Description |
|
| File | Direction | Description |
|
||||||
|------|-----------|-------------|
|
|------|-----------|-------------|
|
||||||
| `north.png` | North (0°) | View looking north from your location |
|
| `north.png` | North (0°) | View looking north from your location |
|
||||||
|
|||||||
35
backgrounds/custom/README.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Custom Backgrounds
|
||||||
|
|
||||||
|
Place your custom background images in this folder.
|
||||||
|
|
||||||
|
## Required Files
|
||||||
|
|
||||||
|
You need to provide 4 directional background images:
|
||||||
|
- `north.png` - View looking north
|
||||||
|
- `east.png` - View looking east
|
||||||
|
- `south.png` - View looking south
|
||||||
|
- `west.png` - View looking west
|
||||||
|
|
||||||
|
## Image Specifications
|
||||||
|
|
||||||
|
- **Dimensions:** 1536 x 1024 pixels
|
||||||
|
- **Format:** PNG with transparency support
|
||||||
|
- **Orientation:** Each image should show the horizon/landscape as seen when facing that cardinal direction from your location
|
||||||
|
|
||||||
|
## Tips
|
||||||
|
|
||||||
|
- Include a horizon line in each image - the sun and moon will set behind it
|
||||||
|
- Keep the upper portion (sky area) relatively simple for aircraft visibility
|
||||||
|
- The bottom portion can have more detail (terrain, buildings, etc.)
|
||||||
|
- Consider your local landmarks and terrain for each direction
|
||||||
|
|
||||||
|
## Enabling Custom Theme
|
||||||
|
|
||||||
|
Set the theme in `config.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"theme": "custom"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then restart the server.
|
||||||
|
Before Width: | Height: | Size: 2.2 MiB After Width: | Height: | Size: 2.2 MiB |
|
Before Width: | Height: | Size: 2.4 MiB After Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 2.4 MiB After Width: | Height: | Size: 2.4 MiB |
@@ -6,5 +6,6 @@
|
|||||||
"lat": 0.0,
|
"lat": 0.0,
|
||||||
"lon": 0.0
|
"lon": 0.0
|
||||||
},
|
},
|
||||||
"web_port": 2001
|
"web_port": 2001,
|
||||||
|
"theme": "desert"
|
||||||
}
|
}
|
||||||
|
|||||||
11
config.json.example
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"receivers": "AUTO",
|
||||||
|
"receiver_port": 30003,
|
||||||
|
"location": {
|
||||||
|
"name": "My Location",
|
||||||
|
"lat": 0.0,
|
||||||
|
"lon": 0.0
|
||||||
|
},
|
||||||
|
"web_port": 2001,
|
||||||
|
"theme": "desert"
|
||||||
|
}
|
||||||
@@ -65,6 +65,9 @@ class PixelADSB {
|
|||||||
this.viewDirectionNames = { 0: 'N', 90: 'E', 180: 'S', 270: 'W' };
|
this.viewDirectionNames = { 0: 'N', 90: 'E', 180: 'S', 270: 'W' };
|
||||||
this.fieldOfView = 90; // 90 degree field of view
|
this.fieldOfView = 90; // 90 degree field of view
|
||||||
|
|
||||||
|
// Theme for background images (loaded from config)
|
||||||
|
this.theme = 'desert';
|
||||||
|
|
||||||
// Hover and selection tracking
|
// Hover and selection tracking
|
||||||
this.mouseX = -1;
|
this.mouseX = -1;
|
||||||
this.mouseY = -1;
|
this.mouseY = -1;
|
||||||
@@ -109,7 +112,7 @@ class PixelADSB {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Load environment images (directional backgrounds, base, sun, clouds)
|
// Load environment images (directional backgrounds, base, sun, clouds)
|
||||||
// Directional background images
|
// Directional background images (loaded after fetching config)
|
||||||
this.backgroundImages = {
|
this.backgroundImages = {
|
||||||
0: new Image(), // North
|
0: new Image(), // North
|
||||||
90: new Image(), // East
|
90: new Image(), // East
|
||||||
@@ -118,24 +121,6 @@ class PixelADSB {
|
|||||||
};
|
};
|
||||||
this.backgroundImagesLoaded = { 0: false, 90: false, 180: false, 270: false };
|
this.backgroundImagesLoaded = { 0: false, 90: false, 180: false, 270: false };
|
||||||
|
|
||||||
// Load directional backgrounds (fallback to desert.png if not available)
|
|
||||||
const directions = [
|
|
||||||
{ deg: 0, name: 'north' },
|
|
||||||
{ deg: 90, name: 'east' },
|
|
||||||
{ deg: 180, name: 'south' },
|
|
||||||
{ deg: 270, name: 'west' }
|
|
||||||
];
|
|
||||||
directions.forEach(dir => {
|
|
||||||
this.backgroundImages[dir.deg].onload = () => {
|
|
||||||
this.backgroundImagesLoaded[dir.deg] = true;
|
|
||||||
console.log(`${dir.name}.png loaded`);
|
|
||||||
};
|
|
||||||
this.backgroundImages[dir.deg].onerror = () => {
|
|
||||||
console.warn(`Failed to load ${dir.name}.png, using fallback`);
|
|
||||||
};
|
|
||||||
this.backgroundImages[dir.deg].src = `${dir.name}.png?v=1`;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.sunImage = new Image();
|
this.sunImage = new Image();
|
||||||
this.sunImage.onload = () => {
|
this.sunImage.onload = () => {
|
||||||
this.sunImageLoaded = true;
|
this.sunImageLoaded = true;
|
||||||
@@ -410,8 +395,11 @@ class PixelADSB {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
// Fetch receiver location
|
// Fetch config (includes theme and location)
|
||||||
await this.fetchReceiverLocation();
|
await this.fetchConfig();
|
||||||
|
|
||||||
|
// Load background images based on theme
|
||||||
|
this.loadBackgroundImages();
|
||||||
|
|
||||||
// Fetch weather
|
// Fetch weather
|
||||||
await this.fetchWeather();
|
await this.fetchWeather();
|
||||||
@@ -547,6 +535,41 @@ class PixelADSB {
|
|||||||
return padding + (normalizedAngle * usableWidth);
|
return padding + (normalizedAngle * usableWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fetchConfig() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/config');
|
||||||
|
const data = await response.json();
|
||||||
|
this.theme = data.theme || 'desert';
|
||||||
|
this.receiverLat = data.location?.lat || 0;
|
||||||
|
this.receiverLon = data.location?.lon || 0;
|
||||||
|
this.locationName = data.location?.name || 'My Location';
|
||||||
|
console.log(`Config loaded - Theme: ${this.theme}, Location: ${this.locationName} (${this.receiverLat}, ${this.receiverLon})`);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Could not fetch config, using defaults');
|
||||||
|
// Fallback to receiver-location API for backwards compatibility
|
||||||
|
await this.fetchReceiverLocation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadBackgroundImages() {
|
||||||
|
const directions = [
|
||||||
|
{ deg: 0, name: 'north' },
|
||||||
|
{ deg: 90, name: 'east' },
|
||||||
|
{ deg: 180, name: 'south' },
|
||||||
|
{ deg: 270, name: 'west' }
|
||||||
|
];
|
||||||
|
directions.forEach(dir => {
|
||||||
|
this.backgroundImages[dir.deg].onload = () => {
|
||||||
|
this.backgroundImagesLoaded[dir.deg] = true;
|
||||||
|
console.log(`${this.theme}/${dir.name}.png loaded`);
|
||||||
|
};
|
||||||
|
this.backgroundImages[dir.deg].onerror = () => {
|
||||||
|
console.warn(`Failed to load backgrounds/${this.theme}/${dir.name}.png`);
|
||||||
|
};
|
||||||
|
this.backgroundImages[dir.deg].src = `backgrounds/${this.theme}/${dir.name}.png?v=1`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async fetchReceiverLocation() {
|
async fetchReceiverLocation() {
|
||||||
try {
|
try {
|
||||||
// Fetch from same server that serves pixel-view
|
// Fetch from same server that serves pixel-view
|
||||||
|
|||||||
12
server.py
@@ -24,7 +24,8 @@ config = {
|
|||||||
"lat": 0.0,
|
"lat": 0.0,
|
||||||
"lon": 0.0
|
"lon": 0.0
|
||||||
},
|
},
|
||||||
"web_port": 2001
|
"web_port": 2001,
|
||||||
|
"theme": "desert"
|
||||||
}
|
}
|
||||||
|
|
||||||
def load_config():
|
def load_config():
|
||||||
@@ -45,6 +46,7 @@ def load_config():
|
|||||||
print(f" Receiver port: {config['receiver_port']}")
|
print(f" Receiver port: {config['receiver_port']}")
|
||||||
print(f" Location: {config['location']['name']} ({config['location']['lat']}, {config['location']['lon']})")
|
print(f" Location: {config['location']['name']} ({config['location']['lat']}, {config['location']['lon']})")
|
||||||
print(f" Web port: {config['web_port']}")
|
print(f" Web port: {config['web_port']}")
|
||||||
|
print(f" Theme: {config.get('theme', 'desert')}")
|
||||||
|
|
||||||
# Flight data storage
|
# Flight data storage
|
||||||
flights: Dict[str, dict] = {}
|
flights: Dict[str, dict] = {}
|
||||||
@@ -302,6 +304,13 @@ async def handle_receiver_location(request):
|
|||||||
"name": config["location"]["name"]
|
"name": config["location"]["name"]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
async def handle_config(request):
|
||||||
|
"""Return client-relevant configuration"""
|
||||||
|
return web.json_response({
|
||||||
|
"theme": config.get("theme", "desert"),
|
||||||
|
"location": config["location"]
|
||||||
|
})
|
||||||
|
|
||||||
async def handle_http(request):
|
async def handle_http(request):
|
||||||
"""Serve static files"""
|
"""Serve static files"""
|
||||||
path = request.path
|
path = request.path
|
||||||
@@ -319,6 +328,7 @@ async def start_http_server():
|
|||||||
app = web.Application()
|
app = web.Application()
|
||||||
app.router.add_get('/ws', websocket_handler)
|
app.router.add_get('/ws', websocket_handler)
|
||||||
app.router.add_get('/api/receiver-location', handle_receiver_location)
|
app.router.add_get('/api/receiver-location', handle_receiver_location)
|
||||||
|
app.router.add_get('/api/config', handle_config)
|
||||||
app.router.add_get('/{tail:.*}', handle_http)
|
app.router.add_get('/{tail:.*}', handle_http)
|
||||||
|
|
||||||
runner = web.AppRunner(app)
|
runner = web.AppRunner(app)
|
||||||
|
|||||||