Browse Source

update 1

pull/12/head
PhamY0601 1 year ago
parent
commit
88386a9f69
8 changed files with 444 additions and 156 deletions
  1. +5
    -1
      angular.json
  2. +1
    -0
      package.json
  3. +52
    -3
      src/app/modules/overview/map/map.component.scss
  4. +376
    -109
      src/app/modules/overview/map/map.component.ts
  5. +2
    -28
      src/app/modules/overview/overall-ground/overall-ground.component.html
  6. +8
    -8
      src/app/modules/overview/overall-ground/overall-ground.component.scss
  7. +0
    -6
      src/app/modules/overview/overall-ground/overall-ground.component.ts
  8. +0
    -1
      src/index.html

+ 5
- 1
angular.json View File

"@angular/material/prebuilt-themes/indigo-pink.css", "@angular/material/prebuilt-themes/indigo-pink.css",
"src/styles.scss", "src/styles.scss",
"node_modules/leaflet/dist/leaflet.css", "node_modules/leaflet/dist/leaflet.css",
"node_modules/leaflet.markercluster/dist/MarkerCluster.Default.css",
"node_modules/leaflet.markercluster/dist/MarkerCluster.css",
"node_modules/ngx-toastr/toastr.css" "node_modules/ngx-toastr/toastr.css"
], ],
"scripts": [], "scripts": [],
"styles": [ "styles": [
"@angular/material/prebuilt-themes/indigo-pink.css", "@angular/material/prebuilt-themes/indigo-pink.css",
"src/styles.scss", "src/styles.scss",
"node_modules/leaflet/dist/leaflet.css"
"node_modules/leaflet/dist/leaflet.css",
"node_modules/leaflet.markercluster/dist/MarkerCluster.Default.css",
"node_modules/leaflet.markercluster/dist/MarkerCluster.css"
], ],
"scripts": [] "scripts": []
} }

+ 1
- 0
package.json View File

"@angular/router": "^16.2.0", "@angular/router": "^16.2.0",
"@asymmetrik/ngx-leaflet": "^16.0.1", "@asymmetrik/ngx-leaflet": "^16.0.1",
"@asymmetrik/ngx-leaflet-markercluster": "^16.0.0", "@asymmetrik/ngx-leaflet-markercluster": "^16.0.0",
"@types/leaflet.markercluster": "^1.5.4",
"express": "^4.19.2", "express": "^4.19.2",
"express-ws": "^5.0.2", "express-ws": "^5.0.2",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",

+ 52
- 3
src/app/modules/overview/map/map.component.scss View File

