├── .gitignore ├── README.md ├── db.sqlite3 ├── irregular_packing ├── __init__.py ├── settings.py ├── urls.py └── wsgi.py ├── manage.py ├── media └── dxf_files │ ├── E6.dxf │ ├── E6_8wra5vG.dxf │ ├── T2.dxf │ ├── T3.dxf │ └── T9.dxf ├── packing ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20170828_1623.py │ ├── 0003_auto_20170829_1413.py │ ├── 0004_dxfmodel_material_guid.py │ ├── 0005_auto_20170830_1826.py │ ├── 0006_auto_20170911_1505.py │ ├── 0007_dxfmodel_rotation.py │ ├── 0008_auto_20170911_1810.py │ ├── 0009_packdetail_project.py │ └── __init__.py ├── models.py ├── no_fit_polygon │ ├── __init__.py │ ├── dxf_file │ │ ├── E1.dxf │ │ ├── E2.dxf │ │ ├── E3.dxf │ │ ├── E4.dxf │ │ ├── E5.dxf │ │ ├── E6.dxf │ │ ├── R2651.5baibu.dxf │ │ ├── R265haimian1.5.dxf │ │ ├── T1.dxf │ │ ├── T10.bak │ │ ├── T10.dxf │ │ ├── T2.dxf │ │ ├── T3.dxf │ │ ├── T4.dxf │ │ ├── T5.bak │ │ ├── T5.dxf │ │ ├── T6.dxf │ │ ├── T7.dxf │ │ ├── T8.dxf │ │ └── T9.dxf │ ├── nfp_function.py │ ├── nfp_tools.py │ ├── settings.py │ ├── sql.py │ └── tools │ │ ├── __init__.py │ │ ├── input_utls.py │ │ ├── nfp_utls.py │ │ └── placement_worker.py ├── tasks │ ├── __init__.py │ └── irregular_package.py ├── templates │ ├── add_dxf_model.html │ ├── base.html │ ├── calc_shape.html │ ├── dxf_index.html │ ├── index.html │ ├── project_detail.html │ └── projects.html ├── tests.py ├── tools.py └── views.py └── static ├── css ├── Common.css ├── bootstrap-select.css ├── bootstrap-theme.min.css ├── bootstrap.min.css └── fm.selectator.jquery.css └── js ├── bootstrap-selectn.js ├── bootstrap.min.js ├── docs.min.js ├── jquery.min.js └── jquery.min.map /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.log 3 | .idea/ 4 | *.png 5 | *.txt 6 | *.ttc 7 | *.json 8 | *.pid 9 | :w: 10 | *.rdb 11 | *.xls 12 | 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 异形排列 2 | 3 | 添加模型 4 | 5 | ``` 6 | POST /add_dxf_model 7 | { 8 | 'name': '模型名字', 9 | 'model_guid': '模型名字', 10 | 'material_guid': '模型名字', 11 | 'rotation': '是否可以旋转' 12 | 'uploads': 'dxf 文档' //注意: 他们软件生成的dxf文档不能直接用, 13 | //需要用CAD打开,按ctrl+A(全选所有),按 x ,按回车, 14 | //然后再全选所有,按x, 按回车,然后再保存。 这样的dxf文档程序才能识别 15 | } 16 | ``` 17 | 18 | 返回结果 19 | 20 | 21 | ``` 22 | # 通用数据 23 | { 24 | "status": 0, //状态, 0:正确 25 | "message": "OK", //消息 26 | } 27 | 添加模型返回 28 | { 29 | "data": {"total_num": 5} //模型包含的图案数量 30 | } 31 | ``` 32 | 33 | ## 计算模型排料 34 | 35 | ``` 36 | POST /calc_shape_use 37 | 38 | [ 39 | 'data': '[{ //需要排列的模型数据 40 | "Guid":"模型Guid", 41 | "Amount":1, //模型的数量 42 | }, 43 | { 44 | "Guid":"8887D88A-FB87-87AA-7677-EDF7777E78CC", 45 | "Amount":1, 46 | }]' 47 | 'comment': '[{ //项目描述 48 | "name":"模型名字", 49 | "amount":1, //模型的数量 50 | "material_name": "材料名称", 51 | "material_code": "材料skucode", 52 | "model_id": "模型的ID", 53 | }]' 54 | 'border': 5, //排列的间隔 55 | 'loop': primary, middle, high //优化的次数 56 | ] 57 | 58 | 返回结果 59 | { 60 | "data":{ 61 | "15A044CA-B840-49EF-99C7-DE5D1D9866D2":{ // 材料Guid 62 | "width":2800, //用料宽度 63 | "unit":"m", //单位 64 | "total_price":113.04240000000004, //总价 65 | "use_width":2479.000000000001, // 用料长度 66 | "file_name":"static\imgs\1504951911.png", //排料图 67 | "model":[ 68 | 1 69 | ], 70 | "price":45.6, //单价 71 | "material_name":"JJ401G-3", //材料名字 72 | "areas":6941200.000000003 //用料面积 73 | }, 74 | } 75 | } 76 | ``` -------------------------------------------------------------------------------- /db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liangxuCHEN/irregular_packing_sys/00c7add9a50d7fb136e0cac2f4850a5067a3ab91/db.sqlite3 -------------------------------------------------------------------------------- /irregular_packing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liangxuCHEN/irregular_packing_sys/00c7add9a50d7fb136e0cac2f4850a5067a3ab91/irregular_packing/__init__.py -------------------------------------------------------------------------------- /irregular_packing/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for irregual_packing project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.10.6. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.10/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.10/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '8y5*zl-*_fmiigb+o#jee6mqpn$3ynl0=_litl9zwkmei@&z3*' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | # Application definition 29 | 30 | INSTALLED_APPS = [ 31 | 'django.contrib.admin', 32 | 'django.contrib.auth', 33 | 'django.contrib.contenttypes', 34 | 'django.contrib.sessions', 35 | 'django.contrib.messages', 36 | 'django.contrib.staticfiles', 37 | 'packing', 38 | ] 39 | 40 | MIDDLEWARE = [ 41 | 'django.middleware.security.SecurityMiddleware', 42 | 'django.contrib.sessions.middleware.SessionMiddleware', 43 | 'django.middleware.common.CommonMiddleware', 44 | 'django.middleware.csrf.CsrfViewMiddleware', 45 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 46 | 'django.contrib.messages.middleware.MessageMiddleware', 47 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 48 | ] 49 | 50 | ROOT_URLCONF = 'irregular_packing.urls' 51 | 52 | TEMPLATES = [ 53 | { 54 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 55 | 'DIRS': [os.path.join(BASE_DIR, 'templates')], 56 | 'APP_DIRS': True, 57 | 'OPTIONS': { 58 | 'context_processors': [ 59 | 'django.template.context_processors.debug', 60 | 'django.template.context_processors.request', 61 | 'django.contrib.auth.context_processors.auth', 62 | 'django.contrib.messages.context_processors.messages', 63 | ], 64 | }, 65 | }, 66 | ] 67 | 68 | WSGI_APPLICATION = 'irregular_packing.wsgi.application' 69 | 70 | 71 | # Database 72 | # https://docs.djangoproject.com/en/1.10/ref/settings/#databases 73 | 74 | DATABASES = { 75 | 'default': { 76 | 'ENGINE': 'django.db.backends.sqlite3', 77 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 78 | } 79 | } 80 | 81 | 82 | # Password validation 83 | # https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators 84 | 85 | AUTH_PASSWORD_VALIDATORS = [ 86 | { 87 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 88 | }, 89 | { 90 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 91 | }, 92 | { 93 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 94 | }, 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 97 | }, 98 | ] 99 | 100 | 101 | # Internationalization 102 | # https://docs.djangoproject.com/en/1.10/topics/i18n/ 103 | 104 | LANGUAGE_CODE = 'en-us' 105 | 106 | TIME_ZONE = 'Asia/Chongqing' 107 | 108 | USE_I18N = True 109 | 110 | USE_L10N = True 111 | 112 | USE_TZ = True 113 | 114 | 115 | # Static files (CSS, JavaScript, Images) 116 | # https://docs.djangoproject.com/en/1.10/howto/static-files/ 117 | 118 | STATIC_URL = '/static/' 119 | STATIC_ROOT = '' 120 | 121 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 122 | MEDIA_URL = '/site_media/' 123 | 124 | ALLOWED_HOSTS = ['*'] 125 | 126 | STATICFILES_DIRS = ( 127 | # Put strings here, like "/home/html/static" or "C:/www/django/static". 128 | # Always use forward slashes, even on Windows. 129 | # Don't forget to use absolute paths, not relative paths. 130 | os.path.join(BASE_DIR, 'static'), 131 | ) 132 | 133 | STATICFILES_FINDERS = ( 134 | 'django.contrib.staticfiles.finders.FileSystemFinder', 135 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 136 | ) 137 | # 138 | # from mrq.context import setup_context, get_current_config 139 | # 140 | # if not get_current_config(): 141 | # setup_context() 142 | -------------------------------------------------------------------------------- /irregular_packing/urls.py: -------------------------------------------------------------------------------- 1 | """irregual_packing URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.10/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf.urls import url 17 | from django.contrib import admin 18 | from packing import views 19 | from django.contrib.staticfiles.urls import staticfiles_urlpatterns, static 20 | from django.conf import settings 21 | 22 | 23 | urlpatterns = [ 24 | url(r'^$', views.home_page, name='index'), 25 | url(r'^dxf_models$', views.DxfModelIndexView.as_view(), name='dxf_index'), 26 | url(r'^projects$', views.ProjectIndexView.as_view(), name='project_list'), 27 | url(r'^add_dxf_model$', views.add_dxf_model, name='add_dxf_model'), 28 | url(r'^calc_shape_num$', views.calc_shape_num, name='calc_shape_num'), 29 | url(r'^calc_shape_use$', views.calc_shape_use, name='calc_shape_use'), 30 | url(r'^shape_use_task$', views.shape_use_task, name='shape_use_task'), 31 | url(r'^product_detail/(?P\d+)/$', views.show_project, name='show_project'), 32 | url(r'^admin/', admin.site.urls), 33 | 34 | url(r'^irregular/dxf$', views.dxf_json, name='dxf_list'), 35 | url(r'^irregular/jobs_list$', views.get_jobs_list, name='jobs_list'), 36 | ] 37 | 38 | urlpatterns += staticfiles_urlpatterns() 39 | 40 | # urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 41 | 42 | -------------------------------------------------------------------------------- /irregular_packing/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for irregual_packing project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "irregular_packing.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "irregular_packing.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /packing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liangxuCHEN/irregular_packing_sys/00c7add9a50d7fb136e0cac2f4850a5067a3ab91/packing/__init__.py -------------------------------------------------------------------------------- /packing/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from packing.models import DxfModel, PackDetail, Project 4 | 5 | 6 | class DxfModelAdmin(admin.ModelAdmin): 7 | list_display = ('model_guid', 'name', 'uploads') 8 | 9 | 10 | class PackDetailAdmin(admin.ModelAdmin): 11 | list_display = ('material_code', 'areas', 'created') 12 | 13 | 14 | class ProjectAdmin(admin.ModelAdmin): 15 | list_display = ('comment', 'created') 16 | 17 | admin.site.register(DxfModel, DxfModelAdmin) 18 | admin.site.register(PackDetail, PackDetailAdmin) 19 | admin.site.register(Project, ProjectAdmin) 20 | -------------------------------------------------------------------------------- /packing/apps.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.apps import AppConfig 4 | 5 | 6 | class PackingConfig(AppConfig): 7 | name = 'packing' 8 | -------------------------------------------------------------------------------- /packing/forms.py: -------------------------------------------------------------------------------- 1 | # encoding=utf8 2 | import json 3 | from django import forms 4 | from packing.models import DxfModel 5 | 6 | from packing.no_fit_polygon import sql 7 | 8 | 9 | class DxfForm(forms.ModelForm): 10 | class Meta: 11 | model = DxfModel 12 | fields = ['name', 'rotation', 'material_guid', 'uploads'] 13 | 14 | material_list = [(data['Guid'], "{} {}".format(data['MaterialCode'], data['MaterialName'])) 15 | for data in json.loads(sql.material_list())] 16 | # print material_list 17 | material_guid = forms.CharField( 18 | label=u'材料选择', 19 | widget=forms.Select( 20 | choices=material_list, 21 | attrs={ 22 | 'class': 'selectpicker', 23 | 'data-live-search': "true", 24 | 'data-actions-box': "true", 25 | 'data-showtick': "true", 26 | 'onchange': "changetype1(this)", 27 | 'data-live-search-placeholder': "Search", 28 | } 29 | ) 30 | ) 31 | 32 | name = forms.CharField(label=u'模型名字', widget=forms.TextInput(attrs={'class': 'form-control'})) 33 | 34 | -------------------------------------------------------------------------------- /packing/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.6 on 2017-08-28 08:21 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='DxfModel', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('model_guid', models.CharField(max_length=128)), 21 | ('name', models.CharField(max_length=512)), 22 | ('file_path', models.FloatField()), 23 | ('created', models.DateTimeField(auto_now_add=True)), 24 | ], 25 | options={ 26 | 'ordering': ('created',), 27 | }, 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /packing/migrations/0002_auto_20170828_1623.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.6 on 2017-08-28 08:23 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('packing', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='dxfmodel', 17 | name='file_path', 18 | field=models.CharField(max_length=512), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /packing/migrations/0003_auto_20170829_1413.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.6 on 2017-08-29 06:13 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('packing', '0002_auto_20170828_1623'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RemoveField( 16 | model_name='dxfmodel', 17 | name='file_path', 18 | ), 19 | migrations.AddField( 20 | model_name='dxfmodel', 21 | name='uploads', 22 | field=models.FileField(default=1, upload_to='dxf_files/'), 23 | preserve_default=False, 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /packing/migrations/0004_dxfmodel_material_guid.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.6 on 2017-08-30 01:02 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('packing', '0003_auto_20170829_1413'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='dxfmodel', 17 | name='material_guid', 18 | field=models.CharField(max_length=128, null=True), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /packing/migrations/0005_auto_20170830_1826.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.6 on 2017-08-30 10:26 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('packing', '0004_dxfmodel_material_guid'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='dxfmodel', 17 | name='material_guid', 18 | field=models.CharField(max_length=128), 19 | ), 20 | migrations.AlterField( 21 | model_name='dxfmodel', 22 | name='name', 23 | field=models.CharField(max_length=512, verbose_name='\u6a21\u578b\u540d\u5b57'), 24 | ), 25 | migrations.AlterField( 26 | model_name='dxfmodel', 27 | name='uploads', 28 | field=models.FileField(upload_to='dxf_files/', verbose_name='\u4e0a\u4f20\u6587\u4ef6'), 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /packing/migrations/0006_auto_20170911_1505.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.6 on 2017-09-11 07:05 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('packing', '0005_auto_20170830_1826'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='dxfmodel', 17 | name='material_code', 18 | field=models.CharField(max_length=18, null=True, verbose_name='\u6750\u6599\u7f16\u7801'), 19 | ), 20 | migrations.AddField( 21 | model_name='dxfmodel', 22 | name='material_name', 23 | field=models.CharField(max_length=256, null=True, verbose_name='\u6750\u6599\u540d\u79f0'), 24 | ), 25 | migrations.AlterField( 26 | model_name='dxfmodel', 27 | name='name', 28 | field=models.CharField(max_length=128, verbose_name='\u6a21\u578b\u540d\u5b57'), 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /packing/migrations/0007_dxfmodel_rotation.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.6 on 2017-09-11 09:03 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('packing', '0006_auto_20170911_1505'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='dxfmodel', 17 | name='rotation', 18 | field=models.BooleanField(default=False, verbose_name='\u65cb\u8f6c'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /packing/migrations/0008_auto_20170911_1810.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.6 on 2017-09-11 10:10 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('packing', '0007_dxfmodel_rotation'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='dxfmodel', 17 | name='shape_num', 18 | field=models.IntegerField(null=True, verbose_name='\u5305\u542b\u56fe\u5f62'), 19 | ), 20 | migrations.AlterField( 21 | model_name='dxfmodel', 22 | name='rotation', 23 | field=models.BooleanField(verbose_name='\u53ef\u65cb\u8f6c'), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /packing/migrations/0009_packdetail_project.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.6 on 2017-09-12 01:20 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('packing', '0008_auto_20170911_1810'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='PackDetail', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('created', models.DateTimeField(auto_now_add=True)), 20 | ('material_guid', models.CharField(max_length=128)), 21 | ('material_name', models.CharField(max_length=256, null=True, verbose_name='\u6750\u6599\u540d\u79f0')), 22 | ('material_code', models.CharField(max_length=18, null=True, verbose_name='\u6750\u6599\u7f16\u7801')), 23 | ('unit', models.CharField(max_length=10)), 24 | ('total_price', models.FloatField(verbose_name='\u7528\u6599\u603b\u4ef7')), 25 | ('pic_path', models.CharField(max_length=64)), 26 | ('dxf_models', models.CharField(max_length=256, verbose_name='dxf_ID')), 27 | ('use_width', models.FloatField(verbose_name='\u4f7f\u7528\u957f\u5ea6')), 28 | ('price', models.FloatField(verbose_name='\u5355\u4ef7')), 29 | ('width', models.FloatField(verbose_name='\u6750\u6599\u5bbd\u5ea6')), 30 | ('areas', models.FloatField(verbose_name='\u7528\u6599\u9762\u79ef')), 31 | ], 32 | ), 33 | migrations.CreateModel( 34 | name='Project', 35 | fields=[ 36 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 37 | ('created', models.DateTimeField(auto_now_add=True)), 38 | ('data_input', models.TextField(null=True)), 39 | ('comment', models.TextField()), 40 | ('products', models.ManyToManyField(to='packing.PackDetail')), 41 | ], 42 | ), 43 | ] 44 | -------------------------------------------------------------------------------- /packing/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liangxuCHEN/irregular_packing_sys/00c7add9a50d7fb136e0cac2f4850a5067a3ab91/packing/migrations/__init__.py -------------------------------------------------------------------------------- /packing/models.py: -------------------------------------------------------------------------------- 1 | # encoding=utf8 2 | from __future__ import unicode_literals 3 | 4 | import uuid 5 | import json 6 | from django.db import models 7 | 8 | from packing.no_fit_polygon import sql 9 | 10 | 11 | class DxfModel(models.Model): 12 | model_guid = models.CharField(max_length=128) 13 | material_guid = models.CharField(max_length=128) 14 | material_name = models.CharField(u'材料名称', max_length=256, null=True) 15 | material_code = models.CharField(u'材料编码', max_length=18, null=True) 16 | rotation = models.BooleanField(u'可旋转') 17 | shape_num = models.IntegerField(u'包含图形', null=True) 18 | name = models.CharField(u'模型名字', max_length=128) 19 | uploads = models.FileField(u'上传文件', upload_to='dxf_files/') 20 | created = models.DateTimeField(auto_now_add=True) 21 | 22 | class Meta: 23 | ordering = ('created',) 24 | 25 | def __unicode__(self): 26 | return '%s' % self.name 27 | 28 | def save(self, *args, **kwargs): 29 | self.model_guid = str(uuid.uuid4()) 30 | material = sql.material_info(self.material_guid) 31 | if material: 32 | self.material_name = material['material_name'] 33 | self.material_code = material['material_code'] 34 | 35 | super(DxfModel, self).save(*args, **kwargs) 36 | 37 | def to_json(self): 38 | d = {} 39 | for field in self._meta.fields: 40 | d[field.name] = getattr(self, field.name) 41 | 42 | d['created'] = d['created'].strftime('%Y-%m-%d %H:%M:%S') 43 | return json.dumps(d) 44 | 45 | 46 | class PackDetail(models.Model): 47 | created = models.DateTimeField(auto_now_add=True) 48 | material_guid = models.CharField(max_length=128) 49 | material_name = models.CharField(u'材料名称', max_length=256, null=True) 50 | material_code = models.CharField(u'材料编码', max_length=18, null=True) 51 | unit = models.CharField(max_length=10) 52 | total_price = models.FloatField(u'用料总价') 53 | pic_path = models.CharField(max_length=64) 54 | dxf_models = models.CharField('dxf_ID', max_length=256) 55 | use_width = models.FloatField(u'使用长度') 56 | price = models.FloatField(u'单价') 57 | width = models.FloatField(u'材料宽度') 58 | areas = models.FloatField(u'用料面积') 59 | 60 | def __unicode__(self): 61 | return '%s' % self.created.strftime('%Y-%m-%d %H:%M:%S') 62 | 63 | 64 | class Project(models.Model): 65 | created = models.DateTimeField(auto_now_add=True) 66 | products = models.ManyToManyField(PackDetail) 67 | data_input = models.TextField(null=True) 68 | comment = models.TextField() 69 | 70 | def __unicode__(self): 71 | return '%s' % self.created.strftime('%Y-%m-%d %H:%M:%S') 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /packing/no_fit_polygon/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liangxuCHEN/irregular_packing_sys/00c7add9a50d7fb136e0cac2f4850a5067a3ab91/packing/no_fit_polygon/__init__.py -------------------------------------------------------------------------------- /packing/no_fit_polygon/dxf_file/R2651.5baibu.dxf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liangxuCHEN/irregular_packing_sys/00c7add9a50d7fb136e0cac2f4850a5067a3ab91/packing/no_fit_polygon/dxf_file/R2651.5baibu.dxf -------------------------------------------------------------------------------- /packing/no_fit_polygon/dxf_file/R265haimian1.5.dxf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liangxuCHEN/irregular_packing_sys/00c7add9a50d7fb136e0cac2f4850a5067a3ab91/packing/no_fit_polygon/dxf_file/R265haimian1.5.dxf -------------------------------------------------------------------------------- /packing/no_fit_polygon/nfp_function.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from tools import placement_worker, nfp_utls 3 | import time 4 | import os 5 | import math 6 | import json 7 | import random 8 | import copy 9 | from Polygon import Polygon 10 | import matplotlib.pyplot as plt 11 | import matplotlib.patches as patches 12 | import pyclipper 13 | from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas 14 | from matplotlib.figure import Figure 15 | from settings import SPACING, ROTATIONS, BIN_HEIGHT, POPULATION_SIZE, MUTA_RATE 16 | from irregular_packing import settings as django_settings 17 | 18 | 19 | class Nester: 20 | def __init__(self, container=None, shapes=None, border=None, rotations=None): 21 | """Nester([container,shapes]): Creates a nester object with a container 22 | shape and a list of other shapes to nest into it. Container and 23 | shapes must be Part.Faces. 24 | Typical workflow: 25 | n = Nester() # creates the nester 26 | n.add_container(object) # adds a doc object as the container 27 | n.add_objects(objects) # adds a list of doc objects as shapes 28 | n.run() # runs the nesting 29 | n.show() # creates a preview (compound) of the results 30 | """ 31 | self.container = container # 承载组件的容器 32 | self.shapes = shapes # 组件信息 33 | self.shapes_max_length = 0 # 在一般无限长的布,设计一个布的尺寸 34 | self.results = list() # storage for the different results 35 | self.nfp_cache = {} # 缓存中间计算结果 36 | # 遗传算法的参数 37 | self.config = { 38 | 'curveTolerance': 0.3, # 允许的最大误差转换贝济耶和圆弧线段。在SVG的单位。更小的公差将需要更长的时间来计算 39 | 'spacing': border or SPACING, # 组件间的间隔 40 | 'rotations': rotations or ROTATIONS, # 旋转的颗粒度,360°的n份,如:4 = [0, 90 ,180, 270] 41 | 'populationSize': POPULATION_SIZE, # 基因群数量 42 | 'mutationRate': MUTA_RATE, # 变异概率 43 | 'useHoles': False, # 是否有洞,暂时都是没有洞 44 | 'exploreConcave': False # 寻找凹面,暂时是否 45 | } 46 | 47 | self.GA = None # 遗传算法类 48 | self.best = None # 记录最佳结果 49 | self.worker = None # 根据NFP结果,计算每个图形的转移数据 50 | self.container_bounds = None # 容器的最小包络矩形作为输出图的坐标 51 | 52 | def add_objects(self, objects): 53 | """add_objects(objects): adds polygon objects to the nester""" 54 | if not isinstance(objects, list): 55 | objects = [objects] 56 | if not self.shapes: 57 | self.shapes = [] 58 | 59 | p_id = 0 60 | total_area = 0 61 | for obj in objects: 62 | points = self.clean_polygon(obj) 63 | shape = { 64 | 'area': 0, 65 | 'p_id': str(p_id), 66 | 'points': [{'x': p[0], 'y': p[1]} for p in points] 67 | } 68 | # 确定多边形的线段方向 69 | area = nfp_utls.polygon_area(shape['points']) 70 | if area > 0: 71 | shape['points'].reverse() 72 | 73 | shape['area'] = abs(area) 74 | total_area += shape['area'] 75 | self.shapes.append(shape) 76 | 77 | # 如果是一般布,需要这个尺寸 78 | self.shapes_max_length = total_area / BIN_HEIGHT * 10 79 | 80 | def add_container(self, container, limited=True): 81 | """add_container(object): adds a polygon objects as the container""" 82 | if not self.container: 83 | self.container = {} 84 | 85 | container = self.clean_polygon(container) 86 | 87 | self.container['points'] = [{'x': p[0], 'y':p[1]} for p in container] 88 | self.container['p_id'] = '-1' 89 | xbinmax = self.container['points'][0]['x'] 90 | xbinmin = self.container['points'][0]['x'] 91 | ybinmax = self.container['points'][0]['y'] 92 | ybinmin = self.container['points'][0]['y'] 93 | 94 | for point in self.container['points']: 95 | if point['x'] > xbinmax: 96 | xbinmax = point['x'] 97 | elif point['x'] < xbinmin: 98 | xbinmin = point['x'] 99 | if point['y'] > ybinmax: 100 | ybinmax = point['y'] 101 | elif point['y'] < ybinmin: 102 | ybinmin = point['y'] 103 | 104 | self.container['width'] = xbinmax - xbinmin 105 | self.container['height'] = ybinmax - ybinmin 106 | self.container['limited'] = limited 107 | # 最小包络多边形 108 | self.container_bounds = nfp_utls.get_polygon_bounds(self.container['points']) 109 | 110 | def clear(self): 111 | """clear(): Removes all objects and shape from the nester""" 112 | self.shapes = None 113 | 114 | def run(self): 115 | """ 116 | run(): Runs a nesting operation. Returns a list of lists of 117 | shapes, each primary list being one filled container, or None 118 | if the operation failed. 119 | 如果开多线程,可以在这里设计检查中断信号 120 | """ 121 | if not self.container: 122 | print("Empty container. Aborting") 123 | return 124 | if not self.shapes: 125 | print("Empty shapes. Aborting") 126 | return 127 | 128 | # and still identify the original face, so we can calculate a transform afterwards 129 | faces = list() 130 | for i in range(0, len(self.shapes)): 131 | shape = copy.deepcopy(self.shapes[i]) 132 | shape['points'] = self.polygon_offset(shape['points'], self.config['spacing']) 133 | faces.append([str(i), shape]) 134 | # build a clean copy so we don't touch the original 135 | # order by area 136 | faces = sorted(faces, reverse=True, key=lambda face: face[1]['area']) 137 | return self.launch_workers(faces) 138 | 139 | def launch_workers(self, adam): 140 | """ 141 | 主过程,根据生成的基因组,求适应值,找最佳结果 142 | :param adam: 143 | :return: 144 | """ 145 | if self.GA is None: 146 | offset_bin = copy.deepcopy(self.container) 147 | offset_bin['points'] = self.polygon_offset(self.container['points'], self.config['spacing']) 148 | self.GA = genetic_algorithm(adam, offset_bin, self.config) 149 | else: 150 | self.GA.generation() 151 | 152 | # 计算每一组基因的适应值 153 | for i in range(0, self.GA.config['populationSize']): 154 | res = self.find_fitness(self.GA.population[i]) 155 | self.GA.population[i]['fitness'] = res['fitness'] 156 | self.results.append(res) 157 | 158 | # 找最佳结果 159 | if len(self.results) > 0: 160 | best_result = self.results[0] 161 | 162 | for p in self.results: 163 | if p['fitness'] < best_result['fitness']: 164 | best_result = p 165 | 166 | if self.best is None or best_result['fitness'] < self.best['fitness']: 167 | self.best = best_result 168 | 169 | def find_fitness(self, individual): 170 | """ 171 | 求解适应值 172 | :param individual: 基因组数据 173 | :return: 174 | """ 175 | place_list = copy.deepcopy(individual['placement']) 176 | rotations = copy.deepcopy(individual['rotation']) 177 | ids = [p[0] for p in place_list] 178 | for i in range(0, len(place_list)): 179 | place_list[i].append(rotations[i]) 180 | 181 | nfp_pairs = list() 182 | new_cache = dict() 183 | for i in range(0, len(place_list)): 184 | # 容器和图形的内切多边形计算 185 | part = place_list[i] 186 | key = { 187 | 'A': '-1', 188 | 'B': part[0], 189 | 'inside': True, 190 | 'A_rotation': 0, 191 | 'B_rotation': rotations[i] 192 | } 193 | 194 | tmp_json_key = json.dumps(key) 195 | if not self.nfp_cache.has_key(tmp_json_key): 196 | nfp_pairs.append({ 197 | 'A': self.container, 198 | 'B': part[1], 199 | 'key': key 200 | }) 201 | else: 202 | # 是否已经计算过结果 203 | new_cache[tmp_json_key] = self.nfp_cache[tmp_json_key] 204 | 205 | # 图形与图形之间的外切多边形计算 206 | for j in range(0, i): 207 | placed = place_list[j] 208 | key = { 209 | 'A': placed[0], 210 | 'B': part[0], 211 | 'inside': False, 212 | 'A_rotation': rotations[j], 213 | 'B_rotation': rotations[i] 214 | } 215 | tmp_json_key = json.dumps(key) 216 | if not self.nfp_cache.has_key(tmp_json_key): 217 | nfp_pairs.append({ 218 | 'A': placed[1], 219 | 'B': part[1], 220 | 'key': key 221 | }) 222 | else: 223 | # 是否已经计算过结果 224 | new_cache[tmp_json_key] = self.nfp_cache[tmp_json_key] 225 | 226 | # only keep cache for one cycle 227 | self.nfp_cache = new_cache 228 | 229 | # 计算图形的转移量和适应值的类 230 | self.worker = placement_worker.PlacementWorker( 231 | self.container, place_list, ids, rotations, self.config, self.nfp_cache) 232 | 233 | # 计算所有图形两两组合的相切多边形(NFP) 234 | pair_list = list() 235 | for pair in nfp_pairs: 236 | pair_list.append(self.process_nfp(pair)) 237 | 238 | # 根据这些NFP,求解图形分布 239 | return self.generate_nfp(pair_list) 240 | 241 | def process_nfp(self, pair): 242 | """ 243 | 计算所有图形两两组合的相切多边形(NFP) 244 | :param pair: 两个组合图形的参数 245 | :return: 246 | """ 247 | if pair is None or len(pair) == 0: 248 | return None 249 | 250 | # 考虑有没有洞和凹面 251 | search_edges = self.config['exploreConcave'] 252 | use_holes = self.config['useHoles'] 253 | 254 | # 图形参数 255 | A = copy.deepcopy(pair['A']) 256 | A['points'] = nfp_utls.rotate_polygon(A['points'], pair['key']['A_rotation'])['points'] 257 | B = copy.deepcopy(pair['B']) 258 | B['points'] = nfp_utls.rotate_polygon(B['points'], pair['key']['B_rotation'])['points'] 259 | 260 | if pair['key']['inside']: 261 | # 内切或者外切 262 | if nfp_utls.is_rectangle(A['points'], 0.0001): 263 | nfp = nfp_utls.nfp_rectangle(A['points'], B['points']) 264 | else: 265 | nfp = nfp_utls.nfp_polygon(A, B, True, search_edges) 266 | 267 | # ensure all interior NFPs have the same winding direction 268 | if nfp and len(nfp) > 0: 269 | for i in range(0, len(nfp)): 270 | if nfp_utls.polygon_area(nfp[i]) > 0: 271 | nfp[i].reverse() 272 | else: 273 | pass 274 | # print('NFP Warning:', pair['key']) 275 | 276 | else: 277 | if search_edges: 278 | nfp = nfp_utls.nfp_polygon(A, B, False, search_edges) 279 | else: 280 | nfp = minkowski_difference(A, B) 281 | 282 | # 检查NFP多边形是否合理 283 | if nfp is None or len(nfp) == 0: 284 | pass 285 | # print('error in NFP 260') 286 | # print('NFP Error:', pair['key']) 287 | # print('A;', A) 288 | # print('B:', B) 289 | return None 290 | 291 | for i in range(0, len(nfp)): 292 | # if search edges is active, only the first NFP is guaranteed to pass sanity check 293 | if not search_edges or i == 0: 294 | if abs(nfp_utls.polygon_area(nfp[i])) < abs(nfp_utls.polygon_area(A['points'])): 295 | pass 296 | # print('error in NFP area 269') 297 | # print('NFP Area Error: ', abs(nfp_utls.polygon_area(nfp[i])), pair['key']) 298 | # print('NFP:', json.dumps(nfp[i])) 299 | # print('A: ', A) 300 | # print('B: ', B) 301 | nfp.pop(i) 302 | return None 303 | 304 | if len(nfp) == 0: 305 | return None 306 | # for outer NFPs, the first is guaranteed to be the largest. 307 | # Any subsequent NFPs that lie inside the first are hole 308 | for i in range(0, len(nfp)): 309 | if nfp_utls.polygon_area(nfp[i]) > 0: 310 | nfp[i].reverse() 311 | 312 | if i > 0: 313 | if nfp_utls.point_in_polygon(nfp[i][0], nfp[0]): 314 | if nfp_utls.polygon_area(nfp[i]) < 0: 315 | nfp[i].reverse() 316 | 317 | # generate nfps for children (holes of parts) if any exist 318 | # 有洞的暂时不管 319 | if use_holes and len(A) > 0: 320 | pass 321 | return {'key': pair['key'], 'value': nfp} 322 | 323 | def generate_nfp(self, nfp): 324 | """ 325 | 计算图形的转移量和适应值 326 | :param nfp: nfp多边形数据 327 | :return: 328 | """ 329 | if nfp: 330 | for i in range(0, len(nfp)): 331 | 332 | if nfp[i]: 333 | key = json.dumps(nfp[i]['key']) 334 | self.nfp_cache[key] = nfp[i]['value'] 335 | 336 | # worker的nfp cache 只保留一次 337 | self.worker.nfpCache = copy.deepcopy(self.nfp_cache) 338 | # self.worker.nfpCache.update(self.nfpCache) 339 | return self.worker.place_paths() 340 | 341 | def show_result(self): 342 | draw_result(self.best['placements'], self.shapes, self.container, self.container_bounds) 343 | 344 | def polygon_offset(self, polygon, offset): 345 | is_list = True 346 | if isinstance(polygon[0], dict): 347 | polygon = [[p['x'], p['y']] for p in polygon] 348 | is_list = False 349 | 350 | miter_limit = 2 351 | co = pyclipper.PyclipperOffset(miter_limit, self.config['curveTolerance']) 352 | co.AddPath(polygon, pyclipper.JT_ROUND, pyclipper.ET_CLOSEDPOLYGON) 353 | result = co.Execute(1*offset) 354 | if not is_list: 355 | result = [{'x': p[0], 'y':p[1]} for p in result[0]] 356 | return result 357 | 358 | def clean_polygon(self, polygon): 359 | simple = pyclipper.SimplifyPolygon(polygon, pyclipper.PFT_NONZERO) 360 | 361 | if simple is None or len(simple) == 0: 362 | return None 363 | 364 | biggest = simple[0] 365 | biggest_area = pyclipper.Area(biggest) 366 | for i in range(1, len(simple)): 367 | area = abs(pyclipper.Area(simple[i])) 368 | if area > biggest_area: 369 | biggest = simple[i] 370 | biggest_area = area 371 | 372 | clean = pyclipper.CleanPolygon(biggest, self.config['curveTolerance']) 373 | if clean is None or len(clean) == 0: 374 | return None 375 | return clean 376 | 377 | 378 | def draw_result(shift_data, polygons, bin_polygon, bin_bounds, save_img=False): 379 | """ 380 | 从结果中得到平移旋转的数据,把原始图像移到到目标地方,然后保存结果 381 | :param shift_data: 平移旋转数据 382 | :param polygons: 原始图形数据 383 | :param bin_polygon: 384 | :param bin_bounds: 385 | :return: 386 | """ 387 | # 若是无限长材料需要返回用料长度 388 | max_width = 0 389 | # 生产多边形类 390 | shapes = list() 391 | for polygon in polygons: 392 | contour = [[p['x'], p['y']] for p in polygon['points']] 393 | shapes.append(Polygon(contour)) 394 | 395 | bin_shape = Polygon([[p['x'], p['y']] for p in bin_polygon['points']]) 396 | 397 | # shape_area = bin_shape.area(0) 398 | solution = list() 399 | for s_data in shift_data: 400 | # 一个循环代表一个容器的排版 401 | tmp_bin = list() 402 | total_area = 0.0 403 | for move_step in s_data: 404 | if move_step['rotation'] != 0: 405 | # 坐标原点旋转 406 | shapes[int(move_step['p_id'])].rotate(math.pi / 180 * move_step['rotation'], 0, 0) 407 | # 平移 408 | shapes[int(move_step['p_id'])].shift(move_step['x'], move_step['y']) 409 | tmp_bin.append(shapes[int(move_step['p_id'])]) 410 | total_area += shapes[int(move_step['p_id'])].area(0) 411 | solution.append(tmp_bin) 412 | 413 | # 无限长的材料,需要找到最大的X坐标 414 | if not bin_polygon['limited']: 415 | for shape in shapes: 416 | for p in shape.contour(0): 417 | if max_width < p[0]: 418 | max_width = p[0] 419 | 420 | # 重新生成BIN 421 | contour = list() 422 | for p in bin_polygon['points']: 423 | if p['x'] > max_width: 424 | p['x'] = max_width 425 | contour.append([p['x'], p['y']]) 426 | bin_shape = Polygon(contour) 427 | bin_bounds['width'] = max_width 428 | 429 | # 显示结果 430 | file_name = '/' 431 | if save_img: 432 | file_name = os.path.join('static', 'imgs', str(time.time()).split('.')[0]) 433 | file_name = '%s.png' % file_name 434 | path = os.path.join(django_settings.BASE_DIR, file_name) 435 | draw_polygon(solution, bin_bounds, bin_shape, path) 436 | else: 437 | draw_polygon_show(solution, bin_bounds, bin_shape) 438 | return { 439 | 'use_width': max_width, 440 | 'areas': bin_bounds['height']*bin_bounds['width'] * len(solution), 441 | 'file_name': file_name 442 | } 443 | 444 | 445 | class genetic_algorithm(): 446 | """ 447 | 遗传算法类 448 | """ 449 | 450 | def __init__(self, adam, bin_polygon, config): 451 | """ 452 | 初始化参数,根据参数生成基因群 453 | :param adam: 图形 454 | :param bin_polygon: 面布 455 | :param config: 算法参数 456 | """ 457 | self.bin_bounds = bin_polygon['points'] 458 | self.bin_bounds = { 459 | 'width': bin_polygon['width'], 460 | 'height': bin_polygon['height'], 461 | } 462 | self.config = config 463 | self.bin_polygon = bin_polygon 464 | angles = list() 465 | shapes = copy.deepcopy(adam) 466 | for shape in shapes: 467 | angles.append(self.random_angle(shape)) 468 | 469 | # 基因群,图形顺序和图形旋转的角度作为基因编码 470 | self.population = [{'placement': shapes, 'rotation': angles}] 471 | 472 | for i in range(1, self.config['populationSize']): 473 | mutant = self.mutate(self.population[0]) 474 | self.population.append(mutant) 475 | 476 | def random_angle(self, shape): 477 | """ 478 | 随机旋转角度的选取 479 | :param shape: 480 | :return: 481 | """ 482 | angle_list = list() 483 | for i in range(0, self.config['rotations']): 484 | angle_list.append(i * (360/self.config['rotations'])) 485 | 486 | # 打乱顺序 487 | def shuffle_array(data): 488 | for i in range(len(data)-1, 0, -1): 489 | j = random.randint(0, i) 490 | data[i], data[j] = data[j], data[i] 491 | return data 492 | 493 | angle_list = shuffle_array(angle_list) 494 | 495 | # 查看选择后图形是否能放置在里面 496 | for angle in angle_list: 497 | rotate_part = nfp_utls.rotate_polygon(shape[1]['points'], angle) 498 | # 是否判断旋转出界,没有出界可以返回旋转角度,rotate 只是尝试去转,没有真正改变图形坐标 499 | if rotate_part['width'] < self.bin_bounds['width'] and rotate_part['height'] < self.bin_bounds['height']: 500 | return angle_list[i] 501 | 502 | return 0 503 | 504 | def mutate(self, individual): 505 | clone = { 506 | 'placement': individual['placement'][:], 507 | 'rotation': individual['rotation'][:] 508 | } 509 | for i in range(0, len(clone['placement'])): 510 | if random.random() < 0.01 * self.config['mutationRate']: 511 | if i+1 < len(clone['placement']): 512 | clone['placement'][i], clone['placement'][i+1] = clone['placement'][i+1], clone['placement'][i] 513 | 514 | clone['rotation'][i] = self.random_angle(clone['placement'][i]) 515 | clone['rotation'][i+1] = self.random_angle(clone['placement'][i+1]) 516 | return clone 517 | 518 | def generation(self): 519 | # 适应度 从大到小排序 520 | self.population = sorted(self.population, key=lambda a: a['fitness']) 521 | new_population = [self.population[0]] 522 | while len(new_population) < self.config['populationSize']: 523 | male = self.random_weighted_individual() 524 | female = self.random_weighted_individual(male) 525 | # 交集下一代 526 | children = self.mate(male, female) 527 | 528 | # 轻微突变 529 | new_population.append(self.mutate(children[0])) 530 | 531 | if len(new_population) < self.config['populationSize']: 532 | new_population.append(self.mutate(children[1])) 533 | 534 | self.population = new_population 535 | 536 | def random_weighted_individual(self, exclude=None): 537 | pop = self.population 538 | if exclude and pop.index(exclude) >= 0: 539 | pop.remove(exclude) 540 | 541 | rand = random.random() 542 | lower = 0 543 | weight = 1.0 / len(pop) 544 | upper = weight 545 | pop_len = len(pop) 546 | for i in range(0, pop_len): 547 | if (rand > lower) and (rand < upper): 548 | return pop[i] 549 | lower = upper 550 | upper += 2 * weight * float(pop_len-i)/pop_len 551 | return pop[0] 552 | 553 | def mate(self, male, female): 554 | cutpoint = random.randint(0, len(male['placement'])-1) 555 | gene1 = male['placement'][:cutpoint] 556 | rot1 = male['rotation'][:cutpoint] 557 | 558 | gene2 = female['placement'][:cutpoint] 559 | rot2 = female['rotation'][:cutpoint] 560 | 561 | def contains(gene, shape_id): 562 | for i in range(0, len(gene)): 563 | if gene[i][0] == shape_id: 564 | return True 565 | return False 566 | 567 | for i in range(len(female['placement'])-1, -1, -1): 568 | if not contains(gene1, female['placement'][i][0]): 569 | gene1.append(female['placement'][i]) 570 | rot1.append(female['rotation'][i]) 571 | 572 | for i in range(len(male['placement'])-1, -1, -1): 573 | if not contains(gene2, male['placement'][i][0]): 574 | gene2.append(male['placement'][i]) 575 | rot2.append(male['rotation'][i]) 576 | 577 | return [{'placement': gene1, 'rotation': rot1}, {'placement': gene2, 'rotation': rot2}] 578 | 579 | 580 | def minkowski_difference(A, B): 581 | """ 582 | 两个多边形的相切空间 583 | http://www.angusj.com/delphi/clipper/documentation/Docs/Units/ClipperLib/Functions/MinkowskiDiff.htm 584 | :param A: 585 | :param B: 586 | :return: 587 | """ 588 | Ac = [[p['x'], p['y']] for p in A['points']] 589 | Bc = [[p['x'] * -1, p['y'] * -1] for p in B['points']] 590 | solution = pyclipper.MinkowskiSum(Ac, Bc, True) 591 | largest_area = None 592 | clipper_nfp = None 593 | for p in solution: 594 | p = [{'x': i[0], 'y':i[1]} for i in p] 595 | sarea = nfp_utls.polygon_area(p) 596 | if largest_area is None or largest_area > sarea: 597 | clipper_nfp = p 598 | largest_area = sarea 599 | 600 | clipper_nfp = [{ 601 | 'x': clipper_nfp[i]['x'] + Bc[0][0] * -1, 602 | 'y':clipper_nfp[i]['y'] + Bc[0][1] * -1 603 | } for i in range(0, len(clipper_nfp))] 604 | return [clipper_nfp] 605 | 606 | 607 | def draw_polygon_png(solution, bin_bounds, bin_shape, path=None): 608 | base_width = 8 609 | base_height = base_width * bin_bounds['height'] / bin_bounds['width'] 610 | num_bin = len(solution) 611 | fig_height = num_bin * base_height 612 | fig1 = Figure(figsize=(base_width, fig_height)) 613 | fig1.suptitle('Polygon packing', fontweight='bold') 614 | FigureCanvas(fig1) 615 | 616 | i_pic = 1 # 记录图片的索引 617 | for shapes in solution: 618 | # 坐标设置 619 | ax = fig1.add_subplot(num_bin, 1, i_pic, aspect='equal') 620 | ax.set_title('Num %d bin' % i_pic) 621 | i_pic += 1 622 | ax.set_xlim(bin_bounds['x']-10, bin_bounds['width']+50) 623 | ax.set_ylim(bin_bounds['y']-10, bin_bounds['height']+50) 624 | 625 | output_obj = list() 626 | output_obj.append(patches.Polygon(bin_shape.contour(0), fc='green')) 627 | for s in shapes[:-1]: 628 | output_obj.append(patches.Polygon(s.contour(0), fc='yellow', lw=1, edgecolor='m')) 629 | for p in output_obj: 630 | ax.add_patch(p) 631 | 632 | if path is None: 633 | path = 'example' 634 | 635 | fig1.savefig('%s.png' % path) 636 | 637 | 638 | def draw_polygon_show(solution, bin_bounds, bin_shape): 639 | base_width = 8 640 | base_height = base_width * bin_bounds['height'] / bin_bounds['width'] 641 | num_bin = len(solution) 642 | fig_height = num_bin * base_height 643 | # fig1 = Figure(figsize=(base_width, fig_height)) 644 | # FigureCanvas(fig1) 645 | fig1 = plt.figure(figsize=(base_width, fig_height)) 646 | fig1.suptitle('Polygon packing', fontweight='bold') 647 | 648 | i_pic = 1 # 记录图片的索引 649 | for shapes in solution: 650 | # 坐标设置 651 | ax = plt.subplot(num_bin, 1, i_pic, aspect='equal') 652 | # ax = fig1.set_subplot(num_bin, 1, i_pic, aspect='equal') 653 | ax.set_title('Num %d bin' % i_pic) 654 | i_pic += 1 655 | ax.set_xlim(bin_bounds['x'] - 10, bin_bounds['width'] + 50) 656 | ax.set_ylim(bin_bounds['y'] - 10, bin_bounds['height'] + 50) 657 | 658 | output_obj = list() 659 | output_obj.append(patches.Polygon(bin_shape.contour(0), fc='green')) 660 | for s in shapes: 661 | output_obj.append(patches.Polygon(s.contour(0), fc='yellow', lw=1, edgecolor='m')) 662 | for p in output_obj: 663 | ax.add_patch(p) 664 | plt.show() 665 | # fig1.save() 666 | 667 | 668 | def draw_polygon(solution, bin_bounds, bin_shape, path): 669 | base_width = 8 670 | base_height = base_width * int((float(bin_bounds['height']) / bin_bounds['width'] + 1)) 671 | num_bin = len(solution) 672 | fig_height = num_bin * base_height 673 | fig1 = Figure(figsize=(base_width, fig_height)) 674 | FigureCanvas(fig1) 675 | fig1.suptitle('Polygon packing', fontweight='bold') 676 | 677 | i_pic = 1 # 记录图片的索引 678 | for shapes in solution: 679 | # 坐标设置 680 | ax = fig1.add_subplot(num_bin, 1, i_pic, aspect='equal') 681 | ax.set_title('Num %d bin' % i_pic) 682 | i_pic += 1 683 | ax.set_xlim(bin_bounds['x'] - 10, bin_bounds['width'] + 50) 684 | ax.set_ylim(bin_bounds['y'] - 10, bin_bounds['height'] + 50) 685 | 686 | output_obj = list() 687 | output_obj.append(patches.Polygon(bin_shape.contour(0), fc='green')) 688 | for s in shapes: 689 | output_obj.append(patches.Polygon(s.contour(0), fc='yellow', lw=1, edgecolor='m')) 690 | for p in output_obj: 691 | ax.add_patch(p) 692 | 693 | fig1.savefig(path) 694 | 695 | 696 | def content_loop_rate(best, n, loop_time=20, save_img=False): 697 | """ 698 | 固定迭代次数 699 | :param best: 700 | :param n: 701 | :param loop_time: 702 | :return: 703 | """ 704 | res = best 705 | run_time = loop_time 706 | total_shape = len(n.shapes) 707 | while run_time: 708 | n.run() 709 | best = n.best 710 | print best['fitness'] 711 | if best['fitness'] < res['fitness']: 712 | res = best 713 | print 'change', res['fitness'] 714 | 715 | if sum([len(place) for place in res['placements']]) < total_shape: 716 | print 'not all place' 717 | continue 718 | run_time -= 1 719 | 720 | return draw_result(res['placements'], n.shapes, n.container, n.container_bounds, save_img=save_img) 721 | 722 | 723 | def set_target_loop(best, nest): 724 | """ 725 | 把所有图形全部放下就退出 726 | :param best: 一个运行结果 727 | :param nest: Nester class 728 | :return: 729 | """ 730 | res = best 731 | total_area = 0 732 | rate = None 733 | num_placed = 0 734 | while 1: 735 | nest.run() 736 | best = nest.best 737 | if best['fitness'] <= res['fitness']: 738 | res = best 739 | for s_data in res['placements']: 740 | tmp_total_area = 0.0 741 | tmp_num_placed = 0 742 | 743 | for move_step in s_data: 744 | tmp_total_area += nest.shapes[int(move_step['p_id'])]['area'] 745 | tmp_num_placed += 1 746 | 747 | tmp_rates = tmp_total_area / abs(nfp_utls.polygon_area(nest.container['points'])) 748 | 749 | if num_placed < tmp_num_placed or total_area < tmp_total_area or rate < tmp_rates: 750 | num_placed = tmp_num_placed 751 | total_area = tmp_total_area 752 | rate = tmp_rates 753 | # 全部图形放下才退出 754 | if num_placed == len(nest.shapes): 755 | break 756 | # 画图 757 | draw_result(res['placements'], nest.shapes, nest.container, nest.container_bounds) -------------------------------------------------------------------------------- /packing/no_fit_polygon/nfp_tools.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | import os 4 | from nfp_function import Nester, content_loop_rate, set_target_loop 5 | from tools import input_utls 6 | from settings import BIN_WIDTH, BIN_NORMAL, BIN_CUT_BIG 7 | from sql import material_info 8 | 9 | 10 | def data_check(data): 11 | try: 12 | # 前端控制输入的字段 13 | input_data = json.loads(data['data']) 14 | return {'is_error': False, 'data': input_data} 15 | except: 16 | return {'is_error': True, 'error_info': u'json结构解析出错'} 17 | 18 | 19 | def shape_num(data): 20 | res = data_check(data) 21 | if res['is_error']: 22 | return res 23 | else: 24 | import django 25 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "irregular_packing.settings") 26 | django.setup() 27 | from packing.models import DxfModel 28 | total_num = 0 29 | for input_data in res['data']: 30 | # TODO: 参数的值合理性判断 31 | if input_data.get('Guid') and input_data.get('Amount'): 32 | dxf_model = DxfModel.objects.filter(model_guid=input_data['Guid']).first() 33 | # print 'rotation', dxf_model.rotation 34 | if dxf_model: 35 | total_num += len(input_utls.input_polygon(dxf_model.uploads.path)) * int(input_data['Amount']) 36 | else: 37 | return {'is_error': True, 'error_info': u'模型{}没有找到'.format(input_data['Guid'])} 38 | else: 39 | return {'is_error': True, 'error_info': u'数据缺少参数 Guid 或 Amount'} 40 | return {'is_error': False, 'data': {'total_num': total_num}} 41 | 42 | 43 | def shape_use(data): 44 | res = data_check(data) 45 | if res['is_error']: 46 | return res 47 | else: 48 | import django 49 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "irregular_packing.settings") 50 | django.setup() 51 | from packing.models import DxfModel 52 | 53 | # 循环特定次数 TODO: 用户去选择优化时间 54 | type_loop = data.get('loop') or 'primary' 55 | 56 | if type_loop == 'primary': 57 | loop_time = 1 58 | routing = 4 59 | elif type_loop == 'middle': 60 | loop_time = 30 61 | routing = 8 62 | else: 63 | loop_time = 90 64 | routing = 16 65 | 66 | # 间距 67 | border = data.get('border') 68 | if border: 69 | border = int(data.get('border')) 70 | 71 | material_dict = dict() 72 | for input_data in res['data']: 73 | # 确保整形 74 | amount = input_data.get('Amount') or 0 75 | if amount == 0: 76 | return {'is_error': True, 'data': input_data, 'error_info': u'Without Amount'} 77 | input_data['Amount'] = int(amount) 78 | 79 | dxf_model = DxfModel.objects.filter(model_guid=input_data['Guid']).first() 80 | shapes = input_utls.input_polygon(dxf_model.uploads.path) 81 | 82 | # 相同材料一起排料,新的材料就创建一个新材料信息 83 | if material_dict.get(dxf_model.material_guid): 84 | material_dict[dxf_model.material_guid]['model'].append(str(dxf_model.id)) 85 | for i in range(0, input_data['Amount']): 86 | material_dict[dxf_model.material_guid]['nesting'].add_objects(shapes) 87 | else: 88 | # TODO:通过材料Guid 找 材料的信息:单价,单位--> 米 平方英尺, pcs--> limited or not 89 | material = material_info(dxf_model.material_guid) 90 | material_dict[dxf_model.material_guid] = { 91 | 'model': [dxf_model.id], 92 | } 93 | if dxf_model.rotation: 94 | # 默认是不旋转图形,True 可以旋转图形 95 | material_dict[dxf_model.material_guid]['nesting'] = Nester(border=border, rotations=routing) 96 | else: 97 | material_dict[dxf_model.material_guid]['nesting'] = Nester(border=border) 98 | # 载入图形 99 | for i in range(0, input_data['Amount']): 100 | material_dict[dxf_model.material_guid]['nesting'].add_objects(shapes) 101 | 102 | if material: 103 | material_dict[dxf_model.material_guid].update(material) 104 | 105 | # TODO:需要选择尺寸 106 | if material_dict[dxf_model.material_guid]['nesting'].shapes_max_length > BIN_WIDTH: 107 | # 预测长度 108 | BIN_NORMAL[2][0] = material_dict[dxf_model.material_guid]['nesting'].shapes_max_length 109 | BIN_NORMAL[3][0] = material_dict[dxf_model.material_guid]['nesting'].shapes_max_length 110 | 111 | if material['width'] > 0: 112 | # 宽度 113 | BIN_NORMAL[1][1] = material['width'] 114 | BIN_NORMAL[2][1] = material['width'] 115 | 116 | # 选择bin, 单位是米==无限长,平方或其他==固定大小的面料 117 | #print BIN_NORMAL 118 | if material['unit'] == '米': 119 | material_dict[dxf_model.material_guid]['nesting'].add_container(BIN_NORMAL, limited=False) 120 | else: 121 | material_dict[dxf_model.material_guid]['nesting'].add_container(BIN_CUT_BIG) 122 | 123 | # 设计退出条件 124 | for key, value in material_dict.items(): 125 | # 启动运算 126 | value['nesting'].run() 127 | # 固定循环次数,找最优解 128 | value.update(content_loop_rate(value['nesting'].best, value['nesting'], 129 | loop_time=int(loop_time), save_img=True)) 130 | value.pop('nesting') 131 | if value['unit'] == '米': 132 | value['unit'] = 'm' 133 | value['total_price'] = float(value['use_width']) / 1000 * value['price'] 134 | else: 135 | value['unit'] = 'psc' 136 | # 1英尺平方 == 92903.04毫米平方 137 | value['total_price'] = (int(value['areas'] / 92903.04)+1) * value['price'] 138 | return {'is_error': False, 'data': material_dict} 139 | 140 | if __name__ == '__main__': 141 | n = Nester(border=None, rotations=4) 142 | s = input_utls.input_polygon('dxf_file/E6.dxf') 143 | n.add_objects(s) 144 | 145 | if n.shapes_max_length > BIN_WIDTH: 146 | BIN_NORMAL[2][0] = n.shapes_max_length 147 | BIN_NORMAL[3][0] = n.shapes_max_length 148 | 149 | # 选择面布 150 | n.add_container(BIN_NORMAL, limited=False) 151 | # 运行计算 152 | n.run() 153 | 154 | # 设计退出条件 155 | best = n.best 156 | # 放置在一个容器里面 157 | # set_target_loop(best, n) # T6 158 | # 循环特定次数 159 | print content_loop_rate(best, n, loop_time=2, save_img=False) # T7 , T4 160 | 161 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /packing/no_fit_polygon/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | POPULATION_SIZE = 10 # 基因组数 4 | MUTA_RATE = 20 # 变异概率 5 | ROTATIONS = 1 # 旋转选择, 1: 不能旋转 6 | # 单位都是MM(毫米) 7 | SPACING = 2 # 图形间隔空间 8 | # 不同面料尺寸 9 | BIN_HEIGHT = 1450 10 | BIN_WIDTH = 3000 11 | BIN_NORMAL = [[0, 0], [0, BIN_HEIGHT], [BIN_WIDTH, BIN_HEIGHT], [BIN_WIDTH, 0]] # 一般布是无限长 12 | BIN_CUT_BIG = [[0, 0], [0, 1570], [2500, 1570], [2500, 0]] # 切割机尺寸 1 13 | BIN_CUT_SMALL = [[0, 0], [0, 1200], [1500, 1200], [1500, 0]] # # 切割机尺寸 2 14 | 15 | BOM_HOST = '192.168.3.253:1433' 16 | BOM_HOST_USER = 'bsdb' 17 | BOM_HOST_PASSWORD = 'ls123123' 18 | BOM_DB = 'BSPRODUCTCENTER' 19 | 20 | 21 | NEW_JOB = u'新任务' 22 | FINISH_STATUS = u'运算结束' 23 | 24 | -------------------------------------------------------------------------------- /packing/no_fit_polygon/sql.py: -------------------------------------------------------------------------------- 1 | # encoding=utf8 2 | import pymssql 3 | import uuid 4 | import json 5 | import settings as default 6 | import pandas as pd 7 | from datetime import datetime as dt 8 | 9 | import sys 10 | reload(sys) 11 | sys.setdefaultencoding("utf-8") 12 | 13 | 14 | JOB_STATE_TABLE = 'T_PRT_PackingJobState' 15 | JOB_DETAIL_TABLE = 'T_PRT_PackingJobDetail' 16 | 17 | 18 | class Mssql: 19 | def __init__(self): 20 | self.host = default.BOM_HOST 21 | self.user = default.BOM_HOST_USER 22 | self.pwd = default.BOM_HOST_PASSWORD 23 | self.db = default.BOM_DB 24 | 25 | def __get_connect(self): 26 | if not self.db: 27 | raise (NameError, "do not have db information") 28 | self.conn = pymssql.connect( 29 | host=self.host, 30 | user=self.user, 31 | password=self.pwd, 32 | database=self.db, 33 | charset="utf8" 34 | ) 35 | cur = self.conn.cursor() 36 | if not cur: 37 | raise (NameError, "Have some Error") 38 | else: 39 | return cur 40 | 41 | def exec_query(self, sql): 42 | cur = self.__get_connect() 43 | cur.execute(sql) 44 | res_list = cur.fetchall() 45 | 46 | # the db object must be closed 47 | self.conn.close() 48 | return res_list 49 | 50 | def exec_non_query(self, sql): 51 | cur = self.__get_connect() 52 | cur.execute(sql) 53 | self.conn.commit() 54 | self.conn.close() 55 | 56 | def exec_many_query(self, sql, param): 57 | cur = self.__get_connect() 58 | try: 59 | cur.executemany(sql, param) 60 | self.conn.commit() 61 | except Exception as e: 62 | print e 63 | self.conn.rollback() 64 | 65 | self.conn.close() 66 | 67 | 68 | def init_sql_253(): 69 | """ 70 | 为了pandas连接数据库的设置 71 | :return: 72 | """ 73 | conn = pymssql.connect( 74 | host=default.BOM_HOST, 75 | user=default.BOM_HOST_USER, 76 | password=default.BOM_HOST_PASSWORD, 77 | database=default.BOM_DB, 78 | charset="utf8" 79 | ) 80 | return conn 81 | 82 | 83 | def material_info(guid): 84 | conn = Mssql() 85 | sql_text = "SELECT ChargeUnit,UnitPrice,MaterialName,WideWidth,MaterialCode FROM V_PRT_SoftMaterialMain where Guid='{guid}'".format(guid=guid) 86 | res = conn.exec_query(sql_text) 87 | if res: 88 | return { 89 | 'unit': res[0][0], 90 | 'price': float(res[0][1]), 91 | 'material_name': res[0][2], 92 | 'width': int(res[0][3]*1000), 93 | 'material_code': res[0][4] 94 | } 95 | return None 96 | 97 | 98 | def material_list(): 99 | conn = init_sql_253() 100 | sql_text = """SELECT Guid, MaterialCode,MaterialName FROM T_PRT_SoftMaterialMain(nolock) WHERE IsDisable=0""" 101 | df = pd.io.sql.read_sql(sql_text, con=conn) 102 | df['Guid'] = df['Guid'].astype(str) 103 | return df.to_json(orient='records') 104 | 105 | 106 | def jobs_list(): 107 | conn = init_sql_253() 108 | sql_text = """SELECT Guid, Url, Status, TotalPrice, CreateDate, UpdateDate FROM %s""" % JOB_STATE_TABLE 109 | df = pd.io.sql.read_sql(sql_text, con=conn) 110 | df['Guid'] = df['Guid'].astype(str) 111 | df['CreateDate'] = df['CreateDate'].apply(lambda x: x.strftime('%Y-%m-%d %H:%M:%S')) 112 | df['UpdateDate'] = df['UpdateDate'].apply(lambda x: x.strftime('%Y-%m-%d %H:%M:%S')) 113 | return df.to_json(orient='records') 114 | 115 | 116 | def has_same_job(data): 117 | conn = Mssql() 118 | sql_text = """SELECT Guid, Status, Url, TotalPrice FROM {table} WHERE Comment='{c}' 119 | and Data='{d}'""".format(c=data['comment'], d=data['data'], table=JOB_STATE_TABLE) 120 | res = conn.exec_query(sql_text) 121 | if res: 122 | return { 123 | 'guid': res[0][0], 124 | 'status': res[0][1], 125 | 'url': res[0][2], 126 | 'total_price': float(res[0][3]) 127 | } 128 | return None 129 | 130 | 131 | def copy_same_job(old_job, new_data): 132 | created = dt.today() 133 | conn = Mssql() 134 | new_guid = uuid.uuid4() 135 | sql_text = "insert into %s values ('%s','%s','%s','%s','%s','%s','%s', '%f')" % ( 136 | JOB_STATE_TABLE, new_guid, new_data['comment'], old_job['url'], new_data['data'], 137 | old_job['status'], created.strftime('%Y-%m-%d %H:%M:%S'), created.strftime('%Y-%m-%d %H:%M:%S'), 138 | old_job['total_price']) 139 | conn.exec_non_query(sql_text) 140 | 141 | # 更新详细内容 142 | sql_text = "SELECT * FROM {table} WHERE Guid='{Guid}'".format( 143 | table=JOB_DETAIL_TABLE, Guid=old_job['guid']) 144 | details = conn.exec_query(sql_text) 145 | 146 | insert_data = list() 147 | for d in details: 148 | insert_data.append((new_guid, d[2], d[3])) 149 | 150 | sql_text = "insert into " + JOB_DETAIL_TABLE + " values (%s, %s, %s)" 151 | conn.exec_many_query(sql_text, insert_data) 152 | return str(new_guid) 153 | 154 | 155 | def insert_new_job(new_data): 156 | created = dt.today() 157 | conn = Mssql() 158 | new_guid = uuid.uuid4() 159 | sql_text = "insert into %s values ('%s','%s','%s','%s','%s','%s','%s', '%f')" % ( 160 | JOB_STATE_TABLE, new_guid, new_data['comment'], '', new_data['data'], 161 | default.NEW_JOB, created.strftime('%Y-%m-%d %H:%M:%S'), created.strftime('%Y-%m-%d %H:%M:%S'), 162 | 0) 163 | conn.exec_non_query(sql_text) 164 | return str(new_guid) 165 | 166 | 167 | def update_job_status(guid, status, url=None, price=None): 168 | conn = Mssql() 169 | update_time = dt.today() 170 | # 结束状态 171 | if url and price: 172 | sql_text = """UPDATE {table} SET UpdateDate='{update_time}', Status='{status}', 173 | Url='{url}', TotalPrice='{price}' WHERE Guid='{guid}'""".format( 174 | guid=guid, status=status, url=url, price=price, 175 | update_time=update_time.strftime('%Y-%m-%d %H:%M:%S'), table=JOB_STATE_TABLE) 176 | else: 177 | # 一般状态 178 | sql_text = "UPDATE {table} SET UpdateDate='{update_time}', Status='{status}' WHERE Guid='{guid}'".format( 179 | guid=guid, status=status, update_time=update_time.strftime('%Y-%m-%d %H:%M:%S'), table=JOB_STATE_TABLE) 180 | conn.exec_non_query(sql_text) 181 | 182 | 183 | def insert_job_result(guid, details): 184 | conn = Mssql() 185 | insert_data = list() 186 | for d in details: 187 | insert_data.append((guid, d.material_code, d.total_price)) 188 | 189 | sql_text = "insert into " + JOB_DETAIL_TABLE + " values (%s, %s, %s)" 190 | conn.exec_many_query(sql_text, insert_data) 191 | 192 | if __name__ == '__main__': 193 | # print material_info('F36FC853-5938-45E1-AE95-0DD31E032EF4') 194 | #r = [(data['Guid'], "{} {}".format(data['MaterialCode'], data['MaterialName'])) for data in material_list()] 195 | # material_list = [(data['Guid'], "{} {}".format(data['MaterialCode'], data['MaterialName'])) 196 | # for data in json.loads(material_list())] 197 | update_job_status('77857C3D-367E-4DA6-A2B3-17FE1BCAB7F7', u'正在保存结果') 198 | print material_list 199 | 200 | -------------------------------------------------------------------------------- /packing/no_fit_polygon/tools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liangxuCHEN/irregular_packing_sys/00c7add9a50d7fb136e0cac2f4850a5067a3ab91/packing/no_fit_polygon/tools/__init__.py -------------------------------------------------------------------------------- /packing/no_fit_polygon/tools/input_utls.py: -------------------------------------------------------------------------------- 1 | # coding=utf8 2 | import dxfgrabber 3 | 4 | 5 | def find_shape_from_dxf(file_name): 6 | """ 7 | 读取DXF文档,从LINE里面找出多边形 8 | :param file_name: 文档路径 9 | :return: 10 | """ 11 | dxf = dxfgrabber.readfile(file_name) 12 | all_shapes = list() 13 | new_polygon = dict() 14 | for e in dxf.entities: 15 | if e.dxftype == 'LINE': 16 | # print (e.start, e.end) 17 | # 找封闭的多边形 18 | # 线条不按顺序画 19 | end_key = '{}x{}'.format(e.end[0], e.end[1]) 20 | star_key = '{}x{}'.format(e.start[0], e.start[1]) 21 | if new_polygon.has_key(end_key): 22 | # 找到闭合的多边形 23 | all_shapes.append(new_polygon[end_key]) 24 | new_polygon.pop(end_key) 25 | continue 26 | 27 | # 开始和结束点转换 28 | if new_polygon.has_key(star_key): 29 | # 找到闭合的多边形 30 | all_shapes.append(new_polygon[star_key]) 31 | new_polygon.pop(star_key) 32 | continue 33 | 34 | # 找连接的点 35 | has_find = False 36 | for key, points in new_polygon.items(): 37 | if points[-1][0] == e.start[0] and points[-1][1] == e.start[1]: 38 | new_polygon[key].append([e.end[0], e.end[1]]) 39 | has_find = True 40 | break 41 | if points[-1][0] == e.end[0] and points[-1][1] == e.end[1]: 42 | new_polygon[key].append([e.start[0], e.start[1]]) 43 | has_find = True 44 | break 45 | 46 | if not has_find: 47 | new_polygon['{}x{}'.format( 48 | e.start[0], e.start[1])] = [[e.start[0], e.start[1]], [e.end[0], e.end[1]]] 49 | return all_shapes 50 | 51 | 52 | def input_polygon(dxf_file): 53 | """ 54 | :param dxf_file: 文件地址 55 | :param is_class: 返回Polygon 类,或者通用的 list 56 | :return: 57 | """ 58 | # 从dxf文件中提取数据 59 | datas = find_shape_from_dxf(dxf_file) 60 | shapes = list() 61 | 62 | for i in range(0, len(datas)): 63 | shapes.append(datas[i]) 64 | 65 | return shapes 66 | 67 | if __name__ == '__main__': 68 | s = find_shape_from_dxf('T2.dxf') 69 | print(s) 70 | print len(s) -------------------------------------------------------------------------------- /packing/no_fit_polygon/tools/nfp_utls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import copy 3 | import math 4 | TOL = 0.0000001 # 计算过程中误差忽略值 5 | 6 | 7 | def almost_equal(a, b, tolerance=None): 8 | """ 9 | A,B 两点是否约为相同 10 | :param a: 坐标 11 | :param b: 坐标 12 | :param tolerance: 误差值 13 | :return: 14 | """ 15 | if tolerance is None: 16 | tolerance = TOL 17 | return abs(a - b) < tolerance 18 | 19 | 20 | def is_rectangle(poly, tolerance=None): 21 | bb = get_polygon_bounds(poly) 22 | tolerance = tolerance or TOL 23 | for point in poly: 24 | if not almost_equal(point['x'], bb['x'], tolerance) and not almost_equal( 25 | point['x'], bb['x'] + bb['width'], tolerance): 26 | return False 27 | if not almost_equal(point['y'], bb['y'], tolerance) and not almost_equal( 28 | point['y'], bb['y'] + bb['height'], tolerance): 29 | return False 30 | 31 | return True 32 | 33 | 34 | def normalize_vector(v): 35 | """ 36 | normalize vector into a unit vector 37 | :return: 38 | """ 39 | if almost_equal(v['x'] * v['x'] + v['y'] * v['y'], 1): 40 | # given vector was already a unit vector 41 | return v 42 | inverse = 1.0 / math.sqrt(v['x']**2 + v['y']**2) 43 | 44 | return {'x': v['x']*inverse, 'y': v['y']*inverse} 45 | 46 | 47 | def on_segment(A, B, p): 48 | """ 49 | returns true if p lies on the line segment defined by AB, but not at any endpoints 50 | :param A: 51 | :param B: 52 | :param p: 53 | :return: 54 | """ 55 | # vertical line 56 | if almost_equal(A['x'], B['x']) and almost_equal(p['x'], A['x']): 57 | if not almost_equal(p['y'], B['y']) and not almost_equal(p['y'], A['y']) and \ 58 | max(B['y'], A['y']) > p['y'] and p['y'] > min(B['y'], A['y']): 59 | return True 60 | else: 61 | return False 62 | # vertical line 63 | if almost_equal(A['y'], B['y']) and almost_equal(p['y'], A['y']): 64 | if not almost_equal(p['x'], B['x']) and not almost_equal(p['x'], A['x']) and \ 65 | max(B['x'], A['x']) > p['x'] and p['x'] > min(B['x'], A['x']): 66 | return True 67 | else: 68 | return False 69 | # range check 70 | if (p['x'] < A['x'] and p['x'] < B['x']) or (p['x'] > A['x'] and p['x'] > B['x']) or ( 71 | p['y'] < A['y'] and p['y'] < B['y']) or (p['y'] > A['y'] and p['y'] > B['y']): 72 | return False 73 | 74 | # exclude end points 75 | if (almost_equal(p['x'], A['x']) and almost_equal(p['y'], A['y'])) or ( 76 | almost_equal(p['x'], B['x']) and almost_equal(p['y'], B['y'])): 77 | return False 78 | 79 | cross = (p['y'] - A['y']) * (B['x'] - A['x']) - (p['x'] - A['x']) * (B['y'] - A['y']) 80 | if abs(cross) > TOL: 81 | return False 82 | dot = (p['x'] - A['x']) * (B['x'] - A['x']) + (p['y'] - A['y']) * (B['y'] - A['y']) 83 | if dot < 0 or almost_equal(dot, 0): 84 | return False 85 | 86 | len2 = (B['x'] - A['x']) * (B['x'] - A['x']) + (B['y'] - A['y']) * (B['y'] - A['y']) 87 | if dot > len2 or almost_equal(dot, len2): 88 | return False 89 | return True 90 | 91 | 92 | def nfp_rectangle(A, B): 93 | """ 94 | :param A: {x:12, y:10} 95 | :param B: {x:12, y:10} 96 | :return: 97 | """ 98 | min_ax = A[0]['x'] 99 | min_ay = A[0]['y'] 100 | max_ax = A[0]['x'] 101 | max_ay = A[0]['y'] 102 | 103 | for point in A[1:]: 104 | if point['x'] < min_ax: 105 | min_ax = point['x'] 106 | if point['x'] > max_ax: 107 | max_ax = point['x'] 108 | if point['y'] < min_ay: 109 | min_ay = point['y'] 110 | if point['y'] > max_ay: 111 | max_ay = point['y'] 112 | 113 | min_bx = B[0]['x'] 114 | min_by = B[0]['y'] 115 | max_bx = B[0]['x'] 116 | max_by = B[0]['y'] 117 | 118 | for point in B[1:]: 119 | if point['x'] < min_bx: 120 | min_bx = point['x'] 121 | if point['x'] > max_bx: 122 | max_bx = point['x'] 123 | if point['y'] < min_by: 124 | min_by = point['y'] 125 | if point['y'] > max_by: 126 | max_by = point['y'] 127 | 128 | if max_bx - min_bx > max_ax - min_ax: 129 | return None 130 | if max_by - min_by > max_ay - min_ay: 131 | return None 132 | 133 | return [[ 134 | {'x': min_ax-min_bx+B[0]['x'], 'y': min_ay-min_by+B[0]['y']}, 135 | {'x': max_ax-max_bx+B[0]['x'], 'y': min_ay-min_by+B[0]['y']}, 136 | {'x': max_ax-max_bx+B[0]['x'], 'y': max_ay-max_by+B[0]['y']}, 137 | {'x': min_ax-min_bx+B[0]['x'], 'y': max_ay-max_by+B[0]['y']} 138 | ]] 139 | 140 | 141 | def nfp_polygon(A, B, inside=True, search_edges=False): 142 | """ 143 | given a static polygon A and a movable polygon B, compute a no fit polygon by orbiting B about A 144 | if the inside flag is set, B is orbited inside of A rather than outside 145 | if the searchEdges flag is set, all edges of A are explored for NFPs - multiple 146 | """ 147 | if A is None or len(A['points']) < 3 or B is None or len(B['points']) < 3: 148 | return None 149 | 150 | # A last point = offsetx, offsety 151 | A['offsetx'] = 0 152 | A['offsety'] = 0 153 | 154 | min_a = A['points'][0]['y'] 155 | min_a_index = 0 156 | max_b = B['points'][0]['y'] 157 | max_b_index = 0 158 | 159 | for i in range(1, len(A['points'])): 160 | A['points'][i]['marked'] = False 161 | if A['points'][i]['y'] < min_a: 162 | min_a = A['points'][i]['y'] 163 | min_a_index = i 164 | 165 | for i in range(1, len(B['points'])): 166 | B['points'][i]['marked'] = False 167 | if B['points'][i]['y'] > max_b: 168 | max_b = B['points'][i]['y'] 169 | max_b_index = i 170 | 171 | if not inside: 172 | # shift B such that the bottom-most point of B is at the top-most point of A. 173 | # This guarantees an initial placement with no intersections 174 | start_point = { 175 | 'x': A['points'][min_a_index]['x'] - B['points'][max_b_index]['x'], 176 | 'y': A['points'][min_a_index]['y'] - B['points'][max_b_index]['y'] 177 | } 178 | else: 179 | # no reliable heuristic for inside 180 | start_point = search_start_point(A, B, inside) 181 | 182 | NFP_list = list() 183 | 184 | while start_point: 185 | B['offsetx'] = start_point['x'] 186 | B['offsety'] = start_point['y'] 187 | 188 | # maintain a list of touching points/edges 189 | prevvector = None 190 | NFP = [{ 191 | 'x': B['points'][0]['x'] + B['offsetx'], 192 | 'y': B['points'][0]['y'] + B['offsety'], 193 | }] 194 | 195 | referencex = B['points'][0]['x'] + B['offsetx'] 196 | referencey = B['points'][0]['y'] + B['offsety'] 197 | startx = referencex 198 | starty = referencey 199 | counter = 0 200 | len_a = len(A['points']) 201 | len_b = len(B['points']) 202 | while counter < 10 * (len_a + len_b): 203 | touching = list() 204 | for i in range(0, len_a): 205 | nexti = 0 if i == len_a - 1 else i + 1 206 | for j in range(len_b): 207 | nextj = 0 if j == len_b - 1 else j + 1 208 | if almost_equal(A['points'][i]['x'], B['points'][j]['x'] + B['offsetx']) and almost_equal( 209 | A['points'][i]['y'], B['points'][j]['y'] + B['offsety']): 210 | touching.append({'type': 0, 'A': i, 'B': j}) 211 | elif on_segment(A['points'][i], A['points'][nexti], 212 | {'x': B['points'][j]['x']+B['offsetx'], 'y': B['points'][j]['y'] + B['offsety']}): 213 | touching.append({'type': 1, 'A': nexti, 'B': j}) 214 | elif on_segment( 215 | {'x': B['points'][j]['x']+B['offsetx'], 'y': B['points'][j]['y'] + B['offsety']}, 216 | {'x': B['points'][nextj]['x'] + B['offsetx'], 'y': B['points'][nextj]['y'] + B['offsety']}, 217 | A['points'][i]): 218 | touching.append({'type': 2, 'A': i, 'B': nextj}) 219 | 220 | # generate translation vectors from touching vertices/edges 221 | vectors = list() 222 | for i in range(0, len(touching)): 223 | vertex_a = {'A': A['points'][touching[i]['A']], 'marked': True} 224 | prev_a_index = touching[i]['A'] - 1 225 | next_a_index = touching[i]['A'] + 1 226 | prev_a_index = len_a - 1 if prev_a_index < 0 else prev_a_index # loop 227 | next_a_index = 0 if next_a_index >= len_a else next_a_index # loop 228 | 229 | prev_a = A['points'][prev_a_index] 230 | next_a = A['points'][next_a_index] 231 | 232 | # adjacent B vertices 233 | vertex_b = {'A': B['points'][touching[i]['B']]} 234 | prev_b_index = touching[i]['B'] - 1 235 | next_b_index = touching[i]['B'] + 1 236 | prev_b_index = len_b - 1 if prev_b_index < 0 else prev_b_index # loop 237 | next_b_index = 0 if next_b_index >= len_b else next_b_index # loop 238 | 239 | prev_b = B['points'][prev_b_index] 240 | next_b = B['points'][next_b_index] 241 | 242 | if touching[i]['type'] == 0: 243 | v_a1 = { 244 | 'x': prev_a['x'] - vertex_a['A']['x'], 245 | 'y': prev_a['y'] - vertex_a['A']['y'], 246 | 'start': vertex_a['A'], 247 | 'end': prev_a 248 | } 249 | 250 | v_a2 = { 251 | 'x': next_a['x'] - vertex_a['A']['x'], 252 | 'y': next_a['y'] - vertex_a['A']['y'], 253 | 'start': vertex_a['A'], 254 | 'end': next_a 255 | } 256 | 257 | v_b1 = { 258 | 'x': vertex_b['A']['x'] - prev_b['x'], 259 | 'y': vertex_b['A']['y'] - prev_b['y'], 260 | 'start': prev_b, 261 | 'end': vertex_b['A'] 262 | } 263 | 264 | v_b2 = { 265 | 'x': vertex_b['A']['x'] - next_b['x'], 266 | 'y': vertex_b['A']['y'] - next_b['y'], 267 | 'start': next_b, 268 | 'end': vertex_b['A'] 269 | } 270 | 271 | vectors.append(v_a1) 272 | vectors.append(v_a2) 273 | vectors.append(v_b1) 274 | vectors.append(v_b2) 275 | elif touching[i]['type'] == 1: 276 | vectors.append({ 277 | 'x': vertex_a['A']['x'] - (vertex_b['A']['x'] + B['offsetx']), 278 | 'y': vertex_a['A']['y'] - (vertex_b['A']['y'] + B['offsety']), 279 | 'start': prev_a, 280 | 'end': vertex_a['A'] 281 | }) 282 | vectors.append({ 283 | 'x': prev_a['x'] - (vertex_b['A']['x'] + B['offsetx']), 284 | 'y': prev_a['y'] - (vertex_b['A']['y'] + B['offsety']), 285 | 'start': vertex_a['A'], 286 | 'end': prev_a 287 | }) 288 | elif touching[i]['type'] == 2: 289 | vectors.append({ 290 | 'x': vertex_a['A']['x'] - (vertex_b['A']['x'] + B['offsetx']), 291 | 'y': vertex_a['A']['y'] - (vertex_b['A']['y'] + B['offsety']), 292 | 'start': prev_b, 293 | 'end': vertex_b['A'] 294 | }) 295 | vectors.append({ 296 | 'x': vertex_a['A']['x'] - (prev_b['x'] + B['offsetx']), 297 | 'y': vertex_a['A']['y'] - (prev_b['y'] + B['offsety']), 298 | 'start': vertex_b['A'], 299 | 'end': prev_b 300 | }) 301 | 302 | translate = None 303 | max_d = 0 304 | for i in range(0, len(vectors)): 305 | if vectors[i]['x'] == 0 and vectors[i]['y'] == 0: 306 | continue 307 | # if this vector points us back to where we came from, ignore it. 308 | # ie cross product = 0, dot product < 0 309 | if prevvector and (vectors[i]['y'] * prevvector['y'] + vectors[i]['x'] * prevvector['x']) < 0: 310 | # compare magnitude with unit vectors 311 | vectorlength = math.sqrt(vectors[i]['x']**2 + vectors[i]['y']**2) 312 | unitv = {'x': vectors[i]['x'] / vectorlength, 'y': vectors[i]['y'] / vectorlength} 313 | prevlength = math.sqrt(prevvector['x']**2+prevvector['y']**2) 314 | prevunit = {'x': prevvector['x'] / prevlength, 'y': prevvector['y'] / prevlength} 315 | 316 | # we need to scale down to unit vectors to normalize vector length. Could also just do a tan here 317 | if abs(unitv['y'] * prevunit['x'] - unitv['x'] * prevunit['y']) < 0.0001: 318 | continue 319 | 320 | d = polygon_slide_distance(A, B, vectors[i], True) 321 | vecd2 = vectors[i]['x']**2 + vectors[i]['y']**2 322 | 323 | if d is None or d**2 > vecd2: 324 | vecd = math.sqrt(vectors[i]['x']**2 + vectors[i]['y']**2) 325 | d = vecd 326 | if d and d > max_d: 327 | max_d = d 328 | translate = vectors[i] 329 | 330 | if translate is None or almost_equal(max_d, 0): 331 | NFP = None 332 | break 333 | 334 | translate['start']['marked'] = True 335 | translate['end']['marked'] = True 336 | 337 | prevvector = translate 338 | 339 | # trim 340 | vlength2 = translate['x']**2 + translate['y']**2 341 | if max_d**2 < vlength2 and not almost_equal(max_d**2, vlength2): 342 | scale = math.sqrt((max_d**2)/vlength2) 343 | translate['x'] *= scale 344 | translate['y'] *= scale 345 | 346 | referencex += translate['x'] 347 | referencey += translate['y'] 348 | 349 | if almost_equal(referencex, startx) and almost_equal(referencey, starty): 350 | # we have made a full loop 351 | break 352 | 353 | # if A and B start on a touching horizontal line, the end point may not be the start point 354 | looped = False 355 | if len(NFP) > 0: 356 | for i in range(0, len(NFP)-1): 357 | if almost_equal(referencex, NFP[i]['x'] and almost_equal(referencey, NFP[i]['y'])): 358 | looped = True 359 | 360 | if looped: 361 | # we've made a full loop 362 | break 363 | 364 | NFP.append({ 365 | 'x': referencex, 366 | 'y': referencey 367 | }) 368 | B['offsetx'] += translate['x'] 369 | B['offsety'] += translate['y'] 370 | 371 | counter += 1 372 | 373 | if NFP and len(NFP) > 0: 374 | NFP_list.append(NFP) 375 | 376 | if not search_edges: 377 | # only get outer NFP or first inner NFP 378 | break 379 | 380 | start_point = search_start_point(A, B, inside, NFP_list) 381 | 382 | return NFP_list 383 | 384 | 385 | def search_start_point(A, B, inside=True, NFP=None): 386 | """ 387 | searches for an arrangement of A and B such that they do not overlap if an NFP is given, 388 | only search for startpoints that have not already been traversed in the given NFP 389 | :param A: 390 | :param B: 391 | :param inside: 392 | :param NFP: 393 | :return: 394 | """ 395 | # clone arrays 396 | A = copy.deepcopy(A) 397 | B = copy.deepcopy(B) 398 | 399 | for i in range(0, len(A['points'])-1): 400 | if not A['points'][i].get('marked'): 401 | A['points'][i]['marked'] = True 402 | for j in range(0, len(B['points'])): 403 | B['offsetx'] = A['points'][i]['x'] - B['points'][j]['x'] 404 | B['offsety'] = A['points'][i]['y'] - B['points'][j]['y'] 405 | 406 | # 判断 A,B 是否一样 407 | # 点是否在多边形边上 408 | bin_side = None 409 | for k in range(0, len(B['points'])): 410 | inpoly = point_in_polygon( 411 | {'x': B['points'][k]['x']+B['offsetx'], 412 | 'y': B['points'][k]['y']+B['offsety']}, A) 413 | if inpoly is not None: 414 | bin_side = inpoly 415 | break 416 | 417 | if bin_side is None: 418 | return None 419 | 420 | start_point = { 421 | 'x': B['offsetx'], 422 | 'y': B['offsety'] 423 | } 424 | if ((bin_side and inside) or (not bin_side and not inside)) and ( 425 | not intersect(A, B) and not inNfp(start_point, NFP)): 426 | return start_point 427 | 428 | # slide B along vector 429 | vx = A['points'][i+1]['x'] - A['points'][i]['x'] 430 | vy = A['points'][i+1]['y'] - A['points'][i]['y'] 431 | 432 | d1 = polygon_projection_distance(A, B, {'x': vx, 'y': vy}) 433 | d2 = polygon_projection_distance(B, A, {'x': -vx, 'y': -vy}) 434 | 435 | d = None 436 | 437 | if d1 is not None and d2 is not None: 438 | d = min(d1, d2) 439 | elif d1 is None and d2 is not None: 440 | d = d2 441 | elif d1 is not None and d2 is None: 442 | d = d1 443 | 444 | # only slide until no longer negative 445 | if not (d is not None and not almost_equal(d, 0) and d > 0): 446 | continue 447 | 448 | vd2 = vx * vx + vy * vy 449 | if d * d < vd2 and not almost_equal(d*d, vd2): 450 | vd = math.sqrt(vx * vx + vy * vy) 451 | vx *= d /vd 452 | vy *= d /vd 453 | 454 | B['offsetx'] += vx 455 | B['offsety'] += vy 456 | 457 | for k in range(0, len(B['points'])): 458 | inpoly = point_in_polygon( 459 | {'x': B['points'][k]['x']+B['offsetx'], 460 | 'y': B['points'][k]['y']+B['offsety']}, A) 461 | if inpoly is not None: 462 | bin_side = inpoly 463 | break 464 | 465 | start_point = {'x': B['offsetx'], 'y': B['offsety']} 466 | if ((bin_side and inside) or (not bin_side and not inside)) and ( 467 | not intersect(A, B) and not inNfp(start_point, NFP)): 468 | return start_point 469 | 470 | return None 471 | 472 | 473 | def inNfp(p, nfp): 474 | """ 475 | returns true if point already exists in the given nfp 476 | :param p: 477 | :param nfp: 478 | :return: 479 | """ 480 | if not nfp or len(nfp) == 0: 481 | return False 482 | 483 | for i in range(0, len(nfp)): 484 | for j in range(0, len(nfp[i])): 485 | if almost_equal(p['x'], nfp[i][j]['x']) and almost_equal(p['y'], nfp[i][j]['y']): 486 | return True 487 | 488 | return False 489 | 490 | 491 | def point_in_polygon(point, polygon): 492 | if isinstance(polygon, list): 493 | polygon = {'points': polygon} 494 | if len(polygon.get('points')) < 3: 495 | return None 496 | 497 | inside = False 498 | offsetx = polygon.get('offsetx') or 0 499 | offsety = polygon.get('offsety') or 0 500 | 501 | j = len(polygon['points']) - 1 502 | for i in range(0, len(polygon['points'])): 503 | xi = polygon['points'][i]['x'] + offsetx 504 | yi = polygon['points'][i]['y'] + offsety 505 | xj = polygon['points'][j]['x'] + offsetx 506 | yj = polygon['points'][j]['y'] + offsety 507 | 508 | if almost_equal(xi, point['x']) and almost_equal(yi, point['y']): 509 | return None 510 | 511 | if on_segment({'x': xi, 'y': yi}, {'x':xj, 'y':yj}, point): 512 | return None # exactly on the segment 513 | 514 | if almost_equal(xi, xj) and almost_equal(yi, yj): 515 | # ignore very small lines 516 | continue 517 | 518 | intersect = ((yi > point['y']) != (yj > point['y'])) and (point['x'] < (xj - xi) * (point['y'] - yi) / (yj - yi) + xi) 519 | if intersect: 520 | inside = not inside 521 | 522 | return inside 523 | 524 | 525 | def intersect(A, B): 526 | 527 | a_offsetx = A['offsetx'] or 0 528 | a_offsety = A['offsety'] or 0 529 | 530 | b_offsetx = B['offsetx'] or 0 531 | b_offsety = B['offsety'] or 0 532 | 533 | A = copy.deepcopy(A) 534 | B = copy.deepcopy(B) 535 | len_a = len(A['points']) 536 | len_b = len(B['points']) 537 | for i in range(0, len_a - 1): 538 | for j in range(0, len_b - 1): 539 | a1 = {'x': A['points'][i]['x']+a_offsetx, 'y': A['points'][i]['y']+a_offsety} 540 | a2 = {'x': A['points'][i+1]['x']+a_offsetx, 'y': A['points'][i+1]['y']+a_offsety} 541 | b1 = {'x': B['points'][j]['x']+b_offsetx, 'y': B['points'][j]['y']+b_offsety} 542 | b2 = {'x': B['points'][j+1]['x']+b_offsetx, 'y': B['points'][j+1]['y']+b_offsety} 543 | 544 | pre_vb_index = len_b - 1 if j == 0 else j - 1 545 | pre_va_index = len_a - 1 if i == 0 else i - 1 546 | next_b_index = 0 if j + 1 == len_b - 1 else j + 2 547 | next_a_index = 0 if i + 1 == len_a - 1 else i + 2 548 | 549 | # go even further back if we happen to hit on a loop end point 550 | if B['points'][pre_vb_index] == B['points'][j] or almost_equal( 551 | B['points'][pre_vb_index]['x'], B['points'][j]['x']) and almost_equal( 552 | B['points'][pre_vb_index]['y'], B['points'][j]['y']): 553 | pre_vb_index = len_b - 1 if pre_vb_index == 0 else pre_vb_index - 1 554 | 555 | if A['points'][pre_va_index] == A['points'][i] or almost_equal( 556 | A['points'][pre_va_index]['x'], A['points'][i]['x']) and almost_equal( 557 | A['points'][pre_va_index]['y'], A['points'][i]['y']): 558 | pre_va_index = len_a - 1 if pre_va_index == 0 else pre_va_index - 1 559 | 560 | # go even further forward if we happen to hit on a loop end point 561 | if B['points'][next_b_index] == B['points'][j+1] or almost_equal( 562 | B['points'][next_b_index]['x'], B['points'][j+1]['x']) and almost_equal( 563 | B['points'][next_b_index]['y'], B['points'][j+1]['y']): 564 | next_b_index = 0 if next_b_index == len_b - 1 else next_b_index + 1 565 | 566 | if A['points'][next_a_index] == A['points'][i+1] or almost_equal( 567 | A['points'][next_a_index]['x'], A['points'][i+1]['x']) and almost_equal( 568 | A['points'][next_a_index]['y'], A['points'][i+1]['y']): 569 | next_a_index = 0 if next_a_index == len_a - 1 else next_a_index + 1 570 | 571 | a0 = {'x': A['points'][pre_va_index]['x']+a_offsetx, 'y': A['points'][pre_va_index]['y']+a_offsety} 572 | b0 = {'x': B['points'][pre_vb_index]['x']+b_offsetx, 'y': B['points'][pre_vb_index]['y']+b_offsety} 573 | a3 = {'x': A['points'][next_a_index]['x']+a_offsetx, 'y': A['points'][next_a_index]['y']+a_offsety} 574 | b3 = {'x': B['points'][next_b_index]['x']+b_offsetx, 'y': B['points'][next_b_index]['y']+b_offsety} 575 | 576 | if on_segment(a1, a2, b1) or almost_equal(a1['x'], b1['x']) and almost_equal(a1['y'], b1['y']): 577 | # if a point is on a segment, it could intersect or it could not. Check via the neighboring points 578 | b0in = point_in_polygon(b0, A) 579 | b2in = point_in_polygon(b2, A) 580 | if (b0in and not b2in) or (not b0in and b2in): 581 | return True 582 | else: 583 | continue 584 | 585 | if on_segment(a1, a2, b2) or almost_equal(a2['x'], b2['x']) and almost_equal(a2['y'], b2['y']): 586 | # if a point is on a segment, it could intersect or it could not.Check via the neighboring points 587 | b1in = point_in_polygon(b1, A) 588 | b3in = point_in_polygon(b3, A) 589 | if (b1in and not b3in) or (not b1in and b3in): 590 | return True 591 | else: 592 | continue 593 | 594 | if on_segment(b1, b2, a1) or almost_equal(a1['x'], b2['x']) and almost_equal(a1['y'], b2['y']): 595 | # if a point is on a segment, it could intersect or it could not.Check via the neighboring points 596 | a0in = point_in_polygon(a0, B) 597 | a2in = point_in_polygon(a2, B) 598 | if (a0in and not a2in) or (not a0in and a2in): 599 | return True 600 | else: 601 | continue 602 | 603 | if on_segment(b1, b2, a2) or almost_equal(a2['x'], b1['x']) and almost_equal(a2['y'], b1['y']): 604 | # if a point is on a segment, it could intersect or it could not.Check via the neighboring points 605 | a1in = point_in_polygon(a1, B) 606 | a3in = point_in_polygon(a3, B) 607 | if (a1in and not a3in) or (not a1in and a3in): 608 | return True 609 | else: 610 | continue 611 | 612 | if line_intersect(b1, b2, a1, a2): 613 | return True 614 | 615 | return False 616 | 617 | 618 | def line_intersect(A, B, E, F, infinite=None): 619 | """ 620 | returns the intersection of AB and EF, or null if there are no intersections or other numerical error 621 | if the infinite flag is set, AE and EF describe infinite lines without endpoints, 622 | they are finite line segments otherwise 623 | :param A: 624 | :param B: 625 | :param E: 626 | :param F: 627 | :param infinite: 628 | :return: 629 | """ 630 | a1 = B['y'] - A['y'] 631 | b1 = A['x'] - B['x'] 632 | c1 = B['x'] * A['y'] - A['x'] * B['y'] 633 | a2 = F['y'] - E['y'] 634 | b2 = E['x'] - F['y'] 635 | c2 = F['x'] * E['y'] - E['x'] * F['y'] 636 | denom = a1 * b2 - a2 * b1 637 | if denom == 0: 638 | return None 639 | x = (b1 * c2 - b2 * c1) / denom 640 | y = (a2 * c1 - a1 * c2) / denom 641 | 642 | if infinite is None: 643 | if abs(A['x'] - B['x']) > TOL: 644 | tmp = x < A['x'] or x > B['x'] if A['x'] < B['x'] else x > A['x'] or x < B['x'] 645 | if tmp: 646 | return None 647 | tmp = y < A['y'] or y > B['y'] if A['y'] < B['y'] else y > A['y'] or y < B['y'] 648 | if tmp: 649 | return None 650 | if abs(E['x'] - F['x']) > TOL: 651 | tmp = x < E['x'] or x > F['x'] if E['x'] < F['x'] else x > E['x'] or x < F['x'] 652 | if tmp: 653 | return None 654 | tmp = y < E['y'] or y > F['y'] if E['y'] < F['y'] else y > E['y'] or y < F['y'] 655 | if tmp: 656 | return None 657 | 658 | return {'x': x, 'y': y} 659 | 660 | 661 | def polygon_projection_distance(A, B, direction): 662 | """ 663 | project each point of B onto A in the given direction, and return the distance 664 | :param A: 665 | :param B: 666 | :param direction: 667 | :return: 668 | """ 669 | b_offsetx = B.get('offsetx') or 0 670 | b_offsety = B.get('offsety') or 0 671 | a_offsetx = A.get('offsetx') or 0 672 | a_offsety = A.get('offsety') or 0 673 | 674 | A = copy.deepcopy(A) 675 | B = copy.deepcopy(B) 676 | 677 | edge_a = A['points'] 678 | edge_b = B['points'] 679 | distance = None 680 | p = dict() 681 | s1 = dict() 682 | s2 = dict() 683 | for i in range(0, len(edge_b)): 684 | # the shortest/most negative projection of B onto A 685 | min_projection = minp = None 686 | for j in range(0, len(edge_a) - 1): 687 | p['x'] = edge_b[i]['x'] + b_offsetx 688 | p['y'] = edge_b[i]['y'] + b_offsety 689 | s1['x'] = edge_a[j]['x'] + a_offsetx 690 | s1['y'] = edge_a[j]['y'] + a_offsety 691 | s2['x'] = edge_a[j+1]['x'] + a_offsetx 692 | s2['y'] = edge_a[j+1]['y'] + a_offsety 693 | 694 | if abs((s2['y'] - s1['y']) * direction['x'] - (s2['x'] - s2['x']) * direction['y']) < TOL: 695 | continue 696 | 697 | # project point, ignore edge boundaries 698 | d = point_distance(p, s1, s2, direction) 699 | if d and (min_projection is None or d < min_projection): 700 | min_projection = d 701 | 702 | if min_projection and (distance is None or min_projection > distance): 703 | distance = min_projection 704 | 705 | return distance 706 | 707 | 708 | def point_distance(p, s1, s2, normal, infinite=None): 709 | normal = normalize_vector(normal) 710 | 711 | dir_point = { 712 | 'x': normal['y'], 713 | 'y': -normal['x'], 714 | } 715 | 716 | pdot = p['x'] * dir_point['x'] + p['y'] * dir_point['y'] 717 | s1dot = s1['x'] * dir_point['x'] + s1['y'] * dir_point['y'] 718 | s2dot = s2['x'] * dir_point['x'] + s2['y'] * dir_point['y'] 719 | 720 | pdotnorm = p['x']*normal['x'] + p['y'] * normal['y'] 721 | s1dotnorm = s1['x']*normal['x'] + s1['y'] * normal['y'] 722 | s2dotnorm = s2['x'] * normal['x'] + s2['y'] * normal['y'] 723 | 724 | if infinite is None: 725 | # dot doesn't collide with segment, or lies directly on the vertex 726 | if ((pdot < s1dot or almost_equal(pdot, s1dot)) and (pdot < s2dot or almost_equal(pdot, s2dot))) or ( 727 | (pdot > s1dot or almost_equal(pdot, s1dot)) and ((pdot > s2dot) or almost_equal(pdot, s2dot))): 728 | return None 729 | if (almost_equal(pdot, s1dot) and almost_equal(pdot, s2dot)) and ( 730 | pdotnorm > s1dotnorm and pdotnorm > s2dotnorm): 731 | return min(pdotnorm - s1dotnorm, pdotnorm - s2dotnorm) 732 | if almost_equal(pdot, s1dot) and almost_equal(pdot, s2dot) and pdotnorm < s1dotnorm and pdotnorm < s2dotnorm: 733 | return -min(s1dotnorm-pdotnorm, s2dotnorm-pdotnorm) 734 | 735 | return -(pdotnorm - s1dotnorm + (s1dotnorm - s2dotnorm) * (s1dot - pdot)/(s1dot - s2dot)) 736 | 737 | 738 | def polygon_slide_distance(A, B, direction, ignorenegative): 739 | 740 | b_offsetx = B.get('offsetx') or 0 741 | b_offsety = B.get('offsety') or 0 742 | a_offsetx = A.get('offsetx') or 0 743 | a_offsety = A.get('offsety') or 0 744 | 745 | A = copy.deepcopy(A) 746 | B = copy.deepcopy(B) 747 | 748 | if not A['points'][-1] == A['points'][0]: 749 | A['points'].append(A['points'][0]) 750 | if not B['points'][0] == B['points'][-1]: 751 | B['points'].append(B['points'][0]) 752 | 753 | edge_a = A['points'] 754 | edge_b = B['points'] 755 | distance = None 756 | 757 | dir_point = normalize_vector(direction) 758 | 759 | for i in range(0, len(edge_b) - 1): 760 | 761 | for j in range(0, len(edge_a) - 1): 762 | A1 = {'x': edge_a[j]['x'] + a_offsetx, 'y': edge_a[j]['y'] + a_offsety} 763 | A2 = {'x': edge_a[j+1]['x'] + a_offsetx, 'y': edge_a[j+1]['y'] + a_offsety} 764 | B1 = {'x': edge_b[i]['x'] + b_offsetx, 'y': edge_b[i]['y'] + b_offsety} 765 | B2 = {'x': edge_b[i + 1]['x'] + b_offsetx, 'y': edge_b[i + 1]['y'] + b_offsety} 766 | 767 | if (almost_equal(A1['x'], A2['x']) and almost_equal(A1['y'], A2['y'])) or almost_equal( 768 | B1['x'], B2['x']) and almost_equal(B1['y'], B2['y']): 769 | continue 770 | 771 | d = segment_distance(A1, A2, B1, B2, dir_point) 772 | if d and (distance is None or d < distance): 773 | if not ignorenegative or d > 0 or almost_equal(d, 0): 774 | distance = d 775 | return distance 776 | 777 | 778 | def segment_distance(A, B, E, F, direction): 779 | normal = { 780 | 'x': direction['y'], 781 | 'y': -direction['x'] 782 | } 783 | reverse = { 784 | 'x': -direction['x'], 785 | 'y': -direction['y'] 786 | } 787 | 788 | dot_a = A['x'] * normal['x'] + A['y'] * normal['y'] 789 | dot_b = B['x'] * normal['x'] + B['y'] * normal['y'] 790 | dot_e = E['x'] * normal['x'] + E['y'] * normal['y'] 791 | dot_f = F['x'] * normal['x'] + F['y'] * normal['y'] 792 | 793 | cross_a = A['x'] * direction['x'] + A['y'] * direction['y'] 794 | cross_b = B['x'] * direction['x'] + B['y'] * direction['y'] 795 | cross_e = E['x'] * direction['x'] + E['y'] * direction['y'] 796 | cross_f = F['x'] * direction['x'] + F['y'] * direction['y'] 797 | 798 | ab_min = min(dot_a, dot_b) 799 | ab_max = max(dot_a, dot_b) 800 | 801 | ef_min = min(dot_e, dot_f) 802 | ef_max = max(dot_e, dot_f) 803 | 804 | # segments that will touch at one point 805 | if almost_equal(ab_max, ef_min, TOL) or almost_equal(ab_min, ef_max, TOL): 806 | return None 807 | 808 | # segments miss each other completely 809 | if ab_max < ef_min or ab_min > ef_max: 810 | return None 811 | 812 | if (ab_max > ef_max and ab_min < ef_min) or (ef_max > ab_max and ef_min < ab_min): 813 | overlap = 1 814 | else: 815 | min_max = min(ab_max, ef_max) 816 | max_min = max(ab_min, ef_min) 817 | max_max = max(ab_max, ef_max) 818 | min_min = min(ab_min, ef_min) 819 | 820 | overlap = (min_max - max_min) / (max_max - min_min) 821 | 822 | cross_abe = (E['y'] - A['y']) * (B['x'] - A['x']) - (E['x'] - A['x']) * (B['y'] - A['y']) 823 | cross_abf = (F['y'] - A['y']) * (B['x'] - A['x']) - (F['x'] - A['x']) * (B['y'] - A['y']) 824 | 825 | # lines are colinear 826 | if almost_equal(cross_abe, 0) and almost_equal(cross_abf, 0): 827 | ab_norm = {'x': B['y'] - A['y'], 'y': A['x'] - B['x']} 828 | ef_norm = {'x': F['y'] - E['y'], 'y': E['x'] - F['x']} 829 | 830 | ab_norm_length = math.sqrt(ab_norm['x']**2 + ab_norm['y']**2) 831 | ab_norm['x'] /= ab_norm_length 832 | ab_norm['y'] /= ab_norm_length 833 | 834 | ef_norm_length = math.sqrt(ef_norm['x']**2 + ef_norm['y']**2) 835 | ef_norm['x'] /= ef_norm_length 836 | ef_norm['y'] /= ef_norm_length 837 | 838 | # segment normals must point in opposite directions 839 | if abs(ab_norm['y'] * ef_norm['x'] - ab_norm['x'] * ef_norm['y']) < TOL and ( 840 | ab_norm['y'] * ef_norm['y'] + ab_norm['x'] * ef_norm['x'] < 0): 841 | # normal of AB segment must point in same direction as given direction vector 842 | norm_dot = ab_norm['y'] * direction['y'] + ab_norm['x'] * direction['x'] 843 | # the segments merely slide along eachother 844 | if almost_equal(norm_dot, 0, TOL): 845 | return None 846 | 847 | if norm_dot < 0: 848 | return 0 849 | 850 | return None 851 | 852 | distances = list() 853 | 854 | # coincident points 855 | if almost_equal(dot_a, dot_e): 856 | distances.append(cross_a - cross_e) 857 | elif almost_equal(dot_a, dot_f): 858 | distances.append(cross_a - cross_f) 859 | elif ef_min < dot_a and dot_a < ef_max: 860 | d = point_distance(A, E, F, reverse) 861 | # A currently touches EF, but AB is moving away from EF 862 | if d and almost_equal(d, 0): 863 | db = point_distance(B, E, F, reverse, True) 864 | if db < 0 or almost_equal(db * overlap, 0): 865 | d = None 866 | if d: 867 | distances.append(d) 868 | 869 | if almost_equal(dot_b, dot_e): 870 | distances.append(cross_b - cross_e) 871 | elif almost_equal(dot_b, dot_f): 872 | distances.append(cross_b - cross_f) 873 | elif dot_b > ef_min and dot_b < ef_max: 874 | d = point_distance(B, E, F, reverse) 875 | 876 | if d and almost_equal(d, 0): 877 | da = point_distance(A, E, F, reverse, True) 878 | if da < 0 or almost_equal(da * overlap, 0): 879 | d = None 880 | 881 | if d: 882 | distances.append(d) 883 | 884 | if dot_e > ab_min and dot_e < ab_max: 885 | d = point_distance(E, A, B, direction) 886 | if d and almost_equal(d, 0): 887 | df = point_distance(F, A, B, direction, True) 888 | if df < 0 or almost_equal(df * overlap, 0): 889 | d = None 890 | if d: 891 | distances.append(d) 892 | 893 | if len(distances) == 0: 894 | return None 895 | 896 | if dot_f > ab_min and dot_f < ab_max: 897 | d = point_distance(F, A, B, direction) 898 | if d and almost_equal(d, 0): 899 | de = point_distance(E, A, B, direction, True) 900 | if de < 0 or almost_equal(de * overlap, 0): 901 | d = None 902 | if d: 903 | distances.append(d) 904 | 905 | if len(distances) == 0: 906 | return None 907 | 908 | return min(distances) 909 | 910 | 911 | def polygon_area(polygon): 912 | area = 0 913 | j = len(polygon) - 1 914 | for i in range(0, len(polygon)): 915 | area += (polygon[j]['x'] + polygon[i]['x']) * (polygon[j]['y'] - polygon[i]['y']) 916 | j = i 917 | 918 | return 0.5 * area 919 | 920 | 921 | def rotate_polygon(polygon, angle): 922 | rotated = {'points': list()} 923 | angle = angle * math.pi / 180 924 | for p in polygon: 925 | x = p['x'] 926 | y = p['y'] 927 | rotated['points'].append({ 928 | 'x': x * math.cos(angle) - y * math.sin(angle), 929 | 'y': x * math.sin(angle) + y * math.cos(angle) 930 | }) 931 | 932 | bounds = get_polygon_bounds(rotated['points']) 933 | rotated['x'] = bounds['x'] 934 | rotated['y'] = bounds['y'] 935 | rotated['width'] = bounds['width'] 936 | rotated['height'] = bounds['height'] 937 | return rotated 938 | 939 | 940 | def get_polygon_bounds(polygon): 941 | # 最小包络矩阵 942 | if polygon is None or len(polygon) < 3: 943 | return None 944 | 945 | xmax = polygon[0]['x'] 946 | xmin = polygon[0]['x'] 947 | ymax = polygon[0]['y'] 948 | ymin = polygon[0]['y'] 949 | 950 | for point in polygon: 951 | if point['x'] > xmax: 952 | xmax = point['x'] 953 | elif point['x'] < xmin: 954 | xmin = point['x'] 955 | if point['y'] > ymax: 956 | ymax = point['y'] 957 | elif point['y'] < ymin: 958 | ymin = point['y'] 959 | 960 | return { 961 | 'x': xmin, 962 | 'y': ymin, 963 | 'width': xmax - xmin, 964 | 'height': ymax - ymin 965 | } 966 | -------------------------------------------------------------------------------- /packing/no_fit_polygon/tools/placement_worker.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | from nfp_utls import almost_equal, rotate_polygon, get_polygon_bounds, polygon_area 4 | import copy 5 | import pyclipper 6 | 7 | 8 | class PlacementWorker(): 9 | def __init__(self, bin_polygon, paths, ids, rotations, config, nfp_cache): 10 | self.bin_polygon = bin_polygon 11 | self.paths = copy.deepcopy(paths) 12 | self.ids = ids # 图形原来的ID顺序 13 | self.rotations = rotations 14 | self.config = config 15 | self.nfpCache = nfp_cache or {} 16 | 17 | def place_paths(self): 18 | # 排列图形 19 | if self.bin_polygon is None: 20 | return None 21 | 22 | # rotate paths by given rotation 23 | rotated = list() 24 | for i in range(0, len(self.paths)): 25 | r = rotate_polygon(self.paths[i][1]['points'], self.paths[i][2]) 26 | r['rotation'] = self.paths[i][2] 27 | r['source'] = self.paths[i][1]['p_id'] 28 | r['p_id'] = self.paths[i][0] 29 | rotated.append(r) 30 | 31 | paths = rotated 32 | # 保存所有转移数据 33 | all_placements = list() 34 | # 基因组的适应值 35 | fitness = 0 36 | bin_area = abs(polygon_area(self.bin_polygon['points'])) 37 | min_width = None 38 | while len(paths) > 0: 39 | placed = list() 40 | placements = list() 41 | # add 1 for each new bin opened (lower fitness is better) 42 | fitness += 1 43 | for i in range(0, len(paths)): 44 | path = paths[i] 45 | # 图形的坐标 46 | key = json.dumps({ 47 | 'A': '-1', 48 | 'B': path['p_id'], 49 | 'inside': True, 50 | 'A_rotation': 0, 51 | 'B_rotation': path['rotation'] 52 | }) 53 | 54 | binNfp = self.nfpCache.get(key) 55 | if binNfp is None or len(binNfp) == 0: 56 | continue 57 | 58 | # part unplaceable, skip 59 | error = False 60 | 61 | # ensure all necessary NFPs exist 62 | for p in placed: 63 | key = json.dumps({ 64 | 'A': p['p_id'], 65 | 'B': path['p_id'], 66 | 'inside': False, 67 | 'A_rotation': p['rotation'], 68 | 'B_rotation': path['rotation'] 69 | }) 70 | nfp = self.nfpCache.get(key) 71 | if nfp is None: 72 | error = True 73 | break 74 | 75 | # part unplaceable, skip 76 | if error: 77 | continue 78 | 79 | position = None 80 | if len(placed) == 0: 81 | for j in range(0, len(binNfp)): 82 | for k in range(0, len(binNfp[j])): 83 | if position is None or (binNfp[j][k]['x']-path['points'][0]['x'] < position['x']): 84 | position = { 85 | 'x': binNfp[j][k]['x'] - path['points'][0]['x'], 86 | 'y': binNfp[j][k]['y'] - path['points'][0]['y'], 87 | 'p_id': path['p_id'], 88 | 'rotation': path['rotation'] 89 | } 90 | 91 | placements.append(position) 92 | placed.append(path) 93 | continue 94 | 95 | clipper_bin_nfp = list() 96 | for j in range(0, len(binNfp)): 97 | clipper_bin_nfp.append([[p['x'], p['y']] for p in binNfp[j]]) 98 | 99 | clipper = pyclipper.Pyclipper() 100 | 101 | for j in range(0, len(placed)): 102 | p = placed[j] 103 | key = json.dumps({ 104 | 'A': p['p_id'], 105 | 'B': path['p_id'], 106 | 'inside': False, 107 | 'A_rotation': p['rotation'], 108 | 'B_rotation': path['rotation'] 109 | }) 110 | nfp = self.nfpCache.get(key) 111 | 112 | if nfp is None: 113 | continue 114 | for k in range(0, len(nfp)): 115 | clone = [[np['x'] + placements[j]['x'], np['y'] + placements[j]['y']] for np in nfp[k]] 116 | clone = pyclipper.CleanPolygon(clone) 117 | if len(clone) > 2: 118 | clipper.AddPath(clone, pyclipper.PT_SUBJECT, True) 119 | combine_nfp = clipper.Execute(pyclipper.CT_UNION, pyclipper.PFT_NONZERO, pyclipper.PFT_NONZERO) 120 | if len(combine_nfp) == 0: 121 | continue 122 | 123 | clipper = pyclipper.Pyclipper() 124 | clipper.AddPaths(combine_nfp, pyclipper.PT_CLIP, True) 125 | try: 126 | clipper.AddPaths(clipper_bin_nfp, pyclipper.PT_SUBJECT, True) 127 | except: 128 | print u'图形坐标出错', clipper_bin_nfp 129 | 130 | # choose placement that results in the smallest bounding box 131 | finalNfp = clipper.Execute(pyclipper.CT_DIFFERENCE, pyclipper.PFT_NONZERO, pyclipper.PFT_NONZERO) 132 | if len(finalNfp) == 0: 133 | continue 134 | finalNfp = pyclipper.CleanPolygons(finalNfp) 135 | 136 | for j in range(len(finalNfp)-1, -1, -1): 137 | if len(finalNfp[j]) < 3: 138 | finalNfp.pop(j) 139 | if len(finalNfp) == 0: 140 | continue 141 | 142 | finalNfp = [[{'x': p[0], 'y': p[1]}for p in polygon] for polygon in finalNfp] 143 | 144 | min_width = None 145 | min_area = None 146 | min_x = None 147 | 148 | for nf in finalNfp: 149 | 150 | if abs(polygon_area(nf)) < 2: 151 | continue 152 | 153 | for p_nf in nf: 154 | # 生成nfp多边形 155 | all_points = list() 156 | for m in range(0, len(placed)): 157 | for p in placed[m]['points']: 158 | all_points.append({ 159 | 'x': p['x']+placements[m]['x'], 160 | 'y': p['y']+placements[m]['y'] 161 | }) 162 | # path 坐标 163 | shift_vector = { 164 | 'x': p_nf['x'] - path['points'][0]['x'], 165 | 'y': p_nf['y'] - path['points'][0]['y'], 166 | 'p_id': path['p_id'], 167 | 'rotation': path['rotation'], 168 | } 169 | 170 | # 找新坐标后的最小矩形 171 | for m in range(0, len(path['points'])): 172 | all_points.append({ 173 | 'x': path['points'][m]['x'] + shift_vector['x'], 174 | 'y': path['points'][m]['y'] + shift_vector['y'] 175 | }) 176 | 177 | rect_bounds = get_polygon_bounds(all_points) 178 | # weigh width more, to help compress in direction of gravity 179 | area = rect_bounds['width'] * 2 + rect_bounds['height'] 180 | 181 | if (min_area is None or area < min_area or almost_equal(min_area, area)) and ( 182 | min_x is None or shift_vector['x'] <= min_x): 183 | min_area = area 184 | min_width = rect_bounds['width'] 185 | position = shift_vector 186 | min_x = shift_vector['x'] 187 | 188 | if position: 189 | placed.append(path) 190 | placements.append(position) 191 | 192 | if min_width: 193 | fitness += min_width / bin_area 194 | 195 | for p in placed: 196 | p_id = paths.index(p) 197 | if p_id >= 0: 198 | paths.pop(p_id) 199 | 200 | if placements and len(placements) > 0: 201 | all_placements.append(placements) 202 | 203 | else: 204 | # something went wrong 205 | break 206 | 207 | fitness += 2 * len(paths) 208 | 209 | return {'placements': all_placements, 'fitness': fitness, 'paths': paths, 'area': bin_area} 210 | -------------------------------------------------------------------------------- /packing/tasks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liangxuCHEN/irregular_packing_sys/00c7add9a50d7fb136e0cac2f4850a5067a3ab91/packing/tasks/__init__.py -------------------------------------------------------------------------------- /packing/tasks/irregular_package.py: -------------------------------------------------------------------------------- 1 | # encoding=utf-8 2 | import sys 3 | #sys.path.append("/home/louis/irregular_packing_sys") 4 | sys.path.append("/home/django/irregular_packing_sys") 5 | 6 | import json 7 | import os 8 | from packing.no_fit_polygon.nfp_tools import shape_use 9 | from packing.no_fit_polygon.sql import has_same_job, copy_same_job, insert_new_job,\ 10 | update_job_status, insert_job_result 11 | 12 | from mrq.task import Task 13 | from mrq.context import run_task, log 14 | from mrq.job import queue_job, get_job_result 15 | 16 | FINISH_STATUS = u'运算结束' 17 | #HOST_URL = 'http://192.168.126.129:8585' 18 | HOST_URL = 'http://192.168.3.172:8585' 19 | 20 | 21 | def wait_for_job(path, params, **kwargs): 22 | job_id = queue_job(path, params, **kwargs) 23 | 24 | while True: 25 | time.sleep(5) 26 | res = get_job_result(job_id) 27 | if res["status"] == "success": 28 | return res.get("result") 29 | elif res["status"] not in ["queued", "started", "interrupt"]: 30 | raise Exception("Job %s was in status %s" % ( 31 | path, res.get("status") 32 | )) 33 | 34 | 35 | class CreateTask(Task): 36 | """ 37 | 通过 Source name 把任务分配到不同的管道中 38 | """ 39 | def run(self, params): 40 | if params["source_name"] == 'PackingTask': 41 | result = subtask( 42 | "tasks.package.%s" % params["source_name"], 43 | params["post_data"], 44 | queue='packing' 45 | ) 46 | 47 | return result 48 | 49 | def save_project(Project_model, PackDetail_model, result, input_data): 50 | """ 51 | 结算结果保存 52 | :param input_data: 53 | :return: 54 | """ 55 | project = Project_model( 56 | comment=input_data['comment'], 57 | data_input=input_data['data'] 58 | ) 59 | project.save() 60 | # save product 61 | total_price = 0 62 | for material, value in result.items(): 63 | total_price += value['total_price'] 64 | pack_detail = PackDetail_model( 65 | material_guid=material, 66 | material_name=value['material_name'], 67 | material_code=value['material_code'], 68 | unit=value['unit'], 69 | total_price=value['total_price'], 70 | pic_path=value['file_name'], 71 | dxf_models=str(value['model'])[1:-1], 72 | use_width=value['use_width'], 73 | price=value['price'], 74 | width=value['width'], 75 | areas=value['areas'], 76 | ) 77 | pack_detail.save() 78 | project.products.add(pack_detail) 79 | 80 | project.save() 81 | return project, total_price 82 | 83 | 84 | 85 | 86 | class BaseTask(Task): 87 | 88 | def connect(self): 89 | """ 90 | load django settings for using the model 91 | :return: 92 | """ 93 | import django 94 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "irregular_packing.settings") 95 | django.setup() 96 | 97 | def run(self, params): 98 | pass 99 | 100 | 101 | class PackingTask(BaseTask): 102 | 103 | def run(self, params): 104 | params = params['post_data'] 105 | # params = params['data'] 106 | log.info(params) 107 | # 项目是否已经存在 108 | same_job = has_same_job(params) 109 | log.info('has same job ?', same_job) 110 | if same_job: 111 | if same_job['status'] == FINISH_STATUS and same_job['url'] != '' and same_job['total_price'] != '': 112 | # 拷贝一份 113 | same_job['new_guid'] = copy_same_job(same_job, params) 114 | return {'data': same_job, 'message': 'has the same job', 'status': 0} 115 | 116 | # 添加新任务 117 | job_guid = insert_new_job(params) 118 | 119 | self.connect() 120 | from packing.models import Project, PackDetail 121 | 122 | # 查看是否有重复计算 123 | project = Project.objects.filter(data_input=params['data']).last() 124 | if project: 125 | log.info('has the same project data') 126 | total_price = 0 127 | all_products = project.products.all() 128 | if project.comment != params['comment']: 129 | # 描述不一样,新增一个 130 | project.comment = params['project_comment'] 131 | 132 | project.pk = None 133 | project.save() 134 | for product in all_products: 135 | total_price += total_price + product.total_price 136 | project.products.add(product) 137 | else: 138 | for product in all_products: 139 | total_price += total_price + product.total_price 140 | 141 | url = '%s/product_detail/%d' % (HOST_URL, project.id) 142 | # 更新任务状态 143 | update_job_status( 144 | job_guid, 145 | FINISH_STATUS, 146 | url=url, 147 | price=total_price 148 | ) 149 | insert_job_result(job_guid, all_products) 150 | 151 | return { 152 | 'data': { 153 | 'project_id': project.id, 154 | 'url': url, 155 | 'price': total_price 156 | }, 157 | 'message': 'the project had been done', 158 | 'status': 0 159 | } 160 | 161 | res = shape_use(params) 162 | if res['is_error']: 163 | log.error(res['error_info']) 164 | 165 | update_job_status(job_guid, res['error_info']) 166 | return { 167 | 'data': '', 168 | 'status': 10, 169 | 'message': res['error_info'] 170 | } 171 | else: 172 | # 保存结果 173 | # 更新任务中间状态 174 | # update_job_status(job_guid, u'正在保存结果') 175 | log.info('saving the result into project') 176 | try: 177 | project, total_price = save_project(Project, PackDetail, res['data'], params) 178 | except Exception as e: 179 | log.error(e) 180 | # 更新任务失败状态 181 | update_job_status(job_guid, u'保存结果失败') 182 | return {'data': res, 'message': 'error in save the result into project', 'status': 100} 183 | 184 | log.info('update job status and finish') 185 | # 完结任务状态 186 | url = '%s/product_detail/%d' % (HOST_URL, project.id) 187 | # 更新任务状态 188 | update_job_status( 189 | job_guid, 190 | FINISH_STATUS, 191 | url=url, 192 | price=total_price 193 | ) 194 | insert_job_result(job_guid, project.products.all()) 195 | 196 | if project: 197 | res['new_project_id'] = project.id 198 | res['total_price'] = total_price 199 | 200 | return {'data': res, 'message': 'OK', 'status': 0} 201 | 202 | 203 | -------------------------------------------------------------------------------- /packing/templates/add_dxf_model.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}异形排布{% endblock %} 3 | {% block head %} 4 | 5 | {% endblock%} 6 | {% block content %} 7 | {% if info %} 8 | 9 | {% endif %} 10 |
11 | {% if form %} 12 |
13 | {% csrf_token %} 14 | {% for field in form %} 15 |
16 | 17 | {{ field }} 18 | {{ field.errors }} 19 |
20 | {% endfor %} 21 | 22 |
23 | {% endif %} 24 | {% endblock %} 25 |
26 | {% block script %} 27 | 28 | {% endblock%} -------------------------------------------------------------------------------- /packing/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {% block head %} 15 | {% block title %}Irregular packing{% endblock %} 16 | {% endblock %} 17 | 18 | 19 | 20 | 21 |
{% block content %}{% endblock %}
22 | 23 | 28 | 29 | 30 | 31 | {% block script %} 32 | 33 | {% endblock %} 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /packing/templates/calc_shape.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}添加series任务{% endblock %} 3 | {% block content %} 4 |
5 |

