GeoJson builders
This commit is contained in:
parent
0f00abb1b2
commit
b6a3ce0fe7
@ -8,11 +8,7 @@ plugins {
|
||||
|
||||
allprojects {
|
||||
group = "center.sciprog"
|
||||
version = "0.2.1-dev-2"
|
||||
}
|
||||
|
||||
apiValidation{
|
||||
validationDisabled = true
|
||||
version = "0.2.1-dev-3"
|
||||
}
|
||||
|
||||
ksciencePublish{
|
||||
|
@ -23,7 +23,7 @@ internal data class GmcRectangle(
|
||||
}
|
||||
|
||||
public val Rectangle<Gmc>.center: GeodeticMapCoordinates
|
||||
get() = GeodeticMapCoordinates(
|
||||
get() = GeodeticMapCoordinates.normalized(
|
||||
(a.latitude + b.latitude) / 2,
|
||||
(a.longitude + b.longitude) / 2
|
||||
)
|
||||
@ -51,8 +51,8 @@ public val Rectangle<Gmc>.bottom: Angle get() = minOf(a.latitude, b.latitude)
|
||||
public val Rectangle<Gmc>.longitudeDelta: Angle get() = abs(a.longitude - b.longitude)
|
||||
public val Rectangle<Gmc>.latitudeDelta: Angle get() = abs(a.latitude - b.latitude)
|
||||
|
||||
public val Rectangle<Gmc>.topLeft: GeodeticMapCoordinates get() = GeodeticMapCoordinates(top, left)
|
||||
public val Rectangle<Gmc>.bottomRight: GeodeticMapCoordinates get() = GeodeticMapCoordinates(bottom, right)
|
||||
public val Rectangle<Gmc>.topLeft: Gmc get() = Gmc.normalized(top, left)
|
||||
public val Rectangle<Gmc>.bottomRight: Gmc get() = Gmc.normalized(bottom, right)
|
||||
|
||||
//public fun GmcRectangle.enlarge(
|
||||
// top: Distance,
|
||||
|
@ -3,7 +3,6 @@ package center.sciprog.maps.compose
|
||||
import center.sciprog.maps.coordinates.GeodeticMapCoordinates
|
||||
import center.sciprog.maps.coordinates.Gmc
|
||||
import center.sciprog.maps.coordinates.WebMercatorProjection
|
||||
import center.sciprog.maps.coordinates.radians
|
||||
import center.sciprog.maps.features.ViewPoint
|
||||
|
||||
/**
|
||||
@ -16,6 +15,6 @@ internal data class MapViewPoint(
|
||||
val scaleFactor: Float by lazy { WebMercatorProjection.scaleFactor(zoom) }
|
||||
|
||||
public companion object{
|
||||
public val globe: MapViewPoint = MapViewPoint(GeodeticMapCoordinates(0.0.radians, 0.0.radians), 1f)
|
||||
public val globe: MapViewPoint = MapViewPoint(Gmc.ofRadians(0.0, 0.0), 1f)
|
||||
}
|
||||
}
|
@ -66,7 +66,7 @@ public class MapViewScope internal constructor(
|
||||
override fun ViewPoint<Gmc>.moveBy(x: Dp, y: Dp): ViewPoint<Gmc> {
|
||||
val deltaX = x.value / tileScale
|
||||
val deltaY = y.value / tileScale
|
||||
val newCoordinates = GeodeticMapCoordinates(
|
||||
val newCoordinates = Gmc.normalized(
|
||||
(focus.latitude + (deltaY / scaleFactor).radians).coerceIn(
|
||||
-MercatorProjection.MAXIMUM_LATITUDE,
|
||||
MercatorProjection.MAXIMUM_LATITUDE
|
||||
|
@ -29,7 +29,7 @@ public object WebMercatorSpace : CoordinateSpace<Gmc> {
|
||||
override fun ViewPoint(center: Gmc, zoom: Float): ViewPoint<Gmc> = MapViewPoint(center, zoom)
|
||||
|
||||
override fun ViewPoint<Gmc>.moveBy(delta: Gmc): ViewPoint<Gmc> {
|
||||
val newCoordinates = GeodeticMapCoordinates(
|
||||
val newCoordinates = Gmc.normalized(
|
||||
(focus.latitude + delta.latitude).coerceIn(
|
||||
-MercatorProjection.MAXIMUM_LATITUDE,
|
||||
MercatorProjection.MAXIMUM_LATITUDE
|
||||
@ -43,7 +43,7 @@ public object WebMercatorSpace : CoordinateSpace<Gmc> {
|
||||
ViewPoint(focus, (zoom + zoomDelta).coerceIn(2f, 18f))
|
||||
} else {
|
||||
val difScale = (1 - 2f.pow(-zoomDelta))
|
||||
val newCenter = GeodeticMapCoordinates(
|
||||
val newCenter = Gmc.normalized(
|
||||
focus.latitude + (invariant.latitude - focus.latitude) * difScale,
|
||||
focus.longitude + (invariant.longitude - focus.longitude) * difScale
|
||||
)
|
||||
@ -60,7 +60,7 @@ public object WebMercatorSpace : CoordinateSpace<Gmc> {
|
||||
val maxLat = maxOf { it.top }
|
||||
val minLong = minOf { it.left }
|
||||
val maxLong = maxOf { it.right }
|
||||
return GmcRectangle(GeodeticMapCoordinates(minLat, minLong), GeodeticMapCoordinates(maxLat, maxLong))
|
||||
return GmcRectangle(Gmc.normalized(minLat, minLong), Gmc.normalized(maxLat, maxLong))
|
||||
}
|
||||
|
||||
override fun Collection<Gmc>.wrapPoints(): Rectangle<Gmc>? {
|
||||
@ -70,7 +70,7 @@ public object WebMercatorSpace : CoordinateSpace<Gmc> {
|
||||
val maxLat = maxOf { it.latitude }
|
||||
val minLong = minOf { it.longitude }
|
||||
val maxLong = maxOf { it.longitude }
|
||||
return GmcRectangle(GeodeticMapCoordinates(minLat, minLong), GeodeticMapCoordinates(maxLat, maxLong))
|
||||
return GmcRectangle(Gmc.normalized(minLat, minLong), Gmc.normalized(maxLat, maxLong))
|
||||
}
|
||||
|
||||
override fun Gmc.offsetTo(b: Gmc, zoom: Float): DpOffset {
|
||||
@ -122,11 +122,11 @@ public fun CoordinateSpace<Gmc>.Rectangle(
|
||||
height: Angle,
|
||||
width: Angle,
|
||||
): Rectangle<Gmc> {
|
||||
val a = GeodeticMapCoordinates(
|
||||
val a = Gmc.normalized(
|
||||
center.latitude - (height / 2),
|
||||
center.longitude - (width / 2)
|
||||
)
|
||||
val b = GeodeticMapCoordinates(
|
||||
val b = Gmc.normalized(
|
||||
center.latitude + (height / 2),
|
||||
center.longitude + (width / 2)
|
||||
)
|
||||
|
@ -5,13 +5,12 @@ package center.sciprog.maps.coordinates
|
||||
*/
|
||||
public class GeodeticMapCoordinates(
|
||||
public val latitude: Angle,
|
||||
longitude: Angle,
|
||||
public val elevation: Distance = 0.kilometers
|
||||
public val longitude: Angle,
|
||||
public val elevation: Distance = 0.kilometers,
|
||||
) {
|
||||
public val longitude: Angle = longitude.normalized(Angle.zero)
|
||||
|
||||
init {
|
||||
require(latitude in (-Angle.piDiv2)..(Angle.piDiv2)) { "Latitude $latitude is not in (-PI/2)..(PI/2)" }
|
||||
require(longitude in (-Angle.pi..Angle.pi)) { "Longitude $longitude is not in (-PI..PI) range" }
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
@ -38,14 +37,29 @@ public class GeodeticMapCoordinates(
|
||||
|
||||
|
||||
public companion object {
|
||||
public fun ofRadians(latitude: Double, longitude: Double): GeodeticMapCoordinates =
|
||||
GeodeticMapCoordinates(latitude.radians, longitude.radians)
|
||||
public fun normalized(
|
||||
latitude: Angle,
|
||||
longitude: Angle,
|
||||
elevation: Distance = 0.kilometers,
|
||||
): GeodeticMapCoordinates = GeodeticMapCoordinates(
|
||||
latitude, longitude.normalized(Angle.zero), elevation
|
||||
)
|
||||
|
||||
public fun ofDegrees(latitude: Double, longitude: Double): GeodeticMapCoordinates =
|
||||
GeodeticMapCoordinates(latitude.degrees.radians, longitude.degrees.radians)
|
||||
public fun ofRadians(
|
||||
latitude: Double,
|
||||
longitude: Double,
|
||||
elevation: Distance = 0.kilometers,
|
||||
): GeodeticMapCoordinates = normalized(latitude.radians, longitude.radians, elevation)
|
||||
|
||||
public fun ofDegrees(
|
||||
latitude: Double,
|
||||
longitude: Double,
|
||||
elevation: Distance = 0.kilometers,
|
||||
): GeodeticMapCoordinates = normalized(latitude.degrees.radians, longitude.degrees.radians, elevation)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Short name for GeodeticMapCoordinates
|
||||
*/
|
||||
|
@ -64,8 +64,8 @@ public fun GeoEllipsoid.meridianCurve(
|
||||
}
|
||||
|
||||
return GmcCurve(
|
||||
forward = GmcPose(Gmc(fromLatitude, longitude), if (up) zero else pi),
|
||||
backward = GmcPose(Gmc(toLatitude, longitude), if (up) pi else zero),
|
||||
forward = GmcPose(Gmc.normalized(fromLatitude, longitude), if (up) zero else pi),
|
||||
backward = GmcPose(Gmc.normalized(toLatitude, longitude), if (up) pi else zero),
|
||||
distance = s
|
||||
)
|
||||
}
|
||||
@ -77,8 +77,8 @@ public fun GeoEllipsoid.parallelCurve(latitude: Angle, fromLongitude: Angle, toL
|
||||
require(latitude in (-piDiv2)..(piDiv2)) { "Latitude must be in (-90, 90) degrees range" }
|
||||
val right = toLongitude > fromLongitude
|
||||
return GmcCurve(
|
||||
forward = GmcPose(Gmc(latitude, fromLongitude), if (right) piDiv2.radians else -piDiv2.radians),
|
||||
backward = GmcPose(Gmc(latitude, toLongitude), if (right) -piDiv2.radians else piDiv2.radians),
|
||||
forward = GmcPose(Gmc.normalized(latitude, fromLongitude), if (right) piDiv2.radians else -piDiv2.radians),
|
||||
backward = GmcPose(Gmc.normalized(latitude, toLongitude), if (right) -piDiv2.radians else piDiv2.radians),
|
||||
distance = reducedRadius(latitude) * abs((fromLongitude - toLongitude).radians.value)
|
||||
)
|
||||
}
|
||||
@ -193,7 +193,7 @@ public fun GeoEllipsoid.curveInDirection(
|
||||
val L = lambda - (1 - C) * f * sinAlpha *
|
||||
(sigma.value + C * sinSigma * (cosSigmaM2 + C * cosSigma * (-1 + 2 * cos2SigmaM2)))
|
||||
|
||||
val endPoint = Gmc(phi2, start.longitude + L.radians)
|
||||
val endPoint = Gmc.normalized(phi2, start.longitude + L.radians)
|
||||
|
||||
// eq. 12
|
||||
|
||||
|
@ -13,4 +13,10 @@ kotlin {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kscience{
|
||||
useSerialization {
|
||||
json()
|
||||
}
|
||||
}
|
@ -3,12 +3,14 @@ package center.sciprog.maps.geojson
|
||||
import center.sciprog.maps.geojson.GeoJson.Companion.PROPERTIES_KEY
|
||||
import center.sciprog.maps.geojson.GeoJson.Companion.TYPE_KEY
|
||||
import center.sciprog.maps.geojson.GeoJsonFeatureCollection.Companion.FEATURES_KEY
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.*
|
||||
import kotlin.jvm.JvmInline
|
||||
|
||||
/**
|
||||
* A utility class to work with GeoJson (https://geojson.org/)
|
||||
*/
|
||||
@Serializable(GeoJsonSerializer::class)
|
||||
public sealed interface GeoJson {
|
||||
public val json: JsonObject
|
||||
public val type: String get() = json[TYPE_KEY]?.jsonPrimitive?.content ?: error("Not a GeoJson")
|
||||
@ -34,6 +36,23 @@ public value class GeoJsonFeature(override val json: JsonObject) : GeoJson {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder function for [GeoJsonFeature]
|
||||
*/
|
||||
public fun GeoJsonFeature(
|
||||
geometry: GeoJsonGeometry?,
|
||||
properties: JsonObject? = null,
|
||||
builder: JsonObjectBuilder.() -> Unit = {},
|
||||
): GeoJsonFeature = GeoJsonFeature(
|
||||
buildJsonObject {
|
||||
put(TYPE_KEY, "Feature")
|
||||
geometry?.json?.let { put(GeoJsonFeature.GEOMETRY_KEY, it) }
|
||||
properties?.let { put(PROPERTIES_KEY, it) }
|
||||
|
||||
builder()
|
||||
}
|
||||
)
|
||||
|
||||
public fun GeoJsonFeature.getProperty(key: String): JsonElement? = json[key] ?: properties?.get(key)
|
||||
|
||||
@JvmInline
|
||||
@ -60,20 +79,33 @@ public value class GeoJsonFeatureCollection(override val json: JsonObject) : Geo
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for [GeoJsonFeatureCollection]
|
||||
*/
|
||||
public fun GeoJsonFeatureCollection(
|
||||
features: List<GeoJsonFeature>,
|
||||
properties: JsonObject? = null,
|
||||
builder: JsonObjectBuilder.() -> Unit = {},
|
||||
): GeoJsonFeatureCollection = GeoJsonFeatureCollection(
|
||||
buildJsonObject {
|
||||
put(TYPE_KEY, "FeatureCollection")
|
||||
putJsonArray(FEATURES_KEY) {
|
||||
features.forEach {
|
||||
add(it.json)
|
||||
}
|
||||
}
|
||||
properties?.let { put(PROPERTIES_KEY, it) }
|
||||
builder()
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
/**
|
||||
* Generic Json to GeoJson converter
|
||||
*/
|
||||
public fun GeoJson(json: JsonObject): GeoJson =
|
||||
when (json[TYPE_KEY]?.jsonPrimitive?.contentOrNull ?: error("Not a GeoJson")) {
|
||||
"Feature" -> GeoJsonFeature(json)
|
||||
"FeatureCollection" -> GeoJsonFeatureCollection(json)
|
||||
else -> GeoJsonGeometry(json)
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine a collection of features to a new [GeoJsonFeatureCollection]
|
||||
*/
|
||||
public fun GeoJsonFeatureCollection(features: Collection<GeoJsonFeature>): GeoJsonFeatureCollection =
|
||||
GeoJsonFeatureCollection(
|
||||
buildJsonObject {
|
||||
put(TYPE_KEY, "FeatureCollection")
|
||||
put(FEATURES_KEY, JsonArray(features.map { it.json }))
|
||||
}
|
||||
)
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
package center.sciprog.maps.geojson
|
||||
|
||||
import center.sciprog.maps.coordinates.Gmc
|
||||
import center.sciprog.maps.coordinates.kilometers
|
||||
import center.sciprog.maps.coordinates.meters
|
||||
import center.sciprog.maps.geojson.GeoJsonGeometry.Companion.COORDINATES_KEY
|
||||
import kotlinx.serialization.json.*
|
||||
import kotlin.jvm.JvmInline
|
||||
@ -25,7 +27,31 @@ public fun GeoJsonGeometry(json: JsonObject): GeoJsonGeometry {
|
||||
}
|
||||
|
||||
internal fun JsonElement.toGmc() = jsonArray.run {
|
||||
Gmc.ofDegrees(get(1).jsonPrimitive.double, get(0).jsonPrimitive.double)
|
||||
Gmc.ofDegrees(
|
||||
get(1).jsonPrimitive.double,
|
||||
get(0).jsonPrimitive.double,
|
||||
get(2).jsonPrimitive.doubleOrNull?.meters ?: 0.kilometers
|
||||
)
|
||||
}
|
||||
|
||||
internal fun Gmc.toJsonArray(): JsonArray = buildJsonArray {
|
||||
add(longitude.degrees.value)
|
||||
add(latitude.degrees.value)
|
||||
if (elevation.kilometers != 0.0) {
|
||||
add(elevation.meters)
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<Gmc>.listToJsonArray(): JsonArray = buildJsonArray {
|
||||
forEach {
|
||||
add(it.toJsonArray())
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<List<Gmc>>.listOfListsToJsonArray(): JsonArray = buildJsonArray {
|
||||
forEach {
|
||||
add(it.listToJsonArray())
|
||||
}
|
||||
}
|
||||
|
||||
@JvmInline
|
||||
@ -39,6 +65,13 @@ public value class GeoJsonPoint(override val json: JsonObject) : GeoJsonGeometry
|
||||
?: error("Coordinates are not provided")
|
||||
}
|
||||
|
||||
public fun GeoJsonPoint(coordinates: Gmc): GeoJsonPoint = GeoJsonPoint(
|
||||
buildJsonObject {
|
||||
put(GeoJson.TYPE_KEY, "Point")
|
||||
put(COORDINATES_KEY, coordinates.toJsonArray())
|
||||
}
|
||||
)
|
||||
|
||||
@JvmInline
|
||||
public value class GeoJsonMultiPoint(override val json: JsonObject) : GeoJsonGeometry {
|
||||
init {
|
||||
@ -51,6 +84,13 @@ public value class GeoJsonMultiPoint(override val json: JsonObject) : GeoJsonGeo
|
||||
?: error("Coordinates are not provided")
|
||||
}
|
||||
|
||||
public fun GeoJsonMultiPoint(coordinates: List<Gmc>): GeoJsonMultiPoint = GeoJsonMultiPoint(
|
||||
buildJsonObject {
|
||||
put(GeoJson.TYPE_KEY, "MultiPoint")
|
||||
put(COORDINATES_KEY, coordinates.listToJsonArray())
|
||||
}
|
||||
)
|
||||
|
||||
@JvmInline
|
||||
public value class GeoJsonLineString(override val json: JsonObject) : GeoJsonGeometry {
|
||||
init {
|
||||
@ -63,6 +103,13 @@ public value class GeoJsonLineString(override val json: JsonObject) : GeoJsonGeo
|
||||
?: error("Coordinates are not provided")
|
||||
}
|
||||
|
||||
public fun GeoJsonLineString(coordinates: List<Gmc>): GeoJsonLineString = GeoJsonLineString(
|
||||
buildJsonObject {
|
||||
put(GeoJson.TYPE_KEY, "LineString")
|
||||
put(COORDINATES_KEY, coordinates.listToJsonArray())
|
||||
}
|
||||
)
|
||||
|
||||
@JvmInline
|
||||
public value class GeoJsonMultiLineString(override val json: JsonObject) : GeoJsonGeometry {
|
||||
init {
|
||||
@ -77,6 +124,13 @@ public value class GeoJsonMultiLineString(override val json: JsonObject) : GeoJs
|
||||
} ?: error("Coordinates are not provided")
|
||||
}
|
||||
|
||||
public fun GeoJsonMultiLineString(coordinates: List<List<Gmc>>): GeoJsonMultiLineString = GeoJsonMultiLineString(
|
||||
buildJsonObject {
|
||||
put(GeoJson.TYPE_KEY, "MultiLineString")
|
||||
put(COORDINATES_KEY, coordinates.listOfListsToJsonArray())
|
||||
}
|
||||
)
|
||||
|
||||
@JvmInline
|
||||
public value class GeoJsonPolygon(override val json: JsonObject) : GeoJsonGeometry {
|
||||
init {
|
||||
@ -91,6 +145,13 @@ public value class GeoJsonPolygon(override val json: JsonObject) : GeoJsonGeomet
|
||||
} ?: error("Coordinates are not provided")
|
||||
}
|
||||
|
||||
public fun GeoJsonPolygon(coordinates: List<List<Gmc>>): GeoJsonPolygon = GeoJsonPolygon(
|
||||
buildJsonObject {
|
||||
put(GeoJson.TYPE_KEY, "Polygon")
|
||||
put(COORDINATES_KEY, coordinates.listOfListsToJsonArray())
|
||||
}
|
||||
)
|
||||
|
||||
@JvmInline
|
||||
public value class GeoJsonMultiPolygon(override val json: JsonObject) : GeoJsonGeometry {
|
||||
init {
|
||||
@ -107,11 +168,31 @@ public value class GeoJsonMultiPolygon(override val json: JsonObject) : GeoJsonG
|
||||
} ?: error("Coordinates are not provided")
|
||||
}
|
||||
|
||||
public fun GeoJsonMultiPolygon(coordinates: List<List<List<Gmc>>>): GeoJsonMultiPolygon = GeoJsonMultiPolygon(
|
||||
buildJsonObject {
|
||||
put(GeoJson.TYPE_KEY, "MultiPolygon")
|
||||
put(COORDINATES_KEY, buildJsonArray { coordinates.forEach { add(it.listOfListsToJsonArray()) } })
|
||||
}
|
||||
)
|
||||
|
||||
@JvmInline
|
||||
public value class GeoJsonGeometryCollection(override val json: JsonObject) : GeoJsonGeometry {
|
||||
init {
|
||||
require(type == "GeometryCollection") { "Not a GeoJson GeometryCollection geometry" }
|
||||
}
|
||||
|
||||
public val geometries: List<GeoJsonGeometry> get() = json.jsonArray.map { GeoJsonGeometry(it.jsonObject) }
|
||||
}
|
||||
public val geometries: List<GeoJsonGeometry>
|
||||
get() = json["geometries"]?.jsonArray?.map { GeoJsonGeometry(it.jsonObject) } ?: emptyList()
|
||||
}
|
||||
|
||||
public fun GeoJsonGeometryCollection(geometries: List<GeoJsonGeometry>): GeoJsonGeometryCollection =
|
||||
GeoJsonGeometryCollection(
|
||||
buildJsonObject {
|
||||
put(GeoJson.TYPE_KEY, "GeometryCollection")
|
||||
put("geometries", buildJsonArray {
|
||||
geometries.forEach {
|
||||
add(it.json)
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
@ -1,6 +0,0 @@
|
||||
package center.sciprog.maps.geojson
|
||||
|
||||
import center.sciprog.attributes.Attribute
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
|
||||
public object GeoJsonPropertiesAttribute : Attribute<JsonObject>
|
@ -0,0 +1,7 @@
|
||||
package center.sciprog.maps.geojson
|
||||
|
||||
import center.sciprog.attributes.SerializableAttribute
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.serializer
|
||||
|
||||
public object GeoJsonPropertiesAttribute : SerializableAttribute<JsonObject>("properties", serializer())
|
@ -0,0 +1,21 @@
|
||||
package center.sciprog.maps.geojson
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
|
||||
|
||||
public object GeoJsonSerializer : KSerializer<GeoJson> {
|
||||
|
||||
private val serializer = JsonObject.serializer()
|
||||
override val descriptor: SerialDescriptor
|
||||
get() = serializer.descriptor
|
||||
|
||||
override fun deserialize(decoder: Decoder): GeoJson = GeoJson(serializer.deserialize(decoder))
|
||||
|
||||
override fun serialize(encoder: Encoder, value: GeoJson) {
|
||||
serializer.serialize(encoder, value.json)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user