padding: 3px 2px; padding: 3px 2px;
background-color: rbg(243,49,82,0.1); background-color: rbg(243,49,82,0.1);
} }
.hidden-background{
background: transparent;
border: none;

::ng-deep.sensor-on {
width: 30px;
height: 30px;
position: relative;
background: linear-gradient(#ff0000, #C70039);
display: flex !important;
justify-content: center;
align-items: center;
border-radius: 50%;
img{
z-index: 9;
}
&:before, &:after {
position: absolute;
content: '';
width: 100%;
height: 100%;
background: #ff0000;
border-radius: 50%;
z-index: 1;
}

&:before {
animation: sensor-on 2s ease-out infinite;
}

&:after {
animation: sensor-on 2s 1s ease-out infinite;
}
}
.sensor-off{
display: inline-block;
}
@keyframes sensor-on {
100% {
transform: scale(2);
opacity: 0;
}
}
::ng-deep tooltip {
display: flex;
flex-direction: column;
gap: 5px;

}

::ng-deep.icon {
width: 200px;
display: flex;
flex-direction: column;
align-items: center;
} }

+ 376
- 109
src/app/modules/overview/map/map.component.ts View File

import { AfterViewInit, Component, OnDestroy, OnInit } from '@angular/core';
import {
AfterViewInit,
Component,
OnDestroy,
OnInit
} from '@angular/core';
import * as L from 'leaflet'; import * as L from 'leaflet';
import 'leaflet.markercluster';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { SocketService } from '../../../shared/services/socket.service'; import { SocketService } from '../../../shared/services/socket.service';
import {ICON} from "../../../app.constants";



const alarmData = [
{
title: ' Alarm System 1',
detail: {
position: 'Hanoi',
coordinates: {
lat: 21.0285,
lng: 105.8542
},
time: '06-07-2024'
},
warning: true
},
{
title: ' Alarm System 2',
detail: {
position: 'Hai Phong',
coordinates: {
lat: 20.8449,
lng: 106.6881
},
time: '06-07-2024'
},
warning: true
},
{
title: ' Alarm System 3',
detail: {
position: 'Ha Long',
coordinates: {
lat: 20.9460,
lng: 107.0740
},
time: '06-07-2024'
},
warning: false
},
{
title: ' Alarm System 4',
detail: {
position: 'Vinh',
coordinates: {
lat: 18.6796,
lng: 105.6813
},
time: '06-07-2024'
},
warning: true
},
{
title: ' Alarm System 5',
detail: {
position: 'Dong Hoi',
coordinates: {
lat: 17.4834,
lng: 106.6000
},
time: '06-07-2024'
},
warning: false
},
{
title: ' Alarm System 6',
detail: {
position: 'Hue',
coordinates: {
lat: 16.4637,
lng: 107.5909
},
time: '06-07-2024'
},
warning: true
},
{
title: ' Alarm System 7',
detail: {
position: 'Da Nang',
coordinates: {
lat: 16.0471,
lng: 108.2068
},
time: '06-07-2024'
},
warning: false
},
{
title: ' Alarm System 8',
detail: {
position: 'Quy Nhon',
coordinates: {
lat: 13.7820,
lng: 109.2198
},
time: '06-07-2024'
},
warning: false
},
{
title: ' Alarm System 9',
detail: {
position: 'Nha Trang',
coordinates: {
lat: 12.2388,
lng: 109.1967
},
time: '06-07-2024'
},
warning: true
},
{
title: ' Alarm System 10',
detail: {
position: 'Da Lat',
coordinates: {
lat: 11.9416,
lng: 108.4580
},
time: '06-07-2024'
},
warning: false
},
{
title: ' Alarm System 11',
detail: {
position: 'Ho Chi Minh City',
coordinates: {
lat: 10.8231,
lng: 106.6297
},
time: '06-07-2024'
},
warning: false
},
{
title: ' Alarm System 12',
detail: {
position: 'Can Tho',
coordinates: {
lat: 10.0452,
lng: 105.7469
},
time: '06-07-2024'
},
warning: true
},
{
title: ' Alarm System 13',
detail: {
position: 'District 1, Ho Chi Minh City',
coordinates: {
lat: 10.7769,
lng: 106.7009
},
time: '06-07-2024'
},
warning: false
},
{
title: ' Alarm System 14',
detail: {
position: 'District 2, Ho Chi Minh City',
coordinates: {
lat: 10.7873,
lng: 106.7494
},
time: '06-07-2024'
},
warning: false
},
{
title: ' Alarm System 15',
detail: {
position: 'District 3, Ho Chi Minh City',
coordinates: {
lat: 10.7842,
lng: 106.6951
},
time: '06-07-2024'
},
warning: false
},
{
title: ' Alarm System 16',
detail: {
position: 'District 4, Ho Chi Minh City',
coordinates: {
lat: 10.7579,
lng: 106.7049
},
time: '06-07-2024'
},
warning: false
},
{
title: ' Alarm System 17',
detail: {
position: 'District 5, Ho Chi Minh City',
coordinates: {
lat: 10.7547,
lng: 106.6635
},
time: '06-07-2024'
},
warning: false
},
{
title: ' Alarm System 18',
detail: {
position: 'District 6, Ho Chi Minh City',
coordinates: {
lat: 10.7540,
lng: 106.6356
},
time: '06-07-2024'
},
warning: true
},
{
title: ' Alarm System 19',
detail: {
position: 'District 7, Ho Chi Minh City',
coordinates: {
lat: 10.7380,
lng: 106.7214
},
time: '06-07-2024'
},
warning: false
},
{
title: ' Alarm System 20',
detail: {
position: 'District 8, Ho Chi Minh City',
coordinates: {
lat: 10.7240,
lng: 106.6291
},
time: '06-07-2024'
},
warning: true
},
{
title: ' Alarm System 21',
detail: {
position: 'District 9, Ho Chi Minh City',
coordinates: {
lat: 10.8410,
lng: 106.8287
},
time: '06-07-2024'
},
warning: true
},
{
title: ' Alarm System 22',
detail: {
position: 'District 10, Ho Chi Minh City',
coordinates: {
lat: 10.7764,
lng: 106.6676
},
time: '06-07-2024'
},
warning: false
}
];


const alarmDemo = {
title: ' Alarm System demo', // Thêm mới warning
detail: {
position: 'Vinhomes Quận 9',
coordinates: {
lat: 10.7483,
lng: 106.8016
},
time: '06-07-2024'
},
}
@Component({ @Component({
selector: 'app-map', selector: 'app-map',
templateUrl: './map.component.html', templateUrl: './map.component.html',
}) })
export class MapComponent implements OnInit, AfterViewInit, OnDestroy { export class MapComponent implements OnInit, AfterViewInit, OnDestroy {
private map!: L.Map; private map!: L.Map;
private markers!: L.MarkerClusterGroup;
private statusSubscription?: Subscription; private statusSubscription?: Subscription;
private messageSubscription?: Subscription; private messageSubscription?: Subscription;
sensorSmoke = ICON.sensorActiveSmoke;
sensorVib = ICON.sensorActiveVib;

sensorSmokeIcon = L.divIcon({
html: this.sensorSmoke,
iconSize: [170, 48],
className: 'hidden-background'
});

sensorVibIcon = L.divIcon({
html: this.sensorVib,
iconSize: [170, 48],
className: 'hidden-background'
});
data = alarmData;
alarmDemo = alarmDemo;
state1 = false;
state2 = false;
state5 = false; state5 = false;
state6 = false; state6 = false;
isReady = true;


constructor(private socketService$: SocketService) { } constructor(private socketService$: SocketService) { }


ngOnInit() { ngOnInit() {
this.statusSubscription = this.socketService$.status$.subscribe(isConnected => { this.statusSubscription = this.socketService$.status$.subscribe(isConnected => {
if (isConnected) { if (isConnected) {
let str = {id: '0', type: 'get'};
this.socketService$.sendMessage(str);
this.socketService$.sendMessage({ id: '0', type: 'get' });
this.messageSubscription = this.socketService$.messages$.subscribe(message => { this.messageSubscription = this.socketService$.messages$.subscribe(message => {
this.onMessage(message); this.onMessage(message);
}); });
this.socketService$.close(); this.socketService$.close();
} }


private initMap(): void {
initMap(): void {
const mapContainer = document.getElementById('map'); const mapContainer = document.getElementById('map');
if (mapContainer) { if (mapContainer) {
this.map = L.map('map', { this.map = L.map('map', {
center: [10.8494, 106.7537],
center: [10.7483, 106.7537],
zoom: 12 zoom: 12
}); });


L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18,
maxZoom: 15,
minZoom: 3, minZoom: 3,
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>' attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(this.map); }).addTo(this.map);
} }


onMessage(message: any) { onMessage(message: any) {
if (message.id == '0' && message.type === 'get') {
this.state5 = message.state5 === '1';
this.state6 = message.state6 === '1';
if (message.id === '0' && message.type === 'get') {
this.state1 = message.state1 === '0';// 1 OFF // alarm 12h
this.state2 = message.state2 === '0'; // 1 OFF // alarm 1h
this.state5 = message.state5 === '1';// alarm 9h && 6h // 1 ON, 0 OFF
this.isReady = message.ready === '1';
this.updateIcons(); this.updateIcons();
} }
} }


addIconToMap() { addIconToMap() {
const popupContent = `<div>Vinhome quận 9</div>
<a href="/overview/overall-ground" target="_blank">Xem chi tiết</a>`;

const fireContent = ICON.fireContent;

L.marker([10.8356, 106.8300], {icon: this.applyIcon(this.state5, this.state6, 'smoke')})
.addTo(this.map)
this.markers = L.markerClusterGroup();
this.data.forEach((item: any) => {
// create icon
const icon = this.createIcon(item.warning);
// add icon into map
L.marker([item.detail.coordinates.lat, item.detail.coordinates.lng], { icon: icon })
.bindPopup(this.popupDetail(item))
.addTo(this.markers);
});


L.marker([10.8661, 106.8029], {icon: this.applyIcon(this.state5, this.state6, 'vib')})
.addTo(this.map)
const iconDemo = this.getIcon(this.state5, this.isReady, this.state1, this.state2);
L.marker([this.alarmDemo.detail.coordinates.lat,this.alarmDemo.detail.coordinates.lng], { icon: iconDemo })
.bindPopup(this.popupDetail(this.alarmDemo))
.addTo(this.markers);


this.markers.addTo(this.map);
} }


updateIcons(): void { updateIcons(): void {
this.map.eachLayer((layer:any) => {
if (layer instanceof L.Marker as any) {
this.map.removeLayer(layer);
}
});

//clear layer icon mỗi lần update
this.markers.clearLayers();
this.addIconToMap(); this.addIconToMap();
} }


applyIcon(state1: boolean, state2: boolean, type:any) {
if (state1 && state2) {
return type === 'smoke' ? this.sensorSmokeIcon : this.sensorVibIcon;
}
const isOn = state1;
const systemName = type === 'smoke' ? 'Hệ thống báo động 1' : 'Hệ thống báo động 2';
const htmlContent = this.createSensorIconHTML(
systemName,
'120 Xa Lộ Hà Nội, Thành Phố, Thủ Đức, Thành phố Hồ Chí Minh',
'10.8661° N, 106.8029° E',
'01:54, 16/05/2022',
isOn
);
return L.divIcon({
html: htmlContent,
iconSize: [150, 38],
className: 'hidden-background'
});
popupDetail(item: any): string {
return `
<div class="tooltip" style="width: 200px">
<div style="display: flex; flex-direction: row; justify-content: space-between; margin-bottom: 5px">
<div style="color: #F33152; padding: 5px 13px; background-color: rgba(243, 49, 82, 0.1); border-radius: 20px; text-align: center; max-width: 180px;">${item.title}</div>
<a href="/overview/camera-stream" target="_blank">
<svg width="24px" height="24px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none">
<g stroke="currentColor" stroke-width="2">
<path d="M16 16V8a1 1 0 00-1-1H5a1 1 0 00-1 1v8a1 1 0 001 1h10a1 1 0 001-1z"/>
<path stroke-linejoin="round" d="M20 7l-4 3v4l4 3V7z"/>
</g>
</svg>
</a>
</div>
<div><strong>Địa điểm:</strong> ${item.detail.position}</div>
<div><strong>Tọa độ:</strong> ${item.detail.coordinates.lat}, ${item.detail.coordinates.lng}</div>
<div><strong>Thời gian:</strong> ${item.detail.time}</div>
<a href="/overview/overall-ground" target="_blank">Xem chi tiết</a>
</div>`;
} }


createIcon(active: boolean, className: string = '', text: any = []): L.Icon<L.IconOptions> | L.DivIcon {
if (text.length < 1) {
return L.icon({
iconUrl: active ? '../../../../assets/images/sensor-on.png' : '../../../../assets/images/sensor-off.png',
iconSize: [30, 30],
className: className
});
} else {
let htmlContent = '';
text.forEach((item: any) => {
htmlContent += `<span><b>${item}</b></span><br>`;
});


createSensorIconHTML(systemName:any, location:any, coordinates:any, time:any, isOn:any) {
const imageUrl = isOn ? 'assets/images/sensor-on.png' : 'assets/images/sensor-off.png';
return `<div style="display: flex; flex-direction: column; align-items: center">
<div style="display: flex; flex-direction: row; gap: 5px">
<div class="tooltip">
<img style="width: 30px; height: 30px;" src="${imageUrl}">
<div class="tooltiptext">
<div style="display: flex; flex-direction: row; justify-content: space-between;">
<div style="color: #F33152; padding: 5px 13px; background-color: rgba(243, 49, 82, 0.1); border-radius: 20px; text-align: center; width: 120px;">${systemName}</div>
<a href="/overview/camera-stream">
<svg width="24px" height="24px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none">
<g stroke="currentColor" stroke-width="2">
<path d="M16 16V8a1 1 0 00-1-1H5a1 1 0 00-1 1v8a1 1 0 001 1h10a1 1 0 001-1z"/>
<path stroke-linejoin="round" d="M20 7l-4 3v4l4 3V7z"/>
</g>
</svg>
</a>
</div>
<div><strong>Địa điểm:</strong> ${location}</div>
<div><strong>Tọa độ:</strong> ${coordinates}</div>
<div><strong>Thời gian:</strong> ${time}</div>
<a href="/overview/overall-ground" target="_blank">Xem chi tiết</a>
</div>
</div>
<a href="/overview/camera-stream" target="_blank">
<img style="width: 30px; height: 30px;" src="assets/images/camera.png">
</a>
</div>
</div>
<style>
.tooltip {
position: relative;
display: inline-block;
}
.tooltip .tooltiptext {
visibility: hidden;
width: 300px;
padding: 10px;
background-color: #fff;
color: black;
border-radius: 6px;
position: absolute;
z-index: 1;
bottom: 100%;
left: 50%;
margin-left: -60px;
}
.tooltip:hover .tooltiptext {
visibility: visible;
}
</style>`;
return L.divIcon({
html: `<div class="icon">
<div style="z-index:9999" class="sensor-on">
<img src ="assets/images/sensor-on.png" style="width: 30px; height: 30px">
</div>
<div style="background: #F11E1E; color: #FFF; padding: 2px 3px; font-size: 11px; margin-top: 10px; text-align: center">
${htmlContent}
</div>
</div>`,
iconSize: [100, 30],
className: className
});
}
} }


getIcon(isTurnOn: boolean, isReady: boolean, fireArm: boolean, fenceArm: boolean): L.Icon<L.IconOptions> | L.DivIcon {
if (isTurnOn && isReady) {
let text = [];
if (fireArm && fenceArm) {
text.push('FIRE ALARM', 'FENCE ALARM');
} else if (fireArm) {
text.push('FIRE ALARM');
} else if (fenceArm) {
text.push('FENCE ALARM');
}
return this.createIcon(true, '', text);
}
return this.createIcon(false);
}
} }

+ 2
- 28
src/app/modules/overview/overall-ground/overall-ground.component.html View File

<div class="card-state"> <div class="card-state">
<img src="assets/images/ground.png"> <img src="assets/images/ground.png">


<div class="state t1" id="State1">
<div>
<img [src]="getImageSource()" style="width: 30px; height: 30px">
<a href="/overview/camera-stream" target="_blank">
<img style="width: 30px; height: 30px;" src="assets/images/camera.png">
</a>
</div>
</div>


<div class="state t2" id="State2"> <div class="state t2" id="State2">
<div> <div>
</a> </a>
</div> </div>
<div *ngIf="(state1 && state5 && isReady)" class="alarm-text" <div *ngIf="(state1 && state5 && isReady)" class="alarm-text"
[ngClass]="{'alarm-text-on': (state1 && state5 && isReady) }">ALARM: <br>VIBRATION ALERT</div>
[ngClass]="{'alarm-text-on': (state1 && state5 && isReady) }">FIRE ALARM</div>
</div> </div>


<div class="state t3" id="State3"> <div class="state t3" id="State3">
</a> </a>
</div> </div>
<div *ngIf="(state2 && state5 && isReady)" class="alarm-text" <div *ngIf="(state2 && state5 && isReady)" class="alarm-text"
[ngClass]="{'alarm-text-on': (state2 && state5 && isReady) }">ALARM: <br>DOOR OPENING</div>
[ngClass]="{'alarm-text-on': (state2 && state5 && isReady) }">FENCE ALARM</div>
</div> </div>
<div class="state t4" id="State4"> <div class="state t4" id="State4">
<div> <div>
</div> </div>
</div> </div>
<div class="state t5 tooltip" id="State5" > <div class="state t5 tooltip" id="State5" >
<div>
<div [ngClass]="{'sensor-on': state5 && state6,
'sensor-off': !(state5 && state6)}">
<img [src]="getImageSource()" style="width: 30px; height: 30px"> <img [src]="getImageSource()" style="width: 30px; height: 30px">
</div>
<a href="/overview/camera-stream" target="_blank"> <a href="/overview/camera-stream" target="_blank">
<img style="width: 30px; height: 30px;" src="assets/images/camera.png"> <img style="width: 30px; height: 30px;" src="assets/images/camera.png">
</a> </a>
</div>

<div *ngIf="state5 && state6" class="alarm-text"
[ngClass]="{'alarm-text-on': state5 && state6 }">ALARM: <br>VIBRATION ALERT</div>

</div> </div>
<div class="state t6 tooltip" id="State6"> <div class="state t6 tooltip" id="State6">
<div>
<div [ngClass]="{'sensor-on': state5 && state6,
'sensor-off': !(state5 && state6)}">
<img [src]="getImageSource()" style="width: 30px; height: 30px"> <img [src]="getImageSource()" style="width: 30px; height: 30px">
</div>
<a href="/overview/camera-stream" target="_blank"> <a href="/overview/camera-stream" target="_blank">
<img style="width: 30px; height: 30px;" src="assets/images/camera.png"> <img style="width: 30px; height: 30px;" src="assets/images/camera.png">
</a> </a>
</div>
<div *ngIf="state5 && state6" class="alarm-text"
[ngClass]="{'alarm-text-on': state5 && state6 }">ALARM: <br>SMOKE ALERT</div>
</div> </div>
</div> </div>
</div> </div>
<div fxFlex="30" fxLayout="column" fxLayoutGap="50px"> <div fxFlex="30" fxLayout="column" fxLayoutGap="50px">
<button [disabled]="!isConnected" mat-flat-button color="{{switchArm ? 'accent' : 'primary'}}" (click)="toggleState1()">{{ switchArm ? 'DISARM' : 'ARM'}}</button> <button [disabled]="!isConnected" mat-flat-button color="{{switchArm ? 'accent' : 'primary'}}" (click)="toggleState1()">{{ switchArm ? 'DISARM' : 'ARM'}}</button>
<button [disabled]="!isConnected" mat-flat-button color="{{switchWarning ? 'accent' : 'primary'}}" (click)="toggleState2()">{{ switchWarning ? 'TURN OFF WARNING' : 'TURN ON WARNING'}}</button>
<a style = "padding: 30px 50px;" mat-stroked-button color="primary" href="/overview/camera-stream" target="_blank"> LIVE CAMERA </a>


</div> </div>
</div> </div>

+ 8
- 8
src/app/modules/overview/overall-ground/overall-ground.component.scss View File

left: 10%; left: 10%;
} }
&.t2{ &.t2{
top: 5%;
left: 42%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
} }
&.t3{ &.t3{
top: 5%; top: 5%;
right: 12%;
left: 50%;
transform: translate(-50%)
} }
&.t4{ &.t4{
top: 47%; top: 47%;
} }
&.t5{ &.t5{
top: 88%; top: 88%;
left: 42%;
width: 100px;
.alarm-text-off{
width: 100px !important;
}
left: 50%;
transform: translate(-50%);

} }
&.t6{ &.t6{
top: 47%; top: 47%;

+ 0
- 6
src/app/modules/overview/overall-ground/overall-ground.component.ts View File

this.socketService$.sendMessage(str); this.socketService$.sendMessage(str);
} }


toggleState2() {
this.switchWarning = !this.switchWarning;
let str = {id: '0', type: 'cmd', state2: this.switchWarning.toString()};
this.socketService$.sendMessage(str);
}

getImageSource(): string { getImageSource(): string {
return (this.state5 && this.state6) || this.state5 ? 'assets/images/sensor-on.png' : 'assets/images/sensor-off.png'; return (this.state5 && this.state6) || this.state5 ? 'assets/images/sensor-on.png' : 'assets/images/sensor-off.png';
} }

+ 0
- 1
src/index.html View File

<link rel="preconnect" href="https://fonts.gstatic.com"> <link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<script src="https://unpkg.com/[email protected]/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
</head> </head>
<body class="mat-typography"> <body class="mat-typography">
<app-root></app-root> <app-root></app-root>

Loading…
Cancel
Save