计算模型的图形数量

6 |

输入例子:[{"Guid":"2deb3c09-1c45-4b43-9eb9-9b87f36ab396", "Amount":4}]

7 |
8 | {% csrf_token %} 9 |
10 | 11 |
12 |
13 | 14 |
15 |

计算模型图像排列用量

16 |

输入例子:[{"Guid":"5e662fc6-3dfa-494b-8132-2edc8b658814", "Amount":1},{"Guid":"2deb3c09-1c45-4b43-9eb9-9b87f36ab396", "Amount":1}]

17 |
18 | {% csrf_token %} 19 |
20 |
21 | 初级优化
22 | 中级优化
23 | 高级优化
24 | 25 |
26 |
27 | 28 |
29 |

V2排列用量

30 |
31 | {% csrf_token %} 32 |
33 |

ex: [{"Guid":"1d64d7a0-00c8-4717-89ad-cb7d13239e12", "Amount":2}]

34 |
35 |

ex: [{"material_code":"31050355","material_name": "JJ401G-3", "model_name": "模板1","amount":2}]

36 |
37 | 初级优化
38 | 中级优化
39 | 高级优化
40 | 41 |
42 |
43 | {% endblock %} -------------------------------------------------------------------------------- /packing/templates/dxf_index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}模板列表{% endblock %} 3 | {% block content %} 4 | {% if info %} 5 |

