Constructing a Map in the Mercator Projection for Android
Tags: webdev, android, tutorial, programming
Published: 2021-01-13 11:08:57

###Have you ever thought of creating a map on Android?

In this article, I will tell you how I created my Mercator projection map from scratch. I’ll describe the basic functionality and methods I used to change camera positions. Finally, I’ll talk about the algorithms I used for multiple touches and, of course, share a link to my repository in Github.

For the record: this is not a magic pill that will solve all your problems but a tutorial, which may be useful for developers to understand the way maps work on Android.

The map is developed from the Mercator projection.
* The Mercator projection is one of the basic map projections used to represent the surface of the Earth or another celestial body on a plane. The projection was developed by the 16th-century geographer Gerardus Mercator and is still used by cartographers, navigators, and geographers.


[![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/bi2fsroprekte47iodh8.png)](https://maddevs.io/blog/reduce-gps-data-error-on-android-with-kalman-filter-and-accelerometer?utm_source=devto&utm_medium=mad-map)



***

The map functionality is quite simple: there are **basic functionality** (e.g., displaying and converting geodata) and **secondary functionality** (e.g., changing camera positions and adding markers). All the features add up to a complete map.

***

The **basic functionality** is divided into two parts: converting the geodata (latitude and longitude) into x/y coordinates and displaying the data on the screen.

##Basic functionality

All objects on the map are geodata that we convert to x/y coordinates and then display on the screen.
Mercator projection geodatabase conversion code:

    private const val A = 6378137
    private const val B = 6356752.3142
    private var ZOOM = 10000
    fun converterLatitudeToX(latitude: Double, zoom: Int = 
    ZOOM): Double {
    val rLat = latitude * PI / 180
    val f = (A - B) / A
    val e = sqrt(2 * f - f.pow(2))
    return (A * Math.log(tan(PI / 4 + rLat / 2) * ((1 - e * sin(rLat)) / (1 + e * sin(rLat))).pow(e / 2))) / zoom
    }
    fun converterLongitudeToY(longitude: Double, zoom: Int = ZOOM): Double {
    val rLong= longitude * PI / 180
    return (A * rLong) / zoom
    }
    fun converterGeoData(latitude: Double, longitude: Double, zoom: Int = ZOOM): Point {
    return Point(converterLatitudeToX(latitude, zoom), converterLongitudeToY(longitude, zoom))
    }

To display, we need to create a custom **View** class, in which we have to describe the logic of displaying objects on the screen in the **onDraw** method with the help of Canvas.
In the custom **View** class, write the following code:

    private lateinit var presenter: MapPresenter
    private lateinit var bitmap: Bitmap
    private var drawCanvas: Canvas? = null
    constructor(context: Context) : super(context, null) {
    initMap(context)
    }
    constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
    initMap(context)
    }
    fun initMap(context: Context) {
    presenter = MapPresenter(context)
    post {
        initMapCanvas()
    }
    }
    private fun initMapCanvas() {
    bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
    drawCanvas = Canvas(bitmap)
    invalidate()
    }
    @SuppressLint("DrawAllocation")
    override fun onDraw(canvas: Canvas) {
        if (drawCanvas != null) {
        canvas.save()
        drawCanvas!!.drawColor(backgroundColor)
        presenter.getShapes()?.forEach {
            val shape = Path()
            for ((index, shapeItem) in it.shapeList.withIndex()) {
                if (index == 0) {
                    shape.moveTo(shapeItem)
                } else {
                    shape.lineTo(shapeItem)
                }
            }
            drawCanvas!!.save()
            paintShape.color = Color.parseColor(it.color)
            drawCanvas!!.drawPath(shape, paintShape)
            drawCanvas!!.restore()
        }
    }
    }

We use **Path** to display shapes on the screen. The **Path** class allows you to create straight lines, curves, patterns, and other lines.

    object PathUtil {
    fun Path.moveTo(point: Point) {
        this.moveTo(point.x.toFloat(), point.y.toFloat())
        }
    fun Path.lineTo(point: Point) {
        this.lineTo(point.x.toFloat(), point.y.toFloat())
    }
    }

