| @@ -1,28 +1,26 @@ | |||
| import { NgModule } from '@angular/core'; | |||
| 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 = [ | |||
| { path: '', | |||
| redirectTo: '/homepage', | |||
| pathMatch: 'full' | |||
| }, | |||
| { path: '', | |||
| { path: '', redirectTo: '/homepage', pathMatch: 'full' }, | |||
| { | |||
| path: '', | |||
| component: LayoutComponent, | |||
| 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({ | |||
| imports: [RouterModule.forRoot(routes)], | |||
| exports: [RouterModule] | |||
| exports: [RouterModule], | |||
| }) | |||
| export class AppRoutingModule { } | |||
| export class AppRoutingModule {} | |||
| @@ -1 +1,7 @@ | |||
| <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> | |||
| @@ -0,0 +1,82 @@ | |||
| .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; | |||
| } | |||
| @@ -1,10 +1,220 @@ | |||
| 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({ | |||
| selector: 'app-centralized-security-management', | |||
| 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); | |||
| } | |||
| } | |||
| @@ -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' | |||
| }, | |||
| } | |||
| @@ -8,18 +8,21 @@ 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 | |||
| SecuritySystemDetailsComponent, | |||
| ], | |||
| imports: [ | |||
| CommonModule, | |||
| RouterModule.forChild(homePageRoutes), | |||
| SharedMaterialModule, | |||
| SharedModule,] | |||
| SharedModule, | |||
| FormsModule, | |||
| ], | |||
| }) | |||
| export class HomePageModule { } | |||
| export class HomePageModule {} | |||
| @@ -1,22 +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 { 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 | |||
| component: CentralizedSecurityManagementComponent, | |||
| }, | |||
| { | |||
| path: 'security-system-details', | |||
| component: CentralizedSecurityManagementComponent | |||
| } | |||
| component: SecuritySystemDetailsComponent, | |||
| }, | |||
| ]; | |||
| @@ -1,19 +1,54 @@ | |||
| <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"> | |||
| <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 class="sound-group"> | |||
| <h2>24-HOUR ZONE ALARM TIME: {{timeEffect2}}s</h2> | |||
| <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> | |||
| </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> | |||
| @@ -1,6 +1,5 @@ | |||
| .sound-group{ | |||
| padding: 3rem; | |||
| width: 70% | |||
| margin: 2rem 3rem; | |||
| } | |||
| button{ | |||
| color: #ff7723 !important; | |||
| @@ -3,9 +3,15 @@ import { Component } from '@angular/core'; | |||
| @Component({ | |||
| selector: 'app-homepage', | |||
| templateUrl: './home-page.component.html', | |||
| styleUrls: ['./home-page.component.scss'] | |||
| styleUrls: ['./home-page.component.scss'], | |||
| }) | |||
| export class HomePageComponent { | |||
| timeEffect1: any = 10; | |||
| timeEffect2: any = 30; | |||
| whistle = { | |||
| time: 10, | |||
| sound: 0, | |||
| }; | |||
| alarm = { | |||
| time: 30, | |||
| sound: 0, | |||
| }; | |||
| } | |||
| @@ -1 +1,56 @@ | |||
| <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> | |||
| @@ -0,0 +1,224 @@ | |||
| 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; | |||
| } | |||
| @@ -1,10 +1,93 @@ | |||
| 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({ | |||
| selector: 'app-security-system-details', | |||
| templateUrl: './security-system-details.component.html', | |||
| 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}); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -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,34 +0,0 @@ | |||
| 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(); | |||
| } | |||
| } | |||
| @@ -1,7 +0,0 @@ | |||
| <div class="map-container"> | |||
| <h2>Security Monitoring and Control</h2> | |||
| <div class="map-frame"> | |||
| <div id="map"></div> | |||
| </div> | |||
| </div> | |||
| @@ -1,82 +0,0 @@ | |||
| .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; | |||
| } | |||
| @@ -1,353 +0,0 @@ | |||
| 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); | |||
| } | |||
| } | |||
| @@ -1,56 +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,225 +0,0 @@ | |||
| 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; | |||
| } | |||
| @@ -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,95 +0,0 @@ | |||
| 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}); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -1,28 +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"; | |||
| import {CameraDialogComponent} from "./camera-dialog/camera-dialog.component"; | |||
| @NgModule({ | |||
| declarations: [ | |||
| OverallGroundComponent, | |||
| MapComponent, | |||
| CameraDialogComponent, | |||
| 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,29 @@ | |||
| <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> | |||
| @@ -0,0 +1,4 @@ | |||
| ul { | |||
| list-style-type: none; | |||
| margin-top: 4px; | |||
| } | |||
| @@ -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,58 @@ | |||
| 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)); | |||
| } | |||
| } | |||
| @@ -3,7 +3,6 @@ | |||
| <img src="../../../../../../assets/images/logo.png" > | |||
| <div> | |||
| <button mat-button routerLink="/homepage" routerLinkActive="active">Home</button> | |||
| <button mat-button routerLink="/overview" routerLinkActive="active">Overview</button> | |||
| </div> | |||
| </div> | |||
| <div> | |||
| @@ -1,29 +1,22 @@ | |||
| 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 { 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({ | |||
| declarations: [ | |||
| 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 {} | |||
| @@ -1,31 +1,8 @@ | |||
| <ng-content></ng-content> | |||
| <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> | |||
| <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()"> | |||
| <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"> | |||
| @@ -1,5 +1,6 @@ | |||
| .volume-group { | |||
| margin-top: 1.5rem; | |||
| padding: 0 2rem; | |||
| display: flex; | |||
| flex-direction: row; | |||
| gap: 1.5rem; | |||
| @@ -11,7 +12,7 @@ | |||
| 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; | |||
| @@ -1,21 +1,55 @@ | |||
| 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({ | |||
| selector: 'app-slider-range', | |||
| 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() 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); | |||
| } | |||
| @@ -24,19 +58,19 @@ export class SliderRangeComponent { | |||
| // Adjust valPercent based on your conditions | |||
| switch (true) { | |||
| case (valPercent > 10 && valPercent <= 30): | |||
| case valPercent > 10 && valPercent <= 30: | |||
| valPercent -= 0.5; | |||
| break; | |||
| case (valPercent > 30 && valPercent <= 50): | |||
| case valPercent > 30 && valPercent <= 50: | |||
| valPercent -= 1; | |||
| break; | |||
| case (valPercent > 50 && valPercent <= 70): | |||
| case valPercent > 50 && valPercent <= 70: | |||
| valPercent -= 1.5; | |||
| break; | |||
| case (valPercent > 70 && valPercent <= 90): | |||
| case valPercent > 70 && valPercent <= 90: | |||
| valPercent -= 2; | |||
| break; | |||
| case (valPercent > 90 && valPercent <= 100): | |||
| case valPercent > 90 && valPercent <= 100: | |||
| valPercent -= 2.3; | |||
| break; | |||
| default: | |||
| @@ -45,13 +79,15 @@ export class SliderRangeComponent { | |||
| // Return CSS object with background style | |||
| return { | |||
| 'background': `linear-gradient(to right, #ff7723 ${valPercent}%, transparent ${valPercent}%)` | |||
| 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); | |||
| } | |||
| } | |||
| @@ -59,6 +95,8 @@ export class SliderRangeComponent { | |||
| decreaseVolume() { | |||
| if (this.value > 0) { | |||
| this.value -= 10; | |||
| this.onChange(this.value); | |||
| this.onTouched(); | |||
| this.valueChange.emit(this.value); | |||
| } | |||
| } | |||
| @@ -1,19 +1,19 @@ | |||
| import {ElementRef, Injectable} from '@angular/core'; | |||
| import { ElementRef, Injectable } from '@angular/core'; | |||
| @Injectable({ | |||
| providedIn: 'root' | |||
| providedIn: 'root', | |||
| }) | |||
| export class AlarmSoundService { | |||
| private alertInterval: any; | |||
| private alertDuration: number = 30000; | |||
| private audio= new Audio(); | |||
| private audio = new Audio(); | |||
| constructor() { | |||
| this.audio.src = 'assets/sound/alarm2.mp3'; | |||
| this.audio.src = 'assets/sound/alarm_5m.mp3'; | |||
| this.audio.load(); | |||
| } | |||
| playSound(): void { | |||
| this.audio.play().catch(error => { | |||
| this.audio.play().catch((error) => { | |||
| console.error('Error playing audio:', error); | |||
| }); | |||
| } | |||
| @@ -25,22 +25,26 @@ export class AlarmSoundService { | |||
| } | |||
| } | |||
| 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)) { | |||
| this.playSound(); | |||
| this.alertInterval = setInterval(() => { | |||
| this.stopAlarm() | |||
| this.stopAlarm(); | |||
| }, this.alertDuration); | |||
| } | |||
| } | |||
| stopAlarm(): void { | |||
| this.stopSound() | |||
| this.stopSound(); | |||
| clearInterval(this.alertInterval); | |||
| } | |||
| simulateClick(element: HTMLElement) { | |||
| element.click(); | |||
| } | |||
| } | |||
| @@ -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(): 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,9 +1,12 @@ | |||
| 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({ | |||
| exports: [ | |||
| @@ -11,7 +14,10 @@ import {MatMenuModule} from "@angular/material/menu"; | |||
| FlexLayoutModule, | |||
| MatDialogModule, | |||
| MatIconModule, | |||
| MatMenuModule | |||
| ] | |||
| MatMenuModule, | |||
| MatExpansionModule, | |||
| MatCardModule, | |||
| MatCheckboxModule, | |||
| ], | |||
| }) | |||
| export class SharedMaterialModule {} | |||