{{info}}

6 | {% endif %} 7 | {% if dxf_list %} 8 | 9 | 10 | 11 | {% for project in dxf_list %} 12 | 13 | 14 | 15 | 16 | {% endfor %} 17 | 18 |
#名称路径
{{ project.id }}{{ project.name }}{{ project.uploads.url }}
19 | {% if is_paginated %} 20 | 33 | {%endif%} 34 | {% else %} 35 |

没有报告

36 | {% endif %} 37 |
38 | 添加新的模型 39 | {% endblock%} -------------------------------------------------------------------------------- /packing/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}异形排布{% endblock %} 3 | {% block content %} 4 |
5 |

欢迎来到这里

6 |
7 |

模式列表

8 |

进入

9 |

计算模型

10 |

进入

11 |

方案列表

12 |

进入

13 | {% endblock %} -------------------------------------------------------------------------------- /packing/templates/project_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}方案详情{% endblock %} 3 | {% block content %} 4 | {% if error %} 5 |

{{error}}

6 | {% else %} 7 |
8 |

创建时间: {{created| date:"Y年n月d日 H:i" }}

9 | {% if comments %} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | {% for data in comments %} 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | {% endfor %} 30 | 31 |
材料编号材料名称模型名称模型ID使用数量
{{data.material_code}}{{data.material_name}}{{data.name}}{{data.model_id}}{{data.amount}}
32 | {% endif %} 33 | {% if comment_text %} 34 |

