@ -0,0 +1,12 @@ | |||||
<?php | |||||
namespace OSM; | |||||
use OSM\Point; | |||||
class Box { | |||||
public Point $mininum; | |||||
public Point $maximum; | |||||
} |
@ -0,0 +1,70 @@ | |||||
<?php | |||||
namespace OSM\Element; | |||||
use OSM\Element\Member\Member; | |||||
use OSM\Element\Tag; | |||||
class Element { | |||||
public ?int $id = null; | |||||
public array $members = []; | |||||
public array $tags = []; | |||||
public static function createFromArray($array) { | |||||
assert(is_array($array)); | |||||
$hasType = isset($array['type']); | |||||
assert($hasType); | |||||
$className = __NAMESPACE__.'\\'.ucfirst($array['type']); | |||||
$hasClass = class_exists($className); | |||||
$instance = new $className(); | |||||
$hasId = isset($array['id']); | |||||
assert($hasId); | |||||
$instance->id = (int) $array['id']; | |||||
$hasMembers = isset($array['members']); | |||||
if ($hasMembers) { | |||||
$items = $array['members']; | |||||
foreach ($items as $item) { | |||||
$member = Member::createFromArray($item); | |||||
$instance->members[] = $member; | |||||
} | |||||
} | |||||
$hasTags = isset($array['tags']); | |||||
if ($hasTags) { | |||||
$items = $array['tags']; | |||||
foreach ($items as $itemKey => $itemValue) { | |||||
$tag = Tag::createFromValues($itemKey, $itemValue); | |||||
$instance->tags[] = $tag; | |||||
} | |||||
} | |||||
$instance->completeFromArray($array); | |||||
return $instance; | |||||
} | |||||
public function completeFromArray(array $array): static | |||||
{ | |||||
return $this; | |||||
} | |||||
public function getTagValue(string $key): ?string { | |||||
$foundTags = array_filter($this->tags, function ($tag) use ($key) { | |||||
return $tag->key === $key; | |||||
}); | |||||
if (count($foundTags) !== 1) { | |||||
return null; | |||||
} | |||||
$tag = reset($foundTags); | |||||
return $tag->value; | |||||
} | |||||
} |
@ -0,0 +1,38 @@ | |||||
<?php | |||||
namespace OSM\Element\Member; | |||||
class Member { | |||||
public int $ref; | |||||
public string $role; | |||||
public static function createFromArray($array) { | |||||
assert(is_array($array)); | |||||
$hasType = isset($array['type']); | |||||
assert($hasType); | |||||
$className = __NAMESPACE__.'\\'.ucfirst($array['type']); | |||||
$hasClass = class_exists($className); | |||||
$instance = new $className(); | |||||
$hasRef = isset($array['ref']); | |||||
assert($hasRef); | |||||
$instance->ref = (int) $array['ref']; | |||||
$hasRole = isset($array['role']); | |||||
assert($hasRole); | |||||
$instance->role = (string) $array['role']; | |||||
$instance->completeFromArray($array); | |||||
return $instance; | |||||
} | |||||
public function isSame(Member $other): bool { | |||||
return ($this->ref === $other->ref); | |||||
} | |||||
} |
@ -0,0 +1,19 @@ | |||||
<?php | |||||
namespace OSM\Element\Member; | |||||
use OSM\Element\Member\Member; | |||||
use OSM\Point; | |||||
class Node extends Member { | |||||
public Point $point; | |||||
public function completeFromArray(array $array): static | |||||
{ | |||||
$this->point = Point::createFromArray($array); | |||||
return $this; | |||||
} | |||||
} |
@ -0,0 +1,38 @@ | |||||
<?php | |||||
namespace OSM\Element\Member; | |||||
use OSM\Element\Member\Member; | |||||
use OSM\Point; | |||||
class Way extends Member { | |||||
public array $points = []; | |||||
public function completeFromArray(array $array): static | |||||
{ | |||||
$hasGeometry = isset($array['geometry']); | |||||
if ($hasGeometry) { | |||||
$items = $array['geometry']; | |||||
foreach ($items as $item) { | |||||
$point = Point::createFromArray($item); | |||||
$this->points[] = $point; | |||||
} | |||||
} | |||||
return $this; | |||||
} | |||||
public function getFirstPoint(): Point { | |||||
return reset($this->points); | |||||
} | |||||
public function getLastPoint(): Point { | |||||
return end($this->points); | |||||
} | |||||
public function reversePoints(): static { | |||||
$this->points = array_reverse($this->points); | |||||
return $this; | |||||
} | |||||
} |
@ -0,0 +1,86 @@ | |||||
<?php | |||||
namespace OSM\Element; | |||||
use OSM\Element\Element; | |||||
use OSM\Element\Member\Way; | |||||
use OSM\Point; | |||||
class Relation extends Element { | |||||
public function getOuterWays(): array { | |||||
return array_filter($this->members, function ($member) { | |||||
return ( | |||||
($member instanceof \OSM\Element\Member\Way) | |||||
and ($member->role === 'outer') | |||||
); | |||||
}); | |||||
} | |||||
public function getOuterWaysStartingWith(Point $point, ?Way $exclude = null): array { | |||||
return array_filter($this->members, function ($member) use ($point, $exclude) { | |||||
return ( | |||||
($member instanceof \OSM\Element\Member\Way) | |||||
and ($member->role === 'outer') | |||||
and ($point->isSame($member->getFirstPoint())) | |||||
and ( | |||||
is_null($exclude) | |||||
or !$member->isSame($exclude) | |||||
) | |||||
); | |||||
}); | |||||
} | |||||
public function getOuterWaysStopingWith(Point $point, ?Way $exclude = null): array { | |||||
return array_filter($this->members, function ($member) use ($point, $exclude) { | |||||
return ( | |||||
($member instanceof \OSM\Element\Member\Way) | |||||
and ($member->role === 'outer') | |||||
and ($point->isSame($member->getLastPoint())) | |||||
and ( | |||||
is_null($exclude) | |||||
or !$member->isSame($exclude) | |||||
) | |||||
); | |||||
}); | |||||
} | |||||
public function getOrderedOuterWays(): array { | |||||
$orderedWays = []; | |||||
$ways = $this->getOuterWays(); | |||||
$currentWay = reset($ways); | |||||
$veryFirstPoint = $currentWay->getFirstPoint(); | |||||
$isDone = false; | |||||
while (!$isDone) { | |||||
$orderedWays[] = $currentWay; | |||||
$nextWays = $this->getOuterWaysStartingWith($currentWay->getLastPoint(), $currentWay); | |||||
assert(count($nextWays) <= 1); | |||||
if (count($nextWays) === 1) { | |||||
$nextWay = reset($nextWays); | |||||
if ($veryFirstPoint->isSame($nextWay->getFirstPoint())) { | |||||
break; | |||||
} | |||||
$currentWay = $nextWay; | |||||
continue; | |||||
} else { | |||||
$nextWays = $this->getOuterWaysStopingWith($currentWay->getLastPoint(), $currentWay); | |||||
assert(count($nextWays) <= 1); | |||||
if (count($nextWays) === 1) { | |||||
$nextWay = reset($nextWays); | |||||
$nextWay->reversePoints(); | |||||
if ($veryFirstPoint->isSame($nextWay->getFirstPoint())) { | |||||
break; | |||||
} | |||||
$currentWay = $nextWay; | |||||
continue; | |||||
} else { | |||||
$isDone = true; | |||||
} | |||||
} | |||||
} | |||||
return $orderedWays; | |||||
} | |||||
} |
@ -0,0 +1,22 @@ | |||||
<?php | |||||
namespace OSM\Element; | |||||
class Tag { | |||||
public ?string $key; | |||||
public ?string $value; | |||||
public static function createFromValues( | |||||
string $key, | |||||
string $value | |||||
): static | |||||
{ | |||||
$instance = new self(); | |||||
$instance->key = $key; | |||||
$instance->value = $value; | |||||
return $instance; | |||||
} | |||||
} |
@ -0,0 +1,25 @@ | |||||
<?php | |||||
namespace OSM; | |||||
class GeoJsonConverter { | |||||
public static function convertRelationToPolygon( | |||||
\OSM\Element\Relation $relation | |||||
): \GeoJson\Geometry\Polygon | |||||
{ | |||||
$positions = []; | |||||
foreach ($relation->getOrderedOuterWays() as $way) { | |||||
foreach ($way->points as $point) { | |||||
$positions[] = new \GeoJson\Geometry\Point([ | |||||
$point->longitude, | |||||
$point->latitude | |||||
]); | |||||
} | |||||
} | |||||
return new \GeoJson\Geometry\Polygon([ $positions ]); | |||||
} | |||||
} |
@ -0,0 +1,25 @@ | |||||
<?php | |||||
namespace OSM; | |||||
use OSM\Element\Element; | |||||
class OSM { | |||||
public array $elements = []; | |||||
public static function createFromJson($json) { | |||||
$array = json_decode($json, true); | |||||
$instance = new self(); | |||||
$items = $array['elements']; | |||||
foreach ($items as $item) { | |||||
$element = Element::createFromArray($item); | |||||
$instance->elements[] = $element; | |||||
} | |||||
return $instance; | |||||
} | |||||
} |
@ -0,0 +1,30 @@ | |||||
<?php | |||||
namespace OSM; | |||||
class Point { | |||||
public float $latitude; | |||||
public float $longitude; | |||||
public static function createFromArray(array $array) | |||||
{ | |||||
$hasLat = isset($array['lat']); | |||||
$hasLon = isset($array['lon']); | |||||
assert($hasLat and $hasLon); | |||||
$instance = new self(); | |||||
$instance->latitude = (float) $array['lat']; | |||||
$instance->longitude = (float) $array['lon']; | |||||
return $instance; | |||||
} | |||||
public function isSame(Point $other): bool { | |||||
$isSame = ( | |||||
($other->latitude === $this->latitude) | |||||
and ($other->longitude === $this->longitude) | |||||
); | |||||
return $isSame; | |||||
} | |||||
} |
@ -0,0 +1,4 @@ | |||||
upload_max_filesize = 40M | |||||
post_max_size = 40M | |||||
memory_limit = 256M | |||||
@ -0,0 +1,96 @@ | |||||
<?php | |||||
namespace App\Controller; | |||||
use App\Form\CityToolType; | |||||
use App\Service\OverpassClient; | |||||
use KnpU\OAuth2ClientBundle\Client\ClientRegistry; | |||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |||||
use Symfony\Bundle\SecurityBundle\Security; | |||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType; | |||||
use Symfony\Component\HttpFoundation\HeaderUtils; | |||||
use Symfony\Component\HttpFoundation\Request; | |||||
use Symfony\Component\HttpFoundation\Response; | |||||
use Symfony\Component\HttpFoundation\ResponseHeaderBag; | |||||
use Symfony\Component\HttpFoundation\StreamedResponse; | |||||
use Symfony\Component\Routing\Attribute\Route; | |||||
#[Route('/tools')] | |||||
class ToolsController extends AbstractController | |||||
{ | |||||
#[Route('/', name: 'app_tools')] | |||||
public function index(): Response | |||||
{ | |||||
return $this->render('tools/index.html.twig', [ | |||||
]); | |||||
} | |||||
#[Route('/city', name: 'app_tools_city')] | |||||
public function city( | |||||
Request $request, | |||||
OverpassClient $overpass, | |||||
): Response | |||||
{ | |||||
$form = $this->createForm(CityToolType::class, []); | |||||
$form->add('submit', SubmitType::class, ['label' => 'Générer']); | |||||
$form->handleRequest($request); | |||||
if ($form->isSubmitted() and $form->isValid()) { | |||||
$areaId = $form->get('area')->getData(); | |||||
$query = sprintf('relation["boundary"="administrative"]["admin_level"="8"]["name"](area:%d);', 3600000000 + $areaId); // Cf. <https://wiki.openstreetmap.org/wiki/Overpass_API/Overpass_QL#By_area_.28area.29> | |||||
$json = $overpass->query($query); | |||||
$response = new StreamedResponse(); | |||||
$response->headers->set('Content-Type', 'text/csv'); | |||||
$response->headers->set( | |||||
'Content-Disposition', | |||||
HeaderUtils::makeDisposition( | |||||
HeaderUtils::DISPOSITION_ATTACHMENT, | |||||
sprintf('cities-in-%d.csv', $areaId) | |||||
) | |||||
); | |||||
$response->setCallback(function () use ($json): void { | |||||
$headings = [ | |||||
'name', | |||||
'description', | |||||
'osm', | |||||
'geojson', | |||||
'status', | |||||
]; | |||||
$csv = fopen('php://output', 'a'); | |||||
fputcsv($csv, $headings); | |||||
$osm = \OSM\OSM::createFromJson($json); | |||||
foreach ($osm->elements as $relation) { | |||||
$name = $relation->getTagValue('name'); | |||||
$feature = new \GeoJson\Feature\Feature( | |||||
\OSM\GeoJsonConverter::convertRelationToPolygon($relation), | |||||
['name' => $name] | |||||
); | |||||
fputcsv($csv, [ | |||||
$name, | |||||
$name, | |||||
'', | |||||
json_encode(new \GeoJson\Feature\FeatureCollection([ $feature ])), | |||||
'todo', | |||||
]); | |||||
} | |||||
fclose($csv); | |||||
}); | |||||
return $response; | |||||
} | |||||
return $this->render('tools/city.html.twig', [ | |||||
'form' => $form, | |||||
]); | |||||
} | |||||
} |
@ -0,0 +1,56 @@ | |||||
<?php | |||||
namespace App\Form; | |||||
use App\Service\OverpassClient; | |||||
use Symfony\Component\Form\AbstractType; | |||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType; | |||||
use Symfony\Component\Form\Extension\Core\Type\NumberType; | |||||
use Symfony\Component\Form\FormBuilderInterface; | |||||
use Symfony\Component\HttpFoundation\RequestStack; | |||||
use Symfony\Component\Validator\Constraints\File; | |||||
class CityToolType extends AbstractType | |||||
{ | |||||
public function __construct( | |||||
private OverpassClient $overpass, | |||||
private RequestStack $requestStack, | |||||
) { | |||||
} | |||||
public function buildForm(FormBuilderInterface $builder, array $options): void | |||||
{ | |||||
$session = $this->requestStack->getSession(); | |||||
$choices = $session->has('city_tool_choices') ? $session->get('city_tool_choices') : []; | |||||
if (empty($choices)) { | |||||
$entries = $this->overpass->rawQuery('[out:csv(::id,"ref","name")][timeout:25];area(3602202162)->.searchArea;relation["name"]["boundary"="administrative"]["border_type"="departement"]["admin_level"="6"](area.searchArea);out;'); | |||||
$col = []; | |||||
$row = []; | |||||
$entries = explode("\n", $entries); | |||||
foreach ($entries as $index => $entry) { | |||||
$entry = explode("\t", $entry); | |||||
if ($index === 0) { | |||||
$col = $entry; | |||||
} else { | |||||
if (count($col) === count($entry)) { | |||||
$row = array_combine($col, $entry); | |||||
$key = sprintf('%s %s', $row['ref'], $row['name']); | |||||
$val = (int) $row['@id']; | |||||
$choices[$key] = $val; | |||||
} | |||||
} | |||||
} | |||||
ksort($choices); | |||||
} | |||||
$session->set('city_tool_choices', $choices); | |||||
$builder | |||||
->add('area', ChoiceType::class, [ | |||||
'label' => 'Département', | |||||
'choices' => $choices, | |||||
]) | |||||
; | |||||
} | |||||
} |
@ -0,0 +1,14 @@ | |||||
{% extends 'base.html.twig' %} | |||||
{% block page_title %}Outils{% endblock %} | |||||
{% block page_content %} | |||||
<ul class="nav nav-tabs mb-3"> | |||||
{% for tool in tools %} | |||||
<li class="nav-item"> | |||||
<a class="nav-link{% if app.current_route == tool.route %} active" aria-current="page{% endif %}" href="{{ path(tool.route) }}">{{ tool.label }}</a> | |||||
</li> | |||||
{% endfor %} | |||||
</ul> | |||||
{% block tools_content %}{% endblock %} | |||||
{% endblock %} |
@ -0,0 +1,5 @@ | |||||
{% extends 'tools/base.html.twig' %} | |||||
{% block tools_content %} | |||||
{{ form(form) }} | |||||
{% endblock %} |
@ -0,0 +1,2 @@ | |||||
{% extends 'tools/base.html.twig' %} | |||||