In the .xml file **Activity**, write:

    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
        <com.maddevs.madmap.map.view.MapView <- our custom View
        android:id="@+id/map"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
    </androidx.constraintlayout.widget.ConstraintLayout>

The **MapPresenter** class stores GeoPoint data. The **GeoPoint** class itself converts latitude and longitude into x/y points during initialization to display in the custom **View**.

    open class GeoPoint(var latitude: Double, var longitude: Double) : Point(converterGeoData(latitude, longitude)) {
    private val angleRotate = 270.0
    private val zoom = 10000
    private var angleRotateXY = 0.0
    private var dx: Float = 0f
    private var dy: Float = 0f
    init {
        rotate(angleRotate)
    }
    open fun updateCoordinateZoom(zoomLevel: Int) {
        updateData(converterGeoData(latitude, longitude, zoomLevel))
        rotate(angleRotate)
    }
    protected fun updateCoordinate(latitude: Double, longitude: Double, changeZoom: Boolean) {
        updateData(converterGeoData(latitude, longitude, zoom))
        rotate(angleRotate + angleRotateXY)
        if (changeZoom) {
            x += dx
            y += dy
        }
    }
    }

The GeoPoint class inherits the x/y parameters from the **Point** class.


    open class Point {
    var x: Double
    var y: Double
    constructor(x: Double, y: Double) {
        this.x = x
        this.y = y
    }
    constructor(point: Point) {
        this.x = point.x
        this.y = point.y
    }
    constructor() {
        this.x = Double.NaN
        this.y = Double.NaN
    }
    fun updateData(point: Point) {
        this.x = point.x
        this.y = point.y
    }
    fun updateData(x: Double, y: Double) {
        this.x = x
        this.y = y
    }
    fun rotate(angle: Double) {
        val rad: Double = angle * PI / 180
        val rx = x
        val ry = y
        x = rx * cos(rad) - ry * sin(rad)
        y = ry * cos(rad) + rx * sin(rad)
    }
    }

