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