Search code examples
leafletgeojsonodkkobotoolbox

How can I convert a geoshape/geotrace/geopoint to GeoJSON?


I Am using API to access spatial data collected using kobotoolbox. the api returns spatial data either as geoshape/geotrace/geopoint, but I need to covert that data to Geojson so that I can display them on my map using leaflet.

here is sample string of geoshape '-6.725577650887138 39.10606026649475 0.0 0.0;-6.72631550943841 39.10506717860699 0.0 0.0;-6.727484560110362 39.10561669617891 0.0 0.0;-6.727484560110362 39.10561669617891 0.0 0.0;-6.725577650887138 39.10606026649475 0.0 0.0;'

thanks a lot in advance!


Solution

  • That "geoshape" doesn't look like any well-known vector data format but rather looks like a string of ;-separated 4-element vectors, which in turn are space-separated numbers.

    Some naïve parsing is in order.

    Start by splitting the string into an array of strings with String.prototype.split():

    let geoshape = '-6.725577650887138 39.10606026649475 0.0 0.0;-6.72631550943841 39.10506717860699 0.0 0.0;-6.727484560110362 39.10561669617891 0.0 0.0;-6.727484560110362 39.10561669617891 0.0 0.0;-6.725577650887138 39.10606026649475 0.0 0.0;';
    
    let points = geoshape.split(';');
    

    Now points is an Array of Strings. We can turn each of those strings into an Array of Numbers by providing a function that splits it by spaces:

    function spaceSeparatedStringToArray(str) {
         return str.split(' ');
    }
    

    ...and another function that coerces a String containing the textual representation of a number into a Number, like...

    function stringToNumber(str) {
        return Number(str);
    }
    

    ...and now let's put in some Array.prototype.map() magic and some method chaining magic, to make a function in which the input is a space-separated string and the output is an Array of Numbers:

    function stringToNumber(str) { return Number(str); }
    
    function spaceSeparatedStringToArrayOfNumbers(str) {
        return str.split(' ').map(stringToNumber);
    }
    

    Sometimes it's nicer to define those auxiliary functions passed to map() (or reduce() or filter() or etc) as anonymous lambda-functions, like:

    function spaceSeparatedStringToArrayOfNumbers(str) {
        return str.split(' ').map(function(str){ return Number(str) });
    }
    

    You can use arrow function syntax instead if you want:

    function spaceSeparatedStringToArrayOfNumbers(str) {
        return str.split(' ').map(str=>Number(str) );
    }
    

    And since we're using Number as a function, we can directly use that as the parameter of map():

    function spaceSeparatedStringToArrayOfNumbers(str) {
        return str.split(' ').map(Number);
    }
    

    Now that we have that function, let's go back to the beginning, and apply that function to each of the ;-separated substrings:

    function spaceSeparatedStringToArrayOfNumbers(str) {
        return str.split(' ').map(Number);
    }
    
    let geoshape = '-6.725577650887138 39.10606026649475 0.0 0.0;-6.72631550943841 39.10506717860699 0.0 0.0;-6.727484560110362 39.10561669617891 0.0 0.0;-6.727484560110362 39.10561669617891 0.0 0.0;-6.725577650887138 39.10606026649475 0.0 0.0;';
    
    let points = geoshape.split(';');
    
    let vectors = points.map(spaceSeparatedStringToArrayOfNumbers);
    

    Let's change that to add even more lambda and method chaining and arrow syntax:

    let vectors = geoshape.split(';').map((str)=>str.split(' ').map(Number));
    

    Also, since the GeoJSON format expects each coordinate as a 2-component vector instead of a 4-component vector, let's map each vector again to keep only the first two components. This uses array destructuring:

    let vectors = geoshape
                   .split(';')
                   .map((str)=>str.split(' ').map(Number))
                   .map(([x,y,w,z])=>[x,y]);
    

    See? I defined a lambda-function that takes a single argument, assumes it's an array (of numbers) with length 4, destructures the array, then returns a new array containing the first two elements. It can also work like:

    let coords = geoshape
                   .split(';')
                   .map((str)=>str.split(' ').map(Number))
                   .map(([x,y])=>[x,y]);
    

    Your sample data ends with a ;, and that causes an empty string, so I'll filter that out as well:

    let coords = geoshape
                   .split(';')
                   .filter(str=> str!=='')
                   .map((str)=>str.split(' ').map(Number))
                   .map(([x,y])=>[x,y]);
    

    I'm gonna assume that you're working in Dar es-Salaam and not in Badajoz, so let's flip latitude and longitude (see https://macwright.com/lonlat/ as well) by swapping x and y in ([x,y])=>[y,x]:

    let coords = geoshape
                   .split(';')
                   .filter(str=> str!=='')
                   .map((str)=>str.split(' ').map(Number))
                   .map(([x,y])=>[y,x]);
    

    Now head over to geojson.org and read RFC 7946 to see how GeoJSON data structures are specified. We already have the coordinates of a LineString geometry, so all it needs is some wrapping:

    let geojsonFeature = {
      "type": "Feature",
      "properties": {},
      "geometry": {
        "type": "LineString",
        "coordinates": coords
      }
    };
    

    If your sample dataset is meant to be a polygon with an outer hull and no inner hulls (read OGC SFA to know what "hull" means in this context), then you can wrap the coordinates in an extra inline array and specify Polygon as the geometry type:

    let geojsonFeature = {
      "type": "Feature",
      "properties": {},
      "geometry": {
        "type": "Polygon",
        "coordinates": [ coords ]
      }
    };
    

    Now you can feed it to L.geoJson, e.g.

    let line = L.geoJson(geojsonFeature).addTo(map);
    

    And finally, I'm gonna compact everything together just for the sake of it:

    let geoshape = '-6.725577650887138 39.10606026649475 0.0 0.0;-6.72631550943841 39.10506717860699 0.0 0.0;-6.727484560110362 39.10561669617891 0.0 0.0;-6.727484560110362 39.10561669617891 0.0 0.0;-6.725577650887138 39.10606026649475 0.0 0.0;';
    
    let leafletLine = L.geoJson({
        "type": "Feature",
        "properties": {},
        "geometry": {
            "type": "LineString",
            "coordinates": geoshape
                       .split(';')
                       .filter(str=> str!=='')
                       .map((str)=>str.split(' ').map(Number))
                       .map(([x,y])=>[y,x])
        }
    }).addTo(map);
    

    See a working example here. And please, please, don't copy-paste code blindly. Do read the linked documentation and resources, and understand what's going on.