diff --git a/.gitignore b/.gitignore index b165945f4..c90a78c3f 100755 --- a/.gitignore +++ b/.gitignore @@ -12,11 +12,13 @@ ### # dsf-bpe ignores ### +dsf-bpe/dsf-bpe-server-jetty/conf/config.properties dsf-bpe/dsf-bpe-server-jetty/docker/dsf_bpe.jar dsf-bpe/dsf-bpe-server-jetty/docker/dsf_status_client.jar dsf-bpe/dsf-bpe-server-jetty/docker/lib/*.jar dsf-bpe/dsf-bpe-server-jetty/docker/lib_external/*.jar dsf-bpe/dsf-bpe-server-jetty/process/*.jar +dsf-bpe/dsf-bpe-server-jetty/ui ### # dsf-fhir ignores @@ -26,6 +28,7 @@ dsf-fhir/dsf-fhir-server-jetty/conf/config.properties dsf-fhir/dsf-fhir-server-jetty/docker/dsf_fhir.jar dsf-fhir/dsf-fhir-server-jetty/docker/dsf_status_client.jar dsf-fhir/dsf-fhir-server-jetty/docker/lib/*.jar +dsf-fhir/dsf-fhir-server-jetty/ui dsf-fhir/dsf-fhir-validation/src/main/resources/fhir/bundle.xml ### @@ -36,6 +39,7 @@ dsf-docker-test-setup/bpe/log/*.log.gz dsf-docker-test-setup/bpe/lib_external/*.jar dsf-docker-test-setup/bpe/process/*.jar dsf-docker-test-setup/bpe/secrets/*.pem +dsf-docker-test-setup/bpe/.env dsf-docker-test-setup/fhir/log/*.log dsf-docker-test-setup/fhir/log/*.log.gz diff --git a/dsf-bpe/dsf-bpe-server-jetty/docker/.dockerignore b/dsf-bpe/dsf-bpe-server-jetty/docker/.dockerignore index 6eee585e4..41fcacd84 100755 --- a/dsf-bpe/dsf-bpe-server-jetty/docker/.dockerignore +++ b/dsf-bpe/dsf-bpe-server-jetty/docker/.dockerignore @@ -3,4 +3,5 @@ Dockerfile lib/README.md lib_external/README.md log/README.md -process/README.md \ No newline at end of file +process/README.md +ui/README.md \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-server-jetty/docker/Dockerfile b/dsf-bpe/dsf-bpe-server-jetty/docker/Dockerfile index 8bd9945ef..fa80c6f33 100755 --- a/dsf-bpe/dsf-bpe-server-jetty/docker/Dockerfile +++ b/dsf-bpe/dsf-bpe-server-jetty/docker/Dockerfile @@ -3,7 +3,7 @@ RUN adduser --system --no-create-home --group --uid 2202 java WORKDIR /opt/bpe COPY --chown=root:java ./ ./ RUN chown root:java ./ && \ - chmod 750 ./ ./conf ./lib ./lib_external ./process ./dsf_bpe_start.sh ./healthcheck.sh && \ + chmod 750 ./ ./conf ./lib ./lib_external ./process ./ui ./dsf_bpe_start.sh ./healthcheck.sh && \ chmod 440 ./conf/log4j2.xml ./dsf_bpe.jar ./lib/*.jar && \ chmod 1775 ./log diff --git a/dsf-bpe/dsf-bpe-server-jetty/docker/ui/README.md b/dsf-bpe/dsf-bpe-server-jetty/docker/ui/README.md new file mode 100644 index 000000000..2cd7bd08b --- /dev/null +++ b/dsf-bpe/dsf-bpe-server-jetty/docker/ui/README.md @@ -0,0 +1 @@ +empty ui directory for static override resources \ No newline at end of file diff --git a/dsf-bpe/dsf-bpe-server-jetty/src/main/java/dev/dsf/bpe/config/BpeHttpJettyConfig.java b/dsf-bpe/dsf-bpe-server-jetty/src/main/java/dev/dsf/bpe/config/BpeHttpJettyConfig.java index 7247be51f..c09ed823c 100644 --- a/dsf-bpe/dsf-bpe-server-jetty/src/main/java/dev/dsf/bpe/config/BpeHttpJettyConfig.java +++ b/dsf-bpe/dsf-bpe-server-jetty/src/main/java/dev/dsf/bpe/config/BpeHttpJettyConfig.java @@ -1,6 +1,5 @@ package dev.dsf.bpe.config; -import java.util.Arrays; import java.util.List; import org.glassfish.jersey.servlet.init.JerseyServletContainerInitializer; @@ -20,6 +19,6 @@ protected String mavenServerModuleName() @Override protected List> servletContainerInitializers() { - return Arrays.asList(JerseyServletContainerInitializer.class, SpringServletContainerInitializer.class); + return List.of(JerseyServletContainerInitializer.class, SpringServletContainerInitializer.class); } } diff --git a/dsf-bpe/dsf-bpe-server-jetty/src/main/java/dev/dsf/bpe/config/BpeHttpsJettyConfig.java b/dsf-bpe/dsf-bpe-server-jetty/src/main/java/dev/dsf/bpe/config/BpeHttpsJettyConfig.java index 523ef5612..3fadb3b9c 100644 --- a/dsf-bpe/dsf-bpe-server-jetty/src/main/java/dev/dsf/bpe/config/BpeHttpsJettyConfig.java +++ b/dsf-bpe/dsf-bpe-server-jetty/src/main/java/dev/dsf/bpe/config/BpeHttpsJettyConfig.java @@ -1,6 +1,5 @@ package dev.dsf.bpe.config; -import java.util.Arrays; import java.util.List; import org.glassfish.jersey.servlet.init.JerseyServletContainerInitializer; @@ -20,6 +19,6 @@ protected String mavenServerModuleName() @Override protected List> servletContainerInitializers() { - return Arrays.asList(JerseyServletContainerInitializer.class, SpringServletContainerInitializer.class); + return List.of(JerseyServletContainerInitializer.class, SpringServletContainerInitializer.class); } } diff --git a/dsf-bpe/dsf-bpe-server/pom.xml b/dsf-bpe/dsf-bpe-server/pom.xml index 66062dee1..8e6340800 100755 --- a/dsf-bpe/dsf-bpe-server/pom.xml +++ b/dsf-bpe/dsf-bpe-server/pom.xml @@ -46,6 +46,10 @@ dev.dsf dsf-common-status + + dev.dsf + dsf-common-ui + de.hs-heilbronn.mi crypto-utils diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/BpeJerseyApplication.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/BpeJerseyApplication.java index 97d3e2f55..ab62d92f6 100755 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/BpeJerseyApplication.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/BpeJerseyApplication.java @@ -1,11 +1,13 @@ package dev.dsf.bpe; import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.WebApplicationContextUtils; +import dev.dsf.common.auth.filter.AuthenticationFilter; import jakarta.inject.Inject; import jakarta.servlet.ServletContext; import jakarta.ws.rs.ApplicationPath; @@ -36,5 +38,8 @@ public BpeJerseyApplication(ServletContext servletContext) register(b); }); + + register(AuthenticationFilter.class); + register(RolesAllowedDynamicFeature.class); } } diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/authentication/BpeServerRole.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/authentication/BpeServerRole.java index 34e84fe3c..a2a108d28 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/authentication/BpeServerRole.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/authentication/BpeServerRole.java @@ -1,8 +1,15 @@ package dev.dsf.bpe.authentication; +import java.util.stream.Stream; + import dev.dsf.common.auth.conf.DsfRole; public enum BpeServerRole implements DsfRole { - ORGANIZATION + ADMIN; + + public static boolean isValid(String role) + { + return role != null && !role.isBlank() && Stream.of(values()).map(Enum::name).anyMatch(n -> n.equals(role)); + } } diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/authentication/IdentityProviderImpl.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/authentication/IdentityProviderImpl.java index c484b8ac9..e467155a5 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/authentication/IdentityProviderImpl.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/authentication/IdentityProviderImpl.java @@ -1,150 +1,73 @@ package dev.dsf.bpe.authentication; import java.security.cert.X509Certificate; -import java.util.Collections; +import java.util.Objects; import java.util.Optional; -import java.util.Set; -import javax.security.auth.x500.X500Principal; - -import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Organization; import org.hl7.fhir.r4.model.Practitioner; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; -import dev.dsf.common.auth.DsfOpenIdCredentials; -import dev.dsf.common.auth.conf.DsfRole; +import dev.dsf.bpe.service.LocalOrganizationProvider; +import dev.dsf.common.auth.conf.AbstractIdentityProvider; import dev.dsf.common.auth.conf.Identity; import dev.dsf.common.auth.conf.IdentityProvider; -import dev.dsf.common.auth.conf.OrganizationIdentity; -import dev.dsf.common.auth.conf.PractitionerIdentity; +import dev.dsf.common.auth.conf.PractitionerIdentityImpl; +import dev.dsf.common.auth.conf.RoleConfig; -public class IdentityProviderImpl implements IdentityProvider +public class IdentityProviderImpl extends AbstractIdentityProvider implements IdentityProvider, InitializingBean { - @Override - public Identity getIdentity(DsfOpenIdCredentials credentials) - { - return new PractitionerIdentity() - { - @Override - public String getName() - { - return credentials.getUserId(); - } - - @Override - public String getDisplayName() - { - return getName(); - } - - @Override - public boolean isLocalIdentity() - { - return true; - } + private static final Logger logger = LoggerFactory.getLogger(IdentityProviderImpl.class); - @Override - public boolean hasDsfRole(DsfRole role) - { - return BpeServerRole.ORGANIZATION.equals(role); - } + private final LocalOrganizationProvider organizationProvider; - @Override - public Set getDsfRoles() - { - return Collections.singleton(BpeServerRole.ORGANIZATION); - } - - @Override - public Optional getOrganizationIdentifierValue() - { - return Optional.empty(); - } - - @Override - public Organization getOrganization() - { - return null; - } + public IdentityProviderImpl(RoleConfig roleConfig, LocalOrganizationProvider organizationProvider) + { + super(roleConfig); - @Override - public Practitioner getPractitioner() - { - return null; - } + this.organizationProvider = organizationProvider; + } - @Override - public Set getPractionerRoles() - { - return Collections.emptySet(); - } + @Override + public void afterPropertiesSet() throws Exception + { + super.afterPropertiesSet(); - @Override - public Optional getCredentials() - { - return Optional.of(credentials); - } + Objects.requireNonNull(organizationProvider, "organizationProvider"); + } - @Override - public Optional getCertificate() - { - return Optional.empty(); - } - }; + @Override + protected Optional getLocalOrganization() + { + return organizationProvider.getLocalOrganization(); } @Override public Identity getIdentity(X509Certificate[] certificates) { - return new OrganizationIdentity() - { - @Override - public String getName() - { - return certificates[0].getSubjectX500Principal().getName(X500Principal.RFC1779); - } - - @Override - public String getDisplayName() - { - return getName(); - } + if (certificates == null || certificates.length == 0) + return null; - @Override - public Set getDsfRoles() - { - return Collections.singleton(BpeServerRole.ORGANIZATION); - } + String thumbprint = getThumbprint(certificates[0]); - @Override - public Organization getOrganization() - { - return null; - } - - @Override - public boolean isLocalIdentity() - { - return true; - } - - @Override - public Optional getOrganizationIdentifierValue() - { - return Optional.empty(); - } - - @Override - public boolean hasDsfRole(DsfRole role) - { - return BpeServerRole.ORGANIZATION.equals(role); - } + Optional practitioner = toPractitioner(certificates[0]); + Optional localOrganization = organizationProvider.getLocalOrganization(); + if (practitioner.isPresent() && localOrganization.isPresent()) + { + Practitioner p = practitioner.get(); + Organization o = localOrganization.get(); - @Override - public Optional getCertificate() - { - return Optional.of(certificates[0]); - } - }; + return new PractitionerIdentityImpl(o, getDsfRolesFor(p, thumbprint, null, null), certificates[0], p, + getPractitionerRolesFor(p, thumbprint, null, null), null); + } + else + { + logger.warn( + "Certificate with thumbprint '{}' for '{}' unknown, not configured as local user or local organization unknown", + thumbprint, getDn(certificates[0])); + return null; + } } } diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/AbstractProcessPlugin.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/AbstractProcessPlugin.java index a49d8711f..6538fea8a 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/AbstractProcessPlugin.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/plugin/AbstractProcessPlugin.java @@ -145,12 +145,12 @@ Resource getResource() .compile(Pattern.quote(PLACEHOLDER_PREFIX_TMP)); private static final Pattern PLACEHOLDER_PREFIX_PATTERN = Pattern.compile(Pattern.quote(PLACEHOLDER_PREFIX)); - private static final String ACTIVITY_DEFINITION_URL_PATTERN_STRING = "^(?http[s]{0,1}://(?(?:(?:[a-zA-Z0-9]{1,63}|[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9])\\.)+(?:[a-zA-Z0-9]{1,63}))" + private static final String ACTIVITY_DEFINITION_URL_PATTERN_STRING = "^(?http[s]{0,1}://(?(?:(?:[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9])\\.)+(?:[a-zA-Z0-9]{1,63}))" + "/bpe/Process/(?[a-zA-Z0-9-]+))$"; private static final Pattern ACTIVITY_DEFINITION_URL_PATTERN = Pattern .compile(ACTIVITY_DEFINITION_URL_PATTERN_STRING); - private static final String INSTANTIATES_CANONICAL_PATTERN_STRING = "(?http[s]{0,1}://(?(?:(?:[a-zA-Z0-9]{1,63}|[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9])\\.)+(?:[a-zA-Z0-9]{1,63}))" + private static final String INSTANTIATES_CANONICAL_PATTERN_STRING = "(?http[s]{0,1}://(?(?:(?:[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9])\\.)+(?:[a-zA-Z0-9]{1,63}))" + "/bpe/Process/(?[a-zA-Z0-9-]+))\\|(?\\d+\\.\\d+)$"; private static final Pattern INSTANTIATES_CANONICAL_PATTERN = Pattern .compile(INSTANTIATES_CANONICAL_PATTERN_STRING); diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/service/LocalOrganizationProvider.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/service/LocalOrganizationProvider.java new file mode 100644 index 000000000..45e6a8157 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/service/LocalOrganizationProvider.java @@ -0,0 +1,10 @@ +package dev.dsf.bpe.service; + +import java.util.Optional; + +import org.hl7.fhir.r4.model.Organization; + +public interface LocalOrganizationProvider +{ + Optional getLocalOrganization(); +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/service/LocalOrganizationProviderImpl.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/service/LocalOrganizationProviderImpl.java new file mode 100644 index 000000000..a2a537221 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/service/LocalOrganizationProviderImpl.java @@ -0,0 +1,54 @@ +package dev.dsf.bpe.service; + +import java.time.LocalDateTime; +import java.time.temporal.TemporalAmount; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; + +import org.hl7.fhir.r4.model.Organization; +import org.springframework.beans.factory.InitializingBean; + +import dev.dsf.bpe.v1.service.OrganizationProvider; + +public class LocalOrganizationProviderImpl implements LocalOrganizationProvider, InitializingBean +{ + private record OrganizationEntry(Optional organization, LocalDateTime readTime) + { + } + + private final AtomicReference organization = new AtomicReference<>(); + + private final TemporalAmount cacheTimeout; + private final OrganizationProvider delegate; + + public LocalOrganizationProviderImpl(TemporalAmount cacheTimeout, OrganizationProvider delegate) + { + this.cacheTimeout = cacheTimeout; + this.delegate = delegate; + } + + @Override + public void afterPropertiesSet() throws Exception + { + Objects.requireNonNull(cacheTimeout, "cacheTimeout"); + Objects.requireNonNull(delegate, "delegate"); + } + + @Override + public Optional getLocalOrganization() + { + OrganizationEntry entry = organization.get(); + if (entry == null || entry.organization().isEmpty() + || LocalDateTime.now().isAfter(entry.readTime().plus(cacheTimeout))) + { + Optional o = delegate.getLocalOrganization(); + if (organization.compareAndSet(entry, new OrganizationEntry(o, LocalDateTime.now()))) + return o; + else + return organization.get().organization(); + } + else + return entry.organization(); + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/AuthenticationConfig.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/AuthenticationConfig.java index 9fae00aa9..34630a48d 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/AuthenticationConfig.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/AuthenticationConfig.java @@ -1,17 +1,52 @@ package dev.dsf.bpe.spring.config; +import java.time.Duration; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import dev.dsf.bpe.authentication.BpeServerRole; import dev.dsf.bpe.authentication.IdentityProviderImpl; +import dev.dsf.bpe.service.LocalOrganizationProvider; +import dev.dsf.bpe.service.LocalOrganizationProviderImpl; import dev.dsf.common.auth.conf.IdentityProvider; +import dev.dsf.common.auth.conf.RoleConfig; +import dev.dsf.common.auth.conf.RoleConfigReader; @Configuration public class AuthenticationConfig { + private static final Logger logger = LoggerFactory.getLogger(AuthenticationConfig.class); + + @Autowired + private PropertiesConfig propertiesConfig; + + @Autowired + private PluginConfig pluginConfig; + + @Bean + public LocalOrganizationProvider localOrganizationProvider() + { + return new LocalOrganizationProviderImpl(Duration.ofSeconds(30), + pluginConfig.processPluginApiV1().getOrganizationProvider()); + } + @Bean public IdentityProvider identityProvider() { - return new IdentityProviderImpl(); + return new IdentityProviderImpl(roleConfig(), localOrganizationProvider()); + } + + @Bean + public RoleConfig roleConfig() + { + RoleConfig config = new RoleConfigReader().read(propertiesConfig.getRoleConfig(), + role -> BpeServerRole.isValid(role) ? BpeServerRole.valueOf(role) : null, s -> null); + + logger.info("Role config: {}", config.toString()); + return config; } } diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/CamundaConfig.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/CamundaConfig.java index ac4f9a6ff..e2c95a525 100755 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/CamundaConfig.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/CamundaConfig.java @@ -79,20 +79,20 @@ private String toString(char[] password) @Bean public StartListener startListener() { - return new StartListener(propertiesConfig.getServerBaseUrl(), VariablesImpl::new); + return new StartListener(propertiesConfig.getFhirServerBaseUrl(), VariablesImpl::new); } @Bean public EndListener endListener() { - return new EndListener(propertiesConfig.getServerBaseUrl(), VariablesImpl::new, + return new EndListener(propertiesConfig.getFhirServerBaseUrl(), VariablesImpl::new, fhirClientConfig.clientProvider().getLocalWebserviceClient()); } @Bean public ContinueListener continueListener() { - return new ContinueListener(propertiesConfig.getServerBaseUrl(), VariablesImpl::new); + return new ContinueListener(propertiesConfig.getFhirServerBaseUrl(), VariablesImpl::new); } @Bean diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/FhirClientConfig.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/FhirClientConfig.java index f986cb2b6..7ae3fbdd9 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/FhirClientConfig.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/FhirClientConfig.java @@ -56,8 +56,9 @@ public void afterPropertiesSet() throws Exception propertiesConfig.getClientCertificateTrustStoreFile(), propertiesConfig.getClientCertificateFile(), propertiesConfig.getClientCertificatePrivateKeyFile(), propertiesConfig.getClientCertificatePrivateKeyFilePassword() != null ? "***" : "null", - propertiesConfig.getServerBaseUrl(), - propertiesConfig.proxyConfig().isEnabled(propertiesConfig.getServerBaseUrl()) ? "enabled" : "disabled"); + propertiesConfig.getFhirServerBaseUrl(), + propertiesConfig.proxyConfig().isEnabled(propertiesConfig.getFhirServerBaseUrl()) ? "enabled" + : "disabled"); logger.info( "Local websocket client config: {trustStorePath: {}, certificatePath: {}, privateKeyPath: {}, privateKeyPassword: {}," + " url: {}, proxy: {}}", @@ -102,7 +103,7 @@ public FhirClientProvider clientProvider() KeyStore webserviceTrustStore = createTrustStore(propertiesConfig.getClientCertificateTrustStoreFile()); return new FhirClientProviderImpl(fhirConfig.fhirContext(), referenceCleaner(), - propertiesConfig.getServerBaseUrl(), propertiesConfig.getWebserviceClientLocalReadTimeout(), + propertiesConfig.getFhirServerBaseUrl(), propertiesConfig.getWebserviceClientLocalReadTimeout(), propertiesConfig.getWebserviceClientLocalConnectTimeout(), propertiesConfig.getWebserviceClientLocalVerbose(), webserviceTrustStore, webserviceKeyStore, keyStorePassword, propertiesConfig.getWebserviceClientRemoteReadTimeout(), @@ -119,7 +120,7 @@ public FhirClientProvider clientProvider() private String getWebsocketUrl() { - String baseUrl = propertiesConfig.getServerBaseUrl(); + String baseUrl = propertiesConfig.getFhirServerBaseUrl(); if (baseUrl.startsWith("https://")) return baseUrl.replace("https://", "wss://") + "/ws"; diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/PluginConfig.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/PluginConfig.java index b8c357bd4..30c3dc46f 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/PluginConfig.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/PluginConfig.java @@ -81,18 +81,18 @@ public ProcessPluginApi processPluginApiV1() FhirWebserviceClientProvider clientProvider = new FhirWebserviceClientProviderImpl( fhirClientConfig.clientProvider()); EndpointProvider endpointProvider = new EndpointProviderImpl(clientProvider, - propertiesConfig.getServerBaseUrl()); + propertiesConfig.getFhirServerBaseUrl()); FhirContext fhirContext = fhirConfig.fhirContext(); MailService mailService = new MailServiceImpl(mailConfig.mailService()); ObjectMapper objectMapper = serializerConfig.objectMapper(); OrganizationProvider organizationProvider = new OrganizationProviderImpl(clientProvider, - propertiesConfig.getServerBaseUrl()); + propertiesConfig.getFhirServerBaseUrl()); ProcessAuthorizationHelper processAuthorizationHelper = new ProcessAuthorizationHelperImpl(); QuestionnaireResponseHelper questionnaireResponseHelper = new QuestionnaireResponseHelperImpl( - propertiesConfig.getServerBaseUrl()); + propertiesConfig.getFhirServerBaseUrl()); ReadAccessHelper readAccessHelper = new ReadAccessHelperImpl(); - TaskHelper taskHelper = new TaskHelperImpl(propertiesConfig.getServerBaseUrl()); + TaskHelper taskHelper = new TaskHelperImpl(propertiesConfig.getFhirServerBaseUrl()); return new ProcessPluginApiImpl(proxyConfig, endpointProvider, fhirContext, clientProvider, mailService, objectMapper, organizationProvider, processAuthorizationHelper, questionnaireResponseHelper, @@ -140,7 +140,7 @@ public ProcessPluginManager processPluginManager() return new ProcessPluginManagerImpl( List.of(camundaConfig.delegateProvider(), camundaConfig.fallbackSerializerFactory()), processPluginLoader(), bpmnProcessStateChangeService(), fhirResourceHandler(), - propertiesConfig.getServerBaseUrl(), fhirClientConfig.clientProvider().getLocalWebserviceClient(), + propertiesConfig.getFhirServerBaseUrl(), fhirClientConfig.clientProvider().getLocalWebserviceClient(), propertiesConfig.getFhirServerRequestMaxRetries(), propertiesConfig.getFhirServerRetryDelayMillis()); } } diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/PropertiesConfig.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/PropertiesConfig.java index 5b86f36d4..59d86d013 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/PropertiesConfig.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/PropertiesConfig.java @@ -20,6 +20,7 @@ import dev.dsf.common.config.ProxyConfig; import dev.dsf.common.config.ProxyConfigImpl; import dev.dsf.common.documentation.Documentation; +import dev.dsf.common.ui.theme.Theme; import dev.dsf.tools.docker.secrets.DockerSecretsPropertySourceFactory; @Configuration @@ -28,26 +29,42 @@ public class PropertiesConfig implements InitializingBean { private static final Logger logger = LoggerFactory.getLogger(PropertiesConfig.class); - @Documentation(required = true, description = "The address of the database used for the DSF BPE server", recommendation = "Change only if you don't use the provided docker-compose from the installation guide or made changes to the database settings/networking in the docker-compose", example = "jdbc:postgresql://db/bpe") + @Documentation(required = true, description = "Address of the database used for the DSF BPE server", recommendation = "Change only if you don't use the provided docker-compose from the installation guide or made changes to the database settings/networking in the docker-compose", example = "jdbc:postgresql://db/bpe") @Value("${dev.dsf.bpe.db.url}") private String dbUrl; - @Documentation(description = "The user name to access the database from the DSF BPE server") + @Documentation(description = "Username to access the database from the DSF BPE server") @Value("${dev.dsf.bpe.db.user.username:bpe_server_user}") private String dbUsername; - @Documentation(required = true, description = "The password to access the database from the DSF BPE server", recommendation = "Use docker secret file to configure using *${env_variable}_FILE*", example = "/run/secrets/db_user.password") + @Documentation(required = true, description = "Password to access the database from the DSF BPE server", recommendation = "Use docker secret file to configure using *${env_variable}_FILE*", example = "/run/secrets/db_user.password") @Value("${dev.dsf.bpe.db.user.password}") private char[] dbPassword; - @Documentation(description = "The user name to access the database from the DSF BPE server for camunda processes", recommendation = "Use a different user then in *DEV_DSF_BPE_DB_USER_USERNAME*") + @Documentation(description = "Username to access the database from the DSF BPE server for camunda processes", recommendation = "Use a different user then in *DEV_DSF_BPE_DB_USER_USERNAME*") @Value("${dev.dsf.bpe.db.user.camunda.username:camunda_server_user}") private String dbCamundaUsername; - @Documentation(required = true, description = "The password to access the database from the DSF BPE server for camunda processes", recommendation = "Use docker secret file to configure using *${env_variable}_FILE*", example = "/run/secrets/db_user_camunda.password") + @Documentation(required = true, description = "Password to access the database from the DSF BPE server for camunda processes", recommendation = "Use docker secret file to configure using *${env_variable}_FILE*", example = "/run/secrets/db_user_camunda.password") @Value("${dev.dsf.bpe.db.user.camunda.password}") private char[] dbCamundaPassword; + @Documentation(description = "UI theme parameter, adds a color indicator to the ui to distinguish `dev`, `test` and `prod` environments im configured; supported values: `dev`, `test` and `prod`") + @Value("${dev.dsf.bpe.server.ui.theme:}") + private String uiTheme; + + @Documentation(description = "Base address of the BPE server, configure when exposing the web-ui", example = "https://foo.bar/bpe") + @Value("${dev.dsf.bpe.server.base.url:https://localhost/bpe}") + private String bpeServerBaseUrl; + + @Documentation(description = "Role config YAML as defined in [FHIR Server: Access Control](access-control).") + @Value("${dev.dsf.bpe.server.roleConfig:}") + private String roleConfig; + + @Documentation(description = "To disable static resource caching, set to `false`", recommendation = "Only set to `false` for development") + @Value("${dev.dsf.bpe.server.static.resource.cache:true}") + private boolean staticResourceCacheEnabled; + @Documentation(required = true, description = "PEM encoded file with one or more trusted root certificates to validate server certificates for https connections to local and remote DSF FHIR servers", recommendation = "Use docker secret file to configure", example = "/run/secrets/app_client_trust_certificates.pem") @Value("${dev.dsf.bpe.fhir.client.trust.server.certificate.cas}") private String clientCertificateTrustStoreFile; @@ -64,11 +81,11 @@ public class PropertiesConfig implements InitializingBean @Value("${dev.dsf.bpe.fhir.client.certificate.private.key.password:#{null}}") private char[] clientCertificatePrivateKeyFilePassword; - @Documentation(description = "The timeout in milliseconds until a reading a resource from a remote DSF FHIR server is aborted", recommendation = "Change default value only if timeout exceptions occur") + @Documentation(description = "Timeout in milliseconds until a reading a resource from a remote DSF FHIR server is aborted", recommendation = "Change default value only if timeout exceptions occur") @Value("${dev.dsf.bpe.fhir.client.remote.timeout.read:60000}") private int webserviceClientRemoteReadTimeout; - @Documentation(description = "The timeout in milliseconds until a connection is established with a remote DSF FHIR server", recommendation = "Change default value only if timeout exceptions occur") + @Documentation(description = "Timeout in milliseconds until a connection is established with a remote DSF FHIR server", recommendation = "Change default value only if timeout exceptions occur") @Value("${dev.dsf.bpe.fhir.client.remote.timeout.connect:5000}") private int webserviceClientRemoteConnectTimeout; @@ -76,15 +93,15 @@ public class PropertiesConfig implements InitializingBean @Value("${dev.dsf.bpe.fhir.client.remote.verbose:false}") private boolean webserviceClientRemoteVerbose; - @Documentation(required = true, description = "The base address of the local DSF FHIR server to read/store fhir resources", example = "https://foo.bar/fhir") + @Documentation(required = true, description = "Base address of the local DSF FHIR server to read/store fhir resources", example = "https://foo.bar/fhir") @Value("${dev.dsf.bpe.fhir.server.base.url}") - private String serverBaseUrl; + private String fhirServerBaseUrl; - @Documentation(description = "The timeout in milliseconds until reading a resource from the local DSF FHIR server is aborted", recommendation = "Change default value only if timeout exceptions occur") + @Documentation(description = "Timeout in milliseconds until reading a resource from the local DSF FHIR server is aborted", recommendation = "Change default value only if timeout exceptions occur") @Value("${dev.dsf.bpe.fhir.client.local.timeout.read:60000}") private int webserviceClientLocalReadTimeout; - @Documentation(description = "The timeout in milliseconds until a connection is established with the local DSF FHIR server", recommendation = "Change default value only if timeout exceptions occur") + @Documentation(description = "Timeout in milliseconds until a connection is established with the local DSF FHIR server", recommendation = "Change default value only if timeout exceptions occur") @Value("${dev.dsf.bpe.fhir.client.local.timeout.connect:2000}") private int webserviceClientLocalConnectTimeout; @@ -267,26 +284,27 @@ public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderCon @Override public void afterPropertiesSet() throws Exception { - URL url = new URL(serverBaseUrl); + URL url = new URL(fhirServerBaseUrl); if (!Arrays.asList("http", "https").contains(url.getProtocol())) { logger.warn("Invalid DSF FHIR server base URL: '{}', URL not starting with 'http://' or 'https://'", - serverBaseUrl); + fhirServerBaseUrl); throw new IllegalArgumentException("Invalid ServerBaseUrl, not starting with 'http://' or 'https://'"); } - else if (serverBaseUrl.endsWith("//")) + else if (fhirServerBaseUrl.endsWith("//")) { - logger.warn("Invalid DSF FHIR server base URL: '{}', URL may not end in '//'", serverBaseUrl); + logger.warn("Invalid DSF FHIR server base URL: '{}', URL may not end in '//'", fhirServerBaseUrl); throw new IllegalArgumentException("Invalid ServerBaseUrl, ending in //"); } - else if (!serverBaseUrl.startsWith("https://")) + else if (!fhirServerBaseUrl.startsWith("https://")) { - logger.warn("Invalid DSF FHIR server base URL: '{}', URL must start with 'https://'", serverBaseUrl); + logger.warn("Invalid DSF FHIR server base URL: '{}', URL must start with 'https://'", fhirServerBaseUrl); throw new IllegalArgumentException("Invalid ServerBaseUrl, not starting with https://"); } - if (serverBaseUrl.endsWith("/")) - logger.warn("DSF FHIR server base URL: '{}', should not end in '/', removing trailing '/'", serverBaseUrl); + if (fhirServerBaseUrl.endsWith("/")) + logger.warn("DSF FHIR server base URL: '{}', should not end in '/', removing trailing '/'", + fhirServerBaseUrl); logger.info( "Concurrency config: {process-threads: {}, engine-core-pool: {}, engine-queue: {}, engine-max-pool: {}}", @@ -319,6 +337,28 @@ public char[] getDbCamundaPassword() return dbCamundaPassword; } + public Theme getUiTheme() + { + return Theme.fromString(uiTheme); + } + + public String getServerBaseUrl() + { + return bpeServerBaseUrl != null && bpeServerBaseUrl.endsWith("/") + ? bpeServerBaseUrl.substring(0, bpeServerBaseUrl.length() - 1) + : bpeServerBaseUrl; + } + + public String getRoleConfig() + { + return roleConfig; + } + + public boolean getStaticResourceCacheEnabled() + { + return staticResourceCacheEnabled; + } + public String getClientCertificateTrustStoreFile() { return clientCertificateTrustStoreFile; @@ -354,11 +394,11 @@ public boolean getWebserviceClientRemoteVerbose() return webserviceClientRemoteVerbose; } - public String getServerBaseUrl() + public String getFhirServerBaseUrl() { - return serverBaseUrl != null && serverBaseUrl.endsWith("/") - ? serverBaseUrl.substring(0, serverBaseUrl.length() - 1) - : serverBaseUrl; + return fhirServerBaseUrl != null && fhirServerBaseUrl.endsWith("/") + ? fhirServerBaseUrl.substring(0, fhirServerBaseUrl.length() - 1) + : fhirServerBaseUrl; } public int getWebserviceClientLocalReadTimeout() diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/WebserviceConfig.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/WebserviceConfig.java index 2558c51ed..81af32731 100755 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/WebserviceConfig.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/spring/config/WebserviceConfig.java @@ -1,14 +1,20 @@ package dev.dsf.bpe.spring.config; +import java.nio.file.Files; +import java.nio.file.Paths; + import org.camunda.bpm.engine.ProcessEngine; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import dev.dsf.bpe.ui.ThymeleafTemplateService; +import dev.dsf.bpe.ui.ThymeleafTemplateServiceImpl; import dev.dsf.bpe.webservice.ProcessService; import dev.dsf.bpe.webservice.RootService; import dev.dsf.common.auth.logout.LogoutService; import dev.dsf.common.status.webservice.StatusService; +import dev.dsf.common.ui.webservice.StaticResourcesService; @Configuration public class WebserviceConfig @@ -22,6 +28,18 @@ public class WebserviceConfig @Autowired private PropertiesConfig propertiesConfig; + @Bean + public ThymeleafTemplateService thymeleafTemplateService() + { + return new ThymeleafTemplateServiceImpl(propertiesConfig.getServerBaseUrl(), propertiesConfig.getUiTheme(), + propertiesConfig.getStaticResourceCacheEnabled(), modCssExists()); + } + + private boolean modCssExists() + { + return Files.isReadable(Paths.get("ui/mod.css")); + } + @Bean public LogoutService logoutService() { @@ -31,13 +49,20 @@ public LogoutService logoutService() @Bean public ProcessService processService() { - return new ProcessService(processEngine.getRuntimeService(), processEngine.getRepositoryService()); + return new ProcessService(thymeleafTemplateService(), processEngine.getRepositoryService()); } @Bean public RootService rootService() { - return new RootService(); + return new RootService(thymeleafTemplateService(), processEngine.getRepositoryService(), + processEngine.getRuntimeService()); + } + + @Bean + public StaticResourcesService staticResourcesService() + { + return new StaticResourcesService(propertiesConfig.getStaticResourceCacheEnabled()); } @Bean diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/TaskHandler.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/TaskHandler.java index e7d4e71b8..78b6948e0 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/TaskHandler.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/subscription/TaskHandler.java @@ -90,7 +90,8 @@ String getShortMessage() public static final String TASK_VARIABLE = TaskHandler.class.getName() + ".task"; - private static final String INSTANTIATES_CANONICAL_PATTERN_STRING = "(?http://(?(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*(?:[A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9]))/bpe/Process/(?[-\\w]+))\\|(?\\d+\\.\\d+)"; + private static final String INSTANTIATES_CANONICAL_PATTERN_STRING = "(?http[s]{0,1}://(?(?:(?:[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9])\\.)+(?:[a-zA-Z0-9]{1,63}))" + + "/bpe/Process/(?[a-zA-Z0-9-]+))\\|(?\\d+\\.\\d+)$"; private static final Pattern INSTANTIATES_CANONICAL_PATTERN = Pattern .compile(INSTANTIATES_CANONICAL_PATTERN_STRING); @@ -125,8 +126,8 @@ public void onResource(Task task) throw new IllegalStateException("InstantiatesCanonical of Task with id " + task.getIdElement().getIdPart() + " does not match " + INSTANTIATES_CANONICAL_PATTERN_STRING); - String processDomain = matcher.group("processDomain").replace(".", ""); - String processDefinitionKey = matcher.group("processDefinitionKey"); + String processDomain = matcher.group("domain").replace(".", ""); + String processDefinitionKey = matcher.group("processName"); String processVersion = matcher.group("processVersion"); String messageName = getFirstInputParameter(task, BpmnMessage.messageName()); diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/ui/ThymeleafTemplateService.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/ui/ThymeleafTemplateService.java new file mode 100644 index 000000000..bf464afdf --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/ui/ThymeleafTemplateService.java @@ -0,0 +1,21 @@ +package dev.dsf.bpe.ui; + +import org.thymeleaf.context.Context; + +import jakarta.ws.rs.core.StreamingOutput; + +public interface ThymeleafTemplateService +{ + record MainValues(String title, String heading, String htmlFragment, String username, boolean openid) + { + } + + /** + * @param context + * not null + * @param mainValues + * not null + * @return + */ + StreamingOutput write(Context context, MainValues mainValues); +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/ui/ThymeleafTemplateServiceImpl.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/ui/ThymeleafTemplateServiceImpl.java new file mode 100644 index 000000000..5744631c1 --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/ui/ThymeleafTemplateServiceImpl.java @@ -0,0 +1,89 @@ +package dev.dsf.bpe.ui; + +import java.io.OutputStreamWriter; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Objects; + +import org.springframework.beans.factory.InitializingBean; +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.context.Context; +import org.thymeleaf.templatemode.TemplateMode; +import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver; + +import dev.dsf.common.ui.theme.Theme; +import jakarta.ws.rs.core.StreamingOutput; + +public class ThymeleafTemplateServiceImpl implements ThymeleafTemplateService, InitializingBean +{ + private final String serverBaseUrl; + private final Theme theme; + private final boolean cacheEnabled; + private final boolean modCssExists; + + private final TemplateEngine templateEngine = new TemplateEngine(); + + /** + * @param serverBaseUrl + * not null + * @param theme + * may be null + * @param cacheEnabled + * @param modCssExists + */ + public ThymeleafTemplateServiceImpl(String serverBaseUrl, Theme theme, boolean cacheEnabled, boolean modCssExists) + { + this.serverBaseUrl = serverBaseUrl; + this.theme = theme; + this.cacheEnabled = cacheEnabled; + this.modCssExists = modCssExists; + + ClassLoaderTemplateResolver resolver = new ClassLoaderTemplateResolver(); + resolver.setTemplateMode(TemplateMode.HTML); + resolver.setPrefix("/template/"); + resolver.setSuffix(".html"); + resolver.setCacheable(cacheEnabled); + + templateEngine.setTemplateResolver(resolver); + } + + @Override + public void afterPropertiesSet() throws Exception + { + Objects.requireNonNull(serverBaseUrl, "serverBaseUrl"); + } + + private String getServerBaseUrlPathWithLeadingSlash() + { + try + { + return new URL(serverBaseUrl).getPath(); + } + catch (MalformedURLException e) + { + throw new RuntimeException(e); + } + } + + @Override + public StreamingOutput write(Context context, MainValues mainValues) + { + Objects.requireNonNull(context, "context"); + Objects.requireNonNull(mainValues, "mainValues"); + + context.setVariable("title", mainValues.title()); + context.setVariable("heading", mainValues.heading()); + context.setVariable("htmlFragment", mainValues.htmlFragment()); + + context.setVariable("username", mainValues.username()); + context.setVariable("openid", mainValues.openid()); + + context.setVariable("basePath", getServerBaseUrlPathWithLeadingSlash()); + context.setVariable("modCssExists", modCssExists); + context.setVariable("theme", theme == null ? null : theme.toString()); + + context.setVariable("bpmnProd", cacheEnabled); + + return output -> templateEngine.process("main", context, new OutputStreamWriter(output)); + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/webservice/AbstractService.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/webservice/AbstractService.java new file mode 100644 index 000000000..8edfc8c4a --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/webservice/AbstractService.java @@ -0,0 +1,50 @@ +package dev.dsf.bpe.webservice; + +import java.security.Principal; +import java.util.Objects; +import java.util.function.Consumer; + +import org.springframework.beans.factory.InitializingBean; +import org.thymeleaf.context.Context; + +import dev.dsf.bpe.ui.ThymeleafTemplateService; +import dev.dsf.bpe.ui.ThymeleafTemplateService.MainValues; +import dev.dsf.common.auth.conf.Identity; +import jakarta.ws.rs.core.SecurityContext; +import jakarta.ws.rs.core.StreamingOutput; + +public abstract class AbstractService implements InitializingBean +{ + @jakarta.ws.rs.core.Context + private volatile SecurityContext securityContext; + + private final ThymeleafTemplateService templateService; + private final String htmlFragment; + + public AbstractService(ThymeleafTemplateService templateService, String htmlFragment) + { + this.templateService = templateService; + this.htmlFragment = htmlFragment; + } + + @Override + public void afterPropertiesSet() throws Exception + { + Objects.requireNonNull(templateService, "templateService"); + Objects.requireNonNull(htmlFragment, "htmlFragment"); + } + + protected StreamingOutput write(String title, String heading, Consumer setValues) + { + Context context = new Context(); + + setValues.accept(context); + + Principal principal = securityContext.getUserPrincipal(); + MainValues mainValues = new MainValues(title, heading, htmlFragment, + principal instanceof Identity i ? i.getDisplayName() : null, + "OPENID".equals(securityContext.getAuthenticationScheme())); + + return templateService.write(context, mainValues); + } +} diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/webservice/ProcessService.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/webservice/ProcessService.java index 5c2e436a4..72291f8fc 100755 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/webservice/ProcessService.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/webservice/ProcessService.java @@ -1,102 +1,155 @@ package dev.dsf.bpe.webservice; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.Objects; +import java.util.function.Consumer; + +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; import org.camunda.bpm.engine.RepositoryService; -import org.camunda.bpm.engine.RuntimeService; import org.camunda.bpm.engine.repository.Deployment; import org.camunda.bpm.engine.repository.ProcessDefinition; import org.camunda.bpm.model.bpmn.BpmnModelInstance; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; +import org.thymeleaf.context.Context; +import dev.dsf.bpe.ui.ThymeleafTemplateService; +import jakarta.annotation.security.RolesAllowed; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.core.Context; -import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.Status; -import jakarta.ws.rs.core.UriInfo; +import jakarta.ws.rs.core.StreamingOutput; +@RolesAllowed("ADMIN") @Path(ProcessService.PATH) -public class ProcessService implements InitializingBean +public class ProcessService extends AbstractService implements InitializingBean { public static final String PATH = "Process"; - private static final Logger logger = LoggerFactory.getLogger(ProcessService.class); - - private final RuntimeService runtimeService; private final RepositoryService repositoryService; + private final TransformerFactory transformerFactory; - public ProcessService(RuntimeService runtimeService, RepositoryService repositoryService) + public ProcessService(ThymeleafTemplateService templateService, RepositoryService repositoryService) { - this.runtimeService = runtimeService; + super(templateService, "Process"); + this.repositoryService = repositoryService; + transformerFactory = TransformerFactory.newInstance(); } @Override public void afterPropertiesSet() throws Exception { - Objects.requireNonNull(runtimeService, "runtimeService"); + super.afterPropertiesSet(); + Objects.requireNonNull(repositoryService, "repositoryService"); } - private ProcessDefinition getProcessDefinition(String processDefinitionDomain, String processDefinitionKey, - String versionTag) + @GET + @Path("/{key}") + @Produces({ MediaType.TEXT_HTML }) + public Response readHtml(@PathParam("key") String key) { - if (versionTag != null && !versionTag.isBlank()) - return repositoryService.createProcessDefinitionQuery() - .processDefinitionKey(processDefinitionDomain + "_" + processDefinitionKey).versionTag(versionTag) - .singleResult(); - else - return repositoryService.createProcessDefinitionQuery() - .processDefinitionKey(processDefinitionDomain + "_" + processDefinitionKey).latestVersion() - .singleResult(); + return readHtml(key, null); } @GET - @Path("/{domain}/{key}") - public Response read(@PathParam("domain") String domain, @PathParam("key") String key, @Context UriInfo uri, - @Context HttpHeaders headers) + @Path("/{key}/{version}") + @Produces({ MediaType.TEXT_HTML }) + public Response readHtml(@PathParam("key") String key, @PathParam("version") String version) { - logger.trace("GET {}", uri.getRequestUri().toString()); + DefinitionDeploymentModel ddm = getProcess(key, version); - ProcessDefinition processDefinition = getProcessDefinition(domain, key, null); - if (processDefinition == null) + if (ddm == null) return Response.status(Status.NOT_FOUND).build(); - Deployment deployment = repositoryService.createDeploymentQuery() - .deploymentId(processDefinition.getDeploymentId()).orderByDeploymentTime().desc().singleResult(); + StreamingOutput output = write("DSF: Process", + "Process: " + ddm.definition().getKey() + "|" + ddm.definition().getVersionTag(), + setContextValues(ddm)); + + return Response.ok(output).build(); + } + private DefinitionDeploymentModel getProcess(String key, String version) + { + ProcessDefinition definition = getProcessDefinition(key, version); + if (definition == null) + return null; + + Deployment deployment = repositoryService.createDeploymentQuery().deploymentId(definition.getDeploymentId()) + .orderByDeploymentTime().desc().singleResult(); if (deployment == null) - return Response.status(Status.NOT_FOUND).build(); + return null; + + BpmnModelInstance model = repositoryService.getBpmnModelInstance(definition.getId()); - BpmnModelInstance bpmnModelInstance = repositoryService.getBpmnModelInstance(processDefinition.getId()); - return Response.ok(bpmnModelInstance.getDocument().getDomSource()) - .header("Content-Disposition", "attachment;filename=" + deployment.getSource()).build(); + return new DefinitionDeploymentModel(definition, deployment, model); } - @GET - @Path("/{domain}/{key}/{version}") - public Response vread(@PathParam("domain") String domain, @PathParam("key") String key, - @PathParam("version") String version, @Context UriInfo uri, @Context HttpHeaders headers) + private ProcessDefinition getProcessDefinition(String processDefinitionKey, String versionTag) + { + if (versionTag != null && !versionTag.isBlank()) + return repositoryService.createProcessDefinitionQuery().processDefinitionKey(processDefinitionKey) + .versionTag(versionTag).singleResult(); + else + return repositoryService.createProcessDefinitionQuery().processDefinitionKey(processDefinitionKey) + .latestVersion().singleResult(); + } + + private record DefinitionDeploymentModel(ProcessDefinition definition, Deployment deployment, + BpmnModelInstance model) { - logger.trace("GET {}", uri.getRequestUri().toString()); + } - ProcessDefinition processDefinition = getProcessDefinition(domain, key, version); - if (processDefinition == null) - return Response.status(Status.NOT_FOUND).build(); + private Consumer setContextValues(DefinitionDeploymentModel ddm) + { + return context -> + { + context.setVariable("bpmnViewer", true); + context.setVariable("download", toDownload(ddm)); + }; + } - Deployment deployment = repositoryService.createDeploymentQuery() - .deploymentId(processDefinition.getDeploymentId()).orderByDeploymentTime().desc().singleResult(); + private String getBpmnBase64Encoded(BpmnModelInstance model) + { + DOMSource domSource = model.getDocument().getDomSource(); - if (deployment == null) - return Response.status(Status.NOT_FOUND).build(); + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) + { + Transformer transformer = transformerFactory.newTransformer(); + transformer.transform(domSource, new StreamResult(out)); + + byte[] encoded = Base64.getEncoder().encode(out.toByteArray()); + + return new String(encoded, StandardCharsets.UTF_8); + } + catch (IOException | TransformerException e) + { + throw new RuntimeException(e); + } + } + + private record Download(String href, String title, String filename) + { + } + + private Download toDownload(DefinitionDeploymentModel ddm) + { + String href = "data:application/xml;base64," + getBpmnBase64Encoded(ddm.model()); + String filename = ddm.definition().getKey() + "_" + ddm.definition().getVersionTag().replaceAll("\\.", "_") + + ".bpmn"; - BpmnModelInstance bpmnModelInstance = repositoryService.getBpmnModelInstance(processDefinition.getId()); - return Response.ok(bpmnModelInstance.getDocument().getDomSource()) - .header("Content-Disposition", "attachment;filename=" + deployment.getSource()).build(); + return new Download(href, "Download as BPMN", filename); } } diff --git a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/webservice/RootService.java b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/webservice/RootService.java index ad336748d..f77c6db58 100644 --- a/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/webservice/RootService.java +++ b/dsf-bpe/dsf-bpe-server/src/main/java/dev/dsf/bpe/webservice/RootService.java @@ -1,67 +1,111 @@ package dev.dsf.bpe.webservice; -import java.security.Principal; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.camunda.bpm.engine.RepositoryService; +import org.camunda.bpm.engine.RuntimeService; +import org.camunda.bpm.engine.repository.ProcessDefinition; +import org.camunda.bpm.engine.runtime.ActivityInstance; +import org.camunda.bpm.engine.runtime.ProcessInstance; +import org.springframework.beans.factory.InitializingBean; +import org.thymeleaf.context.Context; -import dev.dsf.common.auth.conf.Identity; +import dev.dsf.bpe.ui.ThymeleafTemplateService; +import jakarta.annotation.security.RolesAllowed; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.core.SecurityContext; -import jakarta.ws.rs.core.UriInfo; +import jakarta.ws.rs.core.StreamingOutput; +@RolesAllowed("ADMIN") @Path(RootService.PATH) -@Produces({ MediaType.TEXT_HTML }) -public class RootService +public class RootService extends AbstractService implements InitializingBean { public static final String PATH = ""; - private static final Logger logger = LoggerFactory.getLogger(RootService.class); + private final RepositoryService repositoryService; + private final RuntimeService runtimeService; + + public RootService(ThymeleafTemplateService templateService, RepositoryService repositoryService, + RuntimeService runtimeService) + { + super(templateService, "root"); + + this.repositoryService = repositoryService; + this.runtimeService = runtimeService; + } + + @Override + public void afterPropertiesSet() throws Exception + { + super.afterPropertiesSet(); + + Objects.requireNonNull(repositoryService, "repositoryService"); + Objects.requireNonNull(runtimeService, "runtimeService"); + } @GET - public Response root(@Context UriInfo uri, @Context SecurityContext securityContext) + @Produces({ MediaType.TEXT_HTML }) + public Response root() { - logger.trace("GET {}", uri.getRequestUri().toString()); - - StringBuilder out = new StringBuilder(); - out.append(""" - - - BPE Root - - - Hello, ${user} - """.replace("${user}", getDisplayName(securityContext))); - - if ("OPENID".equals(securityContext.getAuthenticationScheme())) - { - final String basePath = uri.getBaseUri().getRawPath(); - - out.append(""" - Logout - """.replace("${basePath}", basePath)); - } - - out.append(""" - - - """); - - return Response.ok(out.toString()).build(); + StreamingOutput output = write("DSF: BPE", "BPE", this::setContextValues); + + return Response.ok(output).build(); } - private String getDisplayName(SecurityContext securityContext) + private void setContextValues(Context context) { - Principal userPrincipal = securityContext.getUserPrincipal(); - if (userPrincipal != null && userPrincipal instanceof Identity identity) - return identity.getDisplayName(); - else - return "?"; + context.setVariable("processes", processes()); + context.setVariable("processInstances", processInstances()); + } + + private record ProcessEntry(String href, String value) + { + } + + private List processes() + { + return repositoryService.createProcessDefinitionQuery().active().unlimitedList().stream() + .map(def -> new ProcessEntry("Process/" + def.getKey() + "/" + def.getVersionTag(), + def.getKey() + " | " + def.getVersionTag())) + .sorted(Comparator.comparing(ProcessEntry::value)).toList(); + } + + private List processInstances() + { + return repositoryService.createProcessDefinitionQuery().active().unlimitedList().stream() + .sorted(Comparator.comparing(ProcessDefinition::getKey).thenComparing(ProcessDefinition::getVersionTag)) + .flatMap(def -> runtimeService.createProcessInstanceQuery().deploymentId(def.getDeploymentId()) + .unlimitedList().stream().sorted(Comparator.comparing(ProcessInstance::getBusinessKey)).map(p -> + { + ActivityInstance activity = runtimeService.getActivityInstance(p.getProcessInstanceId()); + + if (activity != null) + { + String childActivities = Stream.of(activity.getChildActivityInstances()) + .map(a -> a.getActivityType() + ":" + + (a.getActivityName() != null ? a.getActivityName() + : a.getActivityId())) + .collect(Collectors.joining(", ", "[", "]")); + if ("[]".equals(childActivities)) + return def.getKey() + " | " + def.getVersionTag() + ": " + p.getBusinessKey() + + " -> " + activity.getActivityType() + ":" + + (activity.getActivityName() != null ? activity.getActivityName() + : activity.getActivityId()); + else + return def.getKey() + " | " + def.getVersionTag() + ": " + p.getBusinessKey() + + " -> " + childActivities; + } + else + return def.getKey() + " | " + def.getVersionTag() + ": " + p.getBusinessKey(); + })) + .toList(); } } diff --git a/dsf-bpe/dsf-bpe-server/src/main/resources/static/bpmn-viewer-dev.js b/dsf-bpe/dsf-bpe-server/src/main/resources/static/bpmn-viewer-dev.js new file mode 100644 index 000000000..32fd4081b --- /dev/null +++ b/dsf-bpe/dsf-bpe-server/src/main/resources/static/bpmn-viewer-dev.js @@ -0,0 +1,23355 @@ +/*! + * bpmn-js - bpmn-viewer v17.0.2 + * + * Copyright (c) 2014-present, camunda Services GmbH + * + * Released under the bpmn.io license + * http://bpmn.io/license + * + * Source Code: https://github.com/bpmn-io/bpmn-js + * + * Date: 2024-02-16 + */ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.BpmnJS = factory()); +})(this, (function () { 'use strict'; + + function e(e,t){t&&(e.super_=t,e.prototype=Object.create(t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}));} + + /** + * Flatten array, one level deep. + * + * @template T + * + * @param {T[][]} arr + * + * @return {T[]} + */ + + const nativeToString$1 = Object.prototype.toString; + const nativeHasOwnProperty$1 = Object.prototype.hasOwnProperty; + + function isUndefined$2(obj) { + return obj === undefined; + } + + function isDefined(obj) { + return obj !== undefined; + } + + function isNil(obj) { + return obj == null; + } + + function isArray$2(obj) { + return nativeToString$1.call(obj) === '[object Array]'; + } + + function isObject(obj) { + return nativeToString$1.call(obj) === '[object Object]'; + } + + function isNumber(obj) { + return nativeToString$1.call(obj) === '[object Number]'; + } + + /** + * @param {any} obj + * + * @return {boolean} + */ + function isFunction(obj) { + const tag = nativeToString$1.call(obj); + + return ( + tag === '[object Function]' || + tag === '[object AsyncFunction]' || + tag === '[object GeneratorFunction]' || + tag === '[object AsyncGeneratorFunction]' || + tag === '[object Proxy]' + ); + } + + function isString(obj) { + return nativeToString$1.call(obj) === '[object String]'; + } + + /** + * Return true, if target owns a property with the given key. + * + * @param {Object} target + * @param {String} key + * + * @return {Boolean} + */ + function has$1(target, key) { + return nativeHasOwnProperty$1.call(target, key); + } + + /** + * @template T + * @typedef { ( + * ((e: T) => boolean) | + * ((e: T, idx: number) => boolean) | + * ((e: T, key: string) => boolean) | + * string | + * number + * ) } Matcher + */ + + /** + * @template T + * @template U + * + * @typedef { ( + * ((e: T) => U) | string | number + * ) } Extractor + */ + + + /** + * @template T + * @typedef { (val: T, key: any) => boolean } MatchFn + */ + + /** + * @template T + * @typedef { T[] } ArrayCollection + */ + + /** + * @template T + * @typedef { { [key: string]: T } } StringKeyValueCollection + */ + + /** + * @template T + * @typedef { { [key: number]: T } } NumberKeyValueCollection + */ + + /** + * @template T + * @typedef { StringKeyValueCollection | NumberKeyValueCollection } KeyValueCollection + */ + + /** + * @template T + * @typedef { KeyValueCollection | ArrayCollection } Collection + */ + + /** + * Find element in collection. + * + * @template T + * @param {Collection} collection + * @param {Matcher} matcher + * + * @return {Object} + */ + function find(collection, matcher) { + + const matchFn = toMatcher(matcher); + + let match; + + forEach$1(collection, function(val, key) { + if (matchFn(val, key)) { + match = val; + + return false; + } + }); + + return match; + + } + + + /** + * Find element index in collection. + * + * @template T + * @param {Collection} collection + * @param {Matcher} matcher + * + * @return {number} + */ + function findIndex(collection, matcher) { + + const matchFn = toMatcher(matcher); + + let idx = isArray$2(collection) ? -1 : undefined; + + forEach$1(collection, function(val, key) { + if (matchFn(val, key)) { + idx = key; + + return false; + } + }); + + return idx; + } + + + /** + * Filter elements in collection. + * + * @template T + * @param {Collection} collection + * @param {Matcher} matcher + * + * @return {T[]} result + */ + function filter(collection, matcher) { + + const matchFn = toMatcher(matcher); + + let result = []; + + forEach$1(collection, function(val, key) { + if (matchFn(val, key)) { + result.push(val); + } + }); + + return result; + } + + + /** + * Iterate over collection; returning something + * (non-undefined) will stop iteration. + * + * @template T + * @param {Collection} collection + * @param { ((item: T, idx: number) => (boolean|void)) | ((item: T, key: string) => (boolean|void)) } iterator + * + * @return {T} return result that stopped the iteration + */ + function forEach$1(collection, iterator) { + + let val, + result; + + if (isUndefined$2(collection)) { + return; + } + + const convertKey = isArray$2(collection) ? toNum$1 : identity$1; + + for (let key in collection) { + + if (has$1(collection, key)) { + val = collection[key]; + + result = iterator(val, convertKey(key)); + + if (result === false) { + return val; + } + } + } + } + + + /** + * Reduce collection, returning a single result. + * + * @template T + * @template V + * + * @param {Collection} collection + * @param {(result: V, entry: T, index: any) => V} iterator + * @param {V} result + * + * @return {V} result returned from last iterator + */ + function reduce(collection, iterator, result) { + + forEach$1(collection, function(value, idx) { + result = iterator(result, value, idx); + }); + + return result; + } + + + /** + * Return true if every element in the collection + * matches the criteria. + * + * @param {Object|Array} collection + * @param {Function} matcher + * + * @return {Boolean} + */ + function every(collection, matcher) { + + return !!reduce(collection, function(matches, val, key) { + return matches && matcher(val, key); + }, true); + } + + + /** + * Return true if some elements in the collection + * match the criteria. + * + * @param {Object|Array} collection + * @param {Function} matcher + * + * @return {Boolean} + */ + function some(collection, matcher) { + + return !!find(collection, matcher); + } + + + /** + * Transform a collection into another collection + * by piping each member through the given fn. + * + * @param {Object|Array} collection + * @param {Function} fn + * + * @return {Array} transformed collection + */ + function map$1(collection, fn) { + + let result = []; + + forEach$1(collection, function(val, key) { + result.push(fn(val, key)); + }); + + return result; + } + + + /** + * Create an object pattern matcher. + * + * @example + * + * ```javascript + * const matcher = matchPattern({ id: 1 }); + * + * let element = find(elements, matcher); + * ``` + * + * @template T + * + * @param {T} pattern + * + * @return { (el: any) => boolean } matcherFn + */ + function matchPattern(pattern) { + + return function(el) { + + return every(pattern, function(val, key) { + return el[key] === val; + }); + + }; + } + + + /** + * @template T + * @param {Matcher} matcher + * + * @return {MatchFn} + */ + function toMatcher(matcher) { + return isFunction(matcher) ? matcher : (e) => { + return e === matcher; + }; + } + + + function identity$1(arg) { + return arg; + } + + function toNum$1(arg) { + return Number(arg); + } + + /* global setTimeout clearTimeout */ + + /** + * @typedef { { + * (...args: any[]): any; + * flush: () => void; + * cancel: () => void; + * } } DebouncedFunction + */ + + /** + * Debounce fn, calling it only once if the given time + * elapsed between calls. + * + * Lodash-style the function exposes methods to `#clear` + * and `#flush` to control internal behavior. + * + * @param {Function} fn + * @param {Number} timeout + * + * @return {DebouncedFunction} debounced function + */ + function debounce(fn, timeout) { + + let timer; + + let lastArgs; + let lastThis; + + let lastNow; + + function fire(force) { + + let now = Date.now(); + + let scheduledDiff = force ? 0 : (lastNow + timeout) - now; + + if (scheduledDiff > 0) { + return schedule(scheduledDiff); + } + + fn.apply(lastThis, lastArgs); + + clear(); + } + + function schedule(timeout) { + timer = setTimeout(fire, timeout); + } + + function clear() { + if (timer) { + clearTimeout(timer); + } + + timer = lastNow = lastArgs = lastThis = undefined; + } + + function flush() { + if (timer) { + fire(true); + } + + clear(); + } + + /** + * @type { DebouncedFunction } + */ + function callback(...args) { + lastNow = Date.now(); + + lastArgs = args; + lastThis = this; + + // ensure an execution is scheduled + if (!timer) { + schedule(timeout); + } + } + + callback.flush = flush; + callback.cancel = clear; + + return callback; + } + + /** + * Bind function against target . + * + * @param {Function} fn + * @param {Object} target + * + * @return {Function} bound function + */ + function bind$2(fn, target) { + return fn.bind(target); + } + + /** + * Convenience wrapper for `Object.assign`. + * + * @param {Object} target + * @param {...Object} others + * + * @return {Object} the target + */ + function assign$1(target, ...others) { + return Object.assign(target, ...others); + } + + /** + * Sets a nested property of a given object to the specified value. + * + * This mutates the object and returns it. + * + * @template T + * + * @param {T} target The target of the set operation. + * @param {(string|number)[]} path The path to the nested value. + * @param {any} value The value to set. + * + * @return {T} + */ + function set$1(target, path, value) { + + let currentTarget = target; + + forEach$1(path, function(key, idx) { + + if (typeof key !== 'number' && typeof key !== 'string') { + throw new Error('illegal key type: ' + typeof key + '. Key should be of type number or string.'); + } + + if (key === 'constructor') { + throw new Error('illegal key: constructor'); + } + + if (key === '__proto__') { + throw new Error('illegal key: __proto__'); + } + + let nextKey = path[idx + 1]; + let nextTarget = currentTarget[key]; + + if (isDefined(nextKey) && isNil(nextTarget)) { + nextTarget = currentTarget[key] = isNaN(+nextKey) ? {} : []; + } + + if (isUndefined$2(nextKey)) { + if (isUndefined$2(value)) { + delete currentTarget[key]; + } else { + currentTarget[key] = value; + } + } else { + currentTarget = nextTarget; + } + }); + + return target; + } + + /** + * Pick properties from the given target. + * + * @template T + * @template {any[]} V + * + * @param {T} target + * @param {V} properties + * + * @return Pick + */ + function pick(target, properties) { + + let result = {}; + + let obj = Object(target); + + forEach$1(properties, function(prop) { + + if (prop in obj) { + result[prop] = target[prop]; + } + }); + + return result; + } + + /** + * Pick all target properties, excluding the given ones. + * + * @template T + * @template {any[]} V + * + * @param {T} target + * @param {V} properties + * + * @return {Omit} target + */ + function omit(target, properties) { + + let result = {}; + + let obj = Object(target); + + forEach$1(obj, function(prop, key) { + + if (properties.indexOf(key) === -1) { + result[key] = prop; + } + }); + + return result; + } + + var DEFAULT_RENDER_PRIORITY$1 = 1000; + + /** + * @typedef {import('../core/Types').ElementLike} Element + * @typedef {import('../core/Types').ConnectionLike} Connection + * @typedef {import('../core/Types').ShapeLike} Shape + * + * @typedef {import('../core/EventBus').default} EventBus + */ + + /** + * The base implementation of shape and connection renderers. + * + * @param {EventBus} eventBus + * @param {number} [renderPriority=1000] + */ + function BaseRenderer(eventBus, renderPriority) { + var self = this; + + renderPriority = renderPriority || DEFAULT_RENDER_PRIORITY$1; + + eventBus.on([ 'render.shape', 'render.connection' ], renderPriority, function(evt, context) { + var type = evt.type, + element = context.element, + visuals = context.gfx, + attrs = context.attrs; + + if (self.canRender(element)) { + if (type === 'render.shape') { + return self.drawShape(visuals, element, attrs); + } else { + return self.drawConnection(visuals, element, attrs); + } + } + }); + + eventBus.on([ 'render.getShapePath', 'render.getConnectionPath' ], renderPriority, function(evt, element) { + if (self.canRender(element)) { + if (evt.type === 'render.getShapePath') { + return self.getShapePath(element); + } else { + return self.getConnectionPath(element); + } + } + }); + } + + /** + * Checks whether an element can be rendered. + * + * @param {Element} element The element to be rendered. + * + * @return {boolean} Whether the element can be rendered. + */ + BaseRenderer.prototype.canRender = function(element) {}; + + /** + * Draws a shape. + * + * @param {SVGElement} visuals The SVG element to draw the shape into. + * @param {Shape} shape The shape to be drawn. + * + * @return {SVGElement} The SVG element of the shape drawn. + */ + BaseRenderer.prototype.drawShape = function(visuals, shape) {}; + + /** + * Draws a connection. + * + * @param {SVGElement} visuals The SVG element to draw the connection into. + * @param {Connection} connection The connection to be drawn. + * + * @return {SVGElement} The SVG element of the connection drawn. + */ + BaseRenderer.prototype.drawConnection = function(visuals, connection) {}; + + /** + * Gets the SVG path of the graphical representation of a shape. + * + * @param {Shape} shape The shape. + * + * @return {string} The SVG path of the shape. + */ + BaseRenderer.prototype.getShapePath = function(shape) {}; + + /** + * Gets the SVG path of the graphical representation of a connection. + * + * @param {Connection} connection The connection. + * + * @return {string} The SVG path of the connection. + */ + BaseRenderer.prototype.getConnectionPath = function(connection) {}; + + /** + * @typedef { import('../model/Types').Element } Element + * @typedef { import('../model/Types').ModdleElement } ModdleElement + */ + + /** + * Is an element of the given BPMN type? + * + * @param {Element|ModdleElement} element + * @param {string} type + * + * @return {boolean} + */ + function is$1(element, type) { + var bo = getBusinessObject(element); + + return bo && (typeof bo.$instanceOf === 'function') && bo.$instanceOf(type); + } + + + /** + * Return true if element has any of the given types. + * + * @param {Element|ModdleElement} element + * @param {string[]} types + * + * @return {boolean} + */ + function isAny(element, types) { + return some(types, function(t) { + return is$1(element, t); + }); + } + + /** + * Return the business object for a given element. + * + * @param {Element|ModdleElement} element + * + * @return {ModdleElement} + */ + function getBusinessObject(element) { + return (element && element.businessObject) || element; + } + + /** + * Return the di object for a given element. + * + * @param {Element} element + * + * @return {ModdleElement} + */ + function getDi(element) { + return element && element.di; + } + + /** + * @typedef {import('../model/Types').Element} Element + * @typedef {import('../model/Types').ModdleElement} ModdleElement + */ + + /** + * @param {Element} element + * @param {ModdleElement} [di] + * + * @return {boolean} + */ + function isExpanded(element, di) { + + if (is$1(element, 'bpmn:CallActivity')) { + return false; + } + + if (is$1(element, 'bpmn:SubProcess')) { + di = di || getDi(element); + + if (di && is$1(di, 'bpmndi:BPMNPlane')) { + return true; + } + + return di && !!di.isExpanded; + } + + if (is$1(element, 'bpmn:Participant')) { + return !!getBusinessObject(element).processRef; + } + + return true; + } + + /** + * @param {Element} element + * + * @return {boolean} + */ + function isHorizontal(element) { + + if (!is$1(element, 'bpmn:Participant') && !is$1(element, 'bpmn:Lane')) { + return undefined; + } + + var isHorizontal = getDi(element).isHorizontal; + + if (isHorizontal === undefined) { + return true; + } + + return isHorizontal; + } + + /** + * @param {Element} element + * + * @return {boolean} + */ + function isEventSubProcess(element) { + return element && !!getBusinessObject(element).triggeredByEvent; + } + + /** + * Checks whether a value is an instance of Connection. + * + * @param {any} value + * + * @return {boolean} + */ + function isConnection(value) { + return isObject(value) && has$1(value, 'waypoints'); + } + + /** + * @typedef {import('diagram-js/lib/util/Types').Point} Point + * @typedef {import('diagram-js/lib/util/Types').Rect} Rect + * + * @typedef {import('../model/Types').Element} Element + * @typedef {import('../model/Types').ModdleElement} ModdleElement + */ + + var DEFAULT_LABEL_SIZE$1 = { + width: 90, + height: 20 + }; + + var FLOW_LABEL_INDENT = 15; + + + /** + * Return true if the given semantic has an external label. + * + * @param {Element} semantic + * + * @return {boolean} + */ + function isLabelExternal(semantic) { + return is$1(semantic, 'bpmn:Event') || + is$1(semantic, 'bpmn:Gateway') || + is$1(semantic, 'bpmn:DataStoreReference') || + is$1(semantic, 'bpmn:DataObjectReference') || + is$1(semantic, 'bpmn:DataInput') || + is$1(semantic, 'bpmn:DataOutput') || + is$1(semantic, 'bpmn:SequenceFlow') || + is$1(semantic, 'bpmn:MessageFlow') || + is$1(semantic, 'bpmn:Group'); + } + + /** + * Get the position of a sequence flow label. + * + * @param {Point[]} waypoints + * + * @return {Point} + */ + function getFlowLabelPosition(waypoints) { + + // get the waypoints mid + var mid = waypoints.length / 2 - 1; + + var first = waypoints[Math.floor(mid)]; + var second = waypoints[Math.ceil(mid + 0.01)]; + + // get position + var position = getWaypointsMid(waypoints); + + // calculate angle + var angle = Math.atan((second.y - first.y) / (second.x - first.x)); + + var x = position.x, + y = position.y; + + if (Math.abs(angle) < Math.PI / 2) { + y -= FLOW_LABEL_INDENT; + } else { + x += FLOW_LABEL_INDENT; + } + + return { x: x, y: y }; + } + + + /** + * Get the middle of a number of waypoints. + * + * @param {Point[]} waypoints + * + * @return {Point} + */ + function getWaypointsMid(waypoints) { + + var mid = waypoints.length / 2 - 1; + + var first = waypoints[Math.floor(mid)]; + var second = waypoints[Math.ceil(mid + 0.01)]; + + return { + x: first.x + (second.x - first.x) / 2, + y: first.y + (second.y - first.y) / 2 + }; + } + + /** + * Get the middle of the external label of an element. + * + * @param {Element} element + * + * @return {Point} + */ + function getExternalLabelMid(element) { + + if (element.waypoints) { + return getFlowLabelPosition(element.waypoints); + } else if (is$1(element, 'bpmn:Group')) { + return { + x: element.x + element.width / 2, + y: element.y + DEFAULT_LABEL_SIZE$1.height / 2 + }; + } else { + return { + x: element.x + element.width / 2, + y: element.y + element.height + DEFAULT_LABEL_SIZE$1.height / 2 + }; + } + } + + + /** + * Return the bounds of an elements label, parsed from the elements DI or + * generated from its bounds. + * + * @param {ModdleElement} di + * @param {Element} element + * + * @return {Rect} + */ + function getExternalLabelBounds(di, element) { + + var mid, + size, + bounds, + label = di.label; + + if (label && label.bounds) { + bounds = label.bounds; + + size = { + width: Math.max(DEFAULT_LABEL_SIZE$1.width, bounds.width), + height: bounds.height + }; + + mid = { + x: bounds.x + bounds.width / 2, + y: bounds.y + bounds.height / 2 + }; + } else { + + mid = getExternalLabelMid(element); + + size = DEFAULT_LABEL_SIZE$1; + } + + return assign$1({ + x: mid.x - size.width / 2, + y: mid.y - size.height / 2 + }, size); + } + + /** + * @param {ModdleElement} semantic + * + * @returns {string} + */ + function getLabelAttr(semantic) { + if ( + is$1(semantic, 'bpmn:FlowElement') || + is$1(semantic, 'bpmn:Participant') || + is$1(semantic, 'bpmn:Lane') || + is$1(semantic, 'bpmn:SequenceFlow') || + is$1(semantic, 'bpmn:MessageFlow') || + is$1(semantic, 'bpmn:DataInput') || + is$1(semantic, 'bpmn:DataOutput') + ) { + return 'name'; + } + + if (is$1(semantic, 'bpmn:TextAnnotation')) { + return 'text'; + } + + if (is$1(semantic, 'bpmn:Group')) { + return 'categoryValueRef'; + } + } + + /** + * @param {ModdleElement} semantic + * + * @returns {string} + */ + function getCategoryValue(semantic) { + var categoryValueRef = semantic['categoryValueRef']; + + if (!categoryValueRef) { + return ''; + } + + + return categoryValueRef.value || ''; + } + + /** + * @param {Element} element + * + * @return {string} + */ + function getLabel(element) { + var semantic = element.businessObject, + attr = getLabelAttr(semantic); + + if (attr) { + + if (attr === 'categoryValueRef') { + + return getCategoryValue(semantic); + } + + return semantic[attr] || ''; + } + } + + function ensureImported(element, target) { + + if (element.ownerDocument !== target.ownerDocument) { + try { + + // may fail on webkit + return target.ownerDocument.importNode(element, true); + } catch (e) { + + // ignore + } + } + + return element; + } + + /** + * appendTo utility + */ + + /** + * Append a node to a target element and return the appended node. + * + * @param {SVGElement} element + * @param {SVGElement} target + * + * @return {SVGElement} the appended node + */ + function appendTo(element, target) { + return target.appendChild(ensureImported(element, target)); + } + + /** + * append utility + */ + + /** + * Append a node to an element + * + * @param {SVGElement} element + * @param {SVGElement} node + * + * @return {SVGElement} the element + */ + function append(target, node) { + appendTo(node, target); + return target; + } + + /** + * attribute accessor utility + */ + + var LENGTH_ATTR = 2; + + var CSS_PROPERTIES = { + 'alignment-baseline': 1, + 'baseline-shift': 1, + 'clip': 1, + 'clip-path': 1, + 'clip-rule': 1, + 'color': 1, + 'color-interpolation': 1, + 'color-interpolation-filters': 1, + 'color-profile': 1, + 'color-rendering': 1, + 'cursor': 1, + 'direction': 1, + 'display': 1, + 'dominant-baseline': 1, + 'enable-background': 1, + 'fill': 1, + 'fill-opacity': 1, + 'fill-rule': 1, + 'filter': 1, + 'flood-color': 1, + 'flood-opacity': 1, + 'font': 1, + 'font-family': 1, + 'font-size': LENGTH_ATTR, + 'font-size-adjust': 1, + 'font-stretch': 1, + 'font-style': 1, + 'font-variant': 1, + 'font-weight': 1, + 'glyph-orientation-horizontal': 1, + 'glyph-orientation-vertical': 1, + 'image-rendering': 1, + 'kerning': 1, + 'letter-spacing': 1, + 'lighting-color': 1, + 'marker': 1, + 'marker-end': 1, + 'marker-mid': 1, + 'marker-start': 1, + 'mask': 1, + 'opacity': 1, + 'overflow': 1, + 'pointer-events': 1, + 'shape-rendering': 1, + 'stop-color': 1, + 'stop-opacity': 1, + 'stroke': 1, + 'stroke-dasharray': 1, + 'stroke-dashoffset': 1, + 'stroke-linecap': 1, + 'stroke-linejoin': 1, + 'stroke-miterlimit': 1, + 'stroke-opacity': 1, + 'stroke-width': LENGTH_ATTR, + 'text-anchor': 1, + 'text-decoration': 1, + 'text-rendering': 1, + 'unicode-bidi': 1, + 'visibility': 1, + 'word-spacing': 1, + 'writing-mode': 1 + }; + + + function getAttribute(node, name) { + if (CSS_PROPERTIES[name]) { + return node.style[name]; + } else { + return node.getAttributeNS(null, name); + } + } + + function setAttribute(node, name, value) { + var hyphenated = name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); + + var type = CSS_PROPERTIES[hyphenated]; + + if (type) { + + // append pixel unit, unless present + if (type === LENGTH_ATTR && typeof value === 'number') { + value = String(value) + 'px'; + } + + node.style[hyphenated] = value; + } else { + node.setAttributeNS(null, name, value); + } + } + + function setAttributes(node, attrs) { + + var names = Object.keys(attrs), i, name; + + for (i = 0, name; (name = names[i]); i++) { + setAttribute(node, name, attrs[name]); + } + } + + /** + * Gets or sets raw attributes on a node. + * + * @param {SVGElement} node + * @param {Object} [attrs] + * @param {String} [name] + * @param {String} [value] + * + * @return {String} + */ + function attr$1(node, name, value) { + if (typeof name === 'string') { + if (value !== undefined) { + setAttribute(node, name, value); + } else { + return getAttribute(node, name); + } + } else { + setAttributes(node, name); + } + + return node; + } + + /** + * Taken from https://github.com/component/classes + * + * Without the component bits. + */ + + /** + * toString reference. + */ + + const toString$1 = Object.prototype.toString; + + /** + * Wrap `el` in a `ClassList`. + * + * @param {Element} el + * @return {ClassList} + * @api public + */ + + function classes$1(el) { + return new ClassList$1(el); + } + + function ClassList$1(el) { + if (!el || !el.nodeType) { + throw new Error('A DOM element reference is required'); + } + this.el = el; + this.list = el.classList; + } + + /** + * Add class `name` if not already present. + * + * @param {String} name + * @return {ClassList} + * @api public + */ + + ClassList$1.prototype.add = function(name) { + this.list.add(name); + return this; + }; + + /** + * Remove class `name` when present, or + * pass a regular expression to remove + * any which match. + * + * @param {String|RegExp} name + * @return {ClassList} + * @api public + */ + + ClassList$1.prototype.remove = function(name) { + if ('[object RegExp]' == toString$1.call(name)) { + return this.removeMatching(name); + } + + this.list.remove(name); + return this; + }; + + /** + * Remove all classes matching `re`. + * + * @param {RegExp} re + * @return {ClassList} + * @api private + */ + + ClassList$1.prototype.removeMatching = function(re) { + const arr = this.array(); + for (let i = 0; i < arr.length; i++) { + if (re.test(arr[i])) { + this.remove(arr[i]); + } + } + return this; + }; + + /** + * Toggle class `name`, can force state via `force`. + * + * For browsers that support classList, but do not support `force` yet, + * the mistake will be detected and corrected. + * + * @param {String} name + * @param {Boolean} force + * @return {ClassList} + * @api public + */ + + ClassList$1.prototype.toggle = function(name, force) { + if ('undefined' !== typeof force) { + if (force !== this.list.toggle(name, force)) { + this.list.toggle(name); // toggle again to correct + } + } else { + this.list.toggle(name); + } + return this; + }; + + /** + * Return an array of classes. + * + * @return {Array} + * @api public + */ + + ClassList$1.prototype.array = function() { + return Array.from(this.list); + }; + + /** + * Check if class `name` is present. + * + * @param {String} name + * @return {ClassList} + * @api public + */ + + ClassList$1.prototype.has = + ClassList$1.prototype.contains = function(name) { + return this.list.contains(name); + }; + + function remove$2(element) { + var parent = element.parentNode; + + if (parent) { + parent.removeChild(element); + } + + return element; + } + + /** + * Clear utility + */ + + /** + * Removes all children from the given element + * + * @param {DOMElement} element + * @return {DOMElement} the element (for chaining) + */ + function clear$1(element) { + var child; + + while ((child = element.firstChild)) { + remove$2(child); + } + + return element; + } + + var ns = { + svg: 'http://www.w3.org/2000/svg' + }; + + /** + * DOM parsing utility + */ + + var SVG_START = '' + svg + ''; + unwrap = true; + } + + var parsed = parseDocument(svg); + + if (!unwrap) { + return parsed; + } + + var fragment = document.createDocumentFragment(); + + var parent = parsed.firstChild; + + while (parent.firstChild) { + fragment.appendChild(parent.firstChild); + } + + return fragment; + } + + function parseDocument(svg) { + + var parser; + + // parse + parser = new DOMParser(); + parser.async = false; + + return parser.parseFromString(svg, 'text/xml'); + } + + /** + * Create utility for SVG elements + */ + + + /** + * Create a specific type from name or SVG markup. + * + * @param {String} name the name or markup of the element + * @param {Object} [attrs] attributes to set on the element + * + * @returns {SVGElement} + */ + function create$1(name, attrs) { + var element; + + if (name.charAt(0) === '<') { + element = parse$1(name).firstChild; + element = document.importNode(element, true); + } else { + element = document.createElementNS(ns.svg, name); + } + + if (attrs) { + attr$1(element, attrs); + } + + return element; + } + + /** + * Geometry helpers + */ + + // fake node used to instantiate svg geometry elements + var node = null; + + function getNode() { + if (node === null) { + node = create$1('svg'); + } + + return node; + } + + function extend$1(object, props) { + var i, k, keys = Object.keys(props); + + for (i = 0; (k = keys[i]); i++) { + object[k] = props[k]; + } + + return object; + } + + /** + * Create matrix via args. + * + * @example + * + * createMatrix({ a: 1, b: 1 }); + * createMatrix(); + * createMatrix(1, 2, 0, 0, 30, 20); + * + * @return {SVGMatrix} + */ + function createMatrix(a, b, c, d, e, f) { + var matrix = getNode().createSVGMatrix(); + + switch (arguments.length) { + case 0: + return matrix; + case 1: + return extend$1(matrix, a); + case 6: + return extend$1(matrix, { + a: a, + b: b, + c: c, + d: d, + e: e, + f: f + }); + } + } + + function createTransform(matrix) { + if (matrix) { + return getNode().createSVGTransformFromMatrix(matrix); + } else { + return getNode().createSVGTransform(); + } + } + + /** + * Serialization util + */ + + var TEXT_ENTITIES = /([&<>]{1})/g; + var ATTR_ENTITIES = /([\n\r"]{1})/g; + + var ENTITY_REPLACEMENT = { + '&': '&', + '<': '<', + '>': '>', + '"': '\'' + }; + + function escape$1(str, pattern) { + + function replaceFn(match, entity) { + return ENTITY_REPLACEMENT[entity] || entity; + } + + return str.replace(pattern, replaceFn); + } + + function serialize(node, output) { + + var i, len, attrMap, attrNode, childNodes; + + switch (node.nodeType) { + + // TEXT + case 3: + + // replace special XML characters + output.push(escape$1(node.textContent, TEXT_ENTITIES)); + break; + + // ELEMENT + case 1: + output.push('<', node.tagName); + + if (node.hasAttributes()) { + attrMap = node.attributes; + for (i = 0, len = attrMap.length; i < len; ++i) { + attrNode = attrMap.item(i); + output.push(' ', attrNode.name, '="', escape$1(attrNode.value, ATTR_ENTITIES), '"'); + } + } + + if (node.hasChildNodes()) { + output.push('>'); + childNodes = node.childNodes; + for (i = 0, len = childNodes.length; i < len; ++i) { + serialize(childNodes.item(i), output); + } + output.push(''); + } else { + output.push('/>'); + } + break; + + // COMMENT + case 8: + output.push(''); + break; + + // CDATA + case 4: + output.push(''); + break; + + default: + throw new Error('unable to handle node ' + node.nodeType); + } + + return output; + } + + /** + * innerHTML like functionality for SVG elements. + * based on innerSVG (https://code.google.com/p/innersvg) + */ + + + function set(element, svg) { + + var parsed = parse$1(svg); + + // clear element contents + clear$1(element); + + if (!svg) { + return; + } + + if (!isFragment(parsed)) { + + // extract from parsed document + parsed = parsed.documentElement; + } + + var nodes = slice$1(parsed.childNodes); + + // import + append each node + for (var i = 0; i < nodes.length; i++) { + appendTo(nodes[i], element); + } + + } + + function get(element) { + var child = element.firstChild, + output = []; + + while (child) { + serialize(child, output); + child = child.nextSibling; + } + + return output.join(''); + } + + function isFragment(node) { + return node.nodeName === '#document-fragment'; + } + + function innerSVG(element, svg) { + + if (svg !== undefined) { + + try { + set(element, svg); + } catch (e) { + throw new Error('error parsing SVG: ' + e.message); + } + + return element; + } else { + return get(element); + } + } + + + function slice$1(arr) { + return Array.prototype.slice.call(arr); + } + + /** + * transform accessor utility + */ + + function wrapMatrix(transformList, transform) { + if (transform instanceof SVGMatrix) { + return transformList.createSVGTransformFromMatrix(transform); + } + + return transform; + } + + + function setTransforms(transformList, transforms) { + var i, t; + + transformList.clear(); + + for (i = 0; (t = transforms[i]); i++) { + transformList.appendItem(wrapMatrix(transformList, t)); + } + } + + /** + * Get or set the transforms on the given node. + * + * @param {SVGElement} node + * @param {SVGTransform|SVGMatrix|Array} [transforms] + * + * @return {SVGTransform} the consolidated transform + */ + function transform$1(node, transforms) { + var transformList = node.transform.baseVal; + + if (transforms) { + + if (!Array.isArray(transforms)) { + transforms = [ transforms ]; + } + + setTransforms(transformList, transforms); + } + + return transformList.consolidate(); + } + + /** + * @typedef {(string|number)[]} Component + * + * @typedef {import('../util/Types').Point} Point + */ + + /** + * @param {Component[] | Component[][]} elements + * + * @return {string} + */ + function componentsToPath(elements) { + return elements.flat().join(',').replace(/,?([A-z]),?/g, '$1'); + } + + /** + * @param {Point} point + * + * @return {Component[]} + */ + function move(point) { + return [ 'M', point.x, point.y ]; + } + + /** + * @param {Point} point + * + * @return {Component[]} + */ + function lineTo(point) { + return [ 'L', point.x, point.y ]; + } + + /** + * @param {Point} p1 + * @param {Point} p2 + * @param {Point} p3 + * + * @return {Component[]} + */ + function curveTo(p1, p2, p3) { + return [ 'C', p1.x, p1.y, p2.x, p2.y, p3.x, p3.y ]; + } + + /** + * @param {Point[]} waypoints + * @param {number} [cornerRadius] + * @return {Component[][]} + */ + function drawPath(waypoints, cornerRadius) { + const pointCount = waypoints.length; + + const path = [ move(waypoints[0]) ]; + + for (let i = 1; i < pointCount; i++) { + + const pointBefore = waypoints[i - 1]; + const point = waypoints[i]; + const pointAfter = waypoints[i + 1]; + + if (!pointAfter || !cornerRadius) { + path.push(lineTo(point)); + + continue; + } + + const effectiveRadius = Math.min( + cornerRadius, + vectorLength(point.x - pointBefore.x, point.y - pointBefore.y), + vectorLength(pointAfter.x - point.x, pointAfter.y - point.y) + ); + + if (!effectiveRadius) { + path.push(lineTo(point)); + + continue; + } + + const beforePoint = getPointAtLength(point, pointBefore, effectiveRadius); + const beforePoint2 = getPointAtLength(point, pointBefore, effectiveRadius * .5); + + const afterPoint = getPointAtLength(point, pointAfter, effectiveRadius); + const afterPoint2 = getPointAtLength(point, pointAfter, effectiveRadius * .5); + + path.push(lineTo(beforePoint)); + path.push(curveTo(beforePoint2, afterPoint2, afterPoint)); + } + + return path; + } + + function getPointAtLength(start, end, length) { + + const deltaX = end.x - start.x; + const deltaY = end.y - start.y; + + const totalLength = vectorLength(deltaX, deltaY); + + const percent = length / totalLength; + + return { + x: start.x + deltaX * percent, + y: start.y + deltaY * percent + }; + } + + function vectorLength(x, y) { + return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)); + } + + /** + * @param {Point[]} points + * @param {number|Object} [attrs] + * @param {number} [radius] + * + * @return {SVGElement} + */ + function createLine(points, attrs, radius) { + + if (isNumber(attrs)) { + radius = attrs; + attrs = null; + } + + if (!attrs) { + attrs = {}; + } + + const line = create$1('path', attrs); + + if (isNumber(radius)) { + line.dataset.cornerRadius = String(radius); + } + + return updateLine(line, points); + } + + /** + * @param {SVGElement} gfx + * @param {Point[]} points + * + * @return {SVGElement} + */ + function updateLine(gfx, points) { + + const cornerRadius = parseInt(gfx.dataset.cornerRadius, 10) || 0; + + attr$1(gfx, { + d: componentsToPath(drawPath(points, cornerRadius)) + }); + + return gfx; + } + + var black = 'hsl(225, 10%, 15%)'; + var white = 'white'; + + // element utils ////////////////////// + + /** + * Checks if eventDefinition of the given element matches with semantic type. + * + * @param {ModdleElement} event + * @param {string} eventDefinitionType + * + * @return {boolean} + */ + function isTypedEvent(event, eventDefinitionType) { + return some(event.eventDefinitions, function(definition) { + return definition.$type === eventDefinitionType; + }); + } + + /** + * Check if element is a throw event. + * + * @param {ModdleElement} event + * + * @return {boolean} + */ + function isThrowEvent(event) { + return (event.$type === 'bpmn:IntermediateThrowEvent') || (event.$type === 'bpmn:EndEvent'); + } + + /** + * Check if element is a throw event. + * + * @param {ModdleElement} element + * + * @return {boolean} + */ + function isCollection(element) { + var dataObject = element.dataObjectRef; + + return element.isCollection || (dataObject && dataObject.isCollection); + } + + + // color access ////////////////////// + + /** + * @param {Element} element + * @param {string} [defaultColor] + * @param {string} [overrideColor] + * + * @return {string} + */ + function getFillColor(element, defaultColor, overrideColor) { + var di = getDi(element); + + return overrideColor || di.get('color:background-color') || di.get('bioc:fill') || defaultColor || white; + } + + /** + * @param {Element} element + * @param {string} [defaultColor] + * @param {string} [overrideColor] + * + * @return {string} + */ + function getStrokeColor(element, defaultColor, overrideColor) { + var di = getDi(element); + + return overrideColor || di.get('color:border-color') || di.get('bioc:stroke') || defaultColor || black; + } + + /** + * @param {Element} element + * @param {string} [defaultColor] + * @param {string} [defaultStrokeColor] + * @param {string} [overrideColor] + * + * @return {string} + */ + function getLabelColor(element, defaultColor, defaultStrokeColor, overrideColor) { + var di = getDi(element), + label = di.get('label'); + + return overrideColor || (label && label.get('color:color')) || defaultColor || + getStrokeColor(element, defaultStrokeColor); + } + + // cropping path customizations ////////////////////// + + /** + * @param {ShapeLike} shape + * + * @return {string} path + */ + function getCirclePath(shape) { + + var cx = shape.x + shape.width / 2, + cy = shape.y + shape.height / 2, + radius = shape.width / 2; + + var circlePath = [ + [ 'M', cx, cy ], + [ 'm', 0, -radius ], + [ 'a', radius, radius, 0, 1, 1, 0, 2 * radius ], + [ 'a', radius, radius, 0, 1, 1, 0, -2 * radius ], + [ 'z' ] + ]; + + return componentsToPath(circlePath); + } + + /** + * @param {ShapeLike} shape + * @param {number} [borderRadius] + * + * @return {string} path + */ + function getRoundRectPath(shape, borderRadius) { + + var x = shape.x, + y = shape.y, + width = shape.width, + height = shape.height; + + var roundRectPath = [ + [ 'M', x + borderRadius, y ], + [ 'l', width - borderRadius * 2, 0 ], + [ 'a', borderRadius, borderRadius, 0, 0, 1, borderRadius, borderRadius ], + [ 'l', 0, height - borderRadius * 2 ], + [ 'a', borderRadius, borderRadius, 0, 0, 1, -borderRadius, borderRadius ], + [ 'l', borderRadius * 2 - width, 0 ], + [ 'a', borderRadius, borderRadius, 0, 0, 1, -borderRadius, -borderRadius ], + [ 'l', 0, borderRadius * 2 - height ], + [ 'a', borderRadius, borderRadius, 0, 0, 1, borderRadius, -borderRadius ], + [ 'z' ] + ]; + + return componentsToPath(roundRectPath); + } + + /** + * @param {ShapeLike} shape + * + * @return {string} path + */ + function getDiamondPath(shape) { + + var width = shape.width, + height = shape.height, + x = shape.x, + y = shape.y, + halfWidth = width / 2, + halfHeight = height / 2; + + var diamondPath = [ + [ 'M', x + halfWidth, y ], + [ 'l', halfWidth, halfHeight ], + [ 'l', -halfWidth, halfHeight ], + [ 'l', -halfWidth, -halfHeight ], + [ 'z' ] + ]; + + return componentsToPath(diamondPath); + } + + /** + * @param {ShapeLike} shape + * + * @return {string} path + */ + function getRectPath(shape) { + var x = shape.x, + y = shape.y, + width = shape.width, + height = shape.height; + + var rectPath = [ + [ 'M', x, y ], + [ 'l', width, 0 ], + [ 'l', 0, height ], + [ 'l', -width, 0 ], + [ 'z' ] + ]; + + return componentsToPath(rectPath); + } + + /** + * Get width and height from element or overrides. + * + * @param {Dimensions|Rect|ShapeLike} bounds + * @param {Object} overrides + * + * @returns {Dimensions} + */ + function getBounds(bounds, overrides = {}) { + return { + width: getWidth(bounds, overrides), + height: getHeight(bounds, overrides) + }; + } + + /** + * Get width from element or overrides. + * + * @param {Dimensions|Rect|ShapeLike} bounds + * @param {Object} overrides + * + * @returns {number} + */ + function getWidth(bounds, overrides = {}) { + return has$1(overrides, 'width') ? overrides.width : bounds.width; + } + + /** + * Get height from element or overrides. + * + * @param {Dimensions|Rect|ShapeLike} bounds + * @param {Object} overrides + * + * @returns {number} + */ + function getHeight(bounds, overrides = {}) { + return has$1(overrides, 'height') ? overrides.height : bounds.height; + } + + function _mergeNamespaces$1(n, m) { + m.forEach(function (e) { + e && typeof e !== 'string' && !Array.isArray(e) && Object.keys(e).forEach(function (k) { + if (k !== 'default' && !(k in n)) { + var d = Object.getOwnPropertyDescriptor(e, k); + Object.defineProperty(n, k, d.get ? d : { + enumerable: true, + get: function () { return e[k]; } + }); + } + }); + }); + return Object.freeze(n); + } + + /** + * Flatten array, one level deep. + * + * @param {Array} arr + * + * @return {Array} + */ + + const nativeToString = Object.prototype.toString; + const nativeHasOwnProperty = Object.prototype.hasOwnProperty; + + function isUndefined$1(obj) { + return obj === undefined; + } + + function isArray$1(obj) { + return nativeToString.call(obj) === '[object Array]'; + } + + /** + * Return true, if target owns a property with the given key. + * + * @param {Object} target + * @param {String} key + * + * @return {Boolean} + */ + function has(target, key) { + return nativeHasOwnProperty.call(target, key); + } + + + /** + * Iterate over collection; returning something + * (non-undefined) will stop iteration. + * + * @param {Array|Object} collection + * @param {Function} iterator + * + * @return {Object} return result that stopped the iteration + */ + function forEach(collection, iterator) { + + let val, + result; + + if (isUndefined$1(collection)) { + return; + } + + const convertKey = isArray$1(collection) ? toNum : identity; + + for (let key in collection) { + + if (has(collection, key)) { + val = collection[key]; + + result = iterator(val, convertKey(key)); + + if (result === false) { + return val; + } + } + } + } + + + function identity(arg) { + return arg; + } + + function toNum(arg) { + return Number(arg); + } + + /** + * Assigns style attributes in a style-src compliant way. + * + * @param {Element} element + * @param {...Object} styleSources + * + * @return {Element} the element + */ + function assign(element, ...styleSources) { + const target = element.style; + + forEach(styleSources, function(style) { + if (!style) { + return; + } + + forEach(style, function(value, key) { + target[key] = value; + }); + }); + + return element; + } + + /** + * Set attribute `name` to `val`, or get attr `name`. + * + * @param {Element} el + * @param {String} name + * @param {String} [val] + * @api public + */ + function attr(el, name, val) { + + // get + if (arguments.length == 2) { + return el.getAttribute(name); + } + + // remove + if (val === null) { + return el.removeAttribute(name); + } + + // set + el.setAttribute(name, val); + + return el; + } + + /** + * Taken from https://github.com/component/classes + * + * Without the component bits. + */ + + /** + * toString reference. + */ + + const toString = Object.prototype.toString; + + /** + * Wrap `el` in a `ClassList`. + * + * @param {Element} el + * @return {ClassList} + * @api public + */ + + function classes(el) { + return new ClassList(el); + } + + /** + * Initialize a new ClassList for `el`. + * + * @param {Element} el + * @api private + */ + + function ClassList(el) { + if (!el || !el.nodeType) { + throw new Error('A DOM element reference is required'); + } + this.el = el; + this.list = el.classList; + } + + /** + * Add class `name` if not already present. + * + * @param {String} name + * @return {ClassList} + * @api public + */ + + ClassList.prototype.add = function(name) { + this.list.add(name); + return this; + }; + + /** + * Remove class `name` when present, or + * pass a regular expression to remove + * any which match. + * + * @param {String|RegExp} name + * @return {ClassList} + * @api public + */ + + ClassList.prototype.remove = function(name) { + if ('[object RegExp]' == toString.call(name)) { + return this.removeMatching(name); + } + + this.list.remove(name); + return this; + }; + + /** + * Remove all classes matching `re`. + * + * @param {RegExp} re + * @return {ClassList} + * @api private + */ + + ClassList.prototype.removeMatching = function(re) { + const arr = this.array(); + for (let i = 0; i < arr.length; i++) { + if (re.test(arr[i])) { + this.remove(arr[i]); + } + } + return this; + }; + + /** + * Toggle class `name`, can force state via `force`. + * + * For browsers that support classList, but do not support `force` yet, + * the mistake will be detected and corrected. + * + * @param {String} name + * @param {Boolean} force + * @return {ClassList} + * @api public + */ + + ClassList.prototype.toggle = function(name, force) { + if ('undefined' !== typeof force) { + if (force !== this.list.toggle(name, force)) { + this.list.toggle(name); // toggle again to correct + } + } else { + this.list.toggle(name); + } + return this; + }; + + /** + * Return an array of classes. + * + * @return {Array} + * @api public + */ + + ClassList.prototype.array = function() { + return Array.from(this.list); + }; + + /** + * Check if class `name` is present. + * + * @param {String} name + * @return {ClassList} + * @api public + */ + + ClassList.prototype.has = + ClassList.prototype.contains = function(name) { + return this.list.contains(name); + }; + + /** + * Remove all children from the given element. + */ + function clear(el) { + + var c; + + while (el.childNodes.length) { + c = el.childNodes[0]; + el.removeChild(c); + } + + return el; + } + + /** + * @param { HTMLElement } element + * @param { String } selector + * + * @return { boolean } + */ + function matches(element, selector) { + return element && typeof element.matches === 'function' && element.matches(selector); + } + + /** + * Closest + * + * @param {Element} el + * @param {String} selector + * @param {Boolean} checkYourSelf (optional) + */ + function closest(element, selector, checkYourSelf) { + var currentElem = checkYourSelf ? element : element.parentNode; + + while (currentElem && currentElem.nodeType !== document.DOCUMENT_NODE && + currentElem.nodeType !== document.DOCUMENT_FRAGMENT_NODE) { + + if (matches(currentElem, selector)) { + return currentElem; + } + + currentElem = currentElem.parentNode; + } + + return matches(currentElem, selector) ? currentElem : null; + } + + var componentEvent = {}; + + var bind$1, unbind$1, prefix$6; + + function detect () { + bind$1 = window.addEventListener ? 'addEventListener' : 'attachEvent'; + unbind$1 = window.removeEventListener ? 'removeEventListener' : 'detachEvent'; + prefix$6 = bind$1 !== 'addEventListener' ? 'on' : ''; + } + + /** + * Bind `el` event `type` to `fn`. + * + * @param {Element} el + * @param {String} type + * @param {Function} fn + * @param {Boolean} capture + * @return {Function} + * @api public + */ + + var bind_1 = componentEvent.bind = function(el, type, fn, capture){ + if (!bind$1) detect(); + el[bind$1](prefix$6 + type, fn, capture || false); + return fn; + }; + + /** + * Unbind `el` event `type`'s callback `fn`. + * + * @param {Element} el + * @param {String} type + * @param {Function} fn + * @param {Boolean} capture + * @return {Function} + * @api public + */ + + var unbind_1 = componentEvent.unbind = function(el, type, fn, capture){ + if (!unbind$1) detect(); + el[unbind$1](prefix$6 + type, fn, capture || false); + return fn; + }; + + var event = /*#__PURE__*/_mergeNamespaces$1({ + __proto__: null, + bind: bind_1, + unbind: unbind_1, + 'default': componentEvent + }, [componentEvent]); + + /** + * Module dependencies. + */ + + /** + * Delegate event `type` to `selector` + * and invoke `fn(e)`. A callback function + * is returned which may be passed to `.unbind()`. + * + * @param {Element} el + * @param {String} selector + * @param {String} type + * @param {Function} fn + * @param {Boolean} capture + * @return {Function} + * @api public + */ + + // Some events don't bubble, so we want to bind to the capture phase instead + // when delegating. + var forceCaptureEvents = [ 'focus', 'blur' ]; + + function bind(el, selector, type, fn, capture) { + if (forceCaptureEvents.indexOf(type) !== -1) { + capture = true; + } + + return event.bind(el, type, function(e) { + var target = e.target || e.srcElement; + e.delegateTarget = closest(target, selector, true); + if (e.delegateTarget) { + fn.call(el, e); + } + }, capture); + } + + /** + * Unbind event `type`'s callback `fn`. + * + * @param {Element} el + * @param {String} type + * @param {Function} fn + * @param {Boolean} capture + * @api public + */ + function unbind(el, type, fn, capture) { + if (forceCaptureEvents.indexOf(type) !== -1) { + capture = true; + } + + return event.unbind(el, type, fn, capture); + } + + var delegate = { + bind, + unbind + }; + + /** + * Expose `parse`. + */ + + var domify = parse; + + /** + * Tests for browser support. + */ + + var innerHTMLBug = false; + var bugTestDiv; + if (typeof document !== 'undefined') { + bugTestDiv = document.createElement('div'); + // Setup + bugTestDiv.innerHTML = '
a'; + // Make sure that link elements get serialized correctly by innerHTML + // This requires a wrapper element in IE + innerHTMLBug = !bugTestDiv.getElementsByTagName('link').length; + bugTestDiv = undefined; + } + + /** + * Wrap map from jquery. + */ + + var map = { + legend: [1, '
', '
'], + tr: [2, '', '
'], + col: [2, '', '
'], + // for script/link/style tags to work in IE6-8, you have to wrap + // in a div with a non-whitespace character in front, ha! + _default: innerHTMLBug ? [1, 'X
', '
'] : [0, '', ''] + }; + + map.td = + map.th = [3, '', '
']; + + map.option = + map.optgroup = [1, '']; + + map.thead = + map.tbody = + map.colgroup = + map.caption = + map.tfoot = [1, '', '
']; + + map.polyline = + map.ellipse = + map.polygon = + map.circle = + map.text = + map.line = + map.path = + map.rect = + map.g = [1, '','']; + + /** + * Parse `html` and return a DOM Node instance, which could be a TextNode, + * HTML DOM Node of some kind (
for example), or a DocumentFragment + * instance, depending on the contents of the `html` string. + * + * @param {String} html - HTML string to "domify" + * @param {Document} doc - The `document` instance to create the Node for + * @return {DOMNode} the TextNode, DOM Node, or DocumentFragment instance + * @api private + */ + + function parse(html, doc) { + if ('string' != typeof html) throw new TypeError('String expected'); + + // default to the global `document` object + if (!doc) doc = document; + + // tag name + var m = /<([\w:]+)/.exec(html); + if (!m) return doc.createTextNode(html); + + html = html.replace(/^\s+|\s+$/g, ''); // Remove leading/trailing whitespace + + var tag = m[1]; + + // body support + if (tag == 'body') { + var el = doc.createElement('html'); + el.innerHTML = html; + return el.removeChild(el.lastChild); + } + + // wrap map + var wrap = Object.prototype.hasOwnProperty.call(map, tag) ? map[tag] : map._default; + var depth = wrap[0]; + var prefix = wrap[1]; + var suffix = wrap[2]; + var el = doc.createElement('div'); + el.innerHTML = prefix + html + suffix; + while (depth--) el = el.lastChild; + + // one element + if (el.firstChild == el.lastChild) { + return el.removeChild(el.firstChild); + } + + // several elements + var fragment = doc.createDocumentFragment(); + while (el.firstChild) { + fragment.appendChild(el.removeChild(el.firstChild)); + } + + return fragment; + } + + var domify$1 = domify; + + function query(selector, el) { + el = el || document; + + return el.querySelector(selector); + } + + function all(selector, el) { + el = el || document; + + return el.querySelectorAll(selector); + } + + function remove$1(el) { + el.parentNode && el.parentNode.removeChild(el); + } + + /** + * @param {SVGElement} gfx + * @param {number} x + * @param {number} y + * @param {number} [angle] + * @param {number} [amount] + */ + function transform(gfx, x, y, angle, amount) { + var translate = createTransform(); + translate.setTranslate(x, y); + + var rotate = createTransform(); + rotate.setRotate(angle || 0, 0, 0); + + var scale = createTransform(); + scale.setScale(amount || 1, amount || 1); + + transform$1(gfx, [ translate, rotate, scale ]); + } + + + /** + * @param {SVGElement} gfx + * @param {number} x + * @param {number} y + */ + function translate$1(gfx, x, y) { + var translate = createTransform(); + translate.setTranslate(x, y); + + transform$1(gfx, translate); + } + + + /** + * @param {SVGElement} gfx + * @param {number} angle + */ + function rotate(gfx, angle) { + var rotate = createTransform(); + rotate.setRotate(angle, 0, 0); + + transform$1(gfx, rotate); + } + + function createCommonjsModule(fn, module) { + return module = { exports: {} }, fn(module, module.exports), module.exports; + } + + var hat_1 = createCommonjsModule(function (module) { + var hat = module.exports = function (bits, base) { + if (!base) base = 16; + if (bits === undefined) bits = 128; + if (bits <= 0) return '0'; + + var digits = Math.log(Math.pow(2, bits)) / Math.log(base); + for (var i = 2; digits === Infinity; i *= 2) { + digits = Math.log(Math.pow(2, bits / i)) / Math.log(base) * i; + } + + var rem = digits - Math.floor(digits); + + var res = ''; + + for (var i = 0; i < Math.floor(digits); i++) { + var x = Math.floor(Math.random() * base).toString(base); + res = x + res; + } + + if (rem) { + var b = Math.pow(base, rem); + var x = Math.floor(Math.random() * b).toString(base); + res = x + res; + } + + var parsed = parseInt(res, base); + if (parsed !== Infinity && parsed >= Math.pow(2, bits)) { + return hat(bits, base) + } + else return res; + }; + + hat.rack = function (bits, base, expandBy) { + var fn = function (data) { + var iters = 0; + do { + if (iters ++ > 10) { + if (expandBy) bits += expandBy; + else throw new Error('too many ID collisions, use more bits') + } + + var id = hat(bits, base); + } while (Object.hasOwnProperty.call(hats, id)); + + hats[id] = data; + return id; + }; + var hats = fn.hats = {}; + + fn.get = function (id) { + return fn.hats[id]; + }; + + fn.set = function (id, value) { + fn.hats[id] = value; + return fn; + }; + + fn.bits = bits || 128; + fn.base = base || 16; + return fn; + }; + }); + + /** + * Create a new id generator / cache instance. + * + * You may optionally provide a seed that is used internally. + * + * @param {Seed} seed + */ + function Ids(seed) { + if (!(this instanceof Ids)) { + return new Ids(seed); + } + seed = seed || [128, 36, 1]; + this._seed = seed.length ? hat_1.rack(seed[0], seed[1], seed[2]) : seed; + } + + /** + * Generate a next id. + * + * @param {Object} [element] element to bind the id to + * + * @return {String} id + */ + Ids.prototype.next = function (element) { + return this._seed(element || true); + }; + + /** + * Generate a next id with a given prefix. + * + * @param {Object} [element] element to bind the id to + * + * @return {String} id + */ + Ids.prototype.nextPrefixed = function (prefix, element) { + var id; + do { + id = prefix + this.next(true); + } while (this.assigned(id)); + + // claim {prefix}{random} + this.claim(id, element); + + // return + return id; + }; + + /** + * Manually claim an existing id. + * + * @param {String} id + * @param {String} [element] element the id is claimed by + */ + Ids.prototype.claim = function (id, element) { + this._seed.set(id, element || true); + }; + + /** + * Returns true if the given id has already been assigned. + * + * @param {String} id + * @return {Boolean} + */ + Ids.prototype.assigned = function (id) { + return this._seed.get(id) || false; + }; + + /** + * Unclaim an id. + * + * @param {String} id the id to unclaim + */ + Ids.prototype.unclaim = function (id) { + delete this._seed.hats[id]; + }; + + /** + * Clear all claimed ids. + */ + Ids.prototype.clear = function () { + var hats = this._seed.hats, + id; + for (id in hats) { + this.unclaim(id); + } + }; + + var rendererIds = new Ids(); + + var ELEMENT_LABEL_DISTANCE = 10, + INNER_OUTER_DIST = 3, + PARTICIPANT_STROKE_WIDTH = 1.5, + TASK_BORDER_RADIUS = 10; + + var DEFAULT_OPACITY = 0.95, + FULL_OPACITY = 1, + LOW_OPACITY = 0.25; + + /** + * @typedef { Partial<{ + * defaultFillColor: string, + * defaultStrokeColor: string, + * defaultLabelColor: string + * }> } BpmnRendererConfig + * + * @typedef { Partial<{ + * fill: string, + * stroke: string, + * width: string, + * height: string + * }> } Attrs + */ + + /** + * @typedef { import('../model/Types').Element } Element + */ + + /** + * A renderer for BPMN elements + * + * @param {BpmnRendererConfig} config + * @param {import('diagram-js/lib/core/EventBus').default} eventBus + * @param {import('diagram-js/lib/draw/Styles').default} styles + * @param {import('./PathMap').default} pathMap + * @param {import('diagram-js/lib/core/Canvas').default} canvas + * @param {import('./TextRenderer').default} textRenderer + * @param {number} [priority] + */ + function BpmnRenderer( + config, eventBus, styles, pathMap, + canvas, textRenderer, priority) { + + BaseRenderer.call(this, eventBus, priority); + + var defaultFillColor = config && config.defaultFillColor, + defaultStrokeColor = config && config.defaultStrokeColor, + defaultLabelColor = config && config.defaultLabelColor; + + var rendererId = rendererIds.next(); + + var markers = {}; + + function shapeStyle(attrs) { + return styles.computeStyle(attrs, { + strokeLinecap: 'round', + strokeLinejoin: 'round', + stroke: black, + strokeWidth: 2, + fill: 'white' + }); + } + + function lineStyle(attrs) { + return styles.computeStyle(attrs, [ 'no-fill' ], { + strokeLinecap: 'round', + strokeLinejoin: 'round', + stroke: black, + strokeWidth: 2 + }); + } + + function addMarker(id, options) { + var { + ref = { x: 0, y: 0 }, + scale = 1, + element + } = options; + + var marker = create$1('marker', { + id: id, + viewBox: '0 0 20 20', + refX: ref.x, + refY: ref.y, + markerWidth: 20 * scale, + markerHeight: 20 * scale, + orient: 'auto' + }); + + append(marker, element); + + var defs = query('defs', canvas._svg); + + if (!defs) { + defs = create$1('defs'); + + append(canvas._svg, defs); + } + + append(defs, marker); + + markers[id] = marker; + } + + function colorEscape(str) { + + // only allow characters and numbers + return str.replace(/[^0-9a-zA-Z]+/g, '_'); + } + + function marker(type, fill, stroke) { + var id = type + '-' + colorEscape(fill) + '-' + colorEscape(stroke) + '-' + rendererId; + + if (!markers[id]) { + createMarker(id, type, fill, stroke); + } + + return 'url(#' + id + ')'; + } + + function createMarker(id, type, fill, stroke) { + + if (type === 'sequenceflow-end') { + var sequenceflowEnd = create$1('path', { + d: 'M 1 5 L 11 10 L 1 15 Z', + ...shapeStyle({ + fill: stroke, + stroke: stroke, + strokeWidth: 1 + }) + }); + + addMarker(id, { + element: sequenceflowEnd, + ref: { x: 11, y: 10 }, + scale: 0.5 + }); + } + + if (type === 'messageflow-start') { + var messageflowStart = create$1('circle', { + cx: 6, + cy: 6, + r: 3.5, + ...shapeStyle({ + fill, + stroke: stroke, + strokeWidth: 1, + + // fix for safari / chrome / firefox bug not correctly + // resetting stroke dash array + strokeDasharray: [ 10000, 1 ] + }) + }); + + addMarker(id, { + element: messageflowStart, + ref: { x: 6, y: 6 } + }); + } + + if (type === 'messageflow-end') { + var messageflowEnd = create$1('path', { + d: 'm 1 5 l 0 -3 l 7 3 l -7 3 z', + ...shapeStyle({ + fill, + stroke: stroke, + strokeWidth: 1, + + // fix for safari / chrome / firefox bug not correctly + // resetting stroke dash array + strokeDasharray: [ 10000, 1 ] + }) + }); + + addMarker(id, { + element: messageflowEnd, + ref: { x: 8.5, y: 5 } + }); + } + + if (type === 'association-start') { + var associationStart = create$1('path', { + d: 'M 11 5 L 1 10 L 11 15', + ...lineStyle({ + fill: 'none', + stroke, + strokeWidth: 1.5, + + // fix for safari / chrome / firefox bug not correctly + // resetting stroke dash array + strokeDasharray: [ 10000, 1 ] + }) + }); + + addMarker(id, { + element: associationStart, + ref: { x: 1, y: 10 }, + scale: 0.5 + }); + } + + if (type === 'association-end') { + var associationEnd = create$1('path', { + d: 'M 1 5 L 11 10 L 1 15', + ...lineStyle({ + fill: 'none', + stroke, + strokeWidth: 1.5, + + // fix for safari / chrome / firefox bug not correctly + // resetting stroke dash array + strokeDasharray: [ 10000, 1 ] + }) + }); + + addMarker(id, { + element: associationEnd, + ref: { x: 11, y: 10 }, + scale: 0.5 + }); + } + + if (type === 'conditional-flow-marker') { + var conditionalFlowMarker = create$1('path', { + d: 'M 0 10 L 8 6 L 16 10 L 8 14 Z', + ...shapeStyle({ + fill, + stroke: stroke + }) + }); + + addMarker(id, { + element: conditionalFlowMarker, + ref: { x: -1, y: 10 }, + scale: 0.5 + }); + } + + if (type === 'conditional-default-flow-marker') { + var defaultFlowMarker = create$1('path', { + d: 'M 6 4 L 10 16', + ...shapeStyle({ + stroke: stroke + }) + }); + + addMarker(id, { + element: defaultFlowMarker, + ref: { x: 0, y: 10 }, + scale: 0.5 + }); + } + } + + function drawCircle(parentGfx, width, height, offset, attrs = {}) { + + if (isObject(offset)) { + attrs = offset; + offset = 0; + } + + offset = offset || 0; + + attrs = shapeStyle(attrs); + + var cx = width / 2, + cy = height / 2; + + var circle = create$1('circle', { + cx: cx, + cy: cy, + r: Math.round((width + height) / 4 - offset), + ...attrs + }); + + append(parentGfx, circle); + + return circle; + } + + function drawRect(parentGfx, width, height, r, offset, attrs) { + + if (isObject(offset)) { + attrs = offset; + offset = 0; + } + + offset = offset || 0; + + attrs = shapeStyle(attrs); + + var rect = create$1('rect', { + x: offset, + y: offset, + width: width - offset * 2, + height: height - offset * 2, + rx: r, + ry: r, + ...attrs + }); + + append(parentGfx, rect); + + return rect; + } + + function drawDiamond(parentGfx, width, height, attrs) { + + var x_2 = width / 2; + var y_2 = height / 2; + + var points = [ + { x: x_2, y: 0 }, + { x: width, y: y_2 }, + { x: x_2, y: height }, + { x: 0, y: y_2 } + ]; + + var pointsString = points.map(function(point) { + return point.x + ',' + point.y; + }).join(' '); + + attrs = shapeStyle(attrs); + + var polygon = create$1('polygon', { + ...attrs, + points: pointsString + }); + + append(parentGfx, polygon); + + return polygon; + } + + /** + * @param {SVGElement} parentGfx + * @param {Point[]} waypoints + * @param {any} attrs + * @param {number} [radius] + * + * @return {SVGElement} + */ + function drawLine(parentGfx, waypoints, attrs, radius) { + attrs = lineStyle(attrs); + + var line = createLine(waypoints, attrs, radius); + + append(parentGfx, line); + + return line; + } + + /** + * @param {SVGElement} parentGfx + * @param {Point[]} waypoints + * @param {any} attrs + * + * @return {SVGElement} + */ + function drawConnectionSegments(parentGfx, waypoints, attrs) { + return drawLine(parentGfx, waypoints, attrs, 5); + } + + function drawPath(parentGfx, d, attrs) { + attrs = lineStyle(attrs); + + var path = create$1('path', { + ...attrs, + d + }); + + append(parentGfx, path); + + return path; + } + + function drawMarker(type, parentGfx, path, attrs) { + return drawPath(parentGfx, path, assign$1({ 'data-marker': type }, attrs)); + } + + function renderer(type) { + return handlers[type]; + } + + function as(type) { + return function(parentGfx, element, attrs) { + return renderer(type)(parentGfx, element, attrs); + }; + } + + var eventIconRenderers = { + 'bpmn:MessageEventDefinition': function(parentGfx, element, attrs = {}, isThrowing) { + var pathData = pathMap.getScaledPath('EVENT_MESSAGE', { + xScaleFactor: 0.9, + yScaleFactor: 0.9, + containerWidth: element.width, + containerHeight: element.height, + position: { + mx: 0.235, + my: 0.315 + } + }); + + var fill = isThrowing + ? getStrokeColor(element, defaultStrokeColor, attrs.stroke) + : getFillColor(element, defaultFillColor, attrs.fill); + + var stroke = isThrowing + ? getFillColor(element, defaultFillColor, attrs.fill) + : getStrokeColor(element, defaultStrokeColor, attrs.stroke); + + var messagePath = drawPath(parentGfx, pathData, { + fill, + stroke, + strokeWidth: 1 + }); + + return messagePath; + }, + 'bpmn:TimerEventDefinition': function(parentGfx, element, attrs = {}) { + var circle = drawCircle(parentGfx, element.width, element.height, 0.2 * element.height, { + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 2 + }); + + var pathData = pathMap.getScaledPath('EVENT_TIMER_WH', { + xScaleFactor: 0.75, + yScaleFactor: 0.75, + containerWidth: element.width, + containerHeight: element.height, + position: { + mx: 0.5, + my: 0.5 + } + }); + + drawPath(parentGfx, pathData, { + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 2 + }); + + for (var i = 0; i < 12; i++) { + var linePathData = pathMap.getScaledPath('EVENT_TIMER_LINE', { + xScaleFactor: 0.75, + yScaleFactor: 0.75, + containerWidth: element.width, + containerHeight: element.height, + position: { + mx: 0.5, + my: 0.5 + } + }); + + var width = element.width / 2, + height = element.height / 2; + + drawPath(parentGfx, linePathData, { + strokeWidth: 1, + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + transform: 'rotate(' + (i * 30) + ',' + height + ',' + width + ')' + }); + } + + return circle; + }, + 'bpmn:EscalationEventDefinition': function(parentGfx, event, attrs = {}, isThrowing) { + var pathData = pathMap.getScaledPath('EVENT_ESCALATION', { + xScaleFactor: 1, + yScaleFactor: 1, + containerWidth: event.width, + containerHeight: event.height, + position: { + mx: 0.5, + my: 0.2 + } + }); + + var fill = isThrowing + ? getStrokeColor(event, defaultStrokeColor, attrs.stroke) + : getFillColor(event, defaultFillColor, attrs.fill); + + return drawPath(parentGfx, pathData, { + fill, + stroke: getStrokeColor(event, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 + }); + }, + 'bpmn:ConditionalEventDefinition': function(parentGfx, event, attrs = {}) { + var pathData = pathMap.getScaledPath('EVENT_CONDITIONAL', { + xScaleFactor: 1, + yScaleFactor: 1, + containerWidth: event.width, + containerHeight: event.height, + position: { + mx: 0.5, + my: 0.222 + } + }); + + return drawPath(parentGfx, pathData, { + fill: getFillColor(event, defaultFillColor, attrs.fill), + stroke: getStrokeColor(event, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 + }); + }, + 'bpmn:LinkEventDefinition': function(parentGfx, event, attrs = {}, isThrowing) { + var pathData = pathMap.getScaledPath('EVENT_LINK', { + xScaleFactor: 1, + yScaleFactor: 1, + containerWidth: event.width, + containerHeight: event.height, + position: { + mx: 0.57, + my: 0.263 + } + }); + + var fill = isThrowing + ? getStrokeColor(event, defaultStrokeColor, attrs.stroke) + : getFillColor(event, defaultFillColor, attrs.fill); + + return drawPath(parentGfx, pathData, { + fill, + stroke: getStrokeColor(event, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 + }); + }, + 'bpmn:ErrorEventDefinition': function(parentGfx, event, attrs = {}, isThrowing) { + var pathData = pathMap.getScaledPath('EVENT_ERROR', { + xScaleFactor: 1.1, + yScaleFactor: 1.1, + containerWidth: event.width, + containerHeight: event.height, + position: { + mx: 0.2, + my: 0.722 + } + }); + + var fill = isThrowing + ? getStrokeColor(event, defaultStrokeColor, attrs.stroke) + : getFillColor(event, defaultFillColor, attrs.fill); + + return drawPath(parentGfx, pathData, { + fill, + stroke: getStrokeColor(event, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 + }); + }, + 'bpmn:CancelEventDefinition': function(parentGfx, event, attrs = {}, isThrowing) { + var pathData = pathMap.getScaledPath('EVENT_CANCEL_45', { + xScaleFactor: 1.0, + yScaleFactor: 1.0, + containerWidth: event.width, + containerHeight: event.height, + position: { + mx: 0.638, + my: -0.055 + } + }); + + var fill = isThrowing ? getStrokeColor(event, defaultStrokeColor, attrs.stroke) : 'none'; + + var path = drawPath(parentGfx, pathData, { + fill, + stroke: getStrokeColor(event, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 + }); + + rotate(path, 45); + + return path; + }, + 'bpmn:CompensateEventDefinition': function(parentGfx, event, attrs = {}, isThrowing) { + var pathData = pathMap.getScaledPath('EVENT_COMPENSATION', { + xScaleFactor: 1, + yScaleFactor: 1, + containerWidth: event.width, + containerHeight: event.height, + position: { + mx: 0.22, + my: 0.5 + } + }); + + var fill = isThrowing + ? getStrokeColor(event, defaultStrokeColor, attrs.stroke) + : getFillColor(event, defaultFillColor, attrs.fill); + + return drawPath(parentGfx, pathData, { + fill, + stroke: getStrokeColor(event, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 + }); + }, + 'bpmn:SignalEventDefinition': function(parentGfx, event, attrs = {}, isThrowing) { + var pathData = pathMap.getScaledPath('EVENT_SIGNAL', { + xScaleFactor: 0.9, + yScaleFactor: 0.9, + containerWidth: event.width, + containerHeight: event.height, + position: { + mx: 0.5, + my: 0.2 + } + }); + + var fill = isThrowing + ? getStrokeColor(event, defaultStrokeColor, attrs.stroke) + : getFillColor(event, defaultFillColor, attrs.fill); + + return drawPath(parentGfx, pathData, { + strokeWidth: 1, + fill, + stroke: getStrokeColor(event, defaultStrokeColor, attrs.stroke) + }); + }, + 'bpmn:MultipleEventDefinition': function(parentGfx, event, attrs = {}, isThrowing) { + var pathData = pathMap.getScaledPath('EVENT_MULTIPLE', { + xScaleFactor: 1.1, + yScaleFactor: 1.1, + containerWidth: event.width, + containerHeight: event.height, + position: { + mx: 0.222, + my: 0.36 + } + }); + + var fill = isThrowing + ? getStrokeColor(event, defaultStrokeColor, attrs.stroke) + : getFillColor(event, defaultFillColor, attrs.fill); + + return drawPath(parentGfx, pathData, { + fill, + strokeWidth: 1 + }); + }, + 'bpmn:ParallelMultipleEventDefinition': function(parentGfx, event, attrs = {}) { + var pathData = pathMap.getScaledPath('EVENT_PARALLEL_MULTIPLE', { + xScaleFactor: 1.2, + yScaleFactor: 1.2, + containerWidth: event.width, + containerHeight: event.height, + position: { + mx: 0.458, + my: 0.194 + } + }); + + return drawPath(parentGfx, pathData, { + fill: getFillColor(event, defaultFillColor, attrs.fill), + stroke: getStrokeColor(event, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 + }); + }, + 'bpmn:TerminateEventDefinition': function(parentGfx, element, attrs = {}) { + var circle = drawCircle(parentGfx, element.width, element.height, 8, { + fill: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 4 + }); + + return circle; + } + }; + + function renderEventIcon(element, parentGfx, attrs = {}) { + var semantic = getBusinessObject(element), + isThrowing = isThrowEvent(semantic); + + if (semantic.get('eventDefinitions') && semantic.get('eventDefinitions').length > 1) { + if (semantic.get('parallelMultiple')) { + return eventIconRenderers[ 'bpmn:ParallelMultipleEventDefinition' ](parentGfx, element, attrs, isThrowing); + } + else { + return eventIconRenderers[ 'bpmn:MultipleEventDefinition' ](parentGfx, element, attrs, isThrowing); + } + } + + if (isTypedEvent(semantic, 'bpmn:MessageEventDefinition')) { + return eventIconRenderers[ 'bpmn:MessageEventDefinition' ](parentGfx, element, attrs, isThrowing); + } + + if (isTypedEvent(semantic, 'bpmn:TimerEventDefinition')) { + return eventIconRenderers[ 'bpmn:TimerEventDefinition' ](parentGfx, element, attrs, isThrowing); + } + + if (isTypedEvent(semantic, 'bpmn:ConditionalEventDefinition')) { + return eventIconRenderers[ 'bpmn:ConditionalEventDefinition' ](parentGfx, element, attrs, isThrowing); + } + + if (isTypedEvent(semantic, 'bpmn:SignalEventDefinition')) { + return eventIconRenderers[ 'bpmn:SignalEventDefinition' ](parentGfx, element, attrs, isThrowing); + } + + if (isTypedEvent(semantic, 'bpmn:EscalationEventDefinition')) { + return eventIconRenderers[ 'bpmn:EscalationEventDefinition' ](parentGfx, element, attrs, isThrowing); + } + + if (isTypedEvent(semantic, 'bpmn:LinkEventDefinition')) { + return eventIconRenderers[ 'bpmn:LinkEventDefinition' ](parentGfx, element, attrs, isThrowing); + } + + if (isTypedEvent(semantic, 'bpmn:ErrorEventDefinition')) { + return eventIconRenderers[ 'bpmn:ErrorEventDefinition' ](parentGfx, element, attrs, isThrowing); + } + + if (isTypedEvent(semantic, 'bpmn:CancelEventDefinition')) { + return eventIconRenderers[ 'bpmn:CancelEventDefinition' ](parentGfx, element, attrs, isThrowing); + } + + if (isTypedEvent(semantic, 'bpmn:CompensateEventDefinition')) { + return eventIconRenderers[ 'bpmn:CompensateEventDefinition' ](parentGfx, element, attrs, isThrowing); + } + + if (isTypedEvent(semantic, 'bpmn:TerminateEventDefinition')) { + return eventIconRenderers[ 'bpmn:TerminateEventDefinition' ](parentGfx, element, attrs, isThrowing); + } + + return null; + } + + var taskMarkerRenderers = { + 'ParticipantMultiplicityMarker': function(parentGfx, element, attrs = {}) { + var width = getWidth(element, attrs), + height = getHeight(element, attrs); + + var markerPath = pathMap.getScaledPath('MARKER_PARALLEL', { + xScaleFactor: 1, + yScaleFactor: 1, + containerWidth: width, + containerHeight: height, + position: { + mx: ((width / 2 - 6) / width), + my: (height - 15) / height + } + }); + + drawMarker('participant-multiplicity', parentGfx, markerPath, { + strokeWidth: 2, + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke) + }); + }, + 'SubProcessMarker': function(parentGfx, element, attrs = {}) { + var markerRect = drawRect(parentGfx, 14, 14, 0, { + strokeWidth: 1, + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke) + }); + + translate$1(markerRect, element.width / 2 - 7.5, element.height - 20); + + var markerPath = pathMap.getScaledPath('MARKER_SUB_PROCESS', { + xScaleFactor: 1.5, + yScaleFactor: 1.5, + containerWidth: element.width, + containerHeight: element.height, + position: { + mx: (element.width / 2 - 7.5) / element.width, + my: (element.height - 20) / element.height + } + }); + + drawMarker('sub-process', parentGfx, markerPath, { + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke) + }); + }, + 'ParallelMarker': function(parentGfx, element, attrs) { + var width = getWidth(element, attrs), + height = getHeight(element, attrs); + + var markerPath = pathMap.getScaledPath('MARKER_PARALLEL', { + xScaleFactor: 1, + yScaleFactor: 1, + containerWidth: width, + containerHeight: height, + position: { + mx: ((width / 2 + attrs.parallel) / width), + my: (height - 20) / height + } + }); + + drawMarker('parallel', parentGfx, markerPath, { + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke) + }); + }, + 'SequentialMarker': function(parentGfx, element, attrs) { + var markerPath = pathMap.getScaledPath('MARKER_SEQUENTIAL', { + xScaleFactor: 1, + yScaleFactor: 1, + containerWidth: element.width, + containerHeight: element.height, + position: { + mx: ((element.width / 2 + attrs.seq) / element.width), + my: (element.height - 19) / element.height + } + }); + + drawMarker('sequential', parentGfx, markerPath, { + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke) + }); + }, + 'CompensationMarker': function(parentGfx, element, attrs) { + var markerMath = pathMap.getScaledPath('MARKER_COMPENSATION', { + xScaleFactor: 1, + yScaleFactor: 1, + containerWidth: element.width, + containerHeight: element.height, + position: { + mx: ((element.width / 2 + attrs.compensation) / element.width), + my: (element.height - 13) / element.height + } + }); + + drawMarker('compensation', parentGfx, markerMath, { + strokeWidth: 1, + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke) + }); + }, + 'LoopMarker': function(parentGfx, element, attrs) { + var width = getWidth(element, attrs), + height = getHeight(element, attrs); + + var markerPath = pathMap.getScaledPath('MARKER_LOOP', { + xScaleFactor: 1, + yScaleFactor: 1, + containerWidth: width, + containerHeight: height, + position: { + mx: ((width / 2 + attrs.loop) / width), + my: (height - 7) / height + } + }); + + drawMarker('loop', parentGfx, markerPath, { + strokeWidth: 1.5, + fill: 'none', + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeMiterlimit: 0.5 + }); + }, + 'AdhocMarker': function(parentGfx, element, attrs) { + var width = getWidth(element, attrs), + height = getHeight(element, attrs); + + var markerPath = pathMap.getScaledPath('MARKER_ADHOC', { + xScaleFactor: 1, + yScaleFactor: 1, + containerWidth: width, + containerHeight: height, + position: { + mx: ((width / 2 + attrs.adhoc) / width), + my: (height - 15) / height + } + }); + + drawMarker('adhoc', parentGfx, markerPath, { + strokeWidth: 1, + fill: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke) + }); + } + }; + + function renderTaskMarker(type, parentGfx, element, attrs) { + taskMarkerRenderers[ type ](parentGfx, element, attrs); + } + + function renderTaskMarkers(parentGfx, element, taskMarkers, attrs = {}) { + attrs = { + fill: attrs.fill, + stroke: attrs.stroke, + width: getWidth(element, attrs), + height: getHeight(element, attrs) + }; + + var semantic = getBusinessObject(element); + + var subprocess = taskMarkers && taskMarkers.includes('SubProcessMarker'); + + if (subprocess) { + attrs = { + ...attrs, + seq: -21, + parallel: -22, + compensation: -42, + loop: -18, + adhoc: 10 + }; + } else { + attrs = { + ...attrs, + seq: -5, + parallel: -6, + compensation: -27, + loop: 0, + adhoc: 10 + }; + } + + forEach$1(taskMarkers, function(marker) { + renderTaskMarker(marker, parentGfx, element, attrs); + }); + + if (semantic.get('isForCompensation')) { + renderTaskMarker('CompensationMarker', parentGfx, element, attrs); + } + + if (is$1(semantic, 'bpmn:AdHocSubProcess')) { + renderTaskMarker('AdhocMarker', parentGfx, element, attrs); + } + + var loopCharacteristics = semantic.get('loopCharacteristics'), + isSequential = loopCharacteristics && loopCharacteristics.get('isSequential'); + + if (loopCharacteristics) { + + if (isSequential === undefined) { + renderTaskMarker('LoopMarker', parentGfx, element, attrs); + } + + if (isSequential === false) { + renderTaskMarker('ParallelMarker', parentGfx, element, attrs); + } + + if (isSequential === true) { + renderTaskMarker('SequentialMarker', parentGfx, element, attrs); + } + } + } + + function renderLabel(parentGfx, label, attrs = {}) { + attrs = assign$1({ + size: { + width: 100 + } + }, attrs); + + var text = textRenderer.createText(label || '', attrs); + + classes$1(text).add('djs-label'); + + append(parentGfx, text); + + return text; + } + + function renderEmbeddedLabel(parentGfx, element, align, attrs = {}) { + var semantic = getBusinessObject(element); + + var box = getBounds({ + x: element.x, + y: element.y, + width: element.width, + height: element.height + }, attrs); + + return renderLabel(parentGfx, semantic.name, { + align, + box, + padding: 7, + style: { + fill: getLabelColor(element, defaultLabelColor, defaultStrokeColor, attrs.stroke) + } + }); + } + + function renderExternalLabel(parentGfx, element, attrs = {}) { + var box = { + width: 90, + height: 30, + x: element.width / 2 + element.x, + y: element.height / 2 + element.y + }; + + return renderLabel(parentGfx, getLabel(element), { + box: box, + fitBox: true, + style: assign$1( + {}, + textRenderer.getExternalStyle(), + { + fill: getLabelColor(element, defaultLabelColor, defaultStrokeColor, attrs.stroke) + } + ) + }); + } + + function renderLaneLabel(parentGfx, text, element, attrs = {}) { + var isHorizontalLane = isHorizontal(element); + + var textBox = renderLabel(parentGfx, text, { + box: { + height: 30, + width: isHorizontalLane ? getHeight(element, attrs) : getWidth(element, attrs), + }, + align: 'center-middle', + style: { + fill: getLabelColor(element, defaultLabelColor, defaultStrokeColor, attrs.stroke) + } + }); + + if (isHorizontalLane) { + var top = -1 * getHeight(element, attrs); + transform(textBox, 0, -top, 270); + } + } + + function renderActivity(parentGfx, element, attrs = {}) { + var { + width, + height + } = getBounds(element, attrs); + + return drawRect(parentGfx, width, height, TASK_BORDER_RADIUS, { + ...attrs, + fill: getFillColor(element, defaultFillColor, attrs.fill), + fillOpacity: DEFAULT_OPACITY, + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke) + }); + } + + function renderAssociation(parentGfx, element, attrs = {}) { + var semantic = getBusinessObject(element); + + var fill = getFillColor(element, defaultFillColor, attrs.fill), + stroke = getStrokeColor(element, defaultStrokeColor, attrs.stroke); + + if (semantic.get('associationDirection') === 'One' || + semantic.get('associationDirection') === 'Both') { + attrs.markerEnd = marker('association-end', fill, stroke); + } + + if (semantic.get('associationDirection') === 'Both') { + attrs.markerStart = marker('association-start', fill, stroke); + } + + attrs = pickAttrs(attrs, [ + 'markerStart', + 'markerEnd' + ]); + + return drawConnectionSegments(parentGfx, element.waypoints, { + ...attrs, + stroke, + strokeDasharray: '0, 5' + }); + } + + function renderDataObject(parentGfx, element, attrs = {}) { + var fill = getFillColor(element, defaultFillColor, attrs.fill), + stroke = getStrokeColor(element, defaultStrokeColor, attrs.stroke); + + var pathData = pathMap.getScaledPath('DATA_OBJECT_PATH', { + xScaleFactor: 1, + yScaleFactor: 1, + containerWidth: element.width, + containerHeight: element.height, + position: { + mx: 0.474, + my: 0.296 + } + }); + + var dataObject = drawPath(parentGfx, pathData, { + fill, + fillOpacity: DEFAULT_OPACITY, + stroke + }); + + var semantic = getBusinessObject(element); + + if (isCollection(semantic)) { + var collectionPathData = pathMap.getScaledPath('DATA_OBJECT_COLLECTION_PATH', { + xScaleFactor: 1, + yScaleFactor: 1, + containerWidth: element.width, + containerHeight: element.height, + position: { + mx: 0.33, + my: (element.height - 18) / element.height + } + }); + + drawPath(parentGfx, collectionPathData, { + strokeWidth: 2, + fill, + stroke + }); + } + + return dataObject; + } + + function renderEvent(parentGfx, element, attrs = {}) { + return drawCircle(parentGfx, element.width, element.height, { + fillOpacity: DEFAULT_OPACITY, + ...attrs, + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke) + }); + } + + function renderGateway(parentGfx, element, attrs = {}) { + return drawDiamond(parentGfx, element.width, element.height, { + fill: getFillColor(element, defaultFillColor, attrs.fill), + fillOpacity: DEFAULT_OPACITY, + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke) + }); + } + + function renderLane(parentGfx, element, attrs = {}) { + var lane = drawRect(parentGfx, getWidth(element, attrs), getHeight(element, attrs), 0, { + fill: getFillColor(element, defaultFillColor, attrs.fill), + fillOpacity: attrs.fillOpacity || DEFAULT_OPACITY, + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 1.5 + }); + + var semantic = getBusinessObject(element); + + if (is$1(semantic, 'bpmn:Lane')) { + var text = semantic.get('name'); + + renderLaneLabel(parentGfx, text, element, attrs); + } + + return lane; + } + + function renderSubProcess(parentGfx, element, attrs = {}) { + var activity = renderActivity(parentGfx, element, attrs); + + if (isEventSubProcess(element)) { + attr$1(activity, { + strokeDasharray: '0, 5.5', + strokeWidth: 2.5 + }); + } + + var expanded = isExpanded(element); + + renderEmbeddedLabel(parentGfx, element, expanded ? 'center-top' : 'center-middle', attrs); + + if (expanded) { + renderTaskMarkers(parentGfx, element, undefined, attrs); + } else { + renderTaskMarkers(parentGfx, element, [ 'SubProcessMarker' ], attrs); + } + + return activity; + } + + function renderTask(parentGfx, element, attrs = {}) { + var activity = renderActivity(parentGfx, element, attrs); + + renderEmbeddedLabel(parentGfx, element, 'center-middle', attrs); + + renderTaskMarkers(parentGfx, element, undefined, attrs); + + return activity; + } + + var handlers = this.handlers = { + 'bpmn:AdHocSubProcess': function(parentGfx, element, attrs = {}) { + if (isExpanded(element)) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke', + 'width', + 'height' + ]); + } else { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + } + + return renderSubProcess(parentGfx, element, attrs); + }, + 'bpmn:Association': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + return renderAssociation(parentGfx, element, attrs); + }, + 'bpmn:BoundaryEvent': function(parentGfx, element, attrs = {}) { + var { renderIcon = true } = attrs; + + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + var semantic = getBusinessObject(element), + cancelActivity = semantic.get('cancelActivity'); + + attrs = { + strokeWidth: 1.5, + fill: getFillColor(element, defaultFillColor, attrs.fill), + fillOpacity: FULL_OPACITY, + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke) + }; + + if (!cancelActivity) { + attrs.strokeDasharray = '6'; + } + + var event = renderEvent(parentGfx, element, attrs); + + drawCircle(parentGfx, element.width, element.height, INNER_OUTER_DIST, { + ...attrs, + fill: 'none' + }); + + if (renderIcon) { + renderEventIcon(element, parentGfx, attrs); + } + + return event; + }, + 'bpmn:BusinessRuleTask': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + var task = renderTask(parentGfx, element, attrs); + + var headerData = pathMap.getScaledPath('TASK_TYPE_BUSINESS_RULE_MAIN', { + abspos: { + x: 8, + y: 8 + } + }); + + var businessPath = drawPath(parentGfx, headerData); + + attr$1(businessPath, { + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 + }); + + var headerPathData = pathMap.getScaledPath('TASK_TYPE_BUSINESS_RULE_HEADER', { + abspos: { + x: 8, + y: 8 + } + }); + + var businessHeaderPath = drawPath(parentGfx, headerPathData); + + attr$1(businessHeaderPath, { + fill: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 + }); + + return task; + }, + 'bpmn:CallActivity': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + return renderSubProcess(parentGfx, element, { + strokeWidth: 5, + ...attrs + }); + }, + 'bpmn:ComplexGateway': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + var gateway = renderGateway(parentGfx, element, attrs); + + var pathData = pathMap.getScaledPath('GATEWAY_COMPLEX', { + xScaleFactor: 0.5, + yScaleFactor:0.5, + containerWidth: element.width, + containerHeight: element.height, + position: { + mx: 0.46, + my: 0.26 + } + }); + + drawPath(parentGfx, pathData, { + fill: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 + }); + + return gateway; + }, + 'bpmn:DataInput': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + var arrowPathData = pathMap.getRawPath('DATA_ARROW'); + + var dataObject = renderDataObject(parentGfx, element, attrs); + + drawPath(parentGfx, arrowPathData, { + fill: 'none', + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 + }); + + return dataObject; + }, + 'bpmn:DataInputAssociation': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + return renderAssociation(parentGfx, element, { + ...attrs, + markerEnd: marker('association-end', getFillColor(element, defaultFillColor, attrs.fill), getStrokeColor(element, defaultStrokeColor, attrs.stroke)) + }); + }, + 'bpmn:DataObject': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + return renderDataObject(parentGfx, element, attrs); + }, + 'bpmn:DataObjectReference': as('bpmn:DataObject'), + 'bpmn:DataOutput': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + var arrowPathData = pathMap.getRawPath('DATA_ARROW'); + + var dataObject = renderDataObject(parentGfx, element, attrs); + + drawPath(parentGfx, arrowPathData, { + strokeWidth: 1, + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke) + }); + + return dataObject; + }, + 'bpmn:DataOutputAssociation': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + return renderAssociation(parentGfx, element, { + ...attrs, + markerEnd: marker('association-end', getFillColor(element, defaultFillColor, attrs.fill), getStrokeColor(element, defaultStrokeColor, attrs.stroke)) + }); + }, + 'bpmn:DataStoreReference': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + var dataStorePath = pathMap.getScaledPath('DATA_STORE', { + xScaleFactor: 1, + yScaleFactor: 1, + containerWidth: element.width, + containerHeight: element.height, + position: { + mx: 0, + my: 0.133 + } + }); + + return drawPath(parentGfx, dataStorePath, { + fill: getFillColor(element, defaultFillColor, attrs.fill), + fillOpacity: DEFAULT_OPACITY, + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 2 + }); + }, + 'bpmn:EndEvent': function(parentGfx, element, attrs = {}) { + var { renderIcon = true } = attrs; + + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + var event = renderEvent(parentGfx, element, { + ...attrs, + strokeWidth: 4 + }); + + if (renderIcon) { + renderEventIcon(element, parentGfx, attrs); + } + + return event; + }, + 'bpmn:EventBasedGateway': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + var semantic = getBusinessObject(element); + + var diamond = renderGateway(parentGfx, element, attrs); + + drawCircle(parentGfx, element.width, element.height, element.height * 0.20, { + fill: getFillColor(element, 'none', attrs.fill), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 + }); + + var type = semantic.get('eventGatewayType'), + instantiate = !!semantic.get('instantiate'); + + function drawEvent() { + + var pathData = pathMap.getScaledPath('GATEWAY_EVENT_BASED', { + xScaleFactor: 0.18, + yScaleFactor: 0.18, + containerWidth: element.width, + containerHeight: element.height, + position: { + mx: 0.36, + my: 0.44 + } + }); + + drawPath(parentGfx, pathData, { + fill: 'none', + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 2 + }); + } + + if (type === 'Parallel') { + var pathData = pathMap.getScaledPath('GATEWAY_PARALLEL', { + xScaleFactor: 0.4, + yScaleFactor: 0.4, + containerWidth: element.width, + containerHeight: element.height, + position: { + mx: 0.474, + my: 0.296 + } + }); + + drawPath(parentGfx, pathData, { + fill: 'none', + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 + }); + } else if (type === 'Exclusive') { + if (!instantiate) { + drawCircle(parentGfx, element.width, element.height, element.height * 0.26, { + fill: 'none', + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 + }); + } + + drawEvent(); + } + + + return diamond; + }, + 'bpmn:ExclusiveGateway': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + var gateway = renderGateway(parentGfx, element, attrs); + + var pathData = pathMap.getScaledPath('GATEWAY_EXCLUSIVE', { + xScaleFactor: 0.4, + yScaleFactor: 0.4, + containerWidth: element.width, + containerHeight: element.height, + position: { + mx: 0.32, + my: 0.3 + } + }); + + var di = getDi(element); + + if (di.get('isMarkerVisible')) { + drawPath(parentGfx, pathData, { + fill: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 + }); + } + + return gateway; + }, + 'bpmn:Gateway': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + return renderGateway(parentGfx, element, attrs); + }, + 'bpmn:Group': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke', + 'width', + 'height' + ]); + + return drawRect(parentGfx, element.width, element.height, TASK_BORDER_RADIUS, { + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 1.5, + strokeDasharray: '10, 6, 0, 6', + fill: 'none', + pointerEvents: 'none', + width: getWidth(element, attrs), + height: getHeight(element, attrs) + }); + }, + 'bpmn:InclusiveGateway': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + var gateway = renderGateway(parentGfx, element, attrs); + + drawCircle(parentGfx, element.width, element.height, element.height * 0.24, { + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 2.5 + }); + + return gateway; + }, + 'bpmn:IntermediateEvent': function(parentGfx, element, attrs = {}) { + var { renderIcon = true } = attrs; + + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + var outer = renderEvent(parentGfx, element, { + ...attrs, + strokeWidth: 1.5 + }); + + drawCircle(parentGfx, element.width, element.height, INNER_OUTER_DIST, { + fill: 'none', + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 1.5 + }); + + if (renderIcon) { + renderEventIcon(element, parentGfx, attrs); + } + + return outer; + }, + 'bpmn:IntermediateCatchEvent': as('bpmn:IntermediateEvent'), + 'bpmn:IntermediateThrowEvent': as('bpmn:IntermediateEvent'), + 'bpmn:Lane': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke', + 'width', + 'height' + ]); + + return renderLane(parentGfx, element, { + ...attrs, + fillOpacity: LOW_OPACITY + }); + }, + 'bpmn:ManualTask': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + var task = renderTask(parentGfx, element, attrs); + + var pathData = pathMap.getScaledPath('TASK_TYPE_MANUAL', { + abspos: { + x: 17, + y: 15 + } + }); + + drawPath(parentGfx, pathData, { + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 0.5 + }); + + return task; + }, + 'bpmn:MessageFlow': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + var semantic = getBusinessObject(element), + di = getDi(element); + + var fill = getFillColor(element, defaultFillColor, attrs.fill), + stroke = getStrokeColor(element, defaultStrokeColor, attrs.stroke); + + var path = drawConnectionSegments(parentGfx, element.waypoints, { + markerEnd: marker('messageflow-end', fill, stroke), + markerStart: marker('messageflow-start', fill, stroke), + stroke, + strokeDasharray: '10, 11', + strokeWidth: 1.5 + }); + + if (semantic.get('messageRef')) { + var midPoint = path.getPointAtLength(path.getTotalLength() / 2); + + var markerPathData = pathMap.getScaledPath('MESSAGE_FLOW_MARKER', { + abspos: { + x: midPoint.x, + y: midPoint.y + } + }); + + var messageAttrs = { + strokeWidth: 1 + }; + + if (di.get('messageVisibleKind') === 'initiating') { + messageAttrs.fill = fill; + messageAttrs.stroke = stroke; + } else { + messageAttrs.fill = stroke; + messageAttrs.stroke = fill; + } + + var message = drawPath(parentGfx, markerPathData, messageAttrs); + + var messageRef = semantic.get('messageRef'), + name = messageRef.get('name'); + + var label = renderLabel(parentGfx, name, { + align: 'center-top', + fitBox: true, + style: { + fill: stroke + } + }); + + var messageBounds = message.getBBox(), + labelBounds = label.getBBox(); + + var translateX = midPoint.x - labelBounds.width / 2, + translateY = midPoint.y + messageBounds.height / 2 + ELEMENT_LABEL_DISTANCE; + + transform(label, translateX, translateY, 0); + } + + return path; + }, + 'bpmn:ParallelGateway': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + var diamond = renderGateway(parentGfx, element, attrs); + + var pathData = pathMap.getScaledPath('GATEWAY_PARALLEL', { + xScaleFactor: 0.6, + yScaleFactor: 0.6, + containerWidth: element.width, + containerHeight: element.height, + position: { + mx: 0.46, + my: 0.2 + } + }); + + drawPath(parentGfx, pathData, { + fill: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 + }); + + return diamond; + }, + 'bpmn:Participant': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke', + 'width', + 'height' + ]); + + var participant = renderLane(parentGfx, element, attrs); + + var expandedParticipant = isExpanded(element); + var horizontalParticipant = isHorizontal(element); + + var semantic = getBusinessObject(element), + name = semantic.get('name'); + + if (expandedParticipant) { + var waypoints = horizontalParticipant ? [ + { + x: 30, + y: 0 + }, + { + x: 30, + y: getHeight(element, attrs) + } + ] : [ + { + x: 0, + y: 30 + }, + { + x: getWidth(element, attrs), + y: 30 + } + ]; + + drawLine(parentGfx, waypoints, { + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: PARTICIPANT_STROKE_WIDTH + }); + + renderLaneLabel(parentGfx, name, element, attrs); + } else { + var bounds = getBounds(element, attrs); + + if (!horizontalParticipant) { + bounds.height = getWidth(element, attrs); + bounds.width = getHeight(element, attrs); + } + + var textBox = renderLabel(parentGfx, name, { + box: bounds, + align: 'center-middle', + style: { + fill: getLabelColor(element, defaultLabelColor, defaultStrokeColor, attrs.stroke) + } + }); + + if (!horizontalParticipant) { + var top = -1 * getHeight(element, attrs); + transform(textBox, 0, -top, 270); + } + } + + if (semantic.get('participantMultiplicity')) { + renderTaskMarker('ParticipantMultiplicityMarker', parentGfx, element, attrs); + } + + return participant; + }, + 'bpmn:ReceiveTask' : function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + var semantic = getBusinessObject(element); + + var task = renderTask(parentGfx, element, attrs); + + var pathData; + + if (semantic.get('instantiate')) { + drawCircle(parentGfx, 28, 28, 20 * 0.22, { + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 + }); + + pathData = pathMap.getScaledPath('TASK_TYPE_INSTANTIATING_SEND', { + abspos: { + x: 7.77, + y: 9.52 + } + }); + } else { + pathData = pathMap.getScaledPath('TASK_TYPE_SEND', { + xScaleFactor: 0.9, + yScaleFactor: 0.9, + containerWidth: 21, + containerHeight: 14, + position: { + mx: 0.3, + my: 0.4 + } + }); + } + + drawPath(parentGfx, pathData, { + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 + }); + + return task; + }, + 'bpmn:ScriptTask': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + var task = renderTask(parentGfx, element, attrs); + + var pathData = pathMap.getScaledPath('TASK_TYPE_SCRIPT', { + abspos: { + x: 15, + y: 20 + } + }); + + drawPath(parentGfx, pathData, { + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 + }); + + return task; + }, + 'bpmn:SendTask': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + var task = renderTask(parentGfx, element, attrs); + + var pathData = pathMap.getScaledPath('TASK_TYPE_SEND', { + xScaleFactor: 1, + yScaleFactor: 1, + containerWidth: 21, + containerHeight: 14, + position: { + mx: 0.285, + my: 0.357 + } + }); + + drawPath(parentGfx, pathData, { + fill: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + stroke: getFillColor(element, defaultFillColor, attrs.fill), + strokeWidth: 1 + }); + + return task; + }, + 'bpmn:SequenceFlow': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + var fill = getFillColor(element, defaultFillColor, attrs.fill), + stroke = getStrokeColor(element, defaultStrokeColor, attrs.stroke); + + var connection = drawConnectionSegments(parentGfx, element.waypoints, { + markerEnd: marker('sequenceflow-end', fill, stroke), + stroke + }); + + var semantic = getBusinessObject(element); + + var { source } = element; + + if (source) { + var sourceSemantic = getBusinessObject(source); + + // conditional flow marker + if (semantic.get('conditionExpression') && is$1(sourceSemantic, 'bpmn:Activity')) { + attr$1(connection, { + markerStart: marker('conditional-flow-marker', fill, stroke) + }); + } + + // default marker + if (sourceSemantic.get('default') && (is$1(sourceSemantic, 'bpmn:Gateway') || is$1(sourceSemantic, 'bpmn:Activity')) && + sourceSemantic.get('default') === semantic) { + attr$1(connection, { + markerStart: marker('conditional-default-flow-marker', fill, stroke) + }); + } + } + + return connection; + }, + 'bpmn:ServiceTask': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + var task = renderTask(parentGfx, element, attrs); + + drawCircle(parentGfx, 10, 10, { + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: 'none', + transform: 'translate(6, 6)' + }); + + var pathDataService1 = pathMap.getScaledPath('TASK_TYPE_SERVICE', { + abspos: { + x: 12, + y: 18 + } + }); + + drawPath(parentGfx, pathDataService1, { + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 + }); + + drawCircle(parentGfx, 10, 10, { + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: 'none', + transform: 'translate(11, 10)' + }); + + var pathDataService2 = pathMap.getScaledPath('TASK_TYPE_SERVICE', { + abspos: { + x: 17, + y: 22 + } + }); + + drawPath(parentGfx, pathDataService2, { + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 1 + }); + + return task; + }, + 'bpmn:StartEvent': function(parentGfx, element, attrs = {}) { + var { renderIcon = true } = attrs; + + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + var semantic = getBusinessObject(element); + + if (!semantic.get('isInterrupting')) { + attrs = { + ...attrs, + strokeDasharray: '6' + }; + } + + var event = renderEvent(parentGfx, element, attrs); + + if (renderIcon) { + renderEventIcon(element, parentGfx, attrs); + } + + return event; + }, + 'bpmn:SubProcess': function(parentGfx, element, attrs = {}) { + if (isExpanded(element)) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke', + 'width', + 'height' + ]); + } else { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + } + + return renderSubProcess(parentGfx, element, attrs); + }, + 'bpmn:Task': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + return renderTask(parentGfx, element, attrs); + }, + 'bpmn:TextAnnotation': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke', + 'width', + 'height' + ]); + + var { + width, + height + } = getBounds(element, attrs); + + var textElement = drawRect(parentGfx, width, height, 0, 0, { + fill: 'none', + stroke: 'none' + }); + + var textPathData = pathMap.getScaledPath('TEXT_ANNOTATION', { + xScaleFactor: 1, + yScaleFactor: 1, + containerWidth: width, + containerHeight: height, + position: { + mx: 0.0, + my: 0.0 + } + }); + + drawPath(parentGfx, textPathData, { + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke) + }); + + var semantic = getBusinessObject(element), + text = semantic.get('text') || ''; + + renderLabel(parentGfx, text, { + align: 'left-top', + box: getBounds(element, attrs), + padding: 7, + style: { + fill: getLabelColor(element, defaultLabelColor, defaultStrokeColor, attrs.stroke) + } + }); + + return textElement; + }, + 'bpmn:Transaction': function(parentGfx, element, attrs = {}) { + if (isExpanded(element)) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke', + 'width', + 'height' + ]); + } else { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + } + + var outer = renderSubProcess(parentGfx, element, { + strokeWidth: 1.5, + ...attrs + }); + + var innerAttrs = styles.style([ 'no-fill', 'no-events' ], { + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 1.5 + }); + + var expanded = isExpanded(element); + + if (!expanded) { + attrs = {}; + } + + drawRect( + parentGfx, + getWidth(element, attrs), + getHeight(element, attrs), + TASK_BORDER_RADIUS - INNER_OUTER_DIST, + INNER_OUTER_DIST, + innerAttrs + ); + + return outer; + }, + 'bpmn:UserTask': function(parentGfx, element, attrs = {}) { + attrs = pickAttrs(attrs, [ + 'fill', + 'stroke' + ]); + + var task = renderTask(parentGfx, element, attrs); + + var x = 15; + var y = 12; + + var pathDataUser1 = pathMap.getScaledPath('TASK_TYPE_USER_1', { + abspos: { + x: x, + y: y + } + }); + + drawPath(parentGfx, pathDataUser1, { + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 0.5 + }); + + var pathDataUser2 = pathMap.getScaledPath('TASK_TYPE_USER_2', { + abspos: { + x: x, + y: y + } + }); + + drawPath(parentGfx, pathDataUser2, { + fill: getFillColor(element, defaultFillColor, attrs.fill), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 0.5 + }); + + var pathDataUser3 = pathMap.getScaledPath('TASK_TYPE_USER_3', { + abspos: { + x: x, + y: y + } + }); + + drawPath(parentGfx, pathDataUser3, { + fill: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + stroke: getStrokeColor(element, defaultStrokeColor, attrs.stroke), + strokeWidth: 0.5 + }); + + return task; + }, + 'label': function(parentGfx, element, attrs = {}) { + return renderExternalLabel(parentGfx, element, attrs); + } + }; + + // extension API, use at your own risk + this._drawPath = drawPath; + + this._renderer = renderer; + } + + + e(BpmnRenderer, BaseRenderer); + + BpmnRenderer.$inject = [ + 'config.bpmnRenderer', + 'eventBus', + 'styles', + 'pathMap', + 'canvas', + 'textRenderer' + ]; + + + /** + * @param {Element} element + * + * @return {boolean} + */ + BpmnRenderer.prototype.canRender = function(element) { + return is$1(element, 'bpmn:BaseElement'); + }; + + /** + * Draw shape into parentGfx. + * + * @param {SVGElement} parentGfx + * @param {Element} element + * @param {Attrs} [attrs] + * + * @return {SVGElement} mainGfx + */ + BpmnRenderer.prototype.drawShape = function(parentGfx, element, attrs = {}) { + var { type } = element; + + var handler = this._renderer(type); + + return handler(parentGfx, element, attrs); + }; + + /** + * Draw connection into parentGfx. + * + * @param {SVGElement} parentGfx + * @param {Element} element + * @param {Attrs} [attrs] + * + * @return {SVGElement} mainGfx + */ + BpmnRenderer.prototype.drawConnection = function(parentGfx, element, attrs = {}) { + var { type } = element; + + var handler = this._renderer(type); + + return handler(parentGfx, element, attrs); + }; + + /** + * Get shape path. + * + * @param {Element} element + * + * @return {string} path + */ + BpmnRenderer.prototype.getShapePath = function(element) { + if (is$1(element, 'bpmn:Event')) { + return getCirclePath(element); + } + + if (is$1(element, 'bpmn:Activity')) { + return getRoundRectPath(element, TASK_BORDER_RADIUS); + } + + if (is$1(element, 'bpmn:Gateway')) { + return getDiamondPath(element); + } + + return getRectPath(element); + }; + + /** + * Pick attributes if they exist. + * + * @param {Object} attrs + * @param {string[]} keys + * + * @returns {Object} + */ + function pickAttrs(attrs, keys = []) { + return keys.reduce((pickedAttrs, key) => { + if (attrs[ key ]) { + pickedAttrs[ key ] = attrs[ key ]; + } + + return pickedAttrs; + }, {}); + } + + /** + * @typedef {import('../util/Types').Dimensions} Dimensions + * + * @typedef { { + * top: number; + * left: number; + * right: number; + * bottom: number; + * } } Padding + * + * @typedef { number | Partial } PaddingConfig + * + * @typedef { { + * horizontal: 'center' | 'left'; + * vertical: 'top' | 'center'; + * } } Alignment + * + * @typedef { 'center-middle' | 'center-top' } AlignmentConfig + * + * @typedef { Partial<{ + * align: AlignmentConfig; + * style: Record; + * padding: PaddingConfig; + * }> } BaseTextConfig + * + * @typedef { BaseTextConfig & Partial<{ + * size: Dimensions; + * }> } TextConfig + * + * @typedef { BaseTextConfig & Partial<{ + * box: Dimensions; + * fitBox: boolean; + * }> } TextLayoutConfig + * + * @typedef { Dimensions & { + * text: string; + * } } LineDescriptor + */ + + var DEFAULT_BOX_PADDING = 0; + + var DEFAULT_LABEL_SIZE = { + width: 150, + height: 50 + }; + + + /** + * @param {AlignmentConfig} align + * @return {Alignment} + */ + function parseAlign(align) { + + var parts = align.split('-'); + + return { + horizontal: parts[0] || 'center', + vertical: parts[1] || 'top' + }; + } + + /** + * @param {PaddingConfig} padding + * + * @return {Padding} + */ + function parsePadding(padding) { + + if (isObject(padding)) { + return assign$1({ top: 0, left: 0, right: 0, bottom: 0 }, padding); + } else { + return { + top: padding, + left: padding, + right: padding, + bottom: padding + }; + } + } + + /** + * @param {string} text + * @param {SVGTextElement} fakeText + * + * @return {import('../util/Types').Dimensions} + */ + function getTextBBox(text, fakeText) { + + fakeText.textContent = text; + + var textBBox; + + try { + var bbox, + emptyLine = text === ''; + + // add dummy text, when line is empty to + // determine correct height + fakeText.textContent = emptyLine ? 'dummy' : text; + + textBBox = fakeText.getBBox(); + + // take text rendering related horizontal + // padding into account + bbox = { + width: textBBox.width + textBBox.x * 2, + height: textBBox.height + }; + + if (emptyLine) { + + // correct width + bbox.width = 0; + } + + return bbox; + } catch (e) { + return { width: 0, height: 0 }; + } + } + + + /** + * Layout the next line and return the layouted element. + * + * Alters the lines passed. + * + * @param {string[]} lines + * @param {number} maxWidth + * @param {SVGTextElement} fakeText + * + * @return {LineDescriptor} the line descriptor + */ + function layoutNext(lines, maxWidth, fakeText) { + + var originalLine = lines.shift(), + fitLine = originalLine; + + var textBBox; + + for (;;) { + textBBox = getTextBBox(fitLine, fakeText); + + textBBox.width = fitLine ? textBBox.width : 0; + + // try to fit + if (fitLine === ' ' || fitLine === '' || textBBox.width < Math.round(maxWidth) || fitLine.length < 2) { + return fit(lines, fitLine, originalLine, textBBox); + } + + fitLine = shortenLine(fitLine, textBBox.width, maxWidth); + } + } + + /** + * @param {string[]} lines + * @param {string} fitLine + * @param {string} originalLine + * @param {Dimensions} textBBox + * + * @return {LineDescriptor} + */ + function fit(lines, fitLine, originalLine, textBBox) { + if (fitLine.length < originalLine.length) { + var remainder = originalLine.slice(fitLine.length).trim(); + + lines.unshift(remainder); + } + + return { + width: textBBox.width, + height: textBBox.height, + text: fitLine + }; + } + + var SOFT_BREAK = '\u00AD'; + + + /** + * Shortens a line based on spacing and hyphens. + * Returns the shortened result on success. + * + * @param {string} line + * @param {number} maxLength the maximum characters of the string + * + * @return {string} the shortened string + */ + function semanticShorten(line, maxLength) { + + var parts = line.split(/(\s|-|\u00AD)/g), + part, + shortenedParts = [], + length = 0; + + // try to shorten via break chars + if (parts.length > 1) { + + while ((part = parts.shift())) { + if (part.length + length < maxLength) { + shortenedParts.push(part); + length += part.length; + } else { + + // remove previous part, too if hyphen does not fit anymore + if (part === '-' || part === SOFT_BREAK) { + shortenedParts.pop(); + } + + break; + } + } + } + + var last = shortenedParts[shortenedParts.length - 1]; + + // translate trailing soft break to actual hyphen + if (last && last === SOFT_BREAK) { + shortenedParts[shortenedParts.length - 1] = '-'; + } + + return shortenedParts.join(''); + } + + + /** + * @param {string} line + * @param {number} width + * @param {number} maxWidth + * + * @return {string} + */ + function shortenLine(line, width, maxWidth) { + var length = Math.max(line.length * (maxWidth / width), 1); + + // try to shorten semantically (i.e. based on spaces and hyphens) + var shortenedLine = semanticShorten(line, length); + + if (!shortenedLine) { + + // force shorten by cutting the long word + shortenedLine = line.slice(0, Math.max(Math.round(length - 1), 1)); + } + + return shortenedLine; + } + + + /** + * @return {SVGSVGElement} + */ + function getHelperSvg() { + var helperSvg = document.getElementById('helper-svg'); + + if (!helperSvg) { + helperSvg = create$1('svg'); + + attr$1(helperSvg, { + id: 'helper-svg' + }); + + assign(helperSvg, { + visibility: 'hidden', + position: 'fixed', + width: 0, + height: 0 + }); + + document.body.appendChild(helperSvg); + } + + return helperSvg; + } + + + /** + * Creates a new label utility + * + * @param {TextConfig} config + */ + function Text(config) { + + this._config = assign$1({}, { + size: DEFAULT_LABEL_SIZE, + padding: DEFAULT_BOX_PADDING, + style: {}, + align: 'center-top' + }, config || {}); + } + + /** + * Returns the layouted text as an SVG element. + * + * @param {string} text + * @param {TextLayoutConfig} options + * + * @return {SVGElement} + */ + Text.prototype.createText = function(text, options) { + return this.layoutText(text, options).element; + }; + + /** + * Returns a labels layouted dimensions. + * + * @param {string} text to layout + * @param {TextLayoutConfig} options + * + * @return {Dimensions} + */ + Text.prototype.getDimensions = function(text, options) { + return this.layoutText(text, options).dimensions; + }; + + /** + * Creates and returns a label and its bounding box. + * + * @param {string} text the text to render on the label + * @param {TextLayoutConfig} options + * + * @return { { + * element: SVGElement, + * dimensions: Dimensions + * } } + */ + Text.prototype.layoutText = function(text, options) { + var box = assign$1({}, this._config.size, options.box), + style = assign$1({}, this._config.style, options.style), + align = parseAlign(options.align || this._config.align), + padding = parsePadding(options.padding !== undefined ? options.padding : this._config.padding), + fitBox = options.fitBox || false; + + var lineHeight = getLineHeight(style); + + // we split text by lines and normalize + // {soft break} + {line break} => { line break } + var lines = text.split(/\u00AD?\r?\n/), + layouted = []; + + var maxWidth = box.width - padding.left - padding.right; + + // ensure correct rendering by attaching helper text node to invisible SVG + var helperText = create$1('text'); + attr$1(helperText, { x: 0, y: 0 }); + attr$1(helperText, style); + + var helperSvg = getHelperSvg(); + + append(helperSvg, helperText); + + while (lines.length) { + layouted.push(layoutNext(lines, maxWidth, helperText)); + } + + if (align.vertical === 'middle') { + padding.top = padding.bottom = 0; + } + + var totalHeight = reduce(layouted, function(sum, line, idx) { + return sum + (lineHeight || line.height); + }, 0) + padding.top + padding.bottom; + + var maxLineWidth = reduce(layouted, function(sum, line, idx) { + return line.width > sum ? line.width : sum; + }, 0); + + // the y position of the next line + var y = padding.top; + + if (align.vertical === 'middle') { + y += (box.height - totalHeight) / 2; + } + + // magic number initial offset + y -= (lineHeight || layouted[0].height) / 4; + + + var textElement = create$1('text'); + + attr$1(textElement, style); + + // layout each line taking into account that parent + // shape might resize to fit text size + forEach$1(layouted, function(line) { + + var x; + + y += (lineHeight || line.height); + + switch (align.horizontal) { + case 'left': + x = padding.left; + break; + + case 'right': + x = ((fitBox ? maxLineWidth : maxWidth) + - padding.right - line.width); + break; + + default: + + // aka center + x = Math.max((((fitBox ? maxLineWidth : maxWidth) + - line.width) / 2 + padding.left), 0); + } + + var tspan = create$1('tspan'); + attr$1(tspan, { x: x, y: y }); + + tspan.textContent = line.text; + + append(textElement, tspan); + }); + + remove$2(helperText); + + var dimensions = { + width: maxLineWidth, + height: totalHeight + }; + + return { + dimensions: dimensions, + element: textElement + }; + }; + + + function getLineHeight(style) { + if ('fontSize' in style && 'lineHeight' in style) { + return style.lineHeight * parseInt(style.fontSize, 10); + } + } + + var DEFAULT_FONT_SIZE = 12; + var LINE_HEIGHT_RATIO = 1.2; + + var MIN_TEXT_ANNOTATION_HEIGHT = 30; + + /** + * @typedef { { + * fontFamily: string; + * fontSize: number; + * fontWeight: string; + * lineHeight: number; + * } } TextRendererStyle + * + * @typedef { { + * defaultStyle?: Partial; + * externalStyle?: Partial; + * } } TextRendererConfig + * + * @typedef { import('diagram-js/lib/util/Text').TextLayoutConfig } TextLayoutConfig + * + * @typedef { import('diagram-js/lib/util/Types').Rect } Rect + */ + + + /** + * Renders text and computes text bounding boxes. + * + * @param {TextRendererConfig} [config] + */ + function TextRenderer(config) { + + var defaultStyle = assign$1({ + fontFamily: 'Arial, sans-serif', + fontSize: DEFAULT_FONT_SIZE, + fontWeight: 'normal', + lineHeight: LINE_HEIGHT_RATIO + }, config && config.defaultStyle || {}); + + var fontSize = parseInt(defaultStyle.fontSize, 10) - 1; + + var externalStyle = assign$1({}, defaultStyle, { + fontSize: fontSize + }, config && config.externalStyle || {}); + + var textUtil = new Text({ + style: defaultStyle + }); + + /** + * Get the new bounds of an externally rendered, + * layouted label. + * + * @param {Rect} bounds + * @param {string} text + * + * @return {Rect} + */ + this.getExternalLabelBounds = function(bounds, text) { + + var layoutedDimensions = textUtil.getDimensions(text, { + box: { + width: 90, + height: 30 + }, + style: externalStyle + }); + + // resize label shape to fit label text + return { + x: Math.round(bounds.x + bounds.width / 2 - layoutedDimensions.width / 2), + y: Math.round(bounds.y), + width: Math.ceil(layoutedDimensions.width), + height: Math.ceil(layoutedDimensions.height) + }; + + }; + + /** + * Get the new bounds of text annotation. + * + * @param {Rect} bounds + * @param {string} text + * + * @return {Rect} + */ + this.getTextAnnotationBounds = function(bounds, text) { + + var layoutedDimensions = textUtil.getDimensions(text, { + box: bounds, + style: defaultStyle, + align: 'left-top', + padding: 5 + }); + + return { + x: bounds.x, + y: bounds.y, + width: bounds.width, + height: Math.max(MIN_TEXT_ANNOTATION_HEIGHT, Math.round(layoutedDimensions.height)) + }; + }; + + /** + * Create a layouted text element. + * + * @param {string} text + * @param {TextLayoutConfig} [options] + * + * @return {SVGElement} rendered text + */ + this.createText = function(text, options) { + return textUtil.createText(text, options || {}); + }; + + /** + * Get default text style. + */ + this.getDefaultStyle = function() { + return defaultStyle; + }; + + /** + * Get the external text style. + */ + this.getExternalStyle = function() { + return externalStyle; + }; + + } + + TextRenderer.$inject = [ + 'config.textRenderer' + ]; + + /** + * Map containing SVG paths needed by BpmnRenderer + */ + function PathMap() { + + /** + * Contains a map of path elements + * + *

Path definition

+ * A parameterized path is defined like this: + *
+     * 'GATEWAY_PARALLEL': {
+     *   d: 'm {mx},{my} {e.x0},0 0,{e.x1} {e.x1},0 0,{e.y0} -{e.x1},0 0,{e.y1} ' +
+            '-{e.x0},0 0,-{e.y1} -{e.x1},0 0,-{e.y0} {e.x1},0 z',
+     *   height: 17.5,
+     *   width:  17.5,
+     *   heightElements: [2.5, 7.5],
+     *   widthElements: [2.5, 7.5]
+     * }
+     * 
+ *

It's important to specify a correct height and width for the path as the scaling + * is based on the ratio between the specified height and width in this object and the + * height and width that is set as scale target (Note x,y coordinates will be scaled with + * individual ratios).

+ *

The 'heightElements' and 'widthElements' array must contain the values that will be scaled. + * The scaling is based on the computed ratios. + * Coordinates on the y axis should be in the heightElement's array, they will be scaled using + * the computed ratio coefficient. + * In the parameterized path the scaled values can be accessed through the 'e' object in {} brackets. + *

    + *
  • The values for the y axis can be accessed in the path string using {e.y0}, {e.y1}, ....
  • + *
  • The values for the x axis can be accessed in the path string using {e.x0}, {e.x1}, ....
  • + *
+ * The numbers x0, x1 respectively y0, y1, ... map to the corresponding array index. + *

+ */ + this.pathMap = { + 'EVENT_MESSAGE': { + d: 'm {mx},{my} l 0,{e.y1} l {e.x1},0 l 0,-{e.y1} z l {e.x0},{e.y0} l {e.x0},-{e.y0}', + height: 36, + width: 36, + heightElements: [ 6, 14 ], + widthElements: [ 10.5, 21 ] + }, + 'EVENT_SIGNAL': { + d: 'M {mx},{my} l {e.x0},{e.y0} l -{e.x1},0 Z', + height: 36, + width: 36, + heightElements: [ 18 ], + widthElements: [ 10, 20 ] + }, + 'EVENT_ESCALATION': { + d: 'M {mx},{my} l {e.x0},{e.y0} l -{e.x0},-{e.y1} l -{e.x0},{e.y1} Z', + height: 36, + width: 36, + heightElements: [ 20, 7 ], + widthElements: [ 8 ] + }, + 'EVENT_CONDITIONAL': { + d: 'M {e.x0},{e.y0} l {e.x1},0 l 0,{e.y2} l -{e.x1},0 Z ' + + 'M {e.x2},{e.y3} l {e.x0},0 ' + + 'M {e.x2},{e.y4} l {e.x0},0 ' + + 'M {e.x2},{e.y5} l {e.x0},0 ' + + 'M {e.x2},{e.y6} l {e.x0},0 ' + + 'M {e.x2},{e.y7} l {e.x0},0 ' + + 'M {e.x2},{e.y8} l {e.x0},0 ', + height: 36, + width: 36, + heightElements: [ 8.5, 14.5, 18, 11.5, 14.5, 17.5, 20.5, 23.5, 26.5 ], + widthElements: [ 10.5, 14.5, 12.5 ] + }, + 'EVENT_LINK': { + d: 'm {mx},{my} 0,{e.y0} -{e.x1},0 0,{e.y1} {e.x1},0 0,{e.y0} {e.x0},-{e.y2} -{e.x0},-{e.y2} z', + height: 36, + width: 36, + heightElements: [ 4.4375, 6.75, 7.8125 ], + widthElements: [ 9.84375, 13.5 ] + }, + 'EVENT_ERROR': { + d: 'm {mx},{my} {e.x0},-{e.y0} {e.x1},-{e.y1} {e.x2},{e.y2} {e.x3},-{e.y3} -{e.x4},{e.y4} -{e.x5},-{e.y5} z', + height: 36, + width: 36, + heightElements: [ 0.023, 8.737, 8.151, 16.564, 10.591, 8.714 ], + widthElements: [ 0.085, 6.672, 6.97, 4.273, 5.337, 6.636 ] + }, + 'EVENT_CANCEL_45': { + d: 'm {mx},{my} -{e.x1},0 0,{e.x0} {e.x1},0 0,{e.y1} {e.x0},0 ' + + '0,-{e.y1} {e.x1},0 0,-{e.y0} -{e.x1},0 0,-{e.y1} -{e.x0},0 z', + height: 36, + width: 36, + heightElements: [ 4.75, 8.5 ], + widthElements: [ 4.75, 8.5 ] + }, + 'EVENT_COMPENSATION': { + d: 'm {mx},{my} {e.x0},-{e.y0} 0,{e.y1} z m {e.x1},-{e.y2} {e.x2},-{e.y3} 0,{e.y1} -{e.x2},-{e.y3} z', + height: 36, + width: 36, + heightElements: [ 6.5, 13, 0.4, 6.1 ], + widthElements: [ 9, 9.3, 8.7 ] + }, + 'EVENT_TIMER_WH': { + d: 'M {mx},{my} l {e.x0},-{e.y0} m -{e.x0},{e.y0} l {e.x1},{e.y1} ', + height: 36, + width: 36, + heightElements: [ 10, 2 ], + widthElements: [ 3, 7 ] + }, + 'EVENT_TIMER_LINE': { + d: 'M {mx},{my} ' + + 'm {e.x0},{e.y0} l -{e.x1},{e.y1} ', + height: 36, + width: 36, + heightElements: [ 10, 3 ], + widthElements: [ 0, 0 ] + }, + 'EVENT_MULTIPLE': { + d:'m {mx},{my} {e.x1},-{e.y0} {e.x1},{e.y0} -{e.x0},{e.y1} -{e.x2},0 z', + height: 36, + width: 36, + heightElements: [ 6.28099, 12.56199 ], + widthElements: [ 3.1405, 9.42149, 12.56198 ] + }, + 'EVENT_PARALLEL_MULTIPLE': { + d:'m {mx},{my} {e.x0},0 0,{e.y1} {e.x1},0 0,{e.y0} -{e.x1},0 0,{e.y1} ' + + '-{e.x0},0 0,-{e.y1} -{e.x1},0 0,-{e.y0} {e.x1},0 z', + height: 36, + width: 36, + heightElements: [ 2.56228, 7.68683 ], + widthElements: [ 2.56228, 7.68683 ] + }, + 'GATEWAY_EXCLUSIVE': { + d:'m {mx},{my} {e.x0},{e.y0} {e.x1},{e.y0} {e.x2},0 {e.x4},{e.y2} ' + + '{e.x4},{e.y1} {e.x2},0 {e.x1},{e.y3} {e.x0},{e.y3} ' + + '{e.x3},0 {e.x5},{e.y1} {e.x5},{e.y2} {e.x3},0 z', + height: 17.5, + width: 17.5, + heightElements: [ 8.5, 6.5312, -6.5312, -8.5 ], + widthElements: [ 6.5, -6.5, 3, -3, 5, -5 ] + }, + 'GATEWAY_PARALLEL': { + d:'m {mx},{my} 0,{e.y1} -{e.x1},0 0,{e.y0} {e.x1},0 0,{e.y1} {e.x0},0 ' + + '0,-{e.y1} {e.x1},0 0,-{e.y0} -{e.x1},0 0,-{e.y1} -{e.x0},0 z', + height: 30, + width: 30, + heightElements: [ 5, 12.5 ], + widthElements: [ 5, 12.5 ] + }, + 'GATEWAY_EVENT_BASED': { + d:'m {mx},{my} {e.x0},{e.y0} {e.x0},{e.y1} {e.x1},{e.y2} {e.x2},0 z', + height: 11, + width: 11, + heightElements: [ -6, 6, 12, -12 ], + widthElements: [ 9, -3, -12 ] + }, + 'GATEWAY_COMPLEX': { + d:'m {mx},{my} 0,{e.y0} -{e.x0},-{e.y1} -{e.x1},{e.y2} {e.x0},{e.y1} -{e.x2},0 0,{e.y3} ' + + '{e.x2},0 -{e.x0},{e.y1} l {e.x1},{e.y2} {e.x0},-{e.y1} 0,{e.y0} {e.x3},0 0,-{e.y0} {e.x0},{e.y1} ' + + '{e.x1},-{e.y2} -{e.x0},-{e.y1} {e.x2},0 0,-{e.y3} -{e.x2},0 {e.x0},-{e.y1} -{e.x1},-{e.y2} ' + + '-{e.x0},{e.y1} 0,-{e.y0} -{e.x3},0 z', + height: 17.125, + width: 17.125, + heightElements: [ 4.875, 3.4375, 2.125, 3 ], + widthElements: [ 3.4375, 2.125, 4.875, 3 ] + }, + 'DATA_OBJECT_PATH': { + d:'m 0,0 {e.x1},0 {e.x0},{e.y0} 0,{e.y1} -{e.x2},0 0,-{e.y2} {e.x1},0 0,{e.y0} {e.x0},0', + height: 61, + width: 51, + heightElements: [ 10, 50, 60 ], + widthElements: [ 10, 40, 50, 60 ] + }, + 'DATA_OBJECT_COLLECTION_PATH': { + d: 'm{mx},{my} m 3,2 l 0,10 m 3,-10 l 0,10 m 3,-10 l 0,10', + height: 10, + width: 10, + heightElements: [], + widthElements: [] + }, + 'DATA_ARROW': { + d:'m 5,9 9,0 0,-3 5,5 -5,5 0,-3 -9,0 z', + height: 61, + width: 51, + heightElements: [], + widthElements: [] + }, + 'DATA_STORE': { + d:'m {mx},{my} ' + + 'l 0,{e.y2} ' + + 'c {e.x0},{e.y1} {e.x1},{e.y1} {e.x2},0 ' + + 'l 0,-{e.y2} ' + + 'c -{e.x0},-{e.y1} -{e.x1},-{e.y1} -{e.x2},0' + + 'c {e.x0},{e.y1} {e.x1},{e.y1} {e.x2},0 ' + + 'm -{e.x2},{e.y0}' + + 'c {e.x0},{e.y1} {e.x1},{e.y1} {e.x2},0' + + 'm -{e.x2},{e.y0}' + + 'c {e.x0},{e.y1} {e.x1},{e.y1} {e.x2},0', + height: 61, + width: 61, + heightElements: [ 7, 10, 45 ], + widthElements: [ 2, 58, 60 ] + }, + 'TEXT_ANNOTATION': { + d: 'm {mx}, {my} m 10,0 l -10,0 l 0,{e.y0} l 10,0', + height: 30, + width: 10, + heightElements: [ 30 ], + widthElements: [ 10 ] + }, + 'MARKER_SUB_PROCESS': { + d: 'm{mx},{my} m 7,2 l 0,10 m -5,-5 l 10,0', + height: 10, + width: 10, + heightElements: [], + widthElements: [] + }, + 'MARKER_PARALLEL': { + d: 'm{mx},{my} m 3,2 l 0,10 m 3,-10 l 0,10 m 3,-10 l 0,10', + height: 10, + width: 10, + heightElements: [], + widthElements: [] + }, + 'MARKER_SEQUENTIAL': { + d: 'm{mx},{my} m 0,3 l 10,0 m -10,3 l 10,0 m -10,3 l 10,0', + height: 10, + width: 10, + heightElements: [], + widthElements: [] + }, + 'MARKER_COMPENSATION': { + d: 'm {mx},{my} 7,-5 0,10 z m 7.1,-0.3 6.9,-4.7 0,10 -6.9,-4.7 z', + height: 10, + width: 21, + heightElements: [], + widthElements: [] + }, + 'MARKER_LOOP': { + d: 'm {mx},{my} c 3.526979,0 6.386161,-2.829858 6.386161,-6.320661 0,-3.490806 -2.859182,-6.320661 ' + + '-6.386161,-6.320661 -3.526978,0 -6.38616,2.829855 -6.38616,6.320661 0,1.745402 ' + + '0.714797,3.325567 1.870463,4.469381 0.577834,0.571908 1.265885,1.034728 2.029916,1.35457 ' + + 'l -0.718163,-3.909793 m 0.718163,3.909793 -3.885211,0.802902', + height: 13.9, + width: 13.7, + heightElements: [], + widthElements: [] + }, + 'MARKER_ADHOC': { + d: 'm {mx},{my} m 0.84461,2.64411 c 1.05533,-1.23780996 2.64337,-2.07882 4.29653,-1.97997996 2.05163,0.0805 ' + + '3.85579,1.15803 5.76082,1.79107 1.06385,0.34139996 2.24454,0.1438 3.18759,-0.43767 0.61743,-0.33642 ' + + '1.2775,-0.64078 1.7542,-1.17511 0,0.56023 0,1.12046 0,1.6807 -0.98706,0.96237996 -2.29792,1.62393996 ' + + '-3.6918,1.66181996 -1.24459,0.0927 -2.46671,-0.2491 -3.59505,-0.74812 -1.35789,-0.55965 ' + + '-2.75133,-1.33436996 -4.27027,-1.18121996 -1.37741,0.14601 -2.41842,1.13685996 -3.44288,1.96782996 z', + height: 4, + width: 15, + heightElements: [], + widthElements: [] + }, + 'TASK_TYPE_SEND': { + d: 'm {mx},{my} l 0,{e.y1} l {e.x1},0 l 0,-{e.y1} z l {e.x0},{e.y0} l {e.x0},-{e.y0}', + height: 14, + width: 21, + heightElements: [ 6, 14 ], + widthElements: [ 10.5, 21 ] + }, + 'TASK_TYPE_SCRIPT': { + d: 'm {mx},{my} c 9.966553,-6.27276 -8.000926,-7.91932 2.968968,-14.938 l -8.802728,0 ' + + 'c -10.969894,7.01868 6.997585,8.66524 -2.968967,14.938 z ' + + 'm -7,-12 l 5,0 ' + + 'm -4.5,3 l 4.5,0 ' + + 'm -3,3 l 5,0' + + 'm -4,3 l 5,0', + height: 15, + width: 12.6, + heightElements: [ 6, 14 ], + widthElements: [ 10.5, 21 ] + }, + 'TASK_TYPE_USER_1': { + d: 'm {mx},{my} c 0.909,-0.845 1.594,-2.049 1.594,-3.385 0,-2.554 -1.805,-4.62199999 ' + + '-4.357,-4.62199999 -2.55199998,0 -4.28799998,2.06799999 -4.28799998,4.62199999 0,1.348 ' + + '0.974,2.562 1.89599998,3.405 -0.52899998,0.187 -5.669,2.097 -5.794,4.7560005 v 6.718 ' + + 'h 17 v -6.718 c 0,-2.2980005 -5.5279996,-4.5950005 -6.0509996,-4.7760005 z' + + 'm -8,6 l 0,5.5 m 11,0 l 0,-5' + }, + 'TASK_TYPE_USER_2': { + d: 'm {mx},{my} m 2.162,1.009 c 0,2.4470005 -2.158,4.4310005 -4.821,4.4310005 ' + + '-2.66499998,0 -4.822,-1.981 -4.822,-4.4310005 ' + }, + 'TASK_TYPE_USER_3': { + d: 'm {mx},{my} m -6.9,-3.80 c 0,0 2.25099998,-2.358 4.27399998,-1.177 2.024,1.181 4.221,1.537 ' + + '4.124,0.965 -0.098,-0.57 -0.117,-3.79099999 -4.191,-4.13599999 -3.57499998,0.001 ' + + '-4.20799998,3.36699999 -4.20699998,4.34799999 z' + }, + 'TASK_TYPE_MANUAL': { + d: 'm {mx},{my} c 0.234,-0.01 5.604,0.008 8.029,0.004 0.808,0 1.271,-0.172 1.417,-0.752 0.227,-0.898 ' + + '-0.334,-1.314 -1.338,-1.316 -2.467,-0.01 -7.886,-0.004 -8.108,-0.004 -0.014,-0.079 0.016,-0.533 0,-0.61 ' + + '0.195,-0.042 8.507,0.006 9.616,0.002 0.877,-0.007 1.35,-0.438 1.353,-1.208 0.003,-0.768 -0.479,-1.09 ' + + '-1.35,-1.091 -2.968,-0.002 -9.619,-0.013 -9.619,-0.013 v -0.591 c 0,0 5.052,-0.016 7.225,-0.016 ' + + '0.888,-0.002 1.354,-0.416 1.351,-1.193 -0.006,-0.761 -0.492,-1.196 -1.361,-1.196 -3.473,-0.005 ' + + '-10.86,-0.003 -11.0829995,-0.003 -0.022,-0.047 -0.045,-0.094 -0.069,-0.139 0.3939995,-0.319 ' + + '2.0409995,-1.626 2.4149995,-2.017 0.469,-0.4870005 0.519,-1.1650005 0.162,-1.6040005 -0.414,-0.511 ' + + '-0.973,-0.5 -1.48,-0.236 -1.4609995,0.764 -6.5999995,3.6430005 -7.7329995,4.2710005 -0.9,0.499 ' + + '-1.516,1.253 -1.882,2.19 -0.37000002,0.95 -0.17,2.01 -0.166,2.979 0.004,0.718 -0.27300002,1.345 ' + + '-0.055,2.063 0.629,2.087 2.425,3.312 4.859,3.318 4.6179995,0.014 9.2379995,-0.139 13.8569995,-0.158 ' + + '0.755,-0.004 1.171,-0.301 1.182,-1.033 0.012,-0.754 -0.423,-0.969 -1.183,-0.973 -1.778,-0.01 ' + + '-5.824,-0.004 -6.04,-0.004 10e-4,-0.084 0.003,-0.586 10e-4,-0.67 z' + }, + 'TASK_TYPE_INSTANTIATING_SEND': { + d: 'm {mx},{my} l 0,8.4 l 12.6,0 l 0,-8.4 z l 6.3,3.6 l 6.3,-3.6' + }, + 'TASK_TYPE_SERVICE': { + d: 'm {mx},{my} v -1.71335 c 0.352326,-0.0705 0.703932,-0.17838 1.047628,-0.32133 ' + + '0.344416,-0.14465 0.665822,-0.32133 0.966377,-0.52145 l 1.19431,1.18005 1.567487,-1.57688 ' + + '-1.195028,-1.18014 c 0.403376,-0.61394 0.683079,-1.29908 0.825447,-2.01824 l 1.622133,-0.01 ' + + 'v -2.2196 l -1.636514,0.01 c -0.07333,-0.35153 -0.178319,-0.70024 -0.323564,-1.04372 ' + + '-0.145244,-0.34406 -0.321407,-0.6644 -0.522735,-0.96217 l 1.131035,-1.13631 -1.583305,-1.56293 ' + + '-1.129598,1.13589 c -0.614052,-0.40108 -1.302883,-0.68093 -2.022633,-0.82247 l 0.0093,-1.61852 ' + + 'h -2.241173 l 0.0042,1.63124 c -0.353763,0.0736 -0.705369,0.17977 -1.049785,0.32371 -0.344415,0.14437 ' + + '-0.665102,0.32092 -0.9635006,0.52046 l -1.1698628,-1.15823 -1.5667691,1.5792 1.1684265,1.15669 ' + + 'c -0.4026573,0.61283 -0.68308,1.29797 -0.8247287,2.01713 l -1.6588041,0.003 v 2.22174 ' + + 'l 1.6724648,-0.006 c 0.073327,0.35077 0.1797598,0.70243 0.3242851,1.04472 0.1452428,0.34448 ' + + '0.3214064,0.6644 0.5227339,0.96066 l -1.1993431,1.19723 1.5840256,1.56011 1.1964668,-1.19348 ' + + 'c 0.6140517,0.40346 1.3028827,0.68232 2.0233517,0.82331 l 7.19e-4,1.69892 h 2.226848 z ' + + 'm 0.221462,-3.9957 c -1.788948,0.7502 -3.8576,-0.0928 -4.6097055,-1.87438 -0.7521065,-1.78321 ' + + '0.090598,-3.84627 1.8802645,-4.59604 1.78823,-0.74936 3.856881,0.0929 4.608987,1.87437 ' + + '0.752106,1.78165 -0.0906,3.84612 -1.879546,4.59605 z' + }, + 'TASK_TYPE_SERVICE_FILL': { + d: 'm {mx},{my} c -1.788948,0.7502 -3.8576,-0.0928 -4.6097055,-1.87438 -0.7521065,-1.78321 ' + + '0.090598,-3.84627 1.8802645,-4.59604 1.78823,-0.74936 3.856881,0.0929 4.608987,1.87437 ' + + '0.752106,1.78165 -0.0906,3.84612 -1.879546,4.59605 z' + }, + 'TASK_TYPE_BUSINESS_RULE_HEADER': { + d: 'm {mx},{my} 0,4 20,0 0,-4 z' + }, + 'TASK_TYPE_BUSINESS_RULE_MAIN': { + d: 'm {mx},{my} 0,12 20,0 0,-12 z' + + 'm 0,8 l 20,0 ' + + 'm -13,-4 l 0,8' + }, + 'MESSAGE_FLOW_MARKER': { + d: 'm {mx},{my} m -10.5 ,-7 l 0,14 l 21,0 l 0,-14 z l 10.5,6 l 10.5,-6' + } + }; + + /** + * Return raw path for the given ID. + * + * @param {string} pathId + * + * @return {string} raw path + */ + this.getRawPath = function getRawPath(pathId) { + return this.pathMap[pathId].d; + }; + + /** + * Scales the path to the given height and width. + *

Use case

+ *

Use case is to scale the content of elements (event, gateways) based + * on the element bounding box's size. + *

+ *

Why not transform

+ *

Scaling a path with transform() will also scale the stroke and IE does not support + * the option 'non-scaling-stroke' to prevent this. + * Also there are use cases where only some parts of a path should be + * scaled.

+ * + * @param {string} pathId The ID of the path. + * @param {Object} param

+ * Example param object scales the path to 60% size of the container (data.width, data.height). + *

+     *   {
+     *     xScaleFactor: 0.6,
+     *     yScaleFactor:0.6,
+     *     containerWidth: data.width,
+     *     containerHeight: data.height,
+     *     position: {
+     *       mx: 0.46,
+     *       my: 0.2,
+     *     }
+     *   }
+     *   
+ *
    + *
  • targetpathwidth = xScaleFactor * containerWidth
  • + *
  • targetpathheight = yScaleFactor * containerHeight
  • + *
  • Position is used to set the starting coordinate of the path. M is computed: + *
      + *
    • position.x * containerWidth
    • + *
    • position.y * containerHeight
    • + *
    + * Center of the container
     position: {
    +     *       mx: 0.5,
    +     *       my: 0.5,
    +     *     }
    + * Upper left corner of the container + *
     position: {
    +     *       mx: 0.0,
    +     *       my: 0.0,
    +     *     }
    + *
  • + *
+ *

+ * + * @return {string} scaled path + */ + this.getScaledPath = function getScaledPath(pathId, param) { + var rawPath = this.pathMap[pathId]; + + // positioning + // compute the start point of the path + var mx, my; + + if (param.abspos) { + mx = param.abspos.x; + my = param.abspos.y; + } else { + mx = param.containerWidth * param.position.mx; + my = param.containerHeight * param.position.my; + } + + var coordinates = {}; // map for the scaled coordinates + if (param.position) { + + // path + var heightRatio = (param.containerHeight / rawPath.height) * param.yScaleFactor; + var widthRatio = (param.containerWidth / rawPath.width) * param.xScaleFactor; + + + // Apply height ratio + for (var heightIndex = 0; heightIndex < rawPath.heightElements.length; heightIndex++) { + coordinates['y' + heightIndex] = rawPath.heightElements[heightIndex] * heightRatio; + } + + // Apply width ratio + for (var widthIndex = 0; widthIndex < rawPath.widthElements.length; widthIndex++) { + coordinates['x' + widthIndex] = rawPath.widthElements[widthIndex] * widthRatio; + } + } + + // Apply value to raw path + var path = format( + rawPath.d, { + mx: mx, + my: my, + e: coordinates + } + ); + return path; + }; + } + + // helpers ////////////////////// + + // copied and adjusted from https://github.com/adobe-webplatform/Snap.svg/blob/master/src/svg.js + var tokenRegex = /\{([^{}]+)\}/g, + objNotationRegex = /(?:(?:^|\.)(.+?)(?=\[|\.|$|\()|\[('|")(.+?)\2\])(\(\))?/g; // matches .xxxxx or ["xxxxx"] to run over object properties + + function replacer(all, key, obj) { + var res = obj; + key.replace(objNotationRegex, function(all, name, quote, quotedName, isFunc) { + name = name || quotedName; + if (res) { + if (name in res) { + res = res[name]; + } + typeof res == 'function' && isFunc && (res = res()); + } + }); + res = (res == null || res == obj ? all : res) + ''; + + return res; + } + + function format(str, obj) { + return String(str).replace(tokenRegex, function(all, key) { + return replacer(all, key, obj); + }); + } + + var DrawModule$1 = { + __init__: [ 'bpmnRenderer' ], + bpmnRenderer: [ 'type', BpmnRenderer ], + textRenderer: [ 'type', TextRenderer ], + pathMap: [ 'type', PathMap ] + }; + + /** + * @typedef { { + * [key: string]: string; + * } } TranslateReplacements + */ + + /** + * A simple translation stub to be used for multi-language support + * in diagrams. Can be easily replaced with a more sophisticated + * solution. + * + * @example + * + * ```javascript + * // use it inside any diagram component by injecting `translate`. + * + * function MyService(translate) { + * alert(translate('HELLO {you}', { you: 'You!' })); + * } + * ``` + * + * @param {string} template to interpolate + * @param {TranslateReplacements} [replacements] a map with substitutes + * + * @return {string} the translated string + */ + function translate(template, replacements) { + + replacements = replacements || {}; + + return template.replace(/{([^}]+)}/g, function(_, key) { + return replacements[key] || '{' + key + '}'; + }); + } + + /** + * @type { import('didi').ModuleDeclaration } + */ + var TranslateModule = { + translate: [ 'value', translate ] + }; + + /** + * @param {Point} point + * + * @returns {Point} + */ + function roundPoint(point) { + + return { + x: Math.round(point.x), + y: Math.round(point.y) + }; + } + + + /** + * Convert the given bounds to a { top, left, bottom, right } descriptor. + * + * @param {Point|Rect} bounds + * + * @return {RectTRBL} + */ + function asTRBL(bounds) { + return { + top: bounds.y, + right: bounds.x + (bounds.width || 0), + bottom: bounds.y + (bounds.height || 0), + left: bounds.x + }; + } + + + /** + * Convert a { top, left, bottom, right } to an objects bounds. + * + * @param {RectTRBL} trbl + * + * @return {Rect} + */ + function asBounds(trbl) { + return { + x: trbl.left, + y: trbl.top, + width: trbl.right - trbl.left, + height: trbl.bottom - trbl.top + }; + } + + + /** + * Get the mid of the given bounds or point. + * + * @param {Point|Rect} bounds + * + * @return {Point} + */ + function getBoundsMid(bounds) { + return roundPoint({ + x: bounds.x + (bounds.width || 0) / 2, + y: bounds.y + (bounds.height || 0) / 2 + }); + } + + + /** + * Get the mid of the given Connection. + * + * @param {Connection} connection + * + * @return {Point} + */ + function getConnectionMid(connection) { + var waypoints = connection.waypoints; + + // calculate total length and length of each segment + var parts = waypoints.reduce(function(parts, point, index) { + + var lastPoint = waypoints[index - 1]; + + if (lastPoint) { + var lastPart = parts[parts.length - 1]; + + var startLength = lastPart && lastPart.endLength || 0; + var length = distance(lastPoint, point); + + parts.push({ + start: lastPoint, + end: point, + startLength: startLength, + endLength: startLength + length, + length: length + }); + } + + return parts; + }, []); + + var totalLength = parts.reduce(function(length, part) { + return length + part.length; + }, 0); + + // find which segement contains middle point + var midLength = totalLength / 2; + + var i = 0; + var midSegment = parts[i]; + + while (midSegment.endLength < midLength) { + midSegment = parts[++i]; + } + + // calculate relative position on mid segment + var segmentProgress = (midLength - midSegment.startLength) / midSegment.length; + + var midPoint = { + x: midSegment.start.x + (midSegment.end.x - midSegment.start.x) * segmentProgress, + y: midSegment.start.y + (midSegment.end.y - midSegment.start.y) * segmentProgress + }; + + return midPoint; + } + + + /** + * Get the mid of the given Element. + * + * @param {Element} element + * + * @return {Point} + */ + function getMid(element) { + if (isConnection(element)) { + return getConnectionMid(element); + } + + return getBoundsMid(element); + } + + // helpers ////////////////////// + + function distance(a, b) { + return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2)); + } + + function elementToString(e) { + if (!e) { + return ''; + } + + return '<' + e.$type + (e.id ? ' id="' + e.id : '') + '" />'; + } + + /** + * @typedef {import('diagram-js/lib/core/Canvas').default} Canvas + * @typedef {import('diagram-js/lib/core/ElementRegistry').default} ElementRegistry + * @typedef {import('diagram-js/lib/core/EventBus').default} EventBus + * @typedef {import('diagram-js/lib/i18n/translate/translate').default} Translate + * + * @typedef {import('../features/modeling/ElementFactory').default} ElementFactory + * @typedef {import('../draw/TextRenderer').default} TextRenderer + * + * @typedef {import('../model/Types').Element} Element + * @typedef {import('../model/Types').Label} Label + * @typedef {import('../model/Types').Shape} Shape + * @typedef {import('../model/Types').Connection} Connection + * @typedef {import('../model/Types').Root} Root + * @typedef {import('../model/Types').ModdleElement} ModdleElement + */ + + /** + * @param {ModdleElement} semantic + * @param {ModdleElement} di + * @param {Object} [attrs=null] + * + * @return {Object} + */ + function elementData(semantic, di, attrs) { + return assign$1({ + id: semantic.id, + type: semantic.$type, + businessObject: semantic, + di: di + }, attrs); + } + + function getWaypoints(di, source, target) { + + var waypoints = di.waypoint; + + if (!waypoints || waypoints.length < 2) { + return [ getMid(source), getMid(target) ]; + } + + return waypoints.map(function(p) { + return { x: p.x, y: p.y }; + }); + } + + function notYetDrawn(translate, semantic, refSemantic, property) { + return new Error(translate('element {element} referenced by {referenced}#{property} not yet drawn', { + element: elementToString(refSemantic), + referenced: elementToString(semantic), + property: property + })); + } + + + /** + * An importer that adds bpmn elements to the canvas + * + * @param {EventBus} eventBus + * @param {Canvas} canvas + * @param {ElementFactory} elementFactory + * @param {ElementRegistry} elementRegistry + * @param {Function} translate + * @param {TextRenderer} textRenderer + */ + function BpmnImporter( + eventBus, canvas, elementFactory, + elementRegistry, translate, textRenderer) { + + this._eventBus = eventBus; + this._canvas = canvas; + this._elementFactory = elementFactory; + this._elementRegistry = elementRegistry; + this._translate = translate; + this._textRenderer = textRenderer; + } + + BpmnImporter.$inject = [ + 'eventBus', + 'canvas', + 'elementFactory', + 'elementRegistry', + 'translate', + 'textRenderer' + ]; + + + /** + * Add a BPMN element (semantic) to the canvas making it a child of the + * given parent. + * + * @param {ModdleElement} semantic + * @param {ModdleElement} di + * @param {Shape} parentElement + * + * @return {Shape | Root | Connection} + */ + BpmnImporter.prototype.add = function(semantic, di, parentElement) { + var element, + translate = this._translate, + hidden; + + var parentIndex; + + // ROOT ELEMENT + // handle the special case that we deal with a + // invisible root element (process, subprocess or collaboration) + if (is$1(di, 'bpmndi:BPMNPlane')) { + + var attrs = is$1(semantic, 'bpmn:SubProcess') + ? { id: semantic.id + '_plane' } + : {}; + + // add a virtual element (not being drawn) + element = this._elementFactory.createRoot(elementData(semantic, di, attrs)); + + this._canvas.addRootElement(element); + } + + // SHAPE + else if (is$1(di, 'bpmndi:BPMNShape')) { + + var collapsed = !isExpanded(semantic, di), + isFrame = isFrameElement$1(semantic); + + hidden = parentElement && (parentElement.hidden || parentElement.collapsed); + + var bounds = di.bounds; + + element = this._elementFactory.createShape(elementData(semantic, di, { + collapsed: collapsed, + hidden: hidden, + x: Math.round(bounds.x), + y: Math.round(bounds.y), + width: Math.round(bounds.width), + height: Math.round(bounds.height), + isFrame: isFrame + })); + + if (is$1(semantic, 'bpmn:BoundaryEvent')) { + this._attachBoundary(semantic, element); + } + + // insert lanes behind other flow nodes (cf. #727) + if (is$1(semantic, 'bpmn:Lane')) { + parentIndex = 0; + } + + if (is$1(semantic, 'bpmn:DataStoreReference')) { + + // check whether data store is inside our outside of its semantic parent + if (!isPointInsideBBox(parentElement, getMid(bounds))) { + parentElement = this._canvas.findRoot(parentElement); + } + } + + this._canvas.addShape(element, parentElement, parentIndex); + } + + // CONNECTION + else if (is$1(di, 'bpmndi:BPMNEdge')) { + + var source = this._getSource(semantic), + target = this._getTarget(semantic); + + hidden = parentElement && (parentElement.hidden || parentElement.collapsed); + + element = this._elementFactory.createConnection(elementData(semantic, di, { + hidden: hidden, + source: source, + target: target, + waypoints: getWaypoints(di, source, target) + })); + + if (is$1(semantic, 'bpmn:DataAssociation')) { + + // render always on top; this ensures DataAssociations + // are rendered correctly across different "hacks" people + // love to model such as cross participant / sub process + // associations + parentElement = this._canvas.findRoot(parentElement); + } + + this._canvas.addConnection(element, parentElement, parentIndex); + } else { + throw new Error(translate('unknown di {di} for element {semantic}', { + di: elementToString(di), + semantic: elementToString(semantic) + })); + } + + // (optional) LABEL + if (isLabelExternal(semantic) && getLabel(element)) { + this.addLabel(semantic, di, element); + } + + + this._eventBus.fire('bpmnElement.added', { element: element }); + + return element; + }; + + + /** + * Attach a boundary element to the given host. + * + * @param {ModdleElement} boundarySemantic + * @param {Shape} boundaryElement + */ + BpmnImporter.prototype._attachBoundary = function(boundarySemantic, boundaryElement) { + var translate = this._translate; + var hostSemantic = boundarySemantic.attachedToRef; + + if (!hostSemantic) { + throw new Error(translate('missing {semantic}#attachedToRef', { + semantic: elementToString(boundarySemantic) + })); + } + + var host = this._elementRegistry.get(hostSemantic.id), + attachers = host && host.attachers; + + if (!host) { + throw notYetDrawn(translate, boundarySemantic, hostSemantic, 'attachedToRef'); + } + + // wire element.host <> host.attachers + boundaryElement.host = host; + + if (!attachers) { + host.attachers = attachers = []; + } + + if (attachers.indexOf(boundaryElement) === -1) { + attachers.push(boundaryElement); + } + }; + + + /** + * Add a label to a given element. + * + * @param {ModdleElement} semantic + * @param {ModdleElement} di + * @param {Element} element + * + * @return {Label} + */ + BpmnImporter.prototype.addLabel = function(semantic, di, element) { + var bounds, + text, + label; + + bounds = getExternalLabelBounds(di, element); + + text = getLabel(element); + + if (text) { + + // get corrected bounds from actual layouted text + bounds = this._textRenderer.getExternalLabelBounds(bounds, text); + } + + label = this._elementFactory.createLabel(elementData(semantic, di, { + id: semantic.id + '_label', + labelTarget: element, + type: 'label', + hidden: element.hidden || !getLabel(element), + x: Math.round(bounds.x), + y: Math.round(bounds.y), + width: Math.round(bounds.width), + height: Math.round(bounds.height) + })); + + return this._canvas.addShape(label, element.parent); + }; + + /** + * Get the source or target of the given connection. + * + * @param {ModdleElement} semantic + * @param {'source' | 'target'} side + * + * @return {Element} + */ + BpmnImporter.prototype._getConnectedElement = function(semantic, side) { + + var element, + refSemantic, + type = semantic.$type, + translate = this._translate; + + refSemantic = semantic[side + 'Ref']; + + // handle mysterious isMany DataAssociation#sourceRef + if (side === 'source' && type === 'bpmn:DataInputAssociation') { + refSemantic = refSemantic && refSemantic[0]; + } + + // fix source / target for DataInputAssociation / DataOutputAssociation + if (side === 'source' && type === 'bpmn:DataOutputAssociation' || + side === 'target' && type === 'bpmn:DataInputAssociation') { + + refSemantic = semantic.$parent; + } + + element = refSemantic && this._getElement(refSemantic); + + if (element) { + return element; + } + + if (refSemantic) { + throw notYetDrawn(translate, semantic, refSemantic, side + 'Ref'); + } else { + throw new Error(translate('{semantic}#{side} Ref not specified', { + semantic: elementToString(semantic), + side: side + })); + } + }; + + BpmnImporter.prototype._getSource = function(semantic) { + return this._getConnectedElement(semantic, 'source'); + }; + + BpmnImporter.prototype._getTarget = function(semantic) { + return this._getConnectedElement(semantic, 'target'); + }; + + + BpmnImporter.prototype._getElement = function(semantic) { + return this._elementRegistry.get(semantic.id); + }; + + + // helpers //////////////////// + + function isPointInsideBBox(bbox, point) { + var x = point.x, + y = point.y; + + return x >= bbox.x && + x <= bbox.x + bbox.width && + y >= bbox.y && + y <= bbox.y + bbox.height; + } + + function isFrameElement$1(semantic) { + return is$1(semantic, 'bpmn:Group'); + } + + var ImportModule = { + __depends__: [ + TranslateModule + ], + bpmnImporter: [ 'type', BpmnImporter ] + }; + + var CoreModule$1 = { + __depends__: [ + DrawModule$1, + ImportModule + ] + }; + + /** + * @typedef {import('../util/Types').Point} Point + */ + + /** + * @param {import('../core/EventBus').Event} event + * + * @return {Event} + */ + function getOriginal(event) { + return event.originalEvent || event.srcEvent; + } + + /** + * @param {MouseEvent} event + * @param {string} button + * + * @return {boolean} + */ + function isButton(event, button) { + return (getOriginal(event) || event).button === button; + } + + /** + * @param {MouseEvent} event + * + * @return {boolean} + */ + function isPrimaryButton(event) { + + // button === 0 -> left áka primary mouse button + return isButton(event, 0); + } + + /** + * @param {MouseEvent} event + * + * @return {boolean} + */ + function isAuxiliaryButton(event) { + + // button === 1 -> auxiliary áka wheel button + return isButton(event, 1); + } + + /** + * @param {MouseEvent} event + * + * @return {boolean} + */ + function hasSecondaryModifier(event) { + var originalEvent = getOriginal(event) || event; + + return isPrimaryButton(event) && originalEvent.shiftKey; + } + + /** + * @typedef {import('../../model/Types').Element} Element + * + * @typedef {import('../../core/ElementRegistry').default} ElementRegistry + * @typedef {import('../../core/EventBus').default} EventBus + * @typedef {import('../../draw/Styles').default} Styles + * + * @typedef {import('../../util/Types').Point} Point + */ + + function allowAll(event) { return true; } + + function allowPrimaryAndAuxiliary(event) { + return isPrimaryButton(event) || isAuxiliaryButton(event); + } + + var LOW_PRIORITY$3 = 500; + + + /** + * A plugin that provides interaction events for diagram elements. + * + * It emits the following events: + * + * * element.click + * * element.contextmenu + * * element.dblclick + * * element.hover + * * element.mousedown + * * element.mousemove + * * element.mouseup + * * element.out + * + * Each event is a tuple { element, gfx, originalEvent }. + * + * Canceling the event via Event#preventDefault() + * prevents the original DOM operation. + * + * @param {EventBus} eventBus + * @param {ElementRegistry} elementRegistry + * @param {Styles} styles + */ + function InteractionEvents(eventBus, elementRegistry, styles) { + + var self = this; + + /** + * Fire an interaction event. + * + * @param {string} type local event name, e.g. element.click. + * @param {MouseEvent|TouchEvent} event native event + * @param {Element} [element] the diagram element to emit the event on; + * defaults to the event target + */ + function fire(type, event, element) { + + if (isIgnored(type, event)) { + return; + } + + var target, gfx, returnValue; + + if (!element) { + target = event.delegateTarget || event.target; + + if (target) { + gfx = target; + element = elementRegistry.get(gfx); + } + } else { + gfx = elementRegistry.getGraphics(element); + } + + if (!gfx || !element) { + return; + } + + returnValue = eventBus.fire(type, { + element: element, + gfx: gfx, + originalEvent: event + }); + + if (returnValue === false) { + event.stopPropagation(); + event.preventDefault(); + } + } + + // TODO(nikku): document this + var handlers = {}; + + function mouseHandler(localEventName) { + return handlers[localEventName]; + } + + function isIgnored(localEventName, event) { + + var filter = ignoredFilters[localEventName] || isPrimaryButton; + + // only react on left mouse button interactions + // except for interaction events that are enabled + // for secundary mouse button + return !filter(event); + } + + var bindings = { + click: 'element.click', + contextmenu: 'element.contextmenu', + dblclick: 'element.dblclick', + mousedown: 'element.mousedown', + mousemove: 'element.mousemove', + mouseover: 'element.hover', + mouseout: 'element.out', + mouseup: 'element.mouseup', + }; + + var ignoredFilters = { + 'element.contextmenu': allowAll, + 'element.mousedown': allowPrimaryAndAuxiliary, + 'element.mouseup': allowPrimaryAndAuxiliary, + 'element.click': allowPrimaryAndAuxiliary, + 'element.dblclick': allowPrimaryAndAuxiliary + }; + + + // manual event trigger ////////// + + /** + * Trigger an interaction event (based on a native dom event) + * on the target shape or connection. + * + * @param {string} eventName the name of the triggered DOM event + * @param {MouseEvent|TouchEvent} event + * @param {Element} targetElement + */ + function triggerMouseEvent(eventName, event, targetElement) { + + // i.e. element.mousedown... + var localEventName = bindings[eventName]; + + if (!localEventName) { + throw new Error('unmapped DOM event name <' + eventName + '>'); + } + + return fire(localEventName, event, targetElement); + } + + + var ELEMENT_SELECTOR = 'svg, .djs-element'; + + // event handling /////// + + function registerEvent(node, event, localEvent, ignoredFilter) { + + var handler = handlers[localEvent] = function(event) { + fire(localEvent, event); + }; + + if (ignoredFilter) { + ignoredFilters[localEvent] = ignoredFilter; + } + + handler.$delegate = delegate.bind(node, ELEMENT_SELECTOR, event, handler); + } + + function unregisterEvent(node, event, localEvent) { + + var handler = mouseHandler(localEvent); + + if (!handler) { + return; + } + + delegate.unbind(node, event, handler.$delegate); + } + + function registerEvents(svg) { + forEach$1(bindings, function(val, key) { + registerEvent(svg, key, val); + }); + } + + function unregisterEvents(svg) { + forEach$1(bindings, function(val, key) { + unregisterEvent(svg, key, val); + }); + } + + eventBus.on('canvas.destroy', function(event) { + unregisterEvents(event.svg); + }); + + eventBus.on('canvas.init', function(event) { + registerEvents(event.svg); + }); + + + // hit box updating //////////////// + + eventBus.on([ 'shape.added', 'connection.added' ], function(event) { + var element = event.element, + gfx = event.gfx; + + eventBus.fire('interactionEvents.createHit', { element: element, gfx: gfx }); + }); + + // Update djs-hit on change. + // A low priortity is necessary, because djs-hit of labels has to be updated + // after the label bounds have been updated in the renderer. + eventBus.on([ + 'shape.changed', + 'connection.changed' + ], LOW_PRIORITY$3, function(event) { + + var element = event.element, + gfx = event.gfx; + + eventBus.fire('interactionEvents.updateHit', { element: element, gfx: gfx }); + }); + + eventBus.on('interactionEvents.createHit', LOW_PRIORITY$3, function(event) { + var element = event.element, + gfx = event.gfx; + + self.createDefaultHit(element, gfx); + }); + + eventBus.on('interactionEvents.updateHit', function(event) { + var element = event.element, + gfx = event.gfx; + + self.updateDefaultHit(element, gfx); + }); + + + // hit styles //////////// + + var STROKE_HIT_STYLE = createHitStyle('djs-hit djs-hit-stroke'); + + var CLICK_STROKE_HIT_STYLE = createHitStyle('djs-hit djs-hit-click-stroke'); + + var ALL_HIT_STYLE = createHitStyle('djs-hit djs-hit-all'); + + var NO_MOVE_HIT_STYLE = createHitStyle('djs-hit djs-hit-no-move'); + + var HIT_TYPES = { + 'all': ALL_HIT_STYLE, + 'click-stroke': CLICK_STROKE_HIT_STYLE, + 'stroke': STROKE_HIT_STYLE, + 'no-move': NO_MOVE_HIT_STYLE + }; + + function createHitStyle(classNames, attrs) { + + attrs = assign$1({ + stroke: 'white', + strokeWidth: 15 + }, attrs || {}); + + return styles.cls(classNames, [ 'no-fill', 'no-border' ], attrs); + } + + + // style helpers /////////////// + + function applyStyle(hit, type) { + + var attrs = HIT_TYPES[type]; + + if (!attrs) { + throw new Error('invalid hit type <' + type + '>'); + } + + attr$1(hit, attrs); + + return hit; + } + + function appendHit(gfx, hit) { + append(gfx, hit); + } + + + // API + + /** + * Remove hints on the given graphics. + * + * @param {SVGElement} gfx + */ + this.removeHits = function(gfx) { + var hits = all('.djs-hit', gfx); + + forEach$1(hits, remove$2); + }; + + /** + * Create default hit for the given element. + * + * @param {Element} element + * @param {SVGElement} gfx + * + * @return {SVGElement} created hit + */ + this.createDefaultHit = function(element, gfx) { + var waypoints = element.waypoints, + isFrame = element.isFrame, + boxType; + + if (waypoints) { + return this.createWaypointsHit(gfx, waypoints); + } else { + + boxType = isFrame ? 'stroke' : 'all'; + + return this.createBoxHit(gfx, boxType, { + width: element.width, + height: element.height + }); + } + }; + + /** + * Create hits for the given waypoints. + * + * @param {SVGElement} gfx + * @param {Point[]} waypoints + * + * @return {SVGElement} + */ + this.createWaypointsHit = function(gfx, waypoints) { + + var hit = createLine(waypoints); + + applyStyle(hit, 'stroke'); + + appendHit(gfx, hit); + + return hit; + }; + + /** + * Create hits for a box. + * + * @param {SVGElement} gfx + * @param {string} type + * @param {Object} attrs + * + * @return {SVGElement} + */ + this.createBoxHit = function(gfx, type, attrs) { + + attrs = assign$1({ + x: 0, + y: 0 + }, attrs); + + var hit = create$1('rect'); + + applyStyle(hit, type); + + attr$1(hit, attrs); + + appendHit(gfx, hit); + + return hit; + }; + + /** + * Update default hit of the element. + * + * @param {Element} element + * @param {SVGElement} gfx + * + * @return {SVGElement} updated hit + */ + this.updateDefaultHit = function(element, gfx) { + + var hit = query('.djs-hit', gfx); + + if (!hit) { + return; + } + + if (element.waypoints) { + updateLine(hit, element.waypoints); + } else { + attr$1(hit, { + width: element.width, + height: element.height + }); + } + + return hit; + }; + + this.fire = fire; + + this.triggerMouseEvent = triggerMouseEvent; + + this.mouseHandler = mouseHandler; + + this.registerEvent = registerEvent; + this.unregisterEvent = unregisterEvent; + } + + + InteractionEvents.$inject = [ + 'eventBus', + 'elementRegistry', + 'styles' + ]; + + + /** + * An event indicating that the mouse hovered over an element + * + * @event element.hover + * + * @type {Object} + * @property {Element} element + * @property {SVGElement} gfx + * @property {Event} originalEvent + */ + + /** + * An event indicating that the mouse has left an element + * + * @event element.out + * + * @type {Object} + * @property {Element} element + * @property {SVGElement} gfx + * @property {Event} originalEvent + */ + + /** + * An event indicating that the mouse has clicked an element + * + * @event element.click + * + * @type {Object} + * @property {Element} element + * @property {SVGElement} gfx + * @property {Event} originalEvent + */ + + /** + * An event indicating that the mouse has double clicked an element + * + * @event element.dblclick + * + * @type {Object} + * @property {Element} element + * @property {SVGElement} gfx + * @property {Event} originalEvent + */ + + /** + * An event indicating that the mouse has gone down on an element. + * + * @event element.mousedown + * + * @type {Object} + * @property {Element} element + * @property {SVGElement} gfx + * @property {Event} originalEvent + */ + + /** + * An event indicating that the mouse has gone up on an element. + * + * @event element.mouseup + * + * @type {Object} + * @property {Element} element + * @property {SVGElement} gfx + * @property {Event} originalEvent + */ + + /** + * An event indicating that the context menu action is triggered + * via mouse or touch controls. + * + * @event element.contextmenu + * + * @type {Object} + * @property {Element} element + * @property {SVGElement} gfx + * @property {Event} originalEvent + */ + + /** + * @type { import('didi').ModuleDeclaration } + */ + var InteractionEventsModule = { + __init__: [ 'interactionEvents' ], + interactionEvents: [ 'type', InteractionEvents ] + }; + + /** + * Returns the surrounding bbox for all elements in + * the array or the element primitive. + * + * @param {Element|Element[]} elements + * @param {boolean} [stopRecursion=false] + * + * @return {Rect} + */ + function getBBox(elements, stopRecursion) { + + stopRecursion = !!stopRecursion; + if (!isArray$2(elements)) { + elements = [ elements ]; + } + + var minX, + minY, + maxX, + maxY; + + forEach$1(elements, function(element) { + + // If element is a connection the bbox must be computed first + var bbox = element; + if (element.waypoints && !stopRecursion) { + bbox = getBBox(element.waypoints, true); + } + + var x = bbox.x, + y = bbox.y, + height = bbox.height || 0, + width = bbox.width || 0; + + if (x < minX || minX === undefined) { + minX = x; + } + if (y < minY || minY === undefined) { + minY = y; + } + + if ((x + width) > maxX || maxX === undefined) { + maxX = x + width; + } + if ((y + height) > maxY || maxY === undefined) { + maxY = y + height; + } + }); + + return { + x: minX, + y: minY, + height: maxY - minY, + width: maxX - minX + }; + } + + /** + * Get the element's type + * + * @param {Element} element + * + * @return {'connection' | 'shape' | 'root'} + */ + function getType(element) { + + if ('waypoints' in element) { + return 'connection'; + } + + if ('x' in element) { + return 'shape'; + } + + return 'root'; + } + + /** + * @param {Element} element + * + * @return {boolean} + */ + function isFrameElement(element) { + return !!(element && element.isFrame); + } + + var LOW_PRIORITY$2 = 500; + + var DEFAULT_PRIORITY$2 = 1000; + + /** + * @typedef {import('../../model/Types').Element} Element + * + * @typedef {import('../../core/EventBus').default} EventBus + * @typedef {import('../../draw/Styles').default} Styles + */ + + /** + * @class + * + * A plugin that adds an outline to shapes and connections that may be activated and styled + * via CSS classes. + * + * @param {EventBus} eventBus + * @param {Styles} styles + */ + function Outline(eventBus, styles) { + + this._eventBus = eventBus; + + this.offset = 5; + + var OUTLINE_STYLE = styles.cls('djs-outline', [ 'no-fill' ]); + + var self = this; + + /** + * @param {SVGElement} gfx + * + * @return {SVGElement} outline + */ + function createOutline(gfx) { + var outline = create$1('rect'); + + attr$1(outline, assign$1({ + x: 0, + y: 0, + rx: 4, + width: 100, + height: 100 + }, OUTLINE_STYLE)); + + return outline; + } + + // A low priortity is necessary, because outlines of labels have to be updated + // after the label bounds have been updated in the renderer. + eventBus.on([ 'shape.added', 'shape.changed' ], LOW_PRIORITY$2, function(event) { + var element = event.element, + gfx = event.gfx; + + var outline = query('.djs-outline', gfx); + + if (!outline) { + outline = self.getOutline(element) || createOutline(); + append(gfx, outline); + } + + self.updateShapeOutline(outline, element); + }); + + eventBus.on([ 'connection.added', 'connection.changed' ], function(event) { + var element = event.element, + gfx = event.gfx; + + var outline = query('.djs-outline', gfx); + + if (!outline) { + outline = createOutline(); + append(gfx, outline); + } + + self.updateConnectionOutline(outline, element); + }); + } + + + /** + * Updates the outline of a shape respecting the dimension of the + * element and an outline offset. + * + * @param {SVGElement} outline + * @param {Element} element + */ + Outline.prototype.updateShapeOutline = function(outline, element) { + + var updated = false; + var providers = this._getProviders(); + + if (providers.length) { + forEach$1(providers, function(provider) { + updated = updated || provider.updateOutline(element, outline); + }); + } + + if (!updated) { + attr$1(outline, { + x: -this.offset, + y: -this.offset, + width: element.width + this.offset * 2, + height: element.height + this.offset * 2 + }); + } + }; + + /** + * Updates the outline of a connection respecting the bounding box of + * the connection and an outline offset. + * Register an outline provider with the given priority. + * + * @param {SVGElement} outline + * @param {Element} connection + */ + Outline.prototype.updateConnectionOutline = function(outline, connection) { + var bbox = getBBox(connection); + + attr$1(outline, { + x: bbox.x - this.offset, + y: bbox.y - this.offset, + width: bbox.width + this.offset * 2, + height: bbox.height + this.offset * 2 + }); + }; + + /** + * Register an outline provider with the given priority. + * + * @param {number} priority + * @param {OutlineProvider} provider + */ + Outline.prototype.registerProvider = function(priority, provider) { + if (!provider) { + provider = priority; + priority = DEFAULT_PRIORITY$2; + } + + this._eventBus.on('outline.getProviders', priority, function(event) { + event.providers.push(provider); + }); + }; + + /** + * Returns the registered outline providers. + * + * @returns {OutlineProvider[]} + */ + Outline.prototype._getProviders = function() { + var event = this._eventBus.createEvent({ + type: 'outline.getProviders', + providers: [] + }); + + this._eventBus.fire(event); + + return event.providers; + }; + + /** + * Returns the outline for an element. + * + * @param {Element} element + **/ + Outline.prototype.getOutline = function(element) { + var outline; + var providers = this._getProviders(); + + forEach$1(providers, function(provider) { + + if (!isFunction(provider.getOutline)) { + return; + } + + outline = outline || provider.getOutline(element); + }); + + return outline; + }; + + Outline.$inject = [ 'eventBus', 'styles', 'elementRegistry' ]; + + /** + * @type { import('didi').ModuleDeclaration } + */ + var OutlineModule = { + __init__: [ 'outline' ], + outline: [ 'type', Outline ] + }; + + /** + * @typedef {import('../../core/Canvas').default} Canvas + * @typedef {import('../../core/EventBus').default} EventBus + */ + + /** + * A service that offers the current selection in a diagram. + * Offers the api to control the selection, too. + * + * @param {EventBus} eventBus + * @param {Canvas} canvas + */ + function Selection(eventBus, canvas) { + + this._eventBus = eventBus; + this._canvas = canvas; + + /** + * @type {Object[]} + */ + this._selectedElements = []; + + var self = this; + + eventBus.on([ 'shape.remove', 'connection.remove' ], function(e) { + var element = e.element; + self.deselect(element); + }); + + eventBus.on([ 'diagram.clear', 'root.set' ], function(e) { + self.select(null); + }); + } + + Selection.$inject = [ 'eventBus', 'canvas' ]; + + /** + * Deselect an element. + * + * @param {Object} element The element to deselect. + */ + Selection.prototype.deselect = function(element) { + var selectedElements = this._selectedElements; + + var idx = selectedElements.indexOf(element); + + if (idx !== -1) { + var oldSelection = selectedElements.slice(); + + selectedElements.splice(idx, 1); + + this._eventBus.fire('selection.changed', { oldSelection: oldSelection, newSelection: selectedElements }); + } + }; + + /** + * Get the selected elements. + * + * @return {Object[]} The selected elements. + */ + Selection.prototype.get = function() { + return this._selectedElements; + }; + + /** + * Check whether an element is selected. + * + * @param {Object} element The element. + * + * @return {boolean} Whether the element is selected. + */ + Selection.prototype.isSelected = function(element) { + return this._selectedElements.indexOf(element) !== -1; + }; + + + /** + * Select one or many elements. + * + * @param {Object|Object[]} elements The element(s) to select. + * @param {boolean} [add] Whether to add the element(s) to the selected elements. + * Defaults to `false`. + */ + Selection.prototype.select = function(elements, add) { + var selectedElements = this._selectedElements, + oldSelection = selectedElements.slice(); + + if (!isArray$2(elements)) { + elements = elements ? [ elements ] : []; + } + + var canvas = this._canvas; + + var rootElement = canvas.getRootElement(); + + elements = elements.filter(function(element) { + var elementRoot = canvas.findRoot(element); + + return rootElement === elementRoot; + }); + + // selection may be cleared by passing an empty array or null + // to the method + if (add) { + forEach$1(elements, function(element) { + if (selectedElements.indexOf(element) !== -1) { + + // already selected + return; + } else { + selectedElements.push(element); + } + }); + } else { + this._selectedElements = selectedElements = elements.slice(); + } + + this._eventBus.fire('selection.changed', { oldSelection: oldSelection, newSelection: selectedElements }); + }; + + /** + * @typedef {import('../../core/Canvas').default} Canvas + * @typedef {import('../../core/EventBus').default} EventBus + * @typedef {import('./Selection').default} Selection + */ + + var MARKER_HOVER = 'hover', + MARKER_SELECTED = 'selected'; + + var SELECTION_OUTLINE_PADDING = 6; + + + /** + * A plugin that adds a visible selection UI to shapes and connections + * by appending the hover and selected classes to them. + * + * @class + * + * Makes elements selectable, too. + * + * @param {Canvas} canvas + * @param {EventBus} eventBus + * @param {Selection} selection + */ + function SelectionVisuals(canvas, eventBus, selection) { + this._canvas = canvas; + + var self = this; + + this._multiSelectionBox = null; + + function addMarker(e, cls) { + canvas.addMarker(e, cls); + } + + function removeMarker(e, cls) { + canvas.removeMarker(e, cls); + } + + eventBus.on('element.hover', function(event) { + addMarker(event.element, MARKER_HOVER); + }); + + eventBus.on('element.out', function(event) { + removeMarker(event.element, MARKER_HOVER); + }); + + eventBus.on('selection.changed', function(event) { + + function deselect(s) { + removeMarker(s, MARKER_SELECTED); + } + + function select(s) { + addMarker(s, MARKER_SELECTED); + } + + var oldSelection = event.oldSelection, + newSelection = event.newSelection; + + forEach$1(oldSelection, function(e) { + if (newSelection.indexOf(e) === -1) { + deselect(e); + } + }); + + forEach$1(newSelection, function(e) { + if (oldSelection.indexOf(e) === -1) { + select(e); + } + }); + + self._updateSelectionOutline(newSelection); + }); + + + eventBus.on('element.changed', function(event) { + if (selection.isSelected(event.element)) { + self._updateSelectionOutline(selection.get()); + } + }); + } + + SelectionVisuals.$inject = [ + 'canvas', + 'eventBus', + 'selection' + ]; + + SelectionVisuals.prototype._updateSelectionOutline = function(selection) { + var layer = this._canvas.getLayer('selectionOutline'); + + clear$1(layer); + + var enabled = selection.length > 1; + + var container = this._canvas.getContainer(); + + classes$1(container)[enabled ? 'add' : 'remove']('djs-multi-select'); + + if (!enabled) { + return; + } + + var bBox = addSelectionOutlinePadding(getBBox(selection)); + + var rect = create$1('rect'); + + attr$1(rect, assign$1({ + rx: 3 + }, bBox)); + + classes$1(rect).add('djs-selection-outline'); + + append(layer, rect); + }; + + // helpers ////////// + + function addSelectionOutlinePadding(bBox) { + return { + x: bBox.x - SELECTION_OUTLINE_PADDING, + y: bBox.y - SELECTION_OUTLINE_PADDING, + width: bBox.width + SELECTION_OUTLINE_PADDING * 2, + height: bBox.height + SELECTION_OUTLINE_PADDING * 2 + }; + } + + /** + * @typedef {import('../../core/Canvas').default} Canvas + * @typedef {import('../../core/ElementRegistry').default} ElementRegistry + * @typedef {import('../../core/EventBus').default} EventBus + * @typedef {import('./Selection').default} Selection + */ + + /** + * @param {EventBus} eventBus + * @param {Selection} selection + * @param {Canvas} canvas + * @param {ElementRegistry} elementRegistry + */ + function SelectionBehavior(eventBus, selection, canvas, elementRegistry) { + + // Select elements on create + eventBus.on('create.end', 500, function(event) { + var context = event.context, + canExecute = context.canExecute, + elements = context.elements, + hints = context.hints || {}, + autoSelect = hints.autoSelect; + + if (canExecute) { + if (autoSelect === false) { + + // Select no elements + return; + } + + if (isArray$2(autoSelect)) { + selection.select(autoSelect); + } else { + + // Select all elements by default + selection.select(elements.filter(isShown)); + } + } + }); + + // Select connection targets on connect + eventBus.on('connect.end', 500, function(event) { + var context = event.context, + connection = context.connection; + + if (connection) { + selection.select(connection); + } + }); + + // Select shapes on move + eventBus.on('shape.move.end', 500, function(event) { + var previousSelection = event.previousSelection || []; + + var shape = elementRegistry.get(event.context.shape.id); + + // Always select main shape on move + var isSelected = find(previousSelection, function(selectedShape) { + return shape.id === selectedShape.id; + }); + + if (!isSelected) { + selection.select(shape); + } + }); + + // Select elements on click + eventBus.on('element.click', function(event) { + + if (!isPrimaryButton(event)) { + return; + } + + var element = event.element; + + if (element === canvas.getRootElement()) { + element = null; + } + + var isSelected = selection.isSelected(element), + isMultiSelect = selection.get().length > 1; + + // Add to selection if SHIFT pressed + var add = hasSecondaryModifier(event); + + if (isSelected && isMultiSelect) { + if (add) { + + // Deselect element + return selection.deselect(element); + } else { + + // Select element only + return selection.select(element); + } + } else if (!isSelected) { + + // Select element + selection.select(element, add); + } else { + + // Deselect element + selection.deselect(element); + } + }); + } + + SelectionBehavior.$inject = [ + 'eventBus', + 'selection', + 'canvas', + 'elementRegistry' + ]; + + + function isShown(element) { + return !element.hidden; + } + + /** + * @type { import('didi').ModuleDeclaration } + */ + var SelectionModule = { + __init__: [ 'selectionVisuals', 'selectionBehavior' ], + __depends__: [ + InteractionEventsModule, + OutlineModule + ], + selection: [ 'type', Selection ], + selectionVisuals: [ 'type', SelectionVisuals ], + selectionBehavior: [ 'type', SelectionBehavior ] + }; + + /** + * Util that provides unique IDs. + * + * @class + * @constructor + * + * The ids can be customized via a given prefix and contain a random value to avoid collisions. + * + * @param {string} [prefix] a prefix to prepend to generated ids (for better readability) + */ + function IdGenerator(prefix) { + + this._counter = 0; + this._prefix = (prefix ? prefix + '-' : '') + Math.floor(Math.random() * 1000000000) + '-'; + } + + /** + * Returns a next unique ID. + * + * @return {string} the id + */ + IdGenerator.prototype.next = function() { + return this._prefix + (++this._counter); + }; + + // document wide unique overlay ids + var ids = new IdGenerator('ov'); + + var LOW_PRIORITY$1 = 500; + + /** + * @typedef {import('../../core/Canvas').default} Canvas + * @typedef {import('../../core/ElementRegistry').default} ElementRegistry + * @typedef {import('../../core/EventBus').default} EventBus + * + * @typedef {import('../../model/Types').Element} Element + * + * @typedef { { + * minZoom?: number, + * maxZoom?: number + * } } OverlaysConfigShow + * + * @typedef { { + * min?: number, + * max?: number + * } } OverlaysConfigScale + * + * @typedef { { + * id: string, + * type: string | null, + * element: Element | string + * } & OverlayAttrs } Overlay + * + * @typedef { { + * html: HTMLElement | string, + * position: { + * top?: number, + * right?: number, + * bottom?: number, + * left?: number + * } + * } & OverlaysConfigDefault } OverlayAttrs + * + * @typedef { { + * html: HTMLElement, + * element: Element, + * overlays: Overlay[] + * } } OverlayContainer + * + * @typedef {{ + * defaults?: OverlaysConfigDefault + * }} OverlaysConfig + * + * @typedef { { + * show?: OverlaysConfigShow, + * scale?: OverlaysConfigScale | boolean + * } } OverlaysConfigDefault + * + * @typedef { { + * id?: string; + * element?: Element | string; + * type?: string; + * } | string } OverlaysFilter + */ + + /** + * A service that allows users to attach overlays to diagram elements. + * + * The overlay service will take care of overlay positioning during updates. + * + * @example + * + * ```javascript + * // add a pink badge on the top left of the shape + * + * overlays.add(someShape, { + * position: { + * top: -5, + * left: -5 + * }, + * html: '
0
' + * }); + * + * // or add via shape id + * + * overlays.add('some-element-id', { + * position: { + * top: -5, + * left: -5 + * } + * html: '
0
' + * }); + * + * // or add with optional type + * + * overlays.add(someShape, 'badge', { + * position: { + * top: -5, + * left: -5 + * } + * html: '
0
' + * }); + * ``` + * + * ```javascript + * // remove an overlay + * + * var id = overlays.add(...); + * overlays.remove(id); + * + * + * You may configure overlay defaults during tool by providing a `config` module + * with `overlays.defaults` as an entry: + * + * { + * overlays: { + * defaults: { + * show: { + * minZoom: 0.7, + * maxZoom: 5.0 + * }, + * scale: { + * min: 1 + * } + * } + * } + * ``` + * + * @param {OverlaysConfig} config + * @param {EventBus} eventBus + * @param {Canvas} canvas + * @param {ElementRegistry} elementRegistry + */ + function Overlays(config, eventBus, canvas, elementRegistry) { + this._eventBus = eventBus; + this._canvas = canvas; + this._elementRegistry = elementRegistry; + + this._ids = ids; + + /** + * @type {OverlaysConfigDefault} + */ + this._overlayDefaults = assign$1({ + + // no show constraints + show: null, + + // always scale + scale: true + }, config && config.defaults); + + /** + * @type {Map} + */ + this._overlays = {}; + + /** + * @type {OverlayContainer[]} + */ + this._overlayContainers = []; + + /** + * @type {HTMLElement} + */ + this._overlayRoot = createRoot(canvas.getContainer()); + + this._init(); + } + + + Overlays.$inject = [ + 'config.overlays', + 'eventBus', + 'canvas', + 'elementRegistry' + ]; + + + /** + * Returns the overlay with the specified ID or a list of overlays + * for an element with a given type. + * + * @example + * + * ```javascript + * // return the single overlay with the given ID + * overlays.get('some-id'); + * + * // return all overlays for the shape + * overlays.get({ element: someShape }); + * + * // return all overlays on shape with type 'badge' + * overlays.get({ element: someShape, type: 'badge' }); + * + * // shape can also be specified as ID + * overlays.get({ element: 'element-id', type: 'badge' }); + * ``` + * + * @param {OverlaysFilter} search The filter to be used to find the overlay(s). + * + * @return {Overlay|Overlay[]} The overlay(s). + */ + Overlays.prototype.get = function(search) { + + if (isString(search)) { + search = { id: search }; + } + + if (isString(search.element)) { + search.element = this._elementRegistry.get(search.element); + } + + if (search.element) { + var container = this._getOverlayContainer(search.element, true); + + // return a list of overlays when searching by element (+type) + if (container) { + return search.type ? filter(container.overlays, matchPattern({ type: search.type })) : container.overlays.slice(); + } else { + return []; + } + } else + if (search.type) { + return filter(this._overlays, matchPattern({ type: search.type })); + } else { + + // return single element when searching by id + return search.id ? this._overlays[search.id] : null; + } + }; + + /** + * Adds an HTML overlay to an element. + * + * @param {Element|string} element The element to add the overlay to. + * @param {string} [type] An optional type that can be used to filter. + * @param {OverlayAttrs} overlay The overlay. + * + * @return {string} The overlay's ID that can be used to get or remove it. + */ + Overlays.prototype.add = function(element, type, overlay) { + + if (isObject(type)) { + overlay = type; + type = null; + } + + if (!element.id) { + element = this._elementRegistry.get(element); + } + + if (!overlay.position) { + throw new Error('must specifiy overlay position'); + } + + if (!overlay.html) { + throw new Error('must specifiy overlay html'); + } + + if (!element) { + throw new Error('invalid element specified'); + } + + var id = this._ids.next(); + + overlay = assign$1({}, this._overlayDefaults, overlay, { + id: id, + type: type, + element: element, + html: overlay.html + }); + + this._addOverlay(overlay); + + return id; + }; + + + /** + * Remove an overlay with the given ID or all overlays matching the given filter. + * + * @see Overlays#get for filter options. + * + * @param {OverlaysFilter} filter The filter to be used to find the overlay. + */ + Overlays.prototype.remove = function(filter) { + + var overlays = this.get(filter) || []; + + if (!isArray$2(overlays)) { + overlays = [ overlays ]; + } + + var self = this; + + forEach$1(overlays, function(overlay) { + + var container = self._getOverlayContainer(overlay.element, true); + + if (overlay) { + remove$1(overlay.html); + remove$1(overlay.htmlContainer); + + delete overlay.htmlContainer; + delete overlay.element; + + delete self._overlays[overlay.id]; + } + + if (container) { + var idx = container.overlays.indexOf(overlay); + if (idx !== -1) { + container.overlays.splice(idx, 1); + } + } + }); + + }; + + /** + * Checks whether overlays are shown. + * + * @return {boolean} Whether overlays are shown. + */ + Overlays.prototype.isShown = function() { + return this._overlayRoot.style.display !== 'none'; + }; + + /** + * Show all overlays. + */ + Overlays.prototype.show = function() { + setVisible(this._overlayRoot); + }; + + /** + * Hide all overlays. + */ + Overlays.prototype.hide = function() { + setVisible(this._overlayRoot, false); + }; + + /** + * Remove all overlays and their container. + */ + Overlays.prototype.clear = function() { + this._overlays = {}; + + this._overlayContainers = []; + + clear(this._overlayRoot); + }; + + Overlays.prototype._updateOverlayContainer = function(container) { + var element = container.element, + html = container.html; + + // update container left,top according to the elements x,y coordinates + // this ensures we can attach child elements relative to this container + + var x = element.x, + y = element.y; + + if (element.waypoints) { + var bbox = getBBox(element); + x = bbox.x; + y = bbox.y; + } + + setPosition(html, x, y); + + attr(container.html, 'data-container-id', element.id); + }; + + + Overlays.prototype._updateOverlay = function(overlay) { + + var position = overlay.position, + htmlContainer = overlay.htmlContainer, + element = overlay.element; + + // update overlay html relative to shape because + // it is already positioned on the element + + // update relative + var left = position.left, + top = position.top; + + if (position.right !== undefined) { + + var width; + + if (element.waypoints) { + width = getBBox(element).width; + } else { + width = element.width; + } + + left = position.right * -1 + width; + } + + if (position.bottom !== undefined) { + + var height; + + if (element.waypoints) { + height = getBBox(element).height; + } else { + height = element.height; + } + + top = position.bottom * -1 + height; + } + + setPosition(htmlContainer, left || 0, top || 0); + this._updateOverlayVisibilty(overlay, this._canvas.viewbox()); + }; + + + Overlays.prototype._createOverlayContainer = function(element) { + var html = domify$1('
'); + assign(html, { position: 'absolute' }); + + this._overlayRoot.appendChild(html); + + var container = { + html: html, + element: element, + overlays: [] + }; + + this._updateOverlayContainer(container); + + this._overlayContainers.push(container); + + return container; + }; + + + Overlays.prototype._updateRoot = function(viewbox) { + var scale = viewbox.scale || 1; + + var matrix = 'matrix(' + + [ + scale, + 0, + 0, + scale, + -1 * viewbox.x * scale, + -1 * viewbox.y * scale + ].join(',') + + ')'; + + setTransform(this._overlayRoot, matrix); + }; + + + Overlays.prototype._getOverlayContainer = function(element, raw) { + var container = find(this._overlayContainers, function(c) { + return c.element === element; + }); + + + if (!container && !raw) { + return this._createOverlayContainer(element); + } + + return container; + }; + + + Overlays.prototype._addOverlay = function(overlay) { + + var id = overlay.id, + element = overlay.element, + html = overlay.html, + htmlContainer, + overlayContainer; + + // unwrap jquery (for those who need it) + if (html.get && html.constructor.prototype.jquery) { + html = html.get(0); + } + + // create proper html elements from + // overlay HTML strings + if (isString(html)) { + html = domify$1(html); + } + + overlayContainer = this._getOverlayContainer(element); + + htmlContainer = domify$1('
'); + assign(htmlContainer, { position: 'absolute' }); + + htmlContainer.appendChild(html); + + if (overlay.type) { + classes(htmlContainer).add('djs-overlay-' + overlay.type); + } + + var elementRoot = this._canvas.findRoot(element); + var activeRoot = this._canvas.getRootElement(); + + setVisible(htmlContainer, elementRoot === activeRoot); + + overlay.htmlContainer = htmlContainer; + + overlayContainer.overlays.push(overlay); + overlayContainer.html.appendChild(htmlContainer); + + this._overlays[id] = overlay; + + this._updateOverlay(overlay); + this._updateOverlayVisibilty(overlay, this._canvas.viewbox()); + }; + + + Overlays.prototype._updateOverlayVisibilty = function(overlay, viewbox) { + var show = overlay.show, + rootElement = this._canvas.findRoot(overlay.element), + minZoom = show && show.minZoom, + maxZoom = show && show.maxZoom, + htmlContainer = overlay.htmlContainer, + activeRootElement = this._canvas.getRootElement(), + visible = true; + + if (rootElement !== activeRootElement) { + visible = false; + } else if (show) { + if ( + (isDefined(minZoom) && minZoom > viewbox.scale) || + (isDefined(maxZoom) && maxZoom < viewbox.scale) + ) { + visible = false; + } + } + + setVisible(htmlContainer, visible); + + this._updateOverlayScale(overlay, viewbox); + }; + + + Overlays.prototype._updateOverlayScale = function(overlay, viewbox) { + var shouldScale = overlay.scale, + minScale, + maxScale, + htmlContainer = overlay.htmlContainer; + + var scale, transform = ''; + + if (shouldScale !== true) { + + if (shouldScale === false) { + minScale = 1; + maxScale = 1; + } else { + minScale = shouldScale.min; + maxScale = shouldScale.max; + } + + if (isDefined(minScale) && viewbox.scale < minScale) { + scale = (1 / viewbox.scale || 1) * minScale; + } + + if (isDefined(maxScale) && viewbox.scale > maxScale) { + scale = (1 / viewbox.scale || 1) * maxScale; + } + } + + if (isDefined(scale)) { + transform = 'scale(' + scale + ',' + scale + ')'; + } + + setTransform(htmlContainer, transform); + }; + + + Overlays.prototype._updateOverlaysVisibilty = function(viewbox) { + + var self = this; + + forEach$1(this._overlays, function(overlay) { + self._updateOverlayVisibilty(overlay, viewbox); + }); + }; + + + Overlays.prototype._init = function() { + + var eventBus = this._eventBus; + + var self = this; + + + // scroll/zoom integration + + function updateViewbox(viewbox) { + self._updateRoot(viewbox); + self._updateOverlaysVisibilty(viewbox); + + self.show(); + } + + eventBus.on('canvas.viewbox.changing', function(event) { + self.hide(); + }); + + eventBus.on('canvas.viewbox.changed', function(event) { + updateViewbox(event.viewbox); + }); + + + // remove integration + + eventBus.on([ 'shape.remove', 'connection.remove' ], function(e) { + var element = e.element; + var overlays = self.get({ element: element }); + + forEach$1(overlays, function(o) { + self.remove(o.id); + }); + + var container = self._getOverlayContainer(element); + + if (container) { + remove$1(container.html); + var i = self._overlayContainers.indexOf(container); + if (i !== -1) { + self._overlayContainers.splice(i, 1); + } + } + }); + + + // move integration + + eventBus.on('element.changed', LOW_PRIORITY$1, function(e) { + var element = e.element; + + var container = self._getOverlayContainer(element, true); + + if (container) { + forEach$1(container.overlays, function(overlay) { + self._updateOverlay(overlay); + }); + + self._updateOverlayContainer(container); + } + }); + + + // marker integration, simply add them on the overlays as classes, too. + + eventBus.on('element.marker.update', function(e) { + var container = self._getOverlayContainer(e.element, true); + if (container) { + classes(container.html)[e.add ? 'add' : 'remove'](e.marker); + } + }); + + + eventBus.on('root.set', function() { + self._updateOverlaysVisibilty(self._canvas.viewbox()); + }); + + // clear overlays with diagram + + eventBus.on('diagram.clear', this.clear, this); + }; + + + + // helpers ///////////////////////////// + + function createRoot(parentNode) { + var root = domify$1( + '
' + ); + + assign(root, { + position: 'absolute', + width: 0, + height: 0 + }); + + parentNode.insertBefore(root, parentNode.firstChild); + + return root; + } + + function setPosition(el, x, y) { + assign(el, { left: x + 'px', top: y + 'px' }); + } + + /** + * Set element visible + * + * @param {DOMElement} el + * @param {boolean} [visible=true] + */ + function setVisible(el, visible) { + el.style.display = visible === false ? 'none' : ''; + } + + function setTransform(el, transform) { + + el.style['transform-origin'] = 'top left'; + + [ '', '-ms-', '-webkit-' ].forEach(function(prefix) { + el.style[prefix + 'transform'] = transform; + }); + } + + /** + * @type { import('didi').ModuleDeclaration } + */ + var OverlaysModule = { + __init__: [ 'overlays' ], + overlays: [ 'type', Overlays ] + }; + + /** + * @typedef {import('../../core/Canvas').default} Canvas + * @typedef {import('../../core/ElementRegistry').default} ElementRegistry + * @typedef {import('../../core/EventBus').default} EventBus + * @typedef {import('../../core/GraphicsFactory').default} GraphicsFactory + */ + + /** + * Adds change support to the diagram, including + * + *
    + *
  • redrawing shapes and connections on change
  • + *
+ * + * @param {EventBus} eventBus + * @param {Canvas} canvas + * @param {ElementRegistry} elementRegistry + * @param {GraphicsFactory} graphicsFactory + */ + function ChangeSupport( + eventBus, canvas, elementRegistry, + graphicsFactory) { + + + // redraw shapes / connections on change + + eventBus.on('element.changed', function(event) { + + var element = event.element; + + // element might have been deleted and replaced by new element with same ID + // thus check for parent of element except for root element + if (element.parent || element === canvas.getRootElement()) { + event.gfx = elementRegistry.getGraphics(element); + } + + // shape + gfx may have been deleted + if (!event.gfx) { + return; + } + + eventBus.fire(getType(element) + '.changed', event); + }); + + eventBus.on('elements.changed', function(event) { + + var elements = event.elements; + + elements.forEach(function(e) { + eventBus.fire('element.changed', { element: e }); + }); + + graphicsFactory.updateContainments(elements); + }); + + eventBus.on('shape.changed', function(event) { + graphicsFactory.update('shape', event.element, event.gfx); + }); + + eventBus.on('connection.changed', function(event) { + graphicsFactory.update('connection', event.element, event.gfx); + }); + } + + ChangeSupport.$inject = [ + 'eventBus', + 'canvas', + 'elementRegistry', + 'graphicsFactory' + ]; + + /** + * @type { import('didi').ModuleDeclaration } + */ + var ChangeSupportModule = { + __init__: [ 'changeSupport' ], + changeSupport: [ 'type', ChangeSupport ] + }; + + /** + * @typedef {import('../core/Types').ElementLike} ElementLike + * @typedef {import('../core/EventBus').default} EventBus + * @typedef {import('./CommandStack').CommandContext} CommandContext + * + * @typedef {string|string[]} Events + * @typedef { (context: CommandContext) => ElementLike[] | void } HandlerFunction + * @typedef { (context: CommandContext) => void } ComposeHandlerFunction + */ + + var DEFAULT_PRIORITY$1 = 1000; + + /** + * A utility that can be used to plug into the command execution for + * extension and/or validation. + * + * @class + * @constructor + * + * @example + * + * ```javascript + * import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor'; + * + * class CommandLogger extends CommandInterceptor { + * constructor(eventBus) { + * super(eventBus); + * + * this.preExecute('shape.create', (event) => { + * console.log('commandStack.shape-create.preExecute', event); + * }); + * } + * ``` + * + * @param {EventBus} eventBus + */ + function CommandInterceptor(eventBus) { + + /** + * @type {EventBus} + */ + this._eventBus = eventBus; + } + + CommandInterceptor.$inject = [ 'eventBus' ]; + + function unwrapEvent(fn, that) { + return function(event) { + return fn.call(that || null, event.context, event.command, event); + }; + } + + + /** + * Intercept a command during one of the phases. + * + * @param {Events} [events] command(s) to intercept + * @param {string} [hook] phase to intercept + * @param {number} [priority] + * @param {ComposeHandlerFunction|HandlerFunction} handlerFn + * @param {boolean} [unwrap] whether the event should be unwrapped + * @param {any} [that] + */ + CommandInterceptor.prototype.on = function(events, hook, priority, handlerFn, unwrap, that) { + + if (isFunction(hook) || isNumber(hook)) { + that = unwrap; + unwrap = handlerFn; + handlerFn = priority; + priority = hook; + hook = null; + } + + if (isFunction(priority)) { + that = unwrap; + unwrap = handlerFn; + handlerFn = priority; + priority = DEFAULT_PRIORITY$1; + } + + if (isObject(unwrap)) { + that = unwrap; + unwrap = false; + } + + if (!isFunction(handlerFn)) { + throw new Error('handlerFn must be a function'); + } + + if (!isArray$2(events)) { + events = [ events ]; + } + + var eventBus = this._eventBus; + + forEach$1(events, function(event) { + + // concat commandStack(.event)?(.hook)? + var fullEvent = [ 'commandStack', event, hook ].filter(function(e) { return e; }).join('.'); + + eventBus.on(fullEvent, priority, unwrap ? unwrapEvent(handlerFn, that) : handlerFn, that); + }); + }; + + /** + * Add a phase of command interceptor. + * + * @param {Events} [events] command(s) to intercept + * @param {number} [priority] + * @param {ComposeHandlerFunction|HandlerFunction} handlerFn + * @param {boolean} [unwrap] whether the event should be unwrapped + * @param {any} [that] + */ + CommandInterceptor.prototype.canExecute = createHook('canExecute'); + + /** + * Add a phase of command interceptor. + * + * @param {Events} [events] command(s) to intercept + * @param {number} [priority] + * @param {ComposeHandlerFunction|HandlerFunction} handlerFn + * @param {boolean} [unwrap] whether the event should be unwrapped + * @param {any} [that] + */ + CommandInterceptor.prototype.preExecute = createHook('preExecute'); + + /** + * Add a phase of command interceptor. + * + * @param {Events} [events] command(s) to intercept + * @param {number} [priority] + * @param {ComposeHandlerFunction|HandlerFunction} handlerFn + * @param {boolean} [unwrap] whether the event should be unwrapped + * @param {any} [that] + */ + CommandInterceptor.prototype.preExecuted = createHook('preExecuted'); + + /** + * Add a phase of command interceptor. + * + * @param {Events} [events] command(s) to intercept + * @param {number} [priority] + * @param {ComposeHandlerFunction|HandlerFunction} handlerFn + * @param {boolean} [unwrap] whether the event should be unwrapped + * @param {any} [that] + */ + CommandInterceptor.prototype.execute = createHook('execute'); + + /** + * Add a phase of command interceptor. + * + * @param {Events} [events] command(s) to intercept + * @param {number} [priority] + * @param {ComposeHandlerFunction|HandlerFunction} handlerFn + * @param {boolean} [unwrap] whether the event should be unwrapped + * @param {any} [that] + */ + CommandInterceptor.prototype.executed = createHook('executed'); + + /** + * Add a phase of command interceptor. + * + * @param {Events} [events] command(s) to intercept + * @param {number} [priority] + * @param {ComposeHandlerFunction|HandlerFunction} handlerFn + * @param {boolean} [unwrap] whether the event should be unwrapped + * @param {any} [that] + */ + CommandInterceptor.prototype.postExecute = createHook('postExecute'); + + /** + * Add a phase of command interceptor. + * + * @param {Events} [events] command(s) to intercept + * @param {number} [priority] + * @param {ComposeHandlerFunction|HandlerFunction} handlerFn + * @param {boolean} [unwrap] whether the event should be unwrapped + * @param {any} [that] + */ + CommandInterceptor.prototype.postExecuted = createHook('postExecuted'); + + /** + * Add a phase of command interceptor. + * + * @param {Events} [events] command(s) to intercept + * @param {number} [priority] + * @param {ComposeHandlerFunction|HandlerFunction} handlerFn + * @param {boolean} [unwrap] whether the event should be unwrapped + * @param {any} [that] + */ + CommandInterceptor.prototype.revert = createHook('revert'); + + /** + * Add a phase of command interceptor. + * + * @param {Events} [events] command(s) to intercept + * @param {number} [priority] + * @param {ComposeHandlerFunction|HandlerFunction} handlerFn + * @param {boolean} [unwrap] whether the event should be unwrapped + * @param {any} [that] + */ + CommandInterceptor.prototype.reverted = createHook('reverted'); + + /* + * Add prototype methods for each phase of command execution (e.g. execute, + * revert). + * + * @param {string} hook + * + * @return { ( + * events?: Events, + * priority?: number, + * handlerFn: ComposeHandlerFunction|HandlerFunction, + * unwrap?: boolean + * ) => any } + */ + function createHook(hook) { + + /** + * @this {CommandInterceptor} + * + * @param {Events} [events] + * @param {number} [priority] + * @param {ComposeHandlerFunction|HandlerFunction} handlerFn + * @param {boolean} [unwrap] + * @param {any} [that] + */ + const hookFn = function(events, priority, handlerFn, unwrap, that) { + + if (isFunction(events) || isNumber(events)) { + that = unwrap; + unwrap = handlerFn; + handlerFn = priority; + priority = events; + events = null; + } + + this.on(events, hook, priority, handlerFn, unwrap, that); + }; + + return hookFn; + } + + /** + * @typedef {import('didi').Injector} Injector + * + * @typedef {import('../../core/Canvas').default} Canvas + */ + + /** + * A modeling behavior that ensures we set the correct root element + * as we undo and redo commands. + * + * @param {Canvas} canvas + * @param {Injector} injector + */ + function RootElementsBehavior(canvas, injector) { + + injector.invoke(CommandInterceptor, this); + + this.executed(function(event) { + var context = event.context; + + if (context.rootElement) { + canvas.setRootElement(context.rootElement); + } else { + context.rootElement = canvas.getRootElement(); + } + }); + + this.revert(function(event) { + var context = event.context; + + if (context.rootElement) { + canvas.setRootElement(context.rootElement); + } + }); + } + + e(RootElementsBehavior, CommandInterceptor); + + RootElementsBehavior.$inject = [ 'canvas', 'injector' ]; + + /** + * @type { import('didi').ModuleDeclaration } + */ + var RootElementsModule = { + __init__: [ 'rootElementsBehavior' ], + rootElementsBehavior: [ 'type', RootElementsBehavior ] + }; + + /** + * @param {string} str + * + * @return {string} + */ + + var HTML_ESCAPE_MAP = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + '\'': ''' + }; + + /** + * @param {string} str + * + * @return {string} + */ + function escapeHTML(str) { + str = '' + str; + + return str && str.replace(/[&<>"']/g, function(match) { + return HTML_ESCAPE_MAP[match]; + }); + } + + /** + * @typedef {import('../model/Types').Element} Element + * @typedef {import('../model/Types').ModdleElement} ModdleElement + */ + + var planeSuffix = '_plane'; + + /** + * Get plane ID for a primary shape. + * + * @param {Element|ModdleElement} element + * + * @return {string} + */ + function getPlaneIdFromShape(element) { + var id = element.id; + + if (is$1(element, 'bpmn:SubProcess')) { + return addPlaneSuffix(id); + } + + return id; + } + + function addPlaneSuffix(id) { + return id + planeSuffix; + } + + /** + * @typedef {import('diagram-js/lib/core/Canvas').default} Canvas + * @typedef {import('diagram-js/lib/core/ElementRegistry').default} ElementRegistry + * @typedef {import('diagram-js/lib/core/EventBus').default} EventBus + * + * @typedef {import('../../model/Types').Element} Element + * @typedef {import('../../model/Types').Shape} Shape + */ + + var OPEN_CLASS = 'bjs-breadcrumbs-shown'; + + + /** + * Adds overlays that allow switching planes on collapsed subprocesses. + * + * @param {EventBus} eventBus + * @param {ElementRegistry} elementRegistry + * @param {Canvas} canvas + */ + function DrilldownBreadcrumbs(eventBus, elementRegistry, canvas) { + var breadcrumbs = domify$1('
    '); + var container = canvas.getContainer(); + var containerClasses = classes(container); + container.appendChild(breadcrumbs); + + var businessObjectParents = []; + + // update breadcrumbs if name or ID of the primary shape changes + eventBus.on('element.changed', function(event) { + var shape = event.element, + businessObject = getBusinessObject(shape); + + var isPresent = find(businessObjectParents, function(element) { + return element === businessObject; + }); + + if (!isPresent) { + return; + } + + updateBreadcrumbs(); + }); + + /** + * Updates the displayed breadcrumbs. If no element is provided, only the + * labels are updated. + * + * @param {Element} [element] + */ + function updateBreadcrumbs(element) { + if (element) { + businessObjectParents = getBusinessObjectParentChain(element); + } + + var path = businessObjectParents.map(function(parent) { + var title = escapeHTML(parent.name || parent.id); + var link = domify$1('
  • ' + title + '
  • '); + + var parentPlane = canvas.findRoot(getPlaneIdFromShape(parent)) || canvas.findRoot(parent.id); + + // when the root is a collaboration, the process does not have a corresponding + // element in the elementRegisty. Instead, we search for the corresponding participant + if (!parentPlane && is$1(parent, 'bpmn:Process')) { + var participant = elementRegistry.find(function(element) { + var businessObject = getBusinessObject(element); + + return businessObject && businessObject.get('processRef') && businessObject.get('processRef') === parent; + }); + + parentPlane = canvas.findRoot(participant.id); + } + + link.addEventListener('click', function() { + canvas.setRootElement(parentPlane); + }); + + return link; + }); + + breadcrumbs.innerHTML = ''; + + // show breadcrumbs and expose state to .djs-container + var visible = path.length > 1; + + containerClasses.toggle(OPEN_CLASS, visible); + + path.forEach(function(element) { + breadcrumbs.appendChild(element); + }); + } + + eventBus.on('root.set', function(event) { + updateBreadcrumbs(event.element); + }); + + } + + DrilldownBreadcrumbs.$inject = [ 'eventBus', 'elementRegistry', 'canvas' ]; + + + // helpers ////////// + + /** + * Returns the parents for the element using the business object chain, + * starting with the root element. + * + * @param {Shape} child + * + * @return {Shape} + */ + function getBusinessObjectParentChain(child) { + var businessObject = getBusinessObject(child); + + var parents = []; + + for (var element = businessObject; element; element = element.$parent) { + if (is$1(element, 'bpmn:SubProcess') || is$1(element, 'bpmn:Process')) { + parents.push(element); + } + } + + return parents.reverse(); + } + + /** + * @typedef {import('diagram-js/lib/core/Canvas').default} Canvas + * @typedef {import('diagram-js/lib/core/EventBus').default} EventBus + */ + + /** + * Move collapsed subprocesses into view when drilling down. + * + * Zoom and scroll are saved in a session. + * + * @param {EventBus} eventBus + * @param {Canvas} canvas + */ + function DrilldownCentering(eventBus, canvas) { + + var currentRoot = null; + var positionMap = new Map(); + + eventBus.on('root.set', function(event) { + var newRoot = event.element; + var currentViewbox = canvas.viewbox(); + var storedViewbox = positionMap.get(newRoot); + + positionMap.set(currentRoot, { + x: currentViewbox.x, + y: currentViewbox.y, + zoom: currentViewbox.scale + }); + + currentRoot = newRoot; + + // current root was replaced with a collaboration, we don't update the viewbox + if (is$1(newRoot, 'bpmn:Collaboration') && !storedViewbox) { + return; + } + + storedViewbox = storedViewbox || { x: 0, y: 0, zoom: 1 }; + + var dx = (currentViewbox.x - storedViewbox.x) * currentViewbox.scale, + dy = (currentViewbox.y - storedViewbox.y) * currentViewbox.scale; + + if (dx !== 0 || dy !== 0) { + canvas.scroll({ + dx: dx, + dy: dy + }); + } + + if (storedViewbox.zoom !== currentViewbox.scale) { + canvas.zoom(storedViewbox.zoom, { x: 0, y: 0 }); + } + }); + + eventBus.on('diagram.clear', function() { + positionMap.clear(); + currentRoot = null; + }); + + } + + DrilldownCentering.$inject = [ 'eventBus', 'canvas' ]; + + + /** + * ES5 Map implementation. Works. + */ + function Map() { + + this._entries = []; + + this.set = function(key, value) { + + var found = false; + + for (var k in this._entries) { + if (this._entries[k][0] === key) { + this._entries[k][1] = value; + + found = true; + + break; + } + } + + if (!found) { + this._entries.push([ key, value ]); + } + }; + + this.get = function(key) { + + for (var k in this._entries) { + if (this._entries[k][0] === key) { + return this._entries[k][1]; + } + } + + return null; + }; + + this.clear = function() { + this._entries.length = 0; + }; + + this.remove = function(key) { + + var idx = -1; + + for (var k in this._entries) { + if (this._entries[k][0] === key) { + idx = k; + + break; + } + } + + if (idx !== -1) { + this._entries.splice(idx, 1); + } + }; + } + + /** + * @typedef {import('diagram-js/lib/core/EventBus').default} EventBus + * @typedef {import('../../model/Types').Moddle} Moddle + * + * @typedef {import('../../model/Types').Element} Element + * @typedef {import('../../model/Types').Shape} Shape + * + * @typedef {import('diagram-js/lib/core/Canvas').CanvasPlane} CanvasPlane + * + * @typedef {import('diagram-js/lib/util/Types').Rect} Rect + */ + + var DEFAULT_POSITION = { + x: 180, + y: 160 + }; + + /** + * Hook into `import.render.start` and create new planes for diagrams with + * collapsed subprocesses and all DI elements on the same plane. + * + * @param {EventBus} eventBus + * @param {Moddle} moddle + */ + function SubprocessCompatibility(eventBus, moddle) { + this._eventBus = eventBus; + this._moddle = moddle; + + var self = this; + + eventBus.on('import.render.start', 1500, function(e, context) { + self._handleImport(context.definitions); + }); + } + + /** + * @param {ModdleElement} definitions + */ + SubprocessCompatibility.prototype._handleImport = function(definitions) { + if (!definitions.diagrams) { + return; + } + + var self = this; + this._definitions = definitions; + this._processToDiagramMap = {}; + + definitions.diagrams.forEach(function(diagram) { + if (!diagram.plane || !diagram.plane.bpmnElement) { + return; + } + + self._processToDiagramMap[diagram.plane.bpmnElement.id] = diagram; + }); + + var newDiagrams = []; + definitions.diagrams.forEach(function(diagram) { + var createdDiagrams = self._createNewDiagrams(diagram.plane); + Array.prototype.push.apply(newDiagrams, createdDiagrams); + }); + + newDiagrams.forEach(function(diagram) { + self._movePlaneElementsToOrigin(diagram.plane); + }); + }; + + + /** + * Moves all DI elements from collapsed subprocesses to a new plane. + * + * @param {CanvasPlane} plane + * + * @return {ModdleElement[]} new diagrams created for the collapsed subprocesses + */ + SubprocessCompatibility.prototype._createNewDiagrams = function(plane) { + var self = this; + + var collapsedElements = []; + var elementsToMove = []; + + plane.get('planeElement').forEach(function(diElement) { + var businessObject = diElement.bpmnElement; + + if (!businessObject) { + return; + } + + var parent = businessObject.$parent; + + if (is$1(businessObject, 'bpmn:SubProcess') && !diElement.isExpanded) { + collapsedElements.push(businessObject); + } + + if (shouldMoveToPlane(businessObject, plane)) { + + // don't change the array while we iterate over it + elementsToMove.push({ diElement: diElement, parent: parent }); + } + }); + + var newDiagrams = []; + + // create new planes for all collapsed subprocesses, even when they are empty + collapsedElements.forEach(function(element) { + if (!self._processToDiagramMap[ element.id ]) { + var diagram = self._createDiagram(element); + + self._processToDiagramMap[element.id] = diagram; + + newDiagrams.push(diagram); + } + }); + + elementsToMove.forEach(function(element) { + var diElement = element.diElement; + var parent = element.parent; + + // parent is expanded, get nearest collapsed parent + while (parent && collapsedElements.indexOf(parent) === -1) { + parent = parent.$parent; + } + + // false positive, all parents are expanded + if (!parent) { + return; + } + + var diagram = self._processToDiagramMap[ parent.id ]; + + self._moveToDiPlane(diElement, diagram.plane); + }); + + return newDiagrams; + }; + + /** + * @param {CanvasPlane} plane + */ + SubprocessCompatibility.prototype._movePlaneElementsToOrigin = function(plane) { + var elements = plane.get('planeElement'); + + // get bounding box of all elements + var planeBounds = getPlaneBounds(plane); + + var offset = { + x: planeBounds.x - DEFAULT_POSITION.x, + y: planeBounds.y - DEFAULT_POSITION.y + }; + + elements.forEach(function(diElement) { + if (diElement.waypoint) { + diElement.waypoint.forEach(function(waypoint) { + waypoint.x = waypoint.x - offset.x; + waypoint.y = waypoint.y - offset.y; + }); + } else if (diElement.bounds) { + diElement.bounds.x = diElement.bounds.x - offset.x; + diElement.bounds.y = diElement.bounds.y - offset.y; + } + }); + }; + + /** + * @param {ModdleElement} diElement + * @param {CanvasPlane} newPlane + */ + SubprocessCompatibility.prototype._moveToDiPlane = function(diElement, newPlane) { + var containingDiagram = findRootDiagram(diElement); + + // remove DI from old Plane and add it to the new one + var parentPlaneElement = containingDiagram.plane.get('planeElement'); + + parentPlaneElement.splice(parentPlaneElement.indexOf(diElement), 1); + + newPlane.get('planeElement').push(diElement); + }; + + /** + * @param {ModdleElement} businessObject + * + * @return {ModdleElement} + */ + SubprocessCompatibility.prototype._createDiagram = function(businessObject) { + var plane = this._moddle.create('bpmndi:BPMNPlane', { + bpmnElement: businessObject + }); + + var diagram = this._moddle.create('bpmndi:BPMNDiagram', { + plane: plane + }); + + plane.$parent = diagram; + + plane.bpmnElement = businessObject; + + diagram.$parent = this._definitions; + + this._definitions.diagrams.push(diagram); + + return diagram; + }; + + SubprocessCompatibility.$inject = [ 'eventBus', 'moddle' ]; + + + // helpers ////////// + + function findRootDiagram(element) { + if (is$1(element, 'bpmndi:BPMNDiagram')) { + return element; + } else { + return findRootDiagram(element.$parent); + } + } + + /** + * @param {CanvasPlane} plane + * + * @return {Rect} + */ + function getPlaneBounds(plane) { + var planeTrbl = { + top: Infinity, + right: -Infinity, + bottom: -Infinity, + left: Infinity + }; + + plane.planeElement.forEach(function(element) { + if (!element.bounds) { + return; + } + + var trbl = asTRBL(element.bounds); + + planeTrbl.top = Math.min(trbl.top, planeTrbl.top); + planeTrbl.left = Math.min(trbl.left, planeTrbl.left); + }); + + return asBounds(planeTrbl); + } + + /** + * @param {ModdleElement} businessObject + * @param {CanvasPlane} plane + * + * @return {boolean} + */ + function shouldMoveToPlane(businessObject, plane) { + var parent = businessObject.$parent; + + // don't move elements that are already on the plane + if (!is$1(parent, 'bpmn:SubProcess') || parent === plane.bpmnElement) { + return false; + } + + // dataAssociations are children of the subprocess but rendered on process level + // cf. https://github.com/bpmn-io/bpmn-js/issues/1619 + if (isAny(businessObject, [ 'bpmn:DataInputAssociation', 'bpmn:DataOutputAssociation' ])) { + return false; + } + + return true; + } + + /** + * @typedef {import('diagram-js/lib/core/Canvas').default} Canvas + * @typedef {import('diagram-js/lib/core/ElementRegistry').default} ElementRegistry + * @typedef {import('diagram-js/lib/core/EventBus').default} EventBus + * @typedef {import('diagram-js/lib/features/overlays/Overlays').default} Overlays + * + * @typedef {import('../../model/Types').Element} Element + * @typedef {import('../../model/Types').Parent} Parent + * @typedef {import('../../model/Types').Shape} Shape + */ + + var LOW_PRIORITY = 250; + var ARROW_DOWN_SVG = ''; + + var EMPTY_MARKER = 'bjs-drilldown-empty'; + + /** + * @param {Canvas} canvas + * @param {EventBus} eventBus + * @param {ElementRegistry} elementRegistry + * @param {Overlays} overlays + */ + function DrilldownOverlayBehavior( + canvas, eventBus, elementRegistry, overlays + ) { + CommandInterceptor.call(this, eventBus); + + this._canvas = canvas; + this._eventBus = eventBus; + this._elementRegistry = elementRegistry; + this._overlays = overlays; + + var self = this; + + this.executed('shape.toggleCollapse', LOW_PRIORITY, function(context) { + var shape = context.shape; + + // Add overlay to the collapsed shape + if (self._canDrillDown(shape)) { + self._addOverlay(shape); + } else { + self._removeOverlay(shape); + } + }, true); + + + this.reverted('shape.toggleCollapse', LOW_PRIORITY, function(context) { + var shape = context.shape; + + // Add overlay to the collapsed shape + if (self._canDrillDown(shape)) { + self._addOverlay(shape); + } else { + self._removeOverlay(shape); + } + }, true); + + + this.executed([ 'shape.create', 'shape.move', 'shape.delete' ], LOW_PRIORITY, + function(context) { + var oldParent = context.oldParent, + newParent = context.newParent || context.parent, + shape = context.shape; + + // Add overlay to the collapsed shape + if (self._canDrillDown(shape)) { + self._addOverlay(shape); + } + + self._updateDrilldownOverlay(oldParent); + self._updateDrilldownOverlay(newParent); + self._updateDrilldownOverlay(shape); + }, true); + + + this.reverted([ 'shape.create', 'shape.move', 'shape.delete' ], LOW_PRIORITY, + function(context) { + var oldParent = context.oldParent, + newParent = context.newParent || context.parent, + shape = context.shape; + + // Add overlay to the collapsed shape + if (self._canDrillDown(shape)) { + self._addOverlay(shape); + } + + self._updateDrilldownOverlay(oldParent); + self._updateDrilldownOverlay(newParent); + self._updateDrilldownOverlay(shape); + }, true); + + + eventBus.on('import.render.complete', function() { + elementRegistry.filter(function(e) { + return self._canDrillDown(e); + }).map(function(el) { + self._addOverlay(el); + }); + }); + + } + + e(DrilldownOverlayBehavior, CommandInterceptor); + + /** + * @param {Shape} shape + */ + DrilldownOverlayBehavior.prototype._updateDrilldownOverlay = function(shape) { + var canvas = this._canvas; + + if (!shape) { + return; + } + + var root = canvas.findRoot(shape); + + if (root) { + this._updateOverlayVisibility(root); + } + }; + + /** + * @param {Element} element + * + * @return {boolean} + */ + DrilldownOverlayBehavior.prototype._canDrillDown = function(element) { + var canvas = this._canvas; + + return is$1(element, 'bpmn:SubProcess') && canvas.findRoot(getPlaneIdFromShape(element)); + }; + + /** + * Update the visibility of the drilldown overlay. If the plane has no elements, + * the drilldown will only be shown when the element is selected. + * + * @param {Parent} element The collapsed root or shape. + */ + DrilldownOverlayBehavior.prototype._updateOverlayVisibility = function(element) { + var overlays = this._overlays; + + var businessObject = getBusinessObject(element); + + var overlay = overlays.get({ element: businessObject.id, type: 'drilldown' })[0]; + + if (!overlay) { + return; + } + + var hasFlowElements = businessObject + && businessObject.get('flowElements') + && businessObject.get('flowElements').length; + + classes(overlay.html).toggle(EMPTY_MARKER, !hasFlowElements); + }; + + /** + * Add a drilldown button to the given element assuming the plane has the same + * ID as the element. + * + * @param {Shape} element The collapsed shape. + */ + DrilldownOverlayBehavior.prototype._addOverlay = function(element) { + var canvas = this._canvas, + overlays = this._overlays; + + var existingOverlays = overlays.get({ element: element, type: 'drilldown' }); + + if (existingOverlays.length) { + this._removeOverlay(element); + } + + var button = domify$1(''); + + button.addEventListener('click', function() { + canvas.setRootElement(canvas.findRoot(getPlaneIdFromShape(element))); + }); + + overlays.add(element, 'drilldown', { + position: { + bottom: -7, + right: -8 + }, + html: button + }); + + this._updateOverlayVisibility(element); + }; + + DrilldownOverlayBehavior.prototype._removeOverlay = function(element) { + var overlays = this._overlays; + + overlays.remove({ + element: element, + type: 'drilldown' + }); + }; + + DrilldownOverlayBehavior.$inject = [ + 'canvas', + 'eventBus', + 'elementRegistry', + 'overlays' + ]; + + var DrilldownModdule = { + __depends__: [ OverlaysModule, ChangeSupportModule, RootElementsModule ], + __init__: [ 'drilldownBreadcrumbs', 'drilldownOverlayBehavior', 'drilldownCentering', 'subprocessCompatibility' ], + drilldownBreadcrumbs: [ 'type', DrilldownBreadcrumbs ], + drilldownCentering: [ 'type', DrilldownCentering ], + drilldownOverlayBehavior: [ 'type', DrilldownOverlayBehavior ], + subprocessCompatibility: [ 'type', SubprocessCompatibility ] + }; + + const CLASS_PATTERN = /^class[ {]/; + + + /** + * @param {function} fn + * + * @return {boolean} + */ + function isClass(fn) { + return CLASS_PATTERN.test(fn.toString()); + } + + /** + * @param {any} obj + * + * @return {boolean} + */ + function isArray(obj) { + return Array.isArray(obj); + } + + /** + * @param {any} obj + * @param {string} prop + * + * @return {boolean} + */ + function hasOwnProp(obj, prop) { + return Object.prototype.hasOwnProperty.call(obj, prop); + } + + /** + * @typedef {import('./index.js').InjectAnnotated } InjectAnnotated + */ + + /** + * @template T + * + * @params {[...string[], T] | ...string[], T} args + * + * @return {T & InjectAnnotated} + */ + function annotate(...args) { + + if (args.length === 1 && isArray(args[0])) { + args = args[0]; + } + + args = [ ...args ]; + + const fn = args.pop(); + + fn.$inject = args; + + return fn; + } + + + // Current limitations: + // - can't put into "function arg" comments + // function /* (no parenthesis like this) */ (){} + // function abc( /* xx (no parenthesis like this) */ a, b) {} + // + // Just put the comment before function or inside: + // /* (((this is fine))) */ function(a, b) {} + // function abc(a) { /* (((this is fine))) */} + // + // - can't reliably auto-annotate constructor; we'll match the + // first constructor(...) pattern found which may be the one + // of a nested class, too. + + const CONSTRUCTOR_ARGS = /constructor\s*[^(]*\(\s*([^)]*)\)/m; + const FN_ARGS = /^(?:async\s+)?(?:function\s*[^(]*)?(?:\(\s*([^)]*)\)|(\w+))/m; + const FN_ARG = /\/\*([^*]*)\*\//m; + + /** + * @param {unknown} fn + * + * @return {string[]} + */ + function parseAnnotations(fn) { + + if (typeof fn !== 'function') { + throw new Error(`Cannot annotate "${fn}". Expected a function!`); + } + + const match = fn.toString().match(isClass(fn) ? CONSTRUCTOR_ARGS : FN_ARGS); + + // may parse class without constructor + if (!match) { + return []; + } + + const args = match[1] || match[2]; + + return args && args.split(',').map(arg => { + const argMatch = arg.match(FN_ARG); + return (argMatch && argMatch[1] || arg).trim(); + }) || []; + } + + /** + * @typedef { import('./index.js').ModuleDeclaration } ModuleDeclaration + * @typedef { import('./index.js').ModuleDefinition } ModuleDefinition + * @typedef { import('./index.js').InjectorContext } InjectorContext + */ + + /** + * Create a new injector with the given modules. + * + * @param {ModuleDefinition[]} modules + * @param {InjectorContext} [parent] + */ + function Injector(modules, parent) { + parent = parent || { + get: function(name, strict) { + currentlyResolving.push(name); + + if (strict === false) { + return null; + } else { + throw error(`No provider for "${ name }"!`); + } + } + }; + + const currentlyResolving = []; + const providers = this._providers = Object.create(parent._providers || null); + const instances = this._instances = Object.create(null); + + const self = instances.injector = this; + + const error = function(msg) { + const stack = currentlyResolving.join(' -> '); + currentlyResolving.length = 0; + return new Error(stack ? `${ msg } (Resolving: ${ stack })` : msg); + }; + + /** + * Return a named service. + * + * @param {string} name + * @param {boolean} [strict=true] if false, resolve missing services to null + * + * @return {any} + */ + function get(name, strict) { + if (!providers[name] && name.indexOf('.') !== -1) { + const parts = name.split('.'); + let pivot = get(parts.shift()); + + while (parts.length) { + pivot = pivot[parts.shift()]; + } + + return pivot; + } + + if (hasOwnProp(instances, name)) { + return instances[name]; + } + + if (hasOwnProp(providers, name)) { + if (currentlyResolving.indexOf(name) !== -1) { + currentlyResolving.push(name); + throw error('Cannot resolve circular dependency!'); + } + + currentlyResolving.push(name); + instances[name] = providers[name][0](providers[name][1]); + currentlyResolving.pop(); + + return instances[name]; + } + + return parent.get(name, strict); + } + + function fnDef(fn, locals) { + + if (typeof locals === 'undefined') { + locals = {}; + } + + if (typeof fn !== 'function') { + if (isArray(fn)) { + fn = annotate(fn.slice()); + } else { + throw error(`Cannot invoke "${ fn }". Expected a function!`); + } + } + + const inject = fn.$inject || parseAnnotations(fn); + const dependencies = inject.map(dep => { + if (hasOwnProp(locals, dep)) { + return locals[dep]; + } else { + return get(dep); + } + }); + + return { + fn: fn, + dependencies: dependencies + }; + } + + /** + * Instantiate the given type, injecting dependencies. + * + * @template T + * + * @param { Function | [...string[], Function ]} type + * + * @return T + */ + function instantiate(type) { + const { + fn, + dependencies + } = fnDef(type); + + // instantiate var args constructor + const Constructor = Function.prototype.bind.apply(fn, [ null ].concat(dependencies)); + + return new Constructor(); + } + + /** + * Invoke the given function, injecting dependencies. Return the result. + * + * @template T + * + * @param { Function | [...string[], Function ]} func + * @param { Object } [context] + * @param { Object } [locals] + * + * @return {T} invocation result + */ + function invoke(func, context, locals) { + const { + fn, + dependencies + } = fnDef(func, locals); + + return fn.apply(context, dependencies); + } + + /** + * @param {Injector} childInjector + * + * @return {Function} + */ + function createPrivateInjectorFactory(childInjector) { + return annotate(key => childInjector.get(key)); + } + + /** + * @param {ModuleDefinition[]} modules + * @param {string[]} [forceNewInstances] + * + * @return {Injector} + */ + function createChild(modules, forceNewInstances) { + if (forceNewInstances && forceNewInstances.length) { + const fromParentModule = Object.create(null); + const matchedScopes = Object.create(null); + + const privateInjectorsCache = []; + const privateChildInjectors = []; + const privateChildFactories = []; + + let provider; + let cacheIdx; + let privateChildInjector; + let privateChildInjectorFactory; + + for (let name in providers) { + provider = providers[name]; + + if (forceNewInstances.indexOf(name) !== -1) { + if (provider[2] === 'private') { + cacheIdx = privateInjectorsCache.indexOf(provider[3]); + if (cacheIdx === -1) { + privateChildInjector = provider[3].createChild([], forceNewInstances); + privateChildInjectorFactory = createPrivateInjectorFactory(privateChildInjector); + privateInjectorsCache.push(provider[3]); + privateChildInjectors.push(privateChildInjector); + privateChildFactories.push(privateChildInjectorFactory); + fromParentModule[name] = [ privateChildInjectorFactory, name, 'private', privateChildInjector ]; + } else { + fromParentModule[name] = [ privateChildFactories[cacheIdx], name, 'private', privateChildInjectors[cacheIdx] ]; + } + } else { + fromParentModule[name] = [ provider[2], provider[1] ]; + } + matchedScopes[name] = true; + } + + if ((provider[2] === 'factory' || provider[2] === 'type') && provider[1].$scope) { + /* jshint -W083 */ + forceNewInstances.forEach(scope => { + if (provider[1].$scope.indexOf(scope) !== -1) { + fromParentModule[name] = [ provider[2], provider[1] ]; + matchedScopes[scope] = true; + } + }); + } + } + + forceNewInstances.forEach(scope => { + if (!matchedScopes[scope]) { + throw new Error('No provider for "' + scope + '". Cannot use provider from the parent!'); + } + }); + + modules.unshift(fromParentModule); + } + + return new Injector(modules, self); + } + + const factoryMap = { + factory: invoke, + type: instantiate, + value: function(value) { + return value; + } + }; + + /** + * @param {ModuleDefinition} moduleDefinition + * @param {Injector} injector + */ + function createInitializer(moduleDefinition, injector) { + + const initializers = moduleDefinition.__init__ || []; + + return function() { + initializers.forEach(initializer => { + + // eagerly resolve component (fn or string) + if (typeof initializer === 'string') { + injector.get(initializer); + } else { + injector.invoke(initializer); + } + }); + }; + } + + /** + * @param {ModuleDefinition} moduleDefinition + */ + function loadModule(moduleDefinition) { + + const moduleExports = moduleDefinition.__exports__; + + // private module + if (moduleExports) { + const nestedModules = moduleDefinition.__modules__; + + const clonedModule = Object.keys(moduleDefinition).reduce((clonedModule, key) => { + + if (key !== '__exports__' && key !== '__modules__' && key !== '__init__' && key !== '__depends__') { + clonedModule[key] = moduleDefinition[key]; + } + + return clonedModule; + }, Object.create(null)); + + const childModules = (nestedModules || []).concat(clonedModule); + + const privateInjector = createChild(childModules); + const getFromPrivateInjector = annotate(function(key) { + return privateInjector.get(key); + }); + + moduleExports.forEach(function(key) { + providers[key] = [ getFromPrivateInjector, key, 'private', privateInjector ]; + }); + + // ensure child injector initializes + const initializers = (moduleDefinition.__init__ || []).slice(); + + initializers.unshift(function() { + privateInjector.init(); + }); + + moduleDefinition = Object.assign({}, moduleDefinition, { + __init__: initializers + }); + + return createInitializer(moduleDefinition, privateInjector); + } + + // normal module + Object.keys(moduleDefinition).forEach(function(key) { + + if (key === '__init__' || key === '__depends__') { + return; + } + + if (moduleDefinition[key][2] === 'private') { + providers[key] = moduleDefinition[key]; + return; + } + + const type = moduleDefinition[key][0]; + const value = moduleDefinition[key][1]; + + providers[key] = [ factoryMap[type], arrayUnwrap(type, value), type ]; + }); + + return createInitializer(moduleDefinition, self); + } + + /** + * @param {ModuleDefinition[]} moduleDefinitions + * @param {ModuleDefinition} moduleDefinition + * + * @return {ModuleDefinition[]} + */ + function resolveDependencies(moduleDefinitions, moduleDefinition) { + + if (moduleDefinitions.indexOf(moduleDefinition) !== -1) { + return moduleDefinitions; + } + + moduleDefinitions = (moduleDefinition.__depends__ || []).reduce(resolveDependencies, moduleDefinitions); + + if (moduleDefinitions.indexOf(moduleDefinition) !== -1) { + return moduleDefinitions; + } + + return moduleDefinitions.concat(moduleDefinition); + } + + /** + * @param {ModuleDefinition[]} moduleDefinitions + * + * @return { () => void } initializerFn + */ + function bootstrap(moduleDefinitions) { + + const initializers = moduleDefinitions + .reduce(resolveDependencies, []) + .map(loadModule); + + let initialized = false; + + return function() { + + if (initialized) { + return; + } + + initialized = true; + + initializers.forEach(initializer => initializer()); + }; + } + + // public API + this.get = get; + this.invoke = invoke; + this.instantiate = instantiate; + this.createChild = createChild; + + // setup + this.init = bootstrap(modules); + } + + + // helpers /////////////// + + function arrayUnwrap(type, value) { + if (type !== 'value' && isArray(value)) { + value = annotate(value.slice()); + } + + return value; + } + + /** + * @typedef {import('../core/EventBus').default} EventBus + * @typedef {import('./Styles').default} Styles + */ + + // apply default renderer with lowest possible priority + // so that it only kicks in if noone else could render + var DEFAULT_RENDER_PRIORITY = 1; + + /** + * The default renderer used for shapes and connections. + * + * @param {EventBus} eventBus + * @param {Styles} styles + */ + function DefaultRenderer(eventBus, styles) { + + BaseRenderer.call(this, eventBus, DEFAULT_RENDER_PRIORITY); + + this.CONNECTION_STYLE = styles.style([ 'no-fill' ], { strokeWidth: 5, stroke: 'fuchsia' }); + this.SHAPE_STYLE = styles.style({ fill: 'white', stroke: 'fuchsia', strokeWidth: 2 }); + this.FRAME_STYLE = styles.style([ 'no-fill' ], { stroke: 'fuchsia', strokeDasharray: 4, strokeWidth: 2 }); + } + + e(DefaultRenderer, BaseRenderer); + + + /** + * @private + */ + DefaultRenderer.prototype.canRender = function() { + return true; + }; + + /** + * @private + */ + DefaultRenderer.prototype.drawShape = function drawShape(visuals, element, attrs) { + var rect = create$1('rect'); + + attr$1(rect, { + x: 0, + y: 0, + width: element.width || 0, + height: element.height || 0 + }); + + if (isFrameElement(element)) { + attr$1(rect, assign$1({}, this.FRAME_STYLE, attrs || {})); + } else { + attr$1(rect, assign$1({}, this.SHAPE_STYLE, attrs || {})); + } + + append(visuals, rect); + + return rect; + }; + + /** + * @private + */ + DefaultRenderer.prototype.drawConnection = function drawConnection(visuals, connection, attrs) { + + var line = createLine(connection.waypoints, assign$1({}, this.CONNECTION_STYLE, attrs || {})); + append(visuals, line); + + return line; + }; + + /** + * @private + */ + DefaultRenderer.prototype.getShapePath = function getShapePath(shape) { + + var x = shape.x, + y = shape.y, + width = shape.width, + height = shape.height; + + var shapePath = [ + [ 'M', x, y ], + [ 'l', width, 0 ], + [ 'l', 0, height ], + [ 'l', -width, 0 ], + [ 'z' ] + ]; + + return componentsToPath(shapePath); + }; + + /** + * @private + */ + DefaultRenderer.prototype.getConnectionPath = function getConnectionPath(connection) { + var waypoints = connection.waypoints; + + var idx, point, connectionPath = []; + + for (idx = 0; (point = waypoints[idx]); idx++) { + + // take invisible docking into account + // when creating the path + point = point.original || point; + + connectionPath.push([ idx === 0 ? 'M' : 'L', point.x, point.y ]); + } + + return componentsToPath(connectionPath); + }; + + DefaultRenderer.$inject = [ 'eventBus', 'styles' ]; + + /** + * A component that manages shape styles + */ + function Styles() { + + var defaultTraits = { + + 'no-fill': { + fill: 'none' + }, + 'no-border': { + strokeOpacity: 0.0 + }, + 'no-events': { + pointerEvents: 'none' + } + }; + + var self = this; + + /** + * Builds a style definition from a className, a list of traits and an object + * of additional attributes. + * + * @param {string} className + * @param {string[]} [traits] + * @param {Object} [additionalAttrs] + * + * @return {Object} the style definition + */ + this.cls = function(className, traits, additionalAttrs) { + var attrs = this.style(traits, additionalAttrs); + + return assign$1(attrs, { 'class': className }); + }; + + /** + * Builds a style definition from a list of traits and an object of additional + * attributes. + * + * @param {string[]} [traits] + * @param {Object} additionalAttrs + * + * @return {Object} the style definition + */ + this.style = function(traits, additionalAttrs) { + + if (!isArray$2(traits) && !additionalAttrs) { + additionalAttrs = traits; + traits = []; + } + + var attrs = reduce(traits, function(attrs, t) { + return assign$1(attrs, defaultTraits[t] || {}); + }, {}); + + return additionalAttrs ? assign$1(attrs, additionalAttrs) : attrs; + }; + + + /** + * Computes a style definition from a list of traits and an object of + * additional attributes, with custom style definition object. + * + * @param {Object} custom + * @param {string[]} [traits] + * @param {Object} defaultStyles + * + * @return {Object} the style definition + */ + this.computeStyle = function(custom, traits, defaultStyles) { + if (!isArray$2(traits)) { + defaultStyles = traits; + traits = []; + } + + return self.style(traits || [], assign$1({}, defaultStyles, custom || {})); + }; + } + + /** + * @type { import('didi').ModuleDeclaration } + */ + var DrawModule = { + __init__: [ 'defaultRenderer' ], + defaultRenderer: [ 'type', DefaultRenderer ], + styles: [ 'type', Styles ] + }; + + /** + * Failsafe remove an element from a collection + * + * @param {Array} [collection] + * @param {Object} [element] + * + * @return {number} the previous index of the element + */ + function remove(collection, element) { + + if (!collection || !element) { + return -1; + } + + var idx = collection.indexOf(element); + + if (idx !== -1) { + collection.splice(idx, 1); + } + + return idx; + } + + /** + * Fail save add an element to the given connection, ensuring + * it does not yet exist. + * + * @param {Array} collection + * @param {Object} element + * @param {number} [idx] + */ + function add(collection, element, idx) { + + if (!collection || !element) { + return; + } + + if (typeof idx !== 'number') { + idx = -1; + } + + var currentIdx = collection.indexOf(element); + + if (currentIdx !== -1) { + + if (currentIdx === idx) { + + // nothing to do, position has not changed + return; + } else { + + if (idx !== -1) { + + // remove from current position + collection.splice(currentIdx, 1); + } else { + + // already exists in collection + return; + } + } + } + + if (idx !== -1) { + + // insert at specified position + collection.splice(idx, 0, element); + } else { + + // push to end + collection.push(element); + } + } + + /** + * @typedef {import('./Types').ConnectionLike} ConnectionLike + * @typedef {import('./Types').RootLike} RootLike + * @typedef {import('./Types').ParentLike } ParentLike + * @typedef {import('./Types').ShapeLike} ShapeLike + * + * @typedef { { + * container?: HTMLElement; + * deferUpdate?: boolean; + * width?: number; + * height?: number; + * } } CanvasConfig + * @typedef { { + * group: SVGElement; + * index: number; + * visible: boolean; + * } } CanvasLayer + * @typedef { { + * [key: string]: CanvasLayer; + * } } CanvasLayers + * @typedef { { + * rootElement: ShapeLike; + * layer: CanvasLayer; + * } } CanvasPlane + * @typedef { { + * scale: number; + * inner: Rect; + * outer: Dimensions; + * } & Rect } CanvasViewbox + * + * @typedef {import('./ElementRegistry').default} ElementRegistry + * @typedef {import('./EventBus').default} EventBus + * @typedef {import('./GraphicsFactory').default} GraphicsFactory + * + * @typedef {import('../util/Types').Dimensions} Dimensions + * @typedef {import('../util/Types').Point} Point + * @typedef {import('../util/Types').Rect} Rect + * @typedef {import('../util/Types').RectTRBL} RectTRBL + */ + + function round(number, resolution) { + return Math.round(number * resolution) / resolution; + } + + function ensurePx(number) { + return isNumber(number) ? number + 'px' : number; + } + + function findRoot(element) { + while (element.parent) { + element = element.parent; + } + + return element; + } + + /** + * Creates a HTML container element for a SVG element with + * the given configuration + * + * @param {CanvasConfig} options + * + * @return {HTMLElement} the container element + */ + function createContainer(options) { + + options = assign$1({}, { width: '100%', height: '100%' }, options); + + const container = options.container || document.body; + + // create a
    around the svg element with the respective size + // this way we can always get the correct container size + // (this is impossible for elements at the moment) + const parent = document.createElement('div'); + parent.setAttribute('class', 'djs-container djs-parent'); + + assign(parent, { + position: 'relative', + overflow: 'hidden', + width: ensurePx(options.width), + height: ensurePx(options.height) + }); + + container.appendChild(parent); + + return parent; + } + + function createGroup(parent, cls, childIndex) { + const group = create$1('g'); + classes$1(group).add(cls); + + const index = childIndex !== undefined ? childIndex : parent.childNodes.length - 1; + + // must ensure second argument is node or _null_ + // cf. https://developer.mozilla.org/en-US/docs/Web/API/Node/insertBefore + parent.insertBefore(group, parent.childNodes[index] || null); + + return group; + } + + const BASE_LAYER = 'base'; + + // render plane contents behind utility layers + const PLANE_LAYER_INDEX = 0; + const UTILITY_LAYER_INDEX = 1; + + + const REQUIRED_MODEL_ATTRS = { + shape: [ 'x', 'y', 'width', 'height' ], + connection: [ 'waypoints' ] + }; + + /** + * The main drawing canvas. + * + * @class + * @constructor + * + * @emits Canvas#canvas.init + * + * @param {CanvasConfig|null} config + * @param {EventBus} eventBus + * @param {GraphicsFactory} graphicsFactory + * @param {ElementRegistry} elementRegistry + */ + function Canvas(config, eventBus, graphicsFactory, elementRegistry) { + this._eventBus = eventBus; + this._elementRegistry = elementRegistry; + this._graphicsFactory = graphicsFactory; + + /** + * @type {number} + */ + this._rootsIdx = 0; + + /** + * @type {CanvasLayers} + */ + this._layers = {}; + + /** + * @type {CanvasPlane[]} + */ + this._planes = []; + + /** + * @type {RootLike|null} + */ + this._rootElement = null; + + this._init(config || {}); + } + + Canvas.$inject = [ + 'config.canvas', + 'eventBus', + 'graphicsFactory', + 'elementRegistry' + ]; + + /** + * Creates a element that is wrapped into a
    . + * This way we are always able to correctly figure out the size of the svg element + * by querying the parent node. + + * (It is not possible to get the size of a svg element cross browser @ 2014-04-01) + + *
    + * + * ... + * + *
    + * + * @param {CanvasConfig} config + */ + Canvas.prototype._init = function(config) { + + const eventBus = this._eventBus; + + // html container + const container = this._container = createContainer(config); + + const svg = this._svg = create$1('svg'); + attr$1(svg, { width: '100%', height: '100%' }); + + append(container, svg); + + const viewport = this._viewport = createGroup(svg, 'viewport'); + + // debounce canvas.viewbox.changed events when deferUpdate is set + // to help with potential performance issues + if (config.deferUpdate) { + this._viewboxChanged = debounce(bind$2(this._viewboxChanged, this), 300); + } + + eventBus.on('diagram.init', () => { + + /** + * An event indicating that the canvas is ready to be drawn on. + * + * @memberOf Canvas + * + * @event canvas.init + * + * @type {Object} + * @property {SVGElement} svg the created svg element + * @property {SVGElement} viewport the direct parent of diagram elements and shapes + */ + eventBus.fire('canvas.init', { + svg: svg, + viewport: viewport + }); + + }); + + // reset viewbox on shape changes to + // recompute the viewbox + eventBus.on([ + 'shape.added', + 'connection.added', + 'shape.removed', + 'connection.removed', + 'elements.changed', + 'root.set' + ], () => { + delete this._cachedViewbox; + }); + + eventBus.on('diagram.destroy', 500, this._destroy, this); + eventBus.on('diagram.clear', 500, this._clear, this); + }; + + Canvas.prototype._destroy = function() { + this._eventBus.fire('canvas.destroy', { + svg: this._svg, + viewport: this._viewport + }); + + const parent = this._container.parentNode; + + if (parent) { + parent.removeChild(this._container); + } + + delete this._svg; + delete this._container; + delete this._layers; + delete this._planes; + delete this._rootElement; + delete this._viewport; + }; + + Canvas.prototype._clear = function() { + + const allElements = this._elementRegistry.getAll(); + + // remove all elements + allElements.forEach(element => { + const type = getType(element); + + if (type === 'root') { + this.removeRootElement(element); + } else { + this._removeElement(element, type); + } + }); + + // remove all planes + this._planes = []; + this._rootElement = null; + + // force recomputation of view box + delete this._cachedViewbox; + }; + + /** + * Returns the default layer on which + * all elements are drawn. + * + * @return {SVGElement} The SVG element of the layer. + */ + Canvas.prototype.getDefaultLayer = function() { + return this.getLayer(BASE_LAYER, PLANE_LAYER_INDEX); + }; + + /** + * Returns a layer that is used to draw elements + * or annotations on it. + * + * Non-existing layers retrieved through this method + * will be created. During creation, the optional index + * may be used to create layers below or above existing layers. + * A layer with a certain index is always created above all + * existing layers with the same index. + * + * @param {string} name The name of the layer. + * @param {number} [index] The index of the layer. + * + * @return {SVGElement} The SVG element of the layer. + */ + Canvas.prototype.getLayer = function(name, index) { + + if (!name) { + throw new Error('must specify a name'); + } + + let layer = this._layers[name]; + + if (!layer) { + layer = this._layers[name] = this._createLayer(name, index); + } + + // throw an error if layer creation / retrival is + // requested on different index + if (typeof index !== 'undefined' && layer.index !== index) { + throw new Error('layer <' + name + '> already created at index <' + index + '>'); + } + + return layer.group; + }; + + /** + * For a given index, return the number of layers that have a higher index and + * are visible. + * + * This is used to determine the node a layer should be inserted at. + * + * @param {number} index + * + * @return {number} + */ + Canvas.prototype._getChildIndex = function(index) { + return reduce(this._layers, function(childIndex, layer) { + if (layer.visible && index >= layer.index) { + childIndex++; + } + + return childIndex; + }, 0); + }; + + /** + * Creates a given layer and returns it. + * + * @param {string} name + * @param {number} [index=0] + * + * @return {CanvasLayer} + */ + Canvas.prototype._createLayer = function(name, index) { + + if (typeof index === 'undefined') { + index = UTILITY_LAYER_INDEX; + } + + const childIndex = this._getChildIndex(index); + + return { + group: createGroup(this._viewport, 'layer-' + name, childIndex), + index: index, + visible: true + }; + }; + + + /** + * Shows a given layer. + * + * @param {string} name The name of the layer. + * + * @return {SVGElement} The SVG element of the layer. + */ + Canvas.prototype.showLayer = function(name) { + + if (!name) { + throw new Error('must specify a name'); + } + + const layer = this._layers[name]; + + if (!layer) { + throw new Error('layer <' + name + '> does not exist'); + } + + const viewport = this._viewport; + const group = layer.group; + const index = layer.index; + + if (layer.visible) { + return group; + } + + const childIndex = this._getChildIndex(index); + + viewport.insertBefore(group, viewport.childNodes[childIndex] || null); + + layer.visible = true; + + return group; + }; + + /** + * Hides a given layer. + * + * @param {string} name The name of the layer. + * + * @return {SVGElement} The SVG element of the layer. + */ + Canvas.prototype.hideLayer = function(name) { + + if (!name) { + throw new Error('must specify a name'); + } + + const layer = this._layers[name]; + + if (!layer) { + throw new Error('layer <' + name + '> does not exist'); + } + + const group = layer.group; + + if (!layer.visible) { + return group; + } + + remove$2(group); + + layer.visible = false; + + return group; + }; + + + Canvas.prototype._removeLayer = function(name) { + + const layer = this._layers[name]; + + if (layer) { + delete this._layers[name]; + + remove$2(layer.group); + } + }; + + /** + * Returns the currently active layer. Can be null. + * + * @return {CanvasLayer|null} The active layer of `null`. + */ + Canvas.prototype.getActiveLayer = function() { + const plane = this._findPlaneForRoot(this.getRootElement()); + + if (!plane) { + return null; + } + + return plane.layer; + }; + + + /** + * Returns the plane which contains the given element. + * + * @param {ShapeLike|ConnectionLike|string} element The element or its ID. + * + * @return {RootLike|undefined} The root of the element. + */ + Canvas.prototype.findRoot = function(element) { + if (typeof element === 'string') { + element = this._elementRegistry.get(element); + } + + if (!element) { + return; + } + + const plane = this._findPlaneForRoot( + findRoot(element) + ) || {}; + + return plane.rootElement; + }; + + /** + * Return a list of all root elements on the diagram. + * + * @return {(RootLike)[]} The list of root elements. + */ + Canvas.prototype.getRootElements = function() { + return this._planes.map(function(plane) { + return plane.rootElement; + }); + }; + + Canvas.prototype._findPlaneForRoot = function(rootElement) { + return find(this._planes, function(plane) { + return plane.rootElement === rootElement; + }); + }; + + + /** + * Returns the html element that encloses the + * drawing canvas. + * + * @return {HTMLElement} The HTML element of the container. + */ + Canvas.prototype.getContainer = function() { + return this._container; + }; + + + // markers ////////////////////// + + Canvas.prototype._updateMarker = function(element, marker, add) { + let container; + + if (!element.id) { + element = this._elementRegistry.get(element); + } + + // we need to access all + container = this._elementRegistry._elements[element.id]; + + if (!container) { + return; + } + + forEach$1([ container.gfx, container.secondaryGfx ], function(gfx) { + if (gfx) { + + // invoke either addClass or removeClass based on mode + if (add) { + classes$1(gfx).add(marker); + } else { + classes$1(gfx).remove(marker); + } + } + }); + + /** + * An event indicating that a marker has been updated for an element + * + * @event element.marker.update + * @type {Object} + * @property {Element} element the shape + * @property {SVGElement} gfx the graphical representation of the shape + * @property {string} marker + * @property {boolean} add true if the marker was added, false if it got removed + */ + this._eventBus.fire('element.marker.update', { element: element, gfx: container.gfx, marker: marker, add: !!add }); + }; + + + /** + * Adds a marker to an element (basically a css class). + * + * Fires the element.marker.update event, making it possible to + * integrate extension into the marker life-cycle, too. + * + * @example + * + * ```javascript + * canvas.addMarker('foo', 'some-marker'); + * + * const fooGfx = canvas.getGraphics('foo'); + * + * fooGfx; // ... + * ``` + * + * @param {ShapeLike|ConnectionLike|string} element The element or its ID. + * @param {string} marker The marker. + */ + Canvas.prototype.addMarker = function(element, marker) { + this._updateMarker(element, marker, true); + }; + + + /** + * Remove a marker from an element. + * + * Fires the element.marker.update event, making it possible to + * integrate extension into the marker life-cycle, too. + * + * @param {ShapeLike|ConnectionLike|string} element The element or its ID. + * @param {string} marker The marker. + */ + Canvas.prototype.removeMarker = function(element, marker) { + this._updateMarker(element, marker, false); + }; + + /** + * Check whether an element has a given marker. + * + * @param {ShapeLike|ConnectionLike|string} element The element or its ID. + * @param {string} marker The marker. + */ + Canvas.prototype.hasMarker = function(element, marker) { + if (!element.id) { + element = this._elementRegistry.get(element); + } + + const gfx = this.getGraphics(element); + + return classes$1(gfx).has(marker); + }; + + /** + * Toggles a marker on an element. + * + * Fires the element.marker.update event, making it possible to + * integrate extension into the marker life-cycle, too. + * + * @param {ShapeLike|ConnectionLike|string} element The element or its ID. + * @param {string} marker The marker. + */ + Canvas.prototype.toggleMarker = function(element, marker) { + if (this.hasMarker(element, marker)) { + this.removeMarker(element, marker); + } else { + this.addMarker(element, marker); + } + }; + + /** + * Returns the current root element. + * + * Supports two different modes for handling root elements: + * + * 1. if no root element has been added before, an implicit root will be added + * and returned. This is used in applications that don't require explicit + * root elements. + * + * 2. when root elements have been added before calling `getRootElement`, + * root elements can be null. This is used for applications that want to manage + * root elements themselves. + * + * @return {RootLike} The current root element. + */ + Canvas.prototype.getRootElement = function() { + const rootElement = this._rootElement; + + // can return null if root elements are present but none was set yet + if (rootElement || this._planes.length) { + return rootElement; + } + + return this.setRootElement(this.addRootElement(null)); + }; + + /** + * Adds a given root element and returns it. + * + * @param {RootLike} [rootElement] The root element to be added. + * + * @return {RootLike} The added root element or an implicit root element. + */ + Canvas.prototype.addRootElement = function(rootElement) { + const idx = this._rootsIdx++; + + if (!rootElement) { + rootElement = { + id: '__implicitroot_' + idx, + children: [], + isImplicit: true + }; + } + + const layerName = rootElement.layer = 'root-' + idx; + + this._ensureValid('root', rootElement); + + const layer = this.getLayer(layerName, PLANE_LAYER_INDEX); + + this.hideLayer(layerName); + + this._addRoot(rootElement, layer); + + this._planes.push({ + rootElement: rootElement, + layer: layer + }); + + return rootElement; + }; + + /** + * Removes a given root element and returns it. + * + * @param {RootLike|string} rootElement element or element ID + * + * @return {RootLike|undefined} removed element + */ + Canvas.prototype.removeRootElement = function(rootElement) { + + if (typeof rootElement === 'string') { + rootElement = this._elementRegistry.get(rootElement); + } + + const plane = this._findPlaneForRoot(rootElement); + + if (!plane) { + return; + } + + // hook up life-cycle events + this._removeRoot(rootElement); + + // clean up layer + this._removeLayer(rootElement.layer); + + // clean up plane + this._planes = this._planes.filter(function(plane) { + return plane.rootElement !== rootElement; + }); + + // clean up active root + if (this._rootElement === rootElement) { + this._rootElement = null; + } + + return rootElement; + }; + + + /** + * Sets a given element as the new root element for the canvas + * and returns the new root element. + * + * @param {RootLike} rootElement The root element to be set. + * + * @return {RootLike} The set root element. + */ + Canvas.prototype.setRootElement = function(rootElement) { + + if (rootElement === this._rootElement) { + return; + } + + let plane; + + if (!rootElement) { + throw new Error('rootElement required'); + } + + plane = this._findPlaneForRoot(rootElement); + + // give set add semantics for backwards compatibility + if (!plane) { + rootElement = this.addRootElement(rootElement); + } + + this._setRoot(rootElement); + + return rootElement; + }; + + + Canvas.prototype._removeRoot = function(element) { + const elementRegistry = this._elementRegistry, + eventBus = this._eventBus; + + // simulate element remove event sequence + eventBus.fire('root.remove', { element: element }); + eventBus.fire('root.removed', { element: element }); + + elementRegistry.remove(element); + }; + + + Canvas.prototype._addRoot = function(element, gfx) { + const elementRegistry = this._elementRegistry, + eventBus = this._eventBus; + + // resemble element add event sequence + eventBus.fire('root.add', { element: element }); + + elementRegistry.add(element, gfx); + + eventBus.fire('root.added', { element: element, gfx: gfx }); + }; + + + Canvas.prototype._setRoot = function(rootElement, layer) { + + const currentRoot = this._rootElement; + + if (currentRoot) { + + // un-associate previous root element + this._elementRegistry.updateGraphics(currentRoot, null, true); + + // hide previous layer + this.hideLayer(currentRoot.layer); + } + + if (rootElement) { + + if (!layer) { + layer = this._findPlaneForRoot(rootElement).layer; + } + + // associate element with + this._elementRegistry.updateGraphics(rootElement, this._svg, true); + + // show root layer + this.showLayer(rootElement.layer); + } + + this._rootElement = rootElement; + + this._eventBus.fire('root.set', { element: rootElement }); + }; + + Canvas.prototype._ensureValid = function(type, element) { + if (!element.id) { + throw new Error('element must have an id'); + } + + if (this._elementRegistry.get(element.id)) { + throw new Error('element <' + element.id + '> already exists'); + } + + const requiredAttrs = REQUIRED_MODEL_ATTRS[type]; + + const valid = every(requiredAttrs, function(attr) { + return typeof element[attr] !== 'undefined'; + }); + + if (!valid) { + throw new Error( + 'must supply { ' + requiredAttrs.join(', ') + ' } with ' + type); + } + }; + + Canvas.prototype._setParent = function(element, parent, parentIndex) { + add(parent.children, element, parentIndex); + element.parent = parent; + }; + + /** + * Adds an element to the canvas. + * + * This wires the parent <-> child relationship between the element and + * a explicitly specified parent or an implicit root element. + * + * During add it emits the events + * + * * <{type}.add> (element, parent) + * * <{type}.added> (element, gfx) + * + * Extensions may hook into these events to perform their magic. + * + * @param {string} type + * @param {ConnectionLike|ShapeLike} element + * @param {ShapeLike} [parent] + * @param {number} [parentIndex] + * + * @return {ConnectionLike|ShapeLike} The added element. + */ + Canvas.prototype._addElement = function(type, element, parent, parentIndex) { + + parent = parent || this.getRootElement(); + + const eventBus = this._eventBus, + graphicsFactory = this._graphicsFactory; + + this._ensureValid(type, element); + + eventBus.fire(type + '.add', { element: element, parent: parent }); + + this._setParent(element, parent, parentIndex); + + // create graphics + const gfx = graphicsFactory.create(type, element, parentIndex); + + this._elementRegistry.add(element, gfx); + + // update its visual + graphicsFactory.update(type, element, gfx); + + eventBus.fire(type + '.added', { element: element, gfx: gfx }); + + return element; + }; + + /** + * Adds a shape to the canvas. + * + * @param {ShapeLike} shape The shape to be added + * @param {ParentLike} [parent] The shape's parent. + * @param {number} [parentIndex] The index at which to add the shape to the parent's children. + * + * @return {ShapeLike} The added shape. + */ + Canvas.prototype.addShape = function(shape, parent, parentIndex) { + return this._addElement('shape', shape, parent, parentIndex); + }; + + /** + * Adds a connection to the canvas. + * + * @param {ConnectionLike} connection The connection to be added. + * @param {ParentLike} [parent] The connection's parent. + * @param {number} [parentIndex] The index at which to add the connection to the parent's children. + * + * @return {ConnectionLike} The added connection. + */ + Canvas.prototype.addConnection = function(connection, parent, parentIndex) { + return this._addElement('connection', connection, parent, parentIndex); + }; + + + /** + * Internal remove element + */ + Canvas.prototype._removeElement = function(element, type) { + + const elementRegistry = this._elementRegistry, + graphicsFactory = this._graphicsFactory, + eventBus = this._eventBus; + + element = elementRegistry.get(element.id || element); + + if (!element) { + + // element was removed already + return; + } + + eventBus.fire(type + '.remove', { element: element }); + + graphicsFactory.remove(element); + + // unset parent <-> child relationship + remove(element.parent && element.parent.children, element); + element.parent = null; + + eventBus.fire(type + '.removed', { element: element }); + + elementRegistry.remove(element); + + return element; + }; + + + /** + * Removes a shape from the canvas. + * + * @fires ShapeRemoveEvent + * @fires ShapeRemovedEvent + * + * @param {ShapeLike|string} shape The shape or its ID. + * + * @return {ShapeLike} The removed shape. + */ + Canvas.prototype.removeShape = function(shape) { + + /** + * An event indicating that a shape is about to be removed from the canvas. + * + * @memberOf Canvas + * + * @event ShapeRemoveEvent + * @type {Object} + * @property {ShapeLike} element The shape. + * @property {SVGElement} gfx The graphical element. + */ + + /** + * An event indicating that a shape has been removed from the canvas. + * + * @memberOf Canvas + * + * @event ShapeRemovedEvent + * @type {Object} + * @property {ShapeLike} element The shape. + * @property {SVGElement} gfx The graphical element. + */ + return this._removeElement(shape, 'shape'); + }; + + + /** + * Removes a connection from the canvas. + * + * @fires ConnectionRemoveEvent + * @fires ConnectionRemovedEvent + * + * @param {ConnectionLike|string} connection The connection or its ID. + * + * @return {ConnectionLike} The removed connection. + */ + Canvas.prototype.removeConnection = function(connection) { + + /** + * An event indicating that a connection is about to be removed from the canvas. + * + * @memberOf Canvas + * + * @event ConnectionRemoveEvent + * @type {Object} + * @property {ConnectionLike} element The connection. + * @property {SVGElement} gfx The graphical element. + */ + + /** + * An event indicating that a connection has been removed from the canvas. + * + * @memberOf Canvas + * + * @event ConnectionRemovedEvent + * @type {Object} + * @property {ConnectionLike} element The connection. + * @property {SVGElement} gfx The graphical element. + */ + return this._removeElement(connection, 'connection'); + }; + + + /** + * Returns the graphical element of an element. + * + * @param {ShapeLike|ConnectionLike|string} element The element or its ID. + * @param {boolean} [secondary=false] Whether to return the secondary graphical element. + * + * @return {SVGElement} The graphical element. + */ + Canvas.prototype.getGraphics = function(element, secondary) { + return this._elementRegistry.getGraphics(element, secondary); + }; + + + /** + * Perform a viewbox update via a given change function. + * + * @param {Function} changeFn + */ + Canvas.prototype._changeViewbox = function(changeFn) { + + // notify others of the upcoming viewbox change + this._eventBus.fire('canvas.viewbox.changing'); + + // perform actual change + changeFn.apply(this); + + // reset the cached viewbox so that + // a new get operation on viewbox or zoom + // triggers a viewbox re-computation + this._cachedViewbox = null; + + // notify others of the change; this step + // may or may not be debounced + this._viewboxChanged(); + }; + + Canvas.prototype._viewboxChanged = function() { + this._eventBus.fire('canvas.viewbox.changed', { viewbox: this.viewbox() }); + }; + + + /** + * Gets or sets the view box of the canvas, i.e. the + * area that is currently displayed. + * + * The getter may return a cached viewbox (if it is currently + * changing). To force a recomputation, pass `false` as the first argument. + * + * @example + * + * ```javascript + * canvas.viewbox({ x: 100, y: 100, width: 500, height: 500 }) + * + * // sets the visible area of the diagram to (100|100) -> (600|100) + * // and and scales it according to the diagram width + * + * const viewbox = canvas.viewbox(); // pass `false` to force recomputing the box. + * + * console.log(viewbox); + * // { + * // inner: Dimensions, + * // outer: Dimensions, + * // scale, + * // x, y, + * // width, height + * // } + * + * // if the current diagram is zoomed and scrolled, you may reset it to the + * // default zoom via this method, too: + * + * const zoomedAndScrolledViewbox = canvas.viewbox(); + * + * canvas.viewbox({ + * x: 0, + * y: 0, + * width: zoomedAndScrolledViewbox.outer.width, + * height: zoomedAndScrolledViewbox.outer.height + * }); + * ``` + * + * @param {Rect} [box] The viewbox to be set. + * + * @return {CanvasViewbox} The set viewbox. + */ + Canvas.prototype.viewbox = function(box) { + + if (box === undefined && this._cachedViewbox) { + return this._cachedViewbox; + } + + const viewport = this._viewport, + outerBox = this.getSize(); + let innerBox, + matrix, + activeLayer, + transform, + scale, + x, y; + + if (!box) { + + // compute the inner box based on the + // diagrams active layer. This allows us to exclude + // external components, such as overlays + + activeLayer = this._rootElement ? this.getActiveLayer() : null; + innerBox = activeLayer && activeLayer.getBBox() || {}; + + transform = transform$1(viewport); + matrix = transform ? transform.matrix : createMatrix(); + scale = round(matrix.a, 1000); + + x = round(-matrix.e || 0, 1000); + y = round(-matrix.f || 0, 1000); + + box = this._cachedViewbox = { + x: x ? x / scale : 0, + y: y ? y / scale : 0, + width: outerBox.width / scale, + height: outerBox.height / scale, + scale: scale, + inner: { + width: innerBox.width || 0, + height: innerBox.height || 0, + x: innerBox.x || 0, + y: innerBox.y || 0 + }, + outer: outerBox + }; + + return box; + } else { + + this._changeViewbox(function() { + scale = Math.min(outerBox.width / box.width, outerBox.height / box.height); + + const matrix = this._svg.createSVGMatrix() + .scale(scale) + .translate(-box.x, -box.y); + + transform$1(viewport, matrix); + }); + } + + return box; + }; + + + /** + * Gets or sets the scroll of the canvas. + * + * @param {Point} [delta] The scroll to be set. + * + * @return {Point} + */ + Canvas.prototype.scroll = function(delta) { + + const node = this._viewport; + let matrix = node.getCTM(); + + if (delta) { + this._changeViewbox(function() { + delta = assign$1({ dx: 0, dy: 0 }, delta || {}); + + matrix = this._svg.createSVGMatrix().translate(delta.dx, delta.dy).multiply(matrix); + + setCTM(node, matrix); + }); + } + + return { x: matrix.e, y: matrix.f }; + }; + + /** + * Scrolls the viewbox to contain the given element. + * Optionally specify a padding to be applied to the edges. + * + * @param {ShapeLike|ConnectionLike|string} element The element to scroll to or its ID. + * @param {RectTRBL|number} [padding=100] The padding to be applied. Can also specify top, bottom, left and right. + */ + Canvas.prototype.scrollToElement = function(element, padding) { + let defaultPadding = 100; + + if (typeof element === 'string') { + element = this._elementRegistry.get(element); + } + + // set to correct rootElement + const rootElement = this.findRoot(element); + + if (rootElement !== this.getRootElement()) { + this.setRootElement(rootElement); + } + + // element is rootElement, do not change viewport + if (rootElement === element) { + return; + } + + if (!padding) { + padding = {}; + } + if (typeof padding === 'number') { + defaultPadding = padding; + } + + padding = { + top: padding.top || defaultPadding, + right: padding.right || defaultPadding, + bottom: padding.bottom || defaultPadding, + left: padding.left || defaultPadding + }; + + const elementBounds = getBBox(element), + elementTrbl = asTRBL(elementBounds), + viewboxBounds = this.viewbox(), + zoom = this.zoom(); + let dx, dy; + + // shrink viewboxBounds with padding + viewboxBounds.y += padding.top / zoom; + viewboxBounds.x += padding.left / zoom; + viewboxBounds.width -= (padding.right + padding.left) / zoom; + viewboxBounds.height -= (padding.bottom + padding.top) / zoom; + + const viewboxTrbl = asTRBL(viewboxBounds); + + const canFit = elementBounds.width < viewboxBounds.width && elementBounds.height < viewboxBounds.height; + + if (!canFit) { + + // top-left when element can't fit + dx = elementBounds.x - viewboxBounds.x; + dy = elementBounds.y - viewboxBounds.y; + + } else { + + const dRight = Math.max(0, elementTrbl.right - viewboxTrbl.right), + dLeft = Math.min(0, elementTrbl.left - viewboxTrbl.left), + dBottom = Math.max(0, elementTrbl.bottom - viewboxTrbl.bottom), + dTop = Math.min(0, elementTrbl.top - viewboxTrbl.top); + + dx = dRight || dLeft; + dy = dBottom || dTop; + + } + + this.scroll({ dx: -dx * zoom, dy: -dy * zoom }); + }; + + /** + * Gets or sets the current zoom of the canvas, optionally zooming to the + * specified position. + * + * The getter may return a cached zoom level. Call it with `false` as the first + * argument to force recomputation of the current level. + * + * @param {number|'fit-viewport'} [newScale] The new zoom level, either a number, + * i.e. 0.9, or `fit-viewport` to adjust the size to fit the current viewport. + * @param {Point} [center] The reference point { x: ..., y: ...} to zoom to. + * + * @return {number} The set zoom level. + */ + Canvas.prototype.zoom = function(newScale, center) { + + if (!newScale) { + return this.viewbox(newScale).scale; + } + + if (newScale === 'fit-viewport') { + return this._fitViewport(center); + } + + let outer, + matrix; + + this._changeViewbox(function() { + + if (typeof center !== 'object') { + outer = this.viewbox().outer; + + center = { + x: outer.width / 2, + y: outer.height / 2 + }; + } + + matrix = this._setZoom(newScale, center); + }); + + return round(matrix.a, 1000); + }; + + function setCTM(node, m) { + const mstr = 'matrix(' + m.a + ',' + m.b + ',' + m.c + ',' + m.d + ',' + m.e + ',' + m.f + ')'; + node.setAttribute('transform', mstr); + } + + Canvas.prototype._fitViewport = function(center) { + + const vbox = this.viewbox(), + outer = vbox.outer, + inner = vbox.inner; + let newScale, + newViewbox; + + // display the complete diagram without zooming in. + // instead of relying on internal zoom, we perform a + // hard reset on the canvas viewbox to realize this + // + // if diagram does not need to be zoomed in, we focus it around + // the diagram origin instead + + if (inner.x >= 0 && + inner.y >= 0 && + inner.x + inner.width <= outer.width && + inner.y + inner.height <= outer.height && + !center) { + + newViewbox = { + x: 0, + y: 0, + width: Math.max(inner.width + inner.x, outer.width), + height: Math.max(inner.height + inner.y, outer.height) + }; + } else { + + newScale = Math.min(1, outer.width / inner.width, outer.height / inner.height); + newViewbox = { + x: inner.x + (center ? inner.width / 2 - outer.width / newScale / 2 : 0), + y: inner.y + (center ? inner.height / 2 - outer.height / newScale / 2 : 0), + width: outer.width / newScale, + height: outer.height / newScale + }; + } + + this.viewbox(newViewbox); + + return this.viewbox(false).scale; + }; + + + Canvas.prototype._setZoom = function(scale, center) { + + const svg = this._svg, + viewport = this._viewport; + + const matrix = svg.createSVGMatrix(); + const point = svg.createSVGPoint(); + + let centerPoint, + originalPoint, + currentMatrix, + scaleMatrix, + newMatrix; + + currentMatrix = viewport.getCTM(); + + const currentScale = currentMatrix.a; + + if (center) { + centerPoint = assign$1(point, center); + + // revert applied viewport transformations + originalPoint = centerPoint.matrixTransform(currentMatrix.inverse()); + + // create scale matrix + scaleMatrix = matrix + .translate(originalPoint.x, originalPoint.y) + .scale(1 / currentScale * scale) + .translate(-originalPoint.x, -originalPoint.y); + + newMatrix = currentMatrix.multiply(scaleMatrix); + } else { + newMatrix = matrix.scale(scale); + } + + setCTM(this._viewport, newMatrix); + + return newMatrix; + }; + + + /** + * Returns the size of the canvas. + * + * @return {Dimensions} The size of the canvas. + */ + Canvas.prototype.getSize = function() { + return { + width: this._container.clientWidth, + height: this._container.clientHeight + }; + }; + + + /** + * Returns the absolute bounding box of an element. + * + * The absolute bounding box may be used to display overlays in the callers + * (browser) coordinate system rather than the zoomed in/out canvas coordinates. + * + * @param {ShapeLike|ConnectionLike} element The element. + * + * @return {Rect} The element's absolute bounding box. + */ + Canvas.prototype.getAbsoluteBBox = function(element) { + const vbox = this.viewbox(); + let bbox; + + // connection + // use svg bbox + if (element.waypoints) { + const gfx = this.getGraphics(element); + + bbox = gfx.getBBox(); + } + + // shapes + // use data + else { + bbox = element; + } + + const x = bbox.x * vbox.scale - vbox.x * vbox.scale; + const y = bbox.y * vbox.scale - vbox.y * vbox.scale; + + const width = bbox.width * vbox.scale; + const height = bbox.height * vbox.scale; + + return { + x: x, + y: y, + width: width, + height: height + }; + }; + + /** + * Fires an event so other modules can react to the canvas resizing. + */ + Canvas.prototype.resized = function() { + + // force recomputation of view box + delete this._cachedViewbox; + + this._eventBus.fire('canvas.resized'); + }; + + var ELEMENT_ID = 'data-element-id'; + + /** + * @typedef {import('./Types').ElementLike} ElementLike + * + * @typedef {import('./EventBus').default} EventBus + * + * @typedef { (element: ElementLike, gfx: SVGElement) => boolean|any } ElementRegistryFilterCallback + * @typedef { (element: ElementLike, gfx: SVGElement) => any } ElementRegistryForEachCallback + */ + + /** + * A registry that keeps track of all shapes in the diagram. + * + * @class + * @constructor + * + * @param {EventBus} eventBus + */ + function ElementRegistry(eventBus) { + + /** + * @type { { + * [id: string]: { + * element: ElementLike; + * gfx?: SVGElement; + * secondaryGfx?: SVGElement; + * } + * } } + */ + this._elements = {}; + + this._eventBus = eventBus; + } + + ElementRegistry.$inject = [ 'eventBus' ]; + + /** + * Add an element and its graphical representation(s) to the registry. + * + * @param {ElementLike} element The element to be added. + * @param {SVGElement} gfx The primary graphical representation. + * @param {SVGElement} [secondaryGfx] The secondary graphical representation. + */ + ElementRegistry.prototype.add = function(element, gfx, secondaryGfx) { + + var id = element.id; + + this._validateId(id); + + // associate dom node with element + attr$1(gfx, ELEMENT_ID, id); + + if (secondaryGfx) { + attr$1(secondaryGfx, ELEMENT_ID, id); + } + + this._elements[id] = { element: element, gfx: gfx, secondaryGfx: secondaryGfx }; + }; + + /** + * Remove an element from the registry. + * + * @param {ElementLike|string} element + */ + ElementRegistry.prototype.remove = function(element) { + var elements = this._elements, + id = element.id || element, + container = id && elements[id]; + + if (container) { + + // unset element id on gfx + attr$1(container.gfx, ELEMENT_ID, ''); + + if (container.secondaryGfx) { + attr$1(container.secondaryGfx, ELEMENT_ID, ''); + } + + delete elements[id]; + } + }; + + /** + * Update an elements ID. + * + * @param {ElementLike|string} element The element or its ID. + * @param {string} newId The new ID. + */ + ElementRegistry.prototype.updateId = function(element, newId) { + + this._validateId(newId); + + if (typeof element === 'string') { + element = this.get(element); + } + + this._eventBus.fire('element.updateId', { + element: element, + newId: newId + }); + + var gfx = this.getGraphics(element), + secondaryGfx = this.getGraphics(element, true); + + this.remove(element); + + element.id = newId; + + this.add(element, gfx, secondaryGfx); + }; + + /** + * Update the graphical representation of an element. + * + * @param {ElementLike|string} filter The element or its ID. + * @param {SVGElement} gfx The new graphical representation. + * @param {boolean} [secondary=false] Whether to update the secondary graphical representation. + */ + ElementRegistry.prototype.updateGraphics = function(filter, gfx, secondary) { + var id = filter.id || filter; + + var container = this._elements[id]; + + if (secondary) { + container.secondaryGfx = gfx; + } else { + container.gfx = gfx; + } + + if (gfx) { + attr$1(gfx, ELEMENT_ID, id); + } + + return gfx; + }; + + /** + * Get the element with the given ID or graphical representation. + * + * @example + * + * ```javascript + * elementRegistry.get('SomeElementId_1'); + * + * elementRegistry.get(gfx); + * ``` + * + * @param {string|SVGElement} filter The elements ID or graphical representation. + * + * @return {ElementLike|undefined} The element. + */ + ElementRegistry.prototype.get = function(filter) { + var id; + + if (typeof filter === 'string') { + id = filter; + } else { + id = filter && attr$1(filter, ELEMENT_ID); + } + + var container = this._elements[id]; + return container && container.element; + }; + + /** + * Return all elements that match a given filter function. + * + * @param {ElementRegistryFilterCallback} fn The filter function. + * + * @return {ElementLike[]} The matching elements. + */ + ElementRegistry.prototype.filter = function(fn) { + + var filtered = []; + + this.forEach(function(element, gfx) { + if (fn(element, gfx)) { + filtered.push(element); + } + }); + + return filtered; + }; + + /** + * Return the first element that matches the given filter function. + * + * @param {ElementRegistryFilterCallback} fn The filter function. + * + * @return {ElementLike|undefined} The matching element. + */ + ElementRegistry.prototype.find = function(fn) { + var map = this._elements, + keys = Object.keys(map); + + for (var i = 0; i < keys.length; i++) { + var id = keys[i], + container = map[id], + element = container.element, + gfx = container.gfx; + + if (fn(element, gfx)) { + return element; + } + } + }; + + /** + * Get all elements. + * + * @return {ElementLike[]} All elements. + */ + ElementRegistry.prototype.getAll = function() { + return this.filter(function(e) { return e; }); + }; + + /** + * Execute a given function for each element. + * + * @param {ElementRegistryForEachCallback} fn The function to execute. + */ + ElementRegistry.prototype.forEach = function(fn) { + + var map = this._elements; + + Object.keys(map).forEach(function(id) { + var container = map[id], + element = container.element, + gfx = container.gfx; + + return fn(element, gfx); + }); + }; + + /** + * Return the graphical representation of an element. + * + * @example + * + * ```javascript + * elementRegistry.getGraphics('SomeElementId_1'); + * + * elementRegistry.getGraphics(rootElement); // + * + * elementRegistry.getGraphics(rootElement, true); // + * ``` + * + * @param {ElementLike|string} filter The element or its ID. + * @param {boolean} [secondary=false] Whether to return the secondary graphical representation. + * + * @return {SVGElement} The graphical representation. + */ + ElementRegistry.prototype.getGraphics = function(filter, secondary) { + var id = filter.id || filter; + + var container = this._elements[id]; + return container && (secondary ? container.secondaryGfx : container.gfx); + }; + + /** + * Validate an ID and throw an error if invalid. + * + * @param {string} id + * + * @throws {Error} Error indicating that the ID is invalid or already assigned. + */ + ElementRegistry.prototype._validateId = function(id) { + if (!id) { + throw new Error('element must have an id'); + } + + if (this._elements[id]) { + throw new Error('element with id ' + id + ' already added'); + } + }; + + /** + * Extends a collection with {@link Refs} aware methods + * + * @param {Array} collection + * @param {Refs} refs instance + * @param {Object} property represented by the collection + * @param {Object} target object the collection is attached to + * + * @return {RefsCollection} the extended array + */ + function extend(collection, refs, property, target) { + var inverseProperty = property.inverse; + + /** + * Removes the given element from the array and returns it. + * + * @method RefsCollection#remove + * + * @param {Object} element the element to remove + */ + Object.defineProperty(collection, 'remove', { + value: function (element) { + var idx = this.indexOf(element); + if (idx !== -1) { + this.splice(idx, 1); + + // unset inverse + refs.unset(element, inverseProperty, target); + } + return element; + } + }); + + /** + * Returns true if the collection contains the given element + * + * @method RefsCollection#contains + * + * @param {Object} element the element to check for + */ + Object.defineProperty(collection, 'contains', { + value: function (element) { + return this.indexOf(element) !== -1; + } + }); + + /** + * Adds an element to the array, unless it exists already (set semantics). + * + * @method RefsCollection#add + * + * @param {Object} element the element to add + * @param {Number} optional index to add element to + * (possibly moving other elements around) + */ + Object.defineProperty(collection, 'add', { + value: function (element, idx) { + var currentIdx = this.indexOf(element); + if (typeof idx === 'undefined') { + if (currentIdx !== -1) { + // element already in collection (!) + return; + } + + // add to end of array, as no idx is specified + idx = this.length; + } + + // handle already in collection + if (currentIdx !== -1) { + // remove element from currentIdx + this.splice(currentIdx, 1); + } + + // add element at idx + this.splice(idx, 0, element); + if (currentIdx === -1) { + // set inverse, unless element was + // in collection already + refs.set(element, inverseProperty, target); + } + } + }); + + // a simple marker, identifying this element + // as being a refs collection + Object.defineProperty(collection, '__refs_collection', { + value: true + }); + return collection; + } + + /** + * Checks if a given collection is extended + * + * @param {Array} collection + * + * @return {boolean} + */ + function isExtended(collection) { + return collection.__refs_collection === true; + } + + function hasOwnProperty$1(e, property) { + return Object.prototype.hasOwnProperty.call(e, property.name || property); + } + function defineCollectionProperty(ref, property, target) { + var collection = extend(target[property.name] || [], ref, property, target); + Object.defineProperty(target, property.name, { + enumerable: property.enumerable, + value: collection + }); + if (collection.length) { + collection.forEach(function (o) { + ref.set(o, property.inverse, target); + }); + } + } + function defineProperty$1(ref, property, target) { + var inverseProperty = property.inverse; + var _value = target[property.name]; + Object.defineProperty(target, property.name, { + configurable: property.configurable, + enumerable: property.enumerable, + get: function () { + return _value; + }, + set: function (value) { + // return if we already performed all changes + if (value === _value) { + return; + } + var old = _value; + + // temporary set null + _value = null; + if (old) { + ref.unset(old, inverseProperty, target); + } + + // set new value + _value = value; + + // set inverse value + ref.set(_value, inverseProperty, target); + } + }); + } + + /** + * Creates a new references object defining two inversly related + * attribute descriptors a and b. + * + *

    + * When bound to an object using {@link Refs#bind} the references + * get activated and ensure that add and remove operations are applied + * reversely, too. + *

    + * + *

    + * For attributes represented as collections {@link Refs} provides the + * {@link RefsCollection#add}, {@link RefsCollection#remove} and {@link RefsCollection#contains} extensions + * that must be used to properly hook into the inverse change mechanism. + *

    + * + * @class Refs + * + * @classdesc A bi-directional reference between two attributes. + * + * @param {Refs.AttributeDescriptor} a property descriptor + * @param {Refs.AttributeDescriptor} b property descriptor + * + * @example + * + * var refs = Refs({ name: 'wheels', collection: true, enumerable: true }, { name: 'car' }); + * + * var car = { name: 'toyota' }; + * var wheels = [{ pos: 'front-left' }, { pos: 'front-right' }]; + * + * refs.bind(car, 'wheels'); + * + * car.wheels // [] + * car.wheels.add(wheels[0]); + * car.wheels.add(wheels[1]); + * + * car.wheels // [{ pos: 'front-left' }, { pos: 'front-right' }] + * + * wheels[0].car // { name: 'toyota' }; + * car.wheels.remove(wheels[0]); + * + * wheels[0].car // undefined + */ + function Refs(a, b) { + if (!(this instanceof Refs)) { + return new Refs(a, b); + } + + // link + a.inverse = b; + b.inverse = a; + this.props = {}; + this.props[a.name] = a; + this.props[b.name] = b; + } + + /** + * Binds one side of a bi-directional reference to a + * target object. + * + * @memberOf Refs + * + * @param {Object} target + * @param {String} property + */ + Refs.prototype.bind = function (target, property) { + if (typeof property === 'string') { + if (!this.props[property]) { + throw new Error('no property <' + property + '> in ref'); + } + property = this.props[property]; + } + if (property.collection) { + defineCollectionProperty(this, property, target); + } else { + defineProperty$1(this, property, target); + } + }; + Refs.prototype.ensureRefsCollection = function (target, property) { + var collection = target[property.name]; + if (!isExtended(collection)) { + defineCollectionProperty(this, property, target); + } + return collection; + }; + Refs.prototype.ensureBound = function (target, property) { + if (!hasOwnProperty$1(target, property)) { + this.bind(target, property); + } + }; + Refs.prototype.unset = function (target, property, value) { + if (target) { + this.ensureBound(target, property); + if (property.collection) { + this.ensureRefsCollection(target, property).remove(value); + } else { + target[property.name] = undefined; + } + } + }; + Refs.prototype.set = function (target, property, value) { + if (target) { + this.ensureBound(target, property); + if (property.collection) { + this.ensureRefsCollection(target, property).add(value); + } else { + target[property.name] = value; + } + } + }; + + var parentRefs = new Refs({ name: 'children', enumerable: true, collection: true }, { name: 'parent' }), + labelRefs = new Refs({ name: 'labels', enumerable: true, collection: true }, { name: 'labelTarget' }), + attacherRefs = new Refs({ name: 'attachers', collection: true }, { name: 'host' }), + outgoingRefs = new Refs({ name: 'outgoing', collection: true }, { name: 'source' }), + incomingRefs = new Refs({ name: 'incoming', collection: true }, { name: 'target' }); + + /** + * @typedef {import('./Types').Element} Element + * @typedef {import('./Types').Shape} Shape + * @typedef {import('./Types').Root} Root + * @typedef {import('./Types').Label} Label + * @typedef {import('./Types').Connection} Connection + */ + + /** + * The basic graphical representation + * + * @class + * @constructor + */ + function ElementImpl() { + + /** + * The object that backs up the shape + * + * @name Element#businessObject + * @type Object + */ + Object.defineProperty(this, 'businessObject', { + writable: true + }); + + + /** + * Single label support, will mapped to multi label array + * + * @name Element#label + * @type Object + */ + Object.defineProperty(this, 'label', { + get: function() { + return this.labels[0]; + }, + set: function(newLabel) { + + var label = this.label, + labels = this.labels; + + if (!newLabel && label) { + labels.remove(label); + } else { + labels.add(newLabel, 0); + } + } + }); + + /** + * The parent shape + * + * @name Element#parent + * @type Shape + */ + parentRefs.bind(this, 'parent'); + + /** + * The list of labels + * + * @name Element#labels + * @type Label + */ + labelRefs.bind(this, 'labels'); + + /** + * The list of outgoing connections + * + * @name Element#outgoing + * @type Array + */ + outgoingRefs.bind(this, 'outgoing'); + + /** + * The list of incoming connections + * + * @name Element#incoming + * @type Array + */ + incomingRefs.bind(this, 'incoming'); + } + + + /** + * A graphical object + * + * @class + * @constructor + * + * @extends ElementImpl + */ + function ShapeImpl() { + ElementImpl.call(this); + + /** + * Indicates frame shapes + * + * @name ShapeImpl#isFrame + * @type boolean + */ + + /** + * The list of children + * + * @name ShapeImpl#children + * @type Element[] + */ + parentRefs.bind(this, 'children'); + + /** + * @name ShapeImpl#host + * @type Shape + */ + attacherRefs.bind(this, 'host'); + + /** + * @name ShapeImpl#attachers + * @type Shape + */ + attacherRefs.bind(this, 'attachers'); + } + + e(ShapeImpl, ElementImpl); + + + /** + * A root graphical object + * + * @class + * @constructor + * + * @extends ElementImpl + */ + function RootImpl() { + ElementImpl.call(this); + + /** + * The list of children + * + * @name RootImpl#children + * @type Element[] + */ + parentRefs.bind(this, 'children'); + } + + e(RootImpl, ShapeImpl); + + + /** + * A label for an element + * + * @class + * @constructor + * + * @extends ShapeImpl + */ + function LabelImpl() { + ShapeImpl.call(this); + + /** + * The labeled element + * + * @name LabelImpl#labelTarget + * @type Element + */ + labelRefs.bind(this, 'labelTarget'); + } + + e(LabelImpl, ShapeImpl); + + + /** + * A connection between two elements + * + * @class + * @constructor + * + * @extends ElementImpl + */ + function ConnectionImpl() { + ElementImpl.call(this); + + /** + * The element this connection originates from + * + * @name ConnectionImpl#source + * @type Element + */ + outgoingRefs.bind(this, 'source'); + + /** + * The element this connection points to + * + * @name ConnectionImpl#target + * @type Element + */ + incomingRefs.bind(this, 'target'); + } + + e(ConnectionImpl, ElementImpl); + + + var types$6 = { + connection: ConnectionImpl, + shape: ShapeImpl, + label: LabelImpl, + root: RootImpl + }; + + /** + * Creates a root element. + * + * @overlord + * + * @example + * + * ```javascript + * import * as Model from 'diagram-js/lib/model'; + * + * const root = Model.create('root', { + * x: 100, + * y: 100, + * width: 100, + * height: 100 + * }); + * ``` + * + * @param {'root'} type + * @param {any} [attrs] + * + * @return {Root} + */ + + /** + * Creates a connection. + * + * @overlord + * + * @example + * + * ```javascript + * import * as Model from 'diagram-js/lib/model'; + * + * const connection = Model.create('connection', { + * waypoints: [ + * { x: 100, y: 100 }, + * { x: 200, y: 100 } + * ] + * }); + * ``` + * + * @param {'connection'} type + * @param {any} [attrs] + * + * @return {Connection} + */ + + /** + * Creates a shape. + * + * @overlord + * + * @example + * + * ```javascript + * import * as Model from 'diagram-js/lib/model'; + * + * const shape = Model.create('shape', { + * x: 100, + * y: 100, + * width: 100, + * height: 100 + * }); + * ``` + * + * @param {'shape'} type + * @param {any} [attrs] + * + * @return {Shape} + */ + + /** + * Creates a label. + * + * @example + * + * ```javascript + * import * as Model from 'diagram-js/lib/model'; + * + * const label = Model.create('label', { + * x: 100, + * y: 100, + * width: 100, + * height: 100, + * labelTarget: shape + * }); + * ``` + * + * @param {'label'} type + * @param {Object} [attrs] + * + * @return {Label} + */ + function create(type, attrs) { + var Type = types$6[type]; + if (!Type) { + throw new Error('unknown type: <' + type + '>'); + } + return assign$1(new Type(), attrs); + } + + /** + * @typedef {import('../model/Types').Element} Element + * @typedef {import('../model/Types').Connection} Connection + * @typedef {import('../model/Types').Label} Label + * @typedef {import('../model/Types').Root} Root + * @typedef {import('../model/Types').Shape} Shape + */ + + /** + * A factory for model elements. + * + * @template {Connection} [T=Connection] + * @template {Label} [U=Label] + * @template {Root} [V=Root] + * @template {Shape} [W=Shape] + */ + function ElementFactory() { + this._uid = 12; + } + + /** + * Create a root element. + * + * @param {Partial} [attrs] + * + * @return {V} The created root element. + */ + ElementFactory.prototype.createRoot = function(attrs) { + return this.create('root', attrs); + }; + + /** + * Create a label. + * + * @param {Partial