It has the **rotate** method and a formula for rotating a point in two-dimensional space.
We store the data in a [JSON file](https://github.com/Eldar-Akkozov/MadMap/blob/master/app/src/main/assets/shapes.json). And we parse it in the **getShapes** method.

    override fun getShapes(): List<ShapeObject>? {
    val shapes = JSONArray(getAssetsFileString("shapes.json"))
    val result = ArrayList<ShapeObject>()
    for (item in shapes.iterator()) {
        val shapes = ArrayList<ShapeItemObject>()
        val color = item.getString("color")
        val type = item.getString("type")
        for (shape in item.getJSONArray("shape").iterator()) {
            shapes.add(
                ShapeItemObject(
                    shape.getDouble("latitude"),
                    shape.getDouble("longitude")
                )
            )
        }
        result.add(ShapeObject(shapes, type, color))
    }
    return result
    }

In the **ShapeItemObject** class, parameters are inherited from **GeoPoint**, where they are already located.

    class ShapeItemObject(latitude: Double, longitude: Double) : GeoPoint(latitude, longitude)

And in **ShapeObject**, I added the type and color of the shape to the sheet from **ShapeItemObject**.

    class ShapeObject(val shapeList: List<ShapeItemObject>, type: String, val color: String)

And… after all the added logic, we run the application and get the result.

![Mad Map — Island of Madagascar and a Part of Africa.](https://dev-to-uploads.s3.amazonaws.com/i/khiiacylxqz5o7a7c1w6.jpeg)

We now have the shapes displayed on the screen. Initially, we have only the island of Madagascar and a part of Africa displayed.
Now we need to add **secondary** functionality to move the camera around the map.

***

##Secondary functionality

Secondary functionality includes moving the camera by data and by touching the screen with your finger.
The first thing we need to implement is the movement of the camera by the data. For this, the project already has conversion tools and classes for working with geopoints.
In the custom class, we write the **onChangeCameraPosition** method, and latitude and longitude will be written in the parameters.

    fun onChangeCameraPosition(latitude: Double, longitude: Double) {
    post {
        presenter.changeCameraPosition(latitude, longitude)
        invalidate()
    }
    }

In the presenter, we write the **changeCameraPosition** method.

    class MapPresenter(context: Context, repository: MapRepository = MapRepository(context)) {
    private val shapesRendering: List<ShapeObject>? = repository.getShapes()
    private val bordersRendering: List<BorderObject>? = repository.getBorders()
    private val bordersLineRendering: List<BorderLineObject>? = repository.getBordersLine()
    private val shapesStringRendering: List<StringObject>? = repository.getShapesString()
    private lateinit var cameraPosition: CameraPosition
    override fun getShapes(): List<ShapeObject>? {
        return shapesRendering
    }
    override fun getShapesString(): List<StringObject>? {
        return shapesStringRendering
    }
    override fun getBorders(): List<BorderObject>? {
        return bordersRendering
    }
    override fun getBordersLine(): List<BorderLineObject>? {
        return bordersLineRendering
    }
    override fun initCamera(width: Int, height: Int) {
        this.heightY = height / 2
        this.widthC = width / 2
        cameraPosition = CameraPosition(width, height)
        moveCoordiante(
            ((cameraPosition.x * -1) + cameraPosition.centerX).toFloat(),
            ((cameraPosition.y * -1) + cameraPosition.centerY).toFloat()
        )
    }
    override fun changeCameraPosition(latitude: Double, longitude: Double) {
        cameraPosition.updatePosition(latitude, longitude)
        moveCoordiante(
            ((cameraPosition.x * -1) + cameraPosition.centerX).toFloat(),
            ((cameraPosition.y * -1) + cameraPosition.centerY).toFloat()
        )
    }
    fun estimationData(estimation: Estimation<GeoPoint>) {
        estimation.counting(cameraPosition)
        shapesRendering?.forEach {
            it.shapeList.forEach { item ->
                estimation.counting(item)
            }
        }
    }
        fun moveCoordiante(dx: Float, dy: Float) {
        estimationData(object : Estimation<GeoPoint>{
            override fun counting(item: GeoPoint) {
                item.move(dx, dy)
            }
        })
    }
    }

We need the **Estimation** interface to enumerate all the points on the screen.

    interface Estimation<T> {
    fun counting(item: T)
    }

Now, we need to write the **CameraPosition** class in **GeoPoint**; in its constructor, we write the default zero coordinates.

    class CameraPosition(width: Int, height: Int) : GeoPoint(0.0, 0.0) {
    var centerX: Int = width / 2
    var centerY: Int = height / 2
    fun updatePosition(latitude: Double, longitude: Double) {
        updateCoordinate(latitude, longitude, true)
    }
    }

And then in **GeoPoint** we write the **move** method.

    open class GeoPoint(var latitude: Double, var longitude: Double) : Point(converterGeoData(latitude, longitude)) {
    private val angleRotate = 270.0
    private val zoom = 10000
    private var angleRotateXY = 0.0
    private var dx: Float = 0f
    private var dy: Float = 0f
    init {
        rotate(angleRotate)
    }
    fun move(dx: Float, dy: Float) {
        x += dx
        y += dy
    }
        open fun updateCoordinateZoom(zoomLevel: Int) {
        updateData(converterGeoData(latitude, longitude, zoomLevel))
        rotate(angleRotate)
    }
        protected fun updateCoordinate(latitude: Double, longitude: Double, changeZoom: Boolean) {
        updateData(converterGeoData(latitude, longitude, zoom))
        rotate(angleRotate + angleRotateXY)
        if (changeZoom) {
            x += dx
            y += dy
        }
    }
    }

![Mad Map.](https://dev-to-uploads.s3.amazonaws.com/i/t2jamcagqkmxrn11p0bg.jpeg)

Now the entire camera moves at the specified coordinates.

**How it works:** During the initialization of the camera default data is assigned as zero (latitude 0.0 longitude 0.0); after initialization and data conversion, all points on the screen move to the camera point multiplied by -1, and after moving all the points move to the width and height of the custom View divided by 2 (in the middle of the screen).

To check, in **Activity** we have two buttons that will move the camera according to the entered coordinates:

    class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        one.setOnClickListener {
            map.onChangeCameraPosition(18.902389796969448, 14.877051673829557)
        }
        two.setOnClickListener {
            map.onChangeCameraPosition(-20.182886472696126, 46.45624604076147)
        }
    }
    }

![Result moveCoordiante.](https://dev-to-uploads.s3.amazonaws.com/i/a5a81vlfagwq97gatgjf.gif)

***

Now, we need to implement the movement of the map by having the screen touched with your finger. To do this, we need to implement the **onTouchEvent** method in the custom View.

    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent): Boolean {
    val x = event.x
    val y = event.y
    when (event.action) {
        MotionEvent.ACTION_DOWN -> {
            presenter.touchStart(x, y)
           }
        MotionEvent.ACTION_MOVE -> {
            presenter.touchMove(x, y)
        }
        }
    
    invalidate()
    return true
    }

And implement two methods, **touchStart** and **touchMove**, in the presenter. They will contain the code for finding the difference between the touches.

    private var mX = 0f
    private var mY = 0f
    fun touchStart(x: Float, y: Float) {
    mX = x
    mY = y
    }
    fun touchMove(x: Float, y: Float) {
    val dx = x - mX
    val dy = y - mY
    if (Point(x.toDouble(), y.toDouble()).distanceTo(Point(mX.toDouble(), mY.toDouble())) < 300) {
        moveCoordiante(dx, dy)
    }
    mX = x
    mY = y
    }

Also, in the **Point** class, you must add a method for finding the distance between points **distanceTo**.

    fun distanceTo(point: Point) : Double {
    return sqrt((x - point.x).pow(2) + (y - point.y).pow(2))
    }

![Result onTouchEvent.](https://dev-to-uploads.s3.amazonaws.com/i/0trkvap4hql37agby8p9.gif)

*** 

We already have geodata converted into coordinates, movement by touch, and movement by geodata. For full functionality, we need to add a **change of map scale**.

I added a class for scale types.

    class CameraZoom : Point() {
    enum class Type { PLUS, MINUS }
    }

To do this, you need to write a method for changing the size of shapes in **GeoPoint**.

    open fun changeZoom(addedZoom: Double, type: CameraZoom.Type) {
    when (type) {
        CameraZoom.Type.PLUS -> {
            x /= addedZoom
            y /= addedZoom
        }
        CameraZoom.Type.MINUS -> {
            x *= addedZoom
            y *= addedZoom
        }
    }
    }

Also, add in the presenter the enumeration and call the **changeZoom** method.

    fun changeCameraPosition(zoom: Double, type: CameraZoom.Type) {
    zoomCoordiante(zoom, type)
    }
    private fun zoomCoordiante(zoom: Double, type: CameraZoom.Type) {
    moveCoordiante((widthC * -1).toFloat(), (heightY * -1).toFloat())
    estimationData(object : Estimation<GeoPoint> {
        override fun counting(item: GeoPoint) {
            item.changeZoom(zoom, type)
        }
    })
    moveCoordiante((widthC).toFloat(), (heightY).toFloat())
    }

In the custom **View**, let’s add the **onChangeCameraPosition** method.

    fun onChangeCameraPosition(zoom: Double, type: CameraZoom.Type) {
    post {
        presenter.changeCameraPosition(zoom, type)
        invalidate()
    }
    }

Everything is ready. Now all that’s left is to test. Let’s add 2 buttons and call methods **onChangeCameraPosition** in Activity.

    plus.setOnClickListener {
    map.onChangeCameraPosition(2.0, CameraZoom.Type.PLUS)
    }
    minus.setOnClickListener {
    map.onChangeCameraPosition(2.0, CameraZoom.Type.MINUS)
    }

![Result of zoomCoordiante.](https://dev-to-uploads.s3.amazonaws.com/i/gwq326rqaboq8cmrmohm.gif)

**Result of the work:** the code on the map scale change. Everything works by dividing and shifting the camera to the center of the map. From the code in the presenter, you can see that the **moveCoordinate** (move the map) method goes to the center first, multiplied by -1. This moves the center to the upper left corner and zooms the map, and then returns everything to the original center by moving everything to the middle.

*** 

The last thing left to implement is map rotation. For this, we need to add a class to make the map work and display after rotation, just like in the other implementations.

For this, we already have a method for rotation in the **Point** class.

    fun rotate(angle: Double) {
    val rad: Double = angle * PI / 180
    val rx = x
    val ry = y
    x = rx * cos(rad) - ry * sin(rad)
    y = ry * cos(rad) + rx * sin(rad)
    }

It is also necessary to implement the **CameraRotate** class for working with angles and saving the center of the camera position. In it, you need to write the logic of saving the current angle.

    class CameraRotate(width: Int, height: Int) : Point((width / 2).toDouble(), (height / 2).toDouble()) {
    var regulatoryСenterX: Int = width / 2
    var regulatoryСenterY: Int = height / 2
    private var angleRotateXY = 0.0
        fun changeRotate(rotate: Double) {
        rotate(rotate - angleRotateXY)
        angleRotateXY = rotate
        }
    fun move(dx: Float, dy: Float) {
        x += dx
        y += dy
    }
    }

Now we need a method to call **rotate** for all points on the screen in the presenter.

    private fun rotateCoordinate(rotate: Double) {
    cameraRotate.changeRotate(rotate)
    estimationData(object : Estimation<GeoPoint>{
        override fun counting(item: GeoPoint) {
            item.changeRotate(rotate)
        }
    })
    }

Let’s also add the main method for calling and returning the map to its original state (to the center).

    override fun changeCameraPosition(rotate: Double) {
    rotateCoordinate(rotate)
    moveCoordiante(
        ((cameraRotate.x * -1) + 
    cameraRotate.regulatoryСenterX).toFloat(),
        ((cameraRotate.y * -1) + 
    cameraRotate.regulatoryСenterY).toFloat(),
        true
    )
    }

To make the camera work, you need to extend the **moveCoordiante** method and put a mode in it to call the **cameraRotate** method move.

    private fun moveCoordiante(dx: Float, dy: Float, rotateMode: Boolean = false) {
    if (rotateMode) {
        cameraRotate.move(dx, dy)
        }
        estimationData(object : Estimation<GeoPoint>{
        override fun counting(item: GeoPoint) {
            item.move(dx, dy)
        }
    })
    }

And call the **changeCameraPosition** method in the **View**.

    override fun onChangeCameraPosition(rotate: Double) {
    post {
        presenter.changeCameraPosition(rotate)
        invalidate()
    }
    }

For testing, we need to add two buttons to change the rotation in the **Activity**.

    var rotateAngel = 0.0
    rotate_one.setOnClickListener {
    rotateAngel += 5.0
    map.onChangeCameraPosition(rotateAngel)
    }
    rotate_two.setOnClickListener {
    rotateAngel -= 5.0
    map.onChangeCameraPosition(rotateAngel)
    }


![Result of rotateCoordinate.](https://dev-to-uploads.s3.amazonaws.com/i/wfq5i45j9o0w6gzsxnyw.gif)

All rotation functionality works using the **rotate** method, only when the **rotate** method is called, rotation occurs at all points except **CameraRotate**, where data from the camera class is used to return to the center of the map.

*** 

The last things we’ll add to the map are motion, zooming, and rotating the map by multiple screen taps. To do this, we need to add a class to the project for handling **touchManager**, which will use all the methods we developed above.

But first, we need to slightly modify the rotation functionality (add saving the previous state and the map position relative to this state). Add the **rotateNotSaveCoordinate** method to the presenter.

    private fun rotateNotSaveCoordinate(rotate: Double) {
    cameraRotate.rotate(rotate)
    estimationData(object : Estimation<GeoPoint>{
        override fun counting(item: GeoPoint) {
            item.rotate(rotate)
        }
    })
    }

Also add a new method and a point on which to rotate the map.

    override fun changeCameraPosition(rotate: Double, regulatoryPoint: Point) {
    rotateNotSaveCoordinate(rotate)
    moveCoordiante(
        ((cameraRotate.x * -1) + regulatoryPoint.x).toFloat(),
        ((cameraRotate.y * -1) + regulatoryPoint.y).toFloat(),
        true
    )
    }

In the class constructor, let’s specify the interface for calling methods in the presenter.

        class TouchManager(var presenter: MapContract.Presenter, width: Int, height: Int) {
    private val centerPoint = Point((width / 2).toDouble(), (height / 2).toDouble())
    private var checkBearing = 0.0
    private var checkTepm = 0
    private var checkStartBearing = 0.0
    private var checkEndBearing = 0.0
    private var startBearing = 0.0
    private var distancePoint = 0.0
    fun touch(event: MotionEvent) {
        if (event.pointerCount > 1) {
            val firstPoint = Point(event.getX(0).toDouble(), event.getY(0).toDouble())
            val secondPoint = Point(event.getX(1).toDouble(), event.getY(1).toDouble())
            val middlePoint = firstPoint.middleTo(secondPoint)
            val distance = centerPoint.distanceTo(middlePoint)
            when (event.action) {
                MotionEvent.ACTION_MOVE -> {
                    if (distancePoint == 0.0) {
                        distancePoint = firstPoint.distanceTo(secondPoint)
                    }
                    presenter.touchDoubleMove(middlePoint.x.toFloat(), middlePoint.y.toFloat())
                    val checkDistance = distancePoint - firstPoint.distanceTo(secondPoint)
                    if (checkDistance > 10) {
                        distancePoint = 0.0
                        presenter.changeCameraPosition(1.02, CameraZoom.Type.PLUS)
                        presenter.changeCameraPosition(centerPoint.pointTo(centerPoint.bearingTo(middlePoint), (distance / 100) * 5))
                    } else if (checkDistance < -10) {
                        distancePoint = 0.0
                        presenter.changeCameraPosition(1.02, CameraZoom.Type.MINUS)
                        presenter.changeCameraPosition(centerPoint.pointTo(middlePoint.bearingTo(centerPoint), (distance / 100) * 5))
                    }
                    if (checkStartBearing != 0.0) {
                        checkBearing = middlePoint.bearingTo(firstPoint) - checkStartBearing
                        if (checkEndBearing == 0.0) {
                            checkEndBearing = checkBearing
                        }
                        if (abs(checkBearing) > abs(checkEndBearing)) {
                            checkEndBearing = checkBearing
                            checkTepm++
                        }
                        if (checkTepm > 6) {
                            if (startBearing != 0.0) {
                                presenter.changeCameraPosition((middlePoint.bearingTo(firstPoint) - startBearing), middlePoint)
                            }
                            startBearing = middlePoint.bearingTo(firstPoint)
                        }
                    }
                    checkStartBearing = middlePoint.bearingTo(firstPoint)
                }
                MotionEvent.ACTION_POINTER_UP -> {
                    distancePoint = 0.0
                    startBearing = 0.0
                    checkBearing = 0.0
                    checkEndBearing = 0.0
                    checkStartBearing = 0.0
                    checkTepm = 0
                    presenter.touchDoubleEnd()
                }
            }
        } else {
            val x = event.x
            val y = event.y
            when (event.action) {
                MotionEvent.ACTION_DOWN -> {
                    presenter.touchStart(x, y)
                }
                MotionEvent.ACTION_MOVE -> {
                    presenter.touchMove(x, y)
                }
            }
        }
    }
    }

**TouchManager** works based on the number of touches: one will make only **moveCoordinate** call work, and for multiple touches, there are parameters in the class based on which the functionality will work.

To use it, you need to write it in the **View**.

    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent): Boolean {
        touchManager.touch(event)
        invalidate()
        return true
    }

![Result of TouchManager.](https://dev-to-uploads.s3.amazonaws.com/i/y3qn0arpimq5pssr2x8k.gif)

*** 

At this point, we have added the basic toolkit for the map. The next article will deal with the logic of displaying cities (buildings, roads, and other objects) and dynamically adding markers to the map.

Map code: [https://github.com/maddevsio](https://github.com/maddevsio)

[![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/jk954k7eqlhh2gsk7hqn.jpeg)](https://maddevs.io/services/?utm_source=blog&utm_medium=mad-map)

Previously published at [maddevs.io.](https://maddevs.io/blog/map-in-mercator-projection-for-android?utm_source=devto&utm_medium=mad-map)