checkpoint
This commit is contained in:
parent
8cbfa729f0
commit
aca4cf20c0
7 changed files with 52 additions and 57 deletions
|
@ -6,9 +6,8 @@ import android.util.Log
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class Database(trackbook: Trackbook)
|
class Database(val trackbook: Trackbook)
|
||||||
{
|
{
|
||||||
val trackbook = trackbook
|
|
||||||
var ready: Boolean = false
|
var ready: Boolean = false
|
||||||
lateinit var file: File
|
lateinit var file: File
|
||||||
lateinit var connection: SQLiteDatabase
|
lateinit var connection: SQLiteDatabase
|
||||||
|
@ -96,8 +95,12 @@ class Database(trackbook: Trackbook)
|
||||||
fun update_homepoint(id: Long, name: String, radius: Double)
|
fun update_homepoint(id: Long, name: String, radius: Double)
|
||||||
{
|
{
|
||||||
Log.i("VOUSSOIR", "Database.update_homepoint")
|
Log.i("VOUSSOIR", "Database.update_homepoint")
|
||||||
|
val values = ContentValues().apply {
|
||||||
|
put("name", name)
|
||||||
|
put("radius", radius)
|
||||||
|
}
|
||||||
begin_transaction()
|
begin_transaction()
|
||||||
connection.rawQuery("UPDATE homepoints SET name = ?, radius = ? WHERE id = ?", arrayOf(name, radius.toString(), id.toString()))
|
connection.update("homepoints", values, "id = ?", arrayOf(id.toString()))
|
||||||
commit()
|
commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,6 +110,7 @@ class Database(trackbook: Trackbook)
|
||||||
this.connection.execSQL("CREATE TABLE IF NOT EXISTS meta(name TEXT PRIMARY KEY, value TEXT)")
|
this.connection.execSQL("CREATE TABLE IF NOT EXISTS meta(name TEXT PRIMARY KEY, value TEXT)")
|
||||||
this.connection.execSQL("CREATE TABLE IF NOT EXISTS trkpt(lat REAL NOT NULL, lon REAL NOT NULL, time INTEGER NOT NULL, accuracy REAL, device_id INTEGER NOT NULL, ele INTEGER, sat INTEGER, PRIMARY KEY(lat, lon, time, device_id))")
|
this.connection.execSQL("CREATE TABLE IF NOT EXISTS trkpt(lat REAL NOT NULL, lon REAL NOT NULL, time INTEGER NOT NULL, accuracy REAL, device_id INTEGER NOT NULL, ele INTEGER, sat INTEGER, PRIMARY KEY(lat, lon, time, device_id))")
|
||||||
this.connection.execSQL("CREATE TABLE IF NOT EXISTS homepoints(id INTEGER PRIMARY KEY, lat REAL NOT NULL, lon REAL NOT NULL, radius REAL NOT NULL, name TEXT)")
|
this.connection.execSQL("CREATE TABLE IF NOT EXISTS homepoints(id INTEGER PRIMARY KEY, lat REAL NOT NULL, lon REAL NOT NULL, radius REAL NOT NULL, name TEXT)")
|
||||||
|
this.connection.execSQL("PRAGMA user_version = ${Keys.CURRENT_TRACKLIST_FORMAT_VERSION}")
|
||||||
this.connection.setTransactionSuccessful()
|
this.connection.setTransactionSuccessful()
|
||||||
this.connection.endTransaction()
|
this.connection.endTransaction()
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,18 +77,9 @@ object Keys {
|
||||||
const val DIALOG_EMPTY_PAYLOAD_STRING: String = ""
|
const val DIALOG_EMPTY_PAYLOAD_STRING: String = ""
|
||||||
const val DIALOG_EMPTY_PAYLOAD_INT: Int = -1
|
const val DIALOG_EMPTY_PAYLOAD_INT: Int = -1
|
||||||
|
|
||||||
// folder names
|
|
||||||
const val FOLDER_TEMP: String = "temp"
|
|
||||||
const val FOLDER_TRACKS: String = "tracks"
|
|
||||||
const val FOLDER_GPX: String = "gpx"
|
|
||||||
|
|
||||||
// file names and extensions
|
// file names and extensions
|
||||||
const val MIME_TYPE_GPX: String = "application/gpx+xml"
|
const val MIME_TYPE_GPX: String = "application/gpx+xml"
|
||||||
const val GPX_FILE_EXTENSION: String = ".gpx"
|
const val GPX_FILE_EXTENSION: String = ".gpx"
|
||||||
const val TRACKBOOK_LEGACY_FILE_EXTENSION: String = ".trackbook"
|
|
||||||
const val TRACKBOOK_FILE_EXTENSION: String = ".json"
|
|
||||||
const val TEMP_FILE: String = "temp.json"
|
|
||||||
const val TRACKLIST_FILE: String = "tracklist.json"
|
|
||||||
|
|
||||||
// view types
|
// view types
|
||||||
const val VIEW_TYPE_STATISTICS: Int = 1
|
const val VIEW_TYPE_STATISTICS: Int = 1
|
||||||
|
@ -96,13 +87,11 @@ object Keys {
|
||||||
|
|
||||||
// default values
|
// default values
|
||||||
val DEFAULT_DATE: Date = Date(0L)
|
val DEFAULT_DATE: Date = Date(0L)
|
||||||
const val DEFAULT_RFC2822_DATE: String = "Thu, 01 Jan 1970 01:00:00 +0100" // --> Date(0)
|
|
||||||
const val ONE_SECOND_IN_MILLISECONDS: Long = 1000
|
const val ONE_SECOND_IN_MILLISECONDS: Long = 1000
|
||||||
const val ONE_MINUTE_IN_MILLISECONDS: Long = 60 * ONE_SECOND_IN_MILLISECONDS
|
const val ONE_MINUTE_IN_MILLISECONDS: Long = 60 * ONE_SECOND_IN_MILLISECONDS
|
||||||
const val ONE_HOUR_IN_MILLISECONDS: Long = 60 * ONE_MINUTE_IN_MILLISECONDS
|
const val ONE_HOUR_IN_MILLISECONDS: Long = 60 * ONE_MINUTE_IN_MILLISECONDS
|
||||||
const val EMPTY_STRING_RESOURCE: Int = 0
|
const val EMPTY_STRING_RESOURCE: Int = 0
|
||||||
const val REQUEST_CURRENT_LOCATION_INTERVAL: Long = 1 * ONE_SECOND_IN_MILLISECONDS
|
const val REQUEST_CURRENT_LOCATION_INTERVAL: Long = 1 * ONE_SECOND_IN_MILLISECONDS
|
||||||
const val TRACKING_INTERVAL: Long = 1 * ONE_SECOND_IN_MILLISECONDS
|
|
||||||
const val SAVE_TEMP_TRACK_INTERVAL: Long = 30 * ONE_SECOND_IN_MILLISECONDS
|
const val SAVE_TEMP_TRACK_INTERVAL: Long = 30 * ONE_SECOND_IN_MILLISECONDS
|
||||||
const val SIGNIFICANT_TIME_DIFFERENCE: Long = 1 * ONE_MINUTE_IN_MILLISECONDS
|
const val SIGNIFICANT_TIME_DIFFERENCE: Long = 1 * ONE_MINUTE_IN_MILLISECONDS
|
||||||
const val STOP_OVER_THRESHOLD: Long = 5 * ONE_MINUTE_IN_MILLISECONDS
|
const val STOP_OVER_THRESHOLD: Long = 5 * ONE_MINUTE_IN_MILLISECONDS
|
||||||
|
@ -113,7 +102,6 @@ object Keys {
|
||||||
const val DEFAULT_ALTITUDE: Double = 0.0
|
const val DEFAULT_ALTITUDE: Double = 0.0
|
||||||
const val DEFAULT_TIME: Long = 0L
|
const val DEFAULT_TIME: Long = 0L
|
||||||
const val COMMIT_INTERVAL: Int = 30
|
const val COMMIT_INTERVAL: Int = 30
|
||||||
const val DEFAULT_ALTITUDE_SMOOTHING_VALUE: Int = 13
|
|
||||||
const val DEFAULT_THRESHOLD_LOCATION_ACCURACY: Int = 30 // 30 meters
|
const val DEFAULT_THRESHOLD_LOCATION_ACCURACY: Int = 30 // 30 meters
|
||||||
const val DEFAULT_THRESHOLD_LOCATION_AGE: Long = 5_000_000_000L // 5s in nanoseconds
|
const val DEFAULT_THRESHOLD_LOCATION_AGE: Long = 5_000_000_000L // 5s in nanoseconds
|
||||||
const val DEFAULT_THRESHOLD_DISTANCE: Float = 15f // 15 meters
|
const val DEFAULT_THRESHOLD_DISTANCE: Float = 15f // 15 meters
|
||||||
|
|
|
@ -138,6 +138,7 @@ class MapFragment : Fragment()
|
||||||
// basic map setup
|
// basic map setup
|
||||||
controller = mapView.controller
|
controller = mapView.controller
|
||||||
mapView.isTilesScaledToDpi = true
|
mapView.isTilesScaledToDpi = true
|
||||||
|
mapView.isVerticalMapRepetitionEnabled = false
|
||||||
mapView.setTileSource(TileSourceFactory.MAPNIK)
|
mapView.setTileSource(TileSourceFactory.MAPNIK)
|
||||||
mapView.setMultiTouchControls(true)
|
mapView.setMultiTouchControls(true)
|
||||||
mapView.zoomController.setVisibility(org.osmdroid.views.CustomZoomButtonsController.Visibility.NEVER)
|
mapView.zoomController.setVisibility(org.osmdroid.views.CustomZoomButtonsController.Visibility.NEVER)
|
||||||
|
@ -538,7 +539,8 @@ class MapFragment : Fragment()
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
save_button.setOnClickListener {
|
save_button.setOnClickListener {
|
||||||
trackbook.database.update_homepoint(homepoint.id, name=name_input.text.toString(), radius=radius_input.text.toString().toDouble())
|
val radius = radius_input.text.toString().toDoubleOrNull() ?: 25.0
|
||||||
|
trackbook.database.update_homepoint(homepoint.id, name=name_input.text.toString(), radius=radius)
|
||||||
trackbook.load_homepoints()
|
trackbook.load_homepoints()
|
||||||
create_homepoint_overlays(requireContext(), mapView, trackbook.homepoints)
|
create_homepoint_overlays(requireContext(), mapView, trackbook.homepoints)
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
|
@ -568,8 +570,8 @@ class MapFragment : Fragment()
|
||||||
mapView.overlays.remove(currentTrackSpecialMarkerOverlay)
|
mapView.overlays.remove(currentTrackSpecialMarkerOverlay)
|
||||||
}
|
}
|
||||||
if (trkpts.isNotEmpty()) {
|
if (trkpts.isNotEmpty()) {
|
||||||
createTrackOverlay(requireContext(), mapView, trkpts, trackingState)
|
currentTrackOverlay = createTrackOverlay(requireContext(), mapView, trkpts, trackingState)
|
||||||
createSpecialMakersTrackOverlay(requireContext(), mapView, trkpts, trackingState)
|
currentTrackSpecialMarkerOverlay = createSpecialMakersTrackOverlay(requireContext(), mapView, trkpts, trackingState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -85,7 +85,7 @@ data class Track (
|
||||||
var previous: Trkpt? = null
|
var previous: Trkpt? = null
|
||||||
for (trkpt in trkpt_generator())
|
for (trkpt in trkpt_generator())
|
||||||
{
|
{
|
||||||
if (previous != null && (trkpt.time - previous.time) > (5 * Keys.ONE_MINUTE_IN_MILLISECONDS))
|
if (previous != null && (trkpt.time - previous.time) > (Keys.STOP_OVER_THRESHOLD))
|
||||||
{
|
{
|
||||||
write("\t\t</trkseg>")
|
write("\t\t</trkseg>")
|
||||||
write("\t\t<trkseg>")
|
write("\t\t<trkseg>")
|
||||||
|
|
|
@ -57,7 +57,7 @@ class TrackerService: Service(), SensorEventListener
|
||||||
var recording_started: Date = GregorianCalendar.getInstance().time
|
var recording_started: Date = GregorianCalendar.getInstance().time
|
||||||
var commitInterval: Int = Keys.COMMIT_INTERVAL
|
var commitInterval: Int = Keys.COMMIT_INTERVAL
|
||||||
var currentBestLocation: Location = getDefaultLocation()
|
var currentBestLocation: Location = getDefaultLocation()
|
||||||
var lastCommit: Date = Keys.DEFAULT_DATE
|
var lastCommit: Long = 0
|
||||||
var location_min_time_ms: Long = 0
|
var location_min_time_ms: Long = 0
|
||||||
private val RECENT_TRKPT_COUNT = 7200
|
private val RECENT_TRKPT_COUNT = 7200
|
||||||
var stepCountOffset: Float = 0f
|
var stepCountOffset: Float = 0f
|
||||||
|
@ -150,9 +150,36 @@ class TrackerService: Service(), SensorEventListener
|
||||||
return object : LocationListener {
|
return object : LocationListener {
|
||||||
override fun onLocationChanged(location: Location)
|
override fun onLocationChanged(location: Location)
|
||||||
{
|
{
|
||||||
if (isBetterLocation(location, currentBestLocation)) {
|
if (! isBetterLocation(location, currentBestLocation))
|
||||||
currentBestLocation = location
|
{
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
currentBestLocation = location
|
||||||
|
if (trackingState != Keys.STATE_TRACKING_ACTIVE)
|
||||||
|
{
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Log.i("VOUSSOIR", "Processing point ${location.latitude}, ${location.longitude} ${location.time}.")
|
||||||
|
if (should_keep_point((location)))
|
||||||
|
{
|
||||||
|
val now: Long = location.time
|
||||||
|
// val now: Date = GregorianCalendar.getInstance().time
|
||||||
|
val trkpt = Trkpt(location=location)
|
||||||
|
trackbook.database.insert_trkpt(device_id, trkpt)
|
||||||
|
recent_trkpts.add(trkpt)
|
||||||
|
|
||||||
|
while (recent_trkpts.size > RECENT_TRKPT_COUNT)
|
||||||
|
{
|
||||||
|
recent_trkpts.removeFirst()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (now - lastCommit > Keys.SAVE_TEMP_TRACK_INTERVAL)
|
||||||
|
{
|
||||||
|
trackbook.database.commit()
|
||||||
|
lastCommit = now
|
||||||
|
}
|
||||||
|
}
|
||||||
|
displayNotification()
|
||||||
}
|
}
|
||||||
override fun onProviderEnabled(provider: String)
|
override fun onProviderEnabled(provider: String)
|
||||||
{
|
{
|
||||||
|
@ -353,7 +380,6 @@ class TrackerService: Service(), SensorEventListener
|
||||||
}
|
}
|
||||||
PreferencesHelper.saveTrackingState(trackingState)
|
PreferencesHelper.saveTrackingState(trackingState)
|
||||||
startStepCounter()
|
startStepCounter()
|
||||||
handler.postDelayed(periodicTrackUpdate, 0)
|
|
||||||
startForeground(Keys.TRACKER_SERVICE_NOTIFICATION_ID, displayNotification())
|
startForeground(Keys.TRACKER_SERVICE_NOTIFICATION_ID, displayNotification())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -365,7 +391,6 @@ class TrackerService: Service(), SensorEventListener
|
||||||
PreferencesHelper.saveTrackingState(trackingState)
|
PreferencesHelper.saveTrackingState(trackingState)
|
||||||
|
|
||||||
sensorManager.unregisterListener(this)
|
sensorManager.unregisterListener(this)
|
||||||
handler.removeCallbacks(periodicTrackUpdate)
|
|
||||||
|
|
||||||
displayNotification()
|
displayNotification()
|
||||||
stopForeground(STOP_FOREGROUND_DETACH)
|
stopForeground(STOP_FOREGROUND_DETACH)
|
||||||
|
@ -452,31 +477,4 @@ class TrackerService: Service(), SensorEventListener
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private val periodicTrackUpdate: Runnable = object : Runnable
|
|
||||||
{
|
|
||||||
override fun run() {
|
|
||||||
val now: Date = GregorianCalendar.getInstance().time
|
|
||||||
val trkpt = Trkpt(location=currentBestLocation)
|
|
||||||
Log.i("VOUSSOIR", "Processing point ${currentBestLocation.latitude}, ${currentBestLocation.longitude} ${now.time}.")
|
|
||||||
if (should_keep_point((currentBestLocation)))
|
|
||||||
{
|
|
||||||
trackbook.database.insert_trkpt(device_id, trkpt)
|
|
||||||
recent_trkpts.add(trkpt)
|
|
||||||
|
|
||||||
while (recent_trkpts.size > RECENT_TRKPT_COUNT)
|
|
||||||
{
|
|
||||||
recent_trkpts.removeFirst()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (now.time - lastCommit.time > Keys.SAVE_TEMP_TRACK_INTERVAL)
|
|
||||||
{
|
|
||||||
trackbook.database.commit()
|
|
||||||
lastCommit = now
|
|
||||||
}
|
|
||||||
}
|
|
||||||
displayNotification()
|
|
||||||
handler.postDelayed(this, Keys.TRACKING_INTERVAL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
/* Creates icon overlay for track */
|
/* Creates icon overlay for track */
|
||||||
fun createTrackOverlay(context: Context, map_view: MapView, trkpts: Collection<Trkpt>, trackingState: Int)
|
fun createTrackOverlay(context: Context, map_view: MapView, trkpts: Collection<Trkpt>, trackingState: Int): SimpleFastPointOverlay
|
||||||
{
|
{
|
||||||
val color = if (trackingState == Keys.STATE_TRACKING_ACTIVE) context.getColor(R.color.default_red) else context.getColor(R.color.default_blue)
|
val color = if (trackingState == Keys.STATE_TRACKING_ACTIVE) context.getColor(R.color.default_red) else context.getColor(R.color.default_blue)
|
||||||
val points: MutableList<IGeoPoint> = mutableListOf()
|
val points: MutableList<IGeoPoint> = mutableListOf()
|
||||||
|
@ -64,10 +64,11 @@ fun createTrackOverlay(context: Context, map_view: MapView, trkpts: Collection<T
|
||||||
.setCellSize(12) // Sets the grid cell size used for indexing, in pixels. Larger cells result in faster rendering speed, but worse fidelity. Default is 10 pixels, for large datasets (>10k points), use 15.
|
.setCellSize(12) // Sets the grid cell size used for indexing, in pixels. Larger cells result in faster rendering speed, but worse fidelity. Default is 10 pixels, for large datasets (>10k points), use 15.
|
||||||
val overlay = SimpleFastPointOverlay(pointTheme, overlayOptions)
|
val overlay = SimpleFastPointOverlay(pointTheme, overlayOptions)
|
||||||
map_view.overlays.add(overlay)
|
map_view.overlays.add(overlay)
|
||||||
|
return overlay
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Creates overlay containing start, stop, stopover and starred markers for track */
|
/* Creates overlay containing start, stop, stopover and starred markers for track */
|
||||||
fun createSpecialMakersTrackOverlay(context: Context, map_view: MapView, trkpts: Collection<Trkpt>, trackingState: Int, displayStartEndMarker: Boolean = false)
|
fun createSpecialMakersTrackOverlay(context: Context, map_view: MapView, trkpts: Collection<Trkpt>, trackingState: Int, displayStartEndMarker: Boolean = false): ItemizedIconOverlay<OverlayItem>
|
||||||
{
|
{
|
||||||
val overlayItems: ArrayList<OverlayItem> = ArrayList<OverlayItem>()
|
val overlayItems: ArrayList<OverlayItem> = ArrayList<OverlayItem>()
|
||||||
val trackingActive: Boolean = trackingState == Keys.STATE_TRACKING_ACTIVE
|
val trackingActive: Boolean = trackingState == Keys.STATE_TRACKING_ACTIVE
|
||||||
|
@ -110,7 +111,9 @@ fun createSpecialMakersTrackOverlay(context: Context, map_view: MapView, trkpts:
|
||||||
overlayItems.add(overlayItem)
|
overlayItems.add(overlayItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
map_view.overlays.add(createOverlay(context, overlayItems))
|
val overlay: ItemizedIconOverlay<OverlayItem> = createOverlay(context, overlayItems)
|
||||||
|
map_view.overlays.add(overlay)
|
||||||
|
return overlay
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createOverlayItem(context: Context, latitude: Double, longitude: Double, accuracy: Float, provider: String, time: Long): OverlayItem
|
fun createOverlayItem(context: Context, latitude: Double, longitude: Double, accuracy: Float, provider: String, time: Long): OverlayItem
|
||||||
|
|
|
@ -86,7 +86,7 @@
|
||||||
<string name="pref_altitude_smoothing_value_title" translatable="false">Altitude Smoothing</string>
|
<string name="pref_altitude_smoothing_value_title" translatable="false">Altitude Smoothing</string>
|
||||||
<string name="pref_auto_export_interval_summary">Automatically export GPX file after this many hours.</string>
|
<string name="pref_auto_export_interval_summary">Automatically export GPX file after this many hours.</string>
|
||||||
<string name="pref_device_id_summary">A unique ID to distinguish tracks recorded across multiple devices:</string>
|
<string name="pref_device_id_summary">A unique ID to distinguish tracks recorded across multiple devices:</string>
|
||||||
<string name="pref_database_folder_summary">Directory to contain your database file. You could use Syncthing to sync with your PC!</string>
|
<string name="pref_database_folder_summary">Directory to contain your database file. You could use Syncthing to sync with your PC.</string>
|
||||||
<string name="pref_auto_export_interval_title">Auto Export Interval</string>
|
<string name="pref_auto_export_interval_title">Auto Export Interval</string>
|
||||||
<string name="pref_device_id">Device ID</string>
|
<string name="pref_device_id">Device ID</string>
|
||||||
<string name="pref_database_folder">Database directory</string>
|
<string name="pref_database_folder">Database directory</string>
|
||||||
|
@ -96,8 +96,8 @@
|
||||||
<string name="pref_general_title">General</string>
|
<string name="pref_general_title">General</string>
|
||||||
<string name="pref_maintenance_title">Maintenance</string>
|
<string name="pref_maintenance_title">Maintenance</string>
|
||||||
<string name="pref_gps_only_title">Restrict to GPS</string>
|
<string name="pref_gps_only_title">Restrict to GPS</string>
|
||||||
<string name="pref_gps_only_summary_gps_and_network">Currently using GPS and Network for localization.</string>
|
<string name="pref_gps_only_summary_gps_and_network">Currently using GPS and Network for location.</string>
|
||||||
<string name="pref_gps_only_summary_gps_only">Currently using only GPS for localization.</string>
|
<string name="pref_gps_only_summary_gps_only">Currently using only GPS for location.</string>
|
||||||
<string name="pref_imperial_measurement_units_summary_metric">Currently using metric units (Kilometer, Meter).</string>
|
<string name="pref_imperial_measurement_units_summary_metric">Currently using metric units (Kilometer, Meter).</string>
|
||||||
<string name="pref_imperial_measurement_units_summary_imperial">Currently using imperial units (Miles, Feet).</string>
|
<string name="pref_imperial_measurement_units_summary_imperial">Currently using imperial units (Miles, Feet).</string>
|
||||||
<string name="pref_imperial_measurement_units_title">Use Imperial Measurements</string>
|
<string name="pref_imperial_measurement_units_title">Use Imperial Measurements</string>
|
||||||
|
|
Loading…
Reference in a new issue