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,
|
||||
"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.lon` | number | Longitude of your receiver location |
|
||||
| `web_port` | number | Port for the web interface (default: 2001) |
|
||||
| `theme` | string | Background theme: `"desert"` (default) or `"custom"` |
|
||||
|
||||
### 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
|
||||
|
||||
Each theme folder needs these 4 directional images:
|
||||
|
||||
| File | Direction | Description |
|
||||
|------|-----------|-------------|
|
||||
| `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,
|
||||
"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.fieldOfView = 90; // 90 degree field of view
|
||||
|
||||
// Theme for background images (loaded from config)
|
||||
this.theme = 'desert';
|
||||
|
||||
// Hover and selection tracking
|
||||
this.mouseX = -1;
|
||||
this.mouseY = -1;
|
||||
@@ -109,7 +112,7 @@ class PixelADSB {
|
||||
});
|
||||
|
||||
// Load environment images (directional backgrounds, base, sun, clouds)
|
||||
// Directional background images
|
||||
// Directional background images (loaded after fetching config)
|
||||
this.backgroundImages = {
|
||||
0: new Image(), // North
|
||||
90: new Image(), // East
|
||||
@@ -118,24 +121,6 @@ class PixelADSB {
|
||||
};
|
||||
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.onload = () => {
|
||||
this.sunImageLoaded = true;
|
||||
@@ -410,8 +395,11 @@ class PixelADSB {
|
||||
}
|
||||
|
||||
async init() {
|
||||
// Fetch receiver location
|
||||
await this.fetchReceiverLocation();
|
||||
// Fetch config (includes theme and location)
|
||||
await this.fetchConfig();
|
||||
|
||||
// Load background images based on theme
|
||||
this.loadBackgroundImages();
|
||||
|
||||
// Fetch weather
|
||||
await this.fetchWeather();
|
||||
@@ -547,6 +535,41 @@ class PixelADSB {
|
||||
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() {
|
||||
try {
|
||||
// Fetch from same server that serves pixel-view
|
||||
|
||||
12
server.py
@@ -24,7 +24,8 @@ config = {
|
||||
"lat": 0.0,
|
||||
"lon": 0.0
|
||||
},
|
||||
"web_port": 2001
|
||||
"web_port": 2001,
|
||||
"theme": "desert"
|
||||
}
|
||||
|
||||
def load_config():
|
||||
@@ -45,6 +46,7 @@ def load_config():
|
||||
print(f" Receiver port: {config['receiver_port']}")
|
||||
print(f" Location: {config['location']['name']} ({config['location']['lat']}, {config['location']['lon']})")
|
||||
print(f" Web port: {config['web_port']}")
|
||||
print(f" Theme: {config.get('theme', 'desert')}")
|
||||
|
||||
# Flight data storage
|
||||
flights: Dict[str, dict] = {}
|
||||
@@ -302,6 +304,13 @@ async def handle_receiver_location(request):
|
||||
"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):
|
||||
"""Serve static files"""
|
||||
path = request.path
|
||||
@@ -319,6 +328,7 @@ async def start_http_server():
|
||||
app = web.Application()
|
||||
app.router.add_get('/ws', websocket_handler)
|
||||
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)
|
||||
|
||||
runner = web.AppRunner(app)
|
||||
|
||||