diff --git a/angular.json b/angular.json
index b54541e..abb904c 100644
--- a/angular.json
+++ b/angular.json
@@ -35,7 +35,8 @@
}
],
"styles": [
- "src/styles.scss"
+ "src/styles.scss",
+ "node_modules/leaflet/dist/leaflet.css"
]
},
"configurations": {
diff --git a/package-lock.json b/package-lock.json
index 397c971..1cdee23 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,7 +16,9 @@
"@angular/material": "^21.2.11",
"@angular/platform-browser": "^21.2.0",
"@angular/router": "^21.2.0",
+ "@types/leaflet": "^1.9.21",
"chart.js": "^4.5.1",
+ "leaflet": "^1.9.4",
"moment": "^2.30.1",
"ng2-charts": "^10.0.0",
"rxjs": "~7.8.0",
@@ -4051,6 +4053,21 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/geojson": {
+ "version": "7946.0.16",
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
+ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/leaflet": {
+ "version": "1.9.21",
+ "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz",
+ "integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
"node_modules/@vitejs/plugin-basic-ssl": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.4.tgz",
@@ -6089,6 +6106,12 @@
],
"license": "MIT"
},
+ "node_modules/leaflet": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
+ "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
+ "license": "BSD-2-Clause"
+ },
"node_modules/listr2": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz",
diff --git a/package.json b/package.json
index d1a4a75..84161d5 100644
--- a/package.json
+++ b/package.json
@@ -19,7 +19,9 @@
"@angular/material": "^21.2.11",
"@angular/platform-browser": "^21.2.0",
"@angular/router": "^21.2.0",
+ "@types/leaflet": "^1.9.21",
"chart.js": "^4.5.1",
+ "leaflet": "^1.9.4",
"moment": "^2.30.1",
"ng2-charts": "^10.0.0",
"rxjs": "~7.8.0",
diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts
index 8248bff..252cff2 100644
--- a/src/app/app.routes.ts
+++ b/src/app/app.routes.ts
@@ -1,7 +1,9 @@
import { Routes } from '@angular/router';
import { Home } from './components/home/home';
+import { MapaEstacions } from './components/mapa-estacions/mapa-estacions';
export const routes: Routes = [
{ path: '', component: Home },
+ { path: 'mapa', component: MapaEstacions },
{ path: '**', redirectTo: '' }
];
diff --git a/src/app/components/home/home.html b/src/app/components/home/home.html
index 43fe116..1a9f3dc 100644
--- a/src/app/components/home/home.html
+++ b/src/app/components/home/home.html
@@ -67,6 +67,11 @@
+
+
+
diff --git a/src/app/components/home/home.ts b/src/app/components/home/home.ts
index d047165..1f17406 100644
--- a/src/app/components/home/home.ts
+++ b/src/app/components/home/home.ts
@@ -1,5 +1,6 @@
import { Component, OnInit, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
+import { Router, ActivatedRoute } from '@angular/router';
import { MeteocatService } from '../../services/meteocat.service';
import { Municipi } from '../../models/municipi.model';
import { PrediccioDiaria } from '../../models/prediccio-diaria.model';
@@ -38,9 +39,23 @@ export class Home implements OnInit {
showGpsModal = signal(false);
gpsPermissionAsked = signal(false);
- constructor(private meteocatService: MeteocatService) {}
+ constructor(
+ private meteocatService: MeteocatService,
+ private router: Router,
+ private route: ActivatedRoute
+ ) {}
ngOnInit(): void {
+ // Verificar si viene un municipio desde el mapa
+ this.route.queryParams.subscribe(params => {
+ if (params['municipi']) {
+ this.municipiCodi.set(params['municipi']);
+ this.gpsPermissionAsked.set(true);
+ this.loadWeatherData(params['municipi']);
+ return;
+ }
+ });
+
// Verificar si ya se pidió permiso GPS antes
const gpsRejected = localStorage.getItem('gpsRejected');
const gpsAsked = localStorage.getItem('gpsAsked');
@@ -292,4 +307,8 @@ export class Home implements OnInit {
return null;
}
}
+
+ openMap(): void {
+ this.router.navigate(['/mapa']);
+ }
}
diff --git a/src/app/components/mapa-estacions/mapa-estacions.html b/src/app/components/mapa-estacions/mapa-estacions.html
new file mode 100644
index 0000000..0425ce3
--- /dev/null
+++ b/src/app/components/mapa-estacions/mapa-estacions.html
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+ Mostrant: {{ getFilteredCount() }} / {{ estacions().length }}
+
+
+
+
+ @if (loading()) {
+
+
+
Carregant estacions...
+
+ }
+
+
+ @if (error()) {
+
+
{{ error() }}
+
+
+ }
+
+
+
+
+
+
+
+ 🌡️
+ Estació meteorològica
+
+
+ Fes clic en un marcador per veure el temps d'aquell municipi
+
+
+
diff --git a/src/app/components/mapa-estacions/mapa-estacions.scss b/src/app/components/mapa-estacions/mapa-estacions.scss
new file mode 100644
index 0000000..873c6d5
--- /dev/null
+++ b/src/app/components/mapa-estacions/mapa-estacions.scss
@@ -0,0 +1,305 @@
+.mapa-container {
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+ background: linear-gradient(180deg, #072d59 0%, #56556a 100%);
+ color: white;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
+}
+
+.header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px 15px;
+ background: rgba(0, 0, 0, 0.2);
+ backdrop-filter: blur(10px);
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+
+ @media (min-width: 768px) {
+ padding: 16px 20px;
+ }
+
+ h1 {
+ margin: 0;
+ font-size: 18px;
+ font-weight: 600;
+
+ @media (min-width: 768px) {
+ font-size: 24px;
+ }
+ }
+
+ .spacer {
+ width: 80px; // Para balancear el botón
+
+ @media (min-width: 768px) {
+ width: 100px;
+ }
+ }
+}
+
+.back-button {
+ padding: 8px 16px;
+ background: rgba(255, 255, 255, 0.1);
+ color: white;
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ border-radius: 8px;
+ font-size: 14px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.3s ease;
+
+ @media (min-width: 768px) {
+ padding: 10px 20px;
+ font-size: 16px;
+ }
+
+ &:hover {
+ background: rgba(255, 255, 255, 0.2);
+ }
+
+ &:active {
+ transform: scale(0.98);
+ }
+}
+
+.filters {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12px;
+ padding: 12px 15px;
+ background: rgba(0, 0, 0, 0.15);
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+ align-items: center;
+
+ @media (min-width: 768px) {
+ padding: 15px 20px;
+ gap: 20px;
+ }
+}
+
+.filter-checkbox {
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ font-size: 13px;
+
+ @media (min-width: 768px) {
+ font-size: 15px;
+ }
+
+ input[type="checkbox"] {
+ margin-right: 6px;
+ cursor: pointer;
+ width: 16px;
+ height: 16px;
+
+ @media (min-width: 768px) {
+ width: 18px;
+ height: 18px;
+ }
+ }
+
+ .checkbox-label {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ }
+}
+
+.status-dot {
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ display: inline-block;
+
+ &.green {
+ background: #4caf50;
+ box-shadow: 0 0 6px rgba(76, 175, 80, 0.6);
+ }
+
+ &.orange {
+ background: #ff9800;
+ box-shadow: 0 0 6px rgba(255, 152, 0, 0.6);
+ }
+}
+
+.total-count {
+ margin-left: auto;
+ font-size: 12px;
+ opacity: 0.8;
+
+ @media (min-width: 768px) {
+ font-size: 14px;
+ }
+}
+
+.map {
+ flex: 1;
+ width: 100%;
+ position: relative;
+}
+
+.loading, .error {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ flex: 1;
+ text-align: center;
+ padding: 20px;
+}
+
+.spinner {
+ width: 50px;
+ height: 50px;
+ border: 4px solid rgba(255, 255, 255, 0.3);
+ border-top-color: white;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+.error button {
+ margin-top: 20px;
+ padding: 12px 24px;
+ background: white;
+ color: #4a90e2;
+ border: none;
+ border-radius: 20px;
+ font-size: 16px;
+ font-weight: 600;
+ cursor: pointer;
+
+ &:hover {
+ opacity: 0.9;
+ }
+}
+
+.legend {
+ padding: 10px 15px;
+ background: rgba(0, 0, 0, 0.2);
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
+
+ @media (min-width: 768px) {
+ padding: 12px 20px;
+ }
+}
+
+.legend-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 12px;
+ margin-bottom: 6px;
+
+ @media (min-width: 768px) {
+ font-size: 14px;
+ }
+
+ .legend-icon {
+ font-size: 18px;
+
+ @media (min-width: 768px) {
+ font-size: 20px;
+ }
+ }
+}
+
+.legend-note {
+ font-size: 11px;
+ opacity: 0.7;
+ margin-top: 4px;
+
+ @media (min-width: 768px) {
+ font-size: 13px;
+ }
+}
+
+// Estilos para los marcadores personalizados
+:host ::ng-deep {
+ .custom-marker {
+ background: transparent;
+ border: none;
+ }
+
+ .marker-pin {
+ width: 30px;
+ height: 42px;
+ position: relative;
+
+ &::before {
+ content: '';
+ position: absolute;
+ width: 24px;
+ height: 24px;
+ border-radius: 50% 50% 50% 0;
+ transform: rotate(-45deg);
+ left: 3px;
+ top: 0;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
+ }
+
+ &.green::before {
+ background: #4caf50;
+ }
+
+ &.orange::before {
+ background: #ff9800;
+ }
+
+ .marker-icon {
+ position: absolute;
+ top: 2px;
+ left: 8px;
+ font-size: 16px;
+ z-index: 1;
+ }
+ }
+
+ // Tooltips
+ .leaflet-tooltip {
+ background: rgba(0, 0, 0, 0.85);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ border-radius: 8px;
+ color: white;
+ font-size: 12px;
+ padding: 8px 12px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
+
+ &::before {
+ border-top-color: rgba(0, 0, 0, 0.85);
+ }
+
+ .station-tooltip {
+ strong {
+ display: block;
+ margin-bottom: 4px;
+ font-size: 13px;
+ }
+
+ .status {
+ display: inline-block;
+ margin-top: 4px;
+ padding: 2px 6px;
+ border-radius: 4px;
+ font-size: 10px;
+ font-weight: 600;
+
+ &.operativa {
+ background: rgba(76, 175, 80, 0.3);
+ color: #4caf50;
+ }
+
+ &.baixa-temporal {
+ background: rgba(255, 152, 0, 0.3);
+ color: #ff9800;
+ }
+ }
+ }
+ }
+}
diff --git a/src/app/components/mapa-estacions/mapa-estacions.ts b/src/app/components/mapa-estacions/mapa-estacions.ts
new file mode 100644
index 0000000..4e06dc2
--- /dev/null
+++ b/src/app/components/mapa-estacions/mapa-estacions.ts
@@ -0,0 +1,171 @@
+import { Component, OnInit, AfterViewInit, OnDestroy, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { Router } from '@angular/router';
+import * as L from 'leaflet';
+import { MeteocatService } from '../../services/meteocat.service';
+import { EstacioXema } from '../../models/estacio-xema.model';
+
+@Component({
+ selector: 'app-mapa-estacions',
+ imports: [CommonModule],
+ templateUrl: './mapa-estacions.html',
+ styleUrl: './mapa-estacions.scss',
+})
+export class MapaEstacions implements OnInit, AfterViewInit, OnDestroy {
+ private map: L.Map | null = null;
+ private markers: L.Marker[] = [];
+
+ estacions = signal([]);
+ loading = signal(true);
+ error = signal(null);
+
+ // Filtros
+ showOperatives = signal(true);
+ showBaixaTemporal = signal(true);
+
+ constructor(
+ private meteocatService: MeteocatService,
+ private router: Router
+ ) {}
+
+ ngOnInit(): void {
+ this.loadEstacions();
+ }
+
+ ngAfterViewInit(): void {
+ this.initMap();
+ }
+
+ ngOnDestroy(): void {
+ if (this.map) {
+ this.map.remove();
+ }
+ }
+
+ loadEstacions(): void {
+ this.loading.set(true);
+ this.meteocatService.getAllEstacions().subscribe({
+ next: (estacions) => {
+ this.estacions.set(estacions);
+ this.loading.set(false);
+ if (this.map) {
+ this.addMarkers();
+ }
+ },
+ error: (err) => {
+ console.error('Error loading estacions:', err);
+ this.error.set('Error al carregar les estacions meteorològiques');
+ this.loading.set(false);
+ }
+ });
+ }
+
+ initMap(): void {
+ // Crear mapa centrado en Cataluña
+ this.map = L.map('map', {
+ center: [41.8, 1.5],
+ zoom: 8,
+ minZoom: 7,
+ maxZoom: 18
+ });
+
+ // Añadir capa de OpenStreetMap
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
+ attribution: '© OpenStreetMap contributors'
+ }).addTo(this.map);
+
+ // Añadir marcadores si ya se cargaron las estaciones
+ if (this.estacions().length > 0) {
+ this.addMarkers();
+ }
+ }
+
+ addMarkers(): void {
+ if (!this.map) return;
+
+ // Limpiar marcadores anteriores
+ this.markers.forEach(marker => marker.remove());
+ this.markers = [];
+
+ const filteredEstacions = this.estacions().filter(estacio => {
+ if (estacio.estat === 'Operativa' && this.showOperatives()) return true;
+ if (estacio.estat === 'Baixa temporal' && this.showBaixaTemporal()) return true;
+ return false;
+ });
+
+ filteredEstacions.forEach(estacio => {
+ if (!estacio.coordenadesLatitud || !estacio.coordenadesLongitud) {
+ return;
+ }
+
+ // Icono personalizado según estado
+ const iconColor = estacio.estat === 'Operativa' ? 'green' : 'orange';
+ const icon = L.divIcon({
+ className: 'custom-marker',
+ html: `
+
+ `,
+ iconSize: [30, 42],
+ iconAnchor: [15, 42],
+ popupAnchor: [0, -42]
+ });
+
+ const marker = L.marker(
+ [estacio.coordenadesLatitud, estacio.coordenadesLongitud],
+ { icon }
+ ).addTo(this.map!);
+
+ // Tooltip (hover)
+ marker.bindTooltip(`
+
+ ${estacio.nom}
+ ${estacio.emplacament || ''}
+ ${estacio.estat}
+
+ `, {
+ direction: 'top',
+ offset: [0, -42]
+ });
+
+ // Popup (click) - navegar al municipio
+ marker.on('click', () => {
+ if (estacio.municipiCodi) {
+ this.navigateToMunicipi(estacio.municipiCodi);
+ }
+ });
+
+ this.markers.push(marker);
+ });
+ }
+
+ navigateToMunicipi(municipiCodi: string): void {
+ // Cerrar el mapa y volver a Home con el municipio seleccionado
+ this.router.navigate(['/'], {
+ queryParams: { municipi: municipiCodi }
+ });
+ }
+
+ toggleOperatives(): void {
+ this.showOperatives.set(!this.showOperatives());
+ this.addMarkers();
+ }
+
+ toggleBaixaTemporal(): void {
+ this.showBaixaTemporal.set(!this.showBaixaTemporal());
+ this.addMarkers();
+ }
+
+ getFilteredCount(): number {
+ return this.estacions().filter(estacio => {
+ if (estacio.estat === 'Operativa' && this.showOperatives()) return true;
+ if (estacio.estat === 'Baixa temporal' && this.showBaixaTemporal()) return true;
+ return false;
+ }).length;
+ }
+
+ goBack(): void {
+ this.router.navigate(['/']);
+ }
+}
diff --git a/src/app/models/estacio-xema.model.ts b/src/app/models/estacio-xema.model.ts
index 8d3a1a0..6bbbc79 100644
--- a/src/app/models/estacio-xema.model.ts
+++ b/src/app/models/estacio-xema.model.ts
@@ -4,13 +4,15 @@ export interface EstacioXema {
tipus: string;
coordenadesLatitud: number;
coordenadesLongitud: number;
- altitud?: number;
- municipi?: string;
- comarca?: string;
- provincia?: string;
- xarxa?: string;
- estat?: string;
- dataInici?: string;
- dataFi?: string;
- distancia?: number; // Calculado dinámicamente para estación más cercana
+ altitud: number;
+ emplacament: string;
+ municipiCodi: string;
+ comarcaId: number;
+ provinciaCodi: string;
+ xarxaCodi: string;
+ estat: string;
+ dataInici: string | null;
+ dataFi: string | null;
+ createdAt: string;
+ updatedAt: string;
}