| @@ -1,18 +1,26 @@ | |||
| import { NgModule } from '@angular/core'; | |||
| import { RouterModule, Routes } from '@angular/router'; | |||
| import { LayoutComponent } from './shared/component/layout/layout.component'; | |||
| const routes: Routes = [ | |||
| { path: '', | |||
| redirectTo: '/overview', | |||
| pathMatch: 'full' | |||
| }, | |||
| { path: 'overview', | |||
| loadChildren: () => import('./modules/overview/overview.module').then(m => m.OverviewModule) | |||
| { path: '', redirectTo: '/homepage', pathMatch: 'full' }, | |||
| { | |||
| path: '', | |||
| component: LayoutComponent, | |||
| children: [ | |||
| { | |||
| path: 'homepage', | |||
| loadChildren: () => | |||
| import('./modules/homepage/home-page.module').then( | |||
| (m) => m.HomePageModule, | |||
| ), | |||
| }, | |||
| ], | |||
| }, | |||
| ]; | |||
| @NgModule({ | |||
| imports: [RouterModule.forRoot(routes)], | |||
| exports: [RouterModule] | |||
| exports: [RouterModule], | |||
| }) | |||
| export class AppRoutingModule { } | |||
| export class AppRoutingModule {} | |||
| @@ -1,15 +1,23 @@ | |||
| import {Component, OnInit} from '@angular/core'; | |||
| import {SocketService} from "./shared/services/socket.service"; | |||
| import { | |||
| AfterViewInit, | |||
| Component, | |||
| ElementRef, | |||
| OnInit, | |||
| ViewChild, | |||
| } from '@angular/core'; | |||
| import { SocketService } from './shared/services/socket.service'; | |||
| import { AlarmSoundService } from './shared/services/alarm-sound.service'; | |||
| @Component({ | |||
| selector: 'app-root', | |||
| templateUrl: './app.component.html', | |||
| styleUrls: ['./app.component.scss'] | |||
| styleUrls: ['./app.component.scss'], | |||
| }) | |||
| export class AppComponent implements OnInit{ | |||
| export class AppComponent implements OnInit { | |||
| title = 'Iot-web-ui'; | |||
| constructor(private socketService$: SocketService) { | |||
| } | |||
| constructor(private socketService$: SocketService) {} | |||
| ngOnInit() { | |||
| this.socketService$.connect(); | |||
| } | |||
| @@ -1,308 +0,0 @@ | |||
| export const ICON = { | |||
| sensorOff: `<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="assets/images/sensor-off.png"> | |||
| <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;">Hệ thống báo động 2</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> 120 Xa Lộ Hà Nội, Thành Phố, Thủ Đức, Thành phố Hồ Chí Minh</div> | |||
| <div><strong>Tọa độ:</strong> 10.8661° N, 106.8029° E</div> | |||
| <div><strong>Thời gian:</strong> 01:54, 16/05/2022</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>`, | |||
| sensorOn: `<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="assets/images/sensor-on.png"> | |||
| <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;">Hệ thống báo động 2</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> 120 Xa Lộ Hà Nội, Thành Phố, Thủ Đức, Thành phố Hồ Chí Minh</div> | |||
| <div><strong>Tọa độ:</strong> 10.8661° N, 106.8029° E</div> | |||
| <div><strong>Thời gian:</strong> 01:54, 16/05/2022</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="font-size: 11px"><b>ALARM: SMOKE ALERT</b></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>`, | |||
| sensorActive: `<div> | |||
| <div style="width: 30px; height: 30px; position: absolute; display: flex; justify-content: center; align-items: center; border-radius: 50%; background: linear-gradient(#ff0000, #C70039);"> | |||
| <div style="position: absolute !important; width: 100%; height: 100%; background: #ff0000; border-radius: 50%; z-index: 1; animation: sensor-on 2s ease-out infinite;"></div> | |||
| <div style="position: absolute !important; width: 100%; height: 100%; background: #ff0000; border-radius: 50%; z-index: 1; animation: sensor-on 2s ease-out infinite; animation-delay: 1s;"></div> | |||
| <img src="assets/images/sensor-on.png" style="width: 30px; height: 30px; z-index: 9;"> | |||
| </div> | |||
| <img style="width: 30px; height: 30px;margin-left: 40px;" src="assets/images/camera.png"> | |||
| <style> | |||
| @keyframes sensor-on { | |||
| 100% { | |||
| transform: scale(2); | |||
| opacity: 0; | |||
| } | |||
| } | |||
| </style> | |||
| </div>`, | |||
| fireContent: `<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: 100px;">Sự cố cháy</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> 120 Xa Lộ Hà Nội, Thành Phố, Thủ Đức, Thành phố Hồ Chí Minh</div> | |||
| <div><strong>Tọa độ:</strong> 10.8661° N, 106.8029° E</div> | |||
| <div><strong>Thời gian:</strong> 01:54, 16/05/2022</div> | |||
| <a href="/overview/overall-ground" target="_blank">Xem chi tiết</a>`, | |||
| sensorActiveSmoke: ` | |||
| <div style="display: flex; | |||
| flex-direction: column; | |||
| align-items: center"> | |||
| <div style="display: flex; flex-direction: row; gap: 39px;width: 100%; justify-content: center;"> | |||
| <div class="tooltip"> | |||
| <div style="width: 30px; height: 30px; position: absolute; display: flex; justify-content: center; align-items: center; border-radius: 50%; background: linear-gradient(#ff0000, #C70039);"> | |||
| <div style="position: absolute !important; width: 100%; height: 100%; background: #ff0000; border-radius: 50%; z-index: 1; animation: sensor-on 2s ease-out infinite;"></div> | |||
| <div style="position: absolute !important; width: 100%; height: 100%; background: #ff0000; border-radius: 50%; z-index: 1; animation: sensor-on 2s ease-out infinite; animation-delay: 1s;"></div> | |||
| <img src="assets/images/sensor-on.png" style="width: 30px; height: 30px; z-index: 9;"> | |||
| </div> | |||
| <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;">Hệ thống báo động 1</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> 120 Xa Lộ Hà Nội, Thành Phố, Thủ Đức, Thành phố Hồ Chí Minh</div> | |||
| <div><strong>Tọa độ:</strong> 10.8661° N, 106.8029° E</div> | |||
| <div><strong>Thời gian:</strong> 01:54, 16/05/2022</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="background: #F11E1E; color: #FFF; padding: 2px 3px; font-size: 11px"><b>ALARM: SMOKE ALERT</b></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; | |||
| } | |||
| @keyframes sensor-on { | |||
| 100% { | |||
| transform: scale(2); | |||
| opacity: 0; | |||
| } | |||
| } | |||
| </style>`, | |||
| sensorActiveVib: ` | |||
| <div style="display: flex; | |||
| flex-direction: column; | |||
| align-items: center"> | |||
| <div style="display: flex; flex-direction: row; gap: 39px;width: 100%; justify-content: center;"> | |||
| <div class="tooltip"> | |||
| <div style="width: 30px; height: 30px; position: absolute; display: flex; justify-content: center; align-items: center; border-radius: 50%; background: linear-gradient(#ff0000, #C70039);"> | |||
| <div style="position: absolute !important; width: 100%; height: 100%; background: #ff0000; border-radius: 50%; z-index: 1; animation: sensor-on 2s ease-out infinite;"></div> | |||
| <div style="position: absolute !important; width: 100%; height: 100%; background: #ff0000; border-radius: 50%; z-index: 1; animation: sensor-on 2s ease-out infinite; animation-delay: 1s;"></div> | |||
| <img src="assets/images/sensor-on.png" style="width: 30px; height: 30px; z-index: 9;"> | |||
| </div> | |||
| <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;">Hệ thống báo động 2</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> 120 Xa Lộ Hà Nội, Thành Phố, Thủ Đức, Thành phố Hồ Chí Minh</div> | |||
| <div><strong>Tọa độ:</strong> 10.8661° N, 106.8029° E</div> | |||
| <div><strong>Thời gian:</strong> 01:54, 16/05/2022</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="background: #F11E1E; color: #FFF; padding: 2px 3px; font-size: 11px"><b>ALARM: VIBRATION ALERT</b></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; | |||
| } | |||
| @keyframes sensor-on { | |||
| 100% { | |||
| transform: scale(2); | |||
| opacity: 0; | |||
| } | |||
| } | |||
| </style>`, | |||
| } | |||
| @@ -1,15 +1,17 @@ | |||
| .map-container { | |||
| position: absolute; | |||
| top: 0; | |||
| left: 0; | |||
| right: 0; | |||
| bottom: 0; | |||
| margin: 30px; | |||
| width: 100%; | |||
| height: calc(100% - 7rem); | |||
| h2 { | |||
| padding: .5rem 3rem 0 ; | |||
| text-align: center; | |||
| } | |||
| } | |||
| .map-frame { | |||
| border: 2px solid black; | |||
| height: 90%; | |||
| height: 100%; | |||
| } | |||
| #map { | |||
| @@ -63,11 +65,13 @@ p { | |||
| opacity: 0; | |||
| } | |||
| } | |||
| ::ng-deep tooltip { | |||
| ::ng-deep .tooltip { | |||
| display: flex; | |||
| flex-direction: column; | |||
| gap: 5px; | |||
| .dynamic-button{ | |||
| cursor: pointer; | |||
| } | |||
| } | |||
| ::ng-deep.icon { | |||
| @@ -0,0 +1,21 @@ | |||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||
| import { CentralizedSecurityManagementComponent } from './centralized-security-management.component'; | |||
| describe('CentralizedSecurityManagementComponent', () => { | |||
| let component: CentralizedSecurityManagementComponent; | |||
| let fixture: ComponentFixture<CentralizedSecurityManagementComponent>; | |||
| beforeEach(() => { | |||
| TestBed.configureTestingModule({ | |||
| declarations: [CentralizedSecurityManagementComponent] | |||
| }); | |||
| fixture = TestBed.createComponent(CentralizedSecurityManagementComponent); | |||
| component = fixture.componentInstance; | |||
| fixture.detectChanges(); | |||
| }); | |||
| it('should create', () => { | |||
| expect(component).toBeTruthy(); | |||
| }); | |||
| }); | |||
| @@ -0,0 +1,220 @@ | |||
| import { | |||
| AfterViewInit, | |||
| Component, | |||
| OnDestroy, | |||
| OnInit, | |||
| Renderer2, | |||
| } from '@angular/core'; | |||
| import * as L from 'leaflet'; | |||
| import 'leaflet.markercluster'; | |||
| import { Subscription, take } from 'rxjs'; | |||
| import { SocketService } from '../../../shared/services/socket.service'; | |||
| import { MatDialog } from '@angular/material/dialog'; | |||
| import { AlarmSoundService } from '../../../shared/services/alarm-sound.service'; | |||
| import { CameraDialogComponent } from '../../../shared/component/camera-dialog/camera-dialog.component'; | |||
| import { alarmData, alarmDemo } from '../data/fake-data'; | |||
| import { ConfirmDialogService } from '../../../shared/services/confirm-dialog.service'; | |||
| @Component({ | |||
| selector: 'app-centralized-security-management', | |||
| templateUrl: './centralized-security-management.component.html', | |||
| styleUrls: ['./centralized-security-management.component.scss'], | |||
| }) | |||
| export class CentralizedSecurityManagementComponent | |||
| implements OnInit, AfterViewInit, OnDestroy | |||
| { | |||
| private map!: L.Map; | |||
| private markers!: L.MarkerClusterGroup; | |||
| private statusSubscription?: Subscription; | |||
| private messageSubscription?: Subscription; | |||
| data = alarmData; | |||
| alarmDemo = alarmDemo; | |||
| state1 = false; | |||
| state2 = false; | |||
| state5 = false; | |||
| state6 = false; | |||
| isReady = true; | |||
| constructor( | |||
| private socketService$: SocketService, | |||
| private dialog: MatDialog, | |||
| private renderer: Renderer2, | |||
| private alarmSoundService$: AlarmSoundService, | |||
| ) {} | |||
| ngOnInit() { | |||
| this.statusSubscription = this.socketService$.status$.subscribe( | |||
| (isConnected) => { | |||
| if (isConnected) { | |||
| this.socketService$.sendMessage({ id: '0', type: 'get' }); | |||
| this.messageSubscription = this.socketService$.messages$.subscribe( | |||
| (message) => { | |||
| this.onMessage(message); | |||
| }, | |||
| ); | |||
| } | |||
| }, | |||
| ); | |||
| } | |||
| openDialog(): void { | |||
| this.dialog.open(CameraDialogComponent, { | |||
| width: '80vw', | |||
| data: '', | |||
| }); | |||
| } | |||
| ngAfterViewInit(): void { | |||
| this.initMap(); | |||
| } | |||
| ngOnDestroy(): void { | |||
| this.statusSubscription?.unsubscribe(); | |||
| this.messageSubscription?.unsubscribe(); | |||
| this.socketService$.close(); | |||
| this.alarmSoundService$.stopAlarm(); | |||
| } | |||
| initMap(): void { | |||
| const mapContainer = document.getElementById('map'); | |||
| if (mapContainer) { | |||
| this.map = L.map('map', { | |||
| center: [10.7483, 106.7537], | |||
| zoom: 12, | |||
| }); | |||
| L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { | |||
| maxZoom: 15, | |||
| minZoom: 3, | |||
| attribution: | |||
| '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>', | |||
| }).addTo(this.map); | |||
| this.addIconsToMap(); | |||
| } else { | |||
| console.error('Map container not found'); | |||
| } | |||
| } | |||
| onMessage(message: any) { | |||
| 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'; // 1 ON, 0 OFF | |||
| this.isReady = message.ready === '1'; | |||
| this.alarmSoundService$.startAlarm(this.state5, this.isReady, this.state1, this.state2); | |||
| this.updateIcons(); | |||
| } | |||
| } | |||
| addIconsToMap(): void { | |||
| this.markers = L.markerClusterGroup(); | |||
| this.data.forEach((item) => this.addMarker(item)); | |||
| this.addMarker(this.alarmDemo, true); | |||
| this.map.addLayer(this.markers); | |||
| } | |||
| addMarker(item: any, isDemo: boolean = false): void { | |||
| const icon = isDemo | |||
| ? this.getIcon(this.state5, this.isReady, this.state1, this.state2) | |||
| : this.createIcon(item.warning); | |||
| const marker = L.marker( | |||
| [item.detail.coordinates.lat, item.detail.coordinates.lng], | |||
| { icon }, | |||
| ).bindPopup(this.popupDetail(item)); | |||
| marker.on('popupopen', (event) => this.bindPopupEvents(event)); | |||
| this.markers.addLayer(marker); | |||
| } | |||
| bindPopupEvents(event: any): void { | |||
| const popupContainer = event.popup.getElement(); | |||
| const button = popupContainer?.querySelector('.dynamic-button'); | |||
| if (button) { | |||
| this.renderer.listen(button, 'click', () => this.openDialog()); | |||
| } | |||
| } | |||
| updateIcons(): void { | |||
| this.markers.clearLayers(); | |||
| this.addIconsToMap(); | |||
| } | |||
| 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 class="dynamic-button"> | |||
| <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>`; | |||
| }); | |||
| return L.divIcon({ | |||
| html: `<div class="icon"> | |||
| <div style="z-index:9999" class="sensor-on"> | |||
| <img alt="icon-alarm" 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: [200, 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); | |||
| } | |||
| } | |||
| @@ -0,0 +1,159 @@ | |||
| export 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 | |||
| }, | |||
| ]; | |||
| export const alarmDemo = { | |||
| title: ' Demo Alarm System', // Thêm mới warning | |||
| detail: { | |||
| position: 'Vinhomes Quận 9', | |||
| coordinates: { | |||
| lat: 10.7483, | |||
| lng: 106.8016 | |||
| }, | |||
| time: '06-07-2024' | |||
| }, | |||
| } | |||
| @@ -0,0 +1,28 @@ | |||
| import { NgModule } from '@angular/core'; | |||
| import { CommonModule } from '@angular/common'; | |||
| import {RouterModule} from "@angular/router"; | |||
| import {SharedMaterialModule} from "../../shared/shared-material.module"; | |||
| import { HomePageComponent } from './homepage/home-page.component'; | |||
| import {homePageRoutes} from "./home-page.routing"; | |||
| import {SharedModule} from "../../shared/shared.module"; | |||
| import { CentralizedSecurityManagementComponent } from './centralized-security-management/centralized-security-management.component'; | |||
| import { SecuritySystemDetailsComponent } from './security-system-details/security-system-details.component'; | |||
| import {FormsModule} from "@angular/forms"; | |||
| @NgModule({ | |||
| declarations: [ | |||
| HomePageComponent, | |||
| CentralizedSecurityManagementComponent, | |||
| SecuritySystemDetailsComponent, | |||
| ], | |||
| imports: [ | |||
| CommonModule, | |||
| RouterModule.forChild(homePageRoutes), | |||
| SharedMaterialModule, | |||
| SharedModule, | |||
| FormsModule, | |||
| ], | |||
| }) | |||
| export class HomePageModule {} | |||
| @@ -0,0 +1,19 @@ | |||
| import { Routes } from '@angular/router'; | |||
| import { HomePageComponent } from './homepage/home-page.component'; | |||
| import { CentralizedSecurityManagementComponent } from './centralized-security-management/centralized-security-management.component'; | |||
| import { SecuritySystemDetailsComponent } from './security-system-details/security-system-details.component'; | |||
| export const homePageRoutes: Routes = [ | |||
| { | |||
| path: '', | |||
| component: HomePageComponent, | |||
| }, | |||
| { | |||
| path: 'centralized-security-management', | |||
| component: CentralizedSecurityManagementComponent, | |||
| }, | |||
| { | |||
| path: 'security-system-details', | |||
| component: SecuritySystemDetailsComponent, | |||
| }, | |||
| ]; | |||
| @@ -0,0 +1,41 @@ | |||
| <div fxLayout="row" fxLayoutGap="30px" style="padding: 2rem 3rem 0"> | |||
| <button mat-stroked-button routerLink="./centralized-security-management"> | |||
| Centralized Security Management | |||
| </button> | |||
| <button mat-stroked-button routerLink="./security-system-details"> | |||
| Security System Details | |||
| </button> | |||
| </div> | |||
| <mat-card class="sound-group"> | |||
| <mat-card-header> | |||
| <mat-card-title>WHISTLE TIME: {{whistle.time}}s</mat-card-title> | |||
| </mat-card-header> | |||
| <mat-card-content> | |||
| <div class="volume-group"> | |||
| <app-slider-range | |||
| [value]="whistle.time" | |||
| [icon]="'access_alarm'" | |||
| (valueChange)="whistle.time = $event" | |||
| ></app-slider-range> | |||
| </div> | |||
| </mat-card-content> | |||
| </mat-card> | |||
| <mat-card class="sound-group"> | |||
| <mat-card-header> | |||
| <mat-card-title>24-HOUR ZONE ALARM TIME: {{alarm.time}}s</mat-card-title> | |||
| </mat-card-header> | |||
| <mat-card-content> | |||
| <div class="volume-group"> | |||
| <app-slider-range | |||
| [value]="alarm.time" | |||
| [icon]="'access_alarm'" | |||
| (valueChange)="alarm.time = $event" | |||
| ></app-slider-range> | |||
| </div> | |||
| </mat-card-content> | |||
| </mat-card> | |||
| @@ -0,0 +1,6 @@ | |||
| .sound-group{ | |||
| margin: 2rem 3rem; | |||
| } | |||
| button{ | |||
| color: #ff7723 !important; | |||
| } | |||
| @@ -0,0 +1,21 @@ | |||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||
| import { HomePageComponent } from './home-page.component'; | |||
| describe('HomepageComponent', () => { | |||
| let component: HomePageComponent; | |||
| let fixture: ComponentFixture<HomePageComponent>; | |||
| beforeEach(() => { | |||
| TestBed.configureTestingModule({ | |||
| declarations: [HomePageComponent] | |||
| }); | |||
| fixture = TestBed.createComponent(HomePageComponent); | |||
| component = fixture.componentInstance; | |||
| fixture.detectChanges(); | |||
| }); | |||
| it('should create', () => { | |||
| expect(component).toBeTruthy(); | |||
| }); | |||
| }); | |||
| @@ -0,0 +1,17 @@ | |||
| import { Component } from '@angular/core'; | |||
| @Component({ | |||
| selector: 'app-homepage', | |||
| templateUrl: './home-page.component.html', | |||
| styleUrls: ['./home-page.component.scss'], | |||
| }) | |||
| export class HomePageComponent { | |||
| whistle = { | |||
| time: 10, | |||
| sound: 0, | |||
| }; | |||
| alarm = { | |||
| time: 30, | |||
| sound: 0, | |||
| }; | |||
| } | |||
| @@ -0,0 +1,49 @@ | |||
| <div class="px-3 py-5" fxLayout="row" fxLayoutAlign="space-around center" fxLayoutGap="20px"> | |||
| <div fxFlex="50" class="map-image"> | |||
| <div class="card-state"> | |||
| <img src="assets/images/ground.png"> | |||
| <div class="state t2" id="State2"> | |||
| <div fxLayout="row"> | |||
| <div class="sensor-off" [class.sensor-on]="(state1 && state5 && status1)"> | |||
| <img [src]="(state5 && status1) ? 'assets/images/sensor-on.png' : 'assets/images/sensor-off.png'" style="width: 30px; height: 30px"> | |||
| </div> | |||
| <ng-container [ngTemplateOutlet]="camera"></ng-container> | |||
| </div> | |||
| <div *ngIf="(state1 && state5 && status1)" class="alarm-text" | |||
| [ngClass]="{'alarm-text-on': (state1 && state5 && status1) }">FIRE ALARM</div> | |||
| </div> | |||
| <div class="state t3" id="State3"> | |||
| <div fxLayout="row"> | |||
| <div class="sensor-off" [class.sensor-on]="(state2 && state5 && status2)"> | |||
| <img [src]="(state5 && status2) ? 'assets/images/sensor-on.png' : 'assets/images/sensor-off.png'" style="width: 30px; height: 30px"> | |||
| </div> | |||
| <ng-container [ngTemplateOutlet]="camera"></ng-container> | |||
| </div> | |||
| <div *ngIf="(state2 && state5 && status2)" class="alarm-text" | |||
| [ngClass]="{'alarm-text-on': (state2 && state5 && status2) }">FENCE ALARM</div> | |||
| </div> | |||
| <div class="state t4" id="State4" fxLayout="row" > | |||
| <img [src]="getImageSource()" style="width: 30px; height: 30px"> | |||
| <ng-container [ngTemplateOutlet]="camera"></ng-container> | |||
| </div> | |||
| <div class="state t5 tooltip" id="State5" fxLayout="row"> | |||
| <img [src]="getImageSource()" style="width: 30px; height: 30px"> | |||
| <ng-container [ngTemplateOutlet]="camera"></ng-container> | |||
| </div> | |||
| <div class="state t6 tooltip" id="State6" fxLayout="row"> | |||
| <img [src]="getImageSource()" style="width: 30px; height: 30px"> | |||
| <ng-container [ngTemplateOutlet]="camera"></ng-container> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <div fxFlex="30" fxLayout="column" fxLayoutGap="50px"> | |||
| <button [disabled]="!isConnected" mat-flat-button color="{{switchArm ? 'accent' : 'primary'}}" (click)="toggleState1()">{{ switchArm ? 'DISARM' : 'ARM'}}</button> | |||
| </div> | |||
| </div> | |||
| <ng-template #camera> | |||
| <div (click)="openDialog()"> | |||
| <img style="width: 30px; height: 30px; cursor: pointer" src="assets/images/camera.png"> | |||
| </div> | |||
| </ng-template> | |||
| @@ -18,7 +18,7 @@ h1 { | |||
| .topnav { | |||
| overflow: hidden; | |||
| background-color: #0A1128; | |||
| background-color: #0a1128; | |||
| } | |||
| body { | |||
| @@ -41,18 +41,18 @@ p { | |||
| .card { | |||
| background-color: white; | |||
| box-shadow: 2px 2px 12px 1px rgba(140, 140, 140, .5); | |||
| box-shadow: 2px 2px 12px 1px rgba(140, 140, 140, 0.5); | |||
| } | |||
| .card-title { | |||
| font-size: 1.2rem; | |||
| font-weight: bold; | |||
| color: #034078 | |||
| color: #034078; | |||
| } | |||
| .reading { | |||
| font-size: 1.2rem; | |||
| color: #1282A2; | |||
| color: #1282a2; | |||
| } | |||
| .button { | |||
| @@ -76,7 +76,7 @@ p { | |||
| /*.button:hover {background-color: #0f8b8d}*/ | |||
| .button:active { | |||
| background-color: #0f8b8d; | |||
| box-shadow: 2px 2px #CDCDCD; | |||
| box-shadow: 2px 2px #cdcdcd; | |||
| transform: translateY(2px); | |||
| } | |||
| @@ -93,48 +93,47 @@ p { | |||
| margin: 0 auto; | |||
| } | |||
| .map-image{ | |||
| img{ | |||
| .map-image { | |||
| img { | |||
| height: 100%; | |||
| width: 100%; | |||
| } | |||
| .card-state{ | |||
| .card-state { | |||
| width: 100%; | |||
| height: 100%; | |||
| position: relative; | |||
| } | |||
| .state{ | |||
| .state { | |||
| position: absolute; | |||
| color: red; | |||
| &.t1{ | |||
| &.t1 { | |||
| top: 5%; | |||
| left: 10%; | |||
| } | |||
| &.t2{ | |||
| &.t2 { | |||
| top: 50%; | |||
| left: 50%; | |||
| transform: translate(-50%, -50%); | |||
| } | |||
| &.t3{ | |||
| &.t3 { | |||
| top: 5%; | |||
| left: 50%; | |||
| transform: translate(-50%) | |||
| transform: translate(-50%); | |||
| } | |||
| &.t4{ | |||
| &.t4 { | |||
| top: 47%; | |||
| right: 12%; | |||
| } | |||
| &.t5{ | |||
| &.t5 { | |||
| top: 88%; | |||
| left: 50%; | |||
| transform: translate(-50%); | |||
| } | |||
| &.t6{ | |||
| &.t6 { | |||
| top: 47%; | |||
| left: 5%; | |||
| width: 100px; | |||
| .alarm-text-off{ | |||
| .alarm-text-off { | |||
| width: 100px !important; | |||
| } | |||
| } | |||
| @@ -144,17 +143,18 @@ p { | |||
| width: 30px; | |||
| height: 30px; | |||
| position: absolute; | |||
| background: linear-gradient(#ff0000, #C70039); | |||
| background: linear-gradient(#ff0000, #c70039); | |||
| display: flex !important; | |||
| justify-content: center; | |||
| align-items: center; | |||
| border-radius: 50%; | |||
| img{ | |||
| img { | |||
| z-index: 9; | |||
| } | |||
| &:before, &:after { | |||
| &:before, | |||
| &:after { | |||
| position: absolute; | |||
| content: ''; | |||
| content: ""; | |||
| width: 100%; | |||
| height: 100%; | |||
| background: #ff0000; | |||
| @@ -170,17 +170,17 @@ p { | |||
| animation: sensor-on 2s 1s ease-out infinite; | |||
| } | |||
| } | |||
| .sensor-off{ | |||
| .sensor-off { | |||
| display: inline-block; | |||
| } | |||
| @keyframes sensor-on{ | |||
| 100%{ | |||
| @keyframes sensor-on { | |||
| 100% { | |||
| transform: scale(2); | |||
| opacity: 0; | |||
| } | |||
| } | |||
| } | |||
| .alarm-text{ | |||
| .alarm-text { | |||
| font-size: 10px; | |||
| padding: 2px 4px; | |||
| width: 100px; | |||
| @@ -191,11 +191,10 @@ p { | |||
| background: #bfe9f4; | |||
| color: #004aad; | |||
| } | |||
| &-on{ | |||
| background: #F11E1E; | |||
| color: #FFF; | |||
| &-on { | |||
| background: #f11e1e; | |||
| color: #fff; | |||
| } | |||
| } | |||
| .tooltip { | |||
| position: relative; | |||
| @@ -215,11 +214,10 @@ p { | |||
| bottom: 100%; | |||
| left: -50%; | |||
| } | |||
| .tooltip:hover .tooltiptext { | |||
| visibility: visible; | |||
| cursor: pointer; | |||
| } | |||
| a{ | |||
| a { | |||
| color: blue; | |||
| } | |||
| @@ -0,0 +1,21 @@ | |||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||
| import { SecuritySystemDetailsComponent } from './security-system-details.component'; | |||
| describe('SecuritySystemDetailsComponent', () => { | |||
| let component: SecuritySystemDetailsComponent; | |||
| let fixture: ComponentFixture<SecuritySystemDetailsComponent>; | |||
| beforeEach(() => { | |||
| TestBed.configureTestingModule({ | |||
| declarations: [SecuritySystemDetailsComponent] | |||
| }); | |||
| fixture = TestBed.createComponent(SecuritySystemDetailsComponent); | |||
| component = fixture.componentInstance; | |||
| fixture.detectChanges(); | |||
| }); | |||
| it('should create', () => { | |||
| expect(component).toBeTruthy(); | |||
| }); | |||
| }); | |||
| @@ -1,18 +1,22 @@ | |||
| import { Component, OnDestroy, OnInit } from '@angular/core'; | |||
| import {SocketService} from "../../../shared/services/socket.service"; | |||
| import {Component, OnDestroy, OnInit} from '@angular/core'; | |||
| import {Subscription} from "rxjs"; | |||
| import {SocketService} from "../../../shared/services/socket.service"; | |||
| import {ToastrService} from "ngx-toastr"; | |||
| import {CameraDialogComponent} from "../../../shared/component/camera-dialog/camera-dialog.component"; | |||
| import {MatDialog} from "@angular/material/dialog"; | |||
| import { ConfirmDialogService } from '../../../shared/services/confirm-dialog.service'; | |||
| @Component({ | |||
| selector: 'app-overall-ground', | |||
| templateUrl: './overall-ground.component.html', | |||
| styleUrls: ['./overall-ground.component.scss'] | |||
| selector: 'app-security-system-details', | |||
| templateUrl: './security-system-details.component.html', | |||
| styleUrls: ['./security-system-details.component.scss'] | |||
| }) | |||
| export class OverallGroundComponent implements OnInit, OnDestroy { | |||
| export class SecuritySystemDetailsComponent implements OnInit, OnDestroy { | |||
| isConnected = false; | |||
| state1 = false; | |||
| status1 = false; | |||
| state2 = false; | |||
| status2 = false; | |||
| state3 = ''; | |||
| state4 = ''; | |||
| state5 = false; | |||
| @@ -27,9 +31,10 @@ export class OverallGroundComponent implements OnInit, OnDestroy { | |||
| constructor( | |||
| private socketService$: SocketService, | |||
| private toastr: ToastrService | |||
| ) { | |||
| } | |||
| private toastr: ToastrService, | |||
| private dialog: MatDialog, | |||
| private confirm$: ConfirmDialogService | |||
| ) {} | |||
| ngOnInit() { | |||
| // this.socketService$.connect(); | |||
| @@ -76,7 +81,9 @@ export class OverallGroundComponent implements OnInit, OnDestroy { | |||
| onMessage(message: any) { | |||
| if (message.id == '0' && message.type === 'get') { | |||
| this.state1 = message.state1 === '0'; // 1 OFF // alarm 12h | |||
| this.status1 = message.status1 === '1'; // 0 not, 1 ready, 2 error, 3 bypass | |||
| this.state2 = message.state2 === '0'; // 1 OFF // alarm 1h | |||
| this.status2 = message.status2 === '1'; // 0 not, 1 ready, 2 error, 3 bypass | |||
| this.state3 = message.state3 === '1' ? 'ON' : 'OFF'; | |||
| this.state4 = message.state4 === '1' ? 'ON' : 'OFF'; | |||
| this.state5 = message.state5 === '1'; // alarm 9h && 6h // 1 ON, 0 OFF | |||
| @@ -86,10 +93,23 @@ export class OverallGroundComponent implements OnInit, OnDestroy { | |||
| this.switchWarning = message.state6 === '1';// alarm 9h && 6h // 1 ON, 0 OFF | |||
| this.isReady = message.ready === '1'; | |||
| if (message.ready === '0' && this.state5){ // not ready and ON arm | |||
| this.toastr.warning('System not ready', 'Warning', {timeOut: 5000}); | |||
| if ((message.status1 === '0' || message.status2 === '0') && this.state5) { // not ready and ON arm | |||
| const data = []; | |||
| if (message.status1 === '0'){ | |||
| data.push({key: 'status1', value: false}) | |||
| } | |||
| if (message.status2 === '0'){ | |||
| data.push({key: 'status2', value: false}) | |||
| } | |||
| this.confirm$.openDialog(data); | |||
| // this.toastr.warning('System not ready', 'Warning', {timeOut: 5000}); | |||
| } | |||
| } | |||
| } | |||
| openDialog(): void { | |||
| this.dialog.open(CameraDialogComponent, { | |||
| width: '80vw', | |||
| data: '', | |||
| }); | |||
| } | |||
| } | |||
| @@ -1,2 +0,0 @@ | |||
| <h3 style="text-align: center">Camera Stream</h3> | |||
| <canvas class="video" #videoPlayer></canvas> | |||
| @@ -1,5 +0,0 @@ | |||
| .video { | |||
| width: 78% !important; | |||
| display: block; | |||
| margin: 0 auto | |||
| } | |||
| @@ -1,21 +0,0 @@ | |||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||
| import { CameraStreamComponent } from './camera-stream.component'; | |||
| describe('CameraStreamComponent', () => { | |||
| let component: CameraStreamComponent; | |||
| let fixture: ComponentFixture<CameraStreamComponent>; | |||
| beforeEach(() => { | |||
| TestBed.configureTestingModule({ | |||
| declarations: [CameraStreamComponent] | |||
| }); | |||
| fixture = TestBed.createComponent(CameraStreamComponent); | |||
| component = fixture.componentInstance; | |||
| fixture.detectChanges(); | |||
| }); | |||
| it('should create', () => { | |||
| expect(component).toBeTruthy(); | |||
| }); | |||
| }); | |||
| @@ -1,331 +0,0 @@ | |||
| import { | |||
| AfterViewInit, | |||
| Component, | |||
| OnDestroy, | |||
| OnInit | |||
| } from '@angular/core'; | |||
| import * as L from 'leaflet'; | |||
| import 'leaflet.markercluster'; | |||
| import { Subscription } from 'rxjs'; | |||
| import { SocketService } from '../../../shared/services/socket.service'; | |||
| 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 | |||
| }, | |||
| ]; | |||
| const alarmDemo = { | |||
| title: ' Demo Alarm System', // Thêm mới warning | |||
| detail: { | |||
| position: 'Vinhomes Quận 9', | |||
| coordinates: { | |||
| lat: 10.7483, | |||
| lng: 106.8016 | |||
| }, | |||
| time: '06-07-2024' | |||
| }, | |||
| } | |||
| @Component({ | |||
| selector: 'app-map', | |||
| templateUrl: './map.component.html', | |||
| styleUrls: ['./map.component.scss'] | |||
| }) | |||
| export class MapComponent implements OnInit, AfterViewInit, OnDestroy { | |||
| private map!: L.Map; | |||
| private markers!: L.MarkerClusterGroup; | |||
| private statusSubscription?: Subscription; | |||
| private messageSubscription?: Subscription; | |||
| data = alarmData; | |||
| alarmDemo = alarmDemo; | |||
| state1 = false; | |||
| state2 = false; | |||
| state5 = false; | |||
| state6 = false; | |||
| isReady = true; | |||
| constructor(private socketService$: SocketService) { } | |||
| ngOnInit() { | |||
| this.statusSubscription = this.socketService$.status$.subscribe(isConnected => { | |||
| if (isConnected) { | |||
| this.socketService$.sendMessage({ id: '0', type: 'get' }); | |||
| this.messageSubscription = this.socketService$.messages$.subscribe(message => { | |||
| this.onMessage(message); | |||
| }); | |||
| } | |||
| }); | |||
| } | |||
| ngAfterViewInit(): void { | |||
| this.initMap(); | |||
| } | |||
| ngOnDestroy(): void { | |||
| this.statusSubscription?.unsubscribe(); | |||
| this.messageSubscription?.unsubscribe(); | |||
| this.socketService$.close(); | |||
| } | |||
| initMap(): void { | |||
| const mapContainer = document.getElementById('map'); | |||
| if (mapContainer) { | |||
| this.map = L.map('map', { | |||
| center: [10.7483, 106.7537], | |||
| zoom: 12 | |||
| }); | |||
| L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { | |||
| maxZoom: 15, | |||
| minZoom: 3, | |||
| attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>' | |||
| }).addTo(this.map); | |||
| this.addIconToMap(); | |||
| } else { | |||
| console.error('Map container not found'); | |||
| } | |||
| } | |||
| onMessage(message: any) { | |||
| 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';// 1 ON, 0 OFF | |||
| this.isReady = message.ready === '1'; | |||
| this.updateIcons(); | |||
| } | |||
| } | |||
| addIconToMap() { | |||
| 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); | |||
| }); | |||
| 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 { | |||
| //clear layer icon mỗi lần update | |||
| this.markers.clearLayers(); | |||
| this.addIconToMap(); | |||
| } | |||
| 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>`; | |||
| }); | |||
| return L.divIcon({ | |||
| html: `<div class="icon"> | |||
| <div style="z-index:9999" class="sensor-on"> | |||
| <img alt="icon-alarm" 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: [200, 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); | |||
| } | |||
| } | |||
| @@ -1,58 +0,0 @@ | |||
| <div class="px-3 py-5" fxLayout="row" fxLayoutAlign="space-around center" fxLayoutGap="20px"> | |||
| <div fxFlex="50" class="map-image"> | |||
| <div class="card-state"> | |||
| <img src="assets/images/ground.png"> | |||
| <div class="state t2" id="State2"> | |||
| <div> | |||
| <div class="sensor-off" [class.sensor-on]="(state1 && state5 && isReady)"> | |||
| <img [src]="state5 ? 'assets/images/sensor-on.png' : 'assets/images/sensor-off.png'" style="width: 30px; height: 30px"> | |||
| </div> | |||
| <a href="/overview/camera-stream" target="_blank"> | |||
| <img style="width: 30px; height: 30px;" src="assets/images/camera.png"> | |||
| </a> | |||
| </div> | |||
| <div *ngIf="(state1 && state5 && isReady)" class="alarm-text" | |||
| [ngClass]="{'alarm-text-on': (state1 && state5 && isReady) }">FIRE ALARM</div> | |||
| </div> | |||
| <div class="state t3" id="State3"> | |||
| <div> | |||
| <div class="sensor-off" [class.sensor-on]="(state2 && state5 && isReady)"> | |||
| <img [src]="state5 ? 'assets/images/sensor-on.png' : 'assets/images/sensor-off.png'" style="width: 30px; height: 30px"> | |||
| </div> | |||
| <a href="/overview/camera-stream" target="_blank"> | |||
| <img style="width: 30px; height: 30px;" src="assets/images/camera.png"> | |||
| </a> | |||
| </div> | |||
| <div *ngIf="(state2 && state5 && isReady)" class="alarm-text" | |||
| [ngClass]="{'alarm-text-on': (state2 && state5 && isReady) }">FENCE ALARM</div> | |||
| </div> | |||
| <div class="state t4" id="State4"> | |||
| <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 t5 tooltip" id="State5" > | |||
| <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 class="state t6 tooltip" id="State6"> | |||
| <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> | |||
| <div fxFlex="30" fxLayout="column" fxLayoutGap="50px"> | |||
| <button [disabled]="!isConnected" mat-flat-button color="{{switchArm ? 'accent' : 'primary'}}" (click)="toggleState1()">{{ switchArm ? 'DISARM' : 'ARM'}}</button> | |||
| </div> | |||
| </div> | |||
| @@ -1,21 +0,0 @@ | |||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||
| import { OverallGroundComponent } from './overall-ground.component'; | |||
| describe('OverallGroundComponent', () => { | |||
| let component: OverallGroundComponent; | |||
| let fixture: ComponentFixture<OverallGroundComponent>; | |||
| beforeEach(() => { | |||
| TestBed.configureTestingModule({ | |||
| declarations: [OverallGroundComponent] | |||
| }); | |||
| fixture = TestBed.createComponent(OverallGroundComponent); | |||
| component = fixture.componentInstance; | |||
| fixture.detectChanges(); | |||
| }); | |||
| it('should create', () => { | |||
| expect(component).toBeTruthy(); | |||
| }); | |||
| }); | |||
| @@ -1,25 +0,0 @@ | |||
| import { NgModule } from '@angular/core'; | |||
| import { CommonModule } from '@angular/common'; | |||
| import {OverallGroundComponent} from "./overall-ground/overall-ground.component"; | |||
| import {RouterModule} from "@angular/router"; | |||
| import {overviewRoutes} from "./overview.routing"; | |||
| import {MapComponent} from "./map/map.component"; | |||
| import {SharedMaterialModule} from "../../shared/shared-material.module"; | |||
| import { CameraStreamComponent } from './camera-stream/camera-stream.component'; | |||
| @NgModule({ | |||
| declarations: [ | |||
| OverallGroundComponent, | |||
| MapComponent, | |||
| CameraStreamComponent | |||
| ], | |||
| imports: [ | |||
| CommonModule, | |||
| RouterModule.forChild(overviewRoutes), | |||
| SharedMaterialModule, | |||
| ] | |||
| }) | |||
| export class OverviewModule { } | |||
| @@ -1,21 +0,0 @@ | |||
| import {Routes} from "@angular/router"; | |||
| import {MapComponent} from "./map/map.component"; | |||
| import {OverallGroundComponent} from "./overall-ground/overall-ground.component"; | |||
| import {CameraStreamComponent} from "./camera-stream/camera-stream.component"; | |||
| export const overviewRoutes: Routes = [ | |||
| { | |||
| path: '', | |||
| component: MapComponent, | |||
| }, | |||
| { | |||
| path: 'overall-ground', | |||
| component: OverallGroundComponent | |||
| }, | |||
| { | |||
| path: 'camera-stream', | |||
| component: CameraStreamComponent | |||
| } | |||
| ]; | |||
| @@ -0,0 +1,11 @@ | |||
| <div style="padding: 10px 20px" fxLayout="row" fxLayoutAlign=" center"> | |||
| <div style="text-align: center; font-size: 24px; width: 95%">Camera Stream</div> | |||
| <button mat-icon-button color="warn" (click)="onClose()"> | |||
| <mat-icon>close</mat-icon> | |||
| </button> | |||
| </div> | |||
| <div mat-dialog-content> | |||
| <canvas class="video" #videoPlayer></canvas> | |||
| </div> | |||
| @@ -0,0 +1,9 @@ | |||
| .video { | |||
| width: 90% !important; | |||
| //height: 90%; | |||
| display: block; | |||
| margin: 0 auto | |||
| } | |||
| .mat-mdc-dialog-content{ | |||
| max-height: unset; | |||
| } | |||
| @@ -0,0 +1,21 @@ | |||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||
| import { CameraDialogComponent } from './camera-dialog.component'; | |||
| describe('CameraDialogComponent', () => { | |||
| let component: CameraDialogComponent; | |||
| let fixture: ComponentFixture<CameraDialogComponent>; | |||
| beforeEach(() => { | |||
| TestBed.configureTestingModule({ | |||
| declarations: [CameraDialogComponent] | |||
| }); | |||
| fixture = TestBed.createComponent(CameraDialogComponent); | |||
| component = fixture.componentInstance; | |||
| fixture.detectChanges(); | |||
| }); | |||
| it('should create', () => { | |||
| expect(component).toBeTruthy(); | |||
| }); | |||
| }); | |||
| @@ -1,21 +1,20 @@ | |||
| import {Component, OnInit, ViewChild, ElementRef, AfterViewInit, Renderer2} from '@angular/core'; | |||
| import {AfterViewInit, Component, ElementRef, ViewChild} from '@angular/core'; | |||
| import {MatDialogRef} from "@angular/material/dialog"; | |||
| import {loadPlayer, Player} from "rtsp-relay/browser"; | |||
| @Component({ | |||
| selector: 'app-camera-stream', | |||
| templateUrl: './camera-stream.component.html', | |||
| styleUrls: ['./camera-stream.component.scss'] | |||
| selector: 'app-camera-dialog', | |||
| templateUrl: './camera-dialog.component.html', | |||
| styleUrls: ['./camera-dialog.component.scss'] | |||
| }) | |||
| export class CameraStreamComponent implements OnInit, AfterViewInit{ | |||
| player?: Player; | |||
| export class CameraDialogComponent implements AfterViewInit{ | |||
| player?: Player; | |||
| @ViewChild('videoPlayer') | |||
| videoPlayer?: ElementRef<HTMLCanvasElement>; | |||
| constructor(private el: ElementRef, private renderer: Renderer2) {} | |||
| ngOnInit() { | |||
| } | |||
| constructor(public dialogRef: MatDialogRef<CameraDialogComponent>) { } | |||
| async ngAfterViewInit() { | |||
| const connect = async () => { | |||
| @@ -30,5 +29,8 @@ export class CameraStreamComponent implements OnInit, AfterViewInit{ | |||
| connect(); | |||
| } | |||
| onClose(): void { | |||
| this.dialogRef.close(); | |||
| } | |||
| } | |||
| @@ -0,0 +1,11 @@ | |||
| <h5 mat-dialog-title >CONFIRM TO IGNORE THE WARNING</h5> | |||
| <mat-dialog-content style="padding-bottom: 4px"> | |||
| <div *ngFor="let item of data"> | |||
| <mat-checkbox [(ngModel)]="item.value" (ngModelChange)="submitChange(item)" name="sensor"> | |||
| {{ (item.key == 'status1' ? 'Fire' : 'ff')}} | |||
| </mat-checkbox> | |||
| </div> | |||
| </mat-dialog-content> | |||
| <mat-dialog-actions align="end"> | |||
| <button mat-button mat-dialog-close cdkFocusInitial>Close</button> | |||
| </mat-dialog-actions> | |||
| @@ -0,0 +1,10 @@ | |||
| .mat-mdc-dialog-title { | |||
| border-bottom: solid 1px #eeeeee; | |||
| font-size: 14px; | |||
| color: #FFFFFF; | |||
| background: orange; | |||
| } | |||
| ::ng-deep.mdc-checkbox .mdc-checkbox__native-control:enabled:checked~.mdc-checkbox__background{ | |||
| background: orange !important; | |||
| border: orange !important; | |||
| } | |||
| @@ -0,0 +1,21 @@ | |||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||
| import { ConfirmDialogComponent } from './confirm-dialog.component'; | |||
| describe('ConfirmDialogComponent', () => { | |||
| let component: ConfirmDialogComponent; | |||
| let fixture: ComponentFixture<ConfirmDialogComponent>; | |||
| beforeEach(() => { | |||
| TestBed.configureTestingModule({ | |||
| declarations: [ConfirmDialogComponent] | |||
| }); | |||
| fixture = TestBed.createComponent(ConfirmDialogComponent); | |||
| component = fixture.componentInstance; | |||
| fixture.detectChanges(); | |||
| }); | |||
| it('should create', () => { | |||
| expect(component).toBeTruthy(); | |||
| }); | |||
| }); | |||
| @@ -0,0 +1,25 @@ | |||
| import { Component } from '@angular/core'; | |||
| import { MatDialogRef } from '@angular/material/dialog'; | |||
| @Component({ | |||
| selector: 'app-confirm-dialog', | |||
| templateUrl: './confirm-dialog.component.html', | |||
| styleUrls: ['./confirm-dialog.component.scss'], | |||
| }) | |||
| export class ConfirmDialogComponent { | |||
| constructor(public dialogRef: MatDialogRef<ConfirmDialogComponent>) {} | |||
| data = [ | |||
| { | |||
| key: 'status1', | |||
| value: false, | |||
| }, | |||
| { | |||
| name: 'status2', | |||
| value: false, | |||
| }, | |||
| ]; | |||
| submitChange(item: any): void{ | |||
| console.log(item); | |||
| // call socket here | |||
| } | |||
| } | |||
| @@ -0,0 +1,13 @@ | |||
| <div class="layout-container"> | |||
| <div class="header mat-elevation-z1"> | |||
| <img src="../../../../../../assets/images/logo.png" > | |||
| <div> | |||
| <button mat-button routerLink="/homepage" routerLinkActive="active">Home</button> | |||
| </div> | |||
| </div> | |||
| <div> | |||
| <router-outlet></router-outlet> | |||
| </div> | |||
| </div> | |||
| @@ -0,0 +1,23 @@ | |||
| .layout-container{ | |||
| display: flex; | |||
| flex-direction: column; | |||
| justify-content: space-between; | |||
| .header { | |||
| height: 3rem; | |||
| padding: .25rem 2rem; | |||
| display: flex; | |||
| flex-direction: row; | |||
| align-items: center; | |||
| img{ | |||
| width: 7rem; | |||
| height: 3rem; | |||
| } | |||
| button { | |||
| width: 5rem; | |||
| } | |||
| .active { | |||
| color: #ff7723 !important; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,21 @@ | |||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||
| import { LayoutComponent } from './layout.component'; | |||
| describe('LayoutComponent', () => { | |||
| let component: LayoutComponent; | |||
| let fixture: ComponentFixture<LayoutComponent>; | |||
| beforeEach(() => { | |||
| TestBed.configureTestingModule({ | |||
| declarations: [LayoutComponent] | |||
| }); | |||
| fixture = TestBed.createComponent(LayoutComponent); | |||
| component = fixture.componentInstance; | |||
| fixture.detectChanges(); | |||
| }); | |||
| it('should create', () => { | |||
| expect(component).toBeTruthy(); | |||
| }); | |||
| }); | |||
| @@ -0,0 +1,10 @@ | |||
| import { Component } from '@angular/core'; | |||
| @Component({ | |||
| selector: 'app-layout', | |||
| templateUrl: './layout.component.html', | |||
| styleUrls: ['./layout.component.scss'] | |||
| }) | |||
| export class LayoutComponent { | |||
| } | |||
| @@ -1,27 +1,22 @@ | |||
| import {NgModule} from "@angular/core"; | |||
| import {CommonModule} from "@angular/common"; | |||
| import {SharedMaterialModule} from "../shared-material.module"; | |||
| import {RouterModule} from "@angular/router"; | |||
| const components = [ | |||
| ]; | |||
| import { NgModule } from '@angular/core'; | |||
| import { CommonModule } from '@angular/common'; | |||
| import { SharedMaterialModule } from '../shared-material.module'; | |||
| import { RouterModule } from '@angular/router'; | |||
| import { LayoutComponent } from './layout/layout.component'; | |||
| import { SliderRangeComponent } from './slider-range/slider-range.component'; | |||
| import { FormsModule } from '@angular/forms'; | |||
| import { CameraDialogComponent } from './camera-dialog/camera-dialog.component'; | |||
| import { ConfirmDialogComponent } from './confirm-dialog/confirm-dialog.component'; | |||
| @NgModule({ | |||
| declarations: [ | |||
| // ...components, | |||
| LayoutComponent, | |||
| SliderRangeComponent, | |||
| CameraDialogComponent, | |||
| ConfirmDialogComponent, | |||
| ], | |||
| imports: [ | |||
| CommonModule, | |||
| SharedMaterialModule, | |||
| RouterModule, | |||
| ], | |||
| exports: [ | |||
| // ...components, | |||
| ], | |||
| providers: [] | |||
| imports: [CommonModule, SharedMaterialModule, RouterModule, FormsModule], | |||
| exports: [LayoutComponent, SliderRangeComponent, CameraDialogComponent], | |||
| providers: [], | |||
| }) | |||
| export class SharedComponentModule { | |||
| } | |||
| export class SharedComponentModule {} | |||
| @@ -0,0 +1,41 @@ | |||
| <ng-content></ng-content> | |||
| <div class="volume-group"> | |||
| <div class="speaker"> | |||
| <mat-icon>{{icon}}</mat-icon> | |||
| </div> | |||
| <button class="volume-control" (click)="decreaseVolume()"> | |||
| <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" | |||
| xmlns:xlink="http://www.w3.org/1999/xlink"> | |||
| <title>Icons/Minus square fill</title> | |||
| <g id="Icons/Minus-square-fill" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | |||
| <g id="MinusSquare"> | |||
| <path | |||
| d="M19.5,3 L4.5,3 C3.67157288,3 3,3.67157288 3,4.5 L3,19.5 C3,20.3284271 3.67157288,21 4.5,21 L19.5,21 C20.3284271,21 21,20.3284271 21,19.5 L21,4.5 C21,3.67157288 20.3284271,3 19.5,3 Z M15.75,12.75 L8.25,12.75 C7.83578644,12.75 7.5,12.4142136 7.5,12 C7.5,11.5857864 7.83578644,11.25 8.25,11.25 L15.75,11.25 C16.1642136,11.25 16.5,11.5857864 16.5,12 C16.5,12.4142136 16.1642136,12.75 15.75,12.75 Z" | |||
| id="Shape" fill="currentColor" fill-rule="nonzero"></path> | |||
| <rect id="Rectangle" x="0" y="0" width="24" height="24"></rect> | |||
| </g> | |||
| </g> | |||
| </svg> | |||
| </button> | |||
| <div class="volume-control" style="padding: 1rem 0"> | |||
| <div class="volume-slider"> | |||
| <input type="range" [(ngModel)]="value" (input)="onSliderChange($event)" | |||
| [ngStyle]="onSliderChangeBackground()" | |||
| [max]="max"> | |||
| </div> | |||
| </div> | |||
| <button class="volume-control" (click)="increaseVolume()"> | |||
| <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" | |||
| xmlns:xlink="http://www.w3.org/1999/xlink"> | |||
| <title>Icons/Plus square fill</title> | |||
| <g id="Icons/Plus-square-fill" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | |||
| <g id="Plus"> | |||
| <path | |||
| d="M19.5,3 L4.5,3 C3.67157288,3 3,3.67157288 3,4.5 L3,19.5 C3,20.3284271 3.67157288,21 4.5,21 L19.5,21 C20.3284271,21 21,20.3284271 21,19.5 L21,4.5 C21,3.67157288 20.3284271,3 19.5,3 Z M17.25,12.75 L12.75,12.75 L12.75,17.25 C12.75,17.6642136 12.4142136,18 12,18 C11.5857864,18 11.25,17.6642136 11.25,17.25 L11.25,12.75 L6.75,12.75 C6.33578644,12.75 6,12.4142136 6,12 C6,11.5857864 6.33578644,11.25 6.75,11.25 L11.25,11.25 L11.25,6.75 C11.25,6.33578644 11.5857864,6 12,6 C12.4142136,6 12.75,6.33578644 12.75,6.75 L12.75,11.25 L17.25,11.25 C17.6642136,11.25 18,11.5857864 18,12 C18,12.4142136 17.6642136,12.75 17.25,12.75 Z" | |||
| id="Shape" fill="currentColor" fill-rule="nonzero"></path> | |||
| <rect id="Rectangle" x="0" y="0" width="24" height="24"></rect> | |||
| </g> | |||
| </g> | |||
| </svg> | |||
| </button> | |||
| </div> | |||
| @@ -0,0 +1,149 @@ | |||
| .volume-group { | |||
| margin-top: 1.5rem; | |||
| padding: 0 2rem; | |||
| display: flex; | |||
| flex-direction: row; | |||
| gap: 1.5rem; | |||
| align-items: center; | |||
| justify-content: space-between; | |||
| .speaker { | |||
| background: transparent; | |||
| padding: 1rem; | |||
| clip-path: polygon(50% 0, 100% 25%, 100% 75%, 50% 100%, 0 75%, 0 25%); | |||
| position: relative; | |||
| color: #ff7723; | |||
| &:before { | |||
| content: ""; | |||
| position: absolute; | |||
| top: 0; | |||
| left: 0; | |||
| width: 100%; | |||
| height: 100%; | |||
| clip-path: polygon( | |||
| 50% 0, | |||
| 100% 25%, | |||
| 100% 75%, | |||
| 50% 100%, | |||
| 0 75%, | |||
| 0 25%, | |||
| 50% 0, | |||
| 50% 1px, | |||
| 1px calc(25% + .5px), | |||
| 1px calc(75% - .5px), | |||
| 50% calc(100% - 1px), | |||
| calc(100% - 1px) calc(75% - .5px), | |||
| calc(100% - 1px) calc(25% + .5px), | |||
| 50% 1px | |||
| ); | |||
| background-color: #ff7723; | |||
| } | |||
| } | |||
| .volume-control { | |||
| padding: .63rem; | |||
| border-top: solid .06rem #ff7723; | |||
| border-bottom: solid .06rem #ff7723; | |||
| background-color: transparent; | |||
| border-left: none; | |||
| border-right: none; | |||
| svg { | |||
| cursor: pointer; | |||
| width: 1.5rem; | |||
| height: 1.5rem; | |||
| } | |||
| } | |||
| .volume-slider { | |||
| border: .06rem solid #eed3c2; | |||
| height: .75rem; | |||
| width: 39rem; | |||
| position: relative; | |||
| input[type="range"] { | |||
| position: relative; | |||
| -webkit-appearance: none; | |||
| -moz-appearance: none; | |||
| display: block; | |||
| outline: none; | |||
| height: 4px; | |||
| width: 38.5rem; | |||
| appearance: none; | |||
| background-color: transparent; | |||
| margin: .25rem; | |||
| &::-webkit-slider-thumb { | |||
| -webkit-appearance: none; | |||
| appearance: none; | |||
| background: #ff7723 | |||
| ; | |||
| border: .06rem solid #ff7723; | |||
| height: .88rem; | |||
| width: .88rem; | |||
| padding: 2px; | |||
| background-clip: content-box; | |||
| transition: transform .2s ease; | |||
| &:focus { | |||
| cursor: pointer; | |||
| border: .12rem solid #ff7723; | |||
| transform: scale(1.2); | |||
| } | |||
| &:hover { | |||
| cursor: pointer; | |||
| border: .12rem solid #ff7723; | |||
| transform: scale(1.2); | |||
| } | |||
| } | |||
| &::-moz-range-thumb { | |||
| border-radius: 0; | |||
| background: #ff7723; | |||
| border: .06rem solid #ff7723; | |||
| height: .5rem; | |||
| width: .5rem ; | |||
| padding: 3px; | |||
| background-clip: content-box; | |||
| &:focus { | |||
| cursor: pointer; | |||
| border: .14rem solid #ff7723; | |||
| transform: scale(1); | |||
| } | |||
| &:hover { | |||
| cursor: pointer; | |||
| border: .14rem solid #ff7723; | |||
| transform: scale(1); | |||
| } | |||
| } | |||
| &::-ms-thumb { | |||
| -webkit-appearance: none; | |||
| appearance: none; | |||
| background: #ff7723; | |||
| border: .06rem solid #ff7723; | |||
| height: .88rem; | |||
| width: .88rem; | |||
| padding: 3px; | |||
| background-clip: content-box; | |||
| &:focus { | |||
| cursor: pointer; | |||
| border: .12rem solid #ff7723; | |||
| transform: scale(1.2); | |||
| } | |||
| &:hover { | |||
| cursor: pointer; | |||
| border: .12rem solid #ff7723; | |||
| transform: scale(1.2); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| svg{ | |||
| color: #ff7723; | |||
| } | |||
| @@ -0,0 +1,21 @@ | |||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | |||
| import { SliderRangeComponent } from './slider-range.component'; | |||
| describe('SilderRangeComponent', () => { | |||
| let component: SliderRangeComponent; | |||
| let fixture: ComponentFixture<SliderRangeComponent>; | |||
| beforeEach(() => { | |||
| TestBed.configureTestingModule({ | |||
| declarations: [SliderRangeComponent] | |||
| }); | |||
| fixture = TestBed.createComponent(SliderRangeComponent); | |||
| component = fixture.componentInstance; | |||
| fixture.detectChanges(); | |||
| }); | |||
| it('should create', () => { | |||
| expect(component).toBeTruthy(); | |||
| }); | |||
| }); | |||
| @@ -0,0 +1,103 @@ | |||
| import { | |||
| Component, | |||
| EventEmitter, | |||
| forwardRef, | |||
| Input, | |||
| Output, | |||
| } from '@angular/core'; | |||
| import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; | |||
| @Component({ | |||
| selector: 'app-slider-range', | |||
| templateUrl: './slider-range.component.html', | |||
| styleUrls: ['./slider-range.component.scss'], | |||
| providers: [ | |||
| { | |||
| provide: NG_VALUE_ACCESSOR, | |||
| useExisting: forwardRef(() => SliderRangeComponent), | |||
| multi: true, | |||
| }, | |||
| ], | |||
| }) | |||
| export class SliderRangeComponent implements ControlValueAccessor { | |||
| @Input() value: number = 10; | |||
| @Input() max: number = 300; | |||
| @Input() icon: string = ''; | |||
| @Output() valueChange = new EventEmitter<number>(); | |||
| onChange = (value: number) => {}; | |||
| onTouched = () => {}; | |||
| constructor() {} | |||
| writeValue(value: number): void { | |||
| this.value = value; | |||
| } | |||
| registerOnChange(fn: any): void { | |||
| this.onChange = fn; | |||
| } | |||
| registerOnTouched(fn: any): void { | |||
| this.onTouched = fn; | |||
| } | |||
| setDisabledState?(isDisabled: boolean): void { | |||
| // Handle the disabled state if needed | |||
| } | |||
| onSliderChange(event: any) { | |||
| this.value = event.target.value; | |||
| this.onChange(this.value); | |||
| this.onTouched(); | |||
| this.valueChange.emit(this.value); | |||
| } | |||
| onSliderChangeBackground() { | |||
| let valPercent = (this.value / this.max) * 100; | |||
| // Adjust valPercent based on your conditions | |||
| switch (true) { | |||
| case valPercent > 10 && valPercent <= 30: | |||
| valPercent -= 0.5; | |||
| break; | |||
| case valPercent > 30 && valPercent <= 50: | |||
| valPercent -= 1; | |||
| break; | |||
| case valPercent > 50 && valPercent <= 70: | |||
| valPercent -= 1.5; | |||
| break; | |||
| case valPercent > 70 && valPercent <= 90: | |||
| valPercent -= 2; | |||
| break; | |||
| case valPercent > 90 && valPercent <= 100: | |||
| valPercent -= 2.3; | |||
| break; | |||
| default: | |||
| break; | |||
| } | |||
| // Return CSS object with background style | |||
| return { | |||
| background: `linear-gradient(to right, #ff7723 ${valPercent}%, transparent ${valPercent}%)`, | |||
| }; | |||
| } | |||
| increaseVolume() { | |||
| if (this.value < this.max) { | |||
| this.value += 10; | |||
| this.onChange(this.value); | |||
| this.onTouched(); | |||
| this.valueChange.emit(this.value); | |||
| } | |||
| } | |||
| decreaseVolume() { | |||
| if (this.value > 0) { | |||
| this.value -= 10; | |||
| this.onChange(this.value); | |||
| this.onTouched(); | |||
| this.valueChange.emit(this.value); | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,58 @@ | |||
| import { Injectable } from '@angular/core'; | |||
| @Injectable({ | |||
| providedIn: 'root', | |||
| }) | |||
| export class AlarmSoundService { | |||
| private alertInterval: any; | |||
| private alertDuration: number = 10000; | |||
| private audio = new Audio(); | |||
| private isPlaying: boolean = false; | |||
| constructor() { | |||
| this.audio.src = 'assets/sound/alarm_5m.mp3'; | |||
| this.audio.load(); | |||
| this.audio.onended = () => { | |||
| // Khi phát hết âm thanh, đặt biến isPlaying về false | |||
| this.isPlaying = false; | |||
| }; | |||
| } | |||
| playSound(): void { | |||
| if (!this.isPlaying) { | |||
| this.audio.play().then(() => { | |||
| this.isPlaying = true; | |||
| this.alertInterval = setInterval(() => { | |||
| this.stopAlarm(); | |||
| }, this.alertDuration); | |||
| }).catch((error) => { | |||
| console.error('Error playing audio:', error); | |||
| }); | |||
| } | |||
| } | |||
| stopSound(): void { | |||
| if (this.audio) { | |||
| this.audio.pause(); | |||
| this.audio.currentTime = 0; | |||
| this.isPlaying = false; | |||
| } | |||
| } | |||
| startAlarm( isTurnOn: boolean, isReady: boolean, fireArm: boolean, fenceArm: boolean): void { | |||
| if (isTurnOn && isReady && (fireArm || fenceArm)) { | |||
| //this.alertDuration = // setting time | |||
| this.playSound(); | |||
| }else{ | |||
| this.stopAlarm(); | |||
| } | |||
| } | |||
| stopAlarm(): void { | |||
| this.stopSound(); | |||
| if (this.alertInterval){ | |||
| clearInterval(this.alertInterval); | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,27 @@ | |||
| import { ElementRef, Injectable } from '@angular/core'; | |||
| import { MatDialog } from '@angular/material/dialog'; | |||
| import { take } from 'rxjs'; | |||
| import { ConfirmDialogComponent } from '../component/confirm-dialog/confirm-dialog.component'; | |||
| @Injectable({ | |||
| providedIn: 'root', | |||
| }) | |||
| export class ConfirmDialogService { | |||
| private isDialogOpen = false; | |||
| constructor(private dialog: MatDialog) {} | |||
| openDialog(data: any[]): void { | |||
| if (!this.isDialogOpen) { | |||
| this.isDialogOpen = true; | |||
| const dialogRef = this.dialog.open(ConfirmDialogComponent); | |||
| dialogRef | |||
| .afterClosed() | |||
| .pipe(take(1)) | |||
| .subscribe(() => { | |||
| this.isDialogOpen = false; | |||
| }); | |||
| } | |||
| } | |||
| } | |||
| @@ -1,7 +1,5 @@ | |||
| import { Injectable } from '@angular/core'; | |||
| import { webSocket, WebSocketSubject } from 'rxjs/webSocket'; | |||
| import { BehaviorSubject,Subject } from "rxjs"; | |||
| import { config } from "../../../assets/config/config"; | |||
| import { | |||
| IMqttMessage, | |||
| IMqttServiceOptions, | |||
| @@ -1,11 +1,23 @@ | |||
| import { NgModule } from "@angular/core"; | |||
| import { FlexLayoutModule } from "@angular/flex-layout"; | |||
| import { MatButtonModule } from "@angular/material/button"; | |||
| import { NgModule } from '@angular/core'; | |||
| import { FlexLayoutModule } from '@angular/flex-layout'; | |||
| import { MatButtonModule } from '@angular/material/button'; | |||
| import { MatDialogModule } from '@angular/material/dialog'; | |||
| import { MatIconModule } from '@angular/material/icon'; | |||
| import { MatMenuModule } from '@angular/material/menu'; | |||
| import { MatExpansionModule } from '@angular/material/expansion'; | |||
| import { MatCardModule } from '@angular/material/card'; | |||
| import { MatCheckboxModule } from '@angular/material/checkbox'; | |||
| @NgModule({ | |||
| exports: [ | |||
| MatButtonModule, | |||
| FlexLayoutModule | |||
| ] | |||
| FlexLayoutModule, | |||
| MatDialogModule, | |||
| MatIconModule, | |||
| MatMenuModule, | |||
| MatExpansionModule, | |||
| MatCardModule, | |||
| MatCheckboxModule, | |||
| ], | |||
| }) | |||
| export class SharedMaterialModule {} | |||
| @@ -1,13 +1,14 @@ | |||
| import { NgModule } from '@angular/core'; | |||
| import { CommonModule } from '@angular/common'; | |||
| import {SharedComponentModule} from "./component/shared-component.module"; | |||
| import {FormsModule} from "@angular/forms"; | |||
| @NgModule({ | |||
| declarations: [], | |||
| imports: [ | |||
| CommonModule, | |||
| SharedComponentModule, | |||
| FormsModule | |||
| ], | |||
| exports: [ | |||
| SharedComponentModule, | |||