/*
* AndFHEM - Open Source Android application to control a FHEM home automation
* server.
*
* Copyright (c) 2011, Matthias Klass or third-party contributors as
* indicated by the @author tags or express copyright attribution
* statements applied by the authors. All third-party contributions are
* distributed under license by Red Hat Inc.
*
* This copyrighted material is made available to anyone wishing to use, modify,
* copy, or redistribute it subject to the terms and conditions of the GNU GENERAL PUBLIC LICENSE, as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU GENERAL PUBLIC LICENSE
* for more details.
*
* You should have received a copy of the GNU GENERAL PUBLIC LICENSE
* along with this distribution; if not, write to:
* Free Software Foundation, Inc.
* 51 Franklin Street, Fifth Floor
* Boston, MA 02110-1301 USA
*/
package li.klass.fhem.service.graph.gplot;
import android.support.annotation.NonNull;
import com.google.common.base.Charsets;
import com.google.common.base.Optional;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Range;
import com.google.common.io.Resources;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Queue;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.inject.Singleton;
import li.klass.fhem.service.graph.gplot.GPlotSeries.SeriesColor;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Maps.newHashMap;
import static java.util.Collections.emptyMap;
@Singleton
public class GPlotParser {
public static final Pattern SETS_PATTERN = Pattern.compile("set ([a-zA-Z0-9]+) [\"'\\[]?([^\"^']+)[\"'\\]]?");
public static final Pattern AXIS_PATTERN = Pattern.compile("axes x1y([12])");
public static final Pattern TITLE_PATTERN = Pattern.compile("title '([^']*)'");
public static final Pattern TYPE_PATTERN = Pattern.compile("with ([a-zA-Z]+)");
public static final Pattern SERIES_TYPE_PATTERN = Pattern.compile("(l[0-9])((dot|fill(_stripe|_gyr)?)?)");
public static final Pattern LINE_WIDTH_PATTERN = Pattern.compile("lw ([0-9]+(\\.[0-9]+)?)");
private static final Logger LOGGER = LoggerFactory.getLogger(GPlotParser.class);
private ImmutableMap<String, SeriesColor> TO_COLOR = ImmutableMap.<String, SeriesColor>builder()
.put("l0", SeriesColor.RED)
.put("l1", SeriesColor.GREEN)
.put("l2", SeriesColor.BLUE)
.put("l3", SeriesColor.MAGENTA)
.put("l4", SeriesColor.BROWN)
.put("l5", SeriesColor.WHITE)
.put("l6", SeriesColor.OLIVE)
.put("l7", SeriesColor.GRAY)
.put("l8", SeriesColor.YELLOW)
.build();
@Inject
public GPlotParser() {
}
public Optional<GPlotDefinition> parseSafe(String content) {
try {
return Optional.of(parse(content));
} catch (Exception e) {
LOGGER.warn("parseSafe() - cannot parse: \r\n" + content, e);
return Optional.absent();
}
}
public GPlotDefinition parse(String content) {
List<String> lines = newArrayList(content.split("[\\r\\n]"));
Map<String, String> setsDeclarations = extractSetsFrom(lines);
GPlotDefinition definition = new GPlotDefinition();
definition.setLeftAxis(createAxis(setsDeclarations, "y"));
definition.setRightAxis(createAxis(setsDeclarations, "y2"));
List<GPlotSeries> series = extractSeriesFrom(lines);
for (GPlotSeries s : series) {
(s.getAxis() == GPlotSeries.Axis.LEFT ? definition.getLeftAxis() : definition.getRightAxis())
.addSeries(s);
}
return definition;
}
private GPlotAxis createAxis(Map<String, String> setsDeclarations, String prefix) {
String labelKey = prefix + "label";
String rightLabel = setsDeclarations.containsKey(labelKey) ?
setsDeclarations.get(labelKey) : "";
String rangeKey = prefix + "range";
Optional<Range<Double>> optRange = Optional.absent();
if (setsDeclarations.containsKey(rangeKey)) {
String rangeValue = setsDeclarations.get(rangeKey).replaceAll("[\\[\\]]", "")
.replace("min", "")
.replace("max", "")
.trim();
String[] parts = rangeValue.split(":");
optRange = calculateRange(rangeValue, parts);
}
return new GPlotAxis(rightLabel, optRange);
}
@NonNull
private Optional<Range<Double>> calculateRange(String rangeValue, String[] parts) {
if (Strings.isNullOrEmpty(rangeValue) || rangeValue.equals(":")) {
return Optional.absent();
} else if (rangeValue.startsWith(":")) {
return Optional.of(Range.atMost(Double.parseDouble(parts[0])));
} else if (rangeValue.endsWith(":")) {
return Optional.of(Range.atLeast(Double.parseDouble(parts[0])));
} else {
return Optional.of(Range.closed(Double.parseDouble(parts[0]), Double.parseDouble(parts[1])));
}
}
private List<GPlotSeries> extractSeriesFrom(List<String> lines) {
List<GPlotSeries> result = newArrayList();
Queue<GPlotSeries.Builder> builders = new LinkedList<>();
List<SeriesColor> colors = new ArrayList<>(Arrays.asList(SeriesColor.values()));
boolean plotFound = false;
for (String line : lines) {
line = line.trim();
if (line.startsWith("plot")) {
plotFound = true;
}
String[] spaceSeparatedParts = line.split(" ");
if (line.startsWith("#")
&& spaceSeparatedParts.length == 2
&& !spaceSeparatedParts[0].matches("[#]+[ ]*")
&& spaceSeparatedParts[1].contains(":")) {
builders.add(new GPlotSeries.Builder().withLogDef(spaceSeparatedParts[1]));
} else if (plotFound) {
GPlotSeries.Builder builder = builders.peek();
if (builder == null) {
LOGGER.error("extractSeriesFrom - builder is null");
break;
}
boolean attributeFound = handleAxis(line, builder);
attributeFound = handleTitle(line, builder) | attributeFound;
attributeFound = handleLineType(line, builder) | attributeFound;
attributeFound = handleSeriesType(line, builder, colors) | attributeFound;
attributeFound = handleLineWidth(line, builder) | attributeFound;
LOGGER.trace("extractSeriesFrom - builder is " + builder);
if (attributeFound) {
if (!builder.isColorSet()) {
SeriesColor color = Iterables.getFirst(colors, SeriesColor.RED);
builder.withColor(color);
colors.remove(color);
}
result.add(builder.build());
builders.remove();
}
}
}
return result;
}
private boolean handleLineWidth(String line, GPlotSeries.Builder builder) {
Matcher matcher = LINE_WIDTH_PATTERN.matcher(line);
if (matcher.find()) {
float lineWidth = Float.parseFloat(matcher.group(1));
builder.withLineWith(lineWidth);
return true;
}
return false;
}
private boolean handleSeriesType(String line, GPlotSeries.Builder builder, List<SeriesColor> colors) {
Matcher matcher = SERIES_TYPE_PATTERN.matcher(line);
if (matcher.find()) {
String colorDesc = matcher.group(1);
String fillDesc = matcher.group(2);
GPlotSeries.SeriesType seriesType = GPlotSeries.SeriesType.DEFAULT;
if (fillDesc.contains("fill")) {
seriesType = GPlotSeries.SeriesType.FILL;
} else if (fillDesc.contains("dot")) {
seriesType = GPlotSeries.SeriesType.DOT;
}
SeriesColor color = TO_COLOR.get(colorDesc);
colors.remove(color);
builder.withColor(color);
builder.withSeriesType(seriesType);
return true;
}
return false;
}
private boolean handleLineType(String line, GPlotSeries.Builder builder) {
Matcher typeMatcher = TYPE_PATTERN.matcher(line);
if (typeMatcher.find()) {
try {
builder.withLineType(GPlotSeries.LineType.valueOf(typeMatcher.group(1).toUpperCase(Locale.getDefault())));
return true;
} catch (IllegalArgumentException e) {
LOGGER.debug("cannot find type for {}", typeMatcher.group(1));
}
}
return false;
}
private boolean handleTitle(String line, GPlotSeries.Builder builder) {
Matcher titleMatcher = TITLE_PATTERN.matcher(line);
if (titleMatcher.find()) {
builder.withTitle(titleMatcher.group(1));
return true;
}
return false;
}
private boolean handleAxis(String line, GPlotSeries.Builder builder) {
Matcher axesMatcher = AXIS_PATTERN.matcher(line);
if (axesMatcher.find()) {
String axis = axesMatcher.group(1);
switch (axis) {
case "1":
builder.withAxis(GPlotSeries.Axis.LEFT);
break;
case "2":
builder.withAxis(GPlotSeries.Axis.RIGHT);
break;
}
return true;
} else {
builder.withAxis(GPlotSeries.Axis.LEFT);
return false;
}
}
private Map<String, String> extractSetsFrom(List<String> lines) {
Map<String, String> out = newHashMap();
for (String line : lines) {
Matcher matcher = SETS_PATTERN.matcher(line);
if (!matcher.matches()) {
continue;
}
out.put(matcher.group(1), matcher.group(2));
}
return out;
}
public Map<String, GPlotDefinition> getDefaultGPlotFiles() {
try {
URL url = GPlotParser.class.getResource("dummy.txt");
String scheme = url.getProtocol();
checkArgument(scheme.equals("jar"));
return readDefinitionsFromJar(url);
} catch (Exception e) {
LOGGER.error("loadDefaultGPlotFiles() - cannot load default files", e);
}
return emptyMap();
}
private Map<String, GPlotDefinition> readDefinitionsFromJar(URL url) throws IOException, URISyntaxException {
Map<String, GPlotDefinition> result = newHashMap();
JarURLConnection con = (JarURLConnection) url.openConnection();
JarFile archive = con.getJarFile();
Enumeration<JarEntry> entries = archive.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
if (entry.getName().endsWith(".gplot")) {
String filename = entry.getName().substring(entry.getName().lastIndexOf("/") + 1);
String plotName = filename.substring(0, filename.indexOf("."));
URL resource = GPlotParser.class.getResource(filename);
result.put(plotName, parse(Resources.toString(resource, Charsets.UTF_8)));
}
}
return result;
}
}