-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathanalysis.service.ts
More file actions
263 lines (244 loc) · 11.4 KB
/
Copy pathanalysis.service.ts
File metadata and controls
263 lines (244 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
import type { HttpErrorResponse } from '@angular/common/http'
import { HttpClient } from '@angular/common/http'
import { inject, Injectable } from '@angular/core'
import type { Observable, Subscription } from 'rxjs'
import { BehaviorSubject, catchError, interval, map, Subject, takeUntil, takeWhile, tap, withLatestFrom } from 'rxjs'
import type { FullProgressResponse } from '@seed/api'
import { OrganizationService } from '@seed/api'
import { ErrorService } from '@seed/services'
import { SnackBarService } from 'app/core/snack-bar/snack-bar.service'
import { UserService } from '../user'
import type { AnalysesMessage, Analysis, AnalysisCreateData, AnalysisResponse, AnalysisServiceType, AnalysisSummary, AnalysisView, AnalysisViews, ListAnalysesResponse, ListMessagesResponse, PropertyAnalysesResponse, View } from './analysis.types'
@Injectable({ providedIn: 'root' })
export class AnalysisService {
private _httpClient = inject(HttpClient)
private _organizationService = inject(OrganizationService)
private _userService = inject(UserService)
private _snackBar = inject(SnackBarService)
private _errorService = inject(ErrorService)
private _analyses = new BehaviorSubject<Analysis[]>([])
private _analysis = new BehaviorSubject<Analysis>(null)
private _views = new BehaviorSubject<View[]>([])
private _view = new BehaviorSubject<View>(null)
private _originalView = new BehaviorSubject<number>(null)
private _originalViews = new BehaviorSubject<Record<number, number>>(null)
private _messages = new BehaviorSubject<AnalysesMessage[]>([])
private readonly _unsubscribeAll$ = new Subject<void>()
orgId: number
analyses$ = this._analyses.asObservable()
analysis$ = this._analysis.asObservable()
views$ = this._views.asObservable()
view$ = this._view.asObservable()
originalView$ = this._originalView.asObservable()
originalViews$ = this._originalViews.asObservable()
messages$ = this._messages.asObservable()
pollingStatuses?: Subscription
constructor() {
this._userService.currentOrganizationId$
.pipe(
takeUntil(this._unsubscribeAll$),
tap((orgId) => {
this.orgId = orgId
this.getAnalyses(orgId)
}),
)
.subscribe()
}
// Method to update the analyses list
updateAnalyses(analyses: Analysis[]): void {
this._analyses.next(analyses)
}
getAnalyses(orgId: number) {
const url = `/api/v3/analyses/?organization_id=${orgId}`
this._httpClient
.get<ListAnalysesResponse>(url)
.pipe(
map((response) => response),
tap((response) => {
this._analyses.next(response.analyses)
this._views.next(response.views)
this._originalViews.next(response.original_views)
}),
catchError((error: HttpErrorResponse) => {
return this._errorService.handleError(error, 'Error fetching analyses')
}),
)
.subscribe()
}
// get AnalysesMessages (for all analyses or for a single one)
getMessages(orgId: number, analysisId: number) {
const url = `/api/v3/analyses/${analysisId}/messages/?organization_id=${orgId}`
this._httpClient
.get<ListMessagesResponse>(url)
.pipe(
map((response) => response.messages),
tap((response) => {
this._messages.next(response)
}),
catchError((error: HttpErrorResponse) => {
return this._errorService.handleError(error, 'Error fetching analyses messages')
}),
)
.subscribe()
}
// get single analysis
getAnalysis(orgId: number, analysisId: number) {
const url = `/api/v3/analyses/${analysisId}?organization_id=${orgId}`
this._httpClient
.get<AnalysisResponse>(url)
.pipe(
map((response) => response.analysis),
tap((analysis) => {
this._analysis.next(analysis)
}),
catchError((error: HttpErrorResponse) => {
return this._errorService.handleError(error, 'Error fetching analyses')
}),
)
.subscribe()
}
// get analyses for a property (by property ID)
getPropertyAnalyses(_propertyId): Observable<Analysis[]> {
const url = `/api/v3/properties/${_propertyId}/analyses/?organization_id=${this.orgId}`
return this._httpClient.get<PropertyAnalysesResponse>(url).pipe(
map((response) => response.analyses),
catchError((error: HttpErrorResponse) => {
return this._errorService.handleError(error, 'Error fetching analyses for this property')
}),
)
}
// get analysis views
getAnalysisViews(orgId: number, analysisId: number) {
const url = `/api/v3/analyses/${analysisId}/views?organization_id=${orgId}`
this._httpClient
.get<AnalysisViews>(url)
.pipe(
tap((response) => {
this._views.next(response.views)
this._originalViews.next(response.original_views)
}),
catchError((error: HttpErrorResponse) => {
return this._errorService.handleError(error, 'Error fetching analyses')
}),
)
.subscribe()
}
// get analysis view
getAnalysisView(orgId: number, analysisId: number, viewId: number): Observable<AnalysisView> {
const url = `/api/v3/analyses/${analysisId}/views/${viewId}/?organization_id=${orgId}`
return this._httpClient.get<AnalysisView>(url).pipe(
tap(({ view, original_view }) => {
this._view.next(view)
this._originalView.next(original_view)
}),
catchError((error: HttpErrorResponse) => {
return this._errorService.handleError(error, 'Error fetching analysis view')
}),
)
}
// delete analysis
delete(orgId: number, id: number) {
const url = `/api/v3/analyses/${id}/?organization_id=${this.orgId}`
return this._httpClient.delete(url).pipe(
tap(() => {
this._snackBar.success('Analysis deleted successfully')
this.getAnalyses(orgId)
}),
catchError((error: HttpErrorResponse) => {
return this._errorService.handleError(error, 'Error deleting analysis')
}),
)
}
create(orgId: number, data: AnalysisCreateData): Observable<FullProgressResponse> {
const url = `/api/v3/analyses/?organization_id=${orgId}&start_analysis=true`
return this._httpClient.post<FullProgressResponse>(url, data).pipe(
tap((response) => {
if (response.status === 'error') {
return this._errorService.handleError(response.errors as HttpErrorResponse, 'Error creating analysis')
}
this._snackBar.success('Running Analysis')
this.getAnalyses(orgId)
}),
catchError((error: HttpErrorResponse) => {
return this._errorService.handleError(error, 'Error creating analysis')
}),
)
}
summary(orgId: number, cycleId: number): Observable<AnalysisSummary> {
const url = `/api/v4/analyses/stats/?cycle_id=${cycleId}&organization_id=${orgId}`
return this._httpClient.get<AnalysisSummary>(url).pipe(
catchError((error: HttpErrorResponse) => {
return this._errorService.handleError(error, 'Error fetching analysis summary')
}),
)
}
stopAnalysis(orgId: number, analysisId: number): Observable<Analysis> {
const url = `/api/v3/analyses/${analysisId}/stop/?organization_id=${orgId}`
return this._httpClient.post<Analysis>(url, {}).pipe(
tap((data) => {
console.log('Analysis stopped:', data)
this._snackBar.success('Analysis stopped successfully')
this.getAnalyses(orgId)
}),
catchError((error: HttpErrorResponse) => {
return this._errorService.handleError(error, 'Error stopping analysis')
}),
)
}
startAnalysis(orgId: number, analysisId: number): Observable<Analysis> {
const url = `/api/v3/analyses/${analysisId}/start/?organization_id=${orgId}`
return this._httpClient.post<Analysis>(url, {}).pipe(
tap((data) => {
console.log('Analysis started:', data)
this._snackBar.success('Analysis started')
this.getAnalyses(orgId)
}),
catchError((error: HttpErrorResponse) => {
return this._errorService.handleError(error, 'Error starting analysis')
}),
)
}
/*
* Poll for analysis statuses
* Fetch analyses every 5 seconds until all analyses are in a terminal state
*/
pollStatuses(orgId: number) {
const isPolling = this.pollingStatuses && !this.pollingStatuses.closed
if (isPolling) return
const runningStatuses = new Set(['Pending Creation', 'Creating', 'Queued', 'Running'])
this.pollingStatuses = interval(5000)
.pipe(
withLatestFrom(this.analyses$),
takeWhile(([_, analyses]) => analyses.some((a) => runningStatuses.has(a.status)), true),
tap(() => {
this.getAnalyses(orgId)
}),
)
.subscribe()
}
getAnalysisDescription(analysis: Analysis): string {
const descriptionMap: Record<AnalysisServiceType, string> = {
BETTER:
"The BETTER analysis leverages better.lbl.gov to calculate energy, cost, and GHG emission savings by comparing the property's change point model with a benchmarked model. The results include saving potential and a list of recommended high-level energy conservation measures.",
BSyncr:
'The BSyncr analysis leverages the Normalized Metered Energy Consumption (NMEC) analysis to calculate a change point model. The data are passed to the analysis using BuildingSync. The result of the analysis are the coefficients of the change point model.',
'Building Upgrade Recommendation':
'The Building Upgrade Recommendation analysis implements a workflow to identify buildings that may need a deep energy retrofit, equipment replaced or re-tuning based on building attributes such as energy use, year built, and square footage. If your organization contains elements, the Element Statistics Analysis should be run prior to running this analysis.',
CO2: "This analysis calculates the average annual CO2 emissions for the property's meter data. The analysis requires an eGRID Subregion to be defined in order to accurately determine the emission rates.",
EEEJ: "The EEEJ Analysis uses each property's address to identify the 2010 census tract. Based on census tract, disadvantaged community classification and energy burden information can be retrieved from the CEJST dataset. The number of affordable housing locations is retrieved from HUD datasets. Location is used to generate a link to view an EJScreen Report providing more demographic indicators.",
EUI: "The EUI analysis will sum the property's meter readings for the last twelve months to calculate the energy use per square footage per year. If there are missing meter readings, then the analysis will return a less that 100% coverage to alert the user that there is a missing meter reading.",
'Element Statistics':
"The Element Statistics analysis looks through a property's element data (if any) to count the number of elements of type 'D.D.C. Control Panel'. It also generates the aggregated (average) condition index values for scope 1 emission elements and saves those quantities to the property.",
}
return descriptionMap[analysis.service] ?? 'No description available for this analysis.'
}
getRunDuration({ data }: { data: Analysis }) {
if (!data.start_time || !data.end_time) return ''
const start = new Date(data.start_time)
const end = new Date(data.end_time)
const duration = Math.abs(end.getTime() - start.getTime())
const minutes = Math.floor((duration % (1000 * 60 * 60)) / (1000 * 60))
const seconds = Math.floor((duration % (1000 * 60)) / 1000)
return `${minutes}m ${seconds}s`
}
}