Fixing loading in docker environment, updated markers
This commit is contained in:
39
.env
39
.env
@@ -1,21 +1,30 @@
|
|||||||
RUST_LOG=warn,api=info
|
RUST_LOG=warn,api=info
|
||||||
|
|
||||||
|
HTTPD_DOMAIN=localhost
|
||||||
|
HTTPD_PROTOCOL=http
|
||||||
|
HTTPD_HTTP_PORT=8080
|
||||||
|
HTTPD_HTTPS_PORT=8443
|
||||||
|
HTTPD_MINIO_HOST=host.docker.internal
|
||||||
|
HTTPD_API_HOST=host.docker.internal
|
||||||
|
HTTPD_UI_HOST=host.docker.internal
|
||||||
|
|
||||||
|
POSTGRES_HOST=localhost
|
||||||
POSTGRES_USER=aviation
|
POSTGRES_USER=aviation
|
||||||
POSTGRES_PASSWORD=CHANGEME
|
POSTGRES_PASSWORD=CHANGEME
|
||||||
POSTGRES_NAME=aviation
|
POSTGRES_NAME=aviation
|
||||||
POSTGRES_PORT=5432
|
POSTGRES_PORT=5432
|
||||||
|
|
||||||
|
REDIS_HOST=localhost
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
|
|
||||||
|
MINIO_HOST=localhost
|
||||||
MINIO_ROOT_USER=aviation
|
MINIO_ROOT_USER=aviation
|
||||||
MINIO_ROOT_PASSWORD=CHANGEME
|
MINIO_ROOT_PASSWORD=CHANGEME
|
||||||
MINIO_SCHEMA=http
|
MINIO_BUCKET=aviation
|
||||||
|
MINIO_PROTOCOL=http
|
||||||
MINIO_PORT=9000
|
MINIO_PORT=9000
|
||||||
MINIO_PORT_INTERNAL=9001
|
MINIO_PORT_INTERNAL=9001
|
||||||
|
MINIO_BROWSER_REDIRECT_URL=${HTTPD_PROTOCOL}://${HTTPD_DOMAIN}:${HTTPD_HTTP_PORT}/minio/
|
||||||
HTTPD_PROTOCOL=http
|
|
||||||
HTTPD_HTTP_PORT=8080
|
|
||||||
HTTPD_HTTPS_PORT=8443
|
|
||||||
|
|
||||||
UI_PROTOCOL=http
|
UI_PROTOCOL=http
|
||||||
UI_PORT=3000
|
UI_PORT=3000
|
||||||
@@ -23,25 +32,7 @@ UI_PORT=3000
|
|||||||
API_PROTOCOL=http
|
API_PROTOCOL=http
|
||||||
API_PORT=5000
|
API_PORT=5000
|
||||||
|
|
||||||
#################################
|
VITE_API_URL=${HTTPD_PROTOCOL}://${HTTPD_DOMAIN}:${HTTPD_HTTP_PORT}/api
|
||||||
# Development (Running Locally) #
|
|
||||||
#################################
|
|
||||||
POSTGRES_HOST=localhost
|
|
||||||
REDIS_HOST=localhost
|
|
||||||
MINIO_HOST=localhost
|
|
||||||
HTTPD_HOST=localhost
|
|
||||||
HTTPD_API_HOST=host.docker.internal
|
|
||||||
HTTPD_UI_HOST=host.docker.internal
|
|
||||||
VITE_API_URL=http://localhost:8080/api
|
|
||||||
|
|
||||||
##################
|
|
||||||
# Running Docker #
|
|
||||||
##################
|
|
||||||
#POSTGRES_HOST=aviation-postgres
|
|
||||||
#REDIS_HOST=aviation-redis
|
|
||||||
#MINIO_HOST=aviation-redis
|
|
||||||
#HTTPD_API_HOST=aviation-api
|
|
||||||
#HTTPD_UI_HOST=aviation-ui
|
|
||||||
|
|
||||||
ENVIRONMENT=development
|
ENVIRONMENT=development
|
||||||
ADMIN_EMAIL=admin@example.com
|
ADMIN_EMAIL=admin@example.com
|
||||||
|
|||||||
18
Makefile
18
Makefile
@@ -93,5 +93,19 @@ docker-up: ## Start the docker container
|
|||||||
|
|
||||||
docker-refresh: docker-clean up-backend ## Refresh the database
|
docker-refresh: docker-clean up-backend ## Refresh the database
|
||||||
|
|
||||||
docker-build: ## Build the docker images
|
refresh: docker-refresh
|
||||||
@docker compose --profile backend --profile api build
|
|
||||||
|
build: version=$(if $(v),$(v),latest)
|
||||||
|
build: folder=$(if $(f),$(f),httpd)
|
||||||
|
build: image=aviation-${folder}:${version}
|
||||||
|
build: ## Build a specific docker image (`make build f=httpd`)
|
||||||
|
docker buildx build \
|
||||||
|
-f ${folder}/Dockerfile \
|
||||||
|
-t ${image} \
|
||||||
|
--load \
|
||||||
|
--build-arg BUILD_DATE=$$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
|
||||||
|
--build-arg BUILD_VERSION=${version} \
|
||||||
|
--build-arg VCS_REF=$$(git rev-parse head) \
|
||||||
|
${folder}
|
||||||
|
|
||||||
|
docker-build: build
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
use crate::error::ApiResult;
|
use crate::error::ApiResult;
|
||||||
use redis::{Client as RedisClient, aio::MultiplexedConnection as RedisConnection, RedisResult};
|
use redis::{Client as RedisClient, aio::MultiplexedConnection as RedisConnection, RedisResult};
|
||||||
use s3::{
|
use s3::{Bucket, Region, creds::Credentials, BucketConfiguration, request::ResponseData};
|
||||||
Bucket, Region, creds::Credentials, BucketConfiguration, request::ResponseData,
|
|
||||||
bucket_ops::CreateBucketResponse,
|
|
||||||
};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -15,30 +12,33 @@ static REDIS: OnceLock<RedisClient> = OnceLock::new();
|
|||||||
static BUCKET: OnceLock<Bucket> = OnceLock::new();
|
static BUCKET: OnceLock<Bucket> = OnceLock::new();
|
||||||
|
|
||||||
pub async fn initialize() -> ApiResult<()> {
|
pub async fn initialize() -> ApiResult<()> {
|
||||||
let db_user = std::env::var("POSTGRES_USER").unwrap_or("aviation".to_string());
|
// Setup Postgres pool connection
|
||||||
let db_password = std::env::var("POSTGRES_PASSWORD").expect("POSTGRES_PASSWORD must be set");
|
let pool = {
|
||||||
let db_host: String = std::env::var("POSTGRES_HOST").expect("POSTGRES_HOST must be set");
|
let user = std::env::var("POSTGRES_USER").unwrap_or("aviation".to_string());
|
||||||
let db_port = std::env::var("POSTGRES_PORT").unwrap_or("5432".to_string());
|
let password = std::env::var("POSTGRES_PASSWORD").expect("POSTGRES_PASSWORD must be set");
|
||||||
let db_name = std::env::var("POSTGRES_NAME").unwrap_or("aviation".to_string());
|
let host: String = std::env::var("POSTGRES_HOST").expect("POSTGRES_HOST must be set");
|
||||||
|
let port = std::env::var("POSTGRES_PORT").unwrap_or("5432".to_string());
|
||||||
|
let name = std::env::var("POSTGRES_NAME").unwrap_or("aviation".to_string());
|
||||||
|
|
||||||
let db_url = format!(
|
let db_url = format!(
|
||||||
"postgres://{}:{}@{}:{}/{}",
|
"postgres://{}:{}@{}:{}/{}",
|
||||||
&db_user, &db_password, &db_host, &db_port, &db_name
|
&user, &password, &host, &port, &name
|
||||||
);
|
);
|
||||||
|
|
||||||
log::info!(
|
log::info!(
|
||||||
"Connecting to database at postgres://{}:*****@{}:{}/{}...",
|
"Connecting to database at postgres://{}:*****@{}:{}/{}...",
|
||||||
&db_user,
|
&user,
|
||||||
&db_host,
|
&host,
|
||||||
&db_port,
|
&port,
|
||||||
&db_name
|
&name
|
||||||
);
|
);
|
||||||
// Setup Postgres pool connection
|
|
||||||
let pool = PgPoolOptions::new()
|
PgPoolOptions::new()
|
||||||
.max_connections(5)
|
.max_connections(5)
|
||||||
.acquire_timeout(Duration::from_secs(30))
|
.acquire_timeout(Duration::from_secs(30))
|
||||||
.connect(&db_url)
|
.connect(&db_url)
|
||||||
.await?;
|
.await?
|
||||||
|
};
|
||||||
match POOL.set(pool) {
|
match POOL.set(pool) {
|
||||||
Ok(_) => log::info!("Database connection established"),
|
Ok(_) => log::info!("Database connection established"),
|
||||||
Err(_) => log::warn!("Database pool already initialized"),
|
Err(_) => log::warn!("Database pool already initialized"),
|
||||||
@@ -49,6 +49,7 @@ pub async fn initialize() -> ApiResult<()> {
|
|||||||
let host = std::env::var("REDIS_HOST").unwrap_or("localhost".to_string());
|
let host = std::env::var("REDIS_HOST").unwrap_or("localhost".to_string());
|
||||||
let port = std::env::var("REDIS_PORT").unwrap_or("6379".to_string());
|
let port = std::env::var("REDIS_PORT").unwrap_or("6379".to_string());
|
||||||
let url = format!("redis://{}:{}", host, port);
|
let url = format!("redis://{}:{}", host, port);
|
||||||
|
log::info!("Connecting to redis at {}", &url);
|
||||||
RedisClient::open(url).expect("Failed to create redis client")
|
RedisClient::open(url).expect("Failed to create redis client")
|
||||||
};
|
};
|
||||||
match REDIS.set(redis) {
|
match REDIS.set(redis) {
|
||||||
@@ -56,16 +57,19 @@ pub async fn initialize() -> ApiResult<()> {
|
|||||||
Err(_) => log::warn!("Redis client already initialized"),
|
Err(_) => log::warn!("Redis client already initialized"),
|
||||||
}
|
}
|
||||||
|
|
||||||
let schema = std::env::var("MINIO_SCHEMA").unwrap_or("http".to_string());
|
// Setup Bucket connection
|
||||||
let url = std::env::var("MINIO_HOST").unwrap_or("localhost".to_string());
|
let bucket = {
|
||||||
|
let protocol = std::env::var("MINIO_PROTOCOL").unwrap_or("http".to_string());
|
||||||
|
let host = std::env::var("MINIO_HOST").unwrap_or("localhost".to_string());
|
||||||
let port = std::env::var("MINIO_PORT").unwrap_or("9000".to_string());
|
let port = std::env::var("MINIO_PORT").unwrap_or("9000".to_string());
|
||||||
let user = std::env::var("MINIO_ROOT_USER").expect("MINIO_ROOT_USER is not set");
|
let user = std::env::var("MINIO_ROOT_USER").expect("MINIO_ROOT_USER is not set");
|
||||||
let password = std::env::var("MINIO_ROOT_PASSWORD").expect("MINIO_ROOT_PASSWORD is not set");
|
let password = std::env::var("MINIO_ROOT_PASSWORD").expect("MINIO_ROOT_PASSWORD is not set");
|
||||||
let base_url = format!("{}://{}:{}", schema, url, port);
|
let bucket_name = std::env::var("MINIO_BUCKET").unwrap_or("aviation".to_string());
|
||||||
|
let url = format!("{}://{}:{}", protocol, host, port);
|
||||||
|
|
||||||
let region = Region::Custom {
|
let region = Region::Custom {
|
||||||
region: "".to_string(),
|
region: "".to_string(),
|
||||||
endpoint: base_url,
|
endpoint: url.to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let credentials = Credentials {
|
let credentials = Credentials {
|
||||||
@@ -76,13 +80,34 @@ pub async fn initialize() -> ApiResult<()> {
|
|||||||
expiration: None,
|
expiration: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let bucket = Bucket::new("aviation", region.clone(), credentials.clone())
|
let bucket = Bucket::new(&bucket_name, region.clone(), credentials.clone())?.with_path_style();
|
||||||
.expect("Failed to create S3 Bucket")
|
log::info!("Checking for object in bucket at {}", ®ion.endpoint());
|
||||||
.with_path_style();
|
match bucket.head_object("/").await {
|
||||||
|
Ok(_) => bucket,
|
||||||
|
Err(_) => {
|
||||||
|
log::debug!("Creating '{}' bucket", &bucket_name);
|
||||||
|
let response = match Bucket::create_with_path_style(
|
||||||
|
&bucket_name,
|
||||||
|
region,
|
||||||
|
credentials,
|
||||||
|
BucketConfiguration::default(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(response) => response,
|
||||||
|
Err(err) => {
|
||||||
|
log::error!("Failed to create bucket '{}': {}", &bucket_name, err);
|
||||||
|
return Err(err.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
response.bucket
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
match BUCKET.set(*bucket) {
|
match BUCKET.set(*bucket) {
|
||||||
Ok(_) => log::info!("Bucket initialized"),
|
Ok(_) => log::info!("Bucket connection initialized"),
|
||||||
Err(_) => log::warn!("Bucket client already initialized"),
|
Err(_) => log::warn!("Bucket connection already initialized"),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run migrations
|
// Run migrations
|
||||||
@@ -115,42 +140,12 @@ pub async fn redis_async_connection() -> RedisResult<RedisConnection> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn run_migrations() -> ApiResult<()> {
|
async fn run_migrations() -> ApiResult<()> {
|
||||||
log::debug!("Running migrations");
|
log::debug!("Running database migrations");
|
||||||
let pool = pool();
|
let pool = pool();
|
||||||
sqlx::migrate!().run(pool).await?;
|
sqlx::migrate!().run(pool).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_bucket() -> ApiResult<CreateBucketResponse> {
|
|
||||||
let url = std::env::var("MINIO_URL").unwrap_or("localhost".to_string());
|
|
||||||
let port = std::env::var("MINIO_PORT").unwrap_or("9000".to_string());
|
|
||||||
let user = std::env::var("MINIO_ROOT_USER").expect("MINIO_ROOT_USER is not set");
|
|
||||||
let password = std::env::var("MINIO_ROOT_PASSWORD").expect("MINIO_ROOT_PASSWORD is not set");
|
|
||||||
let base_url = format!("http://{}:{}", url, port);
|
|
||||||
|
|
||||||
let region = Region::Custom {
|
|
||||||
region: "".to_string(),
|
|
||||||
endpoint: base_url,
|
|
||||||
};
|
|
||||||
|
|
||||||
let credentials = Credentials {
|
|
||||||
access_key: Some(user),
|
|
||||||
secret_key: Some(password),
|
|
||||||
security_token: None,
|
|
||||||
session_token: None,
|
|
||||||
expiration: None,
|
|
||||||
};
|
|
||||||
let bucket_name = "aviation";
|
|
||||||
let response = Bucket::create_with_path_style(
|
|
||||||
bucket_name,
|
|
||||||
region,
|
|
||||||
credentials,
|
|
||||||
BucketConfiguration::default(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn upload_file(path: &str, content: &[u8]) -> ApiResult<ResponseData> {
|
pub async fn upload_file(path: &str, content: &[u8]) -> ApiResult<ResponseData> {
|
||||||
let response = BUCKET.get().unwrap().put_object(path, content).await?;
|
let response = BUCKET.get().unwrap().put_object(path, content).await?;
|
||||||
Ok(response)
|
Ok(response)
|
||||||
|
|||||||
@@ -935,8 +935,6 @@ impl Metar {
|
|||||||
// Check for missing metars
|
// Check for missing metars
|
||||||
let missing_icao_list = Self::get_missing_metar_icaos(&metars, icao_list).await;
|
let missing_icao_list = Self::get_missing_metar_icaos(&metars, icao_list).await;
|
||||||
if !missing_icao_list.is_empty() {
|
if !missing_icao_list.is_empty() {
|
||||||
log::trace!("Retrieving missing METAR data for {:?}", missing_icao_list);
|
|
||||||
|
|
||||||
let mut updated_missing_icao_list: Vec<&str> = Vec::new();
|
let mut updated_missing_icao_list: Vec<&str> = Vec::new();
|
||||||
for icao in &missing_icao_list {
|
for icao in &missing_icao_list {
|
||||||
let result: RedisResult<Option<bool>> = conn.get(icao).await;
|
let result: RedisResult<Option<bool>> = conn.get(icao).await;
|
||||||
@@ -952,6 +950,11 @@ impl Metar {
|
|||||||
Err(err) => return Err(err.into()),
|
Err(err) => return Err(err.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !updated_missing_icao_list.is_empty() {
|
||||||
|
log::trace!(
|
||||||
|
"Retrieving missing METAR data for {:?}",
|
||||||
|
updated_missing_icao_list
|
||||||
|
);
|
||||||
let mut missing_icao_list = Self::get_remote_metars(&updated_missing_icao_list)
|
let mut missing_icao_list = Self::get_remote_metars(&updated_missing_icao_list)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_else(|err| {
|
.unwrap_or_else(|err| {
|
||||||
@@ -977,6 +980,7 @@ impl Metar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(metars)
|
Ok(metars)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,11 @@ x-env_file: &env
|
|||||||
name: aviation
|
name: aviation
|
||||||
services:
|
services:
|
||||||
httpd:
|
httpd:
|
||||||
build: ./httpd
|
image: aviation-httpd:latest
|
||||||
container_name: aviation-httpd
|
container_name: aviation-httpd
|
||||||
|
build:
|
||||||
|
context: ./httpd
|
||||||
|
dockerfile: Dockerfile
|
||||||
env_file: *env
|
env_file: *env
|
||||||
ports:
|
ports:
|
||||||
- "${HTTPD_HTTP_PORT:-8080}:80"
|
- "${HTTPD_HTTP_PORT:-8080}:80"
|
||||||
@@ -61,6 +64,8 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
|
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
|
||||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
|
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
|
||||||
|
MINIO_BROWSER_REDIRECT_URL: ${MINIO_BROWSER_REDIRECT_URL}
|
||||||
|
MINIO_BROWSER_LOGIN_ANIMATION: false
|
||||||
volumes:
|
volumes:
|
||||||
- minio:/data
|
- minio:/data
|
||||||
ports:
|
ports:
|
||||||
@@ -74,13 +79,21 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
api:
|
api:
|
||||||
|
image: aviation-api:latest
|
||||||
container_name: aviation-api
|
container_name: aviation-api
|
||||||
env_file: *env
|
|
||||||
ports:
|
|
||||||
- "${API_PORT:-5000}:5000"
|
|
||||||
build:
|
build:
|
||||||
context: ./api
|
context: ./api
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
env_file: *env
|
||||||
|
environment:
|
||||||
|
POSTGRES_HOST: aviation-postgres
|
||||||
|
POSTGRES_PORT: 5432
|
||||||
|
REDIS_HOST: aviation-redis
|
||||||
|
REDIS_PORT: 6379
|
||||||
|
MINIO_HOST: aviation-minio
|
||||||
|
MINIO_PORT: 9000
|
||||||
|
ports:
|
||||||
|
- "${API_PORT:-5000}:5000"
|
||||||
depends_on:
|
depends_on:
|
||||||
- postgres
|
- postgres
|
||||||
- redis
|
- redis
|
||||||
@@ -93,15 +106,16 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
ui:
|
ui:
|
||||||
|
image: aviation-ui:latest
|
||||||
container_name: aviation-ui
|
container_name: aviation-ui
|
||||||
|
build:
|
||||||
|
context: ./ui
|
||||||
|
dockerfile: Dockerfile
|
||||||
env_file: *env
|
env_file: *env
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=${NODE_ENV:-development}
|
- NODE_ENV=${NODE_ENV:-development}
|
||||||
ports:
|
ports:
|
||||||
- "${UI_PORT:-3000}:3000"
|
- "${UI_PORT:-3000}:3000"
|
||||||
build:
|
|
||||||
context: ./ui
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./ui/src:/app/src
|
- ./ui/src:/app/src
|
||||||
- ./ui/public:/app/public
|
- ./ui/public:/app/public
|
||||||
|
|||||||
@@ -7,6 +7,9 @@
|
|||||||
ProxyPass "/api" "${API_PROTOCOL}://${HTTPD_API_HOST}:${API_PORT}/api"
|
ProxyPass "/api" "${API_PROTOCOL}://${HTTPD_API_HOST}:${API_PORT}/api"
|
||||||
ProxyPassReverse "/api" "${API_PROTOCOL}://${HTTPD_API_HOST}:${API_PORT}/api"
|
ProxyPassReverse "/api" "${API_PROTOCOL}://${HTTPD_API_HOST}:${API_PORT}/api"
|
||||||
|
|
||||||
|
ProxyPass "/minio" "${MINIO_PROTOCOL}://${HTTPD_MINIO_HOST}:${MINIO_PORT_INTERNAL}"
|
||||||
|
ProxyPassReverse "/minio" "${MINIO_PROTOCOL}://${HTTPD_MINIO_HOST}:${MINIO_PORT_INTERNAL}"
|
||||||
|
|
||||||
ProxyPass "/" "${UI_PROTOCOL}://${HTTPD_UI_HOST}:${UI_PORT}/"
|
ProxyPass "/" "${UI_PROTOCOL}://${HTTPD_UI_HOST}:${UI_PORT}/"
|
||||||
ProxyPassReverse "/" "${UI_PROTOCOL}://${HTTPD_UI_HOST}:${UI_PORT}/"
|
ProxyPassReverse "/" "${UI_PROTOCOL}://${HTTPD_UI_HOST}:${UI_PORT}/"
|
||||||
</VirtualHost>
|
</VirtualHost>
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
<link rel="stylesheet" href="styles/global.css">
|
<link rel="stylesheet" href="styles/global.css">
|
||||||
<link rel="stylesheet" href="styles/leaflet.css">
|
<link rel="stylesheet" href="styles/leaflet.css">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Aviation</title>
|
<title>Aviation Data</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --port 3000",
|
"dev": "vite --host --port 3000",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ L.Icon.Default.mergeOptions({
|
|||||||
shadowUrl: markerShadow
|
shadowUrl: markerShadow
|
||||||
});
|
});
|
||||||
|
|
||||||
const tileLayerUrl = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png';
|
const openStreetMapUrl = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png';
|
||||||
|
// const rainViewerUrl = 'https://tilecache.rainviewer.com/v2/radar/{time}/256/10/290/391/2/1_1.png'
|
||||||
|
// https://api.rainviewer.com/public/weather-maps.json
|
||||||
const defaultZoom = 6;
|
const defaultZoom = 6;
|
||||||
const defaultCenter: L.LatLngExpression = [38.944444, -77.455833];
|
const defaultCenter: L.LatLngExpression = [38.944444, -77.455833];
|
||||||
|
|
||||||
@@ -42,14 +44,14 @@ function App() {
|
|||||||
minZoom={3}
|
minZoom={3}
|
||||||
maxZoom={19}
|
maxZoom={19}
|
||||||
maxBounds={[
|
maxBounds={[
|
||||||
[-85.06, -180],
|
[-85.06, -181],
|
||||||
[85.06, 180]
|
[85.06, 181]
|
||||||
]}
|
]}
|
||||||
scrollWheelZoom={true}
|
scrollWheelZoom={true}
|
||||||
zoomControl={false}
|
zoomControl={false}
|
||||||
>
|
>
|
||||||
<ZoomControl position={'bottomright'} />
|
<ZoomControl position={'bottomright'} />
|
||||||
<TileLayer url={tileLayerUrl} />
|
<TileLayer url={openStreetMapUrl} />
|
||||||
<AirportLayer setAirport={setAirport} />
|
<AirportLayer setAirport={setAirport} />
|
||||||
</MapContainer>
|
</MapContainer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -31,8 +31,7 @@ export default function AirportLayer({ setAirport }: { setAirport: (airport: Air
|
|||||||
getAirports({
|
getAirports({
|
||||||
bounds: boundsParam,
|
bounds: boundsParam,
|
||||||
metars: true,
|
metars: true,
|
||||||
categories: [AirportCategory.SMALL, AirportCategory.MEDIUM, AirportCategory.LARGE],
|
categories: [AirportCategory.HELIPORT, AirportCategory.SMALL, AirportCategory.MEDIUM, AirportCategory.LARGE]
|
||||||
limit: 200
|
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
setAirports(response.data);
|
setAirports(response.data);
|
||||||
@@ -53,9 +52,39 @@ export default function AirportLayer({ setAirport }: { setAirport: (airport: Air
|
|||||||
}
|
}
|
||||||
}, [map]);
|
}, [map]);
|
||||||
|
|
||||||
|
const categoryOrder: { [key in AirportCategory]?: number } = {
|
||||||
|
[AirportCategory.LARGE]: 3,
|
||||||
|
[AirportCategory.MEDIUM]: 2,
|
||||||
|
[AirportCategory.SMALL]: 1,
|
||||||
|
[AirportCategory.HELIPORT]: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortedAirports = airports.slice().sort((a, b) => {
|
||||||
|
// Compare by airport category first.
|
||||||
|
const categoryA = categoryOrder[a.category] ?? 4;
|
||||||
|
const categoryB = categoryOrder[b.category] ?? 4;
|
||||||
|
if (categoryA !== categoryB) {
|
||||||
|
return categoryA - categoryB;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then compare by flight category if available.
|
||||||
|
// Assuming that latest_metar.flight_category is a string and "UNKN" needs to come last.
|
||||||
|
const fcA = a.latest_metar?.flight_category ?? 'UNKN';
|
||||||
|
const fcB = b.latest_metar?.flight_category ?? 'UNKN';
|
||||||
|
|
||||||
|
if (fcA === 'UNKN' && fcB !== 'UNKN') return 1;
|
||||||
|
if (fcB === 'UNKN' && fcA !== 'UNKN') return -1;
|
||||||
|
|
||||||
|
// If both flight categories are not "UNKN", do a simple alphabetical comparison.
|
||||||
|
// (You may wish to customize this logic based on the actual flight category values.)
|
||||||
|
if (fcA < fcB) return -1;
|
||||||
|
if (fcA > fcB) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{airports.map((airport, index) => {
|
{sortedAirports.map((airport, index) => {
|
||||||
return <AirportMarker airport={airport} index={index} setAirport={setAirport} />;
|
return <AirportMarker airport={airport} index={index} setAirport={setAirport} />;
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Airport } from '@lib/airport.types.ts';
|
import { Airport, AirportCategory } from '@lib/airport.types.ts';
|
||||||
import { Marker } from 'react-leaflet';
|
import { Marker, Popup } from 'react-leaflet';
|
||||||
import L from 'leaflet';
|
import L from 'leaflet';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
|
||||||
export default function AirportMarker({
|
export default function AirportMarker({
|
||||||
index,
|
index,
|
||||||
@@ -11,23 +12,30 @@ export default function AirportMarker({
|
|||||||
airport: Airport;
|
airport: Airport;
|
||||||
setAirport: (airport: Airport) => void;
|
setAirport: (airport: Airport) => void;
|
||||||
}) {
|
}) {
|
||||||
const markerColor = getMarkerColor(airport);
|
const icon = createCustomIcon(airport);
|
||||||
const icon = createCustomIcon(markerColor);
|
const markerRef = useRef<L.Marker>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Marker
|
<Marker
|
||||||
key={index}
|
key={index}
|
||||||
|
ref={markerRef}
|
||||||
position={[airport.latitude, airport.longitude]}
|
position={[airport.latitude, airport.longitude]}
|
||||||
icon={icon}
|
icon={icon}
|
||||||
eventHandlers={{
|
eventHandlers={{
|
||||||
click: () => setAirport(airport)
|
click: () => setAirport(airport),
|
||||||
|
mouseover: () => markerRef.current?.openPopup(),
|
||||||
|
mouseout: () => markerRef.current?.closePopup()
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<Popup closeButton={false}>
|
||||||
|
{airport.icao} - {airport.name}
|
||||||
|
</Popup>
|
||||||
|
</Marker>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMarkerColor(airport: Airport): string {
|
function getMarkerColor(flightCategory: 'VFR' | 'MVFR' | 'LIFR' | 'IFR' | 'UNKN'): string {
|
||||||
if (airport.latest_metar) {
|
switch (flightCategory) {
|
||||||
switch (airport.latest_metar.flight_category.toUpperCase()) {
|
|
||||||
case 'IFR':
|
case 'IFR':
|
||||||
return '#ff0100';
|
return '#ff0100';
|
||||||
case 'LIFR':
|
case 'LIFR':
|
||||||
@@ -36,27 +44,64 @@ function getMarkerColor(airport: Airport): string {
|
|||||||
return '#00f';
|
return '#00f';
|
||||||
case 'VFR':
|
case 'VFR':
|
||||||
return '#018000';
|
return '#018000';
|
||||||
case 'UNKNOWN':
|
case 'UNKN':
|
||||||
return '#3e3e3e';
|
|
||||||
default:
|
|
||||||
return '#3e3e3e';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return '#696969';
|
return '#696969';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createCustomIcon(color: string): L.DivIcon {
|
function createCustomIcon(airport: Airport): L.DivIcon {
|
||||||
|
if (airport.category === AirportCategory.HELIPORT) {
|
||||||
return L.divIcon({
|
return L.divIcon({
|
||||||
html: `<div style="
|
html: `
|
||||||
background-color: ${color};
|
<div style="
|
||||||
width: 16px;
|
width: 14px;
|
||||||
height: 16px;
|
height: 14px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: 2px solid #fff;
|
border: 2px solid black;
|
||||||
"></div>`,
|
background-color: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;">
|
||||||
|
<span style="color: black; font-size: 8px; font-weight: bold;">H</span>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
className: '',
|
||||||
|
iconSize: [20, 20],
|
||||||
|
iconAnchor: [10, 10]
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Default to a filled circle.
|
||||||
|
const flightCategory = airport.latest_metar?.flight_category || 'UNKN';
|
||||||
|
const color = getMarkerColor(flightCategory);
|
||||||
|
if (flightCategory == 'UNKN') {
|
||||||
|
return L.divIcon({
|
||||||
|
html: `
|
||||||
|
<div style="
|
||||||
|
background-color: ${color};
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;">
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
className: '',
|
||||||
|
iconSize: [20, 20],
|
||||||
|
iconAnchor: [10, 10]
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return L.divIcon({
|
||||||
|
html: `
|
||||||
|
<div style="
|
||||||
|
background-color: ${color};
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid #fff;">
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
className: '',
|
className: '',
|
||||||
iconSize: [20, 20],
|
iconSize: [20, 20],
|
||||||
iconAnchor: [10, 10]
|
iconAnchor: [10, 10]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ export function Header() {
|
|||||||
<Group align='center' gap='xs'>
|
<Group align='center' gap='xs'>
|
||||||
<Burger opened={opened} onClick={toggle} hiddenFrom='xs' size='sm' />
|
<Burger opened={opened} onClick={toggle} hiddenFrom='xs' size='sm' />
|
||||||
<Avatar src='/logo.svg' alt='logo' />
|
<Avatar src='/logo.svg' alt='logo' />
|
||||||
<Text>FlightLink</Text>
|
<Text>Aviation Data</Text>
|
||||||
</Group>
|
</Group>
|
||||||
{/*<Group gap={5} visibleFrom='xs' className={classes.navGroup}>*/}
|
{/*<Group gap={5} visibleFrom='xs' className={classes.navGroup}>*/}
|
||||||
{/* {navItems}*/}
|
{/* {navItems}*/}
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
// const protocol = process.env.HTTPD_PROTOCOL || 'http';
|
|
||||||
// const host = process.env.HTTPD_HOST || 'localhost';
|
|
||||||
// const port = process.env.HTTPD_PORT || 8080;
|
|
||||||
// const baseUrl = `${protocol}://${host}:${port}/api`;
|
|
||||||
const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080/api';
|
const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080/api';
|
||||||
|
|
||||||
export async function getRequest(endpoint: string, params: Record<string, any> = {}): Promise<Response> {
|
export async function getRequest(endpoint: string, params: Record<string, any> = {}): Promise<Response> {
|
||||||
|
|||||||
@@ -11,5 +11,8 @@ export default defineConfig({
|
|||||||
'@components': path.resolve(__dirname, './src/components'),
|
'@components': path.resolve(__dirname, './src/components'),
|
||||||
'@lib': path.resolve(__dirname, './src/lib'),
|
'@lib': path.resolve(__dirname, './src/lib'),
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
host: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
Reference in New Issue
Block a user