| @ -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' %} | |||||