19 changed files with 1330 additions and 99 deletions
@ -0,0 +1,444 @@ |
|||
///
|
|||
/// Copyright © 2016-2025 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { TbMeasure, TbUnitConvertor, Unit, UnitDescription, UnitSystem } from '@shared/models/unit.models'; |
|||
import { AllMeasures } from '@core/services/unit/definitions/all'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils'; |
|||
|
|||
export interface Conversion< |
|||
TMeasures extends string, |
|||
TSystems extends string, |
|||
TUnits extends string, |
|||
> { |
|||
abbr: TUnits; |
|||
measure: TMeasures; |
|||
system: TSystems; |
|||
unit: Unit; |
|||
} |
|||
|
|||
// export interface BestResult<TUnits extends string> {
|
|||
// val: number;
|
|||
// unit: TUnits;
|
|||
// name: string;
|
|||
// tags: string[];
|
|||
// }
|
|||
|
|||
type Entries<T, S extends keyof T> = [S, T[keyof T]]; |
|||
|
|||
export type UnitCache<TMeasures, TSystems, TUnits> = Map< |
|||
string, |
|||
{ |
|||
system: TSystems; |
|||
measure: TMeasures; |
|||
unit: Unit; |
|||
abbr: TUnits; |
|||
} |
|||
>; |
|||
|
|||
export class Converter< |
|||
TMeasures extends AllMeasures, |
|||
TSystems extends UnitSystem, |
|||
TUnits extends string, |
|||
> { |
|||
private measureData: Record<TMeasures, TbMeasure<TSystems, TUnits>>; |
|||
private unitCache: Map< |
|||
string, |
|||
{ |
|||
system: TSystems; |
|||
measure: TMeasures; |
|||
unit: Unit; |
|||
abbr: TUnits; |
|||
} |
|||
>; |
|||
|
|||
constructor( |
|||
measures: Record<TMeasures, TbMeasure<TSystems, TUnits>>, |
|||
unitCache: UnitCache<TMeasures, TSystems, TUnits> |
|||
) { |
|||
this.measureData = measures; |
|||
this.unitCache = unitCache; |
|||
} |
|||
|
|||
convertor(from: TUnits | (string & {}), to: TUnits | (string & {})): TbUnitConvertor{ |
|||
const origin = this.getUnit(from); |
|||
if (origin === null) { |
|||
throw Error(`Unsupported unit ${from}`); |
|||
} |
|||
const destination = this.getUnit(to); |
|||
if (destination === null) { |
|||
throw Error(`Unsupported unit ${from}`); |
|||
} |
|||
if (origin.abbr === destination.abbr) { |
|||
return (value: number) => value; |
|||
} |
|||
if (destination.measure !== origin.measure) { |
|||
throw Error(`Cannot convert incompatible measures of ${destination.measure} and ${origin.measure}`); |
|||
} |
|||
return (value: number): number => { |
|||
let result = value * origin.unit.to_anchor; |
|||
if (origin.unit.anchor_shift) { |
|||
result -= origin.unit.anchor_shift; |
|||
} |
|||
|
|||
if (origin.system !== destination.system) { |
|||
const measure = this.measureData[origin.measure]; |
|||
const anchors = measure.anchors; |
|||
if (!anchors) { |
|||
throw Error(`Unable to convert units. Anchors are missing for "${origin.measure}" and "${destination.measure}" measures.`); |
|||
} |
|||
|
|||
const anchor = anchors[origin.system]; |
|||
if (!anchor) { |
|||
throw Error(`Unable to convert units. Anchors are missing for "${origin.measure}" and "${destination.measure}" measures.`); |
|||
} |
|||
|
|||
const transform = anchor[destination.system]?.transform; |
|||
const ratio = anchor[destination.system]?.ratio; |
|||
|
|||
if (typeof transform === 'function') { |
|||
result = transform(result); |
|||
} else if (typeof ratio === 'number') { |
|||
result *= ratio; |
|||
} else { |
|||
throw Error('A system anchor needs to either have a defined ratio number or a transform function.'); |
|||
} |
|||
} |
|||
|
|||
if (destination.unit.anchor_shift) { |
|||
result += destination.unit.anchor_shift; |
|||
} |
|||
return result / destination.unit.to_anchor; |
|||
}; |
|||
} |
|||
|
|||
convert(value: number, from: TUnits | (string & {}), to: TUnits | (string & {})): number { |
|||
const origin = this.getUnit(from); |
|||
if (origin === null) { |
|||
throw Error(`Unsupported unit ${from}`); |
|||
} |
|||
const destination = this.getUnit(to); |
|||
if (destination === null) { |
|||
throw Error(`Unsupported unit ${from}`); |
|||
} |
|||
if (origin.abbr === destination.abbr) { |
|||
return value; |
|||
} |
|||
if (destination.measure !== origin.measure) { |
|||
throw Error(`Cannot convert incompatible measures of ${destination.measure} and ${origin.measure}`); |
|||
} |
|||
let result = value * origin.unit.to_anchor; |
|||
if (origin.unit.anchor_shift) { |
|||
result -= origin.unit.anchor_shift; |
|||
} |
|||
if (origin.system !== destination.system) { |
|||
const measure = this.measureData[origin.measure]; |
|||
const anchors = measure.anchors; |
|||
if (!anchors) { |
|||
throw Error(`Unable to convert units. Anchors are missing for "${origin.measure}" and "${destination.measure}" measures.`); |
|||
} |
|||
const anchor = anchors[origin.system]; |
|||
if (!anchor) { |
|||
throw Error(`Unable to convert units. Anchors are missing for "${origin.measure}" and "${destination.measure}" measures.`); |
|||
} |
|||
const transform = anchor[destination.system]?.transform; |
|||
const ratio = anchor[destination.system]?.ratio; |
|||
if (typeof transform === 'function') { |
|||
result = transform(result); |
|||
} else if (typeof ratio === 'number') { |
|||
result *= ratio; |
|||
} else { |
|||
throw Error('A system anchor needs to either have a defined ratio number or a transform function.'); |
|||
} |
|||
} |
|||
|
|||
if (destination.unit.anchor_shift) { |
|||
result += destination.unit.anchor_shift; |
|||
} |
|||
return result / destination.unit.to_anchor; |
|||
} |
|||
|
|||
// toBest(options?: {
|
|||
// exclude?: (TUnits | (string & {}))[];
|
|||
// cutOffNumber?: number;
|
|||
// system?: TSystems | (string & {});
|
|||
// }): BestResult<TUnits> | null {
|
|||
// if (this.origin == null)
|
|||
// throw new OperationOrderError('.toBest must be called after .from');
|
|||
//
|
|||
// const isNegative = this.val < 0;
|
|||
//
|
|||
// let exclude: (TUnits | (string & {}))[] = [];
|
|||
// let cutOffNumber = isNegative ? -1 : 1;
|
|||
// let system: TSystems | (string & {}) = this.origin.system;
|
|||
//
|
|||
// if (typeof options === 'object') {
|
|||
// exclude = options.exclude ?? [];
|
|||
// cutOffNumber = options.cutOffNumber ?? cutOffNumber;
|
|||
// system = options.system ?? this.origin.system;
|
|||
// }
|
|||
//
|
|||
// let best: BestResult<TUnits> | null = null;
|
|||
// /**
|
|||
// Looks through every possibility for the 'best' available unit.
|
|||
// i.e. Where the value has the fewest numbers before the decimal point,
|
|||
// but is still higher than 1.
|
|||
// */
|
|||
// for (const possibility of this.possibilities()) {
|
|||
// const unit = this.describe(possibility);
|
|||
// const isIncluded = exclude.indexOf(possibility) === -1;
|
|||
//
|
|||
// if (isIncluded && unit.system === system) {
|
|||
// const result = this.to(possibility);
|
|||
// if (isNegative ? result > cutOffNumber : result < cutOffNumber) {
|
|||
// continue;
|
|||
// }
|
|||
// if (
|
|||
// best === null ||
|
|||
// (isNegative
|
|||
// ? result <= cutOffNumber && result > best.val
|
|||
// : result >= cutOffNumber && result < best.val)
|
|||
// ) {
|
|||
// best = {
|
|||
// val: result,
|
|||
// unit: possibility,
|
|||
// name: unit.name,
|
|||
// tags: unit.tags
|
|||
// };
|
|||
// }
|
|||
// }
|
|||
// }
|
|||
//
|
|||
// if (best == null) {
|
|||
// return {
|
|||
// val: this.val,
|
|||
// unit: this.origin.abbr,
|
|||
// name: this.origin.unit.name,
|
|||
// tags: this.origin.unit.tags
|
|||
// };
|
|||
// }
|
|||
//
|
|||
// return best;
|
|||
// }
|
|||
|
|||
getUnit(abbr: TUnits | (string & {})): Conversion<TMeasures, TSystems, TUnits> | null { |
|||
return this.unitCache.get(abbr) ?? null; |
|||
} |
|||
|
|||
describe(abbr: TUnits | (string & {})): UnitDescription { |
|||
const result = this.getUnit(abbr); |
|||
|
|||
if (result != null) { |
|||
return this.describeUnit(result); |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
private describeUnit(unit: Conversion<TMeasures, TSystems, TUnits>): UnitDescription { |
|||
return { |
|||
abbr: unit.abbr, |
|||
measure: unit.measure, |
|||
system: unit.system, |
|||
name: unit.unit.name, |
|||
tags: unit.unit.tags |
|||
}; |
|||
} |
|||
|
|||
list(measureName?: TMeasures | (string & {}), unitSystem?: UnitSystem): UnitDescription[] | never { |
|||
const list = []; |
|||
|
|||
if (isDefinedAndNotNull(measureName)) { |
|||
if (!this.isMeasure(measureName)) { |
|||
console.log(`Measure "${measureName}" not found.`); |
|||
return list; |
|||
} |
|||
const measure = this.measureData[measureName]; |
|||
if (isDefinedAndNotNull(unitSystem)) { |
|||
let currentUnitSystem = unitSystem; |
|||
let units = measure.systems[currentUnitSystem]; |
|||
if (isUndefinedOrNull(units)) { |
|||
if (currentUnitSystem === UnitSystem.IMPERIAL) { |
|||
currentUnitSystem = UnitSystem.METRIC; |
|||
units = measure.systems[currentUnitSystem]; |
|||
} |
|||
if (!units) { |
|||
console.log(`Measure "${measureName}" in ${currentUnitSystem} system is not found.`); |
|||
return list; |
|||
} |
|||
} |
|||
for (const [abbr, unit] of Object.entries( |
|||
units |
|||
)) { |
|||
list.push( |
|||
this.describeUnit({ |
|||
abbr: abbr as TUnits, |
|||
measure: measureName as TMeasures, |
|||
system: currentUnitSystem as TSystems, |
|||
unit: unit as Unit, |
|||
}) |
|||
); |
|||
} |
|||
} else { |
|||
for (const [systemName, units] of Object.entries( |
|||
(measure as TbMeasure<TSystems, TUnits>).systems |
|||
)) { |
|||
for (const [abbr, unit] of Object.entries( |
|||
units as Partial<Record<TUnits, Unit>> |
|||
)) { |
|||
list.push( |
|||
this.describeUnit({ |
|||
abbr: abbr as TUnits, |
|||
measure: measureName as TMeasures, |
|||
system: systemName as TSystems, |
|||
unit: unit as Unit, |
|||
}) |
|||
); |
|||
} |
|||
} |
|||
} |
|||
} else { |
|||
for (const [name, measure] of Object.entries(this.measureData)) { |
|||
if (isDefinedAndNotNull(unitSystem)) { |
|||
let currentUnitSystem = unitSystem; |
|||
let units = (measure as TbMeasure<TSystems, TUnits>).systems[currentUnitSystem]; |
|||
if (isUndefinedOrNull(units)) { |
|||
if (currentUnitSystem === UnitSystem.IMPERIAL) { |
|||
currentUnitSystem = UnitSystem.METRIC; |
|||
units = (measure as TbMeasure<TSystems, TUnits>).systems[currentUnitSystem]; |
|||
} |
|||
if (!units) { |
|||
console.log(`Measure "${measureName}" in ${currentUnitSystem} system is not found.`); |
|||
continue; |
|||
} |
|||
} |
|||
for (const [abbr, unit] of Object.entries( |
|||
units as Partial<Record<TUnits, Unit>> |
|||
)) { |
|||
list.push( |
|||
this.describeUnit({ |
|||
abbr: abbr as TUnits, |
|||
measure: name as TMeasures, |
|||
system: currentUnitSystem as TSystems, |
|||
unit: unit as Unit, |
|||
}) |
|||
); |
|||
} |
|||
} else { |
|||
for (const [systemName, units] of Object.entries( |
|||
(measure as TbMeasure<TSystems, TUnits>).systems |
|||
)) { |
|||
for (const [abbr, unit] of Object.entries( |
|||
units as Partial<Record<TUnits, Unit>> |
|||
)) { |
|||
list.push( |
|||
this.describeUnit({ |
|||
abbr: abbr as TUnits, |
|||
measure: name as TMeasures, |
|||
system: systemName as TSystems, |
|||
unit: unit as Unit, |
|||
}) |
|||
); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
return list; |
|||
} |
|||
|
|||
private isMeasure(measureName: string): measureName is TMeasures { |
|||
return measureName in this.measureData; |
|||
} |
|||
|
|||
// possibilities(forMeasure?: TMeasures | (string & {})): TUnits[] {
|
|||
// let possibilities: TUnits[] = [];
|
|||
// let list_measures: TMeasures[] = [];
|
|||
//
|
|||
// if (typeof forMeasure == 'string' && this.isMeasure(forMeasure)) {
|
|||
// list_measures.push(forMeasure);
|
|||
// } else if (this.origin != null) {
|
|||
// list_measures.push(this.origin.measure);
|
|||
// } else {
|
|||
// list_measures = Object.keys(this.measureData) as TMeasures[];
|
|||
// }
|
|||
//
|
|||
// for (const measure of list_measures) {
|
|||
// const systems = this.measureData[measure].systems;
|
|||
//
|
|||
// for (const system of Object.values(systems)) {
|
|||
// possibilities = [
|
|||
// ...possibilities,
|
|||
// ...(Object.keys(system as Record<TUnits, Unit>) as TUnits[]),
|
|||
// ];
|
|||
// }
|
|||
// }
|
|||
//
|
|||
// return possibilities;
|
|||
// }
|
|||
|
|||
// measures(): TMeasures[] {
|
|||
// return Object.keys(this.measureData) as TMeasures[];
|
|||
// }
|
|||
} |
|||
|
|||
export function buildUnitCache< |
|||
TMeasures extends string, |
|||
TSystems extends UnitSystem, |
|||
TUnits extends string, |
|||
>(measures: Record<TMeasures, TbMeasure<TSystems, TUnits>>, |
|||
translate: TranslateService |
|||
) { |
|||
const unitCache: UnitCache<TMeasures, TSystems, TUnits> = new Map(); |
|||
for (const [measureName, measure] of Object.entries(measures) as Entries< |
|||
typeof measures, |
|||
TMeasures |
|||
>[]) { |
|||
for (const [systemName, system] of Object.entries( |
|||
measure.systems |
|||
) as Entries<Record<TSystems, Record<TUnits, Unit>>, TSystems>[]) { |
|||
for (const [testAbbr, unit] of Object.entries(system) as Entries< |
|||
typeof system, |
|||
TUnits |
|||
>[]) { |
|||
unit.name = translate.instant(unit.name); |
|||
unitCache.set(testAbbr, { |
|||
measure: measureName, |
|||
system: systemName, |
|||
abbr: testAbbr, |
|||
unit, |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
return unitCache; |
|||
} |
|||
|
|||
export function configureMeasurements< |
|||
TMeasures extends AllMeasures, |
|||
TSystems extends UnitSystem, |
|||
TUnits extends string, |
|||
>( |
|||
measures: Record<TMeasures, TbMeasure<TSystems, TUnits>>, |
|||
translate: TranslateService |
|||
): Converter<TMeasures, TSystems, TUnits> { |
|||
if (typeof measures !== 'object') { |
|||
throw new TypeError('The measures argument needs to be an object'); |
|||
} |
|||
|
|||
const unitCache = buildUnitCache(measures, translate); |
|||
return new Converter<TMeasures, TSystems, TUnits>(measures, unitCache); |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
///
|
|||
/// Copyright © 2016-2025 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import temperature, { |
|||
TemperatureUnits, |
|||
} from './temperature'; |
|||
import time, { TimeUnits } from './time'; |
|||
import { TbMeasure, UnitSystem } from '@shared/models/unit.models'; |
|||
|
|||
export type AllMeasuresUnits = |
|||
| TemperatureUnits |
|||
| TimeUnits; |
|||
|
|||
export type AllMeasures = |
|||
| 'temperature' |
|||
| 'time'; |
|||
|
|||
const allMeasures: Record< |
|||
AllMeasures, |
|||
TbMeasure<UnitSystem, AllMeasuresUnits> |
|||
> = { |
|||
temperature, |
|||
time, |
|||
}; |
|||
|
|||
export default allMeasures; |
|||
@ -0,0 +1,77 @@ |
|||
///
|
|||
/// Copyright © 2016-2025 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { TbMeasure, Unit, UnitSystem } from '@shared/models/unit.models'; |
|||
|
|||
export type TemperatureMetricUnits = '°C' | 'K'; |
|||
export type TemperatureImperialUnits = '°F' | '°R'; |
|||
|
|||
export type TemperatureUnits = |
|||
| TemperatureMetricUnits |
|||
| TemperatureImperialUnits; |
|||
|
|||
const METRIC: Record<TemperatureMetricUnits, Unit> = { |
|||
'°C': { |
|||
name: 'unit.celsius', |
|||
tags: ['temperature','heat','cold','warmth','degrees','celsius','shipment condition','°C'], |
|||
to_anchor: 1, |
|||
}, |
|||
K: { |
|||
name: 'unit.kelvin', |
|||
tags: ['temperature','heat','cold','warmth','degrees','kelvin','K','color quality','white balance','color temperature'], |
|||
to_anchor: 1, |
|||
anchor_shift: 273.15, |
|||
}, |
|||
}; |
|||
|
|||
const IMPERIAL: Record<TemperatureImperialUnits, Unit> = { |
|||
'°F': { |
|||
name: 'unit.fahrenheit', |
|||
tags: ['temperature','heat','cold','warmth','degrees','fahrenheit','°F'], |
|||
to_anchor: 1, |
|||
}, |
|||
'°R': { |
|||
name: 'unit.rankine', |
|||
tags: ['temperature','heat','cold','warmth','Rankine','°R'], |
|||
to_anchor: 1, |
|||
anchor_shift: 459.67, |
|||
}, |
|||
}; |
|||
|
|||
const measure: TbMeasure<UnitSystem, TemperatureUnits> = { |
|||
systems: { |
|||
METRIC, |
|||
IMPERIAL, |
|||
}, |
|||
anchors: { |
|||
METRIC: { |
|||
IMPERIAL: { |
|||
transform: function (C: number): number { |
|||
return C / (5 / 9) + 32; |
|||
}, |
|||
}, |
|||
}, |
|||
IMPERIAL: { |
|||
METRIC: { |
|||
transform: function (F: number): number { |
|||
return (F - 32) * (5 / 9); |
|||
}, |
|||
}, |
|||
}, |
|||
}, |
|||
}; |
|||
|
|||
export default measure; |
|||
@ -0,0 +1,76 @@ |
|||
///
|
|||
/// Copyright © 2016-2025 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { TbMeasure, Unit, UnitSystem } from '@shared/models/unit.models'; |
|||
|
|||
export type TimeUnits = TimeSIUnits; |
|||
|
|||
export type TimeSIUnits = |
|||
| 's' |
|||
| 'min' |
|||
| 'h' |
|||
| 'd' |
|||
| 'wk' |
|||
| 'mo' |
|||
| 'yr'; |
|||
|
|||
const daysInYear = 365.25; |
|||
|
|||
const METRIC: Record<TimeSIUnits, Unit> = { |
|||
s: { |
|||
name: 'unit.second', |
|||
tags: ["time","duration","interval","angle","second","arcsecond","sec"], |
|||
to_anchor: 1, |
|||
}, |
|||
min: { |
|||
name: 'unit.minute', |
|||
tags: ["time","duration","interval","angle","minute","arcminute","min"], |
|||
to_anchor: 60, |
|||
}, |
|||
h: { |
|||
name: 'unit.hour', |
|||
tags: ["time","duration","interval","h"], |
|||
to_anchor: 60 * 60, |
|||
}, |
|||
d: { |
|||
name: 'unit.day', |
|||
tags: ["time","duration","interval","d"], |
|||
to_anchor: 60 * 60 * 24, |
|||
}, |
|||
wk: { |
|||
name: 'unit.week', |
|||
tags: ["time","duration","interval","wk"], |
|||
to_anchor: 60 * 60 * 24 * 7, |
|||
}, |
|||
mo: { |
|||
name: 'unit.month', |
|||
tags: ["time","duration","interval","mo"], |
|||
to_anchor: (60 * 60 * 24 * daysInYear) / 12, |
|||
}, |
|||
yr: { |
|||
name: 'unit.year', |
|||
tags: ["time","duration","interval","yr"], |
|||
to_anchor: 60 * 60 * 24 * daysInYear, |
|||
}, |
|||
}; |
|||
|
|||
const measure: TbMeasure<UnitSystem, TimeUnits> = { |
|||
systems: { |
|||
METRIC, |
|||
}, |
|||
}; |
|||
|
|||
export default measure; |
|||
@ -0,0 +1,89 @@ |
|||
///
|
|||
/// Copyright © 2016-2025 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Injectable } from '@angular/core'; |
|||
import moment from 'moment-timezone'; |
|||
import { TbUnitConvertor, UnitDescription, UnitSystem } from '@shared/models/unit.models'; |
|||
import { isNotEmptyStr } from '@core/utils'; |
|||
import { configureMeasurements, Converter } from '@core/services/unit/converter-unit'; |
|||
import allMeasures, { AllMeasures, AllMeasuresUnits } from '@core/services/unit/definitions/all'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
|
|||
@Injectable({ |
|||
providedIn: 'root' |
|||
}) |
|||
export class UnitService { |
|||
|
|||
private currentUnitSystem: UnitSystem = UnitSystem.METRIC; |
|||
private converter: Converter<AllMeasures, UnitSystem, AllMeasuresUnits>; |
|||
|
|||
constructor(private store: Store<AppState>, |
|||
private translate: TranslateService) { |
|||
this.translate.onLangChange.pipe( |
|||
takeUntilDestroyed() |
|||
).subscribe(() => { |
|||
this.converter = configureMeasurements<AllMeasures, UnitSystem, AllMeasuresUnits>(allMeasures, this.translate); |
|||
console.warn(this.converter?.list()); |
|||
console.warn(this.converter?.list('temperature')); |
|||
console.warn(this.converter?.list('temperature', UnitSystem.METRIC)); |
|||
console.warn(this.converter?.list(null, UnitSystem.IMPERIAL)); |
|||
}); |
|||
} |
|||
|
|||
getUnitSystem(): UnitSystem { |
|||
return this.currentUnitSystem; |
|||
} |
|||
|
|||
setUnitSystem(unitSystem: UnitSystem) { |
|||
if (isNotEmptyStr(unitSystem)) { |
|||
this.currentUnitSystem = unitSystem; |
|||
} else { |
|||
this.currentUnitSystem = this.getUnitSystemByTimezone(); |
|||
} |
|||
console.warn('[Unit system] setUnitSystem', this.currentUnitSystem); |
|||
} |
|||
|
|||
getUnits(measure?: AllMeasures, unitSystem?: UnitSystem): UnitDescription[] { |
|||
return this.converter?.list(measure, unitSystem) ?? []; |
|||
} |
|||
|
|||
getUnitDescription(abbr: AllMeasuresUnits | string): UnitDescription { |
|||
return this.converter.describe(abbr); |
|||
} |
|||
|
|||
geUnitConvertor(from: string, to: string): TbUnitConvertor { |
|||
return this.converter.convertor(from, to); |
|||
} |
|||
|
|||
convertValue(value: number, from: string, to: string): number { |
|||
return this.converter.convert(value, from, to); |
|||
} |
|||
|
|||
private getUnitSystemByTimezone(): UnitSystem { |
|||
const timeZone = moment.tz.guess(true); |
|||
const imperialCountries = ['US', 'LR', 'MM']; |
|||
|
|||
if (moment.tz.zonesForCountry('GB').includes(timeZone)) { |
|||
return UnitSystem.HYBRID; |
|||
} |
|||
return imperialCountries.some(country => |
|||
moment.tz.zonesForCountry(country).includes(timeZone) |
|||
) ? UnitSystem.IMPERIAL : UnitSystem.METRIC; |
|||
} |
|||
} |
|||
@ -0,0 +1,71 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2025 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<div class="tb-convert-settings-panel"> |
|||
<div class="tb-convert-settings-title">Unit convertion settings</div> |
|||
<div class="tb-convert-settings-panel-content" [formGroup]="convertUnitForm"> |
|||
<div class="tb-form-row"> |
|||
<div class="min-w-25">From</div> |
|||
<tb-unit-input class="flex-1" [required]="required" formControlName="from"></tb-unit-input> |
|||
</div> |
|||
<div class="tb-form-row"> |
|||
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="convertUnit"> |
|||
Convert units |
|||
</mat-slide-toggle> |
|||
</div> |
|||
@if(convertUnitForm.get('convertUnit').value) { |
|||
<div class="tb-form-row"> |
|||
<div class="min-w-25">Metrical</div> |
|||
<tb-unit-input class="flex-1" |
|||
formControlName="METRIC" |
|||
[unitSystem]="UnitSystem.METRIC" |
|||
[measure]="measure"> |
|||
</tb-unit-input> |
|||
</div> |
|||
<div class="tb-form-row"> |
|||
<div class="min-w-25">Imperial</div> |
|||
<tb-unit-input class="flex-1" |
|||
formControlName="IMPERIAL" |
|||
[unitSystem]="UnitSystem.IMPERIAL" |
|||
[measure]="measure"> |
|||
</tb-unit-input> |
|||
</div> |
|||
<div class="tb-form-row"> |
|||
<div class="min-w-25">Hybrid</div> |
|||
<tb-unit-input class="flex-1" |
|||
formControlName="HYBRID" |
|||
[measure]="measure"> |
|||
</tb-unit-input> |
|||
</div> |
|||
} |
|||
</div> |
|||
<div class="tb-convert-settings-panel-buttons"> |
|||
<button mat-button |
|||
color="primary" |
|||
type="button" |
|||
(click)="cancel()"> |
|||
{{ 'action.cancel' | translate }} |
|||
</button> |
|||
<button mat-raised-button |
|||
color="primary" |
|||
type="button" |
|||
(click)="applyUnitSettings()" |
|||
[disabled]="convertUnitForm.invalid || convertUnitForm.pristine"> |
|||
{{ 'action.apply' | translate }} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,49 @@ |
|||
/** |
|||
* Copyright © 2016-2025 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
@import '../scss/constants'; |
|||
|
|||
.tb-convert-settings-panel { |
|||
width: 320px; |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 16px; |
|||
@media #{$mat-lt-md} { |
|||
width: 90vw; |
|||
} |
|||
.tb-convert-settings-title { |
|||
font-size: 16px; |
|||
font-weight: 500; |
|||
line-height: 24px; |
|||
letter-spacing: 0.25px; |
|||
color: rgba(0, 0, 0, 0.87); |
|||
} |
|||
.tb-convert-settings-panel-content { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 16px; |
|||
overflow: auto; |
|||
margin: -10px; |
|||
padding: 10px; |
|||
} |
|||
.tb-convert-settings-panel-buttons { |
|||
height: 40px; |
|||
display: flex; |
|||
flex-direction: row; |
|||
gap: 16px; |
|||
justify-content: flex-end; |
|||
align-items: flex-end; |
|||
} |
|||
} |
|||
@ -0,0 +1,138 @@ |
|||
///
|
|||
/// Copyright © 2016-2025 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; |
|||
import { TbUnit, UnitDescription, UnitSystem } from '@shared/models/unit.models'; |
|||
import { TbPopoverComponent } from '@shared/components/popover.component'; |
|||
import { FormBuilder, Validators } from '@angular/forms'; |
|||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
|||
import { UnitService } from '@core/services/unit/unit.service'; |
|||
import { AllMeasures } from '@core/services/unit/definitions/all'; |
|||
import { debounceTime } from 'rxjs/operators'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-covert-unit-settings-panel', |
|||
templateUrl: './convert-unit-settings-panel.component.html', |
|||
styleUrls: ['./convert-unit-settings-panel.component.scss'], |
|||
providers: [], |
|||
encapsulation: ViewEncapsulation.None |
|||
}) |
|||
export class ConvertUnitSettingsPanelComponent implements OnInit { |
|||
|
|||
@Input() |
|||
unit: TbUnit; |
|||
|
|||
@Input() |
|||
required: boolean; |
|||
|
|||
@Output() |
|||
unitSettingsApplied = new EventEmitter<TbUnit>(); |
|||
|
|||
UnitSystem = UnitSystem; |
|||
|
|||
measure: AllMeasures; |
|||
|
|||
convertUnitForm = this.fb.group({ |
|||
from: [''], |
|||
convertUnit: [true], |
|||
METRIC: [''], |
|||
IMPERIAL: [''], |
|||
HYBRID: [''] |
|||
}) |
|||
|
|||
constructor( |
|||
private popover: TbPopoverComponent<ConvertUnitSettingsPanelComponent>, |
|||
private fb: FormBuilder, |
|||
private unitService: UnitService |
|||
) { |
|||
this.convertUnitForm.get('from').valueChanges.pipe( |
|||
debounceTime(200), |
|||
takeUntilDestroyed() |
|||
).subscribe(unit => { |
|||
const unitDescription = this.unitService.getUnitDescription(unit); |
|||
if (unitDescription) { |
|||
this.convertUnitForm.get('convertUnit').enable({emitEvent: true}); |
|||
this.measure = unitDescription.measure; |
|||
if (unitDescription.system === UnitSystem.IMPERIAL) { |
|||
this.convertUnitForm.get('IMPERIAL').setValue(unit, {emitEvent: false}); |
|||
this.convertUnitForm.get('HYBRID').setValue(unit, {emitEvent: false}); |
|||
} else { |
|||
this.convertUnitForm.get('METRIC').setValue(unit, {emitEvent: false}); |
|||
this.convertUnitForm.get('HYBRID').setValue(unit, {emitEvent: false}); |
|||
} |
|||
} else { |
|||
this.convertUnitForm.get('convertUnit').setValue(false, {onlySelf: true}); |
|||
this.convertUnitForm.get('convertUnit').disable({emitEvent: false}); |
|||
} |
|||
}) |
|||
|
|||
this.convertUnitForm.get('convertUnit').valueChanges.pipe( |
|||
takeUntilDestroyed() |
|||
).subscribe(value => { |
|||
if (value) { |
|||
this.convertUnitForm.get('METRIC').enable({emitEvent: false}); |
|||
this.convertUnitForm.get('IMPERIAL').enable({emitEvent: false}); |
|||
this.convertUnitForm.get('HYBRID').enable({emitEvent: false}); |
|||
} else { |
|||
this.convertUnitForm.get('METRIC').disable({emitEvent: false}); |
|||
this.convertUnitForm.get('IMPERIAL').disable({emitEvent: false}); |
|||
this.convertUnitForm.get('HYBRID').disable({emitEvent: false}); |
|||
} |
|||
setTimeout(() => { |
|||
this.popover.updatePosition(); |
|||
}, 0); |
|||
}); |
|||
} |
|||
|
|||
ngOnInit() { |
|||
let unitDescription: UnitDescription; |
|||
if (this.required) { |
|||
this.convertUnitForm.get('from').setValidators(Validators.required); |
|||
this.convertUnitForm.get('from').updateValueAndValidity({emitEvent: false}); |
|||
} |
|||
if (typeof this.unit === 'string') { |
|||
this.convertUnitForm.get('convertUnit').setValue(false, {onlySelf: true}); |
|||
this.convertUnitForm.get('from').setValue(this.unit, {emitEvent: true}); |
|||
unitDescription = this.unitService.getUnitDescription(this.unit); |
|||
} else if (this.unit === null) { |
|||
this.convertUnitForm.get('convertUnit').setValue(false, {onlySelf: true}); |
|||
this.convertUnitForm.get('from').setValue(null, {emitEvent: true}); |
|||
} else { |
|||
this.convertUnitForm.patchValue(this.unit, {emitEvent: false}); |
|||
unitDescription = this.unitService.getUnitDescription(this.unit.from); |
|||
} |
|||
|
|||
if (unitDescription?.measure) { |
|||
this.measure = unitDescription.measure; |
|||
} else { |
|||
this.convertUnitForm.get('convertUnit').disable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
cancel() { |
|||
this.popover.hide(); |
|||
} |
|||
|
|||
applyUnitSettings() { |
|||
if (this.convertUnitForm.value.convertUnit) { |
|||
const formValue = this.convertUnitForm.value; |
|||
delete formValue.convertUnit; |
|||
this.unitSettingsApplied.emit(formValue as TbUnit); |
|||
} else { |
|||
this.unitSettingsApplied.emit(this.convertUnitForm.value.from); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue