diff --git a/.gitignore b/.gitignore index a10f084..116d048 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,11 @@ -truncated-data/ data/ +out/ +tmp/ +clip.gif +slippage.png +test.sh +batch.sh .stack-work/ -.idea/ -*.csv -R.Rproj -.Rproj.user +.Rproj.user/ +*.Rproj .Rhistory diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..50ae7b9 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Revision history for sync.soccer + +## 0.1.0.0 -- 2020-07-18 + +* First version. diff --git a/LICENSE b/LICENSE index 7876b83..dba13ed 100644 --- a/LICENSE +++ b/LICENSE @@ -1 +1,661 @@ -Copyright Allan Clark and Marek Kwiatkowski (c) 2018-2019. All rights reserved. + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/R/animate.R b/R/animate.R deleted file mode 100644 index f32034a..0000000 --- a/R/animate.R +++ /dev/null @@ -1,53 +0,0 @@ -library(readr, quietly=TRUE) -library(dplyr, quietly=TRUE) -library(ggplot2, quietly=TRUE) -library(tidyr, quietly=TRUE) -library(gganimate) - -# To install ggsoccer: -# > install.packages("devtools") -# > devtools::install_github("torvaney/ggsoccer") -library(ggsoccer) - -frames.file <- "../data/csv/frames.csv" -events.file <- "../data/csv/events.csv" -sync.file <- "../data/csv/sync.csv" - -data <- read_csv(frames.file) %>% - mutate(is.ball=(object == 0)) %>% - filter(team %in% c(0,1)) %>% # Drop officials - left_join(read_csv(sync.file)) %>% - mutate( # Keep events matched to the ball rows only - event=ifelse(is.ball, event, NA), - is.event=!is.na(event) - ) %>% - left_join(read_csv(events.file), by="event", suffix=c(".f", ".e")) %>% - mutate( - team.f=factor(ifelse(is.ball, -1, team.f)), # Give the ball its own "team" - desc.f=sprintf("Implied Tracab clock: %.2fs", clock), - desc.e=ifelse(is.event, sprintf("%02d:%02d %s", minute, second, event_type), "") - ) %>% - # Repeat the frames that are aligned to an event 20 times to simulate a pause - mutate(weight=ifelse(is.event, 20, 1)) %>% - uncount(weight) %>% - arrange(clock, object) %>% - mutate(animation.clock=cumsum(as.numeric(is.ball))) - -# Take only an initial segment of data for animation development -data <- data %>% filter(clock > 60*0.5 & clock < 60*1) - -animation <- ggplot(data, aes(x=x.f, y=y.f)) + - geom_text(data=filter(data, is.ball), aes(x=-3500, y=3550, label=desc.f)) + - geom_text(aes(x=3500, y=3550, label=desc.e), colour='red') + - annotate_pitch(x_scale=105.0, y_scale=68.0, x_shift=-10500/2, y_shift=-6800/2) + - annotate("text", x=4000, y=-3200, label="info@sync.soccer", fontface="italic") + - geom_point(data=filter(data, is.ball), size=1.5) + - geom_point(data=filter(data, !is.ball), aes(color=team.f), size=3, alpha=0.75) + - geom_point(aes(x=x.e, y=y.e), colour="red", shape=4, size=4) + - scale_color_manual(values=c("#034694", "#6cabdd")) + - coord_cartesian(xlim=c(-5500, 5500), ylim=c(-3500, 3500)) + - transition_time(animation.clock) + - theme_pitch() + - theme(legend.position="none") - -animate(animation, fps=5, nframes=nrow(filter(data, is.ball)), renderer=ffmpeg_renderer()) diff --git a/README.md b/README.md index 59c322c..bd755a7 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,207 @@ +![](doc/header-clip.gif) +# sync.soccer ![badge-experimental] -I'm keeping the data in a directory: `data`, and for development purposes I'm keeping truncated data in `truncated-data`. -You can truncate data file with: +sync.soccer synchronises (or _aligns_) soccer +datasets, so that an event in one dataset is matched to the corresponding +event or snapshot in the other. This repository contains an +implementation that aligns Opta's +(now STATS Perform's) F24 feeds to ChyronHego's Tracab files. More +formats may be added in the future. The heart of sync.soccer is the +[Needleman-Wunsch][link-nwwiki] dynamic programming algorithm. For a +fuller explanation of the method see separate [blogpost][link-blog]. -``` -head -n 400 data/tracab/803174_Man\ City-Chelsea.dat > truncated-data/tracab/803174_ManCity-Chelsea.dat -``` -assuming `truncated-data/tracab` exists. +## Table of contents +### [Installation](#installation)
+### [Usage](#usage) +### [Known issues and troubleshooting](#troubleshooting) +* [Missing Tracab metadata](#missing-metadata) +* [Performace and system requirements](#performance) +* [Unsatisfactory alignments](#bad-alignments) +### [License](#license) -# Building + -I installed stack very simply with the instructions here: +## Installation -https://docs.haskellstack.org/en/stable/README/ +sync.soccer has been developed and tested on Ubuntu Linux. +Feedback on compilation and performance on other platforms is +welcome. To build sync.soccer, you need [Stack][link-stack]. +Then, to download and compile, say: +```sh +$ git clone git@github.com:huffyhenry/sync.soccer.git +$ cd sync.soccer +$ stack build ``` -stack build -``` + +## Usage -# Beginnings of a synchronisation program +If the build was successful, you can run sync.soccer like so +(note the double dash, which tells Stack that all arguments that follow +are meant for the sync-soccer executable and not for Stack itself): +``` +$ stack exec -- sync-soccer --help -Assuming you have both appropriate datafiles after: +Usage: sync-soccer TRACAB-META TRACAB-DATA F7 F24 OUTPUT [-t|--time-only] + [-u|--timestamp] [-s|--show-sync] [-c|--scale-clock X] + [-l|--scale-location X] [-p|--scale-player X] + [-b|--scale-ball X] [-e|--event-csv FILEPATH] + [-f|--frame-csv FILEPATH] + Synchronise Tracab and F24 data. +Available options: + -t,--time-only Sync only by time + -u,--timestamp Use F24 event timestamp instead of min:sec + -s,--show-sync Print human-readable sync on screen + -c,--scale-clock X Clock mismatch leading to unit penalty [s, def=1] + -l,--scale-location X Location mismatch leading to unit penalty [m, def=5] + -p,--scale-player X Player-ball gap leading to unit penalty [m, def=1] + -b,--scale-ball X Penalty for syncing to dead-ball frame [def=5] + -e,--event-csv FILEPATH Location to save F24 events CSV + -f,--frame-csv FILEPATH Location to save Tracab frames CSV + -h,--help Show this help text ``` -stack build -``` +You must specify five file paths: to Tracab metadata, +Tracab payload, F7 file (essentially F24 metadata), F24 file and +a path to the output file where the completed alignment will be saved +as a CSV of event and frame IDs. -succeeds you can do: +The meaning of the optional arguments is as follows: +* `--time-only` discards all the added value of the algorithm and matches + the events to the frame nearest to it in time. Useful for debugging, + comparisons or when the algorithm misbehaves on a particuar game. +* `--timestamp` uses the F24 event timestamp instead of minute and second + as the time of the event. +* `--show-sync` will print the alignment on screen. It will look something + like this: + ``` + Event (67:12 Pass) Frame (implied clock: 67:12.920) + --gap length 55-- + Event (67:15 Pass) Frame (implied clock: 67:15.160) + --gap length 30-- + Event (67:16 Clearance) Frame (implied clock: 67:16.400) + --gap length 26-- + Event (67:17 Ball recovery) Frame (implied clock: 67:17.480) + Event (67:18 Pass) Frame (implied clock: 67:17.520) + --gap length 35-- + Event (67:19 Pass) Frame (implied clock: 67:18.960) + --gap length 50-- + Event (67:21 Pass) Frame (implied clock: 67:21.000) + --gap length 118-- + Event (67:26 Pass) Frame (implied clock: 67:25.760) + --gap length 104-- + Event (67:30 Pass) Frame (implied clock: 67:29.960) + --gap length 42-- + Event (67:32 Miss) Frame (implied clock: 67:31.680) + ``` +* The parameters starting with `--scale` control the _relative_ importance of the + four components of the event-frame similarity score: difference of time, location, + distance between the player identified by the event and the ball, and ball + status (alive/dead). The default values were chosen by trial and error, and + force the algorithm to penalise equally a 5 metre discrepancy in location, + 1 second discrepancy in time, and 1 metre distance between the active player + and the ball. Because the similarity score between any event and any frame + falls in (0, 1), the default `--scale-ball` value of 5 means that the algorithm + can never align to a dead-ball frame. +* `--event-csv` and `--frame-csv` write the event and frame data to files + in a format compatible with the visualisation scripts in `viz/`. + + + +## Known issues and troubleshooting + + + +### Missing Tracab metadata +You may have received a Tracab file without the corresponding metadata. +Tracab metadata is a small XML file that nevertheless contains +essential information for the synchronisation algorithm: pitch size, +frame rate and the beginning and end of the first and second half. +If you know +these parameters or can deduce them from raw data, you can mock up a +metadata file based on this template: + +```xml + + + + + + + + ``` -stack exec sync-soccer f24filename tracabfilename -``` +Currently, sync.soccer cares only about the `iFrameRateFps`, +`iStartFrame`, `iEndFrame`, `fPitchXSizeMeters` and `fPitchYSizeMeters` +values. The rest can be set to 0. In fact, you only _really_ need +the `iStartFrame` values: `iEndFrame` can safely be set to something +corresponding to a time after the half ended (at a small performace cost), +frame rate is usually (always?) 25, and you may get decent enough results +using the standard pitch size of 105x68 metres (and perhaps increasing the +`--scale-location` value). + + + +### Performance and system requirements +The core algorithm needs to build two ~75'000 (# of Tracab frames) x +~800 (# of F24 events) matrices, one for each half of the match, +to find the optimal alignment. This requires time and space. +On my machine, the program needs 5-10 minutes and ~10 GB of RAM to +synchronise a single game. There is a lot of low-hanging fruit to be +picked in terms of optimisation, and I expect future versions of the +software to be much more streamlined. Parallel synchronisation of +game halves is also in the works. + + + +### Unsatisfactory alignments +Sometimes an alignment produced by sync.soccer contains segments +that are demonstrably worse than if synchronisation were performed +purely on the basis of time. They are typically a result of discrepancies +between the F24 and Tracab data that are greater than is reasonable, +for example due to very inaccurate F24 event location or even +Tracab ball tracking error. In such situations, the algorithm may align +event(s) to frame(s) that are a significant time apart, but where +(by chance) the +locations agree better. Such mis-alignment can then have a knock-on +effect on the next several seconds of data, until everything straightens +out. + +One solution in these situations is to increase the `--scale-location` +parameter, making the algorithm less sensitive to differences in +event and ball locations. Another is to run a second synchronisation +with `--time-only` and manually (or using a script) merge the two sync +files so that the +problematic events are aligned only based on time. The time slippage +plot in `viz/slippage.R` can help identify the +potentially problematic segments, which can then be animated with +`viz/animate.R` to see what is going on. + +Such pathological situations need to be distinguished from those +when the algorithm produces an imperfect but reasonable +alignment. These may eventually be corrected by developing smarter, +more granular event-frame agreement scoring functions. + + + +## License +Copyright (c) 2018-2020 Allan Clark and Marek Kwiatkowski. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, +or (at your option) any later version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +See the GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License along with this program. If not, see . + -It currently just prints out how many lines/events there are. Even this, will take a while. +[header-clip]: doc/header-clip.gif +[badge-experimental]: https://img.shields.io/badge/lifecycle-proof%20of%20concept-important +[link-nwwiki]: https://en.wikipedia.org/wiki/Needleman%E2%80%93Wunsch_algorithm +[link-blog]: https://kwiatkowski.io/sync.soccer +[link-stack]: https://docs.haskellstack.org/en/stable/README/ \ No newline at end of file diff --git a/app/SyncMain.hs b/app/SyncMain.hs new file mode 100644 index 0000000..bbc60e1 --- /dev/null +++ b/app/SyncMain.hs @@ -0,0 +1,105 @@ +import Control.Monad (when) +import Options.Applicative +import Data.Semigroup ((<>)) +import qualified Tracab as Tcb +import qualified F24 +import qualified NeedlemanWunsch as NW +import qualified Csvs as CSV +import qualified Scoring + + +-- Command line parsing machinery +data Options = Options { + tcbMetaFile :: String, + tcbDataFile :: String, + f7File :: String, + f24File :: String, + outputFile :: String, + timeOnly :: Bool, + timestamp :: Bool, + showsync :: Bool, + cScale :: Double, + lScale :: Double, + pScale :: Double, + bScale :: Double, + eventCSV :: String, + frameCSV :: String +} deriving Show + +options :: Parser Options +options = Options + <$> argument str (metavar "TRACAB-META") + <*> argument str (metavar "TRACAB-DATA") + <*> argument str (metavar "F7") + <*> argument str (metavar "F24") + <*> argument str (metavar "OUTPUT") + <*> switch (long "time-only" <> short 't' <> help "Sync only by time") + <*> switch (long "timestamp" <> short 'u' + <> help "Use F24 event timestamp instead of min:sec") + <*> switch (long "show-sync" <> short 's' + <> help "Print human-readable sync on screen") + <*> option auto (long "scale-clock" <> short 'c' <> value 1 <> metavar "X" + <> help "Clock mismatch leading to unit penalty [s, def=1]") + <*> option auto (long "scale-location" <> short 'l' <> value 5 <> metavar "X" + <> help "Location mismatch leading to unit penalty [m, def=5]") + <*> option auto (long "scale-player" <> short 'p' <> value 1 <> metavar "X" + <> help "Player-ball gap leading to unit penalty\n [m, def=1]") + <*> option auto (long "scale-ball" <> short 'b' <> value 5 <> metavar "X" + <> help "Penalty for syncing to dead-ball frame [def=5]") + <*> strOption (long "event-csv" <> short 'e' <> value "" <> metavar "FILEPATH" + <> help "Location to save F24 events CSV") + <*> strOption (long "frame-csv" <> short 'f' <> value "" <> metavar "FILEPATH" + <> help "Location to save Tracab frames CSV") + +parseOptions :: IO Options +parseOptions = let desc = "Synchronise Tracab and F24 data." + in execParser $ info (options <**> helper) (fullDesc <> progDesc desc) + +main :: IO () +main = do + opts <- parseOptions + tbMeta <- Tcb.parseMetaFile (tcbMetaFile opts) + tbData <- Tcb.parseDataFile tbMeta (tcbDataFile opts) + f24Meta <- F24.parseMetaFile (f7File opts) + f24Raw <- F24.loadGameFromFile (f24File opts) + let f24Data = F24.convertGameCoordinates tbMeta tbData f24Raw + + -- Select events to sync in both halves + let syncable pid e = F24.period_id e == pid && F24.isOTB e + let events1 = filter (syncable 1) (F24.events f24Data) + let events2 = filter (syncable 2) (F24.events f24Data) + + -- Select frames to sync to + let p1start = (Tcb.startFrame . head . Tcb.periods) tbMeta + let p2start = (Tcb.startFrame . head . tail . Tcb.periods) tbMeta + let p1end = (Tcb.endFrame . head . Tcb.periods) tbMeta + let p2end = (Tcb.endFrame . head . tail . Tcb.periods) tbMeta + let frames1 = filter (\f -> (Tcb.frameId f <= p1end) && (Tcb.frameId f >= p1start)) tbData + let frames2 = filter (\f -> (Tcb.frameId f <= p2end) && (Tcb.frameId f >= p2start)) tbData + + -- Define gap penalties. Leaving frames unaligned costs nothing, + -- but all events will get aligned since even a terrible match is > 0. + let gapl f = 1 + let gapr e = -1 + + -- Build the scoring function. + let scoreClock = Scoring.misClock (timestamp opts) f24Data (cScale opts) + let scoreLocation = Scoring.misLocation (100 * lScale opts) + let scorePlayer = Scoring.misPlayer (F24.shirtNumbers f24Meta) (100 * pScale opts) + let scoreBall = Scoring.misBallStatus (bScale opts) + let scoreFull = Scoring.combine [scoreClock, scoreLocation, scorePlayer, scoreBall] + let score = if timeOnly opts then scoreClock else scoreFull + let sim e f = exp (-score e f) + + -- Align! + let sync1 = NW.align events1 frames1 sim gapl gapr + let sync2 = NW.align events2 frames2 sim gapl gapr + CSV.alignment2Csv (NW.joinAlignments sync1 sync2) (outputFile opts) + + -- Print sync on screen if requested + when (showsync opts) $ putStr (show sync1) + when (showsync opts) $ putStr (show sync2) + + -- Write event and frame CSVs if requested + when (eventCSV opts /= "") $ CSV.events2Csv (events1 ++ events2) (eventCSV opts) + when (frameCSV opts /= "") $ CSV.frames2Csv (frames1 ++ frames2) (frameCSV opts) diff --git a/doc/header-clip.gif b/doc/header-clip.gif new file mode 100644 index 0000000..442580d Binary files /dev/null and b/doc/header-clip.gif differ diff --git a/src/Csvs.hs b/src/Csvs.hs index d5a7157..7f3dcc7 100644 --- a/src/Csvs.hs +++ b/src/Csvs.hs @@ -1,35 +1,33 @@ -{-# LANGUAGE FlexibleInstances #-} - module Csvs where import Data.IntMap (elems) import Data.Maybe (fromJust) import Text.Printf (printf) import qualified Tracab as Tcb -import qualified F24 as F24 +import qualified F24 import qualified NeedlemanWunsch as NW -events2Csv :: [F24.Event (Tcb.Coordinates)] -> String -> IO () +events2Csv :: [F24.Event Tcb.Coordinates] -> String -> IO () events2Csv events filepath = do - let header = "event,x,y,event_type,team,minute,second" - let format = "%d,%d,%d,%s,%d,%d,%d" + let header = "event,x,y,event_type,team,half,minute,second" + let format = "%d,%d,%d,%s,%d,%d,%d,%d" let e2record e = printf format (F24.eid e) ((Tcb.x . fromJust . F24.coordinates) e) ((Tcb.y . fromJust . F24.coordinates) e) (F24.eventTypeName e) (F24.team_id e) + (F24.period_id e) (F24.min e) (F24.sec e) - writeFile filepath (unlines (header:(map e2record events))) + writeFile filepath (unlines (header : map e2record events)) showTeam :: Maybe Tcb.TeamKind -> String -showTeam team = - case team of - Just Tcb.Home -> "1" - Just Tcb.Away -> "0" - Nothing -> "3" +showTeam team = case team of + Just Tcb.Home -> "1" + Just Tcb.Away -> "0" + Nothing -> "3" frames2Csv :: Tcb.Frames Tcb.Positions -> String -> IO () frames2Csv frames filepath = do @@ -42,7 +40,7 @@ frames2Csv frames filepath = do (fromJust (Tcb.clock f)) (Tcb.frameId f) let meltFrame f = (f, Tcb.ball $ Tcb.positions f):[(f, pos) | pos <- Tcb.agents $ Tcb.positions f] - let melted = foldr (++) [] (map meltFrame frames) + let melted = foldr ((++) . meltFrame) [] frames let records = map fp2record melted writeFile filepath (unlines (header:records)) diff --git a/src/F24.hs b/src/F24.hs index 1849feb..5f71482 100644 --- a/src/F24.hs +++ b/src/F24.hs @@ -7,9 +7,11 @@ import qualified Data.ByteString as BS import Text.XML.Light.Types (Element) import Text.Printf (printf) import Control.Monad (liftM) -import Data.DateTime +import Data.Time +import Data.Time.LocalTime +import Data.Time.Clock (nominalDiffTimeToSeconds) import Data.Maybe -import XmlUtils ( attrLookupStrict, attrLookup, hasAttributeWithValue ) +import XmlUtils (attrLookupStrict, attrLookup, hasAttributeWithValue) import qualified XmlUtils as Xml data Game coordinates = Game { @@ -18,12 +20,12 @@ data Game coordinates = Game { away_team_name :: String, competition_id :: Int, competition_name :: String, - game_date :: DateTime, + game_date :: LocalTime, home_team_id :: Int, home_team_name :: String, matchday :: Int, - period_1_start :: DateTime, - period_2_start :: DateTime, + period_1_start :: LocalTime, + period_2_start :: LocalTime, season_id :: Int, season_name :: String, events :: [Event coordinates] @@ -49,8 +51,8 @@ data Event coordinates = Event { team_id :: Int, outcome :: Maybe Int, coordinates :: Maybe coordinates, - timestamp :: DateTime, - last_modified :: DateTime, + timestamp :: LocalTime, + last_modified :: LocalTime, qs :: [Q] } @@ -74,6 +76,20 @@ qval i e = let qq = filter (hasQid i) (qs e) hasQid :: Int -> Q -> Bool hasQid i q = qualifier_id q == i +-- The amount of seconds played until the event, treating all completed +-- game periods as having lasted exactly 45 minutes. +-- The first argument controls whether event timestamp should be used. +eventClock :: Bool -> Game cs -> Event cs -> Double +eventClock False _ ee = fromIntegral (60 * min ee + sec ee) + 0.5 +eventClock True gg ee = nominalOffset + actualSincePeriodStart where + half = period_id ee + nominalOffset = fromIntegral (60 * 45 * (half - 1)) + eventTimestamp = timestamp ee + periodStart = if half == 1 then period_1_start gg else period_2_start gg + actualSincePeriodStart = convert (diffLocalTime eventTimestamp periodStart) + convert = read . show . nominalDiffTimeToSeconds + + data Q = Q { qid :: Int, qualifier_id :: Int, @@ -97,6 +113,14 @@ data F24Coordinates = F24Coordinates { yPercentage :: Float } +-- Parse date-times such as 2018-09-15T14:56:30. +parseDatetime :: String -> Maybe LocalTime +parseDatetime = parseTimeM True defaultTimeLocale "%Y-%m-%dT%H:%M:%S" + +-- Parse timestamps such 2018-09-15T14:19:24.588. +parseTimestamp :: String -> Maybe LocalTime +parseTimestamp = parseTimeM True defaultTimeLocale "%Y-%m-%dT%H:%M:%S%Q" + loadGameFromFile :: String -> IO (Game F24Coordinates) loadGameFromFile filepath = do root <- Xml.loadXmlFromFile filepath @@ -122,8 +146,8 @@ makeEvent el = team_id = attrLookupStrict el read "team_id", outcome = attrLookup el read "outcome", coordinates = coordinates, - timestamp = attrLookupStrict el read "timestamp", - last_modified = attrLookupStrict el read "last_modified", + timestamp = attrLookupStrict el (fromJust . parseTimestamp) "timestamp", + last_modified = attrLookupStrict el (fromJust . parseDatetime) "last_modified", qs = map makeQ $ Xml.getChildrenWithQName "Q" el } where @@ -138,12 +162,12 @@ makeGame el = Game { gid = attrLookupStrict el read "id", away_team_name = attrLookupStrict el id "away_team_name", competition_id = attrLookupStrict el read "competition_id", competition_name = attrLookupStrict el id "competition_name", - game_date = attrLookupStrict el read "game_date", + game_date = attrLookupStrict el (fromJust . parseDatetime) "game_date", home_team_id = attrLookupStrict el read "home_team_id", home_team_name = attrLookupStrict el id "home_team_name", matchday = attrLookupStrict el read "matchday", - period_1_start = attrLookupStrict el read "period_1_start", - period_2_start = attrLookupStrict el read "period_2_start", + period_1_start = attrLookupStrict el (fromJust . parseDatetime) "period_1_start", + period_2_start = attrLookupStrict el (fromJust . parseDatetime) "period_2_start", season_id = attrLookupStrict el read "season_id", season_name = attrLookupStrict el id "season_name", events = map makeEvent $ Xml.getChildrenWithQName "Event" el @@ -151,12 +175,10 @@ makeGame el = Game { gid = attrLookupStrict el read "id", isAwayTeam :: Game a -> Event b -> Bool -isAwayTeam game event = - (team_id event) == (away_team_id game) +isAwayTeam game event = team_id event == away_team_id game isHomeTeam :: Game a -> Event b -> Bool -isHomeTeam game event = - (team_id event) == (home_team_id game) +isHomeTeam game event = team_id event == home_team_id game eventTeam :: Game a -> Event b -> Maybe Tcb.TeamKind eventTeam game event @@ -176,7 +198,7 @@ createFlippedTeamMapping :: Tcb.Metadata -> Tcb.Frames Tcb.Positions -> Map.Map createFlippedTeamMapping metaData frames = Map.fromList $ map createKeyFlipped tracabPeriods where - tracabPeriods = filter (\p -> (Tcb.startFrame p) /= (Tcb.endFrame p)) (Tcb.periods metaData) + tracabPeriods = filter (\p -> Tcb.startFrame p /= Tcb.endFrame p) (Tcb.periods metaData) createKeyFlipped period = ( Tcb.periodId period, flipped) @@ -192,8 +214,7 @@ createFlippedTeamMapping metaData frames = kickOffFrame : _ -> Tcb.rightToLeftKickOff kickOffFrame kickOffFrameId = Tcb.startFrame period - isKickOff frame = - (Tcb.frameId frame) == kickOffFrameId + isKickOff frame = Tcb.frameId frame == kickOffFrameId convertGameCoordinates :: Tcb.Metadata -> Tcb.Frames Tcb.Positions -> Game F24Coordinates -> Game Tcb.Coordinates @@ -202,7 +223,7 @@ convertGameCoordinates metaData frames game = where flippedMap = createFlippedTeamMapping metaData frames convertEvent event = - event { coordinates = liftM convertCoordinates $ coordinates event } + event { coordinates = fmap convertCoordinates $ coordinates event } where convertCoordinates coords = Tcb.Coordinates @@ -267,17 +288,14 @@ convertGameCoordinates metaData frames game = perhapsFlipFactor = case Map.lookup (period_id event) flippedMap of - Just Tcb.Home | isHomeTeam game event -> - -1 - Just Tcb.Away | isAwayTeam game event -> - -1 - otherwise -> - 1 + Just Tcb.Home | isHomeTeam game event -> -1 + Just Tcb.Away | isAwayTeam game event -> -1 + _ -> 1 -- Whether the event is an on-the-ball event. -- Consult the list below for the meaning of event type IDs. isOTB :: Event c -> Bool -isOTB e = elem (type_id e) ([1..16] ++ [41..45] ++ [49..61]) +isOTB e = type_id e `elem` ([1..4] ++ [7..16] ++ [41, 42, 44, 45] ++ [49..61]) eventTypeName :: Event a -> String @@ -366,15 +384,12 @@ data TeamData = TeamData { players :: [PlayerData] } - - data PlayerData = PlayerData { playerRef :: String, formationPosition :: String, shirtNumber :: ShirtNumber } - parseMetaFile :: String -> IO Metadata parseMetaFile filename = do root <- Xml.loadXmlFromFile filename diff --git a/src/ParseMain.hs b/src/ParseMain.hs deleted file mode 100644 index 2afc223..0000000 --- a/src/ParseMain.hs +++ /dev/null @@ -1,78 +0,0 @@ -import qualified Data.IntMap as Map -import System.Environment (getArgs) -import qualified F24 -import Tracab - -main :: IO () -main = - do - (f24MetaFilename : f24Filename : tracabMetaFilename : tracabDataFilename : clArguments) <- getArgs - f24Meta <- F24.parseMetaFile f24MetaFilename - game <- F24.loadGameFromFile f24Filename - putStrLn $ displayShirtNumbers f24Meta - - (tcMeta, tcData) <- parseTracab tracabMetaFilename tracabDataFilename - putStrLn ("There are " ++ (show $ length (F24.events game)) ++ " events.") - putStrLn ("There are " ++ (show $ length tcData) ++ " tracab data lines") - -- This tests the parser by forcing us to evaluate the entirety of each frame. - let maxFrameInt = maximum $ map frameInteger tcData - putStrLn ("The maximum frame int (for parser testing) is: " ++ show maxFrameInt) - putStrLn $ show tcMeta - -- This tests the parser by forcing us to evaluate the entirety of each frame. - let maxFrameInt = maximum $ map frameInteger tcData - putStrLn ("The maximum frame int (for parser testing) is: " ++ show maxFrameInt) - - -displayShirtNumbers :: F24.Metadata -> String -displayShirtNumbers meta = - unlines allLines - where - allLines = homeLines ++ awayLines - homeLines = map (displayShirt "Home:") $ F24.players $ F24.homeTeam meta - awayLines = map (displayShirt "Away:") $ F24.players $ F24.awayTeam meta - displayShirt team player = - unwords - [ team - , F24.playerRef player - , "wears number" - , show $ F24.shirtNumber player - ] - -{- - This is really just a dummy function to convert the entirety of a parsed frame, - including the positions, into a single integer. The trick here is to make sure that - we utilise every single part of the frame, this forces evaluation of the parsed frame - and hence any parser errors to come to light. --} -frameInteger :: Frame Positions -> Int -frameInteger frame = - sum - [ frameId frame - , clockInt - , sum $ map positionInt (agents framePositions) - , positionInt $ ball framePositions - ] - where - framePositions = positions frame - clockInt = case clock frame of - Nothing -> 0 - Just x -> round x - positionInt :: Position -> Int - positionInt position = - sum - [ participantId position - , x $ coordinates position - , y $ coordinates position - , round $ speed position - , teamInt - , ballInt - ] - where - teamInt = case mTeam position of - Nothing -> 0 - Just Home -> 1 - Just Away -> 2 - ballInt = case mBallStatus position of - Nothing -> 0 - Just Alive -> 1 - Just Dead -> 2 diff --git a/src/Scoring.hs b/src/Scoring.hs index 0244bd0..68b4f26 100644 --- a/src/Scoring.hs +++ b/src/Scoring.hs @@ -2,50 +2,59 @@ module Scoring where import qualified Data.Map.Strict as Map import qualified Data.List -import Data.Maybe (maybe) -import Statistics.Distribution (logDensity) -import Statistics.Distribution.Normal as Gaussian +import Data.Maybe (maybe, fromMaybe) import qualified Tracab as Tcb import qualified F24 -eventPlayerDistance :: F24.ShirtNumbers -> F24.Event Tcb.Coordinates -> Tcb.Frame Tcb.Positions -> Maybe Double -eventPlayerDistance shirtNumbers event frame = - do - playerId <- F24.player_id event - (teamKind, shirtNumber) <- Map.lookup playerId shirtNumbers - let isPlayer p = Tcb.participantId p == shirtNumber && Tcb.mTeam p == Just teamKind - let positions = Tcb.positions frame - playerPosition <- Data.List.find isPlayer $ Tcb.agents positions - let playerCoords = Tcb.coordinates playerPosition - let ballCoords = Tcb.coordinates $ Tcb.ball positions - return $ euclideanDistance playerCoords ballCoords - -euclideanDistance :: Tcb.Coordinates -> Tcb.Coordinates -> Double -euclideanDistance object target = - sqrt $ fromIntegral (xSquareSide + ySquareSide) - where +-- Distance between the player performing the given event and the ball, +-- assessed using the Tracab frame supplied. +playerBallDistance :: F24.ShirtNumbers -> F24.Event Tcb.Coordinates -> Tcb.Frame Tcb.Positions -> Maybe Double +playerBallDistance shirtNumbers event frame = do + playerId <- F24.player_id event + (teamKind, shirtNumber) <- Map.lookup playerId shirtNumbers + let isPlayer p = Tcb.shirtNumber p == Just shirtNumber && Tcb.mTeam p == Just teamKind + let positions = Tcb.positions frame + playerPosition <- Data.List.find isPlayer $ Tcb.agents positions + let playerCoords = Tcb.coordinates playerPosition + let ballCoords = Tcb.coordinates $ Tcb.ball positions + return $ eucl playerCoords ballCoords + +-- Euclidean distance between a pair of points. +eucl :: Tcb.Coordinates -> Tcb.Coordinates -> Double +eucl object target = sqrt $ fromIntegral (xSquareSide + ySquareSide) where xSquareSide = squareSide (Tcb.x object) (Tcb.x target) ySquareSide = squareSide (Tcb.y object) (Tcb.y target) squareSide p b = (p - b) ^ 2 -clockScore :: Double -> F24.Event Tcb.Coordinates -> Tcb.Frame Tcb.Positions -> Double -clockScore scale e f = - let seconds = fromIntegral $ 60 * (F24.min e) + (F24.sec e) - dist = abs $ seconds - (maybe seconds id (Tcb.clock f)) - in logDensity Gaussian.standard (dist / scale) - -locationScore :: Double -> F24.Event Tcb.Coordinates -> Tcb.Frame Tcb.Positions -> Double -locationScore scale e f = - let eXY = maybe fXY id (F24.coordinates e) - fXY = Tcb.coordinates $ Tcb.ball $ Tcb.positions f - dist = euclideanDistance eXY fXY - in logDensity Gaussian.standard (dist / scale) - -ballStatusScore :: Double -> F24.Event Tcb.Coordinates -> Tcb.Frame Tcb.Positions -> Double -ballStatusScore scale _ f = case Tcb.mBallStatus $ Tcb.ball $ Tcb.positions f of - Nothing -> 0.0 - Just Tcb.Alive -> scale - Just Tcb.Dead -> (-scale) - -totalScore :: F24.Event Tcb.Coordinates -> Tcb.Frame Tcb.Positions -> Double -totalScore e f = (clockScore 1.0 e f) + (locationScore 100.0 e f) + (ballStatusScore 1.0 e f) +-- Functions that measure (in)compatibility between an event and a frame. +-- Raw measurements are normalised using the first arguments (should be +ve.) +type Mismatch = F24.Event Tcb.Coordinates -> Tcb.Frame Tcb.Positions -> Double +type RawMismatch = Double -> Mismatch + +-- Mismatch between clocks. +misClock :: Bool -> F24.Game Tcb.Coordinates -> RawMismatch +misClock useTimestamp game scale e f = dist / scale where + seconds = F24.eventClock useTimestamp game e + dist = abs $ seconds - fromMaybe seconds (Tcb.clock f) + +-- Mismatch between event location and ball position. +misLocation :: RawMismatch +misLocation scale e f = dist / scale where + eXY = fromMaybe fXY (F24.coordinates e) + fXY = Tcb.coordinates $ Tcb.ball $ Tcb.positions f + dist = eucl eXY fXY + +-- Disagreement on ball status (all events imply that the ball is in play.) +misBallStatus :: RawMismatch +misBallStatus scale _ f = if status == Just Tcb.Dead then scale else 0 where + status = Tcb.mBallStatus $ Tcb.ball $ Tcb.positions f + +-- Distance between the player and the ball (all events imply 0.) +misPlayer :: F24.ShirtNumbers -> RawMismatch +misPlayer jerseys scale e f = dist / scale where + dist = fromMaybe 0.0 (playerBallDistance jerseys e f) + +-- Combine an arbitrary number of Mismatch functions to a single one +-- by taking the length of the vector of individual mismatch scores. +combine :: [Mismatch] -> Mismatch +combine funs e f = sqrt $ sum [fun e f ^ 2 | fun <- funs] \ No newline at end of file diff --git a/src/SyncMain.hs b/src/SyncMain.hs deleted file mode 100644 index 34f83f6..0000000 --- a/src/SyncMain.hs +++ /dev/null @@ -1,80 +0,0 @@ -import Control.Monad (when) -import Options.Applicative -import Data.Semigroup ((<>)) -import qualified Tracab as Tcb -import qualified F24 -import qualified NeedlemanWunsch as NW -import qualified Csvs as CSV -import qualified Scoring - - --- Command line parsing machinery -data Options = Options { - tcbMetaFile :: String, - tcbDataFile :: String, - f7File :: String, - f24File :: String, - outputFile :: String, - timeOnly :: Bool, - eventCSV :: String, - frameCSV :: String -} deriving Show - -options :: Parser Options -options = Options - <$> argument str (metavar "TRACAB-META") - <*> argument str (metavar "TRACAB-DATA") - <*> argument str (metavar "F7") - <*> argument str (metavar "F24") - <*> argument str (metavar "OUTPUT") - <*> switch (long "time-only" <> short 't' <> help "Sync only by time") - <*> strOption (long "event-csv" <> short 'e' <> value "" <> metavar "FILEPATH" <> help "Location to save a CSV of F24 events") - <*> strOption (long "frame-csv" <> short 'f' <> value "" <> metavar "FILEPATH" <> help "Location to save a CSV of Tracab frames") - -parseOptions :: IO Options -parseOptions = let desc = "Synchronise Tracab and F24 data." - hdr = "Copyright (c) 2018-2019 Allan Clark and Marek Kwiatkowski." - in execParser $ info (options <**> helper) (fullDesc <> progDesc desc <> header hdr) - -main :: IO () -main = do - opts <- parseOptions - tbMeta <- Tcb.parseMetaFile (tcbMetaFile opts) - tbData <- Tcb.parseDataFile tbMeta (tcbDataFile opts) - f24Meta <- F24.parseMetaFile (f7File opts) - f24Raw <- F24.loadGameFromFile (f24File opts) - let f24Data = F24.convertGameCoordinates tbMeta tbData f24Raw - - -- Select events to sync in both halves - let syncable pid e = (F24.period_id e == pid) && (F24.isOTB e) - let events1 = filter (syncable 1) (F24.events f24Data) - let events2 = filter (syncable 2) (F24.events f24Data) - - -- Select frames to sync to - let p1start = (Tcb.startFrame . head . Tcb.periods) tbMeta - let p2start = (Tcb.startFrame . head . tail . Tcb.periods) tbMeta - let p1end = (Tcb.endFrame . head . Tcb.periods) tbMeta - let p2end = (Tcb.endFrame . head . tail . Tcb.periods) tbMeta - let frames1 = filter (\f -> (Tcb.frameId f <= p1end) && (Tcb.frameId f >= p1start)) tbData - let frames2 = filter (\f -> (Tcb.frameId f <= p2end) && (Tcb.frameId f >= p2start)) tbData - - -- So if you want to do some smoothing using matrices for the frame data then - let frameMatrices = Tcb.translateFrames frames2 - -- If you want just a list of matrices then - let tracabMatrices = map Tcb.positions frameMatrices - - -- The penalty for leaving frames unaligned needs to be small. - -- Conversely, leaving events unaligned should be costly. - -- Note that the score for a Match is negative on the log-density scale. - let gapl = \f -> (-10.0) -- Leaves a frame unaligned for p < exp(-10) = 4.5e-5 - let gapr = \e -> (-1000.0) - let sim = if timeOnly opts then Scoring.clockScore 1.0 else Scoring.totalScore - - -- Align! - let sync1 = NW.align events1 frames1 sim gapl gapr - let sync2 = NW.align events2 frames2 sim gapl gapr - CSV.alignment2Csv (NW.joinAlignments sync1 sync2) (outputFile opts) - - -- Write event and frame CSVs if requested - when (eventCSV opts /= "") $ CSV.events2Csv (events1 ++ events2) (eventCSV opts) - when (frameCSV opts /= "") $ CSV.frames2Csv (frames1 ++ frames2) (frameCSV opts) diff --git a/src/Tracab.hs b/src/Tracab.hs index e770eb4..0fe28ff 100644 --- a/src/Tracab.hs +++ b/src/Tracab.hs @@ -1,6 +1,3 @@ -{-# LANGUAGE DataKinds #-} -{- We need data kinds for the matrix types L 2 22 etc. -} - module Tracab where import qualified Data.IntMap as Map @@ -12,7 +9,6 @@ import Text.XML.Light.Types (Element) import Text.Printf (printf) import XmlUtils (attrLookupStrict, attrLookup) import qualified XmlUtils as Xml -import Numeric.LinearAlgebra.Static (L, matrix) -- Complete Tracab data @@ -41,8 +37,9 @@ data Coordinates = Coordinates { } -- The position information of a single player/ball in a single snapshot -data Position = Position{ +data Position = Position { participantId :: Int, + shirtNumber :: Maybe Int, coordinates :: Coordinates, mTeam :: Maybe TeamKind, speed :: Float, @@ -50,23 +47,23 @@ data Position = Position{ } data BallStatus = Alive | Dead + deriving Eq -- A single complete snapshot of tracking data -data Frame positions = Frame{ +data Frame positions = Frame { frameId :: Int, positions :: positions, clock :: Maybe Double - } +} type Frames positions = [Frame positions] - instance Show (Frame pos) where show f = let formatClock :: Double -> String formatClock c = printf "%02.d:%02d.%03d" mins secs msec where mins = floor (c / 60.0) :: Int - secs = floor (c - 60.0*(fromIntegral mins)) :: Int - msec = round (1000.0*(c - 60.0*(fromIntegral mins) - (fromIntegral secs))) :: Int + secs = floor (c - 60.0 * fromIntegral mins) :: Int + msec = round (1000.0 * (c - 60.0 * fromIntegral mins - fromIntegral secs)) :: Int base = "Frame " ++ show (frameId f) extra = case clock f of Nothing -> "" @@ -76,12 +73,11 @@ instance Show (Frame pos) where -- The key method parsing a line of the Tracab data file into a Frame object parseFrame :: Metadata -> String -> Frame Positions parseFrame meta inputLine = - Frame - { frameId = frameId - , positions = positions - , clock = clock - } - where + Frame { + frameId = frameId, + positions = positions, + clock = clock + } where -- Split input data into chunks [dataLineIdStr, positionsString, ballString, _] = splitOn ':' inputLine positionsStrings = splitOn ';' positionsString @@ -89,74 +85,62 @@ parseFrame meta inputLine = -- Assemble parsed data frameId = read dataLineIdStr positions = - Positions - { agents = map parsePosition positionsStrings - , ball = parseBallPosition ballString - } + Positions { + agents = map parsePosition positionsStrings, + ball = parseBallPosition ballString + } -- Compute the implied timestamp of the frame in seconds from game start - inPeriodClock p = let offset = frameId - (startFrame p) + inPeriodClock p = let offset = frameId - startFrame p fps = frameRateFps meta - clockStart = if (periodId p) == 2 then 45.0*60.0 else 0.0 - in clockStart + (fromIntegral offset) / (fromIntegral fps) + clockStart = if periodId p == 2 then 45.0*60.0 else 0.0 + in clockStart + fromIntegral offset / fromIntegral fps candidatePeriods = [p | p <- periods meta, startFrame p <= frameId, endFrame p >= frameId] - clock = maybe Nothing (Just . inPeriodClock) (listToMaybe candidatePeriods) + clock = fmap inPeriodClock (listToMaybe candidatePeriods) -- Parse individual chunks splitOn c = Split.wordsBy (==c) parsePosition inputStr = Position { participantId = read idStr + , shirtNumber = if jerseyStr == "-1" then Nothing else Just (read jerseyStr) , coordinates = Coordinates { x = read xStr , y = read yStr } , mTeam = team , speed = read speedStr , mBallStatus = Nothing } where - [teamStr,idStr,jerseyNumberStr,xStr,yStr,speedStr] = splitOn ',' inputStr - team = - case teamStr of - "1" -> - Just Home - "0" -> - Just Away - _ -> - Nothing + [teamStr,idStr,jerseyStr,xStr,yStr,speedStr] = splitOn ',' inputStr + team = case teamStr of + "1" -> Just Home + "0" -> Just Away + _ -> Nothing parseBallPosition inputStr = Position { participantId = 0 + , shirtNumber = Nothing , coordinates = Coordinates { x = read xStr , y = read yStr } , mTeam = team , mBallStatus = ballStatus , speed = read speedStr } where - xStr:yStr:zStr:speedStr:rest = (splitOn ',') inputStr - (team, otherFields) = - case rest of - "H" : remainder -> - (Just Home, remainder) - "A" : remainder -> - (Just Away, remainder) - _ -> - (Nothing, rest) - ballStatus = - case otherFields of - "Alive" : _ -> - Just Alive - "Dead" : _ -> - Just Dead - _ -> - Nothing - + xStr:yStr:zStr:speedStr:rest = splitOn ',' inputStr + (team, otherFields) = case rest of + "H" : remainder -> (Just Home, remainder) + "A" : remainder -> (Just Away, remainder) + _ -> (Nothing, rest) + ballStatus = case otherFields of + "Alive" : _ -> Just Alive + "Dead" : _ -> Just Dead + _ -> Nothing -- Parse the entire Tracab data file into a list of frames parseDataFile :: Metadata -> String -> IO (Frames Positions) -parseDataFile meta filename = - do +parseDataFile meta filename = do handle <- openFile filename ReadMode contents <- hGetContents handle let frames = map (parseFrame meta) $ lines contents @@ -209,9 +193,9 @@ indent input = instance Show Metadata where show match = unlines - [ "matchId: " ++ (matchId match) + [ "matchId: " ++ matchId match , "frameRateFps: " ++ show (frameRateFps match) - , "periods: " ++ (indentLines $ map show (periods match)) + , "periods: " ++ indentLines (map show (periods match)) ] instance Show Period where @@ -259,12 +243,7 @@ oppositionKind Home = Away oppositionKind Away = Home rightToLeftKickOff :: Frame Positions -> TeamKind -rightToLeftKickOff kickOffFrame = - case homeX > awayX of - True -> - Home - False -> - Away +rightToLeftKickOff kickOffFrame = if homeX > awayX then Home else Away where -- Might be able to do better than this. kickOffPositions = agents $ positions kickOffFrame @@ -273,36 +252,4 @@ rightToLeftKickOff kickOffFrame = sumX positions = sum $ map (x . coordinates) positions homeX = sumX homePositions - awayX = sumX awayPositions - - --- The type of matrix is "kinded" by the number of rows and columns. I've gone for 30, --- the tracab documentation states that the player positions are an array of *up to* 29 --- so add one for the ball and we get 30. I'm not sure how consistent this data is, and if --- we might need to pad out a shorter array with 'nil values'. The nil for a player position is --- kind of non-trivial because of course the coordinates (0,0) is the center of the pitch, we probably --- want something on the outside of the tracking area. -type MatrixPositions = L 2 30 - -translateTracabData :: Tracab Positions -> Tracab MatrixPositions -translateTracabData (metadata, frames) = - (metadata, translateFrames frames) - -translateFrames :: Frames Positions -> Frames MatrixPositions -translateFrames = - map frameMatrix - -frameMatrix :: Frame Positions -> Frame MatrixPositions -frameMatrix frame = - frame { positions = matrix allPositions } - where - allPositions = map fromIntegral (xpositions ++ ypositions) - ballCoordinates = coordinates $ ball $ positions frame - - -- TODO: This probably has to do some padding in the case that there are *fewer* than - -- 29 agent coordinates. - agentCoordinates = take 29 $ agents $ positions frame - xpositions = - (x ballCoordinates) : map (x . coordinates) agentCoordinates - ypositions = - (y ballCoordinates) : map (y . coordinates) agentCoordinates + awayX = sumX awayPositions \ No newline at end of file diff --git a/src/XmlUtils.hs b/src/XmlUtils.hs index 70dd804..102ec6a 100644 --- a/src/XmlUtils.hs +++ b/src/XmlUtils.hs @@ -14,14 +14,12 @@ import Text.XML.Light.Types ) import Control.Exception import Control.Monad (liftM) -import Data.DateTime -import Data.Maybe +import Data.Maybe (fromMaybe) import Data.Typeable -data XmlFileError - = ParserFailure - | MissingData String +data XmlFileError = ParserFailure + | MissingData String deriving (Show, Typeable) instance Exception XmlFileError @@ -36,18 +34,13 @@ loadXmlFromFile filepath = do attrLookup :: Element -> (String -> a) -> String -> Maybe a attrLookup el cast key = - let - makePair attr = - ( qName $ attrKey attr - , attrVal attr - ) + let makePair attr = ( qName $ attrKey attr, attrVal attr) val = lookup key $ map makePair (elAttribs el) - in - liftM cast val + in fmap cast val attrLookupStrict :: Element -> (String -> a) -> String -> a -attrLookupStrict el cast key = let val = (attrLookup el cast key) - in maybe (throw $ MissingData key) id val +attrLookupStrict el cast key = let val = attrLookup el cast key + in fromMaybe (throw $ MissingData key) val getAllChildren :: Element -> [ Element ] @@ -56,19 +49,16 @@ getAllChildren = getChildren (const True) getChildren :: (Element -> Bool) -> Element -> [Element] getChildren cond el = let getElems :: [Content] -> [Element] getElems [] = [] - getElems ((Elem e):rest) = e:(getElems rest) - getElems ((Text _):rest) = getElems rest - getElems ((CRef _):rest) = getElems rest + getElems (Elem e : rest) = e : getElems rest + getElems (Text _ : rest) = getElems rest + getElems (CRef _ : rest) = getElems rest in filter cond (getElems (elContent el)) qNameEquals :: String -> Element -> Bool -qNameEquals name element = - (qName $ elName element) == name +qNameEquals name element = qName (elName element) == name getChildrenWithQName :: String -> Element -> [ Element ] -getChildrenWithQName name = - getChildren (qNameEquals name) +getChildrenWithQName name = getChildren (qNameEquals name) hasAttributeWithValue :: String -> String -> Element -> Bool -hasAttributeWithValue name value element = - (Just value) == (attrLookup element id name) \ No newline at end of file +hasAttributeWithValue name value element = attrLookup element id name == Just value \ No newline at end of file diff --git a/stack.yaml b/stack.yaml index f64bb99..ddc5833 100644 --- a/stack.yaml +++ b/stack.yaml @@ -1,65 +1 @@ -# This file was automatically generated by 'stack init' -# -# Some commonly used options have been documented as comments in this file. -# For advanced use and comprehensive documentation of the format, please see: -# https://docs.haskellstack.org/en/stable/yaml_configuration/ - -# Resolver to choose a 'specific' stackage snapshot or a compiler version. -# A snapshot resolver dictates the compiler version and the set of packages -# to be used for project dependencies. For example: -# -# resolver: lts-3.5 -# resolver: nightly-2015-09-21 -# resolver: ghc-7.10.2 -# resolver: ghcjs-0.1.0_ghc-7.10.2 -# -# The location of a snapshot can be provided as a file or url. Stack assumes -# a snapshot provided as a file might change, whereas a url resource does not. -# -# resolver: ./custom-snapshot.yaml -# resolver: https://example.com/snapshots/2018-01-01.yaml -resolver: lts-12.16 - -# User packages to be built. -# Various formats can be used as shown in the example below. -# -# packages: -# - some-directory -# - https://example.com/foo/bar/baz-0.0.2.tar.gz -# - location: -# git: https://github.com/commercialhaskell/stack.git -# commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a -# - location: https://github.com/commercialhaskell/stack/commit/e7b331f14bcffb8367cd58fbfc8b40ec7642100a -# subdirs: -# - auto-update -# - wai -packages: -- . -# Dependency packages to be pulled from upstream that are not in the resolver -# using the same syntax as the packages field. -# (e.g., acme-missiles-0.3) -extra-deps: ["datetime-0.3.1"] - -# Override default flag values for local packages and extra-deps -# flags: {} - -# Extra package databases containing global packages -# extra-package-dbs: [] - -# Control whether we use the GHC we find on the path -# system-ghc: true -# -# Require a specific version of stack, using version ranges -# require-stack-version: -any # Default -# require-stack-version: ">=1.7" -# -# Override the architecture used by stack, especially useful on Windows -# arch: i386 -# arch: x86_64 -# -# Extra directories used by stack for building -# extra-include-dirs: [/path/to/dir] -# extra-lib-dirs: [/path/to/dir] -# -# Allow a newer minor version of GHC than the snapshot specifies -# compiler-check: newer-minor \ No newline at end of file +resolver: lts-16.4 \ No newline at end of file diff --git a/sync-soccer.cabal b/sync-soccer.cabal index 274e0c2..3d047eb 100644 --- a/sync-soccer.cabal +++ b/sync-soccer.cabal @@ -1,51 +1,43 @@ +-- Initial sync.soccer.cabal generated by cabal init. For further +-- documentation, see http://haskell.org/cabal/users-guide/ + + name: sync-soccer +-- The package version. See the Haskell package versioning policy (PVP) +-- for standards guiding when and how versions should be incremented. +-- https://wiki.haskell.org/Package_versioning_policy +-- PVP summary: +-+------- breaking API changes +-- | | +----- non-breaking API additions +-- | | | +--- code changes with no API change version: 0.1.0.0 --- synopsis: --- description: +synopsis: Synchronise soccer event and tracking data homepage: https://github.com/huffyhenry/sync.soccer -license: AllRightsReserved +license: AGPL-3 license-file: LICENSE author: Allan Clark and Marek Kwiatkowski -maintainer: info@sync.soccer -copyright: 2018-2019 Allan Clark and Marek Kwiatkowski -category: Web +maintainer: m.kwiatkowski@protonmail.com +copyright: 2018-2020 Allan Clark and Marek Kwiatkowski build-type: Simple +extra-source-files: CHANGELOG.md, README.md cabal-version: >=1.10 -extra-source-files: README.md --- See https://stackoverflow.com/questions/32123475/profiling-builds-with-stack --- for how to invoke stack build and stack exec with profiling. + executable sync-soccer - hs-source-dirs: src + hs-source-dirs: src, app main-is: SyncMain.hs - other-modules: Tracab, F24, Scoring, NeedlemanWunsch, XmlUtils, Csvs - default-language: Haskell2010 ghc-options: -O -fPIC --- ld-options: -static - build-depends: - base >= 4.7 && < 5 - , datetime - , containers - , split - , xml - , bytestring - , array - , statistics - , optparse-applicative - , hmatrix - -executable sync-soccer-parse - hs-source-dirs: src - main-is: ParseMain.hs - other-modules: Tracab, F24, XmlUtils + other-modules: NeedlemanWunsch, + Tracab, + F24, + Scoring, + Csvs, + XmlUtils + build-depends: base >=4.12, + split, + optparse-applicative, + containers, + bytestring, + time >=1.9, + array, + xml default-language: Haskell2010 - build-depends: - base >= 4.7 && < 5 - , datetime - , containers - , split - , xml - , bytestring - , array - , hmatrix - diff --git a/viz/animate.R b/viz/animate.R new file mode 100644 index 0000000..04f730b --- /dev/null +++ b/viz/animate.R @@ -0,0 +1,84 @@ +library(readr) +library(dplyr) +library(tidyr) +library(ggplot2) +library(gganimate) +library(ggsoccer) + + +# Source files +frames.file <- "../tmp/frames.csv" +events.file <- "../tmp/events.csv" +sync.file <- "../tmp/sync.csv" + +# File to save animation to +anim.file <- "../clip.gif" + +# The segment of data to animate (in seconds, based on Tracab's implied clock) +start.clock <- 60*67 + 10 +end.clock <- start.clock + 30 + +# Pitch dimensions in metres, from Tracab metadata. +pitch.length <- 105.0 +pitch.width <- 68.0 + +# Presentation +team1.colour <- "#e6b800" +team2.colour <- "#6cabdd" +tracab.colour <- "black" +opta.colour <- "red" +annotation <- "sync.soccer (full algo)" +font <- "Helvetica-Narrow" + + +# Load both data streams and merge them according to the sync file. +# Each row is the position of a single player or of the ball. +# Positions in frames aligned to an event are annotated with that event data. +data <- read_csv(frames.file, col_types=cols()) %>% + # Drop officials + filter(team != 3) %>% + # Add matched event IDs + left_join(read_csv(sync.file, col_types=cols()), by="frame") %>% + # Add event information + left_join(read_csv(events.file, col_types=cols()), by="event", suffix=c(".f", ".e")) %>% + # Create descriptions to print (running clock / event type and time) + mutate( + desc.f=sprintf( + "Implied frame clock - %02d:%02d.%03d", + floor(clock) %/% 60, floor(clock) %% 60, floor(1000*(clock - floor(clock))) + ), + desc.e=ifelse( + is.na(event), + "", + sprintf("Event - %02d:%02d %s", minute, second, event_type) + ) + ) %>% + # Repeat the ball positions aligned to an event 25 times to simulate a pause + uncount(ifelse(object == 0 & !is.na(event), 25, 1)) %>% + # Create a frame counter (cumulative count of ball positions) + arrange(clock, object) %>% + mutate(animation.clock=cumsum(as.numeric(object == 0))) %>% + # Take only an interval of data as defined by constants above + filter(clock > start.clock & clock < end.clock) + +# Split the dataset to simplify and speed up plotting logic. +ball <- filter(data, object == 0) +players <- filter(data, object != 0) + +# Animate +animation <- ggplot(ball, aes(x=x.f, y=y.f)) + + annotate_pitch(dimensions=make_pitch_tracab(pitch.length, pitch.width)) + + geom_text(aes(x=-25*pitch.length, y=47*pitch.width, label=desc.f), colour=tracab.colour) + + geom_text(aes(x=25*pitch.length, y=47*pitch.width, label=desc.e), colour=opta.colour) + + geom_point(size=1.5) + + geom_point(data=players, aes(color=as.factor(team.f)), size=3, alpha=0.75) + + geom_point(aes(x=x.e, y=y.e), colour=opta.colour, shape=4, size=4) + + scale_color_manual(values=c(team1.colour, team2.colour)) + + annotate("text", x=40*pitch.length, y=-47*pitch.width, label=annotation, size=2.5) + + transition_time(animation.clock) + + theme_pitch(aspect_ratio=pitch.width/pitch.length) + + theme(legend.position="none", text=element_text(family=font)) + +animate(animation, nframes=nrow(ball), duration=end.clock-start.clock) + +anim_save(anim.file) diff --git a/viz/slippage.R b/viz/slippage.R new file mode 100644 index 0000000..54075da --- /dev/null +++ b/viz/slippage.R @@ -0,0 +1,42 @@ +library(readr) +library(dplyr) +library(ggplot2) + +# Source files +frames.file <- "../tmp/frames.csv" +events.file <- "../tmp/events.csv" +sync.file <- "../tmp/sync.csv" + +# File to save the plot to +out.file <- "../slippage.png" + +# Build the plot +read_csv(frames.file, col_types=cols(), progress=FALSE) %>% +filter(object == 0) %>% +left_join(read_csv(sync.file, col_types=cols()), by="frame") %>% +inner_join(read_csv(events.file, col_types=cols()), by="event") %>% +transmute( + half.desc=ifelse(half == 1, "First half", "Second half"), + frame.clock=clock - 45*60*(half - 1), + event.clock=60*minute + second + 0.5 - 45*60*(half - 1), +) %>% +ggplot(aes(x=event.clock/60, y=event.clock-frame.clock)) + + geom_col(position="dodge") + + scale_x_continuous(breaks=c(0, 15, 30, 45), expand=expansion(add=1)) + + scale_y_continuous(breaks=seq(-3, 3, 1)) + + coord_cartesian(xlim=c(0, 50), ylim=c(-3.5, 3.5)) + + theme_bw() + + theme( + panel.grid.major.x=element_blank(), + panel.grid.minor.x=element_blank(), + text=element_text(family="Helvetica-Narrow") + ) + + labs( + title="Clock difference between aligned events and frames (seconds)", + x=NULL, + y=NULL + ) + + facet_wrap(~half.desc, nrow=2, ncol=1, strip.position="right") + +# Save the plot to file +ggsave(out.file)