/* Copyright (c) 2011 Danish Maritime Authority. * * 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 dk.dma.ais.packet; import dk.dma.ais.message.AisMessage; import dk.dma.ais.message.AisPositionMessage; import dk.dma.ais.message.AisStaticCommon; import dk.dma.ais.message.IVesselPositionMessage; import dk.dma.ais.message.NavigationalStatus; import dk.dma.ais.message.ShipTypeCargo; import dk.dma.commons.util.io.OutputStreamSink; import dk.dma.enav.model.geometry.Position; import java.io.IOException; import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.text.DecimalFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.function.BiFunction; import java.util.function.Predicate; import java.util.function.Supplier; import static dk.dma.commons.util.io.IoUtil.writeAscii; /** * Common sink that can be used to convert AIS packets to text. * * @author Kasper Nielsen */ public class AisPacketOutputSinks { /** A thread local with a default text format. */ static final ThreadLocal<SimpleDateFormat> DEFAULT_DATE_FORMAT = new ThreadLocal<SimpleDateFormat>() { protected SimpleDateFormat initialValue() { return new SimpleDateFormat("dd-MM-yyyy HH:mm:ss"); } }; static final ThreadLocal<DecimalFormat> POSITION_FORMATTER = new ThreadLocal<DecimalFormat>() { protected DecimalFormat initialValue() { return new DecimalFormat("###.#####"); } }; /** A sink that writes an ais packet to an output stream. Using the default multi-line format. */ public static final OutputStreamSink<AisPacket> OUTPUT_TO_TEXT = new OutputStreamSink<AisPacket>() { @Override public void process(OutputStream stream, AisPacket message, long count) throws IOException { stream.write(message.getStringMessage().getBytes(StandardCharsets.US_ASCII)); stream.write('\n'); } }; /** * A sink that writes an AIS packet the past track as a JSON to an output stream. The implementation is a little bit * special. Because we are stateless, ending tags are written when the next package is received. Or the end is * reached. */ public static final OutputStreamSink<AisPacket> PAST_TRACK_JSON = new OutputStreamSink<AisPacket>() { @Override public void process(OutputStream stream, AisPacket p, long count) throws IOException { DecimalFormat df = POSITION_FORMATTER.get(); AisMessage m = p.tryGetAisMessage(); if (m instanceof IVesselPositionMessage) { IVesselPositionMessage im = (IVesselPositionMessage) m; Position pos = m.getValidPosition(); StringBuilder sb = new StringBuilder(); if (count > 1) { sb.append(" },"); } sb.append("\n \"point\": {\n"); sb.append(" \"timestamp\": ").append(p.getBestTimestamp()).append(",\n"); sb.append(" \"lon\": ").append(df.format(pos.getLongitude())).append(",\n"); sb.append(" \"lat\": ").append(df.format(pos.getLatitude())).append(",\n"); sb.append(" \"sog\": ").append(im.getSog()).append(",\n"); sb.append(" \"cog\": ").append(im.getCog()).append(",\n"); sb.append(" \"heading\": ").append(im.getTrueHeading()).append("\n"); writeAscii(sb, stream); } } /** {@inheritDoc} */ @Override public void footer(OutputStream stream, long count) throws IOException { if (count > 0) { // write the closing tag, unless we have never written anything. writeAscii(" }\n", stream); } writeAscii("}}", stream); } /** {@inheritDoc} */ @Override public void header(OutputStream stream) throws IOException { stream.write("{\"track\": {".getBytes(StandardCharsets.US_ASCII)); } }; /** * A sink that writes ais messages as JSON to an output stream. Each Line is a valid json object. */ public static OutputStreamSink<AisPacket> jsonMessageSink() { return new OutputStreamSink<AisPacket>() { @Override public void process(OutputStream stream, AisPacket p, long count) throws IOException { AisMessage m = p.tryGetAisMessage(); if (m == null) { return; } StringBuilder sb = new StringBuilder(); sb.append("{"); sb.append("\"mmsi\":").append(m.getUserId()).append(','); sb.append("\"msgId\":").append(m.getMsgId()).append(','); Position pos = m.getValidPosition(); if (pos != null) { DecimalFormat df = POSITION_FORMATTER.get(); sb.append("\"lat\":").append(df.format(pos.getLatitude())).append(','); sb.append("\"lon\":").append(df.format(pos.getLongitude())).append(','); } if (m instanceof AisPositionMessage) { AisPositionMessage im = (AisPositionMessage)m; sb.append("\"sog\":").append(im.getSog()).append(','); sb.append("\"cog\":").append(im.getCog()).append(','); sb.append("\"trueHeading\":").append(im.getTrueHeading()).append(','); } if (m.getTargetType() != null) { sb.append("\"targetType\":").append('\"').append(m.getTargetType().toString()).append("\","); } if (m instanceof AisStaticCommon) { AisStaticCommon asm = (AisStaticCommon)m; sb.append("\"name\":").append('\"').append(AisMessage.trimText(asm.getName())).append("\","); sb.append("\"dimBow\":").append(asm.getDimBow()).append(','); sb.append("\"dimPort\":").append(asm.getDimPort()).append(","); sb.append("\"dimStarboard\":").append(asm.getDimStarboard()).append(","); sb.append("\"dimStern\":").append(asm.getDimStern()).append(","); ShipTypeCargo stc = new ShipTypeCargo(asm.getShipType()); sb.append("\"shipType\":\"").append(stc.getShipType().toString()).append("\","); sb.append("\"cargo\":\"").append(stc.getShipCargo().toString()).append("\","); sb.append("\"callsign\":\"").append(AisMessage.trimText(asm.getCallsign())).append("\","); } sb.append("\"timestamp\":").append(p.getBestTimestamp()).append("}\r\n"); writeAscii(sb, stream); } /** {@inheritDoc} */ @Override public void footer(OutputStream stream, long count) throws IOException { //no footer } /** {@inheritDoc} */ @Override public void header(OutputStream stream) throws IOException { //no header } }; } public static OutputStreamSink<AisPacket> jsonObjectSink(String format) { return new AisPacketOutputSinkJsonObject(format); } public static OutputStreamSink<AisPacket> jsonPosListSink() { return new AisPacketOutputSinkJsonObject("mmsi;timestamp;lat;lon;sog;cog",";","dynamic"); } public static OutputStreamSink<AisPacket> jsonStaticListSink() { return new AisPacketOutputSinkJsonObject("mmsi;name;dimBow;dimPort;dimStarboard;dimStern;shipType;shipCargo;callsign;timestamp;targetType",";","static"); } /** A sink that writes an AIS packet to an output stream. Printing only the actual sentence . */ public static final OutputStreamSink<AisPacket> OUTPUT_PREFIXED_SENTENCES = new OutputStreamSink<AisPacket>() { @Override public void process(OutputStream stream, AisPacket message, long count) throws IOException { List<String> rawSentences = message.getVdm().getRawSentences(); String dateTimeStr = DEFAULT_DATE_FORMAT.get().format(message.getVdm().getTimestamp()); byte[] b = dateTimeStr.getBytes(StandardCharsets.US_ASCII); for (String sentence : rawSentences) { stream.write(b); stream.write(','); writeAscii(sentence, stream); stream.write('\n'); } } }; /** A sink that writes an AIS packet to an output stream. Using the default multi-line format. */ public static final OutputStreamSink<AisPacket> OUTPUT_TO_HTML = new OutputStreamSink<AisPacket>() { @Override public void process(OutputStream stream, AisPacket message, long count) throws IOException { for (String str : message.getStringMessage().split("\r\n")) { str = str.replace("<", "<"); str = str.replace(">", ">"); str = str.replace("/", "&qout;"); str = str.replace("&", "&"); writeAscii(str, stream); writeAscii("</br>", stream); stream.write('\n'); } } }; /** A sink that transforms ais stream into kml. */ public static final OutputStreamSink<AisPacket> OUTPUT_TO_KML() { return new AisPacketKMLOutputSink(); }; /** * Filters a list of packets according to their timestamp. * * @param packets * a list of packets * @param start * inclusive start * @param end * exclusive end * @return */ static List<AisPacket> filterPacketsByTime(Iterable<AisPacket> packets, long start, long end) { ArrayList<AisPacket> result = new ArrayList<>(); for (AisPacket p : packets) { if (start <= p.getBestTimestamp() && p.getBestTimestamp() < end) { result.add(p); } } return result; } // currently unused static List<AisPacket> readFromFile(Path p) throws IOException { final ConcurrentLinkedQueue<AisPacket> list = new ConcurrentLinkedQueue<>(); AisPacket packet; try (AisPacketReader r = new AisPacketReader(Files.newInputStream(p))) { while ((packet = r.readPacket()) != null) { list.add(packet); } } return new ArrayList<>(list); } public static OutputStreamSink<AisPacket> newTableSink(String columns, boolean writeHeader) { return new AisPacketOutputSinkTable(columns, writeHeader); } public static OutputStreamSink<AisPacket> newTableSink(String columns, boolean writeHeader, String seperator) { return new AisPacketOutputSinkTable(columns, writeHeader, seperator); } public static OutputStreamSink<AisPacket> newCsvSink() { return new AisPacketCSVOutputSink(); } public static OutputStreamSink<AisPacket> newCsvSink(String format) { return new AisPacketCSVOutputSink(format); } public static OutputStreamSink<AisPacket> newCsvStatefulSink() { return new AisPacketCSVStatefulOutputSink(); } public static OutputStreamSink<AisPacket> newCsvStatefulSink(String format) { return new AisPacketCSVStatefulOutputSink(format); } public static OutputStreamSink<AisPacket> newKmlSink() { return new AisPacketKMLOutputSink(); } public static OutputStreamSink<AisPacket> newKmlSink(Predicate<? super AisPacket> filter) { return new AisPacketKMLOutputSink(filter); } public static OutputStreamSink<AisPacket> newKmlSink(Predicate<? super AisPacket> filter, boolean createSituationFolder, boolean createMovementsFolder, boolean createTracksFolder, Predicate<? super AisPacket> isPrimaryTarget, Predicate<? super AisPacket> isSecondaryTarget, Predicate<? super AisPacket> triggerSnapshot, Supplier<? extends String> snapshotDescriptionSupplier, Supplier<? extends Integer> movementInterpolationStep, BiFunction<? super ShipTypeCargo, ? super NavigationalStatus, ? extends String> iconHrefSupplier) { return new AisPacketKMLOutputSink(filter, createSituationFolder, createMovementsFolder, createTracksFolder, isPrimaryTarget, isSecondaryTarget, triggerSnapshot, snapshotDescriptionSupplier, movementInterpolationStep, iconHrefSupplier); } public static OutputStreamSink<AisPacket> newKmlSink(Predicate<? super AisPacket> filter, boolean createSituationFolder, boolean createMovementsFolder, boolean createTracksFolder, Predicate<? super AisPacket> isPrimaryTarget, Predicate<? super AisPacket> isSecondaryTarget, Predicate<? super AisPacket> triggerSnapshot, Supplier<? extends String> snapshotDescriptionSupplier, Supplier<? extends Integer> movementInterpolationStep, Supplier<? extends String> supplyTitle, Supplier<? extends String> supplyDescription, BiFunction<? super ShipTypeCargo, ? super NavigationalStatus, ? extends String> iconHrefSupplier) { return new AisPacketKMLOutputSink(filter, createSituationFolder, createMovementsFolder, createTracksFolder, isPrimaryTarget, isSecondaryTarget, triggerSnapshot, snapshotDescriptionSupplier, movementInterpolationStep, supplyTitle, supplyDescription, iconHrefSupplier); } public static OutputStreamSink<AisPacket> newKmzSink() { return new AisPacketKMZOutputSink(); } public static OutputStreamSink<AisPacket> newKmzSink(Predicate<? super AisPacket> filter) { return new AisPacketKMZOutputSink(filter); } public static OutputStreamSink<AisPacket> newKmzSink(Predicate<? super AisPacket> filter, boolean createSituationFolder, boolean createMovementsFolder, boolean createTracksFolder, Predicate<? super AisPacket> isPrimaryTarget, Predicate<? super AisPacket> isSecondaryTarget, Predicate<? super AisPacket> triggerSnapshot, Supplier<? extends String> snapshotDescriptionSupplier, Supplier<? extends Integer> movementInterpolationStep, BiFunction<? super ShipTypeCargo, ? super NavigationalStatus, ? extends String> iconHrefSupplier) { return new AisPacketKMZOutputSink(filter, createSituationFolder, createMovementsFolder, createTracksFolder, isPrimaryTarget, isSecondaryTarget, triggerSnapshot, snapshotDescriptionSupplier, movementInterpolationStep, iconHrefSupplier); } public static OutputStreamSink<AisPacket> newKmzSink(Predicate<? super AisPacket> filter, boolean createSituationFolder, boolean createMovementsFolder, boolean createTracksFolder, Predicate<? super AisPacket> isPrimaryTarget, Predicate<? super AisPacket> isSecondaryTarget, Predicate<? super AisPacket> triggerSnapshot, Supplier<? extends String> snapshotDescriptionSupplier, Supplier<? extends Integer> movementInterpolationStep, Supplier<? extends String> supplyTitle, Supplier<? extends String> supplyDescription, BiFunction<? super ShipTypeCargo, ? super NavigationalStatus, ? extends String> iconHrefSupplier) { return new AisPacketKMZOutputSink(filter, createSituationFolder, createMovementsFolder, createTracksFolder, isPrimaryTarget, isSecondaryTarget, triggerSnapshot, snapshotDescriptionSupplier, movementInterpolationStep, supplyTitle, supplyDescription, iconHrefSupplier); } @SuppressWarnings("unchecked") public static OutputStreamSink<AisPacket> getOutputSink(String...params) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException { switch (params[0].toLowerCase()) { case "raw": return AisPacketOutputSinks.OUTPUT_TO_TEXT; case "table": Objects.requireNonNull(params[1]); if (params[2] != null) { AisPacketOutputSinks.newTableSink(params[1], true,params[2]); } return AisPacketOutputSinks.newTableSink(params[1], true); case "kml": return AisPacketOutputSinks.newKmlSink(); case "kmz": return AisPacketOutputSinks.newKmzSink(); case "jsonobject": if (params[1].equals("")) { return AisPacketOutputSinks.jsonObjectSink(AisPacketOutputSinkJsonObject.ALLCOLUMNS); } else { return AisPacketOutputSinks.jsonObjectSink(params[1]); } case "json": return AisPacketOutputSinks.jsonMessageSink(); case "csv": return params.length > 1 && params[1] != null ? AisPacketOutputSinks.newCsvSink(params[1]) : AisPacketOutputSinks.newCsvSink(); case "csv_stateful": return params.length > 1 && params[1] != null ? AisPacketOutputSinks.newCsvStatefulSink(params[1]) : AisPacketOutputSinks.newCsvStatefulSink(); default: // reflection return (OutputStreamSink<AisPacket>) AisPacketOutputSinks.class.getField(params[0]).get(null); } } }