| import { NgModule } from '@angular/core'; | import { NgModule } from '@angular/core'; | ||||
| import { RouterModule, Routes } from '@angular/router'; | import { RouterModule, Routes } from '@angular/router'; | ||||
| import {LayoutComponent} from "./shared/component/layout/layout.component"; | |||||
| import { LayoutComponent } from './shared/component/layout/layout.component'; | |||||
| const routes: Routes = [ | const routes: Routes = [ | ||||
| { path: '', | |||||
| redirectTo: '/homepage', | |||||
| pathMatch: 'full' | |||||
| }, | |||||
| { path: '', | |||||
| { path: '', redirectTo: '/homepage', pathMatch: 'full' }, | |||||
| { | |||||
| path: '', | |||||
| component: LayoutComponent, | component: LayoutComponent, | ||||
| children: [ | children: [ | ||||
| { path: 'homepage', | |||||
| loadChildren: () => import('./modules/homepage/home-page.module').then(m => m.HomePageModule) | |||||
| }, | |||||
| { path: 'overview', | |||||
| loadChildren: () => import('./modules/overview/overview.module').then(m => m.OverviewModule) | |||||
| { | |||||
| path: 'homepage', | |||||
| loadChildren: () => | |||||
| import('./modules/homepage/home-page.module').then( | |||||
| (m) => m.HomePageModule, | |||||
| ), | |||||
| }, | }, | ||||
| ] | |||||
| ], | |||||
| }, | }, | ||||
| ]; | ]; | ||||
| @NgModule({ | @NgModule({ | ||||
| imports: [RouterModule.forRoot(routes)], | imports: [RouterModule.forRoot(routes)], | ||||
| exports: [RouterModule] | |||||
| exports: [RouterModule], | |||||
| }) | }) | ||||
| export class AppRoutingModule { } | |||||
| export class AppRoutingModule {} |
| <h2 style="text-align: center; padding: 3rem">Centralized Security Management Dashboard</h2> | |||||
| <div class="map-container"> | |||||
| <h2>Security Monitoring and Control</h2> | |||||
| <div class="map-frame"> | |||||
| <div id="map"></div> | |||||
| </div> | |||||
| </div> | |||||
| .map-container { | |||||
| position: absolute; | |||||
| left: 0; | |||||
| right: 0; | |||||
| width: 100%; | |||||
| height: calc(100% - 7rem); | |||||
| h2 { | |||||
| padding: .5rem 3rem 0 ; | |||||
| text-align: center; | |||||
| } | |||||
| } | |||||
| .map-frame { | |||||
| height: 100%; | |||||
| } | |||||
| #map { | |||||
| height: 100%; | |||||
| } | |||||
| p { | |||||
| margin: 0 0 0 16px !important; | |||||
| } | |||||
| .box-custom { | |||||
| color: #F33152; | |||||
| padding: 3px 2px; | |||||
| background-color: rbg(243,49,82,0.1); | |||||
| } | |||||
| ::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; | |||||
| .dynamic-button{ | |||||
| cursor: pointer; | |||||
| } | |||||
| } | |||||
| ::ng-deep.icon { | |||||
| width: 200px; | |||||
| display: flex; | |||||
| flex-direction: column; | |||||
| align-items: center; | |||||
| } |
| import { Component } from '@angular/core'; | |||||
| 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({ | @Component({ | ||||
| selector: 'app-centralized-security-management', | selector: 'app-centralized-security-management', | ||||
| templateUrl: './centralized-security-management.component.html', | templateUrl: './centralized-security-management.component.html', | ||||
| styleUrls: ['./centralized-security-management.component.scss'] | |||||
| styleUrls: ['./centralized-security-management.component.scss'], | |||||
| }) | }) | ||||
| export class CentralizedSecurityManagementComponent { | |||||
| 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, | |||||
| private confirmDialogService$: ConfirmDialogService, | |||||
| ) {} | |||||
| 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(true, true, true, 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); | |||||
| } | |||||
| } | } |
| 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' | |||||
| }, | |||||
| } |
| import {SharedModule} from "../../shared/shared.module"; | import {SharedModule} from "../../shared/shared.module"; | ||||
| import { CentralizedSecurityManagementComponent } from './centralized-security-management/centralized-security-management.component'; | import { CentralizedSecurityManagementComponent } from './centralized-security-management/centralized-security-management.component'; | ||||
| import { SecuritySystemDetailsComponent } from './security-system-details/security-system-details.component'; | import { SecuritySystemDetailsComponent } from './security-system-details/security-system-details.component'; | ||||
| import {FormsModule} from "@angular/forms"; | |||||
| @NgModule({ | @NgModule({ | ||||
| declarations: [ | declarations: [ | ||||
| HomePageComponent, | HomePageComponent, | ||||
| CentralizedSecurityManagementComponent, | CentralizedSecurityManagementComponent, | ||||
| SecuritySystemDetailsComponent | |||||
| SecuritySystemDetailsComponent, | |||||
| ], | ], | ||||
| imports: [ | imports: [ | ||||
| CommonModule, | CommonModule, | ||||
| RouterModule.forChild(homePageRoutes), | RouterModule.forChild(homePageRoutes), | ||||
| SharedMaterialModule, | SharedMaterialModule, | ||||
| SharedModule,] | |||||
| SharedModule, | |||||
| FormsModule, | |||||
| ], | |||||
| }) | }) | ||||
| export class HomePageModule { } | |||||
| export class HomePageModule {} |
| import {Routes} from "@angular/router"; | |||||
| import {HomePageComponent} from "./homepage/home-page.component"; | |||||
| import { | |||||
| CentralizedSecurityManagementComponent | |||||
| } from "./centralized-security-management/centralized-security-management.component"; | |||||
| 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 = [ | export const homePageRoutes: Routes = [ | ||||
| { | { | ||||
| path: '', | path: '', | ||||
| component: HomePageComponent, | component: HomePageComponent, | ||||
| }, | |||||
| }, | |||||
| { | { | ||||
| path: 'centralized-security-management', | path: 'centralized-security-management', | ||||
| component: CentralizedSecurityManagementComponent | |||||
| component: CentralizedSecurityManagementComponent, | |||||
| }, | }, | ||||
| { | { | ||||
| path: 'security-system-details', | path: 'security-system-details', | ||||
| component: CentralizedSecurityManagementComponent | |||||
| } | |||||
| component: SecuritySystemDetailsComponent, | |||||
| }, | |||||
| ]; | ]; | ||||
| <div fxLayout="row" fxLayoutGap="30px" style="padding: 3rem 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> | |||||
| <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> | |||||
| <div class="sound-group"> | |||||
| <h2>WHISTLE TIME: {{timeEffect1}}s</h2> | |||||
| <mat-card class="sound-group"> | |||||
| <mat-card-header> | |||||
| <mat-card-title>WHISTLE TIME</mat-card-title> | |||||
| </mat-card-header> | |||||
| <mat-card-content> | |||||
| <div class="volume-group"> | <div class="volume-group"> | ||||
| <app-slider-range [value]="timeEffect1" (valueChange)="timeEffect1 = $event" ></app-slider-range> | |||||
| <app-slider-range | |||||
| [value]="whistle.time" | |||||
| [icon]="'access_alarm'" | |||||
| (valueChange)="whistle.time = $event" | |||||
| ></app-slider-range> | |||||
| </div> | </div> | ||||
| </div> | |||||
| <div class="sound-group"> | |||||
| <h2>24-HOUR ZONE ALARM TIME: {{timeEffect2}}s</h2> | |||||
| <div class="volume-group"> | <div class="volume-group"> | ||||
| <app-slider-range [value]="timeEffect2" (valueChange)="timeEffect2 = $event" ></app-slider-range> | |||||
| <app-slider-range | |||||
| [value]="alarm.sound" | |||||
| [(ngModel)]="alarm.sound" | |||||
| (valueChange)="alarm.sound = $event" | |||||
| [icon]="alarm.sound == 0 ? 'volume_off' : 'volume_up'" | |||||
| ></app-slider-range> | |||||
| </div> | </div> | ||||
| </div> | |||||
| </mat-card-content> | |||||
| </mat-card> | |||||
| <mat-card class="sound-group"> | |||||
| <mat-card-header> | |||||
| <mat-card-title>24-HOUR ZONE ALARM TIME</mat-card-title> | |||||
| </mat-card-header> | |||||
| <mat-card-content> | |||||
| <div class="volume-group"> | |||||
| <app-slider-range | |||||
| [value]="whistle.time" | |||||
| [icon]="'access_alarm'" | |||||
| ></app-slider-range> | |||||
| </div> | |||||
| <div class="volume-group"> | |||||
| <app-slider-range | |||||
| [value]="whistle.sound" | |||||
| [(ngModel)]="whistle.sound" | |||||
| [icon]="whistle.sound == 0 ? 'volume_off' : 'volume_up'" | |||||
| ></app-slider-range> | |||||
| </div> | |||||
| </mat-card-content> | |||||
| </mat-card> |
| .sound-group{ | .sound-group{ | ||||
| padding: 3rem; | |||||
| width: 70% | |||||
| margin: 2rem 3rem; | |||||
| } | } | ||||
| button{ | button{ | ||||
| color: #ff7723 !important; | color: #ff7723 !important; |
| @Component({ | @Component({ | ||||
| selector: 'app-homepage', | selector: 'app-homepage', | ||||
| templateUrl: './home-page.component.html', | templateUrl: './home-page.component.html', | ||||
| styleUrls: ['./home-page.component.scss'] | |||||
| styleUrls: ['./home-page.component.scss'], | |||||
| }) | }) | ||||
| export class HomePageComponent { | export class HomePageComponent { | ||||
| timeEffect1: any = 10; | |||||
| timeEffect2: any = 30; | |||||
| whistle = { | |||||
| time: 10, | |||||
| sound: 0, | |||||
| }; | |||||
| alarm = { | |||||
| time: 30, | |||||
| sound: 0, | |||||
| }; | |||||
| } | } |
| <h2 style="text-align: center; padding: 3rem">Security System Details Dashboard</h2> | |||||
| <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> |
| button { | |||||
| padding: 30px 50px; | |||||
| } | |||||
| .red-bg { | |||||
| background-color: red !important; | |||||
| color: white !important; | |||||
| } | |||||
| .green-bg { | |||||
| background-color: green !important; | |||||
| color: white !important; | |||||
| } | |||||
| h1 { | |||||
| font-size: 1.8rem; | |||||
| color: white; | |||||
| } | |||||
| .topnav { | |||||
| overflow: hidden; | |||||
| background-color: #0a1128; | |||||
| } | |||||
| body { | |||||
| margin: 0; | |||||
| } | |||||
| p { | |||||
| text-align: center; | |||||
| } | |||||
| .content { | |||||
| padding: 50px; | |||||
| } | |||||
| .card-grid { | |||||
| max-width: 800px; | |||||
| margin: 0 auto; | |||||
| display: grid; | |||||
| grid-gap: 2rem; | |||||
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |||||
| } | |||||
| .card { | |||||
| background-color: white; | |||||
| box-shadow: 2px 2px 12px 1px rgba(140, 140, 140, 0.5); | |||||
| } | |||||
| .card-title { | |||||
| font-size: 1.2rem; | |||||
| font-weight: bold; | |||||
| color: #034078; | |||||
| } | |||||
| .reading { | |||||
| font-size: 1.2rem; | |||||
| color: #1282a2; | |||||
| } | |||||
| .button { | |||||
| padding: 15px 50px; | |||||
| font-size: 24px; | |||||
| text-align: center; | |||||
| outline: none; | |||||
| color: #fff; | |||||
| background-color: #0f8b8d; | |||||
| border: none; | |||||
| border-radius: 5px; | |||||
| -webkit-touch-callout: none; | |||||
| -webkit-user-select: none; | |||||
| -khtml-user-select: none; | |||||
| -moz-user-select: none; | |||||
| -ms-user-select: none; | |||||
| user-select: none; | |||||
| -webkit-tap-highlight-color: rgba(0, 0, 0, 0); | |||||
| } | |||||
| /*.button:hover {background-color: #0f8b8d}*/ | |||||
| .button:active { | |||||
| background-color: #0f8b8d; | |||||
| box-shadow: 2px 2px #cdcdcd; | |||||
| transform: translateY(2px); | |||||
| } | |||||
| .state { | |||||
| font-size: 1.5rem; | |||||
| color: #8c8c8c; | |||||
| font-weight: bold; | |||||
| text-align: center; | |||||
| } | |||||
| .content { | |||||
| padding: 30px; | |||||
| max-width: 600px; | |||||
| margin: 0 auto; | |||||
| } | |||||
| .map-image { | |||||
| img { | |||||
| height: 100%; | |||||
| width: 100%; | |||||
| } | |||||
| .card-state { | |||||
| width: 100%; | |||||
| height: 100%; | |||||
| position: relative; | |||||
| } | |||||
| .state { | |||||
| position: absolute; | |||||
| color: red; | |||||
| &.t1 { | |||||
| top: 5%; | |||||
| left: 10%; | |||||
| } | |||||
| &.t2 { | |||||
| top: 50%; | |||||
| left: 50%; | |||||
| transform: translate(-50%, -50%); | |||||
| } | |||||
| &.t3 { | |||||
| top: 5%; | |||||
| left: 50%; | |||||
| transform: translate(-50%); | |||||
| } | |||||
| &.t4 { | |||||
| top: 47%; | |||||
| right: 12%; | |||||
| } | |||||
| &.t5 { | |||||
| top: 88%; | |||||
| left: 50%; | |||||
| transform: translate(-50%); | |||||
| } | |||||
| &.t6 { | |||||
| top: 47%; | |||||
| left: 5%; | |||||
| width: 100px; | |||||
| .alarm-text-off { | |||||
| width: 100px !important; | |||||
| } | |||||
| } | |||||
| } | |||||
| .sensor-on { | |||||
| width: 30px; | |||||
| height: 30px; | |||||
| position: absolute; | |||||
| 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; | |||||
| } | |||||
| } | |||||
| } | |||||
| .alarm-text { | |||||
| font-size: 10px; | |||||
| padding: 2px 4px; | |||||
| width: 100px; | |||||
| margin-top: 10px; | |||||
| margin-left: -22px; | |||||
| border-radius: 2px; | |||||
| &-off { | |||||
| background: #bfe9f4; | |||||
| color: #004aad; | |||||
| } | |||||
| &-on { | |||||
| background: #f11e1e; | |||||
| color: #fff; | |||||
| } | |||||
| } | |||||
| .tooltip { | |||||
| position: relative; | |||||
| display: inline-block; | |||||
| } | |||||
| .tooltip .tooltiptext { | |||||
| text-align: start; | |||||
| font-size: 10px; | |||||
| visibility: hidden; | |||||
| width: 300px; | |||||
| padding: 10px; | |||||
| background-color: #eee; | |||||
| color: black; | |||||
| border-radius: 6px; | |||||
| position: absolute; | |||||
| z-index: 1; | |||||
| bottom: 100%; | |||||
| left: -50%; | |||||
| } | |||||
| .tooltip:hover .tooltiptext { | |||||
| visibility: visible; | |||||
| cursor: pointer; | |||||
| } | |||||
| a { | |||||
| color: blue; | |||||
| } |
| import { Component } from '@angular/core'; | |||||
| import {Component, OnDestroy, OnInit} from '@angular/core'; | |||||
| import {Subscription} from "rxjs"; | |||||
| import {SocketService} from "../../../shared/services/socket.service"; | |||||
| import {ToastrService} from "ngx-toastr"; | |||||
| @Component({ | @Component({ | ||||
| selector: 'app-security-system-details', | selector: 'app-security-system-details', | ||||
| templateUrl: './security-system-details.component.html', | templateUrl: './security-system-details.component.html', | ||||
| styleUrls: ['./security-system-details.component.scss'] | styleUrls: ['./security-system-details.component.scss'] | ||||
| }) | }) | ||||
| export class SecuritySystemDetailsComponent { | |||||
| export class SecuritySystemDetailsComponent implements OnInit, OnDestroy { | |||||
| isConnected = false; | |||||
| state1 = false; | |||||
| state2 = false; | |||||
| state3 = ''; | |||||
| state4 = ''; | |||||
| state5 = false; | |||||
| state6 = false; | |||||
| switchArm = false; | |||||
| switchWarning = false; | |||||
| isReady = true; | |||||
| private statusSubscription?: Subscription; | |||||
| private messageSubscription?: Subscription; | |||||
| private intervalId: any; | |||||
| constructor( | |||||
| private socketService$: SocketService, | |||||
| private toastr: ToastrService | |||||
| ) { | |||||
| } | |||||
| ngOnInit() { | |||||
| // this.socketService$.connect(); | |||||
| this.statusSubscription = this.socketService$.status$.subscribe(isConnected => { | |||||
| this.isConnected = isConnected; | |||||
| this.getReadings(); | |||||
| if (this.isConnected) { | |||||
| this.intervalId = setInterval(() => this.getReadings(), 5000); | |||||
| this.messageSubscription = this.socketService$.messages$.subscribe(message => { | |||||
| this.onMessage(message); | |||||
| }); | |||||
| } | |||||
| }); | |||||
| } | |||||
| ngOnDestroy(): void { | |||||
| if (this.statusSubscription) { | |||||
| this.statusSubscription.unsubscribe(); | |||||
| } | |||||
| if (this.messageSubscription) { | |||||
| this.messageSubscription.unsubscribe(); | |||||
| } | |||||
| if (this.intervalId) { | |||||
| clearInterval(this.intervalId); | |||||
| } | |||||
| this.socketService$.close(); | |||||
| } | |||||
| getReadings() { | |||||
| let str = {id: '0', type: 'get'}; | |||||
| this.socketService$.sendMessage(str); | |||||
| } | |||||
| toggleState1() { | |||||
| this.switchArm = !this.switchArm; | |||||
| let str = {id: '0', type: 'cmd', state1: this.switchArm.toString()}; | |||||
| this.socketService$.sendMessage(str); | |||||
| } | |||||
| getImageSource(): string { | |||||
| return (this.state5 && this.state6) || this.state5 ? 'assets/images/sensor-on.png' : 'assets/images/sensor-off.png'; | |||||
| } | |||||
| 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.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 | |||||
| this.state6 = message.state6 === '1'; // ? 'ON' : 'OFF'; | |||||
| this.switchArm = message.state5 === '1';// alarm 9h && 6h // 1 ON, 0 OFF | |||||
| 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}); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | } |
| <h3 style="text-align: center">Camera Stream</h3> | |||||
| <canvas class="video" #videoPlayer></canvas> |
| .video { | |||||
| width: 78% !important; | |||||
| display: block; | |||||
| margin: 0 auto | |||||
| } |
| 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(); | |||||
| }); | |||||
| }); |
| import {Component, OnInit, ViewChild, ElementRef, AfterViewInit, Renderer2} from '@angular/core'; | |||||
| import {loadPlayer, Player} from "rtsp-relay/browser"; | |||||
| @Component({ | |||||
| selector: 'app-camera-stream', | |||||
| templateUrl: './camera-stream.component.html', | |||||
| styleUrls: ['./camera-stream.component.scss'] | |||||
| }) | |||||
| export class CameraStreamComponent implements OnInit, AfterViewInit{ | |||||
| player?: Player; | |||||
| @ViewChild('videoPlayer') | |||||
| videoPlayer?: ElementRef<HTMLCanvasElement>; | |||||
| constructor() {} | |||||
| ngOnInit() { | |||||
| } | |||||
| async ngAfterViewInit() { | |||||
| const connect = async () => { | |||||
| this.player = await loadPlayer({ | |||||
| url: 'ws://localhost:8080/stream', | |||||
| canvas: this.videoPlayer!.nativeElement, | |||||
| onDisconnect: () => { | |||||
| setTimeout(connect, 5000); // reconnect after 5 seconds | |||||
| }, | |||||
| }); | |||||
| }; | |||||
| connect(); | |||||
| } | |||||
| } |
| <div class="map-container"> | |||||
| <h2>Security Monitoring and Control</h2> | |||||
| <div class="map-frame"> | |||||
| <div id="map"></div> | |||||
| </div> | |||||
| </div> | |||||
| .map-container { | |||||
| position: absolute; | |||||
| left: 0; | |||||
| right: 0; | |||||
| width: 100%; | |||||
| height: calc(100% - 7rem); | |||||
| h2 { | |||||
| padding: .5rem 3rem 0 ; | |||||
| text-align: center; | |||||
| } | |||||
| } | |||||
| .map-frame { | |||||
| height: 100%; | |||||
| } | |||||
| #map { | |||||
| height: 100%; | |||||
| } | |||||
| p { | |||||
| margin: 0 0 0 16px !important; | |||||
| } | |||||
| .box-custom { | |||||
| color: #F33152; | |||||
| padding: 3px 2px; | |||||
| background-color: rbg(243,49,82,0.1); | |||||
| } | |||||
| ::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; | |||||
| .dynamic-button{ | |||||
| cursor: pointer; | |||||
| } | |||||
| } | |||||
| ::ng-deep.icon { | |||||
| width: 200px; | |||||
| display: flex; | |||||
| flex-direction: column; | |||||
| align-items: center; | |||||
| } |
| import { | |||||
| AfterViewInit, | |||||
| Component, | |||||
| OnDestroy, | |||||
| OnInit, Renderer2 | |||||
| } from '@angular/core'; | |||||
| import * as L from 'leaflet'; | |||||
| import 'leaflet.markercluster'; | |||||
| import { Subscription } from 'rxjs'; | |||||
| import { SocketService } from '../../../shared/services/socket.service'; | |||||
| import {MatDialog} from "@angular/material/dialog"; | |||||
| import {CameraDialogComponent} from "../camera-dialog/camera-dialog.component"; | |||||
| import {AlarmSoundService} from "../../../shared/services/alarm-sound.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, | |||||
| 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(true, true, true, 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); | |||||
| } | |||||
| } |
| <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> |
| button { | |||||
| padding: 30px 50px; | |||||
| } | |||||
| .red-bg { | |||||
| background-color: red !important; | |||||
| color: white !important; | |||||
| } | |||||
| .green-bg { | |||||
| background-color: green !important; | |||||
| color: white !important; | |||||
| } | |||||
| h1 { | |||||
| font-size: 1.8rem; | |||||
| color: white; | |||||
| } | |||||
| .topnav { | |||||
| overflow: hidden; | |||||
| background-color: #0A1128; | |||||
| } | |||||
| body { | |||||
| margin: 0; | |||||
| } | |||||
| p { | |||||
| text-align: center; | |||||
| } | |||||
| .content { | |||||
| padding: 50px; | |||||
| } | |||||
| .card-grid { | |||||
| max-width: 800px; | |||||
| margin: 0 auto; | |||||
| display: grid; | |||||
| grid-gap: 2rem; | |||||
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |||||
| } | |||||
| .card { | |||||
| background-color: white; | |||||
| box-shadow: 2px 2px 12px 1px rgba(140, 140, 140, .5); | |||||
| } | |||||
| .card-title { | |||||
| font-size: 1.2rem; | |||||
| font-weight: bold; | |||||
| color: #034078 | |||||
| } | |||||
| .reading { | |||||
| font-size: 1.2rem; | |||||
| color: #1282A2; | |||||
| } | |||||
| .button { | |||||
| padding: 15px 50px; | |||||
| font-size: 24px; | |||||
| text-align: center; | |||||
| outline: none; | |||||
| color: #fff; | |||||
| background-color: #0f8b8d; | |||||
| border: none; | |||||
| border-radius: 5px; | |||||
| -webkit-touch-callout: none; | |||||
| -webkit-user-select: none; | |||||
| -khtml-user-select: none; | |||||
| -moz-user-select: none; | |||||
| -ms-user-select: none; | |||||
| user-select: none; | |||||
| -webkit-tap-highlight-color: rgba(0, 0, 0, 0); | |||||
| } | |||||
| /*.button:hover {background-color: #0f8b8d}*/ | |||||
| .button:active { | |||||
| background-color: #0f8b8d; | |||||
| box-shadow: 2px 2px #CDCDCD; | |||||
| transform: translateY(2px); | |||||
| } | |||||
| .state { | |||||
| font-size: 1.5rem; | |||||
| color: #8c8c8c; | |||||
| font-weight: bold; | |||||
| text-align: center; | |||||
| } | |||||
| .content { | |||||
| padding: 30px; | |||||
| max-width: 600px; | |||||
| margin: 0 auto; | |||||
| } | |||||
| .map-image{ | |||||
| img{ | |||||
| height: 100%; | |||||
| width: 100%; | |||||
| } | |||||
| .card-state{ | |||||
| width: 100%; | |||||
| height: 100%; | |||||
| position: relative; | |||||
| } | |||||
| .state{ | |||||
| position: absolute; | |||||
| color: red; | |||||
| &.t1{ | |||||
| top: 5%; | |||||
| left: 10%; | |||||
| } | |||||
| &.t2{ | |||||
| top: 50%; | |||||
| left: 50%; | |||||
| transform: translate(-50%, -50%); | |||||
| } | |||||
| &.t3{ | |||||
| top: 5%; | |||||
| left: 50%; | |||||
| transform: translate(-50%) | |||||
| } | |||||
| &.t4{ | |||||
| top: 47%; | |||||
| right: 12%; | |||||
| } | |||||
| &.t5{ | |||||
| top: 88%; | |||||
| left: 50%; | |||||
| transform: translate(-50%); | |||||
| } | |||||
| &.t6{ | |||||
| top: 47%; | |||||
| left: 5%; | |||||
| width: 100px; | |||||
| .alarm-text-off{ | |||||
| width: 100px !important; | |||||
| } | |||||
| } | |||||
| } | |||||
| .sensor-on { | |||||
| width: 30px; | |||||
| height: 30px; | |||||
| position: absolute; | |||||
| 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; | |||||
| } | |||||
| } | |||||
| } | |||||
| .alarm-text{ | |||||
| font-size: 10px; | |||||
| padding: 2px 4px; | |||||
| width: 100px; | |||||
| margin-top: 10px; | |||||
| margin-left: -22px; | |||||
| border-radius: 2px; | |||||
| &-off { | |||||
| background: #bfe9f4; | |||||
| color: #004aad; | |||||
| } | |||||
| &-on{ | |||||
| background: #F11E1E; | |||||
| color: #FFF; | |||||
| } | |||||
| } | |||||
| .tooltip { | |||||
| position: relative; | |||||
| display: inline-block; | |||||
| } | |||||
| .tooltip .tooltiptext { | |||||
| text-align: start; | |||||
| font-size: 10px; | |||||
| visibility: hidden; | |||||
| width: 300px; | |||||
| padding: 10px; | |||||
| background-color: #eee; | |||||
| color: black; | |||||
| border-radius: 6px; | |||||
| position: absolute; | |||||
| z-index: 1; | |||||
| bottom: 100%; | |||||
| left: -50%; | |||||
| } | |||||
| .tooltip:hover .tooltiptext { | |||||
| visibility: visible; | |||||
| cursor: pointer; | |||||
| } | |||||
| a{ | |||||
| color: blue; | |||||
| } |
| 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(); | |||||
| }); | |||||
| }); |
| import { Component, OnDestroy, OnInit } from '@angular/core'; | |||||
| import {SocketService} from "../../../shared/services/socket.service"; | |||||
| import {Subscription} from "rxjs"; | |||||
| import {ToastrService} from "ngx-toastr"; | |||||
| @Component({ | |||||
| selector: 'app-overall-ground', | |||||
| templateUrl: './overall-ground.component.html', | |||||
| styleUrls: ['./overall-ground.component.scss'] | |||||
| }) | |||||
| export class OverallGroundComponent implements OnInit, OnDestroy { | |||||
| isConnected = false; | |||||
| state1 = false; | |||||
| state2 = false; | |||||
| state3 = ''; | |||||
| state4 = ''; | |||||
| state5 = false; | |||||
| state6 = false; | |||||
| switchArm = false; | |||||
| switchWarning = false; | |||||
| isReady = true; | |||||
| private statusSubscription?: Subscription; | |||||
| private messageSubscription?: Subscription; | |||||
| private intervalId: any; | |||||
| constructor( | |||||
| private socketService$: SocketService, | |||||
| private toastr: ToastrService | |||||
| ) { | |||||
| } | |||||
| ngOnInit() { | |||||
| // this.socketService$.connect(); | |||||
| this.statusSubscription = this.socketService$.status$.subscribe(isConnected => { | |||||
| this.isConnected = isConnected; | |||||
| this.getReadings(); | |||||
| if (this.isConnected) { | |||||
| this.intervalId = setInterval(() => this.getReadings(), 5000); | |||||
| this.messageSubscription = this.socketService$.messages$.subscribe(message => { | |||||
| this.onMessage(message); | |||||
| }); | |||||
| } | |||||
| }); | |||||
| } | |||||
| ngOnDestroy(): void { | |||||
| if (this.statusSubscription) { | |||||
| this.statusSubscription.unsubscribe(); | |||||
| } | |||||
| if (this.messageSubscription) { | |||||
| this.messageSubscription.unsubscribe(); | |||||
| } | |||||
| if (this.intervalId) { | |||||
| clearInterval(this.intervalId); | |||||
| } | |||||
| this.socketService$.close(); | |||||
| } | |||||
| getReadings() { | |||||
| let str = {id: '0', type: 'get'}; | |||||
| this.socketService$.sendMessage(str); | |||||
| } | |||||
| toggleState1() { | |||||
| this.switchArm = !this.switchArm; | |||||
| let str = {id: '0', type: 'cmd', state1: this.switchArm.toString()}; | |||||
| this.socketService$.sendMessage(str); | |||||
| } | |||||
| getImageSource(): string { | |||||
| return (this.state5 && this.state6) || this.state5 ? 'assets/images/sensor-on.png' : 'assets/images/sensor-off.png'; | |||||
| } | |||||
| 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.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 | |||||
| this.state6 = message.state6 === '1'; // ? 'ON' : 'OFF'; | |||||
| this.switchArm = message.state5 === '1';// alarm 9h && 6h // 1 ON, 0 OFF | |||||
| 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}); | |||||
| } | |||||
| } | |||||
| } | |||||
| } |
| 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"; | |||||
| import {CameraDialogComponent} from "./camera-dialog/camera-dialog.component"; | |||||
| @NgModule({ | |||||
| declarations: [ | |||||
| OverallGroundComponent, | |||||
| MapComponent, | |||||
| CameraDialogComponent, | |||||
| CameraStreamComponent | |||||
| ], | |||||
| imports: [ | |||||
| CommonModule, | |||||
| RouterModule.forChild(overviewRoutes), | |||||
| SharedMaterialModule, | |||||
| ] | |||||
| }) | |||||
| export class OverviewModule { } |
| 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 | |||||
| } | |||||
| ]; | |||||
| <h2 mat-dialog-title style="border-bottom: solid 1px #eeeeee">CONFIRM TO IGNORE THE WARNING</h2> | |||||
| <mat-dialog-content> | |||||
| <section class="example-section"> | |||||
| <span class="example-list-section"> | |||||
| <mat-checkbox class="example-margin" | |||||
| color="primary" | |||||
| [checked]="allComplete" | |||||
| [indeterminate]="someComplete()" | |||||
| (change)="setAll($event.checked)"> | |||||
| {{data.name}} | |||||
| </mat-checkbox> | |||||
| </span> | |||||
| <span class="example-list-section"> | |||||
| <ul> | |||||
| <li *ngFor="let item of data.gateStatus"> | |||||
| <mat-checkbox [(ngModel)]="item.ignore" | |||||
| color="primary" | |||||
| (ngModelChange)="updateAllComplete()"> | |||||
| {{item.name}} | |||||
| </mat-checkbox> | |||||
| </li> | |||||
| </ul> | |||||
| </span> | |||||
| </section> | |||||
| </mat-dialog-content> | |||||
| <mat-dialog-actions align="end"> | |||||
| <button mat-button mat-dialog-close>Cancel</button> | |||||
| <button mat-button mat-dialog-close style="background: #ff7723; color: #ffffff">Yes</button> | |||||
| </mat-dialog-actions> |
| ul { | |||||
| list-style-type: none; | |||||
| margin-top: 4px; | |||||
| } |
| 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(); | |||||
| }); | |||||
| }); |
| 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 = { | |||||
| name: 'All', | |||||
| ignore: false, | |||||
| gateStatus: [ | |||||
| { | |||||
| name: 'GATE A', | |||||
| position: { latitude: 123.456, longitude: 456.789 }, | |||||
| ignore: false, | |||||
| }, | |||||
| { | |||||
| name: 'GATE B', | |||||
| position: { latitude: 111.222, longitude: 333.444 }, | |||||
| ignore: false, | |||||
| }, | |||||
| { | |||||
| name: 'GATE C', | |||||
| position: { latitude: 222.333, longitude: 444.555 }, | |||||
| ignore: false, | |||||
| }, | |||||
| ], | |||||
| }; | |||||
| allComplete: boolean = false; | |||||
| updateAllComplete() { | |||||
| this.allComplete = | |||||
| this.data.gateStatus != null && | |||||
| this.data.gateStatus.every((t) => t.ignore); | |||||
| } | |||||
| someComplete(): boolean { | |||||
| if (this.data.gateStatus == null) { | |||||
| return false; | |||||
| } | |||||
| return ( | |||||
| this.data.gateStatus.filter((t) => t.ignore).length > 0 && | |||||
| !this.allComplete | |||||
| ); | |||||
| } | |||||
| setAll(completed: boolean) { | |||||
| this.allComplete = completed; | |||||
| if (this.data.gateStatus == null) { | |||||
| return; | |||||
| } | |||||
| this.data.gateStatus.forEach((t) => (t.ignore = completed)); | |||||
| } | |||||
| } |
| <img src="../../../../../../assets/images/logo.png" > | <img src="../../../../../../assets/images/logo.png" > | ||||
| <div> | <div> | ||||
| <button mat-button routerLink="/homepage" routerLinkActive="active">Home</button> | <button mat-button routerLink="/homepage" routerLinkActive="active">Home</button> | ||||
| <button mat-button routerLink="/overview" routerLinkActive="active">Overview</button> | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <div> | <div> |
| import {NgModule} from "@angular/core"; | |||||
| import {CommonModule} from "@angular/common"; | |||||
| import {SharedMaterialModule} from "../shared-material.module"; | |||||
| import {RouterModule} from "@angular/router"; | |||||
| 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 { LayoutComponent } from './layout/layout.component'; | ||||
| import { SliderRangeComponent } from './slider-range/slider-range.component'; | import { SliderRangeComponent } from './slider-range/slider-range.component'; | ||||
| import {FormsModule} from "@angular/forms"; | |||||
| import { FormsModule } from '@angular/forms'; | |||||
| import { CameraDialogComponent } from './camera-dialog/camera-dialog.component'; | |||||
| import { ConfirmDialogComponent } from './confirm-dialog/confirm-dialog.component'; | |||||
| @NgModule({ | @NgModule({ | ||||
| declarations: [ | declarations: [ | ||||
| LayoutComponent, | LayoutComponent, | ||||
| SliderRangeComponent | |||||
| ], | |||||
| imports: [ | |||||
| CommonModule, | |||||
| SharedMaterialModule, | |||||
| RouterModule, | |||||
| FormsModule, | |||||
| SliderRangeComponent, | |||||
| CameraDialogComponent, | |||||
| ConfirmDialogComponent, | |||||
| ], | ], | ||||
| exports: [ | |||||
| LayoutComponent, | |||||
| SliderRangeComponent | |||||
| ], | |||||
| providers: [] | |||||
| imports: [CommonModule, SharedMaterialModule, RouterModule, FormsModule], | |||||
| exports: [LayoutComponent, SliderRangeComponent, CameraDialogComponent], | |||||
| providers: [], | |||||
| }) | }) | ||||
| export class SharedComponentModule { | |||||
| } | |||||
| export class SharedComponentModule {} |
| <ng-content></ng-content> | <ng-content></ng-content> | ||||
| <div class="volume-group"> | <div class="volume-group"> | ||||
| <div class="speaker" *ngIf="value>0; else valueMin"> | |||||
| <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/sound</title> | |||||
| <g id="Icons/sound" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | |||||
| <g id="SpeakerLow"> | |||||
| <path | |||||
| d="M19.500001,12 C19.5006623,12.9135698 19.1672238,13.7958481 18.5625,14.480625 C18.2868064,14.7835586 17.8190602,14.8092747 17.5118188,14.5383902 C17.2045774,14.2675057 17.1714919,13.8002229 17.4375,13.48875 C18.185902,12.6387624 18.185902,11.3649876 17.4375,10.515 C17.1714919,10.2035271 17.2045774,9.73624429 17.5118188,9.46535978 C17.8190602,9.19447528 18.2868064,9.22019142 18.5625,9.523125 C19.1658428,10.2072505 19.4991441,11.0878325 19.500001,12 Z M15,3 L15,21 C14.9997852,21.2863161 14.8365765,21.5475297 14.5793423,21.6732576 C14.3221081,21.7989856 14.0157342,21.7672891 13.7896875,21.5915625 L7.2421875,16.5 L3,16.5 C2.17157288,16.5 1.5,15.8284271 1.5,15 L1.5,9 C1.5,8.17157288 2.17157288,7.5 3,7.5 L7.2421875,7.5 L13.7896875,2.4084375 C14.0157342,2.23271088 14.3221081,2.20101442 14.5793423,2.32674236 C14.8365765,2.4524703 14.9997852,2.71368392 15,3 Z M6.75,9 L3,9 L3,15 L6.75,15 L6.75,9 Z" | |||||
| id="Shape" fill="currentColor" fill-rule="nonzero"></path> | |||||
| <rect id="Rectangle" x="0" y="0" width="24" height="24"></rect> | |||||
| </g> | |||||
| </g> | |||||
| </svg> | |||||
| <div class="speaker"> | |||||
| <mat-icon>{{icon}}</mat-icon> | |||||
| </div> | </div> | ||||
| <ng-template #valueMin> | |||||
| <div class="speaker"> | |||||
| <svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> | |||||
| <path | |||||
| d="M12.1657 2.14424C12.8728 2.50021 13 3.27314 13 3.7446V20.2561C13 20.7286 12.8717 21.4998 12.1656 21.8554C11.416 22.2331 10.7175 21.8081 10.3623 21.4891L4.95001 16.6248H3.00001C1.89544 16.6248 1.00001 15.7293 1.00001 14.6248L1 9.43717C1 8.3326 1.89543 7.43717 3 7.43717H4.94661L10.3623 2.51158C10.7163 2.19354 11.4151 1.76635 12.1657 2.14424Z" | |||||
| fill="currentColor"/> | |||||
| <path | |||||
| d="M21.8232 15.6767C21.4327 16.0673 20.7995 16.0673 20.409 15.6768L18.5 13.7678L16.591 15.6768C16.2005 16.0673 15.5673 16.0673 15.1768 15.6767L14.8233 15.3232C14.4327 14.9327 14.4327 14.2995 14.8233 13.909L16.7322 12L14.8232 10.091C14.4327 9.70044 14.4327 9.06727 14.8232 8.67675L15.1767 8.3232C15.5673 7.93267 16.2004 7.93267 16.591 8.32319L18.5 10.2322L20.409 8.32319C20.7996 7.93267 21.4327 7.93267 21.8233 8.3232L22.1768 8.67675C22.5673 9.06727 22.5673 9.70044 22.1768 10.091L20.2678 12L22.1767 13.909C22.5673 14.2995 22.5673 14.9327 22.1767 15.3232L21.8232 15.6767Z" | |||||
| fill="currentColor"/> | |||||
| </svg> | |||||
| </div> | |||||
| </ng-template> | |||||
| <button class="volume-control" (click)="decreaseVolume()"> | <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" | <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"> | xmlns:xlink="http://www.w3.org/1999/xlink"> |
| .volume-group { | .volume-group { | ||||
| margin-top: 1.5rem; | margin-top: 1.5rem; | ||||
| padding: 0 2rem; | |||||
| display: flex; | display: flex; | ||||
| flex-direction: row; | flex-direction: row; | ||||
| gap: 1.5rem; | gap: 1.5rem; | ||||
| padding: 1rem; | padding: 1rem; | ||||
| clip-path: polygon(50% 0, 100% 25%, 100% 75%, 50% 100%, 0 75%, 0 25%); | clip-path: polygon(50% 0, 100% 25%, 100% 75%, 50% 100%, 0 75%, 0 25%); | ||||
| position: relative; | position: relative; | ||||
| color: #ff7723; | |||||
| &:before { | &:before { | ||||
| content: ""; | content: ""; | ||||
| position: absolute; | position: absolute; |
| import {Component, EventEmitter, Input, Output} from '@angular/core'; | |||||
| import { | |||||
| Component, | |||||
| EventEmitter, | |||||
| forwardRef, | |||||
| Input, | |||||
| Output, | |||||
| } from '@angular/core'; | |||||
| import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; | |||||
| @Component({ | @Component({ | ||||
| selector: 'app-slider-range', | selector: 'app-slider-range', | ||||
| templateUrl: './slider-range.component.html', | templateUrl: './slider-range.component.html', | ||||
| styleUrls: ['./slider-range.component.scss'] | |||||
| styleUrls: ['./slider-range.component.scss'], | |||||
| providers: [ | |||||
| { | |||||
| provide: NG_VALUE_ACCESSOR, | |||||
| useExisting: forwardRef(() => SliderRangeComponent), | |||||
| multi: true, | |||||
| }, | |||||
| ], | |||||
| }) | }) | ||||
| export class SliderRangeComponent { | |||||
| export class SliderRangeComponent implements ControlValueAccessor { | |||||
| @Input() value: number = 10; | @Input() value: number = 10; | ||||
| @Input() max: number = 300; | @Input() max: number = 300; | ||||
| @Input() icon: string = ''; | |||||
| @Output() valueChange = new EventEmitter<number>(); | @Output() valueChange = new EventEmitter<number>(); | ||||
| onChange = (value: number) => {}; | |||||
| onTouched = () => {}; | |||||
| constructor() {} | 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) { | onSliderChange(event: any) { | ||||
| this.value = event.target.value; | this.value = event.target.value; | ||||
| this.onChange(this.value); | |||||
| this.onTouched(); | |||||
| this.valueChange.emit(this.value); | this.valueChange.emit(this.value); | ||||
| } | } | ||||
| // Adjust valPercent based on your conditions | // Adjust valPercent based on your conditions | ||||
| switch (true) { | switch (true) { | ||||
| case (valPercent > 10 && valPercent <= 30): | |||||
| case valPercent > 10 && valPercent <= 30: | |||||
| valPercent -= 0.5; | valPercent -= 0.5; | ||||
| break; | break; | ||||
| case (valPercent > 30 && valPercent <= 50): | |||||
| case valPercent > 30 && valPercent <= 50: | |||||
| valPercent -= 1; | valPercent -= 1; | ||||
| break; | break; | ||||
| case (valPercent > 50 && valPercent <= 70): | |||||
| case valPercent > 50 && valPercent <= 70: | |||||
| valPercent -= 1.5; | valPercent -= 1.5; | ||||
| break; | break; | ||||
| case (valPercent > 70 && valPercent <= 90): | |||||
| case valPercent > 70 && valPercent <= 90: | |||||
| valPercent -= 2; | valPercent -= 2; | ||||
| break; | break; | ||||
| case (valPercent > 90 && valPercent <= 100): | |||||
| case valPercent > 90 && valPercent <= 100: | |||||
| valPercent -= 2.3; | valPercent -= 2.3; | ||||
| break; | break; | ||||
| default: | default: | ||||
| // Return CSS object with background style | // Return CSS object with background style | ||||
| return { | return { | ||||
| 'background': `linear-gradient(to right, #ff7723 ${valPercent}%, transparent ${valPercent}%)` | |||||
| background: `linear-gradient(to right, #ff7723 ${valPercent}%, transparent ${valPercent}%)`, | |||||
| }; | }; | ||||
| } | } | ||||
| increaseVolume() { | increaseVolume() { | ||||
| if (this.value < this.max) { | if (this.value < this.max) { | ||||
| this.value += 10; | this.value += 10; | ||||
| this.onChange(this.value); | |||||
| this.onTouched(); | |||||
| this.valueChange.emit(this.value); | this.valueChange.emit(this.value); | ||||
| } | } | ||||
| } | } | ||||
| decreaseVolume() { | decreaseVolume() { | ||||
| if (this.value > 0) { | if (this.value > 0) { | ||||
| this.value -= 10; | this.value -= 10; | ||||
| this.onChange(this.value); | |||||
| this.onTouched(); | |||||
| this.valueChange.emit(this.value); | this.valueChange.emit(this.value); | ||||
| } | } | ||||
| } | } |
| import {ElementRef, Injectable} from '@angular/core'; | |||||
| import { ElementRef, Injectable } from '@angular/core'; | |||||
| @Injectable({ | @Injectable({ | ||||
| providedIn: 'root' | |||||
| providedIn: 'root', | |||||
| }) | }) | ||||
| export class AlarmSoundService { | export class AlarmSoundService { | ||||
| private alertInterval: any; | private alertInterval: any; | ||||
| private alertDuration: number = 30000; | private alertDuration: number = 30000; | ||||
| private audio= new Audio(); | |||||
| private audio = new Audio(); | |||||
| constructor() { | constructor() { | ||||
| this.audio.src = 'assets/sound/alarm2.mp3'; | |||||
| this.audio.src = 'assets/sound/alarm_5m.mp3'; | |||||
| this.audio.load(); | this.audio.load(); | ||||
| } | } | ||||
| playSound(): void { | playSound(): void { | ||||
| this.audio.play().catch(error => { | |||||
| this.audio.play().catch((error) => { | |||||
| console.error('Error playing audio:', error); | console.error('Error playing audio:', error); | ||||
| }); | }); | ||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| startAlarm(state5: boolean, isReady: boolean, state1: boolean, state2: boolean): void { | |||||
| startAlarm( | |||||
| state5: boolean, | |||||
| isReady: boolean, | |||||
| state1: boolean, | |||||
| state2: boolean, | |||||
| ): void { | |||||
| if (state5 && isReady && (state1 || state2)) { | if (state5 && isReady && (state1 || state2)) { | ||||
| this.playSound(); | this.playSound(); | ||||
| this.alertInterval = setInterval(() => { | this.alertInterval = setInterval(() => { | ||||
| this.stopAlarm() | |||||
| this.stopAlarm(); | |||||
| }, this.alertDuration); | }, this.alertDuration); | ||||
| } | } | ||||
| } | } | ||||
| stopAlarm(): void { | stopAlarm(): void { | ||||
| this.stopSound() | |||||
| this.stopSound(); | |||||
| clearInterval(this.alertInterval); | clearInterval(this.alertInterval); | ||||
| } | } | ||||
| simulateClick(element: HTMLElement) { | simulateClick(element: HTMLElement) { | ||||
| element.click(); | element.click(); | ||||
| } | } | ||||
| } | } |
| 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(): void { | |||||
| if (!this.isDialogOpen) { | |||||
| this.isDialogOpen = true; | |||||
| const dialogRef = this.dialog.open(ConfirmDialogComponent); | |||||
| dialogRef | |||||
| .afterClosed() | |||||
| .pipe(take(1)) | |||||
| .subscribe(() => { | |||||
| this.isDialogOpen = false; | |||||
| }); | |||||
| } | |||||
| } | |||||
| } |
| import { Injectable } from '@angular/core'; | import { Injectable } from '@angular/core'; | ||||
| import { webSocket, WebSocketSubject } from 'rxjs/webSocket'; | |||||
| import { BehaviorSubject,Subject } from "rxjs"; | |||||
| import { config } from "../../../assets/config/config"; | |||||
| import { | import { | ||||
| IMqttMessage, | IMqttMessage, | ||||
| IMqttServiceOptions, | IMqttServiceOptions, |
| 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 { 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({ | @NgModule({ | ||||
| exports: [ | exports: [ | ||||
| FlexLayoutModule, | FlexLayoutModule, | ||||
| MatDialogModule, | MatDialogModule, | ||||
| MatIconModule, | MatIconModule, | ||||
| MatMenuModule | |||||
| ] | |||||
| MatMenuModule, | |||||
| MatExpansionModule, | |||||
| MatCardModule, | |||||
| MatCheckboxModule, | |||||
| ], | |||||
| }) | }) | ||||
| export class SharedMaterialModule {} | export class SharedMaterialModule {} |