committed by
GitHub
154 changed files with 5279 additions and 1794 deletions
@ -0,0 +1,49 @@ |
|||
/** |
|||
* Copyright © 2016-2017 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. |
|||
*/ |
|||
package org.thingsboard.server.actors.plugin; |
|||
|
|||
import com.hazelcast.util.function.Consumer; |
|||
import org.thingsboard.server.extensions.api.exception.UnauthorizedException; |
|||
import org.thingsboard.server.extensions.api.plugins.PluginCallback; |
|||
import org.thingsboard.server.extensions.api.plugins.PluginContext; |
|||
|
|||
/** |
|||
* Created by ashvayka on 21.02.17. |
|||
*/ |
|||
public class ValidationCallback implements PluginCallback<Boolean> { |
|||
|
|||
private final PluginCallback<?> callback; |
|||
private final Consumer<PluginContext> action; |
|||
|
|||
public ValidationCallback(PluginCallback<?> callback, Consumer<PluginContext> action) { |
|||
this.callback = callback; |
|||
this.action = action; |
|||
} |
|||
|
|||
@Override |
|||
public void onSuccess(PluginContext ctx, Boolean value) { |
|||
if (value) { |
|||
action.accept(ctx); |
|||
} else { |
|||
onFailure(ctx, new UnauthorizedException()); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void onFailure(PluginContext ctx, Exception e) { |
|||
callback.onFailure(ctx, e); |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
/** |
|||
* Copyright © 2016-2017 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. |
|||
*/ |
|||
package org.thingsboard.server.common.data.kv; |
|||
|
|||
/** |
|||
* Created by ashvayka on 20.02.17. |
|||
*/ |
|||
public enum Aggregation { |
|||
|
|||
MIN, MAX, AVG, SUM, COUNT, NONE; |
|||
|
|||
} |
|||
@ -0,0 +1,42 @@ |
|||
/** |
|||
* Copyright © 2016-2017 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. |
|||
*/ |
|||
package org.thingsboard.server.dao; |
|||
|
|||
import javax.annotation.PostConstruct; |
|||
import javax.annotation.PreDestroy; |
|||
import java.util.concurrent.ExecutorService; |
|||
import java.util.concurrent.Executors; |
|||
|
|||
/** |
|||
* Created by ashvayka on 21.02.17. |
|||
*/ |
|||
public abstract class AbstractAsyncDao extends AbstractDao { |
|||
|
|||
protected ExecutorService readResultsProcessingExecutor; |
|||
|
|||
@PostConstruct |
|||
public void startExecutor() { |
|||
readResultsProcessingExecutor = Executors.newCachedThreadPool(); |
|||
} |
|||
|
|||
@PreDestroy |
|||
public void stopExecutor() { |
|||
if (readResultsProcessingExecutor != null) { |
|||
readResultsProcessingExecutor.shutdownNow(); |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,193 @@ |
|||
/** |
|||
* Copyright © 2016-2017 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. |
|||
*/ |
|||
package org.thingsboard.server.dao.timeseries; |
|||
|
|||
import com.datastax.driver.core.ResultSet; |
|||
import com.datastax.driver.core.Row; |
|||
import org.thingsboard.server.common.data.kv.*; |
|||
|
|||
import javax.annotation.Nullable; |
|||
import java.util.List; |
|||
import java.util.Optional; |
|||
|
|||
/** |
|||
* Created by ashvayka on 20.02.17. |
|||
*/ |
|||
public class AggregatePartitionsFunction implements com.google.common.base.Function<List<ResultSet>, Optional<TsKvEntry>> { |
|||
|
|||
private static final int LONG_CNT_POS = 0; |
|||
private static final int DOUBLE_CNT_POS = 1; |
|||
private static final int BOOL_CNT_POS = 2; |
|||
private static final int STR_CNT_POS = 3; |
|||
private static final int LONG_POS = 4; |
|||
private static final int DOUBLE_POS = 5; |
|||
private static final int BOOL_POS = 6; |
|||
private static final int STR_POS = 7; |
|||
|
|||
private final Aggregation aggregation; |
|||
private final String key; |
|||
private final long ts; |
|||
|
|||
public AggregatePartitionsFunction(Aggregation aggregation, String key, long ts) { |
|||
this.aggregation = aggregation; |
|||
this.key = key; |
|||
this.ts = ts; |
|||
} |
|||
|
|||
@Nullable |
|||
@Override |
|||
public Optional<TsKvEntry> apply(@Nullable List<ResultSet> rsList) { |
|||
if (rsList == null || rsList.size() == 0) { |
|||
return Optional.empty(); |
|||
} |
|||
long count = 0; |
|||
DataType dataType = null; |
|||
|
|||
Boolean bValue = null; |
|||
String sValue = null; |
|||
Double dValue = null; |
|||
Long lValue = null; |
|||
|
|||
for (ResultSet rs : rsList) { |
|||
for (Row row : rs.all()) { |
|||
long curCount; |
|||
|
|||
Long curLValue = null; |
|||
Double curDValue = null; |
|||
Boolean curBValue = null; |
|||
String curSValue = null; |
|||
|
|||
long longCount = row.getLong(LONG_CNT_POS); |
|||
long doubleCount = row.getLong(DOUBLE_CNT_POS); |
|||
long boolCount = row.getLong(BOOL_CNT_POS); |
|||
long strCount = row.getLong(STR_CNT_POS); |
|||
|
|||
if (longCount > 0) { |
|||
dataType = DataType.LONG; |
|||
curCount = longCount; |
|||
curLValue = getLongValue(row); |
|||
} else if (doubleCount > 0) { |
|||
dataType = DataType.DOUBLE; |
|||
curCount = doubleCount; |
|||
curDValue = getDoubleValue(row); |
|||
} else if (boolCount > 0) { |
|||
dataType = DataType.BOOLEAN; |
|||
curCount = boolCount; |
|||
curBValue = getBooleanValue(row); |
|||
} else if (strCount > 0) { |
|||
dataType = DataType.STRING; |
|||
curCount = strCount; |
|||
curSValue = getStringValue(row); |
|||
} else { |
|||
continue; |
|||
} |
|||
|
|||
if (aggregation == Aggregation.COUNT) { |
|||
count += curCount; |
|||
} else if (aggregation == Aggregation.AVG || aggregation == Aggregation.SUM) { |
|||
count += curCount; |
|||
if (curDValue != null) { |
|||
dValue = dValue == null ? curDValue : dValue + curDValue; |
|||
} else if (curLValue != null) { |
|||
lValue = lValue == null ? curLValue : lValue + curLValue; |
|||
} |
|||
} else if (aggregation == Aggregation.MIN) { |
|||
if (curDValue != null) { |
|||
dValue = dValue == null ? curDValue : Math.min(dValue, curDValue); |
|||
} else if (curLValue != null) { |
|||
lValue = lValue == null ? curLValue : Math.min(lValue, curLValue); |
|||
} else if (curBValue != null) { |
|||
bValue = bValue == null ? curBValue : bValue && curBValue; |
|||
} else if (curSValue != null) { |
|||
if (sValue == null || curSValue.compareTo(sValue) < 0) { |
|||
sValue = curSValue; |
|||
} |
|||
} |
|||
} else if (aggregation == Aggregation.MAX) { |
|||
if (curDValue != null) { |
|||
dValue = dValue == null ? curDValue : Math.max(dValue, curDValue); |
|||
} else if (curLValue != null) { |
|||
lValue = lValue == null ? curLValue : Math.max(lValue, curLValue); |
|||
} else if (curBValue != null) { |
|||
bValue = bValue == null ? curBValue : bValue || curBValue; |
|||
} else if (curSValue != null) { |
|||
if (sValue == null || curSValue.compareTo(sValue) > 0) { |
|||
sValue = curSValue; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
if (dataType == null) { |
|||
return Optional.empty(); |
|||
} else if (aggregation == Aggregation.COUNT) { |
|||
return Optional.of(new BasicTsKvEntry(ts, new LongDataEntry(key, (long) count))); |
|||
} else if (aggregation == Aggregation.AVG || aggregation == Aggregation.SUM) { |
|||
if (count == 0 || (dataType == DataType.DOUBLE && dValue == null) || (dataType == DataType.LONG && lValue == null)) { |
|||
return Optional.empty(); |
|||
} else if (dataType == DataType.DOUBLE) { |
|||
return Optional.of(new BasicTsKvEntry(ts, new DoubleDataEntry(key, aggregation == Aggregation.SUM ? dValue : (dValue / count)))); |
|||
} else if (dataType == DataType.LONG) { |
|||
return Optional.of(new BasicTsKvEntry(ts, new LongDataEntry(key, aggregation == Aggregation.SUM ? lValue : (lValue / count)))); |
|||
} |
|||
} else if (aggregation == Aggregation.MIN || aggregation == Aggregation.MAX) { |
|||
if (dataType == DataType.DOUBLE) { |
|||
return Optional.of(new BasicTsKvEntry(ts, new DoubleDataEntry(key, dValue))); |
|||
} else if (dataType == DataType.LONG) { |
|||
return Optional.of(new BasicTsKvEntry(ts, new LongDataEntry(key, lValue))); |
|||
} else if (dataType == DataType.STRING) { |
|||
return Optional.of(new BasicTsKvEntry(ts, new StringDataEntry(key, sValue))); |
|||
} else { |
|||
return Optional.of(new BasicTsKvEntry(ts, new BooleanDataEntry(key, bValue))); |
|||
} |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
private Boolean getBooleanValue(Row row) { |
|||
if (aggregation == Aggregation.MIN || aggregation == Aggregation.MAX) { |
|||
return row.getBool(BOOL_POS); |
|||
} else { |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
private String getStringValue(Row row) { |
|||
if (aggregation == Aggregation.MIN || aggregation == Aggregation.MAX) { |
|||
return row.getString(STR_POS); |
|||
} else { |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
private Long getLongValue(Row row) { |
|||
if (aggregation == Aggregation.MIN || aggregation == Aggregation.MAX |
|||
|| aggregation == Aggregation.SUM || aggregation == Aggregation.AVG) { |
|||
return row.getLong(LONG_POS); |
|||
} else { |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
private Double getDoubleValue(Row row) { |
|||
if (aggregation == Aggregation.MIN || aggregation == Aggregation.MAX |
|||
|| aggregation == Aggregation.SUM || aggregation == Aggregation.AVG) { |
|||
return row.getDouble(DOUBLE_POS); |
|||
} else { |
|||
return null; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
/** |
|||
* Copyright © 2016-2017 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. |
|||
*/ |
|||
package org.thingsboard.server.dao.timeseries; |
|||
|
|||
import com.google.common.util.concurrent.AbstractFuture; |
|||
|
|||
/** |
|||
* Created by ashvayka on 21.02.17. |
|||
*/ |
|||
public class SimpleListenableFuture<V> extends AbstractFuture<V> { |
|||
|
|||
public boolean set(V value) { |
|||
return super.set(value); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,82 @@ |
|||
/** |
|||
* Copyright © 2016-2017 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. |
|||
*/ |
|||
package org.thingsboard.server.dao.timeseries; |
|||
|
|||
import lombok.Data; |
|||
import lombok.Getter; |
|||
import org.thingsboard.server.common.data.kv.TsKvEntry; |
|||
import org.thingsboard.server.common.data.kv.TsKvQuery; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
import java.util.UUID; |
|||
|
|||
/** |
|||
* Created by ashvayka on 21.02.17. |
|||
*/ |
|||
public class TsKvQueryCursor { |
|||
@Getter |
|||
private final String entityType; |
|||
@Getter |
|||
private final UUID entityId; |
|||
@Getter |
|||
private final String key; |
|||
@Getter |
|||
private final long startTs; |
|||
@Getter |
|||
private final long endTs; |
|||
private final List<Long> partitions; |
|||
@Getter |
|||
private final List<TsKvEntry> data; |
|||
|
|||
private int partitionIndex; |
|||
private int currentLimit; |
|||
|
|||
public TsKvQueryCursor(String entityType, UUID entityId, TsKvQuery baseQuery, List<Long> partitions) { |
|||
this.entityType = entityType; |
|||
this.entityId = entityId; |
|||
this.key = baseQuery.getKey(); |
|||
this.startTs = baseQuery.getStartTs(); |
|||
this.endTs = baseQuery.getEndTs(); |
|||
this.partitions = partitions; |
|||
this.partitionIndex = partitions.size() - 1; |
|||
this.data = new ArrayList<>(); |
|||
this.currentLimit = baseQuery.getLimit(); |
|||
} |
|||
|
|||
public boolean hasNextPartition() { |
|||
return partitionIndex >= 0; |
|||
} |
|||
|
|||
public boolean isFull() { |
|||
return currentLimit <= 0; |
|||
} |
|||
|
|||
public long getNextPartition() { |
|||
long partition = partitions.get(partitionIndex); |
|||
partitionIndex--; |
|||
return partition; |
|||
} |
|||
|
|||
public int getCurrentLimit() { |
|||
return currentLimit; |
|||
} |
|||
|
|||
public void addData(List<TsKvEntry> newData) { |
|||
currentLimit -= newData.size(); |
|||
data.addAll(newData); |
|||
} |
|||
} |
|||
File diff suppressed because one or more lines are too long
@ -0,0 +1,22 @@ |
|||
/** |
|||
* Copyright © 2016-2017 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. |
|||
*/ |
|||
package org.thingsboard.server.extensions.api.exception; |
|||
|
|||
/** |
|||
* Created by ashvayka on 21.02.17. |
|||
*/ |
|||
public class UnauthorizedException extends Exception { |
|||
} |
|||
@ -0,0 +1,74 @@ |
|||
/** |
|||
* Copyright © 2016-2017 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. |
|||
*/ |
|||
package org.thingsboard.server.extensions.core.plugin.telemetry.handlers; |
|||
|
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.thingsboard.server.extensions.api.plugins.PluginCallback; |
|||
import org.thingsboard.server.extensions.api.plugins.PluginContext; |
|||
|
|||
/** |
|||
* Created by ashvayka on 21.02.17. |
|||
*/ |
|||
@Slf4j |
|||
public abstract class BiPluginCallBack<V1, V2> { |
|||
|
|||
private V1 v1; |
|||
private V2 v2; |
|||
|
|||
public PluginCallback<V1> getV1Callback() { |
|||
return new PluginCallback<V1>() { |
|||
@Override |
|||
public void onSuccess(PluginContext ctx, V1 value) { |
|||
synchronized (BiPluginCallBack.this) { |
|||
BiPluginCallBack.this.v1 = value; |
|||
if (v2 != null) { |
|||
BiPluginCallBack.this.onSuccess(ctx, v1, v2); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void onFailure(PluginContext ctx, Exception e) { |
|||
BiPluginCallBack.this.onFailure(ctx, e); |
|||
} |
|||
}; |
|||
} |
|||
|
|||
public PluginCallback<V2> getV2Callback() { |
|||
return new PluginCallback<V2>() { |
|||
@Override |
|||
public void onSuccess(PluginContext ctx, V2 value) { |
|||
synchronized (BiPluginCallBack.this) { |
|||
BiPluginCallBack.this.v2 = value; |
|||
if (v1 != null) { |
|||
BiPluginCallBack.this.onSuccess(ctx, v1, v2); |
|||
} |
|||
} |
|||
|
|||
} |
|||
|
|||
@Override |
|||
public void onFailure(PluginContext ctx, Exception e) { |
|||
BiPluginCallBack.this.onFailure(ctx, e); |
|||
} |
|||
}; |
|||
} |
|||
|
|||
abstract public void onSuccess(PluginContext ctx, V1 v1, V2 v2); |
|||
|
|||
abstract public void onFailure(PluginContext ctx, Exception e); |
|||
|
|||
} |
|||
@ -0,0 +1,274 @@ |
|||
/* |
|||
* Copyright © 2016-2017 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. |
|||
*/ |
|||
|
|||
export default class DataAggregator { |
|||
|
|||
constructor(onDataCb, tsKeyNames, startTs, limit, aggregationType, timeWindow, interval, types, $timeout, $filter) { |
|||
this.onDataCb = onDataCb; |
|||
this.tsKeyNames = tsKeyNames; |
|||
this.startTs = startTs; |
|||
this.aggregationType = aggregationType; |
|||
this.types = types; |
|||
this.$timeout = $timeout; |
|||
this.$filter = $filter; |
|||
this.dataReceived = false; |
|||
this.resetPending = false; |
|||
this.noAggregation = aggregationType === types.aggregation.none.value; |
|||
this.limit = limit; |
|||
this.timeWindow = timeWindow; |
|||
this.interval = interval; |
|||
this.aggregationTimeout = Math.max(this.interval, 1000); |
|||
switch (aggregationType) { |
|||
case types.aggregation.min.value: |
|||
this.aggFunction = min; |
|||
break; |
|||
case types.aggregation.max.value: |
|||
this.aggFunction = max; |
|||
break; |
|||
case types.aggregation.avg.value: |
|||
this.aggFunction = avg; |
|||
break; |
|||
case types.aggregation.sum.value: |
|||
this.aggFunction = sum; |
|||
break; |
|||
case types.aggregation.count.value: |
|||
this.aggFunction = count; |
|||
break; |
|||
case types.aggregation.none.value: |
|||
this.aggFunction = none; |
|||
break; |
|||
default: |
|||
this.aggFunction = avg; |
|||
} |
|||
} |
|||
|
|||
reset(startTs, timeWindow, interval) { |
|||
if (this.intervalTimeoutHandle) { |
|||
this.$timeout.cancel(this.intervalTimeoutHandle); |
|||
this.intervalTimeoutHandle = null; |
|||
} |
|||
this.intervalScheduledTime = currentTime(); |
|||
this.startTs = startTs; |
|||
this.timeWindow = timeWindow; |
|||
this.interval = interval; |
|||
this.endTs = this.startTs + this.timeWindow; |
|||
this.elapsed = 0; |
|||
this.aggregationTimeout = Math.max(this.interval, 1000); |
|||
this.resetPending = true; |
|||
var self = this; |
|||
this.intervalTimeoutHandle = this.$timeout(function() { |
|||
self.onInterval(); |
|||
}, this.aggregationTimeout, false); |
|||
} |
|||
|
|||
onData(data, update, history, apply) { |
|||
if (!this.dataReceived || this.resetPending) { |
|||
var updateIntervalScheduledTime = true; |
|||
if (!this.dataReceived) { |
|||
this.elapsed = 0; |
|||
this.dataReceived = true; |
|||
this.endTs = this.startTs + this.timeWindow; |
|||
} |
|||
if (this.resetPending) { |
|||
this.resetPending = false; |
|||
updateIntervalScheduledTime = false; |
|||
} |
|||
if (update) { |
|||
this.aggregationMap = {}; |
|||
updateAggregatedData(this.aggregationMap, this.aggregationType === this.types.aggregation.count.value, |
|||
this.noAggregation, this.aggFunction, data.data, this.interval, this.startTs); |
|||
} else { |
|||
this.aggregationMap = processAggregatedData(data.data, this.aggregationType === this.types.aggregation.count.value, this.noAggregation); |
|||
} |
|||
if (updateIntervalScheduledTime) { |
|||
this.intervalScheduledTime = currentTime(); |
|||
} |
|||
this.onInterval(history, apply); |
|||
} else { |
|||
updateAggregatedData(this.aggregationMap, this.aggregationType === this.types.aggregation.count.value, |
|||
this.noAggregation, this.aggFunction, data.data, this.interval, this.startTs); |
|||
if (history) { |
|||
this.intervalScheduledTime = currentTime(); |
|||
this.onInterval(history, apply); |
|||
} |
|||
} |
|||
} |
|||
|
|||
onInterval(history, apply) { |
|||
var now = currentTime(); |
|||
this.elapsed += now - this.intervalScheduledTime; |
|||
this.intervalScheduledTime = now; |
|||
if (this.intervalTimeoutHandle) { |
|||
this.$timeout.cancel(this.intervalTimeoutHandle); |
|||
this.intervalTimeoutHandle = null; |
|||
} |
|||
if (!history) { |
|||
var delta = Math.floor(this.elapsed / this.interval); |
|||
if (delta || !this.data) { |
|||
this.startTs += delta * this.interval; |
|||
this.endTs += delta * this.interval; |
|||
this.data = toData(this.tsKeyNames, this.aggregationMap, this.startTs, this.endTs, this.$filter, this.limit); |
|||
this.elapsed = this.elapsed - delta * this.interval; |
|||
} |
|||
} else { |
|||
this.data = toData(this.tsKeyNames, this.aggregationMap, this.startTs, this.endTs, this.$filter, this.limit); |
|||
} |
|||
if (this.onDataCb) { |
|||
this.onDataCb(this.data, this.startTs, this.endTs, apply); |
|||
} |
|||
|
|||
var self = this; |
|||
if (!history) { |
|||
this.intervalTimeoutHandle = this.$timeout(function() { |
|||
self.onInterval(); |
|||
}, this.aggregationTimeout, false); |
|||
} |
|||
} |
|||
|
|||
destroy() { |
|||
if (this.intervalTimeoutHandle) { |
|||
this.$timeout.cancel(this.intervalTimeoutHandle); |
|||
this.intervalTimeoutHandle = null; |
|||
} |
|||
this.aggregationMap = null; |
|||
} |
|||
|
|||
} |
|||
|
|||
/* eslint-disable */ |
|||
function currentTime() { |
|||
return window.performance && window.performance.now ? |
|||
window.performance.now() : Date.now(); |
|||
} |
|||
/* eslint-enable */ |
|||
|
|||
function processAggregatedData(data, isCount, noAggregation) { |
|||
var aggregationMap = {}; |
|||
for (var key in data) { |
|||
var aggKeyData = aggregationMap[key]; |
|||
if (!aggKeyData) { |
|||
aggKeyData = {}; |
|||
aggregationMap[key] = aggKeyData; |
|||
} |
|||
var keyData = data[key]; |
|||
for (var i in keyData) { |
|||
var kvPair = keyData[i]; |
|||
var timestamp = kvPair[0]; |
|||
var value = convertValue(kvPair[1], noAggregation); |
|||
var aggKey = timestamp; |
|||
var aggData = { |
|||
count: isCount ? value : 1, |
|||
sum: value, |
|||
aggValue: value |
|||
} |
|||
aggKeyData[aggKey] = aggData; |
|||
} |
|||
} |
|||
return aggregationMap; |
|||
} |
|||
|
|||
function updateAggregatedData(aggregationMap, isCount, noAggregation, aggFunction, data, interval, startTs) { |
|||
for (var key in data) { |
|||
var aggKeyData = aggregationMap[key]; |
|||
if (!aggKeyData) { |
|||
aggKeyData = {}; |
|||
aggregationMap[key] = aggKeyData; |
|||
} |
|||
var keyData = data[key]; |
|||
for (var i in keyData) { |
|||
var kvPair = keyData[i]; |
|||
var timestamp = kvPair[0]; |
|||
var value = convertValue(kvPair[1], noAggregation); |
|||
var aggTimestamp = noAggregation ? timestamp : (startTs + Math.floor((timestamp - startTs) / interval) * interval + interval/2); |
|||
var aggData = aggKeyData[aggTimestamp]; |
|||
if (!aggData) { |
|||
aggData = { |
|||
count: 1, |
|||
sum: value, |
|||
aggValue: isCount ? 1 : value |
|||
} |
|||
aggKeyData[aggTimestamp] = aggData; |
|||
} else { |
|||
aggFunction(aggData, value); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
function toData(tsKeyNames, aggregationMap, startTs, endTs, $filter, limit) { |
|||
var data = {}; |
|||
for (var k in tsKeyNames) { |
|||
data[tsKeyNames[k]] = []; |
|||
} |
|||
for (var key in aggregationMap) { |
|||
var aggKeyData = aggregationMap[key]; |
|||
var keyData = data[key]; |
|||
for (var aggTimestamp in aggKeyData) { |
|||
if (aggTimestamp <= startTs) { |
|||
delete aggKeyData[aggTimestamp]; |
|||
} else if (aggTimestamp <= endTs) { |
|||
var aggData = aggKeyData[aggTimestamp]; |
|||
var kvPair = [Number(aggTimestamp), aggData.aggValue]; |
|||
keyData.push(kvPair); |
|||
} |
|||
} |
|||
keyData = $filter('orderBy')(keyData, '+this[0]'); |
|||
if (keyData.length > limit) { |
|||
keyData = keyData.slice(keyData.length - limit); |
|||
} |
|||
data[key] = keyData; |
|||
} |
|||
return data; |
|||
} |
|||
|
|||
function convertValue(value, noAggregation) { |
|||
if (!noAggregation || value && isNumeric(value)) { |
|||
return Number(value); |
|||
} else { |
|||
return value; |
|||
} |
|||
} |
|||
|
|||
function isNumeric(value) { |
|||
return (value - parseFloat( value ) + 1) >= 0; |
|||
} |
|||
|
|||
function avg(aggData, value) { |
|||
aggData.count++; |
|||
aggData.sum += value; |
|||
aggData.aggValue = aggData.sum / aggData.count; |
|||
} |
|||
|
|||
function min(aggData, value) { |
|||
aggData.aggValue = Math.min(aggData.aggValue, value); |
|||
} |
|||
|
|||
function max(aggData, value) { |
|||
aggData.aggValue = Math.max(aggData.aggValue, value); |
|||
} |
|||
|
|||
function sum(aggData, value) { |
|||
aggData.aggValue = aggData.aggValue + value; |
|||
} |
|||
|
|||
function count(aggData) { |
|||
aggData.count++; |
|||
aggData.aggValue = aggData.count; |
|||
} |
|||
|
|||
function none(aggData, value) { |
|||
aggData.aggValue = value; |
|||
} |
|||
@ -0,0 +1,330 @@ |
|||
/* |
|||
* Copyright © 2016-2017 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. |
|||
*/ |
|||
export default angular.module('thingsboard.api.time', []) |
|||
.factory('timeService', TimeService) |
|||
.name; |
|||
|
|||
const SECOND = 1000; |
|||
const MINUTE = 60 * SECOND; |
|||
const HOUR = 60 * MINUTE; |
|||
const DAY = 24 * HOUR; |
|||
|
|||
const MIN_INTERVAL = SECOND; |
|||
const MAX_INTERVAL = 365 * 20 * DAY; |
|||
|
|||
const MIN_LIMIT = 10; |
|||
const AVG_LIMIT = 200; |
|||
const MAX_LIMIT = 500; |
|||
|
|||
/*@ngInject*/ |
|||
function TimeService($translate, types) { |
|||
|
|||
var predefIntervals = [ |
|||
{ |
|||
name: $translate.instant('timeinterval.seconds-interval', {seconds: 1}, 'messageformat'), |
|||
value: 1 * SECOND |
|||
}, |
|||
{ |
|||
name: $translate.instant('timeinterval.seconds-interval', {seconds: 5}, 'messageformat'), |
|||
value: 5 * SECOND |
|||
}, |
|||
{ |
|||
name: $translate.instant('timeinterval.seconds-interval', {seconds: 10}, 'messageformat'), |
|||
value: 10 * SECOND |
|||
}, |
|||
{ |
|||
name: $translate.instant('timeinterval.seconds-interval', {seconds: 15}, 'messageformat'), |
|||
value: 15 * SECOND |
|||
}, |
|||
{ |
|||
name: $translate.instant('timeinterval.seconds-interval', {seconds: 30}, 'messageformat'), |
|||
value: 30 * SECOND |
|||
}, |
|||
{ |
|||
name: $translate.instant('timeinterval.minutes-interval', {minutes: 1}, 'messageformat'), |
|||
value: 1 * MINUTE |
|||
}, |
|||
{ |
|||
name: $translate.instant('timeinterval.minutes-interval', {minutes: 2}, 'messageformat'), |
|||
value: 2 * MINUTE |
|||
}, |
|||
{ |
|||
name: $translate.instant('timeinterval.minutes-interval', {minutes: 5}, 'messageformat'), |
|||
value: 5 * MINUTE |
|||
}, |
|||
{ |
|||
name: $translate.instant('timeinterval.minutes-interval', {minutes: 10}, 'messageformat'), |
|||
value: 10 * MINUTE |
|||
}, |
|||
{ |
|||
name: $translate.instant('timeinterval.minutes-interval', {minutes: 15}, 'messageformat'), |
|||
value: 15 * MINUTE |
|||
}, |
|||
{ |
|||
name: $translate.instant('timeinterval.minutes-interval', {minutes: 30}, 'messageformat'), |
|||
value: 30 * MINUTE |
|||
}, |
|||
{ |
|||
name: $translate.instant('timeinterval.hours-interval', {hours: 1}, 'messageformat'), |
|||
value: 1 * HOUR |
|||
}, |
|||
{ |
|||
name: $translate.instant('timeinterval.hours-interval', {hours: 2}, 'messageformat'), |
|||
value: 2 * HOUR |
|||
}, |
|||
{ |
|||
name: $translate.instant('timeinterval.hours-interval', {hours: 5}, 'messageformat'), |
|||
value: 5 * HOUR |
|||
}, |
|||
{ |
|||
name: $translate.instant('timeinterval.hours-interval', {hours: 10}, 'messageformat'), |
|||
value: 10 * HOUR |
|||
}, |
|||
{ |
|||
name: $translate.instant('timeinterval.hours-interval', {hours: 12}, 'messageformat'), |
|||
value: 12 * HOUR |
|||
}, |
|||
{ |
|||
name: $translate.instant('timeinterval.days-interval', {days: 1}, 'messageformat'), |
|||
value: 1 * DAY |
|||
}, |
|||
{ |
|||
name: $translate.instant('timeinterval.days-interval', {days: 7}, 'messageformat'), |
|||
value: 7 * DAY |
|||
}, |
|||
{ |
|||
name: $translate.instant('timeinterval.days-interval', {days: 30}, 'messageformat'), |
|||
value: 30 * DAY |
|||
} |
|||
]; |
|||
|
|||
var service = { |
|||
minIntervalLimit: minIntervalLimit, |
|||
maxIntervalLimit: maxIntervalLimit, |
|||
boundMinInterval: boundMinInterval, |
|||
boundMaxInterval: boundMaxInterval, |
|||
getIntervals: getIntervals, |
|||
matchesExistingInterval: matchesExistingInterval, |
|||
boundToPredefinedInterval: boundToPredefinedInterval, |
|||
defaultTimewindow: defaultTimewindow, |
|||
toHistoryTimewindow: toHistoryTimewindow, |
|||
createSubscriptionTimewindow: createSubscriptionTimewindow, |
|||
avgAggregationLimit: function () { |
|||
return AVG_LIMIT; |
|||
} |
|||
} |
|||
|
|||
return service; |
|||
|
|||
function minIntervalLimit(timewindow) { |
|||
var min = timewindow / MAX_LIMIT; |
|||
return boundMinInterval(min); |
|||
} |
|||
|
|||
function avgInterval(timewindow) { |
|||
var avg = timewindow / AVG_LIMIT; |
|||
return boundMinInterval(avg); |
|||
} |
|||
|
|||
function maxIntervalLimit(timewindow) { |
|||
var max = timewindow / MIN_LIMIT; |
|||
return boundMaxInterval(max); |
|||
} |
|||
|
|||
function boundMinInterval(min) { |
|||
return toBound(min, MIN_INTERVAL, MAX_INTERVAL, MIN_INTERVAL); |
|||
} |
|||
|
|||
function boundMaxInterval(max) { |
|||
return toBound(max, MIN_INTERVAL, MAX_INTERVAL, MAX_INTERVAL); |
|||
} |
|||
|
|||
function toBound(value, min, max, defValue) { |
|||
if (angular.isDefined(value)) { |
|||
value = Math.max(value, min); |
|||
value = Math.min(value, max); |
|||
return value; |
|||
} else { |
|||
return defValue; |
|||
} |
|||
} |
|||
|
|||
function getIntervals(min, max) { |
|||
min = boundMinInterval(min); |
|||
max = boundMaxInterval(max); |
|||
var intervals = []; |
|||
for (var i in predefIntervals) { |
|||
var interval = predefIntervals[i]; |
|||
if (interval.value >= min && interval.value <= max) { |
|||
intervals.push(interval); |
|||
} |
|||
} |
|||
return intervals; |
|||
} |
|||
|
|||
function matchesExistingInterval(min, max, intervalMs) { |
|||
var intervals = getIntervals(min, max); |
|||
for (var i in intervals) { |
|||
var interval = intervals[i]; |
|||
if (intervalMs === interval.value) { |
|||
return true; |
|||
} |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
function boundToPredefinedInterval(min, max, intervalMs) { |
|||
var intervals = getIntervals(min, max); |
|||
var minDelta = MAX_INTERVAL; |
|||
var boundedInterval = intervalMs || min; |
|||
var matchedInterval; |
|||
for (var i in intervals) { |
|||
var interval = intervals[i]; |
|||
var delta = Math.abs(interval.value - boundedInterval); |
|||
if (delta < minDelta) { |
|||
matchedInterval = interval; |
|||
minDelta = delta; |
|||
} |
|||
} |
|||
boundedInterval = matchedInterval.value; |
|||
return boundedInterval; |
|||
} |
|||
|
|||
function defaultTimewindow() { |
|||
var currentTime = (new Date).getTime(); |
|||
var timewindow = { |
|||
displayValue: "", |
|||
selectedTab: 0, |
|||
realtime: { |
|||
interval: SECOND, |
|||
timewindowMs: MINUTE // 1 min by default
|
|||
}, |
|||
history: { |
|||
historyType: 0, |
|||
interval: SECOND, |
|||
timewindowMs: MINUTE, // 1 min by default
|
|||
fixedTimewindow: { |
|||
startTimeMs: currentTime - DAY, // 1 day by default
|
|||
endTimeMs: currentTime |
|||
} |
|||
}, |
|||
aggregation: { |
|||
type: types.aggregation.avg.value, |
|||
limit: AVG_LIMIT |
|||
} |
|||
} |
|||
return timewindow; |
|||
} |
|||
|
|||
function toHistoryTimewindow(timewindow, startTimeMs, endTimeMs) { |
|||
|
|||
var interval = 0; |
|||
if (timewindow.history) { |
|||
interval = timewindow.history.interval; |
|||
} else if (timewindow.realtime) { |
|||
interval = timewindow.realtime.interval; |
|||
} |
|||
|
|||
var historyTimewindow = { |
|||
history: { |
|||
fixedTimewindow: { |
|||
startTimeMs: startTimeMs, |
|||
endTimeMs: endTimeMs |
|||
}, |
|||
interval: boundIntervalToTimewindow(endTimeMs - startTimeMs, interval) |
|||
}, |
|||
aggregation: { |
|||
|
|||
} |
|||
} |
|||
if (timewindow.aggregation) { |
|||
historyTimewindow.aggregation.type = timewindow.aggregation.type || types.aggregation.avg.value; |
|||
} else { |
|||
historyTimewindow.aggregation.type = types.aggregation.avg.value; |
|||
} |
|||
|
|||
return historyTimewindow; |
|||
} |
|||
|
|||
function createSubscriptionTimewindow(timewindow, stDiff) { |
|||
|
|||
var subscriptionTimewindow = { |
|||
fixedWindow: null, |
|||
realtimeWindowMs: null, |
|||
aggregation: { |
|||
interval: SECOND, |
|||
limit: AVG_LIMIT, |
|||
type: types.aggregation.avg.value |
|||
} |
|||
}; |
|||
var aggTimewindow = 0; |
|||
|
|||
if (angular.isDefined(timewindow.aggregation)) { |
|||
subscriptionTimewindow.aggregation = { |
|||
type: timewindow.aggregation.type || types.aggregation.avg.value, |
|||
limit: timewindow.aggregation.limit || AVG_LIMIT |
|||
}; |
|||
} |
|||
if (angular.isDefined(timewindow.realtime)) { |
|||
subscriptionTimewindow.realtimeWindowMs = timewindow.realtime.timewindowMs; |
|||
subscriptionTimewindow.aggregation.interval = |
|||
boundIntervalToTimewindow(subscriptionTimewindow.realtimeWindowMs, timewindow.realtime.interval); |
|||
subscriptionTimewindow.startTs = (new Date).getTime() + stDiff - subscriptionTimewindow.realtimeWindowMs; |
|||
var startDiff = subscriptionTimewindow.startTs % subscriptionTimewindow.aggregation.interval; |
|||
aggTimewindow = subscriptionTimewindow.realtimeWindowMs; |
|||
if (startDiff) { |
|||
subscriptionTimewindow.startTs -= startDiff; |
|||
aggTimewindow += subscriptionTimewindow.aggregation.interval; |
|||
} |
|||
} else if (angular.isDefined(timewindow.history)) { |
|||
if (angular.isDefined(timewindow.history.timewindowMs)) { |
|||
var currentTime = (new Date).getTime(); |
|||
subscriptionTimewindow.fixedWindow = { |
|||
startTimeMs: currentTime - timewindow.history.timewindowMs, |
|||
endTimeMs: currentTime |
|||
} |
|||
aggTimewindow = timewindow.history.timewindowMs; |
|||
|
|||
} else { |
|||
subscriptionTimewindow.fixedWindow = { |
|||
startTimeMs: timewindow.history.fixedTimewindow.startTimeMs, |
|||
endTimeMs: timewindow.history.fixedTimewindow.endTimeMs |
|||
} |
|||
aggTimewindow = subscriptionTimewindow.fixedWindow.endTimeMs - subscriptionTimewindow.fixedWindow.startTimeMs; |
|||
} |
|||
subscriptionTimewindow.startTs = subscriptionTimewindow.fixedWindow.startTimeMs; |
|||
subscriptionTimewindow.aggregation.interval = boundIntervalToTimewindow(aggTimewindow, timewindow.history.interval); |
|||
} |
|||
var aggregation = subscriptionTimewindow.aggregation; |
|||
aggregation.timeWindow = aggTimewindow; |
|||
if (aggregation.type !== types.aggregation.none.value) { |
|||
aggregation.limit = Math.ceil(aggTimewindow / subscriptionTimewindow.aggregation.interval); |
|||
} |
|||
return subscriptionTimewindow; |
|||
} |
|||
|
|||
function boundIntervalToTimewindow(timewindow, intervalMs) { |
|||
var min = minIntervalLimit(timewindow); |
|||
var max = maxIntervalLimit(timewindow); |
|||
if (intervalMs) { |
|||
return toBound(intervalMs, min, max, intervalMs); |
|||
} else { |
|||
return boundToPredefinedInterval(min, max, avgInterval(timewindow)); |
|||
} |
|||
} |
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2017 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. |
|||
|
|||
--> |
|||
<md-button ng-disabled="disabled" class="md-raised md-primary" ng-click="openEditMode($event)"> |
|||
<ng-md-icon icon="toc"></ng-md-icon> |
|||
<span translate>legend.settings</span> |
|||
</md-button> |
|||
@ -0,0 +1,35 @@ |
|||
/* |
|||
* Copyright © 2016-2017 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. |
|||
*/ |
|||
|
|||
/*@ngInject*/ |
|||
export default function LegendConfigPanelController(mdPanelRef, $scope, types, legendConfig, onLegendConfigUpdate) { |
|||
|
|||
var vm = this; |
|||
vm._mdPanelRef = mdPanelRef; |
|||
vm.legendConfig = legendConfig; |
|||
vm.onLegendConfigUpdate = onLegendConfigUpdate; |
|||
vm.positions = types.position; |
|||
|
|||
vm._mdPanelRef.config.onOpenComplete = function () { |
|||
$scope.theForm.$setPristine(); |
|||
} |
|||
|
|||
$scope.$watch('vm.legendConfig', function () { |
|||
if (onLegendConfigUpdate) { |
|||
onLegendConfigUpdate(vm.legendConfig); |
|||
} |
|||
}, true); |
|||
} |
|||
@ -0,0 +1,47 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2017 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. |
|||
|
|||
--> |
|||
<form name="theForm" ng-submit="vm.update()"> |
|||
<fieldset ng-disabled="loading"> |
|||
<md-content style="height: 100%" flex layout="column"> |
|||
<section layout="column"> |
|||
<md-content class="md-padding" layout="column"> |
|||
<md-input-container> |
|||
<label translate>legend.position</label> |
|||
<md-select ng-model="vm.legendConfig.position" style="min-width: 150px;"> |
|||
<md-option ng-repeat="pos in vm.positions" ng-value="pos.value"> |
|||
{{pos.name | translate}} |
|||
</md-option> |
|||
</md-select> |
|||
</md-input-container> |
|||
<md-checkbox flex aria-label="{{ 'legend.show-min' | translate }}" |
|||
ng-model="vm.legendConfig.showMin">{{ 'legend.show-min' | translate }} |
|||
</md-checkbox> |
|||
<md-checkbox flex aria-label="{{ 'legend.show-max' | translate }}" |
|||
ng-model="vm.legendConfig.showMax">{{ 'legend.show-max' | translate }} |
|||
</md-checkbox> |
|||
<md-checkbox flex aria-label="{{ 'legend.show-avg' | translate }}" |
|||
ng-model="vm.legendConfig.showAvg">{{ 'legend.show-avg' | translate }} |
|||
</md-checkbox> |
|||
<md-checkbox flex aria-label="{{ 'legend.show-total' | translate }}" |
|||
ng-model="vm.legendConfig.showTotal">{{ 'legend.show-total' | translate }} |
|||
</md-checkbox> |
|||
</md-content> |
|||
</section> |
|||
</md-content> |
|||
</fieldset> |
|||
</form> |
|||
@ -0,0 +1,151 @@ |
|||
/* |
|||
* Copyright © 2016-2017 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 './legend-config.scss'; |
|||
|
|||
import $ from 'jquery'; |
|||
|
|||
/* eslint-disable import/no-unresolved, import/default */ |
|||
|
|||
import legendConfigButtonTemplate from './legend-config-button.tpl.html'; |
|||
import legendConfigPanelTemplate from './legend-config-panel.tpl.html'; |
|||
|
|||
/* eslint-enable import/no-unresolved, import/default */ |
|||
|
|||
import LegendConfigPanelController from './legend-config-panel.controller'; |
|||
|
|||
|
|||
export default angular.module('thingsboard.directives.legendConfig', []) |
|||
.controller('LegendConfigPanelController', LegendConfigPanelController) |
|||
.directive('tbLegendConfig', LegendConfig) |
|||
.name; |
|||
|
|||
/* eslint-disable angular/angularelement */ |
|||
/*@ngInject*/ |
|||
function LegendConfig($compile, $templateCache, types, $mdPanel, $document) { |
|||
|
|||
var linker = function (scope, element, attrs, ngModelCtrl) { |
|||
|
|||
/* tbLegendConfig (ng-model) |
|||
* { |
|||
* position: types.position.bottom.value, |
|||
* showMin: false, |
|||
* showMax: false, |
|||
* showAvg: true, |
|||
* showTotal: false |
|||
* } |
|||
*/ |
|||
|
|||
var template = $templateCache.get(legendConfigButtonTemplate); |
|||
element.html(template); |
|||
|
|||
scope.openEditMode = function (event) { |
|||
if (scope.disabled) { |
|||
return; |
|||
} |
|||
var position; |
|||
var panelHeight = 220; |
|||
var panelWidth = 220; |
|||
var offset = element[0].getBoundingClientRect(); |
|||
var bottomY = offset.bottom - $(window).scrollTop(); //eslint-disable-line
|
|||
var leftX = offset.left - $(window).scrollLeft(); //eslint-disable-line
|
|||
var yPosition; |
|||
var xPosition; |
|||
if (bottomY + panelHeight > $( window ).height()) { //eslint-disable-line
|
|||
yPosition = $mdPanel.yPosition.ABOVE; |
|||
} else { |
|||
yPosition = $mdPanel.yPosition.BELOW; |
|||
} |
|||
if (leftX + panelWidth > $( window ).width()) { //eslint-disable-line
|
|||
xPosition = $mdPanel.xPosition.ALIGN_END; |
|||
} else { |
|||
xPosition = $mdPanel.xPosition.ALIGN_START; |
|||
} |
|||
position = $mdPanel.newPanelPosition() |
|||
.relativeTo(element) |
|||
.addPanelPosition(xPosition, yPosition); |
|||
var config = { |
|||
attachTo: angular.element($document[0].body), |
|||
controller: 'LegendConfigPanelController', |
|||
controllerAs: 'vm', |
|||
templateUrl: legendConfigPanelTemplate, |
|||
panelClass: 'tb-legend-config-panel', |
|||
position: position, |
|||
fullscreen: false, |
|||
locals: { |
|||
'legendConfig': angular.copy(scope.model), |
|||
'onLegendConfigUpdate': function (legendConfig) { |
|||
scope.model = legendConfig; |
|||
scope.updateView(); |
|||
} |
|||
}, |
|||
openFrom: event, |
|||
clickOutsideToClose: true, |
|||
escapeToClose: true, |
|||
focusOnOpen: false |
|||
}; |
|||
$mdPanel.open(config); |
|||
} |
|||
|
|||
scope.updateView = function () { |
|||
var value = {}; |
|||
var model = scope.model; |
|||
value.position = model.position; |
|||
value.showMin = model.showMin; |
|||
value.showMax = model.showMax; |
|||
value.showAvg = model.showAvg; |
|||
value.showTotal = model.showTotal; |
|||
ngModelCtrl.$setViewValue(value); |
|||
} |
|||
|
|||
ngModelCtrl.$render = function () { |
|||
if (ngModelCtrl.$viewValue) { |
|||
var value = ngModelCtrl.$viewValue; |
|||
if (!scope.model) { |
|||
scope.model = {}; |
|||
} |
|||
var model = scope.model; |
|||
model.position = value.position || types.position.bottom.value; |
|||
model.showMin = angular.isDefined(value.showMin) ? value.showMin : false; |
|||
model.showMax = angular.isDefined(value.showMax) ? value.showMax : false; |
|||
model.showAvg = angular.isDefined(value.showAvg) ? value.showAvg : true; |
|||
model.showTotal = angular.isDefined(value.showTotal) ? value.showTotal : false; |
|||
} else { |
|||
scope.model = { |
|||
position: types.position.bottom.value, |
|||
showMin: false, |
|||
showMax: false, |
|||
showAvg: true, |
|||
showTotal: false |
|||
} |
|||
} |
|||
} |
|||
|
|||
$compile(element.contents())(scope); |
|||
} |
|||
|
|||
return { |
|||
restrict: "E", |
|||
require: "^ngModel", |
|||
scope: { |
|||
disabled:'=ngDisabled' |
|||
}, |
|||
link: linker |
|||
}; |
|||
|
|||
} |
|||
|
|||
/* eslint-enable angular/angularelement */ |
|||
@ -0,0 +1,49 @@ |
|||
/** |
|||
* Copyright © 2016-2017 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. |
|||
*/ |
|||
|
|||
.md-panel { |
|||
&.tb-legend-config-panel { |
|||
position: absolute; |
|||
} |
|||
} |
|||
|
|||
.tb-legend-config-panel { |
|||
max-height: 220px; |
|||
min-width: 220px; |
|||
background: white; |
|||
border-radius: 4px; |
|||
box-shadow: 0 7px 8px -4px rgba(0, 0, 0, 0.2), |
|||
0 13px 19px 2px rgba(0, 0, 0, 0.14), |
|||
0 5px 24px 4px rgba(0, 0, 0, 0.12); |
|||
overflow: hidden; |
|||
form, fieldset { |
|||
height: 100%; |
|||
} |
|||
md-content { |
|||
background-color: #fff; |
|||
overflow: hidden; |
|||
} |
|||
.md-padding { |
|||
padding: 0 16px; |
|||
} |
|||
} |
|||
|
|||
tb-legend-config { |
|||
span { |
|||
pointer-events: all; |
|||
cursor: pointer; |
|||
} |
|||
} |
|||
@ -0,0 +1,85 @@ |
|||
/* |
|||
* Copyright © 2016-2017 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 './legend.scss'; |
|||
|
|||
/* eslint-disable import/no-unresolved, import/default */ |
|||
|
|||
import legendTemplate from './legend.tpl.html'; |
|||
|
|||
/* eslint-enable import/no-unresolved, import/default */ |
|||
|
|||
|
|||
export default angular.module('thingsboard.directives.legend', []) |
|||
.directive('tbLegend', Legend) |
|||
.name; |
|||
|
|||
/*@ngInject*/ |
|||
function Legend($compile, $templateCache, types) { |
|||
|
|||
var linker = function (scope, element) { |
|||
var template = $templateCache.get(legendTemplate); |
|||
element.html(template); |
|||
|
|||
scope.displayHeader = function() { |
|||
return scope.legendConfig.showMin === true || |
|||
scope.legendConfig.showMax === true || |
|||
scope.legendConfig.showAvg === true || |
|||
scope.legendConfig.showTotal === true; |
|||
} |
|||
|
|||
scope.isHorizontal = scope.legendConfig.position === types.position.bottom.value || |
|||
scope.legendConfig.position === types.position.top.value; |
|||
|
|||
scope.$on('legendDataUpdated', function () { |
|||
scope.$digest(); |
|||
}); |
|||
|
|||
scope.toggleHideData = function(index) { |
|||
scope.legendData.data[index].hidden = !scope.legendData.data[index].hidden; |
|||
scope.$emit('legendDataHiddenChanged', index); |
|||
} |
|||
|
|||
$compile(element.contents())(scope); |
|||
|
|||
} |
|||
|
|||
/* scope.legendData = { |
|||
keys: [], |
|||
data: [] |
|||
|
|||
key: { |
|||
label: '', |
|||
color: '' |
|||
dataIndex: 0 |
|||
} |
|||
data: { |
|||
min: null, |
|||
max: null, |
|||
avg: null, |
|||
total: null |
|||
} |
|||
};*/ |
|||
|
|||
return { |
|||
restrict: "E", |
|||
link: linker, |
|||
scope: { |
|||
legendConfig: '=', |
|||
legendData: '=' |
|||
} |
|||
}; |
|||
} |
|||
@ -0,0 +1,53 @@ |
|||
/** |
|||
* Copyright © 2016-2017 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. |
|||
*/ |
|||
|
|||
table.tb-legend { |
|||
width: 100%; |
|||
font-size: 12px; |
|||
.tb-legend-header, .tb-legend-value { |
|||
text-align: right; |
|||
} |
|||
.tb-legend-header { |
|||
th { |
|||
color: rgb(255,110,64); |
|||
white-space: nowrap; |
|||
padding: 0 10px 1px 0; |
|||
} |
|||
} |
|||
.tb-legend-keys { |
|||
td.tb-legend-label, td.tb-legend-value { |
|||
white-space: nowrap; |
|||
padding: 2px 10px; |
|||
} |
|||
.tb-legend-line { |
|||
width: 15px; |
|||
height: 3px; |
|||
display: inline-block; |
|||
vertical-align: middle; |
|||
} |
|||
.tb-legend-label { |
|||
text-align: left; |
|||
outline: none; |
|||
&.tb-horizontal { |
|||
width: 95%; |
|||
} |
|||
&.tb-hidden-label { |
|||
text-decoration: line-through; |
|||
opacity: 0.6; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,42 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2017 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. |
|||
|
|||
--> |
|||
<table class="tb-legend"> |
|||
<thead> |
|||
<tr class="tb-legend-header"> |
|||
<th colspan="2"></th> |
|||
<th ng-if="legendConfig.showMin === true">{{ 'legend.min' | translate }}</th> |
|||
<th ng-if="legendConfig.showMax === true">{{ 'legend.max' | translate }}</th> |
|||
<th ng-if="legendConfig.showAvg === true">{{ 'legend.avg' | translate }}</th> |
|||
<th ng-if="legendConfig.showTotal === true">{{ 'legend.total' | translate }}</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
<tr class="tb-legend-keys" ng-repeat="legendKey in legendData.keys"> |
|||
<td><span class="tb-legend-line" ng-style="{backgroundColor: legendKey.color}"></span></td> |
|||
<td class="tb-legend-label" |
|||
ng-click="toggleHideData(legendKey.dataIndex)" |
|||
ng-class="{ 'tb-hidden-label': legendData.data[legendKey.dataIndex].hidden, 'tb-horizontal': isHorizontal }"> |
|||
{{ legendKey.label }} |
|||
</td> |
|||
<td class="tb-legend-value" ng-if="legendConfig.showMin === true">{{ legendData.data[legendKey.dataIndex].min }}</td> |
|||
<td class="tb-legend-value" ng-if="legendConfig.showMax === true">{{ legendData.data[legendKey.dataIndex].max }}</td> |
|||
<td class="tb-legend-value" ng-if="legendConfig.showAvg === true">{{ legendData.data[legendKey.dataIndex].avg }}</td> |
|||
<td class="tb-legend-value" ng-if="legendConfig.showTotal === true">{{ legendData.data[legendKey.dataIndex].total }}</td> |
|||
</tr> |
|||
</tbody> |
|||
</table> |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue