Code Style
Overview
This documentation describes the code style conventions and best practices for p2d2 development. The focus is on TypeScript, Astro components, and OpenLayers integration.
TypeScript Configuration
tsconfig.json
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"],
"compilerOptions": {
"module": "esnext",
"moduleResolution": "node",
"baseUrl": ".",
"paths": {
"*": ["node_modules/*"],
"@/*": ["./src/*"],
"@utils/*": ["./src/utils/*"]
},
"jsx": "preserve"
}
}Important Settings:
strict: true- Strict TypeScript checkingmodule: esnext- ES modules for modern browserspaths- Aliases for better import structurejsx: preserve- JSX support for Astro components
Path Aliases
// ❌ Avoid
import { registerUtm } from '../../../utils/crs';
// ✅ Recommended
import { registerUtm } from '@utils/crs';
import { MapConfig } from '@/types/map';ESLint & Prettier
Status: Currently not configured
Recommended Setup
# ESLint with TypeScript and Astro support
npm install --save-dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-astro
# Prettier for code formatting
npm install --save-dev prettier eslint-config-prettier eslint-plugin-prettierExample Configuration
.eslintrc.json:
{
"extends": [
"eslint:recommended",
"@typescript-eslint/recommended",
"plugin:astro/recommended",
"prettier"
],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"rules": {
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/explicit-function-return-type": "warn",
"prefer-const": "error"
}
}.prettierrc:
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 80,
"tabWidth": 2
}Naming Conventions
Files
Utility Functions: kebab-case
src/utils/crs.ts
src/utils/layer-management.ts
src/utils/events.tsAstro Components: PascalCase
src/components/Map.astro
src/components/KommuneCard.astro
src/layouts/BaseLayout.astroTypeScript Types: PascalCase
src/types/map.ts
src/types/kommunen.ts
src/types/api.tsVariables and Functions
camelCase for variables and functions:
// ✅ Good
const mapInstance = new Map();
const isValidCoordinate = isValidWgs84Coordinate(coord);
function transformCoordinate(coord: number[], targetEpsg: string) { ... }
// ❌ Avoid
const MAP_INSTANCE = new Map();
function Transform_Coordinate(coord: number[]) { ... }UPPER_CASE for constants:
// ✅ Good
const MAPCONFIG = {
center: [6.9578, 50.9375],
zoom: 10
};
const STORAGE_KEYS = {
SELECTED_CRS: 'p2d2_selected_crs',
SELECTED_KOMMUNE: 'p2d2_selected_kommune'
};PascalCase for types and interfaces:
// ✅ Good
interface MapConfig {
center: [number, number];
zoom: number;
projection: string;
}
type Coordinate = [number, number];
type Extent = [number, number, number, number];OpenLayers-specific Conventions
Layer Names:
// ✅ Good
const luftbildLayer = new TileLayer({ /* ... */ });
const wfsLayer = new VectorLayer({ /* ... */ });
const kommunenLayer = new VectorLayer({ /* ... */ });
// Layer Styles
const highlightStyle = new Style({ /* ... */ });
const defaultStyle = new Style({ /* ... */ });Event Handlers:
// ✅ Good
function handleMapMoveEnd(event: MapEvent) {
const center = map.getView().getCenter();
console.log('Map center:', center);
}
function handleFeatureSelect(event: SelectEvent) {
const feature = event.selected[0];
// Feature processing
}Import Conventions
Import Order
// 1. External Libraries (alphabetical)
import Map from 'ol/Map';
import View from 'ol/View';
import proj4 from 'proj4';
// 2. TypeScript Types (if separate imports)
import type { MapConfig } from '@/types/map';
import type { Coordinate } from '@/types/geo';
// 3. Internal Utilities (alphabetical)
import { registerUtm } from '@utils/crs';
import { dispatchKommunenFocus } from '@utils/events';
import { createLuftbildLayer } from '@utils/layer-management';
// 4. Local Imports (relative)
import { MAPCONFIG } from '../config/map-config';
import KommuneCard from '../components/KommuneCard.astro';Type-only Imports
// ✅ Good - Type-only import (no runtime dependency)
import type { MapConfig } from '@/types/map';
import type { Feature } from 'ol';
// ❌ Avoid - unnecessary runtime imports
import { MapConfig } from '@/types/map'; // When only type is usedCode Organization
Utility Functions
Single responsibility per function:
// ✅ Good - Clear responsibility
export function isValidWgs84Coordinate(coord: any): boolean {
return (
Array.isArray(coord) &&
coord.length === 2 &&
coord.every(Number.isFinite) &&
coord[0] >= -180 && coord[0] <= 180 &&
coord[1] >= -90 && coord[1] <= 90
);
}
export function transformCoordinate(
coord: number[],
sourceEpsg: string,
targetEpsg: string
): number[] | null {
// Transformation logic
}Error handling:
// ✅ Good - Explicit error handling
export function registerUtm(crs: string): boolean {
if (registeredProjections.has(crs)) {
return true; // Already registered
}
if (predefinedUtmDefs[crs]) {
proj4.defs(crs, predefinedUtmDefs[crs]);
registeredProjections.add(crs);
register(proj4);
return true;
}
console.error(`[crs] Unknown UTM projection: ${crs}`);
return false;
}Astro Components
Props typing:
---
// ✅ Good - Explicit props typing
interface Props {
kommune: KommuneData;
showMap?: boolean;
className?: string;
}
const { kommune, showMap = true, className = '' } = Astro.props;
---
<div class={`kommune-card ${className}`}>
<h2>{kommune.name}</h2>
{showMap && <Map.kommune={kommune} />}
</div>Conditional rendering:
---
// ✅ Good - Clear conditional logic
const hasDescription = kommune.description && kommune.description.length > 0;
const hasPopulation = kommune.population > 0;
---
<div>
{hasDescription && (
<p class="description">{kommune.description}</p>
)}
{hasPopulation ? (
<span class="population">{kommune.population} inhabitants</span>
) : (
<span class="no-data">No data</span>
)}
</div>Comments and Documentation
JSDoc for Utility Functions
/**
* Validates WGS84 coordinates
* @param coord - Coordinate to validate as [lng, lat]
* @returns true if coordinate is valid
* @example
* isValidWgs84Coordinate([6.9578, 50.9375]) // true
* isValidWgs84Coordinate([200, 100]) // false
*/
export function isValidWgs84Coordinate(coord: any): boolean {
// Implementation
}
/**
* Transforms coordinates between different CRS
* @param coord - Source coordinate [x, y]
* @param sourceEpsg - Source CRS (e.g. 'EPSG:4326')
* @param targetEpsg - Target CRS (e.g. 'EPSG:25832')
* @returns Transformed coordinate or null on error
*/
export function transformCoordinate(
coord: number[],
sourceEpsg: string,
targetEpsg: string
): number[] | null {
// Implementation
}Inline Comments
// ✅ Good - Explains "why", not "what"
// Proj4 must be registered before OpenLayers can use it
proj4.defs('EPSG:25832', '+proj=utm +zone=32 +ellps=GRS80 +units=m +no_defs');
register(proj4);
// ❌ Bad - Commenting the obvious
// Validate coordinate
if (isValidWgs84Coordinate(coord)) {
// Coordinate is valid
return coord;
}Best Practices
Error Handling
Consistent error messages:
// ✅ Good - Structured error messages
console.error('[crs] Invalid coordinate for transformation:', coord);
console.warn('[events] Event system not ready, queuing event:', eventName);
console.info('[map] Map initialized with projection:', projection);Graceful degradation:
// ✅ Good - Robust against missing environments
export function getSelectedCRS(): string | null {
if (typeof window === 'undefined') return null;
return localStorage.getItem(STORAGE_KEYS.SELECTED_CRS);
}
export function dispatchThrottledEvent(eventName: string, detail: any = {}): void {
if (typeof window === 'undefined') {
console.warn(`[events] cannot dispatch ${eventName} - window undefined`);
return;
}
// Event dispatch logic
}Performance Optimizations
Event throttling:
// ✅ Good - Avoids excessive event triggering
function throttle<T extends (...args: any[]) => void>(
func: T,
delay: number
): (...args: Parameters<T>) => void {
let timeoutId: ReturnType<typeof setTimeout> | null = null;
let lastExecTime = 0;
return (...args: Parameters<T>) => {
const currentTime = Date.now();
if (currentTime - lastExecTime < delay) {
if (timeoutId) clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
lastExecTime = currentTime;
func(...args);
}, delay - (currentTime - lastExecTime));
} else {
lastExecTime = currentTime;
func(...args);
}
};
}Memory management:
// ✅ Good - Cleanup of event listeners
export function setupMapEvents(map: Map): () => void {
const moveEndHandler = () => {
console.log('Map moved');
};
map.on('moveend', moveEndHandler);
// Return cleanup function
return () => {
map.un('moveend', moveEndHandler);
};
}
// Usage
const cleanup = setupMapEvents(map);
// Clean up later
cleanup();VS Code Configuration
.vscode/settings.json:
{
"typescript.preferences.importModuleSpecifier": "relative",
"typescript.suggest.autoImports": true,
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"files.associations": {
"*.astro": "astro"
}
}.vscode/extensions.json:
{
"recommendations": [
"astro-build.astro-vscode",
"bradlc.vscode-tailwindcss",
"esbenp.prettier-vscode"
]
}Debugging
TypeScript Debugging
Identify type errors:
// ✅ Good - Explicit types for better error messages
interface Coordinate {
x: number;
y: number;
}
function transformCoordinate(coord: Coordinate): Coordinate | null {
// Instead of any[] we use the explicit interface
if (!coord || typeof coord.x !== 'number' || typeof coord.y !== 'number') {
console.error('[debug] Invalid coordinate:', coord);
return null;
}
// Transformation logic
}Runtime validation:
// ✅ Good - Validation for unsafe data
function isValidWgs84Coordinate(coord: any): coord is [number, number] {
return (
Array.isArray(coord) &&
coord.length === 2 &&
coord.every(Number.isFinite) &&
coord[0] >= -180 && coord[0] <= 180 &&
coord[1] >= -90 && coord[1] <= 90
);
}
// Usage with type guard
if (isValidWgs84Coordinate(userInput)) {
// TypeScript now knows it's [number, number]
const transformed = transformCoordinate(userInput);
}OpenLayers Debugging
Monitor map status:
// Map events for debugging
map.on('moveend', () => {
const view = map.getView();
console.log('[map] Center:', view.getCenter());
console.log('[map] Zoom:', view.getZoom());
console.log('[map] Projection:', view.getProjection().getCode());
});
// Monitor layer status
const layers = map.getLayers().getArray();
layers.forEach((layer, index) => {
console.log(`[map] Layer ${index}:`, layer.get('name'), 'visible:', layer.getVisible());
});Feature debugging:
// Log feature information
map.on('click', (event) => {
const features = map.getFeaturesAtPixel(event.pixel);
console.log('[map] Click features:', features?.length || 0);
features?.forEach((feature, index) => {
console.log(`[map] Feature ${index}:`, feature.getId(), feature.getProperties());
});
});Browser DevTools Debugging
Console logging with context:
// Check development environment
if (import.meta.env.DEV) {
console.group('[p2d2] Debug Information');
console.log('Environment:', import.meta.env.MODE);
console.log('Map initialized:', !!window.map);
console.log('Projection registered:', registeredProjections);
console.groupEnd();
}
// Performance measurement
const startTime = performance.now();
// Execute code
const endTime = performance.now();
console.log(`[perf] Operation took ${endTime - startTime}ms`);Network debugging:
// Monitor WFS requests
console.log('[wfs] Request URL:', url);
console.log('[wfs] Request parameters:', params);
fetch(url, params)
.then(response => {
console.log('[wfs] Response status:', response.status);
console.log('[wfs] Response headers:', Object.fromEntries(response.headers));
return response.text();
})
.then(data => {
console.log('[wfs] Response data length:', data.length);
// Data processing
})
.catch(error => {
console.error('[wfs] Request failed:', error);
});VS Code Debugging
Set breakpoints:
// Debugger statement for complex logic
function complexTransformation(coord: number[], sourceCrs: string, targetCrs: string) {
// Set breakpoint here
debugger;
// Complex transformation logic
const intermediate = transformToWgs84(coord, sourceCrs);
const result = transformFromWgs84(intermediate, targetCrs);
return result;
}Watch expressions:
// In VS Code Watch Panel:
window.map?.getView().getCenter()
window.map?.getView().getZoom()
registeredProjections.sizeCommon Debugging Scenarios
Coordinate transformation:
function debugCoordinateTransformation(coord: number[], sourceCrs: string, targetCrs: string) {
console.group('[debug] Coordinate Transformation');
console.log('Source coordinate:', coord);
console.log('Source CRS:', sourceCrs);
console.log('Target CRS:', targetCrs);
try {
const result = transformCoordinate(coord, sourceCrs, targetCrs);
console.log('Transformation result:', result);
} catch (error) {
console.error('Transformation failed:', error);
}
console.groupEnd();
return result;
}Layer loading:
function debugLayerLoading(layerName: string, layer: any) {
console.group(`[debug] Layer: ${layerName}`);
console.log('Layer instance:', layer);
console.log('Visible:', layer.getVisible());
console.log('Z-Index:', layer.getZIndex());
console.log('Source:', layer.getSource());
console.groupEnd();
}Code Review Checklist
✅ Must be fulfilled
- [ ] TypeScript typing correct
- [ ] No
anytypes (except in exceptional cases) - [ ] Import order followed
- [ ] Naming conventions followed
- [ ] Error handling implemented
- [ ] No console logs in production code
✅ Should be fulfilled
- [ ] JSDoc for public functions
- [ ] Unit tests for utility functions
- [ ] Performance considerations addressed
- [ ] Browser compatibility checked
✅ Can be fulfilled
- [ ] Accessibility (a11y) considered
- [ ] Internationalization (i18