From bded9081001740f852c9d905b7f8aacd59eef507 Mon Sep 17 00:00:00 2001 From: Roman Kozulia Date: Mon, 15 Sep 2025 17:51:22 -0400 Subject: [PATCH 01/12] Eliminate environment files and replace with unified properties-based configuration --- Dockerfile | 25 +- docker-compose.yml | 47 +--- .../src/main/java/org/ngafid/core/Config.java | 250 ++++++++++++++++-- .../core/kafka/DockerServiceHeartbeat.java | 42 ++- ngafid-frontend/src/heat_map.tsx | 4 +- ngafid-frontend/src/map.js | 4 +- ngafid-frontend/webpack.config.js | 2 +- ngafid-static/templates/heat_map.html | 1 + ngafid-static/templates/ngafid_cesium.html | 1 + .../templates/ngafid_cesium_new.html | 1 + .../www/routes/AnalysisJavalinRoutes.java | 10 + .../www/routes/CesiumDataJavalinRoutes.java | 1 + .../routes/DockerServiceHeartbeatMonitor.java | 5 +- 13 files changed, 310 insertions(+), 83 deletions(-) diff --git a/Dockerfile b/Dockerfile index 14caa737f..87f5c7749 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,14 @@ FROM eclipse-temurin:24 -ARG HOST_DB_INFO -ARG CONTAINER_DB_INFO -ARG HOST_KAFKA_CONFIG -ARG CONTAINER_KAFKA_CONFIG -ARG HOST_EMAIL_INFO -ARG CONTAINER_EMAIL_INFO -ARG HOST_AIRPORTS -ARG CONTAINER_AIRPORTS -ARG HOST_RUNWAYS -ARG CONTAINER_RUNWAYS - -COPY $HOST_DB_INFO $CONTAINER_DB_INFO -COPY $HOST_KAFKA_CONFIG $CONTAINER_KAFKA_CONFIG -COPY $HOST_EMAIL_INFO $CONTAINER_EMAIL_INFO +# Copy configuration files directly (no environment variables needed) +COPY ngafid-core/src/main/resources/ngafid.properties /app/ngafid.properties +COPY ngafid-db-docker.conf /etc/ngafid-db.conf +COPY email-docker.conf /etc/email.conf COPY resources/log.properties /etc/log.properties -COPY $HOST_AIRPORTS $CONTAINER_AIRPORTS -COPY $HOST_RUNWAYS $CONTAINER_RUNWAYS +# Copy data files +COPY resources/airports.csv /etc/airports.csv +COPY resources/runways.csv /etc/runways.csv + +# Set log configuration ENV LOG_CONFIG=/etc/log.properties \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 8cdad1fbf..ac63b827a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,12 +1,10 @@ version: "1.0" -# Split this up into two separate groups since ngafid-kafka-topics needs these variables, but should not depend on -# itself. +# Common configuration for all NGAFID services +# No environment variables needed - everything configured via properties files x-ngafid-service-common-base: &ngafid-service-common-base extra_hosts: - "host.docker.internal:host-gateway" - env_file: - - .env x-ngafid-service-common: &ngafid-service-common <<: *ngafid-service-common-base @@ -14,29 +12,18 @@ x-ngafid-service-common: &ngafid-service-common ngafid-kafka-topics: condition: service_completed_successfully volumes: - - $HOST_UPLOAD_DIR:$CONTAINER_UPLOAD_DIR - - $HOST_ARCHIVE_DIR:$CONTAINER_ARCHIVE_DIR - - $HOST_STATIC_DIR:$CONTAINER_STATIC_DIR - - $HOST_TERRAIN_DIR:$CONTAINER_TERRAIN_DIR + # Mount the properties file and data directories + - ./ngafid-core/src/main/resources/ngafid.properties:/app/ngafid.properties:ro + - ./data/uploads:/mnt/uploads + - ./data/archive:/mnt/archive + - ./ngafid-static:/mnt/static + - ./data/terrain:/mnt/terrain services: base: - env_file: - - .env build: context: . - args: - HOST_DB_INFO: ${HOST_DB_INFO} - CONTAINER_DB_INFO: ${CONTAINER_DB_INFO} - HOST_KAFKA_CONFIG: ${HOST_KAFKA_CONFIG} - CONTAINER_KAFKA_CONFIG: ${CONTAINER_KAFKA_CONFIG} - HOST_EMAIL_INFO: ${HOST_EMAIL_INFO} - CONTAINER_EMAIL_INFO: ${CONTAINER_EMAIL_INFO} - HOST_RUNWAYS: ${HOST_RUNWAYS} - CONTAINER_RUNWAYS: ${CONTAINER_RUNWAYS} - HOST_AIRPORTS: ${HOST_AIRPORTS} - CONTAINER_AIRPORTS: ${CONTAINER_AIRPORTS} - HOST_UPLOAD_DIR: ${HOST_UPLOAD_DIR} + # No build args needed - everything configured via properties files image: ngafid-base # This must run and complete before everything else to create the Kafka topics @@ -51,10 +38,6 @@ services: # Reads emails from email kafka topic and sends them using the supplied credentials ngafid-email-consumer: - environment: - - KAFKA_BOOTSTRAP=${KAFKA_BOOTSTRAP} - - SERVICE_NAME=ngafid-email-consumer - - HEARTBEAT_INTERVAL_MS=10000 build: context: . dockerfile: ngafid-core/Dockerfile.email @@ -69,10 +52,6 @@ services: # Upload processor. Upload topic is created with 6 partitions by default, so up to 6 replicas would work. ngafid-upload-consumer: - environment: - - KAFKA_BOOTSTRAP=${KAFKA_BOOTSTRAP} - - SERVICE_NAME=ngafid-upload-consumer - - HEARTBEAT_INTERVAL_MS=10000 build: context: . dockerfile: ngafid-data-processor/Dockerfile @@ -92,10 +71,6 @@ services: # Event Consumer: reads from event topic and computes the events ngafid-event-consumer: - environment: - - KAFKA_BOOTSTRAP=${KAFKA_BOOTSTRAP} - - SERVICE_NAME=ngafid-event-consumer - - HEARTBEAT_INTERVAL_MS=10000 build: context: . dockerfile: ngafid-data-processor/Dockerfile.event-consumer @@ -104,10 +79,6 @@ services: # Event Observer: scans database for events that have not been computed, and computes them. # This can also be used to re-compute events as potential duplicates are deleted. ngafid-event-observer: - environment: - - KAFKA_BOOTSTRAP=${KAFKA_BOOTSTRAP} - - SERVICE_NAME=ngafid-event-observer - - HEARTBEAT_INTERVAL_MS=10000 build: context: . dockerfile: ngafid-data-processor/Dockerfile.event-observer diff --git a/ngafid-core/src/main/java/org/ngafid/core/Config.java b/ngafid-core/src/main/java/org/ngafid/core/Config.java index c167f333d..548e78d24 100644 --- a/ngafid-core/src/main/java/org/ngafid/core/Config.java +++ b/ngafid-core/src/main/java/org/ngafid/core/Config.java @@ -1,6 +1,14 @@ package org.ngafid.core; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + public class Config { + private static final Properties properties = new Properties(); + private static final String PROPERTIES_FILE = "ngafid.properties"; + private static final boolean IS_DOCKER_ENVIRONMENT; + private static boolean environmentLogged = false; public static final int NGAFID_PORT; public static final int MAX_TERRAIN_CACHE_SIZE; public static final int PARALLELISM; @@ -23,33 +31,140 @@ public class Config { public static final String LOG_PROPERTIES_FILE; static { - PARALLELISM = Integer.parseInt(getEnvironmentVariable("NGAFID_PARALLELISM", Runtime.getRuntime().availableProcessors() + "")); - NGAFID_PORT = Integer.parseInt(getEnvironmentVariable("NGAFID_PORT", "8181")); - MAX_TERRAIN_CACHE_SIZE = Integer.parseInt(getEnvironmentVariable("MAX_TERRAIN_CACHE_SIZE", "384")); + // Check Docker environment once and cache the result + IS_DOCKER_ENVIRONMENT = isRunningInDocker(); + + // Load properties file + loadProperties(); + + // Initialize configuration values with fallback to environment variables + PARALLELISM = getIntProperty("ngafid.parallelism", "NGAFID_PARALLELISM", Runtime.getRuntime().availableProcessors()); + NGAFID_PORT = getIntProperty("ngafid.port", "NGAFID_PORT", 8181); + MAX_TERRAIN_CACHE_SIZE = getIntProperty("ngafid.max.terrain.cache.size", "MAX_TERRAIN_CACHE_SIZE", 384); - NGAFID_USE_MARIA_DB = Boolean.parseBoolean(getEnvironmentVariable("NGAFID_USE_MARIA_DB", "false")); - NGAFID_EMAIL_ENABLED = Boolean.parseBoolean(getEnvironmentVariable("NGAFID_EMAIL_ENABLED", "false")); - DISABLE_PERSISTENT_SESSIONS = Boolean.parseBoolean(getEnvironmentVariable("DISABLE_PERSISTANT_SESSIONS", "false")); + NGAFID_USE_MARIA_DB = getBooleanProperty("ngafid.use.maria.db", "NGAFID_USE_MARIA_DB", false); + NGAFID_EMAIL_ENABLED = getBooleanProperty("ngafid.email.enabled", "NGAFID_EMAIL_ENABLED", false); + DISABLE_PERSISTENT_SESSIONS = getBooleanProperty("ngafid.disable.persistent.sessions", "DISABLE_PERSISTANT_SESSIONS", false); - AIRPORTS_FILE = getEnvironmentVariable("AIRPORTS_FILE"); - RUNWAYS_FILE = getEnvironmentVariable("RUNWAYS_FILE"); - NGAFID_UPLOAD_DIR = getEnvironmentVariable("NGAFID_UPLOAD_DIR"); - NGAFID_ARCHIVE_DIR = getEnvironmentVariable("NGAFID_ARCHIVE_DIR"); - NGAFID_STATIC_DIR = getEnvironmentVariable("NGAFID_STATIC_DIR"); - NGAFID_TERRAIN_DIR = getEnvironmentVariable("NGAFID_TERRAIN_DIR"); + AIRPORTS_FILE = getStringProperty("ngafid.airports.file", "AIRPORTS_FILE"); + RUNWAYS_FILE = getStringProperty("ngafid.runways.file", "RUNWAYS_FILE"); + NGAFID_UPLOAD_DIR = getStringProperty("ngafid.upload.dir", "NGAFID_UPLOAD_DIR"); + NGAFID_ARCHIVE_DIR = getStringProperty("ngafid.archive.dir", "NGAFID_ARCHIVE_DIR"); + NGAFID_STATIC_DIR = getStringProperty("ngafid.static.dir", "NGAFID_STATIC_DIR"); + NGAFID_TERRAIN_DIR = getStringProperty("ngafid.terrain.dir", "NGAFID_TERRAIN_DIR"); MUSTACHE_TEMPLATE_DIR = NGAFID_STATIC_DIR + "/templates"; - NGAFID_DB_INFO = getEnvironmentVariable("NGAFID_DB_INFO"); - KAFKA_CONFIG_FILE = getEnvironmentVariable("KAFKA_CONFIG_FILE"); - EMAIL_INFO_FILE = getEnvironmentVariable("EMAIL_INFO_FILE"); - NGAFID_ADMIN_EMAILS = getEnvironmentVariable("NGAFID_ADMIN_EMAILS", ""); - LOG_PROPERTIES_FILE = getEnvironmentVariable("LOG_PROPERTIES_FILE", "resources/log.properties"); + NGAFID_DB_INFO = getStringProperty("ngafid.db.info", "NGAFID_DB_INFO"); + KAFKA_CONFIG_FILE = getStringProperty("ngafid.kafka.config.file", "KAFKA_CONFIG_FILE"); + EMAIL_INFO_FILE = getStringProperty("ngafid.email.info", "EMAIL_INFO_FILE"); + NGAFID_ADMIN_EMAILS = getStringProperty("ngafid.admin.emails", "NGAFID_ADMIN_EMAILS", ""); + LOG_PROPERTIES_FILE = getStringProperty("ngafid.log.properties.file", "LOG_PROPERTIES_FILE", "resources/log.properties"); + } + + private static void loadProperties() { + // Check for custom properties file first + String customPropertiesFile = System.getProperty("ngafid.config.file"); + if (customPropertiesFile != null) { + try (InputStream input = Config.class.getClassLoader().getResourceAsStream(customPropertiesFile)) { + if (input != null) { + properties.load(input); + System.out.println("Loaded configuration from " + customPropertiesFile); + return; + } + } catch (IOException e) { + System.err.println("Error loading custom properties file " + customPropertiesFile + ": " + e.getMessage()); + } + } + + // Load the unified properties file + try (InputStream input = Config.class.getClassLoader().getResourceAsStream(PROPERTIES_FILE)) { + if (input != null) { + properties.load(input); + System.out.println("Loaded unified configuration from " + PROPERTIES_FILE); + + // Resolve variable substitutions + resolveVariableSubstitutions(); + } else { + System.err.println("Properties file " + PROPERTIES_FILE + " not found!"); + throw new RuntimeException("Configuration file not found: " + PROPERTIES_FILE); + } + } catch (IOException e) { + System.err.println("Error loading properties file: " + e.getMessage()); + throw new RuntimeException("Failed to load configuration file: " + PROPERTIES_FILE, e); + } + } + + private static String getStringProperty(String propertyKey, String envKey) { + return getStringProperty(propertyKey, envKey, null); + } + + private static String getStringProperty(String propertyKey, String envKey, String defaultValue) { + // First try system property (can be set via -D) + String systemProperty = System.getProperty(propertyKey); + if (systemProperty != null) { + return systemProperty; + } + + // Use cached Docker environment check + boolean isDocker = IS_DOCKER_ENVIRONMENT; + + // Try environment-specific property first, then fallback to general property + String propertyValue = null; + if (isDocker) { + String dockerPropertyKey = propertyKey.replace("ngafid.", "ngafid.docker."); + propertyValue = properties.getProperty(dockerPropertyKey); + } + + // If no Docker-specific property found, try the general property + if (propertyValue == null) { + propertyValue = properties.getProperty(propertyKey); + } + + if (propertyValue != null) { + return propertyValue; + } + + // Finally try environment variable + String envValue = System.getenv(envKey); + if (envValue != null) { + return envValue; + } + + if (defaultValue != null) { + return defaultValue; + } + + // If no value found and no default, throw exception + System.err.println("ERROR: Configuration value not found for property '" + propertyKey + "' or environment variable '" + envKey + "'"); + System.err.println("Please either:"); + System.err.println("1. Set the property in ngafid.properties file"); + System.err.println("2. Set the environment variable: export " + envKey + "="); + System.err.println("3. Set the system property: -D" + propertyKey + "="); + throw new RuntimeException("Configuration value not found for '" + propertyKey + "' or '" + envKey + "'"); + } + + private static int getIntProperty(String propertyKey, String envKey, int defaultValue) { + String value = getStringProperty(propertyKey, envKey, String.valueOf(defaultValue)); + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + System.err.println("Invalid integer value for " + propertyKey + ": " + value); + return defaultValue; + } + } + + private static boolean getBooleanProperty(String propertyKey, String envKey, boolean defaultValue) { + String value = getStringProperty(propertyKey, envKey, String.valueOf(defaultValue)); + return Boolean.parseBoolean(value); } + // Backward compatibility methods - these are deprecated but kept for existing code + @Deprecated public static String getEnvironmentVariable(String key, String defaultValue) { String value = System.getenv(key); return value == null ? defaultValue : value; } + @Deprecated public static String getEnvironmentVariable(String key) { String value = System.getenv(key); if (value == null) { @@ -61,4 +176,105 @@ public static String getEnvironmentVariable(String key) { return value; } + + // New methods for property-based configuration + public static String getProperty(String key) { + return getStringProperty(key, key.toUpperCase().replace('.', '_'), null); + } + + public static String getProperty(String key, String defaultValue) { + return getStringProperty(key, key.toUpperCase().replace('.', '_'), defaultValue); + } + + public static int getIntProperty(String key, int defaultValue) { + return getIntProperty(key, key.toUpperCase().replace('.', '_'), defaultValue); + } + + public static boolean getBooleanProperty(String key, boolean defaultValue) { + return getBooleanProperty(key, key.toUpperCase().replace('.', '_'), defaultValue); + } + + /** + * Resolves variable substitutions in properties (e.g., ${ngafid.repo.path}) + */ + private static void resolveVariableSubstitutions() { + // First pass: resolve basic variables + for (String key : properties.stringPropertyNames()) { + String value = properties.getProperty(key); + if (value != null && value.contains("${")) { + String resolved = resolveVariables(value); + properties.setProperty(key, resolved); + } + } + + // Second pass: resolve nested variables + for (String key : properties.stringPropertyNames()) { + String value = properties.getProperty(key); + if (value != null && value.contains("${")) { + String resolved = resolveVariables(value); + properties.setProperty(key, resolved); + } + } + } + + /** + * Resolves variables in a string value + */ + private static String resolveVariables(String value) { + if (value == null || !value.contains("${")) { + return value; + } + + String result = value; + int start = result.indexOf("${"); + while (start != -1) { + int end = result.indexOf("}", start); + if (end != -1) { + String varName = result.substring(start + 2, end); + String varValue = properties.getProperty(varName); + if (varValue != null) { + result = result.substring(0, start) + varValue + result.substring(end + 1); + } else { + // Variable not found, leave as is + start = result.indexOf("${", end); + } + } else { + break; + } + start = result.indexOf("${"); + } + + return result; + } + + /** + * Detects if the application is running inside a Docker container + * by checking for the presence of /.dockerenv file + */ + private static boolean isRunningInDocker() { + try { + java.io.File dockerEnv = new java.io.File("/.dockerenv"); + boolean exists = dockerEnv.exists(); + + // Only log once to avoid spam + if (!environmentLogged) { + System.out.println("Checking /.dockerenv: " + exists); + if (exists) { + System.out.println("Docker environment detected - using Docker properties"); + } else { + System.out.println("Development environment detected - using development properties"); + } + environmentLogged = true; + } + + return exists; + } catch (Exception e) { + if (!environmentLogged) { + System.out.println("Error checking /.dockerenv: " + e.getMessage()); + System.out.println("Development environment detected - using development properties"); + environmentLogged = true; + } + return false; + } + } } diff --git a/ngafid-core/src/main/java/org/ngafid/core/kafka/DockerServiceHeartbeat.java b/ngafid-core/src/main/java/org/ngafid/core/kafka/DockerServiceHeartbeat.java index 0495f8200..57bde7c96 100644 --- a/ngafid-core/src/main/java/org/ngafid/core/kafka/DockerServiceHeartbeat.java +++ b/ngafid-core/src/main/java/org/ngafid/core/kafka/DockerServiceHeartbeat.java @@ -15,6 +15,7 @@ import org.apache.kafka.clients.producer.ProducerConfig; import org.apache.kafka.clients.producer.ProducerRecord; import org.apache.kafka.common.serialization.StringSerializer; +import org.ngafid.core.Config; public class DockerServiceHeartbeat { @@ -58,8 +59,13 @@ public static void autostart() throws UnknownHostException { * when called from the main method of a consumer. */ - //Get service name - final String service = System.getenv().getOrDefault("SERVICE_NAME", SERVICE_NAME_UNKNOWN); + //Get service name - try to detect from context or use default + String service = Config.getProperty("ngafid.service.name", SERVICE_NAME_UNKNOWN); + + // If still unknown, try to detect from main class or stack trace + if (service.equals(SERVICE_NAME_UNKNOWN)) { + service = detectServiceName(); + } //Got unknown service name, do not start heartbeat if (service.equals(SERVICE_NAME_UNKNOWN)) { @@ -75,9 +81,9 @@ public static void autostart() throws UnknownHostException { //Get properties for the heartbeat producer final Properties heartbeatProps = new Properties(); - String bootstrap = System.getenv("KAFKA_BOOTSTRAP"); + String bootstrap = Config.getProperty("ngafid.kafka.bootstrap.servers", "localhost:9092"); if (bootstrap == null || bootstrap.isEmpty()) { - throw new RuntimeException("KAFKA_BOOTSTRAP environment variable must be set!"); + throw new RuntimeException("ngafid.kafka.bootstrap.servers property must be set!"); } heartbeatProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrap); heartbeatProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); @@ -88,11 +94,37 @@ public static void autostart() throws UnknownHostException { //Get instance ID and heartbeat interval final String instance = InetAddress.getLocalHost().getHostName(); - final long heartbeatIntervalMS = Long.parseLong(System.getenv().getOrDefault("HEARTBEAT_INTERVAL_MS", "10_000")); + final long heartbeatIntervalMS = Long.parseLong(Config.getProperty("ngafid.heartbeat.interval.ms", "10000")); //Start the heartbeat DockerServiceHeartbeat.start(heartbeatProducer, service, instance, heartbeatIntervalMS); } + + /** + * Detects the service name from the calling context + */ + private static String detectServiceName() { + try { + StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + for (StackTraceElement element : stackTrace) { + String className = element.getClassName(); + if (className.contains("EmailConsumer")) { + return "ngafid-email-consumer"; + } else if (className.contains("UploadConsumer")) { + return "ngafid-upload-consumer"; + } else if (className.contains("EventConsumer")) { + return "ngafid-event-consumer"; + } else if (className.contains("EventObserver")) { + return "ngafid-event-observer"; + } else if (className.contains("WebServer")) { + return "ngafid-www"; + } + } + } catch (Exception e) { + // Ignore exceptions + } + return SERVICE_NAME_UNKNOWN; + } } \ No newline at end of file diff --git a/ngafid-frontend/src/heat_map.tsx b/ngafid-frontend/src/heat_map.tsx index 7115eec9a..ceb57ae3b 100644 --- a/ngafid-frontend/src/heat_map.tsx +++ b/ngafid-frontend/src/heat_map.tsx @@ -218,8 +218,8 @@ const BLUE_POINT_STYLE = new Style({ const MARKER_VISIBILITY_ZOOM_THRESHOLD = 15; -// Azure Maps configuration -let azureMapsKey = process.env.AZURE_MAPS_KEY; + +declare const azureMapsKey: string | undefined; // Airframes configuration - define airframes if not already defined declare const airframes: string[] | undefined; diff --git a/ngafid-frontend/src/map.js b/ngafid-frontend/src/map.js index af4985c76..1eae61fc2 100644 --- a/ngafid-frontend/src/map.js +++ b/ngafid-frontend/src/map.js @@ -13,8 +13,8 @@ let styles = []; let layers = []; function initializeMap() { - const azureMapsKey = process.env.AZURE_MAPS_KEY; - if (!azureMapsKey) { + // Azure Maps key is now injected from backend via template + if (typeof azureMapsKey === 'undefined' || !azureMapsKey) { console.error("Azure Maps key is missing or undefined!"); return; } diff --git a/ngafid-frontend/webpack.config.js b/ngafid-frontend/webpack.config.js index 2cadbec90..4ebb599b1 100644 --- a/ngafid-frontend/webpack.config.js +++ b/ngafid-frontend/webpack.config.js @@ -193,7 +193,7 @@ module.exports = { }), new webpack.DefinePlugin({ CESIUM_BASE_URL: JSON.stringify("/cesium"), - 'process.env.AZURE_MAPS_KEY': JSON.stringify(process.env.AZURE_MAPS_KEY), + // Azure Maps key is now injected from backend via template, not build-time env vars }), new ShowChangedFilesPlugin(), new DeadCodePlugin({ diff --git a/ngafid-static/templates/heat_map.html b/ngafid-static/templates/heat_map.html index 7b83606c3..4539d1e9c 100644 --- a/ngafid-static/templates/heat_map.html +++ b/ngafid-static/templates/heat_map.html @@ -49,6 +49,7 @@ var plotMapHidden = true; {{{ navbar_js }}} {{{ fleet_info_js }}} + {{{ azure_maps_key }}} diff --git a/ngafid-static/templates/ngafid_cesium.html b/ngafid-static/templates/ngafid_cesium.html index 25bbce4a2..b47d9b94e 100644 --- a/ngafid-static/templates/ngafid_cesium.html +++ b/ngafid-static/templates/ngafid_cesium.html @@ -24,6 +24,7 @@ {{{navbar_js}}} {{{events_js}}} {{{cesium_data_js}}} + {{{azure_maps_key}}}