描述:{{comment_text}}

35 | {% endif %} 36 |
37 |
38 |
39 |
40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | {%for abin in bin_list %} 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | {% endfor %} 68 | 69 |
材料编号材料名称单位单价使用长度使用面积总价包含模型(ID)排版图片
{{abin.material_code}}{{abin.material_name}}{{abin.unit}}{{abin.price}}{{abin.use_width}}{{abin.areas}}{{abin.total_price}}{{abin.dxf_models}}打开
70 |
71 | {% endif %} 72 | 73 | {% endblock%} 74 | -------------------------------------------------------------------------------- /packing/templates/projects.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}模板列表{% endblock %} 3 | {% block content %} 4 | {% if info %} 5 |

{{info}}

6 | {% endif %} 7 | {% if project_list %} 8 | 9 | 10 | 11 | {% for project in project_list %} 12 | 13 | 14 | 15 | 16 | 17 | {% endfor %} 18 | 19 |
#日期详细
{{ project.id }}{{ project.created }} 打开
20 | {% if is_paginated %} 21 | 34 | {%endif%} 35 | {% else %} 36 |

没有报告

37 | {% endif %} 38 |
39 | {% endblock%} -------------------------------------------------------------------------------- /packing/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /packing/tools.py: -------------------------------------------------------------------------------- 1 | # encoding=utf8 2 | 3 | 4 | def handle_uploaded_file(file, path): 5 | try: 6 | with open(path, 'wb+') as destination: 7 | for chunk in file.chunks(): 8 | destination.write(chunk) 9 | return True 10 | except: 11 | return False 12 | -------------------------------------------------------------------------------- /packing/views.py: -------------------------------------------------------------------------------- 1 | # encoding=utf8 2 | import os 3 | import json 4 | from datetime import datetime as dt 5 | 6 | from django.core import serializers 7 | from django.shortcuts import render, get_object_or_404 8 | from django.http import HttpResponse, HttpResponseRedirect, StreamingHttpResponse 9 | from django.views.decorators.csrf import csrf_exempt 10 | from django.views import generic 11 | from packing.models import DxfModel, Project, PackDetail 12 | from packing.forms import DxfForm 13 | 14 | from packing.no_fit_polygon.tools import input_utls 15 | from packing.no_fit_polygon.nfp_tools import shape_num, shape_use 16 | #from tasks.irregular_package import PackingTask 17 | from packing.no_fit_polygon.sql import jobs_list 18 | 19 | #from mrq.job import queue_job, Job 20 | 21 | 22 | def allow_all(response): 23 | """ 24 | 解决跨域的问题 25 | :param response: 26 | :return: 27 | """ 28 | response["Access-Control-Allow-Origin"] = "*" 29 | response["Access-Control-Allow-Methods"] = "POST, GET, OPTIONS" 30 | response["Access-Control-Max-Age"] = "1000" 31 | response["Access-Control-Allow-Headers"] = "*" 32 | return response 33 | 34 | 35 | def home_page(request): 36 | return render(request, 'index.html') 37 | 38 | 39 | class DxfModelIndexView(generic.ListView): 40 | model = DxfModel 41 | template_name = "dxf_index.html" 42 | paginate_by = 10 # 一个页面显示的条目 43 | context_object_name = "dxf_list" 44 | 45 | 46 | class ProjectIndexView(generic.ListView): 47 | model = Project 48 | template_name = "projects.html" 49 | paginate_by = 10 # 一个页面显示的条目 50 | context_object_name = "project_list" 51 | 52 | 53 | def dxf_json(request): 54 | query = DxfModel.objects.all() 55 | if request.GET.get('name'): 56 | query = query.filter(name__contains=request.GET.get('name')) 57 | 58 | if request.GET.get('material_guid'): 59 | query = query.filter(material_guid=request.GET.get('material_guid')) 60 | 61 | content = serializers.serialize("json", query) 62 | response = HttpResponse(content, content_type="application/json") 63 | return allow_all(response) 64 | 65 | 66 | def add_dxf_model(request): 67 | if request.method == 'POST': 68 | form = DxfForm(request.POST, request.FILES) 69 | content = { 70 | 'form': form 71 | } 72 | if form.is_valid(): 73 | dxf_model = form.save() 74 | try: 75 | # 计算模型包含的图形数量 76 | s = input_utls.input_polygon(dxf_model.uploads.path) 77 | dxf_model.shape_num = len(s) 78 | dxf_model.save() 79 | 80 | content = json.dumps({ 81 | 'data': {'num_shape': dxf_model.shape_num}, 82 | 'status': 0, 83 | 'message': 'OK' 84 | }, ensure_ascii=False) 85 | response = HttpResponse(content, content_type="application/json") 86 | except: 87 | dxf_model.delete() 88 | content['info'] = u'上传的文件不符合格式,请确保是DXF文件,并且是炸开为线段,从设备导出的DXF是不能直接使用' 89 | response = render(request, 'add_dxf_model.html', content) 90 | # response = HttpResponse(json.dumps({'info': '读取文件出错'}), content_type="application/json") 91 | else: 92 | # response = render(request, 'add_dxf_model.html', {'info': u'缺少文件'}) 93 | content['info'] = u'缺少文件' 94 | response = render(request, 'add_dxf_model.html', content) 95 | else: 96 | # form = DxfForm(initial={ 97 | # "material_guid": request.GET.get('material_guid'), 98 | # 'model_guid': request.GET.get('model_guid') 99 | # }) 100 | response = render(request, 'add_dxf_model.html', {'form': DxfForm()}) 101 | 102 | return response 103 | 104 | 105 | @csrf_exempt 106 | def calc_shape_num(request): 107 | if request.method == 'POST': 108 | print request.POST 109 | res = shape_num(request.POST) 110 | if res['is_error']: 111 | content = json.dumps({ 112 | 'data': '', 113 | 'status': 0, 114 | 'message': res['error_info'] 115 | }, ensure_ascii=False) 116 | else: 117 | content = json.dumps({ 118 | 'data': res['data'], 119 | 'status': 0, 120 | 'message': 'OK' 121 | }, ensure_ascii=False) 122 | response = HttpResponse(content, content_type="application/json") 123 | else: 124 | response = render(request, 'calc_shape.html') 125 | 126 | return allow_all(response) 127 | 128 | 129 | @csrf_exempt 130 | def calc_shape_use(request): 131 | if request.method == 'POST': 132 | res = shape_use(request.POST) 133 | if res['is_error']: 134 | content = json.dumps({ 135 | 'data': '', 136 | 'status': 0, 137 | 'message': res['error_info'] 138 | }, ensure_ascii=False) 139 | else: 140 | content = json.dumps({ 141 | 'data': res['data'], 142 | 'status': 0, 143 | 'message': 'OK' 144 | }, ensure_ascii=False) 145 | response = HttpResponse(content, content_type="application/json") 146 | else: 147 | response = render(request, 'calc_shape.html') 148 | 149 | return allow_all(response) 150 | 151 | 152 | @csrf_exempt 153 | def shape_use_task(request): 154 | if request.method == 'POST': 155 | taskparams = dict() 156 | taskparams['post_data'] = request.POST 157 | job_id = queue_job("tasks.irregular_package.PackingTask", taskparams) 158 | print job_id 159 | response = HttpResponse(json.dumps({'job_id': str(job_id)}), content_type="application/json") 160 | else: 161 | response = render(request, 'calc_shape.html') 162 | 163 | return allow_all(response) 164 | 165 | 166 | def show_project(request, p_id): 167 | project = get_object_or_404(Project, pk=p_id) 168 | bin_list = project.products.all() 169 | content = { 170 | 'created': project.created, 171 | 'bin_list': bin_list, 172 | 'host': request.get_host() 173 | } 174 | try: 175 | content['comments'] = json.loads(project.comment) 176 | except: 177 | content['comment_text'] = project.comment 178 | return render(request, 'project_detail.html', content) 179 | 180 | 181 | def get_jobs_list(request): 182 | response = HttpResponse(jobs_list(), content_type="application/json") 183 | return allow_all(response) 184 | 185 | 186 | 187 | -------------------------------------------------------------------------------- /static/css/Common.css: -------------------------------------------------------------------------------- 1 | * 2 | { 3 | outline: none; 4 | } 5 | body, div, dl, dt, dd, ul, ol, li, h1, h2, h3, h4, h5, h6, hr, pre, code, form, fieldset, legend, p, blockquote, table, th, tr, td 6 | { 7 | margin: 0; 8 | padding: 0; 9 | border: 0; 10 | outline: none; 11 | } 12 | body 13 | { 14 | background: #f6f6f6; 15 | color: #333; 16 | font-size: 12px; 17 | font-family: "Microsoft YaHei" , "SimSun" , Arial; 18 | } 19 | img 20 | { 21 | border: none; 22 | border-style: none; 23 | } 24 | a:focus 25 | { 26 | -moz-outline-style: none; 27 | outline: none; 28 | } 29 | .wbody{ 30 | margin-top:0px !important; 31 | } 32 | ol, ul, li 33 | { 34 | list-style: none; 35 | } 36 | a:hover 37 | { 38 | text-decoration: none; 39 | } 40 | a:visited 41 | { 42 | text-decoration: none; 43 | } 44 | a:link 45 | { 46 | text-decoration: none; 47 | } 48 | a:active 49 | { 50 | text-decoration: none; 51 | } 52 | a.backToTop 53 | { 54 | width:60px; 55 | height:60px; 56 | background:#eaeaea url(../ref/top.gif) no-repeat -51px 0; 57 | text-indent:-999em; 58 | } 59 | a.backToTop:hover 60 | { 61 | background-position:-113px 0; 62 | } 63 | #tips_box { 64 | display: none; 65 | width: 300px; 66 | border: 1px solid #333; 67 | padding: 12px; 68 | text-align: left; 69 | text-indent: 2em; 70 | position: fixed; 71 | z-index: 99999999; 72 | background-color: #2dc3e8 !important; 73 | color: #fff; 74 | } 75 | #filter-container 76 | { 77 | padding-bottom:5px; 78 | padding-top:5px; 79 | } 80 | .index-sidebar-menu-add 81 | { 82 | overflow:auto; 83 | overflow-x:hidden; 84 | } 85 | -------------------------------------------------------------------------------- /static/css/bootstrap-select.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap-select v1.7.2 (http://silviomoreto.github.io/bootstrap-select) 3 | * 4 | * Copyright 2013-2015 bootstrap-select 5 | * Licensed under MIT (https://github.com/silviomoreto/bootstrap-select/blob/master/LICENSE) 6 | */ 7 | 8 | .bootstrap-select { 9 | width: 220px \0; 10 | /*IE9 and below*/ 11 | } 12 | .bootstrap-select > .dropdown-toggle { 13 | width: 100%; 14 | padding-right: 25px; 15 | } 16 | .has-error .bootstrap-select .dropdown-toggle, 17 | .error .bootstrap-select .dropdown-toggle { 18 | border-color: #b94a48; 19 | } 20 | .bootstrap-select.fit-width { 21 | width: auto !important; 22 | } 23 | .bootstrap-select:not([class*="col-"]):not([class*="form-control"]):not(.input-group-btn) { 24 | /*width: 220px;*/ 25 | } 26 | .bootstrap-select .dropdown-toggle:focus { 27 | outline: thin dotted #333333 !important; 28 | outline: 5px auto -webkit-focus-ring-color !important; 29 | outline-offset: -2px; 30 | } 31 | .bootstrap-select.form-control { 32 | margin-bottom: 0; 33 | padding: 0; 34 | border: none; 35 | } 36 | .bootstrap-select.form-control:not([class*="col-"]) { 37 | width: 100%; 38 | } 39 | .bootstrap-select.form-control.input-group-btn { 40 | z-index: auto; 41 | } 42 | .bootstrap-select.btn-group:not(.input-group-btn), 43 | .bootstrap-select.btn-group[class*="col-"] { 44 | float: none; 45 | display: inline-block; 46 | margin-left: 0; 47 | } 48 | .bootstrap-select.btn-group.dropdown-menu-right, 49 | .bootstrap-select.btn-group[class*="col-"].dropdown-menu-right, 50 | .row .bootstrap-select.btn-group[class*="col-"].dropdown-menu-right { 51 | float: right; 52 | } 53 | .form-inline .bootstrap-select.btn-group, 54 | .form-horizontal .bootstrap-select.btn-group, 55 | .form-group .bootstrap-select.btn-group { 56 | margin-bottom: 0; 57 | } 58 | .form-group-lg .bootstrap-select.btn-group.form-control, 59 | .form-group-sm .bootstrap-select.btn-group.form-control { 60 | padding: 0; 61 | } 62 | .form-inline .bootstrap-select.btn-group .form-control { 63 | width: 100%; 64 | } 65 | .bootstrap-select.btn-group.disabled, 66 | .bootstrap-select.btn-group > .disabled { 67 | cursor: not-allowed; 68 | } 69 | .bootstrap-select.btn-group.disabled:focus, 70 | .bootstrap-select.btn-group > .disabled:focus { 71 | outline: none !important; 72 | } 73 | .bootstrap-select.btn-group .dropdown-toggle .filter-option { 74 | display: inline-block; 75 | overflow: hidden; 76 | width: 100%; 77 | text-align: left; 78 | } 79 | .bootstrap-select.btn-group .dropdown-toggle .caret { 80 | position: absolute; 81 | top: 50%; 82 | right: 12px; 83 | margin-top: -2px; 84 | vertical-align: middle; 85 | } 86 | .bootstrap-select.btn-group[class*="col-"] .dropdown-toggle { 87 | width: 100%; 88 | } 89 | .bootstrap-select.btn-group .dropdown-menu { 90 | min-width: 100%; 91 | z-index: 1035; 92 | -webkit-box-sizing: border-box; 93 | -moz-box-sizing: border-box; 94 | box-sizing: border-box; 95 | } 96 | .bootstrap-select.btn-group .dropdown-menu.inner { 97 | position: static; 98 | float: none; 99 | border: 0; 100 | padding: 0; 101 | margin: 0; 102 | border-radius: 0; 103 | -webkit-box-shadow: none; 104 | box-shadow: none; 105 | } 106 | .bootstrap-select.btn-group .dropdown-menu li { 107 | position: relative; 108 | } 109 | .bootstrap-select.btn-group .dropdown-menu li.active small { 110 | color: #fff; 111 | } 112 | .bootstrap-select.btn-group .dropdown-menu li.disabled a { 113 | cursor: not-allowed; 114 | } 115 | .bootstrap-select.btn-group .dropdown-menu li a { 116 | cursor: pointer; 117 | } 118 | .bootstrap-select.btn-group .dropdown-menu li a.opt { 119 | position: relative; 120 | padding-left: 2.25em; 121 | } 122 | .bootstrap-select.btn-group .dropdown-menu li a span.check-mark { 123 | display: none; 124 | } 125 | .bootstrap-select.btn-group .dropdown-menu li a span.text { 126 | display: inline-block; 127 | } 128 | .bootstrap-select.btn-group .dropdown-menu li small { 129 | padding-left: 0.5em; 130 | } 131 | .bootstrap-select.btn-group .dropdown-menu .notify { 132 | position: absolute; 133 | bottom: 5px; 134 | width: 96%; 135 | margin: 0 2%; 136 | min-height: 26px; 137 | padding: 3px 5px; 138 | background: #f5f5f5; 139 | border: 1px solid #e3e3e3; 140 | -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); 141 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); 142 | pointer-events: none; 143 | opacity: 0.9; 144 | -webkit-box-sizing: border-box; 145 | -moz-box-sizing: border-box; 146 | box-sizing: border-box; 147 | } 148 | .bootstrap-select.btn-group .no-results { 149 | padding: 3px; 150 | background: #f5f5f5; 151 | margin: 0 5px; 152 | white-space: nowrap; 153 | } 154 | .bootstrap-select.btn-group.fit-width .dropdown-toggle .filter-option { 155 | position: static; 156 | } 157 | .bootstrap-select.btn-group.fit-width .dropdown-toggle .caret { 158 | position: static; 159 | top: auto; 160 | margin-top: -1px; 161 | } 162 | .bootstrap-select.btn-group.show-tick .dropdown-menu li.selected a span.check-mark { 163 | position: absolute; 164 | display: inline-block; 165 | right: 15px; 166 | margin-top: 5px; 167 | } 168 | .bootstrap-select.btn-group.show-tick .dropdown-menu li a span.text { 169 | margin-right: 34px; 170 | } 171 | .bootstrap-select.show-menu-arrow.open > .dropdown-toggle { 172 | z-index: 1036; 173 | } 174 | .bootstrap-select.show-menu-arrow .dropdown-toggle:before { 175 | content: ''; 176 | border-left: 7px solid transparent; 177 | border-right: 7px solid transparent; 178 | border-bottom: 7px solid rgba(204, 204, 204, 0.2); 179 | position: absolute; 180 | bottom: -4px; 181 | left: 9px; 182 | display: none; 183 | } 184 | .bootstrap-select.show-menu-arrow .dropdown-toggle:after { 185 | content: ''; 186 | border-left: 6px solid transparent; 187 | border-right: 6px solid transparent; 188 | border-bottom: 6px solid white; 189 | position: absolute; 190 | bottom: -4px; 191 | left: 10px; 192 | display: none; 193 | } 194 | .bootstrap-select.show-menu-arrow.dropup .dropdown-toggle:before { 195 | bottom: auto; 196 | top: -3px; 197 | border-top: 7px solid rgba(204, 204, 204, 0.2); 198 | border-bottom: 0; 199 | } 200 | .bootstrap-select.show-menu-arrow.dropup .dropdown-toggle:after { 201 | bottom: auto; 202 | top: -3px; 203 | border-top: 6px solid white; 204 | border-bottom: 0; 205 | } 206 | .bootstrap-select.show-menu-arrow.pull-right .dropdown-toggle:before { 207 | right: 12px; 208 | left: auto; 209 | } 210 | .bootstrap-select.show-menu-arrow.pull-right .dropdown-toggle:after { 211 | right: 13px; 212 | left: auto; 213 | } 214 | .bootstrap-select.show-menu-arrow.open > .dropdown-toggle:before, 215 | .bootstrap-select.show-menu-arrow.open > .dropdown-toggle:after { 216 | display: block; 217 | } 218 | .bs-searchbox, 219 | .bs-actionsbox, 220 | .bs-donebutton { 221 | padding: 4px 8px; 222 | } 223 | .bs-actionsbox { 224 | float: left; 225 | width: 100%; 226 | -webkit-box-sizing: border-box; 227 | -moz-box-sizing: border-box; 228 | box-sizing: border-box; 229 | } 230 | .bs-actionsbox .btn-group button { 231 | width: 50%; 232 | } 233 | .bs-donebutton { 234 | float: left; 235 | width: 100%; 236 | -webkit-box-sizing: border-box; 237 | -moz-box-sizing: border-box; 238 | box-sizing: border-box; 239 | } 240 | .bs-donebutton .btn-group button { 241 | width: 100%; 242 | } 243 | .bs-searchbox + .bs-actionsbox { 244 | padding: 0 8px 4px; 245 | } 246 | .bs-searchbox .form-control { 247 | margin-bottom: 0; 248 | width: 100%; 249 | } 250 | select.bs-select-hidden, 251 | select.selectpicker { 252 | display: none !important; 253 | } 254 | select.mobile-device { 255 | position: absolute !important; 256 | top: 0; 257 | left: 0; 258 | display: block !important; 259 | width: 100%; 260 | height: 100% !important; 261 | opacity: 0; 262 | } 263 | /*# sourceMappingURL=bootstrap-select.css.map */ -------------------------------------------------------------------------------- /static/css/bootstrap-theme.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.1 (http://getbootstrap.com) 3 | * Copyright 2011-2014 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */.btn-default,.btn-primary,.btn-success,.btn-info,.btn-warning,.btn-danger{text-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075)}.btn-default:active,.btn-primary:active,.btn-success:active,.btn-info:active,.btn-warning:active,.btn-danger:active,.btn-default.active,.btn-primary.active,.btn-success.active,.btn-info.active,.btn-warning.active,.btn-danger.active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-default .badge,.btn-primary .badge,.btn-success .badge,.btn-info .badge,.btn-warning .badge,.btn-danger .badge{text-shadow:none}.btn:active,.btn.active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-o-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e0e0e0));background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc}.btn-default:hover,.btn-default:focus{background-color:#e0e0e0;background-position:0 -15px}.btn-default:active,.btn-default.active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-default:disabled,.btn-default[disabled]{background-color:#e0e0e0;background-image:none}.btn-primary{background-image:-webkit-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-o-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#265a88));background-image:linear-gradient(to bottom,#337ab7 0,#265a88 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#245580}.btn-primary:hover,.btn-primary:focus{background-color:#265a88;background-position:0 -15px}.btn-primary:active,.btn-primary.active{background-color:#265a88;border-color:#245580}.btn-primary:disabled,.btn-primary[disabled]{background-color:#265a88;background-image:none}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#419641));background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:hover,.btn-success:focus{background-color:#419641;background-position:0 -15px}.btn-success:active,.btn-success.active{background-color:#419641;border-color:#3e8f3e}.btn-success:disabled,.btn-success[disabled]{background-color:#419641;background-image:none}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#2aabd2));background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:hover,.btn-info:focus{background-color:#2aabd2;background-position:0 -15px}.btn-info:active,.btn-info.active{background-color:#2aabd2;border-color:#28a4c9}.btn-info:disabled,.btn-info[disabled]{background-color:#2aabd2;background-image:none}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#eb9316));background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:hover,.btn-warning:focus{background-color:#eb9316;background-position:0 -15px}.btn-warning:active,.btn-warning.active{background-color:#eb9316;border-color:#e38d13}.btn-warning:disabled,.btn-warning[disabled]{background-color:#eb9316;background-image:none}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c12e2a));background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:hover,.btn-danger:focus{background-color:#c12e2a;background-position:0 -15px}.btn-danger:active,.btn-danger.active{background-color:#c12e2a;border-color:#b92c28}.btn-danger:disabled,.btn-danger[disabled]{background-color:#c12e2a;background-image:none}.thumbnail,.img-thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{background-color:#2e6da4;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-o-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#f8f8f8));background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-o-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dbdbdb),to(#e2e2e2));background-image:linear-gradient(to bottom,#dbdbdb 0,#e2e2e2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-o-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#3c3c3c),to(#222));background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-o-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#080808),to(#0f0f0f));background-image:linear-gradient(to bottom,#080808 0,#0f0f0f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-static-top,.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}@media (max-width:767px){.navbar .navbar-nav .open .dropdown-menu>.active>a,.navbar .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#c8e5bc));background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);background-repeat:repeat-x;border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#b9def0));background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);background-repeat:repeat-x;border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#f8efc0));background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);background-repeat:repeat-x;border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-o-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#e7c3c3));background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);background-repeat:repeat-x;border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f5f5f5));background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x}.progress-bar{background-image:-webkit-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-o-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#286090));background-image:linear-gradient(to bottom,#337ab7 0,#286090 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);background-repeat:repeat-x}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#449d44));background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);background-repeat:repeat-x}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#31b0d5));background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);background-repeat:repeat-x}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#ec971f));background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);background-repeat:repeat-x}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c9302c));background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);background-repeat:repeat-x}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{text-shadow:0 -1px 0 #286090;background-image:-webkit-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2b669a));background-image:linear-gradient(to bottom,#337ab7 0,#2b669a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);background-repeat:repeat-x;border-color:#2b669a}.list-group-item.active .badge,.list-group-item.active:hover .badge,.list-group-item.active:focus .badge{text-shadow:none}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#d0e9c6));background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);background-repeat:repeat-x}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#c4e3f3));background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);background-repeat:repeat-x}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#faf2cc));background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);background-repeat:repeat-x}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-o-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#ebcccc));background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);background-repeat:repeat-x}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#e8e8e8),to(#f5f5f5));background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x;border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)} -------------------------------------------------------------------------------- /static/css/fm.selectator.jquery.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Selectator jQuery Plugin 3 | * A plugin for select elements 4 | * version 1.1, Dec 11th, 2015 5 | * by Ingi P. Jacobsen 6 | */ 7 | 8 | /* reset */ 9 | .selectator_element * { 10 | box-sizing: border-box; 11 | -moz-box-sizing: border-box; 12 | text-decoration: none; 13 | } 14 | .selectator_element img { 15 | display: block; 16 | } 17 | .selectator_element.focused { 18 | box-shadow: 0 0 1px #39f; 19 | } 20 | 21 | /* dimmer */ 22 | #selectator_dimmer { 23 | background-color: rgba(0,0,0,.1); 24 | width: 100%; 25 | height: 100%; 26 | position: fixed; 27 | z-index: 100; 28 | } 29 | 30 | /* Main box */ 31 | .selectator_element { 32 | border: 1px solid #d0d1d4; 33 | box-sizing: border-box; 34 | background-color: #fff; 35 | display: inline-block; 36 | text-decoration: none; 37 | vertical-align: middle; 38 | } 39 | .selectator_element.multiple { 40 | padding-right: 20px !important; 41 | padding-bottom: 5px !important; 42 | } 43 | .selectator_element.single { 44 | height: 36px; 45 | padding: 7px 10px !important; 46 | } 47 | .selectator_element:after { 48 | position: absolute; 49 | cursor: pointer; 50 | content: '\25BC'; 51 | font-size: 90%; 52 | right: 4px; 53 | color: #aaa; 54 | top: 50%; 55 | line-height: 0; 56 | } 57 | 58 | /* chosen items holder */ 59 | .selectator_chosen_items { 60 | display: inline; 61 | } 62 | .single .selectator_chosen_items { 63 | display: block; 64 | } 65 | 66 | /* chosen item */ 67 | .selectator_chosen_item { 68 | display: inline-block; 69 | background-color: #39f; 70 | border-radius: 2px; 71 | color: #fff; 72 | padding: 4px 20px 4px 4px; 73 | font-size: 13px; 74 | margin: 2px; 75 | position: relative; 76 | vertical-align: top; 77 | } 78 | .single .selectator_chosen_item { 79 | background-color: transparent; 80 | color: #000; 81 | display: block; 82 | text-decoration: none; 83 | padding: 0; 84 | margin: 0; 85 | font-size: inherit; 86 | } 87 | .multiple .selectator_chosen_item { 88 | margin: 5px 0 0 5px; 89 | padding: 3px 20px 2px 5px; 90 | } 91 | /* chosen item - left */ 92 | .selectator_chosen_item_left { 93 | float: left; 94 | width: 25px; 95 | } 96 | .selectator_chosen_item_left img { 97 | height: 23px; 98 | } 99 | .multiple .selectator_chosen_item_left { 100 | float: left; 101 | width: 22px; 102 | } 103 | .multiple .selectator_chosen_item_left img { 104 | height: 18px; 105 | } 106 | /* chosen item - title */ 107 | .selectator_chosen_item_title { 108 | 109 | } 110 | .single .selectator_chosen_item_title { 111 | height: auto; 112 | } 113 | .multiple .selectator_chosen_item_title { 114 | float: left; 115 | padding-top: 2px; 116 | } 117 | /* chosen item - subtitle */ 118 | .selectator_chosen_item_subtitle { 119 | display: none; 120 | } 121 | /* chosen item - right */ 122 | .selectator_chosen_item_right { 123 | float: right; 124 | width: 20px; 125 | background-color: #ccc; 126 | font-size: 15px; 127 | color: #fff; 128 | text-align: center; 129 | border-radius: 3px; 130 | padding: 3px; 131 | margin-right: 20px; 132 | } 133 | .multiple .selectator_chosen_item_right { 134 | display: none; 135 | } 136 | 137 | /* chosen item remove button */ 138 | .selectator_chosen_item_remove { 139 | display: inline-block; 140 | font-weight: bold; 141 | color: #fff; 142 | margin: 0 0 0 5px; 143 | padding: 6px 5px 4px 5px; 144 | cursor: pointer; 145 | font-size: 11px; 146 | line-height: 10px; 147 | vertical-align: top; 148 | border-radius: 0 2px 2px 0; 149 | position: absolute; 150 | right: 0; 151 | top: 0; 152 | bottom: 0; 153 | } 154 | .selectator_chosen_item_remove:hover { 155 | color: #000; 156 | background-color: #8cf; 157 | } 158 | .single .selectator_chosen_item_remove { 159 | display: none; 160 | } 161 | .multiple .selectator_chosen_item_remove { 162 | padding: 7px 5px 4px 5px; 163 | } 164 | .multiple .selectator_chosen_item_remove:hover { 165 | 166 | } 167 | .multiple .selectator_input, 168 | .multiple .selectator_textlength { 169 | padding: 1px 0 0 0; 170 | margin: 7px 0 2px 5px; 171 | } 172 | 173 | 174 | 175 | 176 | /* input box */ 177 | .selectator_input, 178 | .selectator_textlength { 179 | border: 0; 180 | display: inline-block; 181 | margin: 0; 182 | background-color: transparent; 183 | font-size: 13px; 184 | outline: none; 185 | padding: 6px 0 0 0; 186 | } 187 | .single .selectator_input { 188 | border: 1px solid #7f9db9; 189 | position: absolute; 190 | bottom: -40px; 191 | left: -1px; 192 | z-index: 101; 193 | padding: 10px 25px; 194 | width: 100%; 195 | width: calc(100% + 2px); 196 | border-bottom: 0; 197 | background-color: #f6f6f6; 198 | color: #333; 199 | font-size: inherit; 200 | } 201 | .single.options-hidden .selectator_input { 202 | opacity: 0; 203 | position: absolute; 204 | left: -10000px; 205 | } 206 | .single.options-visible .selectator_input { 207 | opacity: 1; 208 | } 209 | .disable_search .selectator_input { 210 | opacity: 0; 211 | padding: 0 1px 1px 0 !important; 212 | } 213 | 214 | /* options holder */ 215 | .selectator_options { 216 | margin: 0; 217 | padding: 0; 218 | border: 1px solid #7f9db9; 219 | border-radius: 0 0 3px 3px; 220 | font-family: sans-serif; 221 | position: absolute; 222 | box-sizing: border-box; 223 | -moz-box-sizing: border-box; 224 | z-index: 101; 225 | background-color: #fff; 226 | overflow-y: scroll; 227 | max-height: 250px; 228 | list-style: none; 229 | left: -1px; 230 | right: -1px; 231 | } 232 | .single .selectator_options { 233 | padding-top: 0; 234 | border-top: 0; 235 | } 236 | .disable_search .selectator_options { 237 | border-top: 1px solid #7f9db9; 238 | } 239 | .single.disable_search .selectator_options { 240 | padding-top: 0; 241 | } 242 | .selectator_element.options-hidden .selectator_options { 243 | display: none; 244 | } 245 | 246 | /* option item group header */ 247 | .selectator_group { 248 | padding: 5px; 249 | font-weight: bold; 250 | } 251 | 252 | /* option item */ 253 | .selectator_option { 254 | padding: 5px; 255 | cursor: pointer; 256 | color: #000; 257 | } 258 | .selectator_option:before, 259 | .selectator_option:after { 260 | content: ""; 261 | display: table; 262 | } 263 | .selectator_option:after { 264 | clear: both; 265 | } 266 | .selectator_option.active { 267 | background-color: #39f; 268 | color: #fff; 269 | } 270 | .selectator_group_option { 271 | padding: 5px 5px 5px 20px; 272 | } 273 | /* option item - left */ 274 | .selectator_option_left { 275 | float: left; 276 | } 277 | .selectator_option_left img { 278 | height: 30px; 279 | } 280 | /* option item - title */ 281 | .selectator_option_title { 282 | margin-left: 35px; 283 | } 284 | /* option item - subtitle */ 285 | .selectator_option_subtitle { 286 | font-size: 75%; 287 | color: #888; 288 | margin-left: 35px; 289 | } 290 | /* option item - right */ 291 | .selectator_option_right { 292 | float: right; 293 | width: 30px; 294 | background-color: #ccc; 295 | font-size: 15px; 296 | color: #fff; 297 | text-align: center; 298 | border-radius: 3px; 299 | padding: 6px; 300 | } 301 | -------------------------------------------------------------------------------- /static/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.1 (http://getbootstrap.com) 3 | * Copyright 2011-2014 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.1",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a(f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.1",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?a=!1:b.find(".active").removeClass("active")),a&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active"));a&&this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),c.preventDefault()}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.1",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c="prev"==a?-1:1,d=this.getItemIndex(b),e=(d+c)%this.$items.length;return this.$items.eq(e)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));return a>this.$items.length-1||0>a?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){return this.sliding?void 0:this.slide("next")},c.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i="next"==b?"first":"last",j=this;if(!f.length){if(!this.options.wrap)return;f=this.$element.find(".item")[i]()}if(f.hasClass("active"))return this.sliding=!1;var k=f[0],l=a.Event("slide.bs.carousel",{relatedTarget:k,direction:h});if(this.$element.trigger(l),!l.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var m=a(this.$indicators.children()[this.getItemIndex(f)]);m&&m.addClass("active")}var n=a.Event("slid.bs.carousel",{relatedTarget:k,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),j.sliding=!1,setTimeout(function(){j.$element.trigger(n)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(n)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&"show"==b&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a(this.options.trigger).filter('[href="#'+b.id+'"], [data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.1",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0,trigger:'[data-toggle="collapse"]'},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.find("> .panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":a.extend({},e.data(),{trigger:this});c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){b&&3===b.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=c(d),f={relatedTarget:this};e.hasClass("open")&&(e.trigger(b=a.Event("hide.bs.dropdown",f)),b.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger("hidden.bs.dropdown",f)))}))}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.1",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('