├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── API └── lambda_function.py ├── DeepLens └── greengrassHelloWorld.py ├── LICENSE ├── NOTICE ├── README.md ├── contributing ├── CODE_OF_CONDUCT.md └── CONTRIBUTING.md ├── images └── diagram-large.png ├── notebooks └── visual-search-feature-generation.ipynb ├── search └── lambda_function.py ├── template.yaml ├── test ├── README.md ├── metadata_to_ddb.py ├── sample-data-larger.txt └── sample-data.txt └── web-app ├── app ├── controllers │ ├── mainControllers.js │ └── searchControllers.js ├── css │ └── app.css ├── directives │ └── directives.js ├── filters │ └── filters.js ├── img │ ├── aws_logo.png │ └── info.png ├── js │ └── app.js ├── lib │ └── angular │ │ ├── angular-resource.js │ │ ├── angular-route.js │ │ └── angular.js ├── partials │ ├── main.html │ ├── search.html │ └── settings.html └── services │ ├── appConfig.js │ └── services.js └── index.html /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Eclipse 3 | .classpath 4 | .project 5 | .settings/ 6 | 7 | # Intellij 8 | .idea/ 9 | *.iml 10 | *.iws 11 | 12 | # Maven 13 | log/ 14 | target/ 15 | 16 | # VIM 17 | *.swp 18 | 19 | # Mac 20 | .DS_Store 21 | .DS_Store? 22 | 23 | # Windows 24 | Desktop.ini 25 | *.lnk 26 | *.cab 27 | *.msi 28 | *.msm 29 | *.msp 30 | $RECYCLE.BIN/ 31 | Thumbs.db 32 | ehthumbs.db 33 | -------------------------------------------------------------------------------- /API/lambda_function.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 3 | VISUAL SEARCH API LAMBDA FUNCTION 4 | 5 | ''' 6 | 7 | import redis 8 | import logging 9 | import json 10 | 11 | #------------------------------------------ 12 | # CONSTANTS 13 | # 14 | # replace 15 | #------------------------------------------ 16 | 17 | redis_hostname = '' 18 | 19 | #------------------------------------------ 20 | 21 | r = redis.StrictRedis(host=redis_hostname, port=6379, db=0, decode_responses=True) 22 | logger = logging.getLogger() 23 | logger.setLevel(logging.INFO) 24 | 25 | def lambda_handler(event, context): 26 | 27 | matches = r.lrange('stack:matches', 0, 0)[0] 28 | 29 | if matches is None: 30 | logger.error('NO MATCHES') 31 | response = { 32 | "statusCode": "400", 33 | "headers": { "Content-type": "application/json" }, 34 | "body": "NO MATCHES" 35 | } 36 | else: 37 | logger.info('FOUND MATCHES: ' + json.dumps(matches)) 38 | response = { 39 | "statusCode": "200", 40 | "headers": { "Content-type": "application/json" }, 41 | "body": matches 42 | } 43 | 44 | return response 45 | -------------------------------------------------------------------------------- /DeepLens/greengrassHelloWorld.py: -------------------------------------------------------------------------------- 1 | import os 2 | import greengrasssdk 3 | import awscam 4 | import mo 5 | import cv2 6 | 7 | 8 | client = greengrasssdk.client('iot-data') 9 | iot_topic = '$aws/things/{}/infer'.format(os.environ['AWS_IOT_THING_NAME']) 10 | 11 | 12 | def greengrass_infinite_infer_run(): 13 | 14 | input_height = 224 15 | input_width = 224 16 | model_name = 'featurizer-v1' 17 | error, model_path = mo.optimize(model_name, input_width, input_height) 18 | 19 | if error != 0: 20 | client.publish(topic=iot_topic, payload="Model optimization FAILED") 21 | else: 22 | client.publish(topic=iot_topic, payload="Model optimization SUCCEEDED") 23 | 24 | model = awscam.Model(model_path, {"GPU" : 1}) 25 | client.publish(topic=iot_topic, payload="Model loaded SUCCESSFULLY") 26 | 27 | while True: 28 | ret, frame = awscam.getLastFrame() 29 | if not ret: 30 | client.publish(topic=iot_topic, payload="FAILED to get frame") 31 | else: 32 | client.publish(topic=iot_topic, payload="frame retrieved") 33 | 34 | frame_resize = cv2.resize(frame, (input_width, input_height)) 35 | infer_output = model.doInference(frame_resize) 36 | features_numpy = None 37 | for _, val in infer_output.iteritems(): 38 | features_numpy = val 39 | 40 | features_string = ','.join(str(e) for e in features_numpy) 41 | msg = '{ "features": "' + features_string + '" }' 42 | client.publish(topic=iot_topic, payload=msg) 43 | 44 | 45 | greengrass_infinite_infer_run() 46 | 47 | 48 | def function_handler(event, context): 49 | return 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Visual Search 2 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Visual Search with Amazon SageMaker and AWS DeepLens 2 | 3 | This repository provides resources for implementing a visual search engine. Visual search is the central component of an interface where instead of asking for something by voice or text, you *show* what you are looking for. For an in-depth discussion of how visual search works and this project, see the related blog post series on the AWS Machine Learning Blog: 4 | 5 | - [Visual search on AWS—Part 1: Engine implementation with Amazon SageMaker](https://aws.amazon.com/blogs/machine-learning/visual-search-on-aws-part-1-engine-implementation-with-amazon-sagemaker/) 6 | - [Visual search on AWS—Part 2: Deployment with AWS DeepLens](https://aws.amazon.com/blogs/machine-learning/visual-search-on-aws-part-2-deploying-a-visual-search-model-with-aws-deeplens/) 7 | 8 | Briefly, here’s how the system works. When shown a real world, physical item, an AWS DeepLens device generates a feature vector representing that item. The feature vector generated by the AWS DeepLens device is sent to the AWS cloud using the AWS IoT service. An AWS IoT Rule is used to direct the incoming feature vector from the device to a cloud-based AWS Lambda function, which then uses that feature vector to search for visually similar items in an index of reference item feature vectors. This index is created using SageMaker's k-Nearest Neighbors (k-NN) algorithm. The search Lambda function returns the top visually similar reference item matches, which are then consumed by a web app via a separate API Lambda function fronted by Amazon API Gateway, as shown in the following architecture diagram. 9 | 10 | ![Overview](./images/diagram-large.png) 11 | 12 | 13 | ## How to Deploy 14 | 15 | In order to deploy this project, you'll need to spin up multiple pieces of infrastructure. Before doing so, please go through the Jupyter notebook in the notebooks directory to gain a deeper understanding of what is happening under the hood: [**Visual Search notebook**](./notebooks/visual-search-feature-generation.ipynb). The notebook also prepares the model you'll deploy to the AWS DeepLens device. 16 | 17 | The following instructions cover setup of the minimum number of pieces needed to view search results from feature vectors generated by AWS DeepLens. The instructions assume you have a basic familiarity with AWS in general and AWS DeepLens in particular. 18 | 19 | 1. **AWS DeepLens setup**: You'll need to create an AWS DeepLens project with (1) the model, and (2) a Lambda function to run on the AWS DeepLens device. 20 | - **Model**: Before proceeding, make sure you have run the Jupyter notebook [**Visual Search**](./notebooks/visual-search-feature-generation.ipynb). In the directory where you ran the notebook, locate the model files ```featurizer-v1-0000.params``` and ```featurizer-v1-symbol.json```. Place the model files in a S3 bucket with a name of the form ```deeplens-sagemaker-```. Click the **Models** link in the left pane of the DeepLens console, then click **Import Model** and select **Externally trained model.** Enter the S3 path and model name. 21 | - **Lambda function**: From the DeepLens directory of this repository, copy the Lambda function code. Create a new Lambda function using the blueprint ```greengrass-hello-world``` and paste the copied code into the code editor to completely replace the code in ```greengrassHelloWorld.py```. Make sure that you save, and then publish the function code by choosing **Publish new version** from the **Actions** menu. 22 | - **DeepLens project**: In the DeepLens console, click **Create new project**, then select **Create new blank project**. Given the project a name, then add the model and Lambda function you created above. 23 | 24 | 2. **Data Store setup**: This project makes use of two separate data stores. 25 | - **Amazon ElastiCache Redis**: using the ElastiCache console, create a one-node Redis cluster. Under **Advanced Redis settings**, make sure the default VPC and default security group are selected. For testing purposes, you can set the instance type and size to ```cache.t2.micro```. Make a note of the Primary Endpoint URL after creation. 26 | - **Amazon DynamoDB**: using the DynamoDB console, create a table named ```VisualSearchFeatures```. Set the table's Partition key to be a String named **id**. Either now or later you can populate the DynamoDB table with reference item metadata (product titles, image URLs etc.), as discussed below in the **Populating the Metadata** section of these instructions. 27 | 28 | 3. **API and Search Lambda function setup**: To create and deploy these Lambda functions, it is recommended (but not necessary) to use the AWS Cloud9 IDE for ease of use, and its built-in management capabilities for serverless applications using AWS Serverless Application Model (SAM). 29 | - **VPC note**: both Lambda functions must be associated with a VPC because they need to communicate with ElastiCache Redis. If you are using SAM either with Cloud9 or separately, see the file ```template.yaml``` in this repository for an example of VPC configuration. Be sure the security group you specify for the Lambda functions is the same as the one for the ElastiCache Redis cluster. For simplicity of test setup, the default security group is suggested (though not for a production environment). 30 | - **PrivateLink**: to ensure that the Lambda functions created below can communicate with the SageMaker endpoint in a secure manner, create a PrivateLink resource, specifying the default VPC, default subnets, and default security groups. For details, see https://aws.amazon.com/blogs/machine-learning/secure-prediction-calls-in-amazon-sagemaker-with-aws-privatelink/. 31 | - **API Lambda function**: From the API directory of this repository, copy the Lambda function code. Create a new Lambda function with a blank template; in the **Network** section of the **Configuration** tab of the Lambda console, make sure that the default VPC, default subnet(s), and default security group are selected. Then paste the copied code into the code editor. In the constants section of the code, change the Redis endpoint URL to the Primary Endpoint URL of your ElastiCache Redis. Do this by changing the value of the constant ```redis_hostname```. 32 | 33 | - **Search Lambda function:** From the Search directory of this repository, copy the Lambda function code. Create a new Lambda function with a blank template. Similarly to the API function, check the **Network** section of the configuration. Then paste the copied code into the code editor. In the constants section of the code, change the SageMaker endpoint name and Redis endpoint URL to the Primary Endpoint URL of your ElastiCache Redis. Do this by changing the values of the constants ```endpoint_name``` and ```redis_hostname```, respectively. Add an IoT trigger to the Lambda function configuration, with an IoT Rule of the form: 34 | ``` 35 | SELECT * FROM '' 36 | ``` 37 | 38 | 4. **Amazon API Gateway setup**: using the API Gateway console, create an API. The API only needs one method, a POST method with a resource of the form ```/matches```, which will invoke the API Lambda function you created above. Be sure to enable CORS. After the API is published, note the API's URL by clicking **Stages**, clicking the Stage name, then copying the **Invoke URL**. 39 | 40 | 5. **Front end / web app setup**: Either download or clone this repository, then in the code replace the API URL with the URL of the API you created in the previous step. To do this, go to the file ```/app/services/appConfig.js```, then change the line shown below by replacing the angle brackets and URL inside them with your Invoke URL: 41 | ``` 42 | .constant('ENV', ''); 43 | ``` 44 | Open the web app code in a text editor that has a captive web server, such as the Brackets editor. Highlight the index.html file, then launch the web server, which will open a browser window. In the web app UI, click through the **Visual Search** link. After you add reference item data to DynamoDB (see **Populating the Metadata** section below), you should see matches populating the UI after a few seconds. 45 | 46 | 47 | ## Populating the Metadata 48 | 49 | In order to view test results in the web app, you'll need to populate the DynamoDB table created above with some reference item data. To extract reference item metadata, please refer to the “Extract the Metadata” section of the Jupyter notebook [**Visual Search**](./notebooks/visual-search-feature-generation.ipynb). The code there writes the metadata out in JSON format to a file. 50 | 51 | Some sample test data is supplied in the test directory of this repository, including a very small sample and a larger sample of Amazon.com product metadata, including image URLs. There also is a Python 3 script named ```metadata-to-ddb.py``` for importing the data to DynamoDB . Execute the script with the following command in a directory that contains both the script and test data: 52 | 53 | ``` 54 | python3 ./metadata_to_ddb.py 55 | ``` 56 | 57 | 58 | # Licenses & Contributing 59 | 60 | The contents of this repository are licensed under the [Apache 2.0 License](./LICENSE). 61 | If you are interested in contributing to this project, please see the [Contributing Guidelines](./contributing/CONTRIBUTING.md). In connection with contributing, also review the [Code of Conduct](./contributing/CODE_OF_CONDUCT.md). 62 | 63 | The larger dataset of Amazon.com product images referenced herein was collected by the authors of the paper, J. McAuley et al (2015), “Image-based recommendations on styles and substitutes,” SIGIR, https://arxiv.org/abs/1506.04757. 64 | 65 | -------------------------------------------------------------------------------- /contributing/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /contributing/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/awslabs/visual-search/issues), or [recently closed](https://github.com/awslabs/visual-search/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels ((enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/awslabs/visual-search/labels/help%20wanted) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/awslabs/visual-search/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /images/diagram-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/visual-search/078df5e4fa67eab0cfeb37fdec72f6fa1df49373/images/diagram-large.png -------------------------------------------------------------------------------- /search/lambda_function.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 3 | VISUAL SEARCH MATCHES LOOKUP LAMBDA FUNCTION 4 | 5 | ''' 6 | 7 | import boto3 8 | import json 9 | import logging 10 | import math 11 | import redis 12 | 13 | #------------------------------------------ 14 | # CONSTANTS 15 | # 16 | # replace 17 | # replace 18 | #------------------------------------------ 19 | 20 | table_name = 'VisualSearchMetadata' 21 | endpoint_name = '' 22 | redis_hostname = '' 23 | 24 | #------------------------------------------ 25 | 26 | logger = logging.getLogger() 27 | logger.setLevel(logging.INFO) 28 | 29 | dynamodb = boto3.resource('dynamodb') 30 | r = redis.StrictRedis( host=redis_hostname, 31 | port=6379, 32 | db=0, 33 | decode_responses=True) 34 | 35 | runtime = boto3.client('runtime.sagemaker') 36 | 37 | 38 | def lambda_handler(event, context): 39 | 40 | #--------------------------------------------- 41 | # UNPACK QUERY 42 | #--------------------------------------------- 43 | 44 | # disregard messages other than those containing features 45 | if 'features' not in event: 46 | logger.info(event) 47 | return 48 | 49 | query = event 50 | # features are sent by the DeepLens device in CSV form 51 | query_features = query['features'] 52 | 53 | #--------------------------------------------- 54 | # k-NN INDEX LOOKUP 55 | #--------------------------------------------- 56 | 57 | res = runtime.invoke_endpoint( 58 | EndpointName=endpoint_name, 59 | Body=query_features, 60 | ContentType='text/csv', 61 | Accept='application/json; verbose=true' 62 | ) 63 | 64 | # extract reference item ids, convert them to a list of strings 65 | neighbors = json.loads(res['Body'].read()) 66 | f_nb = (((neighbors['predictions'])[0])['labels']) 67 | ids = [str(int(e)) for e in f_nb] 68 | 69 | 70 | #--------------------------------------------- 71 | # METADATA LOOKUP 72 | #--------------------------------------------- 73 | 74 | # batch request for reference item metadata 75 | response = dynamodb.batch_get_item( 76 | RequestItems={ 77 | table_name: { 78 | 'Keys': [ 79 | { 'id': ids[0] }, 80 | { 'id': ids[1] }, 81 | { 'id': ids[2] }, 82 | { 'id': ids[3] } 83 | ], 84 | 'ConsistentRead': False, 85 | 'AttributesToGet': ['id', 'title', 'url'] 86 | } 87 | }, 88 | ReturnConsumedCapacity='TOTAL' 89 | ) 90 | 91 | 92 | json_items = json.loads(json.dumps(response['Responses'])) 93 | for _, val in json_items.items(): 94 | matches = val 95 | 96 | # items returned by DynamoDB aren't in nearest match order -> rearrange 97 | ordered_matches = [] 98 | for index in ids: 99 | for match in matches: 100 | if match['id'] == index: 101 | ordered_matches.append(match) 102 | continue 103 | 104 | query_result = {} 105 | query_result['matches'] = ordered_matches 106 | 107 | #--------------------------------------------- 108 | # Validate / return response 109 | #--------------------------------------------- 110 | 111 | logger.info('QUERY RESULTS: ' + json.dumps(query_result)) 112 | # check query results for issues 113 | response = {} 114 | matches = query_result['matches'] 115 | if matches is None or len(matches) < 1: 116 | response['statusCode'] = 404 117 | response['body'] = "NO MATCHES" 118 | else: 119 | response['statusCode'] = 200 120 | response['body'] = "matches found" 121 | r.lpush('stack:matches', query_result) 122 | 123 | return response 124 | 125 | 126 | -------------------------------------------------------------------------------- /template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: 'AWS::Serverless-2016-10-31' 3 | Description: An AWS Serverless Specification template describing your function. 4 | Resources: 5 | VisualSearchAPI: 6 | Type: 'AWS::Serverless::Function' 7 | Properties: 8 | Handler: VisualSearchAPI/lambda_function.lambda_handler 9 | Runtime: python3.6 10 | Description: '' 11 | MemorySize: 3008 12 | Timeout: 15 13 | Role: 'arn:aws:iam::894087409521:role/LambdaRole' 14 | CodeUri: .debug/ 15 | VpcConfig: 16 | SecurityGroupIds: 17 | - sg-b8a788c0 18 | SubnetIds: 19 | - subnet-7591f510 20 | VisualSearchMatches: 21 | Type: 'AWS::Serverless::Function' 22 | Properties: 23 | Handler: VisualSearchMatches/lambda_function.lambda_handler 24 | Runtime: python3.6 25 | Description: '' 26 | MemorySize: 3008 27 | Timeout: 15 28 | Role: 'arn:aws:iam::894087409521:role/LambdaRole' 29 | CodeUri: .debug/ 30 | VpcConfig: 31 | SecurityGroupIds: 32 | - sg-b8a788c0 33 | SubnetIds: 34 | - subnet-7591f510 35 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/visual-search/078df5e4fa67eab0cfeb37fdec72f6fa1df49373/test/README.md -------------------------------------------------------------------------------- /test/metadata_to_ddb.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import json 3 | import os 4 | 5 | dynamodb = boto3.resource('dynamodb') 6 | table = dynamodb.Table('VisualSearchMetadata') 7 | 8 | 9 | with open('./metadata.json') as json_data: 10 | products = json.load(json_data) 11 | 12 | for product in products: 13 | 14 | response = table.put_item(Item=product) 15 | if response['ResponseMetadata']['HTTPStatusCode'] == 200: 16 | pass 17 | else: 18 | print('FAILURE TO WRITE') 19 | break 20 | -------------------------------------------------------------------------------- /test/sample-data.txt: -------------------------------------------------------------------------------- 1 | Bedtime Originals Plush Monkey Ollie, https://images-na.ssl-images-amazon.com/images/I/41ZmuuKMtmL._SL224_.jpg 2 | Gund Baby Nicky Noodle Monkey Stuffed Animal, https://images-na.ssl-images-amazon.com/images/I/51IvSHMnwRL._SL224_.jpg 3 | Ty Beanie Boos - Coconut - Monkey, https://images-na.ssl-images-amazon.com/images/I/51YXM8lvD%2BL._SL224_.jpg 4 | Jellycat Bashful Monkey, https://images-na.ssl-images-amazon.com/images/I/51Zq%2BPzdbXL._SL224_.jpg 5 | Fiesta Toys Brown Orangutan Plush Stuffed Animal, https://images-na.ssl-images-amazon.com/images/I/41496M5M%2BsL._SL224_.jpg 6 | Wild Republic Silverback Gorilla Plush, https://images-na.ssl-images-amazon.com/images/I/41z3mQG4enL._SL224_.jpg 7 | Swinging Safari Monkey, https://images-na.ssl-images-amazon.com/images/I/410aoWiD-qL._SL224_.jpg 8 | Principal Pudding From the Sock Monkey Family, https://images-na.ssl-images-amazon.com/images/I/41W2lANpy2L._SL224_.jpg 9 | Healthy Baby Asthma and Allergy Friendly Floppy Monkey, https://images-na.ssl-images-amazon.com/images/I/416dnws0GHL._SL224_.jpg 10 | Mary Meyer Putty Monkey Soft Toy, https://images-na.ssl-images-amazon.com/images/I/51acEXKlCKL._SL224_.jpg 11 | Monkey Bean Filled Plush Stuffed Animal, https://images-na.ssl-images-amazon.com/images/I/41CUZtSpDfL._SL224_.jpg 12 | GUND Burney Monkey Take Along Stuffed Animal Plush, https://images-na.ssl-images-amazon.com/images/I/41jd-wl2dmL._SL224_.jpg 13 | Lambs & Ivy Lollipop Jungle Plush Pink Monkey, https://images-na.ssl-images-amazon.com/images/I/41XYnryO9xL._SL224_.jpg 14 | Hand Stuffed Animal Plush Monkey by Adventure Planet, https://images-na.ssl-images-amazon.com/images/I/41iS8kYzRcL._SL224_.jpg 15 | Jellycat Fuddlewuddle Monkey, https://images-na.ssl-images-amazon.com/images/I/510LDrxiOaL._SL224_.jpg 16 | Manhattan Toy Lovelies Mocha Monkey Plush Animal, https://images-na.ssl-images-amazon.com/images/I/41O06ewumyL._SL224_.jpg 17 | Wild Republic Golden Lion Tamarin Plush, https://images-na.ssl-images-amazon.com/images/I/51CgTfEAxDL._SL224_.jpg 18 | GUND Wrigley Monkey Stuffed Animal Plush, https://images-na.ssl-images-amazon.com/images/I/4143jNt4IbL._SL224_.jpg 19 | Bedtime Originals Plush Toy Cupcake Monkey, https://images-na.ssl-images-amazon.com/images/I/41CgW8-MB4L._SL224_.jpg 20 | Hansa Plush - 10 inch Japan Monkey, https://images-na.ssl-images-amazon.com/images/I/41QwjYDHkpL._SL224_.jpg 21 | Stanley 43-511 Magnetic Shock Resistant Level, https://images-na.ssl-images-amazon.com/images/I/41xzNyLsB8L._SL224_.jpg 22 | Stanley 42-324 24-Inch I-Beam 180 Level, https://images-na.ssl-images-amazon.com/images/I/01GQbq01dWL._SL224_.jpg 23 | Stanley 42-480 48-Inch Professional I-Beam Level, https://images-na.ssl-images-amazon.com/images/I/21r4c0ZwBVL._SL224_.jpg 24 | Stanley 42-291 9 Inch Magnetic Level, https://images-na.ssl-images-amazon.com/images/I/41noTeU5E5L._SL224_.jpg 25 | Johnson Level & Tool 1402-0900 Level, https://images-na.ssl-images-amazon.com/images/I/31TPhtapB4L._SL224_.jpg 26 | Empire EM71.8 Professional True Blue Magnetic Box Level, https://images-na.ssl-images-amazon.com/images/I/31AEdzxkTlL._SL224_.jpg 27 | Johnson Level & Tool 555 Contractor Aluminum Line Level, https://images-na.ssl-images-amazon.com/images/I/31BB0ognG-L._SL224_.jpg 28 | 24-Inch Magnetic Level Tacklife MT-L04, https://images-na.ssl-images-amazon.com/images/I/41lIO1t1aDL._SL224_.jpg 29 | TACKLIFE MT-L03 Aluminum Alloy Magnetic Level, https://images-na.ssl-images-amazon.com/images/I/41JiZ84U5fL._SL224_.jpg 30 | M-D Building Products 92325 Smart Tool Digital Level, https://images-na.ssl-images-amazon.com/images/I/31%2BN-Qrp8ZL._SL224_.jpg 31 | Stanley FatMax 43-525 24-Inch Magnetic Level, https://images-na.ssl-images-amazon.com/images/I/31N0p40pvFL._SL224_.jpg 32 | Magnetic Level Tacklife MT-L01 Aluminum Alloy, https://images-na.ssl-images-amazon.com/images/I/413-cZ99xhL._SL224_.jpg 33 | Swanson Tool TL041M Heavy-duty Magnetic Level, https://images-na.ssl-images-amazon.com/images/I/41Hz74XRd5L._SL224_.jpg 34 | DOWELL 9 Inch Magnetic Box Level, https://images-na.ssl-images-amazon.com/images/I/31a3bSA-sfL._SL224_.jpg 35 | Black & Decker BDSL10 36-Inch Gecko Grip Level, https://images-na.ssl-images-amazon.com/images/I/3177jo3LcaL._SL224_.jpg 36 | Empire EM81.12 True Blue Magnetic Tool Box Level, https://images-na.ssl-images-amazon.com/images/I/31T830KGQXL._SL224_.jpg 37 | Empire 587-24 Level, https://images-na.ssl-images-amazon.com/images/I/41HlG5JSOGL._SL224_.jpg 38 | Heavy Duty Impact Resistant Carpenters Level by OCM, https://images-na.ssl-images-amazon.com/images/I/21ENuLT3owL._SL224_.jpg 39 | Klein Tools 935AB4V ACCU-BEND Level, https://images-na.ssl-images-amazon.com/images/I/41AX8gNchbL._SL224_.jpg 40 | Empire Level 841.6 Magnetic Billet Level, https://images-na.ssl-images-amazon.com/images/I/41a1hudKXFL._SL224_.jpg 41 | Dirt Devil Simpli-Stik Vacuum, https://images-na.ssl-images-amazon.com/images/I/31s8tf4BTGL._SL224_.jpg 42 | Eureka Blaze 3-in-1 Swivel Vacuum, https://images-na.ssl-images-amazon.com/images/I/31R1NB6OMhL._SL224_.jpg 43 | Dyson V8 Absolute Cordless HEPA Vacuum Cleaner, https://images-na.ssl-images-amazon.com/images/I/31Kfh6raNSL._SL224_.jpg 44 | BLACK+DECKER HHS315J01 Cordless 4-in-1 Vacuum, https://images-na.ssl-images-amazon.com/images/I/31RlR7OlN%2BL._SL224_.jpg 45 | BISSELL Multi Reach Hard Floor Stick and Hand Vacuum, https://images-na.ssl-images-amazon.com/images/I/31XQJ2eM75L._SL224.jpg 46 | Severin HV7158 Bagless Cordless Vacuum, https://images-na.ssl-images-amazon.com/images/I/31VhhjbiAUL._SL224_.jpg 47 | Hoover Linx Cordless Stick Vacuum, https://images-na.ssl-images-amazon.com/images/I/31TGKkLVOBL._SL224_.jpg 48 | Shark Navigator Freestyle Cordless Stick Vacuum, https://images-na.ssl-images-amazon.com/images/I/31QTG3XLlmL._SL2224_.jpg 49 | Shark Rocket Ultra-Light Upright (HV301), https://images-na.ssl-images-amazon.com/images/I/11Ifjqfs3QL._SL224_.jpg 50 | Corded Stick Vacuum Cleaner by BESTEK, https://images-na.ssl-images-amazon.com/images/I/3114aal9cAL._SL224_.jpg 51 | Electrolux Ergorapido Lithium Ion Stick Vacuum, https://images-na.ssl-images-amazon.com/images/I/310vV%2B8hNBL._SL224_.jpg 52 | BLACK+DECKER CHV1410L Hand Vacuum, https://images-na.ssl-images-amazon.com/images/I/41kVO1HjrgL._SL224_.jpg 53 | BISSELL Zing Bagless Canister Vacuum, https://images-na.ssl-images-amazon.com/images/I/41RqA4f2z9L._SL224_.jpg 54 | Armor All 2.5 Gallon Utility Wet Dry Vacuum, https://images-na.ssl-images-amazon.com/images/I/417poHASddL._SL224_.jpg 55 | Deik Cordless Vacuum Cleaner with High Power, https://images-na.ssl-images-amazon.com/images/I/31BgzWiPWYL._SL224_.jpg 56 | Bissell Cleanview Upright Bagless Vacuum Cleaner, https://images-na.ssl-images-amazon.com/images/I/41QEpuDLElL._SL224_.jpg 57 | Miele Compact C1 Pure Suction Canister Vacuum, https://images-na.ssl-images-amazon.com/images/I/418XxxVU8OL._SL224_.jpg 58 | Shark Rotator Slim-Lite Lift-Away, https://images-na.ssl-images-amazon.com/images/I/31p0ETGxYrL._SL224_.jpg 59 | Dirt Devil Tattoo Crimson Bouquet Bagged Canister Vacuum, https://images-na.ssl-images-amazon.com/images/I/41r4wLuuXzL._SL224_.jpg 60 | Hoover Vacuum Windtunnel MAX Upright Vacuum UH30600, https://images-na.ssl-images-amazon.com/images/I/31jtCUsSX3L._SL224_.jpg -------------------------------------------------------------------------------- /web-app/app/controllers/mainControllers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | /** 5 | * general purpose controllers for main page directory, managing login etc. 6 | * 7 | */ 8 | angular.module('angApp.mainControllers', []) 9 | 10 | .controller('MainCtrl', ['$scope', '$rootScope', '$window', '$location', '$http', 'Auth', 11 | function($scope, $rootScope, $window, $location, $http, Auth) { 12 | 13 | $scope.mainData = { 14 | loggedIn: true, // NOTE change to false to begin enabling login 15 | user: {userName: '', password: '', authToken: ''}, 16 | }; 17 | 18 | $scope.isLoggedIn = () => { 19 | 20 | let authInfo = $window.sessionStorage.getItem("auth"); 21 | if ( null == authInfo || "false" === authInfo ) { 22 | return false; 23 | } 24 | else { 25 | 26 | $scope.mainData.loggedIn = true; 27 | $scope.mainData.user.userName = $window.sessionStorage.getItem("userName"); 28 | $scope.mainData.user.authToken = $window.sessionStorage.getItem("authToken"); 29 | return true 30 | } 31 | } 32 | 33 | $scope.setIsLoggedIn = (isTrue, securityLevel) => { 34 | 35 | let sIsTrue = isTrue ? "true" : "false"; 36 | $window.sessionStorage.setItem("auth", sIsTrue); 37 | let curName = isTrue ? $scope.mainData.user.userName : ""; 38 | $window.sessionStorage.setItem("userName", curName); 39 | let authToken = isTrue ? $scope.mainData.user.authToken : ""; 40 | $window.sessionStorage.setItem("authToken", authToken); 41 | 42 | // set security level 43 | // set a common "Authorization" header for all HTTP requests 44 | // (NOTE: the app.js run function has a case to handle page refreshes, any changes 45 | // made here should also be made to that function) 46 | if ( isTrue ) { 47 | $window.sessionStorage.setItem("securityLevel", securityLevel); 48 | $http.defaults.headers.common['Authorization'] = 'Bearer ' + $scope.mainData.user.authToken; 49 | } 50 | } 51 | 52 | // check for login every time this page is launched 53 | $scope.isLoggedIn(); 54 | 55 | // click handler for login button 56 | $scope.login = () => { 57 | 58 | // NOTE: if there are any kind of username and password format limits, validate here. 59 | if ( $scope.mainData.user.userName === '' || $scope.mainData.user.password === '' ) { 60 | $scope.loginError = true; 61 | return; 62 | } 63 | 64 | Auth.user.authUser($scope.mainData.user, function(data) { 65 | 66 | $scope.loginError = false; 67 | $scope.mainData.user.authToken = data.token; 68 | $scope.setIsLoggedIn(true, data.securityLevel); 69 | $scope.mainData.loggedIn = true; 70 | 71 | }, function(e) { 72 | 73 | $scope.loginError = true; 74 | $scope.setIsLoggedIn(false, null); 75 | $scope.mainData.loggedIn = false; 76 | } ); 77 | 78 | }; 79 | 80 | // click handler for directory selection table 81 | $scope.setToolType = (event) => { 82 | 83 | let typeValue = event.currentTarget.attributes.id.nodeValue; 84 | 85 | if ( typeValue === "search" ) { 86 | $window.sessionStorage.setItem("toolType", "search"); 87 | $location.path('/search'); 88 | } 89 | else { 90 | // expand for new pages 91 | } 92 | } 93 | 94 | // click handler for logout button 95 | $scope.logOut = () => { 96 | 97 | $scope.setIsLoggedIn(false, null); 98 | $location.path('/'); 99 | } 100 | 101 | $scope.hasSufficientSecurityLevel = (requiredLevel) => { 102 | 103 | let actualSecurityLevel = $window.sessionStorage.getItem("securityLevel"); 104 | if ( null == actualSecurityLevel || parseInt(actualSecurityLevel) < requiredLevel ) { 105 | alert("You do not have sufficient security permissions to proceed."); 106 | return false; 107 | } 108 | 109 | return true; 110 | } 111 | 112 | 113 | }]); 114 | 115 | -------------------------------------------------------------------------------- /web-app/app/controllers/searchControllers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | angular.module('angApp.searchControllers', ['angApp.appConfig']) 5 | 6 | .controller('SearchCtrl', ['$scope', '$rootScope', '$interval', '$window', '$http', 'ENV', 'Search', 7 | function($scope, $rootScope, $interval, $window, $http, ENV, Search) { 8 | 9 | $scope.params = { 10 | 11 | SearchType: null 12 | }; 13 | 14 | $scope.environmentURL = null; 15 | 16 | var poll; 17 | 18 | $scope.searchResults = null; 19 | $scope.images = null; 20 | $scope.titles = null; 21 | 22 | checkForSearchResults(); 23 | 24 | function checkForSearchResults() { 25 | 26 | // poll every few seconds for new search results i.e. matches 27 | poll = $interval( () => { 28 | 29 | // placeholder, no need yet for params for API call 30 | let params = { 31 | ApiCallType: "check-search-results" 32 | } 33 | 34 | $http.post(ENV + 'matches', params) 35 | 36 | .then( 37 | 38 | // backend responded successfully 39 | function(response) { 40 | 41 | console.log(response); 42 | let rawResult = response.data.body; 43 | rawResult = rawResult.replace(/\'/g, "\""); 44 | let json = JSON.parse(rawResult); 45 | $scope.searchResults = json; 46 | 47 | $scope.images = []; 48 | $scope.titles = []; 49 | let matches = json.matches; 50 | matches.forEach( match => { 51 | $scope.images.push(match.url); 52 | $scope.titles.push(match.title); 53 | }); 54 | 55 | }, 56 | // failure outside the backend function, e.g. couldn't call the backend 57 | function(response) { 58 | 59 | $scope.stop(); 60 | console.log(response); 61 | } 62 | ); 63 | 64 | }, 10000); // end poll 65 | 66 | } 67 | 68 | $scope.stop = () => { 69 | 70 | if (angular.isDefined(poll)) { 71 | 72 | $interval.cancel(poll); 73 | poll = undefined; 74 | } 75 | }; 76 | 77 | $scope.$on('$destroy', () => { 78 | 79 | // make sure that the interval also is destroyed 80 | $scope.stop(); 81 | }); 82 | 83 | // the API call below is a form that can be used for single calls, 84 | // rather than for polling 85 | /* 86 | Search.result.searchResults($scope.params.SearchType, function(data) { 87 | 88 | console.log(data); 89 | $scope.searchResults = data.body 90 | 91 | }, function(e) { 92 | 93 | console.log(e); 94 | } ); 95 | */ 96 | 97 | }]); 98 | 99 | -------------------------------------------------------------------------------- /web-app/app/css/app.css: -------------------------------------------------------------------------------- 1 | /* app css stylesheet */ 2 | 3 | /* ========================== 4 | 5 | Sign In styles 6 | 7 | ============================= */ 8 | 9 | .form-signin 10 | { 11 | max-width: 330px; 12 | padding: 15px; 13 | margin: 0 auto; 14 | } 15 | 16 | .form-signin .form-signin-heading, .form-signin .checkbox 17 | { 18 | margin-bottom: 10px; 19 | } 20 | .form-signin .checkbox 21 | { 22 | font-weight: normal; 23 | } 24 | .form-signin .form-control 25 | { 26 | position: relative; 27 | font-size: 16px; 28 | height: auto; 29 | padding: 10px; 30 | -webkit-box-sizing: border-box; 31 | -moz-box-sizing: border-box; 32 | box-sizing: border-box; 33 | } 34 | 35 | .form-signin .form-error 36 | { 37 | color: red; 38 | position: relative; 39 | font-size: 16px; 40 | height: auto; 41 | padding: 10px; 42 | -webkit-box-sizing: border-box; 43 | -moz-box-sizing: border-box; 44 | box-sizing: border-box; 45 | } 46 | 47 | .form-signin .form-control:focus 48 | { 49 | z-index: 2; 50 | } 51 | .form-signin input[type="text"] 52 | { 53 | margin-bottom: -1px; 54 | border-bottom-left-radius: 0; 55 | border-bottom-right-radius: 0; 56 | } 57 | .form-signin input[type="password"] 58 | { 59 | margin-bottom: 10px; 60 | border-top-left-radius: 0; 61 | border-top-right-radius: 0; 62 | } 63 | .account-wall 64 | { 65 | margin-top: 20px; 66 | padding: 40px 0px 20px 0px; 67 | background-color: #f7f7f7; 68 | -moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); 69 | -webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); 70 | box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); 71 | } 72 | .login-title 73 | { 74 | color: #555; 75 | font-size: 18px; 76 | font-weight: 400; 77 | display: block; 78 | } 79 | .profile-img 80 | { 81 | width: 96px; 82 | height: 96px; 83 | margin: 0 auto 10px; 84 | display: block; 85 | -moz-border-radius: 50%; 86 | -webkit-border-radius: 50%; 87 | border-radius: 50%; 88 | } 89 | .need-help 90 | { 91 | margin-top: 10px; 92 | } 93 | .new-account 94 | { 95 | display: block; 96 | margin-top: 10px; 97 | } 98 | 99 | 100 | /* ========================== 101 | 102 | other styles 103 | 104 | ============================= */ 105 | 106 | .loader { 107 | border: 16px solid #f3f3f3; /* Light grey */ 108 | border-top: 16px solid #3498db; /* Blue */ 109 | border-radius: 50%; 110 | width: 120px; 111 | height: 120px; 112 | animation: spin 2s linear infinite; 113 | } 114 | 115 | @keyframes spin { 116 | 0% { transform: rotate(0deg); } 117 | 100% { transform: rotate(360deg); } 118 | } 119 | 120 | .views { 121 | padding: 10px 30px 10px 30px; 122 | } 123 | 124 | .logo_image { 125 | width:200px; 126 | height:100px; 127 | float: right; 128 | margin-right: 3em; 129 | margin-bottom: 5em; 130 | } 131 | 132 | .info_image { 133 | width:20px; 134 | height:20px; 135 | margin-left: 1em; 136 | } 137 | 138 | .menu { 139 | list-style: none; 140 | margin-top: 1em; 141 | margin-left: 2em; 142 | padding: 0 0 0.5em; 143 | } 144 | 145 | .menu:before { 146 | content: "["; 147 | } 148 | 149 | .menu:after { 150 | content: "]"; 151 | } 152 | 153 | .menu > li { 154 | display: inline; 155 | } 156 | 157 | .menu > li:before { 158 | content: "|"; 159 | padding-right: 0.3em; 160 | } 161 | 162 | .menu > li:nth-child(1):before { 163 | content: ""; 164 | padding: 0; 165 | } 166 | 167 | a { 168 | cursor: pointer; 169 | } 170 | 171 | thead { 172 | cursor: pointer; 173 | } 174 | 175 | th.sortable { 176 | color: #0088cc; 177 | } 178 | 179 | td { 180 | padding: 0.2em 1em; 181 | } 182 | 183 | td.open { 184 | color: darkgreen; 185 | } 186 | 187 | td.closed { 188 | color: maroon; 189 | } 190 | 191 | .sort-desc { 192 | background:no-repeat right center url(data:image/gif;base64,R0lGODlhCgAKALMAAHFxcYKCgp2dnaampq+vr83NzeHh4f///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAAAgAIf/8SUNDUkdCRzEwMTIAAAUwYXBwbAIgAABtbnRyUkdCIFhZWiAH2QACABkACwAaAAthY3NwQVBQTAAAAABhcHBsAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLWFwcGwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAtkc2NtAAABCAAAAvJkZXNjAAAD/AAAAG9nWFlaAAAEbAAAABR3dHB0AAAEgAAAABRyWFlaAAAElAAAABRiWFlaAAAEqAAAABRyVFJDAAAEvAAAAA5jcHJ0AAAEzAAAADhjaGFkAAAFBAAAACxn/1RSQwAABLwAAAAOYlRSQwAABLwAAAAObWx1YwAAAAAAAAARAAAADGVuVVMAAAAmAAACfmVzRVMAAAAmAAABgmRhREsAAAAuAAAB6mRlREUAAAAsAAABqGZpRkkAAAAoAAAA3GZyRlUAAAAoAAABKml0SVQAAAAoAAACVm5sTkwAAAAoAAACGG5iTk8AAAAmAAABBHB0QlIAAAAmAAABgnN2U0UAAAAmAAABBGphSlAAAAAaAAABUmtvS1IAAAAWAAACQHpoVFcAAAAWAAABbHpoQ04AAAAWAAAB1HJ1UlUAAAAiAAACpHBsUEwAAAAsAAACxgBZAGwAZQBpAG4AZf8AbgAgAFIARwBCAC0AcAByAG8AZgBpAGkAbABpAEcAZQBuAGUAcgBpAHMAawAgAFIARwBCAC0AcAByAG8AZgBpAGwAUAByAG8AZgBpAGwAIABHAOkAbgDpAHIAaQBxAHUAZQAgAFIAVgBCTgCCLAAgAFIARwBCACAw1zDtMNUwoTCkMOuQGnUoACAAUgBHAEIAIIJyX2ljz4/wAFAAZQByAGYAaQBsACAAUgBHAEIAIABHAGUAbgDpAHIAaQBjAG8AQQBsAGwAZwBlAG0AZQBpAG4AZQBzACAAUgBHAEIALQBQAHIAbwBmAGkAbGZukBoAIABSAEcAQgAgY8+P8GX/h072AEcAZQBuAGUAcgBlAGwAIABSAEcAQgAtAGIAZQBzAGsAcgBpAHYAZQBsAHMAZQBBAGwAZwBlAG0AZQBlAG4AIABSAEcAQgAtAHAAcgBvAGYAaQBlAGzHfLwYACAAUgBHAEIAINUEuFzTDMd8AFAAcgBvAGYAaQBsAG8AIABSAEcAQgAgAEcAZQBuAGUAcgBpAGMAbwBHAGUAbgBlAHIAaQBjACAAUgBHAEIAIABQAHIAbwBmAGkAbABlBB4EMQRJBDgEOQAgBD8EQAQ+BEQEOAQ7BEwAIABSAEcAQgBVAG4AaQB3AGUAcgBzAGEAbABuAHkAIABwAHIAbwBm/wBpAGwAIABSAEcAQgAAZGVzYwAAAAAAAAAUR2VuZXJpYyBSR0IgUHJvZmlsZQAAAAAAAAAAAAAAFEdlbmVyaWMgUkdCIFByb2ZpbGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABadQAArHMAABc0WFlaIAAAAAAAAPNSAAEAAAABFs9YWVogAAAAAAAAdE0AAD3uAAAD0FhZWiAAAAAAAAAoGgAAFZ8AALg2Y3VydgAAAAAAAAABAc0AAHRleHQAAAAAQ29weXJpZ2h0IDIwMDcgQXBwbGUgSW5jLkMsIGFsbCByaWdodHMgcmVzZXJ2ZWQuAHNmMzIAAAAAAAEMQgAABd7///MmAAAHkgAA/ZH///ui///9owAAA9wAAMBsACwAAAAACgAKAAAEJZAMIcakQZjNtyhFxwEIIRofAookUnapu26t+6KFLYe1TgQ5VwQAOw%3D%3D); 193 | } 194 | .sort-asc { 195 | background:no-repeat right center url(data:image/gif;base64,R0lGODlhCgAKALMAAHFxcYKCgp2dnaampq+vr83NzeHh4f///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAAAgAIf/8SUNDUkdCRzEwMTIAAAUwYXBwbAIgAABtbnRyUkdCIFhZWiAH2QACABkACwAaAAthY3NwQVBQTAAAAABhcHBsAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLWFwcGwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAtkc2NtAAABCAAAAvJkZXNjAAAD/AAAAG9nWFlaAAAEbAAAABR3dHB0AAAEgAAAABRyWFlaAAAElAAAABRiWFlaAAAEqAAAABRyVFJDAAAEvAAAAA5jcHJ0AAAEzAAAADhjaGFkAAAFBAAAACxn/1RSQwAABLwAAAAOYlRSQwAABLwAAAAObWx1YwAAAAAAAAARAAAADGVuVVMAAAAmAAACfmVzRVMAAAAmAAABgmRhREsAAAAuAAAB6mRlREUAAAAsAAABqGZpRkkAAAAoAAAA3GZyRlUAAAAoAAABKml0SVQAAAAoAAACVm5sTkwAAAAoAAACGG5iTk8AAAAmAAABBHB0QlIAAAAmAAABgnN2U0UAAAAmAAABBGphSlAAAAAaAAABUmtvS1IAAAAWAAACQHpoVFcAAAAWAAABbHpoQ04AAAAWAAAB1HJ1UlUAAAAiAAACpHBsUEwAAAAsAAACxgBZAGwAZQBpAG4AZf8AbgAgAFIARwBCAC0AcAByAG8AZgBpAGkAbABpAEcAZQBuAGUAcgBpAHMAawAgAFIARwBCAC0AcAByAG8AZgBpAGwAUAByAG8AZgBpAGwAIABHAOkAbgDpAHIAaQBxAHUAZQAgAFIAVgBCTgCCLAAgAFIARwBCACAw1zDtMNUwoTCkMOuQGnUoACAAUgBHAEIAIIJyX2ljz4/wAFAAZQByAGYAaQBsACAAUgBHAEIAIABHAGUAbgDpAHIAaQBjAG8AQQBsAGwAZwBlAG0AZQBpAG4AZQBzACAAUgBHAEIALQBQAHIAbwBmAGkAbGZukBoAIABSAEcAQgAgY8+P8GX/h072AEcAZQBuAGUAcgBlAGwAIABSAEcAQgAtAGIAZQBzAGsAcgBpAHYAZQBsAHMAZQBBAGwAZwBlAG0AZQBlAG4AIABSAEcAQgAtAHAAcgBvAGYAaQBlAGzHfLwYACAAUgBHAEIAINUEuFzTDMd8AFAAcgBvAGYAaQBsAG8AIABSAEcAQgAgAEcAZQBuAGUAcgBpAGMAbwBHAGUAbgBlAHIAaQBjACAAUgBHAEIAIABQAHIAbwBmAGkAbABlBB4EMQRJBDgEOQAgBD8EQAQ+BEQEOAQ7BEwAIABSAEcAQgBVAG4AaQB3AGUAcgBzAGEAbABuAHkAIABwAHIAbwBm/wBpAGwAIABSAEcAQgAAZGVzYwAAAAAAAAAUR2VuZXJpYyBSR0IgUHJvZmlsZQAAAAAAAAAAAAAAFEdlbmVyaWMgUkdCIFByb2ZpbGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABadQAArHMAABc0WFlaIAAAAAAAAPNSAAEAAAABFs9YWVogAAAAAAAAdE0AAD3uAAAD0FhZWiAAAAAAAAAoGgAAFZ8AALg2Y3VydgAAAAAAAAABAc0AAHRleHQAAAAAQ29weXJpZ2h0IDIwMDcgQXBwbGUgSW5jLkMsIGFsbCByaWdodHMgcmVzZXJ2ZWQuAHNmMzIAAAAAAAEMQgAABd7///MmAAAHkgAA/ZH///ui///9owAAA9wAAMBsACwAAAAACgAKAAAEJRBJREKZsxQDsCSGIVzZFnYTGIqktp7fG46uzAn2TAyCMPC9QAQAOw%3D%3D); 196 | } -------------------------------------------------------------------------------- /web-app/app/directives/directives.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* Directives */ 4 | 5 | 6 | angular.module('angApp.directives', []). 7 | directive('appVersion', ['version', function(version) { 8 | return function(scope, elm, attrs) { 9 | elm.text(version); 10 | }; 11 | }]); 12 | -------------------------------------------------------------------------------- /web-app/app/filters/filters.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* Filters */ 4 | 5 | angular.module('angApp.filters', []). 6 | filter('interpolate', ['version', function(version) { 7 | return function(text) { 8 | return String(text).replace(/\%VERSION\%/mg, version); 9 | } 10 | }]); 11 | -------------------------------------------------------------------------------- /web-app/app/img/aws_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/visual-search/078df5e4fa67eab0cfeb37fdec72f6fa1df49373/web-app/app/img/aws_logo.png -------------------------------------------------------------------------------- /web-app/app/img/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/visual-search/078df5e4fa67eab0cfeb37fdec72f6fa1df49373/web-app/app/img/info.png -------------------------------------------------------------------------------- /web-app/app/js/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | angular.module('angApp', [ 'ngRoute', 'ngResource', 'ui.bootstrap', 5 | 'angApp.appConfig', 6 | 'angApp.filters', 7 | 'angApp.services', 8 | 'angApp.directives', 9 | 'angApp.mainControllers', 10 | 'angApp.searchControllers' ] ) 11 | 12 | 13 | .config(['$routeProvider', '$httpProvider', function($routeProvider, $httpProvider) { 14 | 15 | // main page 16 | $routeProvider.when('/', 17 | {templateUrl: 'app/partials/main.html', controller: 'MainCtrl'}); 18 | 19 | // search page 20 | $routeProvider.when('/search', 21 | {templateUrl: 'app/partials/search.html', controller: 'SearchCtrl'}); 22 | 23 | // settings 24 | $routeProvider.when('/settings', 25 | {templateUrl: 'app/partials/settings.html', controller: 'MainCtrl'}); 26 | 27 | $routeProvider.otherwise({redirectTo: '/'}); 28 | 29 | // to enable CORS (next two lines) 30 | $httpProvider.defaults.useXDomain = true; 31 | delete $httpProvider.defaults.headers.common['X-Requested-With']; 32 | 33 | // to enable storing/setting cookies with XHR requests (by default this is not allowed in most browsers) 34 | //$httpProvider.defaults.withCredentials = true; 35 | }]) 36 | 37 | .run( function($rootScope, $location, $window, $http) { 38 | 39 | // global api error handler 40 | $rootScope.handleApiError = (e) => { 41 | alert( "API ERROR \nError code: " + 42 | e.data.apiError + "\nError message: " + 43 | e.data.apiMessage + "\nRuntime exception: " + 44 | e.data.javaException); 45 | if (401 == e.data.apiError ) { 46 | $location.path('/settings'); 47 | } 48 | }; 49 | 50 | // keep user logged in after page refresh by re-setting the Authorization header, 51 | // which ordinarily would be lost after a refresh due to the browser somehow 52 | // clearing the $http object of its default properties such as headers 53 | var authToken = $window.sessionStorage.getItem("authToken"); 54 | if ( authToken != null && authToken.length > 0 ) { 55 | $http.defaults.headers.common['Authorization'] = 'Bearer ' + authToken; 56 | } 57 | 58 | }); 59 | 60 | -------------------------------------------------------------------------------- /web-app/app/lib/angular/angular-resource.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.5.8 3 | * (c) 2010-2016 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | (function(window, angular) {'use strict'; 7 | 8 | var $resourceMinErr = angular.$$minErr('$resource'); 9 | 10 | // Helper functions and regex to lookup a dotted path on an object 11 | // stopping at undefined/null. The path must be composed of ASCII 12 | // identifiers (just like $parse) 13 | var MEMBER_NAME_REGEX = /^(\.[a-zA-Z_$@][0-9a-zA-Z_$@]*)+$/; 14 | 15 | function isValidDottedPath(path) { 16 | return (path != null && path !== '' && path !== 'hasOwnProperty' && 17 | MEMBER_NAME_REGEX.test('.' + path)); 18 | } 19 | 20 | function lookupDottedPath(obj, path) { 21 | if (!isValidDottedPath(path)) { 22 | throw $resourceMinErr('badmember', 'Dotted member path "@{0}" is invalid.', path); 23 | } 24 | var keys = path.split('.'); 25 | for (var i = 0, ii = keys.length; i < ii && angular.isDefined(obj); i++) { 26 | var key = keys[i]; 27 | obj = (obj !== null) ? obj[key] : undefined; 28 | } 29 | return obj; 30 | } 31 | 32 | /** 33 | * Create a shallow copy of an object and clear other fields from the destination 34 | */ 35 | function shallowClearAndCopy(src, dst) { 36 | dst = dst || {}; 37 | 38 | angular.forEach(dst, function(value, key) { 39 | delete dst[key]; 40 | }); 41 | 42 | for (var key in src) { 43 | if (src.hasOwnProperty(key) && !(key.charAt(0) === '$' && key.charAt(1) === '$')) { 44 | dst[key] = src[key]; 45 | } 46 | } 47 | 48 | return dst; 49 | } 50 | 51 | /** 52 | * @ngdoc module 53 | * @name ngResource 54 | * @description 55 | * 56 | * # ngResource 57 | * 58 | * The `ngResource` module provides interaction support with RESTful services 59 | * via the $resource service. 60 | * 61 | * 62 | *
63 | * 64 | * See {@link ngResource.$resourceProvider} and {@link ngResource.$resource} for usage. 65 | */ 66 | 67 | /** 68 | * @ngdoc provider 69 | * @name $resourceProvider 70 | * 71 | * @description 72 | * 73 | * Use `$resourceProvider` to change the default behavior of the {@link ngResource.$resource} 74 | * service. 75 | * 76 | * ## Dependencies 77 | * Requires the {@link ngResource } module to be installed. 78 | * 79 | */ 80 | 81 | /** 82 | * @ngdoc service 83 | * @name $resource 84 | * @requires $http 85 | * @requires ng.$log 86 | * @requires $q 87 | * @requires ng.$timeout 88 | * 89 | * @description 90 | * A factory which creates a resource object that lets you interact with 91 | * [RESTful](http://en.wikipedia.org/wiki/Representational_State_Transfer) server-side data sources. 92 | * 93 | * The returned resource object has action methods which provide high-level behaviors without 94 | * the need to interact with the low level {@link ng.$http $http} service. 95 | * 96 | * Requires the {@link ngResource `ngResource`} module to be installed. 97 | * 98 | * By default, trailing slashes will be stripped from the calculated URLs, 99 | * which can pose problems with server backends that do not expect that 100 | * behavior. This can be disabled by configuring the `$resourceProvider` like 101 | * this: 102 | * 103 | * ```js 104 | app.config(['$resourceProvider', function($resourceProvider) { 105 | // Don't strip trailing slashes from calculated URLs 106 | $resourceProvider.defaults.stripTrailingSlashes = false; 107 | }]); 108 | * ``` 109 | * 110 | * @param {string} url A parameterized URL template with parameters prefixed by `:` as in 111 | * `/user/:username`. If you are using a URL with a port number (e.g. 112 | * `http://example.com:8080/api`), it will be respected. 113 | * 114 | * If you are using a url with a suffix, just add the suffix, like this: 115 | * `$resource('http://example.com/resource.json')` or `$resource('http://example.com/:id.json')` 116 | * or even `$resource('http://example.com/resource/:resource_id.:format')` 117 | * If the parameter before the suffix is empty, :resource_id in this case, then the `/.` will be 118 | * collapsed down to a single `.`. If you need this sequence to appear and not collapse then you 119 | * can escape it with `/\.`. 120 | * 121 | * @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in 122 | * `actions` methods. If a parameter value is a function, it will be called every time 123 | * a param value needs to be obtained for a request (unless the param was overridden). The function 124 | * will be passed the current data value as an argument. 125 | * 126 | * Each key value in the parameter object is first bound to url template if present and then any 127 | * excess keys are appended to the url search query after the `?`. 128 | * 129 | * Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in 130 | * URL `/path/greet?salutation=Hello`. 131 | * 132 | * If the parameter value is prefixed with `@`, then the value for that parameter will be 133 | * extracted from the corresponding property on the `data` object (provided when calling a 134 | * "non-GET" action method). 135 | * For example, if the `defaultParam` object is `{someParam: '@someProp'}` then the value of 136 | * `someParam` will be `data.someProp`. 137 | * Note that the parameter will be ignored, when calling a "GET" action method (i.e. an action 138 | * method that does not accept a request body) 139 | * 140 | * @param {Object.=} actions Hash with declaration of custom actions that should extend 141 | * the default set of resource actions. The declaration should be created in the format of {@link 142 | * ng.$http#usage $http.config}: 143 | * 144 | * {action1: {method:?, params:?, isArray:?, headers:?, ...}, 145 | * action2: {method:?, params:?, isArray:?, headers:?, ...}, 146 | * ...} 147 | * 148 | * Where: 149 | * 150 | * - **`action`** – {string} – The name of action. This name becomes the name of the method on 151 | * your resource object. 152 | * - **`method`** – {string} – Case insensitive HTTP method (e.g. `GET`, `POST`, `PUT`, 153 | * `DELETE`, `JSONP`, etc). 154 | * - **`params`** – {Object=} – Optional set of pre-bound parameters for this action. If any of 155 | * the parameter value is a function, it will be called every time when a param value needs to 156 | * be obtained for a request (unless the param was overridden). The function will be passed the 157 | * current data value as an argument. 158 | * - **`url`** – {string} – action specific `url` override. The url templating is supported just 159 | * like for the resource-level urls. 160 | * - **`isArray`** – {boolean=} – If true then the returned object for this action is an array, 161 | * see `returns` section. 162 | * - **`transformRequest`** – 163 | * `{function(data, headersGetter)|Array.}` – 164 | * transform function or an array of such functions. The transform function takes the http 165 | * request body and headers and returns its transformed (typically serialized) version. 166 | * By default, transformRequest will contain one function that checks if the request data is 167 | * an object and serializes to using `angular.toJson`. To prevent this behavior, set 168 | * `transformRequest` to an empty array: `transformRequest: []` 169 | * - **`transformResponse`** – 170 | * `{function(data, headersGetter)|Array.}` – 171 | * transform function or an array of such functions. The transform function takes the http 172 | * response body and headers and returns its transformed (typically deserialized) version. 173 | * By default, transformResponse will contain one function that checks if the response looks 174 | * like a JSON string and deserializes it using `angular.fromJson`. To prevent this behavior, 175 | * set `transformResponse` to an empty array: `transformResponse: []` 176 | * - **`cache`** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the 177 | * GET request, otherwise if a cache instance built with 178 | * {@link ng.$cacheFactory $cacheFactory}, this cache will be used for 179 | * caching. 180 | * - **`timeout`** – `{number}` – timeout in milliseconds.
181 | * **Note:** In contrast to {@link ng.$http#usage $http.config}, {@link ng.$q promises} are 182 | * **not** supported in $resource, because the same value would be used for multiple requests. 183 | * If you are looking for a way to cancel requests, you should use the `cancellable` option. 184 | * - **`cancellable`** – `{boolean}` – if set to true, the request made by a "non-instance" call 185 | * will be cancelled (if not already completed) by calling `$cancelRequest()` on the call's 186 | * return value. Calling `$cancelRequest()` for a non-cancellable or an already 187 | * completed/cancelled request will have no effect.
188 | * - **`withCredentials`** - `{boolean}` - whether to set the `withCredentials` flag on the 189 | * XHR object. See 190 | * [requests with credentials](https://developer.mozilla.org/en/http_access_control#section_5) 191 | * for more information. 192 | * - **`responseType`** - `{string}` - see 193 | * [requestType](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType). 194 | * - **`interceptor`** - `{Object=}` - The interceptor object has two optional methods - 195 | * `response` and `responseError`. Both `response` and `responseError` interceptors get called 196 | * with `http response` object. See {@link ng.$http $http interceptors}. 197 | * 198 | * @param {Object} options Hash with custom settings that should extend the 199 | * default `$resourceProvider` behavior. The supported options are: 200 | * 201 | * - **`stripTrailingSlashes`** – {boolean} – If true then the trailing 202 | * slashes from any calculated URL will be stripped. (Defaults to true.) 203 | * - **`cancellable`** – {boolean} – If true, the request made by a "non-instance" call will be 204 | * cancelled (if not already completed) by calling `$cancelRequest()` on the call's return value. 205 | * This can be overwritten per action. (Defaults to false.) 206 | * 207 | * @returns {Object} A resource "class" object with methods for the default set of resource actions 208 | * optionally extended with custom `actions`. The default set contains these actions: 209 | * ```js 210 | * { 'get': {method:'GET'}, 211 | * 'save': {method:'POST'}, 212 | * 'query': {method:'GET', isArray:true}, 213 | * 'remove': {method:'DELETE'}, 214 | * 'delete': {method:'DELETE'} }; 215 | * ``` 216 | * 217 | * Calling these methods invoke an {@link ng.$http} with the specified http method, 218 | * destination and parameters. When the data is returned from the server then the object is an 219 | * instance of the resource class. The actions `save`, `remove` and `delete` are available on it 220 | * as methods with the `$` prefix. This allows you to easily perform CRUD operations (create, 221 | * read, update, delete) on server-side data like this: 222 | * ```js 223 | * var User = $resource('/user/:userId', {userId:'@id'}); 224 | * var user = User.get({userId:123}, function() { 225 | * user.abc = true; 226 | * user.$save(); 227 | * }); 228 | * ``` 229 | * 230 | * It is important to realize that invoking a $resource object method immediately returns an 231 | * empty reference (object or array depending on `isArray`). Once the data is returned from the 232 | * server the existing reference is populated with the actual data. This is a useful trick since 233 | * usually the resource is assigned to a model which is then rendered by the view. Having an empty 234 | * object results in no rendering, once the data arrives from the server then the object is 235 | * populated with the data and the view automatically re-renders itself showing the new data. This 236 | * means that in most cases one never has to write a callback function for the action methods. 237 | * 238 | * The action methods on the class object or instance object can be invoked with the following 239 | * parameters: 240 | * 241 | * - HTTP GET "class" actions: `Resource.action([parameters], [success], [error])` 242 | * - non-GET "class" actions: `Resource.action([parameters], postData, [success], [error])` 243 | * - non-GET instance actions: `instance.$action([parameters], [success], [error])` 244 | * 245 | * 246 | * Success callback is called with (value, responseHeaders) arguments, where the value is 247 | * the populated resource instance or collection object. The error callback is called 248 | * with (httpResponse) argument. 249 | * 250 | * Class actions return empty instance (with additional properties below). 251 | * Instance actions return promise of the action. 252 | * 253 | * The Resource instances and collections have these additional properties: 254 | * 255 | * - `$promise`: the {@link ng.$q promise} of the original server interaction that created this 256 | * instance or collection. 257 | * 258 | * On success, the promise is resolved with the same resource instance or collection object, 259 | * updated with data from server. This makes it easy to use in 260 | * {@link ngRoute.$routeProvider resolve section of $routeProvider.when()} to defer view 261 | * rendering until the resource(s) are loaded. 262 | * 263 | * On failure, the promise is rejected with the {@link ng.$http http response} object, without 264 | * the `resource` property. 265 | * 266 | * If an interceptor object was provided, the promise will instead be resolved with the value 267 | * returned by the interceptor. 268 | * 269 | * - `$resolved`: `true` after first server interaction is completed (either with success or 270 | * rejection), `false` before that. Knowing if the Resource has been resolved is useful in 271 | * data-binding. 272 | * 273 | * The Resource instances and collections have these additional methods: 274 | * 275 | * - `$cancelRequest`: If there is a cancellable, pending request related to the instance or 276 | * collection, calling this method will abort the request. 277 | * 278 | * The Resource instances have these additional methods: 279 | * 280 | * - `toJSON`: It returns a simple object without any of the extra properties added as part of 281 | * the Resource API. This object can be serialized through {@link angular.toJson} safely 282 | * without attaching Angular-specific fields. Notice that `JSON.stringify` (and 283 | * `angular.toJson`) automatically use this method when serializing a Resource instance 284 | * (see [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#toJSON()_behavior)). 285 | * 286 | * @example 287 | * 288 | * # Credit card resource 289 | * 290 | * ```js 291 | // Define CreditCard class 292 | var CreditCard = $resource('/user/:userId/card/:cardId', 293 | {userId:123, cardId:'@id'}, { 294 | charge: {method:'POST', params:{charge:true}} 295 | }); 296 | 297 | // We can retrieve a collection from the server 298 | var cards = CreditCard.query(function() { 299 | // GET: /user/123/card 300 | // server returns: [ {id:456, number:'1234', name:'Smith'} ]; 301 | 302 | var card = cards[0]; 303 | // each item is an instance of CreditCard 304 | expect(card instanceof CreditCard).toEqual(true); 305 | card.name = "J. Smith"; 306 | // non GET methods are mapped onto the instances 307 | card.$save(); 308 | // POST: /user/123/card/456 {id:456, number:'1234', name:'J. Smith'} 309 | // server returns: {id:456, number:'1234', name: 'J. Smith'}; 310 | 311 | // our custom method is mapped as well. 312 | card.$charge({amount:9.99}); 313 | // POST: /user/123/card/456?amount=9.99&charge=true {id:456, number:'1234', name:'J. Smith'} 314 | }); 315 | 316 | // we can create an instance as well 317 | var newCard = new CreditCard({number:'0123'}); 318 | newCard.name = "Mike Smith"; 319 | newCard.$save(); 320 | // POST: /user/123/card {number:'0123', name:'Mike Smith'} 321 | // server returns: {id:789, number:'0123', name: 'Mike Smith'}; 322 | expect(newCard.id).toEqual(789); 323 | * ``` 324 | * 325 | * The object returned from this function execution is a resource "class" which has "static" method 326 | * for each action in the definition. 327 | * 328 | * Calling these methods invoke `$http` on the `url` template with the given `method`, `params` and 329 | * `headers`. 330 | * 331 | * @example 332 | * 333 | * # User resource 334 | * 335 | * When the data is returned from the server then the object is an instance of the resource type and 336 | * all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD 337 | * operations (create, read, update, delete) on server-side data. 338 | 339 | ```js 340 | var User = $resource('/user/:userId', {userId:'@id'}); 341 | User.get({userId:123}, function(user) { 342 | user.abc = true; 343 | user.$save(); 344 | }); 345 | ``` 346 | * 347 | * It's worth noting that the success callback for `get`, `query` and other methods gets passed 348 | * in the response that came from the server as well as $http header getter function, so one 349 | * could rewrite the above example and get access to http headers as: 350 | * 351 | ```js 352 | var User = $resource('/user/:userId', {userId:'@id'}); 353 | User.get({userId:123}, function(user, getResponseHeaders){ 354 | user.abc = true; 355 | user.$save(function(user, putResponseHeaders) { 356 | //user => saved user object 357 | //putResponseHeaders => $http header getter 358 | }); 359 | }); 360 | ``` 361 | * 362 | * You can also access the raw `$http` promise via the `$promise` property on the object returned 363 | * 364 | ``` 365 | var User = $resource('/user/:userId', {userId:'@id'}); 366 | User.get({userId:123}) 367 | .$promise.then(function(user) { 368 | $scope.user = user; 369 | }); 370 | ``` 371 | * 372 | * @example 373 | * 374 | * # Creating a custom 'PUT' request 375 | * 376 | * In this example we create a custom method on our resource to make a PUT request 377 | * ```js 378 | * var app = angular.module('app', ['ngResource', 'ngRoute']); 379 | * 380 | * // Some APIs expect a PUT request in the format URL/object/ID 381 | * // Here we are creating an 'update' method 382 | * app.factory('Notes', ['$resource', function($resource) { 383 | * return $resource('/notes/:id', null, 384 | * { 385 | * 'update': { method:'PUT' } 386 | * }); 387 | * }]); 388 | * 389 | * // In our controller we get the ID from the URL using ngRoute and $routeParams 390 | * // We pass in $routeParams and our Notes factory along with $scope 391 | * app.controller('NotesCtrl', ['$scope', '$routeParams', 'Notes', 392 | function($scope, $routeParams, Notes) { 393 | * // First get a note object from the factory 394 | * var note = Notes.get({ id:$routeParams.id }); 395 | * $id = note.id; 396 | * 397 | * // Now call update passing in the ID first then the object you are updating 398 | * Notes.update({ id:$id }, note); 399 | * 400 | * // This will PUT /notes/ID with the note object in the request payload 401 | * }]); 402 | * ``` 403 | * 404 | * @example 405 | * 406 | * # Cancelling requests 407 | * 408 | * If an action's configuration specifies that it is cancellable, you can cancel the request related 409 | * to an instance or collection (as long as it is a result of a "non-instance" call): 410 | * 411 | ```js 412 | // ...defining the `Hotel` resource... 413 | var Hotel = $resource('/api/hotel/:id', {id: '@id'}, { 414 | // Let's make the `query()` method cancellable 415 | query: {method: 'get', isArray: true, cancellable: true} 416 | }); 417 | 418 | // ...somewhere in the PlanVacationController... 419 | ... 420 | this.onDestinationChanged = function onDestinationChanged(destination) { 421 | // We don't care about any pending request for hotels 422 | // in a different destination any more 423 | this.availableHotels.$cancelRequest(); 424 | 425 | // Let's query for hotels in '' 426 | // (calls: /api/hotel?location=) 427 | this.availableHotels = Hotel.query({location: destination}); 428 | }; 429 | ``` 430 | * 431 | */ 432 | angular.module('ngResource', ['ng']). 433 | provider('$resource', function() { 434 | var PROTOCOL_AND_DOMAIN_REGEX = /^https?:\/\/[^\/]*/; 435 | var provider = this; 436 | 437 | /** 438 | * @ngdoc property 439 | * @name $resourceProvider#defaults 440 | * @description 441 | * Object containing default options used when creating `$resource` instances. 442 | * 443 | * The default values satisfy a wide range of usecases, but you may choose to overwrite any of 444 | * them to further customize your instances. The available properties are: 445 | * 446 | * - **stripTrailingSlashes** – `{boolean}` – If true, then the trailing slashes from any 447 | * calculated URL will be stripped.
448 | * (Defaults to true.) 449 | * - **cancellable** – `{boolean}` – If true, the request made by a "non-instance" call will be 450 | * cancelled (if not already completed) by calling `$cancelRequest()` on the call's return 451 | * value. For more details, see {@link ngResource.$resource}. This can be overwritten per 452 | * resource class or action.
453 | * (Defaults to false.) 454 | * - **actions** - `{Object.}` - A hash with default actions declarations. Actions are 455 | * high-level methods corresponding to RESTful actions/methods on resources. An action may 456 | * specify what HTTP method to use, what URL to hit, if the return value will be a single 457 | * object or a collection (array) of objects etc. For more details, see 458 | * {@link ngResource.$resource}. The actions can also be enhanced or overwritten per resource 459 | * class.
460 | * The default actions are: 461 | * ```js 462 | * { 463 | * get: {method: 'GET'}, 464 | * save: {method: 'POST'}, 465 | * query: {method: 'GET', isArray: true}, 466 | * remove: {method: 'DELETE'}, 467 | * delete: {method: 'DELETE'} 468 | * } 469 | * ``` 470 | * 471 | * #### Example 472 | * 473 | * For example, you can specify a new `update` action that uses the `PUT` HTTP verb: 474 | * 475 | * ```js 476 | * angular. 477 | * module('myApp'). 478 | * config(['resourceProvider', function ($resourceProvider) { 479 | * $resourceProvider.defaults.actions.update = { 480 | * method: 'PUT' 481 | * }; 482 | * }); 483 | * ``` 484 | * 485 | * Or you can even overwrite the whole `actions` list and specify your own: 486 | * 487 | * ```js 488 | * angular. 489 | * module('myApp'). 490 | * config(['resourceProvider', function ($resourceProvider) { 491 | * $resourceProvider.defaults.actions = { 492 | * create: {method: 'POST'} 493 | * get: {method: 'GET'}, 494 | * getAll: {method: 'GET', isArray:true}, 495 | * update: {method: 'PUT'}, 496 | * delete: {method: 'DELETE'} 497 | * }; 498 | * }); 499 | * ``` 500 | * 501 | */ 502 | this.defaults = { 503 | // Strip slashes by default 504 | stripTrailingSlashes: true, 505 | 506 | // Make non-instance requests cancellable (via `$cancelRequest()`) 507 | cancellable: false, 508 | 509 | // Default actions configuration 510 | actions: { 511 | 'get': {method: 'GET'}, 512 | 'save': {method: 'POST'}, 513 | 'query': {method: 'GET', isArray: true}, 514 | 'remove': {method: 'DELETE'}, 515 | 'delete': {method: 'DELETE'} 516 | } 517 | }; 518 | 519 | this.$get = ['$http', '$log', '$q', '$timeout', function($http, $log, $q, $timeout) { 520 | 521 | var noop = angular.noop, 522 | forEach = angular.forEach, 523 | extend = angular.extend, 524 | copy = angular.copy, 525 | isFunction = angular.isFunction; 526 | 527 | /** 528 | * We need our custom method because encodeURIComponent is too aggressive and doesn't follow 529 | * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set 530 | * (pchar) allowed in path segments: 531 | * segment = *pchar 532 | * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" 533 | * pct-encoded = "%" HEXDIG HEXDIG 534 | * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 535 | * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" 536 | * / "*" / "+" / "," / ";" / "=" 537 | */ 538 | function encodeUriSegment(val) { 539 | return encodeUriQuery(val, true). 540 | replace(/%26/gi, '&'). 541 | replace(/%3D/gi, '='). 542 | replace(/%2B/gi, '+'); 543 | } 544 | 545 | 546 | /** 547 | * This method is intended for encoding *key* or *value* parts of query component. We need a 548 | * custom method because encodeURIComponent is too aggressive and encodes stuff that doesn't 549 | * have to be encoded per http://tools.ietf.org/html/rfc3986: 550 | * query = *( pchar / "/" / "?" ) 551 | * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" 552 | * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 553 | * pct-encoded = "%" HEXDIG HEXDIG 554 | * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" 555 | * / "*" / "+" / "," / ";" / "=" 556 | */ 557 | function encodeUriQuery(val, pctEncodeSpaces) { 558 | return encodeURIComponent(val). 559 | replace(/%40/gi, '@'). 560 | replace(/%3A/gi, ':'). 561 | replace(/%24/g, '$'). 562 | replace(/%2C/gi, ','). 563 | replace(/%20/g, (pctEncodeSpaces ? '%20' : '+')); 564 | } 565 | 566 | function Route(template, defaults) { 567 | this.template = template; 568 | this.defaults = extend({}, provider.defaults, defaults); 569 | this.urlParams = {}; 570 | } 571 | 572 | Route.prototype = { 573 | setUrlParams: function(config, params, actionUrl) { 574 | var self = this, 575 | url = actionUrl || self.template, 576 | val, 577 | encodedVal, 578 | protocolAndDomain = ''; 579 | 580 | var urlParams = self.urlParams = {}; 581 | forEach(url.split(/\W/), function(param) { 582 | if (param === 'hasOwnProperty') { 583 | throw $resourceMinErr('badname', "hasOwnProperty is not a valid parameter name."); 584 | } 585 | if (!(new RegExp("^\\d+$").test(param)) && param && 586 | (new RegExp("(^|[^\\\\]):" + param + "(\\W|$)").test(url))) { 587 | urlParams[param] = { 588 | isQueryParamValue: (new RegExp("\\?.*=:" + param + "(?:\\W|$)")).test(url) 589 | }; 590 | } 591 | }); 592 | url = url.replace(/\\:/g, ':'); 593 | url = url.replace(PROTOCOL_AND_DOMAIN_REGEX, function(match) { 594 | protocolAndDomain = match; 595 | return ''; 596 | }); 597 | 598 | params = params || {}; 599 | forEach(self.urlParams, function(paramInfo, urlParam) { 600 | val = params.hasOwnProperty(urlParam) ? params[urlParam] : self.defaults[urlParam]; 601 | if (angular.isDefined(val) && val !== null) { 602 | if (paramInfo.isQueryParamValue) { 603 | encodedVal = encodeUriQuery(val, true); 604 | } else { 605 | encodedVal = encodeUriSegment(val); 606 | } 607 | url = url.replace(new RegExp(":" + urlParam + "(\\W|$)", "g"), function(match, p1) { 608 | return encodedVal + p1; 609 | }); 610 | } else { 611 | url = url.replace(new RegExp("(\/?):" + urlParam + "(\\W|$)", "g"), function(match, 612 | leadingSlashes, tail) { 613 | if (tail.charAt(0) == '/') { 614 | return tail; 615 | } else { 616 | return leadingSlashes + tail; 617 | } 618 | }); 619 | } 620 | }); 621 | 622 | // strip trailing slashes and set the url (unless this behavior is specifically disabled) 623 | if (self.defaults.stripTrailingSlashes) { 624 | url = url.replace(/\/+$/, '') || '/'; 625 | } 626 | 627 | // then replace collapse `/.` if found in the last URL path segment before the query 628 | // E.g. `http://url.com/id./format?q=x` becomes `http://url.com/id.format?q=x` 629 | url = url.replace(/\/\.(?=\w+($|\?))/, '.'); 630 | // replace escaped `/\.` with `/.` 631 | config.url = protocolAndDomain + url.replace(/\/\\\./, '/.'); 632 | 633 | 634 | // set params - delegate param encoding to $http 635 | forEach(params, function(value, key) { 636 | if (!self.urlParams[key]) { 637 | config.params = config.params || {}; 638 | config.params[key] = value; 639 | } 640 | }); 641 | } 642 | }; 643 | 644 | 645 | function resourceFactory(url, paramDefaults, actions, options) { 646 | var route = new Route(url, options); 647 | 648 | actions = extend({}, provider.defaults.actions, actions); 649 | 650 | function extractParams(data, actionParams) { 651 | var ids = {}; 652 | actionParams = extend({}, paramDefaults, actionParams); 653 | forEach(actionParams, function(value, key) { 654 | if (isFunction(value)) { value = value(data); } 655 | ids[key] = value && value.charAt && value.charAt(0) == '@' ? 656 | lookupDottedPath(data, value.substr(1)) : value; 657 | }); 658 | return ids; 659 | } 660 | 661 | function defaultResponseInterceptor(response) { 662 | return response.resource; 663 | } 664 | 665 | function Resource(value) { 666 | shallowClearAndCopy(value || {}, this); 667 | } 668 | 669 | Resource.prototype.toJSON = function() { 670 | var data = extend({}, this); 671 | delete data.$promise; 672 | delete data.$resolved; 673 | return data; 674 | }; 675 | 676 | forEach(actions, function(action, name) { 677 | var hasBody = /^(POST|PUT|PATCH)$/i.test(action.method); 678 | var numericTimeout = action.timeout; 679 | var cancellable = angular.isDefined(action.cancellable) ? action.cancellable : 680 | (options && angular.isDefined(options.cancellable)) ? options.cancellable : 681 | provider.defaults.cancellable; 682 | 683 | if (numericTimeout && !angular.isNumber(numericTimeout)) { 684 | $log.debug('ngResource:\n' + 685 | ' Only numeric values are allowed as `timeout`.\n' + 686 | ' Promises are not supported in $resource, because the same value would ' + 687 | 'be used for multiple requests. If you are looking for a way to cancel ' + 688 | 'requests, you should use the `cancellable` option.'); 689 | delete action.timeout; 690 | numericTimeout = null; 691 | } 692 | 693 | Resource[name] = function(a1, a2, a3, a4) { 694 | var params = {}, data, success, error; 695 | 696 | /* jshint -W086 */ /* (purposefully fall through case statements) */ 697 | switch (arguments.length) { 698 | case 4: 699 | error = a4; 700 | success = a3; 701 | //fallthrough 702 | case 3: 703 | case 2: 704 | if (isFunction(a2)) { 705 | if (isFunction(a1)) { 706 | success = a1; 707 | error = a2; 708 | break; 709 | } 710 | 711 | success = a2; 712 | error = a3; 713 | //fallthrough 714 | } else { 715 | params = a1; 716 | data = a2; 717 | success = a3; 718 | break; 719 | } 720 | case 1: 721 | if (isFunction(a1)) success = a1; 722 | else if (hasBody) data = a1; 723 | else params = a1; 724 | break; 725 | case 0: break; 726 | default: 727 | throw $resourceMinErr('badargs', 728 | "Expected up to 4 arguments [params, data, success, error], got {0} arguments", 729 | arguments.length); 730 | } 731 | /* jshint +W086 */ /* (purposefully fall through case statements) */ 732 | 733 | var isInstanceCall = this instanceof Resource; 734 | var value = isInstanceCall ? data : (action.isArray ? [] : new Resource(data)); 735 | var httpConfig = {}; 736 | var responseInterceptor = action.interceptor && action.interceptor.response || 737 | defaultResponseInterceptor; 738 | var responseErrorInterceptor = action.interceptor && action.interceptor.responseError || 739 | undefined; 740 | var timeoutDeferred; 741 | var numericTimeoutPromise; 742 | 743 | forEach(action, function(value, key) { 744 | switch (key) { 745 | default: 746 | httpConfig[key] = copy(value); 747 | break; 748 | case 'params': 749 | case 'isArray': 750 | case 'interceptor': 751 | case 'cancellable': 752 | break; 753 | } 754 | }); 755 | 756 | if (!isInstanceCall && cancellable) { 757 | timeoutDeferred = $q.defer(); 758 | httpConfig.timeout = timeoutDeferred.promise; 759 | 760 | if (numericTimeout) { 761 | numericTimeoutPromise = $timeout(timeoutDeferred.resolve, numericTimeout); 762 | } 763 | } 764 | 765 | if (hasBody) httpConfig.data = data; 766 | route.setUrlParams(httpConfig, 767 | extend({}, extractParams(data, action.params || {}), params), 768 | action.url); 769 | 770 | var promise = $http(httpConfig).then(function(response) { 771 | var data = response.data; 772 | 773 | if (data) { 774 | // Need to convert action.isArray to boolean in case it is undefined 775 | // jshint -W018 776 | if (angular.isArray(data) !== (!!action.isArray)) { 777 | throw $resourceMinErr('badcfg', 778 | 'Error in resource configuration for action `{0}`. Expected response to ' + 779 | 'contain an {1} but got an {2} (Request: {3} {4})', name, action.isArray ? 'array' : 'object', 780 | angular.isArray(data) ? 'array' : 'object', httpConfig.method, httpConfig.url); 781 | } 782 | // jshint +W018 783 | if (action.isArray) { 784 | value.length = 0; 785 | forEach(data, function(item) { 786 | if (typeof item === "object") { 787 | value.push(new Resource(item)); 788 | } else { 789 | // Valid JSON values may be string literals, and these should not be converted 790 | // into objects. These items will not have access to the Resource prototype 791 | // methods, but unfortunately there 792 | value.push(item); 793 | } 794 | }); 795 | } else { 796 | var promise = value.$promise; // Save the promise 797 | shallowClearAndCopy(data, value); 798 | value.$promise = promise; // Restore the promise 799 | } 800 | } 801 | response.resource = value; 802 | 803 | return response; 804 | }, function(response) { 805 | (error || noop)(response); 806 | return $q.reject(response); 807 | }); 808 | 809 | promise['finally'](function() { 810 | value.$resolved = true; 811 | if (!isInstanceCall && cancellable) { 812 | value.$cancelRequest = angular.noop; 813 | $timeout.cancel(numericTimeoutPromise); 814 | timeoutDeferred = numericTimeoutPromise = httpConfig.timeout = null; 815 | } 816 | }); 817 | 818 | promise = promise.then( 819 | function(response) { 820 | var value = responseInterceptor(response); 821 | (success || noop)(value, response.headers); 822 | return value; 823 | }, 824 | responseErrorInterceptor); 825 | 826 | if (!isInstanceCall) { 827 | // we are creating instance / collection 828 | // - set the initial promise 829 | // - return the instance / collection 830 | value.$promise = promise; 831 | value.$resolved = false; 832 | if (cancellable) value.$cancelRequest = timeoutDeferred.resolve; 833 | 834 | return value; 835 | } 836 | 837 | // instance call 838 | return promise; 839 | }; 840 | 841 | 842 | Resource.prototype['$' + name] = function(params, success, error) { 843 | if (isFunction(params)) { 844 | error = success; success = params; params = {}; 845 | } 846 | var result = Resource[name].call(this, params, this, success, error); 847 | return result.$promise || result; 848 | }; 849 | }); 850 | 851 | Resource.bind = function(additionalParamDefaults) { 852 | return resourceFactory(url, extend({}, paramDefaults, additionalParamDefaults), actions); 853 | }; 854 | 855 | return Resource; 856 | } 857 | 858 | return resourceFactory; 859 | }]; 860 | }); 861 | 862 | 863 | })(window, window.angular); 864 | -------------------------------------------------------------------------------- /web-app/app/lib/angular/angular-route.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.5.8 3 | * (c) 2010-2016 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | (function(window, angular) {'use strict'; 7 | 8 | /* global shallowCopy: true */ 9 | 10 | /** 11 | * Creates a shallow copy of an object, an array or a primitive. 12 | * 13 | * Assumes that there are no proto properties for objects. 14 | */ 15 | function shallowCopy(src, dst) { 16 | if (isArray(src)) { 17 | dst = dst || []; 18 | 19 | for (var i = 0, ii = src.length; i < ii; i++) { 20 | dst[i] = src[i]; 21 | } 22 | } else if (isObject(src)) { 23 | dst = dst || {}; 24 | 25 | for (var key in src) { 26 | if (!(key.charAt(0) === '$' && key.charAt(1) === '$')) { 27 | dst[key] = src[key]; 28 | } 29 | } 30 | } 31 | 32 | return dst || src; 33 | } 34 | 35 | /* global shallowCopy: false */ 36 | 37 | // There are necessary for `shallowCopy()` (included via `src/shallowCopy.js`). 38 | // They are initialized inside the `$RouteProvider`, to ensure `window.angular` is available. 39 | var isArray; 40 | var isObject; 41 | 42 | /** 43 | * @ngdoc module 44 | * @name ngRoute 45 | * @description 46 | * 47 | * # ngRoute 48 | * 49 | * The `ngRoute` module provides routing and deeplinking services and directives for angular apps. 50 | * 51 | * ## Example 52 | * See {@link ngRoute.$route#example $route} for an example of configuring and using `ngRoute`. 53 | * 54 | * 55 | *
56 | */ 57 | /* global -ngRouteModule */ 58 | var ngRouteModule = angular.module('ngRoute', ['ng']). 59 | provider('$route', $RouteProvider), 60 | $routeMinErr = angular.$$minErr('ngRoute'); 61 | 62 | /** 63 | * @ngdoc provider 64 | * @name $routeProvider 65 | * 66 | * @description 67 | * 68 | * Used for configuring routes. 69 | * 70 | * ## Example 71 | * See {@link ngRoute.$route#example $route} for an example of configuring and using `ngRoute`. 72 | * 73 | * ## Dependencies 74 | * Requires the {@link ngRoute `ngRoute`} module to be installed. 75 | */ 76 | function $RouteProvider() { 77 | isArray = angular.isArray; 78 | isObject = angular.isObject; 79 | 80 | function inherit(parent, extra) { 81 | return angular.extend(Object.create(parent), extra); 82 | } 83 | 84 | var routes = {}; 85 | 86 | /** 87 | * @ngdoc method 88 | * @name $routeProvider#when 89 | * 90 | * @param {string} path Route path (matched against `$location.path`). If `$location.path` 91 | * contains redundant trailing slash or is missing one, the route will still match and the 92 | * `$location.path` will be updated to add or drop the trailing slash to exactly match the 93 | * route definition. 94 | * 95 | * * `path` can contain named groups starting with a colon: e.g. `:name`. All characters up 96 | * to the next slash are matched and stored in `$routeParams` under the given `name` 97 | * when the route matches. 98 | * * `path` can contain named groups starting with a colon and ending with a star: 99 | * e.g.`:name*`. All characters are eagerly stored in `$routeParams` under the given `name` 100 | * when the route matches. 101 | * * `path` can contain optional named groups with a question mark: e.g.`:name?`. 102 | * 103 | * For example, routes like `/color/:color/largecode/:largecode*\/edit` will match 104 | * `/color/brown/largecode/code/with/slashes/edit` and extract: 105 | * 106 | * * `color: brown` 107 | * * `largecode: code/with/slashes`. 108 | * 109 | * 110 | * @param {Object} route Mapping information to be assigned to `$route.current` on route 111 | * match. 112 | * 113 | * Object properties: 114 | * 115 | * - `controller` – `{(string|function()=}` – Controller fn that should be associated with 116 | * newly created scope or the name of a {@link angular.Module#controller registered 117 | * controller} if passed as a string. 118 | * - `controllerAs` – `{string=}` – An identifier name for a reference to the controller. 119 | * If present, the controller will be published to scope under the `controllerAs` name. 120 | * - `template` – `{string=|function()=}` – html template as a string or a function that 121 | * returns an html template as a string which should be used by {@link 122 | * ngRoute.directive:ngView ngView} or {@link ng.directive:ngInclude ngInclude} directives. 123 | * This property takes precedence over `templateUrl`. 124 | * 125 | * If `template` is a function, it will be called with the following parameters: 126 | * 127 | * - `{Array.}` - route parameters extracted from the current 128 | * `$location.path()` by applying the current route 129 | * 130 | * - `templateUrl` – `{string=|function()=}` – path or function that returns a path to an html 131 | * template that should be used by {@link ngRoute.directive:ngView ngView}. 132 | * 133 | * If `templateUrl` is a function, it will be called with the following parameters: 134 | * 135 | * - `{Array.}` - route parameters extracted from the current 136 | * `$location.path()` by applying the current route 137 | * 138 | * - `resolve` - `{Object.=}` - An optional map of dependencies which should 139 | * be injected into the controller. If any of these dependencies are promises, the router 140 | * will wait for them all to be resolved or one to be rejected before the controller is 141 | * instantiated. 142 | * If all the promises are resolved successfully, the values of the resolved promises are 143 | * injected and {@link ngRoute.$route#$routeChangeSuccess $routeChangeSuccess} event is 144 | * fired. If any of the promises are rejected the 145 | * {@link ngRoute.$route#$routeChangeError $routeChangeError} event is fired. 146 | * For easier access to the resolved dependencies from the template, the `resolve` map will 147 | * be available on the scope of the route, under `$resolve` (by default) or a custom name 148 | * specified by the `resolveAs` property (see below). This can be particularly useful, when 149 | * working with {@link angular.Module#component components} as route templates.
150 | *
151 | * **Note:** If your scope already contains a property with this name, it will be hidden 152 | * or overwritten. Make sure, you specify an appropriate name for this property, that 153 | * does not collide with other properties on the scope. 154 | *
155 | * The map object is: 156 | * 157 | * - `key` – `{string}`: a name of a dependency to be injected into the controller. 158 | * - `factory` - `{string|function}`: If `string` then it is an alias for a service. 159 | * Otherwise if function, then it is {@link auto.$injector#invoke injected} 160 | * and the return value is treated as the dependency. If the result is a promise, it is 161 | * resolved before its value is injected into the controller. Be aware that 162 | * `ngRoute.$routeParams` will still refer to the previous route within these resolve 163 | * functions. Use `$route.current.params` to access the new route parameters, instead. 164 | * 165 | * - `resolveAs` - `{string=}` - The name under which the `resolve` map will be available on 166 | * the scope of the route. If omitted, defaults to `$resolve`. 167 | * 168 | * - `redirectTo` – `{(string|function())=}` – value to update 169 | * {@link ng.$location $location} path with and trigger route redirection. 170 | * 171 | * If `redirectTo` is a function, it will be called with the following parameters: 172 | * 173 | * - `{Object.}` - route parameters extracted from the current 174 | * `$location.path()` by applying the current route templateUrl. 175 | * - `{string}` - current `$location.path()` 176 | * - `{Object}` - current `$location.search()` 177 | * 178 | * The custom `redirectTo` function is expected to return a string which will be used 179 | * to update `$location.path()` and `$location.search()`. 180 | * 181 | * - `[reloadOnSearch=true]` - `{boolean=}` - reload route when only `$location.search()` 182 | * or `$location.hash()` changes. 183 | * 184 | * If the option is set to `false` and url in the browser changes, then 185 | * `$routeUpdate` event is broadcasted on the root scope. 186 | * 187 | * - `[caseInsensitiveMatch=false]` - `{boolean=}` - match routes without being case sensitive 188 | * 189 | * If the option is set to `true`, then the particular route can be matched without being 190 | * case sensitive 191 | * 192 | * @returns {Object} self 193 | * 194 | * @description 195 | * Adds a new route definition to the `$route` service. 196 | */ 197 | this.when = function(path, route) { 198 | //copy original route object to preserve params inherited from proto chain 199 | var routeCopy = shallowCopy(route); 200 | if (angular.isUndefined(routeCopy.reloadOnSearch)) { 201 | routeCopy.reloadOnSearch = true; 202 | } 203 | if (angular.isUndefined(routeCopy.caseInsensitiveMatch)) { 204 | routeCopy.caseInsensitiveMatch = this.caseInsensitiveMatch; 205 | } 206 | routes[path] = angular.extend( 207 | routeCopy, 208 | path && pathRegExp(path, routeCopy) 209 | ); 210 | 211 | // create redirection for trailing slashes 212 | if (path) { 213 | var redirectPath = (path[path.length - 1] == '/') 214 | ? path.substr(0, path.length - 1) 215 | : path + '/'; 216 | 217 | routes[redirectPath] = angular.extend( 218 | {redirectTo: path}, 219 | pathRegExp(redirectPath, routeCopy) 220 | ); 221 | } 222 | 223 | return this; 224 | }; 225 | 226 | /** 227 | * @ngdoc property 228 | * @name $routeProvider#caseInsensitiveMatch 229 | * @description 230 | * 231 | * A boolean property indicating if routes defined 232 | * using this provider should be matched using a case insensitive 233 | * algorithm. Defaults to `false`. 234 | */ 235 | this.caseInsensitiveMatch = false; 236 | 237 | /** 238 | * @param path {string} path 239 | * @param opts {Object} options 240 | * @return {?Object} 241 | * 242 | * @description 243 | * Normalizes the given path, returning a regular expression 244 | * and the original path. 245 | * 246 | * Inspired by pathRexp in visionmedia/express/lib/utils.js. 247 | */ 248 | function pathRegExp(path, opts) { 249 | var insensitive = opts.caseInsensitiveMatch, 250 | ret = { 251 | originalPath: path, 252 | regexp: path 253 | }, 254 | keys = ret.keys = []; 255 | 256 | path = path 257 | .replace(/([().])/g, '\\$1') 258 | .replace(/(\/)?:(\w+)(\*\?|[\?\*])?/g, function(_, slash, key, option) { 259 | var optional = (option === '?' || option === '*?') ? '?' : null; 260 | var star = (option === '*' || option === '*?') ? '*' : null; 261 | keys.push({ name: key, optional: !!optional }); 262 | slash = slash || ''; 263 | return '' 264 | + (optional ? '' : slash) 265 | + '(?:' 266 | + (optional ? slash : '') 267 | + (star && '(.+?)' || '([^/]+)') 268 | + (optional || '') 269 | + ')' 270 | + (optional || ''); 271 | }) 272 | .replace(/([\/$\*])/g, '\\$1'); 273 | 274 | ret.regexp = new RegExp('^' + path + '$', insensitive ? 'i' : ''); 275 | return ret; 276 | } 277 | 278 | /** 279 | * @ngdoc method 280 | * @name $routeProvider#otherwise 281 | * 282 | * @description 283 | * Sets route definition that will be used on route change when no other route definition 284 | * is matched. 285 | * 286 | * @param {Object|string} params Mapping information to be assigned to `$route.current`. 287 | * If called with a string, the value maps to `redirectTo`. 288 | * @returns {Object} self 289 | */ 290 | this.otherwise = function(params) { 291 | if (typeof params === 'string') { 292 | params = {redirectTo: params}; 293 | } 294 | this.when(null, params); 295 | return this; 296 | }; 297 | 298 | 299 | this.$get = ['$rootScope', 300 | '$location', 301 | '$routeParams', 302 | '$q', 303 | '$injector', 304 | '$templateRequest', 305 | '$sce', 306 | function($rootScope, $location, $routeParams, $q, $injector, $templateRequest, $sce) { 307 | 308 | /** 309 | * @ngdoc service 310 | * @name $route 311 | * @requires $location 312 | * @requires $routeParams 313 | * 314 | * @property {Object} current Reference to the current route definition. 315 | * The route definition contains: 316 | * 317 | * - `controller`: The controller constructor as defined in the route definition. 318 | * - `locals`: A map of locals which is used by {@link ng.$controller $controller} service for 319 | * controller instantiation. The `locals` contain 320 | * the resolved values of the `resolve` map. Additionally the `locals` also contain: 321 | * 322 | * - `$scope` - The current route scope. 323 | * - `$template` - The current route template HTML. 324 | * 325 | * The `locals` will be assigned to the route scope's `$resolve` property. You can override 326 | * the property name, using `resolveAs` in the route definition. See 327 | * {@link ngRoute.$routeProvider $routeProvider} for more info. 328 | * 329 | * @property {Object} routes Object with all route configuration Objects as its properties. 330 | * 331 | * @description 332 | * `$route` is used for deep-linking URLs to controllers and views (HTML partials). 333 | * It watches `$location.url()` and tries to map the path to an existing route definition. 334 | * 335 | * Requires the {@link ngRoute `ngRoute`} module to be installed. 336 | * 337 | * You can define routes through {@link ngRoute.$routeProvider $routeProvider}'s API. 338 | * 339 | * The `$route` service is typically used in conjunction with the 340 | * {@link ngRoute.directive:ngView `ngView`} directive and the 341 | * {@link ngRoute.$routeParams `$routeParams`} service. 342 | * 343 | * @example 344 | * This example shows how changing the URL hash causes the `$route` to match a route against the 345 | * URL, and the `ngView` pulls in the partial. 346 | * 347 | * 349 | * 350 | *
351 | * Choose: 352 | * Moby | 353 | * Moby: Ch1 | 354 | * Gatsby | 355 | * Gatsby: Ch4 | 356 | * Scarlet Letter
357 | * 358 | *
359 | * 360 | *
361 | * 362 | *
$location.path() = {{$location.path()}}
363 | *
$route.current.templateUrl = {{$route.current.templateUrl}}
364 | *
$route.current.params = {{$route.current.params}}
365 | *
$route.current.scope.name = {{$route.current.scope.name}}
366 | *
$routeParams = {{$routeParams}}
367 | *
368 | *
369 | * 370 | * 371 | * controller: {{name}}
372 | * Book Id: {{params.bookId}}
373 | *
374 | * 375 | * 376 | * controller: {{name}}
377 | * Book Id: {{params.bookId}}
378 | * Chapter Id: {{params.chapterId}} 379 | *
380 | * 381 | * 382 | * angular.module('ngRouteExample', ['ngRoute']) 383 | * 384 | * .controller('MainController', function($scope, $route, $routeParams, $location) { 385 | * $scope.$route = $route; 386 | * $scope.$location = $location; 387 | * $scope.$routeParams = $routeParams; 388 | * }) 389 | * 390 | * .controller('BookController', function($scope, $routeParams) { 391 | * $scope.name = "BookController"; 392 | * $scope.params = $routeParams; 393 | * }) 394 | * 395 | * .controller('ChapterController', function($scope, $routeParams) { 396 | * $scope.name = "ChapterController"; 397 | * $scope.params = $routeParams; 398 | * }) 399 | * 400 | * .config(function($routeProvider, $locationProvider) { 401 | * $routeProvider 402 | * .when('/Book/:bookId', { 403 | * templateUrl: 'book.html', 404 | * controller: 'BookController', 405 | * resolve: { 406 | * // I will cause a 1 second delay 407 | * delay: function($q, $timeout) { 408 | * var delay = $q.defer(); 409 | * $timeout(delay.resolve, 1000); 410 | * return delay.promise; 411 | * } 412 | * } 413 | * }) 414 | * .when('/Book/:bookId/ch/:chapterId', { 415 | * templateUrl: 'chapter.html', 416 | * controller: 'ChapterController' 417 | * }); 418 | * 419 | * // configure html5 to get links working on jsfiddle 420 | * $locationProvider.html5Mode(true); 421 | * }); 422 | * 423 | * 424 | * 425 | * 426 | * it('should load and compile correct template', function() { 427 | * element(by.linkText('Moby: Ch1')).click(); 428 | * var content = element(by.css('[ng-view]')).getText(); 429 | * expect(content).toMatch(/controller\: ChapterController/); 430 | * expect(content).toMatch(/Book Id\: Moby/); 431 | * expect(content).toMatch(/Chapter Id\: 1/); 432 | * 433 | * element(by.partialLinkText('Scarlet')).click(); 434 | * 435 | * content = element(by.css('[ng-view]')).getText(); 436 | * expect(content).toMatch(/controller\: BookController/); 437 | * expect(content).toMatch(/Book Id\: Scarlet/); 438 | * }); 439 | * 440 | *
441 | */ 442 | 443 | /** 444 | * @ngdoc event 445 | * @name $route#$routeChangeStart 446 | * @eventType broadcast on root scope 447 | * @description 448 | * Broadcasted before a route change. At this point the route services starts 449 | * resolving all of the dependencies needed for the route change to occur. 450 | * Typically this involves fetching the view template as well as any dependencies 451 | * defined in `resolve` route property. Once all of the dependencies are resolved 452 | * `$routeChangeSuccess` is fired. 453 | * 454 | * The route change (and the `$location` change that triggered it) can be prevented 455 | * by calling `preventDefault` method of the event. See {@link ng.$rootScope.Scope#$on} 456 | * for more details about event object. 457 | * 458 | * @param {Object} angularEvent Synthetic event object. 459 | * @param {Route} next Future route information. 460 | * @param {Route} current Current route information. 461 | */ 462 | 463 | /** 464 | * @ngdoc event 465 | * @name $route#$routeChangeSuccess 466 | * @eventType broadcast on root scope 467 | * @description 468 | * Broadcasted after a route change has happened successfully. 469 | * The `resolve` dependencies are now available in the `current.locals` property. 470 | * 471 | * {@link ngRoute.directive:ngView ngView} listens for the directive 472 | * to instantiate the controller and render the view. 473 | * 474 | * @param {Object} angularEvent Synthetic event object. 475 | * @param {Route} current Current route information. 476 | * @param {Route|Undefined} previous Previous route information, or undefined if current is 477 | * first route entered. 478 | */ 479 | 480 | /** 481 | * @ngdoc event 482 | * @name $route#$routeChangeError 483 | * @eventType broadcast on root scope 484 | * @description 485 | * Broadcasted if any of the resolve promises are rejected. 486 | * 487 | * @param {Object} angularEvent Synthetic event object 488 | * @param {Route} current Current route information. 489 | * @param {Route} previous Previous route information. 490 | * @param {Route} rejection Rejection of the promise. Usually the error of the failed promise. 491 | */ 492 | 493 | /** 494 | * @ngdoc event 495 | * @name $route#$routeUpdate 496 | * @eventType broadcast on root scope 497 | * @description 498 | * The `reloadOnSearch` property has been set to false, and we are reusing the same 499 | * instance of the Controller. 500 | * 501 | * @param {Object} angularEvent Synthetic event object 502 | * @param {Route} current Current/previous route information. 503 | */ 504 | 505 | var forceReload = false, 506 | preparedRoute, 507 | preparedRouteIsUpdateOnly, 508 | $route = { 509 | routes: routes, 510 | 511 | /** 512 | * @ngdoc method 513 | * @name $route#reload 514 | * 515 | * @description 516 | * Causes `$route` service to reload the current route even if 517 | * {@link ng.$location $location} hasn't changed. 518 | * 519 | * As a result of that, {@link ngRoute.directive:ngView ngView} 520 | * creates new scope and reinstantiates the controller. 521 | */ 522 | reload: function() { 523 | forceReload = true; 524 | 525 | var fakeLocationEvent = { 526 | defaultPrevented: false, 527 | preventDefault: function fakePreventDefault() { 528 | this.defaultPrevented = true; 529 | forceReload = false; 530 | } 531 | }; 532 | 533 | $rootScope.$evalAsync(function() { 534 | prepareRoute(fakeLocationEvent); 535 | if (!fakeLocationEvent.defaultPrevented) commitRoute(); 536 | }); 537 | }, 538 | 539 | /** 540 | * @ngdoc method 541 | * @name $route#updateParams 542 | * 543 | * @description 544 | * Causes `$route` service to update the current URL, replacing 545 | * current route parameters with those specified in `newParams`. 546 | * Provided property names that match the route's path segment 547 | * definitions will be interpolated into the location's path, while 548 | * remaining properties will be treated as query params. 549 | * 550 | * @param {!Object} newParams mapping of URL parameter names to values 551 | */ 552 | updateParams: function(newParams) { 553 | if (this.current && this.current.$$route) { 554 | newParams = angular.extend({}, this.current.params, newParams); 555 | $location.path(interpolate(this.current.$$route.originalPath, newParams)); 556 | // interpolate modifies newParams, only query params are left 557 | $location.search(newParams); 558 | } else { 559 | throw $routeMinErr('norout', 'Tried updating route when with no current route'); 560 | } 561 | } 562 | }; 563 | 564 | $rootScope.$on('$locationChangeStart', prepareRoute); 565 | $rootScope.$on('$locationChangeSuccess', commitRoute); 566 | 567 | return $route; 568 | 569 | ///////////////////////////////////////////////////// 570 | 571 | /** 572 | * @param on {string} current url 573 | * @param route {Object} route regexp to match the url against 574 | * @return {?Object} 575 | * 576 | * @description 577 | * Check if the route matches the current url. 578 | * 579 | * Inspired by match in 580 | * visionmedia/express/lib/router/router.js. 581 | */ 582 | function switchRouteMatcher(on, route) { 583 | var keys = route.keys, 584 | params = {}; 585 | 586 | if (!route.regexp) return null; 587 | 588 | var m = route.regexp.exec(on); 589 | if (!m) return null; 590 | 591 | for (var i = 1, len = m.length; i < len; ++i) { 592 | var key = keys[i - 1]; 593 | 594 | var val = m[i]; 595 | 596 | if (key && val) { 597 | params[key.name] = val; 598 | } 599 | } 600 | return params; 601 | } 602 | 603 | function prepareRoute($locationEvent) { 604 | var lastRoute = $route.current; 605 | 606 | preparedRoute = parseRoute(); 607 | preparedRouteIsUpdateOnly = preparedRoute && lastRoute && preparedRoute.$$route === lastRoute.$$route 608 | && angular.equals(preparedRoute.pathParams, lastRoute.pathParams) 609 | && !preparedRoute.reloadOnSearch && !forceReload; 610 | 611 | if (!preparedRouteIsUpdateOnly && (lastRoute || preparedRoute)) { 612 | if ($rootScope.$broadcast('$routeChangeStart', preparedRoute, lastRoute).defaultPrevented) { 613 | if ($locationEvent) { 614 | $locationEvent.preventDefault(); 615 | } 616 | } 617 | } 618 | } 619 | 620 | function commitRoute() { 621 | var lastRoute = $route.current; 622 | var nextRoute = preparedRoute; 623 | 624 | if (preparedRouteIsUpdateOnly) { 625 | lastRoute.params = nextRoute.params; 626 | angular.copy(lastRoute.params, $routeParams); 627 | $rootScope.$broadcast('$routeUpdate', lastRoute); 628 | } else if (nextRoute || lastRoute) { 629 | forceReload = false; 630 | $route.current = nextRoute; 631 | if (nextRoute) { 632 | if (nextRoute.redirectTo) { 633 | if (angular.isString(nextRoute.redirectTo)) { 634 | $location.path(interpolate(nextRoute.redirectTo, nextRoute.params)).search(nextRoute.params) 635 | .replace(); 636 | } else { 637 | $location.url(nextRoute.redirectTo(nextRoute.pathParams, $location.path(), $location.search())) 638 | .replace(); 639 | } 640 | } 641 | } 642 | 643 | $q.when(nextRoute). 644 | then(resolveLocals). 645 | then(function(locals) { 646 | // after route change 647 | if (nextRoute == $route.current) { 648 | if (nextRoute) { 649 | nextRoute.locals = locals; 650 | angular.copy(nextRoute.params, $routeParams); 651 | } 652 | $rootScope.$broadcast('$routeChangeSuccess', nextRoute, lastRoute); 653 | } 654 | }, function(error) { 655 | if (nextRoute == $route.current) { 656 | $rootScope.$broadcast('$routeChangeError', nextRoute, lastRoute, error); 657 | } 658 | }); 659 | } 660 | } 661 | 662 | function resolveLocals(route) { 663 | if (route) { 664 | var locals = angular.extend({}, route.resolve); 665 | angular.forEach(locals, function(value, key) { 666 | locals[key] = angular.isString(value) ? 667 | $injector.get(value) : 668 | $injector.invoke(value, null, null, key); 669 | }); 670 | var template = getTemplateFor(route); 671 | if (angular.isDefined(template)) { 672 | locals['$template'] = template; 673 | } 674 | return $q.all(locals); 675 | } 676 | } 677 | 678 | 679 | function getTemplateFor(route) { 680 | var template, templateUrl; 681 | if (angular.isDefined(template = route.template)) { 682 | if (angular.isFunction(template)) { 683 | template = template(route.params); 684 | } 685 | } else if (angular.isDefined(templateUrl = route.templateUrl)) { 686 | if (angular.isFunction(templateUrl)) { 687 | templateUrl = templateUrl(route.params); 688 | } 689 | if (angular.isDefined(templateUrl)) { 690 | route.loadedTemplateUrl = $sce.valueOf(templateUrl); 691 | template = $templateRequest(templateUrl); 692 | } 693 | } 694 | return template; 695 | } 696 | 697 | 698 | /** 699 | * @returns {Object} the current active route, by matching it against the URL 700 | */ 701 | function parseRoute() { 702 | // Match a route 703 | var params, match; 704 | angular.forEach(routes, function(route, path) { 705 | if (!match && (params = switchRouteMatcher($location.path(), route))) { 706 | match = inherit(route, { 707 | params: angular.extend({}, $location.search(), params), 708 | pathParams: params}); 709 | match.$$route = route; 710 | } 711 | }); 712 | // No route matched; fallback to "otherwise" route 713 | return match || routes[null] && inherit(routes[null], {params: {}, pathParams:{}}); 714 | } 715 | 716 | /** 717 | * @returns {string} interpolation of the redirect path with the parameters 718 | */ 719 | function interpolate(string, params) { 720 | var result = []; 721 | angular.forEach((string || '').split(':'), function(segment, i) { 722 | if (i === 0) { 723 | result.push(segment); 724 | } else { 725 | var segmentMatch = segment.match(/(\w+)(?:[?*])?(.*)/); 726 | var key = segmentMatch[1]; 727 | result.push(params[key]); 728 | result.push(segmentMatch[2] || ''); 729 | delete params[key]; 730 | } 731 | }); 732 | return result.join(''); 733 | } 734 | }]; 735 | } 736 | 737 | ngRouteModule.provider('$routeParams', $RouteParamsProvider); 738 | 739 | 740 | /** 741 | * @ngdoc service 742 | * @name $routeParams 743 | * @requires $route 744 | * 745 | * @description 746 | * The `$routeParams` service allows you to retrieve the current set of route parameters. 747 | * 748 | * Requires the {@link ngRoute `ngRoute`} module to be installed. 749 | * 750 | * The route parameters are a combination of {@link ng.$location `$location`}'s 751 | * {@link ng.$location#search `search()`} and {@link ng.$location#path `path()`}. 752 | * The `path` parameters are extracted when the {@link ngRoute.$route `$route`} path is matched. 753 | * 754 | * In case of parameter name collision, `path` params take precedence over `search` params. 755 | * 756 | * The service guarantees that the identity of the `$routeParams` object will remain unchanged 757 | * (but its properties will likely change) even when a route change occurs. 758 | * 759 | * Note that the `$routeParams` are only updated *after* a route change completes successfully. 760 | * This means that you cannot rely on `$routeParams` being correct in route resolve functions. 761 | * Instead you can use `$route.current.params` to access the new route's parameters. 762 | * 763 | * @example 764 | * ```js 765 | * // Given: 766 | * // URL: http://server.com/index.html#/Chapter/1/Section/2?search=moby 767 | * // Route: /Chapter/:chapterId/Section/:sectionId 768 | * // 769 | * // Then 770 | * $routeParams ==> {chapterId:'1', sectionId:'2', search:'moby'} 771 | * ``` 772 | */ 773 | function $RouteParamsProvider() { 774 | this.$get = function() { return {}; }; 775 | } 776 | 777 | ngRouteModule.directive('ngView', ngViewFactory); 778 | ngRouteModule.directive('ngView', ngViewFillContentFactory); 779 | 780 | 781 | /** 782 | * @ngdoc directive 783 | * @name ngView 784 | * @restrict ECA 785 | * 786 | * @description 787 | * # Overview 788 | * `ngView` is a directive that complements the {@link ngRoute.$route $route} service by 789 | * including the rendered template of the current route into the main layout (`index.html`) file. 790 | * Every time the current route changes, the included view changes with it according to the 791 | * configuration of the `$route` service. 792 | * 793 | * Requires the {@link ngRoute `ngRoute`} module to be installed. 794 | * 795 | * @animations 796 | * | Animation | Occurs | 797 | * |----------------------------------|-------------------------------------| 798 | * | {@link ng.$animate#enter enter} | when the new element is inserted to the DOM | 799 | * | {@link ng.$animate#leave leave} | when the old element is removed from to the DOM | 800 | * 801 | * The enter and leave animation occur concurrently. 802 | * 803 | * @knownIssue If `ngView` is contained in an asynchronously loaded template (e.g. in another 804 | * directive's templateUrl or in a template loaded using `ngInclude`), then you need to 805 | * make sure that `$route` is instantiated in time to capture the initial 806 | * `$locationChangeStart` event and load the appropriate view. One way to achieve this 807 | * is to have it as a dependency in a `.run` block: 808 | * `myModule.run(['$route', function() {}]);` 809 | * 810 | * @scope 811 | * @priority 400 812 | * @param {string=} onload Expression to evaluate whenever the view updates. 813 | * 814 | * @param {string=} autoscroll Whether `ngView` should call {@link ng.$anchorScroll 815 | * $anchorScroll} to scroll the viewport after the view is updated. 816 | * 817 | * - If the attribute is not set, disable scrolling. 818 | * - If the attribute is set without value, enable scrolling. 819 | * - Otherwise enable scrolling only if the `autoscroll` attribute value evaluated 820 | * as an expression yields a truthy value. 821 | * @example 822 | 825 | 826 |
827 | Choose: 828 | Moby | 829 | Moby: Ch1 | 830 | Gatsby | 831 | Gatsby: Ch4 | 832 | Scarlet Letter
833 | 834 |
835 |
836 |
837 |
838 | 839 |
$location.path() = {{main.$location.path()}}
840 |
$route.current.templateUrl = {{main.$route.current.templateUrl}}
841 |
$route.current.params = {{main.$route.current.params}}
842 |
$routeParams = {{main.$routeParams}}
843 |
844 |
845 | 846 | 847 |
848 | controller: {{book.name}}
849 | Book Id: {{book.params.bookId}}
850 |
851 |
852 | 853 | 854 |
855 | controller: {{chapter.name}}
856 | Book Id: {{chapter.params.bookId}}
857 | Chapter Id: {{chapter.params.chapterId}} 858 |
859 |
860 | 861 | 862 | .view-animate-container { 863 | position:relative; 864 | height:100px!important; 865 | background:white; 866 | border:1px solid black; 867 | height:40px; 868 | overflow:hidden; 869 | } 870 | 871 | .view-animate { 872 | padding:10px; 873 | } 874 | 875 | .view-animate.ng-enter, .view-animate.ng-leave { 876 | transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; 877 | 878 | display:block; 879 | width:100%; 880 | border-left:1px solid black; 881 | 882 | position:absolute; 883 | top:0; 884 | left:0; 885 | right:0; 886 | bottom:0; 887 | padding:10px; 888 | } 889 | 890 | .view-animate.ng-enter { 891 | left:100%; 892 | } 893 | .view-animate.ng-enter.ng-enter-active { 894 | left:0; 895 | } 896 | .view-animate.ng-leave.ng-leave-active { 897 | left:-100%; 898 | } 899 | 900 | 901 | 902 | angular.module('ngViewExample', ['ngRoute', 'ngAnimate']) 903 | .config(['$routeProvider', '$locationProvider', 904 | function($routeProvider, $locationProvider) { 905 | $routeProvider 906 | .when('/Book/:bookId', { 907 | templateUrl: 'book.html', 908 | controller: 'BookCtrl', 909 | controllerAs: 'book' 910 | }) 911 | .when('/Book/:bookId/ch/:chapterId', { 912 | templateUrl: 'chapter.html', 913 | controller: 'ChapterCtrl', 914 | controllerAs: 'chapter' 915 | }); 916 | 917 | $locationProvider.html5Mode(true); 918 | }]) 919 | .controller('MainCtrl', ['$route', '$routeParams', '$location', 920 | function($route, $routeParams, $location) { 921 | this.$route = $route; 922 | this.$location = $location; 923 | this.$routeParams = $routeParams; 924 | }]) 925 | .controller('BookCtrl', ['$routeParams', function($routeParams) { 926 | this.name = "BookCtrl"; 927 | this.params = $routeParams; 928 | }]) 929 | .controller('ChapterCtrl', ['$routeParams', function($routeParams) { 930 | this.name = "ChapterCtrl"; 931 | this.params = $routeParams; 932 | }]); 933 | 934 | 935 | 936 | 937 | it('should load and compile correct template', function() { 938 | element(by.linkText('Moby: Ch1')).click(); 939 | var content = element(by.css('[ng-view]')).getText(); 940 | expect(content).toMatch(/controller\: ChapterCtrl/); 941 | expect(content).toMatch(/Book Id\: Moby/); 942 | expect(content).toMatch(/Chapter Id\: 1/); 943 | 944 | element(by.partialLinkText('Scarlet')).click(); 945 | 946 | content = element(by.css('[ng-view]')).getText(); 947 | expect(content).toMatch(/controller\: BookCtrl/); 948 | expect(content).toMatch(/Book Id\: Scarlet/); 949 | }); 950 | 951 |
952 | */ 953 | 954 | 955 | /** 956 | * @ngdoc event 957 | * @name ngView#$viewContentLoaded 958 | * @eventType emit on the current ngView scope 959 | * @description 960 | * Emitted every time the ngView content is reloaded. 961 | */ 962 | ngViewFactory.$inject = ['$route', '$anchorScroll', '$animate']; 963 | function ngViewFactory($route, $anchorScroll, $animate) { 964 | return { 965 | restrict: 'ECA', 966 | terminal: true, 967 | priority: 400, 968 | transclude: 'element', 969 | link: function(scope, $element, attr, ctrl, $transclude) { 970 | var currentScope, 971 | currentElement, 972 | previousLeaveAnimation, 973 | autoScrollExp = attr.autoscroll, 974 | onloadExp = attr.onload || ''; 975 | 976 | scope.$on('$routeChangeSuccess', update); 977 | update(); 978 | 979 | function cleanupLastView() { 980 | if (previousLeaveAnimation) { 981 | $animate.cancel(previousLeaveAnimation); 982 | previousLeaveAnimation = null; 983 | } 984 | 985 | if (currentScope) { 986 | currentScope.$destroy(); 987 | currentScope = null; 988 | } 989 | if (currentElement) { 990 | previousLeaveAnimation = $animate.leave(currentElement); 991 | previousLeaveAnimation.then(function() { 992 | previousLeaveAnimation = null; 993 | }); 994 | currentElement = null; 995 | } 996 | } 997 | 998 | function update() { 999 | var locals = $route.current && $route.current.locals, 1000 | template = locals && locals.$template; 1001 | 1002 | if (angular.isDefined(template)) { 1003 | var newScope = scope.$new(); 1004 | var current = $route.current; 1005 | 1006 | // Note: This will also link all children of ng-view that were contained in the original 1007 | // html. If that content contains controllers, ... they could pollute/change the scope. 1008 | // However, using ng-view on an element with additional content does not make sense... 1009 | // Note: We can't remove them in the cloneAttchFn of $transclude as that 1010 | // function is called before linking the content, which would apply child 1011 | // directives to non existing elements. 1012 | var clone = $transclude(newScope, function(clone) { 1013 | $animate.enter(clone, null, currentElement || $element).then(function onNgViewEnter() { 1014 | if (angular.isDefined(autoScrollExp) 1015 | && (!autoScrollExp || scope.$eval(autoScrollExp))) { 1016 | $anchorScroll(); 1017 | } 1018 | }); 1019 | cleanupLastView(); 1020 | }); 1021 | 1022 | currentElement = clone; 1023 | currentScope = current.scope = newScope; 1024 | currentScope.$emit('$viewContentLoaded'); 1025 | currentScope.$eval(onloadExp); 1026 | } else { 1027 | cleanupLastView(); 1028 | } 1029 | } 1030 | } 1031 | }; 1032 | } 1033 | 1034 | // This directive is called during the $transclude call of the first `ngView` directive. 1035 | // It will replace and compile the content of the element with the loaded template. 1036 | // We need this directive so that the element content is already filled when 1037 | // the link function of another directive on the same element as ngView 1038 | // is called. 1039 | ngViewFillContentFactory.$inject = ['$compile', '$controller', '$route']; 1040 | function ngViewFillContentFactory($compile, $controller, $route) { 1041 | return { 1042 | restrict: 'ECA', 1043 | priority: -400, 1044 | link: function(scope, $element) { 1045 | var current = $route.current, 1046 | locals = current.locals; 1047 | 1048 | $element.html(locals.$template); 1049 | 1050 | var link = $compile($element.contents()); 1051 | 1052 | if (current.controller) { 1053 | locals.$scope = scope; 1054 | var controller = $controller(current.controller, locals); 1055 | if (current.controllerAs) { 1056 | scope[current.controllerAs] = controller; 1057 | } 1058 | $element.data('$ngControllerController', controller); 1059 | $element.children().data('$ngControllerController', controller); 1060 | } 1061 | scope[current.resolveAs || '$resolve'] = locals; 1062 | 1063 | link(scope); 1064 | } 1065 | }; 1066 | } 1067 | 1068 | 1069 | })(window, window.angular); 1070 | -------------------------------------------------------------------------------- /web-app/app/partials/main.html: -------------------------------------------------------------------------------- 1 |

Visual Search

2 |

AWS DeepLens Hackathon

3 | 4 |
5 | 6 |
7 | 13 |
14 | 15 |
16 | 17 |
18 | 19 |

Directory

20 | 27 | 28 |
29 | -------------------------------------------------------------------------------- /web-app/app/partials/search.html: -------------------------------------------------------------------------------- 1 |

Visual Search Results

2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 | 19 | 22 | 23 | 24 | 27 | 30 | 33 | 34 | 35 |
Match 1Match 2Match 3
14 | 15 | 17 | 18 | 20 | 21 |
25 | {{titles[0]}} 26 | 28 | {{titles[1]}} 29 | 31 | {{titles[2]}} 32 |
36 | -------------------------------------------------------------------------------- /web-app/app/partials/settings.html: -------------------------------------------------------------------------------- 1 |

Settings

2 | 3 |
4 | 5 |

Logged in as: {{mainData.user.userName || " (please log in)"}}

6 | 7 |
8 | 9 |
-------------------------------------------------------------------------------- /web-app/app/services/appConfig.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | angular.module('angApp.appConfig', []) 5 | 6 | .constant('ENV', ''); 7 | -------------------------------------------------------------------------------- /web-app/app/services/services.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('angApp.services', ['ngResource', 'angApp.appConfig']) 4 | 5 | 6 | .factory('Search', function($resource, ENV){ 7 | return { 8 | result: $resource(ENV + '/matches', {}, { 9 | searchResults: { method: 'POST', params: {}, isArray: false } 10 | }), 11 | } 12 | }) 13 | 14 | .factory('Auth', function($resource, ENV){ 15 | return { 16 | user: $resource(ENV + '/user/auth', {}, { 17 | authUser: { method: 'POST', params: {}, isArray: false } 18 | }), 19 | } 20 | }) 21 | 22 | .value('version', '0.1'); 23 | -------------------------------------------------------------------------------- /web-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Visual Search 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | --------------------------------------------------------------------------------