├── .babelrc ├── .gitignore ├── .travis.yml ├── COPYING ├── Makefile ├── README.md ├── appinfo ├── app.php ├── application.php ├── database.xml ├── info.xml └── routes.php ├── configure.js ├── css ├── Leaflet.Photo.css ├── MarkerCluster.Default.css ├── MarkerCluster.css ├── app.less ├── images │ ├── gallery.svg │ ├── layers-2x.png │ ├── layers.png │ ├── marker-icon-2x.png │ ├── marker-icon.png │ └── marker-shadow.png ├── leaflet.css ├── map.css └── style.css ├── img └── app.svg ├── js ├── Leaflet.Photo.js ├── leaflet.js ├── leaflet.markercluster.js ├── leaflet.textpath.js └── main.js ├── js_src ├── AlbumInfosCache.js ├── AlbumsView.js ├── App.js ├── FoldersList.js ├── GeoPhotoHelper.js ├── MapView.js └── index.js ├── l10n ├── pl.js ├── pl.json └── pl_PL.php ├── lib ├── Command │ └── RescanPhotos.php ├── Controller │ ├── GeophotosController.php │ ├── PageController.php │ └── PhotofilesController.php ├── DB │ ├── Geophoto.php │ └── GeophotoMapper.php ├── Hook │ └── FileHooks.php └── Service │ ├── GeophotoService.php │ └── PhotofilesService.php ├── package.json ├── phpunit.integration.xml ├── phpunit.xml ├── screenshots ├── 1.png └── 2.png ├── templates ├── content │ └── index.php ├── index.php ├── navigation │ └── index.php └── settings │ └── index.php ├── tests ├── Integration │ └── AppTest.php ├── Unit │ └── Controller │ │ └── PageControllerTest.php └── bootstrap.php └── webpack ├── dev-proxy.js ├── dev.config.js ├── prod.config.js ├── utils ├── notifyStats.js └── writeStats.js └── webpack-dev-server.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "react" 5 | ], 6 | "plugins": [ 7 | "transform-class-properties", 8 | [ 9 | "transform-async-to-module-method", 10 | { 11 | "module": "bluebird", 12 | "method": "coroutine" 13 | } 14 | ] 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # production 5 | /build 6 | /.project 7 | js/main.js.map 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | language: php 4 | php: 5 | - 5.6 6 | - 7 7 | env: 8 | global: 9 | - CORE_BRANCH=stable11 10 | matrix: 11 | - DB=pgsql 12 | 13 | matrix: 14 | allow_failures: 15 | - env: DB=pgsql CORE_BRANCH=master 16 | include: 17 | - php: 5.6 18 | env: DB=sqlite 19 | - php: 5.6 20 | env: DB=mysql 21 | - php: 5.6 22 | env: DB=pgsql CORE_BRANCH=master 23 | fast_finish: true 24 | 25 | before_install: 26 | # enable a display for running JavaScript tests 27 | - export DISPLAY=:99.0 28 | - sh -e /etc/init.d/xvfb start 29 | - if [[ "$DB" == 'mysql' ]]; then sudo apt-get -y install mariadb-server; fi 30 | - nvm install 6 31 | - npm install -g npm@latest 32 | - make 33 | - make appstore 34 | # install core 35 | - cd ../ 36 | - git clone https://github.com/nextcloud/server.git --recursive --depth 1 -b $CORE_BRANCH nextcloud 37 | - mv photomap nextcloud/apps/ 38 | 39 | before_script: 40 | - if [[ "$DB" == 'pgsql' ]]; then createuser -U travis -s oc_autotest; fi 41 | - if [[ "$DB" == 'mysql' ]]; then mysql -u root -e 'create database oc_autotest;'; fi 42 | - if [[ "$DB" == 'mysql' ]]; then mysql -u root -e "CREATE USER 'oc_autotest'@'localhost' IDENTIFIED BY '';"; fi 43 | - if [[ "$DB" == 'mysql' ]]; then mysql -u root -e "grant all on oc_autotest.* to 'oc_autotest'@'localhost';"; fi 44 | - cd nextcloud 45 | - mkdir data 46 | - ./occ maintenance:install --database-name oc_autotest --database-user oc_autotest --admin-user admin --admin-pass admin --database $DB --database-pass='' 47 | - ./occ app:enable photomap 48 | - php -S localhost:8080 & 49 | - cd apps/photomap 50 | 51 | script: 52 | - make test 53 | 54 | after_failure: 55 | - cat ../../data/nextcloud.log 56 | 57 | addons: 58 | firefox: "latest" 59 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published by 637 | the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # This file is licensed under the Affero General Public License version 3 or 2 | # later. See the COPYING file. 3 | # @author Bernhard Posselt 4 | # @copyright Bernhard Posselt 2016 5 | 6 | # Generic Makefile for building and packaging a Nextcloud app which uses npm and 7 | # Composer. 8 | # 9 | # Dependencies: 10 | # * make 11 | # * which 12 | # * curl: used if phpunit and composer are not installed to fetch them from the web 13 | # * tar: for building the archive 14 | # * npm: for building and testing everything JS 15 | # 16 | # If no composer.json is in the app root directory, the Composer step 17 | # will be skipped. The same goes for the package.json which can be located in 18 | # the app root or the js/ directory. 19 | # 20 | # The npm command by launches the npm build script: 21 | # 22 | # npm run build 23 | # 24 | # The npm test command launches the npm test script: 25 | # 26 | # npm run test 27 | # 28 | # The idea behind this is to be completely testing and build tool agnostic. All 29 | # build tools and additional package managers should be installed locally in 30 | # your project, since this won't pollute people's global namespace. 31 | # 32 | # The following npm scripts in your package.json install and update the bower 33 | # and npm dependencies and use gulp as build system (notice how everything is 34 | # run from the node_modules folder): 35 | # 36 | # "scripts": { 37 | # "test": "node node_modules/gulp-cli/bin/gulp.js karma", 38 | # "prebuild": "npm install && node_modules/bower/bin/bower install && node_modules/bower/bin/bower update", 39 | # "build": "node node_modules/gulp-cli/bin/gulp.js" 40 | # }, 41 | 42 | app_name=photomap 43 | project_dir=$(CURDIR)/../$(app_name) 44 | build_tools_directory=$(CURDIR)/build/tools 45 | source_build_directory=$(CURDIR)/build/artifacts/source 46 | source_package_name=$(source_build_directory)/$(app_name) 47 | appstore_build_directory=$(CURDIR)/build/artifacts/appstore 48 | appstore_package_name=$(appstore_build_directory)/$(app_name) 49 | npm=$(shell which npm 2> /dev/null) 50 | composer=$(shell which composer 2> /dev/null) 51 | 52 | all: build 53 | 54 | # Fetches the PHP and JS dependencies and compiles the JS. If no composer.json 55 | # is present, the composer step is skipped, if no package.json or js/package.json 56 | # is present, the npm step is skipped 57 | .PHONY: build 58 | build: 59 | ifneq (,$(wildcard $(CURDIR)/composer.json)) 60 | make composer 61 | endif 62 | ifneq (,$(wildcard $(CURDIR)/package.json)) 63 | make npm 64 | endif 65 | ifneq (,$(wildcard $(CURDIR)/js/package.json)) 66 | make npm 67 | endif 68 | 69 | # Installs and updates the composer dependencies. If composer is not installed 70 | # a copy is fetched from the web 71 | .PHONY: composer 72 | composer: 73 | ifeq (, $(composer)) 74 | @echo "No composer command available, downloading a copy from the web" 75 | mkdir -p $(build_tools_directory) 76 | curl -sS https://getcomposer.org/installer | php 77 | mv composer.phar $(build_tools_directory) 78 | php $(build_tools_directory)/composer.phar install --prefer-dist 79 | php $(build_tools_directory)/composer.phar update --prefer-dist 80 | else 81 | composer install --prefer-dist 82 | composer update --prefer-dist 83 | endif 84 | 85 | # Installs npm dependencies 86 | .PHONY: npm 87 | npm: 88 | ifeq (,$(wildcard $(CURDIR)/package.json)) 89 | cd js && $(npm) run build 90 | else 91 | npm run build 92 | endif 93 | 94 | # Removes the appstore build 95 | .PHONY: clean 96 | clean: 97 | rm -rf ./build 98 | 99 | # Same as clean but also removes dependencies installed by composer, bower and 100 | # npm 101 | .PHONY: distclean 102 | distclean: clean 103 | rm -rf vendor 104 | rm -rf node_modules 105 | rm -rf js/vendor 106 | rm -rf js/node_modules 107 | 108 | # Builds the source and appstore package 109 | .PHONY: dist 110 | dist: 111 | make source 112 | make appstore 113 | 114 | # Builds the source package 115 | .PHONY: source 116 | source: 117 | rm -rf $(source_build_directory) 118 | mkdir -p $(source_build_directory) 119 | tar --exclude-vcs \ 120 | --exclude=$(project_dir)/build \ 121 | --exclude=$(project_dir)/node_modules \ 122 | -cvzf $(source_package_name).tar.gz $(project_dir) 123 | 124 | # Builds the source package for the app store, ignores php and js tests 125 | .PHONY: appstore 126 | appstore: 127 | rm -rf $(appstore_build_directory) 128 | mkdir -p $(appstore_build_directory) 129 | tar --exclude-vcs \ 130 | --exclude=$(project_dir)/build \ 131 | --exclude=$(project_dir)/js_src \ 132 | --exclude=$(project_dir)/js/main.js.map \ 133 | --exclude=$(project_dir)/node_modules \ 134 | --exclude=$(project_dir)/webpack \ 135 | --exclude=$(project_dir)/.gitattributes \ 136 | --exclude=$(project_dir)/.gitignore \ 137 | --exclude=$(project_dir)/.travis.yml \ 138 | --exclude=$(project_dir)/.babelrc \ 139 | --exclude=$(project_dir)/.phpunit* \ 140 | --exclude=$(project_dir)/.scrutinizer.yml \ 141 | --exclude=$(project_dir)/CONTRIBUTING.md \ 142 | --exclude=$(project_dir)/package.json \ 143 | --exclude=$(project_dir)/configure.js \ 144 | --exclude=$(project_dir)/screenshots \ 145 | --exclude=$(project_dir)/Makefile \ 146 | -cvzf $(appstore_package_name).tar.gz $(project_dir) 147 | 148 | # Command for running JS and PHP tests. Works for package.json files in the js/ 149 | # and root directory. If phpunit is not installed systemwide, a copy is fetched 150 | # from the internet 151 | .PHONY: test 152 | test: 153 | ifneq (,$(wildcard $(CURDIR)/js/package.json)) 154 | cd js && $(npm) run test 155 | endif 156 | ifneq (,$(wildcard $(CURDIR)/package.json)) 157 | $(npm) run test 158 | endif 159 | ifeq (, $(shell which phpunit 2> /dev/null)) 160 | @echo "No phpunit command available, downloading a copy from the web" 161 | mkdir -p $(build_tools_directory) 162 | curl -sSL https://phar.phpunit.de/phpunit.phar -o $(build_tools_directory)/phpunit.phar 163 | php $(build_tools_directory)/phpunit.phar -c phpunit.xml 164 | php $(build_tools_directory)/phpunit.phar -c phpunit.integration.xml 165 | else 166 | phpunit -c phpunit.xml --coverage-clover build/php-unit.clover 167 | phpunit -c phpunit.integration.xml --coverage-clover build/php-unit.clover 168 | endif 169 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Photo Map 2 | 3 | Shows geotagged photos on map 4 | 5 | ![screenshot1](https://raw.githubusercontent.com/BatPio/PhotoMap/master/screenshots/1.png) 6 | 7 | ## Building the app 8 | 9 | The app can be built by using the provided Makefile by running: 10 | 11 | make 12 | 13 | This requires the following things to be present: 14 | * make 15 | * which 16 | * tar: for building the archive 17 | * curl: used if phpunit and composer are not installed to fetch them from the web 18 | * npm: for building and testing JS 19 | 20 | 21 | ## Publish to App Store 22 | 23 | First get an account for the [App Store](http://apps.nextcloud.com/) then run: 24 | 25 | make && make appstore 26 | 27 | The archive is located in build/artifacts/appstore and can then be uploaded to the App Store. 28 | -------------------------------------------------------------------------------- /appinfo/app.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Piotr Bator 2017 11 | */ 12 | 13 | namespace OCA\PhotoMap\AppInfo; 14 | 15 | use OCP\AppFramework\App; 16 | use OCA\PhotoMap\AppInfo\Application; 17 | 18 | $app = new Application(); 19 | 20 | $app->register(); 21 | 22 | $container = $app->getContainer(); 23 | $container->query('OCP\INavigationManager')->add(function () use ($container) { 24 | $urlGenerator = $container->query('OCP\IURLGenerator'); 25 | $l10n = $container->query('OCP\IL10N'); 26 | return [ 27 | // the string under which your app will be referenced in Nextcloud 28 | 'id' => 'photomap', 29 | 30 | // sorting weight for the navigation. The higher the number, the higher 31 | // will it be listed in the navigation 32 | 'order' => 10, 33 | 34 | // the route that will be shown on startup 35 | 'href' => $urlGenerator->linkToRoute('photomap.page.index'), 36 | 37 | // the icon that will be shown in the navigation 38 | // this file needs to exist in img/ 39 | 'icon' => $urlGenerator->imagePath('photomap', 'app.svg'), 40 | 41 | // the title of your application. This will be used in the 42 | // navigation or on the settings page of your app 43 | 'name' => $l10n->t('Photo Map'), 44 | ]; 45 | }); -------------------------------------------------------------------------------- /appinfo/application.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright Piotr Bator 2017 10 | */ 11 | 12 | namespace OCA\PhotoMap\AppInfo; 13 | 14 | use OCP\AppFramework\App; 15 | 16 | use OCA\PhotoMap\Hook\FileHooks; 17 | use OCA\PhotoMap\Service\PhotofilesService; 18 | 19 | class Application extends App { 20 | 21 | public function __construct () { 22 | parent::__construct('photomap'); 23 | $this->getContainer()->registerService('FileHooks', function($c) { 24 | return new FileHooks( 25 | $c->query('ServerContainer')->getRootFolder(), 26 | \OC::$server->query(PhotofilesService::class), 27 | $c->query('ServerContainer')->getLogger(), 28 | $c->query('AppName') 29 | ); 30 | }); 31 | } 32 | 33 | public function register() { 34 | $this->getContainer()->query('FileHooks')->register(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /appinfo/database.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | *dbname* 4 | true 5 | false 6 | utf8 7 | 8 | *dbprefix*photomap_photos 9 | 10 | 11 | id 12 | integer 13 | true 14 | 10 15 | 1 16 | true 17 | 18 | 19 | file_id 20 | integer 21 | true 22 | 10 23 | 24 | 25 | lat 26 | float 27 | true 28 | 29 | 30 | lng 31 | float 32 | true 33 | 34 | 35 | user_id 36 | text 37 | true 38 | 64 39 | 40 | 41 | date_taken 42 | integer 43 | 44 | 45 |
46 |
47 | 48 | -------------------------------------------------------------------------------- /appinfo/info.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | photomap 5 | Photo Map 6 | Shows geotagged photos on map 7 | 8 | agpl 9 | Piotr Bator 10 | 0.0.5.1 11 | PhotoMap 12 | multimedia 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | OCA\PhotoMap\Command\RescanPhotos 21 | 22 | 23 | -------------------------------------------------------------------------------- /appinfo/routes.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright Piotr Bator 2017 10 | */ 11 | return [ 12 | 'routes' => [ 13 | ['name' => 'page#index', 'url' => '/', 'verb' => 'GET'], 14 | 15 | //geo photos 16 | ['name' => 'geophotos#getPhotosFromDb', 'url' => '/geoPhotos/all', 'verb' => 'GET'], 17 | 18 | //photo files 19 | ['name' => 'photofiles#rescan', 'url' => '/photos/rescan', 'verb' => 'GET'], 20 | ['name' => 'photofiles#getPhotosByFolder', 'url' => '/photos/getPhotosByFolder/{path}', 'verb' => 'GET', 'requirements' => array('path' => '.+')], 21 | ] 22 | ]; 23 | -------------------------------------------------------------------------------- /configure.js: -------------------------------------------------------------------------------- 1 | var replace = require('replace'); 2 | var prompt = require('prompt'); 3 | 4 | prompt.start(); 5 | 6 | prompt.get(['appId', 'appName'], function (err, result) { 7 | if (err) { 8 | console.log(err); 9 | return; 10 | } 11 | 12 | replace({ 13 | regex: "react_oc_boilerplate", 14 | replacement: result.appId, 15 | paths: ['appinfo/app.php', 'appinfo/info.xml', 'webpack/dev.config.js', 'controller/pagecontroller.php'], 16 | recursive: false, 17 | silent: true 18 | }); 19 | 20 | replace({ 21 | regex: "React Boilerplate", 22 | replacement: result.appName, 23 | paths: ['appinfo/app.php', 'appinfo/info.xml'], 24 | recursive: false, 25 | silent: true 26 | }); 27 | }); 28 | 29 | 30 | -------------------------------------------------------------------------------- /css/Leaflet.Photo.css: -------------------------------------------------------------------------------- 1 | /** 2 | * https://github.com/turban/Leaflet.Photo 3 | * 4 | * 5 | * The MIT License (MIT) 6 | * 7 | * Copyright (c) 2014 Bjorn Sandvik 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | .leaflet-marker-photo { 29 | border: 2px solid #fff; 30 | box-shadow: 3px 3px 10px #888; 31 | } 32 | 33 | .leaflet-marker-photo div { 34 | width: 100%; 35 | height: 100%; 36 | background-size: cover; 37 | background-position: center center; 38 | background-repeat: no-repeat; 39 | } 40 | 41 | .leaflet-marker-photo b { 42 | position: absolute; 43 | top: -7px; 44 | right: -11px; 45 | color: #555; 46 | background-color: #fff; 47 | border-radius: 8px; 48 | height: 12px; 49 | min-width: 12px; 50 | line-height: 12px; 51 | text-align: center; 52 | padding: 3px; 53 | box-shadow: 0 3px 14px rgba(0,0,0,0.4); 54 | } -------------------------------------------------------------------------------- /css/MarkerCluster.Default.css: -------------------------------------------------------------------------------- 1 | .marker-cluster-small { 2 | background-color: rgba(181, 226, 140, 0.6); 3 | } 4 | .marker-cluster-small div { 5 | background-color: rgba(110, 204, 57, 0.6); 6 | } 7 | 8 | .marker-cluster-medium { 9 | background-color: rgba(241, 211, 87, 0.6); 10 | } 11 | .marker-cluster-medium div { 12 | background-color: rgba(240, 194, 12, 0.6); 13 | } 14 | 15 | .marker-cluster-large { 16 | background-color: rgba(253, 156, 115, 0.6); 17 | } 18 | .marker-cluster-large div { 19 | background-color: rgba(241, 128, 23, 0.6); 20 | } 21 | 22 | /* IE 6-8 fallback colors */ 23 | .leaflet-oldie .marker-cluster-small { 24 | background-color: rgb(181, 226, 140); 25 | } 26 | .leaflet-oldie .marker-cluster-small div { 27 | background-color: rgb(110, 204, 57); 28 | } 29 | 30 | .leaflet-oldie .marker-cluster-medium { 31 | background-color: rgb(241, 211, 87); 32 | } 33 | .leaflet-oldie .marker-cluster-medium div { 34 | background-color: rgb(240, 194, 12); 35 | } 36 | 37 | .leaflet-oldie .marker-cluster-large { 38 | background-color: rgb(253, 156, 115); 39 | } 40 | .leaflet-oldie .marker-cluster-large div { 41 | background-color: rgb(241, 128, 23); 42 | } 43 | 44 | .marker-cluster { 45 | background-clip: padding-box; 46 | border-radius: 20px; 47 | } 48 | .marker-cluster div { 49 | width: 30px; 50 | height: 30px; 51 | margin-left: 5px; 52 | margin-top: 5px; 53 | 54 | text-align: center; 55 | border-radius: 15px; 56 | font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif; 57 | } 58 | .marker-cluster span { 59 | line-height: 30px; 60 | } -------------------------------------------------------------------------------- /css/MarkerCluster.css: -------------------------------------------------------------------------------- 1 | .leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow { 2 | -webkit-transition: -webkit-transform 0.3s ease-out, opacity 0.3s ease-in; 3 | -moz-transition: -moz-transform 0.3s ease-out, opacity 0.3s ease-in; 4 | -o-transition: -o-transform 0.3s ease-out, opacity 0.3s ease-in; 5 | transition: transform 0.3s ease-out, opacity 0.3s ease-in; 6 | } 7 | -------------------------------------------------------------------------------- /css/app.less: -------------------------------------------------------------------------------- 1 | h1.header { 2 | text-align: center; 3 | } 4 | -------------------------------------------------------------------------------- /css/images/gallery.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | images 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /css/images/layers-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatPio/PhotoMap/6caa82d9ce5e609a9c8f21986d1c7f9f42c37d5c/css/images/layers-2x.png -------------------------------------------------------------------------------- /css/images/layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatPio/PhotoMap/6caa82d9ce5e609a9c8f21986d1c7f9f42c37d5c/css/images/layers.png -------------------------------------------------------------------------------- /css/images/marker-icon-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatPio/PhotoMap/6caa82d9ce5e609a9c8f21986d1c7f9f42c37d5c/css/images/marker-icon-2x.png -------------------------------------------------------------------------------- /css/images/marker-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatPio/PhotoMap/6caa82d9ce5e609a9c8f21986d1c7f9f42c37d5c/css/images/marker-icon.png -------------------------------------------------------------------------------- /css/images/marker-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatPio/PhotoMap/6caa82d9ce5e609a9c8f21986d1c7f9f42c37d5c/css/images/marker-shadow.png -------------------------------------------------------------------------------- /css/leaflet.css: -------------------------------------------------------------------------------- 1 | /* required styles */ 2 | 3 | .leaflet-map-pane, 4 | .leaflet-tile, 5 | .leaflet-marker-icon, 6 | .leaflet-marker-shadow, 7 | .leaflet-tile-pane, 8 | .leaflet-tile-container, 9 | .leaflet-overlay-pane, 10 | .leaflet-shadow-pane, 11 | .leaflet-marker-pane, 12 | .leaflet-popup-pane, 13 | .leaflet-overlay-pane svg, 14 | .leaflet-zoom-box, 15 | .leaflet-image-layer, 16 | .leaflet-layer { 17 | position: absolute; 18 | left: 0; 19 | top: 0; 20 | } 21 | .leaflet-container { 22 | overflow: hidden; 23 | -ms-touch-action: none; 24 | } 25 | .leaflet-tile, 26 | .leaflet-marker-icon, 27 | .leaflet-marker-shadow { 28 | -webkit-user-select: none; 29 | -moz-user-select: none; 30 | user-select: none; 31 | -webkit-user-drag: none; 32 | } 33 | .leaflet-marker-icon, 34 | .leaflet-marker-shadow { 35 | display: block; 36 | } 37 | /* map is broken in FF if you have max-width: 100% on tiles */ 38 | .leaflet-container img { 39 | max-width: none !important; 40 | } 41 | /* stupid Android 2 doesn't understand "max-width: none" properly */ 42 | .leaflet-container img.leaflet-image-layer { 43 | max-width: 15000px !important; 44 | } 45 | .leaflet-tile { 46 | filter: inherit; 47 | visibility: hidden; 48 | } 49 | .leaflet-tile-loaded { 50 | visibility: inherit; 51 | } 52 | .leaflet-zoom-box { 53 | width: 0; 54 | height: 0; 55 | } 56 | /* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */ 57 | .leaflet-overlay-pane svg { 58 | -moz-user-select: none; 59 | } 60 | 61 | .leaflet-tile-pane { z-index: 2; } 62 | .leaflet-objects-pane { z-index: 3; } 63 | .leaflet-overlay-pane { z-index: 4; } 64 | .leaflet-shadow-pane { z-index: 5; } 65 | .leaflet-marker-pane { z-index: 6; } 66 | .leaflet-popup-pane { z-index: 7; } 67 | 68 | .leaflet-vml-shape { 69 | width: 1px; 70 | height: 1px; 71 | } 72 | .lvml { 73 | behavior: url(#default#VML); 74 | display: inline-block; 75 | position: absolute; 76 | } 77 | 78 | 79 | /* control positioning */ 80 | 81 | .leaflet-control { 82 | position: relative; 83 | z-index: 7; 84 | pointer-events: auto; 85 | } 86 | .leaflet-top, 87 | .leaflet-bottom { 88 | position: absolute; 89 | z-index: 1000; 90 | pointer-events: none; 91 | } 92 | .leaflet-top { 93 | top: 0; 94 | } 95 | .leaflet-right { 96 | right: 0; 97 | } 98 | .leaflet-bottom { 99 | bottom: 0; 100 | } 101 | .leaflet-left { 102 | left: 0; 103 | } 104 | .leaflet-control { 105 | float: left; 106 | clear: both; 107 | } 108 | .leaflet-right .leaflet-control { 109 | float: right; 110 | } 111 | .leaflet-top .leaflet-control { 112 | margin-top: 10px; 113 | } 114 | .leaflet-bottom .leaflet-control { 115 | margin-bottom: 10px; 116 | } 117 | .leaflet-left .leaflet-control { 118 | margin-left: 10px; 119 | } 120 | .leaflet-right .leaflet-control { 121 | margin-right: 10px; 122 | } 123 | 124 | 125 | /* zoom and fade animations */ 126 | 127 | .leaflet-fade-anim .leaflet-tile, 128 | .leaflet-fade-anim .leaflet-popup { 129 | opacity: 0; 130 | -webkit-transition: opacity 0.2s linear; 131 | -moz-transition: opacity 0.2s linear; 132 | -o-transition: opacity 0.2s linear; 133 | transition: opacity 0.2s linear; 134 | } 135 | .leaflet-fade-anim .leaflet-tile-loaded, 136 | .leaflet-fade-anim .leaflet-map-pane .leaflet-popup { 137 | opacity: 1; 138 | } 139 | 140 | .leaflet-zoom-anim .leaflet-zoom-animated { 141 | -webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1); 142 | -moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1); 143 | -o-transition: -o-transform 0.25s cubic-bezier(0,0,0.25,1); 144 | transition: transform 0.25s cubic-bezier(0,0,0.25,1); 145 | } 146 | .leaflet-zoom-anim .leaflet-tile, 147 | .leaflet-pan-anim .leaflet-tile, 148 | .leaflet-touching .leaflet-zoom-animated { 149 | -webkit-transition: none; 150 | -moz-transition: none; 151 | -o-transition: none; 152 | transition: none; 153 | } 154 | 155 | .leaflet-zoom-anim .leaflet-zoom-hide { 156 | visibility: hidden; 157 | } 158 | 159 | 160 | /* cursors */ 161 | 162 | .leaflet-clickable { 163 | cursor: pointer; 164 | } 165 | .leaflet-container { 166 | cursor: -webkit-grab; 167 | cursor: -moz-grab; 168 | } 169 | .leaflet-popup-pane, 170 | .leaflet-control { 171 | cursor: auto; 172 | } 173 | .leaflet-dragging .leaflet-container, 174 | .leaflet-dragging .leaflet-clickable { 175 | cursor: move; 176 | cursor: -webkit-grabbing; 177 | cursor: -moz-grabbing; 178 | } 179 | 180 | 181 | /* visual tweaks */ 182 | 183 | .leaflet-container { 184 | background: #ddd; 185 | outline: 0; 186 | } 187 | .leaflet-container a { 188 | color: #0078A8; 189 | } 190 | .leaflet-container a.leaflet-active { 191 | outline: 2px solid orange; 192 | } 193 | .leaflet-zoom-box { 194 | border: 2px dotted #38f; 195 | background: rgba(255,255,255,0.5); 196 | } 197 | 198 | 199 | /* general typography */ 200 | .leaflet-container { 201 | font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif; 202 | } 203 | 204 | 205 | /* general toolbar styles */ 206 | 207 | .leaflet-bar { 208 | box-shadow: 0 1px 5px rgba(0,0,0,0.65); 209 | border-radius: 4px; 210 | } 211 | .leaflet-bar a, 212 | .leaflet-bar a:hover { 213 | background-color: #fff; 214 | border-bottom: 1px solid #ccc; 215 | width: 26px; 216 | height: 26px; 217 | line-height: 26px; 218 | display: block; 219 | text-align: center; 220 | text-decoration: none; 221 | color: black; 222 | } 223 | .leaflet-bar a, 224 | .leaflet-control-layers-toggle { 225 | background-position: 50% 50%; 226 | background-repeat: no-repeat; 227 | display: block; 228 | } 229 | .leaflet-bar a:hover { 230 | background-color: #f4f4f4; 231 | } 232 | .leaflet-bar a:first-child { 233 | border-top-left-radius: 4px; 234 | border-top-right-radius: 4px; 235 | } 236 | .leaflet-bar a:last-child { 237 | border-bottom-left-radius: 4px; 238 | border-bottom-right-radius: 4px; 239 | border-bottom: none; 240 | } 241 | .leaflet-bar a.leaflet-disabled { 242 | cursor: default; 243 | background-color: #f4f4f4; 244 | color: #bbb; 245 | } 246 | 247 | .leaflet-touch .leaflet-bar a { 248 | width: 30px; 249 | height: 30px; 250 | line-height: 30px; 251 | } 252 | 253 | 254 | /* zoom control */ 255 | 256 | .leaflet-control-zoom-in, 257 | .leaflet-control-zoom-out { 258 | font: bold 18px 'Lucida Console', Monaco, monospace; 259 | text-indent: 1px; 260 | } 261 | .leaflet-control-zoom-out { 262 | font-size: 20px; 263 | } 264 | 265 | .leaflet-touch .leaflet-control-zoom-in { 266 | font-size: 22px; 267 | } 268 | .leaflet-touch .leaflet-control-zoom-out { 269 | font-size: 24px; 270 | } 271 | 272 | 273 | /* layers control */ 274 | 275 | .leaflet-control-layers { 276 | box-shadow: 0 1px 5px rgba(0,0,0,0.4); 277 | background: #fff; 278 | border-radius: 5px; 279 | } 280 | .leaflet-control-layers-toggle { 281 | background-image: url(images/layers.png); 282 | width: 36px; 283 | height: 36px; 284 | } 285 | .leaflet-retina .leaflet-control-layers-toggle { 286 | background-image: url(images/layers-2x.png); 287 | background-size: 26px 26px; 288 | } 289 | .leaflet-touch .leaflet-control-layers-toggle { 290 | width: 44px; 291 | height: 44px; 292 | } 293 | .leaflet-control-layers .leaflet-control-layers-list, 294 | .leaflet-control-layers-expanded .leaflet-control-layers-toggle { 295 | display: none; 296 | } 297 | .leaflet-control-layers-expanded .leaflet-control-layers-list { 298 | display: block; 299 | position: relative; 300 | } 301 | .leaflet-control-layers-expanded { 302 | padding: 6px 10px 6px 6px; 303 | color: #333; 304 | background: #fff; 305 | } 306 | .leaflet-control-layers-selector { 307 | margin-top: 2px; 308 | position: relative; 309 | top: 1px; 310 | } 311 | .leaflet-control-layers label { 312 | display: block; 313 | } 314 | .leaflet-control-layers-separator { 315 | height: 0; 316 | border-top: 1px solid #ddd; 317 | margin: 5px -10px 5px -6px; 318 | } 319 | 320 | 321 | /* attribution and scale controls */ 322 | 323 | .leaflet-container .leaflet-control-attribution { 324 | background: #fff; 325 | background: rgba(255, 255, 255, 0.7); 326 | margin: 0; 327 | } 328 | .leaflet-control-attribution, 329 | .leaflet-control-scale-line { 330 | padding: 0 5px; 331 | color: #333; 332 | } 333 | .leaflet-control-attribution a { 334 | text-decoration: none; 335 | } 336 | .leaflet-control-attribution a:hover { 337 | text-decoration: underline; 338 | } 339 | .leaflet-container .leaflet-control-attribution, 340 | .leaflet-container .leaflet-control-scale { 341 | font-size: 11px; 342 | } 343 | .leaflet-left .leaflet-control-scale { 344 | margin-left: 5px; 345 | } 346 | .leaflet-bottom .leaflet-control-scale { 347 | margin-bottom: 5px; 348 | } 349 | .leaflet-control-scale-line { 350 | border: 2px solid #777; 351 | border-top: none; 352 | line-height: 1.1; 353 | padding: 2px 5px 1px; 354 | font-size: 11px; 355 | white-space: nowrap; 356 | overflow: hidden; 357 | -moz-box-sizing: content-box; 358 | box-sizing: content-box; 359 | 360 | background: #fff; 361 | background: rgba(255, 255, 255, 0.5); 362 | } 363 | .leaflet-control-scale-line:not(:first-child) { 364 | border-top: 2px solid #777; 365 | border-bottom: none; 366 | margin-top: -2px; 367 | } 368 | .leaflet-control-scale-line:not(:first-child):not(:last-child) { 369 | border-bottom: 2px solid #777; 370 | } 371 | 372 | .leaflet-touch .leaflet-control-attribution, 373 | .leaflet-touch .leaflet-control-layers, 374 | .leaflet-touch .leaflet-bar { 375 | box-shadow: none; 376 | } 377 | .leaflet-touch .leaflet-control-layers, 378 | .leaflet-touch .leaflet-bar { 379 | border: 2px solid rgba(0,0,0,0.2); 380 | background-clip: padding-box; 381 | } 382 | 383 | 384 | /* popup */ 385 | 386 | .leaflet-popup { 387 | position: absolute; 388 | text-align: center; 389 | } 390 | .leaflet-popup-content-wrapper { 391 | padding: 1px; 392 | text-align: left; 393 | border-radius: 12px; 394 | } 395 | .leaflet-popup-content { 396 | margin: 13px 19px; 397 | line-height: 1.4; 398 | } 399 | .leaflet-popup-content p { 400 | margin: 18px 0; 401 | } 402 | .leaflet-popup-tip-container { 403 | margin: 0 auto; 404 | width: 40px; 405 | height: 20px; 406 | position: relative; 407 | overflow: hidden; 408 | } 409 | .leaflet-popup-tip { 410 | width: 17px; 411 | height: 17px; 412 | padding: 1px; 413 | 414 | margin: -10px auto 0; 415 | 416 | -webkit-transform: rotate(45deg); 417 | -moz-transform: rotate(45deg); 418 | -ms-transform: rotate(45deg); 419 | -o-transform: rotate(45deg); 420 | transform: rotate(45deg); 421 | } 422 | .leaflet-popup-content-wrapper, 423 | .leaflet-popup-tip { 424 | background: white; 425 | 426 | box-shadow: 0 3px 14px rgba(0,0,0,0.4); 427 | } 428 | .leaflet-container a.leaflet-popup-close-button { 429 | position: absolute; 430 | top: 0; 431 | right: 0; 432 | padding: 4px 4px 0 0; 433 | text-align: center; 434 | width: 18px; 435 | height: 14px; 436 | font: 16px/14px Tahoma, Verdana, sans-serif; 437 | color: #c3c3c3; 438 | text-decoration: none; 439 | font-weight: bold; 440 | background: transparent; 441 | } 442 | .leaflet-container a.leaflet-popup-close-button:hover { 443 | color: #999; 444 | } 445 | .leaflet-popup-scrolled { 446 | overflow: auto; 447 | border-bottom: 1px solid #ddd; 448 | border-top: 1px solid #ddd; 449 | } 450 | 451 | .leaflet-oldie .leaflet-popup-content-wrapper { 452 | zoom: 1; 453 | } 454 | .leaflet-oldie .leaflet-popup-tip { 455 | width: 24px; 456 | margin: 0 auto; 457 | 458 | -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)"; 459 | filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678); 460 | } 461 | .leaflet-oldie .leaflet-popup-tip-container { 462 | margin-top: -1px; 463 | } 464 | 465 | .leaflet-oldie .leaflet-control-zoom, 466 | .leaflet-oldie .leaflet-control-layers, 467 | .leaflet-oldie .leaflet-popup-content-wrapper, 468 | .leaflet-oldie .leaflet-popup-tip { 469 | border: 1px solid #999; 470 | } 471 | 472 | 473 | /* div icon */ 474 | 475 | .leaflet-div-icon { 476 | background: #fff; 477 | border: 1px solid #666; 478 | } 479 | -------------------------------------------------------------------------------- /css/map.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | } 6 | html, body, #map { 7 | height: 100%; 8 | width: 100%; 9 | padding: 0; 10 | margin: 0; 11 | position: static !important; 12 | } 13 | th { 14 | text-align: left; 15 | vertical-align: top; 16 | } 17 | .info { 18 | padding: 6px 8px; 19 | font: 14px/16px Arial, Helvetica, sans-serif; 20 | background: white; 21 | background: rgba(255,255,255,0.8); 22 | box-shadow: 0 0 15px rgba(0,0,0,0.2); 23 | border-radius: 5px; 24 | } 25 | .info h2 { 26 | margin: 0 0 5px; 27 | color: #777; 28 | } 29 | .leaflet-container { 30 | background: #fff; 31 | } -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | #visibleFolders { 2 | height: 100%; 3 | overflow-y: auto; 4 | } 5 | 6 | #visibleFolders .folder-item { 7 | margin: 1px; 8 | } 9 | 10 | #visibleFolders .collapsible .collapse { 11 | z-index: 1; 12 | } 13 | 14 | #visibleFolders .collapsible a { 15 | width: 185px; 16 | display: inline-block; 17 | } 18 | 19 | #visibleFolders .collapsible a.icon-folder{ 20 | white-space: nowrap; 21 | overflow: hidden; /* "overflow" value must be different from "visible" */ 22 | text-overflow: ellipsis; 23 | direction:rtl; 24 | text-align:left; 25 | } 26 | 27 | #visibleFolders .collapsible .folder-link{ 28 | display: inline-block; 29 | width: 44px; 30 | height: 44px; 31 | padding: 0px; 32 | text-align: center; 33 | vertical-align: top; 34 | } 35 | 36 | #visibleFolders .collapsible .folder-link .icon{ 37 | width: 36px; 38 | height: 32px; 39 | display: inline-block; 40 | vertical-align: middle; 41 | background-image: url('images/gallery.svg'); 42 | } -------------------------------------------------------------------------------- /img/app.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | map2 4 | 5 | Layer 1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /js/Leaflet.Photo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * https://github.com/turban/Leaflet.Photo 3 | * 4 | * 5 | * The MIT License (MIT) 6 | * 7 | * Copyright (c) 2014 Bjorn Sandvik 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in all 17 | * copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | * SOFTWARE. 26 | */ 27 | 28 | L.Photo = L.FeatureGroup.extend({ 29 | options: { 30 | icon: { 31 | iconSize: [40, 40] 32 | } 33 | }, 34 | 35 | initialize: function (photos, options) { 36 | L.setOptions(this, options); 37 | L.FeatureGroup.prototype.initialize.call(this, photos); 38 | }, 39 | 40 | addLayers: function (photos) { 41 | if (photos) { 42 | for (var i = 0, len = photos.length; i < len; i++) { 43 | this.addLayer(photos[i]); 44 | } 45 | } 46 | return this; 47 | }, 48 | 49 | addLayer: function (photo) { 50 | L.FeatureGroup.prototype.addLayer.call(this, this.createMarker(photo)); 51 | }, 52 | 53 | createMarker: function (photo) { 54 | var marker = L.marker(photo, { 55 | icon: L.divIcon(L.extend({ 56 | html: '
​', 57 | className: 'leaflet-marker-photo' 58 | }, photo, this.options.icon)), 59 | title: photo.caption || '' 60 | }); 61 | marker.photo = photo; 62 | return marker; 63 | } 64 | }); 65 | 66 | L.photo = function (photos, options) { 67 | return new L.Photo(photos, options); 68 | }; 69 | 70 | if (L.MarkerClusterGroup) { 71 | 72 | L.Photo.Cluster = L.MarkerClusterGroup.extend({ 73 | options: { 74 | featureGroup: L.photo, 75 | maxClusterRadius: 100, 76 | showCoverageOnHover: false, 77 | iconCreateFunction: function(cluster) { 78 | return new L.DivIcon(L.extend({ 79 | className: 'leaflet-marker-photo', 80 | html: '
' + cluster.getChildCount() + '' 81 | }, this.icon)); 82 | }, 83 | icon: { 84 | iconSize: [40, 40] 85 | } 86 | }, 87 | 88 | initialize: function (options) { 89 | options = L.Util.setOptions(this, options); 90 | L.MarkerClusterGroup.prototype.initialize.call(this); 91 | this._photos = options.featureGroup(null, options); 92 | }, 93 | 94 | add: function (photos) { 95 | this.addLayer(this._photos.addLayers(photos)); 96 | return this; 97 | }, 98 | 99 | clear: function () { 100 | this._photos.clearLayers(); 101 | this.clearLayers(); 102 | } 103 | 104 | }); 105 | 106 | L.photo.cluster = function (options) { 107 | return new L.Photo.Cluster(options); 108 | }; 109 | 110 | } -------------------------------------------------------------------------------- /js/leaflet.markercluster.js: -------------------------------------------------------------------------------- 1 | /* 2 | Leaflet.markercluster, Provides Beautiful Animated Marker Clustering functionality for Leaflet, a JS library for interactive maps. 3 | https://github.com/Leaflet/Leaflet.markercluster 4 | (c) 2012-2013, Dave Leaver, smartrak 5 | */ 6 | !function(t,e){L.MarkerClusterGroup=L.FeatureGroup.extend({options:{maxClusterRadius:80,iconCreateFunction:null,spiderfyOnMaxZoom:!0,showCoverageOnHover:!0,zoomToBoundsOnClick:!0,singleMarkerMode:!1,disableClusteringAtZoom:null,removeOutsideVisibleBounds:!0,animateAddingMarkers:!1,spiderfyDistanceMultiplier:1,chunkedLoading:!1,chunkInterval:200,chunkDelay:50,chunkProgress:null,polygonOptions:{}},initialize:function(t){L.Util.setOptions(this,t),this.options.iconCreateFunction||(this.options.iconCreateFunction=this._defaultIconCreateFunction),this._featureGroup=L.featureGroup(),this._featureGroup.on(L.FeatureGroup.EVENTS,this._propagateEvent,this),this._nonPointGroup=L.featureGroup(),this._nonPointGroup.on(L.FeatureGroup.EVENTS,this._propagateEvent,this),this._inZoomAnimation=0,this._needsClustering=[],this._needsRemoving=[],this._currentShownBounds=null,this._queue=[]},addLayer:function(t){if(t instanceof L.LayerGroup){var e=[];for(var i in t._layers)e.push(t._layers[i]);return this.addLayers(e)}if(!t.getLatLng)return this._nonPointGroup.addLayer(t),this;if(!this._map)return this._needsClustering.push(t),this;if(this.hasLayer(t))return this;this._unspiderfy&&this._unspiderfy(),this._addLayer(t,this._maxZoom);var n=t,s=this._map.getZoom();if(t.__parent)for(;n.__parent._zoom>=s;)n=n.__parent;return this._currentShownBounds.contains(n.getLatLng())&&(this.options.animateAddingMarkers?this._animationAddLayer(t,n):this._animationAddLayerNonAnimated(t,n)),this},removeLayer:function(t){if(t instanceof L.LayerGroup){var e=[];for(var i in t._layers)e.push(t._layers[i]);return this.removeLayers(e)}return t.getLatLng?this._map?t.__parent?(this._unspiderfy&&(this._unspiderfy(),this._unspiderfyLayer(t)),this._removeLayer(t,!0),this._featureGroup.hasLayer(t)&&(this._featureGroup.removeLayer(t),t.setOpacity&&t.setOpacity(1)),this):this:(!this._arraySplice(this._needsClustering,t)&&this.hasLayer(t)&&this._needsRemoving.push(t),this):(this._nonPointGroup.removeLayer(t),this)},addLayers:function(t){var e,i,n,s,r=this._featureGroup,o=this._nonPointGroup,a=this.options.chunkedLoading,h=this.options.chunkInterval,_=this.options.chunkProgress;if(this._map){var u=0,l=(new Date).getTime(),d=L.bind(function(){for(var e=(new Date).getTime();uh)break}if(s=t[u],s.getLatLng){if(!this.hasLayer(s)&&(this._addLayer(s,this._maxZoom),s.__parent&&2===s.__parent.getChildCount())){var n=s.__parent.getAllChildMarkers(),p=n[0]===s?n[1]:n[0];r.removeLayer(p)}}else o.addLayer(s)}_&&_(u,t.length,(new Date).getTime()-l),u===t.length?(this._featureGroup.eachLayer(function(t){t instanceof L.MarkerCluster&&t._iconNeedsUpdate&&t._updateIcon()}),this._topClusterLevel._recursivelyAddChildrenToMap(null,this._zoom,this._currentShownBounds)):setTimeout(d,this.options.chunkDelay)},this);d()}else{for(e=[],i=0,n=t.length;n>i;i++)s=t[i],s.getLatLng?this.hasLayer(s)||e.push(s):o.addLayer(s);this._needsClustering=this._needsClustering.concat(e)}return this},removeLayers:function(t){var e,i,n,s=this._featureGroup,r=this._nonPointGroup;if(!this._map){for(e=0,i=t.length;i>e;e++)n=t[e],this._arraySplice(this._needsClustering,n),r.removeLayer(n);return this}for(e=0,i=t.length;i>e;e++)n=t[e],n.__parent?(this._removeLayer(n,!0,!0),s.hasLayer(n)&&(s.removeLayer(n),n.setOpacity&&n.setOpacity(1))):r.removeLayer(n);return this._topClusterLevel._recursivelyAddChildrenToMap(null,this._zoom,this._currentShownBounds),s.eachLayer(function(t){t instanceof L.MarkerCluster&&t._updateIcon()}),this},clearLayers:function(){return this._map||(this._needsClustering=[],delete this._gridClusters,delete this._gridUnclustered),this._noanimationUnspiderfy&&this._noanimationUnspiderfy(),this._featureGroup.clearLayers(),this._nonPointGroup.clearLayers(),this.eachLayer(function(t){delete t.__parent}),this._map&&this._generateInitialClusters(),this},getBounds:function(){var t=new L.LatLngBounds;this._topClusterLevel&&t.extend(this._topClusterLevel._bounds);for(var e=this._needsClustering.length-1;e>=0;e--)t.extend(this._needsClustering[e].getLatLng());return t.extend(this._nonPointGroup.getBounds()),t},eachLayer:function(t,e){var i,n=this._needsClustering.slice();for(this._topClusterLevel&&this._topClusterLevel.getAllChildMarkers(n),i=n.length-1;i>=0;i--)t.call(e,n[i]);this._nonPointGroup.eachLayer(t,e)},getLayers:function(){var t=[];return this.eachLayer(function(e){t.push(e)}),t},getLayer:function(t){var e=null;return this.eachLayer(function(i){L.stamp(i)===t&&(e=i)}),e},hasLayer:function(t){if(!t)return!1;var e,i=this._needsClustering;for(e=i.length-1;e>=0;e--)if(i[e]===t)return!0;for(i=this._needsRemoving,e=i.length-1;e>=0;e--)if(i[e]===t)return!1;return!(!t.__parent||t.__parent._group!==this)||this._nonPointGroup.hasLayer(t)},zoomToShowLayer:function(t,e){var i=function(){if((t._icon||t.__parent._icon)&&!this._inZoomAnimation)if(this._map.off("moveend",i,this),this.off("animationend",i,this),t._icon)e();else if(t.__parent._icon){var n=function(){this.off("spiderfied",n,this),e()};this.on("spiderfied",n,this),t.__parent.spiderfy()}};if(t._icon&&this._map.getBounds().contains(t.getLatLng()))e();else if(t.__parent._zoome;e++)n=this._needsRemoving[e],this._removeLayer(n,!0);this._needsRemoving=[],this._zoom=this._map.getZoom(),this._currentShownBounds=this._getExpandedVisibleBounds(),this._map.on("zoomend",this._zoomEnd,this),this._map.on("moveend",this._moveEnd,this),this._spiderfierOnAdd&&this._spiderfierOnAdd(),this._bindEvents(),i=this._needsClustering,this._needsClustering=[],this.addLayers(i)},onRemove:function(t){t.off("zoomend",this._zoomEnd,this),t.off("moveend",this._moveEnd,this),this._unbindEvents(),this._map._mapPane.className=this._map._mapPane.className.replace(" leaflet-cluster-anim",""),this._spiderfierOnRemove&&this._spiderfierOnRemove(),this._hideCoverage(),this._featureGroup.onRemove(t),this._nonPointGroup.onRemove(t),this._featureGroup.clearLayers(),this._map=null},getVisibleParent:function(t){for(var e=t;e&&!e._icon;)e=e.__parent;return e||null},_arraySplice:function(t,e){for(var i=t.length-1;i>=0;i--)if(t[i]===e)return t.splice(i,1),!0},_removeLayer:function(t,e,i){var n=this._gridClusters,s=this._gridUnclustered,r=this._featureGroup,o=this._map;if(e)for(var a=this._maxZoom;a>=0&&s[a].removeObject(t,o.project(t.getLatLng(),a));a--);var h,_=t.__parent,u=_._markers;for(this._arraySplice(u,t);_&&(_._childCount--,!(_._zoom<0));)e&&_._childCount<=1?(h=_._markers[0]===t?_._markers[1]:_._markers[0],n[_._zoom].removeObject(_,o.project(_._cLatLng,_._zoom)),s[_._zoom].addObject(h,o.project(h.getLatLng(),_._zoom)),this._arraySplice(_.__parent._childClusters,_),_.__parent._markers.push(h),h.__parent=_.__parent,_._icon&&(r.removeLayer(_),i||r.addLayer(h))):(_._recalculateBounds(),i&&_._icon||_._updateIcon()),_=_.__parent;delete t.__parent},_isOrIsParent:function(t,e){for(;e;){if(t===e)return!0;e=e.parentNode}return!1},_propagateEvent:function(t){if(t.layer instanceof L.MarkerCluster){if(t.originalEvent&&this._isOrIsParent(t.layer._icon,t.originalEvent.relatedTarget))return;t.type="cluster"+t.type}this.fire(t.type,t)},_defaultIconCreateFunction:function(t){var e=t.getChildCount(),i=" marker-cluster-";return i+=10>e?"small":100>e?"medium":"large",new L.DivIcon({html:"
"+e+"
",className:"marker-cluster"+i,iconSize:new L.Point(40,40)})},_bindEvents:function(){var t=this._map,e=this.options.spiderfyOnMaxZoom,i=this.options.showCoverageOnHover,n=this.options.zoomToBoundsOnClick;(e||n)&&this.on("clusterclick",this._zoomOrSpiderfy,this),i&&(this.on("clustermouseover",this._showCoverage,this),this.on("clustermouseout",this._hideCoverage,this),t.on("zoomend",this._hideCoverage,this))},_zoomOrSpiderfy:function(t){var e=this._map;e.getMaxZoom()===e.getZoom()?this.options.spiderfyOnMaxZoom&&t.layer.spiderfy():this.options.zoomToBoundsOnClick&&t.layer.zoomToBounds(),t.originalEvent&&13===t.originalEvent.keyCode&&e._container.focus()},_showCoverage:function(t){var e=this._map;this._inZoomAnimation||(this._shownPolygon&&e.removeLayer(this._shownPolygon),t.layer.getChildCount()>2&&t.layer!==this._spiderfied&&(this._shownPolygon=new L.Polygon(t.layer.getConvexHull(),this.options.polygonOptions),e.addLayer(this._shownPolygon)))},_hideCoverage:function(){this._shownPolygon&&(this._map.removeLayer(this._shownPolygon),this._shownPolygon=null)},_unbindEvents:function(){var t=this.options.spiderfyOnMaxZoom,e=this.options.showCoverageOnHover,i=this.options.zoomToBoundsOnClick,n=this._map;(t||i)&&this.off("clusterclick",this._zoomOrSpiderfy,this),e&&(this.off("clustermouseover",this._showCoverage,this),this.off("clustermouseout",this._hideCoverage,this),n.off("zoomend",this._hideCoverage,this))},_zoomEnd:function(){this._map&&(this._mergeSplitClusters(),this._zoom=this._map._zoom,this._currentShownBounds=this._getExpandedVisibleBounds())},_moveEnd:function(){if(!this._inZoomAnimation){var t=this._getExpandedVisibleBounds();this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,this._zoom,t),this._topClusterLevel._recursivelyAddChildrenToMap(null,this._map._zoom,t),this._currentShownBounds=t}},_generateInitialClusters:function(){var t=this._map.getMaxZoom(),e=this.options.maxClusterRadius,i=e;"function"!=typeof e&&(i=function(){return e}),this.options.disableClusteringAtZoom&&(t=this.options.disableClusteringAtZoom-1),this._maxZoom=t,this._gridClusters={},this._gridUnclustered={};for(var n=t;n>=0;n--)this._gridClusters[n]=new L.DistanceGrid(i(n)),this._gridUnclustered[n]=new L.DistanceGrid(i(n));this._topClusterLevel=new L.MarkerCluster(this,-1)},_addLayer:function(t,e){var i,n,s=this._gridClusters,r=this._gridUnclustered;for(this.options.singleMarkerMode&&(t.options.icon=this.options.iconCreateFunction({getChildCount:function(){return 1},getAllChildMarkers:function(){return[t]}}));e>=0;e--){i=this._map.project(t.getLatLng(),e);var o=s[e].getNearObject(i);if(o)return o._addChild(t),t.__parent=o,void 0;if(o=r[e].getNearObject(i)){var a=o.__parent;a&&this._removeLayer(o,!1);var h=new L.MarkerCluster(this,e,o,t);s[e].addObject(h,this._map.project(h._cLatLng,e)),o.__parent=h,t.__parent=h;var _=h;for(n=e-1;n>a._zoom;n--)_=new L.MarkerCluster(this,n,_),s[n].addObject(_,this._map.project(o.getLatLng(),n));for(a._addChild(_),n=e;n>=0&&r[n].removeObject(o,this._map.project(o.getLatLng(),n));n--);return}r[e].addObject(t,i)}this._topClusterLevel._addChild(t),t.__parent=this._topClusterLevel},_enqueue:function(t){this._queue.push(t),this._queueTimeout||(this._queueTimeout=setTimeout(L.bind(this._processQueue,this),300))},_processQueue:function(){for(var t=0;tthis._map._zoom?(this._animationStart(),this._animationZoomOut(this._zoom,this._map._zoom)):this._moveEnd()},_getExpandedVisibleBounds:function(){if(!this.options.removeOutsideVisibleBounds)return this._map.getBounds();var t=this._map,e=t.getBounds(),i=e._southWest,n=e._northEast,s=L.Browser.mobile?0:Math.abs(i.lat-n.lat),r=L.Browser.mobile?0:Math.abs(i.lng-n.lng);return new L.LatLngBounds(new L.LatLng(i.lat-s,i.lng-r,!0),new L.LatLng(n.lat+s,n.lng+r,!0))},_animationAddLayerNonAnimated:function(t,e){if(e===t)this._featureGroup.addLayer(t);else if(2===e._childCount){e._addToMap();var i=e.getAllChildMarkers();this._featureGroup.removeLayer(i[0]),this._featureGroup.removeLayer(i[1])}else e._updateIcon()}}),L.MarkerClusterGroup.include(L.DomUtil.TRANSITION?{_animationStart:function(){this._map._mapPane.className+=" leaflet-cluster-anim",this._inZoomAnimation++},_animationEnd:function(){this._map&&(this._map._mapPane.className=this._map._mapPane.className.replace(" leaflet-cluster-anim","")),this._inZoomAnimation--,this.fire("animationend")},_animationZoomIn:function(t,e){var i,n=this._getExpandedVisibleBounds(),s=this._featureGroup;this._topClusterLevel._recursively(n,t,0,function(r){var o,a=r._latlng,h=r._markers;for(n.contains(a)||(a=null),r._isSingleParent()&&t+1===e?(s.removeLayer(r),r._recursivelyAddChildrenToMap(null,e,n)):(r.setOpacity(0),r._recursivelyAddChildrenToMap(a,e,n)),i=h.length-1;i>=0;i--)o=h[i],n.contains(o._latlng)||s.removeLayer(o)}),this._forceLayout(),this._topClusterLevel._recursivelyBecomeVisible(n,e),s.eachLayer(function(t){t instanceof L.MarkerCluster||!t._icon||t.setOpacity(1)}),this._topClusterLevel._recursively(n,t,e,function(t){t._recursivelyRestoreChildPositions(e)}),this._enqueue(function(){this._topClusterLevel._recursively(n,t,0,function(t){s.removeLayer(t),t.setOpacity(1)}),this._animationEnd()})},_animationZoomOut:function(t,e){this._animationZoomOutSingle(this._topClusterLevel,t-1,e),this._topClusterLevel._recursivelyAddChildrenToMap(null,e,this._getExpandedVisibleBounds()),this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,t,this._getExpandedVisibleBounds())},_animationZoomOutSingle:function(t,e,i){var n=this._getExpandedVisibleBounds();t._recursivelyAnimateChildrenInAndAddSelfToMap(n,e+1,i);var s=this;this._forceLayout(),t._recursivelyBecomeVisible(n,i),this._enqueue(function(){if(1===t._childCount){var r=t._markers[0];r.setLatLng(r.getLatLng()),r.setOpacity&&r.setOpacity(1)}else t._recursively(n,i,0,function(t){t._recursivelyRemoveChildrenFromMap(n,e+1)});s._animationEnd()})},_animationAddLayer:function(t,e){var i=this,n=this._featureGroup;n.addLayer(t),e!==t&&(e._childCount>2?(e._updateIcon(),this._forceLayout(),this._animationStart(),t._setPos(this._map.latLngToLayerPoint(e.getLatLng())),t.setOpacity(0),this._enqueue(function(){n.removeLayer(t),t.setOpacity(1),i._animationEnd()})):(this._forceLayout(),i._animationStart(),i._animationZoomOutSingle(e,this._map.getMaxZoom(),this._map.getZoom())))},_forceLayout:function(){L.Util.falseFn(e.body.offsetWidth)}}:{_animationStart:function(){},_animationZoomIn:function(t,e){this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,t),this._topClusterLevel._recursivelyAddChildrenToMap(null,e,this._getExpandedVisibleBounds()),this.fire("animationend")},_animationZoomOut:function(t,e){this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,t),this._topClusterLevel._recursivelyAddChildrenToMap(null,e,this._getExpandedVisibleBounds()),this.fire("animationend")},_animationAddLayer:function(t,e){this._animationAddLayerNonAnimated(t,e)}}),L.markerClusterGroup=function(t){return new L.MarkerClusterGroup(t)},L.MarkerCluster=L.Marker.extend({initialize:function(t,e,i,n){L.Marker.prototype.initialize.call(this,i?i._cLatLng||i.getLatLng():new L.LatLng(0,0),{icon:this}),this._group=t,this._zoom=e,this._markers=[],this._childClusters=[],this._childCount=0,this._iconNeedsUpdate=!0,this._bounds=new L.LatLngBounds,i&&this._addChild(i),n&&this._addChild(n)},getAllChildMarkers:function(t){t=t||[];for(var e=this._childClusters.length-1;e>=0;e--)this._childClusters[e].getAllChildMarkers(t);for(var i=this._markers.length-1;i>=0;i--)t.push(this._markers[i]);return t},getChildCount:function(){return this._childCount},zoomToBounds:function(){for(var t,e=this._childClusters.slice(),i=this._group._map,n=i.getBoundsZoom(this._bounds),s=this._zoom+1,r=i.getZoom();e.length>0&&n>s;){s++;var o=[];for(t=0;ts?this._group._map.setView(this._latlng,s):r>=n?this._group._map.setView(this._latlng,r+1):this._group._map.fitBounds(this._bounds)},getBounds:function(){var t=new L.LatLngBounds;return t.extend(this._bounds),t},_updateIcon:function(){this._iconNeedsUpdate=!0,this._icon&&this.setIcon(this)},createIcon:function(){return this._iconNeedsUpdate&&(this._iconObj=this._group.options.iconCreateFunction(this),this._iconNeedsUpdate=!1),this._iconObj.createIcon()},createShadow:function(){return this._iconObj.createShadow()},_addChild:function(t,e){this._iconNeedsUpdate=!0,this._expandBounds(t),t instanceof L.MarkerCluster?(e||(this._childClusters.push(t),t.__parent=this),this._childCount+=t._childCount):(e||this._markers.push(t),this._childCount++),this.__parent&&this.__parent._addChild(t,!0)},_expandBounds:function(t){var e,i=t._wLatLng||t._latlng;t instanceof L.MarkerCluster?(this._bounds.extend(t._bounds),e=t._childCount):(this._bounds.extend(i),e=1),this._cLatLng||(this._cLatLng=t._cLatLng||i);var n=this._childCount+e;this._wLatLng?(this._wLatLng.lat=(i.lat*e+this._wLatLng.lat*this._childCount)/n,this._wLatLng.lng=(i.lng*e+this._wLatLng.lng*this._childCount)/n):this._latlng=this._wLatLng=new L.LatLng(i.lat,i.lng)},_addToMap:function(t){t&&(this._backupLatlng=this._latlng,this.setLatLng(t)),this._group._featureGroup.addLayer(this)},_recursivelyAnimateChildrenIn:function(t,e,i){this._recursively(t,0,i-1,function(t){var i,n,s=t._markers;for(i=s.length-1;i>=0;i--)n=s[i],n._icon&&(n._setPos(e),n.setOpacity(0))},function(t){var i,n,s=t._childClusters;for(i=s.length-1;i>=0;i--)n=s[i],n._icon&&(n._setPos(e),n.setOpacity(0))})},_recursivelyAnimateChildrenInAndAddSelfToMap:function(t,e,i){this._recursively(t,i,0,function(n){n._recursivelyAnimateChildrenIn(t,n._group._map.latLngToLayerPoint(n.getLatLng()).round(),e),n._isSingleParent()&&e-1===i?(n.setOpacity(1),n._recursivelyRemoveChildrenFromMap(t,e)):n.setOpacity(0),n._addToMap()})},_recursivelyBecomeVisible:function(t,e){this._recursively(t,0,e,null,function(t){t.setOpacity(1)})},_recursivelyAddChildrenToMap:function(t,e,i){this._recursively(i,-1,e,function(n){if(e!==n._zoom)for(var s=n._markers.length-1;s>=0;s--){var r=n._markers[s];i.contains(r._latlng)&&(t&&(r._backupLatlng=r.getLatLng(),r.setLatLng(t),r.setOpacity&&r.setOpacity(0)),n._group._featureGroup.addLayer(r))}},function(e){e._addToMap(t)})},_recursivelyRestoreChildPositions:function(t){for(var e=this._markers.length-1;e>=0;e--){var i=this._markers[e];i._backupLatlng&&(i.setLatLng(i._backupLatlng),delete i._backupLatlng)}if(t-1===this._zoom)for(var n=this._childClusters.length-1;n>=0;n--)this._childClusters[n]._restorePosition();else for(var s=this._childClusters.length-1;s>=0;s--)this._childClusters[s]._recursivelyRestoreChildPositions(t)},_restorePosition:function(){this._backupLatlng&&(this.setLatLng(this._backupLatlng),delete this._backupLatlng)},_recursivelyRemoveChildrenFromMap:function(t,e,i){var n,s;this._recursively(t,-1,e-1,function(t){for(s=t._markers.length-1;s>=0;s--)n=t._markers[s],i&&i.contains(n._latlng)||(t._group._featureGroup.removeLayer(n),n.setOpacity&&n.setOpacity(1))},function(t){for(s=t._childClusters.length-1;s>=0;s--)n=t._childClusters[s],i&&i.contains(n._latlng)||(t._group._featureGroup.removeLayer(n),n.setOpacity&&n.setOpacity(1))})},_recursively:function(t,e,i,n,s){var r,o,a=this._childClusters,h=this._zoom;if(e>h)for(r=a.length-1;r>=0;r--)o=a[r],t.intersects(o._bounds)&&o._recursively(t,e,i,n,s);else if(n&&n(this),s&&this._zoom===i&&s(this),i>h)for(r=a.length-1;r>=0;r--)o=a[r],t.intersects(o._bounds)&&o._recursively(t,e,i,n,s)},_recalculateBounds:function(){var t,e=this._markers,i=this._childClusters;for(this._bounds=new L.LatLngBounds,delete this._wLatLng,t=e.length-1;t>=0;t--)this._expandBounds(e[t]);for(t=i.length-1;t>=0;t--)this._expandBounds(i[t])},_isSingleParent:function(){return this._childClusters.length>0&&this._childClusters[0]._childCount===this._childCount}}),L.DistanceGrid=function(t){this._cellSize=t,this._sqCellSize=t*t,this._grid={},this._objectPoint={}},L.DistanceGrid.prototype={addObject:function(t,e){var i=this._getCoord(e.x),n=this._getCoord(e.y),s=this._grid,r=s[n]=s[n]||{},o=r[i]=r[i]||[],a=L.Util.stamp(t);this._objectPoint[a]=e,o.push(t)},updateObject:function(t,e){this.removeObject(t),this.addObject(t,e)},removeObject:function(t,e){var i,n,s=this._getCoord(e.x),r=this._getCoord(e.y),o=this._grid,a=o[r]=o[r]||{},h=a[s]=a[s]||[];for(delete this._objectPoint[L.Util.stamp(t)],i=0,n=h.length;n>i;i++)if(h[i]===t)return h.splice(i,1),1===n&&delete a[s],!0},eachObject:function(t,e){var i,n,s,r,o,a,h,_=this._grid;for(i in _){o=_[i];for(n in o)for(a=o[n],s=0,r=a.length;r>s;s++)h=t.call(e,a[s]),h&&(s--,r--)}},getNearObject:function(t){var e,i,n,s,r,o,a,h,_=this._getCoord(t.x),u=this._getCoord(t.y),l=this._objectPoint,d=this._sqCellSize,p=null;for(e=u-1;u+1>=e;e++)if(s=this._grid[e])for(i=_-1;_+1>=i;i++)if(r=s[i])for(n=0,o=r.length;o>n;n++)a=r[n],h=this._sqDist(l[L.Util.stamp(a)],t),d>h&&(d=h,p=a);return p},_getCoord:function(t){return Math.floor(t/this._cellSize)},_sqDist:function(t,e){var i=e.x-t.x,n=e.y-t.y;return i*i+n*n}},function(){L.QuickHull={getDistant:function(t,e){var i=e[1].lat-e[0].lat,n=e[0].lng-e[1].lng;return n*(t.lat-e[0].lat)+i*(t.lng-e[0].lng)},findMostDistantPointFromBaseLine:function(t,e){var i,n,s,r=0,o=null,a=[];for(i=e.length-1;i>=0;i--)n=e[i],s=this.getDistant(n,t),s>0&&(a.push(n),s>r&&(r=s,o=n));return{maxPoint:o,newPoints:a}},buildConvexHull:function(t,e){var i=[],n=this.findMostDistantPointFromBaseLine(t,e);return n.maxPoint?(i=i.concat(this.buildConvexHull([t[0],n.maxPoint],n.newPoints)),i=i.concat(this.buildConvexHull([n.maxPoint,t[1]],n.newPoints))):[t[0]]},getConvexHull:function(t){var e,i=!1,n=!1,s=null,r=null;for(e=t.length-1;e>=0;e--){var o=t[e];(i===!1||o.lat>i)&&(s=o,i=o.lat),(n===!1||o.lat=0;e--)t=i[e].getLatLng(),n.push(t);return L.QuickHull.getConvexHull(n)}}),L.MarkerCluster.include({_2PI:2*Math.PI,_circleFootSeparation:25,_circleStartAngle:Math.PI/6,_spiralFootSeparation:28,_spiralLengthStart:11,_spiralLengthFactor:5,_circleSpiralSwitchover:9,spiderfy:function(){if(this._group._spiderfied!==this&&!this._group._inZoomAnimation){var t,e=this.getAllChildMarkers(),i=this._group,n=i._map,s=n.latLngToLayerPoint(this._latlng);this._group._unspiderfy(),this._group._spiderfied=this,e.length>=this._circleSpiralSwitchover?t=this._generatePointsSpiral(e.length,s):(s.y+=10,t=this._generatePointsCircle(e.length,s)),this._animationSpiderfy(e,t)}},unspiderfy:function(t){this._group._inZoomAnimation||(this._animationUnspiderfy(t),this._group._spiderfied=null)},_generatePointsCircle:function(t,e){var i,n,s=this._group.options.spiderfyDistanceMultiplier*this._circleFootSeparation*(2+t),r=s/this._2PI,o=this._2PI/t,a=[];for(a.length=t,i=t-1;i>=0;i--)n=this._circleStartAngle+i*o,a[i]=new L.Point(e.x+r*Math.cos(n),e.y+r*Math.sin(n))._round();return a},_generatePointsSpiral:function(t,e){var i,n=this._group.options.spiderfyDistanceMultiplier*this._spiralLengthStart,s=this._group.options.spiderfyDistanceMultiplier*this._spiralFootSeparation,r=this._group.options.spiderfyDistanceMultiplier*this._spiralLengthFactor,o=0,a=[];for(a.length=t,i=t-1;i>=0;i--)o+=s/n+5e-4*i,a[i]=new L.Point(e.x+n*Math.cos(o),e.y+n*Math.sin(o))._round(),n+=this._2PI*r/o;return a},_noanimationUnspiderfy:function(){var t,e,i=this._group,n=i._map,s=i._featureGroup,r=this.getAllChildMarkers();for(this.setOpacity(1),e=r.length-1;e>=0;e--)t=r[e],s.removeLayer(t),t._preSpiderfyLatlng&&(t.setLatLng(t._preSpiderfyLatlng),delete t._preSpiderfyLatlng),t.setZIndexOffset&&t.setZIndexOffset(0),t._spiderLeg&&(n.removeLayer(t._spiderLeg),delete t._spiderLeg);i._spiderfied=null}}),L.MarkerCluster.include(L.DomUtil.TRANSITION?{SVG_ANIMATION:function(){return e.createElementNS("http://www.w3.org/2000/svg","animate").toString().indexOf("SVGAnimate")>-1}(),_animationSpiderfy:function(t,i){var n,s,r,o,a=this,h=this._group,_=h._map,u=h._featureGroup,l=_.latLngToLayerPoint(this._latlng);for(n=t.length-1;n>=0;n--)s=t[n],s.setOpacity?(s.setZIndexOffset(1e6),s.setOpacity(0),u.addLayer(s),s._setPos(l)):u.addLayer(s);h._forceLayout(),h._animationStart();var d=L.Path.SVG?0:.3,p=L.Path.SVG_NS;for(n=t.length-1;n>=0;n--)if(o=_.layerPointToLatLng(i[n]),s=t[n],s._preSpiderfyLatlng=s._latlng,s.setLatLng(o),s.setOpacity&&s.setOpacity(1),r=new L.Polyline([a._latlng,o],{weight:1.5,color:"#222",opacity:d}),_.addLayer(r),s._spiderLeg=r,L.Path.SVG&&this.SVG_ANIMATION){var c=r._path.getTotalLength();r._path.setAttribute("stroke-dasharray",c+","+c);var f=e.createElementNS(p,"animate");f.setAttribute("attributeName","stroke-dashoffset"),f.setAttribute("begin","indefinite"),f.setAttribute("from",c),f.setAttribute("to",0),f.setAttribute("dur",.25),r._path.appendChild(f),f.beginElement(),f=e.createElementNS(p,"animate"),f.setAttribute("attributeName","stroke-opacity"),f.setAttribute("attributeName","stroke-opacity"),f.setAttribute("begin","indefinite"),f.setAttribute("from",0),f.setAttribute("to",.5),f.setAttribute("dur",.25),r._path.appendChild(f),f.beginElement()}if(a.setOpacity(.3),L.Path.SVG)for(this._group._forceLayout(),n=t.length-1;n>=0;n--)s=t[n]._spiderLeg,s.options.opacity=.5,s._path.setAttribute("stroke-opacity",.5);setTimeout(function(){h._animationEnd(),h.fire("spiderfied")},200)},_animationUnspiderfy:function(t){var e,i,n,s=this._group,r=s._map,o=s._featureGroup,a=t?r._latLngToNewLayerPoint(this._latlng,t.zoom,t.center):r.latLngToLayerPoint(this._latlng),h=this.getAllChildMarkers(),_=L.Path.SVG&&this.SVG_ANIMATION;for(s._animationStart(),this.setOpacity(1),i=h.length-1;i>=0;i--)e=h[i],e._preSpiderfyLatlng&&(e.setLatLng(e._preSpiderfyLatlng),delete e._preSpiderfyLatlng,e.setOpacity?(e._setPos(a),e.setOpacity(0)):o.removeLayer(e),_&&(n=e._spiderLeg._path.childNodes[0],n.setAttribute("to",n.getAttribute("from")),n.setAttribute("from",0),n.beginElement(),n=e._spiderLeg._path.childNodes[1],n.setAttribute("from",.5),n.setAttribute("to",0),n.setAttribute("stroke-opacity",0),n.beginElement(),e._spiderLeg._path.setAttribute("stroke-opacity",0)));setTimeout(function(){var t=0;for(i=h.length-1;i>=0;i--)e=h[i],e._spiderLeg&&t++;for(i=h.length-1;i>=0;i--)e=h[i],e._spiderLeg&&(e.setOpacity&&(e.setOpacity(1),e.setZIndexOffset(0)),t>1&&o.removeLayer(e),r.removeLayer(e._spiderLeg),delete e._spiderLeg);s._animationEnd()},200)}}:{_animationSpiderfy:function(t,e){var i,n,s,r,o=this._group,a=o._map,h=o._featureGroup;for(i=t.length-1;i>=0;i--)r=a.layerPointToLatLng(e[i]),n=t[i],n._preSpiderfyLatlng=n._latlng,n.setLatLng(r),n.setZIndexOffset&&n.setZIndexOffset(1e6),h.addLayer(n),s=new L.Polyline([this._latlng,r],{weight:1.5,color:"#222"}),a.addLayer(s),n._spiderLeg=s;this.setOpacity(.3),o.fire("spiderfied")},_animationUnspiderfy:function(){this._noanimationUnspiderfy()}}),L.MarkerClusterGroup.include({_spiderfied:null,_spiderfierOnAdd:function(){this._map.on("click",this._unspiderfyWrapper,this),this._map.options.zoomAnimation&&this._map.on("zoomstart",this._unspiderfyZoomStart,this),this._map.on("zoomend",this._noanimationUnspiderfy,this),L.Path.SVG&&!L.Browser.touch&&this._map._initPathRoot()},_spiderfierOnRemove:function(){this._map.off("click",this._unspiderfyWrapper,this),this._map.off("zoomstart",this._unspiderfyZoomStart,this),this._map.off("zoomanim",this._unspiderfyZoomAnim,this),this._unspiderfy()},_unspiderfyZoomStart:function(){this._map&&this._map.on("zoomanim",this._unspiderfyZoomAnim,this)},_unspiderfyZoomAnim:function(t){L.DomUtil.hasClass(this._map._mapPane,"leaflet-touching")||(this._map.off("zoomanim",this._unspiderfyZoomAnim,this),this._unspiderfy(t))},_unspiderfyWrapper:function(){this._unspiderfy()},_unspiderfy:function(t){this._spiderfied&&this._spiderfied.unspiderfy(t)},_noanimationUnspiderfy:function(){this._spiderfied&&this._spiderfied._noanimationUnspiderfy()},_unspiderfyLayer:function(t){t._spiderLeg&&(this._featureGroup.removeLayer(t),t.setOpacity(1),t.setZIndexOffset(0),this._map.removeLayer(t._spiderLeg),delete t._spiderLeg)}})}(window,document); -------------------------------------------------------------------------------- /js/leaflet.textpath.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Leaflet.TextPath - Shows text along a polyline 3 | * Inspired by Tom Mac Wright article : 4 | * http://mapbox.com/osmdev/2012/11/20/getting-serious-about-svg/ 5 | */ 6 | 7 | (function () { 8 | 9 | var __onAdd = L.Polyline.prototype.onAdd, 10 | __onRemove = L.Polyline.prototype.onRemove, 11 | __updatePath = L.Polyline.prototype._updatePath, 12 | __bringToFront = L.Polyline.prototype.bringToFront; 13 | 14 | 15 | var PolylineTextPath = { 16 | 17 | onAdd: function (map) { 18 | __onAdd.call(this, map); 19 | this._textRedraw(); 20 | }, 21 | 22 | onRemove: function (map) { 23 | map = map || this._map; 24 | if (map && this._textNode) 25 | map._pathRoot.removeChild(this._textNode); 26 | __onRemove.call(this, map); 27 | }, 28 | 29 | bringToFront: function () { 30 | __bringToFront.call(this); 31 | this._textRedraw(); 32 | }, 33 | 34 | _updatePath: function () { 35 | __updatePath.call(this); 36 | this._textRedraw(); 37 | }, 38 | 39 | _textRedraw: function () { 40 | var text = this._text, 41 | options = this._textOptions; 42 | if (text) { 43 | this.setText(null).setText(text, options); 44 | } 45 | }, 46 | 47 | setText: function (text, options) { 48 | this._text = text; 49 | this._textOptions = options; 50 | 51 | /* If not in SVG mode or Polyline not added to map yet return */ 52 | /* setText will be called by onAdd, using value stored in this._text */ 53 | if (!L.Browser.svg || typeof this._map === 'undefined') { 54 | return this; 55 | } 56 | 57 | var defaults = { 58 | repeat: false, 59 | fillColor: 'black', 60 | attributes: {}, 61 | below: false, 62 | }; 63 | options = L.Util.extend(defaults, options); 64 | 65 | /* If empty text, hide */ 66 | if (!text) { 67 | if (this._textNode && this._textNode.parentNode) { 68 | this._map._pathRoot.removeChild(this._textNode); 69 | 70 | /* delete the node, so it will not be removed a 2nd time if the layer is later removed from the map */ 71 | delete this._textNode; 72 | } 73 | return this; 74 | } 75 | 76 | text = text.replace(/ /g, '\u00A0'); // Non breakable spaces 77 | var id = 'pathdef-' + L.Util.stamp(this); 78 | var svg = this._map._pathRoot; 79 | this._path.setAttribute('id', id); 80 | 81 | if (options.repeat) { 82 | /* Compute single pattern length */ 83 | var pattern = L.Path.prototype._createElement('text'); 84 | for (var attr in options.attributes) 85 | pattern.setAttribute(attr, options.attributes[attr]); 86 | pattern.appendChild(document.createTextNode(text)); 87 | svg.appendChild(pattern); 88 | var alength = pattern.getComputedTextLength(); 89 | svg.removeChild(pattern); 90 | 91 | /* Create string as long as path */ 92 | text = new Array(Math.ceil(this._path.getTotalLength() / alength)).join(text); 93 | } 94 | 95 | /* Put it along the path using textPath */ 96 | var textNode = L.Path.prototype._createElement('text'), 97 | textPath = L.Path.prototype._createElement('textPath'); 98 | 99 | var dy = options.offset || this._path.getAttribute('stroke-width'); 100 | 101 | textPath.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", '#'+id); 102 | textNode.setAttribute('dy', dy); 103 | for (var attr in options.attributes) 104 | textNode.setAttribute(attr, options.attributes[attr]); 105 | textPath.appendChild(document.createTextNode(text)); 106 | textNode.appendChild(textPath); 107 | this._textNode = textNode; 108 | 109 | if (options.below) { 110 | svg.insertBefore(textNode, svg.firstChild); 111 | } 112 | else { 113 | svg.appendChild(textNode); 114 | } 115 | 116 | /* Center text according to the path's bounding box */ 117 | if (options.center) { 118 | var textLength = textNode.getComputedTextLength(); 119 | var pathLength = this._path.getTotalLength(); 120 | /* Set the position for the left side of the textNode */ 121 | textNode.setAttribute('dx', ((pathLength / 2) - (textLength / 2))); 122 | } 123 | 124 | /* Change label rotation (if required) */ 125 | if (options.orientation) { 126 | var rotateAngle = 0; 127 | switch (options.orientation) { 128 | case 'flip': 129 | rotateAngle = 180; 130 | break; 131 | case 'perpendicular': 132 | rotateAngle = 90; 133 | break; 134 | default: 135 | rotateAngle = options.orientation; 136 | } 137 | 138 | var rotatecenterX = (textNode.getBBox().x + textNode.getBBox().width / 2); 139 | var rotatecenterY = (textNode.getBBox().y + textNode.getBBox().height / 2); 140 | textNode.setAttribute('transform','rotate(' + rotateAngle + ' ' + rotatecenterX + ' ' + rotatecenterY + ')'); 141 | } 142 | 143 | /* Initialize mouse events for the additional nodes */ 144 | if (this.options.clickable) { 145 | if (L.Browser.svg || !L.Browser.vml) { 146 | textPath.setAttribute('class', 'leaflet-clickable'); 147 | } 148 | 149 | L.DomEvent.on(textNode, 'click', this._onMouseClick, this); 150 | 151 | var events = ['dblclick', 'mousedown', 'mouseover', 152 | 'mouseout', 'mousemove', 'contextmenu']; 153 | for (var i = 0; i < events.length; i++) { 154 | L.DomEvent.on(textNode, events[i], this._fireMouseEvent, this); 155 | } 156 | } 157 | 158 | return this; 159 | } 160 | }; 161 | 162 | L.Polyline.include(PolylineTextPath); 163 | 164 | L.LayerGroup.include({ 165 | setText: function(text, options) { 166 | for (var layer in this._layers) { 167 | if (typeof this._layers[layer].setText === 'function') { 168 | this._layers[layer].setText(text, options); 169 | } 170 | } 171 | return this; 172 | } 173 | }); 174 | 175 | })(); -------------------------------------------------------------------------------- /js_src/AlbumInfosCache.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * PhotoMap 5 | * 6 | * This file is licensed under the Affero General Public License version 3 or 7 | * later. See the COPYING file. 8 | * 9 | * @author Piotr Bator 10 | * @copyright Piotr Bator 2017 11 | */ 12 | 13 | export default class AlbumInfosCache { 14 | 15 | constructor() { 16 | this.infosArray = {}; 17 | } 18 | 19 | addAlbumInfosToCache(albumInfo) { 20 | for (var property in albumInfo) { 21 | if (albumInfo.hasOwnProperty(property)) { 22 | var albumId = property; 23 | if (!this.infosArray[albumId]) { 24 | this.infosArray[albumId] = {}; 25 | } 26 | var folderEntry = this.infosArray[albumId]; 27 | folderEntry.id = albumInfo[albumId].id; 28 | folderEntry.path = albumInfo[albumId].path; 29 | folderEntry.name = albumInfo[albumId].name; 30 | } 31 | } 32 | } 33 | 34 | addGeoPhotosToCache(geoPhotos) { 35 | for (var i = 0; i < geoPhotos.length; i++) { 36 | var albumId = geoPhotos[i].folderId; 37 | if (!this.infosArray[albumId]) { 38 | this.infosArray[albumId] = {}; 39 | } 40 | var folderInfo = this.infosArray[albumId]; 41 | if (!folderInfo.geoPhotos) { 42 | folderInfo.geoPhotos = []; 43 | } 44 | var geoPhotosInfo = folderInfo.geoPhotos; 45 | geoPhotosInfo.push(geoPhotos[i]); 46 | } 47 | } 48 | 49 | addAlbumPhotosToCache(albumId, photos) { 50 | if (!this.infosArray[albumId]) { 51 | this.infosArray[albumId] = {}; 52 | } 53 | var folderEntry = this.infosArray[albumId]; 54 | folderEntry.filesList = photos; 55 | } 56 | 57 | getAlbumInfo(albumId) { 58 | return this.infosArray[albumId]; 59 | } 60 | 61 | getManyAlbumsInfo(albumsIds) { 62 | return Object.keys(this.infosArray) 63 | .filter((key) => albumsIds.includes(Number(key))) 64 | .reduce((array, key) => { 65 | array.push(this.infosArray[key]); 66 | return array; 67 | }, []); 68 | } 69 | 70 | getAllAlbumsInfo() { 71 | return Object.keys(this.infosArray) 72 | .reduce((array, key) => { 73 | array.push(this.infosArray[key]); 74 | return array; 75 | }, []); 76 | } 77 | 78 | getAlbumGeoPhotos(albumId) { 79 | return this.getAlbumInfo(albumId).geoPhotos; 80 | } 81 | 82 | getGeoPhotoInfo(albumId, photoId) { 83 | var photos = this.getAlbumInfo(albumId).geoPhotos; 84 | for (var i = 0; i < photos.length; i++) { 85 | if (photos[i].fileId == photoId) { 86 | return photos[i]; 87 | } 88 | } 89 | } 90 | 91 | } -------------------------------------------------------------------------------- /js_src/AlbumsView.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * PhotoMap 5 | * 6 | * This file is licensed under the Affero General Public License version 3 or 7 | * later. See the COPYING file. 8 | * 9 | * @author Piotr Bator 10 | * @copyright Piotr Bator 2017 11 | */ 12 | 13 | import React from 'react'; 14 | import ReactDOM from 'react-dom'; 15 | import FoldersList from './FoldersList'; 16 | 17 | export default class AlbumsView { 18 | 19 | constructor(app) { 20 | this.app = app; 21 | this.filerAlbumsToMapCheckbox = document.getElementById('filter-albums-to-map'); 22 | var filerAlbumsToMapCheckboxListener = (function(_app){ 23 | return function (albumId) { 24 | _app.onFilterAlbumsToMapChanged(); 25 | } 26 | })(this.app); 27 | this.filerAlbumsToMapCheckbox.addEventListener('change', filerAlbumsToMapCheckboxListener); 28 | this.showTracksCheckbox = document.getElementById('show-album-tracks'); 29 | var showTracksCheckboxListener = (function(_app){ 30 | return function (albumId) { 31 | _app.onShowTracksChanged(); 32 | } 33 | })(this.app); 34 | this.showTracksCheckbox.addEventListener('change', showTracksCheckboxListener); 35 | } 36 | 37 | renderAlbumsList(albums) { 38 | var onItemsNeededFunc = (function(_app){ 39 | return function (albumId) { 40 | _app.onAlbumPhotosNeeded(albumId); 41 | } 42 | })(this.app); 43 | var onAlbumClick = (function(_app){ 44 | return function (albumId) { 45 | _app.onAlbumClicked(albumId); 46 | } 47 | })(this.app); 48 | var onPhotoClick = (function(_app){ 49 | return function (albumId, photoId) { 50 | _app.onAlbumItemClicked(albumId, photoId); 51 | } 52 | })(this.app); 53 | 54 | ReactDOM.render( 55 | , 56 | document.getElementById('visibleFolders') 57 | ); 58 | 59 | } 60 | 61 | isFilterAlbumToMapChecked() { 62 | return this.filerAlbumsToMapCheckbox.checked; 63 | } 64 | 65 | isShowAlbumTracksChecked() { 66 | return this.showTracksCheckbox.checked; 67 | } 68 | 69 | hide() { 70 | document.getElementById('app-navigation').style.display = 'none'; 71 | } 72 | 73 | show() { 74 | document.getElementById('app-navigation').style.display = 'block'; 75 | } 76 | 77 | } -------------------------------------------------------------------------------- /js_src/App.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * PhotoMap 5 | * 6 | * This file is licensed under the Affero General Public License version 3 or 7 | * later. See the COPYING file. 8 | * 9 | * @author Piotr Bator 10 | * @copyright Piotr Bator 2017 11 | */ 12 | 13 | import AlbumInfosCache from './AlbumInfosCache' 14 | import AlbumsView from './AlbumsView' 15 | import MapView from './MapView' 16 | import GeoPhotoHelper from './GeoPhotoHelper.js'; 17 | 18 | export default class App { 19 | 20 | TRACK_VISIBLITY_ZOOM_LEVEL = 12; 21 | 22 | SHOW_ONLY_GEOPHOTOS_IN_ALBUM = true; 23 | 24 | constructor() { 25 | this.albumsInfoCache = new AlbumInfosCache(); 26 | this.albumsView = new AlbumsView(this); 27 | this.mapView = new MapView(this); 28 | this.gpHelper = new GeoPhotoHelper(); 29 | } 30 | 31 | onShowTracksChanged() { 32 | this.renderTracks(); 33 | } 34 | 35 | onFilterAlbumsToMapChanged() { 36 | this.renderAlbumsList(); 37 | } 38 | 39 | onMapsBoundMoved() { 40 | this.renderAlbumsList(); 41 | this.renderTracks(); 42 | } 43 | 44 | onAlbumPhotosNeeded(albumId) { 45 | this.callForAlbumPhotos(albumId, this.albumsInfoCache.getAlbumInfo(albumId).path); 46 | } 47 | 48 | onAlbumClicked(albumId) { 49 | var geoPhotos = this.albumsInfoCache.getAlbumGeoPhotos(albumId); 50 | var boundPoints = this.gpHelper.calculateGeoPhotosBoundPoints(geoPhotos); 51 | if (boundPoints) { 52 | this.mapView.fitBounds(boundPoints[0], boundPoints[1]); 53 | } 54 | } 55 | 56 | renderTracks() { 57 | if (this.albumsView.isShowAlbumTracksChecked() && this.mapView.getZoomLevel() >= this.TRACK_VISIBLITY_ZOOM_LEVEL) { 58 | var visibleAlbumsIds = this.getMapVisibleAlbumsIds(); 59 | var currentTracksIds = this.mapView.getTrackIds(); 60 | var tracksOutOfMapBoundsIds = this.gpHelper.calculateOrphanedTracks(visibleAlbumsIds, currentTracksIds); 61 | if (tracksOutOfMapBoundsIds && tracksOutOfMapBoundsIds.length > 0) { 62 | //Hide tracks out of map bounds. 63 | this.mapView.hideTracks(tracksOutOfMapBoundsIds); 64 | } 65 | for (var i = 0; i < visibleAlbumsIds.length; i++) { 66 | var geoPhotos = this.albumsInfoCache.getAlbumGeoPhotos(visibleAlbumsIds[i]); 67 | var tracks = this.gpHelper.calculateTrack(geoPhotos, currentTracksIds); 68 | this.mapView.showTrackList(tracks); 69 | } 70 | } else { 71 | this.mapView.hideTracks(); 72 | } 73 | } 74 | 75 | onAlbumItemClicked(albumId, photoId) { 76 | var pInfo = this.albumsInfoCache.getGeoPhotoInfo(albumId, photoId); 77 | this.mapView.centerMapOnPoint(pInfo.lat, pInfo.lng); 78 | } 79 | 80 | generateThumbnailUrl(filename) { 81 | return "/index.php/core/preview.png?file=" + encodeURI(filename) + "&x=32&y=32"; 82 | } 83 | 84 | generateImageUrl(filename) { 85 | return "/index.php/core/preview.png?file=" + encodeURI(filename) + "&x=400&y=400"; 86 | } 87 | 88 | generateGalleryUrl(path) { 89 | return OC.generateUrl("apps/gallery/#" + path); 90 | } 91 | 92 | showPhotosOnMap(photos) { 93 | var markers = this.preparePhotoMarkersForView(photos); 94 | this.mapView.showPhotoMarkers(markers); 95 | } 96 | 97 | preparePhotoMarkersForView(photos) { 98 | var markers = []; 99 | for (var i = 0; i < photos.length; i++) { 100 | markers.push({ 101 | lat: photos[i].lat, 102 | lng: photos[i].lng, 103 | url: this.generateImageUrl(photos[i].path), 104 | thumbnail: this.generateThumbnailUrl(photos[i].path), 105 | albumId: photos[i].folderId 106 | }); 107 | } 108 | return markers; 109 | } 110 | 111 | prepareAlbumInfosForView(albumInfosList) { 112 | var albumViewInfosList = []; 113 | albumInfosList.forEach(function(item) { 114 | var photosList = undefined; 115 | if(this.SHOW_ONLY_GEOPHOTOS_IN_ALBUM) { 116 | photosList = this.prepareAlbumPhotoInfosForView(item.geoPhotos); 117 | } else { 118 | photosList = item.filesList ? this.prepareAlbumPhotoInfosForView(item.filesList) : undefined; 119 | } 120 | albumViewInfosList.push({ 121 | id: item.id, 122 | label: item.path, 123 | link: this.generateGalleryUrl(item.path.substring(1, item.path.length)), 124 | filesList : photosList 125 | }); 126 | }, this); 127 | return albumViewInfosList; 128 | } 129 | 130 | prepareAlbumPhotoInfosForView(filesList) { 131 | var photoViewInfosList = []; 132 | filesList.forEach(function(item) { 133 | photoViewInfosList.push({ 134 | id: item.fileId, 135 | name: item.name, 136 | thumb: this.generateThumbnailUrl(item.path) 137 | }); 138 | }, this); 139 | return photoViewInfosList; 140 | } 141 | 142 | renderAlbumsList() { 143 | var albumsInfosList; 144 | if (this.albumsView.isFilterAlbumToMapChecked()) { 145 | var visibleAlbumsIds = this.getMapVisibleAlbumsIds(); 146 | albumsInfosList = this.albumsInfoCache.getManyAlbumsInfo(visibleAlbumsIds); 147 | } else { 148 | albumsInfosList = this.albumsInfoCache.getAllAlbumsInfo(); 149 | } 150 | var albumViewInfosList = this.prepareAlbumInfosForView(albumsInfosList); 151 | this.albumsView.renderAlbumsList(albumViewInfosList); 152 | } 153 | 154 | getMapVisibleAlbumsIds() { 155 | var mapBounds = this.mapView.getBounds(); 156 | var ids = []; 157 | var albumInfosList = this.albumsInfoCache.getAllAlbumsInfo(); 158 | albumInfosList.forEach(function(item){ 159 | var albumBounds = this.gpHelper.calculateGeoPhotosBoundPoints(item.geoPhotos); 160 | if (this.gpHelper.boundsIntersecs(albumBounds, mapBounds)) { 161 | ids.push(item.id); 162 | } 163 | }, this); 164 | return ids; 165 | } 166 | 167 | callForImages() { 168 | $.ajax({ 169 | 'url' : OC.generateUrl('apps/photomap/geoPhotos/all'), 170 | 'type': 'GET', 171 | 'success': (function(_app) { 172 | return function(response) { 173 | if (response[0].length == 0) { 174 | _app.albumsView.hide(); 175 | _app.mapView.showFirstRunMessage(); 176 | } else { 177 | _app.albumsInfoCache.addGeoPhotosToCache(response[0]); 178 | _app.albumsInfoCache.addAlbumInfosToCache(response[1]); 179 | _app.showPhotosOnMap(response[0]); 180 | } 181 | } 182 | })(this) 183 | }); 184 | } 185 | 186 | callForAlbumPhotos(folderId, folderPath) { 187 | $.ajax({ 188 | 'url' : OC.generateUrl('apps/photomap/photos/getPhotosByFolder/'+folderPath), 189 | 'type': 'GET', 190 | 'success': (function(_app) { 191 | return function(response) { 192 | _app.albumsInfoCache.addAlbumPhotosToCache(folderId, response); 193 | _app.renderAlbumsList(); 194 | } 195 | })(this) 196 | }); 197 | } 198 | 199 | } -------------------------------------------------------------------------------- /js_src/FoldersList.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * PhotoMap 5 | * 6 | * This file is licensed under the Affero General Public License version 3 or 7 | * later. See the COPYING file. 8 | * 9 | * @author Piotr Bator 10 | * @copyright Piotr Bator 2017 11 | */ 12 | 13 | import React,{ PropTypes } from 'react'; 14 | import ReactDom from 'react-dom'; 15 | 16 | export default class FoldersList extends React.Component { 17 | 18 | static propTypes = { 19 | list: PropTypes.array.isRequired, 20 | componentClassName: PropTypes.string, 21 | onItemsNeededFunc: PropTypes.func, 22 | onFolderItemClick: PropTypes.func, 23 | onFolderClick: PropTypes.func 24 | }; 25 | 26 | constructor(props) { 27 | super(props); 28 | let initialItemsState = {}; 29 | props.list.forEach(function(element) { 30 | initialItemsState[element.id]= { 31 | isOpened: false, 32 | }; 33 | }); 34 | this.state= { 35 | itemsState: initialItemsState 36 | }; 37 | } 38 | 39 | handleToggle (headerIndex) { 40 | let newData = Object.assign({}, this.state.itemsState) 41 | newData[headerIndex].isOpened = !newData[headerIndex].isOpened 42 | this.setState( 43 | newData 44 | ) 45 | } 46 | 47 | onFolderClick(itemId) { 48 | if (this.props.onFolderClick) { 49 | this.props.onFolderClick(itemId); 50 | } 51 | } 52 | 53 | onFolderItemClick(folderId, itemId) { 54 | if (this.props.onFolderItemClick) { 55 | this.props.onFolderItemClick(folderId, itemId); 56 | } 57 | } 58 | 59 | renderFolderContent(folderId, folderItems, itemState) { 60 | if (!itemState.isOpened) { 61 | return; 62 | } 63 | if (!folderItems && this.props.onItemsNeededFunc) { 64 | this.props.onItemsNeededFunc(folderId); 65 | return; 66 | } 67 | const itemsList = folderItems.map((item) => this.renderFolderItem(folderId, item)); 68 | return(
{itemsList}
); 69 | } 70 | 71 | renderFolderItem (folderId, item) { 72 | return(); 73 | } 74 | 75 | render() { 76 | const listItems = this.props.list.map((folder, headerIndex) => { 77 | let itemState = this.state.itemsState[folder.id]; 78 | let headerClass = "collapsible" + ((itemState.isOpened && folder.filesList) ? " open" : ""); 79 | return ( 80 |
  • 81 | 82 | {folder.label} 83 | 84 | {this.renderFolderContent(folder.id, folder.filesList, itemState)} 85 |
  • ); 86 | }); 87 | let componentClassName = this.props.componentClassName || "list-component"; 88 | return ( 89 |
      {listItems}
    90 | ); 91 | } 92 | 93 | } -------------------------------------------------------------------------------- /js_src/GeoPhotoHelper.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * PhotoMap 5 | * 6 | * This file is licensed under the Affero General Public License version 3 or 7 | * later. See the COPYING file. 8 | * 9 | * @author Piotr Bator 10 | * @copyright Piotr Bator 2017 11 | */ 12 | 13 | export default class GeoPhotoHelper { 14 | 15 | calculateGeoPhotosBoundPoints(geoPhotos) { 16 | if (geoPhotos && geoPhotos.length) { 17 | var fPhoto = geoPhotos[0]; 18 | var minLat = fPhoto.lat, maxLat = fPhoto.lat, minLng = fPhoto.lng, maxLng = fPhoto.lng; 19 | for (var i = 0; i < geoPhotos.length; i++) { 20 | var lat = geoPhotos[i].lat; 21 | var lng = geoPhotos[i].lng; 22 | if (minLat > lat) { 23 | minLat = lat; 24 | } else if (maxLat < lat) { 25 | maxLat = lat; 26 | } 27 | if (minLng > lng) { 28 | minLng = lng; 29 | } else if (maxLng < lng) { 30 | maxLng = lng; 31 | } 32 | } 33 | return [[minLat, minLng],[maxLat, maxLng]]; 34 | } 35 | } 36 | 37 | calculateTrack(geoPhotos, ignoreTrackIds) { 38 | var tracksData = []; 39 | for (var i = 0; i < geoPhotos.length; i++) { 40 | var geoPhoto = geoPhotos[i]; 41 | var date = new Date(geoPhoto.dateTaken * 1000); 42 | var trackKey = geoPhotos[i].folderId + '_' + date.getFullYear() + date.getMonth() + date.getDay(); 43 | if (ignoreTrackIds && ignoreTrackIds.includes(trackKey)) { 44 | continue; 45 | } 46 | if (!tracksData[trackKey]) { 47 | tracksData[trackKey] = []; 48 | } 49 | tracksData[trackKey].push({ 50 | lat: geoPhoto.lat, 51 | lng: geoPhoto.lng, 52 | dateTaken: geoPhoto.dateTaken 53 | }); 54 | } 55 | 56 | for (i in tracksData) { 57 | if (tracksData.hasOwnProperty(i)) { 58 | tracksData[i].sort(function(a ,b) { 59 | return a.dateTaken - b.dateTaken; 60 | }); 61 | } 62 | } 63 | return tracksData; 64 | } 65 | 66 | calculateOrphanedTracks(albumsIds, tracksIds) { 67 | var ids = []; 68 | for (var i = 0; i < tracksIds.length; i++) { 69 | var trackAlbumId = Number(tracksIds[i].substring(0, tracksIds[i].indexOf('_'))); 70 | if(!albumsIds.includes(trackAlbumId)) { 71 | ids.push(tracksIds[i]); 72 | } 73 | } 74 | return ids; 75 | } 76 | 77 | boundsIntersecs(bound1, bound2) { 78 | var latIntersects = (bound2[1][0] >= bound1[0][0]) && (bound2[0][0] <= bound1[1][0]); 79 | var lngIntersects = (bound2[1][1] >= bound1[0][1]) && (bound2[0][1] <= bound1[1][1]); 80 | return latIntersects && lngIntersects; 81 | } 82 | 83 | } -------------------------------------------------------------------------------- /js_src/MapView.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * PhotoMap 5 | * 6 | * This file is licensed under the Affero General Public License version 3 or 7 | * later. See the COPYING file. 8 | * 9 | * @author Piotr Bator 10 | * @copyright Piotr Bator 2017 11 | */ 12 | 13 | export default class MapView { 14 | 15 | TRACK_COLORS_ARRAY = ['#3772FF', '#3BB273', '#E1BC29', '#7768AE', '#AC3931']; 16 | 17 | constructor(app) { 18 | this.app = app; 19 | this.initMap(); 20 | this.initEventListeners(); 21 | } 22 | 23 | initMap() { 24 | //Create a map 25 | var map = L.map('map', { maxZoom: 18}); 26 | 27 | //Add Background Mapping 28 | L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { 29 | attribution: 'GISforThought | Leaflet.Photo | Base map data © OpenStreetMap contributors' 30 | }).addTo(map); 31 | 32 | //Create the photolayer 33 | var photoLayer = L.photo.cluster({ spiderfyDistanceMultiplier: 1.2 }).on('click', function (evt) { 34 | evt.layer.bindPopup(L.Util.template('', evt.layer.photo), { 35 | className: 'leaflet-popup-photo', 36 | minWidth: 400 37 | }).openPopup(); 38 | }); 39 | 40 | this.map = map; 41 | this.layer = photoLayer; 42 | this.trackLayer = L.layerGroup([]); 43 | this.trackLayer.addTo(map); 44 | } 45 | 46 | initEventListeners() { 47 | var onMapMoveEnd = (function(_app){ 48 | return function () { 49 | _app.onMapsBoundMoved(); 50 | } 51 | }) (this.app); 52 | this.map.on('moveend', onMapMoveEnd); 53 | } 54 | 55 | centerMapOnPoint(lat, lng) { 56 | this.map.setView(new L.LatLng(lat, lng), 18); 57 | } 58 | 59 | fitBounds(swPoint, nePoint) { 60 | var southWest = L.latLng(swPoint[0], swPoint[1]), 61 | northEast = L.latLng(nePoint[0], nePoint[1]), 62 | bounds = L.latLngBounds(southWest, northEast); 63 | this.map.fitBounds(bounds); 64 | } 65 | 66 | /** 67 | * 68 | * @param {*} images array of markers: {id, lat, lng, url, thumbnail} 69 | */ 70 | showPhotoMarkers(markers) { 71 | this.layer 72 | .add(markers) 73 | .addTo(this.map); 74 | this.map.fitBounds(this.layer.getBounds()); 75 | } 76 | 77 | showTrackList (tracks) { 78 | var i = 0; 79 | for (var id in tracks) { 80 | if (tracks.hasOwnProperty(id)) { 81 | this.showTrack(id, tracks[id], this.TRACK_COLORS_ARRAY[i % this.TRACK_COLORS_ARRAY.length]); 82 | i++; 83 | } 84 | } 85 | } 86 | 87 | getTrackIds() { 88 | var ids = []; 89 | this.trackLayer.eachLayer(function (layer) { 90 | ids.push(layer.trackId); 91 | }); 92 | return ids; 93 | } 94 | 95 | showTrack(id, trackPoints, color) { 96 | var pointList = []; 97 | for (var i = 0; i < trackPoints.length ; i++) { 98 | var tPoint = trackPoints[i]; 99 | pointList.push(new L.LatLng(tPoint.lat, tPoint.lng)); 100 | } 101 | var trackPolyline = new L.Polyline(pointList, { 102 | color: color, 103 | weight: 2, 104 | opacity: 1, 105 | smoothFactor: 1 106 | }); 107 | trackPolyline.trackId = id; 108 | trackPolyline.setText(' ► ', {repeat: true, 109 | offset: 8, 110 | center: false, 111 | attributes: { 112 | 'dy': '6', 113 | 'fill': color, 114 | 'font-weight': 'bold', 115 | 'font-size': '22'} 116 | } 117 | ); 118 | this.trackLayer.addLayer(trackPolyline); 119 | } 120 | 121 | hideTracks(ids) { 122 | if(ids) { 123 | this.trackLayer.eachLayer(function (layer) { 124 | if (ids.includes(layer.trackId)) { 125 | this.removeLayer(layer); 126 | } 127 | }, this.trackLayer); 128 | } else { 129 | this.trackLayer.clearLayers(); 130 | } 131 | } 132 | 133 | showFirstRunMessage() { 134 | var latlng = [25, 0]; 135 | this.map.setView(latlng, 2); 136 | var message = "

    " + t('photomap', 'You have not added photos yet?') + "

    " + 137 | "

    "+ t('photomap', 'Add photos to the cloud. Photos containing data about the geographical location will be automatically pinned to the map.') + "

    " + 138 | "

    "+ t('photomap', 'If photos already are in cloud, You can pin them to the map using command {command}', {"command" : "occ photoMap:rescanPhotos"}, undefined, {escape: false}) + "

    "; 139 | this.showPopup(latlng, message); 140 | } 141 | 142 | showPopup(latlng, message) { 143 | L.popup() 144 | .setLatLng(latlng) 145 | .setContent(message) 146 | .openOn(this.map); 147 | } 148 | 149 | getZoomLevel() { 150 | return this.map.getZoom(); 151 | } 152 | 153 | getBounds() { 154 | var bounds = this.map.getBounds(); 155 | return [[bounds.getSouth(), bounds.getWest()],[bounds.getNorth(), bounds.getEast()]]; 156 | } 157 | 158 | } -------------------------------------------------------------------------------- /js_src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * PhotoMap 5 | * 6 | * This file is licensed under the Affero General Public License version 3 or 7 | * later. See the COPYING file. 8 | * 9 | * @author Piotr Bator 10 | * @copyright Piotr Bator 2017 11 | */ 12 | 13 | import App from './App'; 14 | 15 | $(document).ready(() => { 16 | var app = new App(); 17 | app.callForImages(); 18 | }); 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /l10n/pl.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "photomap", 3 | { 4 | "Only map visible albums" : "Tylko albumy widoczne na mapie", 5 | "Show album track" : "Pokaż ścieżkę albumu", 6 | "You have not added photos yet?" : "Nie dodałeś jeszcze zdjęć?", 7 | "Add photos to the cloud. Photos containing data about the geographical location will be automatically pinned to the map." : "Dodaj zdjęcia do chmury, jeśli zawierają dane o położeniu geograficznym zostaną automatycznie przypięte do mapy.", 8 | "If photos already are in cloud, You can pin them to the map using command {command}" : "Jeśli zdjęcia są w chmurze możesz przypiąć je do mapy skanując poleceniem: {command}" 9 | }, 10 | "nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);"); -------------------------------------------------------------------------------- /l10n/pl.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Only map visible albums" : "Tylko albumy widoczne na mapie", 3 | "Show album track" : "Pokaż ścieżkę albumu", 4 | "You have not added photos yet?" : "Nie dodałeś jeszcze zdjęć?", 5 | "Add photos to the cloud. Photos containing data about the geographical location will be automatically pinned to the map." : "Dodaj zdjęcia do chmury, jeśli zawierają dane o położeniu geograficznym zostaną automatycznie przypięte do mapy.", 6 | "If photos already are in cloud , You can pin them to the map using command {command}" : "Jeśli zdjęcia są w chmurze możesz przypiąć je do mapy skanując poleceniem: {command}" 7 | },"pluralForm" :"nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);" 8 | } -------------------------------------------------------------------------------- /l10n/pl_PL.php: -------------------------------------------------------------------------------- 1 | "Tylko albumy widoczne na mapie", 3 | "Show album track" => "Pokaż ścieżkę albumu" 4 | ); -------------------------------------------------------------------------------- /lib/Command/RescanPhotos.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Piotr Bator 2017 11 | */ 12 | 13 | namespace OCA\PhotoMap\Command; 14 | 15 | use OCP\Encryption\IManager; 16 | use OCP\Files\NotFoundException; 17 | use OCA\PhotoMap\Service\PhotofilesService; 18 | use OCP\IUser; 19 | use OCP\IUserManager; 20 | use Symfony\Component\Console\Command\Command; 21 | use Symfony\Component\Console\Input\InputArgument; 22 | use Symfony\Component\Console\Input\InputInterface; 23 | use Symfony\Component\Console\Output\OutputInterface; 24 | 25 | class RescanPhotos extends Command { 26 | 27 | protected $userManager; 28 | 29 | protected $output; 30 | 31 | protected $encryptionManager; 32 | 33 | private $photofilesService; 34 | 35 | public function __construct(IUserManager $userManager, 36 | IManager $encryptionManager, 37 | PhotofilesService $photofilesService) { 38 | parent::__construct(); 39 | $this->userManager = $userManager; 40 | $this->encryptionManager = $encryptionManager; 41 | $this->photofilesService = $photofilesService; 42 | } 43 | protected function configure() { 44 | $this->setName('photoMap:rescanPhotos') 45 | ->setDescription('Rescan photos GPS exif data') 46 | ->addArgument( 47 | 'user_id', 48 | InputArgument::OPTIONAL, 49 | 'Rescan photos GPS exif data for the given user' 50 | ); 51 | } 52 | 53 | protected function execute(InputInterface $input, OutputInterface $output) { 54 | if ($this->encryptionManager->isEnabled()) { 55 | $output->writeln('Encryption is enabled. Aborted.'); 56 | return 1; 57 | } 58 | $this->output = $output; 59 | $userId = $input->getArgument('user_id'); 60 | if ($userId === null) { 61 | $this->userManager->callForSeenUsers(function (IUser $user) { 62 | $this->rescanUserPhotos($user->getUID()); 63 | }); 64 | } else { 65 | $user = $this->userManager->get($userId); 66 | if ($user !== null) { 67 | $this->rescanUserPhotos($userId); 68 | } 69 | } 70 | return 0; 71 | } 72 | 73 | private function rescanUserPhotos($userId) { 74 | $this->photofilesService->rescan($userId); 75 | } 76 | } -------------------------------------------------------------------------------- /lib/Controller/GeophotosController.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Piotr Bator 2017 11 | */ 12 | 13 | namespace OCA\PhotoMap\Controller; 14 | 15 | use OCP\IRequest; 16 | use OCP\AppFramework\Http\TemplateResponse; 17 | use OCP\AppFramework\Http\DataResponse; 18 | use OCP\AppFramework\Controller; 19 | use OCA\PhotoMap\Service\GeophotoService; 20 | use OCP\ILogger; 21 | 22 | class GeophotosController extends Controller { 23 | private $userId; 24 | private $geophotoService; 25 | private $logger; 26 | 27 | public function __construct($AppName, ILogger $logger, IRequest $request, GeophotoService $GeophotoService, $UserId){ 28 | parent::__construct($AppName, $request); 29 | $this->logger = $logger; 30 | $this->userId = $UserId; 31 | $this->geophotoService = $GeophotoService; 32 | } 33 | 34 | /** 35 | * @NoAdminRequired 36 | */ 37 | public function getPhotosFromDb() { 38 | $result = $this->geophotoService->getAllFromDB($this->userId); 39 | return new DataResponse($result); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /lib/Controller/PageController.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Piotr Bator 2017 11 | */ 12 | 13 | namespace OCA\PhotoMap\Controller; 14 | 15 | use OCP\IRequest; 16 | use OCP\AppFramework\Http\TemplateResponse; 17 | use OCP\AppFramework\Http\DataResponse; 18 | use OCP\AppFramework\Controller; 19 | 20 | class PageController extends Controller { 21 | 22 | private $userId; 23 | 24 | public function __construct($AppName, IRequest $request, $UserId){ 25 | parent::__construct($AppName, $request); 26 | $this->userId = $UserId; 27 | } 28 | 29 | /** 30 | * CAUTION: the @Stuff turns off security checks; for this page no admin is 31 | * required and no CSRF check. If you don't know what CSRF is, read 32 | * it up in the docs or you might create a security hole. This is 33 | * basically the only required method to add this exemption, don't 34 | * add it to any other method if you don't exactly know what it does 35 | * 36 | * @NoAdminRequired 37 | * @NoCSRFRequired 38 | */ 39 | public function index() { 40 | $response = new TemplateResponse('photomap', 'index'); 41 | if (class_exists('OCP\AppFramework\Http\ContentSecurityPolicy')) { 42 | $csp = new \OCP\AppFramework\Http\ContentSecurityPolicy(); 43 | // map tiles 44 | $csp->addAllowedImageDomain('https://*.tile.openstreetmap.org'); 45 | $response->setContentSecurityPolicy($csp); 46 | } 47 | return $response; // templates/index.php 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /lib/Controller/PhotofilesController.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Piotr Bator 2017 11 | */ 12 | 13 | namespace OCA\PhotoMap\Controller; 14 | 15 | use OCP\IRequest; 16 | use OCP\AppFramework\Http\DataResponse; 17 | use OCP\AppFramework\Controller; 18 | use OCP\ILogger; 19 | 20 | use OCA\PhotoMap\Service\PhotofilesService; 21 | 22 | 23 | class PhotofilesController extends Controller { 24 | private $userId; 25 | private $photofilesService; 26 | private $logger; 27 | 28 | public function __construct($AppName, ILogger $logger, IRequest $request, PhotofilesService $photofilesService, $UserId) { 29 | parent::__construct($AppName, $request); 30 | $this->logger = $logger; 31 | $this->userId = $UserId; 32 | $this->photofilesService = $photofilesService; 33 | } 34 | 35 | /** 36 | * @NoAdminRequired 37 | */ 38 | public function rescan() { 39 | $this->photofilesService->rescan($this->userId); 40 | return new DataResponse(true); 41 | } 42 | 43 | /** 44 | * @NoAdminRequired 45 | */ 46 | public function getPhotosByFolder($path) { 47 | $result = $this->photofilesService->getPhotosByFolder($this->userId, $path); 48 | return new DataResponse($result); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /lib/DB/Geophoto.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Piotr Bator 2017 11 | */ 12 | 13 | namespace OCA\PhotoMap\DB; 14 | 15 | use OCP\AppFramework\Db\Entity; 16 | 17 | class Geophoto extends Entity { 18 | 19 | protected $fileId; 20 | protected $lat; 21 | protected $lng; 22 | protected $userId; 23 | protected $dateTaken; 24 | 25 | public function __construct() { 26 | // add types in constructor 27 | $this->addType('fileId', 'integer'); 28 | $this->addType('lat', 'float'); 29 | $this->addType('lng', 'float'); 30 | $this->addType('dateTaken', 'integer'); 31 | } 32 | } -------------------------------------------------------------------------------- /lib/DB/GeophotoMapper.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Piotr Bator 2017 11 | */ 12 | 13 | namespace OCA\PhotoMap\DB; 14 | 15 | use OCP\IDBConnection; 16 | use OCP\AppFramework\Db\Mapper; 17 | 18 | class GeophotoMapper extends Mapper { 19 | 20 | public function __construct(IDBConnection $db) { 21 | parent::__construct($db, 'photomap_photos'); 22 | } 23 | 24 | public function find($id) { 25 | $sql = 'SELECT * FROM `*PREFIX*photomap_photos` ' . 26 | 'WHERE `id` = ?'; 27 | return $this->findEntity($sql, [$id]); 28 | } 29 | 30 | 31 | public function findAll($userId, $limit=null, $offset=null) { 32 | $sql = 'SELECT * FROM `*PREFIX*photomap_photos` where `user_id` = ?'; 33 | return $this->findEntities($sql, [$userId], $limit, $offset); 34 | } 35 | 36 | public function deleteByFileId($fileId) { 37 | $sql = 'DELETE FROM `*PREFIX*photomap_photos` where `file_id` = ?'; 38 | return $this->execute($sql, [$fileId]); 39 | } 40 | 41 | public function deleteAll($userId) { 42 | $sql = 'DELETE FROM `*PREFIX*photomap_photos` where `user_id` = ?'; 43 | return $this->execute($sql, [$userId]); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /lib/Hook/FileHooks.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Piotr Bator 2017 11 | */ 12 | 13 | namespace OCA\PhotoMap\Hook; 14 | 15 | use OCA\PhotoMap\Service\PhotofilesService; 16 | use OC\Files\Filesystem; 17 | use OC\Files\View; 18 | use OCP\Files\FileInfo; 19 | use OCP\ILogger; 20 | use OCP\Files\Node; 21 | use OCP\Files\IRootFolder; 22 | use OCP\Util; 23 | 24 | /** 25 | * Handles files events 26 | */ 27 | class FileHooks { 28 | 29 | private $photofilesService; 30 | 31 | private $logger; 32 | 33 | private $root; 34 | 35 | public function __construct(IRootFolder $root, PhotofilesService $photofilesService, ILogger $logger, $appName) { 36 | $this->photofilesService = $photofilesService; 37 | $this->logger = $logger; 38 | $this->root = $root; 39 | } 40 | 41 | public function register() { 42 | $fileWriteCallback = function(\OCP\Files\Node $node) { 43 | if($this->isUserNode($node)) { 44 | $this->photofilesService->addByFile($node); 45 | } 46 | }; 47 | $this->root->listen('\OC\Files', 'postWrite', $fileWriteCallback); 48 | 49 | $fileDeletionCallback = function(\OCP\Files\Node $node) { 50 | if($this->isUserNode($node)) { 51 | if ($node->getType() === FileInfo::TYPE_FOLDER) { 52 | $this->photofilesService->deleteByFolder($node); 53 | } else { 54 | $this->photofilesService->deleteByFile($node); 55 | } 56 | } 57 | }; 58 | $this->root->listen('\OC\Files', 'preDelete', $fileDeletionCallback); 59 | 60 | Util::connectHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', $this, 'restore'); 61 | } 62 | 63 | public static function restore($params) { 64 | $node = $this->getNodeForPath($params['filePath']); 65 | if($this->isUserNode($node)) { 66 | if ($node->getType() === FileInfo::TYPE_FOLDER) { 67 | $this->photofilesService->addByFolder($node); 68 | } else { 69 | $this->photofilesService->addByFile($node); 70 | } 71 | } 72 | } 73 | 74 | private function getNodeForPath($path) { 75 | $user = \OC::$server->getUserSession()->getUser(); 76 | $fullPath = Filesystem::normalizePath('/' . $user->getUID() . '/files/' . $path); 77 | return $this->root->get($fullPath); 78 | } 79 | 80 | /** 81 | * Ugly Hack, find API way to check if file is added by user. 82 | */ 83 | private function isUserNode(\OCP\Files\Node $node) { 84 | //return strpos($node->getStorage()->getId(), "home::", 0) === 0; 85 | return $node->getStorage()->instanceOfStorage('\OC\Files\Storage\Home'); 86 | } 87 | 88 | } -------------------------------------------------------------------------------- /lib/Service/GeophotoService.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright Piotr Bator 2017 10 | */ 11 | 12 | namespace OCA\PhotoMap\Service; 13 | 14 | use OCP\Files\FileInfo; 15 | use OCP\IL10N; 16 | use OCP\Files\IRootFolder; 17 | use OCP\Files\Storage\IStorage; 18 | use OCP\Files\Folder; 19 | use OCA\PhotoMap\DB\Geophoto; 20 | use OCA\PhotoMap\DB\GeophotoMapper; 21 | use OCP\ILogger; 22 | 23 | class GeophotoService { 24 | 25 | private $l10n; 26 | private $root; 27 | private $photoMapper; 28 | private $logger; 29 | 30 | public function __construct (ILogger $logger, IRootFolder $root, IL10N $l10n, GeophotoMapper $photoMapper) { 31 | $this->root = $root; 32 | $this->l10n = $l10n; 33 | $this->photoMapper = $photoMapper; 34 | $this->logger = $logger; 35 | } 36 | 37 | /** 38 | * @param string $userId 39 | * @return array with all notes in the current directory 40 | */ 41 | public function getAllFromDB ($userId) { 42 | $photoEntities = $this->photoMapper->findAll($userId); 43 | $userFolder = $this->getFolderForUser($userId); 44 | $filesById = []; 45 | $foldersById = []; 46 | foreach ($photoEntities as $photoEntity) { 47 | $path = \OC\Files\Filesystem::getPath($photoEntity->getFileId()); 48 | $photoFile = \OC\Files\Filesystem::getFileInfo($path); 49 | $photoFolder = $userFolder->get($path)->getParent(); 50 | $file_object = new \stdClass(); 51 | $file_object->fileId = $photoEntity->getFileId(); 52 | $file_object->lat = $photoEntity->getLat(); 53 | $file_object->lng = $photoEntity->getLng(); 54 | $file_object->dateTaken = $photoEntity->getDateTaken(); 55 | $file_object->folderId = $photoFolder->getId(); 56 | $file_object->path = $this->normalizePath($photoFile); 57 | $filesById[] = $file_object; 58 | $folder_object = new \stdClass(); 59 | $folder_object->id = $photoFolder->getId(); 60 | $folder_object->name = $photoFolder->getName(); 61 | $folder_object->path = $this->normalizePath($photoFolder); 62 | /*$folder_object->filesList = $this->getPhotosListForFolder($photoFolder);*/ 63 | $foldersById[$photoFolder->getId()] = $folder_object; 64 | } 65 | return [$filesById, $foldersById]; 66 | } 67 | 68 | private function normalizePath($node) { 69 | return str_replace("files","", $node->getInternalPath()); 70 | } 71 | 72 | /** 73 | * @param string $userId the user id 74 | * @return Folder 75 | */ 76 | private function getFolderForUser ($userId) { 77 | $path = '/' . $userId . '/files'; 78 | if ($this->root->nodeExists($path)) { 79 | $folder = $this->root->get($path); 80 | } else { 81 | $folder = $this->root->newFolder($path); 82 | } 83 | return $folder; 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /lib/Service/PhotofilesService.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright Piotr Bator 2017 10 | */ 11 | 12 | namespace OCA\PhotoMap\Service; 13 | 14 | use OCP\Files\FileInfo; 15 | use OCP\IL10N; 16 | use OCP\Files\IRootFolder; 17 | use OCP\Files\Storage\IStorage; 18 | use OCP\Files\Folder; 19 | use OCP\Files\Node; 20 | use OCA\PhotoMap\DB\Geophoto; 21 | use OCA\PhotoMap\DB\GeophotoMapper; 22 | use OCP\ILogger; 23 | 24 | class PhotofilesService { 25 | 26 | private $l10n; 27 | private $root; 28 | private $photoMapper; 29 | private $logger; 30 | 31 | public function __construct (ILogger $logger, IRootFolder $root, IL10N $l10n, GeophotoMapper $photoMapper) { 32 | $this->root = $root; 33 | $this->l10n = $l10n; 34 | $this->photoMapper = $photoMapper; 35 | $this->logger = $logger; 36 | } 37 | 38 | public function rescan ($userId){ 39 | $userFolder = $this->root->getUserFolder($userId); 40 | $photos = $this->gatherPhotoFiles($userFolder, true); 41 | $this->photoMapper->deleteAll($userId); 42 | foreach($photos as $photo) { 43 | $this->addPhoto($photo, $userId); 44 | } 45 | } 46 | 47 | public function addByFile(Node $file) { 48 | $userFolder = $this->root->getUserFolder($file->getOwner()->getUID()); 49 | if($this->isPhoto($file)) { 50 | $this->addPhoto($file, $file->getOwner()->getUID()); 51 | } 52 | } 53 | 54 | public function addByFolder(Node $folder) { 55 | $photos = $this->gatherPhotoFiles($folder, true); 56 | foreach($photos as $photo) { 57 | $this->addPhoto($photo, $folder->getOwner()->getUID()); 58 | } 59 | } 60 | 61 | public function deleteByFile(Node $file) { 62 | $this->photoMapper->deleteByFileId($file->getId()); 63 | } 64 | 65 | public function deleteByFolder(Node $folder) { 66 | $photos = $this->gatherPhotoFiles($folder, true); 67 | foreach($photos as $photo) { 68 | $this->photoMapper->deleteByFileId($photo->getId()); 69 | } 70 | } 71 | 72 | private function addPhoto($photo, $userId) { 73 | $exif = $this->getExif($photo); 74 | if (!is_null($exif) AND !is_null($exif->lat)) { 75 | $photoEntity = new Geophoto(); 76 | $photoEntity->setFileId($photo->getId()); 77 | $photoEntity->setLat($exif->lat); 78 | $photoEntity->setLng($exif->lng); 79 | $photoEntity->setUserId($userId); 80 | $photoEntity->setDateTaken($exif->dateTaken); 81 | $this->photoMapper->insert($photoEntity); 82 | } 83 | } 84 | 85 | private function normalizePath($node) { 86 | return str_replace("files","", $node->getInternalPath()); 87 | } 88 | 89 | public function getPhotosByFolder($userId, $path) { 90 | $userFolder = $this->root->getUserFolder($userId); 91 | $folder = $userFolder->get($path); 92 | return $this->getPhotosListForFolder($folder); 93 | } 94 | 95 | private function getPhotosListForFolder($folder) { 96 | $FilesList = $this->gatherPhotoFiles($folder, false); 97 | $notes = []; 98 | foreach($FilesList as $File) { 99 | $file_object = new \stdClass(); 100 | $file_object->fileId = $File->getId(); 101 | $file_object->path = $this->normalizePath($File); 102 | $notes[] = $file_object; 103 | } 104 | return $notes; 105 | } 106 | 107 | private function gatherPhotoFiles ($folder, $recursive) { 108 | $notes = []; 109 | $nodes = $folder->getDirectoryListing(); 110 | foreach($nodes as $node) { 111 | if($node->getType() === FileInfo::TYPE_FOLDER AND $recursive) { 112 | $notes = array_merge($notes, $this->gatherPhotoFiles($node, $recursive)); 113 | continue; 114 | } 115 | if($this->isPhoto($node)) { 116 | $notes[] = $node; 117 | } 118 | } 119 | return $notes; 120 | } 121 | 122 | private function isPhoto($file) { 123 | $allowedExtensions = ['jpg', 'jpeg']; 124 | 125 | if($file->getType() !== \OCP\Files\FileInfo::TYPE_FILE) return false; 126 | if(!in_array( 127 | pathinfo($file->getName(), PATHINFO_EXTENSION), 128 | $allowedExtensions 129 | )) return false; 130 | 131 | return true; 132 | } 133 | 134 | private function hasExifGeoTags($exif) { 135 | if (!isset($exif["GPSLatitude"]) OR !isset($exif["GPSLongitude"])) { 136 | return false; 137 | } 138 | if (count($exif["GPSLatitude"]) != 3 OR count($exif["GPSLongitude"]) != 3) { 139 | return false; 140 | } 141 | return true; 142 | } 143 | 144 | private function getExif($file) { 145 | $path = $file->getStorage()->getLocalFile($file->getInternalPath()); 146 | $exif = @exif_read_data($path); 147 | //Check photos are on the earth 148 | if($this->hasExifGeoTags($exif) AND $exif["GPSLatitude"][0]<90 AND $exif["GPSLongitude"][0]<180){ 149 | 150 | //Check photos are not on NULL island, remove if they should be. 151 | if($exif["GPSLatitude"][0]!=0 OR $exif["GPSLatitude"][1]!=0 OR $exif["GPSLongitude"][0]!=0 OR $exif["GPSLongitude"][1]!=0){ 152 | //Check if there is exif infor 153 | $LatM = 1; $LongM = 1; 154 | if($exif["GPSLatitudeRef"] == 'S'){ 155 | $LatM = -1; 156 | } 157 | if($exif["GPSLongitudeRef"] == 'W'){ 158 | $LongM = -1; 159 | } 160 | //get the GPS data 161 | $gps['LatDegree']=$exif["GPSLatitude"][0]; 162 | $gps['LatMinute']=$exif["GPSLatitude"][1]; 163 | $gps['LatgSeconds']=$exif["GPSLatitude"][2]; 164 | $gps['LongDegree']=$exif["GPSLongitude"][0]; 165 | $gps['LongMinute']=$exif["GPSLongitude"][1]; 166 | $gps['LongSeconds']=$exif["GPSLongitude"][2]; 167 | 168 | //convert strings to numbers 169 | foreach($gps as $key => $value){ 170 | $pos = strpos($value, '/'); 171 | if($pos !== false){ 172 | $temp = explode('/',$value); 173 | $gps[$key] = $temp[0] / $temp[1]; 174 | } 175 | } 176 | $file_object = new \stdClass(); 177 | //calculate the decimal degree 178 | $file_object->lat = $LatM * ($gps['LatDegree'] + ($gps['LatMinute'] / 60) + ($gps['LatgSeconds'] / 3600)); 179 | $file_object->lng = $LongM * ($gps['LongDegree'] + ($gps['LongMinute'] / 60) + ($gps['LongSeconds'] / 3600)); 180 | if (isset($exif["DateTimeOriginal"])) { 181 | $file_object->dateTaken = strtotime($exif["DateTimeOriginal"]); 182 | } 183 | return $file_object; 184 | } 185 | } 186 | return null; 187 | } 188 | 189 | } 190 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "node ./node_modules/better-npm-run build", 4 | "lint": "eslint -c .eslintrc js", 5 | "watch-client": "node ./node_modules/better-npm-run watch-client", 6 | "proxy": "node ./node_modules/better-npm-run proxy", 7 | "dev": "concurrent --kill-others \"npm run watch-client\" \"npm run proxy\"", 8 | "configure": "node ./configure.js" 9 | }, 10 | "betterScripts": { 11 | "build": { 12 | "command": "webpack --verbose --colors --display-error-details --config webpack/prod.config.js", 13 | "env": { 14 | "NODE_ENV": "production" 15 | } 16 | }, 17 | "proxy": { 18 | "command": "node webpack/dev-proxy.js", 19 | "env": { 20 | "NODE_PATH": "./js", 21 | "NODE_ENV": "development" 22 | } 23 | }, 24 | "watch-client": { 25 | "command": "node webpack/webpack-dev-server.js", 26 | "env": { 27 | "UV_THREADPOOL_SIZE": 100, 28 | "NODE_PATH": "./js" 29 | } 30 | } 31 | }, 32 | "devDependencies": { 33 | "autoprefixer-loader": "^3.2.0", 34 | "babel-core": "6.8.0", 35 | "babel-eslint": "^6.0.4", 36 | "babel-loader": "6.2.4", 37 | "babel-plugin-transform-async-to-module-method": "^6.8.0", 38 | "babel-plugin-transform-class-properties": "^6.8.0", 39 | "babel-preset-es2015": "^6.6.0", 40 | "babel-preset-react": "^6.5.0", 41 | "babel-runtime": "6.6.1", 42 | "better-npm-run": "0.0.8", 43 | "clean-webpack-plugin": "^0.1.3", 44 | "concurrently": "2.0.0", 45 | "css-loader": "^0.23.1", 46 | "eslint": "^2.9.0", 47 | "eslint-config-airbnb": "8.0.0", 48 | "eslint-plugin-react": "^5.0.1", 49 | "express": "^4.13.3", 50 | "express-http-proxy": "^0.6.0", 51 | "extract-text-webpack-plugin": "^1.0.1", 52 | "http-proxy": "^1.11.1", 53 | "json-loader": "0.5.4", 54 | "less": "^2.5.1", 55 | "less-loader": "^2.2.0", 56 | "prompt": "^1.0.0", 57 | "react-a11y": "0.3.3", 58 | "react-hot-loader": "1.3.0", 59 | "replace": "^0.3.0", 60 | "strip-loader": "^0.1.0", 61 | "style-loader": "^0.13.1", 62 | "webpack": "^1.9.11", 63 | "webpack-dev-server": "1.14.1" 64 | }, 65 | "dependencies": { 66 | "babel": "6.5.2", 67 | "babel-loader": "^6.2.4", 68 | "babel-plugin-typecheck": "3.8.0", 69 | "babel-runtime": "^6.6.1", 70 | "bluebird": "^3.3.5", 71 | "oc-react-components": "^0.2.0", 72 | "react": "^15.0.2", 73 | "react-dom": "^15.0.2" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /phpunit.integration.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ./tests/Integration 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ./tests/Unit 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatPio/PhotoMap/6caa82d9ce5e609a9c8f21986d1c7f9f42c37d5c/screenshots/1.png -------------------------------------------------------------------------------- /screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BatPio/PhotoMap/6caa82d9ce5e609a9c8f21986d1c7f9f42c37d5c/screenshots/2.png -------------------------------------------------------------------------------- /templates/content/index.php: -------------------------------------------------------------------------------- 1 |
    -------------------------------------------------------------------------------- /templates/index.php: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 |
    18 |
    19 | inc('navigation/index')); ?> 20 | inc('settings/index')); ?> 21 |
    22 | 23 |
    24 |
    25 |
    26 |
    27 | -------------------------------------------------------------------------------- /templates/navigation/index.php: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
    4 | 5 | 6 | -------------------------------------------------------------------------------- /templates/settings/index.php: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | 6 |
    7 |
    8 |

    9 |

    10 | 11 |

    12 |
    13 | -------------------------------------------------------------------------------- /tests/Integration/AppTest.php: -------------------------------------------------------------------------------- 1 | container = $app->getContainer(); 22 | } 23 | 24 | public function testAppInstalled() { 25 | $appManager = $this->container->query('OCP\App\IAppManager'); 26 | $this->assertTrue($appManager->isInstalled('photomap')); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /tests/Unit/Controller/PageControllerTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder('OCP\IRequest')->getMock(); 16 | 17 | $this->controller = new PageController( 18 | 'photomap', $request, $this->userId 19 | ); 20 | } 21 | 22 | public function testIndex() { 23 | $result = $this->controller->index(); 24 | 25 | $this->assertEquals('index', $result->getTemplateName()); 26 | $this->assertTrue($result instanceof TemplateResponse); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | addValidRoot(OC::$SERVERROOT . '/tests'); 11 | 12 | // Fix for "Autoload path not allowed: .../photomap/tests/testcase.php" 13 | \OC_App::loadApp('photomap'); 14 | 15 | if(!class_exists('PHPUnit_Framework_TestCase')) { 16 | require_once('PHPUnit/Autoload.php'); 17 | } 18 | 19 | OC_Hook::clear(); 20 | -------------------------------------------------------------------------------- /webpack/dev-proxy.js: -------------------------------------------------------------------------------- 1 | var httpProxy = require('http-proxy'); 2 | var config = require('./dev.config'); 3 | var host = 'localhost'; 4 | var port = parseInt(process.env.PORT) || 3000; 5 | var url = require('url'); 6 | 7 | var app = require('express')(); 8 | 9 | 10 | var ocRoot = url.parse(config.ocRoot); 11 | var proxyRoot = ocRoot; 12 | proxyRoot.path = ''; 13 | 14 | var proxy = httpProxy.createProxyServer({ 15 | target: proxyRoot 16 | }); 17 | 18 | var webPackProxy = httpProxy.createProxyServer({ 19 | target: 'http://localhost:' + config.webPackPort 20 | }); 21 | 22 | proxy.on('proxyRes', function (proxyRes, req, res, options) { 23 | if (proxyRes.headers['content-security-policy']) { 24 | // allow the webpack sockets and javascript 25 | var post = 'localhost:' + config.webPackPort + ' '; 26 | proxyRes.headers['content-security-policy'] = proxyRes.headers['content-security-policy'] 27 | .replace('connect-src ', 'connect-src * ') 28 | .replace('style-src ', 'style-src * blob: ') 29 | .replace('script-src ', 'script-src ' + post); 30 | } 31 | }); 32 | 33 | var pathTest = new RegExp(config.appId + '(/js/main.js)'); 34 | 35 | app.use('/', function (req, res) { 36 | var path = req.path.substr(ocRoot.pathname.length); 37 | var matches = path.match(pathTest); 38 | if (matches) { 39 | req.url = matches[1]; 40 | webPackProxy.web(req, res); 41 | } else { 42 | proxy.web(req, res); 43 | } 44 | }); 45 | 46 | app.listen(port, function () { 47 | console.info('==> 🚧 Webpack proxy server listening on %s:%s', host, port); 48 | // print this last 49 | setTimeout(function () { 50 | console.info('----------\n==> 💻 Open http://localhost:%s%s in a browser.', port, ocRoot.pathname); 51 | }, 500); 52 | }); 53 | -------------------------------------------------------------------------------- /webpack/dev.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var assetsPath = path.resolve(__dirname, '../js'); 4 | var host = 'localhost'; 5 | var port = parseInt(process.env.PORT) + 1 || 3001; 6 | var appId = 'photomap' 7 | 8 | module.exports = { 9 | ocRoot: 'http://localhost/', 10 | appId: 'photomap', 11 | 12 | webPackPort: port, 13 | devtool: 'inline-source-map', 14 | context: path.resolve(__dirname, '..'), 15 | entry: { 16 | 'main': [ 17 | 'webpack-dev-server/client?http://' + host + ':' + port, 18 | 'webpack/hot/only-dev-server', 19 | './js_src/index.js' 20 | ] 21 | }, 22 | output: { 23 | path: assetsPath, 24 | filename: '[name].js', 25 | chunkFilename: '[name]-[chunkhash].js', 26 | publicPath: '/js/' 27 | }, 28 | module: { 29 | loaders: [ 30 | { 31 | test: /\.(jpe?g|png|gif|svg)$/, 32 | loader: 'url', 33 | query: {limit: 10240} 34 | }, 35 | { 36 | test: /\.js$/, 37 | exclude: /node_modules/, 38 | loaders: ['react-hot', 'babel-loader'] 39 | }, 40 | {test: /\.json$/, loader: 'json-loader'}, 41 | { 42 | test: /\.css$/, 43 | loader: 'style!css?modules&importLoaders=2&sourceMap&localIdentName=[local]___[hash:base64:5]!autoprefixer?browsers=last 2 version!' 44 | }, 45 | { 46 | test: /\.less$/, 47 | loader: 'style!css?modules&importLoaders=2&sourceMap&localIdentName=[local]___[hash:base64:5]!autoprefixer?browsers=last 2 version!less' 48 | } 49 | ] 50 | }, 51 | progress: true, 52 | resolve: { 53 | modulesDirectories: [ 54 | 'src', 55 | 'node_modules' 56 | ], 57 | extensions: ['', '.json', '.js'] 58 | }, 59 | plugins: [ 60 | // hot reload 61 | new webpack.HotModuleReplacementPlugin(), 62 | new webpack.IgnorePlugin(/\.json$/), 63 | new webpack.NoErrorsPlugin(), 64 | new webpack.DefinePlugin({ 65 | __CLIENT__: true, 66 | __SERVER__: false, 67 | __DEVELOPMENT__: true, 68 | __DEVTOOLS__: false // <-------- DISABLE redux-devtools HERE 69 | }) 70 | ] 71 | }; 72 | -------------------------------------------------------------------------------- /webpack/prod.config.js: -------------------------------------------------------------------------------- 1 | // Webpack config for creating the production bundle. 2 | 3 | var path = require('path'); 4 | var webpack = require('webpack'); 5 | var CleanPlugin = require('clean-webpack-plugin'); 6 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 7 | var strip = require('strip-loader'); 8 | 9 | var relativeAssetsPath = '../js'; 10 | var assetsPath = path.join(__dirname, relativeAssetsPath); 11 | 12 | module.exports = { 13 | devtool: 'source-map', 14 | context: path.resolve(__dirname, '..'), 15 | entry: { 16 | 'main': './js_src/index.js' 17 | }, 18 | output: { 19 | path: assetsPath, 20 | filename: '[name].js', 21 | chunkFilename: '[name]-[chunkhash].js', 22 | publicPath: '/js/' 23 | }, 24 | module: { 25 | loaders: [ 26 | { 27 | test: /\.(jpe?g|png|gif|svg)$/, 28 | loader: 'url', 29 | query: {limit: 10240} 30 | }, 31 | { 32 | test: /\.js$/, 33 | exclude: /node_modules/, 34 | loaders: [strip.loader('debug'), 'babel-loader'] 35 | }, 36 | {test: /\.json$/, loader: 'json-loader'}, 37 | { 38 | test: /\.css$/, 39 | loader: ExtractTextPlugin.extract('style', 'css?modules&importLoaders=2&sourceMap!autoprefixer?browsers=last 2 version') 40 | }, 41 | { 42 | test: /\.less$/, 43 | loader: ExtractTextPlugin.extract('style', 'css?modules&importLoaders=2&sourceMap!autoprefixer?browsers=last 2 version!less') 44 | } 45 | ] 46 | }, 47 | progress: true, 48 | resolve: { 49 | modulesDirectories: [ 50 | 'src', 51 | 'node_modules' 52 | ], 53 | extensions: ['', '.json', '.js'] 54 | }, 55 | plugins: [ 56 | new CleanPlugin([relativeAssetsPath]), 57 | new ExtractTextPlugin("[name].css"), 58 | new webpack.DefinePlugin({ 59 | __CLIENT__: true, 60 | __SERVER__: false, 61 | __DEVELOPMENT__: false, 62 | __DEVTOOLS__: false 63 | }), 64 | 65 | // ignore dev config 66 | new webpack.IgnorePlugin(/\.\/dev/, /\/config$/), 67 | 68 | // set global vars 69 | new webpack.DefinePlugin({ 70 | 'process.env': { 71 | // Useful to reduce the size of client-side libraries, e.g. react 72 | NODE_ENV: JSON.stringify('production') 73 | } 74 | }), 75 | 76 | // optimizations 77 | new webpack.optimize.DedupePlugin(), 78 | new webpack.optimize.OccurenceOrderPlugin(), 79 | new webpack.optimize.UglifyJsPlugin({ 80 | compress: { 81 | warnings: false 82 | } 83 | }) 84 | ] 85 | }; 86 | -------------------------------------------------------------------------------- /webpack/utils/notifyStats.js: -------------------------------------------------------------------------------- 1 | function notifyError(error) { 2 | // BELLs when something goes wrong! 3 | console.log("\x07" + error); 4 | } 5 | 6 | function notifyWarning(warning) { 7 | console.log(warning); 8 | } 9 | 10 | module.exports = function notifyStats(stats) { 11 | var json = stats.toJson(); 12 | if (json.errors.length > 0) { 13 | json.errors.forEach(notifyError); 14 | } else if (json.warnings.length > 0) { 15 | json.warnings.forEach(notifyWarning); 16 | } else { 17 | console.log(stats.toString({ 18 | chunks: false, 19 | colors: true 20 | })); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /webpack/utils/writeStats.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | path = require('path'), 3 | filepath = path.resolve(__dirname, '../../webpack-stats.json'); 4 | 5 | module.exports = function writeStats(stats, env) { 6 | 7 | var publicPath = this.options.output.publicPath; 8 | 9 | var json = stats.toJson(); 10 | 11 | // get chunks by name and extensions 12 | function getChunks(name, ext) { 13 | ext = ext || 'js'; 14 | var chunk = json.assetsByChunkName[name]; 15 | 16 | // a chunk could be a string or an array, so make sure it is an array 17 | if (!(Array.isArray(chunk))) { 18 | chunk = [chunk]; 19 | } 20 | 21 | return chunk 22 | // filter by extension 23 | .filter(function(chunkName) { 24 | return path.extname(chunkName) === '.' + ext; 25 | }) 26 | .map(function(chunkName) { 27 | return publicPath + chunkName; 28 | }); 29 | } 30 | 31 | var script = getChunks('main', 'js'); 32 | var cssFiles = getChunks('main', 'css'); 33 | 34 | var cssModules = {}; 35 | 36 | json.modules.filter(function(m) { 37 | if (env === 'prod') { 38 | return /\.scss$/.test(m.name); 39 | } 40 | //filter by modules with '.scss' inside name string, that also have name and moduleName that end with 'ss'(allows for css, less, sass, and scss extensions) 41 | //this ensures that the proper scss module is returned, so that namePrefix variable is no longer needed 42 | return (/\.scss$/.test(m.name) && m.name.slice(-2) === 'ss' && m.reasons[0].moduleName.slice(-2) === 'ss'); 43 | }).forEach(function(m) { 44 | //find index of '/src' inside the module name, slice it and resolve path 45 | var srcIndex = m.name.indexOf('/src'); 46 | var name = path.resolve(__dirname, '../../', m.name.slice(srcIndex + '/src'.length)); 47 | if (name) { 48 | // Resolve the e.g.: "C:\" issue on windows 49 | const i = name.indexOf(':'); 50 | if (i >= 0) { 51 | name = name.slice(i + 1); 52 | } 53 | } 54 | //end 55 | if (m.source) { 56 | var regex = env === 'prod' ? /module\.exports = ((.|\n)+);/ : /exports\.locals = ((.|\n)+);/; 57 | var match = m.source.match(regex); 58 | cssModules[name] = match ? JSON.parse(match[1]) : {}; 59 | } 60 | }); 61 | 62 | // Find compiled images in modules 63 | // it will be used to map original filename to the compiled one 64 | // for server side rendering 65 | const imagesRegex = /\.(jpe?g|png|gif|svg)$/; 66 | const images = json.modules 67 | .filter(function(module) { 68 | return imagesRegex.test(module.name); 69 | }) 70 | .map(function(image) { 71 | var i = image.source.indexOf('"'); 72 | var imageSource = image.source.slice(i + 1, -1); 73 | imageSource = imageSource.lastIndexOf('data:image', 0) === 0 ? imageSource : publicPath + imageSource; 74 | return { 75 | original: image.name, 76 | compiled: imageSource 77 | }; 78 | }); 79 | 80 | var content = { 81 | script: script, 82 | css: { 83 | files: cssFiles, 84 | modules: cssModules 85 | }, 86 | images: images 87 | }; 88 | 89 | fs.writeFileSync(filepath, JSON.stringify(content)); 90 | 91 | }; 92 | -------------------------------------------------------------------------------- /webpack/webpack-dev-server.js: -------------------------------------------------------------------------------- 1 | var WebpackDevServer = require('webpack-dev-server'), 2 | webpack = require('webpack'), 3 | config = require('./dev.config'), 4 | host = process.env.HOST || 'localhost', 5 | port = parseInt(process.env.PORT) + 1 || 3001, 6 | serverOptions = { 7 | contentBase: config.ocRoot + '/' + config.appId + '/', 8 | quiet: true, 9 | noInfo: true, 10 | hot: true, 11 | inline: true, 12 | lazy: false, 13 | publicPath: config.output.publicPath, 14 | headers: {"Access-Control-Allow-Origin": "*"}, 15 | stats: {colors: true} 16 | }, 17 | compiler = webpack(config, function (err, stats) { 18 | var json = stats.toJson(); 19 | if (json.errors.length) 20 | console.error(json.errors[0]) 21 | }), 22 | webpackDevServer = new WebpackDevServer(compiler, serverOptions); 23 | 24 | webpackDevServer.listen(port, host, function () { 25 | console.info('==> 🚧 Webpack development server listening on %s:%s', host, port); 26 | }); 27 | --------------------------------------------------------------------------------