diff --git a/OsmAnd-java/build.gradle b/OsmAnd-java/build.gradle index dfa6ff47976..382feccc252 100644 --- a/OsmAnd-java/build.gradle +++ b/OsmAnd-java/build.gradle @@ -6,6 +6,10 @@ configurations { android } +test { + exclude '**/*' +} + tasks.withType(JavaCompile).configureEach { sourceCompatibility = "17" targetCompatibility = "17" diff --git a/OsmAnd/build.gradle b/OsmAnd/build.gradle index 55eb4b708dd..b6dc91a573a 100644 --- a/OsmAnd/build.gradle +++ b/OsmAnd/build.gradle @@ -39,18 +39,13 @@ android { compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 - coreLibraryDesugaringEnabled true - } - - kotlinOptions { - jvmTarget = "17" } defaultConfig { minSdkVersion 24 - versionCode 5000 + versionCode 4900 versionCode System.getenv("APK_NUMBER_VERSION") ? System.getenv("APK_NUMBER_VERSION").toInteger() : versionCode - versionName "5.0.0" + versionName "4.9.0" versionName System.getenv("APK_VERSION")? System.getenv("APK_VERSION").toString(): versionName versionName System.getenv("APK_VERSION_SUFFIX")? versionName + System.getenv("APK_VERSION_SUFFIX").toString(): versionName } @@ -268,5 +263,6 @@ dependencies { amazonFreeImplementation "com.amazon:in-app-purchasing:2.0.76@jar" amazonFullImplementation "com.amazon:in-app-purchasing:2.0.76@jar" - coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.3") + + implementation 'net.sf.marineapi:marineapi:0.12.0' } diff --git a/OsmAnd/res/drawable-hdpi/ais_aton.png b/OsmAnd/res/drawable-hdpi/ais_aton.png new file mode 100644 index 00000000000..8fb447e4b4a Binary files /dev/null and b/OsmAnd/res/drawable-hdpi/ais_aton.png differ diff --git a/OsmAnd/res/drawable-hdpi/ais_aton_virt.png b/OsmAnd/res/drawable-hdpi/ais_aton_virt.png new file mode 100644 index 00000000000..eede8622150 Binary files /dev/null and b/OsmAnd/res/drawable-hdpi/ais_aton_virt.png differ diff --git a/OsmAnd/res/drawable-hdpi/ais_land.png b/OsmAnd/res/drawable-hdpi/ais_land.png new file mode 100644 index 00000000000..2d6145f5925 Binary files /dev/null and b/OsmAnd/res/drawable-hdpi/ais_land.png differ diff --git a/OsmAnd/res/drawable-hdpi/ais_plane.png b/OsmAnd/res/drawable-hdpi/ais_plane.png new file mode 100644 index 00000000000..a2298b21b06 Binary files /dev/null and b/OsmAnd/res/drawable-hdpi/ais_plane.png differ diff --git a/OsmAnd/res/drawable-hdpi/ais_sar.png b/OsmAnd/res/drawable-hdpi/ais_sar.png new file mode 100644 index 00000000000..150a2bcdbdf Binary files /dev/null and b/OsmAnd/res/drawable-hdpi/ais_sar.png differ diff --git a/OsmAnd/res/drawable-hdpi/ais_vessel.png b/OsmAnd/res/drawable-hdpi/ais_vessel.png new file mode 100644 index 00000000000..479fbe46d65 Binary files /dev/null and b/OsmAnd/res/drawable-hdpi/ais_vessel.png differ diff --git a/OsmAnd/res/drawable-hdpi/ais_vessel_cross.png b/OsmAnd/res/drawable-hdpi/ais_vessel_cross.png new file mode 100644 index 00000000000..6d534b2bb7d Binary files /dev/null and b/OsmAnd/res/drawable-hdpi/ais_vessel_cross.png differ diff --git a/OsmAnd/res/drawable-hdpi/ais_vessel_red.png b/OsmAnd/res/drawable-hdpi/ais_vessel_red.png new file mode 100644 index 00000000000..32d66b56a1b Binary files /dev/null and b/OsmAnd/res/drawable-hdpi/ais_vessel_red.png differ diff --git a/OsmAnd/res/drawable-mdpi/ais_aton.png b/OsmAnd/res/drawable-mdpi/ais_aton.png new file mode 100644 index 00000000000..a1ae2676d9f Binary files /dev/null and b/OsmAnd/res/drawable-mdpi/ais_aton.png differ diff --git a/OsmAnd/res/drawable-mdpi/ais_aton_virt.png b/OsmAnd/res/drawable-mdpi/ais_aton_virt.png new file mode 100644 index 00000000000..998e6dfeb7f Binary files /dev/null and b/OsmAnd/res/drawable-mdpi/ais_aton_virt.png differ diff --git a/OsmAnd/res/drawable-mdpi/ais_land.png b/OsmAnd/res/drawable-mdpi/ais_land.png new file mode 100644 index 00000000000..0d4a3a8739a Binary files /dev/null and b/OsmAnd/res/drawable-mdpi/ais_land.png differ diff --git a/OsmAnd/res/drawable-mdpi/ais_plane.png b/OsmAnd/res/drawable-mdpi/ais_plane.png new file mode 100644 index 00000000000..4a92c19e333 Binary files /dev/null and b/OsmAnd/res/drawable-mdpi/ais_plane.png differ diff --git a/OsmAnd/res/drawable-mdpi/ais_sar.png b/OsmAnd/res/drawable-mdpi/ais_sar.png new file mode 100644 index 00000000000..78bb51add2c Binary files /dev/null and b/OsmAnd/res/drawable-mdpi/ais_sar.png differ diff --git a/OsmAnd/res/drawable-mdpi/ais_vessel.png b/OsmAnd/res/drawable-mdpi/ais_vessel.png new file mode 100644 index 00000000000..1c40885e13b Binary files /dev/null and b/OsmAnd/res/drawable-mdpi/ais_vessel.png differ diff --git a/OsmAnd/res/drawable-mdpi/ais_vessel_cross.png b/OsmAnd/res/drawable-mdpi/ais_vessel_cross.png new file mode 100644 index 00000000000..c94f4fda65c Binary files /dev/null and b/OsmAnd/res/drawable-mdpi/ais_vessel_cross.png differ diff --git a/OsmAnd/res/drawable-mdpi/ais_vessel_red.png b/OsmAnd/res/drawable-mdpi/ais_vessel_red.png new file mode 100644 index 00000000000..1262ccc1f80 Binary files /dev/null and b/OsmAnd/res/drawable-mdpi/ais_vessel_red.png differ diff --git a/OsmAnd/res/drawable-xhdpi/ais_aton.png b/OsmAnd/res/drawable-xhdpi/ais_aton.png new file mode 100644 index 00000000000..547865fe1c1 Binary files /dev/null and b/OsmAnd/res/drawable-xhdpi/ais_aton.png differ diff --git a/OsmAnd/res/drawable-xhdpi/ais_aton_virt.png b/OsmAnd/res/drawable-xhdpi/ais_aton_virt.png new file mode 100644 index 00000000000..8b28d838841 Binary files /dev/null and b/OsmAnd/res/drawable-xhdpi/ais_aton_virt.png differ diff --git a/OsmAnd/res/drawable-xhdpi/ais_land.png b/OsmAnd/res/drawable-xhdpi/ais_land.png new file mode 100644 index 00000000000..18fced4f5c2 Binary files /dev/null and b/OsmAnd/res/drawable-xhdpi/ais_land.png differ diff --git a/OsmAnd/res/drawable-xhdpi/ais_map.png b/OsmAnd/res/drawable-xhdpi/ais_map.png new file mode 100644 index 00000000000..6b99f9335ad Binary files /dev/null and b/OsmAnd/res/drawable-xhdpi/ais_map.png differ diff --git a/OsmAnd/res/drawable-xhdpi/ais_plane.png b/OsmAnd/res/drawable-xhdpi/ais_plane.png new file mode 100644 index 00000000000..df3f68e4c38 Binary files /dev/null and b/OsmAnd/res/drawable-xhdpi/ais_plane.png differ diff --git a/OsmAnd/res/drawable-xhdpi/ais_sar.png b/OsmAnd/res/drawable-xhdpi/ais_sar.png new file mode 100644 index 00000000000..20b3eedd6ba Binary files /dev/null and b/OsmAnd/res/drawable-xhdpi/ais_sar.png differ diff --git a/OsmAnd/res/drawable-xhdpi/ais_vessel.png b/OsmAnd/res/drawable-xhdpi/ais_vessel.png new file mode 100644 index 00000000000..66ef45efa19 Binary files /dev/null and b/OsmAnd/res/drawable-xhdpi/ais_vessel.png differ diff --git a/OsmAnd/res/drawable-xhdpi/ais_vessel_cross.png b/OsmAnd/res/drawable-xhdpi/ais_vessel_cross.png new file mode 100644 index 00000000000..157094d1a2e Binary files /dev/null and b/OsmAnd/res/drawable-xhdpi/ais_vessel_cross.png differ diff --git a/OsmAnd/res/drawable-xhdpi/ais_vessel_red.png b/OsmAnd/res/drawable-xhdpi/ais_vessel_red.png new file mode 100644 index 00000000000..3ea6f1b10bf Binary files /dev/null and b/OsmAnd/res/drawable-xhdpi/ais_vessel_red.png differ diff --git a/OsmAnd/res/drawable-xxhdpi/ais_aton.png b/OsmAnd/res/drawable-xxhdpi/ais_aton.png new file mode 100644 index 00000000000..674872b248e Binary files /dev/null and b/OsmAnd/res/drawable-xxhdpi/ais_aton.png differ diff --git a/OsmAnd/res/drawable-xxhdpi/ais_aton_virt.png b/OsmAnd/res/drawable-xxhdpi/ais_aton_virt.png new file mode 100644 index 00000000000..fb8768a9d74 Binary files /dev/null and b/OsmAnd/res/drawable-xxhdpi/ais_aton_virt.png differ diff --git a/OsmAnd/res/drawable-xxhdpi/ais_land.png b/OsmAnd/res/drawable-xxhdpi/ais_land.png new file mode 100644 index 00000000000..d9bc317901d Binary files /dev/null and b/OsmAnd/res/drawable-xxhdpi/ais_land.png differ diff --git a/OsmAnd/res/drawable-xxhdpi/ais_plane.png b/OsmAnd/res/drawable-xxhdpi/ais_plane.png new file mode 100644 index 00000000000..01d012f12b8 Binary files /dev/null and b/OsmAnd/res/drawable-xxhdpi/ais_plane.png differ diff --git a/OsmAnd/res/drawable-xxhdpi/ais_sar.png b/OsmAnd/res/drawable-xxhdpi/ais_sar.png new file mode 100644 index 00000000000..259534b079e Binary files /dev/null and b/OsmAnd/res/drawable-xxhdpi/ais_sar.png differ diff --git a/OsmAnd/res/drawable-xxhdpi/ais_vessel.png b/OsmAnd/res/drawable-xxhdpi/ais_vessel.png new file mode 100644 index 00000000000..aa2ab681687 Binary files /dev/null and b/OsmAnd/res/drawable-xxhdpi/ais_vessel.png differ diff --git a/OsmAnd/res/drawable-xxhdpi/ais_vessel_cross.png b/OsmAnd/res/drawable-xxhdpi/ais_vessel_cross.png new file mode 100644 index 00000000000..9a5d73b5ee6 Binary files /dev/null and b/OsmAnd/res/drawable-xxhdpi/ais_vessel_cross.png differ diff --git a/OsmAnd/res/drawable-xxhdpi/ais_vessel_red.png b/OsmAnd/res/drawable-xxhdpi/ais_vessel_red.png new file mode 100644 index 00000000000..29da74357f7 Binary files /dev/null and b/OsmAnd/res/drawable-xxhdpi/ais_vessel_red.png differ diff --git a/OsmAnd/res/layout/fragment_colors_palette.xml b/OsmAnd/res/layout/fragment_colors_palette.xml new file mode 100644 index 00000000000..fddc0ee991f --- /dev/null +++ b/OsmAnd/res/layout/fragment_colors_palette.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/OsmAnd/res/layout/fragment_tripltek_promo.xml b/OsmAnd/res/layout/fragment_tripltek_promo.xml new file mode 100644 index 00000000000..afd0e528c47 --- /dev/null +++ b/OsmAnd/res/layout/fragment_tripltek_promo.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/OsmAnd/res/layout/list_item_keybinding_action.xml b/OsmAnd/res/layout/list_item_keybinding_action.xml new file mode 100644 index 00000000000..b5b1488876e --- /dev/null +++ b/OsmAnd/res/layout/list_item_keybinding_action.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + diff --git a/OsmAnd/res/values/fonts.xml b/OsmAnd/res/values/fonts.xml new file mode 100644 index 00000000000..6436d2705ba --- /dev/null +++ b/OsmAnd/res/values/fonts.xml @@ -0,0 +1,5 @@ + + + ui-fonts/Roboto-Regular.ttf + ui-fonts/Roboto-Medium.ttf + diff --git a/OsmAnd/res/values/strings.xml b/OsmAnd/res/values/strings.xml index e75b808b505..bcf57cbc279 100644 --- a/OsmAnd/res/values/strings.xml +++ b/OsmAnd/res/values/strings.xml @@ -274,7 +274,6 @@ You will be able to pair this scanner again at any time. A toggle to show or hide %1$s on the map. Hugerock Promo for %1$s months Free access to features including unlimited map downloads, 3D relief etc. for %1$s month - UK and similar India Keep left Keep right @@ -302,7 +301,6 @@ You will be able to pair this scanner again at any time. Terrain color scheme Start point Destination - Next destination point To My Location Map to the left Map to the right @@ -318,9 +316,6 @@ You will be able to pair this scanner again at any time. Simulate Display position always in center Terrain colorization type - Underlay - Overlay - Map style First intermediate Audio note Video note @@ -1130,6 +1125,28 @@ You need to activate the sensor so OsmAnd can find it. Cloud Precipitation Measurement units + IP address settings + Choose NMEA protocol (UDP/TCP) and define addresses + Protocol for NMEA data reception + Choose protocol for NMEA data reception + IP address of NMEA data source + Define IP address of the NMEA data source (if TCP is used) + TCP port of NMEA data source + Define TCP port number of the NMEA data source + UDP port of local NMEA data receiver + Define UPD port where OsmAnd receives NMEA data + Timeout settings for AIS signal reception + Set timeout values to identify lost AIS objects if no signal was received for a specific time. + Timeout for visibility when object is lost + Set Timeout for visibility of AIS objects: After this time without signal reception, the AIS object will be removed from screen. + Timeout for ship visibility when no signal received + Set timeout for ship visibility: After this time without signal reception, the ship symbol will change its state on screen: It will be crossed out. + Settings related to CPA + These values define the presentation of AIS objects that may come too close to the own position. + Warning time to reach the Closest Point of Approach (CPA) + If the TCPA (time to reach the CPA with another vessel) is less than this value, the vessel is marked with red color. + Warning distance for the Closes Point of Approach (CPA) + Vessels are marked with red color if the CPA is less than this value and the CPA is reached in the near future (see setting "Warning time to reach the CPA"). Weather Explore Weather forecast. Contours @@ -2214,6 +2231,7 @@ You need to activate the sensor so OsmAnd can find it. Follow track Save as track file Trip recording + Add track waypoint Add track waypoint Import or record track files Add track files @@ -2268,6 +2286,7 @@ You need to activate the sensor so OsmAnd can find it. Please provide a name for the point Volume buttons as zoom Control the map-zoom level using the volume buttons on the device. + Delete next destination point Inline skates This device doesn\'t have speed cameras. Uninstall and Restart @@ -3474,6 +3493,9 @@ You need to activate the sensor so OsmAnd can find it. Button to turn speed-controlled auto-zooming on or off. Turn on auto-zooming Turn off auto-zooming + Set destination + Replace destination + Add first intermediate A button to make the screen center the route destination, a previously selected destination would become the last intermediate destination. A button to make the screen center the point of departure. Will then ask to set destination or trigger the route calculation. A button to make the screen center the new route destination, replacing the previously selected destination (if any). @@ -4223,6 +4245,9 @@ Download tile maps directly, or copy them as SQLite database files to OsmAnd\'s Mark where your car is parked, and notify your calendar when the parking meter will expire. To place the marker, choose a place on the map, go to \"Actions\", and tap \"Add parking\". Distance calculator and planning tool Create paths by tapping the map, or by using or modifying existing GPX files, to plan a trip and measure the distance between points. The result can be saved as a GPX file to use later for guidance. + AIS vessel tracker + Display AIS positions and information about surrounding vessels. The AIS data is received via network from an external AIS receiver. + DISCLAIMER\n\nThis plugin is a hobby project and not designed for reliability and correctness. DO NOT rely upon this software in any way including for navigation and/or safety of life. Accessibility Makes the device\'s accessibility features directly available in OsmAnd. This facilitates e.g. adjusting the speech rate for text-to-speech voices, configuring D-pad navigation, using a trackball for zoom control, or text-to-speech feedback, for example to auto-announce your position. OpenStreetMap editing @@ -4616,6 +4641,7 @@ Download tile maps directly, or copy them as SQLite database files to OsmAnd\'s United States Canada Europe, Asia, Latin America, and similar + UK, India, and similar Australia Announce… Set up announcement of street names, traffic warnings (forced stops, speed bumps), speed camera warnings, and speed limits. @@ -5859,12 +5885,21 @@ Download tile maps directly, or copy them as SQLite database files to OsmAnd\'s Quick action Action %d Screen %d + Add map marker + Add POI + Change map style Map style changed to \"%s\". + New audio note + New video note + New photo note + Add OSM Note Voice on/off Unmute Voice Mute Voice + Add parking place Add action Edit action + Add Favorite Add action Delete action Delete the \"%s\" action? @@ -5906,12 +5941,15 @@ Download tile maps directly, or copy them as SQLite database files to OsmAnd\'s Add a map style Fill out all parameters Map styles + Change map overlay Map overlays Add overlay Map overlay changed to \"%s\". Map underlay changed to \"%s\". + Change map underlay Map underlays Add underlay + Change map source Map sources Add map source Map source changed to \"%s\". diff --git a/OsmAnd/res/xml/ais_settings.xml b/OsmAnd/res/xml/ais_settings.xml new file mode 100644 index 00000000000..0498714b169 --- /dev/null +++ b/OsmAnd/res/xml/ais_settings.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/OsmAnd/res/xml/profile_appearance.xml b/OsmAnd/res/xml/profile_appearance.xml new file mode 100644 index 00000000000..87ec511b691 --- /dev/null +++ b/OsmAnd/res/xml/profile_appearance.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/OsmAnd/src/net/osmand/plus/helpers/FontCache.java b/OsmAnd/src/net/osmand/plus/helpers/FontCache.java new file mode 100644 index 00000000000..fdcdf21589d --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/helpers/FontCache.java @@ -0,0 +1,40 @@ +package net.osmand.plus.helpers; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import android.content.Context; +import android.graphics.Typeface; +import android.util.Log; + +public class FontCache { + private static final String TAG = "FontCache"; + private static final Map fontMap = new ConcurrentHashMap(); + public static final String ROBOTO_MEDIUM = "ui-fonts/Roboto-Medium.ttf"; + public static final String ROBOTO_REGULAR = "ui-fonts/Roboto-Regular.ttf"; + + public static Typeface getRobotoMedium(Context context) { + return getFont(context, ROBOTO_MEDIUM); + } + + public static Typeface getRobotoRegular(Context context) { + return getFont(context, ROBOTO_REGULAR); + } + + public static Typeface getFont(Context context, String fontName) { + Typeface typeface = fontMap.get(fontName); + if (typeface != null) + return typeface; + + try { + typeface = Typeface.createFromAsset(context.getAssets(), fontName); + } catch(Exception e) { + Log.e(TAG, "Failed to create typeface from asset '" + fontName + "'", e); + return null; + } + if (typeface == null) + return null; + fontMap.put(fontName, typeface); + return typeface; + } +} diff --git a/OsmAnd/src/net/osmand/plus/mapcontextmenu/MenuController.java b/OsmAnd/src/net/osmand/plus/mapcontextmenu/MenuController.java index c83f1d1c831..9b46ad07221 100644 --- a/OsmAnd/src/net/osmand/plus/mapcontextmenu/MenuController.java +++ b/OsmAnd/src/net/osmand/plus/mapcontextmenu/MenuController.java @@ -62,6 +62,8 @@ import net.osmand.plus.mapmarkers.MapMarker; import net.osmand.plus.plugins.OsmandPlugin; import net.osmand.plus.plugins.PluginsHelper; +import net.osmand.plus.plugins.aistracker.AisObject; +import net.osmand.plus.plugins.aistracker.AisObjectMenuController; import net.osmand.plus.plugins.audionotes.AudioVideoNoteMenuController; import net.osmand.plus.plugins.audionotes.AudioVideoNotesPlugin.Recording; import net.osmand.plus.plugins.mapillary.MapillaryImage; @@ -242,6 +244,8 @@ public static MenuController getMenuController(@NonNull MapActivity mapActivity, menuController = new RenderedObjectMenuController(mapActivity, pointDescription, (RenderedObject) object); } else if (object instanceof MapillaryImage) { menuController = new MapillaryMenuController(mapActivity, pointDescription, (MapillaryImage) object); + } else if (object instanceof AisObject) { + menuController = new AisObjectMenuController(mapActivity, pointDescription, (AisObject) object); } else if (object instanceof SelectedGpxPoint) { menuController = new SelectedGpxMenuController(mapActivity, pointDescription, (SelectedGpxPoint) object); } else if (object instanceof Pair) { diff --git a/OsmAnd/src/net/osmand/plus/mapcontextmenu/editors/IconsCard.java b/OsmAnd/src/net/osmand/plus/mapcontextmenu/editors/IconsCard.java new file mode 100644 index 00000000000..4788aa2f8f3 --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/mapcontextmenu/editors/IconsCard.java @@ -0,0 +1,364 @@ +package net.osmand.plus.mapcontextmenu.editors; + +import static net.osmand.gpx.GPXUtilities.DEFAULT_ICON_NAME; +import static net.osmand.data.FavouritePoint.DEFAULT_UI_ICON_ID; + +import android.annotation.SuppressLint; +import android.graphics.drawable.Drawable; +import android.view.View; +import android.widget.ImageView; + +import androidx.annotation.ColorInt; +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; + +import net.osmand.PlatformUtil; +import net.osmand.plus.R; +import net.osmand.plus.activities.MapActivity; +import net.osmand.plus.helpers.AndroidUiHelper; +import net.osmand.plus.render.RenderingIcons; +import net.osmand.plus.routepreparationmenu.cards.MapBaseCard; +import net.osmand.plus.utils.AndroidUtils; +import net.osmand.plus.utils.ColorUtilities; +import net.osmand.plus.utils.UiUtilities; +import net.osmand.plus.widgets.FlowLayout; +import net.osmand.plus.widgets.FlowLayout.LayoutParams; +import net.osmand.plus.widgets.chips.ChipItem; +import net.osmand.plus.widgets.chips.HorizontalChipsView; +import net.osmand.util.Algorithms; + +import org.apache.commons.logging.Log; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class IconsCard extends MapBaseCard { + + private static final Log log = PlatformUtil.getLog(IconsCard.class); + + private static final int LAST_USED_ICONS_LIMIT = 20; + private static final String KEY_LAST_USED_ICONS = "last used icons"; + + private List lastUsedIcons; + private final Map iconsCategories; + + private final String preselectedIconName; + private String selectedCategory; + @DrawableRes + private int selectedIconId; + @ColorInt + private int selectedColor; + + private FlowLayout iconsSelector; + private HorizontalChipsView categorySelector; + + @Override + public int getCardLayoutId() { + return R.layout.icons_card; + } + + public IconsCard(@NonNull MapActivity mapActivity, + @DrawableRes int selectedIconId, + @Nullable String preselectedIconName, + @ColorInt int selectedColor) { + super(mapActivity); + this.lastUsedIcons = fetchLastUsedIcons(); + this.iconsCategories = collectIconsCategories(); + this.preselectedIconName = preselectedIconName; + this.selectedCategory = getInitialCategory(RenderingIcons.getBigIconName(selectedIconId)); + this.selectedIconId = selectedIconId; + this.selectedColor = selectedColor; + } + + @NonNull + private List fetchLastUsedIcons() { + List iconsList = app.getSettings().LAST_USED_FAV_ICONS.getStringsList(); + return new ArrayList<>(iconsList == null ? Collections.emptyList() : iconsList); + } + + @NonNull + private Map collectIconsCategories() { + Map iconsCategories = new LinkedHashMap<>(); + if (!Algorithms.isEmpty(lastUsedIcons)) { + iconsCategories.put(KEY_LAST_USED_ICONS, new JSONArray(lastUsedIcons)); + } + + String categoriesJson = loadCategoriesJsonFromAsset(); + if (categoriesJson != null) { + iconsCategories.putAll(getPoiIconsByCategories(categoriesJson)); + } + + return iconsCategories; + } + + @Nullable + private String loadCategoriesJsonFromAsset() { + try { + InputStream is = app.getAssets().open("poi_categories.json"); + return Algorithms.readFromInputStream(is).toString(); + } catch (IOException e) { + log.error("Failed to parse JSON", e); + return null; + } + } + + @NonNull + private Map getPoiIconsByCategories(@NonNull String categoriesJson) { + try { + Map poiIconsCategories = new LinkedHashMap<>(); + JSONObject obj = new JSONObject(categoriesJson); + JSONObject categories = obj.getJSONObject("categories"); + for (int i = 0; i < categories.length(); i++) { + JSONArray names = categories.names(); + if (names != null) { + String name = names.get(i).toString(); + JSONObject icons = categories.getJSONObject(name); + String translatedName = AndroidUtils.getIconStringPropertyName(app, name); + poiIconsCategories.put(translatedName, icons.getJSONArray("icons")); + } + } + return poiIconsCategories; + } catch (JSONException e) { + log.error(e.getMessage()); + return Collections.emptyMap(); + } + } + + @NonNull + private String getInitialCategory(@Nullable String selectedIconName) { + String firstCategory = iconsCategories.keySet().iterator().next(); + if (Algorithms.isEmpty(selectedIconName)) { + return firstCategory; + } + + for (int j = 0; j < iconsCategories.values().size(); j++) { + JSONArray iconJsonArray = (JSONArray) iconsCategories.values().toArray()[j]; + for (int i = 0; i < iconJsonArray.length(); i++) { + try { + if (iconJsonArray.getString(i).equals(selectedIconName)) { + return (String) iconsCategories.keySet().toArray()[j]; + } + } catch (JSONException e) { + log.error(e.getMessage()); + } + } + } + return firstCategory; + } + + @Override + protected void updateContent() { + setupCategoriesSelector(); + fillIconsSelector(); + } + + @SuppressLint("NotifyDataSetChanged") + private void setupCategoriesSelector() { + List items = new ArrayList<>(); + for (String category : iconsCategories.keySet()) { + ChipItem item = new ChipItem(category); + if (!category.equals(KEY_LAST_USED_ICONS)) { + item.title = category; + item.contentDescription = category; + } else { + item.contentDescription = app.getString(R.string.shared_string_last_used); + } + items.add(item); + } + + categorySelector = view.findViewById(R.id.icons_categories_selector); + categorySelector.setItems(items); + + ChipItem selected = categorySelector.getChipById(selectedCategory); + categorySelector.setSelected(selected); + + categorySelector.setOnSelectChipListener(chip -> { + selectCategory(chip.id); + return true; + }); + + ChipItem lastUsedCategory = categorySelector.getChipById(KEY_LAST_USED_ICONS); + if (lastUsedCategory != null) { + lastUsedCategory.icon = getIcon(R.drawable.ic_action_history); + lastUsedCategory.iconColor = ColorUtilities.getActiveColor(app, nightMode); + } + + categorySelector.notifyDataSetChanged(); + categorySelector.scrollTo(selected); + } + + private void selectCategory(@NonNull String category) { + selectedCategory = category; + fillIconsSelector(); + reselectIcon(selectedIconId, false); + + ChipItem selected = categorySelector.getChipById(selectedCategory); + categorySelector.setSelected(selected); + categorySelector.notifyDataSetChanged(); + categorySelector.smoothScrollTo(selected); + } + + private void fillIconsSelector() { + iconsSelector = view.findViewById(R.id.icons_selector); + iconsSelector.removeAllViews(); + iconsSelector.setHorizontalAutoSpacing(true); + + int width = getDimen(R.dimen.favorites_select_icon_button_right_padding); + for (String iconName : getIconNameListToShow()) { + LayoutParams layoutParams = new LayoutParams(width, 0); + iconsSelector.addView(createIconItemView(iconName), layoutParams); + } + } + + @NonNull + private List getIconNameListToShow() { + JSONArray iconJsonArray = iconsCategories.get(selectedCategory); + if (iconJsonArray == null) { + return Collections.emptyList(); + } + + List iconNameList = new ArrayList<>(); + for (int i = 0; i < iconJsonArray.length(); i++) { + try { + String iconName = iconJsonArray.getString(i); + iconNameList.add(iconName); + } catch (JSONException e) { + log.error(e); + } + } + + if (!Algorithms.isEmpty(preselectedIconName)) { + iconNameList.remove(preselectedIconName); + iconNameList.add(0, preselectedIconName); + } + + return iconNameList; + } + + @NonNull + private View createIconItemView(@NonNull String iconName) { + View iconItemView = themedInflater.inflate(R.layout.point_editor_button, iconsSelector, false); + + int iconId = getIconId(iconName); + iconItemView.setTag(iconId); + + ImageView icon = iconItemView.findViewById(R.id.icon); + AndroidUiHelper.updateVisibility(icon, true); + setUnselectedIconColor(icon, iconId); + + ImageView backgroundCircle = iconItemView.findViewById(R.id.background); + setUnselectedBackground(backgroundCircle); + backgroundCircle.setOnClickListener(v -> reselectIcon(iconId, true)); + + ImageView outline = iconItemView.findViewById(R.id.outline); + int outlineColorId = ColorUtilities.getStrokedButtonsOutlineColorId(nightMode); + outline.setImageDrawable(getColoredIcon(R.drawable.bg_point_circle_contour, outlineColorId)); + + return iconItemView; + } + + private void reselectIcon(@DrawableRes int newIconId, boolean notifyListener) { + unselectOldIcon(selectedIconId); + selectNewIcon(newIconId); + selectedIconId = newIconId; + if (notifyListener) { + notifyCardPressed(); + } + } + + private void unselectOldIcon(@DrawableRes int oldIconId) { + View oldIconContainer = iconsSelector.findViewWithTag(oldIconId); + if (oldIconContainer != null) { + setUnselectedIconColor(oldIconContainer.findViewById(R.id.icon), oldIconId); + + ImageView background = oldIconContainer.findViewById(R.id.background); + setUnselectedBackground(background); + + oldIconContainer.findViewById(R.id.outline).setVisibility(View.INVISIBLE); + } + } + + private void selectNewIcon(@DrawableRes int newIconId) { + View newIconContainer = iconsSelector.findViewWithTag(newIconId); + if (newIconContainer != null) { + ImageView icon = newIconContainer.findViewById(R.id.icon); + // Intentionally not accessing icons cache here, because cached icons are wrongly + // positioned in FavoritePointEditorFragment and WptPtEditorFragmentNew + int whiteColor = ContextCompat.getColor(mapActivity, R.color.card_and_list_background_light); + icon.setImageDrawable(UiUtilities.createTintedDrawable(mapActivity, newIconId, whiteColor)); + + ImageView backgroundCircle = newIconContainer.findViewById(R.id.background); + backgroundCircle.setImageDrawable(getPaintedIcon(R.drawable.bg_point_circle, selectedColor)); + + AndroidUiHelper.updateVisibility(newIconContainer.findViewById(R.id.outline), true); + } + } + + private void setUnselectedBackground(@NonNull ImageView background) { + int inactiveColorId = ColorUtilities.getInactiveButtonsAndLinksColorId(nightMode); + Drawable backgroundIcon = getColoredIcon(R.drawable.bg_point_circle, inactiveColorId); + background.setImageDrawable(backgroundIcon); + } + + private void setUnselectedIconColor(@NonNull ImageView icon, @DrawableRes int iconId) { + icon.setImageDrawable(UiUtilities.createTintedDrawable(mapActivity, iconId, ContextCompat.getColor(mapActivity, R.color.icon_color_default_light))); + } + + public void updateSelectedIconId(@DrawableRes int iconId) { + reselectIcon(iconId, true); + } + + public void updateSelectedIcon(@ColorInt int newColor, @NonNull String iconName) { + selectedColor = newColor; + + String category = getInitialCategory(iconName); + if (Algorithms.stringsEqual(selectedCategory, category)) { + int iconId = getIconId(iconName); + reselectIcon(iconId, false); + } else { + selectedIconId = getIconId(iconName); + selectCategory(category); + } + } + + @DrawableRes + public int getSelectedIconId() { + return selectedIconId; + } + + @DrawableRes + public int getIconId(@NonNull String iconName) { + int iconId = RenderingIcons.getBigIconResourceId(iconName); + return iconId != 0 ? iconId : DEFAULT_UI_ICON_ID; + } + + @NonNull + public String getSelectedIconName() { + String iconName = RenderingIcons.getBigIconName(selectedIconId); + return Algorithms.isEmpty(iconName) ? DEFAULT_ICON_NAME : iconName; + } + + @Nullable + public String getLastUsedIconName() { + return Algorithms.isEmpty(lastUsedIcons) ? null : lastUsedIcons.get(0); + } + + public void addLastUsedIcon(@NonNull String iconName) { + lastUsedIcons.remove(iconName); + if (lastUsedIcons.size() >= LAST_USED_ICONS_LIMIT - 1) { + lastUsedIcons = lastUsedIcons.subList(0, LAST_USED_ICONS_LIMIT - 1); + } + lastUsedIcons.add(0, iconName); + app.getSettings().LAST_USED_FAV_ICONS.setStringsList(lastUsedIcons); + } +} \ No newline at end of file diff --git a/OsmAnd/src/net/osmand/plus/plugins/PluginsHelper.java b/OsmAnd/src/net/osmand/plus/plugins/PluginsHelper.java index 756a1e0c4f3..69a3cfeee29 100644 --- a/OsmAnd/src/net/osmand/plus/plugins/PluginsHelper.java +++ b/OsmAnd/src/net/osmand/plus/plugins/PluginsHelper.java @@ -56,6 +56,7 @@ import net.osmand.plus.plugins.skimaps.SkiMapsPlugin; import net.osmand.plus.plugins.srtm.SRTMPlugin; import net.osmand.plus.plugins.weather.WeatherPlugin; +import net.osmand.plus.plugins.aistracker.AisTrackerPlugin; import net.osmand.plus.poi.PoiUIFilter; import net.osmand.plus.quickaction.QuickActionType; import net.osmand.plus.search.dialogs.QuickSearchDialogFragment; @@ -111,6 +112,7 @@ public static void initPlugins(@NonNull OsmandApplication app) { checkMarketPlugin(app, new SRTMPlugin(app)); allPlugins.add(new WeatherPlugin(app)); checkMarketPlugin(app, new NauticalMapsPlugin(app)); + allPlugins.add(new AisTrackerPlugin(app)); checkMarketPlugin(app, new SkiMapsPlugin(app)); allPlugins.add(new AudioVideoNotesPlugin(app)); checkMarketPlugin(app, new ParkingPositionPlugin(app)); @@ -562,7 +564,7 @@ public static List onIndexingFiles(@Nullable IProgress progress) { List l = new ArrayList<>(); for (OsmandPlugin plugin : getEnabledPlugins()) { List ls = plugin.indexingFiles(progress); - if (ls != null && ls.size() > 0) { + if (ls != null && !ls.isEmpty()) { l.addAll(ls); } } diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java new file mode 100644 index 00000000000..9a37998c8be --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisMessageListener.java @@ -0,0 +1,481 @@ +package net.osmand.plus.plugins.aistracker; + +import android.util.Log; + +import androidx.annotation.NonNull; + +import net.sf.marineapi.ais.event.AbstractAISMessageListener; +import net.sf.marineapi.ais.message.AISMessage01; +import net.sf.marineapi.ais.message.AISMessage02; +import net.sf.marineapi.ais.message.AISMessage03; +import net.sf.marineapi.ais.message.AISMessage04; +import net.sf.marineapi.ais.message.AISMessage05; +import net.sf.marineapi.ais.message.AISMessage09; +import net.sf.marineapi.ais.message.AISMessage18; +import net.sf.marineapi.ais.message.AISMessage19; +import net.sf.marineapi.ais.message.AISMessage21; +import net.sf.marineapi.ais.message.AISMessage24; +import net.sf.marineapi.ais.message.AISMessage27; +import net.sf.marineapi.nmea.event.SentenceListener; +import net.sf.marineapi.nmea.io.SentenceReader; +import net.sf.marineapi.nmea.sentence.SentenceId; + +import java.io.IOException; +import java.io.InputStream; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.EmptyStackException; +import java.util.Stack; +import java.util.Timer; +import java.util.TimerTask; + +public class AisMessageListener { + private AisTrackerLayer aisLayer; + private Timer timer; + private DatagramSocket udpSocket; + private Socket tcpSocket; + private InputStream tcpStream; + private SentenceReader sentenceReader = null; + private Stack listenerList = null; + public AisMessageListener(int port, @NonNull AisTrackerLayer aisLayer) { + initMembers(aisLayer); + try { + udpSocket = new DatagramSocket(port); + udpSocket.setReuseAddress(true); + initListeners(); + Log.d("AisMessageListener","new UDP listener, Port " + port); + } + catch (Exception e) { + Log.e("AisMessageListener","exception: " + e.getMessage()); + udpSocket = null; + } + } + public AisMessageListener(@NonNull String serverIp, int serverPort, @NonNull AisTrackerLayer aisLayer) { + TimerTask taskCheckNetworkConnection; + initMembers(aisLayer); + taskCheckNetworkConnection = new TimerTask() { + @Override + public void run() { + Log.d("AisMessageListener", "timer task taskCheckNetworkConnection running"); + if ((tcpSocket == null) || (!tcpSocket.isConnected())) { + try { + tcpSocket = new Socket(); + tcpSocket.setTcpNoDelay(true); + tcpSocket.setReuseAddress(true); + // tcpSocket.connect(new InetSocketAddress(InetAddress.getByName(serverIp), serverPort), 5000); + tcpSocket.connect(new InetSocketAddress(InetAddress.getByName(serverIp), serverPort)); + tcpStream = tcpSocket.getInputStream(); + initListeners(); + Log.d("AisMessageListener","new TCP listener"); + } + catch (IOException e) { + Log.e("AisMessageListener","exception: " + e.getMessage()); + tcpStream = null; + tcpSocket = null; + } + } + } + }; + this.timer = new Timer(); + timer.schedule(taskCheckNetworkConnection, 1000, 30000); + } + private void initMembers(@NonNull AisTrackerLayer aisLayer) { + this.aisLayer = aisLayer; + this.udpSocket = null; + this.tcpSocket = null; + this.tcpStream = null; + this.listenerList = new Stack<>(); + } + private void initListeners() throws IOException { + if (tcpStream != null) { + sentenceReader = new SentenceReader(tcpStream); + } + if (udpSocket != null) { + sentenceReader = new SentenceReader(udpSocket); + } + if (sentenceReader != null) { + new AisListener01(); + new AisListener02(); + new AisListener03(); + new AisListener04(); + new AisListener05(); + new AisListener09(); + new AisListener18(); + new AisListener19(); + new AisListener21(); + new AisListener24(); + new AisListener27(); + sentenceReader.start(); + } else { + Log.e("AisMessageListener", "sentenceReader not initialized"); + } + } + private void removeListeners() { + if (sentenceReader != null) { + sentenceReader.stop(); + while (!this.listenerList.isEmpty()) { + SentenceListener listener; + try { + listener = this.listenerList.pop(); + sentenceReader.removeSentenceListener(listener); + Log.d("AisMessageListener", "SentenceListener removed"); + } catch (EmptyStackException e) { + Log.e("AisMessageListener", "stack empty"); + } + } + } + } + public void stopListener() { + if (this.timer != null) { + this.timer.cancel(); + this.timer.purge(); + this.timer = null; + } + removeListeners(); + if (tcpSocket != null) { + Log.d("AisMessageListener","stopListener (TCP)"); + try { + if (tcpSocket.isConnected()) { + tcpSocket.close(); + } + if (tcpStream != null) { + tcpStream.close(); + } + } catch (Exception ignore) { } + } + if (udpSocket != null) { + Log.d("AisMessageListener","stopListener (UDP)"); + if (udpSocket.isConnected()) { + udpSocket.disconnect(); + } + udpSocket.close(); + } + } + + public boolean checkTcpSocket() { + return (tcpSocket != null) && (tcpStream != null); + } + + private void handleAisMessage(int aisType, Object obj) { + AisObject ais = null; + int msgType = 0; + int mmsi = 0; + int timeStamp = 0; + int imo = 0; + int heading = AisObjectConstants.INVALID_HEADING; + int navStatus = AisObjectConstants.INVALID_NAV_STATUS; + int manInd = AisObjectConstants.INVALID_MANEUVER_INDICATOR; + int shipType = AisObjectConstants.INVALID_SHIP_TYPE; + int dimensionToBow = AisObjectConstants.INVALID_DIMENSION; + int dimensionToStern = AisObjectConstants.INVALID_DIMENSION; + int dimensionToPort = AisObjectConstants.INVALID_DIMENSION; + int dimensionToStarboard = AisObjectConstants.INVALID_DIMENSION; + int etaMon = AisObjectConstants.INVALID_ETA; + int etaDay = AisObjectConstants.INVALID_ETA; + int etaHour = AisObjectConstants.INVALID_ETA_HOUR; + int etaMin = AisObjectConstants.INVALID_ETA_MIN; + int altitude = AisObjectConstants.INVALID_ALTITUDE; + int aidType = AisObjectConstants.UNSPECIFIED_AID_TYPE; + double draught = AisObjectConstants.INVALID_DRAUGHT; + double cog = AisObjectConstants.INVALID_COG; + double sog = AisObjectConstants.INVALID_SOG; + double lat = AisObjectConstants.INVALID_LAT; + double lon = AisObjectConstants.INVALID_LON; + double rot = AisObjectConstants.INVALID_ROT; + String callSign = null; + String shipName = null; + String destination = null; + + switch (aisType) { + case 1: AISMessage01 aisMsg01 = (AISMessage01)obj; // position report class A + Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg01.getMMSI() + + " Type: " + aisMsg01.getMessageType()); + Log.d("AisMessageListener","handleAisMessage()" + aisMsg01); + mmsi = aisMsg01.getMMSI(); + msgType = aisMsg01.getMessageType(); + navStatus = aisMsg01.getNavigationalStatus(); + manInd = aisMsg01.getManouverIndicator(); + if (aisMsg01.hasTimeStamp()) { timeStamp = aisMsg01.getTimeStamp(); } + if (aisMsg01.hasTrueHeading()) { heading = aisMsg01.getTrueHeading(); } + if (aisMsg01.hasCourseOverGround()) { cog = aisMsg01.getCourseOverGround(); } + if (aisMsg01.hasSpeedOverGround()) { sog = aisMsg01.getSpeedOverGround(); } + if (aisMsg01.hasLatitude()) { lat = aisMsg01.getLatitudeInDegrees(); } + if (aisMsg01.hasLongitude()) { lon = aisMsg01.getLongitudeInDegrees(); } + if (aisMsg01.hasRateOfTurn()) { rot = aisMsg01.getRateOfTurn(); } + ais = new AisObject(mmsi, msgType, timeStamp, navStatus, manInd, heading, + cog, sog, lat, lon, rot); + break; + + case 2: AISMessage02 aisMsg02 = (AISMessage02)obj; // position report class A + Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg02.getMMSI() + + " Type: " + aisMsg02.getMessageType()); + Log.d("AisMessageListener","handleAisMessage()" + aisMsg02); + mmsi = aisMsg02.getMMSI(); + msgType = aisMsg02.getMessageType(); + navStatus = aisMsg02.getNavigationalStatus(); + manInd = aisMsg02.getManouverIndicator(); + if (aisMsg02.hasTimeStamp()) { timeStamp = aisMsg02.getTimeStamp(); } + if (aisMsg02.hasTrueHeading()) { heading = aisMsg02.getTrueHeading(); } + if (aisMsg02.hasCourseOverGround()) { cog = aisMsg02.getCourseOverGround(); } + if (aisMsg02.hasSpeedOverGround()) { sog = aisMsg02.getSpeedOverGround(); } + if (aisMsg02.hasLatitude()) { lat = aisMsg02.getLatitudeInDegrees(); } + if (aisMsg02.hasLongitude()) { lon = aisMsg02.getLongitudeInDegrees(); } + if (aisMsg02.hasRateOfTurn()) { rot = aisMsg02.getRateOfTurn(); } + ais = new AisObject(mmsi, msgType, timeStamp, navStatus, manInd, heading, + cog, sog, lat, lon, rot); + break; + + case 3: AISMessage03 aisMsg03 = (AISMessage03)obj; // position report class A + Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg03.getMMSI() + + " Type: " + aisMsg03.getMessageType()); + Log.d("AisMessageListener","handleAisMessage()" + aisMsg03); + mmsi = aisMsg03.getMMSI(); + msgType = aisMsg03.getMessageType(); + navStatus = aisMsg03.getNavigationalStatus(); + manInd = aisMsg03.getManouverIndicator(); + if (aisMsg03.hasTimeStamp()) { timeStamp = aisMsg03.getTimeStamp(); } + if (aisMsg03.hasTrueHeading()) { heading = aisMsg03.getTrueHeading(); } + if (aisMsg03.hasCourseOverGround()) { cog = aisMsg03.getCourseOverGround(); } + if (aisMsg03.hasSpeedOverGround()) { sog = aisMsg03.getSpeedOverGround(); } + if (aisMsg03.hasLatitude()) { lat = aisMsg03.getLatitudeInDegrees(); } + if (aisMsg03.hasLongitude()) { lon = aisMsg03.getLongitudeInDegrees(); } + if (aisMsg03.hasRateOfTurn()) { rot = aisMsg03.getRateOfTurn(); } + ais = new AisObject(mmsi, msgType, timeStamp, navStatus, manInd, heading, + cog, sog, lat, lon, rot); + break; + + case 4: AISMessage04 aisMsg04 = (AISMessage04)obj; // base station report + Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg04.getMMSI() + + " Type: " + aisMsg04.getMessageType()); + Log.d("AisMessageListener","handleAisMessage()" + aisMsg04); + mmsi = aisMsg04.getMMSI(); + msgType = aisMsg04.getMessageType(); + if (aisMsg04.hasLatitude()) { lat = aisMsg04.getLatitudeInDegrees(); } + if (aisMsg04.hasLongitude()) { lon = aisMsg04.getLongitudeInDegrees(); } + ais = new AisObject(mmsi, msgType, lat, lon); + break; + + case 5: AISMessage05 aisMsg05 = (AISMessage05)obj; // static and voyage related data + Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg05.getMMSI() + + " Type: " + aisMsg05.getMessageType()); + Log.d("AisMessageListener","handleAisMessage()" + aisMsg05); + mmsi = aisMsg05.getMMSI(); + msgType = aisMsg05.getMessageType(); + imo = aisMsg05.getIMONumber(); + callSign = aisMsg05.getCallSign(); + shipName = aisMsg05.getName(); + shipType = aisMsg05.getTypeOfShipAndCargoType(); + dimensionToBow = aisMsg05.getBow(); + dimensionToStern = aisMsg05.getStern(); + dimensionToPort = aisMsg05.getPort(); + dimensionToStarboard = aisMsg05.getStarboard(); + draught = aisMsg05.getMaximumDraught(); + destination = aisMsg05.getDestination(); + etaMon = aisMsg05.getETAMonth(); + etaDay = aisMsg05.getETADay(); + etaHour = aisMsg05.getETAHour(); + etaMin = aisMsg05.getETAMinute(); + ais = new AisObject(mmsi, msgType, imo, callSign, shipName, shipType, dimensionToBow, + dimensionToStern, dimensionToPort, dimensionToStarboard, draught, + destination, etaMon, etaDay, etaHour, etaMin); + break; + + case 9: AISMessage09 aisMsg09 = (AISMessage09)obj; // SAR aircraft position report + Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg09.getMMSI() + + " Type: " + aisMsg09.getMessageType()); + Log.d("AisMessageListener","handleAisMessage()" + aisMsg09); + mmsi = aisMsg09.getMMSI(); + msgType = aisMsg09.getMessageType(); + timeStamp = aisMsg09.getTimeStamp(); + cog = aisMsg09.getCourseOverGround(); + sog = aisMsg09.getSpeedOverGround(); + altitude = aisMsg09.getAltitude(); + if (aisMsg09.hasLatitude()) { lat = aisMsg09.getLatitudeInDegrees(); } + if (aisMsg09.hasLongitude()) { lon = aisMsg09.getLongitudeInDegrees(); } + ais = new AisObject(mmsi, msgType, timeStamp, altitude, cog, sog, lat, lon); + break; + + case 18: AISMessage18 aisMsg18 = (AISMessage18)obj; // basic class B position report + Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg18.getMMSI() + + " Type: " + aisMsg18.getMessageType()); + Log.d("AisMessageListener","handleAisMessage()" + aisMsg18); + mmsi = aisMsg18.getMMSI(); + msgType = aisMsg18.getMessageType(); + if (aisMsg18.hasTimeStamp()) { timeStamp = aisMsg18.getTimeStamp(); } + if (aisMsg18.hasTrueHeading()) { heading = aisMsg18.getTrueHeading(); } + if (aisMsg18.hasCourseOverGround()) { cog = aisMsg18.getCourseOverGround(); } + if (aisMsg18.hasSpeedOverGround()) { sog = aisMsg18.getSpeedOverGround(); } + if (aisMsg18.hasLatitude()) { lat = aisMsg18.getLatitudeInDegrees(); } + if (aisMsg18.hasLongitude()) { lon = aisMsg18.getLongitudeInDegrees(); } + ais = new AisObject(mmsi, msgType, timeStamp, navStatus, manInd, heading, + cog, sog, lat, lon, rot); + break; + + case 19: AISMessage19 aisMsg19 = (AISMessage19)obj; // extended class B position report + Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg19.getMMSI() + + " Type: " + aisMsg19.getMessageType()); + Log.d("AisMessageListener","handleAisMessage()" + aisMsg19); + mmsi = aisMsg19.getMMSI(); + msgType = aisMsg19.getMessageType(); + shipType = aisMsg19.getTypeOfShipAndCargoType(); + dimensionToBow = aisMsg19.getBow(); + dimensionToStern = aisMsg19.getStern(); + dimensionToPort = aisMsg19.getPort(); + dimensionToStarboard = aisMsg19.getStarboard(); + if (aisMsg19.hasTimeStamp()) { timeStamp = aisMsg19.getTimeStamp(); } + if (aisMsg19.hasTrueHeading()) { heading = aisMsg19.getTrueHeading(); } + if (aisMsg19.hasCourseOverGround()) { cog = aisMsg19.getCourseOverGround(); } + if (aisMsg19.hasSpeedOverGround()) { sog = aisMsg19.getSpeedOverGround(); } + if (aisMsg19.hasLatitude()) { lat = aisMsg19.getLatitudeInDegrees(); } + if (aisMsg19.hasLongitude()) { lon = aisMsg19.getLongitudeInDegrees(); } + ais = new AisObject(mmsi, msgType, timeStamp, heading, cog, sog, lat, lon, + shipType, dimensionToBow, dimensionToStern, dimensionToPort, dimensionToStarboard); + break; + + case 21: AISMessage21 aisMsg21 = (AISMessage21)obj; // aid-to-navigation report + Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg21.getMMSI() + + " Type: " + aisMsg21.getMessageType()); + Log.d("AisMessageListener","handleAisMessage()" + aisMsg21); + mmsi = aisMsg21.getMMSI(); + msgType = aisMsg21.getMessageType(); + dimensionToBow = aisMsg21.getBow(); + dimensionToStern = aisMsg21.getStern(); + dimensionToPort = aisMsg21.getPort(); + dimensionToStarboard = aisMsg21.getStarboard(); + aidType = aisMsg21.getAidType(); + if (aisMsg21.hasLatitude()) { lat = aisMsg21.getLatitudeInDegrees(); } + if (aisMsg21.hasLongitude()) { lon = aisMsg21.getLongitudeInDegrees(); } + ais = new AisObject(mmsi, msgType, lat, lon, aidType, + dimensionToBow, dimensionToStern, dimensionToPort, dimensionToStarboard); + break; + + case 24: AISMessage24 aisMsg24 = (AISMessage24)obj; // static data report (like type 5) + Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg24.getMMSI() + + " Type: " + aisMsg24.getMessageType()); + Log.d("AisMessageListener","handleAisMessage()" + aisMsg24); + mmsi = aisMsg24.getMMSI(); + msgType = aisMsg24.getMessageType(); + callSign = aisMsg24.getCallSign(); + shipName = aisMsg24.getName(); + shipType = aisMsg24.getTypeOfShipAndCargoType(); + dimensionToBow = aisMsg24.getBow(); + dimensionToStern = aisMsg24.getStern(); + dimensionToPort = aisMsg24.getPort(); + dimensionToStarboard = aisMsg24.getStarboard(); + ais = new AisObject(mmsi, msgType, imo, callSign, shipName, shipType, dimensionToBow, + dimensionToStern, dimensionToPort, dimensionToStarboard, draught, + null, etaMon, etaDay, etaHour, etaMin); + break; + + case 27: AISMessage27 aisMsg27 = (AISMessage27)obj; // long range broadcast message + Log.d("AisMessageListener","handleAisMessage() MMSI: " + aisMsg27.getMMSI() + + " Type: " + aisMsg27.getMessageType()); + Log.d("AisMessageListener","handleAisMessage()" + aisMsg27); + mmsi = aisMsg27.getMMSI(); + msgType = aisMsg27.getMessageType(); + navStatus = aisMsg27.getNavigationalStatus(); + manInd = aisMsg27.getManouverIndicator(); + if (aisMsg27.hasTimeStamp()) { timeStamp = aisMsg27.getTimeStamp(); } + if (aisMsg27.hasTrueHeading()) { heading = aisMsg27.getTrueHeading(); } + if (aisMsg27.hasCourseOverGround()) { cog = aisMsg27.getCourseOverGround(); } + if (aisMsg27.hasSpeedOverGround()) { sog = aisMsg27.getSpeedOverGround(); } + if (aisMsg27.hasLatitude()) { lat = aisMsg27.getLatitudeInDegrees(); } + if (aisMsg27.hasLongitude()) { lon = aisMsg27.getLongitudeInDegrees(); } + if (aisMsg27.hasRateOfTurn()) { rot = aisMsg27.getRateOfTurn(); } + ais = new AisObject(mmsi, msgType, timeStamp, navStatus, manInd, heading, + cog, sog, lat, lon, rot); + break; + + default: + Log.e("AisMessageListener","handleAisMessage() invalid argument aisType: "+ aisType); + return; + } + aisLayer.updateAisObjectList(ais); + } + private void initEmbeddedLister(int aisType, @NonNull SentenceListener listener) { + //AisMessageListener.this.sentenceReader.addSentenceListener(listener); // listen to all (!) NMEA messages + AisMessageListener.this.sentenceReader.addSentenceListener(listener, SentenceId.VDM); + AisMessageListener.this.sentenceReader.addSentenceListener(listener, SentenceId.VDO); + AisMessageListener.this.listenerList.push(listener); + Log.d("AisMessageListener","Listener Type " + aisType + " started"); + } + private class AisListener01 extends AbstractAISMessageListener { + public AisListener01() { initEmbeddedLister(1, this); } + @Override + public void onMessage(AISMessage01 msg) { + handleAisMessage(1, msg); + } + } + private class AisListener02 extends AbstractAISMessageListener { + public AisListener02() { initEmbeddedLister(2, this); } + @Override + public void onMessage(AISMessage02 msg) { + handleAisMessage(2, msg); + } + } + private class AisListener03 extends AbstractAISMessageListener { + public AisListener03() { initEmbeddedLister(3, this); } + @Override + public void onMessage(AISMessage03 msg) { + handleAisMessage(3, msg); + } + } + private class AisListener04 extends AbstractAISMessageListener { + public AisListener04() { initEmbeddedLister(4, this); } + @Override + public void onMessage(AISMessage04 msg) { + handleAisMessage(4, msg); + } + } + private class AisListener05 extends AbstractAISMessageListener { + public AisListener05() { initEmbeddedLister(5, this); } + @Override + public void onMessage(AISMessage05 msg) { + handleAisMessage(5, msg); + } + } + private class AisListener09 extends AbstractAISMessageListener { + public AisListener09() { initEmbeddedLister(9, this); } + @Override + public void onMessage(AISMessage09 msg) { + handleAisMessage(9, msg); + } + } + private class AisListener18 extends AbstractAISMessageListener { + public AisListener18() { initEmbeddedLister(18, this); } + @Override + public void onMessage(AISMessage18 msg) { + handleAisMessage(18, msg); + } + } + private class AisListener19 extends AbstractAISMessageListener { + public AisListener19() { initEmbeddedLister(19, this); } + @Override + public void onMessage(AISMessage19 msg) { + handleAisMessage(19, msg); + } + } + private class AisListener21 extends AbstractAISMessageListener { + public AisListener21() { initEmbeddedLister(21, this); } + @Override + public void onMessage(AISMessage21 msg) { + handleAisMessage(21, msg); + } + } + private class AisListener24 extends AbstractAISMessageListener { + public AisListener24() { initEmbeddedLister(24, this); } + @Override + public void onMessage(AISMessage24 msg) { + handleAisMessage(24, msg); + } + } + private class AisListener27 extends AbstractAISMessageListener { + public AisListener27() { initEmbeddedLister(27, this); } + @Override + public void onMessage(AISMessage27 msg) { + handleAisMessage(27, msg); + } + } +} diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java new file mode 100644 index 00000000000..f0f43bb1975 --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObject.java @@ -0,0 +1,1011 @@ +package net.osmand.plus.plugins.aistracker; + +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.*; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.*; +import static net.osmand.plus.plugins.aistracker.AisTrackerHelper.getCpa; +import static net.osmand.plus.plugins.aistracker.AisTrackerHelper.getNewPosition; +import static net.osmand.plus.plugins.aistracker.AisTrackerPlugin.AIS_CPA_DEFAULT_WARNING_TIME; +import static net.osmand.plus.plugins.aistracker.AisTrackerPlugin.AIS_CPA_WARNING_DEFAULT_DISTANCE; +import static net.osmand.plus.plugins.aistracker.AisTrackerPlugin.AIS_OBJ_LOST_DEFAULT_TIMEOUT; +import static net.osmand.plus.plugins.aistracker.AisTrackerPlugin.AIS_SHIP_LOST_DEFAULT_TIMEOUT; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.LightingColorFilter; +import android.graphics.Paint; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import net.osmand.Location; +import net.osmand.data.LatLon; +import net.osmand.data.RotatedTileBox; +import net.osmand.plus.R; + +import java.util.Arrays; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; + +public class AisObject { + /* variable names starting with "ais_" belong to values received via an AIS message, + * its values may differ from the received values: they can be scaled, + * see https://gpsd.gitlab.io/gpsd/AIVDM.html */ + private int ais_msgType; + private int ais_mmsi; + private int ais_timeStamp = 0; + private int ais_imo = 0; + private int ais_heading = INVALID_HEADING; + private int ais_navStatus = INVALID_NAV_STATUS; + private int ais_manInd = INVALID_MANEUVER_INDICATOR; + private int ais_shipType = INVALID_SHIP_TYPE; + private int ais_dimensionToBow = INVALID_DIMENSION; + private int ais_dimensionToStern = INVALID_DIMENSION; + private int ais_dimensionToPort = INVALID_DIMENSION; + private int ais_dimensionToStarboard = INVALID_DIMENSION; + private int ais_etaMon = INVALID_ETA; + private int ais_etaDay = INVALID_ETA; + private int ais_etaHour = INVALID_ETA_HOUR; + private int ais_etaMin = INVALID_ETA_MIN; + private int ais_altitude = INVALID_ALTITUDE; + private int ais_aidType = UNSPECIFIED_AID_TYPE; + private double ais_draught = INVALID_DRAUGHT; + private double ais_cog = INVALID_COG; + private double ais_sog = INVALID_SOG; + private double ais_rot = INVALID_ROT; + private LatLon ais_position = null; + private String ais_callSign = null; + private String ais_shipName = null; + private String ais_destination = null; + private String countryCode = null; + private SortedSet msgTypes = null; + /* timestamp of last AIS message received for the current instance: */ + private long lastUpdate = 0; + /* timestamp of last AIS message received for all instances: */ + private static long lastMessageReceived = 0; + /* after this time of missing AIS signal the object is outdated and can be removed: */ + private static int maxObjectAgeInMinutes = AIS_OBJ_LOST_DEFAULT_TIMEOUT; + /* after this time of missing AIS signal the vessel symbol can change to mark "lost": */ + private static int vesselLostTimeoutInMinutes = AIS_SHIP_LOST_DEFAULT_TIMEOUT; + private static int cpaWarningTime = AIS_CPA_DEFAULT_WARNING_TIME; // in minutes + private static float cpaWarningDistance = AIS_CPA_WARNING_DEFAULT_DISTANCE; // in miles + private static Location ownPosition = null; // used to calculate distances, CPA etc. + private static boolean ownPositionFaked = false; // used for test purposes to fake own position + private AisObjType objectClass; + private Bitmap bitmap = null; + private boolean bitmapValid = false; + private int bitmapColor; + private AisTrackerHelper.Cpa cpa; + private long lastCpaUpdate = 0; + private boolean vesselAtRest = false; // if true, draw a circle instead of a bitmap + + public AisObject(int mmsi, int msgType, double lat, double lon) { + initObj(mmsi, msgType); + initLatLon(lat, lon); + initObjectClass(); + } + public AisObject(int mmsi, int msgType, int timeStamp, int navStatus, int manInd, int heading, + double cog, double sog, double lat, double lon, double rot) { + initObj(mmsi, msgType); + initLatLon(lat, lon); + this.ais_timeStamp = timeStamp; + this.ais_navStatus = navStatus; + this.ais_manInd = manInd; + this.ais_heading = heading; + this.ais_cog = cog; + this.ais_sog = sog; + this.ais_rot = rot; + initObjectClass(); + } + public AisObject(int mmsi, int msgType, int timeStamp, int altitude, + double cog, double sog, double lat, double lon) { + initObj(mmsi, msgType); + initLatLon(lat, lon); + this.ais_timeStamp = timeStamp; + this.ais_altitude = altitude; + this.ais_cog = cog; + this.ais_sog = sog; + initObjectClass(); + } + public AisObject(int mmsi, int msgType, int timeStamp, int heading, + double cog, double sog, double lat, double lon, + int shipType, int dimensionToBow, int dimensionToStern, + int dimensionToPort, int dimensionToStarboard) { + initObj(mmsi, msgType); + initLatLon(lat, lon); + initDimensions(dimensionToBow, dimensionToStern, dimensionToPort, dimensionToStarboard); + this.ais_timeStamp = timeStamp; + this.ais_heading = heading; + this.ais_cog = cog; + this.ais_sog = sog; + this.ais_shipType = shipType; + initObjectClass(); + } + public AisObject(int mmsi, int msgType, int imo, @Nullable String callSign, @Nullable String shipName, + int shipType, int dimensionToBow, int dimensionToStern, + int dimensionToPort, int dimensionToStarboard, + double draught, @Nullable String destination, int etaMon, + int etaDay, int etaHour, int etaMin) { + initObj(mmsi, msgType); + initDimensions(dimensionToBow, dimensionToStern, dimensionToPort, dimensionToStarboard); + this.ais_shipType = shipType; + this.ais_draught = draught; + this.ais_callSign = callSign; + this.ais_shipName = shipName; + if (destination != null) { + if (!destination.matches("^@+$")) { // string consisting of only "@" characters is invalid + this.ais_destination = destination; + } + } + this.ais_etaMon = etaMon; + this.ais_etaDay = etaDay; + this.ais_etaHour = etaHour; + this.ais_etaMin = etaMin; + this.ais_imo = imo; + initObjectClass(); + } + + public AisObject(int mmsi, int msgType, double lat, double lon, int aidType, + int dimensionToBow, int dimensionToStern, + int dimensionToPort, int dimensionToStarboard) { + initObj(mmsi, msgType); + initLatLon(lat, lon); + initDimensions(dimensionToBow, dimensionToStern, dimensionToPort, dimensionToStarboard); + this.ais_aidType = aidType; + initObjectClass(); + } + public AisObject(@NonNull AisObject ais) { + this.set(ais); + } + private String getCountryCode(Integer mmsi) { + String mmsiString = mmsi.toString(); + + if (mmsiString.length() > 2) { + String countryCode = mmsiString.substring(0, 3); + mmsiString = COUNTRY_CODES.get(Integer.parseInt(countryCode)); + if (mmsiString != null) { + return mmsiString; + } + } + return ""; + } + /* to be called only by a contructor! */ + private void initObj(int mmsi, int msgType) { + this.msgTypes = new TreeSet<>(); + this.cpa = new AisTrackerHelper.Cpa(); + this.ais_mmsi = mmsi; + this.ais_msgType = msgType; + this.countryCode = getCountryCode(this.ais_mmsi); + this.msgTypes.add(ais_msgType); + this.lastUpdate = System.currentTimeMillis(); + lastMessageReceived = this.lastUpdate; + } + private void initLatLon(double lat, double lon) { + if ((lat != INVALID_LAT) && (lon != INVALID_LON)) { + ais_position = new LatLon(lat, lon); + } + } + + private void initDimensions(int dimensionToBow, int dimensionToStern, + int dimensionToPort, int dimensionToStarboard) { + this.ais_dimensionToBow = dimensionToBow; + this.ais_dimensionToStern = dimensionToStern; + this.ais_dimensionToPort = dimensionToPort; + this.ais_dimensionToStarboard = dimensionToStarboard; + } + + private void initObjectClass() { + switch (this.ais_shipType) { + case INVALID_SHIP_TYPE: // not initialized + break; + + case 20: // Wing in ground (WIG) + case 21: // WIG, Hazardous category A + case 22: // WIG, Hazardous category B + case 23: // WIG, Hazardous category C + case 24: // WIG, Hazardous category D + case 40: // High Speed Craft (HSC) + case 41: // HSC, Hazardous category A + case 42: // HSC, Hazardous category B + case 43: // HSC, Hazardous category C + case 44: // HSC, Hazardous category D + case 49: // HSC, No additional information + this.objectClass = AIS_VESSEL_FAST; + break; + + case 30: // Fishing + case 31: // Towing + case 32: // Towing + case 33: // Dredging + case 34: // Diving ops + case 50: // Pilot Vessel + case 52: // Tug + case 53: // Port Tender + case 54: // Anti-pollution equipment + case 56: // Spare - Local Vessel + case 57: // Spare - Local Vessel + case 59: // Noncombatant ship according to RR Resolution No. 18 + this.objectClass = AIS_VESSEL_COMMERCIAL; + break; + + case 35: // Military ops + case 55: // Law Enforcement + this.objectClass = AIS_VESSEL_AUTHORITIES; + break; + + case 51: // Search and Rescue vessel + case 58: // Medical Transport + this.objectClass = AIS_VESSEL_SAR; + break; + + case 36: // Sailing + case 37: // Pleasure Craft + this.objectClass = AIS_VESSEL_SPORT; + break; + + case 60: // Passenger, all ships of this type + case 61: // Passenger, Hazardous category A + case 62: // Passenger, Hazardous category B + case 63: // Passenger, Hazardous category C + case 64: // Passenger, Hazardous category D + case 69: // Passenger, No additional information + this.objectClass = AIS_VESSEL_PASSENGER; + break; + + case 70: // Cargo, all ships of this type + case 71: // Cargo, Hazardous category A + case 72: // Cargo, Hazardous category B + case 73: // Cargo, Hazardous category C + case 74: // Cargo, Hazardous category D + case 79: // Cargo, No additional information + case 80: // Tanker, all ships of this type + case 81: // Tanker, Hazardous category A + case 82: // Tanker, Hazardous category B + case 83: // Tanker, Hazardous category C + case 84: // Tanker, Hazardous category D + case 89: // Tanker, No additional information + this.objectClass = AIS_VESSEL_FREIGHT; + break; + + case 90: // Other Type, all ships of this type + case 91: // Other Type, Hazardous category A + case 92: // Other Type, Hazardous category B + case 93: // Other Type, Hazardous category C + case 94: // Other Type, Hazardous category D + case 99: // Other Type, no additional information + default: + this.objectClass = AIS_VESSEL_OTHER; + break; + } + /* for the case that no ship type was transmitted... */ + if (ais_shipType == INVALID_SHIP_TYPE) { + if (msgTypes.contains(9)) { // aircraft + this.objectClass = AIS_AIRPLANE; + } else if (msgTypes.contains(4)) { // base station + this.objectClass = AIS_LANDSTATION; + } else if (msgTypes.contains(21)) { // aids to navigation + switch (ais_aidType) { + // see https://gpsd.gitlab.io/gpsd/AIVDM.html#_type_21_aid_to_navigation_report + case 29: // Safe Water + case 30: // Special Mark + this.objectClass = AIS_ATON_VIRTUAL; + break; + default: + this.objectClass = AIS_ATON; + } + } else if (msgTypes.contains(18)) { + this.objectClass = AIS_VESSEL; + } else { + switch (ais_navStatus) { + // see https://gpsd.gitlab.io/gpsd/AIVDM.html#_types_1_2_and_3_position_report_class_a + case 0: // Under way using engine + case 1: // At anchor + case 2: // Not under command + case 3: // Restricted manoeuverability + case 4: // Constrained by her draught + case 5: // Moored + case 6: // Aground + case 8: // Under way sailing + case 11: // Power-driven vessel towing astern (regional use) + case 12: // Power-driven vessel pushing ahead or towing alongside (regional use). + this.objectClass = AIS_VESSEL; + break; + + case 7: // Engaged in Fishing + this.objectClass = AIS_VESSEL_COMMERCIAL; + break; + + case 14: // AIS-SART is active + this.objectClass = AIS_SART; + break; + + case INVALID_NAV_STATUS: // no valid value + default: + this.objectClass = AIS_INVALID; + } + } + } + } + + private void invalidateBitmap() { + this.bitmapValid = false; + } + + public void set(@NonNull AisObject ais) { + /* attention: this method does not produce an exact copy of the given object */ + this.ais_mmsi = ais.getMmsi(); + this.ais_msgType = ais.getMsgType(); + if (ais.getTimestamp() != 0) { this.ais_timeStamp = ais.getTimestamp(); } + if (ais.getImo() != 0 ) { this.ais_imo = ais.getImo(); } + if (ais.getShipType() != INVALID_SHIP_TYPE ) { this.ais_shipType = ais.getShipType(); } + if (ais.getDimensionToBow() != INVALID_DIMENSION ) { this.ais_dimensionToBow = ais.getDimensionToBow(); } + if (ais.getDimensionToStern() != INVALID_DIMENSION ) { this.ais_dimensionToStern = ais.getDimensionToStern(); } + if (ais.getDimensionToPort() != INVALID_DIMENSION ) { this.ais_dimensionToPort = ais.getDimensionToPort(); } + if (ais.getDimensionToStarboard() != INVALID_DIMENSION ) { this.ais_dimensionToStarboard = ais.getDimensionToStarboard(); } + if (ais.getEtaMon() != INVALID_ETA ) { this.ais_etaMon = ais.getEtaMon(); } + if (ais.getEtaDay() != INVALID_ETA ) { this.ais_etaDay = ais.getEtaDay(); } + if (ais.getEtaHour() != INVALID_ETA_HOUR ) { this.ais_etaHour = ais.getEtaHour(); } + if (ais.getEtaMin() != INVALID_ETA_MIN ) { this.ais_etaMin = ais.getEtaMin(); } + if (ais.getAltitude() != INVALID_ALTITUDE) { this.ais_altitude = ais.getAltitude(); } + if (ais.getAidType() != UNSPECIFIED_AID_TYPE) { this.ais_aidType = ais.getAidType(); } + if (ais.getDraught() != INVALID_DRAUGHT) { this.ais_draught = ais.getDraught(); } + if (ais.getPosition() != null) { this.ais_position = ais.getPosition(); } + if (ais.getCallSign() != null) { this.ais_callSign = ais.getCallSign(); } + if (ais.getShipName() != null) { this.ais_shipName = ais.getShipName(); } + if (ais.getDestination() != null ) { this.ais_destination = ais.getDestination(); } + + /* the following values may change its value from VALID to INVALID, + hence overwriting with INVALID is accepted in some cases... */ + final List msgListHeading = Arrays.asList(1, 2, 3, 18, 19, 27); + final List msgListStatus = Arrays.asList(1, 2, 3, 27); + final List msgListCourse = Arrays.asList(1, 2, 3, 9, 18, 19, 27); + if (msgListHeading.contains(ais_msgType)) { + this.ais_heading = ais.getHeading(); + } + if (msgListStatus.contains(ais_msgType)) { + this.ais_navStatus = ais.getNavStatus(); + this.ais_manInd = ais.getManInd(); + this.ais_rot = ais.getRot(); + } + if (msgListCourse.contains(ais_msgType)) { + this.ais_cog = ais.getCog(); + this.ais_sog = ais.getSog(); + } + + this.countryCode = ais.getCountryCode(); + this.lastUpdate = System.currentTimeMillis(); + lastMessageReceived = this.lastUpdate; // lastMessageReceived is a static variable for the entire AisObject class + if (this.msgTypes == null) { + this.msgTypes = new TreeSet<>(); + } + this.msgTypes.add(ais_msgType); + if (this.cpa == null) { + cpa = new AisTrackerHelper.Cpa(); + } + this.initObjectClass(); + this.invalidateBitmap(); + this.bitmapColor = 0; + } + + public static int selectBitmap(AisObjType objType) { + switch (objType) { + case AIS_VESSEL: + case AIS_VESSEL_SPORT: + case AIS_VESSEL_FAST: + case AIS_VESSEL_PASSENGER: + case AIS_VESSEL_FREIGHT: + case AIS_VESSEL_COMMERCIAL: + case AIS_VESSEL_AUTHORITIES: + case AIS_VESSEL_SAR: + case AIS_VESSEL_OTHER: + case AIS_INVALID: + return R.drawable.ais_vessel; + case AIS_LANDSTATION: + return R.drawable.ais_land; + case AIS_AIRPLANE: + return R.drawable.ais_plane; + case AIS_SART: + return R.drawable.ais_sar; + case AIS_ATON: + return R.drawable.ais_aton; + case AIS_ATON_VIRTUAL: + return R.drawable.ais_aton_virt; + } + return -1; + } + + public static int selectColor(AisObjType objType) { + switch (objType) { + case AIS_VESSEL: + return Color.GREEN; + case AIS_VESSEL_SPORT: + return Color.YELLOW; + case AIS_VESSEL_FAST: + return Color.BLUE; + case AIS_VESSEL_PASSENGER: + return Color.CYAN; + case AIS_VESSEL_FREIGHT: + return Color.GRAY; + case AIS_VESSEL_COMMERCIAL: + return Color.LTGRAY; + case AIS_VESSEL_AUTHORITIES: + return Color.argb(0xff, 0x55, 0x6b, 0x2f); // 0x556b2f: darkolivegreen + case AIS_VESSEL_SAR: + return Color.argb(0xff, 0xfa, 0x80, 0x72); // 0xfa8072: salmon + case AIS_VESSEL_OTHER: + return Color.argb(0xff, 0x00, 0xbf, 0xff); // 0x00bfff: deepskyblue + default: + return 0; // transparent + } + } + + private void setBitmap(@NonNull AisTrackerLayer mapLayer) { + invalidateBitmap(); + vesselAtRest = isVesselAtRest(); + if (isLost(vesselLostTimeoutInMinutes) && !vesselAtRest) { + if (isMovable()) { + this.bitmap = mapLayer.getBitmap(R.drawable.ais_vessel_cross); + this.bitmapValid = true; + } + } else { + int bitmapId = selectBitmap(this.objectClass); + if (bitmapId >= 0) { + this.bitmap = mapLayer.getBitmap(bitmapId); + this.bitmapValid = true; + } + } + this.setColor(); + } + + private void setColor() { + if (isLost(vesselLostTimeoutInMinutes) && !vesselAtRest) { + if (isMovable()) { + this.bitmapColor = 0; // transparent + } + } else { + this.bitmapColor = selectColor(this.objectClass); + } + } + + private void updateBitmap(@NonNull AisTrackerLayer mapLayer, @NonNull Paint paint) { + if (isLost(vesselLostTimeoutInMinutes)) { + setBitmap(mapLayer); + } else { + if (!this.bitmapValid) { + setBitmap(mapLayer); + } + if (checkCpaWarning()) { + activateCpaWarning(); + } else { + deactivateCpaWarning(); + } + } + if (this.bitmapColor != 0) { + paint.setColorFilter(new LightingColorFilter(this.bitmapColor, 0)); + } else { + paint.setColorFilter(null); + } + } + + private void drawCircle(float locationX, float locationY, + @NonNull Paint paint, @NonNull Canvas canvas) { + Paint localPaint = new Paint(paint); + localPaint.setColorFilter(null); + localPaint.setColor(Color.DKGRAY); + canvas.drawCircle(locationX, locationY, 22.0f, localPaint); + localPaint.setColor(this.bitmapColor); + canvas.drawCircle(locationX, locationY, 18.0f, localPaint); + } + + public void draw(@NonNull AisTrackerLayer mapLayer, @NonNull Paint paint, + @NonNull Canvas canvas, @NonNull RotatedTileBox tileBox) { + updateBitmap(mapLayer, paint); + if (this.bitmap != null) { + canvas.save(); + canvas.rotate(tileBox.getRotate(), (float)tileBox.getCenterPixelX(), (float)tileBox.getCenterPixelY()); + float speedFactor = getMovement(); + int locationX = tileBox.getPixXFromLonNoRot(this.ais_position.getLongitude()); + int locationY = tileBox.getPixYFromLatNoRot(this.ais_position.getLatitude()); + float fx = locationX - this.bitmap.getWidth() / 2.0f; + float fy = locationY - this.bitmap.getHeight() / 2.0f; + if (!vesselAtRest && this.needRotation()) { + float rotation = 0; + if (this.ais_cog != INVALID_COG) { rotation = (float)this.ais_cog; } + else if (this.ais_heading != INVALID_HEADING ) { rotation = this.ais_heading; } + canvas.rotate(rotation, locationX, locationY); + } + if (vesselAtRest) { + drawCircle(locationX, locationY, paint, canvas); + } else { + canvas.drawBitmap(this.bitmap, Math.round(fx), Math.round(fy), paint); + } + if ((speedFactor > 0) && (!isLost(vesselLostTimeoutInMinutes)) && !vesselAtRest) { + float lineStartX = locationX; + float lineLength = (float)this.bitmap.getHeight() * speedFactor; + float lineStartY = locationY - this.bitmap.getHeight() / 4.0f; + float lineEndY = lineStartY - lineLength; + canvas.drawLine(lineStartX, lineStartY, lineStartX, lineEndY, paint); + } + canvas.restore(); + } + } + + public boolean isMovable() { + switch (this.objectClass) { + case AIS_VESSEL: + case AIS_VESSEL_SPORT: + case AIS_VESSEL_FAST: + case AIS_VESSEL_PASSENGER: + case AIS_VESSEL_FREIGHT: + case AIS_VESSEL_COMMERCIAL: + case AIS_VESSEL_AUTHORITIES: + case AIS_VESSEL_SAR: + case AIS_VESSEL_OTHER: + case AIS_AIRPLANE: + return true; + case AIS_INVALID: + return (this.ais_sog != INVALID_SOG) && (this.ais_sog > 0.0d); + default: + return false; + } + } + /* + for AIS objects that are moving, return a value that is taken as multiple of bitmap + height to draw a line to indicate the speed, + otherwise return 0 (no movement) + */ + private float getMovement() { + if (this.ais_sog > 0.0d) { + if (isMovable()) { + if (this.ais_sog < 2.0d) { return 0.0f; } + if (this.ais_sog < 5.0d) { return 1.0f; } + if (this.ais_sog < 10.0d) { return 3.0f; } + if (this.ais_sog < 25.0d) { return 6.0f; } + return 8.0f; + } + } + return 0.0f; + } + private boolean needRotation() { + if (((this.ais_cog != INVALID_COG) && (this.ais_cog != 0)) || + ((this.ais_heading != INVALID_HEADING) && (this.ais_heading != 0))) + { + return isMovable(); + } + return false; + } + /* return true if a vessel is moored etc. and needs to be drawn as a circle */ + private boolean isVesselAtRest() { + switch (this.objectClass) { + case AIS_VESSEL: + case AIS_VESSEL_SPORT: + case AIS_VESSEL_FAST: + case AIS_VESSEL_PASSENGER: + case AIS_VESSEL_FREIGHT: + case AIS_VESSEL_COMMERCIAL: + case AIS_VESSEL_AUTHORITIES: + case AIS_VESSEL_SAR: + case AIS_VESSEL_OTHER: + switch (this.ais_navStatus) { + case 5: // moored + /* sometimes the ais_navStatus is wrong and contradicts other data... */ + return (ais_cog == INVALID_COG) || (ais_sog < 0.2d); + default: + if (msgTypes.contains(18) || msgTypes.contains(24) + || msgTypes.contains(1) || msgTypes.contains(3)) { + if ((ais_cog == INVALID_COG /* maybe remove this condition */) + && (ais_sog < 0.2d)) { + return true; + } + } + return false; + } + default: + return false; + } + } + + /* return true if the vessel gets too close with the own position in the future + * (danger of collusion); + * this situation occurs if all of the following conditions hold: + * (1) the calculated TCPA is in the future (>0) + * (2) the calculated CPA is not bigger than the configured warning distance + * (3) the calculated TCPA is not bigger than the configured warning time + * (4) the time when the own course crosses the course of the other vessel + * is not in the past + * (5) the time when the course of the other vessel crosses the own course + * is not in the past */ + private boolean checkCpaWarning() { + if (isMovable() && (objectClass != AIS_AIRPLANE) && (cpaWarningTime > 0) && (ais_sog > 0.0d)) { + if (checkForCpaTimeout() && (ownPosition != null)) { + Location aisPosition = getCurrentLocation(); + if (aisPosition != null) { + getCpa(ownPosition, aisPosition, cpa); + lastCpaUpdate = System.currentTimeMillis(); + } + } + if (cpa.isValid()) { + double tcpa = cpa.getTcpa(); + if (tcpa > 0.0f) { + return ((cpa.getCpaDist() <= cpaWarningDistance) && + ((tcpa * 60.0d) <= cpaWarningTime) && + (cpa.getCrossingTime1() >= 0.0d) && + (cpa.getCrossingTime2() >= 0.0d)); + } + } + } + return false; + } + + private void activateCpaWarning() { + bitmapColor = Color.RED; + } + private void deactivateCpaWarning() { + if (bitmapColor == Color.RED) { + setColor(); + } + } + private boolean isLost(int maxAgeInMin) { + return ((System.currentTimeMillis() - this.lastUpdate) / 1000 / 60) > maxAgeInMin; + } + private boolean checkForCpaTimeout() { + return ((System.currentTimeMillis() - this.lastCpaUpdate) / 1000) > CPA_UPDATE_TIMEOUT_IN_SECONDS; + } + public static void setMaxObjectAge(int timeInMinutes) { maxObjectAgeInMinutes = timeInMinutes; } + public static void setVesselLostTimeout(int timeInMinutes) { vesselLostTimeoutInMinutes = timeInMinutes; } + public static void setCpaWarningTime(int warningTime) { cpaWarningTime = warningTime; } + public static void setCpaWarningDistance(float warningDistance) { cpaWarningDistance = warningDistance; } + public static void setOwnPosition(Location position) { if (!ownPositionFaked) { ownPosition = position; }} + public static void fakeOwnPosition(Location fakePosition) { // used for test purposes + ownPosition = fakePosition; + ownPositionFaked = fakePosition != null; + } + /* + * this function checks the age of the object (check lastUpdate against its limit) + * and returns true if the object is outdated and can be removed + * */ + public boolean checkObjectAge() { + return isLost(maxObjectAgeInMinutes); + } + public int getMsgType() { return this.ais_msgType; } + public SortedSet getMsgTypes() { return this.msgTypes; } + public int getMmsi() { return this.ais_mmsi; } + public int getTimestamp() { return this.ais_timeStamp; } + public int getImo() { return this.ais_imo; } + public int getHeading() { return this.ais_heading; } + public int getNavStatus() { return this.ais_navStatus; } + public int getManInd() { return this.ais_manInd; } + public int getShipType() { return this.ais_shipType; } + public int getDimensionToBow() { return this.ais_dimensionToBow; } + public int getDimensionToStern() { return this.ais_dimensionToStern; } + public int getDimensionToPort() { return this.ais_dimensionToPort; } + public int getDimensionToStarboard() { return this.ais_dimensionToStarboard; } + public int getEtaMon() { return this.ais_etaMon; } + public int getEtaDay() { return this.ais_etaDay; } + public int getEtaHour() { return this.ais_etaHour; } + public int getEtaMin() { return this.ais_etaMin; } + public int getAltitude() { return this.ais_altitude; } + public int getAidType() { return this.ais_aidType; } + public double getCog() { return this.ais_cog; } + public double getSog() { return this.ais_sog; } + public double getRot() { return this.ais_rot; } + public double getDraught() { return this.ais_draught; } + @Nullable + public LatLon getPosition() { + return this.ais_position; + } + @Nullable + public Location getLocation() { + if (this.ais_position != null) { + Location loc = new Location(AisTrackerPlugin.AISTRACKER_ID, + ais_position.getLatitude(), ais_position.getLongitude()); + if (ais_cog != INVALID_COG) { + loc.setBearing((float)ais_cog); + } + if (ais_sog != INVALID_SOG) { + loc.setSpeed((float)(ais_sog * 1852 / 3600)); // in m/s + } + if (ais_altitude != INVALID_ALTITUDE) { + loc.setAltitude((float)ais_altitude); + } + return loc; + } + return null; + } + /* in contrast to getLocation(), this method considers the timestamp of the creation + * of the AIS object and adjusts the received position using the time difference + * between now and the timestamp (assuming that course and speed is constant) */ + @Nullable + public Location getCurrentLocation() { + Location loc = getLocation(); + Location newLocation = null; + if (loc != null) { + double ageInHours = (System.currentTimeMillis() - this.lastUpdate) / 1000.0 / 3600.0; + newLocation = getNewPosition(loc, ageInHours); + } + return newLocation; + } + @Nullable public String getCallSign() { return this.ais_callSign; } + @Nullable public String getShipName() { return this.ais_shipName; } + @Nullable public String getDestination() { return this.ais_destination; } + @NonNull public String getCountryCode() { return this.countryCode; } + public AisObjType getObjectClass() { return this.objectClass; } + public long getLastUpdate() { return this.lastUpdate; } + public static long getLastMessageReceived() { return lastMessageReceived; } + public static long getAndUpdateLastMessageReceived() { + long timestamp = getLastMessageReceived(); + lastMessageReceived = System.currentTimeMillis(); + return timestamp; + } + @NonNull + public String getShipTypeString() { + switch (this.ais_shipType) { + case INVALID_SHIP_TYPE: // not initialized + return("unknown"); + case 20: + return("Wing in ground (WIG)"); + case 21: + return("WIG, Hazardous category A"); + case 22: + return("WIG, Hazardous category B"); + case 23: + return("WIG, Hazardous category C"); + case 24: + return("WIG, Hazardous category D"); + case 30: + return("Fishing"); + case 31: + return("Towing"); + case 32: + return("Towing"); + case 33: + return("Dredging"); + case 34: + return("Diving ops"); + case 35: + return("Military ops"); + case 36: + return("Sailing"); + case 37: + return("Pleasure Craft"); + case 40: + return("High Speed Craft (HSC)"); + case 41: + return("HSC, Hazardous category A"); + case 42: + return("HSC, Hazardous category B"); + case 43: + return("HSC, Hazardous category C"); + case 44: + return("HSC, Hazardous category D"); + case 49: // HSC, No additional information + return("High Speed Craft (HSC)"); + case 50: + return("Pilot Vessel"); + case 51: + return("Search and Rescue vessel"); + case 52: + return("Tug"); + case 53: + return("Port Tender"); + case 54: + return("Anti-pollution equipment"); + case 55: + return("Law Enforcement"); + case 56: + return("Spare - Local Vessel"); + case 57: + return("Spare - Local Vessel"); + case 58: + return("Medical Transport"); + case 59: + return("Noncombatant ship according to RR Resolution No. 18"); + case 60: + return("Passenger"); + case 61: + return("Passenger, Hazardous category A"); + case 62: + return("Passenger, Hazardous category B"); + case 63: + return("Passenger, Hazardous category C"); + case 64: + return("Passenger, Hazardous category D"); + case 69: // Passenger, No additional information + return("Passenger/Cruise/Ferry"); + case 70: // Cargo, all ships of this type + return("Cargo"); + case 71: + return("Cargo, Hazardous category A"); + case 72: + return("Cargo, Hazardous category B"); + case 73: + return("Cargo, Hazardous category C"); + case 74: + return("Cargo, Hazardous category D"); + case 79: // Cargo, No additional information + return("Cargo"); + case 80: // Tanker, all ships of this type + return("Tanker"); + case 81: + return("Tanker, Hazardous category A"); + case 82: + return("Tanker, Hazardous category B"); + case 83: + return("Tanker, Hazardous category C"); + case 84: + return("Tanker, Hazardous category D"); + case 89: // Tanker, No additional information + return("Tanker"); + case 90: // Other Type, all ships of this type + return("Other Type"); + case 91: + return("Other Type, Hazardous category A"); + case 92: + return("Other Type, Hazardous category B"); + case 93: + return("Other Type, Hazardous category C"); + case 94: + return("Other Type, Hazardous category D"); + case 99: // Other Type, no additional information + return("Other Type"); + default: + return Integer.toString(ais_shipType); + } + } + @NonNull + public String getNavStatusString() { + switch (this.ais_navStatus) { + // see https://gpsd.gitlab.io/gpsd/AIVDM.html#_types_1_2_and_3_position_report_class_a + case 0: + return("Under way using engine"); + case 1: + return("At anchor"); + case 2: + return("Not under command"); + case 3: + return("Restricted manoeuverability"); + case 4: + return("Constrained by her draught"); + case 5: + return("Moored"); + case 6: + return("Aground"); + case 8: + return("Under way sailing"); + case 11: + return("Power-driven vessel towing astern (regional use)"); + case 12: + return("Power-driven vessel pushing ahead or towing alongside (regional use)"); + case 7: + return("Engaged in Fishing"); + case 14: + return("AIS-SART is active"); + case INVALID_NAV_STATUS: // no valid value + return("unknown"); + default: + return(Integer.toString(ais_navStatus)); + } + } + @NonNull + public String getManIndString() { + // see https://gpsd.gitlab.io/gpsd/AIVDM.html#_types_1_2_and_3_position_report_class_a + switch (this.ais_manInd) { + case 0: + return("Not available"); + case 1: + return("No special maneuver"); + case 2: + return("Special maneuver"); + default: + return(Integer.toString(ais_manInd)); + } + } + @NonNull + public String getAidTypeString() { + switch (this.ais_aidType) { + // see https://gpsd.gitlab.io/gpsd/AIVDM.html#_type_21_aid_to_navigation_report + case 0: + return("not specified"); + case 1: + return("Reference point"); + case 2: + return("RACON (radar transponder marking a navigation hazard)"); + case 3: + return("Fixed structure off shore"); + case 4: + return("Spare, Reserved for future use"); + case 5: + return("Light, without sectors"); + case 6: + return("Light, with sectors"); + case 7: + return("Leading Light Front"); + case 8: + return("Leading Light Rear"); + case 9: + return("Beacon, Cardinal N"); + case 10: + return("Beacon, Cardinal E"); + case 11: + return("Beacon, Cardinal S"); + case 12: + return("Beacon, Cardinal W"); + case 13: + return("Beacon, Port hand"); + case 14: + return("Beacon, Starboard hand"); + case 15: + return("Beacon, Preferred Channel port hand"); + case 16: + return("Beacon, Preferred Channel starboard hand"); + case 17: + return("Beacon, Isolated danger"); + case 18: + return("Beacon, Safe wate"); + case 19: + return("Beacon, Special mark"); + case 20: + return("Cardinal Mark N"); + case 21: + return("Cardinal Mark E"); + case 22: + return("Cardinal Mark S"); + case 23: + return("Cardinal Mark W"); + case 24: + return("Port hand Mark"); + case 25: + return("Starboard hand Mark"); + case 26: + return("Preferred Channel Port hand"); + case 27: + return("Preferred Channel Starboard hand"); + case 28: + return("Isolated danger"); + case 29: + return("Safe Water"); + case 30: + return("Special Mark"); + case 31: + return("Light Vessel / LANBY / Rigs"); + default: + return(Integer.toString(ais_aidType)); + } + } + private float getDistanceOrBearing(boolean needBearing) { + Location aisLocation = getLocation(); + if ((ownPosition != null) && (aisLocation != null)) { + return needBearing ? ownPosition.bearingTo(aisLocation) : ownPosition.distanceTo(aisLocation); + } else { + Log.e("AisObject", "getDistanceOrBearing(): ownLocation -> " + ownPosition + + ", aisLocation -> " + aisLocation); + return -500.0f; // invalid + } + } + /* get bearing from own position to the position of the AIS object */ + public float getBearing() { + float bearing = getDistanceOrBearing(true); + if ((bearing < 0.0f) && (bearing > -200.0f)) { + while (bearing < 0.0f) { + bearing += 360.0f; + } + } + return bearing; + } + /* get distance from own position to the position of the AIS object in meters */ + public float getDistanceInMeters() { + return getDistanceOrBearing(false); + } + public float getDistanceInNauticalMiles() { + float dist = getDistanceInMeters(); + if (dist >= 0.0f) { + dist = dist / 1852; + } + return dist; + } + public boolean getSignalLostState() { + return (isLost(vesselLostTimeoutInMinutes) && isMovable() && !vesselAtRest); + } +} diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java new file mode 100644 index 00000000000..2df28c2f327 --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectConstants.java @@ -0,0 +1,337 @@ +package net.osmand.plus.plugins.aistracker; + +import java.util.AbstractMap; +import java.util.Map; + +public final class AisObjectConstants { + public final static int INVALID_HEADING = 511; + public final static int INVALID_NAV_STATUS = 15; + public final static int INVALID_MANEUVER_INDICATOR = 0; + public final static int INVALID_SHIP_TYPE = 0; + public final static int INVALID_DIMENSION = 0; + public final static int INVALID_ETA = 0; + public final static int INVALID_ETA_HOUR = 24; + public final static int INVALID_ETA_MIN = 60; + public final static int INVALID_ALTITUDE = 4095; + public final static int UNSPECIFIED_AID_TYPE = 0; + public final static double INVALID_COG = 360.0; + public final static double INVALID_SOG = 1023.0; + public final static double INVALID_LAT = 91.0; + public final static double INVALID_LON = 181.0; + public final static double INVALID_ROT = 128.0; + public final static double INVALID_DRAUGHT = 0.0; + public final static double INVALID_TCPA = -10000.0d; + public final static float INVALID_CPA = -1.0f; + public final static int CPA_UPDATE_TIMEOUT_IN_SECONDS = 10; + + public static enum AisObjType { + AIS_VESSEL, + AIS_VESSEL_SPORT, + AIS_VESSEL_FAST, + AIS_VESSEL_PASSENGER, + AIS_VESSEL_FREIGHT, + AIS_VESSEL_COMMERCIAL, + AIS_VESSEL_AUTHORITIES, + AIS_VESSEL_SAR, + AIS_VESSEL_OTHER, + AIS_LANDSTATION, + AIS_AIRPLANE, + AIS_SART, + AIS_ATON, // aids to navigation + AIS_ATON_VIRTUAL, + AIS_INVALID + } + public static final Map COUNTRY_CODES = Map.ofEntries( + new AbstractMap.SimpleEntry<>(201, "Albania"), + new AbstractMap.SimpleEntry<>(202, "Andorra"), + new AbstractMap.SimpleEntry<>(203, "Austria"), + new AbstractMap.SimpleEntry<>(204, "Portugal"), + new AbstractMap.SimpleEntry<>(205, "Belgium"), + new AbstractMap.SimpleEntry<>(206, "Belarus"), + new AbstractMap.SimpleEntry<>(207, "Bulgaria"), + new AbstractMap.SimpleEntry<>(208, "Vatican"), + new AbstractMap.SimpleEntry<>(209, "Cyprus"), + new AbstractMap.SimpleEntry<>(210, "Cyprus"), + new AbstractMap.SimpleEntry<>(211, "Germany"), + new AbstractMap.SimpleEntry<>(212, "Cyprus"), + new AbstractMap.SimpleEntry<>(213, "Georgia"), + new AbstractMap.SimpleEntry<>(214, "Moldova"), + new AbstractMap.SimpleEntry<>(215, "Malta"), + new AbstractMap.SimpleEntry<>(216, "Armenia"), + new AbstractMap.SimpleEntry<>(218, "Germany"), + new AbstractMap.SimpleEntry<>(219, "Denmark"), + new AbstractMap.SimpleEntry<>(220, "Denmark"), + new AbstractMap.SimpleEntry<>(224, "Spain"), + new AbstractMap.SimpleEntry<>(225, "Spain"), + new AbstractMap.SimpleEntry<>(226, "France"), + new AbstractMap.SimpleEntry<>(227, "France"), + new AbstractMap.SimpleEntry<>(228, "France"), + new AbstractMap.SimpleEntry<>(229, "Malta"), + new AbstractMap.SimpleEntry<>(230, "Finland"), + new AbstractMap.SimpleEntry<>(231, "Faroe Is"), + new AbstractMap.SimpleEntry<>(232, "United Kingdom"), + new AbstractMap.SimpleEntry<>(233, "United Kingdom"), + new AbstractMap.SimpleEntry<>(234, "United Kingdom"), + new AbstractMap.SimpleEntry<>(235, "United Kingdom"), + new AbstractMap.SimpleEntry<>(236, "Gibraltar"), + new AbstractMap.SimpleEntry<>(237, "Greece"), + new AbstractMap.SimpleEntry<>(238, "Croatia"), + new AbstractMap.SimpleEntry<>(239, "Greece"), + new AbstractMap.SimpleEntry<>(240, "Greece"), + new AbstractMap.SimpleEntry<>(241, "Greece"), + new AbstractMap.SimpleEntry<>(242, "Morocco"), + new AbstractMap.SimpleEntry<>(243, "Hungary"), + new AbstractMap.SimpleEntry<>(244, "Netherlands"), + new AbstractMap.SimpleEntry<>(245, "Netherlands"), + new AbstractMap.SimpleEntry<>(246, "Netherlands"), + new AbstractMap.SimpleEntry<>(247, "Italy"), + new AbstractMap.SimpleEntry<>(248, "Malta"), + new AbstractMap.SimpleEntry<>(249, "Malta"), + new AbstractMap.SimpleEntry<>(250, "Ireland"), + new AbstractMap.SimpleEntry<>(251, "Iceland"), + new AbstractMap.SimpleEntry<>(252, "Liechtenstein"), + new AbstractMap.SimpleEntry<>(253, "Luxembourg"), + new AbstractMap.SimpleEntry<>(254, "Monaco"), + new AbstractMap.SimpleEntry<>(255, "Portugal"), + new AbstractMap.SimpleEntry<>(256, "Malta"), + new AbstractMap.SimpleEntry<>(257, "Norway"), + new AbstractMap.SimpleEntry<>(258, "Norway"), + new AbstractMap.SimpleEntry<>(259, "Norway"), + new AbstractMap.SimpleEntry<>(261, "Poland"), + new AbstractMap.SimpleEntry<>(262, "Montenegro"), + new AbstractMap.SimpleEntry<>(263, "Portugal"), + new AbstractMap.SimpleEntry<>(264, "Romania"), + new AbstractMap.SimpleEntry<>(265, "Sweden"), + new AbstractMap.SimpleEntry<>(266, "Sweden"), + new AbstractMap.SimpleEntry<>(267, "Slovakia"), + new AbstractMap.SimpleEntry<>(268, "San Marino"), + new AbstractMap.SimpleEntry<>(269, "Switzerland"), + new AbstractMap.SimpleEntry<>(270, "Czech Republic"), + new AbstractMap.SimpleEntry<>(271, "Turkey"), + new AbstractMap.SimpleEntry<>(272, "Ukraine"), + new AbstractMap.SimpleEntry<>(273, "Russia"), + new AbstractMap.SimpleEntry<>(274, "FYR Macedonia"), + new AbstractMap.SimpleEntry<>(275, "Latvia"), + new AbstractMap.SimpleEntry<>(276, "Estonia"), + new AbstractMap.SimpleEntry<>(277, "Lithuania"), + new AbstractMap.SimpleEntry<>(278, "Slovenia"), + new AbstractMap.SimpleEntry<>(279, "Serbia"), + new AbstractMap.SimpleEntry<>(301, "Anguilla"), + new AbstractMap.SimpleEntry<>(303, "USA"), + new AbstractMap.SimpleEntry<>(304, "Antigua Barbuda"), + new AbstractMap.SimpleEntry<>(305, "Antigua Barbuda"), + new AbstractMap.SimpleEntry<>(306, "Curacao"), + new AbstractMap.SimpleEntry<>(307, "Aruba"), + new AbstractMap.SimpleEntry<>(308, "Bahamas"), + new AbstractMap.SimpleEntry<>(309, "Bahamas"), + new AbstractMap.SimpleEntry<>(310, "Bermuda"), + new AbstractMap.SimpleEntry<>(311, "Bahamas"), + new AbstractMap.SimpleEntry<>(312, "Belize"), + new AbstractMap.SimpleEntry<>(314, "Barbados"), + new AbstractMap.SimpleEntry<>(316, "Canada"), + new AbstractMap.SimpleEntry<>(319, "Cayman Is"), + new AbstractMap.SimpleEntry<>(321, "Costa Rica"), + new AbstractMap.SimpleEntry<>(323, "Cuba"), + new AbstractMap.SimpleEntry<>(325, "Dominica"), + new AbstractMap.SimpleEntry<>(327, "Dominican Rep"), + new AbstractMap.SimpleEntry<>(329, "Guadeloupe"), + new AbstractMap.SimpleEntry<>(330, "Grenada"), + new AbstractMap.SimpleEntry<>(331, "Greenland"), + new AbstractMap.SimpleEntry<>(332, "Guatemala"), + new AbstractMap.SimpleEntry<>(334, "Honduras"), + new AbstractMap.SimpleEntry<>(336, "Haiti"), + new AbstractMap.SimpleEntry<>(338, "USA"), + new AbstractMap.SimpleEntry<>(339, "Jamaica"), + new AbstractMap.SimpleEntry<>(341, "St Kitts Nevis"), + new AbstractMap.SimpleEntry<>(343, "St Lucia"), + new AbstractMap.SimpleEntry<>(345, "Mexico"), + new AbstractMap.SimpleEntry<>(347, "Martinique"), + new AbstractMap.SimpleEntry<>(348, "Montserrat"), + new AbstractMap.SimpleEntry<>(350, "Nicaragua"), + new AbstractMap.SimpleEntry<>(351, "Panama"), + new AbstractMap.SimpleEntry<>(352, "Panama"), + new AbstractMap.SimpleEntry<>(353, "Panama"), + new AbstractMap.SimpleEntry<>(354, "Panama"), + new AbstractMap.SimpleEntry<>(355, "Panama"), + new AbstractMap.SimpleEntry<>(356, "Panama"), + new AbstractMap.SimpleEntry<>(357, "Panama"), + new AbstractMap.SimpleEntry<>(358, "Puerto Rico"), + new AbstractMap.SimpleEntry<>(359, "El Salvador"), + new AbstractMap.SimpleEntry<>(361, "St Pierre Miquelon"), + new AbstractMap.SimpleEntry<>(362, "Trinidad Tobago"), + new AbstractMap.SimpleEntry<>(364, "Turks Caicos Is"), + new AbstractMap.SimpleEntry<>(366, "USA"), + new AbstractMap.SimpleEntry<>(367, "USA"), + new AbstractMap.SimpleEntry<>(368, "USA"), + new AbstractMap.SimpleEntry<>(369, "USA"), + new AbstractMap.SimpleEntry<>(370, "Panama"), + new AbstractMap.SimpleEntry<>(371, "Panama"), + new AbstractMap.SimpleEntry<>(372, "Panama"), + new AbstractMap.SimpleEntry<>(373, "Panama"), + new AbstractMap.SimpleEntry<>(374, "Panama"), + new AbstractMap.SimpleEntry<>(375, "St Vincent Grenadines"), + new AbstractMap.SimpleEntry<>(376, "St Vincent Grenadines"), + new AbstractMap.SimpleEntry<>(377, "St Vincent Grenadines"), + new AbstractMap.SimpleEntry<>(378, "British Virgin Is"), + new AbstractMap.SimpleEntry<>(379, "US Virgin Is"), + new AbstractMap.SimpleEntry<>(401, "Afghanistan"), + new AbstractMap.SimpleEntry<>(403, "Saudi Arabia"), + new AbstractMap.SimpleEntry<>(405, "Bangladesh"), + new AbstractMap.SimpleEntry<>(408, "Bahrain"), + new AbstractMap.SimpleEntry<>(410, "Bhutan"), + new AbstractMap.SimpleEntry<>(412, "China"), + new AbstractMap.SimpleEntry<>(413, "China"), + new AbstractMap.SimpleEntry<>(414, "China"), + new AbstractMap.SimpleEntry<>(416, "Taiwan"), + new AbstractMap.SimpleEntry<>(417, "Sri Lanka"), + new AbstractMap.SimpleEntry<>(419, "India"), + new AbstractMap.SimpleEntry<>(422, "Iran"), + new AbstractMap.SimpleEntry<>(423, "Azerbaijan"), + new AbstractMap.SimpleEntry<>(425, "Iraq"), + new AbstractMap.SimpleEntry<>(428, "Israel"), + new AbstractMap.SimpleEntry<>(431, "Japan"), + new AbstractMap.SimpleEntry<>(432, "Japan"), + new AbstractMap.SimpleEntry<>(434, "Turkmenistan"), + new AbstractMap.SimpleEntry<>(436, "Kazakhstan"), + new AbstractMap.SimpleEntry<>(437, "Uzbekistan"), + new AbstractMap.SimpleEntry<>(438, "Jordan"), + new AbstractMap.SimpleEntry<>(440, "Korea"), + new AbstractMap.SimpleEntry<>(441, "Korea"), + new AbstractMap.SimpleEntry<>(443, "Palestine"), + new AbstractMap.SimpleEntry<>(445, "DPR Korea"), + new AbstractMap.SimpleEntry<>(447, "Kuwait"), + new AbstractMap.SimpleEntry<>(450, "Lebanon"), + new AbstractMap.SimpleEntry<>(451, "Kyrgyz Republic"), + new AbstractMap.SimpleEntry<>(453, "Macao"), + new AbstractMap.SimpleEntry<>(455, "Maldives"), + new AbstractMap.SimpleEntry<>(457, "Mongolia"), + new AbstractMap.SimpleEntry<>(459, "Nepal"), + new AbstractMap.SimpleEntry<>(461, "Oman"), + new AbstractMap.SimpleEntry<>(463, "Pakistan"), + new AbstractMap.SimpleEntry<>(466, "Qatar"), + new AbstractMap.SimpleEntry<>(468, "Syria"), + new AbstractMap.SimpleEntry<>(470, "UAE"), + new AbstractMap.SimpleEntry<>(471, "UAE"), + new AbstractMap.SimpleEntry<>(472, "Tajikistan"), + new AbstractMap.SimpleEntry<>(473, "Yemen"), + new AbstractMap.SimpleEntry<>(475, "Yemen"), + new AbstractMap.SimpleEntry<>(477, "Hong Kong"), + new AbstractMap.SimpleEntry<>(478, "Bosnia and Herzegovina"), + new AbstractMap.SimpleEntry<>(501, "Antarctica"), + new AbstractMap.SimpleEntry<>(503, "Australia"), + new AbstractMap.SimpleEntry<>(506, "Myanmar"), + new AbstractMap.SimpleEntry<>(508, "Brunei"), + new AbstractMap.SimpleEntry<>(510, "Micronesia"), + new AbstractMap.SimpleEntry<>(511, "Palau"), + new AbstractMap.SimpleEntry<>(512, "New Zealand"), + new AbstractMap.SimpleEntry<>(514, "Cambodia"), + new AbstractMap.SimpleEntry<>(515, "Cambodia"), + new AbstractMap.SimpleEntry<>(516, "Christmas Is"), + new AbstractMap.SimpleEntry<>(518, "Cook Is"), + new AbstractMap.SimpleEntry<>(520, "Fiji"), + new AbstractMap.SimpleEntry<>(523, "Cocos Is"), + new AbstractMap.SimpleEntry<>(525, "Indonesia"), + new AbstractMap.SimpleEntry<>(529, "Kiribati"), + new AbstractMap.SimpleEntry<>(531, "Laos"), + new AbstractMap.SimpleEntry<>(533, "Malaysia"), + new AbstractMap.SimpleEntry<>(536, "N Mariana Is"), + new AbstractMap.SimpleEntry<>(538, "Marshall Is"), + new AbstractMap.SimpleEntry<>(540, "New Caledonia"), + new AbstractMap.SimpleEntry<>(542, "Niue"), + new AbstractMap.SimpleEntry<>(544, "Nauru"), + new AbstractMap.SimpleEntry<>(546, "French Polynesia"), + new AbstractMap.SimpleEntry<>(548, "Philippines"), + new AbstractMap.SimpleEntry<>(553, "Papua New Guinea"), + new AbstractMap.SimpleEntry<>(555, "Pitcairn Is"), + new AbstractMap.SimpleEntry<>(557, "Solomon Is"), + new AbstractMap.SimpleEntry<>(559, "American Samoa"), + new AbstractMap.SimpleEntry<>(561, "Samoa"), + new AbstractMap.SimpleEntry<>(563, "Singapore"), + new AbstractMap.SimpleEntry<>(564, "Singapore"), + new AbstractMap.SimpleEntry<>(565, "Singapore"), + new AbstractMap.SimpleEntry<>(566, "Singapore"), + new AbstractMap.SimpleEntry<>(567, "Thailand"), + new AbstractMap.SimpleEntry<>(570, "Tonga"), + new AbstractMap.SimpleEntry<>(572, "Tuvalu"), + new AbstractMap.SimpleEntry<>(574, "Vietnam"), + new AbstractMap.SimpleEntry<>(576, "Vanuatu"), + new AbstractMap.SimpleEntry<>(577, "Vanuatu"), + new AbstractMap.SimpleEntry<>(578, "Wallis Futuna Is"), + new AbstractMap.SimpleEntry<>(601, "South Africa"), + new AbstractMap.SimpleEntry<>(603, "Angola"), + new AbstractMap.SimpleEntry<>(605, "Algeria"), + new AbstractMap.SimpleEntry<>(607, "St Paul Amsterdam Is"), + new AbstractMap.SimpleEntry<>(608, "Ascension Is"), + new AbstractMap.SimpleEntry<>(609, "Burundi"), + new AbstractMap.SimpleEntry<>(610, "Benin"), + new AbstractMap.SimpleEntry<>(611, "Botswana"), + new AbstractMap.SimpleEntry<>(612, "Cen Afr Rep"), + new AbstractMap.SimpleEntry<>(613, "Cameroon"), + new AbstractMap.SimpleEntry<>(615, "Congo"), + new AbstractMap.SimpleEntry<>(616, "Comoros"), + new AbstractMap.SimpleEntry<>(617, "Cape Verde"), + new AbstractMap.SimpleEntry<>(618, "Antarctica"), + new AbstractMap.SimpleEntry<>(619, "Ivory Coast"), + new AbstractMap.SimpleEntry<>(620, "Comoros"), + new AbstractMap.SimpleEntry<>(621, "Djibouti"), + new AbstractMap.SimpleEntry<>(622, "Egypt"), + new AbstractMap.SimpleEntry<>(624, "Ethiopia"), + new AbstractMap.SimpleEntry<>(625, "Eritrea"), + new AbstractMap.SimpleEntry<>(626, "Gabon"), + new AbstractMap.SimpleEntry<>(627, "Ghana"), + new AbstractMap.SimpleEntry<>(629, "Gambia"), + new AbstractMap.SimpleEntry<>(630, "Guinea-Bissau"), + new AbstractMap.SimpleEntry<>(631, "Equ. Guinea"), + new AbstractMap.SimpleEntry<>(632, "Guinea"), + new AbstractMap.SimpleEntry<>(633, "Burkina Faso"), + new AbstractMap.SimpleEntry<>(634, "Kenya"), + new AbstractMap.SimpleEntry<>(635, "Antarctica"), + new AbstractMap.SimpleEntry<>(636, "Liberia"), + new AbstractMap.SimpleEntry<>(637, "Liberia"), + new AbstractMap.SimpleEntry<>(642, "Libya"), + new AbstractMap.SimpleEntry<>(644, "Lesotho"), + new AbstractMap.SimpleEntry<>(645, "Mauritius"), + new AbstractMap.SimpleEntry<>(647, "Madagascar"), + new AbstractMap.SimpleEntry<>(649, "Mali"), + new AbstractMap.SimpleEntry<>(650, "Mozambique"), + new AbstractMap.SimpleEntry<>(654, "Mauritania"), + new AbstractMap.SimpleEntry<>(655, "Malawi"), + new AbstractMap.SimpleEntry<>(656, "Niger"), + new AbstractMap.SimpleEntry<>(657, "Nigeria"), + new AbstractMap.SimpleEntry<>(659, "Namibia"), + new AbstractMap.SimpleEntry<>(660, "Reunion"), + new AbstractMap.SimpleEntry<>(661, "Rwanda"), + new AbstractMap.SimpleEntry<>(662, "Sudan"), + new AbstractMap.SimpleEntry<>(663, "Senegal"), + new AbstractMap.SimpleEntry<>(664, "Seychelles"), + new AbstractMap.SimpleEntry<>(665, "St Helena"), + new AbstractMap.SimpleEntry<>(666, "Somalia"), + new AbstractMap.SimpleEntry<>(667, "Sierra Leone"), + new AbstractMap.SimpleEntry<>(668, "Sao Tome Principe"), + new AbstractMap.SimpleEntry<>(669, "Swaziland"), + new AbstractMap.SimpleEntry<>(670, "Chad"), + new AbstractMap.SimpleEntry<>(671, "Togo"), + new AbstractMap.SimpleEntry<>(672, "Tunisia"), + new AbstractMap.SimpleEntry<>(674, "Tanzania"), + new AbstractMap.SimpleEntry<>(675, "Uganda"), + new AbstractMap.SimpleEntry<>(676, "DR Congo"), + new AbstractMap.SimpleEntry<>(677, "Tanzania"), + new AbstractMap.SimpleEntry<>(678, "Zambia"), + new AbstractMap.SimpleEntry<>(679, "Zimbabwe"), + new AbstractMap.SimpleEntry<>(701, "Argentina"), + new AbstractMap.SimpleEntry<>(710, "Brazil"), + new AbstractMap.SimpleEntry<>(720, "Bolivia"), + new AbstractMap.SimpleEntry<>(725, "Chile"), + new AbstractMap.SimpleEntry<>(730, "Colombia"), + new AbstractMap.SimpleEntry<>(735, "Ecuador"), + new AbstractMap.SimpleEntry<>(740, "UK"), + new AbstractMap.SimpleEntry<>(745, "Guiana"), + new AbstractMap.SimpleEntry<>(750, "Guyana"), + new AbstractMap.SimpleEntry<>(755, "Paraguay"), + new AbstractMap.SimpleEntry<>(760, "Peru"), + new AbstractMap.SimpleEntry<>(765, "Suriname"), + new AbstractMap.SimpleEntry<>(770, "Uruguay"), + new AbstractMap.SimpleEntry<>(775, "Venezuela") + ); +} + diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuBuilder.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuBuilder.java new file mode 100644 index 00000000000..0e06e81d9a7 --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuBuilder.java @@ -0,0 +1,156 @@ +package net.osmand.plus.plugins.aistracker; + + +import android.graphics.drawable.Drawable; +import android.text.TextUtils; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; + +import net.osmand.plus.R; +import net.osmand.plus.activities.MapActivity; +import net.osmand.plus.helpers.FontCache; +import net.osmand.plus.mapcontextmenu.CollapsableView; +import net.osmand.plus.mapcontextmenu.MenuBuilder; +import net.osmand.plus.utils.AndroidUtils; +import net.osmand.plus.utils.ColorUtilities; +import net.osmand.plus.widgets.TextViewEx; +import net.osmand.util.Algorithms; + +public class AisObjectMenuBuilder extends MenuBuilder { + + public AisObjectMenuBuilder(@NonNull MapActivity mapActivity) { + super(mapActivity); + } + + public View buildRow(View view, Drawable icon, String buttonText, String textPrefix, String text, + int textColor, String secondaryText, boolean collapsable, CollapsableView collapsableView, boolean needLinks, + int textLinesLimit, boolean isUrl, boolean isNumber, boolean isEmail, View.OnClickListener onClickListener, boolean matchWidthDivider) { + + return buildAisRow(view,null, text, textColor, buttonText,null, textLinesLimit, matchWidthDivider); + + /* + return super.buildRow(view, icon, buttonText, textPrefix, text, textColor, secondaryText, collapsable, collapsableView, needLinks, + textLinesLimit, isUrl, isNumber, isEmail, onClickListener, matchWidthDivider); + */ + /* + return super.buildRow(view, icon, null, textPrefix, text, textColor, buttonText, collapsable, collapsableView, needLinks, + textLinesLimit, isUrl, isNumber, isEmail, onClickListener, matchWidthDivider); + */ + } + + private View buildAisRow(View view, String prefixText, String aisType, int aisTypeColor, String aisValue, + String suffixText, int textLinesLimit, boolean matchWidthDivider) { + boolean light = isLightContent(); + + if (!isFirstRow()) { + buildRowDivider(view); + } + + LinearLayout baseView = new LinearLayout(view.getContext()); + baseView.setOrientation(LinearLayout.VERTICAL); + LinearLayout.LayoutParams llBaseViewParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + baseView.setLayoutParams(llBaseViewParams); + + LinearLayout ll = new LinearLayout(view.getContext()); + ll.setOrientation(LinearLayout.HORIZONTAL); + LinearLayout.LayoutParams llParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + ll.setLayoutParams(llParams); + ll.setBackgroundResource(AndroidUtils.resolveAttribute(view.getContext(), android.R.attr.selectableItemBackground)); + ll.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + String textToCopy = Algorithms.isEmpty(prefixText) ? aisType : prefixText + ": " + aisType; + copyToClipboard(textToCopy, view.getContext()); + return true; + } + }); + + baseView.addView(ll); + + // prefixText + LinearLayout llText = new LinearLayout(view.getContext()); + llText.setOrientation(LinearLayout.VERTICAL); + LinearLayout.LayoutParams llTextViewParams = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT); + llTextViewParams.weight = 1f; + AndroidUtils.setMargins(llTextViewParams, 0, 0, dpToPx(10f), 0); + llTextViewParams.gravity = Gravity.CENTER_VERTICAL; + llText.setLayoutParams(llTextViewParams); + ll.addView(llText); + + TextViewEx textPrefixView = null; + if (!Algorithms.isEmpty(prefixText)) { + textPrefixView = new TextViewEx(view.getContext()); + LinearLayout.LayoutParams llTextParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + AndroidUtils.setMargins(llTextParams, dpToPx(16f), dpToPx(8f), 0, 0); + textPrefixView.setLayoutParams(llTextParams); + textPrefixView.setTypeface(FontCache.getRobotoRegular(view.getContext())); + textPrefixView.setTextSize(12); + textPrefixView.setTextColor(ColorUtilities.getSecondaryTextColor(app, !light)); + textPrefixView.setMinLines(1); + textPrefixView.setMaxLines(1); + textPrefixView.setText(prefixText); + llText.addView(textPrefixView); + } + + // aisType + TextViewEx textView = new TextViewEx(view.getContext()); + LinearLayout.LayoutParams llTextParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + AndroidUtils.setMargins(llTextParams, + dpToPx(16f), dpToPx(textPrefixView != null ? 2f : (suffixText != null ? 10f : 8f)), 0, dpToPx(suffixText != null ? 6f : 8f)); + textView.setLayoutParams(llTextParams); + textView.setTypeface(FontCache.getRobotoRegular(view.getContext())); + textView.setTextSize(16); + textView.setTextColor(ColorUtilities.getPrimaryTextColor(app, !light)); + textView.setText(aisType); + + if (textLinesLimit > 0) { + textView.setMinLines(1); + textView.setMaxLines(textLinesLimit); + textView.setEllipsize(TextUtils.TruncateAt.END); + } + if (aisTypeColor > 0) { + textView.setTextColor(getColor(aisTypeColor)); + } + llText.addView(textView); + + // suffixText + if (!TextUtils.isEmpty(suffixText)) { + TextViewEx textViewSecondary = new TextViewEx(view.getContext()); + LinearLayout.LayoutParams llTextSecondaryParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + AndroidUtils.setMargins(llTextSecondaryParams, dpToPx(16f), 0, 0, dpToPx(6f)); + textViewSecondary.setLayoutParams(llTextSecondaryParams); + textViewSecondary.setTypeface(FontCache.getRobotoRegular(view.getContext())); + textViewSecondary.setTextSize(14); + textViewSecondary.setTextColor(ColorUtilities.getSecondaryTextColor(app, !light)); + textViewSecondary.setText(suffixText); + llText.addView(textViewSecondary); + } + + // aisValue + if (!TextUtils.isEmpty(aisValue)) { + TextViewEx buttonTextView = new TextViewEx(view.getContext()); + LinearLayout.LayoutParams buttonTextViewParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + buttonTextViewParams.gravity = Gravity.CENTER_VERTICAL; + AndroidUtils.setMargins(buttonTextViewParams, dpToPx(8), 0, dpToPx(8), 0); + buttonTextView.setLayoutParams(buttonTextViewParams); + buttonTextView.setTypeface(FontCache.getRobotoMedium(view.getContext())); + buttonTextView.setTextSize(16); + buttonTextView.setTextColor(ContextCompat.getColor(view.getContext(), !light ? R.color.ctx_menu_controller_button_text_color_dark_n : R.color.ctx_menu_controller_button_text_color_light_n)); + buttonTextView.setText(aisValue); + ll.addView(buttonTextView); + } + + ((LinearLayout) view).addView(baseView); + + rowBuilt(); + + setDividerWidth(matchWidthDivider); + + return ll; + } +} \ No newline at end of file diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java new file mode 100644 index 00000000000..a5119df39e4 --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisObjectMenuController.java @@ -0,0 +1,264 @@ +package net.osmand.plus.plugins.aistracker; + +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_ATON; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.AIS_ATON_VIRTUAL; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.UNSPECIFIED_AID_TYPE; +import static net.osmand.plus.plugins.aistracker.AisTrackerHelper.getCpa; +import static net.osmand.plus.utils.OsmAndFormatter.FORMAT_MINUTES; + +import android.annotation.SuppressLint; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import net.osmand.Location; +import net.osmand.LocationConvert; +import net.osmand.data.LatLon; +import net.osmand.data.PointDescription; +import net.osmand.plus.OsmandApplication; +import net.osmand.plus.R; +import net.osmand.plus.activities.MapActivity; +import net.osmand.plus.mapcontextmenu.MenuController; + +import java.util.Iterator; +import java.util.SortedSet; + +public class AisObjectMenuController extends MenuController { + private AisObject aisObject; + private final OsmandApplication app; + public AisObjectMenuController(@NonNull MapActivity mapActivity, @NonNull PointDescription pointDescription, + AisObject aisObject) { + //super(new MenuBuilder(mapActivity), pointDescription, mapActivity); + super(new AisObjectMenuBuilder(mapActivity), pointDescription, mapActivity); + + this.aisObject = aisObject; + this.app = builder.getApplication(); + builder.setShowTitleIfTruncated(false); + builder.setShowNearestPoi(false); + builder.setShowOnlinePhotos(false); + builder.setShowNearestWiki(false); + } + + @Override + public int getRightIconId() { return R.drawable.ic_plugin_nautical_map; } + @Override + public boolean isBigRightIcon() { + return true; + } + + @SuppressLint("DefaultLocale") + private void addCpaInfo(@Nullable Location myLocation, @NonNull SortedSet msgTypes) { + if (msgTypes.contains(21) || msgTypes.contains(9)) { + return; + } + if ((aisObject.getCog() != AisObjectConstants.INVALID_COG) && + (aisObject.getSog() != AisObjectConstants.INVALID_SOG)) { + AisTrackerHelper.Cpa cpa = new AisTrackerHelper.Cpa(); + Location aisLocation = aisObject.getCurrentLocation(); + if ((aisLocation != null) && (myLocation != null)) { + getCpa(myLocation, aisLocation, cpa); + if (cpa.isValid()) { + double cpaTime = cpa.getTcpa(); + boolean isPositive = cpaTime >= 0; + cpaTime = Math.abs(cpaTime); + if (cpaTime < Long.MAX_VALUE) { + if (isPositive) { + long hours = (long)cpaTime; + double minutes = (cpaTime % 1 - hours) * 60.0; + addMenuItem("CPA", String.format("%.1f nm", cpa.getCpaDist())); + if (hours >= 2.0) { + addMenuItem("TCPA", String.format("%d hours %.0f min", hours, minutes)); + } else if (hours >= 1.0) { + addMenuItem("TCPA", String.format("%d hour %.0f min", hours, minutes)); + } else { + addMenuItem("TCPA", String.format("%.0f min", minutes)); + } + } + /* else { + addMenuItem("CPA", String.format("%.1f nm", cpa.getCpaDist())); + addMenuItem("TCPA", String.format("-%.1f hours", cpaTime)); + } + */ + } + } + } + } + } + + private void addMenuItem(@NonNull String type, @Nullable String value) { + if (value != null) { + if (!value.isEmpty()) { + addPlainMenuItem(0, value, type, false, false, null); + } + } + } + private void addMenuItem(@NonNull String type, @Nullable String value, + @Nullable SortedSet msgTypes, Integer selection[]) { + if (msgTypes != null) { + for (Integer i : selection) { + if (msgTypes.contains(i)) { + addMenuItem(type, value); + break; + } + } + } + } + private void addMenuItemDimension() { + if (((aisObject.getDimensionToBow() != AisObjectConstants.INVALID_DIMENSION) || + (aisObject.getDimensionToStern() != AisObjectConstants.INVALID_DIMENSION)) && + ((aisObject.getDimensionToPort() != AisObjectConstants.INVALID_DIMENSION) || + (aisObject.getDimensionToStarboard() != AisObjectConstants.INVALID_DIMENSION))) { + addMenuItem("Dimension", + Integer.toString(aisObject.getDimensionToBow() + aisObject.getDimensionToStern()) + + "m x " + + Integer.toString(aisObject.getDimensionToPort() + aisObject.getDimensionToStarboard()) + + "m"); + } + } + + @SuppressLint("DefaultLocale") + @Override + public void addPlainMenuItems(String typeStr, PointDescription pointDescription, LatLon latLon) { + SortedSet msgTypes = aisObject.getMsgTypes(); + Iterator iter = msgTypes.iterator(); + String msgTypesString = ""; + LatLon position = aisObject.getPosition(); + long lastUpdate = (System.currentTimeMillis() - aisObject.getLastUpdate()) / 1000; + + addMenuItem("MMSI", Integer.toString(aisObject.getMmsi())); + if (position != null) { + addMenuItem("Position", + LocationConvert.convertLatitude(position.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(position.getLongitude(), FORMAT_MINUTES, true) ); + if (this.app != null) { + Location ownPosition = app.getLocationProvider().getLastKnownLocation(); + AisObject.setOwnPosition(ownPosition); + float distance = aisObject.getDistanceInNauticalMiles(); + float bearing = aisObject.getBearing(); + if (distance >= 0.0f) { + try { + addMenuItem("Distance", String.format("%.1f nm", distance)); + } catch (Exception ignore) { } + } + if (bearing >= 0.0f) { + try { + addMenuItem("Bearing", String.format("%.0f", bearing)); + } catch (Exception ignore) { } + } + addCpaInfo(ownPosition, msgTypes); + } + } + if (msgTypes.contains(21)) { // ATON (aid to navigation) + addMenuItem("Aid Type", aisObject.getAidTypeString()); + addMenuItemDimension(); + } else if (msgTypes.contains(9)) { // SAR aircraft + addMenuItem("Object Type", "SAR Aircraft"); + if (aisObject.getCog() != AisObjectConstants.INVALID_COG) { + addMenuItem("COG", String.format("%.0f", aisObject.getCog())); + } + if (aisObject.getSog() != AisObjectConstants.INVALID_SOG) { + addMenuItem("SOG", String.format("%.1f kts", aisObject.getSog())); + } + if (aisObject.getAltitude() != AisObjectConstants.INVALID_ALTITUDE) { + addMenuItem("Altitude", String.valueOf(aisObject.getAltitude()) + " m"); + } + } else { + addMenuItem("Callsign", aisObject.getCallSign()); + if (aisObject.getImo() != 0 ) { + addMenuItem("IMO", Integer.toString(aisObject.getImo()), msgTypes, new Integer[]{5}); + } + addMenuItem("Shipname", aisObject.getShipName()); + addMenuItem("Shiptype", aisObject.getShipTypeString(), msgTypes, new Integer[]{5, 19, 24}); + if (aisObject.getNavStatus() != AisObjectConstants.INVALID_NAV_STATUS) { + addMenuItem("Navigation Status", aisObject.getNavStatusString()); + } + if (aisObject.getCog() != AisObjectConstants.INVALID_COG) { + addMenuItem("COG", String.format("%.0f", aisObject.getCog())); + } + if (aisObject.getSog() != AisObjectConstants.INVALID_SOG) { + addMenuItem("SOG", String.format("%.1f kts", aisObject.getSog())); + } + if (aisObject.getHeading() != AisObjectConstants.INVALID_HEADING) { + addMenuItem("Heading", String.valueOf(aisObject.getHeading())); + } + if (aisObject.getRot() != AisObjectConstants.INVALID_ROT) { + addMenuItem("Rate of Turn", String.valueOf(aisObject.getRot())); + } + addMenuItemDimension(); + if (aisObject.getDraught() != AisObjectConstants.INVALID_DRAUGHT) { + addMenuItem("Draught", String.valueOf(aisObject.getDraught()) + " m"); + } + addMenuItem("Destination", aisObject.getDestination()); + if ((aisObject.getEtaDay() != AisObjectConstants.INVALID_ETA) && + (aisObject.getEtaHour() != AisObjectConstants.INVALID_ETA_HOUR) && + (aisObject.getEtaMin() != AisObjectConstants.INVALID_ETA_MIN) && + (aisObject.getEtaMon() != AisObjectConstants.INVALID_ETA)) { + @SuppressLint("DefaultLocale") String eta = new String(aisObject.getEtaDay() + "." + + aisObject.getEtaMon() + ". " + String.format("%02d", aisObject.getEtaHour()) + ":" + + String.format("%02d", aisObject.getEtaMin())); + addMenuItem("ETA", eta); + } + } + if (lastUpdate > 60) { + addMenuItem("Last Update", (lastUpdate / 60) + + " min " + (lastUpdate % 60) + " sec"); + } else { + addMenuItem("Last Update", lastUpdate + " sec"); + } + boolean hasNext = iter.hasNext(); + while (hasNext) { + msgTypesString = msgTypesString.concat(Integer.toString(iter.next())); + hasNext = iter.hasNext(); + if (hasNext) { msgTypesString = msgTypesString.concat(", "); } + } + addMenuItem("Message Type(s)", msgTypesString); + super.addPlainMenuItems(typeStr, pointDescription, latLon); + } + + @Override + protected void setObject(Object object) { + if (object instanceof AisObject) { + this.aisObject = (AisObject) object; + } + } + + @Override + protected Object getObject() { + return aisObject; + } + @Override + public CharSequence getAdditionalInfoStr() { return "Country: " + aisObject.getCountryCode(); } + + @NonNull + @Override + public String getTypeStr() { + String result = ""; + SortedSet msgTypes = aisObject.getMsgTypes(); + AisObjectConstants.AisObjType objectClass = aisObject.getObjectClass(); + for (Integer i : new Integer[]{5, 19, 24}) { + if (msgTypes.contains(i)) { + result += aisObject.getShipTypeString(); + break; + } + } + for (Integer i : new Integer[]{1, 2, 3}) { + if (msgTypes.contains(i)) { + if (result.isEmpty()) { + result = "Vessel"; + } + result += ": " + aisObject.getNavStatusString() + "."; + break; + } + } + if ((objectClass == AIS_ATON) || (objectClass == AIS_ATON_VIRTUAL)) { + int aidType = aisObject.getAidType(); + if (aidType != UNSPECIFIED_AID_TYPE) { + result = aisObject.getAidTypeString(); + } + } + return (result.isEmpty() ? "AIS object" : result); + } + + @Override + public boolean needStreetName() { return false; } +} \ No newline at end of file diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java new file mode 100644 index 00000000000..bd9abd11d7c --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerHelper.java @@ -0,0 +1,292 @@ +package net.osmand.plus.plugins.aistracker; + +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_CPA; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.INVALID_TCPA; + +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.util.Pair; + +import com.jwetherell.openmap.common.LatLonPoint; + +import net.osmand.Location; +import net.osmand.plus.OsmAndLocationProvider; + +public final class AisTrackerHelper { + private static long lastCorrectionUpdate = 0; + private static double correctionFactor = 1.0d; + private static final long maxCorrectionUpdateAgeInMin = 60; + + private static class Vector { + public double x; // Longitude (grows in East direction) + public double y; // Latitude (grows in North direction) + public Vector(double a, double b) { + this.x = a; + this.y = b; + } + public Vector(@NonNull Vector a) { + this.x = a.x; + this.y = a.y; + } + @NonNull + public Vector sub(@NonNull Vector a) { + return new Vector(this.x - a.x, this.y - a.y); + } + public double dot(@NonNull Vector a) { return (this.x * a.x) + (this.y * a.y); } + } + public static class Cpa { + private double tcpa; // in hours + private float cpaDist; // in miles + private Location newPos1; // position of first object at time tcpa + private Location newPos2; // position of first object at time tcpa + private double t1 = 0.0d; // time for object 1 to cross the course of object 2 + private double t2 = 0.0d; // time for object 2 to cross the course of object 1 + private boolean valid; + public Cpa() { + reset(); + } + public void reset() { + cpaDist = INVALID_CPA; + tcpa = INVALID_TCPA; + newPos1 = null; + newPos2 = null; + valid = false; + t1 = t2 = 0.0d; + } + public void setTcpa(double x) { this.tcpa = x; } + public void setCpaDist(float x) { this.cpaDist = x; } + public void setCpaPos1(Location loc) { this.newPos1 = loc; } + public void setCpaPos2(Location loc) { this.newPos2 = loc; } + public void setCrossingTimes(@Nullable Pair t) { + if (t != null) { + t1 = t.first; t2 = t.second; + } + }; + public double getCrossingTime1() { return t1; } + public double getCrossingTime2() { return t2; } + public double getTcpa() { return tcpa; } + public float getCpaDist() { return cpaDist; } + public Location getCpaPos1() { return newPos1; } + public Location getCpaPos2() { return newPos2; } + public void validate() { valid = true; } + public boolean isValid() { return valid; } + } + + /* calculate the Time to Closest Point of Approach (TCPA) of two moving objects: + * object 1 at position x and velocity vector vx + * object 2 at position y and velocity vector vy, + * For the calculation, cartesian coordinates are assumed with a cartesian distance metric + * -> attention: by using spherical coordinates, this will produce an error! */ + private static double getTcpa(@NonNull Vector x, @NonNull Vector y, + @NonNull Vector vx, @NonNull Vector vy, double lonCorrection) { + Vector dx = new Vector( y.sub(x)); + Vector dv = new Vector(vy.sub(vx)); + double divisor = dv.dot(dv); + if ((Math.abs(divisor) < 1.0E-10f) || (lonCorrection < 1.0E-10f)) { + // avoid div by 0 or invalid lonCorrection + return INVALID_TCPA; + } + return -(((dx.x * dv.x / lonCorrection) + (dx.y * dv.y)) / divisor); + } + + /* to calculate the Time to Closest Point of Approach (TCPA) between the objects x and y, + * it is presumed that x and y both contain their position, speed and course */ + private static double getTcpa(@NonNull Location x, @NonNull Location y, double lonCorrection) { + if (checkSpeedAndBearing(x, y)) { + return INVALID_TCPA; + } + return getTcpa(locationToVector(x), locationToVector(y), + courseToVector(x.getBearing(), getSpeedInKnots(x)), + courseToVector(y.getBearing(), getSpeedInKnots(y)), lonCorrection); + } + + public static double getTcpa(@NonNull Location ownLocation, @NonNull Location otherLocation) { + return getTcpa(ownLocation, otherLocation, getLonCorrection(ownLocation)); + } + + @Nullable + private static Location getCpa(@NonNull Location x, @NonNull Location y, boolean useFirstAsReference) { + if (checkSpeedAndBearing(x, y)) { + return null; + } + double tcpa = getTcpa(x,y); + if (tcpa == INVALID_TCPA) { + return null; + } else { + Location base = useFirstAsReference ? x : y; + return getNewPosition(base, tcpa); + } + } + + /* to calculate the Closest Point of Approach (CPA) between the objects x and y, + * it is presumed that x and y both contain their position, speed and course. + * This function returns the position of first object x at time of TCPA */ + @Nullable + public static Location getCpa1(@NonNull Location x, @NonNull Location y) { + return getCpa(x, y, true); + } + + /* to calculate the Closest Point of Approach (CPA) between the objects x and y, + * it is presumed that x and y both contain their position, speed and course. + * This function returns the position of second object y at time of TCPA */ + @Nullable + public static Location getCpa2(@NonNull Location x, @NonNull Location y) { + return getCpa(x, y, false); + } + + /* calculate the distance between the given objects at their Closest Point of Approach (CPA) */ + public static float getCpaDistance(@NonNull Location x, @NonNull Location y) { + Location cpaX = getCpa1(x,y); + Location cpaY = getCpa2(x,y); + if ((cpaX != null) && (cpaY != null)) { + return meterToMiles(cpaX.distanceTo(cpaY)); + } else { + return INVALID_CPA; + } + } + + public static void getCpa(@NonNull Location ownLocation, @NonNull Location otherLocation, + @NonNull Cpa result) { + if (!checkSpeedAndBearing(ownLocation, otherLocation)) { + double tcpa = getTcpa(ownLocation, otherLocation); + if (tcpa != INVALID_TCPA) { + Location cpaX = getNewPosition(ownLocation, tcpa); + Location cpaY = getNewPosition(otherLocation, tcpa); + PaircrossingTimes = getCrossingTimes(ownLocation, otherLocation); + result.setCrossingTimes(crossingTimes); + result.setTcpa(tcpa); + result.setCpaPos1(cpaX); + result.setCpaPos2(cpaY); + if ((cpaX != null) && (cpaY != null)) { + result.setCpaDist(meterToMiles(cpaX.distanceTo(cpaY))); + result.validate(); + } + } + } + } + + private static double bearingInRad(float bearingInDegrees) { + double res = bearingInDegrees * 2 * Math.PI / 360.0; + while (res >= Math.PI) { res -= (2 * Math.PI); } + return res; + } + + /* This method takes the two locations (including position, course and speed) + and calculates the time when the two objects reach the location where the course lines + are crossing. + for each object, the time may be different or even in the past, hence a pair of two + times is returned + in error case or if the courses do not cross each other, Null is returned + * */ + @Nullable + private static Pair getCrossingTimes(@NonNull Location x, @NonNull Location y) { + double lonCorrection = getLonCorrection(x); + Vector vX = locationToVector(x, lonCorrection); // position 1 at time t0 + Vector vY = locationToVector(y, lonCorrection); // position 2 at time t0 + Vector vVX = courseToVector(x.getBearing(), getSpeedInKnots(x)); // velocity vector 1 + Vector vVY = courseToVector(y.getBearing(), getSpeedInKnots(y)); // velocity vector 2 + Vector vDXY = vX.sub(vY); // position difference at time t0 + double divisor = vVX.x * vVY.y - vVX.y * vVY.x; + if ((Math.abs(divisor) < 1.0E-10f) || (lonCorrection < 1.0E-10f)) { + // avoid div by 0 or invalid lonCorrection + Log.d("AisTrackerHelper", "getCollisionTimes(): Division by 0: divisor->" + + divisor + ", lonCorrection->" + lonCorrection); + return null; + } + Pair result = new Pair((vVY.x * vDXY.y - vVY.y * vDXY.x) / divisor, + (vVX.x * vDXY.y - vVX.y * vDXY.x) / divisor); + /* Log.d("AisTrackerHelper", "getCollisionTimes(): t1->" + + result.first.toString() + ", t2->" + result.second.toString()); + */ + + return result; + } + + @Nullable + public static Location getNewPosition(@Nullable Location loc, double timeInHours) { + if (loc != null) { + if (loc.hasBearing() && loc.hasSpeed()) { + LatLonPoint a = new LatLonPoint(loc.getLatitude(), loc.getLongitude()); + LatLonPoint b = a.getPoint(loc.getSpeed() * timeInHours * Math.PI / 5556.0, + bearingInRad(loc.getBearing())); + Location newX = new Location(loc); + newX.setLongitude(b.getLongitude()); + newX.setLatitude(b.getLatitude()); + return newX; + } else { + /* Log.d("AisTrackerHelper", "getNewPosition(): loc.hasBearing->" + + loc.hasBearing() + ", loc.hasSpeed->" + loc.hasSpeed() + + ", speed->" + loc.getSpeed()); + */ + return null; + } + } else { + return null; + } + } + + private static double calculateLonCorrection(@Nullable Location loc) { + if (loc != null) { + Location x = new Location(loc); + // simulate a "measurement" trip towards East... + x.setSpeed(knotsToMeterPerSecond(1.0f)); // speed -> 1 kn + x.setBearing(90.0f); // course -> east + Location yEast = getNewPosition(x, 1.0); // new position after 1 hour + + if (yEast != null) { + double diffLon = yEast.getLongitude() - x.getLongitude(); + return diffLon * 60.0; // correction factor for longitude + } + } + return 1.0f; // fallback + } + + private static double getLonCorrection(@Nullable Location loc) { + long now = System.currentTimeMillis(); + if (((now - lastCorrectionUpdate) / 1000 / 60) > maxCorrectionUpdateAgeInMin) { + correctionFactor = calculateLonCorrection(loc); + lastCorrectionUpdate = now; + } + return correctionFactor; + } + + public static float knotsToMeterPerSecond(float speed) { + return speed * 1852 / 3600; + } + public static float meterPerSecondToKnots(float speed) { + return speed * 3600 / 1852; + } + public static float meterToMiles(float x) { + return x / 1852.0f; + } + + /* calculate a velocity vector from given course (COG) and speed (SOG). + COG is given as heading, SOG as scalar */ + @NonNull + private static Vector courseToVector(double cog, double sog) { + double alpha = 450.0d - cog; + while (alpha < 0) { alpha += 360.0d; } + while (alpha >= 360.0d ) { alpha -= 360.0d; } + alpha = Math.toRadians(alpha); + return new Vector(Math.cos(alpha) * sog, Math.sin(alpha) * sog); + } + + @NonNull + private static Vector locationToVector(@NonNull Location loc) { + return new Vector(loc.getLongitude() * 60.0, loc.getLatitude() * 60.0); + } + + private static Vector locationToVector(@NonNull Location loc, double lonCorrection) { + return new Vector(loc.getLongitude() * 60.0 / lonCorrection, loc.getLatitude() * 60.0); + } + + private static boolean checkSpeedAndBearing(@NonNull Location x, @NonNull Location y) { + return !x.hasBearing() || !y.hasBearing() || !x.hasSpeed() || !y.hasSpeed(); + } + + private static float getSpeedInKnots(@NonNull Location loc) { + return meterPerSecondToKnots(loc.getSpeed()); + } +} diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java new file mode 100644 index 00000000000..fbc2e2016f9 --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerLayer.java @@ -0,0 +1,631 @@ +package net.osmand.plus.plugins.aistracker; + +import static net.osmand.plus.plugins.aistracker.AisTrackerHelper.getCpa; +import static net.osmand.plus.plugins.aistracker.AisTrackerHelper.knotsToMeterPerSecond; +import static net.osmand.plus.plugins.aistracker.AisTrackerHelper.meterToMiles; +import static net.osmand.plus.plugins.aistracker.AisObjectConstants.AisObjType.*; +import static net.osmand.plus.utils.OsmAndFormatter.FORMAT_MINUTES; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PointF; +import android.util.Log; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import net.osmand.Location; +import net.osmand.LocationConvert; +import net.osmand.core.android.MapRendererView; +import net.osmand.core.jni.PointI; +import net.osmand.data.LatLon; +import net.osmand.data.PointDescription; +import net.osmand.data.RotatedTileBox; +import net.osmand.plus.OsmandApplication; +import net.osmand.plus.utils.NativeUtilities; +import net.osmand.plus.views.layers.ContextMenuLayer; +import net.osmand.plus.views.layers.base.OsmandMapLayer; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.SortedSet; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +public class AisTrackerLayer extends OsmandMapLayer implements ContextMenuLayer.IContextMenuProvider { + private static final int START_ZOOM = 10; + private final AisTrackerPlugin plugin; + private ConcurrentMap aisObjectList; + private static final int aisObjectListCounterMax = 200; + private final Context context; + private final Paint bitmapPaint; + private Timer timer; + private AisMessageListener listener; + public AisTrackerLayer(@NonNull Context context, @NonNull AisTrackerPlugin plugin) { + super(context); + this.plugin = plugin; + this.context = context; + this.listener = null; + + this.aisObjectList = new ConcurrentHashMap<>(); + this.bitmapPaint = new Paint(); + this.bitmapPaint.setAntiAlias(true); + this.bitmapPaint.setFilterBitmap(true); + this.bitmapPaint.setStrokeWidth(4); + this.bitmapPaint.setColor(Color.DKGRAY); + + AisObject.setCpaWarningTime(plugin.AIS_CPA_WARNING_TIME.get()); + AisObject.setCpaWarningDistance(plugin.AIS_CPA_WARNING_DISTANCE.get()); + + initTimer(); + startNetworkListener(); + + // for test purposes: remove/disable later... + //initTestObject1(); + //initTestObject2(); + //initTestObject3(); + //initTestObject4(); + //initTestObject5(); + //testCrossingTimes(); + //testCpa(); + //initFakePosition(); + } + + private void testCrossingTimes() { + // here some tests for the geo (CPA) calculation + // intention is to test the function to calculate times of two objects with crossing courses + // define 12 positions on two courses: position a1 ... a6 at course line A (course 90°) + // and position b1 ... b6 at course line B (course 45°) + // the positions are taken from a (paper) map + // for coordinate transformation see https://www.koordinaten-umrechner.de + AisTrackerHelper.Cpa cpa = new AisTrackerHelper.Cpa(); + Location a1 = new Location("test", 49.5d, -3.266667d); // 49°30'N, 3°16'W + Location a2 = new Location("test", 49.5d, -3.166667d); // 49°30'N, 3°10'W + Location a3 = new Location("test", 49.5d, -3.116667d); // 49°30'N, 3°7'W + Location a4 = new Location("test", 49.5d, -3.093333d); // 49°30'N, 3°5.6'W + Location a5 = new Location("test", 49.5d, -3.05d); // 49°30'N, 3°3'W + Location a6 = new Location("test", 49.5d, -3.016667d); // 49°30'N, 3°1'W + Location b1 = new Location("test", 49.395d, -3.25d); // 49°23.7'N, 3°15'W + Location b2 = new Location("test", 49.441667d, -3.183333d); // 49°26.5'N, 3°11'W + Location b3 = new Location("test", 49.47d, -3.133333d); // 49°28.2'N, 3°8'W + Location b4 = new Location("test", 49.5d, -3.093333d); // 49°30'N, 3°5.6'W + Location b5 = new Location("test", 49.513333d, -3.066667d); // 49°30.8'N, 3°4'W + Location b6 = new Location("test", 49.538333d, -3.033333d); // 49°32.3'N, 3°2'W + a1.setSpeed(knotsToMeterPerSecond(1.0f)); a1.setBearing(90.0f); + a2.setSpeed(knotsToMeterPerSecond(1.0f)); a2.setBearing(90.0f); + a3.setSpeed(knotsToMeterPerSecond(1.0f)); a3.setBearing(90.0f); + a4.setSpeed(knotsToMeterPerSecond(1.0f)); a4.setBearing(90.0f); + a5.setSpeed(knotsToMeterPerSecond(1.0f)); a5.setBearing(90.0f); + a6.setSpeed(knotsToMeterPerSecond(1.0f)); a6.setBearing(90.0f); + b1.setSpeed(knotsToMeterPerSecond(1.0f)); b1.setBearing(45.0f); + b2.setSpeed(knotsToMeterPerSecond(1.0f)); b2.setBearing(45.0f); + b3.setSpeed(knotsToMeterPerSecond(1.0f)); b3.setBearing(45.0f); + b4.setSpeed(knotsToMeterPerSecond(1.0f)); b4.setBearing(45.0f); + b5.setSpeed(knotsToMeterPerSecond(1.0f)); b5.setBearing(45.0f); + b6.setSpeed(knotsToMeterPerSecond(1.0f)); b6.setBearing(45.0f); + // now trigger the calculations: + cpa.reset(); getCpa(a3, b2, cpa); // expected: t1>0, t2>0 + Log.d("AisTrackerLayer", "# test(a3, b2): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a3, b3, cpa); // expected: t1>0, t2>0 + Log.d("AisTrackerLayer", "# test(a3, b3): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a3, b4, cpa); // expected: t1>0, t2->0 + Log.d("AisTrackerLayer", "# test(a3, b4): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a3, b5, cpa); // expected: t1>0, t2<0 + Log.d("AisTrackerLayer", "# test(a3, b5): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a3, b6, cpa); // expected: t1>0, t2<0 + Log.d("AisTrackerLayer", "# test(a3, b6): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a4, b2, cpa); // expected: t1->0, t2>0 + Log.d("AisTrackerLayer", "# test(a4, b2): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a4, b3, cpa); // expected: t1->0, t2>0 + Log.d("AisTrackerLayer", "# test(a4, b3): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a4, b4, cpa); // expected: t1->0, t2->0 + Log.d("AisTrackerLayer", "# test(a4, b4): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a4, b5, cpa); // expected: t1->0, t2<0 + Log.d("AisTrackerLayer", "# test(a4, b5): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a4, b6, cpa); // expected: t1->0, t2<0 + Log.d("AisTrackerLayer", "# test(a4, b6): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a5, b2, cpa); // expected: t1<0, t2>0 + Log.d("AisTrackerLayer", "# test(a5, b2): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a5, b3, cpa); // expected: t1<0, t2>0 + Log.d("AisTrackerLayer", "# test(a5, b3): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a5, b4, cpa); // expected: t1<0, t2->0 + Log.d("AisTrackerLayer", "# test(a5, b4): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a5, b5, cpa); // expected: t1<0, t2<0 + Log.d("AisTrackerLayer", "# test(a5, b5): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + cpa.reset(); getCpa(a5, b6, cpa); // expected: t1<0, t2<0 + Log.d("AisTrackerLayer", "# test(a5, b6): t1->" + cpa.getCrossingTime1() + + ", t2->" + cpa.getCrossingTime2() + ", CPA-Dist-> " + cpa.getCpaDist() + + ", TCPA ->" + cpa.getTcpa()); + + } + + private void testCpa() { + // here some tests for the geo (CPA) calculation + // define 3 (vessel) objects + // for coordinate transformation see https://www.koordinaten-umrechner.de + Location x1 = new Location("test", 49.5d, -1.0d); // 49°30'N, 1°00'W + Location x2 = new Location("test", 49.916667d, 0.416667d); // 49°55'N, 0°25'E + Location x3 = new Location("test", 49.666667d, -0.75d); // 49°40'N, 0°45'W + Location x4 = new Location("test", 49.5d, -4.0d); // 49°30'N, 4°00'W + Location x5 = new Location("test", 50.0d, -3.75d); // 50°00'N, 3°45'W + // taken from marine chart: distances: x1 - x3: 13.8 nm, x2 - x3: 47,2 nm, x4 - x5: 31.4 nm + Location y1, y2, y3, y4, y5; + Log.d("AisTrackerLayer", "# test0: position 1 after 0 hours: " + + LocationConvert.convertLatitude(x1.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(x1.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test0: position 2 after 0 hours: " + + LocationConvert.convertLatitude(x2.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(x2.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test0: position 3 after 0 hours: " + + LocationConvert.convertLatitude(x3.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(x3.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test0: position 4 after 0 hours: " + + LocationConvert.convertLatitude(x4.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(x4.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test0: position 5 after 0 hours: " + + LocationConvert.convertLatitude(x5.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(x5.getLongitude(), FORMAT_MINUTES, true)); + + // test case: x1: course 0°, speed 5kn, x3: course 270°, speed 10kn, time: 1h, 1.5h + // taken from marine chart: + // position after 1h: x1: 49°35'N, 1°00'W, x3: 49°40'N, 1°0.5'W, distance: 5.0nm + // position after 1.5h: x1: 49°37.5'N, 1°00'W, x3: 49°40'N, 1°8.5'W, distance: 6.0nm + x1.setSpeed(knotsToMeterPerSecond(5.0f)); + x1.setBearing(0.0f); + x3.setSpeed(knotsToMeterPerSecond(10.0f)); + x3.setBearing(270.0f); + AisTrackerHelper.Cpa cpa1 = new AisTrackerHelper.Cpa(); + getCpa(x1, x3, cpa1); + Log.d("AisTrackerLayer", "# test1: tcpa(x1, x3): " + cpa1.getTcpa()); + Log.d("AisTrackerLayer", "# test1: dist at tcpa: " + cpa1.getCpaDist()); + Log.d("AisTrackerLayer", "# test1: dist0: " + meterToMiles(x1.distanceTo(x3))); + y1 = AisTrackerHelper.getNewPosition(x1, 1.0); + y3 = AisTrackerHelper.getNewPosition(x3, 1.0); + if ((y1 != null) && (y3 != null)) { + Log.d("AisTrackerLayer", "# test1: position 1 after 1 hour: " + + LocationConvert.convertLatitude(y1.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y1.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test1: position 3 after 1 hour: " + + LocationConvert.convertLatitude(y3.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y3.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test1: dist1: " + meterToMiles(y1.distanceTo(y3))); + } + y1 = AisTrackerHelper.getNewPosition(x1, 1.18); + y3 = AisTrackerHelper.getNewPosition(x3, 1.18); + if ((y1 != null) && (y3 != null)) { + Log.d("AisTrackerLayer", "# test1: position 1 after 1.18 hours: " + + LocationConvert.convertLatitude(y1.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y1.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test1: position 3 after 1.18 hours: " + + LocationConvert.convertLatitude(y3.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y3.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test1: dist1: " + meterToMiles(y1.distanceTo(y3))); + } + y1 = AisTrackerHelper.getNewPosition(x1, 1.5); + y3 = AisTrackerHelper.getNewPosition(x3, 1.5); + if ((y1 != null) && (y3 != null)) { + Log.d("AisTrackerLayer", "# test1: position 1 after 1.5 hours: " + + LocationConvert.convertLatitude(y1.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y1.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test1: position 3 after 1.5 hours: " + + LocationConvert.convertLatitude(y3.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y3.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test1: dist1: " + meterToMiles(y1.distanceTo(y3))); + } + + // test case: x1: course 0°, speed 5kn, x3: course 270°, speed 5kn, time 1h, 1.5h, 2h + // taken from marine chart: + // position after 1h: x1: 49°35'N, 1°00'W, x3: 49°40'N, 0°52.7'W, distance: 6.8nm + // position after 1.5h: x1: 49°37.5'N, 1°00'W, x3: 49°40'N, 0°56.7'W, distance: 3.1nm + // position after 2h: x1: 49°40'N, 1°00'W, x3: 49°40'N, 1°0.5'W, distance: 0.3nm + x1.setSpeed(knotsToMeterPerSecond(5.0f)); + x1.setBearing(0.0f); + x3.setSpeed(knotsToMeterPerSecond(5.0f)); + x3.setBearing(270.0f); + cpa1.reset(); + getCpa(x1, x3, cpa1); + Log.d("AisTrackerLayer", "# test2: tcpa(x1, x3): " + cpa1.getTcpa()); + Log.d("AisTrackerLayer", "# test2: dist at tcpa: " + cpa1.getCpaDist()); + Log.d("AisTrackerLayer", "# test2: dist0: " + meterToMiles(x1.distanceTo(x3))); + y1 = AisTrackerHelper.getNewPosition(x1, 1.0); + y3 = AisTrackerHelper.getNewPosition(x3, 1.0); + if ((y1 != null) && (y3 != null)) { + Log.d("AisTrackerLayer", "# test2: position 1 after 1 hour: " + + LocationConvert.convertLatitude(y1.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y1.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test2: position 3 after 1 hour: " + + LocationConvert.convertLatitude(y3.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y3.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test2: dist1: " + meterToMiles(y1.distanceTo(y3))); + } + y1 = AisTrackerHelper.getNewPosition(x1, 1.5); + y3 = AisTrackerHelper.getNewPosition(x3, 1.5); + if ((y1 != null) && (y3 != null)) { + Log.d("AisTrackerLayer", "# test2: position 1 after 1.5 hours: " + + LocationConvert.convertLatitude(y1.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y1.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test2: position 3 after 1.5 hours: " + + LocationConvert.convertLatitude(y3.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y3.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test2: dist1: " + meterToMiles(y1.distanceTo(y3))); + } + y1 = AisTrackerHelper.getNewPosition(x1, 2.0); + y3 = AisTrackerHelper.getNewPosition(x3, 2.0); + if ((y1 != null) && (y3 != null)) { + Log.d("AisTrackerLayer", "# test2: position 1 after 2 hours: " + + LocationConvert.convertLatitude(y1.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y1.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test2: position 3 after 2 hours: " + + LocationConvert.convertLatitude(y3.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y3.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test2: dist1: " + meterToMiles(y1.distanceTo(y3))); + } + + // test case: x2: course 270°, speed 5kn, x3: course 45°, speed 5kn, time 5h + // taken from marine chart: + // position after 5h: x2: 49°55'N, 0°14.1'W, x3: 49°57.8'N, 0°17.5'W, distance: 3.5nm + x2.setSpeed(knotsToMeterPerSecond(5.0f)); + x2.setBearing(270.0f); + x3.setSpeed(knotsToMeterPerSecond(5.0f)); + x3.setBearing(45.0f); + cpa1.reset(); + getCpa(x2, x3, cpa1); + Log.d("AisTrackerLayer", "# test3: tcpa(x1, x3): " + cpa1.getTcpa()); + Log.d("AisTrackerLayer", "# test3: dist at tcpa: " + cpa1.getCpaDist()); + Log.d("AisTrackerLayer", "# test3: dist0: " + meterToMiles(x2.distanceTo(x3))); + y2 = AisTrackerHelper.getNewPosition(x2, 5.0); + y3 = AisTrackerHelper.getNewPosition(x3, 5.0); + if ((y2 != null) && (y3 != null)) { + Log.d("AisTrackerLayer", "# test3: position 2 after 5 hours: " + + LocationConvert.convertLatitude(y2.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y2.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test3: position 3 after 5 hours: " + + LocationConvert.convertLatitude(y3.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y3.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test3: dist1: " + meterToMiles(y2.distanceTo(y3))); + } + + // test case: x4: course 45°, speed 10kn, x5: course 70°, speed 5kn, time 6h + // taken from marine chart: + // position after 6h: x4: 50°12.1'N, 2°54.4'W, x5: 50°10.1'N, 3°1.5'W, distance: 5nm + x4.setSpeed(knotsToMeterPerSecond(10.0f)); + x4.setBearing(45.0f); + x5.setSpeed(knotsToMeterPerSecond(5.0f)); + x5.setBearing(70.0f); + cpa1.reset(); + getCpa(x4, x5, cpa1); + Log.d("AisTrackerLayer", "# test4: tcpa(x4, x5): " + cpa1.getTcpa()); + Log.d("AisTrackerLayer", "# test4: dist at tcpa: " + cpa1.getCpaDist()); + Log.d("AisTrackerLayer", "# test4: dist0: " + meterToMiles(x4.distanceTo(x5))); + y4 = AisTrackerHelper.getNewPosition(x4, 6.0); + y5 = AisTrackerHelper.getNewPosition(x5, 6.0); + if ((y4 != null) && (y5 != null)) { + Log.d("AisTrackerLayer", "# test4: position 4 after 6 hours: " + + LocationConvert.convertLatitude(y4.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y4.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test4: position 5 after 6 hours: " + + LocationConvert.convertLatitude(y5.getLatitude(), FORMAT_MINUTES, true) + + ", " + LocationConvert.convertLongitude(y5.getLongitude(), FORMAT_MINUTES, true)); + Log.d("AisTrackerLayer", "# test4: dist1: " + meterToMiles(y4.distanceTo(y5))); + } + } + + private void initFakePosition() { + // fake the own position, course and speed to a (fixed) hard coded value + double fakeLat = 50.76077d; + double fakeLon = 7.08747d; + float fakeCOG = 340.0f; + //float fakeCOG = 100.0f; + float fakeSOG = 3.0f; + Location fake = new Location("test", fakeLat, fakeLon); + fake.setBearing(fakeCOG); + fake.setSpeed(knotsToMeterPerSecond(fakeSOG)); + AisObject.fakeOwnPosition(fake); + Log.d("AisTrackerLayer", "initFakePosition: fake: " + fake.toString()); + // in order to visualize this faked (own) position on the map, create an AIS object at this location... + AisObject ais = new AisObject(324578, 18, 20, AisObjectConstants.INVALID_NAV_STATUS, + AisObjectConstants.INVALID_MANEUVER_INDICATOR, + (int)fakeCOG, fakeCOG, fakeSOG, fakeLat, fakeLon, AisObjectConstants.INVALID_ROT); + updateAisObjectList(ais); + ais = new AisObject(324578, 24, 0, "callsign", "fake", 60, 56, + 65, 8, 12, AisObjectConstants.INVALID_DRAUGHT, + "home", AisObjectConstants.INVALID_ETA, AisObjectConstants.INVALID_ETA, + AisObjectConstants.INVALID_ETA_HOUR, AisObjectConstants.INVALID_ETA_MIN); + updateAisObjectList(ais); + //AisObject ais = new AisObject(324578, 1, 20, 0, 1, (int)fakeCOG, + // fakeCOG, fakeSOG, fakeLat, fakeLon, 0.0); + //updateAisObjectList(ais); + //ais = new AisObject(324578, 5, 0, "own-position", "fake", 60 /* passenger */, 56, + // 65, 8, 12, 2, + // "home", 8, 15, 22, 5); + //updateAisObjectList(ais); + } + + private void initTestObject1() { + // passenger ship + AisObject ais = new AisObject(34568, 1, 20, 0, 1, 320, + 320.0, 8.4, 50.738d, 7.099d, 0.0); + updateAisObjectList(ais); + ais = new AisObject(34568, 5, 0, "TEST-CALLSIGN1", "TEST-Ship", 60 /* passenger */, 56, + 65, 8, 12, 2, + "Potsdam", 8, 15, 22, 5); + updateAisObjectList(ais); + } + private void initTestObject2() { + // sailing boat + AisObject ais = new AisObject(454011, 18, 20, AisObjectConstants.INVALID_NAV_STATUS, + AisObjectConstants.INVALID_MANEUVER_INDICATOR, + 125, 125.0, 4.4, 50.737d, 7.098d, AisObjectConstants.INVALID_ROT); + updateAisObjectList(ais); + ais = new AisObject(454011, 24, 0, "TEST-CALLSIGN2", "TEST-Sailor", 36 /* sailing */, 0, + 0, 0, 0, AisObjectConstants.INVALID_DRAUGHT, + "home", AisObjectConstants.INVALID_ETA, AisObjectConstants.INVALID_ETA, + AisObjectConstants.INVALID_ETA_HOUR, AisObjectConstants.INVALID_ETA_MIN); + updateAisObjectList(ais); + } + private void initTestObject3() { + // land station + AisObject ais = new AisObject(878121, 4, 50.736d, 7.100d); + updateAisObjectList(ais); + // AIDS + ais = new AisObject( 521077, 21, 50.735d, 7.101d, 1, + 0, 0, 0, 0); + updateAisObjectList(ais); + } + private void initTestObject4() { + // aircraft + AisObject ais = new AisObject(910323, 9, 15, 65, 180.5, 55.0, 50.734d, 7.102d); + updateAisObjectList(ais); + } + private void initTestObject5() { + // law enforcement + AisObject ais = new AisObject(34569, 1, 20, 5 /* moored */, 1, 15, + 25.0, 8.4, 50.739d, 7.0931d, 0.0); + updateAisObjectList(ais); + ais = new AisObject(34569, 5, 0, "TEST-CALLSIGN3", + "Mecklenburg Vorpommern", 55 /* law enforcement */, 26, + 5, 8, 4, 1, + "Potsdam", 8, 15, 22, 5); + updateAisObjectList(ais); + } + + private void initTimer() { + TimerTask taskCheckAisObjectList; + taskCheckAisObjectList = new TimerTask() { + @Override + public void run() { + Log.d("AisTrackerLayer", "timer task taskCheckAisObjectList running"); + removeLostAisObjects(); + } + }; + this.timer = new Timer(); + timer.schedule(taskCheckAisObjectList, 20000, 30000); + } + private void startNetworkListener() { + int proto = plugin.AIS_NMEA_PROTOCOL.get(); + if (proto == AisTrackerPlugin.AIS_NMEA_PROTOCOL_UDP) { + this.listener = new AisMessageListener(plugin.AIS_NMEA_UDP_PORT.get(), this); + } else if (proto == AisTrackerPlugin.AIS_NMEA_PROTOCOL_TCP) { + this.listener = new AisMessageListener(plugin.AIS_NMEA_IP_ADDRESS.get(), plugin.AIS_NMEA_TCP_PORT.get(), this); + } + } + private void stopNetworkListener() { + if (this.listener != null) { + this.listener.stopListener(); + this.listener = null; + } + } + + /* this method restarts the TCP listeners after a "resume" event (the smartphone resumed + * from sleep or from switched off state): in this case the TCP connection might be broken, + * but the sockets are still (logically) open. + * as additional indication of a broken TCP connection it is checked whether any AIS message + * was received in the last 20 seconds */ + public void checkTcpConnection() { + if (listener != null) { + if (listener.checkTcpSocket()) { + if (((System.currentTimeMillis() - AisObject.getAndUpdateLastMessageReceived()) / 1000) > 20) { + Log.d("AisTrackerLayer", "checkTcpConnection(): restart TCP socket"); + restartNetworkListener(); + } + } + } + } + + public void restartNetworkListener() { + stopNetworkListener(); + startNetworkListener(); + } + public void cleanup() { + if (this.timer != null) { + this.timer.cancel(); + this.timer.purge(); + this.timer = null; + } + if (this.aisObjectList != null) { + this.aisObjectList.clear(); + this.aisObjectList = null; + } + stopNetworkListener(); + } + private void removeLostAisObjects() { + for (Iterator> iterator = aisObjectList.entrySet().iterator(); iterator.hasNext(); ) { + Map.Entry entry = iterator.next(); + if (entry.getValue().checkObjectAge()) { + Log.d("AisTrackerLayer", "remove AIS object with MMSI " + entry.getValue().getMmsi()); + iterator.remove(); + } + } + // aisObjectList.entrySet().removeIf(entry -> entry.getValue().checkObjectAge()); + } + private void removeOldestAisObjectListEntry() { + Log.d("AisTrackerLayer", "removeOldestAisObjectListEntry() called"); + long oldestTimeStamp = System.currentTimeMillis(); + AisObject oldest = null; + for (AisObject ais : aisObjectList.values()) { + long timeStamp = ais.getLastUpdate(); + if (timeStamp <= oldestTimeStamp) { + oldestTimeStamp = timeStamp; + oldest = ais; + } + } + if (oldest != null) { + Log.d("AisTrackerLayer", "remove AIS object with MMSI " + oldest.getMmsi()); + aisObjectList.remove(oldest.getMmsi(), oldest); + } + } + + /* add new AIS object to list, or (if already exist) update its value */ + public void updateAisObjectList(@NonNull AisObject ais) { + int mmsi = ais.getMmsi(); + AisObject obj = aisObjectList.get(mmsi); + if (obj == null) { + Log.d("AisTrackerLayer", "add AIS object with MMSI " + ais.getMmsi()); + aisObjectList.put(mmsi, new AisObject(ais)); + if (aisObjectList.size() >= aisObjectListCounterMax) { + this.removeOldestAisObjectListEntry(); + } + } else { + obj.set(ais); + } + } + + @Nullable + public Bitmap getBitmap(@DrawableRes int drawable) { return getScaledBitmap(drawable); } + + @NonNull + public OsmandApplication getApplication() { + return (OsmandApplication) context.getApplicationContext(); + } + public boolean isLocationVisible(RotatedTileBox tileBox, LatLon coordinates) { + //noinspection SimplifiableIfStatement + if (tileBox == null || coordinates == null) { + return false; + } + return tileBox.containsLatLon(coordinates); + } + + @Override + public void onDraw(Canvas canvas, RotatedTileBox tileBox, DrawSettings settings) { + AisObject.setOwnPosition(getApplication().getLocationProvider().getLastKnownLocation()); + for (AisObject ais : aisObjectList.values()) { + if (isLocationVisible(tileBox, ais.getPosition())) { + ais.draw(this, bitmapPaint, canvas, tileBox); + } + } + } + + @Override + public boolean drawInScreenPixels() { + return true; + } + + @Override + public void collectObjectsFromPoint(PointF point, RotatedTileBox tileBox, List objects, + boolean unknownLocation, boolean excludeUntouchableObjects) { + if (tileBox.getZoom() >= START_ZOOM) { + getAisObjectsFromPoint(point, tileBox, objects); + } + } + public void getAisObjectsFromPoint(PointF point, RotatedTileBox tileBox, List aisList) { + if (aisObjectList.isEmpty()) { + return; + } + + MapRendererView mapRenderer = getMapRenderer(); + float radius = getScaledTouchRadius(getApplication(), tileBox.getDefaultRadiusPoi()) * TOUCH_RADIUS_MULTIPLIER; + List touchPolygon31 = null; + if (mapRenderer != null) { + touchPolygon31 = NativeUtilities.getPolygon31FromPixelAndRadius(mapRenderer, point, radius); + if (touchPolygon31 == null) { + return; + } + } + + for (AisObject ais : aisObjectList.values()) { + LatLon pos = ais.getPosition(); + if (pos != null) { + double lat = pos.getLatitude(); + double lon = pos.getLongitude(); + + boolean add = mapRenderer != null + ? NativeUtilities.isPointInsidePolygon(lat, lon, touchPolygon31) + : tileBox.isLatLonNearPixel(lat, lon, point.x, point.y, radius); + if (add) { + aisList.add(ais); + } + } + } + } + + @Override + public LatLon getObjectLocation(Object o) { + if (o instanceof AisObject) { + LatLon pos = ((AisObject) o).getPosition(); + if (pos != null) { + return new LatLon(pos.getLatitude(), pos.getLongitude()); + } + } + return null; + } + + @Override + public PointDescription getObjectName(Object o) { + if (o instanceof AisObject) { + AisObject ais = ((AisObject) o); + AisObjectConstants.AisObjType objectClass = ais.getObjectClass(); + if (ais.getShipName() != null) { + return new PointDescription("AIS object", ais.getShipName() + + (ais.getSignalLostState() ? " (signal lost)" : "")); + } else if (objectClass == AIS_LANDSTATION) { + return new PointDescription("AIS object", "Land Station with MMSI " + ais.getMmsi()); + } else if (objectClass == AIS_AIRPLANE) { + return new PointDescription("AIS object", "Airplane with MMSI " + + ais.getMmsi() + (ais.getSignalLostState() ? " (signal lost)" : "")); + } else if ((objectClass == AIS_ATON) || (objectClass == AIS_ATON_VIRTUAL)) { + return new PointDescription("AIS object", "Aid to Navigation"); + } else if (objectClass == AIS_SART) { + return new PointDescription("AIS object", "SART (Search and Rescue Transmitter)"); + } + return new PointDescription("AIS object", + "AIS object with MMSI " + ais.getMmsi() + + (ais.getSignalLostState() ? " (signal lost)" : "")); + } + return null; + } +} diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java new file mode 100644 index 00000000000..86574c602fa --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerPlugin.java @@ -0,0 +1,187 @@ +package net.osmand.plus.plugins.aistracker; + +import static net.osmand.plus.settings.fragments.SettingsScreenType.AIS_SETTINGS; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import net.osmand.plus.OsmandApplication; +import net.osmand.plus.R; +import net.osmand.plus.activities.MapActivity; +import net.osmand.plus.plugins.OsmandPlugin; +import net.osmand.plus.render.RendererRegistry; +import net.osmand.plus.settings.backend.ApplicationMode; +import net.osmand.plus.settings.backend.preferences.CommonPreference; +import net.osmand.plus.settings.fragments.SettingsScreenType; +import net.osmand.plus.views.OsmandMapTileView; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/* +* This plugin receives AIS positions and other AIS data via network (NMEA protocol) +* from an AIS receiver/decoder and displays symbols at the map at the vessel position +*/ +public class AisTrackerPlugin extends OsmandPlugin { + + private AisTrackerLayer aisTrackerLayer = null; + + private static final String COMPONENT = "net.osmand.aistrackerPlugin"; + public static final String AISTRACKER_ID = "osmand.aistracker"; + public static final String AIS_NMEA_PROTOCOL_ID = "ais_nmea_protocol"; // see xml/ais_settings.xml + public static final String AIS_NMEA_IP_ADDRESS_ID = "ais_address_nmea_server"; // see xml/ais_settings.xml + public static final String AIS_NMEA_TCP_PORT_ID = "ais_port_nmea_server"; // see xml/ais_settings.xml + public static final String AIS_NMEA_UDP_PORT_ID = "ais_port_nmea_local"; // see xml/ais_settings.xml + public static final String AIS_OBJ_LOST_TIMEOUT_ID = "ais_object_lost_timeout"; // see xml/ais_settings.xml + public static final String AIS_SHIP_LOST_TIMEOUT_ID = "ais_ship_lost_timeout"; // see xml/ais_settings.xml + public static final String AIS_CPA_WARNING_TIME_ID = "ais_cpa_warning_time"; // see xml/ais_settings.xml + public static final String AIS_CPA_WARNING_DISTANCE_ID = "ais_cpa_warning_distance"; // see xml/ais_settings.xml + public final CommonPreference AIS_NMEA_PROTOCOL; + public static final int AIS_NMEA_PROTOCOL_UDP = 0; + public static final int AIS_NMEA_PROTOCOL_TCP = 1; + public final CommonPreference AIS_NMEA_IP_ADDRESS; + private static final String AIS_NMEA_DEFAULT_IP = "192.168.200.16"; + public final CommonPreference AIS_NMEA_TCP_PORT; + private static final Integer AIS_NMEA_DEFAULT_TCP_PORT = 4001; + public final CommonPreference AIS_NMEA_UDP_PORT; + private static final Integer AIS_NMEA_DEFAULT_UDP_PORT = 10110; + public final CommonPreference AIS_OBJ_LOST_TIMEOUT; + public static final Integer AIS_OBJ_LOST_DEFAULT_TIMEOUT = 7; + public final CommonPreference AIS_SHIP_LOST_TIMEOUT; + public static final Integer AIS_SHIP_LOST_DEFAULT_TIMEOUT = 4; + public final CommonPreference AIS_CPA_WARNING_TIME; + public static final Integer AIS_CPA_DEFAULT_WARNING_TIME = 0; + public final CommonPreference AIS_CPA_WARNING_DISTANCE; + public static final Float AIS_CPA_WARNING_DEFAULT_DISTANCE = 1.0f; + + public AisTrackerPlugin(OsmandApplication app) { + super(app); + /* "ais_nmea_protocol" etc. is a reference to the content of xml/ais_settings.xml */ + AIS_NMEA_PROTOCOL = registerIntPreference(AIS_NMEA_PROTOCOL_ID, AIS_NMEA_PROTOCOL_UDP); + AIS_NMEA_IP_ADDRESS = registerStringPreference(AIS_NMEA_IP_ADDRESS_ID, AIS_NMEA_DEFAULT_IP); + AIS_NMEA_TCP_PORT = registerIntPreference(AIS_NMEA_TCP_PORT_ID, AIS_NMEA_DEFAULT_TCP_PORT); + AIS_NMEA_UDP_PORT = registerIntPreference(AIS_NMEA_UDP_PORT_ID, AIS_NMEA_DEFAULT_UDP_PORT); + AIS_OBJ_LOST_TIMEOUT = registerIntPreference(AIS_OBJ_LOST_TIMEOUT_ID, AIS_OBJ_LOST_DEFAULT_TIMEOUT); + AIS_SHIP_LOST_TIMEOUT = registerIntPreference(AIS_SHIP_LOST_TIMEOUT_ID, AIS_SHIP_LOST_DEFAULT_TIMEOUT); + AIS_CPA_WARNING_TIME = registerIntPreference(AIS_CPA_WARNING_TIME_ID, AIS_CPA_DEFAULT_WARNING_TIME); + AIS_CPA_WARNING_DISTANCE = registerFloatPreference(AIS_CPA_WARNING_DISTANCE_ID, AIS_CPA_WARNING_DEFAULT_DISTANCE); + + Log.d("AisTrackerPlugin", "constructor"); + } + + @Override + public boolean isMarketPlugin() { + return true; + } + + @Override + public int getVersion() { + return -1; + } + + @Override + public String getComponentId1() { + return COMPONENT; + } + @Override + public String getComponentId2() { + return "net.osmand.dev"; // for test purposes to enable logcat at adb connected physical device + } + @Override + public CharSequence getDescription(boolean linksEnabled) { + return app.getString(R.string.plugin_aistracker_description).concat("\n\n").concat(app.getString(R.string.plugin_aistracker_disclaimer)); + } + + @Override + public String getName() { + return app.getString(R.string.plugin_aistracker_name); + } + + @Override + //public int getLogoResourceId() { return R.drawable.ic_plugin_nautical_map; } + public int getLogoResourceId() { + return R.drawable.mm_sport_sailing; + } + + @Override + public Drawable getAssetResourceImage() { + return app.getUIUtilities().getIcon(R.drawable.ais_map); + } + + @Override + public List getAddedAppModes() { + //return Collections.singletonList(ApplicationMode.BOAT); + return Arrays.asList(ApplicationMode.BOAT, ApplicationMode.DEFAULT); + } + + @Override + public List getRendererNames() { + return Collections.singletonList(RendererRegistry.NAUTICAL_RENDER); + } + + @Override + public String getId() { + return AISTRACKER_ID; + } + + @Nullable + @Override + public SettingsScreenType getSettingsScreenType() { + return AIS_SETTINGS; + } + + @Override + public String getPrefsDescription() { + return app.getString(R.string.ais_address_settings_description); + } + + @Override + public void mapActivityResume(@NonNull MapActivity activity) { + if (aisTrackerLayer != null) { + aisTrackerLayer.checkTcpConnection(); + } + } + + @Override + public void updateLayers(@NonNull Context context, @Nullable MapActivity mapActivity) { + OsmandMapTileView mapView = app.getOsmandMap().getMapView(); + if (isActive()) { + if (aisTrackerLayer == null) { + Log.d("AisTrackerPlugin", "call registerLayers()"); + registerLayers(context, mapActivity); + } + if (!mapView.getLayers().contains(aisTrackerLayer)) { + mapView.addLayer(aisTrackerLayer, 3.5f); + } + } else { + if (aisTrackerLayer != null) { + mapView.removeLayer(aisTrackerLayer); + aisTrackerLayer.cleanup(); + aisTrackerLayer = null; + mapView.refreshMap(); + } + } + } + + @Override + public void registerLayers(@NonNull Context context, @Nullable MapActivity mapActivity) { + if (aisTrackerLayer == null) { + Log.d("AisTrackerPlugin", "new AisTrackerLayer"); + aisTrackerLayer = new AisTrackerLayer(context, this); + app.getOsmandMap().getMapView().addLayer(aisTrackerLayer, 3.5f); + } else { + Log.d("AisTrackerPlugin", "AisTrackerLayer already exists"); + OsmandMapTileView mapView = app.getOsmandMap().getMapView(); + if (!mapView.getLayers().contains(aisTrackerLayer)) { + mapView.addLayer(aisTrackerLayer, 3.5f); + } + } + } + + public AisTrackerLayer getLayer() { return aisTrackerLayer; } +} diff --git a/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java new file mode 100644 index 00000000000..e29a91d6d78 --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/plugins/aistracker/AisTrackerSettingsFragment.java @@ -0,0 +1,244 @@ +package net.osmand.plus.plugins.aistracker; + +import static net.osmand.plus.plugins.aistracker.AisTrackerPlugin.AIS_NMEA_PROTOCOL_TCP; +import static net.osmand.plus.plugins.aistracker.AisTrackerPlugin.AIS_NMEA_PROTOCOL_UDP; + +import static java.lang.Math.ceil; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Bundle; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.preference.Preference; + +import net.osmand.plus.R; +import net.osmand.plus.plugins.PluginsHelper; +import net.osmand.plus.settings.fragments.BaseSettingsFragment; +import net.osmand.plus.settings.preferences.EditTextPreferenceEx; +import net.osmand.plus.settings.preferences.ListPreferenceEx; +import net.osmand.plus.utils.UiUtilities; + +import java.text.MessageFormat; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class AisTrackerSettingsFragment extends BaseSettingsFragment { + private AisTrackerPlugin plugin; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + plugin = PluginsHelper.getPlugin(AisTrackerPlugin.class); + } + + @Override + protected void setupPreferences() { + int currentProtocol; + boolean cpaWarningEnabled; + currentProtocol = setupProtocol(); + setupIpAddress(currentProtocol); + setupTcpPort(currentProtocol); + setupUdpPort(currentProtocol); + setupObjectLostTimeout(); + setupShipLostTimeout(); + cpaWarningEnabled = setupCpaWarningTime(); + setupCpaWarningDistance(cpaWarningEnabled); + } + + private int setupProtocol() { + Integer[] entryValues = {AIS_NMEA_PROTOCOL_UDP, AIS_NMEA_PROTOCOL_TCP}; + String[] entries = {"UDP", "TCP"}; + + ListPreferenceEx aisNmeaProtocol = findPreference(plugin.AIS_NMEA_PROTOCOL.getId()); + if (aisNmeaProtocol != null) { + aisNmeaProtocol.setEntries(entries); + aisNmeaProtocol.setEntryValues(entryValues); + aisNmeaProtocol.setDescription(R.string.ais_nmea_protocol_description); + return (int)aisNmeaProtocol.getValue(); + } + return 0; + } + private void setupIpAddress(int currentProtocol) { + EditTextPreferenceEx aisNmeaIpAddress = findPreference(plugin.AIS_NMEA_IP_ADDRESS.getId()); + if (aisNmeaIpAddress != null) { + String currentValue = plugin.AIS_NMEA_IP_ADDRESS.get(); + if (currentValue == null) { currentValue = ""; } + aisNmeaIpAddress.setDescription(R.string.ais_address_nmea_server_description); + aisNmeaIpAddress.setSummary(currentValue); + if (currentProtocol == AIS_NMEA_PROTOCOL_UDP) { + aisNmeaIpAddress.setEnabled(false); + } else if (currentProtocol == AIS_NMEA_PROTOCOL_TCP) { + aisNmeaIpAddress.setEnabled(true); + } + } + } + private void setupTcpPort(int currentProtocol) { + EditTextPreferenceEx aisNmeaPort = findPreference(plugin.AIS_NMEA_TCP_PORT.getId()); + if (aisNmeaPort != null) { + int currentValue = plugin.AIS_NMEA_TCP_PORT.get(); + aisNmeaPort.setDescription(R.string.ais_port_nmea_server_description); + aisNmeaPort.setSummary(String.valueOf(currentValue)); + if (currentProtocol == AIS_NMEA_PROTOCOL_UDP) { + aisNmeaPort.setEnabled(false); + } else if (currentProtocol == AIS_NMEA_PROTOCOL_TCP) { + aisNmeaPort.setEnabled(true); + } + } + } + private void setupUdpPort(int currentProtocol) { + EditTextPreferenceEx aisNmeaPort = findPreference(plugin.AIS_NMEA_UDP_PORT.getId()); + if (aisNmeaPort != null) { + int currentValue = plugin.AIS_NMEA_UDP_PORT.get(); + aisNmeaPort.setDescription(R.string.ais_port_nmea_local_description); + aisNmeaPort.setSummary(String.valueOf(currentValue)); + if (currentProtocol == AIS_NMEA_PROTOCOL_UDP) { + aisNmeaPort.setEnabled(true); + } else if (currentProtocol == AIS_NMEA_PROTOCOL_TCP) { + aisNmeaPort.setEnabled(false); + } + } + } + private void setupObjectLostTimeout() { + Integer[] entryValues = {3, 5, 7, 10, 12, 15, 20}; + String[] entries = new String[entryValues.length]; + for (int i = 0; i < entryValues.length; i++) { + entries[i] = entryValues[i] + " " + "minutes"; // TODO: move to resource file + } + ListPreferenceEx objectLostTimeout = findPreference(plugin.AIS_OBJ_LOST_TIMEOUT.getId()); + if (objectLostTimeout != null) { + objectLostTimeout.setEntries(entries); + objectLostTimeout.setEntryValues(entryValues); + objectLostTimeout.setDescription(R.string.ais_object_lost_timeout_description); + } + } + private void setupShipLostTimeout() { + Integer[] entryValues = {2, 3, 4, 5, 7, 10, 15, 100 /* disabled: must be bigger than the biggest value of setupObjectLostTimeout() */}; + String[] entries = new String[entryValues.length]; + for (int i = 0; i < entryValues.length - 1; i++) { + entries[i] = entryValues[i] + " " + "minutes"; // TODO: move to resource file + } + entries[entryValues.length - 1] = "disabled"; // TODO: move to resource file + + ListPreferenceEx objectLostTimeout = findPreference(plugin.AIS_SHIP_LOST_TIMEOUT.getId()); + if (objectLostTimeout != null) { + objectLostTimeout.setEntries(entries); + objectLostTimeout.setEntryValues(entryValues); + objectLostTimeout.setDescription(R.string.ais_ship_lost_timeout_description); + } + } + private boolean setupCpaWarningTime() { + Integer[] entryValues = {0, 1, 5, 10, 20, 30, 60}; + String[] entries = new String[entryValues.length]; + entries[0] = "disabled"; + for (int i = 1; i < entryValues.length; i++) { + entries[i] = entryValues[i] + " "; + entries[i] += entryValues[i].equals(1) ? "minute" : "minutes"; // TODO: move to resource file + } + ListPreferenceEx cpaWarningTime = findPreference(plugin.AIS_CPA_WARNING_TIME.getId()); + if (cpaWarningTime != null) { + cpaWarningTime.setEntries(entries); + cpaWarningTime.setEntryValues(entryValues); + cpaWarningTime.setDescription(R.string.ais_cpa_warning_time_description); + return !cpaWarningTime.getValue().equals(0); + } + return false; + } + @SuppressLint("DefaultLocale") + private void setupCpaWarningDistance(boolean enabled) { + Float[] entryValues = {0.02f, 0.05f, 0.1f, 0.2f, 0.5f, 1.0f, 2.0f}; + String[] entries = new String[entryValues.length]; + for (int i = 0; i < entryValues.length; i++) { + entries[i] = (ceil(entryValues[i]) == entryValues[i]) ? + String.format("%.0f ", entryValues[i]) : + ((entryValues[i] < 0.1f) ? String.format("%.2f ", entryValues[i]) : String.format("%.1f ", entryValues[i])); + entries[i] += entryValues[i].equals(1.0f) ? "nautical mile" : "nautical miles"; // TODO: move to resource file + } + ListPreferenceEx cpaWarningDistance = findPreference(plugin.AIS_CPA_WARNING_DISTANCE.getId()); + if (cpaWarningDistance != null) { + cpaWarningDistance.setEntries(entries); + cpaWarningDistance.setEntryValues(entryValues); + cpaWarningDistance.setDescription(R.string.ais_cpa_warning_distance_description); + cpaWarningDistance.setEnabled(enabled); + } + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + boolean restartNetworkListener = false; + if (preference.getKey().equals(AisTrackerPlugin.AIS_NMEA_IP_ADDRESS_ID)) { + if (!isValidIpV4Address(newValue.toString())) { + showAlertDialog("Only IPv4 address accepted (\"a.b.c.d\", where a,b,c,d in range 0..255)."); + return false; + } + restartNetworkListener = true; + } else if (preference.getKey().equals(AisTrackerPlugin.AIS_NMEA_TCP_PORT_ID) || + preference.getKey().equals(AisTrackerPlugin.AIS_NMEA_UDP_PORT_ID)) { + if (!isValidPortNumber(newValue.toString())) { + showAlertDialog("Only numerical values accepted in range 0..65535."); + return false; + } + restartNetworkListener = true; + } else if (preference.getKey().equals(AisTrackerPlugin.AIS_OBJ_LOST_TIMEOUT_ID)) { + if (newValue instanceof Integer) { + AisObject.setMaxObjectAge((Integer) newValue); + } + } else if (preference.getKey().equals(AisTrackerPlugin.AIS_SHIP_LOST_TIMEOUT_ID)) { + if (newValue instanceof Integer) { + AisObject.setVesselLostTimeout((Integer) newValue); + } + } else if (preference.getKey().equals(AisTrackerPlugin.AIS_CPA_WARNING_TIME_ID)) { + if (newValue instanceof Integer) { + AisObject.setCpaWarningTime((Integer) newValue); + } + } else if (preference.getKey().equals(AisTrackerPlugin.AIS_CPA_WARNING_DISTANCE_ID)) { + if (newValue instanceof Float) { + AisObject.setCpaWarningDistance((Float) newValue); + } + } else if (preference.getKey().equals(AisTrackerPlugin.AIS_NMEA_PROTOCOL_ID)) { + restartNetworkListener = true; + } + + boolean ret = super.onPreferenceChange(preference, newValue); + AisTrackerLayer layer = plugin.getLayer(); + if ((layer != null) && (restartNetworkListener)) { + layer.restartNetworkListener(); + } + return ret; + } + private static boolean isValidIpV4Address(@Nullable String value) { + String pattern0to255 = "(\\d{1,2}|(0|1)\\d{2}|2[0-4]\\d|25[0-5])"; + String patternIpV4 = pattern0to255 + "\\." +pattern0to255 + "\\." + + pattern0to255 + "\\." + pattern0to255; + Pattern p = Pattern.compile(patternIpV4); + if (value == null) { + return false; + } + Matcher m = p.matcher(value); + return m.matches(); + } + private static boolean isValidPortNumber(@Nullable String value) { + int i; + if (value == null) { + return false; + } + try { + i = Integer.parseInt(value); + } catch (NumberFormatException e) { + return false; + } + return (i >= 0) && (i <= 65535); + } + private void showAlertDialog(@NonNull String message) { + Context themedContext = UiUtilities.getThemedContext(getActivity(), isNightMode()); + AlertDialog.Builder wrongFormatDialog = new AlertDialog.Builder(themedContext); + wrongFormatDialog.setTitle(MessageFormat.format(getString(R.string.error_message_pattern), + "Unsupported Data Format")); + wrongFormatDialog.setMessage(message); + wrongFormatDialog.setPositiveButton(R.string.shared_string_ok, (dialog, which) -> dismiss()); + wrongFormatDialog.show(); + } +} diff --git a/OsmAnd/src/net/osmand/plus/plugins/srtm/CollectColorPalletsTask.java b/OsmAnd/src/net/osmand/plus/plugins/srtm/CollectColorPalletsTask.java new file mode 100644 index 00000000000..8a91e24df4e --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/plugins/srtm/CollectColorPalletsTask.java @@ -0,0 +1,67 @@ +package net.osmand.plus.plugins.srtm; + +import android.os.AsyncTask; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import net.osmand.ColorPalette; +import net.osmand.IndexConstants; +import net.osmand.PlatformUtil; +import net.osmand.plus.OsmandApplication; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; + +public class CollectColorPalletsTask extends AsyncTask { + + private final OsmandApplication app; + private final String modeKey; + private final CollectColorPalletListener listener; + + public CollectColorPalletsTask(@NonNull OsmandApplication app, @NonNull String modeKey, @NonNull CollectColorPalletListener listener) { + this.app = app; + this.modeKey = modeKey; + this.listener = listener; + } + + @Override + protected void onPreExecute() { + listener.collectingPalletStarted(); + } + + @Override + protected ColorPalette doInBackground(Void... params) { + if (!TerrainMode.isModeExist(modeKey)) { + PlatformUtil.getLog(CollectColorPalletsTask.class).error("Provided terrain mode doesn't exist"); + return null; + } + TerrainMode mode = TerrainMode.getByKey(modeKey); + File heightmapDir = app.getAppPath(IndexConstants.CLR_PALETTE_DIR); + File mainColorFile = new File(heightmapDir, mode.getMainFile()); + + ColorPalette colorPalette = null; + try { + if (mainColorFile.exists()) { + colorPalette = ColorPalette.parseColorPalette(new FileReader(mainColorFile)); + } + } catch (IOException e) { + PlatformUtil.getLog(CollectColorPalletsTask.class).error("Error reading color file ", e); + } + + return colorPalette; + } + + @Override + protected void onPostExecute(ColorPalette colorPalette) { + listener.collectingPalletFinished(colorPalette); + } + + public interface CollectColorPalletListener { + + void collectingPalletStarted(); + + void collectingPalletFinished(@Nullable ColorPalette colorPalette); + } +} diff --git a/OsmAnd/src/net/osmand/plus/plugins/weather/dialogs/ForecastAdapter.java b/OsmAnd/src/net/osmand/plus/plugins/weather/dialogs/ForecastAdapter.java new file mode 100644 index 00000000000..0ced19c8b23 --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/plugins/weather/dialogs/ForecastAdapter.java @@ -0,0 +1,152 @@ +package net.osmand.plus.plugins.weather.dialogs; + +import static androidx.recyclerview.widget.RecyclerView.NO_POSITION; + +import android.content.Context; +import android.graphics.drawable.GradientDrawable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.RecyclerView; + +import net.osmand.CallbackWithObject; +import net.osmand.plus.R; +import net.osmand.plus.plugins.weather.dialogs.ForecastAdapter.DateViewHolder; +import net.osmand.plus.utils.AndroidUtils; +import net.osmand.plus.utils.ColorUtilities; +import net.osmand.plus.utils.OsmAndFormatter; +import net.osmand.plus.utils.UiUtilities; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +class ForecastAdapter extends RecyclerView.Adapter { + + private final SimpleDateFormat DAY_FORMAT = new SimpleDateFormat("d.M", Locale.getDefault()); + private final SimpleDateFormat DAY_OF_WEEK_FORMAT = new SimpleDateFormat("E", Locale.getDefault()); + + private static final int MAX_FORECAST_DAYS = 6; + + private final Context ctx; + private final LayoutInflater inflater; + + private Calendar currentDate; + private Calendar selectedDate; + private final List dates = new ArrayList<>(); + + private final boolean nightMode; + private final CallbackWithObject callback; + + ForecastAdapter(@NonNull Context ctx, @Nullable CallbackWithObject callback, boolean nightMode) { + this.ctx = ctx; + this.callback = callback; + this.nightMode = nightMode; + inflater = UiUtilities.getInflater(ctx, nightMode); + } + + protected void initDates(@NonNull Calendar currentDate, @NonNull Calendar selectedDate) { + this.currentDate = currentDate; + this.selectedDate = selectedDate; + + Calendar calendar = WeatherForecastFragment.getDefaultCalendar(); + dates.add(currentDate.getTime()); + for (int i = 0; i <= MAX_FORECAST_DAYS; i++) { + calendar.add(Calendar.DAY_OF_MONTH, 1); + dates.add(calendar.getTime()); + } + } + + @NonNull + @Override + public DateViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = inflater.inflate(R.layout.forecast_date_item, parent, false); + AndroidUtils.setBackground(ctx, view, nightMode, R.drawable.ripple_solid_light_6dp, R.drawable.ripple_solid_dark_6dp); + return new DateViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull DateViewHolder holder, int position) { + Date date = dates.get(position); + + boolean today = OsmAndFormatter.isSameDay(date, currentDate.getTime()); + boolean selected = OsmAndFormatter.isSameDay(date, selectedDate.getTime()); + + holder.title.setText(today ? ctx.getString(R.string.today) : DAY_FORMAT.format(date)); + holder.description.setText(DAY_OF_WEEK_FORMAT.format(date)); + + int descriptionColor = ColorUtilities.getSecondaryTextColor(ctx, nightMode); + if (selected) { + int activeColor = AndroidUtils.getColorFromAttr(ctx, R.attr.active_color_basic); + descriptionColor = ColorUtilities.getColorWithAlpha(activeColor, 0.5f); + } + holder.description.setTextColor(descriptionColor); + + holder.itemView.setOnClickListener(view -> { + int pos = holder.getAdapterPosition(); + if (callback != null) { + callback.processResult(dates.get(pos)); + } + notifyDataSetChanged(); + }); + updateBackground(holder, selected); + } + + private void updateBackground(@NonNull DateViewHolder holder, boolean selected) { + GradientDrawable rectContourDrawable = (GradientDrawable) AppCompatResources.getDrawable(ctx, R.drawable.bg_select_group_button_outline_small); + if (rectContourDrawable != null) { + if (selected) { + int activeColor = AndroidUtils.getColorFromAttr(ctx, R.attr.active_color_basic); + int strokeColor = ContextCompat.getColor(ctx, ColorUtilities.getActiveColorId(nightMode)); + + rectContourDrawable.setStroke(AndroidUtils.dpToPx(ctx, 2), strokeColor); + rectContourDrawable.setColor(ColorUtilities.getColorWithAlpha(activeColor, 0.1f)); + } else { + int strokeColor = ContextCompat.getColor(ctx, nightMode ? + R.color.stroked_buttons_and_links_outline_dark : + R.color.stroked_buttons_and_links_outline_light); + rectContourDrawable.setStroke(AndroidUtils.dpToPx(ctx, 1), strokeColor); + rectContourDrawable.setColor(AndroidUtils.getColorFromAttr(ctx, R.attr.ctx_menu_card_btn)); + } + holder.outlineRect.setImageDrawable(rectContourDrawable); + } + } + + private int getItemPosition(@NonNull Date date) { + for (int i = 0; i < dates.size(); i++) { + if (OsmAndFormatter.isSameDay(date, dates.get(i))) { + return i; + } + } + return NO_POSITION; + } + + @Override + public int getItemCount() { + return dates.size(); + } + + protected static class DateViewHolder extends RecyclerView.ViewHolder { + + public final TextView title; + public final TextView description; + public final ImageView outlineRect; + + public DateViewHolder(@NonNull View itemView) { + super(itemView); + title = itemView.findViewById(R.id.title); + description = itemView.findViewById(R.id.description); + outlineRect = itemView.findViewById(R.id.outlineRect); + } + } +} \ No newline at end of file diff --git a/OsmAnd/src/net/osmand/plus/profiles/NavigationIcon.java b/OsmAnd/src/net/osmand/plus/profiles/NavigationIcon.java new file mode 100644 index 00000000000..939460c099e --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/profiles/NavigationIcon.java @@ -0,0 +1,43 @@ +package net.osmand.plus.profiles; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; + +import net.osmand.IndexConstants; +import net.osmand.plus.R; + +public enum NavigationIcon { + + DEFAULT(R.drawable.map_navigation_default), + NAUTICAL(R.drawable.map_navigation_nautical), + CAR(R.drawable.map_navigation_car), + MODEL(R.drawable.map_navigation_default); + + NavigationIcon(@DrawableRes int iconId) { + this.iconId = iconId; + } + + @DrawableRes + private final int iconId; + + @DrawableRes + public int getIconId() { + return iconId; + } + + public static boolean isModel(@NonNull String name) { + return name.startsWith(IndexConstants.MODEL_NAME_PREFIX); + } + + @NonNull + public static NavigationIcon fromName(@NonNull String name) { + if (isModel(name)) { + return MODEL; + } + try { + return valueOf(name); + } catch (IllegalArgumentException e) { + return DEFAULT; + } + } +} \ No newline at end of file diff --git a/OsmAnd/src/net/osmand/plus/settings/fragments/SettingsScreenType.java b/OsmAnd/src/net/osmand/plus/settings/fragments/SettingsScreenType.java index 757ef1bebed..15ec7c498d0 100644 --- a/OsmAnd/src/net/osmand/plus/settings/fragments/SettingsScreenType.java +++ b/OsmAnd/src/net/osmand/plus/settings/fragments/SettingsScreenType.java @@ -3,6 +3,7 @@ import net.osmand.plus.R; import net.osmand.plus.keyevent.fragments.MainExternalInputDevicesFragment; import net.osmand.plus.plugins.accessibility.AccessibilitySettingsFragment; +import net.osmand.plus.plugins.aistracker.AisTrackerSettingsFragment; import net.osmand.plus.plugins.audionotes.MultimediaNotesFragment; import net.osmand.plus.plugins.development.DevelopmentSettingsFragment; import net.osmand.plus.plugins.externalsensors.ExternalSettingsWriteToTrackSettingsFragment; @@ -49,7 +50,8 @@ public enum SettingsScreenType { WEATHER_SETTINGS(WeatherSettingsFragment.class.getName(), true, ApplyQueryType.SNACK_BAR, R.xml.weather_settings, R.layout.profile_preference_toolbar), EXTERNAL_SETTINGS_WRITE_TO_TRACK_SETTINGS(ExternalSettingsWriteToTrackSettingsFragment.class.getName(), true, ApplyQueryType.BOTTOM_SHEET, R.xml.external_sensors_write_to_track_settings, R.layout.profile_preference_toolbar), DANGEROUS_GOODS(DangerousGoodsFragment.class.getName(), true, ApplyQueryType.NONE, R.xml.dangerous_goods_parameters, R.layout.global_preference_toolbar), - EXTERNAL_INPUT_DEVICE(MainExternalInputDevicesFragment.class.getName(), true, ApplyQueryType.SNACK_BAR, R.xml.external_input_device_settings, R.layout.profile_preference_toolbar_with_switch); + EXTERNAL_INPUT_DEVICE(MainExternalInputDevicesFragment.class.getName(), true, ApplyQueryType.SNACK_BAR, R.xml.external_input_device_settings, R.layout.profile_preference_toolbar_with_switch), + AIS_SETTINGS(AisTrackerSettingsFragment.class.getName(), true, ApplyQueryType.SNACK_BAR, R.xml.ais_settings, R.layout.profile_preference_toolbar); public final String fragmentName; public final boolean profileDependent; diff --git a/gradle.properties b/gradle.properties index 6f3bf3d5d5d..4f67eec3b53 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,29 +1,20 @@ -## Project-wide Gradle settings. -# -# For more details on how to configure your build environment visit +## For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -# Default value: -Xmx10248m -XX:MaxPermSize=256m +# Default value: -Xmx1024m -XX:MaxPermSize=256m # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 # # When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# This option should only be used with decoupled projects. For more details, visit +# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects # org.gradle.parallel=true -#Fri Apr 08 18:47:31 EEST 2016 -# android.useDeprecatedNdk=true - -# for enableD8=true min sdk must be > 22 -# UPDATE: temporairly commented since gradle plugin updated to 3.1.3 and claims INSTALL_FAILED_DEXOPT is fixed -# UPDATE 2: D8 causes problems on arm64 devices with Android 6.0 (API 23) -# UPDATE 3: Turn on D8 to recover builds with new gradle 6.5 and pluigin 4.1.1 -#android.enableD8=false +#Sun Aug 11 14:50:52 CEST 2024 +android.defaults.buildfeatures.buildconfig=true android.enableJetifier=true -android.useAndroidX=true android.enableR8.fullMode=false -android.defaults.buildfeatures.buildconfig=true -android.nonTransitiveRClass=false android.nonFinalResIds=false -#org.gradle.configuration-cache=true +android.nonTransitiveRClass=false +android.useAndroidX=true +org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M"