This project demonstrates best practices for embedding the SITNA mapping library in an Angular application. This is an example implementation showing how SITNA integration can be done, not a complete production solution.
Key Features Demonstrated:
- Type-safe configuration with dedicated services
- Asset overriding for customizing SITNA defaults
- Custom configuration for layout and UI components
- Runtime behavior changes using meld
- Centralized JSON configuration for all SITNA settings
- Modern Angular patterns and dependency injection
- Project Purpose
- Project Structure
- Installation & Quick Start
- Integration Approaches
- Configuration Management
- SITNA Configuration in Angular
- Project Architecture
- Best Practices
- Troubleshooting
- For Future Developers
- References
This project serves as a demonstration and learning resource, not a production-ready solution.
- Integration Patterns: How to integrate SITNA with Angular using service-based architecture
- Customization Approaches: Different ways to customize SITNA behavior and appearance
- Runtime Patching: How to modify SITNA behavior without changing source code
- Type Safety: How to implement type-safe SITNA integration
- Asset Management: How to override and customize SITNA assets
- Configuration Management: How to externalize all settings to JSON files
- Production Ready: This is a demonstration, not a complete application
- Comprehensive: Does not cover all SITNA features or use cases
- One-Size-Fits-All: Your specific requirements may need different approaches
- Maintained Solution: This is an example to learn from, not a maintained library
- Study the Patterns: Understand the integration approaches demonstrated
- Adapt to Your Needs: Modify the patterns to fit your specific requirements
- Extend Functionality: Add features based on your application needs
- Test Thoroughly: Ensure the adapted solution works for your use case
- Maintain Independently: This project is not maintained as a dependency
src/
├── app/
│ ├── services/
│ │ ├── sitna-config.service.ts # Main SITNA configuration service
│ │ ├── sitna-config-loader.service.ts # JSON configuration loader
│ │ ├── sitna-meld-patch.ts # Runtime patching with meld
│ │ └── app-initializer.service.ts # Application initialization
│ └── components/
│ └── sitna-map/ # Map component
│ ├── sitna-map.component.ts
│ ├── sitna-map.component.html
│ └── sitna-map.component.scss
├── types/ # Centralized type definitions
│ ├── sitna.types.ts # SITNA interfaces
│ ├── meld.types.ts # meld library types
│ ├── meld.d.ts # meld module declaration
│ └── index.ts # Export all types
├── environments/
│ └── sitna-config.json # Centralized configuration
├── assets/
│ ├── js/api-sitna/ # SITNA library files (overriding defaults)
│ │ └── config/ # Custom configuration files
│ └── layout/sample-layout/ # Custom layout configuration
│ ├── config.json # Layout configuration
│ ├── markup.html # HTML template
│ ├── script.js # Custom JavaScript
│ ├── style.css # Custom styles
│ └── resources/ # Layout-specific resources
└── main.ts # Application bootstrap
-
Clone the repository:
git clone <repository-url> cd sitna-npm-test
-
Install dependencies:
npm install
-
Start the development server:
npm start
-
Open your browser and navigate to
http://localhost:4200
The project is pre-configured with SITNA integration. Here's how it works:
Application Bootstrap (src/main.ts):
The application starts by importing SITNA and bootstrapping Angular. Runtime patches are applied after configuration is loaded.
// Import SITNA first to ensure it's available
import 'api-sitna';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));Application Initialization (src/app/services/app-initializer.service.ts):
The application automatically configures SITNA during startup:
async initializeApp(): Promise<void> {
// Load configuration first
await this.configLoader.loadConfig().toPromise();
// Initialize SITNA configuration
await this.initializeSITNAConfiguration();
// Apply meld patches after configuration is loaded
patchSitnaMapLogging(this.configLoader);
}Configuration Service (src/app/services/sitna-config.service.ts):
This service provides a type-safe interface for all SITNA operations:
@Injectable({
providedIn: 'root'
})
export class SitnaConfigService {
setLayout(layoutPath: string): void
addWMSLayer(layer: Omit<SITNALayer, 'type'>): void
initializeMap(elementId: string): any
setDefaultMapOptions(options: Partial<SITNAMapOptions>): void
getConfig(): SITNAConfig
}Automatic Configuration: When the application starts, it automatically configures SITNA with:
- Custom layout (
assets/layout/sample-layout) - A completely custom UI layout - Overridden SITNA assets (
assets/js/api-sitna/) - Custom configuration files - JSON configuration (
environments/sitna-config.json) - All settings externalized - Runtime patching - All SITNA method calls are logged for debugging
- Type-safe interfaces - Full TypeScript support for SITNA objects
This project demonstrates three main approaches to customizing SITNA:
The src/assets/js/api-sitna/ directory is an example of overriding some SITNA defaults. This approach allows you to:
-
Configuration Files (
config/)browser-versions.json- Custom browser compatibility settingspredefined-layers.json- Custom layer definitions
-
Styling and Assets (
css/)- Custom CSS files and styles
- Font files and icons
- Image assets and markers
-
Layout Templates (
layout/)- Custom HTML templates
- Responsive layouts
- UI component configurations
-
Library Files (
lib/)- Custom JavaScript libraries
- Third-party integrations
- Utility functions
-
Resources and Data (
resources/)- Custom coordinate reference systems (CRS)
- Language files and translations
- Geographic data files
-
WMTS Configurations (
wmts/)- Custom tile service configurations
- Map provider settings
- Customization: Tailor SITNA to your specific needs
- Version Control: Track customizations in your repository
- Deployment Control: Ensure consistent behavior across environments
- Maintainability: Keep customizations separate from core library
- Upgrade Safety: Preserve customizations during SITNA updates
The overriding mechanism works through Angular's asset serving and SITNA's file loading system:
Angular Asset Serving:
Angular automatically serves all files from the src/assets/ directory at the root URL of your application. When you place files in src/assets/js/api-sitna/, they become available at /js/api-sitna/ in your web application.
SITNA File Loading:
SITNA loads its configuration files, styles, and resources from relative paths within its library structure. When SITNA requests a file like /js/api-sitna/config/predefined-layers.json, Angular serves your custom file from src/assets/js/api-sitna/config/predefined-layers.json instead of SITNA's original file.
The Override Process:
- SITNA requests a file from its expected path
- Angular intercepts this request and serves your custom file from the assets directory
- SITNA uses your custom configuration instead of its default
Directory Structure Mirroring:
Your override directory structure must mirror SITNA's internal structure. If SITNA expects files in /js/api-sitna/config/, you place your custom files in src/assets/js/api-sitna/config/.
The src/assets/layout/sample-layout/ directory is an example of custom configuration for SITNA layouts. This approach allows you to create entirely new layouts and UI configurations.
Custom configuration involves creating new layouts and UI components that extend SITNA's capabilities:
-
Layout Configuration (
config.json)- Define layout structure and components
- Configure UI element positioning
- Set default behaviors and options
-
HTML Templates (
markup.html)- Define the structure of your custom layout
- Create custom UI components
- Integrate with Angular components
-
Custom Styles (
style.css)- Define the visual appearance
- Create responsive designs
- Implement custom themes
-
Custom JavaScript (
script.js)- Add custom functionality
- Handle user interactions
- Integrate with external services
-
Layout Resources (
resources/)- Language files for the layout
- Custom icons and assets
- Layout-specific configurations
- Complete Control: Design layouts from scratch
- Brand Consistency: Match your application's design
- Functionality: Add custom features and behaviors
- Flexibility: Create layouts for specific use cases
- Maintainability: Keep custom layouts separate from core SITNA
Custom layouts work through SITNA's layout system and Angular's asset serving:
Layout Registration:
When you call setLayout('assets/layout/sample-layout') in your Angular service, you're telling SITNA to use a custom layout instead of its default interface.
Layout File Loading: SITNA expects custom layouts to include several specific files: a configuration file that defines the layout structure, an HTML template that provides the markup, custom styles for visual appearance, JavaScript for custom functionality, and optional resources like language files or icons.
The Layout Loading Process:
- SITNA receives your layout specification
- SITNA requests the necessary files from the specified path
- Angular serves those files from your assets directory
- SITNA uses your custom layout instead of its default
Implementation in AppInitializerService:
// src/app/services/app-initializer.service.ts
private async initializeSITNAConfiguration(): Promise<void> {
// Set custom layout from configuration - this enforces the sample layout
const layout = this.configLoader.getLayout();
this.sitnaConfigService.setLayout(layout);
// Set default map options from configuration
const defaultMapOptions = this.configLoader.getDefaultMapOptions();
this.sitnaConfigService.setDefaultMapOptions(defaultMapOptions);
// Configure default layers from configuration
await this.configureDefaultLayers();
}The custom configuration is enforced during application initialization, ensuring:
- Custom layout is loaded before any map components
- Configuration is applied consistently across the application
- Custom layout takes precedence over SITNA defaults
For logging, analytics, or custom behavior, use meld@1 to patch SITNA methods at runtime. This is robust, non-intrusive, and works for third-party libraries.
The project includes pre-configured patches in src/app/services/sitna-meld-patch.ts that modify the default behavior of SITNA:
import meld from 'meld';
export function patchSitnaMapLogging(configLoader?: SitnaConfigLoaderService) {
const waitForSITNA = () => {
const mapProto = (window as any)?.SITNA?.Map?.prototype;
const coordinatesProto = (window as any)?.TC?.control?.Coordinates?.prototype;
if (mapProto && coordinatesProto) {
applyPatches(mapProto, coordinatesProto, configLoader);
} else {
setTimeout(waitForSITNA, 100);
}
};
waitForSITNA();
}The patches in sitna-meld-patch.ts modify SITNA's default behavior in the following ways:
-
SITNA.Map.addLayerMethod:- Default Behavior: Silently adds layers to the map
- Modified Behavior: Logs every layer addition with arguments
- Impact: Provides visibility into layer management operations
-
TC.control.CoordinatesConstructor:- Default Behavior: Creates coordinate controls without logging
- Modified Behavior: Logs constructor calls with arguments
- Impact: Tracks coordinate control creation
-
TC.control.Coordinates.isPointerOverMethod:- Default Behavior: Checks pointer position without logging
- Modified Behavior: Logs pointer checks and maintains original functionality
- Impact: Provides debugging information for pointer interactions
The patches respect configuration settings from sitna-config.json:
function applyPatches(mapProto: any, coordinatesProto: any, configLoader?: SitnaConfigLoaderService): void {
try {
// Get patching configuration if available
let patchingConfig;
try {
patchingConfig = configLoader?.getPatchingConfig();
} catch (error) {
// If configuration is not available, use default patching behavior
patchingConfig = {
enabled: true,
logAddLayer: true,
logCoordinates: true
};
}
if (!patchingConfig.enabled) {
console.log('meld: Patching disabled by configuration');
return;
}
// Apply patches based on configuration
if (patchingConfig.logAddLayer) {
meld.before(mapProto, 'addLayer', function (...args: any[]) {
console.log('meld: addLayer called with:', ...args);
});
}
} catch (error) {
console.error('meld: Failed to apply patches:', error);
}
}meld.before- Log arguments before the original method runsmeld.around- Full control: log, modify arguments, or skip the original methodmeld.after- Log results after the method completes
| Aspect | Asset Overriding | Custom Configuration | Runtime Behavior Changes |
|---|---|---|---|
| Purpose | Replace SITNA defaults | Create new layouts | Modify method behavior |
| Location | assets/js/api-sitna/ |
assets/layout/custom-name/ |
sitna-meld-patch.ts |
| Scope | Override existing files | Create new layouts | Modify runtime behavior |
| Flexibility | Limited to SITNA structure | Complete freedom | Full control over methods |
| Maintenance | Must follow SITNA conventions | Independent structure | Must test after upgrades |
The project uses a centralized JSON configuration file for all SITNA settings.
Location: src/environments/sitna-config.json
This file contains all SITNA configuration including:
- Base URL: SITNA library location
- Layout: Custom layout path
- Map Options: Default projection, center, zoom levels
- Layers: Default WMS layers to load
- Controls: Layer catalog configuration
- Patching: Runtime patching settings
{
"sitna": {
"baseUrl": "assets/js/api-sitna/",
"layout": "assets/layout/sample-layout",
"defaultMapOptions": {
"projection": "EPSG:4326",
"center": [0, 0],
"zoom": 2,
"minZoom": 1,
"maxZoom": 18
},
"layers": {
"default": [
{
"id": "idena",
"title": "IDENA",
"hideTitle": true,
"url": "//idena.navarra.es/ogc/wms",
"hideTree": false
},
{
"id": "sismica",
"title": "Información sísmica y volcánica",
"url": "//www.ign.es/wms-inspire/geofisica",
"layerNames": ["Ultimos10dias", "Ultimos30dias", "Ultimos365dias"],
"hideTree": false
}
]
},
"controls": {
"layerCatalog": {
"div": "slot2",
"enableSearch": true
}
},
"patching": {
"enabled": true,
"logAddLayer": true,
"logCoordinates": true
}
}
}The SitnaConfigLoaderService handles loading and accessing configuration:
// Load configuration
await this.configLoader.loadConfig().toPromise();
// Access configuration
const layout = this.configLoader.getLayout();
const layers = this.configLoader.getDefaultLayers();
const mapOptions = this.configLoader.getDefaultMapOptions();- Environment-specific: Different configs for dev/prod
- Centralized: All settings in one place
- Type-safe: TypeScript interfaces for configuration
- No Defaults: Service throws errors if config is missing
- Flexible: Easy to modify without code changes
All SITNA configuration is handled via the SitnaConfigService. This service provides a type-safe, Angular-friendly interface for all SITNA operations.
import { SitnaConfigService } from './services/sitna-config.service';
@Component({...})
export class MyComponent {
constructor(private sitnaConfigService: SitnaConfigService) {}
ngOnInit() {
// Set custom layout
this.sitnaConfigService.setLayout('assets/layout/sample-layout');
// Configure default map options
this.sitnaConfigService.setDefaultMapOptions({
projection: 'EPSG:4326',
center: [0, 0],
zoom: 2
});
// Add WMS layers
this.sitnaConfigService.addWMSLayer({
id: 'idena',
title: 'IDENA',
url: '//idena.navarra.es/ogc/wms',
hideTree: false
});
}
}Note: In this project, configuration is handled by the AppInitializerService during application startup, so components only need to initialize the map.
setLayout(layoutPath: string)- Set the SITNA layout (custom or default)setDefaultMapOptions(options: Partial<SITNAMapOptions>)- Configure default map settingsaddWMSLayer(layer: Omit<SITNALayer, 'type'>)- Add WMS layersinitializeMap(elementId: string)- Initialize map in a DOM elementinitializeMapWithOptions(elementId: string, options: Partial<SITNAMapOptions>)- Initialize map with custom optionsgetConfig()- Get current configurationgetSITNAInstance()- Access the SITNA instancegetDefaultMapOptions()- Get current default map optionsconfigureLayerCatalog(config: SITNALayerCatalog)- Configure the layer catalog
The project includes comprehensive TypeScript interfaces:
export interface SITNALayer {
id: string;
title: string;
hideTitle?: boolean;
type: any;
url: string;
layerNames?: string[];
hideTree?: boolean;
}
export interface SITNALayerCatalog {
div: string;
enableSearch: boolean;
layers: SITNALayer[];
}
export interface SITNAControls {
layerCatalog?: SITNALayerCatalog;
}
export interface SITNAConfig {
layout?: string;
controls?: SITNAControls;
}
export interface SITNAMapOptions {
projection?: string;
center?: [number, number];
zoom?: number;
minZoom?: number;
maxZoom?: number;
extent?: [number, number, number, number];
layers?: any[];
controls?: any[];
[key: string]: any;
}The project follows Angular best practices with service-based architecture:
SitnaConfigService- Centralized configuration managementSitnaConfigLoaderService- JSON configuration loadingAppInitializerService- Application-wide initializationsitna-meld-patch.ts- Runtime patching logic- Component integration - Clean separation of concerns
- SITNA Library: Located in
src/assets/js/api-sitna/(overriding defaults) - Custom Layouts: Located in
src/assets/layout/(custom configurations) - Configuration: Located in
src/environments/(JSON configuration) - Type Definitions:
src/types/(centralized types)
// 1. Application bootstrap (main.ts)
import 'api-sitna';
platformBrowserDynamic().bootstrapModule(AppModule);
// 2. Service initialization (app-initializer.service.ts)
await this.configLoader.loadConfig().toPromise();
await this.initializeSITNAConfiguration();
patchSitnaMapLogging(this.configLoader);
// 3. Component usage (sitna-map.component.ts)
this.map = this.sitnaConfigService.initializeMap('mapa');The custom configuration is enforced in initializeSITNAConfiguration():
- Custom Layout:
setLayout()- Enforces custom layout from configuration - Map Options:
setDefaultMapOptions()- Sets default map configuration - Default Layers:
configureDefaultLayers()- Adds predefined WMS layers
- Use services for all SITNA interaction
- Configure before map initialization
- Use TypeScript interfaces for type safety
- Keep configuration centralized in JSON files
- No hardcoded defaults in services
- Override assets in
src/assets/js/api-sitna/ - Maintain the same directory structure as SITNA defaults
- Document customizations for team members
- Test overrides after SITNA upgrades
- Create custom layouts in
src/assets/layout/ - Follow SITNA layout conventions for compatibility
- Document layout structure and dependencies
- Test custom layouts thoroughly
- Patch only after SITNA is loaded
- Never modify SITNA source code directly
- Keep all patching logic in dedicated files
- Document all patches and their purpose
- Use TypeScript for all patching code
- Test behavior changes thoroughly
- Document the original vs modified behavior
- Use configuration to control patching behavior
- Follow Angular dependency injection patterns
- Use service-based architecture
- Maintain type safety throughout
- Test patches after SITNA upgrades
- Externalize all configuration to JSON files
Configuration not loading:
- Check that
src/environments/sitna-config.jsonexists - Verify the file is copied to
dist/assets/environments/ - Ensure
angular.jsonincludes environments in assets
Patch not applied:
- Check that SITNA prototypes are available at patch time
- Ensure patch function is called after configuration is loaded
- Verify configuration includes patching settings
Configuration not working:
- Ensure configuration is done before map initialization
- Check that services are properly injected
- Verify asset paths are correct
Map not displaying:
- Check browser console for errors
- Verify DOM element exists before initialization
- Ensure SITNA assets are properly loaded
Asset overrides not working:
- Verify files are in the correct
src/assets/js/api-sitna/structure - Check that Angular is serving the overridden files
- Ensure file names match SITNA's expected names
Custom layout not loading:
- Verify layout files are in the correct
src/assets/layout/structure - Check that all required files exist (config.json, markup.html, etc.)
- Ensure layout path is correctly specified in configuration
- Enable console logging - Patches provide detailed logging
- Check network requests - Verify SITNA assets are loading
- Inspect DOM - Ensure map container exists
- Review service injection - Verify Angular DI is working
- Check asset paths - Verify overridden files are being served
- Validate layout files - Ensure custom layout files are properly structured
- Check configuration loading - Verify JSON config is accessible
This is an example implementation, not a production solution. Use it to:
- Learn Integration Patterns: Understand how SITNA can be integrated with Angular
- Study Customization Approaches: See different ways to customize SITNA behavior
- Reference Implementation: Use as a reference when building your own solution
- Experiment Safely: Test different approaches in a controlled environment
- Always use services for configuration and patching
- Use meld for runtime patching/logging of third-party libraries
- Keep all patching logic isolated and well-documented
- Test patches after SITNA upgrades
- Prefer
meld.aroundfor full control,meld.before/meld.afterfor logging - Externalize all configuration to JSON files
- No hardcoded defaults in services
- Add new configuration methods to
SitnaConfigServiceor similar. - Create custom patches in
sitna-meld-patch.tsor similar. - Extend type definitions in
src/types/or similar. - Create custom layouts in
assets/layout/or similar. - Override SITNA assets in
assets/js/api-sitna/or similar. - Add new configuration options to
sitna-config.json.
When adapting this example for your project:
- Assess Your Needs: Determine which patterns fit your requirements
- Simplify or Extend: Remove unnecessary features or add missing ones
- Customize Thoroughly: Adapt to your specific use case and data
- Test Extensively: Ensure reliability with your data and users
- Document Changes: Keep track of what you've modified and why
- Keep dependencies updated
- Monitor SITNA library updates
- Test patches after upgrades
- Maintain type safety
- Document asset overrides and custom configurations
- Maintain your own fork: This project is not maintained as a dependency
Before using any patterns from this example in production:
- Security: Review all customizations for security implications
- Performance: Test with your expected load and data volumes
- Compatibility: Ensure compatibility with your target environments
- Accessibility: Verify customizations meet accessibility requirements
- Error Handling: Implement robust error handling for your use case
- Monitoring: Add appropriate logging and monitoring for production use
This approach ensures your Angular+SITNA integration is robust, maintainable, and easy to extend or debug, while understanding that this project serves as a learning resource rather than a complete solution.
- SITNA Documentation
- meld@1 (CujoJS) on npm. Note that you must pin to version prior 2.0.0 as meld@2 is a prompt scripting language.
- meld GitHub
- Angular Asset Configuration
- Angular Dependency Injection