First upload code
This commit is contained in:
parent
5d96465da2
commit
743fb6c29f
13
Dockerfile
Normal file
13
Dockerfile
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
FROM ubuntu:20.04
|
||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
WORKDIR /web
|
||||||
|
|
||||||
|
COPY *.json *.cjs *.ts src static webassembly ./
|
||||||
|
|
||||||
|
|
||||||
|
WORKDIR /
|
||||||
|
|
||||||
|
#RUN npm install
|
||||||
|
|
||||||
|
#RUN npm run build
|
||||||
232
LICENSE
Normal file
232
LICENSE
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright © 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU General Public License is a free, copyleft license for software and other kinds of works.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is 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. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.
|
||||||
|
|
||||||
|
Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it.
|
||||||
|
|
||||||
|
For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions.
|
||||||
|
|
||||||
|
Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users.
|
||||||
|
|
||||||
|
Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
|
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 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. Use with the GNU Affero General Public License.
|
||||||
|
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 Affero 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 special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of the GNU 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 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 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 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.
|
||||||
|
|
||||||
|
SDcmWeb
|
||||||
|
Copyright (C) 2025 develop
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU 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 General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
|
SDcmWeb Copyright (C) 2025 develop
|
||||||
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”.
|
||||||
|
|
||||||
|
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 GPL, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read <https://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||||
57
README.md
Normal file
57
README.md
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
# SDcmWeb
|
||||||
|
|
||||||
|
Cloud PACS Viewer using "Sveltekit and EMSDK"
|
||||||
|
|
||||||
|
|
||||||
|
ssdoctors@6fe57d3cfa83:~/project/SDcmWeb$ emcc -v
|
||||||
|
emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 4.0.12-git (31f8eac8436fbf984f189645ceea16f0b942eb30)
|
||||||
|
clang version 22.0.0git (https:/github.com/llvm/llvm-project e3af202fd212a66700170717856a8fa9aa7ed426)
|
||||||
|
Target: wasm32-unknown-emscripten
|
||||||
|
Thread model: posix
|
||||||
|
InstalledDir: /home/ssdoctors/emsdk/upstream/bin
|
||||||
|
Build config: +assertions
|
||||||
|
|
||||||
|
DCMTK v3.6.9
|
||||||
|
|
||||||
|
"devDependencies": {
|
||||||
|
"@fortawesome/free-brands-svg-icons": "^5.15.4",
|
||||||
|
"@fortawesome/free-regular-svg-icons": "^5.15.4",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||||
|
"@sveltejs/adapter-node": "^1.3.1",
|
||||||
|
"@sveltejs/adapter-static": "^1.0.0-next.50",
|
||||||
|
"@sveltejs/kit": "^1.20.4",
|
||||||
|
"@tailwindcss/forms": "^0.5.6",
|
||||||
|
"@tailwindcss/typography": "github:tailwindcss/typography",
|
||||||
|
"@types/pg": "^8.15.4",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||||
|
"@typescript-eslint/parser": "^6.0.0",
|
||||||
|
"autoprefixer": "^10.4.16",
|
||||||
|
"axios": "^1.6.0",
|
||||||
|
"eslint": "^8.28.0",
|
||||||
|
"eslint-config-prettier": "^8.5.0",
|
||||||
|
"eslint-plugin-svelte": "^2.30.0",
|
||||||
|
"flowbite": "^1.8.1",
|
||||||
|
"flowbite-svelte": "^0.44.18",
|
||||||
|
"flowbite-svelte-icons": "^0.4.4",
|
||||||
|
"postcss": "^8.4.24",
|
||||||
|
"postcss-load-config": "^4.0.1",
|
||||||
|
"prettier": "^2.8.0",
|
||||||
|
"prettier-plugin-svelte": "^2.10.1",
|
||||||
|
"svelte": "^4.0.5",
|
||||||
|
"svelte-check": "^3.4.3",
|
||||||
|
"svelte-icons": "^2.1.0",
|
||||||
|
"svelte-resize-observer": "^2.0.0",
|
||||||
|
"tailwindcss": "^3.3.5",
|
||||||
|
"tslib": "^2.4.1",
|
||||||
|
"typescript": "^5.0.0",
|
||||||
|
"vite": "^4.4.2"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"d3": "^7.8.5",
|
||||||
|
"layercake": "^8.0.2",
|
||||||
|
"pg": "^8.16.3",
|
||||||
|
"svelte-gestures": "^1.5.2",
|
||||||
|
"svelte-table": "^0.6.1",
|
||||||
|
"tw-elements": "^1.0.0"
|
||||||
|
}
|
||||||
4828
package-lock.json
generated
Normal file
4828
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
53
package.json
Normal file
53
package.json
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"name": "t5",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite dev --host",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
|
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
||||||
|
"format": "prettier --plugin-search-dir . --write ."
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@fortawesome/free-brands-svg-icons": "^5.15.4",
|
||||||
|
"@fortawesome/free-regular-svg-icons": "^5.15.4",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||||
|
"@sveltejs/adapter-node": "^1.3.1",
|
||||||
|
"@sveltejs/adapter-static": "^1.0.0-next.50",
|
||||||
|
"@sveltejs/kit": "^1.20.4",
|
||||||
|
"@tailwindcss/forms": "^0.5.6",
|
||||||
|
"@tailwindcss/typography": "github:tailwindcss/typography",
|
||||||
|
"@types/pg": "^8.15.4",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||||
|
"@typescript-eslint/parser": "^6.0.0",
|
||||||
|
"autoprefixer": "^10.4.16",
|
||||||
|
"axios": "^1.6.0",
|
||||||
|
"eslint": "^8.28.0",
|
||||||
|
"eslint-config-prettier": "^8.5.0",
|
||||||
|
"eslint-plugin-svelte": "^2.30.0",
|
||||||
|
"postcss": "^8.4.24",
|
||||||
|
"postcss-load-config": "^4.0.1",
|
||||||
|
"prettier": "^2.8.0",
|
||||||
|
"prettier-plugin-svelte": "^2.10.1",
|
||||||
|
"svelte": "^4.0.5",
|
||||||
|
"svelte-check": "^3.4.3",
|
||||||
|
"svelte-icons": "^2.1.0",
|
||||||
|
"svelte-resize-observer": "^2.0.0",
|
||||||
|
"tailwindcss": "^3.3.5",
|
||||||
|
"tslib": "^2.4.1",
|
||||||
|
"typescript": "^5.0.0",
|
||||||
|
"vite": "^4.4.2"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"d3": "^7.8.5",
|
||||||
|
"layercake": "^8.0.2",
|
||||||
|
"pg": "^8.16.3",
|
||||||
|
"svelte-gestures": "^1.5.2",
|
||||||
|
"svelte-table": "^0.6.1",
|
||||||
|
"tw-elements": "^1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
14
postcss.config.cjs
Normal file
14
postcss.config.cjs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
const tailwindcss = require('tailwindcss');
|
||||||
|
const autoprefixer = require('autoprefixer');
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
plugins: [
|
||||||
|
//Some plugins, like tailwindcss/nesting, need to run before Tailwind,
|
||||||
|
tailwindcss(),
|
||||||
|
//But others, like autoprefixer, need to run after,
|
||||||
|
autoprefixer
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = config;
|
||||||
12
src/app.d.ts
vendored
Normal file
12
src/app.d.ts
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// See https://kit.svelte.dev/docs/types#app
|
||||||
|
// for information about these interfaces
|
||||||
|
declare global {
|
||||||
|
namespace App {
|
||||||
|
// interface Error {}
|
||||||
|
// interface Locals {}
|
||||||
|
// interface PageData {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
15
src/app.html
Normal file
15
src/app.html
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
|
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/flowbite/2.3.0/flowbite.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
4
src/app.postcss
Normal file
4
src/app.postcss
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/* Write your global styles here, in PostCSS syntax */
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
35
src/hooks.server.ts
Normal file
35
src/hooks.server.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import type { Handle } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이 핸들러는 모든 서버 요청을 가로채서
|
||||||
|
* API 경로에 대한 CORS 헤더와 사이트 전반의 보안 헤더를 추가합니다.
|
||||||
|
*/
|
||||||
|
export const handle: Handle = async ({ event, resolve }) => {
|
||||||
|
// 1. API 경로('/api/')에 대한 CORS Preflight 요청(OPTIONS) 처리
|
||||||
|
// - API 경로가 아니라면 이 로직은 건너뜁니다.
|
||||||
|
if (event.url.pathname.startsWith('/api') && event.request.method === 'OPTIONS') {
|
||||||
|
return new Response(null, {
|
||||||
|
headers: {
|
||||||
|
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
|
||||||
|
'Access-Control-Allow-Origin': '*', // 보안을 위해 실제 프로덕션에서는 특정 도메인을 지정하는 것이 좋습니다.
|
||||||
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization', // 필요한 헤더만 명시적으로 허용하는 것이 더 안전합니다.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. SvelteKit이 요청을 정상적으로 처리하도록 전달
|
||||||
|
const response = await resolve(event);
|
||||||
|
|
||||||
|
// 3. 실제 API 응답에 CORS 헤더 추가
|
||||||
|
if (event.url.pathname.startsWith('/api')) {
|
||||||
|
// .append() 대신 .set()을 사용하면 헤더가 중복으로 추가되는 것을 방지할 수 있습니다.
|
||||||
|
response.headers.set('Access-Control-Allow-Origin', '*'); // 위와 동일하게 프로덕션에서는 특정 도메인 사용을 권장합니다.
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 모든 응답에 보안 관련 헤더 추가 (SharedArrayBuffer 등 사용 시 필요)
|
||||||
|
// - 이 헤더들은 사이트의 보안을 강화합니다.
|
||||||
|
response.headers.set('Cross-Origin-Opener-Policy', 'same-origin');
|
||||||
|
response.headers.set('Cross-Origin-Embedder-Policy', 'require-corp');
|
||||||
|
|
||||||
|
return response;
|
||||||
|
};
|
||||||
361
src/lib/components/DatePicker.svelte
Normal file
361
src/lib/components/DatePicker.svelte
Normal file
@ -0,0 +1,361 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
// --- Props ---
|
||||||
|
export let startDate: string = '';
|
||||||
|
export let endDate: string = '';
|
||||||
|
|
||||||
|
// --- 상태(State) 변수 ---
|
||||||
|
let isOpen = false;
|
||||||
|
let currentDate = new Date();
|
||||||
|
let internalStartDate: Date | null = null;
|
||||||
|
let internalEndDate: Date | null = null;
|
||||||
|
let hoveredDate: Date | null = null;
|
||||||
|
let datepickerNode: HTMLElement;
|
||||||
|
|
||||||
|
let currentView: 'days' | 'months' | 'years' = 'days';
|
||||||
|
let yearViewStart: number = Math.floor(new Date().getFullYear() / 10) * 10;
|
||||||
|
|
||||||
|
const today = new Date();
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
const monthNames = [
|
||||||
|
'1월', '2월', '3월', '4월', '5월', '6월',
|
||||||
|
'7월', '8월', '9월', '10월', '11월', '12월'
|
||||||
|
];
|
||||||
|
|
||||||
|
// --- Helper 함수 ---
|
||||||
|
function formatDisplayDate(date: Date | null): string {
|
||||||
|
if (!date) return '';
|
||||||
|
return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatYYYYMMDD(date: Date | null): string {
|
||||||
|
if (!date) return '';
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||||
|
const day = date.getDate().toString().padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSameDay = (d1: Date, d2: Date | null): boolean =>
|
||||||
|
!!d2 && d1.toDateString() === d2.toDateString();
|
||||||
|
const isToday = (day: Date): boolean => isSameDay(day, today);
|
||||||
|
const isSunday = (day: Date): boolean => day.getDay() === 0;
|
||||||
|
const isInRange = (day: Date): boolean =>
|
||||||
|
!!internalStartDate && !!internalEndDate && day > internalStartDate && day < internalEndDate;
|
||||||
|
const isHoverRange = (day: Date): boolean =>
|
||||||
|
!!internalStartDate &&
|
||||||
|
!internalEndDate &&
|
||||||
|
!!hoveredDate &&
|
||||||
|
((day > internalStartDate && day < hoveredDate) || (day < internalStartDate && day > hoveredDate));
|
||||||
|
|
||||||
|
// --- 반응형 로직 ---
|
||||||
|
$: rangeText =
|
||||||
|
internalStartDate
|
||||||
|
? formatDisplayDate(internalStartDate) + (internalEndDate ? ` - ${formatDisplayDate(internalEndDate)}` : '')
|
||||||
|
: '날짜 범위를 선택하세요';
|
||||||
|
|
||||||
|
// Props -> internal state 동기화
|
||||||
|
$: if (startDate && (!internalStartDate || formatYYYYMMDD(internalStartDate) !== startDate)) {
|
||||||
|
internalStartDate = new Date(startDate + 'T00:00:00');
|
||||||
|
} else if (!startDate && internalStartDate) {
|
||||||
|
internalStartDate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (endDate && (!internalEndDate || formatYYYYMMDD(internalEndDate) !== endDate)) {
|
||||||
|
internalEndDate = new Date(endDate + 'T00:00:00');
|
||||||
|
} else if (!endDate && internalEndDate) {
|
||||||
|
internalEndDate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$: calendarDays = (() => {
|
||||||
|
const year = currentDate.getFullYear();
|
||||||
|
const month = currentDate.getMonth();
|
||||||
|
const days = [];
|
||||||
|
const firstDayOfMonth = new Date(year, month, 1);
|
||||||
|
const lastDateOfMonth = new Date(year, month + 1, 0).getDate();
|
||||||
|
const dayOfWeek = firstDayOfMonth.getDay();
|
||||||
|
const offset = dayOfWeek;
|
||||||
|
for (let i = 0; i < offset; i++) days.push(null);
|
||||||
|
for (let day = 1; day <= lastDateOfMonth; day++) days.push(new Date(year, month, day));
|
||||||
|
return days;
|
||||||
|
})();
|
||||||
|
|
||||||
|
$: yearsInView = Array.from({ length: 12 }, (_, i) => yearViewStart + i);
|
||||||
|
|
||||||
|
// --- 이벤트 핸들러 ---
|
||||||
|
function goToPrev() {
|
||||||
|
const newDate = new Date(currentDate);
|
||||||
|
if (currentView === 'days') {
|
||||||
|
newDate.setMonth(newDate.getMonth() - 1);
|
||||||
|
} else if (currentView === 'months') {
|
||||||
|
newDate.setFullYear(newDate.getFullYear() - 1);
|
||||||
|
} else if (currentView === 'years') {
|
||||||
|
yearViewStart -= 12;
|
||||||
|
}
|
||||||
|
currentDate = newDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToNext() {
|
||||||
|
const newDate = new Date(currentDate);
|
||||||
|
if (currentView === 'days') {
|
||||||
|
newDate.setMonth(newDate.getMonth() + 1);
|
||||||
|
} else if (currentView === 'months') {
|
||||||
|
newDate.setFullYear(newDate.getFullYear() + 1);
|
||||||
|
} else if (currentView === 'years') {
|
||||||
|
yearViewStart += 12;
|
||||||
|
}
|
||||||
|
currentDate = newDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchView() {
|
||||||
|
if (currentView === 'days') {
|
||||||
|
currentView = 'months';
|
||||||
|
} else if (currentView === 'months') {
|
||||||
|
yearViewStart = Math.floor(currentDate.getFullYear() / 12) * 12;
|
||||||
|
currentView = 'years';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectYear(year: number) {
|
||||||
|
const newDate = new Date(currentDate);
|
||||||
|
newDate.setFullYear(year);
|
||||||
|
currentDate = newDate;
|
||||||
|
currentView = 'months';
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectMonth(monthIndex: number) {
|
||||||
|
const newDate = new Date(currentDate);
|
||||||
|
newDate.setMonth(monthIndex);
|
||||||
|
currentDate = newDate;
|
||||||
|
currentView = 'days';
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToToday() {
|
||||||
|
currentDate = new Date();
|
||||||
|
internalStartDate = today;
|
||||||
|
internalEndDate = today;
|
||||||
|
startDate = formatYYYYMMDD(internalStartDate);
|
||||||
|
endDate = formatYYYYMMDD(internalEndDate);
|
||||||
|
isOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRange(value: number, unit: 'week' | 'month') {
|
||||||
|
const end = new Date();
|
||||||
|
end.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
const start = new Date();
|
||||||
|
start.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
if (unit === 'week') {
|
||||||
|
start.setDate(start.getDate() - (value * 7) + 1);
|
||||||
|
} else if (unit === 'month') {
|
||||||
|
start.setMonth(start.getMonth() - value);
|
||||||
|
}
|
||||||
|
|
||||||
|
internalStartDate = start;
|
||||||
|
internalEndDate = end;
|
||||||
|
|
||||||
|
startDate = formatYYYYMMDD(internalStartDate);
|
||||||
|
endDate = formatYYYYMMDD(internalEndDate);
|
||||||
|
|
||||||
|
isOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDateClick(day: Date) {
|
||||||
|
if (internalStartDate && !internalEndDate) {
|
||||||
|
if (day < internalStartDate) {
|
||||||
|
internalEndDate = internalStartDate;
|
||||||
|
internalStartDate = day;
|
||||||
|
} else {
|
||||||
|
internalEndDate = day;
|
||||||
|
}
|
||||||
|
isOpen = false;
|
||||||
|
} else {
|
||||||
|
internalStartDate = day;
|
||||||
|
internalEndDate = null;
|
||||||
|
}
|
||||||
|
startDate = formatYYYYMMDD(internalStartDate);
|
||||||
|
endDate = internalEndDate ? formatYYYYMMDD(internalEndDate) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (startDate) {
|
||||||
|
internalStartDate = new Date(startDate + 'T00:00:00');
|
||||||
|
}
|
||||||
|
if (endDate) {
|
||||||
|
internalEndDate = new Date(endDate + 'T00:00:00');
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (isOpen && datepickerNode && !datepickerNode.contains(event.target as Node)) {
|
||||||
|
isOpen = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('click', handleClickOutside);
|
||||||
|
return () => window.removeEventListener('click', handleClickOutside);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="relative w-full font-sans" bind:this={datepickerNode}>
|
||||||
|
<!-- ✨ [수정] 다른 입력 필드와 스타일 통일 -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex items-center justify-between bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 w-full p-2.5 cursor-pointer text-left"
|
||||||
|
on:click={() => (isOpen = !isOpen)}
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<svg class="w-4 h-4 text-gray-500 dark:text-gray-400 mr-2.5" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span class:dark:text-gray-400={!internalStartDate}>{rangeText}</span>
|
||||||
|
</div>
|
||||||
|
<svg
|
||||||
|
class="w-4 h-4 text-gray-500 dark:text-gray-400 transition-transform duration-200"
|
||||||
|
class:-rotate-180={isOpen}
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{#if isOpen}
|
||||||
|
<div
|
||||||
|
class="absolute top-full left-0 mt-2 w-[21rem] bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl shadow-2xl z-10 p-5"
|
||||||
|
>
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
on:click|stopPropagation={goToPrev}
|
||||||
|
class="p-2 rounded-full hover:bg-slate-100 dark:hover:bg-slate-700 focus:outline-none transition-colors"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5 text-slate-600 dark:text-slate-300" fill="none" viewBox="0 0 24 24" stroke="currentColor"
|
||||||
|
><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" /></svg
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="text-lg font-bold text-slate-800 dark:text-slate-100 hover:text-blue-600 dark:hover:text-blue-400 transition-colors rounded-md px-3 py-1"
|
||||||
|
on:click|stopPropagation={switchView}
|
||||||
|
>
|
||||||
|
{#if currentView === 'days'}
|
||||||
|
{currentDate.getFullYear()}년 {currentDate.getMonth() + 1}월
|
||||||
|
{:else if currentView === 'months'}
|
||||||
|
{currentDate.getFullYear()}년
|
||||||
|
{:else if currentView === 'years'}
|
||||||
|
{yearViewStart} - {yearViewStart + 11}
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
on:click|stopPropagation={goToNext}
|
||||||
|
class="p-2 rounded-full hover:bg-slate-100 dark:hover:bg-slate-700 focus:outline-none transition-colors"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5 text-slate-600 dark:text-slate-300" fill="none" viewBox="0 0 24 24" stroke="currentColor"
|
||||||
|
><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /></svg
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if currentView === 'days'}
|
||||||
|
<div class="grid grid-cols-7 gap-1 text-center text-xs font-semibold text-slate-500 dark:text-slate-400 mb-2">
|
||||||
|
<div class="text-red-500 dark:text-red-400">일</div>
|
||||||
|
<div>월</div>
|
||||||
|
<div>화</div>
|
||||||
|
<div>수</div>
|
||||||
|
<div>목</div>
|
||||||
|
<div>금</div>
|
||||||
|
<div>토</div>
|
||||||
|
</div>
|
||||||
|
<div role="group" class="grid grid-cols-7 gap-1" on:mouseleave={() => (hoveredDate = null)}>
|
||||||
|
{#each calendarDays as day}
|
||||||
|
{#if day}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
on:click={() => handleDateClick(day)}
|
||||||
|
on:mouseenter={() => (hoveredDate = day)}
|
||||||
|
class="w-10 h-10 flex items-center justify-center text-sm transition-colors duration-150 focus:outline-none rounded-full
|
||||||
|
{ (isInRange(day) || isHoverRange(day)) ? 'dark:bg-blue-500/30' : '' }"
|
||||||
|
class:text-slate-700={!isSunday(day)}
|
||||||
|
class:dark:text-slate-200={!isSunday(day)}
|
||||||
|
class:text-red-500={isSunday(day)}
|
||||||
|
class:dark:text-red-400={isSunday(day)}
|
||||||
|
class:text-white={isSameDay(day, internalStartDate) || isSameDay(day, internalEndDate)}
|
||||||
|
class:dark:text-white={isSameDay(day, internalStartDate) || isSameDay(day, internalEndDate)}
|
||||||
|
class:bg-blue-600={isSameDay(day, internalStartDate) || isSameDay(day, internalEndDate)}
|
||||||
|
class:!rounded-l-full={isSameDay(day, internalStartDate)}
|
||||||
|
class:!rounded-r-full={isSameDay(day, internalEndDate)}
|
||||||
|
class:bg-blue-100={isInRange(day) || isHoverRange(day)}
|
||||||
|
class:hover:bg-slate-200={!isSameDay(day, internalStartDate) && !isSameDay(day, internalEndDate)}
|
||||||
|
class:dark:hover:bg-slate-700={!isSameDay(day, internalStartDate) && !isSameDay(day, internalEndDate)}
|
||||||
|
class:ring-2={isToday(day)}
|
||||||
|
class:ring-blue-500={isToday(day)}
|
||||||
|
>
|
||||||
|
{day.getDate()}
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<div />
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else if currentView === 'months'}
|
||||||
|
<div class="grid grid-cols-3 gap-2">
|
||||||
|
{#each monthNames as monthName, i}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
on:click|stopPropagation={() => selectMonth(i)}
|
||||||
|
class="p-4 text-sm font-semibold rounded-lg transition-colors"
|
||||||
|
class:bg-blue-600={i === currentDate.getMonth()}
|
||||||
|
class:text-white={i === currentDate.getMonth()}
|
||||||
|
class:hover:bg-slate-100={i !== currentDate.getMonth()}
|
||||||
|
class:dark:hover:bg-slate-700={i !== currentDate.getMonth()}
|
||||||
|
>
|
||||||
|
{monthName}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else if currentView === 'years'}
|
||||||
|
<div class="grid grid-cols-4 gap-2">
|
||||||
|
{#each yearsInView as year}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
on:click|stopPropagation={() => selectYear(year)}
|
||||||
|
class="p-3 text-sm font-semibold rounded-lg transition-colors"
|
||||||
|
class:bg-blue-600={year === currentDate.getFullYear()}
|
||||||
|
class:text-white={year === currentDate.getFullYear()}
|
||||||
|
class:hover:bg-slate-100={year !== currentDate.getFullYear()}
|
||||||
|
class:dark:hover:bg-slate-700={year !== currentDate.getFullYear()}
|
||||||
|
>
|
||||||
|
{year}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="mt-4 pt-4 border-t border-slate-200 dark:border-slate-700">
|
||||||
|
<div class="grid grid-cols-5 gap-2">
|
||||||
|
<button type="button" on:click={goToToday} class="text-xs text-center font-semibold px-2 py-2 rounded-full bg-slate-100 hover:bg-slate-200 dark:bg-slate-700 dark:hover:bg-slate-600 transition-colors">Today</button>
|
||||||
|
<button type="button" on:click={() => setRange(1, 'week')} class="text-xs text-center font-semibold px-2 py-2 rounded-full bg-slate-100 hover:bg-slate-200 dark:bg-slate-700 dark:hover:bg-slate-600 transition-colors">1주</button>
|
||||||
|
<button type="button" on:click={() => setRange(2, 'week')} class="text-xs text-center font-semibold px-2 py-2 rounded-full bg-slate-100 hover:bg-slate-200 dark:bg-slate-700 dark:hover:bg-slate-600 transition-colors">2주</button>
|
||||||
|
<button type="button" on:click={() => setRange(3, 'week')} class="text-xs text-center font-semibold px-2 py-2 rounded-full bg-slate-100 hover:bg-slate-200 dark:bg-slate-700 dark:hover:bg-slate-600 transition-colors">3주</button>
|
||||||
|
<button type="button" on:click={() => setRange(4, 'week')} class="text-xs text-center font-semibold px-2 py-2 rounded-full bg-slate-100 hover:bg-slate-200 dark:bg-slate-700 dark:hover:bg-slate-600 transition-colors">4주</button>
|
||||||
|
|
||||||
|
<button type="button" on:click={() => setRange(1, 'month')} class="text-xs text-center font-semibold px-2 py-2 rounded-full bg-slate-100 hover:bg-slate-200 dark:bg-slate-700 dark:hover:bg-slate-600 transition-colors">1개월</button>
|
||||||
|
<button type="button" on:click={() => setRange(2, 'month')} class="text-xs text-center font-semibold px-2 py-2 rounded-full bg-slate-100 hover:bg-slate-200 dark:bg-slate-700 dark:hover:bg-slate-600 transition-colors">2개월</button>
|
||||||
|
<button type="button" on:click={() => setRange(3, 'month')} class="text-xs text-center font-semibold px-2 py-2 rounded-full bg-slate-100 hover:bg-slate-200 dark:bg-slate-700 dark:hover:bg-slate-600 transition-colors">3개월</button>
|
||||||
|
<button type="button" on:click={() => setRange(4, 'month')} class="text-xs text-center font-semibold px-2 py-2 rounded-full bg-slate-100 hover:bg-slate-200 dark:bg-slate-700 dark:hover:bg-slate-600 transition-colors">4개월</button>
|
||||||
|
<button type="button" on:click={() => setRange(5, 'month')} class="text-xs text-center font-semibold px-2 py-2 rounded-full bg-slate-100 hover:bg-slate-200 dark:bg-slate-700 dark:hover:bg-slate-600 transition-colors">5개월</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
1255
src/lib/components/Viewer.svelte
Normal file
1255
src/lib/components/Viewer.svelte
Normal file
File diff suppressed because it is too large
Load Diff
1
src/lib/index.ts
Normal file
1
src/lib/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
// place files you want to import through the `$lib` alias in this folder.
|
||||||
14
src/lib/server/db.ts
Normal file
14
src/lib/server/db.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// src/lib/server/db.ts
|
||||||
|
|
||||||
|
import pg, { type QueryResult } from 'pg';
|
||||||
|
import { PRIVATE_POSTGRES_URL } from '$env/static/private';
|
||||||
|
|
||||||
|
// Pool 클래스로 pool 객체의 타입을 명시합니다.
|
||||||
|
const pool = new pg.Pool({
|
||||||
|
connectionString: PRIVATE_POSTGRES_URL
|
||||||
|
});
|
||||||
|
|
||||||
|
export default {
|
||||||
|
// query 함수의 매개변수와 반환 값에 타입을 지정합니다.
|
||||||
|
query: (text: string, params: any[]): Promise<QueryResult<any>> => pool.query(text, params)
|
||||||
|
};
|
||||||
30
src/lib/store/theme.ts
Normal file
30
src/lib/store/theme.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// src/lib/store/theme.ts
|
||||||
|
|
||||||
|
import { writable } from 'svelte/store';
|
||||||
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
|
export type Theme = 'light' | 'dark' | 'system';
|
||||||
|
|
||||||
|
// 1. 기본 테마를 'dark'로 설정합니다.
|
||||||
|
const defaultValue: Theme = 'dark';
|
||||||
|
|
||||||
|
// 2. 브라우저 환경에서만 localStorage 값을 읽어옵니다.
|
||||||
|
// 저장된 값이 없으면 위에서 설정한 'dark'를 기본값으로 사용합니다.
|
||||||
|
const initialValue = browser ? (localStorage.getItem('theme') as Theme) || defaultValue : defaultValue;
|
||||||
|
|
||||||
|
// 3. Writable 스토어를 생성합니다.
|
||||||
|
const theme = writable<Theme>(initialValue);
|
||||||
|
|
||||||
|
// 4. 스토어의 값이 변경될 때마다 localStorage에 저장하고 <html> 태그의 클래스를 업데이트합니다.
|
||||||
|
if (browser) {
|
||||||
|
theme.subscribe((value) => {
|
||||||
|
// <html> 요소에 'dark' 클래스를 추가하거나 제거합니다.
|
||||||
|
const isDark = value === 'dark' || (value === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||||
|
document.documentElement.classList.toggle('dark', isDark);
|
||||||
|
|
||||||
|
// 사용자의 테마 선택을 localStorage에 저장합니다.
|
||||||
|
localStorage.setItem('theme', value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { theme };
|
||||||
135
src/lib/types.ts
Normal file
135
src/lib/types.ts
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
// --- TypeScript 타입 정의 ---
|
||||||
|
export interface EmscriptenModule {
|
||||||
|
ccall: (ident: string, returnType: string | null, argTypes: string[], args: any[]) => any;
|
||||||
|
addFunction: (func: Function, signature: string) => number;
|
||||||
|
_malloc: (size: number) => number;
|
||||||
|
HEAPU8: Uint8Array;
|
||||||
|
canvas: HTMLCanvasElement;
|
||||||
|
setStatus: (text: string) => void;
|
||||||
|
UTF8ToString: (ptr: number) => string;
|
||||||
|
_free: (ptr: number) => void;
|
||||||
|
DoMouseDown: (x: number, y: number) => void;
|
||||||
|
DoMouseUp: (x: number, y: number) => void;
|
||||||
|
DoMouseMove: (x: number, y: number) => void;
|
||||||
|
DoMouseWheelZoom: (deltaY: number) => void;
|
||||||
|
DoPinchZoom: (scale: number, x: number, y: number) => void;
|
||||||
|
_getDumpJson: () => JSON|any;
|
||||||
|
_ParseDicomToPixelData: (buffer: number, nDataSize: number) => number;
|
||||||
|
_GetTotalFrames: () => number;
|
||||||
|
_GetFrameWidth: () => number;
|
||||||
|
_GetFrameHeight: () => number;
|
||||||
|
_GetFrameSizeUncompressed: () => number;
|
||||||
|
_GetPaletteEntries: () => number;
|
||||||
|
_GetBitsAllocated: () => number;
|
||||||
|
_GetBitsStored: () => number;
|
||||||
|
_IsUseMonochrome1: () => boolean;
|
||||||
|
_GetSamplesPerPixel: () => number;
|
||||||
|
_GetDefaultWindowCenter: () => number;
|
||||||
|
_GetDefaultWindowWidth: () => number;
|
||||||
|
_GetColorType: () => number;
|
||||||
|
_GetWidth: () => number;
|
||||||
|
_GetHeight: () => number;
|
||||||
|
_GetFrameTime: () => number;
|
||||||
|
_GetFrameDelay: () => number;
|
||||||
|
|
||||||
|
_GetPixelData: () => number; // Returns a pointer (memory address)
|
||||||
|
_GetPaletteData: () => number; // Returns a pointer (memory address)
|
||||||
|
_SetDicomData: (
|
||||||
|
pPixelData: number,
|
||||||
|
pPaletteData: number,
|
||||||
|
nFrameWidth: number,
|
||||||
|
nFrameHeight: number,
|
||||||
|
nBitsAllocated: number,
|
||||||
|
nBitsStored: number,
|
||||||
|
nSamplesPerPixel: number,
|
||||||
|
nDefaultWindowWidth: number,
|
||||||
|
nDefaultWindowCenter: number,
|
||||||
|
nPaletteEntries: number,
|
||||||
|
nColortype: number,
|
||||||
|
bUseMonochrome1: boolean,
|
||||||
|
nTotalFrames: number,
|
||||||
|
fFrameTime: number,
|
||||||
|
fFrameDelay: number
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
// ✨ Wasm 모듈 생성 시 옵션을 위한 타입 추가
|
||||||
|
wasmBinary?: ArrayBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DCM_ViewerState = {
|
||||||
|
fileUrl: string | null | undefined; //Loading중인 fileUrl
|
||||||
|
visible: boolean; //Viewer가 Visible 상태인지 관리 1x1, 2x2, 3x3에 따라 invisible상태일수 있음(이때 opengl update안하게함)
|
||||||
|
viewerID: number; //Viewer의 Index
|
||||||
|
WindowWidthLevel?: { //Viewer의 Window Width/Level
|
||||||
|
WindowWidth: number;
|
||||||
|
WindowCenter: number;
|
||||||
|
};
|
||||||
|
camera: { //Viewer의 Camera State
|
||||||
|
aspect: number;
|
||||||
|
basepan_x: number;
|
||||||
|
basepan_y: number;
|
||||||
|
cameraUpdated: boolean;
|
||||||
|
pan_x: number;
|
||||||
|
pan_y: number;
|
||||||
|
viewport_x: number;
|
||||||
|
viewport_y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
windowResized: boolean;
|
||||||
|
zoom: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
//DicomImage 타입을 정의합니다.
|
||||||
|
export interface DicomImage {
|
||||||
|
metadata: {
|
||||||
|
totalFrames: number; //전체 프레임 개수(MultiFrame Image일수 있음)
|
||||||
|
imageWidth: number;
|
||||||
|
imageHeight: number;
|
||||||
|
frameSizeUncompressed: number;
|
||||||
|
paletteEntries: number;
|
||||||
|
bitsAllocated: number;
|
||||||
|
bitsStored: number;
|
||||||
|
samplesPerPixel: number;
|
||||||
|
defaultWindowWidth: number;
|
||||||
|
defaultWindowCenter: number;
|
||||||
|
colorType: number;
|
||||||
|
bUseMonochrome1: boolean;
|
||||||
|
frameTime: number;
|
||||||
|
frameDelay: number;
|
||||||
|
};
|
||||||
|
pixelData?: Uint8Array;
|
||||||
|
paletteData?: Uint8Array; // 팔레트는 선택사항이므로 '?'를 붙입니다.
|
||||||
|
fileUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Enum 및 상태 변수 ---
|
||||||
|
export enum InteractionMode {
|
||||||
|
MODE_NONE = 0x00,
|
||||||
|
MODE_PAN = 0x01,
|
||||||
|
MODE_WIDTHLEVEL = 0x02,
|
||||||
|
MODE_ZOOM = 0x04,
|
||||||
|
MODE_SCROLL = 0x08,
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 타입 정의 ---
|
||||||
|
export type DownloadState = {
|
||||||
|
url: string;
|
||||||
|
total: number;
|
||||||
|
loaded: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ViewerInstance = {
|
||||||
|
|
||||||
|
setInteractionMode: (mode: number) => void;
|
||||||
|
resetView?: () => void;
|
||||||
|
ResetWindowWidthLevel?: () => void;
|
||||||
|
UpdateViewerState?: (state: DCM_ViewerState) => void;
|
||||||
|
GetViewerStateFromWasm: () => DCM_ViewerState;
|
||||||
|
loadFileAndProcessInWasm: (url: string, state: DCM_ViewerState, updateViewerState?: boolean) => Promise<void>;
|
||||||
|
processDicomData: (packet: ArrayBuffer, state: DCM_ViewerState, updateViewerState?: boolean) => Promise<void>;
|
||||||
|
loadDicomImage: (imageData: DicomImage, transferViewerState: DCM_ViewerState, updateViewerState: boolean) => Promise<void>;
|
||||||
|
clearViewer: () => void;
|
||||||
|
|
||||||
|
initialize: () => Promise<void>;
|
||||||
|
};
|
||||||
11978
src/lib/viewer/test.js
Normal file
11978
src/lib/viewer/test.js
Normal file
File diff suppressed because one or more lines are too long
1
src/lib/viewer/test.wasm.map
Normal file
1
src/lib/viewer/test.wasm.map
Normal file
File diff suppressed because one or more lines are too long
60
src/routes/+layout.svelte
Normal file
60
src/routes/+layout.svelte
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<svelte:head>
|
||||||
|
<script>
|
||||||
|
// 이 스크립트는 Svelte 컴포넌트 로직보다 먼저, 동기적으로 실행되어 깜빡임을 방지합니다.
|
||||||
|
(function() {
|
||||||
|
try {
|
||||||
|
const theme = localStorage.getItem('theme');
|
||||||
|
const docEl = document.documentElement;
|
||||||
|
|
||||||
|
if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||||
|
docEl.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
docEl.classList.remove('dark');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// localStorage 접근이 막혔거나 다른 에러 발생 시 무시
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
// 1. 스타일시트 및 모듈 임포트
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
import '../app.postcss'; // 사용자 파일의 스타일시트 임포트
|
||||||
|
import { theme } from '$lib/store/theme';
|
||||||
|
import { browser } from '$app/environment';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
// 2. 생명주기 함수 (onMount)
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// 컴포넌트가 마운트될 때 (브라우저에서) tw-elements를 동적으로 가져옵니다.
|
||||||
|
// 두 파일의 onMount 로직을 하나로 합쳤습니다.
|
||||||
|
onMount(async () => {
|
||||||
|
if (browser) {
|
||||||
|
// 이 코드는 클라이언트 측에서만 실행되어야 합니다.
|
||||||
|
try {
|
||||||
|
await import('tw-elements');
|
||||||
|
//console.log('tw-elements loaded successfully.');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load tw-elements:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. 반응형 구문 (Reactive Statement)
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// theme 스토어($theme)의 값이 바뀔 때마다 실행됩니다.
|
||||||
|
// <html> 태그에 'dark' 클래스를 추가/제거하여 TailwindCSS 다크 모드를 제어합니다.
|
||||||
|
$: if (browser) {
|
||||||
|
const isDark =
|
||||||
|
$theme === 'dark' ||
|
||||||
|
($theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||||
|
|
||||||
|
document.documentElement.classList.toggle('dark', isDark);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-200 min-h-screen">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
27
src/routes/+page.svelte
Normal file
27
src/routes/+page.svelte
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<div class="flex flex-col justify-center items-center h-screen text-center p-4">
|
||||||
|
<h1 class="text-4xl md:text-5xl font-extrabold mb-4">
|
||||||
|
<span class="text-transparent bg-clip-text bg-gradient-to-r from-blue-500 to-teal-400">
|
||||||
|
SDCMWEB Viewer
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
<p class="text-lg text-gray-600 dark:text-gray-300 mb-8">
|
||||||
|
Storage Solution for Doctors Medical Communication Web Viewer
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="/worklist"
|
||||||
|
class="inline-flex items-center justify-center px-6 py-3 text-lg font-bold text-white bg-blue-600 rounded-lg hover:bg-blue-700 focus:ring-4 focus:ring-blue-300 dark:focus:ring-blue-800 transition-colors"
|
||||||
|
>
|
||||||
|
워크리스트로 이동
|
||||||
|
<svg
|
||||||
|
class="ml-2 w-5 h-5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2.5"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path d="M14 5l7 7m0 0l-7 7m7-7H3" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
45
src/routes/api/dicom/+server.ts
Normal file
45
src/routes/api/dicom/+server.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { json, type RequestHandler } from '@sveltejs/kit';
|
||||||
|
import db from '$lib/server/db';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 기본 조회 API: StudyInstanceUID 또는 accession_number로 이미지를 검색합니다.
|
||||||
|
* StudyInstanceUID가 존재하면 최우선으로 처리됩니다.
|
||||||
|
*/
|
||||||
|
export const GET: RequestHandler = async ({ url }) => {
|
||||||
|
const studyInstanceUIDs = url.searchParams.getAll('StudyInstanceUID');
|
||||||
|
const accessionNumbers = url.searchParams.getAll('accession_number');
|
||||||
|
|
||||||
|
// 1. StudyInstanceUID 파라미터가 있으면 최우선으로 처리
|
||||||
|
if (studyInstanceUIDs.length > 0) {
|
||||||
|
console.log('Calling get_images_by_studyinstanceuid with:', studyInstanceUIDs);
|
||||||
|
try {
|
||||||
|
const result = await db.query('SELECT * FROM public.get_images_by_studyinstanceuid($1)', [
|
||||||
|
studyInstanceUIDs
|
||||||
|
]);
|
||||||
|
return json(result.rows);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error calling get_images_by_studyinstanceuid:', err);
|
||||||
|
return json({ message: 'DB 함수 호출 중 오류 발생' }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. StudyInstanceUID는 없고 accession_number 파라미터만 있을 경우 처리
|
||||||
|
if (accessionNumbers.length > 0) {
|
||||||
|
console.log('Calling get_images_by_accession_number with:', accessionNumbers);
|
||||||
|
try {
|
||||||
|
const result = await db.query('SELECT * FROM public.get_images_by_accession_number($1)', [
|
||||||
|
accessionNumbers
|
||||||
|
]);
|
||||||
|
return json(result.rows);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error calling get_images_by_accession_number:', err);
|
||||||
|
return json({ message: 'DB 함수 호출 중 오류 발생' }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 두 파라미터가 모두 없는 경우
|
||||||
|
return json(
|
||||||
|
{ message: '검색 조건(StudyInstanceUID 또는 accession_number)이 하나 이상 필요합니다.' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
};
|
||||||
27
src/routes/api/dicom/search/+server.ts
Normal file
27
src/routes/api/dicom/search/+server.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
// ✅ 새 파일: src/routes/api/dicom/search/+server.ts
|
||||||
|
import { json, type RequestHandler } from '@sveltejs/kit';
|
||||||
|
import db from '$lib/server/db';
|
||||||
|
|
||||||
|
// 이 엔드포인트는 모든 조건을 AND로 처리합니다.
|
||||||
|
export const GET: RequestHandler = async ({ url }) => {
|
||||||
|
const studyInstanceUIDs = url.searchParams.getAll('StudyInstanceUID');
|
||||||
|
const accessionNumbers = url.searchParams.getAll('accession_number');
|
||||||
|
|
||||||
|
if (studyInstanceUIDs.length === 0 && accessionNumbers.length === 0) {
|
||||||
|
return json({ message: '검색 조건이 하나 이상 필요합니다.' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await db.query(
|
||||||
|
'SELECT * FROM public.get_images_combined($1, $2)',
|
||||||
|
[
|
||||||
|
studyInstanceUIDs.length > 0 ? studyInstanceUIDs : null,
|
||||||
|
accessionNumbers.length > 0 ? accessionNumbers : null
|
||||||
|
]
|
||||||
|
);
|
||||||
|
return json(result.rows);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('API Error in /api/dicom/search:', err);
|
||||||
|
return json({ message: '서버 내부 오류가 발생했습니다.' }, { status: 500 });
|
||||||
|
}
|
||||||
|
};
|
||||||
42
src/routes/api/download/+server.ts
Normal file
42
src/routes/api/download/+server.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
// src/routes/api/download/+server.ts
|
||||||
|
|
||||||
|
import { error } from '@sveltejs/kit';
|
||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { Readable } from 'node:stream'; // 'node:stream'에서 Readable을 가져옵니다.
|
||||||
|
|
||||||
|
const ALLOWED_BASE_PATH = '/workspace/storage/';
|
||||||
|
|
||||||
|
export const GET: RequestHandler = async ({ url }) => {
|
||||||
|
const fileLocation = url.searchParams.get('file');
|
||||||
|
|
||||||
|
if (!fileLocation) {
|
||||||
|
throw error(400, '파일 경로가 필요합니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedPath = path.resolve(fileLocation);
|
||||||
|
if (!resolvedPath.startsWith(ALLOWED_BASE_PATH) || resolvedPath.includes('..')) {
|
||||||
|
throw error(403, '접근이 금지된 경로입니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stats = await fs.promises.stat(resolvedPath);
|
||||||
|
const nodeStream = fs.createReadStream(resolvedPath);
|
||||||
|
|
||||||
|
// Node.js 스트림을 웹 표준 ReadableStream으로 변환합니다.
|
||||||
|
const webStream = Readable.toWeb(nodeStream);
|
||||||
|
|
||||||
|
// 변환된 웹 스트림을 Response 객체에 전달합니다.
|
||||||
|
return new Response(webStream as ReadableStream, {
|
||||||
|
headers: {
|
||||||
|
'Content-Disposition': `attachment; filename="${path.basename(resolvedPath)}"`,
|
||||||
|
'Content-Type': 'application/octet-stream',
|
||||||
|
'Content-Length': stats.size.toString()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('File stream/response error:', err);
|
||||||
|
throw error(404, '파일을 찾을 수 없거나 읽는 중 오류가 발생했습니다.');
|
||||||
|
}
|
||||||
|
};
|
||||||
21
src/routes/api/log/+server.ts
Normal file
21
src/routes/api/log/+server.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { json, type RequestHandler } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 클라이언트로부터 POST 요청을 받아 로그를 서버 콘솔에 출력합니다.
|
||||||
|
*/
|
||||||
|
export const POST: RequestHandler = async ({ request }) => {
|
||||||
|
try {
|
||||||
|
// 요청 본문에서 JSON 데이터를 추출합니다.
|
||||||
|
const logData = await request.json();
|
||||||
|
|
||||||
|
// 서버 터미널에 예쁘게 로그를 출력합니다.
|
||||||
|
console.log(`[CLIENT LOG]:`, JSON.stringify(logData, null, 2));
|
||||||
|
|
||||||
|
// 클라이언트에게 성공적으로 수신했음을 알립니다.
|
||||||
|
return json({ success: true, message: 'Log received' });
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[LOGGING ERROR]:', error);
|
||||||
|
return json({ success: false, message: 'Failed to process log' }, { status: 500 });
|
||||||
|
}
|
||||||
|
};
|
||||||
47
src/routes/api/series/[studyID]/+server.ts
Normal file
47
src/routes/api/series/[studyID]/+server.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { json, error } from '@sveltejs/kit';
|
||||||
|
import db from '$lib/server/db';
|
||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
|
||||||
|
// Series 항목에 대한 타입 정의 (필요에 따라 수정)
|
||||||
|
export interface SeriesItem {
|
||||||
|
id: number;
|
||||||
|
series_number: string;
|
||||||
|
series_date: string;
|
||||||
|
series_time: string;
|
||||||
|
series_description: string;
|
||||||
|
modality: string;
|
||||||
|
image_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GET: RequestHandler = async ({ params }) => {
|
||||||
|
const studyId = params.studyID;
|
||||||
|
|
||||||
|
if (!studyId) {
|
||||||
|
throw error(400, 'Study ID가 필요합니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// studyId를 사용해 series 목록을 조회하는 쿼리
|
||||||
|
// study_index가 studyId와 일치하는 series를 찾습니다. 실제 DB 스키마에 맞게 컬럼명을 확인하세요.
|
||||||
|
const sqlQuery = `
|
||||||
|
SELECT
|
||||||
|
ss.index as id,
|
||||||
|
ss.series_number,
|
||||||
|
TO_CHAR(TO_DATE(ss.series_date, 'YYYYMMDD'), 'YYYY/MM/DD') AS series_date,
|
||||||
|
TO_CHAR(TO_TIMESTAMP(ss.series_time, 'HH24MISS'), 'HH24:MI:SS') AS series_time,
|
||||||
|
series_description,
|
||||||
|
modality,
|
||||||
|
(select count(*) from public.spacs_image as i where i.series_index=ss.index) as image_count
|
||||||
|
FROM public.spacs_series as ss
|
||||||
|
WHERE study_index = $1
|
||||||
|
ORDER BY series_number
|
||||||
|
`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await db.query(sqlQuery, [studyId]);
|
||||||
|
const seriesList: SeriesItem[] = result.rows;
|
||||||
|
return json(seriesList);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('API Error fetching series list:', err);
|
||||||
|
throw error(500, 'Series 목록을 가져오는 데 실패했습니다.');
|
||||||
|
}
|
||||||
|
};
|
||||||
220
src/routes/t1/+page.svelte
Normal file
220
src/routes/t1/+page.svelte
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
// --- 상태(State) 변수 ---
|
||||||
|
let isOpen = false; // 데이트피커 팝업 표시 여부
|
||||||
|
let currentDate = new Date(); // 달력에 표시할 현재 월/년
|
||||||
|
let startDate: Date | null = null;
|
||||||
|
let endDate: Date | null = null;
|
||||||
|
let hoveredDate: Date | null = null; // 마우스 호버 날짜 (미리보기용)
|
||||||
|
|
||||||
|
let datepickerNode: HTMLElement; // 데이트피커 DOM 노드 (외부 클릭 감지용)
|
||||||
|
|
||||||
|
// --- 반응형 파생(Derived) 변수 ---
|
||||||
|
// $: Svelte의 반응성 선언. 의존하는 변수가 바뀌면 자동으로 재계산됩니다.
|
||||||
|
$: year = currentDate.getFullYear();
|
||||||
|
$: month = currentDate.getMonth();
|
||||||
|
|
||||||
|
// 달력 상단에 표시될 '년 월' 텍스트
|
||||||
|
$: monthYearText = `${year}년 ${month + 1}월`;
|
||||||
|
|
||||||
|
// 메인 인풋에 표시될 날짜 범위 텍스트
|
||||||
|
$: rangeText =
|
||||||
|
startDate && endDate
|
||||||
|
? `${formatSimpleDate(startDate)} - ${formatSimpleDate(endDate)}`
|
||||||
|
: '날짜 범위를 선택하세요';
|
||||||
|
|
||||||
|
// 달력 그리드를 구성하는 날짜 배열 생성 로직
|
||||||
|
$: calendarDays = (() => {
|
||||||
|
const days = [];
|
||||||
|
const firstDayOfMonth = new Date(year, month, 1);
|
||||||
|
const lastDateOfMonth = new Date(year, month + 1, 0).getDate();
|
||||||
|
|
||||||
|
// 주의 시작을 월요일로 맞추기 위한 로직
|
||||||
|
const dayOfWeek = firstDayOfMonth.getDay(); // 0=일, 1=월, ..., 6=토
|
||||||
|
const offset = dayOfWeek === 0 ? 6 : dayOfWeek - 1; // 0=월, 1=화, ...
|
||||||
|
|
||||||
|
// 이전 달의 날짜로 앞부분 채우기
|
||||||
|
for (let i = 0; i < offset; i++) {
|
||||||
|
days.push(null);
|
||||||
|
}
|
||||||
|
// 현재 달의 날짜 채우기
|
||||||
|
for (let day = 1; day <= lastDateOfMonth; day++) {
|
||||||
|
days.push(new Date(year, month, day));
|
||||||
|
}
|
||||||
|
return days;
|
||||||
|
})();
|
||||||
|
|
||||||
|
// --- 이벤트 핸들러 및 함수 ---
|
||||||
|
function goToPrevMonth() {
|
||||||
|
currentDate = new Date(year, month - 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToNextMonth() {
|
||||||
|
currentDate = new Date(year, month + 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDateClick(day: Date) {
|
||||||
|
// 종료일까지 선택 완료된 상태에서 다시 클릭하면, 선택 초기화 후 새 시작일 지정
|
||||||
|
if (startDate && endDate) {
|
||||||
|
startDate = day;
|
||||||
|
endDate = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 시작일이 없으면, 시작일로 지정
|
||||||
|
if (!startDate) {
|
||||||
|
startDate = day;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 클릭한 날짜가 시작일보다 이전이면, 시작일을 변경
|
||||||
|
if (day < startDate) {
|
||||||
|
startDate = day;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 그 외의 경우, 종료일로 지정하고 팝업 닫기
|
||||||
|
endDate = day;
|
||||||
|
isOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearDates() {
|
||||||
|
startDate = null;
|
||||||
|
endDate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 헬퍼(Helper) 함수 ---
|
||||||
|
function formatSimpleDate(date: Date | null): string {
|
||||||
|
if (!date) return '';
|
||||||
|
return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 날짜가 같은지 확인 (시간은 무시)
|
||||||
|
const isSameDay = (d1: Date, d2: Date | null): boolean =>
|
||||||
|
!!d2 &&
|
||||||
|
d1.getFullYear() === d2.getFullYear() &&
|
||||||
|
d1.getMonth() === d2.getMonth() &&
|
||||||
|
d1.getDate() === d2.getDate();
|
||||||
|
|
||||||
|
// 날짜가 시작일과 종료일 사이에 있는지 확인
|
||||||
|
const isInRange = (day: Date): boolean =>
|
||||||
|
!!startDate && !!endDate && day > startDate && day < endDate;
|
||||||
|
|
||||||
|
// 사용자가 범위를 선택하는 동안 미리보기를 위한 함수
|
||||||
|
const isHoverRange = (day: Date): boolean =>
|
||||||
|
!!startDate && !endDate && !!hoveredDate &&
|
||||||
|
((day > startDate && day < hoveredDate) || (day < startDate && day > hoveredDate));
|
||||||
|
|
||||||
|
// 외부 클릭 시 데이트피커 닫기
|
||||||
|
onMount(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (isOpen && datepickerNode && !datepickerNode.contains(event.target as Node)) {
|
||||||
|
isOpen = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('click', handleClickOutside);
|
||||||
|
return () => window.removeEventListener('click', handleClickOutside);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="bg-gray-100 flex items-center justify-center min-h-screen">
|
||||||
|
<div class="relative w-80 font-sans" bind:this={datepickerNode}>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between bg-white border border-gray-300 rounded-md p-3 cursor-pointer"
|
||||||
|
on:click={() => (isOpen = !isOpen)}
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5 text-gray-500 mr-2"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
><path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/></svg
|
||||||
|
>
|
||||||
|
<span class="text-gray-700">{rangeText}</span>
|
||||||
|
</div>
|
||||||
|
<svg
|
||||||
|
class="w-4 h-4 text-gray-500 transition-transform duration-200"
|
||||||
|
class:-rotate-180={isOpen}
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if isOpen}
|
||||||
|
<div
|
||||||
|
class="absolute top-full left-0 mt-2 w-full bg-white border border-gray-200 rounded-lg shadow-lg z-10 p-4"
|
||||||
|
>
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
on:click={goToPrevMonth}
|
||||||
|
class="p-2 rounded-full hover:bg-gray-100 focus:outline-none"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div class="text-lg font-semibold text-gray-800">{monthYearText}</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
on:click={goToNextMonth}
|
||||||
|
class="p-2 rounded-full hover:bg-gray-100 focus:outline-none"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-7 gap-1 text-center text-xs text-gray-500 mb-2">
|
||||||
|
<div>Mo</div>
|
||||||
|
<div>Tu</div>
|
||||||
|
<div>We</div>
|
||||||
|
<div>Th</div>
|
||||||
|
<div>Fr</div>
|
||||||
|
<div>Sa</div>
|
||||||
|
<div>Su</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-7 gap-1" on:mouseleave={() => (hoveredDate = null)}>
|
||||||
|
{#each calendarDays as day}
|
||||||
|
{#if day}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
on:click={() => handleDateClick(day)}
|
||||||
|
on:mouseenter={() => (hoveredDate = day)}
|
||||||
|
class="w-9 h-9 flex items-center justify-center text-sm transition-colors duration-150 focus:outline-none"
|
||||||
|
class:text-white={isSameDay(day, startDate) || isSameDay(day, endDate)}
|
||||||
|
class:bg-blue-600={isSameDay(day, startDate) || isSameDay(day, endDate)}
|
||||||
|
class:rounded-l-full={isSameDay(day, startDate)}
|
||||||
|
class:rounded-r-full={isSameDay(day, endDate)}
|
||||||
|
class:bg-blue-100={isInRange(day) || isHoverRange(day)}
|
||||||
|
class:hover:bg-blue-200={!isSameDay(day, startDate) && !isSameDay(day, endDate)}
|
||||||
|
class:rounded-full={!startDate && !endDate}
|
||||||
|
>
|
||||||
|
{day.getDate()}
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<div /> {/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between mt-4 pt-2 border-t">
|
||||||
|
<div class="text-sm text-gray-600">
|
||||||
|
<span class="font-semibold">{formatSimpleDate(startDate) || 'Start Date'}</span>
|
||||||
|
-
|
||||||
|
<span class="font-semibold">{formatSimpleDate(endDate) || 'End Date'}</span>
|
||||||
|
</div>
|
||||||
|
<button on:click={clearDates} class="text-sm text-blue-600 hover:text-blue-800">Clear</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
155
src/routes/t1/test.html
Normal file
155
src/routes/t1/test.html
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Tailwind CSS Range Datepicker</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-100 flex items-center justify-center h-screen">
|
||||||
|
|
||||||
|
<div class="antialiased sans-serif">
|
||||||
|
<div class="container mx-auto px-4 py-2 md:py-10">
|
||||||
|
<div class="w-80 bg-white rounded-lg shadow-lg p-4">
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<button id="prev-month" class="p-2 rounded-full hover:bg-gray-100 focus:outline-none">
|
||||||
|
<svg class="w-5 h-5 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div id="month-year" class="text-lg font-semibold text-gray-800"></div>
|
||||||
|
<button id="next-month" class="p-2 rounded-full hover:bg-gray-100 focus:outline-none">
|
||||||
|
<svg class="w-5 h-5 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-7 gap-1 text-center text-sm text-gray-500 mb-2">
|
||||||
|
<div>일</div>
|
||||||
|
<div>월</div>
|
||||||
|
<div>화</div>
|
||||||
|
<div>수</div>
|
||||||
|
<div>목</div>
|
||||||
|
<div>금</div>
|
||||||
|
<div>토</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="calendar-grid" class="grid grid-cols-7 gap-1">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 text-center">
|
||||||
|
<p class="text-sm text-gray-600">
|
||||||
|
<span id="start-date-display" class="font-medium">-</span> ~
|
||||||
|
<span id="end-date-display" class="font-medium">-</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const monthYearEl = document.getElementById('month-year');
|
||||||
|
const calendarGridEl = document.getElementById('calendar-grid');
|
||||||
|
const prevMonthBtn = document.getElementById('prev-month');
|
||||||
|
const nextMonthBtn = document.getElementById('next-month');
|
||||||
|
const startDateDisplay = document.getElementById('start-date-display');
|
||||||
|
const endDateDisplay = document.getElementById('end-date-display');
|
||||||
|
|
||||||
|
let currentDate = new Date();
|
||||||
|
let startDate = null;
|
||||||
|
let endDate = null;
|
||||||
|
|
||||||
|
function renderCalendar(date) {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = date.getMonth();
|
||||||
|
|
||||||
|
monthYearEl.textContent = `${year}년 ${month + 1}월`;
|
||||||
|
calendarGridEl.innerHTML = '';
|
||||||
|
|
||||||
|
const firstDayOfMonth = new Date(year, month, 1).getDay();
|
||||||
|
const lastDateOfMonth = new Date(year, month + 1, 0).getDate();
|
||||||
|
|
||||||
|
// Add empty cells for days before the first day of the month
|
||||||
|
for (let i = 0; i < firstDayOfMonth; i++) {
|
||||||
|
const emptyCell = document.createElement('div');
|
||||||
|
calendarGridEl.appendChild(emptyCell);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate calendar with dates
|
||||||
|
for (let day = 1; day <= lastDateOfMonth; day++) {
|
||||||
|
const dateCell = document.createElement('button');
|
||||||
|
dateCell.textContent = day;
|
||||||
|
dateCell.dataset.date = new Date(year, month, day).toISOString().split('T')[0];
|
||||||
|
dateCell.className = 'w-10 h-10 flex items-center justify-center rounded-full hover:bg-blue-100 cursor-pointer focus:outline-none';
|
||||||
|
|
||||||
|
const cellDate = new Date(year, month, day);
|
||||||
|
|
||||||
|
// Apply styles based on selection
|
||||||
|
if (startDate && endDate && cellDate >= startDate && cellDate <= endDate) {
|
||||||
|
dateCell.classList.add('bg-blue-200');
|
||||||
|
if (cellDate.getTime() === startDate.getTime()) {
|
||||||
|
dateCell.classList.add('bg-blue-500', 'text-white');
|
||||||
|
dateCell.classList.remove('bg-blue-200');
|
||||||
|
}
|
||||||
|
if (cellDate.getTime() === endDate.getTime()) {
|
||||||
|
dateCell.classList.add('bg-blue-500', 'text-white');
|
||||||
|
dateCell.classList.remove('bg-blue-200');
|
||||||
|
}
|
||||||
|
} else if (startDate && cellDate.getTime() === startDate.getTime()) {
|
||||||
|
dateCell.classList.add('bg-blue-500', 'text-white');
|
||||||
|
}
|
||||||
|
|
||||||
|
calendarGridEl.appendChild(dateCell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDateClick(e) {
|
||||||
|
if (e.target.dataset.date) {
|
||||||
|
const clickedDate = new Date(e.target.dataset.date);
|
||||||
|
|
||||||
|
if (!startDate || (startDate && endDate)) {
|
||||||
|
// Start a new selection
|
||||||
|
startDate = clickedDate;
|
||||||
|
endDate = null;
|
||||||
|
} else if (clickedDate < startDate) {
|
||||||
|
// If clicked date is before start date, set it as new start date
|
||||||
|
startDate = clickedDate;
|
||||||
|
} else {
|
||||||
|
// Set the end date
|
||||||
|
endDate = clickedDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDisplay();
|
||||||
|
renderCalendar(currentDate); // Re-render to show selection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDisplay() {
|
||||||
|
const options = { year: 'numeric', month: 'long', day: 'numeric' };
|
||||||
|
startDateDisplay.textContent = startDate ? startDate.toLocaleDateString('ko-KR', options) : '-';
|
||||||
|
endDateDisplay.textContent = endDate ? endDate.toLocaleDateString('ko-KR', options) : '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
prevMonthBtn.addEventListener('click', () => {
|
||||||
|
currentDate.setMonth(currentDate.getMonth() - 1);
|
||||||
|
renderCalendar(currentDate);
|
||||||
|
});
|
||||||
|
|
||||||
|
nextMonthBtn.addEventListener('click', () => {
|
||||||
|
currentDate.setMonth(currentDate.getMonth() + 1);
|
||||||
|
renderCalendar(currentDate);
|
||||||
|
});
|
||||||
|
|
||||||
|
calendarGridEl.addEventListener('click', handleDateClick);
|
||||||
|
|
||||||
|
// Initial render
|
||||||
|
renderCalendar(currentDate);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
220
src/routes/t2/+page.svelte
Normal file
220
src/routes/t2/+page.svelte
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
// --- 상태(State) 변수 ---
|
||||||
|
let isOpen = false; // 데이트피커 팝업 표시 여부
|
||||||
|
let currentDate = new Date(); // 달력에 표시할 현재 월/년
|
||||||
|
let startDate: Date | null = null;
|
||||||
|
let endDate: Date | null = null;
|
||||||
|
let hoveredDate: Date | null = null; // 마우스 호버 날짜 (미리보기용)
|
||||||
|
|
||||||
|
let datepickerNode: HTMLElement; // 데이트피커 DOM 노드 (외부 클릭 감지용)
|
||||||
|
|
||||||
|
// --- 반응형 파생(Derived) 변수 ---
|
||||||
|
// $: Svelte의 반응성 선언. 의존하는 변수가 바뀌면 자동으로 재계산됩니다.
|
||||||
|
$: year = currentDate.getFullYear();
|
||||||
|
$: month = currentDate.getMonth();
|
||||||
|
|
||||||
|
// 달력 상단에 표시될 '년 월' 텍스트
|
||||||
|
$: monthYearText = `${year}년 ${month + 1}월`;
|
||||||
|
|
||||||
|
// 메인 인풋에 표시될 날짜 범위 텍스트
|
||||||
|
$: rangeText =
|
||||||
|
startDate && endDate
|
||||||
|
? `${formatSimpleDate(startDate)} - ${formatSimpleDate(endDate)}`
|
||||||
|
: '날짜 범위를 선택하세요';
|
||||||
|
|
||||||
|
// 달력 그리드를 구성하는 날짜 배열 생성 로직
|
||||||
|
$: calendarDays = (() => {
|
||||||
|
const days = [];
|
||||||
|
const firstDayOfMonth = new Date(year, month, 1);
|
||||||
|
const lastDateOfMonth = new Date(year, month + 1, 0).getDate();
|
||||||
|
|
||||||
|
// 주의 시작을 월요일로 맞추기 위한 로직
|
||||||
|
const dayOfWeek = firstDayOfMonth.getDay(); // 0=일, 1=월, ..., 6=토
|
||||||
|
const offset = dayOfWeek === 0 ? 6 : dayOfWeek - 1; // 0=월, 1=화, ...
|
||||||
|
|
||||||
|
// 이전 달의 날짜로 앞부분 채우기
|
||||||
|
for (let i = 0; i < offset; i++) {
|
||||||
|
days.push(null);
|
||||||
|
}
|
||||||
|
// 현재 달의 날짜 채우기
|
||||||
|
for (let day = 1; day <= lastDateOfMonth; day++) {
|
||||||
|
days.push(new Date(year, month, day));
|
||||||
|
}
|
||||||
|
return days;
|
||||||
|
})();
|
||||||
|
|
||||||
|
// --- 이벤트 핸들러 및 함수 ---
|
||||||
|
function goToPrevMonth() {
|
||||||
|
currentDate = new Date(year, month - 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToNextMonth() {
|
||||||
|
currentDate = new Date(year, month + 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDateClick(day: Date) {
|
||||||
|
// 종료일까지 선택 완료된 상태에서 다시 클릭하면, 선택 초기화 후 새 시작일 지정
|
||||||
|
if (startDate && endDate) {
|
||||||
|
startDate = day;
|
||||||
|
endDate = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 시작일이 없으면, 시작일로 지정
|
||||||
|
if (!startDate) {
|
||||||
|
startDate = day;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 클릭한 날짜가 시작일보다 이전이면, 시작일을 변경
|
||||||
|
if (day < startDate) {
|
||||||
|
startDate = day;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 그 외의 경우, 종료일로 지정하고 팝업 닫기
|
||||||
|
endDate = day;
|
||||||
|
isOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearDates() {
|
||||||
|
startDate = null;
|
||||||
|
endDate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 헬퍼(Helper) 함수 ---
|
||||||
|
function formatSimpleDate(date: Date | null): string {
|
||||||
|
if (!date) return '';
|
||||||
|
return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 날짜가 같은지 확인 (시간은 무시)
|
||||||
|
const isSameDay = (d1: Date, d2: Date | null): boolean =>
|
||||||
|
!!d2 &&
|
||||||
|
d1.getFullYear() === d2.getFullYear() &&
|
||||||
|
d1.getMonth() === d2.getMonth() &&
|
||||||
|
d1.getDate() === d2.getDate();
|
||||||
|
|
||||||
|
// 날짜가 시작일과 종료일 사이에 있는지 확인
|
||||||
|
const isInRange = (day: Date): boolean =>
|
||||||
|
!!startDate && !!endDate && day > startDate && day < endDate;
|
||||||
|
|
||||||
|
// 사용자가 범위를 선택하는 동안 미리보기를 위한 함수
|
||||||
|
const isHoverRange = (day: Date): boolean =>
|
||||||
|
!!startDate && !endDate && !!hoveredDate &&
|
||||||
|
((day > startDate && day < hoveredDate) || (day < startDate && day > hoveredDate));
|
||||||
|
|
||||||
|
// 외부 클릭 시 데이트피커 닫기
|
||||||
|
onMount(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (isOpen && datepickerNode && !datepickerNode.contains(event.target as Node)) {
|
||||||
|
isOpen = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('click', handleClickOutside);
|
||||||
|
return () => window.removeEventListener('click', handleClickOutside);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="bg-gray-100 flex items-center justify-center min-h-screen">
|
||||||
|
<div class="relative w-80 font-sans" bind:this={datepickerNode}>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between bg-white border border-gray-300 rounded-md p-3 cursor-pointer"
|
||||||
|
on:click={() => (isOpen = !isOpen)}
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5 text-gray-500 mr-2"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
><path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/></svg
|
||||||
|
>
|
||||||
|
<span class="text-gray-700">{rangeText}</span>
|
||||||
|
</div>
|
||||||
|
<svg
|
||||||
|
class="w-4 h-4 text-gray-500 transition-transform duration-200"
|
||||||
|
class:-rotate-180={isOpen}
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if isOpen}
|
||||||
|
<div
|
||||||
|
class="absolute top-full left-0 mt-2 w-full bg-white border border-gray-200 rounded-lg shadow-lg z-10 p-4"
|
||||||
|
>
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
on:click={goToPrevMonth}
|
||||||
|
class="p-2 rounded-full hover:bg-gray-100 focus:outline-none"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div class="text-lg font-semibold text-gray-800">{monthYearText}</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
on:click={goToNextMonth}
|
||||||
|
class="p-2 rounded-full hover:bg-gray-100 focus:outline-none"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-7 gap-1 text-center text-xs text-gray-500 mb-2">
|
||||||
|
<div>Mo</div>
|
||||||
|
<div>Tu</div>
|
||||||
|
<div>We</div>
|
||||||
|
<div>Th</div>
|
||||||
|
<div>Fr</div>
|
||||||
|
<div>Sa</div>
|
||||||
|
<div>Su</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-7 gap-1" on:mouseleave={() => (hoveredDate = null)}>
|
||||||
|
{#each calendarDays as day}
|
||||||
|
{#if day}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
on:click={() => handleDateClick(day)}
|
||||||
|
on:mouseenter={() => (hoveredDate = day)}
|
||||||
|
class="w-9 h-9 flex items-center justify-center text-sm transition-colors duration-150 focus:outline-none"
|
||||||
|
class:text-white={isSameDay(day, startDate) || isSameDay(day, endDate)}
|
||||||
|
class:bg-blue-600={isSameDay(day, startDate) || isSameDay(day, endDate)}
|
||||||
|
class:rounded-l-full={isSameDay(day, startDate)}
|
||||||
|
class:rounded-r-full={isSameDay(day, endDate)}
|
||||||
|
class:bg-blue-100={isInRange(day) || isHoverRange(day)}
|
||||||
|
class:text-blue-700={isInRange(day)} class:font-semibold={isInRange(day)} class:hover:bg-blue-200={!isSameDay(day, startDate) && !isSameDay(day, endDate)}
|
||||||
|
class:rounded-full={!startDate && !endDate}
|
||||||
|
>
|
||||||
|
{day.getDate()}
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<div /> {/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between mt-4 pt-2 border-t">
|
||||||
|
<div class="text-sm text-gray-600">
|
||||||
|
<span class="font-semibold">{formatSimpleDate(startDate) || 'Start Date'}</span>
|
||||||
|
-
|
||||||
|
<span class="font-semibold">{formatSimpleDate(endDate) || 'End Date'}</span>
|
||||||
|
</div>
|
||||||
|
<button on:click={clearDates} class="text-sm text-blue-600 hover:text-blue-800">Clear</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
52
src/routes/test/+page.server.ts
Normal file
52
src/routes/test/+page.server.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
// src/routes/test/+page.server.ts
|
||||||
|
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
|
// 페이지가 로드될 때마다 실행됩니다.
|
||||||
|
export const load: PageServerLoad = async ({ url, fetch }) => {
|
||||||
|
// URL에서 사용자가 입력한 검색어와 모드를 가져옵니다.
|
||||||
|
const uids = url.searchParams.get('uids') || '';
|
||||||
|
const accs = url.searchParams.get('accs') || '';
|
||||||
|
const mode = url.searchParams.get('mode'); // 'basic' 또는 'search'
|
||||||
|
|
||||||
|
// 검색어가 없으면 API를 호출하지 않습니다.
|
||||||
|
if (!uids && !accs) {
|
||||||
|
return { imageData: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = new URLSearchParams();
|
||||||
|
let targetApi = '/api/dicom'; // 기본 API
|
||||||
|
|
||||||
|
// 쉼표로 구분된 입력값을 여러 개의 쿼리 파라미터로 변환합니다.
|
||||||
|
if (uids) {
|
||||||
|
uids.split(',').forEach(uid => {
|
||||||
|
if (uid.trim()) query.append('StudyInstanceUID', uid.trim());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (accs) {
|
||||||
|
accs.split(',').forEach(acc => {
|
||||||
|
if (acc.trim()) query.append('accession_number', acc.trim());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 'search' 모드일 경우 고급 검색 API를 사용합니다.
|
||||||
|
if (mode === 'search') {
|
||||||
|
targetApi = '/api/dicom/search';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// SvelteKit의 내장 fetch를 사용하여 API를 호출합니다.
|
||||||
|
const response = await fetch(`${targetApi}?${query.toString()}`);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
// 조회된 데이터를 imageData라는 이름으로 페이지에 전달합니다.
|
||||||
|
return { imageData: data };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch API:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 에러가 발생하거나 응답이 실패하면 빈 배열을 전달합니다.
|
||||||
|
return { imageData: [] };
|
||||||
|
};
|
||||||
184
src/routes/test/+page.svelte
Normal file
184
src/routes/test/+page.svelte
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
|
import { page } from '$app/stores'; // URL 파라미터를 읽기 위해 추가
|
||||||
|
|
||||||
|
// `+page.server.ts`의 load 함수가 반환한 데이터입니다.
|
||||||
|
export let data: PageData;
|
||||||
|
let uidInput = $page.url.searchParams.get('StudyInstanceUID') || '';
|
||||||
|
let accessionNumberInput = $page.url.searchParams.get('accession_number') || '';
|
||||||
|
|
||||||
|
|
||||||
|
// 사용자가 입력할 값을 바인딩할 변수들
|
||||||
|
let uidsInput = '';
|
||||||
|
let accsInput = '';
|
||||||
|
|
||||||
|
// 검색 버튼 클릭 시 실행될 함수
|
||||||
|
function handleSearch(mode: 'basic' | 'search') {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (uidsInput) params.set('uids', uidsInput);
|
||||||
|
if (accsInput) params.set('accs', accsInput);
|
||||||
|
params.set('mode', mode);
|
||||||
|
|
||||||
|
// URL을 변경하여 load 함수를 다시 실행시킵니다.
|
||||||
|
goto(`/test?${params.toString()}`, { keepFocus: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 초기화 버튼
|
||||||
|
function handleReset() {
|
||||||
|
uidsInput = '';
|
||||||
|
accsInput = '';
|
||||||
|
goto('/test');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>DICOM 조회 테스트</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<h1>DICOM 이미지 조회 테스트</h1>
|
||||||
|
|
||||||
|
<div class="p-4 bg-gray-100 border-b">
|
||||||
|
<!--
|
||||||
|
<form action="/viewer" method="GET" class="flex items-center gap-4">
|
||||||
|
<label for="uid-input">StudyInstanceUID:</label>
|
||||||
|
<input
|
||||||
|
id="uid-input"
|
||||||
|
name="StudyInstanceUID"
|
||||||
|
type="text"
|
||||||
|
class="border p-1 rounded"
|
||||||
|
bind:value={uidInput}
|
||||||
|
on:keydown={(event) => event.stopPropagation()} >
|
||||||
|
<button type="submit" class="bg-blue-500 text-white px-4 py-1 rounded">스터디 불러오기</button>
|
||||||
|
</form>
|
||||||
|
-->
|
||||||
|
<form action="/viewer" method="GET" class="flex items-center gap-2">
|
||||||
|
<label for="accession-input" class="text-sm font-medium text-gray-700">Accession Number:</label>
|
||||||
|
<input
|
||||||
|
id="accession-input"
|
||||||
|
name="accession_number"
|
||||||
|
type="text"
|
||||||
|
class="border p-1 rounded-md text-sm"
|
||||||
|
bind:value={accessionNumberInput}
|
||||||
|
on:keydown={(event) => event.stopPropagation()} >
|
||||||
|
<button type="submit" class="bg-green-500 text-white px-3 py-1 text-sm rounded-md hover:bg-green-600">스터디 불러오기</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="uids">StudyInstanceUIDs (쉼표로 구분)</label>
|
||||||
|
<input id="uids" type="text" bind:value={uidsInput} placeholder="1.2.3..., 4.5.6..." />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="accs">Accession Numbers (쉼표로 구분)</label>
|
||||||
|
<input id="accs" type="text" bind:value={accsInput} placeholder="ACC1, ACC2, ..." />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="buttons">
|
||||||
|
<button on:click={() => handleSearch('basic')}>기본 조회 (/api/dicom)</button>
|
||||||
|
<button on:click={() => handleSearch('search')}>AND 복합 조회 (/api/dicom/search)</button>
|
||||||
|
<button class="secondary" on:click={handleReset}>초기화</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<h2>조회 결과 ({data.imageData?.length || 0} 개)</h2>
|
||||||
|
|
||||||
|
{#if data.imageData && data.imageData.length > 0}
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Accession Number</th>
|
||||||
|
<th>Image Number</th>
|
||||||
|
<th>Study UID</th>
|
||||||
|
<th>File Location (클릭하여 다운로드)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each data.imageData as image (image.file_location)}
|
||||||
|
<tr>
|
||||||
|
<td>{image.accession_number}</td>
|
||||||
|
<td>{image.image_number}</td>
|
||||||
|
<td class="uid">{image.study_instance_uid}</td>
|
||||||
|
<td>
|
||||||
|
<a
|
||||||
|
href={`/api/download?file=${encodeURIComponent(image.file_location)}`}
|
||||||
|
download
|
||||||
|
>
|
||||||
|
{image.file_location}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{:else}
|
||||||
|
<p>조회된 데이터가 없습니다. 검색 조건을 입력 후 조회 버튼을 눌러주세요.</p>
|
||||||
|
{/if}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
main {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 2rem auto;
|
||||||
|
padding: 1rem;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.buttons {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border: none;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
button.secondary {
|
||||||
|
background-color: #6c757d;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 8px;
|
||||||
|
text-align: left;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
}
|
||||||
|
.uid {
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
77
src/routes/viewer/+page.server.ts
Normal file
77
src/routes/viewer/+page.server.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
// src/routes/viewer/+page.server.ts
|
||||||
|
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
import db from '$lib/server/db'; // DB 사용을 위해 import 추가
|
||||||
|
|
||||||
|
// 페이지가 로드될 때 실행됩니다.
|
||||||
|
export const load: PageServerLoad = async ({ url, fetch }) => {
|
||||||
|
const uids = url.searchParams.getAll('StudyInstanceUID');
|
||||||
|
const accs = url.searchParams.getAll('accession_number');
|
||||||
|
|
||||||
|
// URL에 검색 파라미터가 없으면 빈 데이터를 반환합니다.
|
||||||
|
if (uids.length === 0 && accs.length === 0) {
|
||||||
|
return {
|
||||||
|
fileList: [],
|
||||||
|
patientId: null,
|
||||||
|
patientName: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 반환할 변수들을 미리 선언합니다.
|
||||||
|
let fileList: string[] = [];
|
||||||
|
let patientId: string | null = null;
|
||||||
|
let patientName: string | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// --- 1. 환자 정보 조회 (DB 직접 쿼리) ---
|
||||||
|
// 여러 accession number가 올 수 있으므로, 첫 번째 것을 기준으로 환자 정보를 조회합니다.
|
||||||
|
const targetAcc = accs[0];
|
||||||
|
|
||||||
|
if (targetAcc) {
|
||||||
|
const patientQuery = `
|
||||||
|
SELECT patient_id, patient_name
|
||||||
|
FROM public.spacs_study
|
||||||
|
WHERE accession_number = $1
|
||||||
|
LIMIT 1;
|
||||||
|
`;
|
||||||
|
const patientResult = await db.query(patientQuery, [targetAcc]);
|
||||||
|
|
||||||
|
if (patientResult.rows.length > 0) {
|
||||||
|
patientId = patientResult.rows[0].patient_id;
|
||||||
|
patientName = patientResult.rows[0].patient_name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 2. 파일 목록 조회 (기존 API 방식 유지) ---
|
||||||
|
// API 호출을 위한 쿼리 스트링을 만듭니다.
|
||||||
|
const query = new URLSearchParams();
|
||||||
|
uids.forEach((uid) => query.append('StudyInstanceUID', uid));
|
||||||
|
accs.forEach((acc) => query.append('accession_number', acc));
|
||||||
|
|
||||||
|
// 기본 조회 API(/api/dicom)를 호출합니다.
|
||||||
|
const targetApi = `/api/dicom?${query.toString()}`;
|
||||||
|
const response = await fetch(targetApi);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const images: { file_location: string }[] = await response.json();
|
||||||
|
|
||||||
|
// ❗ 중요: 파일 시스템 경로가 아닌, 다운로드 API를 가리키는 URL 목록으로 변환합니다.
|
||||||
|
// 이 부분이 핵심적인 백그라운드 전송 기능을 담당합니다.
|
||||||
|
fileList = images.map(
|
||||||
|
(img) => `/api/download?file=${encodeURIComponent(img.file_location)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch data for viewer:', error);
|
||||||
|
// 에러 발생 시 모든 데이터를 빈 값으로 반환하여 페이지 오류를 방지합니다.
|
||||||
|
return {
|
||||||
|
fileList: [],
|
||||||
|
patientId: null,
|
||||||
|
patientName: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 3. 조회된 모든 데이터를 페이지에 전달 ---
|
||||||
|
return { fileList, patientId, patientName };
|
||||||
|
};
|
||||||
1100
src/routes/viewer/+page.svelte
Normal file
1100
src/routes/viewer/+page.svelte
Normal file
File diff suppressed because it is too large
Load Diff
55
src/routes/viewer/[accession_number]/+page.server.ts
Normal file
55
src/routes/viewer/[accession_number]/+page.server.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { error } from '@sveltejs/kit';
|
||||||
|
import db from '$lib/server/db';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ params }) => {
|
||||||
|
const accessionNumber = params.accession_number;
|
||||||
|
|
||||||
|
if (!accessionNumber) {
|
||||||
|
throw error(400, 'Accession Number가 필요합니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. accession_number로 Study 정보 조회 (환자 정보 포함)
|
||||||
|
const studyQuery = `
|
||||||
|
SELECT
|
||||||
|
patient_id,
|
||||||
|
patient_name
|
||||||
|
FROM public.spacs_study
|
||||||
|
WHERE accession_number = $1
|
||||||
|
LIMIT 1;
|
||||||
|
`;
|
||||||
|
const studyResult = await db.query(studyQuery, [accessionNumber]);
|
||||||
|
|
||||||
|
if (studyResult.rows.length === 0) {
|
||||||
|
throw error(404, '해당하는 검사를 찾을 수 없습니다.');
|
||||||
|
}
|
||||||
|
const patientInfo = studyResult.rows[0];
|
||||||
|
|
||||||
|
// 2. accession_number로 해당 Study의 모든 이미지 파일 URL 목록 조회
|
||||||
|
// (기존 viewer/+page.server.ts의 파일 목록 조회 로직을 여기에 통합)
|
||||||
|
const fileListQuery = `
|
||||||
|
SELECT i.file_location FROM public.spacs_image i
|
||||||
|
JOIN public.spacs_series ss ON i.series_index = ss.index
|
||||||
|
JOIN public.spacs_study s ON ss.study_index = s.index
|
||||||
|
WHERE s.accession_number = $1
|
||||||
|
ORDER BY i.index asc;
|
||||||
|
`;
|
||||||
|
const fileListResult = await db.query(fileListQuery, [accessionNumber]);
|
||||||
|
const fileList = fileListResult.rows.map((row) => `/images/${row.file_path}`); // 실제 경로에 맞게 수정
|
||||||
|
|
||||||
|
// 3. 조회된 데이터를 프론트엔드로 전달
|
||||||
|
return {
|
||||||
|
accessionNumber: accessionNumber,
|
||||||
|
patientId: patientInfo.patient_id,
|
||||||
|
patientName: patientInfo.patient_name,
|
||||||
|
fileList: fileList
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Viewer data loading failed:', err);
|
||||||
|
// err가 SvelteKit의 error 객체일 수 있으므로 그대로 throw
|
||||||
|
if (err.status) throw err;
|
||||||
|
// 그 외의 경우 500 에러 발생
|
||||||
|
throw error(500, '뷰어 데이터를 불러오는 데 실패했습니다.');
|
||||||
|
}
|
||||||
|
};
|
||||||
1063
src/routes/viewer/[accession_number]/+page.svelte
Normal file
1063
src/routes/viewer/[accession_number]/+page.svelte
Normal file
File diff suppressed because it is too large
Load Diff
110
src/routes/worklist/+layout.svelte
Normal file
110
src/routes/worklist/+layout.svelte
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
// --- 수정된 부분 ---
|
||||||
|
// Flowbite 관련 import 구문 모두 삭제
|
||||||
|
import { theme, type Theme } from '$lib/store/theme';
|
||||||
|
|
||||||
|
function handleThemeChange(newTheme: Theme): void {
|
||||||
|
$theme = newTheme;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col h-screen">
|
||||||
|
<header
|
||||||
|
class="flex justify-between items-center px-4 py-2 border-b dark:border-gray-700 bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm shadow-sm sticky top-0 z-10"
|
||||||
|
>
|
||||||
|
<a href="/worklist" class="text-xl font-bold text-blue-600 dark:text-blue-400">
|
||||||
|
🏥 DICOM Worklist
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-1 p-1 bg-gray-200 dark:bg-gray-700 rounded-lg">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
on:click={() => handleThemeChange('light')}
|
||||||
|
class="p-2 rounded-lg transition-colors"
|
||||||
|
class:bg-blue-600={$theme === 'light'}
|
||||||
|
class:text-white={$theme === 'light'}
|
||||||
|
class:text-gray-600={$theme !== 'light'}
|
||||||
|
class:dark:text-gray-300={$theme !== 'light'}
|
||||||
|
class:hover:bg-gray-300={$theme !== 'light'}
|
||||||
|
class:dark:hover:bg-gray-600={$theme !== 'light'}
|
||||||
|
aria-label="Set light theme"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
on:click={() => handleThemeChange('dark')}
|
||||||
|
class="p-2 rounded-lg transition-colors"
|
||||||
|
class:bg-blue-600={$theme === 'dark'}
|
||||||
|
class:text-white={$theme === 'dark'}
|
||||||
|
class:text-gray-600={$theme !== 'dark'}
|
||||||
|
class:dark:text-gray-300={$theme !== 'dark'}
|
||||||
|
class:hover:bg-gray-300={$theme !== 'dark'}
|
||||||
|
class:dark:hover:bg-gray-600={$theme !== 'dark'}
|
||||||
|
aria-label="Set dark theme"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
on:click={() => handleThemeChange('system')}
|
||||||
|
class="p-2 rounded-lg transition-colors"
|
||||||
|
class:bg-blue-600={$theme === 'system'}
|
||||||
|
class:text-white={$theme === 'system'}
|
||||||
|
class:text-gray-600={$theme !== 'system'}
|
||||||
|
class:dark:text-gray-300={$theme !== 'system'}
|
||||||
|
class:hover:bg-gray-300={$theme !== 'system'}
|
||||||
|
class:dark:hover:bg-gray-600={$theme !== 'system'}
|
||||||
|
aria-label="Set system theme"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
||||||
|
/>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="flex-1 overflow-y-auto">
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
131
src/routes/worklist/+page.server.ts
Normal file
131
src/routes/worklist/+page.server.ts
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import { error } from '@sveltejs/kit';
|
||||||
|
import db from '$lib/server/db';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
|
// WorklistItem 타입을 새로운 쿼리 결과에 맞게 재정의
|
||||||
|
export interface WorklistItem {
|
||||||
|
id: number;
|
||||||
|
study_date: string;
|
||||||
|
study_time: string;
|
||||||
|
accession_number: string;
|
||||||
|
study_instance_uid: string;
|
||||||
|
study_description: string;
|
||||||
|
patient_id: string;
|
||||||
|
patient_name: string;
|
||||||
|
patient_sex: string;
|
||||||
|
modalities: string; // 여러 Modality를 쉼표로 연결한 문자열
|
||||||
|
series_number: number; // Series 개수
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatYYYYMMDD(date: Date): string {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
// getMonth()는 0부터 시작하므로 1을 더해줘야 합니다. (0 = 1월, 11 = 12월)
|
||||||
|
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||||
|
const day = date.getDate().toString().padStart(2, '0');
|
||||||
|
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ url }) => {
|
||||||
|
console.log('Fetching worklist with new query and search params...');
|
||||||
|
|
||||||
|
// --- 검색 및 정렬 파라미터 ---
|
||||||
|
const todayStr = formatYYYYMMDD(new Date());
|
||||||
|
const startDate = url.searchParams.get('startDate') || todayStr;
|
||||||
|
const endDate = url.searchParams.get('endDate') || todayStr;
|
||||||
|
const patientId = url.searchParams.get('patientId') || '';
|
||||||
|
const patientName = url.searchParams.get('patientName') || '';
|
||||||
|
const modality = url.searchParams.get('modality') || '';
|
||||||
|
const sortBy = url.searchParams.get('sort_by') || 'study_date';
|
||||||
|
const sortOrder = url.searchParams.get('sort_order') || 'desc';
|
||||||
|
|
||||||
|
// 🚨 보안: 정렬을 위한 컬럼 Whitelist (새로운 쿼리 기준)
|
||||||
|
const allowedSortColumns: { [key: string]: string } = {
|
||||||
|
study_date: 's.study_date',
|
||||||
|
patient_name: 's.patient_name',
|
||||||
|
patient_id: 's.patient_id',
|
||||||
|
accession_number: 's.accession_number',
|
||||||
|
modalities: 'modalities' // 별칭으로 정렬
|
||||||
|
};
|
||||||
|
const sortColumn = allowedSortColumns[sortBy] || 's.study_date';
|
||||||
|
const sortDirection = sortOrder.toLowerCase() === 'asc' ? 'ASC' : 'DESC';
|
||||||
|
|
||||||
|
// --- WHERE 절 구성 (새로운 쿼리 기준) ---
|
||||||
|
const whereClauses: string[] = [];
|
||||||
|
const queryParams: any[] = [];
|
||||||
|
let paramIndex = 1;
|
||||||
|
|
||||||
|
// s 테이블의 컬럼을 기준으로 필터링하도록 수정
|
||||||
|
if (patientId) {
|
||||||
|
whereClauses.push(`s.patient_id ILIKE $${paramIndex++}`);
|
||||||
|
queryParams.push(`%${patientId}%`);
|
||||||
|
}
|
||||||
|
if (patientName) {
|
||||||
|
whereClauses.push(`s.patient_name ILIKE $${paramIndex++}`);
|
||||||
|
queryParams.push(`%${patientName}%`);
|
||||||
|
}
|
||||||
|
if (startDate) {
|
||||||
|
// study_date 컬럼 형식이 'YYYY-MM-DD'라고 가정. 다르다면 수정 필요.
|
||||||
|
whereClauses.push(`s.study_date >= $${paramIndex++}`);
|
||||||
|
const searchStartDate = startDate.replaceAll('-', '');
|
||||||
|
queryParams.push(searchStartDate);
|
||||||
|
}
|
||||||
|
if (endDate) {
|
||||||
|
whereClauses.push(`s.study_date <= $${paramIndex++}`);
|
||||||
|
const searchEndDate = endDate.replaceAll('-', '');
|
||||||
|
queryParams.push(searchEndDate);
|
||||||
|
}
|
||||||
|
// Modality 필터링은 서브쿼리나 HAVING을 사용해야 하므로, 우선 가장 간단한 형태로 구현
|
||||||
|
// 특정 모달리티를 포함하는 Study를 검색
|
||||||
|
if (modality) {
|
||||||
|
whereClauses.push(`s.index IN (SELECT study_index FROM public.spacs_series WHERE modality = $${paramIndex++})`);
|
||||||
|
queryParams.push(modality);
|
||||||
|
}
|
||||||
|
|
||||||
|
const whereStatement = whereClauses.length > 0 ? `WHERE ${whereClauses.join(' AND ')}` : '';
|
||||||
|
|
||||||
|
const sqlQuery = `
|
||||||
|
SELECT
|
||||||
|
s.index as id,
|
||||||
|
TO_CHAR(TO_DATE(s.study_date, 'YYYYMMDD'), 'YYYY/MM/DD') AS study_date,
|
||||||
|
TO_CHAR(TO_TIMESTAMP(s.study_time, 'HH24MISS'), 'HH24:MI:SS') AS study_time,
|
||||||
|
s.accession_number,
|
||||||
|
s.study_instance_uid,
|
||||||
|
s.study_description,
|
||||||
|
s.patient_id,
|
||||||
|
s.patient_name,
|
||||||
|
s.patient_sex,
|
||||||
|
STRING_AGG(DISTINCT ss.modality, ', ') AS modalities,
|
||||||
|
COUNT(ss.index) AS series_number
|
||||||
|
FROM
|
||||||
|
public.spacs_study AS s
|
||||||
|
LEFT JOIN
|
||||||
|
public.spacs_series AS ss ON s.index = ss.study_index
|
||||||
|
${whereStatement}
|
||||||
|
GROUP BY
|
||||||
|
s.index
|
||||||
|
ORDER BY
|
||||||
|
${sortColumn} ${sortDirection}
|
||||||
|
LIMIT 100;
|
||||||
|
`;
|
||||||
|
|
||||||
|
console.log('Executing SQL query:', sqlQuery);
|
||||||
|
console.log('With parameters:', queryParams);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await db.query(sqlQuery, queryParams);
|
||||||
|
// COUNT 결과는 문자열일 수 있으므로 숫자로 변환
|
||||||
|
const worklist: WorklistItem[] = result.rows.map((row) => ({
|
||||||
|
...row,
|
||||||
|
series_number: parseInt(row.series_number, 10)
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
worklist,
|
||||||
|
searchParams: { patientId, patientName, modality, startDate, endDate, sortBy, sortOrder }
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Database query failed:', err);
|
||||||
|
throw error(500, '서버에서 워크리스트를 가져오는 데 실패했습니다.');
|
||||||
|
}
|
||||||
|
};
|
||||||
284
src/routes/worklist/+page.svelte
Normal file
284
src/routes/worklist/+page.svelte
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goto, afterNavigate } from '$app/navigation';
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
import DatePicker from '$lib/components/DatePicker.svelte';
|
||||||
|
import type { SeriesItem } from '../api/series/[studyId]/+server';
|
||||||
|
|
||||||
|
export let data: PageData;
|
||||||
|
|
||||||
|
// --- 헬퍼 함수 ---
|
||||||
|
function formatYYYYMMDD(date: Date): string {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
// getMonth()는 0부터 시작하므로 1을 더해줘야 합니다. (0 = 1월, 11 = 12월)
|
||||||
|
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||||
|
const day = date.getDate().toString().padStart(2, '0');
|
||||||
|
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
const todayStr = formatYYYYMMDD(new Date());
|
||||||
|
|
||||||
|
// --- 상태 변수 ---
|
||||||
|
let patientId: string, patientName: string, selectedModality: string, startDate: string, endDate: string;
|
||||||
|
let sortBy: string, sortOrder: string;
|
||||||
|
let selectedStudyId: number | null = null;
|
||||||
|
let seriesList: SeriesItem[] = [];
|
||||||
|
let isLoadingSeries = false;
|
||||||
|
|
||||||
|
|
||||||
|
// ✨ [변경 1] data.url을 기반으로 반응적으로 displayWorklist를 계산합니다.
|
||||||
|
// 이렇게 하면 data가 변경될 때마다 Svelte가 자동으로 화면을 갱신합니다.
|
||||||
|
$: urlSearchParams = new URLSearchParams(data.searchParams);
|
||||||
|
$: displayWorklist = urlSearchParams.size === 0 ? [] : data.worklist;
|
||||||
|
|
||||||
|
// ✨ [변경 2] URL 파라미터가 없을 때(초기 로드) seriesList도 비웁니다.
|
||||||
|
// selectedStudyId는 afterNavigate에서 이미 처리되고 있습니다.
|
||||||
|
$: if (urlSearchParams.size === 0) {
|
||||||
|
seriesList = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// afterNavigate 훅에서 모든 파라미터 상태를 URL과 동기화
|
||||||
|
afterNavigate(() => {
|
||||||
|
const params = data.searchParams;
|
||||||
|
patientId = params.patientId || '';
|
||||||
|
patientName = params.patientName || '';
|
||||||
|
selectedModality = params.modality || '';
|
||||||
|
startDate = params.startDate || todayStr;
|
||||||
|
endDate = params.endDate || todayStr;
|
||||||
|
sortBy = params.sortBy || 'study_date';
|
||||||
|
sortOrder = params.sortOrder || 'desc';
|
||||||
|
if (selectedStudyId !== null) {
|
||||||
|
selectedStudyId = null;
|
||||||
|
seriesList = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const modalities = [
|
||||||
|
{ value: '', name: 'Modality' },
|
||||||
|
{ value: 'CT', name: 'CT' },
|
||||||
|
{ value: 'MR', name: 'MR' },
|
||||||
|
{ value: 'ES', name: 'ES' },
|
||||||
|
{ value: 'DX', name: 'DX' },
|
||||||
|
{ value: 'SC', name: 'SC' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// --- 이벤트 핸들러 ---
|
||||||
|
function applyFiltersAndSort() {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (patientId) params.set('patientId', patientId);
|
||||||
|
if (patientName) params.set('patientName', patientName);
|
||||||
|
if (selectedModality) params.set('modality', selectedModality);
|
||||||
|
if (startDate) params.set('startDate', startDate);
|
||||||
|
if (endDate) params.set('endDate', endDate);
|
||||||
|
if (sortBy && sortOrder) {
|
||||||
|
params.set('sort_by', sortBy);
|
||||||
|
params.set('sort_order', sortOrder);
|
||||||
|
}
|
||||||
|
goto(`?${params.toString()}`, { keepFocus: true, noScroll: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSort(newSortBy: string) {
|
||||||
|
if (sortBy === newSortBy) {
|
||||||
|
sortOrder = sortOrder === 'desc' ? 'asc' : 'desc';
|
||||||
|
} else {
|
||||||
|
sortBy = newSortBy;
|
||||||
|
sortOrder = 'desc';
|
||||||
|
}
|
||||||
|
applyFiltersAndSort();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleStudySelect(study: (typeof data.worklist)[0]) {
|
||||||
|
if (selectedStudyId === study.id) return;
|
||||||
|
selectedStudyId = study.id;
|
||||||
|
isLoadingSeries = true;
|
||||||
|
//seriesList = [];
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/series/${study.id}`);
|
||||||
|
if (response.ok) {
|
||||||
|
seriesList = await response.json();
|
||||||
|
} else {
|
||||||
|
console.error('Failed to fetch series list');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching series list:', error);
|
||||||
|
} finally {
|
||||||
|
isLoadingSeries = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleStudyDoubleClick(study: (typeof data.worklist)[0]) {
|
||||||
|
if (study.accession_number) {
|
||||||
|
// 새 탭에서 뷰어 열기
|
||||||
|
//window.open(`/viewer?accession_number=${study.accession_number}`, '_blank');
|
||||||
|
window.open(`/viewer?accession_number=${study.accession_number}`, 'viewerTab');
|
||||||
|
//goto(`/viewer/${study.accession_number}`);
|
||||||
|
} else {
|
||||||
|
alert('Accession Number가 없어 뷰어를 열 수 없습니다.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="p-4 sm:p-6 bg-gray-100 dark:bg-gray-900 flex flex-col h-full">
|
||||||
|
<form on:submit|preventDefault={applyFiltersAndSort} class="p-4 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 shadow-sm rounded-lg mb-4 flex-shrink-0">
|
||||||
|
<div class="flex flex-row flex-nowrap items-center w-full gap-4">
|
||||||
|
<div class="w-48">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="환자 ID..."
|
||||||
|
bind:value={patientId}
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="w-48">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="환자 이름..."
|
||||||
|
bind:value={patientName}
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="w-36">
|
||||||
|
<select
|
||||||
|
bind:value={selectedModality}
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
|
||||||
|
>
|
||||||
|
{#each modalities as modality}
|
||||||
|
<option value={modality.value}>{modality.name}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="w-72">
|
||||||
|
<DatePicker bind:startDate bind:endDate />
|
||||||
|
</div>
|
||||||
|
<div class="ml-auto">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="text-white bg-purple-700 hover:bg-purple-800 focus:ring-4 focus:ring-purple-300 font-medium rounded-lg text-sm px-5 py-2.5 flex items-center dark:bg-purple-600 dark:hover:bg-purple-700 dark:focus:ring-purple-800"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-4 h-4 mr-2"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
><path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||||
|
/></svg
|
||||||
|
>
|
||||||
|
검색
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="flex flex-col flex-grow min-h-0 gap-4">
|
||||||
|
<div class="flex flex-col h-2/3 min-h-0">
|
||||||
|
<h2 class="text-lg font-semibold mb-2 text-gray-700 dark:text-gray-200 flex-shrink-0">STUDIES</h2>
|
||||||
|
<div class="relative overflow-auto shadow-md sm:rounded-lg flex-grow">
|
||||||
|
<!-- ✨ [수정] table-fixed 클래스 추가 -->
|
||||||
|
<table class="w-full table-fixed text-sm text-left text-gray-500 dark:text-gray-400">
|
||||||
|
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400 sticky top-0">
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="px-6 py-3 w-40">
|
||||||
|
<button on:click={() => handleSort('patient_id')} class="flex items-center gap-1">
|
||||||
|
Patient ID
|
||||||
|
{#if sortBy === 'patient_id'}<span>{sortOrder === 'desc' ? '↓' : '↑'}</span>{/if}
|
||||||
|
</button>
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="px-6 py-3 w-40">
|
||||||
|
<button on:click={() => handleSort('patient_name')} class="flex items-center gap-1">
|
||||||
|
Name
|
||||||
|
{#if sortBy === 'patient_name'}<span>{sortOrder === 'desc' ? '↓' : '↑'}</span>{/if}
|
||||||
|
</button>
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="px-6 py-3 w-48">
|
||||||
|
<button on:click={() => handleSort('accession_number')} class="flex items-center gap-1">
|
||||||
|
Accession #
|
||||||
|
{#if sortBy === 'accession_number'}<span>{sortOrder === 'desc' ? '↓' : '↑'}</span>{/if}
|
||||||
|
</button>
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="px-6 py-3 w-52">
|
||||||
|
<button on:click={() => handleSort('study_date')} class="flex items-center gap-1">
|
||||||
|
Study Date
|
||||||
|
{#if sortBy === 'study_date'}<span>{sortOrder === 'desc' ? '↓' : '↑'}</span>{/if}
|
||||||
|
</button>
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="px-6 py-3">Description</th>
|
||||||
|
<th scope="col" class="px-6 py-3 w-36">
|
||||||
|
<button on:click={() => handleSort('modalities')} class="flex items-center gap-1">
|
||||||
|
Modalities
|
||||||
|
{#if sortBy === 'modalities'}<span>{sortOrder === 'desc' ? '↓' : '↑'}</span>{/if}
|
||||||
|
</button>
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="px-6 py-3 w-24">Series</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
{#if displayWorklist && displayWorklist.length > 0}
|
||||||
|
{#each displayWorklist as item (item.id)}
|
||||||
|
<!-- ✨ [수정] /60 문제를 해결하기 위해 클래스 문법 수정 -->
|
||||||
|
<tr
|
||||||
|
class="select-none border-b dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600/50 cursor-pointer {selectedStudyId === item.id ? 'dark:!bg-indigo-900/60' : ''}"
|
||||||
|
class:!bg-indigo-200={selectedStudyId === item.id}
|
||||||
|
on:click={() => handleStudySelect(item)}
|
||||||
|
on:dblclick={() => handleStudyDoubleClick(item)}
|
||||||
|
>
|
||||||
|
<td class="px-6 py-4 font-medium" class:!text-white={selectedStudyId === item.id}
|
||||||
|
>{item.patient_id}</td
|
||||||
|
>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">{item.patient_name}</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">{item.accession_number}</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">{item.study_date} {item.study_time}</td>
|
||||||
|
<td class="px-6 py-4 truncate max-w-xs">{item.study_description}</td>
|
||||||
|
<td class="px-6 py-4">{item.modalities}</td>
|
||||||
|
<td class="px-6 py-4 text-center">{item.series_number}</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
{:else}
|
||||||
|
<tr><td colspan="7" class="text-center py-10">검색 버튼을 눌러 환자 목록을 조회하세요.</td></tr>
|
||||||
|
{/if}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col h-1/3 min-h-0">
|
||||||
|
<h2 class="text-lg font-semibold mb-2 text-gray-700 dark:text-gray-200 flex-shrink-0">SERIES</h2>
|
||||||
|
<div class="relative overflow-auto shadow-md sm:rounded-lg flex-grow">
|
||||||
|
<table class="w-full table-fixed text-sm text-left text-gray-500 dark:text-gray-400">
|
||||||
|
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400 sticky top-0">
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="w-24 px-6 py-3">Number</th>
|
||||||
|
<th scope="col" class="w-48 px-6 py-3">Series Date</th>
|
||||||
|
<th scope="col" class="px-auto px-6 py-3">Description</th>
|
||||||
|
<th scope="col" class="w-32 px-6 py-3">Modality</th>
|
||||||
|
<th scope="col" class="w-32 px-6 py-3">Instances</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
{#if isLoadingSeries}
|
||||||
|
<!-- <tr><td colspan="5" class="text-center py-10">Series 정보를 불러오는 중...</td></tr> -->
|
||||||
|
{:else if selectedStudyId && seriesList.length > 0}
|
||||||
|
{#each seriesList as series (series.id)}
|
||||||
|
<tr class="dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600/50">
|
||||||
|
<td class="px-6 py-4">{series.series_number}</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">{series.series_date} {series.series_time}</td>
|
||||||
|
<td class="px-6 py-4 truncate">{series.series_description}</td>
|
||||||
|
<td class="px-6 py-4">{series.modality}</td>
|
||||||
|
<td class="px-6 py-4 text-center">{series.image_count}</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
{:else if selectedStudyId}
|
||||||
|
<tr><td colspan="5" class="text-center py-10">선택된 Study에 Series가 없습니다.</td></tr>
|
||||||
|
{:else}
|
||||||
|
<tr><td colspan="5" class="text-center py-10">Study를 선택하여 Series를 확인하세요.</td></tr>
|
||||||
|
{/if}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
134
src/routes/worklist/+page.svelte.old
Normal file
134
src/routes/worklist/+page.svelte.old
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
|
export let data: PageData;
|
||||||
|
|
||||||
|
// 각 검색 필드에 대한 상태 변수 선언
|
||||||
|
let patientId: string = data.searchParams.patientId || '';
|
||||||
|
let patientName: string = data.searchParams.patientName || '';
|
||||||
|
let selectedModality: string = data.searchParams.modality || '';
|
||||||
|
let startDate: string = data.searchParams.startDate || '';
|
||||||
|
let endDate: string = data.searchParams.endDate || '';
|
||||||
|
|
||||||
|
const modalities = [
|
||||||
|
{ value: '', name: 'Modality' },
|
||||||
|
{ value: 'CT', name: 'CT' },
|
||||||
|
{ value: 'MR', name: 'MR' },
|
||||||
|
{ value: 'ES', name: 'ES' },
|
||||||
|
{ value: 'DX', name: 'DX' },
|
||||||
|
{ value: 'SC', name: 'SC' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// 검색 기능
|
||||||
|
function handleSearch() {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (patientId) params.set('patientId', patientId);
|
||||||
|
if (patientName) params.set('patientName', patientName);
|
||||||
|
if (selectedModality) params.set('modality', selectedModality);
|
||||||
|
if (startDate) params.set('startDate', startDate);
|
||||||
|
if (endDate) params.set('endDate', endDate);
|
||||||
|
goto(`?${params.toString()}`, { keepFocus: true, noScroll: true });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="p-4 sm:p-6 bg-white dark:bg-gray-900 min-h-screen">
|
||||||
|
<form
|
||||||
|
on:submit|preventDefault={handleSearch}
|
||||||
|
class="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg"
|
||||||
|
>
|
||||||
|
<div class="flex flex-row flex-nowrap items-center w-full gap-4">
|
||||||
|
<div class="w-48">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="환자 ID..."
|
||||||
|
bind:value={patientId}
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-48">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="환자 이름..."
|
||||||
|
bind:value={patientName}
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-36">
|
||||||
|
<select
|
||||||
|
bind:value={selectedModality}
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
|
||||||
|
>
|
||||||
|
{#each modalities as modality}
|
||||||
|
<option value={modality.value}>{modality.name}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-auto items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
placeholder="Select date start"
|
||||||
|
bind:value={startDate}
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
|
||||||
|
/>
|
||||||
|
<span class="text-gray-500 dark:text-gray-400">to</span>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
placeholder="Select date end"
|
||||||
|
bind:value={endDate}
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ml-auto">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="text-white bg-purple-700 hover:bg-purple-800 focus:ring-4 focus:ring-purple-300 font-medium rounded-lg text-sm px-5 py-2.5 flex items-center dark:bg-purple-600 dark:hover:bg-purple-700 dark:focus:ring-purple-800"
|
||||||
|
>
|
||||||
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
|
||||||
|
검색
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="relative overflow-x-auto shadow-md sm:rounded-lg mt-4">
|
||||||
|
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
|
||||||
|
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="px-6 py-3">Study Date</th>
|
||||||
|
<th scope="col" class="px-6 py-3">Name</th>
|
||||||
|
<th scope="col" class="px-6 py-3">Sex</th>
|
||||||
|
<th scope="col" class="px-6 py-3">Age</th>
|
||||||
|
<th scope="col" class="px-6 py-3">Study Description</th>
|
||||||
|
<th scope="col" class="px-6 py-3">Modality</th>
|
||||||
|
<th scope="col" class="px-6 py-3">Images</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#if data.worklist && data.worklist.length > 0}
|
||||||
|
{#each data.worklist as item (item.id)}
|
||||||
|
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 cursor-pointer">
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">{item.study_date} {item.study_time}</td>
|
||||||
|
<td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">{item.name}</td>
|
||||||
|
<td class="px-6 py-4">{item.sex}</td>
|
||||||
|
<td class="px-6 py-4">{item.age}</td>
|
||||||
|
<td class="px-6 py-4">{item.study_description}</td>
|
||||||
|
<td class="px-6 py-4">{item.modality}</td>
|
||||||
|
<td class="px-6 py-4 text-center">{item.image_count}</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
{:else}
|
||||||
|
<tr>
|
||||||
|
<td colspan="7" class="text-center py-10 text-gray-500 dark:text-gray-400">
|
||||||
|
검색 조건에 맞는 데이터가 없습니다.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/if}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
24
src/styles/app.css
Normal file
24
src/styles/app.css
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
@layer base {
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
src: local('Roboto Regular'), local('Roboto-Regular'), local('Roboto'), local('roboto'),
|
||||||
|
url('/fonts/Roboto/Roboto-Regular.ttf');
|
||||||
|
font-weight: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
src: local('Roboto Medium'), local('Roboto-Medium'), local('roboto medium'), local('roboto-medium'),
|
||||||
|
url('/fonts/Roboto/Roboto-Medium.ttf');
|
||||||
|
font-weight: medium;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
src: local('Roboto Bold'), local('Roboto-Bold'), local('roboto bold'), local('roboto-bold'),
|
||||||
|
url('/fonts/Roboto/Roboto-Bold.ttf');
|
||||||
|
font-weight: bold;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
BIN
static/0001.dcm
Normal file
BIN
static/0001.dcm
Normal file
Binary file not shown.
BIN
static/0002.dcm
Normal file
BIN
static/0002.dcm
Normal file
Binary file not shown.
BIN
static/0010.dcm
Normal file
BIN
static/0010.dcm
Normal file
Binary file not shown.
BIN
static/0015.dcm
Normal file
BIN
static/0015.dcm
Normal file
Binary file not shown.
BIN
static/0020.dcm
Normal file
BIN
static/0020.dcm
Normal file
Binary file not shown.
BIN
static/favicon.png
Normal file
BIN
static/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
BIN
static/fonts/NotoSans/NotoSans-Black.ttf
Normal file
BIN
static/fonts/NotoSans/NotoSans-Black.ttf
Normal file
Binary file not shown.
BIN
static/fonts/NotoSans/NotoSans-BlackItalic.ttf
Normal file
BIN
static/fonts/NotoSans/NotoSans-BlackItalic.ttf
Normal file
Binary file not shown.
BIN
static/fonts/NotoSans/NotoSans-Bold.ttf
Normal file
BIN
static/fonts/NotoSans/NotoSans-Bold.ttf
Normal file
Binary file not shown.
BIN
static/fonts/NotoSans/NotoSans-BoldItalic.ttf
Normal file
BIN
static/fonts/NotoSans/NotoSans-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
static/fonts/NotoSans/NotoSans-ExtraBold.ttf
Normal file
BIN
static/fonts/NotoSans/NotoSans-ExtraBold.ttf
Normal file
Binary file not shown.
BIN
static/fonts/NotoSans/NotoSans-ExtraBoldItalic.ttf
Normal file
BIN
static/fonts/NotoSans/NotoSans-ExtraBoldItalic.ttf
Normal file
Binary file not shown.
BIN
static/fonts/NotoSans/NotoSans-ExtraLight.ttf
Normal file
BIN
static/fonts/NotoSans/NotoSans-ExtraLight.ttf
Normal file
Binary file not shown.
BIN
static/fonts/NotoSans/NotoSans-ExtraLightItalic.ttf
Normal file
BIN
static/fonts/NotoSans/NotoSans-ExtraLightItalic.ttf
Normal file
Binary file not shown.
BIN
static/fonts/NotoSans/NotoSans-Italic.ttf
Normal file
BIN
static/fonts/NotoSans/NotoSans-Italic.ttf
Normal file
Binary file not shown.
BIN
static/fonts/NotoSans/NotoSans-Light.ttf
Normal file
BIN
static/fonts/NotoSans/NotoSans-Light.ttf
Normal file
Binary file not shown.
BIN
static/fonts/NotoSans/NotoSans-LightItalic.ttf
Normal file
BIN
static/fonts/NotoSans/NotoSans-LightItalic.ttf
Normal file
Binary file not shown.
BIN
static/fonts/NotoSans/NotoSans-Medium.ttf
Normal file
BIN
static/fonts/NotoSans/NotoSans-Medium.ttf
Normal file
Binary file not shown.
BIN
static/fonts/NotoSans/NotoSans-MediumItalic.ttf
Normal file
BIN
static/fonts/NotoSans/NotoSans-MediumItalic.ttf
Normal file
Binary file not shown.
BIN
static/fonts/NotoSans/NotoSans-Regular.ttf
Normal file
BIN
static/fonts/NotoSans/NotoSans-Regular.ttf
Normal file
Binary file not shown.
BIN
static/fonts/NotoSans/NotoSans-SemiBold.ttf
Normal file
BIN
static/fonts/NotoSans/NotoSans-SemiBold.ttf
Normal file
Binary file not shown.
BIN
static/fonts/NotoSans/NotoSans-SemiBoldItalic.ttf
Normal file
BIN
static/fonts/NotoSans/NotoSans-SemiBoldItalic.ttf
Normal file
Binary file not shown.
BIN
static/fonts/NotoSans/NotoSans-Thin.ttf
Normal file
BIN
static/fonts/NotoSans/NotoSans-Thin.ttf
Normal file
Binary file not shown.
BIN
static/fonts/NotoSans/NotoSans-ThinItalic.ttf
Normal file
BIN
static/fonts/NotoSans/NotoSans-ThinItalic.ttf
Normal file
Binary file not shown.
93
static/fonts/NotoSans/OFL.txt
Normal file
93
static/fonts/NotoSans/OFL.txt
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2015-2021 Google LLC. All Rights Reserved.
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
202
static/fonts/Roboto/LICENSE.txt
Normal file
202
static/fonts/Roboto/LICENSE.txt
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
BIN
static/fonts/Roboto/Roboto-Black.ttf
Normal file
BIN
static/fonts/Roboto/Roboto-Black.ttf
Normal file
Binary file not shown.
BIN
static/fonts/Roboto/Roboto-BlackItalic.ttf
Normal file
BIN
static/fonts/Roboto/Roboto-BlackItalic.ttf
Normal file
Binary file not shown.
BIN
static/fonts/Roboto/Roboto-Bold.ttf
Normal file
BIN
static/fonts/Roboto/Roboto-Bold.ttf
Normal file
Binary file not shown.
BIN
static/fonts/Roboto/Roboto-BoldItalic.ttf
Normal file
BIN
static/fonts/Roboto/Roboto-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
static/fonts/Roboto/Roboto-Italic.ttf
Normal file
BIN
static/fonts/Roboto/Roboto-Italic.ttf
Normal file
Binary file not shown.
BIN
static/fonts/Roboto/Roboto-Light.ttf
Normal file
BIN
static/fonts/Roboto/Roboto-Light.ttf
Normal file
Binary file not shown.
BIN
static/fonts/Roboto/Roboto-LightItalic.ttf
Normal file
BIN
static/fonts/Roboto/Roboto-LightItalic.ttf
Normal file
Binary file not shown.
BIN
static/fonts/Roboto/Roboto-Medium.ttf
Normal file
BIN
static/fonts/Roboto/Roboto-Medium.ttf
Normal file
Binary file not shown.
BIN
static/fonts/Roboto/Roboto-MediumItalic.ttf
Normal file
BIN
static/fonts/Roboto/Roboto-MediumItalic.ttf
Normal file
Binary file not shown.
BIN
static/fonts/Roboto/Roboto-Regular.ttf
Normal file
BIN
static/fonts/Roboto/Roboto-Regular.ttf
Normal file
Binary file not shown.
BIN
static/fonts/Roboto/Roboto-Thin.ttf
Normal file
BIN
static/fonts/Roboto/Roboto-Thin.ttf
Normal file
Binary file not shown.
BIN
static/fonts/Roboto/Roboto-ThinItalic.ttf
Normal file
BIN
static/fonts/Roboto/Roboto-ThinItalic.ttf
Normal file
Binary file not shown.
1
static/test.wasm.map
Normal file
1
static/test.wasm.map
Normal file
File diff suppressed because one or more lines are too long
22
svelte.config.js
Normal file
22
svelte.config.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import adapter from '@sveltejs/adapter-node';
|
||||||
|
import { vitePreprocess } from '@sveltejs/kit/vite';
|
||||||
|
|
||||||
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
|
const config = {
|
||||||
|
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
||||||
|
// for more information about preprocessors
|
||||||
|
preprocess: [
|
||||||
|
vitePreprocess({ postcss: true })
|
||||||
|
],
|
||||||
|
|
||||||
|
kit: {
|
||||||
|
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
||||||
|
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
|
||||||
|
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
||||||
|
adapter: adapter()
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
26
tailwind.config.cjs
Normal file
26
tailwind.config.cjs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/** @type {import('tailwindcss').Config}*/
|
||||||
|
const config = {
|
||||||
|
content: ['./src/**/*.{html,js,svelte,ts}', "./node_modules/tw-elements/dist/js/**/*.js"],
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
require("@tailwindcss/forms"),
|
||||||
|
require("@tailwindcss/typography"),
|
||||||
|
//require("tw-elements/dist/plugin"),
|
||||||
|
],
|
||||||
|
darkMode: 'class',
|
||||||
|
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
roboto: ['Roboto'],
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
// flowbite-svelte
|
||||||
|
primary: {"50":"#eef2ff","100":"#e0e7ff","200":"#c7d2fe","300":"#a5b4fc","400":"#818cf8","500":"#6366f1","600":"#4f46e5","700":"#4338ca","800":"#3730a3","900":"#312e81"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = config;
|
||||||
17
tsconfig.json
Normal file
17
tsconfig.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"extends": "./.svelte-kit/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true
|
||||||
|
}
|
||||||
|
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||||
|
//
|
||||||
|
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||||
|
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||||
|
}
|
||||||
11
vite.config.ts
Normal file
11
vite.config.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// vite.config.ts
|
||||||
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [sveltekit()],
|
||||||
|
server: {
|
||||||
|
port: 14013,
|
||||||
|
host: true // 또는 '0.0.0.0'
|
||||||
|
},
|
||||||
|
});
|
||||||
BIN
webassembly/0001.dcm
Normal file
BIN
webassembly/0001.dcm
Normal file
Binary file not shown.
BIN
webassembly/0002.dcm
Normal file
BIN
webassembly/0002.dcm
Normal file
Binary file not shown.
BIN
webassembly/0015.dcm
Normal file
BIN
webassembly/0015.dcm
Normal file
Binary file not shown.
BIN
webassembly/0020.dcm
Normal file
BIN
webassembly/0020.dcm
Normal file
Binary file not shown.
6
webassembly/build_dcm_mod.sh
Executable file
6
webassembly/build_dcm_mod.sh
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
emcc -std=c++17 -s MODULARIZE=1 -s EXPORT_ES6=1 -s USE_ES6_IMPORT_META=0 -s ENVIRONMENT='web' -s EXPORTED_RUNTIME_METHODS="['ccall','cwrap','specialHTMLTargets', 'JSEvents', 'GL', 'callMain', 'abort', 'addFunction']" -s EXPORTED_FUNCTIONS="['_malloc', '_main', '_UpdateDcmImage', '_SetWindowWidthLevel', '_GetWindowWidth', '_GetWindowCenter','_SetCallbackUpdateDcmImageComplete']" -s RESERVED_FUNCTION_POINTERS=20 -s MAXIMUM_MEMORY=4096MB -s ALLOW_TABLE_GROWTH -s ALLOW_MEMORY_GROWTH -s USE_SDL=2 -s WASM=1 -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS="[""png"", ""jpg""]" -I /work/project/emsdk/upstream/emscripten/cache/sysroot/include /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libdcmjpls.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libdcmjpeg.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libdcmimage.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libdcmimgle.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/liboflog.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libofstd.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libdcmiod.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libdcmjpls.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libdcmdata.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libijg8.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libijg12.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libijg16.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libcharls.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libdcmjpeg.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libi2d.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libz.a camera.cpp events.cpp dcm_mod.cc -O2 -o dcm_mod.js &&
|
||||||
|
cp -av dcm_mod.wasm ../static/. &&
|
||||||
|
sed "1d" dcm_mod.js > tmp.js && awk 'BEGIN{printf "\nexport "} {print}' tmp.js > dcm_mod.js && rm tmp.js &&
|
||||||
|
cp -av dcm_mod.js ../src/ui/.
|
||||||
8
webassembly/build_mod3.sh
Executable file
8
webassembly/build_mod3.sh
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
export CURRENT_DIR=$PWD
|
||||||
|
echo $CURRENT_DIR
|
||||||
|
|
||||||
|
emcc -std=c++17 -s MODULARIZE=1 -s EXPORT_ES6=1 -s ASSERTIONS=1 -s ENVIRONMENT='web' -s EXPORTED_RUNTIME_METHODS="['ccall','cwrap','specialHTMLTargets', 'JSEvents', 'GL', 'callMain', 'abort', 'addFunction']" -s EXPORTED_FUNCTIONS="['_malloc', '_main', '_UpdateDcmImage', '_SetWindowWidthLevel', '_GetWindowWidth', '_GetWindowCenter','_SetCallbackUpdateDcmImageComplete', '_CancelLoadImage', '_SetDisplayFrameIndex', '_SetResizeFrame', '_GetTotalBytes', '_GetReadBytes', '_GetTotalFrames', '_GetFrameUpdateTimeDelay' ]" -s RESERVED_FUNCTION_POINTERS=20 -s MAXIMUM_MEMORY=4096MB -s ALLOW_TABLE_GROWTH -s ALLOW_MEMORY_GROWTH -s USE_SDL=2 -s WASM=1 -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS="[""png"", ""jpg""]" -I /work/project/emsdk/upstream/emscripten/cache/sysroot/include /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libdcmjpls.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libdcmjpeg.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libdcmimage.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libdcmimgle.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/liboflog.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libofstd.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libdcmiod.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libdcmjpls.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libdcmdata.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libijg8.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libijg12.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libijg16.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libcharls.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libdcmjpeg.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libi2d.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libz.a camera.cpp events.cpp dcm_image3.cc -O2 -o dcm_image_mod3.js
|
||||||
|
|
||||||
|
|
||||||
9
webassembly/build_mod3.sh.vi
Executable file
9
webassembly/build_mod3.sh.vi
Executable file
@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
export CURRENT_DIR=$PWD
|
||||||
|
echo $CURRENT_DIR
|
||||||
|
|
||||||
|
emcc -std=c++17 -s MODULARIZE=1 -s EXPORT_ES6=1 -s USE_ES6_IMPORT_META=0 -s ENVIRONMENT='web' -s EXPORTED_RUNTIME_METHODS="['ccall','cwrap','specialHTMLTargets', 'JSEvents', 'GL', 'callMain', 'abort', 'addFunction']" -s EXPORTED_FUNCTIONS="['_malloc', '_main', '_UpdateDcmImage', '_SetWindowWidthLevel', '_GetWindowWidth', '_GetWindowCenter','_SetCallbackUpdateDcmImageComplete', '_CancelLoadImage', '_SetDisplayFrameIndex', '_SetResizeFrame', '_GetTotalBytes', '_GetReadBytes', '_GetTotalFrames', '_GetFrameUpdateTimeDelay' ]" -s RESERVED_FUNCTION_POINTERS=20 -s MAXIMUM_MEMORY=4096MB -s ALLOW_TABLE_GROWTH -s ALLOW_MEMORY_GROWTH -s USE_SDL=2 -s WASM=1 -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS="[""png"", ""jpg""]" -I /work/project/emsdk/upstream/emscripten/cache/sysroot/include /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libdcmjpls.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libdcmjpeg.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libdcmimage.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libdcmimgle.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/liboflog.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libofstd.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libdcmiod.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libdcmjpls.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libdcmdata.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libijg8.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libijg12.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libijg16.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libcharls.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libdcmjpeg.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libi2d.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libz.a camera.cpp events.cpp dcm_image3.cc -O2 -o dcm_image_mod3.js &&
|
||||||
|
cp -av dcm_image_mod3.wasm $CURRENT_DIR/../static/. &&
|
||||||
|
sed "1d" dcm_image_mod3.js > tmp.js && awk 'BEGIN{printf "\nexport "} {print}' tmp.js > dcm_image_mod3.js && rm tmp.js &&
|
||||||
|
cp -av dcm_image_mod3.js $CURRENT_DIR/../src/ui/.
|
||||||
150
webassembly/camera.cpp
Normal file
150
webassembly/camera.cpp
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
//
|
||||||
|
// Camera - pan, zoom, and window resizing
|
||||||
|
//
|
||||||
|
#include <algorithm>
|
||||||
|
#include <SDL.h>
|
||||||
|
#include <SDL_opengles2.h>
|
||||||
|
#include "camera.h"
|
||||||
|
|
||||||
|
bool Camera::updated()
|
||||||
|
{
|
||||||
|
bool updated = mCameraUpdated;
|
||||||
|
mCameraUpdated = false;
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Camera::windowResized()
|
||||||
|
{
|
||||||
|
bool resized = mWindowResized;
|
||||||
|
mWindowResized = false;
|
||||||
|
return resized;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera::setWindowSize(int width, int height)
|
||||||
|
{
|
||||||
|
if (mWindowSize.width != width || mWindowSize.height != height)
|
||||||
|
{
|
||||||
|
mWindowResized = true;
|
||||||
|
mWindowSize = {width, height};
|
||||||
|
mViewport = {(float)width, (float)height};
|
||||||
|
//setAspect((float)width / (float)height);
|
||||||
|
setAspect(1.0);
|
||||||
|
|
||||||
|
printf("setWindowSize: %d, %d, %f\n", width, height, (float)width/(float)height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp val between lo and hi
|
||||||
|
float Camera::clamp (float val, float lo, float hi)
|
||||||
|
{
|
||||||
|
return std::max(lo, std::min(val, hi));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert from normalized window coords (x,y) in ([0.0, 1.0], [1.0, 0.0]) to device coords ([-1.0, 1.0], [-1.0,1.0])
|
||||||
|
void Camera::normWindowToDeviceCoords (float normWinX, float normWinY, float& deviceX, float& deviceY)
|
||||||
|
{
|
||||||
|
deviceX = (normWinX - 0.5f) * 2.0f;
|
||||||
|
deviceY = (1.0f - normWinY - 0.5f) * 2.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert from window coords (x,y) in ([0, mWindowWidth], [mWindowHeight, 0]) to device coords ([-1.0, 1.0], [-1.0,1.0])
|
||||||
|
void Camera::windowToDeviceCoords (int winX, int winY, float& deviceX, float& deviceY)
|
||||||
|
{
|
||||||
|
normWindowToDeviceCoords(winX / (float)mWindowSize.width, winY / (float)mWindowSize.height, deviceX, deviceY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert from device coords ([-1.0, 1.0], [-1.0,1.0]) to world coords ([-inf, inf], [-inf, inf])
|
||||||
|
void Camera::deviceToWorldCoords (float deviceX, float deviceY, float& worldX, float& worldY)
|
||||||
|
{
|
||||||
|
worldX = deviceX / mZoom - mPan.x;
|
||||||
|
worldY = deviceY / mAspect / mZoom - mPan.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert from window coords (x,y) in ([0, windowWidth], [windowHeight, 0]) to world coords ([-inf, inf], [-inf, inf])
|
||||||
|
void Camera::windowToWorldCoords(int winX, int winY, float& worldX, float& worldY)
|
||||||
|
{
|
||||||
|
float deviceX, deviceY;
|
||||||
|
windowToDeviceCoords(winX, winY, deviceX, deviceY);
|
||||||
|
deviceToWorldCoords(deviceX, deviceY, worldX, worldY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert from normalized window coords (x,y) in in ([0.0, 1.0], [1.0, 0.0]) to world coords ([-inf, inf], [-inf, inf])
|
||||||
|
void Camera::normWindowToWorldCoords(float normWinX, float normWinY, float& worldX, float& worldY)
|
||||||
|
{
|
||||||
|
float deviceX, deviceY;
|
||||||
|
normWindowToDeviceCoords(normWinX, normWinY, deviceX, deviceY);
|
||||||
|
deviceToWorldCoords(deviceX, deviceY, worldX, worldY);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera::reset()
|
||||||
|
{
|
||||||
|
mZoom = 1.0f;
|
||||||
|
mPan = Vec2{0.0f, 0.0f};
|
||||||
|
mBasePan = Vec2{0.0f, 0.0f};
|
||||||
|
mAspect = 1.0f;
|
||||||
|
|
||||||
|
mCameraUpdated = true;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera::toJson(nlohmann::json& json_obj) const
|
||||||
|
{
|
||||||
|
printf("1");
|
||||||
|
json_obj["camera"]["cameraUpdated"] = mCameraUpdated;
|
||||||
|
json_obj["camera"]["windowResized"] = mWindowResized;
|
||||||
|
|
||||||
|
printf("2");
|
||||||
|
json_obj["camera"]["width"] = mWindowSize.width;
|
||||||
|
json_obj["camera"]["height"] = mWindowSize.height;
|
||||||
|
|
||||||
|
printf("3");
|
||||||
|
json_obj["camera"]["viewport_x"] = mViewport.x;
|
||||||
|
json_obj["camera"]["viewport_y"] = mViewport.y;
|
||||||
|
|
||||||
|
printf("4");
|
||||||
|
json_obj["camera"]["pan_x"] = mPan.x;
|
||||||
|
json_obj["camera"]["pan_y"] = mPan.y;
|
||||||
|
|
||||||
|
printf("5");
|
||||||
|
json_obj["camera"]["zoom"] = mZoom;
|
||||||
|
json_obj["camera"]["aspect"] = mAspect;
|
||||||
|
|
||||||
|
printf("6");
|
||||||
|
json_obj["camera"]["basepan_x"] = mBasePan.x;
|
||||||
|
json_obj["camera"]["basepan_y"] = mBasePan.y;
|
||||||
|
|
||||||
|
printf("exit toJson");
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera::fromString(const char* pStrDumpJson)
|
||||||
|
{
|
||||||
|
printf("Camera::fromString enter!%s\n", pStrDumpJson);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
nlohmann::json json_obj = nlohmann::json::parse(pStrDumpJson);
|
||||||
|
|
||||||
|
printf("1");
|
||||||
|
const auto& camera_data = json_obj["camera"];
|
||||||
|
mCameraUpdated = camera_data["cameraUpdated"];
|
||||||
|
mWindowResized = camera_data["windowResized"];
|
||||||
|
mWindowSize.width = camera_data["width"];
|
||||||
|
mWindowSize.height = camera_data["height"];
|
||||||
|
mViewport.x = camera_data["viewport_x"];
|
||||||
|
mViewport.y = camera_data["viewport_y"];
|
||||||
|
mPan.x = camera_data["pan_x"];
|
||||||
|
mPan.y = camera_data["pan_y"];
|
||||||
|
mZoom = camera_data["zoom"];
|
||||||
|
mAspect = camera_data["aspect"];
|
||||||
|
mBasePan.x = camera_data["basepan_x"];
|
||||||
|
mBasePan.y = camera_data["basepan_y"];
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (const nlohmann::json::exception& e)
|
||||||
|
{
|
||||||
|
// It's good practice to catch potential errors
|
||||||
|
printf("JSON parsing error: %s\n", e.what());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
75
webassembly/camera.h
Normal file
75
webassembly/camera.h
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
//
|
||||||
|
// Camera - pan, zoom, and window resizing
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
#include "nlohmann/json.hpp" // 라이브러리 경로에 맞게 수정
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
struct Rect { int width, height; };
|
||||||
|
struct Vec2 { GLfloat x, y; };
|
||||||
|
|
||||||
|
class Camera
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Camera();
|
||||||
|
|
||||||
|
void reset();
|
||||||
|
bool updated();
|
||||||
|
bool windowResized();
|
||||||
|
|
||||||
|
Rect& windowSize() { return mWindowSize; }
|
||||||
|
void setWindowSize (int width, int height);
|
||||||
|
GLfloat* viewport() { return (GLfloat*)&mViewport; }
|
||||||
|
|
||||||
|
GLfloat* pan() { return (GLfloat*)&mPan; }
|
||||||
|
GLfloat zoom() { return mZoom; }
|
||||||
|
GLfloat aspect() { return mAspect; }
|
||||||
|
|
||||||
|
void setPan (Vec2 pan) { mPan = pan; mCameraUpdated = true; }
|
||||||
|
void setPanDelta (Vec2 panDelta) { mPan.x += panDelta.x; mPan.y += panDelta.y; mCameraUpdated = true; }
|
||||||
|
void setZoom (GLfloat zoom) { mZoom = clamp(zoom, cZoomMin, cZoomMax); mCameraUpdated = true; }
|
||||||
|
void setZoomDelta (GLfloat zoomDelta) { mZoom = clamp(mZoom + zoomDelta, cZoomMin, cZoomMax); mCameraUpdated = true; }
|
||||||
|
void setAspect (GLfloat aspect) { mAspect = aspect; mCameraUpdated = true; }
|
||||||
|
|
||||||
|
Vec2& basePan() { return mBasePan; }
|
||||||
|
void setBasePan () { mBasePan = mPan; }
|
||||||
|
|
||||||
|
void normWindowToDeviceCoords (float normWinX, float normWinY, float& deviceX, float& deviceY);
|
||||||
|
void windowToDeviceCoords (int winX, int winY, float& deviceX, float& deviceY);
|
||||||
|
void deviceToWorldCoords (float deviceX, float deviceY, float& worldX, float& worldY);
|
||||||
|
void windowToWorldCoords (int winX, int winY, float& worldX, float& worldY);
|
||||||
|
void normWindowToWorldCoords (float normWinX, float normWinY, float& worldX, float& worldY);
|
||||||
|
|
||||||
|
//void Camera::toJson(emscripten::val& json)
|
||||||
|
|
||||||
|
void toJson(nlohmann::json& json_obj) const;
|
||||||
|
void fromString(const char* pStrDumpJson);
|
||||||
|
|
||||||
|
private:
|
||||||
|
float clamp (float val, float lo, float hi);
|
||||||
|
|
||||||
|
bool mCameraUpdated;
|
||||||
|
bool mWindowResized;
|
||||||
|
Rect mWindowSize;
|
||||||
|
Vec2 mViewport;
|
||||||
|
const GLfloat cZoomMin, cZoomMax;
|
||||||
|
Vec2 mBasePan, mPan;
|
||||||
|
GLfloat mZoom, mAspect;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline Camera::Camera()
|
||||||
|
: mCameraUpdated (false)
|
||||||
|
, mWindowResized (false)
|
||||||
|
, mWindowSize ({})
|
||||||
|
, mViewport ({})
|
||||||
|
, cZoomMin (0.1f), cZoomMax (10.0f)
|
||||||
|
, mBasePan ({0.0f, 0.0f})
|
||||||
|
, mPan ({0.0f, 0.0f})
|
||||||
|
, mZoom (1.0f)
|
||||||
|
, mAspect (1.0f)
|
||||||
|
{
|
||||||
|
setWindowSize(640, 480);
|
||||||
|
}
|
||||||
1123
webassembly/dcm_image3 copy.cc
Normal file
1123
webassembly/dcm_image3 copy.cc
Normal file
File diff suppressed because it is too large
Load Diff
1197
webassembly/dcm_image3.cc
Normal file
1197
webassembly/dcm_image3.cc
Normal file
File diff suppressed because it is too large
Load Diff
14
webassembly/dcm_image_mod3.js
Normal file
14
webassembly/dcm_image_mod3.js
Normal file
File diff suppressed because one or more lines are too long
970
webassembly/dcm_mod.cc
Normal file
970
webassembly/dcm_mod.cc
Normal file
@ -0,0 +1,970 @@
|
|||||||
|
//
|
||||||
|
// Emscripten/SDL2/OpenGLES2 sample that demonstrates simple geometry and shaders, mouse and touch input, and window resizing
|
||||||
|
//
|
||||||
|
// Setup:
|
||||||
|
// Install emscripten: http://kripken.github.io/emscripten-site/docs/getting_started/downloads.html
|
||||||
|
//
|
||||||
|
// Build:
|
||||||
|
// emcc -std=c++11 hello_triangle.cpp events.cpp camera.cpp -s USE_SDL=2 -s FULL_ES2=1 -s WASM=0 -o hello_triangle.html
|
||||||
|
//
|
||||||
|
// Run:
|
||||||
|
// emrun hello_triangle.html
|
||||||
|
//
|
||||||
|
// Result:
|
||||||
|
// A colorful triangle. Left mouse pans, mouse wheel zooms in/out. Window is resizable.
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
#include <emscripten.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <SDL2/SDL.h>
|
||||||
|
#include <SDL2/SDL_opengles2.h>
|
||||||
|
//#include <SDL2/SDL_opengles2_gl2ext.h>
|
||||||
|
//#include <GLES3/gl3.h>
|
||||||
|
|
||||||
|
#include <dcmtk/ofstd/ofstring.h>
|
||||||
|
#include "dcmtk/config/osconfig.h"
|
||||||
|
#include "dcmtk/dcmdata/dcfilefo.h"
|
||||||
|
#include "dcmtk/dcmdata/dcdeftag.h"
|
||||||
|
#include "dcmtk/dcmdata/dcuid.h"
|
||||||
|
#include "dcmtk/dcmdata/dcmetinf.h"
|
||||||
|
#include "dcmtk/dcmdata/dcdict.h"
|
||||||
|
#include "dcmtk/dcmdata/dcdicent.h"
|
||||||
|
#include "dcmtk/dcmdata/dcxfer.h"
|
||||||
|
|
||||||
|
#include "dcmtk/dcmjpeg/djdecode.h" /* for JPEG decoders */
|
||||||
|
#include "dcmtk/dcmjpeg/djencode.h" /* for JPEG encoders */
|
||||||
|
#include "dcmtk/dcmjpls/djdecode.h" /* for JPEG-LS decoders */
|
||||||
|
#include "dcmtk/dcmjpls/djencode.h" /* for JPEG-LS encoders */
|
||||||
|
#include "dcmtk/dcmdata/dcrledrg.h" /* for RLE decoder */
|
||||||
|
#include "dcmtk/dcmdata/dcrleerg.h" /* for RLE encoder */
|
||||||
|
#include "dcmtk/dcmjpeg/dipijpeg.h" /* for dcmimage JPEG plugin */
|
||||||
|
#include "dcmtk/dcmjpeg/djrplol.h"
|
||||||
|
|
||||||
|
|
||||||
|
#include "dcmtk/dcmimage/diregist.h"
|
||||||
|
#include "dcmtk/dcmdata/dcpixseq.h"
|
||||||
|
#include "dcmtk/dcmjpeg/djcparam.h"
|
||||||
|
#include "dcmtk/dcmjpeg/djeijg8.h"
|
||||||
|
#include "dcmtk/dcmjpeg/djcodece.h"
|
||||||
|
#include "dcmtk/dcmdata/dcchrstr.h"
|
||||||
|
#include "dcmtk/ofstd/ofchrenc.h"
|
||||||
|
|
||||||
|
#include "dcmtk/dcmdata/dcvrui.h"
|
||||||
|
#include "dcmtk/dcmdata/dcvrsh.h"
|
||||||
|
#include "dcmtk/dcmdata/dcistrmb.h"
|
||||||
|
|
||||||
|
|
||||||
|
#include "events.h"
|
||||||
|
|
||||||
|
typedef void (*callback_UpdateDcmImage) ();
|
||||||
|
callback_UpdateDcmImage callback_UpdateDcmImageComplete = NULL;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
DcmDataset* g_pDcmDataset = NULL;
|
||||||
|
DcmElement* g_pixelDataElement = NULL;
|
||||||
|
bool g_bChange = false;
|
||||||
|
|
||||||
|
EventHandler* g_pEventHandler = NULL;
|
||||||
|
|
||||||
|
GLuint textureObj = 0;
|
||||||
|
|
||||||
|
GLuint g_shaderProgramGray16 = 0;
|
||||||
|
GLuint g_shaderProgramRGB = 0;
|
||||||
|
|
||||||
|
// Vertex shader
|
||||||
|
GLint g_shaderPanGray16, g_shaderZoomGray16, g_shaderAspectGray16, g_shaderWindowCenterGray16, g_shaderWindowWidthGray16;
|
||||||
|
GLint g_shaderPanRGB, g_shaderZoomRGB, g_shaderAspectRGB, g_shaderWindowCenterRGB, g_shaderWindowWidthRGB;
|
||||||
|
|
||||||
|
int g_nWindowWidth = 0;
|
||||||
|
int g_nWindowCenter = 0;
|
||||||
|
|
||||||
|
int g_nPrevWindowWidth = 0;
|
||||||
|
int g_nPrevWindowCenter = 0;
|
||||||
|
|
||||||
|
int g_nColorType = 0;
|
||||||
|
|
||||||
|
Uint32 g_nCurrentFrame = 0;
|
||||||
|
int g_nTotalFrames = 0;
|
||||||
|
|
||||||
|
int g_nFrameWidth = 0;
|
||||||
|
int g_nFrameHeight = 0;
|
||||||
|
|
||||||
|
int g_nSamplesPerPixel = 0;
|
||||||
|
int g_nBitsAllocated = 0;
|
||||||
|
|
||||||
|
int g_nFrameSizeUncompressed = 0;
|
||||||
|
|
||||||
|
char* g_pBuf = NULL;
|
||||||
|
|
||||||
|
const GLchar* vertexSourceGray16 =
|
||||||
|
"uniform vec2 pan; \n"
|
||||||
|
"uniform float zoom; \n"
|
||||||
|
"uniform float aspect; \n"
|
||||||
|
"attribute vec4 position; \n"
|
||||||
|
"attribute vec2 a_texCoord; \n"
|
||||||
|
"varying vec2 texCoord; \n"
|
||||||
|
"void main() \n"
|
||||||
|
"{ \n"
|
||||||
|
" gl_Position = vec4(position.xyz, 1.0); \n"
|
||||||
|
" gl_Position.xy += pan; \n"
|
||||||
|
" gl_Position.xy *= zoom; \n"
|
||||||
|
" texCoord = a_texCoord; \n"
|
||||||
|
" gl_Position.y *= aspect; \n"
|
||||||
|
"} \n";
|
||||||
|
|
||||||
|
|
||||||
|
// Fragment/pixel shader
|
||||||
|
const GLchar* fragmentSourceGray16 =
|
||||||
|
"precision mediump float; \n"
|
||||||
|
"varying vec2 texCoord; \n"
|
||||||
|
"uniform sampler2D texSampler; \n"
|
||||||
|
"uniform float fWindowCenter; \n"
|
||||||
|
"uniform float fWindowWidth; \n"
|
||||||
|
"void main() \n"
|
||||||
|
"{ \n"
|
||||||
|
" vec4 colorOut = texture2D(texSampler, texCoord); \n"
|
||||||
|
" float fMin = fWindowCenter - fWindowWidth/2.0;\n"
|
||||||
|
" float fMax = fWindowCenter + fWindowWidth/2.0;\n"
|
||||||
|
" float fData = ( ((colorOut.a + colorOut.r*256.0)*256.0) - fMin)/(fMax-fMin); \n"
|
||||||
|
" fData = ( ((colorOut.a + colorOut.a*256.0)*256.0) - fMin)/(fMax-fMin); \n"
|
||||||
|
" fData = colorOut.r;\n"
|
||||||
|
" fData = (((colorOut.a * 256.0 + colorOut.r)*256.0) - fMin) / (fMax-fMin);"
|
||||||
|
" float fTmpData = fMin;\n"
|
||||||
|
" gl_FragColor = vec4(fData, fData, fData, 1.0); \n"
|
||||||
|
|
||||||
|
"} \n";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const GLchar* vertexSourceRGB =
|
||||||
|
"uniform vec2 panRGB; \n"
|
||||||
|
"uniform float zoomRGB; \n"
|
||||||
|
"uniform float aspectRGB; \n"
|
||||||
|
"attribute vec4 position; \n"
|
||||||
|
"attribute vec2 a_texCoord; \n"
|
||||||
|
"varying vec2 texCoord; \n"
|
||||||
|
"void main() \n"
|
||||||
|
"{ \n"
|
||||||
|
" gl_Position = vec4(position.xyz, 1.0); \n"
|
||||||
|
" gl_Position.xy += panRGB; \n"
|
||||||
|
" gl_Position.xy *= zoomRGB; \n"
|
||||||
|
" texCoord = a_texCoord; \n"
|
||||||
|
" gl_Position.y *= aspectRGB; \n"
|
||||||
|
"} \n";
|
||||||
|
|
||||||
|
|
||||||
|
// Fragment/pixel shader
|
||||||
|
const GLchar* fragmentSourceRGB =
|
||||||
|
"precision mediump float; \n"
|
||||||
|
"varying vec2 texCoord; \n"
|
||||||
|
"uniform sampler2D texSampler; \n"
|
||||||
|
"uniform float fWindowCenterRGB; \n"
|
||||||
|
"uniform float fWindowWidthRGB; \n"
|
||||||
|
"void main() \n"
|
||||||
|
"{ \n"
|
||||||
|
" vec4 colorOut = texture2D(texSampler, texCoord); \n"
|
||||||
|
" float fMin = fWindowCenterRGB - fWindowWidthRGB/2.0;\n"
|
||||||
|
" float fMax = fWindowCenterRGB + fWindowWidthRGB/2.0;\n"
|
||||||
|
" float fTestValue = (fMax - fMin);\n"
|
||||||
|
" float fR = (colorOut.r*256.0 - fMin) / (fMax-fMin);\n"
|
||||||
|
" float fG = (colorOut.g*256.0 - fMin) / (fMax-fMin);\n"
|
||||||
|
" float fB = (colorOut.b*256.0 - fMin) / (fMax-fMin);\n"
|
||||||
|
" gl_FragColor = vec4(fR, fG, fB, 1.0); \n"
|
||||||
|
//" gl_FragColor = vec4(colorOut.r/fTestValue, 1.0, 1.0, 1.0); \n"
|
||||||
|
|
||||||
|
"} \n";
|
||||||
|
|
||||||
|
void updateShader(EventHandler& eventHandler)
|
||||||
|
{
|
||||||
|
Camera& camera = eventHandler.camera();
|
||||||
|
|
||||||
|
printf("updateShader: g_nColorType=%d\n", g_nColorType);
|
||||||
|
|
||||||
|
|
||||||
|
if(g_nColorType==0)
|
||||||
|
{
|
||||||
|
glUseProgram(g_shaderProgramGray16);
|
||||||
|
|
||||||
|
glUniform2fv(g_shaderPanGray16, 1, camera.pan());
|
||||||
|
glUniform1f(g_shaderZoomGray16, camera.zoom());
|
||||||
|
glUniform1f(g_shaderAspectGray16, camera.aspect());
|
||||||
|
}
|
||||||
|
else if(g_nColorType==1)
|
||||||
|
{
|
||||||
|
glUseProgram(g_shaderProgramRGB);
|
||||||
|
|
||||||
|
glUniform2fv(g_shaderPanRGB, 1, camera.pan());
|
||||||
|
glUniform1f(g_shaderZoomRGB, camera.zoom());
|
||||||
|
glUniform1f(g_shaderAspectRGB, camera.aspect());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void onErrorData(void* arg)
|
||||||
|
{
|
||||||
|
printf("onErrorData %d\n", (int)arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateTextureGray16(int nWidth, int nHeight, void* pData)
|
||||||
|
{
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE_ALPHA, nWidth, nHeight, 0, GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE, pData);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateTextureRGB(int nWidth, int nHeight, void* pData)
|
||||||
|
{
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, nWidth, nHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, pData);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateWindowWidthLevelGray16(int nWidth, int nLevel)
|
||||||
|
{
|
||||||
|
glUniform1f(g_shaderWindowCenterGray16, (float)nLevel);
|
||||||
|
glUniform1f(g_shaderWindowWidthGray16, (float)nWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateWindowWidthLevelRGB(int nWidth, int nLevel)
|
||||||
|
{
|
||||||
|
glUniform1f(g_shaderWindowCenterRGB, (float)nLevel);
|
||||||
|
glUniform1f(g_shaderWindowWidthRGB, (float)nWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
void initShaderGray16()
|
||||||
|
{
|
||||||
|
// Create and compile vertex shader
|
||||||
|
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
|
||||||
|
glShaderSource(vertexShader, 1, &vertexSourceGray16, NULL);
|
||||||
|
glCompileShader(vertexShader);
|
||||||
|
|
||||||
|
// Create and compile fragment shader
|
||||||
|
GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
|
||||||
|
glShaderSource(fragmentShader, 1, &fragmentSourceGray16, NULL);
|
||||||
|
glCompileShader(fragmentShader);
|
||||||
|
|
||||||
|
// Link vertex and fragment shader into shader program and use it
|
||||||
|
g_shaderProgramGray16 = glCreateProgram();
|
||||||
|
glAttachShader(g_shaderProgramGray16, vertexShader);
|
||||||
|
glAttachShader(g_shaderProgramGray16, fragmentShader);
|
||||||
|
glLinkProgram(g_shaderProgramGray16);
|
||||||
|
//glUseProgram(g_shaderProgramGray16);
|
||||||
|
|
||||||
|
// Get shader variables and initialize them
|
||||||
|
g_shaderPanGray16 = glGetUniformLocation(g_shaderProgramGray16, "pan");
|
||||||
|
g_shaderZoomGray16 = glGetUniformLocation(g_shaderProgramGray16, "zoom");
|
||||||
|
g_shaderAspectGray16 = glGetUniformLocation(g_shaderProgramGray16, "aspect");
|
||||||
|
g_shaderWindowCenterGray16 = glGetUniformLocation(g_shaderProgramGray16, "fWindowCenter");
|
||||||
|
g_shaderWindowWidthGray16 = glGetUniformLocation(g_shaderProgramGray16, "fWindowWidth");
|
||||||
|
|
||||||
|
glUniform1f(g_shaderWindowCenterGray16, 128.0f);
|
||||||
|
glUniform1f(g_shaderWindowWidthGray16, 255.0f);
|
||||||
|
//printf("pan:%d, zoom:%d, aspect:%d, g_shaderWindowCenterGray16:%d, g_shaderWindowWidthGray16:%d\n", g_shaderPanGray16, g_shaderZoomGray16, g_shaderAspectGray16, g_shaderWindowCenterGray16, g_shaderWindowWidthGray16);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void initShaderRGB()
|
||||||
|
{
|
||||||
|
// Create and compile vertex shader
|
||||||
|
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
|
||||||
|
glShaderSource(vertexShader, 1, &vertexSourceRGB, NULL);
|
||||||
|
glCompileShader(vertexShader);
|
||||||
|
|
||||||
|
// Create and compile fragment shader
|
||||||
|
GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
|
||||||
|
glShaderSource(fragmentShader, 1, &fragmentSourceRGB, NULL);
|
||||||
|
glCompileShader(fragmentShader);
|
||||||
|
|
||||||
|
// Link vertex and fragment shader into shader program and use it
|
||||||
|
g_shaderProgramRGB = glCreateProgram();
|
||||||
|
//printf("g_shaderProgramRGB: %d\n", g_shaderProgramRGB);
|
||||||
|
glAttachShader(g_shaderProgramRGB, vertexShader);
|
||||||
|
glAttachShader(g_shaderProgramRGB, fragmentShader);
|
||||||
|
glLinkProgram(g_shaderProgramRGB);
|
||||||
|
//glUseProgram(g_shaderProgramRGB);
|
||||||
|
|
||||||
|
// Get shader variables and initialize them
|
||||||
|
g_shaderPanRGB = glGetUniformLocation(g_shaderProgramRGB, "panRGB");
|
||||||
|
g_shaderZoomRGB = glGetUniformLocation(g_shaderProgramRGB, "zoomRGB");
|
||||||
|
g_shaderAspectRGB = glGetUniformLocation(g_shaderProgramRGB, "aspectRGB");
|
||||||
|
g_shaderWindowCenterRGB = glGetUniformLocation(g_shaderProgramRGB, "fWindowCenterRGB");
|
||||||
|
g_shaderWindowWidthRGB = glGetUniformLocation(g_shaderProgramRGB, "fWindowWidthRGB");
|
||||||
|
|
||||||
|
glUniform1f(g_shaderWindowCenterRGB, 128.0f);
|
||||||
|
glUniform1f(g_shaderWindowWidthRGB, 255.0f);
|
||||||
|
//printf("pan:%d, zoom:%d, aspect:%d, g_shaderWindowCenterRGB:%d, g_shaderWindowWidthRGB:%d\n", g_shaderPanRGB, g_shaderZoomRGB, g_shaderAspectRGB, g_shaderWindowCenterRGB, g_shaderWindowWidthRGB);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
GLuint initShader(EventHandler& eventHandler)
|
||||||
|
{
|
||||||
|
initShaderGray16();
|
||||||
|
initShaderRGB();
|
||||||
|
updateShader(eventHandler);
|
||||||
|
|
||||||
|
g_pEventHandler = &eventHandler;
|
||||||
|
|
||||||
|
return g_shaderProgramGray16;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint g_vbo = 0;
|
||||||
|
|
||||||
|
void initGeometry(GLuint shaderProgram)
|
||||||
|
{
|
||||||
|
// Create vertex buffer object and copy vertex data into it
|
||||||
|
|
||||||
|
//printf("g_vbo: %d\n", g_vbo);
|
||||||
|
|
||||||
|
glGenBuffers(1, &g_vbo);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, g_vbo);
|
||||||
|
GLfloat vertices[] =
|
||||||
|
{
|
||||||
|
-1.0f, -1.0f, 0.0f, 0.0f, 1.0f,
|
||||||
|
1.0f, -1.0f, 0.0f, 1.0f, 1.0f,
|
||||||
|
-1.0f, 1.0f, 0.0f, 0.0f, 0.0f,
|
||||||
|
|
||||||
|
1.0f, 1.0f, 0.0f, 1.0f, 0.0f,
|
||||||
|
-1.0f, 1.0f, 0.0f, 0.0f, 0.0f,
|
||||||
|
1.0f, -1.0f, 0.0f, 1.0f, 1.0f,
|
||||||
|
};
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
|
||||||
|
|
||||||
|
// Specify the layout of the shader vertex data (positions only, 3 floats)
|
||||||
|
GLint posAttrib = glGetAttribLocation(shaderProgram, "position");
|
||||||
|
glEnableVertexAttribArray(posAttrib);
|
||||||
|
// glVertexAttribPointer(posAttrib, 3, GL_FLOAT, GL_FALSE, 0, 0);
|
||||||
|
glVertexAttribPointer(posAttrib, 3, GL_FLOAT, GL_FALSE, 5*sizeof(GLfloat), (GLvoid*)0);
|
||||||
|
// glEnableVertexAttribArray(0);
|
||||||
|
|
||||||
|
GLint texAttrib = glGetAttribLocation(shaderProgram, "a_texCoord");
|
||||||
|
glEnableVertexAttribArray(texAttrib);
|
||||||
|
glVertexAttribPointer(texAttrib, 2, GL_FLOAT, GL_FALSE, 5*sizeof(GLfloat), (GLvoid*)(3*sizeof(GLfloat)));
|
||||||
|
//glEnableVertexAttribArray(2);
|
||||||
|
|
||||||
|
// glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||||
|
|
||||||
|
//printf("initGeometry(%d, %d)\n", posAttrib, texAttrib);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void PrintError()
|
||||||
|
{
|
||||||
|
int nError = 0;
|
||||||
|
nError = glGetError();
|
||||||
|
printf("Error Code: %d\n", nError);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void onLoadedData(void* arg, void* buffer, int nSize)
|
||||||
|
{
|
||||||
|
//printf("onLoadedData %d, %d\n", (int)arg, nSize);
|
||||||
|
|
||||||
|
OFCondition error;
|
||||||
|
E_TransferSyntax opt_ixfer = EXS_Unknown;
|
||||||
|
E_FileReadMode opt_readMode = ERM_autoDetect;
|
||||||
|
Uint8* pData = (Uint8*)buffer;
|
||||||
|
|
||||||
|
int nFindIndex = 0;
|
||||||
|
bool bFind = false;
|
||||||
|
int i=0;
|
||||||
|
Uint8* pDataTmp = (Uint8*)buffer;
|
||||||
|
for(i=0 ; i<nSize-4 && bFind==false ; i++)
|
||||||
|
{
|
||||||
|
if(pDataTmp[i]=='D' && pDataTmp[i+1]=='I' && pDataTmp[i+2]=='C' && pDataTmp[i+3]=='M')
|
||||||
|
{
|
||||||
|
nFindIndex = i;
|
||||||
|
bFind = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(pData, 0x00, nFindIndex-1);
|
||||||
|
|
||||||
|
// printf("nFindIndex = 0x%x", nFindIndex);
|
||||||
|
|
||||||
|
|
||||||
|
DcmInputBufferStream dcmBuffer;
|
||||||
|
dcmBuffer.setBuffer(pData, nSize);
|
||||||
|
dcmBuffer.setEos();
|
||||||
|
|
||||||
|
DcmInputBufferStream dcmBuffer2;
|
||||||
|
dcmBuffer2.setBuffer(pData, nSize);
|
||||||
|
dcmBuffer2.setEos();
|
||||||
|
|
||||||
|
|
||||||
|
// printf("%02x %02x %02x %02x\n", pData[0], pData[1], pData[2], pData[3]);
|
||||||
|
|
||||||
|
if(g_pDcmDataset!=NULL)
|
||||||
|
{
|
||||||
|
delete g_pDcmDataset;
|
||||||
|
}
|
||||||
|
g_pDcmDataset = new DcmDataset;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
OFString strTransferSyntaxUID;
|
||||||
|
|
||||||
|
g_pDcmDataset->transferInit();
|
||||||
|
{
|
||||||
|
error = g_pDcmDataset->read(dcmBuffer2, EXS_LittleEndianExplicit);
|
||||||
|
g_pDcmDataset->findAndGetOFString(DCM_TransferSyntaxUID, strTransferSyntaxUID);
|
||||||
|
g_pDcmDataset->transferEnd();
|
||||||
|
dcmBuffer2.releaseBuffer();
|
||||||
|
|
||||||
|
delete g_pDcmDataset;
|
||||||
|
g_pDcmDataset = new DcmDataset;
|
||||||
|
}
|
||||||
|
if (strTransferSyntaxUID.size() > 0)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
DcmXfer dcmXfer(strTransferSyntaxUID.c_str());
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
g_pDcmDataset->transferInit();
|
||||||
|
|
||||||
|
//error = g_pDcmDataset->read(dcmBuffer, EXS_JPEGProcess14SV1);
|
||||||
|
error = g_pDcmDataset->read(dcmBuffer, dcmXfer.getXfer());
|
||||||
|
if(error.bad())
|
||||||
|
{
|
||||||
|
printf("Dataset State: %u(%s)\n", g_pDcmDataset->transferState(), error.text());
|
||||||
|
}
|
||||||
|
|
||||||
|
g_pDcmDataset->transferEnd();
|
||||||
|
|
||||||
|
dcmBuffer.releaseBuffer();
|
||||||
|
|
||||||
|
|
||||||
|
g_nTotalFrames = 1;
|
||||||
|
g_nFrameWidth = 0;
|
||||||
|
g_nFrameHeight = 0;
|
||||||
|
g_nSamplesPerPixel = 1;
|
||||||
|
g_nBitsAllocated = 8;
|
||||||
|
|
||||||
|
|
||||||
|
Uint16 val = 0;
|
||||||
|
g_pDcmDataset->findAndGetUint16(DCM_Rows, val);
|
||||||
|
g_nFrameHeight = val;
|
||||||
|
|
||||||
|
g_pDcmDataset->findAndGetUint16(DCM_Columns, val);
|
||||||
|
g_nFrameWidth = val;
|
||||||
|
|
||||||
|
g_pDcmDataset->findAndGetUint16(DCM_SamplesPerPixel, val);
|
||||||
|
g_nSamplesPerPixel = val;
|
||||||
|
|
||||||
|
g_pDcmDataset->findAndGetUint16(DCM_BitsAllocated, val);
|
||||||
|
g_nBitsAllocated = val;
|
||||||
|
|
||||||
|
Sint32 nFrames = 0;
|
||||||
|
OFString strFrames;
|
||||||
|
// g_pDcmDataset->findAndGetSint32(DCM_NumberOfFrames, nFrames);
|
||||||
|
g_pDcmDataset->findAndGetOFString(DCM_NumberOfFrames, strFrames);
|
||||||
|
|
||||||
|
nFrames = atoi(strFrames.c_str());
|
||||||
|
if(nFrames<=0)
|
||||||
|
{
|
||||||
|
nFrames = 1;
|
||||||
|
}
|
||||||
|
g_nTotalFrames = nFrames;
|
||||||
|
|
||||||
|
OFString strTmp;
|
||||||
|
|
||||||
|
strTmp = "";
|
||||||
|
g_pDcmDataset->findAndGetOFString(DCM_WindowCenter, strTmp);
|
||||||
|
if(g_nWindowCenter<=0)
|
||||||
|
g_nWindowCenter = atoi(strTmp.c_str());
|
||||||
|
|
||||||
|
strTmp = "";
|
||||||
|
g_pDcmDataset->findAndGetOFString(DCM_WindowWidth, strTmp);
|
||||||
|
if(g_nWindowWidth<=0)
|
||||||
|
g_nWindowWidth = atoi(strTmp.c_str());
|
||||||
|
|
||||||
|
g_nPrevWindowCenter = g_nWindowCenter;
|
||||||
|
g_nPrevWindowWidth = g_nWindowWidth;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if(g_nSamplesPerPixel==3 && g_nBitsAllocated==8)
|
||||||
|
{
|
||||||
|
g_nColorType = 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
g_nColorType = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
printf("ColorType:%d, Width:%d, Height:%d, TotalFrames:(%s)(%d) SamplesPerPixel:%d, BitsAllocated:%d\n", g_nColorType, g_nFrameWidth, g_nFrameHeight, strFrames.c_str(), g_nTotalFrames, g_nSamplesPerPixel, g_nBitsAllocated);
|
||||||
|
|
||||||
|
if(g_nFrameWidth==0)
|
||||||
|
{
|
||||||
|
DcmElement* pElement = NULL;
|
||||||
|
g_pDcmDataset->findAndGetElement(DCM_Columns, pElement);
|
||||||
|
printf("pElement: %x\n", pElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
g_pixelDataElement = NULL;
|
||||||
|
g_pDcmDataset->findAndGetElement(DCM_PixelData, g_pixelDataElement);
|
||||||
|
|
||||||
|
const Uint8* pImageData = NULL;
|
||||||
|
g_pDcmDataset->findAndGetUint8Array(DCM_PixelData, pImageData);
|
||||||
|
|
||||||
|
if(pImageData!=NULL)
|
||||||
|
{
|
||||||
|
error = g_pDcmDataset->chooseRepresentation(EXS_LittleEndianExplicit, NULL);
|
||||||
|
if(error.bad())
|
||||||
|
{
|
||||||
|
printf("chooseRepresentation: %s\n", error.text());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
g_pDcmDataset->findAndGetUint8Array(DCM_PixelData, pImageData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
printf("Find Pixel Data!!!\n");
|
||||||
|
if(g_pixelDataElement->isEmpty()==true)
|
||||||
|
{
|
||||||
|
printf("PixelData is Empty!!\n");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
printf("ok0\n");
|
||||||
|
Uint32 nFrameSizeUncompressed = 0;
|
||||||
|
g_pixelDataElement->getUncompressedFrameSize(g_pDcmDataset, nFrameSizeUncompressed);
|
||||||
|
g_nFrameSizeUncompressed = nFrameSizeUncompressed;
|
||||||
|
|
||||||
|
printf("ok1: %d\n", g_nFrameSizeUncompressed);
|
||||||
|
|
||||||
|
if(g_pBuf!=NULL)
|
||||||
|
{
|
||||||
|
delete[] g_pBuf;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_pBuf = new char[g_nFrameSizeUncompressed];
|
||||||
|
|
||||||
|
OFCondition ofTest;
|
||||||
|
OFString decompressedColorModel;
|
||||||
|
|
||||||
|
Uint32 startFragment = 0;
|
||||||
|
|
||||||
|
|
||||||
|
ofTest = g_pixelDataElement->getUncompressedFrame(g_pDcmDataset, 1, startFragment, g_pBuf, g_nFrameSizeUncompressed, decompressedColorModel);
|
||||||
|
|
||||||
|
//printf("updateTextureRGB: %d, %d, %d, %d, %s\nerror: %s\n", g_nFrameWidth, g_nFrameHeight, startFragment, g_nFrameSizeUncompressed, decompressedColorModel.c_str(), ofTest.text());
|
||||||
|
|
||||||
|
updateTextureRGB(g_nFrameWidth, g_nFrameHeight, g_pBuf);
|
||||||
|
|
||||||
|
|
||||||
|
//printf("sizeF: %d\n", sizeF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if(callback_UpdateDcmImageComplete!=NULL)
|
||||||
|
{
|
||||||
|
printf("callback_UpdateDcmImageComplete\n");
|
||||||
|
callback_UpdateDcmImageComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
g_bChange = true;
|
||||||
|
//g_bChange = false;
|
||||||
|
|
||||||
|
//printf("load OK!\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
void initTextureGray16()
|
||||||
|
{
|
||||||
|
|
||||||
|
const Uint16* pImageData = NULL;
|
||||||
|
//g_pDcmDataset->findAndGetUint8Array(DCM_PixelData, pImageData);
|
||||||
|
g_pDcmDataset->findAndGetUint16Array(DCM_PixelData, pImageData);
|
||||||
|
//printf("PixelPointer: %x\n", pImageData);
|
||||||
|
|
||||||
|
|
||||||
|
int w = g_nFrameWidth;
|
||||||
|
int h = g_nFrameHeight;
|
||||||
|
glEnable(GL_BLEND);
|
||||||
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||||
|
|
||||||
|
//SDL_Surface* image = SDL_CreateRGBSurface(0, w, h, 24, 0, 0, 0, 0);
|
||||||
|
//memset(image->pixels, 0x1f, image->w * image->h * 3);
|
||||||
|
|
||||||
|
//
|
||||||
|
//Uint8* pData = new Uint8[1920*1080*4];
|
||||||
|
//memset(pData, 0xff, 1920*1080*4);
|
||||||
|
|
||||||
|
//if(image!=NULL)
|
||||||
|
{
|
||||||
|
glGenTextures(1, &textureObj);
|
||||||
|
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, textureObj);
|
||||||
|
|
||||||
|
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
|
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||||
|
|
||||||
|
|
||||||
|
PrintError();
|
||||||
|
// glTexImage2D(GL_TEXTURE_2D, 0, format, w, h, 0, format, GL_UNSIGNED_BYTE, pData);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE_ALPHA, w, h, 0, GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE, pImageData);
|
||||||
|
// glTexImage2D(GL_TEXTURE_2D, 0, GL_R16I, w, h, 0, GL_RED_INTEGER, GL_UNSIGNED_SHORT, pData16);
|
||||||
|
|
||||||
|
PrintError();
|
||||||
|
|
||||||
|
GLint nTextureID = glGetUniformLocation(g_shaderProgramGray16, "texSampler");
|
||||||
|
|
||||||
|
//printf("texSampler: %d\n", nTextureID);
|
||||||
|
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
|
||||||
|
glUniform1i(nTextureID, 0);
|
||||||
|
|
||||||
|
|
||||||
|
// glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// updateWindowWidthLevelGray16(g_nWindowWidth, g_nWindowCenter);
|
||||||
|
// glUniform1f(g_shaderWindowCenterGray16, (float)g_nWindowCenter);
|
||||||
|
// glUniform1f(g_shaderWindowWidthGray16, (float)g_nWindowWidth);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//printf("WindowCenter: %d, WindowWidth: %d\n", g_nWindowCenter, g_nWindowWidth);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//delete g_pDcmDataset;
|
||||||
|
|
||||||
|
//glUseProgram(g_shaderProgramGray16);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void initTextureRGB()
|
||||||
|
{
|
||||||
|
//printf("initTextureRGB\n");
|
||||||
|
const Uint8* pImageData = NULL;
|
||||||
|
g_pDcmDataset->findAndGetUint8Array(DCM_PixelData, pImageData);
|
||||||
|
//printf("PixelPointer: %x\n", pImageData);
|
||||||
|
|
||||||
|
|
||||||
|
int w = g_nFrameWidth;
|
||||||
|
int h = g_nFrameHeight;
|
||||||
|
glEnable(GL_BLEND);
|
||||||
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||||
|
|
||||||
|
GLint format = GL_RGB;
|
||||||
|
|
||||||
|
//printf("textureObj: %d\n", textureObj);
|
||||||
|
if(textureObj>=0)
|
||||||
|
{
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
|
||||||
|
glDeleteTextures(1, &textureObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
glGenTextures(1, &textureObj);
|
||||||
|
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, textureObj);
|
||||||
|
|
||||||
|
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
|
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||||
|
|
||||||
|
|
||||||
|
PrintError();
|
||||||
|
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, w, h, 0, GL_RGB, GL_UNSIGNED_BYTE, pImageData);
|
||||||
|
|
||||||
|
PrintError();
|
||||||
|
|
||||||
|
GLint nTextureID = glGetUniformLocation(g_shaderProgramRGB, "texSampler");
|
||||||
|
|
||||||
|
//printf("texSampler: %d\n", nTextureID);
|
||||||
|
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
|
||||||
|
glUniform1i(nTextureID, 0);
|
||||||
|
|
||||||
|
|
||||||
|
// updateWindowWidthLevelRGB(g_nWindowWidth, g_nWindowCenter);
|
||||||
|
|
||||||
|
|
||||||
|
// glUniform1f(g_shaderWindowCenterRGB, (float)g_nWindowCenter);
|
||||||
|
// glUniform1f(g_shaderWindowWidthRGB, (float)g_nWindowWidth);
|
||||||
|
|
||||||
|
// printf("WindowCenter: %d, WindowWidth: %d\n", g_nWindowCenter, g_nWindowWidth);
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
if(g_pDcmDataset!=NULL)
|
||||||
|
{
|
||||||
|
delete g_pDcmDataset;
|
||||||
|
g_pDcmDataset = NULL;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
void redraw(EventHandler& eventHandler)
|
||||||
|
{
|
||||||
|
if(g_bChange==true)
|
||||||
|
{
|
||||||
|
if(g_nColorType==0)
|
||||||
|
{
|
||||||
|
initGeometry(g_shaderProgramGray16);
|
||||||
|
glUseProgram(g_shaderProgramGray16);
|
||||||
|
initTextureGray16();
|
||||||
|
updateShader(*g_pEventHandler);
|
||||||
|
}
|
||||||
|
else if(g_nColorType==1)
|
||||||
|
{
|
||||||
|
initGeometry(g_shaderProgramRGB);
|
||||||
|
glUseProgram(g_shaderProgramRGB);
|
||||||
|
initTextureRGB();
|
||||||
|
updateShader(*g_pEventHandler);
|
||||||
|
}
|
||||||
|
g_bChange = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
//printf("g_nTotalFrames: %d\n", g_nTotalFrames);
|
||||||
|
|
||||||
|
if(g_nTotalFrames>0)
|
||||||
|
{
|
||||||
|
|
||||||
|
OFCondition ofTest;
|
||||||
|
OFString decompressedColorModel;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ofTest = g_pixelDataElement->getUncompressedFrame(g_pDcmDataset, 1, g_nCurrentFrame, g_pBuf, g_nFrameSizeUncompressed, decompressedColorModel);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if(g_nColorType==0)
|
||||||
|
{
|
||||||
|
printf("updateTextureRGB: %d\n", g_nCurrentFrame);
|
||||||
|
updateTextureGray16(g_nFrameWidth, g_nFrameHeight, g_pBuf);
|
||||||
|
}
|
||||||
|
else if(g_nColorType==1)
|
||||||
|
{
|
||||||
|
//printf("updateTextureRGB: %d, %d, %d, %d, %s\nerror: %s\n", g_nFrameWidth, g_nFrameHeight, g_nCurrentFrame, g_nFrameSizeUncompressed, decompressedColorModel.c_str(), ofTest.text());
|
||||||
|
updateTextureRGB(g_nFrameWidth, g_nFrameHeight, g_pBuf);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(g_nCurrentFrame>=g_nTotalFrames)
|
||||||
|
{
|
||||||
|
g_nCurrentFrame = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if(g_nColorType==0)
|
||||||
|
{
|
||||||
|
updateWindowWidthLevelGray16(g_nPrevWindowWidth, g_nPrevWindowCenter);
|
||||||
|
// glUniform1f(g_shaderWindowCenterGray16, (float)g_nPrevWindowCenter);
|
||||||
|
// glUniform1f(g_shaderWindowWidthGray16, (float)g_nPrevWindowWidth);
|
||||||
|
}
|
||||||
|
else if(g_nColorType==1)
|
||||||
|
{
|
||||||
|
updateWindowWidthLevelRGB(g_nPrevWindowWidth, g_nPrevWindowCenter);
|
||||||
|
// glUniform1f(g_shaderWindowCenterRGB, (float)g_nPrevWindowCenter);
|
||||||
|
// glUniform1f(g_shaderWindowWidthRGB, (float)g_nPrevWindowWidth);
|
||||||
|
|
||||||
|
}
|
||||||
|
// Clear screen
|
||||||
|
glClearColor(0.0, 0.0, 0.0, 1.0);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
|
||||||
|
//glUseProgram(g_shaderProgramGray16);
|
||||||
|
|
||||||
|
// glActiveTexture(GL_TEXTURE0);
|
||||||
|
// glBindTexture(GL_TEXTURE_2D, textureObj);
|
||||||
|
|
||||||
|
//glBindBuffer(GL_ARRAY_BUFFER, g_vbo);
|
||||||
|
|
||||||
|
// Draw the vertex buffer
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||||
|
//glDrawArrays(GL_QUADS, 0, 4);
|
||||||
|
|
||||||
|
// Swap front/back framebuffers
|
||||||
|
eventHandler.swapWindow();
|
||||||
|
|
||||||
|
//printf("redraw\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
void mainLoop(void* mainLoopArg)
|
||||||
|
{
|
||||||
|
EventHandler& eventHandler = *((EventHandler*)mainLoopArg);
|
||||||
|
eventHandler.processEvents();
|
||||||
|
|
||||||
|
int nType = eventHandler.GetEventType();
|
||||||
|
if(nType==1 || nType==2)
|
||||||
|
{
|
||||||
|
float fDeltaX = eventHandler.GetDeltaX();
|
||||||
|
float fDeltaY = eventHandler.GetDeltaY();
|
||||||
|
|
||||||
|
|
||||||
|
g_nPrevWindowCenter = g_nWindowCenter+fDeltaY;
|
||||||
|
g_nPrevWindowWidth = g_nWindowWidth + fDeltaX;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//printf("DeltaX: %f, DeltaY:%f, (%d, %d)\n", fDeltaX, fDeltaY, g_nPrevWindowCenter, g_nPrevWindowWidth);
|
||||||
|
|
||||||
|
// glUniform1f(g_shaderWindowCenterGray16, (float)g_nPrevWindowCenter);
|
||||||
|
// glUniform1f(g_shaderWindowWidthGray16, (float)g_nPrevWindowWidth);
|
||||||
|
//
|
||||||
|
|
||||||
|
}
|
||||||
|
else if(nType==3 || nType==4)
|
||||||
|
{
|
||||||
|
g_nWindowCenter = g_nPrevWindowCenter;
|
||||||
|
g_nWindowWidth = g_nPrevWindowWidth;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// glUniform1f(g_shaderWindowCenterGray16, (float)g_nPrevWindowCenter);
|
||||||
|
// glUniform1f(g_shaderWindowWidthGray16, (float)g_nPrevWindowWidth);
|
||||||
|
|
||||||
|
// Update shader if camera changed
|
||||||
|
if (eventHandler.camera().updated())
|
||||||
|
updateShader(eventHandler);
|
||||||
|
|
||||||
|
redraw(eventHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
#define EXTERN extern "C"
|
||||||
|
#else
|
||||||
|
#define EXTERN
|
||||||
|
#endif
|
||||||
|
|
||||||
|
EXTERN EMSCRIPTEN_KEEPALIVE int UpdateDcmImage(std::string strFile)
|
||||||
|
{
|
||||||
|
printf("UpdateDcmImage: %s\n", strFile.c_str());
|
||||||
|
|
||||||
|
emscripten_async_wget_data(strFile.c_str(), (void*)135, onLoadedData, onErrorData);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
EXTERN EMSCRIPTEN_KEEPALIVE int SetWindowWidthLevel(int nWindowCenter, int nWindowWidth)
|
||||||
|
{
|
||||||
|
printf("New nWindowCenter:%d, nWindowWidth:%d\n", nWindowCenter, nWindowWidth);
|
||||||
|
|
||||||
|
g_nWindowCenter = nWindowCenter;
|
||||||
|
g_nWindowWidth = nWindowWidth;
|
||||||
|
|
||||||
|
g_nPrevWindowCenter = g_nWindowCenter;
|
||||||
|
g_nPrevWindowWidth = g_nWindowWidth;
|
||||||
|
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
EXTERN EMSCRIPTEN_KEEPALIVE int GetWindowWidth()
|
||||||
|
{
|
||||||
|
return g_nWindowWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
EXTERN EMSCRIPTEN_KEEPALIVE int GetWindowCenter()
|
||||||
|
{
|
||||||
|
return g_nWindowCenter;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
EXTERN EMSCRIPTEN_KEEPALIVE bool SetCallbackUpdateDcmImageComplete(callback_UpdateDcmImage callback_)
|
||||||
|
{
|
||||||
|
printf("SetCallbackUpdateDcmImage: %x\n", callback_);
|
||||||
|
|
||||||
|
callback_UpdateDcmImageComplete = callback_;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
int main(int argc, char** argv)
|
||||||
|
{
|
||||||
|
EventHandler eventHandler("dcm image viewer");
|
||||||
|
|
||||||
|
// Initialize shader and geometry
|
||||||
|
GLuint shaderProgram = initShader(eventHandler);
|
||||||
|
|
||||||
|
//initTexture();
|
||||||
|
|
||||||
|
// Start the main loop
|
||||||
|
void* mainLoopArg = &eventHandler;
|
||||||
|
|
||||||
|
|
||||||
|
DJDecoderRegistration::registerCodecs();
|
||||||
|
DJLSDecoderRegistration::registerCodecs();
|
||||||
|
DcmRLEDecoderRegistration::registerCodecs();
|
||||||
|
|
||||||
|
//emscripten_async_wget_data("http://49.171.226.18:1901/3.dcm", (void*)135, onLoadedData, onErrorData);
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
int fps = 0; // Use browser's requestAnimationFrame
|
||||||
|
emscripten_set_main_loop_arg(mainLoop, mainLoopArg, fps, true);
|
||||||
|
#else
|
||||||
|
while(true)
|
||||||
|
mainLoop(mainLoopArg);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
glDeleteTextures(1, &textureObj);
|
||||||
|
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
|
||||||
|
DJDecoderRegistration::cleanup();
|
||||||
|
DJLSDecoderRegistration::cleanup();
|
||||||
|
DcmRLEDecoderRegistration::cleanup();
|
||||||
|
|
||||||
|
if(g_pDcmDataset!=NULL)
|
||||||
|
{
|
||||||
|
delete g_pDcmDataset;
|
||||||
|
g_pDcmDataset = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("exit!!!!! c++ dcm_image_mod");
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
16
webassembly/dcm_mod.js
Normal file
16
webassembly/dcm_mod.js
Normal file
File diff suppressed because one or more lines are too long
405
webassembly/events.cpp
Normal file
405
webassembly/events.cpp
Normal file
@ -0,0 +1,405 @@
|
|||||||
|
//
|
||||||
|
// Window and input event handling
|
||||||
|
//
|
||||||
|
#include <algorithm>
|
||||||
|
#include <SDL.h>
|
||||||
|
#include <SDL_opengles2.h>
|
||||||
|
#include "events.h"
|
||||||
|
|
||||||
|
// #define EVENTS_DEBUG
|
||||||
|
|
||||||
|
void EventHandler::windowResizeEvent(int width, int height)
|
||||||
|
{
|
||||||
|
if(width<=0 || height<=0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("windowResizeEvnet: %d, %d\n", width, height);
|
||||||
|
glViewport(0, 0, width, height);
|
||||||
|
mCamera.setWindowSize(width, height);
|
||||||
|
|
||||||
|
m_InteractionMode = (int)MODE_PAN;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Renderer* EventHandler::GetRenderer()
|
||||||
|
{
|
||||||
|
return m_pRenderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventHandler::SetInteractionMode(int mode)
|
||||||
|
{
|
||||||
|
m_InteractionMode = mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventHandler::initWindow(const char* title)
|
||||||
|
{
|
||||||
|
// Create SDL window
|
||||||
|
/*
|
||||||
|
mpWindow =
|
||||||
|
SDL_CreateWindow(title,
|
||||||
|
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
|
||||||
|
mCamera.windowSize().width, mCamera.windowSize().height,
|
||||||
|
SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE| SDL_WINDOW_SHOWN);
|
||||||
|
*/
|
||||||
|
|
||||||
|
SDL_CreateWindowAndRenderer(mCamera.windowSize().width, mCamera.windowSize().height,
|
||||||
|
SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE| SDL_WINDOW_SHOWN,
|
||||||
|
&mpWindow, &m_pRenderer);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
mWindowID = SDL_GetWindowID(mpWindow);
|
||||||
|
|
||||||
|
printf("Window Created!!!! Width:%d, Height: %d\n", mCamera.windowSize().width, mCamera.windowSize().height);
|
||||||
|
|
||||||
|
// Create OpenGLES 2 context on SDL window
|
||||||
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
|
||||||
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
|
||||||
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
|
||||||
|
SDL_GL_SetSwapInterval(1);
|
||||||
|
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
|
||||||
|
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
|
||||||
|
SDL_GLContext glc = SDL_GL_CreateContext(mpWindow);
|
||||||
|
|
||||||
|
// Set clear color to black
|
||||||
|
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
|
||||||
|
|
||||||
|
// Initialize viewport
|
||||||
|
windowResizeEvent(mCamera.windowSize().width, mCamera.windowSize().height);
|
||||||
|
|
||||||
|
m_nEventType = 0;
|
||||||
|
|
||||||
|
m_fDeltaX = 0;
|
||||||
|
m_fDeltaY = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventHandler::swapWindow()
|
||||||
|
{
|
||||||
|
SDL_GL_SwapWindow(mpWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventHandler::zoom(float fDelta)
|
||||||
|
{
|
||||||
|
//float zoomDelta = mouseWheelDown ? -cMouseWheelZoomDelta : cMouseWheelZoomDelta;
|
||||||
|
|
||||||
|
float zoomDelta = 0.0f;
|
||||||
|
|
||||||
|
if(fDelta>0)
|
||||||
|
{
|
||||||
|
zoomDelta = -cMouseWheelZoomDelta;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
zoomDelta = cMouseWheelZoomDelta;
|
||||||
|
}
|
||||||
|
mCamera.setZoomDelta(zoomDelta);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventHandler::zoomEventMouse(bool mouseWheelDown, int x, int y)
|
||||||
|
{
|
||||||
|
float preZoomWorldX, preZoomWorldY;
|
||||||
|
mCamera.windowToWorldCoords(mMousePositionX, mMousePositionY, preZoomWorldX, preZoomWorldY);
|
||||||
|
|
||||||
|
// Zoom by scaling up/down in 0.05 increments
|
||||||
|
float zoomDelta = mouseWheelDown ? -cMouseWheelZoomDelta : cMouseWheelZoomDelta;
|
||||||
|
mCamera.setZoomDelta(zoomDelta);
|
||||||
|
|
||||||
|
// Zoom to point: Keep the world coords under mouse position the same before and after the zoom
|
||||||
|
float postZoomWorldX, postZoomWorldY;
|
||||||
|
mCamera.windowToWorldCoords(mMousePositionX, mMousePositionY, postZoomWorldX, postZoomWorldY);
|
||||||
|
Vec2 deltaWorld = { postZoomWorldX - preZoomWorldX, postZoomWorldY - preZoomWorldY };
|
||||||
|
mCamera.setPanDelta (deltaWorld);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventHandler::zoomEventPinch (float pinchDist, float pinchX, float pinchY)
|
||||||
|
{
|
||||||
|
float preZoomWorldX, preZoomWorldY;
|
||||||
|
mCamera.normWindowToWorldCoords(pinchX, pinchY, preZoomWorldX, preZoomWorldY);
|
||||||
|
|
||||||
|
// Zoom in/out by positive/negative mPinch distance
|
||||||
|
float zoomDelta = pinchDist * cPinchScale;
|
||||||
|
mCamera.setZoomDelta(zoomDelta);
|
||||||
|
|
||||||
|
// Zoom to point: Keep the world coords under pinch position the same before and after the zoom
|
||||||
|
float postZoomWorldX, postZoomWorldY;
|
||||||
|
mCamera.normWindowToWorldCoords(pinchX, pinchY, postZoomWorldX, postZoomWorldY);
|
||||||
|
Vec2 deltaWorld = { postZoomWorldX - preZoomWorldX, postZoomWorldY - preZoomWorldY };
|
||||||
|
mCamera.setPanDelta (deltaWorld);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventHandler::SetMouseButtonDownPosition(int x, int y)
|
||||||
|
{
|
||||||
|
mMouseButtonDownX = x;
|
||||||
|
mMouseButtonDownY = y;
|
||||||
|
|
||||||
|
mMouseButtonDown = true;
|
||||||
|
|
||||||
|
mCamera.setBasePan();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventHandler::SetMouseButtonUpPosition(int x, int y)
|
||||||
|
{
|
||||||
|
m_nEventType = 3;
|
||||||
|
|
||||||
|
mMouseButtonDown = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventHandler::panEventMouse(int x, int y)
|
||||||
|
{
|
||||||
|
m_nEventType = 1;
|
||||||
|
|
||||||
|
int deltaX = mCamera.windowSize().width / 2 + (x - mMouseButtonDownX),
|
||||||
|
deltaY = mCamera.windowSize().height / 2 + (y - mMouseButtonDownY);
|
||||||
|
|
||||||
|
if(m_InteractionMode&InteractionMode::MODE_WIDTHLEVEL)
|
||||||
|
{
|
||||||
|
m_fDeltaX = x - mMouseButtonDownX;
|
||||||
|
m_fDeltaY = y - mMouseButtonDownY;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
float deviceX, deviceY;
|
||||||
|
mCamera.windowToDeviceCoords(deltaX, deltaY, deviceX, deviceY);
|
||||||
|
|
||||||
|
|
||||||
|
Vec2 pan = { mCamera.basePan().x + deviceX / mCamera.zoom(),
|
||||||
|
mCamera.basePan().y + deviceY / mCamera.zoom() / mCamera.aspect() };
|
||||||
|
mCamera.setPan(pan);
|
||||||
|
/*
|
||||||
|
int deltaX = mCamera.windowSize().width / 2 + (x - mMouseButtonDownX),
|
||||||
|
deltaY = mCamera.windowSize().height / 2 + (y - mMouseButtonDownY);
|
||||||
|
|
||||||
|
m_nEventType = 1;
|
||||||
|
m_fDeltaX = x - mMouseButtonDownX;
|
||||||
|
m_fDeltaY = y - mMouseButtonDownY;
|
||||||
|
|
||||||
|
return;
|
||||||
|
float deviceX, deviceY;
|
||||||
|
mCamera.windowToDeviceCoords(deltaX, deltaY, deviceX, deviceY);
|
||||||
|
|
||||||
|
Vec2 pan = { mCamera.basePan().x + deviceX / mCamera.zoom(),
|
||||||
|
mCamera.basePan().y + deviceY / mCamera.zoom() / mCamera.aspect() };
|
||||||
|
mCamera.setPan(pan);
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventHandler::panEventFinger(float x, float y)
|
||||||
|
{
|
||||||
|
m_nEventType = 2;
|
||||||
|
|
||||||
|
if(m_InteractionMode&InteractionMode::MODE_WIDTHLEVEL)
|
||||||
|
{
|
||||||
|
int nWidth = mCamera.windowSize().width;
|
||||||
|
int nHeight = mCamera.windowSize().height;
|
||||||
|
m_fDeltaX = (x - mFingerDownX)*nWidth;
|
||||||
|
m_fDeltaY = (y - mFingerDownY)*nHeight;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
float deltaX = 0.5f + (x - mFingerDownX),
|
||||||
|
deltaY = 0.5f + (y - mFingerDownY);
|
||||||
|
|
||||||
|
float deviceX, deviceY;
|
||||||
|
mCamera.normWindowToDeviceCoords(deltaX, deltaY, deviceX, deviceY);
|
||||||
|
|
||||||
|
Vec2 pan = { mCamera.basePan().x + deviceX / mCamera.zoom(),
|
||||||
|
mCamera.basePan().y + deviceY / mCamera.zoom() / mCamera.aspect() };
|
||||||
|
mCamera.setPan(pan);
|
||||||
|
|
||||||
|
/*
|
||||||
|
float deltaX = 0.5f + (x - mFingerDownX),
|
||||||
|
deltaY = 0.5f + (y - mFingerDownY);
|
||||||
|
|
||||||
|
m_nEventType = 2;
|
||||||
|
|
||||||
|
int nWidth = mCamera.windowSize().width;
|
||||||
|
int nHeight = mCamera.windowSize().height;
|
||||||
|
m_fDeltaX = (x - mFingerDownX)*nWidth;
|
||||||
|
m_fDeltaY = (y - mFingerDownY)*nHeight;
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
float deviceX, deviceY;
|
||||||
|
mCamera.normWindowToDeviceCoords(deltaX, deltaY, deviceX, deviceY);
|
||||||
|
|
||||||
|
Vec2 pan = { mCamera.basePan().x + deviceX / mCamera.zoom(),
|
||||||
|
mCamera.basePan().y + deviceY / mCamera.zoom() / mCamera.aspect() };
|
||||||
|
mCamera.setPan(pan);
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
int EventHandler::GetEventType()
|
||||||
|
{
|
||||||
|
return m_nEventType;
|
||||||
|
}
|
||||||
|
|
||||||
|
float EventHandler::GetDeltaX()
|
||||||
|
{
|
||||||
|
return m_fDeltaX;
|
||||||
|
}
|
||||||
|
|
||||||
|
float EventHandler::GetDeltaY()
|
||||||
|
{
|
||||||
|
return m_fDeltaY;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EventHandler::processEvents()
|
||||||
|
{
|
||||||
|
// Handle events
|
||||||
|
SDL_Event event;
|
||||||
|
while (SDL_PollEvent(&event))
|
||||||
|
{
|
||||||
|
switch (event.type)
|
||||||
|
{
|
||||||
|
case SDL_QUIT:
|
||||||
|
std::terminate();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDL_WINDOWEVENT:
|
||||||
|
{
|
||||||
|
if (event.window.windowID == mWindowID
|
||||||
|
&& event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED)
|
||||||
|
{
|
||||||
|
int width = event.window.data1, height = event.window.data2;
|
||||||
|
windowResizeEvent(width, height);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case SDL_MOUSEWHEEL:
|
||||||
|
{
|
||||||
|
// SDL_MOUSEWHEEL regression?
|
||||||
|
// m->y no longer reliable (often y is 0 when mouse wheel is spun up or down), use m->preciseY instead
|
||||||
|
SDL_MouseWheelEvent *m = (SDL_MouseWheelEvent*)&event;
|
||||||
|
#ifdef EVENTS_DEBUG
|
||||||
|
printf ("SDL_MOUSEWHEEL= x,y=%d,%d preciseX,preciseY=%f,%f\n", m->x, m->y, m->preciseX, m->preciseY);
|
||||||
|
#endif
|
||||||
|
bool mouseWheelDown = (m->preciseY < 0.0);
|
||||||
|
|
||||||
|
if(m_InteractionMode&InteractionMode::MODE_SCROLL)
|
||||||
|
{
|
||||||
|
zoomEventMouse(mouseWheelDown, mMousePositionX, mMousePositionY);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case SDL_MOUSEMOTION:
|
||||||
|
{
|
||||||
|
SDL_MouseMotionEvent *m = (SDL_MouseMotionEvent*)&event;
|
||||||
|
mMousePositionX = m->x;
|
||||||
|
mMousePositionY = m->y;
|
||||||
|
if (mMouseButtonDown && !mFingerDown && !mPinch)
|
||||||
|
panEventMouse(mMousePositionX, mMousePositionY);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case SDL_MOUSEBUTTONDOWN:
|
||||||
|
{
|
||||||
|
SDL_MouseButtonEvent *m = (SDL_MouseButtonEvent*)&event;
|
||||||
|
if (m->button == SDL_BUTTON_LEFT && !mFingerDown && !mPinch)
|
||||||
|
{
|
||||||
|
mMouseButtonDown = true;
|
||||||
|
mMouseButtonDownX = m->x;
|
||||||
|
mMouseButtonDownY = m->y;
|
||||||
|
mCamera.setBasePan();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m->button == SDL_BUTTON_RIGHT && !mFingerDown && !mPinch)
|
||||||
|
{
|
||||||
|
printf("C++: 오른쪽 마우스 버튼이 눌렸습니다! 좌표: x=%d, y=%d\n", m->x, m->y);
|
||||||
|
|
||||||
|
//mMouseButtonDown = true;
|
||||||
|
//mMouseButtonDownX = m->x;
|
||||||
|
//mMouseButtonDownY = m->y;
|
||||||
|
//mCamera.setBasePan();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case SDL_MOUSEBUTTONUP:
|
||||||
|
{
|
||||||
|
SDL_MouseButtonEvent *m = (SDL_MouseButtonEvent*)&event;
|
||||||
|
if (m->button == SDL_BUTTON_LEFT)
|
||||||
|
{
|
||||||
|
mMouseButtonDown = false;
|
||||||
|
|
||||||
|
m_nEventType = 3;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case SDL_FINGERMOTION:
|
||||||
|
if (mFingerDown)
|
||||||
|
{
|
||||||
|
SDL_TouchFingerEvent *m = (SDL_TouchFingerEvent*)&event;
|
||||||
|
|
||||||
|
// Finger down and finger moving must match
|
||||||
|
if (m->fingerId == mFingerDownId)
|
||||||
|
panEventFinger(m->x, m->y);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDL_FINGERDOWN:
|
||||||
|
if (!mPinch)
|
||||||
|
{
|
||||||
|
// Finger already down means multiple fingers, which is handled by multigesture event
|
||||||
|
if (mFingerDown)
|
||||||
|
mFingerDown = false;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SDL_TouchFingerEvent *m = (SDL_TouchFingerEvent*)&event;
|
||||||
|
|
||||||
|
mFingerDown = true;
|
||||||
|
mFingerDownX = m->x;
|
||||||
|
mFingerDownY = m->y;
|
||||||
|
mFingerDownId = m->fingerId;
|
||||||
|
mCamera.setBasePan();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDL_MULTIGESTURE:
|
||||||
|
{
|
||||||
|
SDL_MultiGestureEvent *m = (SDL_MultiGestureEvent*)&event;
|
||||||
|
if (m->numFingers == 2 && fabs(m->dDist) >= cPinchZoomThreshold)
|
||||||
|
{
|
||||||
|
mPinch = true;
|
||||||
|
mFingerDown = false;
|
||||||
|
mMouseButtonDown = false;
|
||||||
|
|
||||||
|
if(m_InteractionMode&InteractionMode::MODE_SCROLL)
|
||||||
|
{
|
||||||
|
zoomEventPinch(m->dDist, m->x, m->y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case SDL_FINGERUP:
|
||||||
|
m_nEventType = 4;
|
||||||
|
mFingerDown = false;
|
||||||
|
mPinch = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef EVENTS_DEBUG
|
||||||
|
printf ("event=%d mousePos=%d,%d mouseButtonDown=%d fingerDown=%d pinch=%d aspect=%f window=%dx%d\n",
|
||||||
|
event.type, mMousePositionX, mMousePositionY, mMouseButtonDown, mFingerDown, mPinch, mCamera.aspect(), mCamera.windowSize().width, mCamera.windowSize().height);
|
||||||
|
printf (" zoom=%f pan=%f,%f\n", mCamera.zoom(), mCamera.pan()[0], mCamera.pan()[1]);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
97
webassembly/events.h
Normal file
97
webassembly/events.h
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
//
|
||||||
|
// Window and input event handling
|
||||||
|
//
|
||||||
|
#include "camera.h"
|
||||||
|
|
||||||
|
typedef enum _InteractionMode {
|
||||||
|
MODE_PAN = 0x01,
|
||||||
|
MODE_WIDTHLEVEL = 0x02,
|
||||||
|
MODE_ZOOM = 0x04,
|
||||||
|
MODE_SCROLL = 0x08,
|
||||||
|
}InteractionMode;
|
||||||
|
|
||||||
|
|
||||||
|
class EventHandler
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
EventHandler(const char* windowTitle);
|
||||||
|
|
||||||
|
void processEvents();
|
||||||
|
Camera& camera() { return mCamera; }
|
||||||
|
void swapWindow();
|
||||||
|
|
||||||
|
int GetEventType();
|
||||||
|
|
||||||
|
float GetDeltaX();
|
||||||
|
float GetDeltaY();
|
||||||
|
|
||||||
|
SDL_Renderer* GetRenderer();
|
||||||
|
|
||||||
|
void SetInteractionMode(int mode);
|
||||||
|
|
||||||
|
void SetMouseButtonDownPosition(int x, int y);
|
||||||
|
void SetMouseButtonUpPosition(int x, int y);
|
||||||
|
|
||||||
|
|
||||||
|
public:
|
||||||
|
float m_fDeltaX;
|
||||||
|
float m_fDeltaY;
|
||||||
|
int m_nEventType;
|
||||||
|
int m_InteractionMode;
|
||||||
|
// Camera
|
||||||
|
Camera mCamera;
|
||||||
|
|
||||||
|
SDL_Renderer* m_pRenderer;
|
||||||
|
// Window
|
||||||
|
SDL_Window* mpWindow;
|
||||||
|
Uint32 mWindowID;
|
||||||
|
void windowResizeEvent(int width, int height);
|
||||||
|
void initWindow(const char* title);
|
||||||
|
|
||||||
|
// Mouse input
|
||||||
|
const float cMouseWheelZoomDelta;
|
||||||
|
bool mMouseButtonDown;
|
||||||
|
int mMouseButtonDownX, mMouseButtonDownY;
|
||||||
|
int mMousePositionX, mMousePositionY;
|
||||||
|
|
||||||
|
// Finger input
|
||||||
|
bool mFingerDown;
|
||||||
|
float mFingerDownX, mFingerDownY;
|
||||||
|
long long mFingerDownId;
|
||||||
|
|
||||||
|
// Pinch input
|
||||||
|
const float cPinchZoomThreshold, cPinchScale;
|
||||||
|
bool mPinch;
|
||||||
|
|
||||||
|
void zoom(float fDelta);
|
||||||
|
|
||||||
|
// Events
|
||||||
|
void zoomEventMouse(bool mouseWheelDown, int x, int y);
|
||||||
|
void zoomEventPinch (float pinchDist, float pinchX, float pinchY);
|
||||||
|
void panEventMouse(int x, int y);
|
||||||
|
void panEventFinger(float x, float y);
|
||||||
|
};
|
||||||
|
|
||||||
|
inline EventHandler::EventHandler(const char* windowTitle)
|
||||||
|
// Window
|
||||||
|
: mpWindow (nullptr)
|
||||||
|
, mWindowID (0)
|
||||||
|
|
||||||
|
// Mouse input
|
||||||
|
, cMouseWheelZoomDelta (0.05f)
|
||||||
|
, mMouseButtonDown (false)
|
||||||
|
, mMouseButtonDownX (0), mMouseButtonDownY (0)
|
||||||
|
, mMousePositionX (0), mMousePositionY (0)
|
||||||
|
|
||||||
|
// Finger input
|
||||||
|
, mFingerDown (false)
|
||||||
|
, mFingerDownX (0.0f), mFingerDownY (0.0f)
|
||||||
|
, mFingerDownId (0)
|
||||||
|
|
||||||
|
// Pinch input
|
||||||
|
, cPinchZoomThreshold (0.001f)
|
||||||
|
, cPinchScale (8.0f)
|
||||||
|
, mPinch (false)
|
||||||
|
{
|
||||||
|
initWindow(windowTitle);
|
||||||
|
}
|
||||||
719
webassembly/ffmpeg_mod.cc
Normal file
719
webassembly/ffmpeg_mod.cc
Normal file
@ -0,0 +1,719 @@
|
|||||||
|
//
|
||||||
|
// Emscripten/SDL2/OpenGLES2 sample that demonstrates simple geometry and shaders, mouse and touch input, and window resizing
|
||||||
|
//
|
||||||
|
// Setup:
|
||||||
|
// Install emscripten: http://kripken.github.io/emscripten-site/docs/getting_started/downloads.html
|
||||||
|
//
|
||||||
|
// Build:
|
||||||
|
// emcc -std=c++11 hello_triangle.cpp events.cpp camera.cpp -s USE_SDL=2 -s FULL_ES2=1 -s WASM=0 -o hello_triangle.html
|
||||||
|
//
|
||||||
|
// Run:
|
||||||
|
// emrun hello_triangle.html
|
||||||
|
//
|
||||||
|
// Result:
|
||||||
|
// A colorful triangle. Left mouse pans, mouse wheel zooms in/out. Window is resizable.
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
#include <emscripten.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <SDL2/SDL.h>
|
||||||
|
#include <SDL2/SDL_opengles2.h>
|
||||||
|
//#include <SDL2/SDL_opengles2_gl2ext.h>
|
||||||
|
//#include <GLES3/gl3.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
|
||||||
|
#include "events.h"
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include "libavutil/avstring.h"
|
||||||
|
#include "libavutil/channel_layout.h"
|
||||||
|
#include "libavutil/eval.h"
|
||||||
|
#include "libavutil/mathematics.h"
|
||||||
|
#include "libavutil/pixdesc.h"
|
||||||
|
#include "libavutil/imgutils.h"
|
||||||
|
#include "libavutil/dict.h"
|
||||||
|
#include "libavutil/fifo.h"
|
||||||
|
#include "libavutil/parseutils.h"
|
||||||
|
#include "libavutil/samplefmt.h"
|
||||||
|
#include "libavutil/time.h"
|
||||||
|
#include "libavutil/bprint.h"
|
||||||
|
#include "libavformat/avformat.h"
|
||||||
|
#include "libavdevice/avdevice.h"
|
||||||
|
#include "libswscale/swscale.h"
|
||||||
|
#include "libavutil/opt.h"
|
||||||
|
#include "libavcodec/avfft.h"
|
||||||
|
#include "libavcodec/avcodec.h"
|
||||||
|
#include "libswresample/swresample.h"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef void (*callback_UpdateDcmImage) ();
|
||||||
|
callback_UpdateDcmImage callback_UpdateDcmImageComplete = NULL;
|
||||||
|
|
||||||
|
|
||||||
|
uint8_t* g_pImageData = NULL;
|
||||||
|
|
||||||
|
SwsContext *img_convert_ctx = NULL;
|
||||||
|
AVFormatContext* format_ctx = NULL;
|
||||||
|
AVCodecContext* codec_ctx = NULL;
|
||||||
|
int video_stream_index = -1;
|
||||||
|
|
||||||
|
std::string g_strFilename = "";
|
||||||
|
bool g_bLoadFile = false;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
bool g_bChange = false;
|
||||||
|
|
||||||
|
EventHandler* g_pEventHandler = NULL;
|
||||||
|
|
||||||
|
GLuint textureObj = 0;
|
||||||
|
|
||||||
|
GLuint g_shaderProgramGray16 = 0;
|
||||||
|
GLuint g_shaderProgramRGB = 0;
|
||||||
|
|
||||||
|
// Vertex shader
|
||||||
|
GLint g_shaderPanGray16, g_shaderZoomGray16, g_shaderAspectGray16, g_shaderWindowCenterGray16, g_shaderWindowWidthGray16;
|
||||||
|
GLint g_shaderPanRGB, g_shaderZoomRGB, g_shaderAspectRGB, g_shaderWindowCenterRGB, g_shaderWindowWidthRGB;
|
||||||
|
|
||||||
|
int g_nColorType = 1;
|
||||||
|
|
||||||
|
int g_nWindowCenter = 0;
|
||||||
|
int g_nWindowWidth = 0;
|
||||||
|
|
||||||
|
int g_nPrevWindowCenter = 0;
|
||||||
|
int g_nPrevWindowWidth = 0;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Uint32 g_nCurrentFrame = 0;
|
||||||
|
int g_nTotalFrames = 0;
|
||||||
|
|
||||||
|
int g_nFrameWidth = 0;
|
||||||
|
int g_nFrameHeight = 0;
|
||||||
|
|
||||||
|
int g_nSamplesPerPixel = 0;
|
||||||
|
int g_nBitsAllocated = 0;
|
||||||
|
|
||||||
|
int g_nFrameSizeUncompressed = 0;
|
||||||
|
|
||||||
|
const GLchar* vertexSourceGray16 =
|
||||||
|
"uniform vec2 pan; \n"
|
||||||
|
"uniform float zoom; \n"
|
||||||
|
"uniform float aspect; \n"
|
||||||
|
"attribute vec4 position; \n"
|
||||||
|
"attribute vec2 a_texCoord; \n"
|
||||||
|
"varying vec2 texCoord; \n"
|
||||||
|
"void main() \n"
|
||||||
|
"{ \n"
|
||||||
|
" gl_Position = vec4(position.xyz, 1.0); \n"
|
||||||
|
" gl_Position.xy += pan; \n"
|
||||||
|
" gl_Position.xy *= zoom; \n"
|
||||||
|
" texCoord = a_texCoord; \n"
|
||||||
|
" gl_Position.y *= aspect; \n"
|
||||||
|
"} \n";
|
||||||
|
|
||||||
|
|
||||||
|
// Fragment/pixel shader
|
||||||
|
const GLchar* fragmentSourceGray16 =
|
||||||
|
"precision mediump float; \n"
|
||||||
|
"varying vec2 texCoord; \n"
|
||||||
|
"uniform sampler2D texSampler; \n"
|
||||||
|
"uniform float fWindowCenter; \n"
|
||||||
|
"uniform float fWindowWidth; \n"
|
||||||
|
"void main() \n"
|
||||||
|
"{ \n"
|
||||||
|
" vec4 colorOut = texture2D(texSampler, texCoord); \n"
|
||||||
|
" float fMin = fWindowCenter - fWindowWidth/2.0;\n"
|
||||||
|
" float fMax = fWindowCenter + fWindowWidth/2.0;\n"
|
||||||
|
" float fData = ( ((colorOut.a + colorOut.r*256.0)*256.0) - fMin)/(fMax-fMin); \n"
|
||||||
|
" fData = ( ((colorOut.a + colorOut.a*256.0)*256.0) - fMin)/(fMax-fMin); \n"
|
||||||
|
" fData = colorOut.r;\n"
|
||||||
|
" fData = (((colorOut.a * 256.0 + colorOut.r)*256.0) - fMin) / (fMax-fMin);"
|
||||||
|
" float fTmpData = fMin;\n"
|
||||||
|
" gl_FragColor = vec4(fData, fData, fData, 1.0); \n"
|
||||||
|
|
||||||
|
"} \n";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const GLchar* vertexSourceRGB =
|
||||||
|
"uniform vec2 panRGB; \n"
|
||||||
|
"uniform float zoomRGB; \n"
|
||||||
|
"uniform float aspectRGB; \n"
|
||||||
|
"attribute vec4 position; \n"
|
||||||
|
"attribute vec2 a_texCoord; \n"
|
||||||
|
"varying vec2 texCoord; \n"
|
||||||
|
"void main() \n"
|
||||||
|
"{ \n"
|
||||||
|
" gl_Position = vec4(position.xyz, 1.0); \n"
|
||||||
|
" gl_Position.xy += panRGB; \n"
|
||||||
|
" gl_Position.xy *= zoomRGB; \n"
|
||||||
|
" texCoord = a_texCoord; \n"
|
||||||
|
" gl_Position.y *= aspectRGB; \n"
|
||||||
|
"} \n";
|
||||||
|
|
||||||
|
|
||||||
|
// Fragment/pixel shader
|
||||||
|
const GLchar* fragmentSourceRGB =
|
||||||
|
"precision mediump float; \n"
|
||||||
|
"varying vec2 texCoord; \n"
|
||||||
|
"uniform sampler2D texSampler; \n"
|
||||||
|
"uniform float fWindowCenterRGB; \n"
|
||||||
|
"uniform float fWindowWidthRGB; \n"
|
||||||
|
"void main() \n"
|
||||||
|
"{ \n"
|
||||||
|
" vec4 colorOut = texture2D(texSampler, texCoord); \n"
|
||||||
|
" float fMin = fWindowCenterRGB - fWindowWidthRGB/2.0;\n"
|
||||||
|
" float fMax = fWindowCenterRGB + fWindowWidthRGB/2.0;\n"
|
||||||
|
" float fTestValue = (fMax - fMin);\n"
|
||||||
|
" float fR = (colorOut.r*256.0 - fMin) / (fMax-fMin);\n"
|
||||||
|
" float fG = (colorOut.g*256.0 - fMin) / (fMax-fMin);\n"
|
||||||
|
" float fB = (colorOut.b*256.0 - fMin) / (fMax-fMin);\n"
|
||||||
|
" gl_FragColor = vec4(fR, fG, fB, 1.0); \n"
|
||||||
|
//" gl_FragColor = vec4(colorOut.r/fTestValue, 1.0, 1.0, 1.0); \n"
|
||||||
|
|
||||||
|
"} \n";
|
||||||
|
|
||||||
|
void updateShader(EventHandler& eventHandler)
|
||||||
|
{
|
||||||
|
Camera& camera = eventHandler.camera();
|
||||||
|
|
||||||
|
printf("updateShader: g_nColorType=%d\n", g_nColorType);
|
||||||
|
|
||||||
|
|
||||||
|
if(g_nColorType==0)
|
||||||
|
{
|
||||||
|
glUseProgram(g_shaderProgramGray16);
|
||||||
|
|
||||||
|
glUniform2fv(g_shaderPanGray16, 1, camera.pan());
|
||||||
|
glUniform1f(g_shaderZoomGray16, camera.zoom());
|
||||||
|
glUniform1f(g_shaderAspectGray16, camera.aspect());
|
||||||
|
}
|
||||||
|
else if(g_nColorType==1)
|
||||||
|
{
|
||||||
|
glUseProgram(g_shaderProgramRGB);
|
||||||
|
|
||||||
|
glUniform2fv(g_shaderPanRGB, 1, camera.pan());
|
||||||
|
glUniform1f(g_shaderZoomRGB, camera.zoom());
|
||||||
|
glUniform1f(g_shaderAspectRGB, camera.aspect());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void onErrorData(void* arg)
|
||||||
|
{
|
||||||
|
printf("onErrorData %d\n", (int)arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateTextureGray16(int nWidth, int nHeight, void* pData)
|
||||||
|
{
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE_ALPHA, nWidth, nHeight, 0, GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE, pData);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateTextureRGB(int nWidth, int nHeight, void* pData)
|
||||||
|
{
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, nWidth, nHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, pData);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateWindowWidthLevelGray16(int nWidth, int nLevel)
|
||||||
|
{
|
||||||
|
glUniform1f(g_shaderWindowCenterGray16, (float)nLevel);
|
||||||
|
glUniform1f(g_shaderWindowWidthGray16, (float)nWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateWindowWidthLevelRGB(int nWidth, int nLevel)
|
||||||
|
{
|
||||||
|
glUniform1f(g_shaderWindowCenterRGB, (float)nLevel);
|
||||||
|
glUniform1f(g_shaderWindowWidthRGB, (float)nWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
void initShaderGray16()
|
||||||
|
{
|
||||||
|
// Create and compile vertex shader
|
||||||
|
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
|
||||||
|
glShaderSource(vertexShader, 1, &vertexSourceGray16, NULL);
|
||||||
|
glCompileShader(vertexShader);
|
||||||
|
|
||||||
|
// Create and compile fragment shader
|
||||||
|
GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
|
||||||
|
glShaderSource(fragmentShader, 1, &fragmentSourceGray16, NULL);
|
||||||
|
glCompileShader(fragmentShader);
|
||||||
|
|
||||||
|
// Link vertex and fragment shader into shader program and use it
|
||||||
|
g_shaderProgramGray16 = glCreateProgram();
|
||||||
|
glAttachShader(g_shaderProgramGray16, vertexShader);
|
||||||
|
glAttachShader(g_shaderProgramGray16, fragmentShader);
|
||||||
|
glLinkProgram(g_shaderProgramGray16);
|
||||||
|
//glUseProgram(g_shaderProgramGray16);
|
||||||
|
|
||||||
|
// Get shader variables and initialize them
|
||||||
|
g_shaderPanGray16 = glGetUniformLocation(g_shaderProgramGray16, "pan");
|
||||||
|
g_shaderZoomGray16 = glGetUniformLocation(g_shaderProgramGray16, "zoom");
|
||||||
|
g_shaderAspectGray16 = glGetUniformLocation(g_shaderProgramGray16, "aspect");
|
||||||
|
g_shaderWindowCenterGray16 = glGetUniformLocation(g_shaderProgramGray16, "fWindowCenter");
|
||||||
|
g_shaderWindowWidthGray16 = glGetUniformLocation(g_shaderProgramGray16, "fWindowWidth");
|
||||||
|
|
||||||
|
glUniform1f(g_shaderWindowCenterGray16, 128.0f);
|
||||||
|
glUniform1f(g_shaderWindowWidthGray16, 255.0f);
|
||||||
|
//printf("pan:%d, zoom:%d, aspect:%d, g_shaderWindowCenterGray16:%d, g_shaderWindowWidthGray16:%d\n", g_shaderPanGray16, g_shaderZoomGray16, g_shaderAspectGray16, g_shaderWindowCenterGray16, g_shaderWindowWidthGray16);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void initShaderRGB()
|
||||||
|
{
|
||||||
|
// Create and compile vertex shader
|
||||||
|
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
|
||||||
|
glShaderSource(vertexShader, 1, &vertexSourceRGB, NULL);
|
||||||
|
glCompileShader(vertexShader);
|
||||||
|
|
||||||
|
// Create and compile fragment shader
|
||||||
|
GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
|
||||||
|
glShaderSource(fragmentShader, 1, &fragmentSourceRGB, NULL);
|
||||||
|
glCompileShader(fragmentShader);
|
||||||
|
|
||||||
|
// Link vertex and fragment shader into shader program and use it
|
||||||
|
g_shaderProgramRGB = glCreateProgram();
|
||||||
|
//printf("g_shaderProgramRGB: %d\n", g_shaderProgramRGB);
|
||||||
|
glAttachShader(g_shaderProgramRGB, vertexShader);
|
||||||
|
glAttachShader(g_shaderProgramRGB, fragmentShader);
|
||||||
|
glLinkProgram(g_shaderProgramRGB);
|
||||||
|
//glUseProgram(g_shaderProgramRGB);
|
||||||
|
|
||||||
|
// Get shader variables and initialize them
|
||||||
|
g_shaderPanRGB = glGetUniformLocation(g_shaderProgramRGB, "panRGB");
|
||||||
|
g_shaderZoomRGB = glGetUniformLocation(g_shaderProgramRGB, "zoomRGB");
|
||||||
|
g_shaderAspectRGB = glGetUniformLocation(g_shaderProgramRGB, "aspectRGB");
|
||||||
|
g_shaderWindowCenterRGB = glGetUniformLocation(g_shaderProgramRGB, "fWindowCenterRGB");
|
||||||
|
g_shaderWindowWidthRGB = glGetUniformLocation(g_shaderProgramRGB, "fWindowWidthRGB");
|
||||||
|
|
||||||
|
glUniform1f(g_shaderWindowCenterRGB, 128.0f);
|
||||||
|
glUniform1f(g_shaderWindowWidthRGB, 255.0f);
|
||||||
|
//printf("pan:%d, zoom:%d, aspect:%d, g_shaderWindowCenterRGB:%d, g_shaderWindowWidthRGB:%d\n", g_shaderPanRGB, g_shaderZoomRGB, g_shaderAspectRGB, g_shaderWindowCenterRGB, g_shaderWindowWidthRGB);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
GLuint initShader(EventHandler& eventHandler)
|
||||||
|
{
|
||||||
|
initShaderGray16();
|
||||||
|
initShaderRGB();
|
||||||
|
updateShader(eventHandler);
|
||||||
|
|
||||||
|
g_pEventHandler = &eventHandler;
|
||||||
|
|
||||||
|
return g_shaderProgramGray16;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint g_vbo = 0;
|
||||||
|
|
||||||
|
void initGeometry(GLuint shaderProgram)
|
||||||
|
{
|
||||||
|
// Create vertex buffer object and copy vertex data into it
|
||||||
|
|
||||||
|
//printf("g_vbo: %d\n", g_vbo);
|
||||||
|
|
||||||
|
glGenBuffers(1, &g_vbo);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, g_vbo);
|
||||||
|
GLfloat vertices[] =
|
||||||
|
{
|
||||||
|
-1.0f, -1.0f, 0.0f, 0.0f, 1.0f,
|
||||||
|
1.0f, -1.0f, 0.0f, 1.0f, 1.0f,
|
||||||
|
-1.0f, 1.0f, 0.0f, 0.0f, 0.0f,
|
||||||
|
|
||||||
|
1.0f, 1.0f, 0.0f, 1.0f, 0.0f,
|
||||||
|
-1.0f, 1.0f, 0.0f, 0.0f, 0.0f,
|
||||||
|
1.0f, -1.0f, 0.0f, 1.0f, 1.0f,
|
||||||
|
};
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
|
||||||
|
|
||||||
|
// Specify the layout of the shader vertex data (positions only, 3 floats)
|
||||||
|
GLint posAttrib = glGetAttribLocation(shaderProgram, "position");
|
||||||
|
glEnableVertexAttribArray(posAttrib);
|
||||||
|
// glVertexAttribPointer(posAttrib, 3, GL_FLOAT, GL_FALSE, 0, 0);
|
||||||
|
glVertexAttribPointer(posAttrib, 3, GL_FLOAT, GL_FALSE, 5*sizeof(GLfloat), (GLvoid*)0);
|
||||||
|
// glEnableVertexAttribArray(0);
|
||||||
|
|
||||||
|
GLint texAttrib = glGetAttribLocation(shaderProgram, "a_texCoord");
|
||||||
|
glEnableVertexAttribArray(texAttrib);
|
||||||
|
glVertexAttribPointer(texAttrib, 2, GL_FLOAT, GL_FALSE, 5*sizeof(GLfloat), (GLvoid*)(3*sizeof(GLfloat)));
|
||||||
|
//glEnableVertexAttribArray(2);
|
||||||
|
|
||||||
|
// glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||||
|
|
||||||
|
//printf("initGeometry(%d, %d)\n", posAttrib, texAttrib);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void PrintError()
|
||||||
|
{
|
||||||
|
int nError = 0;
|
||||||
|
nError = glGetError();
|
||||||
|
printf("Error Code: %d\n", nError);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void onLoadedData(void* arg, void* buffer, int nSize)
|
||||||
|
{
|
||||||
|
printf("onLoadedData %d, %d\n", (int)arg, nSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
void initTextureGray16(void* pImageData)
|
||||||
|
{
|
||||||
|
//const Uint16* pImageData = NULL;
|
||||||
|
//g_pDcmDataset->findAndGetUint16Array(DCM_PixelData, pImageData);
|
||||||
|
|
||||||
|
int w = g_nFrameWidth;
|
||||||
|
int h = g_nFrameHeight;
|
||||||
|
glEnable(GL_BLEND);
|
||||||
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||||
|
|
||||||
|
{
|
||||||
|
glGenTextures(1, &textureObj);
|
||||||
|
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, textureObj);
|
||||||
|
|
||||||
|
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
|
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||||
|
|
||||||
|
|
||||||
|
PrintError();
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE_ALPHA, w, h, 0, GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE, pImageData);
|
||||||
|
|
||||||
|
PrintError();
|
||||||
|
|
||||||
|
GLint nTextureID = glGetUniformLocation(g_shaderProgramGray16, "texSampler");
|
||||||
|
//printf("texSampler: %d\n", nTextureID);
|
||||||
|
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
|
||||||
|
glUniform1i(nTextureID, 0);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void initTextureRGB(void* pImageData)
|
||||||
|
{
|
||||||
|
printf("initTextureRGB\n");
|
||||||
|
//const Uint8* pImageData = NULL;
|
||||||
|
//g_pDcmDataset->findAndGetUint8Array(DCM_PixelData, pImageData);
|
||||||
|
|
||||||
|
|
||||||
|
int w = g_nFrameWidth;
|
||||||
|
int h = g_nFrameHeight;
|
||||||
|
glEnable(GL_BLEND);
|
||||||
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||||
|
|
||||||
|
GLint format = GL_RGB;
|
||||||
|
|
||||||
|
//printf("textureObj: %d\n", textureObj);
|
||||||
|
if(textureObj>=0)
|
||||||
|
{
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
|
||||||
|
glDeleteTextures(1, &textureObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
glGenTextures(1, &textureObj);
|
||||||
|
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, textureObj);
|
||||||
|
|
||||||
|
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
|
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||||
|
|
||||||
|
|
||||||
|
PrintError();
|
||||||
|
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, w, h, 0, GL_RGB, GL_UNSIGNED_BYTE, pImageData);
|
||||||
|
|
||||||
|
PrintError();
|
||||||
|
|
||||||
|
GLint nTextureID = glGetUniformLocation(g_shaderProgramRGB, "texSampler");
|
||||||
|
|
||||||
|
//printf("texSampler: %d\n", nTextureID);
|
||||||
|
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
|
||||||
|
glUniform1i(nTextureID, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
void redraw(EventHandler& eventHandler)
|
||||||
|
{
|
||||||
|
if(g_bChange==true)
|
||||||
|
{
|
||||||
|
if(g_nColorType==0)
|
||||||
|
{
|
||||||
|
initGeometry(g_shaderProgramGray16);
|
||||||
|
glUseProgram(g_shaderProgramGray16);
|
||||||
|
initTextureGray16(g_pImageData);
|
||||||
|
updateShader(*g_pEventHandler);
|
||||||
|
}
|
||||||
|
else if(g_nColorType==1)
|
||||||
|
{
|
||||||
|
initGeometry(g_shaderProgramRGB);
|
||||||
|
glUseProgram(g_shaderProgramRGB);
|
||||||
|
initTextureRGB(g_pImageData);
|
||||||
|
updateShader(*g_pEventHandler);
|
||||||
|
}
|
||||||
|
g_bChange = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(g_nTotalFrames>0)
|
||||||
|
{
|
||||||
|
if(g_nColorType==0)
|
||||||
|
{
|
||||||
|
printf("updateTextureRGB: %d\n", g_nCurrentFrame);
|
||||||
|
updateTextureGray16(g_nFrameWidth, g_nFrameHeight, g_pImageData);
|
||||||
|
}
|
||||||
|
else if(g_nColorType==1)
|
||||||
|
{
|
||||||
|
//printf("updateTextureRGB: %d, %d, %d, %d, %s\nerror: %s\n", g_nFrameWidth, g_nFrameHeight, g_nCurrentFrame, g_nFrameSizeUncompressed, decompressedColorModel.c_str(), ofTest.text());
|
||||||
|
updateTextureRGB(g_nFrameWidth, g_nFrameHeight, g_pImageData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(g_nCurrentFrame>=g_nTotalFrames)
|
||||||
|
{
|
||||||
|
g_nCurrentFrame = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if(g_nColorType==0)
|
||||||
|
{
|
||||||
|
updateWindowWidthLevelGray16(g_nPrevWindowWidth, g_nPrevWindowCenter);
|
||||||
|
}
|
||||||
|
else if(g_nColorType==1)
|
||||||
|
{
|
||||||
|
updateWindowWidthLevelRGB(g_nPrevWindowWidth, g_nPrevWindowCenter);
|
||||||
|
}
|
||||||
|
// Clear screen
|
||||||
|
glClearColor(0.0, 0.0, 0.0, 1.0);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
|
||||||
|
// Draw the vertex buffer
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||||
|
|
||||||
|
// Swap front/back framebuffers
|
||||||
|
eventHandler.swapWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
void mainLoop(void* mainLoopArg)
|
||||||
|
{
|
||||||
|
EventHandler& eventHandler = *((EventHandler*)mainLoopArg);
|
||||||
|
eventHandler.processEvents();
|
||||||
|
|
||||||
|
if(g_bLoadFile==false && g_strFilename.size()>0)
|
||||||
|
{
|
||||||
|
AVDictionary *opts = NULL;
|
||||||
|
int ret = av_dict_set(&opts, "rtsp_transport", "tcp", 0);
|
||||||
|
// av_dict_set(&opts, "rtsp_transport", "tcp", 0);
|
||||||
|
|
||||||
|
// av_dict_set(&opts, "timeout", "5000", 0);
|
||||||
|
|
||||||
|
printf("UpdateDcmImage: av_dic_set\n");
|
||||||
|
|
||||||
|
g_strFilename = "http://localhost:5173/playlist_1080p.m3u8";
|
||||||
|
|
||||||
|
printf("filename: %s\n", g_strFilename.c_str());
|
||||||
|
|
||||||
|
//open RTSP
|
||||||
|
//
|
||||||
|
// printf("avformat_open_input: start1212121212\n");
|
||||||
|
|
||||||
|
|
||||||
|
// format_ctx = avformat_alloc_context();
|
||||||
|
|
||||||
|
//if (avformat_open_input(&format_ctx, g_strFilename.c_str(), NULL, &opts) != 0)
|
||||||
|
if (avformat_open_input(&format_ctx, g_strFilename.c_str(), NULL, NULL) != 0)
|
||||||
|
{
|
||||||
|
printf("Error avformat_open_input");
|
||||||
|
//return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("avformat_open_input: finish\n");
|
||||||
|
|
||||||
|
|
||||||
|
av_dict_free(&opts);
|
||||||
|
|
||||||
|
callback_UpdateDcmImageComplete();
|
||||||
|
|
||||||
|
g_bLoadFile = true;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
int nType = eventHandler.GetEventType();
|
||||||
|
if(nType==1 || nType==2)
|
||||||
|
{
|
||||||
|
float fDeltaX = eventHandler.GetDeltaX();
|
||||||
|
float fDeltaY = eventHandler.GetDeltaY();
|
||||||
|
|
||||||
|
|
||||||
|
g_nPrevWindowCenter = g_nWindowCenter+fDeltaY;
|
||||||
|
g_nPrevWindowWidth = g_nWindowWidth + fDeltaX;
|
||||||
|
}
|
||||||
|
else if(nType==3 || nType==4)
|
||||||
|
{
|
||||||
|
g_nWindowCenter = g_nPrevWindowCenter;
|
||||||
|
g_nWindowWidth = g_nPrevWindowWidth;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update shader if camera changed
|
||||||
|
if (eventHandler.camera().updated())
|
||||||
|
updateShader(eventHandler);
|
||||||
|
|
||||||
|
redraw(eventHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
#define EXTERN extern "C"
|
||||||
|
#else
|
||||||
|
#define EXTERN
|
||||||
|
#endif
|
||||||
|
|
||||||
|
EXTERN EMSCRIPTEN_KEEPALIVE int UpdateDcmImage(std::string strURL)
|
||||||
|
{
|
||||||
|
|
||||||
|
printf("UpdateDcmImage: %s\n", strURL.c_str());
|
||||||
|
|
||||||
|
g_strFilename = strURL;
|
||||||
|
/*
|
||||||
|
|
||||||
|
AVDictionary *opts = NULL;
|
||||||
|
int ret = av_dict_set(&opts, "rtsp_transport", "tcp", 0);
|
||||||
|
|
||||||
|
printf("UpdateDcmImage: av_dic_set\n");
|
||||||
|
|
||||||
|
//open RTSP
|
||||||
|
|
||||||
|
if (avformat_open_input(&format_ctx, strURL.c_str(), NULL, &opts) != 0)
|
||||||
|
{
|
||||||
|
printf("Error avformat_open_input");
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
av_dict_free(&opts);
|
||||||
|
|
||||||
|
callback_UpdateDcmImageComplete();
|
||||||
|
*/
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
EXTERN EMSCRIPTEN_KEEPALIVE int SetWindowWidthLevel(int nWindowCenter, int nWindowWidth)
|
||||||
|
{
|
||||||
|
printf("New nWindowCenter:%d, nWindowWidth:%d\n", nWindowCenter, nWindowWidth);
|
||||||
|
|
||||||
|
g_nWindowCenter = nWindowCenter;
|
||||||
|
g_nWindowWidth = nWindowWidth;
|
||||||
|
|
||||||
|
g_nPrevWindowCenter = g_nWindowCenter;
|
||||||
|
g_nPrevWindowWidth = g_nWindowWidth;
|
||||||
|
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
EXTERN EMSCRIPTEN_KEEPALIVE int GetWindowWidth()
|
||||||
|
{
|
||||||
|
return g_nWindowWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
EXTERN EMSCRIPTEN_KEEPALIVE int GetWindowCenter()
|
||||||
|
{
|
||||||
|
return g_nWindowCenter;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
EXTERN EMSCRIPTEN_KEEPALIVE bool SetCallbackUpdateDcmImageComplete(callback_UpdateDcmImage callback_)
|
||||||
|
{
|
||||||
|
printf("SetCallbackUpdateDcmImage: %x\n", callback_);
|
||||||
|
|
||||||
|
callback_UpdateDcmImageComplete = callback_;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
int main(int argc, char** argv)
|
||||||
|
{
|
||||||
|
EventHandler eventHandler("dcm image viewer");
|
||||||
|
|
||||||
|
avdevice_register_all();
|
||||||
|
avformat_network_init();
|
||||||
|
|
||||||
|
if(g_pImageData==NULL)
|
||||||
|
{
|
||||||
|
g_pImageData = new uint8_t[1920*1080*3];
|
||||||
|
int i=0;
|
||||||
|
int j=0;
|
||||||
|
for(i=0 ; i<1080 ; i++)
|
||||||
|
{
|
||||||
|
for(j=0 ; j<1920 ; j++)
|
||||||
|
{
|
||||||
|
g_pImageData[(i*1920+j)*3+0] = 128;
|
||||||
|
g_pImageData[(i*1920+j)*3+1] = 128;
|
||||||
|
g_pImageData[(i*1920+j)*3+2] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize shader and geometry
|
||||||
|
GLuint shaderProgram = initShader(eventHandler);
|
||||||
|
|
||||||
|
//initTexture();
|
||||||
|
|
||||||
|
// Start the main loop
|
||||||
|
void* mainLoopArg = &eventHandler;
|
||||||
|
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
int fps = 0; // Use browser's requestAnimationFrame
|
||||||
|
emscripten_set_main_loop_arg(mainLoop, mainLoopArg, fps, true);
|
||||||
|
#else
|
||||||
|
while(true)
|
||||||
|
mainLoop(mainLoopArg);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
glDeleteTextures(1, &textureObj);
|
||||||
|
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
|
||||||
|
if(g_pImageData!=NULL)
|
||||||
|
{
|
||||||
|
delete[] g_pImageData;
|
||||||
|
g_pImageData = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("exit!!!!! c++ dcm_image_mod");
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
8
webassembly/ffmpeg_mod.sh
Executable file
8
webassembly/ffmpeg_mod.sh
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
export CFLAGS="-Wall "
|
||||||
|
|
||||||
|
emcc -std=c++17 -s USE_PTHREADS=0 -s PROXY_TO_PTHREAD=0 -s INVOKE_RUN=0 -s MODULARIZE=1 -s EXPORT_ES6=1 -s USE_ES6_IMPORT_META=0 -s ENVIRONMENT='web' -s EXPORTED_RUNTIME_METHODS="['ccall','cwrap','specialHTMLTargets', 'JSEvents', 'GL', 'callMain', 'abort', 'addFunction']" -s EXPORTED_FUNCTIONS="['_malloc', '_main', '_UpdateDcmImage', '_SetWindowWidthLevel', '_GetWindowWidth', '_GetWindowCenter','_SetCallbackUpdateDcmImageComplete']" -s RESERVED_FUNCTION_POINTERS=20 -s MAXIMUM_MEMORY=4096MB -s ALLOW_TABLE_GROWTH=1 -s ALLOW_MEMORY_GROWTH=1 -s USE_SDL=2 -s WASM=1 -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS="[""png"", ""jpg""]" -I /work/project/emsdk/upstream/emscripten/cache/sysroot/include /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libavcodec.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libavdevice.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libavfilter.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libavformat.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libavutil.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libswresample.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libswscale.a /work/project/emsdk/upstream/emscripten/cache/sysroot/lib/libz.a camera.cpp events.cpp ffmpeg_mod.cc -O2 -o ffmpeg_mod.js &&
|
||||||
|
cp -av ffmpeg_mod.wasm /work/project/web/svelte/test4/static/. &&
|
||||||
|
sed "1d" ffmpeg_mod.js > tmp.js && awk 'BEGIN{printf "\nexport "} {print}' tmp.js > ffmpeg_mod.js && rm tmp.js &&
|
||||||
|
cp -av ffmpeg_mod.js /work/project/web/svelte/test4/src/ui/.
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user