diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml new file mode 100644 index 0000000..70370b5 --- /dev/null +++ b/.github/workflows/python-package.yml @@ -0,0 +1,61 @@ +name: Python package + +on: + pull_request: + branches: [flask_app] + push: + branches: [flask_app] + +jobs: + build: + strategy: + matrix: + python_version: ["3.8", "3.9", "3.10", "3.11"] + platform: ["ubuntu-latest", "macos-latest", "windows-latest"] + + runs-on: ${{ matrix.platform }} + + defaults: + run: + working-directory: ./main + + steps: + - name: Checkout main repository + uses: actions/checkout@v4 + with: + path: main + + - name: Setup ExifTool + uses: actions/checkout@v4 + with: + repository: "exiftool/exiftool" + path: exiftool + + - name: Check ExifTool version + run: | + mv ${{ github.workspace }}/exiftool/exiftool ${{ github.workspace }}/exiftool/exiftool.pl + perl ${{ github.workspace }}/exiftool/exiftool.pl -ver + + - name: Setup FFmpeg + uses: FedericoCarboni/setup-ffmpeg@v2 + if: matrix.platform != 'macos-latest' + + - name: Set up Python ${{ matrix.python_version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python_version }} + + - name: Upgrade pip + run: python -m pip install --upgrade pip + + - name: Install dependencies + run: python -m pip install -e . + + +# - name: Test with pytest +# run: | +# mapilio_kit --version +# pytest -s -vv tests +# env: +# MAPILIO_KIT__TESTS_EXECUTABLE: mapilio-kit +# MAPILIO_KIT__TESTS_EXIFTOOL_EXECUTABLE: perl ${{ github.workspace }}/exiftool/exiftool.pl diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..402d783 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,124 @@ +name: Release + +on: + push: + tags: + - "v*.*.*" + +jobs: + build_and_release: + if: ${{ startsWith(github.ref, 'refs/tags/') }} + + strategy: + matrix: + python-version: [ "3.11" ] + platform: ["macos-latest"] # "ubuntu-20.04","windows-latest" + architecture: [ "x64" ] + include: + - architecture: "x86" + platform: "windows-latest" + python-version: "3.11" + + runs-on: ${{ matrix.platform }} + + defaults: + run: + working-directory: ./main + + steps: + # https://github.com/actions/checkout#Checkout-multiple-repos-side-by-side + - uses: actions/checkout@v4 + with: + path: main + + - name: Setup ExifTool + uses: actions/checkout@v4 + with: + repository: "exiftool/exiftool" + path: exiftool + + - name: Check ExifTool version + run: | + mv ${{ github.workspace }}/exiftool/exiftool ${{ github.workspace }}/exiftool/exiftool.pl + perl ${{ github.workspace }}/exiftool/exiftool.pl -ver + + - name: Setup FFmpeg + uses: FedericoCarboni/setup-ffmpeg@v3 + + if: matrix.platform != 'macos-latest' + + - name: Set up ${{ matrix.architecture }} Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + architecture: ${{ matrix.architecture }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install . + +# - name: Validate version +# run: | +# EXPECTED_GITHUB_REF=$(mapilio_kit --version | awk '{print "refs/tags/v" $1}') +# if [[ "$EXPECTED_GITHUB_REF" != "$GITHUB_REF" ]]; then +# echo "Version mismatch: $EXPECTED_GITHUB_REF != $GITHUB_REF" +# exit 1 +# fi +# if: matrix.platform != 'windows-latest' + + + + - name: Build and test with Pyinstaller on MacOS + if: matrix.platform == 'macos-latest' + run: | + python3 -m pip install pysocks + python3 -m pip install pyinstaller + python3 -m pip install pillow + chmod +x ./script/build_osx + ./script/build_osx + ./dist/releases/MapilioKit-Flask-osx-arm64 + env: + MAPILIO_KIT__TESTS_EXECUTABLE: ./dist/osx/mapilio-kit + MAPILIO_KIT__TESTS_EXIFTOOL_EXECUTABLE: perl ${{ github.workspace }}/exiftool/exiftool.pl + + - name: Build and test with Pyinstaller on Ubuntu + if: matrix.platform == 'ubuntu-20.04' + run: | + python3 -m pip install pysocks + python3 -m pip install pyinstaller + python3 -m pip install pillow + chmod +x ./script/build_linux + ./script/build_linux + chmod +x ./dist/releases/mapilio-kit--linux-x86_64 + env: + MAPILIO_KIT__TESTS_EXECUTABLE: ./dist/linux/mapilio-kit + MAPILIO_KIT__TESTS_EXIFTOOL_EXECUTABLE: perl ${{ github.workspace }}/exiftool/exiftool.pl + + - name: Build and test with Pyinstaller on Windows + if: matrix.platform == 'windows-latest' + run: | + python3 -m pip install pysocks + python3 -m pip install pyinstaller + ./script/build_bootloader.ps1 + ./script/build_win.ps1 + if (Test-Path ./dist/win/mapilio-kit--win-32bit.exe) { + cp ./dist/win/mapilio-kit--win-32bit.exe mapilio-kit--win-32bit.exe + } + if (Test-Path ./dist/win/mapilio-kit--win-64bit.exe) { + cp ./dist/win/mapilio-kit--win-64bit.exe mapilio-kit--win-64bit.exe + } + env: + MAPILIO_KIT__TESTS_EXECUTABLE: mapilio-kit_WINDOWS_VERY_HARD_TO_FIND_YOU_IN_ANOTHER_DIR_SO_I_MOVE_YOU_HERE.exe + MAPILIO_KIT__TESTS_EXIFTOOL_EXECUTABLE: perl ${{ github.workspace }}/exiftool/exiftool.pl + + - name: Release + uses: softprops/action-gh-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + with: + draft: true + generate_release_notes: true + fail_on_unmatched_files: true + files: | + ./main/dist/releases/* diff --git a/create_spec.py b/create_spec.py new file mode 100644 index 0000000..e21f19b --- /dev/null +++ b/create_spec.py @@ -0,0 +1,159 @@ +import os +import subprocess +import sys +from pathlib import Path + +def install_exiftool(): + if sys.platform == 'linux': + subprocess.run(['sudo', 'apt', 'install', '-y', 'exiftool'], check=True) + elif sys.platform == 'darwin': + subprocess.run(['brew', 'install', 'exiftool'], check=True) + elif sys.platform == 'win32': + subprocess.run(['powershell', '-Command', + 'Set-ExecutionPolicy Bypass -Scope Process -Force; ' + '[System.Net.ServicePointManager]::SecurityProtocol = ' + '[System.Net.ServicePointManager]::SecurityProtocol -bor 3072; ' + 'iex ((New-Object System.Net.WebClient).DownloadString(\'https://community.chocolatey.org/install.ps1\'))'], + check=True) + subprocess.run(['choco', 'install', 'exiftool', '-y'], check=True) + else: + raise ValueError("Unsupported platform") + + +def get_exiftool_path(): + try: + if sys.platform == 'linux' or sys.platform == 'darwin': + # Unix-based systems (Linux, macOS) + result = subprocess.run(['which', 'exiftool'], capture_output=True, text=True) + elif sys.platform == 'win32': + # Windows + result = subprocess.run(['where', 'exiftool'], capture_output=True, text=True) + else: + raise ValueError("Unsupported operating system") + + if result.returncode != 0: + raise ValueError("ExifTool not found") + return result.stdout.strip() + except subprocess.CalledProcessError as e: + raise ValueError(f"Error finding ExifTool: {e}") + +def get_installed_package_path(package_name): + result = subprocess.run([sys.executable, '-m', 'pip', 'show', package_name], capture_output=True, text=True) + if result.returncode != 0: + raise ValueError(f"Package {package_name} not found") + + location = None + for line in result.stdout.splitlines(): + if line.startswith('Location:'): + location = line.split(' ', 1)[1].strip() + break + + if not location: + raise ValueError(f"Location not found for package {package_name}") + + package_folder_name = package_name.split('-')[0] + package_path = Path(location) / package_folder_name + if package_path.exists(): + return package_path + + package_folder_name = package_name.replace('-', '_') + package_path = Path(location) / package_folder_name + if package_path.exists(): + return package_path + + package_path_with_py = package_path.with_suffix('.py') + if package_path_with_py.exists(): + return package_path_with_py + + if package_name == 'attrs': + alt_package_name = 'attr' + alt_package_path = Path(location) / alt_package_name + if alt_package_path.exists(): + return alt_package_path + + raise ValueError(f"Package path not found for {package_name}") + +def create_spec_file(): + requirements_file = 'requirements.txt' + spec_file = 'flask_app.spec' + icon_file = 'mapilio_ico.ico' + + current_directory = os.getcwd() + datas = [('templates', 'templates'), ('static', 'static'), ('mapilio_kit', 'mapilio_kit'), ('mapilio_ico.ico', 'mapilio_ico.ico')] + hiddenimports = ['configparser'] + + install_exiftool() + try: + exiftool_path = get_exiftool_path() + datas.append((exiftool_path, 'exiftool')) + except ValueError as e: + print(f"Warning: {e}") + + with open(requirements_file) as f: + packages = [line.split('==')[0].strip() for line in f if line.strip() and not line.startswith('#')] + print(packages) + + for package in packages: + try: + if package == 'ExifRead': + package = 'exifread' + package_path = get_installed_package_path(package) + + package_folder_name = package.replace('-', '_') + hiddenimports.append(package_folder_name) + + if package_folder_name == package: + package_data_name = package + else: + package_data_name = package_path.parts[-1] + + datas.append((str(package_path), package_data_name)) + + except ValueError as e: + print(f"Warning: {e}") + print(datas) + with open(spec_file, 'w') as f: + f.write(f""" +# -*- mode: python ; coding: utf-8 -*- + +block_cipher = None + +options = [("u", None, "OPTION")] + +a = Analysis( + ['flask_app.py'], + pathex=[SPECPATH], + binaries=[], + datas={datas}, + hiddenimports={hiddenimports}, + hookspath=[], + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, +) +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + +exe = EXE( + pyz, + a.scripts, + options, + a.binaries, + a.zipfiles, + a.datas, + [], + name='MapilioKit-Flask', + debug=False, + strip=False, + upx=True, + runtime_tmpdir=None, + console=True, + icon='{icon_file}' +) + +app = BUNDLE(exe, name='kit-gui.app', icon='mapilio_ico.ico', bundle_identifier=None) +""") + +if __name__ == "__main__": + create_spec_file() diff --git a/docker/Dockerfile b/docker/Dockerfile index bc86a1a..69799f9 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -38,3 +38,5 @@ RUN make -C /app/dependencies/max2sphere-batch -j4 RUN cp /app/dependencies/max2sphere-batch/MAX2spherebatch /app/bin/ RUN python3 -m pip install --upgrade git+https://github.com/mapilio/mapilio-kit + +RUN git clone -b flask_app https://github.com/mapilio/mapilio-kit.git diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 8aae482..fada74d 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,10 +1,9 @@ -version: '3' services: - kit: + mapiliokit: image: kit container_name: kit build: . stdin_open: true tty: true - volumes: - - :/app/data/ \ No newline at end of file + ports: + - "8080:8080" \ No newline at end of file diff --git a/flask_app.py b/flask_app.py new file mode 100755 index 0000000..c98f847 --- /dev/null +++ b/flask_app.py @@ -0,0 +1,176 @@ +import json +import os +import shutil +import sys +import webbrowser + +from flask import Flask, render_template, request, redirect, url_for, jsonify + +from mapilio_kit.base import authenticator +from mapilio_kit.components.utilities.edit_config import edit_config +from mapilio_kit.components.geotagging.geotag_property_handler import geotag_property_handler +from mapilio_kit.components.utilities.insert_MAPJson import insert_MAPJson +from mapilio_kit.components.auth.login import list_all_users +from mapilio_kit.components.metadata.metadata_property_handler import metadata_property_handler +from mapilio_kit.components.processing.sequence_property_handler import sequence_property_handler +from mapilio_kit.components.upload.upload import upload + +app = Flask(__name__) + +UPLOAD_FOLDER = os.path.join(os.path.expanduser("~"), ".cache", "mapilio", "MapilioKit", "images/") + +MAPILIO_CONFIG_PATH = os.getenv( + "MAPILIO_CONFIG_PATH", + os.path.join( + os.path.expanduser("~"), + ".config", + "mapilio", + "configs", + "CLIENT_USERS", + ), +) + + +def get_args_mapilio(func): + arg_names = func.__code__.co_varnames[:func.__code__.co_argcount] + return {arg: None for arg in arg_names} + + +def check_authenticate(): + global authentication_status, token + if len(list_all_users()) == 0: + authentication_status = False + token = None + elif len(list_all_users()) >= 2: + token = None + authentication_status = False + remove_accounts() + else: + token = list_all_users()[0]['user_upload_token'] + authentication_status = True + + return token, authentication_status + + +def decompose(import_path, exiftool_path): + metadata_property_handler(import_path=import_path, exiftool_path=exiftool_path) + geotag_property_handler(import_path=import_path) + sequence_property_handler(import_path=import_path) + insert_MAPJson(import_path=import_path) + + +@app.route("/", methods=["GET", "POST"]) +def index(): + token, authentication_status = check_authenticate() + if authentication_status: + return render_template("image-upload.html", token=token) + else: + return render_template('login.html') + + +@app.route('/login', methods=['GET', 'POST']) +def mapilio_login(): + if request.method == 'POST': + args = get_args_mapilio(edit_config) + email = request.form['email'].strip() + password = request.form['password'].strip() + + username = email.split('@')[0] + + args["user_name"] = username + args["user_email"] = email + args["user_password"] = str(password) + args["gui"] = True + check_authenticate = authenticator().perform_task(args) + + if check_authenticate['status']: + message = check_authenticate['message'] + token = check_authenticate['token'] + return render_template("image-upload.html", message=message, token=token) + else: + message = check_authenticate['message'] + return render_template('login.html', message=message) + else: + return render_template('login.html') + + +@app.route('/logout', methods=['GET']) +def remove_accounts(): + if os.path.exists(MAPILIO_CONFIG_PATH): + os.remove(MAPILIO_CONFIG_PATH) + return jsonify(success=True, message="Account successfully removed!"), 200 + else: + return jsonify(success=True, message="No accounts found!"), 200 + + +@app.route('/image-upload', methods=['GET', 'POST']) +def mapilio_upload_page(): + if request.method == 'GET': + token, authentication_status = check_authenticate() + + if authentication_status: + return render_template('image-upload.html', token=token) + else: + return redirect(url_for("mapilio_login")) + elif request.method == 'POST': + if 'file' not in request.files: + return jsonify(success=False, message="No file part") + for file in request.files.getlist('file'): + if file.filename == '': + continue + if not os.path.exists(UPLOAD_FOLDER): + os.makedirs(UPLOAD_FOLDER, exist_ok=True) + file.save(UPLOAD_FOLDER + file.filename) + + bundle_dir = getattr(sys, '_MEIPASS', os.path.abspath(os.path.dirname(__file__))) + if app.debug: + path_to_exiftool = "/usr/bin/exiftool" + else: + path_to_exiftool = os.path.abspath(os.path.join(bundle_dir, 'exiftool/exiftool')) + + try: + decompose(import_path=UPLOAD_FOLDER, exiftool_path=path_to_exiftool) + jsonPath = os.path.join(UPLOAD_FOLDER, "mapilio_image_description.json") + with open(jsonPath, 'r') as f: + data = json.load(f) + total_images = data[-1]["Information"]["total_images"] + processed_images = data[-1]["Information"]["processed_images"] + failed_images = data[-1]["Information"]["failed_images"] + duplicated_images = data[-1]["Information"]["duplicated_images"] + except: + return jsonify(success=False, message="An error occurred during metadata properties extraction.") + + try: + upload_status = upload(import_path=UPLOAD_FOLDER, dry_run=False) + if upload_status.get("Success"): + try: + shutil.rmtree(UPLOAD_FOLDER) + return jsonify(success=True, message="Images uploaded successfully", total_images=total_images, + processed_images=processed_images, failed_images=failed_images, duplicated_images=duplicated_images), 200 + except OSError as err: + print(f"Error: {UPLOAD_FOLDER} could not be deleted. - {err}") + return jsonify(success=False, message=f"{err}") + else: + e = upload_status.get("Error") + return jsonify(success=False, message=f"Error: {e}"), 500 + except: + e = upload_status.get("Error") + return jsonify(success=False, message=f"Error: {e}"), 500 + else: + return jsonify(success=False, message="Method Not Allowed"), 500 + + +@app.route('/video-upload', methods=['GET', 'POST']) +def mapilio_video_upload_page(): + token, authentication_status = check_authenticate() + + if authentication_status: + return render_template('video-upload.html', token=token) + else: + return redirect(url_for("mapilio_login")) + + +if __name__ == "__main__": + webbrowser.open("http://127.0.0.1:8081/") + app.run(host="0.0.0.0", port=8081, debug=True) + # FlaskUI(app=app, server="flask", width=1200, height=800, port=8080).run() diff --git a/mapilio_ico.ico b/mapilio_ico.ico new file mode 100644 index 0000000..d41a8e6 Binary files /dev/null and b/mapilio_ico.ico differ diff --git a/mapilio_kit/components/mapper.py b/mapilio_kit/components/mapper.py new file mode 100644 index 0000000..8ef1b1e --- /dev/null +++ b/mapilio_kit/components/mapper.py @@ -0,0 +1,99 @@ +import json +import os +import webbrowser + +import folium + + +def create_map(json_path, output_html='map.html', tile_type='cartodbdark_matter'): + # Load JSON data + with open(json_path) as f: + data = json.load(f) + + # Create a map centered at the first location + latitude = data[0]['latitude'] + longitude = data[0]['longitude'] + mymap = folium.Map(location=[latitude, longitude], zoom_start=14, tiles=tile_type) + + import_path = '/'.join(json_path.split('/')[:-1]) + + # Add markers to the map + for entry in data[:-1]: + lat = entry['latitude'] + lon = entry['longitude'] + anomaly = entry['anomaly'] + filename = entry['filename'] + + # Extract additional features + capture_time = entry.get('captureTime', 'N/A') + altitude = entry.get('altitude', 'N/A') + heading = entry.get('heading', 'N/A') + source = entry.get('source', 'N/A') + orientation = entry.get('orientation', 'N/A') + roll = entry.get('roll', 'N/A') + pitch = entry.get('pitch', 'N/A') + yaw = entry.get('yaw', 'N/A') + car_speed = entry.get('carSpeed', 'N/A') + device_make = entry.get('deviceMake', 'N/A') + device_model = entry.get('deviceModel', 'N/A') + image_size = entry.get('imageSize', 'N/A') + fov = entry.get('fov', 'N/A') + megapixels = entry.get('megapixels', 'N/A') + vfov = entry.get('vfov', 'N/A') + path = entry.get('path', 'N/A') + + # Define marker color based on anomaly + color = 'green' if anomaly == 0 else 'red' + + # Format popup content + popup_content = f""" +
+ Anomaly: {anomaly}
+ Filename: {filename}
+ Capture Time: {capture_time}
+ Latitude: {lat}
+ Longitude: {lon}
+ Altitude: {altitude}
+ Heading: {heading}
+ Source: {source}
+ Orientation: {orientation}
+ Roll: {roll}
+ Pitch: {pitch}
+ Yaw: {yaw}
+ Car Speed: {car_speed}
+ Device Make: {device_make}
+ Device Model: {device_model}
+ Image Size: {image_size}
+ FOV: {fov}
+ Megapixels: {megapixels}
+ VFOV: {vfov}
+ """ + + # Add a marker + folium.Marker( + location=[lat, lon], + popup=folium.Popup(popup_content, max_width=300), + icon=folium.Icon(color=color) + ).add_to(mymap) + + logo_url = 'https://end.mapilio.com/app/default/assets/images/mapilio_white.png?v=1719386247' + logo_html = f""" +
+ +
+ """ + logo_element = folium.Element(logo_html) + mymap.get_root().html.add_child(logo_element) + # Save the map to an HTML file + mymap.save(output_html) + + # Open the map in a web browser + webbrowser.open('file://' + os.path.realpath(output_html)) + +if __name__ == '__main__': + # Call the function with the path to the JSON file + create_map( + '/home/visio-ai/PycharmProjects/mapilio-kit/videos/mapilio_sampled_video_frames/mapilio_image_description.json') diff --git a/mapilio_kit/components/upload/upload_manager.py b/mapilio_kit/components/upload/upload_manager.py index 2b4fad1..475c9ef 100644 --- a/mapilio_kit/components/upload/upload_manager.py +++ b/mapilio_kit/components/upload/upload_manager.py @@ -130,7 +130,10 @@ class FakeUploadManager(UploadManager): def upload( self, + user_items: types.User, data: T.IO[bytes], + organization_key: str = None, + project_key: str = None, offset: T.Optional[int] = None, chunk_size: int = DEFAULT_CHUNK_SIZE, ) -> str: @@ -154,13 +157,18 @@ def upload( # ) -> int: # return 1 - def fetch_offset(self) -> int: - try: - with open(self.session_key, "rb") as fp: - fp.seek(0, io.SEEK_END) - return fp.tell() - except FileNotFoundError: - return 0 + def fetch_offset(self, email) -> int: + headers = { + "Authorization": f"OAuth {self.user_access_token}" + } + resp = requests.get( + f"{MAPILIO_UPLOAD_ENDPOINT_ZIP}?fileName={self.session_key}&email={email}", + headers=headers + ) + resp.raise_for_status() + data = resp.json() + + return data["totalChunkUploaded"] def _file_stats(fp: T.IO[bytes]): diff --git a/mapilio_kit/components/utilities/utilities.py b/mapilio_kit/components/utilities/utilities.py index f41de88..8e31ba2 100644 --- a/mapilio_kit/components/utilities/utilities.py +++ b/mapilio_kit/components/utilities/utilities.py @@ -104,16 +104,16 @@ def get_exiftool_specific_feature(video_or_image_path: str, exiftool_path=None) Returns: """ - + exiftool_path = exiftool_path if exiftool_path is not None else "exiftool" os_name = platform.system() if os_name == "Windows": - process = subprocess.Popen(["exiftool", video_or_image_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, creationflags=subprocess.CREATE_NO_WINDOW) + process = subprocess.Popen([exiftool_path, video_or_image_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, creationflags=subprocess.CREATE_NO_WINDOW) else: if exiftool_path is not None: process = subprocess.Popen([exiftool_path, video_or_image_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE) else: - process = subprocess.Popen(["exiftool", video_or_image_path], stdout=subprocess.PIPE, + process = subprocess.Popen([exiftool_path, video_or_image_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE) dict_object = { diff --git a/mapilio_kit/components/version.py b/mapilio_kit/components/version.py index 334eda3..c3e7c1a 100644 --- a/mapilio_kit/components/version.py +++ b/mapilio_kit/components/version.py @@ -1,2 +1,2 @@ # TODO check before commit -VERSION = "3.0.1" +VERSION = "3.0.7" diff --git a/requirements.txt b/requirements.txt index 8dc3a66..5776f99 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,6 +25,10 @@ gps-anomaly-detector calculation-mapilio windows-curses; platform_system == "Windows" urllib3==2.2.1 +flask +flaskwebgui +pytest +configparser ffpb sentry-sdk typing_extensions diff --git a/script/build_bootloader.ps1 b/script/build_bootloader.ps1 new file mode 100644 index 0000000..c6e1218 --- /dev/null +++ b/script/build_bootloader.ps1 @@ -0,0 +1,8 @@ +python3 -m pip uninstall -y pyinstaller +git clone --depth=1 --branch v5.12.0 https://github.com/pyinstaller/pyinstaller.git pyinstaller_git +cd pyinstaller_git/bootloader # pwd: ./pyinstaller_git/bootloader +python3 ./waf all +git diff +cd .. # pwd: ./pyinstaller_git +python3 -m pip install . +cd .. # pwd: ./ \ No newline at end of file diff --git a/script/build_linux b/script/build_linux new file mode 100755 index 0000000..b8dbf2d --- /dev/null +++ b/script/build_linux @@ -0,0 +1,37 @@ +#!/bin/bash +set -e + +OS=linux + +# build +mkdir -p dist +rm -rf dist/${OS} +pyinstaller --version +pyinstaller --noconfirm --distpath dist/${OS} flask_app.spec + +# check +SOURCE=dist/${OS}/MapilioKit-Flask + +#nohup $SOURCE --version > /dev/null 2>&1 & + +#VERSION=$($SOURCE --version | awk '{print $1}') +ARCH=$(uname -m) +TARGET=dist/releases/mapilio-kit-${VERSION}-${OS}-${ARCH} + +# package +mkdir -p dist/releases +cp "$SOURCE" "$TARGET" +chmod +x "$TARGET" +# sha256 +TARGET_BASENAME=$(basename "$TARGET") +cd dist/releases +shasum -a 256 "$TARGET_BASENAME" | tee "${TARGET_BASENAME}.sha256.txt" +#ZIP_FILE=mapilio-kit-${VERSION}-${OS}-${ARCH}.zip +#zip -r "$ZIP_FILE" . +#find . -mindepth 1 ! -name '*.zip' -delete + + +cd ../../ + +# summary +ls -l dist/releases diff --git a/script/build_osx b/script/build_osx new file mode 100644 index 0000000..279d8b1 --- /dev/null +++ b/script/build_osx @@ -0,0 +1,35 @@ +#!/bin/bash +set -e + +OS=osx + +# build +mkdir -p dist +rm -rf dist/${OS} +pyinstaller --version +pyinstaller --noconfirm --distpath dist/${OS} flask_app.spec + +# check +SOURCE=dist/${OS}/MapilioKit-Flask.app/Contents/MacOS/MapilioKit-Flask +$SOURCE --version +# VERSION=$($SOURCE --version | awk '{print $1}') + +echo "HELLOOOOO" +ARCH=$(uname -m) +TARGET=dist/releases/MapilioKit-Flask-${OS}-${ARCH}.zip + +# package +mkdir -p dist/releases +zip -j "$TARGET" "$SOURCE" +#cp "$SOURCE" "$TARGET" +#chmod +x "$TARGET" + +# sha256 +TARGET_BASENAME=$(basename "$TARGET") + +cd dist/releases +shasum -a256 "$TARGET_BASENAME" | tee "${TARGET_BASENAME}.sha256.txt" +cd ../../ + +# summary +ls -l dist/releases diff --git a/script/build_win.ps1 b/script/build_win.ps1 new file mode 100644 index 0000000..fb184dd --- /dev/null +++ b/script/build_win.ps1 @@ -0,0 +1,31 @@ +$OS="win" +# this is OS arch +# $ARCH=(wmic OS get OSArchitecture)[2] +$MAXSIZE32=python3 -c "import sys; print(sys.maxsize <= 2**32)" +if ($MAXSIZE32 -ceq "True") { + $ARCH="32bit" +} else { + $ARCH="64bit" +} + +# build +mkdir -Force dist +pyinstaller --version +pyinstaller --noconfirm --distpath dist\win flask_app.spec + +# check +$SOURCE="dist\win\MapilioKit-Flask.exe" +# dist\win\mapilio-kit.exe --version +# $VERSION_OUTPUT=dist\win\mapilio-kit.exe --version +# $VERSION=$VERSION_OUTPUT.split(' ')[2] +$TARGET="dist\releases\mapilio-kit-$VERSION-$OS-$ARCH.exe" + +# package +mkdir -Force dist\releases +Copy-Item "$SOURCE" "$TARGET" + +# sha256 +Get-FileHash $TARGET -Algorithm SHA256 | Select-Object Hash > "$TARGET.sha256.txt" + +# summary +Get-ChildItem dist\releases \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..fb70257 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,3 @@ +[tool:pytest] +testpaths = mapilio_kit +addopts = --doctest-modules \ No newline at end of file diff --git a/setup.py b/setup.py index c16328c..f9a585e 100644 --- a/setup.py +++ b/setup.py @@ -102,4 +102,4 @@ def win_read_requirements(): mapilio_kit=mapilio_kit.__main__:main ''', install_requires=requires - ) + ) \ No newline at end of file diff --git a/static/images/mapilio.png b/static/images/mapilio.png new file mode 100755 index 0000000..07c1471 Binary files /dev/null and b/static/images/mapilio.png differ diff --git a/static/images/mapilioWatermark.svg b/static/images/mapilioWatermark.svg new file mode 100755 index 0000000..03bdb26 --- /dev/null +++ b/static/images/mapilioWatermark.svg @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/static/images/mapilio_beta.svg b/static/images/mapilio_beta.svg new file mode 100755 index 0000000..3214a6b --- /dev/null +++ b/static/images/mapilio_beta.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/static/images/mapilio_ico-modified.png b/static/images/mapilio_ico-modified.png new file mode 100755 index 0000000..37d562b Binary files /dev/null and b/static/images/mapilio_ico-modified.png differ diff --git a/static/images/uploadIcon.svg b/static/images/uploadIcon.svg new file mode 100755 index 0000000..936e876 --- /dev/null +++ b/static/images/uploadIcon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/static/scripts/main.js b/static/scripts/main.js new file mode 100644 index 0000000..1635240 --- /dev/null +++ b/static/scripts/main.js @@ -0,0 +1,304 @@ +document.addEventListener("DOMContentLoaded", function () { + const logoutBtn = document.getElementById('logoutBtn'); + const dropArea = document.getElementById('drop-area'); + const fileList = document.getElementById('fileList').querySelector('ul'); + + ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { + dropArea.addEventListener(eventName, preventDefaults, false); + }); + + function preventDefaults(e) { + e.preventDefault(); + e.stopPropagation(); + } + + ['dragenter', 'dragover'].forEach(eventName => { + dropArea.addEventListener(eventName, highlight, false); + }); + + ['dragleave', 'drop'].forEach(eventName => { + dropArea.addEventListener(eventName, unhighlight, false); + }); + + function highlight(e) { + dropArea.classList.add('highlight'); + } + + function unhighlight(e) { + dropArea.classList.remove('highlight'); + } + + function updateTotalImagesCount() { + const totalImagesCount = fileList.querySelectorAll('li').length; + document.getElementById('totalImagesCount').textContent = totalImagesCount; + } + + dropArea.addEventListener('drop', handleDrop, false); + + function handleDrop(e) { + e.preventDefault(); + e.stopPropagation(); + + let dt = e.dataTransfer; + console.log("Files from Drop:", dt.files); + + let droppedFiles = dt.files; + + + handleFiles(droppedFiles); + updateTotalImagesCount(); + + + let newFileList = new DataTransfer(); + + for (let i = 0; i < fileInput.files.length; i++) { + newFileList.items.add(fileInput.files[i]); + } + + for (let i = 0; i < droppedFiles.length; i++) { + newFileList.items.add(droppedFiles[i]); + } + fileInput.files = newFileList.files; + } + + + + function isDuplicate(fileName) { + let items = fileList.querySelectorAll('li'); + for (let i = 0; i < items.length; i++) { + if (items[i].innerHTML.trim() === fileName.trim()) { + return true; + } + } + return false; + } + + function handleFiles(droppedFiles) { + [...droppedFiles].forEach(file => { + if (!isDuplicate(file.name) && file.type.match('image.*')) { + uploadFile(file); + } else { + Swal.fire({ + icon: 'error', + title: 'Error', + text: `Please drop only image files, and make sure the file does not already exist in the list.`, + timer: 3000, + timerProgressBar: true, + showConfirmButton: false + }); + } + }); + updateTotalImagesCount(); + } + + const fileInput = document.getElementById('fileElem'); + + fileInput.addEventListener('change', function () { + let files = this.files; + handleFiles(files); + }, false); + + let addedFiles = []; + + function uploadFile(file) { + let li = document.createElement('li'); + li.className = 'list-group-item d-flex justify-content-between align-items-center'; + + let imageContainer = document.createElement('div'); + imageContainer.className = 'image-container'; + + let previewImg = document.createElement('img'); + previewImg.className = 'preview-image float-start rounded me-2 img-fluid'; + previewImg.src = URL.createObjectURL(file); + + imageContainer.appendChild(previewImg); + + li.appendChild(imageContainer); + + let fileInfo = document.createElement('div'); + fileInfo.className = 'text-end'; + + let fileNameSpan = document.createElement('span'); + fileNameSpan.textContent = file.name; + fileNameSpan.className = 'text-center me-auto'; + fileInfo.appendChild(fileNameSpan); + + let deleteBtn = document.createElement('button'); + deleteBtn.className = 'btn btn-danger btn-sm ms-2'; + deleteBtn.textContent = 'Delete'; + deleteBtn.addEventListener('click', function () { + let index = addedFiles.indexOf(file); + if (index !== -1) { + addedFiles.splice(index, 1); + } + li.remove(); + updateTotalImagesCount(); + removeFileFromInput(file); + }); + fileInfo.appendChild(deleteBtn); + + li.appendChild(fileInfo); + + fileList.appendChild(li); + + addedFiles.push(file); + } + + + function removeFileFromInput(fileToRemove) { + let newFileList = new DataTransfer(); + for (let i = 0; i < fileInput.files.length; i++) { + if (fileInput.files[i] !== fileToRemove) { + newFileList.items.add(fileInput.files[i]); + } + } + fileInput.files = newFileList.files; + } + + logoutBtn.addEventListener('click', function (event) { + event.preventDefault(); + + fetch('/logout', { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + Swal.fire({ + icon: 'success', + title: 'Logged Out', + text: 'You have been successfully logged out!', + timer: 2000, + timerProgressBar: true, + showConfirmButton: false + }).then(() => { + window.location.href = '/login'; + }); + } else { + Swal.fire({ + icon: 'error', + title: 'Error', + text: data.message, + timer: 2000, + timerProgressBar: true, + showConfirmButton: false + }); + } + }) + .catch(error => { + console.error('Error:', error); + }); + }); + + const uploadBtn = document.getElementById('uploadBtn'); + uploadBtn.addEventListener('click', function () { + let files = fileInput.files; + console.log("Selected files:", files); + let formData = new FormData(); + + for (let i = 0; i < files.length; i++) { + formData.append('file', files[i]); + } + if (files.length === 0) { + Swal.fire({ + icon: 'error', + title: 'Error', + text: 'Please select a file to upload.', + timer: 2000, + timerProgressBar: true, + showConfirmButton: false + }); + return; + } + + Swal.fire({ + icon: 'info', + title: 'Uploading...', + text: 'Please wait while we upload your file.', + showConfirmButton: false, + allowOutsideClick: false, + allowEscapeKey: false, + allowEnterKey: false + }); + + fetch('/image-upload', { + method: 'POST', + body: formData + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + fileList.innerHTML = ''; + fileInput.value = ''; + updateTotalImagesCount(); + Swal.fire({ + icon: 'success', + title: "Upload successfully done!", + html: ` + + + + + + + + + + + + + + + + +
Total images:${data.total_images}
Processed images:${data.processed_images}
Failed images:${data.failed_images}
Duplicated images:${data.duplicated_images}
`, + footer: "Thanks for contributions to Mapilio 🎉", + timerProgressBar: true, + showConfirmButton: false + }) + + } else { + Swal.fire({ + icon: 'error', + title: 'Error', + text: data.message, + timer: 2000, + timerProgressBar: true, + showConfirmButton: false + }); + } + }) + .catch(data => { + Swal.fire({ + icon: 'error', + title: 'Error', + text: data.message, + timer: 2000, + timerProgressBar: true, + showConfirmButton: false + }); + }); + }); + + const removeAllBtn = document.getElementById('removeAllBtn'); + + removeAllBtn.addEventListener('click', function () { + if (fileList.innerHTML === '' && fileInput.value === '') { + Swal.fire({ + icon: 'warning', + title: 'Warning', + text: 'The file list is already empty!', + timer: 2000, + timerProgressBar: true, + showConfirmButton: false + }); + } else { + fileList.innerHTML = ''; + fileInput.value = ''; + updateTotalImagesCount(); + } + }); +}); diff --git a/static/styles/style.css b/static/styles/style.css new file mode 100644 index 0000000..dc78aa4 --- /dev/null +++ b/static/styles/style.css @@ -0,0 +1,37 @@ +* { + margin: 0; + padding: 0; + font-family: "Comfortaa", "sans-serif"; +} + +a { + text-decoration: none; + +} + +#drop-area { + border: 2px dashed #ccc; + padding: 20px; + cursor: pointer; +} + +#drop-area.highlight { + border-color: #2196F3; +} + +p { + margin-top: 0; +} + +.file-preview { + margin-top: 20px; +} + +.border { + border: 1px solid #0518c2 !important; +} + +.preview-image { + width: 250px; /* Maksimum genişlik */ + max-height: 100px; /* Maksimum yükseklik */ +} diff --git a/templates/image-upload.html b/templates/image-upload.html new file mode 100644 index 0000000..17b86ac --- /dev/null +++ b/templates/image-upload.html @@ -0,0 +1,115 @@ + + + + + + Mapilio Uploader + + + + + + + + + + + {% if message %} + + {% endif %} + + + +
+
+ + +
+

Mapilio Kit Uploader

+
+ SVG Image +

Drag files or click here to choose.

+ + +
+ +

Files to Upload

+

Total number of images: 0

+
+
    +
    + + +
    +
    +
    + + + + + + + + + \ No newline at end of file diff --git a/templates/login.html b/templates/login.html new file mode 100755 index 0000000..5172f21 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,50 @@ + + + + + Mapilio Uploader + + + + + + +
    +
    +
    + Image + SVG Image +

    © Mapilio. 2024. All rights reserved

    +
    +
    +
    +

    Mapilio Account Login

    +
    +
    + + +
    +
    + + +
    +
    + +
    +

    Don't have an account yet? Sign up here.

    + {% if message %} + + {% endif %} +
    +
    +
    +
    +
    + + + + + diff --git a/templates/video-upload.html b/templates/video-upload.html new file mode 100644 index 0000000..77f32ca --- /dev/null +++ b/templates/video-upload.html @@ -0,0 +1,83 @@ + + + + + + Mapilio Uploader + + + + + + + + + + +
    +
    + +
    +

    Coming Soon

    +
    +
    +
    + + + + + + + \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29