mirror of
https://forge.chapril.org/tykayn/osm-commerces
synced 2025-10-09 17:02:46 +02:00
Compare commits
No commits in common. "7665f1d99c0e3a8d6f04642b1e75909bf47a5303" and "ca00f8c0be41d8afe40b8d2972d091a24052f2e5" have entirely different histories.
7665f1d99c
...
ca00f8c0be
226 changed files with 2780 additions and 2020347 deletions
File diff suppressed because one or more lines are too long
5
.idea/codeStyles/codeStyleConfig.xml
generated
5
.idea/codeStyles/codeStyleConfig.xml
generated
|
@ -1,5 +0,0 @@
|
|||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||
</state>
|
||||
</component>
|
5247
.idea/commandlinetools/Symfony_03_07_2025_09_48.xml
generated
5247
.idea/commandlinetools/Symfony_03_07_2025_09_48.xml
generated
File diff suppressed because one or more lines are too long
|
@ -1,47 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified"
|
||||
xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:element name="framework" type="frameworkType"/>
|
||||
<xs:complexType name="commandType">
|
||||
<xs:all>
|
||||
<xs:element type="xs:string" name="name" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element type="xs:string" name="params" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element type="xs:string" name="help" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element type="optionsBeforeType" name="optionsBefore" minOccurs="0" maxOccurs="1"/>
|
||||
</xs:all>
|
||||
</xs:complexType>
|
||||
<xs:complexType name="frameworkType">
|
||||
<xs:sequence>
|
||||
<xs:element type="xs:string" name="extraData" minOccurs="0" maxOccurs="1"/>
|
||||
<xs:element type="commandType" name="command" maxOccurs="unbounded" minOccurs="0"/>
|
||||
<xs:element type="xs:string" name="help" minOccurs="0" maxOccurs="1"/>
|
||||
</xs:sequence>
|
||||
<xs:attribute type="xs:string" name="name" use="required"/>
|
||||
<xs:attribute type="xs:string" name="invoke" use="required"/>
|
||||
<xs:attribute type="xs:string" name="alias" use="required"/>
|
||||
<xs:attribute type="xs:boolean" name="enabled" use="required"/>
|
||||
<xs:attribute type="xs:integer" name="version" use="required"/>
|
||||
<xs:attribute type="xs:string" name="frameworkId" use="optional"/>
|
||||
</xs:complexType>
|
||||
<xs:complexType name="optionsBeforeType">
|
||||
<xs:sequence>
|
||||
<xs:element type="optionType" name="option" maxOccurs="unbounded" minOccurs="0"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
<xs:complexType name="optionType">
|
||||
<xs:sequence>
|
||||
<xs:element type="xs:string" name="help" minOccurs="0" maxOccurs="1"/>
|
||||
</xs:sequence>
|
||||
<xs:attribute type="xs:string" name="name" use="required"/>
|
||||
<xs:attribute type="xs:string" name="shortcut" use="optional"/>
|
||||
<xs:attribute name="pattern" use="optional">
|
||||
<xs:simpleType>
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="space"/>
|
||||
<xs:enumeration value="equals"/>
|
||||
<xs:enumeration value="unknown"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
</xs:schema>
|
145
.idea/osm-commerce-sf.iml
generated
145
.idea/osm-commerce-sf.iml
generated
|
@ -2,154 +2,9 @@
|
|||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" packagePrefix="App\" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" packagePrefix="App\Tests\" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/composer" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/collections" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/data-fixtures" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/dbal" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/deprecations" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/doctrine-bundle" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/doctrine-fixtures-bundle" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/doctrine-migrations-bundle" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/event-manager" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/inflector" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/instantiator" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/lexer" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/migrations" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/orm" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/persistence" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/sql-formatter" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/egulias/email-validator" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/fakerphp/faker" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/guzzle" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/promises" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/psr7" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/masterminds/html5" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/monolog/monolog" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/myclabs/deep-copy" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/nikic/php-parser" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/nyholm/psr7" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/phar-io/manifest" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/phar-io/version" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/php-http/discovery" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/phpdocumentor/reflection-common" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/phpdocumentor/reflection-docblock" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/phpdocumentor/type-resolver" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/phpstan/phpdoc-parser" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-code-coverage" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-file-iterator" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-invoker" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-text-template" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-timer" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/phpunit" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/cache" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/clock" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/container" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/event-dispatcher" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-client" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-factory" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-message" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/link" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/log" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/ralouphie/getallheaders" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/cli-parser" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/code-unit" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/code-unit-reverse-lookup" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/comparator" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/complexity" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/diff" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/environment" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/exporter" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/global-state" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/lines-of-code" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/object-enumerator" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/object-reflector" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/recursion-context" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/resource-operations" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/type" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/version" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/asset" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/browser-kit" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/cache" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/cache-contracts" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/clock" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/config" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/console" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/css-selector" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/debug-bundle" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/dependency-injection" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/deprecation-contracts" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/doctrine-bridge" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/doctrine-messenger" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/dom-crawler" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/dotenv" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/error-handler" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/event-dispatcher" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/event-dispatcher-contracts" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/expression-language" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/filesystem" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/finder" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/flex" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/form" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/framework-bundle" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-client" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-client-contracts" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-foundation" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-kernel" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/intl" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/mailer" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/maker-bundle" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/messenger" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/mime" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/monolog-bridge" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/monolog-bundle" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/notifier" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/options-resolver" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/password-hasher" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/phpunit-bridge" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-grapheme" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-icu" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-idn" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-normalizer" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-mbstring" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-php83" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-php84" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/process" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/property-access" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/property-info" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/routing" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/runtime" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/security-bundle" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/security-core" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/security-csrf" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/security-http" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/serializer" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/service-contracts" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/stopwatch" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/string" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/translation" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/translation-contracts" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/twig-bridge" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/twig-bundle" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/type-info" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/validator" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/var-dumper" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/var-exporter" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/web-link" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/web-profiler-bundle" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/webpack-encore-bundle" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/yaml" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/theseer/tokenizer" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/twig/extra-bundle" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/twig/twig" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/webmozart/assert" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/public/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/var" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
|
|
184
.idea/php.xml
generated
184
.idea/php.xml
generated
|
@ -1,184 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="MessDetectorOptionsConfiguration">
|
||||
<option name="transferred" value="true" />
|
||||
</component>
|
||||
<component name="PHPCSFixerOptionsConfiguration">
|
||||
<option name="transferred" value="true" />
|
||||
</component>
|
||||
<component name="PHPCodeSnifferOptionsConfiguration">
|
||||
<option name="highlightLevel" value="WARNING" />
|
||||
<option name="transferred" value="true" />
|
||||
</component>
|
||||
<component name="PhpCodeSniffer">
|
||||
<phpcs_settings>
|
||||
<phpcs_by_interpreter asDefaultInterpreter="true" interpreter_id="9bdd9bda-8cdb-43f2-af13-070085f7956e" timeout="30000" />
|
||||
</phpcs_settings>
|
||||
</component>
|
||||
<component name="PhpIncludePathManager">
|
||||
<include_path>
|
||||
<path value="$PROJECT_DIR$/vendor/phpunit/phpunit" />
|
||||
<path value="$PROJECT_DIR$/vendor/php-http/discovery" />
|
||||
<path value="$PROJECT_DIR$/vendor/myclabs/deep-copy" />
|
||||
<path value="$PROJECT_DIR$/vendor/nyholm/psr7" />
|
||||
<path value="$PROJECT_DIR$/vendor/composer" />
|
||||
<path value="$PROJECT_DIR$/vendor/egulias/email-validator" />
|
||||
<path value="$PROJECT_DIR$/vendor/webmozart/assert" />
|
||||
<path value="$PROJECT_DIR$/vendor/ralouphie/getallheaders" />
|
||||
<path value="$PROJECT_DIR$/vendor/guzzlehttp/guzzle" />
|
||||
<path value="$PROJECT_DIR$/vendor/guzzlehttp/psr7" />
|
||||
<path value="$PROJECT_DIR$/vendor/guzzlehttp/promises" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/global-state" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/complexity" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/recursion-context" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/object-reflector" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/object-enumerator" />
|
||||
<path value="$PROJECT_DIR$/vendor/phar-io/manifest" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/cli-parser" />
|
||||
<path value="$PROJECT_DIR$/vendor/phar-io/version" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/lines-of-code" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/diff" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/type" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/version" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/comparator" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/exporter" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/environment" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/code-unit" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/resource-operations" />
|
||||
<path value="$PROJECT_DIR$/vendor/sebastian/code-unit-reverse-lookup" />
|
||||
<path value="$PROJECT_DIR$/vendor/psr/container" />
|
||||
<path value="$PROJECT_DIR$/vendor/psr/http-factory" />
|
||||
<path value="$PROJECT_DIR$/vendor/psr/http-client" />
|
||||
<path value="$PROJECT_DIR$/vendor/psr/http-message" />
|
||||
<path value="$PROJECT_DIR$/vendor/psr/cache" />
|
||||
<path value="$PROJECT_DIR$/vendor/psr/event-dispatcher" />
|
||||
<path value="$PROJECT_DIR$/vendor/psr/log" />
|
||||
<path value="$PROJECT_DIR$/vendor/psr/link" />
|
||||
<path value="$PROJECT_DIR$/vendor/psr/clock" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/deprecation-contracts" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/dependency-injection" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/security-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/mime" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/validator" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php83" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/browser-kit" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php84" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/http-client-contracts" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/dom-crawler" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/css-selector" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/property-access" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/monolog-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-grapheme" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher-contracts" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/yaml" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-icu" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/cache" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/options-resolver" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/doctrine-bridge" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/webpack-encore-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/twig-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/doctrine-messenger" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/runtime" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/http-kernel" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/clock" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/mailer" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/phpunit-bridge" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/finder" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/security-csrf" />
|
||||
<path value="$PROJECT_DIR$/vendor/monolog/monolog" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/expression-language" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/maker-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/security-core" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/security-http" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/web-link" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/var-exporter" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/web-profiler-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/string" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/service-contracts" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/var-dumper" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/serializer" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-idn" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/type-info" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/debug-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/routing" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/error-handler" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/flex" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/notifier" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/cache-contracts" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/property-info" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/console" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/process" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/framework-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/phpstan/phpdoc-parser" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-normalizer" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/password-hasher" />
|
||||
<path value="$PROJECT_DIR$/vendor/masterminds/html5" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/intl" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/filesystem" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/twig-bridge" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/http-client" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/dotenv" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/form" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/messenger" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/translation" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-mbstring" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/asset" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/monolog-bridge" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/http-foundation" />
|
||||
<path value="$PROJECT_DIR$/vendor/twig/twig" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/config" />
|
||||
<path value="$PROJECT_DIR$/vendor/twig/extra-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/translation-contracts" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/stopwatch" />
|
||||
<path value="$PROJECT_DIR$/vendor/fakerphp/faker" />
|
||||
<path value="$PROJECT_DIR$/vendor/theseer/tokenizer" />
|
||||
<path value="$PROJECT_DIR$/vendor/phpdocumentor/reflection-docblock" />
|
||||
<path value="$PROJECT_DIR$/vendor/phpdocumentor/type-resolver" />
|
||||
<path value="$PROJECT_DIR$/vendor/phpdocumentor/reflection-common" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/dbal" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/event-manager" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/orm" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/deprecations" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/lexer" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/sql-formatter" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/data-fixtures" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/collections" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/migrations" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/instantiator" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-fixtures-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-migrations-bundle" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/inflector" />
|
||||
<path value="$PROJECT_DIR$/vendor/doctrine/persistence" />
|
||||
<path value="$PROJECT_DIR$/vendor/nikic/php-parser" />
|
||||
<path value="$PROJECT_DIR$/vendor/phpunit/php-file-iterator" />
|
||||
<path value="$PROJECT_DIR$/vendor/phpunit/php-timer" />
|
||||
<path value="$PROJECT_DIR$/vendor/phpunit/php-text-template" />
|
||||
<path value="$PROJECT_DIR$/vendor/phpunit/php-invoker" />
|
||||
<path value="$PROJECT_DIR$/vendor/phpunit/php-code-coverage" />
|
||||
</include_path>
|
||||
</component>
|
||||
<component name="PhpProjectSharedConfiguration" php_language_level="8.2" />
|
||||
<component name="PhpStan">
|
||||
<PhpStan_settings>
|
||||
<phpstan_by_interpreter asDefaultInterpreter="true" interpreter_id="9bdd9bda-8cdb-43f2-af13-070085f7956e" timeout="60000" />
|
||||
</PhpStan_settings>
|
||||
</component>
|
||||
<component name="PhpStanOptionsConfiguration">
|
||||
<option name="transferred" value="true" />
|
||||
</component>
|
||||
<component name="PhpUnit">
|
||||
<phpunit_settings>
|
||||
<PhpUnitSettings configuration_file_path="$PROJECT_DIR$/phpunit.xml.dist" custom_loader_path="$PROJECT_DIR$/vendor/autoload.php" use_configuration_file="true" />
|
||||
</phpunit_settings>
|
||||
</component>
|
||||
<component name="Psalm">
|
||||
<Psalm_settings>
|
||||
<psalm_fixer_by_interpreter asDefaultInterpreter="true" interpreter_id="9bdd9bda-8cdb-43f2-af13-070085f7956e" timeout="60000" />
|
||||
</Psalm_settings>
|
||||
</component>
|
||||
<component name="PsalmOptionsConfiguration">
|
||||
<option name="transferred" value="true" />
|
||||
</component>
|
||||
</project>
|
10
.idea/phpunit.xml
generated
10
.idea/phpunit.xml
generated
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="PHPUnit">
|
||||
<option name="directories">
|
||||
<list>
|
||||
<option value="$PROJECT_DIR$/tests" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
6
.idea/symfony2.xml
generated
6
.idea/symfony2.xml
generated
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Symfony2PluginSettings">
|
||||
<option name="pluginEnabled" value="true" />
|
||||
</component>
|
||||
</project>
|
334
README.md
334
README.md
|
@ -6,11 +6,11 @@ Configurer .env.local pour mettre le token bearer d'un compte dédié
|
|||
installer les dépendances avec composer
|
||||
déployer sur un serveur ayant du php 8
|
||||
|
||||
# Dépendances
|
||||
# Dépendances
|
||||
|
||||
- PHP 8.1 ou supérieur
|
||||
- Composer
|
||||
- PostgreSQL 13 ou supériesdfsdffgdfgfdgur, ou Mysql / MariaDB
|
||||
- PostgreSQL 13 ou supérieur, ou Mysql / MariaDB
|
||||
- Symfony 7.2
|
||||
- Extensions PHP requises :
|
||||
- pdo_pgsql
|
||||
|
@ -18,7 +18,7 @@ déployer sur un serveur ayant du php 8
|
|||
- intl
|
||||
- mbstring
|
||||
|
||||
# Base de données
|
||||
# base de données
|
||||
|
||||
créer un utilisateur et sa base
|
||||
|
||||
|
@ -36,333 +36,5 @@ CREATE USER 'sf'@'localhost' IDENTIFIED BY 'sfrgdHYJi56631lyshFSQGfd45452ùwdf54
|
|||
CREATE DATABASE `osm-my-commerce`;
|
||||
GRANT ALL PRIVILEGES ON `osm-my-commerce`.* TO 'sf'@'localhost';
|
||||
FLUSH PRIVILEGES;
|
||||
```
|
||||
|
||||
# Installation et configuration
|
||||
|
||||
## 1. Installation des dépendances
|
||||
```shell
|
||||
composer install
|
||||
npm install
|
||||
```
|
||||
|
||||
## 2. Configuration de l'environnement
|
||||
Créer un fichier `.env.local` avec les variables suivantes :
|
||||
```env
|
||||
DATABASE_URL="mysql://sf:sfrgdHYJi56631lyshFSQGfd45452ùwdf54f8fg5dfhg5_tyfdgthIOPHFUGH@127.0.0.1:3306/osm-my-commerce?serverVersion=8.0.32&charset=utf8mb4"
|
||||
# ou pour PostgreSQL :
|
||||
# DATABASE_URL="postgresql://sf:sfrgdHYJi56631lyshFSQGfd45452ùwdf54f8fg5dfhg5_tyfdgthIOPHFUGH@127.0.0.1:5432/osm-my-commerce?serverVersion=15&charset=utf8"
|
||||
|
||||
# Token OSM pour les modifications
|
||||
OSM_TOKEN="votre_token_osm_ici"
|
||||
```
|
||||
|
||||
## 3. Migrations de base de données
|
||||
|
||||
### Créer une nouvelle migration
|
||||
```shell
|
||||
php bin/console make:migration
|
||||
```
|
||||
|
||||
### Exécuter les migrations
|
||||
```shell
|
||||
php bin/console doctrine:migrations:migrate
|
||||
```
|
||||
|
||||
### Voir le statut des migrations
|
||||
```shell
|
||||
php bin/console doctrine:migrations:status
|
||||
```
|
||||
|
||||
### Annuler la dernière migration
|
||||
```shell
|
||||
php bin/console doctrine:migrations:migrate prev
|
||||
```
|
||||
|
||||
# Commandes custom Symfony
|
||||
|
||||
## Commandes de gestion des données
|
||||
|
||||
### Mise à jour des coordonnées des villes
|
||||
Récupère et stocke les coordonnées lat/lon pour toutes les villes dans la base de données :
|
||||
```shell
|
||||
php bin/console app:update-city-coordinates
|
||||
```
|
||||
|
||||
### Mise à jour des Stats avec kind vide
|
||||
Change les Stats qui ont un kind vide (NULL) pour leur mettre "user" en kind et les enregistre :
|
||||
```shell
|
||||
php bin/console app:update-empty-stats-kind
|
||||
```
|
||||
|
||||
### Test du budget
|
||||
Teste le calcul du budget pour une ville donnée :
|
||||
```shell
|
||||
php bin/console app:test-budget [insee_code]
|
||||
```
|
||||
|
||||
### Labourage d'une ville
|
||||
Ajoute une nouvelle ville à la base de données avec son code INSEE :
|
||||
```shell
|
||||
php bin/console app:labourage [insee_code]
|
||||
```
|
||||
|
||||
### Création des Stats manquantes à partir du CSV
|
||||
Examine le fichier CSV des communes et crée des objets Stats pour les communes qui n'en ont pas encore :
|
||||
```shell
|
||||
php bin/console app:create-missing-stats-from-csv [options]
|
||||
```
|
||||
|
||||
Options disponibles :
|
||||
- `--limit=N` ou `-l N` : Limite le nombre de communes à traiter
|
||||
- `--dry-run` : Simule sans modifier la base de données
|
||||
|
||||
Cette commande utilise le fichier `communes_france.csv` à la racine du projet et crée des objets Stats pour les communes qui n'en ont pas encore. Les objets sont créés avec les informations du CSV et complétés avec des données supplémentaires (coordonnées, budget, etc.). Les objets sont sauvegardés par paquets de 100 pour optimiser les performances.
|
||||
|
||||
## Génération de statistiques pour toute la France
|
||||
|
||||
Le projet inclut un ensemble de commandes Symfony qui permettent de générer des statistiques de complétion pour toutes les communes de France. Ces commandes doivent être exécutées dans l'ordre suivant :
|
||||
|
||||
### 1. Récupération des polygones des villes
|
||||
Récupère les polygones des villes selon leur zone donnée par le code INSEE :
|
||||
```shell
|
||||
php bin/console app:retrieve-city-polygons [insee-code] [options]
|
||||
```
|
||||
|
||||
Arguments :
|
||||
- `insee-code` : (Optionnel) Code INSEE spécifique à traiter
|
||||
|
||||
Options :
|
||||
- `--limit=N` ou `-l N` : Limite le nombre de villes à traiter
|
||||
- `--force` ou `-f` : Force la récupération même si le polygone existe déjà
|
||||
|
||||
Cette commande :
|
||||
- Crée le dossier `counting_osm_objects/polygons` s'il n'existe pas
|
||||
- Utilise le script Python `get_poly.py` pour récupérer les polygones des communes
|
||||
- Affiche une barre de progression et un résumé des résultats
|
||||
|
||||
### 2. Extraction des données OSM pour chaque zone INSEE
|
||||
Extrait les données OSM pour chaque zone INSEE à partir du fichier france-latest.osm.pbf :
|
||||
```shell
|
||||
php bin/console app:extract-insee-zones [insee-code] [options]
|
||||
```
|
||||
|
||||
Arguments :
|
||||
- `insee-code` : (Optionnel) Code INSEE spécifique à traiter
|
||||
|
||||
Options :
|
||||
- `--limit=N` ou `-l N` : Limite le nombre de villes à traiter
|
||||
- `--force` ou `-f` : Force l'extraction même si le fichier JSON existe déjà
|
||||
- `--keep-pbf` ou `-k` : Conserve les fichiers PBF intermédiaires
|
||||
|
||||
Cette commande :
|
||||
- Télécharge automatiquement le fichier france-latest.osm.pbf depuis Geofabrik s'il n'existe pas
|
||||
- Crée le dossier `insee_extracts` s'il n'existe pas
|
||||
- Utilise osmium pour extraire les données OSM pour chaque zone INSEE
|
||||
- Convertit les données extraites en format JSON
|
||||
- Affiche une barre de progression et un résumé des résultats
|
||||
|
||||
### 3. Traitement des extraits JSON pour calculer les mesures de thèmes
|
||||
Traite les extraits JSON des zones INSEE pour calculer les mesures de thèmes :
|
||||
```shell
|
||||
php bin/console app:process-insee-extracts [insee-code] [options]
|
||||
```
|
||||
|
||||
Arguments :
|
||||
- `insee-code` : (Optionnel) Code INSEE spécifique à traiter
|
||||
|
||||
Options :
|
||||
- `--limit=N` ou `-l N` : Limite le nombre de villes à traiter
|
||||
- `--force` ou `-f` : Force le traitement même si déjà effectué
|
||||
|
||||
Cette commande :
|
||||
- Utilise le service Motocultrice pour traiter les données
|
||||
- Met à jour la date de labourage dans l'entité Stats
|
||||
- Affiche une barre de progression et un résumé des résultats
|
||||
|
||||
### Exemple d'utilisation pour générer des statistiques pour toute la France
|
||||
|
||||
```shell
|
||||
# 1. Récupérer les polygones de toutes les communes
|
||||
php bin/console app:retrieve-city-polygons
|
||||
|
||||
# 2. Extraire les données OSM pour chaque zone INSEE
|
||||
php bin/console app:extract-insee-zones
|
||||
|
||||
# 3. Traiter les extraits JSON pour calculer les mesures de thèmes
|
||||
php bin/console app:process-insee-extracts
|
||||
```
|
||||
|
||||
Pour traiter une seule commune (par exemple avec le code INSEE 75056 pour Paris) :
|
||||
```shell
|
||||
php bin/console app:retrieve-city-polygons 75056
|
||||
php bin/console app:extract-insee-zones 75056
|
||||
php bin/console app:process-insee-extracts 75056
|
||||
```
|
||||
|
||||
### Dépendances
|
||||
|
||||
Pour exécuter ces commandes, vous aurez besoin de :
|
||||
- Python 3 avec les bibliothèques requises pour `get_poly.py`
|
||||
- Osmium Tool (`osmium`) installé sur votre système
|
||||
- Suffisamment d'espace disque pour stocker le fichier france-latest.osm.pbf (~4 Go) et les extraits JSON
|
||||
|
||||
# Routes d'administration
|
||||
|
||||
## Création des Stats manquantes à partir du CSV
|
||||
Examine le fichier CSV des communes et crée des objets Stats pour les communes manquantes :
|
||||
```
|
||||
/admin/create-missing-stats-from-csv
|
||||
```
|
||||
Cette route lit le fichier `communes_france.csv` à la racine du projet et crée des objets Stats pour les communes qui n'en ont pas encore. Les objets sont créés avec les informations du CSV uniquement (sans labourage) et sont sauvegardés par paquets de 100.
|
||||
|
||||
Pour plus de détails, consultez la [documentation dédiée](docs/create_missing_stats.md).
|
||||
|
||||
## Commandes de maintenance
|
||||
|
||||
### Nettoyage du cache
|
||||
```shell
|
||||
php bin/console cache:clear
|
||||
```
|
||||
|
||||
### Validation du schéma de base de données
|
||||
```shell
|
||||
php bin/console doctrine:schema:validate
|
||||
```
|
||||
|
||||
### Mise à jour du schéma de base de données
|
||||
```shell
|
||||
php bin/console doctrine:schema:update --force
|
||||
```
|
||||
|
||||
# Fonctionnalités principales
|
||||
|
||||
## Interface publique
|
||||
- **Page d'accueil** : Carte interactive des villes avec taux de complétion
|
||||
- **Ajouter ma ville** : Formulaire pour ajouter une nouvelle ville
|
||||
- **Statistiques par ville** : Graphiques détaillés de progression
|
||||
- **Graphiques thématiques** : Suivi spécifique par type d'objet (ex: arrêts de bus, bornes de recharge, etc.)
|
||||
|
||||
## Interface d'administration
|
||||
- **Tableau de bord** : Vue d'ensemble des statistiques
|
||||
- **Gestion des villes** : Ajout, modification, suppression
|
||||
- **Suivi des modifications** : Historique des changements
|
||||
- **Export de données** : CSV, JSON, API Overpass
|
||||
|
||||
## API et intégrations
|
||||
- **API Overpass** : Récupération de données OSM
|
||||
- **Nominatim** : Géocodage des adresses
|
||||
- **Addok** : Service de géocodage alternatif
|
||||
|
||||
# Structure du projet
|
||||
|
||||
```
|
||||
src/
|
||||
├── Command/ # Commandes custom Symfony
|
||||
├── Controller/ # Contrôleurs (public et admin)
|
||||
├── Entity/ # Entités Doctrine
|
||||
├── Repository/ # Repositories Doctrine
|
||||
├── Service/ # Services métier
|
||||
└── Kernel.php # Configuration du kernel
|
||||
|
||||
templates/
|
||||
├── admin/ # Templates d'administration
|
||||
├── public/ # Templates publics
|
||||
└── base.html.twig # Template de base
|
||||
|
||||
assets/ # Assets frontend (JS, CSS)
|
||||
config/ # Configuration Symfony
|
||||
migrations/ # Migrations de base de données
|
||||
```
|
||||
|
||||
# Développement
|
||||
|
||||
## Ajouter une nouvelle commande
|
||||
```shell
|
||||
php bin/console make:command NomCommande
|
||||
```
|
||||
|
||||
## Ajouter une nouvelle entité
|
||||
```shell
|
||||
php bin/console make:entity NomEntite
|
||||
```
|
||||
|
||||
## Ajouter un nouveau contrôleur
|
||||
```shell
|
||||
php bin/console make:controller NomController
|
||||
```
|
||||
|
||||
## Tests
|
||||
```shell
|
||||
php bin/phpunit
|
||||
```
|
||||
|
||||
# Déploiement
|
||||
|
||||
## Variables d'environnement de production
|
||||
- `APP_ENV=prod`
|
||||
- `APP_SECRET=clé_secrète_ici`
|
||||
- `DATABASE_URL=url_de_la_base_production`
|
||||
- `OSM_TOKEN=token_osm_production`
|
||||
|
||||
## Optimisations
|
||||
```shell
|
||||
composer install --no-dev --optimize-autoloader
|
||||
php bin/console cache:clear --env=prod
|
||||
```
|
||||
|
||||
## Labourage différé des villes
|
||||
|
||||
Depuis la version X, le labourage (mise à jour des lieux OSM pour une ville) peut être différé automatiquement si le serveur manque de RAM.
|
||||
|
||||
- Lorsqu'un admin demande un labourage, la date de requête (`date_labourage_requested`) est enregistrée.
|
||||
- Si le serveur dispose d'au moins 1 Go de RAM libre, le labourage est effectué immédiatement (création/mise à jour des objets Place).
|
||||
- Sinon, seul le suivi (CityFollowUp) est mis à jour, et un message informe que la mise à jour des lieux sera différée.
|
||||
- Une commande cron (`php bin/console app:process-labourage-queue`) traite les villes en attente dès que possible, en respectant la RAM disponible.
|
||||
|
||||
### Lancer le cron de labourage
|
||||
|
||||
Ajoutez dans votre crontab :
|
||||
|
||||
```
|
||||
* * * * * cd /chemin/vers/le/projet && php bin/console app:process-labourage-queue >> var/log/labourage_cron.log 2>&1
|
||||
```
|
||||
|
||||
La commande traite la ville la plus ancienne en attente de labourage, si les ressources le permettent.
|
||||
|
||||
### Exemple de cron pour traiter 300 villes par jour
|
||||
|
||||
Pour labourer environ 300 villes par jour, il faut lancer la commande toutes les 5 minutes (24h * 60min / 5 = 288 passages par jour) :
|
||||
|
||||
```
|
||||
*/5 * * * * cd /poule/encrypted/www/osm-commerces && php bin/console app:process-labourage-queue >> var/log/labourage_cron.log 2>&1
|
||||
```
|
||||
|
||||
Chaque exécution traite une ville si les ressources le permettent. En adaptant la fréquence, vous pouvez ajuster le débit de traitement.
|
||||
|
||||
### Propriétés Stats
|
||||
- `date_labourage_requested` : date de la dernière demande de labourage
|
||||
- `date_labourage_done` : date du dernier labourage effectif
|
||||
|
||||
### Remarques
|
||||
- Les CityFollowUp ne sont plus supprimés lors des labourages.
|
||||
- Le système garantit que les villes sont mises à jour dès que possible sans surcharger le serveur.
|
||||
|
||||
# Analyses complémentaires
|
||||
## Analyse de l'historique des objets dans les villes
|
||||
|
||||
dossier counting_osm_objects
|
||||
|
||||
## Inspection de la fraîcheur des traductions de wiki
|
||||
|
||||
Le dossier `wiki_compare` contient des scripts pour analyser les pages wiki d'OpenStreetMap, identifier celles qui ont besoin de mises à jour ou de traductions, et publier des suggestions sur Mastodon.
|
||||
|
||||
### Scripts disponibles
|
||||
|
||||
- **wiki_compare.py** : Récupère les 10 clés OSM les plus utilisées, compare leurs pages wiki en anglais et en français, et identifie celles qui ont besoin de mises à jour.
|
||||
- **post_outdated_page.py** : Sélectionne aléatoirement une page wiki française qui n'est pas à jour et publie un message sur Mastodon pour suggérer sa mise à jour.
|
||||
- **suggest_translation.py** : Identifie les pages wiki anglaises qui n'ont pas de traduction française et publie une suggestion de traduction sur Mastodon.
|
||||
|
||||
### Utilisation
|
||||
|
||||
Consultez le [README du dossier wiki_compare](wiki_compare/README.md) pour plus de détails sur l'installation, la configuration et l'utilisation de ces scripts.
|
231
assets/app.js
231
assets/app.js
|
@ -7,64 +7,16 @@
|
|||
|
||||
// any CSS you import will output into a single css file (app.css in this case)
|
||||
import './styles/app.css';
|
||||
import jQuery from 'jquery';
|
||||
window.$ = jQuery;
|
||||
window.jQuery = jQuery;
|
||||
import 'tablesort/tablesort.css';
|
||||
|
||||
// start the Stimulus application
|
||||
// import './bootstrap';
|
||||
|
||||
import './utils.js';
|
||||
import './opening_hours.js';
|
||||
import './josm.js';
|
||||
import './edit.js';
|
||||
import './table-map-toggles.js';
|
||||
import './stats-charts.js';
|
||||
import './dashboard-charts.js';
|
||||
|
||||
import Chart from 'chart.js/auto';
|
||||
import ChartDataLabels from 'chartjs-plugin-datalabels';
|
||||
import maplibregl from 'maplibre-gl';
|
||||
import {
|
||||
genererCouleurPastel,
|
||||
setupCitySearch,
|
||||
handleAddCityFormSubmit,
|
||||
enableLabourageForm,
|
||||
getLabourerUrl,
|
||||
adjustListGroupFontSize,
|
||||
toggleCompletionInfo,
|
||||
updateMapHeightForLargeScreens
|
||||
} from './utils.js';
|
||||
import tableSortJs from 'table-sort-js/table-sort.js';
|
||||
import 'chartjs-adapter-date-fns';
|
||||
console.log('TableSort', tableSortJs)
|
||||
|
||||
// Charger table-sortable (version non minifiée locale)
|
||||
// import '../assets/js/table-sortable.js';
|
||||
|
||||
window.Chart = Chart;
|
||||
window.genererCouleurPastel = genererCouleurPastel;
|
||||
window.setupCitySearch = setupCitySearch;
|
||||
window.handleAddCityFormSubmit = handleAddCityFormSubmit;
|
||||
window.getLabourerUrl = getLabourerUrl;
|
||||
window.ChartDataLabels = ChartDataLabels;
|
||||
window.maplibregl = maplibregl;
|
||||
window.toggleCompletionInfo = toggleCompletionInfo;
|
||||
window.updateMapHeightForLargeScreens = updateMapHeightForLargeScreens;
|
||||
// window.Tablesort = Tablesort;
|
||||
|
||||
Chart.register(ChartDataLabels);
|
||||
|
||||
// Attendre le chargement du DOM
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
console.log('DOMContentLoaded');
|
||||
|
||||
if(updateMapHeightForLargeScreens){
|
||||
|
||||
|
||||
window.addEventListener('resize', updateMapHeightForLargeScreens);
|
||||
}
|
||||
|
||||
const randombg = genererCouleurPastel();
|
||||
// Appliquer la couleur au body
|
||||
|
@ -122,19 +74,26 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
updateCompletionProgress();
|
||||
|
||||
// Focus sur le premier champ texte au chargement
|
||||
// const firstTextInput = document.querySelector('input.form-control');
|
||||
// if (firstTextInput) {
|
||||
// firstTextInput.focus();
|
||||
// console.log('focus sur le premier champ texte', firstTextInput);
|
||||
// } else {
|
||||
// console.log('pas de champ texte trouvé');
|
||||
// }
|
||||
const firstTextInput = document.querySelector('input.form-control');
|
||||
if (firstTextInput) {
|
||||
firstTextInput.focus();
|
||||
console.log('focus sur le premier champ texte', firstTextInput);
|
||||
} else {
|
||||
console.log('pas de champ texte trouvé');
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
parseCuisine();
|
||||
|
||||
// Tri automatique des tableaux
|
||||
const tables = document.querySelectorAll('table');
|
||||
tables.forEach(table => {
|
||||
table.classList.add('js-sort-table');
|
||||
});
|
||||
|
||||
|
||||
// Modifier la fonction de recherche existante
|
||||
const searchInput = document.getElementById('app_admin_labourer');
|
||||
const suggestionList = document.getElementById('suggestionList');
|
||||
|
@ -195,166 +154,4 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
}, 300);
|
||||
});
|
||||
}
|
||||
|
||||
if(enableLabourageForm){
|
||||
|
||||
enableLabourageForm();
|
||||
}
|
||||
adjustListGroupFontSize('.list-group-item');
|
||||
|
||||
// Activer le tri naturel sur tous les tableaux avec la classe table-sort
|
||||
if (tableSortJs) {
|
||||
tableSortJs();
|
||||
}else{
|
||||
console.log('pas de tablesort')
|
||||
}
|
||||
|
||||
// Initialisation du tri et filtrage sur les tableaux du dashboard et de la page stats
|
||||
// if (document.querySelector('#dashboard-table')) {
|
||||
// $('#dashboard-table').tableSortable({
|
||||
// pagination: false,
|
||||
// showPaginationLabel: true,
|
||||
// searchField: '#dashboard-table-search',
|
||||
// responsive: false
|
||||
// });
|
||||
// }
|
||||
// if (document.querySelector('#stats-table')) {
|
||||
// $('#stats-table').tableSortable({
|
||||
// pagination: false,
|
||||
// showPaginationLabel: true,
|
||||
// searchField: '#stats-table-search',
|
||||
// responsive: false
|
||||
// });
|
||||
// }
|
||||
|
||||
// Correction pour le formulaire de labourage
|
||||
const labourerForm = document.getElementById('labourerForm');
|
||||
if (labourerForm) {
|
||||
labourerForm.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
const zipInput = document.getElementById('selectedZipCode');
|
||||
const cityInput = document.getElementById('citySearch');
|
||||
let insee = zipInput.value;
|
||||
if (!insee && cityInput && cityInput.value.trim().length > 0) {
|
||||
// Recherche du code INSEE via l'API
|
||||
const response = await fetch(`https://geo.api.gouv.fr/communes?nom=${encodeURIComponent(cityInput.value.trim())}&fields=nom,code&limit=1`);
|
||||
const data = await response.json();
|
||||
if (data.length > 0) {
|
||||
insee = data[0].code;
|
||||
}
|
||||
}
|
||||
if (insee) {
|
||||
window.location.href = `/add-city-without-labourage/${insee}`;
|
||||
} else {
|
||||
alert('Veuillez sélectionner une ville valide.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Ajouter un écouteur pour l'événement 'load' de MapLibre afin d'ajuster la hauteur de la carte
|
||||
if (window.maplibregl && document.getElementById('map')) {
|
||||
// On suppose que la carte est initialisée ailleurs et accessible via window.mapInstance
|
||||
// Sinon, on peut essayer de détecter l'instance automatiquement
|
||||
let mapInstance = window.mapInstance;
|
||||
if (!mapInstance && window.maplibreMap) {
|
||||
mapInstance = window.maplibreMap;
|
||||
}
|
||||
// Si l'instance n'est pas trouvée, essayer de la récupérer via une variable globale courante
|
||||
if (!mapInstance && window.map) {
|
||||
mapInstance = window.map;
|
||||
}
|
||||
if (mapInstance && typeof mapInstance.on === 'function') {
|
||||
mapInstance.on('load', function() {
|
||||
updateMapHeightForLargeScreens();
|
||||
});
|
||||
}
|
||||
}
|
||||
//updateMapHeightForLargeScreens();
|
||||
|
||||
console.log('window.followupSeries',window.followupSeries)
|
||||
if (!window.followupSeries) return;
|
||||
|
||||
const series = window.followupSeries;
|
||||
|
||||
// Données bornes de recharge
|
||||
const chargingStationCount = (series['charging_station_count'] || []).map(point => ({ x: point.date, y: point.value }));
|
||||
const chargingStationCompletion = (series['charging_station_completion'] || []).map(point => ({ x: point.date, y: point.value }));
|
||||
|
||||
// Données bornes incendie
|
||||
const fireHydrantCount = (series['fire_hydrant_count'] || []).map(point => ({ x: point.date, y: point.value }));
|
||||
const fireHydrantCompletion = (series['fire_hydrant_completion'] || []).map(point => ({ x: point.date, y: point.value }));
|
||||
|
||||
// Graphique bornes de recharge
|
||||
const chargingStationChart = document.getElementById('chargingStationChart');
|
||||
if (chargingStationChart) {
|
||||
new Chart(chargingStationChart, {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
label: 'Nombre de bornes de recharge',
|
||||
data: chargingStationCount,
|
||||
borderColor: 'blue',
|
||||
backgroundColor: 'rgba(0,0,255,0.1)',
|
||||
fill: false,
|
||||
yAxisID: 'y',
|
||||
},
|
||||
{
|
||||
label: 'Complétion (%)',
|
||||
data: chargingStationCompletion,
|
||||
borderColor: 'green',
|
||||
backgroundColor: 'rgba(0,255,0,0.1)',
|
||||
fill: false,
|
||||
yAxisID: 'y1',
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
parsing: false,
|
||||
responsive: true,
|
||||
scales: {
|
||||
x: { type: 'time', time: { unit: 'day' }, title: { display: true, text: 'Date' } },
|
||||
y: { beginAtZero: true, title: { display: true, text: 'Nombre' } },
|
||||
y1: { beginAtZero: true, position: 'right', title: { display: true, text: 'Complétion (%)' }, grid: { drawOnChartArea: false } }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Graphique bornes incendie
|
||||
const fireHydrantChart = document.getElementById('fireHydrantChart');
|
||||
if (fireHydrantChart) {
|
||||
new Chart(fireHydrantChart, {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
label: 'Nombre de bornes incendie',
|
||||
data: fireHydrantCount,
|
||||
borderColor: 'red',
|
||||
backgroundColor: 'rgba(255,0,0,0.1)',
|
||||
fill: false,
|
||||
yAxisID: 'y',
|
||||
},
|
||||
{
|
||||
label: 'Complétion (%)',
|
||||
data: fireHydrantCompletion,
|
||||
borderColor: 'orange',
|
||||
backgroundColor: 'rgba(255,165,0,0.1)',
|
||||
fill: false,
|
||||
yAxisID: 'y1',
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
parsing: false,
|
||||
responsive: true,
|
||||
scales: {
|
||||
x: { type: 'time', time: { unit: 'day' }, title: { display: true, text: 'Date' } },
|
||||
y: { beginAtZero: true, title: { display: true, text: 'Nombre' } },
|
||||
y1: { beginAtZero: true, position: 'right', title: { display: true, text: 'Complétion (%)' }, grid: { drawOnChartArea: false } }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,229 +0,0 @@
|
|||
// Bubble chart du dashboard avec option de taille de bulle proportionnelle ou égale
|
||||
|
||||
let bubbleChart = null; // Déclaré en dehors pour garder la référence
|
||||
function waitForChartAndDrawBubble() {
|
||||
if (!window.Chart || !window.ChartDataLabels) {
|
||||
setTimeout(waitForChartAndDrawBubble, 50);
|
||||
return;
|
||||
}
|
||||
const chartCanvas = document.getElementById('bubbleChart');
|
||||
const toggle = document.getElementById('toggleBubbleSize');
|
||||
|
||||
|
||||
|
||||
|
||||
function drawBubbleChart(proportional) {
|
||||
// Détruire toute instance Chart.js existante sur ce canvas (Chart.js v3+)
|
||||
const existing = window.Chart.getChart(chartCanvas);
|
||||
if (existing) {
|
||||
try { existing.destroy(); } catch (e) { console.warn('Erreur destroy Chart:', e); }
|
||||
}
|
||||
if (bubbleChart) {
|
||||
try { bubbleChart.destroy(); } catch (e) { console.warn('Erreur destroy Chart:', e); }
|
||||
bubbleChart = null;
|
||||
}
|
||||
|
||||
// Forcer le canvas à occuper toute la largeur/hauteur du conteneur en pixels
|
||||
if (chartCanvas && chartCanvas.parentElement) {
|
||||
const parentRect = chartCanvas.parentElement.getBoundingClientRect();
|
||||
console.log('parentRect', parentRect)
|
||||
|
||||
chartCanvas.width = (parentRect.width);
|
||||
chartCanvas.height = (parentRect.height);
|
||||
chartCanvas.style.width = parentRect.width + 'px';
|
||||
chartCanvas.style.height = parentRect.height + 'px';
|
||||
}
|
||||
|
||||
|
||||
if(!getBubbleData){
|
||||
console.log('pas de getBubbleData')
|
||||
return ;
|
||||
}
|
||||
const bubbleChartData = getBubbleData(proportional);
|
||||
|
||||
if(!bubbleChartData){
|
||||
console.log('pas de bubbleChartData')
|
||||
return ;
|
||||
}
|
||||
|
||||
// Calcul de la régression linéaire (moindres carrés)
|
||||
// On ne fait la régression que si on veut, mais l'axe X = fraicheur, Y = complétion
|
||||
const validPoints = bubbleChartData.filter(d => d.x !== null && d.y !== null);
|
||||
const n = validPoints.length;
|
||||
let regressionLine = null, slope = 0, intercept = 0;
|
||||
if (n >= 2) {
|
||||
let sumX = 0, sumY = 0, sumXY = 0, sumXX = 0;
|
||||
validPoints.forEach(d => {
|
||||
sumX += d.x;
|
||||
sumY += d.y;
|
||||
sumXY += d.x * d.y;
|
||||
sumXX += d.x * d.x;
|
||||
});
|
||||
const meanX = sumX / n;
|
||||
const meanY = sumY / n;
|
||||
slope = (sumXY - n * meanX * meanY) / (sumXX - n * meanX * meanX);
|
||||
intercept = meanY - slope * meanX;
|
||||
const xMin = Math.min(...validPoints.map(d => d.x));
|
||||
const xMax = Math.max(...validPoints.map(d => d.x));
|
||||
regressionLine = [
|
||||
{ x: xMin, y: slope * xMin + intercept },
|
||||
{ x: xMax, y: slope * xMax + intercept }
|
||||
];
|
||||
}
|
||||
window.Chart.register(window.ChartDataLabels);
|
||||
bubbleChart = new window.Chart(chartCanvas.getContext('2d'), {
|
||||
type: 'bubble',
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
label: 'Villes',
|
||||
data: bubbleChartData,
|
||||
backgroundColor: bubbleChartData.map(d => `rgba(94, 255, 121, ${d.completion / 100})`),
|
||||
borderColor: 'rgb(94, 255, 121)',
|
||||
datalabels: {
|
||||
anchor: 'center',
|
||||
align: 'center',
|
||||
color: '#000',
|
||||
display: true,
|
||||
font: { weight: '400', size : "12px" },
|
||||
formatter: (value, context) => {
|
||||
return context.dataset.data[context.dataIndex].label;
|
||||
}
|
||||
}
|
||||
},
|
||||
regressionLine ? {
|
||||
label: 'Régression linéaire',
|
||||
type: 'line',
|
||||
data: regressionLine,
|
||||
borderColor: 'rgba(95, 168, 0, 0.7)',
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
fill: false,
|
||||
order: 0,
|
||||
tension: 0,
|
||||
datalabels: { display: false }
|
||||
} : null
|
||||
].filter(Boolean)
|
||||
},
|
||||
options: {
|
||||
plugins: {
|
||||
datalabels: {
|
||||
display: false
|
||||
},
|
||||
legend: { display: true },
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (context) => {
|
||||
const d = context.raw;
|
||||
if (context.dataset.type === 'line') {
|
||||
return `Régression: y = ${slope.toFixed(2)} × x + ${intercept.toFixed(2)}`;
|
||||
}
|
||||
return [
|
||||
`${d.label}`,
|
||||
`Fraîcheur moyenne: ${d.freshnessDays ? d.freshnessDays.toLocaleString() + ' jours' : 'N/A'}`,
|
||||
`Complétion: ${d.y.toFixed(2)}%`,
|
||||
`Population: ${d.population ? d.population.toLocaleString() : 'N/A'}`,
|
||||
`Nombre de lieux: ${d.r.toFixed(2)}`,
|
||||
`Budget: ${d.budget ? d.budget.toLocaleString() + ' €' : 'N/A'}`,
|
||||
`Budget/habitant: ${d.budgetParHabitant ? d.budgetParHabitant.toFixed(2) + ' €' : 'N/A'}`,
|
||||
`Budget/lieu: ${d.budgetParLieu ? d.budgetParLieu.toFixed(2) + ' €' : 'N/A'}`
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: 'linear',
|
||||
title: { display: true, text: 'Fraîcheur moyenne (jours, plus petit = plus récent)' }
|
||||
},
|
||||
y: {
|
||||
title: { display: true, text: 'Taux de complétion (%)' },
|
||||
min: 0,
|
||||
max: 100
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// Ajout du clic sur une bulle
|
||||
chartCanvas.onclick = function(evt) {
|
||||
const points = bubbleChart.getElementsAtEventForMode(evt, 'nearest', { intersect: true }, true);
|
||||
if (points.length > 0) {
|
||||
const firstPoint = points[0];
|
||||
const dataIndex = firstPoint.index;
|
||||
const stat = window.statsDataForBubble[dataIndex];
|
||||
if (stat && stat.zone) {
|
||||
window.location.href = '/admin/stats/' + stat.zone;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Initial draw
|
||||
console.log('[bubble chart] Initialisation avec taille proportionnelle ?', toggle?.checked);
|
||||
if(drawBubbleChart){
|
||||
|
||||
drawBubbleChart(toggle && toggle.checked);
|
||||
// Listener
|
||||
toggle?.addEventListener('change', function() {
|
||||
console.log('[bubble chart] Toggle changé, taille proportionnelle ?', toggle?.checked);
|
||||
drawBubbleChart(toggle?.checked);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getBubbleData(proportional) {
|
||||
// Générer les données puis trier par rayon décroissant
|
||||
const data = window.statsDataForBubble?.map(stat => {
|
||||
const population = parseInt(stat.population, 10);
|
||||
const placesCount = parseInt(stat.placesCount, 10);
|
||||
const completion = parseInt(stat.completionPercent, 10);
|
||||
// Fraîcheur moyenne : âge moyen en jours (plus récent à droite)
|
||||
let freshnessDays = null;
|
||||
if (stat.osmDataDateAvg) {
|
||||
const now = new Date();
|
||||
const avgDate = new Date(stat.osmDataDateAvg);
|
||||
freshnessDays = Math.round((now - avgDate) / (1000 * 60 * 60 * 24));
|
||||
}
|
||||
// Pour l'axe X, on veut que les plus récents soient à droite (donc X = -freshnessDays)
|
||||
const x = freshnessDays !== null ? -freshnessDays : 0;
|
||||
// Budget
|
||||
const budget = stat.budgetAnnuel ? parseFloat(stat.budgetAnnuel) : null;
|
||||
const budgetParHabitant = (budget && population) ? budget / population : null;
|
||||
const budgetParLieu = (budget && placesCount) ? budget / placesCount : null;
|
||||
return {
|
||||
x: x,
|
||||
y: completion,
|
||||
r: proportional ? Math.sqrt(placesCount) * 2 : 12,
|
||||
label: stat.name,
|
||||
completion: stat.completionPercent || 0,
|
||||
zone: stat.zone,
|
||||
budget,
|
||||
budgetParHabitant,
|
||||
budgetParLieu,
|
||||
population,
|
||||
placesCount,
|
||||
freshnessDays
|
||||
};
|
||||
});
|
||||
// Trier du plus gros au plus petit rayon
|
||||
if(data){
|
||||
data.sort((a, b) => b.r - a.r);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
waitForChartAndDrawBubble();
|
||||
|
||||
// Forcer deleteMissing=1 sur le formulaire de labourage
|
||||
const labourerForm = document.getElementById('labourerForm');
|
||||
if (labourerForm) {
|
||||
labourerForm.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const zipCode = document.getElementById('selectedZipCode').value;
|
||||
if (zipCode) {
|
||||
window.location.href = '/admin/labourer/' + zipCode + '?deleteMissing=1';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
|
@ -4,36 +4,13 @@
|
|||
* pour le formulaire de modification
|
||||
*/
|
||||
function updateCompletionProgress() {
|
||||
const inputs = document.querySelectorAll('input[data-important]');
|
||||
const inputs = document.querySelectorAll('input[type="text"]');
|
||||
let filledInputs = 0;
|
||||
let totalInputs = inputs.length;
|
||||
let missingFields = [];
|
||||
|
||||
inputs.forEach(input => {
|
||||
if (input.value.trim() !== '') {
|
||||
filledInputs++;
|
||||
} else {
|
||||
// Get the field label or name for display in the popup
|
||||
let fieldName = '';
|
||||
const label = input.closest('.row')?.querySelector('.form-label, .label-translated');
|
||||
if (label) {
|
||||
fieldName = label.textContent.trim();
|
||||
} else {
|
||||
// If no label found, try to get a meaningful name from the input
|
||||
const name = input.getAttribute('name');
|
||||
if (name) {
|
||||
// Extract field name from the attribute (e.g., commerce_tag_value__contact:email -> Email)
|
||||
const parts = name.split('__');
|
||||
if (parts.length > 1) {
|
||||
fieldName = parts[1].replace('commerce_tag_value_', '').replace('contact:', '');
|
||||
// Capitalize first letter
|
||||
fieldName = fieldName.charAt(0).toUpperCase() + fieldName.slice(1);
|
||||
} else {
|
||||
fieldName = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
missingFields.push(fieldName || input.getAttribute('name') || 'Champ inconnu');
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -42,64 +19,7 @@ function updateCompletionProgress() {
|
|||
if (progressBar) {
|
||||
progressBar.style.width = completionPercentage + '%';
|
||||
progressBar.setAttribute('aria-valuenow', completionPercentage);
|
||||
|
||||
// Create the completion display with a clickable question mark
|
||||
const displayElement = document.querySelector('#completion_display');
|
||||
|
||||
// Format missing fields as an HTML list for better readability
|
||||
let missingFieldsContent = '';
|
||||
if (missingFields.length > 0) {
|
||||
missingFieldsContent = '<ul class="list-unstyled mb-0">';
|
||||
// Filter out empty or undefined field names and sort them alphabetically
|
||||
missingFields
|
||||
.filter(field => field && field.trim() !== '')
|
||||
.sort()
|
||||
.forEach(field => {
|
||||
missingFieldsContent += `<li><i class="bi bi-exclamation-circle text-warning"></i> ${field}</li>`;
|
||||
});
|
||||
missingFieldsContent += '</ul>';
|
||||
} else {
|
||||
missingFieldsContent = 'Tous les champs importants sont remplis !';
|
||||
}
|
||||
|
||||
displayElement.innerHTML = `Votre commerce est complété à ${Math.round(completionPercentage)}% <a href="#" class="missing-fields-info badge rounded-pill bg-warning text-dark ms-1" style="text-decoration: none; font-weight: bold;" data-bs-toggle="popover" data-bs-placement="bottom" title="Champs manquants" data-bs-html="true" data-bs-content="${missingFieldsContent.replace(/"/g, '"')}">?</a>`;
|
||||
|
||||
// Initialize the Bootstrap popover
|
||||
const popoverTrigger = displayElement.querySelector('.missing-fields-info');
|
||||
if (popoverTrigger) {
|
||||
// Add click handler to focus on the first missing field
|
||||
popoverTrigger.addEventListener('click', function(e) {
|
||||
e.preventDefault(); // Prevent scrolling to top
|
||||
|
||||
// Find the first missing field
|
||||
const missingInput = document.querySelector('input[data-important]:not(.good_filled)');
|
||||
if (missingInput) {
|
||||
// Focus on the first missing field
|
||||
missingInput.focus();
|
||||
// Scroll to the field if needed
|
||||
missingInput.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
});
|
||||
|
||||
// Use setTimeout to ensure this runs after the current execution context
|
||||
setTimeout(() => {
|
||||
if (typeof bootstrap !== 'undefined' && bootstrap.Popover) {
|
||||
// Destroy existing popover if any
|
||||
const existingPopover = bootstrap.Popover.getInstance(popoverTrigger);
|
||||
if (existingPopover) {
|
||||
existingPopover.dispose();
|
||||
}
|
||||
// Initialize new popover
|
||||
new bootstrap.Popover(popoverTrigger, {
|
||||
html: true,
|
||||
trigger: 'click',
|
||||
container: 'body'
|
||||
});
|
||||
} else {
|
||||
console.warn('Bootstrap popover not available');
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
document.querySelector('#completion_display').innerHTML = `Votre commerce est complété à ${Math.round(completionPercentage)}%`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -123,4 +43,4 @@ function parseCuisine() {
|
|||
}
|
||||
|
||||
window.updateCompletionProgress = updateCompletionProgress;
|
||||
window.parseCuisine = parseCuisine;
|
||||
window.parseCuisine = parseCuisine;
|
File diff suppressed because one or more lines are too long
0
assets/js/table-sortable.min.js
vendored
0
assets/js/table-sortable.min.js
vendored
|
@ -1,44 +0,0 @@
|
|||
// Affichage du graphique de fréquence des mises à jour par trimestre sur la page stats d'une ville
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
function drawModificationsByQuarterChart() {
|
||||
if (!window.Chart) {
|
||||
setTimeout(drawModificationsByQuarterChart, 50);
|
||||
return;
|
||||
}
|
||||
if (typeof window.modificationsByQuarter === 'undefined') return;
|
||||
const modifData = window.modificationsByQuarter;
|
||||
const modifLabels = Object.keys(modifData);
|
||||
const modifCounts = Object.values(modifData);
|
||||
const modifCanvas = document.getElementById('modificationsByQuarterChart');
|
||||
if (modifCanvas && modifLabels.length > 0) {
|
||||
new window.Chart(modifCanvas.getContext('2d'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: modifLabels,
|
||||
datasets: [{
|
||||
label: 'Nombre de lieux modifiés',
|
||||
data: modifCounts,
|
||||
backgroundColor: 'rgba(54, 162, 235, 0.7)',
|
||||
borderColor: 'rgba(54, 162, 235, 1)',
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
title: { display: true, text: 'Fréquence des mises à jour par trimestre' }
|
||||
},
|
||||
scales: {
|
||||
y: { beginAtZero: true, title: { display: true, text: 'Nombre de lieux' } },
|
||||
x: { title: { display: true, text: 'Trimestre' } }
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (modifCanvas) {
|
||||
modifCanvas.parentNode.innerHTML = '<div class="alert alert-info">Aucune donnée de modification disponible pour cette ville.</div>';
|
||||
}
|
||||
}
|
||||
drawModificationsByQuarterChart();
|
||||
});
|
|
@ -19,33 +19,18 @@ body {
|
|||
margin-bottom: 8rem;
|
||||
}
|
||||
|
||||
input[data-important] {
|
||||
border-color: #7a8fbb;
|
||||
border-left-width: 5px;
|
||||
.filled {
|
||||
background-color: rgba(0, 255, 0, 0.2) !important;
|
||||
}
|
||||
|
||||
input[data-important]:before {
|
||||
content: ">" !important;
|
||||
}
|
||||
|
||||
.filled, .good_filled {
|
||||
border-color: rgba(0, 255, 0, 0.8) !important;
|
||||
color: #082b0a !important;
|
||||
}
|
||||
|
||||
.filled:hover, .good_filled:hover {
|
||||
background-color: #d9ffd1 !important;
|
||||
.filled:hover {
|
||||
background-color: #8abb7a !important;
|
||||
}
|
||||
|
||||
.no-name {
|
||||
color: #df5a0d;
|
||||
}
|
||||
|
||||
table {
|
||||
max-height: 100vh;
|
||||
max-width: 100vw;
|
||||
}
|
||||
|
||||
table.js-sort-table th {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
@ -155,11 +140,6 @@ img {
|
|||
max-height: 400px;
|
||||
}
|
||||
|
||||
|
||||
#completionHistoryChart {
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.form-label {
|
||||
margin-bottom: 0.5rem;
|
||||
|
@ -173,20 +153,4 @@ img {
|
|||
table tbody {
|
||||
max-height: 700px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#table_container, .table-container, #table-container {
|
||||
max-height: 700px;
|
||||
overflow: auto;
|
||||
display: block;
|
||||
border: solid 3px rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
#citySuggestions {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
body .card:hover {
|
||||
transform: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
// Gestion du tri des tableaux
|
||||
// import Tablesort from 'tablesort';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Gestion du toggle gouttes/ronds sur la carte
|
||||
const toggle = document.getElementById('toggleMarkers');
|
||||
if (toggle && window.updateMarkers) {
|
||||
toggle.addEventListener('change', (e) => {
|
||||
window.updateMarkers(e.target.value);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Exposer une fonction pour (ré)appliquer le tri si besoin
|
||||
export function applyTableSort() {
|
||||
document.querySelectorAll('.js-sort-table').forEach(table => {
|
||||
new window.Tablesort(table);
|
||||
});
|
||||
}
|
397
assets/utils.js
397
assets/utils.js
|
@ -1,4 +1,5 @@
|
|||
function colorHeadingTable() {
|
||||
|
||||
const headers = document.querySelectorAll('th');
|
||||
headers.forEach(header => {
|
||||
const text = header.textContent;
|
||||
|
@ -12,7 +13,9 @@ function colorHeadingTable() {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
function check_validity(e) {
|
||||
|
||||
list_inputs_good_to_fill = [
|
||||
'input[name="commerce_tag_value__contact:email"]',
|
||||
'input[name="commerce_tag_value__contact:phone"]',
|
||||
|
@ -70,16 +73,20 @@ function check_validity(e) {
|
|||
document.querySelector('#validation_messages').classList.add('is-invalid');
|
||||
}
|
||||
}
|
||||
|
||||
export const genererCouleurPastel = () => {
|
||||
// Générer une couleur pastel aléatoire
|
||||
const genererCouleurPastel = () => {
|
||||
// Utiliser des valeurs plus claires (180-255) pour obtenir des tons pastel
|
||||
const r = Math.floor(Math.random() * 75 + 180);
|
||||
const g = Math.floor(Math.random() * 75 + 180);
|
||||
const b = Math.floor(Math.random() * 75 + 180);
|
||||
return `rgb(${r}, ${g}, ${b})`;
|
||||
};
|
||||
|
||||
|
||||
async function searchInseeCode(query) {
|
||||
try {
|
||||
|
||||
// Afficher l'indicateur de chargement
|
||||
document.querySelector('#loading_search_insee').classList.remove('d-none');
|
||||
|
||||
const response = await fetch(`https://geo.api.gouv.fr/communes?nom=${query}&fields=nom,code,codesPostaux&limit=10`);
|
||||
|
@ -97,18 +104,21 @@ async function searchInseeCode(query) {
|
|||
}
|
||||
}
|
||||
|
||||
export function updateMapHeightForLargeScreens() {
|
||||
|
||||
function updateMapHeightForLargeScreens() {
|
||||
|
||||
const mapFound = document.querySelector('#map');
|
||||
const canvasFound = document.querySelector('#map canvas');
|
||||
const newHeight = window.innerHeight * 0.5 + 'px'
|
||||
if (mapFound && window.innerHeight > 800 && window.innerWidth > 800) {
|
||||
mapFound.style.height = newHeight;
|
||||
|
||||
mapFound.style.height = window.innerWidth * 0.8 + 'px';
|
||||
} else {
|
||||
console.log('window.innerHeight', window.innerHeight);
|
||||
}
|
||||
}
|
||||
|
||||
// lister les changesets de l'utilisateur osm-commerces
|
||||
async function listChangesets() {
|
||||
// Ajouter le header Accept pour demander du JSON
|
||||
const options = {
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
|
@ -118,6 +128,7 @@ async function listChangesets() {
|
|||
const data = await changesets.json();
|
||||
console.log(data.changesets.length);
|
||||
|
||||
// Grouper les changesets par période
|
||||
const now = new Date();
|
||||
const last24h = new Date(now - 24 * 60 * 60 * 1000);
|
||||
const last7days = new Date(now - 7 * 24 * 60 * 60 * 1000);
|
||||
|
@ -143,6 +154,7 @@ async function listChangesets() {
|
|||
}
|
||||
});
|
||||
|
||||
// Afficher les statistiques
|
||||
const historyDiv = document.getElementById('userChangesHistory');
|
||||
if (historyDiv) {
|
||||
historyDiv.innerHTML = `
|
||||
|
@ -158,6 +170,8 @@ async function listChangesets() {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function openInPanoramax() {
|
||||
const center = map.getCenter();
|
||||
const zoom = map.getZoom();
|
||||
|
@ -165,140 +179,150 @@ function openInPanoramax() {
|
|||
window.open(panoramaxUrl);
|
||||
}
|
||||
|
||||
export function enableLabourageForm() {
|
||||
|
||||
function enableLabourageForm() {
|
||||
|
||||
// Récupérer les éléments du formulaire
|
||||
const citySearchInput = document.getElementById('citySearch');
|
||||
const citySuggestionsList = document.getElementById('citySuggestions');
|
||||
|
||||
if (citySearchInput && citySuggestionsList) {
|
||||
const form = citySearchInput.closest('form');
|
||||
// Configurer la recherche de ville avec la fonction existante
|
||||
setupCitySearch('citySearch', 'citySuggestions', function (result_search) {
|
||||
if (form) {
|
||||
const labourageBtn = form.querySelector('button[type="submit"]');
|
||||
if (labourageBtn) {
|
||||
// Remplir le champ caché avec le code INSEE
|
||||
const inseeInput = form.querySelector('#selectedZipCode');
|
||||
if (inseeInput) {
|
||||
inseeInput.value = result_search.insee;
|
||||
}
|
||||
// Changer l'action du formulaire pour pointer vers la bonne URL
|
||||
form.action = getLabourerUrl(result_search);
|
||||
|
||||
// Changer le texte du bouton et le désactiver
|
||||
labourageBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Ajout de ' + result_search.name + '...';
|
||||
labourageBtn.disabled = true;
|
||||
|
||||
// Soumettre le formulaire
|
||||
form.submit();
|
||||
}
|
||||
}else{
|
||||
console.warn('pas de form pour labourage trouvé')
|
||||
console.log('code_insee', result_search.insee);
|
||||
// Activer le spinner dans le bouton de labourage
|
||||
const labourageBtn = document.querySelector('.btn-labourer');
|
||||
if (labourageBtn) {
|
||||
labourageBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Chargement...';
|
||||
labourageBtn.disabled = true;
|
||||
}
|
||||
console.log('result_search', result_search, getLabourerUrl(result_search));
|
||||
window.location.href = getLabourerUrl(result_search);
|
||||
});
|
||||
}else{
|
||||
console.warn('pas de labourage citySearchInput citySuggestionsList trouvé', citySearchInput,citySuggestionsList )
|
||||
}
|
||||
}
|
||||
|
||||
export function setupCitySearch(inputId, suggestionListId, onSelect) {
|
||||
// Fonction pour gérer la recherche de villes
|
||||
/**
|
||||
* Configure la recherche de ville avec autocomplétion
|
||||
* @param {string} inputId - ID de l'input de recherche
|
||||
* @param {string} suggestionListId - ID de la liste des suggestions
|
||||
* @param {Function} onSelect - Callback appelé lors de la sélection d'une ville
|
||||
*/
|
||||
function setupCitySearch(inputId, suggestionListId, onSelect) {
|
||||
const searchInput = document.getElementById(inputId);
|
||||
const suggestionList = document.getElementById(suggestionListId);
|
||||
|
||||
window.searchInput = searchInput;
|
||||
window.suggestionList = suggestionList;
|
||||
window.onSelect = onSelect;
|
||||
|
||||
if (!searchInput || !suggestionList) return;
|
||||
|
||||
let timeoutId = null;
|
||||
let searchOngoing = false;
|
||||
|
||||
searchInput.addEventListener('keyup', function () {
|
||||
let searchOngoing = false;
|
||||
searchInput.addEventListener('input', function () {
|
||||
console.log('input', this.value);
|
||||
clearTimeout(timeoutId);
|
||||
const query = this.value.trim();
|
||||
if (query.length < 3) {
|
||||
clearSuggestions();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
if (!searchOngoing) {
|
||||
searchOngoing = true;
|
||||
performSearch(query).then(() => {
|
||||
searchOngoing = false;
|
||||
});
|
||||
performSearch(query);
|
||||
searchOngoing = false;
|
||||
}
|
||||
}, 300);
|
||||
});
|
||||
|
||||
async function performSearch(query) {
|
||||
try {
|
||||
const response = await fetch(`https://geo.api.gouv.fr/communes?nom=${encodeURIComponent(query)}&fields=nom,code,codesPostaux&limit=5`);
|
||||
const data = await response.json();
|
||||
}
|
||||
|
||||
|
||||
function performSearch(query) {
|
||||
console.log('performSearch', query);
|
||||
fetch(`https://geo.api.gouv.fr/communes?nom=${encodeURIComponent(query)}&fields=nom,code,codesPostaux&limit=5`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const citySuggestions = data.map(city => ({
|
||||
name: city.nom,
|
||||
insee: city.code,
|
||||
postcodes: city.codesPostaux,
|
||||
postcode: city.codesPostaux && city.codesPostaux.length > 0 ? city.codesPostaux[0] : '',
|
||||
display_name: `${city.nom} (${city.codesPostaux && city.codesPostaux.length > 0 ? city.codesPostaux[0] : ''})`
|
||||
postcode: city.codesPostaux[0],
|
||||
insee: city.code
|
||||
}));
|
||||
displaySuggestions(citySuggestions);
|
||||
} catch (error) {
|
||||
console.error("Erreur de recherche:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function displaySuggestions(suggestions) {
|
||||
clearSuggestions();
|
||||
suggestions.forEach(suggestion => {
|
||||
const item = document.createElement('div');
|
||||
item.classList.add('suggestion-item');
|
||||
// Nouveau rendu : nom en gras, INSEE et CP en petit/gris
|
||||
item.innerHTML = `
|
||||
<span class="suggestion-name" style="font-weight:bold;">${suggestion.name}</span><br>
|
||||
<span class="suggestion-details" style="font-size:0.95em;color:#666;">
|
||||
<span>INSEE : <b>${suggestion.insee}</b></span>
|
||||
<span style="margin-left:12px;">CP : <b>${Array.isArray(suggestion.postcodes) ? suggestion.postcodes.join(', ') : suggestion.postcode}</b></span>
|
||||
</span>
|
||||
`;
|
||||
item.addEventListener('click', () => {
|
||||
searchInput.value = suggestion.name;
|
||||
clearSuggestions();
|
||||
if (onSelect) {
|
||||
onSelect(suggestion);
|
||||
}
|
||||
});
|
||||
suggestionList.appendChild(item);
|
||||
});
|
||||
suggestionList.style.display = 'block';
|
||||
}
|
||||
|
||||
function clearSuggestions() {
|
||||
suggestionList.innerHTML = '';
|
||||
suggestionList.style.display = 'none';
|
||||
}
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!searchInput.contains(e.target) && !suggestionList.contains(e.target)) {
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Erreur lors de la recherche:', error);
|
||||
clearSuggestions();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function displaySuggestions(suggestions) {
|
||||
console.log('displaySuggestions', suggestions);
|
||||
clearSuggestions();
|
||||
suggestions.forEach(suggestion => {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'list-group-item p-2';
|
||||
li.textContent = `${suggestion.name} (${suggestion.postcode})`;
|
||||
li.addEventListener('click', () => {
|
||||
searchInput.value = suggestion.name;
|
||||
clearSuggestions();
|
||||
if (onSelect) onSelect(suggestion);
|
||||
});
|
||||
window.suggestionList.appendChild(li);
|
||||
});
|
||||
|
||||
window.suggestionList.classList.remove('d-none');
|
||||
console.log('window.suggestionList', window.suggestionList);
|
||||
}
|
||||
|
||||
export function getLabourerUrl(obj) {
|
||||
if (obj && obj.insee) {
|
||||
return `/add-city-without-labourage/${obj.insee}`;
|
||||
function clearSuggestions() {
|
||||
window.suggestionList.innerHTML = '';
|
||||
}
|
||||
|
||||
// Fermer les suggestions en cliquant en dehors
|
||||
document.addEventListener('click', function (e) {
|
||||
if (window.searchInput && !window.searchInput?.contains(e.target) && !window.suggestionList?.contains(e.target)) {
|
||||
clearSuggestions();
|
||||
}
|
||||
return '#';
|
||||
});
|
||||
|
||||
// Fonction pour formater l'URL de labourage
|
||||
/**
|
||||
* Génère l'URL de labourage pour un code postal donné
|
||||
* @param {string} zipCode - Le code postal
|
||||
* @returns {string} L'URL de labourage
|
||||
*/
|
||||
function getLabourerUrl(obj) {
|
||||
|
||||
return `/admin/labourer/${obj.insee}`;
|
||||
}
|
||||
|
||||
export function handleAddCityFormSubmit(event) {
|
||||
event.preventDefault();
|
||||
const zipCode = document.getElementById('selectedZipCode').value;
|
||||
if (zipCode && zipCode.match(/^\d{5}$/)) {
|
||||
window.location.href = `/add-city-without-labourage/${zipCode}`;
|
||||
} else {
|
||||
alert('Veuillez sélectionner une ville valide avec un code postal.');
|
||||
}
|
||||
// Fonction pour gérer la soumission du formulaire d'ajout de ville
|
||||
function handleAddCityFormSubmit(event) {
|
||||
event.preventDefault();
|
||||
const form = event.target;
|
||||
const submitButton = form.querySelector('button[type="submit"]');
|
||||
const zipCodeInput = form.querySelector('input[name="zip_code"]');
|
||||
if (!zipCodeInput.value) {
|
||||
return;
|
||||
}
|
||||
// Afficher le spinner
|
||||
submitButton.disabled = true;
|
||||
const originalContent = submitButton.innerHTML;
|
||||
submitButton.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Labourer...';
|
||||
// Rediriger
|
||||
window.location.href = getLabourerUrl(zipCodeInput.value);
|
||||
}
|
||||
|
||||
export function colorizePercentageCells(selector, color = '154, 205, 50') {
|
||||
/**
|
||||
* Colore les cellules d'un tableau en fonction des pourcentages
|
||||
* @param {string} selector - Le sélecteur CSS pour cibler les cellules à colorer
|
||||
* @param {string} color - La couleur de base en format RGB (ex: '154, 205, 50')
|
||||
*/
|
||||
function colorizePercentageCells(selector, color = '154, 205, 50') {
|
||||
document.querySelectorAll(selector).forEach(cell => {
|
||||
const percentage = parseInt(cell.textContent.replace('%', ''), 10);
|
||||
const percentage = parseInt(cell.textContent);
|
||||
if (!isNaN(percentage)) {
|
||||
const alpha = percentage / 100;
|
||||
cell.style.backgroundColor = `rgba(${color}, ${alpha})`;
|
||||
|
@ -306,79 +330,144 @@ export function colorizePercentageCells(selector, color = '154, 205, 50') {
|
|||
});
|
||||
}
|
||||
|
||||
export function colorizePercentageCellsRelative(selector, color = '154, 205, 50') {
|
||||
let min = Infinity;
|
||||
let max = -Infinity;
|
||||
/**
|
||||
* Colore les cellules d'un tableau avec un gradient relatif à la valeur maximale
|
||||
* @param {string} selector - Le sélecteur CSS pour cibler les cellules à colorer
|
||||
* @param {string} color - La couleur de base en format RGB (ex: '154, 205, 50')
|
||||
*/
|
||||
function colorizePercentageCellsRelative(selector, color = '154, 205, 50') {
|
||||
// Récupérer toutes les cellules
|
||||
const cells = document.querySelectorAll(selector);
|
||||
|
||||
// Trouver la valeur maximale
|
||||
let maxValue = 0;
|
||||
cells.forEach(cell => {
|
||||
const value = parseInt(cell.textContent.replace('%', ''), 10);
|
||||
if (!isNaN(value)) {
|
||||
min = Math.min(min, value);
|
||||
max = Math.max(max, value);
|
||||
const value = parseInt(cell.textContent);
|
||||
if (!isNaN(value) && value > maxValue) {
|
||||
maxValue = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (max > min) {
|
||||
cells.forEach(cell => {
|
||||
const value = parseInt(cell.textContent.replace('%', ''), 10);
|
||||
if (!isNaN(value)) {
|
||||
const ratio = (value - min) / (max - min);
|
||||
cell.style.backgroundColor = `rgba(${color}, ${ratio.toFixed(2)})`;
|
||||
}
|
||||
});
|
||||
}
|
||||
// Appliquer le gradient relatif à la valeur max
|
||||
cells.forEach(cell => {
|
||||
const value = parseInt(cell.textContent);
|
||||
if (!isNaN(value)) {
|
||||
const alpha = value / maxValue; // Ratio relatif au maximum
|
||||
cell.style.backgroundColor = `rgba(${color}, ${alpha})`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function adjustListGroupFontSize(selector, minFont = 0.8, maxFont = 1.2) {
|
||||
const listItems = document.querySelectorAll(selector);
|
||||
if (listItems.length === 0) return;
|
||||
|
||||
/**
|
||||
* Ajuste dynamiquement la taille du texte des éléments list-group-item selon leur nombre
|
||||
* @param {string} selector - Le sélecteur CSS des éléments à ajuster
|
||||
* @param {number} [minFont=0.8] - Taille de police minimale en rem
|
||||
* @param {number} [maxFont=1.2] - Taille de police maximale en rem
|
||||
*/
|
||||
function adjustListGroupFontSize(selector, minFont = 0.8, maxFont = 1.2) {
|
||||
const items = document.querySelectorAll(selector);
|
||||
const count = items.length;
|
||||
let fontSize = maxFont;
|
||||
const count = listItems.length;
|
||||
if (count > 0) {
|
||||
// Plus il y a d'items, plus la taille diminue, mais jamais en dessous de minFont
|
||||
fontSize = Math.max(minFont, maxFont - (count - 5) * 0.05);
|
||||
}
|
||||
listItems.forEach(item => {
|
||||
items.forEach(item => {
|
||||
item.style.fontSize = fontSize + 'rem';
|
||||
});
|
||||
}
|
||||
|
||||
export function calculateCompletion(properties) {
|
||||
let completed = 0;
|
||||
const total = 7; // Nombre de critères
|
||||
function check_validity() {
|
||||
if (!document.getElementById('editLand')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (properties.name) completed++;
|
||||
if (properties['addr:housenumber'] && properties['addr:street']) completed++;
|
||||
if (properties.opening_hours) completed++;
|
||||
if (properties.website || properties['contact:website']) completed++;
|
||||
if (properties.phone || properties['contact:phone']) completed++;
|
||||
if (properties.wheelchair) completed++;
|
||||
|
||||
return {
|
||||
percentage: total > 0 ? (completed / total) * 100 : 0,
|
||||
completed: completed,
|
||||
total: total
|
||||
};
|
||||
}
|
||||
|
||||
export function toggleCompletionInfo() {
|
||||
const content = document.getElementById('completionInfoContent');
|
||||
const icon = document.getElementById('completionInfoIcon');
|
||||
if (content) {
|
||||
const isVisible = content.style.display === 'block';
|
||||
content.style.display = isVisible ? 'none' : 'block';
|
||||
if (icon) {
|
||||
icon.classList.toggle('bi-chevron-down', isVisible);
|
||||
icon.classList.toggle('bi-chevron-up', !isVisible);
|
||||
}
|
||||
const form = document.getElementById('editLand');
|
||||
const fields = {
|
||||
'name': {
|
||||
required: true,
|
||||
message: 'Le nom est requis'
|
||||
},
|
||||
'contact:street': {
|
||||
required: true,
|
||||
message: 'La rue est requise'
|
||||
},
|
||||
'contact:housenumber': {
|
||||
required: true,
|
||||
message: 'Le numéro est requis'
|
||||
},
|
||||
'contact:phone': {
|
||||
pattern: /^(?:(?:\+|00)33|0)\s*[1-9](?:[\s.-]*\d{2}){4}$/,
|
||||
message: 'Le numéro de téléphone n\'est pas valide'
|
||||
},
|
||||
'contact:email': {
|
||||
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
||||
message: 'L\'adresse email n\'est pas valide'
|
||||
},
|
||||
'contact:website': {
|
||||
pattern: /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/,
|
||||
message: 'L\'URL du site web n\'est pas valide'
|
||||
}
|
||||
};
|
||||
|
||||
let isValid = true;
|
||||
const errorMessages = {};
|
||||
|
||||
// Supprimer les messages d'erreur précédents
|
||||
document.querySelectorAll('.error-message').forEach(el => el.remove());
|
||||
|
||||
// Vérifier chaque champ
|
||||
for (const [fieldName, rules] of Object.entries(fields)) {
|
||||
const input = form.querySelector(`[name="${fieldName}"]`);
|
||||
if (!input) continue;
|
||||
|
||||
const value = input.value.trim();
|
||||
let fieldError = null;
|
||||
|
||||
// Ne valider que si le champ n'est pas vide
|
||||
if (value) {
|
||||
if (rules.pattern && !rules.pattern.test(value)) {
|
||||
fieldError = rules.message;
|
||||
}
|
||||
} else if (rules.required) {
|
||||
// Si le champ est vide et requis
|
||||
fieldError = rules.message;
|
||||
}
|
||||
|
||||
if (fieldError) {
|
||||
isValid = false;
|
||||
errorMessages[fieldName] = fieldError;
|
||||
|
||||
// Créer et afficher le message d'erreur
|
||||
const errorDiv = document.createElement('div');
|
||||
errorDiv.className = 'error-message text-danger small mt-1';
|
||||
errorDiv.textContent = fieldError;
|
||||
input.parentNode.appendChild(errorDiv);
|
||||
|
||||
// Ajouter une classe d'erreur au champ
|
||||
input.classList.add('is-invalid');
|
||||
} else {
|
||||
input.classList.remove('is-invalid');
|
||||
}
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
|
||||
// Exporter les fonctions dans window
|
||||
window.setupCitySearch = setupCitySearch;
|
||||
window.getLabourerUrl = getLabourerUrl;
|
||||
window.handleAddCityFormSubmit = handleAddCityFormSubmit;
|
||||
window.colorizePercentageCells = colorizePercentageCells;
|
||||
window.colorizePercentageCellsRelative = colorizePercentageCellsRelative;
|
||||
window.adjustListGroupFontSize = adjustListGroupFontSize;
|
||||
window.check_validity = check_validity;
|
||||
window.colorHeadingTable = colorHeadingTable;
|
||||
window.enableLabourageForm = enableLabourageForm;
|
||||
window.performSearch = performSearch;
|
||||
window.openInPanoramax = openInPanoramax;
|
||||
window.listChangesets = listChangesets;
|
||||
window.adjustListGroupFontSize = adjustListGroupFontSize;
|
||||
window.calculateCompletion = calculateCompletion;
|
||||
window.toggleCompletionInfo = toggleCompletionInfo;
|
||||
window.updateMapHeightForLargeScreens = updateMapHeightForLargeScreens;
|
||||
window.searchInseeCode = searchInseeCode;
|
||||
window.genererCouleurPastel = genererCouleurPastel;
|
||||
window.check_validity = check_validity;
|
|
@ -12,11 +12,6 @@ if (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) {
|
|||
throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".');
|
||||
}
|
||||
|
||||
// Optimisations pour éviter les timeouts
|
||||
ini_set('max_execution_time', 300);
|
||||
ini_set('memory_limit', '512M');
|
||||
set_time_limit(300);
|
||||
|
||||
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
|
||||
|
||||
return function (array $context) {
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
-- Script SQL pour désactiver temporairement la contrainte d'unicité sur l'INSEE de ville,
|
||||
-- supprimer les doublons, et réactiver la contrainte
|
||||
|
||||
-- ATTENTION: Ce script supprime des enregistrements de la table stats.
|
||||
-- Les entités liées (CityFollowUp, Place) seront également supprimées si des contraintes
|
||||
-- de clé étrangère avec ON DELETE CASCADE sont définies dans la base de données.
|
||||
-- Assurez-vous de faire une sauvegarde de la base de données avant d'exécuter ce script.
|
||||
|
||||
-- 1. Désactiver temporairement la contrainte d'unicité
|
||||
ALTER TABLE stats DROP CONSTRAINT uniq_stats_zone;
|
||||
|
||||
-- 2. Identifier et supprimer les doublons, en gardant l'entrée la plus ancienne
|
||||
-- Créer une table temporaire pour stocker les IDs à supprimer
|
||||
CREATE TEMP TABLE stats_to_delete AS
|
||||
WITH duplicates AS (
|
||||
SELECT
|
||||
id,
|
||||
zone,
|
||||
date_created,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY zone
|
||||
ORDER BY
|
||||
-- Garder l'entrée la plus ancienne si date_created existe
|
||||
CASE WHEN date_created IS NOT NULL THEN 0 ELSE 1 END,
|
||||
date_created,
|
||||
-- Si date_created est NULL, utiliser l'ID le plus petit (probablement le plus ancien)
|
||||
id
|
||||
) AS row_num
|
||||
FROM stats
|
||||
WHERE zone IS NOT NULL
|
||||
)
|
||||
SELECT id FROM duplicates WHERE row_num > 1;
|
||||
|
||||
-- Afficher le nombre de doublons qui seront supprimés
|
||||
SELECT COUNT(*) AS "Nombre de doublons à supprimer" FROM stats_to_delete;
|
||||
|
||||
-- Afficher les détails des doublons qui seront supprimés (pour vérification)
|
||||
SELECT s.id, s.zone, s.name, s.date_created
|
||||
FROM stats s
|
||||
JOIN stats_to_delete std ON s.id = std.id
|
||||
ORDER BY s.zone, s.id;
|
||||
|
||||
-- 3. Supprimer les doublons
|
||||
-- Note: Nous utilisons DELETE ... USING pour éviter les problèmes de contraintes de clé étrangère
|
||||
DELETE FROM stats
|
||||
USING stats_to_delete
|
||||
WHERE stats.id = stats_to_delete.id;
|
||||
|
||||
-- 4. Nettoyer la table temporaire
|
||||
DROP TABLE stats_to_delete;
|
||||
|
||||
-- 5. Réactiver la contrainte d'unicité
|
||||
ALTER TABLE stats ADD CONSTRAINT uniq_stats_zone UNIQUE (zone);
|
||||
|
||||
-- 6. Vérifier qu'il n'y a plus de doublons
|
||||
SELECT zone, COUNT(*)
|
||||
FROM stats
|
||||
WHERE zone IS NOT NULL
|
||||
GROUP BY zone
|
||||
HAVING COUNT(*) > 1;
|
|
@ -1,62 +0,0 @@
|
|||
# Script de nettoyage des doublons de villes
|
||||
|
||||
Ce dossier contient un script SQL pour résoudre le problème de contrainte d'unicité sur le code INSEE des villes dans la table `stats`.
|
||||
|
||||
## Problème
|
||||
|
||||
Lorsque vous rencontrez l'erreur suivante :
|
||||
|
||||
```
|
||||
SQLSTATE[23505]: Unique violation: 7 ERREUR: n'a pas pu créer l'index unique « uniq_stats_zone »
|
||||
DETAIL: La clé (zone)=(91111) est dupliquée.
|
||||
```
|
||||
|
||||
Cela signifie qu'il existe des doublons dans la table `stats` avec le même code INSEE (colonne `zone`), ce qui empêche la création de la contrainte d'unicité.
|
||||
|
||||
## Solution
|
||||
|
||||
Le script `clean_duplicate_stats.sql` permet de :
|
||||
|
||||
1. Désactiver temporairement la contrainte d'unicité
|
||||
2. Identifier et supprimer les doublons de villes, en gardant l'entrée la plus ancienne
|
||||
3. Réactiver la contrainte d'unicité
|
||||
|
||||
## Comment utiliser le script
|
||||
|
||||
### Précautions importantes
|
||||
|
||||
⚠️ **ATTENTION** : Ce script supprime des données de la base. Assurez-vous de faire une sauvegarde complète de votre base de données avant de l'exécuter.
|
||||
|
||||
Les entités liées aux enregistrements `Stats` supprimés (comme `CityFollowUp` et `Place`) seront également supprimées si des contraintes de clé étrangère avec `ON DELETE CASCADE` sont définies dans la base de données.
|
||||
|
||||
### Exécution du script
|
||||
|
||||
Pour exécuter le script sur une base PostgreSQL :
|
||||
|
||||
```bash
|
||||
psql -U username -d database_name -f clean_duplicate_stats.sql
|
||||
```
|
||||
|
||||
Remplacez `username` par votre nom d'utilisateur PostgreSQL et `database_name` par le nom de votre base de données.
|
||||
|
||||
### Vérification
|
||||
|
||||
Le script inclut des requêtes pour :
|
||||
- Afficher le nombre de doublons qui seront supprimés
|
||||
- Afficher les détails des doublons avant suppression
|
||||
- Vérifier qu'il n'y a plus de doublons après l'opération
|
||||
|
||||
## Fonctionnement technique
|
||||
|
||||
Le script :
|
||||
|
||||
1. Désactive la contrainte d'unicité `uniq_stats_zone`
|
||||
2. Crée une table temporaire pour identifier les doublons
|
||||
3. Utilise `ROW_NUMBER()` pour conserver l'entrée la plus ancienne de chaque groupe de doublons
|
||||
4. Supprime les doublons identifiés
|
||||
5. Réactive la contrainte d'unicité
|
||||
6. Vérifie qu'il n'y a plus de doublons
|
||||
|
||||
## Après l'exécution
|
||||
|
||||
Une fois le script exécuté avec succès, vous ne devriez plus rencontrer l'erreur de violation de contrainte d'unicité lors de l'exécution des commandes Symfony.
|
|
@ -1,3 +0,0 @@
|
|||
rm -rf /poule/encrypted/www/osm-commerces/var/log/dev.log
|
||||
rm -rf /poule/encrypted/www/osm-commerces/var/cache/*
|
||||
rm -rf /var/log/journal/71a53459546c4baeb0d4e7c95504ee2d/*
|
34876
communes_france.csv
34876
communes_france.csv
File diff suppressed because it is too large
Load diff
|
@ -1,14 +1,6 @@
|
|||
doctrine:
|
||||
dbal:
|
||||
url: '%env(resolve:DATABASE_URL)%'
|
||||
options:
|
||||
1002: "SET NAMES utf8mb4"
|
||||
1000: true
|
||||
# Optimisations pour éviter les timeouts
|
||||
PDO::ATTR_TIMEOUT: 300
|
||||
PDO::ATTR_PERSISTENT: false
|
||||
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY: true
|
||||
PDO::MYSQL_ATTR_INIT_COMMAND: "SET SESSION wait_timeout=300, interactive_timeout=300"
|
||||
|
||||
# IMPORTANT: You MUST configure your server version,
|
||||
# either here or in the DATABASE_URL env var (see .env file)
|
||||
|
|
|
@ -5,16 +5,7 @@ framework:
|
|||
http_method_override: false
|
||||
handle_all_throwables: true
|
||||
|
||||
# Optimisations pour éviter les timeouts
|
||||
cache:
|
||||
app: cache.adapter.filesystem
|
||||
system: cache.adapter.system
|
||||
directory: '%kernel.cache_dir%/pools/app'
|
||||
default_redis_provider: 'redis://localhost'
|
||||
default_memcached_provider: 'memcached://localhost'
|
||||
default_doctrine_dbal_provider: database_connection
|
||||
default_pdo_provider: null
|
||||
pools: { }
|
||||
|
||||
|
||||
# Enables session support. Note that the session will ONLY be started if you read or write from it.
|
||||
# Remove or comment this section to explicitly disable session support.
|
||||
|
|
15
counting_osm_objects/.gitignore
vendored
15
counting_osm_objects/.gitignore
vendored
|
@ -1,15 +0,0 @@
|
|||
osm_data/*pbf
|
||||
osm_data/*geojson
|
||||
polygons/*.poly
|
||||
test_data/*
|
||||
test_temp/*
|
||||
test_results/*
|
||||
temp/*
|
||||
resultats/*
|
||||
osm_config.txt
|
||||
__pycache__
|
||||
secrets.sh
|
||||
cookie.txt
|
||||
bin/venv
|
||||
activate
|
||||
city_analysis
|
|
@ -1,237 +0,0 @@
|
|||
# Counting OSM Objects
|
||||
|
||||
Ce répertoire contient des scripts pour compter et analyser les objets OpenStreetMap dans différentes zones
|
||||
administratives françaises.
|
||||
|
||||
Pour fonctionner vous aurez besoin du fichier historisé de la france, pour cela connectez vous à geofabrik avec votre
|
||||
compte osm (oui c'est relou).
|
||||
## TODO
|
||||
|
||||
faire une extraction json de toutes les zones insee de france et réaliser des mesures pour tous les thèmes.
|
||||
|
||||
## Scripts disponibles
|
||||
|
||||
activez le venv python et les dépendances
|
||||
|
||||
```shell
|
||||
python -m venv bin/venv activate
|
||||
source bin/venv/bin/activate
|
||||
pip install plotly pandas
|
||||
```
|
||||
|
||||
```shell
|
||||
# extraire un historique pour une ville à partir de l'historique de france et du polygone de la ville
|
||||
# ici pour paris on peut utiliser l'historique de l'ile de france
|
||||
wget https://osm-commerces.cipherbliss.com/admin/export_csv
|
||||
|
||||
py get_all_polys.py # ce qui utilise l'export csv des villes de osm mon commerce
|
||||
osmium extract -p polygons/commune_75056.poly -H osm_data/ile-de-france-internal.osh.pbf -s complete_ways -O -o commune_75056.osh.pbf
|
||||
```
|
||||
|
||||
```shell
|
||||
# générer les historiques de la commune 76216 pour tous ses thèmes et tous ses graphes
|
||||
py loop_thematics_history_in_zone_to_counts.py --input osm_data/commune_76216.osh.pbf --output-dir test_results --temp-dir test_temp --max-dates 100 --poly polygons/commune_76216.poly
|
||||
```
|
||||
|
||||
```shell
|
||||
# générer un graphe pour un thème dans une ville
|
||||
py generate_graph.py --insee 91111 test_results/commune_75056_building.csv
|
||||
Graphique sauvegardé: test_results/commune_75056_building_monthly_graph.png
|
||||
Graphique de complétion sauvegardé: test_results/commune_75056_building_monthly_graph_completion.png
|
||||
Graphiques générés avec succès!
|
||||
|
||||
```
|
||||
|
||||
### historize_zone.py
|
||||
|
||||
Ce script principal permet de lancer l'analyse historique d'une ville dans OpenStreetMap. Il:
|
||||
|
||||
1. Demande à l'utilisateur quelle ville il souhaite traiter
|
||||
2. Trouve le code INSEE de la ville demandée
|
||||
3. Vérifie si le polygone de la ville existe, sinon le récupère
|
||||
4. Traite les données historiques OSM pour cette ville en utilisant loop_thematics_history_in_zone_to_counts.py
|
||||
|
||||
#### Utilisation
|
||||
|
||||
```bash
|
||||
python historize_zone.py [--input fichier_historique.osh.pbf]
|
||||
```
|
||||
|
||||
Si le fichier d'historique n'est pas spécifié, le script utilisera par défaut le fichier
|
||||
`osm_data/france-internal.osh.pbf`.
|
||||
|
||||
#### Exemples
|
||||
|
||||
Lancer l'analyse avec le fichier d'historique par défaut:
|
||||
|
||||
```bash
|
||||
python historize_zone.py
|
||||
```
|
||||
|
||||
Lancer l'analyse avec un fichier d'historique spécifique:
|
||||
|
||||
```bash
|
||||
python historize_zone.py --input osm_data/ile-de-france-internal.osh.pbf
|
||||
```
|
||||
|
||||
### loop_thematics_history_in_zone_to_counts.py
|
||||
|
||||
Ce script compte les objets OSM par thématique sur une zone donnée à différentes dates. Il:
|
||||
|
||||
1. Filtre les données historiques OSM à différentes dates (mensuelles sur les 10 dernières années)
|
||||
2. Compte les objets correspondant à chaque thématique à chaque date
|
||||
3. Calcule le pourcentage de complétion des attributs importants pour chaque thème
|
||||
4. Sauvegarde les résultats dans des fichiers CSV
|
||||
5. Génère des graphiques montrant l'évolution dans le temps
|
||||
|
||||
#### Utilisation
|
||||
|
||||
```bash
|
||||
python loop_thematics_history_in_zone_to_counts.py --input fichier.osh.pbf --poly polygons/commune_XXXXX.poly
|
||||
```
|
||||
|
||||
Ce script est généralement appelé par historize_zone.py et ne nécessite pas d'être exécuté directement.
|
||||
|
||||
### get_poly.py
|
||||
|
||||
Ce script permet de récupérer le polygone d'une commune française à partir de son code INSEE. Il interroge l'API
|
||||
Overpass Turbo pour obtenir les limites administratives de la commune et sauvegarde le polygone dans un fichier au
|
||||
format .poly (compatible avec Osmosis).
|
||||
|
||||
#### Utilisation
|
||||
|
||||
```bash
|
||||
python get_poly.py [code_insee]
|
||||
```
|
||||
|
||||
Si le code INSEE n'est pas fourni en argument, le script le demandera interactivement.
|
||||
|
||||
#### Exemples
|
||||
|
||||
Récupérer le polygone de la commune d'Étampes (code INSEE 91111) :
|
||||
|
||||
```bash
|
||||
python get_poly.py 91111
|
||||
```
|
||||
|
||||
Le polygone sera sauvegardé dans le fichier `polygons/commune_91111.poly`.
|
||||
|
||||
#### Format de sortie
|
||||
|
||||
Le fichier de sortie est au format .poly, qui est utilisé par Osmosis et d'autres outils OpenStreetMap. Il contient :
|
||||
|
||||
- Le nom de la commune
|
||||
- Un numéro de section
|
||||
- Les coordonnées des points du polygone (longitude, latitude)
|
||||
- Des marqueurs "END" pour fermer le polygone et le fichier
|
||||
|
||||
Exemple de contenu :
|
||||
|
||||
```
|
||||
commune_91111
|
||||
1
|
||||
2.1326337 48.6556426
|
||||
2.1323684 48.6554398
|
||||
...
|
||||
2.1326337 48.6556426
|
||||
END
|
||||
END
|
||||
```
|
||||
|
||||
## Dépendances
|
||||
|
||||
- Python 3.6+
|
||||
- Modules Python : argparse, urllib, json
|
||||
- Pour compare_osm_objects.sh : PostgreSQL, curl, jq
|
||||
|
||||
## Installation
|
||||
|
||||
Aucune installation spécifique n'est nécessaire pour ces scripts. Assurez-vous simplement que les dépendances sont
|
||||
installées.
|
||||
|
||||
### analyze_city_polygons.py
|
||||
|
||||
Ce script analyse les polygones de villes et génère des fichiers JSON d'analyse. Il:
|
||||
|
||||
1. Parcourt tous les fichiers de polygones dans le dossier "polygons"
|
||||
2. Pour chaque polygone, vérifie si un fichier d'analyse JSON existe déjà
|
||||
3. Si non, utilise loop_thematics_history_in_zone_to_counts.py pour extraire les données
|
||||
4. Sauvegarde les résultats dans un fichier JSON avec une analyse de complétion
|
||||
5. Ajoute une date de création à chaque analyse
|
||||
|
||||
#### Utilisation
|
||||
|
||||
```bash
|
||||
python analyze_city_polygons.py [--force] [--single CODE_INSEE]
|
||||
```
|
||||
|
||||
Options:
|
||||
- `--force` ou `-f`: Force la recréation des analyses existantes
|
||||
- `--single` ou `-s`: Traite uniquement le polygone spécifié (code INSEE)
|
||||
|
||||
#### Exemples
|
||||
|
||||
Analyser tous les polygones disponibles:
|
||||
|
||||
```bash
|
||||
python analyze_city_polygons.py
|
||||
```
|
||||
|
||||
Analyser uniquement la commune avec le code INSEE 59140:
|
||||
|
||||
```bash
|
||||
python analyze_city_polygons.py --single 59140
|
||||
```
|
||||
|
||||
Forcer la recréation des analyses existantes:
|
||||
|
||||
```bash
|
||||
python analyze_city_polygons.py --force
|
||||
```
|
||||
|
||||
#### Format de sortie
|
||||
|
||||
Les fichiers d'analyse sont au format JSON et sont sauvegardés dans le dossier `city_analysis`. Chaque fichier contient:
|
||||
|
||||
- Une section `themes` avec des données pour chaque thématique (nombre d'objets, pourcentage de complétion, etc.)
|
||||
- Une section `metadata` avec des informations sur l'analyse (code INSEE, date de création, statistiques globales)
|
||||
|
||||
Exemple de contenu:
|
||||
|
||||
```json
|
||||
{
|
||||
"themes": {
|
||||
"borne-de-recharge": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_59140",
|
||||
"theme": "borne-de-recharge",
|
||||
"nombre_total": 0,
|
||||
"nombre_avec_operator": 0,
|
||||
"nombre_avec_capacity": 0,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"borne-incendie": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_59140",
|
||||
"theme": "borne-incendie",
|
||||
"nombre_total": 0,
|
||||
"nombre_avec_ref": 0,
|
||||
"nombre_avec_colour": 0,
|
||||
"pourcentage_completion": 0
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"insee_code": "59140",
|
||||
"creation_date": "2025-08-21 11:12:20",
|
||||
"polygon_file": "commune_59140.poly",
|
||||
"osm_data_file": "france-latest.osm.pbf",
|
||||
"total_objects": 679,
|
||||
"average_completion": 0.44,
|
||||
"theme_count": 7
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Licence
|
||||
|
||||
Ces scripts sont distribués sous la même licence que le projet Osmose-Backend.
|
|
@ -1,295 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Script pour analyser les polygones de villes et générer des fichiers JSON d'analyse.
|
||||
|
||||
Ce script:
|
||||
1. Parcourt tous les fichiers de polygones dans le dossier "polygons"
|
||||
2. Pour chaque polygone, vérifie si un fichier d'analyse JSON existe déjà
|
||||
3. Si non, utilise loop_thematics_history_in_zone_to_counts.py pour extraire les données
|
||||
4. Sauvegarde les résultats dans un fichier JSON avec une analyse de complétion
|
||||
5. Ajoute une date de création à chaque analyse
|
||||
|
||||
Usage:
|
||||
python analyze_city_polygons.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import glob
|
||||
import subprocess
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
# Import des thèmes depuis loop_thematics_history_in_zone_to_counts.py
|
||||
from loop_thematics_history_in_zone_to_counts import THEMES
|
||||
|
||||
# Chemins des répertoires
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
POLYGONS_DIR = os.path.join(SCRIPT_DIR, "polygons")
|
||||
OSM_DATA_FILE = os.path.join(SCRIPT_DIR, "osm_data", "france-latest.osm.pbf")
|
||||
ANALYSIS_DIR = os.path.join(SCRIPT_DIR, "city_analysis")
|
||||
TEMP_DIR = os.path.join(SCRIPT_DIR, "temp")
|
||||
OUTPUT_DIR = os.path.join(SCRIPT_DIR, "resultats")
|
||||
|
||||
def ensure_directories_exist():
|
||||
"""
|
||||
Vérifie que les répertoires nécessaires existent, sinon les crée.
|
||||
"""
|
||||
os.makedirs(POLYGONS_DIR, exist_ok=True)
|
||||
os.makedirs(ANALYSIS_DIR, exist_ok=True)
|
||||
os.makedirs(TEMP_DIR, exist_ok=True)
|
||||
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||
print(f"Dossier d'analyses: {ANALYSIS_DIR}")
|
||||
|
||||
def analysis_exists(insee_code):
|
||||
"""
|
||||
Vérifie si l'analyse pour le code INSEE donné existe déjà.
|
||||
|
||||
Args:
|
||||
insee_code (str): Le code INSEE de la commune
|
||||
|
||||
Returns:
|
||||
bool: True si l'analyse existe, False sinon
|
||||
"""
|
||||
analysis_file = os.path.join(ANALYSIS_DIR, f"analyse_commune_{insee_code}.json")
|
||||
return os.path.isfile(analysis_file)
|
||||
|
||||
def run_command(command):
|
||||
"""
|
||||
Exécute une commande shell et retourne la sortie.
|
||||
|
||||
Args:
|
||||
command (str): La commande à exécuter
|
||||
|
||||
Returns:
|
||||
str: La sortie de la commande ou None en cas d'erreur
|
||||
"""
|
||||
print(f"Exécution: {command}")
|
||||
try:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
shell=True,
|
||||
check=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
universal_newlines=True,
|
||||
)
|
||||
return result.stdout
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Erreur lors de l'exécution de la commande: {e}")
|
||||
print(f"Sortie de la commande: {e.stdout}")
|
||||
print(f"Erreur de la commande: {e.stderr}")
|
||||
return None
|
||||
|
||||
def extract_data_for_polygon(poly_file, insee_code):
|
||||
"""
|
||||
Extrait les données pour un polygone donné en utilisant loop_thematics_history_in_zone_to_counts.py.
|
||||
|
||||
Args:
|
||||
poly_file (str): Chemin vers le fichier de polygone
|
||||
insee_code (str): Code INSEE de la commune
|
||||
|
||||
Returns:
|
||||
dict: Dictionnaire contenant les données extraites ou None en cas d'erreur
|
||||
"""
|
||||
try:
|
||||
# Vérifier que le fichier OSM existe
|
||||
if not os.path.isfile(OSM_DATA_FILE):
|
||||
print(f"Erreur: Le fichier OSM {OSM_DATA_FILE} n'existe pas.")
|
||||
return None
|
||||
|
||||
# Exécuter loop_thematics_history_in_zone_to_counts.py pour extraire les données
|
||||
# Nous utilisons --max-dates=1 pour obtenir uniquement les données les plus récentes
|
||||
command = f"python3 {os.path.join(SCRIPT_DIR, 'loop_thematics_history_in_zone_to_counts.py')} --input {OSM_DATA_FILE} --poly {poly_file} --output-dir {OUTPUT_DIR} --temp-dir {TEMP_DIR} --max-dates=1"
|
||||
run_command(command)
|
||||
|
||||
# Récupérer les résultats depuis les fichiers CSV générés
|
||||
zone_name = Path(poly_file).stem
|
||||
data = {"themes": {}}
|
||||
|
||||
for theme_name in THEMES.keys():
|
||||
csv_file = os.path.join(OUTPUT_DIR, f"{zone_name}_{theme_name}.csv")
|
||||
|
||||
if os.path.exists(csv_file):
|
||||
# Lire le fichier CSV et extraire les données les plus récentes
|
||||
with open(csv_file, "r") as f:
|
||||
lines = f.readlines()
|
||||
if len(lines) > 1: # S'assurer qu'il y a des données (en-tête + au moins une ligne)
|
||||
headers = lines[0].strip().split(",")
|
||||
latest_data = lines[-1].strip().split(",")
|
||||
|
||||
# Créer un dictionnaire pour ce thème
|
||||
theme_data = {}
|
||||
for i, header in enumerate(headers):
|
||||
if i < len(latest_data):
|
||||
# Convertir en nombre si possible
|
||||
try:
|
||||
value = latest_data[i]
|
||||
if value and value != "":
|
||||
theme_data[header] = float(value) if "." in value else int(value)
|
||||
else:
|
||||
theme_data[header] = 0
|
||||
except ValueError:
|
||||
theme_data[header] = latest_data[i]
|
||||
|
||||
data["themes"][theme_name] = theme_data
|
||||
|
||||
return data
|
||||
except Exception as e:
|
||||
print(f"Erreur lors de l'extraction des données pour {insee_code}: {e}")
|
||||
return None
|
||||
|
||||
def create_analysis(poly_file):
|
||||
"""
|
||||
Crée une analyse pour un fichier de polygone donné.
|
||||
|
||||
Args:
|
||||
poly_file (str): Chemin vers le fichier de polygone
|
||||
|
||||
Returns:
|
||||
str: Chemin vers le fichier d'analyse créé ou None en cas d'erreur
|
||||
"""
|
||||
try:
|
||||
# Extraire le code INSEE du nom du fichier (format: commune_XXXXX.poly)
|
||||
poly_filename = os.path.basename(poly_file)
|
||||
if poly_filename.startswith("commune_") and poly_filename.endswith(".poly"):
|
||||
insee_code = poly_filename[8:-5] # Enlever "commune_" et ".poly"
|
||||
else:
|
||||
print(f"Format de nom de fichier non reconnu: {poly_filename}")
|
||||
return None
|
||||
|
||||
print(f"Création de l'analyse pour la commune {insee_code}...")
|
||||
|
||||
# Extraire les données pour ce polygone
|
||||
data = extract_data_for_polygon(poly_file, insee_code)
|
||||
if not data:
|
||||
return None
|
||||
|
||||
# Ajouter des métadonnées à l'analyse
|
||||
data["metadata"] = {
|
||||
"insee_code": insee_code,
|
||||
"creation_date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"polygon_file": poly_filename,
|
||||
"osm_data_file": os.path.basename(OSM_DATA_FILE)
|
||||
}
|
||||
|
||||
# Calculer les statistiques de complétion globales
|
||||
total_objects = 0
|
||||
total_completion = 0
|
||||
theme_count = 0
|
||||
|
||||
for theme_name, theme_data in data["themes"].items():
|
||||
if "nombre_total" in theme_data and theme_data["nombre_total"] > 0:
|
||||
total_objects += theme_data["nombre_total"]
|
||||
if "pourcentage_completion" in theme_data:
|
||||
total_completion += theme_data["pourcentage_completion"] * theme_data["nombre_total"]
|
||||
theme_count += 1
|
||||
|
||||
if total_objects > 0:
|
||||
data["metadata"]["total_objects"] = total_objects
|
||||
data["metadata"]["average_completion"] = total_completion / total_objects if total_objects > 0 else 0
|
||||
data["metadata"]["theme_count"] = theme_count
|
||||
|
||||
# Sauvegarder l'analyse dans un fichier JSON
|
||||
analysis_file = os.path.join(ANALYSIS_DIR, f"analyse_commune_{insee_code}.json")
|
||||
with open(analysis_file, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print(f"Analyse pour la commune {insee_code} sauvegardée dans {analysis_file}")
|
||||
return analysis_file
|
||||
except Exception as e:
|
||||
print(f"Erreur lors de la création de l'analyse: {e}")
|
||||
return None
|
||||
|
||||
def main():
|
||||
"""
|
||||
Fonction principale du script.
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Analyse les polygones de villes et génère des fichiers JSON d'analyse."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--force", "-f", action="store_true", help="Force la recréation des analyses existantes"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--single", "-s", help="Traite uniquement le polygone spécifié (code INSEE)"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
# S'assurer que les répertoires nécessaires existent
|
||||
ensure_directories_exist()
|
||||
|
||||
# Récupérer les fichiers de polygones à traiter
|
||||
if args.single:
|
||||
# Traiter uniquement le polygone spécifié
|
||||
single_poly_file = os.path.join(POLYGONS_DIR, f"commune_{args.single}.poly")
|
||||
if not os.path.isfile(single_poly_file):
|
||||
print(f"Erreur: Le fichier de polygone pour la commune {args.single} n'existe pas.")
|
||||
return 1
|
||||
poly_files = [single_poly_file]
|
||||
print(f"Mode test: traitement uniquement du polygone {args.single}")
|
||||
else:
|
||||
# Traiter tous les polygones
|
||||
poly_files = glob.glob(os.path.join(POLYGONS_DIR, "commune_*.poly"))
|
||||
if not poly_files:
|
||||
print("Aucun fichier de polygone trouvé dans le dossier 'polygons'.")
|
||||
return 1
|
||||
|
||||
# Pour le test, limiter à 3 polygones
|
||||
test_mode = False # Mettre à True pour limiter le traitement à quelques polygones
|
||||
if test_mode:
|
||||
# Trier les polygones par taille et prendre les 3 plus petits
|
||||
poly_files = sorted(poly_files, key=os.path.getsize)[:3]
|
||||
print(f"Mode test: traitement limité à {len(poly_files)} polygones")
|
||||
for poly_file in poly_files:
|
||||
print(f" - {os.path.basename(poly_file)}")
|
||||
|
||||
# Compteurs pour les statistiques
|
||||
total = len(poly_files)
|
||||
existing = 0
|
||||
created = 0
|
||||
failed = 0
|
||||
|
||||
# Pour chaque fichier de polygone, créer une analyse si elle n'existe pas déjà
|
||||
for i, poly_file in enumerate(poly_files, 1):
|
||||
# Extraire le code INSEE du nom du fichier
|
||||
poly_filename = os.path.basename(poly_file)
|
||||
insee_code = poly_filename[8:-5] # Enlever "commune_" et ".poly"
|
||||
|
||||
print(f"\nTraitement du polygone {i}/{total}: {poly_filename}")
|
||||
|
||||
if analysis_exists(insee_code) and not args.force:
|
||||
print(f"L'analyse pour la commune {insee_code} existe déjà.")
|
||||
existing += 1
|
||||
continue
|
||||
|
||||
# Créer l'analyse
|
||||
result = create_analysis(poly_file)
|
||||
|
||||
if result:
|
||||
created += 1
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
# Afficher les statistiques
|
||||
print("\nRésumé:")
|
||||
print(f"Total des polygones traités: {total}")
|
||||
print(f"Analyses déjà existantes: {existing}")
|
||||
print(f"Analyses créées avec succès: {created}")
|
||||
print(f"Échecs: {failed}")
|
||||
|
||||
return 0 # Succès
|
||||
except KeyboardInterrupt:
|
||||
print("\nOpération annulée par l'utilisateur.")
|
||||
return 1 # Erreur
|
||||
except Exception as e:
|
||||
print(f"Erreur inattendue: {e}")
|
||||
return 1 # Erreur
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
File diff suppressed because it is too large
Load diff
|
@ -1,247 +0,0 @@
|
|||
{
|
||||
"themes": {
|
||||
"borne-de-recharge": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_59140",
|
||||
"theme": "borne-de-recharge",
|
||||
"nombre_total": 0,
|
||||
"nombre_avec_operator": 0,
|
||||
"nombre_avec_capacity": 0,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"borne-incendie": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_59140",
|
||||
"theme": "borne-incendie",
|
||||
"nombre_total": 0,
|
||||
"nombre_avec_ref": 0,
|
||||
"nombre_avec_colour": 0,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"arbres": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_59140",
|
||||
"theme": "arbres",
|
||||
"nombre_total": 0,
|
||||
"nombre_avec_species": 0,
|
||||
"nombre_avec_leaf_type": 0,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"defibrillator": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_59140",
|
||||
"theme": "defibrillator",
|
||||
"nombre_total": 0,
|
||||
"nombre_avec_operator": 0,
|
||||
"nombre_avec_access": 0,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"toilets": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_59140",
|
||||
"theme": "toilets",
|
||||
"nombre_total": 0,
|
||||
"nombre_avec_access": 0,
|
||||
"nombre_avec_wheelchair": 0,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"bus_stop": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_59140",
|
||||
"theme": "bus_stop",
|
||||
"nombre_total": 3,
|
||||
"nombre_avec_name": 3,
|
||||
"nombre_avec_shelter": 3,
|
||||
"pourcentage_completion": 100.0
|
||||
},
|
||||
"camera": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_59140",
|
||||
"theme": "camera",
|
||||
"nombre_total": 0,
|
||||
"nombre_avec_operator": 0,
|
||||
"nombre_avec_surveillance": 0,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"recycling": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_59140",
|
||||
"theme": "recycling",
|
||||
"nombre_total": 0,
|
||||
"nombre_avec_recycling_type": 0,
|
||||
"nombre_avec_operator": 0,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"substation": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_59140",
|
||||
"theme": "substation",
|
||||
"nombre_total": 2,
|
||||
"nombre_avec_operator": 0,
|
||||
"nombre_avec_voltage": 0,
|
||||
"pourcentage_completion": 0.0
|
||||
},
|
||||
"laboratory": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_59140",
|
||||
"theme": "laboratory",
|
||||
"nombre_total": 0,
|
||||
"nombre_avec_name": 0,
|
||||
"nombre_avec_operator": 0,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"school": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_59140",
|
||||
"theme": "school",
|
||||
"nombre_total": 1,
|
||||
"nombre_avec_name": 0,
|
||||
"nombre_avec_operator": 0,
|
||||
"pourcentage_completion": 0.0
|
||||
},
|
||||
"police": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_59140",
|
||||
"theme": "police",
|
||||
"nombre_total": 0,
|
||||
"nombre_avec_name": 0,
|
||||
"nombre_avec_operator": 0,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"healthcare": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_59140",
|
||||
"theme": "healthcare",
|
||||
"nombre_total": 0,
|
||||
"nombre_avec_name": 0,
|
||||
"nombre_avec_healthcare": 0,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"bicycle_parking": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_59140",
|
||||
"theme": "bicycle_parking",
|
||||
"nombre_total": 0,
|
||||
"nombre_avec_capacity": 0,
|
||||
"nombre_avec_covered": 0,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"advertising_board": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_59140",
|
||||
"theme": "advertising_board",
|
||||
"nombre_total": 0,
|
||||
"nombre_avec_operator": 0,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"building": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_59140",
|
||||
"theme": "building",
|
||||
"nombre_total": 620,
|
||||
"nombre_avec_building": 0,
|
||||
"nombre_avec_addr:housenumber": 0,
|
||||
"pourcentage_completion": 0.0
|
||||
},
|
||||
"email": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_59140",
|
||||
"theme": "email",
|
||||
"nombre_total": 2,
|
||||
"nombre_avec_email": 0,
|
||||
"nombre_avec_contact:email": 0,
|
||||
"pourcentage_completion": 0.0
|
||||
},
|
||||
"bench": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_59140",
|
||||
"theme": "bench",
|
||||
"nombre_total": 0,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"waste_basket": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_59140",
|
||||
"theme": "waste_basket",
|
||||
"nombre_total": 0,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"street_lamp": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_59140",
|
||||
"theme": "street_lamp",
|
||||
"nombre_total": 0,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"drinking_water": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_59140",
|
||||
"theme": "drinking_water",
|
||||
"nombre_total": 0,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"power_pole": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_59140",
|
||||
"theme": "power_pole",
|
||||
"nombre_total": 49,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"manhole": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_59140",
|
||||
"theme": "manhole",
|
||||
"nombre_total": 0,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"little_free_library": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_59140",
|
||||
"theme": "little_free_library",
|
||||
"nombre_total": 0,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"playground": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_59140",
|
||||
"theme": "playground",
|
||||
"nombre_total": 0,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"siret": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_59140",
|
||||
"theme": "siret",
|
||||
"nombre_total": 2,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"restaurants": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_59140",
|
||||
"theme": "restaurants",
|
||||
"nombre_total": 0,
|
||||
"nombre_avec_opening_hours": 0,
|
||||
"nombre_avec_contact:street": 0,
|
||||
"nombre_avec_contact:housenumber": 0,
|
||||
"nombre_avec_website": 0,
|
||||
"nombre_avec_contact:phone": 0,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"rnb": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_59140",
|
||||
"theme": "rnb",
|
||||
"nombre_total": 0,
|
||||
"pourcentage_completion": 0
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"insee_code": "59140",
|
||||
"creation_date": "2025-08-21 11:12:20",
|
||||
"polygon_file": "commune_59140.poly",
|
||||
"osm_data_file": "france-latest.osm.pbf",
|
||||
"total_objects": 679,
|
||||
"average_completion": 0.4418262150220913,
|
||||
"theme_count": 7
|
||||
}
|
||||
}
|
|
@ -1,247 +0,0 @@
|
|||
{
|
||||
"themes": {
|
||||
"borne-de-recharge": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_78123",
|
||||
"theme": "borne-de-recharge",
|
||||
"nombre_total": 0,
|
||||
"nombre_avec_operator": 0,
|
||||
"nombre_avec_capacity": 0,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"borne-incendie": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_78123",
|
||||
"theme": "borne-incendie",
|
||||
"nombre_total": 14,
|
||||
"nombre_avec_ref": 0,
|
||||
"nombre_avec_colour": 0,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"arbres": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_78123",
|
||||
"theme": "arbres",
|
||||
"nombre_total": 78,
|
||||
"nombre_avec_species": 0,
|
||||
"nombre_avec_leaf_type": 2,
|
||||
"pourcentage_completion": 1.28
|
||||
},
|
||||
"defibrillator": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_78123",
|
||||
"theme": "defibrillator",
|
||||
"nombre_total": 0,
|
||||
"nombre_avec_operator": 0,
|
||||
"nombre_avec_access": 0,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"toilets": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_78123",
|
||||
"theme": "toilets",
|
||||
"nombre_total": 2,
|
||||
"nombre_avec_access": 0,
|
||||
"nombre_avec_wheelchair": 1,
|
||||
"pourcentage_completion": 25.0
|
||||
},
|
||||
"bus_stop": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_78123",
|
||||
"theme": "bus_stop",
|
||||
"nombre_total": 57,
|
||||
"nombre_avec_name": 0,
|
||||
"nombre_avec_shelter": 0,
|
||||
"pourcentage_completion": 0.0
|
||||
},
|
||||
"camera": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_78123",
|
||||
"theme": "camera",
|
||||
"nombre_total": 3,
|
||||
"nombre_avec_operator": 0,
|
||||
"nombre_avec_surveillance": 0,
|
||||
"pourcentage_completion": 0.0
|
||||
},
|
||||
"recycling": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_78123",
|
||||
"theme": "recycling",
|
||||
"nombre_total": 199,
|
||||
"nombre_avec_recycling_type": 0,
|
||||
"nombre_avec_operator": 0,
|
||||
"pourcentage_completion": 0.0
|
||||
},
|
||||
"substation": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_78123",
|
||||
"theme": "substation",
|
||||
"nombre_total": 1,
|
||||
"nombre_avec_operator": 0,
|
||||
"nombre_avec_voltage": 0,
|
||||
"pourcentage_completion": 0.0
|
||||
},
|
||||
"laboratory": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_78123",
|
||||
"theme": "laboratory",
|
||||
"nombre_total": 1,
|
||||
"nombre_avec_name": 0,
|
||||
"nombre_avec_operator": 0,
|
||||
"pourcentage_completion": 0.0
|
||||
},
|
||||
"school": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_78123",
|
||||
"theme": "school",
|
||||
"nombre_total": 15,
|
||||
"nombre_avec_name": 0,
|
||||
"nombre_avec_operator": 0,
|
||||
"pourcentage_completion": 0.0
|
||||
},
|
||||
"police": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_78123",
|
||||
"theme": "police",
|
||||
"nombre_total": 1,
|
||||
"nombre_avec_name": 0,
|
||||
"nombre_avec_operator": 0,
|
||||
"pourcentage_completion": 0.0
|
||||
},
|
||||
"healthcare": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_78123",
|
||||
"theme": "healthcare",
|
||||
"nombre_total": 7,
|
||||
"nombre_avec_name": 0,
|
||||
"nombre_avec_healthcare": 0,
|
||||
"pourcentage_completion": 0.0
|
||||
},
|
||||
"bicycle_parking": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_78123",
|
||||
"theme": "bicycle_parking",
|
||||
"nombre_total": 5,
|
||||
"nombre_avec_capacity": 0,
|
||||
"nombre_avec_covered": 0,
|
||||
"pourcentage_completion": 0.0
|
||||
},
|
||||
"advertising_board": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_78123",
|
||||
"theme": "advertising_board",
|
||||
"nombre_total": 0,
|
||||
"nombre_avec_operator": 0,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"building": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_78123",
|
||||
"theme": "building",
|
||||
"nombre_total": 2713,
|
||||
"nombre_avec_building": 0,
|
||||
"nombre_avec_addr:housenumber": 0,
|
||||
"pourcentage_completion": 0.0
|
||||
},
|
||||
"email": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_78123",
|
||||
"theme": "email",
|
||||
"nombre_total": 1,
|
||||
"nombre_avec_email": 0,
|
||||
"nombre_avec_contact:email": 0,
|
||||
"pourcentage_completion": 0.0
|
||||
},
|
||||
"bench": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_78123",
|
||||
"theme": "bench",
|
||||
"nombre_total": 26,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"waste_basket": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_78123",
|
||||
"theme": "waste_basket",
|
||||
"nombre_total": 12,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"street_lamp": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_78123",
|
||||
"theme": "street_lamp",
|
||||
"nombre_total": 1,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"drinking_water": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_78123",
|
||||
"theme": "drinking_water",
|
||||
"nombre_total": 1,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"power_pole": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_78123",
|
||||
"theme": "power_pole",
|
||||
"nombre_total": 0,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"manhole": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_78123",
|
||||
"theme": "manhole",
|
||||
"nombre_total": 0,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"little_free_library": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_78123",
|
||||
"theme": "little_free_library",
|
||||
"nombre_total": 2,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"playground": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_78123",
|
||||
"theme": "playground",
|
||||
"nombre_total": 12,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"siret": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_78123",
|
||||
"theme": "siret",
|
||||
"nombre_total": 6,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"restaurants": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_78123",
|
||||
"theme": "restaurants",
|
||||
"nombre_total": 4,
|
||||
"nombre_avec_opening_hours": 0,
|
||||
"nombre_avec_contact:street": 0,
|
||||
"nombre_avec_contact:housenumber": 0,
|
||||
"nombre_avec_website": 0,
|
||||
"nombre_avec_contact:phone": 0,
|
||||
"pourcentage_completion": 0.0
|
||||
},
|
||||
"rnb": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_78123",
|
||||
"theme": "rnb",
|
||||
"nombre_total": 0,
|
||||
"pourcentage_completion": 0
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"insee_code": "78123",
|
||||
"creation_date": "2025-08-21 11:15:23",
|
||||
"polygon_file": "commune_78123.poly",
|
||||
"osm_data_file": "france-latest.osm.pbf",
|
||||
"total_objects": 3161,
|
||||
"average_completion": 0.04740272065801961,
|
||||
"theme_count": 22
|
||||
}
|
||||
}
|
|
@ -1,247 +0,0 @@
|
|||
{
|
||||
"themes": {
|
||||
"borne-de-recharge": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_94016",
|
||||
"theme": "borne-de-recharge",
|
||||
"nombre_total": 12,
|
||||
"nombre_avec_operator": 0,
|
||||
"nombre_avec_capacity": 0,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"borne-incendie": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_94016",
|
||||
"theme": "borne-incendie",
|
||||
"nombre_total": 46,
|
||||
"nombre_avec_ref": 12,
|
||||
"nombre_avec_colour": 7,
|
||||
"pourcentage_completion": 20.65
|
||||
},
|
||||
"arbres": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_94016",
|
||||
"theme": "arbres",
|
||||
"nombre_total": 2317,
|
||||
"nombre_avec_species": 18,
|
||||
"nombre_avec_leaf_type": 1648,
|
||||
"pourcentage_completion": 35.95
|
||||
},
|
||||
"defibrillator": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_94016",
|
||||
"theme": "defibrillator",
|
||||
"nombre_total": 13,
|
||||
"nombre_avec_operator": 0,
|
||||
"nombre_avec_access": 0,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"toilets": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_94016",
|
||||
"theme": "toilets",
|
||||
"nombre_total": 4,
|
||||
"nombre_avec_access": 4,
|
||||
"nombre_avec_wheelchair": 3,
|
||||
"pourcentage_completion": 87.5
|
||||
},
|
||||
"bus_stop": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_94016",
|
||||
"theme": "bus_stop",
|
||||
"nombre_total": 49,
|
||||
"nombre_avec_name": 0,
|
||||
"nombre_avec_shelter": 0,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"camera": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_94016",
|
||||
"theme": "camera",
|
||||
"nombre_total": 22,
|
||||
"nombre_avec_operator": 6,
|
||||
"nombre_avec_surveillance": 12,
|
||||
"pourcentage_completion": 40.91
|
||||
},
|
||||
"recycling": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_94016",
|
||||
"theme": "recycling",
|
||||
"nombre_total": 33,
|
||||
"nombre_avec_recycling_type": 0,
|
||||
"nombre_avec_operator": 6,
|
||||
"pourcentage_completion": 9.09
|
||||
},
|
||||
"substation": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_94016",
|
||||
"theme": "substation",
|
||||
"nombre_total": 59,
|
||||
"nombre_avec_operator": 6,
|
||||
"nombre_avec_voltage": 0,
|
||||
"pourcentage_completion": 5.08
|
||||
},
|
||||
"laboratory": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_94016",
|
||||
"theme": "laboratory",
|
||||
"nombre_total": 3,
|
||||
"nombre_avec_name": 0,
|
||||
"nombre_avec_operator": 6,
|
||||
"pourcentage_completion": 100.0
|
||||
},
|
||||
"school": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_94016",
|
||||
"theme": "school",
|
||||
"nombre_total": 17,
|
||||
"nombre_avec_name": 0,
|
||||
"nombre_avec_operator": 6,
|
||||
"pourcentage_completion": 17.65
|
||||
},
|
||||
"police": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_94016",
|
||||
"theme": "police",
|
||||
"nombre_total": 2,
|
||||
"nombre_avec_name": 0,
|
||||
"nombre_avec_operator": 6,
|
||||
"pourcentage_completion": 150.0
|
||||
},
|
||||
"healthcare": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_94016",
|
||||
"theme": "healthcare",
|
||||
"nombre_total": 77,
|
||||
"nombre_avec_name": 0,
|
||||
"nombre_avec_healthcare": 0,
|
||||
"pourcentage_completion": 0.0
|
||||
},
|
||||
"bicycle_parking": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_94016",
|
||||
"theme": "bicycle_parking",
|
||||
"nombre_total": 68,
|
||||
"nombre_avec_capacity": 0,
|
||||
"nombre_avec_covered": 0,
|
||||
"pourcentage_completion": 0.0
|
||||
},
|
||||
"advertising_board": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_94016",
|
||||
"theme": "advertising_board",
|
||||
"nombre_total": 37,
|
||||
"nombre_avec_operator": 6,
|
||||
"pourcentage_completion": 16.22
|
||||
},
|
||||
"building": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_94016",
|
||||
"theme": "building",
|
||||
"nombre_total": 4399,
|
||||
"nombre_avec_building": 0,
|
||||
"nombre_avec_addr:housenumber": 0,
|
||||
"pourcentage_completion": 0.0
|
||||
},
|
||||
"email": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_94016",
|
||||
"theme": "email",
|
||||
"nombre_total": 39,
|
||||
"nombre_avec_email": 0,
|
||||
"nombre_avec_contact:email": 0,
|
||||
"pourcentage_completion": 0.0
|
||||
},
|
||||
"bench": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_94016",
|
||||
"theme": "bench",
|
||||
"nombre_total": 439,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"waste_basket": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_94016",
|
||||
"theme": "waste_basket",
|
||||
"nombre_total": 260,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"street_lamp": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_94016",
|
||||
"theme": "street_lamp",
|
||||
"nombre_total": 1622,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"drinking_water": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_94016",
|
||||
"theme": "drinking_water",
|
||||
"nombre_total": 9,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"power_pole": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_94016",
|
||||
"theme": "power_pole",
|
||||
"nombre_total": 0,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"manhole": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_94016",
|
||||
"theme": "manhole",
|
||||
"nombre_total": 21,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"little_free_library": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_94016",
|
||||
"theme": "little_free_library",
|
||||
"nombre_total": 6,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"playground": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_94016",
|
||||
"theme": "playground",
|
||||
"nombre_total": 23,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"siret": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_94016",
|
||||
"theme": "siret",
|
||||
"nombre_total": 260,
|
||||
"pourcentage_completion": 0
|
||||
},
|
||||
"restaurants": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_94016",
|
||||
"theme": "restaurants",
|
||||
"nombre_total": 40,
|
||||
"nombre_avec_opening_hours": 0,
|
||||
"nombre_avec_contact:street": 0,
|
||||
"nombre_avec_contact:housenumber": 0,
|
||||
"nombre_avec_website": 0,
|
||||
"nombre_avec_contact:phone": 0,
|
||||
"pourcentage_completion": 0.0
|
||||
},
|
||||
"rnb": {
|
||||
"date": "2025-08-21",
|
||||
"zone": "commune_94016",
|
||||
"theme": "rnb",
|
||||
"nombre_total": 0,
|
||||
"pourcentage_completion": 0
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"insee_code": "94016",
|
||||
"creation_date": "2025-08-21 11:16:39",
|
||||
"polygon_file": "commune_94016.poly",
|
||||
"osm_data_file": "france-latest.osm.pbf",
|
||||
"total_objects": 9877,
|
||||
"average_completion": 8.868679761061053,
|
||||
"theme_count": 26
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,2 +0,0 @@
|
|||
# on crée une page web présentant un tableau de bord pour la zone donnée
|
||||
# le dashboard contient le nom de la ville et des graphiques pour l'évolution de chaque thématique
|
|
@ -1,448 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Script pour générer un graphique montrant l'évolution du nombre d'objets OSM
|
||||
à partir d'un fichier CSV
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import pandas as pd
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.dates as mdates
|
||||
from datetime import datetime
|
||||
import argparse
|
||||
import csv
|
||||
|
||||
|
||||
def get_city_name(insee_code):
|
||||
"""
|
||||
Récupère le nom de la ville à partir du code INSEE.
|
||||
|
||||
Args:
|
||||
insee_code: Code INSEE de la commune
|
||||
|
||||
Returns:
|
||||
Nom de la ville ou None si non trouvé
|
||||
"""
|
||||
try:
|
||||
csv_path = os.path.join(os.path.dirname(__file__), "osm-commerces-villes-export.csv")
|
||||
if os.path.exists(csv_path):
|
||||
with open(csv_path, 'r') as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
if row.get('zone') == insee_code:
|
||||
return row.get('name')
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"Erreur lors de la récupération du nom de la ville: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def parse_args():
|
||||
"""Parse command line arguments."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Génère un graphique à partir des données CSV d'objets OSM."
|
||||
)
|
||||
parser.add_argument(
|
||||
"csv_file", help="Chemin vers le fichier CSV contenant les données"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output", "-o", help="Chemin de sortie pour le graphique (PNG)", default=None
|
||||
)
|
||||
parser.add_argument(
|
||||
"--insee", "-i", help="Code INSEE de la commune à analyser", default=None
|
||||
)
|
||||
parser.add_argument(
|
||||
"--period",
|
||||
"-p",
|
||||
help="Période à analyser (annual, monthly, daily)",
|
||||
default="monthly",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def load_data(csv_file, insee_code=None, period="monthly"):
|
||||
"""
|
||||
Charge les données depuis le fichier CSV.
|
||||
|
||||
Args:
|
||||
csv_file: Chemin vers le fichier CSV
|
||||
insee_code: Code INSEE de la commune à filtrer (optionnel)
|
||||
period: Période à analyser (annual, monthly, daily)
|
||||
|
||||
Returns:
|
||||
DataFrame pandas contenant les données filtrées
|
||||
"""
|
||||
# Charger le CSV avec gestion des erreurs pour les lignes mal formatées
|
||||
try:
|
||||
df = pd.read_csv(csv_file, error_bad_lines=False, warn_bad_lines=True)
|
||||
except TypeError: # Pour les versions plus récentes de pandas
|
||||
df = pd.read_csv(csv_file, on_bad_lines="skip")
|
||||
|
||||
# Vérifier si le CSV a la structure attendue
|
||||
if "date" in df.columns:
|
||||
# Format de CSV avec colonne 'date' directement
|
||||
try:
|
||||
df["date"] = pd.to_datetime(df["date"])
|
||||
except:
|
||||
# Si la conversion échoue, essayer différents formats
|
||||
try:
|
||||
if df["date"].iloc[0].count("-") == 2: # Format YYYY-MM-DD
|
||||
df["date"] = pd.to_datetime(df["date"], format="%Y-%m-%d")
|
||||
elif df["date"].iloc[0].count("-") == 1: # Format YYYY-MM
|
||||
df["date"] = pd.to_datetime(df["date"], format="%Y-%m")
|
||||
else:
|
||||
df["date"] = pd.to_datetime(df["date"])
|
||||
except:
|
||||
print("Erreur: Impossible de convertir la colonne 'date'.")
|
||||
sys.exit(1)
|
||||
elif "periode" in df.columns:
|
||||
# Ancien format avec colonne 'periode'
|
||||
# Filtrer par période
|
||||
df = df[df["periode"] == period]
|
||||
|
||||
# Filtrer par code INSEE si spécifié
|
||||
if insee_code and "code_insee" in df.columns:
|
||||
df = df[df["code_insee"] == insee_code]
|
||||
|
||||
# Convertir les dates en objets datetime
|
||||
if period == "annual" and "annee" in df.columns:
|
||||
df["date"] = pd.to_datetime(df["annee"].astype(str), format="%Y")
|
||||
elif period == "monthly":
|
||||
# Vérifier si la première colonne contient déjà un format de date mensuel (YYYY-MM)
|
||||
first_col = df.columns[0]
|
||||
first_val = str(df.iloc[0, 0]) if not df.empty else ""
|
||||
|
||||
if first_col == "annee_mois" or (len(first_val) >= 7 and "-" in first_val):
|
||||
# Si la première colonne est 'annee_mois' ou contient une date au format YYYY-MM
|
||||
df["date"] = pd.to_datetime(df.iloc[:, 0].astype(str), format="%Y-%m")
|
||||
elif "annee" in df.columns:
|
||||
# Sinon, utiliser la colonne 'annee' et ajouter un mois fictif (janvier)
|
||||
df["date"] = pd.to_datetime(
|
||||
df["annee"].astype(str) + "-01", format="%Y-%m"
|
||||
)
|
||||
elif period == "daily":
|
||||
# Vérifier si la première colonne contient déjà un format de date quotidien (YYYY-MM-DD)
|
||||
first_col = df.columns[0]
|
||||
first_val = str(df.iloc[0, 0]) if not df.empty else ""
|
||||
|
||||
if first_col == "jour" or (
|
||||
len(first_val) >= 10 and first_val.count("-") == 2
|
||||
):
|
||||
# Si la première colonne est 'jour' ou contient une date au format YYYY-MM-DD
|
||||
df["date"] = pd.to_datetime(
|
||||
df.iloc[:, 0].astype(str), format="%Y-%m-%d"
|
||||
)
|
||||
elif "annee" in df.columns:
|
||||
# Sinon, utiliser la colonne 'annee' et ajouter un jour fictif (1er janvier)
|
||||
df["date"] = pd.to_datetime(
|
||||
df["annee"].astype(str) + "-01-01", format="%Y-%m-%d"
|
||||
)
|
||||
else:
|
||||
# Si aucune colonne de date n'est trouvée, essayer d'utiliser la première colonne
|
||||
try:
|
||||
df["date"] = pd.to_datetime(df.iloc[:, 0])
|
||||
except:
|
||||
print("Erreur: Impossible de trouver ou convertir une colonne de date.")
|
||||
sys.exit(1)
|
||||
|
||||
# Filtrer par code INSEE si spécifié et si la colonne 'zone' contient des codes INSEE
|
||||
if insee_code and "zone" in df.columns and not "code_insee" in df.columns:
|
||||
# Vérifier si la zone contient le code INSEE
|
||||
if any(
|
||||
zone.endswith(insee_code) for zone in df["zone"] if isinstance(zone, str)
|
||||
):
|
||||
df = df[df["zone"].str.endswith(insee_code)]
|
||||
|
||||
# Trier par date
|
||||
df = df.sort_values("date")
|
||||
|
||||
return df
|
||||
|
||||
|
||||
def generate_graph(df, output_path=None):
|
||||
"""
|
||||
Génère un graphique montrant l'évolution du nombre d'objets dans le temps.
|
||||
|
||||
Args:
|
||||
df: DataFrame pandas contenant les données
|
||||
output_path: Chemin de sortie pour le graphique (optionnel)
|
||||
"""
|
||||
# Créer une figure avec une taille adaptée
|
||||
plt.figure(figsize=(12, 8))
|
||||
|
||||
# Déterminer la colonne pour les types d'objets (type_objet ou theme)
|
||||
type_column = "type_objet" if "type_objet" in df.columns else "theme"
|
||||
label_objet = "objet"
|
||||
# Obtenir la liste des types d'objets uniques
|
||||
if type_column in df.columns:
|
||||
object_types = df[type_column].unique()
|
||||
|
||||
# Créer un graphique pour chaque type d'objet
|
||||
for obj_type in object_types:
|
||||
# Filtrer les données pour ce type d'objet
|
||||
obj_data = df[df[type_column] == obj_type]
|
||||
|
||||
# Filtrer les valeurs nulles
|
||||
obj_data_filtered = obj_data[obj_data["nombre_total"].notna()]
|
||||
|
||||
if not obj_data_filtered.empty:
|
||||
# Tracer la ligne pour le nombre total d'objets (sans marqueurs, avec courbe lissée)
|
||||
line, = plt.plot(
|
||||
obj_data_filtered["date"],
|
||||
obj_data_filtered["nombre_total"],
|
||||
linestyle="-",
|
||||
label=f"{obj_type}",
|
||||
)
|
||||
label_objet = obj_type
|
||||
|
||||
# Ajouter un remplissage pastel sous la courbe
|
||||
plt.fill_between(
|
||||
obj_data_filtered["date"],
|
||||
obj_data_filtered["nombre_total"],
|
||||
alpha=0.3,
|
||||
color=line.get_color()
|
||||
)
|
||||
else:
|
||||
# Si aucune colonne de type n'est trouvée, tracer simplement le nombre total
|
||||
# Filtrer les valeurs nulles
|
||||
df_filtered = df[df["nombre_total"].notna()]
|
||||
|
||||
if not df_filtered.empty:
|
||||
# Tracer la ligne pour le nombre total d'objets (sans marqueurs, avec courbe lissée)
|
||||
line, = plt.plot(
|
||||
df_filtered["date"],
|
||||
df_filtered["nombre_total"],
|
||||
linestyle="-",
|
||||
label="",
|
||||
)
|
||||
|
||||
# Ajouter un remplissage pastel sous la courbe
|
||||
plt.fill_between(
|
||||
df_filtered["date"],
|
||||
df_filtered["nombre_total"],
|
||||
alpha=0.3,
|
||||
color=line.get_color()
|
||||
)
|
||||
|
||||
# Configurer les axes et les légendes
|
||||
plt.xlabel("Date")
|
||||
plt.ylabel("Nombre d'objets")
|
||||
|
||||
plt.grid(True, linestyle="--", alpha=0.7)
|
||||
|
||||
# Formater l'axe des x pour afficher les dates correctement
|
||||
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m"))
|
||||
plt.gca().xaxis.set_major_locator(mdates.MonthLocator(interval=6))
|
||||
plt.gcf().autofmt_xdate()
|
||||
|
||||
# Ajouter une légende
|
||||
plt.legend()
|
||||
|
||||
zone_label = ""
|
||||
city_name = None
|
||||
# Ajouter des informations sur la commune
|
||||
if "code_insee" in df.columns and len(df["code_insee"].unique()) == 1:
|
||||
insee_code = df["code_insee"].iloc[0]
|
||||
city_name = get_city_name(insee_code)
|
||||
if city_name:
|
||||
plt.figtext(0.02, 0.02, f"Commune: {insee_code} - {city_name}", fontsize=10)
|
||||
zone_label = f"{insee_code} - {city_name}"
|
||||
else:
|
||||
plt.figtext(0.02, 0.02, f"Commune: {insee_code}", fontsize=10)
|
||||
zone_label = insee_code
|
||||
elif "zone" in df.columns and len(df["zone"].unique()) == 1:
|
||||
zone = df["zone"].iloc[0]
|
||||
city_name = get_city_name(zone)
|
||||
if city_name:
|
||||
plt.figtext(0.02, 0.02, f"Zone: {zone} - {city_name}", fontsize=10)
|
||||
zone_label = f"{zone} - {city_name}"
|
||||
else:
|
||||
plt.figtext(0.02, 0.02, f"Zone: {zone}", fontsize=10)
|
||||
zone_label = zone
|
||||
|
||||
plt.title(f"{zone_label} : {label_objet} dans le temps")
|
||||
|
||||
# Ajouter la date de génération
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
plt.figtext(0.98, 0.02, f"Généré le: {now}", fontsize=8, ha="right")
|
||||
|
||||
# Ajuster la mise en page
|
||||
plt.tight_layout()
|
||||
|
||||
# Sauvegarder ou afficher le graphique
|
||||
if output_path:
|
||||
plt.savefig(output_path, dpi=300, bbox_inches="tight")
|
||||
print(f"Graphique sauvegardé: {output_path}")
|
||||
else:
|
||||
plt.show()
|
||||
|
||||
|
||||
def generate_completion_graph(df, output_path=None):
|
||||
"""
|
||||
Génère un graphique montrant l'évolution du taux de complétion des attributs dans le temps.
|
||||
|
||||
Args:
|
||||
df: DataFrame pandas contenant les données
|
||||
output_path: Chemin de sortie pour le graphique (optionnel)
|
||||
"""
|
||||
# Vérifier si la colonne de pourcentage de complétion existe
|
||||
if "pourcentage_completion" not in df.columns:
|
||||
print(
|
||||
"Avertissement: La colonne 'pourcentage_completion' n'existe pas dans le CSV. Le graphique de complétion ne sera pas généré."
|
||||
)
|
||||
return
|
||||
|
||||
# Créer une figure avec une taille adaptée
|
||||
plt.figure(figsize=(12, 8))
|
||||
|
||||
# Déterminer la colonne pour les types d'objets (type_objet ou theme)
|
||||
type_column = "type_objet" if "type_objet" in df.columns else "theme"
|
||||
|
||||
# Obtenir la liste des types d'objets uniques
|
||||
if type_column in df.columns:
|
||||
object_types = df[type_column].unique()
|
||||
|
||||
# Créer un graphique pour chaque type d'objet
|
||||
for obj_type in object_types:
|
||||
# Filtrer les données pour ce type d'objet
|
||||
obj_data = df[df[type_column] == obj_type]
|
||||
|
||||
# Filtrer les valeurs nulles
|
||||
obj_data_filtered = obj_data[obj_data["pourcentage_completion"].notna()]
|
||||
|
||||
if not obj_data_filtered.empty:
|
||||
# Tracer la ligne pour le taux de complétion (sans marqueurs, avec courbe lissée)
|
||||
line, = plt.plot(
|
||||
obj_data_filtered["date"],
|
||||
obj_data_filtered["pourcentage_completion"],
|
||||
linestyle="-",
|
||||
label=f"{obj_type} - Complétion (%)",
|
||||
)
|
||||
|
||||
# Ajouter un remplissage pastel sous la courbe
|
||||
plt.fill_between(
|
||||
obj_data_filtered["date"],
|
||||
obj_data_filtered["pourcentage_completion"],
|
||||
alpha=0.3,
|
||||
color=line.get_color()
|
||||
)
|
||||
else:
|
||||
# Si aucune colonne de type n'est trouvée, tracer simplement le taux de complétion global
|
||||
# Filtrer les valeurs nulles
|
||||
df_filtered = df[df["pourcentage_completion"].notna()]
|
||||
|
||||
if not df_filtered.empty:
|
||||
# Tracer la ligne pour le taux de complétion (sans marqueurs, avec courbe lissée)
|
||||
line, = plt.plot(
|
||||
df_filtered["date"],
|
||||
df_filtered["pourcentage_completion"],
|
||||
linestyle="-",
|
||||
label="Complétion (%)",
|
||||
)
|
||||
|
||||
# Ajouter un remplissage pastel sous la courbe
|
||||
plt.fill_between(
|
||||
df_filtered["date"],
|
||||
df_filtered["pourcentage_completion"],
|
||||
alpha=0.3,
|
||||
color=line.get_color()
|
||||
)
|
||||
|
||||
# Configurer les axes et les légendes
|
||||
plt.xlabel("Date")
|
||||
plt.ylabel("Complétion (%)")
|
||||
plt.title("Évolution du taux de complétion dans le temps")
|
||||
plt.grid(True, linestyle="--", alpha=0.7)
|
||||
|
||||
# Définir les limites de l'axe y entre 0 et 100%
|
||||
plt.ylim(0, 100)
|
||||
|
||||
# Formater l'axe des x pour afficher les dates correctement
|
||||
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m"))
|
||||
plt.gca().xaxis.set_major_locator(mdates.MonthLocator(interval=6))
|
||||
plt.gcf().autofmt_xdate()
|
||||
|
||||
# Ajouter une légende
|
||||
plt.legend()
|
||||
|
||||
# Ajouter des informations sur la commune
|
||||
zone_label = ""
|
||||
if "code_insee" in df.columns and len(df["code_insee"].unique()) == 1:
|
||||
insee_code = df["code_insee"].iloc[0]
|
||||
city_name = get_city_name(insee_code)
|
||||
if city_name:
|
||||
plt.figtext(0.02, 0.02, f"Commune: {insee_code} - {city_name}", fontsize=10)
|
||||
zone_label = f"{insee_code} - {city_name}"
|
||||
else:
|
||||
plt.figtext(0.02, 0.02, f"Commune: {insee_code}", fontsize=10)
|
||||
zone_label = insee_code
|
||||
elif "zone" in df.columns and len(df["zone"].unique()) == 1:
|
||||
zone = df["zone"].iloc[0]
|
||||
city_name = get_city_name(zone)
|
||||
if city_name:
|
||||
plt.figtext(0.02, 0.02, f"Zone: {zone} - {city_name}", fontsize=10)
|
||||
zone_label = f"{zone} - {city_name}"
|
||||
else:
|
||||
plt.figtext(0.02, 0.02, f"Zone: {zone}", fontsize=10)
|
||||
zone_label = zone
|
||||
|
||||
# Mettre à jour le titre avec le nom de la zone
|
||||
if zone_label:
|
||||
plt.title(f"{zone_label} : Évolution du taux de complétion dans le temps")
|
||||
|
||||
# Ajouter la date de génération
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
plt.figtext(0.98, 0.02, f"Généré le: {now}", fontsize=8, ha="right")
|
||||
|
||||
# Ajuster la mise en page
|
||||
plt.tight_layout()
|
||||
|
||||
# Sauvegarder ou afficher le graphique
|
||||
if output_path:
|
||||
# Modifier le nom du fichier pour indiquer qu'il s'agit du taux de complétion
|
||||
base, ext = os.path.splitext(output_path)
|
||||
completion_path = f"{base}_completion{ext}"
|
||||
plt.savefig(completion_path, dpi=300, bbox_inches="tight")
|
||||
print(f"Graphique de complétion sauvegardé: {completion_path}")
|
||||
else:
|
||||
plt.show()
|
||||
|
||||
|
||||
def main():
|
||||
"""Fonction principale."""
|
||||
# Analyser les arguments de la ligne de commande
|
||||
args = parse_args()
|
||||
|
||||
# Vérifier que le fichier CSV existe
|
||||
if not os.path.isfile(args.csv_file):
|
||||
print(f"Erreur: Le fichier {args.csv_file} n'existe pas.")
|
||||
sys.exit(1)
|
||||
|
||||
# Charger les données
|
||||
df = load_data(args.csv_file, args.insee, args.period)
|
||||
|
||||
# Vérifier qu'il y a des données
|
||||
if df.empty:
|
||||
print(f"Aucune donnée trouvée pour la période {args.period}.")
|
||||
sys.exit(1)
|
||||
|
||||
# Déterminer le chemin de sortie si non spécifié
|
||||
if not args.output:
|
||||
# Utiliser le même nom que le fichier CSV mais avec l'extension .png
|
||||
base_name = os.path.splitext(args.csv_file)[0]
|
||||
output_path = f"{base_name}_{args.period}_graph.png"
|
||||
else:
|
||||
output_path = args.output
|
||||
|
||||
# Générer les graphiques
|
||||
generate_graph(df, output_path)
|
||||
generate_completion_graph(df, output_path)
|
||||
|
||||
print("Graphiques générés avec succès!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -1,167 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Script pour récupérer les polygones de toutes les communes françaises listées dans un fichier CSV.
|
||||
|
||||
Ce script:
|
||||
1. Ouvre le fichier osm-commerces-villes-export.csv
|
||||
2. Extrait les codes INSEE (colonne 'zone')
|
||||
3. Pour chaque code INSEE, vérifie si le polygone existe déjà
|
||||
4. Si non, utilise get_poly.py pour récupérer le polygone
|
||||
|
||||
Usage:
|
||||
python get_all_polys.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import csv
|
||||
from get_poly import query_overpass_api, extract_polygon, save_polygon_to_file
|
||||
|
||||
# Chemin vers le fichier CSV contenant les codes INSEE
|
||||
CSV_FILE = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)), "osm-commerces-villes-export.csv"
|
||||
)
|
||||
|
||||
# Chemin vers le dossier où sont stockés les polygones
|
||||
POLYGONS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "polygons")
|
||||
|
||||
|
||||
def ensure_polygons_dir_exists():
|
||||
"""
|
||||
Vérifie que le dossier 'polygons' existe, sinon le crée.
|
||||
"""
|
||||
os.makedirs(POLYGONS_DIR, exist_ok=True)
|
||||
print(f"Dossier de polygones: {POLYGONS_DIR}")
|
||||
|
||||
|
||||
def polygon_exists(insee_code):
|
||||
"""
|
||||
Vérifie si le polygone pour le code INSEE donné existe déjà.
|
||||
|
||||
Args:
|
||||
insee_code (str): Le code INSEE de la commune
|
||||
|
||||
Returns:
|
||||
bool: True si le polygone existe, False sinon
|
||||
"""
|
||||
polygon_file = os.path.join(POLYGONS_DIR, f"commune_{insee_code}.poly")
|
||||
return os.path.isfile(polygon_file)
|
||||
|
||||
|
||||
def get_polygon(insee_code):
|
||||
"""
|
||||
Récupère le polygone pour le code INSEE donné.
|
||||
|
||||
Args:
|
||||
insee_code (str): Le code INSEE de la commune
|
||||
|
||||
Returns:
|
||||
str: Le chemin du fichier polygone créé, ou None en cas d'erreur
|
||||
"""
|
||||
try:
|
||||
print(f"Récupération du polygone pour la commune {insee_code}...")
|
||||
|
||||
# Interroger l'API Overpass
|
||||
data = query_overpass_api(insee_code)
|
||||
|
||||
# Extraire le polygone
|
||||
polygon = extract_polygon(data)
|
||||
|
||||
# Sauvegarder le polygone dans un fichier
|
||||
output_file = save_polygon_to_file(polygon, insee_code)
|
||||
|
||||
print(f"Polygone pour la commune {insee_code} sauvegardé dans {output_file}")
|
||||
return output_file
|
||||
except Exception as e:
|
||||
print(f"Erreur lors de la récupération du polygone pour {insee_code}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def read_insee_codes_from_csv():
|
||||
"""
|
||||
Lit le fichier CSV et extrait les codes INSEE (colonne 'zone').
|
||||
|
||||
Returns:
|
||||
list: Liste des codes INSEE
|
||||
"""
|
||||
insee_codes = []
|
||||
|
||||
try:
|
||||
print(f"Lecture du fichier CSV: {CSV_FILE}")
|
||||
|
||||
if not os.path.isfile(CSV_FILE):
|
||||
print(f"Erreur: Le fichier {CSV_FILE} n'existe pas.")
|
||||
return insee_codes
|
||||
|
||||
with open(CSV_FILE, "r", encoding="utf-8") as csvfile:
|
||||
reader = csv.DictReader(csvfile)
|
||||
|
||||
for row in reader:
|
||||
if "zone" in row and row["zone"]:
|
||||
insee_codes.append(row["zone"])
|
||||
|
||||
print(f"Nombre de codes INSEE trouvés: {len(insee_codes)}")
|
||||
return insee_codes
|
||||
except Exception as e:
|
||||
print(f"Erreur lors de la lecture du fichier CSV: {e}")
|
||||
return insee_codes
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Fonction principale du script.
|
||||
"""
|
||||
try:
|
||||
# S'assurer que le dossier des polygones existe
|
||||
ensure_polygons_dir_exists()
|
||||
|
||||
# Lire les codes INSEE depuis le fichier CSV
|
||||
insee_codes = read_insee_codes_from_csv()
|
||||
|
||||
if not insee_codes:
|
||||
print("Aucun code INSEE trouvé dans le fichier CSV.")
|
||||
return 1
|
||||
|
||||
# Compteurs pour les statistiques
|
||||
total = len(insee_codes)
|
||||
existing = 0
|
||||
created = 0
|
||||
failed = 0
|
||||
|
||||
# Pour chaque code INSEE, récupérer le polygone s'il n'existe pas déjà
|
||||
for i, insee_code in enumerate(insee_codes, 1):
|
||||
print(f"\nTraitement de la commune {i}/{total}: {insee_code}")
|
||||
|
||||
if polygon_exists(insee_code):
|
||||
print(f"Le polygone pour la commune {insee_code} existe déjà.")
|
||||
existing += 1
|
||||
continue
|
||||
|
||||
# Récupérer le polygone
|
||||
result = get_polygon(insee_code)
|
||||
|
||||
if result:
|
||||
created += 1
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
# Afficher les statistiques
|
||||
print("\nRésumé:")
|
||||
print(f"Total des communes traitées: {total}")
|
||||
print(f"Polygones déjà existants: {existing}")
|
||||
print(f"Polygones créés avec succès: {created}")
|
||||
print(f"Échecs: {failed}")
|
||||
|
||||
return 0 # Succès
|
||||
except KeyboardInterrupt:
|
||||
print("\nOpération annulée par l'utilisateur.")
|
||||
return 1 # Erreur
|
||||
except Exception as e:
|
||||
print(f"Erreur inattendue: {e}")
|
||||
return 1 # Erreur
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
|
@ -1,287 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Script pour récupérer le polygone d'une commune française à partir de son code INSEE.
|
||||
|
||||
Ce script:
|
||||
1. Demande un code INSEE
|
||||
2. Interroge l'API Overpass Turbo pour obtenir les limites administratives
|
||||
3. Extrait le polygone de la commune
|
||||
4. Sauvegarde le polygone dans un fichier
|
||||
|
||||
Usage:
|
||||
python get_poly.py [code_insee]
|
||||
|
||||
Si le code INSEE n'est pas fourni en argument, le script le demandera interactivement.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
import argparse
|
||||
|
||||
|
||||
def get_insee_code():
|
||||
"""
|
||||
Récupère le code INSEE soit depuis les arguments de ligne de commande,
|
||||
soit en demandant à l'utilisateur.
|
||||
|
||||
Returns:
|
||||
str: Le code INSEE de la commune
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Récupère le polygone d'une commune à partir de son code INSEE"
|
||||
)
|
||||
parser.add_argument("insee", nargs="?", help="Code INSEE de la commune")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.insee:
|
||||
return args.insee
|
||||
|
||||
# Si le code INSEE n'est pas fourni en argument, le demander
|
||||
return input("Entrez le code INSEE de la commune: ")
|
||||
|
||||
|
||||
def query_overpass_api(insee_code):
|
||||
"""
|
||||
Interroge l'API Overpass pour obtenir les limites administratives d'une commune.
|
||||
|
||||
Args:
|
||||
insee_code (str): Le code INSEE de la commune
|
||||
|
||||
Returns:
|
||||
dict: Les données GeoJSON de la commune
|
||||
"""
|
||||
print(f"Récupération des limites administratives pour la commune {insee_code}...")
|
||||
|
||||
# Construire la requête Overpass QL pour obtenir la relation administrative
|
||||
query = f"""
|
||||
[out:json][timeout:60];
|
||||
(
|
||||
relation["boundary"="administrative"]["admin_level"="8"]["ref:INSEE"="{insee_code}"];
|
||||
way(r);
|
||||
node(w);
|
||||
);
|
||||
out geom;
|
||||
"""
|
||||
|
||||
# Encoder la requête pour l'URL
|
||||
encoded_query = urllib.parse.quote(query)
|
||||
|
||||
# Construire l'URL de l'API Overpass
|
||||
url = f"https://overpass-api.de/api/interpreter?data={encoded_query}"
|
||||
|
||||
try:
|
||||
# Envoyer la requête à l'API
|
||||
print("Envoi de la requête à Overpass API...")
|
||||
with urllib.request.urlopen(url) as response:
|
||||
data = json.loads(response.read().decode("utf-8"))
|
||||
|
||||
# Afficher des informations sur la réponse (version réduite pour production)
|
||||
print(
|
||||
f"Réponse reçue de l'API Overpass. Nombre d'éléments: {len(data.get('elements', []))}"
|
||||
)
|
||||
|
||||
return data
|
||||
except Exception as e:
|
||||
print(f"Erreur lors de la requête à l'API Overpass: {e}")
|
||||
raise RuntimeError(f"Erreur lors de la requête à l'API Overpass: {e}")
|
||||
|
||||
|
||||
def extract_polygon(data):
|
||||
"""
|
||||
Extrait le polygone des données GeoJSON.
|
||||
|
||||
Args:
|
||||
data (dict): Les données GeoJSON de la commune
|
||||
|
||||
Returns:
|
||||
list: Liste des coordonnées du polygone
|
||||
"""
|
||||
print("Extraction du polygone des données...")
|
||||
|
||||
# Vérifier si des éléments ont été trouvés
|
||||
if not data.get("elements"):
|
||||
print("Aucune limite administrative trouvée pour ce code INSEE.")
|
||||
raise ValueError("Aucune limite administrative trouvée pour ce code INSEE.")
|
||||
|
||||
try:
|
||||
# Collecter tous les nœuds (points) avec leurs coordonnées
|
||||
nodes = {}
|
||||
for element in data["elements"]:
|
||||
if element["type"] == "node":
|
||||
nodes[element["id"]] = (element["lon"], element["lat"])
|
||||
|
||||
# Trouver les ways qui forment le contour de la commune
|
||||
ways = []
|
||||
for element in data["elements"]:
|
||||
if element["type"] == "way":
|
||||
ways.append(element)
|
||||
|
||||
# Si aucun way n'est trouvé, essayer d'extraire directement les coordonnées des nœuds
|
||||
if not ways and nodes:
|
||||
print("Aucun way trouvé. Utilisation directe des nœuds...")
|
||||
polygon = list(nodes.values())
|
||||
return polygon
|
||||
|
||||
# Trouver la relation administrative
|
||||
relation = None
|
||||
for element in data["elements"]:
|
||||
if (
|
||||
element["type"] == "relation"
|
||||
and element.get("tags", {}).get("boundary") == "administrative"
|
||||
):
|
||||
relation = element
|
||||
break
|
||||
|
||||
if not relation:
|
||||
print("Aucune relation administrative trouvée.")
|
||||
# Si nous avons des ways, nous pouvons essayer de les utiliser directement
|
||||
if ways:
|
||||
print("Tentative d'utilisation directe des ways...")
|
||||
# Prendre le premier way comme contour
|
||||
way = ways[0]
|
||||
polygon = []
|
||||
for node_id in way.get("nodes", []):
|
||||
if node_id in nodes:
|
||||
polygon.append(nodes[node_id])
|
||||
return polygon
|
||||
raise ValueError(
|
||||
"Impossible de trouver une relation administrative ou des ways"
|
||||
)
|
||||
|
||||
# Extraire les ways qui forment le contour extérieur de la relation
|
||||
outer_ways = []
|
||||
for member in relation.get("members", []):
|
||||
if member.get("role") == "outer" and member.get("type") == "way":
|
||||
# Trouver le way correspondant
|
||||
for way in ways:
|
||||
if way["id"] == member["ref"]:
|
||||
outer_ways.append(way)
|
||||
break
|
||||
|
||||
# Si aucun way extérieur n'est trouvé, utiliser tous les ways
|
||||
if not outer_ways:
|
||||
print("Aucun way extérieur trouvé. Utilisation de tous les ways...")
|
||||
outer_ways = ways
|
||||
|
||||
# Construire le polygone à partir des ways extérieurs
|
||||
polygon = []
|
||||
for way in outer_ways:
|
||||
for node_id in way.get("nodes", []):
|
||||
if node_id in nodes:
|
||||
polygon.append(nodes[node_id])
|
||||
|
||||
if not polygon:
|
||||
raise ValueError("Impossible d'extraire le polygone de la relation")
|
||||
|
||||
print(f"Polygone extrait avec {len(polygon)} points.")
|
||||
return polygon
|
||||
except Exception as e:
|
||||
print(f"Erreur lors de l'extraction du polygone: {e}")
|
||||
raise RuntimeError(f"Erreur lors de l'extraction du polygone: {e}")
|
||||
|
||||
|
||||
def save_polygon_to_file(polygon, insee_code):
|
||||
"""
|
||||
Sauvegarde le polygone dans un fichier.
|
||||
|
||||
Args:
|
||||
polygon (list): Liste des coordonnées du polygone
|
||||
insee_code (str): Le code INSEE de la commune
|
||||
|
||||
Returns:
|
||||
str: Le chemin du fichier créé
|
||||
|
||||
Raises:
|
||||
ValueError: Si le polygone est vide ou invalide
|
||||
IOError: Si une erreur survient lors de l'écriture du fichier
|
||||
"""
|
||||
if not polygon:
|
||||
raise ValueError("Le polygone est vide")
|
||||
|
||||
try:
|
||||
# Créer le répertoire de sortie s'il n'existe pas
|
||||
output_dir = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)), "polygons"
|
||||
)
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# Définir le nom du fichier de sortie
|
||||
output_file = os.path.join(output_dir, f"commune_{insee_code}.poly")
|
||||
|
||||
print(f"Sauvegarde du polygone dans le fichier {output_file}...")
|
||||
|
||||
# Écrire le polygone dans le fichier au format .poly (format utilisé par Osmosis)
|
||||
with open(output_file, "w") as f:
|
||||
f.write(f"commune_{insee_code}\n")
|
||||
f.write("1\n") # Numéro de section
|
||||
|
||||
# Écrire les coordonnées
|
||||
for i, (lon, lat) in enumerate(polygon):
|
||||
f.write(f" {lon:.7f} {lat:.7f}\n")
|
||||
|
||||
# Fermer le polygone en répétant le premier point
|
||||
if len(polygon) > 1 and polygon[0] != polygon[-1]:
|
||||
lon, lat = polygon[0]
|
||||
f.write(f" {lon:.7f} {lat:.7f}\n")
|
||||
|
||||
f.write("END\n")
|
||||
f.write("END\n")
|
||||
|
||||
print(f"Polygone sauvegardé avec succès dans {output_file}")
|
||||
return output_file
|
||||
except IOError as e:
|
||||
print(f"Erreur lors de l'écriture du fichier: {e}")
|
||||
raise # Re-raise the IOError
|
||||
except Exception as e:
|
||||
print(f"Erreur inattendue lors de la sauvegarde du polygone: {e}")
|
||||
raise RuntimeError(f"Erreur inattendue lors de la sauvegarde du polygone: {e}")
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Fonction principale du script.
|
||||
"""
|
||||
try:
|
||||
# Récupérer le code INSEE
|
||||
insee_code = get_insee_code()
|
||||
|
||||
# Vérifier que le code INSEE est valide (format numérique ou alphanumérique pour les DOM-TOM)
|
||||
if not insee_code:
|
||||
raise ValueError("Le code INSEE ne peut pas être vide")
|
||||
|
||||
if not insee_code.isalnum() or len(insee_code) not in [5, 3]:
|
||||
raise ValueError(
|
||||
"Code INSEE invalide. Il doit être composé de 5 chiffres (ou 3 pour certains territoires)."
|
||||
)
|
||||
|
||||
# Interroger l'API Overpass
|
||||
data = query_overpass_api(insee_code)
|
||||
|
||||
# Extraire le polygone
|
||||
polygon = extract_polygon(data)
|
||||
|
||||
# Sauvegarder le polygone dans un fichier
|
||||
output_file = save_polygon_to_file(polygon, insee_code)
|
||||
|
||||
print(
|
||||
f"Terminé. Le polygone de la commune {insee_code} a été sauvegardé dans {output_file}"
|
||||
)
|
||||
return 0 # Succès
|
||||
except ValueError as e:
|
||||
print(f"Erreur de validation: {e}")
|
||||
return 1 # Erreur
|
||||
except KeyboardInterrupt:
|
||||
print("\nOpération annulée par l'utilisateur.")
|
||||
return 1 # Erreur
|
||||
except Exception as e:
|
||||
print(f"Erreur inattendue: {e}")
|
||||
return 1 # Erreur
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
|
@ -1,301 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Script principal pour lancer l'analyse historique d'une ville.
|
||||
|
||||
Ce script:
|
||||
1. Demande à l'utilisateur quelle ville il souhaite traiter
|
||||
2. Trouve le code INSEE de la ville demandée
|
||||
3. Vérifie si le polygone de la ville existe, sinon le récupère
|
||||
4. Traite les données historiques OSM pour cette ville
|
||||
|
||||
Usage:
|
||||
python historize_zone.py [--input fichier_historique.osh.pbf]
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import csv
|
||||
import argparse
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
# Chemin vers le répertoire du script
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# Chemin vers le fichier CSV contenant les données des villes
|
||||
CITIES_CSV = os.path.join(SCRIPT_DIR, "osm-commerces-villes-export.csv")
|
||||
|
||||
# Chemin vers le répertoire des polygones
|
||||
POLYGONS_DIR = os.path.join(SCRIPT_DIR, "polygons")
|
||||
|
||||
# Chemin par défaut pour le fichier d'historique OSM France
|
||||
DEFAULT_HISTORY_FILE = os.path.join(SCRIPT_DIR, "osm_data", "france-internal.osh.pbf")
|
||||
|
||||
|
||||
def run_command(command):
|
||||
"""Exécute une commande shell et retourne la sortie"""
|
||||
print(f"Exécution: {command}")
|
||||
try:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
shell=True,
|
||||
check=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
universal_newlines=True,
|
||||
)
|
||||
return result.stdout
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Erreur lors de l'exécution de la commande: {e}")
|
||||
print(f"Sortie de la commande: {e.stdout}")
|
||||
print(f"Erreur de la commande: {e.stderr}")
|
||||
return None
|
||||
|
||||
|
||||
def load_cities():
|
||||
"""
|
||||
Charge les données des villes depuis le fichier CSV.
|
||||
|
||||
Returns:
|
||||
dict: Dictionnaire des villes avec le nom comme clé et les données comme valeur
|
||||
"""
|
||||
cities = {}
|
||||
try:
|
||||
with open(CITIES_CSV, "r", encoding="utf-8") as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
if row.get("name") and row.get("zone"):
|
||||
cities[row["name"].lower()] = row
|
||||
except Exception as e:
|
||||
print(f"Erreur lors du chargement du fichier CSV des villes: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
return cities
|
||||
|
||||
|
||||
def find_city(city_name, cities):
|
||||
"""
|
||||
Recherche une ville par son nom dans le dictionnaire des villes.
|
||||
|
||||
Args:
|
||||
city_name (str): Nom de la ville à rechercher
|
||||
cities (dict): Dictionnaire des villes
|
||||
|
||||
Returns:
|
||||
dict: Données de la ville si trouvée, None sinon
|
||||
"""
|
||||
# Recherche exacte
|
||||
if city_name.lower() in cities:
|
||||
return cities[city_name.lower()]
|
||||
|
||||
# Recherche partielle
|
||||
matches = []
|
||||
for name, data in cities.items():
|
||||
if city_name.lower() in name:
|
||||
matches.append(data)
|
||||
|
||||
if not matches:
|
||||
return None
|
||||
|
||||
# Si plusieurs correspondances, demander à l'utilisateur de choisir
|
||||
if len(matches) > 1:
|
||||
print(f"Plusieurs villes correspondent à '{city_name}':")
|
||||
for i, city in enumerate(matches):
|
||||
print(f"{i+1}. {city['name']} (INSEE: {city['zone']})")
|
||||
|
||||
choice = input("Entrez le numéro de la ville souhaitée (ou 'q' pour quitter): ")
|
||||
if choice.lower() == "q":
|
||||
sys.exit(0)
|
||||
|
||||
try:
|
||||
index = int(choice) - 1
|
||||
if 0 <= index < len(matches):
|
||||
return matches[index]
|
||||
else:
|
||||
print("Choix invalide.")
|
||||
return None
|
||||
except ValueError:
|
||||
print("Veuillez entrer un numéro valide.")
|
||||
return None
|
||||
|
||||
return matches[0]
|
||||
|
||||
|
||||
def check_polygon_exists(insee_code):
|
||||
"""
|
||||
Vérifie si le polygone d'une commune existe déjà.
|
||||
|
||||
Args:
|
||||
insee_code (str): Code INSEE de la commune
|
||||
|
||||
Returns:
|
||||
str: Chemin vers le fichier polygone s'il existe, None sinon
|
||||
"""
|
||||
poly_file = os.path.join(POLYGONS_DIR, f"commune_{insee_code}.poly")
|
||||
if os.path.isfile(poly_file):
|
||||
return poly_file
|
||||
return None
|
||||
|
||||
|
||||
def get_polygon(insee_code):
|
||||
"""
|
||||
Récupère le polygone d'une commune à partir de son code INSEE.
|
||||
|
||||
Args:
|
||||
insee_code (str): Code INSEE de la commune
|
||||
|
||||
Returns:
|
||||
str: Chemin vers le fichier polygone créé, None en cas d'erreur
|
||||
"""
|
||||
get_poly_script = os.path.join(SCRIPT_DIR, "get_poly.py")
|
||||
command = f"python3 {get_poly_script} {insee_code}"
|
||||
|
||||
output = run_command(command)
|
||||
if output:
|
||||
# Vérifier si le polygone a été créé
|
||||
poly_file = check_polygon_exists(insee_code)
|
||||
if poly_file:
|
||||
return poly_file
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def process_city_history(input_file, poly_file, cleanup=False, benchmark=False):
|
||||
"""
|
||||
Traite l'historique OSM pour une ville.
|
||||
|
||||
Args:
|
||||
input_file (str): Chemin vers le fichier d'historique OSM
|
||||
poly_file (str): Chemin vers le fichier polygone de la ville
|
||||
cleanup (bool): Si True, nettoie les fichiers temporaires après traitement
|
||||
benchmark (bool): Si True, affiche des informations de performance détaillées
|
||||
|
||||
Returns:
|
||||
bool: True si le traitement a réussi, False sinon
|
||||
"""
|
||||
loop_script = os.path.join(
|
||||
SCRIPT_DIR, "loop_thematics_history_in_zone_to_counts.py"
|
||||
)
|
||||
output_dir = os.path.join(SCRIPT_DIR, "test_results")
|
||||
temp_dir = os.path.join(SCRIPT_DIR, "test_temp")
|
||||
|
||||
# Créer les répertoires de sortie si nécessaires
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
|
||||
# Construire la commande avec les options supplémentaires
|
||||
command = f"python3 {loop_script} --input {input_file} --poly {poly_file} --output-dir {output_dir} --temp-dir {temp_dir} --max-dates 100"
|
||||
|
||||
# Ajouter les options de nettoyage et de benchmark si activées
|
||||
if cleanup:
|
||||
command += " --cleanup"
|
||||
if benchmark:
|
||||
command += " --benchmark"
|
||||
|
||||
print(f"Exécution de la commande: {command}")
|
||||
output = run_command(command)
|
||||
if output is not None:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""Fonction principale"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Analyse historique d'une ville dans OpenStreetMap."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--input",
|
||||
"-i",
|
||||
default=DEFAULT_HISTORY_FILE,
|
||||
help=f"Fichier d'historique OSM (.osh.pbf). Par défaut: {DEFAULT_HISTORY_FILE}",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--cleanup",
|
||||
"-c",
|
||||
action="store_true",
|
||||
help="Nettoyer les fichiers temporaires après traitement",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--benchmark",
|
||||
"-b",
|
||||
action="store_true",
|
||||
help="Afficher des informations de performance détaillées",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--city",
|
||||
"-v",
|
||||
help="Nom de la ville à traiter (si non spécifié, demande interactive)",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Vérifier que le fichier d'historique existe
|
||||
if not os.path.isfile(args.input):
|
||||
print(f"Erreur: Le fichier d'historique {args.input} n'existe pas.")
|
||||
print(
|
||||
f"Veuillez spécifier un fichier d'historique valide avec l'option --input."
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# Charger les données des villes
|
||||
cities = load_cities()
|
||||
if not cities:
|
||||
print("Aucune ville n'a été trouvée dans le fichier CSV.")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Données chargées pour {len(cities)} villes.")
|
||||
|
||||
# Obtenir le nom de la ville à traiter
|
||||
city_name = args.city
|
||||
if not city_name:
|
||||
# Mode interactif si aucune ville n'est spécifiée
|
||||
city_name = input("Quelle ville souhaitez-vous traiter ? ")
|
||||
|
||||
# Rechercher la ville
|
||||
city = find_city(city_name, cities)
|
||||
if not city:
|
||||
print(f"Aucune ville correspondant à '{city_name}' n'a été trouvée.")
|
||||
sys.exit(1)
|
||||
|
||||
insee_code = city["zone"]
|
||||
print(f"Ville trouvée: {city['name']} (INSEE: {insee_code})")
|
||||
|
||||
# Vérifier si le polygone existe
|
||||
poly_file = check_polygon_exists(insee_code)
|
||||
if poly_file:
|
||||
print(f"Le polygone pour {city['name']} existe déjà: {poly_file}")
|
||||
else:
|
||||
print(f"Le polygone pour {city['name']} n'existe pas. Récupération en cours...")
|
||||
poly_file = get_polygon(insee_code)
|
||||
if not poly_file:
|
||||
print(f"Erreur: Impossible de récupérer le polygone pour {city['name']}.")
|
||||
sys.exit(1)
|
||||
print(f"Polygone récupéré avec succès: {poly_file}")
|
||||
|
||||
# Afficher les options activées
|
||||
if args.benchmark:
|
||||
print("\n=== Options ===")
|
||||
print(
|
||||
f"Nettoyage des fichiers temporaires: {'Activé' if args.cleanup else 'Désactivé'}"
|
||||
)
|
||||
print(f"Benchmark: Activé")
|
||||
print("===============\n")
|
||||
|
||||
# Traiter l'historique pour cette ville
|
||||
print(f"Traitement de l'historique OSM pour {city['name']}...")
|
||||
success = process_city_history(args.input, poly_file, args.cleanup, args.benchmark)
|
||||
|
||||
if success:
|
||||
print(f"Traitement terminé avec succès pour {city['name']}.")
|
||||
else:
|
||||
print(f"Erreur lors du traitement de l'historique pour {city['name']}.")
|
||||
sys.exit(1)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
|
@ -1,9 +0,0 @@
|
|||
# get latest france with cli
|
||||
source ./secrets.sh
|
||||
python3 /home/poule/encrypted/stockage-syncable/www/development/html/sendfile_osm_oauth_protector/oauth_cookie_client.py \
|
||||
-u "$OSM_USER" -p "$OSM_PASS" \
|
||||
-c https://osm-internal.download.geofabrik.de/get_cookie \
|
||||
-o cookie.txt
|
||||
|
||||
|
||||
wget -N --no-cookies --header "Cookie: $(cat cookie.txt | cut -d ';' -f 1)" https://osm-internal.download.geofabrik.de/europe/france-latest-internal.osm.pbf -O osm_data/france-internal.osh.pbf
|
|
@ -1,952 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Script pour compter les objets OSM par thématique sur une zone donnée à différentes dates.
|
||||
|
||||
Ce script utilise osmium pour:
|
||||
1. Filtrer les données historiques OSM à différentes dates (mensuelles sur les 10 dernières années)
|
||||
2. Compter les objets correspondant à chaque thématique à chaque date
|
||||
3. Calculer le pourcentage de complétion des attributs importants pour chaque thème
|
||||
4. Sauvegarder les résultats dans des fichiers CSV
|
||||
5. Générer des graphiques montrant l'évolution dans le temps
|
||||
|
||||
Exemple d'utilisation:
|
||||
python3 loop_thematics_history_in_zone_to_counts.py --input france.osh.pbf --poly polygons/commune_91111.poly
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import csv
|
||||
import json
|
||||
import argparse
|
||||
import multiprocessing
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from functools import lru_cache
|
||||
import time
|
||||
|
||||
# Définition des thématiques et leurs tags correspondants (repris de split_history_to_thematics.py)
|
||||
THEMES = {
|
||||
"borne-de-recharge": {
|
||||
"tag_filter": "amenity=charging_station",
|
||||
"important_tags": ["operator", "capacity"],
|
||||
},
|
||||
"borne-incendie": {
|
||||
"tag_filter": "emergency=fire_hydrant",
|
||||
"important_tags": ["ref", "colour"],
|
||||
},
|
||||
"arbres": {
|
||||
"tag_filter": "natural=tree",
|
||||
"important_tags": ["species", "leaf_type"],
|
||||
},
|
||||
"defibrillator": {
|
||||
"tag_filter": "emergency=defibrillator",
|
||||
"important_tags": ["operator", "access"],
|
||||
},
|
||||
"toilets": {
|
||||
"tag_filter": "amenity=toilets",
|
||||
"important_tags": ["access", "wheelchair"],
|
||||
},
|
||||
"bus_stop": {
|
||||
"tag_filter": "highway=bus_stop",
|
||||
"important_tags": ["name", "shelter"],
|
||||
},
|
||||
"camera": {
|
||||
"tag_filter": "man_made=surveillance",
|
||||
"important_tags": ["operator", "surveillance"],
|
||||
},
|
||||
"recycling": {
|
||||
"tag_filter": "amenity=recycling",
|
||||
"important_tags": ["recycling_type", "operator"],
|
||||
},
|
||||
"substation": {
|
||||
"tag_filter": "power=substation",
|
||||
"important_tags": ["operator", "voltage"],
|
||||
},
|
||||
"laboratory": {
|
||||
"tag_filter": "healthcare=laboratory",
|
||||
"important_tags": ["name", "operator"],
|
||||
},
|
||||
"school": {"tag_filter": "amenity=school", "important_tags": ["name", "operator"]},
|
||||
"police": {"tag_filter": "amenity=police", "important_tags": ["name", "operator"]},
|
||||
"healthcare": {
|
||||
"tag_filter": "healthcare or amenity=doctors or amenity=pharmacy or amenity=hospital or amenity=clinic or amenity=social_facility",
|
||||
"important_tags": ["name", "healthcare"],
|
||||
},
|
||||
"bicycle_parking": {
|
||||
"tag_filter": "amenity=bicycle_parking",
|
||||
"important_tags": ["capacity", "covered"],
|
||||
},
|
||||
"advertising_board": {
|
||||
"tag_filter": "advertising=board and message=political",
|
||||
"important_tags": ["operator", ],
|
||||
},
|
||||
"building": {
|
||||
"tag_filter": "building",
|
||||
"important_tags": ["building", "addr:housenumber"],
|
||||
},
|
||||
"email": {
|
||||
"tag_filter": "email or contact:email",
|
||||
"important_tags": ["email", "contact:email"],
|
||||
},
|
||||
"bench": {
|
||||
"tag_filter": "amenity=bench",
|
||||
# "important_tags": ["backrest", "material"]
|
||||
},
|
||||
"waste_basket": {
|
||||
"tag_filter": "amenity=waste_basket",
|
||||
# "important_tags": ["operator", "capacity"]
|
||||
},
|
||||
"street_lamp": {
|
||||
"tag_filter": "highway=street_lamp",
|
||||
# "important_tags": ["light:method", "operator"]
|
||||
},
|
||||
"drinking_water": {
|
||||
"tag_filter": "amenity=drinking_water",
|
||||
# "important_tags": ["drinking_water", "bottle"]
|
||||
},
|
||||
"power_pole": {
|
||||
"tag_filter": "power=pole",
|
||||
# "important_tags": ["ref", "operator"]
|
||||
},
|
||||
"manhole": {
|
||||
"tag_filter": "man_made=manhole",
|
||||
# "important_tags": ["man_made", "substance"]
|
||||
},
|
||||
"little_free_library": {
|
||||
"tag_filter": "amenity=public_bookcase",
|
||||
# "important_tags": ["amenity", "capacity"]
|
||||
},
|
||||
"playground": {
|
||||
"tag_filter": "leisure=playground",
|
||||
# "important_tags": ["name", "surface"]
|
||||
},
|
||||
"siret": {
|
||||
"tag_filter": "ref:FR:SIRET",
|
||||
# "important_tags": ["name", "surface"]
|
||||
},
|
||||
"restaurants": {
|
||||
"tag_filter": "amenity=restaurant",
|
||||
"important_tags": ["opening_hours", "contact:street", "contact:housenumber", "website", "contact:phone"]
|
||||
},
|
||||
"rnb": {
|
||||
"tag_filter": "ref:FR:RNB",
|
||||
# "important_tags": ["name", "surface"]
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def run_command(command):
|
||||
"""Exécute une commande shell et retourne la sortie"""
|
||||
print(f"Exécution: {command}")
|
||||
try:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
shell=True,
|
||||
check=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
universal_newlines=True,
|
||||
)
|
||||
return result.stdout
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Erreur lors de l'exécution de la commande: {e}")
|
||||
print(f"Sortie de la commande: {e.stdout}")
|
||||
print(f"Erreur de la commande: {e.stderr}")
|
||||
return None
|
||||
|
||||
|
||||
@lru_cache(maxsize=128)
|
||||
def run_command_cached(command):
|
||||
"""Version mise en cache de run_command pour éviter les appels redondants"""
|
||||
return run_command(command)
|
||||
|
||||
|
||||
def count_objects_at_date(
|
||||
input_file, date_str, tag_filter, output_dir=None, insee_code=None
|
||||
):
|
||||
"""
|
||||
Compte les objets correspondant à un filtre de tag à une date spécifique.
|
||||
|
||||
Args:
|
||||
input_file: Fichier d'historique OSM (.osh.pbf)
|
||||
date_str: Date au format ISO (YYYY-MM-DDThh:mm:ssZ)
|
||||
tag_filter: Filtre de tag (ex: "amenity=charging_station")
|
||||
output_dir: Répertoire de sortie pour les fichiers temporaires
|
||||
insee_code: Code INSEE de la commune (optionnel)
|
||||
|
||||
Returns:
|
||||
Nombre d'objets correspondant au filtre à la date spécifiée
|
||||
"""
|
||||
# Créer un répertoire temporaire si nécessaire
|
||||
if output_dir:
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
insee_suffix = f"_insee_{insee_code}" if insee_code else ""
|
||||
temp_file = os.path.join(
|
||||
output_dir, f"temp_{date_str.replace(':', '_')}__{insee_suffix}.osm.pbf"
|
||||
)
|
||||
|
||||
# Vérifier si le fichier temporaire existe déjà
|
||||
if not os.path.exists(temp_file):
|
||||
time_filter_cmd = f"osmium time-filter {input_file} {date_str} -O -o {temp_file} -f osm.pbf"
|
||||
run_command(time_filter_cmd)
|
||||
|
||||
tags_count_cmd = f"osmium tags-count {temp_file} -F osm.pbf {tag_filter}"
|
||||
else:
|
||||
# Utiliser des pipes comme dans l'exemple
|
||||
tags_count_cmd = f"osmium time-filter {input_file} {date_str} -O -o - -f osm.pbf | osmium tags-count - -F osm.pbf {tag_filter}"
|
||||
|
||||
# Exécuter la commande et récupérer le résultat (utiliser la version mise en cache)
|
||||
output = run_command_cached(tags_count_cmd)
|
||||
|
||||
# Analyser la sortie pour obtenir le nombre d'objets
|
||||
if output:
|
||||
# La sortie d'osmium tags-count est au format "count "key" "value"" ou "count "key""
|
||||
# Par exemple: "42 "amenity" "charging_station"" ou "42 "operator""
|
||||
parts = output.strip().split()
|
||||
if len(parts) >= 1:
|
||||
try:
|
||||
return int(parts[0])
|
||||
except ValueError:
|
||||
print(f"Impossible de convertir '{parts[0]}' en entier")
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def export_to_geojson(
|
||||
input_file, date_str, main_tag_filter, output_dir, insee_code=None
|
||||
):
|
||||
"""
|
||||
Exporte les données OSM filtrées à une date spécifique vers un fichier GeoJSON.
|
||||
Si le fichier GeoJSON ou les fichiers temporaires existent déjà, l'export est ignoré.
|
||||
|
||||
Args:
|
||||
input_file: Fichier d'historique OSM (.osh.pbf)
|
||||
date_str: Date au format ISO (YYYY-MM-DDThh:mm:ssZ)
|
||||
main_tag_filter: Filtre de tag principal (ex: "amenity=charging_station")
|
||||
output_dir: Répertoire pour les fichiers temporaires
|
||||
insee_code: Code INSEE de la commune (optionnel)
|
||||
|
||||
Returns:
|
||||
Chemin vers le fichier GeoJSON créé
|
||||
"""
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# Ajouter le code INSEE au nom du fichier si disponible
|
||||
insee_suffix = f"_insee_{insee_code}" if insee_code else ""
|
||||
|
||||
# Définir le chemin du fichier GeoJSON de sortie
|
||||
geojson_file = os.path.join(
|
||||
output_dir, f"export_{date_str.replace(':', '_')}__{insee_suffix}.geojson"
|
||||
)
|
||||
|
||||
# Vérifier si le fichier GeoJSON existe déjà
|
||||
if os.path.exists(geojson_file):
|
||||
return geojson_file
|
||||
|
||||
# Définir les chemins des fichiers temporaires
|
||||
temp_file = os.path.join(
|
||||
output_dir, f"temp_{date_str.replace(':', '_')}__{insee_suffix}.osm.pbf"
|
||||
)
|
||||
filtered_file = os.path.join(
|
||||
output_dir,
|
||||
f"filtered_{date_str.replace(':', '_')}__{main_tag_filter.replace('=', '_').replace(' ', '_')}__{insee_suffix}.osm.pbf",
|
||||
)
|
||||
|
||||
# Vérifier si le fichier temporaire filtré par date existe déjà
|
||||
if not os.path.exists(temp_file):
|
||||
# Créer un fichier temporaire pour les données filtrées par date
|
||||
time_filter_cmd = (
|
||||
f"osmium time-filter {input_file} {date_str} -O -o {temp_file} -f osm.pbf"
|
||||
)
|
||||
run_command_cached(time_filter_cmd)
|
||||
|
||||
# Vérifier si le fichier filtré par tag existe déjà
|
||||
if not os.path.exists(filtered_file):
|
||||
# Filtrer les objets qui ont le tag principal
|
||||
filter_cmd = f"osmium tags-filter {temp_file} {main_tag_filter} -f osm.pbf -O -o {filtered_file}"
|
||||
run_command_cached(filter_cmd)
|
||||
|
||||
# Exporter vers GeoJSON
|
||||
export_cmd = f"osmium export {filtered_file} -O -o {geojson_file} -f geojson --geometry-types point,linestring,polygon"
|
||||
run_command_cached(export_cmd)
|
||||
|
||||
return geojson_file
|
||||
|
||||
|
||||
def count_features_with_tags_in_geojson(geojson_file, attribute_tags):
|
||||
"""
|
||||
Compte les features dans un fichier GeoJSON qui ont des attributs spécifiques.
|
||||
|
||||
Args:
|
||||
geojson_file: Chemin vers le fichier GeoJSON
|
||||
attribute_tags: Liste d'attributs à vérifier (ex: ["operator", "capacity"])
|
||||
|
||||
Returns:
|
||||
Dictionnaire avec le nombre de features ayant chaque attribut spécifié
|
||||
"""
|
||||
try:
|
||||
with open(geojson_file, "r") as f:
|
||||
geojson_data = json.load(f)
|
||||
|
||||
# Initialiser les compteurs pour chaque attribut
|
||||
counts = {tag: 0 for tag in attribute_tags}
|
||||
|
||||
# Compter en une seule passe
|
||||
for feature in geojson_data.get("features", []):
|
||||
properties = feature.get("properties", {})
|
||||
for tag in attribute_tags:
|
||||
if tag in properties and properties[tag]:
|
||||
counts[tag] += 1
|
||||
|
||||
return counts
|
||||
except Exception as e:
|
||||
print(
|
||||
f"Erreur lors du comptage des features avec les attributs {attribute_tags}: {e}"
|
||||
)
|
||||
return {tag: None for tag in attribute_tags}
|
||||
|
||||
|
||||
def count_objects_with_tags(
|
||||
input_file,
|
||||
date_str,
|
||||
main_tag_filter,
|
||||
attribute_tags,
|
||||
output_dir=None,
|
||||
insee_code=None,
|
||||
):
|
||||
"""
|
||||
Compte les objets qui ont à la fois le tag principal et des attributs spécifiques.
|
||||
|
||||
Args:
|
||||
input_file: Fichier d'historique OSM (.osh.pbf)
|
||||
date_str: Date au format ISO (YYYY-MM-DDThh:mm:ssZ)
|
||||
main_tag_filter: Filtre de tag principal (ex: "amenity=charging_station")
|
||||
attribute_tags: Liste d'attributs à vérifier (ex: ["operator", "capacity"])
|
||||
output_dir: Répertoire pour les fichiers temporaires
|
||||
insee_code: Code INSEE de la commune (optionnel)
|
||||
|
||||
Returns:
|
||||
Dictionnaire avec le nombre d'objets ayant chaque attribut spécifié
|
||||
"""
|
||||
# Utiliser l'export GeoJSON si un répertoire de sortie est spécifié
|
||||
if output_dir:
|
||||
# Exporter vers GeoJSON (la fonction export_to_geojson vérifie déjà si le fichier GeoJSON existe)
|
||||
geojson_file = export_to_geojson(
|
||||
input_file, date_str, main_tag_filter, output_dir, insee_code
|
||||
)
|
||||
|
||||
# Compter les features avec les attributs spécifiques en une seule passe
|
||||
return count_features_with_tags_in_geojson(geojson_file, attribute_tags)
|
||||
else:
|
||||
# Méthode alternative si pas de répertoire temporaire
|
||||
counts = {}
|
||||
for tag in attribute_tags:
|
||||
combined_filter = f"{main_tag_filter} and {tag}"
|
||||
counts[tag] = count_objects_at_date(
|
||||
input_file, date_str, combined_filter, output_dir, insee_code
|
||||
)
|
||||
return counts
|
||||
|
||||
|
||||
def count_objects_with_tag(
|
||||
input_file,
|
||||
date_str,
|
||||
main_tag_filter,
|
||||
attribute_tag,
|
||||
output_dir=None,
|
||||
insee_code=None,
|
||||
):
|
||||
"""
|
||||
Compte les objets qui ont à la fois le tag principal et un attribut spécifique.
|
||||
Version compatible avec l'ancienne API pour la rétrocompatibilité.
|
||||
|
||||
Args:
|
||||
input_file: Fichier d'historique OSM (.osh.pbf)
|
||||
date_str: Date au format ISO (YYYY-MM-DDThh:mm:ssZ)
|
||||
main_tag_filter: Filtre de tag principal (ex: "amenity=charging_station")
|
||||
attribute_tag: Attribut à vérifier (ex: "operator")
|
||||
output_dir: Répertoire pour les fichiers temporaires
|
||||
insee_code: Code INSEE de la commune (optionnel)
|
||||
|
||||
Returns:
|
||||
Nombre d'objets ayant à la fois le tag principal et l'attribut spécifié
|
||||
"""
|
||||
result = count_objects_with_tags(
|
||||
input_file, date_str, main_tag_filter, [attribute_tag], output_dir, insee_code
|
||||
)
|
||||
return result.get(attribute_tag, None)
|
||||
|
||||
|
||||
def generate_time_slices(max_dates=None):
|
||||
"""
|
||||
Génère une liste de dates avec différentes fréquences selon l'ancienneté:
|
||||
- 30 derniers jours: quotidien
|
||||
- 12 derniers mois: mensuel
|
||||
- Jusqu'à 2004: annuel
|
||||
|
||||
Args:
|
||||
max_dates: Nombre maximum de dates à générer (pour les tests)
|
||||
|
||||
Returns:
|
||||
Liste de dates au format ISO (YYYY-MM-DDThh:mm:ssZ)
|
||||
"""
|
||||
dates = []
|
||||
now = datetime.now()
|
||||
|
||||
# 1. Dates quotidiennes pour les 30 derniers jours
|
||||
current_date = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
for _ in range(30):
|
||||
date_str = current_date.strftime("%Y-%m-%dT00:00:00Z")
|
||||
dates.append(date_str)
|
||||
current_date -= timedelta(days=1)
|
||||
|
||||
# 2. Dates mensuelles pour les 12 derniers mois (en excluant le mois courant déjà couvert)
|
||||
current_date = datetime(now.year, now.month, 1) - timedelta(
|
||||
days=1
|
||||
) # Dernier jour du mois précédent
|
||||
current_date = datetime(
|
||||
current_date.year, current_date.month, 1
|
||||
) # Premier jour du mois précédent
|
||||
|
||||
for _ in range(
|
||||
60
|
||||
): # 11 mois supplémentaires (12 mois au total avec le mois courant)
|
||||
date_str = current_date.strftime("%Y-%m-%dT00:00:00Z")
|
||||
if date_str not in dates: # Éviter les doublons
|
||||
dates.append(date_str)
|
||||
|
||||
# Passer au mois précédent
|
||||
if current_date.month == 1:
|
||||
current_date = datetime(current_date.year - 1, 12, 1)
|
||||
else:
|
||||
current_date = datetime(current_date.year, current_date.month - 1, 1)
|
||||
|
||||
# 3. Dates annuelles de l'année précédente jusqu'à 2004
|
||||
# Amélioration: deux mesures par an (1er janvier et 1er juillet) pour les périodes éloignées
|
||||
start_year = min(
|
||||
now.year - 1, 2023
|
||||
) # Commencer à l'année précédente (ou 2023 si nous sommes en 2024)
|
||||
for year in range(start_year, 2003, -1):
|
||||
# Ajouter le 1er janvier
|
||||
date_str_jan = f"{year}-01-01T00:00:00Z"
|
||||
if date_str_jan not in dates: # Éviter les doublons
|
||||
dates.append(date_str_jan)
|
||||
|
||||
# Ajouter le 1er juillet
|
||||
date_str_jul = f"{year}-07-01T00:00:00Z"
|
||||
if date_str_jul not in dates: # Éviter les doublons
|
||||
dates.append(date_str_jul)
|
||||
|
||||
# Limiter le nombre de dates si spécifié
|
||||
if max_dates is not None and max_dates > 0:
|
||||
dates = dates[:max_dates]
|
||||
|
||||
# Trier les dates par ordre chronologique
|
||||
dates.sort()
|
||||
|
||||
return dates
|
||||
|
||||
|
||||
def extract_zone_data(input_file, poly_file, output_dir):
|
||||
"""
|
||||
Extrait les données pour une zone spécifique à partir d'un fichier d'historique OSM.
|
||||
|
||||
Args:
|
||||
input_file: Fichier d'historique OSM (.osh.pbf)
|
||||
poly_file: Fichier de polygone (.poly) définissant la zone
|
||||
output_dir: Répertoire de sortie
|
||||
|
||||
Returns:
|
||||
Chemin vers le fichier extrait
|
||||
"""
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# Obtenir le nom de la zone à partir du nom du fichier poly
|
||||
zone_name = Path(poly_file).stem
|
||||
|
||||
# Créer le fichier de sortie
|
||||
output_file = os.path.join(output_dir, f"{zone_name}.osh.pbf")
|
||||
|
||||
# Exécuter la commande osmium extract seulement si le fichier n'existe pas déjà
|
||||
if os.path.exists(output_file):
|
||||
print(f"Le fichier {output_file} existe déjà, utilisation du fichier existant.")
|
||||
else:
|
||||
print(f"Extraction des données pour la zone {zone_name}...")
|
||||
command = f"osmium extract -p {poly_file} -H {input_file} -O -o {output_file}"
|
||||
run_command(command)
|
||||
|
||||
return output_file
|
||||
|
||||
|
||||
def process_theme(
|
||||
theme_name,
|
||||
theme_info,
|
||||
zone_file,
|
||||
zone_name,
|
||||
dates,
|
||||
output_dir,
|
||||
temp_dir,
|
||||
insee_code=None,
|
||||
):
|
||||
"""
|
||||
Traite une thématique spécifique pour une zone donnée.
|
||||
|
||||
Args:
|
||||
theme_name: Nom de la thématique
|
||||
theme_info: Informations sur la thématique (tag_filter, important_tags)
|
||||
zone_file: Fichier de la zone à traiter
|
||||
zone_name: Nom de la zone
|
||||
dates: Liste des dates à traiter
|
||||
output_dir: Répertoire pour les fichiers de sortie
|
||||
temp_dir: Répertoire pour les fichiers temporaires
|
||||
insee_code: Code INSEE de la commune (optionnel)
|
||||
|
||||
Returns:
|
||||
Chemin vers le fichier CSV généré
|
||||
"""
|
||||
start_time = time.time()
|
||||
print(f"Traitement de la thématique '{theme_name}' pour la zone '{zone_name}'...")
|
||||
|
||||
# Préparer le fichier CSV de sortie
|
||||
csv_file = os.path.join(output_dir, f"{zone_name}_{theme_name}.csv")
|
||||
|
||||
# Entêtes du CSV - colonnes de base
|
||||
headers = ["date", "zone", "theme", "nombre_total"]
|
||||
|
||||
# Ajouter une colonne pour chaque tag important
|
||||
# Vérifier si la clé 'important_tags' existe, sinon utiliser une liste vide
|
||||
important_tags = theme_info.get("important_tags", [])
|
||||
for attr in important_tags:
|
||||
headers.append(f"nombre_avec_{attr}")
|
||||
|
||||
# Ajouter la colonne de pourcentage de complétion
|
||||
headers.append("pourcentage_completion")
|
||||
|
||||
# Vérifier si le fichier CSV existe déjà et lire les données existantes
|
||||
existing_dates = set()
|
||||
existing_rows = []
|
||||
existing_data = {} # Dictionnaire pour stocker les données existantes par date
|
||||
file_exists = os.path.exists(csv_file)
|
||||
|
||||
if file_exists:
|
||||
try:
|
||||
with open(csv_file, "r", newline="") as f:
|
||||
reader = csv.reader(f)
|
||||
existing_headers = next(reader) # Lire les entêtes
|
||||
|
||||
# Vérifier que les entêtes correspondent
|
||||
if existing_headers == headers:
|
||||
for row in reader:
|
||||
if len(row) >= 1: # S'assurer que la ligne a au moins une date
|
||||
date = row[0] # La date est dans la première colonne
|
||||
existing_dates.add(date)
|
||||
existing_rows.append(row)
|
||||
|
||||
# Stocker les données existantes dans un dictionnaire pour un accès facile
|
||||
existing_data[date] = row
|
||||
else:
|
||||
print(f"Les entêtes du fichier existant ne correspondent pas, création d'un nouveau fichier.")
|
||||
file_exists = False
|
||||
except Exception as e:
|
||||
print(f"Erreur lors de la lecture du fichier CSV existant: {e}")
|
||||
file_exists = False
|
||||
|
||||
# Filtrer les dates qui n'ont pas encore été traitées
|
||||
dates_to_process = [date_str for date_str in dates if date_str.split("T")[0] not in existing_dates]
|
||||
|
||||
# Mode d'ouverture du fichier (écriture ou ajout)
|
||||
mode = "a" if file_exists else "w"
|
||||
|
||||
with open(csv_file, mode, newline="") as f:
|
||||
writer = csv.writer(f)
|
||||
|
||||
# Écrire les entêtes si c'est un nouveau fichier
|
||||
if not file_exists:
|
||||
writer.writerow(headers)
|
||||
|
||||
# Traiter chaque date qui n'a pas encore été traitée
|
||||
for date_str in dates_to_process:
|
||||
tag_filter = theme_info["tag_filter"]
|
||||
|
||||
# Compter le nombre total d'objets
|
||||
total_count = count_objects_at_date(
|
||||
zone_file, date_str, tag_filter, temp_dir, insee_code
|
||||
)
|
||||
|
||||
# Compter les objets avec chaque attribut important en une seule passe
|
||||
# Vérifier si la clé 'important_tags' existe et n'est pas vide
|
||||
important_tags = theme_info.get("important_tags", [])
|
||||
if important_tags:
|
||||
attr_counts_dict = count_objects_with_tags(
|
||||
zone_file,
|
||||
date_str,
|
||||
tag_filter,
|
||||
important_tags,
|
||||
temp_dir,
|
||||
insee_code,
|
||||
)
|
||||
attr_counts = [
|
||||
(attr, attr_counts_dict.get(attr, 0)) for attr in important_tags
|
||||
]
|
||||
else:
|
||||
attr_counts = []
|
||||
|
||||
# Formater la date pour le CSV (YYYY-MM-DD)
|
||||
csv_date = date_str.split("T")[0]
|
||||
|
||||
# Vérifier si le total_count est 0 et s'il était > 0 dans le passé
|
||||
recalculate = False
|
||||
if total_count == 0:
|
||||
# Parcourir les données existantes pour voir si une valeur était > 0 dans le passé
|
||||
was_greater_than_zero = False
|
||||
for existing_date in sorted(existing_data.keys()):
|
||||
if existing_date >= csv_date:
|
||||
break # Ne pas regarder les dates futures
|
||||
|
||||
existing_row = existing_data[existing_date]
|
||||
if len(existing_row) >= 4: # S'assurer que la ligne a une valeur de nombre_total
|
||||
try:
|
||||
existing_total = existing_row[3]
|
||||
if existing_total and existing_total != "" and int(existing_total) > 0:
|
||||
was_greater_than_zero = True
|
||||
break
|
||||
except (ValueError, TypeError):
|
||||
# Ignorer les valeurs qui ne peuvent pas être converties en entier
|
||||
pass
|
||||
|
||||
if was_greater_than_zero:
|
||||
# Si une valeur était > 0 dans le passé et est maintenant 0, remplacer par une valeur vide
|
||||
total_count = ""
|
||||
print(f"Valeur passée de > 0 à 0 pour {theme_name} à la date {csv_date}, remplacée par une valeur vide.")
|
||||
|
||||
# Relancer le calcul osmium
|
||||
recalculate = True
|
||||
|
||||
# Vérifier également pour chaque attribut important
|
||||
for i, (attr, count) in enumerate(attr_counts):
|
||||
if count == 0:
|
||||
# Parcourir les données existantes pour voir si une valeur était > 0 dans le passé
|
||||
was_greater_than_zero = False
|
||||
for existing_date in sorted(existing_data.keys()):
|
||||
if existing_date >= csv_date:
|
||||
break # Ne pas regarder les dates futures
|
||||
|
||||
existing_row = existing_data[existing_date]
|
||||
if len(existing_row) >= 4 + i + 1: # S'assurer que la ligne a une valeur pour cet attribut
|
||||
try:
|
||||
existing_attr_count = existing_row[4 + i]
|
||||
if existing_attr_count and existing_attr_count != "" and int(existing_attr_count) > 0:
|
||||
was_greater_than_zero = True
|
||||
break
|
||||
except (ValueError, TypeError):
|
||||
# Ignorer les valeurs qui ne peuvent pas être converties en entier
|
||||
pass
|
||||
|
||||
if was_greater_than_zero:
|
||||
# Si une valeur était > 0 dans le passé et est maintenant 0, remplacer par une valeur vide
|
||||
attr_counts[i] = (attr, "")
|
||||
print(f"Valeur de {attr} passée de > 0 à 0 pour {theme_name} à la date {csv_date}, remplacée par une valeur vide.")
|
||||
|
||||
# Relancer le calcul osmium
|
||||
recalculate = True
|
||||
|
||||
# Si on doit recalculer, relancer le calcul osmium
|
||||
if recalculate:
|
||||
print(f"Relancement du calcul osmium pour {theme_name} à la date {csv_date}...")
|
||||
# Supprimer les fichiers temporaires pour forcer un recalcul
|
||||
temp_file_pattern = os.path.join(temp_dir, f"temp_{date_str.replace(':', '_')}__*")
|
||||
run_command(f"rm -f {temp_file_pattern}")
|
||||
|
||||
# Recalculer le nombre total d'objets
|
||||
if total_count == "":
|
||||
total_count = count_objects_at_date(
|
||||
zone_file, date_str, tag_filter, temp_dir, insee_code
|
||||
)
|
||||
|
||||
# Recalculer les objets avec chaque attribut important
|
||||
if important_tags:
|
||||
attr_counts_dict = count_objects_with_tags(
|
||||
zone_file,
|
||||
date_str,
|
||||
tag_filter,
|
||||
important_tags,
|
||||
temp_dir,
|
||||
insee_code,
|
||||
)
|
||||
attr_counts = [
|
||||
(attr, attr_counts_dict.get(attr, 0)) for attr in important_tags
|
||||
]
|
||||
|
||||
# Calculer le pourcentage de complétion
|
||||
if total_count is not None and total_count != "" and total_count > 0 and len(attr_counts) > 0:
|
||||
# Filtrer les comptages None ou vides avant de calculer la moyenne
|
||||
valid_counts = [(attr, count) for attr, count in attr_counts if count is not None and count != ""]
|
||||
if valid_counts:
|
||||
# Moyenne des pourcentages de présence de chaque attribut important
|
||||
completion_pct = sum(
|
||||
count / total_count * 100 for _, count in valid_counts
|
||||
) / len(valid_counts)
|
||||
else:
|
||||
completion_pct = 0
|
||||
else:
|
||||
completion_pct = 0
|
||||
|
||||
# Préparer la ligne CSV avec les colonnes de base
|
||||
# Si le comptage total a échoué (None), ajouter une chaîne vide au lieu de 0
|
||||
row = [csv_date, zone_name, theme_name, "" if total_count is None else total_count]
|
||||
|
||||
# Ajouter les compteurs pour chaque attribut important
|
||||
for attr, count in attr_counts:
|
||||
# Si le comptage a échoué (None), ajouter une chaîne vide au lieu de 0
|
||||
row.append("" if count is None else count)
|
||||
|
||||
# Ajouter le pourcentage de complétion
|
||||
row.append(round(completion_pct, 2))
|
||||
|
||||
# Écrire la ligne dans le CSV
|
||||
writer.writerow(row)
|
||||
|
||||
# Si aucune nouvelle date n'a été traitée, afficher un message
|
||||
if not dates_to_process:
|
||||
print(f"Toutes les dates pour la thématique '{theme_name}' sont déjà traitées dans le fichier CSV existant.")
|
||||
|
||||
print(f"Résultats sauvegardés dans {csv_file}")
|
||||
|
||||
# Générer un graphique pour cette thématique
|
||||
generate_graph(csv_file, zone_name, theme_name)
|
||||
|
||||
end_time = time.time()
|
||||
print(f"Thématique '{theme_name}' traitée en {end_time - start_time:.2f} secondes")
|
||||
|
||||
return csv_file
|
||||
|
||||
|
||||
def cleanup_temp_files(temp_dir, keep_zone_files=True):
|
||||
"""
|
||||
Nettoie les fichiers temporaires dans le répertoire spécifié.
|
||||
|
||||
Args:
|
||||
temp_dir: Répertoire contenant les fichiers temporaires
|
||||
keep_zone_files: Si True, conserve les fichiers de zone extraits (.osh.pbf)
|
||||
"""
|
||||
print(f"Nettoyage des fichiers temporaires dans {temp_dir}...")
|
||||
count = 0
|
||||
|
||||
for file in os.listdir(temp_dir):
|
||||
file_path = os.path.join(temp_dir, file)
|
||||
|
||||
# Conserver les fichiers de zone extraits si demandé
|
||||
if (
|
||||
keep_zone_files
|
||||
and file.endswith(".osh.pbf")
|
||||
and not file.startswith("temp_")
|
||||
and not file.startswith("filtered_")
|
||||
):
|
||||
continue
|
||||
|
||||
# Supprimer les fichiers temporaires
|
||||
if (
|
||||
file.startswith("temp_")
|
||||
or file.startswith("filtered_")
|
||||
or file.startswith("export_")
|
||||
):
|
||||
try:
|
||||
os.remove(file_path)
|
||||
count += 1
|
||||
except Exception as e:
|
||||
print(f"Erreur lors de la suppression du fichier {file_path}: {e}")
|
||||
|
||||
print(f"{count} fichiers temporaires supprimés.")
|
||||
|
||||
|
||||
def process_zone(
|
||||
input_file, poly_file, output_dir, temp_dir, max_dates=None, cleanup=False
|
||||
):
|
||||
"""
|
||||
Traite une zone spécifique pour toutes les thématiques en parallèle.
|
||||
|
||||
Args:
|
||||
input_file: Fichier d'historique OSM (.osh.pbf)
|
||||
poly_file: Fichier de polygone (.poly) définissant la zone
|
||||
output_dir: Répertoire pour les fichiers de sortie
|
||||
temp_dir: Répertoire pour les fichiers temporaires
|
||||
max_dates: Nombre maximum de dates à traiter (pour les tests)
|
||||
cleanup: Si True, nettoie les fichiers temporaires après traitement
|
||||
"""
|
||||
start_time = time.time()
|
||||
|
||||
# Créer les répertoires nécessaires
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
|
||||
# Obtenir le nom de la zone à partir du nom du fichier poly
|
||||
zone_name = Path(poly_file).stem
|
||||
|
||||
# Extraire le code INSEE à partir du nom du fichier poly (format: commune_XXXXX.poly)
|
||||
insee_code = None
|
||||
if zone_name.startswith("commune_"):
|
||||
insee_code = zone_name.replace("commune_", "")
|
||||
|
||||
# Extraire les données pour la zone
|
||||
zone_file = extract_zone_data(input_file, poly_file, temp_dir)
|
||||
|
||||
# Générer les dates avec différentes fréquences selon l'ancienneté
|
||||
dates = generate_time_slices(max_dates) # Limité par max_dates si spécifié
|
||||
|
||||
print(f"Traitement de {len(THEMES)} thématiques pour la zone '{zone_name}'...")
|
||||
|
||||
# Déterminer le nombre de processus à utiliser (nombre de cœurs disponibles - 1, minimum 1)
|
||||
num_processes = max(1, multiprocessing.cpu_count() - 1)
|
||||
print(f"Utilisation de {num_processes} processus pour le traitement parallèle")
|
||||
|
||||
# Créer un pool de processus
|
||||
with multiprocessing.Pool(processes=num_processes) as pool:
|
||||
# Préparer les arguments pour chaque thématique
|
||||
theme_args = [
|
||||
(
|
||||
theme_name,
|
||||
theme_info,
|
||||
zone_file,
|
||||
zone_name,
|
||||
dates,
|
||||
output_dir,
|
||||
temp_dir,
|
||||
insee_code,
|
||||
)
|
||||
for theme_name, theme_info in THEMES.items()
|
||||
]
|
||||
|
||||
# Exécuter le traitement des thématiques en parallèle
|
||||
pool.starmap(process_theme, theme_args)
|
||||
|
||||
# Nettoyer les fichiers temporaires si demandé
|
||||
if cleanup:
|
||||
cleanup_temp_files(temp_dir, keep_zone_files=True)
|
||||
|
||||
end_time = time.time()
|
||||
total_time = end_time - start_time
|
||||
print(f"Traitement de la zone '{zone_name}' terminé en {total_time:.2f} secondes")
|
||||
|
||||
return total_time
|
||||
|
||||
|
||||
def generate_graph(csv_file, zone_name, theme_name):
|
||||
"""
|
||||
Génère un graphique à partir des données CSV.
|
||||
|
||||
Args:
|
||||
csv_file: Fichier CSV contenant les données
|
||||
zone_name: Nom de la zone
|
||||
theme_name: Nom de la thématique
|
||||
"""
|
||||
# Vérifier si le script generate_graph.py existe
|
||||
if os.path.exists(os.path.join(os.path.dirname(__file__), "generate_graph.py")):
|
||||
# Construire le chemin de sortie pour le graphique
|
||||
output_path = os.path.splitext(csv_file)[0] + "_graph.png"
|
||||
|
||||
# Exécuter le script pour générer le graphique
|
||||
command = f"python3 {os.path.join(os.path.dirname(__file__), 'generate_graph.py')} {csv_file} --output {output_path}"
|
||||
run_command(command)
|
||||
print(f"Graphique généré: {output_path}")
|
||||
else:
|
||||
print(
|
||||
"Le script generate_graph.py n'a pas été trouvé. Aucun graphique n'a été généré."
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
"""Fonction principale"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Compte les objets OSM par thématique sur une zone donnée à différentes dates."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--input", "-i", required=True, help="Fichier d'historique OSM (.osh.pbf)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--poly",
|
||||
"-p",
|
||||
required=True,
|
||||
help="Fichier de polygone (.poly) définissant la zone",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-dir",
|
||||
"-o",
|
||||
default="resultats",
|
||||
help="Répertoire pour les fichiers de sortie",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--temp-dir",
|
||||
"-t",
|
||||
default="temp",
|
||||
help="Répertoire pour les fichiers temporaires",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-dates",
|
||||
"-m",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Nombre maximum de dates à traiter (pour les tests, par défaut: toutes les dates)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--cleanup",
|
||||
"-c",
|
||||
action="store_true",
|
||||
help="Nettoyer les fichiers temporaires après traitement",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--benchmark",
|
||||
"-b",
|
||||
action="store_true",
|
||||
help="Afficher des informations de performance détaillées",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Vérifier que les fichiers d'entrée existent
|
||||
if not os.path.isfile(args.input):
|
||||
print(f"Erreur: Le fichier d'entrée {args.input} n'existe pas.")
|
||||
sys.exit(1)
|
||||
|
||||
if not os.path.isfile(args.poly):
|
||||
print(f"Erreur: Le fichier de polygone {args.poly} n'existe pas.")
|
||||
sys.exit(1)
|
||||
|
||||
# Afficher des informations sur la configuration
|
||||
if args.benchmark:
|
||||
print("\n=== Configuration ===")
|
||||
print(f"Nombre de processeurs: {multiprocessing.cpu_count()}")
|
||||
print(f"Nombre de thématiques: {len(THEMES)}")
|
||||
print(
|
||||
f"Nettoyage des fichiers temporaires: {'Activé' if args.cleanup else 'Désactivé'}"
|
||||
)
|
||||
print("=====================\n")
|
||||
|
||||
# Mesurer le temps d'exécution
|
||||
start_time = time.time()
|
||||
|
||||
# Traiter la zone
|
||||
total_time = process_zone(
|
||||
args.input,
|
||||
args.poly,
|
||||
args.output_dir,
|
||||
args.temp_dir,
|
||||
args.max_dates,
|
||||
args.cleanup,
|
||||
)
|
||||
|
||||
# Afficher des informations de performance
|
||||
if args.benchmark:
|
||||
print("\n=== Performance ===")
|
||||
print(f"Temps total d'exécution: {total_time:.2f} secondes")
|
||||
print(f"Temps moyen par thématique: {total_time / len(THEMES):.2f} secondes")
|
||||
print("===================\n")
|
||||
|
||||
print("Traitement terminé.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -1,194 +0,0 @@
|
|||
zone,name,lat,lon,population,budgetAnnuel,completionPercent,placesCount,avecHoraires,avecAdresse,avecSite,avecAccessibilite,avecNote,siren,codeEpci,codesPostaux
|
||||
91111,Briis-sous-Forges,48.6246916,2.1243349,3375,4708125.00,30,67,12,8,9,7,3,219101110,249100074,91640
|
||||
79034,Bessines,46.3020750,-0.5167969,1882,2676204.00,58,56,36,26,34,4,1,217900349,200041317,79000
|
||||
76216,Déville-lès-Rouen,49.4695338,1.0495889,10690,18386800.00,20,110,16,3,11,1,1,217602168,200023414,76250
|
||||
91249,Forges-les-Bains,48.6290401,2.0996273,4138,6310450.00,33,31,7,5,2,5,0,219102498,249100074,91470
|
||||
59140,Caullery,50.0817000,3.3737000,464,824528.00,,,,,,,,,,
|
||||
08122,Chooz,50.0924000,4.7996000,792,1375704.00,,,,,,,,,,
|
||||
12084,Creissels,44.0621000,3.0572000,1564,2493016.00,,,,,,,,,,
|
||||
59183,Dunkerque,51.0183000,2.3431000,87013,153229893.00,,,,,,,,,,
|
||||
86116,Jazeneuil,46.4749000,0.0735000,797,1212237.00,,,,,,,,,,
|
||||
75113,,48.8303000,2.3656000,177735,239764515.00,,,,,,,,,,
|
||||
06088,Nice,43.7032000,7.2528000,353701,557079075.00,,,,,,,,,,
|
||||
38185,Grenoble,45.1842000,5.7155000,156389,231299331.00,,,,,,,,,,
|
||||
75117,,48.8874000,2.3050000,161206,231975434.00,,,,,,,,,,
|
||||
75116,,48.8572000,2.2630000,159733,287199934.00,,,,,,,,,,
|
||||
35236,Redon,47.6557000,-2.0787000,9336,15030960.00,,,,,,,,,,
|
||||
35238,Rennes,48.1159000,-1.6884000,227830,408727020.00,,,,,,,,,,
|
||||
78646,Versailles,48.8039000,2.1191000,83918,142912354.00,,,,,,,,,,
|
||||
12230,Saint-Jean-Delnous,44.0391000,2.4912000,382,665062.00,,,,,,,,,,
|
||||
76193,"La Crique",49.6929000,1.2051000,367,651425.00,,,,,,,,,,
|
||||
49007,Angers,47.4819000,-0.5629000,157555,207815045.00,,,,,,,,,,
|
||||
79003,Aiffres,46.2831000,-0.4147000,5423,9631248.00,,,,,,,,,,
|
||||
29151,Morlaix,48.5971000,-3.8215000,15220,22129880.00,,,,,,,,,,
|
||||
38544,Vienne,45.5221000,4.8803000,31555,55252805.00,,,,,,,,,,
|
||||
42100,"La Gimond",45.5551000,4.4144000,278,428676.00,,,,,,,,,,
|
||||
76008,Ancourt,49.9110000,1.1835000,627,850839.00,,,,,,,,,,
|
||||
76618,Petit-Caux,49.9612000,1.2343000,9626,15334218.00,,,,,,,,,,
|
||||
13202,Marseille,43.3225000,5.3497000,24153,40842723.00,,,,,,,,,,
|
||||
46102,Figeac,44.6067000,2.0231000,9757,17025965.00,,,,,,,,,,
|
||||
75107,,48.8548000,2.3115000,48196,70992708.00,,,,,,,,,,
|
||||
7812,"Les Clayes-sous-Bois",,,,,,,,,,,,,,
|
||||
76192,Criel-sur-Mer,50.0221000,1.3215000,2592,3659904.00,,,,,,,,,,
|
||||
94080,Vincennes,48.8471000,2.4383000,48368,64232704.00,,,,,,,,,,
|
||||
13055,Marseille,43.2803000,5.3806000,877215,1184240250.00,,,,,,,,,,
|
||||
93055,Pantin,48.9006000,2.4085000,60954,90272874.00,,,,,,,,,,
|
||||
69385,,45.7560000,4.8012000,48277,83277825.00,,,,,,,,,,
|
||||
75109,,48.8771000,2.3379000,58419,79625097.00,,,,,,,,,,
|
||||
44184,Saint-Nazaire,47.2768000,-2.2392000,73111,104694952.00,,,,,,,,,,
|
||||
13208,,43.2150000,5.3256000,83414,120866886.00,,,,,,,,,,
|
||||
59271,Grande-Synthe,51.0157000,2.2938000,20347,34284695.00,,,,,,,,,,
|
||||
77379,Provins,48.5629000,3.2845000,11824,20183568.00,,,,,,,,,,
|
||||
75108,,48.8732000,2.3111000,35418,51710280.00,,,,,,,,,,
|
||||
77122,Combs-la-Ville,48.6602000,2.5765000,22712,29661872.00,,,,,,,,,,
|
||||
79298,Saint-Symphorien,46.2688000,-0.4842000,1986,2909490.00,,,,,,,,,,
|
||||
51454,Reims,49.2535000,4.0551000,178478,310908676.00,,,,,,,,,,
|
||||
64102,Bayonne,43.4844000,-1.4611000,53312,82740224.00,,,,,,,,,,
|
||||
39300,Lons-le-Saunier,46.6758000,5.5574000,16942,25379116.00,,,,,,,,,,
|
||||
85288,Talmont-Saint-Hilaire,46.4775000,-1.6299000,8327,10908370.00,,,,,,,,,,
|
||||
13203,,43.3113000,5.3806000,55653,81642951.00,,,,,,,,,,
|
||||
69387,,45.7321000,4.8393000,87491,134998613.00,,,,,,,,,,
|
||||
64024,Anglet,43.4893000,-1.5193000,42288,73665696.00,,,,,,,,,,
|
||||
69265,Ville-sur-Jarnioux,45.9693000,4.5942000,820,1209500.00,,,,,,,,,,
|
||||
75112,,48.8342000,2.4173000,139788,228273804.00,,,,,,,,,,
|
||||
69388,,45.7342000,4.8695000,84956,119703004.00,,,,,,,,,,
|
||||
74010,Annecy,45.9024000,6.1264000,131272,235764512.00,,,,,,,,,,
|
||||
74100,Desingy,46.0022000,5.8870000,759,1032240.00,,,,,,,,,,
|
||||
75114,,48.8297000,2.3230000,137581,190412104.00,,,,,,,,,,
|
||||
75118,,48.8919000,2.3487000,185825,275578475.00,,,,,,,,,,
|
||||
85191,"La Roche-sur-Yon",46.6659000,-1.4162000,54699,80899821.00,,,,,,,,,,
|
||||
85194,"Les Sables-d'Olonne",46.5264000,-1.7611000,48740,85002560.00,,,,,,,,,,
|
||||
12220,Sainte-Eulalie-de-Cernon,43.9605000,3.1550000,321,558540.00,,,,,,,,,,
|
||||
14341,Ifs,49.1444000,-0.3385000,11868,16377840.00,,,,,,,,,,
|
||||
35288,Saint-Malo,48.6465000,-2.0066000,47255,81562130.00,,,,,,,,,,
|
||||
86194,Poitiers,46.5846000,0.3715000,89472,130718592.00,,,,,,,,,,
|
||||
91338,Limours,48.6463000,2.0823000,6408,8676432.00,,,,,,,,,,
|
||||
91640,,,,,,,,,,,,,,,
|
||||
79191,Niort,46.3274000,-0.4613000,60074,92934478.00,,,,,,,,,,
|
||||
06004,Antibes,43.5823000,7.1048000,76612,134147612.00,,,,,,,,,,
|
||||
13207,,43.2796000,5.3274000,34866,53031186.00,,,,,,,,,,
|
||||
91657,Vigneux-sur-Seine,48.7021000,2.4274000,31233,41477424.00,,,,,,,,,,
|
||||
73124,Gilly-sur-Isère,45.6549000,6.3487000,3109,4651064.00,,,,,,,,,,
|
||||
92040,Issy-les-Moulineaux,48.8240000,2.2628000,67695,108108915.00,,,,,,,,,,
|
||||
93051,Noisy-le-Grand,48.8327000,2.5560000,71632,104654352.00,,,,,,,,,,
|
||||
14675,Soliers,49.1315000,-0.2898000,2190,2923650.00,,,,,,,,,,
|
||||
76655,Saint-Valery-en-Caux,49.8582000,0.7094000,3884,5985244.00,,,,,,,,,,
|
||||
34172,Montpellier,43.6100000,3.8742000,307101,508866357.00,,,,,,,,,,
|
||||
76351,"Le Havre",49.4958000,0.1312000,166462,260346568.00,,,,,,,,,,
|
||||
76665,Sauchay,49.9179000,1.2073000,448,618240.00,,,,,,,,,,
|
||||
13204,,43.3063000,5.4002000,49744,75312416.00,,,,,,,,,,
|
||||
75104,Paris,48.8541000,2.3569000,28039,44161425.00,,,,,,,,,,
|
||||
45234,Orléans,47.8734000,1.9122000,116344,178355352.00,,,,,,,,,,
|
||||
13100,Saint-Rémy-de-Provence,43.7815000,4.8455000,9547,16334917.00,,,,,,,,,,
|
||||
27275,Gaillon,49.1602000,1.3405000,6785,11995880.00,,,,,,,,,,
|
||||
44047,Couëron,47.2391000,-1.7472000,23541,31521399.00,,,,,,,,,,
|
||||
16070,Chabanais,45.8656000,0.7076000,1564,2744820.00,,,,,,,,,,
|
||||
12029,Bor-et-Bar,44.1971000,2.0822000,198,304128.00,,,,,,,,,,
|
||||
59155,Coudekerque-Branche,51.0183000,2.3986000,20833,36707746.00,,,,,,,,,,
|
||||
16106,Confolens,46.0245000,0.6639000,2726,3595594.00,,,,,,,,,,
|
||||
75020,,,,,,,,,,,,,,,
|
||||
76217,Dieppe,49.9199000,1.0838000,28599,48446706.00,,,,,,,,,,
|
||||
95128,,,,,,,,,,,,,,,
|
||||
44000,,,,,,,,,,,,,,,
|
||||
79000,,,,,,,,,,,,,,,
|
||||
27562,Saint-Marcel,49.0927000,1.4395000,4474,6259126.00,,,,,,,,,,
|
||||
87011,Bellac,46.1013000,1.0325000,3569,5117946.00,,,,,,,,,,
|
||||
94016,Cachan,48.7914000,2.3318000,30526,50642634.00,,,,,,,,,,
|
||||
50237,"La Haye-Pesnel",48.8134000,-1.3732000,1261,2220621.00,,,,,,,,,,
|
||||
91174,Corbeil-Essonnes,48.5973000,2.4646000,53712,72081504.00,,,,,,,,,,
|
||||
11012,Argeliers,43.3090000,2.9137000,2128,3562272.00,,,,,,,,,,
|
||||
94041,Ivry-sur-Seine,48.8125000,2.3872000,64526,85432424.00,,,,,,,,,,
|
||||
95127,Cergy,49.0373000,2.0455000,69578,108263368.00,,,,,,,,,,
|
||||
69001,Affoux,45.8448000,4.4116000,397,648698.00,,,,,,,,,,
|
||||
44190,Saint-Sébastien-sur-Loire,47.2065000,-1.5023000,28373,39920811.00,,,,,,,,,,
|
||||
69123,Lyon,45.7580000,4.8351000,520774,755643074.00,,,,,,,,,,
|
||||
07336,Vernon,44.5074000,4.2251000,223,371295.00,,,,,,,,,,
|
||||
91201,Draveil,48.6777000,2.4249000,29824,51506048.00,,,,,,,,,,
|
||||
29174,Plonéour-Lanvern,47.9066000,-4.2678000,6403,9931053.00,,,,,,,,,,
|
||||
75017,,,,,,,,,,,,,,,
|
||||
17197,Jonzac,45.4413000,-0.4237000,3576,5188776.00,,,,,,,,,,
|
||||
85195,,,,,,,,,,,,,,,
|
||||
44162,Saint-Herblain,47.2246000,-1.6306000,50561,88481750.00,,,,,,,,,,
|
||||
00000,"toutes les villes",,,,,,,,,,,,,,
|
||||
57463,Metz,49.1048000,6.1962000,121695,192643185.00,,,,,,,,,,
|
||||
57466,Metzing,49.1008000,6.9605000,693,945945.00,,,,,,,,,,
|
||||
37000,,,,,,,,,,,,,,,
|
||||
91312,Igny,48.7375000,2.2229000,10571,14831113.00,,,,,,,,,,
|
||||
25462,Pontarlier,46.9167000,6.3796000,17928,23449824.00,,,,,,,,,,
|
||||
33063,Bordeaux,44.8624000,-0.5848000,265328,467773264.00,,,,,,,,,,
|
||||
16015,Angoulême,45.6458000,0.1450000,41423,66069685.00,,,,,,,,,,
|
||||
02196,Clacy-et-Thierret,49.5546000,3.5651000,294,458934.00,,,,,,,,,,
|
||||
24037,Bergerac,44.8519000,0.4883000,26852,47850264.00,,,,,,,,,,
|
||||
78686,Viroflay,48.8017000,2.1725000,16943,27091857.00,,,,,,,,,,
|
||||
83137,Toulon,43.1364000,5.9334000,180834,319172010.00,,,,,,,,,,
|
||||
44026,Carquefou,47.2968000,-1.4687000,20535,28707930.00,,,,,,,,,,
|
||||
87154,Saint-Junien,45.8965000,0.8853000,11382,19406310.00,,,,,,,,,,
|
||||
79081,Chauray,46.3537000,-0.3862000,7173,12387771.00,,,,,,,,,,
|
||||
73008,Aix-les-Bains,45.6943000,5.9035000,32175,53539200.00,,,,,,,,,,
|
||||
86195,Port-de-Piles,46.9989000,0.5956000,548,744184.00,,,,,,,,,,
|
||||
91470,,,,,,,,,,,,,,,
|
||||
34008,"Les Aires",43.5687000,3.0676000,613,1081332.00,,,,,,,,,,
|
||||
44270,,,,,,,,,,,,,,,
|
||||
25056,Besançon,47.2602000,6.0123000,120057,158355183.00,,,,,,,,,,
|
||||
75019,,,,,,,,,,,,,,,
|
||||
78712,,,,,,,,,,,,,,,
|
||||
11262,Narbonne,43.1493000,3.0337000,56692,79482184.00,,,,,,,,,,
|
||||
60602,Saint-Valery,49.7251000,1.7303000,54,90450.00,,,,,,,,,,
|
||||
91421,Montgeron,48.6952000,2.4638000,23890,40851900.00,,,,,,,,,,
|
||||
82112,Moissac,44.1219000,1.1002000,13652,24027520.00,,,,,,,,,,
|
||||
92033,Garches,48.8469000,2.1861000,17705,25442085.00,,,,,,,,,,
|
||||
38053,Bourgoin-Jallieu,45.6025000,5.2747000,29816,41593320.00,,,,,,,,,,
|
||||
34335,Villemagne-l'Argentière,43.6189000,3.1208000,420,586320.00,,,,,,,,,,
|
||||
13213,,43.3528000,5.4301000,93425,144528475.00,,,,,,,,,,
|
||||
44020,Bouguenais,47.1710000,-1.6181000,20590,34282350.00,,,,,,,,,,
|
||||
36044,Châteauroux,46.8023000,1.6903000,43079,70864955.00,,,,,,,,,,
|
||||
11164,Ginestas,43.2779000,2.8830000,1579,2837463.00,,,,,,,,,,
|
||||
60009,Allonne,49.3952000,2.1157000,1737,2883420.00,,,,,,,,,,
|
||||
41151,"Montrichard Val de Cher",47.3594000,1.1998000,3641,5559807.00,,,,,,,,,,
|
||||
27554,"La Chapelle-Longueville",49.1085000,1.4115000,3283,4796463.00,,,,,,,,,,
|
||||
34189,Olonzac,43.2816000,2.7431000,1683,2511036.00,,,,,,,,,,
|
||||
34028,Bédarieux,43.6113000,3.1637000,5820,9550620.00,,,,,,,,,,
|
||||
74112,"Épagny Metz-Tessy",45.9430000,6.0934000,8642,13956830.00,,,,,,,,,,
|
||||
75102,,48.8677000,2.3411000,20433,35103894.00,,,,,,,,,,
|
||||
60057,Beauvais,49.4425000,2.0877000,55906,84082624.00,,,,,,,,,,
|
||||
59350,Lille,50.6311000,3.0468000,238695,403871940.00,,,,,,,,,,
|
||||
91477,Igny,48.7155000,2.2293000,36067,46923167.00,,,,,,,,,,
|
||||
12145,Millau,44.0982000,3.1176000,21859,34865105.00,,,,,,,,,,
|
||||
12115,L'Hospitalet-du-Larzac,43.9755000,3.2074000,344,569320.00,,,,,,,,,,
|
||||
79004,,,,,,,,,,,,,,,
|
||||
75014,,,,,,,,,,,,,,,
|
||||
75018,,,,,,,,,,,,,,,
|
||||
55154,Dieue-sur-Meuse,49.0790000,5.4293000,1452,2099592.00,,,,,,,,,,
|
||||
79192,,,,,,,,,,,,,,,
|
||||
06018,Biot,43.6273000,7.0821000,10196,15192040.00,,,,,,,,,,
|
||||
25393,Montécheroux,47.3469000,6.7965000,557,839399.00,,,,,,,,,,
|
||||
14554,"Le Castelet",49.0870000,-0.2811000,1829,3076378.00,,,,,,,,,,
|
||||
69384,,45.7805000,4.8260000,35232,49782816.00,,,,,,,,,,
|
||||
78672,Villennes-sur-Seine,48.9372000,1.9975000,5792,9359872.00,,,,,,,,,,
|
||||
78123,Carrières-sous-Poissy,48.9469000,2.0264000,19951,35752192.00,,,,,,,,,,
|
||||
77000,,,,,,,,,,,,,,,
|
||||
31056,Beauzelle,43.6680000,1.3753000,8184,11670384.00,,,,,,,,,,
|
||||
38553,Villefontaine,45.6161000,5.1549000,19018,25579210.00,,,,,,,,,,
|
||||
47001,Agen,44.2010000,0.6302000,32193,48965553.00,,,,,,,,,,
|
||||
54700,,,,,,,,,,,,,,,
|
||||
70279,Gray,47.4310000,5.6153000,5455,9071665.00,,,,,,,,,,
|
||||
74000,,,,,,,,,,,,,,,
|
||||
91339,Linas,48.6261000,2.2525000,7310,12412380.00,,,,,,,,,,
|
||||
17306,Royan,45.6343000,-1.0127000,19322,33311128.00,,,,,,,,,,
|
||||
17300,"La Rochelle",46.1620000,-1.1765000,79961,118182358.00,,,,,,,,,,
|
||||
59000,,,,,,,,,,,,,,,
|
||||
77284,Meaux,48.9573000,2.9035000,56659,84025297.00,,,,,,,,,,
|
||||
76414,Martin-Église,49.9105000,1.1279000,1595,2177175.00,,,,,,,,,,
|
||||
54528,Toul,48.6794000,5.8980000,15570,21626730.00,,,,,,,,,,
|
||||
63124,Cournon-d'Auvergne,45.7420000,3.1885000,20020,30930900.00,,,,,,,,,,
|
||||
87085,Limoges,45.8567000,1.2260000,129754,203194764.00,,,,,,,,,,
|
||||
78000,,,,,,,,,,,,,,,
|
||||
13205,,43.2925000,5.4006000,45020,61632380.00,,,,,,,,,,
|
||||
31584,Villemur-sur-Tarn,43.8582000,1.4947000,6235,9813890.00,,,,,,,,,,
|
||||
91228,Évry-Courcouronnes,48.6287000,2.4313000,66700,90245100.00,,,,,,,,,,
|
||||
41018,Blois,47.5813000,1.3049000,47092,79350020.00,,,,,,,,,,
|
||||
66136,Perpignan,42.6990000,2.9045000,120996,193593600.00,,,,,,,,,,
|
||||
11041,Bize-Minervois,43.3364000,2.8719000,1292,1758412.00,,,,,,,,,,
|
||||
75016,,,,,,,,,,,,,,,
|
||||
76540,Rouen,
|
||||
|
Can't render this file because it has a wrong number of fields in line 193.
|
|
@ -1,3 +0,0 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<osm version="0.6" generator="empty">
|
||||
</osm>
|
|
@ -1,110 +0,0 @@
|
|||
local tables = {}
|
||||
|
||||
-- Table pour les bornes incendie
|
||||
tables.fire_hydrants = osm2pgsql.define_node_table('fire_hydrants', {
|
||||
{ column = 'id_column', type = 'id_type' },
|
||||
{ column = 'geom', type = 'point', projection = 4326 },
|
||||
{ column = 'tags', type = 'hstore' },
|
||||
{ column = 'ref', type = 'text' },
|
||||
{ column = 'color', type = 'text' },
|
||||
{ column = 'insee', type = 'text' },
|
||||
})
|
||||
|
||||
-- Table pour les arbres
|
||||
tables.trees = osm2pgsql.define_node_table('trees', {
|
||||
{ column = 'id_column', type = 'id_type' },
|
||||
{ column = 'geom', type = 'point', projection = 4326 },
|
||||
{ column = 'tags', type = 'hstore' },
|
||||
{ column = 'species', type = 'text' },
|
||||
{ column = 'height', type = 'text' },
|
||||
{ column = 'insee', type = 'text' },
|
||||
})
|
||||
|
||||
-- Table pour les bornes de recharge (nodes)
|
||||
tables.charging_stations = osm2pgsql.define_node_table('charging_stations', {
|
||||
{ column = 'id_column', type = 'id_type' },
|
||||
{ column = 'geom', type = 'point', projection = 4326 },
|
||||
{ column = 'tags', type = 'hstore' },
|
||||
{ column = 'operator', type = 'text' },
|
||||
{ column = 'capacity', type = 'text' },
|
||||
{ column = 'insee', type = 'text' },
|
||||
})
|
||||
|
||||
-- Table pour les bornes de recharge (ways)
|
||||
tables.charging_stations_ways = osm2pgsql.define_way_table('charging_stations_ways', {
|
||||
{ column = 'id_column', type = 'id_type' },
|
||||
{ column = 'geom', type = 'linestring', projection = 4326 },
|
||||
{ column = 'tags', type = 'hstore' },
|
||||
{ column = 'operator', type = 'text' },
|
||||
{ column = 'capacity', type = 'text' },
|
||||
{ column = 'insee', type = 'text' },
|
||||
})
|
||||
|
||||
-- Function to determine the INSEE code from multiple possible sources
|
||||
function get_insee_code(tags)
|
||||
-- Try to get INSEE code from different tags
|
||||
if tags['ref:INSEE'] then
|
||||
return tags['ref:INSEE']
|
||||
elseif tags['addr:postcode'] then
|
||||
-- French postal codes often start with the department code
|
||||
-- For example, 91150 is in department 91, which can help identify the INSEE code
|
||||
return tags['addr:postcode'] and string.sub(tags['addr:postcode'], 1, 2) .. "111"
|
||||
elseif tags['addr:city'] and tags['addr:city'] == 'Étampes' then
|
||||
-- If the city is Étampes, use the INSEE code 91111
|
||||
return "91111"
|
||||
else
|
||||
-- Default to 91111 (Étampes) for this specific use case
|
||||
-- In a production environment, you would use a spatial query to determine the INSEE code
|
||||
return "91111"
|
||||
end
|
||||
end
|
||||
|
||||
function osm2pgsql.process_node(object)
|
||||
-- Check for fire hydrants with different tagging schemes
|
||||
if object.tags.emergency == 'fire_hydrant' or object.tags.amenity == 'fire_hydrant' then
|
||||
tables.fire_hydrants:insert({
|
||||
tags = object.tags,
|
||||
ref = object.tags.ref,
|
||||
color = object.tags.color,
|
||||
insee = get_insee_code(object.tags)
|
||||
})
|
||||
end
|
||||
|
||||
-- Check for trees
|
||||
if object.tags.natural == 'tree' then
|
||||
tables.trees:insert({
|
||||
tags = object.tags,
|
||||
species = object.tags.species,
|
||||
height = object.tags.height,
|
||||
insee = get_insee_code(object.tags)
|
||||
})
|
||||
end
|
||||
|
||||
-- Check for charging stations
|
||||
if object.tags.amenity == 'charging_station' then
|
||||
tables.charging_stations:insert({
|
||||
tags = object.tags,
|
||||
operator = object.tags.operator,
|
||||
capacity = object.tags.capacity,
|
||||
insee = get_insee_code(object.tags)
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
function osm2pgsql.process_way(object)
|
||||
-- Check for charging stations that might be mapped as ways
|
||||
if object.tags.amenity == 'charging_station' then
|
||||
tables.charging_stations_ways:insert({
|
||||
tags = object.tags,
|
||||
operator = object.tags.operator,
|
||||
capacity = object.tags.capacity,
|
||||
insee = get_insee_code(object.tags)
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
function osm2pgsql.process_relation(object)
|
||||
return
|
||||
end
|
||||
|
||||
|
|
@ -1,309 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Script pour générer un graphique interactif avec plotly montrant l'évolution du nombre d'objets OSM
|
||||
à partir d'un fichier CSV thématique d'une ville (code INSEE 91111 par défaut).
|
||||
|
||||
Ce script utilise la bibliothèque plotly pour créer des graphiques interactifs à partir des données
|
||||
contenues dans des fichiers CSV thématiques. Par défaut, il utilise le code INSEE 91111.
|
||||
Le titre du graphique inclut le tag principal (thème) et le nom de la ville.
|
||||
|
||||
Utilisation:
|
||||
python plotly_city.py chemin/vers/fichier.csv [options]
|
||||
|
||||
Options:
|
||||
--output, -o : Chemin de sortie pour le graphique HTML (optionnel)
|
||||
--insee, -i : Code INSEE de la commune à analyser (par défaut: 91111)
|
||||
--city_name, -c : Nom de la ville (si non spécifié, sera généré à partir du code INSEE)
|
||||
|
||||
Exemple:
|
||||
python plotly_city.py test_results/commune_91111_borne-de-recharge.csv
|
||||
|
||||
Dépendances requises:
|
||||
- pandas
|
||||
- plotly
|
||||
|
||||
Installation des dépendances:
|
||||
pip install pandas plotly
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import pandas as pd
|
||||
import plotly.graph_objects as go
|
||||
from plotly.subplots import make_subplots
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def parse_args():
|
||||
"""Parse command line arguments."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Génère un graphique interactif avec plotly à partir des données CSV d'objets OSM."
|
||||
)
|
||||
parser.add_argument(
|
||||
"csv_file", help="Chemin vers le fichier CSV contenant les données"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output", "-o", help="Chemin de sortie pour le graphique (HTML)", default=None
|
||||
)
|
||||
parser.add_argument(
|
||||
"--insee", "-i", help="Code INSEE de la commune à analyser", default="91111"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--city_name",
|
||||
"-c",
|
||||
help="Nom de la ville (si non spécifié, sera extrait du CSV)",
|
||||
default=None,
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def get_city_name(insee_code):
|
||||
"""
|
||||
Récupère le nom de la ville à partir du code INSEE.
|
||||
Cette fonction pourrait être améliorée pour utiliser une API ou une base de données.
|
||||
|
||||
Args:
|
||||
insee_code: Code INSEE de la commune
|
||||
|
||||
Returns:
|
||||
Nom de la ville ou le code INSEE si le nom n'est pas trouvé
|
||||
"""
|
||||
# Pour l'instant, on retourne simplement le code INSEE
|
||||
# Dans une version future, on pourrait implémenter une recherche dans une base de données
|
||||
return f"Commune {insee_code}"
|
||||
|
||||
|
||||
def load_data(csv_file, insee_code="91111"):
|
||||
"""
|
||||
Charge les données depuis le fichier CSV.
|
||||
|
||||
Args:
|
||||
csv_file: Chemin vers le fichier CSV
|
||||
insee_code: Code INSEE de la commune à filtrer
|
||||
|
||||
Returns:
|
||||
DataFrame pandas contenant les données filtrées
|
||||
"""
|
||||
# Charger le CSV avec gestion des erreurs pour les lignes mal formatées
|
||||
try:
|
||||
df = pd.read_csv(csv_file, on_bad_lines="skip")
|
||||
except TypeError: # Pour les versions plus anciennes de pandas
|
||||
df = pd.read_csv(csv_file, error_bad_lines=False, warn_bad_lines=True)
|
||||
|
||||
# Vérifier si le CSV a la structure attendue
|
||||
if "date" in df.columns:
|
||||
# Format de CSV avec colonne 'date' directement
|
||||
df["date"] = pd.to_datetime(df["date"])
|
||||
else:
|
||||
# Si aucune colonne de date n'est trouvée, essayer d'utiliser la première colonne
|
||||
try:
|
||||
df["date"] = pd.to_datetime(df.iloc[:, 0])
|
||||
except:
|
||||
print("Erreur: Impossible de trouver ou convertir une colonne de date.")
|
||||
sys.exit(1)
|
||||
|
||||
# Filtrer par code INSEE si la colonne 'zone' contient des codes INSEE
|
||||
if "zone" in df.columns:
|
||||
# Vérifier si la zone contient le code INSEE
|
||||
if any(
|
||||
zone.endswith(insee_code) for zone in df["zone"] if isinstance(zone, str)
|
||||
):
|
||||
df = df[df["zone"].str.endswith(insee_code)]
|
||||
|
||||
# Trier par date
|
||||
df = df.sort_values("date")
|
||||
|
||||
return df
|
||||
|
||||
|
||||
def generate_plotly_graph(
|
||||
df, city_name=None, output_path=None, insee_code="91111", csv_file=None
|
||||
):
|
||||
"""
|
||||
Génère un graphique interactif avec plotly montrant l'évolution du nombre d'objets dans le temps.
|
||||
|
||||
Args:
|
||||
df: DataFrame pandas contenant les données
|
||||
city_name: Nom de la ville (optionnel)
|
||||
output_path: Chemin de sortie pour le graphique (optionnel)
|
||||
insee_code: Code INSEE de la commune
|
||||
csv_file: Chemin vers le fichier CSV source (pour générer un nom de fichier de sortie par défaut)
|
||||
"""
|
||||
# Si le nom de la ville n'est pas fourni, essayer de le récupérer
|
||||
if not city_name:
|
||||
city_name = get_city_name(insee_code)
|
||||
|
||||
# Déterminer la colonne pour les types d'objets (theme)
|
||||
theme_column = "theme"
|
||||
|
||||
# Créer une figure avec deux sous-graphiques (nombre total et taux de complétion)
|
||||
fig = make_subplots(
|
||||
rows=2,
|
||||
cols=1,
|
||||
subplot_titles=("Nombre d'objets OSM", "Taux de complétion des attributs (%)"),
|
||||
vertical_spacing=0.15,
|
||||
)
|
||||
|
||||
# Obtenir la liste des thèmes uniques
|
||||
if theme_column in df.columns:
|
||||
themes = df[theme_column].unique()
|
||||
|
||||
# Créer un graphique pour chaque thème
|
||||
for theme in themes:
|
||||
# Filtrer les données pour ce thème
|
||||
theme_data = df[df[theme_column] == theme]
|
||||
|
||||
# Tracer la ligne pour le nombre total d'objets
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=theme_data["date"],
|
||||
y=theme_data["nombre_total"],
|
||||
mode="lines+markers",
|
||||
name=f"{theme} - Total",
|
||||
hovertemplate="%{x}<br>Nombre: %{y}<extra></extra>",
|
||||
),
|
||||
row=1,
|
||||
col=1,
|
||||
)
|
||||
|
||||
# Tracer la ligne pour le taux de complétion si disponible
|
||||
if "pourcentage_completion" in theme_data.columns:
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=theme_data["date"],
|
||||
y=theme_data["pourcentage_completion"],
|
||||
mode="lines+markers",
|
||||
name=f"{theme} - Complétion (%)",
|
||||
hovertemplate="%{x}<br>Complétion: %{y}%<extra></extra>",
|
||||
),
|
||||
row=2,
|
||||
col=1,
|
||||
)
|
||||
else:
|
||||
# Si aucune colonne de thème n'est trouvée, tracer simplement le nombre total
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=df["date"],
|
||||
y=df["nombre_total"],
|
||||
mode="lines+markers",
|
||||
name="Total",
|
||||
hovertemplate="%{x}<br>Nombre: %{y}<extra></extra>",
|
||||
),
|
||||
row=1,
|
||||
col=1,
|
||||
)
|
||||
|
||||
# Tracer la ligne pour le taux de complétion si disponible
|
||||
if "pourcentage_completion" in df.columns:
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=df["date"],
|
||||
y=df["pourcentage_completion"],
|
||||
mode="lines+markers",
|
||||
name="Complétion (%)",
|
||||
hovertemplate="%{x}<br>Complétion: %{y}%<extra></extra>",
|
||||
),
|
||||
row=2,
|
||||
col=1,
|
||||
)
|
||||
|
||||
# Configurer les axes et les légendes
|
||||
fig.update_xaxes(title_text="Date", row=1, col=1)
|
||||
fig.update_xaxes(title_text="Date", row=2, col=1)
|
||||
fig.update_yaxes(title_text="Nombre d'objets", row=1, col=1)
|
||||
fig.update_yaxes(title_text="Taux de complétion (%)", range=[0, 100], row=2, col=1)
|
||||
|
||||
# Obtenir le thème principal (premier thème trouvé)
|
||||
main_tag = (
|
||||
themes[0] if theme_column in df.columns and len(themes) > 0 else "Objets OSM"
|
||||
)
|
||||
|
||||
# Mettre à jour le titre du graphique avec le tag principal et le nom de la ville
|
||||
fig.update_layout(
|
||||
title=f"{main_tag} - {city_name}",
|
||||
hovermode="x unified",
|
||||
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
|
||||
height=800,
|
||||
width=1000,
|
||||
margin=dict(t=100, b=50, l=50, r=50),
|
||||
)
|
||||
|
||||
# Ajouter des annotations pour les informations supplémentaires
|
||||
fig.add_annotation(
|
||||
text=f"Code INSEE: {insee_code}",
|
||||
xref="paper",
|
||||
yref="paper",
|
||||
x=0.01,
|
||||
y=-0.15,
|
||||
showarrow=False,
|
||||
font=dict(size=10),
|
||||
)
|
||||
|
||||
# Ajouter la date de génération
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
fig.add_annotation(
|
||||
text=f"Généré le: {now}",
|
||||
xref="paper",
|
||||
yref="paper",
|
||||
x=0.99,
|
||||
y=-0.15,
|
||||
showarrow=False,
|
||||
font=dict(size=8),
|
||||
align="right",
|
||||
)
|
||||
|
||||
# Sauvegarder ou afficher le graphique
|
||||
if output_path:
|
||||
fig.write_html(output_path)
|
||||
print(f"Graphique interactif sauvegardé: {output_path}")
|
||||
elif csv_file:
|
||||
# Déterminer un chemin de sortie par défaut basé sur le fichier CSV
|
||||
base_name = os.path.splitext(csv_file)[0]
|
||||
default_output = f"{base_name}_plotly.html"
|
||||
fig.write_html(default_output)
|
||||
print(f"Graphique interactif sauvegardé: {default_output}")
|
||||
else:
|
||||
# Si aucun chemin de sortie n'est spécifié et aucun fichier CSV n'est fourni,
|
||||
# utiliser un nom par défaut basé sur le code INSEE et le thème
|
||||
default_output = f"commune_{insee_code}_{main_tag}_plotly.html"
|
||||
fig.write_html(default_output)
|
||||
print(f"Graphique interactif sauvegardé: {default_output}")
|
||||
|
||||
|
||||
def main():
|
||||
"""Fonction principale."""
|
||||
# Analyser les arguments de la ligne de commande
|
||||
args = parse_args()
|
||||
|
||||
# Vérifier que le fichier CSV existe
|
||||
if not os.path.isfile(args.csv_file):
|
||||
print(f"Erreur: Le fichier {args.csv_file} n'existe pas.")
|
||||
sys.exit(1)
|
||||
|
||||
# Charger les données
|
||||
df = load_data(args.csv_file, args.insee)
|
||||
|
||||
# Vérifier qu'il y a des données
|
||||
if df.empty:
|
||||
print(f"Aucune donnée trouvée pour le code INSEE {args.insee}.")
|
||||
sys.exit(1)
|
||||
|
||||
# Déterminer le chemin de sortie si non spécifié
|
||||
if not args.output:
|
||||
# Utiliser le même nom que le fichier CSV mais avec l'extension .html
|
||||
base_name = os.path.splitext(args.csv_file)[0]
|
||||
output_path = f"{base_name}_plotly.html"
|
||||
else:
|
||||
output_path = args.output
|
||||
|
||||
# Générer le graphique
|
||||
generate_plotly_graph(df, args.city_name, output_path, args.insee, args.csv_file)
|
||||
|
||||
print("Graphique interactif généré avec succès!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -1,6 +0,0 @@
|
|||
|
||||
osmupdate --verbose --keep-tempfiles --day -t=test_temp/ -v osm_data/france-internal.osh.pbf test_temp/changes.osc.gz
|
||||
osmupdate --verbose --keep-tempfiles --day -t=test_temp/ -v osm_data/france-latest.osm.pbf test_temp/changes.osc.gz
|
||||
#osmium extract -p polyons/commune_91111.poly -s simple changes.osc.gz -O -o test_temp/changes.local.osc.gz
|
||||
osmium apply-changes -H osm_data/france-internal.osh.pbf test_temp/changes.osc.gz -O -o osm_data/france-internal_updated.osh.pbf
|
||||
osmium apply-changes -H osm_data/france-latest.osh.pbf test_temp/changes.osc.gz -O -o osm_data/france-latest_updated.osh.pbf
|
|
@ -1,194 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Script pour mettre à jour le fichier historisé france internal.
|
||||
|
||||
Ce script utilise osmupdate pour mettre à jour le fichier france-internal.osh.pbf
|
||||
avec les dernières modifications d'OpenStreetMap.
|
||||
|
||||
Usage:
|
||||
python update.py [--verbose]
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import subprocess
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
# Chemin vers le répertoire du script
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# Chemin vers le fichier historisé france internal
|
||||
FRANCE_INTERNAL_FILE = os.path.join(SCRIPT_DIR, "osm_data", "france-internal.osh.pbf")
|
||||
|
||||
# Chemin vers le répertoire temporaire pour osmupdate
|
||||
TEMP_DIR = os.path.join(SCRIPT_DIR, "update_temp")
|
||||
|
||||
# Configurer le logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(levelname)s - %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def run_command(command, verbose=False):
|
||||
"""
|
||||
Exécute une commande shell et retourne la sortie.
|
||||
|
||||
Args:
|
||||
command (str): Commande à exécuter
|
||||
verbose (bool): Si True, affiche la sortie de la commande en temps réel
|
||||
|
||||
Returns:
|
||||
tuple: (code de retour, sortie standard, sortie d'erreur)
|
||||
"""
|
||||
logger.info(f"Exécution: {command}")
|
||||
|
||||
if verbose:
|
||||
# Exécuter la commande avec sortie en temps réel
|
||||
process = subprocess.Popen(
|
||||
command,
|
||||
shell=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
universal_newlines=True,
|
||||
bufsize=1
|
||||
)
|
||||
|
||||
stdout_lines = []
|
||||
stderr_lines = []
|
||||
|
||||
# Lire la sortie standard en temps réel
|
||||
for line in process.stdout:
|
||||
line = line.strip()
|
||||
stdout_lines.append(line)
|
||||
print(line)
|
||||
|
||||
# Lire la sortie d'erreur en temps réel
|
||||
for line in process.stderr:
|
||||
line = line.strip()
|
||||
stderr_lines.append(line)
|
||||
print(f"ERREUR: {line}", file=sys.stderr)
|
||||
|
||||
# Attendre la fin du processus
|
||||
return_code = process.wait()
|
||||
return return_code, "\n".join(stdout_lines), "\n".join(stderr_lines)
|
||||
else:
|
||||
# Exécuter la commande sans sortie en temps réel
|
||||
try:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
shell=True,
|
||||
check=False,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
universal_newlines=True,
|
||||
)
|
||||
return result.returncode, result.stdout, result.stderr
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de l'exécution de la commande: {e}")
|
||||
return 1, "", str(e)
|
||||
|
||||
|
||||
def update_france_internal(verbose=False):
|
||||
"""
|
||||
Met à jour le fichier historisé france internal avec osmupdate.
|
||||
|
||||
Args:
|
||||
verbose (bool): Si True, affiche la sortie de la commande en temps réel
|
||||
|
||||
Returns:
|
||||
bool: True si la mise à jour a réussi, False sinon
|
||||
"""
|
||||
# Vérifier si le fichier existe
|
||||
if not os.path.isfile(FRANCE_INTERNAL_FILE):
|
||||
logger.error(f"Le fichier {FRANCE_INTERNAL_FILE} n'existe pas.")
|
||||
logger.error("Veuillez télécharger le fichier initial depuis Geofabrik ou une autre source.")
|
||||
return False
|
||||
|
||||
# Créer le répertoire temporaire s'il n'existe pas
|
||||
os.makedirs(TEMP_DIR, exist_ok=True)
|
||||
|
||||
# Chemin vers le fichier mis à jour
|
||||
updated_file = os.path.join(TEMP_DIR, "france-internal-updated.osh.pbf")
|
||||
|
||||
# Construire la commande osmupdate
|
||||
command = f"osmupdate --verbose --keep-tempfiles -t={TEMP_DIR}/temp {FRANCE_INTERNAL_FILE} {updated_file}"
|
||||
|
||||
# Exécuter la commande
|
||||
logger.info("Mise à jour du fichier france-internal.osh.pbf en cours...")
|
||||
return_code, stdout, stderr = run_command(command, verbose)
|
||||
|
||||
if return_code != 0:
|
||||
logger.error(f"Erreur lors de la mise à jour: {stderr}")
|
||||
return False
|
||||
|
||||
# Remplacer l'ancien fichier par le nouveau
|
||||
if os.path.isfile(updated_file):
|
||||
# Créer une sauvegarde de l'ancien fichier
|
||||
backup_file = f"{FRANCE_INTERNAL_FILE}.bak"
|
||||
try:
|
||||
os.rename(FRANCE_INTERNAL_FILE, backup_file)
|
||||
logger.info(f"Sauvegarde de l'ancien fichier créée: {backup_file}")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la création de la sauvegarde: {e}")
|
||||
return False
|
||||
|
||||
# Déplacer le nouveau fichier
|
||||
try:
|
||||
os.rename(updated_file, FRANCE_INTERNAL_FILE)
|
||||
logger.info(f"Fichier mis à jour avec succès: {FRANCE_INTERNAL_FILE}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors du déplacement du fichier mis à jour: {e}")
|
||||
# Restaurer l'ancien fichier en cas d'erreur
|
||||
try:
|
||||
os.rename(backup_file, FRANCE_INTERNAL_FILE)
|
||||
logger.info("Restauration de l'ancien fichier réussie.")
|
||||
except Exception as e2:
|
||||
logger.error(f"Erreur lors de la restauration de l'ancien fichier: {e2}")
|
||||
return False
|
||||
else:
|
||||
logger.error(f"Le fichier mis à jour {updated_file} n'a pas été créé.")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""Fonction principale"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Met à jour le fichier historisé france internal avec osmupdate."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--verbose", "-v", action="store_true", help="Affiche la sortie des commandes en temps réel"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Afficher l'heure de début
|
||||
start_time = datetime.now()
|
||||
logger.info(f"Début de la mise à jour: {start_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
|
||||
# Mettre à jour le fichier
|
||||
success = update_france_internal(args.verbose)
|
||||
|
||||
# Afficher l'heure de fin et la durée
|
||||
end_time = datetime.now()
|
||||
duration = end_time - start_time
|
||||
logger.info(f"Fin de la mise à jour: {end_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
logger.info(f"Durée totale: {duration}")
|
||||
|
||||
if success:
|
||||
logger.info("Mise à jour terminée avec succès.")
|
||||
return 0
|
||||
else:
|
||||
logger.error("Échec de la mise à jour.")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
|
@ -1,220 +0,0 @@
|
|||
# API - Export des objets Stats
|
||||
|
||||
## Endpoint
|
||||
|
||||
```
|
||||
GET /api/v1/stats/export
|
||||
```
|
||||
|
||||
## Description
|
||||
|
||||
Cet endpoint permet d'exporter les objets Stats au format JSON avec leurs propriétés de nom et de décomptes, similaire à la commande `app:export-stats`.
|
||||
|
||||
## Paramètres de requête
|
||||
|
||||
| Paramètre | Type | Défaut | Description |
|
||||
|-----------|------|--------|-------------|
|
||||
| `zone` | string | - | Code INSEE spécifique à exporter (optionnel) |
|
||||
| `pretty` | boolean | false | Formater le JSON avec indentation |
|
||||
| `include_followups` | boolean | true | Inclure les données de followup |
|
||||
| `include_places` | boolean | false | Inclure les données des lieux (peut être volumineux) |
|
||||
|
||||
## Exemples d'utilisation
|
||||
|
||||
### Export de toutes les zones
|
||||
```bash
|
||||
curl "https://osm-commerces.cipherbliss.com/api/v1/stats/export"
|
||||
```
|
||||
|
||||
### Export avec formatage JSON
|
||||
```bash
|
||||
curl "https://osm-commerces.cipherbliss.com/api/v1/stats/export?pretty=true"
|
||||
```
|
||||
|
||||
### Export d'une zone spécifique
|
||||
```bash
|
||||
curl "https://osm-commerces.cipherbliss.com/api/v1/stats/export?zone=75056"
|
||||
```
|
||||
|
||||
### Export d'une zone avec formatage et sans followups
|
||||
```bash
|
||||
curl "https://osm-commerces.cipherbliss.com/api/v1/stats/export?zone=75056&pretty=true&include_followups=false"
|
||||
```
|
||||
|
||||
### Export complet avec lieux
|
||||
```bash
|
||||
curl "https://osm-commerces.cipherbliss.com/api/v1/stats/export?pretty=true&include_places=true"
|
||||
```
|
||||
|
||||
## Réponse
|
||||
|
||||
### Succès (200 OK)
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"zone": "75056",
|
||||
"name": "Paris",
|
||||
"dateCreated": "2024-01-15 10:30:00",
|
||||
"dateModified": "2024-01-20 14:45:00",
|
||||
"population": 2161000,
|
||||
"budgetAnnuel": "8500000000",
|
||||
"siren": 200054781,
|
||||
"codeEpci": 200054781,
|
||||
"codesPostaux": "75001;75002;75003;...",
|
||||
"decomptes": {
|
||||
"placesCount": 1250,
|
||||
"avecHoraires": 980,
|
||||
"avecAdresse": 1200,
|
||||
"avecSite": 850,
|
||||
"avecAccessibilite": 450,
|
||||
"avecNote": 320,
|
||||
"completionPercent": 75,
|
||||
"placesCountReal": 1250
|
||||
},
|
||||
"followups": [
|
||||
{
|
||||
"name": "fire_hydrant_count",
|
||||
"measure": 1250,
|
||||
"date": "2024-01-20 14:45:00"
|
||||
},
|
||||
{
|
||||
"name": "fire_hydrant_completion",
|
||||
"measure": 85.5,
|
||||
"date": "2024-01-20 14:45:00"
|
||||
}
|
||||
],
|
||||
"places": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Boulangerie du Centre",
|
||||
"mainTag": "shop",
|
||||
"osmId": 123456,
|
||||
"osmKind": "node",
|
||||
"email": "contact@boulangerie.fr",
|
||||
"note": "Boulangerie artisanale",
|
||||
"zipCode": "75001",
|
||||
"siret": "12345678901234",
|
||||
"lat": 48.8566,
|
||||
"lon": 2.3522,
|
||||
"hasOpeningHours": true,
|
||||
"hasAddress": true,
|
||||
"hasWebsite": true,
|
||||
"hasWheelchair": false,
|
||||
"hasNote": true,
|
||||
"completionPercentage": 85
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Erreur - Zone non trouvée (404 Not Found)
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "Aucun objet Stats trouvé",
|
||||
"message": "Aucune zone trouvée pour le code INSEE: 99999"
|
||||
}
|
||||
```
|
||||
|
||||
### Erreur - Erreur serveur (500 Internal Server Error)
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "Erreur lors de l'export",
|
||||
"message": "Description de l'erreur"
|
||||
}
|
||||
```
|
||||
|
||||
## Headers de réponse
|
||||
|
||||
| Header | Description |
|
||||
|--------|-------------|
|
||||
| `Content-Type` | `application/json` |
|
||||
| `Content-Disposition` | `attachment; filename="stats_export.json"` |
|
||||
| `X-Export-Count` | Nombre d'objets exportés |
|
||||
| `X-Export-Generated` | Date/heure de génération (format ISO 8601) |
|
||||
| `X-Export-Zone` | Code INSEE si export d'une zone spécifique |
|
||||
|
||||
## Structure des données
|
||||
|
||||
### Informations générales
|
||||
- `id` : Identifiant unique de l'objet Stats
|
||||
- `zone` : Code INSEE de la zone
|
||||
- `name` : Nom de la ville/zone
|
||||
- `dateCreated` : Date de création
|
||||
- `dateModified` : Date de dernière modification
|
||||
|
||||
### Données démographiques et administratives
|
||||
- `population` : Population de la zone
|
||||
- `budgetAnnuel` : Budget annuel de la collectivité
|
||||
- `siren` : Code SIREN
|
||||
- `codeEpci` : Code EPCI
|
||||
- `codesPostaux` : Codes postaux de la zone
|
||||
|
||||
### Décomptes
|
||||
- `placesCount` : Nombre de lieux enregistrés
|
||||
- `avecHoraires` : Nombre de lieux avec horaires d'ouverture
|
||||
- `avecAdresse` : Nombre de lieux avec adresse complète
|
||||
- `avecSite` : Nombre de lieux avec site web
|
||||
- `avecAccessibilite` : Nombre de lieux avec accessibilité PMR
|
||||
- `avecNote` : Nombre de lieux avec note
|
||||
- `completionPercent` : Pourcentage de complétion global
|
||||
- `placesCountReal` : Nombre réel de lieux (comptage direct)
|
||||
|
||||
### Followups (si `include_followups=true`)
|
||||
- `followups` : Tableau des mesures de suivi (CityFollowUp)
|
||||
- `name` : Nom de la mesure
|
||||
- `measure` : Valeur de la mesure
|
||||
- `date` : Date de la mesure
|
||||
|
||||
### Places (si `include_places=true`)
|
||||
- `places` : Tableau des lieux de la zone
|
||||
- `id` : Identifiant du lieu
|
||||
- `name` : Nom du lieu
|
||||
- `mainTag` : Tag principal OSM
|
||||
- `osmId` : ID OSM
|
||||
- `osmKind` : Type OSM (node/way)
|
||||
- `email` : Email de contact
|
||||
- `note` : Note
|
||||
- `zipCode` : Code postal
|
||||
- `siret` : Numéro SIRET
|
||||
- `lat` : Latitude
|
||||
- `lon` : Longitude
|
||||
- `hasOpeningHours` : A des horaires d'ouverture
|
||||
- `hasAddress` : A une adresse complète
|
||||
- `hasWebsite` : A un site web
|
||||
- `hasWheelchair` : A des informations d'accessibilité
|
||||
- `hasNote` : A une note
|
||||
- `completionPercentage` : Pourcentage de complétion du lieu
|
||||
|
||||
## Cas d'usage
|
||||
|
||||
### Export pour analyse
|
||||
```bash
|
||||
# Export de toutes les villes avec formatage
|
||||
curl "https://osm-commerces.cipherbliss.com/api/v1/stats/export?pretty=true" > analyse_villes.json
|
||||
|
||||
# Export d'une ville spécifique
|
||||
curl "https://osm-commerces.cipherbliss.com/api/v1/stats/export?zone=75056&pretty=true" > paris.json
|
||||
```
|
||||
|
||||
### Export pour traitement automatisé
|
||||
```bash
|
||||
# Export compact pour traitement par script
|
||||
curl "https://osm-commerces.cipherbliss.com/api/v1/stats/export" > stats_compact.json
|
||||
```
|
||||
|
||||
### Export avec données complètes
|
||||
```bash
|
||||
# Export avec tous les lieux (attention: peut être volumineux)
|
||||
curl "https://osm-commerces.cipherbliss.com/api/v1/stats/export?include_places=true&pretty=true" > stats_complet.json
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
- L'option `include_places=true` peut générer des fichiers très volumineux pour les grandes villes
|
||||
- Les requêtes avec `include_places=true` peuvent être plus lentes
|
||||
- Le formatage JSON (`pretty=true`) augmente la taille de la réponse
|
|
@ -1,68 +0,0 @@
|
|||
# Changes Implemented on 2025-08-02
|
||||
|
||||
## Issue Description
|
||||
|
||||
1. Create a command to remove duplicate Places, keeping only one. Sort places by OSM object type and OSM ID, and delete if the same information is found twice in a row.
|
||||
|
||||
2. When processing a city ("labourage"), use the city name found in the API that links INSEE code to city name to define the city name if it's different. Also use this for the command that creates Stats objects from Requests, not the place name from the request.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. New Command to Remove Duplicate Places
|
||||
|
||||
Created a new command `app:remove-duplicate-places` that:
|
||||
- Gets all places sorted by OSM type and ID
|
||||
- Finds duplicates by comparing consecutive places
|
||||
- Removes the duplicates, keeping the first occurrence
|
||||
|
||||
The command supports:
|
||||
- `--dry-run` option to show duplicates without removing them
|
||||
- `--zip-code` option to process only places with a specific ZIP code
|
||||
|
||||
Usage:
|
||||
```bash
|
||||
# Show duplicates without removing them
|
||||
php bin/console app:remove-duplicate-places --dry-run
|
||||
|
||||
# Remove duplicates for a specific ZIP code
|
||||
php bin/console app:remove-duplicate-places --zip-code=75001
|
||||
|
||||
# Remove all duplicates
|
||||
php bin/console app:remove-duplicate-places
|
||||
```
|
||||
|
||||
### 2. City Name Updates from API
|
||||
|
||||
#### During Labourage Process
|
||||
|
||||
Modified `ProcessLabourageQueueCommand` to update the city name from the API after labourage:
|
||||
- After setting the labourage completion date, it gets the city name from the API using the `get_city_osm_from_zip_code` method
|
||||
- If the API returns a city name and it's different from the current name, it updates the Stats entity with the new name
|
||||
- It logs the name change for information
|
||||
|
||||
#### When Creating Stats from Requests
|
||||
|
||||
Modified `CreateStatsFromDemandesCommand` to use the API-provided city name:
|
||||
- It now tries to get the city name from the API based on the INSEE code
|
||||
- If the API returns a city name, it uses that for the Stats object
|
||||
- If the API doesn't return a name, it falls back to using the query from the first Demande (the previous behavior)
|
||||
- It logs which source was used for the city name
|
||||
|
||||
## Testing
|
||||
|
||||
To test these changes:
|
||||
|
||||
1. Test the duplicate removal command:
|
||||
```bash
|
||||
php bin/console app:remove-duplicate-places --dry-run
|
||||
```
|
||||
|
||||
2. Test the city name updates during labourage:
|
||||
```bash
|
||||
php bin/console app:process-labourage-queue
|
||||
```
|
||||
|
||||
3. Test the Stats creation from Requests:
|
||||
```bash
|
||||
php bin/console app:create-stats-from-demandes
|
||||
```
|
|
@ -1,79 +0,0 @@
|
|||
# Création des objets Stats manquants à partir du CSV des communes
|
||||
|
||||
Cette documentation explique comment utiliser la nouvelle fonctionnalité pour créer des objets Stats manquants à partir du fichier CSV des communes françaises.
|
||||
|
||||
## Fonctionnalité
|
||||
|
||||
La route `/admin/create-missing-stats-from-csv` permet d'examiner le fichier CSV des communes françaises (`communes_france.csv`) et de créer des objets Stats pour les communes qui n'en ont pas encore dans la base de données. Les objets Stats sont d'abord créés avec les informations du CSV, puis complétés avec des données supplémentaires (coordonnées, budget, etc.) lors de la sauvegarde par paquet de 100, sans effectuer de "labourage" (traitement complet des données OSM).
|
||||
|
||||
Les objets Stats sont sauvegardés par paquets de 100 pour optimiser les performances et éviter les problèmes de mémoire.
|
||||
|
||||
## Prérequis
|
||||
|
||||
1. Le fichier `communes_france.csv` doit exister à la racine du projet
|
||||
2. Le fichier doit contenir au minimum les colonnes suivantes :
|
||||
- `code` : Le code INSEE de la commune
|
||||
- `nom` : Le nom de la commune
|
||||
|
||||
Si le fichier n'existe pas, vous pouvez le générer en exécutant le script `fetch_communes.py` :
|
||||
|
||||
```bash
|
||||
python fetch_communes.py
|
||||
```
|
||||
|
||||
## Utilisation
|
||||
|
||||
1. Accédez à la route `/admin/create-missing-stats-from-csv` dans votre navigateur
|
||||
2. Le processus s'exécute automatiquement et affiche un message de succès une fois terminé
|
||||
3. Vous serez redirigé vers la page d'administration principale
|
||||
|
||||
## Données importées
|
||||
|
||||
Les données suivantes sont importées du CSV pour chaque commune :
|
||||
|
||||
- `code` → `zone` (code INSEE)
|
||||
- `nom` → `name` (nom de la commune)
|
||||
- `population` → `population` (nombre d'habitants)
|
||||
- `codesPostaux` → `codesPostaux` (codes postaux, séparés par des virgules)
|
||||
- `siren` → `siren` (numéro SIREN)
|
||||
- `codeEpci` → `codeEpci` (code EPCI)
|
||||
|
||||
Les données suivantes sont récupérées automatiquement lors de la sauvegarde par paquet de 100 :
|
||||
|
||||
- `lat` et `lon` : Coordonnées géographiques (récupérées via l'API geo.api.gouv.fr)
|
||||
- `budget_annuel` : Budget annuel de la commune (récupéré via le service BudgetService)
|
||||
- `completion_percent` : Pourcentage de complétion (calculé automatiquement)
|
||||
|
||||
Les objets Stats créés ont également les propriétés suivantes :
|
||||
- `date_created` : Date de création (date actuelle)
|
||||
- `date_modified` : Date de modification (date actuelle)
|
||||
- `kind` : 'command' (indique que l'objet a été créé par une commande)
|
||||
|
||||
## Vérification
|
||||
|
||||
Après avoir exécuté la fonctionnalité, un message de succès s'affiche avec les informations suivantes :
|
||||
- Nombre de communes ajoutées
|
||||
- Nombre de communes déjà existantes (ignorées)
|
||||
- Nombre d'erreurs rencontrées
|
||||
|
||||
Vous pouvez également vérifier dans la base de données que les objets Stats ont bien été créés pour les communes manquantes.
|
||||
|
||||
## Journalisation
|
||||
|
||||
Les actions et erreurs sont journalisées via le service `ActionLogger` :
|
||||
- `admin/create_missing_stats_from_csv` : Journalise le début de l'action
|
||||
- `error_create_missing_stats_from_csv` : Journalise les erreurs rencontrées lors de la création des objets Stats
|
||||
- `error_complete_stats_data` : Journalise les erreurs rencontrées lors de la récupération des données supplémentaires (coordonnées, budget, etc.)
|
||||
|
||||
## Différence avec importStatsFromCsv
|
||||
|
||||
Cette fonctionnalité est similaire à `importStatsFromCsv`, mais avec quelques différences importantes :
|
||||
- Elle est spécifiquement conçue pour créer des objets Stats manquants
|
||||
- Elle utilise 'command' comme valeur pour le champ `kind` (au lieu de 'request')
|
||||
- Elle journalise les erreurs de manière plus détaillée
|
||||
|
||||
## Exemple de message de succès
|
||||
|
||||
```
|
||||
Création des Stats manquantes terminée : 123 communes ajoutées, 456 déjà existantes, 0 erreurs.
|
||||
```
|
|
@ -1,142 +0,0 @@
|
|||
# Commande d'export des objets Stats
|
||||
|
||||
## Description
|
||||
|
||||
La commande `app:export-stats` permet d'exporter les objets Stats au format JSON avec leurs propriétés de nom et de décomptes.
|
||||
|
||||
## Utilisation
|
||||
|
||||
### Export de tous les objets Stats
|
||||
```bash
|
||||
php bin/console app:export-stats
|
||||
```
|
||||
|
||||
### Export avec formatage JSON
|
||||
```bash
|
||||
php bin/console app:export-stats --pretty
|
||||
```
|
||||
|
||||
### Export vers un fichier spécifique
|
||||
```bash
|
||||
php bin/console app:export-stats --output=mon_export.json
|
||||
```
|
||||
|
||||
### Export d'une zone spécifique
|
||||
```bash
|
||||
php bin/console app:export-stats --zone=75056
|
||||
```
|
||||
|
||||
### Export avec toutes les options
|
||||
```bash
|
||||
php bin/console app:export-stats --output=paris_stats.json --zone=75056 --pretty
|
||||
```
|
||||
|
||||
### Export avec mode verbeux
|
||||
```bash
|
||||
php bin/console app:export-stats -v
|
||||
```
|
||||
|
||||
## Options disponibles
|
||||
|
||||
- `--output, -o` : Fichier de sortie (défaut: `stats_export.json`)
|
||||
- `--zone, -z` : Code INSEE spécifique à exporter (optionnel)
|
||||
- `--pretty, -p` : Formater le JSON avec indentation
|
||||
- `-v, --verbose` : Mode verbeux pour afficher un aperçu des données
|
||||
|
||||
## Structure des données exportées
|
||||
|
||||
Le fichier JSON contient un tableau d'objets avec la structure suivante :
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"zone": "75056",
|
||||
"name": "Paris",
|
||||
"dateCreated": "2024-01-15 10:30:00",
|
||||
"dateModified": "2024-01-20 14:45:00",
|
||||
"population": 2161000,
|
||||
"budgetAnnuel": "8500000000",
|
||||
"siren": "200054781",
|
||||
"codeEpci": "200054781",
|
||||
"codesPostaux": "75001;75002;75003;...",
|
||||
"decomptes": {
|
||||
"placesCount": 1250,
|
||||
"avecHoraires": 980,
|
||||
"avecAdresse": 1200,
|
||||
"avecSite": 850,
|
||||
"avecAccessibilite": 450,
|
||||
"avecNote": 320,
|
||||
"completionPercent": 75,
|
||||
"placesCountReal": 1250
|
||||
},
|
||||
"followups": [
|
||||
{
|
||||
"name": "fire_hydrant_count",
|
||||
"measure": 1250,
|
||||
"date": "2024-01-20 14:45:00"
|
||||
},
|
||||
{
|
||||
"name": "fire_hydrant_completion",
|
||||
"measure": 85.5,
|
||||
"date": "2024-01-20 14:45:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Propriétés exportées
|
||||
|
||||
### Informations générales
|
||||
- `id` : Identifiant unique de l'objet Stats
|
||||
- `zone` : Code INSEE de la zone
|
||||
- `name` : Nom de la ville/zone
|
||||
- `dateCreated` : Date de création
|
||||
- `dateModified` : Date de dernière modification
|
||||
|
||||
### Données démographiques et administratives
|
||||
- `population` : Population de la zone
|
||||
- `budgetAnnuel` : Budget annuel de la collectivité
|
||||
- `siren` : Code SIREN
|
||||
- `codeEpci` : Code EPCI
|
||||
- `codesPostaux` : Codes postaux de la zone
|
||||
|
||||
### Décomptes
|
||||
- `placesCount` : Nombre de lieux enregistrés
|
||||
- `avecHoraires` : Nombre de lieux avec horaires d'ouverture
|
||||
- `avecAdresse` : Nombre de lieux avec adresse complète
|
||||
- `avecSite` : Nombre de lieux avec site web
|
||||
- `avecAccessibilite` : Nombre de lieux avec accessibilité PMR
|
||||
- `avecNote` : Nombre de lieux avec note
|
||||
- `completionPercent` : Pourcentage de complétion global
|
||||
- `placesCountReal` : Nombre réel de lieux (comptage direct)
|
||||
|
||||
### Followups
|
||||
- `followups` : Tableau des mesures de suivi (CityFollowUp)
|
||||
- `name` : Nom de la mesure
|
||||
- `measure` : Valeur de la mesure
|
||||
- `date` : Date de la mesure
|
||||
|
||||
## Exemples d'utilisation
|
||||
|
||||
### Export pour analyse
|
||||
```bash
|
||||
# Export de toutes les villes avec formatage
|
||||
php bin/console app:export-stats --pretty --output=analyse_villes.json
|
||||
|
||||
# Export d'une ville spécifique
|
||||
php bin/console app:export-stats --zone=75056 --pretty --output=paris.json
|
||||
```
|
||||
|
||||
### Export pour traitement automatisé
|
||||
```bash
|
||||
# Export compact pour traitement par script
|
||||
php bin/console app:export-stats --output=stats_compact.json
|
||||
```
|
||||
|
||||
### Vérification des données
|
||||
```bash
|
||||
# Export avec aperçu des données
|
||||
php bin/console app:export-stats --pretty -v
|
||||
```
|
|
@ -1,81 +0,0 @@
|
|||
# Commande d'import des mesures thématiques depuis CSV
|
||||
|
||||
Cette documentation décrit l'utilisation de la commande `app:import-cityfollowup-csv` qui permet d'importer des mesures thématiques depuis des fichiers CSV pour une ville donnée.
|
||||
|
||||
## Description
|
||||
|
||||
La commande `app:import-cityfollowup-csv` permet d'importer des données de suivi thématique (CityFollowUp) depuis des fichiers CSV générés par les scripts de comptage d'objets OSM. Elle compare les mesures existantes en base de données avec celles du fichier CSV et ajoute uniquement les nouvelles mesures, en excluant celles qui ont une valeur de 0.
|
||||
|
||||
## Prérequis
|
||||
|
||||
- Les fichiers CSV doivent être présents dans le répertoire `/counting_osm_objects/test_results/`
|
||||
- Le format du nom de fichier doit être `commune_{code_insee}_{theme}.csv`
|
||||
- La ville (entité Stats) doit exister en base de données avec le code INSEE correspondant
|
||||
|
||||
## Format du fichier CSV
|
||||
|
||||
Le fichier CSV doit contenir les colonnes suivantes :
|
||||
- `date` : Date de la mesure (format YYYY-MM-DD)
|
||||
- `zone` : Zone de la mesure (format commune_{code_insee})
|
||||
- `theme` : Thématique de la mesure (ex: borne-de-recharge)
|
||||
- `nombre_total` : Nombre total d'objets
|
||||
- `nombre_avec_operator` : Nombre d'objets avec un opérateur (optionnel)
|
||||
- `nombre_avec_capacity` : Nombre d'objets avec une capacité (optionnel)
|
||||
- `pourcentage_completion` : Pourcentage de complétion (optionnel)
|
||||
|
||||
## Utilisation
|
||||
|
||||
```bash
|
||||
php bin/console app:import-cityfollowup-csv <insee> <theme> [--dry-run]
|
||||
```
|
||||
|
||||
### Arguments
|
||||
|
||||
- `insee` : Code INSEE de la ville (obligatoire)
|
||||
- `theme` : Thématique à importer (ex: borne-de-recharge, defibrillator) (obligatoire)
|
||||
|
||||
### Options
|
||||
|
||||
- `--dry-run` : Simule l'import sans modifier la base de données
|
||||
|
||||
## Exemples
|
||||
|
||||
### Import des bornes de recharge pour Paris
|
||||
|
||||
```bash
|
||||
php bin/console app:import-cityfollowup-csv 75056 borne-de-recharge
|
||||
```
|
||||
|
||||
### Simulation d'import des défibrillateurs pour Paris
|
||||
|
||||
```bash
|
||||
php bin/console app:import-cityfollowup-csv 75056 defibrillator --dry-run
|
||||
```
|
||||
|
||||
## Comportement
|
||||
|
||||
1. La commande vérifie d'abord que la ville existe en base de données avec le code INSEE fourni.
|
||||
2. Elle recherche ensuite le fichier CSV correspondant dans le répertoire des résultats.
|
||||
3. Elle lit le contenu du fichier CSV et récupère les mesures existantes en base de données.
|
||||
4. Pour chaque ligne du CSV :
|
||||
- Les lignes avec des valeurs manquantes sont ignorées
|
||||
- Les mesures avec une valeur de 0 sont ignorées
|
||||
- Les mesures déjà existantes en base de données sont ignorées
|
||||
- Les nouvelles mesures sont ajoutées en base de données
|
||||
5. La commande affiche un résumé des opérations effectuées.
|
||||
|
||||
## Correspondance des thématiques
|
||||
|
||||
La commande effectue une conversion entre les noms de thématiques utilisés dans les fichiers CSV et ceux utilisés dans la base de données :
|
||||
|
||||
| Nom dans le CSV | Nom dans CityFollowUp |
|
||||
|-----------------|------------------------|
|
||||
| borne-de-recharge | charging_station |
|
||||
| defibrillator | defibrillator |
|
||||
| ... | ... |
|
||||
|
||||
## Notes
|
||||
|
||||
- Les mesures sont importées par lots de 50 pour éviter de surcharger la mémoire.
|
||||
- Seules les mesures qui n'existent pas déjà en base de données sont importées.
|
||||
- Les mesures avec une valeur de 0 sont ignorées conformément aux spécifications.
|
|
@ -1,124 +0,0 @@
|
|||
# Import d'objets Stats
|
||||
|
||||
Cette fonctionnalité permet d'importer des objets Stats à partir d'un fichier JSON via l'interface d'administration.
|
||||
|
||||
## Accès
|
||||
|
||||
La page d'import est accessible via :
|
||||
- L'URL : `/admin/import-stats`
|
||||
- Le menu de navigation : "Import Stats"
|
||||
|
||||
## Fonctionnalités
|
||||
|
||||
### Sécurité
|
||||
- **Aucune modification** des objets Stats existants
|
||||
- Seuls les nouveaux objets sont créés
|
||||
- Vérification de l'existence par le code INSEE (`zone`)
|
||||
|
||||
### Validation
|
||||
- Vérification du format JSON
|
||||
- Validation des champs requis (`zone` et `name`)
|
||||
- Gestion des erreurs par ligne
|
||||
- Rapport détaillé des résultats
|
||||
|
||||
### Champs supportés
|
||||
|
||||
#### Champs requis
|
||||
- `zone` : Code INSEE de la zone (ex: "75056")
|
||||
- `name` : Nom de la ville/zone (ex: "Paris")
|
||||
|
||||
#### Champs optionnels
|
||||
- `population` : Population de la zone (nombre)
|
||||
- `budgetAnnuel` : Budget annuel de la collectivité (chaîne)
|
||||
- `siren` : Code SIREN (nombre)
|
||||
- `codeEpci` : Code EPCI (nombre)
|
||||
- `codesPostaux` : Codes postaux séparés par des points-virgules (ex: "75001;75002;75003")
|
||||
|
||||
#### Objet `decomptes` (optionnel)
|
||||
- `placesCount` : Nombre total de lieux
|
||||
- `avecHoraires` : Nombre de lieux avec horaires
|
||||
- `avecAdresse` : Nombre de lieux avec adresse
|
||||
- `avecSite` : Nombre de lieux avec site web
|
||||
- `avecAccessibilite` : Nombre de lieux avec accessibilité
|
||||
- `avecNote` : Nombre de lieux avec note
|
||||
- `completionPercent` : Pourcentage de complétion
|
||||
|
||||
## Format JSON attendu
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"zone": "75056",
|
||||
"name": "Paris",
|
||||
"population": 2161000,
|
||||
"budgetAnnuel": "8500000000",
|
||||
"siren": 200054781,
|
||||
"codeEpci": 200054781,
|
||||
"codesPostaux": "75001;75002;75003",
|
||||
"decomptes": {
|
||||
"placesCount": 1250,
|
||||
"avecHoraires": 980,
|
||||
"avecAdresse": 1200,
|
||||
"avecSite": 850,
|
||||
"avecAccessibilite": 450,
|
||||
"avecNote": 320,
|
||||
"completionPercent": 75
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Utilisation
|
||||
|
||||
1. **Préparer le fichier JSON**
|
||||
- Créer un tableau d'objets Stats
|
||||
- Inclure au minimum les champs `zone` et `name`
|
||||
- Valider le format JSON
|
||||
|
||||
2. **Accéder à la page d'import**
|
||||
- Aller sur `/admin/import-stats`
|
||||
- Ou cliquer sur "Import Stats" dans le menu
|
||||
|
||||
3. **Importer le fichier**
|
||||
- Sélectionner le fichier JSON
|
||||
- Cliquer sur "Importer"
|
||||
- Vérifier les messages de résultat
|
||||
|
||||
4. **Vérifier les résultats**
|
||||
- Nombre d'objets créés
|
||||
- Nombre d'objets ignorés (déjà existants)
|
||||
- Liste des erreurs éventuelles
|
||||
|
||||
## Messages de retour
|
||||
|
||||
### Succès
|
||||
```
|
||||
Import terminé : X objet(s) créé(s), Y objet(s) ignoré(s) (déjà existants).
|
||||
```
|
||||
|
||||
### Erreurs possibles
|
||||
- "Aucun fichier JSON n'a été fourni."
|
||||
- "Le fichier doit être au format JSON."
|
||||
- "Erreur lors du décodage JSON: [message]"
|
||||
- "Le fichier JSON doit contenir un tableau d'objets Stats."
|
||||
- "Ligne X: Champs 'zone' et 'name' requis"
|
||||
- "Ligne X: [message d'erreur spécifique]"
|
||||
|
||||
## Logs
|
||||
|
||||
Toutes les actions d'import sont loggées via le service `ActionLogger` :
|
||||
- `admin/import_stats` : Accès à la page
|
||||
- `admin/import_stats_success` : Import réussi avec statistiques
|
||||
- `admin/import_stats_error` : Erreur lors de l'import
|
||||
|
||||
## Exemple de fichier de test
|
||||
|
||||
Un fichier `test_import_stats.json` est fourni avec des exemples pour Paris, Lyon et Marseille.
|
||||
|
||||
## Notes importantes
|
||||
|
||||
- Les objets existants ne sont jamais modifiés
|
||||
- Seuls les nouveaux objets sont créés
|
||||
- Les dates de création et modification sont automatiquement définies
|
||||
- Les erreurs sont affichées par ligne pour faciliter le débogage
|
||||
- Le système est conçu pour être sûr et non destructif
|
|
@ -1,126 +0,0 @@
|
|||
# Intégration des analyses Osmose
|
||||
|
||||
Ce document décrit l'intégration des analyses Osmose dans les pages détaillées de thématique, à la fois pour les routes publiques et administratives.
|
||||
|
||||
## Fonctionnalités implémentées
|
||||
|
||||
1. Affichage d'une carte avec les analyses Osmose pour la thématique courante
|
||||
2. Affichage des analyses sous forme de points violets sur la carte
|
||||
3. Popups interactives avec:
|
||||
- Titre et description de l'analyse
|
||||
- Tags proposés (tableau clé-valeur)
|
||||
- Bouton pour voir l'analyse sur Osmose
|
||||
- Bouton pour réparer dans JOSM via la télécommande
|
||||
|
||||
## Thèmes supportés
|
||||
|
||||
Les thèmes suivants sont actuellement supportés avec leurs IDs d'items Osmose correspondants:
|
||||
|
||||
| Thème | IDs Osmose |
|
||||
|-------|------------|
|
||||
| charging_station (Bornes de recharge) | 8410, 8411 |
|
||||
| school (Écoles) | 8031 |
|
||||
| healthcare (Santé) | 8211, 7220, 8331 |
|
||||
| laboratory (Laboratoires d'analyses) | 7240, 8351 |
|
||||
| police (Commissariats) | 8190, 8191 |
|
||||
| defibrillator (Défibrillateurs) | 8370 |
|
||||
|
||||
Pour ajouter de nouveaux thèmes, modifiez la constante `osmoseItemsMapping` dans les deux fichiers suivants:
|
||||
- `templates/public/followup_graph.html.twig` (route publique)
|
||||
- `templates/admin/followup_theme_graph.html.twig` (route administrative)
|
||||
|
||||
Assurez-vous que les mappages sont identiques dans les deux fichiers pour garantir un comportement cohérent.
|
||||
|
||||
## Fonctionnement technique
|
||||
|
||||
L'intégration est implémentée à la fois sur la route publique (`templates/public/followup_graph.html.twig`) et sur la route administrative (`templates/admin/followup_theme_graph.html.twig`). Le fonctionnement est similaire sur les deux routes:
|
||||
|
||||
1. La carte est initialisée avec MapLibre GL JS
|
||||
2. Les coordonnées de la commune sont récupérées via l'API Geo.gouv.fr
|
||||
3. Une bounding box est calculée autour du centre de la commune
|
||||
4. Les analyses Osmose sont récupérées via l'API Osmose avec les paramètres:
|
||||
- Les IDs d'items correspondant au thème
|
||||
- La bounding box calculée
|
||||
- Les niveaux de sévérité 1, 2 et 3
|
||||
- Une limite de 500 résultats
|
||||
5. Les analyses sont affichées sous forme de points violets sur la carte
|
||||
6. Au clic sur un point, une popup s'ouvre et charge les détails de l'analyse via l'API Osmose
|
||||
7. La popup affiche les tags proposés et des boutons d'action
|
||||
|
||||
### Différences entre les routes
|
||||
|
||||
- **Route publique**: La carte affiche uniquement les analyses Osmose.
|
||||
- **Route administrative**: La carte affiche à la fois les objets OSM existants (récupérés via Overpass API) et les analyses Osmose, permettant une comparaison directe entre les objets existants et les suggestions d'ajout.
|
||||
|
||||
## Exemple d'URL d'API Osmose
|
||||
|
||||
Pour les bornes de recharge (charging_station):
|
||||
```
|
||||
https://osmose.openstreetmap.fr/api/0.3/issues?zoom=12&item=8410,8411&level=1,2,3&limit=500&bbox=-0.789642333984375,47.35905994178323,-0.3203201293945313,47.598060753627195
|
||||
```
|
||||
|
||||
Pour récupérer les détails d'une analyse:
|
||||
```
|
||||
https://osmose.openstreetmap.fr/api/0.3/issue/5c319a16-3689-b8c7-5427-187e04a6042a?langs=auto
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
Pour tester l'intégration:
|
||||
|
||||
### Route publique
|
||||
|
||||
1. Accédez à une page de thématique détaillée publique, par exemple:
|
||||
- `/stats/12345/followup-graph/charging_station` pour les bornes de recharge
|
||||
- `/stats/12345/followup-graph/school` pour les écoles
|
||||
- `/stats/12345/followup-graph/healthcare` pour les lieux de santé
|
||||
|
||||
2. Vérifiez que:
|
||||
- La carte s'affiche correctement
|
||||
- Les points violets apparaissent sur la carte (s'il y a des analyses Osmose pour cette thématique dans la zone)
|
||||
- Au clic sur un point, une popup s'ouvre avec les détails de l'analyse
|
||||
- Les boutons "Voir sur Osmose" et "Réparer dans JOSM" fonctionnent correctement
|
||||
|
||||
### Route administrative
|
||||
|
||||
1. Accédez à une page de thématique détaillée administrative, par exemple:
|
||||
- `/admin/stats/12345/followup-graph/charging_station` pour les bornes de recharge
|
||||
- `/admin/stats/12345/followup-graph/school` pour les écoles
|
||||
- `/admin/stats/12345/followup-graph/healthcare` pour les lieux de santé
|
||||
|
||||
2. Vérifiez que:
|
||||
- La carte s'affiche correctement avec les objets OSM existants
|
||||
- Les points violets des analyses Osmose apparaissent également sur la carte
|
||||
- Au clic sur un point violet, une popup s'ouvre avec les détails de l'analyse
|
||||
- Les boutons "Voir sur Osmose" et "Réparer dans JOSM" fonctionnent correctement
|
||||
|
||||
## Dépannage
|
||||
|
||||
### Problèmes généraux
|
||||
|
||||
Si la carte ne s'affiche pas correctement:
|
||||
- Vérifiez que la variable d'environnement `MAPTILER_TOKEN` est correctement définie
|
||||
- Vérifiez les erreurs dans la console JavaScript du navigateur
|
||||
|
||||
Si les analyses Osmose ne s'affichent pas:
|
||||
- Vérifiez que le thème a des IDs d'items Osmose associés dans `osmoseItemsMapping`
|
||||
- Vérifiez qu'il y a des analyses Osmose pour ce thème dans la zone
|
||||
- Vérifiez les erreurs dans la console JavaScript du navigateur
|
||||
|
||||
### Erreurs liées à l'API Osmose
|
||||
|
||||
Si vous rencontrez l'erreur "TypeError: right-hand side of 'in' should be an object, got undefined":
|
||||
- Cette erreur peut se produire si l'API Osmose renvoie une réponse où `data.issue` est undefined
|
||||
- Vérifiez que la fonction `loadOsmoseIssueDetails` contient bien la vérification `if (!data || !data.issue) return;` avant d'utiliser `data.issue`
|
||||
- Si l'erreur persiste, vérifiez la structure de la réponse de l'API Osmose en ajoutant `console.log(data)` avant d'utiliser `data.issue`
|
||||
|
||||
### Problèmes spécifiques à la route administrative
|
||||
|
||||
Si les analyses Osmose s'affichent sur la route publique mais pas sur la route administrative:
|
||||
- Vérifiez que la constante `osmoseItemsMapping` est correctement définie dans `templates/admin/followup_theme_graph.html.twig`
|
||||
- Vérifiez que le code d'initialisation des analyses Osmose est appelé après l'initialisation de la carte
|
||||
- Vérifiez que les analyses Osmose ne sont pas masquées par d'autres éléments de la carte (comme les marqueurs des objets OSM existants)
|
||||
|
||||
Si les objets OSM existants et les analyses Osmose se chevauchent de manière confuse:
|
||||
- Les analyses Osmose sont affichées avec des marqueurs violets pour les distinguer des objets OSM existants
|
||||
- Vous pouvez ajuster la couleur des marqueurs Osmose en modifiant la valeur `color: '#8A2BE2'` dans la fonction `loadOsmoseAnalyses`
|
|
@ -1,244 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Script to fetch all communes in France from the geo.api.gouv.fr API
|
||||
and save them to a CSV file with all available information.
|
||||
"""
|
||||
|
||||
import csv
|
||||
import json
|
||||
import requests
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
# Configuration
|
||||
BASE_URL = "https://geo.api.gouv.fr"
|
||||
OUTPUT_FILE = "communes_france.csv"
|
||||
REQUEST_DELAY = 0.5 # Delay between API requests in seconds to avoid rate limiting
|
||||
|
||||
def fetch_departments():
|
||||
"""Fetch the list of all departments in France.
|
||||
|
||||
Department numbers go from 1 to 95 (metropolitan France),
|
||||
then from 971 to 976 (overseas departments).
|
||||
"""
|
||||
# Create a list of all department codes
|
||||
dept_codes = []
|
||||
|
||||
# Metropolitan departments (01-95)
|
||||
for i in range(1, 96):
|
||||
# Format with leading zero for single-digit departments
|
||||
dept_codes.append(f"{i:02d}")
|
||||
|
||||
# Special case for Corsica (2A and 2B instead of 20)
|
||||
if "20" in dept_codes:
|
||||
dept_codes.remove("20")
|
||||
dept_codes.extend(["2A", "2B"])
|
||||
|
||||
# Overseas departments (971-976)
|
||||
for i in range(971, 977):
|
||||
dept_codes.append(str(i))
|
||||
|
||||
# Fetch department details from the API
|
||||
url = f"{BASE_URL}/departements"
|
||||
response = requests.get(url)
|
||||
response.raise_for_status() # Raise an exception for HTTP errors
|
||||
api_departments = response.json()
|
||||
|
||||
# Create a mapping of department code to full department info
|
||||
dept_map = {dept["code"]: dept for dept in api_departments}
|
||||
|
||||
# Build the final list of departments, ensuring all required codes are included
|
||||
departments = []
|
||||
for code in dept_codes:
|
||||
if code in dept_map:
|
||||
# Use the data from the API if available
|
||||
departments.append(dept_map[code])
|
||||
else:
|
||||
# Create a minimal department object if not in the API
|
||||
departments.append({
|
||||
"nom": f"Département {code}",
|
||||
"code": code,
|
||||
"codeRegion": ""
|
||||
})
|
||||
print(f"Warning: Department {code} not found in API, using placeholder data")
|
||||
|
||||
return departments
|
||||
|
||||
def fetch_communes_for_department(dept_code):
|
||||
"""Fetch all communes for a specific department."""
|
||||
url = f"{BASE_URL}/departements/{dept_code}/communes"
|
||||
print(f"Fetching communes for department {dept_code}...")
|
||||
response = requests.get(url)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def main():
|
||||
# Create output directory if it doesn't exist
|
||||
output_path = Path(OUTPUT_FILE)
|
||||
output_path.parent.mkdir(exist_ok=True)
|
||||
|
||||
# Check if the CSV file already exists
|
||||
existing_communes = {}
|
||||
existing_headers = []
|
||||
|
||||
if output_path.exists():
|
||||
print(f"CSV file {OUTPUT_FILE} already exists. Reading existing communes...")
|
||||
try:
|
||||
with open(output_path, 'r', newline='', encoding='utf-8') as csvfile:
|
||||
reader = csv.DictReader(csvfile)
|
||||
existing_headers = reader.fieldnames
|
||||
for row in reader:
|
||||
# Use the INSEE code as the key to avoid duplicates
|
||||
if 'code' in row and row['code']:
|
||||
existing_communes[row['code']] = row
|
||||
print(f"Read {len(existing_communes)} existing communes from CSV file.")
|
||||
except Exception as e:
|
||||
print(f"Error reading existing CSV file: {e}")
|
||||
print("Will create a new file.")
|
||||
existing_communes = {}
|
||||
|
||||
# Fetch all departments
|
||||
try:
|
||||
departments = fetch_departments()
|
||||
print(f"Found {len(departments)} departments")
|
||||
|
||||
# Prepare to collect all communes
|
||||
new_communes = []
|
||||
|
||||
# Fetch communes for each department
|
||||
for dept in departments:
|
||||
dept_code = dept['code']
|
||||
try:
|
||||
# Skip department 975 (Saint-Pierre-et-Miquelon) if it's a placeholder
|
||||
# as it might not be available in the API
|
||||
if dept_code == "975" and dept['nom'] == "Département 975":
|
||||
print(f" - Skipping department {dept_code} (placeholder, not available in API)")
|
||||
continue
|
||||
|
||||
communes = fetch_communes_for_department(dept_code)
|
||||
|
||||
# Filter out communes that already exist in the CSV
|
||||
new_dept_communes = []
|
||||
for commune in communes:
|
||||
if commune['code'] not in existing_communes:
|
||||
new_dept_communes.append(commune)
|
||||
|
||||
if new_dept_communes:
|
||||
new_communes.extend(new_dept_communes)
|
||||
print(f" - Added {len(new_dept_communes)} new communes from department {dept_code} ({dept['nom']})")
|
||||
else:
|
||||
print(f" - No new communes found for department {dept_code} ({dept['nom']})")
|
||||
|
||||
time.sleep(REQUEST_DELAY) # Be nice to the API
|
||||
except Exception as e:
|
||||
print(f"Error fetching communes for department {dept_code}: {e}")
|
||||
|
||||
print(f"Total new communes found: {len(new_communes)}")
|
||||
|
||||
# If no new communes and no existing communes, exit
|
||||
if not new_communes and not existing_communes:
|
||||
print("No communes found. Exiting.")
|
||||
return
|
||||
|
||||
# Process new communes
|
||||
if new_communes:
|
||||
# Get all possible fields from the first commune
|
||||
first_commune = new_communes[0]
|
||||
headers = list(first_commune.keys())
|
||||
|
||||
# Special handling for nested fields like codesPostaux
|
||||
for commune in new_communes:
|
||||
for key, value in commune.items():
|
||||
if isinstance(value, list) and key == "codesPostaux":
|
||||
commune[key] = "|".join(str(v) for v in value)
|
||||
elif isinstance(value, dict) and key == "centre":
|
||||
# Handle coordinates if they exist
|
||||
if "coordinates" in value:
|
||||
commune["longitude"] = value["coordinates"][0]
|
||||
commune["latitude"] = value["coordinates"][1]
|
||||
commune.pop(key, None) # Remove the original nested dict
|
||||
|
||||
# Update headers if we added new fields
|
||||
if "centre" in headers:
|
||||
headers.remove("centre")
|
||||
if any("longitude" in c for c in new_communes):
|
||||
headers.extend(["longitude", "latitude"])
|
||||
else:
|
||||
# If no new communes, use existing headers
|
||||
headers = existing_headers
|
||||
|
||||
# Combine existing and new communes
|
||||
all_communes = list(existing_communes.values())
|
||||
|
||||
# Add new communes to the list
|
||||
for commune in new_communes:
|
||||
# Convert commune to a row with all headers
|
||||
row = {header: commune.get(header, '') for header in headers}
|
||||
all_communes.append(row)
|
||||
|
||||
# Write to CSV
|
||||
with open(output_path, 'w', newline='', encoding='utf-8') as csvfile:
|
||||
writer = csv.DictWriter(csvfile, fieldnames=headers)
|
||||
writer.writeheader()
|
||||
for commune in all_communes:
|
||||
writer.writerow(commune)
|
||||
|
||||
if new_communes:
|
||||
print(f"CSV file updated successfully with {len(new_communes)} new communes: {output_path}")
|
||||
else:
|
||||
print(f"No new communes added. CSV file remains unchanged: {output_path}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"An error occurred: {e}")
|
||||
|
||||
def test_sample():
|
||||
"""Run a test with a small sample of departments."""
|
||||
# Sample departments: one metropolitan (01), Corsica (2A), and one overseas (971)
|
||||
sample_dept_codes = ["01", "2A", "971"]
|
||||
|
||||
print(f"Testing with sample departments: {', '.join(sample_dept_codes)}")
|
||||
|
||||
# Fetch department details from the API
|
||||
url = f"{BASE_URL}/departements"
|
||||
response = requests.get(url)
|
||||
response.raise_for_status()
|
||||
api_departments = response.json()
|
||||
|
||||
# Create a mapping of department code to full department info
|
||||
dept_map = {dept["code"]: dept for dept in api_departments}
|
||||
|
||||
# Prepare to collect all communes
|
||||
all_communes = []
|
||||
|
||||
# Fetch communes for each sample department
|
||||
for dept_code in sample_dept_codes:
|
||||
if dept_code in dept_map:
|
||||
dept = dept_map[dept_code]
|
||||
try:
|
||||
communes = fetch_communes_for_department(dept_code)
|
||||
all_communes.extend(communes)
|
||||
print(f" - Added {len(communes)} communes from department {dept_code} ({dept['nom']})")
|
||||
time.sleep(REQUEST_DELAY)
|
||||
except Exception as e:
|
||||
print(f"Error fetching communes for department {dept_code}: {e}")
|
||||
else:
|
||||
print(f"Department {dept_code} not found in API")
|
||||
|
||||
print(f"Total communes found in sample: {len(all_communes)}")
|
||||
|
||||
# Print a few communes from each department
|
||||
for dept_code in sample_dept_codes:
|
||||
dept_communes = [c for c in all_communes if c.get('codeDepartement') == dept_code]
|
||||
if dept_communes:
|
||||
print(f"\nSample communes from department {dept_code}:")
|
||||
for commune in dept_communes[:3]: # Show first 3 communes
|
||||
print(f" - {commune.get('nom')} (code: {commune.get('code')})")
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Uncomment to run the test with sample departments
|
||||
# test_sample()
|
||||
|
||||
# Run the full script
|
||||
main()
|
10
labourage.sh
10
labourage.sh
|
@ -1,10 +1,5 @@
|
|||
#!/bin/bash
|
||||
|
||||
|
||||
curl -X GET "https://osm-commerces.cipherbliss.com/admin/followup/global"
|
||||
|
||||
|
||||
|
||||
# Les 10 codes postaux des villes les plus peuplées de France
|
||||
codes_insee=(
|
||||
"06088" # Nice
|
||||
|
@ -243,7 +238,4 @@ for code in "${codes_insee[@]}"; do
|
|||
sleep 5
|
||||
done
|
||||
|
||||
echo "Traitement terminé pour les codes insee"
|
||||
|
||||
curl -X GET "https://osm-commerces.cipherbliss.com/admin/followup/global"
|
||||
|
||||
echo "Traitement terminé"
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250622224949 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place CHANGE osm_user osm_user VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place CHANGE osm_user osm_user VARCHAR(255) DEFAULT NULL
|
||||
SQL);
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250622225249 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place CHANGE email email VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE note note VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE name name VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE note_content note_content VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE street street VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE housenumber housenumber VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE siret siret VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE stats CHANGE name name VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE stats CHANGE name name VARCHAR(255) DEFAULT NULL
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place CHANGE email email VARCHAR(255) DEFAULT NULL, CHANGE note note VARCHAR(255) DEFAULT NULL, CHANGE name name VARCHAR(255) DEFAULT NULL, CHANGE note_content note_content VARCHAR(255) DEFAULT NULL, CHANGE street street VARCHAR(255) DEFAULT NULL, CHANGE housenumber housenumber VARCHAR(255) DEFAULT NULL, CHANGE siret siret VARCHAR(255) DEFAULT NULL
|
||||
SQL);
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250623224321 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE stats ADD budget_annuel NUMERIC(15, 2) DEFAULT NULL
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE stats DROP budget_annuel
|
||||
SQL);
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250624103515 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place ADD email_content LONGTEXT CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, ADD place_count INT DEFAULT NULL
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place DROP email_content, DROP place_count
|
||||
SQL);
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250626204942 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TABLE action_log (id INT AUTO_INCREMENT NOT NULL, kind VARCHAR(255) NOT NULL, from_url VARCHAR(500) NOT NULL, who VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP TABLE action_log
|
||||
SQL);
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250626205820 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE action_log ADD from_url VARCHAR(500) DEFAULT NULL
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE action_log DROP from_url
|
||||
SQL);
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250626210012 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE action_log ADD who VARCHAR(255) DEFAULT NULL
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE action_log DROP who
|
||||
SQL);
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250629080450 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place CHANGE email email VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE note note VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE name name VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE note_content note_content VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE street street VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE housenumber housenumber VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE siret siret VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE osm_user osm_user VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE email_content email_content LONGTEXT CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place CHANGE email email VARCHAR(255) DEFAULT NULL, CHANGE note note VARCHAR(255) DEFAULT NULL, CHANGE name name VARCHAR(255) DEFAULT NULL, CHANGE note_content note_content VARCHAR(255) DEFAULT NULL, CHANGE street street VARCHAR(255) DEFAULT NULL, CHANGE housenumber housenumber VARCHAR(255) DEFAULT NULL, CHANGE siret siret VARCHAR(255) DEFAULT NULL, CHANGE osm_user osm_user VARCHAR(255) DEFAULT NULL, CHANGE email_content email_content LONGTEXT DEFAULT NULL
|
||||
SQL);
|
||||
}
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250629130159 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TABLE city_follow_up (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, measure DOUBLE PRECISION NOT NULL, date DATETIME NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place CHANGE email email VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE note note VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE name name VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE note_content note_content VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE street street VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE housenumber housenumber VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE siret siret VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE osm_user osm_user VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE email_content email_content LONGTEXT CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE stats ADD city_follow_ups_id INT DEFAULT NULL
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE stats ADD CONSTRAINT FK_574767AAA543722E FOREIGN KEY (city_follow_ups_id) REFERENCES city_follow_up (id)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE INDEX IDX_574767AAA543722E ON stats (city_follow_ups_id)
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP TABLE city_follow_up
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE stats DROP FOREIGN KEY FK_574767AAA543722E
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP INDEX IDX_574767AAA543722E ON stats
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE stats DROP city_follow_ups_id
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place CHANGE email email VARCHAR(255) DEFAULT NULL, CHANGE note note VARCHAR(255) DEFAULT NULL, CHANGE name name VARCHAR(255) DEFAULT NULL, CHANGE note_content note_content VARCHAR(255) DEFAULT NULL, CHANGE street street VARCHAR(255) DEFAULT NULL, CHANGE housenumber housenumber VARCHAR(255) DEFAULT NULL, CHANGE siret siret VARCHAR(255) DEFAULT NULL, CHANGE osm_user osm_user VARCHAR(255) DEFAULT NULL, CHANGE email_content email_content LONGTEXT DEFAULT NULL
|
||||
SQL);
|
||||
}
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250629130947 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE city_follow_up ADD stats_id INT DEFAULT NULL
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE city_follow_up ADD CONSTRAINT FK_DFE8468970AA3482 FOREIGN KEY (stats_id) REFERENCES stats (id)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE INDEX IDX_DFE8468970AA3482 ON city_follow_up (stats_id)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place CHANGE email email VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE note note VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE name name VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE note_content note_content VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE street street VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE housenumber housenumber VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE siret siret VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE osm_user osm_user VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE email_content email_content LONGTEXT CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE stats DROP FOREIGN KEY FK_574767AAA543722E
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP INDEX IDX_574767AAA543722E ON stats
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE stats DROP city_follow_ups_id
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE stats ADD city_follow_ups_id INT DEFAULT NULL
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE stats ADD CONSTRAINT FK_574767AAA543722E FOREIGN KEY (city_follow_ups_id) REFERENCES city_follow_up (id) ON UPDATE NO ACTION ON DELETE NO ACTION
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE INDEX IDX_574767AAA543722E ON stats (city_follow_ups_id)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE city_follow_up DROP FOREIGN KEY FK_DFE8468970AA3482
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP INDEX IDX_DFE8468970AA3482 ON city_follow_up
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE city_follow_up DROP stats_id
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place CHANGE email email VARCHAR(255) DEFAULT NULL, CHANGE note note VARCHAR(255) DEFAULT NULL, CHANGE name name VARCHAR(255) DEFAULT NULL, CHANGE note_content note_content VARCHAR(255) DEFAULT NULL, CHANGE street street VARCHAR(255) DEFAULT NULL, CHANGE housenumber housenumber VARCHAR(255) DEFAULT NULL, CHANGE siret siret VARCHAR(255) DEFAULT NULL, CHANGE osm_user osm_user VARCHAR(255) DEFAULT NULL, CHANGE email_content email_content LONGTEXT DEFAULT NULL
|
||||
SQL);
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250629142706 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place CHANGE email email VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE note note VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE name name VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE note_content note_content VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE street street VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE housenumber housenumber VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE siret siret VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE osm_user osm_user VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE email_content email_content LONGTEXT CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place CHANGE email email VARCHAR(255) DEFAULT NULL, CHANGE note note VARCHAR(255) DEFAULT NULL, CHANGE name name VARCHAR(255) DEFAULT NULL, CHANGE note_content note_content VARCHAR(255) DEFAULT NULL, CHANGE street street VARCHAR(255) DEFAULT NULL, CHANGE housenumber housenumber VARCHAR(255) DEFAULT NULL, CHANGE siret siret VARCHAR(255) DEFAULT NULL, CHANGE osm_user osm_user VARCHAR(255) DEFAULT NULL, CHANGE email_content email_content LONGTEXT DEFAULT NULL
|
||||
SQL);
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250705104136 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place CHANGE email email VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE note note VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE name name VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE note_content note_content VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE street street VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE housenumber housenumber VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE siret siret VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE osm_user osm_user VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE email_content email_content LONGTEXT CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE stats ADD lat NUMERIC(10, 7) DEFAULT NULL, ADD lon NUMERIC(10, 7) DEFAULT NULL
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE stats DROP lat, DROP lon
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place CHANGE email email VARCHAR(255) DEFAULT NULL, CHANGE note note VARCHAR(255) DEFAULT NULL, CHANGE name name VARCHAR(255) DEFAULT NULL, CHANGE note_content note_content VARCHAR(255) DEFAULT NULL, CHANGE street street VARCHAR(255) DEFAULT NULL, CHANGE housenumber housenumber VARCHAR(255) DEFAULT NULL, CHANGE siret siret VARCHAR(255) DEFAULT NULL, CHANGE osm_user osm_user VARCHAR(255) DEFAULT NULL, CHANGE email_content email_content LONGTEXT DEFAULT NULL
|
||||
SQL);
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250712121647 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place CHANGE email email VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE note note LONGTEXT CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE name name VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE note_content note_content VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE street street VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE housenumber housenumber VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE siret siret VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE osm_user osm_user VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE email_content email_content LONGTEXT CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place CHANGE email email VARCHAR(255) DEFAULT NULL, CHANGE note note VARCHAR(255) DEFAULT NULL, CHANGE name name VARCHAR(255) DEFAULT NULL, CHANGE note_content note_content VARCHAR(255) DEFAULT NULL, CHANGE street street VARCHAR(255) DEFAULT NULL, CHANGE housenumber housenumber VARCHAR(255) DEFAULT NULL, CHANGE siret siret VARCHAR(255) DEFAULT NULL, CHANGE osm_user osm_user VARCHAR(255) DEFAULT NULL, CHANGE email_content email_content LONGTEXT DEFAULT NULL
|
||||
SQL);
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250714154749 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place CHANGE email email VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE note note LONGTEXT CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE name name VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE note_content note_content VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE street street VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE housenumber housenumber VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE siret siret VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE osm_user osm_user VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE email_content email_content LONGTEXT CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE UNIQUE INDEX uniq_stats_zone ON stats (zone)
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP INDEX uniq_stats_zone ON stats
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place CHANGE email email VARCHAR(255) DEFAULT NULL, CHANGE note note LONGTEXT DEFAULT NULL, CHANGE name name VARCHAR(255) DEFAULT NULL, CHANGE note_content note_content VARCHAR(255) DEFAULT NULL, CHANGE street street VARCHAR(255) DEFAULT NULL, CHANGE housenumber housenumber VARCHAR(255) DEFAULT NULL, CHANGE siret siret VARCHAR(255) DEFAULT NULL, CHANGE osm_user osm_user VARCHAR(255) DEFAULT NULL, CHANGE email_content email_content LONGTEXT DEFAULT NULL
|
||||
SQL);
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250714165523 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place CHANGE email email VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE note note LONGTEXT CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE name name VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE note_content note_content VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE street street VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE housenumber housenumber VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE siret siret VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE osm_user osm_user VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE email_content email_content LONGTEXT CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place CHANGE email email VARCHAR(255) DEFAULT NULL, CHANGE note note LONGTEXT DEFAULT NULL, CHANGE name name VARCHAR(255) DEFAULT NULL, CHANGE note_content note_content VARCHAR(255) DEFAULT NULL, CHANGE street street VARCHAR(255) DEFAULT NULL, CHANGE housenumber housenumber VARCHAR(255) DEFAULT NULL, CHANGE siret siret VARCHAR(255) DEFAULT NULL, CHANGE osm_user osm_user VARCHAR(255) DEFAULT NULL, CHANGE email_content email_content LONGTEXT DEFAULT NULL
|
||||
SQL);
|
||||
}
|
||||
}
|
|
@ -1,101 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250716124008 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TABLE demande (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, query VARCHAR(255) NOT NULL, email VARCHAR(255) DEFAULT NULL, insee INT DEFAULT NULL, PRIMARY KEY(id))
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place ALTER email TYPE VARCHAR(255)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place ALTER note TYPE TEXT
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place ALTER name TYPE VARCHAR(255)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place ALTER note_content TYPE VARCHAR(255)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place ALTER street TYPE VARCHAR(255)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place ALTER housenumber TYPE VARCHAR(255)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place ALTER siret TYPE VARCHAR(255)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place ALTER osm_user TYPE VARCHAR(255)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place ALTER email_content TYPE TEXT
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE stats ALTER name TYPE VARCHAR(255)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE UNIQUE INDEX uniq_stats_zone ON stats (zone)
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP TABLE demande
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP INDEX uniq_stats_zone
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE stats ALTER name TYPE VARCHAR(255)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place ALTER email TYPE VARCHAR(255)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place ALTER note TYPE TEXT
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place ALTER name TYPE VARCHAR(255)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place ALTER note_content TYPE VARCHAR(255)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place ALTER street TYPE VARCHAR(255)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place ALTER housenumber TYPE VARCHAR(255)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place ALTER siret TYPE VARCHAR(255)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place ALTER osm_user TYPE VARCHAR(255)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE place ALTER email_content TYPE TEXT
|
||||
SQL);
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Add osmObjectType and osmId fields to demande table
|
||||
*/
|
||||
final class Version20250716160000 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add osmObjectType and osmId fields to demande table';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE demande ADD osm_object_type VARCHAR(10) DEFAULT NULL
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE demande ADD osm_id INT DEFAULT NULL
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE demande DROP osm_object_type
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE demande DROP osm_id
|
||||
SQL);
|
||||
}
|
||||
}
|
495
package-lock.json
generated
495
package-lock.json
generated
|
@ -1,27 +1,16 @@
|
|||
{
|
||||
"name": "osm-commerce-sf",
|
||||
"name": "osm-commerces",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"charjs": "^0.0.1-security",
|
||||
"chartjs-adapter-date-fns": "^3.0.0",
|
||||
"jquery": "^3.7.1",
|
||||
"table-sort": "^1.0.16"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.17.0",
|
||||
"@babel/preset-env": "^7.16.0",
|
||||
"@symfony/webpack-encore": "^5.0.0",
|
||||
"chart.js": "^4.5.0",
|
||||
"chartjs-plugin-datalabels": "^2.2.0",
|
||||
"core-js": "^3.38.0",
|
||||
"maplibre-gl": "^5.6.0",
|
||||
"regenerator-runtime": "^0.13.9",
|
||||
"table-sort-js": "^1.22.2",
|
||||
"tablesort": "^5.6.0",
|
||||
"webpack": "^5.74.0",
|
||||
"webpack-cli": "^5.1.0"
|
||||
}
|
||||
|
@ -1705,97 +1694,6 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@kurkle/color": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
|
||||
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@mapbox/geojson-rewind": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz",
|
||||
"integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"get-stream": "^6.0.1",
|
||||
"minimist": "^1.2.6"
|
||||
},
|
||||
"bin": {
|
||||
"geojson-rewind": "geojson-rewind"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/jsonlint-lines-primitives": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz",
|
||||
"integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/point-geometry": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz",
|
||||
"integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@mapbox/tiny-sdf": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.6.tgz",
|
||||
"integrity": "sha512-qMqa27TLw+ZQz5Jk+RcwZGH7BQf5G/TrutJhspsca/3SHwmgKQ1iq+d3Jxz5oysPVYTGP6aXxCo5Lk9Er6YBAA==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/@mapbox/unitbezier": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz",
|
||||
"integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/@mapbox/vector-tile": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz",
|
||||
"integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@mapbox/point-geometry": "~0.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/whoots-js": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz",
|
||||
"integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@maplibre/maplibre-gl-style-spec": {
|
||||
"version": "23.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-23.3.0.tgz",
|
||||
"integrity": "sha512-IGJtuBbaGzOUgODdBRg66p8stnwj9iDXkgbYKoYcNiiQmaez5WVRfXm4b03MCDwmZyX93csbfHFWEJJYHnn5oA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@mapbox/jsonlint-lines-primitives": "~2.0.2",
|
||||
"@mapbox/unitbezier": "^0.0.1",
|
||||
"json-stringify-pretty-compact": "^4.0.0",
|
||||
"minimist": "^1.2.8",
|
||||
"quickselect": "^3.0.0",
|
||||
"rw": "^1.3.3",
|
||||
"tinyqueue": "^3.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"gl-style-format": "dist/gl-style-format.mjs",
|
||||
"gl-style-migrate": "dist/gl-style-migrate.mjs",
|
||||
"gl-style-validate": "dist/gl-style-validate.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@nuxt/friendly-errors-webpack-plugin": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@nuxt/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-2.6.0.tgz",
|
||||
|
@ -2024,23 +1922,6 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/geojson": {
|
||||
"version": "7946.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
|
||||
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/geojson-vt": {
|
||||
"version": "3.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/geojson-vt/-/geojson-vt-3.2.5.tgz",
|
||||
"integrity": "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/istanbul-lib-coverage": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
|
||||
|
@ -2075,25 +1956,6 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/mapbox__point-geometry": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz",
|
||||
"integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/mapbox__vector-tile": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.4.tgz",
|
||||
"integrity": "sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/geojson": "*",
|
||||
"@types/mapbox__point-geometry": "*",
|
||||
"@types/pbf": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.15.21",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz",
|
||||
|
@ -2104,23 +1966,6 @@
|
|||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/pbf": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz",
|
||||
"integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/supercluster": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz",
|
||||
"integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/yargs": {
|
||||
"version": "17.0.33",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
|
||||
|
@ -2624,43 +2469,6 @@
|
|||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/charjs": {
|
||||
"version": "0.0.1-security",
|
||||
"resolved": "https://registry.npmjs.org/charjs/-/charjs-0.0.1-security.tgz",
|
||||
"integrity": "sha512-O3j2a0oEM2LXTyFdAq5Y2ntGaSekgeAW7FtjOGU78PNe5GReNGQvoEpyGmd8Xv3/8VS+2HbgWnxntMvVGrY4Cg=="
|
||||
},
|
||||
"node_modules/chart.js": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz",
|
||||
"integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@kurkle/color": "^0.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"pnpm": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/chartjs-adapter-date-fns": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-3.0.0.tgz",
|
||||
"integrity": "sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"chart.js": ">=2.8.0",
|
||||
"date-fns": ">=2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/chartjs-plugin-datalabels": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/chartjs-plugin-datalabels/-/chartjs-plugin-datalabels-2.2.0.tgz",
|
||||
"integrity": "sha512-14ZU30lH7n89oq+A4bWaJPnAG8a7ZTk7dKf48YAzMvJjQtjrgg5Dpk9f+LbjCF6bpx3RAGTeL13IXpKQYyRvlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"chart.js": ">=3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/chrome-trace-event": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
|
||||
|
@ -3087,17 +2895,6 @@
|
|||
"dev": true,
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
|
@ -3185,13 +2982,6 @@
|
|||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/earcut": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.1.tgz",
|
||||
"integrity": "sha512-0l1/0gOjESMeQyYaK5IDiPNvFeu93Z/cO0TjZh9eZ1vyCtZnA7KMZ8rQggpsJHIbGSdrqYq9OhuveadOVHCshw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.158",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.158.tgz",
|
||||
|
@ -3455,33 +3245,6 @@
|
|||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/geojson-vt": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-4.0.2.tgz",
|
||||
"integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/get-stream": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
|
||||
"integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/gl-matrix": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz",
|
||||
"integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/glob-to-regexp": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
|
||||
|
@ -3489,47 +3252,6 @@
|
|||
"dev": true,
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/global-prefix": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-4.0.0.tgz",
|
||||
"integrity": "sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ini": "^4.1.3",
|
||||
"kind-of": "^6.0.3",
|
||||
"which": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/global-prefix/node_modules/isexe": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz",
|
||||
"integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/global-prefix/node_modules/which": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz",
|
||||
"integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^3.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/which.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.13.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "11.12.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
||||
|
@ -3603,27 +3325,6 @@
|
|||
"postcss": "^8.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/import-local": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz",
|
||||
|
@ -3723,16 +3424,6 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ini": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz",
|
||||
"integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/interpret": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz",
|
||||
|
@ -3935,12 +3626,6 @@
|
|||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/jquery": {
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
|
||||
"integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
|
@ -3975,13 +3660,6 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-stringify-pretty-compact": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz",
|
||||
"integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
|
@ -3995,13 +3673,6 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/kdbush": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz",
|
||||
"integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/kind-of": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||
|
@ -4104,48 +3775,6 @@
|
|||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/maplibre-gl": {
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.6.0.tgz",
|
||||
"integrity": "sha512-7TuHMozUC4rlIp08bSsxCixFn18P24otrlZU/7UGCO5RufFTJadFzauTrvBHr9FB67MbJ6nvFXEftGd0bUl4Iw==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@mapbox/geojson-rewind": "^0.5.2",
|
||||
"@mapbox/jsonlint-lines-primitives": "^2.0.2",
|
||||
"@mapbox/point-geometry": "^0.1.0",
|
||||
"@mapbox/tiny-sdf": "^2.0.6",
|
||||
"@mapbox/unitbezier": "^0.0.1",
|
||||
"@mapbox/vector-tile": "^1.3.1",
|
||||
"@mapbox/whoots-js": "^3.1.0",
|
||||
"@maplibre/maplibre-gl-style-spec": "^23.3.0",
|
||||
"@types/geojson": "^7946.0.16",
|
||||
"@types/geojson-vt": "3.2.5",
|
||||
"@types/mapbox__point-geometry": "^0.1.4",
|
||||
"@types/mapbox__vector-tile": "^1.3.4",
|
||||
"@types/pbf": "^3.0.5",
|
||||
"@types/supercluster": "^7.1.3",
|
||||
"earcut": "^3.0.1",
|
||||
"geojson-vt": "^4.0.2",
|
||||
"gl-matrix": "^3.4.3",
|
||||
"global-prefix": "^4.0.0",
|
||||
"kdbush": "^4.0.2",
|
||||
"murmurhash-js": "^1.0.0",
|
||||
"pbf": "^3.3.0",
|
||||
"potpack": "^2.0.0",
|
||||
"quickselect": "^3.0.0",
|
||||
"supercluster": "^8.0.1",
|
||||
"tinyqueue": "^3.0.0",
|
||||
"vt-pbf": "^3.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.14.0",
|
||||
"npm": ">=8.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/mdn-data": {
|
||||
"version": "2.0.30",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
|
||||
|
@ -4204,16 +3833,6 @@
|
|||
"webpack": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
|
@ -4221,13 +3840,6 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/murmurhash-js": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz",
|
||||
"integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
|
@ -4343,20 +3955,6 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pbf": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz",
|
||||
"integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"ieee754": "^1.1.12",
|
||||
"resolve-protobuf-schema": "^2.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"pbf": "bin/pbf"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
|
@ -4944,13 +4542,6 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/potpack": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/potpack/-/potpack-2.0.0.tgz",
|
||||
"integrity": "sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/pretty-error": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz",
|
||||
|
@ -4962,20 +4553,6 @@
|
|||
"renderkid": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/protocol-buffers-schema": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz",
|
||||
"integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/quickselect": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz",
|
||||
"integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/randombytes": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||
|
@ -5152,16 +4729,6 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-protobuf-schema": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz",
|
||||
"integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"protocol-buffers-schema": "^3.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-url-loader": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz",
|
||||
|
@ -5186,13 +4753,6 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/rw": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
|
||||
"integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
|
@ -5390,16 +4950,6 @@
|
|||
"postcss": "^8.4.32"
|
||||
}
|
||||
},
|
||||
"node_modules/supercluster": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz",
|
||||
"integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"kdbush": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
|
@ -5528,30 +5078,6 @@
|
|||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/table-sort": {
|
||||
"version": "1.0.16",
|
||||
"resolved": "https://registry.npmjs.org/table-sort/-/table-sort-1.0.16.tgz",
|
||||
"integrity": "sha512-w7TDMfszdFY36aWQKRiAg0qQjOmvIy1IQKplmgpOCimOZ69BP4y5Ne4+jBQeYn990Rn40/wCALR0eAcLqxECWA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/table-sort-js": {
|
||||
"version": "1.22.2",
|
||||
"resolved": "https://registry.npmjs.org/table-sort-js/-/table-sort-js-1.22.2.tgz",
|
||||
"integrity": "sha512-KUpmoYWH1TCnyiylE0HMCtMeAisl0KYBFjZfBL3CPHOlnhA8jy+RFfZbH6DwCpXAvmK73vsDAX54hg9J4DhuRQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tablesort": {
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tablesort/-/tablesort-5.6.0.tgz",
|
||||
"integrity": "sha512-cZZXK3G089PbpxH8N7vN7Z21SEKqXAaCiSVOmZdR/v7z8TFCsF/OFr0rzjhQuFlQQHy9uQtW9P2oQFJzJFGVrg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16",
|
||||
"npm": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz",
|
||||
|
@ -5664,13 +5190,6 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyqueue": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz",
|
||||
"integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/tmp": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
|
||||
|
@ -5777,18 +5296,6 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vt-pbf": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz",
|
||||
"integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mapbox/point-geometry": "0.1.0",
|
||||
"@mapbox/vector-tile": "^1.3.1",
|
||||
"pbf": "^3.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/watchpack": {
|
||||
"version": "2.4.4",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
|
||||
|
|
11
package.json
11
package.json
|
@ -3,13 +3,8 @@
|
|||
"@babel/core": "^7.17.0",
|
||||
"@babel/preset-env": "^7.16.0",
|
||||
"@symfony/webpack-encore": "^5.0.0",
|
||||
"chart.js": "^4.5.0",
|
||||
"chartjs-plugin-datalabels": "^2.2.0",
|
||||
"core-js": "^3.38.0",
|
||||
"maplibre-gl": "^5.6.0",
|
||||
"regenerator-runtime": "^0.13.9",
|
||||
"table-sort-js": "^1.22.2",
|
||||
"tablesort": "^5.6.0",
|
||||
"webpack": "^5.74.0",
|
||||
"webpack-cli": "^5.1.0"
|
||||
},
|
||||
|
@ -20,11 +15,5 @@
|
|||
"dev": "encore dev",
|
||||
"watch": "encore dev --watch",
|
||||
"build": "encore production --progress"
|
||||
},
|
||||
"dependencies": {
|
||||
"charjs": "^0.0.1-security",
|
||||
"chartjs-adapter-date-fns": "^3.0.0",
|
||||
"jquery": "^3.7.1",
|
||||
"table-sort": "^1.0.16"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,92 +0,0 @@
|
|||
/* Styles pour la sidebar */
|
||||
.city-sidebar {
|
||||
background-color: #f8f9fa;
|
||||
border-right: 1px solid #dee2e6;
|
||||
padding: 1rem;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
/* Desktop styles */
|
||||
@media (min-width: 768px) {
|
||||
.city-sidebar {
|
||||
width: 25%;
|
||||
max-width: 280px;
|
||||
}
|
||||
.main-content {
|
||||
margin-left: 25%;
|
||||
width: 75%;
|
||||
padding-top: 20px;
|
||||
}
|
||||
.main-header {
|
||||
margin-left: 25%;
|
||||
width: 75%;
|
||||
z-index: 1001;
|
||||
}
|
||||
.main-footer {
|
||||
margin-left: 25%;
|
||||
width: 75%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile styles */
|
||||
@media (max-width: 767px) {
|
||||
.city-sidebar {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-height: none;
|
||||
}
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.main-header {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.main-footer {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.city-sidebar .nav-link {
|
||||
color: #495057;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.5rem 1rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.city-sidebar .nav-link:hover {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
.city-sidebar .nav-link.active {
|
||||
background-color: #0d6efd;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.city-sidebar .nav-link i {
|
||||
width: 1.5rem;
|
||||
text-align: center;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.city-sidebar .sidebar-heading {
|
||||
font-size: 0.85rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1rem;
|
||||
color: #6c757d;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
.section-anchor {
|
||||
scroll-margin-top: 2rem;
|
||||
}
|
|
@ -60,48 +60,6 @@
|
|||
padding: 0;
|
||||
}
|
||||
|
||||
/* Home page specific styles */
|
||||
.hero-image {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 2rem;
|
||||
color: #fff;
|
||||
border-radius: 0.75rem;
|
||||
background-color: #0d6efd;
|
||||
}
|
||||
|
||||
.step-circle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: 50%;
|
||||
background-color: #0d6efd;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 1.25rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
/* Media queries */
|
||||
@media (max-width: 768px) {
|
||||
.main-header h1 {
|
||||
|
@ -111,13 +69,4 @@
|
|||
.main-footer {
|
||||
padding: 1.5rem 0;
|
||||
}
|
||||
|
||||
.display-4 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.hero-image {
|
||||
max-height: 200px;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,11 +4,6 @@ use App\Kernel;
|
|||
|
||||
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
|
||||
|
||||
// Optimisations pour éviter les timeouts
|
||||
ini_set('max_execution_time', 300);
|
||||
ini_set('memory_limit', '1024M');
|
||||
set_time_limit(300);
|
||||
|
||||
return function (array $context) {
|
||||
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
|
||||
};
|
||||
|
|
File diff suppressed because one or more lines are too long
35
public/js/sort-table/sort-table.css
Normal file
35
public/js/sort-table/sort-table.css
Normal file
|
@ -0,0 +1,35 @@
|
|||
/* These style add the up/down arrows for the current sorted column
|
||||
* To support more columns, just augment these styles in like form.
|
||||
*/
|
||||
table.js-sort-asc.js-sort-0 thead tr:last-child > :nth-child(1):after,
|
||||
table.js-sort-asc.js-sort-1 thead tr:last-child > :nth-child(2):after,
|
||||
table.js-sort-asc.js-sort-2 thead tr:last-child > :nth-child(3):after,
|
||||
table.js-sort-asc.js-sort-3 thead tr:last-child > :nth-child(4):after,
|
||||
table.js-sort-asc.js-sort-4 thead tr:last-child > :nth-child(5):after,
|
||||
table.js-sort-asc.js-sort-5 thead tr:last-child > :nth-child(6):after,
|
||||
table.js-sort-asc.js-sort-6 thead tr:last-child > :nth-child(7):after,
|
||||
table.js-sort-asc.js-sort-7 thead tr:last-child > :nth-child(8):after,
|
||||
table.js-sort-asc.js-sort-8 thead tr:last-child > :nth-child(9):after,
|
||||
table.js-sort-asc.js-sort-9 thead tr:last-child > :nth-child(10):after
|
||||
{
|
||||
content: "\25b2";
|
||||
font-size: 0.7em;
|
||||
padding-left: 3px;
|
||||
line-height: 0.7em;
|
||||
}
|
||||
table.js-sort-desc.js-sort-0 thead tr:last-child > :nth-child(1):after,
|
||||
table.js-sort-desc.js-sort-1 thead tr:last-child > :nth-child(2):after,
|
||||
table.js-sort-desc.js-sort-2 thead tr:last-child > :nth-child(3):after,
|
||||
table.js-sort-desc.js-sort-3 thead tr:last-child > :nth-child(4):after,
|
||||
table.js-sort-desc.js-sort-4 thead tr:last-child > :nth-child(5):after,
|
||||
table.js-sort-desc.js-sort-5 thead tr:last-child > :nth-child(6):after,
|
||||
table.js-sort-desc.js-sort-6 thead tr:last-child > :nth-child(7):after,
|
||||
table.js-sort-desc.js-sort-7 thead tr:last-child > :nth-child(8):after,
|
||||
table.js-sort-desc.js-sort-8 thead tr:last-child > :nth-child(9):after,
|
||||
table.js-sort-desc.js-sort-9 thead tr:last-child > :nth-child(10):after
|
||||
{
|
||||
content: "\25bc";
|
||||
font-size: 0.7em;
|
||||
padding-left: 3px;
|
||||
line-height: 0.7em;
|
||||
}
|
251
public/js/sort-table/sort-table.js
Normal file
251
public/js/sort-table/sort-table.js
Normal file
|
@ -0,0 +1,251 @@
|
|||
/**
|
||||
* sort-table.js
|
||||
* A pure JavaScript (no dependencies) solution to make HTML
|
||||
* Tables sortable
|
||||
*
|
||||
* Copyright (c) 2013 Tyler Uebele
|
||||
* Released under the MIT license. See included LICENSE.txt
|
||||
* or http://opensource.org/licenses/MIT
|
||||
*
|
||||
* latest version available at https://github.com/tyleruebele/sort-table
|
||||
*/
|
||||
|
||||
/**
|
||||
* Sort the rows in a HTML Table
|
||||
*
|
||||
* @param Table The Table DOM object
|
||||
* @param col The zero-based column number by which to sort
|
||||
* @param dir Optional. The sort direction; pass 1 for asc; -1 for desc
|
||||
* @returns void
|
||||
*/
|
||||
function sortTable(Table, col, dir) {
|
||||
var sortClass, i;
|
||||
|
||||
if (!Table) {
|
||||
return;
|
||||
}
|
||||
// get previous sort column
|
||||
sortTable.sortCol = -1;
|
||||
sortClass = Table.className.match(/js-sort-\d+/);
|
||||
if (null != sortClass) {
|
||||
sortTable.sortCol = sortClass[0].replace(/js-sort-/, '');
|
||||
Table.className = Table.className.replace(new RegExp(' ?' + sortClass[0] + '\\b'), '');
|
||||
}
|
||||
// If sort column was not passed, use previous
|
||||
if ('undefined' === typeof col) {
|
||||
col = sortTable.sortCol;
|
||||
}
|
||||
|
||||
if ('undefined' !== typeof dir) {
|
||||
// Accept -1 or 'desc' for descending. All else is ascending
|
||||
sortTable.sortDir = dir == -1 || dir == 'desc' ? -1 : 1;
|
||||
} else {
|
||||
// sort direction was not passed, use opposite of previous
|
||||
sortClass = Table.className.match(/js-sort-(a|de)sc/);
|
||||
if (null != sortClass && sortTable.sortCol == col) {
|
||||
sortTable.sortDir = 'js-sort-asc' == sortClass[0] ? -1 : 1;
|
||||
} else {
|
||||
sortTable.sortDir = 1;
|
||||
}
|
||||
}
|
||||
Table.className = Table.className.replace(/ ?js-sort-(a|de)sc/g, '');
|
||||
|
||||
// update sort column
|
||||
Table.className += ' js-sort-' + col;
|
||||
sortTable.sortCol = col;
|
||||
|
||||
// update sort direction
|
||||
Table.className += ' js-sort-' + (sortTable.sortDir == -1 ? 'desc' : 'asc');
|
||||
|
||||
// get sort type
|
||||
if (col < Table.tHead.rows[Table.tHead.rows.length - 1].cells.length) {
|
||||
sortClass = Table.tHead.rows[Table.tHead.rows.length - 1].cells[col].className.match(/js-sort-[-\w]+/);
|
||||
}
|
||||
// Improved support for colspan'd headers
|
||||
for (i = 0; i < Table.tHead.rows[Table.tHead.rows.length - 1].cells.length; i++) {
|
||||
if (col == Table.tHead.rows[Table.tHead.rows.length - 1].cells[i].getAttribute('data-js-sort-colNum')) {
|
||||
sortClass = Table.tHead.rows[Table.tHead.rows.length - 1].cells[i].className.match(/js-sort-[-\w]+/);
|
||||
}
|
||||
}
|
||||
if (null != sortClass) {
|
||||
sortTable.sortFunc = sortClass[0].replace(/js-sort-/, '');
|
||||
} else {
|
||||
sortTable.sortFunc = 'string';
|
||||
}
|
||||
|
||||
// sort!
|
||||
var rows = [],
|
||||
TBody = Table.tBodies[0];
|
||||
|
||||
for (i = 0; i < TBody.rows.length; i++) {
|
||||
rows[i] = TBody.rows[i];
|
||||
}
|
||||
rows.sort(sortTable.compareRow);
|
||||
|
||||
while (TBody.firstChild) {
|
||||
TBody.removeChild(TBody.firstChild);
|
||||
}
|
||||
for (i = 0; i < rows.length; i++) {
|
||||
TBody.appendChild(rows[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two table rows based on current settings
|
||||
*
|
||||
* @param RowA A TR DOM object
|
||||
* @param RowB A TR DOM object
|
||||
* @returns {number} 1 if RowA is greater, -1 if RowB, 0 if equal
|
||||
*/
|
||||
sortTable.compareRow = function (RowA, RowB) {
|
||||
var valA, valB;
|
||||
if ('function' != typeof sortTable[sortTable.sortFunc]) {
|
||||
sortTable.sortFunc = 'string';
|
||||
}
|
||||
valA = sortTable[sortTable.sortFunc](RowA.cells[sortTable.sortCol]);
|
||||
valB = sortTable[sortTable.sortFunc](RowB.cells[sortTable.sortCol]);
|
||||
|
||||
return valA == valB ? 0 : sortTable.sortDir * (valA > valB ? 1 : -1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Strip all HTML, no exceptions
|
||||
* @param html
|
||||
* @returns {string}
|
||||
*/
|
||||
sortTable.stripTags = function (html) {
|
||||
return html.replace(/<\/?[a-z][a-z0-9]*\b[^>]*>/gi, '');
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function that converts a table cell (TD) to a comparable value
|
||||
* Converts innerHTML to a JS Date object
|
||||
*
|
||||
* @param Cell A TD DOM object
|
||||
* @returns {Date}
|
||||
*/
|
||||
sortTable.date = function (Cell) {
|
||||
return new Date(sortTable.stripTags(Cell.innerHTML));
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function that converts a table cell (TD) to a comparable value
|
||||
* Converts innerHTML to a JS Number object
|
||||
*
|
||||
* @param Cell A TD DOM object
|
||||
* @returns {Number}
|
||||
*/
|
||||
sortTable.number = function (Cell) {
|
||||
return Number(sortTable.stripTags(Cell.innerHTML).replace(/[^-\d.]/g, ''));
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function that converts a table cell (TD) to a comparable value
|
||||
* Converts innerHTML to a lower case string for insensitive compare
|
||||
*
|
||||
* @param Cell A TD DOM object
|
||||
* @returns {String}
|
||||
*/
|
||||
sortTable.string = function (Cell) {
|
||||
return sortTable.stripTags(Cell.innerHTML).toLowerCase();
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function that converts a table cell (TD) to a comparable value
|
||||
* Captures the last space-delimited token from innerHTML
|
||||
*
|
||||
* @param Cell A TD DOM object
|
||||
* @returns {String}
|
||||
*/
|
||||
sortTable.last = function (Cell) {
|
||||
return sortTable.stripTags(Cell.innerHTML).split(' ').pop().toLowerCase();
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function that converts a table cell (TD) to a comparable value
|
||||
* Captures the value of the first childNode
|
||||
*
|
||||
* @param Cell A TD DOM object
|
||||
* @returns {String}
|
||||
*/
|
||||
sortTable.input = function (Cell) {
|
||||
for (var i = 0; i < Cell.children.length; i++) {
|
||||
if ('object' == typeof Cell.children[i]
|
||||
&& 'undefined' != typeof Cell.children[i].value
|
||||
) {
|
||||
return Cell.children[i].value.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
return sortTable.string(Cell);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the click handler appropriate to the specified Table and column
|
||||
*
|
||||
* @param Table Table to sort
|
||||
* @param col Column to sort by
|
||||
* @returns {Function} Click Handler
|
||||
*/
|
||||
sortTable.getClickHandler = function (Table, col) {
|
||||
return function () {
|
||||
sortTable(Table, col);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Attach sortTable() calls to table header cells' onclick events
|
||||
* If the table(s) do not have a THead node, one will be created around the
|
||||
* first row
|
||||
*/
|
||||
sortTable.init = function () {
|
||||
var THead, Tables, Handler;
|
||||
if (document.querySelectorAll) {
|
||||
Tables = document.querySelectorAll('table.js-sort-table');
|
||||
} else {
|
||||
Tables = document.getElementsByTagName('table');
|
||||
}
|
||||
|
||||
for (var i = 0; i < Tables.length; i++) {
|
||||
// Because IE<8 doesn't support querySelectorAll, skip unclassed tables
|
||||
if (!document.querySelectorAll && null === Tables[i].className.match(/\bjs-sort-table\b/)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Prevent repeat processing
|
||||
if (Tables[i].attributes['data-js-sort-table']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ensure table has a tHead element
|
||||
if (!Tables[i].tHead) {
|
||||
THead = document.createElement('thead');
|
||||
THead.appendChild(Tables[i].rows[0]);
|
||||
Tables[i].insertBefore(THead, Tables[i].children[0]);
|
||||
} else {
|
||||
THead = Tables[i].tHead;
|
||||
}
|
||||
|
||||
// Attach click events to table header
|
||||
for (var rowNum = 0; rowNum < THead.rows.length; rowNum++) {
|
||||
for (var cellNum = 0, colNum = 0; cellNum < THead.rows[rowNum].cells.length; cellNum++) {
|
||||
// Define which column the header should invoke sorting for
|
||||
THead.rows[rowNum].cells[cellNum].setAttribute('data-js-sort-colNum', colNum);
|
||||
Handler = sortTable.getClickHandler(Tables[i], colNum);
|
||||
window.addEventListener
|
||||
? THead.rows[rowNum].cells[cellNum].addEventListener('click', Handler)
|
||||
: window.attachEvent && THead.rows[rowNum].cells[cellNum].attachEvent('onclick', Handler);
|
||||
colNum += THead.rows[rowNum].cells[cellNum].colSpan;
|
||||
}
|
||||
}
|
||||
|
||||
// Mark table as processed
|
||||
Tables[i].setAttribute('data-js-sort-table', 'true')
|
||||
}
|
||||
};
|
||||
|
||||
// Run sortTable.init() when the page loads
|
||||
window.addEventListener
|
||||
? window.addEventListener('load', sortTable.init, false)
|
||||
: window.attachEvent && window.attachEvent('onload', sortTable.init)
|
||||
;
|
|
@ -1,104 +0,0 @@
|
|||
{
|
||||
"last_updated": "2025-08-22T17:10:41.058478",
|
||||
"local_groups": [
|
||||
{
|
||||
"name": "Liste des groupes locaux se réunissant régulièrement",
|
||||
"url": "https://framacalc.org/osm-groupes-locaux",
|
||||
"description": "",
|
||||
"type": "local_group"
|
||||
},
|
||||
{
|
||||
"name": "Carte des groupes locaux se réunissant régulièrement",
|
||||
"url": "https://umap.openstreetmap.fr/fr/map/groupes-locaux-openstreetmap_152488",
|
||||
"description": "",
|
||||
"type": "local_group"
|
||||
}
|
||||
],
|
||||
"working_groups": [
|
||||
{
|
||||
"name": "Que venir faire au sein de l'association ?",
|
||||
"url": "https://forum.openstreetmap.fr/t/que-venir-faire-au-sein-de-lassociation/15454",
|
||||
"description": "",
|
||||
"category": "Général",
|
||||
"type": "working_group"
|
||||
},
|
||||
{
|
||||
"name": "GT Inclusivité",
|
||||
"url": "https://wiki.openstreetmap.org/wiki/France/OSM-FR/Groupes_de_travail#GT_Inclusivité",
|
||||
"description": "",
|
||||
"category": "Général",
|
||||
"type": "working_group"
|
||||
},
|
||||
{
|
||||
"name": "GT Technique",
|
||||
"url": "https://wiki.openstreetmap.org/wiki/France/OSM-FR/Groupes_de_travail#GT_Technique",
|
||||
"description": "",
|
||||
"category": "Général",
|
||||
"type": "working_group"
|
||||
},
|
||||
{
|
||||
"name": "GT Communication externe",
|
||||
"url": "https://wiki.openstreetmap.org/wiki/France/OSM-FR/Groupes_de_travail#GT_Communication",
|
||||
"description": "",
|
||||
"category": "Général",
|
||||
"type": "working_group"
|
||||
},
|
||||
{
|
||||
"name": "GT Animation de la communauté",
|
||||
"url": "https://wiki.openstreetmap.org/wiki/France/OSM-FR/Groupes_de_travail#GT_Animation_de_la_communauté",
|
||||
"description": "",
|
||||
"category": "Général",
|
||||
"type": "working_group"
|
||||
},
|
||||
{
|
||||
"name": "GT Communautés locales",
|
||||
"url": "https://wiki.openstreetmap.org/wiki/France/OSM-FR/Groupes_de_travail#GT_Communautés_locales",
|
||||
"description": "",
|
||||
"category": "Général",
|
||||
"type": "working_group"
|
||||
},
|
||||
{
|
||||
"name": "GT International",
|
||||
"url": "https://wiki.openstreetmap.org/wiki/France/OSM-FR/Groupes_de_travail#GT_International",
|
||||
"description": "",
|
||||
"category": "Général",
|
||||
"type": "working_group"
|
||||
},
|
||||
{
|
||||
"name": "GT Gestion et comptabilité",
|
||||
"url": "https://wiki.openstreetmap.org/wiki/France/OSM-FR/Groupes_de_travail#GT_Gestion_et_comptabilité",
|
||||
"description": "",
|
||||
"category": "Général",
|
||||
"type": "working_group"
|
||||
},
|
||||
{
|
||||
"name": "GT Soutiens",
|
||||
"url": "https://wiki.openstreetmap.org/wiki/France/OSM-FR/Groupes_de_travail#GT_Soutiens",
|
||||
"description": "",
|
||||
"category": "Général",
|
||||
"type": "working_group"
|
||||
},
|
||||
{
|
||||
"name": "GT Conférence SotM-FR",
|
||||
"url": "https://wiki.openstreetmap.org/wiki/France/OSM-FR/Groupes_de_travail#GT_Conférence_SotM-FR",
|
||||
"description": "",
|
||||
"category": "Général",
|
||||
"type": "working_group"
|
||||
},
|
||||
{
|
||||
"name": "Groupes spéciaux",
|
||||
"url": "https://wiki.openstreetmap.org/wiki/France/OSM-FR/Groupes_de_travail#Groupes_spéciaux",
|
||||
"description": "",
|
||||
"category": "Général",
|
||||
"type": "working_group"
|
||||
},
|
||||
{
|
||||
"name": "Groupes projets et thématiques",
|
||||
"url": "https://wiki.openstreetmap.org/wiki/France/OSM-FR/Groupes_de_travail#Groupes_projets_et_thématiques",
|
||||
"description": "",
|
||||
"category": "Général",
|
||||
"type": "working_group"
|
||||
}
|
||||
],
|
||||
"umap_url": "https://umap.openstreetmap.fr/fr/map/groupes-locaux-openstreetmap_152488"
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"last_updated": "2025-08-22T23:19:05.767890",
|
||||
"recent_changes": []
|
||||
}
|
|
@ -1,112 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\Stats;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:clean-duplicate-stats',
|
||||
description: 'Supprime les doublons de Stats ayant le même code INSEE, en gardant le plus ancien. Option dry-run pour simuler.'
|
||||
)]
|
||||
class CleanDuplicateStatsCommand extends Command
|
||||
{
|
||||
public function __construct(private EntityManagerInterface $entityManager)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Simule la suppression sans rien effacer')
|
||||
->setHelp('Supprime les doublons de Stats (même code INSEE), en gardant le plus ancien. Utilisez --dry-run pour simuler.');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$dryRun = $input->getOption('dry-run');
|
||||
|
||||
$repo = $this->entityManager->getRepository(Stats::class);
|
||||
$allStats = $repo->findAll();
|
||||
$io->title('Recherche des doublons Stats par code INSEE');
|
||||
|
||||
// Regrouper par code INSEE
|
||||
$statsByZone = [];
|
||||
foreach ($allStats as $stat) {
|
||||
$zone = $stat->getZone();
|
||||
if (!$zone) continue;
|
||||
$statsByZone[$zone][] = $stat;
|
||||
}
|
||||
|
||||
$toDelete = [];
|
||||
$toKeep = [];
|
||||
foreach ($statsByZone as $zone => $statsList) {
|
||||
if (count($statsList) > 1) {
|
||||
// Trier par date_created (le plus ancien d'abord), puis par id si date absente
|
||||
usort($statsList, function($a, $b) {
|
||||
$da = $a->getDateCreated();
|
||||
$db = $b->getDateCreated();
|
||||
if ($da && $db) {
|
||||
return $da <=> $db;
|
||||
} elseif ($da) {
|
||||
return -1;
|
||||
} elseif ($db) {
|
||||
return 1;
|
||||
} else {
|
||||
return $a->getId() <=> $b->getId();
|
||||
}
|
||||
});
|
||||
// Garder le premier, supprimer les autres
|
||||
$toKeep[$zone] = $statsList[0];
|
||||
$toDelete[$zone] = array_slice($statsList, 1);
|
||||
}
|
||||
}
|
||||
|
||||
$totalToDelete = array_sum(array_map('count', $toDelete));
|
||||
if ($totalToDelete === 0) {
|
||||
$io->success('Aucun doublon trouvé.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$io->section('Résumé des actions par code INSEE :');
|
||||
foreach ($toDelete as $zone => $statsList) {
|
||||
$io->writeln("<info>Zone INSEE : $zone</info>");
|
||||
$statKept = $toKeep[$zone];
|
||||
$io->writeln(sprintf(" <fg=green>Gardé : [ID %d] %s | Créé: %s</>",
|
||||
$statKept->getId(),
|
||||
$statKept->getName(),
|
||||
$statKept->getDateCreated() ? $statKept->getDateCreated()->format('Y-m-d H:i:s') : 'N/A'
|
||||
));
|
||||
foreach ($statsList as $stat) {
|
||||
$io->writeln(sprintf(" <fg=red>Supprimé: [ID %d] %s | Créé: %s</>",
|
||||
$stat->getId(),
|
||||
$stat->getName(),
|
||||
$stat->getDateCreated() ? $stat->getDateCreated()->format('Y-m-d H:i:s') : 'N/A'
|
||||
));
|
||||
}
|
||||
}
|
||||
$io->warning($totalToDelete . ' objet(s) Stats seraient supprimés.');
|
||||
|
||||
if ($dryRun) {
|
||||
$io->note('Mode dry-run : aucune suppression effectuée.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
foreach ($toDelete as $statsList) {
|
||||
foreach ($statsList as $stat) {
|
||||
$this->entityManager->remove($stat);
|
||||
}
|
||||
}
|
||||
$this->entityManager->flush();
|
||||
$io->success($totalToDelete . ' doublon(s) supprimé(s) !');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
|
@ -1,194 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\CityFollowUp;
|
||||
use App\Entity\Stats;
|
||||
use App\Repository\StatsRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:create-missing-communes-stats',
|
||||
description: 'Create Stats objects for missing communes using data from cities_insee.csv',
|
||||
)]
|
||||
class CreateMissingCommunesStatsCommand extends Command
|
||||
{
|
||||
private const CSV_PATH = '/home/poule/encrypted/stockage-syncable/www/development/html/osm-commerce-sf/counting_osm_objects/cities_insee.csv';
|
||||
|
||||
// List of themes to create CityFollowUp measurements for
|
||||
private const THEMES = [
|
||||
'charging_station',
|
||||
'defibrillator',
|
||||
'shop',
|
||||
'amenity',
|
||||
// Add more themes as needed
|
||||
];
|
||||
|
||||
private EntityManagerInterface $entityManager;
|
||||
private StatsRepository $statsRepository;
|
||||
|
||||
public function __construct(
|
||||
EntityManagerInterface $entityManager,
|
||||
StatsRepository $statsRepository
|
||||
) {
|
||||
parent::__construct();
|
||||
$this->entityManager = $entityManager;
|
||||
$this->statsRepository = $statsRepository;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addOption('limit', 'l', InputOption::VALUE_REQUIRED, 'Limit the number of communes to process', 0)
|
||||
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Simulate without modifying the database');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->title('Creating Stats objects for missing communes');
|
||||
|
||||
$limit = (int) $input->getOption('limit');
|
||||
$dryRun = $input->getOption('dry-run');
|
||||
|
||||
// Check if CSV file exists
|
||||
if (!file_exists(self::CSV_PATH)) {
|
||||
$io->error('CSV file not found: ' . self::CSV_PATH);
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
// Read CSV file
|
||||
$io->info('Reading CSV file: ' . self::CSV_PATH);
|
||||
$communes = $this->readCsvFile(self::CSV_PATH);
|
||||
$io->info(sprintf('Found %d communes in CSV file', count($communes)));
|
||||
|
||||
// Get existing Stats objects
|
||||
$existingStats = $this->statsRepository->findAll();
|
||||
$existingZones = [];
|
||||
foreach ($existingStats as $stats) {
|
||||
$existingZones[$stats->getZone()] = true;
|
||||
}
|
||||
$io->info(sprintf('Found %d existing Stats objects', count($existingZones)));
|
||||
|
||||
// Find missing communes
|
||||
$missingCommunes = [];
|
||||
foreach ($communes as $commune) {
|
||||
if (!isset($existingZones[$commune['code_insee']])) {
|
||||
$missingCommunes[] = $commune;
|
||||
}
|
||||
}
|
||||
$io->info(sprintf('Found %d missing communes', count($missingCommunes)));
|
||||
|
||||
// Apply limit if specified
|
||||
if ($limit > 0 && count($missingCommunes) > $limit) {
|
||||
$io->info(sprintf('Limiting to %d communes', $limit));
|
||||
$missingCommunes = array_slice($missingCommunes, 0, $limit);
|
||||
}
|
||||
|
||||
// Create Stats objects for missing communes
|
||||
$created = 0;
|
||||
$now = new \DateTime();
|
||||
|
||||
foreach ($missingCommunes as $commune) {
|
||||
$io->text(sprintf('Processing commune: %s (%s)', $commune['nom_standard'], $commune['code_insee']));
|
||||
|
||||
if (!$dryRun) {
|
||||
// Create new Stats object
|
||||
$stats = new Stats();
|
||||
$stats->setZone($commune['code_insee']);
|
||||
$stats->setName($commune['nom_standard']);
|
||||
|
||||
// Handle population - convert empty string to null or cast to int
|
||||
$population = $commune['population'] !== '' ? (int) $commune['population'] : null;
|
||||
$stats->setPopulation($population);
|
||||
|
||||
$stats->setDateCreated($now);
|
||||
$stats->setKind('command'); // Set the kind to 'command'
|
||||
|
||||
// Set coordinates if available and not empty
|
||||
if (isset($commune['latitude_centre']) && isset($commune['longitude_centre'])) {
|
||||
// Convert empty strings to null for numeric fields
|
||||
$lat = $commune['latitude_centre'] !== '' ? $commune['latitude_centre'] : null;
|
||||
$lon = $commune['longitude_centre'] !== '' ? $commune['longitude_centre'] : null;
|
||||
|
||||
$stats->setLat($lat);
|
||||
$stats->setLon($lon);
|
||||
}
|
||||
|
||||
// Create CityFollowUp measurements for each theme
|
||||
foreach (self::THEMES as $theme) {
|
||||
// Create a basic measurement with a default value
|
||||
// In a real scenario, you would fetch actual data for each theme
|
||||
$followUp = new CityFollowUp();
|
||||
$followUp->setName($theme . '_count');
|
||||
$followUp->setMeasure(0); // Default value, should be replaced with actual data
|
||||
$followUp->setDate($now);
|
||||
$followUp->setStats($stats);
|
||||
|
||||
$this->entityManager->persist($followUp);
|
||||
}
|
||||
|
||||
$this->entityManager->persist($stats);
|
||||
$created++;
|
||||
|
||||
// Flush every 20 entities to avoid memory issues
|
||||
if ($created % 20 === 0) {
|
||||
$this->entityManager->flush();
|
||||
$this->entityManager->clear(Stats::class);
|
||||
$this->entityManager->clear(CityFollowUp::class);
|
||||
$io->text(sprintf('Flushed after creating %d Stats objects', $created));
|
||||
}
|
||||
} else {
|
||||
$created++;
|
||||
}
|
||||
}
|
||||
|
||||
// Final flush
|
||||
if (!$dryRun && $created > 0) {
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
$io->success(sprintf('Dry run completed. Would have created %d Stats objects for missing communes.', $created));
|
||||
} else {
|
||||
$io->success(sprintf('Created %d Stats objects for missing communes.', $created));
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read CSV file and return an array of communes
|
||||
*/
|
||||
private function readCsvFile(string $path): array
|
||||
{
|
||||
$communes = [];
|
||||
|
||||
if (($handle = fopen($path, 'r')) !== false) {
|
||||
// Read header
|
||||
$header = fgetcsv($handle, 0, ',');
|
||||
if ($header === false) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Read data
|
||||
while (($row = fgetcsv($handle, 0, ',')) !== false) {
|
||||
$commune = [];
|
||||
foreach ($header as $i => $key) {
|
||||
$commune[$key] = $row[$i] ?? '';
|
||||
}
|
||||
$communes[] = $commune;
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
}
|
||||
|
||||
return $communes;
|
||||
}
|
||||
}
|
|
@ -1,262 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\Stats;
|
||||
use App\Repository\StatsRepository;
|
||||
use App\Service\ActionLogger;
|
||||
use App\Service\BudgetService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:create-missing-stats-from-csv',
|
||||
description: 'Crée des objets Stats manquants à partir du fichier communes_france.csv',
|
||||
)]
|
||||
class CreateMissingStatsFromCsvCommand extends Command
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
private StatsRepository $statsRepository;
|
||||
private ActionLogger $actionLogger;
|
||||
private ?BudgetService $budgetService;
|
||||
|
||||
public function __construct(
|
||||
EntityManagerInterface $entityManager,
|
||||
StatsRepository $statsRepository,
|
||||
ActionLogger $actionLogger,
|
||||
?BudgetService $budgetService = null
|
||||
) {
|
||||
parent::__construct();
|
||||
$this->entityManager = $entityManager;
|
||||
$this->statsRepository = $statsRepository;
|
||||
$this->actionLogger = $actionLogger;
|
||||
$this->budgetService = $budgetService;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addOption('limit', 'l', InputOption::VALUE_REQUIRED, 'Limite le nombre de communes à traiter', 0)
|
||||
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Simule sans modifier la base de données')
|
||||
->setHelp('Cette commande examine le fichier CSV des communes et crée des objets Stats pour les communes qui n\'en ont pas encore. Les objets sont créés avec les informations du CSV et complétés avec des données supplémentaires (coordonnées, budget, etc.).');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->title('Création des objets Stats manquants à partir du fichier CSV');
|
||||
|
||||
$limit = (int) $input->getOption('limit');
|
||||
$dryRun = $input->getOption('dry-run');
|
||||
|
||||
$this->actionLogger->log('command/create_missing_stats_from_csv', [
|
||||
'limit' => $limit,
|
||||
'dry_run' => $dryRun
|
||||
]);
|
||||
|
||||
// Vérifier si le fichier CSV existe
|
||||
$csvFile = 'communes_france.csv';
|
||||
if (!file_exists($csvFile)) {
|
||||
$io->error('Le fichier CSV des communes n\'existe pas. Veuillez exécuter le script fetch_communes.py pour le générer.');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$createdCount = 0;
|
||||
$skippedCount = 0;
|
||||
$errorCount = 0;
|
||||
|
||||
// Ouvrir le fichier CSV
|
||||
$handle = fopen($csvFile, 'r');
|
||||
if (!$handle) {
|
||||
$io->error('Impossible d\'ouvrir le fichier CSV des communes.');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
// Lire l'en-tête pour déterminer les indices des colonnes
|
||||
$header = fgetcsv($handle);
|
||||
$indices = array_flip($header);
|
||||
|
||||
// Vérifier que les colonnes nécessaires existent
|
||||
$requiredColumns = ['code', 'nom'];
|
||||
foreach ($requiredColumns as $column) {
|
||||
if (!isset($indices[$column])) {
|
||||
$io->error("La colonne '$column' est manquante dans le fichier CSV.");
|
||||
fclose($handle);
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
$io->info(sprintf('Lecture du fichier CSV: %s', $csvFile));
|
||||
$io->info(sprintf('Colonnes trouvées: %s', implode(', ', $header)));
|
||||
|
||||
// Compter le nombre total de lignes pour la barre de progression
|
||||
$totalLines = 0;
|
||||
$tempHandle = fopen($csvFile, 'r');
|
||||
if ($tempHandle) {
|
||||
// Skip header
|
||||
fgetcsv($tempHandle);
|
||||
while (fgetcsv($tempHandle) !== false) {
|
||||
$totalLines++;
|
||||
}
|
||||
fclose($tempHandle);
|
||||
}
|
||||
|
||||
$io->info(sprintf('Nombre total de communes dans le CSV: %d', $totalLines));
|
||||
|
||||
// Créer une barre de progression
|
||||
$progressBar = $io->createProgressBar($totalLines);
|
||||
$progressBar->start();
|
||||
|
||||
// Traiter chaque ligne du CSV
|
||||
while (($data = fgetcsv($handle)) !== false) {
|
||||
try {
|
||||
$inseeCode = $data[$indices['code']];
|
||||
|
||||
// Vérifier si une Stats existe déjà pour ce code INSEE
|
||||
$existingStat = $this->statsRepository->findOneBy(['zone' => $inseeCode]);
|
||||
if ($existingStat) {
|
||||
$skippedCount++;
|
||||
$progressBar->advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Créer un nouvel objet Stats
|
||||
$stat = new Stats();
|
||||
$stat->setZone($inseeCode)
|
||||
->setDateCreated(new \DateTime())
|
||||
->setDateModified(new \DateTime())
|
||||
->setKind('command'); // Utiliser 'command' comme source
|
||||
|
||||
// Ajouter le nom si disponible
|
||||
if (isset($indices['nom']) && !empty($data[$indices['nom']])) {
|
||||
$stat->setName($data[$indices['nom']]);
|
||||
}
|
||||
|
||||
// Ajouter la population si disponible
|
||||
if (isset($indices['population']) && !empty($data[$indices['population']])) {
|
||||
$stat->setPopulation((int)$data[$indices['population']]);
|
||||
}
|
||||
|
||||
// Ajouter les codes postaux si disponibles
|
||||
if (isset($indices['codesPostaux']) && !empty($data[$indices['codesPostaux']])) {
|
||||
$stat->setCodesPostaux($data[$indices['codesPostaux']]);
|
||||
}
|
||||
|
||||
// Ajouter le SIREN si disponible
|
||||
if (isset($indices['siren']) && !empty($data[$indices['siren']])) {
|
||||
$stat->setSiren((int)$data[$indices['siren']]);
|
||||
}
|
||||
|
||||
// Ajouter le code EPCI si disponible
|
||||
if (isset($indices['codeEpci']) && !empty($data[$indices['codeEpci']])) {
|
||||
$stat->setCodeEpci((int)$data[$indices['codeEpci']]);
|
||||
}
|
||||
|
||||
// Compléter les données manquantes (coordonnées, budget, etc.)
|
||||
if (!$dryRun) {
|
||||
$this->completeStatsData($stat);
|
||||
}
|
||||
|
||||
// Persister l'objet Stats
|
||||
if (!$dryRun) {
|
||||
$this->entityManager->persist($stat);
|
||||
}
|
||||
$createdCount++;
|
||||
|
||||
// Appliquer la limite si spécifiée
|
||||
if ($limit > 0 && $createdCount >= $limit) {
|
||||
$io->info(sprintf('Limite de %d communes atteinte.', $limit));
|
||||
break;
|
||||
}
|
||||
|
||||
// Flush tous les 100 objets pour éviter de surcharger la mémoire
|
||||
if (!$dryRun && $createdCount % 100 === 0) {
|
||||
$this->entityManager->flush();
|
||||
$this->entityManager->clear(Stats::class);
|
||||
$io->info(sprintf('Flush après création de %d objets Stats', $createdCount));
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$errorCount++;
|
||||
$this->actionLogger->log('error_command_create_missing_stats_from_csv', [
|
||||
'insee_code' => $inseeCode ?? 'unknown',
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
if ($output->isVerbose()) {
|
||||
$io->warning(sprintf('Erreur pour la commune %s: %s', $inseeCode ?? 'unknown', $e->getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
$progressBar->advance();
|
||||
}
|
||||
|
||||
$progressBar->finish();
|
||||
$io->newLine(2);
|
||||
|
||||
// Flush les derniers objets
|
||||
if (!$dryRun && $createdCount > 0) {
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
|
||||
if ($dryRun) {
|
||||
$io->success(sprintf('Simulation terminée. %d communes auraient été ajoutées, %d déjà existantes, %d erreurs.', $createdCount, $skippedCount, $errorCount));
|
||||
} else {
|
||||
$io->success(sprintf('Création des Stats manquantes terminée : %d communes ajoutées, %d déjà existantes, %d erreurs.', $createdCount, $skippedCount, $errorCount));
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complète les données manquantes d'un objet Stats (coordonnées, budget, etc.)
|
||||
*/
|
||||
private function completeStatsData(Stats $stat): void
|
||||
{
|
||||
$insee_code = $stat->getZone();
|
||||
|
||||
// Compléter les coordonnées si manquantes
|
||||
if (!$stat->getLat() || !$stat->getLon()) {
|
||||
try {
|
||||
$apiUrl = 'https://geo.api.gouv.fr/communes/' . $insee_code . '?fields=centre';
|
||||
$response = @file_get_contents($apiUrl);
|
||||
if ($response !== false) {
|
||||
$data = json_decode($response, true);
|
||||
if (isset($data['centre']['coordinates']) && count($data['centre']['coordinates']) === 2) {
|
||||
$stat->setLon((string)$data['centre']['coordinates'][0]);
|
||||
$stat->setLat((string)$data['centre']['coordinates'][1]);
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->actionLogger->log('error_complete_stats_data', [
|
||||
'insee_code' => $insee_code,
|
||||
'error' => 'Failed to fetch coordinates: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Compléter le budget si manquant
|
||||
if (!$stat->getBudgetAnnuel() && $this->budgetService !== null) {
|
||||
try {
|
||||
$budget = $this->budgetService->getBudgetAnnuel($insee_code);
|
||||
if ($budget !== null) {
|
||||
$stat->setBudgetAnnuel((string)$budget);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->actionLogger->log('error_complete_stats_data', [
|
||||
'insee_code' => $insee_code,
|
||||
'error' => 'Failed to fetch budget: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculer le pourcentage de complétion
|
||||
$stat->computeCompletionPercent();
|
||||
}
|
||||
}
|
|
@ -1,116 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\Demande;
|
||||
use App\Entity\Stats;
|
||||
use App\Repository\DemandeRepository;
|
||||
use App\Repository\StatsRepository;
|
||||
use App\Service\Motocultrice;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:create-stats-from-demandes',
|
||||
description: 'Create Stats objects for cities with Demandes but no Stats',
|
||||
)]
|
||||
class CreateStatsFromDemandesCommand extends Command
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
private DemandeRepository $demandeRepository;
|
||||
private StatsRepository $statsRepository;
|
||||
private Motocultrice $motocultrice;
|
||||
|
||||
public function __construct(
|
||||
EntityManagerInterface $entityManager,
|
||||
DemandeRepository $demandeRepository,
|
||||
StatsRepository $statsRepository,
|
||||
Motocultrice $motocultrice
|
||||
) {
|
||||
parent::__construct();
|
||||
$this->entityManager = $entityManager;
|
||||
$this->demandeRepository = $demandeRepository;
|
||||
$this->statsRepository = $statsRepository;
|
||||
$this->motocultrice = $motocultrice;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->title('Creating Stats objects for cities with Demandes but no Stats');
|
||||
|
||||
// Find all Demandes with INSEE codes
|
||||
$demandesWithInsee = $this->demandeRepository->createQueryBuilder('d')
|
||||
->where('d.insee IS NOT NULL')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
|
||||
if (empty($demandesWithInsee)) {
|
||||
$io->warning('No Demandes with INSEE codes found.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$io->info(sprintf('Found %d Demandes with INSEE codes.', count($demandesWithInsee)));
|
||||
|
||||
// Group Demandes by INSEE code
|
||||
$demandesByInsee = [];
|
||||
/** @var Demande $demande */
|
||||
foreach ($demandesWithInsee as $demande) {
|
||||
$insee = $demande->getInsee();
|
||||
if (!isset($demandesByInsee[$insee])) {
|
||||
$demandesByInsee[$insee] = [];
|
||||
}
|
||||
$demandesByInsee[$insee][] = $demande;
|
||||
}
|
||||
|
||||
$io->info(sprintf('Found %d unique INSEE codes.', count($demandesByInsee)));
|
||||
|
||||
// Check which INSEE codes don't have Stats objects
|
||||
$newStatsCount = 0;
|
||||
foreach ($demandesByInsee as $insee => $demandes) {
|
||||
$stats = $this->statsRepository->findOneBy(['zone' => $insee]);
|
||||
if ($stats === null) {
|
||||
// Create a new Stats object for this INSEE code
|
||||
$stats = new Stats();
|
||||
$stats->setZone((string) $insee);
|
||||
|
||||
// Try to get the city name from the API based on INSEE code
|
||||
$apiCityName = $this->motocultrice->get_city_osm_from_zip_code($insee);
|
||||
if ($apiCityName) {
|
||||
// Use the API-provided city name
|
||||
$stats->setName($apiCityName);
|
||||
$io->text(sprintf('Using API city name: %s', $apiCityName));
|
||||
} else {
|
||||
// Fallback to the query from the first Demande if API doesn't return a name
|
||||
$firstDemande = $demandes[0];
|
||||
if ($firstDemande->getQuery()) {
|
||||
$stats->setName($firstDemande->getQuery());
|
||||
$io->text(sprintf('Using query as fallback name: %s', $firstDemande->getQuery()));
|
||||
}
|
||||
}
|
||||
|
||||
$stats->setDateCreated(new \DateTime());
|
||||
$stats->setDateLabourageRequested(new \DateTime());
|
||||
$stats->setKind('request'); // Set the kind to 'request' as it's created from Demandes
|
||||
|
||||
$this->entityManager->persist($stats);
|
||||
$newStatsCount++;
|
||||
|
||||
$io->text(sprintf('Created Stats for INSEE code %s', $insee));
|
||||
}
|
||||
}
|
||||
|
||||
if ($newStatsCount > 0) {
|
||||
$this->entityManager->flush();
|
||||
$io->success(sprintf('Created %d new Stats objects.', $newStatsCount));
|
||||
} else {
|
||||
$io->info('No new Stats objects needed to be created.');
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\CityFollowUp;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:delete-zero-cityfollowup',
|
||||
description: 'Supprime tous les CityFollowUp dont la mesure vaut 0.'
|
||||
)]
|
||||
class DeleteZeroCityFollowUpCommand extends Command
|
||||
{
|
||||
public function __construct(private EntityManagerInterface $em)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$repo = $this->em->getRepository(CityFollowUp::class);
|
||||
$toDelete = $repo->createQueryBuilder('c')
|
||||
->where('c.measure = 0')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
$count = count($toDelete);
|
||||
foreach ($toDelete as $entity) {
|
||||
$this->em->remove($entity);
|
||||
}
|
||||
$this->em->flush();
|
||||
$output->writeln("$count CityFollowUp supprimés (mesure = 0)");
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
|
@ -1,167 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\Stats;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:export-stats',
|
||||
description: 'Exporte les objets Stats au format JSON avec leurs propriétés de nom et de décomptes'
|
||||
)]
|
||||
class ExportStatsCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManagerInterface $entityManager
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addOption(
|
||||
'output',
|
||||
'o',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Fichier de sortie (par défaut: stats_export.json)',
|
||||
'stats_export.json'
|
||||
)
|
||||
->addOption(
|
||||
'zone',
|
||||
'z',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Code INSEE spécifique à exporter (optionnel)'
|
||||
)
|
||||
->addOption(
|
||||
'pretty',
|
||||
'p',
|
||||
InputOption::VALUE_NONE,
|
||||
'Formater le JSON avec indentation'
|
||||
)
|
||||
->setHelp('Cette commande exporte les objets Stats au format JSON avec leurs propriétés de nom et de décomptes.');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$outputFile = $input->getOption('output');
|
||||
$zone = $input->getOption('zone');
|
||||
$pretty = $input->getOption('pretty');
|
||||
|
||||
$io->title('Export des objets Stats');
|
||||
|
||||
try {
|
||||
// Construire la requête
|
||||
$qb = $this->entityManager->getRepository(Stats::class)->createQueryBuilder('s');
|
||||
|
||||
if ($zone) {
|
||||
$qb->where('s.zone = :zone')
|
||||
->setParameter('zone', $zone);
|
||||
$io->note("Export pour la zone INSEE: $zone");
|
||||
}
|
||||
|
||||
$stats = $qb->getQuery()->getResult();
|
||||
|
||||
if (empty($stats)) {
|
||||
$io->warning('Aucun objet Stats trouvé.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$io->info(sprintf('Export de %d objet(s) Stats...', count($stats)));
|
||||
|
||||
// Préparer les données pour l'export
|
||||
$exportData = [];
|
||||
|
||||
foreach ($stats as $stat) {
|
||||
$statData = [
|
||||
'id' => $stat->getId(),
|
||||
'zone' => $stat->getZone(),
|
||||
'name' => $stat->getName(),
|
||||
'dateCreated' => $stat->getDateCreated() ? $stat->getDateCreated()->format('Y-m-d H:i:s') : null,
|
||||
'dateModified' => $stat->getDateModified() ? $stat->getDateModified()->format('Y-m-d H:i:s') : null,
|
||||
'population' => $stat->getPopulation(),
|
||||
'budgetAnnuel' => $stat->getBudgetAnnuel(),
|
||||
'siren' => $stat->getSiren(),
|
||||
'codeEpci' => $stat->getCodeEpci(),
|
||||
'codesPostaux' => $stat->getCodesPostaux(),
|
||||
'decomptes' => [
|
||||
'placesCount' => $stat->getPlacesCount(),
|
||||
'avecHoraires' => $stat->getAvecHoraires(),
|
||||
'avecAdresse' => $stat->getAvecAdresse(),
|
||||
'avecSite' => $stat->getAvecSite(),
|
||||
'avecAccessibilite' => $stat->getAvecAccessibilite(),
|
||||
'avecNote' => $stat->getAvecNote(),
|
||||
'completionPercent' => $stat->getCompletionPercent(),
|
||||
// 'placesCountReal' => $stat->getPlaces()->count(), // SUPPRIMÉ
|
||||
],
|
||||
'followups' => []
|
||||
];
|
||||
|
||||
// Ajouter les followups si disponibles
|
||||
foreach ($stat->getCityFollowUps() as $followup) {
|
||||
$statData['followups'][] = [
|
||||
'name' => $followup->getName(),
|
||||
'measure' => $followup->getMeasure(),
|
||||
'date' => $followup->getDate()->format('Y-m-d H:i:s')
|
||||
];
|
||||
}
|
||||
|
||||
$exportData[] = $statData;
|
||||
}
|
||||
|
||||
// Préparer le JSON
|
||||
$jsonOptions = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES;
|
||||
if ($pretty) {
|
||||
$jsonOptions |= JSON_PRETTY_PRINT;
|
||||
}
|
||||
|
||||
$jsonContent = json_encode($exportData, $jsonOptions);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new \Exception('Erreur lors de l\'encodage JSON: ' . json_last_error_msg());
|
||||
}
|
||||
|
||||
// Écrire dans le fichier
|
||||
$bytesWritten = file_put_contents($outputFile, $jsonContent);
|
||||
|
||||
if ($bytesWritten === false) {
|
||||
throw new \Exception("Impossible d'écrire dans le fichier: $outputFile");
|
||||
}
|
||||
|
||||
$io->success(sprintf(
|
||||
'Export terminé avec succès ! %d objet(s) exporté(s) vers %s (%s octets)',
|
||||
count($stats),
|
||||
$outputFile,
|
||||
number_format($bytesWritten, 0, ',', ' ')
|
||||
));
|
||||
|
||||
// Afficher un aperçu des données
|
||||
if ($io->isVerbose()) {
|
||||
$io->section('Aperçu des données exportées');
|
||||
foreach ($exportData as $index => $data) {
|
||||
$io->text(sprintf(
|
||||
'%d. %s (%s) - %d lieux, %d%% complété',
|
||||
$index + 1,
|
||||
$data['name'],
|
||||
$data['zone'],
|
||||
$data['decomptes']['placesCountReal'],
|
||||
$data['decomptes']['completionPercent']
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$io->error('Erreur lors de l\'export: ' . $e->getMessage());
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,230 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\Stats;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:extract-insee-zones',
|
||||
description: 'Extrait les données OSM pour chaque zone INSEE à partir du fichier france-latest.osm.pbf',
|
||||
)]
|
||||
class ExtractInseeZonesCommand extends Command
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addArgument('insee-code', InputArgument::OPTIONAL, 'Code INSEE spécifique à traiter')
|
||||
->addOption('limit', 'l', InputOption::VALUE_REQUIRED, 'Limite le nombre de villes à traiter', null)
|
||||
->addOption('force', 'f', InputOption::VALUE_NONE, 'Force l\'extraction même si le fichier JSON existe déjà')
|
||||
->addOption('keep-pbf', 'k', InputOption::VALUE_NONE, 'Conserve les fichiers PBF intermédiaires')
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$inseeCode = $input->getArgument('insee-code');
|
||||
$limit = $input->getOption('limit');
|
||||
$force = $input->getOption('force');
|
||||
$keepPbf = $input->getOption('keep-pbf');
|
||||
|
||||
// Créer le dossier oss_data s'il n'existe pas
|
||||
$ossDataDir = __DIR__ . '/../../oss_data';
|
||||
if (!is_dir($ossDataDir)) {
|
||||
$io->note('Création du dossier oss_data');
|
||||
mkdir($ossDataDir, 0755, true);
|
||||
}
|
||||
|
||||
// Vérifier que le fichier france-latest.osm.pbf existe
|
||||
$francePbfFile = $ossDataDir . '/france-latest.osm.pbf';
|
||||
if (!file_exists($francePbfFile)) {
|
||||
$io->note('Le fichier france-latest.osm.pbf n\'existe pas. Téléchargement en cours depuis Geofabrik...');
|
||||
|
||||
// URL de téléchargement
|
||||
$downloadUrl = 'https://download.geofabrik.de/europe/france-latest.osm.pbf';
|
||||
|
||||
// Télécharger le fichier
|
||||
try {
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'header' => "User-Agent: OSM-Commerces/1.0\r\n"
|
||||
]
|
||||
]);
|
||||
|
||||
// Utiliser file_get_contents pour télécharger le fichier
|
||||
$io->section('Téléchargement du fichier france-latest.osm.pbf');
|
||||
$io->progressStart(100);
|
||||
|
||||
// Téléchargement par morceaux pour pouvoir afficher une progression
|
||||
$fileHandle = fopen($francePbfFile, 'w');
|
||||
$curlHandle = curl_init($downloadUrl);
|
||||
|
||||
curl_setopt($curlHandle, CURLOPT_FILE, $fileHandle);
|
||||
curl_setopt($curlHandle, CURLOPT_HEADER, 0);
|
||||
curl_setopt($curlHandle, CURLOPT_USERAGENT, 'OSM-Commerces/1.0');
|
||||
curl_setopt($curlHandle, CURLOPT_NOPROGRESS, false);
|
||||
// Use a simpler progress reporting approach
|
||||
curl_setopt($curlHandle, CURLOPT_PROGRESSFUNCTION, function($resource, $downloadSize, $downloaded) use ($io) {
|
||||
static $lastProgress = 0;
|
||||
if ($downloadSize > 0) {
|
||||
$progress = round(($downloaded / $downloadSize) * 100);
|
||||
if ($progress > $lastProgress) {
|
||||
$io->progressAdvance($progress - $lastProgress);
|
||||
$lastProgress = $progress;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$success = curl_exec($curlHandle);
|
||||
curl_close($curlHandle);
|
||||
fclose($fileHandle);
|
||||
|
||||
$io->progressFinish();
|
||||
|
||||
if (!$success) {
|
||||
throw new \Exception('Échec du téléchargement');
|
||||
}
|
||||
|
||||
$io->success('Le fichier france-latest.osm.pbf a été téléchargé avec succès.');
|
||||
} catch (\Exception $e) {
|
||||
$io->error('Erreur lors du téléchargement du fichier france-latest.osm.pbf: ' . $e->getMessage());
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
// Vérifier que le dossier polygons existe
|
||||
$polygonsDir = __DIR__ . '/../../counting_osm_objects/polygons';
|
||||
if (!is_dir($polygonsDir)) {
|
||||
$io->error('Le dossier des polygones n\'existe pas. Veuillez d\'abord exécuter la commande app:retrieve-city-polygons.');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
// Créer le dossier pour les extractions JSON si nécessaire
|
||||
$extractsDir = __DIR__ . '/../../insee_extracts';
|
||||
if (!is_dir($extractsDir)) {
|
||||
$io->note('Création du dossier insee_extracts');
|
||||
mkdir($extractsDir, 0755, true);
|
||||
}
|
||||
|
||||
// Récupérer les Stats à traiter
|
||||
$statsRepo = $this->entityManager->getRepository(Stats::class);
|
||||
|
||||
if ($inseeCode) {
|
||||
$io->note(sprintf('Traitement du code INSEE spécifique: %s', $inseeCode));
|
||||
$allStats = $statsRepo->findBy(['zone' => $inseeCode]);
|
||||
|
||||
if (empty($allStats)) {
|
||||
$io->error(sprintf('Aucune ville trouvée avec le code INSEE %s', $inseeCode));
|
||||
return Command::FAILURE;
|
||||
}
|
||||
} else {
|
||||
$io->note('Traitement de toutes les villes');
|
||||
$criteria = [];
|
||||
$orderBy = ['id' => 'ASC'];
|
||||
$limitValue = $limit ? (int)$limit : null;
|
||||
|
||||
$allStats = $statsRepo->findBy($criteria, $orderBy, $limitValue);
|
||||
}
|
||||
|
||||
$totalCount = count($allStats);
|
||||
$existingCount = 0;
|
||||
$createdCount = 0;
|
||||
$errorCount = 0;
|
||||
|
||||
$io->progressStart($totalCount);
|
||||
|
||||
// Pour chaque Stats, extraire les données si nécessaire
|
||||
foreach ($allStats as $stat) {
|
||||
$inseeCode = $stat->getZone();
|
||||
if (!$inseeCode) {
|
||||
$io->progressAdvance();
|
||||
continue;
|
||||
}
|
||||
|
||||
$polygonFile = $polygonsDir . '/commune_' . $inseeCode . '.poly';
|
||||
$extractPbfFile = $extractsDir . '/commune_' . $inseeCode . '.osm.pbf';
|
||||
$extractJsonFile = $extractsDir . '/commune_' . $inseeCode . '.json';
|
||||
|
||||
// Vérifier si le polygone existe
|
||||
if (!file_exists($polygonFile)) {
|
||||
$io->debug(sprintf('Polygone manquant pour %s', $inseeCode));
|
||||
$errorCount++;
|
||||
$io->progressAdvance();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Vérifier si l'extraction JSON existe déjà
|
||||
if (file_exists($extractJsonFile) && !$force) {
|
||||
$existingCount++;
|
||||
$io->progressAdvance();
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// Étape 1: Extraire les données de france-latest.osm.pbf vers un fichier PBF pour la zone
|
||||
$extractCommand = 'osmium extract -p ' . $polygonFile . ' ' . $francePbfFile . ' -o ' . $extractPbfFile;
|
||||
$outputLines = [];
|
||||
$returnVar = 0;
|
||||
exec($extractCommand, $outputLines, $returnVar);
|
||||
|
||||
if ($returnVar !== 0 || !file_exists($extractPbfFile)) {
|
||||
$io->debug(sprintf('Erreur lors de l\'extraction PBF pour %s: %s', $inseeCode, implode("\n", $outputLines)));
|
||||
$errorCount++;
|
||||
$io->progressAdvance();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Étape 2: Convertir le fichier PBF en JSON
|
||||
$exportCommand = 'osmium export ' . $extractPbfFile . ' -f json -o ' . $extractJsonFile;
|
||||
$outputLines = [];
|
||||
$returnVar = 0;
|
||||
exec($exportCommand, $outputLines, $returnVar);
|
||||
|
||||
if ($returnVar === 0 && file_exists($extractJsonFile)) {
|
||||
$createdCount++;
|
||||
} else {
|
||||
$io->debug(sprintf('Erreur lors de l\'export JSON pour %s: %s', $inseeCode, implode("\n", $outputLines)));
|
||||
$errorCount++;
|
||||
}
|
||||
|
||||
// Supprimer le fichier PBF intermédiaire pour économiser de l'espace
|
||||
if (!$keepPbf && file_exists($extractPbfFile)) {
|
||||
unlink($extractPbfFile);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$errorCount++;
|
||||
$io->debug(sprintf('Exception pour %s: %s', $inseeCode, $e->getMessage()));
|
||||
}
|
||||
|
||||
$io->progressAdvance();
|
||||
}
|
||||
|
||||
$io->progressFinish();
|
||||
|
||||
$io->success(sprintf(
|
||||
"Extraction des zones INSEE terminée : %d extractions créées, %d déjà existantes, %d erreurs sur un total de %d communes.",
|
||||
$createdCount,
|
||||
$existingCount,
|
||||
$errorCount,
|
||||
$totalCount
|
||||
));
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
|
@ -1,130 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\Stats;
|
||||
use App\Entity\CityFollowUp;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:import-cityfollowup-ctc',
|
||||
description: 'Importe les CityFollowUp à partir du JSON DailyStats d\'une ville (Complète tes commerces)'
|
||||
)]
|
||||
class ImportCityFollowupFromCTCCommand extends Command
|
||||
{
|
||||
public function __construct(private EntityManagerInterface $entityManager)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addArgument('insee_code', InputArgument::REQUIRED, 'Code INSEE de la ville')
|
||||
->addOption('url', null, InputOption::VALUE_OPTIONAL, 'URL CTC de la ville (sinon auto-déduit)')
|
||||
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Simule l\'import sans rien écrire');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$insee = $input->getArgument('insee_code');
|
||||
$url = $input->getOption('url');
|
||||
$dryRun = $input->getOption('dry-run');
|
||||
|
||||
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee]);
|
||||
if (!$stats) {
|
||||
$io->error("Aucune stats trouvée pour le code INSEE $insee");
|
||||
return Command::FAILURE;
|
||||
}
|
||||
if (!$url) {
|
||||
$url = $stats->getCTCurlBase() . '_dailystats.json';
|
||||
}
|
||||
$io->title("Import des CityFollowUp depuis $url");
|
||||
// --- Gestion explicite des erreurs HTTP (404, etc.) ---
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'ignore_errors' => true,
|
||||
'timeout' => 10,
|
||||
]
|
||||
]);
|
||||
$json = @file_get_contents($url, false, $context);
|
||||
$http_response_header = $http_response_header ?? [];
|
||||
$httpCode = null;
|
||||
foreach ($http_response_header as $header) {
|
||||
if (preg_match('#^HTTP/\\d+\\.\\d+ (\\d{3})#', $header, $m)) {
|
||||
$httpCode = (int)$m[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($json === false || $httpCode === 404) {
|
||||
$io->error("Impossible de télécharger le JSON DailyStats depuis $url (erreur HTTP $httpCode)");
|
||||
return Command::FAILURE;
|
||||
}
|
||||
$data = json_decode($json, true);
|
||||
if (!is_array($data)) {
|
||||
$io->error("Le JSON n'est pas un tableau valide");
|
||||
return Command::FAILURE;
|
||||
}
|
||||
$types = [
|
||||
'name' => ['field' => 'no_name', 'label' => 'name'],
|
||||
'hours' => ['field' => 'no_hours', 'label' => 'hours'],
|
||||
'website' => ['field' => 'no_website', 'label' => 'website'],
|
||||
'address' => ['field' => 'no_address', 'label' => 'address'],
|
||||
'siret' => ['field' => 'no_siret', 'label' => 'siret'],
|
||||
];
|
||||
$created = 0;
|
||||
$skipped = 0;
|
||||
$createdEntities = [];
|
||||
foreach ($data as $row) {
|
||||
$date = isset($row['date']) ? new \DateTime($row['date']) : null;
|
||||
if (!$date) continue;
|
||||
$total = $row['total'] ?? null;
|
||||
if (!$total) continue;
|
||||
foreach ($types as $type => $info) {
|
||||
$field = $info['field'];
|
||||
if (!isset($row[$field])) continue;
|
||||
$measure = $total - $row[$field]; // nombre d'objets complets pour ce champ
|
||||
$name = $type . '_count';
|
||||
// Vérifier doublon (même stats, même nom, même date)
|
||||
$existing = $this->entityManager->getRepository(CityFollowUp::class)->findOneBy([
|
||||
'stats' => $stats,
|
||||
'name' => $name,
|
||||
'date' => $date,
|
||||
]);
|
||||
if ($existing) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
$cfu = new CityFollowUp();
|
||||
$cfu->setStats($stats)
|
||||
->setName($name)
|
||||
->setDate($date)
|
||||
->setMeasure($measure);
|
||||
if (!$dryRun) {
|
||||
$this->entityManager->persist($cfu);
|
||||
$createdEntities[] = $cfu;
|
||||
}
|
||||
$created++;
|
||||
}
|
||||
}
|
||||
if (!$dryRun) {
|
||||
$this->entityManager->flush();
|
||||
// Vérification explicite du lien
|
||||
foreach ($createdEntities as $cfu) {
|
||||
if (!$cfu->getStats() || $cfu->getStats()->getId() !== $stats->getId()) {
|
||||
$io->warning('CityFollowUp non lié correctement à Stats ID ' . $stats->getId() . ' (ID CityFollowUp: ' . $cfu->getId() . ')');
|
||||
}
|
||||
}
|
||||
}
|
||||
$io->success("$created CityFollowUp créés, $skipped doublons ignorés.");
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
|
@ -1,235 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\CityFollowUp;
|
||||
use App\Entity\Stats;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:import-cityfollowup-csv',
|
||||
description: 'Importe les mesures thématiques depuis un fichier CSV pour une ville donnée'
|
||||
)]
|
||||
class ImportCityFollowupFromCsvCommand extends Command
|
||||
{
|
||||
private const CSV_DIR = '/home/poule/encrypted/stockage-syncable/www/development/html/osm-commerce-sf/counting_osm_objects/test_results';
|
||||
|
||||
public function __construct(
|
||||
private EntityManagerInterface $entityManager
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addArgument('insee', InputArgument::REQUIRED, 'Code INSEE de la ville')
|
||||
->addArgument('theme', InputArgument::REQUIRED, 'Thématique à importer (ex: borne-de-recharge, defibrillator)')
|
||||
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Simuler l\'import sans modifier la base de données');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$insee = $input->getArgument('insee');
|
||||
$theme = $input->getArgument('theme');
|
||||
$dryRun = $input->getOption('dry-run');
|
||||
|
||||
// Vérifier que la ville existe
|
||||
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee]);
|
||||
if (!$stats) {
|
||||
$io->error("Aucune ville trouvée avec le code INSEE $insee.");
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$io->title("Import des mesures thématiques pour {$stats->getName()} (INSEE: $insee) - Thème: $theme");
|
||||
|
||||
// Trouver le fichier CSV correspondant
|
||||
$csvPath = $this->findCsvFile($insee, $theme);
|
||||
if (!$csvPath) {
|
||||
$io->error("Aucun fichier CSV trouvé pour la ville $insee et la thématique $theme.");
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$io->info("Fichier CSV trouvé: $csvPath");
|
||||
|
||||
// Lire le fichier CSV
|
||||
$csvData = $this->readCsvFile($csvPath);
|
||||
if (empty($csvData)) {
|
||||
$io->error("Le fichier CSV est vide ou mal formaté.");
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$io->info(sprintf("Nombre de lignes dans le CSV: %d", count($csvData)));
|
||||
|
||||
// Récupérer les mesures existantes pour cette ville et cette thématique
|
||||
$existingMeasures = $this->getExistingMeasures($stats, $theme);
|
||||
$io->info(sprintf("Nombre de mesures existantes en base: %d", count($existingMeasures)));
|
||||
|
||||
// Comparer et importer les nouvelles mesures
|
||||
$imported = 0;
|
||||
$skipped = 0;
|
||||
$zeroSkipped = 0;
|
||||
|
||||
foreach ($csvData as $row) {
|
||||
// Ignorer les lignes avec des valeurs manquantes
|
||||
if (empty($row['date']) || empty($row['nombre_total']) || $row['nombre_total'] === '') {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Convertir la date
|
||||
try {
|
||||
$date = new \DateTime($row['date']);
|
||||
} catch (\Exception $e) {
|
||||
$io->warning("Date invalide: {$row['date']}");
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ignorer les mesures qui valent 0
|
||||
if ((float)$row['nombre_total'] === 0.0) {
|
||||
$zeroSkipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Vérifier si la mesure existe déjà
|
||||
$dateStr = $date->format('Y-m-d');
|
||||
if (isset($existingMeasures[$dateStr])) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Créer une nouvelle mesure pour le nombre total
|
||||
if (!$dryRun) {
|
||||
$followUp = new CityFollowUp();
|
||||
$followUp->setName($this->convertThemeToCityFollowUpName($theme) . '_count')
|
||||
->setMeasure((float)$row['nombre_total'])
|
||||
->setDate($date)
|
||||
->setStats($stats);
|
||||
$this->entityManager->persist($followUp);
|
||||
|
||||
// Créer une mesure pour la complétion si disponible
|
||||
if (isset($row['pourcentage_completion']) && $row['pourcentage_completion'] !== '') {
|
||||
$followUpCompletion = new CityFollowUp();
|
||||
$followUpCompletion->setName($this->convertThemeToCityFollowUpName($theme) . '_completion')
|
||||
->setMeasure((float)$row['pourcentage_completion'])
|
||||
->setDate($date)
|
||||
->setStats($stats);
|
||||
$this->entityManager->persist($followUpCompletion);
|
||||
}
|
||||
|
||||
$imported++;
|
||||
|
||||
// Flush toutes les 50 entités pour éviter de surcharger la mémoire
|
||||
if ($imported % 50 === 0) {
|
||||
$this->entityManager->flush();
|
||||
$this->entityManager->clear(CityFollowUp::class);
|
||||
}
|
||||
} else {
|
||||
$imported++;
|
||||
}
|
||||
}
|
||||
|
||||
// Flush final
|
||||
if (!$dryRun && $imported > 0) {
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
$io->success("Simulation terminée: $imported mesures seraient importées, $skipped mesures ignorées (déjà existantes), $zeroSkipped mesures ignorées (valeur zéro).");
|
||||
} else {
|
||||
$io->success("Import terminé: $imported mesures importées, $skipped mesures ignorées (déjà existantes), $zeroSkipped mesures ignorées (valeur zéro).");
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve le fichier CSV correspondant à la ville et à la thématique
|
||||
*/
|
||||
private function findCsvFile(string $insee, string $theme): ?string
|
||||
{
|
||||
$finder = new Finder();
|
||||
$finder->files()
|
||||
->in(self::CSV_DIR)
|
||||
->name("commune_{$insee}_{$theme}.csv");
|
||||
|
||||
if (!$finder->hasResults()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($finder as $file) {
|
||||
return $file->getRealPath();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lit le fichier CSV et retourne un tableau de données
|
||||
*/
|
||||
private function readCsvFile(string $path): array
|
||||
{
|
||||
$data = [];
|
||||
if (($handle = fopen($path, "r")) !== false) {
|
||||
// Lire l'en-tête
|
||||
$header = fgetcsv($handle, 1000, ",");
|
||||
if ($header === false) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Lire les données
|
||||
while (($row = fgetcsv($handle, 1000, ",")) !== false) {
|
||||
$rowData = [];
|
||||
foreach ($header as $i => $key) {
|
||||
$rowData[$key] = $row[$i] ?? '';
|
||||
}
|
||||
$data[] = $rowData;
|
||||
}
|
||||
fclose($handle);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les mesures existantes pour cette ville et cette thématique
|
||||
*/
|
||||
private function getExistingMeasures(Stats $stats, string $theme): array
|
||||
{
|
||||
$themeName = $this->convertThemeToCityFollowUpName($theme);
|
||||
$existingMeasures = [];
|
||||
|
||||
foreach ($stats->getCityFollowUps() as $followUp) {
|
||||
if ($followUp->getName() === $themeName . '_count') {
|
||||
$date = $followUp->getDate()->format('Y-m-d');
|
||||
$existingMeasures[$date] = $followUp;
|
||||
}
|
||||
}
|
||||
|
||||
return $existingMeasures;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit le nom de thématique du CSV en nom utilisé dans CityFollowUp
|
||||
*/
|
||||
private function convertThemeToCityFollowUpName(string $theme): string
|
||||
{
|
||||
$themeMapping = [
|
||||
'borne-de-recharge' => 'charging_station',
|
||||
'defibrillator' => 'defibrillator',
|
||||
// Ajouter d'autres mappings si nécessaire
|
||||
];
|
||||
|
||||
return $themeMapping[$theme] ?? $theme;
|
||||
}
|
||||
}
|
|
@ -1,203 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\Demande;
|
||||
use App\Entity\Place;
|
||||
use App\Repository\DemandeRepository;
|
||||
use App\Repository\PlaceRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:link-demandes-places',
|
||||
description: 'Link Demandes to Places based on name similarity',
|
||||
)]
|
||||
class LinkDemandesPlacesCommand extends Command
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
private DemandeRepository $demandeRepository;
|
||||
private PlaceRepository $placeRepository;
|
||||
|
||||
public function __construct(
|
||||
EntityManagerInterface $entityManager,
|
||||
DemandeRepository $demandeRepository,
|
||||
PlaceRepository $placeRepository
|
||||
) {
|
||||
parent::__construct();
|
||||
$this->entityManager = $entityManager;
|
||||
$this->demandeRepository = $demandeRepository;
|
||||
$this->placeRepository = $placeRepository;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addOption('threshold', null, InputOption::VALUE_REQUIRED, 'Similarity threshold (0-100)', 70)
|
||||
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Show matches without linking');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->title('Linking Demandes to Places based on name similarity');
|
||||
|
||||
$threshold = (int) $input->getOption('threshold');
|
||||
$dryRun = $input->getOption('dry-run');
|
||||
|
||||
if ($threshold < 0 || $threshold > 100) {
|
||||
$io->error('Threshold must be between 0 and 100');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
// Find all Demandes without linked Places
|
||||
$demandesWithoutPlace = $this->demandeRepository->createQueryBuilder('d')
|
||||
->where('d.place IS NULL')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
|
||||
if (empty($demandesWithoutPlace)) {
|
||||
$io->warning('No Demandes without linked Places found.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$io->info(sprintf('Found %d Demandes without linked Places.', count($demandesWithoutPlace)));
|
||||
|
||||
// Process each Demande
|
||||
$linkedCount = 0;
|
||||
/** @var Demande $demande */
|
||||
foreach ($demandesWithoutPlace as $demande) {
|
||||
$query = $demande->getQuery();
|
||||
$insee = $demande->getInsee();
|
||||
|
||||
if (empty($query)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find Places with similar names
|
||||
$places = $this->findSimilarPlaces($query, $insee);
|
||||
if (empty($places)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the best match
|
||||
$bestMatch = null;
|
||||
$bestSimilarity = 0;
|
||||
foreach ($places as $place) {
|
||||
$similarity = $this->calculateSimilarity($query, $place->getName());
|
||||
if ($similarity > $bestSimilarity) {
|
||||
$bestSimilarity = $similarity;
|
||||
$bestMatch = $place;
|
||||
}
|
||||
}
|
||||
|
||||
// If similarity is above threshold, link the Demande to the Place
|
||||
if ($bestMatch && $bestSimilarity >= $threshold) {
|
||||
$io->text(sprintf(
|
||||
'Match found: "%s" (Demande) -> "%s" (Place) with similarity %d%%',
|
||||
$query,
|
||||
$bestMatch->getName(),
|
||||
$bestSimilarity
|
||||
));
|
||||
|
||||
if (!$dryRun) {
|
||||
$demande->setPlace($bestMatch);
|
||||
$demande->setPlaceUuid($bestMatch->getUuidForUrl());
|
||||
$demande->setStatus('linked_to_place');
|
||||
$this->entityManager->persist($demande);
|
||||
$linkedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$dryRun && $linkedCount > 0) {
|
||||
$this->entityManager->flush();
|
||||
$io->success(sprintf('Linked %d Demandes to Places.', $linkedCount));
|
||||
} elseif ($dryRun) {
|
||||
$io->info('Dry run completed. No changes were made.');
|
||||
} else {
|
||||
$io->info('No Demandes were linked to Places.');
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find Places with names similar to the query
|
||||
*/
|
||||
private function findSimilarPlaces(string $query, ?int $insee): array
|
||||
{
|
||||
$queryBuilder = $this->placeRepository->createQueryBuilder('p')
|
||||
->where('p.name IS NOT NULL');
|
||||
|
||||
// If INSEE code is available, filter by Stats zone
|
||||
if ($insee !== null) {
|
||||
$queryBuilder
|
||||
->join('p.stats', 's')
|
||||
->andWhere('s.zone = :insee')
|
||||
->setParameter('insee', (string) $insee);
|
||||
}
|
||||
|
||||
// Use LIKE for initial filtering to reduce the number of results
|
||||
$queryBuilder
|
||||
->andWhere('p.name LIKE :query')
|
||||
->setParameter('query', '%' . $this->sanitizeForLike($query) . '%');
|
||||
|
||||
return $queryBuilder->getQuery()->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate similarity between two strings (0-100)
|
||||
*/
|
||||
private function calculateSimilarity(string $str1, string $str2): int
|
||||
{
|
||||
// Normalize strings for comparison
|
||||
$str1 = $this->normalizeString($str1);
|
||||
$str2 = $this->normalizeString($str2);
|
||||
|
||||
// If either string is empty after normalization, return 0
|
||||
if (empty($str1) || empty($str2)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Calculate Levenshtein distance
|
||||
$levenshtein = levenshtein($str1, $str2);
|
||||
$maxLength = max(strlen($str1), strlen($str2));
|
||||
|
||||
// Convert to similarity percentage (0-100)
|
||||
$similarity = (1 - $levenshtein / $maxLength) * 100;
|
||||
|
||||
return (int) $similarity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a string for comparison
|
||||
*/
|
||||
private function normalizeString(string $str): string
|
||||
{
|
||||
// Convert to lowercase
|
||||
$str = mb_strtolower($str);
|
||||
|
||||
// Remove accents
|
||||
$str = transliterator_transliterate('Any-Latin; Latin-ASCII', $str);
|
||||
|
||||
// Remove special characters and extra spaces
|
||||
$str = preg_replace('/[^a-z0-9\s]/', '', $str);
|
||||
$str = preg_replace('/\s+/', ' ', $str);
|
||||
|
||||
return trim($str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a string for use in LIKE queries
|
||||
*/
|
||||
private function sanitizeForLike(string $str): string
|
||||
{
|
||||
return str_replace(['%', '_'], ['\%', '\_'], $str);
|
||||
}
|
||||
}
|
|
@ -1,116 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\Demande;
|
||||
use App\Entity\Place;
|
||||
use App\Repository\DemandeRepository;
|
||||
use App\Repository\PlaceRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:link-demandes-places-osm',
|
||||
description: 'Link Demandes to Places based on matching OSM type and ID',
|
||||
)]
|
||||
class LinkDemandesPlacesOsmCommand extends Command
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
private DemandeRepository $demandeRepository;
|
||||
private PlaceRepository $placeRepository;
|
||||
|
||||
public function __construct(
|
||||
EntityManagerInterface $entityManager,
|
||||
DemandeRepository $demandeRepository,
|
||||
PlaceRepository $placeRepository
|
||||
) {
|
||||
parent::__construct();
|
||||
$this->entityManager = $entityManager;
|
||||
$this->demandeRepository = $demandeRepository;
|
||||
$this->placeRepository = $placeRepository;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Show matches without linking');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->title('Linking Demandes to Places based on matching OSM type and ID');
|
||||
|
||||
$dryRun = $input->getOption('dry-run');
|
||||
|
||||
// Find all Demandes without a UUID but with OSM type and ID
|
||||
$demandesWithoutUuid = $this->demandeRepository->createQueryBuilder('d')
|
||||
->where('d.placeUuid IS NULL')
|
||||
->andWhere('d.osmObjectType IS NOT NULL')
|
||||
->andWhere('d.osmId IS NOT NULL')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
|
||||
if (empty($demandesWithoutUuid)) {
|
||||
$io->warning('No Demandes without UUID but with OSM type and ID found.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$io->info(sprintf('Found %d Demandes without UUID but with OSM type and ID.', count($demandesWithoutUuid)));
|
||||
|
||||
// Process each Demande
|
||||
$linkedCount = 0;
|
||||
/** @var Demande $demande */
|
||||
foreach ($demandesWithoutUuid as $demande) {
|
||||
$osmType = $demande->getOsmObjectType();
|
||||
$osmId = $demande->getOsmId();
|
||||
|
||||
// Find Place with matching OSM type and ID
|
||||
$place = $this->placeRepository->findOneBy([
|
||||
'osm_kind' => $osmType,
|
||||
'osmId' => $osmId
|
||||
]);
|
||||
|
||||
if ($place) {
|
||||
$io->text(sprintf(
|
||||
'Match found: Demande #%d -> Place #%d (OSM %s/%d)',
|
||||
$demande->getId(),
|
||||
$place->getId(),
|
||||
$osmType,
|
||||
$osmId
|
||||
));
|
||||
|
||||
if (!$dryRun) {
|
||||
$demande->setPlace($place);
|
||||
$demande->setPlaceUuid($place->getUuidForUrl());
|
||||
$demande->setStatus('linked_to_place');
|
||||
$this->entityManager->persist($demande);
|
||||
$linkedCount++;
|
||||
}
|
||||
} else {
|
||||
$io->text(sprintf(
|
||||
'No matching Place found for Demande #%d (OSM %s/%d)',
|
||||
$demande->getId(),
|
||||
$osmType,
|
||||
$osmId
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if (!$dryRun && $linkedCount > 0) {
|
||||
$this->entityManager->flush();
|
||||
$io->success(sprintf('Linked %d Demandes to Places based on OSM type and ID.', $linkedCount));
|
||||
} elseif ($dryRun) {
|
||||
$io->info('Dry run completed. No changes were made.');
|
||||
} else {
|
||||
$io->info('No Demandes were linked to Places.');
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
|
@ -1,149 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\Stats;
|
||||
use App\Service\Motocultrice;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:process-insee-extracts',
|
||||
description: 'Traite les extraits JSON des zones INSEE pour calculer les mesures de thèmes',
|
||||
)]
|
||||
class ProcessInseeExtractsCommand extends Command
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
private Motocultrice $motocultrice;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager, Motocultrice $motocultrice)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->entityManager = $entityManager;
|
||||
$this->motocultrice = $motocultrice;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addArgument('insee-code', InputArgument::OPTIONAL, 'Code INSEE spécifique à traiter')
|
||||
->addOption('limit', 'l', InputOption::VALUE_REQUIRED, 'Limite le nombre de villes à traiter', null)
|
||||
->addOption('force', 'f', InputOption::VALUE_NONE, 'Force le traitement même si déjà effectué')
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$inseeCode = $input->getArgument('insee-code');
|
||||
$limit = $input->getOption('limit');
|
||||
$force = $input->getOption('force');
|
||||
|
||||
// Vérifier que le dossier des extractions existe
|
||||
$extractsDir = __DIR__ . '/../../insee_extracts';
|
||||
if (!is_dir($extractsDir)) {
|
||||
$io->error('Le dossier des extractions n\'existe pas. Veuillez d\'abord exécuter la commande app:extract-insee-zones.');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
// Récupérer les Stats à traiter
|
||||
$statsRepo = $this->entityManager->getRepository(Stats::class);
|
||||
|
||||
if ($inseeCode) {
|
||||
$io->note(sprintf('Traitement du code INSEE spécifique: %s', $inseeCode));
|
||||
$allStats = $statsRepo->findBy(['zone' => $inseeCode]);
|
||||
|
||||
if (empty($allStats)) {
|
||||
$io->error(sprintf('Aucune ville trouvée avec le code INSEE %s', $inseeCode));
|
||||
return Command::FAILURE;
|
||||
}
|
||||
} else {
|
||||
$io->note('Traitement de toutes les villes');
|
||||
$criteria = [];
|
||||
$orderBy = ['id' => 'ASC'];
|
||||
$limitValue = $limit ? (int)$limit : null;
|
||||
|
||||
$allStats = $statsRepo->findBy($criteria, $orderBy, $limitValue);
|
||||
}
|
||||
|
||||
$totalCount = count($allStats);
|
||||
$processedCount = 0;
|
||||
$skippedCount = 0;
|
||||
$errorCount = 0;
|
||||
|
||||
$io->progressStart($totalCount);
|
||||
|
||||
// Pour chaque Stats, traiter les données si nécessaire
|
||||
foreach ($allStats as $stat) {
|
||||
$inseeCode = $stat->getZone();
|
||||
if (!$inseeCode) {
|
||||
$io->progressAdvance();
|
||||
continue;
|
||||
}
|
||||
|
||||
$extractJsonFile = $extractsDir . '/commune_' . $inseeCode . '.json';
|
||||
|
||||
// Vérifier si l'extraction JSON existe
|
||||
if (!file_exists($extractJsonFile)) {
|
||||
$io->debug(sprintf('Fichier JSON manquant pour %s', $inseeCode));
|
||||
$errorCount++;
|
||||
$io->progressAdvance();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Vérifier si le traitement a déjà été effectué
|
||||
if (!$force && $stat->getDateLabourageDone() !== null) {
|
||||
$skippedCount++;
|
||||
$io->progressAdvance();
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// Utiliser la Motocultrice pour traiter les données
|
||||
$result = $this->motocultrice->labourer($inseeCode);
|
||||
|
||||
if ($result) {
|
||||
// Mettre à jour la date de labourage
|
||||
$stat->setDateLabourageDone(new \DateTime());
|
||||
$this->entityManager->persist($stat);
|
||||
$processedCount++;
|
||||
|
||||
// Flush tous les 10 objets pour éviter de surcharger la mémoire
|
||||
if ($processedCount % 10 === 0) {
|
||||
$this->entityManager->flush();
|
||||
$io->debug(sprintf('Flush après %d traitements', $processedCount));
|
||||
}
|
||||
} else {
|
||||
$io->debug(sprintf('Échec du traitement pour %s', $inseeCode));
|
||||
$errorCount++;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$errorCount++;
|
||||
$io->debug(sprintf('Exception pour %s: %s', $inseeCode, $e->getMessage()));
|
||||
}
|
||||
|
||||
$io->progressAdvance();
|
||||
}
|
||||
|
||||
// Flush les derniers objets
|
||||
$this->entityManager->flush();
|
||||
|
||||
$io->progressFinish();
|
||||
|
||||
$io->success(sprintf(
|
||||
"Traitement des extractions INSEE terminé : %d communes traitées, %d ignorées, %d erreurs sur un total de %d communes.",
|
||||
$processedCount,
|
||||
$skippedCount,
|
||||
$errorCount,
|
||||
$totalCount
|
||||
));
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
|
@ -1,179 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\Place;
|
||||
use App\Entity\Stats;
|
||||
use App\Service\FollowUpService;
|
||||
use App\Service\Motocultrice;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:process-labourage-queue',
|
||||
description: 'Traite la file d\'attente de labourage différé des villes (cron)'
|
||||
)]
|
||||
class ProcessLabourageQueueCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManagerInterface $entityManager,
|
||||
private Motocultrice $motocultrice,
|
||||
private FollowUpService $followUpService
|
||||
)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
// Sélectionner la Stats à traiter (date_labourage_requested la plus ancienne, non traitée ou à refaire)
|
||||
$stats = $this->entityManager->getRepository(Stats::class)
|
||||
->createQueryBuilder('s')
|
||||
->where('s.date_labourage_requested IS NOT NULL')
|
||||
->andWhere('s.date_labourage_done IS NULL OR s.date_labourage_done < s.date_labourage_requested')
|
||||
->andWhere('s.zone != :global_zone')
|
||||
->setParameter('global_zone', '00000')
|
||||
->orderBy('s.date_labourage_requested', 'ASC')
|
||||
->setMaxResults(1)
|
||||
->getQuery()
|
||||
->getOneOrNullResult();
|
||||
if (!$stats) {
|
||||
// 1. Villes jamais labourées (date_labourage_done NULL, hors 00000)
|
||||
$stats = $this->entityManager->getRepository(Stats::class)
|
||||
->createQueryBuilder('s')
|
||||
->where('s.zone != :global_zone')
|
||||
->andWhere('s.date_labourage_done IS NULL')
|
||||
->setParameter('global_zone', '00000')
|
||||
->orderBy('s.date_modified', 'ASC')
|
||||
->setMaxResults(1)
|
||||
->getQuery()
|
||||
->getOneOrNullResult();
|
||||
if ($stats) {
|
||||
$io->note('Aucune ville en attente, on traite en priorité une ville jamais labourée : ' . $stats->getName() . ' (' . $stats->getZone() . ')');
|
||||
$stats->setDateLabourageRequested(new \DateTime());
|
||||
$this->entityManager->persist($stats);
|
||||
$this->entityManager->flush();
|
||||
} else {
|
||||
// 2. Ville la plus anciennement modifiée (hors 00000)
|
||||
$stats = $this->entityManager->getRepository(Stats::class)
|
||||
->createQueryBuilder('s')
|
||||
->where('s.zone != :global_zone')
|
||||
->setParameter('global_zone', '00000')
|
||||
->orderBy('s.date_modified', 'ASC')
|
||||
->setMaxResults(1)
|
||||
->getQuery()
|
||||
->getOneOrNullResult();
|
||||
if (!$stats) {
|
||||
$io->success('Aucune ville à traiter.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
$io->note('Aucune ville en attente, on demande le labourage de la ville la plus anciennement modifiée : ' . $stats->getName() . ' (' . $stats->getZone() . ')');
|
||||
$stats->setDateLabourageRequested(new \DateTime());
|
||||
$this->entityManager->persist($stats);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
}
|
||||
$io->section('Traitement de la ville : ' . $stats->getName() . ' (' . $stats->getZone() . ')');
|
||||
// Vérifier la RAM disponible (>= 1 Go)
|
||||
$meminfo = @file_get_contents('/proc/meminfo');
|
||||
$ram_ok = false;
|
||||
if ($meminfo !== false && preg_match('/^MemAvailable:\s+(\d+)/m', $meminfo, $matches)) {
|
||||
$mem_kb = (int)$matches[1];
|
||||
$ram_ok = ($mem_kb >= 1024 * 1024); // 1 Go
|
||||
}
|
||||
if (!$ram_ok) {
|
||||
$io->warning('RAM insuffisante, on attend le prochain cron.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
// Effectuer le labourage complet (reprendre la logique de création/màj des objets Place)
|
||||
$io->info('RAM suffisante, lancement du labourage...');
|
||||
$places_overpass = $this->motocultrice->labourer($stats->getZone());
|
||||
$processedCount = 0;
|
||||
$updatedCount = 0;
|
||||
$existingPlacesQuery = $this->entityManager->getRepository(Place::class)
|
||||
->createQueryBuilder('p')
|
||||
->select('p.osmId, p.osm_kind, p.id')
|
||||
->where('p.zip_code = :zip_code')
|
||||
->setParameter('zip_code', $stats->getZone())
|
||||
->getQuery();
|
||||
$existingPlacesResult = $existingPlacesQuery->getResult();
|
||||
$placesByOsmKey = [];
|
||||
foreach ($existingPlacesResult as $placeData) {
|
||||
$osmKey = $placeData['osm_kind'] . '_' . $placeData['osmId'];
|
||||
$placesByOsmKey[$osmKey] = $placeData['id'];
|
||||
}
|
||||
foreach ($places_overpass as $placeData) {
|
||||
$osmKey = $placeData['type'] . '_' . $placeData['id'];
|
||||
$existingPlaceId = $placesByOsmKey[$osmKey] ?? null;
|
||||
if (!$existingPlaceId) {
|
||||
$place = new Place();
|
||||
$place->setOsmId($placeData['id'])
|
||||
->setOsmKind($placeData['type'])
|
||||
->setZipCode($stats->getZone())
|
||||
->setUuidForUrl($this->motocultrice->uuid_create())
|
||||
->setModifiedDate(new \DateTime())
|
||||
->setStats($stats)
|
||||
->setDead(false)
|
||||
->setOptedOut(false)
|
||||
->setMainTag($this->motocultrice->find_main_tag($placeData['tags']) ?? '')
|
||||
->setStreet($this->motocultrice->find_street($placeData['tags']) ?? '')
|
||||
->setHousenumber($this->motocultrice->find_housenumber($placeData['tags']) ?? '')
|
||||
->setSiret($this->motocultrice->find_siret($placeData['tags']) ?? '')
|
||||
->setAskedHumainsSupport(false)
|
||||
->setLastContactAttemptDate(null)
|
||||
->setPlaceCount(0);
|
||||
$place->update_place_from_overpass_data($placeData);
|
||||
$this->entityManager->persist($place);
|
||||
$stats->addPlace($place);
|
||||
$processedCount++;
|
||||
} else {
|
||||
$existingPlace = $this->entityManager->getRepository(Place::class)->find($existingPlaceId);
|
||||
if ($existingPlace) {
|
||||
$existingPlace->setDead(false);
|
||||
$existingPlace->update_place_from_overpass_data($placeData);
|
||||
$stats->addPlace($existingPlace);
|
||||
$this->entityManager->persist($existingPlace);
|
||||
$updatedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
$stats->setDateLabourageDone(new \DateTime());
|
||||
|
||||
// Update city name from API if available
|
||||
$apiCityName = $this->motocultrice->get_city_osm_from_zip_code($stats->getZone());
|
||||
if ($apiCityName && $apiCityName !== $stats->getName()) {
|
||||
$io->info(sprintf('Updating city name from "%s" to "%s" based on API data', $stats->getName(), $apiCityName));
|
||||
$stats->setName($apiCityName);
|
||||
}
|
||||
|
||||
$io->info('Récupération des followups de cette ville...');
|
||||
// $this->followUpService->generateCityFollowUps($stats, $this->motocultrice, $this->entityManager);
|
||||
|
||||
|
||||
// update completion
|
||||
$stats->computeCompletionPercent();
|
||||
$followups = $stats->getCityFollowUps();
|
||||
if ($followups) {
|
||||
|
||||
$lastFollowUp = $followups[count($followups) - 1];
|
||||
if ($lastFollowUp) {
|
||||
|
||||
$name = $lastFollowUp->getName();
|
||||
|
||||
$io->success("Followup le plus récent : $name : " . $lastFollowUp->getDate()->format('d/m/Y') . ' : ' . $lastFollowUp->getMeasure());
|
||||
}
|
||||
}
|
||||
$io->info('Pourcentage de complétion: ' . $stats->getCompletionPercent());
|
||||
|
||||
|
||||
$this->entityManager->persist($stats);
|
||||
$this->entityManager->flush();
|
||||
$io->success("Labourage terminé : $processedCount nouveaux lieux, $updatedCount lieux mis à jour.");
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\Stats;
|
||||
use App\Service\FollowUpService;
|
||||
use App\Service\Motocultrice;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:regenerate-followups',
|
||||
description: 'Régénère les followups pour une ville spécifique avec les nouveaux critères',
|
||||
)]
|
||||
class RegenerateFollowupsCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManagerInterface $entityManager,
|
||||
private FollowUpService $followUpService,
|
||||
private Motocultrice $motocultrice
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addArgument('insee_code', InputArgument::REQUIRED, 'Code INSEE de la ville')
|
||||
->addOption('delete-existing', 'd', InputOption::VALUE_NONE, 'Supprimer les followups existants avant de régénérer')
|
||||
->addOption('disable-cleanup', 'c', InputOption::VALUE_NONE, 'Désactiver le nettoyage des followups redondants')
|
||||
->setHelp('Cette commande régénère les followups pour une ville avec les nouveaux critères de completion.')
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$inseeCode = $input->getArgument('insee_code');
|
||||
$deleteExisting = $input->getOption('delete-existing');
|
||||
$disableCleanup = $input->getOption('disable-cleanup');
|
||||
|
||||
$io->title('Régénération des followups pour ' . $inseeCode);
|
||||
|
||||
// Vérifier que la ville existe
|
||||
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $inseeCode]);
|
||||
if (!$stats) {
|
||||
$io->error('Aucune stats trouvée pour le code INSEE ' . $inseeCode);
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$io->info('Ville trouvée : ' . $stats->getName());
|
||||
|
||||
// Supprimer les followups existants si demandé
|
||||
if ($deleteExisting) {
|
||||
$io->section('Suppression des followups existants');
|
||||
$followups = $stats->getCityFollowUps();
|
||||
$count = count($followups);
|
||||
|
||||
foreach ($followups as $followup) {
|
||||
$this->entityManager->remove($followup);
|
||||
}
|
||||
$this->entityManager->flush();
|
||||
|
||||
$io->success($count . ' followups supprimés');
|
||||
}
|
||||
|
||||
// Régénérer les followups
|
||||
$io->section('Régénération des followups');
|
||||
$io->note('Utilisation des nouveaux critères de completion plus réalistes');
|
||||
|
||||
try {
|
||||
$this->followUpService->generateCityFollowUps(
|
||||
$stats,
|
||||
$this->motocultrice,
|
||||
$this->entityManager,
|
||||
$disableCleanup
|
||||
);
|
||||
|
||||
$io->success('Followups régénérés avec succès');
|
||||
|
||||
// Afficher les résultats
|
||||
$newFollowups = $stats->getCityFollowUps();
|
||||
$io->section('Résultats');
|
||||
|
||||
$table = [];
|
||||
foreach ($newFollowups as $followup) {
|
||||
$table[] = [
|
||||
$followup->getName(),
|
||||
$followup->getMeasure(),
|
||||
$followup->getDate()->format('Y-m-d H:i:s')
|
||||
];
|
||||
}
|
||||
|
||||
$io->table(['Nom', 'Valeur', 'Date'], $table);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$io->error('Erreur lors de la régénération : ' . $e->getMessage());
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
|
@ -1,123 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\Place;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:remove-duplicate-places',
|
||||
description: 'Remove duplicate Places based on OSM type and ID',
|
||||
)]
|
||||
class RemoveDuplicatePlacesCommand extends Command
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
public function __construct(
|
||||
EntityManagerInterface $entityManager
|
||||
) {
|
||||
parent::__construct();
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Show duplicates without removing them')
|
||||
->addOption('zip-code', null, InputOption::VALUE_REQUIRED, 'Process only places with this ZIP code');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->title('Removing duplicate Places based on OSM type and ID');
|
||||
|
||||
$dryRun = $input->getOption('dry-run');
|
||||
$zipCode = $input->getOption('zip-code');
|
||||
|
||||
// Build the query to get all places sorted by OSM type and ID
|
||||
$queryBuilder = $this->entityManager->createQueryBuilder()
|
||||
->select('p')
|
||||
->from(Place::class, 'p')
|
||||
->orderBy('p.osm_kind', 'ASC')
|
||||
->addOrderBy('p.osmId', 'ASC');
|
||||
|
||||
// Add ZIP code filter if provided
|
||||
if ($zipCode) {
|
||||
$queryBuilder->andWhere('p.zip_code = :zip_code')
|
||||
->setParameter('zip_code', $zipCode);
|
||||
$io->info(sprintf('Processing only places with ZIP code: %s', $zipCode));
|
||||
}
|
||||
|
||||
$places = $queryBuilder->getQuery()->getResult();
|
||||
|
||||
if (empty($places)) {
|
||||
$io->warning('No places found.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$io->info(sprintf('Found %d places.', count($places)));
|
||||
|
||||
// Find duplicates by comparing consecutive places
|
||||
$duplicates = [];
|
||||
$previousPlace = null;
|
||||
$duplicateCount = 0;
|
||||
|
||||
foreach ($places as $place) {
|
||||
if ($previousPlace !== null &&
|
||||
$previousPlace->getOsmKind() === $place->getOsmKind() &&
|
||||
$previousPlace->getOsmId() === $place->getOsmId()) {
|
||||
// This is a duplicate
|
||||
$duplicates[] = [
|
||||
'keep' => $previousPlace,
|
||||
'remove' => $place
|
||||
];
|
||||
$duplicateCount++;
|
||||
}
|
||||
$previousPlace = $place;
|
||||
}
|
||||
|
||||
if (empty($duplicates)) {
|
||||
$io->success('No duplicate places found.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$io->info(sprintf('Found %d duplicate places.', $duplicateCount));
|
||||
|
||||
// Process duplicates
|
||||
$removedCount = 0;
|
||||
foreach ($duplicates as $duplicate) {
|
||||
$keep = $duplicate['keep'];
|
||||
$remove = $duplicate['remove'];
|
||||
|
||||
$io->text(sprintf(
|
||||
'Duplicate found: Keep #%d, Remove #%d (OSM %s/%s)',
|
||||
$keep->getId(),
|
||||
$remove->getId(),
|
||||
$keep->getOsmKind(),
|
||||
$keep->getOsmId()
|
||||
));
|
||||
|
||||
if (!$dryRun) {
|
||||
// Remove the duplicate
|
||||
$this->entityManager->remove($remove);
|
||||
$removedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$dryRun && $removedCount > 0) {
|
||||
$this->entityManager->flush();
|
||||
$io->success(sprintf('Removed %d duplicate places.', $removedCount));
|
||||
} elseif ($dryRun) {
|
||||
$io->info('Dry run completed. No places were removed.');
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
|
@ -1,131 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\Stats;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:retrieve-city-polygons',
|
||||
description: 'Récupère les polygones des villes selon leur zone donnée par le code INSEE',
|
||||
)]
|
||||
class RetrieveCityPolygonsCommand extends Command
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addArgument('insee-code', InputArgument::OPTIONAL, 'Code INSEE spécifique à traiter')
|
||||
->addOption('limit', 'l', InputOption::VALUE_REQUIRED, 'Limite le nombre de villes à traiter', null)
|
||||
->addOption('force', 'f', InputOption::VALUE_NONE, 'Force la récupération même si le polygone existe déjà')
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$inseeCode = $input->getArgument('insee-code');
|
||||
$limit = $input->getOption('limit');
|
||||
$force = $input->getOption('force');
|
||||
|
||||
// Vérifier que le dossier polygons existe, sinon le créer
|
||||
$polygonsDir = __DIR__ . '/../../counting_osm_objects/polygons';
|
||||
if (!is_dir($polygonsDir)) {
|
||||
$io->note('Création du dossier polygons');
|
||||
mkdir($polygonsDir, 0755, true);
|
||||
}
|
||||
|
||||
// Récupérer les Stats à traiter
|
||||
$statsRepo = $this->entityManager->getRepository(Stats::class);
|
||||
|
||||
if ($inseeCode) {
|
||||
$io->note(sprintf('Traitement du code INSEE spécifique: %s', $inseeCode));
|
||||
$allStats = $statsRepo->findBy(['zone' => $inseeCode]);
|
||||
|
||||
if (empty($allStats)) {
|
||||
$io->error(sprintf('Aucune ville trouvée avec le code INSEE %s', $inseeCode));
|
||||
return Command::FAILURE;
|
||||
}
|
||||
} else {
|
||||
$io->note('Traitement de toutes les villes');
|
||||
$criteria = [];
|
||||
$orderBy = ['id' => 'ASC'];
|
||||
$limitValue = $limit ? (int)$limit : null;
|
||||
|
||||
$allStats = $statsRepo->findBy($criteria, $orderBy, $limitValue);
|
||||
}
|
||||
|
||||
$totalCount = count($allStats);
|
||||
$existingCount = 0;
|
||||
$createdCount = 0;
|
||||
$errorCount = 0;
|
||||
|
||||
$io->progressStart($totalCount);
|
||||
|
||||
// Pour chaque Stats, récupérer le polygone si nécessaire
|
||||
foreach ($allStats as $stat) {
|
||||
$inseeCode = $stat->getZone();
|
||||
if (!$inseeCode) {
|
||||
$io->progressAdvance();
|
||||
continue;
|
||||
}
|
||||
|
||||
$polygonFile = $polygonsDir . '/commune_' . $inseeCode . '.poly';
|
||||
|
||||
// Vérifier si le polygone existe déjà
|
||||
if (file_exists($polygonFile) && !$force) {
|
||||
$existingCount++;
|
||||
$io->progressAdvance();
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// Utiliser le script Python existant pour récupérer le polygone
|
||||
$command = 'cd ' . __DIR__ . '/../../counting_osm_objects && python3 get_poly.py ' . $inseeCode;
|
||||
$output = [];
|
||||
$returnVar = 0;
|
||||
exec($command, $output, $returnVar);
|
||||
|
||||
if ($returnVar === 0 && file_exists($polygonFile)) {
|
||||
$createdCount++;
|
||||
} else {
|
||||
$errorCount++;
|
||||
if ($output) {
|
||||
$io->debug(sprintf('Erreur pour %s: %s', $inseeCode, implode("\n", $output)));
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$errorCount++;
|
||||
$io->debug(sprintf('Exception pour %s: %s', $inseeCode, $e->getMessage()));
|
||||
}
|
||||
|
||||
$io->progressAdvance();
|
||||
}
|
||||
|
||||
$io->progressFinish();
|
||||
|
||||
$io->success(sprintf(
|
||||
"Récupération des polygones terminée : %d polygones créés, %d déjà existants, %d erreurs sur un total de %d communes.",
|
||||
$createdCount,
|
||||
$existingCount,
|
||||
$errorCount,
|
||||
$totalCount
|
||||
));
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use App\Service\BudgetService;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:test-budget',
|
||||
description: 'Test du service BudgetService',
|
||||
)]
|
||||
class TestBudgetCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private BudgetService $budgetService
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
// Test avec un code INSEE
|
||||
$codeInsee = '37261'; // Tours
|
||||
$io->info("Test du budget pour le code INSEE: $codeInsee");
|
||||
|
||||
try {
|
||||
$budget = $this->budgetService->getBudgetAnnuel($codeInsee);
|
||||
if ($budget !== null) {
|
||||
$io->success("Budget annuel récupéré: " . number_format($budget, 2) . " €");
|
||||
|
||||
// Test avec une population fictive
|
||||
$population = 138668; // Population de Tours
|
||||
$budgetParHabitant = $this->budgetService->getBudgetParHabitant($budget, $population);
|
||||
$io->info("Budget par habitant: " . number_format($budgetParHabitant, 2) . " €");
|
||||
|
||||
// Test écart à la moyenne
|
||||
$moyenne = 1500; // Moyenne fictive
|
||||
$ecart = $this->budgetService->getEcartMoyenneBudgetParHabitant($budgetParHabitant, $moyenne);
|
||||
$io->info("Écart à la moyenne: " . number_format($ecart, 2) . "%");
|
||||
} else {
|
||||
$io->warning("Aucun budget récupéré");
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$io->error("Erreur: " . $e->getMessage());
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue