Influenced by SPARQL

Influenced by SPARQL
Photo by Tbel Abuseridze / Unsplash

It is some time now that I'm working on a SPARQL ORM for Laravel, directly derived from Eloquent and sharing (at least, in the original intention) the same feeling and interface.

I've not tagged yet a first release, but I'm actually esperimenting with some very simple use cases to test, measure and check the behaviour of the package. Still a lot of work is required, but want to share the current results.

Here, an SVG diagram rappresenting who influenced who (in science, art, philosophy...) though history.

It is derived from the dbo:birthDate, dbo:influenced and dbo:influencedBy properties in DBPedia, which are themselves extracted from Wikipedia's infoboxes.

Here the code of the Laravel command used to generate it. To be true, most of the code is for actual SVG generation (using this PHP package), but the relevant part is SPARQL quering through the ORM.

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;

use DB;

use SolidDataWorkers\SPARQL\Eloquent\Model;
use SolidDataWorkers\SPARQL\Query\Expression;

use SVG\SVG;
use SVG\Nodes\Shapes\SVGCircle;
use SVG\Nodes\Shapes\SVGPath;
use SVG\Nodes\Texts\SVGText;

class Influences extends Command
{
    protected $signature = 'make:influences';
    protected $description = 'Creates an SVG diagram rappresenting who influended who through history';

    private function createSlot($p, &$data)
    {
        if (!isset($data[$p->id])) {
            if ($p->dbo_birthDate == null) {
                return;
            }

            /*
                For simplicity, just the year of birth is considered.
                Here we have to deal with negative years (AD years), which PHP
                doesn't handle very well...
            */
            $bd = explode('-', $p->dbo_birthDate->last());
            if (count($bd) == 3) {
                $bd_year = (int) $bd[0];
            }
            else {
                $bd_year = ((int) $bd[1]) * -1;
            }

            $data[$p->id] = (object) [
                'birthdate' => $bd_year,
                'influenced' => [],
            ];
        }
    }

    private function fetchData()
    {
        /*
            For convenience, once the data are fetched their are saved in a JSON
            file for other usages
        */
        if (file_exists('data.json') == false) {
            $data = [];

            /*
                This is to get the Eloquent-like class associated to the given
                RDF class. Automatically generated from laravel-sparql
            */
            $model = DB::getIntrospector()->getModel('dbo:Person');

            /*
                Get all people who "influended" someone else, and put into the
                main array. Here we consider only influence on other people, not
                things, and only people for which we have birth and death date
                (despite we don't actually use the death date, only the birth
                date to put the person in the final timeline).
                Not yet populating the "influenced" inner array, to leverage
                informations fetched with the "influencedBy" query here below
            */
            $people1 = $model::select('dbo:birthDate')
                ->whereNotNull('dbo:deathDate')
                ->with(['dbo:influenced' => function($query) {
                    $query->where('rdf:type', new Expression('dbo:Person', 'class'))
                        ->whereNotNull('dbo:birthDate')
                        ->whereNotNull('dbo:deathDate');
                }])
                ->get();

            foreach($people1 as $p) {
                $this->createSlot($p, $data);
                $data[$p->id]->influenced = $p->dbo_influenced->pluck('id')->toArray();
            }

            /*
                Get all people which have been "influencedBy".
                Not all relations, in both directions, are present in the
                DBPedia graph, so we fetch both.
            */
            $people2 = $model::select('dbo:birthDate')
                ->whereNotNull('dbo:deathDate')
                ->with(['dbo:influencedBy' => function($query) {
                    $query->where('rdf:type', new Expression('dbo:Person', 'class'))
                        ->whereNotNull('dbo:birthDate')
                        ->whereNotNull('dbo:deathDate');
                }])
                ->get();

            foreach($people2 as $p) {
                $this->createSlot($p, $data);

                foreach($p->dbo_influencedBy as $influencer) {
                    $this->createSlot($influencer, $data);
                    $data[$influencer->id]->influenced[] = $p->id;
                }
            }

            /*
                At this point most the the influenced people should already be
                into the main array, let fill the rest (people referred by
                "influenced" but not having the inverse property "influencedBy")
            */
            foreach($people1 as $p) {
                foreach($p->dbo_influenced as $influenced) {
                    $this->createSlot($influenced, $data);
                }
            }

            file_put_contents('data.json', json_encode($data));
        }

        return json_decode(file_get_contents('data.json'));
    }

    /*
        Utilities to generate an SVG arc path.
        Translated in PHP from
        https://stackoverflow.com/a/18473154/3135371
    */

    private function polarToCartesian($centerX, $centerY, $radius, $angleInDegrees) {
        $angleInRadians = ($angleInDegrees - 90) * M_PI / 180.0;

        return (object) [
            'x' => $centerX + ($radius * cos($angleInRadians)),
            'y' => $centerY + ($radius * sin($angleInRadians))
        ];
    }

    private function describeArc($x, $y, $radius, $startAngle, $endAngle) {
        $start = $this->polarToCartesian($x, $y, $radius, $endAngle);
        $end = $this->polarToCartesian($x, $y, $radius, $startAngle);

        $largeArcFlag = ($endAngle - $startAngle <= 180 ? "0" : "1");

        $d = [
            "M", $start->x, $start->y,
            "A", $radius, $radius, 0, $largeArcFlag, 0, $end->x, $end->y
        ];

        return join(' ', $d);
    }

    /*
        Here the SVG file is actually created
    */
    private function renderToSVG($data, $filepath)
    {
        $minyear = 0;
        $maxyear = 0;
        $width = 4000;
        $height = 2200;
        $global_y = $height - 100;

        foreach($data as $identifier => $meta) {
            if ($meta->birthdate < $minyear) {
                $minyear = $meta->birthdate;
            }
            if ($meta->birthdate > $maxyear) {
                $maxyear = $meta->birthdate;
            }
        }

        $year_offset = $width / (($maxyear - $minyear) + 20);

        $image = new SVG($width, $height);
        $doc = $image->getDocument();

        foreach($data as $identifier => $meta) {
            $x = ($meta->birthdate - $minyear + 10) * $year_offset;
            $person = new SVGCircle($x, $global_y, 3, 3);
            $person->setStyle('fill', '#FF8000');
            $doc->addChild($person);

            foreach($meta->influenced as $subidentifier) {
                $subnode = $data->$subidentifier;
                $min = min($meta->birthdate, $subnode->birthdate);
                $max = max($meta->birthdate, $subnode->birthdate);
                $ax = ($min + (($max - $min) / 2) - $minyear + 10) * $year_offset;
                $radius = (($max - $min) / 2) * $year_offset;
                $arc = new SVGPath($this->describeArc($ax, $global_y, $radius, -90, 90));
                $arc->setStyle('fill', 'transparent')->setStyle('stroke', '#FF8000');
                $doc->addChild($arc);
            }
        }

        for ($i = (ceil($minyear / 100) * 100); $i < (ceil($maxyear / 100) * 100); $i += 100) {
            $x = ($i - $minyear + 10) * $year_offset;
            $text = new SVGText((string) $i, $x, $global_y + 50);
            $text->setSize(30);
            $doc->addChild($text);
        }

        file_put_contents($filepath, $image);
    }

    public function handle()
    {
        $data = $this->fetchData();
        $this->renderToSVG($data, 'image.svg');
    }
}

The example is very simple but still helped a lot to refine some internals of my package (basic relationships management, dynamic fetching of properties, many optimizations...) and raise other issues still to be managed (eager loading of models from the endpoint).

More to follow about the laravel-sparql package...