85 changed files with 5279 additions and 237 deletions
@ -0,0 +1,25 @@ |
|||||
|
{ |
||||
|
"widgetsBundle": { |
||||
|
"alias": "date", |
||||
|
"title": "Date", |
||||
|
"image": null |
||||
|
}, |
||||
|
"widgetTypes": [ |
||||
|
{ |
||||
|
"alias": "date_range_navigator", |
||||
|
"name": "Date-range-navigator", |
||||
|
"descriptor": { |
||||
|
"type": "static", |
||||
|
"sizeX": 5, |
||||
|
"sizeY": 5.5, |
||||
|
"resources": [], |
||||
|
"templateHtml": "<date-range-navigator-widget class=\"date-range-navigator-widget\" ctx=\"ctx\"></date-range-navigator-widget>", |
||||
|
"templateCss": "", |
||||
|
"controllerScript": "self.onInit = function() {\n scope = self.ctx.$scope;\n scope.ctx = self.ctx;\n}", |
||||
|
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"hidePicker\": {\n \"title\": \"Hide date range picker\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"onePanel\": {\n \"title\": \"Date range picker one panel\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"autoConfirm\": {\n \"title\": \"Date range picker auto confirm\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"showTemplate\": {\n \"title\": \"Date range picker show template\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"firstDayOfWeek\": {\n \"title\": \"First day of the week\",\n \"type\": \"number\",\n \"default\": 1\n },\n \"hideInterval\": {\n \"title\": \"Hide interval\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"initialInterval\": {\n\t\t\t\t\"title\": \"Initial interval\",\n\t\t\t\t\"type\": \"string\",\n\t\t\t\t\"default\": \"week\"\n\t\t\t},\n \"hideStepSize\": {\n \"title\": \"Hide step size\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"stepSize\": {\n\t\t\t\t\"title\": \"Initial step size\",\n\t\t\t\t\"type\": \"string\",\n\t\t\t\t\"default\": \"day\"\n\t\t\t},\n \"hideLabels\": {\n \"title\": \"Hide labels\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"useSessionStorage\": {\n \"title\": \"Use session storage\",\n \"type\": \"boolean\",\n \"default\": true\n }\n }\n },\n \"form\": [\n \"hidePicker\",\n\t\t\"onePanel\",\n\t\t\"autoConfirm\",\n\t\t\"showTemplate\",\n\t\t\"firstDayOfWeek\",\n \"hideInterval\",\n {\n\t\t\t\"key\": \"initialInterval\",\n\t\t\t\"type\": \"rc-select\",\n\t\t\t\"multiple\": false,\n\t\t\t\"items\": [\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"hour\",\n\t\t\t\t\t\"label\": \"Hour\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"day\",\n\t\t\t\t\t\"label\": \"Day\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"week\",\n\t\t\t\t\t\"label\": \"Week\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"twoWeeks\",\n\t\t\t\t\t\"label\": \"2 weeks\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"month\",\n\t\t\t\t\t\"label\": \"Month\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"threeMonths\",\n\t\t\t\t\t\"label\": \"3 months\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"sixMonths\",\n\t\t\t\t\t\"label\": \"6 months\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n \"hideStepSize\",\n {\n\t\t\t\"key\": \"stepSize\",\n\t\t\t\"type\": \"rc-select\",\n\t\t\t\"multiple\": false,\n\t\t\t\"items\": [\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"hour\",\n\t\t\t\t\t\"label\": \"Hour\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"day\",\n\t\t\t\t\t\"label\": \"Day\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"week\",\n\t\t\t\t\t\"label\": \"Week\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"twoWeeks\",\n\t\t\t\t\t\"label\": \"2 weeks\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"month\",\n\t\t\t\t\t\"label\": \"Month\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"threeMonths\",\n\t\t\t\t\t\"label\": \"3 months\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"value\": \"sixMonths\",\n\t\t\t\t\t\"label\": \"6 months\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t\"hideLabels\",\n\t\t\"useSessionStorage\"\n ]\n}", |
||||
|
"dataKeySettingsSchema": "{}\n", |
||||
|
"defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"defaultInterval\":\"week\",\"stepSize\":\"day\"},\"title\":\"Date-range-navigator\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
} |
||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,266 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 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.rule.engine.action; |
||||
|
|
||||
|
import com.datastax.driver.core.BoundStatement; |
||||
|
import com.datastax.driver.core.CodecRegistry; |
||||
|
import com.datastax.driver.core.ConsistencyLevel; |
||||
|
import com.datastax.driver.core.PreparedStatement; |
||||
|
import com.datastax.driver.core.ResultSet; |
||||
|
import com.datastax.driver.core.ResultSetFuture; |
||||
|
import com.datastax.driver.core.Session; |
||||
|
import com.datastax.driver.core.Statement; |
||||
|
import com.datastax.driver.core.TypeCodec; |
||||
|
import com.datastax.driver.core.exceptions.CodecNotFoundException; |
||||
|
import com.google.common.base.Function; |
||||
|
import com.google.common.util.concurrent.Futures; |
||||
|
import com.google.common.util.concurrent.ListenableFuture; |
||||
|
import com.google.gson.JsonElement; |
||||
|
import com.google.gson.JsonObject; |
||||
|
import com.google.gson.JsonParser; |
||||
|
import com.google.gson.JsonPrimitive; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.thingsboard.rule.engine.api.RuleNode; |
||||
|
import org.thingsboard.rule.engine.api.TbContext; |
||||
|
import org.thingsboard.rule.engine.api.TbNode; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeConfiguration; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeException; |
||||
|
import org.thingsboard.rule.engine.api.util.TbNodeUtils; |
||||
|
import org.thingsboard.server.common.data.plugin.ComponentType; |
||||
|
import org.thingsboard.server.common.msg.TbMsg; |
||||
|
import org.thingsboard.server.dao.cassandra.CassandraCluster; |
||||
|
import org.thingsboard.server.dao.model.ModelConstants; |
||||
|
import org.thingsboard.server.dao.model.type.AuthorityCodec; |
||||
|
import org.thingsboard.server.dao.model.type.ComponentLifecycleStateCodec; |
||||
|
import org.thingsboard.server.dao.model.type.ComponentScopeCodec; |
||||
|
import org.thingsboard.server.dao.model.type.ComponentTypeCodec; |
||||
|
import org.thingsboard.server.dao.model.type.DeviceCredentialsTypeCodec; |
||||
|
import org.thingsboard.server.dao.model.type.EntityTypeCodec; |
||||
|
import org.thingsboard.server.dao.model.type.JsonCodec; |
||||
|
import org.thingsboard.server.dao.nosql.CassandraStatementTask; |
||||
|
|
||||
|
import javax.annotation.Nullable; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
import java.util.concurrent.ExecutionException; |
||||
|
import java.util.concurrent.ExecutorService; |
||||
|
import java.util.concurrent.Executors; |
||||
|
import java.util.concurrent.atomic.AtomicInteger; |
||||
|
|
||||
|
import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; |
||||
|
import static org.thingsboard.rule.engine.api.util.DonAsynchron.withCallback; |
||||
|
|
||||
|
@Slf4j |
||||
|
@RuleNode(type = ComponentType.ACTION, |
||||
|
name = "save to custom table", |
||||
|
configClazz = TbSaveToCustomCassandraTableNodeConfiguration.class, |
||||
|
nodeDescription = "Node stores data from incoming Message payload to the Cassandra database into the predefined custom table" + |
||||
|
" that should have <b>cs_tb_</b> prefix, to avoid the data insertion to the common TB tables.<br>" + |
||||
|
"<b>Note:</b> rule node can be used only for Cassandra DB.", |
||||
|
nodeDetails = "Administrator should set the custom table name without prefix: <b>cs_tb_</b>. <br>" + |
||||
|
"Administrator can configure the mapping between the Message field names and Table columns name.<br>" + |
||||
|
"<b>Note:</b>If the mapping key is <b>$entity_id</b>, that is identified by the Message Originator, then to the appropriate column name(mapping value) will be write the message originator id.<br><br>" + |
||||
|
"If specified message field does not exist or is not a JSON Primitive, the outbound message will be routed via <b>failure</b> chain," + |
||||
|
" otherwise, the message will be routed via <b>success</b> chain.", |
||||
|
uiResources = {"static/rulenode/rulenode-core-config.js"}, |
||||
|
configDirective = "tbActionNodeCustomTableConfig", |
||||
|
icon = "file_upload") |
||||
|
public class TbSaveToCustomCassandraTableNode implements TbNode { |
||||
|
|
||||
|
private static final String TABLE_PREFIX = "cs_tb_"; |
||||
|
private static final JsonParser parser = new JsonParser(); |
||||
|
private static final String ENTITY_ID = "$entityId"; |
||||
|
|
||||
|
private TbSaveToCustomCassandraTableNodeConfiguration config; |
||||
|
private Session session; |
||||
|
private CassandraCluster cassandraCluster; |
||||
|
private ConsistencyLevel defaultWriteLevel; |
||||
|
private PreparedStatement saveStmt; |
||||
|
private ExecutorService readResultsProcessingExecutor; |
||||
|
private Map<String, String> fieldsMap; |
||||
|
|
||||
|
@Override |
||||
|
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { |
||||
|
config = TbNodeUtils.convert(configuration, TbSaveToCustomCassandraTableNodeConfiguration.class); |
||||
|
cassandraCluster = ctx.getCassandraCluster(); |
||||
|
if (cassandraCluster == null) { |
||||
|
throw new RuntimeException("Unable to connect to Cassandra database"); |
||||
|
} else { |
||||
|
startExecutor(); |
||||
|
saveStmt = getSaveStmt(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException { |
||||
|
withCallback(save(msg, ctx), aVoid -> { |
||||
|
ctx.tellNext(msg, SUCCESS); |
||||
|
}, e -> ctx.tellFailure(msg, e), ctx.getDbCallbackExecutor()); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void destroy() { |
||||
|
stopExecutor(); |
||||
|
saveStmt = null; |
||||
|
} |
||||
|
|
||||
|
private void startExecutor() { |
||||
|
readResultsProcessingExecutor = Executors.newCachedThreadPool(); |
||||
|
} |
||||
|
|
||||
|
private void stopExecutor() { |
||||
|
if (readResultsProcessingExecutor != null) { |
||||
|
readResultsProcessingExecutor.shutdownNow(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private PreparedStatement prepare(String query) { |
||||
|
return getSession().prepare(query); |
||||
|
} |
||||
|
|
||||
|
private Session getSession() { |
||||
|
if (session == null) { |
||||
|
session = cassandraCluster.getSession(); |
||||
|
defaultWriteLevel = cassandraCluster.getDefaultWriteConsistencyLevel(); |
||||
|
CodecRegistry registry = session.getCluster().getConfiguration().getCodecRegistry(); |
||||
|
registerCodecIfNotFound(registry, new JsonCodec()); |
||||
|
registerCodecIfNotFound(registry, new DeviceCredentialsTypeCodec()); |
||||
|
registerCodecIfNotFound(registry, new AuthorityCodec()); |
||||
|
registerCodecIfNotFound(registry, new ComponentLifecycleStateCodec()); |
||||
|
registerCodecIfNotFound(registry, new ComponentTypeCodec()); |
||||
|
registerCodecIfNotFound(registry, new ComponentScopeCodec()); |
||||
|
registerCodecIfNotFound(registry, new EntityTypeCodec()); |
||||
|
} |
||||
|
return session; |
||||
|
} |
||||
|
|
||||
|
private void registerCodecIfNotFound(CodecRegistry registry, TypeCodec<?> codec) { |
||||
|
try { |
||||
|
registry.codecFor(codec.getCqlType(), codec.getJavaType()); |
||||
|
} catch (CodecNotFoundException e) { |
||||
|
registry.register(codec); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
private PreparedStatement getSaveStmt() { |
||||
|
fieldsMap = config.getFieldsMapping(); |
||||
|
if (fieldsMap.isEmpty()) { |
||||
|
throw new RuntimeException("Fields(key,value) map is empty!"); |
||||
|
} else { |
||||
|
return prepareStatement(new ArrayList<>(fieldsMap.values())); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private PreparedStatement prepareStatement(List<String> fieldsList) { |
||||
|
return prepare(createQuery(fieldsList)); |
||||
|
} |
||||
|
|
||||
|
private String createQuery(List<String> fieldsList) { |
||||
|
int size = fieldsList.size(); |
||||
|
StringBuilder query = new StringBuilder(); |
||||
|
query.append("INSERT INTO ") |
||||
|
.append(TABLE_PREFIX) |
||||
|
.append(config.getTableName()) |
||||
|
.append("("); |
||||
|
for (String field : fieldsList) { |
||||
|
query.append(field); |
||||
|
if (fieldsList.get(size - 1).equals(field)) { |
||||
|
query.append(")"); |
||||
|
} else { |
||||
|
query.append(","); |
||||
|
} |
||||
|
} |
||||
|
query.append(" VALUES("); |
||||
|
for (int i = 0; i < size; i++) { |
||||
|
if (i == size - 1) { |
||||
|
query.append("?)"); |
||||
|
} else { |
||||
|
query.append("?, "); |
||||
|
} |
||||
|
} |
||||
|
return query.toString(); |
||||
|
} |
||||
|
|
||||
|
private ListenableFuture<Void> save(TbMsg msg, TbContext ctx) { |
||||
|
JsonElement data = parser.parse(msg.getData()); |
||||
|
if (!data.isJsonObject()) { |
||||
|
throw new IllegalStateException("Invalid message structure, it is not a JSON Object:" + data); |
||||
|
} else { |
||||
|
JsonObject dataAsObject = data.getAsJsonObject(); |
||||
|
BoundStatement stmt = saveStmt.bind(); |
||||
|
AtomicInteger i = new AtomicInteger(0); |
||||
|
fieldsMap.forEach((key, value) -> { |
||||
|
if (key.equals(ENTITY_ID)) { |
||||
|
stmt.setUUID(i.get(), msg.getOriginator().getId()); |
||||
|
} else if (dataAsObject.has(key)) { |
||||
|
if (dataAsObject.get(key).isJsonPrimitive()) { |
||||
|
JsonPrimitive primitive = dataAsObject.get(key).getAsJsonPrimitive(); |
||||
|
if (primitive.isNumber()) { |
||||
|
stmt.setLong(i.get(), dataAsObject.get(key).getAsLong()); |
||||
|
} else if (primitive.isBoolean()) { |
||||
|
stmt.setBool(i.get(), dataAsObject.get(key).getAsBoolean()); |
||||
|
} else if (primitive.isString()) { |
||||
|
stmt.setString(i.get(), dataAsObject.get(key).getAsString()); |
||||
|
} else { |
||||
|
stmt.setToNull(i.get()); |
||||
|
} |
||||
|
} else { |
||||
|
throw new IllegalStateException("Message data key: '" + key + "' with value: '" + value + "' is not a JSON Primitive!"); |
||||
|
} |
||||
|
} else { |
||||
|
throw new RuntimeException("Message data doesn't contain key: " + "'" + key + "'!"); |
||||
|
} |
||||
|
i.getAndIncrement(); |
||||
|
}); |
||||
|
return getFuture(executeAsyncWrite(ctx, stmt), rs -> null); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private ResultSetFuture executeAsyncWrite(TbContext ctx, Statement statement) { |
||||
|
return executeAsync(ctx, statement, defaultWriteLevel); |
||||
|
} |
||||
|
|
||||
|
private ResultSetFuture executeAsync(TbContext ctx, Statement statement, ConsistencyLevel level) { |
||||
|
if (log.isDebugEnabled()) { |
||||
|
log.debug("Execute cassandra async statement {}", statementToString(statement)); |
||||
|
} |
||||
|
if (statement.getConsistencyLevel() == null) { |
||||
|
statement.setConsistencyLevel(level); |
||||
|
} |
||||
|
return ctx.getCassandraBufferedRateExecutor().submit(new CassandraStatementTask(ctx.getTenantId(), getSession(), statement)); |
||||
|
} |
||||
|
|
||||
|
private static String statementToString(Statement statement) { |
||||
|
if (statement instanceof BoundStatement) { |
||||
|
return ((BoundStatement) statement).preparedStatement().getQueryString(); |
||||
|
} else { |
||||
|
return statement.toString(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private <T> ListenableFuture<T> getFuture(ResultSetFuture future, java.util.function.Function<ResultSet, T> transformer) { |
||||
|
return Futures.transform(future, new Function<ResultSet, T>() { |
||||
|
@Nullable |
||||
|
@Override |
||||
|
public T apply(@Nullable ResultSet input) { |
||||
|
return transformer.apply(input); |
||||
|
} |
||||
|
}, readResultsProcessingExecutor); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,41 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 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.rule.engine.action; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
import org.thingsboard.rule.engine.api.NodeConfiguration; |
||||
|
|
||||
|
import java.util.HashMap; |
||||
|
import java.util.Map; |
||||
|
|
||||
|
@Data |
||||
|
public class TbSaveToCustomCassandraTableNodeConfiguration implements NodeConfiguration<TbSaveToCustomCassandraTableNodeConfiguration> { |
||||
|
|
||||
|
|
||||
|
private String tableName; |
||||
|
private Map<String, String> fieldsMapping; |
||||
|
|
||||
|
|
||||
|
@Override |
||||
|
public TbSaveToCustomCassandraTableNodeConfiguration defaultConfiguration() { |
||||
|
TbSaveToCustomCassandraTableNodeConfiguration configuration = new TbSaveToCustomCassandraTableNodeConfiguration(); |
||||
|
configuration.setTableName(""); |
||||
|
Map<String, String> map = new HashMap<>(); |
||||
|
map.put("", ""); |
||||
|
configuration.setFieldsMapping(map); |
||||
|
return configuration; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,131 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 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.rule.engine.geo; |
||||
|
|
||||
|
import com.google.gson.JsonElement; |
||||
|
import com.google.gson.JsonObject; |
||||
|
import com.google.gson.JsonParser; |
||||
|
import org.locationtech.spatial4j.context.jts.JtsSpatialContext; |
||||
|
import org.locationtech.spatial4j.context.jts.JtsSpatialContextFactory; |
||||
|
import org.springframework.util.StringUtils; |
||||
|
import org.thingsboard.rule.engine.api.TbContext; |
||||
|
import org.thingsboard.rule.engine.api.TbNode; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeConfiguration; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeException; |
||||
|
import org.thingsboard.rule.engine.api.util.TbNodeUtils; |
||||
|
import org.thingsboard.server.common.msg.TbMsg; |
||||
|
|
||||
|
import java.util.Collections; |
||||
|
import java.util.List; |
||||
|
|
||||
|
public abstract class AbstractGeofencingNode<T extends TbGpsGeofencingFilterNodeConfiguration> implements TbNode { |
||||
|
|
||||
|
protected T config; |
||||
|
protected JtsSpatialContext jtsCtx; |
||||
|
|
||||
|
@Override |
||||
|
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { |
||||
|
this.config = TbNodeUtils.convert(configuration, getConfigClazz()); |
||||
|
JtsSpatialContextFactory factory = new JtsSpatialContextFactory(); |
||||
|
factory.normWrapLongitude = true; |
||||
|
jtsCtx = factory.newSpatialContext(); |
||||
|
} |
||||
|
|
||||
|
abstract protected Class<T> getConfigClazz(); |
||||
|
|
||||
|
protected boolean checkMatches(TbMsg msg) throws TbNodeException { |
||||
|
JsonElement msgDataElement = new JsonParser().parse(msg.getData()); |
||||
|
if (!msgDataElement.isJsonObject()) { |
||||
|
throw new TbNodeException("Incoming Message is not a valid JSON object"); |
||||
|
} |
||||
|
JsonObject msgDataObj = msgDataElement.getAsJsonObject(); |
||||
|
double latitude = getValueFromMessageByName(msg, msgDataObj, config.getLatitudeKeyName()); |
||||
|
double longitude = getValueFromMessageByName(msg, msgDataObj, config.getLongitudeKeyName()); |
||||
|
List<Perimeter> perimeters = getPerimeters(msg, msgDataObj); |
||||
|
boolean matches = false; |
||||
|
for (Perimeter perimeter : perimeters) { |
||||
|
if (checkMatches(perimeter, latitude, longitude)) { |
||||
|
matches = true; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
return matches; |
||||
|
} |
||||
|
|
||||
|
protected boolean checkMatches(Perimeter perimeter, double latitude, double longitude) throws TbNodeException { |
||||
|
if (perimeter.getPerimeterType() == PerimeterType.CIRCLE) { |
||||
|
Coordinates entityCoordinates = new Coordinates(latitude, longitude); |
||||
|
Coordinates perimeterCoordinates = new Coordinates(perimeter.getCenterLatitude(), perimeter.getCenterLongitude()); |
||||
|
return perimeter.getRange() > GeoUtil.distance(entityCoordinates, perimeterCoordinates, perimeter.getRangeUnit()); |
||||
|
} else if (perimeter.getPerimeterType() == PerimeterType.POLYGON) { |
||||
|
return GeoUtil.contains(perimeter.getPolygonsDefinition(), new Coordinates(latitude, longitude)); |
||||
|
} else { |
||||
|
throw new TbNodeException("Unsupported perimeter type: " + perimeter.getPerimeterType()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected List<Perimeter> getPerimeters(TbMsg msg, JsonObject msgDataObj) throws TbNodeException { |
||||
|
if (config.isFetchPerimeterInfoFromMessageMetadata()) { |
||||
|
//TODO: add fetching perimeters from the message itself, if configuration is empty.
|
||||
|
if (!StringUtils.isEmpty(msg.getMetaData().getValue("perimeter"))) { |
||||
|
Perimeter perimeter = new Perimeter(); |
||||
|
perimeter.setPerimeterType(PerimeterType.POLYGON); |
||||
|
perimeter.setPolygonsDefinition(msg.getMetaData().getValue("perimeter")); |
||||
|
return Collections.singletonList(perimeter); |
||||
|
} else if (!StringUtils.isEmpty(msg.getMetaData().getValue("centerLatitude"))) { |
||||
|
Perimeter perimeter = new Perimeter(); |
||||
|
perimeter.setPerimeterType(PerimeterType.CIRCLE); |
||||
|
perimeter.setCenterLatitude(Double.parseDouble(msg.getMetaData().getValue("centerLatitude"))); |
||||
|
perimeter.setCenterLongitude(Double.parseDouble(msg.getMetaData().getValue("centerLongitude"))); |
||||
|
perimeter.setRange(Double.parseDouble(msg.getMetaData().getValue("range"))); |
||||
|
perimeter.setRangeUnit(RangeUnit.valueOf(msg.getMetaData().getValue("rangeUnit"))); |
||||
|
return Collections.singletonList(perimeter); |
||||
|
} else { |
||||
|
throw new TbNodeException("Missing perimeter definition!"); |
||||
|
} |
||||
|
} else { |
||||
|
Perimeter perimeter = new Perimeter(); |
||||
|
perimeter.setPerimeterType(config.getPerimeterType()); |
||||
|
perimeter.setCenterLatitude(config.getCenterLatitude()); |
||||
|
perimeter.setCenterLongitude(config.getCenterLongitude()); |
||||
|
perimeter.setRange(config.getRange()); |
||||
|
perimeter.setRangeUnit(config.getRangeUnit()); |
||||
|
perimeter.setPolygonsDefinition(config.getPolygonsDefinition()); |
||||
|
return Collections.singletonList(perimeter); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected Double getValueFromMessageByName(TbMsg msg, JsonObject msgDataObj, String keyName) throws TbNodeException { |
||||
|
double value; |
||||
|
if (msgDataObj.has(keyName) && msgDataObj.get(keyName).isJsonPrimitive()) { |
||||
|
value = msgDataObj.get(keyName).getAsDouble(); |
||||
|
} else { |
||||
|
String valueStr = msg.getMetaData().getValue(keyName); |
||||
|
if (!StringUtils.isEmpty(valueStr)) { |
||||
|
value = Double.parseDouble(valueStr); |
||||
|
} else { |
||||
|
throw new TbNodeException("Incoming Message has no " + keyName + " in data or metadata!"); |
||||
|
} |
||||
|
} |
||||
|
return value; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void destroy() { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 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.rule.engine.geo; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
|
||||
|
@Data |
||||
|
public class Coordinates { |
||||
|
private final double latitude; |
||||
|
private final double longitude; |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 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.rule.engine.geo; |
||||
|
|
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.Data; |
||||
|
|
||||
|
@Data |
||||
|
@AllArgsConstructor |
||||
|
public class EntityGeofencingState { |
||||
|
|
||||
|
private boolean inside; |
||||
|
private long stateSwitchTime; |
||||
|
private boolean stayed; |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,68 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 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.rule.engine.geo; |
||||
|
|
||||
|
import com.google.gson.JsonArray; |
||||
|
import com.google.gson.JsonElement; |
||||
|
import com.google.gson.JsonParser; |
||||
|
import org.locationtech.spatial4j.context.SpatialContext; |
||||
|
import org.locationtech.spatial4j.context.jts.JtsSpatialContext; |
||||
|
import org.locationtech.spatial4j.context.jts.JtsSpatialContextFactory; |
||||
|
import org.locationtech.spatial4j.distance.DistanceUtils; |
||||
|
import org.locationtech.spatial4j.shape.Point; |
||||
|
import org.locationtech.spatial4j.shape.Shape; |
||||
|
import org.locationtech.spatial4j.shape.ShapeFactory; |
||||
|
import org.locationtech.spatial4j.shape.SpatialRelation; |
||||
|
|
||||
|
public class GeoUtil { |
||||
|
|
||||
|
private static final SpatialContext distCtx = SpatialContext.GEO; |
||||
|
private static final JtsSpatialContext jtsCtx; |
||||
|
|
||||
|
static { |
||||
|
JtsSpatialContextFactory factory = new JtsSpatialContextFactory(); |
||||
|
factory.normWrapLongitude = true; |
||||
|
jtsCtx = factory.newSpatialContext(); |
||||
|
} |
||||
|
|
||||
|
public static synchronized double distance(Coordinates x, Coordinates y, RangeUnit unit) { |
||||
|
Point xLL = distCtx.getShapeFactory().pointXY(x.getLongitude(), x.getLatitude()); |
||||
|
Point yLL = distCtx.getShapeFactory().pointXY(y.getLongitude(), y.getLatitude()); |
||||
|
return unit.fromKm(distCtx.getDistCalc().distance(xLL, yLL) * DistanceUtils.DEG_TO_KM); |
||||
|
} |
||||
|
|
||||
|
public static synchronized boolean contains(String polygon, Coordinates coordinates) { |
||||
|
ShapeFactory.PolygonBuilder polygonBuilder = jtsCtx.getShapeFactory().polygon(); |
||||
|
JsonArray polygonArray = new JsonParser().parse(polygon).getAsJsonArray(); |
||||
|
boolean first = true; |
||||
|
double firstLat = 0.0; |
||||
|
double firstLng = 0.0; |
||||
|
for (JsonElement jsonElement : polygonArray) { |
||||
|
double lat = jsonElement.getAsJsonArray().get(0).getAsDouble(); |
||||
|
double lng = jsonElement.getAsJsonArray().get(1).getAsDouble(); |
||||
|
if (first) { |
||||
|
firstLat = lat; |
||||
|
firstLng = lng; |
||||
|
first = false; |
||||
|
} |
||||
|
polygonBuilder.pointXY(jtsCtx.getShapeFactory().normX(lng), jtsCtx.getShapeFactory().normY(lat)); |
||||
|
} |
||||
|
polygonBuilder.pointXY(jtsCtx.getShapeFactory().normX(firstLng), jtsCtx.getShapeFactory().normY(firstLat)); |
||||
|
Shape shape = polygonBuilder.buildOrRect(); |
||||
|
Point point = jtsCtx.makePoint(coordinates.getLongitude(), coordinates.getLatitude()); |
||||
|
return shape.relate(point).equals(SpatialRelation.CONTAINS); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,34 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 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.rule.engine.geo; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
|
||||
|
@Data |
||||
|
public class Perimeter { |
||||
|
|
||||
|
private PerimeterType perimeterType; |
||||
|
|
||||
|
//For Polygons
|
||||
|
private String polygonsDefinition; |
||||
|
|
||||
|
//For Circles
|
||||
|
private Double centerLatitude; |
||||
|
private Double centerLongitude; |
||||
|
private Double range; |
||||
|
private RangeUnit rangeUnit; |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 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.rule.engine.geo; |
||||
|
|
||||
|
public enum PerimeterType { |
||||
|
CIRCLE, POLYGON |
||||
|
} |
||||
@ -0,0 +1,30 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 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.rule.engine.geo; |
||||
|
|
||||
|
public enum RangeUnit { |
||||
|
METER(1000.0), KILOMETER(1.0), FOOT(3280.84), MILE(0.62137), NAUTICAL_MILE(0.539957); |
||||
|
|
||||
|
private final double fromKm; |
||||
|
|
||||
|
RangeUnit(double fromKm) { |
||||
|
this.fromKm = fromKm; |
||||
|
} |
||||
|
|
||||
|
public double fromKm(double v) { |
||||
|
return v * fromKm; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,120 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 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.rule.engine.geo; |
||||
|
|
||||
|
import com.google.gson.Gson; |
||||
|
import com.google.gson.JsonObject; |
||||
|
import com.google.gson.JsonParser; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.thingsboard.rule.engine.api.RuleNode; |
||||
|
import org.thingsboard.rule.engine.api.TbContext; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeException; |
||||
|
import org.thingsboard.server.common.data.DataConstants; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.kv.AttributeKvEntry; |
||||
|
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; |
||||
|
import org.thingsboard.server.common.data.kv.StringDataEntry; |
||||
|
import org.thingsboard.server.common.data.plugin.ComponentType; |
||||
|
import org.thingsboard.server.common.msg.TbMsg; |
||||
|
|
||||
|
import java.util.Collections; |
||||
|
import java.util.HashMap; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
import java.util.Optional; |
||||
|
import java.util.concurrent.ExecutionException; |
||||
|
import java.util.concurrent.TimeUnit; |
||||
|
import java.util.concurrent.TimeoutException; |
||||
|
|
||||
|
/** |
||||
|
* Created by ashvayka on 19.01.18. |
||||
|
*/ |
||||
|
@Slf4j |
||||
|
@RuleNode( |
||||
|
type = ComponentType.ACTION, |
||||
|
name = "gps geofencing events", |
||||
|
configClazz = TbGpsGeofencingActionNodeConfiguration.class, |
||||
|
relationTypes = {"Entered", "Left", "Inside", "Outside"}, |
||||
|
nodeDescription = "Produces incoming messages using GPS based geofencing", |
||||
|
nodeDetails = "Extracts latitude and longitude parameters from incoming message and returns different events based on configuration parameters", |
||||
|
uiResources = {"static/rulenode/rulenode-core-config.js"}, |
||||
|
configDirective = "tbActionNodeGpsGeofencingConfig") |
||||
|
public class TbGpsGeofencingActionNode extends AbstractGeofencingNode<TbGpsGeofencingActionNodeConfiguration> { |
||||
|
|
||||
|
private final Map<EntityId, EntityGeofencingState> entityStates = new HashMap<>(); |
||||
|
private final Gson gson = new Gson(); |
||||
|
private final JsonParser parser = new JsonParser(); |
||||
|
|
||||
|
@Override |
||||
|
public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException { |
||||
|
boolean matches = checkMatches(msg); |
||||
|
long ts = System.currentTimeMillis(); |
||||
|
|
||||
|
EntityGeofencingState entityState = entityStates.computeIfAbsent(msg.getOriginator(), key -> { |
||||
|
try { |
||||
|
Optional<AttributeKvEntry> entry = ctx.getAttributesService() |
||||
|
.find(ctx.getTenantId(), msg.getOriginator(), DataConstants.SERVER_SCOPE, ctx.getNodeId()) |
||||
|
.get(1, TimeUnit.MINUTES); |
||||
|
if (entry.isPresent()) { |
||||
|
JsonObject element = parser.parse(entry.get().getValueAsString()).getAsJsonObject(); |
||||
|
return new EntityGeofencingState(element.get("inside").getAsBoolean(), element.get("stateSwitchTime").getAsLong(), element.get("stayed").getAsBoolean()); |
||||
|
} else { |
||||
|
return new EntityGeofencingState(false, 0L, false); |
||||
|
} |
||||
|
} catch (InterruptedException | TimeoutException | ExecutionException e) { |
||||
|
throw new RuntimeException(e); |
||||
|
} |
||||
|
}); |
||||
|
if (entityState.getStateSwitchTime() == 0L || entityState.isInside() != matches) { |
||||
|
switchState(ctx, msg.getOriginator(), entityState, matches, ts); |
||||
|
ctx.tellNext(msg, matches ? "Entered" : "Left"); |
||||
|
} else if (!entityState.isStayed()) { |
||||
|
long stayTime = ts - entityState.getStateSwitchTime(); |
||||
|
if (stayTime > (entityState.isInside() ? |
||||
|
TimeUnit.valueOf(config.getMinInsideDurationTimeUnit()).toMillis(config.getMinInsideDuration()) : TimeUnit.valueOf(config.getMinOutsideDurationTimeUnit()).toMillis(config.getMinOutsideDuration()))) { |
||||
|
setStaid(ctx, msg.getOriginator(), entityState); |
||||
|
ctx.tellNext(msg, entityState.isInside() ? "Inside" : "Outside"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void switchState(TbContext ctx, EntityId entityId, EntityGeofencingState entityState, boolean matches, long ts) { |
||||
|
entityState.setInside(matches); |
||||
|
entityState.setStateSwitchTime(ts); |
||||
|
entityState.setStayed(false); |
||||
|
persist(ctx, entityId, entityState); |
||||
|
} |
||||
|
|
||||
|
private void setStaid(TbContext ctx, EntityId entityId, EntityGeofencingState entityState) { |
||||
|
entityState.setStayed(true); |
||||
|
persist(ctx, entityId, entityState); |
||||
|
} |
||||
|
|
||||
|
private void persist(TbContext ctx, EntityId entityId, EntityGeofencingState entityState) { |
||||
|
JsonObject object = new JsonObject(); |
||||
|
object.addProperty("inside", entityState.isInside()); |
||||
|
object.addProperty("stateSwitchTime", entityState.getStateSwitchTime()); |
||||
|
object.addProperty("stayed", entityState.isStayed()); |
||||
|
AttributeKvEntry entry = new BaseAttributeKvEntry(new StringDataEntry(ctx.getNodeId(), gson.toJson(object)), System.currentTimeMillis()); |
||||
|
List<AttributeKvEntry> attributeKvEntryList = Collections.singletonList(entry); |
||||
|
ctx.getAttributesService().save(ctx.getTenantId(), entityId, DataConstants.SERVER_SCOPE, attributeKvEntryList); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected Class<TbGpsGeofencingActionNodeConfiguration> getConfigClazz() { |
||||
|
return TbGpsGeofencingActionNodeConfiguration.class; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,49 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 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.rule.engine.geo; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
import org.thingsboard.rule.engine.api.NodeConfiguration; |
||||
|
|
||||
|
import java.util.Collections; |
||||
|
import java.util.List; |
||||
|
import java.util.concurrent.TimeUnit; |
||||
|
|
||||
|
/** |
||||
|
* Created by ashvayka on 19.01.18. |
||||
|
*/ |
||||
|
@Data |
||||
|
public class TbGpsGeofencingActionNodeConfiguration extends TbGpsGeofencingFilterNodeConfiguration { |
||||
|
|
||||
|
private int minInsideDuration; |
||||
|
private int minOutsideDuration; |
||||
|
|
||||
|
private String minInsideDurationTimeUnit; |
||||
|
private String minOutsideDurationTimeUnit; |
||||
|
|
||||
|
@Override |
||||
|
public TbGpsGeofencingActionNodeConfiguration defaultConfiguration() { |
||||
|
TbGpsGeofencingActionNodeConfiguration configuration = new TbGpsGeofencingActionNodeConfiguration(); |
||||
|
configuration.setLatitudeKeyName("latitude"); |
||||
|
configuration.setLongitudeKeyName("longitude"); |
||||
|
configuration.setFetchPerimeterInfoFromMessageMetadata(true); |
||||
|
configuration.setMinInsideDurationTimeUnit(TimeUnit.MINUTES.name()); |
||||
|
configuration.setMinOutsideDurationTimeUnit(TimeUnit.MINUTES.name()); |
||||
|
configuration.setMinInsideDuration(1); |
||||
|
configuration.setMinOutsideDuration(1); |
||||
|
return configuration; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,67 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 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.rule.engine.geo; |
||||
|
|
||||
|
import com.google.gson.JsonArray; |
||||
|
import com.google.gson.JsonElement; |
||||
|
import com.google.gson.JsonObject; |
||||
|
import com.google.gson.JsonParser; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.locationtech.spatial4j.context.jts.JtsSpatialContext; |
||||
|
import org.locationtech.spatial4j.context.jts.JtsSpatialContextFactory; |
||||
|
import org.locationtech.spatial4j.shape.Point; |
||||
|
import org.locationtech.spatial4j.shape.Shape; |
||||
|
import org.locationtech.spatial4j.shape.ShapeFactory; |
||||
|
import org.locationtech.spatial4j.shape.SpatialRelation; |
||||
|
import org.springframework.util.StringUtils; |
||||
|
import org.thingsboard.rule.engine.api.RuleNode; |
||||
|
import org.thingsboard.rule.engine.api.TbContext; |
||||
|
import org.thingsboard.rule.engine.api.TbNode; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeConfiguration; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeException; |
||||
|
import org.thingsboard.rule.engine.api.util.TbNodeUtils; |
||||
|
import org.thingsboard.rule.engine.filter.TbMsgTypeFilterNodeConfiguration; |
||||
|
import org.thingsboard.server.common.data.plugin.ComponentType; |
||||
|
import org.thingsboard.server.common.msg.TbMsg; |
||||
|
|
||||
|
import java.util.Collections; |
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* Created by ashvayka on 19.01.18. |
||||
|
*/ |
||||
|
@Slf4j |
||||
|
@RuleNode( |
||||
|
type = ComponentType.FILTER, |
||||
|
name = "gps geofencing filter", |
||||
|
configClazz = TbGpsGeofencingFilterNodeConfiguration.class, |
||||
|
relationTypes = {"True", "False"}, |
||||
|
nodeDescription = "Filter incoming messages by GPS based geofencing", |
||||
|
nodeDetails = "Extracts latitude and longitude parameters from incoming message and returns 'True' if they are inside configured perimeters, 'False' otherwise.", |
||||
|
uiResources = {"static/rulenode/rulenode-core-config.js"}, |
||||
|
configDirective = "tbFilterNodeGpsGeofencingConfig") |
||||
|
public class TbGpsGeofencingFilterNode extends AbstractGeofencingNode<TbGpsGeofencingFilterNodeConfiguration> { |
||||
|
|
||||
|
@Override |
||||
|
public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException { |
||||
|
ctx.tellNext(msg, checkMatches(msg) ? "True" : "False"); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected Class<TbGpsGeofencingFilterNodeConfiguration> getConfigClazz() { |
||||
|
return TbGpsGeofencingFilterNodeConfiguration.class; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,56 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 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.rule.engine.geo; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
import org.thingsboard.rule.engine.api.NodeConfiguration; |
||||
|
import org.thingsboard.server.common.data.EntityType; |
||||
|
import org.thingsboard.server.common.msg.session.SessionMsgType; |
||||
|
|
||||
|
import java.util.Arrays; |
||||
|
import java.util.Collections; |
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* Created by ashvayka on 19.01.18. |
||||
|
*/ |
||||
|
@Data |
||||
|
public class TbGpsGeofencingFilterNodeConfiguration implements NodeConfiguration<TbGpsGeofencingFilterNodeConfiguration> { |
||||
|
|
||||
|
private String latitudeKeyName; |
||||
|
private String longitudeKeyName; |
||||
|
private boolean fetchPerimeterInfoFromMessageMetadata; |
||||
|
|
||||
|
private PerimeterType perimeterType; |
||||
|
|
||||
|
//For Polygons
|
||||
|
private String polygonsDefinition; |
||||
|
|
||||
|
//For Circles
|
||||
|
private Double centerLatitude; |
||||
|
private Double centerLongitude; |
||||
|
private Double range; |
||||
|
private RangeUnit rangeUnit; |
||||
|
|
||||
|
@Override |
||||
|
public TbGpsGeofencingFilterNodeConfiguration defaultConfiguration() { |
||||
|
TbGpsGeofencingFilterNodeConfiguration configuration = new TbGpsGeofencingFilterNodeConfiguration(); |
||||
|
configuration.setLatitudeKeyName("latitude"); |
||||
|
configuration.setLongitudeKeyName("longitude"); |
||||
|
configuration.setFetchPerimeterInfoFromMessageMetadata(true); |
||||
|
return configuration; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,95 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 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.rule.engine.metadata; |
||||
|
|
||||
|
import com.google.gson.Gson; |
||||
|
import com.google.gson.JsonElement; |
||||
|
import com.google.gson.JsonParser; |
||||
|
import com.google.gson.reflect.TypeToken; |
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.Data; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.thingsboard.rule.engine.api.TbContext; |
||||
|
import org.thingsboard.rule.engine.api.TbNode; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeConfiguration; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeException; |
||||
|
import org.thingsboard.server.common.msg.TbMsg; |
||||
|
import org.thingsboard.server.common.msg.TbMsgMetaData; |
||||
|
|
||||
|
import java.lang.reflect.Type; |
||||
|
import java.util.Map; |
||||
|
import java.util.concurrent.ExecutionException; |
||||
|
|
||||
|
import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; |
||||
|
|
||||
|
@Slf4j |
||||
|
public abstract class TbAbstractGetEntityDetailsNode<C extends TbAbstractGetEntityDetailsNodeConfiguration> implements TbNode { |
||||
|
|
||||
|
private static final Gson gson = new Gson(); |
||||
|
private static final JsonParser jsonParser = new JsonParser(); |
||||
|
private static final Type TYPE = new TypeToken<Map<String, String>>() {}.getType(); |
||||
|
|
||||
|
protected C config; |
||||
|
|
||||
|
@Override |
||||
|
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { |
||||
|
this.config = loadGetEntityDetailsNodeConfiguration(configuration); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException { |
||||
|
try { |
||||
|
ctx.tellNext(getDetails(ctx, msg), SUCCESS); |
||||
|
} catch (Exception e) { |
||||
|
ctx.tellFailure(msg, e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void destroy() {} |
||||
|
|
||||
|
protected abstract C loadGetEntityDetailsNodeConfiguration(TbNodeConfiguration configuration) throws TbNodeException; |
||||
|
|
||||
|
protected abstract TbMsg getDetails(TbContext ctx, TbMsg msg); |
||||
|
|
||||
|
protected MessageData getDataAsJson(TbMsg msg) { |
||||
|
if (this.config.isAddToMetadata()) { |
||||
|
return new MessageData(gson.toJsonTree(msg.getMetaData().getData(), TYPE), "metadata"); |
||||
|
} else { |
||||
|
return new MessageData(jsonParser.parse(msg.getData()), "data"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected TbMsg transformMsg(TbContext ctx, TbMsg msg, JsonElement resultObject, MessageData messageData) { |
||||
|
if (messageData.getDataType().equals("metadata")) { |
||||
|
Map<String, String> metadataMap = gson.fromJson(resultObject.toString(), TYPE); |
||||
|
return ctx.transformMsg(msg, msg.getType(), msg.getOriginator(), new TbMsgMetaData(metadataMap), msg.getData()); |
||||
|
} else { |
||||
|
return ctx.transformMsg(msg, msg.getType(), msg.getOriginator(), msg.getMetaData(), gson.toJson(resultObject)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Data |
||||
|
@AllArgsConstructor |
||||
|
protected static class MessageData { |
||||
|
|
||||
|
private JsonElement data; |
||||
|
private String dataType; |
||||
|
|
||||
|
} |
||||
|
|
||||
|
|
||||
|
} |
||||
@ -0,0 +1,31 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 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.rule.engine.metadata; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
import org.thingsboard.rule.engine.util.EntityDetails; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
@Data |
||||
|
public abstract class TbAbstractGetEntityDetailsNodeConfiguration { |
||||
|
|
||||
|
|
||||
|
private List<EntityDetails> detailsList; |
||||
|
|
||||
|
private boolean addToMetadata; |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,142 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 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.rule.engine.metadata; |
||||
|
|
||||
|
import com.google.gson.JsonElement; |
||||
|
import com.google.gson.JsonObject; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.thingsboard.rule.engine.api.RuleNode; |
||||
|
import org.thingsboard.rule.engine.api.TbContext; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeConfiguration; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeException; |
||||
|
import org.thingsboard.rule.engine.api.util.TbNodeUtils; |
||||
|
import org.thingsboard.rule.engine.util.EntityDetails; |
||||
|
import org.thingsboard.server.common.data.Customer; |
||||
|
import org.thingsboard.server.common.data.Device; |
||||
|
import org.thingsboard.server.common.data.EntityView; |
||||
|
import org.thingsboard.server.common.data.asset.Asset; |
||||
|
import org.thingsboard.server.common.data.id.AssetId; |
||||
|
import org.thingsboard.server.common.data.id.DeviceId; |
||||
|
import org.thingsboard.server.common.data.id.EntityViewId; |
||||
|
import org.thingsboard.server.common.data.plugin.ComponentType; |
||||
|
import org.thingsboard.server.common.msg.TbMsg; |
||||
|
|
||||
|
@Slf4j |
||||
|
@RuleNode(type = ComponentType.ENRICHMENT, |
||||
|
name = "customer details", |
||||
|
configClazz = TbGetCustomerDetailsNodeConfiguration.class, |
||||
|
nodeDescription = "Node find the customer of the message originator and fetch his details that selected from the drop-down list and add them to the message if they exist.", |
||||
|
nodeDetails = "If selected checkbox: <b>Add selected details to the message metadata</b>, existing fields will add to the message metadata instead of message data.<br><br>" + |
||||
|
"<b>Note:</b> only Device, Asset, and Entity View type are allowed.<br><br>" + |
||||
|
"If the originator of the message didn't assign to Customer, or originator type is not supported - Message send via <b>Failure</b> chain, otherwise, <b>Success</b> chain will be used.", |
||||
|
uiResources = {"static/rulenode/rulenode-core-config.js"}, |
||||
|
configDirective = "tbEnrichmentNodeEntityDetailsConfig") |
||||
|
public class TbGetCustomerDetailsNode extends TbAbstractGetEntityDetailsNode<TbGetCustomerDetailsNodeConfiguration> { |
||||
|
|
||||
|
private static final String CUSTOMER_PREFIX = "customer_"; |
||||
|
|
||||
|
@Override |
||||
|
protected TbGetCustomerDetailsNodeConfiguration loadGetEntityDetailsNodeConfiguration(TbNodeConfiguration configuration) throws TbNodeException { |
||||
|
return TbNodeUtils.convert(configuration, TbGetCustomerDetailsNodeConfiguration.class); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected TbMsg getDetails(TbContext ctx, TbMsg msg) { |
||||
|
return getCustomerTbMsg(ctx, msg, getDataAsJson(msg)); |
||||
|
} |
||||
|
|
||||
|
private TbMsg getCustomerTbMsg(TbContext ctx, TbMsg msg, MessageData messageData) { |
||||
|
JsonElement resultObject = null; |
||||
|
if (!config.getDetailsList().isEmpty()) { |
||||
|
for (EntityDetails entityDetails : config.getDetailsList()) { |
||||
|
resultObject = addCustomerProperties(messageData.getData(), getCustomer(ctx, msg), entityDetails); |
||||
|
} |
||||
|
return transformMsg(ctx, msg, resultObject, messageData); |
||||
|
} else { |
||||
|
return msg; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private Customer getCustomer(TbContext ctx, TbMsg msg) { |
||||
|
switch (msg.getOriginator().getEntityType()) { |
||||
|
case DEVICE: |
||||
|
Device device = ctx.getDeviceService().findDeviceById(ctx.getTenantId(), new DeviceId(msg.getOriginator().getId())); |
||||
|
if (!device.getCustomerId().isNullUid()) { |
||||
|
return ctx.getCustomerService().findCustomerById(ctx.getTenantId(), device.getCustomerId()); |
||||
|
} else { |
||||
|
throw new RuntimeException("Device with name '" + device.getName() + "' didn't assign to Customer."); |
||||
|
} |
||||
|
case ASSET: |
||||
|
Asset asset = ctx.getAssetService().findAssetById(ctx.getTenantId(), new AssetId(msg.getOriginator().getId())); |
||||
|
if (!asset.getCustomerId().isNullUid()) { |
||||
|
return ctx.getCustomerService().findCustomerById(ctx.getTenantId(), asset.getCustomerId()); |
||||
|
} else { |
||||
|
throw new RuntimeException("Asset with name '" + asset.getName() + "' didn't assign to Customer."); |
||||
|
} |
||||
|
case ENTITY_VIEW: |
||||
|
EntityView entityView = ctx.getEntityViewService().findEntityViewById(ctx.getTenantId(), new EntityViewId(msg.getOriginator().getId())); |
||||
|
if (!entityView.getCustomerId().isNullUid()) { |
||||
|
return ctx.getCustomerService().findCustomerById(ctx.getTenantId(), entityView.getCustomerId()); |
||||
|
} else { |
||||
|
throw new RuntimeException("EntityView with name '" + entityView.getName() + "' didn't assign to Customer."); |
||||
|
} |
||||
|
default: |
||||
|
throw new RuntimeException("Entity with entityType '" + msg.getOriginator().getEntityType() + "' can't be assigned to Customer."); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private JsonElement addCustomerProperties(JsonElement data, Customer customer, EntityDetails entityDetails) { |
||||
|
JsonObject dataAsObject = data.getAsJsonObject(); |
||||
|
switch (entityDetails) { |
||||
|
case ADDRESS: |
||||
|
if (customer.getAddress() != null) |
||||
|
dataAsObject.addProperty(CUSTOMER_PREFIX + "address", customer.getAddress()); |
||||
|
break; |
||||
|
case ADDRESS2: |
||||
|
if (customer.getAddress2() != null) |
||||
|
dataAsObject.addProperty(CUSTOMER_PREFIX + "address2", customer.getAddress2()); |
||||
|
break; |
||||
|
case CITY: |
||||
|
if (customer.getCity() != null) dataAsObject.addProperty(CUSTOMER_PREFIX + "city", customer.getCity()); |
||||
|
break; |
||||
|
case COUNTRY: |
||||
|
if (customer.getCountry() != null) |
||||
|
dataAsObject.addProperty(CUSTOMER_PREFIX + "country", customer.getCountry()); |
||||
|
break; |
||||
|
case STATE: |
||||
|
if (customer.getState() != null) |
||||
|
dataAsObject.addProperty(CUSTOMER_PREFIX + "state", customer.getState()); |
||||
|
break; |
||||
|
case EMAIL: |
||||
|
if (customer.getEmail() != null) |
||||
|
dataAsObject.addProperty(CUSTOMER_PREFIX + "email", customer.getEmail()); |
||||
|
break; |
||||
|
case PHONE: |
||||
|
if (customer.getPhone() != null) |
||||
|
dataAsObject.addProperty(CUSTOMER_PREFIX + "phone", customer.getPhone()); |
||||
|
break; |
||||
|
case ZIP: |
||||
|
if (customer.getZip() != null) dataAsObject.addProperty(CUSTOMER_PREFIX + "zip", customer.getZip()); |
||||
|
break; |
||||
|
case ADDITIONAL_INFO: |
||||
|
if (customer.getAdditionalInfo().hasNonNull("description")) { |
||||
|
dataAsObject.addProperty(CUSTOMER_PREFIX + "additionalInfo", customer.getAdditionalInfo().get("description").asText()); |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
return dataAsObject; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 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.rule.engine.metadata; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
import org.thingsboard.rule.engine.api.NodeConfiguration; |
||||
|
|
||||
|
import java.util.Collections; |
||||
|
|
||||
|
@Data |
||||
|
public class TbGetCustomerDetailsNodeConfiguration extends TbAbstractGetEntityDetailsNodeConfiguration implements NodeConfiguration<TbGetCustomerDetailsNodeConfiguration> { |
||||
|
|
||||
|
|
||||
|
@Override |
||||
|
public TbGetCustomerDetailsNodeConfiguration defaultConfiguration() { |
||||
|
TbGetCustomerDetailsNodeConfiguration configuration = new TbGetCustomerDetailsNodeConfiguration(); |
||||
|
configuration.setDetailsList(Collections.emptyList()); |
||||
|
return configuration; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,105 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 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.rule.engine.metadata; |
||||
|
|
||||
|
import com.google.gson.JsonElement; |
||||
|
import com.google.gson.JsonObject; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.thingsboard.rule.engine.api.RuleNode; |
||||
|
import org.thingsboard.rule.engine.api.TbContext; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeConfiguration; |
||||
|
import org.thingsboard.rule.engine.api.TbNodeException; |
||||
|
import org.thingsboard.rule.engine.api.util.TbNodeUtils; |
||||
|
import org.thingsboard.rule.engine.util.EntityDetails; |
||||
|
import org.thingsboard.server.common.data.Tenant; |
||||
|
import org.thingsboard.server.common.data.plugin.ComponentType; |
||||
|
import org.thingsboard.server.common.msg.TbMsg; |
||||
|
|
||||
|
@Slf4j |
||||
|
@RuleNode(type = ComponentType.ENRICHMENT, |
||||
|
name = "tenant details", |
||||
|
configClazz = TbGetTenantDetailsNodeConfiguration.class, |
||||
|
nodeDescription = "Node fetch current Tenant details that selected from the drop-down list and add them to the message if they exist.", |
||||
|
nodeDetails = "If selected checkbox: <b>Add selected details to the message metadata</b>, existing fields will add to the message metadata instead of message data.", |
||||
|
uiResources = {"static/rulenode/rulenode-core-config.js"}, |
||||
|
configDirective = "tbEnrichmentNodeEntityDetailsConfig") |
||||
|
public class TbGetTenantDetailsNode extends TbAbstractGetEntityDetailsNode<TbGetTenantDetailsNodeConfiguration> { |
||||
|
|
||||
|
|
||||
|
private static final String TENANT_PREFIX = "tenant_"; |
||||
|
|
||||
|
@Override |
||||
|
protected TbGetTenantDetailsNodeConfiguration loadGetEntityDetailsNodeConfiguration(TbNodeConfiguration configuration) throws TbNodeException { |
||||
|
return TbNodeUtils.convert(configuration, TbGetTenantDetailsNodeConfiguration.class); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected TbMsg getDetails(TbContext ctx, TbMsg msg) { |
||||
|
return getTenantTbMsg(ctx, msg, getDataAsJson(msg)); |
||||
|
} |
||||
|
|
||||
|
private TbMsg getTenantTbMsg(TbContext ctx, TbMsg msg, MessageData messageData) { |
||||
|
JsonElement resultObject = null; |
||||
|
Tenant tenant = ctx.getTenantService().findTenantById(ctx.getTenantId()); |
||||
|
if (!config.getDetailsList().isEmpty()) { |
||||
|
for (EntityDetails entityDetails : config.getDetailsList()) { |
||||
|
resultObject = addTenantProperties(messageData.getData(), tenant, entityDetails); |
||||
|
} |
||||
|
return transformMsg(ctx, msg, resultObject, messageData); |
||||
|
} else { |
||||
|
return msg; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private JsonElement addTenantProperties(JsonElement data, Tenant tenant, EntityDetails entityDetails) { |
||||
|
JsonObject dataAsObject = data.getAsJsonObject(); |
||||
|
switch (entityDetails) { |
||||
|
case ADDRESS: |
||||
|
if (tenant.getAddress() != null) |
||||
|
dataAsObject.addProperty(TENANT_PREFIX + "address", tenant.getAddress()); |
||||
|
break; |
||||
|
case ADDRESS2: |
||||
|
if (tenant.getAddress2() != null) |
||||
|
dataAsObject.addProperty(TENANT_PREFIX + "address2", tenant.getAddress2()); |
||||
|
break; |
||||
|
case CITY: |
||||
|
if (tenant.getCity() != null) dataAsObject.addProperty(TENANT_PREFIX + "city", tenant.getCity()); |
||||
|
break; |
||||
|
case COUNTRY: |
||||
|
if (tenant.getCountry() != null) |
||||
|
dataAsObject.addProperty(TENANT_PREFIX + "country", tenant.getCountry()); |
||||
|
break; |
||||
|
case STATE: |
||||
|
if (tenant.getState() != null) dataAsObject.addProperty(TENANT_PREFIX + "state", tenant.getState()); |
||||
|
break; |
||||
|
case EMAIL: |
||||
|
if (tenant.getEmail() != null) dataAsObject.addProperty(TENANT_PREFIX + "email", tenant.getEmail()); |
||||
|
break; |
||||
|
case PHONE: |
||||
|
if (tenant.getPhone() != null) dataAsObject.addProperty(TENANT_PREFIX + "phone", tenant.getPhone()); |
||||
|
break; |
||||
|
case ZIP: |
||||
|
if (tenant.getZip() != null) dataAsObject.addProperty(TENANT_PREFIX + "zip", tenant.getZip()); |
||||
|
break; |
||||
|
case ADDITIONAL_INFO: |
||||
|
if (tenant.getAdditionalInfo().hasNonNull("description")) { |
||||
|
dataAsObject.addProperty(TENANT_PREFIX + "additionalInfo", tenant.getAdditionalInfo().get("description").asText()); |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
return dataAsObject; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 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.rule.engine.metadata; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
import org.thingsboard.rule.engine.api.NodeConfiguration; |
||||
|
|
||||
|
import java.util.Collections; |
||||
|
|
||||
|
@Data |
||||
|
public class TbGetTenantDetailsNodeConfiguration extends TbAbstractGetEntityDetailsNodeConfiguration implements NodeConfiguration<TbGetTenantDetailsNodeConfiguration> { |
||||
|
|
||||
|
|
||||
|
@Override |
||||
|
public TbGetTenantDetailsNodeConfiguration defaultConfiguration() { |
||||
|
TbGetTenantDetailsNodeConfiguration configuration = new TbGetTenantDetailsNodeConfiguration(); |
||||
|
configuration.setDetailsList(Collections.emptyList()); |
||||
|
return configuration; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 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.rule.engine.util; |
||||
|
|
||||
|
public enum EntityDetails { |
||||
|
|
||||
|
COUNTRY, CITY, STATE, ZIP, ADDRESS, ADDRESS2, PHONE, EMAIL, ADDITIONAL_INFO |
||||
|
|
||||
|
} |
||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,23 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 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"; |
||||
|
|
||||
|
md-card.settings-card { |
||||
|
@media (min-width: $layout-breakpoint-sm) { |
||||
|
width: 60%; |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
@ -0,0 +1,301 @@ |
|||||
|
/* |
||||
|
* Copyright © 2016-2019 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. |
||||
|
*/ |
||||
|
/* eslint-disable import/no-unresolved, import/default */ |
||||
|
import widgetTpl from './date-range-navigator.tpl.html'; |
||||
|
import './date-range-navigator.scss'; |
||||
|
/* eslint-enable import/no-unresolved, import/default */ |
||||
|
|
||||
|
export default angular.module('thingsboard.widgets.dateRangeNavigator', []) |
||||
|
.directive('dateRangeNavigatorWidget', DateRangeNavigatorWidget) |
||||
|
.name; |
||||
|
|
||||
|
/*@ngInject*/ |
||||
|
function DateRangeNavigatorWidget() { |
||||
|
return { |
||||
|
restrict: "E", |
||||
|
scope: true, |
||||
|
bindToController: { |
||||
|
tableId: '=', |
||||
|
ctx: '=' |
||||
|
}, |
||||
|
controller: DateRangeNavigatorWidgetController, |
||||
|
controllerAs: 'vm', |
||||
|
templateUrl: widgetTpl |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/*@ngInject*/ |
||||
|
function DateRangeNavigatorWidgetController($scope, $window, $filter) { |
||||
|
|
||||
|
let vm = this, |
||||
|
hour = 3600000, |
||||
|
day = 86400000, |
||||
|
week = 604800000, |
||||
|
month = 2629743000, |
||||
|
words = [ |
||||
|
"Mon", |
||||
|
"Tue", |
||||
|
"Wed", |
||||
|
"Thu", |
||||
|
"Fri", |
||||
|
"Sat", |
||||
|
"Sun", |
||||
|
"January", |
||||
|
"February", |
||||
|
"March", |
||||
|
"April", |
||||
|
"May", |
||||
|
"June", |
||||
|
"July", |
||||
|
"August", |
||||
|
"September", |
||||
|
"October", |
||||
|
"November", |
||||
|
"December", |
||||
|
"Ok", |
||||
|
'Custom Date Range', |
||||
|
'Date Range Template', |
||||
|
'Today', |
||||
|
'Yesterday', |
||||
|
'This Week', |
||||
|
'Last Week', |
||||
|
'Month', |
||||
|
'This Month', |
||||
|
'Last Month', |
||||
|
'Year', |
||||
|
'This Year', |
||||
|
'Last Year' |
||||
|
], |
||||
|
firstUpdate = true; |
||||
|
|
||||
|
$scope.datesMap = { |
||||
|
hour: { |
||||
|
ts: hour, |
||||
|
label: "Hour" |
||||
|
}, |
||||
|
day: { |
||||
|
ts: day, |
||||
|
label: "Day" |
||||
|
}, |
||||
|
week: { |
||||
|
ts: week, |
||||
|
label: "Week" |
||||
|
}, |
||||
|
twoWeeks: { |
||||
|
ts: week * 2, |
||||
|
label: "2 weeks" |
||||
|
}, |
||||
|
month: { |
||||
|
ts: month, |
||||
|
label: "Month" |
||||
|
}, |
||||
|
threeMonths: { |
||||
|
ts: month * 3, |
||||
|
label: "3 months" |
||||
|
}, |
||||
|
sixMonths: { |
||||
|
ts: month * 6, |
||||
|
label: "6 months" |
||||
|
} |
||||
|
}; |
||||
|
$scope.advancedModel = {}; |
||||
|
$scope.endRestrictionDate = Date.now(); |
||||
|
$scope.localizationMap = getLocalizationMap(); |
||||
|
|
||||
|
$scope.changeInterval = changeInterval; |
||||
|
$scope.goForth = goForth; |
||||
|
$scope.goBack = goBack; |
||||
|
$scope.triggerChange = triggerChange; |
||||
|
|
||||
|
$scope.$watch('vm.ctx', function () { |
||||
|
if (vm.ctx && vm.ctx.dashboard.dashboardTimewindow) { |
||||
|
$scope.settings = vm.ctx.widgetConfig.settings; |
||||
|
let selection; |
||||
|
if ($scope.settings.useSessionStorage) { |
||||
|
selection = readFromStorage('date-range'); |
||||
|
} |
||||
|
if (selection) { |
||||
|
$scope.advancedModel = { |
||||
|
selectedTemplateName: selection.name, |
||||
|
dateStart: selection.start, |
||||
|
dateEnd: selection.end |
||||
|
}; |
||||
|
} else { |
||||
|
let end = new Date(); |
||||
|
end.setHours(23, 59, 59, 999); |
||||
|
|
||||
|
let formattedDate = getFormattedDate( |
||||
|
(end.getTime() + 1) - $scope.datesMap[$scope.settings.initialInterval || "week"].ts, |
||||
|
end.getTime() |
||||
|
); |
||||
|
$scope.advancedModel = formattedDate; |
||||
|
} |
||||
|
$scope.selectedStepSize = $scope.datesMap[$scope.settings.stepSize || "day"].ts; |
||||
|
|
||||
|
widgetContextTimewindowSync(); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
$scope.$on('dashboardTimewindowChanged', function () { |
||||
|
$scope.dashboardTimewindowChanged = true; |
||||
|
widgetContextTimewindowSync(); |
||||
|
}); |
||||
|
|
||||
|
function getLocalizationMap() { |
||||
|
let result = {}; |
||||
|
|
||||
|
words.forEach(function (key) { |
||||
|
result[key] = $filter('translate')('widgets.date-range-navigator.localizationMap.' + key); |
||||
|
}); |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
function triggerChange() { |
||||
|
updateTimewindow($scope.advancedModel.dateStart.getTime(), $scope.advancedModel.dateEnd.getTime() + day - 1); |
||||
|
} |
||||
|
|
||||
|
function widgetContextTimewindowSync() { |
||||
|
if (vm.ctx && vm.ctx.dashboardTimewindow && $scope.dashboardTimewindowChanged && |
||||
|
vm.ctx.dashboard.dashboardTimewindow.history && |
||||
|
vm.ctx.dashboard.dashboardTimewindow.history.fixedTimewindow) { |
||||
|
|
||||
|
|
||||
|
if (!firstUpdate) { |
||||
|
updateAdvancedModel(); |
||||
|
} |
||||
|
updateDateInterval(); |
||||
|
if ($scope.settings.useSessionStorage) { |
||||
|
updateStorageDate(); |
||||
|
} |
||||
|
if (firstUpdate) { |
||||
|
updateTimewindow($scope.advancedModel.dateStart.getTime(), $scope.advancedModel.dateEnd.getTime()); |
||||
|
firstUpdate = false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function getFormattedDate(startTime, endTime) { |
||||
|
var template; |
||||
|
|
||||
|
let startDate = new Date(startTime); |
||||
|
let endDate = new Date(endTime); |
||||
|
|
||||
|
if (getDateDiff(startDate, endDate) === 0) { |
||||
|
template = $filter('date')(startDate, 'dd MMM yyyy'); |
||||
|
} else { |
||||
|
template = $filter('date')( |
||||
|
startDate, |
||||
|
'dd' + (startDate.getMonth() !== endDate.getMonth() || startDate.getFullYear() !== endDate.getFullYear() ? ' MMM' : '') + (startDate.getFullYear() !== endDate.getFullYear() ? ' yyyy' : '') |
||||
|
) |
||||
|
+ ' - ' + |
||||
|
$filter('date')( |
||||
|
endDate, |
||||
|
'dd MMM yyyy' |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
selectedTemplateName: template, |
||||
|
dateStart: startDate, |
||||
|
dateEnd: endDate |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
function readFromStorage(itemKey) { |
||||
|
if ($window.sessionStorage.getItem(itemKey)) { |
||||
|
let selection = angular.fromJson($window.sessionStorage.getItem(itemKey)); |
||||
|
selection.start = new Date(parseInt(selection.start)); |
||||
|
selection.end = new Date(parseInt(selection.end)); |
||||
|
return selection; |
||||
|
} |
||||
|
|
||||
|
return undefined; |
||||
|
} |
||||
|
|
||||
|
function goForth() { |
||||
|
let startTime = vm.ctx.dashboard.dashboardTimewindow.history ? |
||||
|
vm.ctx.dashboard.dashboardTimewindow.history.fixedTimewindow.startTimeMs : |
||||
|
$scope.advancedModel.dateStart.getTime(); |
||||
|
let endTime = vm.ctx.dashboard.dashboardTimewindow.history ? |
||||
|
vm.ctx.dashboard.dashboardTimewindow.history.fixedTimewindow.endTimeMs : |
||||
|
$scope.advancedModel.dateEnd.getTime(); |
||||
|
updateTimewindow(startTime + $scope.selectedStepSize, endTime + $scope.selectedStepSize); |
||||
|
} |
||||
|
|
||||
|
function goBack() { |
||||
|
let startTime = vm.ctx.dashboard.dashboardTimewindow.history ? |
||||
|
vm.ctx.dashboard.dashboardTimewindow.history.fixedTimewindow.startTimeMs : |
||||
|
$scope.advancedModel.dateStart.getTime(); |
||||
|
let endTime = vm.ctx.dashboard.dashboardTimewindow.history ? |
||||
|
vm.ctx.dashboard.dashboardTimewindow.history.fixedTimewindow.endTimeMs : |
||||
|
$scope.advancedModel.dateEnd.getTime(); |
||||
|
updateTimewindow(startTime - $scope.selectedStepSize, endTime - $scope.selectedStepSize); |
||||
|
} |
||||
|
|
||||
|
function changeInterval() { |
||||
|
let endTime = vm.ctx.dashboard.dashboardTimewindow.history ? |
||||
|
vm.ctx.dashboard.dashboardTimewindow.history.fixedTimewindow.endTimeMs : |
||||
|
$scope.advancedModel.dateEnd.getTime(); |
||||
|
updateTimewindow(endTime - $scope.selectedDateInterval / 2, endTime + $scope.selectedDateInterval / 2); |
||||
|
} |
||||
|
|
||||
|
function getDateDiff(date1, date2) { |
||||
|
if (!date1 || !date2) return; |
||||
|
var _d1 = new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()), |
||||
|
_d2 = new Date(date2.getFullYear(), date2.getMonth(), date2.getDate()); |
||||
|
return _d2 - _d1; |
||||
|
} |
||||
|
|
||||
|
function updateTimewindow(startTime, endTime) { |
||||
|
vm.ctx.dashboard.dashboardTimewindowApi.onUpdateTimewindow(startTime, endTime, 10); |
||||
|
} |
||||
|
|
||||
|
function updateDateInterval() { |
||||
|
let interval = $scope.advancedModel.dateEnd.getTime() - $scope.advancedModel.dateStart.getTime(); |
||||
|
|
||||
|
for (let i in $scope.datesMap) { |
||||
|
if ($scope.datesMap.hasOwnProperty(i)) { |
||||
|
if ($scope.datesMap[i].ts === interval || $scope.datesMap[i].ts === interval + 1 || $scope.datesMap[i].ts === interval - 1) { |
||||
|
$scope.selectedDateInterval = $scope.datesMap[i].ts; |
||||
|
$scope.customInterval = false; |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
$scope.selectedDateInterval = interval; |
||||
|
$scope.customInterval = {ts: interval, label: "Custom interval"}; |
||||
|
} |
||||
|
|
||||
|
function updateAdvancedModel() { |
||||
|
$scope.advancedModel = getFormattedDate(vm.ctx.dashboard.dashboardTimewindow.history.fixedTimewindow.startTimeMs, vm.ctx.dashboard.dashboardTimewindow.history.fixedTimewindow.endTimeMs); |
||||
|
} |
||||
|
|
||||
|
function updateStorageDate() { |
||||
|
saveIntoStorage('date-range', { |
||||
|
start: $scope.advancedModel.dateStart.getTime(), |
||||
|
end: $scope.advancedModel.dateEnd.getTime(), |
||||
|
name: $scope.advancedModel.selectedTemplateName |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function saveIntoStorage(keyName, selection) { |
||||
|
if (selection) { |
||||
|
$window.sessionStorage.setItem(keyName, angular.toJson(selection)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,108 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 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. |
||||
|
*/ |
||||
|
.date-range-navigator-widget { |
||||
|
display: flex; |
||||
|
height: 100%; |
||||
|
} |
||||
|
|
||||
|
.date-range-navigator { |
||||
|
display: flex; |
||||
|
flex-wrap: wrap; |
||||
|
align-items: center; |
||||
|
justify-content: space-evenly; |
||||
|
width: 100%; |
||||
|
margin: auto; |
||||
|
|
||||
|
.drn__element { |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
align-items: center; |
||||
|
max-width: 100%; |
||||
|
height: 60px; |
||||
|
margin: 4px 0; |
||||
|
} |
||||
|
|
||||
|
.navigation { |
||||
|
md-input-container { |
||||
|
margin: 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.picker { |
||||
|
.picker__wrapper { |
||||
|
position: relative; |
||||
|
max-width: 100%; |
||||
|
padding: 2px; |
||||
|
|
||||
|
> label { |
||||
|
position: absolute; |
||||
|
right: -3px; |
||||
|
bottom: 100%; |
||||
|
left: 0; |
||||
|
padding-left: 3px; |
||||
|
color: #787878; |
||||
|
transform: scale(.75); |
||||
|
transform-origin: left bottom; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.md-select-value { |
||||
|
min-width: 225px; |
||||
|
border-color: #e1e1e1; |
||||
|
|
||||
|
.md-select-icon { |
||||
|
color: #757575; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&.short-mode { |
||||
|
display: block; |
||||
|
width: 90%; |
||||
|
|
||||
|
.drn__element { |
||||
|
width: 100%; |
||||
|
|
||||
|
md-input-container { |
||||
|
flex: 1; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.picker { |
||||
|
.picker__wrapper { |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
.md-select-value { |
||||
|
min-width: initial; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&.labels-hidden { |
||||
|
.drn__element { |
||||
|
margin: 0; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&.long-mode { |
||||
|
&.labels-hidden { |
||||
|
.drn__element { |
||||
|
height: 36px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,72 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2019 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="date-range-navigator" |
||||
|
ng-class="{'short-mode':vm.ctx.width < 400, 'labels-hidden': settings.hideLabels, 'long-mode': vm.ctx.width >= 400}" |
||||
|
> |
||||
|
<div class="drn__element picker" ng-hide="settings.hidePicker" ng-if="vm.ctx"> |
||||
|
<div class="picker__wrapper"> |
||||
|
<label ng-hide="settings.hideLabels" ng-bind="'widgets.date-range-navigator.localizationMap.Date picker' | translate"></label> |
||||
|
<md-date-range ng-model="advancedModel" |
||||
|
md-on-select="triggerChange()" |
||||
|
localization-map="localizationMap" |
||||
|
one-panel="settings.onePanel" |
||||
|
auto-confirm="settings.autoConfirm" |
||||
|
show-template="settings.showTemplate" |
||||
|
first-day-of-week="settings.firstDayOfWeek" |
||||
|
></md-date-range> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="drn__element navigation" ng-hide="settings.hideInterval"> |
||||
|
<md-input-container class="md-block" |
||||
|
flex-gt-sm |
||||
|
> |
||||
|
<label ng-hide="settings.hideLabels" ng-bind="'widgets.date-range-navigator.localizationMap.Interval' | translate"></label> |
||||
|
<md-select ng-model="selectedDateInterval" |
||||
|
ng-change="changeInterval()" |
||||
|
aria-label="Pick date interval" |
||||
|
> |
||||
|
<md-option ng-if="customInterval" ng-value="customInterval.ts">{{'widgets.date-range-navigator.localizationMap.' + customInterval.label | translate}}</md-option> |
||||
|
<md-option ng-repeat="(dateKey, dateValue) in datesMap" ng-value="dateValue.ts"> |
||||
|
{{'widgets.date-range-navigator.localizationMap.' + dateValue.label | translate}} |
||||
|
</md-option> |
||||
|
</md-select> |
||||
|
</md-input-container> |
||||
|
</div> |
||||
|
|
||||
|
<div class="drn__element step" ng-hide="settings.hideStepSize"> |
||||
|
<md-button ng-click="goBack()" class="md-icon-button"> |
||||
|
<md-icon>keyboard_arrow_left</md-icon> |
||||
|
</md-button> |
||||
|
<md-input-container class="md-block" |
||||
|
flex-gt-sm |
||||
|
> |
||||
|
<label ng-hide="settings.hideLabels" ng-bind="'widgets.date-range-navigator.localizationMap.Step size' | translate"></label> |
||||
|
<md-select ng-model="selectedStepSize" |
||||
|
aria-label="Pick date interval" |
||||
|
> |
||||
|
<md-option ng-repeat="(dateKey, dateValue) in datesMap" ng-value="dateValue.ts"> |
||||
|
{{'widgets.date-range-navigator.localizationMap.'+dateValue.label | translate}} |
||||
|
</md-option> |
||||
|
</md-select> |
||||
|
</md-input-container> |
||||
|
<md-button ng-click="goForth()" class="md-icon-button"> |
||||
|
<md-icon>keyboard_arrow_right</md-icon> |
||||
|
</md-button> |
||||
|
</div> |
||||
|
</div> |
||||
Loading…
Reference in new issue